UNPKG

selenium-webdriver

Version:

The official WebDriver JavaScript bindings from the Selenium project

661 lines (584 loc) 20.8 kB
// 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 { InvalidArgumentError, NoSuchFrameError } = require('../lib/error') const { BrowsingContextInfo } = require('./browsingContextTypes') const { SerializationOptions, ReferenceValue, RemoteValue } = require('./protocolValue') const { WebElement } = require('../lib/webdriver') const { CaptureScreenshotParameters } = require('./captureScreenshotParameters') const { CreateContextParameters } = require('./createContextParameters') /** * Represents the locator to locate nodes in the browsing context. * Described in https://w3c.github.io/webdriver-bidi/#type-browsingContext-Locator. */ class Locator { static Type = Object.freeze({ CSS: 'css', INNER_TEXT: 'innerText', XPATH: 'xpath', }) #type #value #ignoreCase #matchType #maxDepth constructor(type, value, ignoreCase = undefined, matchType = undefined, maxDepth = undefined) { this.#type = type this.#value = value this.#ignoreCase = ignoreCase this.#matchType = matchType this.#maxDepth = maxDepth } /** * Creates a new Locator object with CSS selector type. * * @param {string} value - The CSS selector value. * @returns {Locator} A new Locator object with CSS selector type. */ static css(value) { return new Locator(Locator.Type.CSS, value) } /** * Creates a new Locator object with the given XPath value. * * @param {string} value - The XPath value. * @returns {Locator} A new Locator object. */ static xpath(value) { return new Locator(Locator.Type.XPATH, value) } /** * Creates a new Locator object with the specified inner text value. * * @param {string} value - The inner text value to locate. * @param {boolean|undefined} [ignoreCase] - Whether to ignore the case when matching the inner text value. * @param {string|undefined} [matchType] - The type of matching to perform (full or partial). * @param {number|undefined} [maxDepth] - The maximum depth to search for the inner text value. * @returns {Locator} A new Locator object with the specified inner text value. */ static innerText(value, ignoreCase = undefined, matchType = undefined, maxDepth = undefined) { return new Locator(Locator.Type.INNER_TEXT, value, ignoreCase, matchType, maxDepth) } toMap() { const map = new Map() map.set('type', this.#type.toString()) map.set('value', this.#value) map.set('ignoreCase', this.#ignoreCase) map.set('matchType', this.#matchType) map.set('maxDepth', this.#maxDepth) return map } } /** * Represents the contains under BrowsingContext module commands. * Described in https://w3c.github.io/webdriver-bidi/#module-browsingContext * Each browsing context command requires a browsing context id. * Hence, this class represent browsing context lifecycle. */ class BrowsingContext { constructor(driver) { this._driver = driver } /** * @returns id */ get id() { return this._id } async init({ browsingContextId = undefined, type = undefined, createParameters = undefined }) { if (!(await this._driver.getCapabilities()).get('webSocketUrl')) { throw Error('WebDriver instance must support BiDi protocol') } if (browsingContextId === undefined && type === undefined && createParameters === undefined) { throw Error('Either BrowsingContextId or Type or CreateParameters must be provided') } if (type === undefined && createParameters !== undefined) { throw Error('Type must be provided with CreateParameters') } if (type !== undefined && !['window', 'tab'].includes(type)) { throw Error(`Valid types are 'window' & 'tab'. Received: ${type}`) } this.bidi = await this._driver.getBidi() this._id = browsingContextId === undefined ? (await this.create(type, createParameters))['result']['context'] : browsingContextId } /** * Creates a browsing context for the given type with the given parameters */ async create(type, createParameters = undefined) { if (createParameters !== undefined && (!createParameters) instanceof CreateContextParameters) { throw Error(`Pass in the instance of CreateContextParameters. Received: ${createParameters}`) } let parameters = new Map() parameters.set('type', type) if (createParameters !== undefined) { createParameters.asMap().forEach((value, key) => { parameters.set(key, value) }) } const params = { method: 'browsingContext.create', params: Object.fromEntries(parameters), } return await this.bidi.send(params) } /** * @param url the url to navigate to * @param readinessState type of readiness state: "none" / "interactive" / "complete" * @returns NavigateResult object */ async navigate(url, readinessState = undefined) { if (readinessState !== undefined && !['none', 'interactive', 'complete'].includes(readinessState)) { throw Error(`Valid readiness states are 'none', 'interactive' & 'complete'. Received: ${readinessState}`) } const params = { method: 'browsingContext.navigate', params: { context: this._id, url: url, wait: readinessState, }, } const navigateResult = (await this.bidi.send(params))['result'] return new NavigateResult(navigateResult['url'], navigateResult['navigation']) } /** * @param maxDepth the max depth of the descendents of browsing context tree * @returns BrowsingContextInfo object */ async getTree(maxDepth = undefined) { const params = { method: 'browsingContext.getTree', params: { root: this._id, maxDepth: maxDepth, }, } let result = await this.bidi.send(params) if ('error' in result) { throw Error(result['error']) } result = result['result']['contexts'][0] return new BrowsingContextInfo(result['context'], result['url'], result['children'], result['parent']) } /** * @returns {Promise<Array<BrowsingContextInfo>>} A Promise that resolves to an array of BrowsingContextInfo objects representing the top-level browsing contexts. */ async getTopLevelContexts() { const params = { method: 'browsingContext.getTree', params: {}, } let result = await this.bidi.send(params) if ('error' in result) { throw Error(result['error']) } const contexts = result['result']['contexts'] const browsingContexts = contexts.map((context) => { return new BrowsingContextInfo(context['id'], context['url'], context['children'], context['parent']) }) return browsingContexts } /** * Closes the browsing context * @returns {Promise<void>} */ async close() { const params = { method: 'browsingContext.close', params: { context: this._id, }, } await this.bidi.send(params) } /** * Prints PDF of the webpage * @param options print options given by the user * @returns PrintResult object */ async printPage(options = {}) { let params = { method: 'browsingContext.print', // Setting default values for parameters params: { context: this._id, background: false, margin: { bottom: 1.0, left: 1.0, right: 1.0, top: 1.0, }, orientation: 'portrait', page: { height: 27.94, width: 21.59, }, pageRanges: [], scale: 1.0, shrinkToFit: true, }, } // Updating parameter values based on the options passed params.params = this._driver.validatePrintPageParams(options, params.params) const response = await this.bidi.send(params) return new PrintResult(response.result.data) } /** * Captures a screenshot of the browsing context. * * @param {CaptureScreenshotParameters|undefined} [captureScreenshotParameters] - Optional parameters for capturing the screenshot. * @returns {Promise<string>} - A promise that resolves to the base64-encoded string representation of the captured screenshot. * @throws {InvalidArgumentError} - If the provided captureScreenshotParameters is not an instance of CaptureScreenshotParameters. */ async captureScreenshot(captureScreenshotParameters = undefined) { if ( captureScreenshotParameters !== undefined && !(captureScreenshotParameters instanceof CaptureScreenshotParameters) ) { throw new InvalidArgumentError( `Pass in a CaptureScreenshotParameters object. Received: ${captureScreenshotParameters}`, ) } const screenshotParams = new Map() screenshotParams.set('context', this._id) if (captureScreenshotParameters !== undefined) { captureScreenshotParameters.asMap().forEach((value, key) => { screenshotParams.set(key, value) }) } let params = { method: 'browsingContext.captureScreenshot', params: Object.fromEntries(screenshotParams), } const response = await this.bidi.send(params) this.checkErrorInScreenshot(response) return response['result']['data'] } async captureBoxScreenshot(x, y, width, height) { let params = { method: 'browsingContext.captureScreenshot', params: { context: this._id, clip: { type: 'box', x: x, y: y, width: width, height: height, }, }, } const response = await this.bidi.send(params) this.checkErrorInScreenshot(response) return response['result']['data'] } /** * Captures a screenshot of a specific element within the browsing context. * @param {string} sharedId - The shared ID of the element to capture. * @param {string} [handle] - The handle of the element to capture (optional). * @returns {Promise<string>} A promise that resolves to the base64-encoded screenshot data. */ async captureElementScreenshot(sharedId, handle = undefined) { let params = { method: 'browsingContext.captureScreenshot', params: { context: this._id, clip: { type: 'element', element: { sharedId: sharedId, handle: handle, }, }, }, } const response = await this.bidi.send(params) this.checkErrorInScreenshot(response) return response['result']['data'] } checkErrorInScreenshot(response) { if ('error' in response) { const { error, msg } = response switch (error) { case 'invalid argument': throw new InvalidArgumentError(msg) case 'no such frame': throw new NoSuchFrameError(msg) } } } /** * Activates and focuses the top-level browsing context. * @returns {Promise<void>} A promise that resolves when the browsing context is activated. * @throws {Error} If there is an error while activating the browsing context. */ async activate() { const params = { method: 'browsingContext.activate', params: { context: this._id, }, } let result = await this.bidi.send(params) if ('error' in result) { throw Error(result['error']) } } /** * Handles a user prompt in the browsing context. * * @param {boolean} [accept] - Optional. Indicates whether to accept or dismiss the prompt. * @param {string} [userText] - Optional. The text to enter. * @throws {Error} If an error occurs while handling the user prompt. */ async handleUserPrompt(accept = undefined, userText = undefined) { const params = { method: 'browsingContext.handleUserPrompt', params: { context: this._id, accept: accept, userText: userText, }, } let result = await this.bidi.send(params) if ('error' in result) { throw Error(result['error']) } } /** * Reloads the current browsing context. * * @param {boolean} [ignoreCache] - Whether to ignore the cache when reloading. * @param {string} [readinessState] - The readiness state to wait for before returning. * Valid readiness states are 'none', 'interactive', and 'complete'. * @returns {Promise<NavigateResult>} - A promise that resolves to the result of the reload operation. * @throws {Error} - If an invalid readiness state is provided. */ async reload(ignoreCache = undefined, readinessState = undefined) { if (readinessState !== undefined && !['none', 'interactive', 'complete'].includes(readinessState)) { throw Error(`Valid readiness states are 'none', 'interactive' & 'complete'. Received: ${readinessState}`) } const params = { method: 'browsingContext.reload', params: { context: this._id, ignoreCache: ignoreCache, wait: readinessState, }, } const navigateResult = (await this.bidi.send(params))['result'] return new NavigateResult(navigateResult['url'], navigateResult['navigation']) } /** * Sets the viewport size and device pixel ratio for the browsing context. * @param {number} width - The width of the viewport. * @param {number} height - The height of the viewport. * @param {number} [devicePixelRatio] - The device pixel ratio (optional) * @throws {Error} If an error occurs while setting the viewport. */ async setViewport(width, height, devicePixelRatio = undefined) { const params = { method: 'browsingContext.setViewport', params: { context: this._id, viewport: { width: width, height: height }, devicePixelRatio: devicePixelRatio, }, } let result = await this.bidi.send(params) if ('error' in result) { throw Error(result['error']) } } /** * Traverses the browsing context history by a given delta. * * @param {number} delta - The delta value to traverse the history. A positive value moves forward, while a negative value moves backward. * @returns {Promise<void>} - A promise that resolves when the history traversal is complete. */ async traverseHistory(delta) { const params = { method: 'browsingContext.traverseHistory', params: { context: this._id, delta: delta, }, } await this.bidi.send(params) } /** * Moves the browsing context forward by one step in the history. * @returns {Promise<void>} A promise that resolves when the browsing context has moved forward. */ async forward() { await this.traverseHistory(1) } /** * Navigates the browsing context to the previous page in the history. * @returns {Promise<void>} A promise that resolves when the navigation is complete. */ async back() { await this.traverseHistory(-1) } /** * Locates nodes in the browsing context. * * @param {Locator} locator - The locator object used to locate the nodes. * @param {number} [maxNodeCount] - The maximum number of nodes to locate (optional). * @param {string} [sandbox] - The sandbox name for locating nodes (optional). * @param {SerializationOptions} [serializationOptions] - The serialization options for locating nodes (optional). * @param {ReferenceValue[]} [startNodes] - The array of start nodes for locating nodes (optional). * @returns {Promise<RemoteValue[]>} - A promise that resolves to the arrays of located nodes. * @throws {Error} - If the locator is not an instance of Locator. * @throws {Error} - If the serializationOptions is provided but not an instance of SerializationOptions. * @throws {Error} - If the startNodes is provided but not an array of ReferenceValue objects. * @throws {Error} - If any of the startNodes is not an instance of ReferenceValue. */ async locateNodes( locator, maxNodeCount = undefined, sandbox = undefined, serializationOptions = undefined, startNodes = undefined, ) { if (!(locator instanceof Locator)) { throw Error(`Pass in a Locator object. Received: ${locator}`) } if (serializationOptions !== undefined && !(serializationOptions instanceof SerializationOptions)) { throw Error(`Pass in SerializationOptions object. Received: ${serializationOptions} `) } if (startNodes !== undefined && !Array.isArray(startNodes)) { throw Error(`Pass in an array of ReferenceValue objects. Received: ${startNodes}`) } let startNodesSerialized = undefined if (startNodes !== undefined && Array.isArray(startNodes)) { startNodesSerialized = [] startNodes.forEach((node) => { if (!(node instanceof ReferenceValue)) { throw Error(`Pass in a ReferenceValue object. Received: ${node}`) } else { startNodesSerialized.push(node.asMap()) } }) } const params = { method: 'browsingContext.locateNodes', params: { context: this._id, locator: Object.fromEntries(locator.toMap()), maxNodeCount: maxNodeCount, sandbox: sandbox, serializationOptions: serializationOptions, startNodes: startNodesSerialized, }, } let response = await this.bidi.send(params) if ('error' in response) { throw Error(response['error']) } const nodes = response.result.nodes const remoteValues = [] nodes.forEach((node) => { remoteValues.push(new RemoteValue(node)) }) return remoteValues } /** * Locates a single node in the browsing context. * * @param {Locator} locator - The locator used to find the node. * @param {string} [sandbox] - The sandbox of the node (optional). * @param {SerializationOptions} [serializationOptions] - The serialization options for the node (optional). * @param {Array} [startNodes] - The starting nodes for the search (optional). * @returns {Promise<RemoteValue>} - A promise that resolves to the located node. */ async locateNode(locator, sandbox = undefined, serializationOptions = undefined, startNodes = undefined) { const elements = await this.locateNodes(locator, 1, sandbox, serializationOptions, startNodes) return elements[0] } async locateElement(locator) { const elements = await this.locateNodes(locator, 1) return new WebElement(this._driver, elements[0].sharedId) } async locateElements(locator) { const elements = await this.locateNodes(locator) let webElements = [] elements.forEach((element) => { webElements.push(new WebElement(this._driver, element.sharedId)) }) return webElements } } /** * Represents the result of a navigation operation. */ class NavigateResult { constructor(url, navigationId) { this._url = url this._navigationId = navigationId } /** * Gets the URL of the navigated page. * @returns {string} The URL of the navigated page. */ get url() { return this._url } /** * Gets the ID of the navigation operation. * @returns {number} The ID of the navigation operation. */ get navigationId() { return this._navigationId } } /** * Represents a print result. */ class PrintResult { constructor(data) { this._data = data } /** * Gets the data associated with the print result. * @returns {any} The data associated with the print result. */ get data() { return this._data } } /** * initiate browsing context instance and return * @param driver * @param browsingContextId The browsing context of current window/tab * @param type "window" or "tab" * @param createParameters The parameters for creating a new browsing context * @returns {Promise<BrowsingContext>} */ async function getBrowsingContextInstance( driver, { browsingContextId = undefined, type = undefined, createParameters = undefined }, ) { let instance = new BrowsingContext(driver) await instance.init({ browsingContextId, type, createParameters }) return instance } module.exports = getBrowsingContextInstance module.exports.Locator = Locator