View all services
Talk to QA Advisor
/Blog/Selenium Automation Testing (2027): How It Works Today, With a Real Python Run
Automation Testing8 min read

Selenium Automation Testing (2027): How It Works Today, With a Real Python Run

How Selenium automation testing works today, from one Python project run in Chrome and Firefox: automatic drivers, explicit waits, JavaScript errors caught with BiDi, and Grid.

Published January 20, 2023Last updated October 29, 2026
On this page

Selenium automation testing means writing code that drives a real browser the way a user would: open a page, type, click and check what appears. The idea has not changed since Selenium began. The machinery has. Selenium RC, the centre of older guides, is retired. Drivers now download themselves. And tests can listen to what happens inside the browser while they run.

This guide shows how Selenium works today using one small Python project that we ran on 29 October 2026 with Selenium 4.51.0, in Chrome 156 and Firefox 159. Every result quoted here comes from that run.

Figure 1 · What changed
The parts of Selenium you use today
The earlier version of this post centred on Selenium RC. RC is retired; two newer parts now do the heavy lifting.
Selenium RC
Retired
"Selenium 1 is not supported anymore."
WebDriver
Core
Drives the browser through the vendor's own automation support.
Selenium Manager
New in 4.6
Finds or downloads the right driver for the installed browser.
WebDriver BiDi
New
Streams console messages, JavaScript errors and network events.
Selenium Grid
Current
Runs the same tests on remote browsers and machines.
Selenium IDE
Current
A Chrome and Firefox extension that records tests.
Source: selenium.dev documentation (overview, Selenium Manager, BiDi, Grid, legacy Selenium RC), read 29 October 2026.

How Selenium automation works, in four parts

A Selenium test is a normal program. Your code calls the Selenium library, the library sends commands to a browser driver, and the driver controls the browser through support the browser vendor builds in. Selenium's overview puts it simply: WebDriver "uses browser automation APIs provided by browser vendors to control the browser and run tests. This is as if a real user is operating the browser."

Four parts matter for day-to-day work:

  • WebDriver, the API your tests call. It is a W3C standard, so the same commands work across Chrome, Firefox, Edge and Safari.
  • Selenium Manager, which finds the browser and the matching driver for you. It ships with Selenium "as of version 4.6", according to the Selenium Manager documentation.
  • WebDriver BiDi, a newer channel that lets the browser send events back to your test: console messages, JavaScript errors and network activity.
  • Selenium Grid, which runs the same tests on remote browsers so a suite can spread across machines.

Selenium IDE still exists as a Chrome and Firefox extension that records clicks into a test. It is a quick way to learn the commands, but most teams write tests in code for the reasons below: code can be reviewed, versioned and run in a pipeline.

Many teams start the move from manual testing by automating the checks they repeat on every release. Selenium suits that move because it works with the languages your developers already use.

Set up a Selenium project in Python

You need Python, a browser and two packages. We used Selenium 4.51.0 and pytest, run with uv:

uv run --with selenium==4.51.0 --with pytest python -m pytest -v test_selenium_basics.py

With pip, pip install selenium==4.51.0 pytest followed by python -m pytest -v test_selenium_basics.py does the same job.

There is no driver download step. That is the biggest practical change from older Selenium guides. When the test asks for Chrome, Selenium Manager checks which Chrome is installed and fetches the matching ChromeDriver into a local cache. We asked Selenium Manager directly what it had resolved on our machine:

chrome  driver_path: C:\Users\hello\.cache\selenium\chromedriver\win64\156.0.8078.73\chromedriver.exe
firefox driver_path: C:\Users\hello\.cache\selenium\geckodriver\win64\0.37.1\geckodriver.exe
selenium-manager 0.4.51

That matters more than it looks. Chrome updates itself, and a driver you downloaded by hand stops working when the browser moves on. Selenium's own documentation describes the failure: "session not created: This version of ChromeDriver only supports Chrome version 113".

Figure 2 · Driver drift
Chrome moved a version; the tests did not notice
A hand-downloaded ChromeDriver 155 would have stopped working with Chrome 156. Selenium Manager matched the driver to the browser.
28 October 2026
Chrome 155.0.8059.85
Our TestNG run for another post
29 October 2026
Chrome 156.0.8078.48
Chrome updated itself overnight
29 October 2026
ChromeDriver 156.0.8078.73
Chosen by Selenium Manager, no code change
Source: our run, Selenium 4.51.0, Python 3.14.7, Windows 11, 29 October 2026; selenium-manager 0.4.51 output; our 28 October run log.

The fixture: one fresh browser per test

