Resource library

QA How-To

Playwright setInputFiles: Examples and Best Practices

Learn playwright setInputFiles examples for single, multiple, generated, and hidden uploads, with reliable assertions and debugging practices for QA teams.

19 min read | 2,543 words

TL;DR

Use locator.setInputFiles() on an input type=file, passing a path, an array of paths, a directory, or a FilePayload object. Then assert both the selected file metadata and the application's completed upload state.

Key Takeaways

  • Prefer locator.setInputFiles() because locator-based actions are the current, resilient Playwright API.
  • Resolve fixture paths from a known root so tests behave the same locally and in CI.
  • Use FilePayload objects for generated data and file paths for realistic fixture uploads.
  • Treat chooser handling, file selection, transfer completion, and server processing as separate checkpoints.
  • Assert visible product outcomes and request results, not only the input element's file list.
  • Keep upload fixtures small, deterministic, safe, and explicitly versioned with the test suite.

The safest Playwright setInputFiles examples use a locator for the real file input, supply deterministic file data, and verify what the application does after selection. In its simplest form, the API accepts a path. It can also select multiple files, populate a directory input, clear a selection, or create a file from a Buffer without touching the test machine's disk.

File upload testing becomes unreliable when a script treats selection as proof that the upload succeeded. A production upload flow has several stages: the browser accepts a file, client validation runs, bytes move to a service, and the server stores or processes them. This guide shows how to test those stages with TypeScript and Playwright Test, including custom buttons, dynamic choosers, generated payloads, CI paths, negative cases, and interview-ready reasoning.

TL;DR

Use a labeled locator whenever the input exists in the DOM:

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

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

  const resume = path.join(process.cwd(), 'tests/fixtures/resume.pdf');
  await page.getByLabel('Upload resume').setInputFiles(resume);

  await expect(page.getByText('resume.pdf')).toBeVisible();
  await expect(page.getByRole('status')).toHaveText('Upload complete');
});
Situation Input to use Preferred Playwright approach
One realistic fixture Absolute file path locator.setInputFiles(path)
Several fixtures Array of paths locator.setInputFiles(paths)
Generated small file FilePayload object locator.setInputFiles({ name, mimeType, buffer })
Dynamic chooser FileChooser waitForEvent, click, then setFiles
Remove selection Empty array locator.setInputFiles([])
Directory picker One directory path Target an input with webkitdirectory

1. Playwright setInputFiles examples start with the real control

Modern upload interfaces often show a polished button, avatar tile, or drop zone while hiding an input with type=file. The browser ultimately attaches selected files to that input. locator.setInputFiles() writes to the control directly, so the test does not need to drive a native Windows, macOS, or Linux dialog. Native picker automation would be slow, platform-specific, and outside the browser page.

Prefer a user-facing label when the product exposes one:

<label for="resume">Upload resume</label>
<input id="resume" name="resume" type="file" accept=".pdf" />
const upload = page.getByLabel('Upload resume');
await upload.setInputFiles('tests/fixtures/resume.pdf');

If the input is intentionally hidden and has no useful accessible label, add a stable test ID in the application:

<button type="button">Choose resume</button>
<input data-testid="resume-input" type="file" hidden />
await page.getByTestId('resume-input').setInputFiles(
  'tests/fixtures/resume.pdf'
);

Do not target text from the decorative button and assume it is the input. setInputFiles expects the locator to resolve to a file input, or to a label whose associated control is a file input. Also avoid the discouraged page.setInputFiles(selector, files) form in new code. Locator-based calls are clearer and remain correct when a framework replaces the element during rendering.

For a broader locator strategy, see Playwright locator best practices. Good upload tests begin with the same accessible, stable locator rules as every other UI test.

2. Build file paths that work locally and in CI

Relative upload paths are resolved from the process working directory, not from the spec file's directory. That distinction is easy to miss. A command started at the repository root may pass, while an IDE, monorepo task, or CI job started elsewhere fails with a missing-file error.

Create an absolute path from an intentional root:

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

const fixture = (...parts: string[]) =>
  path.join(process.cwd(), 'tests', 'fixtures', ...parts);

test('uploads a profile image', async ({ page }) => {
  await page.goto('/settings/profile');
  await page.getByLabel('Profile photo').setInputFiles(
    fixture('avatar.png')
  );

  await expect(page.getByRole('img', { name: 'Profile preview' }))
    .toBeVisible();
});

In an ESM project, a path relative to the spec itself is another valid option:

import { fileURLToPath } from 'node:url';

