create-shhs 1.0.0 → 1.1.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.
@@ -0,0 +1,565 @@
1
+ # Skill: Playwright End-to-End Testing
2
+
3
+ **ID:** `playwright`
4
+ **Version:** 1.0.0
5
+ **Status:** MANDATORY for critical paths
6
+ **Authority:** Constitution Article III
7
+
8
+ ---
9
+
10
+ ## When to Apply
11
+
12
+ This skill is MANDATORY for:
13
+
14
+ - Authentication flows
15
+ - Payment/transaction flows
16
+ - Data mutation operations
17
+ - Primary user journeys (signup → activation → core action)
18
+
19
+ ---
20
+
21
+ ## What Is E2E Testing?
22
+
23
+ End-to-End testing validates the **entire user flow** from UI to database and back.
24
+
25
+ Unlike unit tests (which test functions in isolation), E2E tests verify:
26
+ - UI renders correctly
27
+ - User interactions work (clicks, typing, navigation)
28
+ - Backend processes data correctly
29
+ - Database persists changes
30
+ - Edge cases and error states
31
+
32
+ ---
33
+
34
+ ## Setup
35
+
36
+ ### Install Playwright
37
+
38
+ ```bash
39
+ npm install -D @playwright/test
40
+ npx playwright install
41
+ ```
42
+
43
+ ### Directory Structure
44
+
45
+ ```
46
+ tests/
47
+ ├── e2e/
48
+ │ ├── auth/
49
+ │ │ ├── login.spec.ts
50
+ │ │ └── signup.spec.ts
51
+ │ ├── billing/
52
+ │ │ ├── checkout.spec.ts
53
+ │ │ └── subscription.spec.ts
54
+ │ └── fixtures/
55
+ │ ├── test-users.json
56
+ │ └── test-data.ts
57
+ └── playwright.config.ts
58
+ ```
59
+
60
+ ### Configuration
61
+
62
+ ```typescript
63
+ // playwright.config.ts
64
+ import { defineConfig, devices } from '@playwright/test';
65
+
66
+ export default defineConfig({
67
+ testDir: './tests/e2e',
68
+ fullyParallel: true,
69
+ forbidOnly: !!process.env.CI,
70
+ retries: process.env.CI ? 2 : 0,
71
+ workers: process.env.CI ? 1 : undefined,
72
+ reporter: 'html',
73
+ use: {
74
+ baseURL: 'http://localhost:3000',
75
+ trace: 'on-first-retry',
76
+ },
77
+ projects: [
78
+ {
79
+ name: 'chromium',
80
+ use: { ...devices['Desktop Chrome'] },
81
+ },
82
+ ],
83
+ webServer: {
84
+ command: 'npm run dev',
85
+ url: 'http://localhost:3000',
86
+ reuseExistingServer: !process.env.CI,
87
+ },
88
+ });
89
+ ```
90
+
91
+ ---
92
+
93
+ ## Writing E2E Tests
94
+
95
+ ### Anatomy of a Playwright Test
96
+
97
+ ```typescript
98
+ import { test, expect } from '@playwright/test';
99
+
100
+ test.describe('User Authentication', () => {
101
+
102
+ test('should log in with valid credentials', async ({ page }) => {
103
+ // ARRANGE: Navigate to login page
104
+ await page.goto('/login');
105
+
106
+ // ACT: Fill in form and submit
107
+ await page.fill('input[name="email"]', 'user@example.com');
108
+ await page.fill('input[name="password"]', 'SecurePass123');
109
+ await page.click('button[type="submit"]');
110
+
111
+ // ASSERT: Verify successful login
112
+ await expect(page).toHaveURL('/dashboard');
113
+ await expect(page.locator('h1')).toContainText('Welcome');
114
+ });
115
+
116
+ });
117
+ ```
118
+
119
+ ### Arrange-Act-Assert Pattern
120
+
121
+ | Phase | Purpose | Example |
122
+ |-------|---------|---------|
123
+ | **Arrange** | Set up initial state | Navigate to page, seed database |
124
+ | **Act** | Perform user action | Click button, fill form, submit |
125
+ | **Assert** | Verify outcome | Check URL, verify text, validate data |
126
+
127
+ ---
128
+
129
+ ## Best Practices
130
+
131
+ ### 1. Use Data-Testid for Stable Selectors
132
+
133
+ ❌ **Fragile:**
134
+ ```typescript
135
+ await page.click('button:nth-child(3)'); // Breaks if button order changes
136
+ await page.click('.btn-primary'); // Breaks if CSS class changes
137
+ ```
138
+
139
+ ✅ **Robust:**
140
+ ```typescript
141
+ await page.click('[data-testid="submit-button"]');
142
+ ```
143
+
144
+ **Implementation:**
145
+ ```html
146
+ <button data-testid="submit-button" class="btn-primary">
147
+ Submit
148
+ </button>
149
+ ```
150
+
151
+ ### 2. Test User Flows, Not Implementation
152
+
153
+ ❌ **Wrong:**
154
+ ```typescript
155
+ test('should call POST /api/users with correct payload', async ({ page }) => {
156
+ // Testing API implementation, not user experience
157
+ });
158
+ ```
159
+
160
+ ✅ **Correct:**
161
+ ```typescript
162
+ test('should display success message after user signup', async ({ page }) => {
163
+ await page.goto('/signup');
164
+ await page.fill('[data-testid="email"]', 'new@example.com');
165
+ await page.fill('[data-testid="password"]', 'Password123');
166
+ await page.click('[data-testid="signup-button"]');
167
+
168
+ await expect(page.locator('[data-testid="success-message"]'))
169
+ .toContainText('Account created successfully');
170
+ });
171
+ ```
172
+
173
+ ### 3. Use Fixtures for Reusable Setup
174
+
175
+ ```typescript
176
+ // tests/e2e/fixtures/auth.fixture.ts
177
+ import { test as base } from '@playwright/test';
178
+
179
+ type AuthFixtures = {
180
+ authenticatedPage: Page;
181
+ };
182
+
183
+ export const test = base.extend<AuthFixtures>({
184
+ authenticatedPage: async ({ page }, use) => {
185
+ // Log in before each test
186
+ await page.goto('/login');
187
+ await page.fill('[data-testid="email"]', 'test@example.com');
188
+ await page.fill('[data-testid="password"]', 'password');
189
+ await page.click('[data-testid="login-button"]');
190
+ await page.waitForURL('/dashboard');
191
+
192
+ await use(page);
193
+ },
194
+ });
195
+ ```
196
+
197
+ **Usage:**
198
+ ```typescript
199
+ import { test } from './fixtures/auth.fixture';
200
+
201
+ test('should access protected dashboard', async ({ authenticatedPage }) => {
202
+ await expect(authenticatedPage).toHaveURL('/dashboard');
203
+ // No need to log in manually - fixture handles it
204
+ });
205
+ ```
206
+
207
+ ### 4. Test Edge Cases and Error States
208
+
209
+ Don't just test happy paths.
210
+
211
+ ```typescript
212
+ test.describe('Login Error Handling', () => {
213
+
214
+ test('should show error for invalid credentials', async ({ page }) => {
215
+ await page.goto('/login');
216
+ await page.fill('[data-testid="email"]', 'wrong@example.com');
217
+ await page.fill('[data-testid="password"]', 'wrongpassword');
218
+ await page.click('[data-testid="login-button"]');
219
+
220
+ await expect(page.locator('[data-testid="error-message"]'))
221
+ .toContainText('Invalid email or password');
222
+ });
223
+
224
+ test('should disable submit while request is pending', async ({ page }) => {
225
+ await page.goto('/login');
226
+ await page.fill('[data-testid="email"]', 'user@example.com');
227
+ await page.fill('[data-testid="password"]', 'password');
228
+
229
+ const submitButton = page.locator('[data-testid="login-button"]');
230
+ await submitButton.click();
231
+
232
+ // Button should be disabled during request
233
+ await expect(submitButton).toBeDisabled();
234
+ });
235
+
236
+ });
237
+ ```
238
+
239
+ ### 5. Ensure Idempotency
240
+
241
+ Tests must be repeatable without manual cleanup.
242
+
243
+ ❌ **Non-idempotent:**
244
+ ```typescript
245
+ test('should create new user', async ({ page }) => {
246
+ await page.goto('/signup');
247
+ await page.fill('[data-testid="email"]', 'test@example.com'); // Fails on second run!
248
+ // ...
249
+ });
250
+ ```
251
+
252
+ ✅ **Idempotent:**
253
+ ```typescript
254
+ import { randomUUID } from 'crypto';
255
+
256
+ test('should create new user', async ({ page }) => {
257
+ const uniqueEmail = `test-${randomUUID()}@example.com`;
258
+ await page.goto('/signup');
259
+ await page.fill('[data-testid="email"]', uniqueEmail);
260
+ // ...
261
+ });
262
+ ```
263
+
264
+ **Or use database cleanup:**
265
+ ```typescript
266
+ test.beforeEach(async () => {
267
+ // Clean up test data before each test
268
+ await database.query('DELETE FROM users WHERE email LIKE "test-%"');
269
+ });
270
+ ```
271
+
272
+ ---
273
+
274
+ ## Critical Flow Examples
275
+
276
+ ### Example 1: Authentication Flow
277
+
278
+ ```typescript
279
+ import { test, expect } from '@playwright/test';
280
+
281
+ test.describe('Complete Authentication Flow', () => {
282
+
283
+ test('should sign up, verify email, and log in', async ({ page, context }) => {
284
+ const email = `user-${Date.now()}@example.com`;
285
+
286
+ // Step 1: Sign up
287
+ await page.goto('/signup');
288
+ await page.fill('[data-testid="email"]', email);
289
+ await page.fill('[data-testid="password"]', 'SecurePass123');
290
+ await page.click('[data-testid="signup-button"]');
291
+
292
+ // Step 2: Verify redirect to email verification page
293
+ await expect(page).toHaveURL('/verify-email');
294
+ await expect(page.locator('[data-testid="verification-message"]'))
295
+ .toContainText('Check your email');
296
+
297
+ // Step 3: Simulate email verification (use test API or direct DB update)
298
+ await page.goto(`/api/test/verify-email?email=${email}`);
299
+
300
+ // Step 4: Log in
301
+ await page.goto('/login');
302
+ await page.fill('[data-testid="email"]', email);
303
+ await page.fill('[data-testid="password"]', 'SecurePass123');
304
+ await page.click('[data-testid="login-button"]');
305
+
306
+ // Step 5: Verify successful login
307
+ await expect(page).toHaveURL('/dashboard');
308
+ await expect(page.locator('[data-testid="user-email"]'))
309
+ .toContainText(email);
310
+ });
311
+
312
+ });
313
+ ```
314
+
315
+ ### Example 2: Payment Flow
316
+
317
+ ```typescript
318
+ test.describe('Checkout and Payment', () => {
319
+
320
+ test('should complete purchase with credit card', async ({ page }) => {
321
+ // Arrange: Log in and add item to cart
322
+ await page.goto('/login');
323
+ await page.fill('[data-testid="email"]', 'buyer@example.com');
324
+ await page.fill('[data-testid="password"]', 'password');
325
+ await page.click('[data-testid="login-button"]');
326
+
327
+ await page.goto('/products/premium-plan');
328
+ await page.click('[data-testid="add-to-cart"]');
329
+
330
+ // Act: Proceed to checkout
331
+ await page.goto('/checkout');
332
+ await page.fill('[data-testid="card-number"]', '4242424242424242'); // Test card
333
+ await page.fill('[data-testid="card-expiry"]', '12/25');
334
+ await page.fill('[data-testid="card-cvc"]', '123');
335
+ await page.click('[data-testid="complete-purchase"]');
336
+
337
+ // Assert: Verify successful payment
338
+ await expect(page).toHaveURL(/\/success/);
339
+ await expect(page.locator('[data-testid="order-confirmation"]'))
340
+ .toBeVisible();
341
+
342
+ // Assert: Verify order in database
343
+ const response = await page.request.get('/api/orders/latest');
344
+ const order = await response.json();
345
+ expect(order.status).toBe('completed');
346
+ expect(order.amount).toBe(9900); // $99.00
347
+ });
348
+
349
+ });
350
+ ```
351
+
352
+ ---
353
+
354
+ ## Assertions
355
+
356
+ ### Common Assertions
357
+
358
+ | Assertion | Purpose |
359
+ |-----------|---------|
360
+ | `await expect(page).toHaveURL('/dashboard')` | Check URL |
361
+ | `await expect(locator).toContainText('Welcome')` | Check text content |
362
+ | `await expect(locator).toBeVisible()` | Check visibility |
363
+ | `await expect(locator).toBeDisabled()` | Check disabled state |
364
+ | `await expect(locator).toHaveCount(3)` | Check number of elements |
365
+ | `await expect(locator).toHaveAttribute('href', '/profile')` | Check attribute |
366
+
367
+ ### API Assertions
368
+
369
+ ```typescript
370
+ test('should create order in database', async ({ page }) => {
371
+ // Perform UI action
372
+ await page.click('[data-testid="submit-order"]');
373
+
374
+ // Assert via API
375
+ const response = await page.request.get('/api/orders/latest');
376
+ expect(response.status()).toBe(200);
377
+
378
+ const order = await response.json();
379
+ expect(order).toMatchObject({
380
+ status: 'pending',
381
+ userId: 'user-123',
382
+ });
383
+ });
384
+ ```
385
+
386
+ ---
387
+
388
+ ## Debugging
389
+
390
+ ### Visual Debugging
391
+
392
+ ```bash
393
+ # Run with headed browser (see what's happening)
394
+ npx playwright test --headed
395
+
396
+ # Run in debug mode (pause and step through)
397
+ npx playwright test --debug
398
+ ```
399
+
400
+ ### Screenshots on Failure
401
+
402
+ ```typescript
403
+ test('should display dashboard', async ({ page }) => {
404
+ await page.goto('/dashboard');
405
+
406
+ // Automatically capture screenshot on failure
407
+ await expect(page.locator('h1')).toContainText('Dashboard');
408
+ });
409
+ ```
410
+
411
+ Configure in `playwright.config.ts`:
412
+ ```typescript
413
+ use: {
414
+ screenshot: 'only-on-failure',
415
+ video: 'retain-on-failure',
416
+ }
417
+ ```
418
+
419
+ ### Trace Viewer
420
+
421
+ ```bash
422
+ # Generate trace
423
+ npx playwright test --trace on
424
+
425
+ # View trace
426
+ npx playwright show-trace trace.zip
427
+ ```
428
+
429
+ ---
430
+
431
+ ## CI/CD Integration
432
+
433
+ ### GitHub Actions
434
+
435
+ ```yaml
436
+ # .github/workflows/e2e.yml
437
+ name: E2E Tests
438
+
439
+ on: [push, pull_request]
440
+
441
+ jobs:
442
+ test:
443
+ runs-on: ubuntu-latest
444
+ steps:
445
+ - uses: actions/checkout@v3
446
+ - uses: actions/setup-node@v3
447
+ with:
448
+ node-version: 18
449
+
450
+ - name: Install dependencies
451
+ run: npm ci
452
+
453
+ - name: Install Playwright browsers
454
+ run: npx playwright install --with-deps
455
+
456
+ - name: Run E2E tests
457
+ run: npx playwright test
458
+
459
+ - name: Upload test results
460
+ if: always()
461
+ uses: actions/upload-artifact@v3
462
+ with:
463
+ name: playwright-report
464
+ path: playwright-report/
465
+ ```
466
+
467
+ ---
468
+
469
+ ## Validation Criteria
470
+
471
+ Before considering Playwright testing complete:
472
+
473
+ - [ ] All critical flows have E2E tests
474
+ - [ ] Tests pass in CI/CD pipeline
475
+ - [ ] Tests are idempotent (can run repeatedly)
476
+ - [ ] Error states are tested
477
+ - [ ] Tests use stable selectors (data-testid)
478
+ - [ ] No flaky tests (tests pass consistently)
479
+
480
+ ---
481
+
482
+ ## Common Mistakes
483
+
484
+ ### Mistake 1: Testing Too Much in One Test
485
+
486
+ ❌ **Wrong:**
487
+ ```typescript
488
+ test('should do everything', async ({ page }) => {
489
+ // Signup + login + profile update + logout = too much
490
+ });
491
+ ```
492
+
493
+ ✅ **Correct:**
494
+ ```typescript
495
+ test('should sign up', async ({ page }) => { /* ... */ });
496
+ test('should log in', async ({ page }) => { /* ... */ });
497
+ test('should update profile', async ({ page }) => { /* ... */ });
498
+ ```
499
+
500
+ ### Mistake 2: Hardcoding Waits
501
+
502
+ ❌ **Wrong:**
503
+ ```typescript
504
+ await page.click('[data-testid="submit"]');
505
+ await page.waitForTimeout(3000); // Fragile!
506
+ ```
507
+
508
+ ✅ **Correct:**
509
+ ```typescript
510
+ await page.click('[data-testid="submit"]');
511
+ await page.waitForURL('/success'); // Wait for specific condition
512
+ ```
513
+
514
+ ### Mistake 3: Not Cleaning Up Test Data
515
+
516
+ ❌ **Wrong:**
517
+ ```typescript
518
+ test('should create user', async ({ page }) => {
519
+ // Creates user but never deletes it → subsequent runs fail
520
+ });
521
+ ```
522
+
523
+ ✅ **Correct:**
524
+ ```typescript
525
+ test.afterEach(async () => {
526
+ await database.query('DELETE FROM users WHERE email LIKE "test-%"');
527
+ });
528
+ ```
529
+
530
+ ---
531
+
532
+ ## Integration with SHHS
533
+
534
+ ### Pipeline Position
535
+
536
+ E2E tests run during **QA Validator** phase.
537
+
538
+ ```
539
+ Developer implements feature
540
+
541
+ Static Reviewer validates structure
542
+
543
+ QA Validator runs E2E tests ← Playwright runs here
544
+
545
+ Domain Architect approves
546
+ ```
547
+
548
+ ### Enforcement by QA Validator
549
+
550
+ QA Validator MUST verify:
551
+ - All critical paths have Playwright tests
552
+ - All tests pass
553
+ - No flaky tests (tests pass 3 consecutive runs)
554
+
555
+ ---
556
+
557
+ ## References
558
+
559
+ - Constitution Article III (E2E testing mandate)
560
+ - [Playwright Documentation](https://playwright.dev)
561
+ - [Playwright Best Practices](https://playwright.dev/docs/best-practices)
562
+
563
+ ---
564
+
565
+ **END OF PLAYWRIGHT SKILL**