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
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@ public WebDriver createDriver() {
// Keep alerts open until the test explicitly handles them.
options.setCapability("unhandledPromptBehavior", "ignore");

// Enable BiDi so WaitEngine.waitForNetworkIdle()/waitForResponse() work out of the box —
// without this, HasBiDi.getBiDi() throws BiDiException instead of the clean
// UnsupportedOperationException NetworkActivityTracker reserves for truly unsupported
// drivers. An explicit "webSocketUrl" in the user's own capabilities still wins.
if (capabilities == null || !capabilities.containsKey("webSocketUrl")) {
options.setCapability("webSocketUrl", true);
}

WebDriver driver = new FirefoxDriver(options);
driver.manage().timeouts().implicitlyWait(Duration.ZERO);

Expand Down
167 changes: 167 additions & 0 deletions src/main/java/com/seleniumboot/wait/NetworkActivityTracker.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package com.seleniumboot.wait;

import com.seleniumboot.network.NetworkMock;
import org.openqa.selenium.TimeoutException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.bidi.HasBiDi;
import org.openqa.selenium.bidi.module.Network;
import org.openqa.selenium.chromium.ChromiumDriver;
import org.openqa.selenium.devtools.DevTools;
import org.openqa.selenium.devtools.v153.network.model.LoadingFailed;
import org.openqa.selenium.devtools.v153.network.model.LoadingFinished;
import org.openqa.selenium.devtools.v153.network.model.RequestWillBeSent;
import org.openqa.selenium.devtools.v153.network.model.ResponseReceived;

import java.time.Duration;
import java.time.Instant;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
* Backs {@link WaitEngine#waitForNetworkIdle} and {@link WaitEngine#waitForResponse}.
*
* <p>CDP (Chrome/Edge) and BiDi (Firefox, or any other {@code HasBiDi} driver) wiring is
* covered by manual verification against real browsers, not unit tests — same convention
* as {@link NetworkMock}. {@link IdleState}, the poll condition both paths feed, is pure
* and unit-tested directly.
*/
final class NetworkActivityTracker {

private NetworkActivityTracker() {
}

static void awaitIdle(WebDriver driver, Duration quietFor, Duration timeout) {
IdleState state = new IdleState();
if (driver instanceof ChromiumDriver chromium) {
try (DevTools devTools = chromium.getDevTools()) {
devTools.createSession();
devTools.send(org.openqa.selenium.devtools.v153.network.Network.enable(
Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty()));
devTools.addListener(org.openqa.selenium.devtools.v153.network.Network.requestWillBeSent(),
(RequestWillBeSent event) -> state.requestStarted());
devTools.addListener(org.openqa.selenium.devtools.v153.network.Network.loadingFinished(),
(LoadingFinished event) -> state.requestEnded());
devTools.addListener(org.openqa.selenium.devtools.v153.network.Network.loadingFailed(),
(LoadingFailed event) -> state.requestEnded());
pollUntilIdle(state, quietFor, timeout);
}
} else if (driver instanceof HasBiDi) {
try (Network network = new Network(driver)) {
network.onBeforeRequestSent(event -> state.requestStarted());
network.onResponseCompleted(event -> state.requestEnded());
network.onFetchError(event -> state.requestEnded());
pollUntilIdle(state, quietFor, timeout);
}
} else {
throw unsupported(driver);
}
}

static int awaitResponse(WebDriver driver, String urlPattern, Duration timeout) {
ResponseLatch latch = new ResponseLatch();
if (driver instanceof ChromiumDriver chromium) {
try (DevTools devTools = chromium.getDevTools()) {
devTools.createSession();
devTools.send(org.openqa.selenium.devtools.v153.network.Network.enable(
Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty()));
devTools.addListener(org.openqa.selenium.devtools.v153.network.Network.responseReceived(),
(ResponseReceived event) -> {
String url = event.getResponse().getUrl();
if (NetworkMock.matches(urlPattern, url)) {
latch.complete(event.getResponse().getStatus());
}
});
return latch.await(timeout);
}
} else if (driver instanceof HasBiDi) {
try (Network network = new Network(driver)) {
network.onResponseCompleted(event -> {
String url = event.getResponseData().getUrl();
if (NetworkMock.matches(urlPattern, url)) {
latch.complete(event.getResponseData().getStatus());
}
});
return latch.await(timeout);
}
} else {
throw unsupported(driver);
}
}

private static void pollUntilIdle(IdleState state, Duration quietFor, Duration timeout) {
Instant deadline = Instant.now().plus(timeout);
while (Instant.now().isBefore(deadline)) {
if (state.isIdle(quietFor)) return;
sleep(100);
}
throw new TimeoutException("waitForNetworkIdle: timed out after " + timeout
+ " with " + state.inFlightCount() + " request(s) still in flight");
}

private static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}

private static UnsupportedOperationException unsupported(WebDriver driver) {
return new UnsupportedOperationException(
"waitForNetworkIdle/waitForResponse need CDP (Chrome/Edge) or BiDi support; got "
+ driver.getClass().getSimpleName());
}

// ------------------------------------------------------------------
// Pure, unit-tested pieces
// ------------------------------------------------------------------

/** In-flight request counter + last-activity timestamp. Idle = none in flight, quiet long enough. */
static final class IdleState {
private final AtomicInteger inFlight = new AtomicInteger();
private volatile long lastActivityNanos = System.nanoTime();

void requestStarted() {
inFlight.incrementAndGet();
lastActivityNanos = System.nanoTime();
}

void requestEnded() {
inFlight.decrementAndGet();
lastActivityNanos = System.nanoTime();
}

boolean isIdle(Duration quietFor) {
return inFlight.get() <= 0 && (System.nanoTime() - lastActivityNanos) >= quietFor.toNanos();
}

int inFlightCount() {
return inFlight.get();
}
}

/** Resolves once a matching response is observed, or times out. */
static final class ResponseLatch {
private final CompletableFuture<Integer> future = new CompletableFuture<>();

void complete(int status) {
future.complete(status);
}

int await(Duration timeout) {
try {
return future.get(timeout.toMillis(), TimeUnit.MILLISECONDS);
} catch (java.util.concurrent.TimeoutException e) {
throw new TimeoutException("waitForResponse: no matching response within " + timeout);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new TimeoutException("waitForResponse: interrupted while waiting", e);
} catch (ExecutionException e) {
throw new RuntimeException("waitForResponse: listener failed", e.getCause());
}
}
}
}
54 changes: 54 additions & 0 deletions src/main/java/com/seleniumboot/wait/WaitEngine.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
@SeleniumBootApi(since = "0.4.0")
public final class WaitEngine {

private static final Duration DEFAULT_NETWORK_QUIET_FOR = Duration.ofMillis(500);

private WaitEngine() {
}

Expand Down Expand Up @@ -248,6 +250,58 @@ public static void waitForReactHydration() {
});
}

