
Research
/Security News
OpenAPI React Query Codegen Compromised in Mini Shai-Hulud npm Supply Chain Attack
Ten malicious OpenAPI React Query Codegen versions were published to npm in the Mini Shai-Hulud attack, all with valid provenance.
@corralimited/snapdiff-playwright
Advanced tools
SnapDiff visual regression reporter for Playwright. Captures PNGs during your tests, ships them to SnapDiff, gates merges on visual changes.
SnapDiff visual regression for Playwright. Captures PNGs during your tests, uploads them to SnapDiff, and gates merges on visual changes.
The reporter is a thin layer over Playwright. It does not replace your test runner, your assertions, or your authentication setup. Add await snapshot(page, 'name') wherever you want a visual check, and the reporter handles upload, build creation, polling, and gating.
The URL-based SnapDiff GitHub Action covers public routes such as marketing sites, documentation, and any unauthenticated page. It cannot reach routes behind a login.
The Playwright reporter covers authenticated routes by reusing your existing end-to-end tests:
storageState, OAuth, or a login flowThe two packages can be used together. Keep the GitHub Action for routes such as / and /pricing, and use the reporter for /dashboard, /account, and similar authenticated pages. Both write to the same SnapDiff project.
npm install -D @corralimited/snapdiff-playwright
# or
pnpm add -D @corralimited/snapdiff-playwright
# or
yarn add -D @corralimited/snapdiff-playwright
Requires @playwright/test 1.40 or later.
Add it to playwright.config.ts:
import { defineConfig } from '@playwright/test';
export default defineConfig({
reporter: [
['list'],
[
'@corralimited/snapdiff-playwright/reporter',
{
project: 'my-app', // your SnapDiff project slug
// apiKey: process.env.SNAPDIFF_API_KEY, // defaults to SNAPDIFF_API_KEY
// apiUrl: 'https://snapdiff.example.com', // self-hosted? set this
},
],
],
});
The recommended API is the fixture extension:
import { test, expect } from '@corralimited/snapdiff-playwright';
test('account page renders correctly', async ({ page, snapshot }) => {
await page.goto('/account');
await expect(page.locator('h1')).toBeVisible();
await snapshot('account');
});
test('billing settings render correctly', async ({ page, snapshot }) => {
await page.goto('/account/billing');
await snapshot('account-billing');
});
The test import extends @playwright/test with a snapshot fixture. Existing fixtures, hooks, and configuration continue to work.
If you would rather not change the test import, the package also exports a standalone snapshot(page, name):
import { test } from '@playwright/test';
import { snapshot } from '@corralimited/snapdiff-playwright';
test('account page', async ({ page }) => {
await page.goto('/account');
await snapshot(page, 'account');
});
In some peer-dependency configurations, the standalone helper can miss tests when @playwright/test is duplicated under node_modules. The fixture API is unaffected and is the preferred approach.
Snapshot names must be unique across the entire test run. They map directly to baselines in SnapDiff. Use prefixes to organize related snapshots, for example account-overview, account-billing, and settings-profile.
Snapshots are taken only when the test passes. If a functional assertion fails, for example toContainText('Ship') after a heading was renamed to 'Catch', the test fails before snapshot() is reached and SnapDiff never sees the page. The visual change that caused the assertion failure is then never captured for review.
The recommendation is to assert on structure rather than copy:
// Brittle: breaks whenever copy changes
await expect(page.locator('h1')).toContainText('Welcome to Acme');
// Resilient: verifies rendering and lets SnapDiff catch copy changes
await expect(page.locator('h1')).toBeVisible();
To capture a snapshot regardless of test outcome, for example to inspect the page when an assertion fails, wrap the assertion in try / finally:
test('account page', async ({ page, snapshot }) => {
await page.goto('/account');
try {
await expect(page.locator('[data-testid=balance]')).toBeVisible();
} finally {
await snapshot('account');
}
});
Before each snapshot the reporter runs a stabilization pass that fixes the most common sources of flaky captures:
document.fonts.ready so web fonts finish loading before capturefullPage: true captures, scrolls to the bottom and back to trigger IntersectionObserver-based lazy-load<img> to finish decoding (with a 3 s safety cap)This is automatic and not configurable — these fixes are universally desired. You still need to handle anything specific to your app:
await page.waitForSelector('[data-loaded]') — wait for a data-driven UI to settleignoreSelectors (per-page) — exclude regions that legitimately differ every run (timestamps, ad slots)Date.now() if your page renders relative timestamps and you don't want to ignoreSelector themawait snapshot(page, 'dashboard', {
fullPage: true, // capture entire scrollable page
selector: '[data-testid="main-content"]', // capture only this element
clip: { x: 0, y: 0, width: 1280, height: 720 }, // explicit region
delayMs: 500, // wait before capture (animations)
});
If your preview deployments are behind Vercel Authentication, Cloudflare Access, basic auth, or any header-based bypass, configure it in your Playwright config — the reporter inherits whatever your tests already do. There is no SnapDiff-specific input.
// playwright.config.ts
export default defineConfig({
use: {
extraHTTPHeaders: {
'x-vercel-protection-bypass': process.env.VERCEL_AUTOMATION_BYPASS_SECRET!,
'x-vercel-set-bypass-cookie': 'true',
},
},
});
Generate the secret in Vercel → Settings → Deployment Protection → Protection Bypass for Automation. Reference: Vercel docs.
use: {
extraHTTPHeaders: {
'CF-Access-Client-Id': process.env.CF_ACCESS_CLIENT_ID!,
'CF-Access-Client-Secret': process.env.CF_ACCESS_CLIENT_SECRET!,
},
},
Same pattern — drop any header into extraHTTPHeaders.
The reporter does not handle login. Authentication is performed by your existing Playwright setup:
// playwright.config.ts
export default defineConfig({
use: {
storageState: 'auth.json', // already-logged-in state
},
});
// global setup (one-time login)
test.beforeAll(async ({ browser }) => {
const ctx = await browser.newContext();
const page = await ctx.newPage();
await page.goto('/login');
await page.fill('[name=email]', process.env.TEST_USER_EMAIL!);
await page.fill('[name=password]', process.env.TEST_USER_PASSWORD!);
await page.click('button[type=submit]');
await page.waitForURL('/dashboard');
await ctx.storageState({ path: 'auth.json' });
});
Whatever authentication mechanism the tests already use, whether cookies, localStorage tokens, or OAuth, is inherited by the reporter. SnapDiff has no separate authentication concept.
| Option | Type | Default | Description |
|---|---|---|---|
project | string | required | SnapDiff project slug or ID |
apiKey | string | process.env.SNAPDIFF_API_KEY | API key |
apiUrl | string | https://api.snapdiff.ai | Override for self-hosted SnapDiff |
branch | string | auto-detected | Override CI detection |
commitSha | string | auto-detected | Override CI detection |
commitMessage | string | auto-detected | Override CI detection |
pullRequestUrl | string | auto-detected | Override CI detection |
wait | boolean | true | Poll the build to completion and print the per-page result. Visual changes do not fail the run — see Merge gating |
waitTimeoutMinutes | number | 5 | Maximum polling duration |
disabled | boolean | false | Disable the reporter, for example during local debugging |
When the reporter detects visual changes, the workflow does not fail and the Playwright check remains green. SnapDiff posts a separate commit status named snapdiff/visual-test through the GitHub API:
Add snapdiff/visual-test as a required check in your branch protection rules. While the status is pending, the merge button is blocked. A reviewer opens the dashboard to approve changes (which become the new baseline) or reject them. Once approved, the status updates to success and the merge is unblocked.
To enable status posting, connect a GitHub repository and personal access token in your project's Settings page in the SnapDiff dashboard (config is per-project). The token requires the repo:status scope.
The reporter auto-detects CI metadata for GitHub Actions, CircleCI, GitLab CI, Vercel, and Buildkite. For other systems, supply explicit overrides through reporter options.
# .github/workflows/visual.yml
name: Visual diff
on:
pull_request:
push:
branches: [main]
jobs:
visual:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- run: npm ci
- run: npx playwright install --with-deps chromium
- run: npx playwright test
env:
SNAPDIFF_API_KEY: ${{ secrets.SNAPDIFF_API_KEY }}
# If your tests need credentials to log in:
TEST_USER_EMAIL: ${{ secrets.TEST_USER_EMAIL }}
TEST_USER_PASSWORD: ${{ secrets.TEST_USER_PASSWORD }}
The reporter creates a build and waits for diffs to complete. The workflow itself remains green regardless of visual changes; merge gating is handled by the snapdiff/visual-test commit status described above.
await snapshot(page, name) calls page.screenshot() and attaches the PNG to the test result.POST /v1/screenshot/upload. SnapDiff returns a screenshot id.POST /v1/projects/:project/builds, referencing the returned ids as snapshots[].screenshot_id.snapdiff/visual-test commit status; only infrastructure failures (upload error, build error, poll timeout) fail the run.Pixels are captured on the CI machine and diffed by SnapDiff. There is no DOM serialization or cloud rendering — captures come from the same browsers your tests already run.
No snapshots captured. The reporter is registered but snapshot() was not called. Add await snapshot(page, 'name') inside a test that passes.
Duplicate snapshot names. Two tests captured a snapshot with the same name. Names are global; rename or add a prefix.
No API key found. Set SNAPDIFF_API_KEY in the environment, or pass apiKey in reporter options.
Build poll failed. Usually transient. Rerun the test job. If the issue persists, contact support.
Authenticated routes show the login form. The storageState file is not being loaded. Confirm playwright.config.ts has use.storageState pointing at a valid auth state file, and that the file is generated before playwright test runs.
MIT
FAQs
SnapDiff visual regression reporter for Playwright. Captures PNGs during your tests, ships them to SnapDiff, gates merges on visual changes.
We found that @corralimited/snapdiff-playwright demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Research
/Security News
Ten malicious OpenAPI React Query Codegen versions were published to npm in the Mini Shai-Hulud attack, all with valid provenance.

Security News
Socket joins more than 100 technology, cybersecurity, and financial organizations calling for a global surge in cyber defense.

Product
Enterprise security teams can now detect malware, credential theft, suspicious network activity, and risky updates across Microsoft Edge extensions.