ScreenshotNeo

BlogHow-to

Playwright Input Type File: Upload Files in Tests

Learn how to upload, replace, clear, and verify files in Playwright with JavaScript and Python, including dynamic file choosers and API uploads.

By the ScreenshotNeo team1 October 20268 min read

Use Playwright’s locator API to upload a file into an HTML <input type="file">:

import path from 'node:path';

await page.getByLabel('Upload file').setInputFiles(
  path.join(__dirname, 'fixtures', 'report.pdf')
);

setInputFiles() accepts one path, multiple paths, a directory for a directory input, an in-memory file payload, or [] to clear the selection. For an input created only after a button click, wait for the file chooser before clicking it. The official Locator API describes this action as “Upload file or multiple files into <input type=file>.” Read the Locator API documentation.

What Playwright input type file means

An HTML file input is controlled by the browser. Playwright does not use fill() for it; it assigns files through setInputFiles() or through a FileChooser. Selecting a file only exercises the browser control. Your test must still assert the application’s result, such as a filename preview, validation message, network response, or saved record.

Prerequisites

  1. Install Playwright and its browsers in your project.
  2. Keep stable fixture files in a directory such as fixtures/.
  3. Give the upload control an accessible label when you own the page:
<label for="document">Upload file</label>
<input id="document" name="document" type="file">

A label-based locator is usually the clearest choice because it follows the name a user sees. If there is no label, use a precise, stable locator and confirm it identifies the intended input.

Upload one file

import { test, expect } from '@playwright/test';
import path from 'node:path';

test('uploads one PDF', async ({ page }) => {
  await page.goto('https://your-app.example/upload');

  const fileInput = page.getByLabel('Upload file');
  await fileInput.setInputFiles(
    path.join(__dirname, 'fixtures', 'report.pdf')
  );

  await expect(page.getByText('report.pdf')).toBeVisible();
});

Relative paths resolve from the current working directory. Test runners can use a different working directory than the directory containing the test file, so construct an explicit path when that distinction matters. In CommonJS projects, use path.join(__dirname, 'fixtures', 'report.pdf'); in ESM, define the equivalent directory from import.meta.url or use a project-root fixture path.

Upload multiple files

Pass an array when the input supports the multiple attribute:

await page.getByLabel('Upload files').setInputFiles([
  'fixtures/first.txt',
  'fixtures/second.txt',
]);

The browser control and application decide whether multiple files are accepted. If the input does not allow multiple selection, the page may retain one file or reject the action. Assert the behavior your application promises.

Clear the selected file

await page.getByLabel('Upload file').setInputFiles([]);

An empty array clears the current selection. Follow it with an assertion for the empty state, such as a removed filename, disabled submit button, or validation message.

Use an in-memory file

Fixtures are not required. Supply a payload with a name, MIME type, and bytes:

await page.getByLabel('Upload file').setInputFiles({
  name: 'note.txt',
  mimeType: 'text/plain',
  buffer: Buffer.from('example content'),
});

This is useful for generated content, boundary cases, and tests that should not depend on a file on disk. Set the MIME type and filename to the values your application validates.

Upload a directory

For an input using webkitdirectory, pass one directory path:

await page.locator('input[type="file"][webkitdirectory]')
  .setInputFiles('fixtures/project');

Only a single directory path is supported for this directory-input behavior. Verify that your application receives the expected relative file paths and handles empty or nested directories according to its own requirements.

Handle a dynamically created file input

Some interfaces create the input only after the user presses an “Attach” button. Register the event wait before the click so the event cannot be missed:

const chooserPromise = page.waitForEvent('filechooser');
await page.getByRole('button', { name: 'Attach file' }).click();
const chooser = await chooserPromise;
await chooser.setFiles('fixtures/report.pdf');

The ordering is deliberate: start waiting, click, then set the returned chooser’s files.

Python Playwright

Python uses snake_case for the same operation:

from pathlib import Path
from playwright.sync_api import Page, expect

def test_upload(page: Page) -> None:
    page.goto("https://your-app.example/upload")
    file_path = Path(__file__).parent / "fixtures" / "report.pdf"
    page.get_by_label("Upload file").set_input_files(str(file_path))
    expect(page.get_by_text("report.pdf")).to_be_visible()

Multiple files, clearing, directories, and in-memory payloads use the same concepts:

page.get_by_label("Upload files").set_input_files([
    "fixtures/first.txt",
    "fixtures/second.txt",
])
page.get_by_label("Upload file").set_input_files([])
page.get_by_label("Upload file").set_input_files({
    "name": "note.txt",
    "mimeType": "text/plain",
    "buffer": b"example content",
})

For a chooser in the synchronous Python API:

with page.expect_file_chooser() as chooser_info:
    page.get_by_role("button", name="Attach file").click()
chooser = chooser_info.value
chooser.set_files("fixtures/report.pdf")

