2026-05-24 20:25:18 -04:00
|
|
|
// Playwright config for the post-PR-17 `Preview smoke` and
|
|
|
|
|
// `Screenshot diff` workflows. ESM per the repo's
|
|
|
|
|
// `"type": "module"` setting in package.json. Companion docs:
|
|
|
|
|
// `.convoys/adopt-playwright-smoke.md` (Decisions 1-6),
|
|
|
|
|
// AGENTS.md § 7 (Vercel preview bypass conventions).
|
|
|
|
|
|
|
|
|
|
import { defineConfig } from '@playwright/test';
|
|
|
|
|
|
|
|
|
|
const BASE_URL = process.env.BASE_URL ?? 'http://localhost:3000';
|
|
|
|
|
const BYPASS_SECRET = process.env.VERCEL_AUTOMATION_BYPASS_SECRET;
|
|
|
|
|
|
|
|
|
|
// `CI === 'true'` is the canonical GitHub Actions signal (set by
|
|
|
|
|
// the runner). Playwright config has no Next.js context, so
|
|
|
|
|
// `NODE_ENV` is not reliable here. Mirrors the rate-limit.js
|
|
|
|
|
// fail-closed pattern documented in AGENTS.md Gotcha #12.
|
|
|
|
|
const IS_CI = process.env.CI === 'true';
|
|
|
|
|
|
|
|
|
|
if (IS_CI && !BYPASS_SECRET) {
|
|
|
|
|
// Fail loud in CI per Decision 2. The workflow's `env:` block
|
|
|
|
|
// (preview-smoke.yml line 101, visual-diff.yml line 85) maps
|
|
|
|
|
// `secrets.VERCEL_AUTOMATION_BYPASS_SECRET` into the process
|
|
|
|
|
// env; if it's empty here, the secret is unseeded or the
|
|
|
|
|
// workflow YAML drift broke the mapping.
|
|
|
|
|
throw new Error(
|
|
|
|
|
'VERCEL_AUTOMATION_BYPASS_SECRET is required in CI to reach ' +
|
|
|
|
|
'Vercel-Protection-protected preview deployments. ' +
|
|
|
|
|
'Reseed via: gh secret set VERCEL_AUTOMATION_BYPASS_SECRET --body "<value>". ' +
|
|
|
|
|
'See AGENTS.md § 7 for the full plumbing context.'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!BYPASS_SECRET && !IS_CI) {
|
|
|
|
|
// Dev fallback per Decision 2 — warn once at config load,
|
|
|
|
|
// proceed without the header. Local runs target localhost
|
|
|
|
|
// (no preview protection) or a non-protected URL.
|
|
|
|
|
// (No eslint-disable for `no-console` here: the active config
|
|
|
|
|
// does not enable `no-console` for repo-root config files;
|
|
|
|
|
// adding the directive here trips `eslint-comments/no-unused-disable`.)
|
|
|
|
|
console.warn(
|
|
|
|
|
'[playwright.config] VERCEL_AUTOMATION_BYPASS_SECRET unset — ' +
|
|
|
|
|
'running without the Vercel bypass header. Targets a non-protected ' +
|
|
|
|
|
'URL (e.g. http://localhost:3000). Hitting a protected preview without ' +
|
|
|
|
|
'this header will return Vercel\'s SSO challenge page.'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default defineConfig({
|
|
|
|
|
// Both `tests/smoke/` and `tests/visual/` live under `tests/`.
|
|
|
|
|
// Project-level `testMatch` (below) partitions them so the
|
|
|
|
|
// two workflows (`--project=smoke` and `--project=visual`)
|
|
|
|
|
// each see only the specs they should run.
|
|
|
|
|
testDir: './tests',
|
|
|
|
|
|
|
|
|
|
// Smoke + visual specs are independent; parallelism within a
|
|
|
|
|
// single spec adds no value here and would complicate the
|
|
|
|
|
// per-test screenshot baseline lifecycle.
|
|
|
|
|
fullyParallel: false,
|
|
|
|
|
workers: IS_CI ? 1 : undefined,
|
|
|
|
|
|
|
|
|
|
// One retry in CI handles transient Vercel preview flakes
|
|
|
|
|
// (cold-start, DNS propagation). Local: zero retries — fail
|
|
|
|
|
// fast so the dev sees the issue immediately.
|
|
|
|
|
retries: IS_CI ? 1 : 0,
|
|
|
|
|
|
|
|
|
|
// 30s per test is plenty for the 3 smoke checks + 1 visual
|
|
|
|
|
// screenshot. The convoy file's success metric is < 5min
|
|
|
|
|
// total workflow runtime; per-test 30s is well inside that.
|
|
|
|
|
timeout: 30_000,
|
|
|
|
|
expect: { timeout: 10_000 },
|
|
|
|
|
|
|
|
|
|
// List reporter in dev for human readability; add HTML in CI
|
|
|
|
|
// so the `Upload Playwright report on failure` step
|
|
|
|
|
// (preview-smoke.yml line 104) has a populated `playwright-report/`
|
|
|
|
|
// to upload. `open: 'never'` keeps the HTML from auto-launching
|
|
|
|
|
// a browser tab in headless CI.
|
|
|
|
|
reporter: IS_CI ? [['list'], ['html', { open: 'never' }]] : 'list',
|
|
|
|
|
|
|
|
|
|
// Visual baselines live at `tests/visual/__screenshots__/<arg>{ext}`.
|
2026-06-12 21:30:34 -04:00
|
|
|
// Workflow `visual-diff.yml` uploads this exact path as the artifact —
|
|
|
|
|
// keep them aligned. {arg} is the snapshot name from
|
2026-05-24 20:25:18 -04:00
|
|
|
// `toHaveScreenshot('home.png')` without the extension; {ext} is
|
|
|
|
|
// the extension with the leading dot.
|
|
|
|
|
//
|
2026-06-12 21:30:34 -04:00
|
|
|
// Cross-platform CAUTION (still live): this template drops Playwright's
|
|
|
|
|
// default `-<browser>-<platform>` suffix. A Mac dev running
|
|
|
|
|
// `npm run test:visual:update` silently overwrites the Linux-CI
|
|
|
|
|
// baseline with a Mac-rendered PNG that the CT 111 runner will reject
|
|
|
|
|
// on the next diff. The seeded Linux baseline (PR #58 `83a358b`,
|
|
|
|
|
// 2026-06-02) is the canonical artifact. Re-seed via the Linux Docker
|
|
|
|
|
// recipe in `tests/visual/homepage.spec.ts`'s module docblock OR
|
|
|
|
|
// dispatch the (queued) `seed-visual-baselines` workflow on CT 111.
|
|
|
|
|
// Never via `npm run test:visual:update` on a Mac.
|
2026-05-24 20:25:18 -04:00
|
|
|
snapshotPathTemplate: 'tests/visual/__screenshots__/{arg}{ext}',
|
|
|
|
|
|
|
|
|
|
use: {
|
|
|
|
|
baseURL: BASE_URL,
|
|
|
|
|
// Headers apply to BOTH browser `page.goto(...)` calls AND the
|
|
|
|
|
// test-level `request` fixture's APIRequestContext (verified
|
|
|
|
|
// against Playwright docs: `testOptions.extraHTTPHeaders` is
|
|
|
|
|
// shared between browser context and APIRequestContext
|
|
|
|
|
// construction). This is why `tests/smoke/app.smoke.spec.ts`'s
|
|
|
|
|
// third test (`request.get('/api/health')`) reaches the
|
|
|
|
|
// protected preview without re-injecting the header in the
|
|
|
|
|
// spec body. If a future hotfix shows the header NOT
|
|
|
|
|
// propagating to APIRequestContext, see Risk R1 in the
|
|
|
|
|
// convoy file's Architecture section.
|
|
|
|
|
extraHTTPHeaders: BYPASS_SECRET
|
|
|
|
|
? { 'x-vercel-protection-bypass': BYPASS_SECRET }
|
|
|
|
|
: undefined,
|
|
|
|
|
// Trace OFF this convoy. Enabling it would land the bypass
|
|
|
|
|
// header in the HAR payload (Risk R6); a future polish convoy
|
|
|
|
|
// owns the trace-on + HAR-sanitization decision.
|
|
|
|
|
trace: 'off',
|
|
|
|
|
screenshot: 'off',
|
|
|
|
|
video: 'off',
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
projects: [
|
|
|
|
|
{
|
|
|
|
|
// `Preview smoke` workflow invokes `--project=smoke` (per
|
|
|
|
|
// preview-smoke.yml line 98). The testMatch keeps the
|
|
|
|
|
// visual specs out of this project.
|
|
|
|
|
name: 'smoke',
|
|
|
|
|
testMatch: 'smoke/**/*.spec.@(ts|js)',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
// `Screenshot diff` workflow invokes `--project=visual`
|
|
|
|
|
// (per visual-diff.yml line 82). The testMatch keeps the
|
|
|
|
|
// smoke specs out of this project (so a `--project=visual`
|
|
|
|
|
// run doesn't redundantly execute the smoke tests).
|
|
|
|
|
name: 'visual',
|
|
|
|
|
testMatch: 'visual/**/*.spec.@(ts|js)',
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
});
|