From df4a41127f189fc69c7e9df104ca8099cf44ab81 Mon Sep 17 00:00:00 2001 From: Hariom Balhara Date: Fri, 8 Apr 2022 22:29:08 +0530 Subject: [PATCH] Embed: Documentation TS errors Fix, IFrame Communication Tests, Updated documentation (#2432) --- apps/docs/package.json | 1 + apps/docs/pages/_app.tsx | 3 +- apps/docs/pages/integrations/embed.mdx | 28 ++++++++++ packages/embeds/embed-core/README.md | 1 + .../playwright/config/playwright.config.ts | 27 +++++++++- .../playwright/fixtures/fixtures.ts | 54 ++++++++++++++++++- .../playwright/tests/action-based.test.ts | 7 ++- .../playwright/tests/inline.test.ts | 9 +++- .../embeds/embed-core/src/embed-iframe.ts | 12 +++-- packages/embeds/embed-core/src/embed.ts | 4 +- 10 files changed, 132 insertions(+), 14 deletions(-) diff --git a/apps/docs/package.json b/apps/docs/package.json index a5f476af..c8381960 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -7,6 +7,7 @@ "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf .next", "dev": "PORT=4000 next", "lint": "next lint", + "type-check": "tsc --pretty --noEmit", "lint:report": "eslint . --format json --output-file ../../lint-results/docs.json", "start": "PORT=4000 next start", "build": "next build" diff --git a/apps/docs/pages/_app.tsx b/apps/docs/pages/_app.tsx index 590e3dfc..6c92686c 100644 --- a/apps/docs/pages/_app.tsx +++ b/apps/docs/pages/_app.tsx @@ -1,7 +1,8 @@ +import { AppProps } from "next/app"; import "nextra-theme-docs/style.css"; import "./style.css"; -export default function Nextra({ Component, pageProps }) { +export default function Nextra({ Component, pageProps }: AppProps) { return ; } diff --git a/apps/docs/pages/integrations/embed.mdx b/apps/docs/pages/integrations/embed.mdx index 694f41c0..c73d494c 100644 --- a/apps/docs/pages/integrations/embed.mdx +++ b/apps/docs/pages/integrations/embed.mdx @@ -178,3 +178,31 @@ Cal("preload", { calLink }); ``` - `calLink` - Cal Link that you want to embed e.g. john. Just give the username. No need to give the full URL [https://cal.com/john]() + +## Actions +You can listen to an action that occurs in embedded cal link as follows. You can think of them as DOM events. We are avoiding the term events to not confuse it with Cal Events. +```javascript +Cal("on", { + action: "ANY_ACTION_NAME", + callback: (e)=>{ + // `data` is properties for the event. + // `type` is the name of the action(You can also call it type of the action.) This would be same as "ANY_ACTION_NAME" except when ANY_ACTION_NAME="*" which listens to all the events. + // `namespace` tells you the Cal namespace for which the event is fired/ + const {data, type, namespace} = e.detail; + } +}) +``` + +Following are the list of supported actions. +- +| action | description | properties | +|----------------------|------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| eventTypeSelected | When user chooses an event-type from the listing. | eventType:object // Event Type that has been selected" | +| bookingSuccessful | When the booking is successfully done. It might not be confirmed. | confirmed: boolean; //Whether confirmation from organizer is pending or not

eventType: "Object for Event Type that has been booked";

date: string; // Date of Event

duration: number; //Duration of booked Event

organizer: object //Organizer details like name, timezone, email | +| linkReady | Tells that the link is ready to be shown now. | None | +| linkFailed | Fired if link fails to load | code: number; // Error Code

msg: string; //Human Readable msg

data: object // More details to debug the error | +| __iframeReady | It is fired when the embedded iframe is ready to communicate with parent snippet. This is mostly for internal use by Embed Snippet | None | +| __windowLoadComplete | Tells that window load for iframe is complete | None | +| __dimensionChanged | Tells that dimensions of the content inside the iframe changed. | iframeWidth:number, iframeHeight:number | + +_Actions that start with __ are internal._ \ No newline at end of file diff --git a/packages/embeds/embed-core/README.md b/packages/embeds/embed-core/README.md index 139f6af6..f223288e 100644 --- a/packages/embeds/embed-core/README.md +++ b/packages/embeds/embed-core/README.md @@ -40,6 +40,7 @@ Make `dist/embed.umd.js` servable on URL - Accessibility and UI/UX Issues - let user choose the loader for ModalBox - If website owner links the booking page directly for an event, should the user be able to go to events-listing page using back button ? + - Let user specify both dark and light theme colors. Right now the colors specified are for light theme. - Automation Tests - Run automation tests in CI diff --git a/packages/embeds/embed-core/playwright/config/playwright.config.ts b/packages/embeds/embed-core/playwright/config/playwright.config.ts index b0636b7f..2b8b443f 100644 --- a/packages/embeds/embed-core/playwright/config/playwright.config.ts +++ b/packages/embeds/embed-core/playwright/config/playwright.config.ts @@ -60,13 +60,23 @@ declare global { namespace PlaywrightTest { //FIXME: how to restrict it to Frame only interface Matchers { - toBeEmbedCalLink(expectedUrlDetails?: ExpectedUrlDetails): R; + toBeEmbedCalLink( + calNamespace: string, + getActionFiredDetails: Function, + expectedUrlDetails?: ExpectedUrlDetails + ): R; } } } expect.extend({ - async toBeEmbedCalLink(iframe: Frame, expectedUrlDetails: ExpectedUrlDetails = {}) { + async toBeEmbedCalLink( + iframe: Frame, + calNamespace: string, + //TODO: Move it to testUtil, so that it doesn't need to be passed + getActionFiredDetails: Function, + expectedUrlDetails: ExpectedUrlDetails = {} + ) { if (!iframe || !iframe.url) { return { pass: false, @@ -112,6 +122,19 @@ expect.extend({ }; } } + + const iframeReadyEventDetail = await getActionFiredDetails({ + calNamespace, + actionType: "__iframeReady", + }); + + if (!iframeReadyEventDetail) { + return { + pass: false, + message: () => `Iframe not ready to communicate with parent`, + }; + } + return { pass: true, message: () => `passed`, diff --git a/packages/embeds/embed-core/playwright/fixtures/fixtures.ts b/packages/embeds/embed-core/playwright/fixtures/fixtures.ts index 01b4dae3..a4a1ac45 100644 --- a/packages/embeds/embed-core/playwright/fixtures/fixtures.ts +++ b/packages/embeds/embed-core/playwright/fixtures/fixtures.ts @@ -1,3 +1,53 @@ -import { test as base } from "@playwright/test"; +import { test as base, Page } from "@playwright/test"; -export const test = base.extend({}); +interface Fixtures { + addEmbedListeners: (calNamespace: string) => Promise; + getActionFiredDetails: (a: { calNamespace: string; actionType: string }) => Promise; +} +export const test = base.extend({ + addEmbedListeners: async ({ page }: { page: Page }, use) => { + await use(async (calNamespace: string) => { + await page.addInitScript( + ({ calNamespace }: { calNamespace: string }) => { + //@ts-ignore + window.eventsFiredStoreForPlaywright = window.eventsFiredStoreForPlaywright || {}; + document.addEventListener("DOMContentLoaded", () => { + if (parent !== window) { + // Firefox seems to execute this snippet for iframe as well. Avoid that. It must be executed only for parent frame. + return; + } + console.log("PlaywrightTest:", "Adding listener for __iframeReady"); + //@ts-ignore + let api = window.Cal; + if (calNamespace) { + //@ts-ignore + api = window.Cal.ns[calNamespace]; + } + api("on", { + action: "*", + callback: (e: any) => { + //@ts-ignore + const store = window.eventsFiredStoreForPlaywright; + let eventStore = (store[`${e.detail.type}-${e.detail.namespace}`] = + store[`${e.detail.type}-${e.detail.namespace}`] || []); + eventStore.push(e.detail); + }, + }); + }); + }, + { calNamespace } + ); + }); + }, + getActionFiredDetails: async ({ page }, use) => { + await use(async ({ calNamespace, actionType }) => { + return await page.evaluate( + ({ actionType, calNamespace }) => { + //@ts-ignore + return window.eventsFiredStoreForPlaywright[`${actionType}-${calNamespace}`]; + }, + { actionType, calNamespace } + ); + }); + }, +}); 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 8a201d9d..2da83610 100644 --- a/packages/embeds/embed-core/playwright/tests/action-based.test.ts +++ b/packages/embeds/embed-core/playwright/tests/action-based.test.ts @@ -3,7 +3,9 @@ import { expect } from "@playwright/test"; import { test } from "../fixtures/fixtures"; import { todo, getEmbedIframe } from "../lib/testUtils"; -test("should open embed iframe on click", async ({ page }) => { +test("should open embed iframe on click", async ({ page, addEmbedListeners, getActionFiredDetails }) => { + const calNamespace = "prerendertestLightTheme"; + await addEmbedListeners(calNamespace); await page.goto("/?only=prerender-test"); let embedIframe = await getEmbedIframe({ page, pathname: "/free" }); expect(embedIframe).toBeFalsy(); @@ -11,7 +13,8 @@ test("should open embed iframe on click", async ({ page }) => { await page.click('[data-cal-link="free?light&popup"]'); embedIframe = await getEmbedIframe({ page, pathname: "/free" }); - expect(embedIframe).toBeEmbedCalLink({ + + expect(embedIframe).toBeEmbedCalLink(calNamespace, getActionFiredDetails, { pathname: "/free", }); }); diff --git a/packages/embeds/embed-core/playwright/tests/inline.test.ts b/packages/embeds/embed-core/playwright/tests/inline.test.ts index 450bb0e5..ac649100 100644 --- a/packages/embeds/embed-core/playwright/tests/inline.test.ts +++ b/packages/embeds/embed-core/playwright/tests/inline.test.ts @@ -3,10 +3,15 @@ import { expect, Frame } from "@playwright/test"; import { test } from "../fixtures/fixtures"; import { todo, getEmbedIframe } from "../lib/testUtils"; -test("Inline Iframe - Configured with Dark Theme", async ({ page }) => { +test("Inline Iframe - Configured with Dark Theme", async ({ + page, + getActionFiredDetails, + addEmbedListeners, +}) => { + await addEmbedListeners(""); await page.goto("/?only=ns:default"); const embedIframe = await getEmbedIframe({ page, pathname: "/pro" }); - expect(embedIframe).toBeEmbedCalLink({ + expect(embedIframe).toBeEmbedCalLink("", getActionFiredDetails, { pathname: "/pro", searchParams: { theme: "dark", diff --git a/packages/embeds/embed-core/src/embed-iframe.ts b/packages/embeds/embed-core/src/embed-iframe.ts index 5f1332ea..fd1d3f8b 100644 --- a/packages/embeds/embed-core/src/embed-iframe.ts +++ b/packages/embeds/embed-core/src/embed-iframe.ts @@ -198,6 +198,12 @@ function getNamespace() { const isEmbed = () => { const namespace = getNamespace(); + const _isValidNamespace = isValidNamespace(namespace); + if (parent !== window && !_isValidNamespace) { + log( + "Looks like you have iframed cal.com but not using Embed Snippet. Directly using an iframe isn't recommended." + ); + } return isValidNamespace(namespace); }; @@ -288,7 +294,7 @@ function keepParentInformedAboutDimensionChanges() { return; } if (!embedStore.windowLoadEventFired) { - sdkActionManager?.fire("windowLoadComplete", {}); + sdkActionManager?.fire("__windowLoadComplete", {}); } embedStore.windowLoadEventFired = true; @@ -308,7 +314,7 @@ function keepParentInformedAboutDimensionChanges() { knownIframeHeight = iframeHeight; numDimensionChanges++; // FIXME: This event shouldn't be subscribable by the user. Only by the SDK. - sdkActionManager?.fire("dimension-changed", { + sdkActionManager?.fire("__dimensionChanged", { iframeHeight, iframeWidth, isFirstTime, @@ -357,7 +363,7 @@ if (isBrowser) { if (!pageStatus || pageStatus == "200") { keepParentInformedAboutDimensionChanges(); - sdkActionManager?.fire("iframeReady", {}); + sdkActionManager?.fire("__iframeReady", {}); } else sdkActionManager?.fire("linkFailed", { code: pageStatus, diff --git a/packages/embeds/embed-core/src/embed.ts b/packages/embeds/embed-core/src/embed.ts index 08fdd153..2db2428a 100644 --- a/packages/embeds/embed-core/src/embed.ts +++ b/packages/embeds/embed-core/src/embed.ts @@ -330,7 +330,7 @@ export class Cal { // 1. Initial iframe width and height would be according to 100% value of the parent element // 2. Once webpage inside iframe renders, it would tell how much iframe height should be increased so that my entire content is visible without iframe scroll // 3. Parent window would check what iframe height can be set according to parent Element - this.actionManager.on("dimension-changed", (e) => { + this.actionManager.on("__dimensionChanged", (e) => { const { data } = e.detail; const iframe = this.iframe!; @@ -347,7 +347,7 @@ export class Cal { } }); - this.actionManager.on("iframeReady", (e) => { + this.actionManager.on("__iframeReady", (e) => { this.iframeReady = true; this.doInIframe({ method: "parentKnowsIframeReady", arg: undefined }); this.iframeDoQueue.forEach(({ method, arg }) => {