PRACTICAL GUIDE / playwright validate downloaded file
Validate Download Names, Content, and Failures in Playwright
Capture Playwright downloads without races, inspect suggested names and bytes, verify business content, handle failures, and keep parallel tests isolated.
In this guide12 sections
- Follow the Download Lifecycle
- Capture the Event Before Export Starts
- Treat the Suggested Filename as Product Data
- Read Bytes Through the Right Interface
- Validate Structure Before Individual Values
- Correlate the Response With the Browser Artifact
- Distinguish Download Failure From Export Failure
- Isolate Outputs Across Workers
- Diagnose Failures From Event to Parser
- Make Abstractions Format Aware
- Run the Export Verification Checklist
- Deliver Confidence in the Artifact
What you will learn
- Follow the Download Lifecycle
- Capture the Event Before Export Starts
- Treat the Suggested Filename as Product Data
- Read Bytes Through the Right Interface
A download assertion is complete only when it proves the right artifact arrived and the artifact says the right thing. Waiting for a download event confirms that the browser began handling an attachment. It does not confirm a trustworthy name, successful transfer, valid file structure, correct report filters, or usable content.
Design the test from the business claim backward. An invoice export should contain the requested invoice number and total. A CSV should have the required schema and records for the selected range. A failure scenario should expose a useful product response rather than leave an empty or corrupt file.
Follow the Download Lifecycle
The official Playwright downloads guide recommends starting the event wait before the action and then saving the captured artifact. Keep those stages separate: register observation, trigger export, resolve the Download, inspect transport outcome, and validate the file.
Temporary names are an implementation detail. The Download API reports a browser-computed suggested filename and can copy the result to a path owned by the test. Build assertions on public naming and content contracts.
Animated field map
Download Validation Flow
The browser event is captured before export, then the completed artifact is identified, inspected, and cleaned up.
01 / register download
Register promise
Arm the page download event before the export action can emit it.
02 / trigger export
Trigger export
Perform the user action with the exact filters under test.
03 / resolve artifact
Resolve artifact
Await completion or capture a concrete transfer failure from Download.
04 / inspect file
Inspect name and bytes
Validate filename policy, format signature, structure, and business data.
05 / cleanup output
Cleanup assertion
Retain failed evidence as needed and isolate successful test output.
Capture the Event Before Export Starts
The reliable ordering is promise, action, result. Do not await the event before clicking, and do not add the listener after the click. The first blocks the trigger; the second can miss a fast response.
import { test, expect } from '@playwright/test'
test('downloads the selected invoice PDF', async ({ page }, testInfo) => {
await page.goto('/billing/invoices/INV-2084')
const downloadPromise = page.waitForEvent('download')
await page.getByRole('button', { name: 'Download PDF' }).click()
const download = await downloadPromise
expect(await download.failure()).toBeNull()
expect(download.suggestedFilename()).toBe('invoice-INV-2084.pdf')
const output = testInfo.outputPath('invoice-INV-2084.pdf')
await download.saveAs(output)
})Keep the export trigger visible in the test. A helper that registers an event around an unknown callback may be reusable, but it can also obscure which filter or button produced the artifact.
Treat the Suggested Filename as Product Data
suggestedFilename() is typically derived by the browser from Content-Disposition or the HTML download attribute. The underlying temporary file may have a random identifier, so asserting its path basename tests the wrong layer.
Define naming rules with product owners: required extension, stable record identifier, date formatting, sanitized user text, and behavior for duplicate exports. Avoid asserting a full timestamp unless the application contract controls timezone and precision. If browsers legitimately compute a name differently, place that expectation in browser-specific project data rather than weakening every test to endsWith('.pdf').
Also test hostile or awkward source names. A report title containing slashes, reserved characters, or non-ASCII text should produce a safe filename without changing the actual report identity. Keep test input ASCII here when the repository requires it, but include international filename coverage in an appropriate Unicode-capable suite.
Read Bytes Through the Right Interface
Use saveAs when a parser expects a file path or when the failed-test artifact should be retained. Use createReadStream when bytes can be processed incrementally and a disk copy adds no value. Both require a successful download; check failure() when diagnosing transfer behavior.
import { Buffer } from 'node:buffer'
async function readDownload(download: import('@playwright/test').Download) {
const stream = await download.createReadStream()
const chunks: Buffer[] = []
for await (const chunk of stream) {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
}
return Buffer.concat(chunks)
}
test('CSV export contains the active account', async ({ page }) => {
await page.goto('/admin/accounts?status=active')
const downloadPromise = page.waitForEvent('download')
await page.getByRole('button', { name: 'Export CSV' }).click()
const download = await downloadPromise
const csv = (await readDownload(download)).toString('utf8')
expect(csv).toContain('account_id,status')
expect(csv).toContain('ACC-104,active')
expect(csv).not.toContain('ACC-109,suspended')
})For large files, stream into a parser or save to disk rather than accumulating every byte. The test's memory profile should not become the limiting factor in a parallel CI worker.
Validate Structure Before Individual Values
Start with a lightweight file identity check: PDF magic bytes, expected CSV headers, JSON parseability, or ZIP entries. Then validate business content. Searching arbitrary binary output for text is fragile because PDF and office formats may compress or encode content.
Use a format-aware parser already approved in the project. For CSV, account for quoted delimiters, embedded newlines, byte-order marks, and line endings. For spreadsheets, read cells and types rather than treating the workbook as a string. For PDFs, extract text and page metadata, then choose a stable assertion such as invoice number, total, and page count.
The export test should verify its filters. Seed one included record and one deliberately excluded record, request the date or status range, and assert both inclusion and exclusion. This catches stale caches and ignored query parameters that a header-only assertion misses.
Correlate the Response With the Browser Artifact
When the export endpoint responds directly with an attachment, capture both the matching response and download around the same click. The response provides status, content type, cache headers, and a request URL; the Download provides browser completion, suggested name, and bytes. Correlate them by the known endpoint or export identifier rather than assuming the next response belongs to the next download.
Background exports often split this into a job request and a later artifact request. In that design, assert job creation, wait on a bounded completed state, and then capture the download from the result link. This identifies whether time was spent generating or transferring the report and prevents one oversized timeout from covering both stages.
Distinguish Download Failure From Export Failure
download.failure() waits for transfer completion and returns an error string when the browser download fails. That covers cancellation and transport failures after a download begins. It does not cover every product error.
An export endpoint may respond with JSON 403, render an error toast, or return an HTML error page without a download event. For those scenarios, wait for the relevant response or visible status around the click instead of waiting only for download. Model the expected event surface separately.
test('reports an expired export permission', async ({ page }) => {
await page.goto('/reports/financial')
const responsePromise = page.waitForResponse(
response => response.url().endsWith('/api/reports/financial/export')
)
await page.getByRole('button', { name: 'Export report' }).click()
const response = await responsePromise
expect(response.status()).toBe(403)
await expect(page.getByRole('alert')).toHaveText('Your export permission has expired')
})Do not race a download wait and an error wait without cancellation and clear ownership; the losing promise can remain pending and produce confusing later output.
Isolate Outputs Across Workers
Use testInfo.outputPath() or another per-test directory. A shared downloads/report.csv path allows parallel tests to overwrite one another and lets a failed export accidentally read a previous run's file. Generate the destination from controlled identifiers, not raw user text.
Browser-context teardown removes its temporary downloads. Save artifacts you need before teardown, and decide whether successful files should persist. Keeping every large success artifact can inflate CI storage, while deleting all failed evidence makes diagnosis slower. A practical policy retains the trace, filename, and parsed failure summary by default, with the full file attached when content is safe and size is bounded.
Diagnose Failures From Event to Parser
If the event never arrives, inspect click actionability, the network response, permission state, and whether the product changed to an inline preview. If failure() returns a string, capture the download URL and browser logs, then check proxy, disk, and server transfer behavior.
If the filename is wrong, inspect response headers and the download attribute. If parsing fails, record the first bytes, content type, size, and response status; many supposed PDFs are HTML sign-in pages. If content is stale, compare request parameters and server-side export job inputs before blaming the parser.
Avoid solving intermittent exports with larger blanket timeouts. Determine whether the wait is for event creation, transfer completion, background report generation, or parsing, and assign evidence and a budget to that stage.
Make Abstractions Format Aware
A small capture helper can enforce event ordering, but format validators should remain separate. That lets the same capture path feed PDF, CSV, JSON, or archive-specific checks.
import type { Locator, Download } from '@playwright/test'
async function captureDownload(trigger: Locator): Promise<Download> {
const page = trigger.page()
const downloadPromise = page.waitForEvent('download')
await trigger.click()
return downloadPromise
}The tradeoff is visibility: a helper removes repeated coordination but can hide event timing from newer maintainers. Give it a precise name, return the raw Download, and avoid embedding generic content assertions inside it.
Run the Export Verification Checklist
- Register the download event before the user action.
- Check
failure()before treating the artifact as successful. - Assert the suggested name against a defined naming policy.
- Save into a path unique to the test when a disk parser is needed.
- Verify format structure before checking business values.
- Assert an included and excluded record for filtered exports.
- Model HTTP or UI export errors that do not emit downloads.
- Retain safe diagnostic evidence and avoid cross-worker output folders.
Deliver Confidence in the Artifact
Start with a causal event capture, but do not stop there. Prove the browser completed the transfer, the public filename is correct, the bytes match the advertised format, and the exported data reflects the user's selection. Separate transport failures from server-side export errors and isolate every saved file. A download test built this way verifies the report customers receive, not merely the click that requested it.
// LIVE COURSE / THE TESTING ACADEMY
Playwright Automation Mastery
Go beyond Selenium. Master Playwright with JS/TS in 90 days.
From the instructor behind this guide.
Playwright jobs are growing 8x faster than Selenium. 90 days / 75+ live hrs / Tue-Thu-Sat 7 AM IST.
PRIMARY REFERENCES
Verify the details at the source
QABattle guides are practical explanations. Product behavior, standards, and APIs can change, so use these primary references for the canonical details.
- 01Playwright documentation
Microsoft
Canonical API, locator, fixture, browser, and test-runner behavior.
- 02Playwright best practices
Microsoft
Official guidance for resilient tests, isolation, and user-facing locators.
- 03
FAQ / QUICK ANSWERS
Questions testers ask
How do I wait for a download in Playwright?
Create page.waitForEvent('download') before clicking the export control, perform the click, and then await the saved promise. This associates the Download object with the action without missing a fast browser event.
How can I verify the downloaded filename?
Use download.suggestedFilename(), which reflects the browser's result from response headers or the download attribute. Assert the meaningful naming rule, while allowing for documented browser-specific behavior when your matrix requires it.
Can Playwright read a download without saving it under a custom name?
Yes. For a successful download, createReadStream provides a readable stream. Reading a saved file is often simpler for parsers that expect a path, and saveAs can copy the artifact to a test-owned location.
How do I assert that a Playwright download failed?
Await download.failure(); it resolves to null for success or an error string for failure. Pair that transport result with the product's visible error contract because some export failures return a normal HTML or JSON response and never emit a download.
Where should downloaded files go in parallel tests?
Use Playwright's test output path or another directory unique to the test, then save with a controlled filename. Avoid one shared downloads folder because workers can overwrite or read one another's artifacts.
RELATED GUIDES
Continue the learning route
GUIDE 01
How to Automate File Download Testing
How to automate file download testing with Playwright and Selenium: wait for files, verify names, types, size, content checks, and common pitfalls.
GUIDE 02
Advanced File Uploads in Playwright: Buffers, Directories, and Choosers
Test Playwright uploads from memory, multiple files, directories, and file choosers while validating server processing, metadata, and failure behavior.
GUIDE 03
18 Playwright File, Dialog, and Browser Event Interview Scenarios
Practice 18 senior Playwright event scenarios covering uploads, downloads, dialogs, promise ordering, artifact validation, listeners, and race diagnosis.
GUIDE 04
Playwright API Testing: Validate APIs Inside Your E2E Suite
Playwright API testing guide covering request contexts, setup flows, assertions, authentication, fixtures, UI strategy, and CI-ready checks.