Handling Multiple Windows and Tabs

8 min read

A click opens a payment popup. A target="_blank" link launches a new tab. A help-link opens documentation in a separate window. Each one is a new browsing context, and Selenium tracks them via opaque string identifiers called window handles. This lesson covers the handle model, the canonical pattern for switching to a freshly-opened window, and the Selenium 4 shortcut for opening one programmatically. Master these and you can drive any flow that spans more than one tab without losing your place.

What a window handle is

Every window or tab Selenium controls has a unique opaque string ID — its handle. The handles look like UUIDs (CDwindow-D7B1A3E2...) and have no semantic meaning beyond "this string identifies this window." You collect handles, you switch to one by handle, and the driver routes future commands to that window's document.

Two methods do most of the work:

  • driver.getWindowHandle() — the handle of the current window.
  • driver.getWindowHandles() — a Set<String> of every window the session controls.

The plural form returns a Set (not a List) because order is not guaranteed across calls or browsers. Don't reach for "the second one"; reach for "the one I haven't seen before."

The canonical "click opens new tab" pattern

import java.util.Set;
 
// 1. Remember where you started
String mainWindow = driver.getWindowHandle();
 
// 2. Trigger the action that opens the new window
driver.findElement(By.linkText("Open documentation")).click();
 
// 3. Wait for the new window to actually exist
wait.until(d -> d.getWindowHandles().size() > 1);
 
// 4. Switch to it — pick the handle that ISN'T the original
Set<String> all = driver.getWindowHandles();
for (String handle : all) {
    if (!handle.equals(mainWindow)) {
        driver.switchTo().window(handle);
        break;
    }
}
 
// 5. Do work in the new window
String docTitle = driver.getTitle();
Assert.assertTrue(docTitle.contains("Documentation"));
 
// 6. Close the new window when done
driver.close();      // close ONLY the current window — driver session stays alive
 
// 7. Switch back to the original
driver.switchTo().window(mainWindow);
 
// 8. Continue the test
driver.findElement(By.id("continue-checkout")).click();

Eight steps for what feels conceptually simple — that's why every Selenium codebase ends up with a helper. We'll build one shortly.

The critical detail is step 6 vs step 7: driver.close() closes the current window. driver.quit() ends the entire driver session — every window. Don't mix them up.

A helper that hides the boilerplate

public static void switchToNewWindow(WebDriver driver, String currentHandle) {
    new WebDriverWait(driver, Duration.ofSeconds(10))
        .until(d -> d.getWindowHandles().size() > 1);
    for (String handle : driver.getWindowHandles()) {
        if (!handle.equals(currentHandle)) {
            driver.switchTo().window(handle);
            return;
        }
    }
    throw new IllegalStateException("Expected a new window but none appeared");
}
 
// Usage
String main = driver.getWindowHandle();
driver.findElement(By.linkText("Help")).click();
switchToNewWindow(driver, main);
// ... work in the popup ...
driver.close();
driver.switchTo().window(main);

The same pattern as iframes: the verbose mechanics live in a helper, the test reads cleanly.

Selenium 4 — programmatic new window/tab

Selenium 4 added a clean API for opening windows yourself, no link-click required:

import org.openqa.selenium.WindowType;
 
// Open a new TAB and switch to it automatically
driver.switchTo().newWindow(WindowType.TAB);
driver.get("https://qa.codes/admin");
 
// Open a new WINDOW and switch to it
driver.switchTo().newWindow(WindowType.WINDOW);
driver.get("https://qa.codes/preview");

Two reasons this matters:

  1. The driver auto-switches the focus, so you skip the four lines of "find the new handle and switch."
  2. It's deterministic — you know exactly when the window appears, no race condition.

Use it when the test wants to open a parallel session (a viewer in one tab, an editor in another) without depending on the app's UI to do it.

Window management — size, position, fullscreen

Once focused on a window, you can resize/reposition it:

import org.openqa.selenium.Dimension;
import org.openqa.selenium.Point;
 
// Maximise — typical at the start of a test
driver.manage().window().maximize();
 
// Set an exact size — useful for responsive testing
driver.manage().window().setSize(new Dimension(375, 667));   // iPhone-ish
 
// Move the window
driver.manage().window().setPosition(new Point(0, 0));
 
// Fullscreen (F11 equivalent)
driver.manage().window().fullscreen();
 
// Read current size and position
Dimension size = driver.manage().window().getSize();
Point pos = driver.manage().window().getPosition();

For responsive testing across breakpoints, parameterise on a Dimension and run the same test at desktop, tablet, and mobile viewports.

The new-window flow, visualised

Step 1 of 6

Save main

String main = driver.getWindowHandle(); — capture the original handle so you can return later

A complete multi-window test

package com.mycompany.tests.tests;
 
import io.github.bonigarcia.wdm.WebDriverManager;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WindowType;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.testng.Assert;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
 
import java.time.Duration;
import java.util.Set;
 
public class WindowsTest {
 
    WebDriver driver;
    WebDriverWait wait;
 
    @BeforeMethod
    public void setup() {
        WebDriverManager.chromedriver().setup();
        driver = new ChromeDriver();
        wait = new WebDriverWait(driver, Duration.ofSeconds(10));
        driver.get("https://practice.expandtesting.com/windows");
    }
 
