Selenium IDE, Legacy Components & Upgrading to Selenium 4
|
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 has more than twenty years of history, and a lot of it is still visible in search results, old test suites and
tutorials: a record-and-playback tool, a proxy-based server, a JSON protocol that predates the W3C standard, page objects
initialised by reflection, and capability objects with free-form keys. This page covers all of it in one place so you can
recognise it, decide whether to keep using it, and move it to the current API. It was written against Selenium 4.51.0
(released 9 October 2026) and checked on 25 October 2026. The runnable examples were run against the
Bookshelf application on a local port with Selenium 4.51.0,
Java 17 with JUnit 5.14.4, Python 3 with pytest, Chrome 155 and the selenium/standalone-chrome:4.51.0 Grid image; the parts
that were not run are marked as such in the text. The modern way to write tests is described in
Selenium: Getting Started,
Drivers, Browsers & Options,
Locating & Interacting and
Waits & Expected Conditions; this page is the map of what
those pages replaced.
Selenium IDE
Selenium IDE records what you do in a browser and plays it back. It is the quickest way to see the Selenium command
vocabulary (open, click, type, assertText), to produce a throwaway reproduction of a bug, or to let someone who does not
program capture a flow. It is not a replacement for a coded suite, and the project that maintains it has been quiet for a long
time, which matters for the decision of whether to invest in it.
What it is today
The official documentation page for the IDE (updated 9 October 2026) describes it as "a standalone application that records
and plays back a user’s actions" for Windows, macOS and Linux, downloaded from the GitHub releases page of the
SeleniumHQ/selenium-ide repository. The downloads page adds that it can also be installed with npm install -g selenium-ide
and that the previous browser extension (version 3) is no longer maintained, is no longer in the Chrome Web Store and can still
be installed for Firefox or Edge. The IDE’s own documentation site (last updated in 2019) still tells you to install the IDE
"from either the Chrome or Firefox web store" and to click its icon in the browser toolbar. That text describes the old
extension; follow the downloads page, not the 2019 instructions.
The facts that matter when you evaluate it, checked on 25 October 2026:
| Item | State |
|---|---|
Latest release |
|
npm |
|
Repository activity |
The repository is not archived and its default branch still receives commits (the last push seen was 6 October 2026), but no release has followed the 2024 beta. |
Project file |
A single JSON file with the extension |
Recording, playback and the project structure
The workflow documented in the IDE’s Getting Started page is unchanged between the extension and the application:
-
Create a project: give it a name and a base URL, the address of the application under test, set once for all tests in the project and overridable later.
-
Record: a browser window opens on the base URL and every click and keystroke becomes a command row (command, target, value). Stop recording from the IDE window.
-
Organise: tests are grouped into suites; a "Default Suite" is created with the project and holds the first test.
-
Save: saving writes the whole project to one
.sidefile. -
Play back: select a test or suite and press play; the test runs in the window used for recording, or in a new one.
-
Cross-browser: to run on other browsers, on a Grid and in parallel, use the command-line runner (below).
Each command has a command (the action, such as click), a target (usually a locator such as id=q, css=... or
xpath=...) and a value (text to type, an expected string or a variable name). The recorder stores several alternative
locators per step (targets) and falls back to them during playback when the primary locator fails. Hand-written steps need
only the primary target, which is how the example below was written.
Control flow
The IDE adds conditional logic and loops through opening and closing commands, with the condition written as a JavaScript
expression in the target field. Variables from execute script and store commands can be used in the expression.
| Commands | Purpose |
|---|---|
|
Conditional branching. |
|
Repeat the block a fixed number of times (the number goes in the target field). |
|
Run the block, then evaluate the expression in |
|
Evaluate first, run the block while the expression is true. |
|
Iterate a JavaScript array held in a variable; the target is the array variable and the value is the name of the loop variable. |
do ... repeat if and while carry infinite-loop protection: after 1000 iterations by default the test stops, and a number in
the value field of repeat if or while overrides the limit.
A project that was run
The project below has two tests in one suite. The first types a query, submits it and waits for the result count; the second
reads the number of book cards with execute script, stores it in the variable cards and uses if / else / end. The
file is the exact .side file that was run.
{
"id": "6a1c9d1e-0000-4000-8000-000000000001",
"version": "2.0",
"name": "Bookshelf",
"url": "http://localhost:3917",
"tests": [
{
"id": "6a1c9d1e-0000-4000-8000-000000000010",
"name": "search finds a book",
"commands": [
{
"id": "6a1c9d1e-0000-4000-8000-000000000011",
"comment": "",
"command": "open",
"target": "/books",
"targets": [],
"value": ""
},
{
"id": "6a1c9d1e-0000-4000-8000-000000000012",
"comment": "",
"command": "type",
"target": "css=[data-testid=search-input]",
"targets": [],
"value": "quiet"
},
{
"id": "6a1c9d1e-0000-4000-8000-000000000013",
"comment": "",
"command": "click",
"target": "css=[data-testid=search-submit]",
"targets": [],
"value": ""
},
{
"id": "6a1c9d1e-0000-4000-8000-000000000014",
"comment": "",
"command": "waitForText",
"target": "css=[data-testid=result-count]",
"targets": [],
"value": "1 book found"
},
{
"id": "6a1c9d1e-0000-4000-8000-000000000015",
"comment": "",
"command": "assertText",
"target": "css=[data-testid=result-count]",
"targets": [],
"value": "1 book found"
}
]
},
{
"id": "6a1c9d1e-0000-4000-8000-000000000020",
"name": "control flow counts cards",
"commands": [
{
"id": "6a1c9d1e-0000-4000-8000-000000000021",
"comment": "",
"command": "open",
"target": "/books",
"targets": [],
"value": ""
},
{
"id": "6a1c9d1e-0000-4000-8000-000000000022",
"comment": "",
"command": "executeScript",
"target": "return document.querySelectorAll('[data-testid=book-card]').length",
"targets": [],
"value": "cards"
},
{
"id": "6a1c9d1e-0000-4000-8000-000000000023",
"comment": "",
"command": "if",
"target": "${cards} > 0",
"targets": [],
"value": ""
},
{
"id": "6a1c9d1e-0000-4000-8000-000000000024",
"comment": "",
"command": "assertElementPresent",
"target": "css=[data-testid=book-card]",
"targets": [],
"value": ""
},
{
"id": "6a1c9d1e-0000-4000-8000-000000000025",
"comment": "",
"command": "else",
"target": "",
"targets": [],
"value": ""
},
{
"id": "6a1c9d1e-0000-4000-8000-000000000026",
"comment": "",
"command": "assertElementPresent",
"target": "css=[data-testid=never-present]",
"targets": [],
"value": ""
},
{
"id": "6a1c9d1e-0000-4000-8000-000000000027",
"comment": "",
"command": "end",
"target": "",
"targets": [],
"value": ""
}
]
}
],
"suites": [
{
"id": "6a1c9d1e-0000-4000-8000-000000000030",
"name": "Default Suite",
"persistSession": false,
"parallel": false,
"timeout": 300,
"tests": [
"6a1c9d1e-0000-4000-8000-000000000010",
"6a1c9d1e-0000-4000-8000-000000000020"
]
}
],
"urls": [
"http://localhost:3917/"
],
"plugins": []
}
Source: Selenium IDE — Getting Started
The first version of this test used waitForElementVisible on book-card after pressing search. It failed with
Actual value '24 books found' did not match '1 book found' during assertText: the cards from the unfiltered list were already
visible, so the wait returned at once and the assertion ran before the filtered result arrived. The IDE reproduces the same
race every coded suite has; the fix is to wait for the thing you assert (waitForText on the result count), as in the file
above.
The command-line runner: selenium-side-runner
selenium-side-runner runs .side files outside the IDE, on any browser that has a WebDriver, in parallel and on a Grid, with
Jest underneath. Its documentation asks for Node.js 8 or 10 and global installation of the runner and a driver package; the
published package declares Node.js 16 or later, and the runner depends on selenium-webdriver 4.51.0, so Selenium Manager finds
the driver and no chromedriver package is needed. The commands below are the ones that were run.
mkdir sel-ide-side && cd sel-ide-side
npm init -y
npm i selenium-side-runner@4.0.13
npx selenium-side-runner --version
Real output of npx selenium-side-runner --version:
4.0.13
Running the project in headless Chrome against Bookshelf, with the capabilities written the way the runner documentation shows for headless mode:
npx selenium-side-runner -c "browserName=chrome goog:chromeOptions.args=[disable-infobars, headless]" \
--base-url http://localhost:3917 --output-directory results bookshelf.side
PASS node_modules/selenium-side-runner/dist/main.test.js
Running project Bookshelf
Running suite Default Suite
✓ Running test search finds a book (1720 ms)
✓ Running test control flow counts cards (910 ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Test results written to: results/results-2026-10-25T20-41-35.835Z.json
Settings can live in a .side.yml file in the working directory, which the runner picks up automatically (--no-sideyml skips
it, and --config-file points at another YAML file). This file produced the same two passing tests when the runner was started
as just npx selenium-side-runner bookshelf.side:
capabilities:
browserName: "chrome"
goog:chromeOptions:
args: ["--headless=new"]
baseUrl: "http://localhost:3917"
Options in 4.0.13 compared with the documentation
The options below are the output of selenium-side-runner --help for 4.0.13. The documentation page was last updated in 2019
and has drifted from the package:
| Option | What it does in 4.0.13 | Documentation drift |
|---|---|---|
|
WebDriver capabilities as space-separated |
The documented |
|
Remote server or Grid URL. |
The documented |
|
Overrides the base URL stored in the project. |
Matches the documentation. |
|
Runs suites whose name matches the regular expression. When no suite matches, the runner prints that it will "attempt to iterate all tests" and filters test names instead (this is what happened with |
The documentation describes filtering test names only. |
|
Number of parallel workers; defaults to the number of CPU cores. |
Matches. |
|
Retries failed tests N times (a thin wrapper over |
Not mentioned in the page. |
|
Maximum time in milliseconds to spend locating an element (default 15000). |
Not mentioned. |
|
Maximum time in milliseconds for one test to finish (default 60000). |
Not mentioned. |
|
Writes the results as JSON to a file named after a timestamp. |
The documented |
|
Saves a screenshot of each failed test. |
Not mentioned. |
|
Proxy configuration ( |
Present in the documentation under "Using a proxy server". |
|
Passes options through to Jest. |
Not mentioned. |
|
Alternative YAML configuration, debug output switches, and running a project written for another project-file version. |
Partly documented. |
Suites run in parallel with each other and tests inside a suite run sequentially unless you tick Run in parallel in the
suite’s settings in the IDE and save the project (the parallel flag in the .side file).
To run on a Grid, add --server:
npx selenium-side-runner --server http://localhost:4444 -c "browserName=chrome" bookshelf.side
That Grid command was not run; the runs above used a local ChromeDriver started by Selenium Manager. Firefox, Edge and Safari were not run with the runner: Firefox does not launch locally on the test host and Safari needs the system setting "Allow remote automation".
Code export
The IDE can export a test or a suite to WebDriver code by right-clicking it, choosing Export, selecting a language and
ticking, if wanted, the origin-tracing comments option, which writes a comment in the generated code naming the IDE step that
produced each line. The code export page lists four supported targets: C# NUnit, Java JUnit, JavaScript Mocha and Python
pytest. The same page still contains sections for C# xUnit and Ruby RSpec that its "Supported Exports" list does not name,
and its dependency examples are from the Selenium 4 alpha era (Java 8 with JUnit 4.12 and 4.0.0-alpha-3, pytest 4.6 with
selenium == 4.0.0a1, .NET Core 2.0). The exporter packages exist on npm (for example @seleniumhq/code-export-python-pytest
at 4.0.13, published in November 2024). Code export was not run for this page: the exported files target old library
versions, so treat the output as a starting point that you port to current Selenium (Options classes, Duration, no driver
paths), using the rest of this page.
When to use it and when not to
Use the IDE to explore locators, show a team what a flow looks like, or capture a bug reproduction. Do not build a regression
suite on it: .side files cannot share page objects, the recorder’s locators are brittle (xpath=//div[3]/a), the last
release is from July 2024 and its documentation describes an extension that no longer exists in the Chrome Web Store. For
suites that last, write code with the bindings and the practices in
Test Design & Page Objects, or use a recorder in another tool
(playwright codegen, Cypress Studio; see Playwright:
Debugging, Tracing & Codegen).
Legacy components
The Selenium documentation keeps a Legacy section "purely for historical reasons and not as an incentive to use deprecated components". This section summarises what is in it, what each generation was, and what replaced it.
The generations
| Generation | What it was | What replaced it |
|---|---|---|
Selenium 1 (Selenium RC) |
A Java server ( |
WebDriver (Selenium 2) |
Selenium 2 |
A rewrite built on WebDriver code, which drives the browser natively through a driver rather than by injecting JavaScript. It still shipped the RC server, a legacy Firefox driver implemented as a browser extension (no longer works after Firefox changed its security model; use geckodriver), the pure-Java HtmlUnit driver, Grid 2 (WebDriver and RC) and the Java page-object helpers |
Selenium 3 |
Selenium 3 |
WebDriver without the RC code. Grid 3, the JSON Wire Protocol and, from about version 3.11, the W3C protocol side by side (the clients negotiated whichever the driver supported). Language bindings such as Python and Ruby had their own Selenium 3 documentation, and JavaScript tests had to be moved off the WebDriverJS promise manager to async/await. |
Selenium 4 |
Selenium 4 |
Implements the W3C WebDriver specification only, a rewritten Grid, Selenium Manager and WebDriver BiDi. |
Current (4.51.0) |
Source: Selenium — Legacy documentation
The legacy tree also holds the original Firefox-extension IDE, a page on migrating from RC to WebDriver, notes about stealing focus in Linux, untrusted certificates and WebDriver for mobile browsers (what Appium replaced), the Grid 2 and Grid 3 set-up guides and the Selenium 2 developer notes. None of that is relevant to code you start today; it is useful for reading and migrating old suites.
The JSON Wire Protocol and the W3C protocol
Before the W3C standard, every driver spoke the JSON Wire Protocol: a REST-like service where the client sent JSON over HTTP to
the driver or the Remote WebDriver server, with endpoints such as POST /session (carrying desiredCapabilities),
POST /session/:sessionId/element and a capabilities object with keys like browserName, version, platform,
javascriptEnabled, takesScreenshot and acceptSslCerts. Elements were returned as an object with the key ELEMENT.
The W3C WebDriver protocol keeps the same shape but standardises it: new sessions send capabilities with alwaysMatch and
firstMatch, standard capability names are browserName, browserVersion, platformName, acceptInsecureCerts,
pageLoadStrategy, proxy, timeouts, unhandledPromptBehavior (and others in the specification), every non-standard
capability needs a vendor prefix containing a colon, and elements are returned under the key
element-6066-11e4-a52e-4f735466cecf. Selenium 4 speaks the W3C protocol only. A Selenium 4.51.0 Grid, asked for a session in
the legacy format, answers:
curl -s -X POST http://localhost:4551/wd/hub/session -H 'Content-Type: application/json' \
-d '{"desiredCapabilities":{"browserName":"chrome"}}'
session not created: Could not start a new session. No nodes support the capabilities in the request
The same request in the W3C shape starts a session, and finding an element returns the W3C element key:
{"value": {"element-6066-11e4-a52e-4f735466cecf": "f.F876D29C343D9E9C9D1503718415A27C.d.5721E3E66EF11A49B6ACE8F67019E8A8.e.16"}}
Source: W3C WebDriver (Level 2)
The first output above is the real answer of the selenium/standalone-chrome:4.51.0 container to the legacy request; the
second is its real response to POST /element on a session created with the W3C request shape.
PageFactory
PageFactory is a Java helper in selenium-support (org.openqa.selenium.support.PageFactory) from the Selenium 2 era. You
declare WebElement or List<WebElement> fields in a page object, call PageFactory.initElements(driver, page) and the
fields become lazy proxies: each use of the field looks the element up again on the live page. The default lookup uses the
field name as an id, then as a name. Annotations change the strategy:
| Feature | Behaviour |
|---|---|
|
Chooses the locator; |
|
Match any of several locators ( |
|
Looks the element up once and keeps the reference; it goes stale after navigation or a re-render. |
|
Retries each lookup until the element is present and displayed or the timeout passes, which gives fields a basic wait. |
|
Creates the page object itself, using a constructor that takes a |
The official page now ends with "For current guidance, read about Page Object Models". PageFactory is still present and not
deprecated in 4.51.0, but it is rarely recommended: it hides when a lookup happens, @CacheLookup produces stale-element
errors, the Python binding has no equivalent at all, and
explicit page object methods with waits are easier to read and to debug. The example below is a page object using the
PageFactory, with the q field found by the id-then-name default, the other fields by @FindBy, and
AjaxElementLocatorFactory supplying a five-second retry; the test that uses it follows.
package demo;
import java.time.Duration;
import java.util.List;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;
import org.openqa.selenium.support.pagefactory.AjaxElementLocatorFactory;
/** Page Object initialised by the PageFactory (a Selenium 2 era pattern, still shipped in selenium-support). */
public class BooksPage {
private final WebDriver driver;
private final String baseUrl;
// No annotation: the PageFactory falls back to the field name and looks for id="q", then name="q".
private WebElement q;
@FindBy(css = "[data-testid=search-submit]")
private WebElement searchSubmit;
@FindBy(css = "[data-testid=result-count]")
private WebElement resultCount;
@FindBy(css = "[data-testid=book-card]")
private List<WebElement> cards;
public BooksPage(WebDriver driver, String baseUrl) {
this.driver = driver;
this.baseUrl = baseUrl;
// Every field access is lazily looked up (and retried for up to 5 seconds) by the AjaxElementLocator.
PageFactory.initElements(new AjaxElementLocatorFactory(driver, (int) Duration.ofSeconds(5).toSeconds()), this);
}
public BooksPage open() {
driver.get(baseUrl + "/books");
return this;
}
public BooksPage search(String text) {
q.sendKeys(text);
searchSubmit.click();
return this;
}
public String resultCountText() { return resultCount.getText(); }
public int cardCount() { return cards.size(); }
}
package demo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.time.Duration;
import org.junit.jupiter.api.*;
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 LegacyPageFactoryTest {
static final String BASE = System.getProperty("BASE_URL", "http://localhost:3917");
WebDriver driver;
@BeforeEach void start() {
ChromeOptions o = new ChromeOptions();
o.addArguments("--headless=new");
driver = new ChromeDriver(o);
}
@AfterEach void stop() { driver.quit(); }
@Test void pageFactoryPageObject() {
BooksPage page = new BooksPage(driver, BASE).open().search("quiet");
new WebDriverWait(driver, Duration.ofSeconds(5))
.until(ExpectedConditions.textToBePresentInElementLocated(By.cssSelector("[data-testid=result-count]"), "1 book found"));
assertEquals("1 book found", page.resultCountText());
assertEquals(1, page.cardCount());
}
}
The test passed with Selenium 4.51.0 and headless Chrome 155. In Python there is no PageFactory; the equivalent is a page
object whose attributes are descriptors that look the element up on every access, which is what the proxies do. The class
below is a small implementation of that idea, with a page object that uses it, and a test that passed in the same run as the
other Python examples on this page:
import pytest
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from conftest import APP_URL, GRID_URL
class LazyElement:
"""A tiny PageFactory equivalent: a descriptor that looks the element up on every access."""
def __init__(self, by, value):
self.locator = (by, value)
def __get__(self, page, owner):
return page.driver.find_element(*self.locator)
class BooksPage:
search_input = LazyElement(By.ID, "q")
search_submit = LazyElement(By.CSS_SELECTOR, "[data-testid=search-submit]")
result_count = LazyElement(By.CSS_SELECTOR, "[data-testid=result-count]")
def __init__(self, driver, base_url):
self.driver = driver
self.base_url = base_url
def open(self):
self.driver.get(f"{self.base_url}/books")
return self
def search(self, text):
self.search_input.send_keys(text)
self.search_submit.click()
WebDriverWait(self.driver, 5).until(
EC.text_to_be_present_in_element((By.CSS_SELECTOR, "[data-testid=result-count]"), "1 book found"))
return self
def test_page_object_replaces_page_factory(driver, base_url):
page = BooksPage(driver, base_url).open().search("quiet")
assert page.result_count.text == "1 book found"
Source: Selenium — Page object models
For the current, explicit page-object style in Java and Python (including LoadableComponent, the other Selenium 2 helper), see
Test Design & Page Objects and
Locating & Interacting.
DesiredCapabilities (legacy) and the Options replacement
In Selenium 2 and 3 you described the browser you wanted with a DesiredCapabilities object (a dictionary in Python, a
hash in Ruby, a plain object in JavaScript) and passed it to the driver or to RemoteWebDriver. Keys were free-form:
platform, version, and whatever your cloud vendor wanted (build, name) at the top level. Selenium 4 sends W3C
capabilities, which reject unknown keys without a vendor prefix. The Options classes (ChromeOptions, FirefoxOptions,
EdgeOptions, Options in each binding) are the typed replacement: standard capabilities are properties, browser
arguments are methods, and everything else goes in a vendor-prefixed capability.
The legacy shape below is what Selenium 3 projects used. It is shown only to explain the error; do not write new code like
this. In Selenium 4.51.0, Java still contains the class org.openqa.selenium.remote.DesiredCapabilities (without the old
static factories such as chrome() and firefox()) and Python still has DesiredCapabilities.CHROME (a plain dictionary,
{'browserName': 'chrome'}), but the clients no longer accept them in the same places.
@Test void legacyDesiredCapabilities() throws Exception {
// Selenium 3 style: free-form keys, no vendor prefix. Kept only to show what a W3C-only server does with them.
DesiredCapabilities caps = new DesiredCapabilities();
caps.setBrowserName("chrome");
caps.setCapability("platform", "Linux");
caps.setCapability("version", "92");
caps.setCapability("build", "nightly-42");
caps.setCapability("name", "legacy caps");
WebDriver driver = null;
try {
driver = new RemoteWebDriver(new URL(GRID), caps);
driver.get(APP + "/books");
System.out.println("LEGACY accepted: " + ((RemoteWebDriver) driver).getCapabilities().getBrowserVersion());
} catch (Exception e) {
System.out.println("LEGACY rejected: " + e.getClass().getSimpleName() + ": " + e.getMessage().lines().limit(6).toList());
} finally {
if (driver != null) driver.quit();
}
}
Run against the Grid container, that code is refused: the request reaches the server with build, name, platform and
version as top-level keys. Posting a W3C request that contains only the extra key build directly to the Grid shows the
reason, Illegal key values seen in w3c capabilities: [build]. The test itself printed:
LEGACY rejected: SessionNotCreatedException: [Could not start a new session. Possible causes are invalid address of the remote server or browser start-up failure. ...
The Selenium 4 version of the same request uses the typed setters for the standard capabilities and one vendor-prefixed block
for the cloud vendor’s keys (cloud:options is the example prefix used by the official guide; use the prefix your vendor
documents):
@Test void optionsReplacement() throws Exception {
// Selenium 4 style: standard capabilities as typed setters, everything else vendor-prefixed.
ChromeOptions options = new ChromeOptions();
options.addArguments("--headless=new");
options.setPlatformName("Linux");
Map<String, Object> cloudOptions = new HashMap<>();
cloudOptions.put("build", "nightly-42");
cloudOptions.put("name", "options");
options.setCapability("cloud:options", cloudOptions);
WebDriver driver = new RemoteWebDriver(new URL(GRID), options);
try {
driver.get(APP + "/books");
Capabilities got = ((RemoteWebDriver) driver).getCapabilities();
assertEquals("chrome", got.getBrowserName());
assertTrue(driver.getTitle().length() > 0);
System.out.println("OPTIONS accepted: Chrome " + got.getBrowserVersion());
} finally {
driver.quit();
}
}
The test printed OPTIONS accepted: Chrome 155.0.8059.79 against the Grid container, which reached Bookshelf through
host.docker.internal. The Python versions of the same two ideas follow: the legacy keyword arguments fail immediately with a
TypeError in 4.51.0 (the keyword arguments were removed), and options= is the replacement.
def test_removed_selenium3_arguments(tmp_path):
# Selenium 3 style calls fail with a TypeError in 4.51 instead of a deprecation warning.
with pytest.raises(TypeError, match="executable_path"):
webdriver.Chrome(executable_path="/usr/local/bin/chromedriver")
with pytest.raises(TypeError, match="desired_capabilities"):
webdriver.Remote(GRID_URL, desired_capabilities=DesiredCapabilities.CHROME)
with pytest.raises(TypeError, match="chrome_options"):
webdriver.Chrome(chrome_options=webdriver.ChromeOptions())
def test_desired_capabilities_is_only_a_dict():
assert DesiredCapabilities.CHROME == {"browserName": "chrome"}
def test_options_replacement_on_a_grid():
options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
options.platform_name = "Linux"
options.set_capability("cloud:options", {"build": "nightly-42", "name": "options"})
driver = webdriver.Remote(GRID_URL, options=options)
try:
driver.get(f"{APP_URL}/books")
assert driver.capabilities["browserName"] == "chrome"
assert driver.find_element(By.CSS_SELECTOR, "[data-testid=result-count]").text == "24 books found"
finally:
driver.quit()
def test_timeouts_in_seconds(driver, base_url):
# Python keeps seconds (floats); only Java moved to Duration.
driver.implicitly_wait(10)
driver.set_page_load_timeout(10)
driver.set_script_timeout(120)
t = driver.timeouts
assert (t.implicit_wait, t.page_load, t.script) == (10, 10, 120)
The C#, Ruby and JavaScript equivalents come from the official upgrade guide; they are shown as the guide writes them and were not run for this page (only the Java and Python bindings were exercised). Their shape is the same as the Java and Python ones:
// Before (Selenium 3)
DesiredCapabilities caps = new DesiredCapabilities();
caps.SetCapability("browserName", "firefox");
caps.SetCapability("platform", "Windows 10");
caps.SetCapability("version", "92");
caps.SetCapability("build", myTestBuild);
caps.SetCapability("name", myTestName);
var driver = new RemoteWebDriver(new Uri(CloudURL), caps);
// After (Selenium 4)
var browserOptions = new FirefoxOptions();
browserOptions.PlatformName = "Windows 10";
browserOptions.BrowserVersion = "92";
var cloudOptions = new Dictionary<string, object>();
cloudOptions.Add("build", myTestBuild);
cloudOptions.Add("name", myTestName);
browserOptions.AddAdditionalOption("cloud:options", cloudOptions);
var driver = new RemoteWebDriver(new Uri(CloudURL), browserOptions);
# Before (Selenium 3)
caps = Selenium::WebDriver::Remote::Capabilities.firefox
caps[:platform] = 'Windows 10'
caps[:version] = '92'
caps[:build] = my_test_build
caps[:name] = my_test_name
driver = Selenium::WebDriver.for :remote, url: cloud_url, desired_capabilities: caps
# After (Selenium 4)
options = Selenium::WebDriver::Options.firefox
options.platform_name = 'Windows 10'
options.browser_version = 'latest'
cloud_options = {}
cloud_options[:build] = my_test_build
cloud_options[:name] = my_test_name
options.add_option('cloud:options', cloud_options)
driver = Selenium::WebDriver.for :remote, capabilities: options
// Before (Selenium 3)
caps = {};
caps['browserName'] = 'Firefox';
caps['platform'] = 'Windows 10';
caps['version'] = '92';
caps['build'] = myTestBuild;
caps['name'] = myTestName;
// After (Selenium 4)
capabilities = {
browserName: 'firefox',
browserVersion: '92',
platformName: 'Windows 10',
'cloud:options': {
build: myTestBuild,
name: myTestName,
},
};
Page-load strategy, timeouts, proxy and the other standard capabilities are covered in Drivers, Browsers & Options.
Upgrading to Selenium 4
The official guide says that upgrading is "a painless process" for the officially supported languages (Ruby, JavaScript, C#,
Python and Java) and walks through three steps: prepare the test code, upgrade the dependencies, then fix the errors and
deprecation messages. Note that the guide’s own examples pin 4.4.0 and 4.4.3, the release in which it was written; replace
those with the current version (4.51.0 on the check date). Selenium 3.x releases from about 3.11 already implemented the W3C
protocol, so W3C-compliant Selenium 3 code works unchanged in Selenium 4. Code that relied on the legacy protocol does not.
Step 1: prepare the code
Selenium 4 removes the legacy protocol and uses W3C WebDriver underneath. For most code that is invisible; the two exceptions are capabilities and the Actions API.
Capabilities. A session fails to start if the requested capabilities are not W3C compliant. The standard ones are
browserName, browserVersion (replaces version), platformName (replaces platform), acceptInsecureCerts,
pageLoadStrategy, proxy, timeouts and unhandledPromptBehavior. Anything else, including browser-specific and cloud
vendor keys, needs a vendor prefix, usually in a block such as goog:chromeOptions, moz:firefoxOptions, ms:edgeOptions or
the vendor’s own cloud:options-style key. Replace DesiredCapabilities with the Options class of the browser, as shown above.
Actions. The Actions API follows the W3C model: input sources (keyboard, pointer, wheel) with sequences of ticks. In the Java
bindings the old Actions calls still work, but perform() builds the sequence itself so .build().perform() can drop the
build(); the old Mouse and Keyboard interfaces and HasInputDevices are gone from the 4.51.0 jars. In every binding
the new pointer and wheel actions, such as scrollToElement, scrollByAmount and clickAndHold, are available. Details are
in Locating & Interacting.
Step 2: upgrade the dependencies
| Binding | How to upgrade |
|---|---|
Java (Maven) |
Change the version of |
Python |
|
C# |
Update the |
Ruby |
|
JavaScript |
|
The Maven snippet below is the dependency block used by the Java examples on this page:
<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>
Step 3: fix the errors
The table lists the changes most projects meet. "Removed" means the 4.51.0 jar or package no longer has it (checked by inspecting the jars, importing the Python package and compiling or running the examples on this page); "changed" means the call exists but its signature or behaviour differs.
| Selenium 3 code | Selenium 4.51 | Notes |
|---|---|---|
|
|
Java. |
|
|
Java. |
|
|
Java. The |
|
|
Java. |
|
|
Java. The |
|
nothing |
Java. The pre-geckodriver Firefox driver (Firefox older than 48) was removed. |
|
|
Java. |
|
|
C#. |
|
|
Python. Better still, omit the path and let Selenium Manager find or download the driver. In 4.51.0 passing |
|
|
Python. The old keyword names raise a |
|
|
Python. |
|
|
Java. The old classes are not in the 4.51.0 jars. |
|
|
Ruby. |
Driver binary on the |
nothing; Selenium Manager (Rust, bundled in the bindings) resolves the driver and, if needed, the browser |
All bindings. Remove the manual driver code. See Selenium: Getting Started. |
Grid 3 hub at |
Grid 4 with Standalone, Hub and Nodes or fully distributed mode; both the root URL and |
See Selenium Grid. |
Chrome DevTools Protocol helpers ( |
WebDriver BiDi for the features it covers |
The CDP integration is labelled legacy in the documentation. See WebDriver BiDi. |
The Java test class below puts the changed calls in their Selenium 4 form and asserts their effect. All five tests passed with Selenium 4.51.0 (the comments show the Selenium 3 form):
package demo;
import static org.junit.jupiter.api.Assertions.*;
import java.time.Duration;
import java.util.NoSuchElementException;
import org.junit.jupiter.api.*;
import org.openqa.selenium.*;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.interactions.Actions;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.FluentWait;
import org.openqa.selenium.support.ui.Wait;
import org.openqa.selenium.support.ui.WebDriverWait;
/** The Selenium 3 -> 4 code changes, written the Selenium 4 way. */
class UpgradeTest {
static final String BASE = System.getProperty("BASE_URL", "http://localhost:3917");
WebDriver driver;
@BeforeEach void start() {
ChromeOptions o = new ChromeOptions();
o.addArguments("--headless=new");
driver = new ChromeDriver(o);
}
@AfterEach void stop() { driver.quit(); }
@Test void timeoutsTakeDurations() {
// 3.x: implicitlyWait(10, TimeUnit.SECONDS), setScriptTimeout(2, TimeUnit.MINUTES), pageLoadTimeout(10, TimeUnit.SECONDS)
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));
driver.manage().timeouts().scriptTimeout(Duration.ofMinutes(2));
driver.manage().timeouts().pageLoadTimeout(Duration.ofSeconds(10));
WebDriver.Timeouts t = driver.manage().timeouts();
assertEquals(Duration.ofSeconds(10), t.getImplicitWaitTimeout());
assertEquals(Duration.ofMinutes(2), t.getScriptTimeout());
assertEquals(Duration.ofSeconds(10), t.getPageLoadTimeout());
driver.manage().timeouts().implicitlyWait(Duration.ZERO);
}
@Test void waitsTakeDurations() {
driver.get(BASE + "/books");
// 3.x: new WebDriverWait(driver, 3)
WebElement search = new WebDriverWait(driver, Duration.ofSeconds(3))
.until(ExpectedConditions.elementToBeClickable(By.cssSelector("[data-testid=search-input]")));
assertTrue(search.isDisplayed());
// 3.x: withTimeout(30, TimeUnit.SECONDS).pollingEvery(5, TimeUnit.SECONDS)
Wait<WebDriver> wait = new FluentWait<>(driver)
.withTimeout(Duration.ofSeconds(30))
.pollingEvery(Duration.ofMillis(250))
.ignoring(NoSuchElementException.class);
assertNotNull(wait.until(d -> d.findElement(By.cssSelector("[data-testid=book-list]"))));
}
@Test void findersUseBy() {
driver.get(BASE + "/books");
// 3.x: driver.findElementByCssSelector(...), findElementsByClassName(...) (the FindsBy* interfaces) are gone
assertEquals(6, driver.findElements(By.cssSelector("[data-testid=book-card]")).size());
assertNotNull(driver.findElement(By.id("q")));
}
@Test void actionsNeedNoBuild() {
driver.get(BASE + "/books");
WebElement input = driver.findElement(By.cssSelector("[data-testid=search-input]"));
// 3.x needed .build().perform(); .perform() alone builds the sequence in 4.x
new Actions(driver).moveToElement(input).click().sendKeys("quiet").perform();
assertEquals("quiet", input.getAttribute("value"));
}
@Test void mergeReturnsANewObject() {
ChromeOptions options = new ChromeOptions();
options.addArguments("--headless=new");
MutableCapabilities extra = new MutableCapabilities();
extra.setCapability("myapp:tag", "smoke");
ChromeOptions merged = options.merge(extra); // 3.x mutated 'options'; 4.x returns the result
assertEquals("smoke", merged.getCapability("myapp:tag"));
assertNull(options.getCapability("myapp:tag"));
}
}
Python keeps its timeouts in seconds, so no change is needed there; the last test in the Python file shown above asserts
implicit_wait, page_load and script as (10, 10, 120). The exact results of the Java and Python runs were:
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.413 s -- in demo.LegacyPageFactoryTest
[INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.855 s -- in demo.UpgradeTest
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.912 s -- in demo.LegacyCapabilitiesTest
5 passed in 1.79s
Source: Apache Maven Surefire Plugin
Beyond the compiler: behaviour changes to check
-
Waits. Selenium 4 does not change the semantics of implicit and explicit waits, but mixing them still gives unpredictable delays; use explicit waits (see Waits & Expected Conditions). From 4.51 the Java
ExpectedConditionno longer implements Guava’sFunction, so code that composed conditions with Guava types needs to usejava.util.function.Functioninstead, and the Java client drops its Guava dependency. -
Window and tab handling.
driver.switchTo().newWindow(WindowType.TAB)creates and switches to a new tab or window; the older "open a link with a keyboard shortcut" workarounds can go. -
Locators. Relative locators (
RelativeLocator.with(By.tagName("input")).above(...)) were added in Selenium 4; they are a convenience, not a replacement for stable test ids. -
Timeouts of the commands. Selenium 4 has an HTTP client timeout (
ClientConfig) separate from the session timeouts. -
Grid URLs. If your suite points at a Selenium 3 Grid at
http://host:4444/wd/hub, it can keep that URL against a Grid 4; but the olddesiredCapabilitiesrequest format is rejected, as shown above. -
Cloud vendors. Move vendor settings under the vendor’s prefixed key (
sauce:options,bstack:options,LT:Options,cloud:optionsin the guide’s example) and read their current documentation: the keys differ.
What changed recently
-
4.51.0 (9 October 2026): Java client no longer depends on Guava;
ExpectedConditionstops implementing Guava’sFunction;RemoteWebDriver.getClientFactory()added; CDP versions 153 to 155 bundled. -
4.50.0: deprecated
HttpClientmethods removed in Java; relative locators from elements and shadow roots. -
September 2026: the Selenium IDE page was rewritten to describe a standalone application, and the downloads page states that the version 3 extension is unmaintained and no longer in the Chrome Web Store.
-
July 2024 / November 2024: last Selenium IDE release (
v4.0.1-beta.14) and lastselenium-side-runnerpublication (4.0.13). -
Selenium 4.0 (October 2021): W3C-only protocol, Options classes as the way to request a session,
Durationin Java timeouts and waits, Grid 4, relative locators, new window API,EventFiringDecorator. -
Selenium 4.6 (late 2022): Selenium Manager first shipped with the bindings, removing the need for manual driver handling (it is the default in 4.51.0).