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 (selenium-java, selenium, Selenium.WebDriver, selenium-webdriver, the selenium-webdriver gem). It contains the WebDriver API, the By locators, the browser Options classes and the code that speaks the protocol.

Driver

A program per browser that implements the WebDriver protocol for it: chromedriver, geckodriver (Firefox), msedgedriver, safaridriver (shipped with macOS) and IEDriverServer. The driver is maintained by the browser vendor where possible; the Selenium project maintains the Internet Explorer driver.

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.

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

org.seleniumhq.selenium:selenium-java:4.51.0 (Maven Central)

Java 11. The project compiles with --release 11; the examples on this page were built with JDK 17.

Python

selenium 4.51.0 (PyPI)

Python 3.10 or later, as declared in the package metadata. The examples ran on Python 3.13.

C#

Selenium.WebDriver 4.51.0 (NuGet)

Targets net462, netstandard2.0 and net8.0. The examples ran on .NET SDK 10.

Ruby

selenium-webdriver 4.51.0 (RubyGems)

Ruby 3.3 or later (MRI), as declared in the gem metadata. The examples ran on Ruby 3.3.13.

JavaScript

selenium-webdriver 4.51.0 (npm)

Node.js 22 or later (engines field of the package). The examples ran on Node.js 24.

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 By locator (or the equivalent in your language). Most commands in a session are about elements.

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

new ChromeDriver(options)

webdriver.Chrome(options=options)

new ChromeDriver(options)

Selenium::WebDriver.for :chrome, options: options

new Builder().forBrowser(Browser.CHROME).build()

2

driver.get(url)

driver.get(url)

driver.Navigate().GoToUrl(url)

driver.get(url)

await driver.get(url)

3

driver.getTitle()

driver.title

driver.Title

driver.title

await driver.getTitle()

4

new WebDriverWait(driver, Duration.ofSeconds(5))

WebDriverWait(driver, 5)

new WebDriverWait(driver, TimeSpan.FromSeconds(5))

Selenium::WebDriver::Wait.new(timeout: 5)

driver.wait(condition, 5000)

5

driver.findElement(By.name("q"))

driver.find_element(By.NAME, "q")

driver.FindElement(By.Name("q"))

driver.find_element(name: 'q')

await driver.findElement(By.name('q'))

6

element.sendKeys("x"), element.click()

element.send_keys("x"), element.click()

element.SendKeys("x"), element.Click()

element.send_keys('x'), element.click

await element.sendKeys('x'), await element.click()

7

element.getText()

element.text

element.Text

element.text

await element.getText()

8

driver.quit()

driver.quit()

driver.Quit()

driver.quit

await driver.quit()

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

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

options.addArguments("--headless=new")

--headless=new is the argument used on this page, and the Selenium Chrome page lists it among the commonly used arguments. It runs the full browser engine without a window.

Edge

options.addArguments("--headless=new") on EdgeOptions

Same as Chrome; Edge is Chromium-based.

Firefox

options.addArguments("-headless") on FirefoxOptions

A single dash. Firefox has no new variant.

Safari

Not available

