File: run_javascript.md

package info (click to toggle)
chromium 139.0.7258.127-1
  • links: PTS, VCS
  • area: main
  • in suites:
  • size: 6,122,068 kB
  • sloc: cpp: 35,100,771; ansic: 7,163,530; javascript: 4,103,002; python: 1,436,920; asm: 946,517; xml: 746,709; pascal: 187,653; perl: 88,691; sh: 88,436; objc: 79,953; sql: 51,488; cs: 44,583; fortran: 24,137; makefile: 22,147; tcl: 15,277; php: 13,980; yacc: 8,984; ruby: 7,485; awk: 3,720; lisp: 3,096; lex: 1,327; ada: 727; jsp: 228; sed: 36
file content (244 lines) | stat: -rw-r--r-- 11,194 bytes parent folder | download | duplicates (11)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# Run JavaScript Code

ChromeDriver often needs to send JavaScript code to Chrome for execution.
This document will explain the following:
* Sources of JavaScript code, user-supplied vs. embedded.
* The DevTools command for sending JavaScript code to Chrome.
* Preprocessing done by ChromeDriver to work around limitations in DevTools.

## Sources of JavaScript Code

JavaScript code run by ChromeDriver can be divided into two broad categories,
depending on where the code comes from.
* The client application can send JavaScript code for executing,
  as described in the
  [Executing Script](https://www.w3.org/TR/webdriver/#executing-script)
  section of the WebDriver spec.
* ChromeDriver itself has a large number of embedded JavaScript fragments,
  which are used to implement certain features.

### User Supplied JavaScript Code

The WebDriver standard provides two commands for running JavaScript code,
[Execute Script](https://www.w3.org/TR/webdriver/#execute-script) and
[Execute Async Script](https://www.w3.org/TR/webdriver/#execute-async-script).
Both commands allow the user to provide a fragment of JavaScript code (as a
string), with zero or more argument. These commands
have a number of features that make them complicated to implement:

* Both the arguments and return value can be a variety of types, including
  objects, collections, and DOM elements. These values need to be
  converted to appropriate JSON representation in order to be transported
  over the network.

* If the user supplied code returns a Promise, ChromeDriver needs to wait
  for that Promise to be resolved or rejected before returning.

* In the case of Execute Async Script command, the standard provides a
  mechanism for the script to notify ChromeDriver when it has finished
  execution.

### Embedded JavaScript Code

ChromeDriver has a large number of JavaScript fragments that it can send to
Chrome for a variety of reasons. Examples include:
* Finding out the page load status with `"document.readyState"`.
* Ensure that pending navigation is started by evaluating
  a simple expression of `"1"`.
* Using one of the JavaScript fragments (called "atoms") supplied by
  Selenium, to find elements on a page,
  to get the text displayed by a DOM element, etc.
* Call one of the JavaScript function in the
  [js](https://source.chromium.org/chromium/chromium/src/+/main:chrome/test/chromedriver/js/)
  directory, e.g., to get the location of an element on the page.

## DevTools JavaScript API

The Chrome DevTools provide [Runtime.evaluate
command](https://source.chromium.org/chromium/chromium/src/+/main:v8/include/js_protocol.pdl?q=command%5C%20evaluate$)
for running JavaScript code.
ChromeDriver uses this command to send all JavaScript code to Chrome.

The Runtime.evaluate command has a large number of parameters.
Here are the ones important to ChromeDriver.

* `string expression`: This is the actual JavaScript code to execute.
  This is the only required parameter.

* `ExecutionContextId contextId`: A web page can have multiple JavaScript
  contexts. For example, there is a separate context for each frame.
  This parameter allows ChromeDriver to select the context to run the code.
  If omitted, the code runs in the context associated with the top-level
  document.

* `boolean awaitPromise`:
  If set to `true` and the JavaScript code returns a Promise object,
  DevTools should wait for the Promise to be resolved or rejected before
  returning. ChromeDriver usually sets this to `true`.

* `boolean returnByValue`: Controls how objects are returned from the script.
  See below for more details. ChromeDriver usually sets this to `true`.

### Return Value

The value generated by the last statement of the script becomes the result
of the script, and is returned by the Runtime.evaluate command.
If the last statement does not generate any result,
then the result of the script is `undefined`.
The script must *not* terminate by executing a `return` statement.

Like most DevTools commands,
Runtime.evaluate returns a JSON object to the caller.
This JSON object always has a property named `result`,
whose value is another JSON object of
[type RemoteObject](https://source.chromium.org/chromium/chromium/src/+/main:v8/include/js_protocol.pdl?q=%22type%20RemoteObject%20%22).

The type of the result (e.g., string, number, object, etc)
is given in the `type` property of the RemoteObject.

If the script generates a primitive result,
the result value is stored in the `value` property of the RemoteObject.
For example, if the script evaluates to "Hello", Runtime.evaluate returns:
```
{
   "result": {
      "type": "string",
      "value": "Hello"
   }
}
```

If the script generates a non-primitive result,
what happens depends on the `returnByValue` parameter passed to
the Runtime.evaluate command.
If `returnByValue` is `true`,
the result is serialized into JSON format and stored in the `value` property.
For example,
```
{
   "result": {
      "type": "object",
      "value": {
         "x": [ 1, 2, 3 ]
      }
   }
}
```

If `returnByValue` is `true` but the result object is not compatible with
JSON format, then information can be lost or an error can be returned.
ChromeDriver should be written to avoid this situation.

If `returnByValue` is `false` or unspecified,
then the returned RemoteObject does not have a `value` property,
but has an `objectId` property instead.
This object ID can be used in other DevTools commands to query for
information for the result object.
ChromeDriver uses this feature only in a very small number of cases,
such as resolving the iframe associated with an element.

If the script ends by throwing an uncaught exception,
Runtime.evaluate returns a JSON object with two properties.
The `exceptionDetails` property contains details about the exception,
while the `result` property contains information about the value passed
to the `throw` statement.
If `awaitPromise` is true and the script returns a rejected Promise,
it is treated the same as an uncaught exception.

### Limitations

The Runtime.evaluate command has a number of limitations.
These limitations are listed here, and the rest of the document will
explain how ChromeDriver works around these limitations.

* It does not allow passing any arguments to the JavaScript code.

* The return value must be compatible with JSON format.

* It does not allow `await` statements,
  unless they are inside async functions.

### Modal Dialog

Usually JavaScript execution is single-threaded, so that if any code is blocked
by a modal dialog, no addition code can be executed until the dialog is
dismissed. However, there is a bit of magic involved with DevTools while
a modal dialog is shown -- it runs a nested message loop to dispatch
DevTools commands, including running additional JavaScript code.

However, promises are only resolved at the top message loop (even trivial ones,
such as `await true`). Thus, setting `awaitPromise` parameter to true would
block Runtime.evaluate command from returning if a modal dialog is shown.

## How ChromeDriver Runs JavaScript Code

ChromeDriver uses several different ways to run JavaScript code,
depending on what features are required. These are explained in this section.

### Direct Call to Runtime.evaluate

ChromeDriver code can directly use the Runtime.evaluate command provided by
the DevTools API.
This can be done by calling `WebView::SendCommand` method or its variations,
or by calling `DevToolsClient::SendCommand` method or its variations.

The advantage of this method is it provides access to all the parameters
supported by the Runtime.evaluate command.
It it is also the most efficient method.
The disadvantage is the caller is faced with all the limitations mentioned
in the previous section.
The caller also has to figure out how to route the script to the right frame.

### Use `WebView::EvaluateScript` Method

The [`WebView::EvaluateScript`](https://source.chromium.org/chromium/chromium/src/+/main:chrome/test/chromedriver/chrome/web_view_impl.cc?q=WebViewImpl::EvaluateScript%5c%28)
method is a thin wrapper around DevTools Runtime.evaluate command.
The only additional service it provides is routing the script to the
desired frame. The caller can provide a frame ID,
and `WebView::EvaluateScript` will make sure that the script is evaluated
in that frame.

Its main drawback is it does not provide access to any of the additional
parameters supported by DevTools API.

### Use `WebView::CallFunction` Method

The [`WebView::CallFunction`](https://source.chromium.org/chromium/chromium/src/+/main:chrome/test/chromedriver/chrome/web_view_impl.cc?q=WebViewImpl::CallFunction%5c%28) method
(and its variation [`WebView::CallFunctionWithTimeout`](https://source.chromium.org/chromium/chromium/src/+/main:chrome/test/chromedriver/chrome/web_view_impl.cc?q=WebViewImpl::CallFunctionWithTimeout%5c%28) method)
wraps the supplied JavaScript code inside
[callFunction](https://source.chromium.org/chromium/chromium/src/+/main:chrome/test/chromedriver/js/call_function.js?q=callFunction).
It requires that the supplied JavaScript code must be a function.

This method provides the following functionality:
* It allows arguments to be passed to the JavaScript function.
* It converts arguments from JSON objects to JavaScript objects.
* Unless `opt_unwrappedReturn` is `true`,
  it converts return value to something compatible with JSON.
* If the return value is not a Promise already, it is wrapped inside
  a Promise. This way, the return value is always a Promise,
  and can be waited for by the Runtime.evaluate command.

### Use `WebView::CallUserSyncScript` Method

The [`WebView::CallUserSyncScript`](https://source.chromium.org/chromium/chromium/src/+/main:chrome/test/chromedriver/chrome/web_view_impl.cc?q=WebViewImpl::CallUserSyncScript%5c%28)
method is used by the Execute Script command in the WebDriver API.
It is responsible for wrapping the user-supplied script inside a
[function](https://source.chromium.org/chromium/chromium/src/+/main:chrome/test/chromedriver/js/execute_script.js),
before passing it to `WebView::CallFunctionWithTimeout`.
This is necessary because the WebDriver standard requires that
the user-supplied script is not a function,
while `WebView::CallFunctionWithTimeout` requires its input to be a function.

Wrapping the script in a function has additional benefits:
* It allows passing arguments to the script, as required by the standard.
* It allows using `await` statement in the script.

### Use `WebView::CallUserAsyncFunction` Method

The [`WebView::CallUserAsyncFunction`](https://source.chromium.org/chromium/chromium/src/+/main:chrome/test/chromedriver/chrome/web_view_impl.cc?q=WebViewImpl::CallUserAsyncFunction%5c%28)
method is used by the Execute Async Script command in the WebDriver API.
It wraps the user-supplied script inside
[`executeAsyncScript`](https://source.chromium.org/chromium/chromium/src/+/main:chrome/test/chromedriver/js/execute_async_script.js) function,
before passing it to `WebView::CallFunctionWithTimeout`.
The [`executeAsyncScript`] function is responsible for waiting for the
async script to finish, as required by the WebDriver standard.