start-vibing 1.1.2 → 1.1.4
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/template/.claude/CLAUDE.md +129 -168
- package/template/.claude/agents/analyzer.md +0 -14
- package/template/.claude/agents/commit-manager.md +0 -19
- package/template/.claude/agents/documenter.md +0 -10
- package/template/.claude/agents/domain-updater.md +194 -200
- package/template/.claude/agents/final-validator.md +0 -18
- package/template/.claude/agents/orchestrator.md +36 -34
- package/template/.claude/agents/quality-checker.md +0 -24
- package/template/.claude/agents/research.md +299 -262
- package/template/.claude/agents/security-auditor.md +1 -14
- package/template/.claude/agents/tester.md +0 -8
- package/template/.claude/agents/ui-ux-reviewer.md +80 -18
- package/template/.claude/commands/feature.md +48 -102
- package/template/.claude/config/README.md +30 -30
- package/template/.claude/config/project-config.json +53 -53
- package/template/.claude/config/quality-gates.json +46 -46
- package/template/.claude/config/security-rules.json +45 -45
- package/template/.claude/config/testing-config.json +168 -168
- package/template/.claude/hooks/SETUP.md +52 -181
- package/template/.claude/hooks/user-prompt-submit.py +184 -46
- package/template/.claude/settings.json +0 -39
- package/template/.claude/skills/codebase-knowledge/SKILL.md +145 -145
- package/template/.claude/skills/codebase-knowledge/domains/claude-system.md +260 -321
- package/template/.claude/skills/docs-tracker/SKILL.md +239 -239
- package/template/.claude/skills/final-check/SKILL.md +284 -284
- package/template/.claude/skills/quality-gate/SKILL.md +278 -278
- package/template/.claude/skills/research-cache/SKILL.md +207 -207
- package/template/.claude/skills/security-scan/SKILL.md +206 -206
- package/template/.claude/skills/test-coverage/SKILL.md +441 -441
- package/template/.claude/skills/ui-ux-audit/SKILL.md +254 -254
- package/template/.claude/config/domain-mapping.json +0 -26
- package/template/.claude/hooks/post-tool-use.py +0 -155
- package/template/.claude/hooks/pre-tool-use.py +0 -159
- package/template/.claude/hooks/stop-validation.py +0 -155
- package/template/.claude/hooks/validate-commit.py +0 -200
- package/template/.claude/hooks/workflow-manager.py +0 -350
- package/template/.claude/workflow-state.schema.json +0 -200
|
@@ -1,441 +1,441 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: test-coverage
|
|
3
|
-
description: Manages test coverage with Playwright E2E and Vitest unit tests. Tracks which files need tests, provides templates with fixture-based cleanup, enforces multi-viewport testing and database validation.
|
|
4
|
-
allowed-tools: Read, Write, Edit, Bash, Grep, Glob
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# Test Coverage - Testing Management System
|
|
8
|
-
|
|
9
|
-
## Purpose
|
|
10
|
-
|
|
11
|
-
This skill manages test coverage with **Playwright E2E** and **Vitest unit tests**:
|
|
12
|
-
|
|
13
|
-
- **Detects** new files that need tests
|
|
14
|
-
- **Maps** components consuming API routes (tRPC/REST)
|
|
15
|
-
- **Tracks** which pages have E2E coverage
|
|
16
|
-
- **Generates** test templates with cleanup
|
|
17
|
-
- **Validates** real authentication usage (MANDATORY)
|
|
18
|
-
- **Ensures** database validation after UI actions
|
|
19
|
-
- **Enforces** multi-viewport testing
|
|
20
|
-
|
|
21
|
-
---
|
|
22
|
-
|
|
23
|
-
## CRITICAL RULES
|
|
24
|
-
|
|
25
|
-
> **MANDATORY - NO EXCEPTIONS:**
|
|
26
|
-
|
|
27
|
-
1. **CLEANUP ALL TEST DATA** - Use fixture-based tracking
|
|
28
|
-
2. **VERIFY IN DATABASE** - Check DB state after UI actions
|
|
29
|
-
3. **TEST ALL VIEWPORTS** - Desktop, tablet, iPhone SE minimum
|
|
30
|
-
4. **REAL AUTH ONLY** - Never mock authentication
|
|
31
|
-
5. **UNIQUE DATA** - Timestamps in emails/names
|
|
32
|
-
6. **NO SKIP** - Never use `.skip()` or `.only()`
|
|
33
|
-
|
|
34
|
-
---
|
|
35
|
-
|
|
36
|
-
## Test Structure
|
|
37
|
-
|
|
38
|
-
```
|
|
39
|
-
tests/
|
|
40
|
-
├── unit/ # Unit tests (Vitest)
|
|
41
|
-
│ └── *.test.ts
|
|
42
|
-
└── e2e/ # E2E tests (Playwright)
|
|
43
|
-
├── fixtures/
|
|
44
|
-
│ ├── index.ts # Custom fixtures (auth, db, cleanup)
|
|
45
|
-
│ ├── auth.fixture.ts # Authentication helpers
|
|
46
|
-
│ └── db.fixture.ts # Database connection & cleanup
|
|
47
|
-
├── pages/ # Page Object Model
|
|
48
|
-
│ ├── base.page.ts # Base page with common methods
|
|
49
|
-
│ ├── login.page.ts
|
|
50
|
-
│ └── register.page.ts
|
|
51
|
-
├── flows/ # User flow tests
|
|
52
|
-
│ ├── auth.spec.ts # Login, register, logout
|
|
53
|
-
│ ├── crud.spec.ts # Create, read, update, delete
|
|
54
|
-
│ └── permissions.spec.ts
|
|
55
|
-
├── api/ # API-only tests (no UI)
|
|
56
|
-
│ ├── rest.spec.ts # REST API tests
|
|
57
|
-
│ └── trpc.spec.ts # tRPC API tests
|
|
58
|
-
└── playwright.config.ts
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
---
|
|
62
|
-
|
|
63
|
-
## Cleanup Fixture (MANDATORY)
|
|
64
|
-
|
|
65
|
-
Every project MUST have cleanup fixtures:
|
|
66
|
-
|
|
67
|
-
```typescript
|
|
68
|
-
// tests/e2e/fixtures/index.ts
|
|
69
|
-
import { test as base, expect } from '@playwright/test';
|
|
70
|
-
import { MongoClient, Db, ObjectId } from 'mongodb';
|
|
71
|
-
|
|
72
|
-
type TestFixtures = {
|
|
73
|
-
db: Db;
|
|
74
|
-
createdIds: Map<string, ObjectId[]>;
|
|
75
|
-
trackCreated: (collection: string, id: ObjectId) => void;
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
export const test = base.extend<TestFixtures>({
|
|
79
|
-
db: async ({}, use) => {
|
|
80
|
-
const client = await MongoClient.connect(process.env.MONGODB_URI!);
|
|
81
|
-
const db = client.db();
|
|
82
|
-
await use(db);
|
|
83
|
-
await client.close();
|
|
84
|
-
},
|
|
85
|
-
|
|
86
|
-
createdIds: async ({}, use) => {
|
|
87
|
-
const ids = new Map<string, ObjectId[]>();
|
|
88
|
-
await use(ids);
|
|
89
|
-
},
|
|
90
|
-
|
|
91
|
-
trackCreated: async ({ createdIds }, use) => {
|
|
92
|
-
const track = (collection: string, id: ObjectId) => {
|
|
93
|
-
const existing = createdIds.get(collection) || [];
|
|
94
|
-
existing.push(id);
|
|
95
|
-
createdIds.set(collection, existing);
|
|
96
|
-
};
|
|
97
|
-
await use(track);
|
|
98
|
-
},
|
|
99
|
-
|
|
100
|
-
// AUTO-CLEANUP runs EVEN IF test fails
|
|
101
|
-
}, async ({ db, createdIds }, use) => {
|
|
102
|
-
await use();
|
|
103
|
-
|
|
104
|
-
for (const [collection, ids] of createdIds.entries()) {
|
|
105
|
-
if (ids.length > 0) {
|
|
106
|
-
await db.collection(collection).deleteMany({
|
|
107
|
-
_id: { $in: ids }
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
export { expect };
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
---
|
|
117
|
-
|
|
118
|
-
## Auth Helper (MANDATORY)
|
|
119
|
-
|
|
120
|
-
```typescript
|
|
121
|
-
// tests/e2e/fixtures/auth.fixture.ts
|
|
122
|
-
import { Page } from '@playwright/test';
|
|
123
|
-
|
|
124
|
-
export interface TestUser {
|
|
125
|
-
name: string;
|
|
126
|
-
email: string;
|
|
127
|
-
password: string;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export function generateTestUser(): TestUser {
|
|
131
|
-
const timestamp = Date.now();
|
|
132
|
-
const random = Math.random().toString(36).substring(7);
|
|
133
|
-
return {
|
|
134
|
-
name: `Test User ${timestamp}`,
|
|
135
|
-
email: `testuser_${timestamp}_${random}@test.com`,
|
|
136
|
-
password: 'TestPassword123!',
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
export async function registerUser(page: Page, user: TestUser): Promise<void> {
|
|
141
|
-
await page.goto('/auth/register');
|
|
142
|
-
await page.getByTestId('name-input').fill(user.name);
|
|
143
|
-
await page.getByTestId('email-input').fill(user.email);
|
|
144
|
-
await page.getByTestId('password-input').fill(user.password);
|
|
145
|
-
await page.getByTestId('confirm-password-input').fill(user.password);
|
|
146
|
-
await page.getByTestId('submit-button').click();
|
|
147
|
-
await page.waitForURL(/\/app/, { timeout: 10000 });
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
export async function loginUser(page: Page, user: TestUser): Promise<void> {
|
|
151
|
-
await page.goto('/auth/login');
|
|
152
|
-
await page.getByTestId('email-input').fill(user.email);
|
|
153
|
-
await page.getByTestId('password-input').fill(user.password);
|
|
154
|
-
await page.getByTestId('submit-button').click();
|
|
155
|
-
await page.waitForURL(/\/app/, { timeout: 10000 });
|
|
156
|
-
}
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
---
|
|
160
|
-
|
|
161
|
-
## Test Templates
|
|
162
|
-
|
|
163
|
-
### E2E Flow Test (with cleanup)
|
|
164
|
-
|
|
165
|
-
```typescript
|
|
166
|
-
import { test, expect } from '../fixtures';
|
|
167
|
-
import { generateTestUser, registerUser } from '../fixtures/auth.fixture';
|
|
168
|
-
|
|
169
|
-
test.describe('[Feature] Flow', () => {
|
|
170
|
-
test('complete user journey', async ({ page, db, trackCreated }) => {
|
|
171
|
-
const user = generateTestUser();
|
|
172
|
-
|
|
173
|
-
// 1. Register
|
|
174
|
-
await registerUser(page, user);
|
|
175
|
-
|
|
176
|
-
// 2. Verify in database
|
|
177
|
-
const dbUser = await db.collection('users').findOne({
|
|
178
|
-
email: user.email
|
|
179
|
-
});
|
|
180
|
-
expect(dbUser).toBeTruthy();
|
|
181
|
-
trackCreated('users', dbUser!._id); // TRACK FOR CLEANUP
|
|
182
|
-
|
|
183
|
-
// 3. Create item
|
|
184
|
-
await page.goto('/items/new');
|
|
185
|
-
await page.getByTestId('title-input').fill('Test Item');
|
|
186
|
-
await page.getByTestId('submit-button').click();
|
|
187
|
-
|
|
188
|
-
// 4. Verify item in DB
|
|
189
|
-
const item = await db.collection('items').findOne({
|
|
190
|
-
userId: dbUser!._id
|
|
191
|
-
});
|
|
192
|
-
expect(item).toBeTruthy();
|
|
193
|
-
trackCreated('items', item!._id); // TRACK FOR CLEANUP
|
|
194
|
-
|
|
195
|
-
// Test continues... cleanup is automatic
|
|
196
|
-
});
|
|
197
|
-
});
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
### Multi-Viewport Test
|
|
201
|
-
|
|
202
|
-
```typescript
|
|
203
|
-
import { test, expect } from '../fixtures';
|
|
204
|
-
|
|
205
|
-
test.describe('Responsive Design', () => {
|
|
206
|
-
const viewports = [
|
|
207
|
-
{ name: 'mobile', width: 375, height: 667 },
|
|
208
|
-
{ name: 'tablet', width: 768, height: 1024 },
|
|
209
|
-
{ name: 'desktop', width: 1280, height: 800 },
|
|
210
|
-
];
|
|
211
|
-
|
|
212
|
-
for (const viewport of viewports) {
|
|
213
|
-
test(`layout works on ${viewport.name}`, async ({ page }) => {
|
|
214
|
-
await page.setViewportSize(viewport);
|
|
215
|
-
await page.goto('/');
|
|
216
|
-
|
|
217
|
-
if (viewport.width < 768) {
|
|
218
|
-
// Mobile: hamburger menu
|
|
219
|
-
await expect(page.getByTestId('hamburger-menu')).toBeVisible();
|
|
220
|
-
await expect(page.getByTestId('sidebar')).toBeHidden();
|
|
221
|
-
} else {
|
|
222
|
-
// Desktop: sidebar visible
|
|
223
|
-
await expect(page.getByTestId('sidebar')).toBeVisible();
|
|
224
|
-
}
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
});
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
### API Test (REST)
|
|
231
|
-
|
|
232
|
-
```typescript
|
|
233
|
-
import { test, expect } from '@playwright/test';
|
|
234
|
-
|
|
235
|
-
test.describe('REST API', () => {
|
|
236
|
-
test('requires authentication', async ({ request }) => {
|
|
237
|
-
const response = await request.get('/api/users');
|
|
238
|
-
expect(response.status()).toBe(401);
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
test('validates input', async ({ request }) => {
|
|
242
|
-
const response = await request.post('/api/users', {
|
|
243
|
-
data: { email: 'invalid' }
|
|
244
|
-
});
|
|
245
|
-
expect(response.status()).toBe(400);
|
|
246
|
-
});
|
|
247
|
-
});
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
### API Test (tRPC)
|
|
251
|
-
|
|
252
|
-
```typescript
|
|
253
|
-
import { test, expect } from '@playwright/test';
|
|
254
|
-
|
|
255
|
-
test.describe('tRPC API', () => {
|
|
256
|
-
test('query without auth fails', async ({ request }) => {
|
|
257
|
-
const response = await request.get('/api/trpc/user.me');
|
|
258
|
-
expect(response.status()).toBe(401);
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
test('mutation validates input', async ({ request }) => {
|
|
262
|
-
const response = await request.post('/api/trpc/user.create', {
|
|
263
|
-
data: { json: { name: '' } }
|
|
264
|
-
});
|
|
265
|
-
const body = await response.json();
|
|
266
|
-
expect(body.error).toBeDefined();
|
|
267
|
-
});
|
|
268
|
-
});
|
|
269
|
-
```
|
|
270
|
-
|
|
271
|
-
### Security Test
|
|
272
|
-
|
|
273
|
-
```typescript
|
|
274
|
-
import { test, expect } from '../fixtures';
|
|
275
|
-
|
|
276
|
-
test.describe('Security - Forbidden Requests', () => {
|
|
277
|
-
test('cannot access other users data', async ({ page, db }) => {
|
|
278
|
-
const userA = await createTestUser(db);
|
|
279
|
-
const userB = await createTestUser(db);
|
|
280
|
-
|
|
281
|
-
await loginAs(page, userA);
|
|
282
|
-
|
|
283
|
-
// Try to access user B's data
|
|
284
|
-
const response = await page.request.get(`/api/users/${userB._id}`);
|
|
285
|
-
expect(response.status()).toBe(403);
|
|
286
|
-
|
|
287
|
-
// Verify nothing changed in DB
|
|
288
|
-
const unchanged = await db.collection('users').findOne({
|
|
289
|
-
_id: userB._id
|
|
290
|
-
});
|
|
291
|
-
expect(unchanged).toEqual(userB);
|
|
292
|
-
});
|
|
293
|
-
});
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
### Unit Test
|
|
297
|
-
|
|
298
|
-
```typescript
|
|
299
|
-
import { describe, it, expect } from 'vitest';
|
|
300
|
-
|
|
301
|
-
describe('[Feature]', () => {
|
|
302
|
-
describe('success cases', () => {
|
|
303
|
-
it('should [expected behavior] when [condition]', () => {
|
|
304
|
-
// Arrange
|
|
305
|
-
// Act
|
|
306
|
-
// Assert
|
|
307
|
-
});
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
describe('error cases', () => {
|
|
311
|
-
it('should throw when [invalid condition]', () => {
|
|
312
|
-
expect(() => fn(invalid)).toThrow();
|
|
313
|
-
});
|
|
314
|
-
});
|
|
315
|
-
});
|
|
316
|
-
```
|
|
317
|
-
|
|
318
|
-
---
|
|
319
|
-
|
|
320
|
-
## Files That NEED Tests
|
|
321
|
-
|
|
322
|
-
| Type | Location | Test Expected | Required |
|
|
323
|
-
| --------- | --------------------- | --------------------- | -------------- |
|
|
324
|
-
| API Route | `server/routers/*.ts` | `tests/unit/*.test.ts` | **YES** |
|
|
325
|
-
| API Route | `app/api/**/*.ts` | `tests/e2e/api/*.spec.ts` | **YES** |
|
|
326
|
-
| Model | `server/models/*.ts` | `tests/unit/*.test.ts` | **YES** |
|
|
327
|
-
| Page | `app/**/page.tsx` | `tests/e2e/flows/*.spec.ts` | **YES** |
|
|
328
|
-
| Component | `components/**/*.tsx` | `tests/e2e/*.spec.ts` | If interactive |
|
|
329
|
-
| Hook | `hooks/*.ts` | `tests/unit/*.test.ts` | YES |
|
|
330
|
-
| Util | `lib/*.ts` | `tests/unit/*.test.ts` | If exported |
|
|
331
|
-
|
|
332
|
-
---
|
|
333
|
-
|
|
334
|
-
## Required Flows (E2E)
|
|
335
|
-
|
|
336
|
-
Every app MUST have tests for:
|
|
337
|
-
|
|
338
|
-
- [ ] **Registration** - Create new user, verify in DB
|
|
339
|
-
- [ ] **Login/Logout** - Auth state changes correctly
|
|
340
|
-
- [ ] **CRUD Create** - Item created, visible, in DB
|
|
341
|
-
- [ ] **CRUD Read** - Item displayed correctly
|
|
342
|
-
- [ ] **CRUD Update** - Item updated, changes in DB
|
|
343
|
-
- [ ] **CRUD Delete** - Item removed from DB
|
|
344
|
-
- [ ] **Permissions** - Forbidden requests blocked
|
|
345
|
-
- [ ] **Responsive** - Works on all viewports
|
|
346
|
-
|
|
347
|
-
---
|
|
348
|
-
|
|
349
|
-
## Required data-testid
|
|
350
|
-
|
|
351
|
-
```html
|
|
352
|
-
<!-- Forms -->
|
|
353
|
-
<input data-testid="name-input" />
|
|
354
|
-
<input data-testid="email-input" />
|
|
355
|
-
<input data-testid="password-input" />
|
|
356
|
-
<input data-testid="confirm-password-input" />
|
|
357
|
-
<button data-testid="submit-button" />
|
|
358
|
-
|
|
359
|
-
<!-- Feedback -->
|
|
360
|
-
<div data-testid="error-message" />
|
|
361
|
-
<div data-testid="success-message" />
|
|
362
|
-
<div data-testid="loading-spinner" />
|
|
363
|
-
|
|
364
|
-
<!-- Navigation -->
|
|
365
|
-
<nav data-testid="sidebar" />
|
|
366
|
-
<button data-testid="hamburger-menu" />
|
|
367
|
-
<nav data-testid="mobile-nav" />
|
|
368
|
-
<button data-testid="logout-button" />
|
|
369
|
-
|
|
370
|
-
<!-- Actions -->
|
|
371
|
-
<button data-testid="delete-button" />
|
|
372
|
-
<button data-testid="edit-button" />
|
|
373
|
-
<button data-testid="confirm-delete" />
|
|
374
|
-
```
|
|
375
|
-
|
|
376
|
-
---
|
|
377
|
-
|
|
378
|
-
## Playwright Commands
|
|
379
|
-
|
|
380
|
-
```bash
|
|
381
|
-
# Install
|
|
382
|
-
bun add -D @playwright/test
|
|
383
|
-
bunx playwright install
|
|
384
|
-
|
|
385
|
-
# Run
|
|
386
|
-
bunx playwright test # All tests
|
|
387
|
-
bunx playwright test --ui # UI mode (recommended)
|
|
388
|
-
bunx playwright test --headed # See browser
|
|
389
|
-
bunx playwright test --debug # Debug mode
|
|
390
|
-
|
|
391
|
-
# Specific
|
|
392
|
-
bunx playwright test flows/auth # Specific folder
|
|
393
|
-
bunx playwright test --project="iPhone SE" # Specific viewport
|
|
394
|
-
|
|
395
|
-
# Reports
|
|
396
|
-
bunx playwright show-report
|
|
397
|
-
```
|
|
398
|
-
|
|
399
|
-
---
|
|
400
|
-
|
|
401
|
-
## Checklist
|
|
402
|
-
|
|
403
|
-
### Before Commit
|
|
404
|
-
|
|
405
|
-
- [ ] All new features have E2E tests?
|
|
406
|
-
- [ ] Tests use fixtures for cleanup?
|
|
407
|
-
- [ ] All created data is tracked and cleaned?
|
|
408
|
-
- [ ] Database state verified after UI actions?
|
|
409
|
-
- [ ] Tests run on all viewports?
|
|
410
|
-
- [ ] Forbidden requests tested?
|
|
411
|
-
- [ ] No `.skip()` in tests?
|
|
412
|
-
- [ ] `bunx playwright test` passes?
|
|
413
|
-
- [ ] `bun run test` passes?
|
|
414
|
-
|
|
415
|
-
---
|
|
416
|
-
|
|
417
|
-
## FORBIDDEN (Never Do)
|
|
418
|
-
|
|
419
|
-
```typescript
|
|
420
|
-
// WRONG - Skipping tests
|
|
421
|
-
test.skip("should work when authenticated", ...);
|
|
422
|
-
|
|
423
|
-
// WRONG - Mocking auth
|
|
424
|
-
const mockAuth = () => { /* fake */ };
|
|
425
|
-
|
|
426
|
-
// WRONG - Fixed test user
|
|
427
|
-
const testUser = { email: "test@test.com" };
|
|
428
|
-
|
|
429
|
-
// WRONG - No cleanup
|
|
430
|
-
const user = await db.create({ ... }); // Orphaned!
|
|
431
|
-
|
|
432
|
-
// WRONG - No DB validation
|
|
433
|
-
await page.click('submit');
|
|
434
|
-
// Just trust the UI? NO!
|
|
435
|
-
```
|
|
436
|
-
|
|
437
|
-
---
|
|
438
|
-
|
|
439
|
-
## Version
|
|
440
|
-
|
|
441
|
-
- **v3.0.0** - Complete E2E architecture with cleanup, DB validation, multi-viewport
|
|
1
|
+
---
|
|
2
|
+
name: test-coverage
|
|
3
|
+
description: Manages test coverage with Playwright E2E and Vitest unit tests. Tracks which files need tests, provides templates with fixture-based cleanup, enforces multi-viewport testing and database validation.
|
|
4
|
+
allowed-tools: Read, Write, Edit, Bash, Grep, Glob
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Test Coverage - Testing Management System
|
|
8
|
+
|
|
9
|
+
## Purpose
|
|
10
|
+
|
|
11
|
+
This skill manages test coverage with **Playwright E2E** and **Vitest unit tests**:
|
|
12
|
+
|
|
13
|
+
- **Detects** new files that need tests
|
|
14
|
+
- **Maps** components consuming API routes (tRPC/REST)
|
|
15
|
+
- **Tracks** which pages have E2E coverage
|
|
16
|
+
- **Generates** test templates with cleanup
|
|
17
|
+
- **Validates** real authentication usage (MANDATORY)
|
|
18
|
+
- **Ensures** database validation after UI actions
|
|
19
|
+
- **Enforces** multi-viewport testing
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## CRITICAL RULES
|
|
24
|
+
|
|
25
|
+
> **MANDATORY - NO EXCEPTIONS:**
|
|
26
|
+
|
|
27
|
+
1. **CLEANUP ALL TEST DATA** - Use fixture-based tracking
|
|
28
|
+
2. **VERIFY IN DATABASE** - Check DB state after UI actions
|
|
29
|
+
3. **TEST ALL VIEWPORTS** - Desktop, tablet, iPhone SE minimum
|
|
30
|
+
4. **REAL AUTH ONLY** - Never mock authentication
|
|
31
|
+
5. **UNIQUE DATA** - Timestamps in emails/names
|
|
32
|
+
6. **NO SKIP** - Never use `.skip()` or `.only()`
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Test Structure
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
tests/
|
|
40
|
+
├── unit/ # Unit tests (Vitest)
|
|
41
|
+
│ └── *.test.ts
|
|
42
|
+
└── e2e/ # E2E tests (Playwright)
|
|
43
|
+
├── fixtures/
|
|
44
|
+
│ ├── index.ts # Custom fixtures (auth, db, cleanup)
|
|
45
|
+
│ ├── auth.fixture.ts # Authentication helpers
|
|
46
|
+
│ └── db.fixture.ts # Database connection & cleanup
|
|
47
|
+
├── pages/ # Page Object Model
|
|
48
|
+
│ ├── base.page.ts # Base page with common methods
|
|
49
|
+
│ ├── login.page.ts
|
|
50
|
+
│ └── register.page.ts
|
|
51
|
+
├── flows/ # User flow tests
|
|
52
|
+
│ ├── auth.spec.ts # Login, register, logout
|
|
53
|
+
│ ├── crud.spec.ts # Create, read, update, delete
|
|
54
|
+
│ └── permissions.spec.ts
|
|
55
|
+
├── api/ # API-only tests (no UI)
|
|
56
|
+
│ ├── rest.spec.ts # REST API tests
|
|
57
|
+
│ └── trpc.spec.ts # tRPC API tests
|
|
58
|
+
└── playwright.config.ts
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Cleanup Fixture (MANDATORY)
|
|
64
|
+
|
|
65
|
+
Every project MUST have cleanup fixtures:
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
// tests/e2e/fixtures/index.ts
|
|
69
|
+
import { test as base, expect } from '@playwright/test';
|
|
70
|
+
import { MongoClient, Db, ObjectId } from 'mongodb';
|
|
71
|
+
|
|
72
|
+
type TestFixtures = {
|
|
73
|
+
db: Db;
|
|
74
|
+
createdIds: Map<string, ObjectId[]>;
|
|
75
|
+
trackCreated: (collection: string, id: ObjectId) => void;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const test = base.extend<TestFixtures>({
|
|
79
|
+
db: async ({}, use) => {
|
|
80
|
+
const client = await MongoClient.connect(process.env.MONGODB_URI!);
|
|
81
|
+
const db = client.db();
|
|
82
|
+
await use(db);
|
|
83
|
+
await client.close();
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
createdIds: async ({}, use) => {
|
|
87
|
+
const ids = new Map<string, ObjectId[]>();
|
|
88
|
+
await use(ids);
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
trackCreated: async ({ createdIds }, use) => {
|
|
92
|
+
const track = (collection: string, id: ObjectId) => {
|
|
93
|
+
const existing = createdIds.get(collection) || [];
|
|
94
|
+
existing.push(id);
|
|
95
|
+
createdIds.set(collection, existing);
|
|
96
|
+
};
|
|
97
|
+
await use(track);
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
// AUTO-CLEANUP runs EVEN IF test fails
|
|
101
|
+
}, async ({ db, createdIds }, use) => {
|
|
102
|
+
await use();
|
|
103
|
+
|
|
104
|
+
for (const [collection, ids] of createdIds.entries()) {
|
|
105
|
+
if (ids.length > 0) {
|
|
106
|
+
await db.collection(collection).deleteMany({
|
|
107
|
+
_id: { $in: ids }
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
export { expect };
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Auth Helper (MANDATORY)
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
// tests/e2e/fixtures/auth.fixture.ts
|
|
122
|
+
import { Page } from '@playwright/test';
|
|
123
|
+
|
|
124
|
+
export interface TestUser {
|
|
125
|
+
name: string;
|
|
126
|
+
email: string;
|
|
127
|
+
password: string;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function generateTestUser(): TestUser {
|
|
131
|
+
const timestamp = Date.now();
|
|
132
|
+
const random = Math.random().toString(36).substring(7);
|
|
133
|
+
return {
|
|
134
|
+
name: `Test User ${timestamp}`,
|
|
135
|
+
email: `testuser_${timestamp}_${random}@test.com`,
|
|
136
|
+
password: 'TestPassword123!',
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function registerUser(page: Page, user: TestUser): Promise<void> {
|
|
141
|
+
await page.goto('/auth/register');
|
|
142
|
+
await page.getByTestId('name-input').fill(user.name);
|
|
143
|
+
await page.getByTestId('email-input').fill(user.email);
|
|
144
|
+
await page.getByTestId('password-input').fill(user.password);
|
|
145
|
+
await page.getByTestId('confirm-password-input').fill(user.password);
|
|
146
|
+
await page.getByTestId('submit-button').click();
|
|
147
|
+
await page.waitForURL(/\/app/, { timeout: 10000 });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function loginUser(page: Page, user: TestUser): Promise<void> {
|
|
151
|
+
await page.goto('/auth/login');
|
|
152
|
+
await page.getByTestId('email-input').fill(user.email);
|
|
153
|
+
await page.getByTestId('password-input').fill(user.password);
|
|
154
|
+
await page.getByTestId('submit-button').click();
|
|
155
|
+
await page.waitForURL(/\/app/, { timeout: 10000 });
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Test Templates
|
|
162
|
+
|
|
163
|
+
### E2E Flow Test (with cleanup)
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
import { test, expect } from '../fixtures';
|
|
167
|
+
import { generateTestUser, registerUser } from '../fixtures/auth.fixture';
|
|
168
|
+
|
|
169
|
+
test.describe('[Feature] Flow', () => {
|
|
170
|
+
test('complete user journey', async ({ page, db, trackCreated }) => {
|
|
171
|
+
const user = generateTestUser();
|
|
172
|
+
|
|
173
|
+
// 1. Register
|
|
174
|
+
await registerUser(page, user);
|
|
175
|
+
|
|
176
|
+
// 2. Verify in database
|
|
177
|
+
const dbUser = await db.collection('users').findOne({
|
|
178
|
+
email: user.email
|
|
179
|
+
});
|
|
180
|
+
expect(dbUser).toBeTruthy();
|
|
181
|
+
trackCreated('users', dbUser!._id); // TRACK FOR CLEANUP
|
|
182
|
+
|
|
183
|
+
// 3. Create item
|
|
184
|
+
await page.goto('/items/new');
|
|
185
|
+
await page.getByTestId('title-input').fill('Test Item');
|
|
186
|
+
await page.getByTestId('submit-button').click();
|
|
187
|
+
|
|
188
|
+
// 4. Verify item in DB
|
|
189
|
+
const item = await db.collection('items').findOne({
|
|
190
|
+
userId: dbUser!._id
|
|
191
|
+
});
|
|
192
|
+
expect(item).toBeTruthy();
|
|
193
|
+
trackCreated('items', item!._id); // TRACK FOR CLEANUP
|
|
194
|
+
|
|
195
|
+
// Test continues... cleanup is automatic
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Multi-Viewport Test
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
import { test, expect } from '../fixtures';
|
|
204
|
+
|
|
205
|
+
test.describe('Responsive Design', () => {
|
|
206
|
+
const viewports = [
|
|
207
|
+
{ name: 'mobile', width: 375, height: 667 },
|
|
208
|
+
{ name: 'tablet', width: 768, height: 1024 },
|
|
209
|
+
{ name: 'desktop', width: 1280, height: 800 },
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
for (const viewport of viewports) {
|
|
213
|
+
test(`layout works on ${viewport.name}`, async ({ page }) => {
|
|
214
|
+
await page.setViewportSize(viewport);
|
|
215
|
+
await page.goto('/');
|
|
216
|
+
|
|
217
|
+
if (viewport.width < 768) {
|
|
218
|
+
// Mobile: hamburger menu
|
|
219
|
+
await expect(page.getByTestId('hamburger-menu')).toBeVisible();
|
|
220
|
+
await expect(page.getByTestId('sidebar')).toBeHidden();
|
|
221
|
+
} else {
|
|
222
|
+
// Desktop: sidebar visible
|
|
223
|
+
await expect(page.getByTestId('sidebar')).toBeVisible();
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### API Test (REST)
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
import { test, expect } from '@playwright/test';
|
|
234
|
+
|
|
235
|
+
test.describe('REST API', () => {
|
|
236
|
+
test('requires authentication', async ({ request }) => {
|
|
237
|
+
const response = await request.get('/api/users');
|
|
238
|
+
expect(response.status()).toBe(401);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test('validates input', async ({ request }) => {
|
|
242
|
+
const response = await request.post('/api/users', {
|
|
243
|
+
data: { email: 'invalid' }
|
|
244
|
+
});
|
|
245
|
+
expect(response.status()).toBe(400);
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### API Test (tRPC)
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
import { test, expect } from '@playwright/test';
|
|
254
|
+
|
|
255
|
+
test.describe('tRPC API', () => {
|
|
256
|
+
test('query without auth fails', async ({ request }) => {
|
|
257
|
+
const response = await request.get('/api/trpc/user.me');
|
|
258
|
+
expect(response.status()).toBe(401);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test('mutation validates input', async ({ request }) => {
|
|
262
|
+
const response = await request.post('/api/trpc/user.create', {
|
|
263
|
+
data: { json: { name: '' } }
|
|
264
|
+
});
|
|
265
|
+
const body = await response.json();
|
|
266
|
+
expect(body.error).toBeDefined();
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Security Test
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
import { test, expect } from '../fixtures';
|
|
275
|
+
|
|
276
|
+
test.describe('Security - Forbidden Requests', () => {
|
|
277
|
+
test('cannot access other users data', async ({ page, db }) => {
|
|
278
|
+
const userA = await createTestUser(db);
|
|
279
|
+
const userB = await createTestUser(db);
|
|
280
|
+
|
|
281
|
+
await loginAs(page, userA);
|
|
282
|
+
|
|
283
|
+
// Try to access user B's data
|
|
284
|
+
const response = await page.request.get(`/api/users/${userB._id}`);
|
|
285
|
+
expect(response.status()).toBe(403);
|
|
286
|
+
|
|
287
|
+
// Verify nothing changed in DB
|
|
288
|
+
const unchanged = await db.collection('users').findOne({
|
|
289
|
+
_id: userB._id
|
|
290
|
+
});
|
|
291
|
+
expect(unchanged).toEqual(userB);
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Unit Test
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
import { describe, it, expect } from 'vitest';
|
|
300
|
+
|
|
301
|
+
describe('[Feature]', () => {
|
|
302
|
+
describe('success cases', () => {
|
|
303
|
+
it('should [expected behavior] when [condition]', () => {
|
|
304
|
+
// Arrange
|
|
305
|
+
// Act
|
|
306
|
+
// Assert
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
describe('error cases', () => {
|
|
311
|
+
it('should throw when [invalid condition]', () => {
|
|
312
|
+
expect(() => fn(invalid)).toThrow();
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## Files That NEED Tests
|
|
321
|
+
|
|
322
|
+
| Type | Location | Test Expected | Required |
|
|
323
|
+
| --------- | --------------------- | --------------------- | -------------- |
|
|
324
|
+
| API Route | `server/routers/*.ts` | `tests/unit/*.test.ts` | **YES** |
|
|
325
|
+
| API Route | `app/api/**/*.ts` | `tests/e2e/api/*.spec.ts` | **YES** |
|
|
326
|
+
| Model | `server/models/*.ts` | `tests/unit/*.test.ts` | **YES** |
|
|
327
|
+
| Page | `app/**/page.tsx` | `tests/e2e/flows/*.spec.ts` | **YES** |
|
|
328
|
+
| Component | `components/**/*.tsx` | `tests/e2e/*.spec.ts` | If interactive |
|
|
329
|
+
| Hook | `hooks/*.ts` | `tests/unit/*.test.ts` | YES |
|
|
330
|
+
| Util | `lib/*.ts` | `tests/unit/*.test.ts` | If exported |
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## Required Flows (E2E)
|
|
335
|
+
|
|
336
|
+
Every app MUST have tests for:
|
|
337
|
+
|
|
338
|
+
- [ ] **Registration** - Create new user, verify in DB
|
|
339
|
+
- [ ] **Login/Logout** - Auth state changes correctly
|
|
340
|
+
- [ ] **CRUD Create** - Item created, visible, in DB
|
|
341
|
+
- [ ] **CRUD Read** - Item displayed correctly
|
|
342
|
+
- [ ] **CRUD Update** - Item updated, changes in DB
|
|
343
|
+
- [ ] **CRUD Delete** - Item removed from DB
|
|
344
|
+
- [ ] **Permissions** - Forbidden requests blocked
|
|
345
|
+
- [ ] **Responsive** - Works on all viewports
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
## Required data-testid
|
|
350
|
+
|
|
351
|
+
```html
|
|
352
|
+
<!-- Forms -->
|
|
353
|
+
<input data-testid="name-input" />
|
|
354
|
+
<input data-testid="email-input" />
|
|
355
|
+
<input data-testid="password-input" />
|
|
356
|
+
<input data-testid="confirm-password-input" />
|
|
357
|
+
<button data-testid="submit-button" />
|
|
358
|
+
|
|
359
|
+
<!-- Feedback -->
|
|
360
|
+
<div data-testid="error-message" />
|
|
361
|
+
<div data-testid="success-message" />
|
|
362
|
+
<div data-testid="loading-spinner" />
|
|
363
|
+
|
|
364
|
+
<!-- Navigation -->
|
|
365
|
+
<nav data-testid="sidebar" />
|
|
366
|
+
<button data-testid="hamburger-menu" />
|
|
367
|
+
<nav data-testid="mobile-nav" />
|
|
368
|
+
<button data-testid="logout-button" />
|
|
369
|
+
|
|
370
|
+
<!-- Actions -->
|
|
371
|
+
<button data-testid="delete-button" />
|
|
372
|
+
<button data-testid="edit-button" />
|
|
373
|
+
<button data-testid="confirm-delete" />
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
## Playwright Commands
|
|
379
|
+
|
|
380
|
+
```bash
|
|
381
|
+
# Install
|
|
382
|
+
bun add -D @playwright/test
|
|
383
|
+
bunx playwright install
|
|
384
|
+
|
|
385
|
+
# Run
|
|
386
|
+
bunx playwright test # All tests
|
|
387
|
+
bunx playwright test --ui # UI mode (recommended)
|
|
388
|
+
bunx playwright test --headed # See browser
|
|
389
|
+
bunx playwright test --debug # Debug mode
|
|
390
|
+
|
|
391
|
+
# Specific
|
|
392
|
+
bunx playwright test flows/auth # Specific folder
|
|
393
|
+
bunx playwright test --project="iPhone SE" # Specific viewport
|
|
394
|
+
|
|
395
|
+
# Reports
|
|
396
|
+
bunx playwright show-report
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
---
|
|
400
|
+
|
|
401
|
+
## Checklist
|
|
402
|
+
|
|
403
|
+
### Before Commit
|
|
404
|
+
|
|
405
|
+
- [ ] All new features have E2E tests?
|
|
406
|
+
- [ ] Tests use fixtures for cleanup?
|
|
407
|
+
- [ ] All created data is tracked and cleaned?
|
|
408
|
+
- [ ] Database state verified after UI actions?
|
|
409
|
+
- [ ] Tests run on all viewports?
|
|
410
|
+
- [ ] Forbidden requests tested?
|
|
411
|
+
- [ ] No `.skip()` in tests?
|
|
412
|
+
- [ ] `bunx playwright test` passes?
|
|
413
|
+
- [ ] `bun run test` passes?
|
|
414
|
+
|
|
415
|
+
---
|
|
416
|
+
|
|
417
|
+
## FORBIDDEN (Never Do)
|
|
418
|
+
|
|
419
|
+
```typescript
|
|
420
|
+
// WRONG - Skipping tests
|
|
421
|
+
test.skip("should work when authenticated", ...);
|
|
422
|
+
|
|
423
|
+
// WRONG - Mocking auth
|
|
424
|
+
const mockAuth = () => { /* fake */ };
|
|
425
|
+
|
|
426
|
+
// WRONG - Fixed test user
|
|
427
|
+
const testUser = { email: "test@test.com" };
|
|
428
|
+
|
|
429
|
+
// WRONG - No cleanup
|
|
430
|
+
const user = await db.create({ ... }); // Orphaned!
|
|
431
|
+
|
|
432
|
+
// WRONG - No DB validation
|
|
433
|
+
await page.click('submit');
|
|
434
|
+
// Just trust the UI? NO!
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
---
|
|
438
|
+
|
|
439
|
+
## Version
|
|
440
|
+
|
|
441
|
+
- **v3.0.0** - Complete E2E architecture with cleanup, DB validation, multi-viewport
|