How to fix Playwright file upload failures with `setInputFiles`?
PlaywrightFile 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.