selenium-webdriver 4.20.0 → 4.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGES.md +14 -0
- package/README.md +2 -2
- package/bidi/browsingContext.js +2 -17
- package/bidi/browsingContextInspector.js +4 -2
- package/bidi/browsingContextTypes.js +17 -1
- package/bidi/logInspector.js +116 -30
- package/bin/linux/selenium-manager +0 -0
- package/bin/macos/selenium-manager +0 -0
- package/bin/windows/selenium-manager.exe +0 -0
- package/chrome.js +2 -0
- package/chromium.js +2 -0
- package/edge.js +2 -0
- package/firefox.js +5 -0
- package/ie.js +2 -0
- package/lib/script.js +63 -0
- package/lib/select.js +97 -55
- package/lib/webdriver.js +12 -0
- package/package.json +6 -3
- package/safari.js +2 -0
package/CHANGES.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## 4.22.0
|
|
2
|
+
|
|
3
|
+
- [bidi] Add types for user prompt related events (#14097)
|
|
4
|
+
- Add preference to enable CDP in Firefox by default (#14091)
|
|
5
|
+
- [bidi] Add callback handlers for logging APIs (#14120)
|
|
6
|
+
- [bidi] Add high-level logging API (#14135)
|
|
7
|
+
- Add CDP for Chrome 126 and remove 123
|
|
8
|
+
|
|
9
|
+
## 4.21.0
|
|
10
|
+
|
|
11
|
+
- Add CDP for Chrome 125 and remove 122
|
|
12
|
+
- Ensure 'selectVisibleByText' method is same as other languages (#13899)
|
|
13
|
+
- Ensure parity in the locators used by methods (#13902)
|
|
14
|
+
|
|
1
15
|
## 4.20.0
|
|
2
16
|
|
|
3
17
|
- Add CDP for Chrome 124 and remove 121
|
package/README.md
CHANGED
|
@@ -217,8 +217,8 @@ under the License.
|
|
|
217
217
|
|
|
218
218
|
[LTS]: https://github.com/nodejs/LTS
|
|
219
219
|
[PATH]: http://en.wikipedia.org/wiki/PATH_%28variable%29
|
|
220
|
-
[api]:
|
|
221
|
-
[chrome]:
|
|
220
|
+
[api]: https://www.selenium.dev/selenium/docs/api/javascript/
|
|
221
|
+
[chrome]: https://googlechromelabs.github.io/chrome-for-testing/#stable
|
|
222
222
|
[gh]: https://github.com/SeleniumHQ/selenium/
|
|
223
223
|
[issues]: https://github.com/SeleniumHQ/selenium/issues
|
|
224
224
|
[edge]: http://go.microsoft.com/fwlink/?LinkId=619687
|
package/bidi/browsingContext.js
CHANGED
|
@@ -500,21 +500,18 @@ class BrowsingContext {
|
|
|
500
500
|
*
|
|
501
501
|
* @param {Locator} locator - The locator object used to locate the nodes.
|
|
502
502
|
* @param {number} [maxNodeCount] - The maximum number of nodes to locate (optional).
|
|
503
|
-
* @param {string} [ownership] - The ownership type of the nodes (optional).
|
|
504
503
|
* @param {string} [sandbox] - The sandbox name for locating nodes (optional).
|
|
505
504
|
* @param {SerializationOptions} [serializationOptions] - The serialization options for locating nodes (optional).
|
|
506
505
|
* @param {ReferenceValue[]} [startNodes] - The array of start nodes for locating nodes (optional).
|
|
507
506
|
* @returns {Promise<RemoteValue[]>} - A promise that resolves to the arrays of located nodes.
|
|
508
507
|
* @throws {Error} - If the locator is not an instance of Locator.
|
|
509
508
|
* @throws {Error} - If the serializationOptions is provided but not an instance of SerializationOptions.
|
|
510
|
-
* @throws {Error} - If the ownership is provided but not 'root' or 'none'.
|
|
511
509
|
* @throws {Error} - If the startNodes is provided but not an array of ReferenceValue objects.
|
|
512
510
|
* @throws {Error} - If any of the startNodes is not an instance of ReferenceValue.
|
|
513
511
|
*/
|
|
514
512
|
async locateNodes(
|
|
515
513
|
locator,
|
|
516
514
|
maxNodeCount = undefined,
|
|
517
|
-
ownership = undefined,
|
|
518
515
|
sandbox = undefined,
|
|
519
516
|
serializationOptions = undefined,
|
|
520
517
|
startNodes = undefined,
|
|
@@ -527,10 +524,6 @@ class BrowsingContext {
|
|
|
527
524
|
throw Error(`Pass in SerializationOptions object. Received: ${serializationOptions} `)
|
|
528
525
|
}
|
|
529
526
|
|
|
530
|
-
if (ownership !== undefined && !['root', 'none'].includes(ownership)) {
|
|
531
|
-
throw Error(`Valid types are 'root' and 'none. Received: ${ownership}`)
|
|
532
|
-
}
|
|
533
|
-
|
|
534
527
|
if (startNodes !== undefined && !Array.isArray(startNodes)) {
|
|
535
528
|
throw Error(`Pass in an array of ReferenceValue objects. Received: ${startNodes}`)
|
|
536
529
|
}
|
|
@@ -549,7 +542,6 @@ class BrowsingContext {
|
|
|
549
542
|
context: this._id,
|
|
550
543
|
locator: Object.fromEntries(locator.toMap()),
|
|
551
544
|
maxNodeCount: maxNodeCount,
|
|
552
|
-
ownership: ownership,
|
|
553
545
|
sandbox: sandbox,
|
|
554
546
|
serializationOptions: serializationOptions,
|
|
555
547
|
startNodes: startNodes,
|
|
@@ -574,20 +566,13 @@ class BrowsingContext {
|
|
|
574
566
|
* Locates a single node in the browsing context.
|
|
575
567
|
*
|
|
576
568
|
* @param {Locator} locator - The locator used to find the node.
|
|
577
|
-
* @param {string} [ownership] - The ownership of the node (optional).
|
|
578
569
|
* @param {string} [sandbox] - The sandbox of the node (optional).
|
|
579
570
|
* @param {SerializationOptions} [serializationOptions] - The serialization options for the node (optional).
|
|
580
571
|
* @param {Array} [startNodes] - The starting nodes for the search (optional).
|
|
581
572
|
* @returns {Promise<RemoteValue>} - A promise that resolves to the located node.
|
|
582
573
|
*/
|
|
583
|
-
async locateNode(
|
|
584
|
-
locator,
|
|
585
|
-
ownership = undefined,
|
|
586
|
-
sandbox = undefined,
|
|
587
|
-
serializationOptions = undefined,
|
|
588
|
-
startNodes = undefined,
|
|
589
|
-
) {
|
|
590
|
-
const elements = await this.locateNodes(locator, 1, ownership, sandbox, serializationOptions, startNodes)
|
|
574
|
+
async locateNode(locator, sandbox = undefined, serializationOptions = undefined, startNodes = undefined) {
|
|
575
|
+
const elements = await this.locateNodes(locator, 1, sandbox, serializationOptions, startNodes)
|
|
591
576
|
return elements[0]
|
|
592
577
|
}
|
|
593
578
|
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// specific language governing permissions and limitations
|
|
16
16
|
// under the License.
|
|
17
17
|
|
|
18
|
-
const { BrowsingContextInfo, NavigationInfo } = require('./browsingContextTypes')
|
|
18
|
+
const { BrowsingContextInfo, NavigationInfo, UserPromptOpened, UserPromptClosed } = require('./browsingContextTypes')
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Represents a browsing context related events.
|
|
@@ -127,8 +127,10 @@ class BrowsingContextInspector {
|
|
|
127
127
|
let response = null
|
|
128
128
|
if ('navigation' in params) {
|
|
129
129
|
response = new NavigationInfo(params.context, params.navigation, params.timestamp, params.url)
|
|
130
|
+
} else if ('type' in params) {
|
|
131
|
+
response = new UserPromptOpened(params.context, params.type, params.message)
|
|
130
132
|
} else if ('accepted' in params) {
|
|
131
|
-
|
|
133
|
+
response = new UserPromptClosed(params.context, params.accepted, params.userText)
|
|
132
134
|
} else {
|
|
133
135
|
response = new BrowsingContextInfo(params.context, params.url, params.children, params.parent)
|
|
134
136
|
}
|
|
@@ -80,4 +80,20 @@ class NavigationInfo {
|
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
class UserPromptOpened {
|
|
84
|
+
constructor(browsingContextId, type, message) {
|
|
85
|
+
this.browsingContextId = browsingContextId
|
|
86
|
+
this.type = type
|
|
87
|
+
this.message = message
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
class UserPromptClosed {
|
|
92
|
+
constructor(browsingContextId, accepted, userText = undefined) {
|
|
93
|
+
this.browsingContextId = browsingContextId
|
|
94
|
+
this.accepted = accepted
|
|
95
|
+
this.userText = userText
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = { BrowsingContextInfo, NavigationInfo, UserPromptOpened, UserPromptClosed }
|
package/bidi/logInspector.js
CHANGED
|
@@ -21,16 +21,31 @@ const { ConsoleLogEntry, JavascriptLogEntry, GenericLogEntry } = require('./logE
|
|
|
21
21
|
const LOG = {
|
|
22
22
|
TYPE_CONSOLE: 'console',
|
|
23
23
|
TYPE_JS_LOGS: 'javascript',
|
|
24
|
+
TYPE_JS_EXCEPTION: 'javascriptException',
|
|
25
|
+
TYPE_LOGS: 'logs',
|
|
26
|
+
TYPE_CONSOLE_FILTER: 'console_filter',
|
|
27
|
+
TYPE_JS_LOGS_FILTER: 'javascript_filter',
|
|
28
|
+
TYPE_JS_EXCEPTION_FILTER: 'javascriptException_filter',
|
|
29
|
+
TYPE_LOGS_FILTER: 'logs_filter',
|
|
24
30
|
}
|
|
25
31
|
|
|
26
32
|
class LogInspector {
|
|
27
33
|
bidi
|
|
28
34
|
ws
|
|
35
|
+
#callbackId = 0
|
|
29
36
|
|
|
30
37
|
constructor(driver, browsingContextIds) {
|
|
31
38
|
this._driver = driver
|
|
32
39
|
this._browsingContextIds = browsingContextIds
|
|
33
|
-
this.listener =
|
|
40
|
+
this.listener = new Map()
|
|
41
|
+
this.listener.set(LOG.TYPE_CONSOLE, new Map())
|
|
42
|
+
this.listener.set(LOG.TYPE_JS_LOGS, new Map())
|
|
43
|
+
this.listener.set(LOG.TYPE_JS_EXCEPTION, new Map())
|
|
44
|
+
this.listener.set(LOG.TYPE_LOGS, new Map())
|
|
45
|
+
this.listener.set(LOG.TYPE_CONSOLE_FILTER, new Map())
|
|
46
|
+
this.listener.set(LOG.TYPE_JS_LOGS_FILTER, new Map())
|
|
47
|
+
this.listener.set(LOG.TYPE_JS_EXCEPTION_FILTER, new Map())
|
|
48
|
+
this.listener.set(LOG.TYPE_LOGS_FILTER, new Map())
|
|
34
49
|
}
|
|
35
50
|
|
|
36
51
|
/**
|
|
@@ -42,12 +57,47 @@ class LogInspector {
|
|
|
42
57
|
await this.bidi.subscribe('log.entryAdded', this._browsingContextIds)
|
|
43
58
|
}
|
|
44
59
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
60
|
+
addCallback(eventType, callback) {
|
|
61
|
+
const id = ++this.#callbackId
|
|
62
|
+
|
|
63
|
+
const eventCallbackMap = this.listener.get(eventType)
|
|
64
|
+
eventCallbackMap.set(id, callback)
|
|
65
|
+
return id
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
removeCallback(id) {
|
|
69
|
+
let hasId = false
|
|
70
|
+
for (const [, callbacks] of this.listener) {
|
|
71
|
+
if (callbacks.has(id)) {
|
|
72
|
+
callbacks.delete(id)
|
|
73
|
+
hasId = true
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!hasId) {
|
|
78
|
+
throw Error(`Callback with id ${id} not found`)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
invokeCallbacks(eventType, data) {
|
|
83
|
+
const callbacks = this.listener.get(eventType)
|
|
84
|
+
if (callbacks) {
|
|
85
|
+
for (const [, callback] of callbacks) {
|
|
86
|
+
callback(data)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
invokeCallbacksWithFilter(eventType, data, filterLevel) {
|
|
92
|
+
const callbacks = this.listener.get(eventType)
|
|
93
|
+
if (callbacks) {
|
|
94
|
+
for (const [, value] of callbacks) {
|
|
95
|
+
const callback = value.callback
|
|
96
|
+
const filter = value.filter
|
|
97
|
+
if (filterLevel === filter.getLevel()) {
|
|
98
|
+
callback(data)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
51
101
|
}
|
|
52
102
|
}
|
|
53
103
|
|
|
@@ -55,13 +105,21 @@ class LogInspector {
|
|
|
55
105
|
* Listen to Console logs
|
|
56
106
|
* @param callback
|
|
57
107
|
* @param filterBy
|
|
58
|
-
* @returns {Promise<
|
|
108
|
+
* @returns {Promise<number>}
|
|
59
109
|
*/
|
|
60
110
|
async onConsoleEntry(callback, filterBy = undefined) {
|
|
61
111
|
if (filterBy !== undefined && !(filterBy instanceof FilterBy)) {
|
|
62
112
|
throw Error(`Pass valid FilterBy object. Received: ${filterBy}`)
|
|
63
113
|
}
|
|
64
114
|
|
|
115
|
+
let id
|
|
116
|
+
|
|
117
|
+
if (filterBy !== undefined) {
|
|
118
|
+
id = this.addCallback(LOG.TYPE_CONSOLE_FILTER, { callback: callback, filter: filterBy })
|
|
119
|
+
} else {
|
|
120
|
+
id = this.addCallback(LOG.TYPE_CONSOLE, callback)
|
|
121
|
+
}
|
|
122
|
+
|
|
65
123
|
this.ws = await this.bidi.socket
|
|
66
124
|
|
|
67
125
|
this.ws.on('message', (event) => {
|
|
@@ -81,27 +139,37 @@ class LogInspector {
|
|
|
81
139
|
|
|
82
140
|
if (filterBy !== undefined) {
|
|
83
141
|
if (params?.level === filterBy.getLevel()) {
|
|
84
|
-
|
|
142
|
+
this.invokeCallbacksWithFilter(LOG.TYPE_CONSOLE_FILTER, consoleEntry, filterBy.getLevel())
|
|
85
143
|
}
|
|
86
144
|
return
|
|
87
145
|
}
|
|
88
146
|
|
|
89
|
-
|
|
147
|
+
this.invokeCallbacks(LOG.TYPE_CONSOLE, consoleEntry)
|
|
90
148
|
}
|
|
91
149
|
})
|
|
150
|
+
|
|
151
|
+
return id
|
|
92
152
|
}
|
|
93
153
|
|
|
94
154
|
/**
|
|
95
155
|
* Listen to JS logs
|
|
96
156
|
* @param callback
|
|
97
157
|
* @param filterBy
|
|
98
|
-
* @returns {Promise<
|
|
158
|
+
* @returns {Promise<number>}
|
|
99
159
|
*/
|
|
100
160
|
async onJavascriptLog(callback, filterBy = undefined) {
|
|
101
161
|
if (filterBy !== undefined && !(filterBy instanceof FilterBy)) {
|
|
102
162
|
throw Error(`Pass valid FilterBy object. Received: ${filterBy}`)
|
|
103
163
|
}
|
|
104
164
|
|
|
165
|
+
let id
|
|
166
|
+
|
|
167
|
+
if (filterBy !== undefined) {
|
|
168
|
+
id = this.addCallback(LOG.TYPE_JS_LOGS_FILTER, { callback: callback, filter: filterBy })
|
|
169
|
+
} else {
|
|
170
|
+
id = this.addCallback(LOG.TYPE_JS_LOGS, callback)
|
|
171
|
+
}
|
|
172
|
+
|
|
105
173
|
this.ws = await this.bidi.socket
|
|
106
174
|
|
|
107
175
|
this.ws.on('message', (event) => {
|
|
@@ -118,29 +186,26 @@ class LogInspector {
|
|
|
118
186
|
|
|
119
187
|
if (filterBy !== undefined) {
|
|
120
188
|
if (params?.level === filterBy.getLevel()) {
|
|
121
|
-
|
|
189
|
+
this.invokeCallbacksWithFilter(LOG.TYPE_JS_LOGS_FILTER, jsEntry, filterBy.getLevel())
|
|
122
190
|
}
|
|
123
191
|
return
|
|
124
192
|
}
|
|
125
193
|
|
|
126
|
-
|
|
194
|
+
this.invokeCallbacks(LOG.TYPE_JS_LOGS, jsEntry)
|
|
127
195
|
}
|
|
128
196
|
})
|
|
197
|
+
|
|
198
|
+
return id
|
|
129
199
|
}
|
|
130
200
|
|
|
131
201
|
/**
|
|
132
202
|
* Listen to JS Exceptions
|
|
133
203
|
* @param callback
|
|
134
|
-
* @returns {Promise<
|
|
204
|
+
* @returns {Promise<number>}
|
|
135
205
|
*/
|
|
136
206
|
async onJavascriptException(callback) {
|
|
207
|
+
const id = this.addCallback(LOG.TYPE_JS_EXCEPTION, callback)
|
|
137
208
|
this.ws = await this.bidi.socket
|
|
138
|
-
let enabled = LOG.TYPE_JS_EXCEPTION in this.listener || this.logListener(LOG.TYPE_JS_EXCEPTION)
|
|
139
|
-
this.listener[LOG.TYPE_JS_EXCEPTION].push(callback)
|
|
140
|
-
|
|
141
|
-
if (enabled) {
|
|
142
|
-
return
|
|
143
|
-
}
|
|
144
209
|
|
|
145
210
|
this.ws.on('message', (event) => {
|
|
146
211
|
const { params } = JSON.parse(Buffer.from(event.toString()))
|
|
@@ -153,24 +218,31 @@ class LogInspector {
|
|
|
153
218
|
params.stackTrace,
|
|
154
219
|
)
|
|
155
220
|
|
|
156
|
-
this.
|
|
157
|
-
listener(jsErrorEntry)
|
|
158
|
-
})
|
|
221
|
+
this.invokeCallbacks(LOG.TYPE_JS_EXCEPTION, jsErrorEntry)
|
|
159
222
|
}
|
|
160
223
|
})
|
|
224
|
+
|
|
225
|
+
return id
|
|
161
226
|
}
|
|
162
227
|
|
|
163
228
|
/**
|
|
164
229
|
* Listen to any logs
|
|
165
230
|
* @param callback
|
|
166
231
|
* @param filterBy
|
|
167
|
-
* @returns {Promise<
|
|
232
|
+
* @returns {Promise<number>}
|
|
168
233
|
*/
|
|
169
234
|
async onLog(callback, filterBy = undefined) {
|
|
170
235
|
if (filterBy !== undefined && !(filterBy instanceof FilterBy)) {
|
|
171
236
|
throw Error(`Pass valid FilterBy object. Received: ${filterBy}`)
|
|
172
237
|
}
|
|
173
238
|
|
|
239
|
+
let id
|
|
240
|
+
if (filterBy !== undefined) {
|
|
241
|
+
id = this.addCallback(LOG.TYPE_LOGS_FILTER, { callback: callback, filter: filterBy })
|
|
242
|
+
} else {
|
|
243
|
+
id = this.addCallback(LOG.TYPE_LOGS, callback)
|
|
244
|
+
}
|
|
245
|
+
|
|
174
246
|
this.ws = await this.bidi.socket
|
|
175
247
|
|
|
176
248
|
this.ws.on('message', (event) => {
|
|
@@ -191,7 +263,16 @@ class LogInspector {
|
|
|
191
263
|
return
|
|
192
264
|
}
|
|
193
265
|
|
|
194
|
-
|
|
266
|
+
if (filterBy !== undefined) {
|
|
267
|
+
if (params?.level === filterBy.getLevel()) {
|
|
268
|
+
{
|
|
269
|
+
this.invokeCallbacksWithFilter(LOG.TYPE_LOGS_FILTER, jsEntry, filterBy.getLevel())
|
|
270
|
+
}
|
|
271
|
+
return
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
this.invokeCallbacks(LOG.TYPE_LOGS, jsEntry)
|
|
195
276
|
return
|
|
196
277
|
}
|
|
197
278
|
|
|
@@ -209,12 +290,12 @@ class LogInspector {
|
|
|
209
290
|
|
|
210
291
|
if (filterBy !== undefined) {
|
|
211
292
|
if (params?.level === filterBy.getLevel()) {
|
|
212
|
-
|
|
293
|
+
this.invokeCallbacksWithFilter(LOG.TYPE_LOGS_FILTER, consoleEntry, filterBy.getLevel())
|
|
213
294
|
}
|
|
214
295
|
return
|
|
215
296
|
}
|
|
216
297
|
|
|
217
|
-
|
|
298
|
+
this.invokeCallbacks(LOG.TYPE_LOGS, consoleEntry)
|
|
218
299
|
return
|
|
219
300
|
}
|
|
220
301
|
|
|
@@ -229,14 +310,19 @@ class LogInspector {
|
|
|
229
310
|
|
|
230
311
|
if (filterBy !== undefined) {
|
|
231
312
|
if (params?.level === filterBy.getLevel()) {
|
|
232
|
-
|
|
313
|
+
{
|
|
314
|
+
this.invokeCallbacksWithFilter(LOG.TYPE_LOGS_FILTER, genericEntry, filterBy.getLevel())
|
|
315
|
+
}
|
|
316
|
+
return
|
|
233
317
|
}
|
|
234
|
-
return
|
|
235
318
|
}
|
|
236
319
|
|
|
237
|
-
|
|
320
|
+
this.invokeCallbacks(LOG.TYPE_LOGS, genericEntry)
|
|
321
|
+
return
|
|
238
322
|
}
|
|
239
323
|
})
|
|
324
|
+
|
|
325
|
+
return id
|
|
240
326
|
}
|
|
241
327
|
|
|
242
328
|
/**
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/chrome.js
CHANGED
|
@@ -118,6 +118,8 @@
|
|
|
118
118
|
* [PATH]: http://en.wikipedia.org/wiki/PATH_%28variable%29
|
|
119
119
|
* [android]: https://chromedriver.chromium.org/getting-started/getting-started---android
|
|
120
120
|
* [webview]: https://developer.chrome.com/multidevice/webview/overview
|
|
121
|
+
*
|
|
122
|
+
* @module selenium-webdriver/chrome
|
|
121
123
|
*/
|
|
122
124
|
|
|
123
125
|
'use strict'
|
package/chromium.js
CHANGED
package/edge.js
CHANGED
package/firefox.js
CHANGED
|
@@ -103,6 +103,8 @@
|
|
|
103
103
|
*
|
|
104
104
|
* [geckodriver release]: https://github.com/mozilla/geckodriver/releases/
|
|
105
105
|
* [PATH]: http://en.wikipedia.org/wiki/PATH_%28variable%29
|
|
106
|
+
*
|
|
107
|
+
* @module selenium-webdriver/firefox
|
|
106
108
|
*/
|
|
107
109
|
|
|
108
110
|
'use strict'
|
|
@@ -244,6 +246,9 @@ class Options extends Capabilities {
|
|
|
244
246
|
constructor(other) {
|
|
245
247
|
super(other)
|
|
246
248
|
this.setBrowserName(Browser.FIREFOX)
|
|
249
|
+
// Firefox 129 onwards the CDP protocol will not be enabled by default. Setting this preference will enable it.
|
|
250
|
+
// https://fxdx.dev/deprecating-cdp-support-in-firefox-embracing-the-future-with-webdriver-bidi/.
|
|
251
|
+
this.setPreference('remote.active-protocols', 3)
|
|
247
252
|
}
|
|
248
253
|
|
|
249
254
|
/**
|
package/ie.js
CHANGED
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
* [PATH](http://en.wikipedia.org/wiki/PATH_%28variable%29). You must also apply
|
|
24
24
|
* the system configuration outlined on the Selenium project
|
|
25
25
|
* [wiki](https://github.com/SeleniumHQ/selenium/wiki/InternetExplorerDriver)
|
|
26
|
+
*
|
|
27
|
+
* @module selenium-webdriver/ie
|
|
26
28
|
*/
|
|
27
29
|
|
|
28
30
|
'use strict'
|
package/lib/script.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
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
|
+
const logInspector = require('../bidi/logInspector')
|
|
19
|
+
|
|
20
|
+
class Script {
|
|
21
|
+
#driver
|
|
22
|
+
#logInspector
|
|
23
|
+
|
|
24
|
+
constructor(driver) {
|
|
25
|
+
this.#driver = driver
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// This should be done in the constructor.
|
|
29
|
+
// But since it needs to call async methods we cannot do that in the constructor.
|
|
30
|
+
// We can have a separate async method that initialises the Script instance.
|
|
31
|
+
// However, that pattern does not allow chaining the methods as we would like the user to use it.
|
|
32
|
+
// Since it involves awaiting to get the instance and then another await to call the method.
|
|
33
|
+
// Using this allows the user to do this "await driver.script().addJavaScriptErrorHandler(callback)"
|
|
34
|
+
async #init() {
|
|
35
|
+
if (this.#logInspector !== undefined) {
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
this.#logInspector = await logInspector(this.#driver)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async addJavaScriptErrorHandler(callback) {
|
|
42
|
+
await this.#init()
|
|
43
|
+
return await this.#logInspector.onJavascriptException(callback)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async removeJavaScriptErrorHandler(id) {
|
|
47
|
+
await this.#init()
|
|
48
|
+
await this.#logInspector.removeCallback(id)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async addConsoleMessageHandler(callback) {
|
|
52
|
+
await this.#init()
|
|
53
|
+
return this.#logInspector.onConsoleEntry(callback)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async removeConsoleMessageHandler(id) {
|
|
57
|
+
await this.#init()
|
|
58
|
+
|
|
59
|
+
await this.#logInspector.removeCallback(id)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = Script
|
package/lib/select.js
CHANGED
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
|
|
37
37
|
'use strict'
|
|
38
38
|
|
|
39
|
-
const { By
|
|
39
|
+
const { By } = require('./by')
|
|
40
40
|
const error = require('./error')
|
|
41
41
|
|
|
42
42
|
/**
|
|
@@ -146,6 +146,10 @@ class Select {
|
|
|
146
146
|
* @param {WebElement} element Select WebElement.
|
|
147
147
|
*/
|
|
148
148
|
constructor(element) {
|
|
149
|
+
if (element === null) {
|
|
150
|
+
throw new Error(`Element must not be null. Please provide a valid <select> element.`)
|
|
151
|
+
}
|
|
152
|
+
|
|
149
153
|
this.element = element
|
|
150
154
|
|
|
151
155
|
this.element.getAttribute('tagName').then(function (tagName) {
|
|
@@ -153,6 +157,10 @@ class Select {
|
|
|
153
157
|
throw new Error(`Select only works on <select> elements`)
|
|
154
158
|
}
|
|
155
159
|
})
|
|
160
|
+
|
|
161
|
+
this.element.getAttribute('multiple').then((multiple) => {
|
|
162
|
+
this.multiple = multiple !== null && multiple !== 'false'
|
|
163
|
+
})
|
|
156
164
|
}
|
|
157
165
|
|
|
158
166
|
/**
|
|
@@ -216,9 +224,7 @@ class Select {
|
|
|
216
224
|
let matched = false
|
|
217
225
|
let isMulti = await this.isMultiple()
|
|
218
226
|
|
|
219
|
-
let options = await this.element.findElements(
|
|
220
|
-
css: 'option[value =' + escapeCss(value) + ']',
|
|
221
|
-
})
|
|
227
|
+
let options = await this.element.findElements(By.xpath('.//option[@value = ' + escapeQuotes(value) + ']'))
|
|
222
228
|
|
|
223
229
|
for (let option of options) {
|
|
224
230
|
await this.setSelected(option)
|
|
@@ -254,30 +260,46 @@ class Select {
|
|
|
254
260
|
async selectByVisibleText(text) {
|
|
255
261
|
text = typeof text === 'number' ? text.toString() : text
|
|
256
262
|
|
|
257
|
-
const
|
|
258
|
-
.trim() // strip leading and trailing white-space characters
|
|
259
|
-
.replace(/\s+/, ' ') // replace sequences of whitespace characters by a single space
|
|
263
|
+
const xpath = './/option[normalize-space(.) = ' + escapeQuotes(text) + ']'
|
|
260
264
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
265
|
+
const options = await this.element.findElements(By.xpath(xpath))
|
|
266
|
+
|
|
267
|
+
for (let option of options) {
|
|
268
|
+
await this.setSelected(option)
|
|
269
|
+
if (!(await this.isMultiple())) {
|
|
270
|
+
return
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
let matched = Array.isArray(options) && options.length > 0
|
|
275
|
+
|
|
276
|
+
if (!matched && text.includes(' ')) {
|
|
277
|
+
const subStringWithoutSpace = getLongestSubstringWithoutSpace(text)
|
|
278
|
+
let candidates
|
|
279
|
+
if ('' === subStringWithoutSpace) {
|
|
280
|
+
candidates = await this.element.findElements(By.tagName('option'))
|
|
281
|
+
} else {
|
|
282
|
+
const xpath = './/option[contains(., ' + escapeQuotes(subStringWithoutSpace) + ')]'
|
|
283
|
+
candidates = await this.element.findElements(By.xpath(xpath))
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const trimmed = text.trim()
|
|
287
|
+
|
|
288
|
+
for (let option of candidates) {
|
|
289
|
+
const optionText = await option.getText()
|
|
290
|
+
if (trimmed === optionText.trim()) {
|
|
291
|
+
await this.setSelected(option)
|
|
292
|
+
if (!(await this.isMultiple())) {
|
|
293
|
+
return
|
|
294
|
+
}
|
|
295
|
+
matched = true
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (!matched) {
|
|
301
|
+
throw new Error(`Cannot locate option with text: ${text}`)
|
|
302
|
+
}
|
|
281
303
|
}
|
|
282
304
|
|
|
283
305
|
/**
|
|
@@ -293,7 +315,7 @@ class Select {
|
|
|
293
315
|
* @returns {Promise<boolean>}
|
|
294
316
|
*/
|
|
295
317
|
async isMultiple() {
|
|
296
|
-
return
|
|
318
|
+
return this.multiple
|
|
297
319
|
}
|
|
298
320
|
|
|
299
321
|
/**
|
|
@@ -353,29 +375,9 @@ class Select {
|
|
|
353
375
|
*/
|
|
354
376
|
text = typeof text === 'number' ? text.toString() : text
|
|
355
377
|
|
|
356
|
-
const
|
|
357
|
-
.
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
/**
|
|
361
|
-
* find option element using xpath
|
|
362
|
-
*/
|
|
363
|
-
const formatted = /"/.test(normalized)
|
|
364
|
-
? 'concat("' + normalized.split('"').join('", \'"\', "') + '")'
|
|
365
|
-
: `"${normalized}"`
|
|
366
|
-
const dotFormat = `[. = ${formatted}]`
|
|
367
|
-
const spaceFormat = `[normalize-space(text()) = ${formatted}]`
|
|
368
|
-
|
|
369
|
-
const selections = [
|
|
370
|
-
`./option${dotFormat}`,
|
|
371
|
-
`./option${spaceFormat}`,
|
|
372
|
-
`./optgroup/option${dotFormat}`,
|
|
373
|
-
`./optgroup/option${spaceFormat}`,
|
|
374
|
-
]
|
|
375
|
-
|
|
376
|
-
const optionElement = await this.element.findElement({
|
|
377
|
-
xpath: selections.join('|'),
|
|
378
|
-
})
|
|
378
|
+
const optionElement = await this.element.findElement(
|
|
379
|
+
By.xpath('.//option[normalize-space(.) = ' + escapeQuotes(text) + ']'),
|
|
380
|
+
)
|
|
379
381
|
if (await optionElement.isSelected()) {
|
|
380
382
|
await optionElement.click()
|
|
381
383
|
}
|
|
@@ -431,9 +433,11 @@ class Select {
|
|
|
431
433
|
|
|
432
434
|
let matched = false
|
|
433
435
|
|
|
434
|
-
let options = await this.element.findElements(
|
|
435
|
-
|
|
436
|
-
|
|
436
|
+
let options = await this.element.findElements(By.xpath('.//option[@value = ' + escapeQuotes(value) + ']'))
|
|
437
|
+
|
|
438
|
+
if (options.length === 0) {
|
|
439
|
+
throw new Error(`Cannot locate option with value: ${value}`)
|
|
440
|
+
}
|
|
437
441
|
|
|
438
442
|
for (let option of options) {
|
|
439
443
|
if (await option.isSelected()) {
|
|
@@ -457,4 +461,42 @@ class Select {
|
|
|
457
461
|
}
|
|
458
462
|
}
|
|
459
463
|
|
|
460
|
-
|
|
464
|
+
function escapeQuotes(toEscape) {
|
|
465
|
+
if (toEscape.includes(`"`) && toEscape.includes(`'`)) {
|
|
466
|
+
const quoteIsLast = toEscape.lastIndexOf(`"`) === toEscape.length - 1
|
|
467
|
+
const substrings = toEscape.split(`"`)
|
|
468
|
+
|
|
469
|
+
// Remove the last element if it's an empty string
|
|
470
|
+
if (substrings[substrings.length - 1] === '') {
|
|
471
|
+
substrings.pop()
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
let result = 'concat('
|
|
475
|
+
|
|
476
|
+
for (let i = 0; i < substrings.length; i++) {
|
|
477
|
+
result += `"${substrings[i]}"`
|
|
478
|
+
result += i === substrings.length - 1 ? (quoteIsLast ? `, '"')` : `)`) : `, '"', `
|
|
479
|
+
}
|
|
480
|
+
return result
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (toEscape.includes('"')) {
|
|
484
|
+
return `'${toEscape}'`
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Otherwise return the quoted string
|
|
488
|
+
return `"${toEscape}"`
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function getLongestSubstringWithoutSpace(text) {
|
|
492
|
+
let words = text.split(' ')
|
|
493
|
+
let longestString = ''
|
|
494
|
+
for (let word of words) {
|
|
495
|
+
if (word.length > longestString.length) {
|
|
496
|
+
longestString = word
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return longestString
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
module.exports = { Select, escapeQuotes }
|
package/lib/webdriver.js
CHANGED
|
@@ -43,6 +43,7 @@ const { isObject } = require('./util')
|
|
|
43
43
|
const BIDI = require('../bidi')
|
|
44
44
|
const { PinnedScript } = require('./pinnedScript')
|
|
45
45
|
const JSZip = require('jszip')
|
|
46
|
+
const Script = require('./script')
|
|
46
47
|
|
|
47
48
|
// Capability names that are defined in the W3C spec.
|
|
48
49
|
const W3C_CAPABILITY_NAMES = new Set([
|
|
@@ -654,6 +655,7 @@ function filterNonW3CCaps(capabilities) {
|
|
|
654
655
|
* @implements {IWebDriver}
|
|
655
656
|
*/
|
|
656
657
|
class WebDriver {
|
|
658
|
+
#script = undefined
|
|
657
659
|
/**
|
|
658
660
|
* @param {!(./session.Session|IThenable<!./session.Session>)} session Either
|
|
659
661
|
* a known session or a promise that will be resolved to a session.
|
|
@@ -1104,6 +1106,16 @@ class WebDriver {
|
|
|
1104
1106
|
return new TargetLocator(this)
|
|
1105
1107
|
}
|
|
1106
1108
|
|
|
1109
|
+
script() {
|
|
1110
|
+
// The Script calls the LogInspector which maintains state of the callbacks.
|
|
1111
|
+
// Returning a new instance of the same driver will not work while removing callbacks.
|
|
1112
|
+
if (this.#script === undefined) {
|
|
1113
|
+
this.#script = new Script(this)
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
return this.#script
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1107
1119
|
validatePrintPageParams(keys, object) {
|
|
1108
1120
|
let page = {}
|
|
1109
1121
|
let margin = {}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "selenium-webdriver",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.22.0",
|
|
4
4
|
"description": "The official WebDriver JavaScript bindings from the Selenium project",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"keywords": [
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"url": "https://github.com/SeleniumHQ/selenium.git"
|
|
21
21
|
},
|
|
22
22
|
"engines": {
|
|
23
|
-
"node": ">= 14.
|
|
23
|
+
"node": ">= 14.21.0"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"jszip": "^3.10.1",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@bazel/runfiles": "^5.8.1",
|
|
32
32
|
"@eslint/js": "^9.1.1",
|
|
33
|
+
"clean-jsdoc-theme": "^4.3.0",
|
|
33
34
|
"eslint": "^9.1.0",
|
|
34
35
|
"eslint-config-prettier": "^9.1.0",
|
|
35
36
|
"eslint-plugin-mocha": "^10.4.3",
|
|
@@ -38,6 +39,7 @@
|
|
|
38
39
|
"eslint-plugin-prettier": "^5.1.3",
|
|
39
40
|
"express": "^4.19.2",
|
|
40
41
|
"globals": "^15.0.0",
|
|
42
|
+
"jsdoc": "^4.0.3",
|
|
41
43
|
"mocha": "^10.4.0",
|
|
42
44
|
"mocha-junit-reporter": "^2.2.1",
|
|
43
45
|
"multer": "^1.4.5-lts.1",
|
|
@@ -49,7 +51,8 @@
|
|
|
49
51
|
"lint": "eslint .",
|
|
50
52
|
"lint:fix": "eslint . --fix",
|
|
51
53
|
"test": "npm run lint && mocha -t 600000 --recursive test",
|
|
52
|
-
"test-jasmine": "bazel test //javascript/node/selenium-webdriver:tests"
|
|
54
|
+
"test-jasmine": "bazel test //javascript/node/selenium-webdriver:tests",
|
|
55
|
+
"generate-docs": "jsdoc --configure jsdoc_conf.json --verbose"
|
|
53
56
|
},
|
|
54
57
|
"mocha": {
|
|
55
58
|
"recursive": true,
|