Skip to content

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)

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.

  • 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.
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 list

Three decoupled pieces that compose:

  1. Live preview — Cloudflare Pages auto-deploys every branch to a unique URL. The full app, navigable.
  2. Screen tour — A Playwright script walks every route, captures viewport screenshots with a semantic filename.
  3. 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 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.

  • / — 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) — 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
  • /(professional) — dashboard
  • /(professional)/calendar
  • /(professional)/jobs
  • /(professional)/chat
  • /(professional)/profile
  • /verification, /portfolio, /quotes

Captured per route:

  • iphone-15 — 393×852
  • pixel-7 — 412×915
  • web-desktop — 1440×900

Web-mobile viewport is omitted (equivalent to iphone-15 in Expo Web).

~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.

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|pro query param

No real API needed. Screenshots reproducible.

New workflow: .github/workflows/preview.yml. Triggers on PR open + push to PR branch. Independent from existing ci.yml and cd.yml.

name: Preview
on:
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)
  1. Cloudflare Pages projectideony-preview, separate from prod. Uses existing CLOUDFLARE_API_TOKEN + CLOUDFLARE_ACCOUNT_ID secrets. No new secrets required.
  2. Baseline storage — GH Actions artifacts on main (fast, 90-day retention). Fallback to a baselines branch if artifact expiry becomes a problem. Start with artifacts.
  3. PR commentmshick/add-pr-comment@v2, sticky (same comment updated on re-push, no spam).
  4. Path filter — PRs touching only apps/api/** or docs/** skip the workflow entirely.
  5. Concurrencyconcurrency: { group: preview-${{ github.ref }}, cancel-in-progress: true } to save minutes on rapid pushes.
🎨 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-15

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"
}
}
Terminal window
git checkout -b feat/sole-hero
# ...make UX changes...
pnpm preview # builds, tours current, tours main, diffs, opens report
# → ~4min total on M-series laptop

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.

Terminal window
pnpm preview:tour --update-baseline # after merging to main, updates preview/baseline/*

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-preview via Wrangler
  • Add preview.yml with 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=1 for preview build only (never prod)
  • Mock Clerk via ?persona=consumer|pro route param
  • Write typed apps/mobile/preview-routes.ts manifest
  • Verify: navigating preview URL with ?persona=pro shows 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.ts iterates manifest, captures 3 viewports
  • Animations disabled, waitForFontsReady, 500ms post-nav settle
  • Date/randomness frozen via page.addInitScript
  • scripts/build-preview-report.ts generates standalone HTML with pixelmatch
  • pnpm preview one-shot works locally
  • Ship: laptop workflow complete, CI still deploy-only
  • 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-comment sticky comment
  • update-baselines job on main merge
  • Ship: full loop live

Total: ~11 hours of focused work.

  • 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.”
  • Smoke test: open a no-op PR, verify preview URL reachable + tour screenshot artifact produced + PR comment posted. Test lives in .github/workflows/preview.yml itself (the first successful run is the smoke test).
  • Local pnpm preview smoke: 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.
RiskLikelihoodImpactMitigation
Expo Web build flakes on gluestack or lottieMediumBlocks everythingReuse working apps/mobile/Dockerfile.deploy path
Font rendering differs CI vs laptopHighFalse diffsPin Playwright version; commit font files used by preview
Dynamic content (dates, random IDs) causes false diffsHighFalse diffsFreeze Date.now() + seed Math.random via init script
PR comment spam on rapid pushesLowAnnoyanceSticky comment action
Artifact storage pressure as PR count growsLowCI errorsMonitor, move baselines to branch if hit
Preview URL leaks unreleased UI publiclyMediumPrivacyCF Pages URL is unguessable per-SHA; accept since app has no real data in preview (MSW mocks only)

None at spec approval. Any surfaced during planning escalate back to this doc.

  • 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)
  • 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)