How to Test File Upload Flows (CSV, Images, Docs)
Short answer
Upload flows fail on MIME validation, size limits, async virus scan, and drag-drop libraries that hide native file inputs. Use Playwright locator.setInputFiles() on input[type=file], fixture files in repo with exact byte sizes, expect.poll probes on scan_status, and ai.act only when drag-drop cannot reach the input—never skip negative type/size specs.
Part of Testing Guides by integrations.
Who this is for
HR applications, CMS, insurance, healthcare, and admin tools accepting résumés, images, PDFs, or CSV imports—with client and server validation plus optional malware scanning.
Why testing uploads matters
- Security — executable masquerading as PDF; path traversal filenames
- UX — valid HEIC from mobile rejected without clear error
- Compliance — PII uploaded but scan pending blocks user with no feedback
- Data integrity — partial upload marked success
Complexity map
| Scenario | Edge case | Why tests break | Approach |
|---|---|---|---|
| Size limit | 11MB on 10MB max | Silent fail | Fixture exact size |
| MIME sniff | .exe renamed .pdf | Malware path | wrong.exe fixture |
| Scan pending | Async ClamAV | Assert too early | Poll scan_status probe |
| Drag-drop UI | No visible input | setInputFiles fails | Expose input or ai.act |
| Multiple files | Order matters | Wrong pairing | setInputFiles array |
| Progress UI | Slow network | Timeout | Mock upload API delay |
| Image preview | EXIF orientation | Wrong display | Probe stored metadata optional |
| Virus detected | Quarantine | User stuck | Assert error + probe quarantine |
| S3 direct upload | Presigned URL | UI bypass | Test presign route separately |
| Chunked upload | Resume | Partial blob | Probe final etag |
setInputFiles (Playwright)
From Playwright file upload docs:
import path from 'path';
const fixtures = path.join(__dirname, '../fixtures/uploads');
await page.getByLabel('Upload document').setInputFiles(
path.join(fixtures, 'valid.pdf'),
);
await expect.poll(async () => {
const res = await request.get(`/api/test/probe-upload?runId=${runId}`);
return (await res.json()).status;
}, { timeout: 30_000 }).toBe('ready');
Multiple files:
await input.setInputFiles([
path.join(fixtures, 'page1.pdf'),
path.join(fixtures, 'page2.pdf'),
]);
Fixture files to keep in repo
fixtures/uploads/
valid.pdf # small valid doc
valid.png # image path
too-large.bin # exactly max+1 bytes
wrong.exe # rejected MIME (rename test)
empty.pdf # edge case
Generate too-large.bin in test setup if git size concern:
fs.writeFileSync(tmpPath, Buffer.alloc(10 * 1024 * 1024 + 1));
Size limit negative
await input.setInputFiles(path.join(fixtures, 'too-large.bin'));
await expect(page.getByText(/file too large|max 10/i)).toBeVisible();
await expect.poll(() => probeUploadCount(runId)).toBe(0);
MIME rejection
await input.setInputFiles(path.join(fixtures, 'wrong.exe'));
await expect(page.getByText(/file type|not allowed/i)).toBeVisible();
await expect.poll(() => probeUploadCount(runId)).toBe(0);
Virus scan pending
await input.setInputFiles(path.join(fixtures, 'valid.pdf'));
await expect(page.getByText(/scanning|processing/i)).toBeVisible();
await expect.poll(() => probeScanStatus(runId)).toBe('clean');
await expect(page.getByText(/upload complete/i)).toBeVisible();
Stub scanner in test env to transition pending → clean deterministically.
Drag-drop libraries
Many UI kits hide <input type="file">. Options:
- Unhide input in test build —
data-testid="file-input" - Dispatch drop event — library-specific, brittle
ai.act('Drop valid.pdf onto upload zone')— last resort
Prefer (1) for CI stability.
Anti-patterns
| Anti-pattern | Why it fails | Better approach |
|---|---|---|
| Skip size negative | Prod rejects silently | too-large fixture |
| Assert toast only | File not stored | Probe upload row |
| Real EICAR in CI | Scanner infra varies | Stub infected flag |
| waitForTimeout after upload | Scan still pending | poll scan_status |
| Upload prod sample docs | PII leak | Synthetic fixtures |
Example scenario
Situation: Candidate uploads PDF résumé; server scans then attaches to application.
Expected outcome: File stored with scan_status clean and linked to application id.
Why UI-only automation breaks: Progress bar completes but probe shows scan pending or wrong application.
- Arrange: Seed application runId; stub scanner fast-path.
- Act: setInputFiles valid.pdf on upload input.
- Assert: Probe upload linked to application; scan_status clean.
TestChimp workflow: Track file_type × upload_outcome when HEIC uploads spike untested.
Same Arrange/Act/Assert pattern as expired-coupon checkout.
Connect scenarios to your QA workflow
Capture business rules in markdown test plans and enforce them with seed routes and probe Assert. Link SmartTests with // @Scenario: for requirement traceability. Use /testchimp test on PRs; /testchimp explore on SmartTest paths for non-functional gaps (ExploreChimp).
Related scenarios
- CSV import/export — structured uploads
- PDF downloads — outbound documents
- HR applications — résumé uploads
External references
- Playwright file upload
- Playwright file chooser — when click opens chooser
- OWASP file upload cheat sheet
Frequently asked questions
How do I upload files in Playwright?
Use locator.setInputFiles(path) on input[type=file]. Register path to fixtures in repo. Poll probe until upload and scan complete—do not assert on toast alone.
Drag-and-drop upload without visible input?
Expose hidden input with data-testid in test builds, or use file chooser event if click opens picker. ai.act is last resort for drop zones.
How do I test file size limits?
Fixture file exactly max+1 bytes. Assert UI error and probe upload count zero.
How do I test virus scan pending state?
Stub scanner in test env. Assert processing UI then poll probe scan_status until clean or infected.
Multiple file upload order?
setInputFiles with array of paths. Probe stored files count and order if product requires it.
Should I use real malware samples?
No—stub scanner returning infected for a dedicated fixture name. EICAR behavior varies by infra.
HEIC uploads popular in prod but untested?
TrueCoverage file_type × upload_outcome highlights gap. Add heic fixture and conversion path spec via /testchimp evolve.
Apply these patterns in your repo
Run `/testchimp init` to connect TestChimp to your repo, then `/testchimp test` on PRs to turn these patterns into maintained SmartTests. Use `/testchimp evolve` when you want to expand coverage as your app grows.