Selenium: Drivers, Browsers & Options

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

A Selenium test never talks to a browser directly. It builds a few small objects — Options that describe the browser session, a Service that starts the driver executable, and optionally an HTTP client configuration — and hands them to a driver class, which starts the session. This page explains those objects, the options every browser shares, and then the specifics of each browser Selenium drives: Chrome, Edge, Firefox, Safari and Internet Explorer mode. It was written against Selenium 4.51.0 (released 9 October 2026) and checked on 25 October 2026. Every Java (JUnit 5.14.4, Java 17) and Python (pytest, Python 3.13) example marked as run was executed against the Bookshelf application with Selenium 4.51.0: Chrome 155 ran locally on macOS, and Chrome 155 and Firefox 158 ran inside the selenium/standalone-chrome:4.51.0 and selenium/standalone-firefox:4.51.0 containers. Edge, Safari and Internet Explorer could not be run on this host, and local Firefox did not start under Selenium on it, so those sections are written from the official documentation and the Selenium API and say so where it matters. C#, Ruby and JavaScript snippets are shown where the official documentation shows a binding-specific difference; they were not run. For installing Selenium and the first script, see Getting Started; for Grid itself, see Selenium Grid.

Anatomy of a session

Creating a driver object corresponds to the W3C WebDriver command New Session. The constructor does the whole job: it resolves the driver executable, starts it, sends the capabilities and returns once the browser is open. Three kinds of argument shape that process:

Argument What it controls Applies to

Options (ChromeOptions, FirefoxOptions, EdgeOptions, SafariOptions, InternetExplorerOptions)

Which browser, which version and platform, how it behaves (page-load strategy, timeouts, prompts, proxy) and the vendor-specific settings (arguments, binary, preferences).

Local and remote sessions. Required for a remote session, because the options decide which browser the remote end starts.

Service (ChromeDriverService, GeckoDriverService, EdgeDriverService, SafariDriverService, InternetExplorerDriverService)

The driver process: its executable, port, log file, log level and driver-specific switches.

Local sessions only. A Service cannot be used with a Remote WebDriver session.

HTTP client configuration (ClientConfig in Java and Python, an HTTP client object in Ruby)

How the client talks to the driver or Grid: timeouts, retries, proxy, TLS trust, credentials.

Remote sessions. Local drivers use a default client of the same kind.

flowchart LR T["Test code"] -->|"Options, Service, ClientConfig"| C["Driver class: ChromeDriver, webdriver.Chrome"] C -->|"Service starts the process"| D["chromedriver, geckodriver, msedgedriver, safaridriver"] C -->|"HTTP: W3C WebDriver commands"| D D -->|"browser automation protocol"| B["Browser"] T -->|"Options, address, ClientConfig"| R["Remote WebDriver"] R -->|"HTTP"| G["Selenium Grid or Selenium Server"] G -->|"HTTP"| N["Driver and browser on a node"]

A session ends with the W3C command Delete Session, which is what quit() sends. quit() closes every window, ends the session and stops the driver process. close() only closes the current window, and the session and the driver process stay alive if it was the last window, so tests should always end with quit() (in a finally block, a JUnit @AfterEach or a pytest fixture). A forgotten quit() leaves a browser and a driver process behind, which is the most common reason a CI machine slows down over time.

Options and capabilities

A capability is a name-value pair in the JSON object the client sends with New Session; the W3C specification defines a small set of standard ones, and every browser vendor adds its own, prefixed with a vendor name (goog:chromeOptions, ms:edgeOptions, moz:firefoxOptions, safari: keys, and se: keys that Selenium Grid reads). An Options class is the typed builder for that JSON object. You never write the JSON by hand: the setters of the Options class fill it in, and the driver class sends it. The result the driver returns is also a set of capabilities, which you can read back to see what the session really got. In Selenium 3 the same job was done by the capabilities helper classes; Selenium 4 requires Options classes, and the old helpers are described on Selenium IDE & Legacy. For a remote session an Options instance is mandatory, as it determines the browser.

The shared options

These capabilities are defined by the W3C specification and are available on every Options class:

Capability Java Python Meaning

browserName

set by the class (ChromeOptions is chrome)

set by the class

Read it back with getBrowserName().

browserVersion

options.setBrowserVersion("stable")

options.browser_version = "stable"

Selects an installed or downloadable browser version. When the version is not found locally, Selenium Manager downloads it.

platformName

options.setPlatformName(...)

options.platform_name = ...

Identifies the operating system of the remote end. With cloud providers, setting it picks the operating system.

acceptInsecureCerts

options.setAcceptInsecureCerts(true)

options.accept_insecure_certs = True

Trust expired or self-signed TLS certificates for the whole session. With false an insecure-certificate error is returned.

pageLoadStrategy

options.setPageLoadStrategy(PageLoadStrategy.EAGER)

options.page_load_strategy = "eager"

When a navigation counts as finished, see the next section.

timeouts

setScriptTimeout, setPageLoadTimeout, setImplicitWaitTimeout (all take a Duration)

options.timeouts = {"script": 5000, "pageLoad": 20000, "implicit": 0} (milliseconds)

Session timeouts, see the table below.

unhandledPromptBehavior

options.setUnhandledPromptBehaviour(UnexpectedAlertBehaviour.DISMISS_AND_NOTIFY)

options.unhandled_prompt_behavior = "dismiss and notify"

What to do with an alert, confirm or prompt dialog nobody handled: dismiss, accept, dismiss and notify (the default), accept and notify or ignore.

strictFileInteractability

options.setCapability(CapabilityType.STRICT_FILE_INTERACTABILITY, true)

options.strict_file_interactability = True

Apply the strict interactability checks to input type=file elements. It is off by default, so sendKeys works on hidden file inputs.

proxy

options.setCapability("proxy", proxy) with a Proxy

options.proxy = Proxy({...})

Send the browser traffic through a proxy: useful to capture traffic, to mock backend calls or to reach a site from behind a corporate network.

setWindowRect

read only

read only

Reported by the driver: whether the resize and reposition commands are supported. ChromeDriver rejects it as an input capability.

The three session timeouts have these W3C defaults:

Timeout Default Behaviour

script

30,000 ms

How long an executing script may run before it is interrupted.

pageLoad

300,000 ms

How long a navigation may take. When it is exceeded the command fails with a TimeoutException.

implicit

0 ms

How long element lookups retry before failing. Leave it at 0 and use explicit waits (see Waits & Expected Conditions); mixing the two produces unpredictable total waits.

The next test builds Chrome options with several of these, prints the JSON the client would send, starts a headless session, and checks what the driver returned:

import static org.junit.jupiter.api.Assertions.*;

import java.time.Duration;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.PageLoadStrategy;
import org.openqa.selenium.Proxy;
import org.openqa.selenium.UnexpectedAlertBehaviour;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.remote.CapabilityType;

class OptionsTest {
  @Test
  void sharedOptionsBecomeSessionCapabilities() {
    ChromeOptions options = new ChromeOptions();
    options.addArguments("--headless=new");
    options.setPageLoadStrategy(PageLoadStrategy.EAGER);
    options.setAcceptInsecureCerts(true);
    options.setScriptTimeout(Duration.ofSeconds(5));
    options.setPageLoadTimeout(Duration.ofSeconds(20));
    options.setImplicitWaitTimeout(Duration.ZERO);
    options.setUnhandledPromptBehaviour(UnexpectedAlertBehaviour.DISMISS_AND_NOTIFY);
    options.setCapability(CapabilityType.STRICT_FILE_INTERACTABILITY, true);

    // What the client would send: the W3C capabilities plus the goog:chromeOptions extension.
    System.out.println("asMap: " + options.asMap());

    WebDriver driver = new ChromeDriver(options);
    try {
      var caps = ((ChromeDriver) driver).getCapabilities();
      System.out.println("browserName=" + caps.getBrowserName() + " platformName=" + caps.getPlatformName()
          + " pageLoadStrategy=" + caps.getCapability("pageLoadStrategy")
          + " timeouts=" + caps.getCapability("timeouts"));
      assertEquals("chrome", caps.getBrowserName());
      assertEquals("eager", caps.getCapability("pageLoadStrategy"));
      // setWindowRect is reported by the driver; it is not accepted as an input capability.
      assertEquals(true, caps.getCapability(CapabilityType.SET_WINDOW_RECT));
      assertEquals(true, caps.getCapability("acceptInsecureCerts"));
      assertEquals(Duration.ofSeconds(5), driver.manage().timeouts().getScriptTimeout());
      assertEquals(Duration.ofSeconds(20), driver.manage().timeouts().getPageLoadTimeout());
      assertEquals(Duration.ZERO, driver.manage().timeouts().getImplicitWaitTimeout());
    } finally {
      driver.quit();
    }
  }

  @Test
  void proxyIsAStandardCapability() {
    Proxy proxy = new Proxy();
    proxy.setHttpProxy("proxy.example.test:8080");
    proxy.setNoProxy("localhost");
    ChromeOptions options = new ChromeOptions();
    options.setCapability("proxy", proxy);
    System.out.println("proxy capability: " + options.asMap().get("proxy"));
    assertEquals("proxy.example.test:8080", ((Proxy) options.getCapability("proxy")).getHttpProxy());
  }
}
def test_shared_options_become_session_capabilities():
    options = webdriver.ChromeOptions()
    options.add_argument("--headless=new")
    options.page_load_strategy = "eager"
    options.accept_insecure_certs = True
    options.timeouts = {"script": 5000, "pageLoad": 20000, "implicit": 0}
    options.unhandled_prompt_behavior = "dismiss and notify"
    options.strict_file_interactability = True

    # What the client would send: the W3C capabilities plus the goog:chromeOptions extension.
    print("to_capabilities:", json.dumps(options.to_capabilities(), sort_keys=True))

    driver = webdriver.Chrome(options=options)
    try:
        caps = driver.capabilities
        print("browserName=%s platformName=%s pageLoadStrategy=%s timeouts=%s" % (
            caps["browserName"], caps["platformName"], caps["pageLoadStrategy"], caps["timeouts"]))
        assert caps["browserName"] == "chrome"
        assert caps["pageLoadStrategy"] == "eager"
        assert caps["acceptInsecureCerts"] is True
        # setWindowRect is reported by the driver; it is not accepted as an input capability.
        assert caps["setWindowRect"] is True
        assert driver.timeouts.script == 5
        assert driver.timeouts.page_load == 20
        assert driver.timeouts.implicit_wait == 0
        driver.get(BASE_URL + "/books")
    finally:
        driver.quit()


