·SavePage Team

Integrating Screenshots into CI/CD Pipelines

ci-cdtestinggithub-actions

Adding visual testing to your CI/CD pipeline catches design regressions before they reach production. With a screenshot API, you do not need to install browsers or manage rendering infrastructure in your CI environment.

GitHub Actions example

A complete workflow that captures screenshots of key pages after deployment and compares them to baseline images:

name: Visual Regression Test
on:
  pull_request:
    branches: [main]

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

      - name: Deploy to preview
        id: deploy
        run: |
          # Your deployment step here
          echo "preview_url=https://preview-${{ github.sha }}.example.com" >> $GITHUB_OUTPUT

      - name: Capture screenshots
        env:
          API_KEY: ${{ secrets.SAVEPAGE_API_KEY }}
          PREVIEW_URL: ${{ steps.deploy.outputs.preview_url }}
        run: |
          mkdir -p screenshots/current

          for page in "/" "/pricing" "/docs" "/about"; do
            filename=$(echo "$page" | sed 's/\//_/g' | sed 's/^_//')
            [ -z "$filename" ] && filename="home"

            curl -s "https://api.savepage.io/v1/?url=${PREVIEW_URL}${page}&width=1440&height=900&format=png" \
              -H "Authorization: Bearer $API_KEY" \
              | jq -r '.image' \
              | xargs curl -s -o "screenshots/current/${filename}.png"
          done

      - name: Compare with baseline
        run: |
          pip install Pillow numpy
          python scripts/compare_screenshots.py

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

The comparison script

A Python script that compares current screenshots against baseline images:

import sys
from pathlib import Path
import numpy as np
from PIL import Image

BASELINE_DIR = Path("screenshots/baseline")
CURRENT_DIR = Path("screenshots/current")
DIFF_DIR = Path("screenshots/diff")
THRESHOLD = 0.01  # 1% difference tolerance

DIFF_DIR.mkdir(exist_ok=True)

failures = []

for current_file in CURRENT_DIR.glob("*.png"):
    baseline_file = BASELINE_DIR / current_file.name

    if not baseline_file.exists():
        print(f"NEW: {current_file.name} (no baseline)")
        continue

    current = np.array(Image.open(current_file))
    baseline = np.array(Image.open(baseline_file))

    if current.shape != baseline.shape:
        print(f"FAIL: {current_file.name} (size changed)")
        failures.append(current_file.name)
        continue

    diff = np.abs(current.astype(int) - baseline.astype(int))
    diff_ratio = np.count_nonzero(diff > 10) / diff.size

    if diff_ratio > THRESHOLD:
        print(f"FAIL: {current_file.name} ({diff_ratio:.2%} changed)")
        failures.append(current_file.name)

        # Save diff image
        diff_image = Image.fromarray(
            np.clip(diff * 5, 0, 255).astype(np.uint8)
        )
        diff_image.save(DIFF_DIR / f"diff_{current_file.name}")
    else:
        print(f"PASS: {current_file.name} ({diff_ratio:.2%} changed)")

if failures:
    print(f"\n{len(failures)} visual regression(s) detected")
    sys.exit(1)

Updating baselines

When visual changes are intentional (new design, new feature), update the baseline images:

# After verifying the changes look correct
cp screenshots/current/* screenshots/baseline/
git add screenshots/baseline/
git commit -m "Update visual regression baselines"

GitLab CI example

visual-test:
  stage: test
  image: python:3.11
  script:
    - pip install requests Pillow numpy
    - python scripts/capture_screenshots.py $CI_ENVIRONMENT_URL
    - python scripts/compare_screenshots.py
  artifacts:
    when: on_failure
    paths:
      - screenshots/diff/
  only:
    - merge_requests

Best practices

Capture key pages, not every page. Focus on pages with distinct layouts: homepage, pricing, documentation, login, and the most-visited content pages. 10-20 pages provide good coverage without excessive API usage.

Use consistent viewport dimensions. Always use the same width, height, and scale factor. Inconsistent dimensions produce false positives in comparisons.

Allow a small tolerance. Font rendering and anti-aliasing can produce 0.1-0.5% pixel differences between captures. A 1% threshold avoids false positives while catching real regressions.

Store baselines in the repo. Baseline images should be committed alongside the code. This keeps them in sync with the codebase and provides a history of visual changes.

Run on pull requests. Visual tests are most useful as a PR check, before changes merge to main.