Testing recipe · Published October 16, 2026 · 8 min read

Screenshot on Failure in Playwright, Cypress & Selenium: 5-Minute Setup

Quick answer

Playwright: set use: { screenshot: "only-on-failure" }. Cypress: nothing to do, cypress run already saves a screenshot for each failed test. Selenium and WebdriverIO: add a hook that calls the screenshot method when a test fails, before the browser is closed. Then upload the screenshot folder as a CI artifact, or it disappears with the runner.

A red build with "element not found" tells you very little. A screenshot of the page at that moment usually shows the reason in a second: a cookie banner, a login wall, an error page, a layout that wrapped. This page collects the setup for the four most common frameworks in one place, plus the part people forget: getting the image out of CI.

The matrix

FrameworkOn by default?How to enableWhere files go
Playwright TestNo (off)screenshot: "only-on-failure"test-results/ + HTML report
CypressYes, in cypress runscreenshotOnRunFailure: true (default)cypress/screenshots/
Selenium (pytest, JUnit, TestNG)NoTest framework hookWherever your hook writes
WebdriverIONoafterTest hookWherever your hook writes

1Playwright Test

One config lineShows in HTML report
playwright.config.ts
import { defineConfig } from "@playwright/test"; export default defineConfig({ retries: process.env.CI ? 2 : 0, use: { screenshot: { mode: "only-on-failure", fullPage: true }, trace: "retain-on-failure", }, });

The screenshot modes are off, on, only-on-failure and on-first-failure. Pairing the screenshot with trace: "retain-on-failure" gives you a step-by-step recording next to the image, which is usually enough to fix a flaky test without rerunning it. Every screenshot option, including masks for dynamic content, is in our Playwright screenshot guide. Files land in the test output directory, typically test-results/. For Python, pytest-playwright has the same switch: pytest --screenshot only-on-failure.

2Cypress

On by defaultFolder is wiped each run
cypress.config.js
const { defineConfig } = require("cypress"); module.exports = defineConfig({ screenshotOnRunFailure: true, // default screenshotsFolder: "cypress/screenshots", trashAssetsBeforeRuns: true, // default: empties the folder before each run e2e: { baseUrl: "http://localhost:3000" }, });

Cypress saves failure screenshots automatically during cypress run, named {testName} (failed).png inside a folder per spec. Two details from the Cypress docs catch people out:

  • Failure screenshots always use capture: "runner", so the image includes the Command Log next to your app. That is useful for debugging but not for sharing with designers.
  • In interactive cypress open mode no failure screenshot is written.

Need a clean image of the app at a specific step? Call cy.screenshot("checkout-summary", { capture: "fullPage" }) yourself.

3Selenium

Timing matterspytest, JUnit 5, TestNG
conftest.py
import os import pytest @pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport(item, call): outcome = yield report = outcome.get_result() if report.when == "call" and report.failed: driver = item.funcargs.get("driver") if driver is not None: os.makedirs("screenshots", exist_ok=True) driver.save_screenshot(f"screenshots/{item.name}.png")

The rule for every Selenium setup: take the screenshot before the driver is quit. In pytest the call report is created before fixture teardown, so the snippet above works with a normal driver fixture (we ran it against headless Chrome). For JUnit 5 use AfterTestExecutionCallback, for TestNG an ITestListener; both versions, plus full-page capture in Chrome, are in our Selenium screenshot guide.

4WebdriverIO

Mocha and Jasmine
wdio.conf.js
export const config = { // ... afterTest: async function (test, context, { error, passed }) { if (!passed) { const name = test.title.replace(/[^a-z0-9]+/gi, "_"); await browser.saveScreenshot(`./screenshots/${name}.png`); } }, };

The afterTest hook receives passed and error in its third argument. The docs note this hook runs for Mocha and Jasmine; with Cucumber use afterScenario instead. Create the screenshots folder in your CI step or in the onPrepare hook.

Keep the screenshots after CI finishes

CI runners are thrown away after the job. Without an artifact step, every screenshot above is deleted with them.

.github/workflows/e2e.yml
- name: Run tests run: npx playwright test - name: Upload failure screenshots if: failure() uses: actions/upload-artifact@v4 with: name: failure-screenshots path: | test-results/ cypress/screenshots/ screenshots/ retention-days: 7 if-no-files-found: ignore
.gitlab-ci.yml
e2e: script: - npx cypress run artifacts: when: on_failure paths: - cypress/screenshots/ expire_in: 1 week

if: failure() and when: on_failure keep artifact storage small: images are uploaded only when something broke.

What makes a failure screenshot useful

DoWhy
Capture the full pageThe error message is often below the fold.
Name files after the test, and the retryParallel workers and retries otherwise overwrite each other.
Keep the URL with the imageA screenshot of a 404 looks like a broken layout without context.
Attach it to the reportNobody downloads a zip of 40 PNGs; the HTML report shows the right one.
Use a fixed viewportLocal and CI screenshots stay comparable.
Failure screenshots answer "what did the test see". For "did the page change since yesterday" you need a baseline and a diff, which is a different workflow: see visual regression testing and website change detection.

Takeaways

  • Playwright: one config line. Cypress: already on in cypress run. Selenium and WebdriverIO: a hook.
  • Capture before teardown closes the browser.
  • Upload screenshots with if: failure(), or they vanish with the runner.
  • Cypress empties its screenshot folder before each run by default.

Sources

FAQ

Does Playwright take screenshots on failure by default?

No. The screenshot option defaults to off. Set use: { screenshot: 'only-on-failure' } in playwright.config.ts to capture one after every failed test.

Does Cypress take screenshots on failure by default?

Yes, during cypress run. screenshotOnRunFailure defaults to true and files go to cypress/screenshots. In interactive cypress open mode no failure screenshot is saved.

Why are my Cypress failure screenshots gone after the next run?

trashAssetsBeforeRuns defaults to true, so cypress run empties the screenshots, videos and downloads folders before each run. Upload them as CI artifacts or set the option to false.

Why does my Selenium failure screenshot throw an error?

Usually the driver was already closed by teardown when the hook ran. Take the screenshot in a hook that runs before teardown, such as pytest_runtest_makereport for the call phase or JUnit 5 AfterTestExecutionCallback.

How do I keep failure screenshots in GitHub Actions?

Add an actions/upload-artifact step with if: failure() and point path at the screenshot folder, for example test-results/ for Playwright or cypress/screenshots for Cypress.

Capture production on a schedule, too

Failure screenshots tell you what broke in CI. Scheduled captures and visual diffs tell you what changed on the live site. Both work from one HTTP API. The free tier gives you 300 screenshots, no credit card required.