Skip to content
happyprimePublic

About

Visual regression at a glance

Resources

Stars

1 star

Watchers

1 watching

Forks

Repository files navigation

Reglance

Visual regression at a glance. Reglance screenshots a site across viewports, compares each page against a saved baseline, and opens an HTML report of the visual and HTML differences. It installs per-project and is configured with one reglance.json file.

Quick start

  1. Install (downloads a headless Chromium on install):

    npm install @happyprime/reglance --save-dev
  2. Add a reglance.json to the project root. List page paths relative to a default domain:

    {
    	"name": "My Site",
    	"domain": "https://site.test",
    	"paths": {
    		"home": "/",
    		"blog": "/blog",
    		"single-post": "/blog/sample-post"
    	}
    }
  3. Run the workflow:

    npx reglance capture   # screenshot every path (against the good state)
    npx reglance control   # save those captures as the baseline
    # ...make your changes...
    npx reglance capture   # screenshot again
    npx reglance compare   # diff against the baseline and open the report

control is run once to set the baseline; capture + compare are repeated to check changes against it. Output (captures, baseline, report) goes to a self-ignored .reglance/ directory — nothing to add to .gitignore.

reglance.json

Field Required Description
paths yes Map of key → path, or key → { path, mask }. A path may be a full URL to point at a different domain.
domain capture Default domain. A bare host (site.test) becomes https://site.test.
name no Label shown in the report. Defaults to the domain host.
viewports no [{ name, width, height, deviceScaleFactor? }]. Defaults to desktop (1920×1080), mobile (390×844).
output no Output directory. Defaults to .reglance.
pixelmatchOptions no pixelmatch options, e.g. { "threshold": 0.1 }.
timeouts no { goto, settle } in ms. goto bounds navigation; settle bounds each post-scroll wait (network idle, then image load/decode). Defaults { goto: 15000, settle: 8000 }. Raise settle for slow, lazy-loading pages.
blockHosts no Hostnames to block requests to during capture, e.g. ["captcha.example.com"]. Each entry also blocks its subdomains.
imageCache no Serve repeat image requests from a local cache during capture. true for a per-run cache, { "persist": true } to keep it across runs. Off by default.
mask no Selectors painted over in every screenshot, e.g. [".cookie-banner"]. Merged with a path's own mask.
browser no { userAgent, launchOptions }. launchOptions goes to Playwright's chromium.launch().
stabilize no { animations, caret, reducedMotion, timezoneId, locale }. Removes render noise. Unset by default.
navigation no { waitUntil }: "networkidle" (default) or "load".
html no { "normalize": true } strips nonces and asset versions before the HTML diff. Off by default.

domain is only needed by capture; control and compare work on the files already captured. See reglance.example.json for a full example.

blockHosts aborts every request to the listed hosts (and their subdomains) before it leaves the browser. Use it for third-party embeds that keep the network busy and stall capture — CAPTCHA widgets, ad tech, analytics — or that render differently on every load and pollute diffs. Captures wait for the network to go idle, so a widget that polls or retries indefinitely will otherwise time out every viewport on pages that embed it. Entries are bare hostnames; "example.org" blocks example.org and sub.example.org alike.

imageCache keeps a capture run from swarming the origin with the same image requests once per viewport per parallel context. Image requests are intercepted in the browser: the first request for a URL is fetched from the origin and stored under .reglance/image-cache/, and every repeat is answered locally — simultaneous requests for the same URL share a single origin fetch. The full URL, query string included, is the cache key, so CDN resize variants (photo.jpg?w=400 vs photo.jpg?w=800) stay distinct. Nothing in the page is rewritten and only images are cached — the HTML, CSS, and JS under test always load from the origin. With true the cache is cleared at the start of every run, so within-run traffic drops with zero risk of a stale image masking a real change. { "persist": true } keeps the cache across runs — useful when re-capturing repeatedly while iterating on CSS — but a changed origin image will then go unnoticed until you clear it with --fresh-images.

mask paints a pink box over each element a selector matches before the screenshot is taken, so ads, carousels, and cookie banners stop showing up as pixel changes. List site-wide selectors at the top level and page-specific ones on the path:

"mask": [".cookie-banner"],
"paths": {
	"home": { "path": "/", "mask": [".hero-carousel"] },
	"blog": "/blog"
}

Masks only affect the screenshot; the HTML diff still sees the element. Adding a mask diffs against existing controls once, until you re-run reglance control.

browser.launchOptions is passed to chromium.launch() as given. Use it for flags like { "chromiumSandbox": true }; args is appended to, not replaced, so --ignore-certificate-errors stays on for local hosts. browser.userAgent is sent by every capture context.

stabilize takes these Playwright options:

Option Values Applied at
animations "disabled", "allow" screenshot
caret "hide", "initial" screenshot
reducedMotion "reduce", "no-preference" browser context
timezoneId e.g. "UTC" browser context
locale e.g. "en-US" browser context

"animations": "disabled" runs finite CSS animations and transitions to their end and resets infinite ones. Playwright already hides the caret by default. reducedMotion changes what sites that honor prefers-reduced-motion render, so turning it on diffs against existing controls once.

