Chapter 08Lesson 05~190 minutes

Checkpoint Lab — Navigation, Windows, Tabs, Frames, and Iframes

Checkpoint: build the context fixture from an empty directory, automate a multi-window and nested-frame workflow, deliberately reference a closed handle, diagnose the resulting error, and prove a deterministic one-handle state before the WebDriver session ends.

Checkpoint labContext evidenceFailure injectionDeterministic teardownChapter 09 bridge

Learning objectives

  • Build and preflight a free loopback-only Chapter 08 fixture from an empty directory.
  • Predict top-level handle, document, frame, DOM, and evidence transitions before executing them.
  • Automate history navigation, application-created child context, explicit frame depth, and deterministic restoration.
  • Inject and diagnose a reversible closed-handle failure without sleeps, JavaScript bypass, or browser restart.
  • Produce a privacy-safe evidence packet containing capabilities, session/handle trace, URL/title/frame-visible state, exception evidence, and screenshot.
  • Prove exactly one known top-level handle remains before failure-safe quit() ends the session.

1. Checkpoint charter and safety boundary

Scenario: a synthetic home page contains normal history navigation, a link that opens a child top-level context, and an outer iframe containing a nested iframe. The checkpoint must prove context ownership rather than visual focus.

Scope: use only 127.0.0.1:8772, synthetic content, a disposable project directory, and a fresh WebDriver session. No real credentials, personal browser profiles, public Grid endpoints, or production sites.

2. Preflight and exact assumptions

  • Python 3.10+.
  • selenium==4.47.0.
  • Supported locally installed Chromium-family browser; Selenium Manager resolves the normal local driver path.
  • No Grid is required. If a team later runs the same test through Grid, returned capabilities and handle/context semantics remain evidence requirements.
  • The fixture is served by Python http.server bound to loopback only.

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

mkdir selenium-ch08-checkpoint
cd selenium-ch08-checkpoint
python -m venv .venv
# Activate the environment for your shell, then:
python -m pip install selenium==4.47.0
python -c "import selenium; print(selenium.__version__)"

3. Generate and serve the fixture

Reuse the exact make_fixture.py from Lesson 2 so the checkpoint tests the same documented state model rather than a new hidden environment.

from pathlib import Path

root = Path("site")
root.mkdir(exist_ok=True)

(root / "index.html").write_text("""<!doctype html>
<html lang="en"><head><meta charset="utf-8"><title>Context Lab Home</title></head>
<body data-page="home">
<h1 id="home-title">Context Lab Home</h1>
<p id="home-state">top-level-home</p>
<a id="page2-link" href="/page2.html">Go to page 2</a>
<a id="child-link" href="/child.html" target="_blank" rel="noopener">Open child context</a>
<iframe id="outer-frame" name="outer-frame" title="Outer training frame" src="/outer.html"></iframe>
</body></html>""", encoding="utf-8")

(root / "page2.html").write_text("""<!doctype html>
<html lang="en"><head><meta charset="utf-8"><title>Context Lab Page 2</title></head>
<body data-page="page2"><h1 id="page2-title">Page 2</h1><p id="load-count"></p>
<a id="home-link" href="/index.html">Home</a>
<script>
const n = Number(sessionStorage.getItem('page2Loads') || '0') + 1;
sessionStorage.setItem('page2Loads', String(n));
document.querySelector('#load-count').textContent = `page2-load:${n}`;
</script></body></html>""", encoding="utf-8")

(root / "child.html").write_text("""<!doctype html>
<html lang="en"><head><meta charset="utf-8"><title>Child Context</title></head>
<body data-page="child"><h1 id="child-title">Child Context</h1><p id="child-state">child-ready</p></body></html>""", encoding="utf-8")

(root / "outer.html").write_text("""<!doctype html>
<html lang="en"><head><meta charset="utf-8"><title>Outer Frame</title></head>
<body data-frame="outer"><p id="outer-state">outer-ready</p>
<iframe id="inner-frame" name="inner-frame" title="Inner training frame" src="/inner.html"></iframe>
</body></html>""", encoding="utf-8")

(root / "inner.html").write_text("""<!doctype html>
<html lang="en"><head><meta charset="utf-8"><title>Inner Frame</title></head>
<body data-frame="inner"><button id="inner-action">Inner action</button><p id="inner-state">inner-ready</p>
<script>document.querySelector('#inner-action').addEventListener('click',()=>document.querySelector('#inner-state').textContent='inner-clicked')</script>
</body></html>""", encoding="utf-8")

