Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
seleniumhq
GitHub Repository: seleniumhq/selenium
Path: blob/trunk/javascript/selenium-webdriver/bidi/index.js
2884 views
1
// Licensed to the Software Freedom Conservancy (SFC) under one
2
// or more contributor license agreements. See the NOTICE file
3
// distributed with this work for additional information
4
// regarding copyright ownership. The SFC licenses this file
5
// to you under the Apache License, Version 2.0 (the
6
// "License"); you may not use this file except in compliance
7
// with the License. You may obtain a copy of the License at
8
//
9
// http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing,
12
// software distributed under the License is distributed on an
13
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
// KIND, either express or implied. See the License for the
15
// specific language governing permissions and limitations
16
// under the License.
17
18
const { EventEmitter } = require('node:events')
19
const WebSocket = require('ws')
20
21
const RESPONSE_TIMEOUT = 1000 * 30
22
23
class Index extends EventEmitter {
24
id = 0
25
connected = false
26
events = []
27
browsingContexts = []
28
29
/**
30
* Create a new websocket connection
31
* @param _webSocketUrl
32
*/
33
constructor(_webSocketUrl) {
34
super()
35
this.connected = false
36
this._ws = new WebSocket(_webSocketUrl)
37
this._ws.on('open', () => {
38
this.connected = true
39
})
40
}
41
42
/**
43
* @returns {WebSocket}
44
*/
45
get socket() {
46
return this._ws
47
}
48
49
/**
50
* @returns {boolean|*}
51
*/
52
get isConnected() {
53
return this.connected
54
}
55
56
/**
57
* Get Bidi Status
58
* @returns {Promise<*>}
59
*/
60
get status() {
61
return this.send({
62
method: 'session.status',
63
params: {},
64
})
65
}
66
67
/**
68
* Resolve connection
69
* @returns {Promise<unknown>}
70
*/
71
async waitForConnection() {
72
return new Promise((resolve) => {
73
if (this.connected) {
74
resolve()
75
} else {
76
this._ws.once('open', () => {
77
resolve()
78
})
79
}
80
})
81
}
82
83
/**
84
* Sends a bidi request
85
* @param params
86
* @returns {Promise<unknown>}
87
*/
88
async send(params) {
89
if (!this.connected) {
90
await this.waitForConnection()
91
}
92
93
const id = ++this.id
94
95
this._ws.send(JSON.stringify({ id, ...params }))
96
97
return new Promise((resolve, reject) => {
98
const timeoutId = setTimeout(() => {
99
reject(new Error(`Request with id ${id} timed out`))
100
handler.off('message', listener)
101
}, RESPONSE_TIMEOUT)
102
103
const listener = (data) => {
104
try {
105
const payload = JSON.parse(data.toString())
106
if (payload.id === id) {
107
clearTimeout(timeoutId)
108
handler.off('message', listener)
109
resolve(payload)
110
}
111
} catch (err) {
112
// eslint-disable-next-line no-undef
113
log.error(`Failed parse message: ${err.message}`)
114
}
115
}
116
117
const handler = this._ws.on('message', listener)
118
})
119
}
120
121
/**
122
* Subscribe to events
123
* @param events
124
* @param browsingContexts
125
* @returns {Promise<void>}
126
*/
127
async subscribe(events, browsingContexts) {
128
function toArray(arg) {
129
if (arg === undefined) {
130
return []
131
}
132
133
return Array.isArray(arg) ? [...arg] : [arg]
134
}
135
136
const eventsArray = toArray(events)
137
const contextsArray = toArray(browsingContexts)
138
139
const params = {
140
method: 'session.subscribe',
141
params: {},
142
}
143
144
if (eventsArray.length && eventsArray.some((event) => typeof event !== 'string')) {
145
throw new TypeError('events should be string or string array')
146
}
147
148
if (contextsArray.length && contextsArray.some((context) => typeof context !== 'string')) {
149
throw new TypeError('browsingContexts should be string or string array')
150
}
151
152
if (eventsArray.length) {
153
params.params.events = eventsArray
154
}
155
156
if (contextsArray.length) {
157
params.params.contexts = contextsArray
158
}
159
160
this.events.push(...eventsArray)
161
162
await this.send(params)
163
}
164
165
/**
166
* Unsubscribe to events
167
* @param events
168
* @param browsingContexts
169
* @returns {Promise<void>}
170
*/
171
async unsubscribe(events, browsingContexts) {
172
const eventsToRemove = typeof events === 'string' ? [events] : events
173
174
// Check if the eventsToRemove are in the subscribed events array
175
// Filter out events that are not in this.events before filtering
176
const existingEvents = eventsToRemove.filter((event) => this.events.includes(event))
177
178
// Remove the events from the subscribed events array
179
this.events = this.events.filter((event) => !existingEvents.includes(event))
180
181
if (typeof browsingContexts === 'string') {
182
this.browsingContexts.pop()
183
} else if (Array.isArray(browsingContexts)) {
184
this.browsingContexts = this.browsingContexts.filter((id) => !browsingContexts.includes(id))
185
}
186
187
if (existingEvents.length === 0) {
188
return
189
}
190
const params = {
191
method: 'session.unsubscribe',
192
params: {
193
events: existingEvents,
194
},
195
}
196
197
if (this.browsingContexts.length > 0) {
198
params.params.contexts = this.browsingContexts
199
}
200
201
await this.send(params)
202
}
203
204
/**
205
* Close ws connection.
206
* @returns {Promise<unknown>}
207
*/
208
close() {
209
const closeWebSocket = (callback) => {
210
// don't close if it's already closed
211
if (this._ws.readyState === 3) {
212
callback()
213
} else {
214
// don't notify on user-initiated shutdown ('disconnect' event)
215
this._ws.removeAllListeners('close')
216
this._ws.once('close', () => {
217
this._ws.removeAllListeners()
218
callback()
219
})
220
this._ws.close()
221
}
222
}
223
return new Promise((fulfill, _) => {
224
closeWebSocket(fulfill)
225
})
226
}
227
}
228
229
/**
230
* API
231
* @type {function(*): Promise<Index>}
232
*/
233
module.exports = Index
234
235