Skip to main content
Glama

AI Agent Template MCP Server

by bswa006
test-generator.tsโ€ข16 kB
import { readFileSync, existsSync } from 'fs'; import { parse } from '@babel/parser'; import * as t from '@babel/types'; // Import traverse properly for ESM import babelTraverse from '@babel/traverse'; const traverse = babelTraverse.default || babelTraverse; type NodePath<T = any> = any; interface TestGenerationConfig { targetFile: string; testFramework?: 'jest' | 'vitest' | 'mocha'; coverageTarget?: number; includeEdgeCases?: boolean; includeAccessibility?: boolean; } interface TestGenerationResult { success: boolean; testCode: string; coverage: { estimated: number; functions: number; branches: number; lines: number; }; testCategories: { unit: number; integration: number; edgeCases: number; errorHandling: number; accessibility: number; }; suggestions: string[]; } export async function generateTestsForCoverage( config: TestGenerationConfig ): Promise<TestGenerationResult> { const result: TestGenerationResult = { success: false, testCode: '', coverage: { estimated: 0, functions: 0, branches: 0, lines: 0, }, testCategories: { unit: 0, integration: 0, edgeCases: 0, errorHandling: 0, accessibility: 0, }, suggestions: [], }; try { // Read target file if (!existsSync(config.targetFile)) { throw new Error(`Target file not found: ${config.targetFile}`); } const fileContent = readFileSync(config.targetFile, 'utf-8'); const fileType = detectFileType(fileContent); // Parse the file to understand structure const ast = parseCode(fileContent); const analysis = analyzeCode(ast, fileType); // Generate tests based on analysis const testFramework = config.testFramework || detectTestFramework(); const tests = generateTests(analysis, testFramework, config); // Calculate coverage estimation const coverage = estimateCoverage(analysis, tests); // Generate suggestions for reaching target coverage const suggestions = generateSuggestions( coverage, config.coverageTarget || 80, analysis ); result.success = true; result.testCode = tests.code; result.coverage = coverage; result.testCategories = tests.categories; result.suggestions = suggestions; } catch (error) { result.success = false; result.suggestions = [`Error generating tests: ${error}`]; } return result; } interface FileType { type: 'component' | 'hook' | 'service' | 'utility' | 'api'; framework: 'react' | 'node' | 'vanilla'; } function detectFileType(content: string): FileType { const hasReactImport = content.includes('from \'react\'') || content.includes('from "react"'); const hasJSX = /<[A-Z][a-zA-Z]*/.test(content); const isHook = /export\s+(?:default\s+)?function\s+use[A-Z]/.test(content); const hasAPIPatterns = /fetch|axios|request/.test(content); if (hasReactImport && hasJSX) { return { type: 'component', framework: 'react' }; } if (isHook) { return { type: 'hook', framework: 'react' }; } if (hasAPIPatterns) { return { type: 'service', framework: 'node' }; } return { type: 'utility', framework: 'vanilla' }; } function parseCode(content: string): any { return parse(content, { sourceType: 'module', plugins: ['typescript', 'jsx'], }); } interface CodeAnalysis { exports: { name: string; type: 'function' | 'class' | 'component' | 'const'; params: string[]; hasAsync: boolean; complexity: number; }[]; imports: string[]; stateVariables: string[]; effects: string[]; conditionals: number; loops: number; errorHandlers: number; } function analyzeCode(ast: any, fileType: FileType): CodeAnalysis { const analysis: CodeAnalysis = { exports: [], imports: [], stateVariables: [], effects: [], conditionals: 0, loops: 0, errorHandlers: 0, }; traverse(ast, { ImportDeclaration(path: NodePath<t.ImportDeclaration>) { analysis.imports.push(path.node.source.value); }, ExportNamedDeclaration(path: NodePath<t.ExportNamedDeclaration>) { if (path.node.declaration) { if (t.isFunctionDeclaration(path.node.declaration)) { analysis.exports.push({ name: path.node.declaration.id?.name || 'anonymous', type: 'function', params: path.node.declaration.params.map(() => 'param'), hasAsync: path.node.declaration.async, complexity: calculateComplexity(path.node.declaration), }); } } }, ExportDefaultDeclaration(path: NodePath<t.ExportDefaultDeclaration>) { if (t.isFunctionDeclaration(path.node.declaration)) { analysis.exports.push({ name: path.node.declaration.id?.name || 'default', type: fileType.type === 'component' ? 'component' : 'function', params: path.node.declaration.params.map(() => 'param'), hasAsync: path.node.declaration.async, complexity: calculateComplexity(path.node.declaration), }); } }, CallExpression(path: NodePath<t.CallExpression>) { if (t.isIdentifier(path.node.callee)) { if (path.node.callee.name === 'useState') { analysis.stateVariables.push('state'); } if (path.node.callee.name === 'useEffect') { analysis.effects.push('effect'); } } }, IfStatement() { analysis.conditionals++; }, ConditionalExpression() { analysis.conditionals++; }, ForStatement() { analysis.loops++; }, WhileStatement() { analysis.loops++; }, TryStatement() { analysis.errorHandlers++; }, }); return analysis; } function calculateComplexity(node: any): number { let complexity = 1; traverse(node, { IfStatement() { complexity++; }, ConditionalExpression() { complexity++; }, ForStatement() { complexity++; }, WhileStatement() { complexity++; }, LogicalExpression() { complexity++; }, }); return complexity; } function detectTestFramework(): 'jest' | 'vitest' | 'mocha' { // Check package.json or use default if (existsSync('package.json')) { const pkg = JSON.parse(readFileSync('package.json', 'utf-8')); const deps = { ...pkg.dependencies, ...pkg.devDependencies }; if (deps.vitest) return 'vitest'; if (deps.mocha) return 'mocha'; } return 'jest'; } interface GeneratedTests { code: string; categories: { unit: number; integration: number; edgeCases: number; errorHandling: number; accessibility: number; }; } function generateTests( analysis: CodeAnalysis, framework: string, config: TestGenerationConfig ): GeneratedTests { const tests: GeneratedTests = { code: '', categories: { unit: 0, integration: 0, edgeCases: 0, errorHandling: 0, accessibility: 0, }, }; const imports = generateImports(analysis, framework); const testSuites = analysis.exports.map(exp => generateTestSuite(exp, analysis, framework, config) ); tests.code = `${imports}\n\n${testSuites.join('\n\n')}`; // Count test categories testSuites.forEach(suite => { tests.categories.unit += (suite.match(/it\(/g) || []).length; tests.categories.edgeCases += (suite.match(/edge case/gi) || []).length; tests.categories.errorHandling += (suite.match(/error|catch|throw/gi) || []).length; tests.categories.accessibility += (suite.match(/aria|role|accessible/gi) || []).length; }); return tests; } function generateImports(analysis: CodeAnalysis, framework: string): string { const fileType = analysis.exports[0]?.type; if (fileType === 'component') { return `import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { ${analysis.exports[0].name} } from '${analysis.exports[0].name}'; // Mock dependencies ${analysis.imports.filter(imp => imp.startsWith('.')).map(imp => `jest.mock('${imp}');` ).join('\n')}`; } return `import { describe, it, expect, beforeEach, jest } from '${framework}'; import { ${analysis.exports.map(e => e.name).join(', ')} } from './module';`; } function generateTestSuite( exportItem: any, analysis: CodeAnalysis, framework: string, config: TestGenerationConfig ): string { if (exportItem.type === 'component') { return generateComponentTests(exportItem, analysis, config); } if (exportItem.type === 'function') { return generateFunctionTests(exportItem, analysis, config); } return generateGenericTests(exportItem, analysis, config); } function generateComponentTests( component: any, analysis: CodeAnalysis, config: TestGenerationConfig ): string { return `describe('${component.name}', () => { const defaultProps = { // Add default props here }; beforeEach(() => { jest.clearAllMocks(); }); describe('Rendering', () => { it('should render without crashing', () => { render(<${component.name} {...defaultProps} />); expect(screen.getByRole('region')).toBeInTheDocument(); }); it('should render with all props', () => { const props = { ...defaultProps, // Add all props }; render(<${component.name} {...props} />); // Add assertions }); ${analysis.stateVariables.length > 0 ? ` it('should handle loading state', () => { render(<${component.name} {...defaultProps} isLoading />); expect(screen.getByTestId('loading-spinner')).toBeInTheDocument(); }); it('should handle error state', () => { const error = new Error('Test error'); render(<${component.name} {...defaultProps} error={error} />); expect(screen.getByText(/error/i)).toBeInTheDocument(); }); it('should handle empty state', () => { render(<${component.name} {...defaultProps} data={[]} />); expect(screen.getByText(/no data/i)).toBeInTheDocument(); });` : ''} }); describe('Interactions', () => { it('should handle user interactions', async () => { const user = userEvent.setup(); const onClick = jest.fn(); render(<${component.name} {...defaultProps} onClick={onClick} />); const button = screen.getByRole('button'); await user.click(button); expect(onClick).toHaveBeenCalledTimes(1); }); }); ${config.includeEdgeCases ? ` describe('Edge Cases', () => { it('should handle null props gracefully', () => { render(<${component.name} {...defaultProps} data={null} />); expect(screen.queryByRole('list')).not.toBeInTheDocument(); }); it('should handle very large datasets', () => { const largeData = Array(1000).fill(null).map((_, i) => ({ id: i })); render(<${component.name} {...defaultProps} data={largeData} />); // Performance assertions }); });` : ''} ${config.includeAccessibility ? ` describe('Accessibility', () => { it('should have proper ARIA labels', () => { render(<${component.name} {...defaultProps} />); expect(screen.getByRole('button')).toHaveAttribute('aria-label'); expect(screen.getByRole('region')).toHaveAttribute('aria-labelledby'); }); it('should be keyboard navigable', async () => { const user = userEvent.setup(); render(<${component.name} {...defaultProps} />); await user.tab(); expect(screen.getByRole('button')).toHaveFocus(); }); it('should announce changes to screen readers', async () => { const { rerender } = render(<${component.name} {...defaultProps} />); rerender(<${component.name} {...defaultProps} status="success" />); expect(screen.getByRole('status')).toHaveTextContent(/success/i); }); });` : ''} });`; } function generateFunctionTests( func: any, analysis: CodeAnalysis, config: TestGenerationConfig ): string { return `describe('${func.name}', () => { ${func.hasAsync ? ` it('should handle async operations', async () => { const result = await ${func.name}(/* params */); expect(result).toBeDefined(); }); it('should handle async errors', async () => { // Mock error scenario await expect(${func.name}(/* invalid params */)).rejects.toThrow(); });` : ` it('should return expected result', () => { const result = ${func.name}(/* params */); expect(result).toBeDefined(); });`} ${analysis.errorHandlers > 0 ? ` it('should handle errors gracefully', () => { expect(() => ${func.name}(/* invalid params */)).not.toThrow(); });` : ''} ${config.includeEdgeCases ? ` describe('Edge Cases', () => { it('should handle null input', () => { const result = ${func.name}(null); expect(result).toBeDefined(); }); it('should handle empty input', () => { const result = ${func.name}([]); expect(result).toBeDefined(); }); it('should handle large input', () => { const largeInput = Array(10000).fill(0); const result = ${func.name}(largeInput); expect(result).toBeDefined(); }); });` : ''} });`; } function generateGenericTests( item: any, analysis: CodeAnalysis, config: TestGenerationConfig ): string { return `describe('${item.name}', () => { it('should be defined', () => { expect(${item.name}).toBeDefined(); }); // Add more specific tests based on the implementation });`; } function estimateCoverage(analysis: CodeAnalysis, tests: GeneratedTests): any { const totalFunctions = analysis.exports.length; const totalBranches = analysis.conditionals; const totalLines = analysis.exports.reduce((acc, exp) => acc + exp.complexity * 10, 0); const coveredFunctions = Math.min(tests.categories.unit, totalFunctions); const coveredBranches = Math.min( tests.categories.unit + tests.categories.edgeCases, totalBranches ); const coveredLines = Math.min( (tests.categories.unit * 10) + (tests.categories.edgeCases * 5), totalLines ); return { estimated: Math.round( ((coveredFunctions / totalFunctions) * 0.3 + (coveredBranches / Math.max(totalBranches, 1)) * 0.3 + (coveredLines / Math.max(totalLines, 1)) * 0.4) * 100 ), functions: Math.round((coveredFunctions / totalFunctions) * 100), branches: Math.round((coveredBranches / Math.max(totalBranches, 1)) * 100), lines: Math.round((coveredLines / Math.max(totalLines, 1)) * 100), }; } function generateSuggestions( coverage: any, target: number, analysis: CodeAnalysis ): string[] { const suggestions: string[] = []; if (coverage.estimated < target) { const gap = target - coverage.estimated; if (coverage.functions < target) { suggestions.push( `Add ${Math.ceil(gap / 10)} more unit tests to cover all exported functions` ); } if (coverage.branches < target) { suggestions.push( 'Add tests for all conditional branches (if/else, switch cases)' ); } if (analysis.errorHandlers > 0 && coverage.estimated < target) { suggestions.push( 'Add specific error handling tests for try/catch blocks' ); } if (analysis.effects.length > 0) { suggestions.push( 'Add tests for useEffect hooks and their cleanup functions' ); } suggestions.push( 'Consider adding integration tests to test component interactions', 'Add edge case tests for boundary conditions', 'Include performance tests for operations with large datasets' ); } else { suggestions.push( `Great! Estimated coverage of ${coverage.estimated}% exceeds target of ${target}%`, 'Consider adding e2e tests for critical user journeys', 'Add visual regression tests for UI components' ); } return suggestions; }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/bswa006/mcp-context-manager'

If you have feedback or need assistance with the MCP directory API, please join our Discord server