Today Platform Web — Dev Docs
ToolchainTesting

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 fileTargetRuns when
playwright.config.tsStorybook iframes (*.visual.spec.ts co-located in src/)Required PR check (Visual Regression)
playwright.smoke.config.tsA live Vercel preview URL (e2e/smoke/)Non-blocking, after deploy-web succeeds
playwright.full.config.tsA live Vercel preview URL (e2e/full/)Non-blocking, on dev branch + main
playwright.local.config.tsBuilt apps/web running under next start (e2e/local/)Required PR check (Local smoke (web) in ci.yml)
playwright.preview.config.tsA 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.tsx

Each 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-snapshots

Visual 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:

  1. The cloud-side /internal/e2e/last-otp endpoint landing (see TODO.md blocking section)
  2. 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

ModeWorkersRetries
CI100%2
Local40%-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).

On this page