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 ( |
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 ( |
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 ( |
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. |
Source: Selenium — Driver Sessions
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 |
|---|---|---|---|
|
set by the class ( |
set by the class |
Read it back with |
|
|
|
Selects an installed or downloadable browser version. When the version is not found locally, Selenium Manager downloads it. |
|
|
|
Identifies the operating system of the remote end. With cloud providers, setting it picks the operating system. |
|
|
|
Trust expired or self-signed TLS certificates for the whole session. With |
|
|
|
When a navigation counts as finished, see the next section. |
|
|
|
Session timeouts, see the table below. |
|
|
|
What to do with an alert, confirm or prompt dialog nobody handled: |
|
|
|
Apply the strict interactability checks to |
|
|
|
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. |
|
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 |
|---|---|---|
|
30,000 ms |
How long an executing script may run before it is interrupted. |
|
300,000 ms |
How long a navigation may take. When it is exceeded the command fails with a |
|
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());
}
}
Source: Selenium — Browser Options
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"
Source: Selenium — Browser Options
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.
Source: Selenium — Browser Options
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 |
|---|---|---|---|
|
|
the |
The safe default; the page is as the user sees it. |
|
|
|
Faster tests when sub-resources do not matter to the test. |
|
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 |
Driver location |
Java |
Port |
Java |
Log output |
Java |
Log level and driver switches |
Java builder methods ( |
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();
}
}
Source: Selenium — Driver Service Class
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()
Source: Selenium — Driver Service Class
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 |
|
msedgedriver |
|
geckodriver |
|
IEDriverServer |
|
safaridriver |
|
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);
}
Source: Selenium — Remote WebDriver
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()
Source: Selenium — Remote WebDriver
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 |
Python |
Adds a |
.NET |
Adds a local file detector by default; set |
Ruby |
Adds one by default; a custom detector is a lambda. |
JavaScript |
No file detector by default; call |
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 throughSE_NODE_ENABLE_MANAGED_DOWNLOADS=true), and -
the client sets the
se:downloadsEnabledcapability, which every binding exposes on its Options class (setEnableDownloads(true)in Java,options.enable_downloads = Truein Python,EnableDownloads = truein C#,enable_downloads: truein 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
RemoteWebDriverthat lacks the browser interfaces. TheAugmenteradds 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.Remotesession had noset_network_conditions,get_sinks,set_permissions,install_addonor 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); }
}
Source: Selenium — Remote WebDriver
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 |
|
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 |
Firefox |
|
|
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.
Source: Selenium — Chrome: Arguments
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 |
|---|---|---|
|
Command-line switches used to start the browser. Commonly |
|
|
Path of an alternative browser executable, so chromedriver can drive Chrome for Testing, Chromium and other Chromium-based browsers. |
|
|
Packed |
|
|
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. |
|
|
chromedriver adds default arguments when it starts the browser; list here the ones you do not want, for example |
|
Source: Selenium — Chrome: Options
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 |
|
|
Log to the console |
|
|
Log level |
|
|
Append and readable timestamps (file output only) |
|
|
Disable the 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.
Source: Selenium — Chrome: Service
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());
}
Source: Selenium — Chrome: Logs
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)
Source: Selenium — Chrome: Logs
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);
}
Source: Selenium — Chrome: Permissions
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"
Source: Selenium — Chrome: Permissions
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);
}
}
Source: Selenium — Chrome: Casting
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)
Source: Selenium — Chrome: Casting
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.
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 |
|---|---|---|
|
Command-line arguments. Common ones are |
|
|
An alternative Firefox location, for example Firefox Nightly instead of the release version when both are installed. |
|
preferences |
|
|
profile |
A |
|
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());
}
Source: Selenium — Firefox: Options
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()
Source: Selenium — Firefox: Options
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 |
|
|
Log level |
|
|
Truncated logs |
|
|
Profile root |
|
|
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.
Source: Selenium — Firefox: Service
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());
}
Source: Selenium — Firefox: Add-ons
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)
Source: Selenium — Firefox: Add-ons
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(""));
}
Source: Selenium — Firefox: Context
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
SafariOptionsofferssetAutomaticInspection,setAutomaticProfiling,setUseTechnologyPreviewandenableBiDi. -
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/(JavawithLogging(true)or the system propertySafariDriverService.SAFARI_DRIVER_LOGGING, PythonSafariService(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 |
|---|---|---|
|
The driver times out after 1000 ms when opening the file-upload dialog; raise it in slow environments. |
|
|
Clears the cache, history and cookies of all running Internet Explorer instances, including ones started by hand. Default |
|
|
The driver expects a 100% zoom level and throws otherwise; this turns the check off. |
|
|
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. |
|
|
Suppresses the driver’s diagnostic output. |
|
command-line options for IE |
|
|
|
Launches Internet Explorer through the CreateProcess API (default |
|
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 |
|---|---|
|
The port of the driver’s HTTP server. Default 5555. |
|
The IP address of the host adapter to listen on. Default 127.0.0.1. |
|
|
|
Full path of the log file. Default is standard output. |
|
Directory used to extract supporting files. Default is the TEMP directory. |
|
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.exewith value 0 must exist underHKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_BFCACHE(underWow6432Nodeon 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
optionelement and submitting a form withsubmit()use JavaScript instead of native events, so if the page’sonchangeoronsubmithandler callsalert(),confirm()orprompt(), 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.forceCreateProcessApitrue plusie.browserCommandLineSwitches-private, IE 8 and later, withTabProcGrowth0) or to setie.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.exeinside a Windows Service is explicitly unsupported.
Source: Selenium — IE Driver Server
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
binaryis the one the driver must match. -
Units differ between bindings. Timeouts are
Durationin Java, milliseconds in Python’soptions.timeoutsand seconds in Python’sdriver.timeouts. -
Do not send return-only capabilities.
setWindowRectis reported by the driver and rejected as input. -
Use the right headless flag.
--headless=newfor Chrome and Edge,-headlessfor Firefox; Safari and IE have none. -
The default page-load strategy waits for everything. Use
eagerornoneonly with explicit waits, and not for pages whose scripts build the content afterDOMContentLoaded. -
quit(), notclose(). 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.internalfor 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. |
4.50.0 |
Deprecated methods removed from the Java |
4.47.0 |
The Java Chrome service passes |
4.40.0 |
The Java client uses the |
4.37.0 |
Python: the WebSocket timeout and wait interval are configurable through |
4.34.0 |
Java: |
4.17.0 |
Selenium recognises |
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 |
|
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) |
|
June 2022 |
Standalone Internet Explorer no longer supported; IE mode in Edge remains. |