"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")}>
<Button
color="secondary"
data-testid="preview-button"
target="_blank"
variant="icon"
href={permalink}

View File

@ -561,7 +561,12 @@ export default function Success(props: SuccessProps) {
<>
<div className="mt-9 font-medium">{label}</div>
<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>
</>
);

View File

@ -44,9 +44,6 @@ const createTeamAndAddUser = async (
slug: `team-${workerInfo.workerIndex}-${Date.now()}`,
},
});
if (!team) {
return;
}
const { role = MembershipRole.OWNER, id: userId } = user;
await prisma.membership.create({
@ -54,8 +51,10 @@ const createTeamAndAddUser = async (
teamId: team.id,
userId,
role: role,
accepted: true,
},
});
return team;
};
// creates a user fixture instance and stores the collection
@ -246,7 +245,29 @@ export const createUsersFixture = (page: Page, workerInfo: WorkerInfo) => {
include: userIncludes,
});
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!);
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 (
<li
key={index}
data-testid={`field-${field.name}`}
className="group relative flex items-center justify-between border-b p-4 last:border-b-0">
<button
type="button"
@ -322,6 +323,7 @@ export const FormBuilder = function FormBuilder({
{field.editable !== "user-readonly" && (
<div className="flex items-center space-x-2">
<Switch
data-testid="toggle-field"
disabled={field.editable === "system"}
tooltip={field.editable === "system" ? t("form_builder_system_field_cant_toggle") : ""}
checked={!field.hidden}
@ -356,7 +358,12 @@ export const FormBuilder = function FormBuilder({
);
})}
</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}
</Button>
</div>
@ -405,6 +412,7 @@ export const FormBuilder = function FormBuilder({
}}>
<SelectField
defaultValue={FieldTypes[3]} // "text" as defaultValue
id="test-field-type"
isDisabled={
fieldForm.getValues("editable") === "system" ||
fieldForm.getValues("editable") === "system-but-optional"
@ -473,7 +481,9 @@ export const FormBuilder = function FormBuilder({
/>
<DialogFooter>
<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>
</Form>
</div>
@ -684,9 +694,7 @@ export const FormBuilderField = ({
const { t } = useLocale();
const { control, formState } = useFormContext();
return (
<div
data-form-builder-field-name={field.name}
className={classNames(className, field.hidden ? "hidden" : "")}>
<div data-fob-field-name={field.name} className={classNames(className, field.hidden ? "hidden" : "")}>
<Controller
control={control}
// Make it a variable
@ -718,7 +726,7 @@ export const FormBuilderField = ({
}
return (
<div
data-field-name={field.name}
data-testid={`error-message-${field.name}`}
className="mt-2 flex items-center text-sm text-red-700 ">
<FiInfo className="h-3 w-3 ltr:mr-2 rtl:ml-2" />
<p>{t(message)}</p>

View File

@ -8,7 +8,15 @@ dotEnv.config({ path: ".env" });
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;
@ -36,7 +44,7 @@ const config: PlaywrightTestConfig = {
forbidOnly: !!process.env.CI,
retries: 2,
workers: os.cpus().length,
timeout: 60_000,
timeout: DEFAULT_TEST_TIMEOUT,
maxFailures: headless ? 10 : undefined,
fullyParallel: true,
reporter: [
@ -58,6 +66,9 @@ const config: PlaywrightTestConfig = {
name: "@calcom/web",
testDir: "./apps/web/playwright",
testMatch: /.*\.e2e\.tsx?/,
expect: {
timeout: DEFAULT_EXPECT_TIMEOUT,
},
use: {
...devices["Desktop Chrome"],
/** 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",
testDir: "./packages/app-store/",
testMatch: /.*\.e2e\.tsx?/,
expect: {
timeout: DEFAULT_EXPECT_TIMEOUT,
},
use: {
...devices["Desktop Chrome"],
/** If navigation takes more than this, then something's wrong, let's fail fast. */