Playwright never opens the operating system's file dialog. Instead, it sets files directly on the page's <input type="file"> and captures downloads as objects you can save and inspect. That makes file tests fast and reliable in headless mode and CI — once you know which API to use.
Upload Files with setInputFiles
If the page has a file input (even a hidden one), call setInputFiles on it:
import path from 'path';
// one file
await page.getByLabel('Upload file').setInputFiles(path.join(__dirname, 'myfile.pdf'));
// several files (the input needs the "multiple" attribute)
await page.getByLabel('Upload files').setInputFiles([
path.join(__dirname, 'file1.txt'),
path.join(__dirname, 'file2.txt'),
]);
// a whole directory (input with "webkitdirectory")
await page.getByLabel('Upload directory').setInputFiles(path.join(__dirname, 'mydir'));
// clear the selection
await page.getByLabel('Upload file').setInputFiles([]);
Upload a file built in memory
No fixture file needed — pass a buffer with a name and MIME type. Great for generating CSVs or JSON on the fly:
await page.getByLabel('Upload file').setInputFiles({
name: 'file.txt',
mimeType: 'text/plain',
buffer: Buffer.from('this is test'),
});
Hidden input? Many styled upload buttons hide the real <input>. Target it with page.locator('input[type=file]') — setInputFiles works even when it is not visible.
Upload When There Is No Input: the filechooser Event
Some apps create the file input dynamically when you click a button. Wait for the filechooser event, then set files on it:
const fileChooserPromise = page.waitForEvent('filechooser');
await page.getByRole('button', { name: 'Upload file' }).click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(path.join(__dirname, 'myfile.pdf'));
Note the order: start waiting before the click, then await the promise. This pattern is the same for downloads, popups and other events.
Drag-and-Drop Uploads
Most drop zones still contain a hidden file input — try setInputFiles on it first. If the component only listens for drop events, create a DataTransfer in the page and dispatch the event:
const dataTransfer = await page.evaluateHandle((content) => {
const dt = new DataTransfer();
dt.items.add(new File([content], 'notes.txt', { type: 'text/plain' }));
return dt;
}, 'file contents');
await page.locator('#dropzone').dispatchEvent('drop', { dataTransfer });
Download Files with waitForEvent('download')
const downloadPromise = page.waitForEvent('download');
await page.getByText('Download file').click();
const download = await downloadPromise;
// keep the file: save it with the name the server suggested
await download.saveAs(test.info().outputPath(download.suggestedFilename()));
download.suggestedFilename()— the file name from the server or thedownloadattribute.download.path()— path of the temporary file Playwright saved.download.saveAs(path)— copy it somewhere permanent.
Downloaded files are deleted when the browser context that produced them closes. Read or saveAs the file inside the test. Saving to test.info().outputPath() keeps it with the test results.
Download a File Straight from a URL
When you only need to check the file — not the click flow — skip the browser and use the request fixture. It shares cookies with the page context when you use page.request:
import { test, expect } from '@playwright/test';
import fs from 'fs';
test('report CSV is served', async ({ page }) => {
const response = await page.request.get('/reports/monthly.csv');
expect(response.ok()).toBeTruthy();
expect(response.headers()['content-type']).toContain('text/csv');
const body = await response.body();
fs.writeFileSync(test.info().outputPath('monthly.csv'), body);
});
More on the request API in our Playwright API testing guide.
Verify the Downloaded File's Contents
A download that “happened” is not the same as a correct download. Read the file and assert on it:
import fs from 'fs';
const downloadPromise = page.waitForEvent('download');
await page.getByRole('button', { name: 'Export CSV' }).click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toMatch(/^orders-\d{4}-\d{2}-\d{2}\.csv$/);
const content = fs.readFileSync(await download.path(), 'utf-8');
const rows = content.trim().split('\n');
expect(rows[0]).toBe('id,customer,total');
expect(rows.length).toBeGreaterThan(1);
For PDFs or spreadsheets, parse the file with a library such as pdf-parse or xlsx and assert on the parsed data. Our assertions guide covers the matchers.
Playwright Python and Java Examples
Python
from playwright.sync_api import Page
def test_upload(page: Page):
page.get_by_label("Upload file").set_input_files("myfile.pdf")
def test_upload_filechooser(page: Page):
with page.expect_file_chooser() as fc_info:
page.get_by_role("button", name="Upload file").click()
fc_info.value.set_files("myfile.pdf")
def test_download(page: Page):
with page.expect_download() as download_info:
page.get_by_text("Download file").click()
download = download_info.value
download.save_as("downloads/" + download.suggested_filename)
Java
import java.nio.file.Paths;
// upload
page.getByLabel("Upload file").setInputFiles(Paths.get("myfile.pdf"));
// filechooser
FileChooser fileChooser = page.waitForFileChooser(() -> {
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Upload file")).click();
});
fileChooser.setFiles(Paths.get("myfile.pdf"));
// download
Download download = page.waitForDownload(() -> {
page.getByText("Download file").click();
});
download.saveAs(Paths.get("downloads", download.suggestedFilename()));
Full setups: Python tutorial · Java tutorial.
Troubleshooting File Upload and Download
| Problem | Fix |
|---|---|
Error: Non-multiple file input can only accept single file | The input has no multiple attribute — pass one file, or upload in separate steps. |
ENOENT on upload | Relative paths resolve from the working directory. Build paths with path.join(__dirname, ...). |
waitForEvent('download') times out | The click opened the file in a new tab instead of downloading it (common for PDFs). Wait for popup instead, or fetch the URL with page.request. |
| Download file is missing after the test | Files are deleted when the context closes. Call saveAs during the test. |
| Upload works locally but not in CI | Fixture files are missing from the repo or Docker image, or paths are OS-specific — use path.join. |
| Native OS dialog appears in headed mode | You clicked a button without handling filechooser. Start waiting for the event before the click. |
Frequently Asked Questions
Where does Playwright save downloaded files?
In a temporary folder (configurable with downloadsPath when launching the browser). The files are deleted when the browser context closes, so call download.saveAs() to keep them.
How do I upload a file without a file on disk?
Pass an object to setInputFiles with name, mimeType and buffer, for example Buffer.from('id,total\n1,10').
How do I handle a native file upload dialog in Playwright?
Playwright never shows the native dialog. Use setInputFiles on the file input, or wait for the filechooser event before clicking and call fileChooser.setFiles().
Can Playwright upload multiple files at once?
Yes. Pass an array of paths to setInputFiles. The input must have the multiple attribute.
How do I check the contents of a downloaded file?
Read it with fs.readFileSync(await download.path()) and assert on the text, or parse it with a CSV, PDF or Excel library first.
Asim Noaman
Senior QA Automation Engineer & AI Testing Specialist
With years of hands-on experience building test automation frameworks for production applications, Asim specializes in combining traditional QA methodologies with cutting-edge AI tools. He has helped teams adopt Playwright and AI-driven testing workflows to ship faster with fewer bugs.
Complete Course
Master Real-World Playwright Automation
Uploads, downloads, APIs and tricky UI flows are covered hands-on in the complete Udemy course — along with AI-assisted test writing using Claude and MCP Server.
- Real-world UI scenarios
- API + UI testing together
- Stable locators and assertions
- AI test generation with Claude