Skip to main content

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​

ScenarioEdge caseWhy tests breakApproach
Size limit11MB on 10MB maxSilent failFixture exact size
MIME sniff.exe renamed .pdfMalware pathwrong.exe fixture
Scan pendingAsync ClamAVAssert too earlyPoll scan_status probe
Drag-drop UINo visible inputsetInputFiles failsExpose input or ai.act
Multiple filesOrder mattersWrong pairingsetInputFiles array
Progress UISlow networkTimeoutMock upload API delay
Image previewEXIF orientationWrong displayProbe stored metadata optional
Virus detectedQuarantineUser stuckAssert error + probe quarantine
S3 direct uploadPresigned URLUI bypassTest presign route separately
Chunked uploadResumePartial blobProbe 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:

  1. Unhide input in test build — data-testid="file-input"
  2. Dispatch drop event — library-specific, brittle
  3. ai.act('Drop valid.pdf onto upload zone') — last resort

Prefer (1) for CI stability.

Anti-patterns​

Anti-patternWhy it failsBetter approach
Skip size negativeProd rejects silentlytoo-large fixture
Assert toast onlyFile not storedProbe upload row
Real EICAR in CIScanner infra variesStub infected flag
waitForTimeout after uploadScan still pendingpoll scan_status
Upload prod sample docsPII leakSynthetic 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.

  1. Arrange: Seed application runId; stub scanner fast-path.
  2. Act: setInputFiles valid.pdf on upload input.
  3. 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 annotations for requirement traceability. Use /testchimp test on PRs; /testchimp explore on SmartTest paths for non-functional gaps (ExploreChimp).

External references​

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.

Start free on TestChimp · Book a demo