Added files for Juan

This commit is contained in:
2024-08-07 11:38:48 -07:00
parent f7878e3e60
commit 795d1d72ce
2871 changed files with 621552 additions and 0 deletions

View File

@@ -0,0 +1,106 @@
// 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 { UrlPattern } = require('./urlPattern')
class AddInterceptParameters {
#phases = []
#urlPatterns = []
constructor(phases) {
if (phases instanceof Array) {
phases.forEach((phase) => this.#phases.push(phase))
} else {
this.#phases.push(phases)
}
}
/**
* Adds a URL pattern to intercept.
*
* @param {UrlPattern} pattern - The URL pattern to add.
* @returns {AddInterceptParameters} - Returns the current instance of the class AddInterceptParameters for chaining.
* @throws {Error} - Throws an error if the pattern is not an instance of UrlPattern.
*/
urlPattern(pattern) {
if (!(pattern instanceof UrlPattern)) {
throw new Error(`Pattern must be an instance of UrlPattern. Received: '${pattern})'`)
}
this.#urlPatterns.push(Object.fromEntries(pattern.asMap()))
return this
}
/**
* Adds array of URL patterns to intercept.
*
* @param {UrlPattern[]} patterns - An array of UrlPattern instances representing the URL patterns to intercept.
* @returns {AddInterceptParameters} - Returns the instance of AddInterceptParameters for chaining.
* @throws {Error} - Throws an error if the pattern is not an instance of UrlPattern.
*/
urlPatterns(patterns) {
patterns.forEach((pattern) => {
if (!(pattern instanceof UrlPattern)) {
throw new Error(`Pattern must be an instance of UrlPattern. Received:'${pattern}'`)
}
this.#urlPatterns.push(Object.fromEntries(pattern.asMap()))
})
return this
}
/**
* Adds string URL to intercept.
*
* @param {string} pattern - The URL pattern to be added.
* @returns {AddInterceptParameters} - Returns the instance of AddInterceptParameters for chaining..
* @throws {Error} - If the pattern is not an instance of String.
*/
urlStringPattern(pattern) {
if (typeof pattern !== 'string') {
throw new Error(`Pattern must be an instance of String. Received:'${pattern}'`)
}
this.#urlPatterns.push({ type: 'string', pattern: pattern })
return this
}
/**
* Adds array of string URLs to intercept.
* @param {string[]} patterns - An array of URL string patterns.
* @returns {this} - Returns the instance of AddInterceptParameters for chaining.
*/
urlStringPatterns(patterns) {
patterns.forEach((pattern) => {
if (typeof pattern !== 'string') {
throw new Error(`Pattern must be an instance of String. Received:'${pattern}'`)
}
this.#urlPatterns.push({ type: 'string', pattern: pattern })
})
return this
}
asMap() {
const map = new Map()
map.set('phases', this.#phases)
if (this.#urlPatterns.length > 0) {
map.set('urlPatterns', this.#urlPatterns)
}
return map
}
}
module.exports = { AddInterceptParameters }

View File

@@ -0,0 +1,35 @@
// 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 { LocalValue } = require('./protocolValue')
class ArgumentValue {
constructor(value) {
this.value = value
}
asMap() {
if (this.value instanceof LocalValue) {
return this.value.toJson()
} else {
// ReferenceValue
return this.value.asMap()
}
}
}
module.exports = { ArgumentValue }

View File

@@ -0,0 +1,94 @@
// 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.
/**
* Represents the commands and events under Browser Module.
* Described in https://w3c.github.io/webdriver-bidi/#module-browser
*/
class Browser {
constructor(driver) {
this._driver = driver
}
async init() {
if (!(await this._driver.getCapabilities()).get('webSocketUrl')) {
throw Error('WebDriver instance must support BiDi protocol')
}
this.bidi = await this._driver.getBidi()
}
/**
* Creates a new user context.
* @returns {Promise<string>} A promise that resolves to the user context id.
*/
async createUserContext() {
const command = {
method: 'browser.createUserContext',
params: {},
}
let response = await this.bidi.send(command)
return response.result.userContext
}
/**
* Gets the list of all user contexts.
* @returns {Promise<string[]>} A promise that resolves to an array of user context ids.
*/
async getUserContexts() {
const command = {
method: 'browser.getUserContexts',
params: {},
}
let response = await this.bidi.send(command)
let userContexts = []
let userContextsArray = response.result.userContexts
for (let userContextJson of userContextsArray) {
userContexts.push(userContextJson.userContext)
}
return userContexts
}
/**
* Removes a user context.
* @param {string} userContext The user context id to be removed.
* @returns {Promise<void>}
*/
async removeUserContext(userContext) {
const command = {
method: 'browser.removeUserContext',
params: { userContext: userContext },
}
await this.bidi.send(command)
}
}
async function getBrowserInstance(driver) {
let instance = new Browser(driver)
await instance.init()
return instance
}
module.exports = getBrowserInstance

View File

@@ -0,0 +1,656 @@
// 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),
}
const res = await this.bidi.send(params)
return res
}
/**
* @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}`)
}
if (startNodes !== undefined && Array.isArray(startNodes)) {
startNodes.forEach((node) => {
if (!(node instanceof ReferenceValue)) {
throw Error(`Pass in a ReferenceValue object. Received: ${node}`)
}
})
}
const params = {
method: 'browsingContext.locateNodes',
params: {
context: this._id,
locator: Object.fromEntries(locator.toMap()),
maxNodeCount: maxNodeCount,
sandbox: sandbox,
serializationOptions: serializationOptions,
startNodes: startNodes,
},
}
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

View File

@@ -0,0 +1,149 @@
// 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 { BrowsingContextInfo, NavigationInfo, UserPromptOpened, UserPromptClosed } = require('./browsingContextTypes')
/**
* Represents a browsing context related events.
* Described in https://w3c.github.io/webdriver-bidi/#module-contexts-events.
* While BrowsingContext class represents a browsing context lifecycle and related commands.
* This class is specific to listening to events. Events can be subscribed to multiple browsing contexts or all of them.
*/
class BrowsingContextInspector {
constructor(driver, browsingContextIds) {
this._driver = driver
this._browsingContextIds = browsingContextIds
}
async init() {
this.bidi = await this._driver.getBidi()
}
/**
* Subscribes to the 'browsingContext.contextCreated' event.
* @param {Function} callback - The callback function to handle the event.
* @returns {Promise<void>} - A promise that resolves when the event is emitted.
*/
async onBrowsingContextCreated(callback) {
await this.subscribeAndHandleEvent('browsingContext.contextCreated', callback)
}
/**
* Subscribes to the 'browsingContext.contextDestroyed' event.
* @param {Function} callback - The callback function to handle the event.
* @returns {Promise<void>} - A promise that resolves when the event is emitted.
*/
async onBrowsingContextDestroyed(callback) {
await this.subscribeAndHandleEvent('browsingContext.contextDestroyed', callback)
}
/**
* Subscribe to the 'browsingContext.navigationStarted' event.
* @param {Function} callback - The callback function to handle the event.
* @returns {Promise<void>} - A promise that resolves when the event is emitted.
*/
async onNavigationStarted(callback) {
await this.subscribeAndHandleEvent('browsingContext.navigationStarted', callback)
}
/**
* Subscribes to the 'browsingContext.fragmentNavigated' event.
*
* @param {Function} callback - The callback function to handle the event.
* @returns {Promise<void>} - A promise that resolves when the event is emitted.
*/
async onFragmentNavigated(callback) {
await this.subscribeAndHandleEvent('browsingContext.fragmentNavigated', callback)
}
/**
* Subscribes to the 'browsingContext.userPromptClosed' event.
*
* @param {Function} callback - The callback function to handle the event.
* @returns {Promise<void>} - A promise that resolves when the event is emitted.
*/
async onUserPromptClosed(callback) {
await this.subscribeAndHandleEvent('browsingContext.userPromptClosed', callback)
}
/**
* Subscribes to the 'browsingContext.userPromptOpened' event.
*
* @param {Function} callback - The callback function to handle the event.
* @returns {Promise<void>} - A promise that resolves when the event is emitted.
*/
async onUserPromptOpened(callback) {
await this.subscribeAndHandleEvent('browsingContext.userPromptOpened', callback)
}
/**
* Subscribes to the 'browsingContext.domContentLoaded' event.
*
* @param {Function} callback - The callback function to handle the event.
* @returns {Promise<void>} - A promise that resolves when the event is emitted.
*/
async onDomContentLoaded(callback) {
await this.subscribeAndHandleEvent('browsingContext.domContentLoaded', callback)
}
/**
* Subscribes to the 'browsingContext.load' event.
*
* @param {Function} callback - The callback function to handle the event.
* @returns {Promise<void>} - A promise that resolves when the event is emitted.
*/
async onBrowsingContextLoaded(callback) {
await this.subscribeAndHandleEvent('browsingContext.load', callback)
}
async subscribeAndHandleEvent(eventType, callback) {
if (this._browsingContextIds != null) {
await this.bidi.subscribe(eventType, this._browsingContextIds)
} else {
await this.bidi.subscribe(eventType)
}
await this._on(callback)
}
async _on(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 ('navigation' in params) {
response = new NavigationInfo(params.context, params.navigation, params.timestamp, params.url)
} else if ('type' in params) {
response = new UserPromptOpened(params.context, params.type, params.message)
} else if ('accepted' in params) {
response = new UserPromptClosed(params.context, params.accepted, params.userText)
} else {
response = new BrowsingContextInfo(params.context, params.url, params.children, params.parent)
}
callback(response)
}
})
}
}
async function getBrowsingContextInstance(driver, browsingContextIds = null) {
let instance = new BrowsingContextInspector(driver, browsingContextIds)
await instance.init()
return instance
}
module.exports = getBrowsingContextInstance

View File

@@ -0,0 +1,99 @@
// 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.
/**
* Represents information about a browsing context.
* Described in https://w3c.github.io/webdriver-bidi/#type-browsingContext-Info
*/
class BrowsingContextInfo {
constructor(id, url, children, parentBrowsingContext) {
this._id = id
this._url = url
this._children = children
this._parentBrowsingContext = parentBrowsingContext
}
/**
* Get the ID of the browsing context.
* @returns {string} The ID of the browsing context.
*/
get id() {
return this._id
}
/**
* Get the URL of the browsing context.
* @returns {string} The URL of the browsing context.
*/
get url() {
return this._url
}
/**
* Get the children of the browsing context.
* @returns {Array<BrowsingContextInfo>} The children of the browsing context.
*/
get children() {
return this._children
}
/**
* Get the parent browsing context.
* @returns {BrowsingContextInfo} The parent browsing context.
*/
get parentBrowsingContext() {
return this._parentBrowsingContext
}
}
/**
* Represents information about a navigation.
* Described in https://w3c.github.io/webdriver-bidi/#type-browsingContext-NavigationInfo.
*/
class NavigationInfo {
/**
* Constructs a new NavigationInfo object.
* @param {string} browsingContextId - The ID of the browsing context.
* @param {string} navigationId - The ID of the navigation.
* @param {number} timestamp - The timestamp of the navigation.
* @param {string} url - The URL of the page navigated to.
*/
constructor(browsingContextId, navigationId, timestamp, url) {
this.browsingContextId = browsingContextId
this.navigationId = navigationId
this.timestamp = timestamp
this.url = url
}
}
class UserPromptOpened {
constructor(browsingContextId, type, message) {
this.browsingContextId = browsingContextId
this.type = type
this.message = message
}
}
class UserPromptClosed {
constructor(browsingContextId, accepted, userText = undefined) {
this.browsingContextId = browsingContextId
this.accepted = accepted
this.userText = userText
}
}
module.exports = { BrowsingContextInfo, NavigationInfo, UserPromptOpened, UserPromptClosed }

View File

@@ -0,0 +1,96 @@
// 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 { BoxClipRectangle, ElementClipRectangle } = require('./clipRectangle')
/**
* Defines the reference point from which to compute offsets for capturing screenshot.
*
* @enum {string}
*/
const Origin = {
VIEWPORT: 'viewport',
DOCUMENT: 'document',
}
/**
* Represents the optional parameters for capturing a screenshot.
* Described in https://w3c.github.io/webdriver-bidi/#command-browsingContext-captureScreenshot.
*/
class CaptureScreenshotParameters {
#map = new Map()
/**
* Sets the origin for capturing the screenshot.
*
* @param {Origin} origin - The origin for capturing the screenshot. Must be one of `Origin.VIEWPORT` or `Origin.DOCUMENT`.
* @returns {CaptureScreenshotParameters} - The current instance of the CaptureScreenshotParameters for chaining.
* @throws {Error} - If the provided origin is not valid.
*/
origin(origin) {
if (origin !== Origin.VIEWPORT && origin !== Origin.DOCUMENT) {
throw new Error(`Origin must be one of ${Object.values(Origin)}. Received:'${origin}'`)
}
this.#map.set('origin', origin)
return this
}
/**
* Sets the image format and quality for capturing a screenshot.
*
* @param {string} type - The image format type.
* @param {number} [quality] - The image quality (optional).
* @throws {Error} If the type is not a string or if the quality is not a number.
* @returns {CaptureScreenshotParameters} - The current instance of the CaptureScreenshotParameters for chaining.
*/
imageFormat(type, quality = undefined) {
if (typeof type !== 'string') {
throw new Error(`Type must be an instance of String. Received:'${type}'`)
}
this.#map.set('type', type)
if (quality !== undefined) {
if (typeof quality !== 'number') {
throw new Error(`Quality must be a number. Received:'${quality}'`)
}
this.#map.set('quality', quality)
}
return this
}
/**
* Sets the clip rectangle for capturing a screenshot.
*
* @param {BoxClipRectangle|ElementClipRectangle} clipRectangle - The clip rectangle to set.
* @throws {Error} If the clipRectangle is not an instance of ClipRectangle.
* @returns {CaptureScreenshotParameters} - The current instance of the CaptureScreenshotParameters for chaining.
*/
clipRectangle(clipRectangle) {
if (!(clipRectangle instanceof BoxClipRectangle || clipRectangle instanceof ElementClipRectangle)) {
throw new Error(`ClipRectangle must be an instance of ClipRectangle. Received:'${clipRectangle}'`)
}
this.#map.set('clip', Object.fromEntries(clipRectangle.asMap()))
return this
}
asMap() {
return this.#map
}
}
module.exports = { CaptureScreenshotParameters, Origin }

