Chapter 03Lesson 02~190 minutes

WebDriver Architecture, W3C Protocol, Sessions, and Capabilities: Guided Hands-On Workflow

Run the same small browser workflow twice: once through a local driver and once through a loopback Selenium Grid Standalone endpoint. Keep the test intent constant so the transport boundary becomes observable.

Local driverRemoteWebDriverGrid StandaloneCapabilitiesInvalid session

Learning objectives

  • Create a disposable loopback AUT and isolated Selenium Python environment.
  • Inspect the pre-session capability request generated by a browser Options object.
  • Create a direct local session and a webdriver.Remote session with equivalent browser intent.
  • Compare session IDs, returned capabilities, URLs, titles, and element-reference handles.
  • Demonstrate that commands after a remote quit() fail because the session identity has been deleted.
  • Preserve a small evidence packet without exposing Grid beyond loopback.

1. Preflight and compatibility contract

Use Selenium Python 4.47.0, Python 3.10+, one supported locally installed Chromium-family browser, and Java 11 or newer for Selenium Server. Place the official selenium-server-4.47.0.jar in the disposable lab directory from the Selenium downloads page. Do not fetch random third-party server JARs or drivers.

python --version
python -c "import selenium; print(selenium.__version__)"
java -version
# Confirm the server artifact you reviewed is present locally.
test -f selenium-server-4.47.0.jar && echo "server jar present"

The browser and driver versions are intentionally discovered from returned capabilities rather than hard-coded. If your environment cannot run Chrome/Chromium, adapt the binding code to another locally installed supported browser while preserving the same protocol observations.

2. Create the disposable AUT

Create fixture/index.html:

The following example makes the Create the disposable AUT behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.

<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><title>Selenium Chapter 03 Fixture</title></head>
<body>
  <main>
    <h1 id="status">Protocol fixture ready</h1>
    <button id="change" type="button">Change state</button>
    <p id="result" aria-live="polite">initial</p>
  </main>
  <script>
    document.querySelector('#change').addEventListener('click', () => {
      document.querySelector('#result').textContent = 'changed';
    });
  </script>
</body>
</html>

Create fixture_server.py:

The following example makes the Create the disposable AUT behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.

from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler
from functools import partial
from pathlib import Path

ROOT = Path(__file__).parent / "fixture"
server = ThreadingHTTPServer(("127.0.0.1", 8013), partial(SimpleHTTPRequestHandler, directory=str(ROOT)))
print("fixture=http://127.0.0.1:8013/")
try:
    server.serve_forever()
except KeyboardInterrupt:
    pass
finally:
    server.server_close()

Start it in a separate terminal with python fixture_server.py. The server binds only to 127.0.0.1:8013; the chapter never requires a public target.

3. Inspect requested capabilities before a session exists

The Options object is the supported binding abstraction. Its capability dictionary is useful evidence, but it is not the complete HTTP request envelope; Selenium later wraps it into the WebDriver New Session payload.

import json
from selenium import webdriver

options = webdriver.ChromeOptions()
options.page_load_strategy = "normal"
requested = options.to_capabilities()
print(json.dumps(requested, indent=2, sort_keys=True))

Expected shape includes browserName: chrome, pageLoadStrategy: normal, and a browser-specific goog:chromeOptions object. Record this as requested intent, not proof of the browser version that will run.

4. Create the direct local session

Save local_session.py:

The following example makes the Create the direct local session behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.

import json
from pathlib import Path
from selenium import webdriver
from selenium.webdriver.common.by import By

URL = "http://127.0.0.1:8013/"
EVIDENCE = Path("evidence")
EVIDENCE.mkdir(exist_ok=True)

def public_caps(caps: dict) -> dict:
    chrome = caps.get("chrome", {})
    return {
        "browserName": caps.get("browserName"),
        "browserVersion": caps.get("browserVersion"),
        "platformName": caps.get("platformName"),
        "pageLoadStrategy": caps.get("pageLoadStrategy"),
        "setWindowRect": caps.get("setWindowRect"),
        "driverVersion": chrome.get("chromedriverVersion", "not-reported"),
    }

options = webdriver.ChromeOptions()
options.page_load_strategy = "normal"
driver = None
try:
    driver = webdriver.Chrome(options=options)
    print("session_id=", driver.session_id)
    caps = public_caps(driver.capabilities)
    print(json.dumps(caps, indent=2))
    driver.get(URL)
    status = driver.find_element(By.ID, "status")
    print("element_id=", status.id)
    print("url=", driver.current_url)
    print("title=", driver.title)
    assert status.text == "Protocol fixture ready"
    (EVIDENCE / "local.json").write_text(json.dumps({"sessionId": driver.session_id, "capabilities": caps, "elementId": status.id}, indent=2), encoding="utf-8")
    driver.save_screenshot(str(EVIDENCE / "local.png"))
finally:
    if driver is not None:
        driver.quit()

This path still uses WebDriver protocol semantics even though you did not type a remote URL. The binding starts/contacts the local driver service for you.

5. Start a loopback Grid Standalone endpoint

Run Selenium Server in another terminal. The host is explicitly loopback-only. Selenium Manager is enabled for missing drivers, and unsupported capabilities are rejected immediately so the later negative test is deterministic.

java -jar selenium-server-4.47.0.jar standalone   --host 127.0.0.1   --port 4444   --selenium-manager true   --reject-unsupported-caps true   --log grid.log