navigation.waitUntil sets what each page load waits for. "networkidle" (the default) waits for 500ms with no network traffic, and a page that never goes quiet times out and is reported as degraded. "load" waits for the load event only; the network-idle wait after scrolling still runs, bounded by timeouts.settle, and a timeout there never marks the capture degraded. Use "load" for pages that poll, stream, or sit behind a bot challenge.

html.normalize removes values that change on every request or deploy before the HTML snapshots are diffed: CSP nonce attributes, _wpnonce values in URLs and hidden inputs, the REST nonce in wpApiSettings and createNonceMiddleware(), and ver= asset versions. The saved snapshots are left as captured. Add your own patterns to remove every match of each:

"html": {
	"normalize": {
		"patterns": ["(?<=data-timestamp=\")\\d+"]
	}
}

Patterns are JavaScript regular expressions, compiled with the g flag. A lookbehind keeps the text before the value. A pattern that matches a newline shifts the line numbers shown in the diff. normalizeHtml(html, patterns) in src/report.mjs is the same function, exported for testing patterns.

A viewport's optional deviceScaleFactor (device pixel ratio) renders the page as it would appear on a higher-density display — use 2 for a retina capture, 3 for some phones. It defaults to 1. Captures sharing a DPR run in one browser context; a new DPR opens a fresh context, so prefer grouping retina and non-retina variants rather than scattering them. Note that a 2× screenshot is twice the pixel dimensions of its 1× counterpart, so changing the deviceScaleFactor of an existing viewport will diff against its controls as fully changed until you re-run reglance control.

A paths value may be a full URL pointing at a different host than domain. This is supported, but capture prints a warning listing such paths so off-domain navigation is a conscious choice — keep configs from trusted sources, since the report renders captured content.

Commands

Command Description
reglance capture Screenshot every path across all viewports into .reglance/captures.
reglance control Promote the latest captures to the baseline (.reglance/controls).
reglance compare Diff the latest captures against the baseline and open an HTML report.

Append path keys to limit a command to specific pages: npx reglance capture home blog.

Options

Flag Command Description
--domain=<host> capture Override the configured domain for this run.
--concurrency=<n> capture Parallel browser contexts (default: 4). Must be a positive integer.
--stagger=<ms> capture Delay between starting contexts (default: 500). 0 disables staggering.
--skip-reload capture Reuse the page between viewports instead of reloading.
--fail-on-degraded capture Exit non-zero if any page failed to load cleanly (for CI). Default: warn, exit 0.
--fresh-images capture Clear a persistent image cache before capturing (see imageCache).
--insecure capture Ignore TLS certificate errors for non-local hosts (already ignored for .test/localhost).
--compare-concurrency=<n> compare Parallel diff workers (default: CPU count − 1). Lower it for very tall pages.
--no-open compare Don't open the report when finished.
--config=<path> any Path to the config file (default: reglance.json).

Per-developer domains

domain is the shared default; override it per run so teammates can point at their own local site without editing the config:

npx reglance capture --domain=site2.test

Trustworthy baselines

A baseline is only useful if it reflects pages that actually loaded. reglance guards against silently baselining bad data:

  • If a page never loads cleanly (after retries), capture reports it as degraded instead of treating it as a success. Add --fail-on-degraded to make the run exit non-zero in CI.
  • capture scrolls each page one viewport at a time so every lazy-loaded image is triggered, then waits (bounded by timeouts.settle) for all images to load and decode before screenshotting — and warns per capture when any image was still loading, instead of silently shipping a partial screenshot.
  • control records each promotion in .reglance/controls/manifest.json and warns when it promoted fewer captures than expected (so the untouched controls are now stale). compare warns when the baseline mixes controls from more than one control run.

Output

.reglance/
	captures/   Latest screenshots + HTML snapshots
	controls/   Baseline screenshots + HTML (+ manifest.json)
	compares/   Diff images
	display/    Downscaled copies of any capture too large for the browser to render
	reports/    The report — open reports/index.html
	assets/     Report stylesheet + script
	image-cache/ Cached image responses (only with imageCache enabled)

The report is a single reports/index.html that embeds every result as JSON and renders three views client-side from the URL hash — a triage overview (grouped by page, changed-only by default, keyboard-navigable), a comparison view (swipe, side-by-side, onion skin, diff overlay, blink), and a unified HTML diff. It opens straight from disk with no network access.

Blink mode

Blink mode alternates between the baseline and the current capture in place, so a shift shows up as movement. Its dwell time is adjustable — a speed slider, or [ / ] to slow down / speed up — and an optional crossfade (f, or the Fade button) eases between the two images instead of hard-cutting, which can make a small change easier to catch than an abrupt swap. Both preferences persist across pages and reloads.

Oversized captures

Browsers refuse to decode an image taller or wider than 32,767px and show it as a broken/corrupt image. Full-page mobile captures cross this easily — a narrow viewport stacks content into a very tall page, and a deviceScaleFactor of 2 doubles the pixel height again. capture warns when it writes a screenshot past the limit, and compare writes a downscaled copy to display/, points the report at it, and flags the view with a "Downscaled" notice. Pixel diffs are still computed against the full-resolution originals; only what the browser paints is shrunk. To capture such a page at full resolution instead, lower that viewport's deviceScaleFactor.

Development

npm run lint   # check style
npm run fix    # auto-fix style

About

Visual regression at a glance

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages