Тестирование WebSocket в Playwright строится по двум паттернам: наблюдение за реальными соединениями через прослушивание событий framesent и framereceived, и полное мокирование сервера через page.routeWebSocket() (Playwright 1.48+): контролируешь какие сообщения получает приложение без живого бэкенда. Статья разбирает оба подхода, плюс тестирование обрыва соединения и логики переподключения, с полными TypeScript-примерами для каждого сценария.

Как выглядит тестирование WebSocket на практике

В отличие от HTTP-запросов, WebSocket работает как постоянное двунаправленное соединение. Одно соединение может передавать тысячи сообщений за время жизни. Тестирование WebSocket включает:

1. Проверку что соединение установлено

2. Проверку что приложение отправляет правильные сообщения

3. Проверку что приложение корректно обрабатывает входящие сообщения

4. Тестирование поведения при обрыве соединения

Прослушивание WebSocket-событий

Playwright генерирует события для WebSocket-соединений:

import { test, expect } from '@playwright/test';

test('chat app establishes WebSocket connection', async ({ page }) => {
  // Слушаем WebSocket-соединения
  const wsConnected = page.waitForEvent('websocket');

  await page.goto('/chat');

  const ws = await wsConnected;
  expect(ws.url()).toContain('/ws/chat');

  // WebSocket теперь открыт
  console.log('Connected to:', ws.url());
});

Захват WebSocket-сообщений

test('chat sends message via WebSocket', async ({ page }) => {
  const messages: string[] = [];

  page.on('websocket', ws => {
    ws.on('framesent', frame => {
      // Сообщения отправленные из браузера на сервер
      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');

  // Проверяем что WebSocket-сообщение было отправлено
  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 => {
      // Сообщения полученные браузером от сервера
      if (frame.text) receivedFrames.push(frame.text);
    });
  });

  await page.goto('/chat');

  // Ждём хотя бы одно сообщение (например, подтверждение входа)
  await expect.poll(() => receivedFrames.length).toBeGreaterThan(0);

  // Проверяем что сообщение отображается в UI
  const firstMessage = JSON.parse(receivedFrames[0]);
  await expect(page.getByText(firstMessage.content)).toBeVisible();
});

framesent: сообщения которые браузер отправил серверу. framereceived: сообщения которые сервер отправил браузеру.

Мокирование WebSocket-ответов

Самый мощный паттерн: перехватить соединение и симулировать сообщения сервера без запуска реального сервера.

Playwright не имеет встроенного API для мокирования WebSocket, но можно внедрить мок в контекст браузера:

test('app shows notification when server sends alert', async ({ page }) => {
  // Перехватываем WebSocket и симулируем поведение сервера
  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;
        
        // Симулируем открытие соединения
        setTimeout(() => {
          this.dispatchEvent(new Event('open'));
          
          // Симулируем отправку уведомления сервером через 100мс
          setTimeout(() => {
            const messageEvent = new MessageEvent('message', {
              data: JSON.stringify({ type: 'notification', text: 'New order received!' }),
            });
            this.dispatchEvent(messageEvent);
          }, 100);
        }, 0);
      }
      
      send(data: string) {
        // Записываем отправленные сообщения если нужно
        console.log('WebSocket send:', data);
      }
      
      close() {
        this.readyState = 3;
        this.dispatchEvent(new CloseEvent('close'));
      }
    } as any;
  });

  await page.goto('/dashboard');

  // Проверяем что уведомление появляется когда приходит WebSocket-сообщение
  await expect(page.getByRole('alert')).toBeVisible({ timeout: 2000 });
  await expect(page.getByRole('alert')).toHaveText('New order received!');
});

Тестирование отключения и переподключения

Приложения реального времени должны корректно обрабатывать потерю соединения:

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);
        // Экспортируем экземпляр для управления из теста
        (window as any).__wsInstance = this;
      }
    } as any;
  });

  await page.goto('/chat');
  
  // Ждём установки соединения
  await page.waitForEvent('websocket');

  // Принудительно закрываем WebSocket-соединение
  await page.evaluate(() => {
    (window as any).__wsInstance?.close();
  });

  // Проверяем что UI показывает состояние переподключения
  await expect(page.getByText('Reconnecting...')).toBeVisible();
  
  // Через некоторое время проверяем что попытка переподключения произошла
  await expect(page.getByText('Connected')).toBeVisible({ timeout: 10000 });
});

Использование routeWebSocket в новых версиях Playwright

Playwright 1.48+ представил page.routeWebSocket() для более удобного мокирования WebSocket:

// Playwright 1.48+
test('order status updates in real time', async ({ page }) => {
  await page.routeWebSocket('/ws/orders', ws => {
    ws.onopen = () => {
      // Отправляем начальное состояние
      ws.send(JSON.stringify({ orderId: '123', status: 'processing' }));
    };

    ws.onmessage = (message) => {
      const data = JSON.parse(message.data);
      if (data.type === 'subscribe' && data.orderId === '123') {
        // Симулируем прогресс статуса
        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 });
});

Перед использованием routeWebSocket проверь версию Playwright: в старых версиях этого метода нет.

Когда тестировать WebSocket

Не все фичи реального времени нуждаются в отдельных WebSocket-тестах. Приоритизируй:

  • Установку соединения для критичных фич (чат, live-дашборд)
  • Поведение при обрыве соединения (показывает ошибку? переподключается? теряет данные?)
  • Порядок сообщений для последовательных операций
  • Аутентификацию через WebSocket (отклоняется ли соединение без валидного auth?)

Пропускай WebSocket-тестирование когда фича работает надёжно и E2E UI-тест уже покрывает её, когда WebSocket от сторонней компании (сервер не под твоим контролем), или когда формат сообщений лучше покрывается юнит-тестом на бэкенде.

→ See also: Перехват сети, моки и стабы в Playwright | API-тестирование с Playwright APIRequestContext (без Postman) | Тестирование GraphQL API с Playwright: запросы, мутации и обработка ошибок