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.heightconst 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-regressionon:pull_request:branches: [ main ]jobs:test:runs-on: ubuntu-lateststeps:- uses: actions/checkout@v4- uses: actions/setup-node@v4with:node-version: 20- run: npm ci- name: Capture current screenshotsenv:SUPACRAWLER_API_KEY: ${{ secrets.SUPACRAWLER_API_KEY }}run: node scripts/capture-current.js- name: Run visual diffsrun: node scripts/run-diffs.js- name: Upload artifactsif: always()uses: actions/upload-artifact@v4with:name: visual-diffspath: diffs/**
Implementation notes:
- Keep devices/viewport identical between baseline and current runs
- Use
wait_until: 'networkidle'
and optionalwait_for_selector
for SPAs - Block noisy UI (cookie banners, chats) with
block_*
andhide_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.