WebSockets trong Playwright (Bắt và kiểm tra realtime connection)
automationplaywright
Playwright hỗ trợ bắt WebSocket connection qua page.on('websocket'), cho phép đọc từng frame gửi/nhận (framereceived/framesent) để verify app realtime (chat, notification, live dashboard) hoạt động đúng. Giới hạn: chỉ inspect được frame, không mock sẵn được WS server — muốn mock phải can thiệp thêm.
Khái niệm & Bản chất
- WebSocket là kênh hai chiều bền (persistent) giữa browser và server — khác HTTP request/response một lần. App realtime (chat, noti, giá chứng khoán, live comment) dùng WS hoặc thư viện trên nó (Socket.IO, SignalR).
- page.on('websocket', handler): event phát khi page mở WS connection (sau handshake HTTP upgrade). Handler nhận
WebSocketobject của Playwright. - Frame là đơn vị dữ liệu: WS truyền bằng frame (text hoặc binary). Playwright phát
ws.on('framereceived')khi server → client,ws.on('framesent')khi client → server. - Bản chất là OBSERVE: Playwright hiện chỉ cho phép đọc/log frame — KHÔNG có
route.fulfilltương đương cho WS. Muốn fake data realtime phải tự build (xem gotchas).
Cách hoạt động & API chính
- ws.url(): URL của socket (
wss://...) — dùng phân biệt nhiều connection (VD: noti socket vs chat socket). - ws.on('framereceived', cb): callback nhận
{ payload }— string (text frame) hoặc Buffer (binary). Parse JSON nếu app dùng JSON message. - ws.on('framesent', cb): frame app gửi lên server — verify client emit đúng event/payload (VD: join room, typing).
- ws.on('close') / ws.isClosed(): theo dõi vòng đời socket.
- ws.on('socketerror'): bắt lỗi tầng socket.
- expect.poll + biến collect: vì frame đến bất đồng bộ, thường đẩy payload vào mảng rồi
await expect.poll(() => frames.length).toBeGreaterThan(0).
Ví dụ & Cú pháp thực tế
- ```ts import { test, expect } from '@playwright/test'; test('bắt và log frame websocket', async ({ page }) => { const received: string[] = []; page.on('websocket', (ws) => { console.log('WS opened:', ws.url()); ws.on('framereceived', ({ payload }) => { received.push(payload.toString()); console.log('<<', payload); }); ws.on('framesent', ({ payload }) => console.log('>>', payload)); }); await page.goto('https://chat.hoctest.com'); await page.getByRole('button', { name: 'Kết nối' }).click(); // Frame đến async — poll cho tới khi có await expect.poll(() => received.length).toBeGreaterThan(0); });
- ```ts test('verify app nhận được message realtime', async ({ page }) => { const messages: any[] = []; page.on('websocket', (ws) => { if (ws.url().includes('/notifications')) { ws.on('framereceived', ({ payload }) => { messages.push(JSON.parse(payload.toString())); }); } }); await page.goto('/dashboard'); // Trigger server push noti (qua API khác chẳng hạn) // ... rồi verify frame đến đúng format await expect .poll(() => messages.some((m) => m.type === 'NEW_ORDER')) .toBe(true); await expect(page.getByText('Đơn hàng mới')).toBeVisible(); });
- ```ts test('verify client emit đúng event', async ({ page }) => { const sent: any[] = []; page.on('websocket', (ws) => { ws.on('framesent', ({ payload }) => sent.push(payload.toString())); }); await page.goto('/chat/room-1'); await page.getByPlaceholder('Nhắn tin').fill('hello'); await page.getByPlaceholder('Nhắn tin').press('Enter'); // Kiểm tra app gửi frame chứa nội dung tin nhắn await expect .poll(() => sent.some((f) => f.includes('hello'))) .toBe(true); });
Best practices & Khi nào dùng
- Khi nào dùng: verify tính năng realtime thật sự hoạt động (socket mở được, frame đến đúng format, client emit đúng), debug flaky test liên quan noti/chat, assert UI update khi nhận push event.
- Filter theo ws.url() ngay trong handler — app thường có nhiều socket (analytics, noti, chat); log hết sẽ rất nhiễu.
- Parse frame thành object ngay:
JSON.parse(payload)vào mảng → assert trên object (m.type, m.payload.id) dễ hơn so sánh chuỗi raw. - Kết hợp API trigger: dùng
request.post(hoặc 1 page khác) để tạo sự kiện phía server, rồi chờ frame về — pattern verify end-to-end cho realtime. - Kiên nhẫn với expect.poll: frame không đến tức thì — poll với timeout hợp lý (5-10s), đừng assert ngay sau click.
Gotchas, So sánh & Lưu ý
- KHÔNG mock được WS bằng page.route: route chỉ làm việc với HTTP request/response — socket sau handshake thoát khỏi route. Đừng tìm
route.fulfillcho WS, không có. - Cách 'mock' thực tế: (1) chạy WS server fake trong test bằng package
wsrồi trỏ app tới đó (override URL qua route/page.addInitScript), hoặc (2)page.addInitScriptthaywindow.WebSocketbằng class fake — phức tạp nhưng kiểm soát 100%. - Socket.IO không phải raw JSON: frame của Socket.IO có protocol prefix (
42["event",{...}]) — parse phải strip prefix hoặc match contains. - Frame binary: protobuf/MessagePack đến dạng Buffer — cần decoder riêng của app, không đọc được thô.
- So với HTTP testing: HTTP có request rõ ràng để wait; WS là stream liên tục nên phải đổi tư duy sang 'collect → poll' thay vì 'wait 1 response'.
- Reconnect/reconnect storm: app reconnect tạo WS object mới → handler cũ vẫn attach vào socket cũ.
page.on('websocket')bắt được từng connection mới nên vẫn ok, nhưng mảng collect cần ý thức socket đổi.
