Path: blob/trunk/javascript/selenium-webdriver/bidi/index.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 { EventEmitter } = require('node:events')18const WebSocket = require('ws')1920const RESPONSE_TIMEOUT = 1000 * 302122class Index extends EventEmitter {23id = 024connected = false25events = []26browsingContexts = []2728/**29* Create a new websocket connection30* @param _webSocketUrl31*/32constructor(_webSocketUrl) {33super()34this.connected = false35this._ws = new WebSocket(_webSocketUrl)36this._ws.on('open', () => {37this.connected = true38})39}4041/**42* @returns {WebSocket}43*/44get socket() {45return this._ws46}4748/**49* @returns {boolean|*}50*/51get isConnected() {52return this.connected53}5455/**56* Get Bidi Status57* @returns {Promise<*>}58*/59get status() {60return this.send({61method: 'session.status',62params: {},63})64}6566/**67* Resolve connection68* @returns {Promise<unknown>}69*/70async waitForConnection() {71return new Promise((resolve) => {72if (this.connected) {73resolve()74} else {75this._ws.once('open', () => {76resolve()77})78}79})80}8182/**83* Sends a bidi request84* @param params85* @returns {Promise<unknown>}86*/87async send(params) {88if (!this.connected) {89await this.waitForConnection()90}9192const id = ++this.id9394this._ws.send(JSON.stringify({ id, ...params }))9596return new Promise((resolve, reject) => {97const timeoutId = setTimeout(() => {98reject(new Error(`Request with id ${id} timed out`))99handler.off('message', listener)100}, RESPONSE_TIMEOUT)101102const listener = (data) => {103try {104const payload = JSON.parse(data.toString())105if (payload.id === id) {106clearTimeout(timeoutId)107handler.off('message', listener)108resolve(payload)109}110} catch (err) {111// eslint-disable-next-line no-undef112log.error(`Failed parse message: ${err.message}`)113}114}115116const handler = this._ws.on('message', listener)117})118}119120/**121* Subscribe to events122* @param events123* @param browsingContexts124* @returns {Promise<void>}125*/126async subscribe(events, browsingContexts) {127function toArray(arg) {128if (arg === undefined) {129return []130}131132return Array.isArray(arg) ? [...arg] : [arg]133}134135const eventsArray = toArray(events)136const contextsArray = toArray(browsingContexts)137138const params = {139method: 'session.subscribe',140params: {},141}142143if (eventsArray.length && eventsArray.some((event) => typeof event !== 'string')) {144throw new TypeError('events should be string or string array')145}146147if (contextsArray.length && contextsArray.some((context) => typeof context !== 'string')) {148throw new TypeError('browsingContexts should be string or string array')149}150151if (eventsArray.length) {152params.params.events = eventsArray153}154155if (contextsArray.length) {156params.params.contexts = contextsArray157}158159this.events.push(...eventsArray)160161await this.send(params)162}163164/**165* Unsubscribe to events166* @param events167* @param browsingContexts168* @returns {Promise<void>}169*/170async unsubscribe(events, browsingContexts) {171const eventsToRemove = typeof events === 'string' ? [events] : events172173// Check if the eventsToRemove are in the subscribed events array174// Filter out events that are not in this.events before filtering175const existingEvents = eventsToRemove.filter((event) => this.events.includes(event))176177// Remove the events from the subscribed events array178this.events = this.events.filter((event) => !existingEvents.includes(event))179180if (typeof browsingContexts === 'string') {181this.browsingContexts.pop()182} else if (Array.isArray(browsingContexts)) {183this.browsingContexts = this.browsingContexts.filter((id) => !browsingContexts.includes(id))184}185186if (existingEvents.length === 0) {187return188}189const params = {190method: 'session.unsubscribe',191params: {192events: existingEvents,193},194}195196if (this.browsingContexts.length > 0) {197params.params.contexts = this.browsingContexts198}199200await this.send(params)201}202203/**204* Close ws connection.205* @returns {Promise<unknown>}206*/207close() {208const closeWebSocket = (callback) => {209// don't close if it's already closed210if (this._ws.readyState === 3) {211callback()212} else {213// don't notify on user-initiated shutdown ('disconnect' event)214this._ws.removeAllListeners('close')215this._ws.once('close', () => {216this._ws.removeAllListeners()217callback()218})219this._ws.close()220}221}222return new Promise((fulfill, _) => {223closeWebSocket(fulfill)224})225}226}227228/**229* API230* @type {function(*): Promise<Index>}231*/232module.exports = Index233234235