Path: blob/trunk/javascript/selenium-webdriver/bidi/browsingContext.js
2884 views
// Licensed to the Software Freedom Conservancy (SFC) under one1// or more contributor license agreements. See the NOTICE file2// distributed with this work for additional information3// regarding copyright ownership. The SFC licenses this file4// to you under the Apache License, Version 2.0 (the5// "License"); you may not use this file except in compliance6// with the License. You may obtain a copy of the License at7//8// http://www.apache.org/licenses/LICENSE-2.09//10// Unless required by applicable law or agreed to in writing,11// software distributed under the License is distributed on an12// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY13// KIND, either express or implied. See the License for the14// specific language governing permissions and limitations15// under the License.1617const { InvalidArgumentError, NoSuchFrameError } = require('../lib/error')18const { BrowsingContextInfo } = require('./browsingContextTypes')19const { SerializationOptions, ReferenceValue, RemoteValue } = require('./protocolValue')20const { WebElement } = require('../lib/webdriver')21const { CaptureScreenshotParameters } = require('./captureScreenshotParameters')22const { CreateContextParameters } = require('./createContextParameters')2324/**25* Represents the locator to locate nodes in the browsing context.26* Described in https://w3c.github.io/webdriver-bidi/#type-browsingContext-Locator.27*/28class Locator {29static Type = Object.freeze({30CSS: 'css',31INNER_TEXT: 'innerText',32XPATH: 'xpath',33})3435#type36#value37#ignoreCase38#matchType39#maxDepth4041constructor(type, value, ignoreCase = undefined, matchType = undefined, maxDepth = undefined) {42this.#type = type43this.#value = value44this.#ignoreCase = ignoreCase45this.#matchType = matchType46this.#maxDepth = maxDepth47}4849/**50* Creates a new Locator object with CSS selector type.51*52* @param {string} value - The CSS selector value.53* @returns {Locator} A new Locator object with CSS selector type.54*/55static css(value) {56return new Locator(Locator.Type.CSS, value)57}5859/**60* Creates a new Locator object with the given XPath value.61*62* @param {string} value - The XPath value.63* @returns {Locator} A new Locator object.64*/65static xpath(value) {66return new Locator(Locator.Type.XPATH, value)67}6869/**70* Creates a new Locator object with the specified inner text value.71*72* @param {string} value - The inner text value to locate.73* @param {boolean|undefined} [ignoreCase] - Whether to ignore the case when matching the inner text value.74* @param {string|undefined} [matchType] - The type of matching to perform (full or partial).75* @param {number|undefined} [maxDepth] - The maximum depth to search for the inner text value.76* @returns {Locator} A new Locator object with the specified inner text value.77*/78static innerText(value, ignoreCase = undefined, matchType = undefined, maxDepth = undefined) {79return new Locator(Locator.Type.INNER_TEXT, value, ignoreCase, matchType, maxDepth)80}8182toMap() {83const map = new Map()8485map.set('type', this.#type.toString())86map.set('value', this.#value)87map.set('ignoreCase', this.#ignoreCase)88map.set('matchType', this.#matchType)89map.set('maxDepth', this.#maxDepth)9091return map92}93}9495/**96* Represents the contains under BrowsingContext module commands.97* Described in https://w3c.github.io/webdriver-bidi/#module-browsingContext98* Each browsing context command requires a browsing context id.99* Hence, this class represent browsing context lifecycle.100*/101class BrowsingContext {102constructor(driver) {103this._driver = driver104}105106/**107* @returns id108*/109get id() {110return this._id111}112113async init({ browsingContextId = undefined, type = undefined, createParameters = undefined }) {114if (!(await this._driver.getCapabilities()).get('webSocketUrl')) {115throw Error('WebDriver instance must support BiDi protocol')116}117118if (browsingContextId === undefined && type === undefined && createParameters === undefined) {119throw Error('Either BrowsingContextId or Type or CreateParameters must be provided')120}121122if (type === undefined && createParameters !== undefined) {123throw Error('Type must be provided with CreateParameters')124}125126if (type !== undefined && !['window', 'tab'].includes(type)) {127throw Error(`Valid types are 'window' & 'tab'. Received: ${type}`)128}129130this.bidi = await this._driver.getBidi()131this._id =132browsingContextId === undefined133? (await this.create(type, createParameters))['result']['context']134: browsingContextId135}136137/**138* Creates a browsing context for the given type with the given parameters139*/140async create(type, createParameters = undefined) {141if (createParameters !== undefined && (!createParameters) instanceof CreateContextParameters) {142throw Error(`Pass in the instance of CreateContextParameters. Received: ${createParameters}`)143}144145let parameters = new Map()146parameters.set('type', type)147148if (createParameters !== undefined) {149createParameters.asMap().forEach((value, key) => {150parameters.set(key, value)151})152}153154const params = {155method: 'browsingContext.create',156params: Object.fromEntries(parameters),157}158return await this.bidi.send(params)159}160161/**162* @param url the url to navigate to163* @param readinessState type of readiness state: "none" / "interactive" / "complete"164* @returns NavigateResult object165*/166async navigate(url, readinessState = undefined) {167if (readinessState !== undefined && !['none', 'interactive', 'complete'].includes(readinessState)) {168throw Error(`Valid readiness states are 'none', 'interactive' & 'complete'. Received: ${readinessState}`)169}170171const params = {172method: 'browsingContext.navigate',173params: {174context: this._id,175url: url,176wait: readinessState,177},178}179const navigateResult = (await this.bidi.send(params))['result']180181return new NavigateResult(navigateResult['url'], navigateResult['navigation'])182}183184/**185* @param maxDepth the max depth of the descendents of browsing context tree186* @returns BrowsingContextInfo object187*/188async getTree(maxDepth = undefined) {189const params = {190method: 'browsingContext.getTree',191params: {192root: this._id,193maxDepth: maxDepth,194},195}196197let result = await this.bidi.send(params)198if ('error' in result) {199throw Error(result['error'])200}201202result = result['result']['contexts'][0]203return new BrowsingContextInfo(result['context'], result['url'], result['children'], result['parent'])204}205206/**207* @returns {Promise<Array<BrowsingContextInfo>>} A Promise that resolves to an array of BrowsingContextInfo objects representing the top-level browsing contexts.208*/209async getTopLevelContexts() {210const params = {211method: 'browsingContext.getTree',212params: {},213}214215let result = await this.bidi.send(params)216if ('error' in result) {217throw Error(result['error'])218}219220const contexts = result['result']['contexts']221const browsingContexts = contexts.map((context) => {222return new BrowsingContextInfo(context['id'], context['url'], context['children'], context['parent'])223})224return browsingContexts225}226227/**228* Closes the browsing context229* @returns {Promise<void>}230*/231async close() {232const params = {233method: 'browsingContext.close',234params: {235context: this._id,236},237}238await this.bidi.send(params)239}240241/**242* Prints PDF of the webpage243* @param options print options given by the user244* @returns PrintResult object245*/246async printPage(options = {}) {247let params = {248method: 'browsingContext.print',249// Setting default values for parameters250params: {251context: this._id,252background: false,253margin: {254bottom: 1.0,255left: 1.0,256right: 1.0,257top: 1.0,258},259orientation: 'portrait',260page: {261height: 27.94,262width: 21.59,263},264pageRanges: [],265scale: 1.0,266shrinkToFit: true,267},268}269270// Updating parameter values based on the options passed271params.params = this._driver.validatePrintPageParams(options, params.params)272273const response = await this.bidi.send(params)274return new PrintResult(response.result.data)275}276277/**278* Captures a screenshot of the browsing context.279*280* @param {CaptureScreenshotParameters|undefined} [captureScreenshotParameters] - Optional parameters for capturing the screenshot.281* @returns {Promise<string>} - A promise that resolves to the base64-encoded string representation of the captured screenshot.282* @throws {InvalidArgumentError} - If the provided captureScreenshotParameters is not an instance of CaptureScreenshotParameters.283*/284async captureScreenshot(captureScreenshotParameters = undefined) {285if (286captureScreenshotParameters !== undefined &&287!(captureScreenshotParameters instanceof CaptureScreenshotParameters)288) {289throw new InvalidArgumentError(290`Pass in a CaptureScreenshotParameters object. Received: ${captureScreenshotParameters}`,291)292}293294const screenshotParams = new Map()295screenshotParams.set('context', this._id)296if (captureScreenshotParameters !== undefined) {297captureScreenshotParameters.asMap().forEach((value, key) => {298screenshotParams.set(key, value)299})300}301302let params = {303method: 'browsingContext.captureScreenshot',304params: Object.fromEntries(screenshotParams),305}306307const response = await this.bidi.send(params)308this.checkErrorInScreenshot(response)309return response['result']['data']310}311312async captureBoxScreenshot(x, y, width, height) {313let params = {314method: 'browsingContext.captureScreenshot',315params: {316context: this._id,317clip: {318type: 'box',319x: x,320y: y,321width: width,322height: height,323},324},325}326327const response = await this.bidi.send(params)328this.checkErrorInScreenshot(response)329return response['result']['data']330}331332/**333* Captures a screenshot of a specific element within the browsing context.334* @param {string} sharedId - The shared ID of the element to capture.335* @param {string} [handle] - The handle of the element to capture (optional).336* @returns {Promise<string>} A promise that resolves to the base64-encoded screenshot data.337*/338async captureElementScreenshot(sharedId, handle = undefined) {339let params = {340method: 'browsingContext.captureScreenshot',341params: {342context: this._id,343clip: {344type: 'element',345element: {346sharedId: sharedId,347handle: handle,348},349},350},351}352353const response = await this.bidi.send(params)354this.checkErrorInScreenshot(response)355return response['result']['data']356}357358checkErrorInScreenshot(response) {359if ('error' in response) {360const { error, msg } = response361362switch (error) {363case 'invalid argument':364throw new InvalidArgumentError(msg)365366case 'no such frame':367throw new NoSuchFrameError(msg)368}369}370}371372/**373* Activates and focuses the top-level browsing context.374* @returns {Promise<void>} A promise that resolves when the browsing context is activated.375* @throws {Error} If there is an error while activating the browsing context.376*/377async activate() {378const params = {379method: 'browsingContext.activate',380params: {381context: this._id,382},383}384385let result = await this.bidi.send(params)386if ('error' in result) {387throw Error(result['error'])388}389}390391/**392* Handles a user prompt in the browsing context.393*394* @param {boolean} [accept] - Optional. Indicates whether to accept or dismiss the prompt.395* @param {string} [userText] - Optional. The text to enter.396* @throws {Error} If an error occurs while handling the user prompt.397*/398async handleUserPrompt(accept = undefined, userText = undefined) {399const params = {400method: 'browsingContext.handleUserPrompt',401params: {402context: this._id,403accept: accept,404userText: userText,405},406}407408let result = await this.bidi.send(params)409if ('error' in result) {410throw Error(result['error'])411}412}413414/**415* Reloads the current browsing context.416*417* @param {boolean} [ignoreCache] - Whether to ignore the cache when reloading.418* @param {string} [readinessState] - The readiness state to wait for before returning.419* Valid readiness states are 'none', 'interactive', and 'complete'.420* @returns {Promise<NavigateResult>} - A promise that resolves to the result of the reload operation.421* @throws {Error} - If an invalid readiness state is provided.422*/423async reload(ignoreCache = undefined, readinessState = undefined) {424if (readinessState !== undefined && !['none', 'interactive', 'complete'].includes(readinessState)) {425throw Error(`Valid readiness states are 'none', 'interactive' & 'complete'. Received: ${readinessState}`)426}427428const params = {429method: 'browsingContext.reload',430params: {431context: this._id,432ignoreCache: ignoreCache,433wait: readinessState,434},435}436const navigateResult = (await this.bidi.send(params))['result']437438return new NavigateResult(navigateResult['url'], navigateResult['navigation'])439}440441/**442* Sets the viewport size and device pixel ratio for the browsing context.443* @param {number} width - The width of the viewport.444* @param {number} height - The height of the viewport.445* @param {number} [devicePixelRatio] - The device pixel ratio (optional)446* @throws {Error} If an error occurs while setting the viewport.447*/448async setViewport(width, height, devicePixelRatio = undefined) {449const params = {450method: 'browsingContext.setViewport',451params: {452context: this._id,453viewport: { width: width, height: height },454devicePixelRatio: devicePixelRatio,455},456}457let result = await this.bidi.send(params)458if ('error' in result) {459throw Error(result['error'])460}461}462463/**464* Traverses the browsing context history by a given delta.465*466* @param {number} delta - The delta value to traverse the history. A positive value moves forward, while a negative value moves backward.467* @returns {Promise<void>} - A promise that resolves when the history traversal is complete.468*/469async traverseHistory(delta) {470const params = {471method: 'browsingContext.traverseHistory',472params: {473context: this._id,474delta: delta,475},476}477await this.bidi.send(params)478}479480/**481* Moves the browsing context forward by one step in the history.482* @returns {Promise<void>} A promise that resolves when the browsing context has moved forward.483*/484async forward() {485await this.traverseHistory(1)486}487488/**489* Navigates the browsing context to the previous page in the history.490* @returns {Promise<void>} A promise that resolves when the navigation is complete.491*/492async back() {493await this.traverseHistory(-1)494}495496/**497* Locates nodes in the browsing context.498*499* @param {Locator} locator - The locator object used to locate the nodes.500* @param {number} [maxNodeCount] - The maximum number of nodes to locate (optional).501* @param {string} [sandbox] - The sandbox name for locating nodes (optional).502* @param {SerializationOptions} [serializationOptions] - The serialization options for locating nodes (optional).503* @param {ReferenceValue[]} [startNodes] - The array of start nodes for locating nodes (optional).504* @returns {Promise<RemoteValue[]>} - A promise that resolves to the arrays of located nodes.505* @throws {Error} - If the locator is not an instance of Locator.506* @throws {Error} - If the serializationOptions is provided but not an instance of SerializationOptions.507* @throws {Error} - If the startNodes is provided but not an array of ReferenceValue objects.508* @throws {Error} - If any of the startNodes is not an instance of ReferenceValue.509*/510async locateNodes(511locator,512maxNodeCount = undefined,513sandbox = undefined,514serializationOptions = undefined,515startNodes = undefined,516) {517if (!(locator instanceof Locator)) {518throw Error(`Pass in a Locator object. Received: ${locator}`)519}520521if (serializationOptions !== undefined && !(serializationOptions instanceof SerializationOptions)) {522throw Error(`Pass in SerializationOptions object. Received: ${serializationOptions} `)523}524525if (startNodes !== undefined && !Array.isArray(startNodes)) {526throw Error(`Pass in an array of ReferenceValue objects. Received: ${startNodes}`)527}528529let startNodesSerialized = undefined530531if (startNodes !== undefined && Array.isArray(startNodes)) {532startNodesSerialized = []533startNodes.forEach((node) => {534if (!(node instanceof ReferenceValue)) {535throw Error(`Pass in a ReferenceValue object. Received: ${node}`)536} else {537startNodesSerialized.push(node.asMap())538}539})540}541542const params = {543method: 'browsingContext.locateNodes',544params: {545context: this._id,546locator: Object.fromEntries(locator.toMap()),547maxNodeCount: maxNodeCount,548sandbox: sandbox,549serializationOptions: serializationOptions,550startNodes: startNodesSerialized,551},552}553554let response = await this.bidi.send(params)555if ('error' in response) {556throw Error(response['error'])557}558559const nodes = response.result.nodes560const remoteValues = []561562nodes.forEach((node) => {563remoteValues.push(new RemoteValue(node))564})565return remoteValues566}567568/**569* Locates a single node in the browsing context.570*571* @param {Locator} locator - The locator used to find the node.572* @param {string} [sandbox] - The sandbox of the node (optional).573* @param {SerializationOptions} [serializationOptions] - The serialization options for the node (optional).574* @param {Array} [startNodes] - The starting nodes for the search (optional).575* @returns {Promise<RemoteValue>} - A promise that resolves to the located node.576*/577async locateNode(locator, sandbox = undefined, serializationOptions = undefined, startNodes = undefined) {578const elements = await this.locateNodes(locator, 1, sandbox, serializationOptions, startNodes)579return elements[0]580}581582async locateElement(locator) {583const elements = await this.locateNodes(locator, 1)584return new WebElement(this._driver, elements[0].sharedId)585}586587async locateElements(locator) {588const elements = await this.locateNodes(locator)589590let webElements = []591elements.forEach((element) => {592webElements.push(new WebElement(this._driver, element.sharedId))593})594return webElements595}596}597598/**599* Represents the result of a navigation operation.600*/601class NavigateResult {602constructor(url, navigationId) {603this._url = url604this._navigationId = navigationId605}606607/**608* Gets the URL of the navigated page.609* @returns {string} The URL of the navigated page.610*/611get url() {612return this._url613}614615/**616* Gets the ID of the navigation operation.617* @returns {number} The ID of the navigation operation.618*/619get navigationId() {620return this._navigationId621}622}623624/**625* Represents a print result.626*/627class PrintResult {628constructor(data) {629this._data = data630}631632/**633* Gets the data associated with the print result.634* @returns {any} The data associated with the print result.635*/636get data() {637return this._data638}639}640641/**642* initiate browsing context instance and return643* @param driver644* @param browsingContextId The browsing context of current window/tab645* @param type "window" or "tab"646* @param createParameters The parameters for creating a new browsing context647* @returns {Promise<BrowsingContext>}648*/649async function getBrowsingContextInstance(650driver,651{ browsingContextId = undefined, type = undefined, createParameters = undefined },652) {653let instance = new BrowsingContext(driver)654await instance.init({ browsingContextId, type, createParameters })655return instance656}657658module.exports = getBrowsingContextInstance659module.exports.Locator = Locator660661662