Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

In Playwright, “load a local file” usually means uploading a fixture through an <input type="file">. For an existing input, call setInputFiles() on a locator. If a click creates the input dynamically, wait for the filechooser event and call setFiles(). Downloads use a different API: wait for the download event and save the file before the browser context closes. Rendering local HTML with setContent() is another workflow entirely.

Choose the Playwright workflow first

The right API depends on what “load” means in your test:

Goal Use What it does
Upload an existing fixture locator.setInputFiles() Assigns one or more files to an existing file input.
Upload after a button creates an input page.waitForEvent('filechooser') and chooser.setFiles() Captures the file chooser triggered by the action, then assigns files.
Generate a file in memory setInputFiles({ name, mimeType, buffer }) Uploads bytes without creating a fixture on disk.
Test a browser download page.waitForEvent('download') and download.saveAs() Saves the downloaded attachment to a persistent path.
Render an HTML string page.setContent(html) Writes supplied markup into the frame; it is not a file upload.

These APIs do not establish a universal recipe for navigating to arbitrary file:// URLs or serving a project directory. Such behavior depends on the browser, runtime, permissions, and how the local assets reference one another. Treat local-file navigation as a separate, environment-specific problem.

Upload a local file with a known input

Use a locator that targets the actual file input, preferably through its associated label. The path may be relative or absolute. Relative paths are resolved from the process’s current working directory, so make the test runner’s working directory explicit or construct an absolute path.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

JavaScript or TypeScript

import path from 'node:path';

const fixture = path.join(process.cwd(), 'fixtures', 'document.pdf');
await page.getByLabel('Upload file').setInputFiles(fixture);

// Continue with the assertion that proves the page accepted the file.
await expect(page.getByText('document.pdf')).toBeVisible();

If the input has no accessible label, use another stable locator, such as page.locator('input[type="file"]') or a test identifier. Avoid selecting a visible button when the page already exposes a regular input; setInputFiles() is more deterministic than opening an operating-system picker.

Python

from pathlib import Path
from playwright.sync_api import expect

fixture = Path.cwd() / "fixtures" / "document.pdf"
page.get_by_label("Upload file").set_input_files(str(fixture))
expect(page.get_by_text("document.pdf")).to_be_visible()

The Python API accepts the same kinds of inputs: a path, multiple paths, a directory where the page permits directory selection, an in-memory payload, or an empty list to clear the selection.

Multiple files and clearing a selection

// Multiple files (the input must allow multiple selection)
await page.getByLabel('Upload files').setInputFiles([
  'fixtures/one.txt',
  'fixtures/two.txt'
]);

// Clear the input
await page.getByLabel('Upload files').setInputFiles([]);

Passing multiple paths to an input that does not have the multiple attribute can produce a page-side validation error. Clearing with an empty array removes the selected files without editing the DOM yourself.

Upload generated data without a fixture

When test data is short, generated, or sensitive, provide a payload object. The payload needs a filename, a MIME type, and a byte buffer.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const csv = Buffer.from('id,namen1,Adan', 'utf8');
await page.getByLabel('Import CSV').setInputFiles({
  name: 'users.csv',
  mimeType: 'text/csv',
  buffer: csv
});

The filename and MIME type are visible to the application, so choose values that match the behavior under test. Generate a unique name when the server rejects duplicates; use a fixed name when deterministic assertions are more important.

Handle a dynamically created file chooser

Some applications create an invisible file input only after a user clicks a custom “Choose file” control. Establish the wait before the click; otherwise the event can be missed.

JavaScript or TypeScript

const chooserPromise = page.waitForEvent('filechooser');
await page.getByRole('button', { name: 'Choose file' }).click();
const chooser = await chooserPromise;
await chooser.setFiles('/absolute/path/to/document.pdf');

The same pattern works when the control is a label, menu item, or custom drag-and-drop substitute, provided the action actually opens a browser file chooser.

Python

with page.expect_file_chooser() as chooser_info:
    page.get_by_role("button", name="Choose file").click()
chooser = chooser_info.value
chooser.set_files("/absolute/path/to/document.pdf")

For multiple files or generated data, pass the same path list or payload structure described for setInputFiles(). If the click does not trigger a chooser, inspect the page: it may be navigating, opening a modal, or using a drop zone that requires a different application-specific interaction.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Wait for and save a downloaded file

Uploading sends data into the page. A download is emitted by the browser and must be handled with the Download API. Start waiting before the action that triggers the attachment, then save it to a path you control.

const downloadPromise = page.waitForEvent('download');
await page.getByText('Download file').click();
const download = await downloadPromise;
const destination = path.join(process.cwd(), 'artifacts', download.suggestedFilename());
await download.saveAs(destination);