Every test gets its own browser, in Chrome and then in Firefox, and closes it afterwards. This fixture also switches on BiDi with enable_bidi, which we use later:

"""Shared fixtures: one fresh browser per test, Chrome or Firefox, local or Grid (GRID_URL env var).
Selenium Manager resolves drivers; no driver downloads by hand."""
import os

import pytest
from selenium import webdriver


def make_options(name: str):
    opts = webdriver.ChromeOptions() if name == "chrome" else webdriver.FirefoxOptions()
    opts.add_argument("--headless=new" if name == "chrome" else "-headless")
    opts.enable_bidi = True
    return opts


@pytest.fixture(params=["chrome", "firefox"])
def driver(request):
    name = request.param
    grid = os.getenv("GRID_URL")
    if grid:
        d = webdriver.Remote(command_executor=grid, options=make_options(name))
    elif name == "chrome":
        d = webdriver.Chrome(options=make_options(name))
    else:
        d = webdriver.Firefox(options=make_options(name))
    yield d
    d.quit()

A fresh browser per test costs a few seconds each time, but it means no test can leave cookies or state behind for the next one. In our run, starting Chrome took 1.36 to 1.60 seconds once the driver was cached; the very first Chrome start took 6.02 seconds. Firefox starts took 5.51 to 5.67 seconds.

Running headless in a pipeline

The fixture starts both browsers headless: --headless=new for Chrome and -headless for Firefox. A headless browser renders the page in full but draws no window, which is what a build server without a screen needs. Every run in this guide was headless, so the same commands work unchanged in a CI job.

Keep one habit from desktop testing, though. When a headless test fails, save a screenshot with driver.save_screenshot("failure.png") before the browser closes. Without it you are debugging a page nobody saw.

Write and run your first tests

We tested against the-internet.herokuapp.com, a practice site built for automation. Four tests cover the jobs most suites start with: a login, a page that loads late, and an error the user never sees.

from selenium.common.exceptions import NoSuchElementException
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait

BASE = "https://the-internet.herokuapp.com"


def test_login_shows_secure_area(driver):
    driver.get(f"{BASE}/login")
    driver.find_element(By.ID, "username").send_keys("tomsmith")
    driver.find_element(By.ID, "password").send_keys("SuperSecretPassword!")
    driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()
    flash = WebDriverWait(driver, 10).until(EC.visibility_of_element_located((By.ID, "flash")))
    assert "You logged into a secure area!" in flash.text


def test_dynamic_text_without_a_wait_fails(driver):
    # The text is added to the page about five seconds after Start is clicked.
    driver.get(f"{BASE}/dynamic_loading/2")
    driver.find_element(By.CSS_SELECTOR, "#start button").click()
    try:
        driver.find_element(By.CSS_SELECTOR, "#finish h4")
        found = True
    except NoSuchElementException:
        found = False
    assert not found, "expected the element to be missing without a wait"


def test_dynamic_text_with_an_explicit_wait(driver):
    driver.get(f"{BASE}/dynamic_loading/2")
    driver.find_element(By.CSS_SELECTOR, "#start button").click()
    text = WebDriverWait(driver, 10).until(
        EC.visibility_of_element_located((By.CSS_SELECTOR, "#finish h4"))).text
    assert text == "Hello World!"


def test_javascript_errors_are_captured_with_bidi(driver):
    errors = []
    driver.script.add_javascript_error_handler(errors.append)
    driver.get(f"{BASE}/javascript_error")
    WebDriverWait(driver, 5).until(lambda d: errors)
    assert "Cannot read properties of undefined" in errors[0].text or "undefined" in errors[0].text
    print("\nJS error captured:", errors[0].text)

All eight runs passed, four tests in each browser, in 89.36 seconds in total.

The login test is the pattern you will repeat most: find elements by a stable locator such as an ID, act on them, then wait for the result before you assert. The wait matters even here, because the message appears after a page load.

Where the time went

Most of the 89.36 seconds was not spent testing. The two explicit-wait tests took 7.68 s and 8.07 s each, because the page itself takes about five seconds to show its text. Each Firefox start took over 5 s, against about 1.5 s for Chrome once its driver was cached. The login checks themselves took a few seconds each.

That pattern is typical of browser tests: start-up and waiting dominate. Keep browser tests for journeys only a browser can check, and push rules and calculations down to faster unit or API tests.

Why the explicit wait is not optional

The second and third tests use the same page, where the text "Hello World!" appears about five seconds after you click Start. The second test looks for it straight away and, by design, finds nothing: Selenium raised NoSuchElementException. The third test waits up to ten seconds for the text to become visible, and passes.

