How to Perform a File Upload Test with Selenium and Python

How to Perform a File Upload Test with Selenium and Python

Want to reliably test file uploads in your web app? In Selenium, the trick is to avoid the OS file picker. Instead, send an absolute file path directly to the underlying <input type="file">. This guide gives you production-ready patterns for local, headless, and remote runs (Selenium Grid/CI), plus solutions for hidden inputs, custom dropzones, multiple files, and flaky timing.


Table of Contents

  • Prerequisites
  • The core idea
  • Complete example with waits & assertions
  • Multiple files
  • Hidden or custom-styled inputs
  • Custom “drag & drop” widgets
  • Remote WebDriver / Grid (LocalFileDetector)
  • Pytest pattern (fixtures, tmp files)
  • CI, Docker, and path gotchas
  • Troubleshooting & FAQ
  • Copy-paste helper

1) Prerequisites

  • Python 3.9+ (3.8 works too)
  • Selenium 4.x
pip install selenium==4.*

Tip: With Selenium 4.6+, Selenium Manager can auto-resolve drivers when you instantiate webdriver.Chrome() without manually downloading chromedriver.

2) The core idea (works headless, cross-platform)

If your page contains:

<input id="upload" type="file" />

Upload like this:

from pathlib import Path
from selenium import webdriver
from selenium.webdriver.common.by import By

driver = webdriver.Chrome()
driver.get("https://example.com/page-with-upload")

file_input = driver.find_element(By.ID, "upload")
file_path = Path("tests/assets/sample.pdf").resolve()  # must be absolute
file_input.send_keys(str(file_path))

Selenium bypasses the OS file dialog by talking directly to the element.

3) Complete example with waits & assertions

Use explicit waits and verify that the page acknowledges the upload (text appears, preview shows, etc.).

from pathlib import Path
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

def test_single_file_upload():
    driver = webdriver.Chrome()
    wait = WebDriverWait(driver, 15)

    try:
        driver.get("https://your-app.test/uploads")  # replace with your route

        # 1) Wait until the file input is present & enabled
        file_input = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "input[type='file']")))
        wait.until(lambda d: file_input.is_enabled())

        # 2) Send absolute path
        file_path = Path("tests/assets/sample.pdf").resolve()
        file_input.send_keys(str(file_path))

        # 3) Click upload button if needed
        upload_btn = driver.find_element(By.CSS_SELECTOR, "[data-test='upload-btn']")
        upload_btn.click()

        # 4) Assert server/UI acknowledgment (filename appears)
        success_banner = wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, ".upload-success")))
        assert "sample.pdf" in success_banner.text

        # or: preview thumbnail appears
        preview = wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, "[data-test='file-preview']")))
        assert preview.is_displayed()

    finally:
        driver.quit()

Why this is stable: Uses WebDriverWait instead of arbitrary sleeps and asserts a real UI signal from your app.

4) Uploading multiple files

Inputs that include multiple let you send more than one file. Join file paths with a newline ( ).

<input id="upload" type="file" multiple />
from pathlib import Path
from selenium.webdriver.common.by import By

paths = [
    Path("tests/assets/a.png").resolve(),
    Path("tests/assets/b.jpg").resolve(),
    Path("tests/assets/c.pdf").resolve(),
]
driver.find_element(By.ID, "upload").send_keys("
".join(map(str, paths)))

5) Hidden or custom-styled inputs

Designers often hide the real file input and show a pretty button. Common cases:

Case A: Input is off-screen but in DOM

Visibility isn’t required for <input type="file"> in most drivers—just send keys.

file_input = driver.find_element(By.CSS_SELECTOR, "input[type='file']")
file_input.send_keys(str(Path("tests/assets/sample.pdf").resolve()))

Case B: Input is display:none or visibility:hidden

Some drivers may block send_keys. Two options:

  • Click the associated label to trigger the native input, then reveal it and send keys.
  • Temporarily unhide via JS and send keys.
file_input = driver.find_element(By.ID, "fileInput")
driver.execute_script("arguments[0].removeAttribute('style');", file_input)
file_input.send_keys(str(Path('tests/assets/sample.pdf').resolve()))

6) Custom drag & drop widgets (Dropzone, Uppy, FilePond…)

Most libraries still keep a hidden <input type="file"> inside the widget. Find it and send_keys.

dropzone = driver.find_element(By.CSS_SELECTOR, ".dropzone")  # visual wrapper
hidden_input = dropzone.find_element(By.CSS_SELECTOR, "input[type='file']")
hidden_input.send_keys(str(Path("tests/assets/sample.pdf").resolve()))

