Screenery

Docs / Testing and review

Publish Cypress screenshots

Cypress already writes a screenshot for every cy.screenshot() call. Screenery publishes that folder from CI and gives each screenshot a URL that follows your default branch. You need no plugin, only a name for each screenshot and one config change.

How Cypress names files

From the Cypress screenshot docs and configuration reference, checked October 2026:

Why the defaults fail a push

Screenery names each screenshot after its path inside the folder you push, minus the extension. It never rewrites a name. Each part of the path must be lowercase letters, digits, ., _ or -. Test titles have spaces and capitals, and failure screenshots have parentheses, so the push stops and names every file it cannot publish:

error: checkout.cy.ts/checkout -- pays with a card (failed).png derives the asset name
  "checkout.cy.ts/checkout -- pays with a card (failed)", which is not legal …
error: checkout.cy.ts/payment-form (1).png derives the asset name …
error: 2 file(s) cannot be published. Nothing was pushed.

Nothing is published when this happens. The rules are on URLs and asset names.

Set up Cypress

Give the screenshots a folder of their own and turn off failure screenshots, so the folder only holds screenshots you named:

// cypress.config.ts
import { defineConfig } from 'cypress';

export default defineConfig({
  screenshotsFolder: 'screenshots',
  screenshotOnRunFailure: false,
  e2e: {
    baseUrl: 'http://localhost:3000',
  },
});

If you keep failure screenshots on for debugging, a failed run still stops the push: a failing Cypress step skips the steps after it. With test retries, though, a test can fail once and then pass, and its (failed) file stays in the folder and fails the push. Turning them off avoids that.

Then name every screenshot you want published. Use lowercase and treat the name like a route:

// cypress/e2e/checkout.cy.ts
describe('checkout', () => {
  it('shows the payment form', () => {
    cy.visit('/checkout');
    cy.contains('h1', 'Payment').should('be.visible');
    cy.screenshot('checkout/payment-form');
    cy.get('[data-test=order-summary]').screenshot('checkout/order-summary');
  });
});

That spec writes these files, which publish under these names:

screenshots/checkout.cy.ts/checkout/payment-form.png   →  checkout.cy.ts/checkout/payment-form
screenshots/checkout.cy.ts/checkout/order-summary.png  →  checkout.cy.ts/checkout/order-summary

https://cdn.screenery.dev/acme/dashboard/checkout.cy.ts/checkout/payment-form@latest.png
npx screenery push ./screenshots --project acme/dashboard --dry-run \
  --commit-timestamp 2026-10-01T00:00:00Z

For a dark version, save it beside the normal screenshot with .dark before the extension, such as cy.screenshot('checkout/payment-form.dark'). See Light, dark and other themes.

The workflow

Run Cypress, then add one step. The permissions are the same as in the minimal workflow:

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

jobs:
  screenshots:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
      pull-requests: write
    steps:
      - uses: actions/checkout@v4
      # … install dependencies and start your app …
      - run: npx cypress run
      - uses: screenery/push@v1
        with:
          project: acme/dashboard
          path: screenshots

On main the push moves @latest. On a pull request it moves @pr-12 and posts a preview comment. Every input is on The GitHub Action.

Failing on a visual change

Cypress does not compare screenshots on its own, but Screenery can. Commit a folder of approved screenshots and point diff-baseline at it. Screenery compares each screenshot with the file of the same name, and a change fails the job before anything is published.

npx screenery diff ./screenshots --baseline ./screenery-baseline --update
- uses: screenery/push@v1
  with:
    project: acme/dashboard
    path: screenshots
    diff-baseline: ./screenery-baseline

Keep the baseline outside screenshots, or it would be pushed too. Grading, thresholds and accepting a change are on Visual testing.

Other Cypress tools

Both of these do more review work than Screenery does. Checked October 2026:

Pick one of them if you want a review app. Pick Screenery if you want the screenshot itself, as a public image URL that a README, help centre or docs page can embed. That URL changes when your default branch does. Playwright users can start from Playwright goldens and Argos folders.