Selenium: Locating & Interacting

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 WebDriver drives a real browser through the W3C WebDriver protocol: you ask the driver for a reference to a page element, then tell the browser to do something with it, or you send the browser itself a command such as "go back", "accept this dialog" or "print this page". This page covers both halves of that vocabulary: how to find things (locators, finders, relative locators, the shadow DOM) and how to act on them and on the browser (element interactions, select lists, the Actions API, navigation, alerts, cookies, frames, windows and tabs, the virtual authenticator and PDF printing). It was written against Selenium 4.51.0 (released 9 October 2026) and checked on 25 October 2026. The Java examples use Java 17 and JUnit 5.14.4; the Python examples use pytest with Selenium 4.51.0 and Python 3.13. All of them were run against the Bookshelf application with Chrome 155 driven headless through Selenium Manager, and a subset of the Python suite also ran against Firefox in a docker-selenium container (the differences are listed near the end). The C#, Ruby and JavaScript snippets show where the official documentation differs between languages; they were not run, because this work only executed the Java and Python bindings.

Waiting for things to be ready, which is what makes any of this reliable, is the subject of Waits & Expected Conditions; starting browsers and setting their options is covered in Drivers, Browsers & Options, and installing and the first script in Getting Started. How to organise the locators of a page into page objects is in Test Design & Page Objects. For the same ideas in the other tools, see Playwright: Locators, Actions & Auto-waiting and Cypress: Queries, Assertions & Custom Commands.

How the examples are set up

Most examples use Bookshelf’s real pages: the catalogue (/books), the sign-in form (/signin) and the account page. Bookshelf has no iframes, alerts, shadow DOM, file input or draggable elements, so for those features the tests load a tiny self-contained page from a data: URL. The helper that builds the URL is the only shared code; the pages themselves are shown next to each example.

// src/test/java/demo/BaseTest.java (excerpt)
static final String BASE_URL = System.getenv().getOrDefault("BASE_URL", "http://localhost:3913");
static final String READER_PASSWORD =
    System.getenv().getOrDefault("BOOKSHELF_READER_PASSWORD", "reader-local-only");

WebDriver driver;

@BeforeEach
void start() {
  ChromeOptions options = new ChromeOptions();
  options.addArguments("--headless=new");
  driver = new ChromeDriver(options);
}

@AfterEach
void stop() {
  driver.quit();
}

/** A self-contained page for features Bookshelf does not have (frames, alerts, shadow DOM...). */
static String dataUrl(String html) {
  return "data:text/html;charset=utf-8,"
      + URLEncoder.encode(html, StandardCharsets.UTF_8).replace("+", "%20");
}
# tests/conftest.py (excerpt)
BASE_URL = os.environ.get("BASE_URL", "http://localhost:3913")
READER_PASSWORD = os.environ.get("BOOKSHELF_READER_PASSWORD", "reader-local-only")


@pytest.fixture
def driver():
    options = webdriver.ChromeOptions()
    options.add_argument("--headless=new")
    d = webdriver.Chrome(options=options)
    yield d
    d.quit()


def data_url(html: str) -> str:
    """A self-contained page for features Bookshelf does not have (frames, alerts, shadow DOM...)."""
    return "data:text/html;charset=utf-8," + urllib.parse.quote(html)

The port is 3913 only because it was a private port for this work; point BASE_URL at your own server. Passwords come from BOOKSHELF_READER_PASSWORD, never from a test file. The Java tests live in four classes (LocatorsTest, ActionsTest, BrowserTest and a BiDi print test), the Python tests in three files; the suites finished like this:

# Java 17, JUnit 5.14.4, Selenium 4.51.0, Chrome 155 headless
mvn -B test
# [INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0 -- in demo.ActionsTest
# [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 -- in demo.BidiPrintTest
# [INFO] Tests run: 9, Failures: 0, Errors: 0, Skipped: 0 -- in demo.BrowserTest
# [INFO] Tests run: 9, Failures: 0, Errors: 0, Skipped: 0 -- in demo.LocatorsTest
# [INFO] Tests run: 23, Failures: 0, Errors: 0, Skipped: 0
# [INFO] BUILD SUCCESS

# Python 3.13, pytest 9.1.1, Selenium 4.51.0, Chrome 155 headless
python -m pytest tests/ -q
# .....................                                                    [100%]
# 21 passed in 25.24s

Locating elements

