A Selenium test asserts on an exported CSV right after clicking Download and fails intermittently; why, and what fixes it?
answer
- click returns before the bytes land
- The driver models pages, not files
- Look for a partial file first
- Two browsers, two partial suffixes
- Clear the folder before the click
basics
~20 sWebDriver has no download-finished signal, so click returns while bytes are still arriving. Poll the download directory until the final filename exists and no partial file remains, Chrome writing .crdownload and Firefox .part while a transfer is in flight.
solid answer
~40 s`click()` returns once the click has been dispatched, not when the transfer ends, and the WebDriver protocol has no download event to wait on — the browser is writing to a filesystem the driver does not model. Meanwhile the file you are looking for does not exist yet under its final name: Chrome writes a `.crdownload` placeholder and Firefox a `.part` file, renaming only on completion. The fix is a poll: clear or freshly create the download directory before the click, then wait until the expected name exists and no partial sibling is left beside it. Under Grid managed downloads the same shape applies, polling `getDownloadableFiles()` for the final name instead of the local filesystem.
code
java · 12 linesPath exports = Files.createTempDirectory("freight-exports");
Path csv = exports.resolve("shipments.csv");
Path partial = exports.resolve("shipments.csv.crdownload");
driver.get("https://freight.internal/shipments");
driver.findElement(By.id("export-shipments-csv")).click();
new WebDriverWait(driver, Duration.ofSeconds(30))
.until(d -> Files.exists(csv) && !Files.exists(partial));
List<String> rows = Files.readAllLines(csv);
System.out.println(rows.get(0));go deeper
Recall that clicking a download link does not block until the file exists, and that a test must wait for the file itself before reading it rather than asserting on the next line.
Explain the mechanics: no download command exists in the protocol, the browser writes a partial placeholder and renames it, and the poll therefore watches for the final name with no partial sibling.
Show the judgment that a stale file passing an assertion is worse than an intermittent failure, and that the directory must be clean before the click rather than merely tidied afterwards.
Own the wider position on how much of an export's correctness a browser suite should verify at all, given the isolation cost of filesystem assertions across parallel workers and hosts.
## Why the assertion races A test on a **freight-tracking dashboard** clicks "Export shipments", reads `shipments.csv` on the next line, and then passes on a fast laptop, fails on a loaded pipeline machine, and passes again on a re-run. Nothing is flaky about the application; the test is asserting on an event it never waited for. Two facts combine: - `click()` completes when the browser has dispatched the click. The response comes back long before the server has streamed a row of the export. - The WebDriver protocol models pages, elements and browsing contexts. **A download is none of those.** There is no command to ask whether a transfer finished and no event to subscribe to, because the destination is a filesystem the driver does not represent. So the only observable is the folder the browser was configured to write into, and the test must read it in a loop rather than once. ## What the browser does on disk Browsers do not create the final file and grow it. They write a placeholder and rename it at the end, which is what makes a name-only check unreliable and a size check unnecessary. | Browser | While the transfer runs | On completion | |---|---|---| | Chrome and Edge | `shipments.csv.crdownload` | renamed to `shipments.csv` | | Firefox | `shipments.csv.part` | renamed to `shipments.csv` | That rename is the signal worth waiting on. A check that only asserts "some file appeared" can match the placeholder, and a check that reads the final name the instant it appears is usually safe because the rename happens after the last byte — but pairing "final name exists" with "no partial sibling exists" removes the ambiguity for a suite that runs against both browsers. ## A completion check that holds 1. Create a fresh directory per test, or empty the existing one, **before** the click. 2. Click the export control on the dashboard. 3. Poll until the expected filename exists and no `.crdownload` or `.part` sibling remains. 4. Read the file and assert on its contents. ```java Path csv = exports.resolve("shipments.csv"); new WebDriverWait(driver, Duration.ofSeconds(30)) .until(d -> Files.exists(csv) && !Files.exists(exports.resolve("shipments.csv.crdownload"))); ``` The condition is a plain predicate over the filesystem; the wait helper is only the polling loop around it. If the export's filename carries a timestamp, poll for the first entry in the directory that does not end in a partial suffix instead of a fixed name. ## The same problem under managed downloads When the browser is on a Grid node and the session requested `se:downloadsEnabled`, the local filesystem is not the observable — `getDownloadableFiles()` is. The race is identical, and so is the shape of the fix: - Poll `getDownloadableFiles()` until the expected final name is in the returned list. - Treat a name carrying a partial suffix as "still in flight", exactly as on a local disk. - Only then call `downloadFile` to copy it to the client. ## Clearing before you click Cleaning up afterwards is not the same as starting clean, and the difference produces the nastiest version of this bug. - If `shipments.csv` already exists, Chrome does not overwrite it — it writes `shipments (1).csv`. A test polling for `shipments.csv` finds yesterday's file instantly and asserts on stale data. - A stale file makes the test **pass** while the feature is broken, which is far worse than the intermittent failure it replaced. - Under managed downloads, `deleteDownloadableFiles()` gives the same clean slate within a session. ## Where the check belongs in the test The poll is not part of the click and it is not part of the assertion; it is a named step between them, and treating it that way keeps the failure readable. - Put it in a small helper that takes a directory and an expected name and returns the completed path, so every export test in the suite waits the same way. - Let it fail with a message naming the directory it watched and what it found there; "timed out waiting for shipments.csv, saw shipments.csv.crdownload" diagnoses itself. - Keep the assertion afterwards about the file's contents — the rows of the manifest — rather than about its existence, which the wait has already established. ## What not to reach for - A fixed sleep after the click. It is tuned to one machine, is either too short under load or wasted on every run, and says the wrong thing about the system. - Re-running the whole case when the read fails. That hides a missing wait behind an infrastructure knob. - Waiting for a spinner or a toast in the page instead of the file. The notification and the transfer are independent; a dashboard can show "Export ready" while the browser is still writing. - Reading the file's size once and calling it done. A partial write can hit any size, which is precisely why browsers use a distinct placeholder name.
- The dashboard shows an 'Export ready' toast. Is waiting for that enough?No. The toast is page state and the download is filesystem state, produced by an independent process. The notification can appear while the browser is still writing, or the file can land while the toast never renders. Wait on the artefact you are about to assert on.
- The exported filename carries a timestamp, so you cannot poll for a fixed name. What then?Poll the directory for any entry that does not end in a partial suffix, having emptied it before the click so exactly one candidate can appear. Match a stable prefix or extension if other files may share the folder, and take the single completed entry as the artefact.
saying these in an interview costs you the question
- Believes click() returns only after the file is written
- Adds a fixed sleep instead of polling for the file
- Waits for a page notification rather than the artefact
- Never clears the directory, so a stale file passes
- Treats any newly appeared file as the completed download