UNPKG

selenium-webdriver

Version:

The official WebDriver JavaScript bindings from the Selenium project

234 lines (200 loc) 5.76 kB
// Licensed to the Software Freedom Conservancy (SFC) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The SFC licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. const { 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 } this.events.push(...eventsArray) await this.send(params) } /** * Unsubscribe to events * @param events * @param browsingContexts * @returns {Promise<void>} */ async unsubscribe(events, browsingContexts) { const eventsToRemove = typeof events === 'string' ? [events] : events // Check if the eventsToRemove are in the subscribed events array // Filter out events that are not in this.events before filtering const existingEvents = eventsToRemove.filter((event) => this.events.includes(event)) // Remove the events from the subscribed events array this.events = this.events.filter((event) => !existingEvents.includes(event)) if (typeof browsingContexts === 'string') { this.browsingContexts.pop() } else if (Array.isArray(browsingContexts)) { this.browsingContexts = this.browsingContexts.filter((id) => !browsingContexts.includes(id)) } if (existingEvents.length === 0) { return } const params = { method: 'session.unsubscribe', params: { events: existingEvents, }, } 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