docs: add cursor rules for test generation and update analyze-component script for complexity scoring and test priority

This commit is contained in:
CodingOnStar 2025-10-28 11:25:41 +08:00
parent 8390ad95b7
commit 1bb42447cf
3 changed files with 685 additions and 160 deletions

193
.cursorrules Normal file
View File

@ -0,0 +1,193 @@
# Cursor Rules for Dify Project - Test Generation
## When Generating Tests
Follow these rules when asked to generate tests for React components:
### Tech Stack
- Next.js 15 + React 19 + TypeScript
- Jest 29.7 + React Testing Library 16.0
- Test environment: @happy-dom/jest-environment
- File naming: `ComponentName.spec.tsx` (same directory)
### Component Complexity Guidelines
#### 🔴 Very Complex Components (Complexity > 50)
- **Split before testing**: Break component into smaller pieces
- **Integration tests**: Test complex workflows end-to-end
- **Data-driven tests**: Use `test.each()` for multiple scenarios
- **Performance benchmarks**: Add performance tests for critical paths
#### ⚠️ Complex Components (Complexity 30-50)
- **Multiple describe blocks**: Group related test cases
- **Integration scenarios**: Test feature combinations
- **Organized structure**: Keep tests maintainable
#### 📏 Large Components (500+ lines)
- **Consider refactoring**: Split into smaller components if possible
- **Section testing**: Test major sections separately
- **Helper functions**: Reduce test complexity with utilities
### Test Scenarios
Apply based on component features:
#### 1. Rendering Tests (REQUIRED for all)
```typescript
describe('Rendering', () => {
it('should render without crashing', () => {
render(<Component />)
expect(screen.getByRole('...')).toBeInTheDocument()
})
})
```
#### 2. Props Testing (REQUIRED for all)
- Test each prop variation
- Test required vs optional props
- Test default values
- Test prop type validation
#### 3. State Management
**State Only (useState):**
- Test initial state values
- Test all state transitions
- Test state reset/cleanup scenarios
**Effects Only (useEffect):**
- Test effect execution conditions
- Verify dependencies array correctness
- Test cleanup on unmount
**State + Effects Combined:**
- Test state initialization and updates
- Test useEffect dependencies array
- Test cleanup functions (return from useEffect)
- Use `waitFor()` for async state changes
#### 4. Performance Optimization
**useCallback:**
- Verify callbacks maintain referential equality
- Test callback dependencies
- Ensure re-renders don't recreate functions unnecessarily
**useMemo:**
- Test memoization dependencies
- Ensure expensive computations are cached
- Verify memo recomputation conditions
#### 5. Event Handlers
- Test all onClick, onChange, onSubmit handlers
- Test keyboard events (Enter, Escape, Tab, etc.)
- Verify event.preventDefault() calls if needed
- Test event bubbling/propagation
- Use `fireEvent` (not userEvent)
#### 6. API Calls & Async Operations
- Mock all API calls using `jest.mock`
- Test retry logic if applicable
- Verify error handling and user feedback
- Use `waitFor()` for async operations
#### 7. Next.js Routing
- Mock useRouter, usePathname, useSearchParams
- Test navigation behavior and parameters
- Test query string handling
- Verify route guards/redirects if any
- Test URL parameter updates
#### 8. Edge Cases (REQUIRED for all)
- Test null/undefined/empty values
- Test boundary conditions
- Test error states
- Test loading states
- Test unexpected inputs
#### 9. Accessibility (Optional)
- Test keyboard navigation
- Verify ARIA attributes
- Test focus management
- Ensure screen reader compatibility
#### 10. Snapshots (use sparingly)
- Only for stable UI (icons, badges, static layouts)
- Snapshot small sections only
- Prefer explicit assertions over snapshots
- Update snapshots intentionally, not automatically
**Note**: Dify is a desktop app. NO responsive/mobile testing.
### Code Style
- Use `fireEvent` not `userEvent`
- AAA pattern: Arrange → Act → Assert
- Descriptive test names: "should [behavior] when [condition]"
- TypeScript: No `any` types
- Cleanup: `afterEach(() => jest.clearAllMocks())`
### Dify-Specific Components
#### General
1. **i18n**: Always return key: `t: (key) => key`
2. **Toast**: Mock `@/app/components/base/toast`
3. **Forms**: Test validation thoroughly
#### Workflow Components (`workflow/`)
- **Node configuration**: Test all node configuration options
- **Data validation**: Verify input/output validation rules
- **Variable passing**: Test data flow between nodes
- **Edge connections**: Test graph structure and connections
- **Error handling**: Verify handling of invalid configurations
- **Integration**: Test complete workflow execution paths
#### Dataset Components (`dataset/`)
- **File upload**: Test file upload and validation
- **File types**: Verify supported format handling
- **Pagination**: Test data loading and pagination
- **Search & filtering**: Test query functionality
- **Data format handling**: Test various data formats
- **Error states**: Test upload failures and invalid data
#### Configuration Components (`app/configuration`, `config/`)
- **Form validation**: Test all validation rules thoroughly
- **Save/reset functionality**: Test data persistence
- **Required vs optional fields**: Verify field validation
- **Configuration persistence**: Test state preservation
- **Error feedback**: Verify user error messages
- **Default values**: Test initial configuration state
### Test Strategy Quick Reference
**Always Test:**
- ✅ Rendering without crashing
- ✅ Props (required, optional, defaults)
- ✅ Edge cases (null, undefined, empty)
**Test When Present:**
- 🔄 **useState** → State initialization, transitions, cleanup
- ⚡ **useEffect** → Execution, dependencies, cleanup
- 🎯 **Event handlers** → All onClick, onChange, onSubmit, keyboard events
- 🌐 **API calls** → Loading, success, error states
- 🔀 **Routing** → Navigation, params, query strings
- 🚀 **useCallback/useMemo** → Referential equality, dependencies
- ⚙️ **Workflow** → Node config, data flow, validation
- 📚 **Dataset** → Upload, pagination, search
- 🎛️ **Configuration** → Form validation, persistence
**Complex Components (30+):**
- Group tests in multiple `describe` blocks
- Test integration scenarios
- Consider splitting component before testing
### Coverage Target
Aim for 100% coverage:
- Line: >95%
- Branch: >95%
- Function: 100%
- Statement: 100%
Generate comprehensive tests covering ALL code paths and scenarios.