If there’s truly no input, JS drag-and-drop can trigger UI logic but won’t transfer real local file bytes. Ask frontend to expose a testable file input.

7) Remote WebDriver / Selenium Grid (LocalFileDetector)

When tests run remotely, your code and browser are on different machines. Enable a file detector so Selenium uploads from your test host to the remote node.

from pathlib import Path
from selenium.webdriver import Remote
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.remote.file_detector import LocalFileDetector

options = Options()
driver = Remote(
    command_executor="http://grid:4444/wd/hub",
    options=options,
)

# Critical: enable LocalFileDetector so send_keys uploads from your local FS
driver.file_detector = LocalFileDetector()

file_input = driver.find_element(By.CSS_SELECTOR, "input[type='file']")
file_input.send_keys(str(Path("tests/assets/sample.pdf").resolve()))

8) Pytest pattern (fixtures + tmp files)

Keep tests independent and reproducible with temporary files and reusable drivers.

# conftest.py
import pytest
from selenium import webdriver
from selenium.webdriver.chrome.options import Options

@pytest.fixture
def driver():
    opts = Options()
    # opts.add_argument("--headless=new")  # enable in CI
    d = webdriver.Chrome(options=opts)
    yield d
    d.quit()
# test_upload.py
from pathlib import Path
import os

def create_temp_file(tmp_path, name="sample.txt", size=64):
    p = tmp_path / name
    with open(p, "wb") as f:
        f.write(os.urandom(size))
    return p

def test_upload_flow(driver, tmp_path):
    driver.get("https://your-app.test/uploads")

    file_input = driver.find_element("css selector", "input[type='file']")
    temp_file = create_temp_file(tmp_path, "upload.bin", 1024)
    file_input.send_keys(str(temp_file.resolve()))

    driver.find_element("css selector", "[data-test='upload-btn']").click()

    from selenium.webdriver.support.ui import WebDriverWait
    from selenium.webdriver.support import expected_conditions as EC
    from selenium.webdriver.common.by import By
    WebDriverWait(driver, 10).until(
        EC.text_to_be_present_in_element(
            (By.CSS_SELECTOR, ".uploaded-file-name"), "upload.bin"
        )
    )

9) CI, Docker, and path gotchas

  • Always use absolute paths (Path(...).resolve()).
  • Headless Chrome: file upload via send_keys works fine.
  • Docker: mount the directory with assets into the container and use the mounted path inside tests.
  • Remote nodes: set driver.file_detector = LocalFileDetector().
  • Windows vs POSIX: use pathlib to keep paths portable.

10) Troubleshooting & FAQ

Q: Clicking a button opens the OS file chooser. How can I select a file?
A: Don’t. Locate the underlying <input type="file"> and call .send_keys(absolute_path). Avoid OS dialogs entirely.

Q: The input is hidden and send_keys errors out.
A: Reveal the input with JS or click the associated label. Or target the real input nested in the widget.

Q: Upload works locally but fails on Grid/CI.
A: Ensure LocalFileDetector is enabled and that the CI job can access the file path (mounted volume, correct repo path).

Q: How do I upload very large files?
A: Increase server/timeouts as needed. For stress tests, generate a large temporary file inside the test.

Copy-paste starter: minimal, robust upload helper

from pathlib import Path
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

def upload_file(driver, selector, local_path, timeout=15):
    # Upload a file to an <input type='file'> identified by CSS selector.
    p = Path(local_path).resolve()
    if not p.exists():
        raise FileNotFoundError(f"File not found: {p}")

    wait = WebDriverWait(driver, timeout)
    input_el = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, selector)))
    # If truly hidden and blocked, temporarily unhide:
    try:
        input_el.send_keys(str(p))
    except Exception:
        driver.execute_script("arguments[0].style.display='block';", input_el)
        input_el.send_keys(str(p))
    return p.name

Usage

driver.get("https://your-app.test/uploads")
uploaded = upload_file(driver, "input[type='file']", "tests/assets/sample.pdf")
driver.find_element(By.CSS_SELECTOR, "[data-test='upload-btn']").click()

Key takeaways: don’t automate the OS dialog; do send_keys(absolute_path) to the real file input; use explicit waits; enable LocalFileDetector in remote runs; handle hidden inputs by revealing or targeting the nested input of custom widgets.

Scroll to Top