diff --git a/java/src/org/openqa/selenium/remote/BUILD.bazel b/java/src/org/openqa/selenium/remote/BUILD.bazel index c26cb18d2d787..e3a54d3bc2bb0 100644 --- a/java/src/org/openqa/selenium/remote/BUILD.bazel +++ b/java/src/org/openqa/selenium/remote/BUILD.bazel @@ -57,6 +57,8 @@ java_library( deps = [ "//java/src/org/openqa/selenium:core", "//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/concurrent", "//java/src/org/openqa/selenium/devtools", "//java/src/org/openqa/selenium/json", diff --git a/java/src/org/openqa/selenium/remote/RemoteScript.java b/java/src/org/openqa/selenium/remote/RemoteScript.java new file mode 100644 index 0000000000000..eb0c3bc332393 --- /dev/null +++ b/java/src/org/openqa/selenium/remote/RemoteScript.java @@ -0,0 +1,58 @@ +// 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 java.util.function.Consumer; +import org.openqa.selenium.Beta; +import org.openqa.selenium.WebDriver; +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; + +@Beta +class RemoteScript implements Script { + private final BiDi biDi; + private final LogInspector logInspector; + + public RemoteScript(WebDriver driver) { + this.biDi = ((HasBiDi) driver).getBiDi(); + this.logInspector = new LogInspector(driver); + } + + @Override + public long addConsoleMessageHandler(Consumer consumer) { + return this.logInspector.onConsoleEntry(consumer); + } + + @Override + public void removeConsoleMessageHandler(long id) { + this.biDi.removeListener(id); + } + + @Override + public long addJavaScriptErrorHandler(Consumer consumer) { + return this.logInspector.onJavaScriptException(consumer); + } + + @Override + public void removeJavaScriptErrorHandler(long id) { + this.biDi.removeListener(id); + } +} diff --git a/java/src/org/openqa/selenium/remote/RemoteWebDriver.java b/java/src/org/openqa/selenium/remote/RemoteWebDriver.java index 3462231c7f17b..db470c89757eb 100644 --- a/java/src/org/openqa/selenium/remote/RemoteWebDriver.java +++ b/java/src/org/openqa/selenium/remote/RemoteWebDriver.java @@ -124,6 +124,8 @@ public class RemoteWebDriver private Logs remoteLogs; private LocalLogs localLogs; + private Script remoteScript; + // For cglib protected RemoteWebDriver() { this.capabilities = init(new ImmutableCapabilities()); @@ -486,6 +488,13 @@ public Options manage() { return new RemoteWebDriverOptions(); } + public Script script() { + if (this.remoteScript == null) { + this.remoteScript = new RemoteScript(this); + } + return this.remoteScript; + } + protected JsonToWebElementConverter getElementConverter() { return converter; } diff --git a/java/src/org/openqa/selenium/remote/Script.java b/java/src/org/openqa/selenium/remote/Script.java new file mode 100644 index 0000000000000..9993c379cf621 --- /dev/null +++ b/java/src/org/openqa/selenium/remote/Script.java @@ -0,0 +1,35 @@ +// 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 java.util.function.Consumer; +import org.openqa.selenium.Beta; +import org.openqa.selenium.bidi.log.ConsoleLogEntry; +import org.openqa.selenium.bidi.log.JavascriptLogEntry; + +@Beta +public interface Script { + + long addConsoleMessageHandler(Consumer consumer); + + void removeConsoleMessageHandler(long id); + + long addJavaScriptErrorHandler(Consumer consumer); + + void removeJavaScriptErrorHandler(long id); +} diff --git a/java/test/org/openqa/selenium/WebScriptTest.java b/java/test/org/openqa/selenium/WebScriptTest.java new file mode 100644 index 0000000000000..a60eb8b9ee290 --- /dev/null +++ b/java/test/org/openqa/selenium/WebScriptTest.java @@ -0,0 +1,189 @@ +// 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; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.fail; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Consumer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.openqa.selenium.bidi.log.ConsoleLogEntry; +import org.openqa.selenium.bidi.log.JavascriptLogEntry; +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.RemoteWebDriver; +import org.openqa.selenium.testing.JupiterTestBase; + +class WebScriptTest extends JupiterTestBase { + + String page; + private AppServer server; + + @BeforeEach + public void setUp() { + server = new NettyAppServer(); + server.start(); + } + + @AfterEach + public void cleanUp() { + driver.quit(); + } + + @Test + void canAddConsoleMessageHandler() + throws ExecutionException, InterruptedException, TimeoutException { + CompletableFuture future = new CompletableFuture<>(); + + long id = ((RemoteWebDriver) driver).script().addConsoleMessageHandler(future::complete); + + page = server.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!"); + assertThat(logEntry.getArgs().size()).isEqualTo(1); + assertThat(logEntry.getArgs().get(0).getType()).isEqualTo("string"); + assertThat(logEntry.getType()).isEqualTo("console"); + assertThat(logEntry.getLevel()).isEqualTo(LogLevel.INFO); + assertThat(logEntry.getMethod()).isEqualTo("log"); + + ((RemoteWebDriver) driver).script().removeConsoleMessageHandler(id); + } + + @Test + void canRemoveConsoleMessageHandler() + throws ExecutionException, InterruptedException, TimeoutException { + CompletableFuture future1 = new CompletableFuture<>(); + CompletableFuture future2 = new CompletableFuture<>(); + + // Adding two consumers + Consumer consumer1 = future1::complete; + Consumer consumer2 = future2::complete; + + long id1 = ((RemoteWebDriver) driver).script().addConsoleMessageHandler(consumer1); + long id2 = ((RemoteWebDriver) driver).script().addConsoleMessageHandler(consumer2); + + // Removing the second consumer, so it will no longer get the console message. + ((RemoteWebDriver) driver).script().removeConsoleMessageHandler(id2); + + page = server.whereIs("/bidi/logEntryAdded.html"); + driver.get(page); + driver.findElement(By.id("consoleLog")).click(); + + ConsoleLogEntry logEntry = future1.get(5, TimeUnit.SECONDS); + assertThat(logEntry.getText()).isEqualTo("Hello, world!"); + + try { + future2.get(5, TimeUnit.SECONDS); + fail("Should be able to read the console messages"); + } catch (TimeoutException e) { + assertThat(e).isNotNull(); + } + ((RemoteWebDriver) driver).script().removeConsoleMessageHandler(id1); + } + + @Test + void canAddJsErrorHandler() throws ExecutionException, InterruptedException, TimeoutException { + CompletableFuture future = new CompletableFuture<>(); + + long id = ((RemoteWebDriver) driver).script().addJavaScriptErrorHandler(future::complete); + + page = server.whereIs("/bidi/logEntryAdded.html"); + driver.get(page); + driver.findElement(By.id("jsException")).click(); + + JavascriptLogEntry logEntry = future.get(5, TimeUnit.SECONDS); + + assertThat(logEntry.getText()).isEqualTo("Error: Not working"); + assertThat(logEntry.getType()).isEqualTo("javascript"); + assertThat(logEntry.getLevel()).isEqualTo(LogLevel.ERROR); + + ((RemoteWebDriver) driver).script().removeJavaScriptErrorHandler(id); + } + + @Test + void canRemoveJsErrorHandler() throws ExecutionException, InterruptedException, TimeoutException { + CompletableFuture future1 = new CompletableFuture<>(); + CompletableFuture future2 = new CompletableFuture<>(); + + // Adding two consumers + Consumer consumer1 = future1::complete; + Consumer consumer2 = future2::complete; + + long id1 = ((RemoteWebDriver) driver).script().addJavaScriptErrorHandler(consumer1); + long id2 = ((RemoteWebDriver) driver).script().addJavaScriptErrorHandler(consumer2); + + // Removing the second consumer, so it will no longer get the JS error. + ((RemoteWebDriver) driver).script().removeJavaScriptErrorHandler(id2); + + page = server.whereIs("/bidi/logEntryAdded.html"); + driver.get(page); + driver.findElement(By.id("jsException")).click(); + + JavascriptLogEntry logEntry = future1.get(5, TimeUnit.SECONDS); + assertThat(logEntry.getText()).isEqualTo("Error: Not working"); + assertThat(logEntry.getType()).isEqualTo("javascript"); + assertThat(logEntry.getLevel()).isEqualTo(LogLevel.ERROR); + + try { + future2.get(5, TimeUnit.SECONDS); + fail("Should be able to read the JS errors"); + } catch (TimeoutException e) { + assertThat(e).isNotNull(); + } + + ((RemoteWebDriver) driver).script().removeConsoleMessageHandler(id1); + } + + @Test + void canAddMultipleHandlers() throws ExecutionException, InterruptedException, TimeoutException { + CompletableFuture future1 = new CompletableFuture<>(); + CompletableFuture future2 = new CompletableFuture<>(); + + // Adding two consumers + Consumer consumer1 = future1::complete; + Consumer consumer2 = future2::complete; + + long id1 = ((RemoteWebDriver) driver).script().addJavaScriptErrorHandler(consumer1); + long id2 = ((RemoteWebDriver) driver).script().addJavaScriptErrorHandler(consumer2); + + page = server.whereIs("/bidi/logEntryAdded.html"); + driver.get(page); + driver.findElement(By.id("jsException")).click(); + + JavascriptLogEntry logEntry1 = future1.get(5, TimeUnit.SECONDS); + assertThat(logEntry1.getText()).isEqualTo("Error: Not working"); + assertThat(logEntry1.getType()).isEqualTo("javascript"); + assertThat(logEntry1.getLevel()).isEqualTo(LogLevel.ERROR); + + JavascriptLogEntry logEntry2 = future2.get(5, TimeUnit.SECONDS); + assertThat(logEntry2.getText()).isEqualTo("Error: Not working"); + assertThat(logEntry2.getType()).isEqualTo("javascript"); + assertThat(logEntry2.getLevel()).isEqualTo(LogLevel.ERROR); + } +}