See the Playwright Python input guide for the corresponding patterns.

JavaScript and TypeScript locator choices

Prefer locator.setInputFiles(). The older page-level page.setInputFiles(selector, files) API is discouraged in the JavaScript Page API; use a locator so the target and action are expressed together.

await page.locator('input[type="file"][name="avatar"]').setInputFiles(
  'fixtures/avatar.png'
);

If several inputs match, narrow the locator with a label, name, ID, role context, or a specific container. Avoid a broad selector that silently chooses the first matching input.

Verify the upload at the right layer

Browser UI and file selection

Use setInputFiles() or a file chooser when you need to test the browser interaction: previews, client-side validation, progress indicators, drag-and-drop substitutes, and submit behavior.

HTTP endpoint only

If the browser UI is irrelevant and you only need to test a multipart endpoint, use Playwright’s APIRequestContext. This bypasses the file picker and tests the HTTP layer instead. The APIRequestContext documentation covers multipart form fields and file-like payloads.

import { test, expect } from '@playwright/test';
import fs from 'node:fs';

test('uploads through the endpoint', async ({ request }) => {
  const response = await request.post('https://your-app.example/upload', {
    multipart: {
      document: {
        name: 'report.pdf',
        mimeType: 'application/pdf',
        buffer: fs.readFileSync('fixtures/report.pdf'),
      },
    },
  });
  expect(response.ok()).toBeTruthy();
});

A direct API request does not prove that the page exposed the input correctly or that the browser submitted it.

cURL multipart example

For a service-level check outside Playwright, send the file as a multipart field. Replace the placeholder endpoint and field name with your application’s contract:

curl -X POST "https://your-app.example/upload" \
  -F "document=@fixtures/report.pdf;type=application/pdf"

Use this when the endpoint itself is under test. Keep a browser test as well when the upload UI is part of the user journey.

Common errors and fixes

Error or symptom Cause Fix
fill() fails on the input File inputs require file assignment, not text input. Call locator.setInputFiles().
File not found The relative path is resolved from the process working directory. Use an absolute or deliberately constructed path and confirm the fixture exists.
Chooser timeout The event wait started after the click, or the click did not open a chooser. Create waitForEvent('filechooser') before clicking; confirm the button really opens a file input.
Wrong upload control receives the file A broad selector matched multiple inputs. Use getByLabel() or narrow the locator by name, ID, or container.
Only one file appears The input lacks multiple, or the application intentionally limits files. Inspect the markup and test the documented application behavior.
Preview appears but upload fails Selection succeeded, but validation, submission, authentication, or the server rejected the file. Assert the network response and application error separately from the selection step.
API test passes while UI test fails The two tests cover different layers. Debug the locator, chooser event, client validation, and submit flow in the browser test.
Directory upload behaves unexpectedly The input is not a directory input or more than one directory was supplied. Use a single directory path with an input configured for directory selection.

Reliability and performance checklist

  • Use accessible, stable locators and avoid positional selectors.
  • Keep small deterministic fixtures in source control; generate large or unusual payloads in memory when that makes the case clearer.
  • Wait for the application’s result rather than adding arbitrary sleeps. Assert a preview, status, response, or saved record.
  • Keep browser-selection tests separate from endpoint tests so a failure identifies the layer that broke.
  • For repeated uploads, clear the input with [] or create a fresh page when the application retains state.
  • Set the filename and MIME type deliberately for validation tests; do not assume the server infers the same values as the browser.
  • Do not infer server storage, virus scanning, authorization, or size limits from a successful setInputFiles() call. Those require application-level assertions.

Or skip the browser setup

If your goal is a clean image or PDF of an upload flow, documentation page, or test result rather than exercising a file input, ScreenshotNeo provides a single screenshot request. The API accepts a URL and returns PNG, JPEG, WebP, or PDF.

Read the ScreenshotNeo API documentation for all options.

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}`);

Cookie banners, popups, and chat widgets are removed before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are never billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. An MCP server lets AI agents take screenshots, inspect pages, and capture PDFs. The Free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000 shots.

Create a free ScreenshotNeo account.

FAQ

Can I upload a file without opening the file picker?

Yes. Call setInputFiles() directly on the file-input locator. Use the chooser API only when the input is created or exposed by a click.

Can I test an upload without a real file?

Yes. Pass an in-memory payload with name, mimeType, and buffer (or Python’s equivalent bytes value).

Does selecting a file prove the server accepted it?

No. It proves that Playwright assigned the file to the browser control. Assert the submit response and the application’s resulting state.

Should I use the page-level setInputFiles method?

Use the locator method in new JavaScript and TypeScript tests. It is the documented, preferred form for targeting a specific control.

When should I use APIRequestContext instead?

Use it when the HTTP multipart endpoint is the subject of the test and browser selection or UI behavior is outside scope.

Primary references