In Playwright, what does WebSocketRoute.connectToServer() change about a routed socket's message flow?
answer
- Opting back in to the real server
- One socket, two route objects
- Automatic both ways until you interfere
- Taking over one direction only
- Forward it yourself with send
basics
~20 sIt 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 sBy 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 linesimport { 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
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.
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.
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.
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