Skip to content

Commit 3f70670

Browse files
harsha509TamsilAmanipujagani
authored
[JS] Bidi Support (#11395)
Co-authored-by: TamsilAmani <tamsajama@gmail.com> Co-authored-by: Puja Jagani <puja.jagani93@gmail.com>
1 parent 10a9e52 commit 3f70670

File tree

11 files changed

+590
-3
lines changed

11 files changed

+590
-3
lines changed

javascript/node/selenium-webdriver/BUILD.bazel

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ SRC_FILES = [
2323
"remote/*.js",
2424
"testing/*.js",
2525
"devtools/*.js",
26-
"common/*.js"
26+
"common/*.js",
27+
"bidi/*.js"
2728
])
2829

2930
pkg_npm(
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
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+
isConnected = false
26+
events = []
27+
browsingContexts = []
28+
29+
/**
30+
* Create a new websocket connection
31+
* @param _webSocketUrl
32+
*/
33+
constructor (_webSocketUrl) {
34+
super()
35+
this.isConnected = false
36+
this._ws = new WebSocket(_webSocketUrl)
37+
this._ws.on('open', () => {
38+
this.isConnected = true
39+
})
40+
}
41+
42+
/**
43+
* Resolve connection
44+
* @returns {Promise<unknown>}
45+
*/
46+
async waitForConnection () {
47+
return new Promise((resolve) => {
48+
if (this.isConnected) {
49+
resolve()
50+
} else {
51+
this._ws.once('open', () => {
52+
resolve()
53+
})
54+
}
55+
})
56+
}
57+
58+
/**
59+
* @returns {WebSocket}
60+
*/
61+
get socket () {
62+
return this._ws
63+
}
64+
65+
/**
66+
* @returns {boolean|*}
67+
*/
68+
get isConnected () {
69+
return this.isConnected
70+
}
71+
72+
/**
73+
* Sends a bidi request
74+
* @param params
75+
* @returns {Promise<unknown>}
76+
*/
77+
async send (params) {
78+
if (!this.isConnected) {
79+
await this.waitForConnection()
80+
}
81+
82+
const id = ++this.id
83+
84+
this._ws.send(JSON.stringify({ id, ...params }))
85+
86+
return new Promise((resolve, reject) => {
87+
const timeoutId = setTimeout(() => {
88+
reject(new Error(`Request with id ${id} timed out`))
89+
handler.off('message', listener)
90+
}, RESPONSE_TIMEOUT)
91+
92+
const listener = (data) => {
93+
try {
94+
const payload = JSON.parse(data.toString())
95+
if (payload.id === id) {
96+
clearTimeout(timeoutId)
97+
handler.off('message', listener)
98+
resolve(payload)
99+
}
100+
} catch (err) {
101+
log.error(`Failed parse message: ${err.message}`)
102+
}
103+
}
104+
105+
const handler = this._ws.on('message', listener)
106+
})
107+
}
108+
109+
/**
110+
* Subscribe to events
111+
* @param events
112+
* @param browsingContexts
113+
* @returns {Promise<void>}
114+
*/
115+
async subscribe (events, browsingContexts) {
116+
function toArray (arg) {
117+
if (arg === undefined) {
118+
return []
119+
}
120+
121+
return Array.isArray(arg) ? [...arg] : [arg]
122+
}
123+
124+
const eventsArray = toArray(events)
125+
const contextsArray = toArray(browsingContexts)
126+
127+
const params = {
128+
method: 'session.subscribe', params: {},
129+
}
130+
131+
if (eventsArray.length && eventsArray.some(
132+
event => typeof event !== 'string')) {
133+
throw new TypeError('events should be string or string array')
134+
}
135+
136+
if (contextsArray.length && contextsArray.some(
137+
context => typeof context !== 'string')) {
138+
throw new TypeError('browsingContexts should be string or string array')
139+
}
140+
141+
if (eventsArray.length) {
142+
params.params.events = eventsArray
143+
}
144+
145+
if (contextsArray.length) {
146+
params.params.contexts = contextsArray
147+
}
148+
149+
await this.send(params)
150+
}
151+
152+
/**
153+
* Unsubscribe to events
154+
* @param events
155+
* @param browsingContexts
156+
* @returns {Promise<void>}
157+
*/
158+
async unsubscribe (events, browsingContexts) {
159+
if (typeof events === 'string') {
160+
this.events = this.events.filter(event => event !== events)
161+
} else if (Array.isArray(events)) {
162+
this.events = this.events.filter(event => !events.includes(event))
163+
}
164+
165+
if (typeof browsingContexts === 'string') {
166+
this.browsingContexts.pop()
167+
} else if (Array.isArray(browsingContexts)) {
168+
this.browsingContexts =
169+
this.browsingContexts.filter(id => !browsingContexts.includes(id))
170+
}
171+
172+
const params = {
173+
method: 'session.unsubscribe', params: {
174+
events: this.events,
175+
}
176+
}
177+
178+
if (this.browsingContexts.length > 0) {
179+
params.params.contexts = this.browsingContexts
180+
}
181+
182+
await this.send(params)
183+
}
184+
185+
/**
186+
* Get Bidi Status
187+
* @returns {Promise<*>}
188+
*/
189+
get status () {
190+
return this.send({
191+
method: 'session.status', params: {}
192+
})
193+
}
194+
195+
/**
196+
* Close ws connection.
197+
* @returns {Promise<unknown>}
198+
*/
199+
close () {
200+
const closeWebSocket = (callback) => {
201+
// don't close if it's already closed
202+
if (this._ws.readyState === 3) {
203+
callback()
204+
} else {
205+
// don't notify on user-initiated shutdown ('disconnect' event)
206+
this._ws.removeAllListeners('close')
207+
this._ws.once('close', () => {
208+
this._ws.removeAllListeners()
209+
callback()
210+
})
211+
this._ws.close()
212+
}
213+
}
214+
return new Promise((fulfill, reject) => {
215+
closeWebSocket(fulfill)
216+
})
217+
}
218+
}
219+
220+
/**
221+
* API
222+
* @type {function(*): Promise<Index>}
223+
*/
224+
module.exports = Index
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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+
'use strict'
19+
20+
class BaseLogEntry {
21+
constructor (level, text, timeStamp, stackTrace) {
22+
this._level = level
23+
this._text = text
24+
this._timeStamp = timeStamp
25+
this._stackTrace = stackTrace
26+
}
27+
28+
get level () {
29+
return this._level
30+
}
31+
32+
get text () {
33+
return this._text
34+
}
35+
36+
get timeStamp () {
37+
return this._timeStamp
38+
}
39+
40+
get stackTrace () {
41+
return this._stackTrace
42+
}
43+
}
44+
45+
class GenericLogEntry extends BaseLogEntry {
46+
constructor (level, text, timeStamp, type, stackTrace) {
47+
super(level, text, timeStamp, stackTrace)
48+
this._type = type
49+
}
50+
51+
get type () {
52+
return this._type
53+
}
54+
}
55+
56+
class ConsoleLogEntry extends GenericLogEntry {
57+
constructor (level, text, timeStamp, type, method, realm, args, stackTrace) {
58+
super(level, text, timeStamp, type, stackTrace)
59+
this._method = method
60+
this._realm = realm
61+
this._args = args
62+
}
63+
64+
get method () {
65+
return this._method
66+
}
67+
68+
get realm () {
69+
return this._realm
70+
}
71+
72+
get args () {
73+
return this._args
74+
}
75+
}
76+
77+
class JavascriptLogEntry extends GenericLogEntry {
78+
constructor (level, text, timeStamp, type, stackTrace) {
79+
super(level, text, timeStamp, type, stackTrace)
80+
}
81+
}
82+
83+
// PUBLIC API
84+
85+
module.exports = {
86+
BaseLogEntry, GenericLogEntry, ConsoleLogEntry, JavascriptLogEntry,
87+
}

0 commit comments

Comments
 (0)