Skip to main content
Ssnap Docs
Use cases

Visual regression testing

Capture deterministic screenshots for diffing, and keep false positives out of your pipeline.

Visual diffing only works if two captures of an unchanged page are byte-comparable. Most of the effort is not in taking the screenshot — it is in removing everything that changes between two runs.

A baseline capture

curl -G https://ssnap.cc/api/v1/screenshot \
  -H "Authorization: Bearer $SSNAP_API_KEY" \
  --data-urlencode "url=https://staging.example.com/pricing" \
  --data-urlencode "width=1280" \
  --data-urlencode "height=800" \
  --data-urlencode "format=png" \
  --data-urlencode "network_idle=true" \
  --data-urlencode "delay=500" \
  --output baseline/pricing-1280.png

Every one of those parameters is doing determinism work.

Pin the viewport

Use an explicit width and height rather than a device preset, and pass both — a lone width is ignored. A pinned viewport means a CSS breakpoint change shows up as a diff instead of silently reflowing the whole image.

Run the same page at two or three widths and treat each as its own baseline:

1280×800   desktop
 768×1024  tablet
 390×844   mobile

Device presets are useful when you want a specific user agent as well — see Device emulation and viewports.

Use PNG

png is lossless. JPEG and WebP are lossy, and their encoders can produce different bytes for identical input, which is a diff your tool will happily report. quality is ignored for PNG, so there is nothing to pin.

Kill the non-determinism in the page

This is the part no capture parameter can solve for you. Anything that differs between two runs will diff:

  • Clocks, relative timestamps, "3 minutes ago".
  • Randomised ordering, A/B buckets, rotating testimonials.
  • Live counters, stock levels, exchange rates.
  • Animations mid-flight, carousels, skeleton loaders.
  • Third-party embeds and ads.

Standard fixes: a ?visual-test=1 flag your app reads to freeze the clock and disable animations, seeded fixture data, and stubbed third-party content in the environment you capture.

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0s !important;
    transition-duration: 0s !important;
  }
}

Wait properly, not longer

network_idle=true
delay=500

network_idle waits for network activity to settle, then delay adds a fixed pause. The pause is what covers CSS transitions and font swaps, which produce no network traffic at all.

Do not reach for a huge delay as insurance. The overall render timeout is 30 seconds and a long delay eats into it — a 25-second delay on a slow page times out with capture_failed.

Full page or element

full_page=true captures the whole scroll height, which is the honest test but also the most fragile: one extra row anywhere shifts everything below it, so a one-line change diffs the entire image.

Scoping to a component with selector gives far more useful failures:

  --data-urlencode "selector=#pricing-table"

The selector is capped at 255 characters and the first match is used. Note that a selector matching nothing fails the render with capture_failed rather than falling back — in a test suite that is a feature, since a vanished element should fail. More in Full page and element screenshots.

Turn caching off

cache_ttl is exactly wrong here. A cache hit returns an earlier render, so a genuine regression would be served the old, passing image. Leave cache_ttl unset in test runs — without it, every call renders fresh.

The reverse is worth knowing too: because the cache key includes every output-affecting parameter, changing a single one of them in your test config invalidates all your baselines. Keep the capture parameters in version control next to the baselines.

Running the suite in CI

The per-minute rate limit is per key, and a suite is a burst by nature. Two habits:

- one API key per environment, so CI cannot throttle production traffic
- bounded concurrency, tuned against X-RateLimit-Remaining

Back off on rate_limit_exceeded using Retry-After, but never loop on quota_exceeded — that one lasts until the billing period rolls over. See Rate limits and quotas and API error codes.

For a large suite, callback_url turns the run into a fan-out: fire every capture, let them queue, collect the webhooks. The job budget is 120 seconds per capture rather than the synchronous 30-second render timeout.

Storing baselines

Do not treat a signed url as a baseline. It expires after 24 hours, and stored captures are pruned on their own schedule — see Storage and retention. Commit the bytes, or push them to your own bucket, and diff against those.

Checklist

  • Explicit width and height, one baseline per breakpoint.
  • format=png.
  • network_idle=true plus a modest delay.
  • No cache_ttl.
  • Animations, clocks and random data frozen in the page itself.
  • Capture parameters version-controlled with the baselines.
  • A separate API key for CI.

Next