Figure 3 · Waits
Faster without a wait, and wrong
The text appears about five seconds after Start is clicked. Without a wait the lookup fails at once; with one, the test waits for the text.
No wait, Chrome
Element missing
2.72 s
No wait, Firefox
Element missing
3.55 s
Explicit wait, Chrome
Hello World!
7.68 s
Explicit wait, Firefox
Hello World!
8.07 s
Source: our run, Selenium 4.51.0, Python 3.14.7, Windows 11, 29 October 2026; test call times from pytest --durations.

The test without a wait was faster and wrong. In a real suite that becomes a flaky test: it passes on a fast machine and fails on a slow one. Use an explicit wait on the condition you need, such as visibility, rather than a fixed sleep. A sleep long enough for the slow day wastes time on every fast one.

Catching JavaScript errors with BiDi

The fourth test listens for JavaScript errors while the page loads, through WebDriver BiDi. Selenium's BiDi documentation describes BiDi as "the W3C standard bidirectional protocol for browser automation", and in Python you switch it on with options.enable_bidi = True, as our fixture does.

Both browsers reported the page's error. They worded it differently.

Figure 4 · BiDi capture
One page error, two different messages
BiDi caught the same JavaScript error in both browsers, worded differently. Assert on the error type, not the exact text.
Chrome 156
TypeError: Cannot read properties of undefined (reading 'xyz')
Firefox 159
TypeError: can't access property "xyz", document.propertyThatDoesNotExist is undefined
Source: our run, Selenium 4.51.0, Python 3.14.7, Windows 11, 29 October 2026; page the-internet.herokuapp.com/javascript_error.

That is why our assertion checks for "undefined" rather than one browser's full sentence. An exact-text assertion would pass in Chrome and fail in Firefox, and the failure would say nothing about your product. When a check runs across browsers, assert on what the browsers agree on.

Catching errors like this is useful well beyond one demo page. A test that fails when the page throws an uncaught error finds broken scripts that a user would only notice as a button that does nothing.

Run the same tests on Selenium Grid

The Grid documentation describes Grid as a way to execute WebDriver scripts "on remote machines by routing commands sent by the client to remote browser instances." You do not need a cluster to try it. The standalone mode runs the whole Grid in one Java process on your own machine, which is the quickest way to check that a suite is ready for remote browsers.

We started Grid 4.51.0 on Java 21 and pointed the same tests at it. The only change is the GRID_URL variable our fixture reads; the tests themselves did not change.

java -jar selenium-server-4.51.0.jar standalone --port 4444
GRID_URL=http://localhost:4444 uv run --with selenium==4.51.0 --with pytest python -m pytest -v test_selenium_basics.py

Grid reported "Selenium Grid ready." with Chrome, Firefox and Edge slots on our Windows machine. All eight tests passed again, in 48.38 seconds, including the BiDi error capture in both browsers.

That total was lower than the 89.36 seconds of our local run, mostly because browser start-up and shutdown were quicker in this run. One run each is not a benchmark, so do not read it as Grid being faster. Grid earns its place when it runs tests in parallel across several machines or browser versions, which a single local run cannot show.

A practical check before you move a suite to Grid: make sure nothing in your tests depends on the local machine, such as file paths, local downloads or a browser profile. Our fixture created every browser from options alone, which is why the same code ran in both places.

Choose locators that survive a redesign

Our tests use IDs wherever the page offers them, then short CSS selectors. That follows Selenium's advice on locators: if IDs are "available, unique, and consistently predictable, they are the preferred method", and otherwise "a well-written CSS selector is the preferred method". The same page warns that XPath selectors "tend to be quite slow".

Avoid locators that depend on styling or position, such as a class used for colour or the third button in a row. A designer can change both without touching behaviour, and your tests fail for no product reason. If the page has no stable hooks, ask the developers to add test IDs. It is a small change on their side, and it removes a whole class of repairs on yours.

Move page details into page objects as the suite grows

Once two tests touch the same page, the locators start to repeat. Selenium's documentation on page object models gives the reason to fix this early: "if the UI changes, the fix needs only to be applied in one place."

We moved our login test into a page object and added a wrong-password case. Both tests passed in Chrome and Firefox, 4 passed in 39.00 s:

from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait


