Playwright architecture, selector reliability, and advanced interaction patterns.

Capturing Screenshots and Video on Test Failure

When a test fails on CI that nobody was watching, the only way to understand it is the evidence the run left behind. Playwright can record a screenshot, a video, and a full trace for every test, but recording all three on every run is slow and fills disk, so the practical pattern is to capture them only when a test fails. Three use options — screenshot: 'only-on-failure', video: 'retain-on-failure', and trace: 'on-first-retry' — give you rich forensic artifacts at almost zero cost on the happy path. This page belongs to Reporters & Test Artifacts, part of the Debugging & Test Observability guide, and walks through configuring, storing, and attaching these artifacts.

Conditional artifact capture by test outcome A test outcome branching to no artifacts on pass and to screenshot, video, and trace on failure or first retry. Test runs Pass no artifacts Fail / retry capture all screenshot video + trace
A passing test leaves nothing behind; a failing or retried test produces a screenshot, video, and trace, keeping the happy path fast and disk usage low.

Root cause: capture is tied to outcome, not to the run

The reason artifact configuration confuses people is that the three recorders work on different clocks. A screenshot is a single moment — Playwright takes it after the test body has finished and the outcome is known, so only-on-failure genuinely costs nothing on a green run. Video is the opposite: the browser has to be recording from the first navigation, because you cannot go back and film a failure that already happened, so retain-on-failure records everything and then deletes the files for tests that passed. A trace is somewhere in between — it is cheap enough to arm at the start of an attempt but expensive enough that you would rather arm it only on the attempt you are going to read, which is why on-first-retry exists.

Once you internalise that split — decide-after for screenshots, record-then-discard for video, arm-per-attempt for traces — every mode name in the config reads sensibly, and the disk and runtime behaviour of your suite becomes predictable instead of surprising.

The three artifact options and what each costs

All three are set under use in playwright.config.ts, and each accepts a mode that ties capture to the outcome:

Comparison of the three failure artifacts A matrix comparing screenshot, video, and trace by recommended mode, runtime cost, and depth of evidence. Artifact Recommended mode Runtime cost Evidence depth Screenshot only-on-failure negligible one frame Video retain-on-failure records always full sequence Trace on-first-retry retry only full replay All three modes live under use in playwright.config.ts
Screenshots are nearly free, video costs recording time on every test, and traces cost only on the retried attempt — which is why the three defaults differ.

The configuration

import { defineConfig } from '@playwright/test';

export default defineConfig({
  // Retries are needed for trace: 'on-first-retry' to ever trigger.
  retries: process.env.CI ? 2 : 0,
  // Every artifact lands under this folder, one sub-folder per failing test.
  outputDir: 'test-results',
  use: {
    // Save one image only when a test fails.
    screenshot: 'only-on-failure',
    // Record every test but keep the video only for failures.
    video: 'retain-on-failure',
    // Record a full trace on the first retry of a failed test.
    trace: 'on-first-retry',
  },
  // The HTML reporter attaches all captured artifacts automatically.
  reporter: [['html', { open: 'never' }]],
});

Because trace: 'on-first-retry' only fires when a test is retried, it does nothing unless retries is above zero — the retry and timeout settings in Configuring Retries and Timeouts for Stable CI are a prerequisite. If you already keep a layered config with shared defaults, fold these three options into the base use block described in Playwright Config & Fixtures so every project inherits them.

What the recorders do across a retry

Following one failing test through both attempts makes the timing concrete. On attempt one the video recorder is running from the moment the context opens; the trace recorder is not armed at all, because no retry has happened yet. The test fails, Playwright takes the screenshot, closes the context, flushes the video file, and schedules the retry. On attempt two the trace recorder starts with the context, the video recorder runs again, and if the test fails a second time you end up with two screenshots, two videos, and one trace.

Recorder activity across an attempt and its retry A timeline showing video recording on both attempts, tracing only on the retry, and a screenshot taken at each failure. video trace shot attempt 1 retry not armed recording PNG PNG run start fail #1 fail #2 time
Video runs for the whole of both attempts, the trace is armed only for the retry, and a screenshot is taken at the instant each attempt is declared failed.

That asymmetry explains a common complaint: the video shows the failure but the trace does not exist, because the test failed once and was not retried. If you need a trace on the very first failure — for a suite where retries are deliberately disabled, or where the failure does not reproduce — use trace: 'retain-on-failure' instead and accept the extra recording cost on every test.

Where the artifacts land on disk

Everything is written under outputDir, which defaults to test-results and is wiped at the start of each run. Playwright creates one sub-folder per failing test, named from the file, the test title, and the project, so parallel workers never collide. Inside it you get test-failed-1.png, video.webm, and trace.zip — the numeric suffix increments per attempt, so a retried test has test-failed-1.png and test-failed-2.png side by side.

Layout of the test-results output directory An annotated tree showing the output directory, a per-test folder, and the screenshot, video, and trace files inside it. test-results/ outputDir, cleaned before each run checkout-fails-chromium/ one folder per failing test test-failed-1.png video.webm trace.zip written only when the test fails kept only for failing tests recorded on the first retry
Each failing test gets its own folder under outputDir; the HTML report links into these files, which is why the report alone is not portable.

The HTML report is a separate folder (playwright-report by default) that references those files by path. Upload both from your pipeline, or pass attachmentsBaseURL when you host them apart — the sharding setup in Running Playwright Tests in GitHub Actions with Sharding covers merging reports from several machines.

Attaching custom artifacts

Beyond the automatic captures, you can attach your own files — a downloaded export, an API response, a manual screenshot — to the report with testInfo.attach(). They appear inline in the HTML report next to the test:

