Part 04 · Architecture, Auth & Data Card 22
Failure Artifacts
Card 22: Failure Artifacts
What This Pattern Solves
A test fails in CI and you open the report — but all you see is “expected A, received B”. No console errors, no stack trace from the browser, no clue what the page actually rendered. You spend 20 minutes reproducing locally. This card shows how to configure Playwright to automatically capture diagnostic data on failure — traces, screenshots, videos, and console errors — so you can debug any failure in under a minute from the HTML report alone.
How It Works
- Configure
trace: 'on-first-retry',screenshot: 'only-on-failure', andvideo: 'on-first-retry'inplaywright.config.tsso artifacts are captured without slowing passing tests - Create an auto-fixture (
captureErrors) that subscribes topage.on('pageerror')(uncaught exceptions) andpage.on('console')(console.error calls) during the test - After the test completes (via
use()), checktestInfo.status !== 'passed'— if the test failed and errors were captured, attach them withtestInfo.attach('page-errors', ...) - The fixture is
{ auto: true }, so it runs for every test without the test needing to mention it - Add a requested
lastApiResponsefixture that mocks an endpoint, records the served body, and attaches it aslast-api-responseJSON after the test - Open the HTML report: each failed test now has a trace viewer, screenshot, video, a “page-errors” attachment with all browser-side errors, and the captured API response
- Retries: first attempt captures trace/video; if it passes on retry, you still have diagnostics from the failed attempt
Code Example
import { test as base, expect } from '@playwright/test';
const test = base.extend<{
captureErrors: void;
lastApiResponse: { response: { status: number; body: unknown } | null };
}>({
captureErrors: [
async ({ page }, use, testInfo) => {
const errors: string[] = [];
page.on('pageerror', (e) => {
errors.push(`PAGEERROR: ${e.message}`);
});
page.on('console', (msg) => {
if (msg.type() === 'error') {
errors.push(`CONSOLE: ${msg.text()}`);
}
});
await use();
if (testInfo.status !== 'passed' && errors.length > 0) {
await testInfo.attach('page-errors', {
body: errors.join('\n'),
contentType: 'text/plain',
});
}
},
{ auto: true },
],
lastApiResponse: async ({ page }, use) => {
const ref: { response: { status: number; body: unknown } | null } = {
response: null,
};
await page.route('**/swapi.dev/api/people/1/**', async (route) => {
ref.response = {
status: 200,
body: { name: 'Luke Skywalker', height: '172', mass: '77' },
};
await route.fulfill({
status: 200,
contentType: 'application/json',
json: ref.response.body,
});
});
await use(ref);
if (ref.response) {
await test.info().attach('last-api-response', {
body: JSON.stringify(ref.response, null, 2),
contentType: 'application/json',
});
}
},
});
test.describe('22-failure-artifacts: Error capture fixture', () => {
// captureErrors is an auto fixture: it runs for every test without being
// requested, and attaches collected errors only when the test does not pass.
test('passing test attaches nothing', async ({ page }) => {
await page.goto('/cards/22');
await expect(page.getByRole('heading', { name: 'Person' })).toBeVisible();
});
test('lastApiResponse captures and attaches the API body', async ({
page,
lastApiResponse,
}) => {
await page.goto('/cards/22');
await expect(page.getByRole('heading', { name: 'Person' })).toBeVisible();
expect(lastApiResponse.response).not.toBeNull();
expect(lastApiResponse.response!.status).toBe(200);
expect(lastApiResponse.response!.body).toEqual(
expect.objectContaining({ name: 'Luke Skywalker' }),
);
});
});
Run This Example
pnpm test src/22-failure-artifacts
Prerequisites
- Card 07: Understanding fixture lifecycle and
test.extend - Card 01: Understanding the Playwright config file
- Concepts: browser events, test lifecycle hooks, HTML reporter attachments
Key Concepts
- Auto-fixture: A fixture with
{ auto: true }runs for every test in the suite automatically. The test doesn’t need to destructure it (but can, for clarity). Perfect for cross-cutting concerns like error capture, timing, or logging. - page.on(‘pageerror’): Listens for uncaught JavaScript exceptions in the page. These are fatal errors that crash the app — you definitely want them in the report.
- page.on(‘console’): Listens for all console output. Filter to
msg.type() === 'error'to captureconsole.error()calls without noise fromconsole.log(). - testInfo.attach(): Adds an arbitrary artifact (text, JSON, binary) to the test result. Visible in the HTML report under the “Attachments” section. Name it descriptively (
page-errors,network-requests,api-responses). - Conditional attachment: Only attach errors when
testInfo.status !== 'passed'. Passing tests don’t need the noise; failing ones get maximum diagnostic data. - trace/screenshot/video config: Set in
playwright.config.tsunderuse:.on-first-retrymeans artifacts are only captured on retries (not first attempt), keeping CI fast for passing suites.only-on-failurecaptures only when the test fails.
When to Use This Pattern
- ✓ Every CI suite — there’s no reason not to capture diagnostics on failure
- ✓ Heisenbugs that only fail in CI and never locally
- ✓ Tests that interact with complex JavaScript apps (race conditions, unhandled rejections)
- ✓ Onboarding new team members — they can read the report without reproducing
- ✗ Local development with
--uimode (use the built-in trace viewer instead, or toggle artifacts off for speed)
Common Mistakes
-
Not filtering console to errors only:
// ❌ WRONG — attaches every console.log, huge attachment page.on('console', (msg) => errors.push(msg.text())); // ✓ CORRECT — only console.error calls page.on('console', (msg) => { if (msg.type() === 'error') errors.push(`CONSOLE: ${msg.text()}`); }); -
Attaching errors on passing tests:
// ❌ WRONG — every test gets an error attachment, even passing ones await testInfo.attach('page-errors', { body: errors.join('\n'), ... }); // ✓ CORRECT — only attach when the test actually failed if (testInfo.status !== 'passed' && errors.length > 0) { await testInfo.attach('page-errors', { body: errors.join('\n'), ... }); } -
Capturing trace on every test (not just retry):
// ❌ WRONG — slows CI by 2-5x for passing suites use: { trace: 'on' } // ✓ CORRECT — trace only on retry, screenshot only on failure use: { trace: 'on-first-retry', screenshot: 'only-on-failure', video: 'on-first-retry', } -
Forgetting to handle the
use()lifecycle correctly:// ❌ WRONG — subscribing after use() so no errors are captured await use(); page.on('pageerror', handler); // ✓ CORRECT — subscribe before use(), capture during test, attach after page.on('pageerror', handler); await use(); if (failed) await testInfo.attach(...);
Flow Diagram
sequenceDiagram
participant Config as playwright.config
participant Fixture as captureErrors fixture
participant Page as Browser Page
participant Test
participant Report as HTML Report
Note over Config: trace: on-first-retry<br/>screenshot: only-on-failure<br/>video: on-first-retry
Fixture->>Page: on('pageerror', capture)
Fixture->>Page: on('console', capture if error)
Test->>Page: goto('/cards/22')
Page-->>Page: (triggers console.error)
Fixture->>Fixture: errors.push('CONSOLE: ...')
Test->>Test: expect(heading).toBeVisible()
Note over Test: Assertion FAILS
Fixture->>Fixture: use() returns
Fixture->>Fixture: testInfo.status = 'failed'
Fixture->>Fixture: errors.length > 0 → true
Fixture->>Report: attach('page-errors', errors.join('\n'))
Config->>Report: attach trace (first retry)
Config->>Report: attach screenshot (on failure)
Config->>Report: attach video (first retry)
Note over Report: One-click debug:<br/>trace viewer, screenshot,<br/>video, console errors
Related Patterns
- Previous: Card 21 (App Driver Fixture) — Errors captured with full app driver context
- Next: Card 23 (API-Only Tests) — Debug API test failures with request-level diagnostics
- Foundation: Card 07 (Patch Fixtures) — Auto-fixture lifecycle deep dive
- Complementary: Card 16 (Debug Unhandled Requests) — Another diagnostic technique for mocked APIs
- Complementary: Card 18 (Stability Techniques) — Reduce flake so these artifacts fire less often
Live Demo
👇 This component is what the Playwright test interacts with:
Loading…
Run This Example
pnpm test src/22-failure-artifacts