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()