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.

Advertisement

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.