Path: blob/trunk/java/src/org/openqa/selenium/JavascriptExecutor.java
4006 views
// Licensed to the Software Freedom Conservancy (SFC) under one1// or more contributor license agreements. See the NOTICE file2// distributed with this work for additional information3// regarding copyright ownership. The SFC licenses this file4// to you under the Apache License, Version 2.0 (the5// "License"); you may not use this file except in compliance6// with the License. You may obtain a copy of the License at7//8// http://www.apache.org/licenses/LICENSE-2.09//10// Unless required by applicable law or agreed to in writing,11// software distributed under the License is distributed on an12// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY13// KIND, either express or implied. See the License for the14// specific language governing permissions and limitations15// under the License.1617package org.openqa.selenium;1819import java.util.Set;20import java.util.stream.Collectors;21import org.jspecify.annotations.Nullable;22import org.openqa.selenium.internal.Require;2324/**25* Indicates that a driver can execute JavaScript, providing access to the mechanism to do so.26*27* <p>Because of cross domain policies browsers enforce your script execution may fail unexpectedly28* and without adequate error messaging. This is particularly pertinent when creating your own XHR29* request or when trying to access another frame. Most times when troubleshooting failure it's best30* to view the browser's console after executing the WebDriver request.31*/32public interface JavascriptExecutor {33/**34* Executes JavaScript in the context of the currently selected frame or window. The script35* fragment provided will be executed as the body of an anonymous function.36*37* <p>Within the script, use <code>document</code> to refer to the current document. Note that38* local variables will not be available once the script has finished executing, though global39* variables will persist.40*41* <p>If the script has a return value (i.e. if the script contains a <code>return</code>42* statement), then the following steps will be taken:43*44* <ul>45* <li>For an HTML element, this method returns a WebElement46* <li>For a decimal, a Double is returned47* <li>For a non-decimal number, a Long is returned48* <li>For a boolean, a Boolean is returned49* <li>For all other cases, a String is returned.50* <li>For an array, return a List<Object> with each object following the rules above. We51* support nested lists.52* <li>For a map, return a Map<String, Object> with values following the rules above.53* <li>Unless the value is null or there is no return value, in which null is returned54* </ul>55*56* <p>Arguments must be a number, a boolean, a String, WebElement, or a List of any combination of57* the above. An exception will be thrown if the arguments do not meet these criteria. The58* arguments will be made available to the JavaScript via the "arguments" magic variable, as if59* the function were called via "Function.apply"60*61* @param script The JavaScript to execute62* @param args The arguments to the script. May be empty63* @return One of Boolean, Long, Double, String, List, Map or WebElement. Or null.64*/65@Nullable Object executeScript(String script, @Nullable Object... args);6667/**68* Execute an asynchronous piece of JavaScript in the context of the currently selected frame or69* window. Unlike executing {@link #executeScript(String, Object...) synchronous JavaScript},70* scripts executed with this method must explicitly signal they are finished by invoking the71* provided callback. This callback is always injected into the executed function as the last72* argument.73*74* <p>The first argument passed to the callback function will be used as the script's result. This75* value will be handled as follows:76*77* <ul>78* <li>For an HTML element, this method returns a WebElement79* <li>For a number, a Long is returned80* <li>For a boolean, a Boolean is returned81* <li>For all other cases, a String is returned.82* <li>For an array, return a List<Object> with each object following the rules above. We83* support nested lists.84* <li>For a map, return a Map<String, Object> with values following the rules above.85* <li>Unless the value is null or there is no return value, in which null is returned86* </ul>87*88* <p>The default timeout for a script to be executed is 0ms. In most cases, including the89* examples below, one must set the script timeout {@link90* WebDriver.Timeouts#scriptTimeout(java.time.Duration)} beforehand to a value sufficiently large91* enough.92*93* <p>Example #1: Performing a sleep in the browser under test.94*95* <pre>{@code96* long start = System.currentTimeMillis();97* ((JavascriptExecutor) driver).executeAsyncScript(98* "window.setTimeout(arguments[arguments.length - 1], 500);");99* System.out.println(100* "Elapsed time: " + (System.currentTimeMillis() - start));101* }</pre>102*103* <p>Example #2: Synchronizing a test with an AJAX application:104*105* <pre>{@code106* WebElement composeButton = driver.findElement(By.id("compose-button"));107* composeButton.click();108* ((JavascriptExecutor) driver).executeAsyncScript(109* "var callback = arguments[arguments.length - 1];" +110* "mailClient.getComposeWindowWidget().onload(callback);");111* driver.switchTo().frame("composeWidget");112* driver.findElement(By.id("to")).sendKeys("[email protected]");113* }</pre>114*115* <p>Example #3: Injecting a XMLHttpRequest and waiting for the result:116*117* <pre>{@code118* Object response = ((JavascriptExecutor) driver).executeAsyncScript(119* "var callback = arguments[arguments.length - 1];" +120* "var xhr = new XMLHttpRequest();" +121* "xhr.open('GET', '/resource/data.json', true);" +122* "xhr.onreadystatechange = function() {" +123* " if (xhr.readyState == 4) {" +124* " callback(xhr.responseText);" +125* " }" +126* "};" +127* "xhr.send();");128* JsonObject json = new JsonParser().parse((String) response);129* assertEquals("cheese", json.get("food").getAsString());130* }</pre>131*132* <p>Script arguments must be a number, a boolean, a String, WebElement, or a List of any133* combination of the above. An exception will be thrown if the arguments do not meet these134* criteria. The arguments will be made available to the JavaScript via the "arguments" variable.135*136* @param script The JavaScript to execute.137* @param args The arguments to the script. May be empty.138* @return One of Boolean, Long, String, List, Map, WebElement, or null.139* @see WebDriver.Timeouts#scriptTimeout(java.time.Duration)140*/141@Nullable Object executeAsyncScript(String script, @Nullable Object... args);142143/**144* Commonly used scripts may be "pinned" to the WebDriver session, allowing them to be called145* efficiently by their handle rather than sending the entire script across the wire for every146* call.147*148* <p>The default implementation of this adheres to the API's expectations but is inefficient.149*150* @see #executeScript(ScriptKey, Object...)151* @param script The Javascript to execute.152* @return A handle which may later be used in {@link #executeScript(ScriptKey, Object...)}153* @throws JavascriptException If the script cannot be pinned for some reason.154*/155default ScriptKey pin(String script) {156Require.nonNull("Script to pin", script);157return UnpinnedScriptKey.pin(this, script);158}159160/**161* Deletes the reference to a script that has previously been pinned. Subsequent calls to {@link162* #executeScript(ScriptKey, Object...)} will fail for the given {@code key}.163*/164default void unpin(ScriptKey key) {165Require.nonNull("Key to unpin", key);166Require.stateCondition(167key instanceof UnpinnedScriptKey, "Script key should have been generated by this driver");168169UnpinnedScriptKey.unpin(this, (UnpinnedScriptKey) key);170}171172/**173* @return The {@link ScriptKey}s of all currently pinned scripts.174*/175default Set<ScriptKey> getPinnedScripts() {176return UnpinnedScriptKey.getPinnedScripts(this).stream()177.map(key -> (ScriptKey) key)178.collect(Collectors.toUnmodifiableSet());179}180181/**182* Calls a script by the {@link ScriptKey} returned by {@link #pin(String)}. This can be thought183* of as inlining the pinned script and simply calling {@link #executeScript(String, Object...)}.184*185* @see #executeScript(String, Object...)186*/187default @Nullable Object executeScript(ScriptKey key, @Nullable Object... args) {188Require.stateCondition(189key instanceof UnpinnedScriptKey, "Script key should have been generated by this driver");190191if (!getPinnedScripts().contains(key)) {192throw new JavascriptException("Script is unpinned");193}194195return executeScript(((UnpinnedScriptKey) key).getScript(), args);196}197}198199200