mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-12 17:22:38 +02:00
10 KiB
10 KiB
WebSocket & Real-Time Testing
Table of Contents
- WebSocket Basics
- Mocking WebSocket Messages
- Testing Real-Time Features
- Server-Sent Events
- Reconnection Testing
WebSocket Basics
Wait for WebSocket Connection
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");
});
Monitor WebSocket Messages
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");
});
Capture Sent Messages
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!",
});
});
Mocking WebSocket Messages
Inject Messages via Page Evaluate
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();
});
Mock WebSocket with Route Handler
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");
});
WebSocket Mock Fixture
// 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!" }),
);
});
Testing Real-Time Features
Live Notifications
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");
});
Live Data Updates
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");
});
Collaborative Editing
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();
});
Server-Sent Events
Test SSE Updates
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");
});
Simulate Multiple SSE Events
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");
});
Reconnection Testing
Test Connection Loss
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 Reconnection
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-Patterns to Avoid
| 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 |
Related References
- Network: See network-advanced.md for HTTP mocking patterns
- Assertions: See assertions-waiting.md for polling patterns
- Multi-User: See multi-user.md for real-time collaboration testing with multiple users