In Selenium, a portal link opens a second tab. How do you obtain that new tab's window handle?
answer
- Handles carry no ordering contract
- Compare the world before and after
- Set arithmetic, never an index
- removeAll against the earlier snapshot
- Whatever handle is newly present
basics
~20 sSnapshot getWindowHandles() before the click, take the same set afterwards, and subtract the first from the second. The single handle left over is the new tab. Handles are opaque and unordered, so indexing into the set is unreliable.
solid answer
~40 sWindow handles are opaque strings and `getWindowHandles()` returns a `Set<String>` with no ordering contract, so "the new tab is index 1" is an assumption the API never made. Instead take a snapshot before the action, perform the click, copy the handles afterwards into a mutable `Set`, and call `removeAll(before)`. What remains is exactly the handles that did not exist before; assert it holds one entry and pass it to `switchTo().window(...)`. Save your own `getWindowHandle()` before you leave, because that saved string is the only reliable way back. Note that the new handle is not guaranteed to be present the instant `click()` returns — synchronising on its arrival is a separate waiting concern from the arithmetic itself.
code
java · 30 linesimport java.util.HashSet;
import java.util.Set;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
public final class PermitPortalTabs {
private PermitPortalTabs() {
}
public static String handleOpenedBy(WebDriver driver, By link) {
Set<String> before = driver.getWindowHandles();
driver.findElement(link).click();
Set<String> opened = new HashSet<>(driver.getWindowHandles());
opened.removeAll(before);
if (opened.size() != 1) {
throw new IllegalStateException("expected one new window handle, got " + opened);
}
return opened.iterator().next();
}
public static void readSitePlan(WebDriver driver) {
String statusPage = driver.getWindowHandle();
String planTab = handleOpenedBy(driver, By.linkText("View submitted site plan"));
driver.switchTo().window(planTab);
System.out.println(driver.getTitle());
driver.close();
driver.switchTo().window(statusPage);
}
}go deeper
Know the two methods apart: getWindowHandle() gives the one you are on, getWindowHandles() gives them all. Remember that the plural one is a Set, so it has no first or last entry.
Explain the before-and-after subtraction and why it is exact where an index is a guess. Be ready to say what a handle is not: not a title, not a URL, not a tab position.
Show the discipline around it: assert the difference holds exactly one handle, keep the origin handle for the return switch, and treat a surprising count as a defect to surface rather than a case to work around.
Own whether flows that open extra tabs are worth automating through the UI at all, and what a suite-wide helper for context arithmetic must guarantee so that every team using it fails loudly instead of silently on the wrong page.
## What a window handle actually is A **window handle** is an opaque string the remote end mints for each open top-level browsing context — each window and each tab. Selenium hands it to you and expects it back; the string itself carries no information you are entitled to parse. It is not the page title, not the URL, not the value of the window's name, and not a position on a tab strip. In the Java binding two methods produce them: - `driver.getWindowHandle()` returns a single `String`: the handle of the **one context commands are currently going to**. It says nothing about how many other contexts are open. - `driver.getWindowHandles()` returns a `Set<String>`: every currently open top-level context in this session, including the one you are focused on. `Set` is the key word. There is no ordering contract, so "the newest tab is last" is an assumption the API never made you. ## Why an index is not an answer | Approach | What it assumes | Why it fails | |---|---|---| | `new ArrayList<>(handles).get(1)` | the set iterates oldest-first | iteration order is unspecified; a third open context silently shifts the index | | `handles.stream().skip(1).findFirst()` | the same, dressed up | identical assumption, identical failure | | Switch by page title | `switchTo().window` accepts a title | the command is defined over handles; a title raises `NoSuchWindowException` | | Set difference | only that a new handle is new | nothing else is needed, and it is exact | The index recipe usually passes on a clean two-tab run, which is why it survives code review and then fails on the machine where an extra context is open. ## The set-difference recipe On the municipal permit portal, the applicant's status page carries a "View submitted site plan" link that opens the plan in its own tab. Finding that tab is arithmetic, not indexing: 1. Snapshot before the action: `Set<String> before = driver.getWindowHandles();`. 2. Save your own position: `String statusPage = driver.getWindowHandle();`. 3. Perform the action that opens the context — click the site-plan link. 4. Snapshot after, into a **mutable copy**: `Set<String> opened = new HashSet<>(driver.getWindowHandles());`. 5. Subtract: `opened.removeAll(before);`. What remains is exactly the handles that did not exist before. 6. Assert there is exactly one, then `driver.switchTo().window(opened.iterator().next());`. Step 4 matters in Java: `getWindowHandles()` may return a set you should not mutate, so copy it before calling `removeAll`. Step 6 matters for diagnosis: if the difference is empty or holds two entries, you want a clear failure at that line rather than a confusing error three commands later. ## Where the recipe still needs care - **The handle may not exist the instant `click()` returns.** Opening a context is the browser's work, not the driver's, so a single snapshot taken immediately afterwards can come back unchanged. Synchronising on that is a waiting concern, separate from the handle arithmetic itself. - **More than one new handle** means the page opened two contexts, or a leftover from an earlier step is still open. Fail loudly; do not pick one. - **`getWindowHandle()` is not a shortcut.** After the click, focus is still on the status page: the driver does not follow the browser's visual focus into whatever tab the user would now be looking at. - **Handles die.** Once a context is closed its handle disappears from `getWindowHandles()`, and reusing the saved string raises `NoSuchWindowException`. - **Keep the way home.** The origin handle from step 2 is what you switch back to; recomputing it later from the set is guesswork. ## Getting back, and closing the loop The full round trip on the portal reads: save `statusPage`, click the link, difference the sets, switch to the plan tab, assert the plan's permit number matches the application, `driver.close()` the plan tab, then `driver.switchTo().window(statusPage)`. The symmetry is the point — every switch away has a matching switch back, and the handle you switch back with was captured before you left, never derived afterwards. One last framing that makes the whole leaf click: a handle set is a *set of identities*, so every question you ask of it should be set arithmetic — what is new, what is gone, is this one still a member. The moment your code sorts, indexes or slices it, you have invented an ordering the browser never promised.
- Why copy the result of getWindowHandles() before calling removeAll on it?`removeAll` mutates the receiver, and the set the binding returns should be treated as the driver's own snapshot rather than a scratch collection. Copying into a `new HashSet<>(...)` keeps the subtraction local, keeps the before-snapshot intact for comparison, and makes the intent obvious to a reader.
- The difference comes back with two handles instead of one. What do you do?Fail at that line with a message naming both handles. Two new contexts means the page opened more than you expected or a previous step leaked a tab; picking one arbitrarily makes the test assert against whichever context happened to win. A loud failure at the point of surprise is far cheaper to diagnose than a wrong-page assertion later.
Window handles are cloakroom tickets: the number tells you nothing about the coat, and the pile at the counter is in no particular order, so you identify the new ticket by comparing the list you held before with the list you hold now.
saying these in an interview costs you the question
- Treats getWindowHandles() as an ordered list of tabs
- Takes index 1 assuming the newest handle is last
- Switches by page title instead of by handle
- Thinks getWindowHandle() returns the most recent context
- Never saves the origin handle needed to switch back