How to Add Playwright Screenshots to Test Reports
Capture screenshots on failures or at chosen test steps, attach them to Playwright reports, and troubleshoot missing images in CI.
Playwright Test can add screenshots to test reports in two ways: enable automatic capture in playwright.config.ts, or take a screenshot at a precise point and attach its bytes with testInfo.attach(). Use automatic capture for failure diagnostics and explicit attachments when the screenshot must show a particular application state or belong to a named step.
Choose the screenshot method
| Goal | Recommended API | When it captures | Where it appears |
|---|---|---|---|
| Capture failed tests automatically | use.screenshot: 'only-on-failure' |
After a test fails | Test result in the HTML report |
| Capture every test | use.screenshot: 'on' |
After every test | Test result in the HTML report |
| Capture the first failure in a retry sequence | use.screenshot: 'on-first-failure' |
On the first failure | Test result in the HTML report |
| Capture a chosen application state | page.screenshot() plus testInfo.attach() |
Exactly where your test calls it | Test-level attachment |
| Capture a named step | step.attach() inside test.step() |
Exactly where the step calls it | Attachment under that step |
Automatic screenshots are off by default. The documented screenshot modes and attachment APIs are part of @playwright/test, the Playwright Test runner, rather than only the lower-level Playwright library. See the TestOptions screenshot reference and TestInfo API.
How do I take a screenshot only when a test fails?
Set screenshot to only-on-failure in the use section of your Playwright configuration and enable the HTML reporter.
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
screenshot: 'only-on-failure',
},
reporter: 'html',
});
Run the tests normally:
npx playwright test
Then open the generated report:
npx playwright show-report
When a test fails, Playwright Test captures a screenshot and includes it with the test result. This is convenient for diagnostics because the test does not need custom screenshot code.
Capture every test or only the first failure
Use on when every test needs an image, and on-first-failure when retries are enabled and you want to avoid an image for every retry after the first failed attempt.
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
screenshot: 'on', // or 'on-first-failure'
},
reporter: 'html',
});
Choose the mode based on report size and diagnostic value. Capturing every test creates more attachment data than capturing failures only.
How do I attach a screenshot to a Playwright test?
Call page.screenshot() at the point where the page is in the state you want to document, then pass the returned buffer to testInfo.attach(). Set an image content type so reporters can display it correctly.
import { test, expect } from '@playwright/test';
test('checkout confirmation', async ({ page }, testInfo) => {
await page.goto('/checkout/confirmation');
await expect(
page.getByRole('heading', { name: 'Order confirmed' })
).toBeVisible();
const screenshot = await page.screenshot();
await testInfo.attach('confirmation', {
body: screenshot,
contentType: 'image/png',
});
});
testInfo.attach() accepts either a file path or a buffer. With a buffer, the screenshot stays in memory until Playwright copies it into the test result’s attachment area. With a path, await attach() before deleting or moving the source file so the reporter can access it. The official API is documented in TestInfo.
Attach a file path
import { test } from '@playwright/test';
import { join } from 'node:path';
test('attach an existing image', async ({ page }, testInfo) => {
await page.goto('/dashboard');
const filePath = join(testInfo.outputDir, 'dashboard.png');
await page.screenshot({ path: filePath, fullPage: true });
await testInfo.attach('dashboard-full-page', {
path: filePath,
contentType: 'image/png',
});
});
Use a buffer when you do not need a persistent intermediate file. Use a path when another tool already generated the image or when you need to inspect the file during the test.
How do I attach a screenshot to a specific Playwright step?
Wrap the operation in test.step() and call step.attach(). The attachment is then associated with that named step instead of being stored only at test level.
import { test, expect } from '@playwright/test';
test('verify confirmation page', async ({ page }) => {
await page.goto('/checkout/confirmation');
await test.step('verify confirmation page', async step => {
await expect(
page.getByRole('heading', { name: 'Order confirmed' })
).toBeVisible();
const screenshot = await page.screenshot();
await step.attach('confirmation', {
body: screenshot,
contentType: 'image/png',
});
});
});
The step attachment API was added in Playwright v1.51. If step.attach() is unavailable, check the installed @playwright/test version and update it according to your project’s compatibility policy. See the TestStepInfo reference.
Configure and open the HTML report
The HTML reporter creates a folder containing the report page and its assets. Configure its output directory and opening behavior in playwright.config.ts:
import { defineConfig } from '@playwright/test';
export default defineConfig({
reporter: [[
'html',
{
outputFolder: 'playwright-report',
open: 'never',
},
]],
use: {
screenshot: 'only-on-failure',
},
});
Open the latest report with:
npx playwright show-report
The HTML reporter’s output folder can also be changed with configuration or the PLAYWRIGHT_HTML_OUTPUT_DIR environment variable. Its opening behavior supports always, never, and on-failure, including the corresponding PLAYWRIGHT_HTML_OPEN environment setting. Refer to the Playwright reporter documentation and running and debugging guide.
Keep report attachments available in CI
If your CI pipeline stores the HTML report and its attachment files in separate locations, configure attachmentsBaseURL so report pages can resolve the images.
import { defineConfig } from '@playwright/test';
export default defineConfig({
reporter: [[
'html',
{
outputFolder: 'playwright-report',
open: 'never',
attachmentsBaseURL: 'https://artifacts.example.test/playwright/attachments/',
},
]],
});
The URL must point to the location where the attachment assets are actually published. Keep the report and attachment retention policies aligned; a report whose image files have expired will show broken attachments.
Capture only the state that explains the failure
A failure screenshot is most useful when the page has reached the state under test. Wait for a stable assertion before taking a deliberate screenshot.
import { test, expect } from '@playwright/test';
test('shows validation error', async ({ page }, testInfo) => {
await page.goto('/signup');
await page.getByRole('button', { name: 'Create account' }).click();
await expect(page.getByText('Email is required')).toBeVisible();
await testInfo.attach('validation-error', {
body: await page.screenshot({ fullPage: true }),
contentType: 'image/png',
});
});
Use fullPage: true when content below the viewport matters. Use a locator screenshot when the report should focus on one component:
const dialog = page.getByRole('dialog');
await testInfo.attach('error-dialog', {
body: await dialog.screenshot(),
contentType: 'image/png',
});
Automatic capture versus manual attachment
| Consideration | Automatic setting | Manual attachment |
|---|---|---|
| Trigger | After a test outcome, according to the selected mode | At an explicit line in the test |
| Timing | Convenient but not chosen for a particular UI state | Exact state, assertion, or transition |
| Scope | Test result | Test result, or named step with step.attach() |
| Maintenance | One configuration setting | Code near the behavior being documented |
| Storage | Reporter-managed attachment files | Reporter-managed files after attach() completes |
A common setup combines both: use only-on-failure as a safety net and add manual attachments at checkpoints whose state is important to understand.
Why don’t my screenshots show up in the HTML report?
- Screenshot capture is still off. Automatic capture defaults to
off. Setuse.screenshottoonly-on-failure,on, oron-first-failure. - The project is not using Playwright Test. The
testInfoand reporter configuration described here require@playwright/test, not only the lower-levelplaywrightpackage. - The attachment call was not awaited. Write
await testInfo.attach(...)orawait step.attach(...)so Playwright copies the attachment before the test finishes. - The content type is missing or wrong. For PNG bytes, use
contentType: 'image/png'. Use the matching type for another image format. - The report is not the HTML reporter. Configure
reporter: 'html'or the equivalent reporter array, then runnpx playwright show-report. - Report assets were uploaded separately. Set
attachmentsBaseURLto the public base URL for those files and verify that the files remain available. - A custom reporter ignores attachments. A custom reporter must inspect
testResult.attachmentsthrough the Reporter API. See the Reporter API. - The screenshot was taken too early. Wait for the relevant locator or assertion before calling
page.screenshot().
Performance, reliability, and storage
- Capture fewer images when reports are large. Failure-only mode usually produces less attachment data than capturing every test.
- Prefer locator screenshots for focused evidence. A component image is smaller and easier to inspect than a full page when surrounding content is irrelevant.
- Use full-page capture selectively. Full-page images include all scrollable content and can be considerably larger.
- Wait for deterministic state. Assertions and explicit waits reduce screenshots of loading spinners, partially rendered components, or transient overlays.
- Keep attachment retention with report retention. Separating the report and assets is workable only while the configured base URL remains valid.
- Account for retries. The
on-first-failuremode limits duplicate screenshots when a test retries; chooseonly-on-failurewhen each failed result needs its own evidence.
Playwright’s screenshot settings do not publish a fixed storage price; your cost depends on the CI artifact or report hosting system you use and how many images you retain.
Or skip the browser setup
If you need a screenshot of a URL outside a Playwright test, ScreenshotNeo provides a single GET request that returns PNG, JPEG, WebP, or PDF. It accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and each response identifies the result with X-Page-Verdict and X-Billed headers. It also provides an MCP server for Claude, Cursor, and other MCP clients, with take_screenshot, get_page_info, and capture_pdf tools.
See the ScreenshotNeo API documentation for all options.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
Python
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://stripe.com'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const bytes = Buffer.from(await res.arrayBuffer());
require('node:fs').writeFileSync('shot.webp', bytes);
ScreenshotNeo supports full-page and element capture, device presets and custom viewports, dark mode, retina scale, PDF options, custom CSS and JavaScript, clicks, waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, configurable caching, signed links, asynchronous jobs with signed webhooks, bulk capture for up to 100 URLs per call, usage reporting, and an OpenAPI specification. Every feature is available on every plan. The Free plan includes 1,000 shots per month without a card; paid plans start at $5 for 3,000 shots.
Create a free ScreenshotNeo account and get 1,000 screenshots a month with no card.
Short FAQ
Does Playwright save screenshots automatically?
No. Automatic screenshot capture defaults to off; enable one of the documented screenshot modes in the test configuration.
Can I attach a screenshot without writing it to disk?
Yes. Pass the buffer returned by page.screenshot() as the body of testInfo.attach() or step.attach().
Can one test have several screenshots?
Yes. Call attach() multiple times with distinct names, ideally at meaningful checkpoints.
Which API puts an image under a step?
Use step.attach() inside the callback passed to test.step(). testInfo.attach() stores the attachment at test level.
Can a custom reporter consume these images?
Yes. A custom reporter can read attachments from each test result through the Reporter API and publish or transform them for another destination.


