Preview System Design — 2026-04-19
Preview System Design — 2026-04-19
Section titled “Preview System Design — 2026-04-19”Status: Approved for implementation planning Scope: Visual preview + regression infrastructure for the Ideony mobile app (Expo Web build) Prerequisite for: UX Overhaul Phase A (Sole direction), Phase C, Phase B (SOS)
Purpose
Section titled “Purpose”Give the team a way to see every screen of the mobile app before merge, with before/after visual diffs, via a standard PR workflow. Replaces “trust me it looks fine” with clickable review.
Non-goals
Section titled “Non-goals”- Not a Storybook-style component catalog. App-screen-level previews only.
- Not a visual testing replacement for unit/E2E tests. Complementary.
- Not a design tool (Figma stays separate).
- Not iOS/Android native previews. Expo Web only — acceptable because Web covers ~95% of visual surface area and iOS/Android rendering diffs are minimal for our stack.
Architecture
Section titled “Architecture”Developer pushes branch ──► GitHub Actions (preview.yml) │ ┌───────────────┴────────────────┐ ▼ ▼ Expo Web build Playwright tour (apps/mobile) captures all routes │ on new + main ▼ │ Cloudflare Pages ▼ branch deploy Lost Pixel diff pr-<sha>.ideony.pages.dev per route │ │ └──────────┬─────────────────────┘ ▼ PR comment with: - Live preview URL - Visual diff summary + report link - Changed routes listThree decoupled pieces that compose:
- Live preview — Cloudflare Pages auto-deploys every branch to a unique URL. The full app, navigable.
- Screen tour — A Playwright script walks every route, captures viewport screenshots with a semantic filename.
- Visual diff — Lost Pixel compares branch screenshots against main’s baseline, highlights pixel diffs in an HTML dashboard.
The output on every PR is two links: a live URL for manual exploration, a diff report for review.
Routes Manifest
Section titled “Routes Manifest”Routes are captured from the Expo Router file tree, described in a typed manifest at apps/mobile/preview-routes.ts. Used both by the tour script (CI) and the local dev command.
Public (no auth)
Section titled “Public (no auth)”/— landing tour/(welcome)/sign-in,/sign-up,/forgot-password/results?q=idraulico&lat=45.46&lng=9.19— search results (mock data)/professional/[id]— pro profile (seeded id)
Consumer (authed)
Section titled “Consumer (authed)”/(consumer)— home/(consumer)/search/(consumer)/bookings/(consumer)/profile/book/[proId]— booking form/booking/[id]— booking detail, one capture per state (PENDING, ACCEPTED, IN_PROGRESS, DONE, DECLINED)/chat/[bookingId]— one capture per state (empty, one message, long thread)/review/[bookingId]/sos— one capture per state (idle, dispatching, countdown, accepted, live)/settings,/stripe-onboarding
Pro (authed)
Section titled “Pro (authed)”/(professional)— dashboard/(professional)/calendar/(professional)/jobs/(professional)/chat/(professional)/profile/verification,/portfolio,/quotes
Viewports
Section titled “Viewports”Captured per route:
iphone-15— 393×852pixel-7— 412×915web-desktop— 1440×900
Web-mobile viewport is omitted (equivalent to iphone-15 in Expo Web).
Volume
Section titled “Volume”~25 routes × 3 viewports = ~75 base screenshots. State-expanded routes (booking detail × 5, chat × 3, SOS × 5) add ~35, total ~110 screenshots per tour run.
State Seeding
Section titled “State Seeding”apps/mobile/mocks/ holds MSW handlers + deterministic fixtures. The preview build runs with EXPO_PUBLIC_MSW=1, which starts a Mock Service Worker that intercepts API calls. Fixtures include:
- 8 seeded professionals (balanced across categories, ratings, locations)
- 5 booking records covering all states
- Chat threads at 3 lengths
- SOS scenarios (dispatching / countdown / accepted / live tracking)
- Clerk mocked to auto-auth; persona chosen via
?persona=consumer|proquery param
No real API needed. Screenshots reproducible.
CI Wiring
Section titled “CI Wiring”New workflow: .github/workflows/preview.yml. Triggers on PR open + push to PR branch. Independent from existing ci.yml and cd.yml.
name: Previewon: pull_request: paths: - apps/mobile/** - packages/design-tokens/** - packages/validators/** - .github/workflows/preview.yml
jobs: build-web: # pnpm install (cached), expo export --platform web, upload dist/ artifact deploy-preview: needs: build-web # Wrangler deploy dist/ to Cloudflare Pages, output preview URL screen-tour: needs: deploy-preview # Playwright install (cached), tour against preview URL → ./current # Download main baselines artifact → ./baseline # Lost Pixel compare, upload report artifact # Sticky PR comment with URL + diff summary + report link update-baselines: if: github.ref == 'refs/heads/main' # Re-run tour on main, upload fresh baselines artifact (90d retention)Wiring Decisions
Section titled “Wiring Decisions”- Cloudflare Pages project —
ideony-preview, separate from prod. Uses existingCLOUDFLARE_API_TOKEN+CLOUDFLARE_ACCOUNT_IDsecrets. No new secrets required. - Baseline storage — GH Actions artifacts on main (fast, 90-day retention). Fallback to a
baselinesbranch if artifact expiry becomes a problem. Start with artifacts. - PR comment —
mshick/add-pr-comment@v2, sticky (same comment updated on re-push, no spam). - Path filter — PRs touching only
apps/api/**ordocs/**skip the workflow entirely. - Concurrency —
concurrency: { group: preview-${{ github.ref }}, cancel-in-progress: true }to save minutes on rapid pushes.
PR Comment Format
Section titled “PR Comment Format”🎨 Preview ready→ Live: https://pr-abc123.ideony.pages.dev→ Visual diff: 4 changed / 106 unchanged routes · view report→ Screens changed: - /professional/[id] · iphone-15 - /professional/[id] · pixel-7 - /(consumer) · web-desktop - /(consumer) · iphone-15Local Dev UX
Section titled “Local Dev UX”Three scripts added to apps/mobile/package.json:
{ "scripts": { "preview:build": "expo export --platform web --output-dir dist-preview", "preview:serve": "pnpm preview:build && npx serve dist-preview -p 4173", "preview:tour": "playwright test scripts/tour.spec.ts", "preview:diff": "lost-pixel update-storybook --config lostpixel.config.ts", "preview:review": "open preview/report/index.html", "preview": "pnpm preview:serve & pnpm preview:tour && pnpm preview:diff && pnpm preview:review" }}Typical Laptop Loop
Section titled “Typical Laptop Loop”git checkout -b feat/sole-hero# ...make UX changes...
pnpm preview # builds, tours current, tours main, diffs, opens report# → ~4min total on M-series laptopReport HTML
Section titled “Report HTML”Per route: three panes — before (main), after (branch), diff overlay (red/green pixelmatch). Filters: changed | all | new | deleted; viewport: all | iphone | pixel | desktop. One-click “open in Pages” pops the live preview at that route.
Baseline Refresh
Section titled “Baseline Refresh”pnpm preview:tour --update-baseline # after merging to main, updates preview/baseline/*Rollout
Section titled “Rollout”Four incremental steps, each shippable in isolation. Zero big-bang.
Step 1 — Cloudflare Pages project + base build (~2hr)
Section titled “Step 1 — Cloudflare Pages project + base build (~2hr)”- Verify/fix Expo Web build (lottie + gluestack quirks documented in CLAUDE.md)
- Create CF Pages project
ideony-previewvia Wrangler - Add
preview.ymlwith deploy-only stage - Verify: opening a PR produces
pr-<sha>.ideony.pages.dev - Ship: preview URL in PR comment, no diff yet
Step 2 — MSW mock layer + routes manifest (~4hr)
Section titled “Step 2 — MSW mock layer + routes manifest (~4hr)”- Install MSW, create
apps/mobile/mocks/{handlers,fixtures}.ts - Seed fixtures: 8 pros, 5 booking states, chat threads, SOS states
- Wire
EXPO_PUBLIC_MSW=1for preview build only (never prod) - Mock Clerk via
?persona=consumer|proroute param - Write typed
apps/mobile/preview-routes.tsmanifest - Verify: navigating preview URL with
?persona=proshows pro dashboard, no backend hits - Ship: preview URL navigable through all personas + states
Step 3 — Playwright screen-tour + HTML report (~3hr)
Section titled “Step 3 — Playwright screen-tour + HTML report (~3hr)”apps/mobile/scripts/tour.spec.tsiterates manifest, captures 3 viewports- Animations disabled,
waitForFontsReady, 500ms post-nav settle - Date/randomness frozen via
page.addInitScript scripts/build-preview-report.tsgenerates standalone HTML with pixelmatchpnpm previewone-shot works locally- Ship: laptop workflow complete, CI still deploy-only
Step 4 — Lost Pixel + PR comment (~2hr)
Section titled “Step 4 — Lost Pixel + PR comment (~2hr)”lostpixel.config.ts: anti-alias tolerance 0.1, threshold per-route override map- CI job: compare tour output vs main’s baseline artifact
mshick/add-pr-commentsticky commentupdate-baselinesjob on main merge- Ship: full loop live
Total: ~11 hours of focused work.
Error Handling
Section titled “Error Handling”- Preview deploy fails → CI job fails, no PR comment. Error visible in Actions logs. No partial state.
- Tour fails mid-way → Partial screenshots uploaded as artifact with failure annotation. Lost Pixel skipped.
- Lost Pixel flakes (font rendering, late animation) → Retry once with a 200ms extra settle. Still failing → post PR comment with “diff inconclusive, see screenshots” and link to artifact.
- Baseline missing (first run, or expired artifact) → Current run treated as the new baseline. Comment flags “no baseline found, establishing this run as baseline.”
Testing
Section titled “Testing”- Smoke test: open a no-op PR, verify preview URL reachable + tour screenshot artifact produced + PR comment posted. Test lives in
.github/workflows/preview.ymlitself (the first successful run is the smoke test). - Local
pnpm previewsmoke: run against main branch twice in a row, expect 0 changed routes. - Failure scenarios simulated by introducing a known CSS change, confirming diff flags correct routes.
Risk Register
Section titled “Risk Register”| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Expo Web build flakes on gluestack or lottie | Medium | Blocks everything | Reuse working apps/mobile/Dockerfile.deploy path |
| Font rendering differs CI vs laptop | High | False diffs | Pin Playwright version; commit font files used by preview |
| Dynamic content (dates, random IDs) causes false diffs | High | False diffs | Freeze Date.now() + seed Math.random via init script |
| PR comment spam on rapid pushes | Low | Annoyance | Sticky comment action |
| Artifact storage pressure as PR count grows | Low | CI errors | Monitor, move baselines to branch if hit |
| Preview URL leaks unreleased UI publicly | Medium | Privacy | CF Pages URL is unguessable per-SHA; accept since app has no real data in preview (MSW mocks only) |
Open Questions
Section titled “Open Questions”None at spec approval. Any surfaced during planning escalate back to this doc.
Dependencies
Section titled “Dependencies”- Cloudflare Pages (free tier)
@lost-pixel/oss(MIT)@playwright/test(already installed via API smoke tests)msw(new dep)pixelmatch(new dep, for local report)mshick/add-pr-comment@v2(GH Action)
Out of scope (follow-up sprints)
Section titled “Out of scope (follow-up sprints)”- Native iOS/Android screen capture (via Maestro or Detox)
- Approval workflow UI beyond PR comment (e.g., hosted dashboard)
- Visual regression testing as merge gate (start as advisory, promote later)
- Chromatic/Argos migration (only if team outgrows OSS dashboard)