skip to content

In Playwright, what does WebSocketRoute.connectToServer() change about a routed socket's message flow?

level: middleimportance: should knowfreq 40%

answer

  1. Opting back in to the real server
  2. One socket, two route objects
  3. Automatic both ways until you interfere
  4. Taking over one direction only
  5. Forward it yourself with send

basics

~20 s

It opens the real connection the route suppressed and returns a second WebSocketRoute for the server side. Messages then flow through automatically in both directions until onMessage is called on a side, which makes that direction the test's responsibility.

solid answer

~40 s

By default a routed socket never reaches the real endpoint. `ws.connectToServer()` opens that connection and returns a second `WebSocketRoute` representing the server side, giving you two objects for one logical socket. Once connected, Playwright forwards messages both ways for you — page to server and server to page. Calling `onMessage()` on one of the two routes cancels the automatic forwarding **for that direction only**: from then on the handler decides, and must call `send()` on the other route to pass anything along. `send()` is directional too — on the original route it reaches the page, on the server-side route it reaches the server. That asymmetry is what lets a test forward a real forecast feed untouched while injecting one extra alert frame.

code

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

test('injects a severe-weather alert into the live feed', async ({ page }) => {
  await page.routeWebSocket(/\/forecast/, ws => {
    const server = ws.connectToServer();

    server.onMessage(message => {
      ws.send(message);
      if (String(message).includes('"kind":"hourly"')) {
        ws.send(JSON.stringify({ kind: 'alert', level: 'severe', city: 'Lisbon' }));
      }
    });
  });

  await page.goto('/dashboard');

  await expect(page.getByRole('alert')).toContainText('Severe');
});

go deeper

for a junior

Recall that a routed socket is mocked by default, and connectToServer is the call that lets the real server back into the picture, returning a second route object for that side.

for a middle

Explain the forwarding rule precisely: automatic in both directions once connected, cancelled per direction by onMessage, and manual from then on via send on the other route.

for a senior

Show when pass-through with one injected frame beats a full mock — an undocumented handshake, a rare alert path — and how you keep the resulting test from inheriting the feed's flakiness.

for a principal

Weigh the dependency you are accepting: a suite that connects to a real feed trades determinism and offline capability for fidelity, and that trade should be a deliberate, project-level choice.

## Two routes for one socket A `page.routeWebSocket()` handler starts out as a full mock: nothing reaches the real endpoint. `ws.connectToServer()` reverses that. It opens the connection Playwright suppressed and returns a **second** `WebSocketRoute` that stands for the server side. You now hold two objects for one logical connection, and every method on them is read relative to which object you called it on. | Call | On the original route | On the route from `connectToServer()` | |---|---|---| | `onMessage(handler)` | Messages the **page** sends | Messages the **server** sends | | `send(message)` | Delivers to the **page** | Delivers to the **server** | | `close(options)` | Ends that side of the connection | Ends that side of the connection | | `onClose(handler)` | The page closed | The server closed | In Playwright 1.63 that symmetry is the whole mental model: the route in your hand is a stand-in for the far end, so `send` always means "deliver to the peer this object faces". ## Automatic forwarding, and how you cancel it Once connected, Playwright pipes the two sides together for you: what the page sends goes to the server, what the server sends goes to the page, and a test that only calls `connectToServer()` is a transparent pass-through. The rule that trips people is what happens next: - Calling `onMessage()` on a route **turns off** the automatic forwarding for the messages that route handles. - It does **not** affect the other direction, which keeps forwarding untouched. - Once you have taken over a direction, forwarding is manual: your handler must call `send()` on the *other* route for the message to arrive. So `server.onMessage(m => ws.send(m))` is the explicit spelling of the behaviour you had for free, and it is the hook where a test can rewrite, drop, delay or duplicate a frame. ## A worked example on a weather dashboard The forecast feed pushes hourly readings all day and a severe-weather alert perhaps twice a year. Mocking the whole channel means hand-writing the hourly stream; ignoring the channel means never testing the alert banner. Pass-through with one injection solves both: 1. Route the forecast URL and call `connectToServer()` immediately. 2. Call `onMessage()` on the server-side route, taking over the server-to-page direction. 3. Forward each real frame with `ws.send(message)` so the dashboard behaves exactly as in production. 4. When a frame of the right kind arrives, additionally `ws.send()` a synthetic alert frame. 5. Assert on the alert banner, knowing every other pixel came from the real feed. The page-to-server direction is left alone, so subscriptions, heartbeats and auth frames continue to work without the test knowing anything about them. ## Where it beats a full mock, and where it does not - **Beats it** when the app's handshake is complex and undocumented — you would spend an afternoon reverse-engineering frames you can simply let through. - **Beats it** when you want a real-environment smoke test that still exercises one hard-to-reach branch. - **Loses** when the suite must run with no access to the feed at all, such as an offline CI job. - **Loses** when determinism is the point: real frames arrive at real times, and a test that asserts on their content inherits the feed's variability. ## Traps - **Forgetting to forward.** The moment you add `onMessage()` on a side, silence on that side is the new default. A dashboard that suddenly renders nothing after you added a handler is almost always a missing `send()`. - **Sending to the wrong end.** Calling `send()` on the server-side route pushes your synthetic alert *to the server*, which will ignore it, while the UI never changes. - **Assuming both directions stop.** Taking over server-to-page leaves page-to-server forwarding intact; that is a feature, not something to work around. - **Assuming payloads are decoded.** Handlers receive a `string` or a `Buffer`; converting, parsing and re-serialising are the test's job. ## Closing, from either side `onClose(handler)` reads the same way as everything else: on the original route it runs when the page closes the socket, on the server-side route when the server does, each receiving the close code and reason. `close({ code, reason })` ends the side you call it on, which is how a test drives the dashboard's offline banner while an otherwise real connection is in place. - Register `onClose` on the page side to assert the dashboard tears its subscription down when the user leaves the view. - Register it on the server side to notice the feed hanging up, and decide whether that is a failure or the scenario under test. - Remember a close is not a message: it never reaches an `onMessage` handler, so a test that only watches messages just sees the stream stop.

  • After adding server.onMessage(), the dashboard stopped updating. Why?
    Registering `onMessage()` on the server-side route cancels automatic forwarding for server-to-page traffic, so real frames now stop at your handler. Nothing reaches the page until the handler calls `ws.send(message)` on the page-side route. Adding that forward restores production behaviour and leaves the handler free to inject extra frames.
  • How do you drop a specific frame coming from the real server?
    Take over that direction with `server.onMessage()`, forward everything with `ws.send(message)`, and simply return without sending for the frames you want suppressed. That is how a test proves the dashboard's stale-data indicator appears when a channel goes quiet, while the rest of the live stream continues normally.

saying these in an interview costs you the question

  • Thinks a routed socket contacts the server without connectToServer
  • Expects forwarding to continue after registering onMessage
  • Calls send on the server route to update the page
  • Believes taking over one direction stops both
  • Assumes Playwright parses frame payloads into objects