diff --git a/CHANGELOG.md b/CHANGELOG.md index e72c6d2..ba4d1ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/). --- +### Unreleased + +- **Network mocking on Firefox via WebDriver BiDi** — `networkMock().stub(...)` now uses WebDriver BiDi network interception under the hood when supported by the driver (including Firefox and BiDi-enabled drivers), falling back to Chrome DevTools Protocol (CDP) for Chromium-based drivers. The existing stubbing API (`returnJson`, `returnBody`, `returnStatus`, `delay`, `clear`) works identically across Chrome and Firefox. (#196) + +--- + ### v3.9.0 — 2026-10-21 - **GitHub Actions job summary: link to the run's artifacts** — the failed-tests summary now adds a line linking to the workflow run page (built from `GITHUB_SERVER_URL`, `GITHUB_REPOSITORY` and `GITHUB_RUN_ID`, no config) where uploaded `target/traces/` artifacts live, so a reviewer can get from a red PR to the trace in one click. Omitted when those variables are missing. (#130) diff --git a/docs-site/docs/changelog.md b/docs-site/docs/changelog.md index 140b4d2..596dda9 100644 --- a/docs-site/docs/changelog.md +++ b/docs-site/docs/changelog.md @@ -9,6 +9,12 @@ sidebar_position: 99 All notable changes to Selenium Boot are documented here. +## [Unreleased] + +- **Network mocking on Firefox via WebDriver BiDi** — `networkMock().stub(...)` now uses WebDriver BiDi network interception under the hood when supported by the driver (including Firefox and BiDi-enabled drivers), falling back to Chrome DevTools Protocol (CDP) for Chromium-based drivers. The existing stubbing API (`returnJson`, `returnBody`, `returnStatus`, `delay`, `clear`) works identically across Chrome and Firefox. (#196) + +--- + ## [3.9.0] — 2026-10-21 - **GitHub Actions job summary: link to the run's artifacts** — the failed-tests summary now adds a line linking to the workflow run page (built from `GITHUB_SERVER_URL`, `GITHUB_REPOSITORY` and `GITHUB_RUN_ID`, no config) where uploaded `target/traces/` artifacts live, so a reviewer can get from a red PR to the trace in one click. Omitted when those variables are missing. (#130) diff --git a/src/main/java/com/seleniumboot/network/NetworkMock.java b/src/main/java/com/seleniumboot/network/NetworkMock.java index b414619..fb83ffb 100644 --- a/src/main/java/com/seleniumboot/network/NetworkMock.java +++ b/src/main/java/com/seleniumboot/network/NetworkMock.java @@ -3,40 +3,53 @@ import com.seleniumboot.api.SeleniumBootApi; import com.seleniumboot.driver.DriverManager; import org.openqa.selenium.WebDriver; +import org.openqa.selenium.bidi.HasBiDi; +import org.openqa.selenium.bidi.module.Network; +import org.openqa.selenium.bidi.network.AddInterceptParameters; +import org.openqa.selenium.bidi.network.BeforeRequestSent; +import org.openqa.selenium.bidi.network.BytesValue; +import org.openqa.selenium.bidi.network.ContinueRequestParameters; +import org.openqa.selenium.bidi.network.Header; +import org.openqa.selenium.bidi.network.InterceptPhase; +import org.openqa.selenium.bidi.network.ProvideResponseParameters; import org.openqa.selenium.chromium.ChromiumDriver; import org.openqa.selenium.devtools.DevTools; import org.openqa.selenium.devtools.v153.fetch.Fetch; import org.openqa.selenium.devtools.v153.fetch.model.HeaderEntry; import org.openqa.selenium.devtools.v153.fetch.model.RequestId; import org.openqa.selenium.devtools.v153.fetch.model.RequestPattern; +import org.openqa.selenium.devtools.v153.fetch.model.RequestPaused; import org.openqa.selenium.devtools.v153.fetch.model.RequestStage; import java.nio.charset.StandardCharsets; import java.util.Base64; +import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.concurrent.CopyOnWriteArrayList; import java.util.logging.Logger; /** - * CDP-backed network interception and request mocking. + * Network interception and request mocking backed by WebDriver BiDi and CDP. * - *
Supported on Chrome and Edge (Chromium-based). On Firefox or other browsers - * a warning is logged and all stub calls are silently ignored — tests continue - * without interception. + *
Supported on Chrome, Edge, and Firefox. On drivers supporting {@link HasBiDi} + * (including Firefox and BiDi-enabled Chromium), WebDriver BiDi is used under the hood; + * otherwise, it falls back to Chrome DevTools Protocol (CDP) for {@link ChromiumDriver}. + * On unsupported browsers without BiDi or CDP, a warning is logged and all stub calls + * are silently ignored — tests continue without interception. * *
Obtain via {@code networkMock()} in {@link com.seleniumboot.test.BasePage} * or {@link com.seleniumboot.test.BaseTest}. * *
* // Return mock JSON for an API call
- * networkMock().stub("** /api/users").returnJson("{\"users\":[{\"id\":1}]}");
+ * networkMock().stub("**/api/users").returnJson("{\"users\":[{\"id\":1}]}");
*
* // Simulate a server error
- * networkMock().stub("** /api/checkout").returnStatus(500);
+ * networkMock().stub("**/api/checkout").returnStatus(500);
*
* // Simulate slow network (2-second delay)
- * networkMock().stub("** /api/products").delay(2000).returnJson("[]");
+ * networkMock().stub("**/api/products").delay(2000).returnJson("[]");
*
* // Clear all stubs (auto-called after each test by the framework)
* networkMock().clear();
@@ -50,11 +63,21 @@ public final class NetworkMock {
private static final ThreadLocal INSTANCE =
ThreadLocal.withInitial(NetworkMock::new);
+ @FunctionalInterface
+ interface BiDiNetworkFactory {
+ Network create(WebDriver driver);
+ }
+
+ // Factory hook for testing BiDi without an active browser WebSocket session
+ static BiDiNetworkFactory bidiFactory = Network::new;
+
private final List stubs = new CopyOnWriteArrayList<>();
- private boolean cdpAttached = false;
+ private boolean attached = false;
private DevTools devTools;
+ private Network bidiNetwork;
+ private String bidiInterceptId;
- private NetworkMock() {
+ NetworkMock() {
}
/** Returns the per-thread {@link NetworkMock} instance. */
@@ -91,11 +114,29 @@ public StubBuilder stub(String urlPattern) {
}
/**
- * Removes all registered stubs and disables CDP interception for this thread.
+ * Removes all registered stubs and disables interception for this thread.
* Called automatically by the framework after each test.
*/
public void clear() {
stubs.clear();
+
+ if (bidiNetwork != null) {
+ try {
+ if (bidiInterceptId != null) {
+ bidiNetwork.removeIntercept(bidiInterceptId);
+ }
+ } catch (Exception ignored) {
+ // BiDi session may already be closed
+ }
+ try {
+ bidiNetwork.close();
+ } catch (Exception ignored) {
+ // BiDi session may already be closed
+ }
+ bidiNetwork = null;
+ bidiInterceptId = null;
+ }
+
if (devTools != null) {
try {
devTools.send(Fetch.disable());
@@ -104,7 +145,8 @@ public void clear() {
}
devTools = null;
}
- cdpAttached = false;
+
+ attached = false;
}
// ------------------------------------------------------------------
@@ -113,46 +155,135 @@ public void clear() {
void register(StubBuilder stub) {
stubs.add(stub);
- ensureCdpAttached();
+ ensureAttached();
}
// ------------------------------------------------------------------
- // CDP wiring
+ // Interception wiring (BiDi with CDP fallback)
// ------------------------------------------------------------------
- private void ensureCdpAttached() {
- if (cdpAttached) return;
+ private void ensureAttached() {
+ if (attached) return;
- WebDriver driver = DriverManager.getDriver();
- if (!(driver instanceof ChromiumDriver)) {
- LOG.warning("[Selenium Boot] NetworkMock requires Chrome/Edge. " +
- "Stubs will be ignored on " + driver.getClass().getSimpleName() + ".");
- cdpAttached = true;
+ WebDriver driver;
+ try {
+ driver = DriverManager.getDriver();
+ } catch (IllegalStateException e) {
+ LOG.warning("[Selenium Boot] No active WebDriver session found for NetworkMock. " +
+ "Stubs will be ignored.");
+ attached = true;
return;
}
- devTools = ((ChromiumDriver) driver).getDevTools();
- devTools.createSession();
+ if (driver == null) {
+ LOG.warning("[Selenium Boot] No active WebDriver session found for NetworkMock. " +
+ "Stubs will be ignored.");
+ attached = true;
+ return;
+ }
- // Enable Fetch domain — intercept all requests before they are sent
- List patterns = List.of(
- new RequestPattern(
- Optional.of("*"),
- Optional.empty(),
- Optional.of(RequestStage.REQUEST)
- )
- );
- devTools.send(Fetch.enable(Optional.of(patterns), Optional.empty()));
+ // 1. Try BiDi first if driver supports HasBiDi
+ if (driver instanceof HasBiDi) {
+ try {
+ bidiNetwork = bidiFactory.create(driver);
+ bidiInterceptId = bidiNetwork.addIntercept(
+ new AddInterceptParameters(InterceptPhase.BEFORE_REQUEST_SENT)
+ );
+ bidiNetwork.onBeforeRequestSent(this::handleBiDiRequest);
+ attached = true;
+ return;
+ } catch (Exception e) {
+ LOG.fine("[Selenium Boot] BiDi network interception unavailable (" + e.getMessage() +
+ "); attempting CDP fallback.");
+ if (bidiNetwork != null) {
+ try { bidiNetwork.close(); } catch (Exception ignored) {}
+ bidiNetwork = null;
+ }
+ bidiInterceptId = null;
+ }
+ }
+
+ // 2. Fall back to CDP for Chromium-based drivers
+ if (driver instanceof ChromiumDriver chromium) {
+ try {
+ devTools = chromium.getDevTools();
+ devTools.createSession();
+
+ List patterns = List.of(
+ new RequestPattern(
+ Optional.of("*"),
+ Optional.empty(),
+ Optional.of(RequestStage.REQUEST)
+ )
+ );
+ devTools.send(Fetch.enable(Optional.of(patterns), Optional.empty()));
+ devTools.addListener(Fetch.requestPaused(), this::handleCdpRequest);
+ attached = true;
+ return;
+ } catch (Exception e) {
+ LOG.fine("[Selenium Boot] CDP interception initialization failed: " + e.getMessage());
+ devTools = null;
+ }
+ }
+
+ // 3. Neither BiDi nor CDP supported
+ LOG.warning("[Selenium Boot] NetworkMock requires BiDi (Firefox/Chrome) or CDP (Chrome/Edge). " +
+ "Stubs will be ignored on " + driver.getClass().getSimpleName() + ".");
+ attached = true;
+ }
- // Handle each intercepted request
- devTools.addListener(Fetch.requestPaused(), event -> {
- String url = event.getRequest().getUrl();
- RequestId requestId = event.getRequestId();
+ // ------------------------------------------------------------------
+ // Request handlers
+ // ------------------------------------------------------------------
+ void handleBiDiRequest(BeforeRequestSent event) {
+ if (!event.isBlocked()) {
+ return;
+ }
+
+ String url = event.getRequest().getUrl();
+ String requestId = event.getRequest().getRequestId();
+ StubBuilder match = findMatch(url);
+
+ try {
+ if (match == null) {
+ bidiNetwork.continueRequest(new ContinueRequestParameters(requestId));
+ return;
+ }
+
+ if (match.delayMs > 0) {
+ try {
+ Thread.sleep(match.delayMs);
+ } catch (InterruptedException ignored) {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ String contentType = match.contentType != null ? match.contentType : "application/json";
+ List headers = List.of(
+ new Header("content-type", new BytesValue(BytesValue.Type.STRING, contentType))
+ );
+
+ String body = match.responseBody != null ? match.responseBody : "";
+ ProvideResponseParameters params = new ProvideResponseParameters(requestId)
+ .statusCode(match.statusCode)
+ .headers(headers)
+ .body(new BytesValue(BytesValue.Type.STRING, body));
+
+ bidiNetwork.provideResponse(params);
+ } catch (Exception e) {
+ LOG.warning("[Selenium Boot] Failed to process BiDi intercepted request for " + url + ": " + e.getMessage());
+ }
+ }
+
+ void handleCdpRequest(RequestPaused event) {
+ String url = event.getRequest().getUrl();
+ RequestId requestId = event.getRequestId();
+
+ try {
StubBuilder match = findMatch(url);
if (match == null) {
- // No stub — let the request continue normally
devTools.send(Fetch.continueRequest(
requestId,
Optional.empty(),
@@ -164,17 +295,21 @@ private void ensureCdpAttached() {
return;
}
- // Apply delay if configured
if (match.delayMs > 0) {
- try { Thread.sleep(match.delayMs); } catch (InterruptedException ignored) {}
+ try {
+ Thread.sleep(match.delayMs);
+ } catch (InterruptedException ignored) {
+ Thread.currentThread().interrupt();
+ }
}
- // Build response body as base64
+ String body = match.responseBody != null ? match.responseBody : "";
String body64 = Base64.getEncoder().encodeToString(
- match.responseBody.getBytes(StandardCharsets.UTF_8));
+ body.getBytes(StandardCharsets.UTF_8));
+ String contentType = match.contentType != null ? match.contentType : "application/json";
List headers = List.of(
- new HeaderEntry("content-type", match.contentType)
+ new HeaderEntry("content-type", contentType)
);
devTools.send(Fetch.fulfillRequest(
@@ -185,9 +320,37 @@ private void ensureCdpAttached() {
Optional.of(body64),
Optional.empty()
));
- });
+ } catch (Exception e) {
+ LOG.warning("[Selenium Boot] Failed to process CDP intercepted request for " + url + ": " + e.getMessage());
+ }
+ }
+
+ // ------------------------------------------------------------------
+ // Testing / Inspection helpers (package-private for test support)
+ // ------------------------------------------------------------------
+
+ static void setBiDiFactory(BiDiNetworkFactory factory) {
+ bidiFactory = factory;
+ }
+
+ static void resetBiDiFactory() {
+ bidiFactory = Network::new;
+ }
+
+ boolean isAttached() {
+ return attached;
+ }
+
+ boolean isBiDiActive() {
+ return bidiNetwork != null;
+ }
+
+ boolean isCdpActive() {
+ return devTools != null;
+ }
- cdpAttached = true;
+ List getStubs() {
+ return Collections.unmodifiableList(stubs);
}
// ------------------------------------------------------------------
diff --git a/src/main/java/com/seleniumboot/network/StubBuilder.java b/src/main/java/com/seleniumboot/network/StubBuilder.java
index 3a83b11..109cd12 100644
--- a/src/main/java/com/seleniumboot/network/StubBuilder.java
+++ b/src/main/java/com/seleniumboot/network/StubBuilder.java
@@ -76,4 +76,24 @@ public StubBuilder delay(long ms) {
this.delayMs = ms;
return this;
}
+
+ public String getPattern() {
+ return pattern;
+ }
+
+ public String getResponseBody() {
+ return responseBody;
+ }
+
+ public String getContentType() {
+ return contentType;
+ }
+
+ public int getStatusCode() {
+ return statusCode;
+ }
+
+ public long getDelayMs() {
+ return delayMs;
+ }
}
diff --git a/src/test/java/com/seleniumboot/network/FirefoxNetworkMockLiveTest.java b/src/test/java/com/seleniumboot/network/FirefoxNetworkMockLiveTest.java
new file mode 100644
index 0000000..4c25a79
--- /dev/null
+++ b/src/test/java/com/seleniumboot/network/FirefoxNetworkMockLiveTest.java
@@ -0,0 +1,137 @@
+package com.seleniumboot.network;
+
+import com.seleniumboot.driver.DriverManager;
+import com.sun.net.httpserver.HttpServer;
+import org.openqa.selenium.JavascriptExecutor;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.firefox.FirefoxDriver;
+import org.openqa.selenium.firefox.FirefoxOptions;
+import org.testng.SkipException;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import java.io.File;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.Map;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+
+/**
+ * Live integration test verifying WebDriver BiDi network mocking on real Firefox.
+ * Skips gracefully if Firefox or geckodriver is not installed in the environment.
+ */
+public class FirefoxNetworkMockLiveTest {
+
+ private HttpServer server;
+ private int port;
+ private WebDriver driver;
+
+ @BeforeClass
+ public void setUp() throws Exception {
+ // 1. Start lightweight local HTTP server
+ server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
+ server.createContext("/", exchange -> {
+ byte[] response = "Firefox BiDi Test
"
+ .getBytes(StandardCharsets.UTF_8);
+ exchange.getResponseHeaders().set("Content-Type", "text/html; charset=UTF-8");
+ exchange.sendResponseHeaders(200, response.length);
+ try (OutputStream os = exchange.getResponseBody()) {
+ os.write(response);
+ }
+ });
+ server.start();
+ port = server.getAddress().getPort();
+
+ // 2. Configure Firefox with BiDi enabled and headless mode
+ FirefoxOptions options = new FirefoxOptions();
+ options.addArguments("-headless");
+ options.setCapability("webSocketUrl", true);
+
+ // Check if Firefox is installed via Scoop or standard locations
+ String userHome = System.getProperty("user.home");
+ File scoopFirefox = new File(userHome, "scoop/apps/firefox/current/firefox.exe");
+ if (scoopFirefox.exists()) {
+ options.setBinary(scoopFirefox.getAbsolutePath());
+ }
+
+ try {
+ driver = new FirefoxDriver(options);
+ driver.manage().timeouts().scriptTimeout(Duration.ofSeconds(10));
+ DriverManager.pushSessionOverride(driver);
+ } catch (Throwable t) {
+ if (server != null) {
+ server.stop(0);
+ }
+ throw new SkipException("Firefox or geckodriver not available: " + t.getMessage(), t);
+ }
+ }
+
+ @AfterClass(alwaysRun = true)
+ public void tearDown() {
+ NetworkMock.cleanup();
+ DriverManager.popSessionOverride();
+ if (driver != null) {
+ try {
+ driver.quit();
+ } catch (Exception ignored) {
+ }
+ }
+ if (server != null) {
+ server.stop(0);
+ }
+ }
+
+ @Test
+ public void testFirefoxBiDiNetworkMockingReturnJson() {
+ NetworkMock mock = NetworkMock.get();
+ mock.stub("**/api/user/profile")
+ .returnJson("{\"name\":\"Alice\",\"browser\":\"firefox\",\"mocked\":true}");
+
+ driver.get("http://127.0.0.1:" + port + "/");
+
+ JavascriptExecutor js = (JavascriptExecutor) driver;
+ String script =
+ "var callback = arguments[arguments.length - 1];\n" +
+ "fetch('/api/user/profile')\n" +
+ " .then(response => response.json())\n" +
+ " .then(data => callback(data))\n" +
+ " .catch(err => callback({error: err.toString()}));";
+
+ @SuppressWarnings("unchecked")
+ Map result = (Map) js.executeAsyncScript(script);
+
+ assertNotNull(result, "Result from fetch should not be null");
+ assertEquals(result.get("name"), "Alice");
+ assertEquals(result.get("browser"), "firefox");
+ assertEquals(result.get("mocked"), Boolean.TRUE);
+
+ mock.clear();
+ }
+
+ @Test
+ public void testFirefoxBiDiNetworkMockingReturnStatus() {
+ NetworkMock mock = NetworkMock.get();
+ mock.stub("**/api/error")
+ .returnStatus(503);
+
+ driver.get("http://127.0.0.1:" + port + "/");
+
+ JavascriptExecutor js = (JavascriptExecutor) driver;
+ String script =
+ "var callback = arguments[arguments.length - 1];\n" +
+ "fetch('/api/error')\n" +
+ " .then(response => callback(response.status))\n" +
+ " .catch(err => callback(-1));";
+
+ Object status = js.executeAsyncScript(script);
+ assertEquals(status, 503L);
+
+ mock.clear();
+ }
+}
+
diff --git a/src/test/java/com/seleniumboot/network/NetworkMockTestSupport.java b/src/test/java/com/seleniumboot/network/NetworkMockTestSupport.java
new file mode 100644
index 0000000..c8c078c
--- /dev/null
+++ b/src/test/java/com/seleniumboot/network/NetworkMockTestSupport.java
@@ -0,0 +1,46 @@
+package com.seleniumboot.network;
+
+import org.openqa.selenium.WebDriver;
+
+import java.util.List;
+
+/**
+ * Test-support helper for accessing package-private testing hooks in {@link NetworkMock}.
+ * Lives exclusively in test scope ({@code src/test/java}) so it is never exposed in the
+ * published framework artifact.
+ */
+public final class NetworkMockTestSupport {
+
+ private NetworkMockTestSupport() {
+ }
+
+ @FunctionalInterface
+ public interface BiDiNetworkFactory {
+ org.openqa.selenium.bidi.module.Network create(WebDriver driver);
+ }
+
+ public static void setBiDiFactory(BiDiNetworkFactory factory) {
+ NetworkMock.setBiDiFactory(factory::create);
+ }
+
+ public static void resetBiDiFactory() {
+ NetworkMock.resetBiDiFactory();
+ }
+
+ public static boolean isAttached(NetworkMock mock) {
+ return mock.isAttached();
+ }
+
+ public static boolean isBiDiActive(NetworkMock mock) {
+ return mock.isBiDiActive();
+ }
+
+ public static boolean isCdpActive(NetworkMock mock) {
+ return mock.isCdpActive();
+ }
+
+ public static List getStubs(NetworkMock mock) {
+ return mock.getStubs();
+ }
+}
+
diff --git a/src/test/java/com/seleniumboot/unit/NetworkMockTest.java b/src/test/java/com/seleniumboot/unit/NetworkMockTest.java
index 7719706..d8eb40a 100644
--- a/src/test/java/com/seleniumboot/unit/NetworkMockTest.java
+++ b/src/test/java/com/seleniumboot/unit/NetworkMockTest.java
@@ -1,16 +1,61 @@
package com.seleniumboot.unit;
+import com.seleniumboot.driver.DriverManager;
import com.seleniumboot.network.NetworkMock;
+import com.seleniumboot.network.NetworkMockTestSupport;
+import com.seleniumboot.network.StubBuilder;
+import org.mockito.ArgumentCaptor;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.bidi.BiDiException;
+import org.openqa.selenium.bidi.HasBiDi;
+import org.openqa.selenium.bidi.module.Network;
+import org.openqa.selenium.bidi.network.AddInterceptParameters;
+import org.openqa.selenium.bidi.network.BeforeRequestSent;
+import org.openqa.selenium.bidi.network.ContinueRequestParameters;
+import org.openqa.selenium.bidi.network.Header;
+import org.openqa.selenium.bidi.network.ProvideResponseParameters;
+import org.openqa.selenium.bidi.network.RequestData;
+import org.openqa.selenium.chromium.ChromiumDriver;
+import org.openqa.selenium.devtools.DevTools;
+import org.openqa.selenium.devtools.Command;
+import org.openqa.selenium.devtools.v153.fetch.Fetch;
+import org.openqa.selenium.devtools.v153.fetch.model.HeaderEntry;
+import org.openqa.selenium.devtools.v153.fetch.model.RequestId;
+import org.openqa.selenium.devtools.v153.fetch.model.RequestPaused;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
import static org.testng.Assert.*;
/**
- * Unit tests for {@link NetworkMock} — pattern matching and stub registration.
- * CDP wiring requires a real browser and is covered by integration tests.
+ * Unit tests for {@link NetworkMock} — URL matching, StubBuilder configurations,
+ * WebDriver BiDi interception, CDP fallback, and unsupported browser handling.
*/
public class NetworkMockTest {
+ @BeforeMethod
+ public void setUp() {
+ NetworkMock.cleanup();
+ NetworkMockTestSupport.resetBiDiFactory();
+ }
+
+ @AfterMethod
+ public void tearDown() {
+ NetworkMock.cleanup();
+ NetworkMockTestSupport.resetBiDiFactory();
+ try {
+ DriverManager.popSessionOverride();
+ } catch (Exception ignored) {
+ }
+ }
+
// ------------------------------------------------------------------
// Pattern matching — globToRegex + matches()
// ------------------------------------------------------------------
@@ -82,21 +127,363 @@ public void globToRegex_singleStarConverted() {
}
// ------------------------------------------------------------------
- // StubBuilder
+ // StubBuilder Configuration
// ------------------------------------------------------------------
@Test
public void stub_returnJson_setsDefaults() {
- // StubBuilder is constructed via NetworkMock.stub() but we can test
- // the pattern is stored correctly via toString-level assertions
- // Full wiring requires CDP — tested in integration.
- // Here we verify the pattern matching that drives stub lookup.
- assertTrue(NetworkMock.matches("**/users", "https://example.com/api/users"),
- "Pattern **/users should match any URL ending in /users");
+ StubBuilder builder = NetworkMock.get().stub("**/users");
+ builder.returnJson("{\"users\":[]}");
+
+ assertEquals(builder.getPattern(), "**/users");
+ assertEquals(builder.getResponseBody(), "{\"users\":[]}");
+ assertEquals(builder.getContentType(), "application/json");
+ assertEquals(builder.getStatusCode(), 200);
+ assertEquals(builder.getDelayMs(), 0);
}
@Test
public void stub_returnStatus_500() {
- assertTrue(NetworkMock.matches("**/checkout", "https://shop.example.com/checkout"));
+ StubBuilder builder = NetworkMock.get().stub("**/checkout");
+ builder.returnStatus(500);
+
+ assertEquals(builder.getStatusCode(), 500);
+ assertEquals(builder.getResponseBody(), "");
+ }
+
+ @Test
+ public void stub_returnStatus_withBody() {
+ StubBuilder builder = NetworkMock.get().stub("**/error");
+ builder.returnStatus(503, "Service Unavailable");
+
+ assertEquals(builder.getStatusCode(), 503);
+ assertEquals(builder.getResponseBody(), "Service Unavailable");
+ }
+
+ @Test
+ public void stub_returnBody_customContentType() {
+ StubBuilder builder = NetworkMock.get().stub("**/document.xml");
+ builder.returnBody(" ", "application/xml");
+
+ assertEquals(builder.getResponseBody(), " ");
+ assertEquals(builder.getContentType(), "application/xml");
+ assertEquals(builder.getStatusCode(), 200);
+ }
+
+ @Test
+ public void stub_delay_setsDelayMillis() {
+ StubBuilder builder = NetworkMock.get().stub("**/slow").delay(1500);
+ assertEquals(builder.getDelayMs(), 1500);
+ }
+
+ // ------------------------------------------------------------------
+ // BiDi Interception Path
+ // ------------------------------------------------------------------
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void bidi_attachesAndInterceptsMatchingRequest() {
+ WebDriver mockDriver = mock(WebDriver.class, withSettings().extraInterfaces(HasBiDi.class));
+ DriverManager.pushSessionOverride(mockDriver);
+
+ Network mockNetwork = mock(Network.class);
+ when(mockNetwork.addIntercept(any(AddInterceptParameters.class))).thenReturn("intercept-bidi-1");
+
+ NetworkMockTestSupport.setBiDiFactory(d -> mockNetwork);
+
+ NetworkMock mock = NetworkMock.get();
+ mock.stub("**/api/users").returnJson("{\"users\":[{\"id\":1}]}");
+
+ assertTrue(NetworkMockTestSupport.isBiDiActive(mock), "BiDi should be active when driver implements HasBiDi");
+ assertTrue(NetworkMockTestSupport.isAttached(mock), "NetworkMock should be attached");
+ verify(mockNetwork).addIntercept(any(AddInterceptParameters.class));
+
+ ArgumentCaptor> captor = ArgumentCaptor.forClass(Consumer.class);
+ verify(mockNetwork).onBeforeRequestSent(captor.capture());
+ Consumer listener = captor.getValue();
+
+ // Simulate intercepted matching request
+ BeforeRequestSent event = mock(BeforeRequestSent.class);
+ RequestData req = mock(RequestData.class);
+ when(event.isBlocked()).thenReturn(true);
+ when(event.getRequest()).thenReturn(req);
+ when(req.getUrl()).thenReturn("https://api.example.com/api/users");
+ when(req.getRequestId()).thenReturn("req-101");
+
+ listener.accept(event);
+
+ ArgumentCaptor respCaptor =
+ ArgumentCaptor.forClass(ProvideResponseParameters.class);
+ verify(mockNetwork).provideResponse(respCaptor.capture());
+
+ Map paramsMap = respCaptor.getValue().toMap();
+ assertEquals(paramsMap.get("request"), "req-101");
+ assertEquals(paramsMap.get("statusCode"), 200);
+
+ List