class LoginPage:
    URL = "https://the-internet.herokuapp.com/login"
    USERNAME = (By.ID, "username")
    PASSWORD = (By.ID, "password")
    SUBMIT = (By.CSS_SELECTOR, "button[type='submit']")
    FLASH = (By.ID, "flash")

    def __init__(self, driver):
        self.driver = driver

    def open(self):
        self.driver.get(self.URL)
        return self

    def log_in_as(self, username, password):
        self.driver.find_element(*self.USERNAME).send_keys(username)
        self.driver.find_element(*self.PASSWORD).send_keys(password)
        self.driver.find_element(*self.SUBMIT).click()
        return WebDriverWait(self.driver, 10).until(EC.visibility_of_element_located(self.FLASH)).text


def test_valid_login(driver):
    assert "You logged into a secure area!" in LoginPage(driver).open().log_in_as("tomsmith", "SuperSecretPassword!")


def test_wrong_password(driver):
    assert "Your password is invalid!" in LoginPage(driver).open().log_in_as("tomsmith", "wrong")

The tests now read as behaviour: log in as this user, expect this message. If the login form changes, only the class changes.

Common Selenium errors and what the message tells you

Most failures in a new Selenium suite come from four errors. We triggered each one on purpose in Chrome 156 and Firefox 159, so you can recognise the real messages. The test file is in the same project, and all eight runs behaved as expected in 88.55 s.

Figure 5 · Errors
Four errors, four different fixes
The real Chrome messages from our run; the last one is quoted from the Selenium Manager documentation.
NoSuchElementException
The locator found nothing
no such element: Unable to locate element
Fix: Check the locator before adding a wait
TimeoutException
The wait ran out
(empty message in both browsers)
Fix: Say in the test what you were waiting for
StaleElementReferenceException
The page replaced the element
stale element not found in the current frame
Fix: Find the element again after the change
SessionNotCreatedException
Driver and browser versions differ
This version of ChromeDriver only supports Chrome version 113
Fix: Remove the pinned driver; let Selenium Manager choose
Source: our run, Selenium 4.51.0, Python 3.14.7, Windows 11, 29 October 2026 (test_selenium_errors.py, 8 passed); selenium.dev Selenium Manager page.

NoSuchElementException: the locator found nothing

We asked for an element with the ID user-name, where the page uses username. Chrome replied "no such element: Unable to locate element" and showed the locator it had actually used: a CSS selector, [id="user-name"]. That detail is useful. Selenium translates By.ID into CSS behind the scenes, so the message shows exactly what was searched for.

Before you add a wait, check the locator. A wait cannot find an element that is not there; it just fails more slowly.

TimeoutException: the condition never came true

On a page where the text exists but stays hidden, we waited 2 s for it to become visible. Both browsers raised TimeoutException, and in both the message was empty. The exception tells you that the wait ran out, not why. Name your waits in the test, for example with a helper that says what it was waiting for, so the failure report explains itself.

StaleElementReferenceException: the page replaced the element

We found a checkbox, clicked the page's Remove button, then clicked the checkbox we had stored. Chrome said "stale element reference: stale element not found in the current frame". Firefox was more specific: the element "is no longer connected to the DOM".

The fix is to find the element again after the page changes, rather than reusing the old reference. This version passed in both browsers:

def test_stale_element_fixed_by_finding_again(driver):
    driver.get(f"{BASE}/dynamic_controls")
    button = driver.find_element(By.CSS_SELECTOR, "#checkbox-example button")
    button.click()  # Remove
    WebDriverWait(driver, 10).until(EC.text_to_be_present_in_element((By.ID, "message"), "It's gone!"))
    driver.find_element(By.CSS_SELECTOR, "#checkbox-example button").click()  # Add, found again
    box = WebDriverWait(driver, 10).until(EC.element_to_be_clickable((By.ID, "checkbox")))
    box.click()
    assert box.is_selected()

SessionNotCreatedException: the driver and browser disagree

We did not hit this one, because Selenium Manager matched the drivers. It is the classic error of a pinned driver after a browser update, and the Selenium Manager documentation quotes it: "session not created: This version of ChromeDriver only supports Chrome version 113". If you see it, remove the hard-coded driver path and let Selenium Manager choose. Selenium's error troubleshooting page covers the rest, and Firefox's messages link straight to it.

Where Selenium fits, and where it does not

Selenium tests what a user does in a web browser: forms, navigation, sign-in, checkout and the scripts that run on the page. It suits checks you repeat on every release across several browsers, and teams that want tests in the language their developers use.

It is the wrong tool for some jobs. Selenium does not test native mobile apps; Appium, which builds on the WebDriver protocol, does that. It is a slow way to check business rules that an API test can reach without starting a browser. And it is no substitute for a person exploring a new feature.

