Playwright tiers
Five Playwright configs (visual / smoke / full / preview / local) split by what they target and when CI runs them.
apps/web ships five Playwright configs, each with a distinct purpose.
| Config file | Target | Runs when |
|---|---|---|
playwright.config.ts | Storybook iframes (*.visual.spec.ts co-located in src/) | Required PR check (Visual Regression) |
playwright.smoke.config.ts | A live Vercel preview URL (e2e/smoke/) | Non-blocking, after deploy-web succeeds |
playwright.full.config.ts | A live Vercel preview URL (e2e/full/) | Non-blocking, on dev branch + main |
playwright.local.config.ts | Built apps/web running under next start (e2e/local/) | Required PR check (Local smoke (web) in ci.yml) |
playwright.preview.config.ts | A live next dev server (visual snapshots that need SSR) | Local dev only (pnpm test:visual:preview) |
Visual regression — playwright.config.ts
This is the default Playwright config (the one playwright.config.ts
filename implies). It targets Storybook stories with co-located
*.visual.spec.ts files:
apps/web/src/components/foo/foo.visual.spec.ts
apps/web/src/components/foo/foo.stories.tsxEach spec navigates to a Storybook iframe URL, waits for theme application,
and snapshots. CI's Visual Regression check runs this against the built
Storybook (pnpm storybook:build).
// excerpt — apps/web/playwright.config.ts
export default defineConfig({
testDir: './src',
testMatch: '**/*.visual.spec.ts',
testIgnore: '**/visual-onboarding-preview/**',
workers: process.env['CI'] ? '100%' : '40%',
})The local workers: '40%' cap is intentional — Storybook's Vite dev server
falls over above ~6 parallel workers on a 16-core Mac because of lazy story
compile + Agentation / react-scan decorators in dev mode. CI runs against
the pre-built storybook-static/ so it can afford 100% workers.
Updating snapshots:
pnpm test:visual:update # all visual snapshots
pnpm --filter @todayai-labs/web exec playwright test path/to/spec.visual.spec.ts --update-snapshotsVisual diffs are committed as PNG into the repo via Git LFS — see
.github/workflows/lfs-check.yml.
Smoke — playwright.smoke.config.ts
Tests that target a live Vercel preview URL. Scope is intentionally narrow:
Can unauthenticated visitors reach
/waitlist, can an authenticated session load/agent, and does the page render without runtime errors?
Triggered by .github/workflows/e2e-smoke.yml
on deployment_status: success events. Currently non-blocking — failures
warn but don't block merge. Promotion to required is gated on:
- The cloud-side
/internal/e2e/last-otpendpoint landing (see TODO.md blocking section) - A week of measurement showing acceptable flake rate
Authentication uses pre-minted state injected via apps/web/e2e/lib/auth.ts +
addCookies. Smoke specs assume that auth state exists; setup is in
apps/web/e2e/global-setup.smoke.ts.
Full — playwright.full.config.ts
Same shape as smoke but covers business-flow specs in e2e/full/. Runs less
frequently (after merge to dev and main) and tolerates longer execution
times. Triggered by
.github/workflows/e2e-full.yml.
Local — playwright.local.config.ts
Despite the "local" filename, this config is wired into CI as the required
Local smoke (web) check (see
ci.yml's local-smoke job).
The job runs the Playwright Docker image, does a pnpm install, then
pnpm --filter @todayai-labs/web run test:visual:local. The Playwright config's
webServer block runs pnpm run build && pnpm next start -p 4070 and waits
for the URL before driving the spec — fundamentally different from smoke/full
(which target a Vercel preview URL).
Scope: the embed canvas (/embed/today-page/v2) and the build-time
integrations that the Storybook visual regression can't reach. Treat it as
the third leg of the required-check tripod: visual regression (Storybook),
build (Next), local smoke (built + next start).
Preview — playwright.preview.config.ts
Spins up next dev and runs visual snapshots against pages that need SSR
(snapshots that don't fit Storybook iframes). Not wired into CI — exists
for human-in-the-loop local validation via pnpm test:visual:preview.
Per-worker isolation
All configs run with fullyParallel: true. Each worker gets its own browser
context. The auth-state.json file is read once per worker via
global-setup.smoke.ts, then browser.newContext({ storageState: ... })
isolates cookies per worker — verified by the two-worktree
simultaneous-login e2e test (see
docs/plans/2026-05-22-localhost-bff-dev-mode.md).
Workers + retries
| Mode | Workers | Retries |
|---|---|---|
| CI | 100% | 2 |
| Local | 40%-50% | 0 |
Local lower workers because dev-mode Storybook can't keep up. CI retries 2x because preview-URL flake (cold-start latency on Vercel) is a real contributor to noise that shouldn't fail a PR.
When you change a Storybook story
Visual regression snapshots are tied to the story output. Any change that
shifts pixels — a Tailwind class tweak, a layout refactor, even a font
fallback hop — needs pnpm test:visual:update and a snapshot commit. The PR
review process expects to see snapshot deltas inline.
If you don't have a Mac with the exact font set CI uses, snapshots taken on
your machine will diverge from CI's. The workaround is to push your code and
let the visual-update-snapshots workflow regenerate them remotely
(triggered via gh workflow run).
Testing
A four-layer testing strategy — Vitest unit tests, Storybook visual regression, MSW-backed integration tests, and Playwright smoke / full e2e.
Storybook topology
Five Storybook installs across the monorepo — one per app or design-system project — and how visual regression hooks into one of them.