@claude-code-mastery/starter-kit 1.0.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/.claude/.starter-kit/profiles/clean.md +113 -0
- package/.claude/.starter-kit/profiles/go.md +458 -0
- package/.claude/.starter-kit/profiles/node.md +429 -0
- package/.claude/.starter-kit/profiles/python.md +475 -0
- package/.claude/.starter-kit/shared/analytics-rybbit.md +55 -0
- package/.claude/.starter-kit/shared/claude-md-base.md +93 -0
- package/.claude/.starter-kit/shared/deployment-dokploy.md +158 -0
- package/.claude/.starter-kit/shared/feature-manifest.md +43 -0
- package/.claude/.starter-kit/shared/mcp-and-pooler.md +38 -0
- package/.claude/.starter-kit/shared/mongo-setup.md +20 -0
- package/.claude/.starter-kit/shared/profile-config.md +65 -0
- package/.claude/.starter-kit/shared/seo.md +113 -0
- package/.claude/.starter-kit/shared/sql-setup.md +37 -0
- package/.claude/commands/add-feature.md +349 -0
- package/.claude/commands/add-project-setup.md +156 -0
- package/.claude/commands/architecture.md +27 -0
- package/.claude/commands/commit.md +61 -0
- package/.claude/commands/convert-project-to-starter-kit.md +508 -0
- package/.claude/commands/create-api.md +385 -0
- package/.claude/commands/create-e2e.md +230 -0
- package/.claude/commands/diagram.md +301 -0
- package/.claude/commands/help.md +120 -0
- package/.claude/commands/install-global.md +145 -0
- package/.claude/commands/new-project.md +244 -0
- package/.claude/commands/optimize-docker.md +352 -0
- package/.claude/commands/progress.md +61 -0
- package/.claude/commands/projects-created.md +79 -0
- package/.claude/commands/quickstart.md +105 -0
- package/.claude/commands/refactor.md +267 -0
- package/.claude/commands/remove-project.md +95 -0
- package/.claude/commands/review.md +59 -0
- package/.claude/commands/security-check.md +77 -0
- package/.claude/commands/set-project-profile-default.md +79 -0
- package/.claude/commands/setup.md +337 -0
- package/.claude/commands/show-user-guide.md +58 -0
- package/.claude/commands/starter-kit.md +90 -0
- package/.claude/commands/test-plan.md +118 -0
- package/.claude/commands/update-project.md +413 -0
- package/.claude/commands/what-is-my-ai-doing.md +42 -0
- package/.claude/commands/worktree.md +124 -0
- package/.claude/hooks/block-dangerous-bash.py +55 -0
- package/.claude/hooks/check-branch.sh +116 -0
- package/.claude/hooks/check-e2e.sh +71 -0
- package/.claude/hooks/check-env-sync.sh +41 -0
- package/.claude/hooks/check-file-length.py +47 -0
- package/.claude/hooks/check-ports.sh +59 -0
- package/.claude/hooks/check-rulecatch.sh +33 -0
- package/.claude/hooks/check-rybbit.sh +63 -0
- package/.claude/hooks/lint-on-save.sh +59 -0
- package/.claude/hooks/verify-no-secrets.sh +80 -0
- package/.claude/settings.json +34 -0
- package/.claude/skills/api-conventions/SKILL.md +34 -0
- package/.claude/skills/code-review/SKILL.md +87 -0
- package/.claude/skills/code-review/references/mongodb-checks.md +25 -0
- package/.claude/skills/code-review/references/project-checks.md +38 -0
- package/.claude/skills/create-service/SKILL.md +222 -0
- package/.claude/skills/debugger/SKILL.md +39 -0
- package/.claude/skills/dependency-vetting/SKILL.md +46 -0
- package/.claude/skills/design-review/SKILL.md +50 -0
- package/.claude/skills/mcp-builder/SKILL.md +57 -0
- package/.claude/skills/mongodb-rules/SKILL.md +62 -0
- package/.claude/skills/terminal-tui/SKILL.md +106 -0
- package/.claude/skills/test-writer/SKILL.md +78 -0
- package/LICENSE +21 -0
- package/README.md +2152 -0
- package/bin/cli.js +205 -0
- package/claude-mastery-project.conf +220 -0
- package/global-claude-md/CLAUDE.md +212 -0
- package/global-claude-md/settings.json +3 -0
- package/package.json +81 -0
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Scaffold a new API endpoint — route, handler, types, tests — wired into the server
|
|
3
|
+
scope: project
|
|
4
|
+
argument-hint: <resource-name> [--no-db]
|
|
5
|
+
allowed-tools: Read, Write, Edit, Grep, Glob, Bash, AskUserQuestion
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Create API Endpoint
|
|
9
|
+
|
|
10
|
+
Scaffold a production-ready API endpoint for: **$ARGUMENTS**
|
|
11
|
+
|
|
12
|
+
## Step 0 — Auto-Branch (if on main)
|
|
13
|
+
|
|
14
|
+
Before creating any files, check the current branch:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
git branch --show-current
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**Default behavior** (`auto_branch = true` in `claude-mastery-project.conf`):
|
|
21
|
+
- If on `main` or `master`: automatically create a feature branch and switch to it:
|
|
22
|
+
```bash
|
|
23
|
+
git checkout -b feat/api-<resource-name>
|
|
24
|
+
```
|
|
25
|
+
Report: "Created branch `feat/api-<resource>` — main stays untouched."
|
|
26
|
+
- If already on a feature branch: proceed
|
|
27
|
+
- If not a git repo: skip this check
|
|
28
|
+
|
|
29
|
+
**To disable:** Set `auto_branch = false` in `claude-mastery-project.conf`. When disabled, warn and ask the user before proceeding on main.
|
|
30
|
+
|
|
31
|
+
## Step 1 — Gather Context
|
|
32
|
+
|
|
33
|
+
Before scaffolding, read the current project:
|
|
34
|
+
|
|
35
|
+
1. **Read `src/server.ts`** (or `src/server.js`, `src/index.ts`) — understand the server setup
|
|
36
|
+
2. **Scan `src/routes/`** — check for existing route patterns to follow
|
|
37
|
+
3. **Scan `src/handlers/`** — check for existing handler patterns
|
|
38
|
+
4. **Scan `src/types/`** — check for existing type patterns
|
|
39
|
+
5. **Read `.env.example`** — check for database config
|
|
40
|
+
|
|
41
|
+
If `--no-db` is in the arguments, skip database integration. Otherwise, use StrictDB if it's installed, or the native MongoDB driver if not (always through the adapter, never Mongoose).
|
|
42
|
+
|
|
43
|
+
## Step 2 — Create Files
|
|
44
|
+
|
|
45
|
+
Generate these files for the resource:
|
|
46
|
+
|
|
47
|
+
### File 1: `src/types/<resource>.ts` — Types first (they're the spec)
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
/** Database document shape */
|
|
51
|
+
export interface <Resource>Doc {
|
|
52
|
+
_id: string;
|
|
53
|
+
// Add fields based on the resource
|
|
54
|
+
createdAt: Date;
|
|
55
|
+
updatedAt: Date;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** API request body for creating a resource */
|
|
59
|
+
export interface Create<Resource>Body {
|
|
60
|
+
// Fields the client sends (NO _id, NO timestamps)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** API request body for updating a resource */
|
|
64
|
+
export interface Update<Resource>Body {
|
|
65
|
+
// Partial fields the client can update
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** API response shape (what the client receives) */
|
|
69
|
+
export interface <Resource>Response {
|
|
70
|
+
id: string;
|
|
71
|
+
// Mapped from the doc — NEVER expose _id directly
|
|
72
|
+
createdAt: string;
|
|
73
|
+
updatedAt: string;
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### File 2: `src/handlers/<resource>.ts` — Business logic
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
import type { StrictDB } from 'strictdb';
|
|
81
|
+
import type { <Resource>Doc, Create<Resource>Body, Update<Resource>Body, <Resource>Response } from '../types/<resource>.js';
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Schema + indexes — registered at startup via db.ensureIndexes()
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
// Called once at app startup with the shared StrictDB instance
|
|
88
|
+
export function register<Resource>Schema(db: StrictDB) {
|
|
89
|
+
db.registerCollection({
|
|
90
|
+
name: '<resources>',
|
|
91
|
+
indexes: [{ collection: '<resources>', fields: { createdAt: -1 } }],
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
// Add more indexes based on query patterns
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Helpers
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
const COLLECTION = '<resources>';
|
|
101
|
+
|
|
102
|
+
/** Map a database document to API response (never expose internals) */
|
|
103
|
+
function toResponse(doc: <Resource>Doc): <Resource>Response {
|
|
104
|
+
return {
|
|
105
|
+
id: String(doc._id),
|
|
106
|
+
// Map other fields
|
|
107
|
+
createdAt: doc.createdAt.toISOString(),
|
|
108
|
+
updatedAt: doc.updatedAt.toISOString(),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// CRUD operations — all receive the shared StrictDB instance
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
export async function create<Resource>(db: StrictDB, body: Create<Resource>Body): Promise<<Resource>Response> {
|
|
117
|
+
const now = new Date();
|
|
118
|
+
const doc: Omit<<Resource>Doc, '_id'> = {
|
|
119
|
+
...body,
|
|
120
|
+
createdAt: now,
|
|
121
|
+
updatedAt: now,
|
|
122
|
+
};
|
|
123
|
+
await db.insertOne(COLLECTION, doc);
|
|
124
|
+
const created = await db.queryOne<<Resource>Doc>(COLLECTION, { createdAt: now });
|
|
125
|
+
if (!created) throw new Error('Failed to create resource');
|
|
126
|
+
return toResponse(created);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function get<Resource>ById(db: StrictDB, id: string): Promise<<Resource>Response | null> {
|
|
130
|
+
const doc = await db.queryOne<<Resource>Doc>(COLLECTION, { _id: id });
|
|
131
|
+
return doc ? toResponse(doc) : null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function list<Resource>s(db: StrictDB, options: {
|
|
135
|
+
page?: number;
|
|
136
|
+
limit?: number;
|
|
137
|
+
sort?: Record<string, 1 | -1>;
|
|
138
|
+
} = {}): Promise<{ data: <Resource>Response[]; total: number; page: number; limit: number }> {
|
|
139
|
+
const page = Math.max(1, options.page ?? 1);
|
|
140
|
+
const limit = Math.min(100, Math.max(1, options.limit ?? 20));
|
|
141
|
+
const sort = options.sort ?? { createdAt: -1 };
|
|
142
|
+
|
|
143
|
+
const [docs, total] = await Promise.all([
|
|
144
|
+
db.queryMany<<Resource>Doc>(COLLECTION, [
|
|
145
|
+
{ $sort: sort },
|
|
146
|
+
{ $skip: (page - 1) * limit },
|
|
147
|
+
{ $limit: limit },
|
|
148
|
+
]),
|
|
149
|
+
db.count(COLLECTION),
|
|
150
|
+
]);
|
|
151
|
+
|
|
152
|
+
return { data: docs.map(toResponse), total, page, limit };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function update<Resource>(
|
|
156
|
+
db: StrictDB,
|
|
157
|
+
id: string,
|
|
158
|
+
body: Update<Resource>Body
|
|
159
|
+
): Promise<<Resource>Response | null> {
|
|
160
|
+
const filter = { _id: id };
|
|
161
|
+
await db.updateOne(COLLECTION, filter, {
|
|
162
|
+
$set: { ...body, updatedAt: new Date() },
|
|
163
|
+
});
|
|
164
|
+
const updated = await db.queryOne<<Resource>Doc>(COLLECTION, filter);
|
|
165
|
+
return updated ? toResponse(updated) : null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export async function delete<Resource>(db: StrictDB, id: string): Promise<boolean> {
|
|
169
|
+
await db.deleteOne(COLLECTION, { _id: id });
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### File 3: `src/routes/v1/<resource>.ts` — Routes (thin, no logic)
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
import { Router } from 'express';
|
|
178
|
+
import type { Request, Response } from 'express';
|
|
179
|
+
import {
|
|
180
|
+
create<Resource>,
|
|
181
|
+
get<Resource>ById,
|
|
182
|
+
list<Resource>s,
|
|
183
|
+
update<Resource>,
|
|
184
|
+
delete<Resource>,
|
|
185
|
+
} from '../../handlers/<resource>.js';
|
|
186
|
+
|
|
187
|
+
const router = Router();
|
|
188
|
+
|
|
189
|
+
// POST /api/v1/<resources>
|
|
190
|
+
router.post('/', async (req: Request, res: Response) => {
|
|
191
|
+
try {
|
|
192
|
+
const result = await create<Resource>(req.body);
|
|
193
|
+
res.status(201).json(result);
|
|
194
|
+
} catch (err) {
|
|
195
|
+
console.error('Create <resource> failed:', err);
|
|
196
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// GET /api/v1/<resources>
|
|
201
|
+
router.get('/', async (req: Request, res: Response) => {
|
|
202
|
+
try {
|
|
203
|
+
const page = parseInt(req.query.page as string) || 1;
|
|
204
|
+
const limit = parseInt(req.query.limit as string) || 20;
|
|
205
|
+
const result = await list<Resource>s({ page, limit });
|
|
206
|
+
res.json(result);
|
|
207
|
+
} catch (err) {
|
|
208
|
+
console.error('List <resources> failed:', err);
|
|
209
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// GET /api/v1/<resources>/:id
|
|
214
|
+
router.get('/:id', async (req: Request, res: Response) => {
|
|
215
|
+
try {
|
|
216
|
+
const result = await get<Resource>ById(req.params.id);
|
|
217
|
+
if (!result) {
|
|
218
|
+
res.status(404).json({ error: '<Resource> not found' });
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
res.json(result);
|
|
222
|
+
} catch (err) {
|
|
223
|
+
console.error('Get <resource> failed:', err);
|
|
224
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// PATCH /api/v1/<resources>/:id
|
|
229
|
+
router.patch('/:id', async (req: Request, res: Response) => {
|
|
230
|
+
try {
|
|
231
|
+
const result = await update<Resource>(req.params.id, req.body);
|
|
232
|
+
if (!result) {
|
|
233
|
+
res.status(404).json({ error: '<Resource> not found' });
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
res.json(result);
|
|
237
|
+
} catch (err) {
|
|
238
|
+
console.error('Update <resource> failed:', err);
|
|
239
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// DELETE /api/v1/<resources>/:id
|
|
244
|
+
router.delete('/:id', async (req: Request, res: Response) => {
|
|
245
|
+
try {
|
|
246
|
+
const found = await delete<Resource>(req.params.id);
|
|
247
|
+
if (!found) {
|
|
248
|
+
res.status(404).json({ error: '<Resource> not found' });
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
res.status(204).send();
|
|
252
|
+
} catch (err) {
|
|
253
|
+
console.error('Delete <resource> failed:', err);
|
|
254
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
export default router;
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### File 4: Wire into server — Add to `src/server.ts`
|
|
262
|
+
|
|
263
|
+
Add the import and route registration to the existing server:
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
// Add this import
|
|
267
|
+
import <resource>Routes from './routes/v1/<resource>.js';
|
|
268
|
+
|
|
269
|
+
// Add this route registration (with /api/v1/ prefix)
|
|
270
|
+
app.use('/api/v1/<resources>', <resource>Routes);
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### File 5: `tests/unit/<resource>.test.ts` — Unit tests
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
277
|
+
// Test the handler functions directly, mock the db layer
|
|
278
|
+
|
|
279
|
+
describe('<Resource> Handlers', () => {
|
|
280
|
+
describe('create<Resource>', () => {
|
|
281
|
+
it('should create a new <resource> and return response', async () => {
|
|
282
|
+
// Arrange — mock db calls
|
|
283
|
+
// Act — call create<Resource>
|
|
284
|
+
// Assert — verify response shape, timestamps set
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
describe('get<Resource>ById', () => {
|
|
289
|
+
it('should return <resource> when found', async () => {
|
|
290
|
+
// Test happy path
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should return null for invalid ObjectId', async () => {
|
|
294
|
+
// Test invalid id returns null
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('should return null when not found', async () => {
|
|
298
|
+
// Test missing doc returns null
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
describe('list<Resource>s', () => {
|
|
303
|
+
it('should return paginated results', async () => {
|
|
304
|
+
// Test pagination defaults
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('should cap limit at 100', async () => {
|
|
308
|
+
// Test max limit enforcement
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
describe('update<Resource>', () => {
|
|
313
|
+
it('should update and return updated doc', async () => {
|
|
314
|
+
// Test updatedAt is refreshed
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
describe('delete<Resource>', () => {
|
|
319
|
+
it('should return true when deleted', async () => {
|
|
320
|
+
// Test happy path
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('should return false for invalid ObjectId', async () => {
|
|
324
|
+
// Test invalid id
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
## Step 3 — Best Practices Enforced
|
|
331
|
+
|
|
332
|
+
Every generated endpoint MUST follow these rules:
|
|
333
|
+
|
|
334
|
+
### Security
|
|
335
|
+
- All user input passes through StrictDB's built-in sanitization and guardrails
|
|
336
|
+
- NEVER trust `req.body` types at runtime — validate or use Zod schemas
|
|
337
|
+
- NEVER expose `_id` directly — always map to `id: string`
|
|
338
|
+
- NEVER expose internal error details to the client
|
|
339
|
+
- ALWAYS return generic "Internal server error" for unexpected errors
|
|
340
|
+
|
|
341
|
+
### Performance
|
|
342
|
+
- Pagination on ALL list endpoints (default 20, max 100)
|
|
343
|
+
- Indexes registered for all query patterns (`registerCollection()`)
|
|
344
|
+
- Uses shared StrictDB instance (NEVER creates new connections)
|
|
345
|
+
- `$limit` enforced before `$lookup` in any join queries
|
|
346
|
+
|
|
347
|
+
### Architecture
|
|
348
|
+
- Routes are THIN — no business logic, just parse and delegate
|
|
349
|
+
- Handlers contain ALL business logic
|
|
350
|
+
- Types defined FIRST — they're the contract
|
|
351
|
+
- One handler file per resource domain
|
|
352
|
+
- One route file per resource
|
|
353
|
+
|
|
354
|
+
### Node.js Best Practices
|
|
355
|
+
- Async error handling with try/catch on every route
|
|
356
|
+
- Proper HTTP status codes (201 created, 204 no content, 404 not found)
|
|
357
|
+
- JSON responses on all endpoints (including errors)
|
|
358
|
+
- No callback-style code — async/await only
|
|
359
|
+
- Receives the shared StrictDB instance (never creates its own)
|
|
360
|
+
|
|
361
|
+
## Step 4 — Verification Checklist
|
|
362
|
+
|
|
363
|
+
After generating, verify:
|
|
364
|
+
|
|
365
|
+
- [ ] Types file created at `src/types/<resource>.ts`
|
|
366
|
+
- [ ] Handler file created at `src/handlers/<resource>.ts`
|
|
367
|
+
- [ ] Route file created at `src/routes/v1/<resource>.ts`
|
|
368
|
+
- [ ] Routes wired into `src/server.ts` with `/api/v1/` prefix
|
|
369
|
+
- [ ] Test file created at `tests/unit/<resource>.test.ts`
|
|
370
|
+
- [ ] All CRUD operations: create, read (single + list), update, delete
|
|
371
|
+
- [ ] Pagination on list endpoint (default 20, max 100)
|
|
372
|
+
- [ ] Indexes registered with `registerCollection()`
|
|
373
|
+
- [ ] No `any` types
|
|
374
|
+
- [ ] No file exceeds 300 lines
|
|
375
|
+
- [ ] _id never exposed — mapped to id string
|
|
376
|
+
- [ ] All errors caught and logged
|
|
377
|
+
- [ ] Data access through the adapter — StrictDB if installed, otherwise the native driver; never Mongoose, never a raw driver in handlers
|
|
378
|
+
|
|
379
|
+
## RuleCatch Report
|
|
380
|
+
|
|
381
|
+
After all files are created and verified, check RuleCatch:
|
|
382
|
+
|
|
383
|
+
- If the RuleCatch MCP server is available: query for violations in the new API files
|
|
384
|
+
- Report any violations found (type issues, missing assertions, security, etc.)
|
|
385
|
+
- If no MCP: remind the user — "Check your RuleCatch dashboard for any violations in the new endpoint files"
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Create a Playwright E2E test with explicit success criteria
|
|
3
|
+
scope: project
|
|
4
|
+
argument-hint: <feature-or-page-name>
|
|
5
|
+
allowed-tools: Read, Write, Grep, Glob, Bash, AskUserQuestion
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Create E2E Test
|
|
9
|
+
|
|
10
|
+
Create a Playwright E2E test for: **$ARGUMENTS**
|
|
11
|
+
|
|
12
|
+
## ABSOLUTE RULES — Read Before Writing a Single Line
|
|
13
|
+
|
|
14
|
+
### 1. Every test MUST have explicit success criteria
|
|
15
|
+
|
|
16
|
+
"Page loads" is NOT a test. Every `test()` block MUST assert:
|
|
17
|
+
- **URL** — verify the page navigated to the correct URL
|
|
18
|
+
- **Visible elements** — verify key elements are present and visible
|
|
19
|
+
- **Correct data** — verify the right content is displayed
|
|
20
|
+
- **Error states** — verify error messages show when expected
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
// CORRECT — explicit success criteria
|
|
24
|
+
test('dashboard shows user data after login', async ({ page }) => {
|
|
25
|
+
await page.goto('/login');
|
|
26
|
+
await page.fill('[name="email"]', 'test@example.com');
|
|
27
|
+
await page.fill('[name="password"]', 'password123');
|
|
28
|
+
await page.click('button[type="submit"]');
|
|
29
|
+
|
|
30
|
+
// ✅ Verify URL changed
|
|
31
|
+
await expect(page).toHaveURL('/dashboard');
|
|
32
|
+
// ✅ Verify key element visible
|
|
33
|
+
await expect(page.locator('h1')).toContainText('Welcome');
|
|
34
|
+
// ✅ Verify correct data displayed
|
|
35
|
+
await expect(page.locator('[data-testid="user-email"]')).toContainText('test@example.com');
|
|
36
|
+
// ✅ Verify sidebar loaded
|
|
37
|
+
await expect(page.locator('nav.sidebar')).toBeVisible();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// WRONG — this passes even when completely broken
|
|
41
|
+
test('dashboard loads', async ({ page }) => {
|
|
42
|
+
await page.goto('/dashboard');
|
|
43
|
+
// no assertions!
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 2. A test is FINISHED when it has ALL of these
|
|
48
|
+
|
|
49
|
+
A test is NOT done until it has:
|
|
50
|
+
- [ ] At least one `await expect(page).toHaveURL()` assertion
|
|
51
|
+
- [ ] At least one `await expect(locator).toBeVisible()` assertion
|
|
52
|
+
- [ ] At least one content/data verification (`toContainText`, `toHaveValue`, etc.)
|
|
53
|
+
- [ ] Error case coverage (what happens when it fails?)
|
|
54
|
+
- [ ] No `// TODO` or placeholder assertions
|
|
55
|
+
|
|
56
|
+
If you cannot check ALL of these, the test is incomplete. Say so and explain what's missing.
|
|
57
|
+
|
|
58
|
+
### 3. Test structure — ALWAYS follow this pattern
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import { test, expect } from '@playwright/test';
|
|
62
|
+
|
|
63
|
+
test.describe('[Feature Name]', () => {
|
|
64
|
+
test.describe('happy path', () => {
|
|
65
|
+
test('should [specific behavior] when [specific condition]', async ({ page }) => {
|
|
66
|
+
// ARRANGE — navigate, set up state
|
|
67
|
+
// ACT — perform the user action
|
|
68
|
+
// ASSERT — verify SPECIFIC outcomes (URL, elements, data)
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test.describe('error handling', () => {
|
|
73
|
+
test('should show error when [invalid input]', async ({ page }) => {
|
|
74
|
+
// Test the failure mode
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test.describe('edge cases', () => {
|
|
79
|
+
test('should handle [empty state / max values / etc]', async ({ page }) => {
|
|
80
|
+
// Test boundaries
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 4. Port configuration — ALWAYS use test ports
|
|
87
|
+
|
|
88
|
+
E2E tests run on TEST ports, never dev ports:
|
|
89
|
+
|
|
90
|
+
| Service | Test Port | Base URL |
|
|
91
|
+
|-----------|-----------|----------------------------|
|
|
92
|
+
| Website | 4000 | http://localhost:4000 |
|
|
93
|
+
| API | 4010 | http://localhost:4010 |
|
|
94
|
+
| Dashboard | 4020 | http://localhost:4020 |
|
|
95
|
+
|
|
96
|
+
The `baseURL` is already set in `playwright.config.ts`. Use relative paths:
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
// CORRECT — uses baseURL from config
|
|
100
|
+
await page.goto('/dashboard');
|
|
101
|
+
|
|
102
|
+
// WRONG — hardcoded URL
|
|
103
|
+
await page.goto('http://localhost:3000/dashboard');
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### 5. What to test for each page type
|
|
107
|
+
|
|
108
|
+
**For a page/route:**
|
|
109
|
+
- URL is correct after navigation
|
|
110
|
+
- Page title / heading is present
|
|
111
|
+
- Key UI elements are visible (nav, sidebar, footer, etc.)
|
|
112
|
+
- Dynamic data is loaded and displayed
|
|
113
|
+
- Links navigate to correct destinations
|
|
114
|
+
- Responsive behavior (if applicable)
|
|
115
|
+
|
|
116
|
+
**For a form:**
|
|
117
|
+
- Empty form shows proper validation messages
|
|
118
|
+
- Valid submission succeeds (verify success state)
|
|
119
|
+
- Invalid input shows specific error messages
|
|
120
|
+
- Submit button disabled during processing
|
|
121
|
+
- Form clears or redirects after success
|
|
122
|
+
|
|
123
|
+
**For an API endpoint:**
|
|
124
|
+
- Correct response status code
|
|
125
|
+
- Response body matches expected shape
|
|
126
|
+
- Error responses have proper status codes and messages
|
|
127
|
+
- Authentication/authorization is enforced
|
|
128
|
+
|
|
129
|
+
**For authentication:**
|
|
130
|
+
- Login with valid credentials succeeds
|
|
131
|
+
- Login with invalid credentials shows error
|
|
132
|
+
- Protected pages redirect to login
|
|
133
|
+
- Logout clears session
|
|
134
|
+
- Token expiry is handled
|
|
135
|
+
|
|
136
|
+
### 6. Naming convention
|
|
137
|
+
|
|
138
|
+
File: `tests/e2e/[feature-name].spec.ts`
|
|
139
|
+
|
|
140
|
+
Examples:
|
|
141
|
+
- `tests/e2e/auth-login.spec.ts`
|
|
142
|
+
- `tests/e2e/dashboard-overview.spec.ts`
|
|
143
|
+
- `tests/e2e/api-users.spec.ts`
|
|
144
|
+
- `tests/e2e/settings-profile.spec.ts`
|
|
145
|
+
|
|
146
|
+
## Step 0 — Auto-Branch (if on main)
|
|
147
|
+
|
|
148
|
+
Before creating any files, check the current branch:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
git branch --show-current
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**Default behavior** (`auto_branch = true` in `claude-mastery-project.conf`):
|
|
155
|
+
- If on `main` or `master`: automatically create a feature branch and switch to it:
|
|
156
|
+
```bash
|
|
157
|
+
git checkout -b test/<feature-name>
|
|
158
|
+
```
|
|
159
|
+
Report: "Created branch `test/<feature>` — main stays untouched."
|
|
160
|
+
- If already on a feature branch: proceed
|
|
161
|
+
- If not a git repo: skip this check
|
|
162
|
+
|
|
163
|
+
**To disable:** Set `auto_branch = false` in `claude-mastery-project.conf`. When disabled, warn and ask the user before proceeding on main.
|
|
164
|
+
|
|
165
|
+
## Step 1 — Gather Information
|
|
166
|
+
|
|
167
|
+
Before writing the test:
|
|
168
|
+
|
|
169
|
+
1. **Read the source code** for the feature/page being tested
|
|
170
|
+
2. **Identify all assertions** — what URLs, elements, and data should be verified?
|
|
171
|
+
3. **Identify error states** — what can go wrong? What should the user see?
|
|
172
|
+
4. **Check for test data** — does the test need seeded data? Mock API responses?
|
|
173
|
+
|
|
174
|
+
## Step 2 — Ask What to Verify (if not obvious)
|
|
175
|
+
|
|
176
|
+
If the feature has multiple possible success criteria, ask the user:
|
|
177
|
+
|
|
178
|
+
Use AskUserQuestion to clarify:
|
|
179
|
+
- "What specific elements should be visible on this page?"
|
|
180
|
+
- "What data should be displayed after this action?"
|
|
181
|
+
- "What error message should appear for invalid input?"
|
|
182
|
+
|
|
183
|
+
## Step 3 — Write the Test
|
|
184
|
+
|
|
185
|
+
Create the test file at `tests/e2e/[feature-name].spec.ts` following ALL rules above.
|
|
186
|
+
|
|
187
|
+
Every test file MUST include:
|
|
188
|
+
1. At least one happy-path test
|
|
189
|
+
2. At least one error-case test
|
|
190
|
+
3. Explicit assertions in every `test()` block
|
|
191
|
+
|
|
192
|
+
## Step 4 — Verification Checklist
|
|
193
|
+
|
|
194
|
+
After writing, verify:
|
|
195
|
+
|
|
196
|
+
- [ ] File is at `tests/e2e/[name].spec.ts`
|
|
197
|
+
- [ ] Every `test()` has at least 3 assertions (URL, element, data)
|
|
198
|
+
- [ ] Error cases are covered
|
|
199
|
+
- [ ] No hardcoded ports (uses baseURL from config)
|
|
200
|
+
- [ ] No `// TODO` placeholders
|
|
201
|
+
- [ ] Test names describe behavior: "should [verb] when [condition]"
|
|
202
|
+
- [ ] No `any` types
|
|
203
|
+
- [ ] No `.only` left in the code
|
|
204
|
+
|
|
205
|
+
## Running Tests
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
# Run all E2E tests (spawns servers automatically on test ports)
|
|
209
|
+
pnpm test:e2e
|
|
210
|
+
|
|
211
|
+
# Run a specific test file
|
|
212
|
+
pnpm test:e2e tests/e2e/auth-login.spec.ts
|
|
213
|
+
|
|
214
|
+
# Run with UI mode (debug)
|
|
215
|
+
pnpm test:e2e:ui
|
|
216
|
+
|
|
217
|
+
# Run headed (see the browser)
|
|
218
|
+
pnpm test:e2e:headed
|
|
219
|
+
|
|
220
|
+
# View the last test report
|
|
221
|
+
pnpm test:e2e:report
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## RuleCatch Report
|
|
225
|
+
|
|
226
|
+
After the test file is created and verified, check RuleCatch:
|
|
227
|
+
|
|
228
|
+
- If the RuleCatch MCP server is available: query for violations in the new test file
|
|
229
|
+
- Report any violations found (missing assertions, TypeScript issues, etc.)
|
|
230
|
+
- If no MCP: suggest checking the RuleCatch dashboard
|