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.
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.
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.serverbound 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:
-
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. -
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. -
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. -
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 infinally.
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?
A handle identifies a live top-level browsing context; closing that context ends its validity, so switching to it raises NoSuchWindowException.
What prediction distinguishes frame switching from opening a new tab?
Frame switching changes the current nested browsing context while the top-level window-handle set remains unchanged; a new tab adds another top-level handle.
Why preserve the injected failure in the evidence packet after recovery?
Recovery should not erase first-failure evidence; the exception proves the actual cause and supports reproducible diagnosis.
What proves deterministic teardown before
quit()?
The handle set is exactly {original}, current handle equals original, the current URL is the home fixture, and a top-level marker is findable after default-content restoration.
What topic follows naturally in Chapter 09?
Browser-native alerts, prompts, modal dialogs, and related browser context changes, which require APIs distinct from DOM frames/windows.
Official references and version notes
- Selenium 4.47 release notes — pinned stable baseline for this chapter.
- Selenium downloads — current stable binding and Selenium Server/Grid versions.
-
Browser navigation
—
get, back, forward, and refresh semantics. - Working with windows and tabs — handles, switching, creating, closing, and quitting top-level browsing contexts.
- Working with frames and iframes — switching by WebElement/name/index and returning to default content.
-
Selenium Python 4.47 SwitchTo API
—
window,new_window,frame,parent_frame, anddefault_content.
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.