selenium-webdriver 4.21.0 → 4.23.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,23 @@
1
+ ## 4.23.0
2
+
3
+ - Expose pnpm as a tool we can use
4
+ - [bidi] Fix the event unsubscribe method. Update modules to have close methods. (#14192)
5
+ - Run Node browser tests on the RBE (#14194)
6
+ - [bidi] Add methods to add/remove handlers in Script module (#14230)
7
+ - [bidi] Add source type to log entry (#14244)
8
+ - [bidi] Add dom mutation handlers (#14238)
9
+ - [bidi] Add high-level script pinning APIs (#14250)
10
+ - [bidi] Deprecate argument value wrapper class (#14251)
11
+ - Add CDP for Chrome 127 and remove 124
12
+
13
+ ## 4.22.0
14
+
15
+ - [bidi] Add types for user prompt related events (#14097)
16
+ - Add preference to enable CDP in Firefox by default (#14091)
17
+ - [bidi] Add callback handlers for logging APIs (#14120)
18
+ - [bidi] Add high-level logging API (#14135)
19
+ - Add CDP for Chrome 126 and remove 123
20
+
1
21
  ## 4.21.0
2
22
 
3
23
  - Add CDP for Chrome 125 and remove 122
package/README.md CHANGED
@@ -217,7 +217,7 @@ 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/
220
+ [api]: https://www.selenium.dev/selenium/docs/api/javascript/
221
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
@@ -17,6 +17,11 @@
17
17
 
18
18
  const { LocalValue } = require('./protocolValue')
19
19
 
20
+ /**
21
+ * @deprecated
22
+ * in favor of LocalValue methods for all argument values.
23
+ * This extra wrapper is not required.
24
+ */
20
25
  class ArgumentValue {
21
26
  constructor(value) {
22
27
  this.value = value
@@ -24,7 +29,7 @@ class ArgumentValue {
24
29
 
25
30
  asMap() {
26
31
  if (this.value instanceof LocalValue) {
27
- return this.value.toJson()
32
+ return this.value.asMap()
28
33
  } else {
29
34
  // ReferenceValue
30
35
  return this.value.asMap()
@@ -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
  }
@@ -136,6 +138,29 @@ class BrowsingContextInspector {
136
138
  }
137
139
  })
138
140
  }
141
+
142
+ async close() {
143
+ if (
144
+ this._browsingContextIds !== null &&
145
+ this._browsingContextIds !== undefined &&
146
+ this._browsingContextIds.length > 0
147
+ ) {
148
+ await this.bidi.unsubscribe(
149
+ 'browsingContext.contextCreated',
150
+ 'browsingContext.contextDestroyed',
151
+ 'browsingContext.fragmentNavigated',
152
+ 'browsingContext.userPromptClosed',
153
+ this._browsingContextIds,
154
+ )
155
+ } else {
156
+ await this.bidi.unsubscribe(
157
+ 'browsingContext.contextCreated',
158
+ 'browsingContext.contextDestroyed',
159
+ 'browsingContext.fragmentNavigated',
160
+ 'browsingContext.userPromptClosed',
161
+ )
162
+ }
163
+ }
139
164
  }
140
165
 
141
166
  async function getBrowsingContextInstance(driver, browsingContextIds = null) {
@@ -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 }
package/bidi/index.js CHANGED
@@ -157,6 +157,8 @@ class Index extends EventEmitter {
157
157
  params.params.contexts = contextsArray
158
158
  }
159
159
 
160
+ this.events.push(...eventsArray)
161
+
160
162
  await this.send(params)
161
163
  }
162
164
 
@@ -167,11 +169,14 @@ class Index extends EventEmitter {
167
169
  * @returns {Promise<void>}
168
170
  */
169
171
  async unsubscribe(events, browsingContexts) {
170
- if (typeof events === 'string') {
171
- this.events = this.events.filter((event) => event !== events)
172
- } else if (Array.isArray(events)) {
173
- this.events = this.events.filter((event) => !events.includes(event))
174
- }
172
+ const eventsToRemove = typeof events === 'string' ? [events] : events
173
+
174
+ // Check if the eventsToRemove are in the subscribed events array
175
+ // Filter out events that are not in this.events before filtering
176
+ const existingEvents = eventsToRemove.filter((event) => this.events.includes(event))
177
+
178
+ // Remove the events from the subscribed events array
179
+ this.events = this.events.filter((event) => !existingEvents.includes(event))
175
180
 
176
181
  if (typeof browsingContexts === 'string') {
177
182
  this.browsingContexts.pop()
@@ -179,10 +184,13 @@ class Index extends EventEmitter {
179
184
  this.browsingContexts = this.browsingContexts.filter((id) => !browsingContexts.includes(id))
180
185
  }
181
186
 
187
+ if (existingEvents.length === 0) {
188
+ return
189
+ }
182
190
  const params = {
183
191
  method: 'session.unsubscribe',
184
192
  params: {
185
- events: this.events,
193
+ events: existingEvents,
186
194
  },
187
195
  }
188
196
 
@@ -17,6 +17,8 @@
17
17
 
18
18
  'use strict'
19
19
 
20
+ const { Source } = require('./scriptTypes')
21
+
20
22
  /**
21
23
  * Represents a base log entry.
22
24
  * Desribed in https://w3c.github.io/webdriver-bidi/#types-log-logentry.
@@ -25,12 +27,14 @@ class BaseLogEntry {
25
27
  /**
26
28
  * Creates a new instance of BaseLogEntry.
27
29
  * @param {string} level - The log level.
30
+ * @param {string} text - The log source.
28
31
  * @param {string} text - The log text.
29
32
  * @param {number} timeStamp - The log timestamp.
30
33
  * @param {string} stackTrace - The log stack trace.
31
34
  */
32
- constructor(level, text, timeStamp, stackTrace) {
35
+ constructor(level, source, text, timeStamp, stackTrace) {
33
36
  this._level = level
37
+ this._source = new Source(source)
34
38
  this._text = text
35
39
  this._timeStamp = timeStamp
36
40
  this._stackTrace = stackTrace
@@ -67,6 +71,10 @@ class BaseLogEntry {
67
71
  get stackTrace() {
68
72
  return this._stackTrace
69
73
  }
74
+
75
+ get source() {
76
+ return this._source
77
+ }
70
78
  }
71
79
 
72
80
  /**
@@ -83,8 +91,8 @@ class GenericLogEntry extends BaseLogEntry {
83
91
  * @param {string} type - The log type.
84
92
  * @param {string} stackTrace - The log stack trace.
85
93
  */
86
- constructor(level, text, timeStamp, type, stackTrace) {
87
- super(level, text, timeStamp, stackTrace)
94
+ constructor(level, source, text, timeStamp, type, stackTrace) {
95
+ super(level, source, text, timeStamp, stackTrace)
88
96
  this._type = type
89
97
  }
90
98
 
@@ -103,10 +111,9 @@ class GenericLogEntry extends BaseLogEntry {
103
111
  * @extends GenericLogEntry
104
112
  */
105
113
  class ConsoleLogEntry extends GenericLogEntry {
106
- constructor(level, text, timeStamp, type, method, realm, args, stackTrace) {
107
- super(level, text, timeStamp, type, stackTrace)
114
+ constructor(level, source, text, timeStamp, type, method, args, stackTrace) {
115
+ super(level, source, text, timeStamp, type, stackTrace)
108
116
  this._method = method
109
- this._realm = realm
110
117
  this._args = args
111
118
  }
112
119
 
@@ -117,15 +124,6 @@ class ConsoleLogEntry extends GenericLogEntry {
117
124
  get method() {
118
125
  return this._method
119
126
  }
120
-
121
- /**
122
- * Gets the realm associated with the log entry.
123
- * @returns {string} The realm associated with the log entry.
124
- */
125
- get realm() {
126
- return this._realm
127
- }
128
-
129
127
  /**
130
128
  * Gets the arguments associated with the log entry.
131
129
  * @returns {Array} The arguments associated with the log entry.
@@ -141,8 +139,8 @@ class ConsoleLogEntry extends GenericLogEntry {
141
139
  * @extends GenericLogEntry
142
140
  */
143
141
  class JavascriptLogEntry extends GenericLogEntry {
144
- constructor(level, text, timeStamp, type, stackTrace) {
145
- super(level, text, timeStamp, type, stackTrace)
142
+ constructor(level, source, text, timeStamp, type, stackTrace) {
143
+ super(level, source, text, timeStamp, type, stackTrace)
146
144
  }
147
145
  }
148
146
 
@@ -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) => {
@@ -70,38 +128,48 @@ class LogInspector {
70
128
  if (params?.type === LOG.TYPE_CONSOLE) {
71
129
  let consoleEntry = new ConsoleLogEntry(
72
130
  params.level,
131
+ params.source,
73
132
  params.text,
74
133
  params.timestamp,
75
134
  params.type,
76
135
  params.method,
77
- params.realm,
78
136
  params.args,
79
137
  params.stackTrace,
80
138
  )
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) => {
@@ -110,6 +178,7 @@ class LogInspector {
110
178
  if (params?.type === LOG.TYPE_JS_LOGS) {
111
179
  let jsEntry = new JavascriptLogEntry(
112
180
  params.level,
181
+ params.source,
113
182
  params.text,
114
183
  params.timestamp,
115
184
  params.type,
@@ -118,59 +187,64 @@ class LogInspector {
118
187
 
119
188
  if (filterBy !== undefined) {
120
189
  if (params?.level === filterBy.getLevel()) {
121
- callback(jsEntry)
190
+ this.invokeCallbacksWithFilter(LOG.TYPE_JS_LOGS_FILTER, jsEntry, filterBy.getLevel())
122
191
  }
123
192
  return
124
193
  }
125
194
 
126
- callback(jsEntry)
195
+ this.invokeCallbacks(LOG.TYPE_JS_LOGS, jsEntry)
127
196
  }
128
197
  })
198
+
199
+ return id
129
200
  }
130
201
 
131
202
  /**
132
203
  * Listen to JS Exceptions
133
204
  * @param callback
134
- * @returns {Promise<void>}
205
+ * @returns {Promise<number>}
135
206
  */
136
207
  async onJavascriptException(callback) {
208
+ const id = this.addCallback(LOG.TYPE_JS_EXCEPTION, callback)
137
209
  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
210
 
145
211
  this.ws.on('message', (event) => {
146
212
  const { params } = JSON.parse(Buffer.from(event.toString()))
147
213
  if (params?.type === 'javascript' && params?.level === 'error') {
148
214
  let jsErrorEntry = new JavascriptLogEntry(
149
215
  params.level,
216
+ params.source,
150
217
  params.text,
151
218
  params.timestamp,
152
219
  params.type,
153
220
  params.stackTrace,
154
221
  )
155
222
 
156
- this.listener[LOG.TYPE_JS_EXCEPTION].forEach((listener) => {
157
- listener(jsErrorEntry)
158
- })
223
+ this.invokeCallbacks(LOG.TYPE_JS_EXCEPTION, jsErrorEntry)
159
224
  }
160
225
  })
226
+
227
+ return id
161
228
  }
162
229
 
163
230
  /**
164
231
  * Listen to any logs
165
232
  * @param callback
166
233
  * @param filterBy
167
- * @returns {Promise<void>}
234
+ * @returns {Promise<number>}
168
235
  */
169
236
  async onLog(callback, filterBy = undefined) {
170
237
  if (filterBy !== undefined && !(filterBy instanceof FilterBy)) {
171
238
  throw Error(`Pass valid FilterBy object. Received: ${filterBy}`)
172
239
  }
173
240
 
241
+ let id
242
+ if (filterBy !== undefined) {
243
+ id = this.addCallback(LOG.TYPE_LOGS_FILTER, { callback: callback, filter: filterBy })
244
+ } else {
245
+ id = this.addCallback(LOG.TYPE_LOGS, callback)
246
+ }
247
+
174
248
  this.ws = await this.bidi.socket
175
249
 
176
250
  this.ws.on('message', (event) => {
@@ -178,6 +252,7 @@ class LogInspector {
178
252
  if (params?.type === 'javascript') {
179
253
  let jsEntry = new JavascriptLogEntry(
180
254
  params.level,
255
+ params.source,
181
256
  params.text,
182
257
  params.timestamp,
183
258
  params.type,
@@ -191,36 +266,46 @@ class LogInspector {
191
266
  return
192
267
  }
193
268
 
194
- callback(jsEntry)
269
+ if (filterBy !== undefined) {
270
+ if (params?.level === filterBy.getLevel()) {
271
+ {
272
+ this.invokeCallbacksWithFilter(LOG.TYPE_LOGS_FILTER, jsEntry, filterBy.getLevel())
273
+ }
274
+ return
275
+ }
276
+ }
277
+
278
+ this.invokeCallbacks(LOG.TYPE_LOGS, jsEntry)
195
279
  return
196
280
  }
197
281
 
198
282
  if (params?.type === 'console') {
199
283
  let consoleEntry = new ConsoleLogEntry(
200
284
  params.level,
285
+ params.source,
201
286
  params.text,
202
287
  params.timestamp,
203
288
  params.type,
204
289
  params.method,
205
- params.realm,
206
290
  params.args,
207
291
  params.stackTrace,
208
292
  )
209
293
 
210
294
  if (filterBy !== undefined) {
211
295
  if (params?.level === filterBy.getLevel()) {
212
- callback(consoleEntry)
296
+ this.invokeCallbacksWithFilter(LOG.TYPE_LOGS_FILTER, consoleEntry, filterBy.getLevel())
213
297
  }
214
298
  return
215
299
  }
216
300
 
217
- callback(consoleEntry)
301
+ this.invokeCallbacks(LOG.TYPE_LOGS, consoleEntry)
218
302
  return
219
303
  }
220
304
 
221
305
  if (params !== undefined && !['console', 'javascript'].includes(params?.type)) {
222
306
  let genericEntry = new GenericLogEntry(
223
307
  params.level,
308
+ params.source,
224
309
  params.text,
225
310
  params.timestamp,
226
311
  params.type,
@@ -229,14 +314,19 @@ class LogInspector {
229
314
 
230
315
  if (filterBy !== undefined) {
231
316
  if (params?.level === filterBy.getLevel()) {
232
- callback(genericEntry)
317
+ {
318
+ this.invokeCallbacksWithFilter(LOG.TYPE_LOGS_FILTER, genericEntry, filterBy.getLevel())
319
+ }
320
+ return
233
321
  }
234
- return
235
322
  }
236
323
 
237
- callback(genericEntry)
324
+ this.invokeCallbacks(LOG.TYPE_LOGS, genericEntry)
325
+ return
238
326
  }
239
327
  })
328
+
329
+ return id
240
330
  }
241
331
 
242
332
  /**
@@ -244,7 +334,15 @@ class LogInspector {
244
334
  * @returns {Promise<void>}
245
335
  */
246
336
  async close() {
247
- await this.bidi.unsubscribe('log.entryAdded', this._browsingContextIds)
337
+ if (
338
+ this._browsingContextIds !== null &&
339
+ this._browsingContextIds !== undefined &&
340
+ this._browsingContextIds.length > 0
341
+ ) {
342
+ await this.bidi.unsubscribe('log.entryAdded', this._browsingContextIds)
343
+ } else {
344
+ await this.bidi.unsubscribe('log.entryAdded')
345
+ }
248
346
  }
249
347
  }
250
348
 
package/bidi/network.js CHANGED
@@ -313,12 +313,26 @@ class Network {
313
313
  * @returns {Promise<void>} A promise that resolves when the network connection is closed.
314
314
  */
315
315
  async close() {
316
- await this.bidi.unsubscribe(
317
- 'network.beforeRequestSent',
318
- 'network.responseStarted',
319
- 'network.responseCompleted',
320
- 'network.authRequired',
321
- )
316
+ if (
317
+ this._browsingContextIds !== null &&
318
+ this._browsingContextIds !== undefined &&
319
+ this._browsingContextIds.length > 0
320
+ ) {
321
+ await this.bidi.unsubscribe(
322
+ 'network.beforeRequestSent',
323
+ 'network.responseStarted',
324
+ 'network.responseCompleted',
325
+ 'network.authRequired',
326
+ this._browsingContextIds,
327
+ )
328
+ } else {
329
+ await this.bidi.unsubscribe(
330
+ 'network.beforeRequestSent',
331
+ 'network.responseStarted',
332
+ 'network.responseCompleted',
333
+ 'network.authRequired',
334
+ )
335
+ }
322
336
  }
323
337
  }
324
338
 
@@ -185,7 +185,11 @@ class LocalValue {
185
185
  return new LocalValue(NonPrimitiveType.CHANNEL, value)
186
186
  }
187
187
 
188
- toJson() {
188
+ static createReferenceValue(handle, sharedId) {
189
+ return new ReferenceValue(handle, sharedId)
190
+ }
191
+
192
+ asMap() {
189
193
  let toReturn = {}
190
194
  toReturn[TYPE_CONSTANT] = this.type
191
195
 
@@ -27,14 +27,58 @@ const { RemoteValue } = require('./protocolValue')
27
27
  const { Source } = require('./scriptTypes')
28
28
  const { WebDriverError } = require('../lib/error')
29
29
 
30
+ const ScriptEvent = {
31
+ MESSAGE: 'script.message',
32
+ REALM_CREATED: 'script.realmCreated',
33
+ REALM_DESTROYED: 'script.realmDestroyed',
34
+ }
35
+
30
36
  /**
31
37
  * Represents class to run events and commands of Script module.
32
38
  * Described in https://w3c.github.io/webdriver-bidi/#module-script.
33
39
  * @class
34
40
  */
35
41
  class ScriptManager {
42
+ #callbackId = 0
43
+ #listener
44
+
36
45
  constructor(driver) {
37
46
  this._driver = driver
47
+ this.#listener = new Map()
48
+ this.#listener.set(ScriptEvent.MESSAGE, new Map())
49
+ this.#listener.set(ScriptEvent.REALM_CREATED, new Map())
50
+ this.#listener.set(ScriptEvent.REALM_DESTROYED, new Map())
51
+ }
52
+
53
+ addCallback(eventType, callback) {
54
+ const id = ++this.#callbackId
55
+
56
+ const eventCallbackMap = this.#listener.get(eventType)
57
+ eventCallbackMap.set(id, callback)
58
+ return id
59
+ }
60
+
61
+ removeCallback(id) {
62
+ let hasId = false
63
+ for (const [, callbacks] of this.#listener) {
64
+ if (callbacks.has(id)) {
65
+ callbacks.delete(id)
66
+ hasId = true
67
+ }
68
+ }
69
+
70
+ if (!hasId) {
71
+ throw Error(`Callback with id ${id} not found`)
72
+ }
73
+ }
74
+
75
+ invokeCallbacks(eventType, data) {
76
+ const callbacks = this.#listener.get(eventType)
77
+ if (callbacks) {
78
+ for (const [, callback] of callbacks) {
79
+ callback(data)
80
+ }
81
+ }
38
82
  }
39
83
 
40
84
  async init(browsingContextIds) {
@@ -251,6 +295,14 @@ class ScriptManager {
251
295
  params.contexts = new Array(this._browsingContextIds)
252
296
  }
253
297
 
298
+ if (argumentValueList != null) {
299
+ let argumentParams = []
300
+ argumentValueList.forEach((argumentValue) => {
301
+ argumentParams.push(argumentValue.asMap())
302
+ })
303
+ params['arguments'] = argumentParams
304
+ }
305
+
254
306
  const command = {
255
307
  method: 'script.addPreloadScript',
256
308
  params,
@@ -433,7 +485,7 @@ class ScriptManager {
433
485
  * @returns {Promise<void>} - A promise that resolves when the subscription is successful.
434
486
  */
435
487
  async onMessage(callback) {
436
- await this.subscribeAndHandleEvent('script.message', callback)
488
+ return await this.subscribeAndHandleEvent(ScriptEvent.MESSAGE, callback)
437
489
  }
438
490
 
439
491
  /**
@@ -443,7 +495,7 @@ class ScriptManager {
443
495
  * @returns {Promise<void>} - A promise that resolves when the subscription is successful.
444
496
  */
445
497
  async onRealmCreated(callback) {
446
- await this.subscribeAndHandleEvent('script.realmCreated', callback)
498
+ return await this.subscribeAndHandleEvent(ScriptEvent.REALM_CREATED, callback)
447
499
  }
448
500
 
449
501
  /**
@@ -453,19 +505,18 @@ class ScriptManager {
453
505
  * @returns {Promise<void>} - A promise that resolves when the subscription is successful.
454
506
  */
455
507
  async onRealmDestroyed(callback) {
456
- await this.subscribeAndHandleEvent('script.realmDestroyed', callback)
508
+ return await this.subscribeAndHandleEvent(ScriptEvent.REALM_DESTROYED, callback)
457
509
  }
458
510
 
459
511
  async subscribeAndHandleEvent(eventType, callback) {
460
- if (this.browsingContextIds != null) {
461
- await this.bidi.subscribe(eventType, this.browsingContextIds)
512
+ if (this._browsingContextIds != null) {
513
+ await this.bidi.subscribe(eventType, this._browsingContextIds)
462
514
  } else {
463
515
  await this.bidi.subscribe(eventType)
464
516
  }
465
- await this._on(callback)
466
- }
467
517
 
468
- async _on(callback) {
518
+ let id = this.addCallback(eventType, callback)
519
+
469
520
  this.ws = await this.bidi.socket
470
521
  this.ws.on('message', (event) => {
471
522
  const { params } = JSON.parse(Buffer.from(event.toString()))
@@ -482,9 +533,28 @@ class ScriptManager {
482
533
  response = params.realm
483
534
  }
484
535
  }
485
- callback(response)
536
+ this.invokeCallbacks(eventType, response)
486
537
  }
487
538
  })
539
+
540
+ return id
541
+ }
542
+
543
+ async close() {
544
+ if (
545
+ this._browsingContextIds !== null &&
546
+ this._browsingContextIds !== undefined &&
547
+ this._browsingContextIds.length > 0
548
+ ) {
549
+ await this.bidi.unsubscribe(
550
+ 'script.message',
551
+ 'script.realmCreated',
552
+ 'script.realmDestroyed',
553
+ this._browsingContextIds,
554
+ )
555
+ } else {
556
+ await this.bidi.unsubscribe('script.message', 'script.realmCreated', 'script.realmDestroyed')
557
+ }
488
558
  }
489
559
  }
490
560
 
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'
@@ -0,0 +1,55 @@
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
+ function observeMutations(channel) {
19
+ const observer = new MutationObserver((mutations) => {
20
+ for (const mutation of mutations) {
21
+ switch (mutation.type) {
22
+ case 'attributes':
23
+ // Don't report our own attribute has changed.
24
+ if (mutation.attributeName === 'data-__webdriver_id') {
25
+ break
26
+ }
27
+ const curr = mutation.target.getAttribute(mutation.attributeName)
28
+ let id = mutation.target.dataset.__webdriver_id
29
+ if (!id) {
30
+ id = Math.random().toString(36).substring(2) + Date.now().toString(36)
31
+ mutation.target.dataset.__webdriver_id = id
32
+ }
33
+ const json = JSON.stringify({
34
+ target: id,
35
+ name: mutation.attributeName,
36
+ value: curr,
37
+ oldValue: mutation.oldValue,
38
+ })
39
+ channel(json)
40
+ break
41
+ default:
42
+ break
43
+ }
44
+ }
45
+ })
46
+
47
+ observer.observe(document, {
48
+ attributes: true,
49
+ attributeOldValue: true,
50
+ characterData: true,
51
+ characterDataOldValue: true,
52
+ childList: true,
53
+ subtree: true,
54
+ })
55
+ }
@@ -20,6 +20,7 @@ const crypto = require('node:crypto')
20
20
  class PinnedScript {
21
21
  constructor(script) {
22
22
  this.scriptSource_ = script
23
+ // eslint-disable-next-line
23
24
  this.scriptHandle_ = crypto.randomUUID().replace(/-/gi, '')
24
25
  }
25
26
 
package/lib/script.js ADDED
@@ -0,0 +1,126 @@
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
+ const scriptManager = require('../bidi//scriptManager')
20
+ const { LocalValue, ChannelValue } = require('../bidi/protocolValue')
21
+ const fs = require('node:fs')
22
+ const path = require('node:path')
23
+ const by = require('./by')
24
+
25
+ class Script {
26
+ #driver
27
+ #logInspector
28
+ #script
29
+
30
+ constructor(driver) {
31
+ this.#driver = driver
32
+ }
33
+
34
+ // This should be done in the constructor.
35
+ // But since it needs to call async methods we cannot do that in the constructor.
36
+ // We can have a separate async method that initialises the Script instance.
37
+ // However, that pattern does not allow chaining the methods as we would like the user to use it.
38
+ // Since it involves awaiting to get the instance and then another await to call the method.
39
+ // Using this allows the user to do this "await driver.script().addJavaScriptErrorHandler(callback)"
40
+ async #init() {
41
+ if (this.#logInspector !== undefined) {
42
+ return
43
+ }
44
+ this.#logInspector = await logInspector(this.#driver)
45
+ }
46
+
47
+ async #initScript() {
48
+ if (this.#script !== undefined) {
49
+ return
50
+ }
51
+ this.#script = await scriptManager([], this.#driver)
52
+ }
53
+
54
+ async addJavaScriptErrorHandler(callback) {
55
+ await this.#init()
56
+ return await this.#logInspector.onJavascriptException(callback)
57
+ }
58
+
59
+ async removeJavaScriptErrorHandler(id) {
60
+ await this.#init()
61
+ await this.#logInspector.removeCallback(id)
62
+ }
63
+
64
+ async addConsoleMessageHandler(callback) {
65
+ await this.#init()
66
+ return this.#logInspector.onConsoleEntry(callback)
67
+ }
68
+
69
+ async removeConsoleMessageHandler(id) {
70
+ await this.#init()
71
+
72
+ await this.#logInspector.removeCallback(id)
73
+ }
74
+
75
+ async addDomMutationHandler(callback) {
76
+ await this.#initScript()
77
+
78
+ let argumentValues = []
79
+ let value = LocalValue.createChannelValue(new ChannelValue('channel_name'))
80
+ argumentValues.push(value)
81
+
82
+ const filePath = path.join(__dirname, 'atoms', 'bidi-mutation-listener.js')
83
+
84
+ let mutationListener = fs.readFileSync(filePath, 'utf-8').toString()
85
+ await this.#script.addPreloadScript(mutationListener, argumentValues)
86
+
87
+ let id = await this.#script.onMessage(async (message) => {
88
+ let payload = JSON.parse(message['data']['value'])
89
+ let elements = await this.#driver.findElements({
90
+ css: '*[data-__webdriver_id=' + by.escapeCss(payload['target']) + ']',
91
+ })
92
+
93
+ if (elements.length === 0) {
94
+ return
95
+ }
96
+
97
+ let event = {
98
+ element: elements[0],
99
+ attribute_name: payload['name'],
100
+ current_value: payload['value'],
101
+ old_value: payload['oldValue'],
102
+ }
103
+ callback(event)
104
+ })
105
+
106
+ return id
107
+ }
108
+
109
+ async removeDomMutationHandler(id) {
110
+ await this.#initScript()
111
+
112
+ await this.#script.removeCallback(id)
113
+ }
114
+
115
+ async pin(script) {
116
+ await this.#initScript()
117
+ return await this.#script.addPreloadScript(script)
118
+ }
119
+
120
+ async unpin(id) {
121
+ await this.#initScript()
122
+ await this.#script.removePreloadScript(id)
123
+ }
124
+ }
125
+
126
+ module.exports = Script
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.21.0",
3
+ "version": "4.23.0",
4
4
  "description": "The official WebDriver JavaScript bindings from the Selenium project",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
@@ -23,33 +23,37 @@
23
23
  "node": ">= 14.21.0"
24
24
  },
25
25
  "dependencies": {
26
+ "@bazel/runfiles": "^5.8.1",
26
27
  "jszip": "^3.10.1",
27
28
  "tmp": "^0.2.3",
28
- "ws": ">=8.16.0"
29
+ "ws": "^8.17.1"
29
30
  },
30
31
  "devDependencies": {
31
- "@bazel/runfiles": "^5.8.1",
32
- "@eslint/js": "^9.1.1",
33
- "eslint": "^9.1.0",
32
+ "@eslint/js": "^9.5.0",
33
+ "clean-jsdoc-theme": "^4.3.0",
34
+ "eslint": "^9.5.0",
34
35
  "eslint-config-prettier": "^9.1.0",
35
36
  "eslint-plugin-mocha": "^10.4.3",
36
- "eslint-plugin-n": "^17.2.1",
37
+ "eslint-plugin-n": "^17.9.0",
37
38
  "eslint-plugin-no-only-tests": "^3.1.0",
38
39
  "eslint-plugin-prettier": "^5.1.3",
39
40
  "express": "^4.19.2",
40
- "globals": "^15.0.0",
41
- "mocha": "^10.4.0",
41
+ "globals": "^15.6.0",
42
+ "has-flag": "^4.0.0",
43
+ "jsdoc": "^4.0.3",
44
+ "mocha": "^10.5.1",
42
45
  "mocha-junit-reporter": "^2.2.1",
43
- "multer": "^1.4.5-lts.1",
44
- "prettier": "^3.2.5",
46
+ "multer": "1.4.5-lts.1",
47
+ "prettier": "^3.3.2",
45
48
  "serve-index": "^1.9.1",
46
- "sinon": "^17.0.1"
49
+ "sinon": "^17.0.1",
50
+ "supports-color": "^9.4.0"
47
51
  },
48
52
  "scripts": {
49
53
  "lint": "eslint .",
50
54
  "lint:fix": "eslint . --fix",
51
- "test": "npm run lint && mocha -t 600000 --recursive test",
52
- "test-jasmine": "bazel test //javascript/node/selenium-webdriver:tests"
55
+ "test": "bazel test //javascript/node/selenium-webdriver/...",
56
+ "generate-docs": "jsdoc --configure jsdoc_conf.json --verbose"
53
57
  },
54
58
  "mocha": {
55
59
  "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'
package/testing/index.js CHANGED
@@ -32,7 +32,10 @@
32
32
 
33
33
  'use strict'
34
34
 
35
+ const fs = require('node:fs')
36
+ const path = require('node:path')
35
37
  const { isatty } = require('node:tty')
38
+ const { runfiles } = require('@bazel/runfiles')
36
39
  const chrome = require('../chrome')
37
40
  const edge = require('../edge')
38
41
  const firefox = require('../firefox')
@@ -288,6 +291,37 @@ class Environment {
288
291
  const urlOrServer = URL_MAP.get(this)
289
292
 
290
293
  const builder = new Builder()
294
+
295
+ // Sniff the environment variables for paths to use for the common browsers
296
+ // Chrome
297
+ if ('SE_CHROMEDRIVER' in process.env) {
298
+ const found = locate(process.env.SE_CHROMEDRIVER)
299
+ const service = new chrome.ServiceBuilder(found)
300
+ builder.setChromeService(service)
301
+ }
302
+ if ('SE_CHROME' in process.env) {
303
+ const binary = locate(process.env.SE_CHROME)
304
+ const options = new chrome.Options()
305
+ options.setChromeBinaryPath(binary)
306
+ options.setAcceptInsecureCerts(true)
307
+ options.addArguments('disable-infobars', 'disable-breakpad', 'disable-dev-shm-usage', 'no-sandbox')
308
+ builder.setChromeOptions(options)
309
+ }
310
+ // Edge
311
+ // Firefox
312
+ if ('SE_GECKODRIVER' in process.env) {
313
+ const found = locate(process.env.SE_GECKODRIVER)
314
+ const service = new firefox.ServiceBuilder(found)
315
+ builder.setFirefoxService(service)
316
+ }
317
+ if ('SE_FIREFOX' in process.env) {
318
+ const binary = locate(process.env.SE_FIREFOX)
319
+ const options = new firefox.Options()
320
+ options.enableBidi()
321
+ options.setBinary(binary)
322
+ builder.setFirefoxOptions(options)
323
+ }
324
+
291
325
  builder.disableEnvironmentOverrides()
292
326
 
293
327
  const realBuild = builder.build
@@ -505,6 +539,59 @@ function getTestHook(name) {
505
539
  return fn
506
540
  }
507
541
 
542
+ function locate(fileLike) {
543
+ if (fs.existsSync(fileLike)) {
544
+ return fileLike
545
+ }
546
+
547
+ try {
548
+ return runfiles.resolve(fileLike)
549
+ } catch {
550
+ // Fall through
551
+ }
552
+
553
+ // Is the item in the workspace?
554
+ try {
555
+ return runfiles.resolveWorkspaceRelative(fileLike)
556
+ } catch {
557
+ // Fall through
558
+ }
559
+
560
+ // Find the repo mapping file
561
+ let repoMappingFile
562
+ try {
563
+ repoMappingFile = runfiles.resolve('_repo_mapping')
564
+ } catch {
565
+ throw new Error('Unable to locate (no repo mapping file): ' + fileLike)
566
+ }
567
+ const lines = fs.readFileSync(repoMappingFile, { encoding: 'utf8' }).split('\n')
568
+
569
+ // Build a map of "repo we declared we need" to "path"
570
+ const mapping = {}
571
+ for (const line of lines) {
572
+ if (line.startsWith(',')) {
573
+ const parts = line.split(',', 3)
574
+ mapping[parts[1]] = parts[2]
575
+ }
576
+ }
577
+
578
+ // Get the first segment of the path
579
+ const pathSegments = fileLike.split('/')
580
+ if (!pathSegments.length) {
581
+ throw new Error('Unable to locate ' + fileLike)
582
+ }
583
+
584
+ pathSegments[0] = mapping[pathSegments[0]] ? mapping[pathSegments[0]] : '_main'
585
+
586
+ try {
587
+ return runfiles.resolve(path.join(...pathSegments))
588
+ } catch {
589
+ // Fall through
590
+ }
591
+
592
+ throw new Error('Unable to find ' + fileLike)
593
+ }
594
+
508
595
  // PUBLIC API
509
596
 
510
597
  module.exports = {