File upload and download are two places where browser automation used to need workarounds. In Playwright both are first-class: setInputFiles() uploads without touching the OS dialog, and the download event gives you the file to save and verify.
File Upload & Download
Time: ~20 minutes. You will upload a file to a form and capture a download, then verify the downloaded file’s actual contents. Uses the public practice site the-internet.herokuapp.com.
Goal
One passing upload test and one passing download test that checks the file’s real content.
Part A — Upload
Step 1 — Create a small file to upload
In your project root, create a folder and a file:
mkdir -p data
echo "This is my test upload." > data/sample.txt
✅ You now have data/sample.txt.
Step 2 — Create the upload test
Create tests/files.spec.ts:
import { test, expect } from '@playwright/test';import path from 'path';test('upload a file and confirm the site received it', async ({ page }) => { await page.goto('https://the-internet.herokuapp.com/upload'); const filePath = path.join(__dirname, '..', 'data', 'sample.txt'); await page.locator('#file-upload').setInputFiles(filePath); // attach the file await page.locator('#file-submit').click(); // submit await expect(page.getByText('File Uploaded!')).toBeVisible(); await expect(page.getByText('sample.txt')).toBeVisible(); // filename shown back});
Step 3 — Run it
npx playwright test files -g "upload a file"
✅ 1 passed. Note two key ideas: - setInputFiles(path) attaches the file directly to the input — you never fight the OS file dialog. - path.join(__dirname, ...) builds an absolute path, so it works locally and in CI.
Step 4 — Watch it (optional)
npx playwright test files -g "upload a file" --headed
✅ You see the file chosen and the “File Uploaded!” confirmation.
Part B — Download
Step 5 — Add a download test
Add to tests/files.spec.ts:
import fs from 'fs';test('download a file and verify its contents', async ({ page }) => { await page.goto('https://the-internet.herokuapp.com/download'); // Start listening for the download BEFORE clicking the link: const [download] = await Promise.all([ page.waitForEvent('download'), page.locator('a', { hasText: '.txt' }).first().click(), ]); // Check the suggested filename const name = download.suggestedFilename(); expect(name).toMatch(/\.txt$/); // Save it and read the real bytes const savePath = path.join(__dirname, '..', 'data', 'downloaded-' + name); await download.saveAs(savePath); const contents = fs.readFileSync(savePath, 'utf-8'); expect(contents.length).toBeGreaterThan(0); // file is not empty console.log(`Downloaded ${name} (${contents.length} bytes)`);});
Step 6 — Run it
npx playwright test files -g "download a file"
✅ 1 passed, and the console prints the filename and size. The critical pattern: - Register the wait first, then click — Promise.all([waitForEvent('download'), click]) avoids a race. - saveAs + fs.readFileSync let you assert the real content, not just “a file appeared.”
Step 7 — Why content checks matter
Downloads (CSV exports, invoices, reports) are features users depend on. A backend change could silently drop a column or produce an empty file. Asserting the content (a header row, a byte count, an expected value) catches that before customers do.
Step 8 — Clean up (optional)
rm -f data/downloaded-*.txt
What you just learned
- Upload: setInputFiles(path) with an absolute path.join path.
- Download: pair waitForEvent('download') with the click, then saveAs + read to verify real content.
- Why content-level assertions on downloads catch silent data bugs.
Next
Walkthrough 14 adds visual regression testing with screenshot comparisons.
File Upload
Level: L3.
What is it?
Automating <input type="file"> uploads (single/multiple) and custom upload widgets.
Why do we need it?
Profile pictures, document submissions, bulk imports — uploads are common and must be tested, including validation (type/size).
How does it work?
setInputFiles() sets files on a file input directly (no OS dialog needed). For custom buttons that open a chooser, capture the filechooser event.
Syntax
await page.getByLabel('Upload').setInputFiles('data/resume.pdf');await page.getByLabel('Upload').setInputFiles(['a.png', 'b.png']); // multipleawait page.getByLabel('Upload').setInputFiles([]); // clear
Custom chooser:
const [chooser] = await Promise.all([ page.waitForEvent('filechooser'), page.getByRole('button', { name: 'Attach' }).click(),]);await chooser.setFiles('data/resume.pdf');
Basic Example
await page.setInputFiles('input[type=file]', 'data/photo.png');
Practical Example — upload + verify + validate type
import path from 'path';test('upload resume', async ({ page }) => { await page.goto('/apply'); const file = path.join(__dirname, '../data/resume.pdf'); await page.getByLabel('Resume').setInputFiles(file); await expect(page.getByText('resume.pdf')).toBeVisible(); // filename shown await page.getByRole('button', { name: 'Submit' }).click(); await expect(page.getByText('Application received')).toBeVisible();});
Line-by-Line Explanation
- Build an absolute path with path.join (robust across OS/working dirs).
- setInputFiles attaches it directly — no native dialog to fight.
- Assert the filename appears (UI acknowledged it), submit, assert success.
Common Mistakes
- Trying to automate the OS file dialog instead of setInputFiles.
- Relative paths that break in CI (use path.join(__dirname, ...)).
- Not testing invalid type/oversize rejection.
Best Practices
- Prefer setInputFiles on the input; use filechooser only for custom triggers.
- Keep small fixture files in data/.
- Test both valid uploads and validation errors.
Interview Questions
- Q: How to upload a file? A: locator.setInputFiles(path) on the file input.
- Q: Custom upload button (no visible input)? A: Capture filechooser and setFiles.
- Q: How to upload multiple files? A: Pass an array to setInputFiles.
Practice Exercise
Automate a single upload (assert filename + success) and a validation case (wrong type → error). Use path.join for paths.
Real-World Scenario
A job-application flow accepts PDFs only. Tests upload a valid PDF (success) and a .exe (rejection), guarding a security-relevant validation on every build.
File Download
Level: L3.
What is it?
Capturing and verifying downloads triggered by the app (PDF invoice, CSV export).
Why do we need it?
Exports/reports are features users depend on. Tests must confirm the download happens and, ideally, that its name/content is correct.
How does it work?
Wait for the download event alongside the trigger. The Download object exposes suggestedFilename() and saveAs(path); you can then read the saved file.
Syntax
const [download] = await Promise.all([ page.waitForEvent('download'), page.getByRole('button', { name: 'Export CSV' }).click(),]);expect(download.suggestedFilename()).toBe('report.csv');await download.saveAs('downloads/report.csv');
Basic Example
const downloadPromise = page.waitForEvent('download');await page.getByText('Download PDF').click();const download = await downloadPromise;
Practical Example — download + name + content
import fs from 'fs';test('export invoice CSV', async ({ page }) => { await page.goto('/invoices'); const [download] = await Promise.all([ page.waitForEvent('download'), page.getByRole('button', { name: 'Export' }).click(), ]); expect(download.suggestedFilename()).toMatch(/invoices.*\.csv/); const p = await download.path(); // temp path const content = fs.readFileSync(p!, 'utf-8'); expect(content).toContain('Invoice ID,Amount'); // header row expect(content.split('\n').length).toBeGreaterThan(1);});
Line-by-Line Explanation
- Pair the download wait with the click.
- suggestedFilename() verifies naming.
- download.path() gives the temp file; read and assert the header and that rows exist — real content validation, not just “a file appeared.”
Common Mistakes
- Clicking before registering the download wait.
- Only checking a file downloaded, never its content/name.
- Relying on the OS Downloads folder (use saveAs/path()).
Best Practices
- Pair trigger + waitForEvent('download').
- Validate filename and content for critical exports.
- Use download.path()/saveAs rather than assuming a system folder.
Interview Questions
- Q: How to handle a download? A: waitForEvent('download') with the trigger, then suggestedFilename/saveAs/path.
- Q: How to verify content? A: download.path() then read/parse the file and assert.
- Q: Where do downloads go by default? A: A temp location; use saveAs to control it.
Practice Exercise
Automate a CSV export: assert the filename pattern and that the header row and at least one data row exist.
Real-World Scenario
A finance export silently dropped a column after a backend change. A content-level download assertion (checking the header row) caught it before customers reconciled bad reports.
FAQs
How do you upload a file in Playwright?
Call locator.setInputFiles(path) on the file input; pass an array for multiple files or an empty array to clear. For custom upload buttons, wait for the filechooser event and call setFiles.
How do you verify a downloaded file?
Start waiting for the download event before clicking, save it with download.saveAs() or read its path, then check the file name, size and contents.