View File

@@ -0,0 +1,127 @@
// 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.
/**
* Represents a clip rectangle.
* Described in https://w3c.github.io/webdriver-bidi/#command-browsingContext-captureScreenshot.
*/
class ClipRectangle {
clipType
/**
* Constructs a new ClipRectangle object.
* @param {string} type - The type of the clip rectangle.
*/
constructor(type) {
this.clipType = type
}
/**
* Gets the type of the clip rectangle.
* @returns {string} The type of the clip rectangle.
*/
get type() {
return this.clipType
}
asMap() {}
}
/**
* Represents a clip rectangle for an element.
* @extends ClipRectangle
*/
class ElementClipRectangle extends ClipRectangle {
#sharedId
#handleId
/**
* Constructs a new ElementClipRectangle instance.
* @param {string} sharedId - The shared ID of the element.
* @param {string} [handleId] - The handle ID of the element (optional).
*/
constructor(sharedId, handleId = undefined) {
super('element')
this.#sharedId = sharedId
if (handleId !== undefined) {
this.#handleId = handleId
}
}
/**
* Converts the ElementClipRectangle instance to a map.
* @returns {Map} - The converted map.
*/
asMap() {
const map = new Map()
map.set('type', super.type)
const sharedReference = new Map()
sharedReference.set('sharedId', this.#sharedId)
if (this.#handleId !== undefined) {
sharedReference.set('handleId', this.#handleId)
}
map.set('element', Object.fromEntries(sharedReference))
return map
}
}
/**
* Represents a box-shaped clip rectangle.
* @extends ClipRectangle
*/
class BoxClipRectangle extends ClipRectangle {
#x
#y
#width
#height
/**
* Constructs a new BoxClipRectangle object.
* @param {number} x - The x-coordinate of the top-left corner of the rectangle.
* @param {number} y - The y-coordinate of the top-left corner of the rectangle.
* @param {number} width - The width of the rectangle.
* @param {number} height - The height of the rectangle.
*/
constructor(x, y, width, height) {
super('box')
this.#x = x
this.#y = y
this.#width = width
this.#height = height
}
/**
* Converts the BoxClipRectangle object to a Map.
* @returns {Map<string, any>} - The Map representation of the BoxClipRectangle object.
*/
asMap() {
const map = new Map()
map.set('type', super.type)
map.set('x', this.#x)
map.set('y', this.#y)
map.set('width', this.#width)
map.set('height', this.#height)
return map
}
}
module.exports = { BoxClipRectangle, ElementClipRectangle }

View File

@@ -0,0 +1,122 @@
// 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 { BytesValue, Header } = require('./networkTypes')
/**
* Represents the parameters for a continue request command.
* Described in https://w3c.github.io/webdriver-bidi/#command-network-continueRequest.
*/
class ContinueRequestParameters {
#map = new Map()
constructor(request) {
this.#map.set('request', request)
}
/**
* Sets the body value for the request.
*
* @param {BytesValue} value - The value to set as the body. Must be an instance of BytesValue.
* @returns {ContinueRequestParameters} - The current instance of the ContinueRequestParameters for chaining.
* @throws {Error} - If the value is not an instance of BytesValue.
*/
body(value) {
if (!(value instanceof BytesValue)) {
throw new Error(`Value must be an instance of BytesValue. Received: '${value})'`)
}
this.#map.set('body', Object.fromEntries(value.asMap()))
return this
}
/**
* Sets the cookies for the request.
*
* @param {Header[]} cookieHeaders - An array of cookie headers.
* @returns {continueRequestParameters} - The current instance of the ContinueRequestParameters for chaining.
* @throws {Error} - If a cookie header is not an instance of Header.
*/
cookies(cookieHeaders) {
const cookies = []
cookieHeaders.forEach((header) => {
if (!(header instanceof Header)) {
throw new Error(`CookieHeader must be an instance of Header. Received:'${header}'`)
}
cookies.push(Object.fromEntries(header.asMap()))
})
this.#map.set('cookies', cookies)
return this
}
/**
* Sets the headers for the request.
*
* @param {Header[]} headers - An array of Header objects.
* @returns {ContinueRequestParameters} - The current instance of the ContinueRequestParameters for chaining.
* @throws {Error} - If the header value is not an instance of Header.
*/
headers(headers) {
const headerList = []
headers.forEach((header) => {
if (!(header instanceof Header)) {
throw new Error(`Header value must be an instance of Header. Received:'${header}'`)
}
headerList.push(Object.fromEntries(header.asMap()))
})
this.#map.set('headers', headerList)
return this
}
/**
* Sets the HTTP method for the request.
*
* @param {string} method - The HTTP method to be set.
* @returns {ContinueRequestParameters} - The updated `continueRequestParameters` object.
* @throws {Error} - If the method parameter is not a string.
*/
method(method) {
if (typeof method !== 'string') {
throw new Error(`Http method must be a string. Received: '${method})'`)
}
this.#map.set('method', method)
return this
}
/**
* Sets the URL for the request.
*
* @param {string} url - The URL to set for the request.
* @returns {ContinueRequestParameters} - The current instance of the ContinueRequestParameters for chaining.
* @throws {Error} - If the url parameter is not a string.
*/
url(url) {
if (typeof url !== 'string') {
throw new Error(`Url must be a string. Received:'${url}'`)
}
this.#map.set('url', url)
return this
}
asMap() {
return this.#map
}
}
module.exports = { ContinueRequestParameters }

View File

@@ -0,0 +1,129 @@
// 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 { Header } = require('./networkTypes')
/**
* Represents the parameters for a continue response.
* Described in https://w3c.github.io/webdriver-bidi/#command-network-continueResponse.
*/
class ContinueResponseParameters {
#map = new Map()
constructor(request) {
this.#map.set('request', request)
}
/**
* Sets the cookies for the response.
*
* @param {Header[]} cookieHeaders - The array of cookie headers.
* @returns {ContinueResponseParameters} - The current instance of the ContinueResponseParameters for chaining.
* @throws {Error} - If the cookieHeader is not an instance of Header.
*/
cookies(cookieHeaders) {
const cookies = []
cookieHeaders.forEach((header) => {
if (!(header instanceof Header)) {
throw new Error(`CookieHeader must be an instance of Header. Received:'${header}'`)
}
cookies.push(Object.fromEntries(header.asMap()))
})
this.#map.set('cookies', cookies)
return this
}
/**
* Sets the credentials for authentication.
*
* @param {string} username - The username for authentication.
* @param {string} password - The password for authentication.
* @returns {ContinueResponseParameters} The current instance of the ContinueResponseParameters for chaining.
* @throws {Error} If username or password is not a string.
*/
credentials(username, password) {
if (typeof username !== 'string') {
throw new Error(`Username must be a string. Received:'${username}'`)
}
if (typeof password !== 'string') {
throw new Error(`Password must be a string. Received:'${password}'`)
}
this.#map.set('credentials', { type: 'password', username: username, password: password })
return this
}
/**
* Sets the headers for the response.
*
* @param {Header[]} headers - An array of Header objects representing the headers.
* @returns {ContinueResponseParameters} - The current instance of the ContinueResponseParameters for chaining.
* @throws {Error} - If the header value is not an instance of Header.
*/
headers(headers) {
const headerList = []
headers.forEach((header) => {
if (!(header instanceof Header)) {
throw new Error(`Header value must be an instance of Header. Received:'${header}'`)
}
headerList.push(Object.fromEntries(header.asMap()))
})
this.#map.set('headers', headerList)
return this
}
/**
* Sets the reason phrase for the response.
*
* @param {string} reasonPhrase - The reason phrase for the response.
* @returns {ContinueResponseParameters} - The current instance of the ContinueResponseParameters for chaining.
* @throws {Error} - If the reason phrase is not a string.
*/
reasonPhrase(reasonPhrase) {
if (typeof reasonPhrase !== 'string') {
throw new Error(`Reason phrase must be a string. Received: '${reasonPhrase})'`)
}
this.#map.set('reasonPhrase', reasonPhrase)
return this
}
/**
* Sets the status code for the response.
*
* @param {number} statusCode - The status code to set.
* @returns {ContinueResponseParameters} - The current instance of the ContinueResponseParameters for chaining.
* @throws {Error} - If the `statusCode` parameter is not an integer.
*/
statusCode(statusCode) {
if (!Number.isInteger(statusCode)) {
throw new Error(`Status must be an integer. Received:'${statusCode}'`)
}
this.#map.set('statusCode', statusCode)
return this
}
asMap() {
return this.#map
}
}
module.exports = { ContinueResponseParameters }

View File

@@ -0,0 +1,139 @@
// 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 { SameSite, BytesValue } = require('./networkTypes')
/**
* Represents a filter for fetching cookies.
* Described in https://w3c.github.io/webdriver-bidi/#command-storage-getCookies
*/
class CookieFilter {
#map = new Map()
/**
* Sets the name of the cookie.
*
* @param {string} name - The name of the cookie.
* @returns {CookieFilter} - The updated CookieFilter instance for chaining.
*/
name(name) {
this.#map.set('name', name)
return this
}
/**
* Sets the value of the cookie.
*
* @param {BytesValue} value - The value to be set. Must be an instance of BytesValue.
* @returns {CookieFilter} - The updated CookieFilter instance for chaining.
* @throws {Error} - If the value is not an instance of BytesValue.
*/
value(value) {
if (!(value instanceof BytesValue)) {
throw new Error(`Value must be an instance of BytesValue. Received:'${value}'`)
}
this.#map.set('value', Object.fromEntries(value.asMap()))
return this
}
/**
* Sets the domain for the cookie.
*
* @param {string} domain - The domain to set.
* @returns {CookieFilter} - The updated CookieFilter instance for chaining.
*/
domain(domain) {
this.#map.set('domain', domain)
return this
}
/**
* Sets the url path for the cookie to be fetched.
*
* @param {string} path - The url path for the cookie to be fetched.
* @returns {CookieFilter} - The updated CookieFilter instance for chaining.
*/
path(path) {
this.#map.set('path', path)
return this
}
/**
* Sets the size of the cookie to be fetched.
*
* @param {number} size - The size of the cookie.
* @returns {CookieFilter} - The updated CookieFilter instance for chaining.
*/
size(size) {
this.#map.set('size', size)
return this
}
/**
* Sets the `httpOnly` flag for the cookie filter.
*
* @param {boolean} httpOnly - The value to set for the `httpOnly` flag.
* @returns {CookieFilter} - The updated CookieFilter instance for chaining.
*/
httpOnly(httpOnly) {
this.#map.set('httpOnly', httpOnly)
return this
}
/**
* Sets the flag to fetch secure cookies.
*
* @param {boolean} secure - Whether the cookie fetched should be secure only or not.
* @returns {CookieFilter} - The updated CookieFilter instance for chaining.
*/
secure(secure) {
this.#map.set('secure', secure)
return this
}
/**
* Sets the SameSite attribute for the cookie.
*
* @param {SameSite} sameSite - The SameSite value to be set for the cookie.
* @returns {CookieFilter} - The updated CookieFilter instance for chaining.
* @throws {Error} - If the provided sameSite value is not an instance of SameSite.
*/
sameSite(sameSite) {
if (!(sameSite instanceof SameSite)) {
throw new Error(`Params must be a value in SameSite. Received:'${sameSite}'`)
}
this.#map.set('sameSite', sameSite)
return this
}
/**
* Sets the expiry value.
*
* @param {number} expiry - The expiry value.
* @returns {CookieFilter} - The updated CookieFilter instance for chaining.
*/
expiry(expiry) {
this.#map.set('expiry', expiry)
return this
}
asMap() {
return this.#map
}
}
module.exports = { CookieFilter }

View File

@@ -0,0 +1,73 @@
// 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.
/**
* Represents a set of parameters for creating a context.
* Described in https://w3c.github.io/webdriver-bidi/#command-browsingContext-create.
*/
class CreateContextParameters {
#map = new Map()
/**
* Sets the reference context.
* @param {string} id - The ID of the reference context.
* @returns {CreateContextParameters} - The updated instance of CreateContextParameters for chaining.
* @throws {Error} - If the provided ID is not a string.
*/
referenceContext(id) {
if (typeof id !== 'string') {
throw new Error(`ReferenceContext must be string. Received:'${id}'`)
}
this.#map.set('referenceContext', id)
return this
}
/**
* Sets the background parameter.
*
* @param {boolean} background - The background value to set.
* @returns {CreateContextParameters} - The updated instance of CreateContextParameters for chaining.
* @throws {Error} - If the background parameter is not a boolean.
*/
background(background) {
if (typeof background !== 'boolean') {
throw new Error(`Background must be boolean. Received:'${background}'`)
}
this.#map.set('background', background)
return this
}
/**
* Sets the user context.
* @param {string} userContext - The user context to set.
* @returns {CreateContextParameters} - The updated instance of CreateContextParameters for chaining.
* @throws {Error} - If the userContext parameter is not a string.
*/
userContext(userContext) {
if (typeof userContext !== 'string') {
throw new Error(`UserContext must be string. Received:'${userContext}'`)
}
this.#map.set('userContext', userContext)
return this
}
asMap() {
return this.#map
}
}
module.exports = { CreateContextParameters }

View File

@@ -0,0 +1,71 @@
// 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.
/**
* Represents the type of script evaluation result.
* Described in https://w3c.github.io/webdriver-bidi/#type-script-EvaluateResult.
* @enum {string}
*/
const EvaluateResultType = {
SUCCESS: 'success',
EXCEPTION: 'exception',
}
/**
* Represents a successful evaluation result.
* @class
*/
class EvaluateResultSuccess {
constructor(realmId, value) {
this.resultType = EvaluateResultType.SUCCESS
this.realmId = realmId
this.result = value
}
}
/**
* Represents an exception that occurred during evaluation of a result.
* @class
*/
class EvaluateResultException {
constructor(realmId, exceptionDetails) {
this.resultType = EvaluateResultType.EXCEPTION
this.realmId = realmId
this.exceptionDetails = exceptionDetails
}
}
/**
* Represents details of an exception.
* @class
*/
class ExceptionDetails {
constructor(exceptionDetails) {
this.columnNumber = 'columnNumber' in exceptionDetails ? exceptionDetails['columnNumber'] : null
this.exception = 'exception' in exceptionDetails ? exceptionDetails['exception'] : null
this.lineNumber = 'lineNumber' in exceptionDetails ? exceptionDetails['lineNumber'] : null
this.stackTrace = 'stackTrace' in exceptionDetails ? exceptionDetails['stackTrace'] : null
this.text = 'text' in exceptionDetails ? exceptionDetails['text'] : null
}
}
module.exports = {
EvaluateResultType,
EvaluateResultSuccess,
EvaluateResultException,
ExceptionDetails,
}

View File

@@ -0,0 +1,42 @@
// 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.
class FilterBy {
constructor(level) {
this.level_ = level
}
static logLevel(level) {
if (level === undefined || (level != undefined && !['debug', 'error', 'info', 'warning'].includes(level))) {
throw Error(
`Please pass valid log level. Valid log levels are 'debug', 'error', 'info' and 'warning'. Received: ${level}`,
)
}
return new FilterBy(level)
}
getLevel() {
return this.level_
}
}
// PUBLIC API
module.exports = {
FilterBy,
}

View File

@@ -0,0 +1,225 @@
// 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 { EventEmitter } = require('node:events')
const WebSocket = require('ws')
const RESPONSE_TIMEOUT = 1000 * 30
class Index extends EventEmitter {
id = 0
connected = false
events = []
browsingContexts = []
/**
* Create a new websocket connection
* @param _webSocketUrl
*/
constructor(_webSocketUrl) {
super()
this.connected = false
this._ws = new WebSocket(_webSocketUrl)
this._ws.on('open', () => {
this.connected = true
})
}
/**
* @returns {WebSocket}
*/
get socket() {
return this._ws
}
/**
* @returns {boolean|*}
*/
get isConnected() {
return this.connected
}
/**
* Get Bidi Status
* @returns {Promise<*>}
*/
get status() {
return this.send({
method: 'session.status',
params: {},
})
}
/**
* Resolve connection
* @returns {Promise<unknown>}
*/
async waitForConnection() {
return new Promise((resolve) => {
if (this.connected) {
resolve()
} else {
this._ws.once('open', () => {
resolve()
})
}
})
}
/**
* Sends a bidi request
* @param params
* @returns {Promise<unknown>}
*/
async send(params) {
if (!this.connected) {
await this.waitForConnection()
}
const id = ++this.id
this._ws.send(JSON.stringify({ id, ...params }))
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
reject(new Error(`Request with id ${id} timed out`))
handler.off('message', listener)
}, RESPONSE_TIMEOUT)
const listener = (data) => {
try {
const payload = JSON.parse(data.toString())
if (payload.id === id) {
clearTimeout(timeoutId)
handler.off('message', listener)
resolve(payload)
}
} catch (err) {
// eslint-disable-next-line no-undef
log.error(`Failed parse message: ${err.message}`)
}
}
const handler = this._ws.on('message', listener)
})
}
/**
* Subscribe to events
* @param events
* @param browsingContexts
* @returns {Promise<void>}
*/
async subscribe(events, browsingContexts) {
function toArray(arg) {
if (arg === undefined) {
return []
}
return Array.isArray(arg) ? [...arg] : [arg]
}
const eventsArray = toArray(events)
const contextsArray = toArray(browsingContexts)
const params = {
method: 'session.subscribe',
params: {},
}
if (eventsArray.length && eventsArray.some((event) => typeof event !== 'string')) {
throw new TypeError('events should be string or string array')
}
if (contextsArray.length && contextsArray.some((context) => typeof context !== 'string')) {
throw new TypeError('browsingContexts should be string or string array')
}
if (eventsArray.length) {
params.params.events = eventsArray
}
if (contextsArray.length) {
params.params.contexts = contextsArray
}
await this.send(params)
}
/**
* Unsubscribe to events
* @param events
* @param browsingContexts
* @returns {Promise<void>}
*/
async unsubscribe(events, browsingContexts) {
if (typeof events === 'string') {
this.events = this.events.filter((event) => event !== events)
} else if (Array.isArray(events)) {
this.events = this.events.filter((event) => !events.includes(event))
}
if (typeof browsingContexts === 'string') {
this.browsingContexts.pop()
} else if (Array.isArray(browsingContexts)) {
this.browsingContexts = this.browsingContexts.filter((id) => !browsingContexts.includes(id))
}
const params = {
method: 'session.unsubscribe',
params: {
events: this.events,
},
}
if (this.browsingContexts.length > 0) {
params.params.contexts = this.browsingContexts
}
await this.send(params)
}
/**
* Close ws connection.
* @returns {Promise<unknown>}
*/
close() {
const closeWebSocket = (callback) => {
// don't close if it's already closed
if (this._ws.readyState === 3) {
callback()
} else {
// don't notify on user-initiated shutdown ('disconnect' event)
this._ws.removeAllListeners('close')
this._ws.once('close', () => {
this._ws.removeAllListeners()
callback()
})
this._ws.close()
}
}
return new Promise((fulfill, _) => {
closeWebSocket(fulfill)
})
}
}
/**
* API
* @type {function(*): Promise<Index>}
*/
module.exports = Index

View File

@@ -0,0 +1,141 @@
// 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.
// type: module added to package.json
// import { WebElement } from '../lib/webdriver'
const { WebElement } = require('../lib/webdriver')
const { RemoteReferenceType, ReferenceValue } = require('./protocolValue')
/**
* Represents commands and events related to the Input module (simulated user input).
* Described in https://w3c.github.io/webdriver-bidi/#module-input.
*/
class Input {
constructor(driver) {
this._driver = driver
}
async init() {
if (!(await this._driver.getCapabilities()).get('webSocketUrl')) {
throw Error('WebDriver instance must support BiDi protocol')
}
this.bidi = await this._driver.getBidi()
}
/**
* Performs the specified actions on the given browsing context.
*
* @param {string} browsingContextId - The ID of the browsing context.
* @param {Array} actions - The actions to be performed.
* @returns {Promise} A promise that resolves with the response from the server.
*/
async perform(browsingContextId, actions) {
const _actions = await updateActions(actions)
const command = {
method: 'input.performActions',
params: {
context: browsingContextId,
actions: _actions,
},
}
return await this.bidi.send(command)
}
/**
* Resets the input state in the specified browsing context.
*
* @param {string} browsingContextId - The ID of the browsing context.
* @returns {Promise} A promise that resolves when the release actions are sent.
*/
async release(browsingContextId) {
const command = {
method: 'input.releaseActions',
params: {
context: browsingContextId,
},
}
return await this.bidi.send(command)
}
/**
* Sets the files property of a given input element.
*
* @param {string} browsingContextId - The ID of the browsing context.
* @param {string | ReferenceValue} element - The ID of the element or a ReferenceValue object representing the element.
* @param {string | string[]} files - The file path or an array of file paths to be set.
* @throws {Error} If the element is not a string or a ReferenceValue.
* @returns {Promise<void>} A promise that resolves when the files are set.
*/
async setFiles(browsingContextId, element, files) {
if (typeof element !== 'string' && !(element instanceof ReferenceValue)) {
throw Error(`Pass in a WebElement id as a string or a ReferenceValue. Received: ${element}`)
}
const command = {
method: 'input.setFiles',
params: {
context: browsingContextId,
element:
typeof element === 'string'
? new ReferenceValue(RemoteReferenceType.SHARED_ID, element).asMap()
: element.asMap(),
files: typeof files === 'string' ? [files] : files,
},
}
await this.bidi.send(command)
}
}
async function updateActions(actions) {
const _actions = []
for (const action of actions) {
const sequenceList = action.actions
let updatedSequenceList = []
if (action.type === 'pointer' || action.type === 'wheel') {
for (const sequence of sequenceList) {
if ((sequence.type === 'pointerMove' || sequence.type === 'scroll') && sequence.origin instanceof WebElement) {
const element = sequence.origin
const elementId = await element.getId()
sequence.origin = {
type: 'element',
element: { sharedId: elementId },
}
}
updatedSequenceList.push(sequence)
}
const updatedAction = { ...action, actions: updatedSequenceList }
_actions.push(updatedAction)
} else {
_actions.push(action)
}
}
return _actions
}
async function getInputInstance(driver) {
let instance = new Input(driver)
await instance.init()
return instance
}
module.exports = getInputInstance

View File

@@ -0,0 +1,28 @@
// 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.
/**
* Represents the different phases of intercepting network requests and responses.
* @enum {string}
*/
const InterceptPhase = {
BEFORE_REQUEST_SENT: 'beforeRequestSent',
RESPONSE_STARTED: 'responseStarted',
AUTH_REQUIRED: 'authRequired',
}
module.exports = { InterceptPhase }

View File

@@ -0,0 +1,156 @@
// 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.
'use strict'
/**
* Represents a base log entry.
* Desribed in https://w3c.github.io/webdriver-bidi/#types-log-logentry.
*/
class BaseLogEntry {
/**
* Creates a new instance of BaseLogEntry.
* @param {string} level - The log level.
* @param {string} text - The log text.
* @param {number} timeStamp - The log timestamp.
* @param {string} stackTrace - The log stack trace.
*/
constructor(level, text, timeStamp, stackTrace) {
this._level = level
this._text = text
this._timeStamp = timeStamp
this._stackTrace = stackTrace
}
/**
* Gets the log level.
* @returns {string} The log level.
*/
get level() {
return this._level
}
/**
* Gets the log text.
* @returns {string} The log text.
*/
get text() {
return this._text
}
/**
* Gets the log timestamp.
* @returns {number} The log timestamp.
*/
get timeStamp() {
return this._timeStamp
}
/**
* Gets the log stack trace.
* @returns {string} The log stack trace.
*/
get stackTrace() {
return this._stackTrace
}
}
/**
* Represents a generic log entry.
* @class
* @extends BaseLogEntry
*/
class GenericLogEntry extends BaseLogEntry {
/**
* Creates an instance of GenericLogEntry.
* @param {string} level - The log level.
* @param {string} text - The log text.
* @param {Date} timeStamp - The log timestamp.
* @param {string} type - The log type.
* @param {string} stackTrace - The log stack trace.
*/
constructor(level, text, timeStamp, type, stackTrace) {
super(level, text, timeStamp, stackTrace)
this._type = type
}
/**
* Gets the log type.
* @returns {string} The log type.
*/
get type() {
return this._type
}
}
/**
* Represents a log entry for console logs.
* @class
* @extends GenericLogEntry
*/
class ConsoleLogEntry extends GenericLogEntry {
constructor(level, text, timeStamp, type, method, realm, args, stackTrace) {
super(level, text, timeStamp, type, stackTrace)
this._method = method
this._realm = realm
this._args = args
}
/**
* Gets the method associated with the log entry.
* @returns {string} The method associated with the log entry.
*/
get method() {
return this._method
}
/**
* Gets the realm associated with the log entry.
* @returns {string} The realm associated with the log entry.
*/
get realm() {
return this._realm
}
/**
* Gets the arguments associated with the log entry.
* @returns {Array} The arguments associated with the log entry.
*/
get args() {
return this._args
}
}
/**
* Represents a log entry for JavaScript logs.
* @class
* @extends GenericLogEntry
*/
class JavascriptLogEntry extends GenericLogEntry {
constructor(level, text, timeStamp, type, stackTrace) {
super(level, text, timeStamp, type, stackTrace)
}
}
// PUBLIC API
module.exports = {
BaseLogEntry,
GenericLogEntry,
ConsoleLogEntry,
JavascriptLogEntry,
}

View File

@@ -0,0 +1,353 @@
// 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 { FilterBy } = require('./filterBy')
const { ConsoleLogEntry, JavascriptLogEntry, GenericLogEntry } = require('./logEntries')
const LOG = {
TYPE_CONSOLE: 'console',
TYPE_JS_LOGS: 'javascript',
TYPE_JS_EXCEPTION: 'javascriptException',
TYPE_LOGS: 'logs',
TYPE_CONSOLE_FILTER: 'console_filter',
TYPE_JS_LOGS_FILTER: 'javascript_filter',
TYPE_JS_EXCEPTION_FILTER: 'javascriptException_filter',
TYPE_LOGS_FILTER: 'logs_filter',
}
class LogInspector {
bidi
ws
#callbackId = 0
constructor(driver, browsingContextIds) {
this._driver = driver
this._browsingContextIds = browsingContextIds
this.listener = new Map()
this.listener.set(LOG.TYPE_CONSOLE, new Map())
this.listener.set(LOG.TYPE_JS_LOGS, new Map())
this.listener.set(LOG.TYPE_JS_EXCEPTION, new Map())
this.listener.set(LOG.TYPE_LOGS, new Map())
this.listener.set(LOG.TYPE_CONSOLE_FILTER, new Map())
this.listener.set(LOG.TYPE_JS_LOGS_FILTER, new Map())
this.listener.set(LOG.TYPE_JS_EXCEPTION_FILTER, new Map())
this.listener.set(LOG.TYPE_LOGS_FILTER, new Map())
}
/**
* Subscribe to log event
* @returns {Promise<void>}
*/
async init() {
this.bidi = await this._driver.getBidi()
await this.bidi.subscribe('log.entryAdded', this._browsingContextIds)
}
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)
}
}
}
invokeCallbacksWithFilter(eventType, data, filterLevel) {
const callbacks = this.listener.get(eventType)
if (callbacks) {
for (const [, value] of callbacks) {
const callback = value.callback
const filter = value.filter
if (filterLevel === filter.getLevel()) {
callback(data)
}
}
}
}
/**
* Listen to Console logs
* @param callback
* @param filterBy
* @returns {Promise<number>}
*/
async onConsoleEntry(callback, filterBy = undefined) {
if (filterBy !== undefined && !(filterBy instanceof FilterBy)) {
throw Error(`Pass valid FilterBy object. Received: ${filterBy}`)
}
let id
if (filterBy !== undefined) {
id = this.addCallback(LOG.TYPE_CONSOLE_FILTER, { callback: callback, filter: filterBy })
} else {
id = this.addCallback(LOG.TYPE_CONSOLE, callback)
}
this.ws = await this.bidi.socket
this.ws.on('message', (event) => {
const { params } = JSON.parse(Buffer.from(event.toString()))
if (params?.type === LOG.TYPE_CONSOLE) {
let consoleEntry = new ConsoleLogEntry(
params.level,
params.text,
params.timestamp,
params.type,
params.method,
params.realm,
params.args,
params.stackTrace,
)
if (filterBy !== undefined) {
if (params?.level === filterBy.getLevel()) {
this.invokeCallbacksWithFilter(LOG.TYPE_CONSOLE_FILTER, consoleEntry, filterBy.getLevel())
}
return
}
this.invokeCallbacks(LOG.TYPE_CONSOLE, consoleEntry)
}
})
return id
}
/**
* Listen to JS logs
* @param callback
* @param filterBy
* @returns {Promise<number>}
*/
async onJavascriptLog(callback, filterBy = undefined) {
if (filterBy !== undefined && !(filterBy instanceof FilterBy)) {
throw Error(`Pass valid FilterBy object. Received: ${filterBy}`)
}
let id
if (filterBy !== undefined) {
id = this.addCallback(LOG.TYPE_JS_LOGS_FILTER, { callback: callback, filter: filterBy })
} else {
id = this.addCallback(LOG.TYPE_JS_LOGS, callback)
}
this.ws = await this.bidi.socket
this.ws.on('message', (event) => {
const { params } = JSON.parse(Buffer.from(event.toString()))
if (params?.type === LOG.TYPE_JS_LOGS) {
let jsEntry = new JavascriptLogEntry(
params.level,
params.text,
params.timestamp,
params.type,
params.stackTrace,
)
if (filterBy !== undefined) {
if (params?.level === filterBy.getLevel()) {
this.invokeCallbacksWithFilter(LOG.TYPE_JS_LOGS_FILTER, jsEntry, filterBy.getLevel())
}
return
}
this.invokeCallbacks(LOG.TYPE_JS_LOGS, jsEntry)
}
})
return id
}
/**
* Listen to JS Exceptions
* @param callback
* @returns {Promise<number>}
*/
async onJavascriptException(callback) {
const id = this.addCallback(LOG.TYPE_JS_EXCEPTION, callback)
this.ws = await this.bidi.socket
this.ws.on('message', (event) => {
const { params } = JSON.parse(Buffer.from(event.toString()))
if (params?.type === 'javascript' && params?.level === 'error') {
let jsErrorEntry = new JavascriptLogEntry(
params.level,
params.text,
params.timestamp,
params.type,
params.stackTrace,
)
this.invokeCallbacks(LOG.TYPE_JS_EXCEPTION, jsErrorEntry)
}
})
return id
}
/**
* Listen to any logs
* @param callback
* @param filterBy
* @returns {Promise<number>}
*/
async onLog(callback, filterBy = undefined) {
if (filterBy !== undefined && !(filterBy instanceof FilterBy)) {
throw Error(`Pass valid FilterBy object. Received: ${filterBy}`)
}
let id
if (filterBy !== undefined) {
id = this.addCallback(LOG.TYPE_LOGS_FILTER, { callback: callback, filter: filterBy })
} else {
id = this.addCallback(LOG.TYPE_LOGS, callback)
}
this.ws = await this.bidi.socket
this.ws.on('message', (event) => {
const { params } = JSON.parse(Buffer.from(event.toString()))
if (params?.type === 'javascript') {
let jsEntry = new JavascriptLogEntry(
params.level,
params.text,
params.timestamp,
params.type,
params.stackTrace,
)
if (filterBy !== undefined) {
if (params?.level === filterBy.getLevel()) {
callback(jsEntry)
}
return
}
if (filterBy !== undefined) {
if (params?.level === filterBy.getLevel()) {
{
this.invokeCallbacksWithFilter(LOG.TYPE_LOGS_FILTER, jsEntry, filterBy.getLevel())
}
return
}
}
this.invokeCallbacks(LOG.TYPE_LOGS, jsEntry)
return
}
if (params?.type === 'console') {
let consoleEntry = new ConsoleLogEntry(
params.level,
params.text,
params.timestamp,
params.type,
params.method,
params.realm,
params.args,
params.stackTrace,
)
if (filterBy !== undefined) {
if (params?.level === filterBy.getLevel()) {
this.invokeCallbacksWithFilter(LOG.TYPE_LOGS_FILTER, consoleEntry, filterBy.getLevel())
}
return
}
this.invokeCallbacks(LOG.TYPE_LOGS, consoleEntry)
return
}
if (params !== undefined && !['console', 'javascript'].includes(params?.type)) {
let genericEntry = new GenericLogEntry(
params.level,
params.text,
params.timestamp,
params.type,
params.stackTrace,
)
if (filterBy !== undefined) {
if (params?.level === filterBy.getLevel()) {
{
this.invokeCallbacksWithFilter(LOG.TYPE_LOGS_FILTER, genericEntry, filterBy.getLevel())
}
return
}
}
this.invokeCallbacks(LOG.TYPE_LOGS, genericEntry)
return
}
})
return id
}
/**
* Unsubscribe to log event
* @returns {Promise<void>}
*/
async close() {
await this.bidi.unsubscribe('log.entryAdded', this._browsingContextIds)
}
}
/**
* initiate inspector instance and return
* @param driver
* @param browsingContextIds
* @returns {Promise<LogInspector>}
*/
async function getLogInspectorInstance(driver, browsingContextIds) {
let instance = new LogInspector(driver, browsingContextIds)
await instance.init()
return instance
}
/**
* API
* @type {function(*, *): Promise<LogInspector>}
*/
module.exports = getLogInspectorInstance

