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 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]: http://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/
221
- [chrome]: http://chromedriver.storage.googleapis.com/index.html
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
@@ -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
- /* Needs to be updated when browsers implement other events */
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
- module.exports = { BrowsingContextInfo, NavigationInfo }
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 }
@@ -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
- * @param kind
47
- */
48
- logListener(kind) {
49
- if (!(kind in this.listener)) {
50
- this.listener[kind] = []
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<void>}
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
- callback(consoleEntry)
142
+ this.invokeCallbacksWithFilter(LOG.TYPE_CONSOLE_FILTER, consoleEntry, filterBy.getLevel())
85
143
  }
86
144
  return
87
145
  }
88
146
 
89
- callback(consoleEntry)
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<void>}
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
- callback(jsEntry)
189
+ this.invokeCallbacksWithFilter(LOG.TYPE_JS_LOGS_FILTER, jsEntry, filterBy.getLevel())
122
190
  }
123
191
  return
124
192
  }
125
193
 
126
- callback(jsEntry)
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<void>}
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.listener[LOG.TYPE_JS_EXCEPTION].forEach((listener) => {
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<void>}
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
- callback(jsEntry)
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
- callback(consoleEntry)
293
+ this.invokeCallbacksWithFilter(LOG.TYPE_LOGS_FILTER, consoleEntry, filterBy.getLevel())
213
294
  }
214
295
  return
215
296
  }
216
297
 
217
- callback(consoleEntry)
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
- callback(genericEntry)
313
+ {
314
+ this.invokeCallbacksWithFilter(LOG.TYPE_LOGS_FILTER, genericEntry, filterBy.getLevel())
315
+ }
316
+ return
233
317
  }
234
- return
235
318
  }
236
319
 
237
- callback(genericEntry)
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
@@ -66,6 +66,8 @@
66
66
  * // configure browser options ...
67
67
  *
68
68
  * let driver = chrome.Driver.createSession(options, service);
69
+ *
70
+ * @module selenium-webdriver/chromium
69
71
  */
70
72
 
71
73
  'use strict'
package/edge.js CHANGED
@@ -73,6 +73,8 @@
73
73
  * {@link ./builder.Builder selenium-webdriver.Builder}.
74
74
  *
75
75
  * [WebDriver (Chromium)]: https://docs.microsoft.com/en-us/microsoft-edge/webdriver-chromium
76
+ *
77
+ * @module selenium-webdriver/edge
76
78
  */
77
79
 
78
80
  'use strict'
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, escapeCss } = require('./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 normalized = text
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
- * find option element using xpath
263
- */
264
- const formatted = /"/.test(normalized)
265
- ? 'concat("' + normalized.split('"').join('", \'"\', "') + '")'
266
- : `"${normalized}"`
267
- const dotFormat = `[. = ${formatted}]`
268
- const spaceFormat = `[normalize-space(text()) = ${formatted}]`
269
-
270
- const selections = [
271
- `./option${dotFormat}`,
272
- `./option${spaceFormat}`,
273
- `./optgroup/option${dotFormat}`,
274
- `./optgroup/option${spaceFormat}`,
275
- ]
276
-
277
- const optionElement = await this.element.findElement({
278
- xpath: selections.join('|'),
279
- })
280
- await this.setSelected(optionElement)
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 (await this.element.getAttribute('multiple')) !== null
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 normalized = text
357
- .trim() // strip leading and trailing white-space characters
358
- .replace(/\s+/, ' ') // replace sequences of whitespace characters by a single space
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
- css: 'option[value =' + escapeCss(value) + ']',
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
- module.exports = { Select }
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.20.0",
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.20.0"
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,
package/safari.js CHANGED
@@ -17,6 +17,8 @@
17
17
 
18
18
  /**
19
19
  * @fileoverview Defines a WebDriver client for Safari.
20
+ *
21
+ * @module selenium-webdriver/safari
20
22
  */
21
23
 
22
24
  'use strict'