| // 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 |