|
| 1 | +--- |
| 2 | +name: page-visual-tests |
| 3 | +description: Playwright + Chromatic full-page visual tests for ethereum.org. Trigger on "add a page to the visual suite", "the snapshot keeps changing", "chromatic pages", "chromatic playwright", or edits to `tests/visual/`, `playwright.visual.config.ts`, or `.github/workflows/chromatic-pages.yml`. Skip for Storybook Chromatic (`chromatic.yml`), e2e (`tests/e2e/`), unit (`tests/unit/`). |
| 4 | +--- |
| 5 | + |
| 6 | +# Page Visual Tests (Playwright + Chromatic) |
| 7 | + |
| 8 | +This repo has two Chromatic projects: Storybook (`chromatic.yml` + `pnpm chromatic`) and **page visual tests** (`chromatic-pages.yml` + `pnpm chromatic:pages`). This skill is the second one only. |
| 9 | + |
| 10 | +The Playwright suite captures DOM archives (not PNGs) per page × viewport; Chromatic re-renders them in the cloud to diff. A green local `pnpm test:visual` just means archives were produced — the diff happens after upload. |
| 11 | + |
| 12 | +## Files that matter |
| 13 | + |
| 14 | +- `playwright.visual.config.ts` — visual-only config (3 viewports + `webServer`) |
| 15 | +- `playwright.config.ts` — base (e2e + unit; **no `webServer`**) |
| 16 | +- `tests/visual/pages.spec.ts` — page list + readiness pattern |
| 17 | +- `.github/workflows/chromatic-pages.yml` — CI |
| 18 | +- `src/components/ui/skeleton.tsx`, `src/components/ui/spinner.tsx` — loading primitives |
| 19 | +- `package.json` scripts: `test:visual*`, `chromatic:pages` |
| 20 | + |
| 21 | +## Non-obvious constraints |
| 22 | + |
| 23 | +**Dual Playwright config.** `webServer` lives only in `playwright.visual.config.ts`. Moving it into the base config breaks `pnpm test:unit` and `pnpm test:e2e` in CI — they try to start Next against a missing `.next` build. |
| 24 | + |
| 25 | +**Desktop viewport is 1024, not 1280.** Chromatic caps snapshots at `width × height ≤ 25M` px. The tallest tested pages reach ~22.5k px; 1280 overflows, 1024 fits. Measure `document.documentElement.scrollHeight` before raising the viewport or adding a long page. |
| 26 | + |
| 27 | +**Loading contract: `data-slot="loading"`.** The shared `Skeleton` and `Spinner` primitives carry this attribute. Each test waits until `document.querySelectorAll('[data-slot="loading"]').length === 0` before snapshotting. Any bespoke loader — raw `animate-pulse-light`, a local Skeleton copy, a custom spinner — is invisible to the wait and will silently flake. Fix by routing through the shared primitive or adding `data-slot="loading"` to the bespoke loader's root. |
| 28 | + |
| 29 | +**Imports come from `@chromatic-com/playwright`, not `@playwright/test`.** The two packages re-export `expect` with skewed types, so `expect(...).toHaveCount(0)` misbehaves — prefer `page.waitForFunction` for the loading wait. |
| 30 | + |
| 31 | +**Environment.** `USE_MOCK_DATA=true` and `NEXT_PUBLIC_BUILD_LOCALES=en` are required at build and test time. Paths in the spec are unprefixed (`/wallets/`, not `/en/wallets/`) because `localePrefix: "as-needed"` serves English at the root — adding `/en` would just trigger a redirect. |
| 32 | + |
| 33 | +**Random ordering: `maybeShuffle`.** Lodash `shuffle` and `.sort(() => Math.random() - 0.5)` flake snapshots independently of loaders. Wrap them with `maybeShuffle` from `src/lib/utils/random.ts` — it returns the list unchanged when `IS_VISUAL_TEST=true`. Current call sites: `wallets.ts`, `apps.ts` (Highlights/Discover/AppOfTheWeek), `useStakingProductsCardGrid.ts`. The env var is exposed to the client bundle via `next.config.js`'s `env` block; without that, `process.env.IS_VISUAL_TEST` evaluates to `undefined` in client components and the shuffle still runs. |
| 34 | + |
| 35 | +**Use `domcontentloaded`, not `networkidle`.** Analytics and background fetches keep the network perpetually busy. |
| 36 | + |
| 37 | +## Canonical test |
| 38 | + |
| 39 | +```ts |
| 40 | +import { takeSnapshot, test } from "@chromatic-com/playwright" |
| 41 | + |
| 42 | +const pages: Array<{ name: string; path: string }> = [ |
| 43 | + { name: "Homepage", path: "/" }, |
| 44 | + { name: "Docs - Smart Contracts", path: "/developers/docs/smart-contracts/" }, |
| 45 | + // ... |
| 46 | +] |
| 47 | + |
| 48 | +test.describe("Page Visual Tests", () => { |
| 49 | + for (const { name, path } of pages) { |
| 50 | + test(name, async ({ page }, testInfo) => { |
| 51 | + await page.goto(path, { waitUntil: "domcontentloaded" }) |
| 52 | + await page.waitForFunction( |
| 53 | + () => document.querySelectorAll('[data-slot="loading"]').length === 0 |
| 54 | + ) |
| 55 | + await takeSnapshot(page, testInfo) |
| 56 | + }) |
| 57 | + } |
| 58 | +}) |
| 59 | +``` |
| 60 | + |
| 61 | +## Common situations |
| 62 | + |
| 63 | +**Adding a page.** Each entry costs three snapshots (one per viewport) against Chromatic's budget, so check whether the page's layout (under `src/layouts/`) is already covered before adding. Scan the page subtree for bespoke loaders — they're the single biggest flake cause — and confirm full-page height stays under the 25M-pixel budget. Local loop: `pnpm test:visual:build` once, then `pnpm test:visual:desktop` for iteration, `pnpm test:visual` for the full sweep. |
| 64 | + |
| 65 | +**Flaky snapshot.** Two main causes. (1) A loader without `data-slot="loading"` — run with `--trace=on` and inspect the `waitForFunction` step; ~0 ms duration means it isn't being waited on. (2) Random ordering — grep the page subtree for `shuffle(`, `Math.random()`, or `.sort(() =>` and route through `safeShuffle`. If dynamic content is drifting, double-check `USE_MOCK_DATA=true` is set in both build and test steps. |
| 66 | + |
| 67 | +**Local `pnpm dev` masks a regression.** `playwright.visual.config.ts` sets `reuseExistingServer: true`, which is correct for CI but means a `pnpm dev` already running on :3000 will be used silently in place of the production build the suite assumes. If a snapshot diff doesn't reproduce in CI, kill the dev server and run `pnpm test:visual:build` to rebuild against the production output before retrying. |
| 68 | + |
| 69 | +**Pixel-limit error.** Measure the page's full-page height at 1024 px; if it exceeds ~24,400 px, the page needs shortening or removal from the suite. Cropping to viewport was considered and rejected — it defeats the below-the-fold regression coverage that justifies using Playwright over Storybook here. |
| 70 | + |
| 71 | +**Works locally, fails in CI.** Usually `HOME: /root` missing from the test step — GitHub Actions overrides `HOME` inside containers, and Playwright can no longer find the browsers baked into the `mcr.microsoft.com/playwright` image. Also check that the image tag matches `@playwright/test` in `package.json`. |
| 72 | + |
| 73 | +Branch: `feat/playwright-chromatic-page-visual-tests` · PR: <https://github.com/ethereum/ethereum-org-website/pull/18009> |
0 commit comments