A locator is a way to identify one or more elements on the page; it is the argument you give to findElement / find_element. Selenium supports eight traditional strategies, all built on the By class (Java, C#, Ruby and JavaScript spell it By.id(...), By.Id(...), {id: ...} and By.id(...); Python uses By.ID constants):

Strategy Matches Java / Python

id

The element whose id attribute equals the value.

By.id("q") / By.ID, "q"

name

Elements whose name attribute equals the value.

By.name("q") / By.NAME, "q"

class name

Elements whose class attribute contains the value. A compound class name ("a b") is not permitted; use a CSS selector for that.

By.className("card") / By.CLASS_NAME, "card"

tag name

Elements with that tag.

By.tagName("h1") / By.TAG_NAME, "h1"

link text

Anchor elements whose visible text matches the value exactly.

By.linkText("Catalogue") / By.LINK_TEXT, "Catalogue"

partial link text

Anchor elements whose visible text contains the value; with several matches findElement returns the first.

By.partialLinkText("Sign") / By.PARTIAL_LINK_TEXT, "Sign"

CSS selector

Anything a CSS selector can express: attributes, structure, pseudo-classes.

By.cssSelector("[data-testid=book-author]") / By.CSS_SELECTOR, "[data-testid=book-author]"

XPath

A path through the document tree, absolute or relative; also the only strategy that can match on text of any element or climb to a parent.

By.xpath("//li[@data-testid='book-card']//h3") / By.XPATH, "//li[@data-testid='book-card']//h3"

The id, name, class name and tag name strategies are, in the WebDriver protocol, shortcuts that the driver turns into CSS selectors, so the CSS selector and XPath strategies are the two that carry real expressive power. All eight run against Bookshelf’s catalogue page like this:

@Test
void eightStrategies() {
  driver.get(BASE_URL + "/books?q=quiet");
  assertEquals("quiet", driver.findElement(By.id("q")).getDomProperty("value"));
  assertEquals("search", driver.findElement(By.name("q")).getDomAttribute("type"));
  assertEquals("1", driver.findElement(By.className("card")).getDomAttribute("data-book-id"));
  assertTrue(driver.findElement(By.linkText("Catalogue")).getDomProperty("href").endsWith("/books"));
  assertEquals("Sign in", driver.findElement(By.partialLinkText("Sign")).getText());
  assertEquals("Catalogue", driver.findElement(By.tagName("h1")).getText());
  assertEquals("The Quiet Compiler",
      driver.findElement(By.xpath("//li[@data-testid='book-card']//h3")).getText());
  assertEquals("Mara Velde", driver.findElement(By.cssSelector("[data-testid=book-author]")).getText());
}
def test_eight_strategies(driver, base_url):
    driver.get(f"{base_url}/books?q=quiet")
    assert driver.find_element(By.ID, "q").get_attribute("value") == "quiet"
    assert driver.find_element(By.NAME, "q").get_attribute("type") == "search"
    assert driver.find_element(By.CLASS_NAME, "card").get_attribute("data-book-id") == "1"
    assert driver.find_element(By.LINK_TEXT, "Catalogue").get_attribute("href").endswith("/books")
    assert driver.find_element(By.PARTIAL_LINK_TEXT, "Sign").text == "Sign in"
    assert driver.find_element(By.TAG_NAME, "h1").text == "Catalogue"
    assert driver.find_element(By.XPATH, "//li[@data-testid='book-card']//h3").text == "The Quiet Compiler"
    assert driver.find_element(By.CSS_SELECTOR, "[data-testid=book-author]").text == "Mara Velde"

Choosing a locator

Locators are the part of a Selenium suite that breaks most often, so choose them with the page’s contract in mind rather than its current markup. A practical order, which matches the guidance of the other tools:

  1. A dedicated test attribute, such as Bookshelf’s data-testid, through a CSS selector: [data-testid=search-submit]. Selenium has no built-in test-id locator (Playwright has getByTestId), so wrap the selector once in a helper or a page object.

  2. An id or name that is part of the page’s behaviour (form fields, anchors targeted by labels): stable, fast and unambiguous when unique. Ids are only unique by convention, so a page with duplicate ids returns the first match.

  3. Link text or partial link text for navigation links whose wording is part of the user-facing contract. They break on any copy change or translation, and only match a elements.

  4. CSS selectors on structure (.card > a) when no attribute exists. Prefer short selectors tied to meaningful ancestors over long chains copied from the browser’s developer tools.

  5. XPath when you need what CSS cannot do: matching by text (//button[normalize-space()='Search']), selecting a parent or ancestor, or position-based logic. Avoid absolute paths (/html/body/div[2]/...), which break on any layout change.

  6. Relative locators (below) when an element is hard to describe but easy to place next to one that is not.

Selenium has no role- or label-based locators like Playwright’s getByRole and getByLabel. The accessibility information is available after the fact (getAriaRole() and getAccessibleName(), in the element-information section), and the XPath-with-label pattern //label[text()='Username']/following-sibling::input is the usual workaround.

ByChained and ByAll (Java)

The Java support package has two composite locators. ByChained applies several locators in sequence, each searching inside the previous result, so one findElement replaces a parent lookup followed by a child lookup. ByAll returns elements that match any of the locators, in document order, so one findElements finds, for example, the username and password fields together:

import org.openqa.selenium.support.pagefactory.ByAll;
import org.openqa.selenium.support.pagefactory.ByChained;

@Test
void chainedAndAllLocators() {
  driver.get(BASE_URL + "/books");
  WebElement title = driver.findElement(new ByChained(By.cssSelector("[data-testid=book-list]"), By.tagName("h3")));
  assertFalse(title.getText().isEmpty());
  driver.get(BASE_URL + "/signin");
  List<WebElement> fields = driver.findElements(new ByAll(By.id("username"), By.id("password")));
  assertEquals(2, fields.size());
}

The Python bindings have no equivalent classes (a search in the 4.51.0 package finds none); chain find_element calls on the parent element, or use a combined CSS selector, instead.

Relative locators

Relative locators (called friendly locators in Selenium 3 documentation) describe an element by where it is relative to another one. Selenium asks the browser for the position and size of the candidate elements and picks those that satisfy the spatial relationship. They are useful when an element has no good attribute but its neighbour does:

Method (Java / Python) Meaning

above / above

The element’s bottom edge is above the reference element’s top edge.

below / below

The element is below the reference.

toLeftOf / to_left_of

The element is left of the reference.

toRightOf / to_right_of

The element is right of the reference.

near / near

The element is at most 50 pixels from the reference by default; Java accepts a different distance as a second argument.

The reference can be a locator or a previously located element; the starting point is the tag, or any other locator, that filters the candidates (with(By.tagName("input"))). Calls chain, which combines constraints (below the email field and right of the cancel button). They run on Bookshelf’s sign-in form:

import static org.openqa.selenium.support.locators.RelativeLocator.with;

@Test
void relativeLocators() {
  driver.get(BASE_URL + "/signin");
  WebElement above = driver.findElement(with(By.tagName("input")).above(By.id("password")));
  WebElement below = driver.findElement(with(By.tagName("input")).below(By.id("username")));
  WebElement near = driver.findElement(with(By.tagName("input")).near(By.cssSelector("label[for=username]")));
  assertEquals("username", above.getDomAttribute("id"));
  assertEquals("password", below.getDomAttribute("id"));
  assertEquals("username", near.getDomAttribute("id"));
  WebElement button = driver.findElement(with(By.tagName("button")).below(below).toLeftOf(By.id("theme-toggle")));
  assertEquals("Sign in", button.getText());
}
from selenium.webdriver.support.relative_locator import locate_with

def test_relative_locators(driver, base_url):
    driver.get(f"{base_url}/signin")
    above = driver.find_element(locate_with(By.TAG_NAME, "input").above({By.ID: "password"}))
    below = driver.find_element(locate_with(By.TAG_NAME, "input").below({By.ID: "username"}))
    near = driver.find_element(locate_with(By.TAG_NAME, "input").near({By.CSS_SELECTOR: "label[for=username]"}))
    assert above.get_attribute("id") == "username"
    assert below.get_attribute("id") == "password"
    assert near.get_attribute("id") == "username"
    # The button sits below the password field: chain two constraints
    button = driver.find_element(locate_with(By.TAG_NAME, "button").below(below).to_left_of({By.ID: "theme-toggle"}))
    assert button.text == "Sign in"

In Python the reference locator is a one-entry dictionary ({By.ID: "password"}), and the older with_tag_name(...) helper still works but emits a deprecation warning that points to locate_with. The same lookup in the other bindings (documented, not run here):

var emailLocator = RelativeBy.WithLocator(By.TagName("input")).Above(By.Id("password"));
driver.find_element({relative: {tag_name: 'input', above: {id: 'password'}}})
const { locateWith } = require('selenium-webdriver');
const emailLocator = locateWith(By.tagName('input')).above(By.id('password'));

Relative locators are computed from the rendered geometry, so they depend on viewport size and layout: a responsive design that stacks a row of buttons vertically at phone width changes what toLeftOf means. Use them as a fallback, check them at the viewport your tests actually run in, and prefer near only when the relative positions vary.

Finding elements

find_element and find_elements

findElement returns a reference to the first matching element in document order and throws NoSuchElementException when nothing matches. findElements returns a list of every match and an empty list when there are none, which makes it the idiomatic existence check. A reference is a handle to an element in the browser’s current document; if the page replaces that element (a re-render, a navigation) the handle becomes stale and any use throws StaleElementReferenceException, so look the element up again rather than caching it across page changes.

WebDriver, WebElement and ShadowRoot all implement the SearchContext interface, so a search can start from any of them. A lookup from an element searches only its descendants; use it to find a field inside the right card when the page repeats the same markup, instead of building one long locator:

@Test
void findersScopingAndActiveElement() {
  driver.get(BASE_URL + "/books");
  List<WebElement> cards = driver.findElements(By.cssSelector("[data-testid=book-card]"));
  assertEquals(6, cards.size());
  assertFalse(cards.get(0).findElement(By.tagName("h3")).getText().isEmpty());
  assertTrue(driver.findElements(By.id("does-not-exist")).isEmpty());
  assertThrows(NoSuchElementException.class, () -> driver.findElement(By.id("does-not-exist")));
  driver.findElement(By.id("q")).click();
  assertEquals("q", driver.switchTo().activeElement().getDomAttribute("id"));
}
def test_finders_scoping_and_active_element(driver, base_url):
    driver.get(f"{base_url}/books")
    cards = driver.find_elements(By.CSS_SELECTOR, "[data-testid=book-card]")
    assert len(cards) == 6
    titles = [c.find_element(By.TAG_NAME, "h3").text for c in cards]
    assert titles[0]
    assert driver.find_elements(By.ID, "does-not-exist") == []  # plural finders never raise
    driver.find_element(By.ID, "q").click()
    assert driver.switch_to.active_element.get_attribute("id") == "q"

The last two lines show get active element: switchTo().activeElement() / switch_to.active_element returns the element that currently has keyboard focus in the browsing context, which is how you check where focus landed after an interaction (see also Accessibility Checks in E2E Tests). Nested lookups cost two protocol round trips; when speed matters, a single CSS selector or XPath that expresses the same path is cheaper.

The shadow DOM

A shadow DOM is an encapsulated DOM tree hidden inside an element (a shadow host): ordinary locators on the document do not see into it. Selenium can reach an open shadow root with getShadowRoot() (Java) or shadow_root (Python), which returns a SearchContext you can search from. Inside a shadow root use CSS selectors: when tried with Python on Chrome 155, By.ID and CSS selectors worked, while an XPath and a tag-name lookup failed with invalid argument: invalid locator. A lookup stays inside that one root and does not cross into nested shadow roots (a selector for an element two roots down raised NoSuchElementException), so nested components are searched one root at a time. The examples use an open shadow root; closed roots were not tested.

@Test
void shadowDom() {
  driver.get(dataUrl("""
      <div id=host></div><script>
      const r = document.getElementById('host').attachShadow({mode: 'open'});
      r.innerHTML = '<button id=inner class=pill>Inside</button><input name=q>';
      </script>"""));
  SearchContext root = driver.findElement(By.id("host")).getShadowRoot();
  assertEquals("Inside", root.findElement(By.cssSelector("#inner")).getText());
  assertEquals(2, root.findElements(By.cssSelector("button, input")).size());
  // since 4.50 relative locators also work from a shadow root
  WebElement input = root.findElement(with(By.tagName("input")).near(By.id("inner")));
  assertEquals("q", input.getDomAttribute("name"));
}
def test_shadow_dom(driver):
    driver.get(data_url("""<div id=host></div><script>
      const r = document.getElementById('host').attachShadow({mode: 'open'});
      r.innerHTML = '<button id=inner class=pill>Inside</button><input name=q>';
    </script>"""))
    host = driver.find_element(By.ID, "host")
    root = host.shadow_root
    assert root.find_element(By.CSS_SELECTOR, "#inner").text == "Inside"
    assert len(root.find_elements(By.CSS_SELECTOR, "button, input")) == 2
    assert root.find_element(By.CSS_SELECTOR, "input").get_attribute("name") == "q"

The Java relative locator call at the end is a Selenium 4.50 feature. In Python 4.51.0 passing a relative locator to shadow_root.find_element raised TypeError: Object of type RelativeBy is not JSON serializable when tried, so the Python test uses a CSS selector there. The Selenium documentation added runnable shadow DOM examples for every binding in October 2026. Playwright pierces open shadow roots with its ordinary locators and Cypress needs includeShadowDom; Selenium needs the explicit hop shown here.

Interacting with elements

The high-level element commands are click, sendKeys / send_keys, clear and submit, plus the Select helper covered below. They are designed to emulate a user’s experience, and unlike the Actions API they try to make the action possible first:

  • If the element is outside the viewport, Selenium scrolls it into view (aligning the bottom of the element with the bottom of the viewport) before acting. If that fails, or the element is not displayed for another reason, the command throws.

  • Selenium decides whether an element is displayed by running a JavaScript atom that checks for things that would keep it from being visible (display: none, zero size, hidden by an ancestor and so on). The W3C specification mentions the concept but does not define it, which is why isDisplayed() is an approximation.

  • If the centre of the element is covered by another element, click throws ElementClickInterceptedException. A sticky header, a cookie banner or an overlay still animating away are the usual culprits; wait for the covering element to go (an explicit condition, see Waits & Expected Conditions) rather than forcing the click with JavaScript, which would hide the bug a real user would hit.

click clicks the centre of the element. sendKeys types the keys into a keyboard-interactable element, typically a text input or an element with contenteditable; otherwise it throws. The key constants (Keys.ENTER, Keys.TAB, Keys.CONTROL and so on) can be sent too. clear resets the content of an editable element. submit was a separate endpoint in Selenium 3; in Selenium 4 it works by running a script, so the documentation recommends clicking the page’s submit button instead. The test below exercises all four on a small page:

@Test
void informationAndInteractions() {
  driver.get(dataUrl("""
      <style>#lnk{color:rgb(0,0,255);font-size:20px}</style>
      <input id=t value=start><input id=c type=checkbox checked><input id=d disabled>
      <a id=lnk href="#x">Link text</a><div id=hidden style="display:none">hidden</div>
      <button id=b onclick="this.textContent='clicked'">Press</button>
      <form id=f onsubmit="document.title='submitted';return false"><input id=s name=s><input type=submit></form>"""));
  WebElement t = driver.findElement(By.id("t"));
  t.clear();
  t.sendKeys("typed");
  // ... information checks, shown in the next section ...
  driver.findElement(By.id("b")).click();
  assertEquals("clicked", driver.findElement(By.id("b")).getText());
  driver.findElement(By.id("s")).sendKeys("x");
  driver.findElement(By.id("s")).submit();
  assertEquals("submitted", driver.getTitle());
}

The Python test is the same page and steps: t.clear(), t.send_keys("typed"), .click(), .send_keys("x") and .submit().

Information about elements

You can query a lot about an element once you have it:

Java / Python Returns Notes

isDisplayed() / is_displayed()

boolean

Whether the element is displayed, by the approximation described above.

isEnabled() / is_enabled()

boolean

False for disabled form controls.

isSelected() / is_selected()

boolean

For checkboxes, radio buttons and option elements.

getTagName() / tag_name

string

Lower-case tag name.

getRect() / rect

x, y, width, height

Position from the top-left of the page and size of the element’s bounding box.

getCssValue(name) / value_of_css_property(name)

string

The computed style property.

getText() / text

string

The rendered text, as a user sees it: hidden text is excluded and whitespace is normalised.

getDomAttribute(name) / get_dom_attribute(name)

string or null

The value of the HTML attribute as written in the markup.

getDomProperty(name) / get_property(name)

object

The JavaScript property of the element: the live state.

getAttribute(name) / get_attribute(name)

string or null

The older helper: returns the property if there is one and otherwise the attribute, plus a few boolean-attribute conversions. It is what most older code uses.

getAriaRole() / aria_role

string

The computed WAI-ARIA role (for example link, button).

getAccessibleName() / accessible_name

string

The computed accessible name that assistive technology would announce.

getScreenshotAs(...) / screenshot(path)

file, bytes or base64

A screenshot of just this element.

The attribute versus property distinction is worth a test, because it explains most "my assertion on value is wrong" questions: after typing, the property changes but the markup’s attribute keeps the original text.

WebElement t = driver.findElement(By.id("t"));
t.clear();
t.sendKeys("typed");
assertEquals("typed", t.getDomProperty("value"));
assertEquals("start", t.getDomAttribute("value"));
assertEquals("typed", t.getAttribute("value"));
assertEquals("input", t.getTagName());
assertTrue(t.isEnabled() && t.isDisplayed());
assertTrue(driver.findElement(By.id("c")).isSelected());
assertFalse(driver.findElement(By.id("d")).isEnabled());
assertFalse(driver.findElement(By.id("hidden")).isDisplayed());
WebElement lnk = driver.findElement(By.id("lnk"));
assertEquals("rgba(0, 0, 255, 1)", lnk.getCssValue("color"));
assertEquals("20px", lnk.getCssValue("font-size"));
Rectangle rect = lnk.getRect();
assertTrue(rect.getWidth() > 0 && rect.getHeight() > 0);
assertEquals("link", lnk.getAriaRole());
assertEquals("Link text", lnk.getAccessibleName());
t = driver.find_element(By.ID, "t")
t.clear()
t.send_keys("typed")
assert t.get_property("value") == "typed"
assert t.get_dom_attribute("value") == "start"      # attribute stays, property changed
assert t.get_attribute("value") == "typed"
assert t.tag_name == "input" and t.is_enabled() and t.is_displayed()
assert driver.find_element(By.ID, "c").is_selected()
assert not driver.find_element(By.ID, "d").is_enabled()
assert not driver.find_element(By.ID, "hidden").is_displayed()
lnk = driver.find_element(By.ID, "lnk")
assert lnk.value_of_css_property("color") == "rgba(0, 0, 255, 1)"
assert lnk.value_of_css_property("font-size") == "20px"
rect = lnk.rect
assert rect["width"] > 0 and rect["height"] > 0
assert lnk.aria_role == "link" and lnk.accessible_name == "Link text"

The formatting of getCssValue is not the same in every browser. Chrome 155 returned rgba(0, 0, 255, 1) for a colour declared as rgb(0,0,255), while Firefox returned rgb(0, 0, 255); compare colours through the Color class (below), not as strings.

File upload

Selenium cannot drive the operating system’s file chooser dialog, so it offers a shortcut: when the element is an input type="file", send the absolute path of the file with sendKeys / send_keys and the browser behaves as if the user had picked it. No click is needed (clicking opens the dialog and blocks), and for several files on an input with multiple, in Java and most bindings, join the paths with a newline. The test checks the page’s own change handler saw the file:

@Test
void fileUpload(@TempDir Path tmp) throws Exception {
  Path file = Files.writeString(tmp.resolve("cover.txt"), "hello");
  driver.get(dataUrl("<input type=file id=up><p id=out></p><script>up.onchange=()=>out.textContent=up.files[0].name</script>"));
  driver.findElement(By.id("up")).sendKeys(file.toAbsolutePath().toString());
  assertEquals("cover.txt", driver.findElement(By.id("out")).getText());
}
def test_file_upload(driver, tmp_path):
    f = tmp_path / "cover.txt"
    f.write_text("hello")
    driver.get(data_url("<input type=file id=up><p id=out></p><script>up.onchange=()=>out.textContent=up.files[0].name</script>"))
    driver.find_element(By.ID, "up").send_keys(str(f))
    assert driver.find_element(By.ID, "out").text == "cover.txt"

When the browser runs on another machine (a Grid or a docker-selenium node), the path must exist on that machine. The remote drivers solve this with a file detector: the Python Remote driver uses a LocalFileDetector by default, which uploads the local file to the node first, and in Java you set it yourself with remoteDriver.setFileDetector(new LocalFileDetector()). In the Firefox container run, the Python upload test passed without extra code for that reason. See Selenium Grid. Downloads, the other direction, are not part of the WebDriver protocol and are better handled by the tool’s browser preferences or by asserting on the network response.

Select lists

select elements have special behaviour (options, a selected state, a multiple mode), and the support package wraps them in a Select class so tests say what they mean. It only works with the HTML select and option elements; a JavaScript drop-down built from div or li elements is driven with ordinary clicks. A standard list allows exactly one selected option; a list with the multiple attribute allows several, and only those can be deselected. Since Selenium 4.5, constructing a Select for a disabled select element throws.

Operation Java / Python

Create

new Select(element) / Select(element)

Select an option

selectByVisibleText, selectByValue, selectByIndex / select_by_visible_text, select_by_value, select_by_index

Deselect (multiple lists only)

deselectByVisibleText, deselectByValue, deselectByIndex, deselectAll / the same names in snake case

Inspect

getOptions, getAllSelectedOptions, getFirstSelectedOption, isMultiple / options, all_selected_options, first_selected_option, is_multiple

Options with the disabled attribute cannot be selected. Both examples run on a page with one single and one multiple select:

import org.openqa.selenium.support.ui.Select;

@Test
void selectLists() {
  driver.get(dataUrl("""
      <select id=one><option value=a>Apple<option value=b selected>Banana<option value=c disabled>Cherry</select>
      <select id=many multiple><option value=1>One<option value=2 selected>Two<option value=3>Three</select>"""));
  Select one = new Select(driver.findElement(By.id("one")));
  assertFalse(one.isMultiple());
  assertEquals("Banana", one.getFirstSelectedOption().getText());
  one.selectByVisibleText("Apple");
  one.selectByValue("b");
  one.selectByIndex(0);
  assertEquals("a", one.getAllSelectedOptions().get(0).getDomAttribute("value"));
  assertEquals(List.of("Apple", "Banana", "Cherry"), one.getOptions().stream().map(WebElement::getText).toList());
  assertThrows(UnsupportedOperationException.class, () -> one.deselectAll());
  Select many = new Select(driver.findElement(By.id("many")));
  many.selectByValue("3");
  assertEquals(List.of("Two", "Three"), many.getAllSelectedOptions().stream().map(WebElement::getText).toList());
  many.deselectByVisibleText("Two");
  many.deselectByIndex(2);
  assertTrue(many.getAllSelectedOptions().isEmpty());
  many.selectByValue("1");
  many.deselectAll();
  assertTrue(many.getAllSelectedOptions().isEmpty());
}
from selenium.webdriver.support.ui import Select

def test_select_lists(driver, base_url):
    driver.get(data_url("""<select id=one><option value=a>Apple<option value=b selected>Banana<option value=c disabled>Cherry</select>
      <select id=many multiple><option value=1>One<option value=2 selected>Two<option value=3>Three</select>"""))
    one = Select(driver.find_element(By.ID, "one"))
    assert not one.is_multiple and one.first_selected_option.text == "Banana"
    one.select_by_visible_text("Apple")
    one.select_by_value("b")
    one.select_by_index(0)
    assert one.all_selected_options[0].get_attribute("value") == "a"
    assert [o.text for o in one.options] == ["Apple", "Banana", "Cherry"]
    many = Select(driver.find_element(By.ID, "many"))
    many.select_by_value("3")
    assert [o.text for o in many.all_selected_options] == ["Two", "Three"]
    many.deselect_by_visible_text("Two")
    many.deselect_by_index(2)
    assert [o.text for o in many.all_selected_options] == []
    many.select_by_value("1"); many.deselect_all()
    assert many.all_selected_options == []

Calling deselectAll on a single-select list throws UnsupportedOperationException in Java, as the test asserts. In C# the class is SelectElement (new SelectElement(driver.FindElement(By.Id("one"))).SelectByText("Apple")) and in Ruby Selenium::WebDriver::Support::Select.new(element).select_by(:text, 'Apple'); the JavaScript bindings document no Select helper, so there you click the option element instead. These were documented, not run. Bookshelf’s own rating select is in the "add review" form (data-testid=review-rating-input), which needs a signed-in user; the same calls apply to it.

Colors

Colour definitions on the web are not constant: the same colour can be hex, rgb, rgba, hsl or hsla, and getCssValue returns whatever format the browser chooses (see the Chrome and Firefox difference above). The Color class, in the support package, parses any of the common formats into one object you can compare and convert. Java and Python provide it (the official page documents Java, Python and Kotlin).

import org.openqa.selenium.support.Color;
import org.openqa.selenium.support.Colors;

@Test
void colors() {
  assertEquals("rgba(47, 126, 216, 1)", Color.fromString("#2F7ED8").asRgba());
  assertEquals("#2f7ed8", Color.fromString("rgb(47, 126, 216)").asHex());
  assertTrue(Color.fromString("hsl(210, 69%, 52%)").asRgb().startsWith("rgb("));
  assertEquals("rgba(0, 0, 0, 0)", Color.fromString("transparent").asRgba());
  assertEquals(Colors.BLUE.getColorValue(), Color.fromString("rgba(0,0,255,1)"));
  assertEquals("#008000", Colors.GREEN.getColorValue().asHex());
  driver.get(dataUrl("<p id=p style='color:#2F7ED8;background:transparent'>x</p>"));
  WebElement p = driver.findElement(By.id("p"));
  assertEquals(Color.fromString("#2f7ed8"), Color.fromString(p.getCssValue("color")));
  assertEquals("rgba(0, 0, 0, 0)", Color.fromString(p.getCssValue("background-color")).asRgba());
}
from selenium.webdriver.support.color import Color

def test_colors(driver):
    assert Color.from_string("#2F7ED8").rgba == "rgba(47, 126, 216, 1)"
    assert Color.from_string("rgb(47, 126, 216)").hex == "#2f7ed8"
    assert Color.from_string("hsl(210, 69%, 52%)").rgb.startswith("rgb(")
    assert Color.from_string("transparent").rgba == "rgba(0, 0, 0, 0)"
    assert Color.from_string("rgba(0,0,255,1)") == Color.from_string("blue")
    assert Color.from_string("green").hex == "#008000"
    driver.get(data_url("<p id=p style='color:#2F7ED8;background:transparent'>x</p>"))
    p = driver.find_element(By.ID, "p")
    assert Color.from_string(p.value_of_css_property("color")) == Color.from_string("#2f7ed8")
    assert Color.from_string(p.value_of_css_property("background-color")).rgba == "rgba(0, 0, 0, 0)"

The class accepts hex (three or six digits), rgb and rgba (including percentages, such as rgb(40%, 20%, 40%)), hsl and hsla, and the named colours; "transparent", which browsers return for an unset background, becomes rgba(0, 0, 0, 0). asHex, asRgb and asRgba (hex, rgb, rgba in Python) convert the other way. Java additionally has a Colors enum of the named colours (Colors.GREEN.getColorValue()); the Python package has no such enum, so pass the name to Color.from_string.

The Actions API

The element commands above are high-level. The Actions API is the low-level interface: it sends virtualised device input to the browser, exactly as the W3C specification describes it. Selenium provides three kinds of input source: a key input for keyboards, a pointer input for a mouse, a pen or a touch device, and a wheel input for a scroll wheel (added in Selenium 4.2). You build a chain of actions assigned to those inputs and run them with perform(); the browser executes them in order. Each pointer movement and wheel scroll can take a duration, and pause adds a plain wait between actions.

You rarely need the raw building blocks: the Actions class (Java) and ActionChains (Python) combine them into convenience methods for the common gestures, as shown below. Two things to remember:

  • The driver remembers the state of every input device for the whole session. A key that was pressed and not released stays pressed, and the pointer stays wherever the last action left it, even if you create a new Actions object. There is a special method to release everything: ((RemoteWebDriver) driver).resetInputState() in Java, ActionChains(driver).reset_actions() in Python. In a suite with a fresh browser per test this rarely matters, but with a shared driver call it between tests.

  • Actions do not scroll for you, unlike click and sendKeys. Moving the pointer to an element that is outside the viewport fails, and the wheel input exists precisely to scroll first.

All the Actions examples share one fixture page: an input, a target div, a draggable box and a drop zone, with a log that records the events the browser delivered, and a tall div so the page can scroll.

<style>body{margin:0} #a{width:120px;height:60px;background:#cde}
#drag{width:50px;height:50px;background:#c66;position:absolute;left:10px;top:150px}
#drop{width:100px;height:100px;background:#6c6;position:absolute;left:200px;top:150px}
#tall{height:3000px}</style>
<input id=field><div id=a>target</div><div id=drag></div><div id=drop></div><p id=log></p><div id=tall></div>
<script>
const log=m=>document.getElementById('log').textContent+=m+' ';
a.addEventListener('click',()=>log('click'));a.addEventListener('dblclick',()=>log('dbl'));
a.addEventListener('contextmenu',e=>{e.preventDefault();log('ctx')});a.addEventListener('mouseover',()=>log('over'));
let down=false;drag.addEventListener('mousedown',()=>down=true);
document.addEventListener('mouseup',e=>{if(down){down=false;const r=drop.getBoundingClientRect();
 if(e.clientX>=r.left&&e.clientX<=r.right&&e.clientY>=r.top&&e.clientY<=r.bottom)log('dropped');}});
</script>

Keyboard actions

A keyboard can do exactly two things: press a key down and release a pressed key. Regular characters are keys; the non-character keys (Shift, Control, Enter, arrows) have unicode values that Selenium exposes as the Keys class (the constants differ by language, and the full list is in each binding’s API documentation). sendKeys in the Actions API is a convenience that combines keyDown and keyUp for typing several characters in the middle of other actions; given an element, it first moves to or focuses that element. The example types text, then holds Shift for a capital letter, then does select-all and cut with the platform’s shortcut key (Command on macOS, Control elsewhere):

@Test
void keyboard() {
  driver.get(dataUrl(PAGE));
  WebElement field = driver.findElement(By.id("field"));
  new Actions(driver).sendKeys(field, "selenium").keyDown(Keys.SHIFT).sendKeys("a").keyUp(Keys.SHIFT).perform();
  assertEquals("seleniumA", field.getDomProperty("value"));
  Keys cmd = System.getProperty("os.name").toLowerCase().contains("mac") ? Keys.COMMAND : Keys.CONTROL;
  new Actions(driver).keyDown(cmd).sendKeys("a").sendKeys("x").keyUp(cmd).perform(); // select all, cut
  assertEquals("", field.getDomProperty("value"));
}
from selenium.webdriver import ActionChains, Keys

def test_keyboard(driver):
    driver.get(data_url(PAGE))
    field = driver.find_element(By.ID, "field")
    ActionChains(driver).send_keys_to_element(field, "selenium").key_down(Keys.SHIFT).send_keys("a").key_up(Keys.SHIFT).perform()
    assert field.get_attribute("value") == "seleniumA"
    cmd = Keys.COMMAND if driver.capabilities["platformName"].lower().startswith("mac") else Keys.CONTROL
    ActionChains(driver).key_down(cmd).send_keys("a").key_down("x").key_up("x").key_up(cmd).perform()  # select all + cut
    assert field.get_attribute("value") == ""

Mouse actions

A mouse can do three things: press a button, release a pressed button and move. A mouse has five defined buttons: 0 left, 1 middle, 2 right, 3 back and 4 forward. The convenience methods combine the three primitives:

Java / Python What it does

click(element) / click(element)

Moves to the in-view centre of the element, then presses and releases the left button. With no argument it clicks at the current pointer location.

clickAndHold(element) / click_and_hold(element)

Moves to the centre and presses the left button without releasing.

release() / release()

Releases the left button wherever the pointer is.

contextClick(element) / context_click(element)

Moves to the centre and presses and releases the right button (button 2).

doubleClick(element) / double_click(element)

Moves to the centre and presses and releases the left button twice.

moveToElement(element) / move_to_element(element)

Moves to the in-view centre of the element (hover). The element must be in the viewport, or the command errors. Java also has moveToElement(element, x, y), and Python move_to_element_with_offset(element, x, y): offsets are measured from the element’s centre.

moveByOffset(x, y) / move_by_offset(x, y)

Moves the pointer from its current position (the top-left of the viewport if it has not moved yet). Positive x moves right and positive y moves down, so moveByOffset(30, -10) is 30 right and 10 up. The destination must be inside the viewport.

dragAndDrop(source, target) / drag_and_drop(source, target)

Click-and-hold on the source, move to the centre of the target, release.

dragAndDropBy(source, x, y) / drag_and_drop_by_offset(source, x, y)

Click-and-hold on the source, move by the given offset, release.

The pointer position does not change when the page is scrolled, which matters when mixing scrolls and offsets. The mouse example clicks, right-clicks and double-clicks the target, drags the box onto the drop zone, hovers, and moves by an offset:

@Test
void mouse() {
  driver.get(dataUrl(PAGE));
  WebElement a = driver.findElement(By.id("a"));
  new Actions(driver).click(a).contextClick(a).doubleClick(a).perform();
  assertTrue(log().contains("click") && log().contains("ctx") && log().contains("dbl"));
  new Actions(driver).dragAndDrop(driver.findElement(By.id("drag")), driver.findElement(By.id("drop"))).perform();
  assertTrue(log().contains("dropped"));
  new Actions(driver).moveToElement(a).pause(Duration.ofMillis(100)).moveByOffset(5, 5).perform();
  new Actions(driver).moveToElement(a, 10, 10).clickAndHold().release().perform();
}
def test_mouse(driver):
    driver.get(data_url(PAGE))
    a = driver.find_element(By.ID, "a")
    ActionChains(driver).click(a).context_click(a).double_click(a).perform()
    assert "click" in log(driver) and "ctx" in log(driver) and "dbl" in log(driver)
    drag, drop = driver.find_element(By.ID, "drag"), driver.find_element(By.ID, "drop")
    ActionChains(driver).drag_and_drop(drag, drop).perform()
    assert "dropped" in log(driver)
    ActionChains(driver).move_to_element(a).pause(0.1).move_by_offset(5, 5).perform()  # hover, then relative move
    ActionChains(driver).move_to_element_with_offset(a, 10, 10).click_and_hold().release().perform()

Drag and drop depends on how the page implements it: sites using the HTML5 draggable attribute and dragstart/drop events do not always respond to synthesised pointer events the same way in every browser. The example above uses mouse events, which is what the fixture listens for. If a real drag-and-drop widget does not react, check whether it needs intermediate moves (add an explicit moveByOffset between press and release) before reaching for JavaScript.

Pen actions

A pen is a pointer input with the same behaviour as a mouse plus event properties unique to a stylus (pressure, tilt, twist). Where a mouse has five buttons, a pen has three button states: 0 touch contact (the default, equivalent to a left click), 2 barrel button (equivalent to a right click) and 5 eraser (currently unsupported by the drivers). In Java you tell Actions to use a pen as the active pointer; in Python you build the sequence with an ActionBuilder whose pointer is a PointerInput of kind pen.

@Test
void penAndReleaseAll() {
  driver.get(dataUrl(PAGE));
  WebElement a = driver.findElement(By.id("a"));
  new Actions(driver)
      .setActivePointer(PointerInput.Kind.PEN, "default pen")
      .moveToElement(a)
      .clickAndHold()
      .moveByOffset(2, 2)
      .release()
      .perform();
  assertTrue(log().contains("over"));
  new Actions(driver).keyDown(Keys.SHIFT).perform();
  ((RemoteWebDriver) driver).resetInputState(); // release every depressed key and pointer button
}
from selenium.webdriver.common.actions.action_builder import ActionBuilder
from selenium.webdriver.common.actions.pointer_input import PointerInput

def test_pen_and_release(driver):
    driver.get(data_url(PAGE))
    a = driver.find_element(By.ID, "a")
    pen = PointerInput("pen", "default pen")
    builder = ActionBuilder(driver, mouse=pen)
    builder.pointer_action.move_to(a).pointer_down().move_by(2, 2).pointer_up()
    builder.perform()
    assert "over" in log(driver)
    ActionChains(driver).key_down(Keys.SHIFT).perform()
    ActionChains(driver).reset_actions()  # release every depressed key and pointer button

Both pen tests passed on Chrome 155. On Firefox in the docker-selenium image, the same Python pen test failed with Unimplemented pointerMove for pointerType pen: geckodriver does not implement the pen pointer type, so pen gestures are a Chromium-only feature for now.

Scroll wheel actions

Because Actions does not scroll by itself, the wheel input provides four ways to scroll:

  • Scroll to an element (scrollToElement / scroll_to_element): scrolls so that the bottom of the element is at the bottom of the viewport, whether the element was above or below. If the element is already inside the viewport nothing happens. The most common case.

  • Scroll by an amount (scrollByAmount(dx, dy) / scroll_by_amount(dx, dy)): positive values go right and down, negative left and up.

  • Scroll from an origin (scrollFromOrigin / scroll_from_origin), which takes a scroll origin and a delta. The origin is either the viewport (an offset from its upper-left corner) or an element (an offset from its centre). Use it to scroll only part of the page, for example a nested scrollable container or an iframe. If the element is out of view it is first scrolled to the bottom of the viewport, and if the resulting origin falls outside the viewport the command errors.

Scrolling is asynchronous in a browser with smooth scrolling, so assert on the final scroll position with a wait, not immediately:

@Test
void wheel() {
  driver.get(dataUrl(PAGE));
  WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(5));
  new Actions(driver).scrollByAmount(0, 500).perform();
  wait.until(d -> scrollY() == 500);
  new Actions(driver).scrollFromOrigin(ScrollOrigin.fromViewport(10, 10), 0, -200).perform();
  wait.until(d -> scrollY() == 300);
  new Actions(driver).scrollToElement(driver.findElement(By.id("tall"))).perform();
}
from selenium.webdriver.common.actions.wheel_input import ScrollOrigin

def scroll_y(driver):
    return driver.execute_script("return Math.round(window.scrollY)")

def test_wheel(driver):
    driver.get(data_url(PAGE))
    wait = WebDriverWait(driver, 5)
    ActionChains(driver).scroll_by_amount(0, 500).perform()
    wait.until(lambda d: scroll_y(d) == 500)
    ActionChains(driver).scroll_from_origin(ScrollOrigin.from_viewport(10, 10), 0, -200).perform()
    wait.until(lambda d: scroll_y(d) == 300)
    ActionChains(driver).scroll_to_element(driver.find_element(By.ID, "tall")).perform()

In Java, ScrollOrigin is the nested class WheelInput.ScrollOrigin and ScrollOrigin.fromElement(element) gives an element origin; the helper scrollY() in the Java test reads window.scrollY through executeScript. In C#, Ruby and JavaScript the same operations are available (for example actions.scroll_by(0, 500) in Ruby and driver.actions().scroll(...) in JavaScript); they were not run here.

Browser interactions

The remaining features act on the browser itself rather than on an element.

Title, URL and navigation

getTitle() / title and getCurrentUrl() / current_url read the title and the address bar. get(url) opens a page and, by default, waits for the page load strategy to finish (see Drivers, Browsers & Options for normal, eager and none). The history commands are navigate().to(url) (the same as get), navigate().back(), navigate().forward() and navigate().refresh() in Java, and driver.back(), driver.forward() and driver.refresh() in Python.

@Test
void navigation() {
  driver.get(BASE_URL + "/books");
  assertTrue(driver.getTitle().contains("Bookshelf"));
  driver.findElement(By.cssSelector("[data-testid=brand]")).click();
  assertEquals(BASE_URL + "/", driver.getCurrentUrl());
  driver.navigate().back();
  assertEquals(BASE_URL + "/books", driver.getCurrentUrl());
  driver.navigate().forward();
  assertEquals(BASE_URL + "/", driver.getCurrentUrl());
  driver.navigate().refresh();
  driver.navigate().to(BASE_URL + "/signin");
  assertTrue(driver.getCurrentUrl().endsWith("/signin"));
}
def test_navigation(driver, base_url):
    driver.get(f"{base_url}/books")
    assert "Bookshelf" in driver.title
    driver.find_element(By.CSS_SELECTOR, "[data-testid=brand]").click()
    assert driver.current_url == f"{base_url}/"
    driver.back()
    assert driver.current_url == f"{base_url}/books"
    driver.forward()
    assert driver.current_url == f"{base_url}/"
    driver.refresh()
    driver.execute_script("window.location.href = arguments[0]", f"{base_url}/books")
    WebDriverWait(driver, 5).until(EC.url_contains("/books"))

A click that triggers a navigation returns once the click is dispatched, and the next command may run against the old or the new page depending on timing; the Python example shows the safe idiom for navigation started by script: wait for the URL. Chains of navigations are the main source of stale element exceptions, so re-find elements after a page change.

Alerts, prompts and confirmations

WebDriver can handle the three kinds of native JavaScript popups. An alert shows a message and an OK button; a confirm adds a Cancel button; a prompt also has a text input. While one is open, the page is blocked, and any other WebDriver command fails with UnexpectedAlertOpenException unless the session’s unhandled-prompt behaviour says otherwise. Switch to it, read it and answer it:

Java / Python What it does

driver.switchTo().alert() / driver.switch_to.alert

Returns the open alert, or throws NoAlertPresentException when there is none.

getText() / text

The message.

accept(), dismiss() / accept(), dismiss()

Press OK or Cancel (and OK or close on an alert).

sendKeys(text) / send_keys(text)

Types into a prompt, replacing its default text. Cancelling does not submit text.

The popup appears asynchronously after the click, so wait for it with the alertIsPresent expected condition instead of switching immediately:

@Test
void alerts() {
  driver.get(dataUrl("""
      <button id=a onclick="alert('Hi')">alert</button>
      <button id=c onclick="document.title=confirm('Sure?')">confirm</button>
      <button id=p onclick="document.title=prompt('Name?','x')">prompt</button>"""));
  driver.findElement(By.id("a")).click();
  Alert alert = waitFor().until(ExpectedConditions.alertIsPresent());
  assertEquals("Hi", alert.getText());
  alert.accept();
  driver.findElement(By.id("c")).click();
  waitFor().until(ExpectedConditions.alertIsPresent()).dismiss();
  assertEquals("false", driver.getTitle());
  driver.findElement(By.id("p")).click();
  Alert prompt = waitFor().until(ExpectedConditions.alertIsPresent());
  prompt.sendKeys("Alberto");
  prompt.accept();
  assertEquals("Alberto", driver.getTitle());
  assertThrows(NoAlertPresentException.class, () -> driver.switchTo().alert());
}
def test_alerts(driver):
    driver.get(data_url("""<button id=a onclick="alert('Hi')">alert</button>
      <button id=c onclick="document.title=confirm('Sure?')">confirm</button>
      <button id=p onclick="document.title=prompt('Name?','x')">prompt</button>"""))
    driver.find_element(By.ID, "a").click()
    alert = WebDriverWait(driver, 5).until(EC.alert_is_present())
    assert alert.text == "Hi"
    alert.accept()
    driver.find_element(By.ID, "c").click()
    WebDriverWait(driver, 5).until(EC.alert_is_present()).dismiss()
    assert driver.title == "false"
    driver.find_element(By.ID, "p").click()
    prompt = WebDriverWait(driver, 5).until(EC.alert_is_present())
    prompt.send_keys("Alberto")
    prompt.accept()
    assert driver.title == "Alberto"
    with pytest.raises(NoAlertPresentException):
        driver.switch_to.alert.accept()

By default a session uses the W3C unhandled-prompt behaviour dismiss and notify: a popup you did not handle is dismissed and your next command throws. The unhandledPromptBehavior capability changes that (accept, dismiss, the and notify variants or ignore); with ignore the popup stays open until your test deals with it, which is what you want when a test has to assert the popup’s text. Recent drivers also dismiss beforeunload prompts automatically; to keep the old behaviour set unhandledPromptBehavior explicitly.

ChromeOptions options = new ChromeOptions();
options.addArguments("--headless=new");
options.setUnhandledPromptBehaviour(UnexpectedAlertBehaviour.IGNORE);
WebDriver d = new ChromeDriver(options);
options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
options.unhandled_prompt_behavior = "ignore"
d = webdriver.Chrome(options=options)

Both tests then click a button that opens an alert, wait for it, and dismiss it; in Python d.capabilities["unhandledPromptBehavior"] echoes the configured value.

Cookies

A cookie is a small piece of data a site stores in the browser to recognise the user. WebDriver can add, read and delete cookies of the current browsing context:

Java / Python What it does

manage().addCookie(cookie) / add_cookie(dict)

Adds a cookie. The cookie must be for the domain of the page you are on, so navigate to the site first. If the home page is slow, pick a small page of the same site (a 404 page is typical) to set cookies before the real test. Java builds one with new Cookie.Builder(name, value)...build(); Python takes a dictionary with name, value, path, domain, secure, httpOnly, expiry and sameSite.

manage().getCookieNamed(name) / get_cookie(name)

The serialised cookie with that name, or null.

manage().getCookies() / get_cookies()

Every cookie visible to the current page.

manage().deleteCookieNamed(name), deleteCookie(cookie) / delete_cookie(name)

Deletes one cookie.

manage().deleteAllCookies() / delete_all_cookies()

Deletes all cookies of the current browsing context.

The sameSite attribute (Strict or Lax) controls whether the cookie is sent with requests that originate from third-party sites, and is a defence against cross-site request forgery. It is supported by Chrome 80 and later and Firefox 79 and later with Selenium 4. HttpOnly cookies, such as Bookshelf’s bookshelf_session, are readable through WebDriver even though page JavaScript cannot see them. That makes cookies the cheapest way to reuse a login: sign in once through the UI (or the API), keep the session cookie, and add it to later browsers instead of repeating the form. This test does the whole cycle on Bookshelf:

@Test
void cookies() {
  driver.get(BASE_URL + "/signin");
  driver.manage().addCookie(new Cookie.Builder("theme-hint", "dark").sameSite("Strict").build());
  Cookie hint = driver.manage().getCookieNamed("theme-hint");
  assertEquals("dark", hint.getValue());
  assertEquals("Strict", hint.getSameSite());
  driver.findElement(By.id("username")).sendKeys("reader");
  driver.findElement(By.id("password")).sendKeys(READER_PASSWORD);
  driver.findElement(By.cssSelector("[data-testid=signin-submit]")).click();
  waitFor().until(ExpectedConditions.urlContains("/account"));
  Cookie session = driver.manage().getCookieNamed("bookshelf_session");
  assertTrue(session.isHttpOnly());
  assertEquals("Lax", session.getSameSite());
  Set<String> names = driver.manage().getCookies().stream().map(Cookie::getName).collect(Collectors.toSet());
  assertEquals(Set.of("theme-hint", "bookshelf_session"), names);
  driver.manage().deleteCookie(session);
  driver.get(BASE_URL + "/account");
  assertTrue(driver.getCurrentUrl().contains("/signin"));
  driver.manage().addCookie(new Cookie("bookshelf_session", session.getValue()));
  driver.get(BASE_URL + "/account");
  assertTrue(driver.getCurrentUrl().endsWith("/account"));
  driver.manage().deleteAllCookies();
  assertTrue(driver.manage().getCookies().isEmpty());
}
def test_cookies(driver, base_url):
    driver.get(f"{base_url}/signin")
    driver.add_cookie({"name": "theme-hint", "value": "dark", "sameSite": "Strict"})
    cookie = driver.get_cookie("theme-hint")
    assert cookie["value"] == "dark" and cookie["sameSite"] == "Strict" and cookie["path"] == "/"
    driver.find_element(By.ID, "username").send_keys("reader")
    driver.find_element(By.ID, "password").send_keys(READER_PASSWORD)
    driver.find_element(By.CSS_SELECTOR, "[data-testid=signin-submit]").click()
    WebDriverWait(driver, 5).until(EC.url_contains("/account"))
    session = driver.get_cookie("bookshelf_session")
    assert session["httpOnly"] is True and session["sameSite"] == "Lax"
    assert {c["name"] for c in driver.get_cookies()} == {"theme-hint", "bookshelf_session"}
    saved = session
    driver.delete_cookie("bookshelf_session")
    driver.get(f"{base_url}/account")
    assert "/signin" in driver.current_url          # anonymous again
    driver.add_cookie({"name": saved["name"], "value": saved["value"]})  # restore the session without the UI
    driver.get(f"{base_url}/account")
    assert driver.current_url.endswith("/account")
    driver.delete_all_cookies()
    assert driver.get_cookies() == []

Bookshelf’s POST /api/test/reset clears every server-side session, which invalidates a reused cookie, so use /api/test/seed between tests that share a login (see Example Application & Test Layout). The equivalent of this pattern in the other tools is Playwright’s storage state and Cypress cy.session().

Frames and iframes

Frames (a deprecated way of building layouts from several documents) and iframes (a document embedded from the same or another origin) are separate browsing contexts. Selenium only sees the elements of the top-level document until you switch into the frame, so looking for a button that lives in an iframe fails with NoSuchElementException even though you can see it. There are three ways to switch:

  • By element (the most flexible): find the iframe element with any locator and pass it to switchTo().frame(element).

  • By name or id: switchTo().frame("inner-name"); if the value is not unique, the first match wins.

  • By index: switchTo().frame(1), the position among the frames of the current document (zero-based).

To leave, switchTo().defaultContent() returns to the top-level document and switchTo().parentFrame() goes up one level. The tests use two iframes created from srcdoc attributes:

@Test
void frames() {
  driver.get(dataUrl("""
      <h1 id=top>Top</h1><iframe id=inner name=inner-name srcdoc="<button id=b onclick=&quot;this.textContent='clicked'&quot;>Inside</button>"></iframe>
      <iframe srcdoc="<p id=second>Second</p>"></iframe>"""));
  assertThrows(NoSuchElementException.class, () -> driver.findElement(By.id("b")));
  driver.switchTo().frame(driver.findElement(By.id("inner")));   // by element
  driver.findElement(By.id("b")).click();
  assertEquals("clicked", driver.findElement(By.id("b")).getText());
  driver.switchTo().defaultContent();
  driver.switchTo().frame("inner-name");                         // by name or id
  driver.switchTo().parentFrame();
  driver.switchTo().frame(1);                                    // by index
  assertEquals("Second", driver.findElement(By.id("second")).getText());
  driver.switchTo().defaultContent();
  assertEquals("Top", driver.findElement(By.id("top")).getText());
}
def test_frames(driver):
    driver.get(data_url(FRAMES))
    with pytest.raises(NoSuchElementException):
        driver.find_element(By.ID, "b")  # not in the top-level document
    driver.switch_to.frame(driver.find_element(By.ID, "inner"))   # by element
    driver.find_element(By.ID, "b").click()
    assert driver.find_element(By.ID, "b").text == "clicked"
    driver.switch_to.default_content()
    driver.switch_to.frame("inner-name")                          # by name or id
    driver.switch_to.parent_frame()
    driver.switch_to.frame(1)                                     # by index
    assert driver.find_element(By.ID, "second").text == "Second"
    driver.switch_to.default_content()
    assert driver.find_element(By.ID, "top").text == "Top"

The Python FRAMES constant is the same HTML as the Java text block. Playwright and Cypress cope with frames with frameLocator and cy.iframe-style plugins respectively; with Selenium the switch is explicit and stateful, so a helper that always returns to defaultContent() in a finally block avoids tests that fail only because a previous test left the driver inside a frame.

Windows and tabs

WebDriver makes no distinction between windows and tabs. If a site opens a new one, you work with it through its window handle, a string that identifies it for the session. getWindowHandle() returns the handle of the current one and getWindowHandles() the handles of all of them (in the order they were opened, though you should not depend on it for anything but the first). Opening a window with a link or window.open focuses it on screen, but WebDriver does not know which window the operating system considers active, so you must switch to it:

@Test
void windowsAndTabs() {
  driver.get(BASE_URL + "/books");
  String original = driver.getWindowHandle();
  assertEquals(1, driver.getWindowHandles().size());
  ((JavascriptExecutor) driver).executeScript("window.open(arguments[0], '_blank')", BASE_URL + "/signin");
  waitFor().until(ExpectedConditions.numberOfWindowsToBe(2));
  for (String handle : driver.getWindowHandles()) {
    if (!handle.equals(original)) {
      driver.switchTo().window(handle);
    }
  }
  waitFor().until(ExpectedConditions.urlContains("/signin"));
  driver.close();
  driver.switchTo().window(original);   // forgetting this raises NoSuchWindowException
  assertTrue(driver.getCurrentUrl().endsWith("/books"));
  driver.switchTo().newWindow(WindowType.TAB);   // created and focused in one call
  driver.get(BASE_URL + "/");
  assertEquals(2, driver.getWindowHandles().size());
  driver.close();
  driver.switchTo().window(original);
  driver.switchTo().newWindow(WindowType.WINDOW);
  driver.close();
  driver.switchTo().window(original);
}
def test_windows_and_tabs(driver, base_url):
    driver.get(f"{base_url}/books")
    original = driver.current_window_handle
    assert len(driver.window_handles) == 1
    driver.execute_script("window.open(arguments[0], '_blank')", f"{base_url}/signin")
    WebDriverWait(driver, 5).until(EC.number_of_windows_to_be(2))
    new = [h for h in driver.window_handles if h != original][0]
    driver.switch_to.window(new)
    WebDriverWait(driver, 5).until(EC.url_contains("/signin"))
    driver.close()
    driver.switch_to.window(original)          # forgetting this raises NoSuchWindowException
    assert driver.current_url.endswith("/books")
    driver.switch_to.new_window("tab")        # created and focused in one call
    driver.get(f"{base_url}/")
    assert len(driver.window_handles) == 2
    driver.close()
    driver.switch_to.window(original)
    driver.switch_to.new_window("window")
    driver.close()
    driver.switch_to.window(original)

close() closes the current window only and leaves the session on a window that no longer exists, so always switch to a valid handle afterwards. quit() ends the whole session: it closes every window, notifies a Grid that the browser is free and ends the driver process. Forgetting quit() leaves browser and driver processes and ports behind, which is why every example above calls it in a teardown (@AfterEach or a pytest fixture); outside a test framework use try/finally, and in Python the driver also works as a context manager (with webdriver.Chrome() as driver:). switchTo().newWindow(WindowType.TAB) (Selenium 4) creates a window or tab and switches to it in one call.

Window size, position and states

Screen size changes how a page lays out, so tests often set it. The window commands are under manage().window() in Java and directly on the driver in Python:

Java / Python What it does

getSize(), setSize(Dimension) / get_window_size(), set_window_size(w, h)

Reads or sets the window’s outer size.

getPosition(), setPosition(Point) / get_window_position(), set_window_position(x, y)

Reads or sets the position of the window’s top-left corner.

set_window_rect(x, y, width, height) / setSize plus setPosition

Restores the window if needed and sets position and size at once.

maximize() / maximize_window()

Enlarges the window to fill the screen without hiding the operating system’s menus.

minimize() / minimize_window()

Typically hides the window.

fullscreen() / fullscreen_window()

Fills the entire screen, like F11.

In headless Chrome the window is virtual, so set the size you want explicitly, or pass --window-size=1280,800 as an argument; rendering at a default of 800 by 600 can trigger a mobile layout. See Cross-browser Testing for viewports in general.

Screenshots and running JavaScript

Screenshots are part of the same interaction group. ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE) (Java) and driver.save_screenshot(path) (Python, also get_screenshot_as_file, get_screenshot_as_png and get_screenshot_as_base64) capture the visible viewport; every WebElement is also a TakesScreenshot, and Python’s element.screenshot(path) captures just that element. The executeScript(script, args...) / execute_script(script, *args) call runs JavaScript in the current context; elements passed as arguments arrive as DOM nodes and can be read back as arguments[0]. Treat it as an escape hatch for reading state and not as a way to click or type, since it bypasses the checks that make interactions realistic.

