Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
SeleniumHQ
GitHub Repository: SeleniumHQ/Selenium
Path: blob/trunk/java/src/org/openqa/selenium/JavascriptExecutor.java
4006 views
1
// Licensed to the Software Freedom Conservancy (SFC) under one
2
// or more contributor license agreements. See the NOTICE file
3
// distributed with this work for additional information
4
// regarding copyright ownership. The SFC licenses this file
5
// to you under the Apache License, Version 2.0 (the
6
// "License"); you may not use this file except in compliance
7
// with the License. You may obtain a copy of the License at
8
//
9
// http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing,
12
// software distributed under the License is distributed on an
13
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
// KIND, either express or implied. See the License for the
15
// specific language governing permissions and limitations
16
// under the License.
17
18
package org.openqa.selenium;
19
20
import java.util.Set;
21
import java.util.stream.Collectors;
22
import org.jspecify.annotations.Nullable;
23
import org.openqa.selenium.internal.Require;
24
25
/**
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 unexpectedly
29
* and without adequate error messaging. This is particularly pertinent when creating your own XHR
30
* request or when trying to access another frame. Most times when troubleshooting failure it's best
31
* to view the browser's console after executing the WebDriver request.
32
*/
33
public interface JavascriptExecutor {
34
/**
35
* Executes JavaScript in the context of the currently selected frame or window. The script
36
* fragment provided will be executed as the body of an anonymous function.
37
*
38
* <p>Within the script, use <code>document</code> to refer to the current document. Note that
39
* local variables will not be available once the script has finished executing, though global
40
* variables will persist.
41
*
42
* <p>If the script has a return value (i.e. if the script contains a <code>return</code>
43
* statement), then the following steps will be taken:
44
*
45
* <ul>
46
* <li>For an HTML element, this method returns a WebElement
47
* <li>For a decimal, a Double is returned
48
* <li>For a non-decimal number, a Long is returned
49
* <li>For a boolean, a Boolean is returned
50
* <li>For all other cases, a String is returned.
51
* <li>For an array, return a List&lt;Object&gt; with each object following the rules above. We
52
* support nested lists.
53
* <li>For a map, return a Map&lt;String, Object&gt; with values following the rules above.
54
* <li>Unless the value is null or there is no return value, in which null is returned
55
* </ul>
56
*
57
* <p>Arguments must be a number, a boolean, a String, WebElement, or a List of any combination of
58
* the above. An exception will be thrown if the arguments do not meet these criteria. The
59
* arguments will be made available to the JavaScript via the "arguments" magic variable, as if
60
* the function were called via "Function.apply"
61
*
62
* @param script The JavaScript to execute
63
* @param args The arguments to the script. May be empty
64
* @return One of Boolean, Long, Double, String, List, Map or WebElement. Or null.
65
*/
66
@Nullable Object executeScript(String script, @Nullable Object... args);
67
68
/**
69
* Execute an asynchronous piece of JavaScript in the context of the currently selected frame or
70
* window. Unlike executing {@link #executeScript(String, Object...) synchronous JavaScript},
71
* scripts executed with this method must explicitly signal they are finished by invoking the
72
* provided callback. This callback is always injected into the executed function as the last
73
* argument.
74
*
75
* <p>The first argument passed to the callback function will be used as the script's result. This
76
* value will be handled as follows:
77
*
78
* <ul>
79
* <li>For an HTML element, this method returns a WebElement
80
* <li>For a number, a Long is returned
81
* <li>For a boolean, a Boolean is returned
82
* <li>For all other cases, a String is returned.
83
* <li>For an array, return a List&lt;Object&gt; with each object following the rules above. We
84
* support nested lists.
85
* <li>For a map, return a Map&lt;String, Object&gt; with values following the rules above.
86
* <li>Unless the value is null or there is no return value, in which null is returned
87
* </ul>
88
*
89
* <p>The default timeout for a script to be executed is 0ms. In most cases, including the
90
* examples below, one must set the script timeout {@link
91
* WebDriver.Timeouts#scriptTimeout(java.time.Duration)} beforehand to a value sufficiently large
92
* enough.
93
*
94
* <p>Example #1: Performing a sleep in the browser under test.
95
*
96
* <pre>{@code
97
* long start = System.currentTimeMillis();
98
* ((JavascriptExecutor) driver).executeAsyncScript(
99
* "window.setTimeout(arguments[arguments.length - 1], 500);");
100
* System.out.println(
101
* "Elapsed time: " + (System.currentTimeMillis() - start));
102
* }</pre>
103
*
104
* <p>Example #2: Synchronizing a test with an AJAX application:
105
*
106
* <pre>{@code
107
* WebElement composeButton = driver.findElement(By.id("compose-button"));
108
* composeButton.click();
109
* ((JavascriptExecutor) driver).executeAsyncScript(
110
* "var callback = arguments[arguments.length - 1];" +
111
* "mailClient.getComposeWindowWidget().onload(callback);");
112
* driver.switchTo().frame("composeWidget");
113
* driver.findElement(By.id("to")).sendKeys("[email protected]");
114
* }</pre>
115
*
116
* <p>Example #3: Injecting a XMLHttpRequest and waiting for the result:
117
*
118
* <pre>{@code
119
* Object response = ((JavascriptExecutor) driver).executeAsyncScript(
120
* "var callback = arguments[arguments.length - 1];" +
121
* "var xhr = new XMLHttpRequest();" +
122
* "xhr.open('GET', '/resource/data.json', true);" +
123
* "xhr.onreadystatechange = function() {" +
124
* " if (xhr.readyState == 4) {" +
125
* " callback(xhr.responseText);" +
126
* " }" +
127
* "};" +
128
* "xhr.send();");
129
* JsonObject json = new JsonParser().parse((String) response);
130
* assertEquals("cheese", json.get("food").getAsString());
131
* }</pre>
132
*
133
* <p>Script arguments must be a number, a boolean, a String, WebElement, or a List of any
134
* combination of the above. An exception will be thrown if the arguments do not meet these
135
* criteria. The arguments will be made available to the JavaScript via the "arguments" variable.
136
*
137
* @param script The JavaScript to execute.
138
* @param args The arguments to the script. May be empty.
139
* @return One of Boolean, Long, String, List, Map, WebElement, or null.
140
* @see WebDriver.Timeouts#scriptTimeout(java.time.Duration)
141
*/
142
@Nullable Object executeAsyncScript(String script, @Nullable Object... args);
143
144
/**
145
* Commonly used scripts may be "pinned" to the WebDriver session, allowing them to be called
146
* efficiently by their handle rather than sending the entire script across the wire for every
147
* call.
148
*
149
* <p>The default implementation of this adheres to the API's expectations but is inefficient.
150
*
151
* @see #executeScript(ScriptKey, Object...)
152
* @param script The Javascript to execute.
153
* @return A handle which may later be used in {@link #executeScript(ScriptKey, Object...)}
154
* @throws JavascriptException If the script cannot be pinned for some reason.
155
*/
156
default ScriptKey pin(String script) {
157
Require.nonNull("Script to pin", script);
158
return UnpinnedScriptKey.pin(this, script);
159
}
160
161
/**
162
* Deletes the reference to a script that has previously been pinned. Subsequent calls to {@link
163
* #executeScript(ScriptKey, Object...)} will fail for the given {@code key}.
164
*/
165
default void unpin(ScriptKey key) {
166
Require.nonNull("Key to unpin", key);
167
Require.stateCondition(
168
key instanceof UnpinnedScriptKey, "Script key should have been generated by this driver");
169
170
UnpinnedScriptKey.unpin(this, (UnpinnedScriptKey) key);
171
}
172
173
/**
174
* @return The {@link ScriptKey}s of all currently pinned scripts.
175
*/
176
default Set<ScriptKey> getPinnedScripts() {
177
return UnpinnedScriptKey.getPinnedScripts(this).stream()
178
.map(key -> (ScriptKey) key)
179
.collect(Collectors.toUnmodifiableSet());
180
}
181
182
/**
183
* Calls a script by the {@link ScriptKey} returned by {@link #pin(String)}. This can be thought
184
* of as inlining the pinned script and simply calling {@link #executeScript(String, Object...)}.
185
*
186
* @see #executeScript(String, Object...)
187
*/
188
default @Nullable Object executeScript(ScriptKey key, @Nullable Object... args) {
189
Require.stateCondition(
190
key instanceof UnpinnedScriptKey, "Script key should have been generated by this driver");
191
192
if (!getPinnedScripts().contains(key)) {
193
throw new JavascriptException("Script is unpinned");
194
}
195
196
return executeScript(((UnpinnedScriptKey) key).getScript(), args);
197
}
198
}
199
200