const avatar = fileURLToPath(
  new URL('../fixtures/avatar.png', import.meta.url)
);

Whichever convention you choose, use it across the suite. Keep filenames case-correct because Linux runners are commonly case-sensitive even when a developer machine is not. Add the fixtures to source control unless they contain secrets or personal information. Before selecting a fixture, Node's existsSync or statSync can produce a clearer setup failure, but it should not replace the upload assertion.

Small purpose-built fixtures are easier to review than copied customer documents. Record why each file exists, such as valid PDF, oversized image, malformed CSV, or unsupported extension. That makes a failure explainable without opening a mystery binary.

3. Upload one file and assert the FileList

A focused test can verify browser selection before it waits for the application. The DOM input exposes a FileList with the selected file's name, MIME type, and size. Reading it is especially useful when diagnosing whether a failure happened before or after the network request.

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

test('selects one PDF with expected metadata', async ({ page }) => {
  await page.goto('/documents');
  const input = page.getByLabel('Identity document');
  const filePath = path.join(
    process.cwd(),
    'tests/fixtures/identity-sample.pdf'
  );

  await input.setInputFiles(filePath);

  const selected = await input.evaluate((element: HTMLInputElement) => {
    const file = element.files?.[0];
    return file
      ? { name: file.name, type: file.type, size: file.size }
      : null;
  });

  expect(selected).not.toBeNull();
  expect(selected?.name).toBe('identity-sample.pdf');
  expect(selected?.type).toBe('application/pdf');
  expect(selected?.size).toBeGreaterThan(0);
});

This is a diagnostic assertion, not the full business assertion. A malicious client can falsify a MIME type, and selecting a file does not prove the server accepted it. Continue with an observable application outcome. For an instant client preview, check a filename, thumbnail, validation message, or enabled Submit button. For automatic upload, wait for a response or a stable status component.

Avoid fixed waits such as waitForTimeout(3000). Transfer time changes with file size, machine load, and service latency. Web-first assertions retry until their condition is met. If you need deeper guidance on retries and readiness, read how Playwright auto-waiting works.

4. Upload multiple files without losing coverage

For an input with the multiple attribute, pass an array. The browser preserves the supplied order, but the application may sort or deduplicate the files. Write the assertion around the product contract, not an accidental display order.

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

test('uploads two evidence files', async ({ page }) => {
  await page.goto('/claims/new');
  const files = ['front.png', 'back.png'].map(name =>
    path.join(process.cwd(), 'tests/fixtures', name)
  );
  const input = page.getByLabel('Evidence files');

  await expect(input).toHaveAttribute('multiple', '');
  await input.setInputFiles(files);

  const names = await input.evaluate((element: HTMLInputElement) =>
    Array.from(element.files ?? [], file => file.name)
  );
  expect(names).toEqual(['front.png', 'back.png']);

  await expect(page.getByTestId('upload-row')).toHaveCount(2);
  await expect(page.getByText('front.png')).toBeVisible();
  await expect(page.getByText('back.png')).toBeVisible();
});

Multiple-file behavior deserves more than a happy path. Cover a mixed batch where one file is invalid. Clarify whether the product rejects the full batch, accepts valid members, or reports per-file results. Check duplicate filenames, cumulative size limits, individual size limits, and the maximum count. Those are different rules and should not be collapsed into one vague test.

Do not loop and call setInputFiles once per file unless the product intentionally supports incremental selection. Each call normally replaces the current input selection. Supply the complete array for one chooser action, or click an application-level Add more control between batches if that is the real user workflow.

5. Generate files in memory with FilePayload

A FilePayload has three required fields: name, mimeType, and buffer. It is ideal when the content is short, generated for a test, or unique per run. It also prevents a fixture directory from filling with trivial permutations.

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

test('imports a generated CSV', async ({ page }) => {
  await page.goto('/contacts/import');

  const csv = [
    'email,first_name,last_name',
    'ada@example.test,Ada,Lovelace',
    'grace@example.test,Grace,Hopper',
  ].join('\n');

  await page.getByLabel('CSV file').setInputFiles({
    name: 'contacts.csv',
    mimeType: 'text/csv',
    buffer: Buffer.from(csv, 'utf8'),
  });

  await page.getByRole('button', { name: 'Import' }).click();
  await expect(page.getByRole('status'))
    .toHaveText('2 contacts imported');
});

The same technique supports JSON, plain text, XML, or tiny synthetic binary buffers. Use a realistic name and MIME type because applications often validate both. Remember that these client values are advisory. Secure server code should inspect content and enforce its own allowlist.

