#!/usr/bin/env node const fs = require('node:fs') const path = require('node:path') // ============================================================================ // Simple Analyzer // ============================================================================ class ComponentAnalyzer { analyze(code, filePath, absolutePath) { const resolvedPath = absolutePath ?? path.resolve(process.cwd(), filePath) const fileName = path.basename(filePath, path.extname(filePath)) const lineCount = code.split('\n').length const complexity = this.calculateComplexity(code, lineCount) // Count usage references (may take a few seconds) const usageCount = this.countUsageReferences(filePath, resolvedPath) // Calculate test priority const priority = this.calculateTestPriority(complexity, usageCount) return { name: fileName.charAt(0).toUpperCase() + fileName.slice(1), path: filePath, type: this.detectType(filePath, code), hasProps: code.includes('Props') || code.includes('interface'), hasState: code.includes('useState') || code.includes('useReducer'), hasEffects: code.includes('useEffect'), hasCallbacks: code.includes('useCallback'), hasMemo: code.includes('useMemo'), hasEvents: /on[A-Z]\w+/.test(code), hasRouter: code.includes('useRouter') || code.includes('usePathname'), hasAPI: code.includes('service/') || code.includes('fetch(') || code.includes('useSWR'), hasForwardRef: code.includes('forwardRef'), hasComponentMemo: /React\.memo|memo\(/.test(code), hasSuspense: code.includes('Suspense') || /\blazy\(/.test(code), hasPortal: code.includes('createPortal'), hasImperativeHandle: code.includes('useImperativeHandle'), hasSWR: code.includes('useSWR'), hasReactQuery: code.includes('useQuery') || code.includes('useMutation'), hasAhooks: code.includes("from 'ahooks'"), complexity, lineCount, usageCount, priority, } } detectType(filePath, code) { const normalizedPath = filePath.replace(/\\/g, '/') if (normalizedPath.includes('/hooks/')) return 'hook' if (normalizedPath.includes('/utils/')) return 'util' if (/\/page\.(t|j)sx?$/.test(normalizedPath)) return 'page' if (/\/layout\.(t|j)sx?$/.test(normalizedPath)) return 'layout' if (/\/providers?\//.test(normalizedPath)) return 'provider' // Dify-specific types if (normalizedPath.includes('/components/base/')) return 'base-component' if (normalizedPath.includes('/context/')) return 'context' if (normalizedPath.includes('/store/')) return 'store' if (normalizedPath.includes('/service/')) return 'service' if (/use[A-Z]\w+/.test(code)) return 'component' return 'component' } /** * Calculate component complexity score * Based on Cognitive Complexity + React-specific metrics * * Score Ranges: * 0-10: ๐ŸŸข Simple (5-10 min to test) * 11-30: ๐ŸŸก Medium (15-30 min to test) * 31-50: ๐ŸŸ  Complex (30-60 min to test) * 51+: ๐Ÿ”ด Very Complex (60+ min, consider splitting) */ calculateComplexity(code, lineCount) { let score = 0 const count = pattern => this.countMatches(code, pattern) // ===== React Hooks (State Management Complexity) ===== const stateHooks = count(/useState/g) const reducerHooks = count(/useReducer/g) const effectHooks = count(/useEffect/g) const callbackHooks = count(/useCallback/g) const memoHooks = count(/useMemo/g) const refHooks = count(/useRef/g) const imperativeHandleHooks = count(/useImperativeHandle/g) const builtinHooks = stateHooks + reducerHooks + effectHooks + callbackHooks + memoHooks + refHooks + imperativeHandleHooks const totalHooks = count(/use[A-Z]\w+/g) const customHooks = Math.max(0, totalHooks - builtinHooks) score += stateHooks * 5 // Each state +5 (need to test state changes) score += reducerHooks * 6 // Each reducer +6 (complex state management) 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 += imperativeHandleHooks * 4 // Each imperative handle +4 (exposes methods) score += customHooks * 3 // Each custom hook +3 // ===== Control Flow Complexity (Cyclomatic Complexity) ===== score += count(/if\s*\(/g) * 2 // if statement score += count(/else\s+if/g) * 2 // else if score += count(/\?\s*[^:]+\s*:/g) * 1 // ternary operator score += count(/switch\s*\(/g) * 3 // switch score += count(/case\s+/g) * 1 // case branch score += count(/&&/g) * 1 // logical AND score += count(/\|\|/g) * 1 // logical OR score += count(/\?\?/g) * 1 // nullish coalescing // ===== Loop Complexity ===== score += count(/\.map\(/g) * 2 // map score += count(/\.filter\(/g) * 1 // filter score += count(/\.reduce\(/g) * 3 // reduce (complex) score += count(/for\s*\(/g) * 2 // for loop score += count(/while\s*\(/g) * 3 // while loop // ===== Props and Events Complexity ===== // Count unique props from interface/type definitions only (avoid duplicates) const propsCount = this.countUniqueProps(code) score += Math.floor(propsCount / 2) // Every 2 props +1 // Count unique event handler names (avoid duplicates from type defs, params, usage) const uniqueEventHandlers = this.countUniqueEventHandlers(code) score += uniqueEventHandlers * 2 // Each unique event handler +2 // ===== API Call Complexity ===== score += count(/fetch\(/g) * 4 // fetch score += count(/axios\./g) * 4 // axios score += count(/useSWR/g) * 4 // SWR score += count(/useQuery/g) * 4 // React Query score += count(/\.then\(/g) * 2 // Promise score += count(/await\s+/g) * 2 // async/await // ===== Third-party Library Integration ===== // Only count complex UI libraries that require integration testing // Data fetching libs (swr, react-query, ahooks) don't add complexity // because they are already well-tested; we only need to mock them const complexUILibs = [ { pattern: /reactflow|ReactFlow/, weight: 15 }, { pattern: /@monaco-editor/, weight: 12 }, { pattern: /echarts/, weight: 8 }, { pattern: /lexical/, weight: 10 }, ] complexUILibs.forEach(({ pattern, weight }) => { if (pattern.test(code)) score += weight }) // ===== Code Size Complexity ===== if (lineCount > 500) score += 10 else if (lineCount > 300) score += 6 else if (lineCount > 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 // ===== Context and Global State ===== score += count(/useContext/g) * 3 score += count(/useStore|useAppStore/g) * 4 score += count(/zustand|redux/g) * 3 // ===== React Advanced Features ===== score += count(/React\.memo|memo\(/g) * 2 // Component memoization score += count(/forwardRef/g) * 3 // Ref forwarding score += count(/Suspense/g) * 4 // Suspense boundaries score += count(/\blazy\(/g) * 3 // Lazy loading score += count(/createPortal/g) * 3 // Portal rendering return Math.min(score, 100) // Max 100 points } /** * Calculate maximum nesting depth */ calculateNestingDepth(code) { let maxDepth = 0 let currentDepth = 0 let inString = false let stringChar = '' let escapeNext = false let inSingleLineComment = false let inMultiLineComment = false for (let i = 0; i < code.length; i++) { const char = code[i] const nextChar = code[i + 1] if (inSingleLineComment) { if (char === '\n') inSingleLineComment = false continue } if (inMultiLineComment) { if (char === '*' && nextChar === '/') { inMultiLineComment = false i++ } continue } if (inString) { if (escapeNext) { escapeNext = false continue } if (char === '\\') { escapeNext = true continue } if (char === stringChar) { inString = false stringChar = '' } continue } if (char === '/' && nextChar === '/') { inSingleLineComment = true i++ continue } if (char === '/' && nextChar === '*') { inMultiLineComment = true i++ continue } if (char === '"' || char === '\'' || char === '`') { inString = true stringChar = char continue } if (char === '{') { currentDepth++ maxDepth = Math.max(maxDepth, currentDepth) continue } if (char === '}') { currentDepth = Math.max(currentDepth - 1, 0) } } return maxDepth } /** * Count how many times a component is referenced in the codebase * Scans TypeScript sources for import statements referencing the component */ countUsageReferences(filePath, absolutePath) { try { const resolvedComponentPath = absolutePath ?? path.resolve(process.cwd(), filePath) const fileName = path.basename(resolvedComponentPath, path.extname(resolvedComponentPath)) let searchName = fileName if (fileName === 'index') { const parentDir = path.dirname(resolvedComponentPath) searchName = path.basename(parentDir) } if (!searchName) return 0 const searchRoots = this.collectSearchRoots(resolvedComponentPath) if (searchRoots.length === 0) return 0 const escapedName = ComponentAnalyzer.escapeRegExp(searchName) const patterns = [ new RegExp(`from\\s+['\"][^'\"]*(?:/|^)${escapedName}(?:['\"/]|$)`), new RegExp(`import\\s*\\(\\s*['\"][^'\"]*(?:/|^)${escapedName}(?:['\"/]|$)`), new RegExp(`export\\s+(?:\\*|{[^}]*})\\s*from\\s+['\"][^'\"]*(?:/|^)${escapedName}(?:['\"/]|$)`), new RegExp(`require\\(\\s*['\"][^'\"]*(?:/|^)${escapedName}(?:['\"/]|$)`), ] const visited = new Set() let usageCount = 0 const stack = [...searchRoots] while (stack.length > 0) { const currentDir = stack.pop() if (!currentDir || visited.has(currentDir)) continue visited.add(currentDir) const entries = fs.readdirSync(currentDir, { withFileTypes: true }) entries.forEach(entry => { const entryPath = path.join(currentDir, entry.name) if (entry.isDirectory()) { if (this.shouldSkipDir(entry.name)) return stack.push(entryPath) return } if (!this.shouldInspectFile(entry.name)) return const normalizedEntryPath = path.resolve(entryPath) if (normalizedEntryPath === path.resolve(resolvedComponentPath)) return const source = fs.readFileSync(entryPath, 'utf-8') if (!source.includes(searchName)) return if (patterns.some(pattern => { pattern.lastIndex = 0 return pattern.test(source) })) { usageCount += 1 } }) } return usageCount } catch { // If command fails, return 0 return 0 } } collectSearchRoots(resolvedComponentPath) { const roots = new Set() let currentDir = path.dirname(resolvedComponentPath) const workspaceRoot = process.cwd() while (currentDir && currentDir !== path.dirname(currentDir)) { if (path.basename(currentDir) === 'app') { roots.add(currentDir) break } if (currentDir === workspaceRoot) break currentDir = path.dirname(currentDir) } const fallbackRoots = [ path.join(workspaceRoot, 'app'), path.join(workspaceRoot, 'web', 'app'), path.join(workspaceRoot, 'src'), ] fallbackRoots.forEach(root => { if (fs.existsSync(root) && fs.statSync(root).isDirectory()) roots.add(root) }) return Array.from(roots) } shouldSkipDir(dirName) { const normalized = dirName.toLowerCase() return [ 'node_modules', '.git', '.next', 'dist', 'out', 'coverage', 'build', '__tests__', '__mocks__', ].includes(normalized) } shouldInspectFile(fileName) { const normalized = fileName.toLowerCase() if (!(/\.(ts|tsx)$/i.test(fileName))) return false if (normalized.endsWith('.d.ts')) return false if (/\.(spec|test)\.(ts|tsx)$/.test(normalized)) return false if (normalized.endsWith('.stories.tsx')) return false return true } countMatches(code, pattern) { const matches = code.match(pattern) return matches ? matches.length : 0 } /** * Count unique props from interface/type definitions * Only counts props defined in type/interface blocks, not usage */ countUniqueProps(code) { const uniqueProps = new Set() // Match interface or type definition blocks const typeBlockPattern = /(?:interface|type)\s+\w*Props[^{]*\{([^}]+)\}/g let match while ((match = typeBlockPattern.exec(code)) !== null) { const blockContent = match[1] // Match prop names (word followed by optional ? and :) const propPattern = /(\w+)\s*\??:/g let propMatch while ((propMatch = propPattern.exec(blockContent)) !== null) { uniqueProps.add(propMatch[1]) } } return Math.min(uniqueProps.size, 20) // Max 20 props } /** * Count unique event handler names (on[A-Z]...) * Avoids counting the same handler multiple times across type defs, params, and usage */ countUniqueEventHandlers(code) { const uniqueHandlers = new Set() const pattern = /on[A-Z]\w+/g let match while ((match = pattern.exec(code)) !== null) { uniqueHandlers.add(match[0]) } return uniqueHandlers.size } static escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') } /** * 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' } } // ============================================================================ // Prompt Builder for AI Assistants // ============================================================================ class TestPromptBuilder { build(analysis) { const testPath = analysis.path.replace(/\.tsx?$/, '.spec.tsx') return ` โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— โ•‘ ๐Ÿ“‹ GENERATE TEST FOR DIFY COMPONENT โ•‘ โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• ๐Ÿ“ Component: ${analysis.name} ๐Ÿ“‚ Path: ${analysis.path} ๐ŸŽฏ Test File: ${testPath} ๐Ÿ“Š Component Analysis: โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” 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 ${analysis.hasState ? 'โœ“' : 'โœ—'} Local state (useState/useReducer) ${analysis.hasEffects ? 'โœ“' : 'โœ—'} Side effects (useEffect) ${analysis.hasCallbacks ? 'โœ“' : 'โœ—'} Callbacks (useCallback) ${analysis.hasMemo ? 'โœ“' : 'โœ—'} Memoization (useMemo) ${analysis.hasEvents ? 'โœ“' : 'โœ—'} Event handlers ${analysis.hasRouter ? 'โœ“' : 'โœ—'} Next.js routing ${analysis.hasAPI ? 'โœ“' : 'โœ—'} API calls ${analysis.hasSWR ? 'โœ“' : 'โœ—'} SWR data fetching ${analysis.hasReactQuery ? 'โœ“' : 'โœ—'} React Query ${analysis.hasAhooks ? 'โœ“' : 'โœ—'} ahooks ${analysis.hasForwardRef ? 'โœ“' : 'โœ—'} Ref forwarding (forwardRef) ${analysis.hasComponentMemo ? 'โœ“' : 'โœ—'} Component memoization (React.memo) ${analysis.hasImperativeHandle ? 'โœ“' : 'โœ—'} Imperative handle โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” ๐Ÿ“ TASK: โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” Please generate a comprehensive test file for this component at: ${testPath} The component is located at: ${analysis.path} ${this.getSpecificGuidelines(analysis)} โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” ๐Ÿ“‹ PROMPT FOR AI ASSISTANT (COPY THIS TO YOUR AI ASSISTANT): โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” Generate a comprehensive test file for @${analysis.path} Including but not limited to: ${this.buildFocusPoints(analysis)} Create the test file at: ${testPath} โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” ` } getComplexityLevel(score) { // Aligned with testing.md guidelines if (score <= 10) return '๐ŸŸข Simple' if (score <= 30) return '๐ŸŸก Medium' if (score <= 50) return '๐ŸŸ  Complex' return '๐Ÿ”ด Very Complex' } buildFocusPoints(analysis) { const points = [] 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') if (analysis.hasForwardRef) points.push('- Testing ref forwarding behavior') if (analysis.hasComponentMemo) points.push('- Testing component memoization') if (analysis.hasSuspense) points.push('- Testing Suspense boundaries and lazy loading') if (analysis.hasPortal) points.push('- Testing Portal rendering') if (analysis.hasImperativeHandle) points.push('- Testing imperative handle methods') points.push('- Testing edge cases and error handling') points.push('- Testing all prop variations') return points.join('\n') } getSpecificGuidelines(analysis) { const guidelines = [] // ===== 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 || analysis.hasComponentMemo) { const features = [] if (analysis.hasCallbacks) features.push('useCallback') if (analysis.hasMemo) features.push('useMemo') if (analysis.hasComponentMemo) features.push('React.memo') 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') if (analysis.hasComponentMemo) { guidelines.push(' - Test component re-render behavior with prop changes') } } // ===== Ref Forwarding ===== if (analysis.hasForwardRef || analysis.hasImperativeHandle) { guidelines.push('๐Ÿ”— Ref forwarding detected:') guidelines.push(' - Test ref attachment to DOM elements') if (analysis.hasImperativeHandle) { guidelines.push(' - Test all exposed imperative methods') guidelines.push(' - Verify method behavior with different ref types') } } // ===== Suspense and Lazy Loading ===== if (analysis.hasSuspense) { guidelines.push('โณ Suspense/Lazy loading detected:') guidelines.push(' - Test fallback UI during loading') guidelines.push(' - Test component behavior after lazy load completes') guidelines.push(' - Test error boundaries with failed loads') } // ===== Portal ===== if (analysis.hasPortal) { guidelines.push('๐Ÿšช Portal rendering detected:') guidelines.push(' - Test content renders in portal target') guidelines.push(' - Test portal cleanup on unmount') guidelines.push(' - Verify event bubbling through portal') } // ===== API Calls ===== if (analysis.hasAPI) { guidelines.push('๐ŸŒ API calls detected:') guidelines.push(' - Mock API calls/hooks (useSWR, useQuery, fetch, etc.)') guidelines.push(' - Test loading, success, and error states') guidelines.push(' - Focus on component behavior, not the data fetching lib') } // ===== ahooks ===== if (analysis.hasAhooks) { guidelines.push('๐Ÿช ahooks detected (mock only, no need to test the lib):') guidelines.push(' - Mock ahooks utilities (useBoolean, useRequest, etc.)') guidelines.push(' - Focus on testing how your component uses the hooks') guidelines.push(' - Use fake timers if debounce/throttle is involved') } // ===== Routing ===== if (analysis.hasRouter) { 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('โš™๏ธ 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') } 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` : '' } } class TestReviewPromptBuilder { build({ analysis, testPath, testCode, originalPromptSection }) { const formattedOriginalPrompt = originalPromptSection ? originalPromptSection .split('\n') .map(line => (line.trim().length > 0 ? ` ${line}` : '')) .join('\n') .trimEnd() : ' (original generation prompt unavailable)' return ` โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— โ•‘ โœ… REVIEW TEST FOR DIFY COMPONENT โ•‘ โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• ๐Ÿ“‚ Component Path: ${analysis.path} ๐Ÿงช Test File: ${testPath} โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” ๐Ÿ“ REVIEW TASK: โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” ๐Ÿ“‹ PROMPT FOR AI ASSISTANT (COPY THIS TO YOUR AI ASSISTANT): โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” You are reviewing the frontend test coverage for @${analysis.path}. Original generation requirements: ${formattedOriginalPrompt} Test file under review: ${testPath} Checklist (ensure every item is addressed in your review): - Confirm the tests satisfy all requirements listed above and in web/testing/TESTING.md. - Verify Arrange โ†’ Act โ†’ Assert structure, mocks, and cleanup follow project conventions. - Ensure all detected component features (state, effects, routing, API, events, etc.) are exercised, including edge cases and error paths. - Check coverage of prop variations, null/undefined inputs, and high-priority workflows implied by usage score. - Validate mocks/stubs interact correctly with Next.js router, network calls, and async updates. - Ensure naming, describe/it structure, and placement match repository standards. Output format: 1. Start with a single word verdict: PASS or FAIL. 2. If FAIL, list each missing requirement or defect as a separate bullet with actionable fixes. 3. Highlight any optional improvements or refactors after mandatory issues. 4. Mention any additional tests or tooling steps (e.g., pnpm lint/test) the developer should run. โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” ` } } function extractCopyContent(prompt) { const marker = '๐Ÿ“‹ PROMPT FOR AI ASSISTANT' const markerIndex = prompt.indexOf(marker) if (markerIndex === -1) return '' const section = prompt.slice(markerIndex) const lines = section.split('\n') const firstDivider = lines.findIndex(line => line.includes('โ”โ”โ”โ”โ”โ”โ”โ”')) if (firstDivider === -1) return '' const startIdx = firstDivider + 1 let endIdx = lines.length for (let i = startIdx; i < lines.length; i++) { if (lines[i].includes('โ”โ”โ”โ”โ”โ”โ”โ”')) { endIdx = i break } } if (startIdx >= endIdx) return '' return lines.slice(startIdx, endIdx).join('\n').trim() } // ============================================================================ // Main Function // ============================================================================ function showHelp() { console.log(` ๐Ÿ“‹ Component Analyzer - Generate test prompts for AI assistants Usage: node analyze-component.js [options] pnpm analyze-component [options] Options: --help Show this help message --json Output analysis result as JSON (for programmatic use) --review Generate a review prompt for existing test file Examples: # Analyze a component and generate test prompt pnpm analyze-component app/components/base/button/index.tsx # Output as JSON pnpm analyze-component app/components/base/button/index.tsx --json # Review existing test pnpm analyze-component app/components/base/button/index.tsx --review For complete testing guidelines, see: web/testing/testing.md `) } function main() { const rawArgs = process.argv.slice(2) let isReviewMode = false let isJsonMode = false const args = [] rawArgs.forEach(arg => { if (arg === '--review') { isReviewMode = true return } if (arg === '--json') { isJsonMode = true return } if (arg === '--help' || arg === '-h') { showHelp() process.exit(0) } args.push(arg) }) if (args.length === 0) { showHelp() process.exit(1) } let componentPath = args[0] let absolutePath = path.resolve(process.cwd(), componentPath) // Check if path exists if (!fs.existsSync(absolutePath)) { console.error(`โŒ Error: Path not found: ${componentPath}`) process.exit(1) } // If directory, try to find index file if (fs.statSync(absolutePath).isDirectory()) { const indexFiles = ['index.tsx', 'index.ts', 'index.jsx', 'index.js'] let found = false for (const indexFile of indexFiles) { const indexPath = path.join(absolutePath, indexFile) if (fs.existsSync(indexPath)) { absolutePath = indexPath componentPath = path.join(componentPath, indexFile) found = true break } } if (!found) { console.error(`โŒ Error: Directory does not contain index file: ${componentPath}`) console.error(` Expected one of: ${indexFiles.join(', ')}`) process.exit(1) } } // Read source code const sourceCode = fs.readFileSync(absolutePath, 'utf-8') // Analyze const analyzer = new ComponentAnalyzer() const analysis = analyzer.analyze(sourceCode, componentPath, absolutePath) // Check if component is too complex - suggest refactoring instead of testing // Skip this check in JSON mode to always output analysis result if (!isReviewMode && !isJsonMode && (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 AI assistant const builder = new TestPromptBuilder() const generationPrompt = builder.build(analysis) let prompt = generationPrompt if (isReviewMode) { const providedTestPath = args[1] const inferredTestPath = inferTestPath(componentPath) const testPath = providedTestPath ?? inferredTestPath const absoluteTestPath = path.resolve(process.cwd(), testPath) if (!fs.existsSync(absoluteTestPath)) { console.error(`โŒ Error: Test file not found: ${testPath}`) process.exit(1) } const testCode = fs.readFileSync(absoluteTestPath, 'utf-8') const reviewBuilder = new TestReviewPromptBuilder() const originalPromptSection = extractCopyContent(generationPrompt) const normalizedTestPath = path.relative(process.cwd(), absoluteTestPath) || testPath prompt = reviewBuilder.build({ analysis, testPath: normalizedTestPath, testCode, originalPromptSection, }) } // JSON output mode if (isJsonMode) { console.log(JSON.stringify(analysis, null, 2)) return } // Output console.log(prompt) try { const { spawnSync } = require('node:child_process') const checkPbcopy = spawnSync('which', ['pbcopy'], { stdio: 'pipe' }) if (checkPbcopy.status !== 0) return const copyContent = extractCopyContent(prompt) if (!copyContent) return const result = spawnSync('pbcopy', [], { input: copyContent, encoding: 'utf-8', }) if (result.status === 0) { console.log('\n๐Ÿ“‹ Prompt copied to clipboard!') console.log(' Paste it in your AI assistant:') console.log(' - Cursor: Cmd+L (Chat) or Cmd+I (Composer)') console.log(' - GitHub Copilot Chat: Cmd+I') console.log(' - Or any other AI coding tool\n') } } catch { // pbcopy failed, but don't break the script } } function inferTestPath(componentPath) { const ext = path.extname(componentPath) if (!ext) return `${componentPath}.spec.ts` return componentPath.replace(ext, `.spec${ext}`) } // ============================================================================ // Run // ============================================================================ main()