Back to Blog

Visual Regression Testing in CI with the Screenshots API

Visual diffs are the fastest way to catch unexpected UI changes. Instead of standing up and maintaining Playwright/Puppeteer infrastructure, delegate rendering to Supacrawler’s Screenshots API and run diffs in any CI.

What you’ll build:

  • A baseline snapshot set per page/device
  • A CI job that captures fresh screenshots and compares them to baselines
  • A readable diff report with uploaded artifacts

1) Create baseline screenshots

Capture the initial (approved) state for each page and device. Store the files in your repo (e.g., ./__snapshots__) or in object storage.

Create baseline with SDKs

import { SupacrawlerClient, ScreenshotCreateRequest } from '@supacrawler/js'
import fs from 'fs/promises'
const client = new SupacrawlerClient({ apiKey: process.env.SUPACRAWLER_API_KEY || 'YOUR_API_KEY' })
async function captureBaseline(name, url) {
const job = await client.createScreenshotJob({
url,
device: ScreenshotCreateRequest.device.DESKTOP,
full_page: true,
format: ScreenshotCreateRequest.format.PNG,
wait_until: 'networkidle',
block_ads: true,
})
const result = await client.waitForScreenshot(job.job_id!)
const img = await fetch(result.screenshot).then(r => r.arrayBuffer())
await fs.writeFile(`__snapshots__/${name}.png`, Buffer.from(img))
}
await captureBaseline('home_desktop', 'https://example.com')

Tip: Screenshot URLs are short‑lived signed URLs. Download the file to persist.

2) Compare in CI (pixelmatch / Pillow)

Use a small image diff library to compare the new render to the baseline.

Image diff examples

import { createCanvas, loadImage } from 'canvas'
import pixelmatch from 'pixelmatch'
import fs from 'fs'
function diffImages(baselinePath, currentPath, diffPath) {
return Promise.all([loadImage(baselinePath), loadImage(currentPath)]).then(([base, curr]) => {
const width = base.width, height = base.height
const canvas = createCanvas(width, height)
const ctx = canvas.getContext('2d')
const baseCanvas = createCanvas(width, height)
const baseCtx = baseCanvas.getContext('2d')
baseCtx.drawImage(base, 0, 0)
const currCanvas = createCanvas(width, height)
const currCtx = currCanvas.getContext('2d')
currCtx.drawImage(curr, 0, 0)
const baseImg = baseCtx.getImageData(0, 0, width, height)
const currImg = currCtx.getImageData(0, 0, width, height)
const diffImg = ctx.createImageData(width, height)
const mismatches = pixelmatch(baseImg.data, currImg.data, diffImg.data, width, height, { threshold: 0.1 })
fs.writeFileSync(diffPath, Buffer.from(diffImg.data))
return mismatches
})
}

3) Automate with GitHub Actions

Run on pull requests, upload diffs as artifacts, and fail the job if mismatches are detected.

name: visual-regression
on:
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- name: Capture current screenshots
env:
SUPACRAWLER_API_KEY: ${{ secrets.SUPACRAWLER_API_KEY }}
run: node scripts/capture-current.js
- name: Run visual diffs
run: node scripts/run-diffs.js
- name: Upload artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: visual-diffs
path: diffs/**

Implementation notes:

  • Keep devices/viewport identical between baseline and current runs
  • Use wait_until: 'networkidle' and optional wait_for_selector for SPAs
  • Block noisy UI (cookie banners, chats) with block_* and hide_selectors

4) Scaling tips

  • Parallelize independent pages in CI matrix jobs
  • Prefer PNG for pixel‑perfect UI diffs; use JPEG/WebP for speed in non‑diff flows
  • Periodically refresh baselines after intentional UI changes (protected branch)

With the Screenshots API, you get deterministic rendering without managing browsers. Wire it into CI once, and catch visual bugs before they reach users.

By Supacrawler Team
Published on August 24, 2025