Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
SeleniumHQ
GitHub Repository: SeleniumHQ/Selenium
Path: blob/trunk/javascript/selenium-webdriver/test/bidi/index_test.js
10193 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
'use strict'
19
20
const assert = require('node:assert')
21
const net = require('node:net')
22
const { WebSocketServer } = require('ws')
23
const BiDi = require('selenium-webdriver/bidi')
24
25
function startEchoServer() {
26
return new Promise((resolve) => {
27
const server = new WebSocketServer({ port: 0 }, () => {
28
const { port } = server.address()
29
resolve({ server, url: `ws://127.0.0.1:${port}` })
30
})
31
server.on('connection', (ws) => {
32
ws.on('message', (data) => {
33
const { id } = JSON.parse(data.toString())
34
ws.send(JSON.stringify({ id, result: {} }))
35
})
36
})
37
})
38
}
39
40
// Plain TCP listener that accepts connections but never completes the
41
// WebSocket upgrade — keeps the client stuck in CONNECTING so we can
42
// exercise the close()-during-connect path deterministically.
43
function startStallingServer() {
44
return new Promise((resolve) => {
45
const server = net.createServer(() => {})
46
server.listen(0, '127.0.0.1', () => {
47
const { port } = server.address()
48
resolve({ server, url: `ws://127.0.0.1:${port}` })
49
})
50
})
51
}
52
53
describe('BiDi connection', function () {
54
let server
55
let bidi
56
57
beforeEach(async function () {
58
const started = await startEchoServer()
59
server = started.server
60
bidi = new BiDi(started.url)
61
await bidi.waitForConnection()
62
})
63
64
afterEach(async function () {
65
await bidi.close()
66
await new Promise((resolve) => server.close(resolve))
67
})
68
69
// Regression test: BiDi network interception during a navigation issues many
70
// concurrent send() calls, which previously each attached a 'message'
71
// listener to the underlying WebSocket and tripped Node's
72
// MaxListenersExceededWarning once more than 10 were in flight.
73
it('does not emit MaxListenersExceededWarning under concurrent sends', async function () {
74
const warnings = []
75
const onWarning = (warning) => warnings.push(warning)
76
process.on('warning', onWarning)
77
78
try {
79
const sends = []
80
for (let i = 0; i < 50; i++) {
81
sends.push(bidi.send({ method: 'session.status', params: {} }))
82
}
83
await Promise.all(sends)
84
} finally {
85
process.off('warning', onWarning)
86
}
87
88
const offenders = warnings.filter((w) => w.name === 'MaxListenersExceededWarning')
89
assert.deepStrictEqual(offenders, [], `unexpected warnings: ${offenders.map((w) => w.message).join(', ')}`)
90
})
91
92
it('uses one shared message listener regardless of in-flight sends', async function () {
93
const before = bidi.socket.listenerCount('message')
94
95
const inFlight = []
96
for (let i = 0; i < 25; i++) {
97
inFlight.push(bidi.send({ method: 'session.status', params: {} }))
98
}
99
100
// While requests are in flight the listener count must not grow.
101
assert.strictEqual(bidi.socket.listenerCount('message'), before)
102
103
await Promise.all(inFlight)
104
105
// And it stays the same after they resolve.
106
assert.strictEqual(bidi.socket.listenerCount('message'), before)
107
})
108
109
// Surface parse failures rather than dropping silently — otherwise callers
110
// see misleading send() timeouts when a peer sends a malformed frame.
111
it('emits an error when the server sends a non-JSON message', async function () {
112
const errors = []
113
bidi.on('error', (err) => errors.push(err))
114
115
for (const client of server.clients) {
116
client.send('not-json')
117
}
118
119
await new Promise((resolve) => setTimeout(resolve, 50))
120
121
assert.strictEqual(errors.length, 1, `expected 1 error, got ${errors.length}`)
122
assert.match(errors[0].message, /Failed to parse BiDi message/)
123
})
124
125
// If the peer disconnects mid-request, callers should fail promptly via the
126
// socket's 'close' event instead of waiting for RESPONSE_TIMEOUT.
127
it('rejects pending sends when the connection drops unexpectedly', async function () {
128
// Stop the server from replying so the send stays pending.
129
for (const client of server.clients) {
130
client.removeAllListeners('message')
131
}
132
const inFlight = bidi.send({ method: 'session.status', params: {} })
133
134
for (const client of server.clients) {
135
client.terminate()
136
}
137
138
await assert.rejects(inFlight, /BiDi connection closed unexpectedly/)
139
})
140
141
// Once the connection is closed, subsequent send() calls must fail fast
142
// rather than hanging on waitForConnection() awaiting an 'open' event that
143
// will never fire.
144
it('rejects send() after the connection has been closed', async function () {
145
for (const client of server.clients) {
146
client.terminate()
147
}
148
await new Promise((resolve) => setTimeout(resolve, 50))
149
150
await assert.rejects(bidi.send({ method: 'session.status', params: {} }), /BiDi connection is closed/)
151
})
152
153
// Race regression: close() must unblock waitForConnection() callers even
154
// when the socket is still CONNECTING. Previously close() ran
155
// removeAllListeners('close') before the socket actually closed, which
156
// could strip the rejection listener that waitForConnection() relied on
157
// and leave the wait pending forever.
158
it('unblocks waitForConnection() when close() is called during connect', async function () {
159
const stalling = await startStallingServer()
160
try {
161
const stalled = new BiDi(stalling.url)
162
const wait = stalled.waitForConnection()
163
164
// Close while the underlying socket is still CONNECTING.
165
const close = stalled.close()
166
167
await assert.rejects(wait, /BiDi connection closed/)
168
await close
169
} finally {
170
await new Promise((resolve) => stalling.server.close(resolve))
171
}
172
})
173
174
// Race regression: if close() runs while the WebSocket is still CONNECTING
175
// and the handshake then completes anyway, the 'open' handler must not
176
// flip the instance back to connected=true.
177
it('does not become connected if open fires after close', async function () {
178
const late = await startEchoServer()
179
try {
180
const racer = new BiDi(late.url)
181
// Close immediately, before 'open' fires.
182
const close = racer.close()
183
184
// Give the handshake a chance to complete.
185
await new Promise((resolve) => setTimeout(resolve, 100))
186
187
assert.strictEqual(racer.isConnected, false, 'connection should remain closed after open race')
188
await close
189
} finally {
190
await new Promise((resolve) => late.server.close(resolve))
191
}
192
})
193
})
194
195