print(root.resolve())

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

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

4. Write predictions before running

Record at least these predictions in evidence/predictions.txt before execution:

  1. Child creation: after clicking #child-link, the session handle set grows by one; the original handle remains valid; switching changes current URL/title to the child.
  2. Frame depth: switching outer → inner does not create a new window handle; only the search/interaction context changes. default_content() restores top-level DOM lookup.
  3. Failure injection: after closing the child and restoring original, trying to switch to the saved closed child handle raises NoSuchWindowException; the surviving original context remains recoverable.
  4. Final state: before quit(), exactly the original handle remains and the current document is the home page at top-level frame context.

5. Checkpoint implementation

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

from pathlib import Path
from urllib.parse import urlparse
import json
import selenium
from selenium import webdriver
from selenium.common.exceptions import NoSuchWindowException
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

BASE = "http://127.0.0.1:8772/"
if (urlparse(BASE).hostname or "") not in {"127.0.0.1", "localhost", "::1"}:
    raise RuntimeError("Checkpoint is restricted to the loopback fixture")

EVIDENCE = Path("evidence")
EVIDENCE.mkdir(exist_ok=True)
trace = []

driver = webdriver.Chrome()
wait = WebDriverWait(driver, 5)

def record(label, **extra):
    row = {"label": label}
    try:
        row.update({
            "current_handle": driver.current_window_handle,
            "handles": sorted(driver.window_handles),
            "url": driver.current_url,
            "title": driver.title,
        })
    except NoSuchWindowException as exc:
        row.update({
            "context_error": type(exc).__name__,
            "handles": sorted(driver.window_handles),
        })
    row.update(extra)
    trace.append(row)
    print(row)

try:
    original = driver.current_window_handle
    driver.get(BASE + "index.html")
    assert driver.find_element(By.ID, "home-state").text == "top-level-home"
    record("home")

    # History/document proof: same top-level handle, changing document.
    driver.find_element(By.ID, "page2-link").click()
    wait.until(EC.title_is("Context Lab Page 2"))
    assert driver.current_window_handle == original
    record("page2")
    driver.back()
    wait.until(EC.title_is("Context Lab Home"))
    assert driver.current_window_handle == original
    record("history-restored-home")

    # Application-created child; discover identity from handle-set difference.
    before = set(driver.window_handles)
    driver.find_element(By.ID, "child-link").click()
    wait.until(EC.number_of_windows_to_be(len(before) + 1))
    child = (set(driver.window_handles) - before).pop()
    driver.switch_to.window(child)
    wait.until(EC.title_is("Child Context"))
    assert driver.find_element(By.ID, "child-state").text == "child-ready"
    record("child-open", child_handle=child)
    driver.save_screenshot(EVIDENCE / "child.png")

    # Close only owned child, then restore known original.
    driver.close()
    driver.switch_to.window(original)
    record("child-closed-original-restored")

    # Reversible intentional failure: switch to the known CLOSED handle.
    injected = None
    try:
        driver.switch_to.window(child)
    except NoSuchWindowException as exc:
        injected = {"type": type(exc).__name__, "message_prefix": str(exc)[:240]}
        record("expected-closed-handle-failure", injected=injected)
    assert injected and injected["type"] == "NoSuchWindowException"

    # Continue from the surviving original context.
    driver.switch_to.window(original)
    driver.get(BASE + "index.html")

    # Nested frames: handle set must not change.
    handles_before_frames = set(driver.window_handles)
    wait.until(EC.frame_to_be_available_and_switch_to_it((By.ID, "outer-frame")))
    outer_state = driver.find_element(By.ID, "outer-state").text
    wait.until(EC.frame_to_be_available_and_switch_to_it((By.ID, "inner-frame")))
    inner_state_before = driver.find_element(By.ID, "inner-state").text
    driver.find_element(By.ID, "inner-action").click()
    inner_state_after = driver.find_element(By.ID, "inner-state").text
    assert set(driver.window_handles) == handles_before_frames

    driver.switch_to.default_content()
    top_state = driver.find_element(By.ID, "home-state").text
    assert top_state == "top-level-home"
    record("frames-complete", outer_state=outer_state,
           inner_before=inner_state_before, inner_after=inner_state_after,
           top_state=top_state)

    # Deterministic pre-teardown state.
    assert set(driver.window_handles) == {original}
    assert driver.current_window_handle == original
    assert driver.current_url == BASE + "index.html"
    assert driver.find_element(By.ID, "home-state").text == "top-level-home"
    driver.save_screenshot(EVIDENCE / "final-one-handle.png")
    record("verified-final-one-handle")

    packet = {
        "selenium": selenium.__version__,
        "session_id": driver.session_id,
        "capabilities": {
            "browserName": driver.capabilities.get("browserName"),
            "browserVersion": driver.capabilities.get("browserVersion"),
            "platformName": driver.capabilities.get("platformName"),
        },
        "original_handle": original,
        "closed_child_handle": child,
        "trace": trace,
    }
    (EVIDENCE / "context-evidence.json").write_text(
        json.dumps(packet, indent=2), encoding="utf-8"
    )
