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|bodywith 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
200with 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-fixturesalias, match + serve),canonical.ts(the request key).apps/dev-app/fixtures/ionic/<scenario>/— fixture files (referenced in ionic via the@ionic-fixturesVite alias):manifest.json—capturedAtMs,hosts,selections(slug/package/pax/date/time), andentries(canonicalKey → { file, status }).responses/NNN.json— the captured response bodies.
apps/ionic/public/mockServiceWorker.js— the MSW service worker (generated bymsw init).- Manage fixtures (create/edit/delete/recapture) via
http://localhost:5176/scenarios— start withpnpm --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.tsmust stay byte-identical to the skill’sscripts/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:
| Param | Required | Values | Effect |
|---|---|---|---|
?mock=<scenario> | yes | any scenario name from the table | Activates MSW and replays that scenario’s fixtures |
&target=<stage> | no | restaurant funnel datetime occasion checkout | Skips 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:
- Open DevTools → Application → Service Workers
- Click Unregister next to the
mockServiceWorker - 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-runcapture.pyto 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_tokenlocalStorage 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.