From da1ed228e8fafc5d39bf9579ff90a10f57b8720b Mon Sep 17 00:00:00 2001 From: Delta456 Date: Fri, 24 Apr 2026 00:13:23 +0530 Subject: [PATCH 1/3] [java][BiDi] add clearListners via browsingContextIds for inspectors --- java/src/org/openqa/selenium/bidi/BiDi.java | 15 +++ .../bidi/module/BrowsingContextInspector.java | 22 ++++ .../selenium/bidi/module/LogInspector.java | 10 ++ .../bidi/module/SpeculationInspector.java | 10 ++ .../BrowsingContextInspectorTest.java | 69 +++++++++++ .../selenium/bidi/log/LogInspectorTest.java | 70 +++++++++++ .../speculation/SpeculationInspectorTest.java | 112 ++++++++++++++++++ 7 files changed, 308 insertions(+) diff --git a/java/src/org/openqa/selenium/bidi/BiDi.java b/java/src/org/openqa/selenium/bidi/BiDi.java index 50b4ae35ca4f3..a7dcd502b8130 100644 --- a/java/src/org/openqa/selenium/bidi/BiDi.java +++ b/java/src/org/openqa/selenium/bidi/BiDi.java @@ -127,6 +127,21 @@ public void clearListener(Event event) { } } + public void clearListener(Set browsingContextIds, Event event) { + Require.nonNull("List of browsing context ids", browsingContextIds); + Require.nonNull("Event to listen for", event); + + // The browser throws an error if we try to unsubscribe an event that was not subscribed in the + // first place + if (connection.isEventSubscribed(event)) { + send( + new Command<>( + "session.unsubscribe", + Map.of("contexts", browsingContextIds, "events", List.of(event.getMethod())))); + connection.clearListener(event); + } + } + public void removeListener(long id) { connection.removeListener(id); } diff --git a/java/src/org/openqa/selenium/bidi/module/BrowsingContextInspector.java b/java/src/org/openqa/selenium/bidi/module/BrowsingContextInspector.java index d37e0df91a064..78bb6ae73460e 100644 --- a/java/src/org/openqa/selenium/bidi/module/BrowsingContextInspector.java +++ b/java/src/org/openqa/selenium/bidi/module/BrowsingContextInspector.java @@ -20,6 +20,7 @@ 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; @@ -240,6 +241,27 @@ private void addNavigationEventListener(String name, Consumer co } } + public void clearListener(String browsingContextId) { + Require.nonNull("Browsing context id", browsingContextId); + clearListeners(Collections.singleton(browsingContextId)); + } + + public void clearListeners(Set browsingContextIds) { + Require.nonNull("Browsing context id list", browsingContextIds); + + List.of( + browsingContextCreated, + browsingContextDestroyed, + userPromptOpened, + userPromptClosed, + historyUpdated, + downloadWillBeginEvent, + downloadEndEvent) + .forEach(event -> this.bidi.clearListener(browsingContextIds, event)); + + navigationEventSet.forEach(event -> this.bidi.clearListener(browsingContextIds, event)); + } + @Override public void close() { this.bidi.clearListener(browsingContextCreated); diff --git a/java/src/org/openqa/selenium/bidi/module/LogInspector.java b/java/src/org/openqa/selenium/bidi/module/LogInspector.java index cc1d30f8bf7b2..53edd1ac72966 100644 --- a/java/src/org/openqa/selenium/bidi/module/LogInspector.java +++ b/java/src/org/openqa/selenium/bidi/module/LogInspector.java @@ -152,6 +152,16 @@ private long addLogEntryAddedListener(Consumer consumer) { } } + public void clearListener(String browsingContextId) { + Require.nonNull("Browsing context id", browsingContextId); + clearListeners(Collections.singleton(browsingContextId)); + } + + public void clearListeners(Set browsingContextIds) { + Require.nonNull("Browsing context id list", browsingContextIds); + this.bidi.clearListener(browsingContextIds, this.logEntryAddedEvent); + } + @Override public void close() { this.bidi.clearListener(Log.entryAdded()); diff --git a/java/src/org/openqa/selenium/bidi/module/SpeculationInspector.java b/java/src/org/openqa/selenium/bidi/module/SpeculationInspector.java index 30b0ea059bde0..57d9b1d6796eb 100644 --- a/java/src/org/openqa/selenium/bidi/module/SpeculationInspector.java +++ b/java/src/org/openqa/selenium/bidi/module/SpeculationInspector.java @@ -69,6 +69,16 @@ public void removeListener(long subscriptionId) { this.bidi.removeListener(subscriptionId); } + public void clearListener(String browsingContextId) { + Require.nonNull("Browsing context id", browsingContextId); + clearListeners(Collections.singleton(browsingContextId)); + } + + public void clearListeners(Set browsingContextIds) { + Require.nonNull("Browsing context id list", browsingContextIds); + this.bidi.clearListener(browsingContextIds, this.prefetchStatusUpdatedEvent); + } + @Override public void close() { this.bidi.clearListener(Speculation.prefetchStatusUpdated()); diff --git a/java/test/org/openqa/selenium/bidi/browsingcontext/BrowsingContextInspectorTest.java b/java/test/org/openqa/selenium/bidi/browsingcontext/BrowsingContextInspectorTest.java index 46045194da2a0..c9b7ba10d5c14 100644 --- a/java/test/org/openqa/selenium/bidi/browsingcontext/BrowsingContextInspectorTest.java +++ b/java/test/org/openqa/selenium/bidi/browsingcontext/BrowsingContextInspectorTest.java @@ -24,11 +24,15 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.openqa.selenium.testing.drivers.Browser.FIREFOX; +import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -318,6 +322,71 @@ void canListenToNavigationFailedEvent() } } + @Test + @NeedsFreshDriver + void canClearListenersForBrowsingContext() + throws ExecutionException, InterruptedException, TimeoutException { + try (BrowsingContextInspector inspector = new BrowsingContextInspector(driver)) { + CompletableFuture future = new CompletableFuture<>(); + inspector.onDomContentLoaded(future::complete); + + BrowsingContext context = new BrowsingContext(driver, driver.getWindowHandle()); + context.navigate(appServer.whereIs("/bidi/logEntryAdded.html"), ReadinessState.COMPLETE); + + NavigationInfo navigationInfo = future.get(5, TimeUnit.SECONDS); + assertThat(navigationInfo.getBrowsingContextId()).isEqualTo(context.getId()); + + // Clear listeners for this browsing context + inspector.clearListener(context.getId()); + + // Re-subscribe and verify events still work after re-subscribing + CompletableFuture newFuture = new CompletableFuture<>(); + inspector.onDomContentLoaded(newFuture::complete); + + context.navigate(appServer.whereIs("/simpleTest.html"), ReadinessState.COMPLETE); + + NavigationInfo newNavigationInfo = newFuture.get(5, TimeUnit.SECONDS); + assertThat(newNavigationInfo.getBrowsingContextId()).isEqualTo(context.getId()); + assertThat(newNavigationInfo.getUrl()).contains("/simpleTest.html"); + } + } + + @Test + @NeedsFreshDriver + void canClearListenersForMultipleBrowsingContexts() + throws ExecutionException, InterruptedException, TimeoutException { + Set browsingContextIds = new HashSet<>(); + browsingContextIds.add(driver.getWindowHandle()); + + try (BrowsingContextInspector inspector = new BrowsingContextInspector(driver)) { + List receivedEvents = new ArrayList<>(); + CountDownLatch latch = new CountDownLatch(1); + inspector.onNavigationStarted( + info -> { + receivedEvents.add(info); + latch.countDown(); + }); + + BrowsingContext context = new BrowsingContext(driver, driver.getWindowHandle()); + context.navigate(appServer.whereIs("/bidi/logEntryAdded.html"), ReadinessState.COMPLETE); + + latch.await(5, TimeUnit.SECONDS); + assertThat(receivedEvents).hasSizeGreaterThanOrEqualTo(1); + + // Clear listeners for the set of browsing context ids + inspector.clearListeners(browsingContextIds); + + // Re-subscribe with a new listener after clearing + CompletableFuture newFuture = new CompletableFuture<>(); + inspector.onNavigationStarted(newFuture::complete); + + context.navigate(appServer.whereIs("/simpleTest.html"), ReadinessState.COMPLETE); + + NavigationInfo newNavigationInfo = newFuture.get(5, TimeUnit.SECONDS); + assertThat(newNavigationInfo.getBrowsingContextId()).isEqualTo(context.getId()); + } + } + @Test @NeedsFreshDriver void canListenToHistoryUpdatedEvent() diff --git a/java/test/org/openqa/selenium/bidi/log/LogInspectorTest.java b/java/test/org/openqa/selenium/bidi/log/LogInspectorTest.java index 35ddedfaf8a58..bff1d5ee11e22 100644 --- a/java/test/org/openqa/selenium/bidi/log/LogInspectorTest.java +++ b/java/test/org/openqa/selenium/bidi/log/LogInspectorTest.java @@ -20,7 +20,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import java.util.ArrayList; import java.util.HashSet; +import java.util.List; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; @@ -430,6 +432,74 @@ void canListenToAnyTypeOfLogForMultipleBrowsingContexts() throws InterruptedExce } } + @Test + @NeedsFreshDriver + void canClearListenersForBrowsingContext() + throws ExecutionException, InterruptedException, TimeoutException { + String browsingContextId = driver.getWindowHandle(); + try (LogInspector logInspector = new LogInspector(driver)) { + CompletableFuture future = new CompletableFuture<>(); + logInspector.onConsoleEntry(future::complete); + + page = appServer.whereIs("/bidi/logEntryAdded.html"); + driver.get(page); + driver.findElement(By.id("consoleLog")).click(); + + ConsoleLogEntry logEntry = future.get(5, TimeUnit.SECONDS); + assertThat(logEntry.getText()).isEqualTo("Hello, world!"); + + // Clear listeners for this browsing context + logInspector.clearListener(browsingContextId); + + // Re-subscribe and verify events still work after re-subscribing + CompletableFuture newFuture = new CompletableFuture<>(); + logInspector.onConsoleEntry(newFuture::complete); + + driver.findElement(By.id("consoleLog")).click(); + + ConsoleLogEntry newLogEntry = newFuture.get(5, TimeUnit.SECONDS); + assertThat(newLogEntry.getText()).isEqualTo("Hello, world!"); + } + } + + @Test + @NeedsFreshDriver + void canClearListenersForMultipleBrowsingContexts() + throws ExecutionException, InterruptedException, TimeoutException { + Set browsingContextIds = new HashSet<>(); + browsingContextIds.add(driver.getWindowHandle()); + + try (LogInspector logInspector = new LogInspector(driver)) { + List receivedEntries = new ArrayList<>(); + CountDownLatch latch = new CountDownLatch(1); + logInspector.onConsoleEntry( + entry -> { + receivedEntries.add(entry); + latch.countDown(); + }); + + page = appServer.whereIs("/bidi/logEntryAdded.html"); + driver.get(page); + driver.findElement(By.id("consoleLog")).click(); + + latch.await(5, TimeUnit.SECONDS); + assertThat(receivedEntries).hasSize(1); + assertThat(receivedEntries.get(0).getText()).isEqualTo("Hello, world!"); + + // Clear listeners for the set of browsing context ids + logInspector.clearListeners(browsingContextIds); + + // Re-subscribe with a new listener after clearing + CompletableFuture newFuture = new CompletableFuture<>(); + logInspector.onConsoleEntry(newFuture::complete); + + driver.findElement(By.id("consoleLog")).click(); + + ConsoleLogEntry newLogEntry = newFuture.get(5, TimeUnit.SECONDS); + assertThat(newLogEntry.getText()).isEqualTo("Hello, world!"); + } + } + @Test @NeedsFreshDriver void canListenToLogsWithMultipleConsumers() diff --git a/java/test/org/openqa/selenium/bidi/speculation/SpeculationInspectorTest.java b/java/test/org/openqa/selenium/bidi/speculation/SpeculationInspectorTest.java index 459a3e058bbe3..244bf997838a0 100644 --- a/java/test/org/openqa/selenium/bidi/speculation/SpeculationInspectorTest.java +++ b/java/test/org/openqa/selenium/bidi/speculation/SpeculationInspectorTest.java @@ -22,8 +22,10 @@ import static org.openqa.selenium.testing.drivers.Browser.SAFARI; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; @@ -223,6 +225,116 @@ void canListenToPrefetchStatusUpdatedWithFailureEvents() throws InterruptedExcep assertThat(firstEvent.getStatus()).isIn(PreloadingStatus.PENDING, PreloadingStatus.FAILURE); } + @Test + @NeedsFreshDriver + @NotYetImplemented(FIREFOX) + @NotYetImplemented(SAFARI) + void canClearListenersForBrowsingContext() throws InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + List events = new ArrayList<>(); + + speculationInspector.onPrefetchStatusUpdated( + event -> { + events.add(event); + latch.countDown(); + }); + + String testUrl = appServer.whereIs("/common/blank.html"); + driver.get(testUrl); + + String prefetchTarget = appServer.whereIs("/common/dummy.xml"); + String speculationRules = + String.format( + "{\"prefetch\": [{\"source\": \"list\", \"urls\": [\"%s\"]}]}", prefetchTarget); + + addSpeculationRulesAndLink(speculationRules, prefetchTarget, "Test Link", "prefetch-page"); + + latch.await(5, TimeUnit.SECONDS); + assertThat(events).hasSizeGreaterThanOrEqualTo(1); + + // Clear listeners for this browsing context + speculationInspector.clearListener(driver.getWindowHandle()); + + // Re-subscribe after clearing + CountDownLatch newLatch = new CountDownLatch(1); + List newEvents = new ArrayList<>(); + + speculationInspector.onPrefetchStatusUpdated( + event -> { + newEvents.add(event); + newLatch.countDown(); + }); + + driver.get(testUrl); + + String prefetchTarget2 = appServer.whereIs("/common/square.png"); + String speculationRules2 = + String.format( + "{\"prefetch\": [{\"source\": \"list\", \"urls\": [\"%s\"]}]}", prefetchTarget2); + + addSpeculationRulesAndLink(speculationRules2, prefetchTarget2, "Test Link 2", "prefetch-page-2"); + + newLatch.await(5, TimeUnit.SECONDS); + assertThat(newEvents).hasSizeGreaterThanOrEqualTo(1); + assertThat(newEvents.get(0).getUrl()).isEqualTo(prefetchTarget2); + } + + @Test + @NeedsFreshDriver + @NotYetImplemented(FIREFOX) + @NotYetImplemented(SAFARI) + void canClearListenersForMultipleBrowsingContexts() throws InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + List events = new ArrayList<>(); + + speculationInspector.onPrefetchStatusUpdated( + event -> { + events.add(event); + latch.countDown(); + }); + + String testUrl = appServer.whereIs("/common/blank.html"); + driver.get(testUrl); + + String prefetchTarget = appServer.whereIs("/common/dummy.xml"); + String speculationRules = + String.format( + "{\"prefetch\": [{\"source\": \"list\", \"urls\": [\"%s\"]}]}", prefetchTarget); + + addSpeculationRulesAndLink(speculationRules, prefetchTarget, "Test Link", "prefetch-page"); + + latch.await(5, TimeUnit.SECONDS); + assertThat(events).hasSizeGreaterThanOrEqualTo(1); + + // Clear listeners for the set of browsing context ids + Set browsingContextIds = new HashSet(); + browsingContextIds.add(driver.getWindowHandle()); + speculationInspector.clearListeners(browsingContextIds); + + // Re-subscribe after clearing + CountDownLatch newLatch = new CountDownLatch(1); + List newEvents = new ArrayList<>(); + + speculationInspector.onPrefetchStatusUpdated( + event -> { + newEvents.add(event); + newLatch.countDown(); + }); + + driver.get(testUrl); + + String prefetchTarget2 = appServer.whereIs("/common/square.png"); + String speculationRules2 = + String.format( + "{\"prefetch\": [{\"source\": \"list\", \"urls\": [\"%s\"]}]}", prefetchTarget2); + + addSpeculationRulesAndLink(speculationRules2, prefetchTarget2, "Test Link 2", "prefetch-page-2"); + + newLatch.await(5, TimeUnit.SECONDS); + assertThat(newEvents).hasSizeGreaterThanOrEqualTo(1); + assertThat(newEvents.get(0).getUrl()).isEqualTo(prefetchTarget2); + } + @Test @NeedsFreshDriver @NotYetImplemented(FIREFOX) From cf94c6b6bdb6030d5fcdbeb93053f95ff61a0efb Mon Sep 17 00:00:00 2001 From: Delta456 Date: Fri, 24 Apr 2026 00:45:03 +0530 Subject: [PATCH 2/3] style: wrap addSpeculationRulesAndLink params for formatting --- .../selenium/bidi/speculation/SpeculationInspectorTest.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/java/test/org/openqa/selenium/bidi/speculation/SpeculationInspectorTest.java b/java/test/org/openqa/selenium/bidi/speculation/SpeculationInspectorTest.java index 244bf997838a0..249db36281109 100644 --- a/java/test/org/openqa/selenium/bidi/speculation/SpeculationInspectorTest.java +++ b/java/test/org/openqa/selenium/bidi/speculation/SpeculationInspectorTest.java @@ -272,7 +272,8 @@ void canClearListenersForBrowsingContext() throws InterruptedException { String.format( "{\"prefetch\": [{\"source\": \"list\", \"urls\": [\"%s\"]}]}", prefetchTarget2); - addSpeculationRulesAndLink(speculationRules2, prefetchTarget2, "Test Link 2", "prefetch-page-2"); + addSpeculationRulesAndLink( + speculationRules2, prefetchTarget2, "Test Link 2", "prefetch-page-2"); newLatch.await(5, TimeUnit.SECONDS); assertThat(newEvents).hasSizeGreaterThanOrEqualTo(1); @@ -328,7 +329,8 @@ void canClearListenersForMultipleBrowsingContexts() throws InterruptedException String.format( "{\"prefetch\": [{\"source\": \"list\", \"urls\": [\"%s\"]}]}", prefetchTarget2); - addSpeculationRulesAndLink(speculationRules2, prefetchTarget2, "Test Link 2", "prefetch-page-2"); + addSpeculationRulesAndLink( + speculationRules2, prefetchTarget2, "Test Link 2", "prefetch-page-2"); newLatch.await(5, TimeUnit.SECONDS); assertThat(newEvents).hasSizeGreaterThanOrEqualTo(1); From 4b3763713ee3d6298c0fb4f45e925c35db65ded6 Mon Sep 17 00:00:00 2001 From: Delta456 Date: Wed, 29 Apr 2026 00:39:52 +0530 Subject: [PATCH 3/3] refactor as per comments --- .idea/misc.xml | 5 ++-- java/src/org/openqa/selenium/bidi/BiDi.java | 24 +++++++++++++++---- .../selenium/bidi/module/LogInspector.java | 2 +- .../bidi/module/SpeculationInspector.java | 2 +- 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/.idea/misc.xml b/.idea/misc.xml index 43262c34163c2..24c10f29685da 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,9 +1,10 @@ + - + - + \ No newline at end of file diff --git a/java/src/org/openqa/selenium/bidi/BiDi.java b/java/src/org/openqa/selenium/bidi/BiDi.java index a7dcd502b8130..85205aa0e90ee 100644 --- a/java/src/org/openqa/selenium/bidi/BiDi.java +++ b/java/src/org/openqa/selenium/bidi/BiDi.java @@ -21,6 +21,8 @@ import java.io.Closeable; import java.time.Duration; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -34,6 +36,7 @@ public class BiDi implements Closeable { private final Duration timeout; private final Connection connection; + private final Map, List> contextListenerIds = new HashMap<>(); /** * @deprecated Use constructor with timeout parameter: {@link #BiDi(Connection, Duration)} @@ -104,7 +107,7 @@ public long addListener(String browsingContextId, Event event, Consumer long addListener(Set browsingContextIds, Event event, Consumer handler) { - Require.nonNull("List of browsing context ids", browsingContextIds); + Require.nonEmpty("List of browsing context ids", browsingContextIds); Require.nonNull("Event to listen for", event); Require.nonNull("Handler to call", handler); @@ -113,7 +116,9 @@ public long addListener(Set browsingContextIds, Event event, Cons "session.subscribe", Map.of("contexts", browsingContextIds, "events", List.of(event.getMethod())))); - return connection.addListener(event, handler); + long id = connection.addListener(event, handler); + contextListenerIds.computeIfAbsent(event, k -> new ArrayList<>()).add(id); + return id; } public void clearListener(Event event) { @@ -128,16 +133,27 @@ public void clearListener(Event event) { } public void clearListener(Set browsingContextIds, Event event) { - Require.nonNull("List of browsing context ids", browsingContextIds); + Require.nonEmpty("List of browsing context ids", browsingContextIds); Require.nonNull("Event to listen for", event); // The browser throws an error if we try to unsubscribe an event that was not subscribed in the // first place - if (connection.isEventSubscribed(event)) { + if (!connection.isEventSubscribed(event)) { + return; + } + + List ids = contextListenerIds.remove(event); + if (ids != null && !ids.isEmpty()) { + // Subscription was context-specific: unsubscribe only for those contexts. send( new Command<>( "session.unsubscribe", Map.of("contexts", browsingContextIds, "events", List.of(event.getMethod())))); + ids.forEach(connection::removeListener); + } else { + // Subscription was global: context-specific unsubscription is invalid per the BiDi protocol, + // so fall back to a global unsubscription. + send(new Command<>("session.unsubscribe", Map.of("events", List.of(event.getMethod())))); connection.clearListener(event); } } diff --git a/java/src/org/openqa/selenium/bidi/module/LogInspector.java b/java/src/org/openqa/selenium/bidi/module/LogInspector.java index 53edd1ac72966..26fb5415ffe14 100644 --- a/java/src/org/openqa/selenium/bidi/module/LogInspector.java +++ b/java/src/org/openqa/selenium/bidi/module/LogInspector.java @@ -164,6 +164,6 @@ public void clearListeners(Set browsingContextIds) { @Override public void close() { - this.bidi.clearListener(Log.entryAdded()); + this.bidi.clearListener(this.logEntryAddedEvent); } } diff --git a/java/src/org/openqa/selenium/bidi/module/SpeculationInspector.java b/java/src/org/openqa/selenium/bidi/module/SpeculationInspector.java index 57d9b1d6796eb..1a8954ec00fbb 100644 --- a/java/src/org/openqa/selenium/bidi/module/SpeculationInspector.java +++ b/java/src/org/openqa/selenium/bidi/module/SpeculationInspector.java @@ -81,6 +81,6 @@ public void clearListeners(Set browsingContextIds) { @Override public void close() { - this.bidi.clearListener(Speculation.prefetchStatusUpdated()); + this.bidi.clearListener(this.prefetchStatusUpdatedEvent); } }