File upload testing: the whole surface, in four checklists

Four manual checklists for file-upload testing: input paths, validation, progress, and errors. Monito can't upload files yet, but it can check the surrounding UI.

playbookfile-uploadno-codeqa

A file input looks like the least interesting element on the page — a button, a dialog, a spinner, done. It is, in practice, one of the most bug-dense widgets you ship, because the moment a user hands you a file you've taken on a small distributed system: a drag target with its own event model, client-side validation you can't trust, an upload that streams over a flaky network, a progress bar that has to track it, an abort path, and a server that has to re-validate everything the browser already "checked" because the browser's checks are a courtesy, not a guarantee.

Most teams test the happy path — pick a normal JPEG, watch it upload, see the thumbnail — and ship. The bugs live in the other ninety percent: the dragged folder, the file renamed invoice.pdf.exe, the upload cancelled at 80%, the 12 MB image that the client waved through and the server rejected with a raw 500. This is the playbook for covering the whole surface, in four manual checklists to work through before deploying changes to the uploader.

What "testing file upload" actually has to cover

The button is the part that works. Here's the surface that doesn't, roughly in order of how often it breaks:

  1. Both input paths. The hidden <input type="file"> and the drag-and-drop target are two different code paths that are supposed to end in the same place. They rarely get equal testing. Drag-drop is where the fun edge cases live: dropping a folder, dropping multiple files when the UI expects one, dropping outside the target, dropping a file type the click-picker would have filtered out.
  2. Validation the client does — and the server redoes. Size limits, type limits, filename rules. The rule that matters is whether the client and server agree, because a client that allows what the server rejects produces the worst failure mode in this whole list: a confident upload that 500s at the end.
  3. Type validation that isn't just the extension. OWASP's File Upload Cheat Sheet is blunt about this: the Content-Type sent by the browser "is provided by the user, and as such cannot be trusted, as it is trivial to spoof." Real type validation checks the file's signature — its magic bytes — not the extension and not the header. The classic bypass is the double extension (shell.php.jpg) or the null-byte trick the cheat sheet calls out; a test pass that only renames the extension and calls it covered is testing nothing.
  4. The progress bar's honesty. Does it track real bytes, or is it a CSS animation that hits 100% and then waits? On a slow connection the difference is the entire user experience.
  5. Abort and resume. Cancel mid-upload: does the request actually abort, or does the UI lie while the bytes keep flowing? Start again: clean, or duplicate?
  6. Post-upload state. The file appears in the list, the thumbnail renders, a reload still shows it, and a failed upload leaves no ghost row behind.

A scripted suite tends to cover items 1 and 6 with the click-picker only, mock the network, and never touch drag-drop or the size/type boundary at all — because driving a native file dialog and a real multi-megabyte upload from Playwright is genuinely annoying. The checklists below are for a person testing against a real backend, or for a tool that supports file uploads.

A note on test files before the checklists: prepare them yourself — a small valid image, files just under and over the size limit, a non-image renamed to .png, and a folder. Monito's agent can't upload files yet, create test files, or download files. These upload checklists aren't Monito scenarios. Monito can check the surrounding UI: visible upload instructions, stated limits, and previews or file lists you have already populated. There's a prompt for that at the end.

Checklist 1: both input paths, including the dragged folder

Test the file uploader on https://staging.yourapp.com/upload.

1. Choose a prepared valid image (PNG or JPEG) and upload it using the
   normal file picker. Confirm it uploads, a thumbnail/preview appears,
   and the file shows in the uploaded list.
2. Now drag-and-drop the same file onto the drop zone instead of using
   the picker. Confirm the result is identical to step 1.
3. Drag-and-drop a FOLDER (not a file) onto the drop zone. Report
   exactly what happens — is it rejected with a clear message, silently
   ignored, or does it error?
4. Drag-and-drop three files at once. If the UI is single-file, report
   whether it takes the first, rejects all, or breaks.

Throughout: report any console errors and any network request that
returns 4xx/5xx without a corresponding visible message in the UI.

Step 2 is the path-parity check most suites skip. Step 3 is the one that finds real bugs — a drop handler that calls readAsDataURL on a directory entry and throws an uncaught error nobody styled. Record what the page shows: "silently ignored" and "errored in a way the user can see" are different outcomes, and your report should distinguish them.

Checklist 2: the size and type boundary, both sides of the wire

This is the checklist that earns its keep. Use the files you prepared: one just under the limit, an oversized one, a wrong-type one, and a disguised one.

On https://staging.yourapp.com/upload, test validation boundaries.
Assume the stated limit is 5 MB and the allowed types are PNG/JPEG —
adjust to whatever the UI actually states.

