Skip to main content
Selenium beginner Lesson 5 of 10

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

DialogReachable?Do instead
alert / confirm / promptswitch_to.alert
basic auth prompthttps://user:pass@host/, or set a header via CDP
file pickersend_keys(path) on the <input type="file">
print dialogChrome’s --kiosk-printing, or driver.print_page()
browser permission promptgrant 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.

Frequently Asked Questions

Why can't Selenium find an element that is clearly on the page?
It is probably inside an iframe. WebDriver commands apply to one browsing context at a time, and the default context does not see into frames. Call `driver.switch_to.frame(...)` first, and `switch_to.default_content()` when you are done.
How do I handle a new tab opened by a link?
Record `driver.window_handles` before the click, wait for the count to change, then switch to the new handle. Selenium never switches automatically — after the click your commands still go to the original tab.
Why does my test throw UnexpectedAlertPresentException?
A native `alert`, `confirm` or `prompt` is open, and WebDriver blocks almost every other command while one is. Switch to the alert and accept or dismiss it before continuing, or set the unhandled prompt behaviour capability.
Can Selenium interact with the browser's file picker or a print dialog?
No. Those are operating-system windows, not page content, so WebDriver cannot see them. Avoid triggering them: send a file path to the `<input type="file">` directly, and use Chrome options to print to PDF without a dialog.