Bitbucket Pipelines for component snapshot tests

This page is part of the Bitbucket Pipelines section of the CI/CD test gating guide, which covers running component and visual tests inside a pipeline so a snapshot mismatch stops a bad merge.

The specific problem: your jest-image-snapshot or Playwright snapshot suite runs green on your laptop, but in bitbucket-pipelines.yml it either cannot launch a browser at all, fails on every snapshot because the fonts differ, or passes without ever producing a downloadable diff when something breaks.

Problem statement

Component snapshot tests capture a rendered component to a PNG and compare future runs against that baseline. Getting them to run under Bitbucket Pipelines has three distinct failure modes. First, the default Node image has no browser libraries, so Playwright’s Chromium exits immediately with a missing-library error. Second, even when the browser launches, CI renders text with different fonts and antialiasing than your machine, so every snapshot fails on subpixel noise. Third, when a snapshot legitimately fails, the pipeline reports red but the diff PNG stays inside the container and is never surfaced, so nobody can see what changed.

The result is a pipeline that is either permanently broken, permanently flaky, or opaque when it matters — none of which can gate a merge.

Root cause

Bitbucket Pipelines runs each step in a fresh Docker container defined by the step’s image, and that container is discarded when the step ends. A plain node image lacks the system libraries headless browsers link against, and its font stack differs from a developer’s OS, so both the launch failures and the antialiasing diffs trace back to the image. The diffs go missing because Bitbucket only preserves paths you explicitly list under artifacts. Choosing a browser-capable image and pinning it — the same discipline behind handling font rendering differences across browsers — makes the rendering environment reproducible, and declaring artifacts makes the diff reviewable.

Bitbucket Pipelines snapshot step anatomy A container labelled Playwright Docker image holds a snapshot run. Two cache stores, node_modules and ms-playwright, feed into it. The run branches to a green pass on match, or a red fail that emits a diff PNG into the artifacts store for download. mcr.microsoft.com/playwright image snapshot run jest / playwright node_modules ms-playwright caches match → pass diff → fail emits diff.png artifacts (download)

Minimal reproduction

A bitbucket-pipelines.yml that uses a plain Node image and runs Playwright snapshots. It fails before a single test asserts, because Chromium has no libraries to load.

# bitbucket-pipelines.yml — BROKEN: node image can't launch a browser
image: node:20

pipelines:
  pull-requests:
    '**':
      - step:
          name: Snapshot tests
          script:
            - npm ci
            - npx playwright install chromium   # binaries only, no system deps
            - npx playwright test --grep @snapshot

The step dies with an error like:

browserType.launch: Host system is missing dependencies to run browsers.
    Missing libraries:
      libnss3.so
      libatk-1.0.so.0
      libgbm.so.1

npx playwright install chromium fetched the browser binary, but the node:20 image has none of the shared libraries Chromium needs, so it cannot start. No test runs, and the failure has nothing to do with your components.

Step-by-step fix

1. Use a browser-capable image pinned to the Playwright version

Swap the base image for Microsoft’s official Playwright image, which ships the browsers and every system library they need, with a version tag that matches your @playwright/test.

# bitbucket-pipelines.yml
image: mcr.microsoft.com/playwright:v1.48.2-noble

What this does: the Playwright image includes Chromium, Firefox, WebKit, and their apt dependencies, so browserType.launch succeeds without any install --with-deps step. Pinning the tag to v1.48.2 — the same version as your dependency — keeps the browser build and rendering identical from run to run, which is what stops baselines drifting under you.

2. Cache node_modules and the browser directory

Bitbucket discards the container after each step, so declare caches to avoid reinstalling on every run. node is a built-in cache; add a custom one for the browser directory.

definitions:
  caches:
    playwright: ~/.cache/ms-playwright

pipelines:
  pull-requests:
    '**':
      - step:
          name: Snapshot tests
          caches:
            - node                # built-in: caches node_modules
            - playwright          # custom: caches browser binaries
          script:
            - npm ci

What this does: the node cache restores node_modules so npm ci is fast, and the playwright cache restores ~/.cache/ms-playwright. With the Playwright image the browsers are already present, but the cache still matters for any project that installs extra browser channels or additional locales.

3. Run the snapshot suite so a mismatch fails the step

Run the tests as the step’s script. Both jest-image-snapshot and Playwright exit non-zero on a mismatch, and in Bitbucket a non-zero script command fails the step — which is exactly the gate you want.

          script:
            - npm ci
            - npx playwright test --grep @snapshot

For a Jest + jest-image-snapshot suite the script is analogous:

          script:
            - npm ci
            - npx jest --ci src/components   # toMatchImageSnapshot() fails on diff

What this does: a failing toHaveScreenshot or toMatchImageSnapshot assertion exits the process with code 1; Bitbucket marks the step red and, if the step is on a required branch or part of a merge check, blocks the merge. Choosing the comparison tolerance is covered under tolerance thresholds.

