|

Playwright Video Recording: Configuration and Failure Debugging

A test fails in CI, the logs are cryptic, and you cannot reproduce it locally no matter how many times you re-run the spec. This is the exact moment a recorded video earns its keep: a frame-by-frame replay of what the browser actually did. In this guide you will master Playwright video recording config from the ground up, learn how to capture clips only when tests fail, control resolution and storage, and turn raw .webm files into a fast failure-debugging workflow.

🎭 Want to master this with real projects? Join the Playwright Automation Mastery course at The Testing Academy.

Contents

Why Video Recording Matters for Flaky Tests

Screenshots freeze a single instant; videos preserve the full sequence of clicks, navigations, and animations that led to a failure. When a test is flaky in a headless CI runner but green on your laptop, a video is often the fastest route to the root cause. You can see whether a modal appeared late, whether a network spinner never resolved, or whether an element shifted under the cursor right as Playwright clicked.

Playwright records video at the browser context level. Every page opened inside a context is captured into a single WebM file, and the file is finalized only when the context closes. This is important: if you forget to close the context, the video may be truncated or missing. The Playwright Test runner manages context lifecycle for you, which is why the recommended path is to configure video in playwright.config.ts rather than wiring it up by hand.

The Core Video Recording Config in playwright.config.ts

The single most useful setting is video: 'retain-on-failure'. Playwright records every test but deletes the clip for any test that passes, leaving you only the videos that matter. This keeps your artifact storage small while guaranteeing evidence for every failure. Here is a production-ready Playwright video recording config.

// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  retries: process.env.CI ? 2 : 0,
  use: {
    // Record a video for every test, but keep only failures.
    video: 'retain-on-failure',
    // Trace is the perfect companion for video-based debugging.
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
  ],
});

The video option accepts a string shorthand or an object. The shorthand values are 'off', 'on', 'retain-on-failure', and 'on-first-retry'. The last one is a sweet spot for CI: the first attempt runs without recording overhead, and only when Playwright retries a failed test does it record, so you pay the cost only when something is already broken.

Comparing Video Recording Modes

Choosing the right mode is a trade-off between coverage, CI runtime, and storage. The table below compares each value so you can pick the one that fits your pipeline.

ModeRecords whenKeeps file whenBest for
offNeverNeverFast local TDD loops
onEvery testAlwaysDebugging one suite locally
retain-on-failureEvery testOnly on failureCI with rich failure evidence
on-first-retryOnly during a retryOnly on retried failureLarge CI suites, minimal overhead

Note that on-first-retry requires retries to be greater than zero, otherwise no retry ever happens and you will never get a video. Pair it with retries: 2 in CI as shown earlier.

Controlling Video Resolution and Size

By default Playwright scales the video down to fit a 800×800 box while preserving aspect ratio, which keeps files small. When you need crisp, full-resolution footage, pass an object to video with an explicit size. The mode key takes the same values as the shorthand string.

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  use: {
    // Match the recording size to the viewport for sharp video.
    viewport: { width: 1280, height: 720 },
    video: {
      mode: 'retain-on-failure',
      size: { width: 1280, height: 720 },
    },
  },
});

A few practical rules keep file sizes sane. Set the size equal to your viewport so frames are not upscaled or letterboxed. Larger dimensions mean bigger files and slightly slower tests, so 1280×720 is a good default for most dashboards. If your app only matters at mobile width, record at 390x844 instead and your artifacts shrink dramatically.

Recording Video at the Context Level Manually

If you use the raw Playwright library instead of the Test runner, you enable recording by passing recordVideo to browser.newContext(). You must close the context to flush the file to disk, then read the path with page.video().path(). This is the underlying mechanism the Test runner wraps for you.

import { chromium } from 'playwright';

(async () => {
  const browser = await chromium.launch();
  const context = await browser.newContext({
    recordVideo: {
      dir: './videos/',
      size: { width: 1280, height: 720 },
    },
  });

  const page = await context.newPage();
  await page.goto('https://playwright.dev/');
  await page.getByRole('link', { name: 'Get started' }).click();

  // Resolve the saved file path BEFORE closing for logging.
  const videoPath = await page.video()?.path();
  console.log('Video will be saved to:', videoPath);

  // Closing the context finalizes and writes the .webm file.
  await context.close();
  await browser.close();
})();

Two gotchas trip people up here. First, page.video() returns undefined if recordVideo was not set on the context, so guard it with optional chaining. Second, the file does not exist until the context is closed; calling saveAs() before context.close() waits for the recording to finish, but path() only gives you the eventual location, not a ready file.