FilePayload is less suitable for a large performance fixture. Holding a large buffer increases the test process's memory use and makes failures harder to reproduce outside automation. Keep a reviewed file on disk for representative large uploads, or test high-volume transfer behavior at a service layer designed for that purpose.

Generated content should be deterministic. If uniqueness is necessary, derive it from testInfo.parallelIndex or a stable run identifier and log it. Random unrecorded data creates failures that cannot be replayed.

6. More playwright setInputFiles examples for dynamic choosers

Sometimes the file input is created only after a click, or it lives behind component logic that makes a stable direct locator impractical. In that case, wait for the page's filechooser event. Create the promise before clicking so the event cannot happen before the test starts listening.

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

test('uses a dynamic avatar chooser', async ({ page }) => {
  await page.goto('/account');

  const chooserPromise = page.waitForEvent('filechooser');
  await page.getByRole('button', { name: 'Change avatar' }).click();
  const chooser = await chooserPromise;

  expect(chooser.isMultiple()).toBe(false);
  await chooser.setFiles(
    path.join(process.cwd(), 'tests/fixtures/avatar.png')
  );

  await expect(page.getByRole('status'))
    .toHaveText('Avatar uploaded');
});

Do not use Promise.all with an already awaited event registration in the wrong order. The important rule is that the listener exists before the click. The explicit promise sequence above is easy to read and debug.

Use chooser handling only when it represents a necessary component behavior. Directly locating the input is usually shorter and less coupled to the trigger button. Also distinguish setInputFiles on a Locator from setFiles on a FileChooser. They accept the same general file shapes but belong to different objects.

If a custom drop area has a backing input, populate that input. On current Playwright versions, locator.drop() is available for a true drop target that depends on dragenter, dragover, and drop events. Choose the API that matches the application's event model, then assert the same transfer and persistence outcomes.

7. Clear, replace, and upload a directory

Passing an empty array clears the input. This small feature enables useful tests for remove and replace flows:

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

test('replaces an attachment before submitting', async ({ page }) => {
  await page.goto('/support/ticket');
  const input = page.getByLabel('Attachment');

  await input.setInputFiles('tests/fixtures/old-log.txt');
  await expect(page.getByText('old-log.txt')).toBeVisible();

  await input.setInputFiles([]);
  await expect(page.getByText('old-log.txt')).toBeHidden();

  await input.setInputFiles('tests/fixtures/current-log.txt');
  await expect(page.getByText('current-log.txt')).toBeVisible();
});

The application may keep its own preview state after the DOM selection clears. If so, the test has found a real integration boundary to assert. Confirm both FileList length and UI removal when the feature promises both.

For an input with webkitdirectory, pass one directory path. Playwright recursively supplies files through that directory input. Only a single directory path is supported for this input type:

await page.getByLabel('Project folder').setInputFiles(
  'tests/fixtures/sample-project'
);

Directory upload behavior varies by product. Assert relative paths if the application displays them, ignored hidden files if specified, nested folder handling, and server-side path sanitization. Do not put secrets such as local environment files inside a directory fixture.

8. Synchronize with the network and server processing

An upload may start automatically after selection or only after a Submit click. In either design, register a response wait before the action that triggers the request. This turns a vague timeout into a useful HTTP assertion.

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

test('observes the document upload response', async ({ page }) => {
  await page.goto('/documents');

  const responsePromise = page.waitForResponse(response =>
    response.url().includes('/api/documents') &&
    response.request().method() === 'POST'
  );

  await page.getByLabel('Document').setInputFiles({
    name: 'note.txt',
    mimeType: 'text/plain',
    buffer: Buffer.from('reviewed by QA', 'utf8'),
  });

  const response = await responsePromise;
  expect(response.ok()).toBeTruthy();

  await expect(page.getByRole('status'))
    .toHaveText('Processing complete');
});

The response predicate should be specific enough that analytics or unrelated background calls cannot satisfy it. For multipart services, avoid asserting opaque implementation details in every UI test. A few contract tests can validate field names and headers, while end-to-end tests focus on user-visible acceptance and persistence.

Some systems return 202 Accepted and process the file asynchronously. In that case, a successful upload response proves only that the job was queued. Wait for a status transition, poll a supported API, or navigate to the resulting record and assert its processed state. Use a bounded product timeout based on the service-level expectation, not a blind sleep.

For API-focused coverage that complements the browser path, see Playwright APIRequestContext examples.

9. Design high-value negative and security tests