Downloads normally live in a temporary directory and are removed when the browser context closes. Call saveAs() before closing that context if another test, process, or assertion needs the file. Create the destination directory in your test setup and sanitize any filename before using it in a shared artifact location.

Python download example

from pathlib import Path

destination_dir = Path.cwd() / "artifacts"
destination_dir.mkdir(parents=True, exist_ok=True)
with page.expect_download() as download_info:
    page.get_by_text("Download file").click()
download = download_info.value
download.save_as(str(destination_dir / download.suggested_filename()))

If the response is an inline preview rather than an attachment, no download event may occur. Assert the resulting page or response instead of waiting indefinitely for a download.

Render supplied HTML with setContent()

To test markup you already have as a string, use:

await page.setContent('

Fixture page

');

setContent(html) assigns HTML to the frame and internally calls document.write(). It does not select a local file, upload bytes through a file input, or guarantee that adjacent relative assets from a local directory will resolve. If your test needs CSS, images, scripts, or fonts, decide explicitly how those resources will be served in the test environment.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Paths, fixtures, and test isolation

Make path resolution predictable

  • Resolve fixtures from a known project root or the current working directory used by the runner.
  • Prefer absolute paths in CI when the runner may start from a different directory.
  • Use platform-aware path helpers such as Node’s path.join() or Python’s pathlib.Path; do not concatenate slash strings.
  • Keep fixture names and generated artifact names deterministic unless uniqueness is part of the test.

Keep tests independent

  • Use a fresh temporary artifact directory per test or worker.
  • Clear a reusable input with setInputFiles([]) before a second scenario.
  • Close pages only after downloads have been saved.
  • Do not rely on a previous test’s current working directory or downloaded temporary file.

Troubleshooting common failures

“File not found” or an empty upload

Print or log the resolved path, verify the file exists in the worker’s environment, and check the runner’s working directory. A path valid on a developer laptop may not exist in a container or CI checkout.

Locator matches the wrong element

Confirm that the locator resolves to input[type="file"], not a styled button. Use an accessible label or a unique test identifier and assert the locator count when the page contains several upload controls.

The file chooser wait times out

The wait may have been started after the click, or the control may not open a native chooser. Move the event wait before the action and verify the application’s click handler. For a drop zone, follow the application’s documented input mechanism instead.

Only one file is accepted

Check for the input’s multiple attribute and the application’s own validation. Pass one path for a single-file control; do not assume that an array changes the page’s capabilities.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Download disappears after the test

Save it with download.saveAs() while the producing context is still open. Temporary downloads are deleted when that context closes.

Assertions race the upload or download

Wait for a visible page result, network-driven status, or the download event rather than adding arbitrary sleeps. A delay can hide a race and still fail on a slower worker.

Local HTML has missing images or scripts

setContent() writes markup but does not define a project server or a universal local-file policy. Serve assets through a test web server, use URLs that the page can reach, or test the resource loading setup separately for your chosen browser and runtime.

Performance and reliability choices

  • Use disk paths for large, reusable fixtures. They avoid rebuilding the same bytes in every test, but require reliable fixture packaging in CI.
  • Use in-memory payloads for small or generated cases. They reduce filesystem setup and make edge-case data easy to vary.
  • Wait on events, not fixed delays. Event-based synchronization is both faster on a healthy run and less flaky under load.
  • Save only the artifacts you need. Persistent copies aid debugging, while unnecessary downloads increase disk usage on parallel workers.
  • Keep browser and Playwright versions aligned with your project. The exact signatures and behavior are version-sensitive; check the API documentation for the version installed in your lockfile.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If your goal is to obtain a clean image or PDF of a URL rather than exercise a Playwright upload flow, ScreenshotNeo provides a one-request screenshot API. It accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be disabled. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response reports the page verdict and billing status in X-Page-Verdict and X-Billed headers.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Start with cURL (see the ScreenshotNeo documentation):

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python

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)

Node.js

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo also offers an MCP server with take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. Its Free plan includes 1,000 shots per month without a card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

Frequently Asked Questions

Can I upload a directory in Playwright?

Python’s documented file-input API covers directory selection when the page’s input is configured for it. Confirm that the input and browser scenario support directory uploads before relying on that behavior.

Should I use a file URL instead of setInputFiles()?

No single rule covers every file URL, browser, and local asset arrangement. Use setInputFiles() for uploads; treat file URL navigation and local asset serving as a separate environment-specific design.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Why does my downloaded filename differ from the link text?

The browser exposes the server-provided suggested filename through download.suggestedFilename(). Use that value, after applying your own path and safety rules, rather than assuming the visible link text is the filename.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.