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

v4.0.1-beta.14, published 20 July 2024 on GitHub (assets: Windows installer, macOS universal .dmg and .zip, Linux AppImage). There is no stable 4.0.1 or 4.1 release.

npm

selenium-ide and selenium-side-runner are both at 4.0.13 / 4.0.1-beta.14; the last publication of selenium-ide was in July 2024 and of selenium-side-runner in November 2024.

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 .side that holds the project name, base URL, tests, suites and the list of recorded URLs.

Recording, playback and the project structure

The workflow documented in the IDE’s Getting Started page is unchanged between the extension and the application:

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

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

  3. Organise: tests are grouped into suites; a "Default Suite" is created with the project and holds the first test.

  4. Save: saving writes the whole project to one .side file.

  5. Play back: select a test or suite and press play; the test runs in the window used for recording, or in a new one.

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

if, else if, else, end

Conditional branching. if and else if evaluate a JavaScript expression; else runs when none matched; end closes the block (a missing end is reported when you run the test).

times, end

Repeat the block a fixed number of times (the number goes in the target field).

do, repeat if

Run the block, then evaluate the expression in repeat if; loop while it is true.

while, end

Evaluate first, run the block while the expression is true.

forEach, end

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": []
}

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

-c, --capabilities [list]

WebDriver capabilities as space-separated key=value pairs, for example browserName=chrome.

The documented browserName='internet explorer' version='11.0' platform='Windows 8.1' uses pre-W3C names; use browserVersion and platformName and vendor prefixes (see Upgrading to Selenium 4).

-s, --server [url]

Remote server or Grid URL.

The documented /wd/hub path is a Selenium 3 Grid convention; a Selenium 4 Grid also accepts it, and the root URL works too.

--base-url [url]

Overrides the base URL stored in the project.

Matches the documentation.

-f, --filter [string]

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 -f control).

The documentation describes filtering test names only.

-w, --max-workers [number]

Number of parallel workers; defaults to the number of CPU cores.

Matches.

-r, --retries [number]

Retries failed tests N times (a thin wrapper over jest.retryTimes).

Not mentioned in the page.

-t, --timeout [number]

Maximum time in milliseconds to spend locating an element (default 15000).

Not mentioned.

-T, --jest-timeout [number]

Maximum time in milliseconds for one test to finish (default 60000).

Not mentioned.

-o, --output-directory [directory]

Writes the results as JSON to a file named after a timestamp.

The documented --output-format=junit (and the jest format option) is rejected: error: unknown option '--output-format=junit'. Only JSON output exists in 4.0.13.

-z, --screenshot-failure-directory [directory]

Saves a screenshot of each failed test.

Not mentioned.

-x, --proxy-type, -y, --proxy-options

Proxy configuration (direct, manual, pac, socks, system).

Present in the documentation under "Using a proxy server".

-j, --jest-options [list]

Passes options through to Jest.

Not mentioned.

-n, --config-file [filepath], -d, -D, -X, --force

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 (selenium-server-standalone-<version>.jar) that launched and killed browsers, acted as an HTTP proxy between the browser and the application and injected the Selenium Core JavaScript into the page. Client libraries sent Selenese commands to the server as HTTP GET/POST requests and the injected JavaScript executed them in the page. Not supported any more.

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 PageFactory and LoadableComponent.

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)

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"}}

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

@FindBy(how = How.NAME, using = "q") or @FindBy(css = "...")

Chooses the locator; id, name, className, css, tagName, linkText, partialLinkText and xpath are accepted.

@FindAll, @FindBys

Match any of several locators (@FindAll) or chain them (@FindBys).

@CacheLookup

Looks the element up once and keeps the reference; it goes stale after navigation or a re-render.

AjaxElementLocatorFactory(driver, timeoutInSeconds)

Retries each lookup until the element is present and displayed or the timeout passes, which gives fields a basic wait.

PageFactory.initElements(driver, SomePage.class)

Creates the page object itself, using a constructor that takes a WebDriver if there is one.

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"

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 org.seleniumhq.selenium:selenium-java (for example from 3.141.59 to 4.51.0) and run mvn clean compile. Gradle: change the selenium-java version and run ./gradlew clean build. Selenium 4 started on Java 8; later 4.x releases raised the floor, and the project’s current build targets a recent JDK, so check the requirements of the release you pick. The examples on this page ran on JDK 17.

