.agents/skills/playwright-best-practices/browser-apis/websockets.md
test('chat connects via websocket', async ({page}) => {
// Listen for WebSocket connection
const wsPromise = page.waitForEvent('websocket')
await page.goto('/chat')
const ws = await wsPromise
expect(ws.url()).toContain('/ws/chat')
// Wait for connection to be established
await ws.waitForEvent('framesent')
})
test('receives real-time updates', async ({page}) => {
const messages: string[] = []
// Set up listener before navigation
page.on('websocket', (ws) => {
ws.on('framereceived', (frame) => {
messages.push(frame.payload as string)
})
})
await page.goto('/dashboard')
// Wait for some messages
await expect.poll(() => messages.length).toBeGreaterThan(0)
// Verify message format
const data = JSON.parse(messages[0])
expect(data).toHaveProperty('type')
})
test('sends correct message format', async ({page}) => {
const sentMessages: string[] = []
page.on('websocket', (ws) => {
ws.on('framesent', (frame) => {
sentMessages.push(frame.payload as string)
})
})
await page.goto('/chat')
await page.getByLabel('Message').fill('Hello!')
await page.getByRole('button', {name: 'Send'}).click()
// Verify sent message
await expect.poll(() => sentMessages.length).toBeGreaterThan(0)
const sent = JSON.parse(sentMessages[sentMessages.length - 1])
expect(sent).toEqual({
type: 'message',
content: 'Hello!',
})
})
test('displays incoming chat message', async ({page}) => {
await page.goto('/chat')
// Wait for WebSocket to be ready
await page.waitForFunction(() => (window as any).chatSocket?.readyState === 1)
// Simulate incoming message
await page.evaluate(() => {
const event = new MessageEvent('message', {
data: JSON.stringify({
type: 'message',
from: 'Alice',
content: 'Hello there!',
}),
})
;(window as any).chatSocket.dispatchEvent(event)
})
await expect(page.getByText('Alice: Hello there!')).toBeVisible()
})
test('mock websocket entirely', async ({page, context}) => {
// Intercept the WebSocket upgrade
await context.route('**/ws/**', async (route) => {
// For WebSocket routes, we can't fulfill directly
// Instead, use page.evaluate to mock the client-side
})
// Alternative: Mock at application level
await page.addInitScript(() => {
const OriginalWebSocket = window.WebSocket
;(window as any).WebSocket = function (url: string) {
const ws = {
readyState: 1,
send: (data: string) => {
console.log('WS Send:', data)
},
close: () => {},
addEventListener: () => {},
removeEventListener: () => {},
}
setTimeout(() => ws.onopen?.(), 100)
return ws
}
})
await page.goto('/chat')
})
// fixtures/websocket.fixture.ts
import {test as base, Page} from '@playwright/test'
type WsMessage = {type: string; [key: string]: any}
type WebSocketFixtures = {
mockWebSocket: {
injectMessage: (message: WsMessage) => Promise<void>
getSentMessages: () => Promise<WsMessage[]>
}
}
export const test = base.extend<WebSocketFixtures>({
mockWebSocket: async ({page}, use) => {
const sentMessages: WsMessage[] = []
// Capture sent messages
await page.addInitScript(() => {
;(window as any).__wsSent = []
const OriginalWebSocket = window.WebSocket
window.WebSocket = function (url: string) {
const ws = new OriginalWebSocket(url)
const originalSend = ws.send.bind(ws)
ws.send = (data: string) => {
;(window as any).__wsSent.push(JSON.parse(data))
originalSend(data)
}
;(window as any).__ws = ws
return ws
} as any
})
await use({
injectMessage: async (message) => {
await page.evaluate((msg) => {
const event = new MessageEvent('message', {
data: JSON.stringify(msg),
})
;(window as any).__ws?.dispatchEvent(event)
}, message)
},
getSentMessages: async () => {
return page.evaluate(() => (window as any).__wsSent || [])
},
})
},
})
// Usage
test('chat with mocked websocket', async ({page, mockWebSocket}) => {
await page.goto('/chat')
// Inject incoming message
await mockWebSocket.injectMessage({
type: 'message',
from: 'Bob',
content: 'Hi!',
})
await expect(page.getByText('Bob: Hi!')).toBeVisible()
// Send a reply
await page.getByLabel('Message').fill('Hello Bob!')
await page.getByRole('button', {name: 'Send'}).click()
// Verify sent message
const sent = await mockWebSocket.getSentMessages()
expect(sent).toContainEqual(expect.objectContaining({content: 'Hello Bob!'}))
})
test('displays live notification', async ({page}) => {
await page.goto('/dashboard')
// Simulate notification via WebSocket
await page.evaluate(() => {
const event = new MessageEvent('message', {
data: JSON.stringify({
type: 'notification',
title: 'New Order',
message: 'Order #123 received',
}),
})
;(window as any).notificationSocket.dispatchEvent(event)
})
await expect(page.getByRole('alert')).toContainText('Order #123 received')
})
test('updates stock price in real-time', async ({page}) => {
await page.goto('/stocks/AAPL')
const priceElement = page.getByTestId('stock-price')
const initialPrice = await priceElement.textContent()
// Simulate price update
await page.evaluate(() => {
const event = new MessageEvent('message', {
data: JSON.stringify({
type: 'price_update',
symbol: 'AAPL',
price: 150.25,
}),
})
;(window as any).stockSocket.dispatchEvent(event)
})
await expect(priceElement).not.toHaveText(initialPrice!)
await expect(priceElement).toContainText('150.25')
})
test('shows collaborator cursor', async ({page}) => {
await page.goto('/document/123')
// Simulate another user's cursor position
await page.evaluate(() => {
const event = new MessageEvent('message', {
data: JSON.stringify({
type: 'cursor',
userId: 'user-456',
userName: 'Alice',
position: {x: 100, y: 200},
}),
})
;(window as any).docSocket.dispatchEvent(event)
})
await expect(page.getByTestId('cursor-user-456')).toBeVisible()
await expect(page.getByText('Alice')).toBeVisible()
})
test('receives SSE updates', async ({page}) => {
// Mock SSE endpoint
await page.route('**/api/events', (route) => {
route.fulfill({
status: 200,
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
body: `data: {"type":"update","value":42}\n\n`,
})
})
await page.goto('/live-data')
await expect(page.getByTestId('value')).toHaveText('42')
})
test('handles multiple SSE events', async ({page}) => {
await page.route('**/api/events', async (route) => {
const encoder = new TextEncoder()
const events = [`data: {"count":1}\n\n`, `data: {"count":2}\n\n`, `data: {"count":3}\n\n`]
route.fulfill({
status: 200,
headers: {'Content-Type': 'text/event-stream'},
body: events.join(''),
})
})
await page.goto('/counter')
// Should receive all events
await expect(page.getByTestId('count')).toHaveText('3')
})
test('handles connection loss gracefully', async ({page}) => {
await page.goto('/chat')
// Simulate connection close
await page.evaluate(() => {
;(window as any).chatSocket.close()
})
// Should show disconnected state
await expect(page.getByText('Reconnecting...')).toBeVisible()
})
test('reconnects after connection loss', async ({page}) => {
await page.goto('/chat')
// Simulate disconnect
await page.evaluate(() => {
;(window as any).chatSocket.close()
})
await expect(page.getByText('Reconnecting...')).toBeVisible()
// Simulate reconnection
await page.evaluate(() => {
const event = new Event('open')
;(window as any).chatSocket = {readyState: 1}
;(window as any).chatSocket.dispatchEvent?.(event)
})
// Force component to re-check connection
await page.evaluate(() => {
window.dispatchEvent(new Event('online'))
})
await expect(page.getByText('Connected')).toBeVisible()
})
| Anti-Pattern | Problem | Solution |
|---|---|---|
| Not waiting for WebSocket ready | Messages sent too early | Wait for readyState === 1 |
| Testing against real WebSocket server | Flaky, timing-dependent | Mock WebSocket messages |
| Ignoring connection state | Tests pass but feature broken | Test connected/disconnected states |
| No cleanup of listeners | Memory leaks in tests | Clean up event listeners |