DEV Community

Custodia-Admin
Custodia-Admin

Posted on • Originally published at pagebolt.dev

Visual regression testing with screenshots: catch UI bugs before users do

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

  1. Capture a baseline screenshot of each page you want to protect
  2. After each deployment, capture a current screenshot of the same pages
  3. 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
Enter fullscreen mode Exit fullscreen mode

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}`);
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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/
Enter fullscreen mode Exit fullscreen mode

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)