568 lines
17 KiB
JavaScript
568 lines
17 KiB
JavaScript
// Licensed to the Software Freedom Conservancy (SFC) under one
|
|
// or more contributor license agreements. See the NOTICE file
|
|
// distributed with this work for additional information
|
|
// regarding copyright ownership. The SFC licenses this file
|
|
// to you under the Apache License, Version 2.0 (the
|
|
// "License"); you may not use this file except in compliance
|
|
// with the License. You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing,
|
|
// software distributed under the License is distributed on an
|
|
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
// KIND, either express or implied. See the License for the
|
|
// specific language governing permissions and limitations
|
|
// under the License.
|
|
|
|
const {
|
|
EvaluateResultType,
|
|
EvaluateResultSuccess,
|
|
EvaluateResultException,
|
|
ExceptionDetails,
|
|
} = require('./evaluateResult')
|
|
const { Message } = require('./scriptTypes')
|
|
const { RealmInfo, RealmType, WindowRealmInfo } = require('./realmInfo')
|
|
const { RemoteValue } = require('./protocolValue')
|
|
const { Source } = require('./scriptTypes')
|
|
const { WebDriverError } = require('../lib/error')
|
|
|
|
const ScriptEvent = {
|
|
MESSAGE: 'script.message',
|
|
REALM_CREATED: 'script.realmCreated',
|
|
REALM_DESTROYED: 'script.realmDestroyed',
|
|
}
|
|
|
|
/**
|
|
* Represents class to run events and commands of Script module.
|
|
* Described in https://w3c.github.io/webdriver-bidi/#module-script.
|
|
* @class
|
|
*/
|
|
class ScriptManager {
|
|
#callbackId = 0
|
|
#listener
|
|
|
|
constructor(driver) {
|
|
this._driver = driver
|
|
this.#listener = new Map()
|
|
this.#listener.set(ScriptEvent.MESSAGE, new Map())
|
|
this.#listener.set(ScriptEvent.REALM_CREATED, new Map())
|
|
this.#listener.set(ScriptEvent.REALM_DESTROYED, new Map())
|
|
}
|
|
|
|
addCallback(eventType, callback) {
|
|
const id = ++this.#callbackId
|
|
|
|
const eventCallbackMap = this.#listener.get(eventType)
|
|
eventCallbackMap.set(id, callback)
|
|
return id
|
|
}
|
|
|
|
removeCallback(id) {
|
|
let hasId = false
|
|
for (const [, callbacks] of this.#listener) {
|
|
if (callbacks.has(id)) {
|
|
callbacks.delete(id)
|
|
hasId = true
|
|
}
|
|
}
|
|
|
|
if (!hasId) {
|
|
throw Error(`Callback with id ${id} not found`)
|
|
}
|
|
}
|
|
|
|
invokeCallbacks(eventType, data) {
|
|
const callbacks = this.#listener.get(eventType)
|
|
if (callbacks) {
|
|
for (const [, callback] of callbacks) {
|
|
callback(data)
|
|
}
|
|
}
|
|
}
|
|
|
|
async init(browsingContextIds) {
|
|
if (!(await this._driver.getCapabilities()).get('webSocketUrl')) {
|
|
throw Error('WebDriver instance must support BiDi protocol')
|
|
}
|
|
|
|
this.bidi = await this._driver.getBidi()
|
|
this._browsingContextIds = browsingContextIds
|
|
}
|
|
|
|
/**
|
|
* Disowns the handles in the specified realm.
|
|
*
|
|
* @param {string} realmId - The ID of the realm.
|
|
* @param {string[]} handles - The handles to disown to allow garbage collection.
|
|
* @returns {Promise<void>} - A promise that resolves when the command is sent.
|
|
*/
|
|
async disownRealmScript(realmId, handles) {
|
|
const params = {
|
|
method: 'script.disown',
|
|
params: {
|
|
handles: handles,
|
|
target: {
|
|
realm: realmId,
|
|
},
|
|
},
|
|
}
|
|
|
|
await this.bidi.send(params)
|
|
}
|
|
|
|
/**
|
|
* Disowns the handles in the specified browsing context.
|
|
* @param {string} browsingContextId - The ID of the browsing context.
|
|
* @param {string[]} handles - The handles to disown to allow garbage collection.
|
|
* @param {String|null} [sandbox=null] - The sandbox name.
|
|
* @returns {Promise<void>} - A promise that resolves when the command is sent.
|
|
*/
|
|
async disownBrowsingContextScript(browsingContextId, handles, sandbox = null) {
|
|
const params = {
|
|
method: 'script.disown',
|
|
params: {
|
|
handles: handles,
|
|
target: {
|
|
context: browsingContextId,
|
|
},
|
|
},
|
|
}
|
|
|
|
if (sandbox != null) {
|
|
params.params.target['sandbox'] = sandbox
|
|
}
|
|
|
|
await this.bidi.send(params)
|
|
}
|
|
|
|
/**
|
|
* Calls a function in the specified realm.
|
|
*
|
|
* @param {string} realmId - The ID of the realm.
|
|
* @param {string} functionDeclaration - The function to call.
|
|
* @param {boolean} awaitPromise - Whether to await the promise returned by the function.
|
|
* @param {LocalValue[]} [argumentValueList|null] - The list of argument values to pass to the function.
|
|
* @param {Object} [thisParameter|null] - The value of 'this' parameter for the function.
|
|
* @param {ResultOwnership} [resultOwnership|null] - The ownership of the result.
|
|
* @returns {Promise<EvaluateResultSuccess|EvaluateResultException>} - A promise that resolves to the evaluation result or exception.
|
|
*/
|
|
async callFunctionInRealm(
|
|
realmId,
|
|
functionDeclaration,
|
|
awaitPromise,
|
|
argumentValueList = null,
|
|
thisParameter = null,
|
|
resultOwnership = null,
|
|
) {
|
|
const params = this.getCallFunctionParams(
|
|
'realm',
|
|
realmId,
|
|
null,
|
|
functionDeclaration,
|
|
awaitPromise,
|
|
argumentValueList,
|
|
thisParameter,
|
|
resultOwnership,
|
|
)
|
|
|
|
const command = {
|
|
method: 'script.callFunction',
|
|
params,
|
|
}
|
|
|
|
let response = await this.bidi.send(command)
|
|
return this.createEvaluateResult(response)
|
|
}
|
|
|
|
/**
|
|
* Calls a function in the specified browsing context.
|
|
*
|
|
* @param {string} realmId - The ID of the browsing context.
|
|
* @param {string} functionDeclaration - The function to call.
|
|
* @param {boolean} awaitPromise - Whether to await the promise returned by the function.
|
|
* @param {LocalValue[]} [argumentValueList|null] - The list of argument values to pass to the function.
|
|
* @param {Object} [thisParameter|null] - The value of 'this' parameter for the function.
|
|
* @param {ResultOwnership} [resultOwnership|null] - The ownership of the result.
|
|
* @returns {Promise<EvaluateResultSuccess|EvaluateResultException>} - A promise that resolves to the evaluation result or exception.
|
|
*/
|
|
async callFunctionInBrowsingContext(
|
|
browsingContextId,
|
|
functionDeclaration,
|
|
awaitPromise,
|
|
argumentValueList = null,
|
|
thisParameter = null,
|
|
resultOwnership = null,
|
|
sandbox = null,
|
|
) {
|
|
const params = this.getCallFunctionParams(
|
|
'contextTarget',
|
|
browsingContextId,
|
|
sandbox,
|
|
functionDeclaration,
|
|
awaitPromise,
|
|
argumentValueList,
|
|
thisParameter,
|
|
resultOwnership,
|
|
)
|
|
|
|
const command = {
|
|
method: 'script.callFunction',
|
|
params,
|
|
}
|
|
const response = await this.bidi.send(command)
|
|
return this.createEvaluateResult(response)
|
|
}
|
|
|
|
/**
|
|
* Evaluates a function in the specified realm.
|
|
*
|
|
* @param {string} realmId - The ID of the realm.
|
|
* @param {string} expression - The expression to function to evaluate.
|
|
* @param {boolean} awaitPromise - Whether to await the promise.
|
|
* @param {ResultOwnership|null} resultOwnership - The ownership of the result.
|
|
* @returns {Promise<EvaluateResultSuccess|EvaluateResultException>} - A promise that resolves to the evaluation result or exception.
|
|
*/
|
|
async evaluateFunctionInRealm(realmId, expression, awaitPromise, resultOwnership = null) {
|
|
const params = this.getEvaluateParams('realm', realmId, null, expression, awaitPromise, resultOwnership)
|
|
|
|
const command = {
|
|
method: 'script.evaluate',
|
|
params,
|
|
}
|
|
|
|
let response = await this.bidi.send(command)
|
|
return this.createEvaluateResult(response)
|
|
}
|
|
|
|
/**
|
|
* Evaluates a function in the browsing context.
|
|
*
|
|
* @param {string} realmId - The ID of the browsing context.
|
|
* @param {string} expression - The expression to function to evaluate.
|
|
* @param {boolean} awaitPromise - Whether to await the promise.
|
|
* @param {ResultOwnership|null} resultOwnership - The ownership of the result.
|
|
* @returns {Promise<EvaluateResultSuccess|EvaluateResultException>} - A promise that resolves to the evaluation result or exception.
|
|
*/
|
|
async evaluateFunctionInBrowsingContext(
|
|
browsingContextId,
|
|
expression,
|
|
awaitPromise,
|
|
resultOwnership = null,
|
|
sandbox = null,
|
|
) {
|
|
const params = this.getEvaluateParams(
|
|
'contextTarget',
|
|
browsingContextId,
|
|
sandbox,
|
|
expression,
|
|
awaitPromise,
|
|
resultOwnership,
|
|
)
|
|
|
|
const command = {
|
|
method: 'script.evaluate',
|
|
params,
|
|
}
|
|
|
|
let response = await this.bidi.send(command)
|
|
return this.createEvaluateResult(response)
|
|
}
|
|
|
|
/**
|
|
* Adds a preload script.
|
|
*
|
|
* @param {string} functionDeclaration - The declaration of the function to be added as a preload script.
|
|
* @param {LocalValue[]} [argumentValueList=[]] - The list of argument values to be passed to the preload script function.
|
|
* @param {string} [sandbox|null] - The sandbox object to be used for the preload script.
|
|
* @returns {Promise<number>} - A promise that resolves to the added preload script ID.
|
|
*/
|
|
async addPreloadScript(functionDeclaration, argumentValueList = [], sandbox = null) {
|
|
const params = {
|
|
functionDeclaration: functionDeclaration,
|
|
arguments: argumentValueList,
|
|
}
|
|
|
|
if (sandbox !== null) {
|
|
params.sandbox = sandbox
|
|
}
|
|
|
|
if (Array.isArray(this._browsingContextIds) && this._browsingContextIds.length > 0) {
|
|
params.contexts = this._browsingContextIds
|
|
}
|
|
|
|
if (typeof this._browsingContextIds === 'string') {
|
|
params.contexts = new Array(this._browsingContextIds)
|
|
}
|
|
|
|
if (argumentValueList != null) {
|
|
let argumentParams = []
|
|
argumentValueList.forEach((argumentValue) => {
|
|
argumentParams.push(argumentValue.asMap())
|
|
})
|
|
params['arguments'] = argumentParams
|
|
}
|
|
|
|
const command = {
|
|
method: 'script.addPreloadScript',
|
|
params,
|
|
}
|
|
|
|
let response = await this.bidi.send(command)
|
|
return response.result.script
|
|
}
|
|
|
|
/**
|
|
* Removes a preload script.
|
|
*
|
|
* @param {string} script - The ID for the script to be removed.
|
|
* @returns {Promise<any>} - A promise that resolves with the result of the removal.
|
|
* @throws {WebDriverError} - If an error occurs during the removal process.
|
|
*/
|
|
async removePreloadScript(script) {
|
|
const params = { script: script }
|
|
const command = {
|
|
method: 'script.removePreloadScript',
|
|
params,
|
|
}
|
|
let response = await this.bidi.send(command)
|
|
if ('error' in response) {
|
|
throw new WebDriverError(response.error)
|
|
}
|
|
return response.result
|
|
}
|
|
|
|
getCallFunctionParams(
|
|
targetType,
|
|
id,
|
|
sandbox,
|
|
functionDeclaration,
|
|
awaitPromise,
|
|
argumentValueList = null,
|
|
thisParameter = null,
|
|
resultOwnership = null,
|
|
) {
|
|
const params = {
|
|
functionDeclaration: functionDeclaration,
|
|
awaitPromise: awaitPromise,
|
|
}
|
|
if (targetType === 'contextTarget') {
|
|
if (sandbox != null) {
|
|
params['target'] = { context: id, sandbox: sandbox }
|
|
} else {
|
|
params['target'] = { context: id }
|
|
}
|
|
} else {
|
|
params['target'] = { realm: id }
|
|
}
|
|
|
|
if (argumentValueList != null) {
|
|
let argumentParams = []
|
|
argumentValueList.forEach((argumentValue) => {
|
|
argumentParams.push(argumentValue.asMap())
|
|
})
|
|
params['arguments'] = argumentParams
|
|
}
|
|
|
|
if (thisParameter != null) {
|
|
params['this'] = thisParameter
|
|
}
|
|
|
|
if (resultOwnership != null) {
|
|
params['resultOwnership'] = resultOwnership
|
|
}
|
|
|
|
return params
|
|
}
|
|
|
|
getEvaluateParams(targetType, id, sandbox, expression, awaitPromise, resultOwnership = null) {
|
|
const params = {
|
|
expression: expression,
|
|
awaitPromise: awaitPromise,
|
|
}
|
|
if (targetType === 'contextTarget') {
|
|
if (sandbox != null) {
|
|
params['target'] = { context: id, sandbox: sandbox }
|
|
} else {
|
|
params['target'] = { context: id }
|
|
}
|
|
} else {
|
|
params['target'] = { realm: id }
|
|
}
|
|
if (resultOwnership != null) {
|
|
params['resultOwnership'] = resultOwnership
|
|
}
|
|
|
|
return params
|
|
}
|
|
|
|
createEvaluateResult(response) {
|
|
const type = response.result.type
|
|
const realmId = response.result.realm
|
|
let evaluateResult
|
|
|
|
if (type === EvaluateResultType.SUCCESS) {
|
|
const result = response.result.result
|
|
evaluateResult = new EvaluateResultSuccess(realmId, new RemoteValue(result))
|
|
} else {
|
|
const exceptionDetails = response.result.exceptionDetails
|
|
evaluateResult = new EvaluateResultException(realmId, new ExceptionDetails(exceptionDetails))
|
|
}
|
|
return evaluateResult
|
|
}
|
|
|
|
realmInfoMapper(realms) {
|
|
const realmsList = []
|
|
realms.forEach((realm) => {
|
|
realmsList.push(RealmInfo.fromJson(realm))
|
|
})
|
|
return realmsList
|
|
}
|
|
|
|
/**
|
|
* Retrieves all realms.
|
|
* @returns {Promise<RealmInfo[]>} - A promise that resolves to an array of RealmInfo objects.
|
|
*/
|
|
async getAllRealms() {
|
|
const command = {
|
|
method: 'script.getRealms',
|
|
params: {},
|
|
}
|
|
let response = await this.bidi.send(command)
|
|
return this.realmInfoMapper(response.result.realms)
|
|
}
|
|
|
|
/**
|
|
* Retrieves the realms by type.
|
|
*
|
|
* @param {Type} type - The type of realms to retrieve.
|
|
* @returns {Promise<RealmInfo[]>} - A promise that resolves to an array of RealmInfo objects.
|
|
*/
|
|
async getRealmsByType(type) {
|
|
const command = {
|
|
method: 'script.getRealms',
|
|
params: { type: type },
|
|
}
|
|
let response = await this.bidi.send(command)
|
|
return this.realmInfoMapper(response.result.realms)
|
|
}
|
|
|
|
/**
|
|
* Retrieves the realms in the specified browsing context.
|
|
*
|
|
* @param {string} browsingContext - The browsing context ID.
|
|
* @returns {Promise<RealmInfo[]>} - A promise that resolves to an array of RealmInfo objects.
|
|
*/
|
|
async getRealmsInBrowsingContext(browsingContext) {
|
|
const command = {
|
|
method: 'script.getRealms',
|
|
params: { context: browsingContext },
|
|
}
|
|
let response = await this.bidi.send(command)
|
|
return this.realmInfoMapper(response.result.realms)
|
|
}
|
|
|
|
/**
|
|
* Retrieves the realms in a browsing context based on the specified type.
|
|
*
|
|
* @param {string} browsingContext - The browsing context ID.
|
|
* @param {string} type - The type of realms to retrieve.
|
|
* @returns {Promise<RealmInfo[]>} - A promise that resolves to an array of RealmInfo objects.
|
|
*/
|
|
async getRealmsInBrowsingContextByType(browsingContext, type) {
|
|
const command = {
|
|
method: 'script.getRealms',
|
|
params: { context: browsingContext, type: type },
|
|
}
|
|
let response = await this.bidi.send(command)
|
|
return this.realmInfoMapper(response.result.realms)
|
|
}
|
|
|
|
/**
|
|
* Subscribes to the 'script.message' event and handles the callback function when a message is received.
|
|
*
|
|
* @param {Function} callback - The callback function to be executed when a message is received.
|
|
* @returns {Promise<void>} - A promise that resolves when the subscription is successful.
|
|
*/
|
|
async onMessage(callback) {
|
|
return await this.subscribeAndHandleEvent(ScriptEvent.MESSAGE, callback)
|
|
}
|
|
|
|
/**
|
|
* Subscribes to the 'script.realmCreated' event and handles it with the provided callback.
|
|
*
|
|
* @param {Function} callback - The callback function to handle the 'script.realmCreated' event.
|
|
* @returns {Promise<void>} - A promise that resolves when the subscription is successful.
|
|
*/
|
|
async onRealmCreated(callback) {
|
|
return await this.subscribeAndHandleEvent(ScriptEvent.REALM_CREATED, callback)
|
|
}
|
|
|
|
/**
|
|
* Subscribes to the 'script.realmDestroyed' event and handles it with the provided callback function.
|
|
*
|
|
* @param {Function} callback - The callback function to be executed when the 'script.realmDestroyed' event occurs.
|
|
* @returns {Promise<void>} - A promise that resolves when the subscription is successful.
|
|
*/
|
|
async onRealmDestroyed(callback) {
|
|
return await this.subscribeAndHandleEvent(ScriptEvent.REALM_DESTROYED, callback)
|
|
}
|
|
|
|
async subscribeAndHandleEvent(eventType, callback) {
|
|
if (this._browsingContextIds != null) {
|
|
await this.bidi.subscribe(eventType, this._browsingContextIds)
|
|
} else {
|
|
await this.bidi.subscribe(eventType)
|
|
}
|
|
|
|
let id = this.addCallback(eventType, callback)
|
|
|
|
this.ws = await this.bidi.socket
|
|
this.ws.on('message', (event) => {
|
|
const { params } = JSON.parse(Buffer.from(event.toString()))
|
|
if (params) {
|
|
let response = null
|
|
if ('channel' in params) {
|
|
response = new Message(params.channel, new RemoteValue(params.data), new Source(params.source))
|
|
} else if ('realm' in params) {
|
|
if (params.type === RealmType.WINDOW) {
|
|
response = new WindowRealmInfo(params.realm, params.origin, params.type, params.context, params.sandbox)
|
|
} else if (params.realm !== null && params.type !== null) {
|
|
response = new RealmInfo(params.realm, params.origin, params.type)
|
|
} else if (params.realm !== null) {
|
|
response = params.realm
|
|
}
|
|
}
|
|
this.invokeCallbacks(eventType, response)
|
|
}
|
|
})
|
|
|
|
return id
|
|
}
|
|
|
|
async close() {
|
|
if (
|
|
this._browsingContextIds !== null &&
|
|
this._browsingContextIds !== undefined &&
|
|
this._browsingContextIds.length > 0
|
|
) {
|
|
await this.bidi.unsubscribe(
|
|
'script.message',
|
|
'script.realmCreated',
|
|
'script.realmDestroyed',
|
|
this._browsingContextIds,
|
|
)
|
|
} else {
|
|
await this.bidi.unsubscribe('script.message', 'script.realmCreated', 'script.realmDestroyed')
|
|
}
|
|
}
|
|
}
|
|
|
|
async function getScriptManagerInstance(browsingContextId, driver) {
|
|
let instance = new ScriptManager(driver)
|
|
await instance.init(browsingContextId)
|
|
return instance
|
|
}
|
|
|
|
module.exports = getScriptManagerInstance
|