View File

@@ -0,0 +1,331 @@
// 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 { BeforeRequestSent, ResponseStarted, FetchError } = require('./networkTypes')
const { AddInterceptParameters } = require('./addInterceptParameters')
const { ContinueResponseParameters } = require('./continueResponseParameters')
const { ContinueRequestParameters } = require('./continueRequestParameters')
const { ProvideResponseParameters } = require('./provideResponseParameters')
/**
* Represents all commands and events of Network module.
* Described in https://w3c.github.io/webdriver-bidi/#module-network.
*/
class Network {
/**
* Represents a Network object.
* @constructor
* @param {Driver} driver - The driver to fetch the BiDi connection.
* @param {Array} browsingContextIds - An array of browsing context IDs that the network events will be subscribed to.
*/
constructor(driver, browsingContextIds) {
this._driver = driver
this._browsingContextIds = browsingContextIds
}
async init() {
this.bidi = await this._driver.getBidi()
}
/**
* Subscribes to the 'network.beforeRequestSent' event and handles it with the provided callback.
*
* @param {Function} callback - The callback function to handle the event.
* @returns {Promise<void>} - A promise that resolves when the subscription is successful.
*/
async beforeRequestSent(callback) {
await this.subscribeAndHandleEvent('network.beforeRequestSent', callback)
}
/**
* Subscribes to the 'network.responseStarted' event and handles it with the provided callback.
*
* @param {Function} callback - The callback function to handle the event.
* @returns {Promise<void>} - A promise that resolves when the subscription is successful.
*/
async responseStarted(callback) {
await this.subscribeAndHandleEvent('network.responseStarted', callback)
}
/**
* Subscribes to the 'network.responseCompleted' event and handles it with the provided callback.
*
* @param {Function} callback - The callback function to handle the event.
* @returns {Promise<void>} - A promise that resolves when the subscription is successful.
*/
async responseCompleted(callback) {
await this.subscribeAndHandleEvent('network.responseCompleted', callback)
}
/**
* Subscribes to the 'network.authRequired' event and handles it with the provided callback.
*
* @param {Function} callback - The callback function to handle the event.
* @returns {Promise<void>} - A promise that resolves when the subscription is successful.
*/
async authRequired(callback) {
await this.subscribeAndHandleEvent('network.authRequired', callback)
}
/**
* Subscribes to the 'network.fetchError' event and handles it with the provided callback.
*
* @param {Function} callback - The callback function to handle the event.
* @returns {Promise<void>} - A promise that resolves when the subscription is successful.
*/
async fetchError(callback) {
await this.subscribeAndHandleEvent('network.fetchError', callback)
}
async subscribeAndHandleEvent(eventType, callback) {
if (this._browsingContextIds != null) {
await this.bidi.subscribe(eventType, this._browsingContextIds)
} else {
await this.bidi.subscribe(eventType)
}
await this._on(callback)
}
async _on(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 ('initiator' in params) {
response = new BeforeRequestSent(
params.context,
params.navigation,
params.redirectCount,
params.request,
params.timestamp,
params.initiator,
)
} else if ('response' in params) {
response = new ResponseStarted(
params.context,
params.navigation,
params.redirectCount,
params.request,
params.timestamp,
params.response,
)
} else if ('errorText' in params) {
response = new FetchError(
params.context,
params.navigation,
params.redirectCount,
params.request,
params.timestamp,
params.errorText,
)
}
callback(response)
}
})
}
/**
* Adds a network intercept.
*
* @param {AddInterceptParameters} params - The parameters for the network intercept.
* @returns {Promise<string>} - A promise that resolves to the added intercept's id.
* @throws {Error} - If params is not an instance of AddInterceptParameters.
*/
async addIntercept(params) {
if (!(params instanceof AddInterceptParameters)) {
throw new Error(`Params must be an instance of AddInterceptParameters. Received:'${params}'`)
}
const command = {
method: 'network.addIntercept',
params: Object.fromEntries(params.asMap()),
}
let response = await this.bidi.send(command)
return response.result.intercept
}
/**
* Removes an intercept.
*
* @param {string} interceptId - The ID of the intercept to be removed.
* @returns {Promise<void>} - A promise that resolves when the intercept is successfully removed.
*/
async removeIntercept(interceptId) {
const command = {
method: 'network.removeIntercept',
params: { intercept: interceptId },
}
await this.bidi.send(command)
}
/**
* Continues the network request with authentication credentials.
* @param {string} requestId - The ID of the request to continue.
* @param {string} username - The username for authentication.
* @param {string} password - The password for authentication.
* @returns {Promise<void>} - A promise that resolves when the command is sent.
*/
async continueWithAuth(requestId, username, password) {
const command = {
method: 'network.continueWithAuth',
params: {
request: requestId.toString(),
action: 'provideCredentials',
credentials: {
type: 'password',
username: username,
password: password,
},
},
}
await this.bidi.send(command)
}
/**
* Fails a network request.
*
* @param {number} requestId - The ID of the request to fail.
* @returns {Promise<void>} - A promise that resolves when the command is sent.
*/
async failRequest(requestId) {
const command = {
method: 'network.failRequest',
params: {
request: requestId.toString(),
},
}
await this.bidi.send(command)
}
/**
* Continues the network request with authentication but without providing credentials.
* @param {string} requestId - The ID of the request to continue with authentication.
* @returns {Promise<void>} - A promise that resolves when the command is sent.
*/
async continueWithAuthNoCredentials(requestId) {
const command = {
method: 'network.continueWithAuth',
params: {
request: requestId.toString(),
action: 'default',
},
}
await this.bidi.send(command)
}
/**
* Cancels the authentication for a specific request.
*
* @param {string} requestId - The ID of the request to cancel authentication for.
* @returns {Promise<void>} - A promise that resolves when the command is sent.
*/
async cancelAuth(requestId) {
const command = {
method: 'network.continueWithAuth',
params: {
request: requestId.toString(),
action: 'cancel',
},
}
await this.bidi.send(command)
}
/**
* Continues the network request with the provided parameters.
*
* @param {ContinueRequestParameters} params - The parameters for continuing the request.
* @throws {Error} If params is not an instance of ContinueRequestParameters.
* @returns {Promise<void>} A promise that resolves when the command is sent.
*/
async continueRequest(params) {
if (!(params instanceof ContinueRequestParameters)) {
throw new Error(`Params must be an instance of ContinueRequestParameters. Received:'${params}'`)
}
const command = {
method: 'network.continueRequest',
params: Object.fromEntries(params.asMap()),
}
await this.bidi.send(command)
}
/**
* Continues the network response with the given parameters.
*
* @param {ContinueResponseParameters} params - The parameters for continuing the response.
* @throws {Error} If params is not an instance of ContinueResponseParameters.
* @returns {Promise<void>} A promise that resolves when the command is sent.
*/
async continueResponse(params) {
if (!(params instanceof ContinueResponseParameters)) {
throw new Error(`Params must be an instance of ContinueResponseParameters. Received:'${params}'`)
}
const command = {
method: 'network.continueResponse',
params: Object.fromEntries(params.asMap()),
}
await this.bidi.send(command)
}
/**
* Provides a response for the network.
*
* @param {ProvideResponseParameters} params - The parameters for providing the response.
* @throws {Error} If params is not an instance of ProvideResponseParameters.
* @returns {Promise<void>} A promise that resolves when the command is sent.
*/
async provideResponse(params) {
if (!(params instanceof ProvideResponseParameters)) {
throw new Error(`Params must be an instance of ProvideResponseParameters. Received:'${params}'`)
}
const command = {
method: 'network.provideResponse',
params: Object.fromEntries(params.asMap()),
}
await this.bidi.send(command)
}
/**
* Unsubscribes from network events for all browsing contexts.
* @returns {Promise<void>} A promise that resolves when the network connection is closed.
*/
async close() {
await this.bidi.unsubscribe(
'network.beforeRequestSent',
'network.responseStarted',
'network.responseCompleted',
'network.authRequired',
)
}
}
async function getNetworkInstance(driver, browsingContextIds = null) {
let instance = new Network(driver, browsingContextIds)
await instance.init()
return instance
}
module.exports = getNetworkInstance