finally:
    driver.quit()

6. Interpret the injected failure without hiding it

The expected NoSuchWindowException proves that handle identity has lifecycle. Once the child is closed, a saved string is not a valid browsing context merely because the test still possesses it. The repair is not retry/sleep; it is switching to the known surviving handle and continuing from a verified document/frame state.

The failure remains in the evidence packet even though the checkpoint recovers. That is how incident evidence should work: recovery must not erase the original cause.

7. Required evidence packet

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

Artifact / field What it proves
predictions.txt the expected state transitions were reasoned about before mutation
context-evidence.json Selenium/browser provenance, session ID, original/closed handles, ordered context trace
child.png the child context was actually current before close
final-one-handle.png the final surviving top-level context was restored
URL/title/application markers commands operated on the intended document/context
NoSuchWindowException prefix the reversible failure was identity/lifecycle, not a timing guess

8. Verification checklist

  • All traffic targets 127.0.0.1:8772.
  • Selenium is pinned to 4.47.0 and actual browser capability values are recorded.
  • History navigation changes document-visible state without changing the original handle.
  • The child handle is found by set difference.
  • The closed child is never treated as valid after the diagnostic exception.
  • Nested frame switching leaves the top-level handle set unchanged.
  • default_content() restores the home page locator scope.
  • Exactly one known original handle remains before teardown.
  • driver.quit() executes in finally.

9. Cleanup and rollback

Stop the local HTTP server with Ctrl+C. Preserve the evidence directory if it is part of a review, then remove only this disposable checkpoint directory. There is no production data or remote state to roll back.

cd ..
rm -rf selenium-ch08-checkpoint   # Linux/macOS
# Windows PowerShell alternative:
# Remove-Item -Recurse -Force .\selenium-ch08-checkpoint

10. What Chapter 08 adds to the operating model

The Selenium operating model now includes explicit context ownership: history/document transitions, opaque top-level handle identity, nested frame scope, state-based discovery, deterministic restoration, evidence correlation, and failure-safe teardown. These rules prevent browser-state leakage from masquerading as random CI flakiness.

Chapter 09 moves to a different class of context change: browser-native alerts/prompts and modal interactions. Those are not ordinary DOM frames/windows and require their own mental model and APIs.

11. Summary

The checkpoint proved that reliable multi-context automation is a state machine, not a sequence of visually focused browser actions. Handles, frame depth, document lifetime, and teardown are all explicit and independently verifiable.

Knowledge check

Why is switching to the saved closed child handle expected to fail?

What prediction distinguishes frame switching from opening a new tab?

Why preserve the injected failure in the evidence packet after recovery?

What proves deterministic teardown before quit()?

What topic follows naturally in Chapter 09?

Next chapter

Alerts, Prompts, Modal Dialogs, and Browser Context Changes: Core Concepts and Mental Model

Continue with Alerts, Prompts, Modal Dialogs, and Browser Context Changes: Core Concepts and Mental Model. It builds directly on the state, evidence, and operating assumptions established here, so carry those constraints forward rather than treating the next page as an isolated topic.

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 Selenium Manager for normal local driver resolution, and target only loopback fixtures. Window handles are treated as opaque identifiers; examples do not assume handle ordering. Chapter 08 intentionally avoids WebDriver BiDi/CDP because classic WebDriver navigation and context APIs are sufficient for the mandatory learning objective.

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.