"Manage Booking Questions" - Add a comprehensive test (#7465)

* Add first test

* Add test for team event as well
pull/7562/head
Hariom Balhara 2023-03-07 23:10:47 +05:30 committed by GitHub
parent a2fd5ba2a2
commit 6f8ea490d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 471 additions and 13 deletions

View File

@ -220,6 +220,7 @@ function EventTypeSingleLayout({
<Tooltip content={t("preview")}> <Tooltip content={t("preview")}>
<Button <Button
color="secondary" color="secondary"
data-testid="preview-button"
target="_blank" target="_blank"
variant="icon" variant="icon"
href={permalink} href={permalink}

View File

@ -561,7 +561,12 @@ export default function Success(props: SuccessProps) {
<> <>
<div className="mt-9 font-medium">{label}</div> <div className="mt-9 font-medium">{label}</div>
<div className="col-span-2 mb-2 mt-9"> <div className="col-span-2 mb-2 mt-9">
<p className="break-words">{response.toString()}</p> <p
className="break-words"
data-testid="field-response"
data-fob-field={field.name}>
{response.toString()}
</p>
</div> </div>
</> </>
); );

View File

@ -44,9 +44,6 @@ const createTeamAndAddUser = async (
slug: `team-${workerInfo.workerIndex}-${Date.now()}`, slug: `team-${workerInfo.workerIndex}-${Date.now()}`,
}, },
}); });
if (!team) {
return;
}
const { role = MembershipRole.OWNER, id: userId } = user; const { role = MembershipRole.OWNER, id: userId } = user;
await prisma.membership.create({ await prisma.membership.create({
@ -54,8 +51,10 @@ const createTeamAndAddUser = async (
teamId: team.id, teamId: team.id,
userId, userId,
role: role, role: role,
accepted: true,
}, },
}); });
return team;
}; };
// creates a user fixture instance and stores the collection // creates a user fixture instance and stores the collection
@ -246,7 +245,29 @@ export const createUsersFixture = (page: Page, workerInfo: WorkerInfo) => {
include: userIncludes, include: userIncludes,
}); });
if (scenario.hasTeam) { if (scenario.hasTeam) {
await createTeamAndAddUser({ user: { id: user.id, role: "OWNER" } }, workerInfo); const team = await createTeamAndAddUser({ user: { id: user.id, role: "OWNER" } }, workerInfo);
await prisma.eventType.create({
data: {
team: {
connect: {
id: team.id,
},
},
users: {
connect: {
id: _user.id,
},
},
owner: {
connect: {
id: _user.id,
},
},
title: "Team Event - 30min",
slug: "team-event-30min",
length: 30,
},
});
} }
const userFixture = createUserFixture(user, store.page!); const userFixture = createUserFixture(user, store.page!);
store.users.push(userFixture); store.users.push(userFixture);

View File

