New:Socket for Asana Is Now Available.Learn more
Get Started

@corralimited/snapdiff-playwright

Package Overview
Dependencies
Maintainers
1
Versions
3
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@corralimited/snapdiff-playwright

SnapDiff visual regression reporter for Playwright. Captures PNGs during your tests, ships them to SnapDiff, gates merges on visual changes.

Source
npmnpm
Version
0.1.3
Version published
Weekly downloads
4
-20%
Maintainers
1
Weekly downloads
 
Created
Source

@corralimited/snapdiff-playwright

npm

SnapDiff visual regression for Playwright. Captures PNGs during your tests, ships them to SnapDiff, gates merges on visual changes.

Designed to be a thin layer: it doesn't replace your test runner, your assertions, or your auth setup. You add await snapshot(page, 'name') wherever you want a visual check, and the reporter handles upload, build creation, polling, and gating.

Why this and not just URL captures

The URL-based SnapDiff GitHub Action works great for marketing sites, docs, and any unauthenticated route. For authenticated routes inside your app, it can't see past the login screen.

The Playwright reporter solves that by piggybacking on your existing E2E tests:

  • Your tests already log in (via storageState, OAuth, or login flow)
  • You add one line per page you want a visual snapshot of
  • Reporter captures the page in the authenticated state and ships it

You can use both: keep the GitHub Action for /, /pricing, etc., and use this reporter for /dashboard, /account, etc. — all inside one SnapDiff project.

Install

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+.

Configure the reporter

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
      },
    ],
  ],
});
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 is a thin extension over @playwright/test adding a snapshot fixture. All your existing fixtures, hooks, and config keep working.

Standalone helper alternative

If you don't want to switch 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');
});

Caveat: in some peer-dep configurations the standalone helper can miss tests when @playwright/test is duplicated in node_modules. The fixture API has no such issue. Prefer the fixture API unless you have a specific reason not to.

Names must be unique across the entire test run — they map directly to baselines in SnapDiff. Use prefixes if you need to organize (account-overview, account-billing, settings-profile).

Important: keep functional assertions content-agnostic

Snapshots only fire if the test passes. If a functional assertion fails — say, toContainText('Ship') after you renamed the heading to 'Catch' — the test fails before snapshot() runs, and SnapDiff never sees the page. That defeats the purpose: a visual change broke a string assertion, so it never got captured for the diff that would have made it obvious.

Rule of thumb: assert structure, not copy.

// ✘ Brittle — breaks when copy changes
await expect(page.locator('h1')).toContainText('Welcome to Acme');

// ✓ Resilient — checks rendering, lets SnapDiff catch copy changes
await expect(page.locator('h1')).toBeVisible();

If you want a snapshot regardless of test outcome (e.g. to debug what the page looks like when an assertion fails), use 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');
  }
});

Snapshot options

await 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)
});

How auth works (no extra setup required)

The reporter doesn't handle login itself. Your existing Playwright auth setup does:

// 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 auth your tests already use — cookies, localStorage tokens, OAuth — the reporter inherits. There's no separate auth concept in SnapDiff.

Reporter options

OptionTypeDefaultDescription
projectstring(required)SnapDiff project slug or id
apiKeystringprocess.env.SNAPDIFF_API_KEYAPI key
apiUrlstringhttps://api.snapdiff.devOverride for self-hosted
branchstringauto-detectedOverride CI detection
commitShastringauto-detectedOverride CI detection
commitMessagestringauto-detectedOverride CI detection
pullRequestUrlstringauto-detectedOverride CI detection
waitbooleantruePoll the build until done; fail the run on visual changes
waitTimeoutMinutesnumber5Cap polling duration
disabledbooleanfalseNo-op the reporter (useful for local debugging)

How the merge gate actually works

When the reporter detects visual changes, the workflow itself does not fail. The Playwright check stays green. Instead, SnapDiff posts a separate commit status — snapdiff/visual-test — directly via the GitHub API. That status's lifecycle:

  • 🟡 pending — build is processing, or changes detected and awaiting review
  • success — no changes, or all changes accepted in the dashboard
  • error — build failed (real error, not a visual regression)

In your repo's branch protection rules, require the snapdiff/visual-test status to pass. Pending = merge button greyed out. The reviewer opens the dashboard, accepts (becomes a new baseline) or rejects (must fix the code). On accept, the status flips to ✅ and merge unblocks.

This mirrors how Chromatic and Argos work — visual changes are never a "failure," they're a "needs review" gate.

To enable: in the SnapDiff dashboard for your project, add a GitHub repo + PAT under integrations. The PAT needs repo:status scope on the repo you want SnapDiff to post to.

CI integration

The reporter auto-detects CI metadata for GitHub Actions, CircleCI, GitLab CI, Vercel, and Buildkite. For other systems, pass explicit overrides via reporter options.

GitHub Actions example

# .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 finish. The workflow itself stays green regardless of visual changes — see "How the merge gate actually works" above for why, and how to set up branch protection against the snapdiff/visual-test status.

How it works

  • Your tests run normally. await snapshot(page, name) calls page.screenshot() and attaches the PNG to the test result.
  • After each passing test, the reporter uploads attached PNGs to POST /v1/screenshot/upload. SnapDiff returns a screenshot_id.
  • After the entire test run, the reporter creates a build via POST /v1/projects/:project/builds referencing those screenshot_ids.
  • The reporter polls until the build finishes diffing. If any page changed, it returns status: 'failed' to Playwright, which exits non-zero.

There's no DOM serialization, no cloud rendering. Pixels are captured on your CI machine, diffed by SnapDiff. This is deliberate — see POSITIONING.md.

Troubleshooting

"No snapshots captured" — you registered the reporter but didn't call snapshot() anywhere. 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 namespace.

"No API key found" — set SNAPDIFF_API_KEY in the environment, or pass apiKey in reporter options.

"Build poll failed" — usually transient. Check Railway worker logs if persistent. The reporter retries upload but not poll; rerun the test job.

Authenticated routes show login form — your storageState isn't loaded. Confirm playwright.config.ts has use.storageState pointing at a valid auth state file, and that file is generated before playwright test runs.

License

MIT

Keywords

playwright

FAQs

Package last updated on 04 May 2026

Related posts