digiqagent 1.2.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.
Files changed (33) hide show
  1. package/.cursor-plugin/plugin.json +29 -0
  2. package/README.md +197 -0
  3. package/agents/code-reviewer.md +48 -0
  4. package/bin/cli.js +210 -0
  5. package/package.json +41 -0
  6. package/skills/playwright-test-generator/SKILL.md +563 -0
  7. package/skills/playwright-test-generator/interaction-test-patterns.md +987 -0
  8. package/skills/playwright-test-generator/page-object-patterns.md +833 -0
  9. package/skills/postman-collection-generator/SKILL.md +310 -0
  10. package/skills/postman-collection-generator/collection-patterns.md +493 -0
  11. package/skills/postman-test-suite-generator/SKILL.md +653 -0
  12. package/skills/postman-test-suite-generator/test-scenario-patterns.md +612 -0
  13. package/skills/receiving-code-review/SKILL.md +213 -0
  14. package/skills/requesting-code-review/SKILL.md +105 -0
  15. package/skills/requesting-code-review/code-reviewer.md +146 -0
  16. package/skills/review-prompts/code-quality-reviewer-prompt.md +26 -0
  17. package/skills/review-prompts/spec-reviewer-prompt.md +61 -0
  18. package/skills/swagger-generator/SKILL.md +238 -0
  19. package/skills/swagger-generator/openapi-patterns.md +667 -0
  20. package/skills/systematic-debugging/CREATION-LOG.md +119 -0
  21. package/skills/systematic-debugging/SKILL.md +296 -0
  22. package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
  23. package/skills/systematic-debugging/condition-based-waiting.md +115 -0
  24. package/skills/systematic-debugging/defense-in-depth.md +122 -0
  25. package/skills/systematic-debugging/find-polluter.sh +63 -0
  26. package/skills/systematic-debugging/root-cause-tracing.md +169 -0
  27. package/skills/systematic-debugging/test-academic.md +14 -0
  28. package/skills/systematic-debugging/test-pressure-1.md +58 -0
  29. package/skills/systematic-debugging/test-pressure-2.md +68 -0
  30. package/skills/systematic-debugging/test-pressure-3.md +69 -0
  31. package/skills/test-driven-development/SKILL.md +371 -0
  32. package/skills/test-driven-development/testing-anti-patterns.md +299 -0
  33. package/skills/verification-before-completion/SKILL.md +139 -0
