Skip to main content
Back to Blog
Reference
2026-05-22

Playwright setInputFiles File Upload: Complete Reference

Complete 2026 reference for Playwright setInputFiles file upload. Learn how to upload single files, multiple files, in-memory buffers, drag-and-drop uploads, hidden inputs, and how to handle file pickers and large files.

File uploads are one of the trickiest parts of any test suite because the operating system file picker is outside the browser sandbox. Playwright solves this elegantly with locator.setInputFiles() — a single API that handles every upload pattern, from a basic <input type="file"> to drag-and-drop zones, hidden inputs, multiple files, and even in-memory buffers.

This complete 2026 reference covers every setInputFiles pattern you will hit in production tests, with TypeScript examples and a comparison table at the end.


Table of Contents

  1. Basic Single-File Upload
  2. Multiple File Upload
  3. In-Memory Buffer Upload
  4. Hidden File Inputs
  5. Drag-and-Drop Uploads
  6. File Chooser Event API
  7. Large File Uploads
  8. Clearing the File Selection
  9. Asserting Upload Success
  10. Common Errors and Fixes
  11. Comparison: Upload Strategies
  12. Frequently Asked Questions

Basic Single-File Upload {#basic-single-file-upload}

The canonical pattern: locate the input, point it at a file on disk.

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

test('uploads a profile picture', async ({ page }) => {
  await page.goto('/settings/profile');

  await page.getByLabel('Profile picture').setInputFiles(
    path.join(__dirname, 'fixtures', 'avatar.png')
  );

  await page.getByRole('button', { name: 'Save' }).click();
  await expect(page.getByRole('img', { name: 'avatar' })).toBeVisible();
});

Note that you must pass an absolute path. Relative paths break in different working directories. Always use path.join(__dirname, ...) or path.resolve().


Multiple File Upload {#multiple-file-upload}

When the input has multiple attribute, pass an array.

test('uploads multiple attachments', async ({ page }) => {
  await page.goto('/tickets/new');

  await page.getByLabel('Attachments').setInputFiles([
    path.join(__dirname, 'fixtures', 'log.txt'),
    path.join(__dirname, 'fixtures', 'screenshot.png'),
    path.join(__dirname, 'fixtures', 'diagnostic.json'),
  ]);

  await expect(page.getByText('3 files selected')).toBeVisible();
});

If you pass multiple files to a non-multiple input, Playwright throws Error: Non-multiple file input can only accept single file.


In-Memory Buffer Upload {#in-memory-buffer-upload}

For files that don't exist on disk (CSV generated by a test, random binary, etc.), pass an object with name, mimeType, and buffer.

test('uploads dynamically generated CSV', async ({ page }) => {
  await page.goto('/import');

  const csv = 'id,name\n1,Alice\n2,Bob\n';

  await page.getByLabel('Upload CSV').setInputFiles({
    name: 'users.csv',
    mimeType: 'text/csv',
    buffer: Buffer.from(csv, 'utf-8'),
  });

  await page.getByRole('button', { name: 'Import' }).click();
  await expect(page.getByText('2 users imported')).toBeVisible();
});

This pattern is invaluable for property-based testing — generate random valid (or invalid) inputs in-memory and feed them to your form without polluting the filesystem.

You can also pass an array of buffers for multiple files:

await page.getByLabel('Bulk upload').setInputFiles([
  { name: 'a.txt', mimeType: 'text/plain', buffer: Buffer.from('A') },
  { name: 'b.txt', mimeType: 'text/plain', buffer: Buffer.from('B') },
]);

Hidden File Inputs {#hidden-file-inputs}

Many modern apps style their file inputs with a custom button and hide the actual <input type="file"> via CSS. The styled button opens the picker; the input never appears.

The standard getByLabel won't work because the input is not associated with a visible label. Two solutions:

Solution A: Target the Input Directly

// Find the hidden input by its type
await page.locator('input[type="file"]').setInputFiles('avatar.png');

setInputFiles works on hidden inputs by default — Playwright bypasses the visibility check.

Solution B: Force Visibility

If the input has a custom locator, just call setInputFiles — it does not require visibility:

await page.locator('#hidden-file-input').setInputFiles('file.png');

Drag-and-Drop Uploads {#drag-and-drop-uploads}

Some apps only support drag-and-drop — there is no <input type="file"> at all. The DataTransfer JS API is the standard trick:

test('uploads via drag and drop', async ({ page }) => {
  await page.goto('/upload');

  const buffer = await fs.promises.readFile('fixtures/image.png');
  const dataTransfer = await page.evaluateHandle(
    ({ data, name, type }) => {
      const dt = new DataTransfer();
      const file = new File([new Uint8Array(data)], name, { type });
      dt.items.add(file);
      return dt;
    },
    { data: [...buffer], name: 'image.png', type: 'image/png' }
  );

  await page.locator('.drop-zone').dispatchEvent('drop', { dataTransfer });
  await expect(page.getByText('Upload complete')).toBeVisible();
});

This pattern works for any drag-and-drop library: React Dropzone, FilePond, Uppy, native HTML5 drag events.


File Chooser Event API {#file-chooser-event-api}

Sometimes the file input is created dynamically when a button is clicked (Google Drive-style "Upload from computer"). The file input may not exist in the DOM until the user clicks. For this, use the filechooser event:

test('handles dynamic file chooser', async ({ page }) => {
  await page.goto('/files');

  const fileChooserPromise = page.waitForEvent('filechooser');
  await page.getByRole('button', { name: 'Upload' }).click();
  const fileChooser = await fileChooserPromise;

  await fileChooser.setFiles(path.join(__dirname, 'fixtures', 'doc.pdf'));
});

The waitForEvent listener must be set up before the click — otherwise you race the picker opening.


Large File Uploads {#large-file-uploads}

For files > 50 MB, network IO between the test runner and the browser can become a bottleneck. Best practices:

  1. Pre-stage on disk: save large fixtures to a directory and pass the path; Playwright streams the file without loading it all into Node memory.
  2. Increase timeout: large uploads need bigger actionTimeout:
await page.getByLabel('Upload').setInputFiles('big-video.mp4', { timeout: 120_000 });
  1. Verify chunk-by-chunk: if the app supports resumable uploads (tus, Uppy), assert that each chunk completes via network interception.
let chunksReceived = 0;
page.on('request', (req) => {
  if (req.url().includes('/upload') && req.method() === 'PATCH') chunksReceived++;
});
await page.getByLabel('Upload').setInputFiles('200mb.bin');
await expect.poll(() => chunksReceived).toBeGreaterThan(10);

Clearing the File Selection {#clearing-the-file-selection}

To clear a previously-selected file, pass an empty array:

await page.getByLabel('Resume').setInputFiles([]);
await expect(page.getByText('No file chosen')).toBeVisible();

Asserting Upload Success {#asserting-upload-success}

Don't assert that the file was selected — assert that the server accepted it. Pick from this menu:

Assertion StyleWhat to Check
UI feedback"Upload complete" text appears
Network call successPOST returns 200 with expected JSON
Visual confirmationAvatar image src matches the uploaded file
DB / API verificationA follow-up GET returns the uploaded file's metadata
await page.getByLabel('Avatar').setInputFiles('avatar.png');
const response = await page.waitForResponse((r) =>
  r.url().includes('/api/uploads') && r.status() === 201
);
const { id } = await response.json();
expect(id).toBeTruthy();

Common Errors and Fixes {#common-errors-and-fixes}

"Element is not an "

You're calling setInputFiles on the wrong element (probably a styled label or button). Locate the actual <input type="file">.

"ENOENT: no such file or directory"

Relative path resolved from the wrong working directory. Use path.join(__dirname, ...).

"Non-multiple file input can only accept single file"

The input lacks the multiple attribute but you passed an array. Pass a single string or add multiple to the input.

Picker opens but nothing happens

You called setInputFiles on a hidden input, but the app dispatches no change event because it expects a real user click. Some apps need explicit dispatchEvent('change'):

const input = page.locator('input[type="file"]');
await input.setInputFiles('file.png');
await input.dispatchEvent('change');

Comparison: Upload Strategies {#comparison-upload-strategies}

StrategyUse WhenSpeedRealism
setInputFiles(path)Standard <input type="file">FastHigh
setInputFiles(buffer)Generated dataFastestHigh
filechooser eventPicker created dynamicallyFastHigh
DataTransfer dragDrag-and-drop only zoneMediumHigh
API upload bypassE2E doesn't care about UI flowFastestLow

Frequently Asked Questions {#frequently-asked-questions}

Does setInputFiles work on hidden inputs?

Yes. Playwright does not require visibility for file inputs.

Can I upload from a URL instead of disk?

No, you must provide a path or a Buffer. Download the file first if you only have a URL.

How do I test "file too large" validation?

Use an in-memory buffer at the exact threshold size: Buffer.alloc(11 * 1024 * 1024) for an 11 MB file.

Can setInputFiles trigger image preview?

Yes — the browser fires the change event automatically, which most apps listen to for previews.

What about iframe-based file inputs?

Use frameLocator() to scope into the frame, then chain setInputFiles:

await page.frameLocator('#upload-iframe').locator('input[type="file"]').setInputFiles('a.png');

How do I mock file uploads to skip the real backend?

Use page.route() to intercept the POST and respond with a fake success.

Does setInputFiles support shadow DOM inputs?

Yes — Playwright pierces shadow DOM, so the locator just works.

Are uploaded files isolated per browser context?

Yes — each context has its own filesystem sandbox.


Related QASkills Skills

Add file-upload patterns to your AI agent's skill library:

npx qaskills add playwright-file-upload
npx qaskills add playwright-drag-drop
npx qaskills add playwright-form-testing

See more at qaskills.sh/skills.


setInputFiles is one of those rare APIs that handles every variation gracefully. Master the patterns above and you'll never write a file-upload test workaround again.

Playwright setInputFiles File Upload: Complete Reference | QASkills.sh