import { test, expect } from '@playwright/test';

test('attaches a manual screenshot to the report', async ({ page }, testInfo) => {
  await page.goto('/dashboard');
  // Capture a buffer rather than writing to disk first.
  const shot = await page.screenshot();
  // Attach it so it shows up inline in the HTML report for this test.
  await testInfo.attach('dashboard-state', { body: shot, contentType: 'image/png' });
  await expect(page.getByRole('heading', { name: 'Overview' })).toBeVisible();
});

Attachments are the extension point the whole artifact system is built on: the automatic screenshot, video, and trace are themselves attachments with reserved names, which is why a custom reporter can read them the same way. If you emit artifacts into another system — a dashboard, a bug tracker — read them off result.attachments as described in Writing a Custom Playwright Reporter.

Step-by-step setup

  1. Enable retries so traces can trigger. Set retries: process.env.CI ? 2 : 0, because trace: 'on-first-retry' records nothing unless a failed test is actually retried.
  2. Set screenshot to only-on-failure. Add screenshot: 'only-on-failure' under use so an image is saved for failures and nothing is written for passing tests.
  3. Set video to retain-on-failure. Add video: 'retain-on-failure' so every test is recorded but only failure videos are kept, giving you the full sequence that led to the break without storing thousands of green recordings.
  4. Set trace to on-first-retry. Add trace: 'on-first-retry' so the second attempt of a failing test produces a complete replayable trace, the single most useful artifact for diagnosis.
  5. Pin outputDir so CI can collect the files. Set outputDir: 'test-results' explicitly and make sure the path is not inside a folder your pipeline cleans between steps, otherwise the report will link to files that no longer exist.
  6. Use the HTML reporter to surface artifacts. Configure ['html', { open: 'never' }] so all captured screenshots, videos, and traces attach to their tests automatically and the report does not pop open in CI.
  7. Attach any custom evidence with testInfo.attach(). For files Playwright does not capture by default, call await testInfo.attach(name, { body, contentType }) inside the test so the artifact appears inline in the report.
  8. Upload the report and the results folder from CI. Publish playwright-report and test-results as build artifacts with a retention window — a week is usually enough — following the pipeline patterns in CI/CD Integration.

Troubleshooting variants

The report opens but every video and trace link is broken

The report folder was uploaded without test-results, so the HTML references files that were never published. Upload both directories from the same job, or unpack them into the same parent folder before opening the report locally. When the report is served from object storage and the artifacts live elsewhere, set attachmentsBaseURL on the HTML reporter so the links resolve against the correct host.

Videos are enormous and the runner disk fills up

retain-on-failure still records every test, and on a large suite the temporary files can outgrow the runner before the passing recordings are deleted. Shrink the frame size with the object form — video: { mode: 'retain-on-failure', size: { width: 800, height: 600 } } — or scope recording to a single project so only the browser you actually debug in is filmed. The same tuning matters for scraping-style runs where hundreds of contexts open in sequence.

A screenshot is captured but shows a blank or half-rendered page

The image is taken at the moment the test is declared failed, which is often mid-navigation or mid-animation. That is accurate evidence, not a bug: the page really was in that state when the assertion timed out. Read the video or the trace for the frames leading up to it, and if animations make the frame useless, disable them for capture the way baseline snapshots do in Masking Dynamic Regions in Snapshots.

Verification

Confirm capture works four ways. First, deliberately fail a test locally and check the test-results folder — a screenshot and video should appear for that test and for no passing test. Second, open the HTML report and verify the failed test shows its image, video, and a trace launcher inline. Third, force a retry on CI and confirm the trace from the second attempt downloads and opens in Analyzing Test Failures with Playwright Trace Viewer, where the network and console panes are described in Reading Network and Console Tabs in Traces. Fourth, run the suite green and confirm test-results is empty afterwards — leftover files there mean a mode is set to on rather than a failure-scoped value.

Once the evidence pipeline is reliable, it changes how you triage: a failure report that arrives with a frame, a film, and a replay is usually diagnosable without reproducing anything locally, which is the foundation of the workflow in Detecting and Fixing Flaky Playwright Tests.

Frequently Asked Questions

Why is no trace captured even though I set trace on-first-retry?

The on-first-retry mode only records a trace when a failed test is retried, so it never fires if retries is zero. Set retries above zero in your config — commonly process.env.CI ? 2 : 0 — or switch to trace: 'retain-on-failure' if you want a trace on the very first failure without a retry.

Does video: 'retain-on-failure' slow down passing tests?

Playwright records every test to produce the failure videos, so there is a small overhead even on passing runs, but the recording is discarded the moment a test passes. The cost is modest and usually worth the evidence; if it matters for a huge suite, scope video capture to a single project rather than the whole run.

How do I attach a file Playwright does not capture automatically?

Use testInfo.attach(name, { body, contentType }) inside the test, passing a buffer or a path and the correct MIME type. The attachment appears inline in the HTML report next to that test, which is the way to surface downloaded exports, API payloads, or manual screenshots alongside the automatic artifacts.

Can I record video for one project only?

Yes. The use block is per project as well as global, so declare video: 'retain-on-failure' inside a single entry of the projects array and leave the top-level default at 'off'. Teams commonly film only the Chromium project and rely on traces for the other engines, which cuts recording overhead by roughly two thirds while keeping one watchable failure per test.

Why does the same test produce two screenshots?

The numeric suffix on test-failed-N.png counts attempts, so a test that failed, was retried, and failed again writes one image per attempt. Comparing the two is genuinely useful — an identical pair points at a real defect, while two different end states point at a timing problem worth chasing rather than a broken assertion.

Back to overview