diff --git a/packages/embeds/embed-core/README.md b/packages/embeds/embed-core/README.md index 4e45fe0251..c8fc7f989e 100644 --- a/packages/embeds/embed-core/README.md +++ b/packages/embeds/embed-core/README.md @@ -18,7 +18,7 @@ yarn dev ## Running Tests -Ensure that the main App is running on port 3000 (e.g. yarn dx) already and then run the following command: +Ensure that the main App is running on port 3000 (e.g. yarn dx) already. Also ensure dev server for embed-core is running and then run the following command: Start the server on 3100 port ```bash @@ -31,6 +31,8 @@ And from another terminal you can run the following command to execute tests: yarn embed-tests-quick ``` +Note: `getEmbedIframe` and `addEmbedListeners` work as a team but they only support opening up embed in a fresh load. Opening an embed closing it and then opening another embed isn't supported yet. + ## Shipping to Production ```bash diff --git a/packages/embeds/embed-core/index.html b/packages/embeds/embed-core/index.html index 6a60af8e81..84bd6e647e 100644 --- a/packages/embeds/embed-core/index.html +++ b/packages/embeds/embed-core/index.html @@ -97,7 +97,10 @@ - + diff --git a/packages/embeds/embed-core/playwright/config/playwright.config.ts b/packages/embeds/embed-core/playwright/config/playwright.config.ts index a5786c9f2f..a5339a7f8b 100644 --- a/packages/embeds/embed-core/playwright/config/playwright.config.ts +++ b/packages/embeds/embed-core/playwright/config/playwright.config.ts @@ -137,9 +137,9 @@ expect.extend({ }; } } - + let iframeReadyCheckInterval; const iframeReadyEventDetail = await new Promise(async (resolve) => { - setInterval(async () => { + iframeReadyCheckInterval = setInterval(async () => { const iframeReadyEventDetail = await getActionFiredDetails({ calNamespace, actionType: "linkReady", @@ -150,6 +150,8 @@ expect.extend({ }, 500); }); + clearInterval(iframeReadyCheckInterval); + //At this point we know that window.initialBodyVisibility would be set as DOM would already have been ready(because linkReady event can only fire after that) const { visibility: visibilityBefore, diff --git a/packages/embeds/embed-core/playwright/fixtures/fixtures.ts b/packages/embeds/embed-core/playwright/fixtures/fixtures.ts index 29374b8711..52a9fff218 100644 --- a/packages/embeds/embed-core/playwright/fixtures/fixtures.ts +++ b/packages/embeds/embed-core/playwright/fixtures/fixtures.ts @@ -1,6 +1,6 @@ import { test as base, Page } from "@playwright/test"; -interface Fixtures { +export interface Fixtures { addEmbedListeners: (calNamespace: string) => Promise; getActionFiredDetails: (a: { calNamespace: string; actionType: string }) => Promise; } diff --git a/packages/embeds/embed-core/playwright/lib/testUtils.ts b/packages/embeds/embed-core/playwright/lib/testUtils.ts index 413df54293..f83f8e41b1 100644 --- a/packages/embeds/embed-core/playwright/lib/testUtils.ts +++ b/packages/embeds/embed-core/playwright/lib/testUtils.ts @@ -3,8 +3,10 @@ import { Page, Frame, test, expect } from "@playwright/test"; import prisma from "@calcom/prisma"; export function todo(title: string) { + // eslint-disable-next-line @typescript-eslint/no-empty-function, playwright/no-skipped-test test.skip(title, () => {}); } + export const deleteAllBookingsByEmail = async (email: string) => await prisma.booking.deleteMany({ where: { @@ -33,31 +35,41 @@ export const getBooking = async (bookingId: string) => { export const getEmbedIframe = async ({ page, pathname }: { page: Page; pathname: string }) => { // We can't seem to access page.frame till contentWindow is available. So wait for that. - await page.evaluate(() => { + const iframeReady = await page.evaluate(() => { return new Promise((resolve) => { - const iframe = document.querySelector(".cal-embed") as HTMLIFrameElement; - if (!iframe) { - resolve(false); - return; - } const interval = setInterval(() => { + const iframe = document.querySelector(".cal-embed") as HTMLIFrameElement | null; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - if (iframe.contentWindow && window.iframeReady) { + if (iframe && iframe.contentWindow && window.iframeReady) { clearInterval(interval); resolve(true); + } else { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + console.log("Iframe Status:", !!iframe, !!iframe?.contentWindow, window.iframeReady); } - }, 10); + }, 500); + + // A hard timeout if iframe isn't ready in that time. Avoids infinite wait + setTimeout(() => { + clearInterval(interval); + resolve(false); + }, 5000); }); }); - const embedIframe = page.frame("cal-embed"); - if (!embedIframe) { + if (!iframeReady) { return null; } + + // We just verified that iframeReady is true here, so obviously embedIframe is not null + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const embedIframe = page.frame("cal-embed")!; const u = new URL(embedIframe.url()); if (u.pathname === pathname + "/embed") { return embedIframe; } + console.log('Embed iframe url pathname match. Expected: "' + pathname + '/embed"', "Actual: " + u.pathname); return null; }; @@ -91,7 +103,7 @@ export async function bookFirstEvent(username: string, frame: Frame, page: Page) // It would also allow correct snapshot to be taken for current month. await frame.waitForTimeout(1000); expect(await page.screenshot()).toMatchSnapshot("availability-page-1.png"); - + const eventSlug = new URL(frame.url()).pathname; await selectFirstAvailableTimeSlotNextMonth(frame, page); await frame.waitForNavigation({ url(url) { @@ -104,10 +116,36 @@ export async function bookFirstEvent(username: string, frame: Frame, page: Page) await frame.fill('[name="email"]', "embed-user@example.com"); await frame.press('[name="email"]', "Enter"); const response = await page.waitForResponse("**/api/book/event"); - const responseObj = await response.json(); - const bookingId = responseObj.uid; + const booking = (await response.json()) as { uid: string; eventSlug: string }; + booking.eventSlug = eventSlug; + // Make sure we're navigated to the success page await expect(frame.locator("[data-testid=success-page]")).toBeVisible(); expect(await page.screenshot()).toMatchSnapshot("success-page.png"); - return bookingId; + + //NOTE: frame.click('body') won't work here. Because the way it works, it clicks on the center of the body tag which is an element inside the popup view and that won't close the popup + await frame.evaluate(() => { + // Closes popup - if it is a popup. If not a popup, it will just do nothing + document.body.click(); + }); + + return booking; +} + +export async function rescheduleEvent(username, frame, page) { + await selectFirstAvailableTimeSlotNextMonth(frame, page); + await frame.waitForNavigation({ + url(url) { + return url.pathname.includes(`/${username}/book`); + }, + }); + // --- fill form + await frame.press('[name="email"]', "Enter"); + await frame.click("[data-testid=confirm-reschedule-button]"); + const response = await page.waitForResponse("**/api/book/event"); + const responseObj = await response.json(); + const booking = responseObj.uid; + // Make sure we're navigated to the success page + await expect(frame.locator("[data-testid=success-page]")).toBeVisible(); + return booking; } diff --git a/packages/embeds/embed-core/playwright/tests/action-based.test.ts b/packages/embeds/embed-core/playwright/tests/action-based.test.ts index ef5ec4b5bc..f708cd3575 100644 --- a/packages/embeds/embed-core/playwright/tests/action-based.test.ts +++ b/packages/embeds/embed-core/playwright/tests/action-based.test.ts @@ -1,66 +1,131 @@ -import { expect } from "@playwright/test"; +import { expect, Page } from "@playwright/test"; -import { test } from "../fixtures/fixtures"; -import { todo, getEmbedIframe, bookFirstEvent, getBooking, deleteAllBookingsByEmail } from "../lib/testUtils"; +import { Fixtures, test } from "../fixtures/fixtures"; +import { + todo, + getEmbedIframe, + bookFirstEvent, + getBooking, + deleteAllBookingsByEmail, + rescheduleEvent, +} from "../lib/testUtils"; -test("should open embed iframe on click - Configured with light theme", async ({ - page, +async function bookFirstFreeUserEventThroughEmbed({ addEmbedListeners, + page, getActionFiredDetails, -}) => { - await deleteAllBookingsByEmail("embed-user@example.com"); - - const calNamespace = "prerendertestLightTheme"; +}: { + addEmbedListeners: Fixtures["addEmbedListeners"]; + page: Page; + getActionFiredDetails: Fixtures["getActionFiredDetails"]; +}) { + const embedButtonLocator = page.locator('[data-cal-link="free"]').first(); + await page.goto("/"); + // Obtain cal namespace from the element being clicked itself, so that addEmbedListeners always listen to correct namespace + const calNamespace = (await embedButtonLocator.getAttribute("data-cal-namespace")) || ""; await addEmbedListeners(calNamespace); - await page.goto("/?only=prerender-test"); - let embedIframe = await getEmbedIframe({ page, pathname: "/free" }); - expect(embedIframe).toBeFalsy(); + // Goto / again so that initScript attached using addEmbedListeners can work now. + await page.goto("/"); - await page.click('[data-cal-link="free?light&popup"]'); + await embedButtonLocator.click(); - embedIframe = await getEmbedIframe({ page, pathname: "/free" }); + const embedIframe = await getEmbedIframe({ page, pathname: "/free" }); await expect(embedIframe).toBeEmbedCalLink(calNamespace, getActionFiredDetails, { pathname: "/free", }); - expect(await page.screenshot()).toMatchSnapshot("event-types-list.png"); if (!embedIframe) { throw new Error("Embed iframe not found"); } - const bookingId = await bookFirstEvent("free", embedIframe, page); - const booking = await getBooking(bookingId); + const booking = await bookFirstEvent("free", embedIframe, page); + return booking; +} - expect(booking.attendees.length).toBe(1); - await deleteAllBookingsByEmail("embed-user@example.com"); -}); - -todo("Floating Button Test with Dark Theme"); - -todo("Floating Button Test with Light Theme"); - -todo("Add snapshot test for embed iframe"); - -test("should open Routing Forms embed on click", async ({ - page, - addEmbedListeners, - getActionFiredDetails, -}) => { - await deleteAllBookingsByEmail("embed-user@example.com"); - - const calNamespace = "routingFormAuto"; - await addEmbedListeners(calNamespace); - await page.goto("/?only=prerender-test"); - let embedIframe = await getEmbedIframe({ page, pathname: "/forms/948ae412-d995-4865-875a-48302588de03" }); - expect(embedIframe).toBeFalsy(); - await page.click( - `[data-cal-namespace=${calNamespace}][data-cal-link="forms/948ae412-d995-4865-875a-48302588de03"]` - ); - embedIframe = await getEmbedIframe({ page, pathname: "/forms/948ae412-d995-4865-875a-48302588de03" }); - if (!embedIframe) { - throw new Error("Routing Form embed iframe not found"); - } - await expect(embedIframe).toBeEmbedCalLink(calNamespace, getActionFiredDetails, { - pathname: "/forms/948ae412-d995-4865-875a-48302588de03", +test.describe("Popup Tests", () => { + test.afterEach(async () => { + await deleteAllBookingsByEmail("embed-user@example.com"); + }); + test("should open embed iframe on click - Configured with light theme", async ({ + page, + addEmbedListeners, + getActionFiredDetails, + }) => { + await deleteAllBookingsByEmail("embed-user@example.com"); + + const calNamespace = "prerendertestLightTheme"; + await addEmbedListeners(calNamespace); + await page.goto("/?only=prerender-test"); + let embedIframe = await getEmbedIframe({ page, pathname: "/free" }); + expect(embedIframe).toBeFalsy(); + + await page.click('[data-cal-link="free?light&popup"]'); + + embedIframe = await getEmbedIframe({ page, pathname: "/free" }); + + await expect(embedIframe).toBeEmbedCalLink(calNamespace, getActionFiredDetails, { + pathname: "/free", + }); + expect(await page.screenshot()).toMatchSnapshot("event-types-list.png"); + if (!embedIframe) { + throw new Error("Embed iframe not found"); + } + const { uid: bookingId } = await bookFirstEvent("free", embedIframe, page); + const booking = await getBooking(bookingId); + + expect(booking.attendees.length).toBe(1); + await deleteAllBookingsByEmail("embed-user@example.com"); + }); + + test("should be able to reschedule", async ({ page, addEmbedListeners, getActionFiredDetails }) => { + const booking = await test.step("Create a booking", async () => { + return await bookFirstFreeUserEventThroughEmbed({ + page, + addEmbedListeners, + getActionFiredDetails, + }); + }); + + await test.step("Reschedule the booking", async () => { + await addEmbedListeners("popupReschedule"); + await page.goto(`/?popupRescheduleId=${booking.uid}`); + await page.click('[data-cal-namespace="popupReschedule"]'); + + const embedIframe = await getEmbedIframe({ page, pathname: booking.eventSlug }); + if (!embedIframe) { + throw new Error("Embed iframe not found"); + } + await rescheduleEvent("free", embedIframe, page); + }); + }); + + todo("Floating Button Test with Dark Theme"); + + todo("Floating Button Test with Light Theme"); + + todo("Add snapshot test for embed iframe"); + + test("should open Routing Forms embed on click", async ({ + page, + addEmbedListeners, + getActionFiredDetails, + }) => { + await deleteAllBookingsByEmail("embed-user@example.com"); + + const calNamespace = "routingFormAuto"; + await addEmbedListeners(calNamespace); + await page.goto("/?only=prerender-test"); + let embedIframe = await getEmbedIframe({ page, pathname: "/forms/948ae412-d995-4865-875a-48302588de03" }); + expect(embedIframe).toBeFalsy(); + await page.click( + `[data-cal-namespace=${calNamespace}][data-cal-link="forms/948ae412-d995-4865-875a-48302588de03"]` + ); + embedIframe = await getEmbedIframe({ page, pathname: "/forms/948ae412-d995-4865-875a-48302588de03" }); + if (!embedIframe) { + throw new Error("Routing Form embed iframe not found"); + } + await expect(embedIframe).toBeEmbedCalLink(calNamespace, getActionFiredDetails, { + pathname: "/forms/948ae412-d995-4865-875a-48302588de03", + }); + await expect(embedIframe.locator("text=Seeded Form - Pro")).toBeVisible(); }); - await expect(embedIframe.locator("text=Seeded Form - Pro")).toBeVisible(); });