CU-19: Embeddable booking widget (inline / popup / badge)
Priority: P3 Accounts/sessions: P1 host (ravikantguptaofficial@gmail.com) logged into Calendo dashboard; an external/data-URI test page that the agent constructs in-browser. No host-email login is required to render the widgets; an email check is optional (L3) and uses a P1 plus-alias invitee. Parallel-safe: true (does not touch global availability; only reads the existing booking page and creates at most one disposable test booking) Exclusive (rewrites global host availability?): No Estimated time: 25 minutes L3 reality checks: None required. One OPTIONAL L3 email check (the confirmation email for the booking completed through the embedded iframe) is described and may be skipped without failing the suite.
Goal
This suite proves that a Calendo host can drop a single snippet onto a completely unrelated third-party website and have a working booking experience appear there, in three forms: an inline scheduler (full booking page rendered in an iframe in the page body), a popup button (a button that opens the booking page in a modal overlay), and a floating badge (a fixed bottom-right "Book a meeting" pill that opens the same modal). It further proves the cross-origin plumbing works: the embed scripts are served with Access-Control-Allow-Origin: *, the booking page is iframable (no X-Frame-Options: DENY), the embed=1 chrome-stripping flag is applied, a real booking can be completed inside the embedded iframe with the confirmation rendering inside the embed, and the booking page fires a calendo:booking-confirmed postMessage to the parent window that the embed script re-dispatches as a DOM event. This matters because the embed widget is how Calendo reaches invitees who never visit calendo.dev directly — it is the product's distribution surface on customer websites.
Preconditions
- P1 dashboard session is active. Open https://calendo.dev/dashboard/ — the app layout must load (sidebar visible,
#welcomeHeaderpresent) WITHOUT a login screen. If a login/email-password screen appears, STOP and FLAG this as a precondition failure per00-setup-preconditions.md. Do NOT perform a cold Google/password login mid-run. - P1 has a published booking page with at least one bookable event type. The dashboard overview must show a booking link in
#bookingLinkValueand at least one event type in#overviewEventTypes. If there is no event type or no slug, FLAG it — the embed has nothing to render. Do not improvise a new event type unless the broader run plan explicitly told you to (creating event types is owned by other suites). - P1 has availability with at least one open future day (so the embedded calendar shows a
.calendar-day.available). If the embedded calendar shows no available days at all, the render checks (inline/popup/badge appearing) still pass, but the "complete a booking through the embed" step cannot run — record it as blocked, not failed, and note it in Manual residue. - The browser agent can load a data: URI or about:blank page and inject HTML, OR can reach the public demo page https://calendo.dev/docs/embed.html. Note: the demo page at
/docs/embed.htmlcontains only static mockups of the widgets (.demo-badge,.demo-popup-btn, fake slot chips) — it does NOT load a live widget. So it is useful only as a visual reference, not as a live embed host. The live external host MUST be constructed by the agent (see Test data). If the agent's environment cannot create or load any external HTML host page at all, document the entire "external page render" portion as Manual residue and run only the dashboard-snippet-generation steps (steps 1-7).
Test data
- RUNID: pick a fresh UTC token at execution time, e.g.
20260601-1530. Use it everywhere below. - HOST_SLUG: read at runtime from the dashboard (do not hardcode). It is the value after
user=in#bookingLinkValue, e.g. forhttps://calendo.dev/booking/?user=ravi-gthe slug isravi-g. - Booking page path URL (what the inline/popup snippets use):
https://calendo.dev/booking/<HOST_SLUG>— the worker rewrites this to/booking/?user=<HOST_SLUG>. - Invitee for the optional completed booking: Name
Embed Test <RUNID>; Emailravikantguptaofficial+inv-<RUNID>@gmail.com(a P1 plus-alias so any confirmation email lands in the P1 Gmail inbox and is searchable byinv-<RUNID>). - External host page: the agent builds a minimal HTML document (loaded via a
data:text/htmlURI, anabout:blankdocument the agent writes into, or any scratch page the agent can author) titledEmbed Host <RUNID>. It must embed the three snippets EXACTLY as copied from the dashboard (see Steps). Because the page origin is NOT calendo.dev, this also exercises the cross-origin path.
Steps
Order rationale: Steps 1-7 generate and capture the three real snippets from the dashboard (source of truth for what a customer would paste). Steps 8-12 build the external host and verify each render mode in isolation. Steps 13-17 complete a real booking through the embedded iframe and verify in-embed confirmation + the
postMessageevent. Steps 18-19 verify the cross-origin transport headers. Doing snippet capture first guarantees the host page uses the app's actual generated code, not invented markup.
- Action: Go to https://calendo.dev/dashboard/ and confirm you are logged in (sidebar visible,
#welcomeHeaderpresent). On the Overview tab, locate the booking link field (#bookingLinkValue). Read its text. Expect: It readshttps://calendo.dev/booking/?user=<something>. Record the part afteruser=as HOST_SLUG. [L2]- Capture screenshot:
cu19-01-dashboard-overview-bookinglink
- Capture screenshot:
- Action: Open the Settings tab (left sidebar link with
data-tab="settings"), then click the Branding settings sub-tab (button labeled "Branding", which callsswitchSettingsTab(this,'branding'); panel#settingsTabBranding). Scroll to the card titled "Embed on Your Website" (.settings-card-titletext "Embed on Your Website"). Expect: The card shows three toggle buttons — Inline (.embedTabBtn[data-embed="inline"], active by default), Popup Button (.embedTabBtn[data-embed="popup"]), Floating Badge (.embedTabBtn[data-embed="badge"]) — a code block (#embedCodeBlock), a Copy Code button (#copyEmbedBtn), and a live Preview (#embedPreview). [L1]- Capture screenshot:
cu19-02-embed-card-inline-default
- Capture screenshot:
- Action: With Inline selected, read the code in
#embedCodeBlock. Expect: It is exactly two lines of the form:- Capture screenshot:
cu19-03-inline-snippet-and-preview
- Capture screenshot:
- Action: Click the Popup Button toggle (
.embedTabBtn[data-embed="popup"]). Read#embedCodeBlock. Expect: Snippet becomes:- Capture screenshot:
cu19-04-popup-snippet-and-preview
- Capture screenshot:
- Action: Click the Floating Badge toggle (
.embedTabBtn[data-embed="badge"]). Read#embedCodeBlock. Expect: Snippet becomes a single line:- Capture screenshot:
cu19-05-badge-snippet-and-preview
- Capture screenshot:
- Action: Click Copy Code (
#copyEmbedBtn). Expect: The button gives visual feedback that the code was copied (e.g. a toast or a temporary "Copied" state). This confirms the copy affordance works; treat the recorded snippet text from steps 3-5 as the source of truth for the host page regardless of clipboard access. [L1]
- Action: Sanity-check the three snippet script URLs resolve. In the browser, open in turn: https://calendo.dev/embed/calendo-embed.js and https://calendo.dev/embed.js . Expect: Each returns JavaScript (not a 404 / HTML error).
calendo-embed.jscontains the stringsdata-mode,calendo-embed-overlay, andcalendo:booking-confirmed;embed.jscontainsdata-calendo-userandcalendo-badge. [L2]- Capture screenshot:
cu19-07-embed-scripts-load
- Capture screenshot:
- Action: Build the external host page. Create an HTML document (via
data:text/htmlURI or a scratch page the agent can author) with<title>Embed Host <RUNID></title>and a body that contains, in this order: a heading "Embed Host <RUNID>", then SNIPPET_INLINE, then a horizontal rule, then SNIPPET_POPUP, then a horizontal rule, then SNIPPET_BADGE. IMPORTANT: this page's origin must NOT be calendo.dev (that is the whole point of the cross-origin test). Load this page in the browser. Expect: The page loads with the heading visible and the browser does not block the calendo.dev scripts. [L1]- Capture screenshot:
cu19-08-host-page-loaded
- Capture screenshot:
- Action: On the host page, locate the inline embed region (the
<div id="calendo-inline">). Expect: The embed script has replaced/filled it with a bordered card (.calendo-embed-inline) containing an<iframe title="Calendo Booking">whosesrcishttps://calendo.dev/booking/<HOST_SLUG>?embed=1. The iframe renders the booking page (host name, event type list, or calendar) in chrome-stripped form (white background, tight padding fromembed=1). [L1]- Capture screenshot:
cu19-09-inline-iframe-rendered
- Capture screenshot:
- Action: Locate the popup embed (
<div id="calendo-popup">). Expect: The script rendered a button.calendo-embed-popup-btnreading "Book a Meeting" with a calendar icon. No iframe is visible yet (popup is lazy). [L1]
- Action: Click the "Book a Meeting" popup button. Expect: A full-screen overlay appears (
.calendo-embed-overlay) with a centered modal (.calendo-embed-modal) containing a close "×" button (.calendo-embed-modal-close) and an iframe whosesrcishttps://calendo.dev/booking/<HOST_SLUG>?embed=1showing the booking page. [L1]- Capture screenshot:
cu19-11-popup-modal-open - Action: Press
Escape(or click the "×" / the dark area outside the modal). Expect: The overlay closes and is removed from the page. [L1]
- Capture screenshot:
- Action: Look at the bottom-right corner of the host page for the floating badge (
.calendo-badge, injected by/embed.js). Expect: A fixed pill-shaped blue "Book a meeting" badge with a calendar icon is pinned to the bottom-right (position: fixed; bottom/right 24px). [L1]- Capture screenshot:
cu19-12-floating-badge - Action: Click the badge. Expect: A modal overlay (
.calendo-overlay>.calendo-modalwith.calendo-iframe) opens showing the booking page (iframesrchttps://calendo.dev/booking/?user=<HOST_SLUG>&embed=1). Close it with "×" or Escape and confirm it is removed. [L1] - Capture screenshot:
cu19-12b-badge-modal-open
- Capture screenshot:
- Action: (Begin the in-embed booking — uses the inline iframe from step 9 so the confirmation is clearly observed in the page body.) Inside the inline iframe, if an event type list is shown, click the first event card (
.event-card, name in.event-card-name). Ifdata-calendo-eventhad pinned an event you would skip straight to the calendar; here the inline snippet has no event pin, so expect the event list first. Expect: The view advances to the date/time picker (calendar grid with.calendar-daycells). [L1]
- Action: In the embedded calendar, click an available day (
.calendar-day.available). Then in the time slots panel (#time-slots-panel, slots are.time-slot) click the first available slot. Expect: The time slots panel becomes visible after picking a day; after clicking a slot the confirm form (#confirm-view) appears with#input-nameand#input-emailfields and a#btn-confirmbutton. If there are NO available days, abort this booking sub-flow, mark it blocked (precondition shortfall), and continue to step 18. [L1]- Capture screenshot:
cu19-14-embedded-confirm-form
- Capture screenshot:
- Action: In the embedded confirm form, fill
#input-namewithEmbed Test <RUNID>and#input-emailwithravikantguptaofficial+inv-<RUNID>@gmail.com. Click Confirm Booking (#btn-confirm). Expect: No validation error; the button shows a pending/confirming state. [L1]
- Action: Wait for the booking to complete. Expect: The confirmation view (
#confirmation-success-view) renders INSIDE the embedded iframe (a success heading and#confirmation-detailsshowing the event/date/time). The confirmation appears within the embed, not in a new tab or the top-level page. [L1]- Capture screenshot:
cu19-16-in-embed-confirmation
- Capture screenshot:
- Action: Verify the
postMessagecross-frame event fired. The booking page callswindow.parent.postMessage({type:'calendo:booking-confirmed', ...}, '*')when inside an iframe, andcalendo-embed.jsre-dispatches it on the hostwindowas acalendo:booking-confirmedCustomEvent. To observe this, BEFORE step 13 (or by reloading the host and re-booking) the agent should have a listener registered on the host page, e.g. injectwindow.addEventListener('calendo:booking-confirmed', e => { window.__calendoEvt = e.detail; });into the host page. After step 16, readwindow.__calendoEvt. Expect:window.__calendoEvtis a non-null object withtype === 'calendo:booking-confirmed'and abookingId(and likelyevent/date/time). If the agent cannot inject/inspect JS on the host page, record this as Manual residue (the in-embed confirmation in step 16 is the primary evidence the booking succeeded). [L1][L2]- Capture note:
cu19-17-postmessage-detail(the captured event detail object, or "could not inspect — manual residue")
- Capture note:
- Action: Verify the booking iframe is allowed to frame (X-Frame). Confirm steps 9/11/12 already proved the booking page rendered inside an iframe on a non-calendo.dev origin — that is the functional proof there is no
X-Frame-Options: DENYand no frame-blocking CSP. As corroboration, if the agent can read network response headers, inspect the response headers forhttps://calendo.dev/booking/?user=<HOST_SLUG>&embed=1. Expect:X-Frame-Optionsis absent orSAMEORIGIN, neverDENY; noContent-Security-Policy: frame-ancestors 'none'. [L2]
- Action: Verify CORS on the embed scripts. If the agent can read response headers, inspect the response headers for
https://calendo.dev/embed/calendo-embed.js. Expect:Access-Control-Allow-Origin: *,Content-Type: application/javascript; charset=utf-8, andCache-Control: public, max-age=3600. If headers cannot be inspected, the fact that the script executed from the non-calendo.dev host page (steps 9-12) is acceptable functional evidence — note it. [L2]- Capture screenshot:
cu19-19-embed-script-headers(or note "headers not inspectable; functional cross-origin load confirmed")
- Capture screenshot:
L3 reality checks
None required for a PASS. Optional (only if step 13-16 completed a real booking and time permits):
- Gmail (mail.google.com), logged in as P1. Search the inbox for
inv-<RUNID>. Expect: A booking-confirmation email addressed toravikantguptaofficial+inv-<RUNID>@gmail.comwhose subject references the booked event type and whose body contains the meeting date/time and manage/cancel links. This confirms the booking made through the embedded iframe produced a real downstream email exactly like a direct booking. Capture screenshotcu19-L3-gmail-confirmation. If absent within ~2 minutes, note it but do NOT fail the suite (email delivery is out of CU-19's required scope and is owned by the booking/email suites).
Cleanup
The only durable artifact this suite creates is the one test booking from steps 13-16 (the snippets, host page, and modals are ephemeral). Remove it so host calendars/inboxes stay clean:
- Go to https://calendo.dev/dashboard/ , open the Bookings tab (
data-tab="bookings"). Find the booking with invitee nameEmbed Test <RUNID>/ emailravikantguptaofficial+inv-<RUNID>@gmail.com. - Cancel it (open the booking, use the cancel action). Confirm it moves to Cancelled / disappears from upcoming. [L2]
- If the optional L3 booking created a Google Calendar event (only if P1's Google Calendar is connected), open calendar.google.com as P1, find the event at the booked date/time titled with the event-type name, and confirm cancellation removed it (or delete it manually). Note in evidence if a stale event remained.
- Discard the external host page (close the tab / data: URI). No dashboard-side cleanup is needed for the embed snippets — nothing was saved server-side by viewing them.
- Do NOT delete the host's booking page, event types, or availability — those are shared baseline owned by other suites.
Pass/Fail criteria
The run PASSES only if ALL of the following are true:
- The dashboard Settings > Branding "Embed on Your Website" card produced all three snippets with the exact expected structure: inline and popup use
/embed/calendo-embed.jswithdata-url="https://calendo.dev/booking/<HOST_SLUG>"anddata-mode="inline"/"popup"; badge uses/embed.jswithdata-calendo-user="<HOST_SLUG>" data-calendo-mode="badge". (steps 3-5) - Both embed scripts load as JavaScript (not 404/HTML) when fetched directly. (step 7)
- On the non-calendo.dev host page: inline mode renders the booking page in an in-body iframe with
?embed=1chrome stripped (step 9); popup mode renders a button that opens a modal iframe and closes cleanly (step 11); badge mode renders a fixed bottom-right pill that opens a modal iframe and closes cleanly (step 12). - The booking page actually rendered booking content inside an iframe on the foreign origin (functional proof of iframability + CORS), with no
X-Frame-Options: DENY. (steps 9/11/12, 18-19) - Either (a) a booking completed through the embedded iframe with the confirmation rendering INSIDE the embed (steps 13-16), OR (b) the booking sub-flow was correctly marked blocked because the host had zero available days (a documented precondition shortfall, not a defect). A booking that errors out or whose confirmation opens outside the embed is a FAIL.
- If JS inspection was possible, the
calendo:booking-confirmedevent was received on the host window with abookingId. If JS inspection was not possible, this is moved to Manual residue and does not fail the suite. (step 17) - The test booking was cancelled in Cleanup so no live booking/calendar event remains. (Cleanup)
The run FAILS if: any snippet is malformed or points at the wrong slug/script; an embed script 404s; any mode fails to render its widget on the foreign origin; the booking page refuses to frame (blank iframe due to X-Frame-Options: DENY/CSP); a completed booking's confirmation does not render inside the embed; or the test booking is left un-cancelled.
Evidence to capture
- Screenshots:
cu19-01overview booking link;cu19-02embed card (inline default);cu19-03/04/05the three snippets + previews;cu19-07embed scripts loading;cu19-08host page loaded;cu19-09inline iframe rendered;cu19-11popup modal open;cu19-12/cu19-12bfloating badge + its modal;cu19-14embedded confirm form;cu19-16in-embed confirmation;cu19-19embed script headers (or note). - Recorded values: RUNID, HOST_SLUG, the three verbatim snippets (SNIPPET_INLINE/POPUP/BADGE), the invitee email used.
- Note
cu19-17-postmessage-detail: the capturedcalendo:booking-confirmedevent detail object, or an explicit "could not inspect" note. - Header notes for steps 18-19 if response headers were inspectable (
X-Frame-Options,Access-Control-Allow-Origin,Content-Type,Cache-Control). - Cleanup confirmation: screenshot/note that the
Embed Test <RUNID>booking is cancelled.
Manual residue / cannot-verify
- Creating/loading a true foreign-origin host page depends on the agent's browser capabilities. If the agent cannot author a
data:/scratch HTML page or inject the snippets, the entire external-render portion (steps 8-17) is manual residue: a human should paste the three snippets onto a real test site and confirm render + booking. The static demo at https://calendo.dev/docs/embed.html is NOT a substitute — it shows only mockups, not a live widget. postMessageevent inspection (step 17) requires the agent to inject and read JS on the host page. If the agent cannot run arbitrary JS in the host page context, hand off verifying thecalendo:booking-confirmedparent event to a human with devtools.- Response-header inspection (steps 18-19) requires network-panel access. If unavailable, only functional (it-renders-in-an-iframe-cross-origin) evidence is captured; a human can confirm the exact
Access-Control-Allow-Origin: */ no-X-Frame-Options: DENYheaders with curl/devtools. data-calendo-event"direct to event type" pinning,data-calendo-color, anddata-calendo-textcustomization for the badge/popup snippets are documented in/docs/embed.htmlbut are NOT emitted by the current dashboard generator (it emits fixed defaults). Verifying those custom attributes is out of scope for this suite (TBD).- Email/Google-Calendar downstream effects of the in-embed booking are optional L3 here and are the primary responsibility of the booking-confirmation and calendar-sync suites.