El testing de WebSockets con Playwright se divide en dos patrones: observar conexiones reales escuchando los eventos framesent y framereceived, y mockear el servidor completamente usando page.routeWebSocket() (Playwright 1.48+) para controlar qué mensajes recibe la app sin un backend en ejecución. Este artículo cubre ambos enfoques, más el testing del comportamiento ante caída y reconexión, con ejemplos completos en TypeScript para cada escenario.

Cómo se ve el testing de WebSockets en la práctica

A diferencia de las peticiones HTTP, los WebSockets son conexiones bidireccionales persistentes. Una sola conexión puede llevar miles de mensajes a lo largo de su vida útil. Probar WebSockets significa:

1. Verificar que la conexión se establece

2. Verificar que la app envía los mensajes correctos

3. Verificar que la app maneja correctamente los mensajes entrantes

4. Probar qué pasa cuando la conexión se cae

Escuchar eventos de WebSocket

Playwright lanza eventos para las conexiones WebSocket:

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

test('la app de chat establece conexión WebSocket', async ({ page }) => {
  // Escuchar conexiones WebSocket
  const wsConnected = page.waitForEvent('websocket');

  await page.goto('/chat');

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

  // El WebSocket ahora está abierto
  console.log('Conectado a:', ws.url());
});

Capturar mensajes de WebSocket

test('el chat envía mensaje via WebSocket', async ({ page }) => {
  const messages: string[] = [];

  page.on('websocket', ws => {
    ws.on('framesent', frame => {
      // Mensajes enviados desde el navegador al servidor
      if (frame.text) messages.push(frame.text);
    });
  });

  await page.goto('/chat');
  await page.getByPlaceholder('Escribí un mensaje').fill('¡Hola, mundo!');
  await page.keyboard.press('Enter');

  // Verificar que el mensaje WebSocket fue enviado
  await expect.poll(() => messages).toContain(
    JSON.stringify({ type: 'message', content: '¡Hola, mundo!' })
  );
});

test('el chat recibe mensaje y lo muestra', async ({ page }) => {
  const receivedFrames: string[] = [];

  page.on('websocket', ws => {
    ws.on('framereceived', frame => {
      // Mensajes recibidos del servidor por el navegador
      if (frame.text) receivedFrames.push(frame.text);
    });
  });

  await page.goto('/chat');

  // Esperar que llegue al menos un mensaje (por ejemplo, confirmación de unión)
  await expect.poll(() => receivedFrames.length).toBeGreaterThan(0);

  // Verificar que el mensaje se muestra en la UI
  const firstMessage = JSON.parse(receivedFrames[0]);
  await expect(page.getByText(firstMessage.content)).toBeVisible();
});

framesent: mensajes que el navegador envió al servidor. framereceived: mensajes que el servidor envió al navegador.

Mockear respuestas de WebSocket

El patrón más poderoso: interceptar la conexión y simular mensajes del servidor sin ejecutar un servidor real.

Playwright no tiene una API de mock de WebSocket incorporada, pero puedes inyectar un mock en el contexto del navegador:

test('la app muestra notificación cuando el servidor envía una alerta', async ({ page }) => {
  // Interceptar WebSocket y simular comportamiento del servidor
  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;
        
        // Simular apertura de conexión
        setTimeout(() => {
          this.dispatchEvent(new Event('open'));
          
          // Simular que el servidor envía una notificación después de 100ms
          setTimeout(() => {
            const messageEvent = new MessageEvent('message', {
              data: JSON.stringify({ type: 'notification', text: '¡Nuevo pedido recibido!' }),
            });
            this.dispatchEvent(messageEvent);
          }, 100);
        }, 0);
      }
      
      send(data: string) {
        // Registrar mensajes enviados si es necesario
        console.log('WebSocket send:', data);
      }
      
      close() {
        this.readyState = 3;
        this.dispatchEvent(new CloseEvent('close'));
      }
    } as any;
  });

  await page.goto('/dashboard');

  // Verificar que la notificación aparece cuando llega el mensaje WebSocket
  await expect(page.getByRole('alert')).toBeVisible({ timeout: 2000 });
  await expect(page.getByRole('alert')).toHaveText('¡Nuevo pedido recibido!');
});

Probar desconexión y reconexión

Las apps en tiempo real necesitan manejar la pérdida de conexión de manera adecuada:

test('la app muestra estado de reconexión cuando cae el WebSocket', async ({ page }) => {
  await page.addInitScript(() => {
    const OriginalWebSocket = window.WebSocket;
    
    window.WebSocket = class extends OriginalWebSocket {
      constructor(url: string) {
        super(url);
        // Exponer instancia para control desde el test
        (window as any).__wsInstance = this;
      }
    } as any;
  });

  await page.goto('/chat');
  
  // Esperar que se establezca la conexión
  await page.waitForEvent('websocket');

  // Forzar el cierre de la conexión WebSocket
  await page.evaluate(() => {
    (window as any).__wsInstance?.close();
  });

  // Verificar que la UI muestra el estado de reconexión
  await expect(page.getByText('Reconectando...')).toBeVisible();
  
  // Después de un tiempo, verificar que intenta reconectarse
  await expect(page.getByText('Conectado')).toBeVisible({ timeout: 10000 });
});

Usar routeWebSocket en versiones más nuevas de Playwright

Playwright 1.48+ introdujo page.routeWebSocket() para un mockeo de WebSocket más ergonómico:

// Playwright 1.48+
test('el estado del pedido se actualiza en tiempo real', async ({ page }) => {
  await page.routeWebSocket('/ws/orders', ws => {
    ws.onopen = () => {
      // Enviar estado inicial
      ws.send(JSON.stringify({ orderId: '123', status: 'processing' }));
    };

    ws.onmessage = (message) => {
      const data = JSON.parse(message.data);
      if (data.type === 'subscribe' && data.orderId === '123') {
        // Simular progresión de estado
        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('Procesando');
  await expect(page.getByTestId('status')).toHaveText('Enviado', { timeout: 2000 });
  await expect(page.getByTestId('status')).toHaveText('Entregado', { timeout: 3000 });
});

Verifica tu versión de Playwright antes de usar routeWebSocket: no está disponible en versiones anteriores.

Cuándo probar WebSockets

No todas las funcionalidades en tiempo real necesitan tests dedicados de WebSocket. Prioriza:

  • El establecimiento de conexión para funcionalidades críticas (chat, dashboard en vivo)
  • El comportamiento cuando la conexión cae (¿muestra error? ¿reconecta? ¿pierde datos?)
  • El orden de mensajes para operaciones secuenciales
  • La autenticación sobre WebSocket (¿se rechaza la conexión sin autenticación válida?)

Omite el testing a nivel de WebSocket cuando la funcionalidad funciona de manera confiable y el test E2E de UI ya la cubre, cuando el WebSocket es de terceros (no controlas el servidor), o cuando probar el formato del mensaje está mejor cubierto por un test unitario del backend.

→ See also: Interceptación de Red, Mocks y Stubs en Playwright | Pruebas de API con el APIRequestContext de Playwright (Sin Postman) | Pruebas de API GraphQL con Playwright: Consultas, Mutaciones y Manejo de Errores