How to fix Playwright file upload failures with `setInputFiles`?

Playwright

File upload failures in Playwright almost always come down to three root causes: targeting the wrong element (a styled <div> or <button> rather than the actual <input type="file">), passing an invalid file path that doesn't exist at test runtime, or calling setInputFiles on a hidden input that the browser won't accept without special handling. The page.setInputFiles and locator.setInputFiles APIs only work on actual <input type="file"> elements — they bypass the OS file picker dialog entirely, which is the correct approach for test automation.

Common mistake

test('uploads avatar', async ({ page }) => {
  await page.goto('https://app.example.com/profile');

  // Clicking the label or drop zone — not the actual file input
  await page.locator('.upload-dropzone').setInputFiles('avatar.jpg');
  //                                    ^^^^^^^^^^^^^ fails if not an <input type="file">
});

Also common: wrong path relative to the working directory when running from CI:

// Relative path — depends on where npm test is run from
await page.getByLabel('Upload').setInputFiles('fixtures/invoice.pdf');

The fix

Target the <input type="file"> element directly, and use path.join(__dirname, ...) for file paths:

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

test('uploads profile photo', async ({ page }) => {
  await page.goto('https://app.example.com/profile');

  // Use an absolute path anchored to the test file's directory
  const fixturePath = path.join(__dirname, '../fixtures/avatar.jpg');

  await page.getByLabel('Profile photo').setInputFiles(fixturePath);

  // Confirm upload feedback
  await expect(page.getByText('Photo uploaded')).toBeVisible({ timeout: 10000 });
});

For hidden or visually replaced inputs (common with drag-and-drop upload UIs):

test('uploads via hidden input', async ({ page }) => {
  await page.goto('https://app.example.com/documents');

  const fileInput = page.locator('input[type="file"]');

  // Make the input interactable even if it's visually hidden
  await fileInput.evaluate((input) => {
    (input as HTMLInputElement).removeAttribute('style');
  });

  await fileInput.setInputFiles(path.join(__dirname, '../fixtures/contract.pdf'));
  await expect(page.getByText('contract.pdf')).toBeVisible();
});

For multiple file uploads:

await page.getByLabel('Attachments').setInputFiles([
  path.join(__dirname, '../fixtures/file1.pdf'),
  path.join(__dirname, '../fixtures/file2.pdf'),
]);

For uploading a file generated in memory without writing to disk:

await page.getByLabel('CSV import').setInputFiles({
  name: 'users.csv',
  mimeType: 'text/csv',
  buffer: Buffer.from('name,email\nAda,ada@example.com'),
});

Why it works

setInputFiles interacts directly with Chromium's file input mechanism via the Chrome DevTools Protocol, bypassing the OS file dialog completely. It sets the files property on the <input type="file"> element and dispatches change and input events, which is what the application code listens to. Using absolute paths via path.join(__dirname, ...) ensures the path resolves correctly regardless of which directory the test runner is invoked from. The in-memory buffer form avoids filesystem access entirely, which is useful in environments with restricted file I/O.

Tips

  • Keep test fixture files in a tests/fixtures/ directory committed to your repository — verify this directory is not excluded by .gitignore or CI checkout configuration.
  • If the upload component shows a preview or progress bar, use expect(page.getByText(filename)).toBeVisible() to confirm the upload was acknowledged before proceeding.
  • For upload size limits on your application, generate test fixtures that are clearly within the limit — CI failures from oversized fixtures are hard to diagnose.
  • Playwright's setInputFiles does not trigger drag events — if your upload UI requires a drag-and-drop interaction specifically (not a click), use page.dispatchEvent with dragstart, drop, and dragend events to simulate it.