Handling Multiple File Uploads in Playwright
Bypass native OS file dialogs by passing an array of absolute paths directly to setInputFiles. Strict async/await execution is mandatory for deterministic test runs. This approach guarantees reliable CI/CD execution without relying on fragile UI interactions.
Root Cause: Why Multi-File Uploads Fail in Playwright
Multi-file uploads frequently break due to synchronous array mapping, missing explicit waits for input visibility, and incorrect Node.js path resolution. When browsers re-render file inputs after selection, detached element exceptions immediately halt execution.
Understanding the underlying mechanics of File Uploads & Downloads clarifies why bypassing the OS dialog is mandatory for headless execution. Framework-level synchronization must replace manual timing assumptions.
Diagnosing Path Resolution and Visibility Timeouts
Relative paths fail unpredictably across different execution contexts and CI runners. Always resolve to absolute paths using path.resolve().
Before invoking setInputFiles, enforce locator.waitFor({ state: 'visible' }). This prevents ElementNotAttachedError and guarantees the DOM node accepts the payload. Hardcoded sleeps mask synchronization gaps and must be avoided.
Step-by-Step Implementation for Reliable Multi-Upload
Construct a deterministic workflow: resolve file paths, verify input attachment, execute the upload, and assert completion. This sequence aligns with Advanced Interactions & Test Assertions standards for state verification.
Each step must await its predecessor to maintain atomicity. Parallel execution should only apply to independent operations like path resolution.
Minimal Reproducible Example (Async/Await)
The following snippet demonstrates parallel path resolution, explicit visibility waits, and atomic file assignment. Note the strict async/await chaining and avoidance of deprecated elementHandle methods.
const { test, expect } = require('@playwright/test');
const path = require('path');
test('upload multiple files reliably', async ({ page }) => {
await page.goto('/upload-form');
const fileInput = page.locator('input[type="file"]');
await fileInput.waitFor({ state: 'visible' });
const filePaths = [
path.resolve(__dirname, 'fixtures', 'doc1.pdf'),
path.resolve(__dirname, 'fixtures', 'img2.png')
];
await fileInput.setInputFiles(filePaths);
await expect(page.locator('.upload-status')).toContainText('2 files ready');
});
Handling Dynamic File Inputs and Race Conditions
Modern SPAs often replace native <input type="file"> with custom UI components or re-render the DOM immediately after selection. If the input detaches, Playwright throws synchronization errors.
Mitigate this by waiting for state: 'attached' before interaction. For heavily dynamic forms, implement a lightweight retry wrapper around the upload action. Always verify the network request completes before proceeding to subsequent test steps.
Validation and Error Handling Patterns
Relying solely on UI feedback introduces flakiness. Validate uploads by intercepting the underlying HTTP request or asserting DOM state changes post-upload. Multipart/form-data boundaries require precise payload verification.
Implement fallback assertions that check both the response status and the rendered file list. This ensures tests remain stable across varying network latencies.
Asserting Upload Completion and File Metadata
Network interception provides deterministic verification of server-side processing. The example below captures the upload response, parses the JSON payload, and asserts the expected file count. This pattern eliminates reliance on arbitrary UI delays.
test('verify multi-upload via network interception', async ({ page }) => {
const uploadPromise = page.waitForResponse(resp =>
resp.url().includes('/api/upload') && resp.status() === 200
);
await page.locator('input[type="file"]').setInputFiles([
'path/to/file1.txt',
'path/to/file2.txt'
]);
const response = await uploadPromise;
const body = await response.json();
expect(body.uploadedCount).toBe(2);
});