Initial commit
This commit is contained in:
@@ -0,0 +1,377 @@
|
||||
# Anthropic Skill Standards
|
||||
|
||||
Official standards for creating Claude Code skills based on Anthropic's skill-creator best practices.
|
||||
|
||||
## Core Principles
|
||||
|
||||
Skills should follow these fundamental principles:
|
||||
1. **Progressive Disclosure**: Metadata → SKILL.md → Resources (load only what's needed)
|
||||
2. **Imperative Voice**: Use verb-first instructions, not second-person
|
||||
3. **Third-Person Descriptions**: Describe what the skill does, not what "you" do
|
||||
4. **Resource Bundling**: Scripts, references, and assets organized in standard directories
|
||||
|
||||
## Skill Structure
|
||||
|
||||
### Required Files
|
||||
|
||||
```
|
||||
skill-name/
|
||||
├── SKILL.md (required)
|
||||
└── Optional bundled resources:
|
||||
├── scripts/ - Executable code
|
||||
├── references/ - Documentation loaded as needed
|
||||
└── assets/ - Templates and output files
|
||||
```
|
||||
|
||||
### SKILL.md Format
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: skill-name-in-hyphen-case
|
||||
description: Third-person description with trigger terms. Max 1024 chars.
|
||||
allowed-tools: Read, Grep, Glob # Optional, for read-only skills
|
||||
---
|
||||
|
||||
# Skill Title
|
||||
|
||||
Brief overview paragraph.
|
||||
|
||||
## Overview
|
||||
|
||||
To accomplish [goal], this skill [explains approach].
|
||||
|
||||
## When to Use
|
||||
|
||||
Use this skill when:
|
||||
- [Specific scenario 1]
|
||||
- [Specific scenario 2]
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Step 1: [Action]
|
||||
|
||||
To [accomplish substep], do:
|
||||
|
||||
1. [Specific instruction]
|
||||
2. [Specific instruction]
|
||||
|
||||
[Code example if applicable]
|
||||
|
||||
...
|
||||
|
||||
## Resources
|
||||
|
||||
- `scripts/[name].py` - Description
|
||||
- `references/[name].md` - Description
|
||||
- `assets/[name]` - Description
|
||||
```
|
||||
|
||||
## Frontmatter Standards
|
||||
|
||||
### Name Field
|
||||
|
||||
**Format**: `hyphen-case` (lowercase letters, digits, hyphens only)
|
||||
|
||||
**Valid:**
|
||||
- `nextjs-fullstack-scaffold`
|
||||
- `api-contracts-and-zod-validation`
|
||||
- `form-generator-rhf-zod`
|
||||
|
||||
**Invalid:**
|
||||
- `NextjsScaffold` (PascalCase)
|
||||
- `nextjs_scaffold` (snake_case)
|
||||
- `nextjsScaffold` (camelCase)
|
||||
- `-nextjs-scaffold` (starts with hyphen)
|
||||
- `nextjs--scaffold` (consecutive hyphens)
|
||||
|
||||
### Description Field
|
||||
|
||||
**Requirements:**
|
||||
- Use third-person voice
|
||||
- Include WHAT the skill does
|
||||
- Include WHEN to use it
|
||||
- Include specific trigger terms users would search
|
||||
- Maximum 1024 characters
|
||||
- No angle brackets (< or >)
|
||||
|
||||
**Format:**
|
||||
```yaml
|
||||
description: This skill should be used when [scenarios]. Apply when [cases]. Trigger terms include [keywords, phrases, terms].
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```yaml
|
||||
description: This skill should be used when generating React forms with React Hook Form, Zod validation, and shadcn/ui components. Applies when creating entity forms, character editors, location forms, or any form requiring client and server validation. Trigger terms include create form, generate form, React Hook Form, RHF, Zod validation, form component.
|
||||
```
|
||||
|
||||
### allowed-tools Field (Optional)
|
||||
|
||||
**When to use**: For read-only or analysis skills that don't modify code
|
||||
|
||||
**Format:**
|
||||
```yaml
|
||||
allowed-tools: Read, Grep, Glob, Bash
|
||||
```
|
||||
|
||||
**Common combinations:**
|
||||
- Analysis only: `Read, Grep, Glob, Bash`
|
||||
- File operations: `Read, Edit, Write, Bash`
|
||||
- Full access: Omit field entirely
|
||||
|
||||
## Instruction Voice Standards
|
||||
|
||||
### Imperative/Infinitive Form
|
||||
|
||||
**Always use verb-first instructions:**
|
||||
|
||||
[OK] **Correct Examples:**
|
||||
- "To create a form, use the generator script"
|
||||
- "Generate schemas using Zod"
|
||||
- "Install dependencies with npm"
|
||||
- "Configure the database connection"
|
||||
- "Use the template from assets/form.tsx"
|
||||
- "Consult references/api-patterns.md for details"
|
||||
|
||||
[ERROR] **Incorrect Examples:**
|
||||
- "You should create forms using the generator"
|
||||
- "You can generate schemas with Zod"
|
||||
- "You need to install dependencies"
|
||||
- "You have to configure the database"
|
||||
- "You will use the template"
|
||||
- "You should consult the reference"
|
||||
|
||||
### Description Voice
|
||||
|
||||
**Use third-person for descriptions:**
|
||||
|
||||
[OK] **Correct:**
|
||||
- "This skill should be used when..."
|
||||
- "Apply this skill when creating..."
|
||||
- "Use for generating..."
|
||||
|
||||
[ERROR] **Incorrect:**
|
||||
- "Use this skill when you need to..."
|
||||
- "You should use this when..."
|
||||
- "This helps you create..."
|
||||
|
||||
## Resource Organization
|
||||
|
||||
### scripts/ Directory
|
||||
|
||||
**Purpose**: Executable code for deterministic operations
|
||||
|
||||
**When to include:**
|
||||
- Code is repeatedly rewritten in skill usage
|
||||
- Deterministic operations needed
|
||||
- Complex logic that benefits from separate execution
|
||||
|
||||
**Format:**
|
||||
- Python: `script_name.py` (executable, #!/usr/bin/env python3)
|
||||
- Bash: `script_name.sh` (executable, #!/bin/bash)
|
||||
|
||||
**Documentation in SKILL.md:**
|
||||
```markdown
|
||||
### scripts/generate_form.py
|
||||
|
||||
Generates form component from field specification.
|
||||
|
||||
Usage:
|
||||
\`\`\`bash
|
||||
python scripts/generate_form.py --fields fields.json --output components/forms
|
||||
\`\`\`
|
||||
```
|
||||
|
||||
### references/ Directory
|
||||
|
||||
**Purpose**: Documentation loaded into context as needed
|
||||
|
||||
**When to include:**
|
||||
- Detailed information too long for SKILL.md (>5k words)
|
||||
- API reference documentation
|
||||
- Pattern catalogs
|
||||
- Specification documents
|
||||
|
||||
**Format**: Markdown files with descriptive names
|
||||
|
||||
**For large files (>10k words):**
|
||||
Include grep patterns in SKILL.md:
|
||||
```markdown
|
||||
Consult `references/api-reference.md` for API details.
|
||||
|
||||
To find specific endpoints:
|
||||
\`\`\`bash
|
||||
Grep: pattern="GET /api/entities" path="references/api-reference.md"
|
||||
\`\`\`
|
||||
```
|
||||
|
||||
### assets/ Directory
|
||||
|
||||
**Purpose**: Files used in output (not loaded into context)
|
||||
|
||||
**When to include:**
|
||||
- Templates
|
||||
- Boilerplate code
|
||||
- Starter files
|
||||
- Configuration examples
|
||||
|
||||
**Format**: Any file type appropriate for the skill
|
||||
|
||||
**Reference in SKILL.md:**
|
||||
```markdown
|
||||
Use template from `assets/form-template.tsx` as starting point.
|
||||
```
|
||||
|
||||
## Trigger Terms
|
||||
|
||||
Include specific terms users would search for:
|
||||
|
||||
### Technical Terms
|
||||
- Framework names: Next.js, React, Vitest, Playwright
|
||||
- Library names: Zod, React Hook Form, Prisma
|
||||
- Tool names: ESLint, Prettier, Husky
|
||||
- Pattern names: Server Actions, RLS, CSP
|
||||
|
||||
### Action Terms
|
||||
- create, generate, build, scaffold, initialize
|
||||
- setup, configure, install, integrate
|
||||
- test, validate, check, audit, review
|
||||
- optimize, improve, refactor, enhance
|
||||
|
||||
### Domain Terms (for worldbuilding)
|
||||
- entity, character, location, item, faction
|
||||
- relationship, timeline, event, lore
|
||||
- world, map, narrative, description
|
||||
|
||||
### Problem Terms
|
||||
- error, issue, bug, vulnerability
|
||||
- performance, slow, bottleneck
|
||||
- inconsistent, duplicate, broken
|
||||
|
||||
## Skill Categories
|
||||
|
||||
### development
|
||||
Code generation, refactoring, debugging, setup, configuration, frameworks, libraries, tooling
|
||||
|
||||
### data-modeling
|
||||
Schemas, types, validation, database design, entity relationships, migrations
|
||||
|
||||
### ui-components
|
||||
React components, styling, responsive design, accessibility, user interface
|
||||
|
||||
### documentation
|
||||
API docs, guides, comments, changelogs, ADRs, PRDs
|
||||
|
||||
### testing
|
||||
Unit tests, integration tests, E2E tests, accessibility tests, coverage, mocks
|
||||
|
||||
### utilities
|
||||
Helpers, formatters, converters, validators, analyzers, auditors
|
||||
|
||||
## Quality Checklist
|
||||
|
||||
Before finalizing a skill, verify:
|
||||
|
||||
- [ ] Name is hyphen-case (lowercase, hyphens only)
|
||||
- [ ] Description is third-person voice
|
||||
- [ ] Description includes trigger terms
|
||||
- [ ] Description explains WHAT and WHEN
|
||||
- [ ] Description is under 1024 characters
|
||||
- [ ] Instructions use imperative form (no "you")
|
||||
- [ ] Section structure is logical
|
||||
- [ ] Resources are properly referenced
|
||||
- [ ] Scripts have usage examples
|
||||
- [ ] Large references include grep patterns
|
||||
- [ ] Assets are described in context
|
||||
- [ ] Examples are clear and complete
|
||||
- [ ] Code follows current best practices
|
||||
- [ ] Validation passes: `python scripts/quick_validate.py`
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
### Mistake 1: Second-Person Voice
|
||||
|
||||
[ERROR] "You should install the dependencies"
|
||||
[OK] "Install the dependencies"
|
||||
|
||||
[ERROR] "You can use the template"
|
||||
[OK] "Use the template" or "To use the template"
|
||||
|
||||
### Mistake 2: Wrong Name Format
|
||||
|
||||
[ERROR] `FormGenerator` (PascalCase)
|
||||
[OK] `form-generator`
|
||||
|
||||
[ERROR] `form_generator` (snake_case)
|
||||
[OK] `form-generator`
|
||||
|
||||
### Mistake 3: Vague Description
|
||||
|
||||
[ERROR] "This skill helps with forms and validation stuff."
|
||||
[OK] "This skill should be used when generating React forms with React Hook Form and Zod validation. Use for creating entity forms, data entry interfaces, or any form requiring validation."
|
||||
|
||||
### Mistake 4: Missing Trigger Terms
|
||||
|
||||
[ERROR] "This skill generates forms." (no searchable terms)
|
||||
[OK] "This skill generates React Hook Form components with Zod validation. Trigger terms include create form, generate form, RHF, React Hook Form, Zod validation, form component."
|
||||
|
||||
### Mistake 5: Incomplete Resource References
|
||||
|
||||
[ERROR] "Use the script to generate forms." (which script?)
|
||||
[OK] "Use `scripts/generate_form.py` to generate form components."
|
||||
|
||||
## Version Guidelines
|
||||
|
||||
Keep skills current with latest versions:
|
||||
|
||||
### Framework Versions
|
||||
- Next.js: 15/16 (App Router, Server Components)
|
||||
- React: 19 (Server Components, Actions)
|
||||
- TypeScript: 5.x
|
||||
- Node.js: 20+
|
||||
|
||||
### Testing Tools
|
||||
- Vitest (not Jest)
|
||||
- React Testing Library
|
||||
- Playwright
|
||||
- axe-core for a11y
|
||||
|
||||
### Build Tools
|
||||
- Vite (for fast builds)
|
||||
- ESM (not CommonJS)
|
||||
- Modern bundlers
|
||||
|
||||
### Deprecated Patterns to Avoid
|
||||
- Next.js Pages Router (use App Router)
|
||||
- Jest (use Vitest)
|
||||
- getServerSideProps (use Server Components)
|
||||
- API routes for mutations (use Server Actions)
|
||||
- Class components (use function components)
|
||||
|
||||
## Maintenance
|
||||
|
||||
### Regular Updates
|
||||
- Review skills quarterly for outdated patterns
|
||||
- Update framework versions
|
||||
- Replace deprecated APIs
|
||||
- Add new best practices
|
||||
- Improve trigger terms based on usage
|
||||
|
||||
### Validation
|
||||
Run validation regularly:
|
||||
```bash
|
||||
python scripts/quick_validate.py skills/[category]/[skill-name]
|
||||
```
|
||||
|
||||
### Quality Audits
|
||||
Use skill-reviewer-and-enhancer to audit:
|
||||
```bash
|
||||
# Trigger the skill
|
||||
"Review and enhance the [skill-name] skill"
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- Official Anthropic skill-creator skill
|
||||
- Claude Code documentation
|
||||
- Community best practices
|
||||
- Framework official documentation
|
||||
@@ -0,0 +1,589 @@
|
||||
# Next.js Best Practices (2025)
|
||||
|
||||
Current best practices for Next.js 15/16 development with App Router, Server Components, and modern patterns.
|
||||
|
||||
## Framework Versions
|
||||
|
||||
### Current Versions
|
||||
- **Next.js**: 15.x or 16.x (latest stable)
|
||||
- **React**: 19.x (Server Components, Actions)
|
||||
- **TypeScript**: 5.x
|
||||
- **Node.js**: 20 LTS or higher
|
||||
|
||||
### Deprecated Versions to Flag
|
||||
- Next.js 12.x or older (Pages Router era)
|
||||
- React 17.x or older
|
||||
- TypeScript 4.x or older
|
||||
|
||||
## App Router vs Pages Router
|
||||
|
||||
### [OK] Modern (App Router)
|
||||
|
||||
```
|
||||
app/
|
||||
├── layout.tsx # Root layout
|
||||
├── page.tsx # Homepage
|
||||
├── entities/
|
||||
│ ├── layout.tsx # Nested layout
|
||||
│ ├── page.tsx # Entity list
|
||||
│ └── [id]/
|
||||
│ └── page.tsx # Entity detail
|
||||
└── api/ # API route handlers (minimal)
|
||||
└── webhook/
|
||||
└── route.ts # External webhooks only
|
||||
```
|
||||
|
||||
### [ERROR] Deprecated (Pages Router)
|
||||
|
||||
```
|
||||
pages/
|
||||
├── _app.tsx # Old pattern
|
||||
├── _document.tsx # Old pattern
|
||||
├── index.tsx # Old pattern
|
||||
├── entities/
|
||||
│ ├── index.tsx
|
||||
│ └── [id].tsx
|
||||
└── api/ # Old API routes
|
||||
└── entities.ts
|
||||
```
|
||||
|
||||
**Migration Check**: Flag any skills using `pages/` directory or `getServerSideProps`
|
||||
|
||||
## Server Components vs Client Components
|
||||
|
||||
### Default: Server Components
|
||||
|
||||
```tsx
|
||||
// app/entities/page.tsx
|
||||
// No 'use client' directive = Server Component (default)
|
||||
|
||||
import { db } from '@/lib/db'
|
||||
|
||||
export default async function EntitiesPage() {
|
||||
// Direct database access - only in Server Components
|
||||
const entities = await db.entity.findMany()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Entities</h1>
|
||||
{entities.map(entity => (
|
||||
<EntityCard key={entity.id} entity={entity} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Client Components (When Needed)
|
||||
|
||||
```tsx
|
||||
// components/EntityForm.tsx
|
||||
'use client' // Required for hooks, events, browser APIs
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
|
||||
export function EntityForm() {
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const form = useForm()
|
||||
|
||||
return <form>...</form>
|
||||
}
|
||||
```
|
||||
|
||||
**When to use 'use client':**
|
||||
- useState, useEffect, other hooks
|
||||
- Event handlers (onClick, onChange)
|
||||
- Browser APIs (localStorage, window)
|
||||
- Third-party libraries requiring browser
|
||||
|
||||
**Best Practice**: Keep 'use client' boundary as low as possible in component tree
|
||||
|
||||
## Data Fetching
|
||||
|
||||
### [OK] Modern: Server Components + fetch
|
||||
|
||||
```tsx
|
||||
// app/characters/page.tsx
|
||||
export default async function CharactersPage() {
|
||||
// Fetch in Server Component
|
||||
const characters = await fetch('https://api.example.com/characters', {
|
||||
next: { revalidate: 3600 } // Cache for 1 hour
|
||||
}).then(res => res.json())
|
||||
|
||||
return <CharacterList characters={characters} />
|
||||
}
|
||||
```
|
||||
|
||||
### [OK] Modern: Server Components + Database
|
||||
|
||||
```tsx
|
||||
// app/locations/page.tsx
|
||||
import { db } from '@/lib/db'
|
||||
|
||||
export default async function LocationsPage() {
|
||||
// Direct database access
|
||||
const locations = await db.location.findMany({
|
||||
include: { region: true }
|
||||
})
|
||||
|
||||
return <LocationList locations={locations} />
|
||||
}
|
||||
```
|
||||
|
||||
### [ERROR] Deprecated: getServerSideProps
|
||||
|
||||
```tsx
|
||||
// pages/characters.tsx - OLD PATTERN
|
||||
export async function getServerSideProps() {
|
||||
const characters = await fetchCharacters()
|
||||
return { props: { characters } }
|
||||
}
|
||||
```
|
||||
|
||||
**Migration Check**: Replace `getServerSideProps`, `getStaticProps`, `getInitialProps` with Server Component async data fetching
|
||||
|
||||
## Server Actions
|
||||
|
||||
### [OK] Modern: Server Actions for Mutations
|
||||
|
||||
```tsx
|
||||
// app/actions/character.ts
|
||||
'use server'
|
||||
|
||||
import { db } from '@/lib/db'
|
||||
import { revalidatePath } from 'next/cache'
|
||||
|
||||
export async function createCharacter(formData: FormData) {
|
||||
const name = formData.get('name') as string
|
||||
|
||||
const character = await db.character.create({
|
||||
data: { name }
|
||||
})
|
||||
|
||||
revalidatePath('/characters')
|
||||
return { success: true, character }
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
// app/characters/CreateForm.tsx
|
||||
'use client'
|
||||
|
||||
import { createCharacter } from '@/app/actions/character'
|
||||
|
||||
export function CreateForm() {
|
||||
return (
|
||||
<form action={createCharacter}>
|
||||
<input name="name" />
|
||||
<button type="submit">Create</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### [ERROR] Deprecated: API Routes for Simple Mutations
|
||||
|
||||
```tsx
|
||||
// pages/api/characters.ts - OLD PATTERN
|
||||
export default async function handler(req, res) {
|
||||
if (req.method === 'POST') {
|
||||
const character = await createCharacter(req.body)
|
||||
res.json({ character })
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**When to still use API Routes:**
|
||||
- Webhooks from external services
|
||||
- OAuth callbacks
|
||||
- Third-party integrations requiring public endpoints
|
||||
|
||||
**Migration Check**: Replace API routes used for form submissions with Server Actions
|
||||
|
||||
## Metadata and SEO
|
||||
|
||||
### [OK] Modern: generateMetadata
|
||||
|
||||
```tsx
|
||||
// app/characters/[id]/page.tsx
|
||||
import { Metadata } from 'next'
|
||||
|
||||
export async function generateMetadata({ params }): Promise<Metadata> {
|
||||
const character = await db.character.findUnique({
|
||||
where: { id: params.id }
|
||||
})
|
||||
|
||||
return {
|
||||
title: character.name,
|
||||
description: character.bio,
|
||||
openGraph: {
|
||||
title: character.name,
|
||||
description: character.bio,
|
||||
images: [character.image],
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### [ERROR] Deprecated: next/head
|
||||
|
||||
```tsx
|
||||
// OLD PATTERN
|
||||
import Head from 'next/head'
|
||||
|
||||
<Head>
|
||||
<title>{character.name}</title>
|
||||
</Head>
|
||||
```
|
||||
|
||||
## Routing and Navigation
|
||||
|
||||
### [OK] Modern: Link from next/link
|
||||
|
||||
```tsx
|
||||
import Link from 'next/link'
|
||||
|
||||
<Link href="/characters/123">View Character</Link>
|
||||
```
|
||||
|
||||
### [OK] Modern: useRouter from next/navigation
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import { useRouter } from 'next/navigation' // Not 'next/router'!
|
||||
|
||||
export function BackButton() {
|
||||
const router = useRouter()
|
||||
return <button onClick={() => router.back()}>Back</button>
|
||||
}
|
||||
```
|
||||
|
||||
### [ERROR] Deprecated: useRouter from next/router
|
||||
|
||||
```tsx
|
||||
// OLD PATTERN
|
||||
import { useRouter } from 'next/router' // Pages Router only
|
||||
```
|
||||
|
||||
## Loading States
|
||||
|
||||
### [OK] Modern: loading.tsx
|
||||
|
||||
```tsx
|
||||
// app/characters/loading.tsx
|
||||
export default function Loading() {
|
||||
return <div>Loading characters...</div>
|
||||
}
|
||||
```
|
||||
|
||||
### [OK] Modern: Suspense Boundaries
|
||||
|
||||
```tsx
|
||||
// app/characters/page.tsx
|
||||
import { Suspense } from 'react'
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<Suspense fallback={<LoadingSkeleton />}>
|
||||
<CharacterList />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### [OK] Modern: error.tsx
|
||||
|
||||
```tsx
|
||||
// app/characters/error.tsx
|
||||
'use client'
|
||||
|
||||
export default function Error({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string }
|
||||
reset: () => void
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<h2>Something went wrong!</h2>
|
||||
<button onClick={() => reset()}>Try again</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Caching and Revalidation
|
||||
|
||||
### [OK] Modern: Revalidation Strategies
|
||||
|
||||
```tsx
|
||||
// Time-based revalidation
|
||||
fetch(url, { next: { revalidate: 3600 } }) // 1 hour
|
||||
|
||||
// On-demand revalidation
|
||||
import { revalidatePath } from 'next/cache'
|
||||
revalidatePath('/characters')
|
||||
|
||||
// Tag-based revalidation
|
||||
fetch(url, { next: { tags: ['characters'] } })
|
||||
import { revalidateTag } from 'next/cache'
|
||||
revalidateTag('characters')
|
||||
|
||||
// Disable caching
|
||||
fetch(url, { cache: 'no-store' })
|
||||
```
|
||||
|
||||
## Middleware
|
||||
|
||||
### [OK] Modern: middleware.ts
|
||||
|
||||
```tsx
|
||||
// middleware.ts
|
||||
import { NextResponse } from 'next/server'
|
||||
import type { NextRequest } from 'next/server'
|
||||
|
||||
export function middleware(request: NextRequest) {
|
||||
// Auth check
|
||||
const token = request.cookies.get('token')
|
||||
|
||||
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
|
||||
return NextResponse.redirect(new URL('/login', request.url))
|
||||
}
|
||||
|
||||
return NextResponse.next()
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: '/dashboard/:path*',
|
||||
}
|
||||
```
|
||||
|
||||
## TypeScript Patterns
|
||||
|
||||
### [OK] Type-Safe Server Components
|
||||
|
||||
```tsx
|
||||
interface Character {
|
||||
id: string
|
||||
name: string
|
||||
bio: string
|
||||
}
|
||||
|
||||
export default async function CharacterPage({
|
||||
params,
|
||||
}: {
|
||||
params: { id: string }
|
||||
}) {
|
||||
const character: Character = await db.character.findUnique({
|
||||
where: { id: params.id }
|
||||
})
|
||||
|
||||
return <div>{character.name}</div>
|
||||
}
|
||||
```
|
||||
|
||||
### [OK] Type-Safe Server Actions
|
||||
|
||||
```tsx
|
||||
'use server'
|
||||
|
||||
import { z } from 'zod'
|
||||
|
||||
const createCharacterSchema = z.object({
|
||||
name: z.string().min(2),
|
||||
bio: z.string().max(5000),
|
||||
})
|
||||
|
||||
export async function createCharacter(
|
||||
data: z.infer<typeof createCharacterSchema>
|
||||
) {
|
||||
const validated = createCharacterSchema.parse(data)
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
### [OK] Modern: Type-Safe Env
|
||||
|
||||
```typescript
|
||||
// env.mjs
|
||||
import { createEnv } from "@t3-oss/env-nextjs"
|
||||
import { z } from "zod"
|
||||
|
||||
export const env = createEnv({
|
||||
server: {
|
||||
DATABASE_URL: z.string().url(),
|
||||
NEXTAUTH_SECRET: z.string().min(1),
|
||||
},
|
||||
client: {
|
||||
NEXT_PUBLIC_API_URL: z.string().url(),
|
||||
},
|
||||
runtimeEnv: {
|
||||
DATABASE_URL: process.env.DATABASE_URL,
|
||||
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
|
||||
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### [OK] Modern: next.config.js
|
||||
|
||||
```javascript
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
experimental: {
|
||||
serverActions: {
|
||||
bodySizeLimit: '2mb',
|
||||
},
|
||||
},
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'example.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export default nextConfig
|
||||
```
|
||||
|
||||
## Common Anti-Patterns to Flag
|
||||
|
||||
### [ERROR] Fetching in Client Components
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
// BAD: Fetch in client component
|
||||
export function Characters() {
|
||||
const [data, setData] = useState([])
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/characters').then(r => r.json()).then(setData)
|
||||
}, [])
|
||||
|
||||
return <div>{data.map(...)}</div>
|
||||
}
|
||||
```
|
||||
|
||||
[OK] **Fix**: Move data fetching to Server Component or use React Query for client-side data management
|
||||
|
||||
### [ERROR] Unnecessary 'use client'
|
||||
|
||||
```tsx
|
||||
'use client' // Not needed!
|
||||
|
||||
export function StaticCard({ title, description }) {
|
||||
return (
|
||||
<div>
|
||||
<h3>{title}</h3>
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
[OK] **Fix**: Remove 'use client' if component doesn't use hooks, events, or browser APIs
|
||||
|
||||
### [ERROR] Mixing Data Fetching Methods
|
||||
|
||||
```tsx
|
||||
// BAD: Using both old and new patterns
|
||||
export async function getServerSideProps() { ... }
|
||||
|
||||
export default async function Page() {
|
||||
const data = await fetch(...)
|
||||
}
|
||||
```
|
||||
|
||||
[OK] **Fix**: Use only Server Component data fetching in App Router
|
||||
|
||||
## Performance Best Practices
|
||||
|
||||
### Image Optimization
|
||||
|
||||
```tsx
|
||||
import Image from 'next/image'
|
||||
|
||||
<Image
|
||||
src="/character.jpg"
|
||||
alt="Character portrait"
|
||||
width={800}
|
||||
height={600}
|
||||
priority // For LCP images
|
||||
/>
|
||||
```
|
||||
|
||||
### Font Optimization
|
||||
|
||||
```tsx
|
||||
import { Inter } from 'next/font/google'
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] })
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
return (
|
||||
<html lang="en" className={inter.className}>
|
||||
{children}
|
||||
</html>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Code Splitting
|
||||
|
||||
```tsx
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
const HeavyComponent = dynamic(() => import('@/components/HeavyComponent'), {
|
||||
loading: () => <p>Loading...</p>,
|
||||
ssr: false, // Client-only if needed
|
||||
})
|
||||
```
|
||||
|
||||
## Validation Checklist
|
||||
|
||||
When reviewing Next.js skills, check for:
|
||||
|
||||
- [ ] Uses App Router (app/ directory), not Pages Router
|
||||
- [ ] Server Components for data fetching
|
||||
- [ ] Server Actions for mutations
|
||||
- [ ] Proper 'use client' boundaries
|
||||
- [ ] generateMetadata for SEO
|
||||
- [ ] useRouter from 'next/navigation'
|
||||
- [ ] Modern caching with fetch options
|
||||
- [ ] loading.tsx and error.tsx files
|
||||
- [ ] Type-safe environment variables
|
||||
- [ ] Image and font optimization
|
||||
- [ ] No deprecated APIs (getServerSideProps, etc.)
|
||||
- [ ] Proper middleware usage
|
||||
- [ ] Correct import paths
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Must Use (Modern)
|
||||
- [OK] `app/` directory
|
||||
- [OK] Server Components (default)
|
||||
- [OK] Server Actions ('use server')
|
||||
- [OK] `generateMetadata`
|
||||
- [OK] `useRouter` from 'next/navigation'
|
||||
- [OK] `revalidatePath`/`revalidateTag`
|
||||
|
||||
### Must Avoid (Deprecated)
|
||||
- [ERROR] `pages/` directory
|
||||
- [ERROR] `getServerSideProps`
|
||||
- [ERROR] `getStaticProps`
|
||||
- [ERROR] `getInitialProps`
|
||||
- [ERROR] API routes for mutations
|
||||
- [ERROR] `useRouter` from 'next/router'
|
||||
- [ERROR] `next/head`
|
||||
@@ -0,0 +1,674 @@
|
||||
# Testing Best Practices (2025)
|
||||
|
||||
Modern testing patterns using Vitest, React Testing Library, Playwright, and accessibility testing.
|
||||
|
||||
## Testing Stack
|
||||
|
||||
### Current Tools (2025)
|
||||
- **Unit/Integration**: Vitest (not Jest)
|
||||
- **Component Testing**: React Testing Library
|
||||
- **E2E Testing**: Playwright
|
||||
- **Accessibility**: axe-core + @axe-core/playwright
|
||||
- **Coverage**: Vitest with v8 provider
|
||||
|
||||
### Deprecated Tools to Flag
|
||||
- [ERROR] Jest (replaced by Vitest)
|
||||
- [ERROR] Enzyme (replaced by React Testing Library)
|
||||
- [ERROR] Karma, Jasmine (outdated)
|
||||
|
||||
## Vitest Configuration
|
||||
|
||||
### [OK] Modern: vitest.config.ts
|
||||
|
||||
```typescript
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import path from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
setupFiles: './test/setup.ts',
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
exclude: [
|
||||
'node_modules/',
|
||||
'test/',
|
||||
'**/*.config.{ts,js}',
|
||||
'**/*.d.ts',
|
||||
],
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### [ERROR] Deprecated: jest.config.js
|
||||
|
||||
```javascript
|
||||
// OLD PATTERN - Don't use Jest
|
||||
module.exports = {
|
||||
testEnvironment: 'jsdom',
|
||||
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
|
||||
}
|
||||
```
|
||||
|
||||
## Unit Testing Patterns
|
||||
|
||||
### [OK] Modern: Vitest Imports
|
||||
|
||||
```typescript
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||
|
||||
describe('validateEntity', () => {
|
||||
it('validates entity with required fields', () => {
|
||||
const result = validateEntity({
|
||||
name: 'Character',
|
||||
type: 'character'
|
||||
})
|
||||
|
||||
expect(result.valid).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects entity with missing name', () => {
|
||||
const result = validateEntity({
|
||||
type: 'character'
|
||||
})
|
||||
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors).toContain('Name is required')
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Test Organization
|
||||
|
||||
```typescript
|
||||
// Good structure: Arrange, Act, Assert (AAA)
|
||||
describe('CharacterService', () => {
|
||||
describe('createCharacter', () => {
|
||||
it('creates character with valid data', async () => {
|
||||
// Arrange
|
||||
const characterData = {
|
||||
name: 'Aria',
|
||||
class: 'Rogue',
|
||||
}
|
||||
|
||||
// Act
|
||||
const result = await createCharacter(characterData)
|
||||
|
||||
// Assert
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.character).toMatchObject(characterData)
|
||||
})
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Component Testing with RTL
|
||||
|
||||
### [OK] Modern: React Testing Library
|
||||
|
||||
```typescript
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { CharacterForm } from './CharacterForm'
|
||||
|
||||
describe('CharacterForm', () => {
|
||||
it('renders form fields', () => {
|
||||
render(<CharacterForm />)
|
||||
|
||||
expect(screen.getByLabelText(/name/i)).toBeInTheDocument()
|
||||
expect(screen.getByLabelText(/class/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('submits form with valid data', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onSubmit = vi.fn()
|
||||
|
||||
render(<CharacterForm onSubmit={onSubmit} />)
|
||||
|
||||
await user.type(screen.getByLabelText(/name/i), 'Aria')
|
||||
await user.selectOptions(screen.getByLabelText(/class/i), 'rogue')
|
||||
await user.click(screen.getByRole('button', { name: /submit/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onSubmit).toHaveBeenCalledWith({
|
||||
name: 'Aria',
|
||||
class: 'rogue',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('shows validation errors', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<CharacterForm />)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /submit/i }))
|
||||
|
||||
expect(await screen.findByText(/name is required/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### [ERROR] Deprecated: Enzyme
|
||||
|
||||
```typescript
|
||||
// OLD PATTERN - Don't use Enzyme
|
||||
import { shallow } from 'enzyme'
|
||||
|
||||
const wrapper = shallow(<CharacterForm />)
|
||||
wrapper.find('input').simulate('change')
|
||||
```
|
||||
|
||||
## Custom Render Function
|
||||
|
||||
### [OK] Create Test Utils
|
||||
|
||||
```typescript
|
||||
// test/utils/render.tsx
|
||||
import { render, RenderOptions } from '@testing-library/react'
|
||||
import { ReactElement } from 'react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
})
|
||||
|
||||
function AllProviders({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export function renderWithProviders(
|
||||
ui: ReactElement,
|
||||
options?: Omit<RenderOptions, 'wrapper'>
|
||||
) {
|
||||
return render(ui, { wrapper: AllProviders, ...options })
|
||||
}
|
||||
|
||||
export * from '@testing-library/react'
|
||||
export { renderWithProviders as render }
|
||||
```
|
||||
|
||||
## Mocking Patterns
|
||||
|
||||
### [OK] Vitest Mocks
|
||||
|
||||
```typescript
|
||||
import { vi } from 'vitest'
|
||||
|
||||
// Mock module
|
||||
vi.mock('@/lib/db', () => ({
|
||||
db: {
|
||||
character: {
|
||||
findMany: vi.fn(),
|
||||
create: vi.fn(),
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock function
|
||||
const mockFetch = vi.fn()
|
||||
global.fetch = mockFetch
|
||||
|
||||
// Mock implementation
|
||||
mockFetch.mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({ name: 'Test' }),
|
||||
})
|
||||
|
||||
// Spy on method
|
||||
const spy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
|
||||
// Cleanup
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
```
|
||||
|
||||
### [ERROR] Deprecated: Jest Mocks
|
||||
|
||||
```typescript
|
||||
// OLD PATTERN
|
||||
jest.mock('./module')
|
||||
jest.fn()
|
||||
jest.spyOn()
|
||||
```
|
||||
|
||||
## Playwright E2E Testing
|
||||
|
||||
### [OK] Modern: Playwright Config
|
||||
|
||||
```typescript
|
||||
// playwright.config.ts
|
||||
import { defineConfig, devices } from '@playwright/test'
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './test/e2e',
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
reporter: 'html',
|
||||
|
||||
use: {
|
||||
baseURL: 'http://localhost:3000',
|
||||
trace: 'on-first-retry',
|
||||
screenshot: 'only-on-failure',
|
||||
},
|
||||
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
{
|
||||
name: 'firefox',
|
||||
use: { ...devices['Desktop Firefox'] },
|
||||
},
|
||||
{
|
||||
name: 'webkit',
|
||||
use: { ...devices['Desktop Safari'] },
|
||||
},
|
||||
],
|
||||
|
||||
webServer: {
|
||||
command: 'npm run dev',
|
||||
url: 'http://localhost:3000',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### [OK] E2E Test Patterns
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test.describe('Character Creation', () => {
|
||||
test('creates new character', async ({ page }) => {
|
||||
await page.goto('/characters')
|
||||
|
||||
// Navigate to create form
|
||||
await page.getByRole('button', { name: /create character/i }).click()
|
||||
|
||||
// Fill form
|
||||
await page.getByLabel(/name/i).fill('Aria Shadowblade')
|
||||
await page.getByLabel(/class/i).selectOption('rogue')
|
||||
await page.getByLabel(/level/i).fill('5')
|
||||
|
||||
// Submit
|
||||
await page.getByRole('button', { name: /save/i }).click()
|
||||
|
||||
// Verify success
|
||||
await expect(page.getByText('Aria Shadowblade')).toBeVisible()
|
||||
await expect(page).toHaveURL(/\/characters\/\d+/)
|
||||
})
|
||||
|
||||
test('shows validation errors', async ({ page }) => {
|
||||
await page.goto('/characters/create')
|
||||
|
||||
await page.getByRole('button', { name: /save/i }).click()
|
||||
|
||||
await expect(page.getByText(/name is required/i)).toBeVisible()
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Accessibility Testing
|
||||
|
||||
### [OK] Component-Level A11y
|
||||
|
||||
```typescript
|
||||
import { render } from '@testing-library/react'
|
||||
import { axe, toHaveNoViolations } from 'jest-axe'
|
||||
import { CharacterCard } from './CharacterCard'
|
||||
|
||||
expect.extend(toHaveNoViolations)
|
||||
|
||||
describe('CharacterCard Accessibility', () => {
|
||||
it('has no accessibility violations', async () => {
|
||||
const { container } = render(
|
||||
<CharacterCard
|
||||
character={{
|
||||
name: 'Test',
|
||||
class: 'Warrior',
|
||||
level: 5,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
const results = await axe(container)
|
||||
expect(results).toHaveNoViolations()
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### [OK] E2E A11y with Playwright
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '@playwright/test'
|
||||
import AxeBuilder from '@axe-core/playwright'
|
||||
|
||||
test.describe('Accessibility', () => {
|
||||
test('homepage meets WCAG 2.1 AA', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
|
||||
const accessibilityScanResults = await new AxeBuilder({ page })
|
||||
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
|
||||
.analyze()
|
||||
|
||||
expect(accessibilityScanResults.violations).toEqual([])
|
||||
})
|
||||
|
||||
test('character form is accessible', async ({ page }) => {
|
||||
await page.goto('/characters/create')
|
||||
|
||||
const results = await new AxeBuilder({ page })
|
||||
.exclude('#third-party-widget') // Exclude external widgets
|
||||
.analyze()
|
||||
|
||||
expect(results.violations).toEqual([])
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Test Coverage
|
||||
|
||||
### [OK] Coverage Configuration
|
||||
|
||||
```typescript
|
||||
// vitest.config.ts
|
||||
export default defineConfig({
|
||||
test: {
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html', 'lcov'],
|
||||
exclude: [
|
||||
'node_modules/',
|
||||
'test/',
|
||||
'**/*.config.{ts,js}',
|
||||
'**/*.d.ts',
|
||||
'**/types.ts',
|
||||
],
|
||||
thresholds: {
|
||||
lines: 80,
|
||||
functions: 80,
|
||||
branches: 80,
|
||||
statements: 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Running Coverage
|
||||
|
||||
```bash
|
||||
# Run with coverage
|
||||
vitest run --coverage
|
||||
|
||||
# Watch mode with coverage
|
||||
vitest --coverage --watch
|
||||
|
||||
# Coverage for specific files
|
||||
vitest run --coverage --changed
|
||||
```
|
||||
|
||||
## Testing Best Practices
|
||||
|
||||
### 1. Query Priority (React Testing Library)
|
||||
|
||||
Use queries in this priority order:
|
||||
|
||||
1. **Accessible to everyone**:
|
||||
- `getByRole`
|
||||
- `getByLabelText`
|
||||
- `getByPlaceholderText`
|
||||
- `getByText`
|
||||
|
||||
2. **Semantic queries**:
|
||||
- `getByAltText`
|
||||
- `getByTitle`
|
||||
|
||||
3. **Test IDs** (last resort):
|
||||
- `getByTestId`
|
||||
|
||||
```typescript
|
||||
// [OK] Good
|
||||
screen.getByRole('button', { name: /submit/i })
|
||||
screen.getByLabelText(/email/i)
|
||||
|
||||
// [ERROR] Avoid
|
||||
screen.getByTestId('submit-button')
|
||||
```
|
||||
|
||||
### 2. Async Testing
|
||||
|
||||
```typescript
|
||||
import { waitFor, screen } from '@testing-library/react'
|
||||
|
||||
// [OK] Use waitFor for async assertions
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/success/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// [OK] Use findBy queries (combines getBy + waitFor)
|
||||
const element = await screen.findByText(/success/i)
|
||||
|
||||
// [ERROR] Don't use arbitrary timeouts
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
```
|
||||
|
||||
### 3. User Interactions
|
||||
|
||||
```typescript
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
||||
// [OK] Use userEvent (more realistic)
|
||||
const user = userEvent.setup()
|
||||
await user.type(input, 'text')
|
||||
await user.click(button)
|
||||
|
||||
// [ERROR] Avoid fireEvent
|
||||
import { fireEvent } from '@testing-library/react'
|
||||
fireEvent.click(button)
|
||||
```
|
||||
|
||||
### 4. Test Independence
|
||||
|
||||
```typescript
|
||||
// [OK] Each test is independent
|
||||
describe('CharacterList', () => {
|
||||
beforeEach(() => {
|
||||
// Fresh data for each test
|
||||
mockCharacters = [...]
|
||||
})
|
||||
|
||||
it('displays characters', () => {
|
||||
render(<CharacterList characters={mockCharacters} />)
|
||||
// Test logic
|
||||
})
|
||||
|
||||
it('filters characters', () => {
|
||||
render(<CharacterList characters={mockCharacters} />)
|
||||
// Test logic
|
||||
})
|
||||
})
|
||||
|
||||
// [ERROR] Tests depend on each other
|
||||
let sharedState
|
||||
|
||||
it('creates character', () => {
|
||||
sharedState = createCharacter()
|
||||
})
|
||||
|
||||
it('updates character', () => {
|
||||
updateCharacter(sharedState) // Depends on previous test
|
||||
})
|
||||
```
|
||||
|
||||
### 5. What to Test
|
||||
|
||||
**[OK] Do Test:**
|
||||
- User interactions and workflows
|
||||
- Component rendering with different props
|
||||
- Conditional logic and edge cases
|
||||
- Form validation
|
||||
- Error handling
|
||||
- Accessibility
|
||||
|
||||
**[ERROR] Don't Test:**
|
||||
- Implementation details
|
||||
- Third-party library internals
|
||||
- Exact CSS values
|
||||
- Component internal state
|
||||
- Trivial code
|
||||
|
||||
## Common Anti-Patterns
|
||||
|
||||
### [ERROR] Testing Implementation Details
|
||||
|
||||
```typescript
|
||||
// BAD: Testing state directly
|
||||
const { result } = renderHook(() => useCounter())
|
||||
expect(result.current.count).toBe(0)
|
||||
|
||||
// GOOD: Testing behavior
|
||||
render(<Counter />)
|
||||
expect(screen.getByText(/count: 0/i)).toBeInTheDocument()
|
||||
```
|
||||
|
||||
### [ERROR] Snapshot Testing Overuse
|
||||
|
||||
```typescript
|
||||
// BAD: Large snapshots
|
||||
expect(container).toMatchSnapshot()
|
||||
|
||||
// GOOD: Specific assertions
|
||||
expect(screen.getByRole('heading')).toHaveTextContent('Characters')
|
||||
```
|
||||
|
||||
### [ERROR] Not Cleaning Up
|
||||
|
||||
```typescript
|
||||
// BAD: No cleanup
|
||||
afterEach(() => {
|
||||
// Missing cleanup
|
||||
})
|
||||
|
||||
// GOOD: Proper cleanup
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
cleanup()
|
||||
})
|
||||
```
|
||||
|
||||
## Package Scripts
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"test": "vitest",
|
||||
"test:ui": "vitest --ui",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"test:watch": "vitest --watch",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:ui": "playwright test --ui",
|
||||
"test:e2e:debug": "playwright test --debug",
|
||||
"test:a11y": "playwright test a11y.spec.ts"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Validation Checklist
|
||||
|
||||
When reviewing testing skills, check for:
|
||||
|
||||
- [ ] Uses Vitest (not Jest)
|
||||
- [ ] Uses React Testing Library (not Enzyme)
|
||||
- [ ] Uses Playwright for E2E
|
||||
- [ ] Includes accessibility testing with axe-core
|
||||
- [ ] Proper query priority (role, label, text)
|
||||
- [ ] userEvent instead of fireEvent
|
||||
- [ ] Async testing with waitFor/findBy
|
||||
- [ ] Proper mocking with vi.*
|
||||
- [ ] Coverage configuration
|
||||
- [ ] Independent tests
|
||||
- [ ] Tests behavior, not implementation
|
||||
- [ ] Cleanup in afterEach
|
||||
- [ ] Descriptive test names
|
||||
- [ ] AAA pattern (Arrange, Act, Assert)
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### Jest → Vitest
|
||||
|
||||
```typescript
|
||||
// Jest
|
||||
import { jest } from '@jest/globals'
|
||||
jest.fn()
|
||||
jest.spyOn()
|
||||
jest.mock()
|
||||
|
||||
// Vitest
|
||||
import { vi } from 'vitest'
|
||||
vi.fn()
|
||||
vi.spyOn()
|
||||
vi.mock()
|
||||
```
|
||||
|
||||
### Update Imports
|
||||
|
||||
```typescript
|
||||
// Old
|
||||
import { describe, it, expect } from '@jest/globals'
|
||||
|
||||
// New
|
||||
import { describe, it, expect } from 'vitest'
|
||||
```
|
||||
|
||||
### Update Config
|
||||
|
||||
```bash
|
||||
# Remove
|
||||
npm uninstall jest @types/jest
|
||||
|
||||
# Install
|
||||
npm install -D vitest @vitejs/plugin-react jsdom
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Must Use (Modern)
|
||||
- [OK] Vitest (not Jest)
|
||||
- [OK] React Testing Library
|
||||
- [OK] Playwright
|
||||
- [OK] axe-core for a11y
|
||||
- [OK] userEvent for interactions
|
||||
- [OK] waitFor/findBy for async
|
||||
- [OK] getByRole queries
|
||||
|
||||
### Must Avoid (Deprecated)
|
||||
- [ERROR] Jest
|
||||
- [ERROR] Enzyme
|
||||
- [ERROR] fireEvent
|
||||
- [ERROR] getByTestId (overuse)
|
||||
- [ERROR] Snapshot tests (overuse)
|
||||
- [ERROR] Testing implementation details
|
||||
- [ERROR] Arbitrary timeouts
|
||||
Reference in New Issue
Block a user