Chapter 09Lesson 02~185 minutes

Alerts, Prompts, Modal Dialogs, and Browser Context Changes: Guided Hands-On Workflow

Use one synthetic loopback page to compare browser-native alert/confirm/prompt behavior with a DOM modal. Every interaction records the prompt or DOM state before mutation and verifies the application-visible result afterward.

Local labAlert/confirm/promptDOM dialogUnexpected promptEvidence

Learning objectives

  • Create a free, disposable local dialog fixture and pin the Selenium Python binding.
  • Trigger and handle alert, confirm, and prompt dialogs with bounded prompt-presence synchronization.
  • Provide prompt input to the browser prompt rather than the page DOM.
  • Demonstrate an unexpected prompt blocking the next WebDriver command and preserve the exception evidence.
  • Automate a DOM modal through ordinary locators/waits and compare its focus/state model with native prompts.
  • Finish with a challenge that selects the correct control plane from observed state.

1. Setup and preflight

Create an empty directory such as selenium-ch09-lab. Use Python 3.10+ and an isolated environment. The pinned mandatory path is Selenium 4.47.0 with one supported local Chromium-family browser.

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.__version__)"
Safety boundary: the fixture is synthetic and served only from 127.0.0.1. Do not substitute a real account, production admin UI, commerce flow, MFA/CAPTCHA page, or customer site.

2. Generate the local dialog fixture

The following example makes the Generate the local dialog 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())

Save the generator as make_fixture.py, run it, then serve the generated site directory:

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

Keep that server terminal open. All dialog side effects are represented by visible result labels, so the browser action and application outcome can be verified separately.

3. Alert: inspect meaning, then accept

An alert has text but no business choice beyond closure. Wait for prompt presence, assert the message, accept, and only then verify the page state produced after alert() returns.

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:8774/index.html"
driver = webdriver.Chrome()
wait = WebDriverWait(driver, 5)
try:
    driver.get(URL)
    driver.find_element(By.ID, "alert-btn").click()
    alert = wait.until(EC.alert_is_present())
    print("alert text:", alert.text)
    assert alert.text == "Synthetic alert: build complete"
    alert.accept()
    assert driver.find_element(By.ID, "native-result").text == "alert:closed"
finally:
    driver.quit()

4. Confirm: accept and dismiss are different business paths

The confirm dialog returns true on accept and false on dismiss. The test should choose the path required by the scenario and verify the AUT outcome, not merely prove that Selenium closed a prompt.

driver.find_element(By.ID, "confirm-btn").click()
confirm = wait.until(EC.alert_is_present())
assert confirm.text == "Promote synthetic artifact?"
confirm.dismiss()
assert driver.find_element(By.ID, "native-result").text == "confirm:false"

# Re-open to prove the other branch deliberately.
driver.find_element(By.ID, "confirm-btn").click()
confirm = wait.until(EC.alert_is_present())
confirm.accept()
assert driver.find_element(By.ID, "native-result").text == "confirm:true"

5. Prompt: input belongs to the browser prompt

send_keys() here targets the prompt's input field through the Alert API. It is not the same as sending keys to the active page element.

driver.find_element(By.ID, "prompt-btn").click()
prompt = wait.until(EC.alert_is_present())
assert prompt.text == "Synthetic release label?"
prompt.send_keys("rc-42")
prompt.accept()
assert driver.find_element(By.ID, "native-result").text == "prompt:rc-42"

6. Unexpected prompt: preserve the first failure

For this controlled demonstration, create a separate session with unhandled_prompt_behavior="ignore". That policy leaves the prompt open and reports an error when an unrelated command encounters it.

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

options = Options()
options.unhandled_prompt_behavior = "ignore"
driver = webdriver.Chrome(options=options)
wait = WebDriverWait(driver, 5)
try:
    driver.get("http://127.0.0.1:8774/index.html")
    print("returned policy:", driver.capabilities.get("unhandledPromptBehavior"))
    driver.find_element(By.ID, "confirm-btn").click()
    try:
        # This command is unrelated to prompt handling and should be blocked.
        print(driver.title)
        raise AssertionError("Expected an unexpected-alert failure")
    except UnexpectedAlertPresentException as exc:
        print("blocked command:", type(exc).__name__)
        print("reported alert text:", exc.alert_text)

    confirm = wait.until(EC.alert_is_present())
    assert confirm.text == "Promote synthetic artifact?"
    confirm.dismiss()
    assert driver.find_element(By.ID, "native-result").text == "confirm:false"
finally:
    driver.quit()

The repair is not “retry title.” The repair is to recognize the prompt as the blocking state, assert its meaning, resolve it intentionally, and then continue from a verified post-dialog state.

7. DOM modal: ordinary element semantics

The DOM modal has a role, title, input, buttons, visibility state, and focus. No Alert API is involved.

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


driver = webdriver.Chrome()
wait = WebDriverWait(driver, 5)
try:
    driver.get("http://127.0.0.1:8774/index.html")
    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"
    assert modal.find_element(By.ID, "modal-title").text == "Review synthetic change"
    active = driver.switch_to.active_element
    assert active.get_attribute("id") == "modal-note"
    note = modal.find_element(By.ID, "modal-note")
    note.clear()
    note.send_keys("checkpoint-42")
    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:checkpoint-42"
finally:
    driver.quit()

8. Before/after state: prove what each action changed

The following table organizes the key choices and evidence for Before/after state: prove what each action changed. Use it together with the surrounding prose so the rows serve as a comparison aid rather than standalone rules.

Action Before WebDriver control After / evidence
open alert no native prompt click DOM button prompt text visible through Alert API; normal commands blocked until resolution
dismiss confirm confirm open alert.dismiss() AUT writes confirm:false
accept prompt with text prompt open send_keys + accept AUT writes prompt:rc-42
open DOM modal modal hidden ordinary button click role=dialog visible, modal input focused
approve DOM modal modal visible ordinary WebElement click modal hidden, application result updated

9. Challenge: choose the control from the state model

Add a second DOM button labeled “Delete synthetic record” that opens the existing DOM modal with different title text. Your task is to decide—without copying a sequence—whether to use the Alert API or normal DOM controls. State the evidence that proves your classification before interacting.

A strong answer checks for the expected DOM role/title and uses WebElement semantics. It does not call switch_to.alert just because the UI blocks the background.

10. Cleanup

Stop the local server with Ctrl+C. Preserve any text output/screenshots required for review, then remove only the disposable lab directory. No external account, browser-cloud session, or production data was created.

11. Summary and next step

You have now exercised both control planes and observed the causal chain from trigger → prompt/modal state → explicit decision → application-visible result. Lesson 3 turns these mechanics into design choices for maintainable suites.

Knowledge check

Why does alert.send_keys("rc-42") not type into the page?

What is the main assertion after dismissing a confirm?

Why use a separate ignore-policy session for the unexpected-prompt demo?

A visible overlay has role="dialog" and DOM buttons. Which API should control it?

Why is a fixed sleep unnecessary before switch_to.alert?

Next lesson

Alerts, Prompts, Modal Dialogs, and Browser Context Changes: Configuration, Design Patterns, and Trade-Offs

Continue with Alerts, Prompts, Modal Dialogs, and Browser Context Changes: Configuration, Design Patterns, and Trade-Offs. 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.