Chapter 09Lesson 05~195 minutes

Checkpoint Lab — Alerts, Prompts, Modal Dialogs, and Browser Context Changes

Checkpoint the chapter by building a disposable dialog workflow that includes native confirm/prompt decisions, a DOM modal, an intentionally unhandled prompt, evidence preservation, explicit repair, and a deterministic final browser/application state.

Checkpoint labFailure injectionPrompt evidenceDeterministic recoveryChapter 10 bridge

Learning objectives

  • Build the chapter fixture from an empty directory with exact pinned assumptions and loopback-only scope.
  • Predict native-prompt, DOM-modal, focus, and AUT-result transitions before performing them.
  • Execute confirm, prompt, and DOM-modal paths with explicit text/state assertions.
  • Inject an unhandled-prompt failure using a reversible session capability and preserve first-failure evidence.
  • Repair the failure without blanket retries, fixed sleeps, JavaScript bypass, or browser restart.
  • Produce an evidence packet and prove a known final state before quitting the session.

1. Checkpoint scenario and safety boundary

You are validating a synthetic release workflow. A confirm decides whether to promote a fake artifact, a prompt captures a fake release label, and a DOM modal records a review note. Then a second disposable session intentionally leaves a browser prompt unhandled so you can observe the configured failure behavior and recover explicitly.

Mandatory scope: loopback fixture, synthetic labels, no credentials, no production sites, no public Grid, no TLS changes, no real deployment or commerce action.

2. Setup and preflight

Create selenium-ch09-checkpoint, then create and activate an isolated Python environment.

python -m venv .venv
# Linux/macOS
. .venv/bin/activate
# Windows PowerShell alternative:
# .\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install selenium==4.47.0
python -c "import selenium; print('selenium', selenium.__version__)"

Preflight checklist:

  • Python 3.10+.
  • Selenium Python exactly 4.47.0.
  • One supported local Chromium-family browser available.
  • Selenium Manager allowed to resolve the normal local driver path.
  • Ports 8775 available on loopback.
  • No real browser profile or account session reused.

3. Generate and serve the checkpoint fixture

The following example makes the Generate and serve the checkpoint fixture 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

root = Path("site")
root.mkdir(exist_ok=True)
(root / "index.html").write_text("""<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><title>Dialog Lab</title>
<style>
body{font-family:system-ui,sans-serif;max-width:760px;margin:2rem auto;padding:0 1rem}
button{margin:.25rem;padding:.55rem .8rem}.modal-backdrop{position:fixed;inset:0;background:#0008;display:grid;place-items:center}
.modal-card{background:white;color:#111;padding:1rem;border-radius:.6rem;min-width:280px}.modal-backdrop[hidden]{display:none}
</style></head>
<body>
<h1 id="page-title">Dialog Lab</h1>
<p id="native-result">native:none</p>
<p id="modal-result">modal:none</p>
<button id="alert-btn">Open alert</button>
<button id="confirm-btn">Open confirm</button>
<button id="prompt-btn">Open prompt</button>
<button id="dom-modal-btn">Open DOM modal</button>
<div id="dom-modal" class="modal-backdrop" role="dialog" aria-modal="true" aria-labelledby="modal-title" hidden>
  <div class="modal-card">
    <h2 id="modal-title">Review synthetic change</h2>
    <label>Note <input id="modal-note" value="fixture-note"></label>
    <button id="modal-approve">Approve</button>
    <button id="modal-cancel">Cancel</button>
  </div>
</div>
<script>
const nativeResult = document.querySelector('#native-result');
const modalResult = document.querySelector('#modal-result');
document.querySelector('#alert-btn').addEventListener('click', () => {
  alert('Synthetic alert: build complete');
  nativeResult.textContent = 'alert:closed';
});
document.querySelector('#confirm-btn').addEventListener('click', () => {
  const ok = confirm('Promote synthetic artifact?');
  nativeResult.textContent = `confirm:${ok}`;
});
document.querySelector('#prompt-btn').addEventListener('click', () => {
  const value = prompt('Synthetic release label?', '');
  nativeResult.textContent = value === null ? 'prompt:cancelled' : `prompt:${value}`;
});
const modal = document.querySelector('#dom-modal');
document.querySelector('#dom-modal-btn').addEventListener('click', () => {
  modal.hidden = false;
  document.querySelector('#modal-note').focus();
});
document.querySelector('#modal-approve').addEventListener('click', () => {
  modalResult.textContent = `modal:approved:${document.querySelector('#modal-note').value}`;
  modal.hidden = true;
});
document.querySelector('#modal-cancel').addEventListener('click', () => {
  modalResult.textContent = 'modal:cancelled';
  modal.hidden = true;
});
</script></body></html>""", encoding="utf-8")
print(root.resolve())

