Visual Regression Testing with Screenshots: Catch UI Bugs Before Users Do
A CSS change that breaks the header on mobile. A font that reverts to the wrong weight after a dependency update. A button that disappears because a class name changed.
These bugs are invisible to unit tests. They're easy to miss in code review. Users find them.
Visual regression testing catches them automatically: take a screenshot before a deployment, take one after, diff the pixels. If anything changed unexpectedly, the test fails.
Here's a lightweight implementation using PageBolt and a pixel-diff library.
The approach
- Capture a baseline screenshot of each page you want to protect
- After each deployment, capture a current screenshot of the same pages
- Diff the images — flag anything above a pixel threshold
No Playwright. No Chromium binary in CI. No Storybook. Just API calls and a diff.
Setup
npm install pixelmatch pngjs
Capture baseline screenshots
Run this once to establish your baseline. Commit the PNGs to your repo.
// capture-baseline.js
import fs from 'fs/promises';
const pages = [
{ name: 'home', url: 'https://yourapp.com' },
{ name: 'pricing', url: 'https://yourapp.com/pricing' },
{ name: 'docs', url: 'https://yourapp.com/docs' },
];
await fs.mkdir('visual-baselines', { recursive: true });
for (const page of pages) {
const res = await fetch('https://api.pagebolt.dev/v1/screenshot', {
method: 'POST',
headers: { 'x-api-key': process.env.PAGEBOLT_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ url: page.url, fullPage: true, blockBanners: true })
});
await fs.writeFile(`visual-baselines/${page.name}.png`, Buffer.from(await res.arrayBuffer()));
console.log(`✓ baseline: ${page.name}`);
}
Compare after deployment
// visual-regression.js
import fs from 'fs/promises';
import { PNG } from 'pngjs';
import pixelmatch from 'pixelmatch';
const pages = [
{ name: 'home', url: 'https://yourapp.com' },
{ name: 'pricing', url: 'https://yourapp.com/pricing' },
{ name: 'docs', url: 'https://yourapp.com/docs' },
];
await fs.mkdir('visual-diffs', { recursive: true });
let failed = false;
for (const page of pages) {
// Capture current screenshot
const res = await fetch('https://api.pagebolt.dev/v1/screenshot', {
method: 'POST',
headers: { 'x-api-key': process.env.PAGEBOLT_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ url: page.url, fullPage: true, blockBanners: true })
});
const currentBuf = Buffer.from(await res.arrayBuffer());
// Load both images
const baseline = PNG.sync.read(await fs.readFile(`visual-baselines/${page.name}.png`));
const current = PNG.sync.read(currentBuf);
const { width, height } = baseline;
const diff = new PNG({ width, height });
const changedPixels = pixelmatch(baseline.data, current.data, diff.data, width, height, {
threshold: 0.1
});
const diffPct = (changedPixels / (width * height) * 100).toFixed(2);
if (changedPixels > 100) { // allow minor anti-aliasing differences
await fs.writeFile(`visual-diffs/${page.name}-diff.png`, PNG.sync.write(diff));
console.error(`✗ ${page.name}: ${diffPct}% changed (${changedPixels}px) — diff saved`);
failed = true;
} else {
console.log(`✓ ${page.name}: no significant changes`);
}
}
if (failed) process.exit(1);
GitHub Actions integration
name: Visual Regression
on:
deployment_status:
jobs:
visual-check:
if: github.event.deployment_status.state == 'success'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install deps
run: npm install pixelmatch pngjs
- name: Run visual regression
env:
PAGEBOLT_API_KEY: ${{ secrets.PAGEBOLT_API_KEY }}
run: node visual-regression.js
- name: Upload diffs on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: visual-diffs
path: visual-diffs/
The diff PNGs upload as artifacts when the check fails — reviewers can see exactly what changed at a glance.
No browser driver. No Storybook dependency. Baseline PNGs live in your repo and evolve with intentional design changes. Unexpected changes fail the build.
Free tier: 100 requests/month. → pagebolt.dev
Top comments (0)