@assertrx/dino 1.0.10 → 1.0.11
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/package.json +1 -1
- package/prompts/generate-k6-test-from-endpoint.md +68 -0
- package/prompts/generate-k6-test-from-openapi.md +85 -0
- package/prompts/setup-k6.md +130 -0
- package/scripts/api-server.mjs +23 -0
- package/skills/analyzing-test-results/SKILL.md +150 -0
- package/skills/analyzing-test-results/reference/bottleneck-patterns.md +130 -0
- package/skills/analyzing-test-results/reference/metrics.md +134 -0
- package/skills/analyzing-test-results/reference/visualization.md +152 -0
- package/skills/designing-test-scenarios/SKILL.md +247 -0
- package/skills/designing-test-scenarios/reference/executors.md +215 -0
- package/skills/designing-test-scenarios/reference/test-types.md +215 -0
- package/skills/designing-test-scenarios/reference/thresholds.md +208 -0
- package/skills/generating-api-load-tests/SKILL.md +199 -0
- package/skills/generating-api-load-tests/reference/data-parameterization.md +343 -0
- package/skills/generating-api-load-tests/reference/grpc.md +315 -0
- package/skills/generating-api-load-tests/reference/http.md +380 -0
- package/skills/generating-api-load-tests/reference/websocket.md +369 -0
- package/skills/generating-browser-tests/SKILL.md +269 -0
- package/skills/generating-browser-tests/reference/browser-api.md +251 -0
- package/skills/generating-browser-tests/reference/web-vitals.md +148 -0
- package/skills/generating-tests-from-code/SKILL.md +142 -0
- package/skills/generating-tests-from-code/reference/auth-analysis.md +220 -0
- package/skills/generating-tests-from-code/reference/framework-patterns.md +242 -0
- package/skills/generating-tests-from-openapi/SKILL.md +234 -0
- package/skills/generating-tests-from-openapi/reference/openapi-auth-mapping.md +247 -0
- package/skills/generating-tests-from-openapi/reference/openapi-parsing.md +194 -0
- package/skills/operating-k6-in-ci-cd/SKILL.md +98 -0
- package/skills/operating-k6-in-ci-cd/reference/ci-cd-pipelines.md +265 -0
- package/skills/operating-k6-in-ci-cd/reference/extensions.md +144 -0
- package/skills/operating-k6-in-ci-cd/reference/k6-operator.md +214 -0
- package/skills/testing-resilience/SKILL.md +230 -0
- package/skills/testing-resilience/reference/fault-injection.md +153 -0
- package/skills/testing-resilience/reference/resilience-patterns.md +500 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
# k6 WebSocket API Reference
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
k6 provides two WebSocket modules:
|
|
6
|
+
- **`k6/websockets`** (recommended) — Uses global event loop, supports multiple concurrent connections per VU
|
|
7
|
+
- **`k6/ws`** (legacy) — Uses local event loop, one connection per VU
|
|
8
|
+
|
|
9
|
+
This reference covers the recommended `k6/websockets` module.
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```javascript
|
|
14
|
+
import { WebSocket } from 'k6/websockets';
|
|
15
|
+
import { check } from 'k6';
|
|
16
|
+
|
|
17
|
+
export const options = {
|
|
18
|
+
vus: 10,
|
|
19
|
+
duration: '30s',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default function () {
|
|
23
|
+
const url = 'wss://echo.websocket.org';
|
|
24
|
+
const ws = new WebSocket(url);
|
|
25
|
+
|
|
26
|
+
ws.onopen = () => {
|
|
27
|
+
console.log('Connected');
|
|
28
|
+
ws.send('Hello from k6!');
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
ws.onmessage = (event) => {
|
|
32
|
+
console.log('Received:', event.data);
|
|
33
|
+
ws.close();
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
ws.onerror = (event) => {
|
|
37
|
+
console.error('Error:', event.error());
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
ws.onclose = (event) => {
|
|
41
|
+
console.log('Disconnected:', event.code);
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Creating a WebSocket Connection
|
|
47
|
+
|
|
48
|
+
```javascript
|
|
49
|
+
import { WebSocket } from 'k6/websockets';
|
|
50
|
+
|
|
51
|
+
// Basic connection
|
|
52
|
+
const ws = new WebSocket('wss://example.com/ws');
|
|
53
|
+
|
|
54
|
+
// With subprotocols
|
|
55
|
+
const ws = new WebSocket('wss://example.com/ws', ['graphql-ws', 'json']);
|
|
56
|
+
|
|
57
|
+
// With connection parameters
|
|
58
|
+
const ws = new WebSocket('wss://example.com/ws', null, {
|
|
59
|
+
headers: {
|
|
60
|
+
'Authorization': 'Bearer token123',
|
|
61
|
+
'X-Custom-Header': 'value',
|
|
62
|
+
},
|
|
63
|
+
tags: { name: 'MyWebSocket' },
|
|
64
|
+
jar: http.cookieJar(), // Custom cookie jar
|
|
65
|
+
compression: 'deflate', // Enable compression
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Connection Parameters
|
|
70
|
+
|
|
71
|
+
| Parameter | Type | Description |
|
|
72
|
+
|-----------|------|-------------|
|
|
73
|
+
| `headers` | object | Custom HTTP headers for the initial handshake |
|
|
74
|
+
| `tags` | object | Custom metric tags |
|
|
75
|
+
| `jar` | CookieJar | Custom cookie jar for the handshake |
|
|
76
|
+
| `compression` | string | `'deflate'` for per-message compression |
|
|
77
|
+
|
|
78
|
+
## WebSocket Properties
|
|
79
|
+
|
|
80
|
+
| Property | Type | Description |
|
|
81
|
+
|----------|------|-------------|
|
|
82
|
+
| `readyState` | number | `0` CONNECTING, `1` OPEN, `2` CLOSING, `3` CLOSED |
|
|
83
|
+
| `url` | string | Resolved connection URL |
|
|
84
|
+
| `bufferedAmount` | number | Bytes queued but not yet sent |
|
|
85
|
+
| `binaryType` | string | `'blob'` (default) or `'arraybuffer'` |
|
|
86
|
+
|
|
87
|
+
## Event Handlers
|
|
88
|
+
|
|
89
|
+
### onopen — Connection Established
|
|
90
|
+
|
|
91
|
+
```javascript
|
|
92
|
+
ws.onopen = () => {
|
|
93
|
+
console.log('Connection established');
|
|
94
|
+
ws.send('Initial message');
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Alternative: addEventListener
|
|
98
|
+
ws.addEventListener('open', (event) => {
|
|
99
|
+
console.log('Connected');
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### onmessage — Message Received
|
|
104
|
+
|
|
105
|
+
```javascript
|
|
106
|
+
ws.onmessage = (event) => {
|
|
107
|
+
const data = event.data;
|
|
108
|
+
|
|
109
|
+
// Text message
|
|
110
|
+
if (typeof data === 'string') {
|
|
111
|
+
const json = JSON.parse(data);
|
|
112
|
+
console.log('JSON message:', json);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// Multiple handlers via addEventListener
|
|
117
|
+
ws.addEventListener('message', (event) => {
|
|
118
|
+
console.log('Handler 1:', event.data);
|
|
119
|
+
});
|
|
120
|
+
ws.addEventListener('message', (event) => {
|
|
121
|
+
console.log('Handler 2:', event.data);
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### onclose — Connection Closed
|
|
126
|
+
|
|
127
|
+
```javascript
|
|
128
|
+
ws.onclose = (event) => {
|
|
129
|
+
console.log('Closed with code:', event.code);
|
|
130
|
+
// Common close codes:
|
|
131
|
+
// 1000 - Normal closure
|
|
132
|
+
// 1001 - Going away
|
|
133
|
+
// 1006 - Abnormal closure (no close frame)
|
|
134
|
+
// 1011 - Server error
|
|
135
|
+
};
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### onerror — Error Occurred
|
|
139
|
+
|
|
140
|
+
```javascript
|
|
141
|
+
ws.onerror = (event) => {
|
|
142
|
+
console.error('WebSocket error:', event.error());
|
|
143
|
+
};
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### onping / onpong — Ping/Pong Frames
|
|
147
|
+
|
|
148
|
+
```javascript
|
|
149
|
+
ws.addEventListener('ping', () => {
|
|
150
|
+
console.log('Received ping from server');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
ws.addEventListener('pong', () => {
|
|
154
|
+
console.log('Received pong from server');
|
|
155
|
+
});
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Sending Messages
|
|
159
|
+
|
|
160
|
+
```javascript
|
|
161
|
+
// Text message
|
|
162
|
+
ws.send('Hello, server!');
|
|
163
|
+
|
|
164
|
+
// JSON message
|
|
165
|
+
ws.send(JSON.stringify({ type: 'subscribe', channel: 'updates' }));
|
|
166
|
+
|
|
167
|
+
// Binary data
|
|
168
|
+
const buffer = new ArrayBuffer(4);
|
|
169
|
+
const view = new Uint8Array(buffer);
|
|
170
|
+
view[0] = 0x01;
|
|
171
|
+
ws.send(buffer);
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Closing Connection
|
|
175
|
+
|
|
176
|
+
```javascript
|
|
177
|
+
// Normal close
|
|
178
|
+
ws.close();
|
|
179
|
+
|
|
180
|
+
// Close with code
|
|
181
|
+
ws.close(1000); // Normal closure
|
|
182
|
+
|
|
183
|
+
// Common close codes:
|
|
184
|
+
// 1000 - Normal
|
|
185
|
+
// 1001 - Going away
|
|
186
|
+
// 1008 - Policy violation
|
|
187
|
+
// 1011 - Server error
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Ping
|
|
191
|
+
|
|
192
|
+
```javascript
|
|
193
|
+
ws.ping(); // Send ping to server (pong is automatic)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Patterns
|
|
197
|
+
|
|
198
|
+
### Request-Response Pattern
|
|
199
|
+
|
|
200
|
+
```javascript
|
|
201
|
+
export default function () {
|
|
202
|
+
const ws = new WebSocket('wss://api.example.com/ws');
|
|
203
|
+
let responseReceived = false;
|
|
204
|
+
|
|
205
|
+
ws.onopen = () => {
|
|
206
|
+
ws.send(JSON.stringify({ action: 'getData', id: 123 }));
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
ws.onmessage = (event) => {
|
|
210
|
+
const data = JSON.parse(event.data);
|
|
211
|
+
check(data, {
|
|
212
|
+
'has result': (d) => d.result !== undefined,
|
|
213
|
+
'status is success': (d) => d.status === 'success',
|
|
214
|
+
});
|
|
215
|
+
responseReceived = true;
|
|
216
|
+
ws.close();
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
ws.onerror = (event) => {
|
|
220
|
+
console.error('Error:', event.error());
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Subscription Pattern (Pub/Sub)
|
|
226
|
+
|
|
227
|
+
```javascript
|
|
228
|
+
import { setTimeout } from 'k6/timers';
|
|
229
|
+
|
|
230
|
+
export default function () {
|
|
231
|
+
const ws = new WebSocket('wss://api.example.com/ws');
|
|
232
|
+
let messageCount = 0;
|
|
233
|
+
|
|
234
|
+
ws.onopen = () => {
|
|
235
|
+
// Subscribe to channels
|
|
236
|
+
ws.send(JSON.stringify({ type: 'subscribe', channel: 'prices' }));
|
|
237
|
+
ws.send(JSON.stringify({ type: 'subscribe', channel: 'trades' }));
|
|
238
|
+
|
|
239
|
+
// Close after 10 seconds
|
|
240
|
+
setTimeout(() => {
|
|
241
|
+
ws.send(JSON.stringify({ type: 'unsubscribe', channel: 'prices' }));
|
|
242
|
+
ws.send(JSON.stringify({ type: 'unsubscribe', channel: 'trades' }));
|
|
243
|
+
ws.close();
|
|
244
|
+
}, 10000);
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
ws.onmessage = (event) => {
|
|
248
|
+
messageCount++;
|
|
249
|
+
const data = JSON.parse(event.data);
|
|
250
|
+
check(data, {
|
|
251
|
+
'valid message': (d) => d.channel !== undefined && d.data !== undefined,
|
|
252
|
+
});
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Chat/Interactive Pattern
|
|
258
|
+
|
|
259
|
+
```javascript
|
|
260
|
+
import { setTimeout } from 'k6/timers';
|
|
261
|
+
|
|
262
|
+
export default function () {
|
|
263
|
+
const ws = new WebSocket('wss://chat.example.com/ws');
|
|
264
|
+
|
|
265
|
+
ws.onopen = () => {
|
|
266
|
+
ws.send(JSON.stringify({ type: 'join', room: 'loadtest' }));
|
|
267
|
+
|
|
268
|
+
// Simulate periodic messages
|
|
269
|
+
let i = 0;
|
|
270
|
+
const interval = setInterval(() => {
|
|
271
|
+
ws.send(JSON.stringify({
|
|
272
|
+
type: 'message',
|
|
273
|
+
text: `Hello from VU ${__VU}, message ${i}`,
|
|
274
|
+
}));
|
|
275
|
+
i++;
|
|
276
|
+
if (i >= 5) {
|
|
277
|
+
clearInterval(interval);
|
|
278
|
+
ws.send(JSON.stringify({ type: 'leave', room: 'loadtest' }));
|
|
279
|
+
ws.close();
|
|
280
|
+
}
|
|
281
|
+
}, 1000);
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
ws.onmessage = (event) => {
|
|
285
|
+
const msg = JSON.parse(event.data);
|
|
286
|
+
if (msg.type === 'message') {
|
|
287
|
+
console.log(`${msg.user}: ${msg.text}`);
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Multiple Concurrent Connections
|
|
294
|
+
|
|
295
|
+
The `k6/websockets` module supports multiple connections per VU:
|
|
296
|
+
|
|
297
|
+
```javascript
|
|
298
|
+
export default function () {
|
|
299
|
+
const ws1 = new WebSocket('wss://api.example.com/stream1');
|
|
300
|
+
const ws2 = new WebSocket('wss://api.example.com/stream2');
|
|
301
|
+
|
|
302
|
+
ws1.onmessage = (event) => {
|
|
303
|
+
console.log('Stream 1:', event.data);
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
ws2.onmessage = (event) => {
|
|
307
|
+
console.log('Stream 2:', event.data);
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
ws1.onopen = () => ws1.send('subscribe');
|
|
311
|
+
ws2.onopen = () => ws2.send('subscribe');
|
|
312
|
+
|
|
313
|
+
setTimeout(() => {
|
|
314
|
+
ws1.close();
|
|
315
|
+
ws2.close();
|
|
316
|
+
}, 10000);
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
## WebSocket Metrics
|
|
321
|
+
|
|
322
|
+
k6 automatically emits these metrics for WebSocket connections:
|
|
323
|
+
|
|
324
|
+
| Metric | Type | Description |
|
|
325
|
+
|--------|------|-------------|
|
|
326
|
+
| `ws_connecting` | Trend | Time to establish WebSocket connection |
|
|
327
|
+
| `ws_session_duration` | Trend | Total session duration |
|
|
328
|
+
| `ws_msgs_sent` | Counter | Number of messages sent |
|
|
329
|
+
| `ws_msgs_received` | Counter | Number of messages received |
|
|
330
|
+
| `ws_ping` | Trend | Ping round-trip time |
|
|
331
|
+
| `ws_sessions` | Counter | Number of sessions started |
|
|
332
|
+
|
|
333
|
+
## Legacy Module (k6/ws)
|
|
334
|
+
|
|
335
|
+
The `k6/ws` module uses a callback-based pattern with a local event loop:
|
|
336
|
+
|
|
337
|
+
```javascript
|
|
338
|
+
import ws from 'k6/ws';
|
|
339
|
+
|
|
340
|
+
export default function () {
|
|
341
|
+
const res = ws.connect('wss://echo.websocket.org', {}, function (socket) {
|
|
342
|
+
socket.on('open', () => {
|
|
343
|
+
socket.send('Hello');
|
|
344
|
+
socket.setInterval(() => {
|
|
345
|
+
socket.send('Periodic message');
|
|
346
|
+
}, 1000);
|
|
347
|
+
socket.setTimeout(() => {
|
|
348
|
+
socket.close();
|
|
349
|
+
}, 5000);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
socket.on('message', (data) => {
|
|
353
|
+
console.log('Received:', data);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
socket.on('close', () => {
|
|
357
|
+
console.log('Disconnected');
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
check(res, { 'status is 101': (r) => r && r.status === 101 });
|
|
362
|
+
}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
Key differences from `k6/websockets`:
|
|
366
|
+
- Uses callback function wrapping all logic
|
|
367
|
+
- `socket.setInterval()` and `socket.setTimeout()` instead of `k6/timers`
|
|
368
|
+
- Only one connection per VU at a time
|
|
369
|
+
- Returns response object with status 101
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: generating-browser-tests
|
|
3
|
+
description: Use when creating k6 browser-based performance tests, testing frontend performance, measuring Web Vitals (LCP, FCP, CLS, INP, TTFB), or automating browser interactions for load testing. Use when the user mentions browser testing, page load testing, E2E performance, or Chromium-based testing with k6.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Generating k6 Browser Tests
|
|
7
|
+
|
|
8
|
+
Generate browser-based E2E performance tests using the k6/browser module. Tests real browser interactions with Chromium, measures Web Vitals, and supports hybrid protocol+browser testing.
|
|
9
|
+
|
|
10
|
+
## Quick Start
|
|
11
|
+
|
|
12
|
+
```javascript
|
|
13
|
+
import { browser } from 'k6/browser';
|
|
14
|
+
import { check } from 'k6';
|
|
15
|
+
|
|
16
|
+
export const options = {
|
|
17
|
+
scenarios: {
|
|
18
|
+
ui: {
|
|
19
|
+
executor: 'shared-iterations',
|
|
20
|
+
iterations: 5,
|
|
21
|
+
vus: 1,
|
|
22
|
+
options: {
|
|
23
|
+
browser: {
|
|
24
|
+
type: 'chromium',
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
thresholds: {
|
|
30
|
+
browser_web_vital_lcp: ['p(90)<2500'],
|
|
31
|
+
browser_web_vital_fcp: ['p(90)<1800'],
|
|
32
|
+
browser_web_vital_cls: ['p(95)<0.1'],
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export default async function () {
|
|
37
|
+
const page = await browser.newPage();
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
await page.goto('https://example.com');
|
|
41
|
+
await page.waitForLoadState('networkidle');
|
|
42
|
+
|
|
43
|
+
const title = await page.title();
|
|
44
|
+
check(null, { 'page title loaded': () => title !== '' });
|
|
45
|
+
|
|
46
|
+
await page.screenshot({ path: 'screenshot.png' });
|
|
47
|
+
} finally {
|
|
48
|
+
await page.close(); // Always close pages
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Key Concepts
|
|
54
|
+
|
|
55
|
+
- **All browser functions are `async`** — use `await` and `async function`
|
|
56
|
+
- **Always close pages** — use `try/finally` to ensure `page.close()` is called
|
|
57
|
+
- **Browser type** — only `'chromium'` is supported
|
|
58
|
+
- **Browser scenario** — requires `options.browser.type` in scenario config
|
|
59
|
+
- **Avoid `networkidle`** — `waitForLoadState('networkidle')` may never fire on chatty pages; prefer `'load'` or `locator.waitFor()` when possible
|
|
60
|
+
- **Resource intensive** — browser VUs consume more resources; start with few VUs
|
|
61
|
+
|
|
62
|
+
## Element Selection
|
|
63
|
+
|
|
64
|
+
### Locator API (Recommended)
|
|
65
|
+
|
|
66
|
+
```javascript
|
|
67
|
+
// CSS selector
|
|
68
|
+
const btn = page.locator('button.submit');
|
|
69
|
+
|
|
70
|
+
// Semantic selectors (preferred for resilience)
|
|
71
|
+
const submitBtn = page.getByRole('button', { name: 'Submit' });
|
|
72
|
+
const email = page.getByLabel('Email');
|
|
73
|
+
const search = page.getByPlaceholder('Search...');
|
|
74
|
+
const heading = page.getByText('Welcome');
|
|
75
|
+
const card = page.getByTestId('user-card');
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Common Interactions
|
|
79
|
+
|
|
80
|
+
```javascript
|
|
81
|
+
// Click
|
|
82
|
+
await page.locator('button').click();
|
|
83
|
+
|
|
84
|
+
// Fill text input
|
|
85
|
+
await page.getByLabel('Username').fill('testuser');
|
|
86
|
+
|
|
87
|
+
// Select dropdown option
|
|
88
|
+
await page.locator('select#country').selectOption('US');
|
|
89
|
+
|
|
90
|
+
// Checkbox
|
|
91
|
+
await page.getByRole('checkbox', { name: 'Terms' }).check();
|
|
92
|
+
|
|
93
|
+
// Type character by character (with key events)
|
|
94
|
+
await page.locator('#search').type('search query', { delay: 50 });
|
|
95
|
+
|
|
96
|
+
// Press key
|
|
97
|
+
await page.locator('#search').press('Enter');
|
|
98
|
+
|
|
99
|
+
// Hover
|
|
100
|
+
await page.locator('.menu-item').hover();
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Query Element State
|
|
104
|
+
|
|
105
|
+
```javascript
|
|
106
|
+
const isVisible = await page.locator('.modal').isVisible();
|
|
107
|
+
const isEnabled = await page.locator('button').isEnabled();
|
|
108
|
+
const text = await page.locator('.message').textContent();
|
|
109
|
+
const value = await page.locator('input').inputValue();
|
|
110
|
+
const count = await page.locator('li.item').count();
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Web Vitals
|
|
114
|
+
|
|
115
|
+
k6/browser automatically collects Core Web Vitals:
|
|
116
|
+
|
|
117
|
+
| Metric | Name | Good Threshold |
|
|
118
|
+
|--------|------|----------------|
|
|
119
|
+
| `browser_web_vital_lcp` | Largest Contentful Paint | < 2500ms |
|
|
120
|
+
| `browser_web_vital_fcp` | First Contentful Paint | < 1800ms |
|
|
121
|
+
| `browser_web_vital_cls` | Cumulative Layout Shift | < 0.1 |
|
|
122
|
+
| `browser_web_vital_inp` | Interaction to Next Paint | < 200ms |
|
|
123
|
+
| `browser_web_vital_ttfb` | Time to First Byte | < 600ms |
|
|
124
|
+
|
|
125
|
+
```javascript
|
|
126
|
+
export const options = {
|
|
127
|
+
thresholds: {
|
|
128
|
+
'browser_web_vital_lcp': ['p(90)<2500'],
|
|
129
|
+
'browser_web_vital_fcp': ['p(90)<1800'],
|
|
130
|
+
'browser_web_vital_cls': ['p(95)<0.1'],
|
|
131
|
+
'browser_web_vital_inp': ['p(90)<200'],
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Common Test Patterns
|
|
137
|
+
|
|
138
|
+
### Login Flow
|
|
139
|
+
|
|
140
|
+
```javascript
|
|
141
|
+
export default async function () {
|
|
142
|
+
const page = await browser.newPage();
|
|
143
|
+
try {
|
|
144
|
+
await page.goto('https://app.example.com/login');
|
|
145
|
+
|
|
146
|
+
await page.getByLabel('Email').fill('test@example.com');
|
|
147
|
+
await page.getByLabel('Password').fill('password123');
|
|
148
|
+
await page.getByRole('button', { name: 'Sign in' }).click();
|
|
149
|
+
|
|
150
|
+
await page.waitForNavigation();
|
|
151
|
+
|
|
152
|
+
check(null, {
|
|
153
|
+
'redirected to dashboard': () => page.url().includes('/dashboard'),
|
|
154
|
+
});
|
|
155
|
+
} finally {
|
|
156
|
+
await page.close();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Form Submission
|
|
162
|
+
|
|
163
|
+
```javascript
|
|
164
|
+
export default async function () {
|
|
165
|
+
const page = await browser.newPage();
|
|
166
|
+
try {
|
|
167
|
+
await page.goto('https://app.example.com/contact');
|
|
168
|
+
|
|
169
|
+
await page.getByLabel('Name').fill('Load Test User');
|
|
170
|
+
await page.getByLabel('Email').fill('test@example.com');
|
|
171
|
+
await page.getByLabel('Message').fill('Performance test submission');
|
|
172
|
+
await page.locator('select#department').selectOption('support');
|
|
173
|
+
await page.getByRole('checkbox', { name: 'Subscribe' }).check();
|
|
174
|
+
|
|
175
|
+
await page.getByRole('button', { name: 'Submit' }).click();
|
|
176
|
+
await page.waitForLoadState('networkidle');
|
|
177
|
+
|
|
178
|
+
const confirmation = await page.locator('.success-message').textContent();
|
|
179
|
+
check(null, {
|
|
180
|
+
'form submitted': () => confirmation.includes('Thank you'),
|
|
181
|
+
});
|
|
182
|
+
} finally {
|
|
183
|
+
await page.close();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Page Navigation with Screenshots
|
|
189
|
+
|
|
190
|
+
```javascript
|
|
191
|
+
export default async function () {
|
|
192
|
+
const page = await browser.newPage();
|
|
193
|
+
try {
|
|
194
|
+
await page.goto('https://app.example.com');
|
|
195
|
+
await page.screenshot({ path: 'home.png', fullPage: true });
|
|
196
|
+
|
|
197
|
+
await page.locator('a[href="/products"]').click();
|
|
198
|
+
await page.waitForNavigation();
|
|
199
|
+
await page.screenshot({ path: 'products.png' });
|
|
200
|
+
|
|
201
|
+
await page.locator('.product-card').first().click();
|
|
202
|
+
await page.waitForLoadState('networkidle');
|
|
203
|
+
await page.screenshot({ path: 'product-detail.png' });
|
|
204
|
+
} finally {
|
|
205
|
+
await page.close();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Hybrid Testing (Browser + Protocol)
|
|
211
|
+
|
|
212
|
+
Combine browser tests with protocol-level load tests:
|
|
213
|
+
|
|
214
|
+
```javascript
|
|
215
|
+
import { browser } from 'k6/browser';
|
|
216
|
+
import http from 'k6/http';
|
|
217
|
+
import { check } from 'k6';
|
|
218
|
+
|
|
219
|
+
export const options = {
|
|
220
|
+
scenarios: {
|
|
221
|
+
// High-volume API load
|
|
222
|
+
api_load: {
|
|
223
|
+
exec: 'apiTest',
|
|
224
|
+
executor: 'constant-arrival-rate',
|
|
225
|
+
rate: 100,
|
|
226
|
+
timeUnit: '1s',
|
|
227
|
+
duration: '5m',
|
|
228
|
+
preAllocatedVUs: 50,
|
|
229
|
+
},
|
|
230
|
+
// Low-volume browser tests (resource intensive)
|
|
231
|
+
browser_test: {
|
|
232
|
+
exec: 'browserTest',
|
|
233
|
+
executor: 'constant-vus',
|
|
234
|
+
vus: 2,
|
|
235
|
+
duration: '5m',
|
|
236
|
+
options: { browser: { type: 'chromium' } },
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
export function apiTest() {
|
|
242
|
+
const res = http.get('https://api.example.com/products');
|
|
243
|
+
check(res, { 'API 200': (r) => r.status === 200 });
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export async function browserTest() {
|
|
247
|
+
const page = await browser.newPage();
|
|
248
|
+
try {
|
|
249
|
+
await page.goto('https://app.example.com');
|
|
250
|
+
// Browser interactions...
|
|
251
|
+
} finally {
|
|
252
|
+
await page.close();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## Advanced Features
|
|
258
|
+
|
|
259
|
+
For detailed API reference including BrowserContext, Keyboard, Mouse, Touchscreen, and advanced patterns:
|
|
260
|
+
|
|
261
|
+
See [reference/browser-api.md](reference/browser-api.md) — Complete Page, Locator, BrowserContext API with all methods and parameters
|
|
262
|
+
|
|
263
|
+
See [reference/web-vitals.md](reference/web-vitals.md) — Web Vitals metrics explanation, threshold guidance, and custom performance measurement
|
|
264
|
+
|
|
265
|
+
## Related Skills
|
|
266
|
+
|
|
267
|
+
- For protocol-level load tests (HTTP, gRPC, WebSocket): `/k6:generating-api-load-tests`
|
|
268
|
+
- For scenario and executor configuration: `/k6:designing-test-scenarios`
|
|
269
|
+
- For interpreting test results: `/k6:analyzing-test-results`
|