@wipal/agent-team 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/rules/common/general-rules.md +141 -0
- package/.claude/rules/lessons/lessons.md +91 -0
- package/.claude/rules/role-rules/dev-fe-rules.md +146 -0
- package/.claude/rules/role-rules/sa-rules.md +226 -0
- package/.claude/skills/SKILL-INDEX.md +299 -0
- package/.claude/skills/community/security-validator/SKILL.md +392 -0
- package/.claude/skills/core/agent-creation/SKILL.md +338 -0
- package/.claude/skills/core/code-review/SKILL.md +154 -0
- package/.claude/skills/core/git-automation/SKILL.md +93 -0
- package/.claude/skills/core/retrospect-work/SKILL.md +172 -0
- package/.claude/skills/domain/architecture/adr-writing/SKILL.md +254 -0
- package/.claude/skills/domain/architecture/adr-writing/references/adr-best-practices.md +257 -0
- package/.claude/skills/domain/architecture/adr-writing/references/adr-examples.md +246 -0
- package/.claude/skills/domain/architecture/adr-writing/references/adr-template.md +160 -0
- package/.claude/skills/domain/architecture/architecture-patterns/SKILL.md +316 -0
- package/.claude/skills/domain/architecture/architecture-patterns/references/event-driven.md +393 -0
- package/.claude/skills/domain/architecture/architecture-patterns/references/microservices.md +315 -0
- package/.claude/skills/domain/architecture/architecture-patterns/references/monolith.md +321 -0
- package/.claude/skills/domain/architecture/architecture-patterns/references/serverless.md +457 -0
- package/.claude/skills/domain/architecture/performance-engineering/SKILL.md +227 -0
- package/.claude/skills/domain/architecture/performance-engineering/references/benchmarking.md +336 -0
- package/.claude/skills/domain/architecture/performance-engineering/references/caching-strategies.md +284 -0
- package/.claude/skills/domain/architecture/performance-engineering/references/optimization.md +298 -0
- package/.claude/skills/domain/architecture/security-architecture/SKILL.md +206 -0
- package/.claude/skills/domain/architecture/security-architecture/references/auth-patterns.md +209 -0
- package/.claude/skills/domain/architecture/security-architecture/references/compliance.md +246 -0
- package/.claude/skills/domain/architecture/security-architecture/references/threat-modeling.md +219 -0
- package/.claude/skills/domain/architecture/system-design/SKILL.md +227 -0
- package/.claude/skills/domain/architecture/system-design/references/distributed-systems.md +231 -0
- package/.claude/skills/domain/architecture/system-design/references/resilience.md +344 -0
- package/.claude/skills/domain/architecture/system-design/references/scalability.md +303 -0
- package/.claude/skills/domain/architecture/tech-selection/SKILL.md +192 -0
- package/.claude/skills/domain/architecture/tech-selection/references/build-vs-buy.md +258 -0
- package/.claude/skills/domain/architecture/tech-selection/references/evaluation-framework.md +203 -0
- package/.claude/skills/domain/architecture/tech-selection/references/tech-radar.md +257 -0
- package/.claude/skills/domain/backend/api-design/SKILL.md +121 -0
- package/.claude/skills/domain/backend/database-design/SKILL.md +156 -0
- package/.claude/skills/domain/backend/performance-be/SKILL.md +210 -0
- package/.claude/skills/domain/backend/security/SKILL.md +138 -0
- package/.claude/skills/domain/backend/testing-be/SKILL.md +203 -0
- package/.claude/skills/domain/devops/ci-cd/SKILL.md +188 -0
- package/.claude/skills/domain/devops/containerization/SKILL.md +177 -0
- package/.claude/skills/domain/devops/deployment/SKILL.md +198 -0
- package/.claude/skills/domain/devops/infrastructure-as-code/SKILL.md +178 -0
- package/.claude/skills/domain/devops/monitoring/SKILL.md +163 -0
- package/.claude/skills/domain/frontend/accessibility/SKILL.md +179 -0
- package/.claude/skills/domain/frontend/frontend-design/SKILL.md +138 -0
- package/.claude/skills/domain/frontend/performance-fe/SKILL.md +195 -0
- package/.claude/skills/domain/frontend/state-management/SKILL.md +190 -0
- package/.claude/skills/domain/frontend/testing-fe/SKILL.md +193 -0
- package/.claude/skills/domain/product/requirements-gathering/SKILL.md +136 -0
- package/.claude/skills/domain/product/roadmap-planning/SKILL.md +169 -0
- package/.claude/skills/domain/product/sprint-planning/SKILL.md +151 -0
- package/.claude/skills/domain/product/stakeholder-communication/SKILL.md +162 -0
- package/.claude/skills/domain/product/user-stories/SKILL.md +141 -0
- package/.claude/skills/domain/quality/bug-reporting/SKILL.md +150 -0
- package/.claude/skills/domain/quality/regression-testing/SKILL.md +178 -0
- package/.claude/skills/domain/quality/test-automation/SKILL.md +185 -0
- package/.claude/skills/domain/quality/test-planning/SKILL.md +177 -0
- package/.claude/skills/leadership/code-review-advanced/SKILL.md +167 -0
- package/.claude/skills/leadership/mentoring/SKILL.md +151 -0
- package/.claude/skills/leadership/technical-debt/SKILL.md +166 -0
- package/.claude/skills/leadership/technical-decision/SKILL.md +160 -0
- package/.claude/skills/security-reports/.gitkeep +0 -0
- package/.claude/skills/skills-registry.yaml +441 -0
- package/README.md +232 -0
- package/bin/agent-team.js +107 -0
- package/package.json +51 -0
- package/src/commands/add.js +227 -0
- package/src/commands/init.js +136 -0
- package/src/commands/list.js +66 -0
- package/src/commands/remove.js +71 -0
- package/src/commands/switch.js +53 -0
- package/src/index.js +11 -0
- package/src/interactive/prompts.js +153 -0
- package/src/server/api/agents.js +150 -0
- package/src/server/api/roles.js +97 -0
- package/src/server/api/skills.js +79 -0
- package/src/server/index.js +78 -0
- package/src/ui/agents.html +174 -0
- package/src/ui/css/styles.css +470 -0
- package/src/ui/index.html +107 -0
- package/src/ui/roles.html +371 -0
- package/src/ui/skills.html +332 -0
- package/src/utils/file-utils.js +193 -0
- package/src/utils/skill-resolver.js +594 -0
- package/src/utils/skill-scanner.js +154 -0
- package/templates/CLAUDE.md.tmpl +42 -0
- package/templates/knowledge.md.tmpl +31 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: frontend-design
|
|
3
|
+
description: |
|
|
4
|
+
Create distinctive, production-grade frontend interfaces. Use when building
|
|
5
|
+
UI components, pages, websites, dashboards, React/Vue components, HTML/CSS
|
|
6
|
+
layouts, or when styling/beautifying any web UI. Use when user mentions
|
|
7
|
+
"design", "UI", "component", "page", "style", "dashboard", "landing page".
|
|
8
|
+
Make designs UNFORGETTABLE - avoid generic AI aesthetics.
|
|
9
|
+
version: 1.0.0
|
|
10
|
+
category: frontend
|
|
11
|
+
tags:
|
|
12
|
+
- ui
|
|
13
|
+
- ux
|
|
14
|
+
- design
|
|
15
|
+
- components
|
|
16
|
+
depends_on:
|
|
17
|
+
- code-review
|
|
18
|
+
recommends:
|
|
19
|
+
- accessibility
|
|
20
|
+
- state-management
|
|
21
|
+
used_by:
|
|
22
|
+
- accessibility
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
# Skill: Frontend Design
|
|
26
|
+
|
|
27
|
+
## Core Principle
|
|
28
|
+
**Design with intention.** Bold minimalism and refined maximalism both work - mediocrity never does. Every choice should be deliberate.
|
|
29
|
+
|
|
30
|
+
## Hard Rules
|
|
31
|
+
|
|
32
|
+
1. **NEVER use Inter, Roboto, Arial, system fonts** - Choose distinctive, characterful fonts
|
|
33
|
+
2. **NEVER use purple gradients on white** - Most AI-generated aesthetic
|
|
34
|
+
3. **NEVER use Space Grotesk repeatedly** - Vary font choices
|
|
35
|
+
4. **ALWAYS commit to an aesthetic** - Timid design is bad design
|
|
36
|
+
5. **ALWAYS consider motion** - Animations create delight
|
|
37
|
+
6. **ALWAYS test responsive** - Mobile is not optional
|
|
38
|
+
|
|
39
|
+
## Design Decision Framework
|
|
40
|
+
|
|
41
|
+
### Before Coding
|
|
42
|
+
1. **Purpose**: What problem does this solve? Who uses it?
|
|
43
|
+
2. **Tone**: Pick extreme - brutalist, maximalist, organic, luxury, playful, editorial...
|
|
44
|
+
3. **Differentiation**: What makes this UNFORGETTABLE?
|
|
45
|
+
|
|
46
|
+
### Typography
|
|
47
|
+
- Choose fonts with character
|
|
48
|
+
- Pair distinctive display + refined body
|
|
49
|
+
- Avoid generic defaults
|
|
50
|
+
|
|
51
|
+
### Color
|
|
52
|
+
- Commit to cohesive palette
|
|
53
|
+
- Dominant colors with sharp accents
|
|
54
|
+
- Avoid evenly-distributed "safe" palettes
|
|
55
|
+
|
|
56
|
+
### Motion
|
|
57
|
+
- One well-orchestrated page load > scattered micro-interactions
|
|
58
|
+
- Use `animation-delay` for staggered reveals
|
|
59
|
+
- Hover states should surprise
|
|
60
|
+
|
|
61
|
+
### Layout
|
|
62
|
+
- Unexpected layouts, asymmetry, grid-breaking
|
|
63
|
+
- Generous negative space OR controlled density
|
|
64
|
+
- Not predictable 3-column layouts
|
|
65
|
+
|
|
66
|
+
## Common Mistakes
|
|
67
|
+
|
|
68
|
+
| ❌ Mistake | ✅ Fix |
|
|
69
|
+
|------------|--------|
|
|
70
|
+
| Inter/Roboto font | Characterful fonts like Instrument Serif |
|
|
71
|
+
| Purple gradient on white | Bold single color or unexpected palette |
|
|
72
|
+
| Predictable 3-column | Asymmetry, overlap, diagonal flow |
|
|
73
|
+
| No animation | CSS transitions, staggered reveals |
|
|
74
|
+
| Safe, even spacing | Generous space OR controlled density |
|
|
75
|
+
| Same design every time | Vary between light/dark, different aesthetics |
|
|
76
|
+
|
|
77
|
+
## Quick Patterns
|
|
78
|
+
|
|
79
|
+
```css
|
|
80
|
+
/* Distinctive typography */
|
|
81
|
+
font-family: 'Instrument Serif', serif;
|
|
82
|
+
font-family: 'DM Sans', sans-serif;
|
|
83
|
+
|
|
84
|
+
/* Commit to color */
|
|
85
|
+
--primary: #1a1a1a;
|
|
86
|
+
--accent: #ff3366;
|
|
87
|
+
|
|
88
|
+
/* Motion */
|
|
89
|
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
90
|
+
|
|
91
|
+
/* Break the grid */
|
|
92
|
+
transform: translateY(-20px);
|
|
93
|
+
mix-blend-mode: difference;
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Component Patterns
|
|
97
|
+
|
|
98
|
+
### Atomic Structure
|
|
99
|
+
```text
|
|
100
|
+
components/
|
|
101
|
+
├── atoms/ # Button, Input, Label
|
|
102
|
+
├── molecules/ # SearchBar, FormField
|
|
103
|
+
├── organisms/ # Header, Sidebar
|
|
104
|
+
├── templates/ # PageLayouts
|
|
105
|
+
└── pages/ # Complete pages
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Compound Components
|
|
109
|
+
```tsx
|
|
110
|
+
<Card>
|
|
111
|
+
<Card.Header>Title</Card.Header>
|
|
112
|
+
<Card.Body>Content</Card.Body>
|
|
113
|
+
<Card.Footer>Action</Card.Footer>
|
|
114
|
+
</Card>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Accessibility (Non-negotiable)
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
// Icon button needs label
|
|
121
|
+
<button aria-label="Close">
|
|
122
|
+
<XIcon />
|
|
123
|
+
</button>
|
|
124
|
+
|
|
125
|
+
// Form field with hint
|
|
126
|
+
<label htmlFor="email">Email</label>
|
|
127
|
+
<input id="email" aria-describedby="email-hint" />
|
|
128
|
+
<span id="email-hint">We'll never share</span>
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Output Checklist
|
|
132
|
+
|
|
133
|
+
- [ ] Typography: Distinctive, not generic
|
|
134
|
+
- [ ] Color: Cohesive, committed
|
|
135
|
+
- [ ] Layout: Intentional, not default
|
|
136
|
+
- [ ] Motion: Purposeful
|
|
137
|
+
- [ ] Responsive: Works at 375px
|
|
138
|
+
- [ ] Accessible: ARIA labels, focus states
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: performance-fe
|
|
3
|
+
description: |
|
|
4
|
+
React/frontend performance optimization patterns. Use when: optimizing render
|
|
5
|
+
performance, reducing bundle size, improving Core Web Vitals, or when user
|
|
6
|
+
mentions "performance", "slow", "optimize", "bundle size", "Lighthouse", "LCP".
|
|
7
|
+
Fast apps = happy users.
|
|
8
|
+
version: 1.0.0
|
|
9
|
+
category: frontend
|
|
10
|
+
tags:
|
|
11
|
+
- performance
|
|
12
|
+
- optimization
|
|
13
|
+
- core-web-vitals
|
|
14
|
+
- bundle-size
|
|
15
|
+
depends_on:
|
|
16
|
+
- frontend-design
|
|
17
|
+
recommends: []
|
|
18
|
+
used_by: []
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
# Skill: Performance FE
|
|
22
|
+
|
|
23
|
+
## Core Principle
|
|
24
|
+
**Measure before optimizing.** Premature optimization is waste. Fix real bottlenecks.
|
|
25
|
+
|
|
26
|
+
## Core Web Vitals
|
|
27
|
+
|
|
28
|
+
| Metric | Target | What |
|
|
29
|
+
|--------|--------|------|
|
|
30
|
+
| **LCP** | < 2.5s | Largest Contentful Paint |
|
|
31
|
+
| **FID** | < 100ms | First Input Delay |
|
|
32
|
+
| **CLS** | < 0.1 | Cumulative Layout Shift |
|
|
33
|
+
|
|
34
|
+
## Hard Rules
|
|
35
|
+
|
|
36
|
+
1. **NEVER optimize without measuring** - Profile first
|
|
37
|
+
2. **NEVER use useEffect for derived state** - Compute in render
|
|
38
|
+
3. **ALWAYS lazy load below fold** - Code splitting
|
|
39
|
+
4. **ALWAYS memoize expensive computes** - useMemo, useCallback
|
|
40
|
+
5. **ALWAYS optimize images** - WebP, lazy load, srcset
|
|
41
|
+
|
|
42
|
+
## React Performance Patterns
|
|
43
|
+
|
|
44
|
+
### Memoization
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
import { memo, useMemo, useCallback } from 'react';
|
|
48
|
+
|
|
49
|
+
// Memo component to prevent re-renders
|
|
50
|
+
const ExpensiveComponent = memo(function ExpensiveComponent({ data }) {
|
|
51
|
+
return <div>{/* expensive render */}</div>;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Memo expensive computation
|
|
55
|
+
function Dashboard({ items }) {
|
|
56
|
+
const sortedItems = useMemo(
|
|
57
|
+
() => items.sort((a, b) => b.value - a.value),
|
|
58
|
+
[items]
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Memo callback passed to children
|
|
63
|
+
function Parent() {
|
|
64
|
+
const handleClick = useCallback((id) => {
|
|
65
|
+
console.log(id);
|
|
66
|
+
}, []);
|
|
67
|
+
|
|
68
|
+
return <Child onClick={handleClick} />;
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### List Virtualization
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
import { FixedSizeList } from 'react-window';
|
|
76
|
+
|
|
77
|
+
function VirtualList({ items }) {
|
|
78
|
+
return (
|
|
79
|
+
<FixedSizeList
|
|
80
|
+
height={600}
|
|
81
|
+
itemCount={items.length}
|
|
82
|
+
itemSize={50}
|
|
83
|
+
>
|
|
84
|
+
{({ index, style }) => (
|
|
85
|
+
<div style={style}>{items[index].name}</div>
|
|
86
|
+
)}
|
|
87
|
+
</FixedSizeList>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Code Splitting
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
import { lazy, Suspense } from 'react';
|
|
96
|
+
|
|
97
|
+
// Lazy load heavy components
|
|
98
|
+
const HeavyChart = lazy(() => import('./HeavyChart'));
|
|
99
|
+
|
|
100
|
+
function Dashboard() {
|
|
101
|
+
return (
|
|
102
|
+
<Suspense fallback={<Loading />}>
|
|
103
|
+
<HeavyChart />
|
|
104
|
+
</Suspense>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Route-based splitting
|
|
109
|
+
const routes = {
|
|
110
|
+
'/settings': lazy(() => import('./Settings')),
|
|
111
|
+
'/analytics': lazy(() => import('./Analytics')),
|
|
112
|
+
};
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Common Mistakes
|
|
116
|
+
|
|
117
|
+
| ❌ Mistake | ✅ Fix |
|
|
118
|
+
|------------|--------|
|
|
119
|
+
| Inline objects in render | useMemo, move outside |
|
|
120
|
+
| Inline functions in render | useCallback |
|
|
121
|
+
| useEffect for derived state | Compute in render |
|
|
122
|
+
| Large initial bundle | Code splitting |
|
|
123
|
+
| Unoptimized images | WebP, lazy load, srcset |
|
|
124
|
+
| No virtualization | react-window |
|
|
125
|
+
|
|
126
|
+
## Image Optimization
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
// Responsive images
|
|
130
|
+
<picture>
|
|
131
|
+
<source srcSet="/image.avif" type="image/avif" />
|
|
132
|
+
<source srcSet="/image.webp" type="image/webp" />
|
|
133
|
+
<img src="/image.jpg" loading="lazy" alt="Description" />
|
|
134
|
+
</picture>
|
|
135
|
+
|
|
136
|
+
// Next.js Image
|
|
137
|
+
import Image from 'next/image';
|
|
138
|
+
|
|
139
|
+
<Image
|
|
140
|
+
src="/hero.jpg"
|
|
141
|
+
alt="Hero"
|
|
142
|
+
width={1200}
|
|
143
|
+
height={600}
|
|
144
|
+
priority // For above fold
|
|
145
|
+
/>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Bundle Analysis
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
# Analyze bundle size
|
|
152
|
+
npx vite-bundle-visualizer
|
|
153
|
+
|
|
154
|
+
# Check what's in your bundle
|
|
155
|
+
npx source-map-explorer dist/assets/*.js
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Performance Checklist
|
|
159
|
+
|
|
160
|
+
### JavaScript
|
|
161
|
+
- [ ] Code splitting implemented
|
|
162
|
+
- [ ] Tree shaking working
|
|
163
|
+
- [ ] No unnecessary dependencies
|
|
164
|
+
- [ ] Lazy loading for routes
|
|
165
|
+
|
|
166
|
+
### React
|
|
167
|
+
- [ ] No useEffect for derived state
|
|
168
|
+
- [ ] Lists virtualized
|
|
169
|
+
- [ ] Expensive computes memoized
|
|
170
|
+
- [ ] Props stable (useCallback)
|
|
171
|
+
|
|
172
|
+
### Assets
|
|
173
|
+
- [ ] Images optimized (WebP/AVIF)
|
|
174
|
+
- [ ] Lazy loading for below fold
|
|
175
|
+
- [ ] Fonts preloaded
|
|
176
|
+
- [ ] Critical CSS inlined
|
|
177
|
+
|
|
178
|
+
### Metrics
|
|
179
|
+
- [ ] LCP < 2.5s
|
|
180
|
+
- [ ] FID < 100ms
|
|
181
|
+
- [ ] CLS < 0.1
|
|
182
|
+
|
|
183
|
+
## Profiling
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
# Lighthouse
|
|
187
|
+
npx lighthouse https://yoursite.com --view
|
|
188
|
+
|
|
189
|
+
# React DevTools Profiler
|
|
190
|
+
# 1. Open DevTools
|
|
191
|
+
# 2. Go to Profiler tab
|
|
192
|
+
# 3. Click record
|
|
193
|
+
# 4. Interact with app
|
|
194
|
+
# 5. Analyze flame graph
|
|
195
|
+
```
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: state-management
|
|
3
|
+
description: |
|
|
4
|
+
Frontend state management patterns with Zustand, TanStack Query, React Context.
|
|
5
|
+
Use when: managing application state, server state, URL state, or when user
|
|
6
|
+
mentions "state", "Zustand", "React Query", "TanStack Query", "context", "store".
|
|
7
|
+
Choose the right tool for each type of state.
|
|
8
|
+
version: 1.0.0
|
|
9
|
+
category: frontend
|
|
10
|
+
tags:
|
|
11
|
+
- state
|
|
12
|
+
- zustand
|
|
13
|
+
- tanstack-query
|
|
14
|
+
- react-query
|
|
15
|
+
depends_on:
|
|
16
|
+
- frontend-design
|
|
17
|
+
recommends: []
|
|
18
|
+
used_by: []
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
# Skill: State Management
|
|
22
|
+
|
|
23
|
+
## Core Principle
|
|
24
|
+
**Different state types need different solutions.** Don't use a hammer for everything.
|
|
25
|
+
|
|
26
|
+
## State Types
|
|
27
|
+
|
|
28
|
+
| Type | Tool | Examples |
|
|
29
|
+
|------|------|----------|
|
|
30
|
+
| **Server State** | TanStack Query | API data, cached responses |
|
|
31
|
+
| **Client State** | Zustand | UI state, preferences |
|
|
32
|
+
| **URL State** | URL params | Filters, pagination |
|
|
33
|
+
| **Form State** | React Hook Form | Form inputs, validation |
|
|
34
|
+
| **Local State** | useState | Component-only state |
|
|
35
|
+
|
|
36
|
+
## Server State (TanStack Query)
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
40
|
+
|
|
41
|
+
// Fetching data
|
|
42
|
+
function useUser(id: string) {
|
|
43
|
+
return useQuery({
|
|
44
|
+
queryKey: ['user', id],
|
|
45
|
+
queryFn: () => fetchUser(id),
|
|
46
|
+
staleTime: 5 * 60 * 1000, // 5 minutes
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Mutations
|
|
51
|
+
function useUpdateUser() {
|
|
52
|
+
const queryClient = useQueryClient();
|
|
53
|
+
|
|
54
|
+
return useMutation({
|
|
55
|
+
mutationFn: updateUser,
|
|
56
|
+
onSuccess: (data, variables) => {
|
|
57
|
+
// Invalidate and refetch
|
|
58
|
+
queryClient.invalidateQueries({ queryKey: ['user', variables.id] });
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Usage
|
|
64
|
+
function UserProfile({ userId }: { userId: string }) {
|
|
65
|
+
const { data, isLoading, error } = useUser(userId);
|
|
66
|
+
const update = useUpdateUser();
|
|
67
|
+
|
|
68
|
+
if (isLoading) return <Loading />;
|
|
69
|
+
if (error) return <Error />;
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div>
|
|
73
|
+
<h1>{data.name}</h1>
|
|
74
|
+
<button onClick={() => update.mutate({ id: userId, name: 'New' })}>
|
|
75
|
+
Update
|
|
76
|
+
</button>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Client State (Zustand)
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
import { create } from 'zustand';
|
|
86
|
+
import { persist, devtools } from 'zustand/middleware';
|
|
87
|
+
|
|
88
|
+
// Store definition
|
|
89
|
+
interface AppState {
|
|
90
|
+
theme: 'light' | 'dark';
|
|
91
|
+
sidebarOpen: boolean;
|
|
92
|
+
toggleTheme: () => void;
|
|
93
|
+
toggleSidebar: () => void;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const useStore = create<AppState>()(
|
|
97
|
+
devtools(
|
|
98
|
+
persist(
|
|
99
|
+
(set) => ({
|
|
100
|
+
theme: 'light',
|
|
101
|
+
sidebarOpen: false,
|
|
102
|
+
toggleTheme: () => set((s) => ({ theme: s.theme === 'light' ? 'dark' : 'light' })),
|
|
103
|
+
toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),
|
|
104
|
+
}),
|
|
105
|
+
{ name: 'app-store' }
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// Usage
|
|
111
|
+
function ThemeToggle() {
|
|
112
|
+
const { theme, toggleTheme } = useStore();
|
|
113
|
+
return <button onClick={toggleTheme}>{theme}</button>;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Selectors for performance
|
|
117
|
+
const theme = useStore((s) => s.theme); // Only re-renders when theme changes
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## URL State
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
import { useSearchParams } from 'react-router-dom';
|
|
124
|
+
|
|
125
|
+
function ProductList() {
|
|
126
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
127
|
+
|
|
128
|
+
// Read from URL
|
|
129
|
+
const page = Number(searchParams.get('page')) || 1;
|
|
130
|
+
const category = searchParams.get('category') || 'all';
|
|
131
|
+
|
|
132
|
+
// Update URL
|
|
133
|
+
const setPage = (p: number) => {
|
|
134
|
+
setSearchParams({ page: String(p), category });
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<div>
|
|
139
|
+
<Pagination current={page} onChange={setPage} />
|
|
140
|
+
</div>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Common Mistakes
|
|
146
|
+
|
|
147
|
+
| ❌ Mistake | ✅ Fix |
|
|
148
|
+
|------------|--------|
|
|
149
|
+
| Storing API data in Zustand | Use TanStack Query |
|
|
150
|
+
| useState for global state | Use Zustand |
|
|
151
|
+
| useEffect for derived state | Compute during render |
|
|
152
|
+
| Prop drilling | Context or Zustand |
|
|
153
|
+
| Not persisting preferences | Add persist middleware |
|
|
154
|
+
|
|
155
|
+
## Derived State
|
|
156
|
+
|
|
157
|
+
```tsx
|
|
158
|
+
// ❌ Bad: useEffect for derived state
|
|
159
|
+
const [total, setTotal] = useState(0);
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
setTotal(items.reduce((sum, item) => sum + item.price, 0));
|
|
162
|
+
}, [items]);
|
|
163
|
+
|
|
164
|
+
// ✅ Good: Compute during render
|
|
165
|
+
const total = items.reduce((sum, item) => sum + item.price, 0);
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## When to Use What
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
API data → TanStack Query
|
|
172
|
+
↓
|
|
173
|
+
Is it cached? Server state
|
|
174
|
+
↓
|
|
175
|
+
Is it shared? Zustand
|
|
176
|
+
↓
|
|
177
|
+
Is it form? React Hook Form
|
|
178
|
+
↓
|
|
179
|
+
Is it URL? Search params
|
|
180
|
+
↓
|
|
181
|
+
Local only? useState
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Quick Checklist
|
|
185
|
+
|
|
186
|
+
- [ ] Server state uses TanStack Query
|
|
187
|
+
- [ ] Client state uses Zustand
|
|
188
|
+
- [ ] No useEffect for derived state
|
|
189
|
+
- [ ] Form state uses React Hook Form
|
|
190
|
+
- [ ] URL state for shareable state
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: testing-fe
|
|
3
|
+
description: |
|
|
4
|
+
Frontend testing with Vitest, Testing Library, Playwright. Use when: writing
|
|
5
|
+
unit tests, integration tests, component tests, E2E tests, or when user
|
|
6
|
+
mentions "test", "vitest", "jest", "testing library", "playwright", "cypress".
|
|
7
|
+
Test behavior, not implementation.
|
|
8
|
+
version: 1.0.0
|
|
9
|
+
category: frontend
|
|
10
|
+
tags:
|
|
11
|
+
- testing
|
|
12
|
+
- vitest
|
|
13
|
+
- testing-library
|
|
14
|
+
- playwright
|
|
15
|
+
depends_on:
|
|
16
|
+
- code-review
|
|
17
|
+
recommends: []
|
|
18
|
+
used_by: []
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
# Skill: Testing FE
|
|
22
|
+
|
|
23
|
+
## Core Principle
|
|
24
|
+
**Test behavior, not implementation.** Users don't care about your code structure—they care if it works.
|
|
25
|
+
|
|
26
|
+
## Testing Trophy
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
▲
|
|
30
|
+
/E2E\ Few, slow, expensive
|
|
31
|
+
/─────\
|
|
32
|
+
/ API \ Some, medium speed
|
|
33
|
+
/─────────\
|
|
34
|
+
/ Integration \ Many, fast
|
|
35
|
+
/───────────────\
|
|
36
|
+
/ Unit Tests \ Many, very fast
|
|
37
|
+
/───────────────────\
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Hard Rules
|
|
41
|
+
|
|
42
|
+
1. **NEVER test implementation** - Test user behavior
|
|
43
|
+
2. **NEVER use container.querySelector** - Use getBy* queries
|
|
44
|
+
3. **ALWAYS use userEvent** - Not fireEvent
|
|
45
|
+
4. **ALWAYS wait for async** - findBy*, waitFor
|
|
46
|
+
5. **ALWAYS clean up** - No state leakage
|
|
47
|
+
|
|
48
|
+
## Component Testing (Vitest + Testing Library)
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
// Button.test.tsx
|
|
52
|
+
import { render, screen } from '@testing-library/react';
|
|
53
|
+
import userEvent from '@testing-library/user-event';
|
|
54
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
55
|
+
import { Button } from './Button';
|
|
56
|
+
|
|
57
|
+
describe('Button', () => {
|
|
58
|
+
it('renders with text', () => {
|
|
59
|
+
render(<Button>Click me</Button>);
|
|
60
|
+
expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('calls onClick when clicked', async () => {
|
|
64
|
+
const user = userEvent.setup();
|
|
65
|
+
const onClick = vi.fn();
|
|
66
|
+
|
|
67
|
+
render(<Button onClick={onClick}>Click me</Button>);
|
|
68
|
+
|
|
69
|
+
await user.click(screen.getByRole('button'));
|
|
70
|
+
|
|
71
|
+
expect(onClick).toHaveBeenCalledOnce();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('shows loading state', () => {
|
|
75
|
+
render(<Button loading>Submit</Button>);
|
|
76
|
+
|
|
77
|
+
expect(screen.getByRole('button')).toBeDisabled();
|
|
78
|
+
expect(screen.getByText(/loading/i)).toBeInTheDocument();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Async Testing
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
87
|
+
|
|
88
|
+
it('shows user after loading', async () => {
|
|
89
|
+
render(<UserProfile userId="1" />);
|
|
90
|
+
|
|
91
|
+
// Wait for element to appear
|
|
92
|
+
expect(await screen.findByText('John Doe')).toBeInTheDocument();
|
|
93
|
+
|
|
94
|
+
// Or use waitFor for complex conditions
|
|
95
|
+
await waitFor(() => {
|
|
96
|
+
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Mocking
|
|
102
|
+
|
|
103
|
+
```tsx
|
|
104
|
+
import { vi } from 'vitest';
|
|
105
|
+
|
|
106
|
+
// Mock module
|
|
107
|
+
vi.mock('../api', () => ({
|
|
108
|
+
fetchUser: vi.fn().mockResolvedValue({ id: '1', name: 'John' }),
|
|
109
|
+
}));
|
|
110
|
+
|
|
111
|
+
// Mock hooks
|
|
112
|
+
const mockNavigate = vi.fn();
|
|
113
|
+
vi.mock('react-router-dom', () => ({
|
|
114
|
+
useNavigate: () => mockNavigate,
|
|
115
|
+
}));
|
|
116
|
+
|
|
117
|
+
// Spy on console
|
|
118
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Common Mistakes
|
|
122
|
+
|
|
123
|
+
| ❌ Mistake | ✅ Fix |
|
|
124
|
+
|------------|--------|
|
|
125
|
+
| Testing props/state | Test rendered output |
|
|
126
|
+
| Using fireEvent | Use userEvent |
|
|
127
|
+
| Not waiting for async | findBy*, waitFor |
|
|
128
|
+
| Testing implementation | Test user behavior |
|
|
129
|
+
| Brittle selectors | Use accessible queries |
|
|
130
|
+
|
|
131
|
+
## Query Priority
|
|
132
|
+
|
|
133
|
+
```tsx
|
|
134
|
+
// 1. Accessible queries (best)
|
|
135
|
+
screen.getByRole('button', { name: /submit/i });
|
|
136
|
+
screen.getByLabelText(/email/i);
|
|
137
|
+
screen.getByPlaceholderText(/search/i);
|
|
138
|
+
screen.getByText(/welcome/i);
|
|
139
|
+
|
|
140
|
+
// 2. Semantic queries
|
|
141
|
+
screen.getByAltText(/profile/i);
|
|
142
|
+
screen.getByTitle(/close/i);
|
|
143
|
+
|
|
144
|
+
// 3. Test ID (last resort)
|
|
145
|
+
screen.getByTestId('submit-button');
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## E2E Testing (Playwright)
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
// example.spec.ts
|
|
152
|
+
import { test, expect } from '@playwright/test';
|
|
153
|
+
|
|
154
|
+
test('user can login', async ({ page }) => {
|
|
155
|
+
await page.goto('/login');
|
|
156
|
+
|
|
157
|
+
await page.fill('[name="email"]', 'test@example.com');
|
|
158
|
+
await page.fill('[name="password"]', 'password123');
|
|
159
|
+
await page.click('button[type="submit"]');
|
|
160
|
+
|
|
161
|
+
await expect(page).toHaveURL('/dashboard');
|
|
162
|
+
await expect(page.locator('h1')).toContainText('Welcome');
|
|
163
|
+
});
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Vitest Config
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
// vitest.config.ts
|
|
170
|
+
import { defineConfig } from 'vitest/config';
|
|
171
|
+
import react from '@vitejs/plugin-react';
|
|
172
|
+
|
|
173
|
+
export default defineConfig({
|
|
174
|
+
plugins: [react()],
|
|
175
|
+
test: {
|
|
176
|
+
environment: 'jsdom',
|
|
177
|
+
setupFiles: ['./src/test/setup.ts'],
|
|
178
|
+
coverage: {
|
|
179
|
+
reporter: ['text', 'html'],
|
|
180
|
+
exclude: ['node_modules/', 'src/test/'],
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Quick Checklist
|
|
187
|
+
|
|
188
|
+
- [ ] Tests user behavior, not implementation
|
|
189
|
+
- [ ] Uses accessible queries (getByRole)
|
|
190
|
+
- [ ] Uses userEvent, not fireEvent
|
|
191
|
+
- [ ] Waits for async with findBy/waitFor
|
|
192
|
+
- [ ] Mocks external dependencies
|
|
193
|
+
- [ ] Coverage reports generated
|