PRACTICAL GUIDE / playwright websocket mocking
Mock and Inspect WebSocket Traffic in Playwright Tests
Mock and inspect Playwright WebSocket traffic with controlled messages, frame evidence, proxy interception, reconnect tests, and stable UI assertions.
In this guide10 sections
- Model the Conversation Before Writing the Route
- Fully Mock a Small Protocol
- Inspect Frames Without Changing the Server
- Proxy and Modify Real Traffic Carefully
- Test Close, Error, and Reconnect Behavior
- Synchronize on Protocol and UI Signals
- Diagnose WebSocket Test Failures
- Choose Observation, Mocking, or Proxying
- Operational Checklist
- Action Plan
What you will learn
- Model the Conversation Before Writing the Route
- Fully Mock a Small Protocol
- Inspect Frames Without Changing the Server
- Proxy and Modify Real Traffic Carefully
Real-time UI tests become unreliable when they wait for an uncontrolled socket server and then assert whatever arrives first. The stable approach is to choose the layer under test: observe real frames, replace the server with a protocol-aware mock, or proxy the real connection while modifying one direction. Each mode answers a different question.
Keep the UI assertion central. A socket frame matters because it changes a notification badge, collaboration status, market value, or reconnect indicator. Frame assertions can explain the transport contract, but a green frame listener alone does not prove that the application handled the event.
Model the Conversation Before Writing the Route
List the handshake URL, requested subprotocol if any, client subscription message, server snapshot, incremental update, heartbeat, error, and close behavior. Mark which messages the scenario needs and which are transport noise. This protocol sketch prevents a mock from sending an update before the application has subscribed or from ignoring a heartbeat that the client requires to stay connected.
The WebSocketRoute API documentation explains two core modes. Without connectToServer(), the route acts as the server and can fully mock communication. With connectToServer(), it can forward, modify, or block traffic between the page and the real service.
Animated field map
WebSocket Test From Handshake to UI
The test owns the route before connection, controls or inspects messages, and ties protocol evidence to a visible application outcome.
01 / socket handshake
Socket handshake
Open the expected URL and negotiate the application's protocol boundary.
02 / websocket route
WebSocket route
Choose observation, complete mocking, or a connection to the real server.
03 / message transform
Message transform
Send, forward, delay, modify, or reject selected protocol frames.
04 / application update
Application update
Let the client reducer and rendering path process the controlled event.
05 / frame ui assertions
Frame and UI assertions
Verify transport intent together with the user-visible result.
Fully Mock a Small Protocol
Register the route before navigation because many applications connect during startup. The mock below waits for the page's subscription, sends an initial snapshot, then sends one update. It parses the client message instead of depending on exact JSON field order.
import { expect, test } from "@playwright/test";
test("updates the incident count from a socket event", async ({ page }) => {
await page.routeWebSocket("**/realtime", (socket) => {
socket.onMessage((raw) => {
const message = JSON.parse(raw.toString()) as {
type: string;
channel?: string;
};
if (message.type !== "subscribe" || message.channel !== "incidents") {
socket.close({ code: 1008, reason: "Unsupported subscription" });
return;
}
socket.send(JSON.stringify({
type: "incident.snapshot",
incidents: [{ id: "inc-7", status: "open" }],
}));
socket.send(JSON.stringify({
type: "incident.created",
incident: { id: "inc-8", status: "open" },
}));
});
});
await page.goto("/operations");
await expect(page.getByTestId("open-incident-count")).toHaveText("2");
await expect(page.getByText("inc-8")).toBeVisible();
});The scenario controls ordering, but it should not send frames impossibly fast if the real protocol requires an acknowledgement between them. Add only enough state to represent valid server behavior. A mock that accepts every client message can conceal a broken subscription contract.
Inspect Frames Without Changing the Server
Passive inspection is appropriate when the live test environment is stable and the goal is to prove that the page sends a subscription or receives a particular event. The Playwright network guide exposes framesent and framereceived events after the page emits a websocket event.
import { expect, test } from "@playwright/test";
test("subscribes to the current workspace channel", async ({ page }) => {
const sentMessages: unknown[] = [];
page.on("websocket", (socket) => {
socket.on("framesent", ({ payload }) => {
if (typeof payload !== "string") return;
try {
sentMessages.push(JSON.parse(payload));
} catch {
// Ignore non-JSON heartbeat frames for this assertion.
}
});
});
await page.goto("/workspaces/ws-42/activity");
await expect.poll(() => sentMessages).toContainEqual({
type: "subscribe",
channel: "workspace:ws-42",
});
});Attach a filtered message summary only on failure. Raw frame streams can contain tokens, personal content, or high-volume heartbeat data. Parsing also makes the assertion resilient to JSON property order while preserving the fields that define the protocol contract.
Proxy and Modify Real Traffic Carefully
Call connectToServer() when the test needs the actual service but must alter a selected message. Automatic forwarding applies until an onMessage handler is installed for that direction. Once the handler exists, the test owns forwarding on that side.
import { expect, test } from "@playwright/test";
test("renders a server event with an unknown severity safely", async ({ page }) => {
await page.routeWebSocket("**/realtime", (client) => {
const server = client.connectToServer();
server.onMessage((raw) => {
const message = JSON.parse(raw.toString()) as Record<string, unknown>;
if (message.type === "incident.created") {
client.send(JSON.stringify({ ...message, severity: "future-value" }));
return;
}
client.send(raw);
});
});
await page.goto("/operations");
await expect(page.getByText("Unknown severity")).toBeVisible();
await expect(page.getByTestId("operations-shell")).toBeVisible();
});This intercepts only server-to-page traffic. Page-to-server messages continue through automatically because no client-side onMessage handler was installed. If both sides get handlers, explicitly forward both sides or the socket will appear connected while messages disappear in the route.
Test Close, Error, and Reconnect Behavior
A realtime client must handle more than successful messages. Close the mocked route with the code and reason relevant to the product, then assert connection status, retry policy, and recovery. Use the page clock when reconnect backoff is implemented with browser timers, and avoid real sleeps.
Test at least a transient close that reconnects, a policy or authentication close that does not loop, and a malformed frame that the client contains without crashing the page. If the product queues outbound edits while disconnected, verify ordering and deduplication after recovery. An automatically retried create operation needs an idempotency strategy at the backend as well as a green UI test.
Do not assert exact reconnect milliseconds unless timing is the requirement. Assert states such as "offline indicator appears," "one reconnect attempt occurs after the controlled interval," and "the indicator clears after a valid snapshot." Those observations survive harmless timer tuning.
Synchronize on Protocol and UI Signals
Fixed waits are especially weak for sockets because connection, subscription, and message delivery are separate asynchronous stages. Let the mock respond to the actual subscription message, and let expect wait for the visible result. For passive inspection, use expect.poll over a filtered collection rather than sleeping and reading an array once.
When a test needs to send a server event at a precise moment, expose a promise or callback from a helper that captures the routed socket. Wait until the application has subscribed, perform the UI action that establishes preconditions, then send the event. This creates a clear happens-before relationship without depending on machine speed.
Diagnose WebSocket Test Failures
If the route never runs, it was probably registered after connection or its URL pattern does not match the complete socket URL. Log the URL from the page's websocket event in a protected debug run. Also check whether the application uses a fallback transport such as polling in that environment.
If the page connects but receives nothing, inspect forwarding ownership. Installing onMessage stops automatic forwarding for that direction. A proxy handler that observes a frame but never calls send has intentionally swallowed it. If disconnects happen after a few seconds, model required heartbeats, acknowledgement frames, or subprotocol negotiation.
Binary payloads require protocol-aware decoding; converting arbitrary bytes to UTF-8 can corrupt them. Move codec testing to a lower layer and keep browser assertions focused on a few known fixtures. For intermittent ordering failures, verify that the mock waits for subscription and that the application reducer defines behavior for out-of-order events.
Choose Observation, Mocking, or Proxying
Observation preserves real integration behavior but depends on service data and availability. Full mocking is deterministic and ideal for rare messages, errors, and ordering, yet it can drift from the server protocol. Proxy interception retains the live handshake and most traffic while allowing one mutation, but it is the most complex mode and can accidentally change forwarding semantics.
Use schema or contract tests to keep mocks aligned with protocol definitions. Reserve a few live tests for connection and authorization. Put most browser edge cases behind a complete mock, where message order and close behavior are controlled. The layers complement one another rather than compete.
Operational Checklist
- Document handshake URL, subscription, snapshot, update, heartbeat, and close messages.
- Register listeners and routes before the page creates its socket.
- Parse structured frames and assert only behavior-relevant fields.
- Tie frame evidence to a visible or stored application outcome.
- Forward every intercepted direction explicitly in proxy mode.
- Model required heartbeats and acknowledgements without asserting noise.
- Cover transient reconnect and final authentication rejection.
- Replace fixed waits with subscription handshakes and web-first assertions.
- Filter and redact frame diagnostics before attaching them.
- Keep one live protocol path alongside deterministic mocked edge cases.
Action Plan
Select one realtime feature and write its smallest valid conversation on paper. Add a fully mocked test that waits for the subscription, sends a snapshot and update, and checks the resulting UI. Add passive frame inspection for one live handshake, then cover a close or malformed-message path with controlled timing. Only introduce proxy interception when a real connection plus one deliberate mutation answers a risk that the simpler modes cannot.
// LIVE COURSE / THE TESTING ACADEMY
Playwright Automation Mastery
Go beyond Selenium. Master Playwright with JS/TS in 90 days.
From the instructor behind this guide.
Playwright jobs are growing 8x faster than Selenium. 90 days / 75+ live hrs / Tue-Thu-Sat 7 AM IST.
PRIMARY REFERENCES
Verify the details at the source
QABattle guides are practical explanations. Product behavior, standards, and APIs can change, so use these primary references for the canonical details.
- 01Playwright documentation
Microsoft
Canonical API, locator, fixture, browser, and test-runner behavior.
- 02Playwright best practices
Microsoft
Official guidance for resilient tests, isolation, and user-facing locators.
- 03
FAQ / QUICK ANSWERS
Questions testers ask
Can Playwright mock a WebSocket without starting a real server?
Yes. Register routeWebSocket before the page opens the connection and handle client messages on the route. If the handler does not connect to the server, the test can send controlled frames directly to the page.
How can a Playwright test inspect WebSocket frames?
Listen for the page websocket event, then collect framesent and framereceived events from the WebSocket object. Register listeners before navigation and assert selected protocol messages rather than dumping every frame.
What happens after connectToServer is called?
The route connects to the real server and forwards messages automatically until an onMessage handler is installed for a direction. Once intercepted, that direction must be forwarded explicitly or the message stops there.
How should WebSocket heartbeat messages be handled in tests?
Model the heartbeat contract when it influences connection health, reconnect, or UI state. Otherwise keep it out of assertions, but ensure the mock responds well enough that the application does not close the socket for a reason unrelated to the scenario.
Should WebSocket tests assert raw JSON strings?
Usually no. Parse frames and assert message type, identifiers, ordering, and fields relevant to the behavior. Exact raw-string assertions are brittle when harmless field order or metadata changes.
RELATED GUIDES
Continue the learning route
GUIDE 01
Deterministic Network Tests with Playwright HAR Recording and Replay
Record, sanitize, and replay Playwright HAR fixtures for deterministic network tests, strict request matching, safer updates, and clear failures.
GUIDE 02
Model Multi-User Workflows with Isolated Playwright Browser Contexts
Model Playwright multi-user workflows with isolated browser contexts, actor-specific state, deterministic handoffs, parallel-safe data, and cleanup.
GUIDE 03
20 Playwright Network Mocking and Routing Interview Scenarios
Practice 20 senior Playwright network scenarios on routing, HAR replay, service workers, WebSockets, pass-through traffic, and mock failure analysis.
GUIDE 04
Mock Browser APIs Before App Startup with Playwright addInitScript
Mock browser APIs before app startup with Playwright addInitScript, stateful event doubles, read-only overrides, call recording, and focused UI assertions.