Selenium: Getting Started
|
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. |
Selenium automates browsers through the W3C WebDriver protocol: your test code (the language binding) talks to a small driver
program such as chromedriver, and the driver controls the browser. This page takes you from nothing to a running suite: it installs
the library in each supported language, walks through the eight components that every Selenium script is made of, shows how to run
the script headless or headed, organises it with a test runner, and explains Selenium Manager, the tool that finds, downloads and
caches the drivers and browsers for you. It was written against Selenium 4.51.0 (released 9 October 2026) and checked on 25 October
2026. The Java, Python, JavaScript and C# examples were run against the Bookshelf
application on a local port with headless Chrome 155 on macOS (arm64); the Ruby examples were run on Linux arm64 in a Docker
container with Ruby 3.3.13 and the Debian build of Chromium 156 against the same application, because the macOS host ships Ruby 2.6,
which Selenium 4.51 no longer supports. Where something was not run, the text says so. For why WebDriver is built the way it is, see
How Browser Automation Works; for the other tools see
Choosing a Tool. Later Selenium pages cover
drivers, browsers and options,
locating and interacting,
waits and
Selenium Grid.
What you need before the first script
Selenium setup differs from most commercial tools because three separate pieces have to fit together:
| Piece | What it is |
|---|---|
Language binding |
The library you add to your project ( |
Driver |
A program per browser that implements the WebDriver protocol for it: |
Browser |
The real Chrome, Edge, Firefox or Safari that is driven. It must be compatible with the driver version, because browsers update themselves and a driver only supports a narrow range of browser versions. |
Before Selenium 4.6 you downloaded the driver by hand and put it on the PATH. When Chrome silently updated, the script broke with
session not created: This version of ChromeDriver only supports Chrome version 113. Since Selenium 4.6 every binding ships
Selenium Manager, and since 4.11 it can also fetch the browser, so for the common case the only thing you install is the binding.
Selenium Manager is explained in detail below. Low-code recording is a separate product, covered in
Selenium IDE & Legacy, and running browsers on other machines is covered in
Selenium Grid.
Source: Selenium — Getting started
Installing a Selenium library
The current release of every binding is 4.51.0. The downloads page lists them, plus the
Selenium Server (Grid) jar selenium-server-4.51.0.jar, a selenium-java-4.51.0.zip and a selenium-dotnet-4.51.0.zip for projects
that do not use a package manager, the Internet Explorer driver server, and nightly builds (the Java
4.52.0-SNAPSHOT artifacts come from the Central Portal snapshot repository). Prefer the package manager of your language; the zip
files exist for locked-down environments.
Requirements by language
| Language | Package | Minimum runtime |
|---|---|---|
Java |
|
Java 11. The project compiles with |
Python |
|
Python 3.10 or later, as declared in the package metadata. The examples ran on Python 3.13. |
C# |
|
Targets |
Ruby |
|
Ruby 3.3 or later (MRI), as declared in the gem metadata. The examples ran on Ruby 3.3.13. |
JavaScript |
|
Node.js 22 or later ( |
Kotlin |
The Java binding |
As Java. There is no separate Kotlin package. |
Always read the minimum version from the registry page of the version you install (the official page says the same), because it changes between releases.
Java: Maven and Gradle
Java libraries are installed with a build tool. A Maven pom.xml for the examples on this page, which also adds JUnit 5 and the
surefire plugin that runs it:
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>demo</groupId>
<artifactId>bookshelf-selenium</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>4.51.0</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.14.4</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.4</version>
</plugin>
</plugins>
</build>
</project>
The equivalent Gradle build declares the same artifacts as testImplementation. This build file was not run, because Gradle is not
installed on the host used for this page; it follows the official example.
plugins {
id 'java'
}
repositories {
mavenCentral()
}
dependencies {
testImplementation 'org.seleniumhq.selenium:selenium-java:4.51.0'
testImplementation platform('org.junit:junit-bom:5.14.4')
testImplementation 'org.junit.jupiter:junit-jupiter'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
test {
useJUnitPlatform()
}
selenium-java is an umbrella artifact. The dependency tree printed by mvn dependency:tree for the pom above contains
selenium-api, the Chrome, Edge, Firefox, Internet Explorer and Safari driver modules, selenium-remote-driver,
selenium-support (the WebDriverWait, Select and ExpectedConditions classes), selenium-manager (the Java wrapper that bundles the
Selenium Manager binaries) and the CDP modules for Chrome DevTools versions 153, 154 and 155. The 4.51.0 change log says Guava was
removed from the client bindings, but selenium-remote-driver still brings Guava 33.7.2 along in this tree, so do not rely on it being absent.
Python: pip
python3 -m venv .venv
source .venv/bin/activate
pip install selenium==4.51.0 pytest
To record the dependency, add selenium==4.51.0 to requirements.txt (pinning an exact version keeps CI reproducible). You can also
download the wheel from PyPI (selenium-4.51.0-py3-none-any.whl) and install the file with pip. The package depends on
urllib3, certifi, trio, trio-websocket, typing_extensions and websocket-client; the last three are used by WebDriver BiDi.
C#: NuGet and the .NET CLI
dotnet add package Selenium.WebDriver --version 4.51.0
In a project file the dependency is a PackageReference; the Package Manager Console form is Install-Package Selenium.WebDriver.
<ItemGroup>
<PackageReference Include="Selenium.WebDriver" Version="4.51.0" />
</ItemGroup>
WebDriverWait and the other support classes are part of Selenium.WebDriver 4.51.0; the projects on this page needed no second
Selenium package. The official page adds advice for Visual Studio Code: install the .NET SDK and the C# and NuGet extensions, and if
the nuget.config file is empty, restore the nuget.org package source in it, otherwise the build cannot restore the package.
Ruby: gem and Bundler
gem install selenium-webdriver -v 4.51.0
In a project, add it to the Gemfile and run bundle install. The selenium-devtools gem is only needed for features that use the
Chrome DevTools Protocol.
source 'https://rubygems.org'
gem 'rspec', '~> 3.13'
gem 'selenium-webdriver', '= 4.51.0'
JavaScript: npm
npm install selenium-webdriver@4.51.0
npm install --save-dev mocha@12.0.3
The result in package.json:
{
"name": "bookshelf-selenium-js",
"version": "1.0.0",
"private": true,
"scripts": { "test": "mocha test/*.spec.js --timeout 60000" },
"dependencies": { "selenium-webdriver": "4.51.0" },
"devDependencies": { "mocha": "12.0.3" }
}
A browser, and nothing else
With the binding installed, you need an ordinary browser on the machine (or none at all, because Selenium Manager can download Chrome for Testing, Firefox and Edge). On Linux servers and containers, Chrome and Firefox also need their shared libraries; see Known limitations. Safari needs one manual step covered in Browsers that need more than a binding.
Your first script: the eight basic components
Everything Selenium does is either a command to the browser or a request for information. Almost every script is a combination of eight building blocks:
| # | Component | What it does |
|---|---|---|
1 |
Start the session |
Creates a driver object. This launches the driver process and the browser, and it is where Selenium Manager runs. The Options object chooses the browser, the headless mode and other settings. |
2 |
Take action on the browser |
Navigates to a page. The call returns once the page has loaded (see the page load strategy on the options page). |
3 |
Request browser information |
Title, URL, window handles, window size and position, cookies, alerts and so on. |
4 |
Establish a waiting strategy |
Make sure the element exists and is interactable before you use it. This is the hardest part of Selenium to get right. |
5 |
Find an element |
Locate it with a |
6 |
Take action on an element |
Click, type, clear, submit. There are only a handful of element actions. |
7 |
Request element information |
Text, attributes, state (displayed, enabled, selected), size and CSS values. |
8 |
End the session |
Quits the driver process and, by default, closes the browser. No more commands can be sent to that driver object. |
The same components in each binding (the official examples show them one by one):
| # | Java | Python | C# | Ruby | JavaScript |
|---|---|---|---|---|---|
1 |
|
|
|
|
|
2 |
|
|
|
|
|
3 |
|
|
|
|
|
4 |
|
|
|
|
|
5 |
|
|
|
|
|
6 |
|
|
|
|
|
7 |
|
|
|
|
|
8 |
|
|
|
|
|
Java
The script below tests the Bookshelf catalogue: it opens the catalogue, searches for "quiet" and reads the result count. The numbers in
the comments are the eight components. The locators use the data-testid attributes that
Bookshelf defines, and the browser is started headless so the example
also runs on a server with no display.
package demo;
import java.time.Duration;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
public class FirstScript {
public static void main(String[] args) {
String baseUrl = System.getenv().getOrDefault("BASE_URL", "http://localhost:3000");
// 1. Start the session (Selenium Manager finds or downloads chromedriver)
ChromeOptions options = new ChromeOptions();
options.addArguments("--headless=new");
WebDriver driver = new ChromeDriver(options);
try {
// 2. Take action on the browser
driver.get(baseUrl + "/books");
// 3. Request browser information
String title = driver.getTitle();
System.out.println("title = " + title);
// 4. Establish a waiting strategy: an explicit wait of at most five seconds
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(5));
// 5. Find elements
WebElement searchBox = driver.findElement(By.cssSelector("[data-testid=search-input]"));
WebElement submitButton = driver.findElement(By.cssSelector("[data-testid=search-submit]"));
// 6. Take action on the elements
searchBox.sendKeys("quiet");
submitButton.click();
// 7. Request element information
By resultCount = By.cssSelector("[data-testid=result-count]");
wait.until(ExpectedConditions.textToBePresentInElementLocated(resultCount, "1 book found"));
System.out.println("result count = " + driver.findElement(resultCount).getText());
} finally {
// 8. End the session
driver.quit();
}
}
}
The output, with Bookshelf listening on port 3911 (the class reads the address from the BASE_URL environment variable):
title = Catalogue - Bookshelf
result count = 1 book found
The try and finally pair is not decoration. If an exception skips driver.quit(), the driver process and the browser keep running;
on a CI machine such orphans pile up and eventually exhaust memory. A test runner’s teardown hook does the same job (see
Test runners).
Python
The Python script is the same eight components. find_element takes the strategy and the value as two arguments, and the explicit
wait comes from selenium.webdriver.support.
import os
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
base_url = os.environ.get("BASE_URL", "http://localhost:3000")
# 1. Start the session (Selenium Manager finds or downloads chromedriver)
options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
driver = webdriver.Chrome(options=options)
try:
# 2. Take action on the browser
driver.get(f"{base_url}/books")
# 3. Request browser information
title = driver.title
print("title =", title)
# 4. Establish a waiting strategy: an explicit wait of at most five seconds
wait = WebDriverWait(driver, 5)
# 5. Find elements
search_box = driver.find_element(By.CSS_SELECTOR, "[data-testid=search-input]")
submit_button = driver.find_element(By.CSS_SELECTOR, "[data-testid=search-submit]")
# 6. Take action on the elements
search_box.send_keys("quiet")
submit_button.click()
# 7. Request element information
result_count = (By.CSS_SELECTOR, "[data-testid=result-count]")
wait.until(EC.text_to_be_present_in_element(result_count, "1 book found"))
print("result count =", driver.find_element(*result_count).text)
finally:
# 8. End the session
driver.quit()
BASE_URL=http://localhost:3911 python first_script.py
title = Catalogue - Bookshelf
result count = 1 book found
Ruby
The Ruby script is run with ruby spec/first_script.rb. It ran in the Linux container described in the introduction, where Selenium
Manager found the chromedriver that the Debian chromium-driver package installs on the PATH and used it.
require 'selenium-webdriver'
base_url = ENV.fetch('BASE_URL', 'http://localhost:3000')
# 1. Start the session (Selenium Manager finds or downloads chromedriver)
options = Selenium::WebDriver::Chrome::Options.new
options.add_argument('--headless=new')
driver = Selenium::WebDriver.for :chrome, options: options
begin
# 2. Take action on the browser
driver.get("#{base_url}/books")
# 3. Request browser information
puts "title = #{driver.title}"
# 4. Establish a waiting strategy: an explicit wait of at most five seconds
wait = Selenium::WebDriver::Wait.new(
timeout: 5,
ignore: [Selenium::WebDriver::Error::NoSuchElementError,
Selenium::WebDriver::Error::StaleElementReferenceError]
)
# 5. Find elements
search_box = driver.find_element(css: '[data-testid=search-input]')
submit_button = driver.find_element(css: '[data-testid=search-submit]')
# 6. Take action on the elements
search_box.send_keys('quiet')
submit_button.click
# 7. Request element information
wait.until { driver.find_element(css: '[data-testid=result-count]').text == '1 book found' }
puts "result count = #{driver.find_element(css: '[data-testid=result-count]').text}"
ensure
# 8. End the session
driver.quit
end
The ignore: list replaces Ruby’s default, which is only NoSuchElementError, so it names both errors that can occur while the old
page is being replaced by the new one. The next section explains why that matters.
Why the example waits for the text
The official first script sets an implicit wait of 500 ms in component 4 and reads the element right after the click. That works against
the static demo page the official documentation uses, but not against a page that is replaced after a click, as Bookshelf’s search form
does (it is a plain GET form). click() returns as soon as the click has been dispatched, and the old page is still in the browser for
a moment. Looking for result-count then finds the element of the old page. Running the official shape of the script against Bookshelf
(implicit wait of 0.5 s, no explicit wait) gave, in four consecutive Python runs:
result count = 24 books found
result count = 24 books found
selenium.common.exceptions.WebDriverException: Message: unknown error: unhandled inspector error: {"code":-32000,"message":"Node with given id does not belong to the document"}
result count = 24 books found
Source: Selenium — Waits
and the Java version printed 1 book found in two of four runs and the stale 24 books found in the other two. The JavaScript
version of the same pattern failed with StaleElementReferenceError, and the Ruby spec with the default wait failed intermittently for
the same reason. The fix is always the same: after an action that changes the page, wait for the state you expect (here, the text
1 book found), not for the element to exist. The scripts above do that with an explicit wait, and the Ruby and JavaScript versions
also treat a stale element as "not yet". Implicit and explicit waits are covered, with the rule against mixing them, in
Waits & Expected Conditions.
Headless and headed
Selenium starts a visible browser window by default. That is what you want while writing and debugging a script, and it needs a desktop session. In CI, on servers and in containers there is no display, so you ask the browser to run without a window:
| Browser | Option | Notes |
|---|---|---|
Chrome |
|
|
Edge |
|
Same as Chrome; Edge is Chromium-based. |
Firefox |
|
A single dash. Firefox has no |
Safari |
Not available |
|
Every example on this page that creates a browser passes --headless=new through the Options class, and nothing changes in the rest of
the script. A headed run is the same code without that line. The headless runs on this page are real; a headed run opens a window on
the desktop, which was not exercised for this page. In headless mode the window size is not the one of your screen, so set it
explicitly (for example --window-size=1280,800) when a layout depends on it. The per-browser options, including Service, Options
and remote sessions, are the subject of
Drivers, Browsers & Options.
Source: Selenium — Chrome
Browsers that need more than a binding
Chrome, Edge and Firefox are started the same way and Selenium Manager supplies the drivers. Two exceptions need action from you:
-
Safari.
safaridriverships with macOS, but you must runsafaridriver --enableonce (it asks for an administrator password), or switch on "Allow remote automation" in the Developer section of the Safari settings. Without it,webdriver.Safari()fails with a message that tells you to enable that setting. This is a system setting, so it was not changed on the host used for this page and no Safari example was run. -
Firefox on this host. Local Firefox did not start on the macOS 27 host used for this page (the geckodriver session timed out), so Firefox runs in the Selenium Docker images. The Docker images are covered in Selenium Grid.
Source: Selenium — Safari
Test runners
A script is a program. To run tens or hundreds of checks, you put the same calls into tests managed by a test runner, which gives you set-up and tear-down hooks, assertions, grouping, reports and parallel execution. Selenium itself ships no test framework; it works with the standard runner of each language. The official documentation recommends these (the first in each list is the one its own examples use):
| Language | Runners |
|---|---|
Java |
JUnit 5 (this page), TestNG (adds parallel execution and parameterised tests of its own) |
Python |
pytest (this page), |
C# |
NUnit, MSTest (both below) |
Ruby |
RSpec (this page), Minitest |
JavaScript |
Mocha (this page), Jest |
Kotlin |
Kotest, JUnit 5 |
Whatever the runner, the same four rules apply, and the examples below follow them:
-
Start the driver in the set-up hook and quit it in the tear-down hook, so a failing assertion cannot leak a browser. A fresh browser per test is the safest default; share a browser only when start-up time is a measured problem, and then reset cookies and storage between tests.
-
Assert with the runner’s assertion library. Selenium has no assertions of its own.
-
Take the base URL from configuration, here the
BASE_URLenvironment variable, so the same tests run against a local server, a container or a staging host. -
Never share a
WebDriverbetween threads. A driver instance is not thread safe; give each parallel test its own (the JavaThreadGuardclass exists to catch mistakes).
Selenium also fits non-test jobs: repetitive browser tasks that run on a schedule, and scraping of sites that do not offer an API (check the site’s terms of service first, because some forbid it and some block Selenium).
JUnit 5 (Java)
package demo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.time.Duration;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
class BookshelfSearchTest {
static final String BASE_URL = System.getenv().getOrDefault("BASE_URL", "http://localhost:3000");
WebDriver driver;
@BeforeEach
void setUp() {
ChromeOptions options = new ChromeOptions();
options.addArguments("--headless=new");
driver = new ChromeDriver(options);
}
@AfterEach
void tearDown() {
if (driver != null) {
driver.quit();
}
}
@Test
void searchFindsTheQuietCompiler() {
driver.get(BASE_URL + "/books");
assertEquals("Catalogue - Bookshelf", driver.getTitle());
driver.findElement(By.cssSelector("[data-testid=search-input]")).sendKeys("quiet");
driver.findElement(By.cssSelector("[data-testid=search-submit]")).click();
By resultCount = By.cssSelector("[data-testid=result-count]");
new WebDriverWait(driver, Duration.ofSeconds(5))
.until(ExpectedConditions.textToBePresentInElementLocated(resultCount, "1 book found"));
assertEquals("1 book found", driver.findElement(resultCount).getText());
}
}
BASE_URL=http://localhost:3911 mvn -B test
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.919 s -- in demo.BookshelfSearchTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
With Gradle the command is gradle clean test. It was not run (no Gradle on the host). @BeforeEach and @AfterEach create and quit a
browser around every test; JUnit 5 also offers @BeforeAll and @AfterAll for a browser shared by a class, and extensions for
injecting the driver. The 4.51 change that stops ExpectedCondition from extending Guava’s Function does not affect this code,
because until still accepts a lambda.
pytest (Python)
pytest uses fixtures instead of set-up and tear-down methods. A fixture that yields the driver runs its teardown (the code after
yield) whether the test passes or fails. Put it in conftest.py so every test file can request it by name.
# tests/conftest.py
import os
import pytest
from selenium import webdriver
BASE_URL = os.environ.get("BASE_URL", "http://localhost:3000")
@pytest.fixture
def base_url():
return BASE_URL
@pytest.fixture
def driver():
options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
driver = webdriver.Chrome(options=options)
yield driver
driver.quit()
# tests/test_search.py
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
def test_search_finds_the_quiet_compiler(driver, base_url):
driver.get(f"{base_url}/books")
assert driver.title == "Catalogue - Bookshelf"
driver.find_element(By.CSS_SELECTOR, "[data-testid=search-input]").send_keys("quiet")
driver.find_element(By.CSS_SELECTOR, "[data-testid=search-submit]").click()
result_count = (By.CSS_SELECTOR, "[data-testid=result-count]")
WebDriverWait(driver, 5).until(EC.text_to_be_present_in_element(result_count, "1 book found"))
assert driver.find_element(*result_count).text == "1 book found"
BASE_URL=http://localhost:3911 pytest -q
1 passed in 0.69s
The fixture has function scope, so each test gets a new browser. Use scope="session" for a shared browser, and the
pytest-xdist plugin (pytest -n 4) to run tests in several worker processes, each with its own fixtures.
MSTest and NUnit (C#)
The same test in MSTest 4.0.2 and in NUnit 4.3.2 differs only in attributes and assertions. Both projects reference
Selenium.WebDriver 4.51.0 and were created with dotnet new mstest and dotnet new nunit.
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Support.UI;
namespace Bk;
[TestClass]
public class BookshelfSearchTest
{
private static readonly string BaseUrl =
Environment.GetEnvironmentVariable("BASE_URL") ?? "http://localhost:3000";
private IWebDriver driver = null!;
[TestInitialize]
public void SetUp()
{
var options = new ChromeOptions();
options.AddArgument("--headless=new");
driver = new ChromeDriver(options);
}
[TestCleanup]
public void TearDown() => driver.Quit();
[TestMethod]
public void SearchFindsTheQuietCompiler()
{
driver.Navigate().GoToUrl($"{BaseUrl}/books");
Assert.AreEqual("Catalogue - Bookshelf", driver.Title);
driver.FindElement(By.CssSelector("[data-testid=search-input]")).SendKeys("quiet");
driver.FindElement(By.CssSelector("[data-testid=search-submit]")).Click();
var wait = new WebDriverWait(driver, TimeSpan.FromSeconds(5));
wait.IgnoreExceptionTypes(typeof(StaleElementReferenceException));
var text = wait.Until(d =>
{
var count = d.FindElement(By.CssSelector("[data-testid=result-count]")).Text;
return count == "1 book found" ? count : null;
});
Assert.AreEqual("1 book found", text);
}
}
For NUnit, replace [TestClass] with nothing (NUnit finds tests by [Test]), [TestInitialize] with [SetUp], [TestCleanup] with
[TearDown], [TestMethod] with [Test], and Assert.AreEqual(expected, actual) with Assert.That(actual, Is.EqualTo(expected)).
NUnit’s analyzer also requires the disposable driver to be disposed in the teardown:
[TearDown]
public void TearDown()
{
driver.Quit();
driver.Dispose();
}
BASE_URL=http://localhost:3911 dotnet test
Both projects passed (1 test each, about 0.8 s). The wait lambda returns null until the text matches, which WebDriverWait.Until treats
as "not yet", and IgnoreExceptionTypes makes a stale element count as "not yet" too.
RSpec (Ruby)
require 'selenium-webdriver'
RSpec.describe 'Bookshelf search' do
let(:base_url) { ENV.fetch('BASE_URL', 'http://localhost:3000') }
before do
options = Selenium::WebDriver::Chrome::Options.new
options.add_argument('--headless=new')
@driver = Selenium::WebDriver.for :chrome, options: options
end
after { @driver.quit }
it 'finds The Quiet Compiler' do
@driver.get("#{base_url}/books")
expect(@driver.title).to eq('Catalogue - Bookshelf')
@driver.find_element(css: '[data-testid=search-input]').send_keys('quiet')
@driver.find_element(css: '[data-testid=search-submit]').click
wait = Selenium::WebDriver::Wait.new(
timeout: 5,
ignore: [Selenium::WebDriver::Error::NoSuchElementError,
Selenium::WebDriver::Error::StaleElementReferenceError]
)
wait.until { @driver.find_element(css: '[data-testid=result-count]').text == '1 book found' }
expect(@driver.find_element(css: '[data-testid=result-count]').text).to eq('1 book found')
end
end
BASE_URL=http://host.docker.internal:3911 bundle exec rspec
.
Finished in 0.85 seconds (files took 0.07 seconds to load)
1 example, 0 failures
This ran in the Linux container (RSpec 3.13.2, Ruby 3.3.13), with host.docker.internal pointing at Bookshelf on the host. Across
repeated runs it passed every time after the ignore: list was added; before that, one run in about eight failed with a stale element.
Early on, one container run also failed with a WebDriverError raised by Chromium in the container; later runs did not repeat it
and it was not investigated further.
Mocha (JavaScript)
Mocha provides describe, it, before and after. Because Selenium’s JavaScript API returns promises, the hooks and tests are
async functions.
const { Builder, By, Browser, error } = require('selenium-webdriver');
const chrome = require('selenium-webdriver/chrome');
const assert = require('assert');
const baseUrl = process.env.BASE_URL || 'http://localhost:3000';
describe('Bookshelf search', function () {
let driver;
before(async function () {
const options = new chrome.Options().addArguments('--headless=new');
driver = await new Builder().forBrowser(Browser.CHROME).setChromeOptions(options).build();
});
it('finds The Quiet Compiler', async function () {
await driver.get(`${baseUrl}/books`);
assert.equal(await driver.getTitle(), 'Catalogue - Bookshelf');
await driver.findElement(By.css('[data-testid=search-input]')).sendKeys('quiet');
await driver.findElement(By.css('[data-testid=search-submit]')).click();
// Poll until the results page shows the new count. The old page can still
// answer for a moment, so a stale element simply means "try again".
await driver.wait(async () => {
try {
const count = await driver.findElement(By.css('[data-testid=result-count]'));
return (await count.getText()) === '1 book found';
} catch (e) {
if (e instanceof error.StaleElementReferenceError) return false;
throw e;
}
}, 5000);
});
after(async () => await driver.quit());
});
BASE_URL=http://localhost:3911 npx mocha test/first-script.spec.js --timeout 60000
1 passing (672ms)
Raise Mocha’s --timeout above the default 2000 ms, because starting a browser takes longer than that. The command is equally valid as
mocha test.spec.js when Mocha is installed globally. Jest also works, but Mocha is the one the Selenium documentation uses. Kotlin
projects use the Java snippets with JUnit 5 or Kotest; the Selenium documentation has no separate Kotlin examples for this page.
Selenium Manager
Selenium Manager is the official driver manager of the Selenium project. It is a command line tool written in Rust, compiled for
Windows, Linux and macOS, and shipped inside every binding. It replaced the practice of downloading chromedriver yourself, and the
third-party managers (WebDriverManager for Java, webdriver-manager for Python and JavaScript, WebDriverManager.Net, webdrivers for Ruby),
which inspired it. Its version follows Selenium’s with a leading 0, so 4.51.0 ships Selenium Manager 0.4.51.
When it runs
The bindings call Selenium Manager only as a fallback, at the moment your script creates a driver object (new ChromeDriver(),
webdriver.Chrome(), and so on). If you provide a driver yourself, Selenium Manager is not used for it. The precedence is:
Source: Selenium — Selenium Manager
Using it needs no code at all. In older Selenium the same session needed a system property pointing at a driver you had downloaded:
// Before Selenium Manager: a driver downloaded by hand
System.setProperty("webdriver.chrome.driver", "/path/to/chromedriver");
WebDriver driver = new ChromeDriver();
// With Selenium Manager: nothing to set
WebDriver driver = new ChromeDriver();
This snippet only shows the difference and was not run on its own, but the Selenium Manager form is what every example on this page uses.
Automated driver management
The process has four steps, which the manager calls management rather than download because there is more to it:
-
Browser discovery. It finds the installed browser and runs
--versionon it. -
Driver version discovery. It maps the browser version to a driver version using the metadata the browser vendors publish: Chrome for Testing (CfT) JSON endpoints for Chrome, the geckodriver releases for Firefox, Microsoft’s endpoints for Edge.
-
Driver download. It downloads and unpacks the driver from the resolved URL.
-
Driver cache. It stores the unpacked driver under
~/.cache/seleniumand reuses it next time.
You can watch these steps by running the Selenium Manager binary directly. A copy is extracted to the cache by the Java binding (here
~/.cache/selenium/manager/4.51.0/selenium-manager) and the Python wheel contains one in
selenium/webdriver/common/macos/. With a new empty cache and --debug:
selenium-manager --browser chrome --cache-path ./cache --debug
[2026-10-25T20:43:24.820Z DEBUG] chromedriver not found in PATH
[2026-10-25T20:43:24.820Z DEBUG] chrome detected at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
[2026-10-25T20:43:24.820Z DEBUG] Running command: /Applications/Google Chrome.app/Contents/MacOS/Google Chrome --version
[2026-10-25T20:43:24.848Z DEBUG] Output: "Google Chrome 155.0.8059.85 "
[2026-10-25T20:43:24.849Z DEBUG] Detected browser: chrome 155.0.8059.85
[2026-10-25T20:43:24.849Z DEBUG] Discovering versions from https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json
[2026-10-25T20:43:25.111Z DEBUG] Required driver: chromedriver 155.0.8059.84
[2026-10-25T20:43:25.111Z DEBUG] Acquiring lock: .../cache/chromedriver/mac-arm64/155.0.8059.84/sm.lock
[2026-10-25T20:43:25.111Z DEBUG] Downloading chromedriver 155.0.8059.84 from https://storage.googleapis.com/chrome-for-testing-public/155.0.8059.84/mac-arm64/chromedriver-mac-arm64.zip
[2026-10-25T20:43:25.852Z INFO ] Driver path: .../cache/chromedriver/mac-arm64/155.0.8059.84/chromedriver
[2026-10-25T20:43:25.852Z INFO ] Browser path: /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
The start of the cache path is shortened to ... in this output; nothing else is edited. Note that the driver version (155.0.8059.84) is
not the browser version (155.0.8059.85): only the major version has to match. Running it a second time prints chromedriver
155.0.8059.84 already in the cache instead of the download lines. The result of a run is two paths, the driver and the browser, and with
--output json they come back as a document that a script can parse (the result object has code, message, driver_path and
browser_path):
selenium-manager --browser chrome --cache-path ./cache --output json
The --output values are LOGGER (the default), JSON, SHELL and MIXED (log lines on stderr and a minimal JSON object on stdout).
Automated browser management
Since Selenium 4.11.0 Selenium Manager can also find, download and cache browsers. It supports:
| Browser | Source | Since |
|---|---|---|
Chrome |
Chrome for Testing |
4.11.0 |
Firefox |
Public Firefox releases ( |
4.12.0 |
Edge |
Edge downloads |
4.14.0 |
If the browser is not installed, the current stable build is downloaded into the cache (~/.cache/selenium/chrome for Chrome). You can also ask
for a specific browser with the browserVersion option of the session: a major version number (114; for Chrome the first available CfT
release is 113), or a label:
| Label | Meaning |
|---|---|
|
The current Chrome for Testing release. |
|
The release that will become stable next. |
|
The version in development. |
|
The nightly build, meant for developers. |
|
Extended Support Release (Firefox only). |
For a number or a label, Selenium Manager first looks for an installed browser of that version or channel and downloads one only when it
finds none. Forcing a download with --force-browser-download (SE_FORCE_BROWSER_DOWNLOAD) downloaded Chrome for Testing even though
Chrome was installed. On this host that fetched 156.0.8078.47 in about 8.5 seconds (the browser alone then occupied 367 MB in the
cache, and the driver 34 MB):
selenium-manager --browser chrome --browser-version stable --force-browser-download --cache-path ./cache --debug
[...DEBUG] Discovering versions from https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions-with-downloads.json
[...DEBUG] Required browser: chrome 156.0.8078.47
[...DEBUG] Downloading chrome 156.0.8078.47 from https://storage.googleapis.com/chrome-for-testing-public/156.0.8078.47/mac-arm64/chrome-mac-arm64.zip
[...DEBUG] chrome 156.0.8078.47 is available at .../cache/chrome/mac-arm64/156.0.8078.47/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing
[...DEBUG] Required driver: chromedriver 156.0.8078.47
[...DEBUG] Downloading chromedriver 156.0.8078.47 from https://storage.googleapis.com/chrome-for-testing-public/156.0.8078.47/mac-arm64/chromedriver-mac-arm64.zip
[...INFO ] Driver path: .../cache/chromedriver/mac-arm64/156.0.8078.47/chromedriver
[...INFO ] Browser path: .../cache/chrome/mac-arm64/156.0.8078.47/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing
(Timestamps are cut to ... and a few lines of the same shape are omitted.) The opposite switch is --avoid-browser-download
(SE_AVOID_BROWSER_DOWNLOAD), which stops Selenium Manager from downloading a browser even when you asked for a version.
Edge on Windows is the exception to all this. Its installer is an MSI that needs administrator rights, so on a non-administrator
session Selenium Manager prints edge can only be installed in Windows with administrator permissions and does not install it. On
macOS and Linux Edge goes to the cache like the others.
The cache
Everything Selenium Manager downloads lives in one folder, ~/.cache/selenium by default (on Windows, the same path under the user
profile). After the runs above the folder looked like this:
cache/
chromedriver/mac-arm64/155.0.8059.84/chromedriver
chromedriver/mac-arm64/156.0.8078.47/chromedriver
chrome/mac-arm64/156.0.8078.47/Google Chrome for Testing.app/...
se-metadata.json
Two files live at the root of the cache next to the downloaded assets:
-
se-config.toml: optional, written by you, read by Selenium Manager (see the next section). -
se-metadata.json: written and maintained by Selenium Manager. It remembers the versions it discovered over the network, so a test suite that creates hundreds of drivers does not query the CfT endpoints hundreds of times. In the observed file the entries arebrowsers,drivers,statsandcached_assets, each driver entry with the browser major version, the driver version and an expiry timestamp (driver_ttl).
The TTL (time to live) is how long a discovered version stays valid. It defaults to 3600 seconds (one hour), is set with --ttl or
SE_TTL, and 0 disables the metadata cache. Within the TTL the version is read from the file; afterwards it is stale and Selenium
Manager asks the network again. A driver already unpacked in the cache is reused even then; the TTL only governs the lookup.
To keep the cache from growing for ever, Selenium Manager records a last_used timestamp each time it resolves an asset from the cache
and removes driver or browser version folders that have not been used for 30 days (pruning was added in Selenium Manager 0.4.45). --clear-cache (SE_CLEAR_CACHE=true) deletes the whole folder and --clear-metadata
(SE_CLEAR_METADATA=true) deletes only the metadata file.
In CI, restore the cache folder between runs, so jobs do not download the driver every time:
- uses: actions/cache@v6
with:
path: ~/.cache/selenium
key: selenium-${{ runner.os }}-${{ hashFiles('**/pom.xml') }}
This workflow snippet was not run. Its key is only a suggestion: any stable key works, and a cache hit does not stop Selenium Manager from refreshing a stale version. The CI pages of this section continue from here (see Running in CI/CD).
Configuration: flags, se-config.toml and environment variables
Selenium Manager takes its settings from three places. The documented order, which was confirmed by running it, is:
-
Command line arguments. The bindings pass the options of your session this way.
-
The configuration file
se-config.tomlin the cache folder. -
Environment variables. Each key becomes an environment variable by upper-casing it, replacing
-with_and adding the prefixSE_(cache-pathbecomesSE_CACHE_PATH).
A file entry wins over an environment variable, and a flag wins over both. With browser = "chrome" in se-config.toml:
SE_CACHE_PATH=./cache selenium-manager # the file chooses chrome
SE_CACHE_PATH=./cache SE_BROWSER=edge selenium-manager # still chrome: the file beats the variable
SE_CACHE_PATH=./cache selenium-manager --browser edge # edge: the flag beats the file
The first two commands printed the cached chromedriver and the installed Chrome. The third downloaded Edge for Testing 156.0.4314.42 and
its msedgedriver. Without SE_CACHE_PATH, Selenium Manager looked for se-config.toml in ~/.cache/selenium and printed
You need to specify a browser or driver, so the file belongs in whichever cache folder is in effect. All keys:
| CLI argument | se-config.toml key |
Environment variable | Meaning |
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
A major version, or |
|
|
|
An exact driver version, such as |
|
|
|
Absolute path of the browser to detect the version of. |
|
|
|
Mirror of the driver repositories. |
|
|
|
Mirror of the browser repositories. |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
HTTP proxy for Selenium Manager’s own downloads, for example |
|
|
|
Timeout of network requests, default 300. |
|
|
|
Lifetime of discovered versions, default 3600. |
|
|
|
No network requests and no downloads. |
|
|
|
Download the browser even if one is installed. |
|
|
|
Never download the browser. |
|
|
|
Ignore a driver found on the |
|
|
|
Ignore a browser found on the |
|
|
|
Show DEBUG or TRACE messages. The binary also has |
|
|
|
Folder for downloads, metadata and |
|
(none) |
|
Delete the cache folder or the metadata file. |
|
|
|
Which binding is calling. Set by the bindings. |
|
|
|
Do not send usage statistics. |
The table lists every key the official page documents. The 0.4.51 binary has several more flags: --grid [version],
--log-level, --license, --init-skills [file] and --init-rules [file], and electron is accepted as a browser name. --init-skills and --init-rules write a guidance file with Selenium best practices for AI coding assistants into
the current folder (skills/skills.md of 122 lines and rules/selenium.md of 475 lines when run on this host); they have no environment
variable listed in the documentation. They are covered with the other AI helpers in
AI Features in Cypress & Selenium.
Per-browser variants of the keys exist as well: chrome-version, firefox-version, edge-version (and SE_CHROME_VERSION and so on),
chromedriver-version, geckodriver-version, msedgedriver-version, chrome-path, firefox-path, edge-path,
chromedriver-mirror-url, chrome-mirror-url and the matching names for Firefox and Edge. They let one file or one environment
configure several browsers at once.
A complete se-config.toml, as in the official page, shows the value of every key (do not use all of them together; for instance
offline = true with cache-path pointing at an empty folder leaves nothing to download):
browser = "chrome" # --browser BROWSER
driver = "chromedriver" # --driver DRIVER
browser-version = "106" # --browser-version BROWSER_VERSION
driver-version = "106.0.5249.61" # --driver-version DRIVER_VERSION
browser-path = "/usr/bin/google-chrome" # --browser-path BROWSER_PATH
driver-mirror-url = "https://custom-driver-mirror.com" # --driver-mirror-url
browser-mirror-url = "https://custom-browser-mirror.com" # --browser-mirror-url
output = "LOGGER" # --output OUTPUT
os = "linux" # --os OS
arch = "x64" # --arch ARCH
proxy = "myproxy:8080" # --proxy PROXY
timeout = 300 # --timeout TIMEOUT
offline = true # --offline
force-browser-download = true # --force-browser-download
avoid-browser-download = false # --avoid-browser-download
debug = true # --debug
trace = true # --trace
cache-path = "/custom/cache/path" # --cache-path CACHE_PATH
ttl = 3600 # --ttl TTL
language-binding = "Python" # --language-binding LANGUAGE
avoid-stats = true # --avoid-stats
Two differences from the documentation showed up in the 0.4.51 binary. First, with debug = true in se-config.toml no DEBUG lines were
printed, while --debug and SE_DEBUG=true did print them (a small file with browser, avoid-stats and offline keys behaved as
documented). Second, a boolean written as a string (offline = "true" or debug = "true") made the binary stop with a Rust panic in
src/config.rs instead of an error message. Write booleans without quotes, and prefer a flag or an environment variable for debug.
What the bindings pass
The bindings do not read se-config.toml themselves; they run the Selenium Manager binary as a child process and give it arguments taken
from the session’s options:
| Session option | Argument |
|---|---|
|
|
|
|
Browser binary location (for example Chrome’s |
|
|
|
Anything else, such as cache-path, ttl, offline or the mirrors, cannot be set from the browser options; set it in the process
environment (the child process inherits it) or in se-config.toml. To see exactly what a binding sends, point SE_MANAGER_PATH at a
wrapper script that records its arguments and then runs the real binary:
#!/bin/sh
echo "$@" >> sm-args.log
exec "$HOME/.cache/selenium/manager/4.51.0/selenium-manager" "$@"
After SE_MANAGER_PATH=$PWD/sm-wrapper.sh SE_DEBUG=true python first_script.py and the Java first script, the log held:
--browser chrome --debug --language-binding python --output json
--browser chrome --language-binding java --output json
So SE_DEBUG is turned into --debug by the binding, and the language binding is always reported. SE_MANAGER_PATH (supported since
Selenium 4.13.0) is the supported way to run your own build of Selenium Manager; see Building a custom Selenium Manager. In Python
the logging module shows the same lines: with logging.basicConfig(level=logging.DEBUG) the logger
selenium.webdriver.common.selenium_manager prints each line of the debug output shown earlier.
Offline mode
--offline or SE_OFFLINE=true disables every network request and download. Selenium Manager then can only reuse what is in the
cache. With a populated cache it behaves normally. With an empty one it warns and returns no driver:
SE_CACHE_PATH=./empty SE_OFFLINE=true SE_AVOID_STATS=true selenium-manager --browser chrome --output json
{
"logs": [
{ "level": "WARN", "message": "Unable to discover proper chromedriver version in offline mode" },
{ "level": "INFO", "message": "Browser path: /Applications/Google Chrome.app/Contents/MacOS/Google Chrome" }
],
"result": { "code": 0, "message": "", "driver_path": "", "browser_path": "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" }
}
(The timestamp fields and the path layout of the real output are omitted from this JSON; the code is 0 even though no driver was
found.) The exit status is 0, so a script cannot rely on it; the binding is what fails, with NoSuchDriverException in Python:
selenium.common.exceptions.NoSuchDriverException: Message: Unable to obtain driver for chrome; For documentation on this error, please visit: https://www.selenium.dev/documentation/webdriver/troubleshooting/errors/driver_location
Offline CI agents either pre-populate the cache (build it with one online run and ship the folder) or skip Selenium Manager by naming the drivers, as the next section shows.
Skipping Selenium Manager: drivers you provide
Selenium Manager is a fallback, so any of these makes it unnecessary:
-
A
Servicewith an explicit path (ChromeDriverService.Builder().usingDriverExecutable(...)in Java,Service(executable_path=...)in Python). The Service classes are covered in Drivers, Browsers & Options. -
A driver on the
PATH. Selenium Manager finds it and uses it (chromedriver not found in PATHappears in the debug output above when there is none). Add--skip-driver-in-pathto ignore it. -
The
SE_CHROMEDRIVER,SE_EDGEDRIVER,SE_GECKODRIVERandSE_IEDRIVERenvironment variables, which hold the path of the driver executable. They are supported in the Java, Python (from 4.26.0) and Ruby (from 4.25.0) bindings, but not in the others. WithSE_CHROMEDRIVERset to the cached driver,SE_OFFLINE=trueand an emptySE_CACHE_PATH, the Python first script ran and created nothing in the cache folder.
Proxies, mirrors and connectivity
Selenium Manager downloads from public endpoints: googlechromelabs.github.io and storage.googleapis.com for Chrome and
chromedriver, the geckodriver releases on GitHub, ftp.mozilla.org, and the Microsoft Edge endpoints. Behind a corporate proxy or firewall
you may see errors like these:
error sending request for url (https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json)
error trying to connect: dns error: failed to lookup address information
error trying to connect: An existing connection was forcibly closed by the remote host. (os error 10054)
The first message is what the 0.4.51 binary printed when run with --proxy 127.0.0.1:9, a proxy that is not listening. Fixes, in order:
-
Set the proxy for the session with the Proxy option; the bindings pass it to Selenium Manager (
--proxy), so the same proxy is used by the browser and by the downloads. -
Use
SE_PROXYorproxy = "..."inse-config.tomlwhen Selenium Manager needs a proxy that the browser must not use. -
Allow the endpoints in your firewall, or mirror them with
driver-mirror-urlandbrowser-mirror-url(or the per-browser variants). A mirror must repeat the layout and response format of the original: the Chrome for Testing JSON files for Chrome (the download URLs in them can point at your storage), the geckodriver release layout plus ageckodriver-support.jsonfile for Firefox, andmsedgedriver.microsoft.comandedgeupdates.microsoft.com/api/products/for Edge. Driver and browser mirrors are separate because, for Firefox and Edge, they come from different repositories. A mirror cannot point at a source with a different structure, such as a Linux distribution’s package repository; install the browser and driver yourself in that case. The mirror setting was not exercised for this page because it needs a mirror server.
Usage statistics and SE_AVOID_STATS
Selenium Manager sends anonymised usage statistics to Plausible once a day (the same TTL mechanism). The data is the Selenium version,
the language binding, the operating system and architecture, the browser and its version, and a rough geolocation derived from the IP
address. It is on by default. Opt out with SE_AVOID_STATS=true, --avoid-stats or avoid-stats = true in se-config.toml. The
selenium-manager change log also lists support for the DO_NOT_TRACK environment variable since 0.4.48; this was not tested.
You can see the effect in se-metadata.json: the shared cache used by runs without an opt-out had stats entries (browser, OS,
architecture, language binding and Selenium version, each with a stats_ttl), and every cache created with --avoid-stats had an empty
"stats": [] array. In an organisation that blocks outbound traffic, set SE_AVOID_STATS=true anyway, so no request is attempted.
Known limitations
-
Architectures. Each Selenium release ships four Selenium Manager binaries: macOS (Intel and Apple silicon in one), Windows (x86 and x64, also used on Windows arm64), Linux x64, and, since Selenium 4.49.0, Linux arm64. On Linux arm64 it can manage Chrome and chromedriver from version 153 (the first Chrome for Testing build for that platform) and Firefox from version 136 plus geckodriver; Edge is not available there. Other platforms, such as 32-bit Linux on ARM, are not supported because the vendors publish nothing for them. Install the browser and driver with the system package manager and set the driver location in a
Serviceor in aSE_*DRIVERvariable. This is what happened in the Ruby container on this page: the container is Linux arm64 with Chromium 156 and the Debianchromium-driveron thePATH. -
Linux libraries. Browsers that Selenium Manager downloads are portable builds that still need system libraries. If Firefox fails with
libdbus-glib-1.so.2: cannot open shared object file, install it (sudo apt-get install libdbus-glib-1-2); if Chrome for Testing fails withlibatk-1.0.so.0: cannot open shared object file, installlibatk-bridge2.0-0. -
Custom package managers. Anaconda or snap installs of browsers need a specific driver; set the driver location, the browser location, or both, because Selenium Manager cannot discover those.
-
Grid.
--selenium-manager trueon a Grid node lets the node use Selenium Manager to find drivers (see Selenium Grid), andselenium-manager --grid [version]downloads the Selenium Server jar into the cache.
Building a custom Selenium Manager
The bindings use the Selenium Manager binary named in SE_MANAGER_PATH when it is set (since Selenium 4.13.0). The documented recipe for a
platform without a shipped binary is: install the Rust toolchain, clone SeleniumHQ/selenium, run cargo build --release in the rust
folder, set SE_MANAGER_PATH to rust/target/release/selenium-manager, and put the driver you want on the PATH. That last step is needed
because a custom build still cannot download what the vendors do not publish. The recipe was not run here; the wrapper script above
shows that SE_MANAGER_PATH works with a prebuilt binary. The Selenium Manager binaries can also be taken from the
SeleniumHQ/selenium_manager_artifacts repository or from the CI workflow; the Java binding extracts its copy into
~/.cache/selenium/manager/<version> (since 4.15.0).
Running locally
Run the tests with whatever the language provides: mvn test or gradle test for Java, pytest for Python, dotnet test for C#,
bundle exec rspec for Ruby, npx mocha (or npm test) for JavaScript. Start the application first (Bookshelf with npm start), set
BASE_URL, and choose headed or headless in the Options class. Typical local loops:
# Terminal 1: the application under test
cd bookshelf && PORT=3911 npm start
# Terminal 2: the tests
BASE_URL=http://localhost:3911 pytest -q
BASE_URL=http://localhost:3911 mvn -B test
Running the suite in CI, in containers and against a Grid is the subject of Running in CI/CD and Selenium Grid; structuring the tests so they stay maintainable is covered in Test Design & Page Objects. Where the other tools in this section install and run with one command, compare Playwright and Cypress.
Troubleshooting the first run
| Symptom | Likely cause and fix |
|---|---|
|
A manually installed driver no longer matches the auto-updated browser. Remove the driver from the |
|
Selenium Manager could not find or download a driver. Run it by hand with |
|
The binary was run without |
|
The page was replaced while you held an element. Wait for the new state and look the element up again. |
The script hangs or Chrome stays after a failure |
|
Safari: "You must enable Allow remote automation" |
Run |
Linux: |
Install the missing library (see Known limitations). |
Source: Selenium — Troubleshooting
More error messages, logging and the listener classes are collected in Waits & Expected Conditions.
What changed recently
| Version | Change |
|---|---|
Selenium 4.51.0 (9 October 2026) |
Chrome DevTools versions 153 to 155; Guava removed from the client bindings (the Java |
Selenium 4.50.0 |
Relative locators from elements and shadow roots; deprecated |
Selenium 4.49.0 |
A Linux arm64 Selenium Manager binary, managing Chrome 153 and later, Firefox 136 and later, and their drivers. |
Selenium Manager 0.4.48 (Selenium 4.48) |
Honours the |
Selenium Manager 0.4.45 (Selenium 4.45) |
Cache entries unused for 30 days are pruned automatically. |
Selenium Manager 0.4.44 (Selenium 4.44) |
|
Selenium 4.26.0 and 4.25.0 |
|
Selenium 4.15.0 |
The Java binding extracts the Selenium Manager binary into the cache. |
Selenium 4.14.0 |
Edge browser management. |
Selenium 4.13.0 |
|
Selenium 4.12.0 |
Firefox browser management. |
Selenium 4.11.0 |
Browser management starts, with Chrome for Testing. |
Selenium 4.6.0 |
Selenium Manager is bundled with every binding and manages drivers. |
Source: Selenium — Java change log
Source: Selenium — Selenium Manager
Selenium Manager 0.4.N ships with Selenium 4.N (Selenium 4.12 ships Selenium Manager 0.4.12), so the rows for Selenium Manager releases
apply from the Selenium release with the same minor number. Selenium Manager itself is still versioned 0.x and described as beta.