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.

By The Testing AcademyUpdated July 11, 20269 min read
All field guides
In this guide12 sections
  1. Follow the Download Lifecycle
  2. Capture the Event Before Export Starts
  3. Treat the Suggested Filename as Product Data
  4. Read Bytes Through the Right Interface
  5. Validate Structure Before Individual Values
  6. Correlate the Response With the Browser Artifact
  7. Distinguish Download Failure From Export Failure
  8. Isolate Outputs Across Workers
  9. Diagnose Failures From Event to Parser
  10. Make Abstractions Format Aware
  11. Run the Export Verification Checklist
  12. 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.

  1. 01 / register download

    Register promise

    Arm the page download event before the export action can emit it.

  2. 02 / trigger export

    Trigger export

    Perform the user action with the exact filters under test.

  3. 03 / resolve artifact

    Resolve artifact

    Await completion or capture a concrete transfer failure from Download.

  4. 04 / inspect file

    Inspect name and bytes

    Validate filename policy, format signature, structure, and business data.

  5. 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.

TypeScript
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.

TypeScript
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.

TypeScript
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.

TypeScript
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.

Code PROMODE / 10% offJoin the batch

The Testing Academy editorial desk

Practical QA guidance built around test evidence, production tradeoffs, and interview-ready explanations.

Published July 11, 2026 / Reviewed July 11, 2026

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.

  1. 01
    Playwright documentation

    Microsoft

    Canonical API, locator, fixture, browser, and test-runner behavior.

  2. 02
    Playwright best practices

    Microsoft

    Official guidance for resilient tests, isolation, and user-facing locators.

  3. 03
    WebDriver standard

    W3C

    The browser automation protocol specification used by major automation stacks.

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.