safaridriver drives the visible Safari application only.

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.

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. safaridriver ships with macOS, but you must run safaridriver --enable once (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.

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), unittest from the standard library

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:

  1. 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.

  2. Assert with the runner’s assertion library. Selenium has no assertions of its own.

  3. Take the base URL from configuration, here the BASE_URL environment variable, so the same tests run against a local server, a container or a staging host.

  4. Never share a WebDriver between threads. A driver instance is not thread safe; give each parallel test its own (the Java ThreadGuard class 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:

flowchart TD A["Script creates a driver object"] --> B{"Driver location given by a Service, a SE_*DRIVER variable or the PATH?"} B -- yes --> C["Use that driver, no download"] B -- no --> D["Selenium Manager runs"] D --> E["1. Detect the installed browser and its version"] E --> F["2. Resolve the matching driver version from the vendor metadata"] F --> G{"Driver in the cache?"} G -- yes --> H["Use the cached driver"] G -- no --> I["3. Download and unpack it into the cache"] I --> H D --> J{"Browser not installed, or a version or channel requested?"} J -- yes --> K["Download the browser into the cache too"] J -- no --> L["Use the installed browser"] H --> M["Binding starts the driver, the driver starts the browser"] K --> M L --> M

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:

  1. Browser discovery. It finds the installed browser and runs --version on it.

  2. 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.

  3. Driver download. It downloads and unpacks the driver from the resolved URL.

  4. Driver cache. It stores the unpacked driver under ~/.cache/selenium and 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 (ftp.mozilla.org)

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

stable

The current Chrome for Testing release.

beta

The release that will become stable next.

dev

The version in development.

canary

The nightly build, meant for developers.

esr

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 are browsers, drivers, stats and cached_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:

  1. Command line arguments. The bindings pass the options of your session this way.

  2. The configuration file se-config.toml in the cache folder.

  3. Environment variables. Each key becomes an environment variable by upper-casing it, replacing - with _ and adding the prefix SE_ (cache-path becomes SE_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

--browser <BROWSER>

browser = "chrome"

SE_BROWSER

chrome, firefox, edge, iexplorer, safari, safaritp, webview2 or electron.

--driver <DRIVER>

driver = "chromedriver"

SE_DRIVER

chromedriver, geckodriver, msedgedriver, IEDriverServer or safaridriver.

--browser-version <V>

browser-version = "106"

SE_BROWSER_VERSION

A major version, or beta, dev, canary (or nightly), esr (Firefox).

--driver-version <V>

driver-version = "106.0.5249.61"

SE_DRIVER_VERSION

An exact driver version, such as 106.0.5249.61 or 0.31.0.

--browser-path <PATH>

browser-path = "/usr/bin/google-chrome"

SE_BROWSER_PATH

Absolute path of the browser to detect the version of.

--driver-mirror-url <URL>

driver-mirror-url = "..."

SE_DRIVER_MIRROR_URL

Mirror of the driver repositories.

--browser-mirror-url <URL>

browser-mirror-url = "..."

SE_BROWSER_MIRROR_URL

Mirror of the browser repositories.

--output <OUTPUT>

output = "LOGGER"

SE_OUTPUT

LOGGER (default), JSON, SHELL or MIXED.

--os <OS>

os = "linux"

SE_OS

windows, linux or macos, for drivers and browsers of another platform.

--arch <ARCH>

arch = "x64"

SE_ARCH

x32, x64 or arm64.

--proxy <PROXY>

proxy = "myproxy:8080"

SE_PROXY

HTTP proxy for Selenium Manager’s own downloads, for example myproxy:port or myuser:mypass@myproxy:port.

--timeout <SECONDS>

timeout = 300

SE_TIMEOUT

Timeout of network requests, default 300.

--ttl <SECONDS>

ttl = 3600

SE_TTL

Lifetime of discovered versions, default 3600.

--offline

offline = true

SE_OFFLINE

No network requests and no downloads.

--force-browser-download

force-browser-download = true

SE_FORCE_BROWSER_DOWNLOAD

Download the browser even if one is installed.

--avoid-browser-download

avoid-browser-download = true

SE_AVOID_BROWSER_DOWNLOAD

Never download the browser.

--skip-driver-in-path

skip-driver-in-path = true

SE_SKIP_DRIVER_IN_PATH

Ignore a driver found on the PATH.

--skip-browser-in-path

skip-browser-in-path = true

SE_SKIP_BROWSER_IN_PATH

Ignore a browser found on the PATH.

--debug, --trace

debug = true, trace = true

SE_DEBUG, SE_TRACE

Show DEBUG or TRACE messages. The binary also has --log-level <info|debug|trace|warn|error>.

--cache-path <PATH>

cache-path = "/custom/cache"

SE_CACHE_PATH

Folder for downloads, metadata and se-config.toml. In the TOML file a Windows path needs doubled backslashes.

--clear-cache, --clear-metadata

(none)

SE_CLEAR_CACHE, SE_CLEAR_METADATA

Delete the cache folder or the metadata file.

--language-binding <LANG>

language-binding = "Python"

SE_LANGUAGE_BINDING

Which binding is calling. Set by the bindings.

--avoid-stats

avoid-stats = true

SE_AVOID_STATS

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

browserName

--browser

browserVersion

--browser-version

Browser binary location (for example Chrome’s setBinary)

--browser-path

proxy (HTTP or SSL)

--proxy

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 Service with 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 PATH appears in the debug output above when there is none). Add --skip-driver-in-path to ignore it.

  • The SE_CHROMEDRIVER, SE_EDGEDRIVER, SE_GECKODRIVER and SE_IEDRIVER environment 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. With SE_CHROMEDRIVER set to the cached driver, SE_OFFLINE=true and an empty SE_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:

  1. 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.

  2. Use SE_PROXY or proxy = "..." in se-config.toml when Selenium Manager needs a proxy that the browser must not use.

  3. Allow the endpoints in your firewall, or mirror them with driver-mirror-url and browser-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 a geckodriver-support.json file for Firefox, and msedgedriver.microsoft.com and edgeupdates.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 Service or in a SE_*DRIVER variable. This is what happened in the Ruby container on this page: the container is Linux arm64 with Chromium 156 and the Debian chromium-driver on the PATH.

  • 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 with libatk-1.0.so.0: cannot open shared object file, install libatk-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 true on a Grid node lets the node use Selenium Manager to find drivers (see Selenium Grid), and selenium-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

session not created: This version of ChromeDriver only supports Chrome version N

A manually installed driver no longer matches the auto-updated browser. Remove the driver from the PATH (or pass --skip-driver-in-path) so Selenium Manager resolves a matching one.

NoSuchDriverException: Unable to obtain driver for chrome

Selenium Manager could not find or download a driver. Run it by hand with --debug; check offline mode, the proxy and the firewall.

You need to specify a browser or driver from the binary

The binary was run without --browser or --driver and no se-config.toml supplied them from the cache in effect.

StaleElementReferenceError after a click

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

quit() was skipped. Use try/finally or the runner’s teardown hook.

Safari: "You must enable Allow remote automation"

Run safaridriver --enable once, or enable remote automation in the Developer section of the Safari settings.

Linux: error while loading shared libraries

Install the missing library (see Known limitations).

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 ExpectedCondition stops extending Guava’s Function; selenium-remote-driver still lists Guava as a dependency in the printed tree); RemoteWebDriver.getClientFactory(); a BiDi code generator; the Java binding no longer passes a bare --proxy to Selenium Manager. Selenium Manager 0.4.51 degrades to empty metadata when the cache cannot be read.

Selenium 4.50.0

Relative locators from elements and shadow roots; deprecated HttpClient methods removed; Selenium Manager 0.4.50 gives a clear error when the metadata file cannot be opened.

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 DO_NOT_TRACK environment variable.

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)

--init-skills and --init-rules write a guidance file for AI coding assistants.

Selenium 4.26.0 and 4.25.0

SE_CHROMEDRIVER and the other driver-path environment variables in Python (4.26.0) and Ruby (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

SE_MANAGER_PATH selects a custom Selenium Manager binary.

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.

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.