Q37 of 42 · Playwright

How would you test a feature that depends on real-time WebSocket data?

PlaywrightSeniorplaywrightwebsocketsreal-timemockingsenior

Short answer

Short answer: Two paths: (1) run a local mock WebSocket server (`ws` library or `mock-socket`) and point the app at it via env var — full handshake + message control. (2) Stub `window.WebSocket` via `page.addInitScript` for client-side simulation. Playwright also exposes `page.on('websocket')` to spy on real connections.

Detail

Playwright's WebSocket support is more complete than Cypress's. Three layers:

1. Spy on real WebSocket connections:

page.on('websocket', (ws) => {
  console.log(`WS opened: ${ws.url()}`);
  ws.on('framesent', (frame) => console.log('→', frame.payload));
  ws.on('framereceived', (frame) => console.log('←', frame.payload));
  ws.on('close', () => console.log('WS closed'));
});

Useful for asserting the right messages are sent without modifying behaviour.

2. Mock WebSocket on the client side via page.addInitScript:

await page.addInitScript(() => {
  class MockWS extends EventTarget {
    readyState = 1;
    send() {}
    close() {}
    simulate(data: object) {
      this.dispatchEvent(new MessageEvent('message', { data: JSON.stringify(data) }));
    }
  }
  const mock = new MockWS();
  (window as any).WebSocket = function() { return mock; } as any;
  (window as any).__mockWs = mock;
});

await page.goto('/dashboard');
await page.evaluate(() => (window as any).__mockWs.simulate({ type: 'orderUpdate', id: 'o1' }));
await expect(page.getByTestId('order-status')).toContainText('Updated');

The app thinks it has a real socket; you control every message from the test.

3. Run a local mock WebSocket server:

import { WebSocketServer } from 'ws';

let wss: WebSocketServer;

test.beforeAll(() => {
  wss = new WebSocketServer({ port: 8080 });
  wss.on('connection', (ws) => {
    ws.on('message', (msg) => ws.send(JSON.stringify({ echo: msg.toString() })));
  });
});

test.afterAll(() => wss.close());

test('app reconnects after server restart', async ({ page }) => {
  await page.goto('/?ws=ws://localhost:8080');
  await expect(page.getByTestId('connection')).toHaveText('Connected');

  // Restart the server to test reconnect
  wss.close();
  await new Promise((r) => setTimeout(r, 500));
  wss = new WebSocketServer({ port: 8080 });
  await expect(page.getByTestId('connection')).toHaveText('Connected', { timeout: 10_000 });
});

The local-server approach exercises the real handshake and reconnection logic.

Things to test in any approach:

  • Initial state (empty / loading).
  • Receiving a message updates the UI.
  • Multiple messages in sequence preserve order.
  • Reconnection after disconnection.
  • Backpressure / message dropping under load.
  • Stale-state handling when the user disconnects briefly.

Senior signal: knowing all three approaches, distinguishing client-stub (lightest) from local-server (full handshake), and using page.on('websocket') for spy-only assertions.

// WHAT INTERVIEWERS LOOK FOR

Three approaches (spy / client stub / local server), the use cases for each, and naming `page.addInitScript` for the client-stub pattern.

// COMMON PITFALL

Trying to use `page.route` for WebSocket frames — `route` only intercepts the upgrade request, not the WS protocol itself.