When the dev-server websocket fails to open, the run dies after testsStartTimeout with a message that never mentions the websocket — and both things it suggests are dead ends. We hit this with an intermittently failing socket; twenty silent seconds was the only signal.
Reproduce
web-test-runner.config.mjs
export default {
files: ['test.js'],
plugins: [
{
name: 'break-the-websocket',
serverStart({ server }) {
// drop every upgrade request, so the browser's WebSocket never opens
server.removeAllListeners('upgrade');
server.on('upgrade', (_req, socket) => socket.destroy());
},
},
],
};
test.js
Expected: an error naming the websocket.
Actual: exit 1 after 20 s, and no line of the output contains "socket".
❌ Browser tests did not start after 20000ms You can increase this timeout with the
testsStartTimeout option. Check the browser logs or open the browser in debug
mode for more information.
Delete the plugin and the same test passes in 0.5 s.
Cause
packages/dev-server-core/src/web-sockets/webSocketsPlugin.ts — the promise has a resolver and no rejecter, and sendMessage awaits it unconditionally:
webSocketOpened = new Promise(resolve => {
if (!webSocket) { resolve(); }
else { webSocket.addEventListener('open', () => { resolve(); }); } // the only resolver
});
sendMessage = async message => {
if (!message.type) throw new Error('Missing message type');
await webSocketOpened; // never settles if the socket does not open
webSocket.send(stable(message));
};
The caller that matters is not awaited — test-runner-mocha's autorun, deminified:
!async function(){ i && await e({type:"wtr-session-started", sessionId:i, testFile:n}) }();
So nothing rejects and nothing logs. The test file still imports, evaluates and registers its tests normally; only the "started" message is lost.
That also makes the message's two suggestions unusable in exactly this case: browser logs travel over the same socket, and in debug mode there is no sessionId, so sessionStarted() returns early and the failure cannot occur.
An error/close listener that rejects webSocketOpened, or a connect timeout, would turn twenty silent seconds into one line.
Versions
@web/test-runner 1.0.0, @web/test-runner-mocha 1.0.0, @web/dev-server-core 1.0.1, Node 24, macOS. The code above is byte-identical on main today.
When the dev-server websocket fails to open, the run dies after
testsStartTimeoutwith a message that never mentions the websocket — and both things it suggests are dead ends. We hit this with an intermittently failing socket; twenty silent seconds was the only signal.Reproduce
web-test-runner.config.mjstest.jsExpected: an error naming the websocket.
Actual: exit 1 after 20 s, and no line of the output contains "socket".
Delete the plugin and the same test passes in 0.5 s.
Cause
packages/dev-server-core/src/web-sockets/webSocketsPlugin.ts— the promise has a resolver and no rejecter, andsendMessageawaits it unconditionally:The caller that matters is not awaited —
test-runner-mocha'sautorun, deminified:So nothing rejects and nothing logs. The test file still imports, evaluates and registers its tests normally; only the "started" message is lost.
That also makes the message's two suggestions unusable in exactly this case: browser logs travel over the same socket, and in debug mode there is no
sessionId, sosessionStarted()returns early and the failure cannot occur.An
error/closelistener that rejectswebSocketOpened, or a connect timeout, would turn twenty silent seconds into one line.Versions
@web/test-runner1.0.0,@web/test-runner-mocha1.0.0,@web/dev-server-core1.0.1, Node 24, macOS. The code above is byte-identical onmaintoday.