Path: blob/trunk/javascript/selenium-webdriver/test/bidi/index_test.js
10193 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.1617'use strict'1819const assert = require('node:assert')20const net = require('node:net')21const { WebSocketServer } = require('ws')22const BiDi = require('selenium-webdriver/bidi')2324function startEchoServer() {25return new Promise((resolve) => {26const server = new WebSocketServer({ port: 0 }, () => {27const { port } = server.address()28resolve({ server, url: `ws://127.0.0.1:${port}` })29})30server.on('connection', (ws) => {31ws.on('message', (data) => {32const { id } = JSON.parse(data.toString())33ws.send(JSON.stringify({ id, result: {} }))34})35})36})37}3839// Plain TCP listener that accepts connections but never completes the40// WebSocket upgrade — keeps the client stuck in CONNECTING so we can41// exercise the close()-during-connect path deterministically.42function startStallingServer() {43return new Promise((resolve) => {44const server = net.createServer(() => {})45server.listen(0, '127.0.0.1', () => {46const { port } = server.address()47resolve({ server, url: `ws://127.0.0.1:${port}` })48})49})50}5152describe('BiDi connection', function () {53let server54let bidi5556beforeEach(async function () {57const started = await startEchoServer()58server = started.server59bidi = new BiDi(started.url)60await bidi.waitForConnection()61})6263afterEach(async function () {64await bidi.close()65await new Promise((resolve) => server.close(resolve))66})6768// Regression test: BiDi network interception during a navigation issues many69// concurrent send() calls, which previously each attached a 'message'70// listener to the underlying WebSocket and tripped Node's71// MaxListenersExceededWarning once more than 10 were in flight.72it('does not emit MaxListenersExceededWarning under concurrent sends', async function () {73const warnings = []74const onWarning = (warning) => warnings.push(warning)75process.on('warning', onWarning)7677try {78const sends = []79for (let i = 0; i < 50; i++) {80sends.push(bidi.send({ method: 'session.status', params: {} }))81}82await Promise.all(sends)83} finally {84process.off('warning', onWarning)85}8687const offenders = warnings.filter((w) => w.name === 'MaxListenersExceededWarning')88assert.deepStrictEqual(offenders, [], `unexpected warnings: ${offenders.map((w) => w.message).join(', ')}`)89})9091it('uses one shared message listener regardless of in-flight sends', async function () {92const before = bidi.socket.listenerCount('message')9394const inFlight = []95for (let i = 0; i < 25; i++) {96inFlight.push(bidi.send({ method: 'session.status', params: {} }))97}9899// While requests are in flight the listener count must not grow.100assert.strictEqual(bidi.socket.listenerCount('message'), before)101102await Promise.all(inFlight)103104// And it stays the same after they resolve.105assert.strictEqual(bidi.socket.listenerCount('message'), before)106})107108// Surface parse failures rather than dropping silently — otherwise callers109// see misleading send() timeouts when a peer sends a malformed frame.110it('emits an error when the server sends a non-JSON message', async function () {111const errors = []112bidi.on('error', (err) => errors.push(err))113114for (const client of server.clients) {115client.send('not-json')116}117118await new Promise((resolve) => setTimeout(resolve, 50))119120assert.strictEqual(errors.length, 1, `expected 1 error, got ${errors.length}`)121assert.match(errors[0].message, /Failed to parse BiDi message/)122})123124// If the peer disconnects mid-request, callers should fail promptly via the125// socket's 'close' event instead of waiting for RESPONSE_TIMEOUT.126it('rejects pending sends when the connection drops unexpectedly', async function () {127// Stop the server from replying so the send stays pending.128for (const client of server.clients) {129client.removeAllListeners('message')130}131const inFlight = bidi.send({ method: 'session.status', params: {} })132133for (const client of server.clients) {134client.terminate()135}136137await assert.rejects(inFlight, /BiDi connection closed unexpectedly/)138})139140// Once the connection is closed, subsequent send() calls must fail fast141// rather than hanging on waitForConnection() awaiting an 'open' event that142// will never fire.143it('rejects send() after the connection has been closed', async function () {144for (const client of server.clients) {145client.terminate()146}147await new Promise((resolve) => setTimeout(resolve, 50))148149await assert.rejects(bidi.send({ method: 'session.status', params: {} }), /BiDi connection is closed/)150})151152// Race regression: close() must unblock waitForConnection() callers even153// when the socket is still CONNECTING. Previously close() ran154// removeAllListeners('close') before the socket actually closed, which155// could strip the rejection listener that waitForConnection() relied on156// and leave the wait pending forever.157it('unblocks waitForConnection() when close() is called during connect', async function () {158const stalling = await startStallingServer()159try {160const stalled = new BiDi(stalling.url)161const wait = stalled.waitForConnection()162163// Close while the underlying socket is still CONNECTING.164const close = stalled.close()165166await assert.rejects(wait, /BiDi connection closed/)167await close168} finally {169await new Promise((resolve) => stalling.server.close(resolve))170}171})172173// Race regression: if close() runs while the WebSocket is still CONNECTING174// and the handshake then completes anyway, the 'open' handler must not175// flip the instance back to connected=true.176it('does not become connected if open fires after close', async function () {177const late = await startEchoServer()178try {179const racer = new BiDi(late.url)180// Close immediately, before 'open' fires.181const close = racer.close()182183// Give the handshake a chance to complete.184await new Promise((resolve) => setTimeout(resolve, 100))185186assert.strictEqual(racer.isConnected, false, 'connection should remain closed after open race')187await close188} finally {189await new Promise((resolve) => late.server.close(resolve))190}191})192})193194195