diff --git a/java/src/org/openqa/selenium/bidi/module/Script.java b/java/src/org/openqa/selenium/bidi/module/Script.java index 10bd588d230a2..0a95069d2c9b8 100644 --- a/java/src/org/openqa/selenium/bidi/module/Script.java +++ b/java/src/org/openqa/selenium/bidi/module/Script.java @@ -343,11 +343,11 @@ public void removePreloadScript(String id) { this.bidi.send(new Command<>("script.removePreloadScript", Map.of("script", id))); } - public void onMessage(Consumer consumer) { + public long onMessage(Consumer consumer) { if (browsingContextIds.isEmpty()) { - this.bidi.addListener(messageEvent, consumer); + return this.bidi.addListener(messageEvent, consumer); } else { - this.bidi.addListener(browsingContextIds, messageEvent, consumer); + return this.bidi.addListener(browsingContextIds, messageEvent, consumer); } } diff --git a/java/src/org/openqa/selenium/remote/BUILD.bazel b/java/src/org/openqa/selenium/remote/BUILD.bazel index e3a54d3bc2bb0..07cf0a0ffea52 100644 --- a/java/src/org/openqa/selenium/remote/BUILD.bazel +++ b/java/src/org/openqa/selenium/remote/BUILD.bazel @@ -44,6 +44,7 @@ java_library( name = "api", srcs = glob(["**/*.java"]), resources = [ + ":bidi-mutation-listener", ":get-attribute", ":is-displayed", ], @@ -59,6 +60,7 @@ java_library( "//java/src/org/openqa/selenium/bidi", "//java/src/org/openqa/selenium/bidi/log", "//java/src/org/openqa/selenium/bidi/module", + "//java/src/org/openqa/selenium/bidi/script", "//java/src/org/openqa/selenium/concurrent", "//java/src/org/openqa/selenium/devtools", "//java/src/org/openqa/selenium/json", @@ -84,3 +86,9 @@ copy_file( src = "//javascript/atoms/fragments:is-displayed.js", out = "isDisplayed.js", ) + +copy_file( + name = "bidi-mutation-listener", + src = "//javascript/bidi-support:bidi-mutation-listener.js", + out = "bidi-mutation-listener.js", +) diff --git a/java/src/org/openqa/selenium/remote/DomMutation.java b/java/src/org/openqa/selenium/remote/DomMutation.java new file mode 100644 index 0000000000000..a79672a020649 --- /dev/null +++ b/java/src/org/openqa/selenium/remote/DomMutation.java @@ -0,0 +1,52 @@ +// 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.remote; + +import org.openqa.selenium.WebElement; + +public class DomMutation { + + private final WebElement element; + private final String attributeName; + private final String currentValue; + private final String oldValue; + + public DomMutation( + WebElement element, String attributeName, String currentValue, String oldValue) { + this.element = element; + this.attributeName = attributeName; + this.currentValue = currentValue; + this.oldValue = oldValue; + } + + public WebElement getElement() { + return element; + } + + public String getAttributeName() { + return attributeName; + } + + public String getCurrentValue() { + return currentValue; + } + + public String getOldValue() { + return oldValue; + } +} diff --git a/java/src/org/openqa/selenium/remote/RemoteScript.java b/java/src/org/openqa/selenium/remote/RemoteScript.java index eb0c3bc332393..b070f71ada309 100644 --- a/java/src/org/openqa/selenium/remote/RemoteScript.java +++ b/java/src/org/openqa/selenium/remote/RemoteScript.java @@ -17,23 +17,41 @@ package org.openqa.selenium.remote; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.openqa.selenium.json.Json.MAP_TYPE; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.Map; import java.util.function.Consumer; import org.openqa.selenium.Beta; +import org.openqa.selenium.By; import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; import org.openqa.selenium.bidi.BiDi; import org.openqa.selenium.bidi.HasBiDi; import org.openqa.selenium.bidi.log.ConsoleLogEntry; import org.openqa.selenium.bidi.log.JavascriptLogEntry; import org.openqa.selenium.bidi.module.LogInspector; +import org.openqa.selenium.bidi.script.ChannelValue; +import org.openqa.selenium.json.Json; @Beta class RemoteScript implements Script { + + private static final Json JSON = new Json(); private final BiDi biDi; private final LogInspector logInspector; + private final org.openqa.selenium.bidi.module.Script script; + + private final WebDriver driver; public RemoteScript(WebDriver driver) { + this.driver = driver; this.biDi = ((HasBiDi) driver).getBiDi(); this.logInspector = new LogInspector(driver); + this.script = new org.openqa.selenium.bidi.module.Script(driver); } @Override @@ -55,4 +73,52 @@ public long addJavaScriptErrorHandler(Consumer consumer) { public void removeJavaScriptErrorHandler(long id) { this.biDi.removeListener(id); } + + @Override + public long addDomMutationHandler(Consumer consumer) { + String scriptValue; + try (InputStream stream = + RemoteScript.class.getResourceAsStream( + "/org/openqa/selenium/remote/bidi-mutation-listener.js")) { + if (stream == null) { + throw new IllegalStateException("Unable to find helper script"); + } + scriptValue = new String(stream.readAllBytes(), UTF_8); + } catch (IOException e) { + throw new IllegalStateException("Unable to read helper script"); + } + + this.script.addPreloadScript(scriptValue, List.of(new ChannelValue("channel_name"))); + + return this.script.onMessage( + message -> { + String value = message.getData().getValue().get().toString(); + + Map values = JSON.toType(value, MAP_TYPE); + String id = (String) values.get("target"); + + List elements; + + synchronized (this) { + elements = + this.driver.findElements( + By.cssSelector(String.format("*[data-__webdriver_id='%s']", id))); + } + + if (!elements.isEmpty()) { + DomMutation event = + new DomMutation( + elements.get(0), + String.valueOf(values.get("name")), + String.valueOf(values.get("value")), + String.valueOf(values.get("oldValue"))); + consumer.accept(event); + } + }); + } + + @Override + public void removeDomMutationHandler(long id) { + this.biDi.removeListener(id); + } } diff --git a/java/src/org/openqa/selenium/remote/Script.java b/java/src/org/openqa/selenium/remote/Script.java index 9993c379cf621..e02527010a076 100644 --- a/java/src/org/openqa/selenium/remote/Script.java +++ b/java/src/org/openqa/selenium/remote/Script.java @@ -32,4 +32,8 @@ public interface Script { long addJavaScriptErrorHandler(Consumer consumer); void removeJavaScriptErrorHandler(long id); + + long addDomMutationHandler(Consumer event); + + void removeDomMutationHandler(long id); } diff --git a/java/test/org/openqa/selenium/WebScriptTest.java b/java/test/org/openqa/selenium/WebScriptTest.java index a60eb8b9ee290..c05d630ffb9f4 100644 --- a/java/test/org/openqa/selenium/WebScriptTest.java +++ b/java/test/org/openqa/selenium/WebScriptTest.java @@ -17,14 +17,17 @@ package org.openqa.selenium; +import static java.util.concurrent.TimeUnit.SECONDS; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.fail; +import static org.openqa.selenium.support.ui.ExpectedConditions.visibilityOf; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; +import java.time.Duration; +import java.util.concurrent.*; import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -33,7 +36,9 @@ import org.openqa.selenium.bidi.log.LogLevel; import org.openqa.selenium.environment.webserver.AppServer; import org.openqa.selenium.environment.webserver.NettyAppServer; +import org.openqa.selenium.remote.DomMutation; import org.openqa.selenium.remote.RemoteWebDriver; +import org.openqa.selenium.support.ui.WebDriverWait; import org.openqa.selenium.testing.JupiterTestBase; class WebScriptTest extends JupiterTestBase { @@ -186,4 +191,58 @@ void canAddMultipleHandlers() throws ExecutionException, InterruptedException, T assertThat(logEntry2.getType()).isEqualTo("javascript"); assertThat(logEntry2.getLevel()).isEqualTo(LogLevel.ERROR); } + + @Test + void canAddDomMutationHandler() throws InterruptedException { + AtomicReference seen = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + + ((RemoteWebDriver) driver) + .script() + .addDomMutationHandler( + mutation -> { + seen.set(mutation); + latch.countDown(); + }); + + driver.get(pages.dynamicPage); + + WebElement reveal = driver.findElement(By.id("reveal")); + reveal.click(); + WebElement revealed = driver.findElement(By.id("revealed")); + + new WebDriverWait(driver, Duration.ofSeconds(10)).until(visibilityOf(revealed)); + + Assertions.assertThat(latch.await(10, SECONDS)).isTrue(); + assertThat(seen.get().getAttributeName()).isEqualTo("style"); + assertThat(seen.get().getCurrentValue()).isEmpty(); + assertThat(seen.get().getOldValue()).isEqualTo("display:none;"); + } + + @Test + void canRemoveDomMutationHandler() throws InterruptedException { + AtomicReference seen = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + + long id = + ((RemoteWebDriver) driver) + .script() + .addDomMutationHandler( + mutation -> { + seen.set(mutation); + latch.countDown(); + }); + + driver.get(pages.dynamicPage); + + ((RemoteWebDriver) driver).script().removeDomMutationHandler(id); + + WebElement reveal = driver.findElement(By.id("reveal")); + reveal.click(); + WebElement revealed = driver.findElement(By.id("revealed")); + + new WebDriverWait(driver, Duration.ofSeconds(10)).until(visibilityOf(revealed)); + + Assertions.assertThat(latch.await(10, SECONDS)).isFalse(); + } } diff --git a/javascript/bidi-support/BUILD.bazel b/javascript/bidi-support/BUILD.bazel index 2c023ea6098f3..916adebab0655 100644 --- a/javascript/bidi-support/BUILD.bazel +++ b/javascript/bidi-support/BUILD.bazel @@ -2,7 +2,10 @@ package(default_visibility = [ "//dotnet/src/webdriver:__pkg__", "//java/src/org/openqa/selenium/bidi:__pkg__", "//java/src/org/openqa/selenium/remote:__pkg__", + "//javascript:__pkg__", + "//javascript:__subpackages__", "//javascript/node/selenium-webdriver:__pkg__", + "//javascript/node/selenium-webdriver/lib/atoms:__subpackages__", ]) exports_files([