def test_proxy_is_a_standard_capability():
    options = webdriver.ChromeOptions()
    options.proxy = Proxy({"proxyType": ProxyType.MANUAL, "httpProxy": "proxy.example.test:8080", "noProxy": "localhost"})
    caps = options.to_capabilities()
    print("proxy capability:", caps["proxy"])
    assert caps["proxy"]["httpProxy"] == "proxy.example.test:8080"

Both ran and passed (two Java tests, two Python tests). The relevant output was:

asMap: {acceptInsecureCerts=true, browserName=chrome, goog:chromeOptions={args=[--headless=new], extensions=[]}, pageLoadStrategy=eager, strictFileInteractability=true, timeouts={implicit=0, pageLoad=20000, script=5000}, unhandledPromptBehavior=dismiss and notify}
browserName=chrome platformName=mac pageLoadStrategy=eager timeouts={implicit=0, pageLoad=20000, script=5000}
proxy capability: Proxy(manual, http=proxy.example.test:8080)

Three details are worth noting from this run. First, the official examples for setWindowRect only build an Options object and read the capability back; if you send it to ChromeDriver the session fails with unrecognized capability: setWindowRect, because it is a capability the driver returns. Second, the same timeout is a Duration in Java, plain milliseconds in the Python options.timeouts dictionary, and seconds when read back through Python’s driver.timeouts object (driver.timeouts.script was 5 for the 5000 ms set here). Third, the returned capabilities contain more than you sent: Chrome adds chrome.chromedriverVersion, goog:chromeOptions.debuggerAddress, networkConnectionEnabled, WebAuthn extension flags and the process id, the Grid adds se: keys (se:cdp, se:vnc, se:downloadsEnabled, se:noVncPort and others), and Firefox adds moz: keys (moz:profile, moz:headless, moz:geckodriverVersion, moz:debuggerAddress).

Options versus capabilities

The two terms are not interchangeable. The capabilities are the data sent on the wire and returned by the driver; the Options object is the per-browser class that builds the data safely. Every Options class is itself a capabilities object, so getCapability, setCapability and merge work on it, and options.setCapability("key", value) (options.set_capability("key", value) in Python) is the escape hatch for a capability the class has no setter for. When a feature is configured both through a typed setter and through a raw capability, prefer the setter, which also validates the value. Capability names that contain a colon belong to a vendor, and the Grid ignores or rejects keys it does not know.

The returned capabilities can differ from the requested ones. Selenium Manager and the driver can pick a different browser version than you assumed, and a browser may ignore an option. Reading driver.capabilities (driver.getCapabilities() in Java) is the way to check what a session actually got, which is what the example above does.

Page-load strategies

driver.get(url) returns when the navigation is finished, and the page-load strategy decides what "finished" means. It queries document.readyState:

Strategy Ready state Event waited for Use

normal (default)

complete

the load event, after every resource has downloaded

The safe default; the page is as the user sees it.

eager

interactive

DOMContentLoaded: the DOM is ready, but images, style sheets and frames may still be loading

Faster tests when sub-resources do not matter to the test.

none

any

only the initial HTML document is downloaded; WebDriver does not block at all

You wait yourself, for example for a long-polling or streaming page, with explicit waits.

The strategy applies to the whole session, so the waiting strategy of the test must be strong enough for the page (a single-page application can report complete long before its JavaScript has drawn anything). It also only applies to navigation commands such as get, navigate().to and refresh; a navigation that is the result of clicking an element or submitting a form is not governed by it, and needs an explicit wait.

Bookshelf’s pages load too quickly to show a difference, so this test starts a tiny HTTP server in the test process whose page contains an image that takes two seconds to arrive, and measures how long get blocks under each strategy:

import static org.junit.jupiter.api.Assertions.*;

import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.Base64;
import java.util.concurrent.Executors;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.PageLoadStrategy;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;

class PageLoadStrategyTest {
  static final int SLOW_PORT = Integer.getInteger("slow.port", 4518);
  static final byte[] PIXEL = Base64.getDecoder().decode(
      "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/q842iQAAAABJRU5ErkJggg==");
  static HttpServer server;

  @BeforeAll
  static void startServer() throws IOException {
    server = HttpServer.create(new InetSocketAddress(SLOW_PORT), 0);
    server.createContext("/", ex -> {
      byte[] body = "<html><body><h1 id=title>Slow page</h1><img src='/slow.png'></body></html>".getBytes();
      ex.getResponseHeaders().add("Content-Type", "text/html");
      ex.sendResponseHeaders(200, body.length);
      ex.getResponseBody().write(body);
      ex.close();
    });
    server.createContext("/slow.png", ex -> {
      try { Thread.sleep(2000); } catch (InterruptedException ignored) { } // the slow sub-resource
      ex.getResponseHeaders().add("Content-Type", "image/png");
      ex.sendResponseHeaders(200, PIXEL.length);
      ex.getResponseBody().write(PIXEL);
      ex.close();
    });
    server.setExecutor(Executors.newCachedThreadPool()); // the slow image must not block other requests
    server.start();
  }

  @AfterAll
  static void stopServer() { server.stop(0); }

  @ParameterizedTest
  @EnumSource(value = PageLoadStrategy.class, names = {"NORMAL", "EAGER", "NONE"})
  void getReturnsAtTheStrategysReadyState(PageLoadStrategy strategy) {
    ChromeOptions options = new ChromeOptions();
    options.addArguments("--headless=new");
    options.setPageLoadStrategy(strategy);
    WebDriver driver = new ChromeDriver(options);
    try {
      long start = System.nanoTime();
      driver.get("http://localhost:" + SLOW_PORT + "/");
      long ms = (System.nanoTime() - start) / 1_000_000;
      Object state = ((JavascriptExecutor) driver).executeScript("return document.readyState");
      System.out.printf("%-6s get() took %4d ms, readyState=%s%n", strategy, ms, state);
      if (strategy == PageLoadStrategy.NORMAL) {
        assertTrue(ms >= 1800, "normal waits for the 2 s image");
        assertEquals("complete", state);
      } else {
        assertTrue(ms < 1500, strategy + " does not wait for the image");
      }
    } finally {
      driver.quit();
    }
  }
}
@pytest.mark.parametrize("strategy", ["normal", "eager", "none"])
def test_get_returns_at_the_strategys_ready_state(slow_server, strategy):
    options = webdriver.ChromeOptions()
    options.add_argument("--headless=new")
    options.page_load_strategy = strategy
    driver = webdriver.Chrome(options=options)
    try:
        start = time.monotonic()
        driver.get(slow_server + "/")
        ms = round((time.monotonic() - start) * 1000)
        state = driver.execute_script("return document.readyState")
        print(f"{strategy:<6} get() took {ms:4d} ms, readyState={state}")
        if strategy == "normal":
            assert ms >= 1800 and state == "complete"
        else:
            assert ms < 1500
    finally:
        driver.quit()

The Python version uses a fixture server from conftest.py that serves the same page and the same delayed image from a thread. Both versions passed; the Java run printed:

none   get() took    4 ms, readyState=interactive
eager  get() took   44 ms, readyState=interactive
normal get() took 2038 ms, readyState=complete

normal waited for the two-second image, eager returned as soon as the DOM was ready, and none returned almost immediately; the ready state it reported afterwards was already interactive because the page is tiny, but nothing guaranteed it. One caveat from building this test: the first version used the JDK’s default single-threaded HTTP server, and the pending image request of the none run blocked the page request of the next run. It looked like eager taking 1.4 seconds. When a strategy other than normal seems slow, look at the server or at the previous test’s leftover requests before blaming the strategy.

The driver Service

The Service classes manage the starting and stopping of local driver executables. They let you choose the driver location and the port, and they pass command-line arguments to the driver, most of which are about logging. Without a Service the driver class builds a default one. The shape of the API differs by language: in Java a Service is built once through a Builder and is immutable afterwards; in Python the values are constructor arguments; .NET services expose them as properties; and Ruby accepts either constructor arguments or attributes.

Setting How

Default service

Java new ChromeDriverService.Builder().build(), Python webdriver.ChromeService() (Python Service constructors with values date from Selenium 4.11).

Driver location

Java usingDriverExecutable(File), Python ChromeService(executable_path=...). With Selenium 4.6 or later you should not need it, because Selenium Manager finds or downloads the driver; set it only for an unusual setup.

Port

Java usingPort(int), Python ChromeService(port=...). By default a free port is chosen.

Log output

Java withLogFile(File) or withLogOutput(OutputStream), Python ChromeService(log_output=path_or_stream). Logging is ignored unless you direct it somewhere.

Log level and driver switches

Java builder methods (withLogLevel, withAppendLog, withReadableTimestamp, withBuildCheckDisabled), Python service_args=[...].

This test covers port, log file, log level and the two file-only log features, and then shows how to point at explicit driver and browser executables found by DriverFinder, which asks Selenium Manager where they are:

