Checkpoint Lab — WebDriver Architecture, W3C Protocol, Sessions, and Capabilities
Build a protocol evidence packet around two equivalent browser sessions—direct local and Grid-backed—then force one controlled capability-negotiation failure and explain why no browser test should run after that failure.
Learning objectives
- Build a disposable Chapter 03 project with pinned Selenium Python and a loopback AUT.
- Run equivalent local and RemoteWebDriver sessions with the same browser intent.
- Compare requested capabilities, returned capabilities, session IDs, element handles, and transport ownership.
- Start Selenium Server 4.47.0 Standalone on loopback with deterministic unsupported-capability rejection.
- Request an unavailable platform capability and classify the resulting session-negotiation failure.
- Produce a sanitized runbook/evidence packet and clean up only disposable resources.
1. Checkpoint acceptance contract
The checkpoint is complete when the evidence shows:
-
Selenium Python exactly
4.47.0and Python 3.10+; -
Java 11+ and reviewed
selenium-server-4.47.0.jar; - a fixture reachable only at
127.0.0.1:8013; -
a Grid Standalone endpoint bound only to
127.0.0.1:4444; - one successful local session and one successful remote session;
- requested and returned capability records for both paths;
- different session IDs and opaque element handles;
- one unsupported capability request that fails before AUT interaction;
- a diagnosis that names capability matching/session creation as the failing layer;
- clean teardown, sanitized evidence, and no production target or credential.
2. Setup and preflight
PowerShell
The following example makes the Setup and preflight behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
$LAB = Join-Path $PWD "selenium-ch03-checkpoint"
Remove-Item -Recurse -Force $LAB -ErrorAction SilentlyContinue
New-Item -ItemType Directory $LAB | Out-Null
Set-Location $LAB
py -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install selenium==4.47.0
python -c "import sys,selenium; assert sys.version_info >= (3,10); assert selenium.__version__ == '4.47.0'; print(sys.version); print(selenium.__version__)"
java -version
POSIX shell
The following example makes the Setup and preflight behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
LAB="$PWD/selenium-ch03-checkpoint"
rm -rf "$LAB"
mkdir -p "$LAB" && cd "$LAB"
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install selenium==4.47.0
python -c "import sys,selenium; assert sys.version_info >= (3,10); assert selenium.__version__ == '4.47.0'; print(sys.version); print(selenium.__version__)"
java -version
Copy the reviewed official
selenium-server-4.47.0.jar into this disposable
directory. The checkpoint does not change system browser profiles,
Grid firewall rules, DNS, or production CI.
3. Create the loopback fixture
Create fixture/index.html:
The following example makes the Create the loopback fixture 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 loopback fixture 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 Terminal A with python fixture_server.py.
4. Start deterministic local Grid Standalone
Start Terminal B:
The following example makes the Start deterministic local Grid Standalone behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
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
Verify health:
The following example makes the Start deterministic local Grid Standalone behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
curl --fail http://127.0.0.1:4444/status
Prediction 1: before any remote session, Grid is healthy but has no session ID for your test. Prediction 2: a valid Chrome request will create a Grid-tracked session; an intentionally impossible platform request will not create one.
5. Create the checkpoint runner
Save checkpoint.py:
The following example makes the Create the checkpoint runner 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 __version__ as selenium_version
from selenium import webdriver
from selenium.common.exceptions import SessionNotCreatedException, WebDriverException
from selenium.webdriver.common.by import By
URL = "http://127.0.0.1:8013/"
GRID = "http://127.0.0.1:4444"
E = Path("evidence")
E.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"),
}
def requested_options():
options = webdriver.ChromeOptions()
options.page_load_strategy = "normal"
return options
def exercise(driver, label):
requested = requested_options().to_capabilities()
driver.get(URL)
status = driver.find_element(By.ID, "status")
assert status.text == "Protocol fixture ready"
driver.find_element(By.ID, "change").click()
result = driver.find_element(By.ID, "result")
assert result.text == "changed"
record = {
"label": label,
"selenium": selenium_version,
"requested": requested,
"sessionId": driver.session_id,
"returned": public_caps(driver.capabilities),
"elementId": status.id,
"url": driver.current_url,
"title": driver.title,
}
(E / f"{label}.json").write_text(json.dumps(record, indent=2, sort_keys=True), encoding="utf-8")
driver.save_screenshot(str(E / f"{label}.png"))
return record
local = None
remote = None
try:
local = webdriver.Chrome(options=requested_options())
local_record = exercise(local, "local")
finally:
if local is not None:
local.quit()
try:
remote = webdriver.Remote(command_executor=GRID, options=requested_options())
remote_record = exercise(remote, "remote")
finally:
if remote is not None:
remote.quit()
print("local_session=", local_record["sessionId"])
print("remote_session=", remote_record["sessionId"])
print("different_sessions=", local_record["sessionId"] != remote_record["sessionId"])
bad = webdriver.ChromeOptions()
bad.set_capability("platformName", "definitely-unavailable-academy-platform")
try:
webdriver.Remote(command_executor=GRID, options=bad)
raise RuntimeError("negative test unexpectedly created a session")
except (SessionNotCreatedException, WebDriverException) as exc:
summary = f"{type(exc).__name__}: {str(exc).splitlines()[0]}"
print("negative=", summary)
(E / "unsupported-capability.txt").write_text(summary + "\n", encoding="utf-8")
The negative request is syntactically valid:
platformName is a standard string capability. It is
intentionally impossible for this local Grid to match. Because the
Grid started with --reject-unsupported-caps true, the
failure should be immediate rather than waiting in the session
queue.
6. Compare requested and returned state
Create compare.py:
The following example makes the Compare requested and returned state 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
local = json.loads(Path("evidence/local.json").read_text(encoding="utf-8"))
remote = json.loads(Path("evidence/remote.json").read_text(encoding="utf-8"))
print("session_ids_differ=", local["sessionId"] != remote["sessionId"])
for key in ["browserName", "browserVersion", "platformName", "pageLoadStrategy"]:
print(key, "local=", local["returned"].get(key), "remote=", remote["returned"].get(key))
print("local_element_id=", local["elementId"])
print("remote_element_id=", remote["elementId"])
print("same_test_url=", local["url"] == remote["url"])
print("same_title=", local["title"] == remote["title"])
Explain differences rather than forcing equality. The acceptance condition is equivalent browser intent and AUT outcome, with explicit evidence of which remote end actually executed each session.
7. Write the negative-test diagnosis
Your runbook entry should state:
The following example makes the Write the negative-test diagnosis behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
Observed
- New RemoteWebDriver request included platformName=definitely-unavailable-academy-platform.
- Grid was healthy on 127.0.0.1:4444.
- No usable session ID was returned for the negative request.
- AUT navigation did not begin.
Interpretation
- Request syntax was valid, but the Grid had no matching environment.
- --reject-unsupported-caps true made the Distributor reject the request immediately.
- This is a capability matching / new-session failure, not a locator, wait, or assertion failure.
Correction
- Request an available platform/browser pair, or provision a node stereotype that legitimately satisfies the requirement.
- Do not hide the failure with retries, longer WebDriver command timeouts, or relaxed assertions.
8. Evidence packet
-
local.jsonandremote.jsonwith requested/returned capabilities and session IDs; -
local.pngandremote.pngshowing only the synthetic loopback fixture; -
unsupported-capability.txtwith the first line of the negative failure; -
grid.logretained locally and sanitized before sharing; - a short environment note containing Python, Selenium, Java, browser, driver, and Selenium Server versions.
Do not publish full capability dictionaries without reviewing them. Vendor fields may contain local paths, debugger addresses, or infrastructure details.
9. Verification checklist
- Exactly one local and one remote successful session were created and both were quit.
- Both navigated only to
127.0.0.1:8013. -
Both produced the same intended UI outcome:
changed. - The returned browser/platform evidence is present for both.
- The unsupported platform request created no usable session and never touched the AUT.
- Grid remained bound to loopback and was never exposed publicly.
- No real credential, personal profile, proxy secret, production URL, or PII appears in evidence.
10. Cleanup and rollback
Stop Grid and the fixture server with Ctrl+C. Deactivate the virtual environment, preserve only the reviewed evidence you need, and delete the disposable directory. Do not wipe global Selenium Manager caches, installed browsers, or Java runtimes as part of course cleanup.
11. What Chapter 03 adds to the operating model
You can now explain a browser session as a protocol contract: requested capabilities are validated and matched; a remote end returns effective capabilities and a session identity; commands and element handles are scoped to that identity; Grid can act as an intermediary; and teardown deletes the session. Chapter 04 moves up one layer to locators—how the local end asks the remote end to resolve DOM elements reliably without coupling tests to incidental markup.
Knowledge check
What evidence proves the local and remote tests were separate sessions?
They have different session IDs and separate element-reference handles, even though they execute equivalent actions against the same loopback fixture.
Why is the negative platform request a negotiation failure rather than a locator failure?
The session could not be established and no AUT navigation occurred, so element lookup never became possible.
Why use --reject-unsupported-caps true only as a deliberate Grid policy for this lab?
It makes unsupported requests fail immediately for diagnosis. Without it, Grid may queue requests while waiting for matching capacity, which is a different operational policy.
Should requested and returned capability JSON be identical?
No. Requested capabilities express intent; returned capabilities describe the effective session and can contain defaults or implementation-specific metadata.
What is the Chapter 04 bridge?
Using WebDriver element-finding commands and resilient locator strategies to obtain element references within the correct session and browsing context.
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.