Embed: Documentation TS errors Fix, IFrame Communication Tests, Updated documentation (#2432)

This commit is contained in:
Hariom Balhara 2022-04-08 22:29:08 +05:30 committed by GitHub
parent eceba51020
commit df4a41127f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 132 additions and 14 deletions

View File

@ -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"

View File

@ -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 <Component {...pageProps} />;
}

View File

@ -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 <br/><br/>eventType: "Object for Event Type that has been booked"; <br/><br/>date: string; // Date of Event <br/><br/>duration: number; //Duration of booked Event <br/><br/>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 <br/><br/>msg: string; //Human Readable msg <br/><br/>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._

View File

@ -40,6 +40,7 @@ Make `dist/embed.umd.js` servable on URL <http://cal.com/embed.js>
- 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

View File

@ -60,13 +60,23 @@ declare global {
namespace PlaywrightTest {
//FIXME: how to restrict it to Frame only
interface Matchers<R> {
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`,

View File

@ -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<void>;
getActionFiredDetails: (a: { calNamespace: string; actionType: string }) => Promise<any>;
}
export const test = base.extend<Fixtures>({
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 }
);
});
},
});

View File

@ -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",
});
});

View File

@ -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",

View File

@ -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,

View File

@ -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 }) => {