How to Automate File Upload Testing
Automate file uploads by setting files on the page’s file input, submitting through the real UI, and asserting the result. Examples cover Playwright, Selenium, Cypress, and security cases.
To automate a browser file-upload test, set a fixture on the page’s input[type=file] through your browser automation framework, submit using the application’s normal UI, and assert a stable result such as an uploaded filename, success message, or record. This avoids trying to control the operating system’s file-picker dialog. Selenium uses sendKeys, Playwright uses setInputFiles, and Cypress uses selectFile.
Choosing a file is only one step of the test. A useful end-to-end test verifies the application’s response, including rejection behavior and, where applicable, multiple files and security rules.
1. Choose the upload behavior to verify
Start with the product’s documented upload rules and visible behavior. Keep fixtures in the test suite or generate them in memory; avoid relying on a developer’s Downloads folder or a machine-specific path. A typical test plan includes:
- Accepted file: Upload a small valid fixture, submit or wait for processing, then check an observable success state.
- Multiple files: Test only if the product supports it. Verify every expected file or the resulting count, not just the first filename.
- Empty selection: Submit without selecting a file and assert the app’s expected behavior.
- Rejected type: Upload a file outside the documented allowlist and check for a safe, useful rejection.
- Size boundary and interrupted processing: If the product defines a size limit or asynchronous scanning, test around the documented limit and assert the final state.
- Remote browser: Make sure the fixture reaches the browser session. A path on the test runner may not exist on the remote node.
The exact size limits, messages, and state names depend on the application. Assert what the product promises rather than assuming a generic error string.
2. Prepare a stable fixture and observable outcome
Use a small, deterministic fixture with a descriptive name, such as valid-report.pdf. For negative tests, create a deliberately disallowed fixture that is safe to use in your test environment. If your framework supports in-memory files, that can avoid filesystem setup and make test data explicit.
Find the real file input, which may be visually hidden behind a styled drop zone or button. Prefer a label, role, or other accessible locator when the markup supports it. If the app creates an input only after a click, wait for that interaction and then target the newly created input or file chooser.
After setting the file, follow the application’s regular submission path. Wait for the meaningful result, such as the uploaded filename or a completed record. Do not treat the input accepting a value, or a transient spinner appearing, as proof that the upload succeeded.
3. Playwright: set files with setInputFiles
Playwright’s locator.setInputFiles() sets files directly on a file input. The example below uses a fixture, submits the form, and asserts the displayed filename. Replace the selectors and expected message with the app’s actual accessible markup and outcome.
import { test, expect } from '@playwright/test';
import path from 'node:path';
test('uploads a report', async ({ page }) => {
await page.goto('http://localhost:3000/upload');
const fileInput = page.getByLabel('Choose a file');
await fileInput.setInputFiles(path.join(__dirname, 'fixtures', 'valid-report.pdf'));
await page.getByRole('button', { name: 'Upload' }).click();
await expect(page.getByText('valid-report.pdf')).toBeVisible();
});
When the input is created dynamically by a click, use the file chooser event and set its files:
const chooserPromise = page.waitForEvent('filechooser');
await page.getByRole('button', { name: 'Choose file' }).click();
const chooser = await chooserPromise;
await chooser.setFiles('tests/fixtures/valid-report.pdf');
For an in-memory fixture, pass a payload with a name, MIME type, and buffer:
await page.locator('input[type="file"]').setInputFiles({
name: 'valid-report.pdf',
mimeType: 'application/pdf',
buffer: Buffer.from('%PDF-1.4\nexample test fixture')
});
For multiple files, pass an array, but only when the input supports multiple selection. To clear the selection, pass an empty array:
const input = page.locator('input[type="file"]');
await input.setInputFiles([
'tests/fixtures/first.pdf',
'tests/fixtures/second.pdf'
]);
// Clear the selection when the test needs to return to the empty state.
await input.setInputFiles([]);
Playwright also documents directory selection through setInputFiles(). Use it only when directory upload is part of the product behavior and supported by the target browser and input.
4. Selenium WebDriver: send the file path
Selenium’s documented approach is to locate the file input and send it the full path. It does not automate the native file-picker dialog; setting the input path avoids opening that dialog. The Java example uses Selenium 4 and JUnit-style assertions. Adjust the wait condition and result selector to match the page.
import java.nio.file.Path;
import java.time.Duration;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
public class UploadTest {
public static void main(String[] args) {
WebDriver driver = new ChromeDriver();
try {
driver.get("http://localhost:3000/upload");
Path fixture = Path.of("tests", "fixtures", "valid-report.pdf")
.toAbsolutePath();
WebElement fileInput = driver.findElement(By.cssSelector("input[type='file']"));
fileInput.sendKeys(fixture.toString());
driver.findElement(By.cssSelector("button[type='submit']")).click();
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
WebElement uploadedName = wait.until(
ExpectedConditions.visibilityOfElementLocated(By.id("uploaded-file-name")));
if (!uploadedName.getText().contains("valid-report.pdf")) {
throw new AssertionError("Uploaded filename was not shown");
}
} finally {
driver.quit();
}
}
}
For remote sessions, check how the grid transfers a local file to the remote browser node. Selenium’s local path may refer to the runner, not the node. BrowserStack documents a workflow using Selenium’s LocalFileDetector for this case; follow the provider’s instructions for your session setup.
5. Cypress: select a fixture or drag it onto a drop zone
Cypress uses selectFile(). This example selects a fixture and checks the app’s resulting state:
describe('file upload', () => {
it('uploads a report', () => {
cy.visit('/upload');
cy.get('input[type="file"]').selectFile('cypress/fixtures/valid-report.pdf');
cy.contains('button', 'Upload').click();
cy.contains('valid-report.pdf').should('be.visible');
});
});
For a generated payload, provide file contents and metadata. The MIME type and filename should represent the case being tested; neither alone proves that the server validates the actual contents.
cy.get('input[type="file"]').selectFile({
contents: Cypress.Buffer.from('%PDF-1.4\nexample test fixture'),
fileName: 'valid-report.pdf',
mimeType: 'application/pdf',
lastModified: Date.now()
});
If the product’s real interaction is drag and drop, exercise the drop target:
cy.get('[data-testid="upload-drop-zone"]').selectFile(
'cypress/fixtures/valid-report.pdf',
{ action: 'drag-drop' }
);
Cypress notes that multiple file selection requires the input to have the multiple property. Hidden inputs may require { force: true }; use that deliberately because it bypasses normal actionability checks.
cy.get('input[type="file"][multiple]').selectFile([
'cypress/fixtures/first.pdf',
'cypress/fixtures/second.pdf'
]);
// Use force only when the hidden input is the intended target.
cy.get('input[type="file"]').selectFile(
'cypress/fixtures/valid-report.pdf',
{ force: true }
);
6. Check multiple files and HTML input behavior
The HTML multiple attribute permits selecting more than one file. If it is absent, a test that sends an array may fail or may not represent user behavior. Check both the markup and the product requirement before adding a multiple-file case. After submission, verify all files, their order if order matters, or the app’s reported count.
Some applications use a hidden file input behind a visible button or drop zone. Setting the input is appropriate for testing upload processing, but it may skip defects in the visible interaction itself. If the user-facing workflow is important, include a separate test of the button or drag-and-drop interaction. Keep that distinct from the core upload-processing assertion so failures are easier to diagnose.
For framework-specific details, consult the primary documentation: Selenium file upload, Playwright input and file chooser, Cypress selectFile, and MDN file input.
7. Test validation and upload security
Client-side checks improve feedback but are not a substitute for server-side validation. Use a test environment and the application’s rules to verify that unwanted types are rejected and handled safely. OWASP’s upload testing guidance calls out checking for reliance on JavaScript-only checks, the request’s Content-Type, or the filename extension alone. It also covers batch uploads, direct access to uploaded files, script or code handling, and file-path handling.
- Try an extension outside the allowlist and assert a clear rejection.
- Where safe and relevant, test whether changing the claimed MIME type or extension bypasses validation.
- Check that rejected files do not become publicly accessible or execute as active content.
- Test batch behavior if multiple uploads are supported, including how a mixed valid/invalid batch is handled.
- Verify filenames and paths are handled safely; do not assume a client-provided filename is trustworthy.
OWASP’s test objective is to verify that unwanted file types are rejected and handled safely. These checks should follow the application’s acceptance policy rather than assuming a universal list of safe formats. See the OWASP WSTG upload testing guidance.
8. Troubleshooting common failures
| Symptom | Likely cause | Fix |
|---|---|---|
| Native file dialog appears or automation hangs | The test clicks the picker button and tries to control the operating-system dialog. | Target the underlying input[type=file] with the framework API. For a dynamic Playwright input, wait for the file chooser event and set files through it. |
| File input locator is missing | The input is rendered after an interaction, is inside a frame, or the selector does not match the page. | Wait for the relevant UI state, inspect the page structure, and locate the actual input in the correct frame. Use an accessible label when available. |
| Fixture path works locally but fails in CI | The path is relative to a different working directory, or the fixture was not included in the CI checkout. | Resolve paths from the test file or project root and ensure the fixture is committed or generated in the job. |
| Remote browser reports that the file does not exist | The path exists on the runner but not on the remote browser node. | Use the remote provider’s file-transfer mechanism. BrowserStack’s documented Selenium workflow uses LocalFileDetector. |
| Multiple file selection fails | The input does not support multiple selection, or the app intentionally accepts one file at a time. | Check for the HTML multiple attribute and the product requirement; split the test into separate uploads if that is the intended flow. |
| Cypress says the input is not actionable | The input is hidden behind a styled control. | Prefer the real visible interaction when testing it. If deliberately setting the hidden input, use { force: true } and keep a separate interaction test. |
| Input accepts file but no success state appears | Selection was mistaken for upload completion, the form was not submitted, processing is asynchronous, or the server rejected the file. | Follow the app’s submit flow, wait for a stable completion or error state, and inspect the application response and logs. |
| Test passes despite invalid content | The test checks only the extension or client-side message, while server-side validation is absent or not exercised. | Use a safe negative fixture and assert server-observable rejection behavior in the test environment. Cover MIME and content checks according to the app’s policy. |
9. Reliability, performance, and cost
Deterministic fixtures and stable result assertions make upload tests easier to reproduce. Keep test files small unless the test specifically covers size limits or throughput. Avoid fixed sleeps where the framework can wait for a visible result or completed state; fixed delays slow the suite and can still be too short under load.
Upload time depends on fixture size, network conditions, server processing, and any scanning or conversion performed by the application. The research sources do not establish comparative performance figures for Selenium, Playwright, or Cypress, so choose based on the framework and language already used by the suite, plus requirements such as generated buffers, dynamic chooser handling, drag and drop, directory selection, accessibility locators, and remote-grid transfer.
Browser test cost comes from the infrastructure and runtime your team uses; no universal cost or benchmark follows from the framework APIs. Reduce unnecessary work by testing a small representative file in the happy path and reserving larger files and boundary cases for targeted tests. Do not remove important security or error-path coverage just to shorten the suite.
10. Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. It does not select or upload files through your application; it can capture a page for visual review or documentation around your upload workflow. Its one-request API returns an image or PDF, and the API parameters used by other screenshot APIs also work. See the ScreenshotNeo API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);
Before capture, ScreenshotNeo accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and billing status. Its MCP server gives AI agents tools to take screenshots, get page information, and capture PDFs. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots.
Sign up for ScreenshotNeo’s free 1,000 screenshots a month, with no card required.
11. FAQ
Can I automate the operating system’s file picker?
Usually, use the framework’s file-input API instead. Selenium explicitly documents setting the path on the input, and Playwright and Cypress provide equivalent file-selection APIs.
Should I test uploads through the UI or the API?
This guide covers browser UI behavior, including selection, submission, and visible results. API-level upload tests can complement it, but they do not verify the browser workflow.
Which browser automation framework should I choose?
Prefer the framework already used by the test suite unless a specific need—such as drag and drop, in-memory payloads, dynamic chooser handling, or remote file transfer—determines the choice.
Is a successful file selection a passing upload test?
No. Selection only sets the browser input. The test should exercise processing or submission and verify a meaningful application result.


