Windows, Frames, and Alerts
Three contexts a locator cannot cross — new tabs, iframes and native dialogs — and the switch_to calls that move between them without losing your place.
WebDriver commands apply to exactly one browsing context. A locator that works perfectly will find nothing if the element lives in a different tab or inside an iframe — and a native dialog blocks everything until it is dismissed.
Windows and tabs
LINKS = """data:text/html,
<a id="external" href="https://demo.playwright.dev/todomvc" target="_blank">Open todos</a>
<p id="here">original tab</p>"""
with webdriver.Chrome(options=opts) as driver:
driver.get(LINKS)
original = driver.current_window_handle
print("handles before:", len(driver.window_handles))
driver.find_element(By.ID, "external").click()
WebDriverWait(driver, 10).until(EC.number_of_windows_to_be(2))
print("handles after: ", len(driver.window_handles))
print("still on:", driver.find_element(By.ID, "here").text) # not switched!
new_tab = next(h for h in driver.window_handles if h != original)
driver.switch_to.window(new_tab)
print("now on: ", driver.title)
driver.close()
driver.switch_to.window(original)
print("back on: ", driver.find_element(By.ID, "here").text)
handles before: 1
handles after: 2
still on: original tab
now on: React • TodoMVC
back on: original tab
The line that surprises people is the third: after the click, commands still go to the original tab. Selenium never switches for you.
Two habits make this safe. Wait for the window count with number_of_windows_to_be rather
than reading window_handles immediately — the tab may not exist yet. And after close(),
always switch_to.window(...) something: the driver is left pointing at a dead handle, and
the next command fails with NoSuchWindowException.
Selenium 4 can open a tab directly, which is cleaner when you are not testing the link itself:
driver.switch_to.new_window("tab")
driver.get("https://demo.playwright.dev/todomvc")
print("tab:", driver.title)
driver.close()
driver.switch_to.window(original)
driver.switch_to.new_window("window")
print("windows:", len(driver.window_handles))
driver.close()
driver.switch_to.window(original)
tab: React • TodoMVC
windows: 2
A helper is worth having, because the pattern recurs constantly:
from contextlib import contextmanager
@contextmanager
def new_tab(driver, trigger=None):
original = driver.current_window_handle
before = set(driver.window_handles)
if trigger:
trigger.click()
WebDriverWait(driver, 10).until(lambda d: len(d.window_handles) > len(before))
handle = (set(driver.window_handles) - before).pop()
else:
driver.switch_to.new_window("tab")
handle = driver.current_window_handle
driver.switch_to.window(handle)
try:
yield handle
finally:
driver.close()
driver.switch_to.window(original)
with new_tab(driver, driver.find_element(By.ID, "external")):
print("inside:", driver.title)
print("outside:", driver.find_element(By.ID, "here").text)
inside: React • TodoMVC
outside: original tab
The finally matters: a failing assertion inside the block still restores the original tab,
so the next test does not start in a stray window.
Iframes
FRAMES = """data:text/html,
<h1>Checkout</h1>
<iframe id="payment" name="payment-frame" srcdoc="
<label>Card number <input id='card'></label>
<button id='pay'>Pay</button>
<p id='status'>waiting</p>
"></iframe>
<p id="outer">outer page</p>"""
with webdriver.Chrome(options=opts) as driver:
driver.get(FRAMES)
driver.find_element(By.ID, "card")
selenium.common.exceptions.NoSuchElementException: Message: no such element: Unable to
locate element: {"method":"css selector","selector":"[id="card"]"}
The input is visible on screen and invisible to the driver. Switch first:
driver.switch_to.frame("payment-frame") # by name or id
driver.find_element(By.ID, "card").send_keys("4242424242424242")
print("inside frame:", driver.find_element(By.ID, "status").text)
driver.switch_to.default_content()
print("outside:", driver.find_element(By.ID, "outer").text)
inside frame: waiting
outside: outer page
Three ways to switch, in order of robustness:
driver.switch_to.frame(0) # index — fragile
driver.switch_to.frame("payment-frame") # name or id
driver.switch_to.frame(driver.find_element(By.CSS_SELECTOR, "iframe.payment")) # element
The element form is best, because it lets you locate the frame with the same care as anything else. It also composes with a wait:
WebDriverWait(driver, 10).until(
EC.frame_to_be_available_and_switch_to_it((By.ID, "payment"))
)
print("switched after waiting")
driver.switch_to.default_content()
switched after waiting
Nested frames need one switch per level, and parent_frame() goes up one:
driver.switch_to.frame("outer-frame")
driver.switch_to.frame("inner-frame")
# ... work in the inner frame
driver.switch_to.parent_frame() # back to outer
driver.switch_to.default_content() # all the way out
Forgetting to switch back is the classic bug. The next locator in your test looks fine and fails, and the error says nothing about frames. A context manager removes the problem:
@contextmanager
def in_frame(driver, locator):
WebDriverWait(driver, 10).until(EC.frame_to_be_available_and_switch_to_it(locator))
try:
yield
finally:
driver.switch_to.default_content()
with in_frame(driver, (By.ID, "payment")):
driver.find_element(By.ID, "card").send_keys("4242424242424242")
driver.find_element(By.ID, "pay").click()
print("back outside:", driver.find_element(By.ID, "outer").text)
back outside: outer page
Note that a frame switch is also invalidated by a page navigation — after driver.get() you
are back in the default content whether you asked to be or not.
Alerts
DIALOGS = """data:text/html,
<button id="del" onclick="result.textContent = confirm('Delete order?') ? 'deleted' : 'kept'">
Delete</button>
<button id="ask" onclick="result.textContent = prompt('Reason?', '') || 'none'">Ask</button>
<p id="result"></p>"""
with webdriver.Chrome(options=opts) as driver:
driver.get(DIALOGS)
driver.find_element(By.ID, "del").click()
alert = WebDriverWait(driver, 5).until(EC.alert_is_present())
print("alert says:", alert.text)
alert.accept()
print("result:", driver.find_element(By.ID, "result").text)
driver.find_element(By.ID, "del").click()
WebDriverWait(driver, 5).until(EC.alert_is_present()).dismiss()
print("result:", driver.find_element(By.ID, "result").text)
driver.find_element(By.ID, "ask").click()
prompt = driver.switch_to.alert
prompt.send_keys("damaged in transit")
prompt.accept()
print("result:", driver.find_element(By.ID, "result").text)
alert says: Delete order?
result: deleted
result: kept
result: damaged in transit
Both branches of a confirm are worth testing — “cancel actually cancels” is behaviour users rely on and almost nobody covers.
Leave a dialog open and everything else stops:
driver.find_element(By.ID, "del").click()
driver.find_element(By.ID, "result").text
selenium.common.exceptions.UnexpectedAlertPresentException: Message: unexpected alert open:
{Alert text : Delete order?}
(Session info: chrome=133.0.6943.16)
A modal dialog blocks the WebDriver session, not just the page. If a whole test file starts failing with this, one earlier test left a dialog open.
For pages that throw up dialogs you do not care about, set the behaviour once:
options = webdriver.ChromeOptions()
options.set_capability("unhandledPromptBehavior", "dismiss and notify")
Alert dismissed automatically; test continues
Values are dismiss, accept, dismiss and notify (the default), accept and notify and
ignore. Prefer handling dialogs explicitly in tests that are about the dialog, and use
this for incidental ones.
What Selenium cannot reach
| Dialog | Reachable? | Do instead |
|---|---|---|
alert / confirm / prompt | ✅ switch_to.alert | — |
| basic auth prompt | ❌ | https://user:pass@host/, or set a header via CDP |
| file picker | ❌ | send_keys(path) on the <input type="file"> |
| print dialog | ❌ | Chrome’s --kiosk-printing, or driver.print_page() |
| browser permission prompt | ❌ | grant it in browser options up front |
options.add_experimental_option("prefs", {
"profile.default_content_setting_values.geolocation": 1,
"profile.default_content_setting_values.notifications": 2, # 1 allow, 2 block
})
Geolocation granted, notifications blocked — no prompts appear
These are OS-level windows, so no amount of switch_to will find them. Configure them away
before they appear.
Practice
1. Click a target="_blank" link and assert on the new tab.
handles after: 2
still on: original tab
now on: React • TodoMVC
The assertion between the click and the switch proves Selenium stayed put. Forgetting to
switch produces a NoSuchElementException that looks like a locator problem.
2. Find an element inside an iframe without switching.
selenium.common.exceptions.NoSuchElementException: Message: no such element
Identical to the error for a genuinely missing element, which is why “is it in a frame?” should be an early question whenever a locator fails on an element you can see.
3. Switch into a frame and forget to switch back.
driver.switch_to.frame("payment-frame")
driver.find_element(By.ID, "card").send_keys("4242")
driver.find_element(By.ID, "outer").text
selenium.common.exceptions.NoSuchElementException: Message: no such element: Unable to
locate element: {"method":"css selector","selector":"[id="outer"]"}
The outer page is now the invisible one. A context manager with finally makes this
impossible to get wrong.
4. Trigger a confirm and leave it open.
selenium.common.exceptions.UnexpectedAlertPresentException: Message: unexpected alert open:
{Alert text : Delete order?}
The session is blocked, not just the page. When an entire test file fails this way, look for the earlier test that opened a dialog and never dismissed it.
Next: the Page Object Model — where locators live once a suite has more than ten tests.