* feat: add no users found fallback action for routing forms - Add NoUsersFoundFallbackActionType enum with CustomPageMessage and ExternalRedirectUrl options - Add noUsersFoundFallbackAction field to zodNonRouterRoute schema - Add NoUsersFoundFallback UI component in RouteBuilder for configuring fallback action - Update handleResponse to return noUsersFoundFallbackAction when no team members match - Add i18n translation keys for new UI strings Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * refactor: change fallback to use same action options as main route - Replace noUsersFoundFallbackAction with fallbackAction that has same structure as main action - Add action dropdown and input fields to fallback attributes query builder section - Support Event redirect, External URL, and Custom page options in fallback - Remove separate NoUsersFoundFallback toggle component - Update handleResponse to return fallbackAction Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: ensure fallbackAction type is always defined in onChange handlers Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: add guards for route.fallbackAction in onChange handlers Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: only return fallbackAction when no users are found Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * refactor: restructure fallback section to mirror main route structure Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: ensure backwards compatibility for existing routes with fallbackAttributesQueryValue Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: initialize fallbackAction with main event type for backwards compatibility Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: include fallbackAction in getRoutesToSave to persist changes Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: use fallbackAction in getRoutedUrl when no team members found Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: use i18n for fallback label and add tests for fallback action Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: do not trigger fallback action when CRM contact owner is found Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: prioritize fallbackAction over fallbackAttributesQueryValue When a route has a fallbackAction configured, skip the fallbackAttributesQueryValue to ensure the fallbackAction is triggered when no team members are found. This maintains backwards compatibility by only using fallbackAttributesQueryValue when fallbackAction is not set. Also treats teamMemberIdsMatchingAttributeLogic being null (routing couldn't run, e.g., missing orgId) the same as an empty array for the purpose of triggering the fallbackAction. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: extract RouteActionSelector shared component for action selection UI Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: add i18n string for default custom page message Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * feat: add webhook trigger when routing form fallback route is hit - Add ROUTING_FORM_FALLBACK_HIT to WebhookTriggerEvents enum in Prisma schema - Add new trigger to routing-forms webhook triggers in constants - Add translation string for the new webhook trigger - Implement triggerFallbackWebhook function in formSubmissionUtils.ts - Call webhook trigger from handleResponse.ts when fallback action is used Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: remove webhook object from error log to avoid exposing secrets Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * revert: remove webhook trigger changes (to be moved to separate PR) Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: only show fallback query builder when fallbackAction is explicitly set The fallback attributes query builder was being shown for new routes when fallbackAction was not set, causing an extra 'Add rule' button to appear. This broke e2e tests that relied on the button index. Now the fallback query builder is only shown when fallbackAction is explicitly set to EventTypeRedirectUrl, which happens after the route is saved and reloaded (via auto-initialization in useRoutes). Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: use data-testid for attribute query builder selectors in e2e tests - Add data-testid='attributes-query-builder' to the main attributes query builder div - Update addAttributeRoutingRule and addAttributeRoutingRuleWithOperator to use the data-testid selector instead of nth() indices - This makes the tests more robust and independent of DOM structure changes Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: initialize fallbackAction for new routes in useCreateRoute hook - New routes created via useCreateRoute now initialize fallbackAction - This ensures new routes have a valid fallbackAction when saved - Matches the initialization logic for existing routes in useRoutes Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: initialize fallbackAction at save time when action is EventTypeRedirectUrl - When saving routes, if fallbackAction is undefined but action is EventTypeRedirectUrl, initialize fallbackAction from the main action - This handles the case where user creates a new route and changes action to an event type - Fixes e2e test failures where form save was failing due to undefined fallbackAction Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: initialize fallbackAction when main action changes to EventTypeRedirectUrl Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: only show fallback section when main action has valid event type selected Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: check for valid fallbackAction event type before preserving existing value Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * Give more structure to main and fallback routes * Update comment * Rename fallbackAttributesQueryBuilder to matchingMembersFallbackRoute * Display custom event redirect input * fix: address Cubic AI review feedback for routing form fallback - Simplify getFallbackAction logic to use checkedFallback flag instead of manually parsing attributesQueryValue (confidence 9/10) - Add i18n translation for 'And connect with specific team members' string using t('connect_with_specific_team_members') (confidence 9/10) Co-Authored-By: unknown <> * fix: update getFallbackAction to use checkedFallback and handle missing orgId case - Use checkedFallback flag as primary indicator for triggering fallback action - Also handle edge case where attribute routing was configured but couldn't run due to missing orgId (teamMemberIdsMatchingAttributeLogic is null) - Update test to mock checkedFallback: true when expecting fallback action Co-Authored-By: unknown <> * Address UI comment * fix: Use fallbackAttributesQueryValue when fallback action is event redirect Only use fallbackAttributesQueryValue when: 1. fallbackAction is EventTypeRedirectUrl (attribute routing applies) 2. fallbackAction is not set (backwards compatibility) Skip it for CustomPageMessage and ExternalRedirectUrl fallback actions since attribute routing doesn't apply to those action types. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: correct JSX syntax error in matchingMembersFallbackRoute Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * refactor: extract getCustomSlug helper to deduplicate custom event type slug logic Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: only show fallback attribute filtering for teams under orgs Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> * fix: show fallback action for all team forms, gate attribute filtering on isOrganization Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
1168 lines
42 KiB
TypeScript
1168 lines
42 KiB
TypeScript
import type { Locator, Page } from "@playwright/test";
|
|
import { expect } from "@playwright/test";
|
|
|
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
|
import { prisma } from "@calcom/prisma";
|
|
import { AttributeType, MembershipRole, SchedulingType } from "@calcom/prisma/enums";
|
|
import type { Fixtures } from "@calcom/web/playwright/lib/fixtures";
|
|
import { test } from "@calcom/web/playwright/lib/fixtures";
|
|
import { selectInteractions } from "@calcom/web/playwright/lib/pageObject";
|
|
import {
|
|
confirmBooking,
|
|
getEmailsReceivedByUser,
|
|
gotoRoutingLink,
|
|
selectFirstAvailableTimeSlotNextMonth,
|
|
testEmail,
|
|
testName,
|
|
} from "@calcom/web/playwright/lib/testUtils";
|
|
|
|
import {
|
|
addForm,
|
|
saveCurrentForm,
|
|
verifySelectOptions,
|
|
addOneFieldAndDescriptionAndSaveForm,
|
|
} from "./testUtils";
|
|
|
|
function todo(title: string) {
|
|
// eslint-disable-next-line playwright/no-skipped-test
|
|
test.skip(title, () => {});
|
|
}
|
|
|
|
const Identifiers = {
|
|
multi: "multi",
|
|
multiNewFormat: "multi-new-format",
|
|
select: "test-select",
|
|
selectNewFormat: "test-select-new-format",
|
|
};
|
|
|
|
async function enableContactOwnerOverride(page: Page) {
|
|
await page.click("text=Contact owner will be the Round Robin host if available");
|
|
}
|
|
|
|
async function selectFirstAttributeOption({ fromLocator }: { fromLocator: Locator }) {
|
|
await selectOptionUsingLocator({
|
|
locator: fromLocator,
|
|
option: 1,
|
|
});
|
|
}
|
|
|
|
async function selectFirstValueForAttributeValue({
|
|
fromLocator,
|
|
option,
|
|
}: {
|
|
fromLocator: Locator;
|
|
option: number;
|
|
}) {
|
|
await selectOptionUsingLocator({
|
|
locator: fromLocator,
|
|
option,
|
|
});
|
|
}
|
|
|
|
async function selectOperatorOption({ fromLocator, option }: { fromLocator: Locator; option: number }) {
|
|
await selectOptionUsingLocator({
|
|
locator: fromLocator,
|
|
option,
|
|
});
|
|
}
|
|
|
|
async function addAttributeRoutingRule(page: Page) {
|
|
// Use data-testid to target the main attributes query builder specifically
|
|
const attributeQueryBuilder = page.locator('[data-testid="attributes-query-builder"]');
|
|
await attributeQueryBuilder.locator('text="Add rule"').click();
|
|
const attributeSelectorFirstRule = attributeQueryBuilder.locator(".rule--field").nth(0);
|
|
await selectFirstAttributeOption({
|
|
fromLocator: attributeSelectorFirstRule,
|
|
});
|
|
|
|
const attributeValueSelector = attributeQueryBuilder.locator(".rule--value").nth(0);
|
|
await selectFirstValueForAttributeValue({
|
|
fromLocator: attributeValueSelector,
|
|
// Select 'Value of Field Short Text' option
|
|
option: 1,
|
|
});
|
|
}
|
|
|
|
async function addAttributeRoutingRuleWithOperator(page: Page, valueFrom: string, valueTo: string) {
|
|
// Use data-testid to target the main attributes query builder specifically
|
|
const attributeQueryBuilder = page.locator('[data-testid="attributes-query-builder"]');
|
|
await attributeQueryBuilder.locator('text="Add rule"').click();
|
|
const attributeSelectorFirstRule = attributeQueryBuilder.locator(".rule--field").nth(0);
|
|
await selectFirstAttributeOption({
|
|
fromLocator: attributeSelectorFirstRule,
|
|
});
|
|
|
|
const attributeOperatorSelector = attributeQueryBuilder.locator(".rule--operator").nth(0);
|
|
await selectOperatorOption({
|
|
fromLocator: attributeOperatorSelector,
|
|
option: 7,
|
|
});
|
|
|
|
const attributeValueSelector = attributeQueryBuilder.locator(".rule--value").nth(0);
|
|
await attributeValueSelector.locator("input").nth(0).fill(valueFrom);
|
|
await attributeValueSelector.locator("input").nth(1).fill(valueTo);
|
|
}
|
|
|
|
async function selectFirstEventRedirectOption(page: Page) {
|
|
await selectOption({
|
|
selector: {
|
|
selector: ".data-testid-eventTypeRedirectUrl-select",
|
|
nth: 0,
|
|
},
|
|
option: 2,
|
|
page,
|
|
});
|
|
}
|
|
|
|
test.describe("Routing Forms", () => {
|
|
test.describe("Zero State Routing Forms", () => {
|
|
test("should be able to add a new form and view it", async ({ page }) => {
|
|
const formId = await addForm(page);
|
|
|
|
await page.click('[data-testid="back-button"]');
|
|
|
|
await page.waitForSelector('[data-testid="routing-forms-list"]');
|
|
// Ensure that it's visible in forms list
|
|
await expect(page.locator('[data-testid="routing-forms-list"] > div')).toHaveCount(1);
|
|
|
|
await gotoRoutingLink({ page, formId });
|
|
await expect(page.locator("text=Test Form Name")).toBeVisible();
|
|
|
|
await page.goto(`apps/routing-forms/route-builder/${formId}`);
|
|
await page.waitForSelector('[data-testid="add-route-button"]');
|
|
await expect(page.locator('[data-testid="add-route-button"]')).toBeVisible();
|
|
});
|
|
|
|
test.skip("should be able to disable form", async ({ page }) => {
|
|
const formId = await addForm(page);
|
|
await page.click('[data-testid="back-button"]');
|
|
await disableForm(page);
|
|
await gotoRoutingLink({ page, formId });
|
|
await expect(page.locator('[data-testid="404-page"]')).toBeVisible();
|
|
});
|
|
|
|
test("recently added form appears first in the list", async ({ page }) => {
|
|
await addForm(page, { name: "Test Form 1" });
|
|
await page.goto(`apps/routing-forms/forms`);
|
|
await page.waitForSelector('[data-testid="routing-forms-list"]');
|
|
await expect(page.locator('[data-testid="routing-forms-list"] > div h1')).toHaveCount(1);
|
|
|
|
await addForm(page, { name: "Test Form 2" });
|
|
await page.goto(`apps/routing-forms/forms`);
|
|
await page.waitForSelector('[data-testid="routing-forms-list"]');
|
|
await expect(page.locator('[data-testid="routing-forms-list"] > div h1')).toHaveCount(2);
|
|
|
|
const firstForm = page.locator('[data-testid="routing-forms-list"] > div h1').first();
|
|
await expect(firstForm).toHaveText("Test Form 2");
|
|
});
|
|
|
|
test("should be able to edit a newly created form", async ({ page }) => {
|
|
const formId = await addForm(page);
|
|
const description = "Test Description";
|
|
|
|
const label = "Test Label";
|
|
|
|
const createdFields: Record<number, { label: string; typeIndex: number }> = {};
|
|
|
|
const { fieldTypesList: types, fields } = await addAllTypesOfFieldsAndSaveForm(formId, page, {
|
|
description,
|
|
label,
|
|
});
|
|
|
|
await page.click('[data-testid="settings-button"]');
|
|
await expect(page.locator('[data-testid="description"]')).toHaveValue(description);
|
|
await expect(page.locator('[data-testid="field"]')).toHaveCount(types.length);
|
|
await page.click('[data-testid="settings-slider-over-cancel"]');
|
|
|
|
fields.forEach((item, index) => {
|
|
createdFields[index] = { label: item.label, typeIndex: index };
|
|
});
|
|
|
|
await expectCurrentFormToHaveFields(page, createdFields, types);
|
|
|
|
await page.locator('[data-testid="toggle-group-item-route-builder"]').nth(1).click();
|
|
await page.waitForURL("/routing/route-builder/**");
|
|
|
|
await page.click('[data-testid="add-route-button"]');
|
|
await page.click('[data-testid="add-rule"]');
|
|
|
|
const options = Object.values(createdFields).map((item) => item.label);
|
|
await verifyFieldOptionsInRule(options, page);
|
|
});
|
|
|
|
// This feature is disable till it is fully supported and tested with Routing Form with Attributes.
|
|
test.describe.skip("F1<-F2 Relationship", () => {
|
|
test("Create relationship by adding F1 as route.Editing F1 should update F2", async ({ page }) => {
|
|
const form1Id = await addForm(page, { name: "F1" });
|
|
await page.goto(`/routing-forms/forms`);
|
|
const form2Id = await addForm(page, { name: "F2" });
|
|
|
|
await addOneFieldAndDescriptionAndSaveForm(form1Id, page, {
|
|
name: "F1",
|
|
description: "Form 1 Description",
|
|
field: {
|
|
label: "F1 Field1",
|
|
typeIndex: 1,
|
|
},
|
|
});
|
|
|
|
const { types } = await addOneFieldAndDescriptionAndSaveForm(form2Id, page, {
|
|
name: "F2",
|
|
description: "Form 2 Description",
|
|
field: {
|
|
label: "F2 Field1",
|
|
//TODO: Maybe choose some other type and choose type by it's name and not index
|
|
typeIndex: 1,
|
|
},
|
|
});
|
|
|
|
// Add F1 as Router to F2
|
|
await page.goto(`/routing-forms/route-builder/${form2Id}`);
|
|
// await addNewRoute(page, {
|
|
// // It should be F1. TODO: Verify that it's F1
|
|
// routeSelectNumber: 2,
|
|
// });
|
|
await addNewRoute(page);
|
|
await saveCurrentForm(page);
|
|
|
|
// Expect F1 fields to be available in F2
|
|
await page.goto(`/routing-forms/form-edit/${form2Id}`);
|
|
//FIXME: Figure out why this delay is required. Without it field count comes out to be 1 only
|
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
|
|
await expect(page.locator('[data-testid="field"]')).toHaveCount(2);
|
|
await expectCurrentFormToHaveFields(page, { 1: { label: "F1 Field1", typeIndex: 1 } }, types);
|
|
// Add 1 more field in F1
|
|
await addOneFieldAndDescriptionAndSaveForm(form1Id, page, {
|
|
name: "F1",
|
|
field: {
|
|
label: "F1 Field2",
|
|
typeIndex: 1,
|
|
},
|
|
});
|
|
|
|
await page.goto(`/routing-forms/form-edit/${form2Id}`);
|
|
//FIXME: Figure out why this delay is required. Without it field count comes out to be 1 only
|
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
await expect(page.locator('[data-testid="field"]')).toHaveCount(3);
|
|
await expectCurrentFormToHaveFields(page, { 2: { label: "F1 Field2", typeIndex: 1 } }, types);
|
|
});
|
|
todo("Create relationship by using duplicate with live connect");
|
|
});
|
|
|
|
test("should be able to submit a prefilled form with all types of fields", async ({ page }) => {
|
|
const formId = await addForm(page);
|
|
// Click desktop toggle group item
|
|
await page.locator('[data-testid="toggle-group-item-route-builder"]').nth(1).click();
|
|
await page.waitForURL("/routing/route-builder/**");
|
|
await addNewRoute(page);
|
|
await selectOption({
|
|
selector: {
|
|
selector: ".data-testid-select-routing-action",
|
|
nth: 0,
|
|
},
|
|
option: 2,
|
|
page,
|
|
});
|
|
await page.fill("[name=externalRedirectUrl]", `${WEBAPP_URL}/pro`);
|
|
await saveCurrentForm(page);
|
|
|
|
const { fields } = await addAllTypesOfFieldsAndSaveForm(formId, page, {
|
|
description: "Description",
|
|
label: "Test Field",
|
|
});
|
|
const queryString =
|
|
"firstfield=456&test-field-number=456&test-field-single-choice-selection=456&test-field-multiple-choice-selection=456&test-field-multiple-choice-selection=789&test-field-phone=456&test-field-email=456@example.com";
|
|
|
|
await gotoRoutingLink({ page, queryString });
|
|
|
|
await page.fill('[data-testid="form-field-test-field-long-text"]', "manual-fill");
|
|
|
|
await expect(page.locator('[data-testid="form-field-firstfield"]')).toHaveValue("456");
|
|
await expect(page.locator('[data-testid="form-field-test-field-number"]')).toHaveValue("456");
|
|
|
|
// TODO: Verify select and multiselect has prefilled values.
|
|
// expect(await page.locator(`[data-testid="form-field-test-field-select"]`).inputValue()).toBe("456");
|
|
// expect(await page.locator(`[data-testid="form-field-test-field-multiselect"]`).inputValue()).toBe("456");
|
|
|
|
await expect(page.locator('[data-testid="form-field-test-field-phone"]')).toHaveValue("456");
|
|
await expect(page.locator('[data-testid="form-field-test-field-email"]')).toHaveValue(
|
|
"456@example.com"
|
|
);
|
|
|
|
await page.click('button[type="submit"]');
|
|
await page.waitForURL((url) => {
|
|
return url.pathname.endsWith("/pro");
|
|
});
|
|
|
|
const url = new URL(page.url());
|
|
|
|
// Coming from the response filled by booker
|
|
expect(url.searchParams.get("firstfield")).toBe("456");
|
|
|
|
// All other params come from prefill URL
|
|
expect(url.searchParams.get("test-field-number")).toBe("456");
|
|
expect(url.searchParams.get("test-field-long-text")).toBe("manual-fill");
|
|
expect(url.searchParams.get("test-field-multiple-choice-selection")).toBe("456");
|
|
expect(url.searchParams.getAll("test-field-multiple-choice-selection")).toMatchObject(["456", "789"]);
|
|
expect(url.searchParams.get("test-field-phone")).toBe("456");
|
|
expect(url.searchParams.get("test-field-email")).toBe("456@example.com");
|
|
});
|
|
|
|
// TODO: How to install the app just once?
|
|
test.beforeEach(async ({ page, users }) => {
|
|
const user = await users.create(
|
|
{ username: "routing-forms" },
|
|
{
|
|
hasTeam: true,
|
|
}
|
|
);
|
|
await user.apiLogin();
|
|
});
|
|
|
|
test.afterEach(async ({ users }) => {
|
|
// This also delete forms on cascade
|
|
await users.deleteAll();
|
|
});
|
|
});
|
|
|
|
todo("should be able to duplicate form");
|
|
|
|
test.describe("Routing Form to Event Booking", () => {
|
|
test("should redirect to event page and successfully complete booking", async ({ page, users }) => {
|
|
const user = await users.create(
|
|
{ username: "routing-to-booking" },
|
|
{
|
|
hasTeam: true,
|
|
}
|
|
);
|
|
await user.apiLogin();
|
|
|
|
// Create a routing form
|
|
const formId = await addForm(page, { name: "Event Redirect Form" });
|
|
|
|
await addShortTextFieldAndSaveForm({ page, formId });
|
|
|
|
await page.locator('[data-testid="toggle-group-item-route-builder"]').nth(1).click();
|
|
await page.waitForURL("/routing/route-builder/**");
|
|
await addNewRoute(page);
|
|
await selectFirstEventRedirectOption(page);
|
|
|
|
await saveCurrentForm(page);
|
|
|
|
await gotoRoutingLink({ page, formId });
|
|
await page.fill('[data-testid="form-field-short-text"]', "Test Input");
|
|
await page.click('button[type="submit"]');
|
|
|
|
// Wait for redirect and verify presence of valid routingFormResponseId in URL
|
|
await page.waitForURL((url) => {
|
|
const routingFormResponseId = url.searchParams.get("cal.routingFormResponseId");
|
|
const parsedId = Number(routingFormResponseId);
|
|
return !!routingFormResponseId && !isNaN(parsedId);
|
|
});
|
|
|
|
await selectFirstAvailableTimeSlotNextMonth(page);
|
|
|
|
// Fill booking form
|
|
await page.fill('[name="name"]', testName);
|
|
await page.fill('[name="email"]', testEmail);
|
|
await page.fill('[name="notes"]', "Booked via routing form");
|
|
|
|
await confirmBooking(page);
|
|
|
|
// Verify booking was successful
|
|
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
|
await expect(page.locator(`[data-testid="attendee-name-${testName}"]`)).toBeVisible();
|
|
await expect(page.locator(`[data-testid="attendee-email-${testEmail}"]`)).toBeVisible();
|
|
});
|
|
|
|
test.afterEach(async ({ users }) => {
|
|
await users.deleteAll();
|
|
});
|
|
});
|
|
|
|
test.describe("Seeded Routing Form ", () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto(`/routing-forms/forms`);
|
|
});
|
|
test.afterEach(async ({ users }) => {
|
|
// This also delete forms on cascade
|
|
await users.deleteAll();
|
|
});
|
|
const createUserAndLogin = async function ({ users, page }: { users: Fixtures["users"]; page: Page }) {
|
|
const user = await users.create(
|
|
{ username: "routing-forms" },
|
|
{ seedRoutingForms: true, hasTeam: true }
|
|
);
|
|
await user.apiLogin();
|
|
return user;
|
|
};
|
|
|
|
test("Routing Link - Reporting and CSV Download ", async ({ page, users }) => {
|
|
const user = await createUserAndLogin({ users, page });
|
|
const routingForm = user.routingForms[0];
|
|
// Fill form when you are logged out
|
|
await users.logout();
|
|
|
|
await fillSeededForm(page, routingForm.id);
|
|
|
|
// Log back in to view form responses.
|
|
await user.apiLogin();
|
|
|
|
await page.goto(`apps/routing-forms/route-builder/${routingForm.id}`);
|
|
|
|
const downloadPromise = page.waitForEvent("download");
|
|
await page.locator('[data-testid="form-dropdown"]').nth(1).click();
|
|
await page.locator('[data-testid="download-responses"]').click();
|
|
const download = await downloadPromise;
|
|
const downloadStream = await download.createReadStream();
|
|
expect(download.suggestedFilename()).toEqual(`${routingForm.name}-${routingForm.id}.csv`);
|
|
const csv: string = await new Promise((resolve) => {
|
|
let body = "";
|
|
downloadStream?.on("data", (chunk) => {
|
|
body += chunk;
|
|
});
|
|
downloadStream?.on("end", () => {
|
|
resolve(body);
|
|
});
|
|
});
|
|
const csvRows = csv.trim().split("\n");
|
|
const csvHeaderRow = csvRows[0];
|
|
expect(csvHeaderRow).toEqual(
|
|
"Test field,Multi Select(with Legacy `selectText`),Multi Select,Legacy Select,Select,Submission Time"
|
|
);
|
|
|
|
const firstResponseCells = csvRows[1].split(",");
|
|
const secondResponseCells = csvRows[2].split(",");
|
|
const thirdResponseCells = csvRows[3].split(",");
|
|
|
|
expect(firstResponseCells.slice(0, -1).join(",")).toEqual(
|
|
"event-routing,Option-2,Option-2,Option-2,Option-2"
|
|
);
|
|
expect(new Date(firstResponseCells.at(-1) as string).getDay()).toEqual(new Date().getDay());
|
|
|
|
expect(secondResponseCells.slice(0, -1).join(",")).toEqual(
|
|
"external-redirect,Option-2,Option-2,Option-2,Option-2"
|
|
);
|
|
expect(new Date(secondResponseCells.at(-1) as string).getDay()).toEqual(new Date().getDay());
|
|
|
|
expect(thirdResponseCells.slice(0, -1).join(",")).toEqual(
|
|
"custom-page,Option-2,Option-2,Option-2,Option-2"
|
|
);
|
|
expect(new Date(thirdResponseCells.at(-1) as string).getDay()).toEqual(new Date().getDay());
|
|
});
|
|
|
|
test("Router URL should work", async ({ page, users }) => {
|
|
const user = await createUserAndLogin({ users, page });
|
|
const routingForm = user.routingForms[0];
|
|
|
|
// Router should be publicly accessible
|
|
await users.logout();
|
|
await page.goto(`/router?form=${routingForm.id}&Test field=event-routing`);
|
|
await page.waitForURL((url) => {
|
|
return url.pathname.endsWith("/pro/30min") && url.searchParams.get("Test field") === "event-routing";
|
|
});
|
|
|
|
await page.goto(`/router?form=${routingForm.id}&Test field=external-redirect`);
|
|
await page.waitForURL((url) => {
|
|
return url.pathname.endsWith("/pro") && url.searchParams.get("Test field") === "external-redirect";
|
|
});
|
|
|
|
await page.goto(`/router?form=${routingForm.id}&Test field=custom-page`);
|
|
await expect(page.locator("text=Custom Page Result")).toBeVisible();
|
|
|
|
await page.goto(`/router?form=${routingForm.id}&Test field=doesntmatter&${Identifiers.multi}=Option-2`);
|
|
await expect(page.locator("text=Multiselect(Legacy) chosen")).toBeVisible({ timeout: 10000 });
|
|
|
|
await page.goto(
|
|
`/router?form=${routingForm.id}&Test field=doesntmatter&${Identifiers.multiNewFormat}=d1302635-9f12-17b1-9153-c3a854649182`
|
|
);
|
|
await expect(page.locator("text=Multiselect chosen")).toBeVisible({ timeout: 10000 });
|
|
|
|
// Negative test - Ensures that the "No logic" situation where a Route is considered to be always passing isn't hitting
|
|
await page.goto(`/router?form=${routingForm.id}&Test field=kuchbhi`);
|
|
await page.waitForURL((url) => {
|
|
return url.searchParams.get("Test field") === "kuchbhi";
|
|
});
|
|
|
|
expect(page.url()).not.toContain("/pro/30min");
|
|
});
|
|
|
|
test("Routing Link should validate fields", async ({ page, users }) => {
|
|
const user = await createUserAndLogin({ users, page });
|
|
const routingForm = user.routingForms[0];
|
|
await gotoRoutingLink({ page, formId: routingForm.id });
|
|
await page.click('button[type="submit"]');
|
|
const firstInputMissingValue = await page.evaluate(() => {
|
|
return document.querySelectorAll("input")[0].validity.valueMissing;
|
|
});
|
|
expect(firstInputMissingValue).toBe(true);
|
|
await expect(page.locator('button[type="submit"][disabled]')).toHaveCount(0);
|
|
});
|
|
|
|
test("Test preview should return correct route", async ({ page, users }) => {
|
|
const user = await createUserAndLogin({ users, page });
|
|
const routingForm = user.routingForms[0];
|
|
await page.goto(`apps/routing-forms/form-edit/${routingForm.id}`);
|
|
|
|
//event redirect
|
|
await page.click('[data-testid="preview-button"]');
|
|
await page.fill('[data-testid="form-field-Test field"]', "event-routing");
|
|
await page.click('[data-testid="submit-button"]');
|
|
let routingType = await page.locator('[data-testid="chosen-route-title"]').innerText();
|
|
let route = await page.locator('[data-testid="test-routing-result"]').innerText();
|
|
expect(routingType).toBe("Event Redirect");
|
|
expect(route).toBe("pro/30min");
|
|
await page.click('[data-testid="close-results-button"]');
|
|
|
|
//custom page
|
|
await page.click('[data-testid="preview-button"]');
|
|
await page.fill('[data-testid="form-field-Test field"]', "custom-page");
|
|
await page.click('[data-testid="submit-button"]');
|
|
routingType = await page.locator('[data-testid="chosen-route-title"]').innerText();
|
|
route = await page.locator('[data-testid="test-routing-result"]').innerText();
|
|
expect(routingType).toBe("Custom Page");
|
|
expect(route).toBe("Custom Page Result");
|
|
await page.click('[data-testid="close-results-button"]');
|
|
|
|
//external redirect
|
|
await page.click('[data-testid="preview-button"]');
|
|
await page.fill('[data-testid="form-field-Test field"]', "external-redirect");
|
|
await page.click('[data-testid="submit-button"]');
|
|
routingType = await page.locator('[data-testid="chosen-route-title"]').innerText();
|
|
route = await page.locator('[data-testid="test-routing-result"]').innerText();
|
|
expect(routingType).toBe("External Redirect");
|
|
expect(route).toBe(`${WEBAPP_URL}/pro`);
|
|
await page.click('[data-testid="close-results-button"]');
|
|
|
|
// Multiselect(Legacy)
|
|
await page.click('[data-testid="preview-button"]');
|
|
await page.fill('[data-testid="form-field-Test field"]', "doesntmatter");
|
|
await page.click(`[data-testid="form-field-${Identifiers.multi}"]`); // Open dropdown
|
|
await page.click("text=Option-2"); // Select option
|
|
await page.click('[data-testid="submit-button"]');
|
|
routingType = await page.locator('[data-testid="chosen-route-title"]').innerText();
|
|
route = await page.locator('[data-testid="test-routing-result"]').innerText();
|
|
expect(routingType).toBe("Custom Page");
|
|
expect(route).toBe("Multiselect(Legacy) chosen");
|
|
await page.click('[data-testid="close-results-button"]');
|
|
|
|
// Multiselect
|
|
await page.click('[data-testid="preview-button"]');
|
|
await page.fill('[data-testid="form-field-Test field"]', "doesntmatter");
|
|
await page.click(`[data-testid="form-field-${Identifiers.multiNewFormat}"]`); // Open dropdown
|
|
await page.click("text=Option-2"); // Select option
|
|
await page.click('[data-testid="submit-button"]');
|
|
routingType = await page.locator('[data-testid="chosen-route-title"]').innerText();
|
|
route = await page.locator('[data-testid="test-routing-result"]').innerText();
|
|
expect(routingType).toBe("Custom Page");
|
|
expect(route).toBe("Multiselect chosen");
|
|
await page.click('[data-testid="close-results-button"]');
|
|
|
|
//fallback route
|
|
await page.click('[data-testid="preview-button"]');
|
|
await page.fill('[data-testid="form-field-Test field"]', "fallback");
|
|
await page.click('[data-testid="submit-button"]');
|
|
routingType = await page.locator('[data-testid="chosen-route-title"]').innerText();
|
|
route = await page.locator('[data-testid="test-routing-result"]').innerText();
|
|
expect(routingType).toBe("Custom Page");
|
|
expect(route).toBe("Fallback Message");
|
|
await page.click('[data-testid="close-results-button"]');
|
|
});
|
|
});
|
|
|
|
test.describe("Form with Attribute Routing - Team Form", () => {
|
|
test.beforeEach(async ({ page, users }) => {
|
|
const userFixture = await users.create(
|
|
{ username: "routing-forms" },
|
|
{
|
|
hasTeam: true,
|
|
isOrg: true,
|
|
hasSubteam: true,
|
|
schedulingType: SchedulingType.ROUND_ROBIN,
|
|
}
|
|
);
|
|
|
|
const orgMembership = await userFixture.getOrgMembership();
|
|
|
|
const createdAttribute = await prisma.attribute.create({
|
|
data: {
|
|
teamId: orgMembership.teamId,
|
|
type: AttributeType.SINGLE_SELECT,
|
|
name: "Company Size",
|
|
slug: `company-size-orgId-${orgMembership.teamId}`,
|
|
options: {
|
|
create: [
|
|
{
|
|
slug: "large",
|
|
value: "large",
|
|
},
|
|
{
|
|
slug: "medium",
|
|
value: "medium",
|
|
},
|
|
{
|
|
slug: "small",
|
|
value: "small",
|
|
},
|
|
],
|
|
},
|
|
},
|
|
include: {
|
|
options: true,
|
|
},
|
|
});
|
|
|
|
await prisma.attributeToUser.create({
|
|
data: {
|
|
member: {
|
|
connect: {
|
|
userId_teamId: {
|
|
userId: userFixture.id,
|
|
teamId: orgMembership.teamId,
|
|
},
|
|
},
|
|
},
|
|
attributeOption: {
|
|
connect: {
|
|
id: createdAttribute.options[0].id,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
await userFixture.apiLogin();
|
|
});
|
|
|
|
test.afterEach(async ({ users }) => {
|
|
// This also delete forms on cascade
|
|
await users.deleteAll();
|
|
});
|
|
|
|
test("should be able to add attribute routing to a newly created team form", async ({ page }) => {
|
|
const formId = await addForm(page, {
|
|
forTeam: true,
|
|
});
|
|
|
|
await addShortTextFieldAndSaveForm({
|
|
page,
|
|
formId,
|
|
});
|
|
|
|
// Click toggle group item
|
|
await page.locator('[data-testid="toggle-group-item-route-builder"]').nth(1).click();
|
|
await page.waitForURL("/routing/route-builder/**");
|
|
await page.click('[data-testid="add-route-button"]');
|
|
// This would select Round Robin event that we created above
|
|
await selectFirstEventRedirectOption(page);
|
|
await addAttributeRoutingRule(page);
|
|
await saveCurrentForm(page);
|
|
|
|
await (async function testPreviewWhereThereIsMatch() {
|
|
await page.click('[data-testid="preview-button"]');
|
|
await page.fill('[data-testid="form-field-short-text"]', "large");
|
|
await page.click('[data-testid="submit-button"]');
|
|
await page.waitForSelector("text=@example.com");
|
|
await page.click('[data-testid="close-results-button"]');
|
|
})();
|
|
|
|
await (async function testPreviewWhereThereIsNoMatch() {
|
|
await page.click('[data-testid="preview-button"]');
|
|
await page.fill('[data-testid="form-field-short-text"]', "medium");
|
|
await page.click('[data-testid="submit-button"]');
|
|
await expect(page.locator('[data-testid="attribute-logic-matched"]')).toHaveText("No");
|
|
await expect(page.locator('[data-testid="attribute-logic-fallback-matched"]')).toHaveText("Yes");
|
|
await page.click('[data-testid="close-results-button"]');
|
|
})();
|
|
});
|
|
});
|
|
|
|
test.describe("Form with Attribute Routing with Between operator - Not Matching - Team Form", () => {
|
|
test.beforeEach(async ({ page, users }) => {
|
|
const userFixture = await users.create(
|
|
{ username: "routing-forms" },
|
|
{
|
|
hasTeam: true,
|
|
isOrg: true,
|
|
hasSubteam: true,
|
|
schedulingType: SchedulingType.ROUND_ROBIN,
|
|
}
|
|
);
|
|
|
|
const orgMembership = await userFixture.getOrgMembership();
|
|
|
|
const createdAttribute = await prisma.attribute.create({
|
|
data: {
|
|
teamId: orgMembership.teamId,
|
|
type: AttributeType.NUMBER,
|
|
name: "Company Size",
|
|
slug: `company-size-orgId-${orgMembership.teamId}`,
|
|
options: {
|
|
create: [
|
|
{
|
|
slug: "10",
|
|
value: "10",
|
|
},
|
|
],
|
|
},
|
|
},
|
|
include: {
|
|
options: true,
|
|
},
|
|
});
|
|
|
|
await prisma.attributeToUser.create({
|
|
data: {
|
|
member: {
|
|
connect: {
|
|
userId_teamId: {
|
|
userId: userFixture.id,
|
|
teamId: orgMembership.teamId,
|
|
},
|
|
},
|
|
},
|
|
attributeOption: {
|
|
connect: {
|
|
id: createdAttribute.options[0].id,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
await userFixture.apiLogin();
|
|
});
|
|
|
|
test.afterEach(async ({ users }) => {
|
|
// This also delete forms on cascade
|
|
await users.deleteAll();
|
|
});
|
|
|
|
test("should not match any member if between operator values are input such that", async ({ page }) => {
|
|
await addForm(page, {
|
|
forTeam: true,
|
|
});
|
|
|
|
await page.locator('[data-testid="toggle-group-item-route-builder"]').nth(1).click();
|
|
await page.waitForURL("/routing/route-builder/**");
|
|
await addNewRoute(page);
|
|
// This would select Round Robin event that we created above
|
|
await selectFirstEventRedirectOption(page);
|
|
await addAttributeRoutingRuleWithOperator(page, "1", "5");
|
|
await saveCurrentForm(page);
|
|
|
|
// asserting there is no match as attribute value for the member is 10, while input is between 1 and 5
|
|
await (async function testPreviewWhereThereIsNoMatch() {
|
|
await page.click('[data-testid="preview-button"]');
|
|
await page.click('[data-testid="submit-button"]');
|
|
await expect(page.locator('[data-testid="attribute-logic-matched"]')).toHaveText("No");
|
|
await expect(page.locator('[data-testid="attribute-logic-fallback-matched"]')).toHaveText("Yes");
|
|
await page.click('[data-testid="close-results-button"]');
|
|
})();
|
|
});
|
|
});
|
|
|
|
test.describe("Form with Attribute Routing with Between operator - Matching - Team Form", () => {
|
|
test.beforeEach(async ({ page, users }) => {
|
|
const userFixture = await users.create(
|
|
{ username: "routing-forms" },
|
|
{
|
|
hasTeam: true,
|
|
isOrg: true,
|
|
hasSubteam: true,
|
|
schedulingType: SchedulingType.ROUND_ROBIN,
|
|
}
|
|
);
|
|
|
|
const orgMembership = await userFixture.getOrgMembership();
|
|
|
|
const createdAttribute = await prisma.attribute.create({
|
|
data: {
|
|
teamId: orgMembership.teamId,
|
|
type: AttributeType.NUMBER,
|
|
name: "Company Size",
|
|
slug: `company-size-orgId-${orgMembership.teamId}`,
|
|
options: {
|
|
create: [
|
|
{
|
|
slug: "3",
|
|
value: "3",
|
|
},
|
|
],
|
|
},
|
|
},
|
|
include: {
|
|
options: true,
|
|
},
|
|
});
|
|
|
|
await prisma.attributeToUser.create({
|
|
data: {
|
|
member: {
|
|
connect: {
|
|
userId_teamId: {
|
|
userId: userFixture.id,
|
|
teamId: orgMembership.teamId,
|
|
},
|
|
},
|
|
},
|
|
attributeOption: {
|
|
connect: {
|
|
id: createdAttribute.options[0].id,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
await userFixture.apiLogin();
|
|
});
|
|
|
|
test.afterEach(async ({ users }) => {
|
|
// This also delete forms on cascade
|
|
await users.deleteAll();
|
|
});
|
|
|
|
test("should match the member if between operator values are input such that", async ({ page }) => {
|
|
await addForm(page, {
|
|
forTeam: true,
|
|
});
|
|
|
|
await page.locator('[data-testid="toggle-group-item-route-builder"]').nth(1).click();
|
|
await page.waitForURL("/routing/route-builder/**");
|
|
await addNewRoute(page);
|
|
// This would select Round Robin event that we created above
|
|
await selectFirstEventRedirectOption(page);
|
|
await addAttributeRoutingRuleWithOperator(page, "1", "5");
|
|
await saveCurrentForm(page);
|
|
|
|
// asserting there is a match as attribute value for the member is 3 that is between 1 and 5
|
|
await (async function testPreviewWhereThereIsMatch() {
|
|
await page.click('[data-testid="preview-button"]');
|
|
await page.click('[data-testid="submit-button"]');
|
|
await page.waitForSelector("text=@example.com");
|
|
await page.click('[data-testid="close-results-button"]');
|
|
})();
|
|
});
|
|
});
|
|
|
|
test.describe("Team Routing Form", () => {
|
|
test.afterEach(async ({ users }) => {
|
|
await users.deleteAll();
|
|
});
|
|
|
|
const createUserAndTeamRoutingForm = async ({
|
|
users,
|
|
page,
|
|
}: {
|
|
users: Fixtures["users"];
|
|
page: Page;
|
|
}) => {
|
|
const owner = await users.create(
|
|
{ username: "routing-forms" },
|
|
{
|
|
hasTeam: true,
|
|
isOrg: true,
|
|
hasSubteam: true,
|
|
}
|
|
);
|
|
const { team } = await owner.getFirstTeamMembership();
|
|
await owner.apiLogin();
|
|
const formId = await addForm(page, {
|
|
forTeam: true,
|
|
});
|
|
await addShortTextFieldAndSaveForm({
|
|
page,
|
|
formId,
|
|
});
|
|
await page.locator('[data-testid="toggle-group-item-route-builder"]').nth(1).click();
|
|
await page.waitForURL("/routing/route-builder/**");
|
|
await addNewRoute(page);
|
|
await selectOption({
|
|
selector: {
|
|
selector: ".data-testid-select-routing-action",
|
|
nth: 0,
|
|
},
|
|
option: 2,
|
|
page,
|
|
});
|
|
await page.fill("[name=externalRedirectUrl]", `${WEBAPP_URL}/pro`);
|
|
await saveCurrentForm(page);
|
|
return {
|
|
formId,
|
|
teamId: team.id,
|
|
owner,
|
|
};
|
|
};
|
|
|
|
const selectSendMailToAllMembers = async ({ page, formId }: { page: Page; formId: string }) => {
|
|
await page.goto(`apps/routing-forms/form-edit/${formId}`);
|
|
await page.getByTestId("settings-button").click();
|
|
await page.getByTestId("assign-all-team-members-toggle").click();
|
|
await page.getByTestId("settings-slider-over-done").click();
|
|
|
|
await saveCurrentForm(page);
|
|
};
|
|
|
|
const selectSendMailToUser = async ({
|
|
page,
|
|
formId,
|
|
text,
|
|
}: {
|
|
page: Page;
|
|
formId: string;
|
|
text: string;
|
|
}) => {
|
|
await page.goto(`apps/routing-forms/form-edit/${formId}`);
|
|
await page.getByTestId("settings-button").click();
|
|
await page.click('[data-testid="routing-form-select-members"]');
|
|
// Wait for the react-select menu to appear and click the option within it
|
|
// This is more robust than using .nth(1) which assumes specific DOM structure
|
|
const menuListbox = page.locator('[id$="-listbox"]');
|
|
await menuListbox.waitFor({ state: "visible" });
|
|
await menuListbox.getByText(text).click();
|
|
await page.getByTestId("settings-slider-over-done").click();
|
|
await saveCurrentForm(page);
|
|
};
|
|
|
|
const addNewUserToTeam = async ({ users, teamId }: { users: Fixtures["users"]; teamId: number }) => {
|
|
const newUser = await users.create();
|
|
await prisma.membership.create({
|
|
data: {
|
|
teamId: teamId,
|
|
userId: newUser.id,
|
|
role: MembershipRole.MEMBER,
|
|
accepted: true,
|
|
},
|
|
});
|
|
return newUser;
|
|
};
|
|
|
|
const goToRoutingLinkAndSubmit = async ({ page, formId }: { page: Page; formId: string }) => {
|
|
await gotoRoutingLink({ page, formId });
|
|
await page.fill('[data-testid="form-field-short-text"]', "test");
|
|
await page.click('button[type="submit"]');
|
|
await page.waitForURL((url) => {
|
|
return url.pathname.endsWith("/pro");
|
|
});
|
|
};
|
|
|
|
test("newly added team member gets mail updates, when `Add all team members, including future members` switch is selected", async ({
|
|
page,
|
|
emails,
|
|
users,
|
|
}) => {
|
|
const { formId, teamId, owner } = await createUserAndTeamRoutingForm({ users, page });
|
|
await selectSendMailToAllMembers({ page, formId });
|
|
const newUser = await addNewUserToTeam({ users, teamId });
|
|
await goToRoutingLinkAndSubmit({ page, formId });
|
|
|
|
const receivedEmailsOwner = await getEmailsReceivedByUser({ emails, userEmail: owner.email });
|
|
expect(receivedEmailsOwner?.total).toBe(1);
|
|
const receivedEmailsNewUser = await getEmailsReceivedByUser({ emails, userEmail: newUser.email });
|
|
expect(receivedEmailsNewUser?.total).toBe(1);
|
|
});
|
|
|
|
test("newly added team member Not gets mail updates, when `Add all team members, including future members` switch is Not selected", async ({
|
|
page,
|
|
emails,
|
|
users,
|
|
}) => {
|
|
const { formId, teamId, owner } = await createUserAndTeamRoutingForm({ users, page });
|
|
await selectSendMailToUser({ page, formId, text: owner.email as string });
|
|
const newUser = await addNewUserToTeam({ users, teamId });
|
|
await goToRoutingLinkAndSubmit({ page, formId });
|
|
|
|
const receivedEmailsOwner = await getEmailsReceivedByUser({ emails, userEmail: owner.email });
|
|
expect(receivedEmailsOwner?.total).toBe(1);
|
|
const receivedEmailsNewUser = await getEmailsReceivedByUser({ emails, userEmail: newUser.email });
|
|
expect(receivedEmailsNewUser?.total).toBe(0);
|
|
});
|
|
});
|
|
});
|
|
|
|
async function disableForm(page: Page) {
|
|
await page.click('[data-testid="toggle-form-switch"]');
|
|
await page.waitForSelector(".data-testid-toast-success");
|
|
}
|
|
|
|
async function expectCurrentFormToHaveFields(
|
|
page: Page,
|
|
fields: {
|
|
[key: number]: { label: string; typeIndex: number };
|
|
},
|
|
types: string[]
|
|
) {
|
|
for (const [index, field] of Object.entries(fields)) {
|
|
expect(await page.inputValue(`[data-testid="fields.${index}.label"]`)).toBe(field.label);
|
|
expect(await page.locator(".data-testid-field-type").nth(+index).locator("div").nth(1).innerText()).toBe(
|
|
types[field.typeIndex]
|
|
);
|
|
}
|
|
}
|
|
|
|
async function fillSeededForm(page: Page, routingFormId: string) {
|
|
await gotoRoutingLink({ page, formId: routingFormId });
|
|
|
|
await (async function firstResponse() {
|
|
await page.fill('[data-testid="form-field-Test field"]', "event-routing");
|
|
await fillAllOptionsBasedFields();
|
|
page.click('button[type="submit"]');
|
|
|
|
await page.waitForURL((url) => {
|
|
return url.pathname.endsWith("/pro/30min");
|
|
});
|
|
})();
|
|
|
|
await gotoRoutingLink({ page, formId: routingFormId });
|
|
await (async function secondResponse() {
|
|
await page.fill('[data-testid="form-field-Test field"]', "external-redirect");
|
|
await fillAllOptionsBasedFields();
|
|
page.click('button[type="submit"]');
|
|
await page.waitForURL((url) => {
|
|
return url.pathname.endsWith("/pro");
|
|
});
|
|
})();
|
|
|
|
await gotoRoutingLink({ page, formId: routingFormId });
|
|
await (async function thirdResponse() {
|
|
await page.fill('[data-testid="form-field-Test field"]', "custom-page");
|
|
await fillAllOptionsBasedFields();
|
|
page.click('button[type="submit"]');
|
|
await expect(page.locator("text=Custom Page Result")).toBeVisible();
|
|
})();
|
|
|
|
async function fillAllOptionsBasedFields() {
|
|
await selectInteractions.chooseOption({
|
|
selector: `[data-testid="form-field-${Identifiers.multiNewFormat}"]`,
|
|
optionText: "Option-2",
|
|
page,
|
|
});
|
|
await selectInteractions.chooseOption({
|
|
selector: `[data-testid="form-field-${Identifiers.multi}"]`,
|
|
optionText: "Option-2",
|
|
page,
|
|
});
|
|
await selectInteractions.chooseOption({
|
|
selector: `[data-testid="form-field-${Identifiers.selectNewFormat}"]`,
|
|
optionText: "Option-2",
|
|
page,
|
|
});
|
|
await selectInteractions.chooseOption({
|
|
selector: `[data-testid="form-field-${Identifiers.select}"]`,
|
|
optionText: "Option-2",
|
|
page,
|
|
});
|
|
}
|
|
}
|
|
|
|
async function addAllTypesOfFieldsAndSaveForm(
|
|
formId: string,
|
|
page: Page,
|
|
form: { description: string; label: string }
|
|
) {
|
|
await page.goto(`apps/routing-forms/form-edit/${formId}`);
|
|
await page.click('[data-testid="add-field"]');
|
|
|
|
const { optionsInUi: fieldTypesList } = await verifySelectOptions(
|
|
{ selector: ".data-testid-field-type", nth: 0 },
|
|
["Email", "Long text", "Multiple choice selection", "Number", "Phone", "Single-choice selection", "Short text"],
|
|
page
|
|
);
|
|
|
|
const fields = [];
|
|
for (let index = 0; index < fieldTypesList.length; index++) {
|
|
const fieldTypeLabel = fieldTypesList[index];
|
|
const nth = index;
|
|
const label = `${form.label} ${fieldTypeLabel}`;
|
|
let identifier = "";
|
|
|
|
if (index !== 0) {
|
|
identifier = label;
|
|
// Click on the field type dropdown.
|
|
await page.locator(".data-testid-field-type").nth(nth).click();
|
|
// Click on the dropdown option.
|
|
await page.locator(`[data-testid^="select-option-"]`).filter({ hasText: fieldTypeLabel }).click();
|
|
} else {
|
|
// Set the identifier manually for the first field to test out a case when identifier isn't computed from label automatically
|
|
// First field type is by default selected. So, no need to choose from dropdown
|
|
identifier = "firstField";
|
|
}
|
|
|
|
if (fieldTypeLabel === "Multiple choice selection" || fieldTypeLabel === "Single-choice selection") {
|
|
await page.fill(`[data-testid="fields.${nth}.options.0-input"]`, "123");
|
|
await page.fill(`[data-testid="fields.${nth}.options.1-input"]`, "456");
|
|
await page.fill(`[data-testid="fields.${nth}.options.2-input"]`, "789");
|
|
await page.fill(`[data-testid="fields.${nth}.options.3-input"]`, "10-11-12");
|
|
}
|
|
|
|
await page.fill(`[name="fields.${nth}.label"]`, label);
|
|
|
|
if (identifier !== label) {
|
|
await page.fill(`[name="fields.${nth}.identifier"]`, identifier);
|
|
}
|
|
|
|
if (index !== fieldTypesList.length - 1) {
|
|
await page.click('[data-testid="add-field"]');
|
|
}
|
|
fields.push({ identifier: identifier, label, type: fieldTypeLabel });
|
|
}
|
|
|
|
await page.locator('[data-testid="settings-button"]').scrollIntoViewIfNeeded();
|
|
await page.click('[data-testid="settings-button"]');
|
|
await page.fill('[data-testid="description"]', form.description);
|
|
await page.click('[data-testid="settings-slider-over-done"]');
|
|
await saveCurrentForm(page);
|
|
return {
|
|
fieldTypesList,
|
|
fields,
|
|
};
|
|
}
|
|
|
|
async function addShortTextFieldAndSaveForm({ page, formId }: { page: Page; formId: string }) {
|
|
await page.goto(`apps/routing-forms/form-edit/${formId}`);
|
|
await page.click('[data-testid="add-field"]');
|
|
await page.locator(".data-testid-field-type").nth(0).click();
|
|
await page.fill(`[name="fields.0.label"]`, "Short Text");
|
|
await page.fill(`[name="fields.0.identifier"]`, "short-text");
|
|
await saveCurrentForm(page);
|
|
}
|
|
|
|
async function selectOption({
|
|
page,
|
|
selector,
|
|
option,
|
|
}: {
|
|
page: Page;
|
|
selector: { selector: string; nth: number };
|
|
/**
|
|
* Index of option to select. Starts from 1
|
|
*/
|
|
option: number;
|
|
}) {
|
|
const locatorForSelect = page.locator(selector.selector).nth(selector.nth);
|
|
await locatorForSelect.click();
|
|
await locatorForSelect
|
|
.locator('[id*="react-select-"][aria-disabled]')
|
|
.nth(option - 1)
|
|
.click();
|
|
}
|
|
|
|
export async function selectOptionUsingLocator({ locator, option }: { locator: Locator; option: number }) {
|
|
await locator.click();
|
|
await locator
|
|
.locator('[id*="react-select-"][aria-disabled]')
|
|
.nth(option - 1)
|
|
.click();
|
|
}
|
|
|
|
async function verifyFieldOptionsInRule(options: string[], page: Page) {
|
|
await verifySelectOptions(
|
|
{
|
|
selector: ".rule-container .data-testid-field-select",
|
|
nth: 0,
|
|
},
|
|
options,
|
|
page
|
|
);
|
|
}
|
|
|
|
async function addNewRoute(page: Page) {
|
|
await page.locator('[data-testid="add-route-button"]').click();
|
|
}
|