Chapter 07Lesson 05~180 minutes

Checkpoint Lab — Keyboard, Pointer, Wheel, and Composite Actions API

Build and operate a disposable accessible interaction checkpoint: predict focus/order/viewport transitions, automate keyboard selection and movement, exercise wheel scrolling, inject one reversible coordinate failure, preserve evidence, and rewrite the failing action around semantic browser state.

CheckpointAccessibilityWheelFailure injectionOperating model

Learning objectives

  • Build the checkpoint from an empty directory with pinned Selenium and a loopback-only accessible fixture.
  • Predict keyboard focus, selection/order, viewport, and evidence transitions before running the automation.
  • Automate a keyboard-accessible selection/reorder interaction with balanced modifier state.
  • Perform a wheel-source viewport interaction and verify application-visible scroll evidence.
  • Inject and diagnose one out-of-bounds coordinate error without JavaScript bypass or retries.
  • Produce a versioned evidence packet, verify cleanup, and explain how Actions discipline feeds the production browser-automation operating model.

1. Checkpoint scenario and safety boundary

The AUT is an accessible priority board. Each card is a real keyboard-focusable button with role="option". Space selects a card; Alt+ArrowDown moves a selected card down one position. A distant viewport checkpoint records when scrolling brings it into view.

You will automate the keyboard path, prove focus/order/state, use the wheel source to reach the checkpoint, then deliberately request an impossible pointer offset. After preserving the original exception/evidence, you will replace the coordinate assumption with an element-semantic action.

Scope: only http://127.0.0.1:8772, synthetic cards, a temporary WebDriver session, and a local evidence/ directory. No real accounts, production URLs, public Grid, credentials, paid services, or browser-profile reuse.

2. Setup and preflight

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.

mkdir selenium-ch07-checkpoint
cd selenium-ch07-checkpoint
python -m venv .venv
# Linux/macOS:
. .venv/bin/activate
# Windows PowerShell:
# .\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install "selenium==4.47.0"
mkdir site evidence

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.