@Test
void windowManagementAndScreenshots(@TempDir Path tmp) throws Exception {
  driver.get(BASE_URL + "/");
  driver.manage().window().setSize(new Dimension(900, 700));
  assertEquals(900, driver.manage().window().getSize().getWidth());
  driver.manage().window().setPosition(new Point(10, 20));
  driver.manage().window().maximize();
  File shot = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
  Files.copy(shot.toPath(), tmp.resolve("page.png"));
  File brand = driver.findElement(By.cssSelector("[data-testid=brand]")).getScreenshotAs(OutputType.FILE);
  assertTrue(Files.size(tmp.resolve("page.png")) > 1000);
  assertTrue(brand.length() > 0 && brand.length() < shot.length());
  Object text = ((JavascriptExecutor) driver).executeScript("return arguments[0].innerText", driver.findElement(By.tagName("h1")));
  assertNotNull(text);
}
def test_window_management_and_screenshots(driver, base_url, tmp_path):
    driver.get(f"{base_url}/")
    driver.set_window_size(900, 700)
    assert driver.get_window_size()["width"] == 900
    driver.set_window_position(10, 20)
    driver.set_window_rect(x=0, y=0, width=1024, height=768)
    driver.maximize_window()
    page = tmp_path / "page.png"
    assert driver.save_screenshot(str(page)) and page.stat().st_size > 1000
    el = tmp_path / "brand.png"
    driver.find_element(By.CSS_SELECTOR, "[data-testid=brand]").screenshot(str(el))
    assert 0 < el.stat().st_size < page.stat().st_size
    assert driver.execute_script("return arguments[0].innerText", driver.find_element(By.TAG_NAME, "h1"))