1. Upload a prepared image just UNDER the size limit. Expect success.
2. Upload a prepared file just OVER the size limit. Expect a clear,
   specific error ("max 5 MB"), NOT a generic failure or a silent no-op.
3. Upload a prepared .txt or .pdf when only images are allowed.
   Confirm it's rejected with a message a human can act on.
4. Take a plain .txt file, RENAME it to end in .png, and upload it.
   Report whether it's accepted. If accepted, this is a finding: type
   was validated by extension, not by file signature.
5. For every rejection above, watch the network tab: did the file get
   rejected in the browser, or did it upload fully and get rejected by
   the server? Report which, for each case.

Flag any case where the client accepts a file the server then rejects.

Step 4 is the spoofed-type check straight out of OWASP's guidance. Checking where the rejection happened (step 5) exposes the boundary a mocked-network test removes. A client-only size check that lets a 50 MB file upload over a hotel Wi-Fi connection before failing is a real bug with a real support ticket attached, and it passes every test that stubs the request.

Checklist 3: progress, abort, and the network throttle

On https://staging.yourapp.com/upload, test the progress and abort
behavior. If you can throttle the network to a slow connection, do so
first so the upload takes several seconds.

1. Start uploading a large (but allowed) file. Watch the progress
   indicator: does it advance gradually in step with the upload, or
   does it jump to 100% and then hang while the request finishes?
2. Mid-upload (around 50%), click cancel/abort. Confirm: does the UI
   return to a clean ready state, AND does the network request actually
   get cancelled? Report whether the request kept running after the UI
   said it stopped.
3. After cancelling, immediately upload a different file. Confirm it
   succeeds cleanly and doesn't carry over state from the cancelled one
   (no duplicate, no stuck spinner, no leftover progress bar).
4. Start an upload and let it complete. Reload the page. Confirm the
   uploaded file is still listed (it persisted server-side, not just
   in component state).

Report all console errors, including ones that didn't visibly break
anything.

Step 2 separates a real abort from a cosmetic one — a UI that clears the progress bar while the XMLHttpRequest keeps streaming to the server is one of those bugs that's invisible until your storage bill or your moderation queue notices. Watching the browser's network log catches the divergence that a screenshot never would. (Reading the network log even when a run passes is the habit that pays off across all of these flows.)

Checklist 4: the error messages, judged like a human

On https://staging.yourapp.com/upload, focus only on error quality.
For each failure case, capture the exact message shown and judge
whether a non-technical user could understand and recover from it.

1. Oversized file: is the message specific about the limit?
2. Wrong type: does it say which types ARE allowed?
3. Trigger a network failure mid-upload (or upload something the
   server will reject). Is the error recoverable — can the user retry
   without reloading — or is it a dead end?
4. Look for the silent failures: any case where the file simply
   doesn't appear and no message explains why. Those are the worst,
   because the user thinks it worked.

For each, rate the message as clear / confusing / missing, and quote it.

This checklist needs a human judgment: "could a confused human recover from this message?" It's also where the highest-leverage bugs hide: not the crash, but the upload that quietly does nothing while the user waits, reloads, and gives up. Read the screen the way the person filing your next support ticket will.

Reading the results

For the manual upload checks, save screenshots, console output, and the browser's network log with the actual multipart request and its status. The network log is the document to read — it shows the "client accepted, server rejected" mismatch, and it's the difference between "the test passed" and "the test passed but the server quietly 500s on anything over 8 MB."

For the surrounding UI checks Monito can run, each Monito Session includes screenshots, console and network logs, and bug reports when it finds bugs. Runs without a login record a video; runs using a login get a masked checkpoint replay, not continuous video. Because Monito's agent can't upload files yet, these runs won't prove that an upload succeeds or that the server rejects a bad file.

The one to run right now

If you do a single manual check today, use checklist 2: upload the disguised file and the file just over the limit, then inspect the server response. Monito's agent can't upload files yet, but you can give it this separate check of the surrounding UI. First, populate the staging page with a known image named sample.png yourself:

On https://staging.yourapp.com/upload, check the upload page without
creating, uploading, or downloading any files.

1. Confirm the upload control has a visible label and the page states
   the allowed file types and size limit. Quote the instructions.
2. Confirm the already-uploaded sample.png appears in the file list
   and its preview renders.
3. Reload the page and confirm sample.png and its preview still appear.

Capture screenshots and report console errors or failed network
requests encountered during these checks.

Save this UI check as a Test Scenario, pin it to your staging Project, and wire it into CI against every preview deploy that touches the uploader. A test run costs 1 credit, and your first run is free. When Monito is the one that fails, you don't pay. Use this run to check the instructions and existing-file display; keep the actual upload, validation, progress, and abort checks in your manual pass or an upload-capable test tool.