Python

pip install selenium==4.51.0. The guide notes the minimum Python version rose in Selenium 4; the 4.51.0 package requires Python 3.10 or later.

C#

Update the Selenium.WebDriver NuGet package (Install-Package Selenium.WebDriver -Version 4.51.0, or dotnet add package Selenium.WebDriver). 4.51.0 targets net462, netstandard2.0 and net8.0.

Ruby

gem install selenium-webdriver, or gem 'selenium-webdriver', '~> 4.51.0' in the Gemfile. 4.51.0 supports MRI 3.3 or later.

JavaScript

npm install selenium-webdriver, or "selenium-webdriver": "^4.51.0" in package.json. 4.51.0 needs Node.js 22 or later.

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

driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS)

implicitlyWait(Duration.ofSeconds(10))

Java. pageLoadTimeout takes a Duration too, and setScriptTimeout(2, TimeUnit.MINUTES) became scriptTimeout(Duration.ofMinutes(2)). The long/TimeUnit overloads are removed.

new WebDriverWait(driver, 3)

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

Java. FluentWait.withTimeout and pollingEvery also take a Duration.

driver.findElementByCssSelector(".x"), findElementsByXPath(...)

driver.findElement(By.cssSelector(".x")), findElements(By.xpath(...))

Java. The FindsBy* interfaces were internal and are removed; use By.

options.merge(capabilities);

options = options.merge(capabilities);

Java. merge no longer mutates the caller; assign the result.

FirefoxOptions.setHeadless(true), ChromeOptions.setHeadless(true)

options.addArguments("--headless") or "--headless=new" for Chrome

Java. The setHeadless methods are gone from both Options classes in 4.51.0.

FirefoxOptions.setLegacy(true)

nothing

Java. The pre-geckodriver Firefox driver (Firefox older than 48) was removed.

BrowserType.FIREFOX

Browser.FIREFOX (org.openqa.selenium.remote.Browser)

Java. BrowserType is removed.

AddAdditionalCapability(name, value, true)

AddAdditionalOption(name, value)

C#.

webdriver.Chrome(executable_path=path, options=options)

webdriver.Chrome(service=Service(executable_path=path), options=options)

Python. Better still, omit the path and let Selenium Manager find or download the driver. In 4.51.0 passing executable_path raises a TypeError.

webdriver.Chrome(chrome_options=options), webdriver.Firefox(firefox_options=options)

webdriver.Chrome(options=options)

Python. The old keyword names raise a TypeError.

webdriver.Remote(url, desired_capabilities=caps)

webdriver.Remote(url, options=options)

Python. desired_capabilities raises a TypeError. DesiredCapabilities.CHROME remains only as a dictionary.

EventFiringWebDriver and WebDriverEventListener

EventFiringDecorator with a WebDriverListener

Java. The old classes are not in the 4.51.0 jars.

Selenium::WebDriver.for :remote, desired_capabilities: caps

Selenium::WebDriver.for :remote, capabilities: options

Ruby.

Driver binary on the PATH, System.setProperty("webdriver.chrome.driver", ...), WebDriverManager

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 /wd/hub, node registration with a JSON config

Grid 4 with Standalone, Hub and Nodes or fully distributed mode; both the root URL and /wd/hub accept sessions (both were run against the 4.51.0 container)

See Selenium Grid.

Chrome DevTools Protocol helpers (getDevTools(), versioned CDP packages)

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

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 ExpectedCondition no longer implements Guava’s Function, so code that composed conditions with Guava types needs to use java.util.function.Function instead, 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 old desiredCapabilities request format is rejected, as shown above.

  • Cloud vendors. Move vendor settings under the vendor’s prefixed key (sauce:options, bstack:options, LT:Options, cloud:options in 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; ExpectedCondition stops implementing Guava’s Function; RemoteWebDriver.getClientFactory() added; CDP versions 153 to 155 bundled.

  • 4.50.0: deprecated HttpClient methods 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 last selenium-side-runner publication (4.0.13).

  • Selenium 4.0 (October 2021): W3C-only protocol, Options classes as the way to request a session, Duration in 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).