// ----------------------------------------------------------
// Network
// ----------------------------------------------------------

/**
* Waits until the browser has no in-flight network request for {@link #DEFAULT_NETWORK_QUIET_FOR}
* (500ms), bounded by the configured explicit timeout. Supported on Chrome/Edge (via CDP) and
* any BiDi-capable browser such as Firefox; throws {@link UnsupportedOperationException} on others.
*
* <pre>
* page.clickLoadMore();
* WaitEngine.waitForNetworkIdle();
* </pre>
*/
public static void waitForNetworkIdle() {
waitForNetworkIdle(DEFAULT_NETWORK_QUIET_FOR);
}

/**
* Like {@link #waitForNetworkIdle()}, but with a custom quiet period instead of the 500ms default.
*/
public static void waitForNetworkIdle(Duration quietFor) {
NetworkActivityTracker.awaitIdle(DriverManager.getDriver(), quietFor, explicitTimeout());
}

/**
* Waits until a response whose URL matches {@code urlPattern} is observed, and returns its
* HTTP status code. Bounded by the configured explicit timeout. Same glob syntax as
* {@link com.seleniumboot.network.NetworkMock#stub(String)} ({@code *} within a path segment,
* {@code **} across segments). Supported on Chrome/Edge (via CDP) and any BiDi-capable browser
* such as Firefox; throws {@link UnsupportedOperationException} on others.
*
* <pre>
* page.clickCheckout();
* int status = WaitEngine.waitForResponse("&#42;&#42;/api/checkout");
* </pre>
*/
public static int waitForResponse(String urlPattern) {
return waitForResponse(urlPattern, explicitTimeout());
}