View File

@@ -0,0 +1,98 @@
// 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 { BeforeRequestSent, ResponseStarted } = require('./networkTypes')
/**
* @deprecated
* in favor of using the `Network` class from `bidi/network.js`
* Inspector is specific to listening to events.
* Goal is to club commands and events under one class called Network.
*/
class NetworkInspector {
constructor(driver, browsingContextIds) {
this._driver = driver
this._browsingContextIds = browsingContextIds
}
async init() {
this.bidi = await this._driver.getBidi()
}
async beforeRequestSent(callback) {
await this.subscribeAndHandleEvent('network.beforeRequestSent', callback)
}
async responseStarted(callback) {
await this.subscribeAndHandleEvent('network.responseStarted', callback)
}
async responseCompleted(callback) {
await this.subscribeAndHandleEvent('network.responseCompleted', callback)
}
async authRequired(callback) {
await this.subscribeAndHandleEvent('network.authRequired', callback)
}
async subscribeAndHandleEvent(eventType, callback) {
if (this._browsingContextIds != null) {
await this.bidi.subscribe(eventType, this._browsingContextIds)
} else {
await this.bidi.subscribe(eventType)
}
await this._on(callback)
}
async _on(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 ('initiator' in params) {
response = new BeforeRequestSent(
params.context,
params.navigation,
params.redirectCount,
params.request,
params.timestamp,
params.initiator,
)
} else if ('response' in params) {
response = new ResponseStarted(
params.context,
params.navigation,
params.redirectCount,
params.request,
params.timestamp,
params.response,
)
}
callback(response)
}
})
}
}
async function getNetworkInspectorInstance(driver, browsingContextIds = null) {
let instance = new NetworkInspector(driver, browsingContextIds)
await instance.init()
return instance
}
module.exports = getNetworkInspectorInstance