View File

@ -1,38 +1,89 @@
# Production Build Optimization Scripts
# Web Scripts
## optimize-standalone.js
Frontend development utility scripts.
This script removes unnecessary development dependencies from the Next.js standalone build output to reduce the production Docker image size.
## 📋 Scripts
### What it does
- `generate-icons.js` - Generate PWA icons
- `optimize-standalone.js` - Optimize build output
- `analyze-component.js` - **Test generation helper**
The script specifically targets and removes `jest-worker` packages that are bundled with Next.js but not needed in production. These packages are included because:
---
1. Next.js includes jest-worker in its compiled dependencies
1. terser-webpack-plugin (used by Next.js for minification) depends on jest-worker
1. pnpm's dependency resolution creates symlinks to jest-worker in various locations
## 🚀 Generate Tests (Using Cursor AI)
### Usage
The script is automatically run during Docker builds via the `build:docker` npm script:
### Quick Start
```bash
# Docker build (removes jest-worker after build)
pnpm build:docker
# 1. Analyze component
pnpm test:gen app/components/base/button/index.tsx
# Output: Component analysis + Cursor prompt (auto-copied)
# 2. In Cursor: Cmd+L → Cmd+V → Enter → Apply
# 3. Verify
pnpm test app/components/base/button/index.spec.tsx
```
To run the optimization manually:
**Done in < 1 minute!**
---
## 📊 How It Works
### Component Complexity
Script analyzes and scores components:
- **0-10**: 🟢 Simple (5-10 min to test)
- **11-30Menu 🟡 Medium (15-30 min to test)
- **31-50Menu 🟠 Complex (30-60 min to test)
- **51+**: 🔴 Very Complex (60+ min, consider refactoring)
### Test Scenarios (11 types)
Defined in `.cursorrules`:
**Must test**: Rendering, Props, Edge Cases
**CommonMenuInteractions, Accessibility, i18n, Async
**OptionalMenuState, Security, Performance, Snapshots
Cursor AI auto-selects scenarios based on component features.
---
## 💡 Daily Workflow
```bash
node scripts/optimize-standalone.js
# New component
pnpm test:gen app/components/new-feature/index.tsx
# → Cursor → Apply → Done
# Or even simpler in Cursor
# Cmd+I → "Generate test" → Apply
```
### What gets removed
---
- `node_modules/.pnpm/next@*/node_modules/next/dist/compiled/jest-worker`
- `node_modules/.pnpm/terser-webpack-plugin@*/node_modules/jest-worker` (symlinks)
- `node_modules/.pnpm/jest-worker@*` (actual packages)
## 📋 Commands
### Impact
```bash
pnpm test:gen <path> # Generate test
pnpm test [file] # Run tests
pnpm test --coverage # View coverage
pnpm lint # Code check
pnpm type-check # Type check
```
---
## 🎯 Customize
Edit `.cursorrules` to modify test standards for your team.
```bash
code .cursorrules
git commit -m "docs: update test rules"
```
Removing jest-worker saves approximately 36KB per instance from the production image. While this may seem small, it helps ensure production images only contain necessary runtime dependencies.