For screenshots as test artefacts, comparing images and videos in CI, see Screenshots, Videos & Traces and Visual Regression Testing.

Printing a page to PDF

Selenium can ask the browser to print the current page and return the PDF. Configure the output with PrintOptions, then call print through the PrintsPage interface (Java: ((PrintsPage) driver).print(options) returning a Pdf) or driver.print_page(options) (Python). The result is a base64-encoded string, which you decode and write to a file. The Selenium documentation says Chromium browsers must be in headless mode for the command (the examples here ran headless Chrome 155), and the Python test also passed against Firefox in a container. The options mirror the print dialog and the W3C "Print" command:

Java / Python Default Meaning

setOrientation / orientation

portrait

PORTRAIT or LANDSCAPE.

setScale / scale

1.0

Scale factor from 0.1 to 2; 1.0 is 100 percent.

setBackground / background

false

Whether background colours and images are printed.

setShrinkToFit / shrink_to_fit

true

Shrink the page content to the page width.

setPageRanges / page_ranges

all pages

Pages to print, such as "2-4" (Java) or a list of strings (Python). C# has AddPageRangeToPrint and AddPageToPrint.

setPageSize / page_width, page_height

21.59 by 27.94 cm (US Letter)

Paper size in centimetres; Java also has named sizes.

