From 057a7a992c9e401882b7278e63a9602481e1bdd5 Mon Sep 17 00:00:00 2001 From: Puja Jagani Date: Thu, 1 Aug 2024 17:15:10 +0530 Subject: [PATCH] [bidi][java] Add execute script high-level API Related to #13992 --- .../selenium/bidi/script/LocalValue.java | 84 +++++ .../openqa/selenium/remote/RemoteScript.java | 35 ++ .../org/openqa/selenium/remote/Script.java | 3 + .../openqa/selenium/WebScriptExecuteTest.java | 330 ++++++++++++++++++ 4 files changed, 452 insertions(+) create mode 100644 java/test/org/openqa/selenium/WebScriptExecuteTest.java diff --git a/java/src/org/openqa/selenium/bidi/script/LocalValue.java b/java/src/org/openqa/selenium/bidi/script/LocalValue.java index 907a29f63e6e9..cd82ceceeca43 100644 --- a/java/src/org/openqa/selenium/bidi/script/LocalValue.java +++ b/java/src/org/openqa/selenium/bidi/script/LocalValue.java @@ -17,12 +17,20 @@ package org.openqa.selenium.bidi.script; +import java.math.BigInteger; +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import org.openqa.selenium.json.Json; public abstract class LocalValue { + private static Json JSON = new Json(); + enum SpecialNumberType { NAN("NaN"), MINUS_ZERO("-0"), @@ -123,4 +131,80 @@ public static LocalValue remoteReference(String handle, String sharedId) { public static LocalValue remoteReference(RemoteReference.Type type, String id) { return new RemoteReference(type, id); } + + public static LocalValue getArgument(Object arg) { + LocalValue localValue = null; + + if (arg instanceof String) { + switch ((String) arg) { + case "undefined": + localValue = undefinedValue(); + break; + case "null": + localValue = nullValue(); + break; + case "-Infinity": + localValue = numberValue(SpecialNumberType.MINUS_INFINITY); + break; + case "Infinity": + localValue = numberValue(SpecialNumberType.INFINITY); + break; + case "NaN": + localValue = numberValue(SpecialNumberType.NAN); + break; + case "-0": + localValue = numberValue(SpecialNumberType.MINUS_ZERO); + break; + default: + localValue = stringValue((String) arg); + break; + } + } else if (arg instanceof Number) { + if (arg instanceof Integer || arg instanceof Long) { + localValue = numberValue(((Number) arg).longValue()); + } else if (arg instanceof Double || arg instanceof Float) { + localValue = numberValue(((Number) arg).doubleValue()); + } else if (arg instanceof BigInteger) { + localValue = bigIntValue(arg.toString()); + } + } else if (arg instanceof Boolean) { + localValue = booleanValue((Boolean) arg); + } else if (arg instanceof Instant) { + localValue = dateValue(((Instant) arg).toString()); + } else if (arg instanceof Map) { + Map map = new HashMap<>(); + for (Map.Entry entry : ((Map) arg).entrySet()) { + Object key = + (entry.getKey() instanceof String) ? entry.getKey() : getArgument(entry.getKey()); + map.put(key, getArgument(entry.getValue())); + } + localValue = mapValue(map); + } else if (arg instanceof List) { + List values = new ArrayList<>(); + ((List) arg).forEach(value -> values.add(getArgument(value))); + localValue = arrayValue(values); + } else if (arg instanceof Set) { + Set values = new HashSet<>(); + ((Set) arg).forEach(value -> values.add(getArgument(value))); + localValue = setValue(values); + } else if (arg instanceof RegExpValue) { + localValue = (RegExpValue) arg; + } else { + String json = JSON.toJson(arg); + Map objectMap = JSON.toType(json, Map.class); + + Map map = new HashMap<>(); + + for (Map.Entry entry : objectMap.entrySet()) { + Object key = + (entry.getKey() instanceof String) ? entry.getKey() : getArgument(entry.getKey()); + map.put(key, getArgument(entry.getValue())); + } + localValue = objectValue(map); + + return localValue; + } + + return localValue; + } } diff --git a/java/src/org/openqa/selenium/remote/RemoteScript.java b/java/src/org/openqa/selenium/remote/RemoteScript.java index df47192e87ace..601fce6fa42af 100644 --- a/java/src/org/openqa/selenium/remote/RemoteScript.java +++ b/java/src/org/openqa/selenium/remote/RemoteScript.java @@ -22,12 +22,16 @@ import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.function.Consumer; import org.openqa.selenium.Beta; import org.openqa.selenium.By; import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebDriverException; import org.openqa.selenium.WebElement; import org.openqa.selenium.bidi.BiDi; import org.openqa.selenium.bidi.HasBiDi; @@ -35,6 +39,11 @@ 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.bidi.script.EvaluateResult; +import org.openqa.selenium.bidi.script.EvaluateResultExceptionValue; +import org.openqa.selenium.bidi.script.EvaluateResultSuccess; +import org.openqa.selenium.bidi.script.LocalValue; +import org.openqa.selenium.bidi.script.RemoteValue; import org.openqa.selenium.json.Json; @Beta @@ -131,4 +140,30 @@ public String pin(String script) { public void unpin(String id) { this.script.removePreloadScript(id); } + + @Override + public RemoteValue execute(String script, Object... args) { + String browsingContextId = this.driver.getWindowHandle(); + + List arguments = new ArrayList<>(); + + Arrays.stream(args).forEach(arg -> arguments.add(LocalValue.getArgument(arg))); + + EvaluateResult result = + this.script.callFunctionInBrowsingContext( + browsingContextId, + script, + true, + Optional.of(arguments), + Optional.empty(), + Optional.empty()); + + if (result.getResultType().equals(EvaluateResult.Type.SUCCESS)) { + return ((EvaluateResultSuccess) result).getResult(); + } else { + throw new WebDriverException( + "Error while executing script: " + + ((EvaluateResultExceptionValue) result).getExceptionDetails().getText()); + } + } } diff --git a/java/src/org/openqa/selenium/remote/Script.java b/java/src/org/openqa/selenium/remote/Script.java index e8ea6d154d128..cd0498b5854ca 100644 --- a/java/src/org/openqa/selenium/remote/Script.java +++ b/java/src/org/openqa/selenium/remote/Script.java @@ -21,6 +21,7 @@ import org.openqa.selenium.Beta; import org.openqa.selenium.bidi.log.ConsoleLogEntry; import org.openqa.selenium.bidi.log.JavascriptLogEntry; +import org.openqa.selenium.bidi.script.RemoteValue; @Beta public interface Script { @@ -40,4 +41,6 @@ public interface Script { String pin(String script); void unpin(String id); + + RemoteValue execute(String script, Object... args); } diff --git a/java/test/org/openqa/selenium/WebScriptExecuteTest.java b/java/test/org/openqa/selenium/WebScriptExecuteTest.java new file mode 100644 index 0000000000000..35500086ce69c --- /dev/null +++ b/java/test/org/openqa/selenium/WebScriptExecuteTest.java @@ -0,0 +1,330 @@ +// 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 java.math.BigInteger; +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.openqa.selenium.bidi.script.RegExpValue; +import org.openqa.selenium.bidi.script.RemoteValue; +import org.openqa.selenium.print.PrintOptions; +import org.openqa.selenium.remote.RemoteWebDriver; +import org.openqa.selenium.testing.JupiterTestBase; + +class WebScriptExecuteTest extends JupiterTestBase { + + @Test + void canExecuteScriptWithUndefinedArgument() { + RemoteValue value = + ((RemoteWebDriver) driver) + .script() + .execute( + "(arg) => {{\n" + + " if(arg!==undefined)\n" + + " throw Error(\"Argument should be undefined, but was" + + " \"+arg);\n" + + " return arg;\n" + + " }}", + "undefined"); + + assertThat(value.getType()).isEqualTo("undefined"); + } + + @Test + void canExecuteScriptWithNullArgument() { + RemoteValue value = + ((RemoteWebDriver) driver) + .script() + .execute( + "(arg) => {{\n" + + " if(arg!==null)\n" + + " throw Error(\"Argument should be undefined, but was" + + " \"+arg);\n" + + " return arg;\n" + + " }}", + "null"); + + assertThat(value.getType()).isEqualTo("null"); + } + + @Test + void canExecuteScriptWithMinusZeroArgument() { + RemoteValue value = + ((RemoteWebDriver) driver) + .script() + .execute( + "(arg) => {{\n" + + " if(arg!==-0)\n" + + " throw Error(\"Argument should be -0, but was \"+arg);\n" + + " return arg;\n" + + " }}", + "-0"); + + assertThat(value.getType()).isEqualTo("number"); + assertThat(value.getValue().get()).isEqualTo("-0"); + } + + @Test + void canExecuteScriptWithInfinityArgument() { + RemoteValue value = + ((RemoteWebDriver) driver) + .script() + .execute( + "(arg) => {{\n" + + " if(arg!==Infinity)\n" + + " throw Error(\"Argument should be Infinity, but was" + + " \"+arg);\n" + + " return arg;\n" + + " }}", + "Infinity"); + + assertThat(value.getType()).isEqualTo("number"); + assertThat(value.getValue().get()).isEqualTo("Infinity"); + } + + @Test + void canExecuteScriptWithMinusInfinityArgument() { + RemoteValue value = + ((RemoteWebDriver) driver) + .script() + .execute( + "(arg) => {{\n" + + " if(arg!==-Infinity)\n" + + " throw Error(\"Argument should be -Infinity, but was" + + " \"+arg);\n" + + " return arg;\n" + + " }}", + "-Infinity"); + + assertThat(value.getType()).isEqualTo("number"); + assertThat(value.getValue().get()).isEqualTo("-Infinity"); + } + + @Test + void canExecuteScriptWithNumberArgument() { + RemoteValue value = + ((RemoteWebDriver) driver) + .script() + .execute( + "(arg) => {{\n" + + " if(arg!==1.4)\n" + + " throw Error(\"Argument should be 1.4, but was \"+arg);\n" + + " return arg;\n" + + " }}", + 1.4); + + assertThat(value.getType()).isEqualTo("number"); + assertThat(value.getValue().get()).isEqualTo(1.4); + } + + @Test + void canExecuteScriptWithIntegerArgument() { + RemoteValue value = + ((RemoteWebDriver) driver) + .script() + .execute( + "(arg) => {{\n" + + " if(arg!==1)\n" + + " throw Error(\"Argument should be 1, but was \"+arg);\n" + + " return arg;\n" + + " }}", + 1); + + assertThat(value.getType()).isEqualTo("number"); + assertThat(value.getValue().get()).isEqualTo(1L); + } + + @Test + void canExecuteScriptWithBooleanArgument() { + RemoteValue value = + ((RemoteWebDriver) driver) + .script() + .execute( + "(arg) => {{\n" + + " if(arg!==true)\n" + + " throw Error(\"Argument should be true, but was \"+arg);\n" + + " return arg;\n" + + " }}", + true); + + assertThat(value.getType()).isEqualTo("boolean"); + assertThat(value.getValue().get()).isEqualTo(true); + } + + @Test + void canExecuteScriptWithBigIntArgument() { + RemoteValue value = + ((RemoteWebDriver) driver) + .script() + .execute( + "(arg) => {{\n" + + " if(arg!==42n)\n" + + " throw Error(\"Argument should be 42n, but was \"+arg);\n" + + " return arg;\n" + + " }}", + BigInteger.valueOf(42L)); + + assertThat(value.getType()).isEqualTo("bigint"); + assertThat(value.getValue().get()).isEqualTo("42"); + } + + @Test + void canExecuteScriptWithArrayArgument() { + List list = new ArrayList<>(); + list.add(1); + list.add(2); + + RemoteValue value = + ((RemoteWebDriver) driver) + .script() + .execute( + "(arg) => {{\n" + + " if(! (arg instanceof Array))\n" + + " throw Error(\"Argument type should be Array, but was \"+\n" + + " Object.prototype.toString.call(arg));\n" + + " return arg;\n" + + " }}", + list); + + assertThat(value.getType()).isEqualTo("array"); + List values = (List) value.getValue().get(); + assertThat(values.size()).isEqualTo(2); + } + + @Test + void canExecuteScriptWithSetArgument() { + Set set = new HashSet<>(); + set.add(1); + set.add(2); + + RemoteValue value = + ((RemoteWebDriver) driver) + .script() + .execute( + "(arg) => {{\n" + + " if(! (arg instanceof Set))\n" + + " throw Error(\"Argument type should be Set, but was \"+\n" + + " Object.prototype.toString.call(arg));\n" + + " return arg;\n" + + " }}", + set); + + assertThat(value.getType()).isEqualTo("set"); + List values = (List) value.getValue().get(); + assertThat(values.size()).isEqualTo(2); + } + + @Test + void canExecuteScriptWithDateArgument() { + RemoteValue value = + ((RemoteWebDriver) driver) + .script() + .execute( + "(arg) => {{\n" + + " if(! (arg instanceof Date))\n" + + " throw Error(\"Argument type should be Date, but was \"+\n" + + " Object.prototype.toString.call(arg));\n" + + " return arg;\n" + + " }}", + Instant.now()); + + assertThat(value.getType()).isEqualTo("date"); + } + + @Test + void canExecuteScriptWithMapArgument() { + Map mapValue = new HashMap<>(); + mapValue.put("foobar", 1); + mapValue.put(List.of(1, 2), List.of(4, 5, 6)); + + RemoteValue value = + ((RemoteWebDriver) driver) + .script() + .execute( + "(arg) => {{\n" + + " if(! (arg instanceof Map))\n" + + " throw Error(\"Argument type should be Map, but was \"+\n" + + " Object.prototype.toString.call(arg));\n" + + " return arg;\n" + + " }}", + mapValue); + + assertThat(value.getType()).isEqualTo("map"); + + Map values = (Map) value.getValue().get(); + assertThat(values.size()).isEqualTo(2); + } + + @Test + void canExecuteScriptWithObjectArgument() { + + PrintOptions options = new PrintOptions(); + + RemoteValue value = + ((RemoteWebDriver) driver) + .script() + .execute( + "(arg) => {{\n" + + " if(! (arg instanceof Object))\n" + + " throw Error(\"Argument type should be Object, but was \"+\n" + + " Object.prototype.toString.call(arg));\n" + + " return arg;\n" + + " }}", + options); + + assertThat(value.getType()).isEqualTo("object"); + + Map values = (Map) value.getValue().get(); + assertThat(values.size()).isEqualTo(6); + } + + @Test + void canExecuteScriptWithRegExpArgument() { + RemoteValue value = + ((RemoteWebDriver) driver) + .script() + .execute( + "(arg) => {{\n" + + " if(! (arg instanceof RegExp))\n" + + " throw Error(\"Argument type should be RegExp, but was \"+\n" + + " Object.prototype.toString.call(arg));\n" + + " return arg;\n" + + " }}", + new RegExpValue("foo", "g")); + + assertThat(value.getType()).isEqualTo("regexp"); + + RegExpValue resultValue = (RegExpValue) value.getValue().get(); + assertThat(resultValue.getPattern()).isEqualTo("foo"); + assertThat(resultValue.getFlags()).isEqualTo("g"); + } + + @AfterEach + public void cleanUp() { + driver.quit(); + } +}