@assertrx/dino 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +293 -0
- package/extensions/dino.ts +70 -0
- package/extensions/mabl-mcp.ts +306 -0
- package/extensions/playwright-mcp.ts +142 -0
- package/package.json +47 -0
- package/prompts/create-pr.md +110 -0
- package/prompts/generate-bdd-from-url.md +65 -0
- package/prompts/generate-pom-from-url-cli.md +48 -0
- package/prompts/generate-pom-from-url.md +45 -0
- package/prompts/generate-spec-from-feature.md +64 -0
- package/prompts/heal-playwright-failures.md +106 -0
- package/prompts/setup-playwright.md +81 -0
- package/scripts/start-dino.cmd +8 -0
- package/scripts/start-dino.mjs +226 -0
- package/scripts/start-dino.ps1 +9 -0
- package/scripts/start-dino.sh +10 -0
- package/skills/acli/SKILL.md +292 -0
- package/skills/bdd-gherkin/SKILL.md +552 -0
- package/skills/build-review-interface/SKILL.md +96 -0
- package/skills/error-analysis/SKILL.md +164 -0
- package/skills/eval-audit/SKILL.md +183 -0
- package/skills/evaluate-rag/SKILL.md +177 -0
- package/skills/generate-synthetic-data/SKILL.md +131 -0
- package/skills/mabl-mcp/SKILL.md +79 -0
- package/skills/playwright-cli/SKILL.md +246 -0
- package/skills/playwright-cli/advanced-workflows.md +679 -0
- package/skills/playwright-cli/core-commands.md +375 -0
- package/skills/playwright-cli/device-emulation.md +475 -0
- package/skills/playwright-cli/request-mocking.md +373 -0
- package/skills/playwright-cli/running-custom-code.md +553 -0
- package/skills/playwright-cli/screenshots-and-media.md +457 -0
- package/skills/playwright-cli/session-management.md +425 -0
- package/skills/playwright-cli/storage-and-auth.md +487 -0
- package/skills/playwright-cli/test-generation.md +336 -0
- package/skills/playwright-cli/tracing-and-debugging.md +501 -0
- package/skills/playwright-core/SKILL.md +101 -0
- package/skills/playwright-core/accessibility.md +1517 -0
- package/skills/playwright-core/angular.md +1086 -0
- package/skills/playwright-core/api-testing.md +1623 -0
- package/skills/playwright-core/assertions-and-waiting.md +703 -0
- package/skills/playwright-core/auth-flows.md +1081 -0
- package/skills/playwright-core/authentication.md +1409 -0
- package/skills/playwright-core/browser-apis.md +673 -0
- package/skills/playwright-core/browser-extensions.md +319 -0
- package/skills/playwright-core/canvas-and-webgl.md +494 -0
- package/skills/playwright-core/clock-and-time-mocking.md +427 -0
- package/skills/playwright-core/common-pitfalls.md +1318 -0
- package/skills/playwright-core/component-testing.md +1179 -0
- package/skills/playwright-core/configuration.md +731 -0
- package/skills/playwright-core/crud-testing.md +945 -0
- package/skills/playwright-core/debugging.md +827 -0
- package/skills/playwright-core/drag-and-drop.md +992 -0
- package/skills/playwright-core/electron-testing.md +622 -0
- package/skills/playwright-core/error-and-edge-cases.md +1181 -0
- package/skills/playwright-core/error-index.md +1903 -0
- package/skills/playwright-core/file-operations.md +749 -0
- package/skills/playwright-core/file-upload-download.md +1012 -0
- package/skills/playwright-core/fixtures-and-hooks.md +1072 -0
- package/skills/playwright-core/flaky-tests.md +883 -0
- package/skills/playwright-core/forms-and-validation.md +1056 -0
- package/skills/playwright-core/i18n-and-localization.md +622 -0
- package/skills/playwright-core/iframes-and-shadow-dom.md +488 -0
- package/skills/playwright-core/locator-strategy.md +588 -0
- package/skills/playwright-core/locators.md +743 -0
- package/skills/playwright-core/mobile-and-responsive.md +1669 -0
- package/skills/playwright-core/multi-context-and-popups.md +581 -0
- package/skills/playwright-core/multi-user-and-collaboration.md +477 -0
- package/skills/playwright-core/network-mocking.md +1395 -0
- package/skills/playwright-core/nextjs.md +1013 -0
- package/skills/playwright-core/performance-testing.md +608 -0
- package/skills/playwright-core/react.md +1082 -0
- package/skills/playwright-core/search-and-filter.md +1366 -0
- package/skills/playwright-core/security-testing.md +624 -0
- package/skills/playwright-core/service-workers-and-pwa.md +524 -0
- package/skills/playwright-core/test-architecture.md +569 -0
- package/skills/playwright-core/test-data-management.md +1209 -0
- package/skills/playwright-core/test-organization.md +947 -0
- package/skills/playwright-core/third-party-integrations.md +754 -0
- package/skills/playwright-core/trace-analysis.md +161 -0
- package/skills/playwright-core/visual-regression.md +1027 -0
- package/skills/playwright-core/vue.md +1162 -0
- package/skills/playwright-core/websockets-and-realtime.md +623 -0
- package/skills/playwright-core/when-to-mock.md +827 -0
- package/skills/playwright-pom/SKILL.md +94 -0
- package/skills/playwright-pom/page-object-model.md +921 -0
- package/skills/playwright-pom/pom-vs-fixtures-vs-helpers.md +937 -0
- package/skills/validate-evaluator/SKILL.md +215 -0
- package/skills/write-judge-prompt/SKILL.md +144 -0
- package/themes/dino.json +66 -0
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
# WebSockets and Real-Time Testing
|
|
2
|
+
|
|
3
|
+
> **When to use**: When your application uses WebSockets, Server-Sent Events (SSE), or polling for real-time features -- chat, live dashboards, notifications, collaborative editing, stock tickers, live sports scores.
|
|
4
|
+
> **Prerequisites**: [core/assertions-and-waiting.md](assertions-and-waiting.md), [core/fixtures-and-hooks.md](fixtures-and-hooks.md)
|
|
5
|
+
|
|
6
|
+
## Quick Reference
|
|
7
|
+
|
|
8
|
+
```typescript
|
|
9
|
+
// Listen for WebSocket connections
|
|
10
|
+
page.on('websocket', (ws) => {
|
|
11
|
+
console.log('WebSocket opened:', ws.url());
|
|
12
|
+
|
|
13
|
+
ws.on('framesent', (frame) => console.log('Sent:', frame.payload));
|
|
14
|
+
ws.on('framereceived', (frame) => console.log('Received:', frame.payload));
|
|
15
|
+
ws.on('close', () => console.log('WebSocket closed'));
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Mock a WebSocket via route (Playwright 1.48+)
|
|
19
|
+
await page.routeWebSocket('**/ws', (ws) => {
|
|
20
|
+
ws.onMessage((message) => {
|
|
21
|
+
ws.send(JSON.stringify({ echo: message }));
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Patterns
|
|
27
|
+
|
|
28
|
+
### Observing WebSocket Traffic
|
|
29
|
+
|
|
30
|
+
**Use when**: You need to verify that your app sends and receives the correct WebSocket messages without modifying them.
|
|
31
|
+
**Avoid when**: You need to intercept or mock the messages. Use `routeWebSocket` instead.
|
|
32
|
+
|
|
33
|
+
> **Playwright 1.61+**: HAR and trace recordings now include WebSocket requests. For post-mortem debugging you often don't need listener code at all — record a trace and inspect the WS traffic in the Trace Viewer or with `npx playwright trace requests` (see [trace-analysis.md](trace-analysis.md)).
|
|
34
|
+
|
|
35
|
+
**TypeScript**
|
|
36
|
+
```typescript
|
|
37
|
+
import { test, expect } from '@playwright/test';
|
|
38
|
+
|
|
39
|
+
test('chat message is sent over WebSocket', async ({ page }) => {
|
|
40
|
+
const messages: { direction: string; payload: string }[] = [];
|
|
41
|
+
|
|
42
|
+
page.on('websocket', (ws) => {
|
|
43
|
+
ws.on('framesent', (frame) => {
|
|
44
|
+
messages.push({ direction: 'sent', payload: String(frame.payload) });
|
|
45
|
+
});
|
|
46
|
+
ws.on('framereceived', (frame) => {
|
|
47
|
+
messages.push({ direction: 'received', payload: String(frame.payload) });
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
await page.goto('/chat');
|
|
52
|
+
await page.getByRole('textbox', { name: 'Message' }).fill('Hello!');
|
|
53
|
+
await page.getByRole('button', { name: 'Send' }).click();
|
|
54
|
+
|
|
55
|
+
// Wait for the message to appear in UI (confirms round-trip)
|
|
56
|
+
await expect(page.getByText('Hello!')).toBeVisible();
|
|
57
|
+
|
|
58
|
+
// Verify WebSocket traffic
|
|
59
|
+
const sentMessage = messages.find(
|
|
60
|
+
(m) => m.direction === 'sent' && m.payload.includes('Hello!')
|
|
61
|
+
);
|
|
62
|
+
expect(sentMessage).toBeDefined();
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**JavaScript**
|
|
67
|
+
```javascript
|
|
68
|
+
const { test, expect } = require('@playwright/test');
|
|
69
|
+
|
|
70
|
+
test('chat message is sent over WebSocket', async ({ page }) => {
|
|
71
|
+
const messages = [];
|
|
72
|
+
|
|
73
|
+
page.on('websocket', (ws) => {
|
|
74
|
+
ws.on('framesent', (frame) => {
|
|
75
|
+
messages.push({ direction: 'sent', payload: String(frame.payload) });
|
|
76
|
+
});
|
|
77
|
+
ws.on('framereceived', (frame) => {
|
|
78
|
+
messages.push({ direction: 'received', payload: String(frame.payload) });
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
await page.goto('/chat');
|
|
83
|
+
await page.getByRole('textbox', { name: 'Message' }).fill('Hello!');
|
|
84
|
+
await page.getByRole('button', { name: 'Send' }).click();
|
|
85
|
+
|
|
86
|
+
await expect(page.getByText('Hello!')).toBeVisible();
|
|
87
|
+
|
|
88
|
+
const sentMessage = messages.find(
|
|
89
|
+
(m) => m.direction === 'sent' && m.payload.includes('Hello!')
|
|
90
|
+
);
|
|
91
|
+
expect(sentMessage).toBeDefined();
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Waiting for a Specific WebSocket Message
|
|
96
|
+
|
|
97
|
+
**Use when**: Your test depends on a particular server-pushed message before proceeding.
|
|
98
|
+
**Avoid when**: The UI already reflects the message. Assert on the UI instead.
|
|
99
|
+
|
|
100
|
+
**TypeScript**
|
|
101
|
+
```typescript
|
|
102
|
+
import { test, expect } from '@playwright/test';
|
|
103
|
+
|
|
104
|
+
test('wait for server acknowledgment over WebSocket', async ({ page }) => {
|
|
105
|
+
// Create a promise that resolves when we get the specific message
|
|
106
|
+
const ackPromise = new Promise<void>((resolve) => {
|
|
107
|
+
page.on('websocket', (ws) => {
|
|
108
|
+
ws.on('framereceived', (frame) => {
|
|
109
|
+
const data = JSON.parse(String(frame.payload));
|
|
110
|
+
if (data.type === 'message_ack') {
|
|
111
|
+
resolve();
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
await page.goto('/chat');
|
|
118
|
+
await page.getByRole('textbox', { name: 'Message' }).fill('Important update');
|
|
119
|
+
await page.getByRole('button', { name: 'Send' }).click();
|
|
120
|
+
|
|
121
|
+
// Wait for server to acknowledge
|
|
122
|
+
await ackPromise;
|
|
123
|
+
|
|
124
|
+
// Now verify the message shows a "delivered" checkmark
|
|
125
|
+
await expect(page.getByTestId('message-status').last()).toHaveText('Delivered');
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**JavaScript**
|
|
130
|
+
```javascript
|
|
131
|
+
const { test, expect } = require('@playwright/test');
|
|
132
|
+
|
|
133
|
+
test('wait for server acknowledgment over WebSocket', async ({ page }) => {
|
|
134
|
+
const ackPromise = new Promise((resolve) => {
|
|
135
|
+
page.on('websocket', (ws) => {
|
|
136
|
+
ws.on('framereceived', (frame) => {
|
|
137
|
+
const data = JSON.parse(String(frame.payload));
|
|
138
|
+
if (data.type === 'message_ack') {
|
|
139
|
+
resolve();
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
await page.goto('/chat');
|
|
146
|
+
await page.getByRole('textbox', { name: 'Message' }).fill('Important update');
|
|
147
|
+
await page.getByRole('button', { name: 'Send' }).click();
|
|
148
|
+
|
|
149
|
+
await ackPromise;
|
|
150
|
+
await expect(page.getByTestId('message-status').last()).toHaveText('Delivered');
|
|
151
|
+
});
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Mocking WebSocket Messages with `routeWebSocket`
|
|
155
|
+
|
|
156
|
+
**Use when**: You want to control what the server sends to test specific UI states -- error messages, edge cases, high-volume data -- without a real backend.
|
|
157
|
+
**Avoid when**: You need to test actual server behavior. Use a real backend.
|
|
158
|
+
|
|
159
|
+
**TypeScript**
|
|
160
|
+
```typescript
|
|
161
|
+
import { test, expect } from '@playwright/test';
|
|
162
|
+
|
|
163
|
+
test('display notification when server pushes an alert', async ({ page }) => {
|
|
164
|
+
const wsRoute = await page.routeWebSocket('**/ws/notifications', (ws) => {
|
|
165
|
+
// Let the app send its initial handshake
|
|
166
|
+
ws.onMessage((message) => {
|
|
167
|
+
const data = JSON.parse(message);
|
|
168
|
+
if (data.type === 'subscribe') {
|
|
169
|
+
ws.send(JSON.stringify({ type: 'subscribed', channel: data.channel }));
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Push a notification after a short delay
|
|
174
|
+
setTimeout(() => {
|
|
175
|
+
ws.send(JSON.stringify({
|
|
176
|
+
type: 'notification',
|
|
177
|
+
title: 'Server Alert',
|
|
178
|
+
body: 'Deployment completed successfully',
|
|
179
|
+
severity: 'info',
|
|
180
|
+
}));
|
|
181
|
+
}, 500);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
await page.goto('/dashboard');
|
|
185
|
+
|
|
186
|
+
// Verify the notification appears in the UI
|
|
187
|
+
await expect(page.getByRole('alert')).toContainText('Deployment completed successfully');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test('handle WebSocket server error gracefully', async ({ page }) => {
|
|
191
|
+
await page.routeWebSocket('**/ws', (ws) => {
|
|
192
|
+
// Immediately close with an error code
|
|
193
|
+
ws.close({ code: 1011, reason: 'Internal server error' });
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
await page.goto('/chat');
|
|
197
|
+
|
|
198
|
+
// App should show a reconnection message, not crash
|
|
199
|
+
await expect(page.getByText('Connection lost. Reconnecting...')).toBeVisible();
|
|
200
|
+
});
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**JavaScript**
|
|
204
|
+
```javascript
|
|
205
|
+
const { test, expect } = require('@playwright/test');
|
|
206
|
+
|
|
207
|
+
test('display notification when server pushes an alert', async ({ page }) => {
|
|
208
|
+
await page.routeWebSocket('**/ws/notifications', (ws) => {
|
|
209
|
+
ws.onMessage((message) => {
|
|
210
|
+
const data = JSON.parse(message);
|
|
211
|
+
if (data.type === 'subscribe') {
|
|
212
|
+
ws.send(JSON.stringify({ type: 'subscribed', channel: data.channel }));
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
setTimeout(() => {
|
|
217
|
+
ws.send(JSON.stringify({
|
|
218
|
+
type: 'notification',
|
|
219
|
+
title: 'Server Alert',
|
|
220
|
+
body: 'Deployment completed successfully',
|
|
221
|
+
severity: 'info',
|
|
222
|
+
}));
|
|
223
|
+
}, 500);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
await page.goto('/dashboard');
|
|
227
|
+
await expect(page.getByRole('alert')).toContainText('Deployment completed successfully');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test('handle WebSocket server error gracefully', async ({ page }) => {
|
|
231
|
+
await page.routeWebSocket('**/ws', (ws) => {
|
|
232
|
+
ws.close({ code: 1011, reason: 'Internal server error' });
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
await page.goto('/chat');
|
|
236
|
+
await expect(page.getByText('Connection lost. Reconnecting...')).toBeVisible();
|
|
237
|
+
});
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
#### Inspecting Requested Subprotocols (`webSocketRoute.protocols()`, Playwright 1.60+)
|
|
241
|
+
|
|
242
|
+
**Use when**: Your client negotiates a WebSocket subprotocol (e.g. `graphql-transport-ws`, `wamp`, a versioned protocol string) and you want to assert it sent the right one, or branch your mock based on it.
|
|
243
|
+
**Avoid when**: Your app doesn't use subprotocols — there's nothing to inspect.
|
|
244
|
+
|
|
245
|
+
Playwright 1.60 adds `webSocketRoute.protocols()`, returning the array of subprotocols the client requested during the handshake.
|
|
246
|
+
|
|
247
|
+
**TypeScript**
|
|
248
|
+
```typescript
|
|
249
|
+
import { test, expect } from '@playwright/test';
|
|
250
|
+
|
|
251
|
+
test('client requests the graphql-ws subprotocol', async ({ page }) => {
|
|
252
|
+
await page.routeWebSocket('**/graphql', (ws) => {
|
|
253
|
+
// Assert the client negotiated the expected subprotocol
|
|
254
|
+
expect(ws.protocols()).toContain('graphql-transport-ws');
|
|
255
|
+
|
|
256
|
+
ws.onMessage((message) => {
|
|
257
|
+
const data = JSON.parse(message);
|
|
258
|
+
if (data.type === 'connection_init') {
|
|
259
|
+
ws.send(JSON.stringify({ type: 'connection_ack' }));
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
await page.goto('/explorer');
|
|
265
|
+
await expect(page.getByText('Connected')).toBeVisible();
|
|
266
|
+
});
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
**JavaScript**
|
|
270
|
+
```javascript
|
|
271
|
+
const { test, expect } = require('@playwright/test');
|
|
272
|
+
|
|
273
|
+
test('client requests the graphql-ws subprotocol', async ({ page }) => {
|
|
274
|
+
await page.routeWebSocket('**/graphql', (ws) => {
|
|
275
|
+
expect(ws.protocols()).toContain('graphql-transport-ws');
|
|
276
|
+
ws.onMessage((message) => {
|
|
277
|
+
const data = JSON.parse(message);
|
|
278
|
+
if (data.type === 'connection_init') {
|
|
279
|
+
ws.send(JSON.stringify({ type: 'connection_ack' }));
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
await page.goto('/explorer');
|
|
285
|
+
await expect(page.getByText('Connected')).toBeVisible();
|
|
286
|
+
});
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### Forwarding with Modification (Man-in-the-Middle)
|
|
290
|
+
|
|
291
|
+
**Use when**: You want to connect to the real server but intercept, modify, or inject messages.
|
|
292
|
+
**Avoid when**: Full mocking (`routeWebSocket` without `connectToServer`) is sufficient.
|
|
293
|
+
|
|
294
|
+
**TypeScript**
|
|
295
|
+
```typescript
|
|
296
|
+
import { test, expect } from '@playwright/test';
|
|
297
|
+
|
|
298
|
+
test('inject a fake high-priority message into real stream', async ({ page }) => {
|
|
299
|
+
await page.routeWebSocket('**/ws/feed', (ws) => {
|
|
300
|
+
const server = ws.connectToServer();
|
|
301
|
+
|
|
302
|
+
// Forward all messages from server to client, but inject extras
|
|
303
|
+
server.onMessage((message) => {
|
|
304
|
+
ws.send(message); // Forward the real message
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// Forward all client messages to server
|
|
308
|
+
ws.onMessage((message) => {
|
|
309
|
+
server.send(message);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// Inject a synthetic message after 1 second
|
|
313
|
+
setTimeout(() => {
|
|
314
|
+
ws.send(JSON.stringify({
|
|
315
|
+
type: 'alert',
|
|
316
|
+
priority: 'high',
|
|
317
|
+
text: 'Injected test alert',
|
|
318
|
+
}));
|
|
319
|
+
}, 1000);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
await page.goto('/live-feed');
|
|
323
|
+
await expect(page.getByText('Injected test alert')).toBeVisible();
|
|
324
|
+
});
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
**JavaScript**
|
|
328
|
+
```javascript
|
|
329
|
+
const { test, expect } = require('@playwright/test');
|
|
330
|
+
|
|
331
|
+
test('inject a fake high-priority message into real stream', async ({ page }) => {
|
|
332
|
+
await page.routeWebSocket('**/ws/feed', (ws) => {
|
|
333
|
+
const server = ws.connectToServer();
|
|
334
|
+
|
|
335
|
+
server.onMessage((message) => {
|
|
336
|
+
ws.send(message);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
ws.onMessage((message) => {
|
|
340
|
+
server.send(message);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
setTimeout(() => {
|
|
344
|
+
ws.send(JSON.stringify({
|
|
345
|
+
type: 'alert',
|
|
346
|
+
priority: 'high',
|
|
347
|
+
text: 'Injected test alert',
|
|
348
|
+
}));
|
|
349
|
+
}, 1000);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
await page.goto('/live-feed');
|
|
353
|
+
await expect(page.getByText('Injected test alert')).toBeVisible();
|
|
354
|
+
});
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
### Server-Sent Events (SSE) Testing
|
|
358
|
+
|
|
359
|
+
**Use when**: Your app uses `EventSource` for server-to-client streaming (live logs, progress updates, news feeds).
|
|
360
|
+
**Avoid when**: The app uses WebSockets. SSE is HTTP-based and intercepted differently.
|
|
361
|
+
|
|
362
|
+
SSE responses are standard HTTP -- intercept them with `page.route()` and return a streaming response.
|
|
363
|
+
|
|
364
|
+
**TypeScript**
|
|
365
|
+
```typescript
|
|
366
|
+
import { test, expect } from '@playwright/test';
|
|
367
|
+
|
|
368
|
+
test('SSE live log stream displays entries', async ({ page }) => {
|
|
369
|
+
// Intercept the SSE endpoint and return controlled events
|
|
370
|
+
await page.route('**/api/logs/stream', async (route) => {
|
|
371
|
+
const events = [
|
|
372
|
+
'data: {"level":"info","message":"Server started"}\n\n',
|
|
373
|
+
'data: {"level":"warn","message":"High memory usage"}\n\n',
|
|
374
|
+
'data: {"level":"error","message":"Connection timeout"}\n\n',
|
|
375
|
+
];
|
|
376
|
+
|
|
377
|
+
await route.fulfill({
|
|
378
|
+
status: 200,
|
|
379
|
+
headers: {
|
|
380
|
+
'Content-Type': 'text/event-stream',
|
|
381
|
+
'Cache-Control': 'no-cache',
|
|
382
|
+
'Connection': 'keep-alive',
|
|
383
|
+
},
|
|
384
|
+
body: events.join(''),
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
await page.goto('/admin/logs');
|
|
389
|
+
|
|
390
|
+
await expect(page.getByText('Server started')).toBeVisible();
|
|
391
|
+
await expect(page.getByText('High memory usage')).toBeVisible();
|
|
392
|
+
await expect(page.getByText('Connection timeout')).toBeVisible();
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
test('SSE reconnection on connection drop', async ({ page }) => {
|
|
396
|
+
let requestCount = 0;
|
|
397
|
+
|
|
398
|
+
await page.route('**/api/events', async (route) => {
|
|
399
|
+
requestCount++;
|
|
400
|
+
if (requestCount === 1) {
|
|
401
|
+
// First request: send one event then close abruptly
|
|
402
|
+
await route.fulfill({
|
|
403
|
+
status: 200,
|
|
404
|
+
headers: { 'Content-Type': 'text/event-stream' },
|
|
405
|
+
body: 'data: {"msg":"first"}\n\n',
|
|
406
|
+
});
|
|
407
|
+
} else {
|
|
408
|
+
// Reconnection: send the next event
|
|
409
|
+
await route.fulfill({
|
|
410
|
+
status: 200,
|
|
411
|
+
headers: { 'Content-Type': 'text/event-stream' },
|
|
412
|
+
body: 'data: {"msg":"reconnected"}\n\n',
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
await page.goto('/live');
|
|
418
|
+
await expect(page.getByText('first')).toBeVisible();
|
|
419
|
+
// EventSource auto-reconnects; verify the app handles it
|
|
420
|
+
await expect(page.getByText('reconnected')).toBeVisible({ timeout: 10000 });
|
|
421
|
+
});
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
**JavaScript**
|
|
425
|
+
```javascript
|
|
426
|
+
const { test, expect } = require('@playwright/test');
|
|
427
|
+
|
|
428
|
+
test('SSE live log stream displays entries', async ({ page }) => {
|
|
429
|
+
await page.route('**/api/logs/stream', async (route) => {
|
|
430
|
+
const events = [
|
|
431
|
+
'data: {"level":"info","message":"Server started"}\n\n',
|
|
432
|
+
'data: {"level":"warn","message":"High memory usage"}\n\n',
|
|
433
|
+
'data: {"level":"error","message":"Connection timeout"}\n\n',
|
|
434
|
+
];
|
|
435
|
+
|
|
436
|
+
await route.fulfill({
|
|
437
|
+
status: 200,
|
|
438
|
+
headers: {
|
|
439
|
+
'Content-Type': 'text/event-stream',
|
|
440
|
+
'Cache-Control': 'no-cache',
|
|
441
|
+
'Connection': 'keep-alive',
|
|
442
|
+
},
|
|
443
|
+
body: events.join(''),
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
await page.goto('/admin/logs');
|
|
448
|
+
|
|
449
|
+
await expect(page.getByText('Server started')).toBeVisible();
|
|
450
|
+
await expect(page.getByText('High memory usage')).toBeVisible();
|
|
451
|
+
await expect(page.getByText('Connection timeout')).toBeVisible();
|
|
452
|
+
});
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### Polling-Based Real-Time Testing
|
|
456
|
+
|
|
457
|
+
**Use when**: Your app uses HTTP polling (setInterval + fetch) instead of WebSockets or SSE.
|
|
458
|
+
**Avoid when**: The app uses WebSockets or SSE -- use the patterns above.
|
|
459
|
+
|
|
460
|
+
**TypeScript**
|
|
461
|
+
```typescript
|
|
462
|
+
import { test, expect } from '@playwright/test';
|
|
463
|
+
|
|
464
|
+
test('polling updates dashboard data every interval', async ({ page }) => {
|
|
465
|
+
let callCount = 0;
|
|
466
|
+
|
|
467
|
+
await page.route('**/api/dashboard/stats', async (route) => {
|
|
468
|
+
callCount++;
|
|
469
|
+
const data = callCount === 1
|
|
470
|
+
? { activeUsers: 100, revenue: 5000 }
|
|
471
|
+
: { activeUsers: 142, revenue: 5250 };
|
|
472
|
+
|
|
473
|
+
await route.fulfill({
|
|
474
|
+
status: 200,
|
|
475
|
+
contentType: 'application/json',
|
|
476
|
+
body: JSON.stringify(data),
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
await page.goto('/dashboard');
|
|
481
|
+
|
|
482
|
+
// First poll result
|
|
483
|
+
await expect(page.getByTestId('active-users')).toHaveText('100');
|
|
484
|
+
|
|
485
|
+
// Wait for the second poll to update the UI
|
|
486
|
+
await expect(page.getByTestId('active-users')).toHaveText('142', { timeout: 15000 });
|
|
487
|
+
|
|
488
|
+
// Verify at least 2 requests were made
|
|
489
|
+
expect(callCount).toBeGreaterThanOrEqual(2);
|
|
490
|
+
});
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
**JavaScript**
|
|
494
|
+
```javascript
|
|
495
|
+
const { test, expect } = require('@playwright/test');
|
|
496
|
+
|
|
497
|
+
test('polling updates dashboard data every interval', async ({ page }) => {
|
|
498
|
+
let callCount = 0;
|
|
499
|
+
|
|
500
|
+
await page.route('**/api/dashboard/stats', async (route) => {
|
|
501
|
+
callCount++;
|
|
502
|
+
const data = callCount === 1
|
|
503
|
+
? { activeUsers: 100, revenue: 5000 }
|
|
504
|
+
: { activeUsers: 142, revenue: 5250 };
|
|
505
|
+
|
|
506
|
+
await route.fulfill({
|
|
507
|
+
status: 200,
|
|
508
|
+
contentType: 'application/json',
|
|
509
|
+
body: JSON.stringify(data),
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
await page.goto('/dashboard');
|
|
514
|
+
await expect(page.getByTestId('active-users')).toHaveText('100');
|
|
515
|
+
await expect(page.getByTestId('active-users')).toHaveText('142', { timeout: 15000 });
|
|
516
|
+
expect(callCount).toBeGreaterThanOrEqual(2);
|
|
517
|
+
});
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
### WebSocket Connection Lifecycle
|
|
521
|
+
|
|
522
|
+
**Use when**: You need to verify that your app handles connection, disconnection, and reconnection properly.
|
|
523
|
+
**Avoid when**: Connection lifecycle is not user-visible.
|
|
524
|
+
|
|
525
|
+
**TypeScript**
|
|
526
|
+
```typescript
|
|
527
|
+
import { test, expect } from '@playwright/test';
|
|
528
|
+
|
|
529
|
+
test('app reconnects after WebSocket drops', async ({ page }) => {
|
|
530
|
+
let connectionCount = 0;
|
|
531
|
+
|
|
532
|
+
await page.routeWebSocket('**/ws', (ws) => {
|
|
533
|
+
connectionCount++;
|
|
534
|
+
|
|
535
|
+
if (connectionCount === 1) {
|
|
536
|
+
// First connection: close after a brief moment
|
|
537
|
+
setTimeout(() => ws.close({ code: 1006, reason: 'Abnormal closure' }), 500);
|
|
538
|
+
} else {
|
|
539
|
+
// Second connection (reconnect): stay open and respond
|
|
540
|
+
ws.onMessage((message) => {
|
|
541
|
+
ws.send(JSON.stringify({ type: 'pong' }));
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
await page.goto('/app');
|
|
547
|
+
|
|
548
|
+
// App detects disconnect and shows status
|
|
549
|
+
await expect(page.getByText('Reconnecting...')).toBeVisible();
|
|
550
|
+
|
|
551
|
+
// App reconnects and status returns to normal
|
|
552
|
+
await expect(page.getByText('Connected')).toBeVisible({ timeout: 10000 });
|
|
553
|
+
|
|
554
|
+
expect(connectionCount).toBe(2);
|
|
555
|
+
});
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
**JavaScript**
|
|
559
|
+
```javascript
|
|
560
|
+
const { test, expect } = require('@playwright/test');
|
|
561
|
+
|
|
562
|
+
test('app reconnects after WebSocket drops', async ({ page }) => {
|
|
563
|
+
let connectionCount = 0;
|
|
564
|
+
|
|
565
|
+
await page.routeWebSocket('**/ws', (ws) => {
|
|
566
|
+
connectionCount++;
|
|
567
|
+
|
|
568
|
+
if (connectionCount === 1) {
|
|
569
|
+
setTimeout(() => ws.close({ code: 1006, reason: 'Abnormal closure' }), 500);
|
|
570
|
+
} else {
|
|
571
|
+
ws.onMessage((message) => {
|
|
572
|
+
ws.send(JSON.stringify({ type: 'pong' }));
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
await page.goto('/app');
|
|
578
|
+
await expect(page.getByText('Reconnecting...')).toBeVisible();
|
|
579
|
+
await expect(page.getByText('Connected')).toBeVisible({ timeout: 10000 });
|
|
580
|
+
expect(connectionCount).toBe(2);
|
|
581
|
+
});
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
## Decision Guide
|
|
585
|
+
|
|
586
|
+
| Scenario | Approach | Why |
|
|
587
|
+
|---|---|---|
|
|
588
|
+
| Verify app sends correct WS message | `page.on('websocket')` + `ws.on('framesent')` | Observe without intercepting |
|
|
589
|
+
| Verify app handles server push | `page.routeWebSocket()` with mock server | Full control over what the "server" sends |
|
|
590
|
+
| Test with real server but inject messages | `routeWebSocket` + `connectToServer()` | Man-in-the-middle: forward real traffic plus inject extras |
|
|
591
|
+
| Test SSE endpoint | `page.route()` with `text/event-stream` content type | SSE is HTTP -- standard route interception works |
|
|
592
|
+
| Test HTTP polling | `page.route()` with changing responses per call | Increment a counter; return different data each call |
|
|
593
|
+
| Verify reconnection logic | `routeWebSocket` that closes the first connection | Simulate server failure, verify the app retries |
|
|
594
|
+
| Test binary WebSocket data | `ws.on('framereceived')`, check `frame.payload` as Buffer | Binary frames arrive as `Buffer` in Node.js |
|
|
595
|
+
|
|
596
|
+
## Anti-Patterns
|
|
597
|
+
|
|
598
|
+
| Don't Do This | Problem | Do This Instead |
|
|
599
|
+
|---|---|---|
|
|
600
|
+
| `page.waitForTimeout(3000)` to wait for WS message | Arbitrary delay; flaky and slow | `await expect(page.getByText('msg')).toBeVisible()` or wait on a Promise |
|
|
601
|
+
| Directly construct `WebSocket` in `page.evaluate` | You lose Playwright's observation and routing capabilities | Let the app create its own WebSocket; intercept via `routeWebSocket` |
|
|
602
|
+
| Ignore WebSocket close codes in mocks | App may behave differently for 1000 (normal) vs 1006 (abnormal) | Use the correct close code: `ws.close({ code: 1000 })` |
|
|
603
|
+
| Test real-time features against live third-party servers | Flaky, slow, and may incur costs | Mock the WebSocket or SSE endpoint |
|
|
604
|
+
| Assert on raw WebSocket frame content in every test | Couples tests to wire protocol; breaks on payload format changes | Assert on the UI -- that is what users see |
|
|
605
|
+
| Forget to handle binary vs text frames | `frame.payload` can be `string` or `Buffer` | Check frame type or use `String(frame.payload)` consistently |
|
|
606
|
+
|
|
607
|
+
## Troubleshooting
|
|
608
|
+
|
|
609
|
+
| Symptom | Cause | Fix |
|
|
610
|
+
|---|---|---|
|
|
611
|
+
| `page.on('websocket')` never fires | WebSocket connects before the listener is attached | Register the listener before `page.goto()` |
|
|
612
|
+
| `routeWebSocket` does not intercept | URL pattern does not match the actual WebSocket URL | Check the URL in DevTools Network tab; update the glob pattern |
|
|
613
|
+
| SSE mock returns all events at once | `route.fulfill` sends the body synchronously | For true streaming, use the real server or chunk the response with pauses via `page.evaluate` |
|
|
614
|
+
| WebSocket messages arrive but UI does not update | App processes messages asynchronously; assertion runs too early | Use `await expect(...).toBeVisible()` which auto-retries |
|
|
615
|
+
| Binary frames show as garbled text | `String(frame.payload)` on binary data produces garbage | Treat `frame.payload` as `Buffer` and decode appropriately |
|
|
616
|
+
| Reconnection test is flaky | App has exponential backoff; timeout too short | Increase assertion timeout: `toBeVisible({ timeout: 15000 })` |
|
|
617
|
+
|
|
618
|
+
## Related
|
|
619
|
+
|
|
620
|
+
- [core/multi-user-and-collaboration.md](multi-user-and-collaboration.md) -- multi-user tests that rely on WebSocket for real-time sync
|
|
621
|
+
- [core/assertions-and-waiting.md](assertions-and-waiting.md) -- auto-retrying assertions for async UI updates
|
|
622
|
+
- [core/when-to-mock.md](when-to-mock.md) -- deciding when to mock WebSocket vs use real server
|
|
623
|
+
- [core/debugging.md](debugging.md) -- tracing WebSocket frames in Playwright traces
|