setPageMargin / margin_top, margin_bottom, margin_left, margin_right

1 cm

Page margins in centimetres.

@Test
void printPage(@TempDir Path tmp) throws Exception {
  driver.get(BASE_URL + "/books");
  PrintOptions options = new PrintOptions();
  options.setOrientation(PrintOptions.Orientation.LANDSCAPE);
  options.setScale(0.8);
  options.setBackground(true);
  options.setShrinkToFit(true);
  options.setPageRanges("1");
  Pdf pdf = ((PrintsPage) driver).print(options);
  byte[] bytes = Base64.getDecoder().decode(pdf.getContent());
  Files.write(tmp.resolve("catalogue.pdf"), bytes);
  assertEquals("%PDF-", new String(bytes, 0, 5));
}
def test_print_page(driver, base_url, tmp_path):
    driver.get(f"{base_url}/books")
    options = PrintOptions()
    options.orientation = "landscape"
    options.scale = 0.8
    options.background = True
    options.shrink_to_fit = True
    options.page_ranges = ["1"]
    pdf = base64.b64decode(driver.print_page(options))
    out = tmp_path / "catalogue.pdf"
    out.write_bytes(pdf)
    assert pdf.startswith(b"%PDF-")
    assert len(re.findall(rb"/Type\s*/Page\b", pdf)) >= 1

