@corralimited/snapdiff-stabilize
v0.1.0
Published
Capture-stability primitives for SnapDiff: pre-navigation init scripts, anti-animation CSS, deterministic clock, and post-load image stabilization. Used by the SnapDiff backend and the @corralimited/snapdiff-playwright reporter; usable standalone with any
Downloads
82
Maintainers
Readme
@corralimited/snapdiff-stabilize
Capture-stability primitives shared by SnapDiff and the
@corralimited/snapdiff-playwright
reporter. Usable standalone with any Playwright project that wants Percy/Chromatic-grade capture
stability.
What it does
- Anti-animation CSS — cancels CSS animations and transitions, hides the text caret, hides scrollbars. Eliminates frame-timing flake and OS-specific scrollbar widths.
- Deterministic clock — pins
Date.now(),new Date(), andMath.random()to fixed values. Eliminates "X minutes ago" and randomized-content false positives. - Image-set stability poll — waits for
<img>count andcompletestate to stabilize across two consecutive ticks, with a configurable total budget. Far more reliable on slow networks and progressive UIs than a fixed per-image timeout. - Lazy-load triggering — step-scrolls through full-page captures to wake
IntersectionObserver-based lazy loaders along the way, not just at the final position. - Hover and focus reset — moves the pointer off the page and blurs
document.activeElementso:hoverand:focusstyles drop before the screenshot.
Usage
Two paths depending on whether the caller owns the Playwright BrowserContext.
A. You own the context (recommended — server-side captures)
import { chromium } from 'playwright';
import { buildInitScript, stabilizePostLoad } from '@corralimited/snapdiff-stabilize';
const browser = await chromium.launch();
const context = await browser.newContext({ deviceScaleFactor: 2 });
// Pre-navigation: install the CSS injector and clock freeze before any
// author script runs. Animations can't fire, timestamps render frozen.
await context.addInitScript({ content: buildInitScript({ freezeClock: true }) });
const page = await context.newPage();
await page.goto('https://example.com', { waitUntil: 'load' });
// Post-load: settle fonts, lazy images, and reset hover/focus. CSS and
// clock effects are already applied via the init script — leave the post-
// load CSS/clock flags off.
await stabilizePostLoad(page, { fullPage: false });
const png = await page.screenshot({ type: 'png' });B. You don't own the context (client-side captures inside a test)
When the user's Playwright test owns the context, addInitScript is unavailable to you. Apply
the effects post-load on a best-effort basis:
import { test } from '@playwright/test';
import { stabilizePostLoad } from '@corralimited/snapdiff-stabilize';
test('account page', async ({ page }) => {
await page.goto('/account');
await stabilizePostLoad(page, {
fullPage: false,
injectStabilizeCss: true, // CSS not in place yet — inject now
freezeClock: true, // optional; off by default
});
await page.screenshot({ path: 'account.png' });
});The trade-off: animations that already fired during page load can't be undone. Anything still in flight is neutralized before the screenshot.
API
Constants
FIXED_TIME—Date.UTC(2024, 0, 1, 0, 0, 0). Bumping this is a baseline-breaking change.RANDOM_SEED—1337. LCG seed for the deterministicMath.random().LAZY_IMAGE_STABILIZE_MS—5000. Total budget for the image stability poll.PER_IMAGE_TIMEOUT_MS—3000. Per-image fallback inside the poll.STABILIZE_CSS— the raw CSS string injected by the stabilizer.
Script builders
Return strings suitable for BrowserContext.addInitScript({ content }) or page.evaluate.
buildStabilizeCssScript()— CSS-injection script withMutationObserverfallback for pre-documentElementexecution.buildClockFreezeScript({ fixedTime?, randomSeed? })—Date/Math.randomshim.buildInitScript({ freezeClock?, fixedTime?, randomSeed? })— combined script foraddInitScript.
Post-load helper
stabilizePostLoad(page, opts)— runs the post-load settle sequence.fullPage?: boolean— step-scroll for lazy-load triggering.injectStabilizeCss?: boolean— inject CSS post-load (when no init script ran).freezeClock?: boolean— apply the clock shim post-load.lazyImageStabilizeMs?: number— override total poll budget.perImageTimeoutMs?: number— override per-image fallback.
License
MIT — see LICENSE.