4. Configure jest-image-snapshot for a reproducible comparison

If you use jest-image-snapshot, set an explicit small threshold and antialiasing handling so the comparison is stable inside the container.

// src/test/setupSnapshots.ts
import { toMatchImageSnapshot } from 'jest-image-snapshot';

expect.extend({
  toMatchImageSnapshot: (received, options) =>
    toMatchImageSnapshot.call(
      { ...arguments[0] },
      received,
      {
        failureThreshold: 0.01,          // allow ≤1% differing pixels
        failureThresholdType: 'percent',
        comparisonMethod: 'ssim',        // structural similarity, tolerant of AA noise
        customDiffDir: '__image_snapshots__/__diff_output__',
        ...options,
      }
    ),
});

What this does: failureThresholdType: 'percent' with a 0.01 threshold absorbs a handful of antialiased edge pixels while still catching real changes, and customDiffDir writes the diff PNG to a known path so the next step can publish it as an artifact.

5. Publish diff artifacts for review

Declare artifacts globs so Bitbucket preserves the diff output from a failed run. Without this, the PNGs are discarded with the container.

          script:
            - npm ci
            - npx playwright test --grep @snapshot
          artifacts:
            - test-results/**                              # Playwright: actual/expected/diff
            - __image_snapshots__/__diff_output__/**       # jest-image-snapshot diffs
            - playwright-report/**

What this does: any file matching these globs is attached to the pipeline result under the step’s Artifacts tab, downloadable as individual files. Reviewers open test-results/**/diff.png (or the jest diff) to see exactly which pixels moved before deciding whether to update the baseline or fix the component.

Verification

Push a branch with a deliberate one-pixel change and confirm the step fails with a downloadable diff. The pipeline log shows the mismatch:

Running 1 test using 1 worker

  1) [chromium] › card.snapshot.spec.ts:6:3 › Card @snapshot ──────────────

    Error: Screenshot comparison failed:
      1877 pixels (ratio 0.02 of all image pixels) are different.

    Expected: card-default-expected.png
    Received: card-default-actual.png
    Diff:     card-default-diff.png

  1 failed

Confirm the pipeline result is red and the artifact is attached:

# Bitbucket REST API — latest pipeline state for the branch
curl -s -u "$BB_USER:$BB_APP_PASSWORD" \
  "https://api.bitbucket.org/2.0/repositories/acme/design-system/pipelines/?sort=-created_on&pagelen=1" \
  | python3 -c "import sys,json; p=json.load(sys.stdin)['values'][0]; print(p['state']['result']['name'])"

Expected:

FAILED

In the Bitbucket UI, open the failed pipeline, select the Snapshot tests step, and switch to the Artifacts tab — card-default-diff.png is listed and downloadable. A red step plus a retrievable diff is the signal the gate works: the mismatch stops the pipeline, and the evidence is one click away.

Edge cases and caveats

  • Baselines must be generated in the pipeline image. A snapshot committed from macOS will not match one rendered in the Linux Playwright container, because fonts and antialiasing differ. Generate and commit baselines by running the suite with --update-snapshots inside the same mcr.microsoft.com/playwright image (locally via Docker, or by committing what a first pipeline run produces), so the CI comparison runs against a matching environment.
  • Bitbucket’s default 1 GB memory can starve Chromium. Snapshot steps that launch several browser contexts can hit the container memory limit and crash with a non-obvious Target closed error. Raise the step’s allocation with size: 2x (which doubles memory to 8 GB on a standard runner) when the suite renders many components in parallel.
  • Artifacts expire after 14 days. Bitbucket keeps step artifacts for 14 days by default, so a diff from an old failed run may be gone when you revisit it. Download the diff while triaging, or push the images to external storage if you need a longer audit trail for design-review sign-off.

FAQ

Why do Playwright snapshots fail to launch a browser in Bitbucket Pipelines?

The default node Docker image has no browser system libraries, so Chromium exits with a missing shared-library error. Use the mcr.microsoft.com/playwright image, which ships the browsers and their dependencies, or run npx playwright install --with-deps on a plain node image to install the apt libraries first.

How do I download the diff image from a failed Bitbucket Pipelines snapshot run?

Declare an artifacts section in the step listing the diff paths — __image_snapshots__/__diff_output__/** for jest-image-snapshot or test-results/** for Playwright. Bitbucket attaches matching files to the pipeline result, where they appear under the step’s Artifacts tab as downloads.

Why do my component snapshots pass locally but fail only in Bitbucket Pipelines?

Local and CI environments render text with different fonts and antialiasing, so pixels shift even when the component is unchanged. Commit baselines generated inside the same Docker image the pipeline uses, and allow a small failureThreshold, so the CI comparison runs against a matching rendering environment.