View File

@@ -0,0 +1,833 @@
// 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 { NavigationInfo } = require('./browsingContextTypes')
/**
* Represents the possible values for the SameSite attribute of a cookie.
* @enum {string}
*/
const SameSite = {
STRICT: 'strict',
LAX: 'lax',
NONE: 'none',
findByName(name) {
return (
Object.values(this).find((type) => {
return typeof type === 'string' && name.toLowerCase() === type.toLowerCase()
}) || null
)
},
}
/**
* Represents a BytesValue object.
* Described in https://w3c.github.io/webdriver-bidi/#type-network-BytesValue.
*/
class BytesValue {
static Type = {
STRING: 'string',
BASE64: 'base64',
}
/**
* Creates a new BytesValue instance.
* @param {string} type - The type of the BytesValue.
* @param {string} value - The value of the BytesValue.
*/
constructor(type, value) {
this._type = type
this._value = value
}
/**
* Gets the type of the BytesValue.
* @returns {string} The type of the BytesValue.
*/
get type() {
return this._type
}
/**
* Gets the value of the BytesValue.
* @returns {string} The value of the BytesValue.
*/
get value() {
return this._value
}
/**
* Converts the BytesValue to a map.
* @returns {Map<string, string>} A map representation of the BytesValue.
*/
asMap() {
const map = new Map()
map.set('type', this._type)
map.set('value', this._value)
return map
}
}
/**
* Represents a header with a name and value.
* Described in https://w3c.github.io/webdriver-bidi/#type-network-Header.
*/
class Header {
/**
* Creates a new Header instance.
* @param {string} name - The name of the header.
* @param {BytesValue} value - The value of the header.
* @throws {Error} If the value is not an instance of BytesValue.
*/
constructor(name, value) {
this._name = name
if (!(value instanceof BytesValue)) {
throw new Error(`Value must be an instance of BytesValue. Received: '${value}'`)
}
this._value = value
}
/**
* Gets the name of the header.
* @returns {string} The name of the header.
*/
get name() {
return this._name
}
/**
* Gets the value of the header.
* @returns {BytesValue} The value of the header.
*/
get value() {
return this._value
}
}
/**
* Represents a cookie.
* Described in https://w3c.github.io/webdriver-bidi/#type-network-Cookie.
* @class
*/
class Cookie {
constructor(name, value, domain, path, size, httpOnly, secure, sameSite, expires) {
this._name = name
this._value = value
this._domain = domain
this._path = path
this._expires = expires
this._size = size
this._httpOnly = httpOnly
this._secure = secure
this._sameSite = sameSite
}
/**
* Gets the name of the cookie.
* @returns {string} The name of the cookie.
*/
get name() {
return this._name
}
/**
* Gets the value of the cookie.
* @returns {BytesValue} The value of the cookie.
*/
get value() {
return this._value
}
/**
* Gets the domain of the cookie.
* @returns {string} The domain of the cookie.
*/
get domain() {
return this._domain
}
/**
* Gets the path of the cookie.
* @returns {string} The path of the cookie.
*/
get path() {
return this._path
}
/**
* Gets the expiration date of the cookie.
* @returns {number} The expiration date of the cookie.
*/
get expires() {
return this._expires
}
/**
* Gets the size of the cookie.
* @returns {number} The size of the cookie.
*/
get size() {
return this._size
}
/**
* Checks if the cookie is HTTP-only.
* @returns {boolean} True if the cookie is HTTP-only, false otherwise.
*/
get httpOnly() {
return this._httpOnly
}
/**
* Checks if the cookie is secure.
* @returns {boolean} True if the cookie is secure, false otherwise.
*/
get secure() {
return this._secure
}
/**
* Gets the same-site attribute of the cookie.
* @returns {string} The same-site attribute of the cookie.
*/
get sameSite() {
return this._sameSite
}
}
// No tests written for FetchTimingInfo. Must be updated after browsers implment it and corresponding WPT test are written.
/**
* Represents the time of each part of the request.
* Described in https://w3c.github.io/webdriver-bidi/#type-network-FetchTimingInfo.
* @class
*/
class FetchTimingInfo {
constructor(
originTime,
requestTime,
redirectStart,
redirectEnd,
fetchStart,
dnsStart,
dnsEnd,
connectStart,
connectEnd,
tlsStart,
requestStart,
responseStart,
responseEnd,
) {
this._originTime = originTime
this._requestTime = requestTime
this._redirectStart = redirectStart
this._redirectEnd = redirectEnd
this._fetchStart = fetchStart
this._dnsStart = dnsStart
this._dnsEnd = dnsEnd
this._connectStart = connectStart
this._connectEnd = connectEnd
this._tlsStart = tlsStart
this._requestStart = requestStart
this._responseStart = responseStart
this._responseEnd = responseEnd
}
/**
* Gets the origin time.
*
* @returns {number} The origin time.
*/
get originTime() {
return this._originTime
}
/**
* Get the request time.
*
* @returns {number} The request time.
*/
get requestTime() {
return this._requestTime
}
/**
* Gets the timestamp when the redirect started.
*
* @returns {number} The timestamp when the redirect started.
*/
get redirectStart() {
return this._redirectStart
}
/**
* Gets the timestamp when the redirect ended.
*
* @returns {number} The timestamp when the redirect ended.
*/
get redirectEnd() {
return this._redirectEnd
}
/**
* Gets the timestamp when the fetch started.
*
* @returns {number} The timestamp when the fetch started.
*/
get fetchStart() {
return this._fetchStart
}
/**
* Gets the timestamp when the domain lookup started.
*
* @returns {number} The timestamp when the domain lookup started.
*/
get dnsStart() {
return this._dnsStart
}
/**
* Gets the timestamp when the domain lookup ended.
*
* @returns {number} The timestamp when the domain lookup ended.
*/
get dnsEnd() {
return this._dnsEnd
}
/**
* Gets the timestamp when the connection started.
*
* @returns {number} The timestamp when the connection ended.
*/
get connectStart() {
return this._connectStart
}
/**
* Gets the timestamp when the connection ended.
*
* @returns {number} The timestamp when the connection ended.
*/
get connectEnd() {
return this._connectEnd
}
/**
* Gets the timestamp when the secure connection started.
*
* @returns {number} The timestamp when the secure connection started.
*/
get tlsStart() {
return this._tlsStart
}
/**
* Gets the timestamp when the request started.
*
* @returns {number} The timestamp when the request started.
*/
get requestStart() {
return this._requestStart
}
/**
* Gets the timestamp when the response started.
*
* @returns {number} The timestamp when the response started.
*/
get responseStart() {
return this._responseStart
}
/**
* Gets the timestamp when the response ended.
*
* @returns {number} The timestamp when the response ended.
*/
get responseEnd() {
return this._responseEnd
}
}
/**
* Represents the data of a network request.
* Described in https://w3c.github.io/webdriver-bidi/#type-network-RequestData.
*/
class RequestData {
constructor(request, url, method, headers, cookies, headersSize, bodySize, timings) {
this._request = request
this._url = url
this._method = method
this._headers = []
headers.forEach((header) => {
let name = header.name
let value = 'value' in header ? header.value : null
this._headers.push(new Header(name, new BytesValue(value.type, value.value)))
})
this._cookies = []
cookies.forEach((cookie) => {
let name = cookie.name
let domain = cookie.domain
let path = cookie.path
let size = cookie.size
let httpOnly = cookie.httpOnly
let secure = cookie.secure
let sameSite = cookie.sameSite
let value = 'value' in cookie ? cookie.value : null
let expires = 'expires' in cookie ? cookie.expires : null
this._cookies.push(new Cookie(name, value, domain, path, size, httpOnly, secure, sameSite, expires))
})
this._headersSize = headersSize
this._bodySize = bodySize
this._timings = new FetchTimingInfo(
timings.originTime,
timings.requestTime,
timings.redirectStart,
timings.redirectEnd,
timings.fetchStart,
timings.dnsStart,
timings.dnsEnd,
timings.connectStart,
timings.connectEnd,
timings.tlsStart,
timings.requestStart,
timings.responseStart,
timings.responseEnd,
)
}
/**
* Get the request id.
* @returns {string} The request id.
*/
get request() {
return this._request
}
/**
* Get the URL of the request.
* @returns {string} The URL of the request.
*/
get url() {
return this._url
}
/**
* Get the HTTP method of the request.
* @returns {string} The HTTP method of the request.
*/
get method() {
return this._method
}
/**
* Get the headers of the request.
* @returns {Header[]} An array of header objects.
*/
get headers() {
return this._headers
}
/**
* Get the cookies of the request.
* @returns {Cookie[]} An array of cookie objects.
*/
get cookies() {
return this._cookies
}
/**
* Get the size of the headers in bytes.
* @returns {number} The size of the headers in bytes.
*/
get headersSize() {
return this._headersSize
}
/**
* Get the size of the request body in bytes.
* @returns {number} The size of the request body in bytes.
*/
get bodySize() {
return this._bodySize
}
/**
* Get the timing information of the request.
* @returns {FetchTimingInfo} The timing information of the request.
*/
get timings() {
return this._timings
}
}
/**
* Represents the base parameters for a network request.
* Described in https://w3c.github.io/webdriver-bidi/#type-network-BaseParameters.
*/
class BaseParameters {
constructor(id, navigation, redirectCount, request, timestamp) {
this._id = id
this._navigation =
navigation != null
? new NavigationInfo(navigation.context, navigation.navigation, navigation.timestamp, navigation.url)
: null
this._redirectCount = redirectCount
this._request = new RequestData(
request.request,
request.url,
request.method,
request.headers,
request.cookies,
request.headersSize,
request.bodySize,
request.timings,
)
this._timestamp = timestamp
}
/**
* Gets the browsing context ID of the network request.
* @returns {string|null} The browsing context ID of the network request.
*/
get id() {
return this._id
}
/**
* Gets the navigation information associated with the network request.
* @returns {NavigationInfo|null} The navigation information associated with the network request, or null if not available.
*/
get navigation() {
return this._navigation
}
/**
* Gets the number of redirects that occurred during the network request.
* @returns {number} The number of redirects that occurred during the network request.
*/
get redirectCount() {
return this._redirectCount
}
/**
* Gets the request data for the network request.
* @returns {RequestData} The request data for the network request.
*/
get request() {
return this._request
}
/**
* Gets the timestamp of the network request.
* @returns {number} The timestamp of the network request.
*/
get timestamp() {
return this._timestamp
}
}
/**
* Represents source in the network.
* Described in https://w3c.github.io/webdriver-bidi/#type-network-Initiator.
*/
class Initiator {
/**
* Constructs a new Initiator instance.
* @param {string} type - The type of the initiator.
* @param {number} columnNumber - The column number.
* @param {number} lineNumber - The line number.
* @param {string} stackTrace - The stack trace.
* @param {string} request - The request id.
*/
constructor(type, columnNumber, lineNumber, stackTrace, request) {
this._type = type
this._columnNumber = columnNumber
this._lineNumber = lineNumber
this._stackTrace = stackTrace
this._request = request
}
/**
* Gets the type of the initiator.
* @returns {string} The type of the initiator.
*/
get type() {
return this._type
}
/**
* Gets the column number.
* @returns {number} The column number.
*/
get columnNumber() {
return this._columnNumber
}
/**
* Gets the line number.
* @returns {number} The line number.
*/
get lineNumber() {
return this._lineNumber
}
/**
* Gets the stack trace.
* @returns {string} The stack trace.
*/
get stackTrace() {
return this._stackTrace
}
/**
* Gets the request ID.
* @returns {string} The request ID.
*/
get request() {
return this._request
}
}
/**
* Represents the BeforeRequestSent event parameters.
* @class
* @extends BaseParameters
* Described in https://w3c.github.io/webdriver-bidi/#event-network-beforeSendRequest.
*/
class BeforeRequestSent extends BaseParameters {
constructor(id, navigation, redirectCount, request, timestamp, initiator) {
super(id, navigation, redirectCount, request, timestamp)
this._initiator = new Initiator(
initiator.type,
initiator.columnNumber,
initiator.lineNumber,
initiator.stackTrace,
initiator.request,
)
}
/**
* Get the initiator of the request.
* @returns {Initiator} The initiator object.
*/
get initiator() {
return this._initiator
}
}
/**
* Represents the FetchError event parameters.
* Described https://w3c.github.io/webdriver-bidi/#event-network-fetchError
* @extends BaseParameters
*/
class FetchError extends BaseParameters {
/**
* Creates a new FetchError instance.
* @param {string} id - The ID of the error.
* @param {string} navigation - The navigation information.
* @param {number} redirectCount - The number of redirects.
* @param {RequestData} request - The request object.
* @param {number} timestamp - The timestamp of the error.
* @param {string} errorText - The error text.
*/
constructor(id, navigation, redirectCount, request, timestamp, errorText) {
super(id, navigation, redirectCount, request, timestamp)
this._errorText = errorText
}
/**
* Gets the error text.
* @returns {string} The error text.
*/
get errorText() {
return this._errorText
}
}
/**
* Represents the response data received from a network request.
* Described in https://w3c.github.io/webdriver-bidi/#type-network-ResponseData.
* @class
*/
class ResponseData {
constructor(
url,
protocol,
status,
statusText,
fromCache,
headers,
mimeType,
bytesReceived,
headersSize,
bodySize,
content,
) {
this._url = url
this._protocol = protocol
this._status = status
this._statusText = statusText
this._fromCache = fromCache
this._headers = headers
this._mimeType = mimeType
this._bytesReceived = bytesReceived
this._headersSize = headersSize
this._bodySize = bodySize
this._content = content
}
/**
* Get the URL.
*
* @returns {string} The URL.
*/
get url() {
return this._url
}
/**
* Get the protocol.
*
* @returns {string} The protocol.
*/
get protocol() {
return this._protocol
}
/**
* Get the HTTP status.
*
* @returns {string} The HTTP status.
*/
get status() {
return this._status
}
/**
* Gets the status text.
*
* @returns {string} The status text.
*/
get statusText() {
return this._statusText
}
/**
* Gets the value indicating whether the data is retrieved from cache.
*
* @returns {boolean} The value indicating whether the data is retrieved from cache.
*/
get fromCache() {
return this._fromCache
}
/**
* Get the headers.
*
* @returns {Object} The headers object.
*/
get headers() {
return this._headers
}
/**
* The MIME type of the network resource.
*
* @type {string}
*/
get mimeType() {
return this._mimeType
}
/**
* Gets the number of bytes received.
*
* @returns {number} The number of bytes received.
*/
get bytesReceived() {
return this._bytesReceived
}
/**
* Get the size of the headers.
*
* @returns {number} The size of the headers.
*/
get headerSize() {
return this._headersSize
}
/**
* Get the size of the body.
*
* @returns {number} The size of the body.
*/
get bodySize() {
return this._bodySize
}
/**
* Gets the content.
*
* @returns {any} The content.
*/
get content() {
return this._content
}
}
/**
* Represents the ResponseStarted event parameters.
* Described in https://w3c.github.io/webdriver-bidi/#event-network-responseStarted.
* @class
* @extends BaseParameters
*/
class ResponseStarted extends BaseParameters {
constructor(id, navigation, redirectCount, request, timestamp, response) {
super(id, navigation, redirectCount, request, timestamp)
this._response = new ResponseData(
response.url,
response.protocol,
response.status,
response.statusText,
response.fromCache,
response.headers,
response.mimeType,
response.bytesReceived,
response.headerSize,
response.bodySize,
response.content,
)
}
/**
* Get the response data.
* @returns {ResponseData} The response data.
*/
get response() {
return this._response
}
}
module.exports = { Header, BytesValue, Cookie, SameSite, BeforeRequestSent, ResponseStarted, FetchError }