Attaching Videos to the HTML Report

When you run with the built-in HTML reporter, videos retained on failure are linked automatically inside each failed test’s detail panel. Enable it in your config and the wiring is automatic.

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  reporter: [['html', { open: 'never' }], ['list']],
  use: {
    video: 'retain-on-failure',
    trace: 'retain-on-failure',
  },
});

After a run, open the report with npx playwright show-report. Each failed test exposes the recorded WebM under an “Attachments” section, alongside the trace and the failure screenshot. For custom reporting you can also attach a video yourself inside a fixture or hook using testInfo.attachments, but for most teams the HTML reporter’s automatic linking is all you need.

A Fixture That Logs the Video Path on Failure

Sometimes you want the raw video path printed to the CI log so a teammate can grab it from the artifact bucket without opening the HTML report. A small custom fixture using testInfo does exactly that, reading the status after the test body runs.

// fixtures.ts
import { test as base } from '@playwright/test';

export const test = base.extend<{}>({
  page: async ({ page }, use, testInfo) => {
    await use(page);

    // Runs after the test body; check the outcome here.
    if (testInfo.status !== testInfo.expectedStatus) {
      const video = page.video();
      if (video) {
        const path = await video.path();
        console.log(`[FAILED] ${testInfo.title} -> video: ${path}`);
      }
    }
  },
});

export { expect } from '@playwright/test';

This pattern composes cleanly with retain-on-failure: Playwright keeps the file because the test failed, and your fixture surfaces the path so it is one click away in the CI console. Import this test in your specs instead of the default one and every failing test self-documents its recording.

🚀 Level Up Your Playwright

From locators to CI pipelines — build a production-grade Playwright + TypeScript framework step by step.

Debugging Workflow: From Failure to Fix

A video tells you what happened; a trace tells you why. The most effective debugging loop combines both. Configure trace: 'retain-on-failure' next to your video setting, and when a test fails, follow this sequence.

  1. Open the HTML report and watch the WebM at the moment of failure to see the visual symptom.
  2. Note the timestamp where the UI looked wrong, then open the trace with npx playwright show-trace.
  3. Scrub the trace timeline to the same moment to read the exact action, the DOM snapshot, and the network log.
  4. Reproduce locally with --headed --debug or page.pause() once you know the failing step.

Because the trace already includes per-action screenshots and a DOM snapshot, you may not always need video. The rule of thumb: keep video for timing and animation bugs where seeing motion matters, and lean on the trace for assertion and locator failures where you need to inspect the DOM.

Common Pitfalls and How to Avoid Them

  • Empty or missing files: the context never closed. Let the Test runner own the lifecycle, or always await context.close() in library mode.
  • Huge artifact folders: you used video: 'on' in CI. Switch to retain-on-failure or on-first-retry.
  • Blurry video: the recording size does not match the viewport. Set them equal.
  • No video despite on-first-retry: retries is zero, so no retry ever runs. Set retries to at least 1 in CI.
  • Slow local runs: recording adds overhead. Keep video: 'off' for day-to-day TDD and only enable it when chasing a bug.

Conclusion

A well-tuned Playwright video recording config converts mysterious CI failures into a thirty-second clip you can actually watch. Start with retain-on-failure, match the recording size to your viewport, and pair video with trace so you have both the visual symptom and the technical cause in one place. Once recording, reporting, and a path-logging fixture are wired up, every red test in your pipeline arrives with the evidence already attached, and debugging flaky behavior stops being guesswork.

FAQ

Where does Playwright save recorded videos?

With the Test runner, videos land inside the per-test output folder under test-results/, named like video.webm, and retained clips are linked in the HTML report. In raw library mode the file is written to the dir you pass to recordVideo, and you can read its exact location with await page.video().path() after the context closes.

Does video recording slow down my tests?

Yes, recording adds measurable overhead because Playwright captures frames continuously for the life of the context. The impact is usually small but grows with larger video size values. To minimize it, use on-first-retry so recording only kicks in during a retry, keep the resolution at or below your viewport, and disable video entirely for fast local TDD loops.

Can I record only failing tests without recording everything?

Not directly, because Playwright cannot know a test will fail until it runs, so retain-on-failure records every test and then deletes the passing ones. If you want to avoid recording on the happy path entirely, use on-first-retry with retries set to at least 1: the first run is unrecorded and the retry produces the clip only for tests that already failed.

🎓 Master Playwright End to End

Join hundreds of SDETs building real automation frameworks. Lifetime access, hands-on projects, and a job-ready portfolio.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.