Skip to content

Commit b83f8dd

Browse files
fix: improve E2E test stability and reduce flakiness (calcom#25897)
* fix: improve E2E test stability and reduce flakiness - Replace hardcoded waits with proper Playwright waitFor assertions in slot selection - Add explicit waits for calendar and time slot elements to be visible before clicking - Replace fixed 2s wait in gotoRoutingLink with networkidle wait - Replace fixed 5s email wait with retry logic (10 retries, 500ms intervals) - Use unique usernames with timestamps to avoid parallel test collisions - Use features fixture instead of direct prisma calls for feature flag mutations - Fix login.e2e.ts to use unique username instead of hardcoded 'pro' - Fix signup.e2e.ts to use unique usernames and proper feature flag handling - Remove unused 'users' parameter from tests that don't need it Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> * perf: replace waitForTimeout with smart waits for faster E2E tests - Replace 30 waitForTimeout calls with proper Playwright waits - Use waitFor({ state: 'visible' }) for element visibility - Use waitForLoadState('networkidle') for page load completion - Use waitForFunction for localStorage state changes Performance improvements: - limit-tab.e2e.ts: 10s fixed wait -> element visibility wait - booking-duplicate-api-calls.e2e.ts: 5s fixed wait -> networkidle - change-theme.e2e.ts: 3s fixed wait -> localStorage state check - team-invitation.e2e.ts: multiple 500ms-3s waits -> element waits - booking-seats.e2e.ts: 2s waits -> dropdown visibility waits - embed-code-generator.e2e.ts: 1s waits -> iframe visibility waits - organization-privacy.e2e.ts: 500ms waits -> element/networkidle waits - organization-invitation.e2e.ts: 500ms-1s waits -> element waits - analyticsApps.e2e.ts: 1s wait -> networkidle - integrations.e2e.ts: 1s wait -> calendar element wait - fixtures/apps.ts: 1s waits -> element visibility waits Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent 883ac10 commit b83f8dd

14 files changed

Lines changed: 115 additions & 73 deletions

apps/web/playwright/apps/analytics/analyticsApps.e2e.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ test.describe("check analytics Apps", () => {
1717
await user.apiLogin();
1818
await page.goto("apps/categories/analytics");
1919
await appsPage.installAnalyticsAppSkipConfigure(app);
20-
// eslint-disable-next-line playwright/no-wait-for-timeout
21-
await page.waitForTimeout(1000); // waits for 1 second
20+
// Wait for navigation to complete instead of fixed 1s wait
21+
await page.waitForLoadState("networkidle");
2222
await page.goto("/event-types");
2323
await appsPage.goToEventType("30 min");
2424
await appsPage.goToAppsTab();

apps/web/playwright/booking-duplicate-api-calls.e2e.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,12 @@ async function testDuplicateAPICalls(
2727
});
2828

2929
await page.goto(url);
30-
await page.waitForTimeout(5000);
30+
// Wait for the booking page to fully load and time slots to appear instead of fixed 5s wait
31+
await page.waitForLoadState("networkidle");
32+
// Also wait for the calendar to be visible as a more specific indicator
33+
await page.locator('[data-testid="calendar"]').or(page.locator('[data-testid="time"]')).first().waitFor({ state: "visible", timeout: 10000 }).catch(() => {
34+
// If neither element appears, the page might have a different structure - continue anyway
35+
});
3136

3237
return {
3338
totalCalls: trpcCalls.length + apiV2Calls.length,

apps/web/playwright/booking-seats.e2e.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -555,15 +555,17 @@ test.describe("Reschedule for booking with seats", () => {
555555
await page.waitForSelector('[data-testid="bookings"]');
556556

557557
await page.locator('[data-testid="booking-actions-dropdown"]').nth(0).click();
558-
await page.waitForTimeout(2000);
558+
// Wait for the dropdown menu to appear instead of fixed 2s wait
559+
await page.locator('[data-testid="reschedule"]').waitFor({ state: "visible" });
559560
const href = await page.locator('[data-testid="reschedule"]').getAttribute("href");
560561
const url = new URL(href!, page.url());
561562
const seatReferenceUid = url.searchParams.get('seatReferenceUid');
562563
if(!seatReferenceUid) {
563564
await page.reload();
564565
await page.waitForSelector('[data-testid="bookings"]');
565566
await page.locator('[data-testid="booking-actions-dropdown"]').nth(0).click();
566-
await page.waitForTimeout(2000);
567+
// Wait for the dropdown menu to appear instead of fixed 2s wait
568+
await page.locator('[data-testid="reschedule"]').waitFor({ state: "visible" });
567569
}
568570
await page.locator('[data-testid="reschedule"]').click();
569571
await expect(page.getByText("Seats available").first()).toBeVisible();

apps/web/playwright/change-theme.e2e.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ test.describe("Change App Theme Test", () => {
5555
const toast2 = await page.waitForSelector('[data-testid="toast-success"]');
5656
expect(toast2).toBeTruthy();
5757

58-
await page.waitForTimeout(3000);
58+
// Wait for localStorage to be updated after theme change instead of fixed 3s wait
59+
await page.waitForFunction(() => localStorage.getItem("app-theme") !== null, { timeout: 5000 });
5960
const themeValue = await page.evaluate(() => localStorage.getItem("app-theme"));
6061
expect(themeValue).toBe("light");
6162

apps/web/playwright/embed-code-generator.e2e.ts

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ test.describe("Embed Code Generator Tests", () => {
5959
orgSlug: null,
6060
});
6161

62-
// To prevent early timeouts
63-
await page.waitForTimeout(1000);
62+
// Wait for the preview iframe to be ready instead of fixed 1s wait
63+
await page.locator('iframe[data-testid="embed-preview"]').waitFor({ state: "visible", timeout: 5000 }).catch(() => {});
6464
await expectToContainValidPreviewIframe(page, {
6565
embedType: "inline",
6666
calLink: `${pro.username}/multiple-duration`,
@@ -96,8 +96,8 @@ test.describe("Embed Code Generator Tests", () => {
9696
orgSlug: null,
9797
});
9898

99-
// To prevent early timeouts
100-
await page.waitForTimeout(1000);
99+
// Wait for the preview iframe to be ready instead of fixed 1s wait
100+
await page.locator('iframe[data-testid="embed-preview"]').waitFor({ state: "visible", timeout: 5000 }).catch(() => {});
101101
await expectToContainValidPreviewIframe(page, {
102102
embedType: "floating-popup",
103103
calLink: `${pro.username}/multiple-duration`,
@@ -133,8 +133,8 @@ test.describe("Embed Code Generator Tests", () => {
133133
orgSlug: null,
134134
});
135135

136-
// To prevent early timeouts
137-
await page.waitForTimeout(1000);
136+
// Wait for the preview iframe to be ready instead of fixed 1s wait
137+
await page.locator('iframe[data-testid="embed-preview"]').waitFor({ state: "visible", timeout: 5000 }).catch(() => {});
138138
await expectToContainValidPreviewIframe(page, {
139139
embedType: "element-click",
140140
calLink: `${pro.username}/multiple-duration`,
@@ -172,8 +172,8 @@ test.describe("Embed Code Generator Tests", () => {
172172
orgSlug: null,
173173
});
174174

175-
// To prevent early timeouts
176-
await page.waitForTimeout(1000);
175+
// Wait for the preview iframe to be ready instead of fixed 1s wait
176+
await page.locator('iframe[data-testid="embed-preview"]').waitFor({ state: "visible", timeout: 5000 }).catch(() => {});
177177
await expectToContainValidPreviewIframe(page, {
178178
embedType: "inline",
179179
calLink: decodeURIComponent(embedUrl),
@@ -229,8 +229,8 @@ test.describe("Embed Code Generator Tests", () => {
229229
orgSlug: org.slug,
230230
});
231231

232-
// To prevent early timeouts
233-
await page.waitForTimeout(1000);
232+
// Wait for the preview iframe to be ready instead of fixed 1s wait
233+
await page.locator('iframe[data-testid="embed-preview"]').waitFor({ state: "visible", timeout: 5000 }).catch(() => {});
234234
await expectToContainValidPreviewIframe(page, {
235235
embedType: "inline",
236236
calLink: `${user.username}/multiple-duration`,
@@ -269,8 +269,8 @@ test.describe("Embed Code Generator Tests", () => {
269269
orgSlug: org.slug,
270270
});
271271

272-
// To prevent early timeouts
273-
await page.waitForTimeout(1000);
272+
// Wait for the preview iframe to be ready instead of fixed 1s wait
273+
await page.locator('iframe[data-testid="embed-preview"]').waitFor({ state: "visible", timeout: 5000 }).catch(() => {});
274274
await expectToContainValidPreviewIframe(page, {
275275
embedType: "floating-popup",
276276
calLink: `${user.username}/multiple-duration`,
@@ -308,8 +308,8 @@ test.describe("Embed Code Generator Tests", () => {
308308
orgSlug: org.slug,
309309
});
310310

311-
// To prevent early timeouts
312-
await page.waitForTimeout(1000);
311+
// Wait for the preview iframe to be ready instead of fixed 1s wait
312+
await page.locator('iframe[data-testid="embed-preview"]').waitFor({ state: "visible", timeout: 5000 }).catch(() => {});
313313
await expectToContainValidPreviewIframe(page, {
314314
embedType: "element-click",
315315
calLink: `${user.username}/multiple-duration`,
@@ -326,8 +326,8 @@ function chooseEmbedType(page: Page, embedType: EmbedType) {
326326
}
327327

328328
async function goToReactCodeTab(page: Page) {
329-
// To prevent early timeo
330-
await page.waitForTimeout(1000);
329+
// Wait for the React tab to be visible instead of fixed 1s wait
330+
await page.locator("[data-testid=horizontal-tab-react]").waitFor({ state: "visible" });
331331
await page.locator("[data-testid=horizontal-tab-react]").click();
332332
}
333333

apps/web/playwright/eventType/limit-tab.e2e.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ test.describe("Limits Tab - Event Type", () => {
1818
await bookingPage.updateEventType();
1919
const eventTypePage = await bookingPage.previewEventType();
2020

21-
await eventTypePage.waitForTimeout(10000);
21+
// Wait for time slots to load instead of fixed 10s wait
22+
await eventTypePage.getByTestId("time").first().waitFor({ state: "visible", timeout: 30000 });
2223

2324
const counter = await eventTypePage.getByTestId("time").count();
2425
await bookingPage.checkTimeSlotsCount(eventTypePage, counter);

apps/web/playwright/fixtures/apps.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ export function createAppsFixture(page: Page) {
3333
await page.click('[data-testid="install-app-button-personal"]');
3434
await page.waitForURL(`apps/installation/event-types?slug=${app}`);
3535

36-
// eslint-disable-next-line playwright/no-wait-for-timeout
37-
await page.waitForTimeout(1000);
36+
// Wait for event type checkboxes to be visible instead of fixed 1s wait
37+
await page.locator('[data-testid^="select-event-type-"]').first().waitFor({ state: "visible" });
3838
for (const id of eventTypeIds) {
3939
await page.click(`[data-testid="select-event-type-${id}"]`);
4040
}
@@ -81,8 +81,8 @@ export function createAppsFixture(page: Page) {
8181
await page.getByTestId("install-app-button").click();
8282
await page.waitForURL(`apps/installation/event-types?slug=${app.slug}`);
8383

84-
// eslint-disable-next-line playwright/no-wait-for-timeout
85-
await page.waitForTimeout(1000);
84+
// Wait for event type checkboxes to be visible instead of fixed 1s wait
85+
await page.locator('[data-testid^="select-event-type-"]').first().waitFor({ state: "visible" });
8686
for (const id of eventTypeIds) {
8787
await page.click(`[data-testid="select-event-type-${id}"]`);
8888
}

apps/web/playwright/integrations.e2e.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -99,13 +99,10 @@ const addLocationIntegrationToFirstEvent = async function ({ user }: { user: { u
9999
};
100100

101101
async function bookEvent(page: Page, calLink: string) {
102-
// Let current month dates fully render.
103-
// There is a bug where if we don't let current month fully render and quickly click go to next month, current month gets rendered
104-
// This doesn't seem to be replicable with the speed of a person, only during automation.
105-
// It would also allow correct snapshot to be taken for current month.
106-
// eslint-disable-next-line playwright/no-wait-for-timeout
107-
await page.waitForTimeout(1000);
102+
// Navigate to the booking page and wait for calendar to be ready
108103
await page.goto(`/${calLink}`);
104+
// Wait for the calendar day elements to be visible instead of fixed 1s wait
105+
await page.locator('[data-testid="day"][data-disabled="false"]').first().waitFor({ state: "visible" });
109106

110107
await page.locator('[data-testid="day"][data-disabled="false"]').nth(0).click();
111108
page.locator('[data-testid="time"]').nth(0).click();

apps/web/playwright/lib/testUtils.ts

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -109,19 +109,30 @@ export async function selectFirstAvailableTimeSlotNextMonth(page: Page | Frame)
109109
// Let current month dates fully render.
110110
await page.getByTestId("incrementMonth").click();
111111

112-
// Waiting for full month increment
113-
await page.locator('[data-testid="day"][data-disabled="false"]').nth(0).click();
114-
115-
await page.locator('[data-testid="time"]').nth(0).click();
112+
// Wait for the calendar to update after month increment before clicking
113+
const availableDay = page.locator('[data-testid="day"][data-disabled="false"]').nth(0);
114+
await availableDay.waitFor({ state: "visible" });
115+
await availableDay.click();
116+
117+
// Wait for time slots to load after selecting a day
118+
const timeSlot = page.locator('[data-testid="time"]').nth(0);
119+
await timeSlot.waitFor({ state: "visible" });
120+
await timeSlot.click();
116121
}
117122

118123
export async function selectSecondAvailableTimeSlotNextMonth(page: Page) {
119124
// Let current month dates fully render.
120125
await page.getByTestId("incrementMonth").click();
121126

122-
await page.locator('[data-testid="day"][data-disabled="false"]').nth(1).click();
127+
// Wait for the calendar to update after month increment before clicking
128+
const availableDay = page.locator('[data-testid="day"][data-disabled="false"]').nth(1);
129+
await availableDay.waitFor({ state: "visible" });
130+
await availableDay.click();
123131

124-
await page.locator('[data-testid="time"]').nth(0).click();
132+
// Wait for time slots to load after selecting a day
133+
const timeSlot = page.locator('[data-testid="time"]').nth(0);
134+
await timeSlot.waitFor({ state: "visible" });
135+
await timeSlot.click();
125136
}
126137

127138
export async function bookEventOnThisPage(page: Page) {
@@ -253,8 +264,9 @@ export async function gotoRoutingLink({
253264

254265
await page.goto(`${previewLink}${queryString ? `?${queryString}` : ""}`);
255266

256-
// HACK: There seems to be some issue with the inputs to the form getting reset if we don't wait.
257-
await new Promise((resolve) => setTimeout(resolve, 2000));
267+
// Wait for the form to be fully loaded and stable before interacting
268+
// This replaces the previous hardcoded 2s wait with a proper Playwright wait
269+
await page.waitForLoadState("networkidle");
258270
}
259271

260272
export async function installAppleCalendar(page: Page) {
@@ -274,21 +286,31 @@ export async function getInviteLink(page: Page) {
274286
export async function getEmailsReceivedByUser({
275287
emails,
276288
userEmail,
277-
waitForEmailMs = 5000,
289+
maxRetries = 10,
290+
retryIntervalMs = 500,
278291
}: {
279292
emails?: ReturnType<typeof createEmailsFixture>;
280293
userEmail: string;
281-
waitForEmailMs?: number;
294+
maxRetries?: number;
295+
retryIntervalMs?: number;
282296
}): Promise<Messages | null> {
283297
if (!emails) return null;
284298

285-
// Wait for email to be sent/received
286-
await new Promise((resolve) => setTimeout(resolve, waitForEmailMs));
299+
// Use retry logic instead of a fixed wait to handle email delivery timing
300+
for (let attempt = 0; attempt < maxRetries; attempt++) {
301+
const matchingEmails = await emails.search(userEmail, "to");
302+
if (matchingEmails?.total) {
303+
return matchingEmails;
304+
}
305+
// Wait before retrying
306+
await new Promise((resolve) => setTimeout(resolve, retryIntervalMs));
307+
}
287308

309+
// Final attempt after all retries
288310
const matchingEmails = await emails.search(userEmail, "to");
289311
if (!matchingEmails?.total) {
290312
console.log(
291-
`No emails received by ${userEmail}. All emails sent to:`,
313+
`No emails received by ${userEmail} after ${maxRetries} retries. All emails sent to:`,
292314
(await emails.messages())?.items.map((e) => e.to)
293315
);
294316
}

apps/web/playwright/login.e2e.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,11 @@ test.describe("Login and logout tests", () => {
7272
test("Should warn when password is incorrect", async ({ page, users }) => {
7373
const alertMessage = (await localize("en"))("incorrect_email_password");
7474
// by default password===username with the users fixture
75-
const pro = await users.create({ username: "pro" });
75+
// Use unique username to avoid collisions with parallel tests
76+
const user = await users.create();
7677

7778
// login with a wrong password
78-
await login({ username: pro.username, password: "wrong" }, page);
79+
await login({ username: user.username, password: "wrong" }, page);
7980

8081
// assert for the visibility of the localized alert message
8182
await expect(page.locator(`text=${alertMessage}`)).toBeVisible();

0 commit comments

Comments
 (0)