View File

@@ -0,0 +1,115 @@
// 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 { BytesValue } = require('./networkTypes')
/**
* Represents a partial cookie used to set cookies.
* Described in https://w3c.github.io/webdriver-bidi/#command-storage-setCookie.
* @class
*/
class PartialCookie {
#map = new Map()
/**
* Represents a partial cookie.
* @class
* @param {string} name - The name of the cookie.
* @param {BytesValue} value - The value of the cookie as an instance of BytesValue.
* @param {string} domain - The domain of the cookie.
*/
constructor(name, value, domain) {
this.#map.set('name', name)
if (!(value instanceof BytesValue)) {
throw new Error(`Value must be an instance of BytesValue. Received:'${value}'`)
}
this.#map.set('value', Object.fromEntries(value.asMap()))
this.#map.set('domain', domain)
}
/**
* Sets the path for the cookie.
*
* @param {string} path - The path for the cookie.
* @returns {PartialCookie} - The updated PartialCookie instance for chaining.
*/
path(path) {
this.#map.set('path', path)
return this
}
/**
* Sets the size of the cookie.
*
* @param {number} size - The size of the cookie.
* @returns {PartialCookie} - The updated PartialCookie instance for chaining.
*/
size(size) {
this.#map.set('size', size)
return this
}
/**
* Sets the `httpOnly` flag for the cookie.
*
* @param {boolean} httpOnly - The value to set for the `httpOnly` flag.
* @returns {PartialCookie} - The updated PartialCookie instance for chaining.
*/
httpOnly(httpOnly) {
this.#map.set('httpOnly', httpOnly)
return this
}
/**
* Sets the secure flag for the cookie.
*
* @param {boolean} secure - Indicates whether the cookie should only be sent over secure connections.
* @returns {PartialCookie} - The updated PartialCookie instance for chaining.
*/
secure(secure) {
this.#map.set('secure', secure)
return this
}
/**
* Sets the SameSite attribute for the cookie.
*
* @param {SameSite} sameSite - The SameSite attribute value for the cookie.
* @returns {PartialCookie} - The updated PartialCookie instance for chaining.
*/
sameSite(sameSite) {
this.#map.set('sameSite', sameSite)
return this
}
/**
* Sets the expiry for the cookie.
*
* @param {number} expiry - The expiry time of the cookie.
* @returns {PartialCookie} - The updated PartialCookie instance for chaining.
*/
expiry(expiry) {
this.#map.set('expiry', expiry)
return this
}
asMap() {
return this.#map
}
}
module.exports = { PartialCookie }

View File

