Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Booking→checkout mock-mode (MSW)

Drive the booking funnel and checkout page on deterministic, offline data — responses captured once from the real API and replayed via MSW. Use it to fix bugs or build features in the booking/checkout flow without fighting real-API drift (availability, prices, and dates change daily) or network flakiness. Dev-only.

Enable it

Open a restaurant with the mock query param:

http://localhost:5174/en/restaurants/<slug>?mock=<scenario>
# seed scenario:
http://localhost:5174/en/restaurants/ma-maison-bangkok-thailand?mock=ma-maison-prepaid-guest

Or set VITE_MOCK=<scenario> in apps/ionic/.env.local. The console prints [mock] booking mock-mode ACTIVE … when it’s on.

Mock-mode is DEV-only: main.ts dynamic-imports it behind import.meta.env.DEV and the flag, so production bundles exclude it entirely.

What it does

  • Serves responses captured once from the real API — never hand-written, so the mocks can’t drift from real response shapes.
  • Matches each request to a fixture by a canonical key — METHOD path?query|body with volatile fields (X-HH-Request-Id, locale/client_type, accessToken) stripped — so the varying POSTs (calculate_package_price, available_*, temporary_reservations) resolve correctly.
  • Freezes the clock to the scenario’s capture time. The calendar grid + the availability request window are computed client-side from dayjs(), so without this the requested dates drift daily and fixtures stop matching. (Date.now()/new Date() are pinned; timers keep running.)
  • Unmatched requests (off the captured path) return 200 with an empty envelope and log [mock] UNMATCHED <key> so coverage gaps are visible, not silent.

Where it lives

  • apps/ionic/src/mocks/ — browser.ts (worker + clock-freeze), handlers.ts (glob fixtures via @ionic-fixtures alias, match + serve), canonical.ts (the request key).
  • apps/dev-app/fixtures/ionic/<scenario>/ — fixture files (referenced in ionic via the @ionic-fixtures Vite alias):
    • manifest.json — capturedAtMs, hosts, selections (slug/package/pax/date/time), and entries (canonicalKey → { file, status }).
    • responses/NNN.json — the captured response bodies.
  • apps/ionic/public/mockServiceWorker.js — the MSW service worker (generated by msw init).
  • Manage fixtures (create/edit/delete/recapture) via http://localhost:5176/scenarios — start with pnpm --filter @hh-pegasus/dev-app dev.

Capture & drive (the booking-checkout-harness skill)

Capturing and driving are done by the booking-checkout-harness agent skill (~/.agents/skills/booking-checkout-harness/). Requires the dev server (pnpm --filter ionic-app dev).

# (re)capture a scenario — the ONLY step that touches the real API (needs network):
python ~/.agents/skills/booking-checkout-harness/scripts/capture.py \
  --scenario <name> --slug <restaurant-slug> --package "<Package Name>" --adults 2 --kids 0

# drive the flow on mock-mode to a target + capture a screenshot:
python ~/.agents/skills/booking-checkout-harness/scripts/drive.py \
  --scenario <name> --target checkout    # restaurant|funnel|datetime|checkout|occasion|payment
#   add --offline to prove there is no real-API dependency

Keep the canonical key in sync. apps/ionic/src/mocks/canonical.ts must stay byte-identical to the skill’s scripts/canonical.py — a recorded response is matched to a live request only if both compute the same key.

URL query params

Two query params control mock-mode in the browser:

ParamRequiredValuesEffect
?mock=<scenario>yesany scenario name from the tableActivates MSW and replays that scenario’s fixtures
&target=<stage>norestaurant funnel datetime occasion checkoutSkips to that stage of the booking flow

Example — open directly at the checkout confirmation step:

http://localhost:5174/en/restaurants/ma-maison-bangkok-thailand?mock=ma-maison-prepaid-guest&target=checkout

The target param is generated for you by the run ▾ button in the scenario manager at http://localhost:5176/scenarios.

What breaks with an invalid ?mock= value

If the scenario name doesn’t exist in the fixture files, the MSW handler throws synchronously:

Error: [mock] scenario "xyz" not found. Available: ma-maison-prepaid-guest

This surfaces as an unhandled exception in the browser console and the page renders broken — the booking funnel has no fixture data to serve. Fix: use a name that exactly matches a row in the scenario table.


How to confirm MSW is active

Console: When mock-mode activates, it logs this line to the browser DevTools console:

[mock] booking mock-mode ACTIVE — scenario="ma-maison-prepaid-guest", clock frozen to capture time. Off-path requests are served as UNMATCHED (see warnings).

If you don’t see this line, mock-mode did not activate — check the ?mock= param and that the Vite dev server restarted after any new MSW installs.

Network tab: Open DevTools → Network. Any request intercepted by MSW shows (service worker) in the Initiator column instead of a real remote URL. Requests that bypass MSW (unmatched or outside the captured hosts) show a normal initiator pointing at the real API domain.

Unmatched requests: If you change inputs in-browser (different party size, voucher code), the POST body no longer matches any fixture. The console prints:

[mock] UNMATCHED GET /api/v5/restaurants/...

The request returns a 200 with an empty envelope — the funnel stays alive but the data is hollow. Re-capture the scenario with the new inputs to fix.


Clock freeze — what you will see

When mock-mode activates, Date.now() and new Date() are frozen to the scenario’s capturedAtMs timestamp. This means:

  • The date picker will show dates from the capture time, not today. This is intentional — availability requests must match the captured window.
  • Timers still run (setTimeout, Ionic transitions). Only the wall clock is frozen.
  • If you need to work in a different date window, re-capture the scenario.

Stale service worker

MSW runs as a browser service worker (apps/ionic/public/mockServiceWorker.js). If the worker gets stuck on an old version after an MSW upgrade or a hard reset:

  1. Open DevTools → Application → Service Workers
  2. Click Unregister next to the mockServiceWorker
  3. Hard-refresh (Ctrl+Shift+R / Cmd+Shift+R)

After installing msw for the first time (or upgrading it), always restart the Vite dev server — HMR alone will not re-optimize the new dependency.


Limits

  • Faithful only for the captured path. Changing inputs in-browser (party size, vouchers) produces request bodies with no fixture → served as UNMATCHED (200 empty + warn). Re-run capture.py to mock the new path.
  • Fixtures go stale as the real API evolves — re-capture to refresh.
  • Fully-offline boot needs every boot call captured; an uncaptured boot call bypasses (hits the real API online, fails offline).
  • Member/auth scenarios need a token captured at record time plus the hh_access_token localStorage injection; the seed scenario is a guest (reaches checkout with no auth).
  • After installing msw (or any new dependency), restart the Vite dev server — HMR alone won’t re-optimize it.