How to Handle Multiple Windows in Selenium with Protractor
Switch Protractor to a newly opened window by saving the parent handle, waiting for a new handle, and switching back after the child closes.
To handle multiple windows or tabs in a Protractor test, save the current window handle, trigger the action that opens another context, wait until a new handle appears, and explicitly switch to it. After working in the child window, close it if needed and switch back to the saved parent handle. WebDriver does not automatically select a new context just because the browser visibly opened or focused it.
This is a maintenance recipe for existing Protractor suites. Protractor reached end-of-life in August 2023; its project recommends that new users choose another end-to-end testing solution and existing users migrate. Protractor project lifecycle notice
1. The handle-based workflow
- Read and save the original handle before clicking.
- Take a snapshot of all existing handles. This lets you distinguish a new popup from tabs that were already open.
- Trigger the link or action that opens the window.
- Wait until the number of handles increases.
- Read the handles again and select one absent from the pre-action snapshot.
- Switch explicitly with
browser.switchTo().window(childHandle). - When finished, close the child if appropriate, then switch back to the original handle.
A handle is an identifier for a top-level browsing context. Treat it as opaque: compare handles for equality, but do not infer that a particular array position always represents a particular window.
2. Protractor example: wait, switch, and return
This example uses native async/await and the Protractor-style global browser and element. It is an illustrative maintenance pattern based on Selenium’s documented workflow, not a verified compatibility promise for every historical Protractor and Selenium version.
it('switches to a newly opened window and returns', async function () {
const original = await browser.getWindowHandle();
const before = await browser.getAllWindowHandles();
await element(by.css('a.opens-popup')).click();
await browser.wait(async function () {
const current = await browser.getAllWindowHandles();
return current.length > before.length;
}, 10000);
const after = await browser.getAllWindowHandles();
const child = after.find(handle => !before.includes(handle));
if (!child) {
throw new Error('A new window handle did not appear');
}
await browser.switchTo().window(child);
// Interact with or assert against the child window here.
// Example: await expect(element(by.css('h1')).getText()).to.eventually.equal('Popup');
await browser.driver.close();
await browser.switchTo().window(original);
});
The wait is important because opening a window is asynchronous. The example compares against the whole initial snapshot, so it remains correct when the test already has more than one context. If your test expects a specific number of contexts, wait for that exact count instead and validate it before switching.
Use cleanup so failures do not strand the test
If an assertion fails before the child is closed, the browser may remain in the child context and contaminate later steps. Put cleanup in a finally block when the test structure allows it. Check that the child still exists before closing, and only switch to the original if it remains open.
const original = await browser.getWindowHandle();
const before = await browser.getAllWindowHandles();
let child;
try {
await element(by.css('a.opens-popup')).click();
await browser.wait(async function () {
const handles = await browser.getAllWindowHandles();
return handles.some(handle => !before.includes(handle));
}, 10000);
const after = await browser.getAllWindowHandles();
child = after.find(handle => !before.includes(handle));
if (!child) throw new Error('Popup did not open');
await browser.switchTo().window(child);
// Assertions and interactions in the child context.
} finally {
const openHandles = await browser.getAllWindowHandles();
if (child && openHandles.includes(child)) {
await browser.switchTo().window(child);
await browser.driver.close();
}
const remaining = await browser.getAllWindowHandles();
if (remaining.includes(original)) {
await browser.switchTo().window(original);
}
}
Adapt cleanup to your test runner: a session-level failure or a browser that has already quit can also make cleanup commands fail. In that case, report the original failure and let the runner tear down the session.
3. Existing tabs and multiple popups
When multiple contexts are already open, select the new handle by set difference rather than assuming handles[1] is the popup. If one action may open more than one context, compute all newly added handles and decide which is the intended target by checking page content or URL after switching.
const before = await browser.getAllWindowHandles();
await element(by.css('button.open-windows')).click();
await browser.wait(async function () {
const now = await browser.getAllWindowHandles();
return now.length > before.length;
}, 10000);
const now = await browser.getAllWindowHandles();
const added = now.filter(handle => !before.includes(handle));
if (added.length !== 1) {
throw new Error(`Expected one new context, found ${added.length}`);
}
await browser.switchTo().window(added[0]);
For several expected windows, wait until handles.length reaches the expected count, then inspect the full added set. Do not use window ordering as a substitute for identifying the target.
4. cURL, Python, and Node.js examples
These examples show the underlying Selenium WebDriver window-handle workflow outside Protractor. They use Selenium 4 style APIs; install Selenium and the browser driver supported by your environment. They illustrate the same sequence: save, trigger, wait, identify, switch, close, return.
cURL
cURL cannot switch a browser’s WebDriver context by itself. It can call a WebDriver remote server’s HTTP endpoints if you already manage the session and protocol details. For a maintainable test, use a Selenium client binding such as Python or Node.js below instead of manually constructing session-specific WebDriver commands.
Python
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
# Requires Selenium 4 and a compatible browser/driver installation.
driver = webdriver.Chrome()
try:
driver.get("https://example.com")
original = driver.current_window_handle
before = set(driver.window_handles)
driver.find_element(By.CSS_SELECTOR, "a.opens-popup").click()
WebDriverWait(driver, 10).until(
lambda d: len(set(d.window_handles) - before) > 0
)
added = set(driver.window_handles) - before
if len(added) != 1:
raise RuntimeError(f"Expected one new window, found {len(added)}")
child = added.pop()
driver.switch_to.window(child)
# Interact with or assert against the child window here.
finally:
handles = driver.window_handles
for handle in handles:
if handle != original:
driver.switch_to.window(handle)
driver.close()
if original in driver.window_handles:
driver.switch_to.window(original)
driver.quit()
Node.js
const { Builder, By, until } = require('selenium-webdriver');
(async function handlePopup() {
const driver = await new Builder().forBrowser('chrome').build();
let original;
try {
await driver.get('https://example.com');
original = await driver.getWindowHandle();
const before = new Set(await driver.getAllWindowHandles());
await driver.findElement(By.css('a.opens-popup')).click();
await driver.wait(async () => {
const handles = await driver.getAllWindowHandles();
return handles.some(handle => !before.has(handle));
}, 10000);
const handles = await driver.getAllWindowHandles();
const added = handles.filter(handle => !before.has(handle));
if (added.length !== 1) {
throw new Error(`Expected one new window, found ${added.length}`);
}
await driver.switchTo().window(added[0]);
// Interact with or assert against the child window here.
} finally {
if (original) {
const handles = await driver.getAllWindowHandles();
for (const handle of handles) {
if (handle !== original) {
await driver.switchTo().window(handle);
await driver.close();
}
}
const remaining = await driver.getAllWindowHandles();
if (remaining.includes(original)) {
await driver.switchTo().window(original);
}
}
await driver.quit();
}
})();
5. Protractor and Selenium version compatibility
Before copying code into an older suite, check the versions pinned in its package lock and test configuration. The Protractor project is archived, while Selenium’s current guidance documents modern WebDriver behavior. That does not establish that a current Selenium example works unchanged with every historical Protractor, Selenium, browser, and driver combination.
Some older projects used promise chains or legacy WebDriver control-flow behavior. If native async/await causes syntax or promise-handling errors, follow the syntax supported by the project’s pinned runtime and client versions. The handle logic remains the same: snapshot, wait, compare, switch, close, return.
6. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| The popup is visible, but commands still affect the original page. | WebDriver remains selected on the original context. | Identify the new handle and call switchTo().window(handle) before interacting. |
| The handle list has no popup immediately after the click. | The browser has not finished creating the new context. | Wait for the handle count or set difference to change; use a timeout appropriate to the application and environment. |
| The test picks the wrong tab intermittently. | It assumes a fixed handle index or there are pre-existing contexts. | Snapshot all handles before the action and select a handle absent from that snapshot. |
A command throws No Such Window. |
The selected context was closed, or the browser closed it during navigation. | Read the open handles and switch to one that still exists before issuing more commands. If the parent is gone, fail with a clear test error. |
| Closing the popup leaves later steps failing. | Closing a child does not automatically select the parent. | Explicitly switch to the saved parent handle after closing the child. |
| The wait times out although the site appears to open a popup. | The action may open a same-tab navigation, the browser may block the popup, or the selector may target the wrong control. | Confirm the application actually creates a separate top-level context, check browser popup behavior, and verify the click target and test environment. |
child is undefined or no new handle is found. |
The code reads handles before the wait succeeds, or the action opened no new context. | Only select after the wait; fail explicitly if the set difference is empty. |
| Syntax or promise errors appear in an old Protractor project. | The example’s async style may not match pinned runtime or package versions. | Inspect the project’s Protractor, Selenium, Node.js, browser, and driver versions and adapt the asynchronous syntax accordingly. |
7. Reliability, speed, and cost
Window switching adds little work compared with starting a browser or loading the page; the main reliability risk is timing. Wait on a state change rather than adding a fixed sleep, and keep the timeout bounded so a blocked or missing popup fails promptly. Close temporary contexts and restore the parent in cleanup to prevent state leaking into later tests.
For large suites, reuse the existing browser session where the test runner supports it, and avoid opening extra windows unless the application behavior requires them. This reduces setup work and makes failures easier to isolate. Test execution cost depends on your own browser and CI environment; the research sources provide no benchmark or cost figure.
8. Or skip the browser setup
If the goal is to capture the page rather than test interactions across contexts, ScreenshotNeo provides a website screenshot API and MCP server. A single GET request returns an image or PDF. For a public screenshot, use the documented API call below; see the ScreenshotNeo API documentation for options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
- Cookie banners, popups, and chat widgets are removed before the shot.
- Bot checks, blank pages, and failed loads are never billed.
- An MCP server lets AI agents use screenshot tools.
- 1,000 screenshots a month are free with no card; paid plans start at $5 for 3,000.
Sign up for 1,000 free screenshots a month, with no card required.
9. FAQ
Does a new browser tab count as a new window handle?
Yes. WebDriver represents tabs and separate browser windows as top-level browsing contexts, each with a handle.
Does switching handles create a window?
No. Switching selects an already existing context. The page action or browser operation must create it first.
Should I use Protractor for a new test suite?
No. The Protractor project says it reached end-of-life in August 2023 and recommends other end-to-end testing solutions for new users.
Can I use this pattern if an action opens two popups?
Yes. Compare the post-action handles with the full pre-action snapshot, then identify the intended new context using application-specific information such as its URL or page content.


