Skip to main content
SeleniumDecoded

Wait for a File Download to Complete

Configure a download directory, trigger the download, and wait until the file exists and is no longer a partial file.

Medium Verified against Selenium 4.48.0

Problem

Clicking “Export CSV” starts a download. The test needs to assert on the file, but the download happens outside the DOM: there is no element to wait for, and the browser writes a temporary .crdownload or .part file first.

Solution

Point the browser at a known empty directory, click, then poll the filesystem until the expected file exists and no temporary files remain.

Local download with a filesystem wait
Selenium 4 Medium
Path downloads = Files.createTempDirectory("dl");
ChromeOptions options = new ChromeOptions();
options.setExperimentalOption("prefs", Map.of(
"download.default_directory", downloads.toAbsolutePath().toString(),
"download.prompt_for_download", false,
"safebrowsing.enabled", true));
// Firefox equivalent:
// FirefoxOptions ff = new FirefoxOptions();
// ff.addPreference("browser.download.folderList", 2);
// ff.addPreference("browser.download.dir", downloads.toString());
// ff.addPreference("browser.helperApps.neverAsk.saveToDisk", "text/csv,application/pdf");
WebDriver driver = new ChromeDriver(options);
driver.get("https://app.example.com/reports");
driver.findElement(By.id("export-csv")).click();
Path file = new WebDriverWait(driver, Duration.ofSeconds(30)).until(d -> {
try (Stream<Path> files = Files.list(downloads)) {
List<Path> all = files.toList();
boolean partial = all.stream().anyMatch(p -> p.toString().endsWith(".crdownload") || p.toString().endsWith(".part"));
return all.stream()
.filter(p -> p.getFileName().toString().equals("report.csv"))
.filter(p -> !partial)
.findFirst().orElse(null);
} catch (IOException e) { return null; }
});
assertTrue(Files.size(file) > 0);
import tempfile, os
from pathlib import Path
downloads = Path(tempfile.mkdtemp("dl"))
options = webdriver.ChromeOptions()
options.add_experimental_option("prefs", {
"download.default_directory": str(downloads),
"download.prompt_for_download": False,
"safebrowsing.enabled": True,
})
# Firefox:
# options.set_preference("browser.download.folderList", 2)
# options.set_preference("browser.download.dir", str(downloads))
# options.set_preference("browser.helperApps.neverAsk.saveToDisk", "text/csv,application/pdf")
driver = webdriver.Chrome(options=options)
driver.get("https://app.example.com/reports")
driver.find_element(By.ID, "export-csv").click()
def downloaded(_):
files = list(downloads.iterdir())
if any(f.suffix in (".crdownload", ".part", ".tmp") for f in files):
return False
target = downloads / "report.csv"
return target if target.exists() and target.stat().st_size > 0 else False
file = WebDriverWait(driver, 30).until(downloaded)
assert file.read_text().startswith("order_id,")
const fs = require('fs');
const os = require('os');
const path = require('path');
const downloads = fs.mkdtempSync(path.join(os.tmpdir(), 'dl-'));
const options = new chrome.Options().setUserPreferences({
'download.default_directory': downloads,
'download.prompt_for_download': false,
});
const driver = await new Builder().forBrowser('chrome').setChromeOptions(options).build();
await driver.get('https://app.example.com/reports');
await driver.findElement(By.id('export-csv')).click();
const file = await driver.wait(async () => {
const files = fs.readdirSync(downloads);
if (files.some((f) => f.endsWith('.crdownload') || f.endsWith('.part'))) return null;
const target = path.join(downloads, 'report.csv');
return fs.existsSync(target) && fs.statSync(target).size > 0 ? target : null;
}, 30000);
var downloads = Directory.CreateTempSubdirectory("dl").FullName;
var options = new ChromeOptions();
options.AddUserProfilePreference("download.default_directory", downloads);
options.AddUserProfilePreference("download.prompt_for_download", false);
var driver = new ChromeDriver(options);
driver.Navigate().GoToUrl("https://app.example.com/reports");
driver.FindElement(By.Id("export-csv")).Click();
var file = new WebDriverWait(driver, TimeSpan.FromSeconds(30)).Until(_ =>
{
var files = Directory.GetFiles(downloads);
if (files.Any(f => f.EndsWith(".crdownload") || f.EndsWith(".part"))) return null;
var target = Path.Combine(downloads, "report.csv");
return File.Exists(target) && new FileInfo(target).Length > 0 ? target : null;
});

Why It Works

Browsers write to a temporary name and rename on completion. Checking for the absence of temporary files plus the presence of the final name with non-zero size is the reliable completion signal. A fresh temporary directory per test avoids collisions and stale files.

Gotchas

  • On a Grid or cloud, the file lands on the remote machine. Use Selenium 4’s managed downloads instead (se:downloadsEnabled capability, getDownloadableFiles() and downloadFile()); see Grid on Docker and Kubernetes.
  • Headless Chrome allows downloads by default in the new headless mode; the old headless mode required a CDP Browser.setDownloadBehavior call.
  • If the file name is dynamic, wait for “exactly one non-temporary file” instead of a fixed name.

Learn the theory