    @Test
    public void shouldOpenAndSwitchToNewWindowFromLink() {
        String main = driver.getWindowHandle();
 
        // The practice site has a link that opens a new window
        driver.findElement(By.linkText("Click Here")).click();
 
        wait.until(d -> d.getWindowHandles().size() > 1);
 
        for (String handle : driver.getWindowHandles()) {
            if (!handle.equals(main)) {
                driver.switchTo().window(handle);
                break;
            }
        }
 
        // The new window has its own heading
        Assert.assertTrue(driver.getPageSource().contains("New Window"));
 
        driver.close();
        driver.switchTo().window(main);
 
        // Still on the original page after returning
        Assert.assertTrue(driver.getCurrentUrl().contains("/windows"));
    }
 
    @Test
    public void shouldOpenNewTabProgrammatically() {
        String main = driver.getWindowHandle();
 
        // Selenium 4 — auto-switches to the new tab
        driver.switchTo().newWindow(WindowType.TAB);
        driver.get("https://qa.codes");
        Assert.assertTrue(driver.getCurrentUrl().contains("qa.codes"));
 
        // Close it, return to the original
        driver.close();
        driver.switchTo().window(main);
        Assert.assertTrue(driver.getCurrentUrl().contains("/windows"));
    }
 
    @Test
    public void shouldResizeWindow() {
        driver.manage().window().setSize(new org.openqa.selenium.Dimension(800, 600));
        Assert.assertEquals(driver.manage().window().getSize().getWidth(), 800);
        Assert.assertEquals(driver.manage().window().getSize().getHeight(), 600);
    }
 
    @AfterMethod
    public void teardown() {
        if (driver != null) driver.quit();
    }
}

Three tests covering the three cases you'll meet: link-opens-window, programmatic-new-tab, and window resize.

How Cypress and Playwright handle this

// Cypress — single-tab limitation. Most workarounds remove target="_blank" first
cy.get("a").invoke("removeAttr", "target").click();
// Now the link opens in the same tab, which Cypress can follow
 
// Playwright — first-class new-page handling
const [newPage] = await Promise.all([
  context.waitForEvent("page"),
  page.click("a"),
]);
await newPage.waitForLoadState();
await expect(newPage).toHaveTitle(/Documentation/);

Cypress's single-tab limitation is the biggest functional gap between it and Selenium/Playwright. Playwright's waitForEvent("page") is arguably the cleanest API of the three. Selenium's window-handle approach is the most verbose but the most explicit — and it works regardless of which browser you're driving.

The Selenium tool entry covers every switchTo() and manage().window() method.

⚠️ Common mistakes

  • driver.quit() when you meant driver.close(). Quit ends the whole session — every window plus the driver process. If you quit() after a popup test, the test is over and your @AfterMethod's second quit() does nothing. close() only closes the current window; the session lives on. Get this distinction right on day one.
  • Switching by index into the Set. getWindowHandles() returns a Set<String>, which has no ordering guarantee. Code like windows.toArray()[1] may pick a different window between runs and across browsers. Always pick by which handle isn't the one you started in.
  • Forgetting to switch back to the original window. Just like iframes, leaving the driver focused on a closed or background window means the next findElement searches the wrong document — or throws if the window was closed. Pair every "switch to new" with a switchTo().window(originalHandle) once you're done.

🎯 Practice task

Drive multi-window flows. 30–40 minutes.

  1. Add WindowsTest from this lesson to your project. Run all three tests; all should pass.
  2. Build the helper. Add WindowUtils.switchToNewWindow(driver, main) from earlier in the lesson to your base/ package. Refactor shouldOpenAndSwitchToNewWindowFromLink to use it. The test code drops to ~6 lines.
  3. Three open at once. Use driver.switchTo().newWindow(WindowType.TAB) three times in a row to open qa.codes, qa.codes/learn, qa.codes/tools. Then iterate getWindowHandles() and getTitle() for each, switching between them. This is the test pattern for "log in once, then open multiple admin tools."
  4. Cycle and close. From the same setup, write a method that closes every window except the original. Call getWindowHandles(), iterate, skip the main handle, switch to each other and close(). Verify only one window remains.
  5. Quit vs close — feel the difference. Replace driver.close() with driver.quit() in shouldOpenAndSwitchToNewWindowFromLink. Run the test. The driver.switchTo().window(main) call now throws — the session is dead. Restore close() and watch the test pass again.
  6. Stretch — responsive testing. Parameterise shouldResizeWindow over three viewport sizes — 1920×1080 (desktop), 768×1024 (tablet), 375×667 (mobile). For each viewport, navigate to a real responsive site (Bootstrap docs, your own app) and assert that a known mobile-only or desktop-only element is/isn't visible. This is the foundation of cross-viewport testing — you'll formalise it via @DataProvider in chapter 5.

Chapter 4 is done. You can drive any complex interaction Selenium will throw at you — hover, drag-drop, alerts, iframes, multiple windows. Chapter 5 takes a step back from interactions and goes deep on TestNG itself: annotations, suites, groups, dependencies, data providers, listeners. The framework that organises all the test code you've been writing.

// tip to track lessons you complete and pick up where you left off across devices.