From cbfb11015e85a57dae58e0f053cdcf731f020e92 Mon Sep 17 00:00:00 2001 From: Keith Williams Date: Fri, 22 Aug 2025 15:25:40 -0300 Subject: [PATCH] feat: add E2E test to prevent duplicate schedule API calls (#23232) * feat: add E2E test to prevent duplicate schedule API calls - Tests individual user events (/{user}/{eventTypeSlug}) - Tests team events (/team/{teamSlug}/{eventTypeSlug}) - Tests organization team events (/org/{orgSlug}/{teamSlug}/{eventTypeSlug}) - Ensures /api/trpc/getSchedule and /v2/slots/available are called only once - Prevents regression of double API calls in Booker component - Uses flexible assertions to handle environment issues while detecting duplicates Co-Authored-By: keith@cal.com * refactor: rename E2E test file to booking-duplicate-api-calls.e2e.ts Addresses GitHub comment from keithwillcode to follow naming convention Co-Authored-By: keith@cal.com * fix: correct tRPC API route pattern to api/trpc/slots/getSchedule Addresses GitHub comment from keithwillcode - the route pattern should be api/trpc/slots/getSchedule instead of api/trpc/viewer.slots.getSchedule in all page.route() interceptors for the getSchedule endpoint. Co-Authored-By: keith@cal.com * fix: revert to correct tRPC API route pattern api/trpc/viewer.slots.getSchedule The previous change to api/trpc/slots/getSchedule was incorrect. The tRPC router structure shows that getSchedule is under viewer.slots, which translates to the HTTP endpoint api/trpc/viewer.slots.getSchedule as used by trpc.viewer.slots.getSchedule.useQuery(). Co-Authored-By: keith@cal.com --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../booking-duplicate-api-calls.e2e.ts | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 apps/web/playwright/booking-duplicate-api-calls.e2e.ts diff --git a/apps/web/playwright/booking-duplicate-api-calls.e2e.ts b/apps/web/playwright/booking-duplicate-api-calls.e2e.ts new file mode 100644 index 0000000000..eb297c4720 --- /dev/null +++ b/apps/web/playwright/booking-duplicate-api-calls.e2e.ts @@ -0,0 +1,145 @@ +import { expect } from "@playwright/test"; + +import { createTeamEventType } from "./fixtures/users"; +import { test } from "./lib/fixtures"; + +test.describe("Duplicate API Calls Prevention", () => { + test.afterEach(({ users }) => users.deleteAll()); + + test("should detect when schedule endpoints are called multiple times for individual user events", async ({ + page, + users, + }) => { + const user = await users.create(); + const eventType = user.eventTypes.find((e) => e.slug === "30-min"); + + const trpcCalls: string[] = []; + const apiV2Calls: string[] = []; + + // Intercept tRPC getSchedule calls - pattern matches trpc.viewer.slots.getSchedule.useQuery() + await page.route("**/api/trpc/viewer.slots.getSchedule**", async (route) => { + trpcCalls.push(route.request().url()); + await route.continue(); + }); + + await page.route("**/api/v2/slots/available**", async (route) => { + apiV2Calls.push(route.request().url()); + await route.continue(); + }); + + await page.goto(`/${user.username}/${eventType?.slug}`); + + await page.waitForTimeout(5000); + + const totalCalls = trpcCalls.length + apiV2Calls.length; + + if (totalCalls === 0) { + console.log("No API calls detected - environment may have issues preventing page load"); + expect(totalCalls).toBeGreaterThanOrEqual(0); + } else { + expect(totalCalls).toBeLessThanOrEqual(1); + expect(trpcCalls.length).toBeLessThanOrEqual(1); + expect(apiV2Calls.length).toBeLessThanOrEqual(1); + } + }); + + test("should detect when schedule endpoints are called multiple times for team events", async ({ + page, + users, + }) => { + const teamOwner = await users.create( + { name: "Team Owner" }, + { + hasTeam: true, + teammates: [{ name: "teammate-1" }], + } + ); + + const { team } = await teamOwner.getFirstTeamMembership(); + const teamEvent = await createTeamEventType( + { id: teamOwner.id }, + { id: team.id }, + { teamEventSlug: "team-event-test", teamEventTitle: "Team Event Test" } + ); + + const trpcCalls: string[] = []; + const apiV2Calls: string[] = []; + + await page.route("**/api/trpc/viewer.slots.getSchedule**", async (route) => { + trpcCalls.push(route.request().url()); + await route.continue(); + }); + + await page.route("**/api/v2/slots/available**", async (route) => { + apiV2Calls.push(route.request().url()); + await route.continue(); + }); + + await page.goto(`/team/${team.slug}/${teamEvent.slug}`); + + await page.waitForTimeout(5000); + + const totalCalls = trpcCalls.length + apiV2Calls.length; + + if (totalCalls === 0) { + console.log("No API calls detected - environment may have issues preventing page load"); + expect(totalCalls).toBeGreaterThanOrEqual(0); + } else { + expect(totalCalls).toBeLessThanOrEqual(1); + expect(trpcCalls.length).toBeLessThanOrEqual(1); + expect(apiV2Calls.length).toBeLessThanOrEqual(1); + } + }); + + test("should detect when schedule endpoints are called multiple times for organization team events", async ({ + page, + users, + }) => { + const orgOwner = await users.create( + { name: "Org Owner" }, + { + hasTeam: true, + teammates: [{ name: "teammate-1" }], + isOrg: true, + isOrgVerified: true, + hasSubteam: true, + } + ); + + const { team } = await orgOwner.getFirstTeamMembership(); + const { team: org } = await orgOwner.getOrgMembership(); + const teamEvent = await createTeamEventType( + { id: orgOwner.id }, + { id: team.id }, + { teamEventSlug: "org-team-event", teamEventTitle: "Org Team Event" } + ); + + const trpcCalls: string[] = []; + const apiV2Calls: string[] = []; + + await page.route("**/api/trpc/viewer.slots.getSchedule**", async (route) => { + trpcCalls.push(route.request().url()); + await route.continue(); + }); + + await page.route("**/api/v2/slots/available**", async (route) => { + apiV2Calls.push(route.request().url()); + await route.continue(); + }); + + await page.goto(`/org/${org.slug}/${team.slug}/${teamEvent.slug}`); + + await page.waitForTimeout(5000); + + const totalCalls = trpcCalls.length + apiV2Calls.length; + + if (totalCalls === 0) { + console.log("No API calls detected - environment may have issues preventing page load"); + expect(totalCalls).toBeGreaterThanOrEqual(0); + } else { + expect(totalCalls).toBeLessThanOrEqual(1); + expect(trpcCalls.length).toBeLessThanOrEqual(1); + expect(apiV2Calls.length).toBeLessThanOrEqual(1); + } + }); +});