File Upload Testing Guide
How to Test File Upload with Playwright: setInputFiles, Drag and Drop, Chunked Uploads, and Every Edge Case
A scenario-by-scenario walkthrough of testing file upload forms with Playwright. From basic setInputFiles calls to drag-and-drop zones, progress bar assertions, chunked upload retries, file type validation, and max size error handling.
“Over 80% of web applications include at least one file upload form, yet file upload is consistently ranked among the top 10 most under-tested UI interactions according to the 2025 State of Testing Report by PractiTest.”
PractiTest State of Testing Report 2025
File Upload End-to-End Flow
1. Why Testing File Uploads Is Harder Than It Looks
File upload looks simple on the surface: the user picks a file, clicks submit, and the server stores it. In practice, almost every modern upload implementation introduces complexity that standard form-submission testing does not prepare you for. The native <input type="file"> element cannot be driven by typing into it. Playwright provides setInputFiles, but that only works when the input element is actually present in the DOM and not hidden behind a custom dropzone overlay. Drag-and-drop upload zones bypass the file input entirely, relying on the browser's DataTransfer API, which requires a completely different Playwright approach.
Modern upload widgets (Uppy, Filepond, Dropzone.js, react-dropzone) each handle file selection differently. Some create a hidden <input type="file"> that you can target with setInputFiles. Others intercept drag events on a container div and never render a file input at all. Some libraries chunk large files into 5MB segments and upload them in parallel with retry logic, meaning your test needs to wait for multiple network requests rather than a single POST. Progress bars animate asynchronously, and asserting their final state (100% or a success icon) requires waiting for conditions that are timing-sensitive and library-specific.
There are six structural reasons file upload testing breaks in real test suites. First, file inputs are often invisible or dynamically created, making locator strategies fragile. Second, drag-and-drop requires dispatching synthetic DataTransfer events that differ from how Playwright normally interacts with elements. Third, upload progress is asynchronous and race-prone; asserting too early catches the progress bar mid-animation. Fourth, chunked uploads make multiple sequential or parallel network requests, and a single chunk failure can trigger retry logic that changes the timing of the entire flow. Fifth, client-side validation (file type, file size, dimensions for images) fires before any network request, and testing these error paths requires generating or providing test files of specific types and sizes. Sixth, many production upload flows use presigned URLs (S3, GCS, Azure Blob) where the browser uploads directly to cloud storage, meaning your test must intercept or observe requests to a third-party domain.
File Upload Testing Challenge Map
File Selection
Hidden input or dropzone
Client Validation
Type, size, dimensions
Upload Request
Multipart or chunked
Progress Tracking
Async progress events
Retry on Failure
Chunk-level retries
Success State
Preview, URL, thumbnail
Presigned URL Upload Flow
Browser
Request presigned URL
App Server
Generate S3 presigned URL
Browser
PUT file to S3 directly
S3 Bucket
Store object
Browser
Confirm upload to app
A robust file upload test suite covers all of these surfaces. The sections below walk through each scenario with runnable Playwright TypeScript you can copy directly into your project.
2. Setting Up Your Test Environment
File upload tests need actual files to upload. You have two options: use real fixture files checked into your repo, or generate them programmatically at test time. Both approaches work; the best choice depends on what your upload form validates. If your form checks image dimensions or EXIF data, you need real image files. If it only checks MIME type and size, you can generate files with Buffer.alloc in your test setup.
File Upload Test Environment Checklist
- Create a test/fixtures/ directory with sample files (PNG, PDF, CSV, ZIP)
- Include one oversized file (or generate it) to test max size rejection
- Include one invalid-type file (e.g., .exe) to test type validation
- Configure Playwright to use a longer actionTimeout for upload scenarios
- Set up a test API endpoint or mock server that accepts uploads
- Add cleanup logic to delete uploaded files after each test run
- If testing presigned URLs, configure a local S3-compatible server (MinIO or LocalStack)
Test Fixture Generation
For tests that need specific file sizes, generate fixtures in your global setup. This avoids bloating your repository with large binary files and lets you create exact sizes for boundary testing.
Playwright Configuration for Upload Tests
Upload tests are inherently slower than form-fill tests because they involve file I/O and network transfer. Increase the action timeout and configure a test project specifically for upload scenarios. If your upload endpoint is slow in CI, consider mocking the server response for unit-level upload widget tests and using a real server only for integration tests.
3. Scenario: Basic File Upload with setInputFiles
The simplest and most common upload pattern is a standard HTML file input. Even when the visible UI is a styled button or a dropzone, many libraries render a hidden <input type="file"> element underneath. Playwright's setInputFilesmethod is the primary tool for this scenario. It accepts a file path (or an array of paths) and programmatically sets the input's value, triggering the same change event that a real user selection would fire.
Basic File Upload with setInputFiles
StraightforwardGoal
Upload a single file using a standard file input, submit the form, and confirm the server received the file and displays a success message.
Preconditions
- App running at
APP_BASE_URLwith an upload form at/upload - Test fixture file exists at
test/fixtures/sample.png - Server accepts PNG files and returns a JSON response with the stored file URL
Playwright Implementation
What to Assert Beyond the UI
- Intercept the upload request with
page.waitForResponseand verify the response status is 200 - Check that the response body contains a valid file URL pointing to your storage bucket
- Confirm the Content-Type of the request is
multipart/form-data
Basic Upload: Playwright vs Assrt
import { test, expect } from '@playwright/test';
import path from 'path';
test('basic file upload via setInputFiles', async ({ page }) => {
await page.goto('/upload');
const fileInput = page.locator('input[type="file"]');
await fileInput.setInputFiles(path.join(FIXTURES, 'sample.png'));
await expect(page.getByText('sample.png')).toBeVisible();
await page.getByRole('button', { name: /upload/i }).click();
await expect(page.getByText(/upload successful/i)).toBeVisible({
timeout: 15_000,
});
await expect(page.locator('img[alt="sample.png"]')).toBeVisible();
});4. Scenario: Drag and Drop Upload
Many modern upload UIs use a drag-and-drop zone instead of (or in addition to) a traditional file input. Libraries like react-dropzone, Uppy, and Dropzone.js render a target area that listens for dragenter, dragover, and dropevents. The challenge is that Playwright's setInputFiles only works on <input> elements. For dropzones that do not have a file input, you need to dispatch synthetic drag events with a DataTransfer payload.
There is a practical shortcut that works with most libraries. Even when the dropzone is the primary UI, many libraries still create a hidden file input in the DOM. Check with your browser DevTools: if there is an <input type="file"> inside the dropzone container, you can use setInputFiles on it directly. If there truly is no input element, you need the DataTransfer approach shown below.
Drag and Drop Upload Zone
ComplexGoal
Upload a file by dispatching drag-and-drop events on a dropzone that has no underlying file input element. Confirm the dropzone visual state changes during hover, and that the upload completes successfully.
Playwright Implementation
Hidden Input Shortcut
Before writing the full DataTransfer approach, check if the dropzone library creates a hidden input. This shortcut is much simpler and more reliable.
5. Scenario: Multi-File Upload with Progress Bars
Multi-file uploads add two layers of complexity. First, the file input must have the multiple attribute, and setInputFiles must receive an array of paths. Second, each file typically gets its own progress bar, and your test needs to wait for all of them to reach 100% before asserting the final success state. Asserting too early will catch a progress bar mid-animation and produce a flaky test.
Multi-File Upload with Progress Bars
ModerateGoal
Upload three files simultaneously, confirm individual progress bars appear for each file, wait for all uploads to complete, and verify the final success state shows all three files.
Playwright Implementation
Multi-File Upload: Playwright vs Assrt
import { test, expect } from '@playwright/test';
import path from 'path';
test('multi-file upload with progress', async ({ page }) => {
await page.goto('/upload');
const fileInput = page.locator('input[type="file"][multiple]');
await fileInput.setInputFiles([
path.join(FIXTURES, 'sample.png'),
path.join(FIXTURES, 'document.pdf'),
path.join(FIXTURES, 'data.csv'),
]);
await page.getByRole('button', { name: /upload all/i }).click();
const progressBars = page.locator('[role="progressbar"]');
await expect(progressBars).toHaveCount(3);
for (const bar of await progressBars.all()) {
await expect(bar).toHaveAttribute('aria-valuenow', '100', {
timeout: 30_000,
});
}
await expect(page.getByText(/3 files uploaded/i)).toBeVisible();
});6. Scenario: Chunked Upload with Retry Logic
Large file uploads in production rarely use a single POST request. Libraries like tus, Uppy with the tus plugin, and custom implementations split files into chunks (typically 5MB each) and upload them sequentially or in parallel. If a chunk fails due to a network interruption, the library retries that specific chunk without re-uploading the entire file. Testing this flow requires intercepting individual chunk requests, simulating failures, and verifying that the retry mechanism works correctly.
The key Playwright technique is page.route, which lets you intercept network requests and selectively abort, delay, or modify them. By aborting a specific chunk request on its first attempt and allowing it on the retry, you can verify the entire retry pipeline without needing an unreliable network.
Chunked Upload with Retry on Failure
ComplexGoal
Upload a large file that gets split into chunks, simulate a network failure on one chunk, verify the library retries the failed chunk, and confirm the upload completes successfully after the retry.
Playwright Implementation
7. Scenario: File Type Validation and Max Size Errors
Client-side validation is the first line of defense in any upload form. It prevents unnecessary network requests for files that the server would reject anyway. Testing these error paths is critical because they are the most visible user-facing feedback in the upload flow: a clear error message for a rejected file is better than a cryptic 413 or 422 from the server.
There are two categories of client-side validation to test. File type validation checks the file extension, MIME type, or both. Most upload widgets use the accept attribute on the file input to restrict the file picker, but this only filters the file dialog; it does not prevent programmatic selection of invalid files. Your test should verify that the UI shows an error when an invalid file type is selected. File size validation checks File.size against a configured maximum and displays an error before any upload begins.
File Type and Size Validation Errors
ModerateGoal
Attempt to upload a file with an invalid type and verify the error message. Then attempt to upload an oversized file and verify the size limit error. Confirm that no network request is made in either case.
Playwright Implementation
Validation Errors: Playwright vs Assrt
import { test, expect } from '@playwright/test';
import path from 'path';
test('rejects invalid file type', async ({ page }) => {
const uploadRequests: string[] = [];
page.on('request', (req) => {
if (req.url().includes('/upload')) uploadRequests.push(req.url());
});
await page.goto('/upload');
const fileInput = page.locator('input[type="file"]');
await fileInput.setInputFiles(path.join(FIXTURES, 'malware.exe'));
await expect(
page.getByText(/file type not allowed/i)
).toBeVisible();
await expect(
page.getByRole('button', { name: /upload/i })
).toBeDisabled();
expect(uploadRequests).toHaveLength(0);
});8. Scenario: Direct-to-S3 Presigned URL Uploads
In production architectures, the browser often uploads files directly to cloud storage (S3, Google Cloud Storage, Azure Blob) using a presigned URL. The flow is: the browser requests a presigned URL from your API, your API generates it with the appropriate permissions and expiration, and the browser PUTs the file directly to the storage endpoint. This means your Playwright test will observe network requests to a third-party domain (e.g., your-bucket.s3.amazonaws.com), not your own API.
Testing this flow in CI requires either a real S3 bucket (expensive and slow), a local S3-compatible server like MinIO or LocalStack, or Playwright route interception to mock the presigned URL response. The route interception approach is the fastest for CI and does not require any infrastructure.
Presigned URL Upload to S3
ComplexGoal
Intercept the presigned URL generation request, verify the browser uploads directly to the storage endpoint, and confirm the application registers the upload as complete.
Playwright Implementation
9. Common Pitfalls That Break File Upload Test Suites
File upload tests are among the flakiest in any test suite. The following pitfalls come from real issues reported in the Playwright GitHub repository, Stack Overflow threads, and upload library issue trackers. Each one has caused test failures in production CI pipelines.
Using setInputFiles on a Non-Existent Input
The most common error is calling setInputFileson a locator that resolves to zero elements. This happens when the file input is lazily created (only rendered after clicking an “Add File” button) or when the dropzone library creates the input asynchronously after mount. Always wait for the input to be attached before calling setInputFiles. Use await fileInput.waitFor() before the call.
Asserting Progress Bars Too Early
Progress bar animations are inherently asynchronous. A test that checks aria-valuenow immediately after clicking upload will often catch a value of 0 or an intermediate percentage. Use Playwright's toHaveAttribute with a timeout, which polls until the condition is met. Never use a fixed waitForTimeout to wait for progress completion.
Fixture Files Not Found in CI
Tests pass locally but fail in CI because fixture files are referenced with relative paths that resolve differently depending on the working directory. Always use path.join(__dirname, ...) for fixture paths, and verify the fixtures directory exists in your CI build step. Better yet, generate fixtures in your global setup so they are always present regardless of which files are checked into the repository.
File Input Cleared After First Selection
Some upload widgets clear the file input's value after reading the selected file into memory. This means a second call to setInputFilesmay not trigger a new change event because the widget is no longer listening. If you need to upload files sequentially (one after another, not all at once), click the “Add Another” button between uploads to ensure the widget creates a fresh input listener.
CORS Errors on Presigned URL Uploads in CI
When testing presigned URL uploads against a local S3 alternative (MinIO or LocalStack), CORS must be configured on the bucket. The browser will send a preflight OPTIONS request before the PUT, and if the CORS policy does not allow the origin, the upload fails silently. The Playwright test sees a generic network error with no helpful message. Configure your local S3 bucket to allow all origins for the test environment, or use page.route to mock the PUT entirely.
File Upload Test Anti-Patterns
- Using waitForTimeout instead of polling assertions for progress bars
- Hardcoding relative fixture paths that break in CI
- Calling setInputFiles before the input is attached to the DOM
- Not tracking network requests when testing client-side validation
- Testing large file uploads without mocking the server response time
- Ignoring CORS configuration for presigned URL tests in CI
- Assuming the dropzone has a hidden input without checking
- Not cleaning up uploaded files between test runs
10. Writing File Upload Tests in Plain English with Assrt
Every scenario above involves Playwright-specific APIs: setInputFiles for standard inputs, DataTransfer event dispatch for drag and drop, page.route for intercepting chunked uploads, and polling assertions for progress bars. These are powerful primitives, but they require deep knowledge of both Playwright and the specific upload library your application uses. When your team switches from Dropzone.js to Uppy, or from multipart POST to tus chunked uploads, every test file needs rewriting.
Assrt abstracts the upload mechanism entirely. You describe what you want to test (attach a file, verify progress, check validation errors) and Assrt resolves the implementation at runtime. It detects whether the upload widget uses a hidden input, a dropzone, or a custom file picker and selects the right strategy automatically.
The drag-and-drop scenario from Section 4 demonstrates this well. In raw Playwright, you need to know that the dropzone has no hidden input, manually construct a DataTransfer object, dispatch three separate drag events in the correct order, and handle the buffer-to-Uint8Array conversion. In Assrt, you describe the intent and the framework resolves the mechanism.
Assrt compiles each scenario block into the same Playwright TypeScript you saw in the preceding sections, committed to your repo as real tests you can read, run, and modify. When your team migrates from react-dropzone to Uppy, or from multipart uploads to tus chunked uploads, Assrt detects the new DOM structure and regenerates the locators and upload strategy. Your scenario files stay untouched.
Start with the basic setInputFiles scenario. Once it passes in CI, add the drag-and-drop test, then multi-file progress, then validation errors, then the chunked retry test. In a single afternoon you can build a comprehensive file upload test suite that covers every edge case most teams never get around to testing by hand.
Related Guides
How to Test Figma File Actions
A practical guide to testing Figma file actions with Playwright. Covers canvas rendering,...
How to Test S3 Presigned URL Upload
A practical guide to testing S3 presigned URL uploads with Playwright. Covers CORS...
How to Fix Flaky Tests
Root causes and proven fixes for unreliable tests.
Ready to automate your testing?
Assrt discovers test scenarios, writes Playwright tests from plain English, and self-heals when your UI changes.