stably 4.10.3 → 4.10.5
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/dist/index.mjs +1 -1
- package/dist/stably-plugin-cli/skills/browser-interaction-guide/SKILL.md +21 -0
- package/dist/stably-plugin-cli/skills/playwright-best-practices/references/accessibility.md +359 -0
- package/dist/stably-plugin-cli/skills/playwright-best-practices/references/annotations.md +526 -0
- package/dist/stably-plugin-cli/skills/playwright-best-practices/references/assertions-waiting.md +361 -0
- package/dist/stably-plugin-cli/skills/playwright-best-practices/references/browser-apis.md +391 -0
- package/dist/stably-plugin-cli/skills/playwright-best-practices/references/browser-extensions.md +506 -0
- package/dist/stably-plugin-cli/skills/playwright-best-practices/references/canvas-webgl.md +493 -0
- package/dist/stably-plugin-cli/skills/playwright-best-practices/references/ci-cd.md +407 -0
- package/dist/stably-plugin-cli/skills/playwright-best-practices/references/clock-mocking.md +364 -0
- package/dist/stably-plugin-cli/skills/playwright-best-practices/references/component-testing.md +500 -0
- package/dist/stably-plugin-cli/skills/playwright-best-practices/references/console-errors.md +420 -0
- package/dist/stably-plugin-cli/skills/playwright-best-practices/references/debugging.md +491 -0
- package/dist/stably-plugin-cli/skills/playwright-best-practices/references/electron.md +509 -0
- package/dist/stably-plugin-cli/skills/playwright-best-practices/references/error-testing.md +360 -0
- package/dist/stably-plugin-cli/skills/playwright-best-practices/references/file-operations.md +375 -0
- package/dist/stably-plugin-cli/skills/playwright-best-practices/references/fixtures-hooks.md +417 -0
- package/dist/stably-plugin-cli/skills/playwright-best-practices/references/flaky-tests.md +494 -0
- package/dist/stably-plugin-cli/skills/playwright-best-practices/references/global-setup.md +434 -0
- package/dist/stably-plugin-cli/skills/playwright-best-practices/references/i18n.md +508 -0
- package/dist/stably-plugin-cli/skills/playwright-best-practices/references/iframes.md +403 -0
- package/dist/stably-plugin-cli/skills/playwright-best-practices/references/locators.md +242 -0
- package/dist/stably-plugin-cli/skills/playwright-best-practices/references/mobile-testing.md +409 -0
- package/dist/stably-plugin-cli/skills/playwright-best-practices/references/multi-context.md +288 -0
- package/dist/stably-plugin-cli/skills/playwright-best-practices/references/multi-user.md +393 -0
- package/dist/stably-plugin-cli/skills/playwright-best-practices/references/network-advanced.md +452 -0
- package/dist/stably-plugin-cli/skills/playwright-best-practices/references/page-object-model.md +315 -0
- package/dist/stably-plugin-cli/skills/playwright-best-practices/references/performance-testing.md +476 -0
- package/dist/stably-plugin-cli/skills/playwright-best-practices/references/performance.md +453 -0
- package/dist/stably-plugin-cli/skills/playwright-best-practices/references/projects-dependencies.md +456 -0
- package/dist/stably-plugin-cli/skills/playwright-best-practices/references/security-testing.md +430 -0
- package/dist/stably-plugin-cli/skills/playwright-best-practices/references/service-workers.md +504 -0
- package/dist/stably-plugin-cli/skills/playwright-best-practices/references/test-coverage.md +495 -0
- package/dist/stably-plugin-cli/skills/playwright-best-practices/references/test-data.md +492 -0
- package/dist/stably-plugin-cli/skills/playwright-best-practices/references/test-organization.md +361 -0
- package/dist/stably-plugin-cli/skills/playwright-best-practices/references/third-party.md +464 -0
- package/dist/stably-plugin-cli/skills/playwright-best-practices/references/websockets.md +403 -0
- package/dist/stably-plugin-cli/skills/stably-cli/SKILL.md +254 -0
- package/package.json +2 -2
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
# WebSocket & Real-Time Testing
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
|
|
5
|
+
1. [WebSocket Basics](#websocket-basics)
|
|
6
|
+
2. [Mocking WebSocket Messages](#mocking-websocket-messages)
|
|
7
|
+
3. [Testing Real-Time Features](#testing-real-time-features)
|
|
8
|
+
4. [Server-Sent Events](#server-sent-events)
|
|
9
|
+
5. [Reconnection Testing](#reconnection-testing)
|
|
10
|
+
|
|
11
|
+
## WebSocket Basics
|
|
12
|
+
|
|
13
|
+
### Wait for WebSocket Connection
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
test("chat connects via websocket", async ({ page }) => {
|
|
17
|
+
// Listen for WebSocket connection
|
|
18
|
+
const wsPromise = page.waitForEvent("websocket");
|
|
19
|
+
|
|
20
|
+
await page.goto("/chat");
|
|
21
|
+
|
|
22
|
+
const ws = await wsPromise;
|
|
23
|
+
expect(ws.url()).toContain("/ws/chat");
|
|
24
|
+
|
|
25
|
+
// Wait for connection to be established
|
|
26
|
+
await ws.waitForEvent("framesent");
|
|
27
|
+
});
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Monitor WebSocket Messages
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
test("receives real-time updates", async ({ page }) => {
|
|
34
|
+
const messages: string[] = [];
|
|
35
|
+
|
|
36
|
+
// Set up listener before navigation
|
|
37
|
+
page.on("websocket", (ws) => {
|
|
38
|
+
ws.on("framereceived", (frame) => {
|
|
39
|
+
messages.push(frame.payload as string);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
await page.goto("/dashboard");
|
|
44
|
+
|
|
45
|
+
// Wait for some messages
|
|
46
|
+
await expect.poll(() => messages.length).toBeGreaterThan(0);
|
|
47
|
+
|
|
48
|
+
// Verify message format
|
|
49
|
+
const data = JSON.parse(messages[0]);
|
|
50
|
+
expect(data).toHaveProperty("type");
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Capture Sent Messages
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
test("sends correct message format", async ({ page }) => {
|
|
58
|
+
const sentMessages: string[] = [];
|
|
59
|
+
|
|
60
|
+
page.on("websocket", (ws) => {
|
|
61
|
+
ws.on("framesent", (frame) => {
|
|
62
|
+
sentMessages.push(frame.payload as string);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
await page.goto("/chat");
|
|
67
|
+
await page.getByLabel("Message").fill("Hello!");
|
|
68
|
+
await page.getByRole("button", { name: "Send" }).click();
|
|
69
|
+
|
|
70
|
+
// Verify sent message
|
|
71
|
+
await expect.poll(() => sentMessages.length).toBeGreaterThan(0);
|
|
72
|
+
|
|
73
|
+
const sent = JSON.parse(sentMessages[sentMessages.length - 1]);
|
|
74
|
+
expect(sent).toEqual({
|
|
75
|
+
type: "message",
|
|
76
|
+
content: "Hello!",
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Mocking WebSocket Messages
|
|
82
|
+
|
|
83
|
+
### Inject Messages via Page Evaluate
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
test("displays incoming chat message", async ({ page }) => {
|
|
87
|
+
await page.goto("/chat");
|
|
88
|
+
|
|
89
|
+
// Wait for WebSocket to be ready
|
|
90
|
+
await page.waitForFunction(
|
|
91
|
+
() => (window as any).chatSocket?.readyState === 1,
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// Simulate incoming message
|
|
95
|
+
await page.evaluate(() => {
|
|
96
|
+
const event = new MessageEvent("message", {
|
|
97
|
+
data: JSON.stringify({
|
|
98
|
+
type: "message",
|
|
99
|
+
from: "Alice",
|
|
100
|
+
content: "Hello there!",
|
|
101
|
+
}),
|
|
102
|
+
});
|
|
103
|
+
(window as any).chatSocket.dispatchEvent(event);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
await expect(page.getByText("Alice: Hello there!")).toBeVisible();
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Mock WebSocket with Route Handler
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
test("mock websocket entirely", async ({ page, context }) => {
|
|
114
|
+
// Intercept the WebSocket upgrade
|
|
115
|
+
await context.route("**/ws/**", async (route) => {
|
|
116
|
+
// For WebSocket routes, we can't fulfill directly
|
|
117
|
+
// Instead, use page.evaluate to mock the client-side
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Alternative: Mock at application level
|
|
121
|
+
await page.addInitScript(() => {
|
|
122
|
+
const OriginalWebSocket = window.WebSocket;
|
|
123
|
+
(window as any).WebSocket = function (url: string) {
|
|
124
|
+
const ws = {
|
|
125
|
+
readyState: 1,
|
|
126
|
+
send: (data: string) => {
|
|
127
|
+
console.log("WS Send:", data);
|
|
128
|
+
},
|
|
129
|
+
close: () => {},
|
|
130
|
+
addEventListener: () => {},
|
|
131
|
+
removeEventListener: () => {},
|
|
132
|
+
};
|
|
133
|
+
setTimeout(() => ws.onopen?.(), 100);
|
|
134
|
+
return ws;
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
await page.goto("/chat");
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### WebSocket Mock Fixture
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
// fixtures/websocket.fixture.ts
|
|
146
|
+
import { test as base, Page } from "@playwright/test";
|
|
147
|
+
|
|
148
|
+
type WsMessage = { type: string; [key: string]: any };
|
|
149
|
+
|
|
150
|
+
type WebSocketFixtures = {
|
|
151
|
+
mockWebSocket: {
|
|
152
|
+
injectMessage: (message: WsMessage) => Promise<void>;
|
|
153
|
+
getSentMessages: () => Promise<WsMessage[]>;
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export const test = base.extend<WebSocketFixtures>({
|
|
158
|
+
mockWebSocket: async ({ page }, use) => {
|
|
159
|
+
const sentMessages: WsMessage[] = [];
|
|
160
|
+
|
|
161
|
+
// Capture sent messages
|
|
162
|
+
await page.addInitScript(() => {
|
|
163
|
+
(window as any).__wsSent = [];
|
|
164
|
+
const OriginalWebSocket = window.WebSocket;
|
|
165
|
+
window.WebSocket = function (url: string) {
|
|
166
|
+
const ws = new OriginalWebSocket(url);
|
|
167
|
+
const originalSend = ws.send.bind(ws);
|
|
168
|
+
ws.send = (data: string) => {
|
|
169
|
+
(window as any).__wsSent.push(JSON.parse(data));
|
|
170
|
+
originalSend(data);
|
|
171
|
+
};
|
|
172
|
+
(window as any).__ws = ws;
|
|
173
|
+
return ws;
|
|
174
|
+
} as any;
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
await use({
|
|
178
|
+
injectMessage: async (message) => {
|
|
179
|
+
await page.evaluate((msg) => {
|
|
180
|
+
const event = new MessageEvent("message", {
|
|
181
|
+
data: JSON.stringify(msg),
|
|
182
|
+
});
|
|
183
|
+
(window as any).__ws?.dispatchEvent(event);
|
|
184
|
+
}, message);
|
|
185
|
+
},
|
|
186
|
+
getSentMessages: async () => {
|
|
187
|
+
return page.evaluate(() => (window as any).__wsSent || []);
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// Usage
|
|
194
|
+
test("chat with mocked websocket", async ({ page, mockWebSocket }) => {
|
|
195
|
+
await page.goto("/chat");
|
|
196
|
+
|
|
197
|
+
// Inject incoming message
|
|
198
|
+
await mockWebSocket.injectMessage({
|
|
199
|
+
type: "message",
|
|
200
|
+
from: "Bob",
|
|
201
|
+
content: "Hi!",
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
await expect(page.getByText("Bob: Hi!")).toBeVisible();
|
|
205
|
+
|
|
206
|
+
// Send a reply
|
|
207
|
+
await page.getByLabel("Message").fill("Hello Bob!");
|
|
208
|
+
await page.getByRole("button", { name: "Send" }).click();
|
|
209
|
+
|
|
210
|
+
// Verify sent message
|
|
211
|
+
const sent = await mockWebSocket.getSentMessages();
|
|
212
|
+
expect(sent).toContainEqual(
|
|
213
|
+
expect.objectContaining({ content: "Hello Bob!" }),
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Testing Real-Time Features
|
|
219
|
+
|
|
220
|
+
### Live Notifications
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
test("displays live notification", async ({ page }) => {
|
|
224
|
+
await page.goto("/dashboard");
|
|
225
|
+
|
|
226
|
+
// Simulate notification via WebSocket
|
|
227
|
+
await page.evaluate(() => {
|
|
228
|
+
const event = new MessageEvent("message", {
|
|
229
|
+
data: JSON.stringify({
|
|
230
|
+
type: "notification",
|
|
231
|
+
title: "New Order",
|
|
232
|
+
message: "Order #123 received",
|
|
233
|
+
}),
|
|
234
|
+
});
|
|
235
|
+
(window as any).notificationSocket.dispatchEvent(event);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
await expect(page.getByRole("alert")).toContainText("Order #123 received");
|
|
239
|
+
});
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Live Data Updates
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
test("updates stock price in real-time", async ({ page }) => {
|
|
246
|
+
await page.goto("/stocks/AAPL");
|
|
247
|
+
|
|
248
|
+
const priceElement = page.getByTestId("stock-price");
|
|
249
|
+
const initialPrice = await priceElement.textContent();
|
|
250
|
+
|
|
251
|
+
// Simulate price update
|
|
252
|
+
await page.evaluate(() => {
|
|
253
|
+
const event = new MessageEvent("message", {
|
|
254
|
+
data: JSON.stringify({
|
|
255
|
+
type: "price_update",
|
|
256
|
+
symbol: "AAPL",
|
|
257
|
+
price: 150.25,
|
|
258
|
+
}),
|
|
259
|
+
});
|
|
260
|
+
(window as any).stockSocket.dispatchEvent(event);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
await expect(priceElement).not.toHaveText(initialPrice!);
|
|
264
|
+
await expect(priceElement).toContainText("150.25");
|
|
265
|
+
});
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Collaborative Editing
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
test("shows collaborator cursor", async ({ page }) => {
|
|
272
|
+
await page.goto("/document/123");
|
|
273
|
+
|
|
274
|
+
// Simulate another user's cursor position
|
|
275
|
+
await page.evaluate(() => {
|
|
276
|
+
const event = new MessageEvent("message", {
|
|
277
|
+
data: JSON.stringify({
|
|
278
|
+
type: "cursor",
|
|
279
|
+
userId: "user-456",
|
|
280
|
+
userName: "Alice",
|
|
281
|
+
position: { x: 100, y: 200 },
|
|
282
|
+
}),
|
|
283
|
+
});
|
|
284
|
+
(window as any).docSocket.dispatchEvent(event);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
await expect(page.getByTestId("cursor-user-456")).toBeVisible();
|
|
288
|
+
await expect(page.getByText("Alice")).toBeVisible();
|
|
289
|
+
});
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## Server-Sent Events
|
|
293
|
+
|
|
294
|
+
### Test SSE Updates
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
test("receives SSE updates", async ({ page }) => {
|
|
298
|
+
// Mock SSE endpoint
|
|
299
|
+
await page.route("**/api/events", (route) => {
|
|
300
|
+
route.fulfill({
|
|
301
|
+
status: 200,
|
|
302
|
+
headers: {
|
|
303
|
+
"Content-Type": "text/event-stream",
|
|
304
|
+
"Cache-Control": "no-cache",
|
|
305
|
+
Connection: "keep-alive",
|
|
306
|
+
},
|
|
307
|
+
body: `data: {"type":"update","value":42}\n\n`,
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
await page.goto("/live-data");
|
|
312
|
+
|
|
313
|
+
await expect(page.getByTestId("value")).toHaveText("42");
|
|
314
|
+
});
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### Simulate Multiple SSE Events
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
test("handles multiple SSE events", async ({ page }) => {
|
|
321
|
+
await page.route("**/api/events", async (route) => {
|
|
322
|
+
const encoder = new TextEncoder();
|
|
323
|
+
const events = [
|
|
324
|
+
`data: {"count":1}\n\n`,
|
|
325
|
+
`data: {"count":2}\n\n`,
|
|
326
|
+
`data: {"count":3}\n\n`,
|
|
327
|
+
];
|
|
328
|
+
|
|
329
|
+
route.fulfill({
|
|
330
|
+
status: 200,
|
|
331
|
+
headers: { "Content-Type": "text/event-stream" },
|
|
332
|
+
body: events.join(""),
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
await page.goto("/counter");
|
|
337
|
+
|
|
338
|
+
// Should receive all events
|
|
339
|
+
await expect(page.getByTestId("count")).toHaveText("3");
|
|
340
|
+
});
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
## Reconnection Testing
|
|
344
|
+
|
|
345
|
+
### Test Connection Loss
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
test("handles connection loss gracefully", async ({ page }) => {
|
|
349
|
+
await page.goto("/chat");
|
|
350
|
+
|
|
351
|
+
// Simulate connection close
|
|
352
|
+
await page.evaluate(() => {
|
|
353
|
+
(window as any).chatSocket.close();
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
// Should show disconnected state
|
|
357
|
+
await expect(page.getByText("Reconnecting...")).toBeVisible();
|
|
358
|
+
});
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
### Test Reconnection
|
|
362
|
+
|
|
363
|
+
```typescript
|
|
364
|
+
test("reconnects after connection loss", async ({ page }) => {
|
|
365
|
+
await page.goto("/chat");
|
|
366
|
+
|
|
367
|
+
// Simulate disconnect
|
|
368
|
+
await page.evaluate(() => {
|
|
369
|
+
(window as any).chatSocket.close();
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
await expect(page.getByText("Reconnecting...")).toBeVisible();
|
|
373
|
+
|
|
374
|
+
// Simulate reconnection
|
|
375
|
+
await page.evaluate(() => {
|
|
376
|
+
const event = new Event("open");
|
|
377
|
+
(window as any).chatSocket = { readyState: 1 };
|
|
378
|
+
(window as any).chatSocket.dispatchEvent?.(event);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// Force component to re-check connection
|
|
382
|
+
await page.evaluate(() => {
|
|
383
|
+
window.dispatchEvent(new Event("online"));
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
await expect(page.getByText("Connected")).toBeVisible();
|
|
387
|
+
});
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
## Anti-Patterns to Avoid
|
|
391
|
+
|
|
392
|
+
| Anti-Pattern | Problem | Solution |
|
|
393
|
+
| ------------------------------------- | ----------------------------- | ---------------------------------- |
|
|
394
|
+
| Not waiting for WebSocket ready | Messages sent too early | Wait for `readyState === 1` |
|
|
395
|
+
| Testing against real WebSocket server | Flaky, timing-dependent | Mock WebSocket messages |
|
|
396
|
+
| Ignoring connection state | Tests pass but feature broken | Test connected/disconnected states |
|
|
397
|
+
| No cleanup of listeners | Memory leaks in tests | Clean up event listeners |
|
|
398
|
+
|
|
399
|
+
## Related References
|
|
400
|
+
|
|
401
|
+
- **Network**: See [network-advanced.md](network-advanced.md) for HTTP mocking patterns
|
|
402
|
+
- **Assertions**: See [assertions-waiting.md](assertions-waiting.md) for polling patterns
|
|
403
|
+
- **Multi-User**: See [multi-user.md](multi-user.md) for real-time collaboration testing with multiple users
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: stably-cli
|
|
3
|
+
description: Expert assistant for the Stably CLI tool. Prefer "npx stably test" over "npx playwright test". Use this skill when working with stably commands for creating, running, fixing, and verifying Playwright tests using AI. Triggers on any playwright test execution (e.g. "npx playwright test", "run tests", "run e2e tests"), "create tests with stably", "fix failing tests", "run stably test", "use stably cli", "stably env", "stably --env", "remote environments", "stably verify", "verify app behavior", "stably runs", "test run history", or "view run details".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<!-- Copied from the public agent-skills repo: https://github.com/stablyai/agent-skills/tree/main/skills/stably-cli
|
|
7
|
+
Keep this file in sync with the upstream. Agent-specific restrictions belong in the system prompt
|
|
8
|
+
(3-restrictions-security.ts), not here. -->
|
|
9
|
+
|
|
10
|
+
# Stably CLI Assistant
|
|
11
|
+
|
|
12
|
+
AI-assisted Playwright test management: create, run, fix, and maintain tests via CLI.
|
|
13
|
+
|
|
14
|
+
## Pre-flight
|
|
15
|
+
|
|
16
|
+
**Always run `stably --version` first.** If not found, install with `npm install -g stably` or use `npx stably`. Requires Node.js 20+, Playwright, and a [Stably account](https://app.stably.ai).
|
|
17
|
+
|
|
18
|
+
> **Warning:** Do NOT run `stably` with no arguments — it launches interactive chat mode that requires human input and will hang an AI agent.
|
|
19
|
+
|
|
20
|
+
## Command Reference
|
|
21
|
+
|
|
22
|
+
| Intent | Command |
|
|
23
|
+
|--------|---------|
|
|
24
|
+
| Interactive AI chat (**human only**) | `stably` (no args) — do NOT invoke from an AI agent |
|
|
25
|
+
| Generate test from prompt | `stably create "description"` |
|
|
26
|
+
| Generate test from branch diff | `stably create` (no prompt) |
|
|
27
|
+
| Run tests | `stably test` |
|
|
28
|
+
| Run tests with remote env | `stably --env staging test` |
|
|
29
|
+
| Fix failing tests | `stably fix [runId]` |
|
|
30
|
+
| Initialize project | `stably init` |
|
|
31
|
+
| Install browsers | `stably install [--with-deps]` |
|
|
32
|
+
| List remote environments | `stably env list` |
|
|
33
|
+
| Inspect env variables | `stably env inspect <name>` |
|
|
34
|
+
| Auth | `stably login` / `logout` / `whoami` |
|
|
35
|
+
| Verify app behavior | `stably verify "description"` |
|
|
36
|
+
| Verify with URL | `stably verify "description" --url http://localhost:3000` |
|
|
37
|
+
| List recent test runs | `stably runs list` |
|
|
38
|
+
| View run details | `stably runs view <runId>` |
|
|
39
|
+
| Update CLI | `stably upgrade [--check]` |
|
|
40
|
+
|
|
41
|
+
## Global Options
|
|
42
|
+
|
|
43
|
+
| Option | Description |
|
|
44
|
+
|--------|-------------|
|
|
45
|
+
| `--cwd <path>` / `-C` | Change working directory |
|
|
46
|
+
| `--env <name>` | Load vars from remote Stably environment |
|
|
47
|
+
| `--env-file <path>` | Load vars from local file (repeatable) |
|
|
48
|
+
| `--verbose` / `-v` | Verbose logging |
|
|
49
|
+
| `--no-telemetry` | Disable telemetry |
|
|
50
|
+
|
|
51
|
+
**Env var precedence** (highest → lowest): Stably auth (`STABLY_API_KEY`) → `--env` → `--env-file` → `process.env`
|
|
52
|
+
|
|
53
|
+
## Core Commands
|
|
54
|
+
|
|
55
|
+
### `stably create [prompt...]`
|
|
56
|
+
|
|
57
|
+
Generates tests from a prompt (quotes optional) or infers from branch diff when no prompt given.
|
|
58
|
+
|
|
59
|
+
- `--output <dir>` — override output directory (auto-detected from `playwright.config.ts` `testDir` or common dirs like `tests/`, `e2e/`)
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
stably create "test the checkout flow"
|
|
63
|
+
stably create # infer from diff
|
|
64
|
+
stably create "test registration" --output ./e2e
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Prompt tips:** be specific about user flows, UI elements, auth requirements, and error states.
|
|
68
|
+
|
|
69
|
+
### `stably test`
|
|
70
|
+
|
|
71
|
+
Runs Playwright tests with Stably reporter. Auto-enables `--trace=on`. All Playwright CLI options pass through:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
stably test --headed --project=chromium --workers=4
|
|
75
|
+
stably test --grep="login" tests/login.spec.ts
|
|
76
|
+
stably --env staging test --headed
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### `stably fix [runId]`
|
|
80
|
+
|
|
81
|
+
Fixes failing tests using AI analysis of traces, screenshots, logs, and DOM state.
|
|
82
|
+
|
|
83
|
+
**Run ID resolution:** explicit arg → CI env (the CLI maps GitHub's run ID to the corresponding Stably run) → `.stably/last-run.json` (warns if >24h old). Requires git repo.
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
stably fix # auto-detect last run
|
|
87
|
+
stably fix abc123 # explicit run ID
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Typical workflow:** `stably test` → (failures?) → `stably fix` → `stably test`
|
|
91
|
+
|
|
92
|
+
### `stably verify <prompt...>`
|
|
93
|
+
|
|
94
|
+
Verifies app behavior against a natural-language description without generating test files.
|
|
95
|
+
|
|
96
|
+
- `-u, --url <url>` — target URL (otherwise auto-detected)
|
|
97
|
+
- `--max-budget <dollars>` — max budget in USD (default: 5)
|
|
98
|
+
- `--no-interactive` — disable interactive prompts
|
|
99
|
+
|
|
100
|
+
Exit codes: `0` = PASS, `1` = FAIL, `2` = INCONCLUSIVE.
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
stably verify "users can sign up with email"
|
|
104
|
+
stably verify "checkout flow works" --url http://localhost:3000
|
|
105
|
+
stably verify "login page loads" --no-interactive
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**Agent note:** The default $5 budget is sufficient for most verifications. Avoid increasing `--max-budget` without explicit user approval.
|
|
109
|
+
|
|
110
|
+
### `stably runs list [options]`
|
|
111
|
+
|
|
112
|
+
Lists recent test runs for the current project.
|
|
113
|
+
|
|
114
|
+
- `-b, --branch <name>` — filter by git branch
|
|
115
|
+
- `-n, --limit <number>` — max results (default 20, max 100)
|
|
116
|
+
- `--after <runId>` / `--before <runId>` — cursor-based pagination by run ID
|
|
117
|
+
- `--source <source>` — filter by source (`local`, `ci`, `web`)
|
|
118
|
+
- `-s, --status <status>` — filter by status (e.g. `passed`, `failed`)
|
|
119
|
+
- `--suite <name>` — filter by test suite
|
|
120
|
+
- `--trigger <trigger>` — filter by trigger type
|
|
121
|
+
- `--json` — output as JSON (preferred for AI agents)
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
stably runs list # recent runs
|
|
125
|
+
stably runs list --status failed # find failed runs
|
|
126
|
+
stably runs list --branch main --limit 5 # recent runs on main
|
|
127
|
+
stably runs list --json # machine-readable output
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Agent note:** Always use `--json` for machine-readable output when parsing run data programmatically.
|
|
131
|
+
|
|
132
|
+
### `stably runs view <runId> [options]`
|
|
133
|
+
|
|
134
|
+
Shows details for a specific test run including metadata, issues with root causes, and individual test results.
|
|
135
|
+
|
|
136
|
+
- `--json` — output as JSON (preferred for AI agents)
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
stably runs view abc123
|
|
140
|
+
stably runs view abc123 --json
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Remote Environments
|
|
144
|
+
|
|
145
|
+
`stably env list` — list environments in current project.
|
|
146
|
+
`stably env inspect <name>` — show variable names/metadata (values never printed).
|
|
147
|
+
|
|
148
|
+
Use `--env` for team-shared dashboard variables; `--env-file` for local `.env` files. Both combine (`--env` wins).
|
|
149
|
+
|
|
150
|
+
## Long-Running Commands (AI Agents)
|
|
151
|
+
|
|
152
|
+
`stably create`, `stably fix`, and `stably verify` are AI-powered and can take **several minutes**.
|
|
153
|
+
|
|
154
|
+
| Agent | Configuration |
|
|
155
|
+
|-------|--------------|
|
|
156
|
+
| **Claude Code** | `timeout: 600000` (preferred — retains command output), or `run_in_background: true` when parallel work is needed |
|
|
157
|
+
| **Cursor** | `block_until_ms: 900000` (default 30s is too short) |
|
|
158
|
+
|
|
159
|
+
`stably test` duration depends on suite size — use the same timeout for large suites. All other commands complete in seconds.
|
|
160
|
+
|
|
161
|
+
**If a command times out or fails:** retry once with `--verbose` for diagnostics. AI-powered commands are idempotent — retrying is safe. If failures persist, check `stably whoami` (auth) and network connectivity.
|
|
162
|
+
|
|
163
|
+
For general long-running command patterns (dev servers, watchers), see `bash-commands` skill.
|
|
164
|
+
|
|
165
|
+
## Configuration
|
|
166
|
+
|
|
167
|
+
### Required Env Vars
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
# NEVER hardcode real values — use .env files (gitignored) or CI secrets
|
|
171
|
+
STABLY_API_KEY=your_key # from https://auth.stably.ai/org/api_keys/
|
|
172
|
+
STABLY_PROJECT_ID=your_id # from dashboard
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Set via `.env` file, `--env-file`, or `--env` (remote).
|
|
176
|
+
|
|
177
|
+
### Playwright Config
|
|
178
|
+
|
|
179
|
+
`stably test` auto-enables tracing. Set `trace: 'on'` in config too for direct `npx playwright test` runs:
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
import { defineConfig, stablyReporter } from '@stablyai/playwright-test';
|
|
183
|
+
|
|
184
|
+
export default defineConfig({
|
|
185
|
+
use: { trace: 'on' },
|
|
186
|
+
reporter: [
|
|
187
|
+
['list'],
|
|
188
|
+
stablyReporter({
|
|
189
|
+
apiKey: process.env.STABLY_API_KEY,
|
|
190
|
+
projectId: process.env.STABLY_PROJECT_ID,
|
|
191
|
+
}),
|
|
192
|
+
],
|
|
193
|
+
});
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## CI/CD: GitHub Actions
|
|
197
|
+
|
|
198
|
+
### Self-healing test pipeline
|
|
199
|
+
|
|
200
|
+
```yaml
|
|
201
|
+
name: E2E Tests with Auto-Fix
|
|
202
|
+
on: [push, pull_request]
|
|
203
|
+
jobs:
|
|
204
|
+
test:
|
|
205
|
+
runs-on: ubuntu-latest
|
|
206
|
+
steps:
|
|
207
|
+
- uses: actions/checkout@v4
|
|
208
|
+
- uses: actions/setup-node@v4
|
|
209
|
+
with: { node-version: '20' }
|
|
210
|
+
- run: npm ci
|
|
211
|
+
- run: npx stably install --with-deps
|
|
212
|
+
- name: Run tests
|
|
213
|
+
id: test
|
|
214
|
+
run: npx stably --env staging test || echo "TEST_FAILED=true" >> "$GITHUB_ENV"
|
|
215
|
+
env:
|
|
216
|
+
STABLY_API_KEY: ${{ secrets.STABLY_API_KEY }}
|
|
217
|
+
STABLY_PROJECT_ID: ${{ secrets.STABLY_PROJECT_ID }}
|
|
218
|
+
- name: Auto-fix failures
|
|
219
|
+
if: env.TEST_FAILED == 'true'
|
|
220
|
+
run: npx stably --env staging fix
|
|
221
|
+
env:
|
|
222
|
+
STABLY_API_KEY: ${{ secrets.STABLY_API_KEY }}
|
|
223
|
+
STABLY_PROJECT_ID: ${{ secrets.STABLY_PROJECT_ID }}
|
|
224
|
+
- name: Re-run tests after fix
|
|
225
|
+
if: env.TEST_FAILED == 'true'
|
|
226
|
+
run: npx stably --env staging test
|
|
227
|
+
env:
|
|
228
|
+
STABLY_API_KEY: ${{ secrets.STABLY_API_KEY }}
|
|
229
|
+
STABLY_PROJECT_ID: ${{ secrets.STABLY_PROJECT_ID }}
|
|
230
|
+
- name: Commit fixes
|
|
231
|
+
if: env.TEST_FAILED == 'true' && github.event_name == 'push'
|
|
232
|
+
run: |
|
|
233
|
+
git config --local user.email "action@github.com"
|
|
234
|
+
git config --local user.name "GitHub Action"
|
|
235
|
+
# Adjust paths to match your project's test directories
|
|
236
|
+
git add 'tests/' 'e2e/' '**/*.spec.ts' '**/*.test.ts' 2>/dev/null || true
|
|
237
|
+
git diff --staged --quiet || git commit -m "fix: auto-repair failing tests [skip ci]"
|
|
238
|
+
git push
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Troubleshooting
|
|
242
|
+
|
|
243
|
+
| Problem | Solution |
|
|
244
|
+
|---------|----------|
|
|
245
|
+
| "Not authenticated" | `stably login` |
|
|
246
|
+
| API key not recognized | `stably whoami` to verify |
|
|
247
|
+
| Tests in wrong directory | `stably create "..." --output ./tests/e2e` |
|
|
248
|
+
| Missing browser | `stably install --with-deps` |
|
|
249
|
+
| Traces not uploading | Set `trace: 'on'` in `playwright.config.ts` |
|
|
250
|
+
| "Run ID not found" | Run `stably test` first, then `stably fix` |
|
|
251
|
+
|
|
252
|
+
## Links
|
|
253
|
+
|
|
254
|
+
[Docs](https://docs.stably.ai) · [CLI Quickstart](https://docs.stably.ai/stably2/cli-quickstart) · [Dashboard](https://app.stably.ai) · [API Keys](https://auth.stably.ai/org/api_keys/)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stably",
|
|
3
|
-
"version": "4.10.
|
|
3
|
+
"version": "4.10.5",
|
|
4
4
|
"packageManager": "pnpm@10.24.0",
|
|
5
5
|
"description": "AI-powered E2E Playwright testing CLI. Stably can understand your codebase, edit/run tests, and handle complex test scenarios for you.",
|
|
6
6
|
"main": "dist/index.mjs",
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"@stablyhq/runner-sdk": "^1.0.8",
|
|
71
71
|
"commander": "^14.0.1",
|
|
72
72
|
"dotenv": "^17.2.3",
|
|
73
|
-
"ink": "^6.
|
|
73
|
+
"ink": "^6.8.0",
|
|
74
74
|
"open": "^11.0.0",
|
|
75
75
|
"picocolors": "^1.1.1",
|
|
76
76
|
"pino": "^9.6.0",
|