omgkit 2.1.1 → 2.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.
- package/package.json +1 -1
- package/plugin/skills/SKILL_STANDARDS.md +743 -0
- package/plugin/skills/databases/mongodb/SKILL.md +797 -28
- package/plugin/skills/databases/prisma/SKILL.md +776 -30
- package/plugin/skills/databases/redis/SKILL.md +885 -25
- package/plugin/skills/devops/aws/SKILL.md +686 -28
- package/plugin/skills/devops/github-actions/SKILL.md +684 -29
- package/plugin/skills/devops/kubernetes/SKILL.md +621 -24
- package/plugin/skills/frameworks/django/SKILL.md +920 -20
- package/plugin/skills/frameworks/express/SKILL.md +1361 -35
- package/plugin/skills/frameworks/fastapi/SKILL.md +1260 -33
- package/plugin/skills/frameworks/laravel/SKILL.md +1244 -31
- package/plugin/skills/frameworks/nestjs/SKILL.md +1005 -26
- package/plugin/skills/frameworks/rails/SKILL.md +594 -28
- package/plugin/skills/frameworks/spring/SKILL.md +528 -35
- package/plugin/skills/frameworks/vue/SKILL.md +1296 -27
- package/plugin/skills/frontend/accessibility/SKILL.md +1108 -34
- package/plugin/skills/frontend/frontend-design/SKILL.md +1304 -26
- package/plugin/skills/frontend/responsive/SKILL.md +847 -21
- package/plugin/skills/frontend/shadcn-ui/SKILL.md +976 -38
- package/plugin/skills/frontend/tailwindcss/SKILL.md +831 -35
- package/plugin/skills/frontend/threejs/SKILL.md +1298 -29
- package/plugin/skills/languages/javascript/SKILL.md +935 -31
- package/plugin/skills/methodology/brainstorming/SKILL.md +597 -23
- package/plugin/skills/methodology/defense-in-depth/SKILL.md +832 -34
- package/plugin/skills/methodology/dispatching-parallel-agents/SKILL.md +665 -31
- package/plugin/skills/methodology/executing-plans/SKILL.md +556 -24
- package/plugin/skills/methodology/finishing-development-branch/SKILL.md +595 -25
- package/plugin/skills/methodology/problem-solving/SKILL.md +429 -61
- package/plugin/skills/methodology/receiving-code-review/SKILL.md +536 -24
- package/plugin/skills/methodology/requesting-code-review/SKILL.md +632 -21
- package/plugin/skills/methodology/root-cause-tracing/SKILL.md +641 -30
- package/plugin/skills/methodology/sequential-thinking/SKILL.md +262 -3
- package/plugin/skills/methodology/systematic-debugging/SKILL.md +571 -32
- package/plugin/skills/methodology/test-driven-development/SKILL.md +779 -24
- package/plugin/skills/methodology/testing-anti-patterns/SKILL.md +691 -29
- package/plugin/skills/methodology/token-optimization/SKILL.md +598 -29
- package/plugin/skills/methodology/verification-before-completion/SKILL.md +543 -22
- package/plugin/skills/methodology/writing-plans/SKILL.md +590 -18
- package/plugin/skills/omega/omega-architecture/SKILL.md +838 -39
- package/plugin/skills/omega/omega-coding/SKILL.md +636 -39
- package/plugin/skills/omega/omega-sprint/SKILL.md +855 -48
- package/plugin/skills/omega/omega-testing/SKILL.md +940 -41
- package/plugin/skills/omega/omega-thinking/SKILL.md +703 -50
- package/plugin/skills/security/better-auth/SKILL.md +1065 -28
- package/plugin/skills/security/oauth/SKILL.md +968 -31
- package/plugin/skills/security/owasp/SKILL.md +894 -33
- package/plugin/skills/testing/playwright/SKILL.md +764 -38
- package/plugin/skills/testing/pytest/SKILL.md +873 -36
- package/plugin/skills/testing/vitest/SKILL.md +980 -35
|
@@ -1,71 +1,970 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: omega-testing
|
|
3
|
-
description: Comprehensive
|
|
3
|
+
description: Comprehensive testing strategies covering all dimensions - accuracy, performance, security, and accessibility
|
|
4
|
+
category: omega
|
|
5
|
+
triggers:
|
|
6
|
+
- omega testing
|
|
7
|
+
- comprehensive testing
|
|
8
|
+
- test strategy
|
|
9
|
+
- quality assurance
|
|
10
|
+
- test pyramid
|
|
11
|
+
- test coverage
|
|
12
|
+
- testing best practices
|
|
4
13
|
---
|
|
5
14
|
|
|
6
|
-
# Omega Testing
|
|
15
|
+
# Omega Testing
|
|
7
16
|
|
|
8
|
-
|
|
17
|
+
Master **comprehensive testing strategies** that cover all quality dimensions - accuracy, performance, security, and accessibility. This skill provides frameworks for building confidence in your code through systematic, thorough testing.
|
|
18
|
+
|
|
19
|
+
## Purpose
|
|
20
|
+
|
|
21
|
+
Achieve Omega-level quality assurance:
|
|
22
|
+
|
|
23
|
+
- Design test strategies that catch bugs before production
|
|
24
|
+
- Build layered test suites (unit, integration, E2E)
|
|
25
|
+
- Test all quality dimensions, not just functionality
|
|
26
|
+
- Create maintainable, fast, and reliable tests
|
|
27
|
+
- Implement property-based and mutation testing
|
|
28
|
+
- Achieve meaningful coverage metrics
|
|
29
|
+
- Enable confident refactoring and deployment
|
|
30
|
+
|
|
31
|
+
## Features
|
|
32
|
+
|
|
33
|
+
### 1. The Omega Testing Pyramid
|
|
34
|
+
|
|
35
|
+
```markdown
|
|
36
|
+
## Multi-Dimensional Testing Pyramid
|
|
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
|
+
└─────────────────────────────────────────────────────────────────────────┘
|
|
9
65
|
```
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
66
|
+
|
|
67
|
+
### 2. Four Dimensions of Quality Testing
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
/**
|
|
71
|
+
* Omega Testing covers ALL quality dimensions
|
|
72
|
+
* Don't just test functionality - test quality
|
|
73
|
+
*/
|
|
74
|
+
|
|
75
|
+
type QualityDimension =
|
|
76
|
+
| 'accuracy' // Does it work correctly?
|
|
77
|
+
| 'performance' // Does it work fast enough?
|
|
78
|
+
| 'security' // Is it safe from attacks?
|
|
79
|
+
| 'accessibility'; // Can everyone use it?
|
|
80
|
+
|
|
81
|
+
interface OmegaTestSuite {
|
|
82
|
+
accuracy: AccuracyTests;
|
|
83
|
+
performance: PerformanceTests;
|
|
84
|
+
security: SecurityTests;
|
|
85
|
+
accessibility: AccessibilityTests;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Dimension 1: Accuracy (Functional Correctness)
|
|
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
|
+
}
|
|
95
|
+
|
|
96
|
+
// Dimension 2: Performance
|
|
97
|
+
interface PerformanceTests {
|
|
98
|
+
responseTime: Test[]; // Operations complete in time
|
|
99
|
+
throughput: Test[]; // System handles expected load
|
|
100
|
+
memory: Test[]; // No memory leaks
|
|
101
|
+
scalability: Test[]; // Performance under scale
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Dimension 3: Security
|
|
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
|
+
}
|
|
111
|
+
|
|
112
|
+
// Dimension 4: Accessibility
|
|
113
|
+
interface AccessibilityTests {
|
|
114
|
+
screenReader: Test[]; // Works with assistive tech
|
|
115
|
+
keyboard: Test[]; // Keyboard navigation works
|
|
116
|
+
contrast: Test[]; // Visual contrast sufficient
|
|
117
|
+
motion: Test[]; // Respects motion preferences
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### 3. Unit Testing Patterns
|
|
122
|
+
|
|
123
|
+
```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)
|
|
131
|
+
describe('calculateDiscount', () => {
|
|
132
|
+
it('applies 10% discount for orders over $100', () => {
|
|
133
|
+
// Arrange
|
|
134
|
+
const order = createOrder({ subtotal: 150 });
|
|
135
|
+
const discountRules = createDiscountRules();
|
|
136
|
+
|
|
137
|
+
// Act
|
|
138
|
+
const result = calculateDiscount(order, discountRules);
|
|
139
|
+
|
|
140
|
+
// Assert
|
|
141
|
+
expect(result.discount).toBe(15);
|
|
142
|
+
expect(result.total).toBe(135);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Pattern 2: Testing Edge Cases Systematically
|
|
147
|
+
describe('validateEmail', () => {
|
|
148
|
+
// Happy path
|
|
149
|
+
it('accepts valid email formats', () => {
|
|
150
|
+
const validEmails = [
|
|
151
|
+
'user@example.com',
|
|
152
|
+
'user.name@example.co.uk',
|
|
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
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Pattern 4: Testing Pure Functions with Properties
|
|
225
|
+
describe('sortUsers (property-based)', () => {
|
|
226
|
+
it('output length equals input length', () => {
|
|
227
|
+
fc.assert(
|
|
228
|
+
fc.property(fc.array(fc.record({
|
|
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
|
+
});
|
|
260
|
+
});
|
|
17
261
|
```
|
|
18
262
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
263
|
+
### 4. Integration Testing Patterns
|
|
264
|
+
|
|
265
|
+
```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
|
+
describe('UserService Integration', () => {
|
|
274
|
+
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
|
+
|
|
330
|
+
it('rolls back on failure', async () => {
|
|
331
|
+
const sender = await userService.createUser({ credits: 100 });
|
|
332
|
+
const receiver = await userService.createUser({ credits: 0 });
|
|
333
|
+
|
|
334
|
+
// Simulate failure mid-transaction
|
|
335
|
+
vi.spyOn(db, 'commit').mockRejectedValueOnce(new Error('DB error'));
|
|
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);
|
|
349
|
+
});
|
|
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');
|
|
23
357
|
|
|
24
|
-
|
|
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
|
|
25
388
|
|
|
26
|
-
### 1. Happy Path
|
|
27
389
|
```typescript
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
+
|
|
440
|
+
// Critical Path: E-commerce Checkout
|
|
441
|
+
test.describe('Checkout Flow', () => {
|
|
442
|
+
test.beforeEach(async ({ page }) => {
|
|
443
|
+
// Login as test user
|
|
444
|
+
await loginAsTestUser(page);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
test('complete purchase flow', async ({ page }) => {
|
|
448
|
+
// Add item to cart
|
|
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
|
+
});
|
|
475
|
+
|
|
476
|
+
// Visual Regression Testing
|
|
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');
|
|
482
|
+
|
|
483
|
+
await expect(page).toHaveScreenshot('dashboard.png', {
|
|
484
|
+
maxDiffPixelRatio: 0.01
|
|
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');
|
|
493
|
+
});
|
|
31
494
|
});
|
|
32
495
|
```
|
|
33
496
|
|
|
34
|
-
###
|
|
497
|
+
### 6. Performance Testing
|
|
498
|
+
|
|
35
499
|
```typescript
|
|
36
|
-
|
|
37
|
-
|
|
500
|
+
/**
|
|
501
|
+
* Performance Tests: Ensure system meets performance requirements
|
|
502
|
+
*/
|
|
503
|
+
|
|
504
|
+
import { describe, it, expect } from 'vitest';
|
|
505
|
+
|
|
506
|
+
// Response Time Testing
|
|
507
|
+
describe('API Performance', () => {
|
|
508
|
+
it('GET /users responds within 100ms', async () => {
|
|
509
|
+
const iterations = 100;
|
|
510
|
+
const times: number[] = [];
|
|
511
|
+
|
|
512
|
+
for (let i = 0; i < iterations; i++) {
|
|
513
|
+
const start = performance.now();
|
|
514
|
+
await api.get('/users');
|
|
515
|
+
times.push(performance.now() - start);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const p50 = percentile(times, 50);
|
|
519
|
+
const p95 = percentile(times, 95);
|
|
520
|
+
const p99 = percentile(times, 99);
|
|
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
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it('handles concurrent requests', async () => {
|
|
528
|
+
const concurrency = 50;
|
|
529
|
+
const requests = Array(concurrency).fill(null).map(() =>
|
|
530
|
+
api.get('/users')
|
|
531
|
+
);
|
|
532
|
+
|
|
533
|
+
const start = performance.now();
|
|
534
|
+
const responses = await Promise.all(requests);
|
|
535
|
+
const duration = performance.now() - start;
|
|
536
|
+
|
|
537
|
+
// All should succeed
|
|
538
|
+
expect(responses.every(r => r.status === 200)).toBe(true);
|
|
539
|
+
// Should complete in reasonable time
|
|
540
|
+
expect(duration).toBeLessThan(1000);
|
|
541
|
+
});
|
|
38
542
|
});
|
|
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
|
+
};
|
|
39
608
|
```
|
|
40
609
|
|
|
41
|
-
###
|
|
610
|
+
### 7. Security Testing
|
|
611
|
+
|
|
42
612
|
```typescript
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
613
|
+
/**
|
|
614
|
+
* Security Tests: Verify protection against common attacks
|
|
615
|
+
*/
|
|
616
|
+
|
|
617
|
+
describe('Security Tests', () => {
|
|
618
|
+
// SQL Injection Prevention
|
|
619
|
+
describe('SQL Injection', () => {
|
|
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
|
+
});
|
|
641
|
+
|
|
642
|
+
// XSS Prevention
|
|
643
|
+
describe('XSS Prevention', () => {
|
|
644
|
+
const xssPayloads = [
|
|
645
|
+
'<script>alert("xss")</script>',
|
|
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
|
+
);
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
// Authentication Security
|
|
669
|
+
describe('Authentication', () => {
|
|
670
|
+
it('rate limits login attempts', async () => {
|
|
671
|
+
const attempts = 10;
|
|
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
|
+
});
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
// Authorization Testing
|
|
713
|
+
describe('Authorization', () => {
|
|
714
|
+
it('prevents accessing other users data', async () => {
|
|
715
|
+
const user1Token = await loginAs('user1');
|
|
716
|
+
const user2Token = await loginAs('user2');
|
|
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
|
+
});
|
|
735
|
+
});
|
|
46
736
|
});
|
|
47
737
|
```
|
|
48
738
|
|
|
49
|
-
###
|
|
739
|
+
### 8. Accessibility Testing
|
|
740
|
+
|
|
50
741
|
```typescript
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
+
test.describe('Accessibility', () => {
|
|
750
|
+
// Automated WCAG Compliance
|
|
751
|
+
test('home page has no accessibility violations', async ({ page }) => {
|
|
752
|
+
await page.goto('/');
|
|
753
|
+
|
|
754
|
+
const results = await new AxeBuilder({ page })
|
|
755
|
+
.withTags(['wcag2a', 'wcag2aa', 'wcag21aa'])
|
|
756
|
+
.analyze();
|
|
757
|
+
|
|
758
|
+
expect(results.violations).toEqual([]);
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
// Keyboard Navigation
|
|
762
|
+
test('all interactive elements are keyboard accessible', async ({ page }) => {
|
|
763
|
+
await page.goto('/');
|
|
764
|
+
|
|
765
|
+
// Tab through all focusable elements
|
|
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) {
|
|
770
|
+
await page.keyboard.press('Tab');
|
|
771
|
+
const focused = await page.evaluate(() => document.activeElement?.tagName);
|
|
772
|
+
expect(focused).toBeDefined();
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
|
|
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
|
+
test('images have alt text', async ({ page }) => {
|
|
803
|
+
await page.goto('/');
|
|
804
|
+
|
|
805
|
+
const images = await page.locator('img').all();
|
|
806
|
+
for (const img of images) {
|
|
807
|
+
const alt = await img.getAttribute('alt');
|
|
808
|
+
expect(alt).toBeDefined();
|
|
809
|
+
expect(alt?.length).toBeGreaterThan(0);
|
|
810
|
+
}
|
|
811
|
+
});
|
|
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
|
+
});
|
|
55
837
|
});
|
|
56
838
|
```
|
|
57
839
|
|
|
58
|
-
|
|
840
|
+
## Use Cases
|
|
841
|
+
|
|
842
|
+
### Testing a New Feature
|
|
843
|
+
|
|
59
844
|
```typescript
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
845
|
+
/**
|
|
846
|
+
* Complete test suite for a new user profile feature
|
|
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
|
+
});
|
|
63
878
|
});
|
|
879
|
+
|
|
880
|
+
// E2E Tests
|
|
881
|
+
test('user can update their profile', async ({ page }) => {
|
|
882
|
+
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
|
+
|
|
887
|
+
await expect(page.locator('[data-testid="success"]')).toBeVisible();
|
|
888
|
+
});
|
|
889
|
+
```
|
|
890
|
+
|
|
891
|
+
### CI/CD Test Configuration
|
|
892
|
+
|
|
893
|
+
```yaml
|
|
894
|
+
# .github/workflows/test.yml
|
|
895
|
+
name: Test Suite
|
|
896
|
+
|
|
897
|
+
on: [push, pull_request]
|
|
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/
|
|
64
934
|
```
|
|
65
935
|
|
|
66
|
-
##
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
-
|
|
71
|
-
-
|
|
936
|
+
## Best Practices
|
|
937
|
+
|
|
938
|
+
### Do's
|
|
939
|
+
|
|
940
|
+
- **Test all four quality dimensions** (accuracy, performance, security, accessibility)
|
|
941
|
+
- **Follow the test pyramid** - more unit tests, fewer E2E tests
|
|
942
|
+
- **Use descriptive test names** that explain the expected behavior
|
|
943
|
+
- **Test edge cases and error conditions** systematically
|
|
944
|
+
- **Keep tests independent** - no shared state between tests
|
|
945
|
+
- **Use appropriate assertions** that give helpful error messages
|
|
946
|
+
- **Mock external dependencies** in unit tests
|
|
947
|
+
- **Test real integrations** in integration tests
|
|
948
|
+
- **Run tests in CI/CD** on every commit
|
|
949
|
+
- **Maintain test data factories** for consistent test data
|
|
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/)
|