"Manage Booking Questions" - Add a comprehensive test (#7465)
* Add first test * Add test for team event as wellpull/7562/head
parent
a2fd5ba2a2
commit
6f8ea490d0
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -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. */
|
||||||
|
|
Loading…
Reference in New Issue