@clawhub-hibehero-74e1ca1d4d
One-click initialization of an AI-driven unit testing environment for frontend projects (supports React/Vue/pure TypeScript/Next.js). Automatically detects p...
---
name: setup-unit-test
description: >
One-click initialization of an AI-driven unit testing environment for frontend projects (supports React/Vue/pure TypeScript/Next.js).
Automatically detects project framework, installs Vitest + Testing Library + MSW, generates config files,
and injects Claude Code custom commands for cross-project reuse.
Trigger scenarios:
(1) Need to initialize unit testing in a new or existing project.
(2) User says "initialize unit tests", "configure test environment", or "setup unit test".
(3) Need to enable /gen-unit-test and other AI test generation commands in the project.
(4) Need a single command to set up test configurations across multiple tech stacks (React/Vue/Next.js/TS).
---
## Security & Permissions Statement
This skill performs the following privileged operations to automate test environment configuration:
- **File System**: Reads `package.json` and writes config files (`vitest.config.ts`, `.claude/commands/*.md`).
- **Shell Execution**: Runs `npm/yarn/pnpm` commands to install dependencies, and runs `git` commands to detect staged files.
- **Git Hooks**: Initializes Husky and modifies `.husky/pre-commit` to automate the testing workflow.
**All scripts run locally and will not transmit project data to external servers (unless explicitly sent to Claude via a command invocation).**
---
# Initialize Unit Testing Environment
One-click setup of a production-grade unit testing solution for any frontend project. Detect environment → Install enhanced plugins → Auto alias resolution → AI command injection.
## Workflow
### Step 1: Detect Project Environment
Run the detection script to identify project details:
```bash
node <skill-dir>/scripts/detect-framework.mjs <project-dir>
```
Returns JSON containing: `os`, `framework`, `isNext`, `typescript`, `hasTsConfig`, `packageManager`, `hasGit`, `hasVitest`, etc.
### Step 2: Install Enhanced Dependencies
Install dev dependencies (`-D`) using the corresponding package manager. Skip already-installed dependencies.
**Core Toolchain**:
- `vitest`
- `@vitest/ui` (visual test interface)
- `@vitest/coverage-v8` (code coverage)
- `jsdom` (browser environment simulation)
- `msw` (API network request mocking)
- `vitest-tsconfig-paths` (auto-resolve path aliases from tsconfig.json)
**Additional for React/Next.js projects**:
- `@testing-library/react`
- `@testing-library/jest-dom`
- `@testing-library/user-event`
- `@vitejs/plugin-react`
**Additional for Vue projects**:
- `@testing-library/vue`
- `@testing-library/jest-dom`
- `@testing-library/user-event`
- `@vitejs/plugin-vue`
### Step 3: Generate Smart Config Files
#### vitest.config.ts
Uses the `tsconfigPaths()` plugin for "zero-config alias resolution".
```typescript
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import vue from '@vitejs/plugin-vue'
import tsconfigPaths from 'vitest-tsconfig-paths'
export default defineConfig({
plugins: [
tsconfigPaths(),
// react(), // Enable for React/Next.js projects
// vue(), // Enable for Vue projects
],
test: {
globals: true,
environment: 'jsdom',
include: ['tests/unit/**/*.test.{ts,tsx}'],
setupFiles: ['./tests/unit/setup/index.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'json-summary'],
include: ['src/**/*.{ts,tsx,vue}'],
exclude: ['src/**/*.stories.*', 'src/**/*.d.ts'],
thresholds: { statements: 70, branches: 70, functions: 70, lines: 70 },
},
// Additional config for Next.js server component mocking can be added here
},
})
```
#### tests/unit/setup/index.ts
```typescript
import '@testing-library/jest-dom/vitest'
import { afterAll, afterEach, beforeAll, vi } from 'vitest'
import { server } from './msw-server'
// Global Mock example (e.g., Next.js Router)
// vi.mock('next/navigation', () => ({ useRouter: () => ({ push: vi.fn() }) }))
beforeAll(() => server.listen({ onUnhandledRequest: 'warn' }))
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
```
### Step 4: Inject AI Commands (Cross-Project Reuse)
Write prompt templates to the project's `.claude/commands/`:
- `gen-unit-test.md`: Core test generation instructions.
- `fix-test.md`: Automated failure repair instructions.
### Step 5: Automation Integration (Git Hooks)
#### 5.1 Install Husky & lint-staged
#### 5.2 Copy `check-missing-tests.mjs`
#### 5.3 Write `.husky/pre-commit` (dual-layer guard)
- **Layer 1**: `vitest related --run` (only run tests affected by the current changes).
- **Layer 2**: When `AUTO_GEN_TEST=1`, detect missing tests and invoke Claude Code to auto-generate them.
### Step 6: Verification & Summary Output
```
Initialization complete:
- Framework: [detection result] (Next.js compatible)
- Alias resolution: Enabled (vitest-tsconfig-paths)
- Visual UI: Enabled (npm run test:ui)
- Coverage threshold: 70% (manually adjustable in vitest.config.ts)
- AI automation: Injected /gen-unit-test and /fix-test commands
- Auto-generation: Disabled by default, enable via export AUTO_GEN_TEST=1
```
## Resource Files
- `scripts/detect-framework.mjs` — Environment detection script (with OS and Next.js detection)
- `scripts/check-missing-tests.mjs` — Cross-platform path-compatible test checking script
- `references/gen-unit-test-prompt.md`
- `references/fix-test-prompt.md`
FILE:references/fix-test-prompt.md
# Test Failure Repair Rules
> This file is written to the project's .claude/commands/fix-test.md by /setup-unit-test
## Input
The file path of the currently failing test or the output from the most recent test run.
## Workflow
1. Run the failing test and capture the complete error output.
2. Classify the failure cause:
- **Test code bug**: Incorrect selector/assertion/mock → automatically fix the test code.
- **Source code bug**: Business logic does not match expectations → output a diagnostic report and suggest fixes.
- **Environment issue**: Timeout/missing dependency → flag and re-run.
3. After automatic repair, re-run to verify (up to 3 rounds).
4. Output a repair summary.
FILE:references/gen-unit-test-prompt.md
# AI Unit Test Generation Rules
> This file is written to the project's .claude/commands/gen-unit-test.md by /setup-unit-test
## Input
Analyze the path specified by $ARGUMENTS. Supports the following modes:
- **Single file**: `/gen-unit-test src/utils/format.ts` → generate tests for that file
- **Directory**: `/gen-unit-test src/services/` → scan all source files in the directory and generate tests one by one
- **Full scan**: `/gen-unit-test src/` → scan the entire src directory and generate tests for files missing them
- **No arguments**: `/gen-unit-test` → equivalent to `/gen-unit-test src/`
## Workflow
1. Determine the input type (file / directory):
- If a directory, recursively scan all `.ts` / `.tsx` / `.vue` files (excluding `.test.`, `.spec.`, `.stories.`, `index.ts`, etc.).
- For each file, check whether a corresponding test file already exists under `tests/unit/`; skip if it does.
2. For each source file to generate tests for:
a. Read the source code and extract all exported functions/classes/components.
b. Analyze parameter types, return values, branch paths, and external dependencies.
c. Determine the test type:
- Pure function → Vitest unit test
- React component → @testing-library/react component test
- Vue component → @testing-library/vue component test
- API call → MSW mock integration test
- Custom Hook/Composable → renderHook test
d. Generate the test file according to the specification, including:
- Happy Path (at least 1)
- Boundary values (at least 2)
- Error paths (at least 1)
3. Run `vitest run <generated test file>` to verify.
4. If it fails, analyze the error and auto-repair (up to 3 rounds).
5. After all tests pass, output a generation summary: which files were generated, pass/fail counts.
## Framework Requirements
- Use Vitest (`import { describe, it, expect, vi } from 'vitest'`).
- Use @testing-library/react or @testing-library/vue for component tests (based on the project framework).
- Use MSW (Mock Service Worker) for API mocking.
- Follow the AAA pattern (Arrange-Act-Assert).
## Coverage Requirements
- Every exported function/component must cover:
1. Happy Path (normal input → expected output).
2. Boundary values (null, zero, extremely large/small values).
3. Error paths (invalid input → expected error).
4. Branch coverage (at least one test case per if/switch branch).
## Naming Conventions
- describe: Name of the function/component under test.
- it: "should [verb] [expected behavior] when [condition]".
- Example: `it('should return 0 when both arguments are 0')`.
## Mocking Conventions
- Prefer MSW to intercept network requests rather than directly mocking modules.
- Use `vi.fn()` only for callback function verification.
- Never mock the internal implementation of the module under test.
## File Conventions
- Test files are centralized in the `tests/unit/` directory, mirroring the source code structure by module.
- Unit tests: `src/utils/format.ts` → `tests/unit/utils/format.test.ts`.
- Component tests: `src/components/Button.tsx` → `tests/unit/components/Button.test.tsx`.
## Constraints
- Use Vitest, not Jest.
- Mock network requests with MSW; do not mock internal module functions.
- Test files are centralized in the `tests/unit/` directory, mirroring the `src/` structure.
- Use the component/function name for describe; use English to describe expected behavior in it (for easier maintenance).
FILE:scripts/check-missing-tests.mjs
#!/usr/bin/env node
/**
* Check whether Git staged source files have corresponding unit test files.
* Outputs the paths of source files missing tests (one per line) for use by pre-commit hooks.
*
* Usage: node check-missing-tests.js [project-dir]
*
* Logic:
* 1. Get staged .ts/.tsx/.vue source files via git diff --cached
* 2. Exclude non-business files (test files, configs, stories, etc.)
* 3. Check whether a corresponding .test.ts/.test.tsx exists under tests/unit/
* 4. Output file paths that are missing tests
*
* Exit codes:
* 0 — All files have tests (or no files need checking)
* 1 — Some files are missing tests (paths printed to stdout)
*/
import { execSync } from 'child_process'
import fs from 'fs'
import path from 'path'
const projectDir = path.resolve(process.argv[2] || process.cwd())
// Safety check: validate directory
if (!fs.existsSync(projectDir) || !fs.statSync(projectDir).isDirectory()) {
console.error('Error: Invalid project directory.')
process.exit(1)
}
// Safety check: verify we are inside a Git repository
try {
execSync('git rev-parse --is-inside-work-tree', { cwd: projectDir, stdio: 'ignore' })
} catch (e) {
console.error('Error: Not a Git repository.')
process.exit(1)
}
// Get the list of staged files (added and modified only)
function getStagedFiles() {
try {
const output = execSync('git diff --cached --name-only --diff-filter=ACM', {
cwd: projectDir,
encoding: 'utf-8',
})
// Git path output always uses forward slashes (/). Ensure consistency on Windows.
return output.trim().split('\n').filter(Boolean).map(f => f.replace(/\\/g, '/'))
} catch {
return []
}
}
// Determine whether a file is business source code that needs tests
function isBusinessSource(filePath) {
// Normalize paths to forward slashes for consistent pattern matching
const normalizedPath = filePath.replace(/\\/g, '/')
// Only consider files under the src/ directory
if (!normalizedPath.startsWith('src/')) return false
// Only consider .ts / .tsx / .vue files
if (!/\.(ts|tsx|vue)$/.test(normalizedPath)) return false
// Exclude test files, stories, type declarations, pure re-export index files
const excludePatterns = [
/\.test\./,
/\.spec\./,
/\.stories\./,
/\.d\.ts$/,
/\/index\.(ts|tsx)$/, // Pure re-export index files
/\/types\//, // Type definition directories
/\/constants\//, // Constants directories
]
return !excludePatterns.some(p => p.test(normalizedPath))
}
// Derive the test file path from the source file path
// src/utils/format.ts → tests/unit/utils/format.test.ts
// src/components/Button.tsx → tests/unit/components/Button.test.tsx
function getTestFilePath(srcPath) {
const relativePath = srcPath.replace(/^src\//, '')
const ext = path.extname(relativePath)
const withoutExt = relativePath.slice(0, -ext.length)
// .vue files use .test.ts for their tests
const testExt = ext === '.vue' ? '.test.ts' : `.testext`
return path.join('tests', 'unit', withoutExt + testExt)
}
// Main logic
const stagedFiles = getStagedFiles()
const businessFiles = stagedFiles.filter(isBusinessSource)
const missingTests = businessFiles.filter(srcFile => {
const testFile = getTestFilePath(srcFile)
const fullPath = path.join(projectDir, testFile)
return !fs.existsSync(fullPath)
})
if (missingTests.length > 0) {
missingTests.forEach(f => console.log(f))
process.exit(1)
} else {
process.exit(0)
}
FILE:scripts/detect-framework.mjs
#!/usr/bin/env node
/**
* Detect frontend project framework, language, package manager, and other details.
* Outputs JSON: { framework, typescript, packageManager, hasVitest, hasMSW, hasTestingLibrary }
*/
import { execSync } from 'child_process'
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
function detectFramework(projectDir) {
const result = {
os: process.platform,
framework: 'unknown',
isNext: false,
typescript: false,
hasTsConfig: false,
packageManager: 'npm',
hasGit: false,
hasVitest: false,
hasVitestUI: false,
hasMSW: false,
hasTestingLibrary: false,
}
// Check if Git is available
try {
execSync('git --version', { stdio: 'ignore' })
result.hasGit = true
} catch (e) {
result.hasGit = false
}
const pkgPath = path.join(projectDir, 'package.json')
if (!fs.existsSync(pkgPath)) {
return result
}
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
const allDeps = {
...pkg.dependencies,
...pkg.devDependencies,
}
// Detect framework
if (allDeps['next']) {
result.framework = 'react'
result.isNext = true
} else if (allDeps['react'] || allDeps['react-dom']) {
result.framework = 'react'
} else if (allDeps['vue'] || allDeps['nuxt']) {
result.framework = 'vue'
}
// Detect TypeScript and tsconfig
result.hasTsConfig = fs.existsSync(path.join(projectDir, 'tsconfig.json'))
if (allDeps['typescript'] || result.hasTsConfig) {
result.typescript = true
}
// Detect package manager
if (fs.existsSync(path.join(projectDir, 'pnpm-lock.yaml'))) {
result.packageManager = 'pnpm'
} else if (fs.existsSync(path.join(projectDir, 'yarn.lock'))) {
result.packageManager = 'yarn'
} else {
result.packageManager = 'npm'
}
// Detect existing test tools
result.hasVitest = 'vitest' in allDeps
result.hasMSW = 'msw' in allDeps
result.hasTestingLibrary = Object.keys(allDeps).some(k => k.startsWith('@testing-library/'))
return result
}
const projectDir = path.resolve(process.argv[2] || process.cwd())
// Safety check: validate project directory
if (!fs.existsSync(projectDir) || !fs.statSync(projectDir).isDirectory()) {
console.error('Error: Invalid project directory.')
process.exit(1)
}
const result = detectFramework(projectDir)
console.log(JSON.stringify(result, null, 2))