@ -0,0 +1,409 @@
import type { Page, PlaywrightTestArgs } from "@playwright/test";
import { expect } from "@playwright/test";
import { WebhookTriggerEvents } from "@prisma/client";
import type { createUsersFixture } from "playwright/fixtures/users";
import { uuid } from "short-uuid";
import prisma from "@calcom/prisma";
import { test } from "./lib/fixtures";
import { createHttpServer, waitFor, selectFirstAvailableTimeSlotNextMonth } from "./lib/testUtils";
test.describe("Manage Booking Questions", () => {
test.afterEach(async ({ users }) => {
await users.deleteAll();
});
test.describe("For User EventType", () => {
test("Do a booking with a user added question and verify a few thing in b/w", async ({
page,
users,
context,
}, testInfo) => {
// Considering there are many steps in it, it would need more than default test timeout
test.setTimeout(testInfo.timeout * 3);
const user = await createAndLoginUserWithEventTypes({ users });
const webhookReceiver = await addWebhook(user);
await test.step("Go to EventType Page ", async () => {
const $eventTypes = page.locator("[data-testid=event-types] > li a");
const firstEventTypeElement = $eventTypes.first();
await firstEventTypeElement.click();
});
await test.step("Add Question and see that it's shown on Booking Page at appropriate position", async () => {
await addQuestionAndSave({
page,
question: {
name: "how_are_you",
type: "Name",
label: "How are you?",
placeholder: "I'm fine, thanks",
required: true,
},
});
await doOnFreshPreview(page, context, async (page) => {
const allFieldsLocator = await expectSystemFieldsToBeThere(page);
const userFieldLocator = allFieldsLocator.nth(5);
await expect(userFieldLocator.locator('[name="how_are_you"]')).toBeVisible();
// There are 2 labels right now. Will be one in future. The second one is hidden
expect(await userFieldLocator.locator("label").nth(0).innerText()).toBe("How are you?");
await expect(userFieldLocator.locator("input[type=text]")).toBeVisible();
});
});
await test.step("Hide Question and see that it's not shown on Booking Page", async () => {
await toggleQuestionAndSave({
name: "how_are_you",
page,
});
await doOnFreshPreview(page, context, async (page) => {
const formBuilderFieldLocator = page.locator('[data-fob-field-name="how_are_you"]');
await expect(formBuilderFieldLocator).toBeHidden();
});
});
await test.step("Show Question Again", async () => {
await toggleQuestionAndSave({
name: "how_are_you",
page,
});
});
await test.step('Try to book without providing "How are you?" response', async () => {
await doOnFreshPreview(page, context, async (page) => {
await bookTimeSlot({ page, name: "Booker", email: "booker@example.com" });
await expectErrorToBeThereFor({ page, name: "how_are_you" });
});
});
await test.step("Do a booking", async () => {
await doOnFreshPreview(page, context, async (page) => {
const formBuilderFieldLocator = page.locator('[data-fob-field-name="how_are_you"]');
await expect(formBuilderFieldLocator).toBeVisible();
expect(
await formBuilderFieldLocator.locator('[name="how_are_you"]').getAttribute("placeholder")
).toBe("I'm fine, thanks");
expect(await formBuilderFieldLocator.locator("label").nth(0).innerText()).toBe("How are you?");
await formBuilderFieldLocator.locator('[name="how_are_you"]').fill("I am great!");
await bookTimeSlot({ page, name: "Booker", email: "booker@example.com" });
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
expect(
await page.locator('[data-testid="field-response"][data-fob-field="how_are_you"]').innerText()
).toBe("I am great!");
await waitFor(() => {
expect(webhookReceiver.requestList.length).toBe(1);
});
const [request] = webhookReceiver.requestList;
const payload = (request.body as any).payload as any;
expect(payload.responses).toMatchObject({
name: "Booker",
email: "booker@example.com",
how_are_you: "I am great!",
});
expect(payload.location).toBe("integrations:daily");
expect(payload.attendees[0]).toMatchObject({
name: "Booker",
email: "booker@example.com",
});
expect(payload.userFieldsResponses).toMatchObject({
how_are_you: "I am great!",
});
});
});
});
});
test.describe("For Team EventType", () => {
test("Do a booking with a user added question and verify a few thing in b/w", async ({
page,
users,
context,
}, testInfo) => {
// Considering there are many steps in it, it would need more than default test timeout
test.setTimeout(testInfo.timeout * 3);
const user = await createAndLoginUserWithEventTypes({ users });
const webhookReceiver = await addWebhook(user);
await test.step("Go to First Team Event", async () => {
const $eventTypes = page.locator("[data-testid=event-types]").nth(1).locator("li a");
const firstEventTypeElement = $eventTypes.first();
await firstEventTypeElement.click();
});
await runTestStepsCommonForTeamAndUserEventType(page, context, webhookReceiver);
});
});
});
async function runTestStepsCommonForTeamAndUserEventType(
page: Page,
context: PlaywrightTestArgs["context"],
webhookReceiver: {
port: number;
close: () => import("http").Server;
requestList: (import("http").IncomingMessage & { body?: unknown })[];
url: string;
}
) {
await test.step("Add Question and see that it's shown on Booking Page at appropriate position", async () => {
await addQuestionAndSave({
page,
question: {
name: "how_are_you",
type: "Name",
label: "How are you?",
placeholder: "I'm fine, thanks",
required: true,
},
});
await doOnFreshPreview(page, context, async (page) => {
const allFieldsLocator = await expectSystemFieldsToBeThere(page);
const userFieldLocator = allFieldsLocator.nth(5);
await expect(userFieldLocator.locator('[name="how_are_you"]')).toBeVisible();
// There are 2 labels right now. Will be one in future. The second one is hidden
expect(await userFieldLocator.locator("label").nth(0).innerText()).toBe("How are you?");
await expect(userFieldLocator.locator("input[type=text]")).toBeVisible();
});
});
await test.step("Hide Question and see that it's not shown on Booking Page", async () => {
await toggleQuestionAndSave({
name: "how_are_you",
page,
});
await doOnFreshPreview(page, context, async (page) => {
const formBuilderFieldLocator = page.locator('[data-fob-field-name="how_are_you"]');
await expect(formBuilderFieldLocator).toBeHidden();
});
});
await test.step("Show Question Again", async () => {
await toggleQuestionAndSave({
name: "how_are_you",
page,
});
});
await test.step('Try to book without providing "How are you?" response', async () => {
await doOnFreshPreview(page, context, async (page) => {
await bookTimeSlot({ page, name: "Booker", email: "booker@example.com" });
await expectErrorToBeThereFor({ page, name: "how_are_you" });
});
});
await test.step("Do a booking", async () => {
await doOnFreshPreview(page, context, async (page) => {
const formBuilderFieldLocator = page.locator('[data-fob-field-name="how_are_you"]');
await expect(formBuilderFieldLocator).toBeVisible();
expect(await formBuilderFieldLocator.locator('[name="how_are_you"]').getAttribute("placeholder")).toBe(
"I'm fine, thanks"
);
expect(await formBuilderFieldLocator.locator("label").nth(0).innerText()).toBe("How are you?");
await formBuilderFieldLocator.locator('[name="how_are_you"]').fill("I am great!");
await bookTimeSlot({ page, name: "Booker", email: "booker@example.com" });
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
expect(
await page.locator('[data-testid="field-response"][data-fob-field="how_are_you"]').innerText()
).toBe("I am great!");
await waitFor(() => {
expect(webhookReceiver.requestList.length).toBe(1);
});
const [request] = webhookReceiver.requestList;
const payload = (request.body as any).payload as any;
expect(payload.responses).toMatchObject({
name: "Booker",
email: "booker@example.com",
how_are_you: "I am great!",
});
expect(payload.location).toBe("integrations:daily");
expect(payload.attendees[0]).toMatchObject({
name: "Booker",
email: "booker@example.com",
});
expect(payload.userFieldsResponses).toMatchObject({
how_are_you: "I am great!",
});
});
});
}
async function expectSystemFieldsToBeThere(page: Page) {
const allFieldsLocator = page.locator("[data-fob-field-name]:not(.hidden)");
const nameLocator = allFieldsLocator.nth(0);
const emailLocator = allFieldsLocator.nth(1);
// Location isn't rendered unless explicitly set which isn't the case here
// const locationLocator = allFieldsLocator.nth(2);
const additionalNotes = allFieldsLocator.nth(3);
const guestsLocator = allFieldsLocator.nth(4);
await expect(nameLocator.locator('[name="name"]')).toBeVisible();
await expect(emailLocator.locator('[name="email"]')).toBeVisible();
await expect(additionalNotes.locator('[name="notes"]')).toBeVisible();
await expect(guestsLocator.locator("button")).toBeVisible();
return allFieldsLocator;
}
//TODO: Add one question for each type and see they are rendering labels and only once and are showing appropriate native component
// Verify webhook is sent with the correct data, DB is correct (including metadata)
//TODO: Verify that prefill works
async function bookTimeSlot({ page, name, email }: { page: Page; name: string; email: string }) {
// --- fill form
await page.fill('[name="name"]', name);
await page.fill('[name="email"]', email);
await page.press('[name="email"]', "Enter");
}
/**
* 'option' starts from 1
*/
async function selectOption({
page,
selector,
optionText,
}: {
page: Page;
selector: { selector: string; nth: number };
optionText: string;
}) {
const locatorForSelect = page.locator(selector.selector).nth(selector.nth);
await locatorForSelect.click();
await locatorForSelect.locator(`text="${optionText}"`).click();
}
async function addQuestionAndSave({
page,
question,
}: {
page: Page;
question: {
name?: string;
type?: string;
label?: string;
placeholder?: string;
required?: boolean;
};
}) {
await page.click('[href$="tabName=advanced"]');
await page.click('[data-testid="add-field"]');
if (question.type !== undefined) {
await selectOption({
page,
selector: {
selector: "[id=test-field-type]",
nth: 0,
},
optionText: question.type,
});
}
if (question.name !== undefined) {
await page.fill('[name="name"]', question.name);
}
if (question.label !== undefined) {
await page.fill('[name="label"]', question.label);
}
if (question.placeholder !== undefined) {
await page.fill('[name="placeholder"]', question.placeholder);
}
if (question.required !== undefined) {
// await page.fill('[name="name"]', question.required);
}
await page.click('[data-testid="field-add-save"]');
await saveEventType(page);
}
async function expectErrorToBeThereFor({ page, name }: { page: Page; name: string }) {
await expect(page.locator(`[data-testid=error-message-${name}]`)).toHaveCount(1);
// TODO: We should either verify the error message or error code in the test so we know that the correct error is shown
// Checking for the error message isn't well maintainable as translation can change and we might want to verify in non english language as well.
}
/**
* Opens a fresh preview window and runs the callback on it giving it the preview tab's `page`
*/
async function doOnFreshPreview(
page: Page,
context: PlaywrightTestArgs["context"],
callback: (page: Page) => Promise<void>
) {
const previewTabPage = await openBookingFormInPreviewTab(context, page);
await callback(previewTabPage);
await previewTabPage.close();
}
async function toggleQuestionAndSave({ name, page }: { name: string; page: Page }) {
await page.locator(`[data-testid="field-${name}"]`).locator('[data-testid="toggle-field"]').click();
await saveEventType(page);
}
async function createAndLoginUserWithEventTypes({ users }: { users: ReturnType<typeof createUsersFixture> }) {
const user = await users.create(null, {
hasTeam: true,
});
await user.login();
return user;
}
async function openBookingFormInPreviewTab(context: PlaywrightTestArgs["context"], page: Page) {
const previewTabPromise = context.waitForEvent("page");
await page.locator('[data-testid="preview-button"]').click();
const previewTabPage = await previewTabPromise;
await previewTabPage.waitForLoadState();
await selectFirstAvailableTimeSlotNextMonth(previewTabPage);
await previewTabPage.waitForNavigation({
url: (url) => url.pathname.endsWith("/book"),
});
return previewTabPage;
}
async function saveEventType(page: Page) {
await page.locator("[data-testid=update-eventtype]").click();
}
async function addWebhook(user: Awaited<ReturnType<typeof createAndLoginUserWithEventTypes>>) {
const webhookReceiver = createHttpServer();
await prisma.webhook.create({
data: {
id: uuid(),
userId: user.id,
subscriberUrl: webhookReceiver.url,
eventTriggers: [
WebhookTriggerEvents.BOOKING_CREATED,
WebhookTriggerEvents.BOOKING_CANCELLED,
WebhookTriggerEvents.BOOKING_RESCHEDULED,
],
},
});
return webhookReceiver;
}

