✓ The required accounts are already signed in in this browser — you do NOT need any passwords
To open Calendo, go to https://calendo.dev/auth/login.html and click "Sign in with Google" (Gmail) or "Sign in with Microsoft" (Outlook), then choose the account this suite needs. The sessions are already authenticated, so it signs straight in.
  • Primary host (P1): ravikantguptaofficial@gmail.com — "Sign in with Google"
  • Teammate / 2nd account (P3): everythingaichannelemail@gmail.com — "Sign in with Google"
  • Outlook host: ravikant0909@outlook.com — "Sign in with Microsoft"
Pick a fresh RUNID (a timestamp, e.g. 20260603-1400) and embed it in everything you create. This suite is safe to run in parallel with the other suites — if a time slot was just taken by another run, simply choose the next available one. Clean up what you create at the end, then report results using RESULTS-TEMPLATE.

CU-11: Public booking page UX: timezones, calendar nav, empty-state, QR, mobile

Priority: P1 Accounts/sessions: Anonymous invitee experience for most steps (no login needed to view/book a public page). One sub-section (QR code) requires the P1 dashboard session (ravikantguptaofficial@gmail.com, signed into Calendo via Sign in with Google) because — see the IMPORTANT note in Goal — the QR feature lives in the dashboard, not on the public confirmation page. Parallel-safe: Yes. This suite only reads the public page, navigates the calendar, changes the timezone client-side, and makes at most ONE throwaway booking. It does not edit global host availability or event types. Exclusive (rewrites global host availability?): No. Estimated time: 22 minutes L3 reality checks: None required. One optional booking is made to reach the confirmation screen; its L3 calendar/email reality is NOT in scope for this suite (covered by CU-01/CU-02). This suite stops at the in-UI confirmation screen and cleans the booking up.

Goal

This suite proves the invitee-facing booking page — the screen the overwhelming majority of Calendo users actually see — is usable, correct, and not broken. It verifies the timezone picker actually shifts displayed slot times, the month calendar navigates forward/back and correctly marks days available vs unavailable, that empty availability shows a clear human message instead of a blank page, that the booking page honors the host's brand color, that the layout collapses sensibly on a 375px phone, and that a happy-path booking reaches the animated "Booking Confirmed!" screen with working add-to-calendar buttons. It also verifies the QR-code feature renders and is downloadable.

