Q20 of 42 · Playwright
How do you handle file downloads in Playwright?
Short answer
Short answer: Listen for the `download` event with `page.waitForEvent('download')`, then save with `download.saveAs(path)` or read the path via `download.path()`. Pair with `Promise.all` plus the click that triggers the download. Validate the saved file's contents in test.
Detail
Playwright treats downloads as first-class events. The pattern:
const [download] = await Promise.all([
page.waitForEvent('download'),
page.getByRole('button', { name: 'Export CSV' }).click(),
]);
// Get the path to the downloaded file (Playwright manages temp storage)
const path = await download.path();
console.log(`Downloaded ${download.suggestedFilename()} to ${path}`);
// Or save to a specific location
await download.saveAs('test-results/export.csv');
Why Promise.all: same race issue as waitForResponse — if you set up the listener after the click, the download event may have already fired.
Validating the contents:
import fs from 'node:fs/promises';
const path = await download.path();
const content = await fs.readFile(path!, 'utf8');
expect(content).toContain('order_id,customer,total');
For binary files (PDF, ZIP), read as Buffer and check size or magic bytes:
const buf = await fs.readFile(path!);
expect(buf.length).toBeGreaterThan(1000);
expect(buf.slice(0, 4).toString('hex')).toBe('25504446'); // %PDF
Triggering a download via API instead (more deterministic for content tests):
const response = await page.request.get('/api/exports/orders.csv');
expect(response.status()).toBe(200);
expect(await response.text()).toContain('order_id');
This bypasses the click flow entirely and is faster + more reliable when you only need to validate the content. Use the click-flow when you want to verify the trigger UI works.
Cleanup: Playwright stores downloaded files in a temp directory. download.delete() removes them; the test runner cleans up at the end of the run anyway.
// EXAMPLE
downloads.spec.ts
import { test, expect } from '@playwright/test';
import fs from 'node:fs/promises';
test('exports orders as CSV', async ({ page }) => {
await page.goto('/orders');
const [download] = await Promise.all([
page.waitForEvent('download'),
page.getByRole('button', { name: 'Export CSV' }).click(),
]);
const path = await download.path();
expect(path).toBeTruthy();
const content = await fs.readFile(path!, 'utf8');
const lines = content.split('\n');
expect(lines[0]).toBe('order_id,customer,total,created_at');
expect(lines.length).toBeGreaterThan(1);
});