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.
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.Remotesession 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?
No. Selenium still communicates with a browser-specific remote end; the local service startup and endpoint details are mostly hidden by the binding.
Why does webdriver.Remote require an options object in Selenium Python 4.47.0?
The browser Options object supplies the requested capabilities, including which browser the remote session should use.
Why can element IDs differ between local and remote runs against identical HTML?
They are opaque session-scoped remote handles, not stable DOM identifiers intended to be shared between sessions.
What should a command after quit prove in the Grid-backed run?
That the old session identity is no longer active; the remote end should reject commands for it rather than attach to a new browser.
Why bind Grid to 127.0.0.1 in this lab?
A Grid endpoint can control browsers and reach application/network resources. Loopback binding keeps the disposable training endpoint from being exposed externally.
Official references and version notes
- Selenium 4.47 release notes — current release baseline used by this chapter.
- Selenium downloads — language bindings and Selenium Server/Grid artifacts.
- W3C WebDriver — current standards-track definition of local/remote ends, capabilities, sessions, commands, errors, and element references.
- Browser options — current Selenium capability/options guidance and browser-specific configuration boundary.
-
Python Remote WebDriver API 4.47.0
—
command_executor, requiredoptions,session_id, and returnedcapabilities. - Getting started with Selenium Grid — Standalone mode and local RemoteWebDriver workflow.
-
Grid CLI options
— current
--host,--port,--selenium-manager,--reject-unsupported-caps, logging, and security-relevant options. - Common WebDriver errors — current Selenium descriptions for invalid session ID, session-not-created, and related failures.
- Finding web elements — Selenium element-reference behavior at binding level.
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.