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