Visual Regression Testing skill for Codex
Every pixel diff is real, every baseline reviewed. Codex sets up screenshot tests that only report real visual changes. It freezes time, fonts, data and animations, masks volatile regions, renders baselines in the same container CI uses, and keeps thresholds tight. Baseline updates go through a review step instead of a blanket update to make the build pass.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- visual-regression-testing Installs to ~/.agents/skills/visual-regression-testing/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install visual-regression-testing The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project visual-regression-testing Installs to .agents/skills/visual-regression-testing/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/visual-regression-testing.zipunzip -o visual-regression-testing.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) visual-regression-testing PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $visual-regression-testing, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Sets up screenshot-based visual regression tests with Playwright toHaveScreenshot or Chromatic for Storybook - deterministic rendering (fonts, animations, time, data), masking dynamic regions, diff thresholds, platform-matched baselines in Docker, baseline update and review workflow, and CI wiring. Use when CSS or design-system changes keep causing unnoticed UI breakage, when adding screenshot tests to components or pages, when visual tests flake on pixel noise, or when baselines differ between macOS and CI.
Use it when
- CSS or design-system changes break layouts unnoticed
- Adding screenshot tests for pages or components
- Visual tests flake on anti-aliasing, fonts or timestamps
- Baselines differ between local machines and CI
Use something else when
- You need behavioral E2E assertions, not pixels: use playwright-e2e
- You want automated accessibility checks: use accessibility-testing
How to check the result
Two consecutive runs in the CI container produce zero diffs, and a deliberate one-line CSS change fails only the affected snapshots with a localized diff.
The mistake it prevents
Committing baselines generated on macOS and comparing them on Linux CI, then raising thresholds until the font-rendering noise disappears along with real regressions.
A visual suite earns its keep when every diff it reports is a real visual change, baselines are rendered in the same environment that compares them, and updating a baseline is a reviewed decision rather than a reflex to turn CI green.
Inspect first
- Read
AGENTS.md, then look for existing visual tooling:toHaveScreenshotin Playwright specs,__screenshots__or*-snapshots/folders, a Chromatic project token in CI, Storybook config, or Vitest browsertoMatchScreenshot. - Find where baselines are generated today. Baselines produced on a developer’s Mac and compared on Linux CI differ in font rendering and will never pass reliably.
- Check whether Storybook exists. Component stories are the cheapest visual surface.
- Note that Docker images, browser downloads and Chromatic uploads need network access; Chromatic also needs a project token stored as a CI secret.
Choose the approach
| Situation | Approach |
|---|---|
| Design system or component library with Storybook | Chromatic (hosted review UI, TurboSnap), or Storybook test runner + Playwright screenshots self-hosted |
| App pages and flows already tested with Playwright | expect(page).toHaveScreenshot() in a dedicated visual project |
| Components tested with Vitest browser mode | toMatchScreenshot in a browser project |
| Budget for no hosted service, strict data residency | Self-hosted Playwright baselines in git |
Start with a small set: core layout pages and the 10-20 most reused components in their key states. Screenshot everything and the team stops reading diffs.
Deterministic rendering checklist
- Fonts: self-hosted and loaded before capture (
await page.evaluate(() => document.fonts.ready)); no network font that may time out - Animations and transitions disabled (
animations: "disabled", pluscaret: "hide") - Time frozen (
page.clock.setFixedTime(...)) for relative dates and countdowns - Data seeded or mocked; no live feeds, ads, avatars from third-party URLs
- Viewport and device scale factor fixed per project
- Dynamic regions masked (timestamps, user ids, charts with random data, maps)
- Lazy images scrolled into view or loaded eagerly in the test build
- Same OS, browser build and fonts for baseline creation and comparison
Playwright setup
// playwright.config.ts (excerpt)
export default defineConfig({
expect: {
toHaveScreenshot: { maxDiffPixelRatio: 0.002, animations: "disabled", caret: "hide" },
},
snapshotPathTemplate: "{testDir}/__screenshots__/{projectName}/{testFilePath}/{arg}{ext}",
projects: [
{
name: "visual-chromium",
testMatch: /.*\.visual\.ts/,
use: { ...devices["Desktop Chrome"], viewport: { width: 1280, height: 800 } },
},
],
});
// e2e/pricing.visual.ts
import { test, expect } from "@playwright/test";
test("pricing page", async ({ page }) => {
await page.clock.setFixedTime(new Date("2026-03-01T10:00:00Z"));
await page.route("**/api/plans", (r) => r.fulfill({ path: "e2e/fixtures/plans.json" }));
await page.goto("/pricing");
await page.evaluate(() => document.fonts.ready);
await expect(page).toHaveScreenshot("pricing.png", {
fullPage: true,
mask: [page.getByTestId("live-chat-badge")],
});
});
test("plan card states", async ({ page }) => {
await page.goto("/pricing");
const card = page.getByRole("article", { name: "Team plan" });
await expect(card).toHaveScreenshot("team-card.png");
await card.hover();
await expect(card).toHaveScreenshot("team-card-hover.png");
});
- Element screenshots (
locator.toHaveScreenshot) are more stable and produce clearer diffs than full pages; use full pages only for layout. stylePathcan inject a CSS file during capture to hide noise globally (scrollbars, blinking cursors) without changing the app.- The default template includes platform in file names; the custom template above drops it, so baselines must always be generated in the same container.
Baselines in the CI environment
Generate and compare inside the official Playwright image matching the installed
@playwright/test version:
docker run --rm --ipc=host -v "$PWD":/work -w /work \
mcr.microsoft.com/playwright:v<version>-noble \
npx playwright test --project=visual-chromium --update-snapshots
--update-snapshots (-u) accepts modes in recent versions (changed, missing,
all); missing writes only new baselines without touching existing ones.
Chromatic with Storybook
npx chromatic --project-token="$CHROMATIC_PROJECT_TOKEN" --only-changed --exit-zero-on-changes
--only-changed(TurboSnap) snapshots only stories affected by the git diff; it needs full git history in CI (fetch-depth: 0).--exit-zero-on-changeskeeps the build green while visual changes wait for review in Chromatic; the PR status check from Chromatic is the gate.--auto-accept-changes mainaccepts on the main branch after merge so main becomes the new baseline.- Story-level control through parameters:
export const Loading: Story = {
parameters: {
chromatic: { delay: 300, diffThreshold: 0.2, pauseAnimationAtEnd: true },
},
};
export const Playground: Story = { parameters: { chromatic: { disableSnapshot: true } } };
- Capture each meaningful state as its own story (default, hover via play function, error, empty, long text, RTL, dark mode) instead of one story with knobs.
- Use
modes(viewports, themes) only for components that change across them; every mode multiplies snapshot count and cost.
Thresholds
| Setting | Use |
|---|---|
maxDiffPixels: 0 / ratio 0 | static icons, logos in a pinned environment |
maxDiffPixelRatio around 0.001-0.005 | pages and components, absorbs anti-aliasing |
threshold (per-pixel color distance, 0-1) | raise slightly only for gradient or image-heavy areas |
| Masks | anything whose content legitimately varies |
Raising thresholds to silence a flaky diff hides real one-pixel regressions such as a border disappearing. Remove the nondeterminism first.
Review workflow
- CI fails (or Chromatic marks changes) on a PR.
- Open the diff artifact or Chromatic review. Classify each diff: intended, regression, or noise.
- Intended: update baselines in the CI container (or accept in Chromatic) in the same PR, and mention it in the PR description.
- Regression: fix the CSS; do not update.
- Noise: fix the source of nondeterminism (font, time, data, animation), then rerun.
Never run --update-snapshots across the whole suite to make a red build green without
looking at each diff. Ask the user before bulk-accepting.
Gotchas
- Baselines committed from macOS, compared on Linux: always fails. Regenerate in Docker.
- Git LFS or large PNG churn bloats the repo; keep the set small or use a hosted service.
- Web fonts with
font-display: swapcapture the fallback font if the test does not wait. - Hover and focus states leak between screenshots when the mouse stays put; move the
mouse away (
page.mouse.move(0, 0)) before the next capture. - Visual tests do not prove accessibility or behavior; keep functional assertions in
regular E2E (
$playwright-e2e) and component tests.
Verification
- Run the visual project twice in the CI container without code changes: zero diffs.
- Change one CSS value (padding, color) deliberately: the affected snapshots fail and the diff image highlights only that area. Revert.
- Confirm baselines in git were produced by the container, not a local OS.
- In Chromatic, confirm the PR check is required and TurboSnap reports affected stories.
What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Visual Regression Testing"
short_description: "Every pixel diff is real, every baseline reviewed"
default_prompt: "$visual-regression-testing Add visual regression tests for our pricing page and plan cards with Playwright" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.