Visual Regression Tests
Build visual regression tests with Claude Code, Next.js 16, and Playwright. Create stable screenshot baselines, review diffs, and block accidental UI changes in CI.
Hören Sie auf zu konfigurieren. Fangen Sie an zu bauen.
SaaS-Builder-Vorlagen mit KI-Orchestrierung.
Visual regression tests compare a rendered page with an approved screenshot and fail when pixels change beyond a defined threshold. In a Next.js 16 project, Claude Code can build and maintain that loop, while Playwright supplies the browser, stable screenshot capture, comparison engine, and failure artifacts.
This is most useful for changes that remain valid to the DOM but damage the interface: a clipped price, a shifted card, an invisible focus ring, or a mobile layout that wraps one line too early. The goal is not to reject every pixel difference. It is to turn meaningful visual changes into a reviewable engineering decision.
What visual regression testing covers
A visual test is one layer of a healthy test suite. It does not replace component assertions or user-flow tests, because a screenshot can look correct while a button is impossible to activate.
| Test type | Best at detecting | Typical blind spot |
|---|---|---|
| Unit or component test | Logic, props, states, and accessibility roles | Cross-component layout |
| End-to-end test | Navigation, forms, and real user flows | Subtle visual damage |
| Screenshot comparison | Spacing, typography, color, clipping, and responsive layout | Behavior hidden behind a correct image |
Playwright's toHaveScreenshot assertion captures the page repeatedly until two consecutive screenshots match, then compares the stable result with the stored baseline. That built-in stabilization helps, but it cannot make a live clock, random avatar, changing advertisement, or late-loading third-party widget deterministic. Those elements still need to be controlled or masked.
If the project has no browser coverage yet, read the Claude Code browser automation guide before expanding this setup. Visual checks work best after the core navigation path is already testable.
Build a deterministic test page
Start with one route and one desktop viewport. A narrow first target makes failures understandable and establishes a baseline-review habit before the suite expands.
The example below creates a small pricing preview with one intentionally dynamic value. Add it as app/visual-test/page.tsx; the test will mask the changing timestamp but verify the rest of the page.
import styles from "./page.module.css";
const features = [
"Unlimited private projects",
"Preview deployments",
"Visual review history",
];
export default function VisualTestPage() {
return (
<main className={styles.shell}>
<section className={styles.hero}>
<p className={styles.eyebrow}>Developer workspace</p>
<h1>Ship interface changes with confidence.</h1>
<p className={styles.intro}>
Review the rendered result before a visual mistake reaches production.
</p>
<article className={styles.card}>
<div>
<p className={styles.plan}>Team</p>
<p className={styles.price}>
$24 <span>per member</span>
</p>
</div>
<ul>
{features.map((feature) => (
<li key={feature}>{feature}</li>
))}
</ul>
<button type="button">Start a workspace</button>
</article>
<p className={styles.build}>
Preview generated at{" "}
<span data-testid="build-time">{new Date().toISOString()}</span>
</p>
</section>
</main>
);
}The CSS creates enough spacing, color, typography, and responsive behavior to make a visual check useful. Save it as app/visual-test/page.module.css; CSS Modules keep the fixture independent of whether the application uses Tailwind.
.shell {
min-height: 100vh;
padding: 72px 24px;
color: #172033;
background:
radial-gradient(circle at 50% 0%, #e0e7ff 0, transparent 38%),
#f8fafc;
}
.hero {
width: min(100%, 720px);
margin: 0 auto;
text-align: center;
}
.eyebrow {
margin: 0 0 12px;
color: #4f46e5;
font-size: 14px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.hero h1 {
max-width: 650px;
margin: 0 auto;
font-size: clamp(40px, 7vw, 68px);
line-height: 0.98;
letter-spacing: -0.045em;
}
.intro {
max-width: 540px;
margin: 24px auto 40px;
color: #526078;
font-size: 19px;
line-height: 1.6;
}
.card {
display: grid;
gap: 28px;
width: min(100%, 460px);
margin: 0 auto;
padding: 32px;
text-align: left;
background: white;
border: 1px solid #dbe2ee;
border-radius: 24px;
box-shadow: 0 24px 70px rgb(43 55 83 / 12%);
}
.plan {
margin: 0 0 8px;
font-weight: 700;
}
.price {
margin: 0;
font-size: 42px;
font-weight: 800;
letter-spacing: -0.04em;
}
.price span {
color: #64748b;
font-size: 15px;
font-weight: 500;
letter-spacing: 0;
}
.card ul {
display: grid;
gap: 12px;
margin: 0;
padding: 0;
list-style: none;
}
.card li::before {
margin-right: 10px;
color: #4f46e5;
content: "✓";
}
.card button {
min-height: 48px;
color: white;
font: inherit;
font-weight: 700;
background: #312e81;
border: 0;
border-radius: 12px;
}
.build {
margin-top: 22px;
color: #64748b;
font-size: 12px;
}
@media (max-width: 520px) {
.shell {
padding: 48px 16px;
}
.card {
padding: 24px;
}
}The timestamp is realistic dynamic content, but it is not the subject of the test. Masking only that value keeps the surrounding sentence, typography, and position under visual coverage.
Install and configure Playwright
Install the Playwright test runner and the Chromium browser it controls. The browser installation is separate because Playwright pins compatible browser binaries for reliable local and CI execution.
Run these commands from the Next.js project root. The final command adds reusable scripts without asking you to edit package.json by hand.
npm install --save-dev @playwright/test
npx playwright install chromium
npm pkg set scripts.test:visual="playwright test"
npm pkg set scripts.test:visual:update="playwright test --update-snapshots"A visual suite should control the environment more tightly than a general end-to-end suite. The configuration below fixes the browser, viewport, color scheme, locale, and time zone, disables animations for screenshot assertions, and keeps CI workers at one to reduce machine-level variance.
Save this complete configuration as playwright.config.ts. The webServer option starts Next.js before the tests and reuses an existing local server outside CI.
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "./tests/visual",
fullyParallel: false,
forbidOnly: Boolean(process.env.CI),
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: process.env.CI
? [["line"], ["html", { open: "never" }]]
: [["list"], ["html", { open: "never" }]],
expect: {
toHaveScreenshot: {
animations: "disabled",
caret: "hide",
scale: "css",
maxDiffPixelRatio: 0.005,
pathTemplate:
"{testDir}/__screenshots__{/projectName}/{testFilePath}/{arg}{ext}",
},
},
use: {
baseURL: "http://127.0.0.1:3000",
colorScheme: "light",
locale: "en-US",
timezoneId: "UTC",
trace: "on-first-retry",
screenshot: "only-on-failure",
},
projects: [
{
name: "chromium-desktop",
use: {
...devices["Desktop Chrome"],
viewport: { width: 1440, height: 1000 },
},
},
],
webServer: {
command: "npm run dev",
url: "http://127.0.0.1:3000",
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
});The ratio allows a very small amount of pixel variation without accepting a visibly broken layout. Tune it from observed, reviewed failures rather than choosing a large tolerance up front. Playwright documents the complete screenshot options in its toHaveScreenshot API.
Write the first screenshot test
Wait for web fonts before capturing the page. A screenshot taken while the fallback font is visible can shift every line even though the application code is correct.
Create tests/visual/visual-test.spec.ts with the following test. The assertion verifies the complete scrollable page and replaces the changing timestamp with Playwright's standard mask color.
import { expect, test } from "@playwright/test";
test("pricing preview matches its approved design", async ({ page }) => {
await page.goto("/visual-test");
await page.evaluate(() => document.fonts.ready);
await expect(page).toHaveScreenshot("pricing-preview.png", {
fullPage: true,
mask: [page.getByTestId("build-time")],
});
});Generate the first approved image locally, inspect it, and commit it with the test. The update command is an approval operation, not a generic repair command.
npm run test:visual:update
npm run test:visual
git status --shortAfter a later UI change, run the normal test command first. If it fails, open playwright-report/index.html with npx playwright show-report, compare the expected, actual, and diff views, then either fix the regression or deliberately regenerate the baseline.
Add visual tests to CI
CI should use the same package lock, browser family, and command as local development. Playwright's CI documentation recommends installing browser dependencies in the workflow, while the HTML report gives reviewers the evidence needed to diagnose a failure.
The following GitHub Actions workflow runs on pull requests and uploads the report only when the visual command fails. Save it as .github/workflows/visual-regression.yml.
name: Visual regression
on:
pull_request:
jobs:
screenshots:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: Install dependencies
run: npm ci
- name: Install Chromium
run: npx playwright install --with-deps chromium
- name: Compare screenshots
run: npm run test:visual
- name: Upload Playwright report
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
retention-days: 7Linux and macOS can render fonts differently. Generate the committed baseline in the same Linux environment used by CI, or use Playwright's official container for both baseline generation and comparison. Do not compensate for operating-system drift with a broad threshold.
For a broader delivery pipeline, pair this job with the checks in the Claude Code GitHub Actions guide. Screenshot approval should remain visible in the pull request instead of becoming an invisible automation side effect.
Give Claude Code review rules
Claude Code is effective at adding page coverage because it can inspect the component, identify dynamic regions, run the test, and explain the diff. It still needs a firm rule against blindly replacing evidence.
Add a focused section to the repository's CLAUDE.md. These instructions make the expected behavior repeatable across sessions and keep snapshot updates under human review.
## Visual regression rules
- Run `npm run test:visual` after changing shared layout, typography, or pricing UI.
- Never run `npm run test:visual:update` only to make a failure disappear.
- Inspect the expected, actual, and diff images before proposing a baseline update.
- Fix unstable data at its source. Mask only content that must change between runs.
- Keep one intentional assertion per screenshot and use descriptive baseline names.
- Report whether each failure is a regression, an intended design change, or instability.A useful Claude Code prompt names the route, viewport, dynamic regions, and acceptance rule. For example: “Add visual coverage for the account settings route at the existing desktop project. Mask only the signed-in user's avatar, run the test, and explain every diff before changing a baseline.”
This process fits naturally beside test-driven development with Claude Code. The behavioral test defines what the feature does; the screenshot defines the approved rendered state.
Grow the suite without making it noisy
Expand by user risk, not by page count. Checkout, authentication, pricing, navigation, and shared responsive shells usually deserve coverage before low-traffic informational pages.
Use a separate Playwright project when a second viewport represents a real design contract. A mobile project at a fixed device profile is clearer than changing viewport dimensions inside one test, and its baseline path remains separate through the project-name token.
Keep these boundaries as the suite grows:
- Mock or seed API data so the same entities render every time.
- Freeze clocks when dates affect layout, and mask only when the exact date is irrelevant.
- Host fonts locally or ensure they finish loading before capture.
- Disable animations through Playwright rather than adding test-only CSS throughout the product.
- Review each updated PNG in the pull request.
- Delete obsolete baselines when routes or projects disappear.
Avoid pixel snapshots for highly personalized dashboards, live maps, videos, and advertising surfaces unless those regions can be made deterministic. A role-based assertion may provide more signal for those areas.
The practical success measure is not the number of images. It is whether a failed comparison points to one understandable decision and gives the reviewer enough evidence to make it quickly.
Frequently asked questions
How do I run visual regression tests with Claude Code?
Give Claude Code one deterministic Playwright command, a small set of screenshot tests, and an explicit rule that baselines change only after a human reviews the diff. Generate and commit the approved baseline, then run the comparison in CI on every relevant pull request.
Should Playwright screenshots be committed to Git?
Yes. Approved baseline PNGs are test inputs and belong in Git beside the test that uses them. Failure artifacts such as the actual image and pixel diff belong in the CI artifact, where reviewers can inspect them without adding them to repository history.
How do I prevent flaky Playwright screenshot tests?
Use the same browser and operating system for baseline creation and CI. Fix the viewport, locale, time zone, color scheme, fonts, and test data; disable animations; wait for font loading; and mask only the smallest region that must change. A large pixel threshold usually hides instability instead of fixing it.
Code Kit is a $29 one-time harness that runs on top of Claude Code. It helps plan, build, evaluate, and test changes with quality gates for zero type errors, zero lint errors, and a clean production build; Claude Code itself requires a paid Anthropic plan.
Posted by @speedy_devv
Hören Sie auf zu konfigurieren. Fangen Sie an zu bauen.
SaaS-Builder-Vorlagen mit KI-Orchestrierung.
CI Repair Agent
Build a guarded Claude Code CI repair agent that reads failed GitHub Actions logs, proposes a bounded patch, and leaves verification and merge control to CI.
Agent Cost Dashboard
Build a Claude Agent SDK cost dashboard in Next.js 16. Record per-run estimates, token usage, cache activity, and model mix without confusing estimates with billing.