spec-lite 1.1.6 → 1.1.7
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 +4 -2
- package/references/accessibility-checklist.md +160 -0
- package/references/orchestration-patterns.md +370 -0
- package/references/performance-checklist.md +153 -0
- package/references/security-checklist.md +134 -0
- package/references/testing-patterns.md +236 -0
- package/skills-overview.md +443 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# Performance Checklist
|
|
2
|
+
|
|
3
|
+
Quick reference checklist for web application performance. Use alongside the `performance-optimization` skill.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Core Web Vitals Targets](#core-web-vitals-targets)
|
|
8
|
+
- [TTFB Diagnosis](#ttfb-diagnosis)
|
|
9
|
+
- [Frontend Checklist](#frontend-checklist)
|
|
10
|
+
- [Backend Checklist](#backend-checklist)
|
|
11
|
+
- [Measurement Commands](#measurement-commands)
|
|
12
|
+
- [Common Anti-Patterns](#common-anti-patterns)
|
|
13
|
+
|
|
14
|
+
## Core Web Vitals Targets
|
|
15
|
+
|
|
16
|
+
| Metric | Good | Needs Work | Poor |
|
|
17
|
+
|--------|------|------------|------|
|
|
18
|
+
| LCP (Largest Contentful Paint) | ≤ 2.5s | ≤ 4.0s | > 4.0s |
|
|
19
|
+
| INP (Interaction to Next Paint) | ≤ 200ms | ≤ 500ms | > 500ms |
|
|
20
|
+
| CLS (Cumulative Layout Shift) | ≤ 0.1 | ≤ 0.25 | > 0.25 |
|
|
21
|
+
|
|
22
|
+
## TTFB Diagnosis
|
|
23
|
+
|
|
24
|
+
When TTFB is slow (> 800ms), check each component in DevTools Network waterfall:
|
|
25
|
+
|
|
26
|
+
- [ ] **DNS resolution** slow → add `<link rel="dns-prefetch">` or `<link rel="preconnect">` for known origins
|
|
27
|
+
- [ ] **TCP/TLS handshake** slow → enable HTTP/2, consider edge deployment, verify keep-alive
|
|
28
|
+
- [ ] **Server processing** slow → profile backend, check slow queries, add caching
|
|
29
|
+
|
|
30
|
+
## Frontend Checklist
|
|
31
|
+
|
|
32
|
+
### Images
|
|
33
|
+
- [ ] Images use modern formats (WebP, AVIF)
|
|
34
|
+
- [ ] Images are responsively sized (`srcset` and `sizes`)
|
|
35
|
+
- [ ] Images and `<source>` elements have explicit `width` and `height` (prevents CLS in art direction)
|
|
36
|
+
- [ ] Below-the-fold images use `loading="lazy"` and `decoding="async"`
|
|
37
|
+
- [ ] Hero/LCP images use `fetchpriority="high"` and no lazy loading
|
|
38
|
+
|
|
39
|
+
### JavaScript
|
|
40
|
+
- [ ] Bundle size under 200KB gzipped (initial load)
|
|
41
|
+
- [ ] Code splitting with dynamic `import()` for routes and heavy features
|
|
42
|
+
- [ ] Tree shaking enabled (verify dependency ships ESM and marks `sideEffects: false`)
|
|
43
|
+
- [ ] No blocking JavaScript in `<head>` (use `defer` or `async`)
|
|
44
|
+
- [ ] Heavy computation offloaded to Web Workers (if applicable)
|
|
45
|
+
- [ ] `React.memo()` on expensive components that re-render with same props
|
|
46
|
+
- [ ] `useMemo()` / `useCallback()` only where profiling shows benefit
|
|
47
|
+
- [ ] Long tasks (> 50ms) broken up to keep the main thread available — main lever for INP
|
|
48
|
+
- [ ] `yieldToMain` pattern used inside long-running loops so input events can run between chunks
|
|
49
|
+
- [ ] Modern scheduling APIs used where available: `scheduler.yield()` (preferred), `scheduler.postTask()` with priorities, `isInputPending()` to yield only when needed
|
|
50
|
+
- [ ] `requestIdleCallback` for deferrable, non-urgent work (analytics flush, prefetch, warmup)
|
|
51
|
+
- [ ] Non-critical work deferred out of event handlers (e.g. analytics, logging) so the response to the interaction is not delayed
|
|
52
|
+
- [ ] Third-party scripts loaded with `async` / `defer`, audited for size, and fronted by a facade when heavy (chat widgets, embeds)
|
|
53
|
+
|
|
54
|
+
### CSS
|
|
55
|
+
- [ ] Critical CSS inlined or preloaded
|
|
56
|
+
- [ ] No render-blocking CSS for non-critical styles
|
|
57
|
+
- [ ] No CSS-in-JS runtime cost in production (use extraction)
|
|
58
|
+
|
|
59
|
+
### Fonts
|
|
60
|
+
- [ ] Limited to 2–3 font families, 2–3 weights each (every additional weight is another request)
|
|
61
|
+
- [ ] WOFF2 format only (smallest, universal support — skip WOFF/TTF/EOT)
|
|
62
|
+
- [ ] Self-hosted when possible (third-party font CDNs add DNS + TCP + TLS round-trips)
|
|
63
|
+
- [ ] LCP-critical fonts preloaded: `<link rel="preload" as="font" type="font/woff2" crossorigin>`
|
|
64
|
+
- [ ] `font-display: swap` (or `optional` for non-critical) to avoid FOIT blocking render
|
|
65
|
+
- [ ] Subsetted via `unicode-range` to ship only the glyphs each page needs
|
|
66
|
+
- [ ] Variable fonts considered when multiple weights/styles are required (one file replaces many)
|
|
67
|
+
- [ ] Fallback font metrics adjusted with `size-adjust`, `ascent-override`, `descent-override` to reduce CLS on font swap
|
|
68
|
+
- [ ] System font stack considered before any custom font
|
|
69
|
+
|
|
70
|
+
### Network
|
|
71
|
+
- [ ] Static assets cached with long `max-age` + content hashing
|
|
72
|
+
- [ ] API responses cached where appropriate (`Cache-Control`)
|
|
73
|
+
- [ ] HTTP/2 or HTTP/3 enabled
|
|
74
|
+
- [ ] Resources preconnected (`<link rel="preconnect">`) for known origins
|
|
75
|
+
- [ ] `fetchpriority` used on critical non-image resources (e.g., key `<link rel="preload">`, above-the-fold `<script>`) — not only on `<img>`
|
|
76
|
+
- [ ] No unnecessary redirects
|
|
77
|
+
|
|
78
|
+
### Rendering
|
|
79
|
+
- [ ] No layout thrashing (forced synchronous layouts)
|
|
80
|
+
- [ ] Animations use `transform` and `opacity` (GPU-accelerated)
|
|
81
|
+
- [ ] Long lists use virtualization (e.g., `react-window`)
|
|
82
|
+
- [ ] No unnecessary full-page re-renders
|
|
83
|
+
- [ ] Off-screen sections use `content-visibility: auto` with `contain-intrinsic-size` to skip layout/paint of non-visible areas
|
|
84
|
+
- [ ] No `unload` event handlers and no `Cache-Control: no-store` on HTML responses — preserves back/forward cache (bfcache) eligibility
|
|
85
|
+
|
|
86
|
+
## Backend Checklist
|
|
87
|
+
|
|
88
|
+
### Database
|
|
89
|
+
- [ ] No N+1 query patterns (use eager loading / joins)
|
|
90
|
+
- [ ] Queries have appropriate indexes
|
|
91
|
+
- [ ] List endpoints paginated (never `SELECT * FROM table`)
|
|
92
|
+
- [ ] Connection pooling configured
|
|
93
|
+
- [ ] Slow query logging enabled
|
|
94
|
+
|
|
95
|
+
### API
|
|
96
|
+
- [ ] Response times < 200ms (p95)
|
|
97
|
+
- [ ] No synchronous heavy computation in request handlers
|
|
98
|
+
- [ ] Bulk operations instead of loops of individual calls
|
|
99
|
+
- [ ] Response compression (gzip/brotli)
|
|
100
|
+
- [ ] Appropriate caching (in-memory, Redis, CDN)
|
|
101
|
+
|
|
102
|
+
### Infrastructure
|
|
103
|
+
- [ ] CDN for static assets
|
|
104
|
+
- [ ] Server located close to users (or edge deployment)
|
|
105
|
+
- [ ] Horizontal scaling configured (if needed)
|
|
106
|
+
- [ ] Health check endpoint for load balancer
|
|
107
|
+
|
|
108
|
+
## Measurement Commands
|
|
109
|
+
|
|
110
|
+
### INP field data and DevTools workflow
|
|
111
|
+
|
|
112
|
+
1. **Field data first** — check [CrUX Vis](https://developer.chrome.com/docs/crux/vis) or your RUM tool for real-user INP before optimising
|
|
113
|
+
2. **Identify slow interactions** — open DevTools → Performance panel → record while interacting; look for long tasks triggered by clicks/keystrokes
|
|
114
|
+
3. **Test on mid-range Android** — INP issues often only surface on slower hardware; use a real device or DevTools CPU throttling (4×–6× slowdown)
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
# Lighthouse CLI
|
|
118
|
+
npx lighthouse https://localhost:3000 --output json --output-path ./report.json
|
|
119
|
+
|
|
120
|
+
# Bundle analysis
|
|
121
|
+
npx webpack-bundle-analyzer stats.json
|
|
122
|
+
# or for Vite:
|
|
123
|
+
npx vite-bundle-visualizer
|
|
124
|
+
|
|
125
|
+
# Check bundle size
|
|
126
|
+
npx bundlesize
|
|
127
|
+
|
|
128
|
+
# Web Vitals in code
|
|
129
|
+
import { onLCP, onINP, onCLS } from 'web-vitals';
|
|
130
|
+
onLCP(console.log);
|
|
131
|
+
onINP(console.log);
|
|
132
|
+
onCLS(console.log);
|
|
133
|
+
|
|
134
|
+
# INP with interaction-level detail (attribution build)
|
|
135
|
+
import { onINP } from 'web-vitals/attribution';
|
|
136
|
+
onINP(({ value, attribution }) => {
|
|
137
|
+
const { interactionTarget, inputDelay, processingDuration, presentationDelay } = attribution;
|
|
138
|
+
console.log({ value, interactionTarget, inputDelay, processingDuration, presentationDelay });
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Common Anti-Patterns
|
|
143
|
+
|
|
144
|
+
| Anti-Pattern | Impact | Fix |
|
|
145
|
+
|---|---|---|
|
|
146
|
+
| N+1 queries | Linear DB load growth | Use joins, includes, or batch loading |
|
|
147
|
+
| Unbounded queries | Memory exhaustion, timeouts | Always paginate, add LIMIT |
|
|
148
|
+
| Missing indexes | Slow reads as data grows | Add indexes for filtered/sorted columns |
|
|
149
|
+
| Layout thrashing | Jank, dropped frames | Batch DOM reads, then batch writes |
|
|
150
|
+
| Unoptimized images | Slow LCP, wasted bandwidth | Use WebP, responsive sizes, lazy load |
|
|
151
|
+
| Large bundles | Slow Time to Interactive | Code split, tree shake, audit deps |
|
|
152
|
+
| Blocking main thread | Poor INP, unresponsive UI | Chunk long tasks with `scheduler.yield()` / `yieldToMain`, offload to Web Workers |
|
|
153
|
+
| Memory leaks | Growing memory, eventual crash | Clean up listeners, intervals, refs |
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Security Checklist
|
|
2
|
+
|
|
3
|
+
Quick reference for web application security. Use alongside the `security-and-hardening` skill.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Pre-Commit Checks](#pre-commit-checks)
|
|
8
|
+
- [Authentication](#authentication)
|
|
9
|
+
- [Authorization](#authorization)
|
|
10
|
+
- [Input Validation](#input-validation)
|
|
11
|
+
- [Security Headers](#security-headers)
|
|
12
|
+
- [CORS Configuration](#cors-configuration)
|
|
13
|
+
- [Data Protection](#data-protection)
|
|
14
|
+
- [Dependency Security](#dependency-security)
|
|
15
|
+
- [Error Handling](#error-handling)
|
|
16
|
+
- [OWASP Top 10 Quick Reference](#owasp-top-10-quick-reference)
|
|
17
|
+
|
|
18
|
+
## Pre-Commit Checks
|
|
19
|
+
|
|
20
|
+
- [ ] No secrets in code (`git diff --cached | grep -i "password\|secret\|api_key\|token"`)
|
|
21
|
+
- [ ] `.gitignore` covers: `.env`, `.env.local`, `*.pem`, `*.key`
|
|
22
|
+
- [ ] `.env.example` uses placeholder values (not real secrets)
|
|
23
|
+
|
|
24
|
+
## Authentication
|
|
25
|
+
|
|
26
|
+
- [ ] Passwords hashed with bcrypt (≥12 rounds), scrypt, or argon2
|
|
27
|
+
- [ ] Session cookies: `httpOnly`, `secure`, `sameSite: 'lax'`
|
|
28
|
+
- [ ] Session expiration configured (reasonable max-age)
|
|
29
|
+
- [ ] Rate limiting on login endpoint (≤10 attempts per 15 minutes)
|
|
30
|
+
- [ ] Password reset tokens: time-limited (≤1 hour), single-use
|
|
31
|
+
- [ ] Account lockout after repeated failures (optional, with notification)
|
|
32
|
+
- [ ] MFA supported for sensitive operations (optional but recommended)
|
|
33
|
+
|
|
34
|
+
## Authorization
|
|
35
|
+
|
|
36
|
+
- [ ] Every protected endpoint checks authentication
|
|
37
|
+
- [ ] Every resource access checks ownership/role (prevents IDOR)
|
|
38
|
+
- [ ] Admin endpoints require admin role verification
|
|
39
|
+
- [ ] API keys scoped to minimum necessary permissions
|
|
40
|
+
- [ ] JWT tokens validated (signature, expiration, issuer)
|
|
41
|
+
|
|
42
|
+
## Input Validation
|
|
43
|
+
|
|
44
|
+
- [ ] All user input validated at system boundaries (API routes, form handlers)
|
|
45
|
+
- [ ] Validation uses allowlists (not denylists)
|
|
46
|
+
- [ ] String lengths constrained (min/max)
|
|
47
|
+
- [ ] Numeric ranges validated
|
|
48
|
+
- [ ] Email, URL, and date formats validated with proper libraries
|
|
49
|
+
- [ ] File uploads: type restricted, size limited, content verified
|
|
50
|
+
- [ ] SQL queries parameterized (no string concatenation)
|
|
51
|
+
- [ ] HTML output encoded (use framework auto-escaping)
|
|
52
|
+
- [ ] URLs validated before redirect (prevent open redirect)
|
|
53
|
+
|
|
54
|
+
## Security Headers
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
Content-Security-Policy: default-src 'self'; script-src 'self'
|
|
58
|
+
Strict-Transport-Security: max-age=31536000; includeSubDomains
|
|
59
|
+
X-Content-Type-Options: nosniff
|
|
60
|
+
X-Frame-Options: DENY
|
|
61
|
+
X-XSS-Protection: 0 (disabled, rely on CSP)
|
|
62
|
+
Referrer-Policy: strict-origin-when-cross-origin
|
|
63
|
+
Permissions-Policy: camera=(), microphone=(), geolocation=()
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## CORS Configuration
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
// Restrictive (recommended)
|
|
70
|
+
cors({
|
|
71
|
+
origin: ['https://yourdomain.com', 'https://app.yourdomain.com'],
|
|
72
|
+
credentials: true,
|
|
73
|
+
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
|
|
74
|
+
allowedHeaders: ['Content-Type', 'Authorization'],
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
// NEVER use in production:
|
|
78
|
+
cors({ origin: '*' }) // Allows any origin
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Data Protection
|
|
82
|
+
|
|
83
|
+
- [ ] Sensitive fields excluded from API responses (`passwordHash`, `resetToken`, etc.)
|
|
84
|
+
- [ ] Sensitive data not logged (passwords, tokens, full CC numbers)
|
|
85
|
+
- [ ] PII encrypted at rest (if required by regulation)
|
|
86
|
+
- [ ] HTTPS for all external communication
|
|
87
|
+
- [ ] Database backups encrypted
|
|
88
|
+
|
|
89
|
+
## Dependency Security
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# Audit dependencies
|
|
93
|
+
npm audit
|
|
94
|
+
|
|
95
|
+
# Fix automatically where possible
|
|
96
|
+
npm audit fix
|
|
97
|
+
|
|
98
|
+
# Check for critical vulnerabilities
|
|
99
|
+
npm audit --audit-level=critical
|
|
100
|
+
|
|
101
|
+
# Keep dependencies updated
|
|
102
|
+
npx npm-check-updates
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Error Handling
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
// Production: generic error, no internals
|
|
109
|
+
res.status(500).json({
|
|
110
|
+
error: { code: 'INTERNAL_ERROR', message: 'Something went wrong' }
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// NEVER in production:
|
|
114
|
+
res.status(500).json({
|
|
115
|
+
error: err.message,
|
|
116
|
+
stack: err.stack, // Exposes internals
|
|
117
|
+
query: err.sql, // Exposes database details
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## OWASP Top 10 Quick Reference
|
|
122
|
+
|
|
123
|
+
| # | Vulnerability | Prevention |
|
|
124
|
+
|---|---|---|
|
|
125
|
+
| 1 | Broken Access Control | Auth checks on every endpoint, ownership verification |
|
|
126
|
+
| 2 | Cryptographic Failures | HTTPS, strong hashing, no secrets in code |
|
|
127
|
+
| 3 | Injection | Parameterized queries, input validation |
|
|
128
|
+
| 4 | Insecure Design | Threat modeling, spec-driven development |
|
|
129
|
+
| 5 | Security Misconfiguration | Security headers, minimal permissions, audit deps |
|
|
130
|
+
| 6 | Vulnerable Components | `npm audit`, keep deps updated, minimal deps |
|
|
131
|
+
| 7 | Auth Failures | Strong passwords, rate limiting, session management |
|
|
132
|
+
| 8 | Data Integrity Failures | Verify updates/dependencies, signed artifacts |
|
|
133
|
+
| 9 | Logging Failures | Log security events, don't log secrets |
|
|
134
|
+
| 10 | SSRF | Validate/allowlist URLs, restrict outbound requests |
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# Testing Patterns Reference
|
|
2
|
+
|
|
3
|
+
Quick reference for common testing patterns across the stack. Use alongside the `test-driven-development` skill.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Test Structure (Arrange-Act-Assert)](#test-structure-arrange-act-assert)
|
|
8
|
+
- [Test Naming Conventions](#test-naming-conventions)
|
|
9
|
+
- [Common Assertions](#common-assertions)
|
|
10
|
+
- [Mocking Patterns](#mocking-patterns)
|
|
11
|
+
- [React/Component Testing](#reactcomponent-testing)
|
|
12
|
+
- [API / Integration Testing](#api--integration-testing)
|
|
13
|
+
- [E2E Testing (Playwright)](#e2e-testing-playwright)
|
|
14
|
+
- [Test Anti-Patterns](#test-anti-patterns)
|
|
15
|
+
|
|
16
|
+
## Test Structure (Arrange-Act-Assert)
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
it('describes expected behavior', () => {
|
|
20
|
+
// Arrange: Set up test data and preconditions
|
|
21
|
+
const input = { title: 'Test Task', priority: 'high' };
|
|
22
|
+
|
|
23
|
+
// Act: Perform the action being tested
|
|
24
|
+
const result = createTask(input);
|
|
25
|
+
|
|
26
|
+
// Assert: Verify the outcome
|
|
27
|
+
expect(result.title).toBe('Test Task');
|
|
28
|
+
expect(result.priority).toBe('high');
|
|
29
|
+
expect(result.status).toBe('pending');
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Test Naming Conventions
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
// Pattern: [unit] [expected behavior] [condition]
|
|
37
|
+
describe('TaskService.createTask', () => {
|
|
38
|
+
it('creates a task with default pending status', () => {});
|
|
39
|
+
it('throws ValidationError when title is empty', () => {});
|
|
40
|
+
it('trims whitespace from title', () => {});
|
|
41
|
+
it('generates a unique ID for each task', () => {});
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Common Assertions
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
// Equality
|
|
49
|
+
expect(result).toBe(expected); // Strict equality (===)
|
|
50
|
+
expect(result).toEqual(expected); // Deep equality (objects/arrays)
|
|
51
|
+
expect(result).toStrictEqual(expected); // Deep equality + type matching
|
|
52
|
+
|
|
53
|
+
// Truthiness
|
|
54
|
+
expect(result).toBeTruthy();
|
|
55
|
+
expect(result).toBeFalsy();
|
|
56
|
+
expect(result).toBeNull();
|
|
57
|
+
expect(result).toBeDefined();
|
|
58
|
+
expect(result).toBeUndefined();
|
|
59
|
+
|
|
60
|
+
// Numbers
|
|
61
|
+
expect(result).toBeGreaterThan(5);
|
|
62
|
+
expect(result).toBeLessThanOrEqual(10);
|
|
63
|
+
expect(result).toBeCloseTo(0.3, 5); // Floating point
|
|
64
|
+
|
|
65
|
+
// Strings
|
|
66
|
+
expect(result).toMatch(/pattern/);
|
|
67
|
+
expect(result).toContain('substring');
|
|
68
|
+
|
|
69
|
+
// Arrays / Objects
|
|
70
|
+
expect(array).toContain(item);
|
|
71
|
+
expect(array).toHaveLength(3);
|
|
72
|
+
expect(object).toHaveProperty('key', 'value');
|
|
73
|
+
|
|
74
|
+
// Errors
|
|
75
|
+
expect(() => fn()).toThrow();
|
|
76
|
+
expect(() => fn()).toThrow(ValidationError);
|
|
77
|
+
expect(() => fn()).toThrow('specific message');
|
|
78
|
+
|
|
79
|
+
// Async
|
|
80
|
+
await expect(asyncFn()).resolves.toBe(value);
|
|
81
|
+
await expect(asyncFn()).rejects.toThrow(Error);
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Mocking Patterns
|
|
85
|
+
|
|
86
|
+
### Mock Functions
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
const mockFn = jest.fn();
|
|
90
|
+
mockFn.mockReturnValue(42);
|
|
91
|
+
mockFn.mockResolvedValue({ data: 'test' });
|
|
92
|
+
mockFn.mockImplementation((x) => x * 2);
|
|
93
|
+
|
|
94
|
+
expect(mockFn).toHaveBeenCalled();
|
|
95
|
+
expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2');
|
|
96
|
+
expect(mockFn).toHaveBeenCalledTimes(3);
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Mock Modules
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
// Mock an entire module
|
|
103
|
+
jest.mock('./database', () => ({
|
|
104
|
+
query: jest.fn().mockResolvedValue([{ id: 1, title: 'Test' }]),
|
|
105
|
+
}));
|
|
106
|
+
|
|
107
|
+
// Mock specific exports
|
|
108
|
+
jest.mock('./utils', () => ({
|
|
109
|
+
...jest.requireActual('./utils'),
|
|
110
|
+
generateId: jest.fn().mockReturnValue('test-id'),
|
|
111
|
+
}));
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Mock at Boundaries Only
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
Mock these: Don't mock these:
|
|
118
|
+
├── Database calls ├── Internal utility functions
|
|
119
|
+
├── HTTP requests ├── Business logic
|
|
120
|
+
├── File system operations ├── Data transformations
|
|
121
|
+
├── External API calls ├── Validation functions
|
|
122
|
+
└── Time/Date (when needed) └── Pure functions
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## React/Component Testing
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
129
|
+
|
|
130
|
+
describe('TaskForm', () => {
|
|
131
|
+
it('submits the form with entered data', async () => {
|
|
132
|
+
const onSubmit = jest.fn();
|
|
133
|
+
render(<TaskForm onSubmit={onSubmit} />);
|
|
134
|
+
|
|
135
|
+
// Find elements by accessible role/label (not test IDs)
|
|
136
|
+
await screen.findByRole('textbox', { name: /title/i });
|
|
137
|
+
fireEvent.change(screen.getByRole('textbox', { name: /title/i }), {
|
|
138
|
+
target: { value: 'New Task' },
|
|
139
|
+
});
|
|
140
|
+
fireEvent.click(screen.getByRole('button', { name: /create/i }));
|
|
141
|
+
|
|
142
|
+
await waitFor(() => {
|
|
143
|
+
expect(onSubmit).toHaveBeenCalledWith({ title: 'New Task' });
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('shows validation error for empty title', async () => {
|
|
148
|
+
render(<TaskForm onSubmit={jest.fn()} />);
|
|
149
|
+
|
|
150
|
+
fireEvent.click(screen.getByRole('button', { name: /create/i }));
|
|
151
|
+
|
|
152
|
+
expect(await screen.findByText(/title is required/i)).toBeInTheDocument();
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## API / Integration Testing
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
import request from 'supertest';
|
|
161
|
+
import { app } from '../src/app';
|
|
162
|
+
|
|
163
|
+
describe('POST /api/tasks', () => {
|
|
164
|
+
it('creates a task and returns 201', async () => {
|
|
165
|
+
const response = await request(app)
|
|
166
|
+
.post('/api/tasks')
|
|
167
|
+
.send({ title: 'Test Task' })
|
|
168
|
+
.set('Authorization', `Bearer ${testToken}`)
|
|
169
|
+
.expect(201);
|
|
170
|
+
|
|
171
|
+
expect(response.body).toMatchObject({
|
|
172
|
+
id: expect.any(String),
|
|
173
|
+
title: 'Test Task',
|
|
174
|
+
status: 'pending',
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('returns 422 for invalid input', async () => {
|
|
179
|
+
const response = await request(app)
|
|
180
|
+
.post('/api/tasks')
|
|
181
|
+
.send({ title: '' })
|
|
182
|
+
.set('Authorization', `Bearer ${testToken}`)
|
|
183
|
+
.expect(422);
|
|
184
|
+
|
|
185
|
+
expect(response.body.error.code).toBe('VALIDATION_ERROR');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('returns 401 without authentication', async () => {
|
|
189
|
+
await request(app)
|
|
190
|
+
.post('/api/tasks')
|
|
191
|
+
.send({ title: 'Test' })
|
|
192
|
+
.expect(401);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## E2E Testing (Playwright)
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
import { test, expect } from '@playwright/test';
|
|
201
|
+
|
|
202
|
+
test('user can create and complete a task', async ({ page }) => {
|
|
203
|
+
// Navigate and authenticate
|
|
204
|
+
await page.goto('/');
|
|
205
|
+
await page.fill('[name="email"]', 'test@example.com');
|
|
206
|
+
await page.fill('[name="password"]', 'testpass123');
|
|
207
|
+
await page.click('button:has-text("Log in")');
|
|
208
|
+
|
|
209
|
+
// Create a task
|
|
210
|
+
await page.click('button:has-text("New Task")');
|
|
211
|
+
await page.fill('[name="title"]', 'Buy groceries');
|
|
212
|
+
await page.click('button:has-text("Create")');
|
|
213
|
+
|
|
214
|
+
// Verify task appears
|
|
215
|
+
await expect(page.locator('text=Buy groceries')).toBeVisible();
|
|
216
|
+
|
|
217
|
+
// Complete the task
|
|
218
|
+
await page.click('[aria-label="Complete Buy groceries"]');
|
|
219
|
+
await expect(page.locator('text=Buy groceries')).toHaveCSS(
|
|
220
|
+
'text-decoration-line', 'line-through'
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Test Anti-Patterns
|
|
226
|
+
|
|
227
|
+
| Anti-Pattern | Problem | Better Approach |
|
|
228
|
+
|---|---|---|
|
|
229
|
+
| Testing implementation details | Breaks on refactor | Test inputs/outputs |
|
|
230
|
+
| Snapshot everything | No one reviews snapshot diffs | Assert specific values |
|
|
231
|
+
| Shared mutable state | Tests pollute each other | Setup/teardown per test |
|
|
232
|
+
| Testing third-party code | Wastes time, not your bug | Mock the boundary |
|
|
233
|
+
| Skipping tests to pass CI | Hides real bugs | Fix or delete the test |
|
|
234
|
+
| Using `test.skip` permanently | Dead code | Remove or fix it |
|
|
235
|
+
| Overly broad assertions | Doesn't catch regressions | Be specific |
|
|
236
|
+
| No async error handling | Swallowed errors, false passes | Always `await` async tests |
|