@tekyzinc/gsd-t 2.46.11 → 2.50.10
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/CHANGELOG.md +11 -0
- package/README.md +22 -2
- package/bin/debug-ledger.js +193 -0
- package/bin/gsd-t.js +259 -1
- package/commands/gsd-t-debug.md +26 -1
- package/commands/gsd-t-execute.md +31 -3
- package/commands/gsd-t-help.md +18 -2
- package/commands/gsd-t-integrate.md +16 -0
- package/commands/gsd-t-quick.md +18 -1
- package/commands/gsd-t-test-sync.md +5 -1
- package/commands/gsd-t-verify.md +6 -1
- package/commands/gsd-t-wave.md +26 -0
- package/docs/GSD-T-README.md +83 -1
- package/docs/architecture.md +9 -1
- package/docs/requirements.md +30 -0
- package/package.json +1 -1
- package/templates/CLAUDE-global.md +19 -2
- package/templates/stacks/_security.md +243 -0
- package/templates/stacks/desktop.ini +2 -0
- package/templates/stacks/docker.md +202 -0
- package/templates/stacks/firebase.md +166 -0
- package/templates/stacks/flutter.md +205 -0
- package/templates/stacks/github-actions.md +201 -0
- package/templates/stacks/graphql.md +216 -0
- package/templates/stacks/neo4j.md +218 -0
- package/templates/stacks/nextjs.md +184 -0
- package/templates/stacks/node-api.md +196 -0
- package/templates/stacks/playwright.md +528 -0
- package/templates/stacks/postgresql.md +225 -0
- package/templates/stacks/python.md +243 -0
- package/templates/stacks/react-native.md +216 -0
- package/templates/stacks/react.md +293 -0
- package/templates/stacks/redux.md +193 -0
- package/templates/stacks/rest-api.md +202 -0
- package/templates/stacks/supabase.md +188 -0
- package/templates/stacks/tailwind.md +169 -0
- package/templates/stacks/typescript.md +176 -0
- package/templates/stacks/vite.md +176 -0
- package/templates/stacks/vue.md +189 -0
- package/templates/stacks/zustand.md +203 -0
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
# Playwright Standards
|
|
2
|
+
|
|
3
|
+
These rules are MANDATORY. Violations fail the task. No exceptions.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Functional Tests Only — No Layout Tests
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
MANDATORY:
|
|
11
|
+
├── Every assertion must verify BEHAVIOR — not element existence
|
|
12
|
+
├── A test that passes on an empty HTML page with matching IDs is NOT a test
|
|
13
|
+
├── After every user action, assert the OUTCOME (data changed, content loaded, state updated)
|
|
14
|
+
├── NEVER assert only isVisible, toBeAttached, toBeEnabled without a behavioral follow-up
|
|
15
|
+
└── If a test has no assertion after a click/submit/navigation, it is incomplete
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
**BAD** — layout test (passes even if everything is broken):
|
|
19
|
+
```typescript
|
|
20
|
+
test('user list page', async ({ page }) => {
|
|
21
|
+
await page.goto('/users');
|
|
22
|
+
await expect(page.locator('#user-table')).toBeVisible();
|
|
23
|
+
await expect(page.locator('.user-row')).toHaveCount(5);
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**GOOD** — functional test (fails if the feature is broken):
|
|
28
|
+
```typescript
|
|
29
|
+
test('user list loads and displays real user data', async ({ page }) => {
|
|
30
|
+
await page.goto('/users');
|
|
31
|
+
await expect(page.locator('.user-row').first()).toContainText('jane@example.com');
|
|
32
|
+
await expect(page.locator('[data-testid="user-count"]')).toHaveText('5 users');
|
|
33
|
+
});
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 2. Test Coverage Depth — Permutations and Combinations
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
MANDATORY — every feature MUST be tested across these dimensions:
|
|
42
|
+
|
|
43
|
+
├── HAPPY PATH: Standard successful flow end-to-end
|
|
44
|
+
├── VALIDATION: Every form field with invalid, empty, boundary, and valid input
|
|
45
|
+
├── ERROR STATES: Network failure, server error (500), not found (404), timeout
|
|
46
|
+
├── EMPTY STATES: No data, empty lists, first-time user with zero records
|
|
47
|
+
├── EDGE CASES: Boundary values, special characters, max-length input, Unicode
|
|
48
|
+
├── PERMISSIONS: Unauthorized access, role-based visibility, disabled actions
|
|
49
|
+
├── STATE TRANSITIONS: Every state a record can be in, and transitions between them
|
|
50
|
+
└── CONCURRENT: Actions while loading, double-submit, rapid navigation
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Coverage Matrix — Build One Per Feature
|
|
54
|
+
|
|
55
|
+
For any feature with N inputs or states, build a coverage matrix:
|
|
56
|
+
|
|
57
|
+
**Example: User creation form (3 fields, 2 roles, invite toggle)**
|
|
58
|
+
|
|
59
|
+
| Test | Name | Email | Role | Invite | Expected |
|
|
60
|
+
|------|------|-------|------|--------|----------|
|
|
61
|
+
| Happy path | "Jane" | jane@x.com | Admin | on | Created + invite sent |
|
|
62
|
+
| Happy path (no invite) | "Jane" | jane@x.com | Viewer | off | Created, no invite |
|
|
63
|
+
| Empty name | "" | jane@x.com | Admin | on | Validation error on name |
|
|
64
|
+
| Empty email | "Jane" | "" | Admin | on | Validation error on email |
|
|
65
|
+
| Invalid email | "Jane" | "notanemail" | Admin | on | Validation error on email |
|
|
66
|
+
| Duplicate email | "Jane" | existing@x.com | Admin | on | 409 conflict error shown |
|
|
67
|
+
| Name at max length | "A"×100 | jane@x.com | Admin | on | Created (boundary) |
|
|
68
|
+
| Name exceeds max | "A"×101 | jane@x.com | Admin | on | Validation error |
|
|
69
|
+
| Special chars in name | "O'Brien-José" | jane@x.com | Admin | on | Created (Unicode safe) |
|
|
70
|
+
| XSS in name | `<script>` | jane@x.com | Admin | on | Sanitized, no execution |
|
|
71
|
+
| Each role option | "Jane" | jane@x.com | {each role} | on | Correct role assigned |
|
|
72
|
+
| Server error | "Jane" | jane@x.com | Admin | on | Error message, form preserved |
|
|
73
|
+
| Network offline | "Jane" | jane@x.com | Admin | on | Offline indicator, retry option |
|
|
74
|
+
| Double submit | "Jane" | jane@x.com | Admin | on | Only one user created |
|
|
75
|
+
| Unauthorized user | — | — | — | — | Redirected or 403 shown |
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
MINIMUM COVERAGE PER FEATURE:
|
|
79
|
+
├── 1 happy path per valid combination that produces different outcomes
|
|
80
|
+
├── 1 validation test per field per validation rule
|
|
81
|
+
├── 1 test per error type (400, 401, 403, 404, 409, 500, network)
|
|
82
|
+
├── 1 empty state test
|
|
83
|
+
├── 1 boundary test per field with limits (min, max, exact boundary)
|
|
84
|
+
├── 1 test per role/permission that affects visibility or access
|
|
85
|
+
├── 1 test per state transition in the feature's state machine
|
|
86
|
+
└── 1 concurrent/race condition test per form submission
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## 3. State Transition Testing
|
|
92
|
+
|
|
93
|
+
For features with multiple states (orders, subscriptions, tickets, etc.), test EVERY valid transition:
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
MANDATORY:
|
|
97
|
+
├── Map the state machine: identify all states and valid transitions
|
|
98
|
+
├── Test each transition: action + assertion that new state is correct
|
|
99
|
+
├── Test invalid transitions: verify they're rejected or unavailable
|
|
100
|
+
└── Test the full lifecycle: create → intermediate states → terminal state
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Example: Order lifecycle**
|
|
104
|
+
```typescript
|
|
105
|
+
test.describe('Order state transitions', () => {
|
|
106
|
+
test('new order starts as pending', async ({ page }) => {
|
|
107
|
+
await createOrder(page, testData.validOrder);
|
|
108
|
+
await expect(page.locator('[data-testid="order-status"]')).toHaveText('Pending');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('pending → confirmed on payment', async ({ page }) => {
|
|
112
|
+
const order = await createPendingOrder(page);
|
|
113
|
+
await page.click('[data-testid="confirm-payment"]');
|
|
114
|
+
await expect(page.locator('[data-testid="order-status"]')).toHaveText('Confirmed');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('confirmed → shipped on dispatch', async ({ page }) => {
|
|
118
|
+
const order = await createConfirmedOrder(page);
|
|
119
|
+
await page.click('[data-testid="mark-shipped"]');
|
|
120
|
+
await expect(page.locator('[data-testid="order-status"]')).toHaveText('Shipped');
|
|
121
|
+
await expect(page.locator('[data-testid="tracking-number"]')).not.toBeEmpty();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('pending → cancelled is allowed', async ({ page }) => {
|
|
125
|
+
const order = await createPendingOrder(page);
|
|
126
|
+
await page.click('[data-testid="cancel-order"]');
|
|
127
|
+
await expect(page.locator('[data-testid="order-status"]')).toHaveText('Cancelled');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('shipped → cancelled is NOT allowed', async ({ page }) => {
|
|
131
|
+
const order = await createShippedOrder(page);
|
|
132
|
+
await expect(page.locator('[data-testid="cancel-order"]')).toBeDisabled();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## 4. Selectors — Resilient and Maintainable
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
MANDATORY:
|
|
143
|
+
├── Prefer user-facing selectors: getByRole, getByLabel, getByText, getByPlaceholder
|
|
144
|
+
├── Use data-testid for elements with no accessible role or visible text
|
|
145
|
+
├── NEVER use CSS class selectors (.btn-primary) — they break on styling changes
|
|
146
|
+
├── NEVER use DOM structure selectors (div > span:nth-child(2)) — they break on layout changes
|
|
147
|
+
├── NEVER use auto-generated IDs or dynamic class names
|
|
148
|
+
└── Combine selectors for precision: page.getByRole('button', { name: 'Submit' })
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**BAD**
|
|
152
|
+
```typescript
|
|
153
|
+
await page.click('.btn.btn-primary.submit-form');
|
|
154
|
+
await page.locator('div.user-list > div:nth-child(3) > span').click();
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**GOOD**
|
|
158
|
+
```typescript
|
|
159
|
+
await page.getByRole('button', { name: 'Submit' }).click();
|
|
160
|
+
await page.getByLabel('Email').fill('jane@example.com');
|
|
161
|
+
await page.locator('[data-testid="user-row-jane"]').click();
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## 5. Waiting and Assertions
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
MANDATORY:
|
|
170
|
+
├── Use Playwright auto-waiting — NEVER add manual sleep/setTimeout
|
|
171
|
+
├── Assert on network completion for data-dependent tests: page.waitForResponse
|
|
172
|
+
├── Use toHaveText, toContainText for content verification — not just toBeVisible
|
|
173
|
+
├── Use web-first assertions (expect with auto-retry) — not page.evaluate checks
|
|
174
|
+
├── Set assertion timeout for slow operations: expect(...).toHaveText('...', { timeout: 10000 })
|
|
175
|
+
└── Wait for navigation after clicks that change pages: page.waitForURL
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**BAD**
|
|
179
|
+
```typescript
|
|
180
|
+
await page.click('#submit');
|
|
181
|
+
await page.waitForTimeout(3000); // arbitrary sleep!
|
|
182
|
+
const text = await page.locator('#result').innerText();
|
|
183
|
+
expect(text).toBe('Success'); // not auto-retrying
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**GOOD**
|
|
187
|
+
```typescript
|
|
188
|
+
await page.click('#submit');
|
|
189
|
+
await page.waitForResponse(resp => resp.url().includes('/api/users') && resp.status() === 201);
|
|
190
|
+
await expect(page.locator('[data-testid="result"]')).toHaveText('User created successfully');
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## 6. Page Object Model
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
MANDATORY for projects with 10+ tests:
|
|
199
|
+
├── One page object per page or major component
|
|
200
|
+
├── Page objects encapsulate selectors and actions — tests read like user stories
|
|
201
|
+
├── Methods return data or other page objects (for navigation)
|
|
202
|
+
├── NEVER put assertions in page objects — assertions belong in tests
|
|
203
|
+
└── Page objects live in a tests/pages/ or tests/pom/ directory
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**GOOD**
|
|
207
|
+
```typescript
|
|
208
|
+
// tests/pages/LoginPage.ts
|
|
209
|
+
export class LoginPage {
|
|
210
|
+
constructor(private page: Page) {}
|
|
211
|
+
|
|
212
|
+
async goto() {
|
|
213
|
+
await this.page.goto('/login');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async login(email: string, password: string) {
|
|
217
|
+
await this.page.getByLabel('Email').fill(email);
|
|
218
|
+
await this.page.getByLabel('Password').fill(password);
|
|
219
|
+
await this.page.getByRole('button', { name: 'Sign in' }).click();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
emailError() {
|
|
223
|
+
return this.page.locator('[data-testid="email-error"]');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
passwordError() {
|
|
227
|
+
return this.page.locator('[data-testid="password-error"]');
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// tests/login.spec.ts
|
|
232
|
+
test('successful login redirects to dashboard', async ({ page }) => {
|
|
233
|
+
const loginPage = new LoginPage(page);
|
|
234
|
+
await loginPage.goto();
|
|
235
|
+
await loginPage.login('jane@example.com', 'validpassword');
|
|
236
|
+
await expect(page).toHaveURL('/dashboard');
|
|
237
|
+
await expect(page.getByText('Welcome, Jane')).toBeVisible();
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test('invalid email shows validation error', async ({ page }) => {
|
|
241
|
+
const loginPage = new LoginPage(page);
|
|
242
|
+
await loginPage.goto();
|
|
243
|
+
await loginPage.login('notanemail', 'password');
|
|
244
|
+
await expect(loginPage.emailError()).toHaveText('Enter a valid email address');
|
|
245
|
+
});
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## 7. API Mocking and Network Control
|
|
251
|
+
|
|
252
|
+
```
|
|
253
|
+
MANDATORY for isolated, deterministic tests:
|
|
254
|
+
├── Use page.route() to mock API responses — control what the UI receives
|
|
255
|
+
├── Mock error responses to test error handling: route.fulfill({ status: 500 })
|
|
256
|
+
├── Mock empty responses to test empty states
|
|
257
|
+
├── Mock slow responses to test loading states: route.fulfill with delay
|
|
258
|
+
├── Use page.waitForResponse to verify the app made the expected API call
|
|
259
|
+
└── For integration tests against real API: use a test/seed database, not mocks
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
**GOOD**
|
|
263
|
+
```typescript
|
|
264
|
+
test('shows error message on server failure', async ({ page }) => {
|
|
265
|
+
await page.route('**/api/users', route =>
|
|
266
|
+
route.fulfill({ status: 500, body: JSON.stringify({ error: 'Internal error' }) })
|
|
267
|
+
);
|
|
268
|
+
await page.goto('/users');
|
|
269
|
+
await expect(page.getByText('Failed to load users')).toBeVisible();
|
|
270
|
+
await expect(page.getByRole('button', { name: 'Retry' })).toBeVisible();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test('shows empty state when no users exist', async ({ page }) => {
|
|
274
|
+
await page.route('**/api/users', route =>
|
|
275
|
+
route.fulfill({ status: 200, body: JSON.stringify({ data: [], meta: { total: 0 } }) })
|
|
276
|
+
);
|
|
277
|
+
await page.goto('/users');
|
|
278
|
+
await expect(page.getByText('No users found')).toBeVisible();
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test('shows loading skeleton while fetching', async ({ page }) => {
|
|
282
|
+
await page.route('**/api/users', async route => {
|
|
283
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
284
|
+
await route.fulfill({ status: 200, body: JSON.stringify({ data: testUsers }) });
|
|
285
|
+
});
|
|
286
|
+
await page.goto('/users');
|
|
287
|
+
await expect(page.locator('[data-testid="loading-skeleton"]')).toBeVisible();
|
|
288
|
+
await expect(page.locator('.user-row').first()).toContainText('jane@example.com');
|
|
289
|
+
});
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## 8. Test Organization
|
|
295
|
+
|
|
296
|
+
```
|
|
297
|
+
MANDATORY:
|
|
298
|
+
├── One spec file per feature or page: login.spec.ts, user-management.spec.ts
|
|
299
|
+
├── Group related tests with test.describe
|
|
300
|
+
├── Use test.beforeEach for common setup (navigation, auth, seeding)
|
|
301
|
+
├── Use test fixtures for reusable authenticated state
|
|
302
|
+
├── Tag tests for selective runs: test('...', { tag: '@smoke' }, ...)
|
|
303
|
+
└── Keep individual tests independent — no test should depend on another's state
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
**GOOD**
|
|
307
|
+
```typescript
|
|
308
|
+
test.describe('User Management', () => {
|
|
309
|
+
test.beforeEach(async ({ page }) => {
|
|
310
|
+
await loginAsAdmin(page);
|
|
311
|
+
await page.goto('/users');
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test.describe('List View', () => {
|
|
315
|
+
test('displays paginated user list', ...);
|
|
316
|
+
test('filters by role', ...);
|
|
317
|
+
test('searches by name or email', ...);
|
|
318
|
+
test('shows empty state when filter matches nothing', ...);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test.describe('Create User', () => {
|
|
322
|
+
test('creates user with valid data', ...);
|
|
323
|
+
test('shows validation errors for each invalid field', ...);
|
|
324
|
+
test('handles duplicate email conflict', ...);
|
|
325
|
+
test('handles server error gracefully', ...);
|
|
326
|
+
test('prevents double submission', ...);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test.describe('Edit User', () => {
|
|
330
|
+
test('pre-fills form with existing data', ...);
|
|
331
|
+
test('saves changes and shows confirmation', ...);
|
|
332
|
+
test('handles concurrent edit conflict', ...);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test.describe('Delete User', () => {
|
|
336
|
+
test('confirms before deleting', ...);
|
|
337
|
+
test('shows success after deletion', ...);
|
|
338
|
+
test('handles deletion of already-deleted user', ...);
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
## 9. Test Data Management
|
|
346
|
+
|
|
347
|
+
```
|
|
348
|
+
MANDATORY:
|
|
349
|
+
├── Use factories or fixtures for test data — NEVER hardcode in test bodies
|
|
350
|
+
├── Each test creates its own data — no shared mutable state between tests
|
|
351
|
+
├── Use realistic data (not "test123") — catches encoding, truncation, display issues
|
|
352
|
+
├── Include edge case data in fixtures: Unicode, long strings, special chars, empty strings
|
|
353
|
+
├── Clean up test data after each test (or use isolated test databases)
|
|
354
|
+
└── Store reusable test data in tests/fixtures/ directory
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
**GOOD**
|
|
358
|
+
```typescript
|
|
359
|
+
// tests/fixtures/users.ts
|
|
360
|
+
export const testUsers = {
|
|
361
|
+
standard: { name: 'Jane Doe', email: 'jane@example.com', role: 'member' },
|
|
362
|
+
admin: { name: 'Admin User', email: 'admin@example.com', role: 'admin' },
|
|
363
|
+
unicode: { name: "José O'Brien-García", email: 'jose@example.com', role: 'member' },
|
|
364
|
+
longName: { name: 'A'.repeat(100), email: 'long@example.com', role: 'member' },
|
|
365
|
+
specialChars: { name: 'Test <script>alert(1)</script>', email: 'xss@example.com', role: 'member' },
|
|
366
|
+
};
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
## 10. Combinatorial Testing Strategy
|
|
372
|
+
|
|
373
|
+
For features with multiple interacting inputs, use pairwise/combinatorial coverage:
|
|
374
|
+
|
|
375
|
+
```
|
|
376
|
+
MANDATORY for forms with 3+ independent inputs:
|
|
377
|
+
├── Identify all inputs and their valid values
|
|
378
|
+
├── Test all single-field validations independently
|
|
379
|
+
├── Use pairwise combinations for multi-field interactions (not full cartesian product)
|
|
380
|
+
├── Always test the extreme corners: all-empty, all-max, all-invalid
|
|
381
|
+
└── Add specific combinations for known business rules
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
**Example: Search with 3 filters (status, role, date range)**
|
|
385
|
+
|
|
386
|
+
Instead of testing all 4×3×5 = 60 combinations, use pairwise:
|
|
387
|
+
|
|
388
|
+
```typescript
|
|
389
|
+
const filterCombinations = [
|
|
390
|
+
// Pairwise covers all 2-way interactions with fewer tests
|
|
391
|
+
{ status: 'active', role: 'admin', dateRange: 'last7days' },
|
|
392
|
+
{ status: 'active', role: 'viewer', dateRange: 'last30days' },
|
|
393
|
+
{ status: 'active', role: 'member', dateRange: 'allTime' },
|
|
394
|
+
{ status: 'inactive', role: 'admin', dateRange: 'last30days' },
|
|
395
|
+
{ status: 'inactive', role: 'viewer', dateRange: 'allTime' },
|
|
396
|
+
{ status: 'inactive', role: 'member', dateRange: 'last7days' },
|
|
397
|
+
{ status: 'all', role: 'admin', dateRange: 'allTime' },
|
|
398
|
+
{ status: 'all', role: 'viewer', dateRange: 'last7days' },
|
|
399
|
+
{ status: 'all', role: 'member', dateRange: 'last30days' },
|
|
400
|
+
// Extremes
|
|
401
|
+
{ status: 'all', role: undefined, dateRange: undefined }, // no filters
|
|
402
|
+
];
|
|
403
|
+
|
|
404
|
+
for (const combo of filterCombinations) {
|
|
405
|
+
test(`filters: status=${combo.status}, role=${combo.role}, date=${combo.dateRange}`, async ({ page }) => {
|
|
406
|
+
await applyFilters(page, combo);
|
|
407
|
+
const results = await getVisibleResults(page);
|
|
408
|
+
// Assert each result matches ALL active filters
|
|
409
|
+
for (const result of results) {
|
|
410
|
+
if (combo.status !== 'all') expect(result.status).toBe(combo.status);
|
|
411
|
+
if (combo.role) expect(result.role).toBe(combo.role);
|
|
412
|
+
if (combo.dateRange) expect(isInDateRange(result.date, combo.dateRange)).toBe(true);
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
---
|
|
419
|
+
|
|
420
|
+
## 11. Multi-Step Workflow Testing
|
|
421
|
+
|
|
422
|
+
```
|
|
423
|
+
MANDATORY for multi-page or multi-step features:
|
|
424
|
+
├── Test the complete end-to-end flow: start → each step → completion → verification
|
|
425
|
+
├── Test backward navigation: step 3 → step 2 → step 3 (data preserved?)
|
|
426
|
+
├── Test abandonment: start flow → navigate away → return (state preserved or reset?)
|
|
427
|
+
├── Test each step's validation independently
|
|
428
|
+
├── Test the flow with pre-filled data (edit mode vs create mode)
|
|
429
|
+
└── Verify the final result by reading it back (not just checking the success message)
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
**GOOD**
|
|
433
|
+
```typescript
|
|
434
|
+
test('complete checkout flow end-to-end', async ({ page }) => {
|
|
435
|
+
// Step 1: Add to cart
|
|
436
|
+
await page.goto('/products');
|
|
437
|
+
await page.getByRole('button', { name: 'Add Widget to cart' }).click();
|
|
438
|
+
await expect(page.locator('[data-testid="cart-count"]')).toHaveText('1');
|
|
439
|
+
|
|
440
|
+
// Step 2: Cart review
|
|
441
|
+
await page.goto('/cart');
|
|
442
|
+
await expect(page.getByText('Widget')).toBeVisible();
|
|
443
|
+
await page.getByRole('button', { name: 'Checkout' }).click();
|
|
444
|
+
|
|
445
|
+
// Step 3: Shipping
|
|
446
|
+
await page.getByLabel('Address').fill('123 Main St');
|
|
447
|
+
await page.getByLabel('City').fill('Springfield');
|
|
448
|
+
await page.getByRole('button', { name: 'Continue to payment' }).click();
|
|
449
|
+
|
|
450
|
+
// Step 4: Payment
|
|
451
|
+
await page.getByLabel('Card number').fill('4242424242424242');
|
|
452
|
+
await page.getByRole('button', { name: 'Place order' }).click();
|
|
453
|
+
|
|
454
|
+
// Verify: Check the confirmation AND read back the order
|
|
455
|
+
await expect(page).toHaveURL(/\/orders\/[a-z0-9-]+/);
|
|
456
|
+
await expect(page.getByText('Order confirmed')).toBeVisible();
|
|
457
|
+
await expect(page.getByText('Widget')).toBeVisible();
|
|
458
|
+
await expect(page.getByText('123 Main St')).toBeVisible();
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
test('checkout preserves data on backward navigation', async ({ page }) => {
|
|
462
|
+
// Fill shipping, go to payment, go BACK to shipping
|
|
463
|
+
// Assert: shipping fields still populated
|
|
464
|
+
});
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
---
|
|
468
|
+
|
|
469
|
+
## 12. Cross-Browser and Responsive Testing
|
|
470
|
+
|
|
471
|
+
```
|
|
472
|
+
RECOMMENDED:
|
|
473
|
+
├── Configure projects in playwright.config for chromium, firefox, webkit
|
|
474
|
+
├── Add mobile viewport project for responsive testing
|
|
475
|
+
├── Run cross-browser in CI — chromium-only acceptable for local dev
|
|
476
|
+
├── Test touch interactions on mobile viewports (tap, swipe)
|
|
477
|
+
└── Verify responsive breakpoints: mobile (375px), tablet (768px), desktop (1280px)
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
**GOOD** — playwright.config.ts:
|
|
481
|
+
```typescript
|
|
482
|
+
projects: [
|
|
483
|
+
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
|
|
484
|
+
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
|
|
485
|
+
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
|
|
486
|
+
{ name: 'mobile-chrome', use: { ...devices['Pixel 5'] } },
|
|
487
|
+
{ name: 'mobile-safari', use: { ...devices['iPhone 13'] } },
|
|
488
|
+
],
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
---
|
|
492
|
+
|
|
493
|
+
## 13. Anti-Patterns
|
|
494
|
+
|
|
495
|
+
```
|
|
496
|
+
NEVER:
|
|
497
|
+
├── Layout-only assertions (isVisible, toBeAttached without behavioral follow-up)
|
|
498
|
+
├── waitForTimeout / manual sleep — use auto-waiting and waitForResponse
|
|
499
|
+
├── CSS class selectors (.btn-primary) — use role, label, testid
|
|
500
|
+
├── DOM structure selectors (div > span:nth-child) — breaks on any layout change
|
|
501
|
+
├── Tests that depend on other tests' state — each test is independent
|
|
502
|
+
├── Assertions in page objects — page objects encapsulate actions, tests assert
|
|
503
|
+
├── Hardcoded test data in test bodies — use fixtures
|
|
504
|
+
├── Skipping error/empty/loading state tests — they catch real bugs
|
|
505
|
+
├── Testing only the happy path — most bugs live in edge cases
|
|
506
|
+
├── Full cartesian product when pairwise suffices — wastes CI time
|
|
507
|
+
└── console.log for debugging — use Playwright trace viewer and screenshots
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
---
|
|
511
|
+
|
|
512
|
+
## Playwright Verification Checklist
|
|
513
|
+
|
|
514
|
+
- [ ] Every assertion verifies behavior — not just element existence
|
|
515
|
+
- [ ] Coverage matrix built per feature (happy, validation, error, empty, edge, permissions, state transitions)
|
|
516
|
+
- [ ] All form fields tested: valid, invalid, empty, boundary, special chars
|
|
517
|
+
- [ ] Error states tested: 400, 401, 403, 404, 500, network failure
|
|
518
|
+
- [ ] Empty states tested: zero records, no search results
|
|
519
|
+
- [ ] State transitions tested: every valid + invalid transition
|
|
520
|
+
- [ ] Multi-step flows tested end-to-end with back-navigation
|
|
521
|
+
- [ ] Pairwise combinations for multi-input features
|
|
522
|
+
- [ ] Double-submit / concurrent action protection tested
|
|
523
|
+
- [ ] Selectors use role, label, testid — no CSS classes or DOM structure
|
|
524
|
+
- [ ] No manual waits — auto-waiting and waitForResponse only
|
|
525
|
+
- [ ] Page Object Model used (if 10+ tests)
|
|
526
|
+
- [ ] Test data in fixtures — not hardcoded
|
|
527
|
+
- [ ] Each test is independent — no shared mutable state
|
|
528
|
+
- [ ] API mocked for error/empty/loading scenarios
|