Java has a second way, through the BiDi BrowsingContext class, which returns the base64 string directly. It needs BiDi enabled with the webSocketUrl capability (see WebDriver BiDi) and passed in this run too:

ChromeOptions options = new ChromeOptions();
options.addArguments("--headless=new");
options.setCapability("webSocketUrl", true);   // enable BiDi
WebDriver driver = new ChromeDriver(options);
BrowsingContext context = new BrowsingContext(driver, driver.getWindowHandle());
driver.get(BaseTest.BASE_URL + "/books");
String pdf = context.print(new PrintOptions());
assertEquals("%PDF-", new String(Base64.getDecoder().decode(pdf), 0, 5));

Virtual authenticator

Web Authentication (WebAuthn) lets users sign in without passwords using public-key credentials held by an authenticator, which can be a hardware key or software in the device. The virtual authenticator emulates one for testing, so a test can register and use a passkey without touching a real device or answering a biometric prompt. In Selenium you create it from VirtualAuthenticatorOptions and add it to the driver; credentials then live in that authenticator, and you can list, add and remove them. Support is a Chromium feature through the DevTools WebAuthn domain; Firefox and Safari do not currently document it. The properties you can set:

Option Meaning

protocol

CTAP2 or U2F. U2F authenticators cannot hold resident (discoverable) credentials.

