ScreenshotNeo

BlogHow-to

How to Run Screenshot and Visual Tests With GitHub Actions

Run Playwright screenshot comparisons on every pull request, keep rendering consistent, and inspect visual failures with GitHub Actions artifacts.

By the ScreenshotNeo team4 October 202611 min read

Run Playwright’s native screenshot assertions in a GitHub Actions workflow triggered by pushes and pull requests. Commit reviewed reference screenshots to the repository, install the same Playwright browser environment in CI that you used to create the references, run npx playwright test, and upload the HTML report and failure images as workflow artifacts. On the first run, Playwright creates a baseline; later runs compare the page against it. Playwright visual comparisons documentation explains baseline behavior and comparison options.

This guide uses JavaScript with Playwright Test. It assumes an npm project whose application can be started by a command such as npm run start -- --port 4173. Adjust the start command, URL, test path, and reviewed GitHub Action references for your repository.

1. Add a visual test

Install Playwright Test and create a spec. In a project that already uses Playwright Test, keep its existing dependency and configuration.

npm install --save-dev @playwright/test
npx playwright install chromium

Create tests/home.visual.spec.js:

const { test, expect } = require('@playwright/test');

test('home page matches its visual baseline', async ({ page }) => {
  await page.goto('http://127.0.0.1:4173/', {
    waitUntil: 'networkidle',
  });

  await expect(page).toHaveScreenshot('home.png', {
    fullPage: true,
  });
});

For many applications, networkidle is not the best readiness signal: analytics, polling, or long-lived requests can keep the network active. Prefer waiting for the specific content that means the page is ready, for example await page.getByRole('heading', { name: 'Welcome' }).waitFor(). Avoid arbitrary sleeps unless the page genuinely needs a fixed settling delay.

Configure Playwright for predictable screenshots

Create or update playwright.config.js. This example uses one Chromium project and starts the app before tests. If your existing config already defines projects, retries, reporters, or a web server, integrate the relevant settings instead of replacing it.

const { defineConfig } = require('@playwright/test');

module.exports = defineConfig({
  testDir: './tests',
  reporter: [['html', { open: 'never' }]],
  use: {
    browserName: 'chromium',
    headless: true,
    viewport: { width: 1440, height: 900 },
    deviceScaleFactor: 1,
    locale: 'en-US',
    timezoneId: 'UTC',
    colorScheme: 'light',
    reducedMotion: 'reduce',
  },
  expect: {
    toHaveScreenshot: {
      animations: 'disabled',
      caret: 'hide',
      // Leave strict by default. Set a small maxDiffPixels only when
      // reviewed rendering noise requires a documented tolerance.
    },
  },
  webServer: {
    command: 'npm run start -- --port 4173',
    url: 'http://127.0.0.1:4173',
    reuseExistingServer: !process.env.CI,
    timeout: 120_000,
  },
});

If using the example config, add the environment access it uses at the top:

const process = require('node:process');

Alternatively, use a configuration format that already has process available in your project’s runtime. Confirm that the configured command actually starts the app and serves the expected page.

toHaveScreenshot() captures a screenshot and compares it with a reference file. On its first execution, Playwright writes the missing baseline; inspect that image before committing it. Snapshot names and locations can be configured, and image assertions support options such as maxDiffPixels and a stylesheet for neutralizing volatile content. See the visual comparison options.

2. Create the GitHub Actions workflow

Save this as .github/workflows/visual-tests.yml. Replace each <reviewed-ref> with an action version reference you have reviewed, and update the branch filters to match your repository. Action versions change over time; GitHub documents the workflow and action model, and Playwright maintains a current GitHub Actions CI example.

