fix: flaky e2e patterns (#16696)

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
This commit is contained in:
Omar LópezandAnik Dhabal Babu authored and GitHub committed 2024-09-20 22:47:21 +05:30
1 parent c1c4b1278b
commit f52497f181
29 files changed
+262 -193

No files matched your search

@@ -1,4 +1,3 @@
import { useRouter } from "next/navigation";
import { useCallback, useState } from "react";
import { sdkActionManager } from "@calcom/embed-core/embed-iframe";
@@ -7,6 +6,8 @@ import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calco
import type { RecurringEvent } from "@calcom/types/Calendar";
import { Button, Icon, TextArea } from "@calcom/ui";
import { useRefreshData } from "@lib/hooks/useRefreshData";
type Props = {
booking: {
title?: string;
@@ -38,7 +39,7 @@ type Props = {
export default function CancelBooking(props: Props) {
const [cancellationReason, setCancellationReason] = useState<string>("");
const { t } = useLocale();
const router = useRouter();
const refreshData = useRefreshData();
const { booking, allRemainingBookings, seatReferenceUid, bookingCancelledEventProps, currentUserEmail } =
props;
const [loading, setLoading] = useState(false);
@@ -120,7 +121,7 @@ export default function CancelBooking(props: Props) {
...bookingCancelledEventProps,
booking: bookingWithCancellationReason,
});
router.refresh();
refreshData();
} else {
setLoading(false);
setError(
+12
View File
@@ -0,0 +1,12 @@
import { usePathname, useSearchParams } from "next/navigation";
import { useMemo } from "react";
export function useAsPath() {
const pathname = usePathname();
const searchParams = useSearchParams();
const asPath = useMemo(
() => `${pathname}${searchParams ? `?${searchParams.toString()}` : ""}`,
[pathname, searchParams]
);
return asPath;
}
+13
View File
@@ -0,0 +1,13 @@
import { useRouter } from "next/navigation";
import { useAsPath } from "./useAsPath";
/** @see https://www.joshwcomeau.com/nextjs/refreshing-server-side-props/ */
export function useRefreshData() {
const router = useRouter();
const asPath = useAsPath();
const refreshData = () => {
router.replace(asPath);
};
return refreshData;
}
+3 -3
View File
@@ -33,9 +33,9 @@ test.describe("Availablity", () => {
await page.getByTestId("add-override-submit-btn").click();
await page.getByTestId("dialog-rejection").click();
await expect(page.locator('[data-testid="date-overrides-list"] > li')).toHaveCount(1);
await page.locator('[form="availability-form"][type="submit"]').click();
const response = await page.waitForResponse("**/api/trpc/availability/schedule.update?batch=1");
const json = await response.json();
await submitAndWaitForResponse(page, "/api/trpc/availability/schedule.update?batch=1", {
action: () => page.locator('[form="availability-form"][type="submit"]').click(),
});
const nextMonth = dayjs().add(1, "month").startOf("month");
const troubleshooterURL = `/availability/troubleshoot?date=${nextMonth.format("YYYY-MM-DD")}`;
await page.goto(troubleshooterURL);
+2 -2
View File
@@ -13,7 +13,7 @@ import { entries } from "@calcom/prisma/zod-utils";
import type { IntervalLimit } from "@calcom/types/Calendar";
import { test } from "./lib/fixtures";
import { bookTimeSlot, createUserWithLimits } from "./lib/testUtils";
import { bookTimeSlot, confirmReschedule, createUserWithLimits } from "./lib/testUtils";
test.describe.configure({ mode: "parallel" });
test.afterEach(async ({ users }) => {
@@ -163,7 +163,7 @@ test.describe("Booking limits", () => {
await expect(page.locator('[name="name"]')).toBeDisabled();
await expect(page.locator('[name="email"]')).toBeDisabled();
await page.locator('[data-testid="confirm-reschedule-button"]').click();
await confirmReschedule(page);
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
+2 -1
View File
@@ -12,6 +12,7 @@ import {
bookFirstEvent,
bookOptinEvent,
bookTimeSlot,
confirmReschedule,
selectFirstAvailableTimeSlotNextMonth,
testEmail,
testName,
@@ -128,7 +129,7 @@ testBothFutureAndLegacyRoutes.describe("pro user", () => {
});
await selectFirstAvailableTimeSlotNextMonth(page);
await page.locator('[data-testid="confirm-reschedule-button"]').click();
await confirmReschedule(page);
await page.waitForURL((url) => {
return url.pathname.startsWith("/booking");
});
+7 -5
View File
@@ -7,9 +7,11 @@ import { BookingStatus } from "@calcom/prisma/enums";
import { test } from "./lib/fixtures";
import {
confirmReschedule,
createNewSeatedEventType,
selectFirstAvailableTimeSlotNextMonth,
createUserWithSeatedEventAndAttendees,
selectFirstAvailableTimeSlotNextMonth,
submitAndWaitForResponse,
} from "./lib/testUtils";
test.describe.configure({ mode: "parallel" });
@@ -160,9 +162,9 @@ test.describe("Reschedule for booking with seats", () => {
`/booking/${references[0].referenceUid}?cancel=true&seatReferenceUid=${references[0].referenceUid}`
);
await page.locator('[data-testid="confirm_cancel"]').click();
await page.waitForResponse((res) => res.url().includes("api/cancel") && res.status() === 200);
await submitAndWaitForResponse(page, "/api/cancel", {
action: () => page.locator('[data-testid="confirm_cancel"]').click(),
});
const oldBooking = await prisma.booking.findFirst({
where: { uid: booking.uid },
@@ -396,7 +398,7 @@ test.describe("Reschedule for booking with seats", () => {
await expect(reasonElement).toBeVisible();
// expect to be redirected to reschedule page
await page.locator('[data-testid="confirm-reschedule-button"]').click();
await confirmReschedule(page);
// should wait for URL but that path starts with booking/
await page.waitForURL(/\/booking\/.*/);
+4 -6
View File
@@ -1,6 +1,7 @@
import { expect } from "@playwright/test";
import { test } from "./lib/fixtures";
import { submitAndWaitForResponse } from "./lib/testUtils";
test.afterEach(({ users }) => users.deleteAll());
@@ -18,11 +19,8 @@ test.describe("Change Password Test", () => {
const $newPasswordField = page.locator('[name="newPassword"]');
$newPasswordField.fill(`${pro.username}Aa1111`);
await page.locator("text=Update").click();
const toast = await page.waitForSelector('[data-testid="toast-success"]');
expect(toast).toBeTruthy();
await submitAndWaitForResponse(page, "/api/trpc/auth/changePassword?batch=1", {
action: () => page.locator("text=Update").click(),
});
});
});
+31 -45
View File
@@ -7,7 +7,7 @@ import { MembershipRole } from "@calcom/prisma/enums";
import { moveUserToOrg } from "@lib/orgMigration";
import { test } from "./lib/fixtures";
import { IS_STRIPE_ENABLED } from "./lib/testUtils";
import { IS_STRIPE_ENABLED, submitAndWaitForResponse } from "./lib/testUtils";
test.describe.configure({ mode: "parallel" });
@@ -15,59 +15,45 @@ const IS_SELF_HOSTED = !(
new URL(WEBAPP_URL).hostname.endsWith(".cal.dev") || !!new URL(WEBAPP_URL).hostname.endsWith(".cal.com")
);
const TESTING_USERNAMES = [
{
username: "demousernamex",
description: "",
},
{
username: "demo.username",
description: " to include periods(or dots)",
},
];
test.describe("Change username on settings", () => {
test.afterEach(async ({ users }) => {
await users.deleteAll();
});
test("User can change username", async ({ page, users, prisma }) => {
const user = await users.create();
TESTING_USERNAMES.forEach((item) => {
test(`User can change username${item.description}`, async ({ page, users, prisma }) => {
const user = await users.create();
await user.apiLogin();
// Try to go homepage
await page.goto("/settings/my-account/profile");
// Change username from normal to normal
const usernameInput = page.locator("[data-testid=username-input]");
await user.apiLogin();
// Try to go homepage
await page.goto("/settings/my-account/profile");
// Change username from normal to normal
const usernameInput = page.locator("[data-testid=username-input]");
await usernameInput.fill(item.username);
await page.click("[data-testid=update-username-btn]");
await submitAndWaitForResponse(page, "/api/trpc/viewer/updateProfile?batch=1", {
action: () => page.click("[data-testid=save-username]"),
});
await usernameInput.fill("demousernamex");
await page.click("[data-testid=update-username-btn]");
await Promise.all([
page.click("[data-testid=save-username]"),
page.getByTestId("toast-success").waitFor(),
]);
const newUpdatedUser = await prisma.user.findUniqueOrThrow({
where: {
id: user.id,
},
});
const newUpdatedUser = await prisma.user.findUniqueOrThrow({
where: {
id: user.id,
},
expect(newUpdatedUser.username).toBe(item.username);
});
expect(newUpdatedUser.username).toBe("demousernamex");
});
test("User can change username to include periods(or dots)", async ({ page, users, prisma }) => {
const user = await users.create();
await user.apiLogin();
// Try to go homepage
await page.goto("/settings/my-account/profile");
// Change username from normal to normal
const usernameInput = page.locator("[data-testid=username-input]");
// User can change username to include dots(or periods)
await usernameInput.fill("demo.username");
await page.click("[data-testid=update-username-btn]");
await Promise.all([
page.click("[data-testid=save-username]"),
page.getByTestId("toast-success").waitFor(),
]);
const updatedUser = await prisma.user.findUniqueOrThrow({
where: {
id: user.id,
},
});
expect(updatedUser.username).toBe("demo.username");
});
test("User can update to PREMIUM username", async ({ page, users }, testInfo) => {
@@ -5,6 +5,7 @@ import { MembershipRole } from "@calcom/prisma/client";
import { test } from "./lib/fixtures";
import {
bookTimeSlot,
confirmReschedule,
doOnOrgDomain,
selectFirstAvailableTimeSlotNextMonth,
selectSecondAvailableTimeSlotNextMonth,
@@ -42,7 +43,7 @@ test("dynamic booking", async ({ page, users }) => {
await selectSecondAvailableTimeSlotNextMonth(page);
// No need to fill fields since they should be already filled
await page.locator('[data-testid="confirm-reschedule-button"]').click();
await confirmReschedule(page);
await page.waitForURL((url) => {
return url.pathname.startsWith("/booking");
});
+10 -7
View File
@@ -13,6 +13,7 @@ import {
gotoFirstEventType,
saveEventType,
selectFirstAvailableTimeSlotNextMonth,
submitAndWaitForResponse,
} from "./lib/testUtils";
test.describe.configure({ mode: "parallel" });
@@ -130,9 +131,9 @@ testBothFutureAndLegacyRoutes.describe("Event Types tests", () => {
await page.waitForURL((url) => {
return !!url.pathname.match(/\/event-types\/.+/);
});
await page.locator("[data-testid=update-eventtype]").click();
const toast = await page.waitForSelector('[data-testid="toast-success"]');
expect(toast).toBeTruthy();
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/update?batch=1", {
action: () => page.locator("[data-testid=update-eventtype]").click(),
});
});
test("can add multiple organizer address", async ({ page }) => {
@@ -153,7 +154,9 @@ testBothFutureAndLegacyRoutes.describe("Event Types tests", () => {
await page.locator("[data-testid=add-location]").click();
await fillLocation(page, locationData[2], 2);
await page.locator("[data-testid=update-eventtype]").click();
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/update?batch=1", {
action: () => page.locator("[data-testid=update-eventtype]").click(),
});
await page.goto("/event-types");
@@ -224,7 +227,6 @@ testBothFutureAndLegacyRoutes.describe("Event Types tests", () => {
await page.locator(`text="Cal Video (Global)"`).click();
await saveEventType(page);
await page.getByTestId("toast-success").waitFor();
await gotoBookingPage(page);
await selectFirstAvailableTimeSlotNextMonth(page);
@@ -246,7 +248,6 @@ testBothFutureAndLegacyRoutes.describe("Event Types tests", () => {
await page.locator(`input[name="${locationInputName}"]`).fill(testUrl);
await saveEventType(page);
await page.getByTestId("toast-success").waitFor();
await gotoBookingPage(page);
await selectFirstAvailableTimeSlotNextMonth(page);
@@ -298,7 +299,9 @@ testBothFutureAndLegacyRoutes.describe("Event Types tests", () => {
const locationAddress = "New Delhi";
await fillLocation(page, locationAddress, 0, false);
await page.locator("[data-testid=update-eventtype]").click();
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/update?batch=1", {
action: () => page.locator("[data-testid=update-eventtype]").click(),
});
await page.goto("/event-types");
+5 -3
View File
@@ -1,4 +1,4 @@
import type { Page } from "@playwright/test";
import type { Page, WorkerInfo } from "@playwright/test";
import type { Booking, Prisma } from "@prisma/client";
import short from "short-uuid";
import { v5 as uuidv5 } from "uuid";
@@ -14,7 +14,7 @@ type BookingFixture = ReturnType<typeof createBookingFixture>;
const dayjs = (...args: Parameters<typeof _dayjs>) => _dayjs(...args).tz("Europe/London");
// creates a user fixture instance and stores the collection
export const createBookingsFixture = (page: Page) => {
export const createBookingsFixture = (page: Page, workerInfo: WorkerInfo) => {
const store = { bookings: [], page } as { bookings: BookingFixture[]; page: typeof page };
return {
create: async (
@@ -40,7 +40,9 @@ export const createBookingsFixture = (page: Page) => {
endDateParam?: Date
) => {
const startDate = startDateParam || dayjs().add(1, "day").toDate();
const seed = `${username}:${dayjs(startDate).utc().format()}:${new Date().getTime()}`;
const seed = `${username}:${dayjs(startDate).utc().format()}:${new Date().getTime()}:${
workerInfo.workerIndex
}`;
const uid = translator.fromUUID(uuidv5(seed, uuidv5.URL));
const booking = await prisma.booking.create({
data: {
@@ -2,7 +2,7 @@ import { expect, type Page } from "@playwright/test";
import type { MembershipRole } from "@calcom/prisma/enums";
import { localize } from "../lib/testUtils";
import { localize, submitAndWaitForResponse } from "../lib/testUtils";
import type { createUsersFixture } from "./users";
export const scheduleSuccessfullyText = "This meeting is scheduled";
@@ -40,9 +40,9 @@ export function createBookingPageFixture(page: Page) {
await page.goto("/event-types");
},
updateEventType: async () => {
await page.getByTestId("update-eventtype").click();
const toast = await page.waitForSelector('[data-testid="toast-success"]');
expect(toast).toBeTruthy();
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/update?batch=1", {
action: () => page.locator("[data-testid=update-eventtype]").click(),
});
},
previewEventType: async () => {
const eventtypePromise = page.waitForEvent("popup");
+3 -2
View File
@@ -1014,10 +1014,11 @@ export async function login(
await emailLocator.fill(user.email ?? `${user.username}@example.com`);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await passwordLocator.fill(user.password ?? user.username!);
await signInLocator.click();
// waiting for specific login request to resolve
await page.waitForResponse(/\/api\/auth\/callback\/credentials/);
const responsePromise = page.waitForResponse(/\/api\/auth\/callback\/credentials/);
await signInLocator.click();
await responsePromise;
}
export async function apiLogin(
+8 -5
View File
@@ -1,7 +1,11 @@
import { expect } from "@playwright/test";
import { test } from "./lib/fixtures";
import { bookTimeSlot, selectFirstAvailableTimeSlotNextMonth } from "./lib/testUtils";
import {
bookTimeSlot,
selectFirstAvailableTimeSlotNextMonth,
submitAndWaitForResponse,
} from "./lib/testUtils";
test.describe.configure({ mode: "parallel" });
@@ -55,10 +59,9 @@ test.describe("hash my url", () => {
await page.getByTestId("vertical-tab-event_setup_tab_title").click();
await page.locator("[data-testid=event-title]").first().fill("somethingrandom");
await page.locator("[data-testid=event-slug]").first().fill("somethingrandom");
const submitPromise = page.waitForResponse("/api/trpc/eventTypes/update?batch=1");
await page.locator("[data-testid=update-eventtype]").click();
const response = await submitPromise;
expect(response.status()).toBe(200);
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/update?batch=1", {
action: () => page.locator("[data-testid=update-eventtype]").click(),
});
await page.locator(".primary-navigation >> text=Advanced").click();
const $url2 = await page.locator('//*[@data-testid="generated-hash-url"]').inputValue();
expect($url2.includes("somethingrandom")).toBeTruthy();
@@ -7,7 +7,7 @@ import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { test, todo } from "./lib/fixtures";
import type { Fixtures } from "./lib/fixtures";
import { IS_STRIPE_ENABLED, selectFirstAvailableTimeSlotNextMonth } from "./lib/testUtils";
import { confirmReschedule, IS_STRIPE_ENABLED, selectFirstAvailableTimeSlotNextMonth } from "./lib/testUtils";
test.describe.configure({ mode: "parallel" });
test.afterEach(({ users }) => users.deleteAll());
@@ -438,7 +438,7 @@ test.describe("Stripe integration with the new app install flow skip false", ()
await selectFirstAvailableTimeSlotNextMonth(page);
await page.click('[data-testid="confirm-reschedule-button"]');
await confirmReschedule(page);
await expect(page.getByText("This meeting is scheduled")).toBeVisible();
});
+5 -2
View File
@@ -8,6 +8,7 @@ import { v4 as uuidv4 } from "uuid";
import { prisma } from "@calcom/prisma";
import { test, todo } from "./lib/fixtures";
import { submitAndWaitForResponse } from "./lib/testUtils";
declare let global: {
E2E_EMAILS?: ({ text: string } | Record<string, unknown>)[];
@@ -130,8 +131,10 @@ async function bookEvent(page: Page, calLink: string) {
// --- fill form
await page.fill('[name="name"]', "Integration User");
await page.fill('[name="email"]', "integration-user@example.com");
await page.press('[name="email"]', "Enter");
const response = await page.waitForResponse("**/api/book/event");
const response = await submitAndWaitForResponse(page, "**/api/book/event", {
action: () => page.press('[name="email"]', "Enter"),
});
const responseObj = await response.json();
const bookingId = responseObj.uid;
await page.waitForSelector("[data-testid=success-page]");
+2 -2
View File
@@ -68,8 +68,8 @@ export const test = base.extend<Fixtures>({
const usersFixture = createUsersFixture(page, emails, workerInfo);
await use(usersFixture);
},
bookings: async ({ page }, use) => {
const bookingsFixture = createBookingsFixture(page);
bookings: async ({ page }, use, workerInfo) => {
const bookingsFixture = createBookingsFixture(page, workerInfo);
await use(bookingsFixture);
},
payments: async ({ page }, use) => {
+17 -5
View File
@@ -218,9 +218,9 @@ export async function installAppleCalendar(page: Page) {
}
export async function getInviteLink(page: Page) {
const response = await page.waitForResponse("/api/trpc/teams/createInvite?batch=1");
expect(response.status()).toBe(200);
const json = await response.json();
const json = await submitAndWaitForResponse(page, "/api/trpc/teams/createInvite?batch=1", {
action: () => page.locator(`[data-testid="copy-invite-link-button"]`).click(),
});
return json[0].result.data.json.inviteLink as string;
}
@@ -428,10 +428,15 @@ export async function gotoBookingPage(page: Page) {
}
export async function saveEventType(page: Page) {
await page.locator("[data-testid=update-eventtype]").click();
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/update?batch=1", {
action: () => page.locator("[data-testid=update-eventtype]").click(),
});
}
/** Fastest way so far to test for saving changes and form submissions */
/**
* Fastest way so far to test for saving changes and form submissions
* @see https://playwright.dev/docs/api/class-page#page-wait-for-response
*/
export async function submitAndWaitForResponse(
page: Page,
url: string,
@@ -441,4 +446,11 @@ export async function submitAndWaitForResponse(
await action();
const response = await submitPromise;
expect(response.status()).toBe(expectedStatusCode);
return response.json();
}
export async function confirmReschedule(page: Page, url = "/api/book/event") {
await submitAndWaitForResponse(page, url, {
action: () => page.locator('[data-testid="confirm-reschedule-button"]').click(),
});
}
@@ -10,8 +10,12 @@ import { WebhookTriggerEvents } from "@calcom/prisma/enums";
import type { CalendarEvent } from "@calcom/types/Calendar";
import { test } from "./lib/fixtures";
import { createNewEventType } from "./lib/testUtils";
import { createHttpServer, selectFirstAvailableTimeSlotNextMonth } from "./lib/testUtils";
import {
createHttpServer,
createNewEventType,
selectFirstAvailableTimeSlotNextMonth,
submitAndWaitForResponse,
} from "./lib/testUtils";
function getLabelLocator(field: Locator) {
// There are 2 labels right now. Will be one in future. The second one is hidden
@@ -655,7 +659,7 @@ async function rescheduleFromTheLinkOnPage({ page }: { page: Page }) {
await page.locator('[data-testid="reschedule-link"]').click();
await page.waitForLoadState();
await selectFirstAvailableTimeSlotNextMonth(page);
await page.click('[data-testid="confirm-reschedule-button"]');
await page.locator('[data-testid="confirm-reschedule-button"]').click();
}
async function openBookingFormInPreviewTab(context: PlaywrightTestArgs["context"], page: Page) {
@@ -668,7 +672,9 @@ async function openBookingFormInPreviewTab(context: PlaywrightTestArgs["context"
}
async function saveEventType(page: Page) {
await page.locator("[data-testid=update-eventtype]").click();
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/update?batch=1", {
action: () => page.locator("[data-testid=update-eventtype]").click(),
});
}
async function addWebhook(
@@ -12,6 +12,7 @@ import {
bookTimeSlot,
doOnOrgDomain,
selectFirstAvailableTimeSlotNextMonth,
submitAndWaitForResponse,
testName,
} from "../lib/testUtils";
import { expectExistingUserToBeInvitedToOrganization } from "../team/expects";
@@ -660,11 +661,9 @@ const markPhoneNumberAsRequiredAndEmailAsOptional = async (page: Page, eventId:
const emailRequiredFiled = await page.locator('[data-testid="field-required"]');
await emailRequiredFiled.locator("> :nth-child(2)").click();
await page.getByTestId("field-add-save").click();
const submitPromise = page.waitForResponse("/api/trpc/eventTypes/update?batch=1");
await page.locator("[data-testid=update-eventtype]").click();
const response = await submitPromise;
expect(response.status()).toBe(200);
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/update?batch=1", {
action: () => page.locator("[data-testid=update-eventtype]").click(),
});
};
const markPhoneNumberAsRequiredField = async (page: Page, eventId: number) => {
@@ -675,9 +674,7 @@ const markPhoneNumberAsRequiredField = async (page: Page, eventId: number) => {
const phoneRequiredFiled = await page.locator('[data-testid="field-required"]');
await phoneRequiredFiled.locator("> :nth-child(1)").click();
await page.getByTestId("field-add-save").click();
const submitPromise = page.waitForResponse("/api/trpc/eventTypes/update?batch=1");
await page.locator("[data-testid=update-eventtype]").click();
const response = await submitPromise;
expect(response.status()).toBe(200);
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/update?batch=1", {
action: () => page.locator("[data-testid=update-eventtype]").click(),
});
};
@@ -1,6 +1,8 @@
import type { Page } from "@playwright/test";
import type { createUsersFixture } from "playwright/fixtures/users";
import { submitAndWaitForResponse } from "../../lib/testUtils";
export const inviteUserToOrganization = async ({
page,
organizationId,
@@ -35,19 +37,21 @@ export const inviteExistingUserToOrganization = async ({
usersFixture: ReturnType<typeof createUsersFixture>;
}) => {
await page.goto("/settings/organizations/members");
await inviteAnEmail(page, user.email);
await page.waitForSelector('[data-testid="toast-success"]');
return { invitedUserEmail: user.email };
};
export async function acceptTeamOrOrgInvite(page: Page) {
await page.goto("/settings/teams");
await page.click('[data-testid^="accept-invitation"]');
await submitAndWaitForResponse(page, "/api/trpc/teams/acceptOrLeave?batch=1", {
action: () => page.click('[data-testid^="accept-invitation"]'),
});
}
async function inviteAnEmail(page: Page, invitedUserEmail: string) {
await page.locator('button:text("Add")').click();
await page.locator('input[name="inviteUser"]').fill(invitedUserEmail);
await page.locator('button:text("Send invite")').click();
await submitAndWaitForResponse(page, "/api/trpc/teams/inviteMember?batch=1", {
action: () => page.locator('button:text("Send invite")').click(),
});
}
@@ -558,7 +558,6 @@ function assertInviteLink(inviteLink: string | null | undefined): asserts invite
async function copyInviteLink(page: Page) {
await page.locator('button:text("Add")').click();
await page.locator(`[data-testid="copy-invite-link-button"]`).click();
const inviteLink = await getInviteLink(page);
return inviteLink;
}
+4 -8
View File
@@ -7,7 +7,7 @@ import type { PrismaClient } from "@calcom/prisma";
import type { createEmailsFixture } from "./fixtures/emails";
import { test } from "./lib/fixtures";
import { getEmailsReceivedByUser } from "./lib/testUtils";
import { getEmailsReceivedByUser, submitAndWaitForResponse } from "./lib/testUtils";
import { expectInvitationEmailToBeReceived } from "./team/expects";
test.describe.configure({ mode: "parallel" });
@@ -62,13 +62,9 @@ test.describe("Update Profile", () => {
await page.getByTestId("password").fill(user?.username ?? "Nameless User");
await page.getByTestId("profile-update-email-submit-button").click();
const toastLocator = await page.getByTestId("toast-success");
const textContent = await toastLocator.textContent();
expect(textContent).toContain(email);
await submitAndWaitForResponse(page, "/api/trpc/viewer/updateProfile?batch=1", {
action: () => page.getByTestId("profile-update-email-submit-button").click(),
});
// Instead of dealing with emails in e2e lets just get the token and navigate to it
const verificationToken = await prisma.verificationToken.findFirst({
+59 -54
View File
@@ -6,13 +6,16 @@ import { MembershipRole } from "@calcom/prisma/client";
import { BookingStatus } from "@calcom/prisma/enums";
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
import { apiLogin } from "./fixtures/users";
import { test } from "./lib/fixtures";
import {
selectFirstAvailableTimeSlotNextMonth,
bookTimeSlot,
confirmReschedule,
doOnOrgDomain,
goToUrlWithErrorHandling,
IS_STRIPE_ENABLED,
selectFirstAvailableTimeSlotNextMonth,
submitAndWaitForResponse,
} from "./lib/testUtils";
test.describe.configure({ mode: "parallel" });
@@ -93,9 +96,7 @@ test.describe("Reschedule Tests", async () => {
await expect(page.locator('[name="name"]')).toBeDisabled();
await expect(page.locator('[name="email"]')).toBeDisabled();
await page.locator('[data-testid="confirm-reschedule-button"]').click();
await confirmReschedule(page);
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
const newBooking = await prisma.booking.findFirstOrThrow({ where: { fromReschedule: booking.uid } });
@@ -147,7 +148,7 @@ test.describe("Reschedule Tests", async () => {
await selectFirstAvailableTimeSlotNextMonth(page);
await page.locator('[data-testid="confirm-reschedule-button"]').click();
await confirmReschedule(page);
await page.waitForURL((url) => {
return url.pathname.indexOf("/payment") > -1;
@@ -177,7 +178,7 @@ test.describe("Reschedule Tests", async () => {
await selectFirstAvailableTimeSlotNextMonth(page);
await page.locator('[data-testid="confirm-reschedule-button"]').click();
await confirmReschedule(page);
await expect(page).toHaveURL(/.*booking/);
});
@@ -194,13 +195,13 @@ test.describe("Reschedule Tests", async () => {
await selectFirstAvailableTimeSlotNextMonth(page);
await page.locator('[data-testid="confirm-reschedule-button"]').click();
await confirmReschedule(page);
await expect(page).toHaveURL(/.*booking/);
const newBooking = await prisma.booking.findFirst({ where: { fromReschedule: booking?.uid } });
const newBooking = await prisma.booking.findFirstOrThrow({ where: { fromReschedule: booking?.uid } });
expect(newBooking).not.toBeNull();
expect(newBooking?.status).toBe(BookingStatus.PENDING);
expect(newBooking.status).toBe(BookingStatus.PENDING);
});
test("Opt in event should be ACCEPTED when rescheduled by OWNER", async ({ page, users, bookings }) => {
@@ -216,13 +217,13 @@ test.describe("Reschedule Tests", async () => {
await selectFirstAvailableTimeSlotNextMonth(page);
await page.locator('[data-testid="confirm-reschedule-button"]').click();
await confirmReschedule(page);
await expect(page).toHaveURL(/.*booking/);
const newBooking = await prisma.booking.findFirst({ where: { fromReschedule: booking?.uid } });
const newBooking = await prisma.booking.findFirstOrThrow({ where: { fromReschedule: booking?.uid } });
expect(newBooking).not.toBeNull();
expect(newBooking?.status).toBe(BookingStatus.ACCEPTED);
expect(newBooking.status).toBe(BookingStatus.ACCEPTED);
});
test("Attendee should be able to reschedule a booking", async ({ page, users, bookings }) => {
@@ -235,13 +236,13 @@ test.describe("Reschedule Tests", async () => {
await selectFirstAvailableTimeSlotNextMonth(page);
await page.locator('[data-testid="confirm-reschedule-button"]').click();
await confirmReschedule(page);
await expect(page).toHaveURL(/.*booking/);
const newBooking = await prisma.booking.findFirst({ where: { fromReschedule: booking?.uid } });
const newBooking = await prisma.booking.findFirstOrThrow({ where: { fromReschedule: booking?.uid } });
expect(newBooking).not.toBeNull();
expect(newBooking?.status).toBe(BookingStatus.ACCEPTED);
expect(newBooking.status).toBe(BookingStatus.ACCEPTED);
});
test("Should be able to book slot that overlaps with original rescheduled booking", async ({
@@ -270,26 +271,31 @@ test.describe("Reschedule Tests", async () => {
await selectFirstAvailableTimeSlotNextMonth(page);
await page.locator('[data-testid="confirm-reschedule-button"]').click();
await confirmReschedule(page);
await expect(page).toHaveURL(/.*booking/);
});
test("Should load Valid Cal video url after rescheduling Opt in events", async ({
page,
users,
bookings,
browser,
}) => {
// eslint-disable-next-line playwright/no-skipped-test
test.skip(!process.env.DAILY_API_KEY, "DAILY_API_KEY is needed for this test");
const user = await users.create();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const eventType = user.eventTypes.find((e) => e.slug === "opt-in")!;
const confirmBooking = async (bookingId: number) => {
await user.apiLogin();
await page.goto("/bookings/upcoming");
const elem = await page.locator(`[data-bookingid="${bookingId}"][data-testid="confirm"]`);
await elem.click();
await page.getByTestId("toast-success").waitFor();
await user.logout();
const authedContext = await browser.newContext();
const authedPage = await authedContext.newPage();
const userSnapshot = await user.self();
await apiLogin({ ...userSnapshot, password: userSnapshot.username }, authedPage);
await authedPage.goto("/bookings/upcoming");
await submitAndWaitForResponse(authedPage, "/api/trpc/bookings/confirm?batch=1", {
action: () => authedPage.locator(`[data-bookingid="${bookingId}"][data-testid="confirm"]`).click(),
});
await authedContext.close();
};
await page.goto(`/${user.username}/${eventType.slug}`);
@@ -301,38 +307,34 @@ test.describe("Reschedule Tests", async () => {
const pathSegments = pageUrl.pathname.split("/");
const bookingUID = pathSegments[pathSegments.length - 1];
const currentBooking = await prisma.booking.findFirst({ where: { uid: bookingUID } });
const currentBooking = await prisma.booking.findFirstOrThrow({ where: { uid: bookingUID } });
expect(currentBooking).not.toBeUndefined();
await confirmBooking(currentBooking.id);
await page.goto(`/reschedule/${currentBooking.uid}`);
await selectFirstAvailableTimeSlotNextMonth(page);
await confirmReschedule(page);
await expect(page).toHaveURL(/.*booking/);
const newBooking = await prisma.booking.findFirstOrThrow({
where: { fromReschedule: currentBooking.uid },
});
expect(newBooking).not.toBeUndefined();
expect(newBooking.status).toBe(BookingStatus.PENDING);
await confirmBooking(newBooking.id);
const booking = await prisma.booking.findFirstOrThrow({ where: { id: newBooking.id } });
expect(booking).not.toBeUndefined();
expect(booking.status).toBe(BookingStatus.ACCEPTED);
const locationVideoCallUrl = bookingMetadataSchema.parse(booking.metadata || {})?.videoCallUrl;
// FIXME: This should be consistent or skip the whole test
// eslint-disable-next-line playwright/no-conditional-in-test
if (currentBooking) {
await confirmBooking(currentBooking.id);
await page.goto(`/reschedule/${currentBooking.uid}`);
await selectFirstAvailableTimeSlotNextMonth(page);
await page.locator('[data-testid="confirm-reschedule-button"]').click();
await expect(page).toHaveURL(/.*booking/);
const newBooking = await prisma.booking.findFirst({ where: { fromReschedule: currentBooking.uid } });
expect(newBooking).not.toBeUndefined();
expect(newBooking?.status).toBe(BookingStatus.PENDING);
// eslint-disable-next-line playwright/no-conditional-in-test
if (newBooking) {
await confirmBooking(newBooking?.id);
const booking = await prisma.booking.findFirst({ where: { id: newBooking.id } });
expect(booking).not.toBeUndefined();
expect(booking?.status).toBe(BookingStatus.ACCEPTED);
const locationVideoCallUrl = bookingMetadataSchema.parse(booking?.metadata || {})?.videoCallUrl;
expect(locationVideoCallUrl).not.toBeUndefined();
// eslint-disable-next-line playwright/no-conditional-in-test
if (booking && locationVideoCallUrl) {
await page.goto(locationVideoCallUrl);
await expect(page.frameLocator("iFrame").locator('text="Continue"')).toBeVisible();
}
}
}
if (!locationVideoCallUrl) return;
expect(locationVideoCallUrl).not.toBeUndefined();
await page.goto(locationVideoCallUrl);
await expect(page.frameLocator("iFrame").locator('text="Continue"')).toBeVisible();
});
test("Should be able to a dynamic group booking", async () => {
@@ -409,7 +411,10 @@ test.describe("Reschedule Tests", async () => {
async function expectSuccessfulReschedule() {
await selectFirstAvailableTimeSlotNextMonth(page);
await page.locator("[data-testid=confirm-reschedule-button]").click();
const { protocol, host } = new URL(page.url());
// Needed since we we're expecting a non-org URL, causing timeouts.
const url = getNonOrgUrlFromOrgUrl(`${protocol}//${host}/api/book/event`);
await confirmReschedule(page, url);
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
}
});
@@ -3,7 +3,7 @@ import { expect } from "@playwright/test";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { test } from "../lib/fixtures";
import { localize, getInviteLink } from "../lib/testUtils";
import { getInviteLink, localize } from "../lib/testUtils";
import { expectInvitationEmailToBeReceived } from "./expects";
test.describe.configure({ mode: "parallel" });
@@ -75,7 +75,6 @@ test.describe("Team", () => {
password: "P4ssw0rd!",
});
await page.locator(`button:text("${t("add")}")`).click();
await page.locator(`[data-testid="copy-invite-link-button"]`).click();
const inviteLink = await getInviteLink(page);
const context = await browser.newContext();
+2 -1
View File
@@ -8,6 +8,7 @@ import { test, todo } from "./lib/fixtures";
import { testBothFutureAndLegacyRoutes } from "./lib/future-legacy-routes";
import {
bookTimeSlot,
confirmReschedule,
fillStripeTestCheckout,
selectFirstAvailableTimeSlotNextMonth,
testName,
@@ -300,7 +301,7 @@ testBothFutureAndLegacyRoutes.describe("Teams - NonOrg", (routeVariant) => {
const booking = await bookings.create(owner.id, owner.username, eventType.id);
await page.goto(`/reschedule/${booking.uid}`);
await selectFirstAvailableTimeSlotNextMonth(page);
await page.locator("[data-testid=confirm-reschedule-button]").click();
await confirmReschedule(page);
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
});
todo("Reschedule a Round Robin EventType booking");
+31 -9
View File
@@ -9,9 +9,11 @@ import { test } from "./lib/fixtures";
import {
bookOptinEvent,
bookTimeSlot,
confirmReschedule,
createUserWithSeatedEventAndAttendees,
gotoRoutingLink,
selectFirstAvailableTimeSlotNextMonth,
submitAndWaitForResponse,
} from "./lib/testUtils";
// remove dynamic properties that differs depending on where you run the tests
@@ -145,8 +147,10 @@ test.describe("BOOKING_REJECTED", async () => {
const webhookReceiver = await webhooks.createReceiver();
await page.goto("/bookings/unconfirmed");
await page.click('[data-testid="reject"]');
await page.click('[data-testid="rejection-confirm"]');
await page.waitForResponse((response) => response.url().includes("/api/trpc/bookings/confirm"));
await submitAndWaitForResponse(page, "/api/trpc/bookings/confirm?batch=1", {
action: () => page.click('[data-testid="rejection-confirm"]'),
});
await webhookReceiver.waitForRequestCount(1);
@@ -358,7 +362,7 @@ test.describe("BOOKING_RESCHEDULED", async () => {
await selectFirstAvailableTimeSlotNextMonth(page);
await page.locator('[data-testid="confirm-reschedule-button"]').click();
await confirmReschedule(page);
await expect(page.getByTestId("success-page")).toBeVisible();
@@ -426,7 +430,7 @@ test.describe("BOOKING_RESCHEDULED", async () => {
await selectFirstAvailableTimeSlotNextMonth(page);
await page.locator('[data-testid="confirm-reschedule-button"]').click();
await confirmReschedule(page);
await expect(page.getByTestId("success-page")).toBeVisible();
@@ -459,7 +463,7 @@ test.describe("BOOKING_RESCHEDULED", async () => {
await selectFirstAvailableTimeSlotNextMonth(page);
await page.locator('[data-testid="confirm-reschedule-button"]').click();
await confirmReschedule(page);
await expect(page).toHaveURL(/.*booking/);
@@ -577,10 +581,28 @@ test.describe("MEETING_ENDED, MEETING_STARTED", async () => {
expect(newMeetingEndedTriggers.length).toBe(0);
// disable webhook
const submitPromise = page.waitForResponse("/api/trpc/webhook/edit?batch=1");
await page.getByTestId("webhook-switch").click();
const response = await submitPromise;
expect(response.status()).toBe(200);
await submitAndWaitForResponse(page, "/api/trpc/webhook/edit?batch=1", {
action: () => page.getByTestId("webhook-switch").click(),
});
const scheduledTriggersAfterDisabling = await prisma.webhookScheduledTriggers.findMany({
where: {
webhook: {
userId: user.id,
},
},
select: {
payload: true,
webhook: {
select: {
userId: true,
},
},
startAfter: true,
},
});
expect(scheduledTriggersAfterDisabling.length).toBe(0);
});
});
@@ -102,8 +102,9 @@ export async function bookFirstEvent(username: string, frame: Frame, page: Page)
// --- fill form
await frame.fill('[name="name"]', "Embed User");
await frame.fill('[name="email"]', "embed-user@example.com");
const responsePromise = page.waitForResponse("**/api/book/event");
await frame.press('[name="email"]', "Enter");
const response = await page.waitForResponse("**/api/book/event");
const response = await responsePromise;
const booking = (await response.json()) as { uid: string; eventSlug: string };
expect(response.status()).toBe(200);
booking.eventSlug = eventSlug;
@@ -114,8 +115,9 @@ export async function rescheduleEvent(username: string, frame: Frame, page: Page
await selectFirstAvailableTimeSlotNextMonth(frame, page);
// --- fill form
await frame.press('[name="email"]', "Enter");
const responsePromise = page.waitForResponse("**/api/book/event");
await frame.click("[data-testid=confirm-reschedule-button]");
const response = await page.waitForResponse("**/api/book/event");
const response = await responsePromise;
expect(response.status()).toBe(200);
const responseObj = await response.json();
const booking = responseObj.uid;