@@ -0,0 +1,102 @@
// 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.
/**
* Represents the types of partition descriptors.
* @enum {string}
* Described in https://w3c.github.io/webdriver-bidi/#command-storage-getCookies.
*/
const Type = {
CONTEXT: 'context',
STORAGE_KEY: 'storageKey',
}
/**
* Represents a partition descriptor.
* Described in https://w3c.github.io/webdriver-bidi/#command-storage-getCookies.
*/
class PartitionDescriptor {
/*eslint no-unused-private-class-members: "off"*/
#type
/**
* Constructs a new PartitionDescriptor instance.
* @param {Type} type - The type of the partition.
*/
constructor(type) {
this.#type = type
}
}
/**
* Represents a partition descriptor for a browsing context.
* @extends PartitionDescriptor
*/
class BrowsingContextPartitionDescriptor extends PartitionDescriptor {
#context = null
constructor(context) {
super(Type.CONTEXT)
this.#context = context
}
asMap() {
const map = new Map()
map.set('type', Type.CONTEXT)
map.set('context', this.#context)
return map
}
}
/**
* Represents a partition descriptor for storage key.
* @extends PartitionDescriptor
*/
class StorageKeyPartitionDescriptor extends PartitionDescriptor {
#map = new Map()
constructor() {
super(Type.STORAGE_KEY)
}
/**
* Sets the user context for the partition descriptor.
* @param {any} userContext - The user context to set.
* @returns {PartitionDescriptor} - The updated partition descriptor instance for chaining.
*/
userContext(userContext) {
this.#map.set('userContext', userContext)
return this
}
/**
* Sets the source origin for the partition descriptor.
*
* @param {string} sourceOrigin - The source origin to set.
* @returns {PartitionDescriptor} - The updated PartitionDescriptor instance for chaining.
*/
sourceOrigin(sourceOrigin) {
this.#map.set('sourceOrigin', sourceOrigin)
return this
}
asMap() {
return this.#map
}
}
module.exports = { BrowsingContextPartitionDescriptor, StorageKeyPartitionDescriptor }

View File

@@ -0,0 +1,53 @@
// 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.
/**
* Represents a partition key of cookie storage.
* Described in https://w3c.github.io/webdriver-bidi/#type-storage-PartitionKey.
*/
class PartitionKey {
#userContext
#sourceOrigin
/**
* Constructs a new PartitionKey object.
* @param {string} userContext - The user context.
* @param {string} sourceOrigin - The source origin.
*/
constructor(userContext, sourceOrigin) {
this.#userContext = userContext
this.#sourceOrigin = sourceOrigin
}
/**
* Gets the user context.
* @returns {string} The user context.
*/
get userContext() {
return this.#userContext
}
/**
* Gets the source origin.
* @returns {string} The source origin.
*/
get sourceOrigin() {
return this.#sourceOrigin
}
}
module.exports = { PartitionKey }

View File

@@ -0,0 +1,112 @@
// 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.
/**
* Represents a primitive type.
* @enum
* Described in https://w3c.github.io/webdriver-bidi/#type-script-PrimitiveProtocolValue.
*/
const PrimitiveType = {
UNDEFINED: 'undefined',
NULL: 'null',
STRING: 'string',
NUMBER: 'number',
SPECIAL_NUMBER: 'number',
BOOLEAN: 'boolean',
BIGINT: 'bigint',
findByName(name) {
return (
Object.values(this).find((type) => {
return typeof type === 'string' && name.toLowerCase() === type.toLowerCase()
}) || null
)
},
}
/**
* Represents a non-primitive type.
* @enum
* Described inhttps://w3c.github.io/webdriver-bidi/#type-script-RemoteValue.
*/
const NonPrimitiveType = {
ARRAY: 'array',
DATE: 'date',
MAP: 'map',
OBJECT: 'object',
REGULAR_EXPRESSION: 'regexp',
SET: 'set',
CHANNEL: 'channel',
findByName(name) {
return (
Object.values(this).find((type) => {
return typeof type === 'string' && name.toLowerCase() === type.toLowerCase()
}) || null
)
},
}
/**
* Represents a remote value type.
* @enum
* Described inhttps://w3c.github.io/webdriver-bidi/#type-script-RemoteValue.
*/
const RemoteType = {
SYMBOL: 'symbol',
FUNCTION: 'function',
WEAK_MAP: 'weakmap',
WEAK_SET: 'weakset',
ITERATOR: 'iterator',
GENERATOR: 'generator',
ERROR: 'error',
PROXY: 'proxy',
PROMISE: 'promise',
TYPED_ARRAY: 'typedarray',
ARRAY_BUFFER: 'arraybuffer',
NODE_LIST: 'nodelist',
HTML_COLLECTION: 'htmlcollection',
NODE: 'node',
WINDOW: 'window',
findByName(name) {
return (
Object.values(this).find((type) => {
return typeof type === 'string' && name.toLowerCase() === type.toLowerCase()
}) || null
)
},
}
/**
* Represents a speacial number type.
* @enum
* Described in https://w3c.github.io/webdriver-bidi/#type-script-PrimitiveProtocolValue.
*/
const SpecialNumberType = {
NAN: 'NaN',
MINUS_ZERO: '-0',
INFINITY: 'Infinity',
MINUS_INFINITY: '-Infinity',
}
module.exports = {
PrimitiveType,
NonPrimitiveType,
RemoteType,
SpecialNumberType,
}

View File

@@ -0,0 +1,366 @@
// 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 { PrimitiveType, NonPrimitiveType, RemoteType } = require('./protocolType')
const TYPE_CONSTANT = 'type'
const VALUE_CONSTANT = 'value'
/**
* Represents the types of remote reference.
* @enum {string}
*/
const RemoteReferenceType = {
HANDLE: 'handle',
SHARED_ID: 'sharedId',
}
/**
* Represents a local value with a specified type and optional value.
* @class
* Described in https://w3c.github.io/webdriver-bidi/#type-script-LocalValue
*/
class LocalValue {
constructor(type, value = null) {
if (type === PrimitiveType.UNDEFINED || type === PrimitiveType.NULL) {
this.type = type
} else {
this.type = type
this.value = value
}
}
/**
* Creates a new LocalValue object with a string value.
*
* @param {string} value - The string value to be stored in the LocalValue object.
* @returns {LocalValue} - The created LocalValue object.
*/
static createStringValue(value) {
return new LocalValue(PrimitiveType.STRING, value)
}
/**
* Creates a new LocalValue object with a number value.
*
* @param {number} value - The number value.
* @returns {LocalValue} - The created LocalValue object.
*/
static createNumberValue(value) {
return new LocalValue(PrimitiveType.NUMBER, value)
}
/**
* Creates a new LocalValue object with a special number value.
*
* @param {number} value - The value of the special number.
* @returns {LocalValue} - The created LocalValue object.
*/
static createSpecialNumberValue(value) {
return new LocalValue(PrimitiveType.SPECIAL_NUMBER, value)
}
/**
* Creates a new LocalValue object with an undefined value.
* @returns {LocalValue} - The created LocalValue object.
*/
static createUndefinedValue() {
return new LocalValue(PrimitiveType.UNDEFINED)
}
/**
* Creates a new LocalValue object with a null value.
* @returns {LocalValue} - The created LocalValue object.
*/
static createNullValue() {
return new LocalValue(PrimitiveType.NULL)
}
/**
* Creates a new LocalValue object with a boolean value.
*
* @param {boolean} value - The boolean value.
* @returns {LocalValue} - The created LocalValue object.
*/
static createBooleanValue(value) {
return new LocalValue(PrimitiveType.BOOLEAN, value)
}
/**
* Creates a new LocalValue object with a BigInt value.
*
* @param {BigInt} value - The BigInt value.
* @returns {LocalValue} - The created LocalValue object.
*/
static createBigIntValue(value) {
return new LocalValue(PrimitiveType.BIGINT, value)
}
/**
* Creates a new LocalValue object with an array.
*
* @param {Array} value - The array.
* @returns {LocalValue} - The created LocalValue object.
*/
static createArrayValue(value) {
return new LocalValue(NonPrimitiveType.ARRAY, value)
}
/**
* Creates a new LocalValue object with date value.
*
* @param {string} value - The date.
* @returns {LocalValue} - The created LocalValue object.
*/
static createDateValue(value) {
return new LocalValue(NonPrimitiveType.DATE, value)
}
/**
* Creates a new LocalValue object of map value.
* @param {Map} map - The map.
* @returns {LocalValue} - The created LocalValue object.
*/
static createMapValue(map) {
let value = []
Object.entries(map).forEach((entry) => {
value.push(entry)
})
return new LocalValue(NonPrimitiveType.MAP, value)
}
/**
* Creates a new LocalValue object from the passed object.
*
* @param {Object} map - The object.
* @returns {LocalValue} - The created LocalValue object.
*/
static createObjectValue(object) {
let value = []
Object.entries(object).forEach((entry) => {
value.push(entry)
})
return new LocalValue(NonPrimitiveType.OBJECT, value)
}
/**
* Creates a new LocalValue object of regular expression value.
*
* @param {string} value - The value of the regular expression.
* @returns {LocalValue} - The created LocalValue object.
*/
static createRegularExpressionValue(value) {
return new LocalValue(NonPrimitiveType.REGULAR_EXPRESSION, value)
}
/**
* Creates a new LocalValue object with the specified value.
* @param {Set} value - The value to be set.
* @returns {LocalValue} - The created LocalValue object.
*/
static createSetValue(value) {
return new LocalValue(NonPrimitiveType.SET, value)
}
/**
* Creates a new LocalValue object with the given channel value
*
* @param {ChannelValue} value - The channel value.
* @returns {LocalValue} - The created LocalValue object.
*/
static createChannelValue(value) {
return new LocalValue(NonPrimitiveType.CHANNEL, value)
}
toJson() {
let toReturn = {}
toReturn[TYPE_CONSTANT] = this.type
if (!(this.type === PrimitiveType.NULL || this.type === PrimitiveType.UNDEFINED)) {
toReturn[VALUE_CONSTANT] = this.value
}
return toReturn
}
}
/**
* Represents a remote value.
* Described in https://w3c.github.io/webdriver-bidi/#type-script-RemoteValue.
* @class
*/
class RemoteValue {
constructor(remoteValue) {
this.type = null
this.handle = null
this.internalId = null
this.value = null
this.sharedId = null
if ('type' in remoteValue) {
const typeString = remoteValue['type']
if (PrimitiveType.findByName(typeString) != null) {
this.type = PrimitiveType.findByName(typeString)
} else if (NonPrimitiveType.findByName(typeString) != null) {
this.type = NonPrimitiveType.findByName(typeString)
} else {
this.type = RemoteType.findByName(typeString)
}
}
if ('handle' in remoteValue) {
this.handle = remoteValue['handle']
}
if ('internalId' in remoteValue) {
this.internalId = remoteValue['internalId']
}
if ('value' in remoteValue) {
this.value = remoteValue['value']
}
if ('sharedId' in remoteValue) {
this.sharedId = remoteValue['sharedId']
}
if (this.value != null) {
this.value = this.deserializeValue(this.value, this.type)
}
}
deserializeValue(value, type) {
if ([NonPrimitiveType.MAP, NonPrimitiveType.OBJECT].includes(type)) {
return Object.fromEntries(value)
} else if (type === NonPrimitiveType.REGULAR_EXPRESSION) {
return new RegExpValue(value.pattern, value.flags)
}
return value
}
}
/**
* Represents a reference value in the protocol.
* Described in https://w3c.github.io/webdriver-bidi/#type-script-RemoteReference.
*/
class ReferenceValue {
#handle
#sharedId
/**
* Constructs a new ReferenceValue object.
* @param {string} handle - The handle value.
* @param {string} sharedId - The shared ID value.
*/
constructor(handle, sharedId) {
if (handle === RemoteReferenceType.HANDLE) {
this.#handle = sharedId
} else if (handle === RemoteReferenceType.SHARED_ID) {
this.#sharedId = sharedId
} else {
this.#handle = handle
this.#sharedId = sharedId
}
}
asMap() {
const toReturn = {}
if (this.#handle != null) {
toReturn[RemoteReferenceType.HANDLE] = this.#handle
}
if (this.#sharedId != null) {
toReturn[RemoteReferenceType.SHARED_ID] = this.#sharedId
}
return toReturn
}
}
/**
* Represents a regular expression value.
* Described in https://w3c.github.io/webdriver-bidi/#type-script-LocalValue.
*/
class RegExpValue {
/**
* Constructs a new RegExpValue object.
* @param {string} pattern - The pattern of the regular expression.
* @param {string|null} [flags=null] - The flags of the regular expression.
*/
constructor(pattern, flags = null) {
this.pattern = pattern
this.flags = flags
}
}
/**
* Represents serialization options.
* Described in https://w3c.github.io/webdriver-bidi/#type-script-SerializationOptions.
*/
class SerializationOptions {
/**
* Constructs a new instance of SerializationOptions.
* @param {number} [maxDomDepth=0] - The maximum depth to serialize the DOM.
* @param {number|null} [maxObjectDepth=null] - The maximum depth to serialize objects.
* @param {'none'|'open'|'all'} [includeShadowTree='none'] - The inclusion level of the shadow tree.
* @throws {Error} If the `includeShadowTree` value is not one of 'none', 'open', or 'all'.
*/
constructor(maxDomDepth = 0, maxObjectDepth = null, includeShadowTree = 'none') {
this._maxDomDepth = maxDomDepth
this._maxObjectDepth = maxObjectDepth
if (['none', 'open', 'all'].includes(includeShadowTree)) {
throw Error(`Valid types are 'none', 'open', and 'all'. Received: ${includeShadowTree}`)
}
this._includeShadowTree = includeShadowTree
}
}
/**
* Represents a channel value.
* Described in https://w3c.github.io/webdriver-bidi/#type-script-ChannelValue.
* @class
*/
class ChannelValue {
constructor(channel, options = undefined, resultOwnership = undefined) {
this.channel = channel
if (options !== undefined) {
if (options instanceof SerializationOptions) {
this.options = options
} else {
throw Error(`Pass in SerializationOptions object. Received: ${options} `)
}
}
if (resultOwnership != undefined) {
if (['root', 'none'].includes(resultOwnership)) {
this.resultOwnership = resultOwnership
} else {
throw Error(`Valid types are 'root' and 'none. Received: ${resultOwnership}`)
}
}
}
}
module.exports = {
ChannelValue,
LocalValue,
RemoteValue,
ReferenceValue,
RemoteReferenceType,
RegExpValue,
SerializationOptions,
}

View File

@@ -0,0 +1,123 @@
// 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 { BytesValue, Header } = require('./networkTypes')
/**
* Represents parameters for providingResponse command.
* Described in https://w3c.github.io/webdriver-bidi/#command-network-provideResponse.
* @class
*/
class ProvideResponseParameters {
#map = new Map()
constructor(request) {
this.#map.set('request', request)
}
/**
* Sets the body value for the response parameters.
*
* @param {BytesValue} value - The value to set as the body. Must be an instance of BytesValue.
* @returns {ProvideResponseParameters} - Returns the ProvideResponseParameters object for chaining.
* @throws {Error} - Throws an error if the value is not an instance of BytesValue.
*/
body(value) {
if (!(value instanceof BytesValue)) {
throw new Error(`Value must be an instance of BytesValue. Received: ${typeof value} with value: ${value}`)
}
this.#map.set('body', Object.fromEntries(value.asMap()))
return this
}
/**
* Sets the cookie headers for the response.
*
* @param {Header[]} cookieHeaders - An array of cookie headers.
* @returns {ProvideResponseParameters} - Returns the ProvideResponseParameters object for chaining.
* @throws {Error} - Throws an error if a cookie header is not an instance of Header.
*/
cookies(cookieHeaders) {
const cookies = []
cookieHeaders.forEach((header) => {
if (!(header instanceof Header)) {
throw new Error(`CookieHeader must be an instance of Header. Received:'${header}'`)
}
cookies.push(Object.fromEntries(header.asMap()))
})
this.#map.set('cookies', cookies)
return this
}
/**
* Sets the headers for the response.
*
* @param {Header[]} headers - The headers to be set.
* @returns {ProvideResponseParameters} - Returns the ProvideResponseParameters object for chaining.
* @throws {Error} - If the provided header is not an instance of Header.
*/
headers(headers) {
const headerList = []
headers.forEach((header) => {
if (!(header instanceof Header)) {
throw new Error(`Header must be an instance of Header. Received:'${header}'`)
}
headerList.push(Object.fromEntries(header.asMap()))
})
this.#map.set('headers', headerList)
return this
}
/**
* Sets the reason phrase for the response.
*
* @param {string} reasonPhrase - The reason phrase to set.
* @returns {ProvideResponseParameters} - Returns the ProvideResponseParameters object for chaining.
* @throws {Error} - If the reason phrase is not a string.
*/
reasonPhrase(reasonPhrase) {
if (typeof reasonPhrase !== 'string') {
throw new Error(`Reason phrase must be a string. Received: '${reasonPhrase})'`)
}
this.#map.set('reasonPhrase', reasonPhrase)
return this
}
/**
* Sets the status code for the response.
*
* @param {number} statusCode - The status code to set.
* @returns {ProvideResponseParameters} - Returns the ProvideResponseParameters object for chaining.
* @throws {Error} - If the status code is not an integer.
*/
statusCode(statusCode) {
if (!Number.isInteger(statusCode)) {
throw new Error(`Status must be an integer. Received:'${statusCode}'`)
}
this.#map.set('statusCode', statusCode)
return this
}
asMap() {
return this.#map
}
}
module.exports = { ProvideResponseParameters }

View File

@@ -0,0 +1,119 @@
// 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.
/**
* Represents the types of realms.
* Described in https://w3c.github.io/webdriver-bidi/#type-script-RealmType.
* @enum
*/
const RealmType = {
AUDIO_WORKLET: 'audio-worklet',
DEDICATED_WORKER: 'dedicated-worker',
PAINT_WORKLET: 'paint-worklet',
SERVICE_WORKED: 'service-worker',
SHARED_WORKED: 'shared-worker',
WINDOW: 'window',
WORKER: 'worker',
WORKLET: 'worklet',
findByName(name) {
return (
Object.values(this).find((type) => {
return typeof type === 'string' && name.toLowerCase() === type.toLowerCase()
}) || null
)
},
}
/**
* Represents information about a realm.
* Described in https://w3c.github.io/webdriver-bidi/#type-script-RealmInfo.
*/
class RealmInfo {
/**
* Constructs a new RealmInfo object.
* @param {string} realmId - The ID of the realm.
* @param {string} origin - The origin of the realm.
* @param {string} realmType - The type of the realm.
*/
constructor(realmId, origin, realmType) {
this.realmId = realmId
this.origin = origin
this.realmType = realmType
}
static fromJson(input) {
let realmId = null
let origin = null
let realmType = null
let browsingContext = null
let sandbox = null
if ('type' in input) {
let typeString = input['type']
realmType = RealmType.findByName(typeString)
}
if ('realm' in input) {
realmId = input['realm']
}
if ('origin' in input) {
origin = input['origin']
}
if ('context' in input) {
browsingContext = input['context']
}
if ('sandbox' in input) {
sandbox = input['sandbox']
}
if (realmType === RealmType.WINDOW) {
return new WindowRealmInfo(realmId, origin, realmType, browsingContext, sandbox)
}
return new RealmInfo(realmId, origin, realmType)
}
}
/**
* Represents information about a window realm.
* @extends RealmInfo
*/
class WindowRealmInfo extends RealmInfo {
/**
* Constructs a new instance of the WindowRealmInfo class.
* @param {string} realmId - The ID of the realm.
* @param {string} origin - The origin of the realm.
* @param {string} realmType - The type of the realm.
* @param {string} browsingContext - The browsing context of the realm.
* @param {string|null} sandbox - The sandbox of the realm (optional).
*/
constructor(realmId, origin, realmType, browsingContext, sandbox = null) {
super(realmId, origin, realmType)
this.browsingContext = browsingContext
this.sandbox = sandbox
}
}
module.exports = {
RealmInfo,
RealmType,
WindowRealmInfo,
}

View File

@@ -0,0 +1,27 @@
// 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.
/**
* Enum representing the ownership types.
* @enum {string}
*/
const ResultOwnership = {
ROOT: 'root',
NONE: 'none',
}
module.exports = { ResultOwnership }

View File

@@ -0,0 +1,497 @@
// 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')
/**
* Represents class to run events and commands of Script module.
* Described in https://w3c.github.io/webdriver-bidi/#module-script.
* @class
*/
class ScriptManager {
constructor(driver) {
this._driver = driver
}
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)
}
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) {
await this.subscribeAndHandleEvent('script.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) {
await this.subscribeAndHandleEvent('script.realmCreated', 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) {
await this.subscribeAndHandleEvent('script.realmDestroyed', callback)
}
async subscribeAndHandleEvent(eventType, callback) {
if (this.browsingContextIds != null) {
await this.bidi.subscribe(eventType, this.browsingContextIds)
} else {
await this.bidi.subscribe(eventType)
}
await this._on(callback)
}
async _on(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
}
}
callback(response)
}
})
}
}
async function getScriptManagerInstance(browsingContextId, driver) {
let instance = new ScriptManager(driver)
await instance.init(browsingContextId)
return instance
}
module.exports = getScriptManagerInstance

