omgkit 2.2.0 → 2.3.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 +3 -3
- package/package.json +1 -1
- package/plugin/skills/databases/database-management/SKILL.md +288 -0
- package/plugin/skills/databases/database-migration/SKILL.md +285 -0
- package/plugin/skills/databases/database-schema-design/SKILL.md +195 -0
- package/plugin/skills/databases/mongodb/SKILL.md +60 -776
- package/plugin/skills/databases/prisma/SKILL.md +53 -744
- package/plugin/skills/databases/redis/SKILL.md +53 -860
- package/plugin/skills/databases/supabase/SKILL.md +283 -0
- package/plugin/skills/devops/aws/SKILL.md +68 -672
- package/plugin/skills/devops/github-actions/SKILL.md +54 -657
- package/plugin/skills/devops/kubernetes/SKILL.md +67 -602
- package/plugin/skills/devops/performance-profiling/SKILL.md +59 -863
- package/plugin/skills/frameworks/django/SKILL.md +87 -853
- package/plugin/skills/frameworks/express/SKILL.md +95 -1301
- package/plugin/skills/frameworks/fastapi/SKILL.md +90 -1198
- package/plugin/skills/frameworks/laravel/SKILL.md +87 -1187
- package/plugin/skills/frameworks/nestjs/SKILL.md +106 -973
- package/plugin/skills/frameworks/react/SKILL.md +94 -962
- package/plugin/skills/frameworks/vue/SKILL.md +95 -1242
- package/plugin/skills/frontend/accessibility/SKILL.md +91 -1056
- package/plugin/skills/frontend/frontend-design/SKILL.md +69 -1262
- package/plugin/skills/frontend/responsive/SKILL.md +76 -799
- package/plugin/skills/frontend/shadcn-ui/SKILL.md +73 -921
- package/plugin/skills/frontend/tailwindcss/SKILL.md +60 -788
- package/plugin/skills/frontend/threejs/SKILL.md +72 -1266
- package/plugin/skills/languages/javascript/SKILL.md +106 -849
- package/plugin/skills/methodology/brainstorming/SKILL.md +70 -576
- package/plugin/skills/methodology/defense-in-depth/SKILL.md +79 -831
- package/plugin/skills/methodology/dispatching-parallel-agents/SKILL.md +81 -654
- package/plugin/skills/methodology/executing-plans/SKILL.md +86 -529
- package/plugin/skills/methodology/finishing-development-branch/SKILL.md +95 -586
- package/plugin/skills/methodology/problem-solving/SKILL.md +67 -681
- package/plugin/skills/methodology/receiving-code-review/SKILL.md +70 -533
- package/plugin/skills/methodology/requesting-code-review/SKILL.md +70 -610
- package/plugin/skills/methodology/root-cause-tracing/SKILL.md +70 -646
- package/plugin/skills/methodology/sequential-thinking/SKILL.md +70 -478
- package/plugin/skills/methodology/systematic-debugging/SKILL.md +66 -559
- package/plugin/skills/methodology/test-driven-development/SKILL.md +91 -752
- package/plugin/skills/methodology/testing-anti-patterns/SKILL.md +78 -687
- package/plugin/skills/methodology/token-optimization/SKILL.md +72 -602
- package/plugin/skills/methodology/verification-before-completion/SKILL.md +108 -529
- package/plugin/skills/methodology/writing-plans/SKILL.md +79 -566
- package/plugin/skills/omega/omega-architecture/SKILL.md +91 -752
- package/plugin/skills/omega/omega-coding/SKILL.md +161 -552
- package/plugin/skills/omega/omega-sprint/SKILL.md +132 -777
- package/plugin/skills/omega/omega-testing/SKILL.md +157 -845
- package/plugin/skills/omega/omega-thinking/SKILL.md +165 -606
- package/plugin/skills/security/better-auth/SKILL.md +46 -1034
- package/plugin/skills/security/oauth/SKILL.md +80 -934
- package/plugin/skills/security/owasp/SKILL.md +78 -862
- package/plugin/skills/testing/playwright/SKILL.md +77 -700
- package/plugin/skills/testing/pytest/SKILL.md +73 -811
- package/plugin/skills/testing/vitest/SKILL.md +60 -920
- package/plugin/skills/tools/document-processing/SKILL.md +111 -838
- package/plugin/skills/tools/image-processing/SKILL.md +126 -659
- package/plugin/skills/tools/mcp-development/SKILL.md +85 -758
- package/plugin/skills/tools/media-processing/SKILL.md +118 -735
- package/plugin/stdrules/SKILL_STANDARDS.md +490 -0
- package/plugin/skills/SKILL_STANDARDS.md +0 -743
|
@@ -1,141 +1,107 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: omega-
|
|
3
|
-
description:
|
|
2
|
+
name: testing-omega-quality
|
|
3
|
+
description: Implements comprehensive testing across all quality dimensions - accuracy, performance, security, and accessibility. Use when building test strategies or ensuring production-grade quality assurance.
|
|
4
4
|
category: omega
|
|
5
5
|
triggers:
|
|
6
6
|
- omega testing
|
|
7
7
|
- comprehensive testing
|
|
8
8
|
- test strategy
|
|
9
9
|
- quality assurance
|
|
10
|
-
- test pyramid
|
|
11
|
-
- test coverage
|
|
12
|
-
- testing best practices
|
|
13
10
|
---
|
|
14
11
|
|
|
15
|
-
# Omega
|
|
12
|
+
# Testing Omega Quality
|
|
16
13
|
|
|
17
|
-
Master **comprehensive testing strategies**
|
|
14
|
+
Master **comprehensive testing strategies** covering all quality dimensions - accuracy, performance, security, and accessibility.
|
|
18
15
|
|
|
19
|
-
##
|
|
16
|
+
## Quick Start
|
|
20
17
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
┌─────────────────────────────────────────────────────────────────────────┐
|
|
39
|
-
│ OMEGA TESTING PYRAMID │
|
|
40
|
-
├─────────────────────────────────────────────────────────────────────────┤
|
|
41
|
-
│ │
|
|
42
|
-
│ /\ │
|
|
43
|
-
│ /E2E\ ← Critical paths only │
|
|
44
|
-
│ /─────\ Slowest, most expensive │
|
|
45
|
-
│ / \ │
|
|
46
|
-
│ / Visual \ ← Screenshot comparisons │
|
|
47
|
-
│ /───────────\ │
|
|
48
|
-
│ / \ │
|
|
49
|
-
│ / Integration \ ← Service boundaries │
|
|
50
|
-
│ /─────────────────\ API contracts │
|
|
51
|
-
│ / \ │
|
|
52
|
-
│ / Component \ ← UI components isolated │
|
|
53
|
-
│ /───────────────────────\ │
|
|
54
|
-
│ / \ │
|
|
55
|
-
│ / Unit \ ← Fast, isolated │
|
|
56
|
-
│ /─────────────────────────────\ Business logic │
|
|
57
|
-
│ │
|
|
58
|
-
│ Target Coverage by Layer: │
|
|
59
|
-
│ • Unit: 80%+ (pure functions, business logic) │
|
|
60
|
-
│ • Component: 70%+ (UI components with mocks) │
|
|
61
|
-
│ • Integration: 60%+ (API endpoints, data flow) │
|
|
62
|
-
│ • E2E: Critical paths 100% (happy paths, auth, checkout) │
|
|
63
|
-
│ │
|
|
64
|
-
└─────────────────────────────────────────────────────────────────────────┘
|
|
18
|
+
```yaml
|
|
19
|
+
# 1. Define test strategy with 4 dimensions
|
|
20
|
+
TestStrategy:
|
|
21
|
+
Accuracy: { unit: 80%, integration: 60%, e2e: "critical paths" }
|
|
22
|
+
Performance: { p95: "<200ms", concurrent: 50 }
|
|
23
|
+
Security: { injection: true, auth: true, xss: true }
|
|
24
|
+
Accessibility: { wcag: "2.1 AA", keyboard: true }
|
|
25
|
+
|
|
26
|
+
# 2. Follow the test pyramid
|
|
27
|
+
Pyramid:
|
|
28
|
+
Unit: 80% # Fast, isolated, business logic
|
|
29
|
+
Component: 70% # UI components with mocks
|
|
30
|
+
Integration: 60% # API endpoints, data flow
|
|
31
|
+
E2E: "critical" # Happy paths, auth, checkout
|
|
32
|
+
|
|
33
|
+
# 3. Run quality gates in CI
|
|
34
|
+
Gates: ["coverage > 80%", "no-security-issues", "a11y-pass"]
|
|
65
35
|
```
|
|
66
36
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
```typescript
|
|
70
|
-
/**
|
|
71
|
-
* Omega Testing covers ALL quality dimensions
|
|
72
|
-
* Don't just test functionality - test quality
|
|
73
|
-
*/
|
|
37
|
+
## Features
|
|
74
38
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
39
|
+
| Feature | Description | Guide |
|
|
40
|
+
|---------|-------------|-------|
|
|
41
|
+
| 4D Testing | Accuracy, Performance, Security, Accessibility | Cover all quality dimensions |
|
|
42
|
+
| Test Pyramid | Unit, Component, Integration, E2E layers | More units, fewer E2E |
|
|
43
|
+
| Property-Based | Test with generated inputs | Catch edge cases automatically |
|
|
44
|
+
| Performance | Response time, load, memory testing | Percentile-based thresholds |
|
|
45
|
+
| Security | SQL injection, XSS, auth bypass tests | OWASP-aligned coverage |
|
|
46
|
+
| Accessibility | WCAG compliance, keyboard, screen reader | Automated a11y scanning |
|
|
47
|
+
| Visual Regression | Screenshot comparison testing | Catch UI regressions |
|
|
80
48
|
|
|
81
|
-
|
|
82
|
-
accuracy: AccuracyTests;
|
|
83
|
-
performance: PerformanceTests;
|
|
84
|
-
security: SecurityTests;
|
|
85
|
-
accessibility: AccessibilityTests;
|
|
86
|
-
}
|
|
49
|
+
## Common Patterns
|
|
87
50
|
|
|
88
|
-
|
|
89
|
-
interface AccuracyTests {
|
|
90
|
-
happyPath: Test[]; // Normal use cases work
|
|
91
|
-
edgeCases: Test[]; // Boundary conditions handled
|
|
92
|
-
errorCases: Test[]; // Failures handled gracefully
|
|
93
|
-
regressions: Test[]; // Previously fixed bugs stay fixed
|
|
94
|
-
}
|
|
51
|
+
### The Omega Test Pyramid
|
|
95
52
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
53
|
+
```
|
|
54
|
+
/\
|
|
55
|
+
/E2E\ <- Critical paths only (slowest)
|
|
56
|
+
/─────\
|
|
57
|
+
/ Visual \ <- Screenshot comparisons
|
|
58
|
+
/───────────\
|
|
59
|
+
/ Integration \ <- Service boundaries, APIs
|
|
60
|
+
/───────────────\
|
|
61
|
+
/ Component \ <- UI components isolated
|
|
62
|
+
/───────────────────\
|
|
63
|
+
/ Unit \ <- Fast, business logic (most)
|
|
64
|
+
/─────────────────────────\
|
|
65
|
+
```
|
|
103
66
|
|
|
104
|
-
|
|
105
|
-
interface SecurityTests {
|
|
106
|
-
authentication: Test[]; // Auth works correctly
|
|
107
|
-
authorization: Test[]; // Permissions enforced
|
|
108
|
-
injection: Test[]; // SQL, XSS, etc. prevented
|
|
109
|
-
dataProtection: Test[]; // Sensitive data secured
|
|
110
|
-
}
|
|
67
|
+
### Four Quality Dimensions
|
|
111
68
|
|
|
112
|
-
|
|
113
|
-
interface
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
69
|
+
```typescript
|
|
70
|
+
interface OmegaTestSuite {
|
|
71
|
+
accuracy: {
|
|
72
|
+
happyPath: Test[]; // Normal use cases
|
|
73
|
+
edgeCases: Test[]; // Boundary conditions
|
|
74
|
+
errorCases: Test[]; // Failure handling
|
|
75
|
+
};
|
|
76
|
+
performance: {
|
|
77
|
+
responseTime: Test[]; // p50, p95, p99 latency
|
|
78
|
+
throughput: Test[]; // Requests per second
|
|
79
|
+
memory: Test[]; // Leak detection
|
|
80
|
+
};
|
|
81
|
+
security: {
|
|
82
|
+
authentication: Test[];
|
|
83
|
+
authorization: Test[];
|
|
84
|
+
injection: Test[]; // SQL, XSS prevention
|
|
85
|
+
};
|
|
86
|
+
accessibility: {
|
|
87
|
+
wcag: Test[]; // WCAG 2.1 AA
|
|
88
|
+
keyboard: Test[]; // Tab navigation
|
|
89
|
+
screenReader: Test[]; // ARIA labels
|
|
90
|
+
};
|
|
118
91
|
}
|
|
119
92
|
```
|
|
120
93
|
|
|
121
|
-
###
|
|
94
|
+
### Unit Testing Patterns
|
|
122
95
|
|
|
123
96
|
```typescript
|
|
124
|
-
|
|
125
|
-
* Unit Tests: Fast, isolated, focused on business logic
|
|
126
|
-
*/
|
|
127
|
-
|
|
128
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
129
|
-
|
|
130
|
-
// Pattern 1: Arrange-Act-Assert (AAA)
|
|
97
|
+
// Arrange-Act-Assert pattern
|
|
131
98
|
describe('calculateDiscount', () => {
|
|
132
99
|
it('applies 10% discount for orders over $100', () => {
|
|
133
100
|
// Arrange
|
|
134
101
|
const order = createOrder({ subtotal: 150 });
|
|
135
|
-
const discountRules = createDiscountRules();
|
|
136
102
|
|
|
137
103
|
// Act
|
|
138
|
-
const result = calculateDiscount(order
|
|
104
|
+
const result = calculateDiscount(order);
|
|
139
105
|
|
|
140
106
|
// Assert
|
|
141
107
|
expect(result.discount).toBe(15);
|
|
@@ -143,828 +109,174 @@ describe('calculateDiscount', () => {
|
|
|
143
109
|
});
|
|
144
110
|
});
|
|
145
111
|
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
'user+tag@example.org'
|
|
154
|
-
];
|
|
155
|
-
|
|
156
|
-
validEmails.forEach(email => {
|
|
157
|
-
expect(validateEmail(email)).toBe(true);
|
|
158
|
-
});
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
// Edge cases
|
|
162
|
-
it.each([
|
|
163
|
-
['missing @', 'userexample.com'],
|
|
164
|
-
['missing domain', 'user@'],
|
|
165
|
-
['missing local part', '@example.com'],
|
|
166
|
-
['invalid characters', 'user<script>@example.com'],
|
|
167
|
-
['empty string', ''],
|
|
168
|
-
['whitespace only', ' '],
|
|
169
|
-
])('rejects %s: %s', (_description, email) => {
|
|
170
|
-
expect(validateEmail(email)).toBe(false);
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
// Boundary conditions
|
|
174
|
-
it('handles maximum length email', () => {
|
|
175
|
-
const longEmail = 'a'.repeat(64) + '@' + 'b'.repeat(63) + '.com';
|
|
176
|
-
expect(validateEmail(longEmail)).toBe(true);
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it('rejects email exceeding maximum length', () => {
|
|
180
|
-
const tooLongEmail = 'a'.repeat(65) + '@' + 'b'.repeat(64) + '.com';
|
|
181
|
-
expect(validateEmail(tooLongEmail)).toBe(false);
|
|
182
|
-
});
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
// Pattern 3: Testing Error Handling
|
|
186
|
-
describe('fetchUserData', () => {
|
|
187
|
-
const mockApi = vi.fn();
|
|
188
|
-
|
|
189
|
-
beforeEach(() => {
|
|
190
|
-
mockApi.mockReset();
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
it('throws UserNotFoundError when user does not exist', async () => {
|
|
194
|
-
mockApi.mockRejectedValue(new Error('404'));
|
|
195
|
-
|
|
196
|
-
await expect(fetchUserData('nonexistent-id', mockApi))
|
|
197
|
-
.rejects
|
|
198
|
-
.toThrow(UserNotFoundError);
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
it('retries on transient failures', async () => {
|
|
202
|
-
mockApi
|
|
203
|
-
.mockRejectedValueOnce(new Error('timeout'))
|
|
204
|
-
.mockRejectedValueOnce(new Error('timeout'))
|
|
205
|
-
.mockResolvedValueOnce({ id: '123', name: 'Test' });
|
|
206
|
-
|
|
207
|
-
const result = await fetchUserData('123', mockApi);
|
|
208
|
-
|
|
209
|
-
expect(mockApi).toHaveBeenCalledTimes(3);
|
|
210
|
-
expect(result.name).toBe('Test');
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
it('gives up after max retries', async () => {
|
|
214
|
-
mockApi.mockRejectedValue(new Error('timeout'));
|
|
215
|
-
|
|
216
|
-
await expect(fetchUserData('123', mockApi))
|
|
217
|
-
.rejects
|
|
218
|
-
.toThrow(MaxRetriesExceededError);
|
|
219
|
-
|
|
220
|
-
expect(mockApi).toHaveBeenCalledTimes(3);
|
|
221
|
-
});
|
|
112
|
+
// Parameterized edge cases
|
|
113
|
+
it.each([
|
|
114
|
+
['missing @', 'userexample.com', false],
|
|
115
|
+
['valid format', 'user@example.com', true],
|
|
116
|
+
['empty string', '', false],
|
|
117
|
+
])('validateEmail %s: %s -> %s', (_desc, email, expected) => {
|
|
118
|
+
expect(validateEmail(email)).toBe(expected);
|
|
222
119
|
});
|
|
223
120
|
|
|
224
|
-
//
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
id: fc.string(),
|
|
230
|
-
name: fc.string(),
|
|
231
|
-
age: fc.nat()
|
|
232
|
-
})), (users) => {
|
|
233
|
-
const sorted = sortUsers(users, 'name');
|
|
234
|
-
return sorted.length === users.length;
|
|
235
|
-
})
|
|
236
|
-
);
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
it('maintains all original elements', () => {
|
|
240
|
-
fc.assert(
|
|
241
|
-
fc.property(fc.array(userArbitrary), (users) => {
|
|
242
|
-
const sorted = sortUsers(users, 'name');
|
|
243
|
-
const originalIds = new Set(users.map(u => u.id));
|
|
244
|
-
const sortedIds = new Set(sorted.map(u => u.id));
|
|
245
|
-
return setsEqual(originalIds, sortedIds);
|
|
246
|
-
})
|
|
247
|
-
);
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
it('result is actually sorted', () => {
|
|
251
|
-
fc.assert(
|
|
252
|
-
fc.property(fc.array(userArbitrary), (users) => {
|
|
253
|
-
const sorted = sortUsers(users, 'name');
|
|
254
|
-
return sorted.every((user, i) =>
|
|
255
|
-
i === 0 || user.name >= sorted[i - 1].name
|
|
256
|
-
);
|
|
257
|
-
})
|
|
258
|
-
);
|
|
259
|
-
});
|
|
121
|
+
// Property-based testing
|
|
122
|
+
it('sorted array has same length as input', () => {
|
|
123
|
+
fc.assert(fc.property(fc.array(fc.nat()), (arr) => {
|
|
124
|
+
return sortArray(arr).length === arr.length;
|
|
125
|
+
}));
|
|
260
126
|
});
|
|
261
127
|
```
|
|
262
128
|
|
|
263
|
-
###
|
|
129
|
+
### Integration Testing
|
|
264
130
|
|
|
265
131
|
```typescript
|
|
266
|
-
/**
|
|
267
|
-
* Integration Tests: Test service boundaries and contracts
|
|
268
|
-
*/
|
|
269
|
-
|
|
270
|
-
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
271
|
-
import { createTestDatabase, cleanupTestDatabase } from './test-utils';
|
|
272
|
-
|
|
273
132
|
describe('UserService Integration', () => {
|
|
274
133
|
let db: TestDatabase;
|
|
275
|
-
let userService: UserService;
|
|
276
|
-
|
|
277
|
-
beforeAll(async () => {
|
|
278
|
-
db = await createTestDatabase();
|
|
279
|
-
userService = new UserService(db);
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
afterAll(async () => {
|
|
283
|
-
await cleanupTestDatabase(db);
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
// Test real database interactions
|
|
287
|
-
describe('createUser', () => {
|
|
288
|
-
it('persists user to database', async () => {
|
|
289
|
-
const userData = {
|
|
290
|
-
email: 'test@example.com',
|
|
291
|
-
name: 'Test User'
|
|
292
|
-
};
|
|
293
|
-
|
|
294
|
-
const user = await userService.createUser(userData);
|
|
295
|
-
|
|
296
|
-
// Verify in database directly
|
|
297
|
-
const dbUser = await db.query('SELECT * FROM users WHERE id = $1', [user.id]);
|
|
298
|
-
expect(dbUser.email).toBe(userData.email);
|
|
299
|
-
expect(dbUser.name).toBe(userData.name);
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
it('enforces unique email constraint', async () => {
|
|
303
|
-
const userData = { email: 'duplicate@example.com', name: 'User 1' };
|
|
304
|
-
await userService.createUser(userData);
|
|
305
|
-
|
|
306
|
-
await expect(userService.createUser({
|
|
307
|
-
email: 'duplicate@example.com',
|
|
308
|
-
name: 'User 2'
|
|
309
|
-
})).rejects.toThrow(DuplicateEmailError);
|
|
310
|
-
});
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
// Test transactions
|
|
314
|
-
describe('transferCredits', () => {
|
|
315
|
-
it('atomically transfers credits between users', async () => {
|
|
316
|
-
const sender = await userService.createUser({ credits: 100 });
|
|
317
|
-
const receiver = await userService.createUser({ credits: 0 });
|
|
318
|
-
|
|
319
|
-
await userService.transferCredits(sender.id, receiver.id, 50);
|
|
320
|
-
|
|
321
|
-
const [updatedSender, updatedReceiver] = await Promise.all([
|
|
322
|
-
userService.getUser(sender.id),
|
|
323
|
-
userService.getUser(receiver.id)
|
|
324
|
-
]);
|
|
325
|
-
|
|
326
|
-
expect(updatedSender.credits).toBe(50);
|
|
327
|
-
expect(updatedReceiver.credits).toBe(50);
|
|
328
|
-
});
|
|
329
134
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
const receiver = await userService.createUser({ credits: 0 });
|
|
135
|
+
beforeAll(async () => { db = await createTestDatabase(); });
|
|
136
|
+
afterAll(async () => { await cleanupTestDatabase(db); });
|
|
333
137
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
await expect(
|
|
338
|
-
userService.transferCredits(sender.id, receiver.id, 50)
|
|
339
|
-
).rejects.toThrow();
|
|
340
|
-
|
|
341
|
-
// Verify no changes persisted
|
|
342
|
-
const [s, r] = await Promise.all([
|
|
343
|
-
userService.getUser(sender.id),
|
|
344
|
-
userService.getUser(receiver.id)
|
|
345
|
-
]);
|
|
346
|
-
|
|
347
|
-
expect(s.credits).toBe(100);
|
|
348
|
-
expect(r.credits).toBe(0);
|
|
138
|
+
it('persists user to database', async () => {
|
|
139
|
+
const user = await userService.createUser({
|
|
140
|
+
email: 'test@example.com', name: 'Test'
|
|
349
141
|
});
|
|
350
|
-
});
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
// API Contract Testing
|
|
354
|
-
describe('API Contract Tests', () => {
|
|
355
|
-
it('GET /users/:id returns correct schema', async () => {
|
|
356
|
-
const response = await api.get('/users/123');
|
|
357
|
-
|
|
358
|
-
expect(response.status).toBe(200);
|
|
359
|
-
expect(response.body).toMatchSchema({
|
|
360
|
-
type: 'object',
|
|
361
|
-
required: ['id', 'email', 'name', 'createdAt'],
|
|
362
|
-
properties: {
|
|
363
|
-
id: { type: 'string', format: 'uuid' },
|
|
364
|
-
email: { type: 'string', format: 'email' },
|
|
365
|
-
name: { type: 'string', minLength: 1 },
|
|
366
|
-
createdAt: { type: 'string', format: 'date-time' }
|
|
367
|
-
}
|
|
368
|
-
});
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
it('POST /users validates request body', async () => {
|
|
372
|
-
const response = await api.post('/users', {
|
|
373
|
-
body: { email: 'invalid-email' } // Missing name, invalid email
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
expect(response.status).toBe(400);
|
|
377
|
-
expect(response.body.errors).toContainEqual(
|
|
378
|
-
expect.objectContaining({ field: 'email', message: expect.any(String) })
|
|
379
|
-
);
|
|
380
|
-
expect(response.body.errors).toContainEqual(
|
|
381
|
-
expect.objectContaining({ field: 'name', message: expect.any(String) })
|
|
382
|
-
);
|
|
383
|
-
});
|
|
384
|
-
});
|
|
385
|
-
```
|
|
386
|
-
|
|
387
|
-
### 5. E2E Testing Patterns
|
|
388
|
-
|
|
389
|
-
```typescript
|
|
390
|
-
/**
|
|
391
|
-
* E2E Tests: Test critical user journeys
|
|
392
|
-
* Use sparingly - slow and expensive
|
|
393
|
-
*/
|
|
394
|
-
|
|
395
|
-
import { test, expect } from '@playwright/test';
|
|
396
|
-
|
|
397
|
-
// Critical Path: User Registration & Login
|
|
398
|
-
test.describe('Authentication Flow', () => {
|
|
399
|
-
test('new user can register and login', async ({ page }) => {
|
|
400
|
-
const testEmail = `test-${Date.now()}@example.com`;
|
|
401
|
-
|
|
402
|
-
// Registration
|
|
403
|
-
await page.goto('/register');
|
|
404
|
-
await page.fill('[data-testid="email"]', testEmail);
|
|
405
|
-
await page.fill('[data-testid="password"]', 'SecurePass123!');
|
|
406
|
-
await page.fill('[data-testid="confirm-password"]', 'SecurePass123!');
|
|
407
|
-
await page.click('[data-testid="register-button"]');
|
|
408
|
-
|
|
409
|
-
// Verify registration success
|
|
410
|
-
await expect(page).toHaveURL('/verify-email');
|
|
411
|
-
await expect(page.locator('[data-testid="success-message"]'))
|
|
412
|
-
.toContainText('verification email sent');
|
|
413
|
-
|
|
414
|
-
// Simulate email verification (in test environment)
|
|
415
|
-
await verifyEmailInTestMode(testEmail);
|
|
416
|
-
|
|
417
|
-
// Login
|
|
418
|
-
await page.goto('/login');
|
|
419
|
-
await page.fill('[data-testid="email"]', testEmail);
|
|
420
|
-
await page.fill('[data-testid="password"]', 'SecurePass123!');
|
|
421
|
-
await page.click('[data-testid="login-button"]');
|
|
422
|
-
|
|
423
|
-
// Verify login success
|
|
424
|
-
await expect(page).toHaveURL('/dashboard');
|
|
425
|
-
await expect(page.locator('[data-testid="user-menu"]')).toBeVisible();
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
test('handles invalid credentials', async ({ page }) => {
|
|
429
|
-
await page.goto('/login');
|
|
430
|
-
await page.fill('[data-testid="email"]', 'wrong@example.com');
|
|
431
|
-
await page.fill('[data-testid="password"]', 'wrongpassword');
|
|
432
|
-
await page.click('[data-testid="login-button"]');
|
|
433
|
-
|
|
434
|
-
await expect(page.locator('[data-testid="error-message"]'))
|
|
435
|
-
.toContainText('Invalid email or password');
|
|
436
|
-
await expect(page).toHaveURL('/login');
|
|
437
|
-
});
|
|
438
|
-
});
|
|
439
142
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
test.beforeEach(async ({ page }) => {
|
|
443
|
-
// Login as test user
|
|
444
|
-
await loginAsTestUser(page);
|
|
143
|
+
const dbUser = await db.query('SELECT * FROM users WHERE id = $1', [user.id]);
|
|
144
|
+
expect(dbUser.email).toBe('test@example.com');
|
|
445
145
|
});
|
|
446
146
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
await page.goto('/products/test-product');
|
|
450
|
-
await page.click('[data-testid="add-to-cart"]');
|
|
451
|
-
await expect(page.locator('[data-testid="cart-count"]')).toHaveText('1');
|
|
452
|
-
|
|
453
|
-
// Go to checkout
|
|
454
|
-
await page.click('[data-testid="cart-icon"]');
|
|
455
|
-
await page.click('[data-testid="checkout-button"]');
|
|
456
|
-
|
|
457
|
-
// Fill shipping info
|
|
458
|
-
await page.fill('[data-testid="address"]', '123 Test St');
|
|
459
|
-
await page.fill('[data-testid="city"]', 'Test City');
|
|
460
|
-
await page.fill('[data-testid="zip"]', '12345');
|
|
461
|
-
await page.click('[data-testid="continue-to-payment"]');
|
|
462
|
-
|
|
463
|
-
// Payment (use test card)
|
|
464
|
-
await page.fill('[data-testid="card-number"]', '4242424242424242');
|
|
465
|
-
await page.fill('[data-testid="expiry"]', '12/25');
|
|
466
|
-
await page.fill('[data-testid="cvc"]', '123');
|
|
467
|
-
await page.click('[data-testid="place-order"]');
|
|
468
|
-
|
|
469
|
-
// Verify success
|
|
470
|
-
await expect(page).toHaveURL(/\/orders\/[a-z0-9-]+/);
|
|
471
|
-
await expect(page.locator('[data-testid="order-status"]'))
|
|
472
|
-
.toHaveText('Order Confirmed');
|
|
473
|
-
});
|
|
474
|
-
});
|
|
147
|
+
it('rolls back transaction on failure', async () => {
|
|
148
|
+
vi.spyOn(db, 'commit').mockRejectedValueOnce(new Error('DB error'));
|
|
475
149
|
|
|
476
|
-
|
|
477
|
-
test.describe('Visual Regression', () => {
|
|
478
|
-
test('dashboard matches snapshot', async ({ page }) => {
|
|
479
|
-
await loginAsTestUser(page);
|
|
480
|
-
await page.goto('/dashboard');
|
|
481
|
-
await page.waitForLoadState('networkidle');
|
|
150
|
+
await expect(userService.transfer(from, to, 50)).rejects.toThrow();
|
|
482
151
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
});
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
test('responsive layout on mobile', async ({ page }) => {
|
|
489
|
-
await page.setViewportSize({ width: 375, height: 667 });
|
|
490
|
-
await page.goto('/');
|
|
491
|
-
|
|
492
|
-
await expect(page).toHaveScreenshot('home-mobile.png');
|
|
152
|
+
// Verify no changes persisted
|
|
153
|
+
expect(await userService.getBalance(from)).toBe(originalBalance);
|
|
493
154
|
});
|
|
494
155
|
});
|
|
495
156
|
```
|
|
496
157
|
|
|
497
|
-
###
|
|
158
|
+
### Performance Testing
|
|
498
159
|
|
|
499
160
|
```typescript
|
|
500
|
-
/**
|
|
501
|
-
* Performance Tests: Ensure system meets performance requirements
|
|
502
|
-
*/
|
|
503
|
-
|
|
504
|
-
import { describe, it, expect } from 'vitest';
|
|
505
|
-
|
|
506
|
-
// Response Time Testing
|
|
507
161
|
describe('API Performance', () => {
|
|
508
|
-
it('
|
|
509
|
-
const iterations = 100;
|
|
162
|
+
it('responds within SLA', async () => {
|
|
510
163
|
const times: number[] = [];
|
|
511
|
-
|
|
512
|
-
for (let i = 0; i < iterations; i++) {
|
|
164
|
+
for (let i = 0; i < 100; i++) {
|
|
513
165
|
const start = performance.now();
|
|
514
166
|
await api.get('/users');
|
|
515
167
|
times.push(performance.now() - start);
|
|
516
168
|
}
|
|
517
169
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
expect(p50).toBeLessThan(50); // Median under 50ms
|
|
523
|
-
expect(p95).toBeLessThan(100); // 95th percentile under 100ms
|
|
524
|
-
expect(p99).toBeLessThan(200); // 99th percentile under 200ms
|
|
170
|
+
expect(percentile(times, 50)).toBeLessThan(50); // p50 < 50ms
|
|
171
|
+
expect(percentile(times, 95)).toBeLessThan(100); // p95 < 100ms
|
|
172
|
+
expect(percentile(times, 99)).toBeLessThan(200); // p99 < 200ms
|
|
525
173
|
});
|
|
526
174
|
|
|
527
|
-
it('handles concurrent
|
|
528
|
-
const
|
|
529
|
-
const requests = Array(concurrency).fill(null).map(() =>
|
|
530
|
-
api.get('/users')
|
|
531
|
-
);
|
|
532
|
-
|
|
533
|
-
const start = performance.now();
|
|
175
|
+
it('handles concurrent load', async () => {
|
|
176
|
+
const requests = Array(50).fill(null).map(() => api.get('/users'));
|
|
534
177
|
const responses = await Promise.all(requests);
|
|
535
|
-
const duration = performance.now() - start;
|
|
536
178
|
|
|
537
|
-
// All should succeed
|
|
538
179
|
expect(responses.every(r => r.status === 200)).toBe(true);
|
|
539
|
-
// Should complete in reasonable time
|
|
540
|
-
expect(duration).toBeLessThan(1000);
|
|
541
180
|
});
|
|
542
181
|
});
|
|
543
|
-
|
|
544
|
-
// Memory Leak Detection
|
|
545
|
-
describe('Memory Stability', () => {
|
|
546
|
-
it('does not leak memory over many operations', async () => {
|
|
547
|
-
const initialMemory = process.memoryUsage().heapUsed;
|
|
548
|
-
|
|
549
|
-
// Perform many operations
|
|
550
|
-
for (let i = 0; i < 10000; i++) {
|
|
551
|
-
await processData(generateLargePayload());
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
// Force garbage collection if available
|
|
555
|
-
if (global.gc) global.gc();
|
|
556
|
-
|
|
557
|
-
const finalMemory = process.memoryUsage().heapUsed;
|
|
558
|
-
const growth = finalMemory - initialMemory;
|
|
559
|
-
|
|
560
|
-
// Memory should not grow significantly (< 10MB)
|
|
561
|
-
expect(growth).toBeLessThan(10 * 1024 * 1024);
|
|
562
|
-
});
|
|
563
|
-
});
|
|
564
|
-
|
|
565
|
-
// Load Testing Configuration
|
|
566
|
-
const loadTestConfig = {
|
|
567
|
-
scenarios: {
|
|
568
|
-
normalLoad: {
|
|
569
|
-
executor: 'ramping-vus',
|
|
570
|
-
startVUs: 0,
|
|
571
|
-
stages: [
|
|
572
|
-
{ duration: '2m', target: 100 }, // Ramp up
|
|
573
|
-
{ duration: '5m', target: 100 }, // Steady state
|
|
574
|
-
{ duration: '2m', target: 0 } // Ramp down
|
|
575
|
-
],
|
|
576
|
-
gracefulRampDown: '30s'
|
|
577
|
-
},
|
|
578
|
-
|
|
579
|
-
stressTest: {
|
|
580
|
-
executor: 'ramping-vus',
|
|
581
|
-
startVUs: 0,
|
|
582
|
-
stages: [
|
|
583
|
-
{ duration: '2m', target: 200 },
|
|
584
|
-
{ duration: '5m', target: 200 },
|
|
585
|
-
{ duration: '2m', target: 400 }, // Push beyond normal
|
|
586
|
-
{ duration: '5m', target: 400 },
|
|
587
|
-
{ duration: '2m', target: 0 }
|
|
588
|
-
]
|
|
589
|
-
},
|
|
590
|
-
|
|
591
|
-
spikeTest: {
|
|
592
|
-
executor: 'ramping-vus',
|
|
593
|
-
startVUs: 0,
|
|
594
|
-
stages: [
|
|
595
|
-
{ duration: '10s', target: 500 }, // Sudden spike
|
|
596
|
-
{ duration: '1m', target: 500 },
|
|
597
|
-
{ duration: '10s', target: 0 }
|
|
598
|
-
]
|
|
599
|
-
}
|
|
600
|
-
},
|
|
601
|
-
|
|
602
|
-
thresholds: {
|
|
603
|
-
http_req_duration: ['p(95)<200', 'p(99)<500'],
|
|
604
|
-
http_req_failed: ['rate<0.01'],
|
|
605
|
-
http_reqs: ['rate>100']
|
|
606
|
-
}
|
|
607
|
-
};
|
|
608
182
|
```
|
|
609
183
|
|
|
610
|
-
###
|
|
184
|
+
### Security Testing
|
|
611
185
|
|
|
612
186
|
```typescript
|
|
613
|
-
/**
|
|
614
|
-
* Security Tests: Verify protection against common attacks
|
|
615
|
-
*/
|
|
616
|
-
|
|
617
187
|
describe('Security Tests', () => {
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
const sqlInjectionPayloads = [
|
|
621
|
-
"'; DROP TABLE users; --",
|
|
622
|
-
"' OR '1'='1",
|
|
623
|
-
"'; INSERT INTO users VALUES ('hacker', 'hacked'); --",
|
|
624
|
-
"1; UPDATE users SET role='admin' WHERE id=1; --"
|
|
625
|
-
];
|
|
626
|
-
|
|
627
|
-
it.each(sqlInjectionPayloads)(
|
|
628
|
-
'safely handles SQL injection attempt: %s',
|
|
629
|
-
async (payload) => {
|
|
630
|
-
const response = await api.get(`/users?search=${encodeURIComponent(payload)}`);
|
|
631
|
-
|
|
632
|
-
// Should not error (indicates parameterized queries)
|
|
633
|
-
expect(response.status).not.toBe(500);
|
|
634
|
-
|
|
635
|
-
// Verify database integrity
|
|
636
|
-
const users = await db.query('SELECT * FROM users');
|
|
637
|
-
expect(users).toBeDefined();
|
|
638
|
-
}
|
|
639
|
-
);
|
|
640
|
-
});
|
|
188
|
+
const sqlPayloads = ["'; DROP TABLE users; --", "' OR '1'='1"];
|
|
189
|
+
const xssPayloads = ['<script>alert("xss")</script>', '<img onerror=alert(1)>'];
|
|
641
190
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
'<img src=x onerror=alert("xss")>',
|
|
647
|
-
'"><script>alert(document.cookie)</script>',
|
|
648
|
-
"javascript:alert('xss')"
|
|
649
|
-
];
|
|
650
|
-
|
|
651
|
-
it.each(xssPayloads)(
|
|
652
|
-
'escapes XSS payload: %s',
|
|
653
|
-
async (payload) => {
|
|
654
|
-
// Create content with XSS payload
|
|
655
|
-
await api.post('/posts', { body: { content: payload } });
|
|
656
|
-
|
|
657
|
-
// Retrieve and verify it's escaped
|
|
658
|
-
const response = await api.get('/posts');
|
|
659
|
-
const html = response.body.posts[0].content;
|
|
660
|
-
|
|
661
|
-
expect(html).not.toContain('<script>');
|
|
662
|
-
expect(html).not.toContain('onerror=');
|
|
663
|
-
expect(html).not.toContain('javascript:');
|
|
664
|
-
}
|
|
665
|
-
);
|
|
191
|
+
it.each(sqlPayloads)('prevents SQL injection: %s', async (payload) => {
|
|
192
|
+
const response = await api.get(`/users?search=${encodeURIComponent(payload)}`);
|
|
193
|
+
expect(response.status).not.toBe(500);
|
|
194
|
+
expect(await db.query('SELECT * FROM users')).toBeDefined();
|
|
666
195
|
});
|
|
667
196
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
const responses: Response[] = [];
|
|
673
|
-
|
|
674
|
-
for (let i = 0; i < attempts; i++) {
|
|
675
|
-
responses.push(await api.post('/login', {
|
|
676
|
-
body: { email: 'test@example.com', password: 'wrong' }
|
|
677
|
-
}));
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
// Later attempts should be rate limited
|
|
681
|
-
const rateLimited = responses.filter(r => r.status === 429);
|
|
682
|
-
expect(rateLimited.length).toBeGreaterThan(0);
|
|
683
|
-
});
|
|
684
|
-
|
|
685
|
-
it('uses secure session cookies', async () => {
|
|
686
|
-
const response = await api.post('/login', {
|
|
687
|
-
body: { email: 'test@example.com', password: 'correct' }
|
|
688
|
-
});
|
|
689
|
-
|
|
690
|
-
const sessionCookie = response.headers['set-cookie'];
|
|
691
|
-
expect(sessionCookie).toContain('HttpOnly');
|
|
692
|
-
expect(sessionCookie).toContain('Secure');
|
|
693
|
-
expect(sessionCookie).toContain('SameSite');
|
|
694
|
-
});
|
|
695
|
-
|
|
696
|
-
it('invalidates session on logout', async () => {
|
|
697
|
-
const loginResponse = await api.post('/login', {
|
|
698
|
-
body: { email: 'test@example.com', password: 'correct' }
|
|
699
|
-
});
|
|
700
|
-
const sessionToken = extractSessionToken(loginResponse);
|
|
701
|
-
|
|
702
|
-
await api.post('/logout', { headers: { Cookie: sessionToken } });
|
|
703
|
-
|
|
704
|
-
// Old session should be invalid
|
|
705
|
-
const response = await api.get('/me', {
|
|
706
|
-
headers: { Cookie: sessionToken }
|
|
707
|
-
});
|
|
708
|
-
expect(response.status).toBe(401);
|
|
709
|
-
});
|
|
197
|
+
it.each(xssPayloads)('escapes XSS payload: %s', async (payload) => {
|
|
198
|
+
await api.post('/posts', { content: payload });
|
|
199
|
+
const html = (await api.get('/posts')).body.posts[0].content;
|
|
200
|
+
expect(html).not.toContain('<script>');
|
|
710
201
|
});
|
|
711
202
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
// User 1 tries to access User 2's data
|
|
719
|
-
const response = await api.get('/users/user2/private-data', {
|
|
720
|
-
headers: { Authorization: `Bearer ${user1Token}` }
|
|
721
|
-
});
|
|
722
|
-
|
|
723
|
-
expect(response.status).toBe(403);
|
|
724
|
-
});
|
|
725
|
-
|
|
726
|
-
it('admin routes require admin role', async () => {
|
|
727
|
-
const userToken = await loginAs('regular-user');
|
|
728
|
-
|
|
729
|
-
const response = await api.get('/admin/users', {
|
|
730
|
-
headers: { Authorization: `Bearer ${userToken}` }
|
|
731
|
-
});
|
|
732
|
-
|
|
733
|
-
expect(response.status).toBe(403);
|
|
734
|
-
});
|
|
203
|
+
it('rate limits login attempts', async () => {
|
|
204
|
+
for (let i = 0; i < 10; i++) {
|
|
205
|
+
await api.post('/login', { email: 'x', password: 'wrong' });
|
|
206
|
+
}
|
|
207
|
+
const response = await api.post('/login', { email: 'x', password: 'wrong' });
|
|
208
|
+
expect(response.status).toBe(429);
|
|
735
209
|
});
|
|
736
210
|
});
|
|
737
211
|
```
|
|
738
212
|
|
|
739
|
-
###
|
|
213
|
+
### Accessibility Testing
|
|
740
214
|
|
|
741
215
|
```typescript
|
|
742
|
-
/**
|
|
743
|
-
* Accessibility Tests: Ensure usability for all users
|
|
744
|
-
*/
|
|
745
|
-
|
|
746
|
-
import { test, expect } from '@playwright/test';
|
|
747
|
-
import AxeBuilder from '@axe-core/playwright';
|
|
748
|
-
|
|
749
216
|
test.describe('Accessibility', () => {
|
|
750
|
-
|
|
751
|
-
test('home page has no accessibility violations', async ({ page }) => {
|
|
217
|
+
test('page has no WCAG violations', async ({ page }) => {
|
|
752
218
|
await page.goto('/');
|
|
753
|
-
|
|
754
219
|
const results = await new AxeBuilder({ page })
|
|
755
220
|
.withTags(['wcag2a', 'wcag2aa', 'wcag21aa'])
|
|
756
221
|
.analyze();
|
|
757
|
-
|
|
758
222
|
expect(results.violations).toEqual([]);
|
|
759
223
|
});
|
|
760
224
|
|
|
761
|
-
|
|
762
|
-
test('all interactive elements are keyboard accessible', async ({ page }) => {
|
|
225
|
+
test('keyboard navigation works', async ({ page }) => {
|
|
763
226
|
await page.goto('/');
|
|
227
|
+
const focusable = 'a, button, input, [tabindex]:not([tabindex="-1"])';
|
|
228
|
+
const elements = await page.locator(focusable).all();
|
|
764
229
|
|
|
765
|
-
|
|
766
|
-
const focusableSelectors = 'a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])';
|
|
767
|
-
const elements = await page.locator(focusableSelectors).all();
|
|
768
|
-
|
|
769
|
-
for (const element of elements) {
|
|
230
|
+
for (const _ of elements) {
|
|
770
231
|
await page.keyboard.press('Tab');
|
|
771
232
|
const focused = await page.evaluate(() => document.activeElement?.tagName);
|
|
772
233
|
expect(focused).toBeDefined();
|
|
773
234
|
}
|
|
774
235
|
});
|
|
775
236
|
|
|
776
|
-
// Focus Management
|
|
777
|
-
test('modal traps focus correctly', async ({ page }) => {
|
|
778
|
-
await page.goto('/');
|
|
779
|
-
await page.click('[data-testid="open-modal"]');
|
|
780
|
-
|
|
781
|
-
// Focus should be on modal
|
|
782
|
-
const modalFocused = await page.locator('[data-testid="modal"]').evaluate(
|
|
783
|
-
el => el.contains(document.activeElement)
|
|
784
|
-
);
|
|
785
|
-
expect(modalFocused).toBe(true);
|
|
786
|
-
|
|
787
|
-
// Tab should stay within modal
|
|
788
|
-
for (let i = 0; i < 10; i++) {
|
|
789
|
-
await page.keyboard.press('Tab');
|
|
790
|
-
const stillInModal = await page.locator('[data-testid="modal"]').evaluate(
|
|
791
|
-
el => el.contains(document.activeElement)
|
|
792
|
-
);
|
|
793
|
-
expect(stillInModal).toBe(true);
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
// Escape closes modal
|
|
797
|
-
await page.keyboard.press('Escape');
|
|
798
|
-
await expect(page.locator('[data-testid="modal"]')).not.toBeVisible();
|
|
799
|
-
});
|
|
800
|
-
|
|
801
|
-
// Screen Reader Compatibility
|
|
802
237
|
test('images have alt text', async ({ page }) => {
|
|
803
|
-
await page.goto('/');
|
|
804
|
-
|
|
805
238
|
const images = await page.locator('img').all();
|
|
806
239
|
for (const img of images) {
|
|
807
|
-
|
|
808
|
-
expect(alt).toBeDefined();
|
|
809
|
-
expect(alt?.length).toBeGreaterThan(0);
|
|
240
|
+
expect(await img.getAttribute('alt')).toBeTruthy();
|
|
810
241
|
}
|
|
811
242
|
});
|
|
812
|
-
|
|
813
|
-
// Color Contrast
|
|
814
|
-
test('text has sufficient contrast', async ({ page }) => {
|
|
815
|
-
await page.goto('/');
|
|
816
|
-
|
|
817
|
-
const results = await new AxeBuilder({ page })
|
|
818
|
-
.withRules(['color-contrast'])
|
|
819
|
-
.analyze();
|
|
820
|
-
|
|
821
|
-
expect(results.violations).toEqual([]);
|
|
822
|
-
});
|
|
823
|
-
|
|
824
|
-
// Reduced Motion
|
|
825
|
-
test('respects prefers-reduced-motion', async ({ page }) => {
|
|
826
|
-
await page.emulateMedia({ reducedMotion: 'reduce' });
|
|
827
|
-
await page.goto('/');
|
|
828
|
-
|
|
829
|
-
// Check animations are disabled
|
|
830
|
-
const animatedElement = page.locator('[data-animated]');
|
|
831
|
-
const animationDuration = await animatedElement.evaluate(el =>
|
|
832
|
-
getComputedStyle(el).animationDuration
|
|
833
|
-
);
|
|
834
|
-
|
|
835
|
-
expect(animationDuration).toBe('0s');
|
|
836
|
-
});
|
|
837
243
|
});
|
|
838
244
|
```
|
|
839
245
|
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
### Testing a New Feature
|
|
246
|
+
### E2E Critical Path
|
|
843
247
|
|
|
844
248
|
```typescript
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
*/
|
|
848
|
-
|
|
849
|
-
// Unit Tests
|
|
850
|
-
describe('ProfileService', () => {
|
|
851
|
-
it('validates profile data', () => {
|
|
852
|
-
expect(validateProfile({ name: '' })).toEqual({
|
|
853
|
-
valid: false,
|
|
854
|
-
errors: ['Name is required']
|
|
855
|
-
});
|
|
856
|
-
});
|
|
857
|
-
|
|
858
|
-
it('sanitizes bio field', () => {
|
|
859
|
-
const result = sanitizeProfile({
|
|
860
|
-
bio: '<script>alert("xss")</script>Hello'
|
|
861
|
-
});
|
|
862
|
-
expect(result.bio).toBe('Hello');
|
|
863
|
-
});
|
|
864
|
-
});
|
|
865
|
-
|
|
866
|
-
// Integration Tests
|
|
867
|
-
describe('Profile API', () => {
|
|
868
|
-
it('updates profile in database', async () => {
|
|
869
|
-
const response = await api.put('/profile', {
|
|
870
|
-
body: { name: 'New Name', bio: 'New bio' }
|
|
871
|
-
});
|
|
872
|
-
|
|
873
|
-
expect(response.status).toBe(200);
|
|
874
|
-
|
|
875
|
-
const dbProfile = await db.profiles.findById(userId);
|
|
876
|
-
expect(dbProfile.name).toBe('New Name');
|
|
877
|
-
});
|
|
878
|
-
});
|
|
879
|
-
|
|
880
|
-
// E2E Tests
|
|
881
|
-
test('user can update their profile', async ({ page }) => {
|
|
249
|
+
test('complete purchase flow', async ({ page }) => {
|
|
250
|
+
// Login
|
|
882
251
|
await loginAsTestUser(page);
|
|
883
|
-
await page.goto('/settings/profile');
|
|
884
|
-
await page.fill('[data-testid="name"]', 'Updated Name');
|
|
885
|
-
await page.click('[data-testid="save"]');
|
|
886
252
|
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
253
|
+
// Add to cart
|
|
254
|
+
await page.goto('/products/test-product');
|
|
255
|
+
await page.click('[data-testid="add-to-cart"]');
|
|
890
256
|
|
|
891
|
-
|
|
257
|
+
// Checkout
|
|
258
|
+
await page.click('[data-testid="checkout-button"]');
|
|
259
|
+
await page.fill('[data-testid="card-number"]', '4242424242424242');
|
|
260
|
+
await page.click('[data-testid="place-order"]');
|
|
892
261
|
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
jobs:
|
|
900
|
-
unit-tests:
|
|
901
|
-
runs-on: ubuntu-latest
|
|
902
|
-
steps:
|
|
903
|
-
- uses: actions/checkout@v4
|
|
904
|
-
- name: Run unit tests
|
|
905
|
-
run: npm run test:unit -- --coverage
|
|
906
|
-
- name: Upload coverage
|
|
907
|
-
uses: codecov/codecov-action@v4
|
|
908
|
-
|
|
909
|
-
integration-tests:
|
|
910
|
-
runs-on: ubuntu-latest
|
|
911
|
-
services:
|
|
912
|
-
postgres:
|
|
913
|
-
image: postgres:15
|
|
914
|
-
env:
|
|
915
|
-
POSTGRES_PASSWORD: test
|
|
916
|
-
steps:
|
|
917
|
-
- uses: actions/checkout@v4
|
|
918
|
-
- name: Run integration tests
|
|
919
|
-
run: npm run test:integration
|
|
920
|
-
|
|
921
|
-
e2e-tests:
|
|
922
|
-
runs-on: ubuntu-latest
|
|
923
|
-
steps:
|
|
924
|
-
- uses: actions/checkout@v4
|
|
925
|
-
- name: Install Playwright
|
|
926
|
-
run: npx playwright install --with-deps
|
|
927
|
-
- name: Run E2E tests
|
|
928
|
-
run: npm run test:e2e
|
|
929
|
-
- uses: actions/upload-artifact@v4
|
|
930
|
-
if: failure()
|
|
931
|
-
with:
|
|
932
|
-
name: playwright-report
|
|
933
|
-
path: playwright-report/
|
|
262
|
+
// Verify
|
|
263
|
+
await expect(page).toHaveURL(/\/orders\/[a-z0-9-]+/);
|
|
264
|
+
await expect(page.locator('[data-testid="order-status"]'))
|
|
265
|
+
.toHaveText('Order Confirmed');
|
|
266
|
+
});
|
|
934
267
|
```
|
|
935
268
|
|
|
936
269
|
## Best Practices
|
|
937
270
|
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
### Don'ts
|
|
952
|
-
|
|
953
|
-
- Don't test implementation details - test behavior
|
|
954
|
-
- Don't write flaky tests - fix or delete them
|
|
955
|
-
- Don't skip tests without documented reason
|
|
956
|
-
- Don't test framework code - trust your dependencies
|
|
957
|
-
- Don't use sleep/delays - use proper async handling
|
|
958
|
-
- Don't hardcode test data - use factories
|
|
959
|
-
- Don't ignore failing tests - fix them immediately
|
|
960
|
-
- Don't over-mock - some integration is valuable
|
|
961
|
-
- Don't write tests after bugs escape - prevent them
|
|
962
|
-
- Don't chase 100% coverage - chase meaningful coverage
|
|
963
|
-
|
|
964
|
-
## References
|
|
965
|
-
|
|
966
|
-
- [Testing Library](https://testing-library.com/)
|
|
967
|
-
- [Vitest Documentation](https://vitest.dev/)
|
|
968
|
-
- [Playwright Documentation](https://playwright.dev/)
|
|
969
|
-
- [OWASP Testing Guide](https://owasp.org/www-project-web-security-testing-guide/)
|
|
970
|
-
- [Web Content Accessibility Guidelines](https://www.w3.org/WAI/standards-guidelines/wcag/)
|
|
271
|
+
| Do | Avoid |
|
|
272
|
+
|----|-------|
|
|
273
|
+
| Test all four quality dimensions | Testing only happy paths |
|
|
274
|
+
| Follow the test pyramid (more units) | Relying heavily on E2E tests |
|
|
275
|
+
| Use descriptive test names | Testing implementation details |
|
|
276
|
+
| Test edge cases systematically | Writing flaky tests |
|
|
277
|
+
| Keep tests independent (no shared state) | Using sleep/delays for timing |
|
|
278
|
+
| Use factories for test data | Hardcoding test data |
|
|
279
|
+
| Mock external dependencies in unit tests | Over-mocking in integration tests |
|
|
280
|
+
| Run tests in CI on every commit | Ignoring failing tests |
|
|
281
|
+
| Fix flaky tests immediately | Skipping tests without reason |
|
|
282
|
+
| Chase meaningful coverage, not 100% | Testing framework code |
|