Skip to content

sendMessage awaits a promise that can never reject, so a websocket that fails to open reports only "Browser tests did not start" #3158

Description

@mkutny

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

it('passes', () => {});

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions