Free tools Windows power users keep installed
One-click scans. No signup required.
Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.
To place a screenshot next to one Playwright test step, capture the page as a buffer inside that step’s callback and call step.attach() with contentType: 'image/png'. Playwright added this step-scoped API in v1.51. Use testInfo.attach() instead when the image belongs to the entire test, not an individual step.
Prerequisites and the version check
Install Playwright Test in the project that runs your tests:
npm install --save-dev @playwright/test
npx playwright install
Check the installed version before using step attachments:
npx playwright --version
TestStepInfo.attach() is documented as available from Playwright v1.51. If the command reports an older version, update the package and lockfile before changing your test code. The API reference is at playwright.dev/docs/api/class-teststepinfo.
#1 Best Overall
Attach a screenshot to a specific test step
Pass the callback argument supplied by test.step() (often named step) to step.attach(). The following TypeScript test captures the current viewport and associates the PNG with the “verify confirmation page” step:
import { test, expect } from '@playwright/test';
test('checkout shows confirmation', async ({ page }) => {
await page.goto('https://example.com/checkout');
await test.step('verify confirmation page', async step => {
const screenshot = await page.screenshot();
await step.attach('confirmation screenshot', {
body: screenshot,
contentType: 'image/png',
});
await expect(
page.getByRole('heading', { name: 'Order confirmed' })
).toBeVisible();
});
});
page.screenshot() returns a Buffer when no path is supplied. The awaited attach() call copies the bytes to a location reporters can access, so a temporary buffer or file can be discarded after the call completes. Supply exactly one input: body or path, never both. Declaring the PNG MIME type makes the intended image format explicit to reporters; the TestStepInfo API documents image/png for this use.
Choose the correct attachment scope
Step-level evidence with step.attach()
Use step scope when the screenshot explains one action or assertion: a checkout confirmation, a failed validation message, or a menu opened during a workflow. Keep the capture and the assertion in the same callback so the report shows the evidence beside the operation it describes.
Recommended Free Tools
Test-level evidence with testInfo.attach()
Use the test fixture’s testInfo when an image represents the complete test rather than one step:
Rank #2
import { test } from '@playwright/test';
test('account page loads', async ({ page }, testInfo) => {
await page.goto('https://example.com/account');
const screenshot = await page.screenshot();
await testInfo.attach('account page', {
body: screenshot,
contentType: 'image/png',
});
});
Playwright documents these as different scopes: step.attach() belongs to a test.step(), while testInfo.attach() belongs to the test as a whole. Choosing the wrong method is the usual reason an image appears at the test level instead of under the expected step. See the TestInfo API for the test-level form.
Capture the amount of page that provides useful evidence
Viewport screenshot
The default page.screenshot() captures the visible viewport. It is usually the smallest and fastest artifact for a step such as “submit form” or “show error.”
Full-page screenshot
Set fullPage: true when content below the fold matters:
await test.step('verify order summary', async step => {
const image = await page.screenshot({ fullPage: true });
await step.attach('full order summary', {
body: image,
contentType: 'image/png',
});
});
Full-page images can be much larger. Use them for evidence that genuinely spans the document; otherwise a viewport or element capture is easier to inspect in a report.
Element-only screenshot
Capture only the component relevant to the step with a locator:
await test.step('verify payment error', async step => {
const alert = page.getByRole('alert');
const image = await alert.screenshot();
await step.attach('payment error alert', {
body: image,
contentType: 'image/png',
});
});
Playwright’s screenshot documentation covers viewport, full-page, and locator screenshots at github.com/microsoft/playwright/blob/main/docs/src/screenshots.md. Screenshot capture is separate from visual comparison: use toHaveScreenshot() when you want Playwright to compare pixels with an expected snapshot, and use an attachment when you want report evidence.
Use a file path when another process creates the image
A path is useful when a helper, image-diff tool, or diagnostic routine already writes a file. Pass the path to the step attachment and omit body:
The Tool Desk
Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →import { test } from '@playwright/test';
test('product card is visible', async ({ page }) => {
await page.goto('https://example.com/products');
await test.step('capture product card', async step => {
const file = 'test-results/product-card.png';
await page.locator('[data-testid="product-card"]').screenshot({
path: file,
});
await step.attach('product card', {
path: file,
contentType: 'image/png',
});
});
});
Because attach() is awaited, the reporter has finished copying the attachment before the step continues. That lets cleanup code remove a temporary file after the call if your workflow creates one.
Rank #4
Make the report and its attachments easy to inspect
Playwright HTML reporter
Generate the built-in HTML report explicitly:
npx playwright test --reporter=html
npx playwright show-report
The default output directory is playwright-report. The report is a self-contained folder that can be served as a web page. You can configure opening behavior with PLAYWRIGHT_HTML_OPEN and the destination with PLAYWRIGHT_HTML_OUTPUT_DIR. For example:
PLAYWRIGHT_HTML_OUTPUT_DIR=artifacts/html-report
PLAYWRIGHT_HTML_OPEN=never
npx playwright test --reporter=html
Open the generated report after the test run with npx playwright show-report artifacts/html-report when you use a custom directory. Reporter configuration and these environment variables are documented at playwright.dev/docs/next/test-reporters.
Other reporters
Recording an attachment and rendering it are separate concerns. Playwright’s API documentation cautions that “Some reporters show test step attachments.” A non-HTML reporter may preserve the attachment for later processing without displaying it inline, or may present it in a different location. Confirm the behavior of the reporter used by your local and CI commands before treating the visual layout as a contract.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
A complete multi-step example
This pattern gives each important transition its own artifact while keeping assertions readable:
import { test, expect } from '@playwright/test';
test('user can complete checkout', async ({ page }) => {
await test.step('open checkout', async step => {
await page.goto('https://example.com/checkout');
await step.attach('checkout loaded', {
body: await page.screenshot(),
contentType: 'image/png',
});
});
await test.step('submit shipping details', async step => {
await page.getByLabel('Address').fill('10 Main Street');
await page.getByRole('button', { name: 'Continue' }).click();
await step.attach('shipping submitted', {
body: await page.screenshot({ fullPage: true }),
contentType: 'image/png',
});
});
await test.step('verify confirmation page', async step => {
await expect(
page.getByRole('heading', { name: 'Order confirmed' })
).toBeVisible();
await step.attach('confirmation', {
body: await page.screenshot(),
contentType: 'image/png',
});
});
});
Attach after the UI state you want to document has been reached. If an assertion fails before the attachment line, no image is produced for that step; place a diagnostic capture before a fragile assertion when the pre-assertion state is what you need to investigate.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Troubleshoot missing or misplaced screenshots
“step.attach is not a function”
- Cause: Playwright is older than v1.51, or the callback is not receiving the step object.
- Fix: update
@playwright/test, verify withnpx playwright --version, and declare the callback asasync step => { ... }.
The image appears beside the test, not the step
- Cause: the code used
testInfo.attach(). - Fix: move the call inside the intended
test.step()callback and use itsstepargument.
The reporter does not show the image
- Cause: not every reporter renders step attachments; the API promises recording, not identical presentation across reporters.
- Fix: try the HTML reporter, inspect the generated report directory, and check that your selected reporter supports step-level rendering.
“body and path cannot both be supplied”
- Cause: the attachment object contains both properties.
- Fix: use the in-memory buffer with
bodyandcontentType, or use onlypath(with its content type).
The screenshot captures the wrong state
- Cause: capture occurred before navigation, animation, or the relevant element became ready.
- Fix: wait for a meaningful locator or assertion before calling
screenshot(); avoid arbitrary delays unless the application has no observable readiness signal.
CI artifacts are too large or slow
- Prefer viewport or element captures for routine steps.
- Reserve
fullPage: truefor evidence that needs below-the-fold content. - Use descriptive names so a failed step can be found without opening every image.
- Keep screenshots attached to the step that explains them instead of duplicating the same buffer at test and step scope.
Or skip the browser setup
If you need a screenshot of a URL rather than evidence from an already-running Playwright interaction, ScreenshotNeo provides a single HTTP request. Its API accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each behavior can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response reports the result in X-Page-Verdict and X-Billed headers. It also offers an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.
See the complete parameter reference in the ScreenshotNeo documentation. A direct cURL call is:
curl -G "https://api.screenshotneo.com/v1/shot"
-d access_key=YOUR_API_KEY
--data-urlencode url=https://example.com/checkout
-o checkout.webp
Equivalent Python:
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com/checkout"},
timeout=90,
)
r.raise_for_status()
open("checkout.webp", "wb").write(r.content)
Equivalent Node.js:
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://example.com/checkout'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const data = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('checkout.webp', data));
ScreenshotNeo supports full-page captures with lazy images loaded, CSS-selector element captures, dark mode, 12 device presets or custom viewports, retina scale, PDF options, custom CSS and JavaScript, click-before-capture actions, hidden selectors, waits for a selector, delay, or network idle, request and resource blocking, custom headers/cookies/user agents/Authorization, timezone and geolocation, transparent backgrounds, resizing, configurable-TTL caching, signed links, asynchronous jobs with signed webhooks, bulk capture for up to 100 URLs per call, a usage API, and an OpenAPI specification. Parameters commonly used by other screenshot APIs are accepted to ease migration.
| Plan | Included screenshots | Price |
|---|---|---|
| Free | 1,000 per month | $0, no card |
| Starter | 3,000 | $5 |
| Growth | 15,000 | $15 |
| Pro | 60,000 | $39 |
| Scale | 250,000 | $99 |
| Business | 1,000,000 | $249 |
Yearly billing provides two months free, and every feature is available on every plan. Create a free ScreenshotNeo account to get 1,000 screenshots each month without a card.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