File uploads are a security boundary. UI validation is helpful to users but cannot be the only control, so test client and server behavior separately. Useful cases include an unsupported extension, an allowed extension with mismatched content, a zero-byte file, a filename with multiple dots, Unicode characters, a very long safe name, and an exact boundary-size file.

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

test('rejects a disguised executable payload', async ({ page }) => {
  await page.goto('/profile');

  await page.getByLabel('Resume').setInputFiles({
    name: 'resume.pdf.exe',
    mimeType: 'application/pdf',
    buffer: Buffer.from('MZ-not-a-pdf', 'utf8'),
  });

  await page.getByRole('button', { name: 'Upload' }).click();
  await expect(page.getByRole('alert'))
    .toContainText('File type is not allowed');
});

Use harmless synthetic bytes. Never include real malware in the repository or CI environment. If antivirus integration is in scope, coordinate a recognized safe test string and isolated environment with the security team.

Boundary tests need exact fixture construction. If the limit is defined by the server, cover one byte below, exactly at, and one byte above through an API or component layer. Running huge files repeatedly in the browser slows feedback without adding proportional confidence.

Also verify cleanup. Rejected uploads should not create a visible record or leave an accessible object behind. Authorized users should not be able to retrieve another user's uploaded file by guessing a URL. Those checks extend beyond setInputFiles, but they are part of a credible upload test strategy.

10. Make upload tests maintainable in a parallel suite

Parallel tests must not overwrite the same server object or mutate one shared account's attachment list. Give created records unique, traceable names and clean them through supported APIs. A FilePayload name can include testInfo.parallelIndex without making the content unpredictable:

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

test('uploads a worker-specific note', async ({ page }, testInfo) => {
  const name = `note-worker-${testInfo.parallelIndex}.txt`;
  await page.goto('/notes');
  await page.getByLabel('Attachment').setInputFiles({
    name,
    mimeType: 'text/plain',
    buffer: Buffer.from('deterministic test content', 'utf8'),
  });

  await expect(page.getByText(name)).toBeVisible();
});

Keep a small fixture catalog with ownership and intent. Avoid dynamically downloading public files during a test, because network availability and third-party changes become unrelated failure modes. Do not embed credentials, customer exports, or personally identifiable information.

Trace capture is valuable when an upload fails because it preserves actions and network context, but a trace is not a substitute for assertions. Attach the selected filename, response status, and relevant server correlation ID to the test report when available. For systematic failure analysis, use Playwright trace viewer debugging.

Finally, decide where each check belongs. Browser tests validate control wiring and user journeys. API tests validate multipart contracts and authorization. Unit tests validate parsers and filename utilities. A balanced pyramid catches more defects with fewer slow, repetitive uploads.

Interview Questions and Answers

Q: Why does Playwright not open the operating-system file dialog?

The browser exposes a file input, while the native picker belongs to the operating system. setInputFiles sets the files on the input directly, producing the same browser-side selection without platform-specific desktop automation. This keeps the test portable across headless CI and developer machines.

Q: What argument types can locator.setInputFiles accept?

It accepts one path, an array of paths, one FilePayload object, or an array of FilePayload objects. A FilePayload contains name, mimeType, and buffer. An empty array clears the selected files, and a webkitdirectory input accepts one directory path.

Q: How do you test an upload control whose input is hidden?

I locate the hidden input by its associated label or a stable test ID and call setInputFiles directly. Visibility is not the business requirement for the hidden control, because the visible button is only its presentation. I still test the button separately if opening the chooser is meaningful product behavior.

Q: How do you prove that an upload succeeded?

I assert at more than one layer. FileList metadata proves selection, a response assertion proves the transfer endpoint responded, and a durable UI or API assertion proves server processing. The exact combination depends on whether the service is synchronous or asynchronous.

Q: When is FileChooser necessary?

It is useful when the application creates the input only as part of the click flow or when direct location is impractical. The test must start waiting for filechooser before clicking. Then it calls setFiles on the emitted object.

Q: How would you make large upload tests efficient?

I keep only a small number of browser journeys for representative boundaries and move exhaustive size validation to API or component tests. Large fixtures stay deterministic and are not loaded into unnecessary buffers. I wait for explicit progress or processing signals and collect correlation data on failure.

Q: What would you review in a flaky upload test?

I would check path resolution, fixture availability, locator uniqueness, event registration order, parallel data collisions, and whether completion relies on a sleep. I would inspect the trace and upload response, then identify whether selection, transfer, or processing failed. That breakdown usually points to the responsible layer quickly.