@Test
void logFileLevelTimestampsAndPort() throws Exception {
  File log = File.createTempFile("chromedriver-", ".log");
  ChromeDriverService service = new ChromeDriverService.Builder()
      .usingPort(4513)
      .withLogFile(log)
      .withLogLevel(ChromiumDriverLogLevel.DEBUG)
      .withAppendLog(true)
      .withReadableTimestamp(true)
      .build();
  ChromeDriver driver = new ChromeDriver(service, headless());
  try {
    driver.get(Env.BASE_URL + "/books");
    assertEquals("http://localhost:4513", service.getUrl().toString());
  } finally {
    driver.quit();
  }
  String text = Files.readString(log.toPath());
  System.out.println(text.lines().limit(3).toList());
  assertTrue(text.contains("Starting ChromeDriver"));
  assertTrue(text.contains("[DEBUG]:"));
  assertTrue(text.matches("(?s)^\\[\\d\\d-\\d\\d-\\d\\d\\d\\d.*"), "readable timestamp");
}

@Test
void explicitDriverAndBrowserLocations() {
  ChromeOptions probe = headless();
  probe.setBrowserVersion("stable");
  // DriverFinder asks Selenium Manager where the driver and the browser are.
  DriverFinder finder = new DriverFinder(ChromeDriverService.createDefaultService(), probe);
  System.out.println("driver:  " + finder.getDriverPath());
  System.out.println("browser: " + finder.getBrowserPath());

  ChromeOptions options = headless();
  options.setBinary(new File(finder.getBrowserPath()));
  ChromeDriverService service = new ChromeDriverService.Builder()
      .usingDriverExecutable(new File(finder.getDriverPath()))
      .build();
  ChromeDriver driver = new ChromeDriver(service, options);
  try {
    driver.get(Env.BASE_URL + "/books");
    assertEquals("Catalogue - Bookshelf", driver.getTitle());
  } finally {
    driver.quit();
  }
}
def test_log_file_level_timestamps_and_port(tmp_path):
    log = tmp_path / "chromedriver.log"
    service = webdriver.ChromeService(
        port=4514,
        log_output=str(log),
        service_args=["--log-level=DEBUG", "--append-log", "--readable-timestamp"],
    )
    driver = webdriver.Chrome(service=service, options=headless())
    try:
        driver.get(BASE_URL + "/books")
        assert service.service_url == "http://localhost:4514"
    finally:
        driver.quit()
    text = log.read_text()
    print(text.splitlines()[:3])
    assert "Starting ChromeDriver" in text
    assert "[DEBUG]:" in text
    assert re.match(r"\[\d\d-\d\d-\d\d\d\d", text)


def test_driver_and_browser_locations_from_selenium_manager():
    from selenium.webdriver.common.selenium_manager import SeleniumManager

    options = headless()
    options.browser_version = "stable"
    result = SeleniumManager().binary_paths(["--browser", "chrome", "--browser-version", "stable"])
    print("driver: ", result["driver_path"])
    print("browser:", result["browser_path"])

    options.binary_location = result["browser_path"]
    service = webdriver.ChromeService(executable_path=result["driver_path"])
    driver = webdriver.Chrome(service=service, options=options)
    try:
        driver.get(BASE_URL + "/books")
        assert driver.title == "Catalogue - Bookshelf"
    finally:
        driver.quit()

Both passed. The first log lines written by chromedriver with --readable-timestamp and debug level were:

[10-25-2026 21:49:14.404663][INFO]: Starting ChromeDriver 155.0.8059.84 (...) on port 4514
[10-25-2026 21:49:14.405166][INFO]: Please see https://chromedriver.chromium.org/security-considerations for suggestions on keeping ChromeDriver safe.
[10-25-2026 21:49:14.407664][INFO]: ChromeDriver was started successfully on port 4514

The second test also shows a side effect worth knowing: asking DriverFinder or Selenium Manager for browserVersion stable does not use the Chrome installed on the machine. It downloaded Chrome for Testing 156 and the matching chromedriver 156 into the Selenium Manager cache (~/.cache/selenium), although Chrome 155 was installed, and the session ran with those. Use stable (or any browser version) only when you want a managed browser; leave browserVersion out to use the installed one.

The Service also controls driver-specific switches for each browser; they are described in the browser sections below. The Java system properties, which can set the same values from outside the code (for example from mvn -D), are:

Driver Java system property constants

ChromeDriver

ChromeDriverService.CHROME_DRIVER_LOG_PROPERTY, CHROME_DRIVER_LOG_LEVEL_PROPERTY, CHROME_DRIVER_APPEND_LOG_PROPERTY, CHROME_DRIVER_READABLE_TIMESTAMP, CHROME_DRIVER_DISABLE_BUILD_CHECK

msedgedriver

EdgeDriverService.EDGE_DRIVER_LOG_PROPERTY, EDGE_DRIVER_LOG_LEVEL_PROPERTY (and the same append, timestamp and build-check constants)

geckodriver

GeckoDriverService.GECKO_DRIVER_LOG_PROPERTY, GECKO_DRIVER_LOG_LEVEL_PROPERTY, GECKO_DRIVER_LOG_NO_TRUNCATE, GECKO_DRIVER_PROFILE_ROOT

IEDriverServer

InternetExplorerDriverService.IE_DRIVER_LOGFILE_PROPERTY, IE_DRIVER_LOGLEVEL_PROPERTY, IE_DRIVER_EXTRACT_PATH_PROPERTY

safaridriver

SafariDriverService.SAFARI_DRIVER_LOGGING

HTTP client configuration

Underneath, every command is an HTTP request. The HTTP client configuration lets you set the parameters of that client: connection and read timeouts, retries, an HTTP version, a proxy, a custom TLS trust store (or none), and credentials for a Grid protected with basic authentication. The implementation varies by language. In Java you build a ClientConfig and pass it to the RemoteWebDriver builder; in Python you pass a ClientConfig as client_config; in Ruby you create an HTTP client object and pass it as http_client. The official documentation shows no example for .NET and JavaScript. The following test configures a Java and a Python client for the Chrome container: retries, timeouts, and (Java only) the augmented driver the builder returns.

@Test
void clientConfigAndBuilderGiveYouBrowserSpecificFeatures() {
  ClientConfig config = ClientConfig.defaultConfig()
      .withRetries()
      .connectionTimeout(Duration.ofSeconds(30))
      .readTimeout(Duration.ofMinutes(5));
  ChromeOptions options = new ChromeOptions();
  options.addArguments("--headless=new");
  driver = RemoteWebDriver.builder()
      .address(Env.url(Env.CHROME_GRID))
      .oneOf(options)
      .config(config)
      .build();

  // The builder returns an augmented driver, so the Chromium interfaces are available remotely too.
  assertTrue(driver instanceof HasCasting);
  assertTrue(driver instanceof HasNetworkConditions);
  driver.get(Env.BASE_URL_FROM_DOCKER + "/books");
  assertEquals("Catalogue - Bookshelf", driver.getTitle());
}
def test_client_config():
    config = ClientConfig(remote_server_addr=CHROME_GRID, timeout=300, keep_alive=True)
    driver = webdriver.Remote(command_executor=CHROME_GRID, options=chrome_options(), client_config=config)
    try:
        driver.get(BASE_URL_FROM_DOCKER + "/books")
        assert driver.title == "Catalogue - Bookshelf"
    finally:
        driver.quit()

Both passed against the Grid container. The official example goes further, with a custom CA certificate (or none), a 300-second connection timeout, a 3,600-second read timeout, an HTTP version and credentials. That variant needs a Grid served over TLS with basic authentication, which was not set up here, so it was not run; the Java form follows, with the password read from the environment:

ClientConfig clientConfig = ClientConfig.defaultConfig()
    .withRetries()
    .sslContext(createSSLContextWithCA(Path.of("tls.crt").toAbsolutePath().toString()))
    .connectionTimeout(Duration.ofSeconds(300))
    .readTimeout(Duration.ofSeconds(3600))
    .authenticateAs(new UsernameAndPassword("admin", System.getenv("GRID_PASSWORD")))
    .version(HTTP_1_1.toString());
WebDriver driver = RemoteWebDriver.builder()
    .oneOf(options)
    .address(gridUrl)
    .config(clientConfig)
    .build();
import os
from urllib3.util import Retry, Timeout
from selenium.webdriver.common.proxy import Proxy, ProxyType
from selenium.webdriver.remote.client_config import ClientConfig

client_config = ClientConfig(
    remote_server_addr=grid_url,
    proxy=Proxy({"proxyType": ProxyType.AUTODETECT}),
    init_args_for_pool_manager={"init_args_for_pool_manager": {
        "retries": Retry(connect=2, read=2, redirect=2), "timeout": Timeout(connect=300, read=3600)}},
    ca_certs="tls.crt",                      # or ignore_certificates=True for a throw-away test Grid
    username="admin", password=os.environ["GRID_PASSWORD"],
)
driver = webdriver.Remote(command_executor=grid_url, options=options, client_config=client_config)

In Ruby the equivalent is a client object, for example Selenium::WebDriver::Remote::Http::Default.new(open_timeout: 30, read_timeout: 30) passed as http_client:. Ignoring TLS validation (ignore_certificates=True in Python, a trust-everything context in Java) is only acceptable against a throw-away test Grid; prefer trusting your own certificate authority. Credentials can also be embedded in the Grid URL, but then they end up in logs and stack traces, so prefer authenticateAs or username and password.

Remote WebDriver

Selenium can automate browsers on other computers when a Selenium Grid (or a Selenium Server in standalone mode) runs there. The machine that executes your test is the client; the machine with the browser and the driver is the remote end or node. To direct a test to it, use the Remote WebDriver class with the URL (including the port) of the Grid and an Options instance: both the address and the options are required, because the driver must know where to send commands and which browser the remote end should start. For how to build and operate the Grid, see Selenium Grid.

The Java tests of this section share a small base: a Fixture helper server (shown at the end of the section) started once per class, a driver field and an @AfterEach that quits it. The Python tests omit imports, use a small helper chrome_options() that returns a ChromeOptions with --headless=new, and take their constants (BASE_URL, CHROME_GRID and so on) from conftest.py, which reads them from environment variables with the defaults of these runs.

