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:
- Screenshots go to screenshotsFolder, which defaults to cypress/screenshots.
- Each spec gets a folder named after the spec file, such as checkout.cy.ts/. Cypress drops the parent folders that all your specs share.
- With no name, cy.screenshot() uses the test's title: checkout -- pays with a card.png.
- With a name, it uses the name. Slashes in it make folders: cy.screenshot('checkout/payment-form') writes checkout.cy.ts/checkout/payment-form.png.
- When a test fails during cypress run, Cypress takes a screenshot on its own and adds (failed) to the name. screenshotOnRunFailure controls this and is on by default.
- A second screenshot with the same name gets a number: payment-form (1).png.
- trashAssetsBeforeRuns, also on by default, empties the folder before each cypress run. Screenshots from an old run are not pushed by mistake.
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
- The spec file is part of the name. Renaming or moving a spec file renames its screenshots, and their old @latest URLs answer 404 after the next default-branch push. Keep spec file names lowercase, too: Checkout.cy.ts fails the push.
- Use each name once per spec. A repeated name gets (1) and fails the push. Pick another name, or pass { overwrite: true } to replace the first file.
- Check before you push. --dry-run lists every name and contacts nobody.
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:
- Chromatic's package, @chromatic-com/cypress, saves each page's DOM, styles and assets during your run. Chromatic then renders and compares the snapshots in its cloud, and reviewers approve changes in its web app.
- Argos's package, @argos-ci/cypress, adds cy.argosScreenshot. It waits for images, fonts and busy elements before it captures, and your team reviews changes in the Argos web app.
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.