661 lines
20 KiB
JavaScript
661 lines
20 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 { 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
|