CI-runnable visual snapshot testing: capture DOM snapshots in your e2e tests, re-render and pixel-diff them against human-approved baselines on a server, review and approve in a web viewer.
| Path | What | Stack |
|---|---|---|
packages/client/ |
npm library installed into target projects; captures and uploads snapshots | TypeScript |
backend/ |
receives snapshots, re-renders, pixel-diffs against baselines | Python / Flask |
viewer/ |
web UI for runs, diffs, and baseline approval | React + Vite + TypeScript |
docs/ |
the frozen contracts: snapshot format · HTTP API · self-hosting |
Each package documents its public surface in a CODEMAP.md.
Add PixelPerfectSnapshot to your own e2e test suite.
- Install the client in your project:
npm install --save-dev pixelperfectsnapshot
- Start the backend. Simplest for local use — a Python venv running Flask directly:
(see
cd backend python3 -m venv .venv && .venv/bin/pip install -e '.[dev]' .venv/bin/flask --app app run # serves on http://localhost:5000 by default
backend/CODEMAP.mdfor the full command list). Alternatively, rundocker compose up(see "Running with Docker" below) — in that case the API is reached through the viewer's proxy athttp://localhost:8080, not:5000; use that as yourserverUrlinstead. - Capture, upload, and process a snapshot from your own test code (assumes the local-venv
backend above, reachable at
:5000):import { captureSnapshot, createRun, sendSnapshots, processRun } from "pixelperfectsnapshot"; const { id: runId } = await createRun({ serverUrl: "http://localhost:5000" }); const snapshot = await captureSnapshot(document, "my-page"); await sendSnapshots([snapshot], { serverUrl: "http://localhost:5000", runId }); await processRun({ serverUrl: "http://localhost:5000", runId });
- Review and approve in the viewer (
http://localhost:8080underdocker compose up): open the run, inspect the diff, and approve a snapshot to promote it to the new baseline.
For the full HTTP contract see docs/API.md, for the client's public API see packages/client/README.md, and for a complete working example see examples/demo-app.
npm install # TS workspaces (packages/client, viewer)
npm run lint && npm test
cd backend
python3 -m venv .venv && .venv/bin/pip install -e '.[dev]'
.venv/bin/ruff check . && .venv/bin/pytestdocker compose up --build # first run (or plain `docker compose up` after images are built)This builds and starts the backend and viewer services. The viewer is reachable at
http://localhost:8080; nginx reverse-proxies its /api/* requests to the backend container, so
the backend itself is not published to the host.
Data persists in the pps-data named volume, mounted at /data inside the backend container
(PPS_DATA_DIR=/data). It holds pps.sqlite3 (run/snapshot metadata), blobs/ (uploaded snapshot
documents), images/ (rendered candidate and diff PNGs), and baselines/ (approved baseline
PNGs) — see backend/CODEMAP.md for the full layout. This data survives docker compose down
(without -v); use docker compose down -v to also delete the volume.
PPS_ALLOWED_ORIGIN (CORS) does not need to be set in this setup: since nginx reverse-proxies
/api/* to the backend, the browser sees everything as same-origin.
The backend container runs the Flask dev server intentionally, not gunicorn/waitress/uwsgi. Each
/api/runs/<run_id>/process call launches one real headless Chromium instance per snapshot
rendered, and "concurrent calls for the same run may duplicate render work" (see
docs/API.md) — the system is designed around single-worker, roughly-serial
processing. A multi-worker WSGI server would just multiply concurrent Chromium instances for no
benefit. This is a self-hosted local tool, not built for concurrent multi-worker load.
This docker-compose.yml deploys backend and viewer together, same host. To deploy them on
separate hosts/domains instead (e.g. viewer on a static host/CDN, backend elsewhere), see
docs/SELF_HOSTING.md — it also has the full environment variable
reference (auth, CORS, diffing thresholds) that applies either way.