Chapter 03Lesson 05~260 minutes

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.

CheckpointLocal vs remoteCapability negotiationGridEvidence packet

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:

  1. Selenium Python exactly 4.47.0 and Python 3.10+;
  2. Java 11+ and reviewed selenium-server-4.47.0.jar;
  3. a fixture reachable only at 127.0.0.1:8013;
  4. a Grid Standalone endpoint bound only to 127.0.0.1:4444;
  5. one successful local session and one successful remote session;
  6. requested and returned capability records for both paths;
  7. different session IDs and opaque element handles;
  8. one unsupported capability request that fails before AUT interaction;
  9. a diagnosis that names capability matching/session creation as the failing layer;
  10. 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.json and remote.json with requested/returned capabilities and session IDs;
  • local.png and remote.png showing only the synthetic loopback fixture;
  • unsupported-capability.txt with the first line of the negative failure;
  • grid.log retained 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?

Why is the negative platform request a negotiation failure rather than a locator failure?

Why use --reject-unsupported-caps true only as a deliberate Grid policy for this lab?

Should requested and returned capability JSON be identical?

What is the Chapter 04 bridge?

Next chapter

Locators and resilient element selection

Chapter 04 uses the session/protocol model you now understand to explain how element-finding requests are scoped, why selector quality matters, and how to avoid brittle implementation-detail locators.

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.