Skip to content

Commit f52497f

Browse files
zomarsanikdhabal
andauthored
fix: flaky e2e patterns (#16696)
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
1 parent c1c4b12 commit f52497f

29 files changed

+262
-193
lines changed

apps/web/components/booking/CancelBooking.tsx

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { useRouter } from "next/navigation";
21
import { useCallback, useState } from "react";
32

43
import { sdkActionManager } from "@calcom/embed-core/embed-iframe";
@@ -7,6 +6,8 @@ import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calco
76
import type { RecurringEvent } from "@calcom/types/Calendar";
87
import { Button, Icon, TextArea } from "@calcom/ui";
98

9+
import { useRefreshData } from "@lib/hooks/useRefreshData";
10+
1011
type Props = {
1112
booking: {
1213
title?: string;
@@ -38,7 +39,7 @@ type Props = {
3839
export default function CancelBooking(props: Props) {
3940
const [cancellationReason, setCancellationReason] = useState<string>("");
4041
const { t } = useLocale();
41-
const router = useRouter();
42+
const refreshData = useRefreshData();
4243
const { booking, allRemainingBookings, seatReferenceUid, bookingCancelledEventProps, currentUserEmail } =
4344
props;
4445
const [loading, setLoading] = useState(false);
@@ -120,7 +121,7 @@ export default function CancelBooking(props: Props) {
120121
...bookingCancelledEventProps,
121122
booking: bookingWithCancellationReason,
122123
});
123-
router.refresh();
124+
refreshData();
124125
} else {
125126
setLoading(false);
126127
setError(

apps/web/lib/hooks/useAsPath.ts

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { usePathname, useSearchParams } from "next/navigation";
2+
import { useMemo } from "react";
3+
4+
export function useAsPath() {
5+
const pathname = usePathname();
6+
const searchParams = useSearchParams();
7+
const asPath = useMemo(
8+
() => `${pathname}${searchParams ? `?${searchParams.toString()}` : ""}`,
9+
[pathname, searchParams]
10+
);
11+
return asPath;
12+
}

apps/web/lib/hooks/useRefreshData.ts

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { useRouter } from "next/navigation";
2+
3+
import { useAsPath } from "./useAsPath";
4+
5+
/** @see https://www.joshwcomeau.com/nextjs/refreshing-server-side-props/ */
6+
export function useRefreshData() {
7+
const router = useRouter();
8+
const asPath = useAsPath();
9+
const refreshData = () => {
10+
router.replace(asPath);
11+
};
12+
return refreshData;
13+
}

apps/web/playwright/availability.e2e.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ test.describe("Availablity", () => {
3333
await page.getByTestId("add-override-submit-btn").click();
3434
await page.getByTestId("dialog-rejection").click();
3535
await expect(page.locator('[data-testid="date-overrides-list"] > li')).toHaveCount(1);
36-
await page.locator('[form="availability-form"][type="submit"]').click();
37-
const response = await page.waitForResponse("**/api/trpc/availability/schedule.update?batch=1");
38-
const json = await response.json();
36+
await submitAndWaitForResponse(page, "/api/trpc/availability/schedule.update?batch=1", {
37+
action: () => page.locator('[form="availability-form"][type="submit"]').click(),
38+
});
3939
const nextMonth = dayjs().add(1, "month").startOf("month");
4040
const troubleshooterURL = `/availability/troubleshoot?date=${nextMonth.format("YYYY-MM-DD")}`;
4141
await page.goto(troubleshooterURL);

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { entries } from "@calcom/prisma/zod-utils";
1313
import type { IntervalLimit } from "@calcom/types/Calendar";
1414

1515
import { test } from "./lib/fixtures";
16-
import { bookTimeSlot, createUserWithLimits } from "./lib/testUtils";
16+
import { bookTimeSlot, confirmReschedule, createUserWithLimits } from "./lib/testUtils";
1717

1818
test.describe.configure({ mode: "parallel" });
1919
test.afterEach(async ({ users }) => {
@@ -163,7 +163,7 @@ test.describe("Booking limits", () => {
163163
await expect(page.locator('[name="name"]')).toBeDisabled();
164164
await expect(page.locator('[name="email"]')).toBeDisabled();
165165

166-
await page.locator('[data-testid="confirm-reschedule-button"]').click();
166+
await confirmReschedule(page);
167167

168168
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
169169

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
bookFirstEvent,
1313
bookOptinEvent,
1414
bookTimeSlot,
15+
confirmReschedule,
1516
selectFirstAvailableTimeSlotNextMonth,
1617
testEmail,
1718
testName,
@@ -128,7 +129,7 @@ testBothFutureAndLegacyRoutes.describe("pro user", () => {
128129
});
129130
await selectFirstAvailableTimeSlotNextMonth(page);
130131

131-
await page.locator('[data-testid="confirm-reschedule-button"]').click();
132+
await confirmReschedule(page);
132133
await page.waitForURL((url) => {
133134
return url.pathname.startsWith("/booking");
134135
});

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

+7-5
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ import { BookingStatus } from "@calcom/prisma/enums";
77

88
import { test } from "./lib/fixtures";
99
import {
10+
confirmReschedule,
1011
createNewSeatedEventType,
11-
selectFirstAvailableTimeSlotNextMonth,
1212
createUserWithSeatedEventAndAttendees,
13+
selectFirstAvailableTimeSlotNextMonth,
14+
submitAndWaitForResponse,
1315
} from "./lib/testUtils";
1416

1517
test.describe.configure({ mode: "parallel" });
@@ -160,9 +162,9 @@ test.describe("Reschedule for booking with seats", () => {
160162
`/booking/${references[0].referenceUid}?cancel=true&seatReferenceUid=${references[0].referenceUid}`
161163
);
162164

163-
await page.locator('[data-testid="confirm_cancel"]').click();
164-
165-
await page.waitForResponse((res) => res.url().includes("api/cancel") && res.status() === 200);
165+
await submitAndWaitForResponse(page, "/api/cancel", {
166+
action: () => page.locator('[data-testid="confirm_cancel"]').click(),
167+
});
166168

167169
const oldBooking = await prisma.booking.findFirst({
168170
where: { uid: booking.uid },
@@ -396,7 +398,7 @@ test.describe("Reschedule for booking with seats", () => {
396398
await expect(reasonElement).toBeVisible();
397399

398400
// expect to be redirected to reschedule page
399-
await page.locator('[data-testid="confirm-reschedule-button"]').click();
401+
await confirmReschedule(page);
400402

401403
// should wait for URL but that path starts with booking/
402404
await page.waitForURL(/\/booking\/.*/);

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

+4-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { expect } from "@playwright/test";
22

33
import { test } from "./lib/fixtures";
4+
import { submitAndWaitForResponse } from "./lib/testUtils";
45

56
test.afterEach(({ users }) => users.deleteAll());
67

@@ -18,11 +19,8 @@ test.describe("Change Password Test", () => {
1819

1920
const $newPasswordField = page.locator('[name="newPassword"]');
2021
$newPasswordField.fill(`${pro.username}Aa1111`);
21-
22-
await page.locator("text=Update").click();
23-
24-
const toast = await page.waitForSelector('[data-testid="toast-success"]');
25-
26-
expect(toast).toBeTruthy();
22+
await submitAndWaitForResponse(page, "/api/trpc/auth/changePassword?batch=1", {
23+
action: () => page.locator("text=Update").click(),
24+
});
2725
});
2826
});

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

+31-45
Original file line numberDiff line numberDiff line change
@@ -7,67 +7,53 @@ import { MembershipRole } from "@calcom/prisma/enums";
77
import { moveUserToOrg } from "@lib/orgMigration";
88

99
import { test } from "./lib/fixtures";
10-
import { IS_STRIPE_ENABLED } from "./lib/testUtils";
10+
import { IS_STRIPE_ENABLED, submitAndWaitForResponse } from "./lib/testUtils";
1111

1212
test.describe.configure({ mode: "parallel" });
1313

1414
const IS_SELF_HOSTED = !(
1515
new URL(WEBAPP_URL).hostname.endsWith(".cal.dev") || !!new URL(WEBAPP_URL).hostname.endsWith(".cal.com")
1616
);
1717

18+
const TESTING_USERNAMES = [
19+
{
20+
username: "demousernamex",
21+
description: "",
22+
},
23+
{
24+
username: "demo.username",
25+
description: " to include periods(or dots)",
26+
},
27+
];
28+
1829
test.describe("Change username on settings", () => {
1930
test.afterEach(async ({ users }) => {
2031
await users.deleteAll();
2132
});
2233

23-
test("User can change username", async ({ page, users, prisma }) => {
24-
const user = await users.create();
25-
26-
await user.apiLogin();
27-
// Try to go homepage
28-
await page.goto("/settings/my-account/profile");
29-
// Change username from normal to normal
30-
const usernameInput = page.locator("[data-testid=username-input]");
31-
32-
await usernameInput.fill("demousernamex");
33-
await page.click("[data-testid=update-username-btn]");
34-
await Promise.all([
35-
page.click("[data-testid=save-username]"),
36-
page.getByTestId("toast-success").waitFor(),
37-
]);
38-
39-
const newUpdatedUser = await prisma.user.findUniqueOrThrow({
40-
where: {
41-
id: user.id,
42-
},
43-
});
34+
TESTING_USERNAMES.forEach((item) => {
35+
test(`User can change username${item.description}`, async ({ page, users, prisma }) => {
36+
const user = await users.create();
37+
await user.apiLogin();
38+
// Try to go homepage
39+
await page.goto("/settings/my-account/profile");
40+
// Change username from normal to normal
41+
const usernameInput = page.locator("[data-testid=username-input]");
4442

45-
expect(newUpdatedUser.username).toBe("demousernamex");
46-
});
43+
await usernameInput.fill(item.username);
44+
await page.click("[data-testid=update-username-btn]");
45+
await submitAndWaitForResponse(page, "/api/trpc/viewer/updateProfile?batch=1", {
46+
action: () => page.click("[data-testid=save-username]"),
47+
});
4748

48-
test("User can change username to include periods(or dots)", async ({ page, users, prisma }) => {
49-
const user = await users.create();
49+
const newUpdatedUser = await prisma.user.findUniqueOrThrow({
50+
where: {
51+
id: user.id,
52+
},
53+
});
5054

51-
await user.apiLogin();
52-
// Try to go homepage
53-
await page.goto("/settings/my-account/profile");
54-
// Change username from normal to normal
55-
const usernameInput = page.locator("[data-testid=username-input]");
56-
// User can change username to include dots(or periods)
57-
await usernameInput.fill("demo.username");
58-
await page.click("[data-testid=update-username-btn]");
59-
await Promise.all([
60-
page.click("[data-testid=save-username]"),
61-
page.getByTestId("toast-success").waitFor(),
62-
]);
63-
64-
const updatedUser = await prisma.user.findUniqueOrThrow({
65-
where: {
66-
id: user.id,
67-
},
55+
expect(newUpdatedUser.username).toBe(item.username);
6856
});
69-
70-
expect(updatedUser.username).toBe("demo.username");
7157
});
7258

7359
test("User can update to PREMIUM username", async ({ page, users }, testInfo) => {

apps/web/playwright/dynamic-booking-pages.e2e.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { MembershipRole } from "@calcom/prisma/client";
55
import { test } from "./lib/fixtures";
66
import {
77
bookTimeSlot,
8+
confirmReschedule,
89
doOnOrgDomain,
910
selectFirstAvailableTimeSlotNextMonth,
1011
selectSecondAvailableTimeSlotNextMonth,
@@ -42,7 +43,7 @@ test("dynamic booking", async ({ page, users }) => {
4243
await selectSecondAvailableTimeSlotNextMonth(page);
4344

4445
// No need to fill fields since they should be already filled
45-
await page.locator('[data-testid="confirm-reschedule-button"]').click();
46+
await confirmReschedule(page);
4647
await page.waitForURL((url) => {
4748
return url.pathname.startsWith("/booking");
4849
});

apps/web/playwright/event-types.e2e.ts

+10-7
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
gotoFirstEventType,
1414
saveEventType,
1515
selectFirstAvailableTimeSlotNextMonth,
16+
submitAndWaitForResponse,
1617
} from "./lib/testUtils";
1718

1819
test.describe.configure({ mode: "parallel" });
@@ -130,9 +131,9 @@ testBothFutureAndLegacyRoutes.describe("Event Types tests", () => {
130131
await page.waitForURL((url) => {
131132
return !!url.pathname.match(/\/event-types\/.+/);
132133
});
133-
await page.locator("[data-testid=update-eventtype]").click();
134-
const toast = await page.waitForSelector('[data-testid="toast-success"]');
135-
expect(toast).toBeTruthy();
134+
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/update?batch=1", {
135+
action: () => page.locator("[data-testid=update-eventtype]").click(),
136+
});
136137
});
137138

138139
test("can add multiple organizer address", async ({ page }) => {
@@ -153,7 +154,9 @@ testBothFutureAndLegacyRoutes.describe("Event Types tests", () => {
153154
await page.locator("[data-testid=add-location]").click();
154155
await fillLocation(page, locationData[2], 2);
155156

156-
await page.locator("[data-testid=update-eventtype]").click();
157+
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/update?batch=1", {
158+
action: () => page.locator("[data-testid=update-eventtype]").click(),
159+
});
157160

158161
await page.goto("/event-types");
159162

@@ -224,7 +227,6 @@ testBothFutureAndLegacyRoutes.describe("Event Types tests", () => {
224227
await page.locator(`text="Cal Video (Global)"`).click();
225228

226229
await saveEventType(page);
227-
await page.getByTestId("toast-success").waitFor();
228230
await gotoBookingPage(page);
229231
await selectFirstAvailableTimeSlotNextMonth(page);
230232

@@ -246,7 +248,6 @@ testBothFutureAndLegacyRoutes.describe("Event Types tests", () => {
246248
await page.locator(`input[name="${locationInputName}"]`).fill(testUrl);
247249

248250
await saveEventType(page);
249-
await page.getByTestId("toast-success").waitFor();
250251
await gotoBookingPage(page);
251252
await selectFirstAvailableTimeSlotNextMonth(page);
252253

@@ -298,7 +299,9 @@ testBothFutureAndLegacyRoutes.describe("Event Types tests", () => {
298299
const locationAddress = "New Delhi";
299300

300301
await fillLocation(page, locationAddress, 0, false);
301-
await page.locator("[data-testid=update-eventtype]").click();
302+
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/update?batch=1", {
303+
action: () => page.locator("[data-testid=update-eventtype]").click(),
304+
});
302305

303306
await page.goto("/event-types");
304307

apps/web/playwright/fixtures/bookings.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Page } from "@playwright/test";
1+
import type { Page, WorkerInfo } from "@playwright/test";
22
import type { Booking, Prisma } from "@prisma/client";
33
import short from "short-uuid";
44
import { v5 as uuidv5 } from "uuid";
@@ -14,7 +14,7 @@ type BookingFixture = ReturnType<typeof createBookingFixture>;
1414
const dayjs = (...args: Parameters<typeof _dayjs>) => _dayjs(...args).tz("Europe/London");
1515

1616
// creates a user fixture instance and stores the collection
17-
export const createBookingsFixture = (page: Page) => {
17+
export const createBookingsFixture = (page: Page, workerInfo: WorkerInfo) => {
1818
const store = { bookings: [], page } as { bookings: BookingFixture[]; page: typeof page };
1919
return {
2020
create: async (
@@ -40,7 +40,9 @@ export const createBookingsFixture = (page: Page) => {
4040
endDateParam?: Date
4141
) => {
4242
const startDate = startDateParam || dayjs().add(1, "day").toDate();
43-
const seed = `${username}:${dayjs(startDate).utc().format()}:${new Date().getTime()}`;
43+
const seed = `${username}:${dayjs(startDate).utc().format()}:${new Date().getTime()}:${
44+
workerInfo.workerIndex
45+
}`;
4446
const uid = translator.fromUUID(uuidv5(seed, uuidv5.URL));
4547
const booking = await prisma.booking.create({
4648
data: {

apps/web/playwright/fixtures/regularBookings.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { expect, type Page } from "@playwright/test";
22

33
import type { MembershipRole } from "@calcom/prisma/enums";
44

5-
import { localize } from "../lib/testUtils";
5+
import { localize, submitAndWaitForResponse } from "../lib/testUtils";
66
import type { createUsersFixture } from "./users";
77

88
export const scheduleSuccessfullyText = "This meeting is scheduled";
@@ -40,9 +40,9 @@ export function createBookingPageFixture(page: Page) {
4040
await page.goto("/event-types");
4141
},
4242
updateEventType: async () => {
43-
await page.getByTestId("update-eventtype").click();
44-
const toast = await page.waitForSelector('[data-testid="toast-success"]');
45-
expect(toast).toBeTruthy();
43+
await submitAndWaitForResponse(page, "/api/trpc/eventTypes/update?batch=1", {
44+
action: () => page.locator("[data-testid=update-eventtype]").click(),
45+
});
4646
},
4747
previewEventType: async () => {
4848
const eventtypePromise = page.waitForEvent("popup");

0 commit comments

Comments
 (0)