ScreenshotNeo

BlogHow-to

How to Run Cypress Screenshot Tests in GitHub Actions with a Stored Baseline

Capture Cypress screenshots in GitHub Actions, compare them with a versioned visual baseline, and keep CI artifacts useful for review.

By the ScreenshotNeo team4 October 20269 min read

Cypress can capture screenshots in GitHub Actions, but Cypress does not compare them with a stored baseline by itself. Add a visual comparison plugin or service, keep approved baseline images in a predictable location, and upload each run’s screenshots and diffs as workflow artifacts for review. Artifacts preserve run output; they are not automatically the approved baseline for the next run.

1. Choose where the baseline lives

For a repository-managed workflow, commit approved baseline images alongside the test code. A pull request that changes a baseline then shows the image changes in the same review as the application code. A compatible Cypress visual-diff plugin reads the baseline, compares it with the new screenshot, and fails the test when differences exceed its configured tolerance.

Alternatively, a hosted visual-testing service can manage baseline history and approval in its own review interface. Consider review history, masking and threshold controls, browser consistency, data retention, pricing, and vendor dependency before choosing. Cypress documents both local plugins that store baselines with code and hosted services. [Cypress visual testing documentation](https://docs.cypress.io/app/tooling/visual-testing)

Approach Good fit Watch for
Committed files Small, auditable suites where baseline changes belong in pull requests Repository size and ensuring CI checks out the exact approved images
Hosted service Teams that want centralized review and baseline history Service-specific workflow, retention, pricing, and dependency

GitHub Actions artifacts are for outputs from a particular run: screenshots, diffs, and optionally videos. They can also move files between jobs. Use an artifact as a baseline only if the workflow explicitly retrieves a known, approved artifact and identifies which version it is. A randomly selected “latest” run is not a safe baseline.

2. Make screenshots reproducible

Visual comparison is sensitive to the environment and page state. Keep the browser, operating system, fonts, viewport, device scale, and relevant test data consistent between baseline generation and CI. Wait for the page to reach the state you intend to test; control API responses with fixtures or intercepts; disable or wait out animations; and mask only unavoidable dynamic regions. Prefer an element snapshot when it gives a clearer and more stable assertion than a whole-page capture.

Cypress recommends a visual-testing tool for comparison: its screenshot command captures pixels, but comparison and approval are supplied by a plugin or service. Cypress visual testing documentation

3. Add a visual assertion to a Cypress test

Install and configure a Cypress-compatible visual-diff plugin, following that plugin’s current instructions for baseline location, snapshot command, and threshold. There is no universal comparison command shared by all plugins, so the example below marks the plugin call as the one project-specific line. Replace it with the API provided by the tool you selected; do not treat cy.screenshot() alone as an assertion.

// cypress/e2e/home.visual.cy.js
// Assumes the selected visual-diff plugin is configured in Cypress support files.
describe('home page visual baseline', () => {
  it('matches the approved home page appearance', () => {
    cy.intercept('GET', '/api/home', { fixture: 'home.json' });
    cy.visit('/');
    cy.get('[data-testid="home-ready"]').should('be.visible');

    // Replace with your plugin's documented snapshot/comparison command.
    // It should compare against the approved baseline and fail on excess diff.
    cy.compareSnapshot('home-page');
  });
});

The plugin must be installed and registered for cy.compareSnapshot to exist; this code is illustrative until that integration is selected. If your plugin supports a fixed viewport, animation disabling, element snapshots, masking, and a difference threshold, configure those explicitly and use the same settings when approving a new baseline. Keep intentional baseline updates reviewable in version control.

Cypress also supports explicit image capture with cy.screenshot(). It captures asynchronously, so the visible page can change between calling it and the actual capture. Wait for a meaningful ready condition before taking it. Cypress also captures screenshots on test failure during cypress run by default; its default screenshot directory is cypress/screenshots. See the Cypress screenshot API.

// Optional diagnostic screenshot; this does not compare with a baseline.
cy.get('[data-testid="home-ready"]').should('be.visible');
cy.screenshot('home-page-debug', { capture: 'fullPage' });

4. Run Cypress and upload screenshots in GitHub Actions

The workflow below checks out the repository (including committed baselines), uses Cypress’s maintained GitHub Action, then uploads screenshot and diff output even when the test step fails. Adjust the build and start commands and the diff directory to match the app and plugin. Cypress’s guide currently recommends the action’s v7 major and uses ubuntu-24.04 in its basic example; action and runner versions change, so consult the official guide when adopting or updating this workflow.

name: Cypress visual tests

on:
  pull_request:
  push:
    branches: [main]

jobs:
  visual:
    runs-on: ubuntu-24.04
    steps:
      - name: Check out repository and approved baselines
        uses: actions/checkout@v4

      - name: Run Cypress
        uses: cypress-io/github-action@v7
        with:
          build: npm run build
          start: npm start
          browser: chrome

      - name: Upload screenshot and diff output
        if: always()
        uses: actions/upload-artifact@v7
        with:
          name: cypress-visual-output
          path: |
            cypress/screenshots
            cypress-image-diff
          if-no-files-found: ignore
          retention-days: 14

This is an illustrative workflow, not a verified drop-in configuration. Confirm the current action inputs and runner support in the Cypress GitHub Action documentation. Check your plugin’s actual output path and configure it in the artifact step. The baseline path is separate: it must be committed or restored from an explicitly approved source before comparison starts.

To upload failure screenshots only, change the upload condition to if: failure(). Use if: always() when passing artifacts to reviewers after both passing and failing runs. GitHub documents artifact upload and download actions in its upload-artifact and workflow artifact references.

When tests and comparison run in separate jobs

Upload generated screenshots and diffs from the Cypress job, then download them in a dependent review or processing job with actions/download-artifact. If the comparison job needs the baseline, check it out from the same commit or download a specifically named and versioned approved baseline artifact. Keep the provenance clear in artifact names; never let a concurrent build silently replace the baseline used by another run.

5. Review and update the baseline safely

  1. Run the visual test against the current approved baseline and inspect the generated diff when it fails.
  2. Decide whether the difference is a real regression, expected product change, or rendering noise. Check page state and environment before changing the baseline.
  3. For an intended change, generate the new baseline using the plugin’s documented update process in the same rendering environment used by CI.
  4. Commit baseline changes with the application change so reviewers can inspect both together, or approve the change in the hosted service’s review flow.
  5. Re-run CI against the newly approved baseline and verify that the comparison job loaded the intended version.

Do not promote an arbitrary CI artifact just because it is the newest image. The baseline is an approved expectation, while an artifact is a record of one run.

Or skip the browser setup

For a website screenshot rather than an in-browser Cypress test, ScreenshotNeo provides a single GET request that returns an image or PDF. It is a website screenshot API and MCP server for developers. Its clean-shot steps accept cookie and consent banners and remove more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the page verdict and billing status in headers. Its MCP server offers take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. ScreenshotNeo has 1,000 free shots a month with no card; paid plans start at $5 for 3,000. It does not replace Cypress assertions for app behavior or the stored visual baseline workflow above.

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
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
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 import('node:fs/promises').then(async fs => {
  await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
});

For continuous visual regression, keep using a comparison tool that manages your baseline and diff. For a one-off clean capture, ScreenshotNeo can avoid local browser setup. Sign up for 1,000 free screenshots each month with no card.

Troubleshooting

Symptom Likely cause Fix
The test passes but visual changes are not detected The test only calls cy.screenshot(), or the plugin comparison assertion is not configured Use the selected plugin’s comparison command and verify that it loads the intended baseline and applies a threshold.
CI reports that the baseline is missing It is ignored by Git, stored outside the checkout, or expected from an artifact that was not downloaded Commit the approved files or add an explicit download step before comparison; check paths and case sensitivity.
Snapshots differ on every run Uncontrolled data, animation, timing, font loading, browser, viewport, or operating system Stub changing API responses, wait for a stable ready selector and fonts, disable animations, and align rendering settings.
Only CI fails with broad diffs CI and baseline were generated with different browsers, fonts, viewport, device scale, or OS Pin the rendering environment and regenerate baselines there only after reviewing the intended change.
Artifact upload says no files found The configured output path differs from the plugin’s real output, or no screenshot was produced Inspect the run logs, correct the path, and use if: always() if output should be uploaded after failure. ignore prevents a missing artifact from failing the job.
Screenshot is blank or captured too early The page had not reached the intended state when Cypress captured it Wait for an app-specific ready condition and required content before snapshotting. Avoid fixed sleeps where a condition can be asserted.
Baseline changes unexpectedly between parallel jobs Jobs are consuming mutable or ambiguously named artifacts Use a baseline tied to the checked-out commit or a uniquely identified approved artifact, and pass its identity explicitly.

Performance, reliability, and cost

Screenshot capture and image comparison add work to the test run; keep the suite focused on pages and states where visual appearance is a meaningful contract. Cypress documents screenshot capture as asynchronous and taking around 100 ms, but that is an API behavior note, not an end-to-end CI performance guarantee. Rendering and comparison time depend on the page, browser, environment, and selected plugin or service. Avoid treating that capture note as a benchmark.

Reliability comes from deterministic state, a stable comparison environment, and traceable baselines. Save screenshots and diffs as artifacts with a useful retention period so reviewers can inspect failures. Repository baselines make approval history visible in commits; hosted services may centralize history and review but introduce service-specific pricing and retention choices. No service prices are assumed here. GitHub artifact retention and storage behavior depend on the workflow and repository settings.

FAQ

Does Cypress compare screenshots automatically?

No. Cypress captures screenshots; a plugin or visual-testing service must compare them with an approved baseline.

Where does Cypress save screenshots in CI?

The default screenshot directory is cypress/screenshots. A plugin may write snapshots and diffs elsewhere, so configure artifact paths to match it.

Can GitHub Actions artifacts be the baseline?

They can carry baseline files between jobs if the workflow explicitly retrieves an identified, approved artifact. Ordinary run-output artifacts should be treated as that run’s results.

Can I use screenshots to test layout across browsers?

Yes, but establish and approve baselines for each browser and rendering environment you intend to support. Cross-environment pixel differences can otherwise obscure meaningful changes.

What should be included in a visual snapshot?

Capture a page or component in a defined state that represents a useful user experience. Keep the scope narrow enough that a diff points reviewers toward the responsible change.