The following example makes the Generate and serve the checkpoint 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 8775 --bind 127.0.0.1 --directory site

The page itself is unchanged from Lesson 2; only the port differs so the checkpoint is self-contained.

4. Predict state transitions before running

Write these predictions into predictions.txt before automation:

Prediction A — confirm dismiss
- Browser-native confirm becomes active; DOM modal state does not change.
- After dismiss, prompt is gone and #native-result becomes confirm:false.

Prediction B — prompt accept with text
- Browser-native prompt becomes active and blocks ordinary page commands.
- After send_keys('release-9') + accept, #native-result becomes prompt:release-9.

Prediction C — DOM modal approve
- No browser-native prompt exists.
- #dom-modal becomes visible, #modal-note owns focus, then the modal hides and #modal-result records the note.

Prediction D — unhandled prompt with policy=ignore
- An unrelated command raises UnexpectedAlertPresentException.
- The prompt remains active until explicitly accepted/dismissed.

5. Run the explicit native + DOM flow

Save as checkpoint_flow.py. This session uses the documented default prompt policy and handles all expected prompts explicitly.

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

URL = "http://127.0.0.1:8775/index.html"
if (urlparse(URL).hostname or "") not in {"127.0.0.1", "localhost", "::1"}:
    raise RuntimeError("Checkpoint requires loopback AUT")

Path("evidence").mkdir(exist_ok=True)
driver = webdriver.Chrome()
wait = WebDriverWait(driver, 5)
evidence = {"selenium": selenium.__version__}
try:
    driver.get(URL)
    evidence["session_id"] = driver.session_id
    evidence["browser"] = driver.capabilities.get("browserName")
    evidence["browser_version"] = driver.capabilities.get("browserVersion")
    evidence["prompt_policy"] = driver.capabilities.get("unhandledPromptBehavior")
    evidence["initial_url"] = driver.current_url
    evidence["initial_title"] = driver.title

    # 1) Confirm: dismiss deliberately.
    driver.find_element(By.ID, "confirm-btn").click()
    confirm = wait.until(EC.alert_is_present())
    evidence["confirm_text"] = confirm.text
    assert confirm.text == "Promote synthetic artifact?"
    confirm.dismiss()
    assert driver.find_element(By.ID, "native-result").text == "confirm:false"

    # 2) Prompt: supply synthetic release label deliberately.
    driver.find_element(By.ID, "prompt-btn").click()
    prompt = wait.until(EC.alert_is_present())
    evidence["prompt_text"] = prompt.text
    assert prompt.text == "Synthetic release label?"
    prompt.send_keys("release-9")
    prompt.accept()
    assert driver.find_element(By.ID, "native-result").text == "prompt:release-9"

    # 3) DOM modal: no Alert API.
    driver.find_element(By.ID, "dom-modal-btn").click()
    modal = wait.until(EC.visibility_of_element_located((By.ID, "dom-modal")))
    assert modal.get_attribute("role") == "dialog"
    evidence["modal_title"] = modal.find_element(By.ID, "modal-title").text
    assert evidence["modal_title"] == "Review synthetic change"
    active = driver.switch_to.active_element
    evidence["modal_focus"] = active.get_attribute("id")
    assert evidence["modal_focus"] == "modal-note"
    note = modal.find_element(By.ID, "modal-note")
    note.clear()
    note.send_keys("review-ok")
    modal.find_element(By.ID, "modal-approve").click()
    wait.until(EC.invisibility_of_element_located((By.ID, "dom-modal")))
    assert driver.find_element(By.ID, "modal-result").text == "modal:approved:review-ok"

    driver.save_screenshot("evidence/explicit-flow-final.png")
    evidence["final_native"] = driver.find_element(By.ID, "native-result").text
    evidence["final_modal"] = driver.find_element(By.ID, "modal-result").text
    evidence["final_url"] = driver.current_url
    evidence["final_title"] = driver.title
finally:
    Path("evidence/explicit-flow.json").write_text(json.dumps(evidence, indent=2), encoding="utf-8")
    driver.quit()

6. Inject one reversible unhandled-prompt failure

Save this as checkpoint_failure.py. The only injected change is the new session's prompt policy. No browser files, profiles, network settings, or AUT data are modified.

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

URL = "http://127.0.0.1:8775/index.html"
if (urlparse(URL).hostname or "") not in {"127.0.0.1", "localhost", "::1"}:
    raise RuntimeError("Checkpoint requires loopback AUT")

