File Upload and Download in Playwright
Learn reliable Playwright patterns for uploading files, handling dynamic file choosers, waiting for downloads, and saving artifacts for later assertions.
Playwright handles file transfers without simulating the operating system file picker. Upload an existing file with locator.setInputFiles(). If the file input appears only after a click, wait for the filechooser event before clicking and then call fileChooser.setFiles(). For downloads, register page.waitForEvent('download') before the action that starts the download, then call download.saveAs() before the browser context closes.
The examples below use Playwright Test and JavaScript. The same APIs are available in Playwright libraries for Python, Java and .NET.
Upload a file with setInputFiles
Use a locator for an <input type="file">. The input does not need to be visible, and Playwright does not require you to open a native file dialog.
import { test, expect } from '@playwright/test';
test('uploads a profile image', async ({ page }) => {
await page.goto('https://example.com/profile');
await page.locator('input[type="file"]').setInputFiles('fixtures/avatar.png');
await page.getByRole('button', { name: 'Upload' }).click();
await expect(page.getByText('Upload complete')).toBeVisible();
});
Relative paths resolve from the current working directory. Use an absolute path when your test runner changes directories or when the fixture is generated elsewhere.
Choosing the right locator
Prefer a stable label, test id, or accessible relationship over a broad CSS selector.
await page.getByLabel('Resume').setInputFiles('/tmp/candidate.pdf');
await page.getByTestId('invoice-upload').setInputFiles('fixtures/invoice.pdf');
If the page has several file inputs, scope the locator to the relevant form or component.
const form = page.getByRole('form', { name: 'Identity documents' });
await form.locator('input[type="file"]').setInputFiles([
'fixtures/passport.jpg',
'fixtures/proof-of-address.pdf'
]);
Upload multiple files
Pass an array of paths when the input has the multiple attribute. The order supplied to Playwright is the order exposed to the page.
await page.locator('input[type="file"]').setInputFiles([
'fixtures/photo-front.jpg',
'fixtures/photo-back.jpg'
]);
To clear the selected files, pass an empty array.
await page.locator('input[type="file"]').setInputFiles([]);
An input that does not allow multiple files may reject an array containing more than one path. Check the element’s multiple attribute and the application’s validation rules.
Upload an in-memory file
You do not need to create a fixture on disk. Supply an object with name, mimeType, and buffer. This is useful for generated CSV files, API responses, and small test documents.
import { test, expect } from '@playwright/test';
test('uploads generated CSV content', async ({ page }) => {
await page.goto('https://example.com/import');
const csv = Buffer.from('id,name\\n1,Ada\\n2,Grace\\n', 'utf8');
await page.getByLabel('CSV file').setInputFiles({
name: 'users.csv',
mimeType: 'text/csv',
buffer: csv
});
await page.getByRole('button', { name: 'Import' }).click();
await expect(page.getByText('2 records imported')).toBeVisible();
});
The filename and MIME type can affect server-side validation. Use values that match the kind of file your application expects, and include realistic content when validation inspects the bytes.
Handle a file chooser that appears after a click
Some interfaces create the file input only after a button is pressed. Start waiting for the chooser before the click so a fast event cannot be missed.
import { test, expect } from '@playwright/test';
test('uploads through a dynamic chooser', async ({ page }) => {
await page.goto('https://example.com/documents');
const chooserPromise = page.waitForEvent('filechooser');
await page.getByRole('button', { name: 'Choose file' }).click();
const fileChooser = await chooserPromise;
await fileChooser.setFiles('fixtures/contract.pdf');
await expect(page.getByText('contract.pdf')).toBeVisible();
});
The order matters: create the event promise, perform the action that opens the chooser, await the chooser, and set the files. The older page-level page.setInputFiles() API is discouraged; use a locator or the chooser API instead.
Dynamic chooser with multiple files
const chooserPromise = page.waitForEvent('filechooser');
await page.getByText('Add attachments').click();
const chooser = await chooserPromise;
await chooser.setFiles([
'fixtures/log.txt',
'fixtures/screenshot.png'
]);
Wait for and save a download
Register the download listener before clicking the link or button. Then save the resulting artifact to a known path.
import { test, expect } from '@playwright/test';
test('downloads a report', async ({ page }) => {
await page.goto('https://example.com/reports');
const downloadPromise = page.waitForEvent('download');
await page.getByRole('link', { name: 'Download report' }).click();
const download = await downloadPromise;
await download.saveAs('artifacts/report.csv');
expect(download.suggestedFilename()).toMatch(/report.*\\.csv$/);
});
Waiting after the click is unreliable because a quick download can finish before the listener is attached. Downloads initially live in temporary browser-context storage. Playwright removes them when the context closes, so call saveAs() before teardown if a later assertion, upload, or debugging step needs the file.
Inspect download metadata
const downloadPromise = page.waitForEvent('download');
await page.getByRole('button', { name: 'Export' }).click();
const download = await downloadPromise;
console.log(download.url());
console.log(download.suggestedFilename());
console.log(await download.failure());
await download.saveAs('artifacts/export.bin');
suggestedFilename() commonly comes from the response’s Content-Disposition header or the HTML download attribute. Browsers can compute different names, so assert a stable suffix or save to your own fixed filename instead of requiring an exact browser-generated name.
Read a download as a stream
Use createReadStream() when the test needs to process bytes without first choosing a destination.
const downloadPromise = page.waitForEvent('download');
await page.getByText('Export JSON').click();
const download = await downloadPromise;
const stream = await download.createReadStream();
let body = '';
for await (const chunk of stream) body += chunk.toString('utf8');
const data = JSON.parse(body);
expect(data.records).toBeDefined();
For a persistent artifact, saveAs() is usually simpler. The API reference notes that download.path() throws when the browser is connected remotely; copy the file with saveAs() instead.
Configure downloads in Playwright Test
Playwright Test’s acceptDownloads option controls whether downloads are accepted automatically. It is documented as true by default, but set it explicitly when a project has custom browser or context configuration.
// playwright.config.js
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
acceptDownloads: true
}
});
If your test uses a manually created context, pass the option there:
const context = await browser.newContext({ acceptDownloads: true });
const page = await context.newPage();
Combine uploads and downloads in one test
import { test, expect } from '@playwright/test';
test('imports a file and downloads the result', async ({ page }) => {
await page.goto('https://example.com/data');
await page.getByLabel('Input CSV').setInputFiles('fixtures/input.csv');
await page.getByRole('button', { name: 'Process' }).click();
await expect(page.getByText('Processing complete')).toBeVisible();
const downloadPromise = page.waitForEvent('download');
await page.getByRole('link', { name: 'Download processed CSV' }).click();
const download = await downloadPromise;
await download.saveAs('artifacts/processed.csv');
});
Reliability checklist
- Use
locator.setInputFiles()for an input already present in the DOM. - Use
page.waitForEvent('filechooser')before clicking a control that opens a dynamic picker. - Attach
page.waitForEvent('download')before the download-triggering action. - Call
download.saveAs()before the browser context closes. - Use stable labels or test ids instead of brittle positional selectors.
- Keep fixture files in a predictable directory and resolve paths consistently in CI.
- Assert application state after upload rather than assuming that selecting a file completed the server-side upload.
- Do not require an identical suggested filename across browsers.
Troubleshooting uploads
“File input is not found”
Cause: the input is rendered after navigation, inside a component, or only after a button click.
Fix: wait for the relevant UI state, scope the locator to the correct form, or use the file chooser pattern. Inspect the DOM with Playwright’s trace viewer if the selector is wrong.
“Element is not an input type=file”
Cause: the locator matched a custom button or another input.
Fix: target the real input[type="file"], or wait for filechooser when the page hides the input behind a custom control.
“File does not exist” in CI
Cause: the relative path is resolved from a different working directory, or the fixture was not copied into the CI workspace.
Fix: use a path based on the test file or repository root, verify the fixture is checked in, and log the resolved path during diagnosis.
The application rejects the file
Cause: the extension, MIME type, size, or file contents fail application validation.
Fix: use a realistic fixture, set the correct in-memory mimeType, and assert the validation message so the failure explains the rejected condition.
Troubleshooting downloads
The test times out waiting for a download
Cause: the click did not trigger a download, the listener was attached to the wrong page, or the application opened a new tab.
Fix: confirm the control’s behavior, attach the listener before clicking, and wait for a popup or new page when the action opens one. If the response is rendered inline, assert the new page or response instead of waiting for a download event.
The file disappears after the test
Cause: it remained in temporary context storage.
Fix: call saveAs() to a directory retained by your CI artifacts before the test or context closes.
The filename assertion is flaky
Cause: browsers may derive suggested names differently from headers or the download attribute.
Fix: save to a deterministic name and assert the extension, file contents, or a stable substring.
download.path() fails on a remote browser
Cause: the path is local to the browser process and is unavailable through a remote connection.
Fix: use download.saveAs() or createReadStream() and handle the bytes from the test process.
Performance, parallelism, and cost
Uploads and downloads are usually dominated by application and network time, not the Playwright method call. Keep fixtures small for unit-level tests, reuse authenticated state where appropriate, and avoid uploading the same large file in every parallel worker. For a download, save only when later steps need a persistent artifact; streaming can reduce temporary file work.
Parallel tests must write to unique artifact paths. Include the worker index, test title, or a generated identifier in the destination filename to prevent concurrent tests from overwriting one another. Clean retained artifacts after the run or configure CI retention deliberately.
Retries can repeat a real upload or create duplicate server-side records. Make test data idempotent, use unique request identifiers when the application supports them, and verify that a retry has not left an unwanted record behind.
Or skip the browser setup
If your goal is a screenshot of a page after an upload or download workflow, ScreenshotNeo can capture the final URL through one API request. It is a separate capture service, so it does not replace Playwright when you need to interact with a file picker or verify downloaded bytes.
See the ScreenshotNeo API documentation for the available options and response headers.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Before capture, ScreenshotNeo accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. Its MCP server lets Claude, Cursor, and other MCP clients use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots each month with no card, and paid plans start at $5 for 3,000 shots.
Create a free ScreenshotNeo account.
FAQ
Can Playwright upload a directory?
Yes. The file input API accepts a directory path when the page supports directory selection. Confirm that the input is configured for directory uploads and that the browser environment exposes the expected files.
Can I clear a file input without reloading?
Yes. Call setInputFiles([]) on the locator or file chooser.
Should I wait for a network response after selecting a file?
Only if selection starts an upload request. Selecting a file and uploading it are separate application steps, so wait for the application’s completion signal or the specific response your workflow requires.
Why does a download event not fire for a PDF opened in the browser?
The server may be displaying the PDF inline instead of downloading it. Treat it as navigation or a new page unless the response includes download behavior.
Where should CI keep downloaded files?
Save them under the CI system’s artifact directory with unique names, then upload that directory only when the test or debugging workflow needs the files.