View File

@ -13,8 +13,8 @@
* node scripts/analyze-component.js app/components/workflow/nodes/llm/panel.tsx
*/
const fs = require('fs');
const path = require('path');
const fs = require('node:fs')
const path = require('node:path')
// ============================================================================
// Simple Analyzer
@ -22,7 +22,15 @@ const path = require('path');
class ComponentAnalyzer {
analyze(code, filePath) {
const fileName = path.basename(filePath, path.extname(filePath));
const fileName = path.basename(filePath, path.extname(filePath))
const complexity = this.calculateComplexity(code)
const lineCount = code.split('\n').length
// Count usage references (may take a few seconds)
const usageCount = this.countUsageReferences(filePath)
// Calculate test priority
const priority = this.calculateTestPriority(complexity, usageCount)
return {
name: fileName.charAt(0).toUpperCase() + fileName.slice(1),
@ -34,21 +42,21 @@ class ComponentAnalyzer {
hasCallbacks: code.includes('useCallback'),
hasMemo: code.includes('useMemo'),
hasEvents: /on[A-Z]\w+/.test(code),
hasI18n: code.includes('useTranslation') || code.includes('react-i18next'),
hasRouter: code.includes('useRouter') || code.includes('usePathname'),
hasAPI: code.includes('service/') || code.includes('fetch('),
isClientComponent: code.includes("'use client'") || code.includes('"use client"'),
complexity: this.calculateComplexity(code),
lineCount: code.split('\n').length,
};
complexity,
lineCount,
usageCount,
priority,
}
}
detectType(filePath, code) {
if (filePath.includes('/hooks/')) return 'hook';
if (filePath.includes('/utils/')) return 'util';
if (filePath.includes('/page.tsx')) return 'page';
if (code.includes('useState') || code.includes('useEffect')) return 'component';
return 'component';
if (filePath.includes('/hooks/')) return 'hook'
if (filePath.includes('/utils/')) return 'util'
if (filePath.includes('/page.tsx')) return 'page'
if (code.includes('useState') || code.includes('useEffect')) return 'component'
return 'component'
}
/**
@ -62,103 +70,204 @@ class ComponentAnalyzer {
* 51+: 🔴 Very Complex (60+ min, consider splitting)
*/
calculateComplexity(code) {
let score = 0;
let score = 0
// ===== React Hooks (State Management Complexity) =====
const stateHooks = (code.match(/useState/g) || []).length;
const effectHooks = (code.match(/useEffect/g) || []).length;
const callbackHooks = (code.match(/useCallback/g) || []).length;
const memoHooks = (code.match(/useMemo/g) || []).length;
const refHooks = (code.match(/useRef/g) || []).length;
const customHooks = (code.match(/use[A-Z]\w+/g) || []).length -
(stateHooks + effectHooks + callbackHooks + memoHooks + refHooks);
const stateHooks = (code.match(/useState/g) || []).length
const effectHooks = (code.match(/useEffect/g) || []).length
const callbackHooks = (code.match(/useCallback/g) || []).length
const memoHooks = (code.match(/useMemo/g) || []).length
const refHooks = (code.match(/useRef/g) || []).length
const customHooks = (code.match(/use[A-Z]\w+/g) || []).length
- (stateHooks + effectHooks + callbackHooks + memoHooks + refHooks)
score += stateHooks * 5; // Each state +5 (need to test state changes)
score += effectHooks * 6; // Each effect +6 (need to test deps & cleanup)
score += callbackHooks * 2; // Each callback +2
score += memoHooks * 2; // Each memo +2
score += refHooks * 1; // Each ref +1
score += customHooks * 3; // Each custom hook +3
score += stateHooks * 5 // Each state +5 (need to test state changes)
score += effectHooks * 6 // Each effect +6 (need to test deps & cleanup)
score += callbackHooks * 2 // Each callback +2
score += memoHooks * 2 // Each memo +2
score += refHooks * 1 // Each ref +1
score += customHooks * 3 // Each custom hook +3
// ===== Control Flow Complexity (Cyclomatic Complexity) =====
score += (code.match(/if\s*\(/g) || []).length * 2; // if statement
score += (code.match(/else\s+if/g) || []).length * 2; // else if
score += (code.match(/\?\s*[^:]+\s*:/g) || []).length * 1; // ternary operator
score += (code.match(/switch\s*\(/g) || []).length * 3; // switch
score += (code.match(/case\s+/g) || []).length * 1; // case branch
score += (code.match(/&&/g) || []).length * 1; // logical AND
score += (code.match(/\|\|/g) || []).length * 1; // logical OR
score += (code.match(/\?\?/g) || []).length * 1; // nullish coalescing
score += (code.match(/if\s*\(/g) || []).length * 2 // if statement
score += (code.match(/else\s+if/g) || []).length * 2 // else if
score += (code.match(/\?\s*[^:]+\s*:/g) || []).length * 1 // ternary operator
score += (code.match(/switch\s*\(/g) || []).length * 3 // switch
score += (code.match(/case\s+/g) || []).length * 1 // case branch
score += (code.match(/&&/g) || []).length * 1 // logical AND
score += (code.match(/\|\|/g) || []).length * 1 // logical OR
score += (code.match(/\?\?/g) || []).length * 1 // nullish coalescing
// ===== Loop Complexity =====
score += (code.match(/\.map\(/g) || []).length * 2; // map
score += (code.match(/\.filter\(/g) || []).length * 1; // filter
score += (code.match(/\.reduce\(/g) || []).length * 3; // reduce (complex)
score += (code.match(/for\s*\(/g) || []).length * 2; // for loop
score += (code.match(/while\s*\(/g) || []).length * 3; // while loop
score += (code.match(/\.map\(/g) || []).length * 2 // map
score += (code.match(/\.filter\(/g) || []).length * 1 // filter
score += (code.match(/\.reduce\(/g) || []).length * 3 // reduce (complex)
score += (code.match(/for\s*\(/g) || []).length * 2 // for loop
score += (code.match(/while\s*\(/g) || []).length * 3 // while loop
// ===== Props and Events Complexity =====
const propsMatches = code.match(/(\w+)\s*:\s*\w+/g) || [];
const propsCount = Math.min(propsMatches.length, 20); // Max 20 props
score += Math.floor(propsCount / 2); // Every 2 props +1
const propsMatches = code.match(/(\w+)\s*:\s*\w+/g) || []
const propsCount = Math.min(propsMatches.length, 20) // Max 20 props
score += Math.floor(propsCount / 2) // Every 2 props +1
const eventHandlers = (code.match(/on[A-Z]\w+/g) || []).length;
score += eventHandlers * 2; // Each event handler +2
const eventHandlers = (code.match(/on[A-Z]\w+/g) || []).length
score += eventHandlers * 2 // Each event handler +2
// ===== API Call Complexity =====
score += (code.match(/fetch\(/g) || []).length * 4; // fetch
score += (code.match(/axios\./g) || []).length * 4; // axios
score += (code.match(/useSWR/g) || []).length * 4; // SWR
score += (code.match(/useQuery/g) || []).length * 4; // React Query
score += (code.match(/\.then\(/g) || []).length * 2; // Promise
score += (code.match(/await\s+/g) || []).length * 2; // async/await
score += (code.match(/fetch\(/g) || []).length * 4 // fetch
score += (code.match(/axios\./g) || []).length * 4 // axios
score += (code.match(/useSWR/g) || []).length * 4 // SWR
score += (code.match(/useQuery/g) || []).length * 4 // React Query
score += (code.match(/\.then\(/g) || []).length * 2 // Promise
score += (code.match(/await\s+/g) || []).length * 2 // async/await
// ===== Third-party Library Integration =====
const hasReactFlow = /reactflow|ReactFlow/.test(code);
const hasMonaco = /@monaco-editor/.test(code);
const hasEcharts = /echarts/.test(code);
const hasLexical = /lexical/.test(code);
const hasReactFlow = /reactflow|ReactFlow/.test(code)
const hasMonaco = /@monaco-editor/.test(code)
const hasEcharts = /echarts/.test(code)
const hasLexical = /lexical/.test(code)
if (hasReactFlow) score += 15; // ReactFlow is very complex
if (hasMonaco) score += 12; // Monaco Editor
if (hasEcharts) score += 8; // Echarts
if (hasLexical) score += 10; // Lexical Editor
if (hasReactFlow) score += 15 // ReactFlow is very complex
if (hasMonaco) score += 12 // Monaco Editor
if (hasEcharts) score += 8 // Echarts
if (hasLexical) score += 10 // Lexical Editor
// ===== Code Size Complexity =====
const lines = code.split('\n').length;
if (lines > 500) score += 10;
else if (lines > 300) score += 6;
else if (lines > 150) score += 3;
const lines = code.split('\n').length
if (lines > 500) score += 10
else if (lines > 300) score += 6
else if (lines > 150) score += 3
// ===== Nesting Depth (deep nesting reduces readability) =====
const maxNesting = this.calculateNestingDepth(code);
score += Math.max(0, (maxNesting - 3)) * 2; // Over 3 levels, +2 per level
const maxNesting = this.calculateNestingDepth(code)
score += Math.max(0, (maxNesting - 3)) * 2 // Over 3 levels, +2 per level
// ===== Context and Global State =====
score += (code.match(/useContext/g) || []).length * 3;
score += (code.match(/useStore|useAppStore/g) || []).length * 4;
score += (code.match(/zustand|redux/g) || []).length * 3;
score += (code.match(/useContext/g) || []).length * 3
score += (code.match(/useStore|useAppStore/g) || []).length * 4
score += (code.match(/zustand|redux/g) || []).length * 3
return Math.min(score, 100); // Max 100 points
return Math.min(score, 100) // Max 100 points
}
/**
* Calculate maximum nesting depth
*/
calculateNestingDepth(code) {
let maxDepth = 0;
let currentDepth = 0;
let maxDepth = 0
let currentDepth = 0
for (let i = 0; i < code.length; i++) {
if (code[i] === '{') {
currentDepth++;
maxDepth = Math.max(maxDepth, currentDepth);
} else if (code[i] === '}') {
currentDepth--;
currentDepth++
maxDepth = Math.max(maxDepth, currentDepth)
}
else if (code[i] === '}') {
currentDepth--
}
}
return maxDepth;
return maxDepth
}
/**
* Count how many times a component is referenced in the codebase
* Uses grep for searching import statements
*/
countUsageReferences(filePath) {
try {
const { execSync } = require('node:child_process')
// Get component name from file path
const fileName = path.basename(filePath, path.extname(filePath))
// If the file is index.tsx, use the parent directory name as the component name
// e.g., app/components/base/avatar/index.tsx -> search for 'avatar'
// Otherwise use the file name
// e.g., app/components/base/button.tsx -> search for 'button'
let searchName = fileName
if (fileName === 'index') {
const parentDir = path.dirname(filePath)
searchName = path.basename(parentDir)
}
// Build search pattern for import statements
// Match: from '@/app/components/base/avatar'
// Match: from './avatar'
// Match: from '../avatar'
// Simplified pattern to avoid shell quote issues
const searchPattern = `/${searchName}'`
// Use grep to search across all TypeScript files
// -r: recursive
// -l: list files with matches only
// --include: only .ts and .tsx files
// --exclude: exclude test and story files
const grepCommand = `grep -rl --include="*.ts" --include="*.tsx" --exclude="*.spec.ts" --exclude="*.spec.tsx" --exclude="*.test.ts" --exclude="*.test.tsx" --exclude="*.stories.tsx" "${searchPattern}" app/ 2>/dev/null | wc -l`
// eslint-disable-next-line sonarjs/os-command
const result = execSync(grepCommand, {
cwd: process.cwd(),
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'ignore'],
})
return Number.parseInt(result.trim(), 10) || 0
}
catch {
// If command fails, return 0
return 0
}
}
/**
* Calculate test priority based on complexity and usage
*
* Priority Score = Complexity Score + Usage Score
* - Complexity: 0-100
* - Usage: 0-50
* - Total: 0-150
*
* Priority Levels:
* - 0-30: Low
* - 31-70: Medium
* - 71-100: High
* - 100+: Critical
*/
calculateTestPriority(complexity, usageCount) {
const complexityScore = complexity
// Usage score calculation
let usageScore
if (usageCount === 0)
usageScore = 0
else if (usageCount <= 5)
usageScore = 10
else if (usageCount <= 20)
usageScore = 20
else if (usageCount <= 50)
usageScore = 35
else
usageScore = 50
const totalScore = complexityScore + usageScore
return {
score: totalScore,
level: this.getPriorityLevel(totalScore),
usageScore,
complexityScore,
}
}
/**
* Get priority level based on score
*/
getPriorityLevel(score) {
if (score > 100) return '🔴 CRITICAL'
if (score > 70) return '🟠 HIGH'
if (score > 30) return '🟡 MEDIUM'
return '🟢 LOW'
}
}
@ -167,8 +276,8 @@ class ComponentAnalyzer {
// ============================================================================
class CursorPromptBuilder {
build(analysis, sourceCode) {
const testPath = analysis.path.replace(/\.tsx?$/, '.spec.tsx');
build(analysis, _sourceCode) {
const testPath = analysis.path.replace(/\.tsx?$/, '.spec.tsx')
return `
@ -184,6 +293,8 @@ class CursorPromptBuilder {
Type: ${analysis.type}
Complexity: ${analysis.complexity} ${this.getComplexityLevel(analysis.complexity)}
Lines: ${analysis.lineCount}
Usage: ${analysis.usageCount} reference${analysis.usageCount !== 1 ? 's' : ''}
Test Priority: ${analysis.priority.score} ${analysis.priority.level}
Features Detected:
${analysis.hasProps ? '✓' : '✗'} Props/TypeScript interfaces
@ -192,10 +303,8 @@ Features Detected:
${analysis.hasCallbacks ? '✓' : '✗'} Callbacks (useCallback)
${analysis.hasMemo ? '✓' : '✗'} Memoization (useMemo)
${analysis.hasEvents ? '✓' : '✗'} Event handlers
${analysis.hasI18n ? '✓' : '✗'} Internationalization
${analysis.hasRouter ? '✓' : '✗'} Next.js routing
${analysis.hasAPI ? '✓' : '✗'} API calls
${analysis.isClientComponent ? '✓' : '✗'} Client component
📝 TASK FOR CURSOR AI:
@ -218,70 +327,172 @@ ${this.getSpecificGuidelines(analysis)}
Generate a comprehensive test file for @${analysis.path} following the project's testing guidelines in .cursorrules.
Focus on:
Including but not limited to:
${this.buildFocusPoints(analysis)}
Create the test file at: ${testPath}
`;
`
}
getComplexityLevel(score) {
if (score < 10) return '🟢 Simple';
if (score < 30) return '🟡 Medium';
return '🔴 Complex';
if (score < 10) return '🟢 Simple'
if (score < 30) return '🟡 Medium'
return '🔴 Complex'
}
buildFocusPoints(analysis) {
const points = [];
const points = []
if (analysis.hasProps) points.push('- Testing all prop variations');
if (analysis.hasState) points.push('- Testing state management and updates');
if (analysis.hasEffects) points.push('- Testing side effects and cleanup');
if (analysis.hasEvents) points.push('- Testing user interactions and event handlers');
if (analysis.hasI18n) points.push('- Mocking i18n (useTranslation)');
if (analysis.hasRouter) points.push('- Mocking Next.js router hooks');
if (analysis.hasAPI) points.push('- Mocking API calls');
if (analysis.hasState) points.push('- Testing state management and updates')
if (analysis.hasEffects) points.push('- Testing side effects and cleanup')
if (analysis.hasCallbacks) points.push('- Testing callback stability and memoization')
if (analysis.hasMemo) points.push('- Testing memoization logic and dependencies')
if (analysis.hasEvents) points.push('- Testing user interactions and event handlers')
if (analysis.hasRouter) points.push('- Mocking Next.js router hooks')
if (analysis.hasAPI) points.push('- Mocking API calls')
points.push('- Testing edge cases and error handling')
points.push('- Testing all prop variations')
points.push('- Testing accessibility (ARIA attributes)');
points.push('- Testing edge cases and error handling');
return points.join('\n');
return points.join('\n')
}
getSpecificGuidelines(analysis) {
const guidelines = [];
const guidelines = []
if (analysis.complexity > 30) {
guidelines.push('⚠️ This is a COMPLEX component. Consider:');
guidelines.push(' - Breaking tests into multiple describe blocks');
guidelines.push(' - Testing integration scenarios');
guidelines.push(' - Adding performance tests');
// ===== Test Priority Guidance =====
if (analysis.priority.level.includes('CRITICAL')) {
guidelines.push('🔴 CRITICAL PRIORITY component:')
guidelines.push(` - Used in ${analysis.usageCount} places across the codebase`)
guidelines.push(' - Changes will have WIDE impact')
guidelines.push(' - Require comprehensive test coverage')
guidelines.push(' - Add regression tests for all use cases')
guidelines.push(' - Consider integration tests with dependent components')
}
else if (analysis.usageCount > 50) {
guidelines.push('🟠 VERY HIGH USAGE component:')
guidelines.push(` - Referenced ${analysis.usageCount} times in the codebase`)
guidelines.push(' - Changes may affect many parts of the application')
guidelines.push(' - Comprehensive test coverage is CRITICAL')
guidelines.push(' - Add tests for all common usage patterns')
guidelines.push(' - Consider regression tests')
}
else if (analysis.usageCount > 20) {
guidelines.push('🟡 HIGH USAGE component:')
guidelines.push(` - Referenced ${analysis.usageCount} times in the codebase`)
guidelines.push(' - Test coverage is important to prevent widespread bugs')
guidelines.push(' - Add tests for common usage patterns')
}
// ===== Complexity Warning =====
if (analysis.complexity > 50) {
guidelines.push('🔴 VERY COMPLEX component detected. Consider:')
guidelines.push(' - Splitting component into smaller pieces before testing')
guidelines.push(' - Creating integration tests for complex workflows')
guidelines.push(' - Using test.each() for data-driven tests')
guidelines.push(' - Adding performance benchmarks')
}
else if (analysis.complexity > 30) {
guidelines.push('⚠️ This is a COMPLEX component. Consider:')
guidelines.push(' - Breaking tests into multiple describe blocks')
guidelines.push(' - Testing integration scenarios')
guidelines.push(' - Grouping related test cases')
}
// ===== State Management =====
if (analysis.hasState && analysis.hasEffects) {
guidelines.push('🔄 State + Effects detected:')
guidelines.push(' - Test state initialization and updates')
guidelines.push(' - Test useEffect dependencies array')
guidelines.push(' - Test cleanup functions (return from useEffect)')
guidelines.push(' - Use waitFor() for async state changes')
}
else if (analysis.hasState) {
guidelines.push('📊 State management detected:')
guidelines.push(' - Test initial state values')
guidelines.push(' - Test all state transitions')
guidelines.push(' - Test state reset/cleanup scenarios')
}
else if (analysis.hasEffects) {
guidelines.push('⚡ Side effects detected:')
guidelines.push(' - Test effect execution conditions')
guidelines.push(' - Verify dependencies array correctness')
guidelines.push(' - Test cleanup on unmount')
}
// ===== Performance Optimization =====
if (analysis.hasCallbacks || analysis.hasMemo) {
const features = []
if (analysis.hasCallbacks) features.push('useCallback')
if (analysis.hasMemo) features.push('useMemo')
guidelines.push(`🚀 Performance optimization (${features.join(', ')}):`)
guidelines.push(' - Verify callbacks maintain referential equality')
guidelines.push(' - Test memoization dependencies')
guidelines.push(' - Ensure expensive computations are cached')
}
// ===== API Calls =====
if (analysis.hasAPI) {
guidelines.push('🌐 This component makes API calls:');
guidelines.push(' - Mock all API calls using jest.mock');
guidelines.push(' - Test loading, success, and error states');
guidelines.push(' - Test retry logic if applicable');
guidelines.push('🌐 API calls detected:')
guidelines.push(' - Mock all API calls using jest.mock')
guidelines.push(' - Test retry logic if applicable')
guidelines.push(' - Verify error handling and user feedback')
}
// ===== Routing =====
if (analysis.hasRouter) {
guidelines.push('🔀 This component uses routing:');
guidelines.push(' - Mock useRouter, usePathname, useSearchParams');
guidelines.push(' - Test navigation behavior');
guidelines.push(' - Test URL parameter handling');
guidelines.push('🔀 Next.js routing detected:')
guidelines.push(' - Mock useRouter, usePathname, useSearchParams')
guidelines.push(' - Test navigation behavior and parameters')
guidelines.push(' - Test query string handling')
guidelines.push(' - Verify route guards/redirects if any')
}
// ===== Event Handlers =====
if (analysis.hasEvents) {
guidelines.push('🎯 Event handlers detected:')
guidelines.push(' - Test all onClick, onChange, onSubmit handlers')
guidelines.push(' - Test keyboard events (Enter, Escape, etc.)')
guidelines.push(' - Verify event.preventDefault() calls if needed')
guidelines.push(' - Test event bubbling/propagation')
}
// ===== Domain-Specific Components =====
if (analysis.path.includes('workflow')) {
guidelines.push('⚙️ This is a Workflow component:');
guidelines.push(' - Test node configuration');
guidelines.push(' - Test data validation');
guidelines.push(' - Test variable passing');
guidelines.push('⚙️ Workflow component:')
guidelines.push(' - Test node configuration and validation')
guidelines.push(' - Test data flow and variable passing')
guidelines.push(' - Test edge connections and graph structure')
guidelines.push(' - Verify error handling for invalid configs')
}
return guidelines.length > 0 ? '\n' + guidelines.join('\n') + '\n' : '';
if (analysis.path.includes('dataset')) {
guidelines.push('📚 Dataset component:')
guidelines.push(' - Test file upload and validation')
guidelines.push(' - Test pagination and data loading')
guidelines.push(' - Test search and filtering')
guidelines.push(' - Verify data format handling')
}
if (analysis.path.includes('app/configuration') || analysis.path.includes('config')) {
guidelines.push('⚙️ Configuration component:')
guidelines.push(' - Test form validation thoroughly')
guidelines.push(' - Test save/reset functionality')
guidelines.push(' - Test required vs optional fields')
guidelines.push(' - Verify configuration persistence')
}
// ===== File Size Warning =====
if (analysis.lineCount > 500) {
guidelines.push('📏 Large component (500+ lines):')
guidelines.push(' - Consider splitting into smaller components')
guidelines.push(' - Test major sections separately')
guidelines.push(' - Use helper functions to reduce test complexity')
}
return guidelines.length > 0 ? `\n${guidelines.join('\n')}\n` : ''
}
}
@ -290,7 +501,7 @@ Create the test file at: ${testPath}
// ============================================================================
function main() {
const args = process.argv.slice(2);
const args = process.argv.slice(2)
if (args.length === 0) {
console.error(`
@ -305,43 +516,114 @@ Examples:
This tool analyzes your component and generates a prompt for Cursor AI.
Copy the output and use it in Cursor Chat (Cmd+L) or Composer (Cmd+I).
`);
process.exit(1);
`)
process.exit(1)
}
const componentPath = args[0];
const absolutePath = path.resolve(process.cwd(), componentPath);
const componentPath = args[0]
const absolutePath = path.resolve(process.cwd(), componentPath)
// Check if file exists
if (!fs.existsSync(absolutePath)) {
console.error(`❌ Error: File not found: ${componentPath}`);
process.exit(1);
console.error(`❌ Error: File not found: ${componentPath}`)
process.exit(1)
}
// Read source code
const sourceCode = fs.readFileSync(absolutePath, 'utf-8');
const sourceCode = fs.readFileSync(absolutePath, 'utf-8')
// Analyze
const analyzer = new ComponentAnalyzer();
const analysis = analyzer.analyze(sourceCode, componentPath);
const analyzer = new ComponentAnalyzer()
const analysis = analyzer.analyze(sourceCode, componentPath)
// Check if component is too complex - suggest refactoring instead of testing
if (analysis.complexity > 50 || analysis.lineCount > 300) {
console.log(`
COMPONENT TOO COMPLEX TO TEST
📍 Component: ${analysis.name}
📂 Path: ${analysis.path}
📊 Component Metrics:
Complexity: ${analysis.complexity} ${analysis.complexity > 50 ? '🔴 TOO HIGH' : '⚠️ WARNING'}
Lines: ${analysis.lineCount} ${analysis.lineCount > 300 ? '🔴 TOO LARGE' : '⚠️ WARNING'}
🚫 RECOMMENDATION: REFACTOR BEFORE TESTING
This component is too complex to test effectively. Please consider:
1 **Split into smaller components**
- Extract reusable UI sections into separate components
- Separate business logic from presentation
- Create smaller, focused components (< 300 lines each)
2 **Extract custom hooks**
- Move state management logic to custom hooks
- Extract complex data transformation logic
- Separate API calls into dedicated hooks
3 **Simplify logic**
- Reduce nesting depth
- Break down complex conditions
- Extract helper functions
4 **After refactoring**
- Run this tool again on each smaller component
- Generate tests for the refactored components
- Tests will be easier to write and maintain
💡 TIP: Aim for components with:
- Complexity score < 30 (preferably < 20)
- Line count < 300 (preferably < 200)
- Single responsibility principle
`)
process.exit(0)
}
// Build prompt for Cursor
const builder = new CursorPromptBuilder();
const prompt = builder.build(analysis, sourceCode);
const builder = new CursorPromptBuilder()
const prompt = builder.build(analysis, sourceCode)
// Output
console.log(prompt);
console.log(prompt)
// Also save to clipboard if pbcopy is available (macOS)
try {
const { execSync } = require('child_process');
execSync('which pbcopy', { stdio: 'ignore' });
const { spawnSync } = require('node:child_process')
// Save prompt to clipboard
execSync('pbcopy', { input: prompt.split('📋 COPY THIS TO CURSOR:')[1].split('━━━')[0].trim() });
console.log('\n📋 Prompt copied to clipboard! Paste it in Cursor Chat (Cmd+L).\n');
} catch {
// pbcopy not available, skip
const checkPbcopy = spawnSync('which', ['pbcopy'], { stdio: 'pipe' })
if (checkPbcopy.status !== 0) return
const parts = prompt.split('📋 COPY THIS TO CURSOR:')
if (parts.length < 2) return
const afterMarker = parts[1]
const lines = afterMarker.split('\n')
const startIdx = lines.findIndex(line => line.includes('━━━')) + 1
const endIdx = lines.findIndex((line, idx) => idx > startIdx && line.includes('━━━'))
if (startIdx === 0 || endIdx === -1) return
const copyContent = lines.slice(startIdx, endIdx).join('\n').trim()
if (!copyContent) return
const result = spawnSync('pbcopy', [], {
input: copyContent,
encoding: 'utf-8',
})
if (result.status === 0)
console.log('\n📋 Prompt copied to clipboard! Paste it in Cursor Chat (Cmd+L).\n')
}
catch {
// pbcopy failed, but don't break the script
}
}
@ -349,5 +631,4 @@ Copy the output and use it in Cursor Chat (Cmd+L) or Composer (Cmd+I).
// Run
// ============================================================================
main();
main()