agent-bober 0.4.3 → 0.5.1
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 +30 -0
- package/agents/bober-evaluator.md +277 -8
- package/agents/bober-generator.md +155 -0
- package/agents/bober-planner.md +70 -0
- package/dist/cli/commands/init.js +1 -0
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/evaluators/builtin/playwright.d.ts +11 -0
- package/dist/evaluators/builtin/playwright.d.ts.map +1 -1
- package/dist/evaluators/builtin/playwright.js +259 -12
- package/dist/evaluators/builtin/playwright.js.map +1 -1
- package/package.json +1 -1
- package/skills/bober.eval/SKILL.md +145 -148
- package/skills/bober.playwright/SKILL.md +429 -0
- package/skills/bober.playwright/references/playwright-patterns.md +377 -0
- package/skills/bober.run/SKILL.md +425 -118
- package/skills/bober.sprint/SKILL.md +147 -57
- package/templates/presets/nextjs/bober.config.json +2 -1
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
# Playwright Best Practices for Bober Projects
|
|
2
|
+
|
|
3
|
+
This reference document describes patterns and best practices for writing Playwright E2E tests within the bober agent workflow. The generator and evaluator agents should follow these patterns to produce robust, maintainable tests.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Selector Strategy: `data-testid` First
|
|
8
|
+
|
|
9
|
+
When Playwright is an evaluation strategy, all UI components **must** include `data-testid` attributes on interactive elements and key content areas. This is non-negotiable.
|
|
10
|
+
|
|
11
|
+
### Why `data-testid`?
|
|
12
|
+
|
|
13
|
+
- **Stable across refactors.** CSS classes, tag names, and text content change frequently. `data-testid` attributes are explicitly for testing and survive refactors.
|
|
14
|
+
- **No coupling to styling.** Tests do not break when Tailwind classes change or when a component library is swapped.
|
|
15
|
+
- **Clear intent.** A `data-testid` attribute signals "this element is tested" to every developer on the team.
|
|
16
|
+
- **Framework-agnostic.** Works identically in React, Vue, Svelte, and plain HTML.
|
|
17
|
+
|
|
18
|
+
### Naming Convention
|
|
19
|
+
|
|
20
|
+
Use descriptive, kebab-case names that describe the element's purpose:
|
|
21
|
+
|
|
22
|
+
```html
|
|
23
|
+
<!-- Good -->
|
|
24
|
+
<form data-testid="login-form">
|
|
25
|
+
<input data-testid="email-input" />
|
|
26
|
+
<input data-testid="password-input" />
|
|
27
|
+
<button data-testid="login-submit-button">Log In</button>
|
|
28
|
+
<p data-testid="login-error-message">Invalid credentials</p>
|
|
29
|
+
</form>
|
|
30
|
+
|
|
31
|
+
<!-- Bad -->
|
|
32
|
+
<form data-testid="form1">
|
|
33
|
+
<input data-testid="input1" />
|
|
34
|
+
<button data-testid="btn">Log In</button>
|
|
35
|
+
</form>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Where to Add `data-testid`
|
|
39
|
+
|
|
40
|
+
The generator must add `data-testid` to:
|
|
41
|
+
- Forms and form elements (inputs, buttons, selects, textareas)
|
|
42
|
+
- Navigation links and menu items
|
|
43
|
+
- Content containers that display dynamic data (cards, lists, tables)
|
|
44
|
+
- Error messages and status indicators
|
|
45
|
+
- Modal dialogs and their trigger buttons
|
|
46
|
+
- Loading indicators and empty state messages
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Test File Structure
|
|
51
|
+
|
|
52
|
+
### One File Per Feature or Sprint
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
e2e/
|
|
56
|
+
auth.spec.ts # Authentication flows (login, register, logout)
|
|
57
|
+
dashboard.spec.ts # Dashboard page tests
|
|
58
|
+
settings.spec.ts # Settings page tests
|
|
59
|
+
auth.setup.ts # Authentication setup (shared storage state)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Standard Test Template
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
import { test, expect } from '@playwright/test';
|
|
66
|
+
|
|
67
|
+
test.describe('Feature: Dashboard', () => {
|
|
68
|
+
test.beforeEach(async ({ page }) => {
|
|
69
|
+
await page.goto('/dashboard');
|
|
70
|
+
await page.waitForLoadState('networkidle');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('displays the dashboard header', async ({ page }) => {
|
|
74
|
+
const header = page.getByTestId('dashboard-header');
|
|
75
|
+
await expect(header).toBeVisible();
|
|
76
|
+
await expect(header).toHaveText(/Dashboard/);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('shows data cards when data is loaded', async ({ page }) => {
|
|
80
|
+
const cardContainer = page.getByTestId('dashboard-cards');
|
|
81
|
+
await expect(cardContainer).toBeVisible();
|
|
82
|
+
|
|
83
|
+
const cards = page.getByTestId('dashboard-card');
|
|
84
|
+
await expect(cards.first()).toBeVisible();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('handles empty state gracefully', async ({ page }) => {
|
|
88
|
+
// If testing empty state, you may need to intercept the API
|
|
89
|
+
await page.route('**/api/dashboard', (route) =>
|
|
90
|
+
route.fulfill({ status: 200, json: { items: [] } }),
|
|
91
|
+
);
|
|
92
|
+
await page.reload();
|
|
93
|
+
await page.waitForLoadState('networkidle');
|
|
94
|
+
|
|
95
|
+
const emptyState = page.getByTestId('dashboard-empty-state');
|
|
96
|
+
await expect(emptyState).toBeVisible();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Common Patterns
|
|
104
|
+
|
|
105
|
+
### Form Submission
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
test('submits the registration form', async ({ page }) => {
|
|
109
|
+
await page.goto('/register');
|
|
110
|
+
|
|
111
|
+
await page.getByTestId('name-input').fill('Test User');
|
|
112
|
+
await page.getByTestId('email-input').fill('test@example.com');
|
|
113
|
+
await page.getByTestId('password-input').fill('SecurePass123!');
|
|
114
|
+
await page.getByTestId('register-submit-button').click();
|
|
115
|
+
|
|
116
|
+
// Wait for navigation after successful submission
|
|
117
|
+
await expect(page).toHaveURL(/\/dashboard/);
|
|
118
|
+
|
|
119
|
+
// Or wait for a success message
|
|
120
|
+
await expect(page.getByTestId('success-message')).toBeVisible();
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Form Validation
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
test('shows validation errors for invalid input', async ({ page }) => {
|
|
128
|
+
await page.goto('/register');
|
|
129
|
+
|
|
130
|
+
// Submit empty form
|
|
131
|
+
await page.getByTestId('register-submit-button').click();
|
|
132
|
+
|
|
133
|
+
// Check for validation messages
|
|
134
|
+
await expect(page.getByTestId('name-error')).toBeVisible();
|
|
135
|
+
await expect(page.getByTestId('email-error')).toBeVisible();
|
|
136
|
+
await expect(page.getByTestId('password-error')).toBeVisible();
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Navigation
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
test('navigates to the settings page', async ({ page }) => {
|
|
144
|
+
await page.goto('/');
|
|
145
|
+
|
|
146
|
+
await page.getByTestId('nav-settings-link').click();
|
|
147
|
+
await expect(page).toHaveURL(/\/settings/);
|
|
148
|
+
await expect(page.getByTestId('settings-page')).toBeVisible();
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### SPA Route Changes
|
|
153
|
+
|
|
154
|
+
For single-page applications, use `waitForLoadState('networkidle')` after client-side navigation:
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
test('client-side navigation works', async ({ page }) => {
|
|
158
|
+
await page.goto('/');
|
|
159
|
+
await page.waitForLoadState('networkidle');
|
|
160
|
+
|
|
161
|
+
await page.getByTestId('nav-about-link').click();
|
|
162
|
+
await page.waitForLoadState('networkidle');
|
|
163
|
+
|
|
164
|
+
await expect(page).toHaveURL(/\/about/);
|
|
165
|
+
await expect(page.getByTestId('about-page-content')).toBeVisible();
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### API Mocking
|
|
170
|
+
|
|
171
|
+
Use Playwright's `page.route()` to mock API responses when testing specific UI states:
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
test('displays error when API fails', async ({ page }) => {
|
|
175
|
+
// Intercept the API call and return an error
|
|
176
|
+
await page.route('**/api/users', (route) =>
|
|
177
|
+
route.fulfill({
|
|
178
|
+
status: 500,
|
|
179
|
+
json: { error: 'Internal Server Error' },
|
|
180
|
+
}),
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
await page.goto('/users');
|
|
184
|
+
await page.waitForLoadState('networkidle');
|
|
185
|
+
|
|
186
|
+
await expect(page.getByTestId('error-message')).toBeVisible();
|
|
187
|
+
await expect(page.getByTestId('error-message')).toContainText(/error/i);
|
|
188
|
+
});
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Waiting for API Responses
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
test('loads user data from API', async ({ page }) => {
|
|
195
|
+
await page.goto('/users');
|
|
196
|
+
|
|
197
|
+
// Wait for the specific API response
|
|
198
|
+
const response = await page.waitForResponse('**/api/users');
|
|
199
|
+
expect(response.status()).toBe(200);
|
|
200
|
+
|
|
201
|
+
// Then assert on the rendered data
|
|
202
|
+
await expect(page.getByTestId('user-list')).toBeVisible();
|
|
203
|
+
});
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## Authentication Handling
|
|
209
|
+
|
|
210
|
+
### Shared Auth State with `storageState`
|
|
211
|
+
|
|
212
|
+
For projects requiring authenticated users, create a setup file that logs in once and saves the browser state:
|
|
213
|
+
|
|
214
|
+
**`e2e/auth.setup.ts`:**
|
|
215
|
+
```typescript
|
|
216
|
+
import { test as setup, expect } from '@playwright/test';
|
|
217
|
+
|
|
218
|
+
const authFile = 'e2e/.auth/user.json';
|
|
219
|
+
|
|
220
|
+
setup('authenticate', async ({ page }) => {
|
|
221
|
+
await page.goto('/login');
|
|
222
|
+
|
|
223
|
+
await page.getByTestId('email-input').fill('test@example.com');
|
|
224
|
+
await page.getByTestId('password-input').fill('testpassword');
|
|
225
|
+
await page.getByTestId('login-submit-button').click();
|
|
226
|
+
|
|
227
|
+
// Wait for successful login (redirect to dashboard or similar)
|
|
228
|
+
await expect(page).toHaveURL(/\/dashboard/);
|
|
229
|
+
|
|
230
|
+
// Save the authentication state
|
|
231
|
+
await page.context().storageState({ path: authFile });
|
|
232
|
+
});
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
**`playwright.config.ts` additions:**
|
|
236
|
+
```typescript
|
|
237
|
+
projects: [
|
|
238
|
+
{
|
|
239
|
+
name: 'setup',
|
|
240
|
+
testMatch: /auth\.setup\.ts/,
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
name: 'chromium',
|
|
244
|
+
use: {
|
|
245
|
+
...devices['Desktop Chrome'],
|
|
246
|
+
storageState: 'e2e/.auth/user.json',
|
|
247
|
+
},
|
|
248
|
+
dependencies: ['setup'],
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Add `e2e/.auth/` to `.gitignore`.
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## Visual Verification Without Flakiness
|
|
258
|
+
|
|
259
|
+
Avoid pixel-comparison screenshot tests. They are extremely flaky across different environments (font rendering, anti-aliasing, OS differences). Instead:
|
|
260
|
+
|
|
261
|
+
### Check Structural Presence
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
// Good: Check that elements exist and have the right content
|
|
265
|
+
await expect(page.getByTestId('hero-title')).toHaveText('Welcome');
|
|
266
|
+
await expect(page.getByTestId('hero-image')).toBeVisible();
|
|
267
|
+
|
|
268
|
+
// Bad: Screenshot comparison
|
|
269
|
+
// expect(await page.screenshot()).toMatchSnapshot();
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Check CSS Properties When Needed
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
// Check that an element has a specific visual state
|
|
276
|
+
const button = page.getByTestId('submit-button');
|
|
277
|
+
await expect(button).toHaveCSS('background-color', 'rgb(37, 99, 235)');
|
|
278
|
+
await expect(button).toBeEnabled();
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Use Failure Screenshots for Debugging Only
|
|
282
|
+
|
|
283
|
+
The `playwright.config.ts` is configured with `screenshot: 'only-on-failure'`. This captures screenshots when tests fail, which is useful for debugging but does not introduce flaky assertions.
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## Error Handling in Tests
|
|
288
|
+
|
|
289
|
+
### Capture Console Errors
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
test('page has no JavaScript errors', async ({ page }) => {
|
|
293
|
+
const errors: string[] = [];
|
|
294
|
+
page.on('console', (msg) => {
|
|
295
|
+
if (msg.type() === 'error') {
|
|
296
|
+
errors.push(msg.text());
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
await page.goto('/dashboard');
|
|
301
|
+
await page.waitForLoadState('networkidle');
|
|
302
|
+
|
|
303
|
+
// Filter out known acceptable errors if needed
|
|
304
|
+
const criticalErrors = errors.filter(
|
|
305
|
+
(e) => !e.includes('favicon.ico'),
|
|
306
|
+
);
|
|
307
|
+
expect(criticalErrors).toEqual([]);
|
|
308
|
+
});
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### Handle Network Failures Gracefully
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
test('shows offline message when network fails', async ({ page }) => {
|
|
315
|
+
await page.goto('/dashboard');
|
|
316
|
+
await page.waitForLoadState('networkidle');
|
|
317
|
+
|
|
318
|
+
// Simulate network failure
|
|
319
|
+
await page.route('**/*', (route) => route.abort());
|
|
320
|
+
|
|
321
|
+
// Trigger an action that requires network
|
|
322
|
+
await page.getByTestId('refresh-button').click();
|
|
323
|
+
|
|
324
|
+
await expect(page.getByTestId('network-error-message')).toBeVisible();
|
|
325
|
+
});
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
## Debugging Tips
|
|
331
|
+
|
|
332
|
+
### Interactive Mode
|
|
333
|
+
|
|
334
|
+
```bash
|
|
335
|
+
npx playwright test --ui
|
|
336
|
+
```
|
|
337
|
+
Opens a browser-based UI for running and debugging tests interactively.
|
|
338
|
+
|
|
339
|
+
### Headed Mode
|
|
340
|
+
|
|
341
|
+
```bash
|
|
342
|
+
npx playwright test --headed
|
|
343
|
+
```
|
|
344
|
+
Runs tests in a visible browser window.
|
|
345
|
+
|
|
346
|
+
### Trace Viewer
|
|
347
|
+
|
|
348
|
+
When a test fails with `trace: 'on-first-retry'` configured:
|
|
349
|
+
```bash
|
|
350
|
+
npx playwright show-trace test-results/<test-folder>/trace.zip
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### Specific Test Execution
|
|
354
|
+
|
|
355
|
+
```bash
|
|
356
|
+
npx playwright test e2e/auth.spec.ts # specific file
|
|
357
|
+
npx playwright test -g "submits the login form" # specific test by name
|
|
358
|
+
npx playwright test e2e/auth.spec.ts:15 # specific line
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
|
|
363
|
+
## Things to Avoid
|
|
364
|
+
|
|
365
|
+
1. **Never use `page.waitForTimeout()`**. It is a hardcoded delay that introduces flakiness. Use auto-waiting assertions or event-based waits.
|
|
366
|
+
|
|
367
|
+
2. **Never use CSS class selectors** (`.btn-primary`, `.card`). Classes change during refactors and are not stable test anchors.
|
|
368
|
+
|
|
369
|
+
3. **Never use XPath selectors**. They are fragile and hard to maintain. Use `data-testid` attributes.
|
|
370
|
+
|
|
371
|
+
4. **Never test implementation details**. Do not assert on Redux store state, internal component state, or private API responses. Test what the user sees.
|
|
372
|
+
|
|
373
|
+
5. **Never rely on test execution order**. Each test must be independently runnable. Use `test.beforeEach` for setup.
|
|
374
|
+
|
|
375
|
+
6. **Never hardcode wait times**. If you find yourself adding `waitForTimeout(2000)`, there is a better way to wait (usually an assertion or `waitForResponse`).
|
|
376
|
+
|
|
377
|
+
7. **Never commit `e2e-results/` or `test-results/`**. These are ephemeral outputs. They belong in `.gitignore`.
|