Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions docs-site/docs/recipes/handle-new-windows.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
description: "Test a link that opens in a new tab without hand-rolling window handles: withNewWindow switches to it, runs your checks, closes it and restores the original window."
id: handle-new-windows
title: Handle new windows and tabs
sidebar_label: Handle new windows and tabs
---

# Handle new windows and tabs

A link with `target="_blank"` opens a new tab, and every later driver call still targets the old one until you switch. `BasePage`'s `withNewWindow(opener, body)` does the handle bookkeeping: it runs `opener` (the click), switches to the window that opened, runs `body` there, closes it, and switches back to the original window — even if `body` throws.

```java title="FooterPage.java"
public void verifyTermsOpensInNewTab() {
withNewWindow(
() -> click(By.linkText("Terms")),
() -> assertEquals(driver.getTitle(), "Terms of Service"));
// back on the original window here
}
```

`withNewTab(opener, body)` is an alias — browsers open both the same way.

If no new window appears within `timeouts.explicit`, a `TimeoutException` is thrown and `body` does not run, so a link that silently fails to open fails the test rather than passing it.
1 change: 1 addition & 0 deletions docs-site/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ const sidebars = {
'recipes/upload-a-file',
'recipes/download-and-verify-a-pdf',
'recipes/handle-iframes',
'recipes/handle-new-windows',
'recipes/handle-shadow-dom',
],
},
Expand Down
53 changes: 53 additions & 0 deletions src/main/java/com/seleniumboot/test/BasePage.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
import java.io.File;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.HashSet;
import java.util.Set;

/**
* Base class for all page objects.
Expand Down Expand Up @@ -333,6 +335,57 @@ protected WebElement smartFind(By primary, By... fallbacks) {
return SmartLocator.find(driver, all);
}

// ----------------------------------------------------------
// Window / tab helpers
// ----------------------------------------------------------

/**
* Runs {@code opener} (the click that opens a new window or tab), switches to the window it
* opened, runs {@code body} there, then closes that window and switches back to the original.
* The original window is restored even if {@code opener} or {@code body} throws.
*
* <pre>
* withNewWindow(() -> click(By.linkText("Terms")), () -> {
* assertEquals(driver.getTitle(), "Terms of Service");
* });
* // back on the original window here
* </pre>
*
* @throws org.openqa.selenium.TimeoutException if no new window appears within
* {@code timeouts.explicit} — the body is not run, so a missing window fails the test
* instead of passing it silently
*/
protected void withNewWindow(Runnable opener, Runnable body) {
String original = driver.getWindowHandle();
Set<String> before = new HashSet<>(driver.getWindowHandles());
String opened = null;
try {
opener.run();
int timeout = SeleniumBootContext.getConfig().getTimeouts().getExplicit();
opened = new WebDriverWait(driver, Duration.ofSeconds(timeout)).until(d -> {
Set<String> added = new HashSet<>(d.getWindowHandles());
added.removeAll(before);
return added.isEmpty() ? null : added.iterator().next();
});
driver.switchTo().window(opened);
body.run();
} finally {
try {
if (opened != null && driver.getWindowHandles().contains(opened)) {
driver.switchTo().window(opened);
driver.close();
}
} finally {
driver.switchTo().window(original);
}
}
}

/** Alias for {@link #withNewWindow(Runnable, Runnable)} — browsers open both the same way. */
protected void withNewTab(Runnable opener, Runnable body) {
withNewWindow(opener, body);
}

// ----------------------------------------------------------
// iFrame helpers
// ----------------------------------------------------------
Expand Down
112 changes: 112 additions & 0 deletions src/test/java/com/seleniumboot/unit/BasePageNewWindowTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package com.seleniumboot.unit;

import com.seleniumboot.config.SeleniumBootConfig;
import com.seleniumboot.internal.SeleniumBootContext;
import com.seleniumboot.test.BasePage;
import org.mockito.MockedStatic;
import org.openqa.selenium.TimeoutException;
import org.openqa.selenium.WebDriver;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;

import static org.mockito.Mockito.*;
import static org.testng.Assert.*;

/** {@code withNewWindow} / {@code withNewTab}: switch to the opened window, always restore the original. */
public class BasePageNewWindowTest {

private WebDriver driver;
private WebDriver.TargetLocator target;
private MockedStatic<SeleniumBootContext> contextMock;
private Page page;

private static final class Page extends BasePage {
Page(WebDriver d) { super(d); }
void window(Runnable o, Runnable b) { withNewWindow(o, b); }
void tab(Runnable o, Runnable b) { withNewTab(o, b); }
}

@BeforeMethod
public void setup() {
driver = mock(WebDriver.class);
target = mock(WebDriver.TargetLocator.class);
when(driver.switchTo()).thenReturn(target);
when(driver.getWindowHandle()).thenReturn("main");
when(driver.getWindowHandles()).thenReturn(Set.of("main"));

SeleniumBootConfig.Timeouts timeouts = new SeleniumBootConfig.Timeouts();
timeouts.setExplicit(1);
SeleniumBootConfig config = new SeleniumBootConfig();
config.setTimeouts(timeouts);
contextMock = mockStatic(SeleniumBootContext.class);
contextMock.when(SeleniumBootContext::getConfig).thenReturn(config);
page = new Page(driver);
}

@AfterMethod
public void teardown() { contextMock.close(); }

private void openingWindowAddsHandle() {
when(driver.getWindowHandles()).thenReturn(Set.of("main")).thenReturn(new LinkedHashSet<>(List.of("main", "popup")));
}

@Test
public void newWindow_switchesRunsBodyClosesItAndRestoresOriginal() {
openingWindowAddsHandle();
AtomicBoolean ran = new AtomicBoolean();

page.window(() -> {}, () -> {
ran.set(true);
verify(target, atLeastOnce()).window("popup");
});

assertTrue(ran.get());
verify(driver).close();
verify(target, atLeastOnce()).window("main");
}

@Test
public void withNewTab_behavesTheSame() {
openingWindowAddsHandle();
page.tab(() -> {}, () -> {});
verify(target, atLeastOnce()).window("popup");
verify(target, atLeastOnce()).window("main");
}

@Test
public void noNewWindow_failsLoudlyWithoutRunningBodyAndStaysOnOriginal() {
AtomicBoolean ran = new AtomicBoolean();

assertThrows(TimeoutException.class, () -> page.window(() -> {}, () -> ran.set(true)));

assertFalse(ran.get());
verify(driver, never()).close();
verify(target).window("main");
}

@Test
public void bodyThrows_stillClosesNewWindowAndRestoresOriginal() {
openingWindowAddsHandle();

assertThrows(IllegalStateException.class,
() -> page.window(() -> {}, () -> { throw new IllegalStateException("boom"); }));

verify(driver).close();
verify(target, atLeastOnce()).window("main");
}

@Test
public void openerThrows_restoresOriginal() {
assertThrows(IllegalStateException.class,
() -> page.window(() -> { throw new IllegalStateException("click failed"); }, () -> {}));

verify(target).window("main");
verify(driver, never()).close();
}
}
Loading