Back to Blog

How to Use Screenshot APIs for Visual Regression Testing

10 min readMarch 14, 2026

Your test suite passes. Your unit tests are green. Your integration tests confirm the API responds correctly. But when you deploy, the checkout button has disappeared behind a div, the header overlaps the hero section, and mobile users see a blank white page. Welcome to the world of visual regressions -- bugs that only the human eye (or a screenshot) can catch.

What is Visual Regression Testing?

Visual regression testing captures screenshots of your application and compares them against known-good baseline images. When pixels differ beyond a threshold, the test flags a visual regression. This catches:

Why Use a Screenshot API Instead of Puppeteer Directly?

You could run Puppeteer in your CI pipeline. But there are significant advantages to using a managed screenshot API:

ConsiderationSelf-hosted PuppeteerScreenshot API
CI setupInstall Chrome, configure headless, manage dependenciesSingle HTTP call, no browser needed
ConsistencyVaries by CI runner OS, Chrome version, fontsIdentical environment every time
SpeedCold start: 5-10s per browser launchWarm pool: 1-3s per screenshot
MaintenanceChrome updates, memory leaks, crash handlingZero maintenance, automatic retries
ParallelismLimited by CI runner memoryUnlimited parallel requests

Architecture: Screenshot API + Visual Diff in CI/CD

Here is the high-level flow for integrating visual regression testing into your deployment pipeline:

  1. Deploy to staging: Push your branch and deploy to a preview/staging URL
  2. Capture screenshots: Use the screenshot API to capture key pages at multiple viewports
  3. Compare against baseline: Pixel-diff the new screenshots against saved baselines
  4. Report results: Post diff results as a GitHub PR comment with highlighted changes
  5. Gate deployment: Block production deploy if visual diffs exceed threshold

Implementation: Node.js Visual Testing Script

Here is a complete implementation using ScreenshotAPI and the pixelmatch library for image comparison:

const fs = require('fs');
const { PNG } = require('pngjs');
const pixelmatch = require('pixelmatch');

const API_KEY = process.env.SCREENSHOT_API_KEY;
const BASE_URL = 'https://screenshotapi-api-production.up.railway.app';

// Pages and viewports to test
const testCases = [
  { name: 'homepage-desktop', url: '/home', width: 1920, height: 1080 },
  { name: 'homepage-mobile', url: '/home', width: 375, height: 812 },
  { name: 'pricing-desktop', url: '/pricing', width: 1920, height: 1080 },
  { name: 'checkout-desktop', url: '/checkout', width: 1920, height: 1080 },
  { name: 'dashboard-desktop', url: '/dashboard', width: 1920, height: 1080 },
];

async function captureScreenshot(pageUrl, width, height) {
  const url = `${process.env.STAGING_URL}${pageUrl}`;
  const params = new URLSearchParams({
    url,
    width: String(width),
    height: String(height),
    format: 'png',
    wait: '2000',
    retries: '2',  // Auto-retry on transient failures
  });

  const res = await fetch(`${BASE_URL}/v1/screenshot?${params}`, {
    headers: { 'Authorization': `Bearer ${API_KEY}` },
  });

  if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
  return Buffer.from(await res.arrayBuffer());
}

async function compareImages(baselinePath, currentBuffer) {
  if (!fs.existsSync(baselinePath)) {
    // No baseline -- this is a new page, save and pass
    return { isNew: true, diffPercent: 0 };
  }

  const baseline = PNG.sync.read(fs.readFileSync(baselinePath));
  const current = PNG.sync.read(currentBuffer);

  // Handle size differences
  if (baseline.width !== current.width || baseline.height !== current.height) {
    return { isNew: false, diffPercent: 100, reason: 'size-mismatch' };
  }

  const diff = new PNG({ width: baseline.width, height: baseline.height });
  const mismatchedPixels = pixelmatch(
    baseline.data, current.data, diff.data,
    baseline.width, baseline.height,
    { threshold: 0.1 }
  );

  const totalPixels = baseline.width * baseline.height;
  const diffPercent = (mismatchedPixels / totalPixels) * 100;

  return { isNew: false, diffPercent, mismatchedPixels, diffImage: PNG.sync.write(diff) };
}