The ISTQB Foundation Level syllabus (v4.0.1, section 6.2) lists the risks every automation tool carries. Two apply directly to Selenium suites:

  • "Using a test tool when manual testing is more appropriate."
  • "Using an open-source software which may be abandoned, meaning that no further updates are available, or its internal components may require quite frequent updates as a further development."

The second risk is the driver problem in figure 2. Browser drivers change with every browser release. Selenium Manager now absorbs that churn for you, which is one of the strongest practical reasons to stay on a current Selenium 4 release.

Selenium components at a glance

ComponentWhat it doesWhen you need it
WebDriverDrives the browser through the vendor's automation supportEvery Selenium test
Selenium ManagerResolves the browser and the matching driverAutomatically, unless you pin a driver
WebDriver BiDiStreams console messages, errors and network eventsWhen a test should react to what the browser reports
Selenium GridRuns tests on remote browsers and machinesParallel runs and wider browser coverage
Selenium IDERecords clicks into a test in Chrome or FirefoxLearning commands or quick prototypes

Where Selenium stands today

Selenium is not getting outdated. It is still released and still changing: we used version 4.51.0, BiDi support is being added across the project, and Selenium Manager removed the most common setup failure. Its official bindings cover Java, Python, C#, Ruby and JavaScript, so most teams can write tests in a language they already use.

Newer tools such as Playwright bundle their own browsers and auto-waiting, and for some teams that is the better fit. We compare the three main options in Selenium vs Playwright vs Cypress. Selenium's strengths are the W3C standard behind it, the widest language support and Grid for running at scale.

It is not hard to start. The project above is 71 lines, blank lines included, and the hardest lessons, waiting for conditions and asserting on what matters, apply to every automation tool.

From our work
Between two of our own Selenium runs, a day apart, Chrome updated itself from 155 to 156. The tests ran unchanged because Selenium Manager picked ChromeDriver 156 for us. On projects that still pin a downloaded driver, this is the moment a green suite turns red for no reason in the product.
We have no measured rate for how often this happens across client projects, so we state it as an observation: when a Selenium suite fails on a Monday morning with "session not created", check the browser version before you check the code.
Source: our run, Selenium 4.51.0, Python 3.14.7, Windows 11, 29 October 2026; our 28 October 2026 run with Chrome 155.0.8059.85.

Start small, then scale

A good first Selenium suite is small: a fresh browser per test, explicit waits, locators that do not change with styling, and assertions that hold in every browser you test. Add BiDi checks for errors early, because they catch failures no assertion was written for. Move to Grid once the suite is too slow on one machine.

If you want a team to build or rescue a Selenium suite for your product, QAble's engineers use Selenium for the automation testing process.

Frequently Asked Questions

What language is used in Selenium?

Selenium has official libraries for Java, Python, C#, Ruby and JavaScript, and its documentation also shows Kotlin examples. You write tests in the language your team already uses. The browser commands are the same in every language because they follow the W3C WebDriver standard.

Can we do Selenium with Python?

Yes. Install the selenium package, add a test runner such as pytest, and Selenium Manager fetches the right browser driver for you. We ran a Python suite with Selenium 4.51.0 in Chrome and Firefox: eight tests covering a login, an explicit wait and a JavaScript error capture all passed.

Do I need to download ChromeDriver for Selenium?

Not any more. Since Selenium 4.6, Selenium Manager finds the installed browser and downloads the matching driver into a local cache. In our run it picked ChromeDriver 156.0.8078.73 for Chrome 156 and GeckoDriver 0.37.1 for Firefox without any setup step.

Is Selenium difficult to learn?

The basics are quick to learn: open a page, find elements, act on them and check the result. The hard part is the same in every tool: waiting for the right condition instead of sleeping, choosing locators that survive design changes and writing assertions that hold across browsers.

Is Selenium getting outdated?

No. Selenium is still actively released, and recent versions added Selenium Manager for automatic driver setup and WebDriver BiDi for listening to browser events such as console messages and JavaScript errors. What is outdated is Selenium RC, which the project no longer supports.

Which is better, Playwright or Selenium?

It depends on the team. Playwright bundles its own browsers and waits automatically, which makes it quick to start. Selenium follows the W3C WebDriver standard, supports more languages and scales with Selenium Grid. Our Selenium vs Playwright vs Cypress comparison covers how to choose.

Free Assessment

Get a free QA audit for your project

Identify quality gaps before they become production bugs.

Get Free Audit

Ship software with confidence

Talk to a QA advisor and find out how QAble can help your team build quality in at every stage.

No sales pitch
Technical walkthrough
No lock-in commitment

Talk to QA Advisor

Direct access to QAble's QA specialists.

Response within 24 hours