View File

@@ -0,0 +1,94 @@
// 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.
/**
* Represents a message received through a channel.
* Described in https://w3c.github.io/webdriver-bidi/#event-script-message.
* @class
*/
class Message {
/**
* Creates a new Message instance.
* @param {string} channel - The channel through which the message is received.
* @param {RemoteValue} data - The data contained in the message.
* @param {Source} source - The source of the message.
*/
constructor(channel, data, source) {
this._channel = channel
this._data = data
this._source = source
}
/**
* Gets the channel through which the message is received.
* @returns {string} The channel.
*/
get channel() {
return this._channel
}
/**
* Gets the data contained in the message.
* @returns {RemoteValue} The data.
*/
get data() {
return this._data
}
/**
* Gets the source of the message.
* @returns {Source} The source.
*/
get source() {
return this._source
}
}
/**
* Represents a source object.
* Described in https://w3c.github.io/webdriver-bidi/#type-script-Source.
* @class
*/
class Source {
constructor(source) {
this._browsingContextId = null
this._realmId = source.realm
// Browsing context is returned as an optional parameter
if ('context' in source) {
this._browsingContextId = source.context
}
}
/**
* Get the browsing context ID.
* @returns {string|null} The browsing context ID.
*/
get browsingContextId() {
return this._browsingContextId
}
/**
* Get the realm ID.
* @returns {string} The realm ID.
*/
get realmId() {
return this._realmId
}
}
module.exports = { Message, Source }

View File

@@ -0,0 +1,201 @@
// 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 { CookieFilter } = require('./cookieFilter')
const { BrowsingContextPartitionDescriptor, StorageKeyPartitionDescriptor } = require('./partitionDescriptor')
const { PartitionKey } = require('./partitionKey')
const { PartialCookie } = require('./partialCookie')
const { Cookie, BytesValue } = require('./networkTypes')
/**
* Represents commands of Storage module.
* Described in https://w3c.github.io/webdriver-bidi/#module-storage.
* @class
*/
class Storage {
constructor(driver) {
this._driver = driver
}
async init() {
if (!(await this._driver.getCapabilities()).get('webSocketUrl')) {
throw Error('WebDriver instance must support BiDi protocol')
}
this.bidi = await this._driver.getBidi()
}
/**
* Retrieves cookies based on the provided filter and partition.
*
* @param {CookieFilter} [filter] - The filter to apply to the cookies.
* @param {(BrowsingContextPartitionDescriptor|StorageKeyPartitionDescriptor)} [partition] - The partition to retrieve cookies from.
* @returns {Promise<{ cookies: Cookie[], partitionKey?: PartitionKey }>} - A promise that resolves to an object containing the retrieved cookies and an optional partition key.
* @throws {Error} If the filter parameter is provided but is not an instance of CookieFilter.
* @throws {Error} If the partition parameter is provided but is not an instance of BrowsingContextPartitionDescriptor or StorageKeyPartitionDescriptor.
*/
async getCookies(filter = undefined, partition = undefined) {
if (filter !== undefined && !(filter instanceof CookieFilter)) {
throw new Error(`Params must be an instance of CookieFilter. Received:'${filter}'`)
}
if (
partition !== undefined &&
!(partition instanceof (BrowsingContextPartitionDescriptor || StorageKeyPartitionDescriptor))
) {
throw new Error(`Params must be an instance of PartitionDescriptor. Received:'${partition}'`)
}
const command = {
method: 'storage.getCookies',
params: {
filter: filter ? Object.fromEntries(filter.asMap()) : undefined,
partition: partition ? Object.fromEntries(partition.asMap()) : undefined,
},
}
let response = await this.bidi.send(command)
let cookies = []
response.result.cookies.forEach((cookie) => {
cookies.push(
new Cookie(
cookie.name,
new BytesValue(cookie.value.type, cookie.value.value),
cookie.domain,
cookie.path,
cookie.size,
cookie.httpOnly,
cookie.secure,
cookie.sameSite,
cookie.expiry,
),
)
})
if (Object.prototype.hasOwnProperty.call(response.result, 'partitionKey')) {
if (
Object.prototype.hasOwnProperty.call(response.result.partitionKey, 'userContext') &&
Object.prototype.hasOwnProperty.call(response.result.partitionKey, 'sourceOrigin')
) {
let partitionKey = new PartitionKey(
response.result.partitionKey.userContext,
response.result.partitionKey.sourceOrigin,
)
return { cookies, partitionKey }
}
return { cookies }
}
}
/**
* Sets a cookie using the provided cookie object and partition.
*
* @param {PartialCookie} cookie - The cookie object to set.
* @param {(BrowsingContextPartitionDescriptor|StorageKeyPartitionDescriptor)} [partition] - The partition to use for the cookie.
* @returns {PartitionKey} The partition key of the set cookie.
* @throws {Error} If the cookie parameter is not an instance of PartialCookie or if the partition parameter is not an instance of PartitionDescriptor.
*/
async setCookie(cookie, partition = undefined) {
if (!(cookie instanceof PartialCookie)) {
throw new Error(`Params must be an instance of PartialCookie. Received:'${cookie}'`)
}
if (
partition !== undefined &&
!(partition instanceof (BrowsingContextPartitionDescriptor || StorageKeyPartitionDescriptor))
) {
throw new Error(`Params must be an instance of PartitionDescriptor. Received:'${partition}'`)
}
const command = {
method: 'storage.setCookie',
params: {
cookie: cookie ? Object.fromEntries(cookie.asMap()) : undefined,
partition: partition ? Object.fromEntries(partition.asMap()) : undefined,
},
}
let response = await this.bidi.send(command)
if (Object.prototype.hasOwnProperty.call(response.result, 'partitionKey')) {
if (
Object.prototype.hasOwnProperty.call(response.result.partitionKey, 'userContext') &&
Object.prototype.hasOwnProperty.call(response.result.partitionKey, 'sourceOrigin')
) {
let partitionKey = new PartitionKey(
response.result.partitionKey.userContext,
response.result.partitionKey.sourceOrigin,
)
return partitionKey
}
}
}
/**
* Deletes cookies based on the provided filter and partition.
*
* @param {CookieFilter} [cookieFilter] - The filter to apply to the cookies. Must be an instance of CookieFilter.
* @param {(BrowsingContextPartitionDescriptor|StorageKeyPartitionDescriptor)} [partition] - The partition to delete cookies from. Must be an instance of either BrowsingContextPartitionDescriptor or StorageKeyPartitionDescriptor.
* @returns {PartitionKey} - The partition key of the deleted cookies, if available.
* @throws {Error} - If the provided parameters are not of the correct type.
*/
async deleteCookies(cookieFilter = undefined, partition = undefined) {
if (cookieFilter !== undefined && !(cookieFilter instanceof CookieFilter)) {
throw new Error(`Params must be an instance of CookieFilter. Received:'${cookieFilter}'`)
}
if (
partition !== undefined &&
!(partition instanceof (BrowsingContextPartitionDescriptor || StorageKeyPartitionDescriptor))
) {
throw new Error(`Params must be an instance of PartitionDescriptor. Received:'${partition}'`)
}
const command = {
method: 'storage.deleteCookies',
params: {
filter: cookieFilter ? Object.fromEntries(cookieFilter.asMap()) : undefined,
partition: partition ? Object.fromEntries(partition.asMap()) : undefined,
},
}
let response = await this.bidi.send(command)
if (Object.prototype.hasOwnProperty.call(response.result, 'partitionKey')) {
if (
Object.prototype.hasOwnProperty.call(response.result.partitionKey, 'userContext') &&
Object.prototype.hasOwnProperty.call(response.result.partitionKey, 'sourceOrigin')
) {
let partitionKey = new PartitionKey(
response.result.partitionKey.userContext,
response.result.partitionKey.sourceOrigin,
)
return partitionKey
}
}
}
}
async function getStorageInstance(driver) {
let instance = new Storage(driver)
await instance.init()
return instance
}
module.exports = getStorageInstance

View File

@@ -0,0 +1,91 @@
// 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.
/**
* Represents a URL pattern to intercept.
* Described in network.UrlPatternPattern https://w3c.github.io/webdriver-bidi/#type-network-UrlPattern
*/
class UrlPattern {
#map = new Map()
/**
* Sets the protocol for the URL pattern.
*
* @param {string} protocol - The protocol to set.
* @returns {UrlPattern} - Returns the updated instance of the URL pattern for chaining.
*/
protocol(protocol) {
this.#map.set('protocol', protocol)
return this
}
/**
* Sets the hostname for the URL pattern.
*
* @param {string} hostname - The hostname to set.
* @returns {UrlPattern} - Returns the updated instance of the URL pattern for chaining.
*/
hostname(hostname) {
this.#map.set('hostname', hostname)
return this
}
/**
* Sets the port for the URL pattern.
*
* @param {number} port - The port number to set.
* @returns {UrlPattern} - Returns the updated instance of the URL pattern for chaining.
* @throws {Error} - Throws an error if the port is not a number.
*/
port(port) {
if (typeof port === 'number') {
this.#map.set('port', port.toString())
} else {
throw new Error(`Port must be a number. Received:'${port}'`)
}
return this
}
/**
* Sets the pathname for the URL pattern.
*
* @param {string} pathname - The pathname to set.
* @returns {UrlPattern} - Returns the updated instance of the URL pattern for chaining.
*/
pathname(pathname) {
this.#map.set('pathname', pathname)
return this
}
/**
* Sets the search parameter in the URL pattern.
*
* @param {string} search - The search parameter to be set.
* @returns {UrlPattern} - Returns the updated instance of the URL pattern for chaining.
*/
search(search) {
this.#map.set('search', search)
return this
}
asMap() {
this.#map.set('type', 'pattern')
return this.#map
}
}
module.exports = { UrlPattern }