Common Mistakes

  • Using page.setInputFiles in new tests. Prefer locator.setInputFiles because the page-level form is discouraged.
  • Clicking a styled upload button and trying to control the native picker. Target the file input or use the filechooser event.
  • Building paths from an assumed working directory. Resolve from a documented root and honor case sensitivity.
  • Calling setInputFiles repeatedly for a multiple input. A later call replaces the current selection unless the product supplies a separate incremental workflow.
  • Asserting only that a filename appears. Verify the response and final processed or persisted state when those outcomes matter.
  • Waiting a fixed number of seconds. Wait for a response, progress completion, status transition, or web-first UI assertion.
  • Keeping sensitive or enormous fixtures in source control. Use safe synthetic data and the smallest representative files.
  • Trusting file extension and MIME type as security proof. The server must validate content, authorization, limits, and storage behavior.
  • Sharing one mutable uploaded record across parallel workers. Create isolated records and clean them through supported interfaces.

Conclusion

Strong Playwright setInputFiles examples are short at the selection layer and thorough at the outcome layer. Locate the actual file input, choose a path or FilePayload that matches the scenario, and register any network wait before the action that triggers transfer. Use FileChooser only for genuinely dynamic controls, and use an empty array when the test needs to clear the selection.

Your next step is to take one existing upload test and label its checkpoints: selected, transferred, processed, and persisted. Replace any arbitrary sleep with the product signal for that checkpoint, then add one focused negative case using a safe generated payload.

Interview Questions and Answers

What does locator.setInputFiles() do in Playwright?

It sets the files of an input type=file without automating the operating-system picker. It accepts file paths, arrays, directories for webkitdirectory inputs, and in-memory FilePayload objects. An empty array clears the selection.

Why is locator.setInputFiles preferred over page.setInputFiles?

The page-level method is discouraged in favor of locators. A locator expresses how to find the current element and participates in Playwright's retry model, which makes it safer when the DOM re-renders.

How would you verify a file upload test?

I separate selection, transfer, and processing. I can inspect the input's FileList for fast feedback, observe the upload response, and assert the final UI or persisted server record. A success toast alone is not enough if the backend can still reject or corrupt the file.

What is a FilePayload in Playwright?

It is an object with name, mimeType, and buffer fields. Playwright uses it to create an in-memory file for the browser input. It keeps generated test data deterministic and removes unnecessary fixture files.

How do you handle a dynamically created file chooser?

I create page.waitForEvent('filechooser') before clicking the control that opens it. After the click, I await the promise and call fileChooser.setFiles(). Registering the wait first prevents a race with the event.

What makes file upload automation flaky?

Common causes include ambiguous locators, relative paths tied to a local working directory, reused mutable fixtures, arbitrary sleeps, and weak completion checks. Large files and asynchronous virus scanning also require explicit, product-level readiness signals.

Can setInputFiles automate drag and drop uploads?

It can populate the backing file input when the drop zone uses one. If the product genuinely depends on drag events rather than an input, use the supported locator.drop() API with files on current Playwright versions, then verify the same upload outcomes.

Frequently Asked Questions

How do I upload a file with Playwright setInputFiles?

Locate the file input and call locator.setInputFiles() with an absolute or relative path. Prefer an accessible label or stable test ID, then assert the application's success state after the upload completes.

Can Playwright upload a file without keeping it on disk?

Yes. Pass a FilePayload object containing name, mimeType, and a Node.js Buffer. This is useful for generated CSV, JSON, text, and small negative-test payloads.

How do I upload multiple files in Playwright?

Pass an array of paths or FilePayload objects to setInputFiles(). The underlying input must support multiple selection, so also validate its multiple property and the application's handling of every file.

Does setInputFiles work with hidden file inputs?

Yes. You can target the input directly even when the product hides it behind a styled button. Avoid force unless a specific application condition requires it, because setInputFiles does not need a visible operating-system dialog.

When should I use fileChooser.setFiles instead?

Use FileChooser when the input is created dynamically and cannot be located until a user action opens the chooser. Start waiting for the filechooser event before clicking, then call setFiles on the returned chooser.

How do I clear a selected upload in Playwright?

Call locator.setInputFiles([]). This resets the file input selection and is useful for testing replace, cancel, and client-side validation flows.

Why does my Playwright file upload pass locally but fail in CI?

The usual causes are a path based on the wrong working directory, filename case differences, missing fixtures, permissions, or a test that waits only for selection rather than upload completion. Log and assert the resolved path, and wait on a product-specific completion signal.

Related Guides