transport

BLE, INTERNAL, NFC or USB, the transport the authenticator pretends to use.

hasResidentKey

Whether the authenticator supports resident (discoverable) credentials.

hasUserVerification

Whether it supports user verification such as a PIN or biometric.

isUserConsenting

Whether it simulates the user consenting to the operation.

isUserVerified

Whether user verification succeeds or fails; flip it with setUserVerified on the authenticator to test the failure path.

The authenticator exposes these methods: addCredential and addResidentCredential forms (Credential.createResidentCredential and createNonResidentCredential build credentials from an id, relying-party id, a PKCS#8-encoded private key, a user handle for resident ones and a signature counter), getCredentials, removeCredential(id), removeAllCredentials, setUserVerified(boolean), and finally removing the authenticator itself. The test registers a passkey on Bookshelf’s origin (localhost counts as a secure context, so WebAuthn is available), reads the credential back from the authenticator, removes it, adds it again, and uses it to authenticate:

@Test
void virtualAuthenticator() {
  driver.get(BASE_URL + "/");   // localhost is a secure context, so WebAuthn is available
  VirtualAuthenticatorOptions options = new VirtualAuthenticatorOptions()
      .setProtocol(VirtualAuthenticatorOptions.Protocol.CTAP2)
      .setHasResidentKey(true)
      .setHasUserVerification(true)
      .setIsUserVerified(true);
  VirtualAuthenticator authenticator = ((HasVirtualAuthenticator) driver).addVirtualAuthenticator(options);
  Object result = ((JavascriptExecutor) driver).executeAsyncScript("""
      const done = arguments[arguments.length - 1];
      navigator.credentials.create({publicKey: {
        rp: {name: 'Bookshelf'}, user: {id: new Uint8Array([1]), name: 'reader', displayName: 'Reader'},
        challenge: new Uint8Array([1, 2, 3]), pubKeyCredParams: [{type: 'public-key', alg: -7}],
        authenticatorSelection: {residentKey: 'required', userVerification: 'required'}}})
        .then(c => done(c.id), e => done('error: ' + e.name));""");
  assertFalse(result.toString().startsWith("error"));
  List<Credential> credentials = authenticator.getCredentials();
  assertEquals(1, credentials.size());
  assertTrue(credentials.get(0).isResidentCredential());
  Credential stored = credentials.get(0);
  authenticator.removeAllCredentials();
  assertTrue(authenticator.getCredentials().isEmpty());
  authenticator.addCredential(stored);   // a Credential read from the authenticator can be added back
  Object login = ((JavascriptExecutor) driver).executeAsyncScript("""
      const done = arguments[arguments.length - 1];
      navigator.credentials.get({publicKey: {challenge: new Uint8Array([4, 5, 6]), userVerification: 'required'}})
        .then(c => done('ok'), e => done('error: ' + e.name));""");
  assertEquals("ok", login);
  authenticator.setUserVerified(false);   // simulate a failed biometric check
  ((HasVirtualAuthenticator) driver).removeVirtualAuthenticator(authenticator);
}
from selenium.webdriver.common.virtual_authenticator import Credential, VirtualAuthenticatorOptions

def test_virtual_authenticator(driver, base_url):
    driver.get(f"{base_url}/")   # localhost is a secure context, so WebAuthn is available
    options = VirtualAuthenticatorOptions()
    options.protocol = VirtualAuthenticatorOptions.Protocol.CTAP2
    options.has_resident_key = True
    options.has_user_verification = True
    options.is_user_verified = True
    driver.add_virtual_authenticator(options)
    result = driver.execute_async_script("""
      const done = arguments[arguments.length - 1];
      navigator.credentials.create({publicKey: {
        rp: {name: 'Bookshelf'}, user: {id: new Uint8Array([1]), name: 'reader', displayName: 'Reader'},
        challenge: new Uint8Array([1, 2, 3]), pubKeyCredParams: [{type: 'public-key', alg: -7}],
        authenticatorSelection: {residentKey: 'required', userVerification: 'required'}}})
        .then(c => done(c.id), e => done('error: ' + e.name));""")
    assert not result.startswith("error")
    credentials = driver.get_credentials()
    assert len(credentials) == 1 and credentials[0].is_resident_credential
    stored = credentials[0]
    driver.remove_all_credentials()
    assert driver.get_credentials() == []
    driver.add_credential(stored)   # a Credential read from the authenticator can be added back
    assert driver.get_credentials()[0].id == stored.id
    login = driver.execute_async_script("""
      const done = arguments[arguments.length - 1];
      navigator.credentials.get({publicKey: {challenge: new Uint8Array([4, 5, 6]), userVerification: 'required'}})
        .then(c => done('ok'), e => done('error: ' + e.name));""")
    assert login == "ok"
    assert driver.get_credentials()[0].sign_count >= 1   # the authenticator signed the assertion
    driver.remove_virtual_authenticator()

In Python the authenticator is attached to the driver itself (add_virtual_authenticator, get_credentials, add_credential, remove_credential, remove_all_credentials, set_user_verified, remove_virtual_authenticator), while in Java the object returned by addVirtualAuthenticator carries the credential methods. Bookshelf has no passkey sign-in, so the tests call the browser’s WebAuthn API from the page, which is exactly what a real site’s JavaScript would do. The Selenium documentation also shows PKCS#8 RSA and EC private keys embedded in the tests to build credentials from scratch; reading one back from the authenticator, as above, avoids committing key material to a repository.

Cross-browser differences seen in this run

The whole Python suite was run a second time against Firefox in the selenium/standalone-firefox:4.51.0 container (Grid on a mapped host port, Bookshelf reached at host.docker.internal, the same driver fixture switched to webdriver.Remote with FirefoxOptions), excluding the two Chrome-only tests (virtual authenticator and the Chrome-specific prompt option). 17 tests passed and 2 failed, for reasons that were real behaviour differences rather than test mistakes:

Feature Chrome 155 Firefox (docker-selenium 4.51.0)

Pen pointer input

Works.

Unimplemented pointerMove for pointerType pen.

getCssValue("color") on a blue rgb(0,0,255)

rgba(0, 0, 255, 1)

rgb(0, 0, 255)

File upload through a Remote session

Local driver, no extra code.

Works: the Python Remote driver uploads the local file.

Virtual authenticator

Works.

Not exercised (a Chromium feature).

Local Firefox and Safari were not run on this host: Firefox does not launch from Selenium on this macOS version, and Safari needs the "Allow remote automation" setting changed in its developer settings, a system setting that was left alone. Treat any Firefox or Safari claim on this page as coming from the container run or the official documentation.

Pitfalls checklist

  • Stale elements. Any action that rebuilds the DOM invalidates references; re-find after navigation, filters, tab switches and Single Page Application re-renders.

  • Click intercepted. An overlay is covering the target; wait for it, do not click through with JavaScript.

  • Implicit waits and findElements. A list lookup that should be empty can burn the whole implicit wait; see Waits & Expected Conditions.

  • Frame state. After switchTo().frame(...) every lookup runs inside it until you leave; reset with defaultContent() in teardown.

  • Window state. After close() switch back; an unclosed extra window leaks into later tests unless quit() runs.

  • Prompt state. A popup you did not handle makes the next command fail; handle it or choose an unhandled-prompt behaviour.

  • Action state. Pressed keys and pointer position persist for the session; call resetInputState() / reset_actions() if a test may leave them dirty.

  • Colour and CSS strings. Normalise through Color before comparing, and never compare computed font or colour strings across browsers.

  • Headless differences. Headless Chrome (--headless=new) shares the real Chrome engine, so locators and interactions behave the same as headed mode, but window size, fonts and popup behaviour can differ; print-to-PDF and some viewport-dependent relative locators are the likeliest to change.

  • Parallel tests. Give each test its own driver; a WebDriver instance is not thread-safe, and the ThreadGuard wrapper (see Waits & Expected Conditions) makes misuse fail loudly.

What changed recently

  • Selenium 4.51 (9 October 2026): the Java client bindings dropped the Guava dependency, so ExpectedCondition no longer implements Guava’s Function; until(d -> ...) lambdas still work. The release also updates the CDP versions (v153 to v155) and adds the BiDi code generator.

  • Selenium 4.50: relative locators can start from an element or a shadow root (the Java example above), and the removed deprecated HTTP client methods may affect custom client factories.

  • October 2026 documentation: runnable shadow DOM finder examples for all bindings (8 October) and tested C# and Kotlin alert examples (20 October). The alert page notes that recent drivers dismiss beforeunload prompts automatically.

  • Selenium 4.5: creating a Select for a disabled select element throws.

  • Selenium 4.2: the wheel input and its scroll actions arrived in the Actions API.

  • Selenium 4.0 and the 4.x line: the W3C-only protocol, getDomAttribute / getDomProperty, switchTo().newWindow, the print command and the virtual authenticator replaced or extended Selenium 3 behaviour; Selenium 3 style capability objects and the JSON Wire Protocol commands are legacy (see Selenium IDE & Legacy).