Snapshot testing

Snapshot tests catch unintended visual regressions in UI components. Instead of asserting on DOM structure, they render a component to a real PNG screenshot using Playwright and compare it over time.

Snapshot tests leverage Sentry's upcoming snapshot testing product which hosts and diffs images using Git data as the source of truth. Snapshot tests will appear as a status check on your PRs.

Snapshot tests run on all PRs against the master branch. Snapshot tests uses Git as the source of truth, meaning it will always diff the HEAD of your branch against the base of your branch on master. This means any changes you see in your PR are caused by your changes only.

Any change in snapshot tests (removed, changed, renamed) will result in a failing status check on your PR. Only added and unchanged snapshots will resunt in a green check. This is intentional to get you to review the changes, as often times these tests will catch real regressions.

Any failing check can be approved in the Sentry UI in the top right of the page or directly in the GitHub status check UI by clicking the approve button.

GitHub Approve ButtonSentry Approve Button
GitHub approval UISentry approval UI

Once approved, your status check will show a green check and the Sentry UI will show approved.

GitHub Approved UISentry Approved UI
GitHub approvedApproved UI Snapshot

We want to avoid flakes - stability is key for a good snapshot test. Flakes are almost always a result of rendering noise or indeterminism.

A few key tenets to focus on when choosing to create a snapshot test:

  • Stability: avoid snapshotting components with heavy animations or dynamic data (like timestamps). You can always mock or pass data as props to fake this.
  • UI-only: avoid snapshotting components that have heavy business logic. Snapshots are not meant to be a full integration test (but can be used as a light test to ensure rendering works).

Have claude (or your favorite agent) create a snapshot test for you! Use the /generate-snapshot-tests skill to create a new snapshot tests.

Create a file named <component>.snapshots.tsx alongside your component.

Use the it.snapshot API (extended onto Jest's test by the snapshot framework):

button.snapshots.tsx
Copied
import { ThemeProvider } from "@emotion/react";
import { Button, type ButtonProps } from "@sentry/scraps/button";

// eslint-disable-next-line no-restricted-imports -- SSR snapshot rendering needs direct theme access
import { darkTheme, lightTheme } from "sentry/utils/theme/theme";

const themes = { light: lightTheme, dark: darkTheme };

describe("Button", () => {
  describe.each(["light", "dark"] as const)("%s", (themeName) => {
    it.snapshot.each<ButtonProps["priority"]>([
      "default",
      "primary",
      "danger",
      "warning",
      "link",
      "transparent",
    ])(
      "%s",
      (priority) => (
        <ThemeProvider theme={themes[themeName]}>
          {/* Add padding to avoid clipping box-shadows/focus rings outside #root */}
          <div style={{ padding: 8 }}>
            <Button priority={priority}>{priority}</Button>
          </div>
        </ThemeProvider>
      ),
      (priority) => ({ theme: themeName, priority: String(priority) }),
    );
  });
});

it.snapshot(name, renderFn, metadata?)

Renders renderFn() and saves a screenshot named name.

  • name: display name for the snapshot
  • renderFn: returns the JSX element to render
  • metadata: optional Record<string, string> saved to the accompanying .json file. These will be used in Sentry's hosted Snapshots UI in the future.

it.snapshot.each(values)(name, renderFn, metadataFn?)

Iterates over values, substituting %s in name with each value. metadataFn receives each value and returns its metadata.

Always wrap snapshot renders in ThemeProvider. Test both light and dark themes using describe.each.

Import themes directly (not via the useTheme hook) since there is no React context in SSR:

Copied
// eslint-disable-next-line no-restricted-imports -- SSR snapshot rendering needs direct theme access
import { darkTheme, lightTheme } from "sentry/utils/theme/theme";

The screenshot clips to the #root element's CSS border-box. If your component renders box-shadows, outlines, or focus rings that extend outside the element, wrap it in a padded <div> to prevent clipping.

Copied
pnpm snapshots

This runs jest --config jest.config.snapshots.ts, which only matches *.snapshots.tsx files.

Snapshots are written to .artifacts/snapshots/. Each snapshot produces two files:

  • <slug>.png: 2x-resolution screenshot with a transparent background
  • <slug>.json: metadata including display_name, group, and any custom fields

The file slug is derived from the describe block ancestry and test name. For example, a test in describe('Button') > describe('dark') named primary produces button/dark/primary.png.

Snapshot tests run in a Node SSR environment (not jsdom). Each test:

  1. Renders the component to HTML using Emotion SSR
  2. Loads the HTML in a headless Chromium browser via Playwright
  3. Waits for fonts to load, then captures a PNG
  4. Saves the image and a metadata JSON file to .artifacts/snapshots/

The test runner uses a separate Jest config (jest.config.snapshots.ts) and only picks up files named *.snapshots.tsx.

Was this helpful?
Help improve this content
Our documentation is open source and available on GitHub. Your contributions are welcome, whether fixing a typo (drat!) or suggesting an update ("yeah, this would be better").