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