Check health before creating a remote session:

curl --fail http://127.0.0.1:4444/status

Grid documentation warns that Grid must be protected from external access. This lab binds to loopback and does not add public firewall rules, Router credentials, or external DNS.

6. Create the equivalent RemoteWebDriver session

Save remote_session.py:

The following example makes the Create the equivalent RemoteWebDriver session behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.

import json
from pathlib import Path
from selenium import webdriver
from selenium.common.exceptions import InvalidSessionIdException
from selenium.webdriver.common.by import By

GRID = "http://127.0.0.1:4444"
URL = "http://127.0.0.1:8013/"
EVIDENCE = Path("evidence")
EVIDENCE.mkdir(exist_ok=True)

def public_caps(caps: dict) -> dict:
    chrome = caps.get("chrome", {})
    return {
        "browserName": caps.get("browserName"),
        "browserVersion": caps.get("browserVersion"),
        "platformName": caps.get("platformName"),
        "pageLoadStrategy": caps.get("pageLoadStrategy"),
        "setWindowRect": caps.get("setWindowRect"),
        "driverVersion": chrome.get("chromedriverVersion", "not-reported"),
    }

options = webdriver.ChromeOptions()
options.page_load_strategy = "normal"
driver = webdriver.Remote(command_executor=GRID, options=options)
session_id = driver.session_id
try:
    caps = public_caps(driver.capabilities)
    driver.get(URL)
    status = driver.find_element(By.ID, "status")
    print("session_id=", session_id)
    print("element_id=", status.id)
    print(json.dumps(caps, indent=2))
    assert driver.title == "Selenium Chapter 03 Fixture"
    (EVIDENCE / "remote.json").write_text(json.dumps({"sessionId": session_id, "capabilities": caps, "elementId": status.id}, indent=2), encoding="utf-8")
    driver.save_screenshot(str(EVIDENCE / "remote.png"))
finally:
    driver.quit()

try:
    _ = driver.title
except InvalidSessionIdException as exc:
    print("post_quit=", type(exc).__name__)
    (EVIDENCE / "post-quit.txt").write_text(f"{type(exc).__name__}: {str(exc).splitlines()[0]}\n", encoding="utf-8")

The important differences are transport and infrastructure ownership. The test still navigates, finds an element, reads title/state, and quits through WebDriver semantics. The remote session ID is known to Grid and the endpoint node, so logs can correlate it.

7. Compare the two evidence records

Do not expect byte-for-byte identical capability dictionaries. Compare the standard fields that matter to this chapter:

import json
from pathlib import Path

local = json.loads(Path("evidence/local.json").read_text(encoding="utf-8"))
remote = json.loads(Path("evidence/remote.json").read_text(encoding="utf-8"))

for key in ["browserName", "browserVersion", "platformName", "pageLoadStrategy"]:
    print(key, local["capabilities"].get(key), remote["capabilities"].get(key))

print("different_session_ids=", local["sessionId"] != remote["sessionId"])
print("local_element_handle=", local["elementId"])
print("remote_element_handle=", remote["elementId"])

The element IDs are opaque handles. They do not need to match across sessions even when both sessions point at identical HTML. Session identity is part of the meaning of an element reference.

8. Small challenge: identify the owner of each fact

For each item below, write whether it belongs primarily to the test runner/binding, Grid/intermediary, endpoint/browser session, or AUT:

  • Selenium Python version;
  • Grid URL and server log;
  • returned browserVersion;
  • current page title;
  • fixture text Protocol fixture ready;
  • session ID;
  • element-reference ID;
  • assertion pass/fail.

The goal is to classify state before debugging it.

9. Verification and cleanup

  • Local and remote runs each created a fresh session ID.
  • Both reached only 127.0.0.1:8013.
  • Both captured returned browser/platform capability evidence.
  • The remote post-quit command produced an invalid-session failure rather than silently reconnecting.
  • Grid listened on loopback only and wrote a local log.

Stop Grid and the fixture server with Ctrl+C. Remove only the disposable lab directory after preserving sanitized evidence. Do not clear global Selenium Manager caches as automatic cleanup.

Knowledge check

Does webdriver.Chrome() avoid the WebDriver protocol because it is local?

Why does webdriver.Remote require an options object in Selenium Python 4.47.0?

Why can element IDs differ between local and remote runs against identical HTML?

What should a command after quit prove in the Grid-backed run?

Why bind Grid to 127.0.0.1 in this lab?

Next lesson

Turn protocol facts into design choices

Lesson 3 decides how much capability configuration and protocol visibility a maintainable test platform actually needs.

Official references and version notes

Version and compatibility note

Version-sensitive behavior was rechecked against current Selenium and W3C primary documentation on 2026-08-27. The mandatory path pins Selenium Python 4.47.0, uses Python 3.10+, one installed supported Chromium-family browser, Java 11+ for the local Selenium Server/Grid Standalone exercise, Selenium Server 4.47.0, and loopback-only endpoints at 127.0.0.1. The Grid lab starts Standalone with --selenium-manager true and --reject-unsupported-caps true so driver discovery can remain automatic and an intentionally unavailable capability is rejected deterministically. WebDriver BiDi is explained only as a distinct bidirectional protocol boundary here; Chapter 18 teaches its evolving APIs in depth.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.