async function runVisualTests() {
  const results = [];
  const THRESHOLD = 0.5; // 0.5% pixel difference allowed

  for (const test of testCases) {
    console.log(`Capturing: ${test.name}...`);
    const screenshot = await captureScreenshot(test.url, test.width, test.height);

    const baselinePath = `./baselines/${test.name}.png`;
    const comparison = await compareImages(baselinePath, screenshot);

    const passed = comparison.isNew || comparison.diffPercent <= THRESHOLD;

    results.push({
      name: test.name,
      passed,
      ...comparison,
    });

    // Save current screenshot
    fs.writeFileSync(`./current/${test.name}.png`, screenshot);
    if (comparison.diffImage) {
      fs.writeFileSync(`./diffs/${test.name}-diff.png`, comparison.diffImage);
    }

    console.log(`  ${passed ? 'PASS' : 'FAIL'} - ${comparison.diffPercent.toFixed(2)}% diff`);
  }

  // Summary
  const failed = results.filter(r => !r.passed);
  if (failed.length > 0) {
    console.error(`\n${failed.length} visual regression(s) detected!`);
    process.exit(1);
  } else {
    console.log('\nAll visual tests passed.');
  }
}

runVisualTests();

GitHub Actions Integration

Add this workflow to your GitHub Actions to run visual tests on every pull request:

# .github/workflows/visual-tests.yml
name: Visual Regression Tests

on:
  pull_request:
    branches: [main]

jobs:
  visual-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20

      - name: Install dependencies
        run: npm ci

      - name: Deploy to preview
        id: deploy
        run: |
          # Your preview deploy command here
          echo "STAGING_URL=https://preview-${{ github.sha }}.yourdomain.com" >> $GITHUB_OUTPUT

      - name: Run visual regression tests
        env:
          SCREENSHOT_API_KEY: ${{ secrets.SCREENSHOT_API_KEY }}
          STAGING_URL: ${{ steps.deploy.outputs.STAGING_URL }}
        run: node visual-tests.js

      - name: Upload diff artifacts
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: visual-diffs
          path: ./diffs/

Best Practices

  1. Test at multiple viewports. At minimum, test desktop (1920x1080), tablet (768x1024), and mobile (375x812). Many visual regressions only appear at specific breakpoints.
  2. Use the wait parameter. Set a 2-3 second wait to let fonts load, lazy images appear, and animations settle.
  3. Use the retries parameter. Set retries to 2 or 3 to handle transient network issues in CI environments.
  4. Hide dynamic content with CSS injection. Use the css parameter to hide clocks, timestamps, or randomized content that would cause false positives.
  5. Block ads and cookie banners. Use blockads=true to remove overlays that vary between captures.
  6. Set a reasonable diff threshold. Anti-aliasing and font rendering can cause 0.1-0.3% pixel differences. A 0.5% threshold catches real changes while ignoring rendering noise.
  7. Update baselines intentionally. When you deliberately change the UI, update baselines as part of the PR. Never auto-update baselines on failure.

Advanced: Multi-Browser Testing

For comprehensive visual testing, capture screenshots at different conditions:

// Test with dark mode CSS injection
await captureScreenshot('/home', 1920, 1080, {
  css: 'html { color-scheme: dark; }',
});

// Test with different locales (use JS injection)
await captureScreenshot('/pricing', 1920, 1080, {
  js: 'document.documentElement.lang = "sv";',
});

// Test with slow-loading content fully rendered
await captureScreenshot('/dashboard', 1920, 1080, {
  wait_for_selector: '.chart-container canvas',
  wait: 3000,
});

Cost Analysis

For a typical project with 10 pages tested at 3 viewports per PR:

Compare this to the cost of a visual bug reaching production: lost revenue, customer support tickets, emergency hotfixes, and damaged brand trust.

Start visual testing today

Get your API key and add visual regression tests to your CI/CD pipeline in under 30 minutes.

Related Articles