skip to content

In Playwright, what happens to a page's WebSocket traffic once page.routeWebSocket() handles it?

level: middleimportance: must knowfreq 55%

answer

  1. The test becomes the other end
  2. Mocked unless you opt back in
  3. Handler gets a route, not a socket
  4. Register before the navigation
  5. send pushes to the page

basics

~20 s

The route replaces the server. Playwright hands the handler a WebSocketRoute and, in Playwright 1.63, makes no connection to the real endpoint unless connectToServer() is called, so the test answers the page's messages with send() and closes with close().

solid answer

~40 s

`page.routeWebSocket(url, handler)` intercepts the socket the page opens and gives the handler a `WebSocketRoute`. The key default in Playwright 1.63: a routed socket is **not** connected to the real server, so the test itself becomes the peer. From the route you call `ws.onMessage()` to see what the page sends, `ws.send()` to push a frame to the page as though the server had, `ws.close({ code, reason })` to end the connection, `ws.onClose()` to react when the page closes, and `ws.url()` to read the URL. Only sockets created *after* registration are routed, so register before `page.goto()`. Matching accepts a glob string, a `RegExp`, or a predicate over the `URL`.

code

typescript · 15 lines
typescript
import { test, expect } from '@playwright/test';

test('dashboard renders a pushed forecast', async ({ page }) => {
  await page.routeWebSocket('wss://feed.example.com/forecast', ws => {
    ws.onMessage(message => {
      if (String(message).includes('subscribe')) {
        ws.send(JSON.stringify({ city: 'Lisbon', temperature: 21 }));
      }
    });
  });

  await page.goto('/dashboard');

  await expect(page.getByTestId('temperature')).toHaveText('21°C');
});

go deeper

for a junior

Learn the shape: page.routeWebSocket takes a URL pattern and a handler, the handler gets a WebSocketRoute, and ws.send pushes a frame to the page. Register it before page.goto.

for a middle

Explain the default that catches people out — no connection to the real server unless connectToServer is called — and what then happens to messages the page sends.

for a senior

Diagnose the two silent failures: an unanswered handshake that leaves the app loading, and a pattern that never matches because the socket host differs from the page origin.

for a principal

Own the drift problem. Hand-written frames are a snapshot of a contract; decide how the suite refreshes them and which layer catches a feed that changed shape.

## The route replaces the server `page.routeWebSocket(url, handler)` intercepts the page's `WebSocket` construction. Instead of the browser dialling the real endpoint, Playwright calls your handler with a `WebSocketRoute` object and the test becomes the peer on the other end of the connection. In Playwright 1.63 the surprising part is the default: a routed socket does **not** talk to the real server at all unless you explicitly opt back in with `connectToServer()`. Route `wss://feed.example.com/forecast` on a weather dashboard and the third-party feed is out of the picture — every frame the tiles render is one your test wrote. That is what makes a socket-driven UI testable. A dashboard that only changes when a frame arrives is otherwise hostage to whatever the live feed happens to be pushing that afternoon. ## The WebSocketRoute surface - `ws.url()` — the URL the page asked for, useful when one pattern matches several channels. - `ws.onMessage(handler)` — runs for each message the page sends; the payload is a `string` or a `Buffer`. - `ws.send(message)` — pushes a message to the page, arriving exactly as a server frame would. - `ws.close({ code, reason })` — ends the connection, so a "feed offline" banner becomes deterministic to test. - `ws.onClose(handler)` — runs when the other side closes, receiving the close code and reason. - `ws.connectToServer()` — opts back in to the real server and returns a second `WebSocketRoute` standing for that server side. ## Matching, and when the handler runs The `url` argument takes the same matcher family as Playwright's HTTP routing: a glob string, a `RegExp`, or a predicate receiving a `URL`. It is matched against the socket URL, which starts `ws://` or `wss://` and often points at a different host from the page. Two ordering facts decide whether your handler ever runs: 1. Only sockets created **after** the call are routed, so `page.routeWebSocket()` must come before the `page.goto()` that opens the socket. 2. The handler runs once per matching socket, so a page that opens a forecast channel and an alerts channel enters it twice — branch on `ws.url()` inside. `browserContext.routeWebSocket()` applies the same handler to every page in the context, which is the right level when a popup or a second tab opens its own feed. ## Mocked versus connected, side by side | Behaviour | Handler without `connectToServer()` | Handler that calls `connectToServer()` | |---|---|---| | Real endpoint contacted | No | Yes | | Page messages | Reach `onMessage`, or go nowhere | Forwarded to the server by default | | Server messages | None exist | Forwarded to the page by default | | Test writes frames with | `ws.send()` | `ws.send()` on the page-side route | | Determinism | Total | Bounded by the live feed | ## A mocked socket end to end 1. Register the route before navigating, matching the forecast endpoint. 2. In the handler, use `ws.onMessage()` to recognise the dashboard's subscribe frame — the app usually will not render anything until its subscription is acknowledged. 3. Answer with `ws.send(JSON.stringify({ city: 'Lisbon', temperature: 21 }))`, the exact payload shape the app parses. 4. Assert on the rendered tile with a web-first assertion, which retries while the frame makes its way through the app's state. ## Traps worth knowing - **Unanswered handshakes hang.** With no `connectToServer()` and no `onMessage`, everything the page sends simply goes nowhere. An app waiting for a server ack will sit at its loading state until the test times out — and the failure looks like a rendering bug. - **Payload shape is your problem.** Playwright sends the bytes you hand it. If the app expects a JSON envelope, send valid JSON; a plain object will not be serialised for you. - **A mock is a snapshot.** Frames you wrote by hand stop matching the day the feed adds a field or renames a channel, and nothing in the suite will notice. - **Register once, per socket.** Each new socket the page opens re-enters the handler; state you keep in a closure is shared across those calls unless you scope it inside. - **The URL is not the page origin.** Dashboards frequently stream from a different host, so a pattern built from the page's own origin will silently match nothing.

  • The routed handler runs, but the dashboard stays on its loading spinner. What is happening?
    The app is waiting for a server response that never comes. Without `connectToServer()`, messages the page sends go nowhere unless the handler answers them, so a subscribe or auth frame is left unacknowledged. Add an `ws.onMessage()` branch that recognises that frame and `ws.send()` the acknowledgement the app expects before pushing data frames.
  • How do you make the dashboard's disconnected state appear from a routed socket?
    Call `ws.close({ code: 1011, reason: 'feed down' })` from the handler, at the point in the test where the drop should happen. The page's socket sees a close with that code and reason, so the app's own close handling runs and the offline banner renders deterministically instead of depending on a real outage.
  • Why might a route match nothing even though the pattern looks right?
    Patterns run against the `ws://` or `wss://` socket URL, which often lives on a different host from the page. A glob built from the page origin therefore misses. Log `ws.url()` from a `page.on('websocket')` listener to see the real URL, then match with a `RegExp` or a predicate over the `URL` object.

saying these in an interview costs you the question

  • Assumes a routed socket still reaches the real server
  • Registers the route after the navigation that opens the socket
  • Expects Playwright to serialise objects into frames
  • Forgets the app needs its subscribe frame answered
  • Builds the pattern from the page origin, not the socket URL
  • Treats a hand-written mock as permanently accurate