From d01fbafdc51ba86d2cecf4c2445975fd785ee210 Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Sat, 20 Sep 2025 00:36:52 +0530 Subject: [PATCH 1/4] add `getData`, `addDataCollector` and `removeDataCollector` --- .../openqa/selenium/bidi/module/Network.java | 44 +++++++++++ .../network/AddDataCollectorParameters.java | 78 +++++++++++++++++++ .../selenium/bidi/network/DataType.java | 33 ++++++++ .../bidi/network/GetDataParameters.java | 59 ++++++++++++++ 4 files changed, 214 insertions(+) create mode 100644 java/src/org/openqa/selenium/bidi/network/AddDataCollectorParameters.java create mode 100644 java/src/org/openqa/selenium/bidi/network/DataType.java create mode 100644 java/src/org/openqa/selenium/bidi/network/GetDataParameters.java diff --git a/java/src/org/openqa/selenium/bidi/module/Network.java b/java/src/org/openqa/selenium/bidi/module/Network.java index c295009d39c93..dd7db9288a37c 100644 --- a/java/src/org/openqa/selenium/bidi/module/Network.java +++ b/java/src/org/openqa/selenium/bidi/module/Network.java @@ -17,32 +17,41 @@ package org.openqa.selenium.bidi.module; +import java.io.StringReader; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Function; import org.openqa.selenium.UsernameAndPassword; import org.openqa.selenium.WebDriver; import org.openqa.selenium.bidi.BiDi; import org.openqa.selenium.bidi.Command; import org.openqa.selenium.bidi.Event; import org.openqa.selenium.bidi.HasBiDi; +import org.openqa.selenium.bidi.network.AddDataCollectorParameters; 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.CacheBehavior; import org.openqa.selenium.bidi.network.ContinueRequestParameters; import org.openqa.selenium.bidi.network.ContinueResponseParameters; import org.openqa.selenium.bidi.network.FetchError; +import org.openqa.selenium.bidi.network.GetDataParameters; import org.openqa.selenium.bidi.network.ProvideResponseParameters; import org.openqa.selenium.bidi.network.ResponseDetails; import org.openqa.selenium.internal.Require; +import org.openqa.selenium.json.Json; +import org.openqa.selenium.json.JsonInput; public class Network implements AutoCloseable { private final Set browsingContextIds; + private static final Json JSON = new Json(); + private final BiDi bidi; private final Event beforeRequestSentEvent = @@ -60,6 +69,18 @@ public class Network implements AutoCloseable { private final Event authRequired = new Event<>("network.authRequired", ResponseDetails::fromJsonMap); + private final Function getDataResultMapper = + jsonInput -> { + Map result = jsonInput.read(Map.class); + @SuppressWarnings("unchecked") + Map bytesMap = (Map) result.get("bytes"); + + try (StringReader reader = new StringReader(JSON.toJson(bytesMap)); + JsonInput bytesInput = JSON.newInput(reader)) { + return BytesValue.fromJson(bytesInput); + } + }; + public Network(WebDriver driver) { this(new HashSet<>(), driver); } @@ -155,6 +176,29 @@ public void setCacheBehavior(CacheBehavior cacheBehavior, List contexts) Map.of("cacheBehavior", cacheBehavior.toString(), "contexts", contexts))); } + public String addDataCollector(AddDataCollectorParameters parameters) { + Require.nonNull("Add data collector parameters", parameters); + return this.bidi.send( + new Command<>( + "network.addDataCollector", + parameters.toMap(), + jsonInput -> { + Map result = jsonInput.read(Map.class); + return (String) result.get("collector"); + })); + } + + public void removeDataCollector(String collector) { + Require.nonNull("Collector", collector); + this.bidi.send(new Command<>("network.removeDataCollector", Map.of("collector", collector))); + } + + public BytesValue getData(GetDataParameters parameters) { + Require.nonNull("Get data parameters", parameters); + return this.bidi.send( + new Command<>("network.getData", parameters.toMap(), getDataResultMapper)); + } + public void onBeforeRequestSent(Consumer consumer) { if (browsingContextIds.isEmpty()) { this.bidi.addListener(beforeRequestSentEvent, consumer); diff --git a/java/src/org/openqa/selenium/bidi/network/AddDataCollectorParameters.java b/java/src/org/openqa/selenium/bidi/network/AddDataCollectorParameters.java new file mode 100644 index 0000000000000..c01e092ddfcda --- /dev/null +++ b/java/src/org/openqa/selenium/bidi/network/AddDataCollectorParameters.java @@ -0,0 +1,78 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.openqa.selenium.bidi.network; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.openqa.selenium.internal.Require; + +public class AddDataCollectorParameters { + + private final List dataTypes = new ArrayList<>(); + private final long maxEncodedDataSize; + private String collectorType = "blob"; + private List contexts; + private List userContexts; + + public AddDataCollectorParameters(List dataTypes, long maxEncodedDataSize) { + Require.nonNull("Data types", dataTypes); + if (maxEncodedDataSize <= 0) { + throw new IllegalArgumentException("Max encoded data size must be positive"); + } + + dataTypes.forEach(dataType -> this.dataTypes.add(dataType.toString())); + this.maxEncodedDataSize = maxEncodedDataSize; + } + + public AddDataCollectorParameters collectorType(String collectorType) { + this.collectorType = Require.nonNull("Collector type", collectorType); + return this; + } + + public AddDataCollectorParameters contexts(List contexts) { + this.contexts = Require.nonNull("Contexts", contexts); + return this; + } + + public AddDataCollectorParameters userContexts(List userContexts) { + this.userContexts = Require.nonNull("User contexts", userContexts); + return this; + } + + public Map toMap() { + Map map = new HashMap<>(); + map.put("dataTypes", dataTypes); + map.put("maxEncodedDataSize", maxEncodedDataSize); + + if (collectorType != null) { + map.put("collectorType", collectorType); + } + + if (contexts != null && !contexts.isEmpty()) { + map.put("contexts", contexts); + } + + if (userContexts != null && !userContexts.isEmpty()) { + map.put("userContexts", userContexts); + } + + return Map.copyOf(map); + } +} diff --git a/java/src/org/openqa/selenium/bidi/network/DataType.java b/java/src/org/openqa/selenium/bidi/network/DataType.java new file mode 100644 index 0000000000000..0e453842df785 --- /dev/null +++ b/java/src/org/openqa/selenium/bidi/network/DataType.java @@ -0,0 +1,33 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.openqa.selenium.bidi.network; + +public enum DataType { + RESPONSE("response"); + + private final String value; + + DataType(String value) { + this.value = value; + } + + @Override + public String toString() { + return value; + } +} diff --git a/java/src/org/openqa/selenium/bidi/network/GetDataParameters.java b/java/src/org/openqa/selenium/bidi/network/GetDataParameters.java new file mode 100644 index 0000000000000..487f55102e198 --- /dev/null +++ b/java/src/org/openqa/selenium/bidi/network/GetDataParameters.java @@ -0,0 +1,59 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.openqa.selenium.bidi.network; + +import java.util.HashMap; +import java.util.Map; +import org.openqa.selenium.internal.Require; + +public class GetDataParameters { + + private final DataType dataType; + private final String request; + private String collector; + private boolean disown = false; + + public GetDataParameters(DataType dataType, String request) { + this.dataType = Require.nonNull("Data type", dataType); + this.request = Require.nonNull("Request", request); + } + + public GetDataParameters collector(String collector) { + this.collector = Require.nonNull("Collector", collector); + return this; + } + + public GetDataParameters disown(boolean disown) { + this.disown = disown; + return this; + } + + public Map toMap() { + Map map = new HashMap<>(); + map.put("dataType", dataType.toString()); + map.put("request", request); + + if (collector != null) { + map.put("collector", collector); + } + + map.put("disown", disown); + + return Map.copyOf(map); + } +} From 2cb8736a5d73c83c9fedd87b7cf795fa76640d2c Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Sat, 20 Sep 2025 00:38:02 +0530 Subject: [PATCH 2/4] add tests --- .../bidi/network/NetworkCommandsTest.java | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) diff --git a/java/test/org/openqa/selenium/bidi/network/NetworkCommandsTest.java b/java/test/org/openqa/selenium/bidi/network/NetworkCommandsTest.java index b6347925cd67e..28572069ccf17 100644 --- a/java/test/org/openqa/selenium/bidi/network/NetworkCommandsTest.java +++ b/java/test/org/openqa/selenium/bidi/network/NetworkCommandsTest.java @@ -25,6 +25,7 @@ import java.time.Duration; import java.time.temporal.ChronoUnit; import java.util.Collections; +import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @@ -353,4 +354,160 @@ void throwsExceptionForInvalidContext() { .hasMessageContaining("no such frame"); } } + + @Test + @NeedsFreshDriver + void canAddDataCollector() { + try (Network network = new Network(driver)) { + AddDataCollectorParameters parameters = + new AddDataCollectorParameters(List.of(DataType.RESPONSE), 1024); + + String collector = network.addDataCollector(parameters); + assertThat(collector).isNotNull(); + } + } + + @Test + @NeedsFreshDriver + void canAddDataCollectorWithCollectorType() { + try (Network network = new Network(driver)) { + AddDataCollectorParameters parameters = + new AddDataCollectorParameters(List.of(DataType.RESPONSE), 2048).collectorType("blob"); + + String collector = network.addDataCollector(parameters); + assertThat(collector).isNotNull(); + } + } + + @Test + @NeedsFreshDriver + void canAddDataCollectorWithContexts() { + try (Network network = new Network(driver)) { + String contextId = driver.getWindowHandle(); + AddDataCollectorParameters parameters = + new AddDataCollectorParameters(List.of(DataType.RESPONSE), 1024) + .contexts(List.of(contextId)); + + String collector = network.addDataCollector(parameters); + assertThat(collector).isNotNull(); + } + } + + @Test + @NeedsFreshDriver + void canGetDataWithoutCollector() throws InterruptedException { + try (Network network = new Network(driver)) { + CountDownLatch latch = new CountDownLatch(1); + + network.onResponseCompleted( + responseDetails -> { + String requestId = responseDetails.getRequest().getRequestId(); + GetDataParameters parameters = new GetDataParameters(DataType.RESPONSE, requestId); + + // browsers don't store body bytes for responses without a collector + assertThatThrownBy(() -> network.getData(parameters)) + .isInstanceOf(BiDiException.class) + .hasMessageContaining("no such network data"); + + latch.countDown(); + }); + + BrowsingContext browsingContext = new BrowsingContext(driver, driver.getWindowHandle()); + browsingContext.navigate( + appServer.whereIs("/bidi/logEntryAdded.html"), ReadinessState.COMPLETE); + + boolean countdown = latch.await(5, TimeUnit.SECONDS); + assertThat(countdown).isTrue(); + } + } + + @Test + @NeedsFreshDriver + void canGetDataWithCollector() throws InterruptedException { + try (Network network = new Network(driver)) { + AddDataCollectorParameters collectorParams = + new AddDataCollectorParameters(List.of(DataType.RESPONSE), 1024); + String collector = network.addDataCollector(collectorParams); + + CountDownLatch latch = new CountDownLatch(1); + + network.onResponseCompleted( + responseDetails -> { + String requestId = responseDetails.getRequest().getRequestId(); + GetDataParameters parameters = + new GetDataParameters(DataType.RESPONSE, requestId).collector(collector); + + BytesValue result = network.getData(parameters); + assertThat(result).isNotNull(); + assertThat(result.getValue()).isNotNull(); + + latch.countDown(); + }); + + BrowsingContext browsingContext = new BrowsingContext(driver, driver.getWindowHandle()); + browsingContext.navigate( + appServer.whereIs("/bidi/logEntryAdded.html"), ReadinessState.COMPLETE); + + boolean countdown = latch.await(5, TimeUnit.SECONDS); + assertThat(countdown).isTrue(); + } + } + + @Test + @NeedsFreshDriver + void canGetDataWithDisown() throws InterruptedException { + try (Network network = new Network(driver)) { + AddDataCollectorParameters collectorParams = + new AddDataCollectorParameters(List.of(DataType.RESPONSE), 2048); + String collector = network.addDataCollector(collectorParams); + + CountDownLatch latch = new CountDownLatch(1); + + network.onResponseCompleted( + responseDetails -> { + String requestId = responseDetails.getRequest().getRequestId(); + GetDataParameters disownParams = + new GetDataParameters(DataType.RESPONSE, requestId) + .collector(collector) + .disown(true); + + // First fetch should succeed + BytesValue result = network.getData(disownParams); + assertThat(result).isNotNull(); + assertThat(result.getValue()).isNotNull(); + + // Second fetch with same collector should fail, because disowned + GetDataParameters secondParams = + new GetDataParameters(DataType.RESPONSE, requestId).collector(collector); + + assertThatThrownBy(() -> network.getData(secondParams)) + .isInstanceOf(BiDiException.class) + .hasMessageContaining("no such network data"); + + latch.countDown(); + }); + + BrowsingContext browsingContext = new BrowsingContext(driver, driver.getWindowHandle()); + browsingContext.navigate( + appServer.whereIs("/bidi/logEntryAdded.html"), ReadinessState.COMPLETE); + + boolean countdown = latch.await(5, TimeUnit.SECONDS); + assertThat(countdown).isTrue(); + } + } + + @Test + @NeedsFreshDriver + void canRemoveDataCollector() { + try (Network network = new Network(driver)) { + AddDataCollectorParameters parameters = + new AddDataCollectorParameters(List.of(DataType.RESPONSE), 1024); + + String collector = network.addDataCollector(parameters); + assertThat(collector).isNotNull(); + + // Remove the collector, should not throw any exception + network.removeDataCollector(collector); + } + } } From 28177769ffd32f4c6fe75437011802a7a4262f0b Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Sat, 20 Sep 2025 00:54:48 +0530 Subject: [PATCH 3/4] remove type warning --- java/src/org/openqa/selenium/bidi/module/Network.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/java/src/org/openqa/selenium/bidi/module/Network.java b/java/src/org/openqa/selenium/bidi/module/Network.java index dd7db9288a37c..8a91155b04bea 100644 --- a/java/src/org/openqa/selenium/bidi/module/Network.java +++ b/java/src/org/openqa/selenium/bidi/module/Network.java @@ -72,10 +72,9 @@ public class Network implements AutoCloseable { private final Function getDataResultMapper = jsonInput -> { Map result = jsonInput.read(Map.class); - @SuppressWarnings("unchecked") - Map bytesMap = (Map) result.get("bytes"); + Object bytesObj = result.get("bytes"); - try (StringReader reader = new StringReader(JSON.toJson(bytesMap)); + try (StringReader reader = new StringReader(JSON.toJson(bytesObj)); JsonInput bytesInput = JSON.newInput(reader)) { return BytesValue.fromJson(bytesInput); } From ce707f2fb3d2203e5b187dd6040494ecce9b4e49 Mon Sep 17 00:00:00 2001 From: Navin Chandra Date: Sat, 20 Sep 2025 11:49:30 +0530 Subject: [PATCH 4/4] fix getData mapper func --- .../openqa/selenium/bidi/module/Network.java | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/java/src/org/openqa/selenium/bidi/module/Network.java b/java/src/org/openqa/selenium/bidi/module/Network.java index 8a91155b04bea..3ba06e94260fe 100644 --- a/java/src/org/openqa/selenium/bidi/module/Network.java +++ b/java/src/org/openqa/selenium/bidi/module/Network.java @@ -17,7 +17,6 @@ package org.openqa.selenium.bidi.module; -import java.io.StringReader; import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -43,15 +42,12 @@ import org.openqa.selenium.bidi.network.ProvideResponseParameters; import org.openqa.selenium.bidi.network.ResponseDetails; import org.openqa.selenium.internal.Require; -import org.openqa.selenium.json.Json; import org.openqa.selenium.json.JsonInput; public class Network implements AutoCloseable { private final Set browsingContextIds; - private static final Json JSON = new Json(); - private final BiDi bidi; private final Event beforeRequestSentEvent = @@ -71,13 +67,18 @@ public class Network implements AutoCloseable { private final Function getDataResultMapper = jsonInput -> { - Map result = jsonInput.read(Map.class); - Object bytesObj = result.get("bytes"); - - try (StringReader reader = new StringReader(JSON.toJson(bytesObj)); - JsonInput bytesInput = JSON.newInput(reader)) { - return BytesValue.fromJson(bytesInput); + jsonInput.beginObject(); + BytesValue bytes = null; + while (jsonInput.hasNext()) { + String name = jsonInput.nextName(); + if ("bytes".equals(name)) { + bytes = BytesValue.fromJson(jsonInput); + } else { + jsonInput.skipValue(); + } } + jsonInput.endObject(); + return bytes; }; public Network(WebDriver driver) {