Testing WebSocket with Playwright
Playwright WebSocket testing provides end-to-end automation for real-time applications. Using Playwright’s built-in WebSocket interception, you can monitor messages, validate data flow, and test live features like chat applications, notifications, and streaming dashboards without manual intervention.
Understanding Playwright’s WebSocket API
Playwright exposes WebSocket connections through the page.on('websocket') event listener. This API captures all WebSocket traffic initiated by the browser page, allowing you to intercept both outgoing and incoming messages.
import { test, expect } from '@playwright/test';
test('intercept websocket connection', async ({ page }) => {
const wsMessages: string[] = [];
page.on('websocket', ws => {
console.log(`WebSocket opened: ${ws.url()}`);
ws.on('framesent', event => {
console.log('Sent:', event.payload);
});
ws.on('framereceived', event => {
console.log('Received:', event.payload);
wsMessages.push(event.payload);
});
ws.on('close', () => console.log('WebSocket closed'));
});
await page.goto('https://example.com/chat');
// WebSocket events are captured automatically
await page.waitForTimeout(2000);
expect(wsMessages.length).toBeGreaterThan(0);
});
The websocket event fires whenever a new WebSocket connection is established. Each WebSocket object provides framesent and framereceived events to track individual messages.
Intercepting WebSocket Messages
Intercepting WebSocket messages enables validation of real-time communication patterns. You can verify message structure, timing, and content without modifying application code.
import { test, expect } from '@playwright/test';
interface ChatMessage {
type: 'message' | 'join' | 'leave';
user: string;
content: string;
timestamp: number;
}
test('validate chat message structure', async ({ page }) => {
const receivedMessages: ChatMessage[] = [];
page.on('websocket', ws => {
if (ws.url().includes('wss://api.example.com/chat')) {
ws.on('framereceived', event => {
try {
const message = JSON.parse(event.payload) as ChatMessage;
receivedMessages.push(message);
} catch (error) {
console.error('Failed to parse message:', error);
}
});
}
});
await page.goto('https://example.com/chat');
await page.fill('#username', 'testuser');
await page.click('#join-chat');
// Wait for join confirmation
await page.waitForFunction(() => {
return document.querySelector('.chat-status')?.textContent?.includes('Connected');
});
// Send a message
await page.fill('#message-input', 'Hello, world!');
await page.click('#send-button');
// Wait for message to be received
await page.waitForTimeout(1000);
const sentMessage = receivedMessages.find(
msg => msg.type === 'message' && msg.content === 'Hello, world!'
);
expect(sentMessage).toBeDefined();
expect(sentMessage?.user).toBe('testuser');
expect(sentMessage?.timestamp).toBeGreaterThan(Date.now() - 5000);
});
Asserting on WebSocket Messages
Effective WebSocket testing requires assertions on message content, order, and timing. Playwright’s flexible API allows you to build custom matchers and validation logic.
import { test, expect } from '@playwright/test';
test('assert message order and content', async ({ page }) => {
const messages: Array<{ type: string; data: any }> = [];
page.on('websocket', ws => {
ws.on('framereceived', event => {
messages.push(JSON.parse(event.payload));
});
});
await page.goto('https://example.com/live-dashboard');
// Wait for specific message sequence
await page.waitForFunction(() => {
return window.performance.now() > 3000;
});
// Assert connection established
expect(messages[0].type).toBe('connection_ack');
expect(messages[0].data.sessionId).toBeTruthy();
// Assert data stream started
const dataMessages = messages.filter(msg => msg.type === 'data_update');
expect(dataMessages.length).toBeGreaterThan(0);
// Assert data structure
dataMessages.forEach(msg => {
expect(msg.data).toHaveProperty('timestamp');
expect(msg.data).toHaveProperty('value');
expect(typeof msg.data.value).toBe('number');
});
});
For more comprehensive testing strategies, see how to test WebSockets.
Testing Chat and Real-Time UIs
Chat applications and real-time interfaces require validation of user interactions, message delivery, and UI updates. Playwright excels at testing these scenarios end-to-end.
import { test, expect } from '@playwright/test';
test('multi-user chat simulation', async ({ browser }) => {
const context1 = await browser.newContext();
const context2 = await browser.newContext();
const page1 = await context1.newPage();
const page2 = await context2.newPage();
const user1Messages: string[] = [];
const user2Messages: string[] = [];
page1.on('websocket', ws => {
ws.on('framereceived', event => {
user1Messages.push(event.payload);
});
});
page2.on('websocket', ws => {
ws.on('framereceived', event => {
user2Messages.push(event.payload);
});
});
// User 1 joins
await page1.goto('https://example.com/chat');
await page1.fill('#username', 'Alice');
await page1.click('#join-chat');
// User 2 joins
await page2.goto('https://example.com/chat');
await page2.fill('#username', 'Bob');
await page2.click('#join-chat');
// User 1 sends message
await page1.fill('#message-input', 'Hi Bob!');
await page1.click('#send-button');
// Wait for message to propagate
await page2.waitForSelector('.message:has-text("Hi Bob!")');
// Verify message appears in User 2's UI
const messageElement = await page2.locator('.message').last();
await expect(messageElement).toContainText('Alice');
await expect(messageElement).toContainText('Hi Bob!');
// Verify both users received the message via WebSocket
const user2ReceivedMessage = user2Messages.some(msg =>
msg.includes('Hi Bob!') && msg.includes('Alice')
);
expect(user2ReceivedMessage).toBe(true);
await context1.close();
await context2.close();
});
Mocking WebSocket with MSW
Mock Service Worker (MSW) provides powerful WebSocket mocking capabilities for controlled testing environments. Combine MSW with Playwright for deterministic e2e tests.
First, install MSW:
npm install msw --save-dev
Create a WebSocket mock handler:
// mocks/handlers.ts
import { ws } from 'msw';
const chat = ws.link('wss://api.example.com/chat');
export const handlers = [
chat.addEventListener('connection', ({ client }) => {
console.log('Mock WebSocket connection opened');
// Send welcome message
client.send(JSON.stringify({
type: 'connection_ack',
data: { sessionId: 'mock-session-123' }
}));
client.addEventListener('message', (event) => {
const message = JSON.parse(event.data as string);
// Echo messages back with mock response
client.send(JSON.stringify({
type: 'message',
user: message.user,
content: message.content,
timestamp: Date.now()
}));
});
})
];
Integrate MSW with Playwright:
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
webServer: {
command: 'npm run dev:mocks',
port: 3000,
reuseExistingServer: !process.env.CI,
},
use: {
baseURL: 'http://localhost:3000',
},
});
// tests/chat-with-msw.spec.ts
import { test, expect } from '@playwright/test';
test.beforeEach(async ({ page }) => {
// MSW intercepts WebSocket connections automatically
await page.goto('/chat');
});
test('chat with mocked websocket', async ({ page }) => {
await page.fill('#username', 'testuser');
await page.click('#join-chat');
// Wait for mock connection acknowledgment
await expect(page.locator('.chat-status')).toContainText('Connected');
await page.fill('#message-input', 'Test message');
await page.click('#send-button');
// Mock handler echoes message back
await expect(page.locator('.message').last()).toContainText('Test message');
});
MSW WebSocket mocking eliminates dependencies on live servers, enabling faster and more reliable test execution. Learn more about WebSocket testing tools.
Insomnia WebSocket Testing
Insomnia provides a graphical interface for manual WebSocket testing during development. While Playwright handles automated e2e tests, Insomnia excels at exploratory testing and debugging.
To test WebSockets in Insomnia:
- Create a new WebSocket request
- Enter your WebSocket URL:
wss://api.example.com/chat - Click “Connect” to establish the connection
- Send JSON messages through the interface
- Inspect received messages in real-time
Export Insomnia collections to share test scenarios with your team. For automated workflows, use Insomnia’s CLI with Playwright tests:
import { test } from '@playwright/test';
import { exec } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);
test('run insomnia collection', async () => {
const { stdout, stderr } = await execAsync(
'inso run test "WebSocket Tests" --env Dev'
);
console.log('Insomnia output:', stdout);
if (stderr) {
throw new Error(`Insomnia tests failed: ${stderr}`);
}
});
For foundational concepts, review what is WebSocket.
Unit Testing WebSocket Handlers with Jest and Vitest
Unit testing WebSocket event handlers requires mocking WebSocket connections. Jest and Vitest both support WebSocket testing with appropriate mocks.
// ws-handler.ts
export class ChatHandler {
private ws: WebSocket;
constructor(url: string) {
this.ws = new WebSocket(url);
this.setupListeners();
}
private setupListeners() {
this.ws.onopen = () => {
console.log('Connected');
this.ws.send(JSON.stringify({ type: 'join', user: 'testuser' }));
};
this.ws.onmessage = (event) => {
const message = JSON.parse(event.data);
this.handleMessage(message);
};
}
private handleMessage(message: any) {
if (message.type === 'message') {
console.log(`${message.user}: ${message.content}`);
}
}
sendMessage(content: string) {
this.ws.send(JSON.stringify({
type: 'message',
content
}));
}
}
Test with Vitest:
// ws-handler.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ChatHandler } from './ws-handler';
import WS from 'jest-websocket-mock';
describe('ChatHandler', () => {
let server: WS;
beforeEach(async () => {
server = new WS('ws://localhost:8080/chat');
});
afterEach(() => {
WS.clean();
});
it('sends join message on connection', async () => {
const handler = new ChatHandler('ws://localhost:8080/chat');
await server.connected;
const message = await server.nextMessage;
const parsed = JSON.parse(message as string);
expect(parsed.type).toBe('join');
expect(parsed.user).toBe('testuser');
});
it('handles incoming messages', async () => {
const consoleSpy = vi.spyOn(console, 'log');
const handler = new ChatHandler('ws://localhost:8080/chat');
await server.connected;
await server.nextMessage; // consume join message
server.send(JSON.stringify({
type: 'message',
user: 'alice',
content: 'Hello!'
}));
expect(consoleSpy).toHaveBeenCalledWith('alice: Hello!');
});
it('sends messages to server', async () => {
const handler = new ChatHandler('ws://localhost:8080/chat');
await server.connected;
await server.nextMessage; // consume join message
handler.sendMessage('Test message');
const message = await server.nextMessage;
const parsed = JSON.parse(message as string);
expect(parsed.type).toBe('message');
expect(parsed.content).toBe('Test message');
});
});
Install the required package:
npm install jest-websocket-mock --save-dev
CI Integration
Integrate Playwright WebSocket tests into continuous integration pipelines for automated validation on every commit.
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npx playwright test
env:
CI: true
WS_TEST_URL: wss://staging-api.example.com/chat
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
Configure environment-specific WebSocket URLs:
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
baseURL: process.env.BASE_URL || 'http://localhost:3000',
},
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
wsEndpoint: process.env.WS_TEST_URL || 'ws://localhost:8080'
},
},
],
});
For Docker-based CI environments, ensure WebSocket support is enabled:
FROM mcr.microsoft.com/playwright:v1.40.0-jammy
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
CMD ["npx", "playwright", "test"]
FAQ
How do I test WebSocket reconnection logic with Playwright?
Use Playwright’s network interception to simulate connection failures:
test('websocket reconnection', async ({ page, context }) => {
let connectionCount = 0;
page.on('websocket', ws => {
connectionCount++;
if (connectionCount === 1) {
// Simulate connection drop after 2 seconds
setTimeout(() => {
ws.close();
}, 2000);
}
});
await page.goto('https://example.com/chat');
// Wait for reconnection
await page.waitForTimeout(5000);
expect(connectionCount).toBeGreaterThan(1);
await expect(page.locator('.connection-status')).toContainText('Connected');
});
Can I test WebSocket connections with authentication headers?
Playwright captures WebSocket connections but doesn’t directly modify headers. Set authentication at the page level before establishing connections:
test('authenticated websocket', async ({ page }) => {
await page.goto('https://example.com/login');
await page.fill('#username', 'testuser');
await page.fill('#password', 'password');
await page.click('#login-button');
// Authentication token stored in localStorage/cookies
await page.waitForURL('https://example.com/dashboard');
page.on('websocket', ws => {
// WebSocket uses existing auth from page context
expect(ws.url()).toContain('wss://api.example.com');
});
});
How do I test WebSocket message performance and timing?
Track message timestamps to validate latency and throughput:
test('websocket message latency', async ({ page }) => {
const latencies: number[] = [];
page.on('websocket', ws => {
ws.on('framesent', event => {
const sentTime = Date.now();
(event as any).sentTime = sentTime;
});
ws.on('framereceived', event => {
const receivedTime = Date.now();
const payload = JSON.parse(event.payload);
if (payload.echo) {
const latency = receivedTime - (payload.sentTime || 0);
latencies.push(latency);
}
});
});
await page.goto('https://example.com/chat');
// Send 10 messages and measure round-trip time
for (let i = 0; i < 10; i++) {
await page.evaluate((index) => {
(window as any).ws.send(JSON.stringify({
type: 'echo',
sentTime: Date.now(),
index
}));
}, i);
await page.waitForTimeout(100);
}
await page.waitForTimeout(2000);
const avgLatency = latencies.reduce((a, b) => a + b, 0) / latencies.length;
expect(avgLatency).toBeLessThan(100); // Assert <100ms latency
});
How do I test binary WebSocket messages with Playwright?
Playwright’s framereceived event provides payload as string or buffer. Handle binary data accordingly:
test('binary websocket messages', async ({ page }) => {
const binaryMessages: Buffer[] = [];
page.on('websocket', ws => {
ws.on('framereceived', event => {
if (typeof event.payload !== 'string') {
binaryMessages.push(Buffer.from(event.payload));
}
});
});
await page.goto('https://example.com/binary-stream');
await page.waitForTimeout(3000);
expect(binaryMessages.length).toBeGreaterThan(0);
// Verify binary data structure
const firstMessage = binaryMessages[0];
expect(firstMessage.readUInt32BE(0)).toBeGreaterThan(0);
});