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 50b4ae35ca4f3..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) { @@ -127,6 +132,32 @@ public void clearListener(Event event) { } } + public void clearListener(Set browsingContextIds, Event event) { + 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)) { + 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); + } + } + 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..26fb5415ffe14 100644 --- a/java/src/org/openqa/selenium/bidi/module/LogInspector.java +++ b/java/src/org/openqa/selenium/bidi/module/LogInspector.java @@ -152,8 +152,18 @@ 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()); + 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 30b0ea059bde0..1a8954ec00fbb 100644 --- a/java/src/org/openqa/selenium/bidi/module/SpeculationInspector.java +++ b/java/src/org/openqa/selenium/bidi/module/SpeculationInspector.java @@ -69,8 +69,18 @@ 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()); + this.bidi.clearListener(this.prefetchStatusUpdatedEvent); } } 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..249db36281109 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,118 @@ 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)