@@ -0,0 +1,987 @@
1
+ # Interaction Test Patterns
2
+
3
+ Comprehensive Playwright test code patterns organized by interaction type. Copy and adapt for generated test suites.
4
+
5
+ ## Forms — Fill, Submit, Validate
6
+
7
+ ### Positive — Valid Form Submission
8
+
9
+ ```typescript
10
+ test('should create user with valid data', async ({ page }) => {
11
+ const formPage = new UserFormPage(page);
12
+ await formPage.gotoCreate();
13
+
14
+ await formPage.fillAndSubmit({
15
+ name: 'Jane Smith',
16
+ email: 'jane.smith@example.com',
17
+ role: 'User',
18
+ });
19
+
20
+ await expect(page).toHaveURL(/\/users$/);
21
+ await expect(page.getByRole('alert')).toContainText(/created|success/i);
22
+ });
23
+ ```
24
+
25
+ ### Negative — Missing Required Fields (One at a Time)
26
+
27
+ ```typescript
28
+ test('should show error when name is empty', async ({ page }) => {
29
+ const formPage = new UserFormPage(page);
30
+ await formPage.gotoCreate();
31
+
32
+ await formPage.fillEmail('jane@example.com');
33
+ await formPage.clickSubmit();
34
+
35
+ await expect(formPage.nameError).toBeVisible();
36
+ await expect(formPage.nameError).toContainText(/required|name/i);
37
+ });
38
+
39
+ test('should show error when email is empty', async ({ page }) => {
40
+ const formPage = new UserFormPage(page);
41
+ await formPage.gotoCreate();
42
+
43
+ await formPage.fillName('Jane Smith');
44
+ await formPage.clickSubmit();
45
+
46
+ await expect(formPage.emailError).toBeVisible();
47
+ await expect(formPage.emailError).toContainText(/required|email/i);
48
+ });
49
+ ```
50
+
51
+ ### Negative — Invalid Formats
52
+
53
+ ```typescript
54
+ test('should show error for invalid email format', async ({ page }) => {
55
+ const formPage = new UserFormPage(page);
56
+ await formPage.gotoCreate();
57
+
58
+ await formPage.fillName('Jane Smith');
59
+ await formPage.fillEmail('not-an-email');
60
+ await formPage.clickSubmit();
61
+
62
+ await expect(formPage.emailError).toBeVisible();
63
+ await expect(formPage.emailError).toContainText(/valid|format|email/i);
64
+ });
65
+
66
+ test('should show error for invalid phone format', async ({ page }) => {
67
+ const formPage = new UserFormPage(page);
68
+ await formPage.gotoCreate();
69
+
70
+ await formPage.fillName('Jane Smith');
71
+ await formPage.fillEmail('jane@example.com');
72
+ await formPage.fillPhone('abc');
73
+ await formPage.clickSubmit();
74
+
75
+ await expect(formPage.phoneError).toBeVisible();
76
+ });
77
+ ```
78
+
79
+ ### Negative — Boundary Values
80
+
81
+ ```typescript
82
+ test('should show error when name is too short', async ({ page }) => {
83
+ const formPage = new UserFormPage(page);
84
+ await formPage.gotoCreate();
85
+
86
+ await formPage.fillName('A');
87
+ await formPage.fillEmail('jane@example.com');
88
+ await formPage.clickSubmit();
89
+
90
+ await expect(formPage.nameError).toBeVisible();
91
+ await expect(formPage.nameError).toContainText(/minimum|at least|characters/i);
92
+ });
93
+
94
+ test('should show error when name exceeds max length', async ({ page }) => {
95
+ const formPage = new UserFormPage(page);
96
+ await formPage.gotoCreate();
97
+
98
+ await formPage.fillName('A'.repeat(256));
99
+ await formPage.fillEmail('jane@example.com');
100
+ await formPage.clickSubmit();
101
+
102
+ await expect(formPage.nameError).toBeVisible();
103
+ });
104
+ ```
105
+
106
+ ### Form Reset and Dirty State
107
+
108
+ ```typescript
109
+ test('should clear form on cancel', async ({ page }) => {
110
+ const formPage = new UserFormPage(page);
111
+ await formPage.gotoCreate();
112
+
113
+ await formPage.fillName('Jane Smith');
114
+ await formPage.fillEmail('jane@example.com');
115
+ await formPage.clickCancel();
116
+
117
+ await expect(page).toHaveURL(/\/users$/);
118
+ });
119
+
120
+ test('should warn before leaving dirty form', async ({ page }) => {
121
+ const formPage = new UserFormPage(page);
122
+ await formPage.gotoCreate();
123
+
124
+ await formPage.fillName('Jane Smith');
125
+
126
+ // Try to navigate away
127
+ page.on('dialog', dialog => dialog.dismiss());
128
+ await page.getByRole('link', { name: /dashboard/i }).click();
129
+
130
+ // Should stay on form page
131
+ await expect(page).toHaveURL(/\/users\/new/);
132
+ });
133
+ ```
134
+
135
+ ### Inline Validation (Blur Trigger)
136
+
137
+ ```typescript
138
+ test('should show validation on field blur (Angular)', async ({ page }) => {
139
+ const formPage = new UserFormPage(page);
140
+ await formPage.gotoCreate();
141
+
142
+ await formPage.emailInput.focus();
143
+ await formPage.emailInput.blur();
144
+
145
+ await expect(formPage.emailError).toBeVisible();
146
+ });
147
+
148
+ test('should clear validation when valid input entered', async ({ page }) => {
149
+ const formPage = new UserFormPage(page);
150
+ await formPage.gotoCreate();
151
+
152
+ await formPage.emailInput.focus();
153
+ await formPage.emailInput.blur();
154
+ await expect(formPage.emailError).toBeVisible();
155
+
156
+ await formPage.fillEmail('jane@example.com');
157
+ await expect(formPage.emailError).not.toBeVisible();
158
+ });
159
+ ```
160
+
161
+ ## Click Events
162
+
163
+ ### Button Click
164
+
165
+ ```typescript
166
+ test('should navigate to create form on Add button click', async ({ page }) => {
167
+ const listPage = new UserListPage(page);
168
+ await listPage.goto();
169
+
170
+ await listPage.clickAdd();
171
+
172
+ await expect(page).toHaveURL(/\/users\/new/);
173
+ });
174
+ ```
175
+
176
+ ### Toggle Switch
177
+
178
+ ```typescript
179
+ test('should toggle active status', async ({ page }) => {
180
+ const formPage = new UserFormPage(page);
181
+ await formPage.gotoEdit('123');
182
+
183
+ const toggle = page.getByRole('switch', { name: /active/i });
184
+ await expect(toggle).toBeChecked();
185
+
186
+ await toggle.click();
187
+ await expect(toggle).not.toBeChecked();
188
+
189
+ await toggle.click();
190
+ await expect(toggle).toBeChecked();
191
+ });
192
+ ```
193
+
194
+ ### Double-Click Prevention
195
+
196
+ ```typescript
197
+ test('should not submit form twice on rapid double-click', async ({ page }) => {
198
+ const formPage = new UserFormPage(page);
199
+ await formPage.gotoCreate();
200
+
201
+ await formPage.fillName('Jane Smith');
202
+ await formPage.fillEmail('jane@example.com');
203
+
204
+ await formPage.submitButton.dblclick();
205
+
206
+ // Should navigate once, not show duplicate error
207
+ await expect(page).toHaveURL(/\/users$/);
208
+
209
+ // Verify submit button was disabled after first click
210
+ // or only one success toast appeared
211
+ const toasts = page.getByRole('alert');
212
+ await expect(toasts).toHaveCount(1);
213
+ });
214
+ ```
215
+
216
+ ### Icon Button Actions
217
+
218
+ ```typescript
219
+ test('should open edit form on row edit icon click', async ({ page }) => {
220
+ const listPage = new UserListPage(page);
221
+ await listPage.goto();
222
+
223
+ await listPage.clickEditRow(0);
224
+
225
+ await expect(page).toHaveURL(/\/users\/[^/]+\/edit/);
226
+ });
227
+ ```
228
+
229
+ ## Navigation
230
+
231
+ ### Route Transitions
232
+
233
+ ```typescript
234
+ test('should navigate between pages via sidebar', async ({ page }) => {
235
+ await page.goto('/dashboard');
236
+ const nav = new NavigationComponent(page);
237
+
238
+ await nav.navigateTo('Users');
239
+ await expect(page).toHaveURL(/\/users/);
240
+ await expect(page.getByRole('heading', { name: /users/i })).toBeVisible();
241
+
242
+ await nav.navigateTo('Settings');
243
+ await expect(page).toHaveURL(/\/settings/);
244
+ });
245
+ ```
246
+
247
+ ### Browser Back/Forward
248
+
249
+ ```typescript
250
+ test('should support browser back and forward navigation', async ({ page }) => {
251
+ await page.goto('/dashboard');
252
+ await page.getByRole('link', { name: /users/i }).click();
253
+ await expect(page).toHaveURL(/\/users/);
254
+
255
+ await page.goBack();
256
+ await expect(page).toHaveURL(/\/dashboard/);
257
+
258
+ await page.goForward();
259
+ await expect(page).toHaveURL(/\/users/);
260
+ });
261
+ ```
262
+
263
+ ### Deep Linking
264
+
265
+ ```typescript
266
+ test('should load correct page from direct URL', async ({ page }) => {
267
+ await page.goto('/users');
268
+ await expect(page.getByRole('heading', { name: /users/i })).toBeVisible();
269
+ await expect(page.locator('mat-table, table')).toBeVisible();
270
+ });
271
+ ```
272
+
273
+ ### Protected Routes
274
+
275
+ ```typescript
276
+ test('should redirect to login when accessing protected route without auth', async ({ browser }) => {
277
+ const context = await browser.newContext();
278
+ const page = await context.newPage();
279
+
280
+ await page.goto('/dashboard');
281
+
282
+ await expect(page).toHaveURL(/\/login/);
283
+ await context.close();
284
+ });
285
+
286
+ test('should redirect back to intended page after login', async ({ browser }) => {
287
+ const context = await browser.newContext();
288
+ const page = await context.newPage();
289
+
290
+ await page.goto('/users');
291
+ await expect(page).toHaveURL(/\/login/);
292
+
293
+ await page.getByLabel('Email').fill('admin@example.com');
294
+ await page.getByLabel('Password').fill('SecureP@ss123');
295
+ await page.getByRole('button', { name: /login/i }).click();
296
+
297
+ await expect(page).toHaveURL(/\/users/);
298
+ await context.close();
299
+ });
300
+ ```
301
+
302
+ ### 404 Page
303
+
304
+ ```typescript
305
+ test('should display 404 page for unknown routes', async ({ page }) => {
306
+ await page.goto('/nonexistent-route');
307
+ await expect(page.getByText(/404|not found|page.*exist/i)).toBeVisible();
308
+ });
309
+ ```
310
+
311
+ ## Modals and Dialogs
312
+
313
+ ### Open, Verify Content, Close
314
+
315
+ ```typescript
316
+ test('should open delete confirmation dialog', async ({ page }) => {
317
+ const listPage = new UserListPage(page);
318
+ await listPage.goto();
319
+
320
+ await listPage.clickDeleteRow(0);
321
+
322
+ const dialog = new ConfirmDialogComponent(page);
323
+ await expect(dialog.dialog).toBeVisible();
324
+ await expect(dialog.title).toContainText(/delete|confirm/i);
325
+ await expect(dialog.message).toContainText(/sure|permanently/i);
326
+ });
327
+
328
+ test('should close dialog on cancel', async ({ page }) => {
329
+ const listPage = new UserListPage(page);
330
+ await listPage.goto();
331
+
332
+ await listPage.clickDeleteRow(0);
333
+ const dialog = new ConfirmDialogComponent(page);
334
+ await dialog.cancel();
335
+
336
+ await expect(dialog.dialog).not.toBeVisible();
337
+ // Row should still be present
338
+ await expect(listPage.tableRows).toHaveCount(await listPage.getRowCount());
339
+ });
340
+
341
+ test('should delete item on confirm', async ({ page }) => {
342
+ const listPage = new UserListPage(page);
343
+ await listPage.goto();
344
+ const initialCount = await listPage.getRowCount();
345
+
346
+ await listPage.clickDeleteRow(0);
347
+ const dialog = new ConfirmDialogComponent(page);
348
+ await dialog.confirm();
349
+
350
+ await expect(dialog.dialog).not.toBeVisible();
351
+ await expect(page.getByRole('alert')).toContainText(/deleted|removed|success/i);
352
+ });
353
+ ```
354
+
355
+ ### Close via Escape Key
356
+
357
+ ```typescript
358
+ test('should close dialog with Escape key', async ({ page }) => {
359
+ const listPage = new UserListPage(page);
360
+ await listPage.goto();
361
+
362
+ await listPage.clickDeleteRow(0);
363
+ const dialog = new ConfirmDialogComponent(page);
364
+ await expect(dialog.dialog).toBeVisible();
365
+
366
+ await dialog.closeWithEscape();
367
+
368
+ await expect(dialog.dialog).not.toBeVisible();
369
+ });
370
+ ```
371
+
372
+ ### Close via Overlay Click
373
+
374
+ ```typescript
375
+ test('should close dialog on backdrop click', async ({ page }) => {
376
+ const listPage = new UserListPage(page);
377
+ await listPage.goto();
378
+
379
+ await listPage.clickDeleteRow(0);
380
+ const dialog = new ConfirmDialogComponent(page);
381
+ await expect(dialog.dialog).toBeVisible();
382
+
383
+ await dialog.closeWithOverlayClick();
384
+
385
+ await expect(dialog.dialog).not.toBeVisible();
386
+ });
387
+ ```
388
+
389
+ ## Dropdowns and Select
390
+
391
+ ### Open and Select Option
392
+
393
+ ```typescript
394
+ test('should select role from dropdown', async ({ page }) => {
395
+ const formPage = new UserFormPage(page);
396
+ await formPage.gotoCreate();
397
+
398
+ await formPage.selectRole('Admin');
399
+
400
+ // Verify selection (Angular Material)
401
+ await expect(formPage.roleSelect).toContainText('Admin');
402
+ });
403
+ ```
404
+
405
+ ### Multi-Select
406
+
407
+ ```typescript
408
+ test('should select multiple tags', async ({ page }) => {
409
+ await page.goto('/products/new');
410
+
411
+ const tagSelect = page.getByLabel('Tags');
412
+ await tagSelect.click();
413
+ await page.getByRole('option', { name: 'Electronics' }).click();
414
+ await page.getByRole('option', { name: 'Featured' }).click();
415
+ await page.keyboard.press('Escape');
416
+
417
+ await expect(page.getByText('Electronics')).toBeVisible();
418
+ await expect(page.getByText('Featured')).toBeVisible();
419
+ });
420
+ ```
421
+
422
+ ### Searchable Select / Autocomplete
423
+
424
+ ```typescript
425
+ test('should filter and select from autocomplete', async ({ page }) => {
426
+ await page.goto('/users/new');
427
+
428
+ const cityInput = page.getByLabel('City');
429
+ await cityInput.fill('New');
430
+
431
+ const options = page.getByRole('option');
432
+ await expect(options).toHaveCount(3); // New York, New Delhi, New Orleans
433
+ await page.getByRole('option', { name: 'New York' }).click();
434
+
435
+ await expect(cityInput).toHaveValue('New York');
436
+ });
437
+ ```
438
+
439
+ ## Tables
440
+
441
+ ### Sort Column
442
+
443
+ ```typescript
444
+ test('should sort table by name column', async ({ page }) => {
445
+ const listPage = new UserListPage(page);
446
+ await listPage.goto();
447
+
448
+ await listPage.sortByColumn('Name');
449
+
450
+ const firstCell = listPage.cellInRow(0, 'name');
451
+ const firstName = await firstCell.textContent();
452
+
453
+ await listPage.sortByColumn('Name'); // Sort descending
454
+
455
+ const firstCellDesc = listPage.cellInRow(0, 'name');
456
+ const firstNameDesc = await firstCellDesc.textContent();
457
+
458
+ expect(firstName).not.toEqual(firstNameDesc);
459
+ });
460
+ ```
461
+
462
+ ### Pagination
463
+
464
+ ```typescript
465
+ test('should navigate between pages', async ({ page }) => {
466
+ const listPage = new UserListPage(page);
467
+ await listPage.goto();
468
+
469
+ const firstPageLabel = await listPage.paginatorLabel.textContent();
470
+ await listPage.goToNextPage();
471
+
472
+ const secondPageLabel = await listPage.paginatorLabel.textContent();
473
+ expect(firstPageLabel).not.toEqual(secondPageLabel);
474
+
475
+ await listPage.goToPreviousPage();
476
+ const backToFirstLabel = await listPage.paginatorLabel.textContent();
477
+ expect(backToFirstLabel).toEqual(firstPageLabel);
478
+ });
479
+
480
+ test('should change page size', async ({ page }) => {
481
+ const listPage = new UserListPage(page);
482
+ await listPage.goto();
483
+
484
+ await listPage.changePageSize('50');
485
+ const rowCount = await listPage.getRowCount();
486
+ expect(rowCount).toBeLessThanOrEqual(50);
487
+ });
488
+ ```
489
+
490
+ ### Empty State
491
+
492
+ ```typescript
493
+ test('should display empty state when no results', async ({ page }) => {
494
+ const listPage = new UserListPage(page);
495
+ await listPage.goto();
496
+
497
+ await listPage.search('xyznonexistentzyx');
498
+
499
+ await expect(listPage.emptyState).toBeVisible();
500
+ await expect(listPage.tableRows).toHaveCount(0);
501
+ });
502
+ ```
503
+
504
+ ### Loading State
505
+
506
+ ```typescript
507
+ test('should show loading indicator during data fetch', async ({ page }) => {
508
+ const listPage = new UserListPage(page);
509
+
510
+ // Slow down API response to catch loading state
511
+ await page.route('**/api/users*', async route => {
512
+ await new Promise(resolve => setTimeout(resolve, 1000));
513
+ await route.continue();
514
+ });
515
+
516
+ await listPage.goto();
517
+ await expect(listPage.loadingSpinner).toBeVisible();
518
+ await expect(listPage.loadingSpinner).not.toBeVisible({ timeout: 10000 });
519
+ await expect(listPage.tableRows.first()).toBeVisible();
520
+ });
521
+ ```
522
+
523
+ ### Row Click
524
+
525
+ ```typescript
526
+ test('should navigate to detail on row click', async ({ page }) => {
527
+ const listPage = new UserListPage(page);
528
+ await listPage.goto();
529
+
530
+ await listPage.rowByIndex(0).click();
531
+
532
+ await expect(page).toHaveURL(/\/users\/[^/]+$/);
533
+ });
534
+ ```
535
+
536
+ ## Responsive
537
+
538
+ ### Mobile Layout
539
+
540
+ ```typescript
541
+ import { test, expect, devices } from '@playwright/test';
542
+
543
+ test.describe('Mobile Layout', () => {
544
+ test.use({ ...devices['iPhone 13'] });
545
+
546
+ test('should show hamburger menu on mobile', async ({ page }) => {
547
+ await page.goto('/dashboard');
548
+
549
+ const nav = new NavigationComponent(page);
550
+ await expect(nav.hamburgerMenu).toBeVisible();
551
+ await expect(nav.sidebar).not.toBeVisible();
552
+ });
553
+
554
+ test('should open navigation drawer on hamburger click', async ({ page }) => {
555
+ await page.goto('/dashboard');
556
+
557
+ const nav = new NavigationComponent(page);
558
+ await nav.toggleSidebar();
559
+ await expect(nav.sidebar).toBeVisible();
560
+ });
561
+
562
+ test('should close drawer after navigation', async ({ page }) => {
563
+ await page.goto('/dashboard');
564
+
565
+ const nav = new NavigationComponent(page);
566
+ await nav.toggleSidebar();
567
+ await nav.navigateTo('Users');
568
+
569
+ await expect(page).toHaveURL(/\/users/);
570
+ await expect(nav.sidebar).not.toBeVisible();
571
+ });
572
+
573
+ test('should stack form fields vertically on mobile', async ({ page }) => {
574
+ await page.goto('/users/new');
575
+
576
+ const nameInput = page.getByLabel('Name');
577
+ const emailInput = page.getByLabel('Email');
578
+
579
+ const nameBox = await nameInput.boundingBox();
580
+ const emailBox = await emailInput.boundingBox();
581
+
582
+ // Fields should be stacked (email below name)
583
+ expect(emailBox!.y).toBeGreaterThan(nameBox!.y);
584
+ });
585
+ });
586
+ ```
587
+
588
+ ### Tablet Layout
589
+
590
+ ```typescript
591
+ test.describe('Tablet Layout', () => {
592
+ test.use({ viewport: { width: 768, height: 1024 } });
593
+
594
+ test('should show collapsed sidebar on tablet', async ({ page }) => {
595
+ await page.goto('/dashboard');
596
+
597
+ const nav = new NavigationComponent(page);
598
+ const sidebarBox = await nav.sidebar.boundingBox();
599
+
600
+ // Collapsed sidebar should be narrow (icons only)
601
+ expect(sidebarBox!.width).toBeLessThan(100);
602
+ });
603
+ });
604
+ ```
605
+
606
+ ## Accessibility
607
+
608
+ ### Keyboard Navigation
609
+
610
+ ```typescript
611
+ test('should navigate form fields with Tab', async ({ page }) => {
612
+ await page.goto('/users/new');
613
+
614
+ await page.keyboard.press('Tab');
615
+ await expect(page.getByLabel('Name')).toBeFocused();
616
+
617
+ await page.keyboard.press('Tab');
618
+ await expect(page.getByLabel('Email')).toBeFocused();
619
+
620
+ await page.keyboard.press('Tab');
621
+ await expect(page.getByLabel('Phone')).toBeFocused();
622
+ });
623
+
624
+ test('should submit form with Enter key', async ({ page }) => {
625
+ const loginPage = new LoginPage(page);
626
+ await loginPage.goto();
627
+
628
+ await loginPage.fillEmail('admin@example.com');
629
+ await loginPage.fillPassword('SecureP@ss123');
630
+ await page.keyboard.press('Enter');
631
+
632
+ await expect(page).toHaveURL(/\/dashboard/);
633
+ });
634
+ ```
635
+
636
+ ### ARIA Labels
637
+
638
+ ```typescript
639
+ test('should have proper ARIA labels on interactive elements', async ({ page }) => {
640
+ await page.goto('/users');
641
+
642
+ // Buttons should have accessible names
643
+ const addButton = page.getByRole('button', { name: /add|create/i });
644
+ await expect(addButton).toBeVisible();
645
+
646
+ // Table should have proper role
647
+ await expect(page.getByRole('table')).toBeVisible();
648
+
649
+ // Column headers should have sort indicators
650
+ const sortableHeader = page.getByRole('columnheader', { name: /name/i });
651
+ await expect(sortableHeader).toHaveAttribute('aria-sort', /(ascending|descending|none)/);
652
+ });
653
+ ```
654
+
655
+ ### Axe Accessibility Audit
656
+
657
+ ```typescript
658
+ import AxeBuilder from '@axe-core/playwright';
659
+
660
+ test('should have no accessibility violations on user list page', async ({ page }) => {
661
+ await page.goto('/users');
662
+
663
+ const results = await new AxeBuilder({ page })
664
+ .withTags(['wcag2a', 'wcag2aa'])
665
+ .analyze();
666
+
667
+ expect(results.violations).toEqual([]);
668
+ });
669
+
670
+ test('should have no accessibility violations on form page', async ({ page }) => {
671
+ await page.goto('/users/new');
672
+
673
+ const results = await new AxeBuilder({ page })
674
+ .withTags(['wcag2a', 'wcag2aa'])
675
+ .exclude('.third-party-widget') // Exclude elements you can't control
676
+ .analyze();
677
+
678
+ expect(results.violations).toEqual([]);
679
+ });
680
+ ```
681
+
682
+ ### Focus Management
683
+
684
+ ```typescript
685
+ test('should return focus to trigger element after dialog closes', async ({ page }) => {
686
+ await page.goto('/users');
687
+
688
+ const deleteButton = page.getByRole('button', { name: /delete/i }).first();
689
+ await deleteButton.click();
690
+
691
+ const dialog = new ConfirmDialogComponent(page);
692
+ await dialog.cancel();
693
+
694
+ await expect(deleteButton).toBeFocused();
695
+ });
696
+
697
+ test('should trap focus inside open modal', async ({ page }) => {
698
+ await page.goto('/users');
699
+
700
+ await page.getByRole('button', { name: /delete/i }).first().click();
701
+ const dialog = page.getByRole('dialog');
702
+ await expect(dialog).toBeVisible();
703
+
704
+ // Tab through all focusable elements in dialog
705
+ await page.keyboard.press('Tab');
706
+ const focused1 = await page.evaluate(() => document.activeElement?.closest('[role="dialog"]'));
707
+ expect(focused1).not.toBeNull();
708
+
709
+ await page.keyboard.press('Tab');
710
+ const focused2 = await page.evaluate(() => document.activeElement?.closest('[role="dialog"]'));
711
+ expect(focused2).not.toBeNull();
712
+ });
713
+ ```
714
+
715
+ ## Auth Flows
716
+
717
+ ### Login
718
+
719
+ ```typescript
720
+ test.describe('Login Flow', () => {
721
+ test('should login and redirect to dashboard', async ({ page }) => {
722
+ const loginPage = new LoginPage(page);
723
+ await loginPage.goto();
724
+
725
+ await loginPage.login('admin@example.com', 'SecureP@ss123');
726
+
727
+ await expect(page).toHaveURL(/\/dashboard/);
728
+ await expect(page.getByText(/welcome|dashboard/i)).toBeVisible();
729
+ });
730
+
731
+ test('should show error for invalid credentials', async ({ page }) => {
732
+ const loginPage = new LoginPage(page);
733
+ await loginPage.goto();
734
+
735
+ await loginPage.login('wrong@example.com', 'WrongPass');
736
+
737
+ await expect(page).toHaveURL(/\/login/);
738
+ await expect(loginPage.generalError).toBeVisible();
739
+ });
740
+ });
741
+ ```
742
+
743
+ ### Logout
744
+
745
+ ```typescript
746
+ test('should logout and redirect to login', async ({ page }) => {
747
+ await page.goto('/dashboard');
748
+
749
+ const nav = new NavigationComponent(page);
750
+ await nav.logout();
751
+
752
+ await expect(page).toHaveURL(/\/login/);
753
+ });
754
+
755
+ test('should not access protected routes after logout', async ({ page }) => {
756
+ await page.goto('/dashboard');
757
+
758
+ const nav = new NavigationComponent(page);
759
+ await nav.logout();
760
+
761
+ await page.goto('/users');
762
+ await expect(page).toHaveURL(/\/login/);
763
+ });
764
+ ```
765
+
766
+ ### Session Expiry
767
+
768
+ ```typescript
769
+ test('should handle expired session gracefully', async ({ page }) => {
770
+ await page.goto('/dashboard');
771
+
772
+ // Clear auth tokens to simulate session expiry
773
+ await page.evaluate(() => {
774
+ localStorage.removeItem('authToken');
775
+ sessionStorage.removeItem('authToken');
776
+ });
777
+
778
+ // Next API call should trigger redirect
779
+ await page.goto('/users');
780
+ await expect(page).toHaveURL(/\/login/);
781
+ });
782
+ ```
783
+
784
+ ### Role-Based Visibility
785
+
786
+ ```typescript
787
+ test('should hide admin-only elements for regular users', async ({ page }) => {
788
+ // Login as regular user
789
+ await page.goto('/login');
790
+ await page.getByLabel('Email').fill('user@example.com');
791
+ await page.getByLabel('Password').fill('UserP@ss123');
792
+ await page.getByRole('button', { name: /login/i }).click();
793
+
794
+ await page.goto('/users');
795
+
796
+ // Delete button should not be visible for regular users
797
+ await expect(page.getByRole('button', { name: /delete/i })).not.toBeVisible();
798
+
799
+ // Settings link should not be in nav
800
+ const nav = new NavigationComponent(page);
801
+ await expect(nav.navLink('Settings')).not.toBeVisible();
802
+ });
803
+ ```
804
+
805
+ ## Error Handling
806
+
807
+ ### API Failure
808
+
809
+ ```typescript
810
+ test('should display error message when API fails', async ({ page }) => {
811
+ // Mock API failure
812
+ await page.route('**/api/users*', route => {
813
+ route.fulfill({
814
+ status: 500,
815
+ contentType: 'application/json',
816
+ body: JSON.stringify({ message: 'Internal Server Error' }),
817
+ });
818
+ });
819
+
820
+ await page.goto('/users');
821
+
822
+ await expect(page.getByText(/error|failed|something went wrong/i)).toBeVisible();
823
+ });
824
+
825
+ test('should offer retry option on API failure', async ({ page }) => {
826
+ let callCount = 0;
827
+
828
+ await page.route('**/api/users*', route => {
829
+ callCount++;
830
+ if (callCount === 1) {
831
+ route.fulfill({ status: 500, body: '{}' });
832
+ } else {
833
+ route.continue();
834
+ }
835
+ });
836
+
837
+ await page.goto('/users');
838
+ await expect(page.getByText(/error|failed/i)).toBeVisible();
839
+
840
+ await page.getByRole('button', { name: /retry|try again/i }).click();
841
+ await expect(page.locator('mat-table, table')).toBeVisible();
842
+ });
843
+ ```
844
+
845
+ ### Network Offline
846
+
847
+ ```typescript
848
+ test('should show offline indicator when network is down', async ({ page, context }) => {
849
+ await page.goto('/dashboard');
850
+
851
+ await context.setOffline(true);
852
+
853
+ // Trigger a navigation or action that needs network
854
+ await page.getByRole('link', { name: /users/i }).click();
855
+
856
+ await expect(page.getByText(/offline|network|connection/i)).toBeVisible();
857
+
858
+ await context.setOffline(false);
859
+ });
860
+ ```
861
+
862
+ ### Form Submit API Error
863
+
864
+ ```typescript
865
+ test('should display server error on form submit failure', async ({ page }) => {
866
+ await page.route('**/api/users', route => {
867
+ if (route.request().method() === 'POST') {
868
+ route.fulfill({
869
+ status: 422,
870
+ contentType: 'application/json',
871
+ body: JSON.stringify({
872
+ message: 'A user with this email already exists',
873
+ }),
874
+ });
875
+ } else {
876
+ route.continue();
877
+ }
878
+ });
879
+
880
+ const formPage = new UserFormPage(page);
881
+ await formPage.gotoCreate();
882
+
883
+ await formPage.fillAndSubmit({
884
+ name: 'Jane Smith',
885
+ email: 'existing@example.com',
886
+ });
887
+
888
+ await expect(page.getByRole('alert')).toContainText(/already exists|duplicate/i);
889
+ // Should stay on form page, not navigate away
890
+ await expect(page).toHaveURL(/\/users\/new/);
891
+ });
892
+ ```
893
+
894
+ ## File Upload
895
+
896
+ ```typescript
897
+ test('should upload avatar image', async ({ page }) => {
898
+ const formPage = new UserFormPage(page);
899
+ await formPage.gotoEdit('123');
900
+
901
+ const fileChooserPromise = page.waitForEvent('filechooser');
902
+ await page.getByRole('button', { name: /upload|choose.*file/i }).click();
903
+ const fileChooser = await fileChooserPromise;
904
+ await fileChooser.setFiles('e2e/fixtures/avatar.png');
905
+
906
+ await expect(page.getByRole('img', { name: /avatar|preview/i })).toBeVisible();
907
+ });
908
+
909
+ test('should reject files exceeding size limit', async ({ page }) => {
910
+ const formPage = new UserFormPage(page);
911
+ await formPage.gotoEdit('123');
912
+
913
+ // Use setInputFiles directly for input[type="file"]
914
+ await formPage.avatarFileInput.setInputFiles('e2e/fixtures/large-file.png');
915
+
916
+ await expect(page.getByText(/too large|max.*size|exceeds/i)).toBeVisible();
917
+ });
918
+
919
+ test('should reject unsupported file types', async ({ page }) => {
920
+ const formPage = new UserFormPage(page);
921
+ await formPage.gotoEdit('123');
922
+
923
+ await formPage.avatarFileInput.setInputFiles('e2e/fixtures/document.exe');
924
+
925
+ await expect(page.getByText(/unsupported|invalid.*type|allowed/i)).toBeVisible();
926
+ });
927
+ ```
928
+
929
+ ## Tabs
930
+
931
+ ```typescript
932
+ test('should switch between tabs', async ({ page }) => {
933
+ await page.goto('/users/123');
934
+
935
+ await page.getByRole('tab', { name: /profile/i }).click();
936
+ await expect(page.getByRole('tabpanel')).toContainText(/name|email/i);
937
+
938
+ await page.getByRole('tab', { name: /activity/i }).click();
939
+ await expect(page.getByRole('tabpanel')).toContainText(/log|history/i);
940
+
941
+ await page.getByRole('tab', { name: /settings/i }).click();
942
+ await expect(page.getByRole('tabpanel')).toContainText(/preferences|notification/i);
943
+ });
944
+
945
+ test('should navigate tabs with keyboard', async ({ page }) => {
946
+ await page.goto('/users/123');
947
+
948
+ await page.getByRole('tab', { name: /profile/i }).focus();
949
+ await page.keyboard.press('ArrowRight');
950
+ await expect(page.getByRole('tab', { name: /activity/i })).toBeFocused();
951
+
952
+ await page.keyboard.press('ArrowRight');
953
+ await expect(page.getByRole('tab', { name: /settings/i })).toBeFocused();
954
+ });
955
+ ```
956
+
957
+ ## Notifications / Toasts
958
+
959
+ ```typescript
960
+ test('should show success toast after save', async ({ page }) => {
961
+ const formPage = new UserFormPage(page);
962
+ await formPage.gotoCreate();
963
+
964
+ await formPage.fillAndSubmit({
965
+ name: 'Jane Smith',
966
+ email: 'jane@example.com',
967
+ });
968
+
969
+ const toast = page.getByRole('alert');
970
+ await expect(toast).toBeVisible();
971
+ await expect(toast).toContainText(/success|created|saved/i);
972
+ });
973
+
974
+ test('should auto-dismiss toast after timeout', async ({ page }) => {
975
+ const formPage = new UserFormPage(page);
976
+ await formPage.gotoCreate();
977
+
978
+ await formPage.fillAndSubmit({
979
+ name: 'Jane Smith',
980
+ email: 'jane@example.com',
981
+ });
982
+
983
+ const toast = page.getByRole('alert');
984
+ await expect(toast).toBeVisible();
985
+ await expect(toast).not.toBeVisible({ timeout: 10000 });
986
+ });
987
+ ```