Path: blob/trunk/javascript/selenium-webdriver/bidi/scriptManager.js
2884 views
// Licensed to the Software Freedom Conservancy (SFC) under one1// or more contributor license agreements. See the NOTICE file2// distributed with this work for additional information3// regarding copyright ownership. The SFC licenses this file4// to you under the Apache License, Version 2.0 (the5// "License"); you may not use this file except in compliance6// with the License. You may obtain a copy of the License at7//8// http://www.apache.org/licenses/LICENSE-2.09//10// Unless required by applicable law or agreed to in writing,11// software distributed under the License is distributed on an12// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY13// KIND, either express or implied. See the License for the14// specific language governing permissions and limitations15// under the License.1617const {18EvaluateResultType,19EvaluateResultSuccess,20EvaluateResultException,21ExceptionDetails,22} = require('./evaluateResult')23const { Message } = require('./scriptTypes')24const { RealmInfo, RealmType, WindowRealmInfo } = require('./realmInfo')25const { RemoteValue } = require('./protocolValue')26const { Source } = require('./scriptTypes')27const { WebDriverError } = require('../lib/error')2829const ScriptEvent = {30MESSAGE: 'script.message',31REALM_CREATED: 'script.realmCreated',32REALM_DESTROYED: 'script.realmDestroyed',33}3435/**36* Represents class to run events and commands of Script module.37* Described in https://w3c.github.io/webdriver-bidi/#module-script.38* @class39*/40class ScriptManager {41#callbackId = 042#listener4344constructor(driver) {45this._driver = driver46this.#listener = new Map()47this.#listener.set(ScriptEvent.MESSAGE, new Map())48this.#listener.set(ScriptEvent.REALM_CREATED, new Map())49this.#listener.set(ScriptEvent.REALM_DESTROYED, new Map())50}5152addCallback(eventType, callback) {53const id = ++this.#callbackId5455const eventCallbackMap = this.#listener.get(eventType)56eventCallbackMap.set(id, callback)57return id58}5960removeCallback(id) {61let hasId = false62for (const [, callbacks] of this.#listener) {63if (callbacks.has(id)) {64callbacks.delete(id)65hasId = true66}67}6869if (!hasId) {70throw Error(`Callback with id ${id} not found`)71}72}7374invokeCallbacks(eventType, data) {75const callbacks = this.#listener.get(eventType)76if (callbacks) {77for (const [, callback] of callbacks) {78callback(data)79}80}81}8283async init(browsingContextIds) {84if (!(await this._driver.getCapabilities()).get('webSocketUrl')) {85throw Error('WebDriver instance must support BiDi protocol')86}8788this.bidi = await this._driver.getBidi()89this._browsingContextIds = browsingContextIds90}9192/**93* Disowns the handles in the specified realm.94*95* @param {string} realmId - The ID of the realm.96* @param {string[]} handles - The handles to disown to allow garbage collection.97* @returns {Promise<void>} - A promise that resolves when the command is sent.98*/99async disownRealmScript(realmId, handles) {100const params = {101method: 'script.disown',102params: {103handles: handles,104target: {105realm: realmId,106},107},108}109110await this.bidi.send(params)111}112113/**114* Disowns the handles in the specified browsing context.115* @param {string} browsingContextId - The ID of the browsing context.116* @param {string[]} handles - The handles to disown to allow garbage collection.117* @param {String|null} [sandbox=null] - The sandbox name.118* @returns {Promise<void>} - A promise that resolves when the command is sent.119*/120async disownBrowsingContextScript(browsingContextId, handles, sandbox = null) {121const params = {122method: 'script.disown',123params: {124handles: handles,125target: {126context: browsingContextId,127},128},129}130131if (sandbox != null) {132params.params.target['sandbox'] = sandbox133}134135await this.bidi.send(params)136}137138/**139* Calls a function in the specified realm.140*141* @param {string} realmId - The ID of the realm.142* @param {string} functionDeclaration - The function to call.143* @param {boolean} awaitPromise - Whether to await the promise returned by the function.144* @param {LocalValue[]} [argumentValueList|null] - The list of argument values to pass to the function.145* @param {Object} [thisParameter|null] - The value of 'this' parameter for the function.146* @param {ResultOwnership} [resultOwnership|null] - The ownership of the result.147* @returns {Promise<EvaluateResultSuccess|EvaluateResultException>} - A promise that resolves to the evaluation result or exception.148*/149async callFunctionInRealm(150realmId,151functionDeclaration,152awaitPromise,153argumentValueList = null,154thisParameter = null,155resultOwnership = null,156) {157const params = this.getCallFunctionParams(158'realm',159realmId,160null,161functionDeclaration,162awaitPromise,163argumentValueList,164thisParameter,165resultOwnership,166)167168const command = {169method: 'script.callFunction',170params,171}172173let response = await this.bidi.send(command)174return this.createEvaluateResult(response)175}176177/**178* Calls a function in the specified browsing context.179*180* @param {string} realmId - The ID of the browsing context.181* @param {string} functionDeclaration - The function to call.182* @param {boolean} awaitPromise - Whether to await the promise returned by the function.183* @param {LocalValue[]} [argumentValueList|null] - The list of argument values to pass to the function.184* @param {Object} [thisParameter|null] - The value of 'this' parameter for the function.185* @param {ResultOwnership} [resultOwnership|null] - The ownership of the result.186* @returns {Promise<EvaluateResultSuccess|EvaluateResultException>} - A promise that resolves to the evaluation result or exception.187*/188async callFunctionInBrowsingContext(189browsingContextId,190functionDeclaration,191awaitPromise,192argumentValueList = null,193thisParameter = null,194resultOwnership = null,195sandbox = null,196) {197const params = this.getCallFunctionParams(198'contextTarget',199browsingContextId,200sandbox,201functionDeclaration,202awaitPromise,203argumentValueList,204thisParameter,205resultOwnership,206)207208const command = {209method: 'script.callFunction',210params,211}212const response = await this.bidi.send(command)213return this.createEvaluateResult(response)214}215216/**217* Evaluates a function in the specified realm.218*219* @param {string} realmId - The ID of the realm.220* @param {string} expression - The expression to function to evaluate.221* @param {boolean} awaitPromise - Whether to await the promise.222* @param {ResultOwnership|null} resultOwnership - The ownership of the result.223* @returns {Promise<EvaluateResultSuccess|EvaluateResultException>} - A promise that resolves to the evaluation result or exception.224*/225async evaluateFunctionInRealm(realmId, expression, awaitPromise, resultOwnership = null) {226const params = this.getEvaluateParams('realm', realmId, null, expression, awaitPromise, resultOwnership)227228const command = {229method: 'script.evaluate',230params,231}232233let response = await this.bidi.send(command)234return this.createEvaluateResult(response)235}236237/**238* Evaluates a function in the browsing context.239*240* @param {string} realmId - The ID of the browsing context.241* @param {string} expression - The expression to function to evaluate.242* @param {boolean} awaitPromise - Whether to await the promise.243* @param {ResultOwnership|null} resultOwnership - The ownership of the result.244* @returns {Promise<EvaluateResultSuccess|EvaluateResultException>} - A promise that resolves to the evaluation result or exception.245*/246async evaluateFunctionInBrowsingContext(247browsingContextId,248expression,249awaitPromise,250resultOwnership = null,251sandbox = null,252) {253const params = this.getEvaluateParams(254'contextTarget',255browsingContextId,256sandbox,257expression,258awaitPromise,259resultOwnership,260)261262const command = {263method: 'script.evaluate',264params,265}266267let response = await this.bidi.send(command)268return this.createEvaluateResult(response)269}270271/**272* Adds a preload script.273*274* @param {string} functionDeclaration - The declaration of the function to be added as a preload script.275* @param {LocalValue[]} [argumentValueList=[]] - The list of argument values to be passed to the preload script function.276* @param {string} [sandbox|null] - The sandbox object to be used for the preload script.277* @returns {Promise<number>} - A promise that resolves to the added preload script ID.278*/279async addPreloadScript(functionDeclaration, argumentValueList = [], sandbox = null) {280const params = {281functionDeclaration: functionDeclaration,282arguments: argumentValueList,283}284285if (sandbox !== null) {286params.sandbox = sandbox287}288289if (Array.isArray(this._browsingContextIds) && this._browsingContextIds.length > 0) {290params.contexts = this._browsingContextIds291}292293if (typeof this._browsingContextIds === 'string') {294params.contexts = new Array(this._browsingContextIds)295}296297if (argumentValueList != null) {298let argumentParams = []299argumentValueList.forEach((argumentValue) => {300argumentParams.push(argumentValue.asMap())301})302params['arguments'] = argumentParams303}304305const command = {306method: 'script.addPreloadScript',307params,308}309310let response = await this.bidi.send(command)311return response.result.script312}313314/**315* Removes a preload script.316*317* @param {string} script - The ID for the script to be removed.318* @returns {Promise<any>} - A promise that resolves with the result of the removal.319* @throws {WebDriverError} - If an error occurs during the removal process.320*/321async removePreloadScript(script) {322const params = { script: script }323const command = {324method: 'script.removePreloadScript',325params,326}327let response = await this.bidi.send(command)328if ('error' in response) {329throw new WebDriverError(response.error)330}331return response.result332}333334getCallFunctionParams(335targetType,336id,337sandbox,338functionDeclaration,339awaitPromise,340argumentValueList = null,341thisParameter = null,342resultOwnership = null,343) {344const params = {345functionDeclaration: functionDeclaration,346awaitPromise: awaitPromise,347}348if (targetType === 'contextTarget') {349if (sandbox != null) {350params['target'] = { context: id, sandbox: sandbox }351} else {352params['target'] = { context: id }353}354} else {355params['target'] = { realm: id }356}357358if (argumentValueList != null) {359let argumentParams = []360argumentValueList.forEach((argumentValue) => {361argumentParams.push(argumentValue.asMap())362})363params['arguments'] = argumentParams364}365366if (thisParameter != null) {367params['this'] = thisParameter368}369370if (resultOwnership != null) {371params['resultOwnership'] = resultOwnership372}373374return params375}376377getEvaluateParams(targetType, id, sandbox, expression, awaitPromise, resultOwnership = null) {378const params = {379expression: expression,380awaitPromise: awaitPromise,381}382if (targetType === 'contextTarget') {383if (sandbox != null) {384params['target'] = { context: id, sandbox: sandbox }385} else {386params['target'] = { context: id }387}388} else {389params['target'] = { realm: id }390}391if (resultOwnership != null) {392params['resultOwnership'] = resultOwnership393}394395return params396}397398createEvaluateResult(response) {399const type = response.result.type400const realmId = response.result.realm401let evaluateResult402403if (type === EvaluateResultType.SUCCESS) {404const result = response.result.result405evaluateResult = new EvaluateResultSuccess(realmId, new RemoteValue(result))406} else {407const exceptionDetails = response.result.exceptionDetails408evaluateResult = new EvaluateResultException(realmId, new ExceptionDetails(exceptionDetails))409}410return evaluateResult411}412413realmInfoMapper(realms) {414const realmsList = []415realms.forEach((realm) => {416realmsList.push(RealmInfo.fromJson(realm))417})418return realmsList419}420421/**422* Retrieves all realms.423* @returns {Promise<RealmInfo[]>} - A promise that resolves to an array of RealmInfo objects.424*/425async getAllRealms() {426const command = {427method: 'script.getRealms',428params: {},429}430let response = await this.bidi.send(command)431return this.realmInfoMapper(response.result.realms)432}433434/**435* Retrieves the realms by type.436*437* @param {Type} type - The type of realms to retrieve.438* @returns {Promise<RealmInfo[]>} - A promise that resolves to an array of RealmInfo objects.439*/440async getRealmsByType(type) {441const command = {442method: 'script.getRealms',443params: { type: type },444}445let response = await this.bidi.send(command)446return this.realmInfoMapper(response.result.realms)447}448449/**450* Retrieves the realms in the specified browsing context.451*452* @param {string} browsingContext - The browsing context ID.453* @returns {Promise<RealmInfo[]>} - A promise that resolves to an array of RealmInfo objects.454*/455async getRealmsInBrowsingContext(browsingContext) {456const command = {457method: 'script.getRealms',458params: { context: browsingContext },459}460let response = await this.bidi.send(command)461return this.realmInfoMapper(response.result.realms)462}463464/**465* Retrieves the realms in a browsing context based on the specified type.466*467* @param {string} browsingContext - The browsing context ID.468* @param {string} type - The type of realms to retrieve.469* @returns {Promise<RealmInfo[]>} - A promise that resolves to an array of RealmInfo objects.470*/471async getRealmsInBrowsingContextByType(browsingContext, type) {472const command = {473method: 'script.getRealms',474params: { context: browsingContext, type: type },475}476let response = await this.bidi.send(command)477return this.realmInfoMapper(response.result.realms)478}479480/**481* Subscribes to the 'script.message' event and handles the callback function when a message is received.482*483* @param {Function} callback - The callback function to be executed when a message is received.484* @returns {Promise<void>} - A promise that resolves when the subscription is successful.485*/486async onMessage(callback) {487return await this.subscribeAndHandleEvent(ScriptEvent.MESSAGE, callback)488}489490/**491* Subscribes to the 'script.realmCreated' event and handles it with the provided callback.492*493* @param {Function} callback - The callback function to handle the 'script.realmCreated' event.494* @returns {Promise<void>} - A promise that resolves when the subscription is successful.495*/496async onRealmCreated(callback) {497return await this.subscribeAndHandleEvent(ScriptEvent.REALM_CREATED, callback)498}499500/**501* Subscribes to the 'script.realmDestroyed' event and handles it with the provided callback function.502*503* @param {Function} callback - The callback function to be executed when the 'script.realmDestroyed' event occurs.504* @returns {Promise<void>} - A promise that resolves when the subscription is successful.505*/506async onRealmDestroyed(callback) {507return await this.subscribeAndHandleEvent(ScriptEvent.REALM_DESTROYED, callback)508}509510async subscribeAndHandleEvent(eventType, callback) {511if (this._browsingContextIds != null) {512await this.bidi.subscribe(eventType, this._browsingContextIds)513} else {514await this.bidi.subscribe(eventType)515}516517let id = this.addCallback(eventType, callback)518519this.ws = await this.bidi.socket520this.ws.on('message', (event) => {521const { params } = JSON.parse(Buffer.from(event.toString()))522if (params) {523let response = null524if ('channel' in params) {525response = new Message(params.channel, new RemoteValue(params.data), new Source(params.source))526} else if ('realm' in params) {527if (params.type === RealmType.WINDOW) {528response = new WindowRealmInfo(params.realm, params.origin, params.type, params.context, params.sandbox)529} else if (params.realm !== null && params.type !== null) {530response = new RealmInfo(params.realm, params.origin, params.type)531} else if (params.realm !== null) {532response = params.realm533}534}535this.invokeCallbacks(eventType, response)536}537})538539return id540}541542async close() {543if (544this._browsingContextIds !== null &&545this._browsingContextIds !== undefined &&546this._browsingContextIds.length > 0547) {548await this.bidi.unsubscribe(549'script.message',550'script.realmCreated',551'script.realmDestroyed',552this._browsingContextIds,553)554} else {555await this.bidi.unsubscribe('script.message', 'script.realmCreated', 'script.realmDestroyed')556}557}558}559560async function getScriptManagerInstance(browsingContextId, driver) {561let instance = new ScriptManager(driver)562await instance.init(browsingContextId)563return instance564}565566module.exports = getScriptManagerInstance567568569