<!doctype html>
<html lang="en"><head><meta charset="utf-8"><title>Chapter 07 Checkpoint</title>
<style>body{font:16px system-ui;max-width:800px;margin:30px auto;padding:0 20px}.board{border:1px solid #888;padding:16px}.card{display:block;width:100%;text-align:left;margin:8px 0;padding:12px}.card[aria-selected="true"]{outline:3px solid #36c}.spacer{height:900px}.target{border:1px solid #777;padding:18px}#events{font:13px ui-monospace,monospace}</style></head>
<body data-build="ch07-checkpoint-v1"><h1>Accessible priority board</h1>
<p id="help">Focus a card. Space selects it; Alt+ArrowDown moves the selected card one position down.</p>
<div id="board" class="board" role="listbox" aria-describedby="help">
<button class="card" id="card-a" role="option" aria-selected="false">A — Build</button>
<button class="card" id="card-b" role="option" aria-selected="false">B — Test</button>
<button class="card" id="card-c" role="option" aria-selected="false">C — Package</button></div>
<p id="board-status">No selection</p><div class="spacer"></div>
<div id="wheel-target" class="target" tabindex="0"><strong>Viewport checkpoint</strong><p id="wheel-status">Not reached</p></div>
<h2>Events</h2><ol id="events"></ol>
<script>
const ev=(m)=>{const li=document.createElement('li');li.textContent=m;document.querySelector('#events').append(li)};
const board=document.querySelector('#board'), status=document.querySelector('#board-status');
board.addEventListener('focusin',e=>{if(e.target.matches('.card'))ev(`focus:${e.target.id}`)});
board.addEventListener('keydown',e=>{if(!e.target.matches('.card'))return;const card=e.target;if(e.key===' '){e.preventDefault();board.querySelectorAll('.card').forEach(x=>x.setAttribute('aria-selected','false'));card.setAttribute('aria-selected','true');status.textContent=`Selected:${card.id}`;ev(`select:${card.id}`);}if(e.altKey&&e.key==='ArrowDown'){e.preventDefault();if(card.getAttribute('aria-selected')!=='true'){status.textContent='Move rejected: select first';ev(`move-rejected:${card.id}`);return;}const next=card.nextElementSibling;if(next){board.insertBefore(next,card);card.focus();status.textContent=`Moved:${card.id}`;ev(`move:${card.id}`);}}});
window.addEventListener('scroll',()=>{const r=document.querySelector('#wheel-target').getBoundingClientRect();if(r.top<innerHeight&&r.bottom>0){document.querySelector('#wheel-status').textContent='Reached by viewport scroll';ev('wheel-target-visible');}},{passive:true});
</script></body></html>

Save as site/index.html and start the fixture in another terminal:

python -m http.server 8772 --bind 127.0.0.1 --directory site

Preflight: verify Python 3.10+, Selenium 4.47.0, the page title Chapter 07 Checkpoint, and one supported local Chromium-family browser. Record the actual browser/platform returned by capabilities.

3. Predict state transitions before execution

The following example makes the Predict state transitions before execution behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.

Prediction 1: after clicking card A, active_element becomes card-a but no card is selected yet.
Prediction 2: after Space on card A, aria-selected changes false -> true and board-status becomes Selected:card-a.
Prediction 3: after Alt+ArrowDown, card A remains focused/selected but its DOM order becomes card-b, card-a, card-c.
Prediction 4: wheel scrolling to #wheel-target changes viewport state and the fixture records Reached by viewport scroll.
Prediction 5: move_by_offset(5000, 5000) should fail as an out-of-bounds coordinate request on an ordinary viewport; the semantic rewrite should target an element instead.

Do not rewrite predictions after seeing the result. Differences are diagnostic evidence.

4. Execute the checkpoint

The following example makes the Execute the checkpoint 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 urllib.parse import urlparse

import selenium
from selenium import webdriver
from selenium.common.exceptions import MoveTargetOutOfBoundsException
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys

BASE = "http://127.0.0.1:8772/"
E = Path("evidence")
E.mkdir(exist_ok=True)

def guard(url):
    host = (urlparse(url).hostname or "").lower()
    if host not in {"127.0.0.1", "localhost", "::1"}:
        raise RuntimeError(f"Refusing non-loopback host: {host}")

def order(driver):
    return [e.get_attribute("id") for e in driver.find_elements(By.CSS_SELECTOR, "#board .card")]

def events(driver):
    return [e.text for e in driver.find_elements(By.CSS_SELECTOR, "#events li")]

guard(BASE)
driver = webdriver.Chrome()
cleanup_actions = ActionChains(driver)
packet = {}
try:
    driver.get(BASE)
    assert driver.title == "Chapter 07 Checkpoint"
    caps = driver.capabilities
    packet["provenance"] = {
        "selenium": selenium.__version__,
        "session_id": driver.session_id,
        "browserName": caps.get("browserName"),
        "browserVersion": caps.get("browserVersion"),
        "platformName": caps.get("platformName"),
    }
    packet["before"] = {
        "order": order(driver),
        "active": driver.switch_to.active_element.get_attribute("id"),
        "status": driver.find_element(By.ID, "board-status").text,
    }

    # Keyboard-accessible selection and movement.
    card_a = driver.find_element(By.ID, "card-a")
    card_a.click()
    assert driver.switch_to.active_element.get_attribute("id") == "card-a"
    ActionChains(driver).send_keys(Keys.SPACE).perform()
    assert card_a.get_dom_attribute("aria-selected") == "true"
    assert driver.find_element(By.ID, "board-status").text == "Selected:card-a"

    move = ActionChains(driver)
    move.key_down(Keys.ALT).send_keys(Keys.ARROW_DOWN).key_up(Keys.ALT).perform()
    assert order(driver) == ["card-b", "card-a", "card-c"]
    assert driver.switch_to.active_element.get_attribute("id") == "card-a"

    # Wheel-based viewport interaction.
    wheel_target = driver.find_element(By.ID, "wheel-target")
    ActionChains(driver).scroll_to_element(wheel_target).perform()
    assert driver.find_element(By.ID, "wheel-status").text == "Reached by viewport scroll"

    # Reversible failure injection: impossible current-pointer offset.
    try:
        ActionChains(driver).move_by_offset(5000, 5000).click().perform()
        packet["coordinate_failure"] = {"unexpected": "no exception"}
    except MoveTargetOutOfBoundsException as exc:
        packet["coordinate_failure"] = {
            "exception": type(exc).__name__,
            "message_prefix": str(exc)[:240],
        }
        driver.save_screenshot(str(E / "coordinate-first-failure.png"))

    # Least-destructive semantic rewrite.
    card_c = driver.find_element(By.ID, "card-c")
    ActionChains(driver).move_to_element(card_c).click().perform()
    assert driver.switch_to.active_element.get_attribute("id") == "card-c"

    packet["after"] = {
        "order": order(driver),
        "card_a_selected": driver.find_element(By.ID, "card-a").get_dom_attribute("aria-selected"),
        "active": driver.switch_to.active_element.get_attribute("id"),
        "board_status": driver.find_element(By.ID, "board-status").text,
        "wheel_status": driver.find_element(By.ID, "wheel-status").text,
        "events": events(driver),
    }
    driver.save_screenshot(str(E / "checkpoint-final.png"))
    (E / "checkpoint.json").write_text(json.dumps(packet, indent=2), encoding="utf-8")
finally:
    try:
        cleanup_actions.reset_actions()
    finally:
        driver.quit()

Run python checkpoint.py. The test intentionally catches only the expected coordinate exception. Other failures should fail the script and preserve their traceback; do not add a blanket exception handler that converts an unexpected failure into a pass.

5. Verify every prediction independently

Open evidence/checkpoint.json and verify:

  • provenance identifies Selenium 4.47.0, the actual browser version/platform, and a non-empty session ID.
  • Before state has order card-a, card-b, card-c.
  • After keyboard selection/reorder, order is card-b, card-a, card-c and card A remains selected.
  • The event list contains focus/select/move evidence for card A.
  • Wheel status is Reached by viewport scroll.
  • coordinate_failure.exception is MoveTargetOutOfBoundsException on normal supported environments; if not, record actual viewport/browser behavior rather than inventing the expected exception.
  • After semantic repair, active element is card C.

The screenshot from the first coordinate failure must be preserved before the semantic rewrite because later actions change focus and visible state.

6. Evidence packet and trust boundaries

The following table organizes the key choices and evidence for Evidence packet and trust boundaries. Use it together with the surrounding prose so the rows serve as a comparison aid rather than standalone rules.

Artifact Purpose Sensitive-data rule
checkpoint.json versions/session/browser, before/after order/focus/status, event evidence synthetic fixture only; real keyboard/event logs require minimization/redaction
coordinate-first-failure.png preserves the original visible failure state never capture real secrets/account pages in this lab
checkpoint-final.png proves final accessible/viewport state local fixture contains no PII
terminal traceback/output unexpected failure semantics do not paste tokens, proxy credentials, or private paths into course artifacts

7. Explain the coordinate failure and repair

The injected action uses the current pointer as the implicit origin and asks for a 5000×5000 move. On an ordinary viewport that request is outside the browsing context and the remote end rejects it. This is a deterministic geometry/input-contract problem.

The rewrite uses move_to_element(card_c), which makes the WebElement the semantic anchor. It does not “make Selenium more tolerant”; it removes an unnecessary pixel assumption. A production test should go one step further and use card_c.click() if pointer movement itself is not under test.

8. Cleanup and rollback

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

# Stop the local http.server with Ctrl+C.
# Preserve evidence only if your review process requires it, then:
cd ..
rm -rf selenium-ch07-checkpoint

The rollback surface is only this disposable directory and its browser session. Do not delete ordinary browser profiles, global caches, drivers, or unrelated CI workspaces.

9. What Chapter 07 adds to the production operating model

The course operating model now treats complex user input as explicit session state: identify the correct context and semantic target, choose the smallest necessary input source, balance depressed keys/buttons, prefer element/state semantics over fixed pixels, capture focus/event/browser evidence, and release virtual-device state during cleanup.

This discipline makes cross-browser CI failures explainable rather than “mouse flakiness.” It also creates the prerequisite for Chapter 08, where the same actions must be sent to the correct window, tab, frame, or iframe browsing context.

10. Checkpoint summary and Chapter 08 bridge

You proved keyboard selection/reordering, wheel-based viewport movement, focus/event evidence, an intentionally broken coordinate assumption, and a semantic repair—without JavaScript bypasses, retries, production targets, or paid tooling.

Knowledge check

Why does the checkpoint select card A with Space before moving it with Alt+ArrowDown?

What makes move_by_offset(5000, 5000) intentionally fragile?

Why save the first-failure screenshot before the semantic repair?

The keyboard move works but aria-selected is false afterward. Is checking DOM order alone sufficient?

What new failure dimension does Chapter 08 add?

Next chapter

Navigation, Windows, Tabs, Frames, and Iframes

Chapter 08 adds browsing-context identity. The next question is no longer only “which element and which input source?” but also “which window/frame currently receives the command?”

Official references and version notes

Version and compatibility note

Version-sensitive behavior was rechecked against current primary documentation on 2026-08-28. Mandatory examples pin Selenium Python 4.47.0, require Python 3.10+, use a supported local Chromium-family browser with the actual browser/driver/session provenance recorded at runtime, and use only loopback fixtures. Wheel and detailed pointer behavior can differ at browser/platform edges; assertions therefore target application/focus/event state rather than incidental pixel coordinates. The examples intentionally avoid low-level private ActionBuilder internals except where public documentation is referenced; normal teaching uses public ActionChains conveniences.

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.