Path("evidence").mkdir(exist_ok=True)
options = Options()
options.unhandled_prompt_behavior = "ignore"
driver = webdriver.Chrome(options=options)
wait = WebDriverWait(driver, 5)
record = {}
try:
    driver.get(URL)
    record["session_id"] = driver.session_id
    record["prompt_policy"] = driver.capabilities.get("unhandledPromptBehavior")
    driver.find_element(By.ID, "alert-btn").click()

    try:
        _ = driver.title  # intentionally unrelated command while prompt is open
        raise AssertionError("Expected UnexpectedAlertPresentException")
    except UnexpectedAlertPresentException as exc:
        record["first_failure"] = type(exc).__name__
        record["reported_alert_text"] = exc.alert_text
        record["exception_message"] = str(exc)

    # Least-destructive correction: inspect the same live prompt and resolve it.
    alert = wait.until(EC.alert_is_present())
    record["live_prompt_text"] = alert.text
    assert alert.text == "Synthetic alert: build complete"
    alert.accept()

    # Deterministic post-dialog state verification.
    assert driver.title == "Dialog Lab"
    assert driver.current_url == URL
    assert driver.find_element(By.ID, "native-result").text == "alert:closed"
    assert driver.find_element(By.ID, "dom-modal").get_property("hidden") is True
    record["final_title"] = driver.title
    record["final_url"] = driver.current_url
    record["final_native"] = driver.find_element(By.ID, "native-result").text
    driver.save_screenshot("evidence/recovered-final.png")
finally:
    Path("evidence/unhandled-prompt.json").write_text(json.dumps(record, indent=2), encoding="utf-8")
    driver.quit()

7. Interpret the failure instead of hiding it

The important evidence sequence is:

  1. policy returned as ignore;
  2. an unrelated title command fails with UnexpectedAlertPresentException;
  3. the reported/live prompt text identifies the blocker;
  4. explicit Alert handling resolves the same state;
  5. title, URL, DOM result, and modal-hidden state prove deterministic recovery.

A blanket retry or browser restart would have destroyed this causal chain.

8. Required evidence packet

Keep these files together:

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

predictions.txt
evidence/explicit-flow.json
evidence/explicit-flow-final.png
evidence/unhandled-prompt.json
evidence/recovered-final.png

Record in review notes:
- Python version
- Selenium 4.47.0
- browser name/version and returned prompt policy
- loopback fixture URL
- expected confirm/prompt/modal texts
- first-failure exception and alert text
- final URL/title/native-result/modal-result
- confirmation that driver.quit() ran for both sessions

9. Verification checklist

  • Exactly the expected confirm text was dismissed and AUT reported confirm:false.
  • Exactly the expected prompt text received release-9 and AUT reported it.
  • The DOM modal was found as DOM state, had expected title/focus, and closed after ordinary WebElement interaction.
  • The injected unhandled prompt produced UnexpectedAlertPresentException under returned ignore policy.
  • The same live prompt was explicitly asserted and accepted.
  • Final URL/title and result labels are deterministic.
  • No fixed sleeps, JavaScript force-clicks, browser restarts, TLS changes, credentials, or production targets were used.

10. Cleanup and rollback

Stop the loopback HTTP server with Ctrl+C. Preserve the evidence packet if required, then remove only the checkpoint directory. The injected behavior was session-local, so quitting the diagnostic session is the rollback.

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

11. What Chapter 09 adds to the production operating model

The operating model now distinguishes browser-native prompt state from DOM modal state, records the negotiated prompt policy, asserts dialog meaning before business decisions, preserves unexpected-prompt evidence, and verifies deterministic post-dialog context/application state. This removes a common source of “mysterious modal flakiness” from CI.

Chapter 10 moves deeper into modern DOM boundaries: Shadow DOM, Web Components, dynamic rerendering, and element-reference lifetime. The same discipline applies—identify the actual browser/document boundary before choosing an API.

12. Summary

The checkpoint demonstrated that dialogs are not one generic Selenium concept. Native prompts and DOM modals have different owners, control planes, evidence, and failure semantics. Reliability comes from classification, explicit business intent, bounded synchronization, and verified recovery.

Knowledge check

Why is ignore useful in the checkpoint failure-injection session?

What proves the DOM modal was not a browser-native alert?

Why do the checkpoint scripts assert prompt text before deciding?

What is the correct recovery after the intentionally blocked title command?

What production topic comes next in Chapter 10?

Next chapter

Shadow DOM, Web Components, Dynamic DOMs, and Modern Front Ends: Core Concepts and Mental Model

Continue with Shadow DOM, Web Components, Dynamic DOMs, and Modern Front Ends: 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. Selenium Python 4.47.0 documents dismiss and notify as the default unhandled_prompt_behavior; the chapter uses ignore only in a controlled failure-injection session so the learner can observe and then explicitly resolve the prompt. Classic WebDriver is sufficient for the mandatory chapter path; BiDi/CDP are not required to handle these dialog semantics.

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.