UNPKG

@neurolint/cli

Version:

NeuroLint CLI - Deterministic code fixing for TypeScript, JavaScript, React, and Next.js with 8-layer architecture including Security Forensics, Next.js 16, React Compiler, and Turbopack support

449 lines (392 loc) 14 kB
#!/usr/bin/env node /** * NeuroLint - Licensed under Apache License 2.0 * Copyright (c) 2025 NeuroLint * http://www.apache.org/licenses/LICENSE-2.0 */ /** * Layer 6: Testing Fixes * Enhances testing infrastructure and patterns */ const fs = require('fs').promises; const path = require('path'); const BackupManager = require('../backup-manager'); /** * Transform code for testing improvements */ async function transform(code, options = {}) { const { dryRun = false, verbose = false, filePath = process.cwd() } = options; try { let updatedCode = code; const changes = []; const warnings = []; let changeCount = 0; // Test file detection patterns const testPatterns = [ /\.test\.(js|jsx|ts|tsx)$/, /\.spec\.(js|jsx|ts|tsx)$/, /__tests__\//, /test\//, /tests\// ]; const isTestFile = testPatterns.some(pattern => pattern.test(filePath)); // Always run testing improvements, not just on test files // This allows us to add testing suggestions to any component file const beforeTesting = updatedCode; // Add testing utilities imports if missing (only for test files) if (isTestFile && !updatedCode.includes('@testing-library/react') && !updatedCode.includes('@testing-library/jest-dom') && (updatedCode.includes('render') || updatedCode.includes('screen'))) { updatedCode = `import { render, screen } from '@testing-library/react';\nimport '@testing-library/jest-dom';\n\n${updatedCode}`; changes.push({ type: 'TestingImports', description: 'Added testing library imports', location: { line: 1 } }); } // Improve test descriptions (only for test files) if (isTestFile) { updatedCode = updatedCode.replace( /(describe|it|test)\s*\(\s*['"`]([^'"`]+)['"`]/g, (match, testType, description) => { if (description.length < 10) { const improvedDescription = `${description} should work correctly`; changes.push({ type: 'TestDescription', description: `Improved test description: "${description}" -> "${improvedDescription}"`, location: null }); return `${testType}('${improvedDescription}'`; } return match; } ); // Add accessibility testing if (updatedCode.includes('render') && !updatedCode.includes('toBeInTheDocument')) { updatedCode = updatedCode.replace( /(render\s*\(\s*<[^>]+>)/g, (match) => { changes.push({ type: 'AccessibilityTesting', description: 'Added accessibility testing suggestion', location: null }); return `${match}\n // Consider adding: expect(screen.getByRole('button')).toBeInTheDocument();`; } ); } } // Next.js 16 & React 19: RSC (React Server Components) testing patterns const isServerComponent = !code.includes("'use client'") && !code.includes('"use client"'); const isClientComponent = code.includes("'use client'") || code.includes('"use client"'); if (isTestFile && isServerComponent && updatedCode.includes('async function')) { // Server Component detected in test - add guidance if (!updatedCode.includes('@testing-library/react')) { warnings.push({ type: 'RSCTestingWarning', message: 'Testing Server Components requires different approach - consider integration tests instead of unit tests', location: null }); // Add RSC testing comment updatedCode = `// WARNING: React Server Component Testing:\n// - Use integration tests (Playwright/Cypress) instead of RTL\n// - Or mock fetch/database calls and test business logic separately\n// - Server Components cannot use traditional React testing tools\n\n${updatedCode}`; changes.push({ type: 'RSCTestingGuidance', description: 'Added React Server Component testing guidance', location: { line: 1 } }); } } // MSW (Mock Service Worker) compatibility check - breaks with Next.js App Router if (isTestFile && (updatedCode.includes('msw') || updatedCode.includes('setupServer'))) { if (updatedCode.includes('next') || filePath.includes('app/')) { warnings.push({ type: 'MSWCompatibilityWarning', message: 'MSW may not work properly with Next.js App Router due to Edge Runtime restrictions', location: null }); // Add MSW alternative suggestion updatedCode = `// WARNING: MSW Compatibility Issue with Next.js App Router:\n// - MSW doesn't work in Edge Runtime\n// - Consider using fetch mocking: vi.mock('node:fetch') or jest.mock('node:fetch')\n// - Or use Next.js route handlers for API mocking\n\n${updatedCode}`; changes.push({ type: 'MSWCompatibilityWarning', description: 'Added MSW compatibility warning for App Router', location: { line: 1 } }); } } // Detect untested Server Components if (!isTestFile && isServerComponent && updatedCode.includes('async function') && updatedCode.includes('export default')) { const componentName = filePath ? path.basename(filePath, path.extname(filePath)) : 'Component'; changes.push({ type: 'UntestedServerComponent', description: `Server Component '${componentName}' detected - consider adding integration tests`, location: null }); } // Add general testing suggestions for component files if (!isTestFile && updatedCode.includes('export default') && updatedCode.includes('function')) { changes.push({ type: 'TestingSuggestion', description: 'Component detected - consider adding unit tests', location: null }); } if (updatedCode !== beforeTesting) { changeCount = changes.length; } // No changes -> fail with expected message if (changeCount === 0) { return { success: false, code, originalCode: code, changeCount: 0, error: 'No changes were made', states: [code], changes, warnings }; } // Dry-run behavior if (dryRun) { if (verbose && changeCount > 0) { process.stdout.write(`[SUCCESS] Layer 6 identified ${changeCount} testing improvements (dry-run)\n`); } return { success: true, code: updatedCode, originalCode: code, changeCount, states: [code, updatedCode], changes, warnings, dryRun: true }; } if (verbose && changeCount > 0) { process.stdout.write(`[SUCCESS] Layer 6 applied ${changeCount} testing improvements to ${path.basename(filePath)}\n`); } return { success: true, code: updatedCode, originalCode: code, changeCount, states: [code, updatedCode], changes, warnings }; } catch (error) { return { success: false, code, originalCode: code, changeCount: 0, error: error.message, states: [code], changes: [], warnings: [error.message] }; } } /** * Generate test files for components */ async function generateTestFiles(componentPath, options = {}) { const { dryRun = false, verbose = false } = options; try { const componentContent = await fs.readFile(componentPath, 'utf8'); const componentName = path.basename(componentPath, path.extname(componentPath)); const testPath = componentPath.replace(/\.(jsx?|tsx?)$/, '.test.$1'); // Extract component props from TypeScript interfaces or PropTypes const propsMatch = componentContent.match(/interface\s+(\w+Props)\s*\{([^}]+)\}/); const propTypesMatch = componentContent.match(/PropTypes\.shape\(\{([^}]+)\}\)/); let props = []; if (propsMatch) { props = propsMatch[2].split('\n') .map(line => line.trim()) .filter(line => line.includes(':')) .map(line => { const [name] = line.split(':'); return name.trim(); }); } else if (propTypesMatch) { props = propTypesMatch[1].split('\n') .map(line => line.trim()) .filter(line => line.includes(':')) .map(line => { const [name] = line.split(':'); return name.trim(); }); } const testContent = `import { render, screen } from '@testing-library/react'; import '@testing-library/jest-dom'; import ${componentName} from './${componentName}'; describe('${componentName}', () => { it('should render correctly', () => { render(<${componentName} />); expect(screen.getByRole('main')).toBeInTheDocument(); }); it('should handle props correctly', () => { const testProps = { ${props.map(prop => `${prop}: 'test-${prop}'`).join(',\n ')} }; render(<${componentName} {...testProps} />); // Add specific prop testing here }); it('should be accessible', () => { render(<${componentName} />); // Add accessibility testing here expect(screen.getByRole('main')).toBeInTheDocument(); }); }); `; if (dryRun) { return { success: true, testPath, testContent, dryRun: true }; } // Create backup if file exists, then write const backupManager = new BackupManager({ backupDir: '.neurolint-backups', maxBackups: 10 }); try { await fs.access(testPath); const backupResult = await backupManager.createBackup(testPath, 'layer-6-testing'); if (!backupResult.success) { console.warn(`Warning: Could not create backup for ${testPath}: ${backupResult.error}`); } } catch { // File doesn't exist, no backup needed } await fs.writeFile(testPath, testContent); return { success: true, testPath, testContent }; } catch (error) { return { success: false, error: error.message }; } } /** * Setup testing environment */ async function setupTestingEnvironment(projectPath, options = {}) { const { dryRun = false, verbose = false } = options; try { const packageJsonPath = path.join(projectPath, 'package.json'); const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8')); // Add testing dependencies if missing (updated December 2025) const testingDeps = { '@testing-library/react': '^16.3.1', '@testing-library/jest-dom': '^6.6.3', '@testing-library/user-event': '^14.6.1', 'jest': '^30.0.0', 'jest-environment-jsdom': '^30.0.0' }; let updated = false; for (const [dep, version] of Object.entries(testingDeps)) { if (!packageJson.devDependencies?.[dep]) { packageJson.devDependencies = { ...packageJson.devDependencies, [dep]: version }; updated = true; } } // Add test scripts if missing if (!packageJson.scripts?.test) { packageJson.scripts = { ...packageJson.scripts, test: 'jest', 'test:watch': 'jest --watch', 'test:coverage': 'jest --coverage' }; updated = true; } if (updated && !dryRun) { const backupManager = new BackupManager({ backupDir: '.neurolint-backups', maxBackups: 10 }); const backupResult = await backupManager.createBackup(packageJsonPath, 'layer-6-testing'); if (!backupResult.success) { console.warn(`Warning: Could not create backup for ${packageJsonPath}: ${backupResult.error}`); } await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2)); } // Create Jest configuration const jestConfigPath = path.join(projectPath, 'jest.config.js'); if (!dryRun) { const backupManager = new BackupManager({ backupDir: '.neurolint-backups', maxBackups: 10 }); const jestConfig = `module.exports = { testEnvironment: 'jsdom', setupFilesAfterEnv: ['<rootDir>/jest.setup.js'], moduleNameMapping: { '^@/(.*)$': '<rootDir>/src/$1', }, testMatch: [ '**/__tests__/**/*.(js|jsx|ts|tsx)', '**/*.(test|spec).(js|jsx|ts|tsx)' ], collectCoverageFrom: [ 'src/**/*.(js|jsx|ts|tsx)', '!src/**/*.d.ts', '!src/**/*.stories.(js|jsx|ts|tsx)' ] };`; // Backup jest config if it exists try { await fs.access(jestConfigPath); const backupResult = await backupManager.createBackup(jestConfigPath, 'layer-6-testing'); if (!backupResult.success) { console.warn(`Warning: Could not create backup for ${jestConfigPath}: ${backupResult.error}`); } } catch { // File doesn't exist, no backup needed } await fs.writeFile(jestConfigPath, jestConfig); } // Create Jest setup file const jestSetupPath = path.join(projectPath, 'jest.setup.js'); if (!dryRun) { const jestSetup = `import '@testing-library/jest-dom';`; // Backup jest setup if it exists try { await fs.access(jestSetupPath); const backupResult = await backupManager.createBackup(jestSetupPath, 'layer-6-testing'); if (!backupResult.success) { console.warn(`Warning: Could not create backup for ${jestSetupPath}: ${backupResult.error}`); } } catch { // File doesn't exist, no backup needed } await fs.writeFile(jestSetupPath, jestSetup); } return { success: true, updated, jestConfigPath: dryRun ? 'jest.config.js' : jestConfigPath, jestSetupPath: dryRun ? 'jest.setup.js' : jestSetupPath }; } catch (error) { return { success: false, error: error.message }; } } module.exports = { transform, generateTestFiles, setupTestingEnvironment };