fix: Namespaced react embeds not working with Floating and Element Click Popups (#13386)

* Fix embed queue

* Fix getCalApi namespace use

* Fixes

* Add more e2e

* Fix test

* Fix syntax error in generated code

* Make getCalApi argument optional

* Fix types

* Add more namespacing tests

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
This commit is contained in:
Hariom Balhara
2024-04-18 14:41:39 +05:30
committed by GitHub
co-authored by Joe Au-Yeung
parent be128181e2
commit 4f69c0e502
18 changed files with 467 additions and 92 deletions
+14 -5
View File
@@ -3,12 +3,17 @@ import type { Page } from "@playwright/test";
export const createEmbedsFixture = (page: Page) => {
return {
/**
* @deprecated Use gotoPlayground instead
* @deprecated
* Use 'gotoPlayground' instead, to navigate. It calls `addEmbedListeners` automatically.
*/
async addEmbedListeners(calNamespace: string) {
await page.addInitScript(
({ calNamespace }: { calNamespace: string }) => {
console.log("PlaywrightTest:", "Adding listener for __iframeReady on namespace:", calNamespace);
console.log(
"PlaywrightTest - InitScript:",
"Adding listener for __iframeReady on namespace:",
calNamespace
);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
window.eventsFiredStoreForPlaywright = window.eventsFiredStoreForPlaywright || {};
@@ -43,14 +48,18 @@ export const createEmbedsFixture = (page: Page) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
api = window.Cal.ns[calNamespace];
console.log("Using api from namespace-", { calNamespace, api });
}
if (!api) {
console.log(`namespace "${calNamespace}" not found yet - Trying again`);
setTimeout(tryAddingListener, 500);
return;
}
console.log("PlaywrightTest:", `Adding listener for __iframeReady on namespace:${calNamespace}`);
if (!api) {
throw new Error(`namespace "${calNamespace}" not found`);
}
api("on", {
action: "*",
callback: (e) => {
console.log("Playwright Embed Fixture: Received event", e);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
window.iframeReady = true; // Technically if there are multiple cal embeds, it can be set due to some other iframe. But it works for now. Improve it when it doesn't work
+1 -1
View File
@@ -16,7 +16,7 @@ global $post;extract(shortcode_atts(array(
'for' => $post->post_title,
), $atts));
if(empty($content)) $content='Embed Cal.com';
// TODO: How to reuse embed-snippet export here?
return '<script>(function (C, A, L){let p=function (a, ar){a.q.push(ar);}; let d=C.document; C.Cal=C.Cal || function (){let cal=C.Cal; let ar=arguments; if (!cal.loaded){cal.ns={}; cal.q=cal.q || []; d.head.appendChild(d.createElement("script")).src=A; cal.loaded=true;}if (ar[0]===L){const api=function (){p(api, arguments);}; const namespace=ar[1]; api.q=api.q || []; typeof namespace==="string" ? (cal.ns[namespace]=api) && p(api, ar) : p(cal, ar); return;}p(cal, ar);};})(window, "https://cal.com/embed.js", "init"); Cal("init") </script> <script>Cal("inline",{calLink: '.$content.'});</script>';
}
add_shortcode('cal', 'cal_shortcode');
+129 -26
View File
@@ -20,43 +20,126 @@
(() => {
const url = new URL(document.URL);
// Only run the example specified by only=, avoids distraction and faster to test.
const only = (window.only = url.searchParams.get("only"));
const only = (window.only = url.searchParams.get("only") || '');
const elementIdentifier = only !== "all" ? only.replace("ns:", "") : null;
if (elementIdentifier) {
location.hash = "#cal-booking-place-" + elementIdentifier + "-iframe";
}
})();
function addInlineEmbedInNewNamespaceWithoutReload(selector) {
Cal("init", "withoutReloadNamespace", {
debug: true,
calOrigin: "http://localhost:3000",
});
Cal.ns.withoutReloadNamespace("inline", {
elementOrSelector: selector,
calLink: "pro?case=addInlineEmbedInANewNamespaceWithoutReload",
});
};
</script>
<script>
(function (C, A, L) {
// Put the snippet in a function so that it can be re-executed for scenario testing
// TODO: How to reuse embed-snippet export here?
function embedSnippet() {
(function (C, A, L) {
let p = function (a, ar) {
a.q.push(ar);
};
let d = C.document;
C.Cal =
C.Cal ||
function () {
let cal = C.Cal;
let ar = arguments;
if (!cal.loaded) {
cal.ns = {};
cal.q = cal.q || [];
d.head.appendChild(d.createElement("script")).src = A;
cal.loaded = true;
}
if (ar[0] === L) {
const api = function () {
p(api, arguments);
};
const namespace = ar[1];
api.q = api.q || [];
typeof namespace === "string" ? (cal.ns[namespace] = api) && p(api, ar) : p(cal, ar);
return;
}
p(cal, ar);
};
})(window, "//localhost:3000/embed/embed.js", "init");
</script>
C.Cal =
C.Cal ||
function () {
let cal = C.Cal;
let ar = arguments;
if (!cal.loaded) {
cal.ns = {};
cal.q = cal.q || [];
d.head.appendChild(d.createElement("script")).src = A;
cal.loaded = true;
}
if (ar[0] === L) {
const api = function () {
p(api, arguments);
};
const namespace = ar[1];
api.q = api.q || [];
if (typeof namespace === "string"){
// Make sure that even after re-execution of the snippet, the namespace is not overridden
cal.ns[namespace] = cal.ns[namespace] || api;
p(cal.ns[namespace], ar);
p(cal, ['initNamespace', namespace])
} else p(cal, ar);
return;
}
p(cal, ar);
};
})(window, "//localhost:3000/embed/embed.js", "init");
}
embedSnippet();
</script>
<script>
function doubleInstallSnippetWithInlineEmbedWithNonDefaultNamespace(selector) {
Cal("init", "doubleInstall", {
debug: true,
calOrigin: "http://localhost:3000",
});
Cal.ns.doubleInstall("inline", {
elementOrSelector: selector,
calLink: "pro?case=doubleInstallSnippetWithInlineEmbedWithNonDefaultNamespace",
});
embedSnippet();
Cal("init", "doubleInstall", {
debug: true,
calOrigin: "http://localhost:3000",
});
Cal.ns.doubleInstall("inline", {
elementOrSelector: selector,
calLink: "pro",
});
}
function doubleInstallSnippetWithInlineEmbed(selector) {
Cal("init", {
debug: true,
calOrigin: "http://localhost:3000",
});
Cal("inline", {
elementOrSelector: selector,
calLink: "pro?case=doubleInstallSnippetWithInlineEmbed",
});
embedSnippet();
Cal("init", {
debug: true,
calOrigin: "http://localhost:3000",
});
Cal("inline", {
elementOrSelector: selector,
calLink: "pro",
});
}
function addTwoInlineEmbedWithTwoDifferentNamespacesWithDifferentInitConfig() {
Cal("init", "namespace-init-test-1", {
debug: true,
calOrigin: "http://localhost:3000",
});
Cal("init", "namespace-init-test-2", {
debug: true,
calOrigin: "http://127.0.0.1:3000",
});
Cal.ns['namespace-init-test-2']("inline", {
elementOrSelector: '#two-different-namespace-with-different-init-config-content-2',
calLink: "pro",
});
Cal.ns['namespace-init-test-1']("inline", {
elementOrSelector: '#two-different-namespace-with-different-init-config-content-1',
calLink: "pro",
});
}
</script>
<style>
body {
@@ -82,6 +165,25 @@
</style>
</head>
<body>
<div>
<button id="add-inline-embed-in-a-new-namespace-without-reload-button" onclick="addInlineEmbedInNewNamespaceWithoutReload('#add-inline-embed-in-a-new-namespace-without-reload-button-content')">Add Inline Embed in a new namespace without reload</button>
<div id="add-inline-embed-in-a-new-namespace-without-reload-button-content"></div>
</div>
<div>
<button id="double-install-snippet-with-inline-embed-default-namespace-button" onclick="doubleInstallSnippetWithInlineEmbed('#double-install-snippet-with-inline-embed-default-namespace-content')">Double Install Embed Snippet with inline embed - Default Namespace</button>
<div id="double-install-snippet-with-inline-embed-default-namespace-content"></div>
</div>
<div>
<button id="double-install-snippet-with-inline-embed-non-default-namespace-button" onclick="doubleInstallSnippetWithInlineEmbedWithNonDefaultNamespace('#double-install-snippet-with-inline-embed-non-default-namespace-content')">Double Install Embed Snippet with inline embed - Non Default Namespace</button>
<div id="double-install-snippet-with-inline-embed-non-default-namespace-content"></div>
</div>
<div>
<button id="two-different-namespace-with-different-init-config" onclick="addTwoInlineEmbedWithTwoDifferentNamespacesWithDifferentInitConfig()">Two different namespace with two different config</button>
<div id="two-different-namespace-with-different-init-config-area">
<div id="two-different-namespace-with-different-init-config-content-1"></div>
<div id="two-different-namespace-with-different-init-config-content-2"></div>
</div>
</div>
<span style="display: block"><a href="?color-scheme=dark">With Dark Color Scheme for the Page</a></span>
<span style="display: block"><a href="?nonResponsive">Non responsive version of this page here</a></span>
<span style="display: block"
@@ -175,6 +277,7 @@
<button data-cal-namespace="upcomingBookings" data-cal-config='{"theme":"dark"}' data-cal-link="bookings/upcoming">Show Upcoming Bookings</button>
</div> -->
</div>
<h2>Inline Embed Examples</h2>
<div id="namespaces-test">
<div class="inline-embed-container" id="cal-booking-place-default">
@@ -0,0 +1,67 @@
import { expect } from "@playwright/test";
// eslint-disable-next-line no-restricted-imports
import { test } from "@calcom/web/playwright/lib/fixtures";
import { getEmbedIframe } from "../lib/testUtils";
test.describe("Namespacing", () => {
test.describe("Inline Embed", () => {
test("Add inline embed using a namespace without reload", async ({ page, embeds }) => {
const calNamespace = "withoutReloadNamespace";
await embeds.gotoPlayground({ calNamespace, url: "/" });
await page.click("#add-inline-embed-in-a-new-namespace-without-reload-button");
const embedIframe = await getEmbedIframe({ calNamespace, page, pathname: "/pro" });
expect(embedIframe).toBeEmbedCalLink(calNamespace, embeds.getActionFiredDetails, {
pathname: "/pro",
searchParams: {
case: "addInlineEmbedInANewNamespaceWithoutReload",
},
});
});
test("Double install Embed Snippet with inline embed using a namespace", async ({ page, embeds }) => {
const calNamespace = "doubleInstall";
await embeds.gotoPlayground({ calNamespace, url: "/" });
await page.click("#double-install-snippet-with-inline-embed-non-default-namespace-button");
const embedIframe = await getEmbedIframe({ calNamespace, page, pathname: "/pro" });
expect(embedIframe).toBeEmbedCalLink(calNamespace, embeds.getActionFiredDetails, {
pathname: "/pro",
searchParams: {
case: "doubleInstallSnippetWithInlineEmbedWithNonDefaultNamespace",
},
});
expect(await page.locator("iframe").count()).toBe(1);
});
test("Double install Embed Snippet with inline embed without a namespace(i.e. default namespace)", async ({
page,
embeds,
}) => {
const calNamespace = "";
await embeds.gotoPlayground({ calNamespace, url: "/" });
await page.click("#double-install-snippet-with-inline-embed-default-namespace-button");
const embedIframe = await getEmbedIframe({ calNamespace, page, pathname: "/pro" });
expect(embedIframe).toBeEmbedCalLink(calNamespace, embeds.getActionFiredDetails, {
pathname: "/pro",
searchParams: {
case: "doubleInstallSnippetWithInlineEmbed",
},
});
expect(await page.locator("iframe").count()).toBe(1);
});
});
test("Different namespaces can have different init configs", async ({ page, embeds }) => {
await Promise.all([
embeds.addEmbedListeners("namespace-init-test-1"),
embeds.addEmbedListeners("namespace-init-test-2"),
]);
await page.goto("/");
await page.click("#two-different-namespace-with-different-init-config");
const namespace1IframeSrc = await page.locator("iframe").nth(0).getAttribute("src");
const namespace2IframeSrc = await page.locator("iframe").nth(1).getAttribute("src");
expect(namespace1IframeSrc).toContain("http://localhost:3000/pro");
expect(namespace2IframeSrc).toContain("http://127.0.0.1:3000/pro");
});
});
+39 -4
View File
@@ -211,7 +211,7 @@ export class Cal {
return instruction;
}
processQueue(queue: IArguments[]) {
processQueue(queue: Queue) {
queue.forEach((instruction) => {
this.processInstruction(instruction);
});
@@ -315,7 +315,7 @@ export class Cal {
}
}
constructor(namespace: string, q: IArguments[]) {
constructor(namespace: string, q: Queue) {
this.__config = {
// Use WEBAPP_URL till full page reload problem with website URL is solved
calOrigin: WEBAPP_URL,
@@ -413,9 +413,22 @@ class CalApi {
this.cal = cal;
}
/**
* If namespaceOrConfig is a string, config is available in config argument
* If namespaceOrConfig is an object, namespace is assumed to be default and config isn't provided
*/
init(namespaceOrConfig?: string | InitArgConfig, config = {} as InitArgConfig) {
let initForNamespace = "";
if (typeof namespaceOrConfig !== "string") {
config = (namespaceOrConfig || {}) as Config;
} else {
initForNamespace = namespaceOrConfig;
}
// Just in case 'init' instruction belongs to another namespace, ignore it
// Though it shouldn't happen normally as the snippet takes care of delegating the init instruction to appropriate namespace queue
if (initForNamespace !== this.cal.namespace) {
return;
}
CalApi.initializedNamespaces.push(this.cal.namespace);
@@ -426,6 +439,16 @@ class CalApi {
this.cal.__config = { ...this.cal.__config, ...restConfig };
}
/**
* Used when a non-default namespace is to be initialized
* It allows default queue to take care of instantiation of the non-default namespace queue
*/
initNamespace(namespace: string) {
// Creating this instance automatically starts processing the queue for the namespace
globalCal.ns[namespace].instance =
globalCal.ns[namespace].instance || new Cal(namespace, globalCal.ns[namespace].q);
}
/**
* It is an instruction that adds embed iframe inline as last child of the element
*/
@@ -458,6 +481,12 @@ class CalApi {
},
});
// If someone re-executes inline embed instruction, we want to ensure that duplicate inlineEl isn't added to the page per namespace
if (this.cal.inlineEl && document.body.contains(this.cal.inlineEl)) {
console.warn("Inline embed already exists. Ignoring this call");
return;
}
config = config || {};
if (typeof config.iframeAttrs === "string" || config.iframeAttrs instanceof Array) {
throw new Error("iframeAttrs should be an object");
@@ -766,13 +795,16 @@ class CalApi {
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type Queue = any[];
// This is a full fledged Cal instance but doesn't have ns property because it would be nested inside an ns instance already
export interface GlobalCalWithoutNs {
<T extends keyof SingleInstructionMap>(methodName: T, ...arg: Rest<SingleInstructionMap[T]>): void;
/** Marks that the embed.js is loaded. Avoids re-downloading it. */
loaded?: boolean;
/** Maintains a queue till the time embed.js isn't loaded */
q: IArguments[];
q: Queue;
/** If user registers multiple namespaces, those are available here */
instance?: Cal;
__css?: string;
@@ -800,8 +832,11 @@ export interface CalWindow extends Window {
const DEFAULT_NAMESPACE = "";
globalCal.instance = new Cal(DEFAULT_NAMESPACE, globalCal.q);
// Namespaces created before embed.js executes are instantiated here for old Embed Snippets which don't use 'initNamespace' instruction
// Snippets that support 'initNamespace' instruction don't really need this but it is okay if it's done because it's idempotent
for (const [ns, api] of Object.entries(globalCal.ns)) {
api.instance = new Cal(ns, api.q);
api.instance = api.instance ?? new Cal(ns, api.q);
}
/**
+14 -8
View File
@@ -6,10 +6,12 @@ const embedLibUrl = searchParams.get("embedLibUrl");
if (!bookerUrl || !embedLibUrl) {
throw new Error('Can\'t Preview: Missing "bookerUrl" or "embedLibUrl" query parameter');
}
// TODO: Reuse the embed code snippet from the embed-snippet package - Not able to use it because of circular dependency
// Install Cal Embed Code Snippet
(function (C, A, L) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const p = function (a: any, ar: any) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
const p = function (a, ar) {
a.q.push(ar);
};
const d = C.document;
@@ -17,6 +19,7 @@ if (!bookerUrl || !embedLibUrl) {
C.Cal ||
function () {
const cal = C.Cal;
// eslint-disable-next-line prefer-rest-params
const ar = arguments;
if (!cal.loaded) {
@@ -26,22 +29,25 @@ if (!bookerUrl || !embedLibUrl) {
cal.loaded = true;
}
if (ar[0] === L) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const api: { (): void; q?: any[] } = function () {
const api = function () {
// eslint-disable-next-line prefer-rest-params
p(api, arguments);
};
const namespace = ar[1];
api.q = api.q || [];
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
typeof namespace === "string" ? (cal.ns[namespace] = api) && p(api, ar) : p(cal, ar);
api.q = api.q || [];
if (typeof namespace === "string") {
// Make sure that even after re-execution of the snippet, the namespace is not overridden
cal.ns[namespace] = cal.ns[namespace] || api;
p(cal.ns[namespace], ar);
p(cal, ["initNamespace", namespace]);
} else p(cal, ar);
return;
}
p(cal, ar);
};
})(window, embedLibUrl, "init");
})(window, "//localhost:3000/embed/embed.js", "init");
const previewWindow = window;
previewWindow.Cal.fingerprint = process.env.EMBED_PUBLIC_EMBED_FINGER_PRINT as string;
@@ -0,0 +1,10 @@
<html>
<head>
<script src="https://unpkg.com/react@17/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js" crossorigin></script>
<script type="module" src="./element-click.tsx"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>
@@ -0,0 +1,31 @@
/**
* @fileoverview This file is an example file and tells how to use the element-click popup embed in a React application. This is also used by playwright e2e
*/
import { useEffect } from "react";
import * as React from "react";
import ReactDom from "react-dom";
import { getCalApi } from "./src/index";
const calNamespace = "element-click";
function App() {
useEffect(() => {
(async function () {
const cal = await getCalApi({
embedJsUrl: "http://localhost:3000/embed/embed.js",
namespace: calNamespace,
});
cal("ui", { styles: { branding: { brandColor: "#000000" } }, hideEventTypeDetails: false });
})();
}, []);
return (
<button
data-cal-namespace={calNamespace}
data-cal-link="pro"
data-cal-config='{"layout":"month_view", "theme":"dark"}'>
Click me
</button>
);
}
ReactDom.render(<App />, document.getElementById("root"));
+14 -9
View File
@@ -1,10 +1,6 @@
/* First make sure that you have installed the package */
/* If you are using yarn */
// yarn add @calcom/embed-react
/* If you are using npm */
// npm install @calcom/embed-react
/**
* @fileoverview This file is an example file and tells how to use floating popup button in a React application. This is also used by playwright e2e
*/
import { useEffect } from "react";
import * as React from "react";
import ReactDom from "react-dom";
@@ -14,8 +10,17 @@ import { getCalApi } from "./src/index";
function App() {
useEffect(() => {
(async function () {
const cal = await getCalApi("http://localhost:3000/embed/embed.js");
cal("floatingButton", { calLink: "teampro/abc", calOrigin: "http://localhost:3000" });
const cal = await getCalApi({
namespace: "floating",
embedJsUrl: "http://localhost:3000/embed/embed.js",
});
cal("floatingButton", {
calLink: "pro",
calOrigin: "http://localhost:3000",
config: {
theme: "dark",
},
});
cal("ui", { styles: { branding: { brandColor: "#000000" } }, hideEventTypeDetails: false });
})();
}, []);
+12 -6
View File
@@ -1,10 +1,16 @@
<html>
<head>
<script src="https://unpkg.com/react@17/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js" crossorigin></script>
<script type="module" src="./inline.tsx"></script>
</head>
<body>
<div id="root"></div>
<h1>Playground has following demos</h1>
<ul>
<li>
<a href="./inline.html">Inline</a>
</li>
<li>
<a href="./floating.html">Floating Button Popup</a>
</li>
<li>
<a href="./element-click.html">Element Click Popup</a>
</li>
</ul>
</body>
</html>
+10
View File
@@ -0,0 +1,10 @@
<html>
<head>
<script src="https://unpkg.com/react@17/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js" crossorigin></script>
<script type="module" src="./inline.tsx"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>
+8 -1
View File
@@ -1,3 +1,6 @@
/**
* @fileoverview This file is an example file and tells how to use the Cal component in a React application. This is also used by playwright e2e
*/
import * as React from "react";
import { useEffect } from "react";
import { useState } from "react";
@@ -7,7 +10,9 @@ import ReactDom from "react-dom";
// There are tests in test/built which verifiy that the types from built package are correctly generated and exported correctly.
import Cal, { getCalApi } from "./src/index";
const api = getCalApi();
const api = getCalApi({
namespace: "inline",
});
function App() {
const [, setLoaded] = useState(false);
@@ -37,6 +42,7 @@ function App() {
},
});
});
return () => {
api.then((api) => {
api("off", {
@@ -54,6 +60,7 @@ function App() {
<Cal
calOrigin="http://localhost:3000"
embedJsUrl="//localhost:3000/embed/embed.js"
namespace="inline"
style={{ width: "100%", height: "100%", overflow: "scroll" }}
calLink="pro"
config={{
@@ -3,22 +3,63 @@ import { expect } from "@playwright/test";
import { getEmbedIframe } from "@calcom/embed-core/playwright/lib/testUtils";
import { test } from "@calcom/web/playwright/lib/fixtures";
test.describe("Inline Embed", () => {
test("should verify that the iframe got created with correct URL", async ({
page,
embeds: { getActionFiredDetails, addEmbedListeners },
}) => {
//TODO: Do it with page.goto automatically
await addEmbedListeners("");
await page.goto("/");
const calNamespace = "";
const embedIframe = await getEmbedIframe({ calNamespace, page, pathname: "/pro" });
expect(embedIframe).toBeEmbedCalLink("", getActionFiredDetails, {
pathname: "/pro",
searchParams: {
theme: "dark",
},
test.describe("React Embed", () => {
test.describe("Inline", () => {
test("should verify that the iframe got created with correct URL - namespaced", async ({
page,
embeds,
}) => {
const calNamespace = "inline";
await embeds.gotoPlayground({ url: "/inline.html", calNamespace });
const embedIframe = await getEmbedIframe({ calNamespace, page, pathname: "/pro" });
expect(embedIframe).toBeEmbedCalLink("", embeds.getActionFiredDetails, {
pathname: "/pro",
searchParams: {
theme: "dark",
},
});
// expect(await page.screenshot()).toMatchSnapshot("react-component-inline.png");
});
});
test.describe("Floating button Popup", () => {
test("should verify that the iframe got created with correct URL - namespaced", async ({
page,
embeds,
}) => {
const calNamespace = "floating";
await page.waitForLoadState();
await embeds.gotoPlayground({ url: "/floating.html", calNamespace });
await page.click("text=Book my Cal");
const embedIframe = await getEmbedIframe({ calNamespace, page, pathname: "/pro" });
expect(embedIframe).toBeEmbedCalLink(calNamespace, embeds.getActionFiredDetails, {
pathname: "/pro",
searchParams: {
theme: "dark",
},
});
});
});
test.describe("Element Click Popup", () => {
test("should verify that the iframe got created with correct URL - namespaced", async ({
page,
embeds,
}) => {
const calNamespace = "element-click";
await embeds.gotoPlayground({ url: "/element-click.html", calNamespace });
await page.waitForLoadState();
await page.click("text=Click me");
const embedIframe = await getEmbedIframe({ calNamespace, page, pathname: "/pro" });
expect(embedIframe).toBeEmbedCalLink(calNamespace, embeds.getActionFiredDetails, {
pathname: "/pro",
searchParams: {
theme: "dark",
},
});
});
// expect(await page.screenshot()).toMatchSnapshot("react-component-inline.png");
});
});
+24 -5
View File
@@ -1,14 +1,32 @@
"use client";
import type { GlobalCal } from "@calcom/embed-core";
import type { GlobalCal, GlobalCalWithoutNs } from "@calcom/embed-core";
import EmbedSnippet from "@calcom/embed-snippet";
import Cal from "./Cal";
export const getCalApi = (embedJsUrl?: string): Promise<GlobalCal> =>
new Promise(function tryReadingFromWindow(resolve) {
EmbedSnippet(embedJsUrl);
const api = window.Cal;
export function getCalApi(options?: {
embedJsUrl?: string;
namespace?: string;
}): Promise<GlobalCal | GlobalCalWithoutNs>;
export function getCalApi(embedJsUrl: string): Promise<GlobalCal | GlobalCalWithoutNs>;
export function getCalApi(
optionsOrEmbedJsUrl?:
| {
embedJsUrl?: string;
namespace?: string;
}
| string
): Promise<GlobalCal | GlobalCalWithoutNs> {
const options =
typeof optionsOrEmbedJsUrl === "string" ? { embedJsUrl: optionsOrEmbedJsUrl } : optionsOrEmbedJsUrl ?? {};
const { namespace = "", embedJsUrl } = options;
return new Promise(function tryReadingFromWindow(resolve) {
const globalCal = EmbedSnippet(embedJsUrl);
globalCal("init", namespace);
const api = namespace ? globalCal.ns[namespace as keyof typeof globalCal.ns] : globalCal;
if (!api) {
setTimeout(() => {
tryReadingFromWindow(resolve);
@@ -17,5 +35,6 @@ export const getCalApi = (embedJsUrl?: string): Promise<GlobalCal> =>
}
resolve(api);
});
}
export default Cal;
+1 -1
View File
@@ -4,7 +4,7 @@ Vanilla JS embed snippet that is responsible to fetch @calcom/embed-core and thu
## Development
`yarn build` will generate dist/snippet.es.js
`yarn build` will generate dist/snippet.es.js. If you are going to test react embeds, make sure that you have built it so that they get the upto-date snippet
- which can be used as `<script type="module" src=...`
- You can also copy the appropriate portion of the code and install it directly as `<script>CODE_SUGGESTED_TO_BE_COPIED</script>`
+21 -6
View File
@@ -1,5 +1,7 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable @typescript-eslint/ban-ts-comment,prefer-rest-params,prefer-const */
import type { GlobalCal, GlobalCalWithoutNs } from "@calcom/embed-core";
import type { GlobalCal, GlobalCalWithoutNs, Queue } from "@calcom/embed-core";
// FIXME: embed-snippet is a published package and shouldn't import from @calcom/types which is unpublished
// This isn't a problem at the moment because embed-snippet isn't directly imported and embed-react which uses it doesn't depend on this
// eslint-disable-next-line no-restricted-imports
@@ -15,9 +17,19 @@ const WEBAPP_URL =
const EMBED_LIB_URL = import.meta.env.EMBED_PUBLIC_EMBED_LIB_URL || `${WEBAPP_URL}/embed/embed.js`;
type QueuePushArg = {
[k: number]: Queue[number];
};
/**
* When modifying this snippet, make sure to keep the snippets in following places in sync
* 1. EmbedTabs.tsx
* 2. embed-core/index.html
* 3. app-store/wordpress/plugin.php
*/
export default function EmbedSnippet(url = EMBED_LIB_URL) {
(function (C, A, L) {
let p = function (a: GlobalCalWithoutNs, ar: IArguments) {
let p = function (a: GlobalCalWithoutNs, ar: QueuePushArg) {
a.q.push(ar);
};
let d = C.document;
@@ -42,10 +54,13 @@ export default function EmbedSnippet(url = EMBED_LIB_URL) {
};
const namespace = ar[1];
api.q = api.q || [];
typeof namespace === "string"
? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
(cal.ns![namespace] = api) && p(api, ar)
: p(cal as GlobalCal, ar);
if (typeof namespace === "string") {
// Make sure that even after re-execution of the snippet, the namespace is not overridden
cal.ns![namespace] = cal.ns![namespace] || api;
p(cal.ns![namespace], ar);
// Inform the default namespace queue to initialize this namespace
p(cal as GlobalCal, ["initNamespace", namespace]);
} else p(cal as GlobalCal, ar);
return;
}
p(cal as GlobalCal, ar);
+12 -3
View File
@@ -30,13 +30,14 @@ export const Codes = {
const width = getDimension(previewState.inline.width);
const height = getDimension(previewState.inline.height);
const namespaceProp = `${namespace ? `namespace="${namespace}"` : ""}`;
const argumentForGetCalApi = getArgumentForGetCalApi(namespace);
return code`
import Cal, { getCalApi } from "@calcom/embed-react";
import { useEffect } from "react";
export default function MyApp() {
useEffect(()=>{
(async function () {
const cal = await getCalApi();
const cal = await getCalApi(${argumentForGetCalApi ? JSON.stringify(argumentForGetCalApi) : ""});
${uiInstructionCode}
})();
}, [])
@@ -58,13 +59,14 @@ export const Codes = {
uiInstructionCode: string;
namespace: string;
}) => {
const argumentForGetCalApi = getArgumentForGetCalApi(namespace);
return code`
import { getCalApi } from "@calcom/embed-react";
import { useEffect } from "react";
export default function MyApp() {
useEffect(()=>{
(async function () {
const cal = await getCalApi(${IS_SELF_HOSTED ? `"${embedLibUrl}"` : ""});
const cal = await getCalApi(${argumentForGetCalApi ? JSON.stringify(argumentForGetCalApi) : ""});
${getApiName({ namespace, mainApiName: "cal" })}("floatingButton", ${floatingButtonArg});
${uiInstructionCode}
})();
@@ -84,13 +86,14 @@ export const Codes = {
embedCalOrigin: string;
namespace: string;
}) => {
const argumentForGetCalApi = getArgumentForGetCalApi(namespace);
return code`
import { getCalApi } from "@calcom/embed-react";
import { useEffect } from "react";
export default function MyApp() {
useEffect(()=>{
(async function () {
const cal = await getCalApi(${IS_SELF_HOSTED ? `"${embedLibUrl}"` : ""});
const cal = await getCalApi(${argumentForGetCalApi ? JSON.stringify(argumentForGetCalApi) : ""});
${uiInstructionCode}
})();
}, [])
@@ -196,3 +199,9 @@ const code = (partsWithoutBlock: TemplateStringsArray, ...blocksOrVariables: str
}
return constructedCode.join("");
};
function getArgumentForGetCalApi(namespace: string) {
const libUrl = IS_SELF_HOSTED ? embedLibUrl : undefined;
const argumentForGetCalApi = namespace ? { namespace, embedLibUrl: libUrl } : { embedLibUrl: libUrl };
return argumentForGetCalApi;
}
+3 -1
View File
@@ -275,7 +275,9 @@ const getInstructionString = ({
function useGetEmbedSnippetString(namespace: string | null) {
const bookerUrl = useBookerUrl();
// TODO: Import this string from @calcom/embed-snippet
return `(function (C, A, L) { let p = function (a, ar) { a.q.push(ar); }; let d = C.document; C.Cal = C.Cal || function () { let cal = C.Cal; let ar = arguments; if (!cal.loaded) { cal.ns = {}; cal.q = cal.q || []; d.head.appendChild(d.createElement("script")).src = A; cal.loaded = true; } if (ar[0] === L) { const api = function () { p(api, arguments); }; const namespace = ar[1]; api.q = api.q || []; typeof namespace === "string" ? (cal.ns[namespace] = api) && p(api, ar) : p(cal, ar); return; } p(cal, ar); }; })(window, "${embedLibUrl}", "init");
// Right now the problem is that embed-snippet export is not minified and has comments which makes it unsuitable for giving it to users.
// If we can minify that during build time and then import the built code here, that could work
return `(function (C, A, L) { let p = function (a, ar) { a.q.push(ar); }; let d = C.document; C.Cal = C.Cal || function () { let cal = C.Cal; let ar = arguments; if (!cal.loaded) { cal.ns = {}; cal.q = cal.q || []; d.head.appendChild(d.createElement("script")).src = A; cal.loaded = true; } if (ar[0] === L) { const api = function () { p(api, arguments); }; const namespace = ar[1]; api.q = api.q || []; if(typeof namespace === "string"){cal.ns[namespace] = cal.ns[namespace] || api;p(cal.ns[namespace], ar);p(cal, ["initNamespace", namespace]);} else p(cal, ar); return;} p(cal, ar); }; })(window, "${embedLibUrl}", "init");
Cal("init", ${namespace ? `"${namespace}",` : ""} {origin:"${bookerUrl}"});
`;
}