From 4226d2262d457c52ff55b34ff6bc9cdc49972773 Mon Sep 17 00:00:00 2001 From: Keith Williams Date: Mon, 29 Dec 2025 10:05:50 -0300 Subject: [PATCH] fix: add postMessage listener to capture __iframeReady events early (#26258) This fixes a race condition in the embed E2E tests where the __iframeReady event could fire before the Cal.ns[namespace] API was ready to receive it. The fix adds a window.message listener immediately in the addInitScript that captures __iframeReady events directly from postMessage, which doesn't depend on the namespace API being ready. This ensures window.iframeReady is set even if the event fires before the Cal API listener is attached. Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- apps/web/playwright/filter-helpers.ts | 8 ++-- apps/web/playwright/fixtures/embeds.ts | 40 +++++++++++++++++++ apps/web/playwright/system-segments.e2e.ts | 2 +- .../playwright/team/team-invitation.e2e.ts | 32 +++++++++++---- 4 files changed, 69 insertions(+), 13 deletions(-) diff --git a/apps/web/playwright/filter-helpers.ts b/apps/web/playwright/filter-helpers.ts index 1c899692ca..3d73a613b0 100644 --- a/apps/web/playwright/filter-helpers.ts +++ b/apps/web/playwright/filter-helpers.ts @@ -114,7 +114,7 @@ export async function createFilterSegment( * Select a segment from the dropdown */ export async function selectSegment(page: Page, segmentName: string) { - await page.getByTestId("filter-segment-select").click(); + await page.getByTestId("filter-segment-select").first().click(); await page .locator('[data-testid="filter-segment-select-content"] [role="menuitem"]') @@ -126,7 +126,7 @@ export async function selectSegment(page: Page, segmentName: string) { * Open submenu of a certain segment */ export async function openSegmentSubmenu(page: Page, segmentName: string) { - await page.getByTestId("filter-segment-select").click(); + await page.getByTestId("filter-segment-select").first().click(); await page .locator('[data-testid="filter-segment-select-content"] [role="menuitem"]') @@ -183,7 +183,7 @@ export function locateSelectedSegmentName(page: Page, expectedName: string) { * Check if a system segment is visible in the dropdown */ export async function expectSystemSegmentVisible(page: Page, segmentName: string) { - await page.getByTestId("filter-segment-select").click(); + await page.getByTestId("filter-segment-select").first().click(); await expect( page.locator('[data-testid="filter-segment-select-content"]').getByText("Default") ).toBeVisible(); @@ -240,7 +240,7 @@ export async function renameSegment(page: Page, originalName: string, newName: s * Check if a segment group (like "Default" or "Personal") is visible */ export async function expectSegmentGroupVisible(page: Page, groupName: string) { - await page.getByTestId("filter-segment-select").click(); + await page.getByTestId("filter-segment-select").first().click(); await expect( page.locator('[data-testid="filter-segment-select-content"]').getByText(groupName) ).toBeVisible(); diff --git a/apps/web/playwright/fixtures/embeds.ts b/apps/web/playwright/fixtures/embeds.ts index f480b42879..0452c4afb7 100644 --- a/apps/web/playwright/fixtures/embeds.ts +++ b/apps/web/playwright/fixtures/embeds.ts @@ -17,6 +17,46 @@ export const createEmbedsFixture = (page: Page) => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore window.eventsFiredStoreForPlaywright = window.eventsFiredStoreForPlaywright || {}; + + // Add a postMessage listener immediately to capture __iframeReady events + // This avoids the race condition where the event fires before Cal.ns[namespace] is ready + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + if (!window.postMessageListenerAdded) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + window.postMessageListenerAdded = true; + window.addEventListener("message", (event) => { + // Filter for Cal.com embed messages + if ( + event.data && + typeof event.data === "object" && + event.data.originator === "CAL" && + event.data.type + ) { + const { type, namespace, data } = event.data; + console.log("PlaywrightTest postMessage:", `Received ${type} for namespace ${namespace}`); + + // Set iframeReady when we receive __iframeReady event for our namespace + if (type === "__iframeReady" && namespace === calNamespace) { + console.log("PlaywrightTest postMessage:", "Setting window.iframeReady = true"); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + window.iframeReady = true; + } + + // Store the event in our event store + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + const store = window.eventsFiredStoreForPlaywright; + if (store) { + const eventStore = (store[`${type}-${namespace}`] = store[`${type}-${namespace}`] || []); + eventStore.push({ type, namespace, fullType: `CAL:${namespace}:${type}`, data }); + } + } + }); + } + document.addEventListener("DOMContentLoaded", function tryAddingListener() { if (parent !== window) { // Firefox seems to execute this snippet for iframe as well. Avoid that. It must be executed only for parent frame. diff --git a/apps/web/playwright/system-segments.e2e.ts b/apps/web/playwright/system-segments.e2e.ts index c74914a230..11ac507727 100644 --- a/apps/web/playwright/system-segments.e2e.ts +++ b/apps/web/playwright/system-segments.e2e.ts @@ -46,7 +46,7 @@ test.describe("System Segments", () => { await navigateToBookings(page); - await page.getByTestId("filter-segment-select").click(); + await page.getByTestId("filter-segment-select").first().click(); await page .locator('[data-testid="filter-segment-select-content"] [role="menuitem"]') .filter({ hasText: "My Bookings" }) diff --git a/apps/web/playwright/team/team-invitation.e2e.ts b/apps/web/playwright/team/team-invitation.e2e.ts index fb934709eb..dd958496c1 100644 --- a/apps/web/playwright/team/team-invitation.e2e.ts +++ b/apps/web/playwright/team/team-invitation.e2e.ts @@ -33,7 +33,10 @@ test.describe("Team", () => { await page.waitForTimeout(500); // Add a small delay to ensure UI is fully loaded await page.getByTestId("new-member-button").click(); await page.locator('input[name="inviteUser"]').fill(invitedUserEmail); - await page.getByText(t("send_invite")).click(); + const submitPromise = page.waitForResponse("/api/trpc/teams/inviteMember?batch=1"); + await page.getByTestId("invite-new-member-button").click(); + const response = await submitPromise; + expect(response.status()).toBe(200); const inviteLink = await expectInvitationEmailToBeReceived( page, emails, @@ -119,7 +122,10 @@ test.describe("Team", () => { await page.waitForTimeout(500); // Add a small delay to ensure UI is fully loaded await page.getByTestId("new-member-button").click(); await page.locator('input[name="inviteUser"]').fill(invitedUserEmail); - await page.getByText(t("send_invite")).click(); + const submitPromise = page.waitForResponse("/api/trpc/teams/inviteMember?batch=1"); + await page.getByTestId("invite-new-member-button").click(); + const response = await submitPromise; + expect(response.status()).toBe(200); await expectInvitationEmailToBeReceived( page, emails, @@ -167,13 +173,17 @@ test.describe("Team", () => { await page.waitForTimeout(500); // Add a small delay to ensure UI is fully loaded await page.getByTestId("new-member-button").click(); await page.locator('input[name="inviteUser"]').fill(invitedMember.email); - await page.getByText(t("send_invite")).click(); + const submitPromise = page.waitForResponse("/api/trpc/teams/inviteMember?batch=1"); + await page.getByTestId("invite-new-member-button").click(); + const response = await submitPromise; + expect(response.status()).toBe(200); await invitedMember.apiLogin(); await page.goto(`/teams`); - await page.getByTestId(`accept-invitation-${team.id}`).click(); - const response = await page.waitForResponse("/api/trpc/teams/acceptOrLeave?batch=1"); - expect(response.status()).toBe(200); + const response2Promise = page.waitForResponse("/api/trpc/teams/acceptOrLeave?batch=1"); + await page.getByTestId(`accept-invitation-${team.id}`).first().click(); + const response2 = await response2Promise; + expect(response2.status()).toBe(200); await page.goto(`/event-types`); //ensure managed event-type is created for the invited member @@ -211,7 +221,10 @@ test.describe("Team", () => { await page.waitForTimeout(500); await page.getByTestId("new-member-button").click(); await page.locator('input[name="inviteUser"]').fill(invitedUser.email); - await page.getByText(t("send_invite")).click(); + const submitPromise = page.waitForResponse("/api/trpc/teams/inviteMember?batch=1"); + await page.getByTestId("invite-new-member-button").click(); + const response = await submitPromise; + expect(response.status()).toBe(200); inviteLink = await expectInvitationEmailToBeReceived( page, @@ -275,7 +288,10 @@ test.describe("Team", () => { await page.waitForTimeout(500); await page.getByTestId("new-member-button").click(); await page.locator('input[name="inviteUser"]').fill(invitedUser.email); - await page.getByText(t("send_invite")).click(); + const submitPromise = page.waitForResponse("/api/trpc/teams/inviteMember?batch=1"); + await page.getByTestId("invite-new-member-button").click(); + const response = await submitPromise; + expect(response.status()).toBe(200); inviteLink = await expectInvitationEmailToBeReceived( page,