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.
@@ -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`.