/**
* Like {@link #waitForResponse(String)}, but with a custom timeout instead of the configured one.
*/
public static int waitForResponse(String urlPattern, Duration timeout) {
return NetworkActivityTracker.awaitResponse(DriverManager.getDriver(), urlPattern, timeout);
}

private static Duration explicitTimeout() {
return Duration.ofSeconds(SeleniumBootContext.getConfig().getTimeouts().getExplicit());
}

// ----------------------------------------------------------
// Alert
// ----------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package com.seleniumboot.wait;

import org.openqa.selenium.TimeoutException;
import org.testng.annotations.Test;

import java.time.Duration;

import static org.testng.Assert.*;

/**
* Unit tests for the pure request-tracking logic behind {@link WaitEngine#waitForNetworkIdle}
* and {@link WaitEngine#waitForResponse}. CDP/BiDi wiring itself needs a real browser and is
* verified manually — same convention as {@code NetworkMockTest} for CDP request interception.
*/
public class NetworkActivityTrackerTest {

// ------------------------------------------------------------------
// IdleState
// ------------------------------------------------------------------

@Test
public void idleState_startsIdle() {
NetworkActivityTracker.IdleState state = new NetworkActivityTracker.IdleState();
assertTrue(state.isIdle(Duration.ZERO));
}

@Test
public void idleState_notIdleWhileRequestInFlight() {
NetworkActivityTracker.IdleState state = new NetworkActivityTracker.IdleState();
state.requestStarted();
assertFalse(state.isIdle(Duration.ZERO));
assertEquals(state.inFlightCount(), 1);
}

@Test
public void idleState_idleAgainOnceRequestEnds() {
NetworkActivityTracker.IdleState state = new NetworkActivityTracker.IdleState();
state.requestStarted();
state.requestEnded();
assertTrue(state.isIdle(Duration.ZERO));
assertEquals(state.inFlightCount(), 0);
}

@Test
public void idleState_notIdleUntilQuietPeriodElapses() throws InterruptedException {
NetworkActivityTracker.IdleState state = new NetworkActivityTracker.IdleState();
state.requestStarted();
state.requestEnded();
// just ended — a long quiet period hasn't elapsed yet
assertFalse(state.isIdle(Duration.ofMinutes(1)));
Thread.sleep(20);
assertTrue(state.isIdle(Duration.ofMillis(5)));
}

@Test
public void idleState_multipleOverlappingRequests() {
NetworkActivityTracker.IdleState state = new NetworkActivityTracker.IdleState();
state.requestStarted();
state.requestStarted();
assertEquals(state.inFlightCount(), 2);
state.requestEnded();
assertFalse(state.isIdle(Duration.ZERO), "one request still in flight");
state.requestEnded();
assertTrue(state.isIdle(Duration.ZERO));
}

// ------------------------------------------------------------------
// ResponseLatch
// ------------------------------------------------------------------

@Test
public void responseLatch_returnsStatusOnceCompleted() {
NetworkActivityTracker.ResponseLatch latch = new NetworkActivityTracker.ResponseLatch();
latch.complete(204);
assertEquals(latch.await(Duration.ofSeconds(1)), 204);
}

@Test
public void responseLatch_returnsStatusFromAnotherThread() throws InterruptedException {
NetworkActivityTracker.ResponseLatch latch = new NetworkActivityTracker.ResponseLatch();
Thread producer = new Thread(() -> {
try {
Thread.sleep(20);
} catch (InterruptedException ignored) {
}
latch.complete(200);
});
producer.start();
assertEquals(latch.await(Duration.ofSeconds(1)), 200);
producer.join();
}

@Test(expectedExceptions = TimeoutException.class)
public void responseLatch_timesOutWithNoMatch() {
NetworkActivityTracker.ResponseLatch latch = new NetworkActivityTracker.ResponseLatch();
latch.await(Duration.ofMillis(50));
}
}
Loading