name: Visual tests

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  visual-tests:
    timeout-minutes: 30
    runs-on: ubuntu-latest
    steps:
      - name: Check out repository
        uses: actions/checkout@<reviewed-ref>

      - name: Set up Node.js
        uses: actions/setup-node@<reviewed-ref>
        with:
          node-version: lts/*
          cache: npm

      - name: Install locked dependencies
        run: npm ci

      - name: Install Playwright browsers and OS dependencies
        run: npx playwright install --with-deps chromium

      - name: Run visual tests
        run: npx playwright test

      - name: Upload Playwright report and test results
        if: ${{ !cancelled() }}
        uses: actions/upload-artifact@<reviewed-ref>
        with:
          name: playwright-visual-test-results
          path: |
            playwright-report/
            test-results/
          retention-days: 14

Playwright’s CI guidance uses the same basic sequence: check out the project, install dependencies, install browsers and operating-system dependencies, run tests, and upload the report. Use npm ci with a committed lockfile so CI installs the dependency tree represented by that lockfile. If the project uses another package manager, use its lockfile-enforcing install command and configure the runtime accordingly.

The workflow uploads diagnostics even when the test step fails. GitHub artifacts preserve files produced by a workflow run so they can be downloaded after the job completes. Reports and screenshot diffs are artifacts; dependency caches serve a different purpose. See GitHub’s workflow artifacts guide.

3. Generate and review the baseline

  1. Run the test locally in the intended baseline environment: npx playwright test.
  2. On the first run, expect a failure indicating that the reference image did not exist. Playwright writes the generated screenshot into the test’s snapshot directory.
  3. Open and inspect the new image. Confirm that it represents the intended UI at the chosen viewport, state, and data.
  4. Commit the generated snapshot directory with the test. Playwright snapshots are ordinary project files and should be reviewed alongside code changes.
  5. Push the branch or open a pull request. CI creates a new screenshot and compares it against the committed reference.

A visual failure is evidence of a difference, not proof that the application is wrong. Check the expected image, received image, and diff before deciding whether to fix the page, stabilize the test, or accept a reviewed UI change.

Update baselines only for intentional changes

When a design change is intended, run:

npx playwright test --update-snapshots

Inspect every changed reference and commit only the accepted baseline updates with the corresponding UI change. Do not automatically update baselines after a failed CI run: that can turn regressions or environment drift into the new expected output.

4. Stabilize the screenshot environment

Browser output can vary with the operating system, browser version, settings, hardware, and headless mode. Playwright recommends generating and comparing screenshots in the same environment. The workflow above runs on Linux with an installed Chromium version; generate and update baselines in that environment too when exact consistency matters. Playwright includes browser and platform information in snapshot names, and separate environments may require separate baselines. Read the Playwright snapshot guidance before changing the baseline strategy.

Source of noise How to reduce it
Different OS, browser build, or fonts Use the same CI image and Playwright version when creating and comparing baselines. Avoid generating baselines on one operating system and comparing them on another.
Animations and blinking carets Disable animations for screenshot assertions and hide the caret, as in the config example.
Current date, rotating content, randomized IDs Freeze or seed the data, mock the relevant API, or apply a narrowly scoped screenshot stylesheet to hide only genuinely irrelevant regions.
Images that load late Wait for the image or page landmark to be visible and, where needed, wait for image decoding before taking the screenshot.
Locale, timezone, color scheme, viewport Set these values explicitly in Playwright config so local and CI runs use the same conditions.
External services and live data Use deterministic fixtures or route mocks instead of relying on changing third-party content.

When a single widget is dynamic, target the test to stable content or neutralize that widget with a scoped stylesheet. A global diff threshold can hide real regressions. Playwright supports maxDiffPixels and stylePath; keep any tolerance small, intentional, and reviewed rather than increasing it until flaky tests pass.

5. Capture a full page, an element, or multiple states

Full-page screenshot

Use fullPage: true for a page-length image, as in the initial example. This can reveal regressions below the fold, but it also makes the assertion sensitive to more content and layout. For long pages, test important sections separately when that produces clearer failures.

Element screenshot

To check one component, assert against a locator:

test('pricing card matches its baseline', async ({ page }) => {
  await page.goto('http://127.0.0.1:4173/pricing');
  const card = page.getByRole('article', { name: 'Pro plan' });
  await expect(card).toBeVisible();
  await expect(card).toHaveScreenshot('pro-plan-card.png');
});

Choose a locator that identifies one element reliably. A locator matching multiple elements or an element that has not appeared yet can fail before comparison.

Test meaningful states

For a menu, dialog, or responsive layout, set up the state explicitly and give each screenshot a distinct name. For example, click the menu button, assert that the menu is visible, then compare its screenshot. To test a mobile layout, define a separate Playwright project with its own viewport and baseline set rather than relying on the machine’s default window size.

6. Inspect a failed visual comparison

  1. Open the failed workflow run and inspect the failed assertion in the job log.
  2. Download the playwright-visual-test-results artifact from the run summary.
  3. Open the HTML report and review the expected screenshot, actual screenshot, and comparison diff produced for the assertion.
  4. Check whether the difference is a real product change, a missing or late-loaded resource, test data drift, or environment mismatch.
  5. Fix the cause. If the appearance intentionally changed, regenerate and review the reference locally in the same environment, then commit it.

Artifacts persist after the job ends, but they are not a replacement for committed baselines. Set retention according to how long reviewers need the evidence and repository policy. Avoid putting credentials or private page data into artifacts unless the repository’s access and retention rules allow it.

7. Troubleshooting

Symptom Likely cause Fix
Every screenshot differs in CI but passes locally OS, browser version, fonts, viewport, device scale, or headless settings differ. Create and compare baselines in the same environment. Pin the Playwright dependency via the lockfile, set screenshot conditions explicitly, and avoid cross-platform baseline updates.
“A snapshot doesn’t exist” on the first run No baseline has been committed yet. Run the test once, inspect the generated reference, and commit it. A missing baseline is an initialization step, not a reason to accept an unreviewed image.
Browser executable or shared library is missing The CI job did not install the browser binary or Linux dependencies for the configured browser. Run npx playwright install --with-deps chromium, or install the browsers matching the project’s configured Playwright projects.
App URL refuses the connection The server did not start, listens on a different port, or is bound to an unreachable host. Verify the webServer.command and URL match. Ensure the server listens on the CI runner’s loopback interface and inspect server startup output.
Navigation times out or the page is partially rendered The app is slow to start, the readiness condition is wrong, or a third-party request hangs. Wait for an application landmark or a specific response, increase the web server timeout if startup genuinely needs longer, and mock nonessential external services.
Flaky pixel differences appear intermittently Dynamic content, animations, time, random data, or delayed assets change between runs. Make inputs deterministic, disable animations, wait for the relevant asset, and mask only the unstable region when it is not under test.
Artifact is missing after a failure The upload path does not match the configured reporter/output directories, or the upload step was skipped. Confirm Playwright writes to playwright-report/ and test-results/, ensure the upload step uses if: ${{ !cancelled() }}, and check the upload step log.
Pull request workflow does not run The event or branch filter does not match the PR target branch, or repository workflow settings prevent the run. Check the on.pull_request.branches filter, workflow file location, Actions run list, and repository policy.
Diff threshold is too strict or too permissive Rendering noise is being treated as a regression, or meaningful pixel changes are being tolerated. First stabilize the rendering inputs. Adjust maxDiffPixels narrowly and review the diff; do not use a large threshold to suppress unexplained changes.

8. Performance, reliability, and cost

  • Run the essential visual suite on pull requests. A focused set of high-value pages catches common regressions sooner; broader browser and viewport coverage can run in a separate job or on a schedule.
  • Install the browser required by the suite. The example installs Chromium only. Installing all browsers and OS dependencies takes longer when the suite does not use them.
  • Use dependency caching appropriately. The workflow caches npm packages through setup-node. Playwright’s CI guide cautions that caching browser binaries may take about as long as downloading them, and Linux OS dependencies are not cacheable; consult the current CI guidance before adding browser caching.
  • Use parallelism deliberately. Playwright can run tests in parallel, but contention for CPU or memory can make rendering less predictable. If screenshots become flaky under load, reduce worker concurrency or split suites, then compare runtime and stability.
  • Retain useful failure evidence. Reports and images consume artifact storage and may contain page content. Choose a practical retention period and keep artifact access appropriate for that content.
  • Budget CI minutes. Runtime depends on suite size, browser installation, app startup, and GitHub runner choice. The dossier provides no benchmark, so measure your own workflow rather than assuming a fixed duration or cost.

For a large suite, Playwright documents --only-changed as a way to run likely-affected tests first, while warning it is a heuristic that can miss tests. Treat it as a quick preliminary signal, not as a replacement for a full run when full coverage is required.

9. Native Playwright baselines or hosted review?

Playwright’s built-in screenshot assertions keep the test and reference images in your project. This is a direct route when repository-based review fits the team’s workflow. Percy also documents a Playwright client that uploads screenshots for hosted visual testing when configured with a project token; evaluate its setup, screenshot handling, review process, and current service terms against your needs. The available research does not establish comparative pricing or suitability, and a hosted service is not required to run screenshot comparisons in GitHub Actions.

If your CI task is to capture pages through an API rather than assert application screenshots against committed Playwright baselines, ScreenshotNeo is a website screenshot API and MCP server. It complements the browser-based test workflow; use Playwright assertions when you need code-level visual regression checks against reviewed baselines.

Or skip the browser setup

For capturing a website screenshot from a GitHub job without installing and managing a browser in that job, make one request to ScreenshotNeo. See the ScreenshotNeo API documentation for request options.

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://stripe.com \
  -o shot.webp

Python equivalent:

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)

Node.js equivalent:

const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://stripe.com',
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await require('node:fs/promises').writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
  • Cookie banners are accepted before capture; known consent platforms, newsletter popups, and chat widgets are removed. Each step can be turned off.
  • Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing. Responses include X-Page-Verdict and X-Billed headers.
  • An MCP server lets AI agents, including Claude and Cursor, take screenshots with tools for screenshots, page information, and PDF capture.
  • 1,000 screenshots a month are free with no card; paid plans start at $5 for 3,000 screenshots.

Sign up for ScreenshotNeo and get 1,000 free screenshots a month with no card.

FAQ

Do I need a separate visual testing service?

No. Playwright Test includes screenshot assertions and local reference images. Hosted visual review is an optional workflow choice.

Should screenshot baselines be committed?

Yes. They are the expected images that future runs compare against, so they need to be available to CI and reviewed when changed.

Can GitHub Actions run visual tests on every pull request?

Yes. Configure the workflow’s pull_request trigger and a branch filter that matches your repository. The job runs the same test command used locally.

Can I use Playwright screenshots as visual tests without capturing a full page?

Yes. Use a locator’s toHaveScreenshot() assertion to compare an element, or configure a distinct viewport and test state for each case.