The tests in this section talk to a selenium/standalone-chrome:4.51.0 container started with docker run -d --rm --name sel-drivers-chrome --shm-size=2g -p 4511:4444 selenium/standalone-chrome:4.51.0. The browser inside the container reaches Bookshelf on the host through host.docker.internal, which is why the base URL for remote sessions differs from the local one.

static Fixture fixture;
WebDriver driver;

@BeforeAll
static void startFixture() throws Exception { fixture = new Fixture(); }

@AfterAll
static void stopFixture() { fixture.stop(); }

@AfterEach
void stop() { if (driver != null) driver.quit(); }

@Test
void addressPlusOptions() {
  ChromeOptions options = new ChromeOptions();
  options.addArguments("--headless=new");
  driver = new RemoteWebDriver(Env.url(Env.CHROME_GRID), options);

  driver.get(Env.BASE_URL_FROM_DOCKER + "/books");
  assertEquals("Catalogue - Bookshelf", driver.getTitle());
  var caps = ((RemoteWebDriver) driver).getCapabilities();
  System.out.println(caps.getBrowserName() + " " + caps.getBrowserVersion() + " on " + caps.getPlatformName());
  // A plain RemoteWebDriver has no Chrome-only methods.
  assertFalse(driver instanceof HasCasting);
}
def test_address_plus_options():
    driver = webdriver.Remote(command_executor=CHROME_GRID, options=chrome_options())
    try:
        driver.get(BASE_URL_FROM_DOCKER + "/books")
        assert driver.title == "Catalogue - Bookshelf"
        caps = driver.capabilities
        print(caps["browserName"], caps["browserVersion"], "on", caps["platformName"])
        # Chromium-only helpers are not present on the Python remote class.
        assert not hasattr(driver, "set_network_conditions")
    finally:
        driver.quit()

Both passed; the session reported chrome 155.0.8059.79 on linux. The other bindings differ only in syntax. In C# it is new RemoteWebDriver(gridUrl, options), in Ruby Selenium::WebDriver.for :remote, url: grid_url, options: Selenium::WebDriver::Options.chrome, and in JavaScript new Builder().forBrowser(Browser.CHROME).setChromeOptions(options).usingServer(gridUrl).build(); none of these was run.

Uploads

Uploading a file through a remote session is harder than locally, because the file lives on the computer running the test while the driver looks for the path on its own file system. The solution is a local file detector: when one is set, Selenium bundles the file, sends it to the remote machine and rewrites the path the driver sees. The bindings differ in what they set up for you:

Binding Behaviour

Java

No file detector by default; always call setFileDetector(new LocalFileDetector()) before sendKeys.

Python

Adds a LocalFileDetector to remote sessions by default; you can also write your own class.

.NET

Adds a local file detector by default; set FileDetector to replace it.

Ruby

Adds one by default; a custom detector is a lambda.

JavaScript

No file detector by default; call driver.setFileDetector(new remote.FileDetector()).

Bookshelf has no upload form, so the tests serve one from the same small helper server used elsewhere on this page: a page with an input type=file that prints the chosen file name.

@Test
void uploadsNeedALocalFileDetector() throws Exception {
  ChromeOptions options = new ChromeOptions();
  options.addArguments("--headless=new");
  driver = new RemoteWebDriver(Env.url(Env.CHROME_GRID), options);
  driver.get(Fixture.FROM_DOCKER + "/upload");

  Path file = Files.createTempFile("cover-", ".txt");
  Files.writeString(file, "not really a cover");
  ((RemoteWebDriver) driver).setFileDetector(new LocalFileDetector());
  driver.findElement(By.id("file")).sendKeys(file.toAbsolutePath().toString());

  assertEquals(file.getFileName().toString(), driver.findElement(By.id("uploaded")).getText());
}
def test_uploads_use_a_local_file_detector_by_default(fixture_server, tmp_path):
    driver = webdriver.Remote(command_executor=CHROME_GRID, options=chrome_options())
    try:
        driver.get(fixture_server + "/upload")
        file = tmp_path / "cover.txt"
        file.write_text("not really a cover")
        driver.find_element(By.ID, "file").send_keys(str(file))
        assert driver.find_element(By.ID, "uploaded").text == "cover.txt"
    finally:
        driver.quit()

Both passed. The Python test never mentions a file detector, which shows the default at work. For the Fixture class used by the Java tests (a com.sun.net.httpserver server with /upload and /download/hello.txt routes) and its Python counterpart in conftest.py, see the end of this section.

Downloads

