WebSocket testing with Playwright splits into two patterns: observing real connections by listening to framesent and framereceived events, and mocking the server entirely using page.routeWebSocket() (Playwright 1.48+) to control what messages the app receives without a live backend. This article covers both approaches, plus testing connection drop and reconnection behavior, with full TypeScript examples for each scenario.
What WebSocket testing looks like in practice
Unlike HTTP requests, WebSockets are persistent bidirectional connections. A single connection can carry thousands of messages over its lifetime. Testing WebSockets means:
1. Verifying that the connection is established
2. Verifying that the app sends the right messages
3. Verifying that the app correctly handles incoming messages
4. Testing what happens when the connection drops
Listening to WebSocket events
Playwright fires events for WebSocket connections:
import { test, expect } from '@playwright/test';
test('chat app establishes WebSocket connection', async ({ page }) => {
// Listen for WebSocket connections
const wsConnected = page.waitForEvent('websocket');
await page.goto('/chat');
const ws = await wsConnected;
expect(ws.url()).toContain('/ws/chat');
// WebSocket is now open
console.log('Connected to:', ws.url());
});Capturing WebSocket messages
test('chat sends message via WebSocket', async ({ page }) => {
const messages: string[] = [];
page.on('websocket', ws => {
ws.on('framesent', frame => {
// Messages sent from browser to server
if (frame.text) messages.push(frame.text);
});
});
await page.goto('/chat');
await page.getByPlaceholder('Type a message').fill('Hello, world!');
await page.keyboard.press('Enter');
// Verify the WebSocket message was sent
await expect.poll(() => messages).toContain(
JSON.stringify({ type: 'message', content: 'Hello, world!' })
);
});
test('chat receives message and displays it', async ({ page }) => {
const receivedFrames: string[] = [];
page.on('websocket', ws => {
ws.on('framereceived', frame => {
// Messages received from server by browser
if (frame.text) receivedFrames.push(frame.text);
});
});
await page.goto('/chat');
// Wait for at least one message to arrive (e.g., join confirmation)
await expect.poll(() => receivedFrames.length).toBeGreaterThan(0);
// Verify message is displayed in UI
const firstMessage = JSON.parse(receivedFrames[0]);
await expect(page.getByText(firstMessage.content)).toBeVisible();
});framesent: messages the browser sent to the server.
framereceived: messages the server sent to the browser.
Mocking WebSocket responses
The most powerful pattern: intercept the connection and simulate server messages without running a real server.
Playwright doesn't have a built-in WebSocket mock API, but you can inject a mock in the browser context:
test('app shows notification when server sends alert', async ({ page }) => {
// Intercept WebSocket and simulate server behavior
await page.addInitScript(() => {
const OriginalWebSocket = window.WebSocket;
window.WebSocket = class MockWebSocket extends EventTarget {
url: string;
readyState = 1; // OPEN
constructor(url: string) {
super();
this.url = url;
// Simulate connection open
setTimeout(() => {
this.dispatchEvent(new Event('open'));
// Simulate server sending a notification after 100ms
setTimeout(() => {
const messageEvent = new MessageEvent('message', {
data: JSON.stringify({ type: 'notification', text: 'New order received!' }),
});
this.dispatchEvent(messageEvent);
}, 100);
}, 0);
}
send(data: string) {
// Record sent messages if needed
console.log('WebSocket send:', data);
}
close() {
this.readyState = 3;
this.dispatchEvent(new CloseEvent('close'));
}
} as any;
});
await page.goto('/dashboard');
// Verify the notification appears when the WebSocket message arrives
await expect(page.getByRole('alert')).toBeVisible({ timeout: 2000 });
await expect(page.getByRole('alert')).toHaveText('New order received!');
});Testing disconnection and reconnection
Real-time apps need to handle connection loss gracefully:
test('app shows reconnecting state when WebSocket drops', async ({ page }) => {
let wsInstance: any;
await page.addInitScript(() => {
const OriginalWebSocket = window.WebSocket;
window.WebSocket = class extends OriginalWebSocket {
constructor(url: string) {
super(url);
// Expose instance for test control
(window as any).__wsInstance = this;
}
} as any;
});
await page.goto('/chat');
// Wait for connection to establish
await page.waitForEvent('websocket');
// Force-close the WebSocket connection
await page.evaluate(() => {
(window as any).__wsInstance?.close();
});
// Verify UI shows reconnecting state
await expect(page.getByText('Reconnecting...')).toBeVisible();
// After some time, verify it attempts to reconnect
await expect(page.getByText('Connected')).toBeVisible({ timeout: 10000 });
});Using route.fulfill for WebSocket in newer Playwright versions
Playwright 1.48+ introduced page.routeWebSocket() for more ergonomic WebSocket mocking:
// Playwright 1.48+
test('order status updates in real time', async ({ page }) => {
await page.routeWebSocket('/ws/orders', ws => {
ws.onopen = () => {
// Send initial state
ws.send(JSON.stringify({ orderId: '123', status: 'processing' }));
};
ws.onmessage = (message) => {
const data = JSON.parse(message.data);
if (data.type === 'subscribe' && data.orderId === '123') {
// Simulate status progression
setTimeout(() => ws.send(JSON.stringify({ orderId: '123', status: 'shipped' })), 500);
setTimeout(() => ws.send(JSON.stringify({ orderId: '123', status: 'delivered' })), 1000);
}
};
});
await page.goto('/orders/123');
await expect(page.getByTestId('status')).toHaveText('Processing');
await expect(page.getByTestId('status')).toHaveText('Shipped', { timeout: 2000 });
await expect(page.getByTestId('status')).toHaveText('Delivered', { timeout: 3000 });
});Check your Playwright version before using routeWebSocket: it's not available in older versions.
When to test WebSockets
Not all real-time features need dedicated WebSocket tests. Prioritize:
- Connection establishment for critical features (chat, live dashboard)
- Behavior when the connection drops (shows error? reconnects? loses data?)
- Message ordering for sequential operations
- Authentication over WebSocket (is the connection rejected without valid auth?)
Skip WebSocket-level testing when:
- The feature works reliably and the E2E UI test already covers it
- The WebSocket is third-party (you don't control the server)
- Testing the message format is better covered by a backend unit test