Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
seleniumhq
GitHub Repository: seleniumhq/selenium
Path: blob/trunk/javascript/selenium-webdriver/bidi/scriptManager.js
2884 views
1
// Licensed to the Software Freedom Conservancy (SFC) under one
2
// or more contributor license agreements. See the NOTICE file
3
// distributed with this work for additional information
4
// regarding copyright ownership. The SFC licenses this file
5
// to you under the Apache License, Version 2.0 (the
6
// "License"); you may not use this file except in compliance
7
// with the License. You may obtain a copy of the License at
8
//
9
// http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing,
12
// software distributed under the License is distributed on an
13
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
// KIND, either express or implied. See the License for the
15
// specific language governing permissions and limitations
16
// under the License.
17
18
const {
19
EvaluateResultType,
20
EvaluateResultSuccess,
21
EvaluateResultException,
22
ExceptionDetails,
23
} = require('./evaluateResult')
24
const { Message } = require('./scriptTypes')
25
const { RealmInfo, RealmType, WindowRealmInfo } = require('./realmInfo')
26
const { RemoteValue } = require('./protocolValue')
27
const { Source } = require('./scriptTypes')
28
const { WebDriverError } = require('../lib/error')
29
30
const ScriptEvent = {
31
MESSAGE: 'script.message',
32
REALM_CREATED: 'script.realmCreated',
33
REALM_DESTROYED: 'script.realmDestroyed',
34
}
35
36
/**
37
* Represents class to run events and commands of Script module.
38
* Described in https://w3c.github.io/webdriver-bidi/#module-script.
39
* @class
40
*/
41
class ScriptManager {
42
#callbackId = 0
43
#listener
44
45
constructor(driver) {
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
}
82
}
83
84
async init(browsingContextIds) {
85
if (!(await this._driver.getCapabilities()).get('webSocketUrl')) {
86
throw Error('WebDriver instance must support BiDi protocol')
87
}
88
89
this.bidi = await this._driver.getBidi()
90
this._browsingContextIds = browsingContextIds
91
}
92
93
/**
94
* Disowns the handles in the specified realm.
95
*
96
* @param {string} realmId - The ID of the realm.
97
* @param {string[]} handles - The handles to disown to allow garbage collection.
98
* @returns {Promise<void>} - A promise that resolves when the command is sent.
99
*/
100
async disownRealmScript(realmId, handles) {
101
const params = {
102
method: 'script.disown',
103
params: {
104
handles: handles,
105
target: {
106
realm: realmId,
107
},
108
},
109
}
110
111
await this.bidi.send(params)
112
}
113
114
/**
115
* Disowns the handles in the specified browsing context.
116
* @param {string} browsingContextId - The ID of the browsing context.
117
* @param {string[]} handles - The handles to disown to allow garbage collection.
118
* @param {String|null} [sandbox=null] - The sandbox name.
119
* @returns {Promise<void>} - A promise that resolves when the command is sent.
120
*/
121
async disownBrowsingContextScript(browsingContextId, handles, sandbox = null) {
122
const params = {
123
method: 'script.disown',
124
params: {
125
handles: handles,
126
target: {
127
context: browsingContextId,
128
},
129
},
130
}
131
132
if (sandbox != null) {
133
params.params.target['sandbox'] = sandbox
134
}
135
136
await this.bidi.send(params)
137
}
138
139
/**
140
* Calls a function in the specified realm.
141
*
142
* @param {string} realmId - The ID of the realm.
143
* @param {string} functionDeclaration - The function to call.
144
* @param {boolean} awaitPromise - Whether to await the promise returned by the function.
145
* @param {LocalValue[]} [argumentValueList|null] - The list of argument values to pass to the function.
146
* @param {Object} [thisParameter|null] - The value of 'this' parameter for the function.
147
* @param {ResultOwnership} [resultOwnership|null] - The ownership of the result.
148
* @returns {Promise<EvaluateResultSuccess|EvaluateResultException>} - A promise that resolves to the evaluation result or exception.
149
*/
150
async callFunctionInRealm(
151
realmId,
152
functionDeclaration,
153
awaitPromise,
154
argumentValueList = null,
155
thisParameter = null,
156
resultOwnership = null,
157
) {
158
const params = this.getCallFunctionParams(
159
'realm',
160
realmId,
161
null,
162
functionDeclaration,
163
awaitPromise,
164
argumentValueList,
165
thisParameter,
166
resultOwnership,
167
)
168
169
const command = {
170
method: 'script.callFunction',
171
params,
172
}
173
174
let response = await this.bidi.send(command)
175
return this.createEvaluateResult(response)
176
}
177
178
/**
179
* Calls a function in the specified browsing context.
180
*
181
* @param {string} realmId - The ID of the browsing context.
182
* @param {string} functionDeclaration - The function to call.
183
* @param {boolean} awaitPromise - Whether to await the promise returned by the function.
184
* @param {LocalValue[]} [argumentValueList|null] - The list of argument values to pass to the function.
185
* @param {Object} [thisParameter|null] - The value of 'this' parameter for the function.
186
* @param {ResultOwnership} [resultOwnership|null] - The ownership of the result.
187
* @returns {Promise<EvaluateResultSuccess|EvaluateResultException>} - A promise that resolves to the evaluation result or exception.
188
*/
189
async callFunctionInBrowsingContext(
190
browsingContextId,
191
functionDeclaration,
192
awaitPromise,
193
argumentValueList = null,
194
thisParameter = null,
195
resultOwnership = null,
196
sandbox = null,
197
) {
198
const params = this.getCallFunctionParams(
199
'contextTarget',
200
browsingContextId,
201
sandbox,
202
functionDeclaration,
203
awaitPromise,
204
argumentValueList,
205
thisParameter,
206
resultOwnership,
207
)
208
209
const command = {
210
method: 'script.callFunction',
211
params,
212
}
213
const response = await this.bidi.send(command)
214
return this.createEvaluateResult(response)
215
}
216
217
/**
218
* Evaluates a function in the specified realm.
219
*
220
* @param {string} realmId - The ID of the realm.
221
* @param {string} expression - The expression to function to evaluate.
222
* @param {boolean} awaitPromise - Whether to await the promise.
223
* @param {ResultOwnership|null} resultOwnership - The ownership of the result.
224
* @returns {Promise<EvaluateResultSuccess|EvaluateResultException>} - A promise that resolves to the evaluation result or exception.
225
*/
226
async evaluateFunctionInRealm(realmId, expression, awaitPromise, resultOwnership = null) {
227
const params = this.getEvaluateParams('realm', realmId, null, expression, awaitPromise, resultOwnership)
228
229
const command = {
230
method: 'script.evaluate',
231
params,
232
}
233
234
let response = await this.bidi.send(command)
235
return this.createEvaluateResult(response)
236
}
237
238
/**
239
* Evaluates a function in the browsing context.
240
*
241
* @param {string} realmId - The ID of the browsing context.
242
* @param {string} expression - The expression to function to evaluate.
243
* @param {boolean} awaitPromise - Whether to await the promise.
244
* @param {ResultOwnership|null} resultOwnership - The ownership of the result.
245
* @returns {Promise<EvaluateResultSuccess|EvaluateResultException>} - A promise that resolves to the evaluation result or exception.
246
*/
247
async evaluateFunctionInBrowsingContext(
248
browsingContextId,
249
expression,
250
awaitPromise,
251
resultOwnership = null,
252
sandbox = null,
253
) {
254
const params = this.getEvaluateParams(
255
'contextTarget',
256
browsingContextId,
257
sandbox,
258
expression,
259
awaitPromise,
260
resultOwnership,
261
)
262
263
const command = {
264
method: 'script.evaluate',
265
params,
266
}
267
268
let response = await this.bidi.send(command)
269
return this.createEvaluateResult(response)
270
}
271
272
/**
273
* Adds a preload script.
274
*
275
* @param {string} functionDeclaration - The declaration of the function to be added as a preload script.
276
* @param {LocalValue[]} [argumentValueList=[]] - The list of argument values to be passed to the preload script function.
277
* @param {string} [sandbox|null] - The sandbox object to be used for the preload script.
278
* @returns {Promise<number>} - A promise that resolves to the added preload script ID.
279
*/
280
async addPreloadScript(functionDeclaration, argumentValueList = [], sandbox = null) {
281
const params = {
282
functionDeclaration: functionDeclaration,
283
arguments: argumentValueList,
284
}
285
286
if (sandbox !== null) {
287
params.sandbox = sandbox
288
}
289
290
if (Array.isArray(this._browsingContextIds) && this._browsingContextIds.length > 0) {
291
params.contexts = this._browsingContextIds
292
}
293
294
if (typeof this._browsingContextIds === 'string') {
295
params.contexts = new Array(this._browsingContextIds)
296
}
297
298
if (argumentValueList != null) {
299
let argumentParams = []
300
argumentValueList.forEach((argumentValue) => {
301
argumentParams.push(argumentValue.asMap())
302
})
303
params['arguments'] = argumentParams
304
}
305
306
const command = {
307
method: 'script.addPreloadScript',
308
params,
309
}
310
311
let response = await this.bidi.send(command)
312
return response.result.script
313
}
314
315
/**
316
* Removes a preload script.
317
*
318
* @param {string} script - The ID for the script to be removed.
319
* @returns {Promise<any>} - A promise that resolves with the result of the removal.
320
* @throws {WebDriverError} - If an error occurs during the removal process.
321
*/
322
async removePreloadScript(script) {
323
const params = { script: script }
324
const command = {
325
method: 'script.removePreloadScript',
326
params,
327
}
328
let response = await this.bidi.send(command)
329
if ('error' in response) {
330
throw new WebDriverError(response.error)
331
}
332
return response.result
333
}
334
335
getCallFunctionParams(
336
targetType,
337
id,
338
sandbox,
339
functionDeclaration,
340
awaitPromise,
341
argumentValueList = null,
342
thisParameter = null,
343
resultOwnership = null,
344
) {
345
const params = {
346
functionDeclaration: functionDeclaration,
347
awaitPromise: awaitPromise,
348
}
349
if (targetType === 'contextTarget') {
350
if (sandbox != null) {
351
params['target'] = { context: id, sandbox: sandbox }
352
} else {
353
params['target'] = { context: id }
354
}
355
} else {
356
params['target'] = { realm: id }
357
}
358
359
if (argumentValueList != null) {
360
let argumentParams = []
361
argumentValueList.forEach((argumentValue) => {
362
argumentParams.push(argumentValue.asMap())
363
})
364
params['arguments'] = argumentParams
365
}
366
367
if (thisParameter != null) {
368
params['this'] = thisParameter
369
}
370
371
if (resultOwnership != null) {
372
params['resultOwnership'] = resultOwnership
373
}
374
375
return params
376
}
377
378
getEvaluateParams(targetType, id, sandbox, expression, awaitPromise, resultOwnership = null) {
379
const params = {
380
expression: expression,
381
awaitPromise: awaitPromise,
382
}
383
if (targetType === 'contextTarget') {
384
if (sandbox != null) {
385
params['target'] = { context: id, sandbox: sandbox }
386
} else {
387
params['target'] = { context: id }
388
}
389
} else {
390
params['target'] = { realm: id }
391
}
392
if (resultOwnership != null) {
393
params['resultOwnership'] = resultOwnership
394
}
395
396
return params
397
}
398
399
createEvaluateResult(response) {
400
const type = response.result.type
401
const realmId = response.result.realm
402
let evaluateResult
403
404
if (type === EvaluateResultType.SUCCESS) {
405
const result = response.result.result
406
evaluateResult = new EvaluateResultSuccess(realmId, new RemoteValue(result))
407
} else {
408
const exceptionDetails = response.result.exceptionDetails
409
evaluateResult = new EvaluateResultException(realmId, new ExceptionDetails(exceptionDetails))
410
}
411
return evaluateResult
412
}
413
414
realmInfoMapper(realms) {
415
const realmsList = []
416
realms.forEach((realm) => {
417
realmsList.push(RealmInfo.fromJson(realm))
418
})
419
return realmsList
420
}
421
422
/**
423
* Retrieves all realms.
424
* @returns {Promise<RealmInfo[]>} - A promise that resolves to an array of RealmInfo objects.
425
*/
426
async getAllRealms() {
427
const command = {
428
method: 'script.getRealms',
429
params: {},
430
}
431
let response = await this.bidi.send(command)
432
return this.realmInfoMapper(response.result.realms)
433
}
434
435
/**
436
* Retrieves the realms by type.
437
*
438
* @param {Type} type - The type of realms to retrieve.
439
* @returns {Promise<RealmInfo[]>} - A promise that resolves to an array of RealmInfo objects.
440
*/
441
async getRealmsByType(type) {
442
const command = {
443
method: 'script.getRealms',
444
params: { type: type },
445
}
446
let response = await this.bidi.send(command)
447
return this.realmInfoMapper(response.result.realms)
448
}
449
450
/**
451
* Retrieves the realms in the specified browsing context.
452
*
453
* @param {string} browsingContext - The browsing context ID.
454
* @returns {Promise<RealmInfo[]>} - A promise that resolves to an array of RealmInfo objects.
455
*/
456
async getRealmsInBrowsingContext(browsingContext) {
457
const command = {
458
method: 'script.getRealms',
459
params: { context: browsingContext },
460
}
461
let response = await this.bidi.send(command)
462
return this.realmInfoMapper(response.result.realms)
463
}
464
465
/**
466
* Retrieves the realms in a browsing context based on the specified type.
467
*
468
* @param {string} browsingContext - The browsing context ID.
469
* @param {string} type - The type of realms to retrieve.
470
* @returns {Promise<RealmInfo[]>} - A promise that resolves to an array of RealmInfo objects.
471
*/
472
async getRealmsInBrowsingContextByType(browsingContext, type) {
473
const command = {
474
method: 'script.getRealms',
475
params: { context: browsingContext, type: type },
476
}
477
let response = await this.bidi.send(command)
478
return this.realmInfoMapper(response.result.realms)
479
}
480
481
/**
482
* Subscribes to the 'script.message' event and handles the callback function when a message is received.
483
*
484
* @param {Function} callback - The callback function to be executed when a message is received.
485
* @returns {Promise<void>} - A promise that resolves when the subscription is successful.
486
*/
487
async onMessage(callback) {
488
return await this.subscribeAndHandleEvent(ScriptEvent.MESSAGE, callback)
489
}
490
491
/**
492
* Subscribes to the 'script.realmCreated' event and handles it with the provided callback.
493
*
494
* @param {Function} callback - The callback function to handle the 'script.realmCreated' event.
495
* @returns {Promise<void>} - A promise that resolves when the subscription is successful.
496
*/
497
async onRealmCreated(callback) {
498
return await this.subscribeAndHandleEvent(ScriptEvent.REALM_CREATED, callback)
499
}
500
501
/**
502
* Subscribes to the 'script.realmDestroyed' event and handles it with the provided callback function.
503
*
504
* @param {Function} callback - The callback function to be executed when the 'script.realmDestroyed' event occurs.
505
* @returns {Promise<void>} - A promise that resolves when the subscription is successful.
506
*/
507
async onRealmDestroyed(callback) {
508
return await this.subscribeAndHandleEvent(ScriptEvent.REALM_DESTROYED, callback)
509
}
510
511
async subscribeAndHandleEvent(eventType, callback) {
512
if (this._browsingContextIds != null) {
513
await this.bidi.subscribe(eventType, this._browsingContextIds)
514
} else {
515
await this.bidi.subscribe(eventType)
516
}
517
518
let id = this.addCallback(eventType, callback)
519
520
this.ws = await this.bidi.socket
521
this.ws.on('message', (event) => {
522
const { params } = JSON.parse(Buffer.from(event.toString()))
523
if (params) {
524
let response = null
525
if ('channel' in params) {
526
response = new Message(params.channel, new RemoteValue(params.data), new Source(params.source))
527
} else if ('realm' in params) {
528
if (params.type === RealmType.WINDOW) {
529
response = new WindowRealmInfo(params.realm, params.origin, params.type, params.context, params.sandbox)
530
} else if (params.realm !== null && params.type !== null) {
531
response = new RealmInfo(params.realm, params.origin, params.type)
532
} else if (params.realm !== null) {
533
response = params.realm
534
}
535
}
536
this.invokeCallbacks(eventType, response)
537
}
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
}
558
}
559
}
560
561
async function getScriptManagerInstance(browsingContextId, driver) {
562
let instance = new ScriptManager(driver)
563
await instance.init(browsingContextId)
564
return instance
565
}
566
567
module.exports = getScriptManagerInstance
568
569