Chrome, Edge and Firefox each let you set the download directory, but on a remote machine that directory is on the remote file system. Managed downloads bring the files back to the client. Two switches are needed:

  • the Grid (in node or standalone mode) must start with --enable-managed-downloads true (the docker-selenium images enable it by default through SE_NODE_ENABLE_MANAGED_DOWNLOADS=true), and

  • the client sets the se:downloadsEnabled capability, which every binding exposes on its Options class (setEnableDownloads(true) in Java, options.enable_downloads = True in Python, EnableDownloads = true in C#, enable_downloads: true in Ruby, enableDownloads() in JavaScript).

The client then lists, fetches and deletes the files of the session. Selenium does not wait for a download to finish, so the list is an immediate snapshot of the file names currently in the session’s download directory, which is why the tests wait for the name to appear. By default the directory is deleted when the session ends.

@Test
void managedDownloadsBringFilesBackToTheClient() throws Exception {
  ChromeOptions options = new ChromeOptions();
  options.addArguments("--headless=new");
  options.setEnableDownloads(true); // se:downloadsEnabled
  driver = new RemoteWebDriver(Env.url(Env.CHROME_GRID), options);

  driver.get(Fixture.FROM_DOCKER + "/download/hello.txt");
  new WebDriverWait(driver, Duration.ofSeconds(10))
      .until(d -> ((HasDownloads) d).getDownloadableFiles().contains("hello.txt"));

  List<String> names = ((HasDownloads) driver).getDownloadableFiles();
  System.out.println("downloadable files: " + names);

  Path target = Files.createTempDirectory("download");
  ((HasDownloads) driver).downloadFile("hello.txt", target);
  assertEquals("Hello, World!", Files.readString(target.resolve("hello.txt")));

  ((HasDownloads) driver).deleteDownloadableFiles();
  assertTrue(((HasDownloads) driver).getDownloadableFiles().isEmpty());
}
def test_managed_downloads_bring_files_back_to_the_client(fixture_server, tmp_path):
    options = chrome_options()
    options.enable_downloads = True  # se:downloadsEnabled
    driver = webdriver.Remote(command_executor=CHROME_GRID, options=options)
    try:
        driver.get(fixture_server + "/download/hello.txt")
        WebDriverWait(driver, 10).until(lambda d: "hello.txt" in d.get_downloadable_files())
        print("downloadable files:", driver.get_downloadable_files())

        driver.download_file("hello.txt", str(tmp_path))
        assert (tmp_path / "hello.txt").read_text() == "Hello, World!"

        driver.delete_downloadable_files()
        assert driver.get_downloadable_files() == []
    finally:
        driver.quit()

Both passed and printed downloadable files: [hello.txt]. The Java calls are getDownloadableFiles, downloadFile(name, targetDirectory) and deleteDownloadableFiles on the HasDownloads interface; the Python ones are get_downloadable_files, download_file(name, target_directory) and delete_downloadable_files.

Browser-specific features on a remote driver

Each browser has features available only to it (casting, network conditions, Firefox add-ons and so on), and the bindings expose them differently in a remote session:

  • Java returns a plain RemoteWebDriver that lacks the browser interfaces. The Augmenter adds the implementations for every interface that matches the session’s capabilities. RemoteWebDriver.builder() augments the driver automatically, so it is the easiest way to get everything.

  • .NET uses a custom command executor for the commands valid for the browser.

  • Ruby adds the browser methods to the remote driver through mixins; they just work.

  • JavaScript returns the browser-specific class (such as chrome.Driver) directly, so no step is needed.

  • Python did not expose them in this run: a webdriver.Remote session had no set_network_conditions, get_sinks, set_permissions, install_addon or full-page-screenshot methods (the first Python test above asserts that). Use the BiDi modules (driver.network, driver.webextension) or a local driver for those features.

@Test
void augmenterAddsTheSameInterfacesToAPlainRemoteDriver() {
  ChromeOptions options = new ChromeOptions();
  options.addArguments("--headless=new");
  driver = new RemoteWebDriver(Env.url(Env.CHROME_GRID), options);
  assertFalse(driver instanceof HasCasting);

  driver = new Augmenter().augment(driver);
  assertTrue(driver instanceof HasCasting);
}

This passed: a driver created with new RemoteWebDriver(...) is not a HasCasting, and after new Augmenter().augment(driver) it is.

The helper server used by the upload and download tests is this class:

import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.Executors;

/** A tiny HTTP server for the two things Bookshelf does not have: a file download and a file upload form. */
final class Fixture {
  static final int PORT = Integer.getInteger("fixture.port", 4519);
  static final String FROM_DOCKER = "http://host.docker.internal:" + PORT;
  private final HttpServer server;

  Fixture() throws IOException {
    server = HttpServer.create(new InetSocketAddress(PORT), 0);
    server.setExecutor(Executors.newCachedThreadPool());
    server.createContext("/download/hello.txt", ex -> {
      byte[] body = "Hello, World!".getBytes(StandardCharsets.UTF_8);
      ex.getResponseHeaders().add("Content-Type", "text/plain");
      ex.getResponseHeaders().add("Content-Disposition", "attachment; filename=\"hello.txt\"");
      ex.sendResponseHeaders(200, body.length);
      ex.getResponseBody().write(body);
      ex.close();
    });
    server.createContext("/upload", ex -> {
      byte[] body = ("<html><body><input type=file id=file>"
          + "<p id=uploaded></p>"
          + "<script>file.onchange = () => uploaded.textContent = file.files[0].name;</script>"
          + "</body></html>").getBytes(StandardCharsets.UTF_8);
      ex.getResponseHeaders().add("Content-Type", "text/html");
      ex.sendResponseHeaders(200, body.length);
      ex.getResponseBody().write(body);
      ex.close();
    });
    server.start();
  }

  void stop() { server.stop(0); }
}

Tracing client requests

The Java client can emit OpenTelemetry traces for every HTTP request to the Grid, so a request can be followed from the client through the router and distributor to the node. Tracing must be enabled on the server and the client, and both need a trace exporter pointing at a visualisation tool such as Jaeger UI; the Java dependencies are an OpenTelemetry exporter and a gRPC transport, and the exporter is configured with system properties (otel.traces.exporter, otel.exporter.jaeger.endpoint and otel.resource.attributes). This is a Java-only feature, and the official page it comes from was written for the Selenium 4.0 betas, so the dependency versions it names are old; check the Grid observability page for current ones. It was not run here. The Java log line of the runs above, Using OpenTelemetry for tracing, shows that the tracer is on by default.

Headless and headed browsers

Headless means the browser runs without showing a window. It is the normal mode for CI and containers; headed is useful for debugging a failing test by watching it. Selenium has no common "headless" switch since the per-browser setters were removed (see "What changed recently"), so you pass the browser’s own flag as an argument:

Browser Flag Notes

Chrome and Edge

--headless=new (also plain --headless)

The browser’s real headless mode, with the same code as the headed browser. The old headless implementation was removed from the Chrome binary in Chrome 132 and lives on as the separate chrome-headless-shell binary.

Firefox

-headless (one dash)

moz:headless is true in the returned capabilities.

Safari

none

Safari has no headless mode. On a Mac it needs a logged-in desktop session.

Internet Explorer

none

The driver needs a normal interactive Windows session.

On this host Chrome 155 started and ran Bookshelf with --headless=new and with plain --headless; both reported a HeadlessChrome/155.0.0.0 user agent. A run with --headless=old also started a session with a HeadlessChrome user agent, although Chrome’s own announcement says Chrome 132 and later print an error message for that flag instead of starting the old mode, so do not rely on it and migrate to --headless=new or to chrome-headless-shell. Selenium Manager does not download chrome-headless-shell (its --browser choices are chrome, firefox, edge, iexplorer, safari, safaritp, webview2 and electron), so you point binary at it yourself. Headless pages can differ from headed ones in default window size, in GPU-dependent rendering and in features that need a window (a headless browser has no real focus or print preview), so set an explicit --window-size, as the Chrome examples below do, rather than relying on the default. Headed runs were not performed on this host, to avoid popping windows up on the desktop; Firefox ran headless in the container.

Chrome

Selenium 4 is compatible with Chrome 75 and later, and the major version of the Chrome browser and of chromedriver must match. Selenium Manager takes care of that by default: it detects the installed Chrome and downloads the matching chromedriver (in these runs, chromedriver 155.0.8059.84 for the installed Chrome 155.0.8059.85). Capabilities unique to Chrome and Chromium are documented by the ChromeDriver project in Capabilities & ChromeOptions. A session with basic options looks like this in every binding:

ChromeOptions options = new ChromeOptions();
options.addArguments("--headless=new");
WebDriver driver = new ChromeDriver(options);
options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
driver = webdriver.Chrome(options=options)
var options = new ChromeOptions();
options.AddArgument("--headless=new");
var driver = new ChromeDriver(options);
options = Selenium::WebDriver::Options.chrome(args: ['--headless=new'])
driver = Selenium::WebDriver.for :chrome, options: options
const options = new chrome.Options().addArguments('--headless=new');
const driver = new Builder().forBrowser(Browser.CHROME).setChromeOptions(options).build();

The Java and Python forms were run (every test above starts that way); the C#, Ruby and JavaScript forms were not.

Chrome options

Option What it does Java and Python

args

Command-line switches used to start the browser. Commonly --start-maximized, --headless=new, --window-size=... and --user-data-dir=.... Two good lists are Chrome Flags for Tooling and the List of Chromium Command Line Switches.

options.addArguments("--window-size=1280,800"), options.add_argument("--window-size=1280,800")

binary

Path of an alternative browser executable, so chromedriver can drive Chrome for Testing, Chromium and other Chromium-based browsers.

options.setBinary(File), options.binary_location = path

extensions

Packed .crx files. Unpacked extension directories need the BiDi route below, or Chrome’s load-extension argument.

options.addExtensions(File...), options.add_extension(path)

detach

Keep the browser open after the process ends, as long as the quit command is not sent. This is already the default in Java and .NET.

options.add_experimental_option("detach", True), JavaScript options.detachDriver(true)

excludeSwitches

chromedriver adds default arguments when it starts the browser; list here the ones you do not want, for example disable-popup-blocking to turn the popup blocker back on. The defaults are in the Chromium source (chrome_launcher.cc).

options.setExperimentalOption("excludeSwitches", List.of("disable-popup-blocking")), options.add_experimental_option("excludeSwitches", ["disable-popup-blocking"])

The binary option deserves a remark about versions: when you set it to a Chrome for Testing build, the chromedriver must match that browser’s major version, not the installed Chrome’s, which is what Selenium Manager arranges when it downloads both (as seen above with Chrome for Testing 156).

To load an unpacked extension in current Chrome, Selenium’s Java documentation uses the WebDriver BiDi webExtension module together with two Chrome arguments. This ran against Bookshelf with a Manifest V3 content-script extension that adds a banner element to every page (manifest.json lists the content script banner.js, which creates a div with the id bookshelf-banner and appends it to the page). The Python equivalent (driver.webextension.install(path=...)) also ran and passed.

@Test
@DisabledOnOs(OS.WINDOWS)
void installUnpackedExtensionOverBiDi() {
  ChromeOptions options = new ChromeOptions();
  options.addArguments("--headless=new");
  options.enableBiDi();
  options.addArguments("--remote-debugging-pipe", "--enable-unsafe-extension-debugging");
  ChromeDriver driver = new ChromeDriver(options);
  try {
    Path dir = Path.of("src/test/resources/extensions/bookshelf-banner-chrome").toAbsolutePath();
    new WebExtension(driver).install(new InstallExtensionParameters(new ExtensionPath(dir.toString())));

    driver.get(Env.BASE_URL + "/books");
    assertEquals("Injected by the bookshelf-banner add-on", driver.findElement(By.id("bookshelf-banner")).getText());
  } finally {
    driver.quit();
  }
}
def test_install_unpacked_extension_over_bidi():
    options = webdriver.ChromeOptions()
    options.add_argument("--headless=new")
    options.enable_bidi = True
    options.add_argument("--remote-debugging-pipe")
    options.add_argument("--enable-unsafe-extension-debugging")
    driver = webdriver.Chrome(options=options)
    try:
        driver.webextension.install(path=EXTENSION_DIR)

        driver.get(BASE_URL + "/books")
        assert driver.find_element(By.ID, "bookshelf-banner").text == "Injected by the bookshelf-banner add-on"
    finally:
        driver.quit()

Installing a packed .crx with add_extension or addExtensions was not run, because no signed or packed extension was available. The BiDi module itself is covered on WebDriver BiDi.

Chrome driver service and logs

Driver logs help when a session will not start. The Service directs where they go, and output is ignored unless you ask for it:

Feature Java Python

Log to a file

new ChromeDriverService.Builder().withLogFile(file).build(), or system property ChromeDriverService.CHROME_DRIVER_LOG_PROPERTY

webdriver.ChromeService(log_output="chromedriver.log")

Log to the console

withLogOutput(System.out) (Selenium 4.10), or the property set to DriverService.LOG_STDOUT or LOG_STDERR

webdriver.ChromeService(log_output=subprocess.STDOUT) (4.11)

Log level

withLogLevel(ChromiumDriverLogLevel.DEBUG) (4.8), or CHROME_DRIVER_LOG_LEVEL_PROPERTY

service_args=["--log-level=DEBUG"]

Append and readable timestamps (file output only)

withAppendLog(true) and withReadableTimestamp(true) (4.8)

service_args=["--append-log", "--readable-timestamp"]

Disable the build check

withBuildCheckDisabled(true) (4.8)

service_args=["--disable-build-check"]

There are six levels: ALL, DEBUG, INFO, WARNING, SEVERE and OFF; --verbose equals --log-level=ALL and --silent equals --log-level=OFF. Append and timestamps need an explicit log path and level, and are handled by the driver rather than the process, so you may see small differences. Disabling the build check lets a driver be used with any Chrome version; it is an unsupported feature and bugs will not be investigated. The file, level and timestamp settings were run in the Service tests above; the console output and the build check were not run.

Chrome special features

These are available on the local ChromeDriver (and on a remote one through the Augmenter or the builder in Java). They also work on Edge, whose driver is Chromium-based. The tests share this set-up, which keeps the driver’s browser and performance log buffers:

ChromeDriver driver;

@BeforeEach
void start() {
  ChromeOptions options = new ChromeOptions();
  options.addArguments("--headless=new", "--window-size=1280,800");
  // Keep the driver's log buffers: the browser console and Chrome's performance (CDP) events.
  LoggingPreferences prefs = new LoggingPreferences();
  prefs.enable(LogType.BROWSER, Level.ALL);
  prefs.enable(LogType.PERFORMANCE, Level.ALL);
  options.setCapability(ChromeOptions.LOGGING_PREFS, prefs);
  driver = new ChromeDriver(options);
}

@AfterEach
void stop() { driver.quit(); }
@pytest.fixture
def driver():
    options = webdriver.ChromeOptions()
    options.add_argument("--headless=new")
    options.add_argument("--window-size=1280,800")
    # Keep the driver's log buffers: the browser console and Chrome's performance (CDP) events.
    options.set_capability("goog:loggingPrefs", {"browser": "ALL", "performance": "ALL"})
    d = webdriver.Chrome(options=options)
    yield d
    d.quit()

Network conditions simulate latency, throughput and being offline for the whole browser session, through ChromeDriver’s chromium/network_conditions command. Throughput is in bytes per second (the test converts kilobits), and -1 means no limit. deleteNetworkConditions / delete_network_conditions restores the defaults. Be careful: conditions persist for the session, so a test that sets them and fails before resetting them poisons later steps.

@Test
void networkConditionsSlowTheBrowserDown() {
  ChromiumNetworkConditions slow = new ChromiumNetworkConditions();
  slow.setOffline(false);
  slow.setLatency(Duration.ofMillis(400));
  slow.setDownloadThroughput(500 * 1024 / 8); // 500 kbps
  slow.setUploadThroughput(500 * 1024 / 8);
  driver.setNetworkConditions(slow);
  assertEquals(Duration.ofMillis(400), driver.getNetworkConditions().getLatency());

  long start = System.nanoTime();
  driver.get(Env.BASE_URL + "/books");
  long slowMs = (System.nanoTime() - start) / 1_000_000;

  driver.deleteNetworkConditions();
  start = System.nanoTime();
  driver.get(Env.BASE_URL + "/books");
  long fastMs = (System.nanoTime() - start) / 1_000_000;
  System.out.printf("with latency 400 ms: %d ms, without: %d ms%n", slowMs, fastMs);
  assertTrue(slowMs > fastMs + 300);
}

@Test
void offlineMakesNavigationFail() {
  ChromiumNetworkConditions offline = new ChromiumNetworkConditions();
  offline.setOffline(true);
  offline.setLatency(Duration.ZERO);
  offline.setDownloadThroughput(-1); // -1 means no throttling
  offline.setUploadThroughput(-1);
  driver.setNetworkConditions(offline);
  WebDriverException e = assertThrows(WebDriverException.class, () -> driver.get(Env.BASE_URL + "/books"));
  System.out.println(e.getMessage().lines().findFirst().orElse(""));
  assertTrue(e.getMessage().contains("ERR_INTERNET_DISCONNECTED"));
}
def test_network_conditions_slow_the_browser_down(driver):
    driver.set_network_conditions(offline=False, latency=400, download_throughput=500 * 1024 // 8,
                                  upload_throughput=500 * 1024 // 8)
    assert driver.get_network_conditions()["latency"] == 400

    start = time.monotonic()
    driver.get(BASE_URL + "/books")
    slow_ms = round((time.monotonic() - start) * 1000)

    driver.delete_network_conditions()
    start = time.monotonic()
    driver.get(BASE_URL + "/books")
    fast_ms = round((time.monotonic() - start) * 1000)
    print(f"with latency 400 ms: {slow_ms} ms, without: {fast_ms} ms")
    assert slow_ms > fast_ms + 300


def test_offline_makes_navigation_fail(driver):
    driver.set_network_conditions(offline=True, latency=0, download_throughput=-1, upload_throughput=-1)
    with pytest.raises(WebDriverException) as error:
        driver.get(BASE_URL + "/books")
    print(error.value.msg.splitlines()[0])
    assert "ERR_INTERNET_DISCONNECTED" in error.value.msg

Both ran and passed. The output of the Java run shows the 400 ms latency was felt on the request (about a second against 24 ms without), and that offline mode makes the navigation fail with Chrome’s own error:

with latency 400 ms: 1185 ms, without: 24 ms
unknown error: net::ERR_INTERNET_DISCONNECTED

Logs. Chrome’s driver exposes the browser console and performance (CDP event) logs through the old logging API. They must be requested through the goog:loggingPrefs capability (a LoggingPreferences object in Java), after which driver.manage().logs().get(LogType.BROWSER) in Java and driver.get_log("browser") in Python return and clear the buffered entries. The available types were [driver, browser, performance]. For live console events, prefer the BiDi log module (see WebDriver BiDi).

@Test
void browserConsoleAndPerformanceLogs() {
  driver.get(Env.BASE_URL + "/");
  driver.executeScript("console.error('bookshelf test message')");

  LogEntries console = driver.manage().logs().get(LogType.BROWSER);
  console.forEach(e -> System.out.println(e.getLevel() + " " + e.getMessage()));
  assertTrue(console.getAll().stream().anyMatch(e -> e.getMessage().contains("bookshelf test message")));

  LogEntries performance = driver.manage().logs().get(LogType.PERFORMANCE);
  assertFalse(performance.getAll().isEmpty());
  System.out.println("available log types: " + driver.manage().logs().getAvailableLogTypes());
}
def test_browser_console_and_performance_logs(driver):
    driver.get(BASE_URL + "/")
    driver.execute_script("console.error('bookshelf test message')")

    console = driver.get_log("browser")
    for entry in console:
        print(entry["level"], entry["message"])
    assert any("bookshelf test message" in entry["message"] for entry in console)

    assert driver.get_log("performance")
    print("available log types:", driver.log_types)

Both passed; the console entry came back as SEVERE console-api 2:32 "bookshelf test message".

Permissions. setPermission("camera", "denied") (set_permissions in Python) sets a permission state without any prompt, so the test can then assert what the page sees:

@Test
void permissions() {
  driver.get(Env.BASE_URL + "/");
  driver.setPermission("camera", "denied");
  Object state = driver.executeAsyncScript(
      "const done = arguments[arguments.length - 1];"
          + "navigator.permissions.query({name: 'camera'}).then(s => done(s.state));");
  assertEquals("denied", state);
}
def test_permissions(driver):
    driver.get(BASE_URL + "/")
    driver.set_permissions("camera", "denied")
    state = driver.execute_async_script(
        "const done = arguments[arguments.length - 1];"
        "navigator.permissions.query({name: 'camera'}).then(s => done(s.state));")
    assert state == "denied"

Casting. ChromeDriver can drive Chrome Cast devices, including sharing tabs. The methods are getCastSinks, selectCastSink, startTabMirroring, startDesktopMirroring, getCastIssueMessage and stopCasting in Java and get_sinks, set_sink_to_use, start_tab_mirroring, start_desktop_mirroring, get_issue_message and stop_casting in Python. They need a Cast device on the network; there was none here, so the test only exercises the discovery call and the rest is guarded by if sinks:

@Test
void castingNeedsACastDevice() {
  List<Map<String, String>> sinks = driver.getCastSinks();
  System.out.println("cast sinks: " + sinks);
  if (!sinks.isEmpty()) {
    String name = sinks.get(0).get("name");
    driver.startTabMirroring(name);
    driver.stopCasting(name);
  }
}
def test_casting_needs_a_cast_device(driver):
    sinks = driver.get_sinks()
    print("cast sinks:", sinks)
    if sinks:
        name = sinks[0]["name"]
        driver.start_tab_mirroring(name)
        driver.stop_casting(name)

It passed and printed cast sinks: []; mirroring a tab to a real device was not exercised.

DevTools. The Chrome DevTools Protocol integration (network interception, emulation, performance data and so on) is documented on the WebDriver BiDi page, together with the BiDi equivalents. Selenium 4.51 supports CDP versions 153 to 155; when the browser is newer, Selenium logs that no exact match exists and uses the closest version, as it did with Chrome for Testing 156 above.

Microsoft Edge

Microsoft Edge is built on Chromium, with Edge 79 as the earliest supported version, and msedgedriver follows the same rule as chromedriver: its major version must match the browser’s. Everything in the Chrome section applies, with the Edge class names: EdgeOptions and EdgeDriverService in Java, webdriver.EdgeOptions, webdriver.EdgeService and webdriver.Edge in Python. Arguments (including --headless=new), binary, extensions, detach, excludeSwitches, driver logging, casting, network conditions, logs, permissions and DevTools behave as on Chrome. The headless flag is the same as on Chrome, and the options are stored under ms:edgeOptions. Edge is not installed on this host, so this section was not run; the code below follows the official examples:

EdgeOptions options = new EdgeOptions();
options.addArguments("--headless=new");
EdgeDriverService service = new EdgeDriverService.Builder()
    .withLogFile(new File("msedgedriver.log"))
    .withLoglevel(ChromiumDriverLogLevel.DEBUG)   // note the spelling: withLoglevel, not withLogLevel
    .build();
WebDriver driver = new EdgeDriver(service, options);
options = webdriver.EdgeOptions()
options.add_argument("--headless=new")
service = webdriver.EdgeService(log_output="msedgedriver.log", service_args=["--log-level=DEBUG"])
driver = webdriver.Edge(service=service, options=options)

Two Edge-specific points: the Java builder method for the log level is spelled withLoglevel on EdgeDriverService.Builder (checked against the 4.51.0 jar), unlike the withLogLevel of the Chrome and Firefox builders; and Edge file output for the Java service is available from Selenium 4.10. EdgeOptions.useWebView(true) selects the WebView2 control instead of the Edge browser (WebView2 support was added in Selenium 4.10); Playwright’s WebView2 support is covered in Playwright Advanced Topics.

Internet Explorer mode in Edge

Edge can run pages in an "Internet Explorer Compatibility Mode", and Selenium drives that mode with the Internet Explorer driver classes together with Edge, as described in the Internet Explorer section below.

Firefox

Selenium 4 requires Firefox 78 or later and works with geckodriver, whose latest release is the recommended one. In the Firefox container used here, Firefox was 158.0 and geckodriver 0.37.1. Firefox options live under moz:firefoxOptions (documented by Mozilla in firefoxOptions). Selenium Manager downloads geckodriver when needed. On this macOS host, Selenium could not start the installed Firefox through geckodriver (the session hung and timed out), so every Firefox example on this page ran against the Firefox container, with RemoteWebDriver rather than a local FirefoxDriver; the local forms below are documented, not run.

Firefox options

Option What it does Java and Python

args

Command-line arguments. Common ones are -headless and -profile /path/to/profile (note the single dash).

options.addArguments("-headless"), options.add_argument("-headless")

binary

An alternative Firefox location, for example Firefox Nightly instead of the release version when both are installed.

options.setBinary(Path), options.binary_location = path

preferences

about:config values set for the session.

options.addPreference("intl.accept_languages", "en-US"), options.set_preference("intl.accept_languages", "en-US")

profile

A FirefoxProfile, empty or based on an existing profile directory.

options.setProfile(profile), options.profile = profile

Whether you create an empty FirefoxProfile or point it at your own profile directory, Selenium creates a temporary directory to hold the new profile or a copy of the existing one, and a different directory each run. Selenium does not clean these up (the operating system eventually does); if a copied profile is large, remove it yourself, using the path the FirefoxProfile object exposes. For finding the profile directory, see Mozilla’s profiles documentation.

This test starts a headless Firefox in the container and checks the browser name and the Bookshelf page title. It shares the start helper below with the add-on, screenshot and context tests (the class keeps a driver field and quits it in an @AfterEach, as in the Remote WebDriver tests):

private WebDriver start() {
  FirefoxOptions options = new FirefoxOptions();
  options.addArguments("-headless");
  options.addPreference("intl.accept_languages", "en-US");
  return RemoteWebDriver.builder().address(Env.url(Env.FIREFOX_GRID)).oneOf(options).build();
}

@Test
void headlessFirefox() {
  driver = start();
  driver.get(Env.BASE_URL_FROM_DOCKER + "/books");
  var caps = ((RemoteWebDriver) driver).getCapabilities();
  System.out.println(caps.getBrowserName() + " " + caps.getBrowserVersion());
  assertEquals("firefox", caps.getBrowserName());
  assertEquals("Catalogue - Bookshelf", driver.getTitle());
}
def test_headless_firefox_in_a_container():
    options = webdriver.FirefoxOptions()
    options.add_argument("-headless")
    options.set_preference("intl.accept_languages", "en-US")
    driver = webdriver.Remote(command_executor=FIREFOX_GRID, options=options)
    try:
        driver.get(BASE_URL_FROM_DOCKER + "/books")
        caps = driver.capabilities
        print(caps["browserName"], caps["browserVersion"])
        assert caps["browserName"] == "firefox"
        assert driver.title == "Catalogue - Bookshelf"
    finally:
        driver.quit()

Both passed and printed firefox 158.0. The local forms are new FirefoxDriver(options) in Java and webdriver.Firefox(options=options) in Python.

Firefox driver service and logs

The geckodriver Service is similar to Chrome’s, with Firefox’s own log levels:

Feature Java Python

Log to a file or the console

new GeckoDriverService.Builder().withLogFile(file) or withLogOutput(System.out)

webdriver.FirefoxService(log_output=path_or_stream)

Log level

withLogLevel(FirefoxDriverLogLevel.DEBUG) (4.10)

service_args=["--log", "debug"]

Truncated logs

withTruncatedLogs(false) (4.10). The driver logs everything sent to it, including large binaries, so it truncates lines by default.

service_args=["--log-no-truncate"]

Profile root

withProfileRoot(File) (4.10): where profiles are created instead of the system temporary directory.

service_args=["--profile-root", directory]

System access

withAllowSystemAccess(true)

service_args=["--allow-system-access"]

There are seven levels: fatal, error, warn, info, config, debug and trace, with info the default once logging is on; -v equals -log debug and -vv equals -log trace. These service settings apply to a local geckodriver and were not run here, because in a Grid the node starts geckodriver for you.

Firefox special features

Add-ons. Unlike Chrome extensions, Firefox add-ons are not part of the capabilities (see the geckodriver discussion in issue 1476); they are installed after the driver has started. A signed .xpi from the Mozilla add-ons site installs permanently for the session. An unfinished extension is unsigned, and can only be installed as temporary, by passing a zip file or a directory (Selenium 4.5). Uninstalling needs the add-on id, which the install call returns. The example extension adds a banner element, like the Chrome one. Its manifest.json carries a browser_specific_settings.gecko.id (bookshelf-banner@example.test) and lists the content script banner.js:

@Test
void temporaryUnsignedAddon() {
  driver = start();
  Path dir = Path.of("src/test/resources/extensions/bookshelf-banner");
  String id = ((HasExtensions) driver).installExtension(dir, true);
  System.out.println("add-on id: " + id);

  driver.get(Env.BASE_URL_FROM_DOCKER + "/books");
  String text = new WebDriverWait(driver, Duration.ofSeconds(10))
      .until(d -> d.findElement(By.id("bookshelf-banner"))).getText();
  assertEquals("Injected by the bookshelf-banner add-on", text);

  ((HasExtensions) driver).uninstallExtension(id);
  driver.navigate().refresh();
  assertTrue(driver.findElements(By.id("bookshelf-banner")).isEmpty());
}

It ran and passed through the remote driver, printing add-on id: bookshelf-banner@example.test; that works because the builder returns an augmented driver, as described in the Remote WebDriver section. The local Python form is driver.install_addon(path, temporary=True) followed by driver.uninstall_addon(id). Python’s webdriver.Remote has no such method, so that form was not run:

driver = webdriver.Firefox(options=options)
addon_id = driver.install_addon("/path/to/bookshelf-banner", temporary=True)
driver.get(f"{BASE_URL}/books")
assert driver.find_element(By.ID, "bookshelf-banner").text == "Injected by the bookshelf-banner add-on"
driver.uninstall_addon(addon_id)

Full-page screenshots. An ordinary screenshot captures the viewport. Firefox can capture the whole scrollable page, using Java getFullPageScreenshotAs(OutputType) on HasFullPageScreenshot or Python save_full_page_screenshot(path) (also get_full_page_screenshot_as_file, _png and _base64). It is not part of the W3C standard, so it works only with geckodriver.

@Test
void fullPageScreenshot() throws Exception {
  driver = start();
  driver.get(Env.BASE_URL_FROM_DOCKER + "/books");
  byte[] png = ((HasFullPageScreenshot) driver).getFullPageScreenshotAs(OutputType.BYTES);
  Path out = Files.createTempFile("full-page-", ".png");
  Files.write(out, png);
  System.out.println("full page screenshot: " + png.length + " bytes");
  assertTrue(png.length > 1000);
}

It passed against the container (the catalogue page produced a PNG of about 108 KB). The local Python form is driver.save_full_page_screenshot("full_page.png"). See Screenshots, Videos & Traces for evidence capture in general.

Context. Firefox can run commands in the page (content, the default) or in the browser itself (chrome), where privileged scripts can touch the browser interface. Since Firefox 138, geckodriver must be started with --allow-system-access to switch to the chrome context. Java uses setContext(FirefoxCommandContext.CHROME) and Python a context manager, with driver.context(driver.CONTEXT_CHROME): ....

@Test
void contentContextIsTheDefaultAndChromeNeedsSystemAccess() {
  driver = start();
  HasContext withContext = (HasContext) driver;
  assertEquals(FirefoxCommandContext.CONTENT, withContext.getContext());

  // Firefox 138+ refuses the CHROME context unless geckodriver was started with --allow-system-access.
  var e = assertThrows(Exception.class, () -> withContext.setContext(FirefoxCommandContext.CHROME));
  System.out.println(e.getClass().getSimpleName() + ": " + e.getMessage().lines().findFirst().orElse(""));
}

It passed. The container’s geckodriver was not started with the flag, so the chrome switch was refused, which is the expected result here:

UnsupportedCommandException: System access is required. Start Firefox with "-remote-allow-system-access" to enable it.

Passing -remote-allow-system-access as a Firefox argument in the options did not help: the node answered Argument --remote-allow-system-access can't be set via capabilities. For a remote Firefox you therefore have to start the node’s geckodriver with --allow-system-access through the Grid’s driver configuration (see Selenium Grid); that was not set up here, so a successful chrome context switch was not run.

Safari

Unlike the Chromium and Firefox drivers, safaridriver is installed with the operating system (this host has the one shipped with Safari 27.0.1), so there is nothing for Selenium Manager to download. Automation must be enabled once, from a terminal:

safaridriver --enable

--enable applies configuration changes so that later sessions run without further authentication, and it asks for an administrator password; the same switch exists in Safari as the Develop menu item Allow Remote Automation. safaridriver also has -p (port), -b (a port for WebDriver BiDi WebSocket connections), --diagnose (diagnostic logging) and, in this version, --mcp (run as an MCP server over standard input and output). Enabling remote automation is a system setting, which was not changed on this host, so a Safari session was attempted but not completed: starting webdriver.Safari() fails with the message that "Allow remote automation" must be enabled in Safari’s Developer settings. The code below follows the official examples and was not run.

SafariOptions options = new SafariOptions();
SafariDriverService service = new SafariDriverService.Builder().withLogging(true).build();
WebDriver driver = new SafariDriver(service, options);
options = webdriver.SafariOptions()
service = webdriver.SafariService(enable_logging=True)
driver = webdriver.Safari(service=service, options=options)

Key points:

  • Capabilities specific to Safari are listed by Apple in About WebDriver for Safari; the Java SafariOptions offers setAutomaticInspection, setAutomaticProfiling, setUseTechnologyPreview and enableBiDi.

  • Safari on iOS is not automated with Selenium; the documentation points to the Appium project.

  • There is no headless mode, and logging is only on or off: you cannot choose the location or level. When on, the logs are written to ~/Library/Logs/com.apple.WebDriver/ (Java withLogging(true) or the system property SafariDriverService.SAFARI_DRIVER_LOGGING, Python SafariService(enable_logging=True)).

Safari Technology Preview

Apple provides a development version of its browser, Safari Technology Preview, with its own driver. Select it with the option instead of a different driver class:

SafariOptions options = new SafariOptions();
options.setUseTechnologyPreview(true);
WebDriver driver = new SafariDriver(options);
options = webdriver.SafariOptions()
options.use_technology_preview = True
service = webdriver.SafariService(
    executable_path="/Applications/Safari Technology Preview.app/Contents/MacOS/safaridriver")
driver = webdriver.Safari(options=options, service=service)

Safari Technology Preview is not installed here, so this was not run. In the Python form the Service points at the preview’s own safaridriver.

Internet Explorer and IE mode

As of June 2022 Selenium no longer officially supports standalone Internet Explorer. What remains is the Internet Explorer driver’s ability to run Microsoft Edge in IE Compatibility Mode, which is the way to test sites that still need Internet Explorer behaviour. The IE driver is the only driver maintained by the Selenium project itself. Both 32-bit and 64-bit builds of IEDriverServer.exe exist, and the bitness of the driver decides the bitness of the browser it launches, but the 64-bit driver has known limitations, so the 32-bit one is recommended. IE mode runs only on Windows, so none of this section was run on this macOS host; the code follows the official examples.

Starting IE mode

Since IE Driver 4.5.0 the options are simple. If Internet Explorer is not on the system (the default on Windows 11), you need no special options: the driver uses Edge and locates it. If both are present, you only need to ask to attach to Edge. On Windows 10 with an explicit location:

InternetExplorerOptions options = new InternetExplorerOptions();
options.attachToEdgeChrome();
options.withEdgeExecutablePath(System.getenv("EDGE_BIN"));
WebDriver driver = new InternetExplorerDriver(options);
options = webdriver.IeOptions()
options.attach_to_edge_chrome = True
options.edge_executable_path = os.environ["EDGE_BIN"]
driver = webdriver.Ie(options=options)
var options = new InternetExplorerOptions();   // Windows 11: nothing else is needed
var driver = new InternetExplorerDriver(options);

Microsoft documents the Edge side of IE mode (enabling it and the site list) in Use Internet Explorer mode in Microsoft Edge with WebDriver.

IE options

Capability What it does Java and Python

fileUploadDialogTimeout

The driver times out after 1000 ms when opening the file-upload dialog; raise it in slow environments.

options.waitForUploadDialogUpTo(Duration.ofSeconds(2)), options.file_upload_timeout = 2000

ensureCleanSession

Clears the cache, history and cookies of all running Internet Explorer instances, including ones started by hand. Default false. It slows the launch, because the driver waits for the cache to clear.

options.destructivelyEnsureCleanSession(), options.ensure_clean_session = True

ignoreZoomSetting

The driver expects a 100% zoom level and throws otherwise; this turns the check off.

options.ignoreZoomSettings(), options.ignore_zoom_level = True

ignoreProtectedModeSettings

Skips the check that every security zone has the same Protected Mode setting. Tests may become flaky, unresponsive or hang; only "best effort" support is given. The right fix is to set Protected Mode on every zone.

options.introduceFlakinessByIgnoringSecurityDomains(), options.ignore_protected_mode_settings = True

silent

Suppresses the driver’s diagnostic output.

options.setCapability("silent", true), IeService(service_args=["--silent"])

command-line options for IE

-private (private browsing, IE 8+), -k (kiosk mode) and -extoff (no add-ons, IE 7+). They only work together with forceCreateProcessApi.

options.useCreateProcessApiToLaunchIe() with options.addCommandSwitches("-k"), Python options.add_argument("-private")

forceCreateProcessApi

Launches Internet Explorer through the CreateProcess API (default false); for IE 8 and later it needs the TabProcGrowth registry value set to 0.

options.useCreateProcessApiToLaunchIe()

IE driver service

The Service for IE directs the log file and level and the place where supporting files are extracted. File output and console output have been available since Selenium 4.10; the log levels are FATAL, ERROR, WARN, INFO, DEBUG and TRACE, and when logging output is specified the default level is FATAL. The Java builder methods are withLogFile, withLogOutput, withLogLevel(InternetExplorerDriverLogLevel...) and withExtractPath.

The IE Driver Server

IEDriverServer.exe is a standalone server that implements the WebDriver protocol. It was tested with Internet Explorer 11 on Windows 10 and may work with older versions, which is not supported. You do not need an installer: download the executable from the Selenium downloads page and put it in the PATH (Selenium Manager can also find it since Selenium 4.7). Its command-line switches (-, -- and / prefixes all work) are:

Switch Meaning

--port=<portNumber>

The port of the driver’s HTTP server. Default 5555.

--host=<hostAdapterIPAddress>

The IP address of the host adapter to listen on. Default 127.0.0.1.

--log-level=<logLevel>

FATAL, ERROR, WARN, INFO, DEBUG or TRACE. Default FATAL.

--log-file=<logFile>

Full path of the log file. Default is standard output.

--extract-path=<path>

Directory used to extract supporting files. Default is the TEMP directory.

--silent

Suppresses diagnostic output at start-up.

In Java the same settings can be passed as the system properties webdriver.ie.driver (the binary location), webdriver.ie.driver.host, webdriver.ie.driver.loglevel, webdriver.ie.driver.logfile, webdriver.ie.driver.silent and webdriver.ie.driver.extractpath.

The driver needs this configuration on the target machine:

  • The Protected Mode setting must be the same for every zone (on or off), and Enhanced Protected Mode must be off for IE 10 and later. Internet Explorer needs a new browser session when a navigation crosses a Protected Mode boundary, which orphans the COM object the driver controls; requiring identical settings avoids the transition. Current drivers check the settings and return an error if they differ.

  • The browser zoom must be 100%, so that native mouse events get the right coordinates, and on Windows 10 "Change the size of text, apps, and other items" must also be 100%.

  • For IE 11 only, a registry DWORD named iexplore.exe with value 0 must exist under HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_BFCACHE (under Wow6432Node on 64-bit Windows), so that the driver keeps its connection to the browser.

Other behaviour to know about:

  • The driver uses native events (operating-system input) for the mouse and keyboard where it can, instead of simulated JavaScript events. Native events do not depend on the JavaScript sandbox and give proper event propagation, but the browser window needs focus, and the machine is effectively unavailable for other work while a test runs. Hovering fails while the real mouse pointer is inside the browser window.

  • Clicking an option element and submitting a form with submit() use JavaScript instead of native events, so if the page’s onchange or onsubmit handler calls alert(), confirm() or prompt(), the call blocks until the dialog is dismissed by hand, and there is no WebDriver-only workaround.

  • Several instances of the driver are largely untested and may share cookies and window focus. The two documented workarounds are to start IE in private mode (ie.forceCreateProcessApi true plus ie.browserCommandLineSwitches -private, IE 8 and later, with TabProcGrowth 0) or to set ie.ensureCleanSession. Otherwise use Remote WebDriver and virtual machines.

  • The server accepts connections only from the local machine, and this cannot be changed without modifying its source. To run IE on another machine, use the Java standalone server with the Remote WebDriver class of your binding.

  • Running IEDriverServer.exe inside a Windows Service is explicitly unsupported.

The driver’s internals page adds that the driver talks to a local HTTP server using the W3C protocol and keeps the native interop surface small, uses ATL smart pointers to manage COM objects, and synthesises keyboard and mouse input by posting window messages to the browser instance (for typing, only key-down and key-up messages are posted, plus a "probe" event so key-press events are ordered correctly).

Pitfalls

  • Versions of browser and driver must match. Let Selenium Manager choose; set a driver path only for a reason, and remember that a browser binary set through binary is the one the driver must match.

  • Units differ between bindings. Timeouts are Duration in Java, milliseconds in Python’s options.timeouts and seconds in Python’s driver.timeouts.

  • Do not send return-only capabilities. setWindowRect is reported by the driver and rejected as input.

  • Use the right headless flag. --headless=new for Chrome and Edge, -headless for Firefox; Safari and IE have none.

  • The default page-load strategy waits for everything. Use eager or none only with explicit waits, and not for pages whose scripts build the content after DOMContentLoaded.

  • quit(), not close(). Leaked drivers and browsers are the usual CI slowdown.

  • Remote sessions do not see your files or localhost. Set a file detector for uploads, enable managed downloads for downloads, and give the remote browser an address it can reach (host.docker.internal for a container, a service name in Compose or Kubernetes).

  • Browser-specific features need an augmented driver. In Java use the builder or the Augmenter; in Python use a local driver or BiDi.

  • Conditions and permissions persist for the session. Network conditions and permissions set through the driver stay until deleted or until quit().

  • Never put credentials in code. Read Grid and application passwords from the environment.

What changed recently

Version Change

4.51.0 (October 2026)

CDP versions 153 to 155 supported. Guava removed from the Java client bindings. RemoteWebDriver.getClientFactory() added. A bare --proxy is no longer passed to Selenium Manager.

4.50.0

Deprecated methods removed from the Java HttpClient interface, and the deprecated Firefox profile extension and certificate methods removed. Python applies the ClientConfig user_agent and extra_headers per connection.

4.47.0

The Java Chrome service passes --enable-chrome-logs unless the CHROME_LOG_FILE environment variable is set.

4.40.0

The Java client uses the ClientConfig you provide instead of a default one.

4.37.0

Python: the WebSocket timeout and wait interval are configurable through ClientConfig.

4.34.0

Java: RemoteWebDriver.isDownloadsEnabled(). Python gained service-argument properties.

4.17.0

Selenium recognises chrome-headless-shell as Chrome. The Java server stops accepting session requests that use the Selenium 3 capabilities format.

4.15.0

File downloads from remote sessions introduced in the Java client (managed downloads in the Grid first appeared in 4.8.2).

4.13.0

The deprecated per-browser headless methods were removed from the Options classes (Java and Python); pass the browser’s headless flag as an argument.

4.11

Python Service classes accept the values described above; Selenium Manager can download browsers.

4.10

Edge log-file and console output for the Java service, WebView2 support in Edge, Firefox service log level, truncation and profile root.

4.8

setHeadless deprecated in the Chrome and Firefox Options classes; Java Chrome service gained log level, append-log and build-check options.

4.6 and 4.7

Selenium Manager bundled in the bindings (4.6); IE driver support added to Selenium Manager (4.7).

Chrome 132 (January 2025)

--headless=old removed from the Chrome binary; --headless and --headless=new run the new mode, and the old one is available as chrome-headless-shell.

June 2022

Standalone Internet Explorer no longer supported; IE mode in Edge remains.