test: flaky e2e tests (#26308)

* fix another

* fix flakes

* Update apps/web/playwright/fixtures/apps.ts

Co-authored-by: Keith Williams <keithwillcode@gmail.com>

* fix

* fix

* test fix

* fix test

* tweak

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
This commit is contained in:
Anik Dhabal Babu
2025-12-30 13:45:01 +00:00
committed by GitHub
co-authored by Keith Williams
parent 2c7cb530a1
commit cc8da034c0
4 changed files with 218 additions and 72 deletions
+214 -68
View File
@@ -1,12 +1,3 @@
import { expect } from "@playwright/test";
import { JSDOM } from "jsdom";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { generateHashedLink } from "@calcom/lib/generateHashedLink";
import { randomString } from "@calcom/lib/random";
import { SchedulingType } from "@calcom/prisma/enums";
import type { Schedule, TimeRange } from "@calcom/types/schedule";
import { test, todo } from "./lib/fixtures";
import {
bookFirstEvent,
@@ -20,6 +11,13 @@ import {
testName,
cancelBookingFromBookingsList,
} from "./lib/testUtils";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { generateHashedLink } from "@calcom/lib/generateHashedLink";
import { randomString } from "@calcom/lib/random";
import { SchedulingType } from "@calcom/prisma/enums";
import type { Schedule, TimeRange } from "@calcom/types/schedule";
import { expect } from "@playwright/test";
import { JSDOM } from "jsdom";
const freeUserObj = { name: `Free-user-${randomString(3)}` };
test.describe.configure({ mode: "parallel" });
@@ -33,7 +31,9 @@ test("check SSR and OG - User Event Type", async ({ page, users }) => {
name,
});
const responsePromise = page.waitForResponse(
(response) => response.url().includes(`/${user.username}/30-min`) && response.status() === 200
(response) =>
response.url().includes(`/${user.username}/30-min`) &&
response.status() === 200
);
await page.goto(`/${user.username}/30-min`);
await page.content();
@@ -42,17 +42,27 @@ test("check SSR and OG - User Event Type", async ({ page, users }) => {
const document = new JSDOM(ssrResponse).window.document;
const titleText = document.querySelector("title")?.textContent;
const ogImage = document.querySelector('meta[property="og:image"]')?.getAttribute("content");
const ogUrl = document.querySelector('meta[property="og:url"]')?.getAttribute("content");
const canonicalLink = document.querySelector('link[rel="canonical"]')?.getAttribute("href");
const ogImage = document
.querySelector('meta[property="og:image"]')
?.getAttribute("content");
const ogUrl = document
.querySelector('meta[property="og:url"]')
?.getAttribute("content");
const canonicalLink = document
.querySelector('link[rel="canonical"]')
?.getAttribute("href");
expect(titleText).toContain(name);
expect(ogUrl).toEqual(`${WEBAPP_URL}/${user.username}/30-min`);
await page.waitForSelector('[data-testid="avatar-href"]');
const avatarLocators = await page.locator('[data-testid="avatar-href"]').all();
const avatarLocators = await page
.locator('[data-testid="avatar-href"]')
.all();
expect(avatarLocators.length).toBe(1);
for (const avatarLocator of avatarLocators) {
expect(await avatarLocator.getAttribute("href")).toEqual(`${WEBAPP_URL}/${user.username}?redirect=false`);
expect(await avatarLocator.getAttribute("href")).toEqual(
`${WEBAPP_URL}/${user.username}?redirect=false`
);
}
expect(canonicalLink).toEqual(`${WEBAPP_URL}/${user.username}/30-min`);
@@ -157,7 +167,10 @@ test.describe("pro user", () => {
await page.goto("/bookings/upcoming");
await page.waitForSelector('[data-testid="bookings"]');
// Click the ellipsis menu button to open the dropdown
await page.locator('[data-testid="booking-actions-dropdown"]').nth(0).click();
await page
.locator('[data-testid="booking-actions-dropdown"]')
.nth(0)
.click();
await page.locator('[data-testid="reschedule"]').click();
await page.waitForURL((url) => {
const bookingId = url.searchParams.get("rescheduleUid");
@@ -178,17 +191,31 @@ test.describe("pro user", () => {
}) => {
const [pro] = users.get();
const [eventType] = pro.eventTypes;
const bookingFixture = await bookings.create(pro.id, pro.username, eventType.id);
const bookingFixture = await bookings.create(
pro.id,
pro.username,
eventType.id
);
// open the wrong eventType (rescheduleUid created for /30min event)
await page.goto(`${pro.username}/${pro.eventTypes[1].slug}?rescheduleUid=${bookingFixture.uid}`);
await page.goto(
`${pro.username}/${pro.eventTypes[1].slug}?rescheduleUid=${bookingFixture.uid}`
);
await expect(page).toHaveURL(new RegExp(`${pro.username}/${eventType.slug}`));
await expect(page).toHaveURL(
new RegExp(`${pro.username}/${eventType.slug}`)
);
});
test("it returns a 404 when a requested event type does not exist", async ({ page, users }) => {
test("it returns a 404 when a requested event type does not exist", async ({
page,
users,
}) => {
const [pro] = users.get();
const unexistingPageUrl = new URL(`${pro.username}/invalid-event-type`, WEBAPP_URL);
const unexistingPageUrl = new URL(
`${pro.username}/invalid-event-type`,
WEBAPP_URL
);
const response = await page.goto(unexistingPageUrl.href);
expect(response?.status()).toBe(404);
});
@@ -200,8 +227,12 @@ test.describe("pro user", () => {
// Because it tests the entire booking flow + the cancellation + rebooking
test.setTimeout(testInfo.timeout * 3);
await bookFirstEvent(page);
await expect(page.locator(`[data-testid="attendee-email-${testEmail}"]`)).toHaveText(testEmail);
await expect(page.locator(`[data-testid="attendee-name-${testName}"]`)).toHaveText(testName);
await expect(
page.locator(`[data-testid="attendee-email-${testEmail}"]`)
).toHaveText(testEmail);
await expect(
page.locator(`[data-testid="attendee-name-${testName}"]`)
).toHaveText(testName);
const [pro] = users.get();
await pro.apiLogin();
@@ -224,8 +255,12 @@ test.describe("pro user", () => {
// Because it tests the entire booking flow + the cancellation + rebooking
test.setTimeout(testInfo.timeout * 3);
await bookFirstEvent(page);
await expect(page.locator(`[data-testid="attendee-email-${testEmail}"]`)).toHaveText(testEmail);
await expect(page.locator(`[data-testid="attendee-name-${testName}"]`)).toHaveText(testName);
await expect(
page.locator(`[data-testid="attendee-email-${testEmail}"]`)
).toHaveText(testEmail);
await expect(
page.locator(`[data-testid="attendee-name-${testName}"]`)
).toHaveText(testName);
const [pro] = users.get();
await pro.apiLogin();
@@ -242,7 +277,9 @@ test.describe("pro user", () => {
await page.goto(`/reschedule/${bookingUid}`);
expect(page.url()).not.toContain("rescheduleUid");
const cancelledHeadline = page.locator('[data-testid="cancelled-headline"]');
const cancelledHeadline = page.locator(
'[data-testid="cancelled-headline"]'
);
await expect(cancelledHeadline).toBeVisible();
});
@@ -257,14 +294,18 @@ test.describe("pro user", () => {
await page.goto("/bookings/unconfirmed");
await Promise.all([
page.click('[data-testid="confirm"]'),
page.waitForResponse((response) => response.url().includes("/api/trpc/bookings/confirm")),
page.waitForResponse((response) =>
response.url().includes("/api/trpc/bookings/confirm")
),
]);
// This is the only booking in there that needed confirmation and now it should be empty screen
await expect(page.locator('[data-testid="empty-screen"]')).toBeVisible();
});
test("can book an unconfirmed event multiple times", async ({ page }) => {
await page.locator('[data-testid="event-type-link"]:has-text("Opt in")').click();
await page
.locator('[data-testid="event-type-link"]:has-text("Opt in")')
.click();
await selectFirstAvailableTimeSlotNextMonth(page);
const pageUrl = page.url();
@@ -281,7 +322,9 @@ test.describe("pro user", () => {
test("booking an unconfirmed event with the same email brings you to the original request", async ({
page,
}) => {
await page.locator('[data-testid="event-type-link"]:has-text("Opt in")').click();
await page
.locator('[data-testid="event-type-link"]:has-text("Opt in")')
.click();
await selectFirstAvailableTimeSlotNextMonth(page);
const pageUrl = page.url();
@@ -312,12 +355,17 @@ test.describe("pro user", () => {
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
const promises = additionalGuests.map(async (email) => {
await expect(page.locator(`[data-testid="attendee-email-${email}"]`)).toHaveText(email);
await expect(
page.locator(`[data-testid="attendee-email-${email}"]`)
).toHaveText(email);
});
await Promise.all(promises);
});
test("Time slots should be reserved when selected", async ({ page, browser }) => {
test("Time slots should be reserved when selected", async ({
page,
browser,
}) => {
const initialUrl = page.url();
await page.locator('[data-testid="event-type-link"]').first().click();
await selectFirstAvailableTimeSlotNextMonth(page);
@@ -325,16 +373,29 @@ test.describe("pro user", () => {
const pageTwoInNewContext = await newContext.newPage();
await pageTwoInNewContext.goto(initialUrl);
await pageTwoInNewContext.waitForURL(initialUrl);
await pageTwoInNewContext.locator('[data-testid="event-type-link"]').first().click();
await pageTwoInNewContext
.locator('[data-testid="event-type-link"]')
.first()
.click();
await pageTwoInNewContext.locator('[data-testid="incrementMonth"]').waitFor();
await pageTwoInNewContext
.locator('[data-testid="incrementMonth"]')
.waitFor();
await pageTwoInNewContext.click('[data-testid="incrementMonth"]');
await pageTwoInNewContext.locator('[data-testid="day"][data-disabled="false"]').nth(0).waitFor();
await pageTwoInNewContext.locator('[data-testid="day"][data-disabled="false"]').nth(0).click();
await pageTwoInNewContext
.locator('[data-testid="day"][data-disabled="false"]')
.nth(0)
.waitFor();
await pageTwoInNewContext
.locator('[data-testid="day"][data-disabled="false"]')
.nth(0)
.click();
// 9:30 should be the first available time slot
await pageTwoInNewContext.locator('[data-testid="time"]').nth(0).waitFor();
const firstSlotAvailable = pageTwoInNewContext.locator('[data-testid="time"]').nth(0);
const firstSlotAvailable = pageTwoInNewContext
.locator('[data-testid="time"]')
.nth(0);
// Find text inside the element
const firstSlotAvailableText = await firstSlotAvailable.innerText();
expect(firstSlotAvailableText).toContain("9:30");
@@ -346,7 +407,9 @@ test.describe("pro user", () => {
}) => {
const initialUrl = page.url();
await page.waitForSelector('[data-testid="event-type-link"]');
const eventTypeLink = page.locator('[data-testid="event-type-link"]').first();
const eventTypeLink = page
.locator('[data-testid="event-type-link"]')
.first();
await eventTypeLink.click();
await selectFirstAvailableTimeSlotNextMonth(page);
@@ -355,7 +418,9 @@ test.describe("pro user", () => {
await pageTwo.waitForURL(initialUrl);
await pageTwo.waitForSelector('[data-testid="event-type-link"]');
const eventTypeLinkTwo = pageTwo.locator('[data-testid="event-type-link"]').first();
const eventTypeLinkTwo = pageTwo
.locator('[data-testid="event-type-link"]')
.first();
await eventTypeLinkTwo.waitFor();
await eventTypeLinkTwo.click();
@@ -364,8 +429,14 @@ test.describe("pro user", () => {
await pageTwo.locator('[data-testid="incrementMonth"]').waitFor();
await pageTwo.click('[data-testid="incrementMonth"]');
await pageTwo.locator('[data-testid="day"][data-disabled="false"]').nth(0).waitFor();
await pageTwo.locator('[data-testid="day"][data-disabled="false"]').nth(0).click();
await pageTwo
.locator('[data-testid="day"][data-disabled="false"]')
.nth(0)
.waitFor();
await pageTwo
.locator('[data-testid="day"][data-disabled="false"]')
.nth(0)
.click();
await pageTwo.locator('[data-testid="time"]').nth(0).waitFor();
const firstSlotAvailable = pageTwo.locator('[data-testid="time"]').nth(0);
@@ -384,7 +455,9 @@ test.describe("prefill", () => {
await test.step("from session", async () => {
await selectFirstAvailableTimeSlotNextMonth(page);
await expect(page.locator('[name="name"]')).toHaveValue(prefill.name || "");
await expect(page.locator('[name="name"]')).toHaveValue(
prefill.name || ""
);
await expect(page.locator('[name="email"]')).toHaveValue(prefill.email);
});
@@ -399,7 +472,9 @@ test.describe("prefill", () => {
});
});
test("Persist the field values when going back and coming back to the booking form", async ({ page }) => {
test("Persist the field values when going back and coming back to the booking form", async ({
page,
}) => {
await page.goto("/pro/30min");
await selectFirstAvailableTimeSlotNextMonth(page);
await page.fill('[name="name"]', "John Doe");
@@ -409,7 +484,9 @@ test.describe("prefill", () => {
await selectFirstAvailableTimeSlotNextMonth(page);
await expect(page.locator('[name="name"]')).toHaveValue("John Doe");
await expect(page.locator('[name="email"]')).toHaveValue("john@example.com");
await expect(page.locator('[name="email"]')).toHaveValue(
"john@example.com"
);
await expect(page.locator('[name="notes"]')).toHaveValue("Test notes");
});
@@ -429,7 +506,9 @@ test.describe("prefill", () => {
});
});
test("skip confirm step if all fields are prefilled from query params", async ({ page }) => {
test("skip confirm step if all fields are prefilled from query params", async ({
page,
}) => {
await page.goto("/pro/30min");
const url = new URL(page.url());
url.searchParams.set("name", testName);
@@ -440,7 +519,9 @@ test.describe("prefill", () => {
await page.goto(url.toString());
await selectFirstAvailableTimeSlotNextMonth(page);
await expect(page.locator('[data-testid="skip-confirm-book-button"]')).toBeVisible();
await expect(
page.locator('[data-testid="skip-confirm-book-button"]')
).toBeVisible();
await page.click('[data-testid="skip-confirm-book-button"]');
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
@@ -455,7 +536,15 @@ test.describe("Booking on different layouts", () => {
start: new Date(new Date().setUTCHours(9, 0, 0, 0)),
end: new Date(new Date().setUTCHours(17, 0, 0, 0)),
};
const schedule: Schedule = [[], [dateRanges], [dateRanges], [dateRanges], [dateRanges], [dateRanges], []];
const schedule: Schedule = [
[],
[dateRanges],
[dateRanges],
[dateRanges],
[dateRanges],
[dateRanges],
[],
];
const user = await users.create({ schedule });
await page.goto(`/${user.username}`);
@@ -490,6 +579,13 @@ test.describe("Booking on different layouts", () => {
await page.click('[data-testid="incrementMonth"]');
await page.waitForURL((url) => {
return url.searchParams.has("month");
})
await page.reload();
await page.waitForLoadState("networkidle");
await page.locator('[data-testid="time"]').nth(1).click();
// Fill what is this meeting about? name email and notes
@@ -513,7 +609,15 @@ test.describe("Booking round robin event", () => {
end: new Date(new Date().setUTCHours(17, 0, 0, 0)),
};
const schedule: Schedule = [[], [dateRanges], [dateRanges], [dateRanges], [dateRanges], [dateRanges], []];
const schedule: Schedule = [
[],
[dateRanges],
[dateRanges],
[dateRanges],
[dateRanges],
[dateRanges],
[],
];
const testUser = await users.create(
{ schedule },
@@ -621,12 +725,16 @@ test.describe("Event type with disabled cancellation and rescheduling", () => {
bookingId = pathSegments[pathSegments.length - 1];
});
test("Reschedule and cancel buttons should be hidden on success page", async ({ page }) => {
test("Reschedule and cancel buttons should be hidden on success page", async ({
page,
}) => {
await expect(page.locator('[data-testid="reschedule-link"]')).toBeHidden();
await expect(page.locator('[data-testid="cancel"]')).toBeHidden();
});
test("Direct access to reschedule/{bookingId} should redirect to success page", async ({ page }) => {
test("Direct access to reschedule/{bookingId} should redirect to success page", async ({
page,
}) => {
await page.goto(`/reschedule/${bookingId}`);
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
@@ -634,15 +742,21 @@ test.describe("Event type with disabled cancellation and rescheduling", () => {
await page.waitForURL((url) => url.pathname === `/booking/${bookingId}`);
});
test("Using rescheduleUid query parameter should redirect to success page", async ({ page }) => {
await page.goto(`/${user.username}/no-cancel-no-reschedule?rescheduleUid=${bookingId}`);
test("Using rescheduleUid query parameter should redirect to success page", async ({
page,
}) => {
await page.goto(
`/${user.username}/no-cancel-no-reschedule?rescheduleUid=${bookingId}`
);
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
await page.waitForURL((url) => url.pathname === `/booking/${bookingId}`);
});
test("Should prevent cancellation and show an error message", async ({ page }) => {
test("Should prevent cancellation and show an error message", async ({
page,
}) => {
const csrfTokenResponse = await page.request.get("/api/csrf");
const { csrfToken } = await csrfTokenResponse.json();
const response = await page.request.post("/api/cancel", {
@@ -657,10 +771,15 @@ test.describe("Event type with disabled cancellation and rescheduling", () => {
expect(response.status()).toBe(400);
const responseBody = await response.json();
expect(responseBody.message).toBe("This event type does not allow cancellations");
expect(responseBody.message).toBe(
"This event type does not allow cancellations"
);
});
});
test("Should throw error when both seatsPerTimeSlot and recurringEvent are set", async ({ page, users }) => {
test("Should throw error when both seatsPerTimeSlot and recurringEvent are set", async ({
page,
users,
}) => {
const user = await users.create({
name: `Test-user-${randomString(4)}`,
eventTypes: [
@@ -699,7 +818,11 @@ test.describe("GTM container", () => {
await users.create();
});
test("global GTM should not be loaded on private booking link", async ({ page, users, prisma }) => {
test("global GTM should not be loaded on private booking link", async ({
page,
users,
prisma,
}) => {
const [user] = users.get();
const eventType = await user.getFirstEventAsOwner();
@@ -722,9 +845,12 @@ test.describe("GTM container", () => {
});
const getScheduleRespPromise = page.waitForResponse(
(response) => response.url().includes("getSchedule") && response.status() === 200
(response) =>
response.url().includes("getSchedule") && response.status() === 200
);
await page.goto(
`/d/${eventWithPrivateLink.hashedLink[0]?.link}/${eventWithPrivateLink.slug}`
);
await page.goto(`/d/${eventWithPrivateLink.hashedLink[0]?.link}/${eventWithPrivateLink.slug}`);
await page.waitForLoadState("domcontentloaded");
await getScheduleRespPromise;
@@ -732,15 +858,23 @@ test.describe("GTM container", () => {
await expect(injectedScript).not.toBeAttached();
});
test("global GTM should be loaded on non-booking pages", async ({ page, users }) => {
test.skip(!process.env.NEXT_PUBLIC_BODY_SCRIPTS, "Skipping test as NEXT_PUBLIC_BODY_SCRIPTS is not set");
test("global GTM should be loaded on non-booking pages", async ({
page,
users,
}) => {
test.skip(
!process.env.NEXT_PUBLIC_BODY_SCRIPTS,
"Skipping test as NEXT_PUBLIC_BODY_SCRIPTS is not set"
);
const [user] = users.get();
await user.apiLogin();
// Go to /insights page and wait for one of the common API call to complete
const eventsByStatusRespPromise = page.waitForResponse(
(response) => response.url().includes("getEventTypesFromGroup") && response.status() === 200
(response) =>
response.url().includes("getEventTypesFromGroup") &&
response.status() === 200
);
await page.goto(`/insights`);
await page.waitForLoadState("domcontentloaded");
@@ -755,7 +889,11 @@ test.describe("GTM container", () => {
});
test.describe("Past booking cancellation", () => {
test("Cancel button should be hidden for past bookings", async ({ page, users, bookings }) => {
test("Cancel button should be hidden for past bookings", async ({
page,
users,
bookings,
}) => {
const user = await users.create({
name: "Test User",
});
@@ -766,15 +904,23 @@ test.describe("Past booking cancellation", () => {
pastDate.setDate(pastDate.getDate() - 1);
const endDate = new Date(pastDate.getTime() + 30 * 60 * 1000);
const booking = await bookings.create(user.id, user.username, user.eventTypes[0].id, {
title: "Past Meeting",
startTime: pastDate,
endTime: endDate,
status: "ACCEPTED",
});
const booking = await bookings.create(
user.id,
user.username,
user.eventTypes[0].id,
{
title: "Past Meeting",
startTime: pastDate,
endTime: endDate,
status: "ACCEPTED",
}
);
await page.goto("/bookings/past");
await page.locator('[data-testid="booking-actions-dropdown"]').nth(0).click();
await page
.locator('[data-testid="booking-actions-dropdown"]')
.nth(0)
.click();
await expect(page.locator('[data-testid="cancel"]')).toBeDisabled();
await page.goto(`/booking/${booking.uid}`);
+2 -2
View File
@@ -117,7 +117,7 @@ export function createAppsFixture(page: Page) {
},
goToAppsTab: async () => {
await page.getByTestId("vertical-tab-apps").click();
await expect(page.getByTestId("vertical-tab-apps")).toHaveAttribute("aria-current", "page");
await expect(page.getByTestId("vertical-tab-apps").first()).toHaveAttribute("aria-current", "page");
},
activeApp: async (app: string) => {
await page.locator(`[data-testid='${app}-app-switch']`).click();
@@ -127,7 +127,7 @@ export function createAppsFixture(page: Page) {
},
verifyAppsInfoNew: async (app: string, eventTypeId: number) => {
await page.goto(`event-types/${eventTypeId}?tabName=apps`);
await expect(page.getByTestId("vertical-tab-apps")).toHaveAttribute("aria-current", "page"); // fix the race condition
await expect(page.getByTestId("vertical-tab-apps").first()).toHaveAttribute("aria-current", "page"); // fix the race condition
await expect(page.locator(`[data-testid='${app}-app-switch'][data-state="checked"]`)).toBeVisible();
},
};
@@ -34,7 +34,7 @@ test.describe("Managed Event Types", () => {
// Let's create a team
// Going to create an event type
await page.goto("/event-types");
const tabItem = page.getByTestId(`horizontal-tab-Owner`);
const tabItem = page.getByTestId("horizontal-tab-Owner").first();
await expect(tabItem).toBeVisible();
// We wait until loading is finished
await page.waitForSelector('[data-testid="event-types"]');
+1 -1
View File
@@ -11,7 +11,7 @@ test.afterEach(({ users }) => users.deleteAll());
async function goToAppsTab(page: Page, eventTypeId?: number) {
await page.goto(`event-types/${eventTypeId}?tabName=apps`);
await expect(page.getByTestId("vertical-tab-apps")).toHaveAttribute("aria-current", "page");
await expect(page.getByTestId("vertical-tab-apps").first()).toHaveAttribute("aria-current", "page");
}
test.describe("Payment app", () => {