Playwright Test Reports With Screenshots
Generate Playwright HTML reports with failure screenshots, traces, custom attachments, CI artifacts, troubleshooting, and practical retention guidance.
Use Playwright’s HTML reporter with screenshot: 'only-on-failure' and trace: 'on-first-retry'. This keeps the report focused on failed tests while preserving a trace for retry debugging. Playwright writes screenshots, videos, and traces to the test output directory, usually test-results, and the HTML report to playwright-report.
1. Create an HTML report with failure screenshots
Install Playwright Test if it is not already in your project:
npm install -D @playwright/test
npx playwright install
Add this configuration to playwright.config.ts:
import { defineConfig } from '@playwright/test';
export default defineConfig({
reporter: [['html', { open: 'never' }]],
use: {
screenshot: 'only-on-failure',
trace: 'on-first-retry',
},
});
Run the suite and open the report:
npx playwright test
npx playwright show-report
The HTML reporter produces a self-contained folder that can be served as a web page. The report lists every test, browser project, duration, failure, attachment, and trace. Read the official HTML reporter documentation for reporter-specific options.
2. What the generated files contain
playwright-report/: the HTML report and its referenced assets.test-results/: per-test screenshots, traces, videos, stdout, and other attachments.- Failure screenshots: captured automatically when a test fails.
- Traces: captured on the first retry when a test is retried.
Keep the entire report directory together when publishing it. Moving only the HTML file can break links to screenshots or traces.
3. Configure screenshot capture
| Value | Behavior | Use it when |
|---|---|---|
'off' |
No automatic screenshots | You only attach deliberate screenshots or need minimal artifacts |
'on' |
Capture every test | You need visual evidence for successful and failed tests |
'only-on-failure' |
Capture failed tests | You want useful failure evidence with lower storage and runtime overhead |
These settings belong under use and apply to every project unless overridden:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
reporter: [['html', { open: 'never' }]],
retries: process.env.CI ? 2 : 0,
use: {
screenshot: 'only-on-failure',
trace: 'on-first-retry',
baseURL: 'https://staging.example.com',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
],
});
For a single project or test, override the setting locally:
import { test } from '@playwright/test';
test.use({ screenshot: 'on' });
test('checkout has the expected layout', async ({ page }) => {
await page.goto('/checkout');
});
4. Capture and attach a custom screenshot
Automatic failure screenshots are convenient, but a deliberate screenshot is better when you need a named checkpoint, a particular element, or a screenshot taken before an assertion.
import { test, expect } from '@playwright/test';
test('attach the checkout state', async ({ page }, testInfo) => {
await page.goto('/checkout');
await page.getByRole('button', { name: 'Review order' }).click();
const path = testInfo.outputPath('checkout-review.png');
await page.screenshot({ path, fullPage: true });
await testInfo.attach('checkout-review', {
path,
contentType: 'image/png',
});
await expect(page.getByRole('heading', { name: 'Review order' })).toBeVisible();
});
testInfo.outputPath() places the file in the test’s output directory, avoiding collisions between workers and retries. testInfo.attach() gives the reporter the name and MIME type needed to display the image. See Playwright test attachments.
Element, viewport, and full-page screenshots
await page.screenshot({ path: testInfo.outputPath('viewport.png') });
await page.screenshot({ path: testInfo.outputPath('full-page.png'), fullPage: true });
await page.locator('[data-testid="invoice"]').screenshot({
path: testInfo.outputPath('invoice.png'),
});
Use an element screenshot for a stable component. Use fullPage for a document, but expect taller files and more rendering time. If the page contains lazy content, scroll or wait for the content before capturing it.
5. Add useful context around failures
Assertions should identify the state that matters. Add a custom attachment for structured data when an image alone is not enough:
import { test } from '@playwright/test';
test('attach diagnostic data', async ({ page }, testInfo) => {
await page.goto('/account');
const state = await page.locator('body').innerText();
await testInfo.attach('visible-text.txt', {
body: Buffer.from(state, 'utf8'),
contentType: 'text/plain',
});
});
Use stable locators and explicit waits rather than arbitrary delays. A screenshot records what Playwright saw; it does not prove that asynchronous data finished loading unless your test waits for that state.
6. Use traces to inspect failed tests
Set trace: 'on-first-retry' for CI. When a test fails and is retried, Playwright records the first retry. The Trace Viewer includes action snapshots, logs, source locations, network information, metadata, and attachments.
npx playwright show-trace test-results/path-to-trace/trace.zip
The HTML report links to the trace when it is retained with the report artifacts. See the Trace Viewer documentation.
Choosing a trace mode
| Mode | Evidence | Trade-off |
|---|---|---|
'off' |
No trace | Smallest artifacts |
'on-first-retry' |
Trace on the first retry | Good CI default for intermittent failures |
'on-all-retries' |
Trace on every retry | More diagnostic history and storage |
'retain-on-failure' |
Keep traces for failures | Useful when tests pass after a retry |
'on' |
Trace every test | Highest artifact volume |
7. Control the HTML reporter
import { defineConfig } from '@playwright/test';
export default defineConfig({
reporter: [['html', {
outputFolder: 'artifacts/playwright-report',
open: 'never',
title: 'Web app end-to-end tests',
host: '127.0.0.1',
port: 9323,
}]],
});
outputFolder: choose where the report is written.open: use'always','never', or'on-failure'depending on your local workflow.title: set a recognizable report title.hostandport: control the local server used byshow-report.attachmentsBaseURL: point report links at separately hosted attachments when your artifact layout requires it.
8. Publish reports from CI
Always upload both directories (or the equivalent configured paths) as one artifact:
npx playwright test
# Upload artifacts/playwright-report and test-results with your CI provider
npx playwright show-report artifacts/playwright-report
Set a retention period that matches your debugging needs. A practical policy is to retain every failed run long enough to investigate regressions, while deleting passing-run artifacts sooner. Restrict report access because screenshots and traces can contain account data, tokens in URLs, or personal information.
GitHub Actions example
- name: Install browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npx playwright test
- name: Upload Playwright report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: |
playwright-report/
test-results/
retention-days: 14
9. Performance, reliability, and cost considerations
- Runtime: screenshots add encoding and file-write work. Full-page images and screenshots for every test cost more than failure-only capture.
- Parallel workers: keep each file under
testInfo.outputPath()so workers and retries do not overwrite one another. - Flaky tests: combine failure screenshots with
trace: 'on-first-retry'. A screenshot shows appearance; a trace explains actions, timing, requests, and console activity. - Artifact size: limit screenshots to the states you need, avoid duplicate custom attachments, and choose retention deliberately.
- Reproducibility: pin browser versions in CI, use the same viewport and timezone, and wait for deterministic application state before capture.
- Security: treat reports as sensitive build artifacts. Redact secrets from URLs and avoid uploading reports publicly unless the application data is safe to expose.
10. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| No screenshot after a failure | Screenshot capture is 'off', or the wrong config file is loaded |
Set use.screenshot to 'only-on-failure' and confirm the command uses the intended config |
| Report opens but images are missing | Only the HTML file was copied, or attachments were moved | Publish playwright-report with its assets and retain test-results |
| Custom image is not shown | The attachment has no path, body, or content type | Use testInfo.attach with path or body and contentType: 'image/png' |
| Screenshot captures a spinner | The test captures before application data settles | Wait for a meaningful locator, response, or network-idle condition; prefer a state assertion over a fixed sleep |
| Full-page image is incomplete | Lazy-loaded content was never triggered | Scroll through the page or wait for the relevant content before calling fullPage |
| Trace is absent | No retry occurred, or traces were discarded | Run with retries and use 'on-first-retry', then upload the complete test output directory |
| CI report is too large | Every test stores screenshots, videos, and traces | Use failure-only screenshots, retry traces, shorter retention, and targeted attachments |
| Different browsers show different images | Viewport, fonts, timezone, or browser rendering differs | Define projects explicitly and compare like-for-like environments |
11. Or skip the browser setup
If you need screenshots of deployed pages for a report, documentation, or an external monitoring workflow, ScreenshotNeo returns an image or PDF from one request. Its capture steps can accept consent banners and remove more than 60 known consent platforms, newsletter popups, and chat widgets before the shot. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing result.
See the ScreenshotNeo API documentation for all options. A minimal request is:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo also supports full-page and element capture, dark mode, device presets, retina scale, PDF output, custom CSS and JavaScript, clicks, waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, resizing, caching, signed links, asynchronous jobs, webhooks, bulk capture, a usage API, and an OpenAPI specification. Its MCP server lets Claude, Cursor, and other MCP clients use take_screenshot, get_page_info, and capture_pdf. There are 1,000 free screenshots per month with no card; paid plans start at $5 for 3,000.
Create a free ScreenshotNeo account and start with the included 1,000 screenshots.
12. FAQ
Does the HTML report include screenshots automatically?
Only when screenshot capture is enabled. Set screenshot: 'only-on-failure' for automatic failure evidence.
Where does Playwright save screenshots?
Playwright normally writes screenshots, traces, and videos under the test output directory, typically test-results.
Can I attach a screenshot without failing the test?
Yes. Save it with page.screenshot and attach it with testInfo.attach.
Should I use screenshots or traces?
Use screenshots to see the rendered state. Use traces when you need the action sequence, timing, network details, logs, and attachment context.
Can I host the report as a static artifact?
Yes. The HTML reporter creates a self-contained report folder that can be served as a web page, provided its assets and referenced attachments are retained.


