Skip to content

Commit

Permalink
fix: flaky e2e patterns (#16696)
Browse files Browse the repository at this point in the history
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
  • Loading branch information
zomars and anikdhabal authored Sep 20, 2024
1 parent c1c4b12 commit f52497f
Show file tree
Hide file tree
Showing 29 changed files with 262 additions and 193 deletions.
7 changes: 4 additions & 3 deletions apps/web/components/booking/CancelBooking.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { useRouter } from "next/navigation";
import { useCallback, useState } from "react";

import { sdkActionManager } from "@calcom/embed-core/embed-iframe";
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -120,7 +121,7 @@ export default function CancelBooking(props: Props) {
...bookingCancelledEventProps,
booking: bookingWithCancellationReason,
});
router.refresh();
refreshData();
} else {
setLoading(false);
setError(
Expand Down
12 changes: 12 additions & 0 deletions apps/web/lib/hooks/useAsPath.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 13 additions & 0 deletions apps/web/lib/hooks/useRefreshData.ts
Original file line number Diff line number Diff line change
@@ -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;
}
6 changes: 3 additions & 3 deletions apps/web/playwright/availability.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions apps/web/playwright/booking-limits.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => {
Expand Down Expand Up @@ -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();

Expand Down
3 changes: 2 additions & 1 deletion apps/web/playwright/booking-pages.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
bookFirstEvent,
bookOptinEvent,
bookTimeSlot,
confirmReschedule,
selectFirstAvailableTimeSlotNextMonth,
testEmail,
testName,
Expand Down Expand Up @@ -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");
});
Expand Down
12 changes: 7 additions & 5 deletions apps/web/playwright/booking-seats.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" });
Expand Down Expand Up @@ -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 },
Expand Down Expand Up @@ -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\/.*/);
Expand Down
10 changes: 4 additions & 6 deletions apps/web/playwright/change-password.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { expect } from "@playwright/test";

import { test } from "./lib/fixtures";
import { submitAndWaitForResponse } from "./lib/testUtils";

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

Expand All @@ -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(),
});
});
});
76 changes: 31 additions & 45 deletions apps/web/playwright/change-username.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,67 +7,53 @@ 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" });

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();

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("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,
},
});
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]");

expect(newUpdatedUser.username).toBe("demousernamex");
});
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]"),
});

test("User can change username to include periods(or dots)", async ({ page, users, prisma }) => {
const user = await users.create();
const newUpdatedUser = await prisma.user.findUniqueOrThrow({
where: {
id: user.id,
},
});

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(newUpdatedUser.username).toBe(item.username);
});

expect(updatedUser.username).toBe("demo.username");
});

test("User can update to PREMIUM username", async ({ page, users }, testInfo) => {
Expand Down
3 changes: 2 additions & 1 deletion apps/web/playwright/dynamic-booking-pages.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { MembershipRole } from "@calcom/prisma/client";
import { test } from "./lib/fixtures";
import {
bookTimeSlot,
confirmReschedule,
doOnOrgDomain,
selectFirstAvailableTimeSlotNextMonth,
selectSecondAvailableTimeSlotNextMonth,
Expand Down Expand Up @@ -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");
});
Expand Down
17 changes: 10 additions & 7 deletions apps/web/playwright/event-types.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
gotoFirstEventType,
saveEventType,
selectFirstAvailableTimeSlotNextMonth,
submitAndWaitForResponse,
} from "./lib/testUtils";

test.describe.configure({ mode: "parallel" });
Expand Down Expand Up @@ -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 }) => {
Expand All @@ -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");

Expand Down Expand Up @@ -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);

Expand All @@ -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);

Expand Down Expand Up @@ -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");

Expand Down
8 changes: 5 additions & 3 deletions apps/web/playwright/fixtures/bookings.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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 (
Expand All @@ -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: {
Expand Down
8 changes: 4 additions & 4 deletions apps/web/playwright/fixtures/regularBookings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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");
Expand Down
Loading

0 comments on commit f52497f

Please sign in to comment.