tests.ws

Testing WebSocket with Playwright

websocket testing playwright automation e2e msw

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:

  1. Create a new WebSocket request
  2. Enter your WebSocket URL: wss://api.example.com/chat
  3. Click “Connect” to establish the connection
  4. Send JSON messages through the interface
  5. 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);
});