Path: blob/trunk/java/src/org/openqa/selenium/JavascriptExecutor.java
1865 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.NullMarked;22import org.jspecify.annotations.Nullable;23import org.openqa.selenium.internal.Require;2425/**26* Indicates that a driver can execute JavaScript, providing access to the mechanism to do so.27*28* <p>Because of cross domain policies browsers enforce your script execution may fail unexpectedly29* and without adequate error messaging. This is particularly pertinent when creating your own XHR30* request or when trying to access another frame. Most times when troubleshooting failure it's best31* to view the browser's console after executing the WebDriver request.32*/33@NullMarked34public interface JavascriptExecutor {35/**36* Executes JavaScript in the context of the currently selected frame or window. The script37* fragment provided will be executed as the body of an anonymous function.38*39* <p>Within the script, use <code>document</code> to refer to the current document. Note that40* local variables will not be available once the script has finished executing, though global41* variables will persist.42*43* <p>If the script has a return value (i.e. if the script contains a <code>return</code>44* statement), then the following steps will be taken:45*46* <ul>47* <li>For an HTML element, this method returns a WebElement48* <li>For a decimal, a Double is returned49* <li>For a non-decimal number, a Long is returned50* <li>For a boolean, a Boolean is returned51* <li>For all other cases, a String is returned.52* <li>For an array, return a List<Object> with each object following the rules above. We53* support nested lists.54* <li>For a map, return a Map<String, Object> with values following the rules above.55* <li>Unless the value is null or there is no return value, in which null is returned56* </ul>57*58* <p>Arguments must be a number, a boolean, a String, WebElement, or a List of any combination of59* the above. An exception will be thrown if the arguments do not meet these criteria. The60* arguments will be made available to the JavaScript via the "arguments" magic variable, as if61* the function were called via "Function.apply"62*63* @param script The JavaScript to execute64* @param args The arguments to the script. May be empty65* @return One of Boolean, Long, Double, String, List, Map or WebElement. Or null.66*/67@Nullable Object executeScript(String script, @Nullable Object... args);6869/**70* Execute an asynchronous piece of JavaScript in the context of the currently selected frame or71* window. Unlike executing {@link #executeScript(String, Object...) synchronous JavaScript},72* scripts executed with this method must explicitly signal they are finished by invoking the73* provided callback. This callback is always injected into the executed function as the last74* argument.75*76* <p>The first argument passed to the callback function will be used as the script's result. This77* value will be handled as follows:78*79* <ul>80* <li>For an HTML element, this method returns a WebElement81* <li>For a number, a Long is returned82* <li>For a boolean, a Boolean is returned83* <li>For all other cases, a String is returned.84* <li>For an array, return a List<Object> with each object following the rules above. We85* support nested lists.86* <li>For a map, return a Map<String, Object> with values following the rules above.87* <li>Unless the value is null or there is no return value, in which null is returned88* </ul>89*90* <p>The default timeout for a script to be executed is 0ms. In most cases, including the91* examples below, one must set the script timeout {@link92* WebDriver.Timeouts#scriptTimeout(java.time.Duration)} beforehand to a value sufficiently large93* enough.94*95* <p>Example #1: Performing a sleep in the browser under test.96*97* <pre>{@code98* long start = System.currentTimeMillis();99* ((JavascriptExecutor) driver).executeAsyncScript(100* "window.setTimeout(arguments[arguments.length - 1], 500);");101* System.out.println(102* "Elapsed time: " + (System.currentTimeMillis() - start));103* }</pre>104*105* <p>Example #2: Synchronizing a test with an AJAX application:106*107* <pre>{@code108* WebElement composeButton = driver.findElement(By.id("compose-button"));109* composeButton.click();110* ((JavascriptExecutor) driver).executeAsyncScript(111* "var callback = arguments[arguments.length - 1];" +112* "mailClient.getComposeWindowWidget().onload(callback);");113* driver.switchTo().frame("composeWidget");114* driver.findElement(By.id("to")).sendKeys("[email protected]");115* }</pre>116*117* <p>Example #3: Injecting a XMLHttpRequest and waiting for the result:118*119* <pre>{@code120* Object response = ((JavascriptExecutor) driver).executeAsyncScript(121* "var callback = arguments[arguments.length - 1];" +122* "var xhr = new XMLHttpRequest();" +123* "xhr.open('GET', '/resource/data.json', true);" +124* "xhr.onreadystatechange = function() {" +125* " if (xhr.readyState == 4) {" +126* " callback(xhr.responseText);" +127* " }" +128* "};" +129* "xhr.send();");130* JsonObject json = new JsonParser().parse((String) response);131* assertEquals("cheese", json.get("food").getAsString());132* }</pre>133*134* <p>Script arguments must be a number, a boolean, a String, WebElement, or a List of any135* combination of the above. An exception will be thrown if the arguments do not meet these136* criteria. The arguments will be made available to the JavaScript via the "arguments" variable.137*138* @param script The JavaScript to execute.139* @param args The arguments to the script. May be empty.140* @return One of Boolean, Long, String, List, Map, WebElement, or null.141* @see WebDriver.Timeouts#scriptTimeout(java.time.Duration)142*/143@Nullable Object executeAsyncScript(String script, @Nullable Object... args);144145/**146* Commonly used scripts may be "pinned" to the WebDriver session, allowing them to be called147* efficiently by their handle rather than sending the entire script across the wire for every148* call.149*150* <p>The default implementation of this adheres to the API's expectations but is inefficient.151*152* @see #executeScript(ScriptKey, Object...)153* @param script The Javascript to execute.154* @return A handle which may later be used in {@link #executeScript(ScriptKey, Object...)}155* @throws JavascriptException If the script cannot be pinned for some reason.156*/157default ScriptKey pin(String script) {158Require.nonNull("Script to pin", script);159return UnpinnedScriptKey.pin(this, script);160}161162/**163* Deletes the reference to a script that has previously been pinned. Subsequent calls to {@link164* #executeScript(ScriptKey, Object...)} will fail for the given {@code key}.165*/166default void unpin(ScriptKey key) {167Require.nonNull("Key to unpin", key);168Require.stateCondition(169key instanceof UnpinnedScriptKey, "Script key should have been generated by this driver");170171UnpinnedScriptKey.unpin(this, (UnpinnedScriptKey) key);172}173174/**175* @return The {@link ScriptKey}s of all currently pinned scripts.176*/177default Set<ScriptKey> getPinnedScripts() {178return UnpinnedScriptKey.getPinnedScripts(this).stream()179.map(key -> (ScriptKey) key)180.collect(Collectors.toUnmodifiableSet());181}182183/**184* Calls a script by the {@link ScriptKey} returned by {@link #pin(String)}. This can be thought185* of as inlining the pinned script and simply calling {@link #executeScript(String, Object...)}.186*187* @see #executeScript(String, Object...)188*/189default @Nullable Object executeScript(ScriptKey key, @Nullable Object... args) {190Require.stateCondition(191key instanceof UnpinnedScriptKey, "Script key should have been generated by this driver");192193if (!getPinnedScripts().contains(key)) {194throw new JavascriptException("Script is unpinned");195}196197return executeScript(((UnpinnedScriptKey) key).getScript(), args);198}199}200201202