View File

@ -286,6 +286,7 @@ export const FormBuilder = function FormBuilder({
return ( return (
<li <li
key={index} key={index}
data-testid={`field-${field.name}`}
className="group relative flex items-center justify-between border-b p-4 last:border-b-0"> className="group relative flex items-center justify-between border-b p-4 last:border-b-0">
<button <button
type="button" type="button"
@ -322,6 +323,7 @@ export const FormBuilder = function FormBuilder({
{field.editable !== "user-readonly" && ( {field.editable !== "user-readonly" && (
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Switch <Switch
data-testid="toggle-field"
disabled={field.editable === "system"} disabled={field.editable === "system"}
tooltip={field.editable === "system" ? t("form_builder_system_field_cant_toggle") : ""} tooltip={field.editable === "system" ? t("form_builder_system_field_cant_toggle") : ""}
checked={!field.hidden} checked={!field.hidden}
@ -356,7 +358,12 @@ export const FormBuilder = function FormBuilder({
); );
})} })}
</ul> </ul>
<Button color="minimal" onClick={addField} className="mt-4" StartIcon={FiPlus}> <Button
color="minimal"
data-testid="add-field"
onClick={addField}
className="mt-4"
StartIcon={FiPlus}>
{addFieldLabel} {addFieldLabel}
</Button> </Button>
</div> </div>
@ -405,6 +412,7 @@ export const FormBuilder = function FormBuilder({
}}> }}>
<SelectField <SelectField
defaultValue={FieldTypes[3]} // "text" as defaultValue defaultValue={FieldTypes[3]} // "text" as defaultValue
id="test-field-type"
isDisabled={ isDisabled={
fieldForm.getValues("editable") === "system" || fieldForm.getValues("editable") === "system" ||
fieldForm.getValues("editable") === "system-but-optional" fieldForm.getValues("editable") === "system-but-optional"
@ -473,7 +481,9 @@ export const FormBuilder = function FormBuilder({
/> />
<DialogFooter> <DialogFooter>
<DialogClose color="secondary">Cancel</DialogClose> <DialogClose color="secondary">Cancel</DialogClose>
<Button type="submit">{isFieldEditMode ? t("save") : t("add")}</Button> <Button data-testid="field-add-save" type="submit">
{isFieldEditMode ? t("save") : t("add")}
</Button>
</DialogFooter> </DialogFooter>
</Form> </Form>
</div> </div>
@ -684,9 +694,7 @@ export const FormBuilderField = ({
const { t } = useLocale(); const { t } = useLocale();
const { control, formState } = useFormContext(); const { control, formState } = useFormContext();
return ( return (
<div <div data-fob-field-name={field.name} className={classNames(className, field.hidden ? "hidden" : "")}>
data-form-builder-field-name={field.name}
className={classNames(className, field.hidden ? "hidden" : "")}>
<Controller <Controller
control={control} control={control}
// Make it a variable // Make it a variable
@ -718,7 +726,7 @@ export const FormBuilderField = ({
} }
return ( return (
<div <div
data-field-name={field.name} data-testid={`error-message-${field.name}`}
className="mt-2 flex items-center text-sm text-red-700 "> className="mt-2 flex items-center text-sm text-red-700 ">
<FiInfo className="h-3 w-3 ltr:mr-2 rtl:ml-2" /> <FiInfo className="h-3 w-3 ltr:mr-2 rtl:ml-2" />
<p>{t(message)}</p> <p>{t(message)}</p>

View File

@ -8,7 +8,15 @@ dotEnv.config({ path: ".env" });
const outputDir = path.join(__dirname, "test-results"); const outputDir = path.join(__dirname, "test-results");
const DEFAULT_NAVIGATION_TIMEOUT = 15000; // Dev Server on local can be slow to start up and process requests. So, keep timeouts really high on local, so that tests run reliably locally
// So, if not in CI, keep the timers high, if the test is stuck somewhere and there is unnecessary wait developer can see in browser that it's stuck
const DEFAULT_NAVIGATION_TIMEOUT = process.env.CI ? 15000 : 50000;
const DEFAULT_EXPECT_TIMEOUT = process.env.CI ? 10000 : 50000;
// Test Timeout can hit due to slow expect, slow navigation.
// So, it should me much higher than sum of expect and navigation timeouts as there can be many async expects and navigations in a single test
const DEFAULT_TEST_TIMEOUT = process.env.CI ? 60000 : 120000;
const headless = !!process.env.CI || !!process.env.PLAYWRIGHT_HEADLESS; const headless = !!process.env.CI || !!process.env.PLAYWRIGHT_HEADLESS;
@ -36,7 +44,7 @@ const config: PlaywrightTestConfig = {
forbidOnly: !!process.env.CI, forbidOnly: !!process.env.CI,
retries: 2, retries: 2,
workers: os.cpus().length, workers: os.cpus().length,
timeout: 60_000, timeout: DEFAULT_TEST_TIMEOUT,
maxFailures: headless ? 10 : undefined, maxFailures: headless ? 10 : undefined,
fullyParallel: true, fullyParallel: true,
reporter: [ reporter: [
@ -58,6 +66,9 @@ const config: PlaywrightTestConfig = {
name: "@calcom/web", name: "@calcom/web",
testDir: "./apps/web/playwright", testDir: "./apps/web/playwright",
testMatch: /.*\.e2e\.tsx?/, testMatch: /.*\.e2e\.tsx?/,
expect: {
timeout: DEFAULT_EXPECT_TIMEOUT,
},
use: { use: {
...devices["Desktop Chrome"], ...devices["Desktop Chrome"],
/** If navigation takes more than this, then something's wrong, let's fail fast. */ /** If navigation takes more than this, then something's wrong, let's fail fast. */
@ -68,6 +79,9 @@ const config: PlaywrightTestConfig = {
name: "@calcom/app-store", name: "@calcom/app-store",
testDir: "./packages/app-store/", testDir: "./packages/app-store/",
testMatch: /.*\.e2e\.tsx?/, testMatch: /.*\.e2e\.tsx?/,
expect: {
timeout: DEFAULT_EXPECT_TIMEOUT,
},
use: { use: {
...devices["Desktop Chrome"], ...devices["Desktop Chrome"],
/** If navigation takes more than this, then something's wrong, let's fail fast. */ /** If navigation takes more than this, then something's wrong, let's fail fast. */