IMPORTANT GROUNDING NOTE (read before executing): The original coverage brief said "QR code on the confirmation page." That is incorrect against the current code. In /Users/ravf/projects/calendo/public/booking/index.html the confirmation view (#confirmation-success-view) contains add-to-calendar buttons and cancel/reschedule links but NO QR code. The QR code is a HOST dashboard feature (#qrLinkBtn in the booking-link bar, and per-event showEventQR() buttons, rendered into the #qrModal / #qrCanvas with a downloadQR() button). This runbook therefore tests QR where it really lives (the dashboard, as P1) and the agent must FLAG if it ever finds a QR element on the public confirmation page or fails to find one on the dashboard. Do not invent a QR element on the public page.

Preconditions

Test data

Steps

Section A — Resolve the host slug and base booking URL (host session)

  1. Action: Open the dashboard (https://calendo.dev/dashboard/). If it redirects to a login page, STOP this section, FLAG precondition failure, and jump to Section B using a known HOST_SLUG from 00-setup-preconditions.md if one is documented; otherwise skip QR (Section F) and proceed with the rest using that documented slug. -> Expect: The dashboard loads showing the left sidebar with tabs (data-tab values: overview, bookings, calendar, analytics, availability, routing-forms, contacts, settings, event-types) and the Overview tab is active. -> [L1]
  2. Action: On the Overview tab, find the public booking link displayed in the share bar (the value element #bookingLinkValue). Read its full text. -> Expect: A full URL like https://calendo.dev/booking/?user=<HOST_SLUG>. Record HOST_SLUG (everything after ?user=). -> [L2]
    • Capture screenshot: A2-dashboard-booking-link.
  3. Action: Construct BASE_BOOKING_URL = https://calendo.dev/booking/?user=<HOST_SLUG>&event=<EVENT_SLUG> using the slug from Section A and EVENT_SLUG=30-minute-meeting. -> Expect: A well-formed URL you will reuse below. -> [L1]

Section B — Timezone picker shifts displayed slot times

  1. Action: In a fresh anonymous browser tab open BASE_BOOKING_URL. -> Expect: The booking view (#booking-view) becomes visible, showing the left event sidebar, a month calendar (#calendar), and a timezone selector (#timezone-select) in the calendar header. The step indicator (#step-indicator) reads "Select Time -> Your Details -> Confirmation". -> [L1]
  2. Action: Read the current value of the timezone selector (#timezone-select). If it is not already America/Los_Angeles, select the option "America/Los Angeles" (value America/Los_Angeles). -> Expect: The selector shows America/Los Angeles. The calendar reloads availability for that timezone (a brief load). The selector contains many options (more than 1) including UTC, Europe/London, Asia/Kolkata, etc. -> [L1]
  3. Action: Click the first calendar day that is selectable (.calendar-day.available); if the current month has none, click the next-month arrow (.calendar-nav-btn, the LAST one = right arrow ) up to 3 times until an available day appears, then click it. -> Expect: The time-slots panel (#time-slots-panel) becomes visible. It shows a date header (#time-slots-date), a timezone label (#time-slots-tz-label) reading "Times in America/Los Angeles", and one or more time-slot buttons (.time-slot). -> [L1]
    • Capture screenshot: B6-slots-LA.
  4. Action: Record the exact text of the FIRST time-slot button and the SELECTED day number, so you can compare after switching timezone. -> Expect: A 12-hour time like "9:00am" (format from formatTime12h). -> [L1]
  5. Action: Change the timezone selector (#timezone-select) to "Asia/Kolkata" (value Asia/Kolkata). -> Expect: The time-slots panel collapses (selection resets) and the calendar reloads for the new timezone. -> [L1]
  6. Action: Re-select an available day (same approach as step 6) and read the new time-slots panel. -> Expect: (a) The timezone label (#time-slots-tz-label) now reads "Times in Asia Kolkata". (b) The displayed slot times are DIFFERENT from step 7 — Asia/Kolkata is 12.5 hours ahead of Los Angeles, so the same underlying availability appears at clearly different clock times (and often on a different calendar day). The displayed times must not be identical to the LA times. -> [L1]
    • Capture screenshot: B9-slots-Kolkata.
  7. Action: (Robustness) Confirm the timezone change actually re-queried the server, not just relabeled. Compare the set of available day numbers before vs after the switch, or confirm the first slot time changed by a non-zero, non-24h amount. -> Expect: The displayed local times shifted consistent with a ~12.5h offset (e.g. an LA-morning slot now reads as a late-evening/next-day Kolkata time). -> [L1]

Section C — Empty-availability shows a clear message (not a blank page)

  1. Action: Open https://calendo.dev/booking/?user=<HOST_SLUG>&event=<EMPTY_EVENT_SLUG> (using EMPTY_EVENT_SLUG=cu11-empty if it exists). If no reserved empty event exists, instead use the happy-path event and click the NEXT-month arrow (.calendar-nav-btn last) repeatedly to reach a far-future month, OR pick a single specific day that is unavailable — the goal is to land on a state with zero .available days and a day with no slots. Do NOT edit P1's global availability to manufacture this state. -> Expect: The booking view loads. -> [L1]
  2. Action: Inspect the calendar grid for the displayed month. -> Expect: With a truly empty event/month, every day cell carries the class .calendar-day.unavailable (greyed #ccc) and there are zero .calendar-day.available cells. The page is NOT blank — host sidebar, month label, and timezone selector are still present. -> [L1]
  3. Action: If you can reach a selectable but slot-less day (group/override edge), click it; otherwise verify the per-day empty message directly: the code renders <div class="time-slots-empty">No available times</div> into #time-slots-list when a selected day has zero slots. -> Expect: Where applicable, the time-slots panel shows the literal text "No available times" (.time-slots-empty), not an empty box. -> [L1]
  4. Action: (Event-list empty state, additional coverage) Open https://calendo.dev/booking/?user=<HOST_SLUG> WITHOUT an &event= param ONLY IF P1 is known to have at least one event (it does) — this shows the event LIST, not an empty state. To genuinely see the "no event types" empty state you need a host with zero events, which P1 is not. So instead just VERIFY the event-list renders cards (.event-card) for P1. The "No meeting types available yet." empty branch (rendered into #event-types-grid) cannot be triggered on P1 without removing all events — note this in Manual residue, do not delete P1's events. -> Expect: #event-list-view visible, #event-types-grid contains at least one .event-card. -> [L1]
    • Capture screenshot: C12-empty-availability.

Section D — Calendar navigation and day states

  1. Action: Reopen BASE_BOOKING_URL (happy-path event) in an anonymous tab and wait for #calendar to render. Read the current month label text (.calendar-month). -> Expect: A label like "June 2026". -> [L1]
  2. Action: Confirm the back/previous arrow behavior: locate the first .calendar-nav-btn (left arrow ). On the current real-time month it should be disabled (the code disables prev when month <= now.getMonth() in the current year). -> Expect: The left/prev nav button is disabled (greyed, not clickable) while viewing the current month — you cannot navigate into the past. -> [L1]
  3. Action: Click the next-month arrow (the LAST .calendar-nav-btn, right arrow ). -> Expect: The month label (.calendar-month) changes to the following month. Availability reloads. -> [L1]
  4. Action: Now that you are on a future month, the previous arrow should be enabled. Click the FIRST .calendar-nav-btn (left ) to go back one month. -> Expect: The month label returns to the original month from step 15. -> [L1]
    • Capture screenshot: D18-calendar-nav.
  5. Action: Verify day states in the visible month: there must be at least one .calendar-day.available (clickable, dark text/bold) and at least one .calendar-day.unavailable (greyed). Hover an available day. -> Expect: Available days exist and react to hover; unavailable days are visually disabled (#ccc) and not clickable. The current date carries .today (with a dot indicator). -> [L1]
  6. Action: Click an available day, confirm slots appear, then verify the selected day gets the .selected class (brand background). -> Expect: #time-slots-panel shows, the clicked day has class .calendar-day.selected with the brand-colored background, and .time-slot buttons render. -> [L1]

Section E — Branding / colors applied

  1. Action: With the booking page open (Section D), determine P1's configured brand color. In the dashboard (Section A context) this is the booking-page brand color setting; the public page applies it via applyBranding() setting the CSS variable --brand from page.brand_color. Inspect the computed color of a primary brand element — e.g. a .time-slot button border/text or the selected calendar day background (both use var(--brand)). -> Expect: The brand color is applied consistently: time-slot accents and the selected-day background share the same brand color. If P1 has set a custom (non-default) brand color, it must be that custom value, NOT the default #0069ff. If P1 has no custom color, the default #0069ff is acceptable. -> [L1]
    • Capture screenshot: E21-branding.
  2. Action: Confirm the "powered by Calendo" badge (.powered-by) at the bottom of the page is present (unless P1 has hide_branding enabled, in which case it is correctly hidden). -> Expect: Either the badge shows "powered by Calendo" with the logo, or — if hide_branding is on — it is absent. Note which case applies. -> [L1]

Section F — QR code renders and downloads (host dashboard; requires P1 session)

  1. Action: Go to the dashboard Overview tab (https://calendo.dev/dashboard/). In the booking-link share bar, click the "QR" button (#qrLinkBtn). -> Expect: The QR modal (#qrModal) opens (gets class active), showing the booking URL text (#qrModalUrl) and a rendered QR image drawn into the canvas (#qrCanvas, 300x300). The QR must be a real rendered code (canvas has non-blank pixels), NOT a broken image. The code must be generated client-side — there must be NO external api.qrserver.com network request. -> [L1]
    • Capture screenshot: F23-qr-modal.
  2. Action: Click the "Download" button in the modal (#qrDownloadBtn, calls downloadQR()). -> Expect: A PNG download is triggered (a .png file from the canvas via toDataURL). Confirm the browser registers a download. -> [L1]
  3. Action: Close the QR modal (Close button), navigate to the Event Types tab (sidebar a[data-tab="event-types"]), and on any event card click its per-event "QR" button (calls showEventQR(<id>)). -> Expect: The same #qrModal opens with #qrModalUrl showing that event's deep booking link (...&event=<slug>) and a freshly rendered #qrCanvas. -> [L1]
    • Capture screenshot: F25-event-qr.
  4. Action: (Negative check on the original brief) Verify there is NO QR element on the PUBLIC confirmation page. After completing Section G, on the #confirmation-success-view, search for any QR canvas/image. -> Expect: No QR element exists on the public confirmation page (only add-to-calendar buttons #calendar-links and manage links #confirmation-manage). If a QR unexpectedly appears, FLAG it — the code does not put one there. -> [L1]

Section G — Mobile layout (375px) + one happy-path booking to the confirmation screen

  1. Action: Resize the browser/emulate a phone viewport at 375px wide (iPhone width). Open BASE_BOOKING_URL. -> Expect: The page renders without horizontal overflow. The desktop left sidebar (#booking-sidebar) is hidden, and the mobile host card (#mobile-host-card) appears at the top with the host name and event. The calendar and time-slots stack vertically (time-slots panel #time-slots-panel goes full width below the calendar, not side-by-side). The timezone selector is still reachable. -> [L1]
    • Capture screenshot: G27-mobile-375.
  2. Action: At 375px, tap the mobile host card header (#mobile-host-toggle) to expand it. -> Expect: The host details (#mobile-host-details) expand showing duration/location/description; the toggle chevron (#mobile-toggle-btn) rotates. -> [L1]
  3. Action: Still at 375px, select an available day, then tap a time slot (.time-slot). -> Expect: The confirm form (#confirm-view) appears with name (#input-name) and email (#input-email) fields, the selected date/time summary (#confirm-datetime-text), and a "Confirm Booking" button (#btn-confirm). The form is usable at phone width (fields full-width, buttons stacked per the mobile .form-actions { flex-direction: column }). -> [L1]
  4. Action: Fill name CU11 Invitee <RUNID> (#input-name) and email ravikantguptaofficial+inv-<RUNID>@gmail.com (#input-email), then click "Confirm Booking" (#btn-confirm). -> Expect: The confirmation success view (#confirmation-success-view) appears with the animated check and title "Booking Confirmed!" (.confirmation-title). The booking details (#confirmation-details) reference the event (contains "30"). Add-to-calendar buttons (#calendar-links: Google Calendar, Outlook, Download .ics) are visible, and cancel/reschedule manage links (#confirmation-manage) are present. -> [L1]
    • Capture screenshot: G30-confirmation.
  5. Action: Confirm the add-to-calendar buttons are real links. Read the href of the "Download .ics" button (a download="booking.ics" link with a blob URL) and the Google Calendar button (https://calendar.google.com/calendar/r/eventedit?...). Do NOT click through to actually create external calendar events (out of scope for this suite). -> Expect: The .ics link is a downloadable blob; the Google/Outlook links point to the correct calendar compose URLs with the event title and times. -> [L1]
  6. Action: Capture the cancel link href from #confirmation-manage (it contains /booking/cancel.html?token=) for use in Cleanup. -> Expect: A cancel URL with a token query param. Record it. -> [L2]

L3 reality checks

None — see Pass/Fail. This suite deliberately stops at the in-UI confirmation screen. It does NOT verify the Google Calendar event or the confirmation email (those are owned by CU-01/CU-02). The single booking created here is throwaway and is cancelled in Cleanup.

Cleanup

  1. Cancel the one booking created in Section G: open the cancel URL recorded in step 32 (https://calendo.dev/booking/cancel.html?token=<token>), confirm the cancellation in that page's UI, and verify it reports the booking as cancelled. -> [L2]
  2. As a backstop, in the dashboard go to the Bookings tab (sidebar a[data-tab="bookings"]), search for inv-<RUNID> or CU11 Invitee <RUNID>, and confirm the booking shows status Cancelled (or is gone). If it is still active, cancel it from there. -> [L2]
  3. No event types, forms, polls, throwaway accounts, or availability changes were created by this suite, so nothing else to delete. If Section C used the reserved cu11-empty event, leave it in place (it is a shared fixture, not per-run state).
  4. Close any QR modal and reset the browser viewport to desktop width.

Pass/Fail criteria

The run PASSES only if ALL of the following are true:

The run FAILS if: timezone change does not shift times; calendar navigation is broken or lets you go into the past; empty availability shows a blank/error page; the QR canvas is blank or pulls from an external QR API; the mobile layout overflows horizontally or does not collapse the sidebar; or the booking does not reach the confirmation screen.

Evidence to capture

Manual residue / cannot-verify