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, @typescript-eslint/no-empty-function 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) { // TODO: Use a better selector maybe? await page.locator('text="Add rule"').nth(1).click(); const attributeQueryBuilder = page.locator(".group-container").nth(1); 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) { await page.locator('text="Add rule"').nth(1).click(); const attributeQueryBuilder = page.locator(".group-container").nth(1); 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 = {}; 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 Selection=456&Test Field Multiple Selection=456&Test Field Multiple 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 Selection")).toBe("456"); expect(url.searchParams.getAll("Test Field Multiple 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"]'); await page.getByText(text).nth(1).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 }); // eslint-disable-next-line playwright/no-wait-for-timeout await page.waitForTimeout(2000); 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 }); // eslint-disable-next-line playwright/no-wait-for-timeout await page.waitForTimeout(2000); 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 Selection", "Number", "Phone", "Single 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 Selection" || fieldTypeLabel === "Single 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(); }