diff --git a/.cursorrules b/.cursorrules new file mode 100644 index 0000000000..0529506fc2 --- /dev/null +++ b/.cursorrules @@ -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() + 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. + diff --git a/web/scripts/README.md b/web/scripts/README.md index 2c575a244c..bfd733c07b 100644 --- a/web/scripts/README.md +++ b/web/scripts/README.md @@ -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 # 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. diff --git a/web/scripts/analyze-component.js b/web/scripts/analyze-component.js index a07b6665c6..49189c8093 100755 --- a/web/scripts/analyze-component.js +++ b/web/scripts/analyze-component.js @@ -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()