#!/usr/bin/env node import { spawnSync } from 'node:child_process' import fs from 'node:fs' import path from 'node:path' import tsParser from '@typescript-eslint/parser' import { Linter } from 'eslint' import sonarPlugin from 'eslint-plugin-sonarjs' // ============================================================================ // 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 // Calculate complexity metrics const { total: rawComplexity, max: rawMaxComplexity } = this.calculateCognitiveComplexity(code) const complexity = this.normalizeComplexity(rawComplexity) const maxComplexity = this.normalizeComplexity(rawMaxComplexity) // 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, maxComplexity, rawComplexity, rawMaxComplexity, 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 Cognitive Complexity using SonarJS ESLint plugin * Reference: https://www.sonarsource.com/blog/5-clean-code-tips-for-reducing-cognitive-complexity/ * * Returns raw (unnormalized) complexity values: * - total: sum of all functions' complexity in the file * - max: highest single function complexity in the file * * Raw Score Thresholds (per function): * 0-15: Simple | 16-30: Medium | 31-50: Complex | 51+: Very Complex * * @returns {{ total: number, max: number }} raw total and max complexity */ calculateCognitiveComplexity(code) { const linter = new Linter() const baseConfig = { languageOptions: { parser: tsParser, parserOptions: { ecmaVersion: 'latest', sourceType: 'module', ecmaFeatures: { jsx: true }, }, }, plugins: { sonarjs: sonarPlugin }, } try { // Get total complexity using 'metric' option (more stable) const totalConfig = { ...baseConfig, rules: { 'sonarjs/cognitive-complexity': ['error', 0, 'metric'] }, } const totalMessages = linter.verify(code, totalConfig) const totalMsg = totalMessages.find( msg => msg.ruleId === 'sonarjs/cognitive-complexity' && msg.messageId === 'fileComplexity', ) const total = totalMsg ? Number.parseInt(totalMsg.message, 10) : 0 // Get max function complexity by analyzing each function const maxConfig = { ...baseConfig, rules: { 'sonarjs/cognitive-complexity': ['error', 0] }, } const maxMessages = linter.verify(code, maxConfig) let max = 0 const complexityPattern = /reduce its Cognitive Complexity from (\d+)/ maxMessages.forEach((msg) => { if (msg.ruleId === 'sonarjs/cognitive-complexity') { const match = msg.message.match(complexityPattern) if (match && match[1]) max = Math.max(max, Number.parseInt(match[1], 10)) } }) return { total, max } } catch { return { total: 0, max: 0 } } } /** * Normalize cognitive complexity to 0-100 scale * * Mapping (aligned with SonarJS thresholds): * Raw 0-15 (Simple) -> Normalized 0-25 * Raw 16-30 (Medium) -> Normalized 25-50 * Raw 31-50 (Complex) -> Normalized 50-75 * Raw 51+ (Very Complex) -> Normalized 75-100 (asymptotic) */ normalizeComplexity(rawComplexity) { if (rawComplexity <= 15) { // Linear: 0-15 -> 0-25 return Math.round((rawComplexity / 15) * 25) } else if (rawComplexity <= 30) { // Linear: 16-30 -> 25-50 return Math.round(25 + ((rawComplexity - 15) / 15) * 25) } else if (rawComplexity <= 50) { // Linear: 31-50 -> 50-75 return Math.round(50 + ((rawComplexity - 30) / 20) * 25) } else { // Asymptotic: 51+ -> 75-100 // Formula ensures score approaches but never exceeds 100 return Math.round(75 + 25 * (1 - 1 / (1 + (rawComplexity - 50) / 100))) } } /** * 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 } static escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') } /** * Calculate test priority based on cognitive complexity and usage * * Priority Score = 0.7 * Complexity + 0.3 * Usage Score (all normalized to 0-100) * - Complexity Score: 0-100 (normalized from SonarJS) * - Usage Score: 0-100 (based on reference count) * * Priority Levels (0-100): * - 0-25: ๐ŸŸข LOW * - 26-50: ๐ŸŸก MEDIUM * - 51-75: ๐ŸŸ  HIGH * - 76-100: ๐Ÿ”ด CRITICAL */ calculateTestPriority(complexity, usageCount) { const complexityScore = complexity // Normalize usage score to 0-100 let usageScore if (usageCount === 0) usageScore = 0 else if (usageCount <= 5) usageScore = 20 else if (usageCount <= 20) usageScore = 40 else if (usageCount <= 50) usageScore = 70 else usageScore = 100 // Weighted average: complexity (70%) + usage (30%) const totalScore = Math.round(0.7 * complexityScore + 0.3 * usageScore) return { score: totalScore, level: this.getPriorityLevel(totalScore), usageScore, complexityScore, } } /** * Get priority level based on score (0-100 scale) */ getPriorityLevel(score) { if (score > 75) return '๐Ÿ”ด CRITICAL' if (score > 50) return '๐ŸŸ  HIGH' if (score > 25) 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} Total Complexity: ${analysis.complexity}/100 ${this.getComplexityLevel(analysis.complexity)} Max Func Complexity: ${analysis.maxComplexity}/100 ${this.getComplexityLevel(analysis.maxComplexity)} 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) { // Normalized complexity thresholds (0-100 scale) if (score <= 25) return '๐ŸŸข Simple' if (score <= 50) return '๐ŸŸก Medium' if (score <= 75) 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 > 75) { guidelines.push(`๐Ÿ”ด HIGH Total Complexity (${analysis.complexity}/100). 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') } else if (analysis.complexity > 50) { guidelines.push(`โš ๏ธ MODERATE Total Complexity (${analysis.complexity}/100). Consider:`) guidelines.push(' - Breaking tests into multiple describe blocks') guidelines.push(' - Testing integration scenarios') guidelines.push(' - Grouping related test cases') } // ===== Max Function Complexity Warning ===== if (analysis.maxComplexity > 75) { guidelines.push(`๐Ÿ”ด HIGH Single Function Complexity (max: ${analysis.maxComplexity}/100). Consider:`) guidelines.push(' - Breaking down the complex function into smaller helpers') guidelines.push(' - Extracting logic into custom hooks or utility functions') } else if (analysis.maxComplexity > 50) { guidelines.push(`โš ๏ธ MODERATE Single Function Complexity (max: ${analysis.maxComplexity}/100). Consider:`) guidelines.push(' - Simplifying conditional logic') guidelines.push(' - Using early returns to reduce nesting') } // ===== 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 // ============================================================================ /** * Resolve directory to entry file * Priority: index files > common entry files (node.tsx, panel.tsx, etc.) */ function resolveDirectoryEntry(absolutePath, componentPath) { // Entry files in priority order: index files first, then common entry files const entryFiles = [ 'index.tsx', 'index.ts', // Priority 1: index files 'node.tsx', 'panel.tsx', 'component.tsx', 'main.tsx', 'container.tsx', // Priority 2: common entry files ] for (const entryFile of entryFiles) { const entryPath = path.join(absolutePath, entryFile) if (fs.existsSync(entryPath)) { return { absolutePath: entryPath, componentPath: path.join(componentPath, entryFile), } } } return null } /** * List analyzable files in directory (for user guidance) */ function listAnalyzableFiles(dirPath) { try { const entries = fs.readdirSync(dirPath, { withFileTypes: true }) return entries .filter(entry => !entry.isDirectory() && /\.(tsx?|jsx?)$/.test(entry.name) && !entry.name.endsWith('.d.ts')) .map(entry => entry.name) .sort((a, b) => { // Prioritize common entry files const priority = ['index.tsx', 'index.ts', 'node.tsx', 'panel.tsx', 'component.tsx', 'main.tsx', 'container.tsx'] const aIdx = priority.indexOf(a) const bIdx = priority.indexOf(b) if (aIdx !== -1 && bIdx !== -1) return aIdx - bIdx if (aIdx !== -1) return -1 if (bIdx !== -1) return 1 return a.localeCompare(b) }) } catch { return [] } } 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 entry file if (fs.statSync(absolutePath).isDirectory()) { const resolvedFile = resolveDirectoryEntry(absolutePath, componentPath) if (resolvedFile) { absolutePath = resolvedFile.absolutePath componentPath = resolvedFile.componentPath } else { // List available files for user to choose const availableFiles = listAnalyzableFiles(absolutePath) console.error(`โŒ Error: Directory does not contain a recognizable entry file: ${componentPath}`) if (availableFiles.length > 0) { console.error(`\n Available files to analyze:`) availableFiles.forEach(f => console.error(` - ${path.join(componentPath, f)}`)) console.error(`\n Please specify the exact file path, e.g.:`) console.error(` pnpm analyze-component ${path.join(componentPath, availableFiles[0])}`) } 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 > 75 || analysis.lineCount > 300)) { console.log(` โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— โ•‘ โš ๏ธ COMPONENT TOO COMPLEX TO TEST โ•‘ โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• ๐Ÿ“ Component: ${analysis.name} ๐Ÿ“‚ Path: ${analysis.path} ๐Ÿ“Š Component Metrics: โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” Total Complexity: ${analysis.complexity}/100 ${analysis.complexity > 75 ? '๐Ÿ”ด TOO HIGH' : analysis.complexity > 50 ? 'โš ๏ธ WARNING' : '๐ŸŸข OK'} Max Func Complexity: ${analysis.maxComplexity}/100 ${analysis.maxComplexity > 75 ? '๐Ÿ”ด TOO HIGH' : analysis.maxComplexity > 50 ? 'โš ๏ธ WARNING' : '๐ŸŸข OK'} Lines: ${analysis.lineCount} ${analysis.lineCount > 300 ? '๐Ÿ”ด TOO LARGE' : '๐ŸŸข OK'} โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” ๐Ÿšซ 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: - Cognitive Complexity < 50/100 (preferably < 25/100) - 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 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()