UNPKG

vineguard-mcp-server-standalone

Version:

VineGuard MCP Server v2.1 - Intelligent QA Workflow System with advanced test generation for Jest/RTL, Cypress, and Playwright. Features smart project analysis, progressive testing strategies, and comprehensive quality patterns for React/Vue/Angular proje

524 lines 22.4 kB
/** * Component testing tool for VineGuard MCP Server * Specialized testing for React, Vue, Angular, and other frontend components */ import * as fs from 'fs/promises'; import * as path from 'path'; export class ComponentTester { /** * Analyze and generate tests for a component */ static async testComponent(componentPath, options = {}) { const { testType = 'unit', framework = 'auto-detect', includeVisualTests = false, includeAccessibilityTests = true, mockDependencies = true } = options; try { // Read and analyze the component const componentContent = await fs.readFile(componentPath, 'utf-8'); const analysis = this.analyzeComponent(componentContent, componentPath); // Determine framework if not specified const detectedFramework = framework === 'auto-detect' ? analysis.framework : framework; // Generate appropriate test code const testCode = this.generateComponentTest(analysis, detectedFramework, testType, { includeVisualTests, includeAccessibilityTests, mockDependencies }); // Generate recommendations const recommendations = this.generateRecommendations(analysis, testType); return { componentPath, analysis, testCode, testType, framework: detectedFramework, recommendations, generatedAt: new Date().toISOString() }; } catch (error) { throw new Error(`Component testing failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Analyze component structure and extract metadata */ static analyzeComponent(content, filePath) { const componentName = this.extractComponentName(content, filePath); const framework = this.detectFramework(content, filePath); const analysis = { componentName, framework, props: this.extractProps(content, framework), hooks: this.extractHooks(content, framework), state: this.extractState(content, framework), events: this.extractEvents(content, framework), lifecycle: this.extractLifecycleMethods(content, framework), dependencies: this.extractDependencies(content), complexity: this.calculateComplexity(content) }; return analysis; } /** * Extract component name from file content or path */ static extractComponentName(content, filePath) { // Try to extract from export statement const exportMatch = content.match(/export\s+(?:default\s+)?(?:function|const|class)\s+(\w+)/); if (exportMatch) { return exportMatch[1]; } // Try to extract from function/class declaration const functionMatch = content.match(/(?:function|class)\s+(\w+)/); if (functionMatch) { return functionMatch[1]; } // Fall back to filename const basename = path.basename(filePath, path.extname(filePath)); return basename.charAt(0).toUpperCase() + basename.slice(1); } /** * Detect component framework */ static detectFramework(content, filePath) { if (content.includes('import React') || content.includes('from "react"') || content.includes('useState') || content.includes('useEffect')) { return 'react'; } if (content.includes('Vue') || filePath.endsWith('.vue') || content.includes('@Component')) { return 'vue'; } if (content.includes('@Component') || content.includes('NgModule') || content.includes('Angular')) { return 'angular'; } if (filePath.endsWith('.svelte') || content.includes('onMount')) { return 'svelte'; } return 'unknown'; } /** * Extract component props/properties */ static extractProps(content, framework) { const props = []; switch (framework) { case 'react': // Extract from TypeScript interface const interfaceMatch = content.match(/interface\s+\w+Props\s*{([^}]+)}/s); if (interfaceMatch) { const propsDef = interfaceMatch[1]; const propMatches = propsDef.matchAll(/(\w+)(\?)?:\s*([^;,\n]+)/g); for (const match of propMatches) { props.push({ name: match[1], type: match[3].trim(), required: !match[2], defaultValue: this.extractDefaultValue(content, match[1]) }); } } // Extract from destructuring const destructureMatch = content.match(/\{\s*([^}]+)\s*\}\s*:\s*\w+Props/); if (destructureMatch) { const propNames = destructureMatch[1].split(',').map(p => p.trim()); for (const propName of propNames) { if (!props.find(p => p.name === propName)) { props.push({ name: propName, type: 'any', required: true }); } } } break; case 'vue': // Extract from Vue props definition const vuePropsMatch = content.match(/props:\s*{([^}]+)}/s); if (vuePropsMatch) { const propsDef = vuePropsMatch[1]; const propMatches = propsDef.matchAll(/(\w+):\s*{([^}]+)}/g); for (const match of propMatches) { const propName = match[1]; const propConfig = match[2]; const typeMatch = propConfig.match(/type:\s*(\w+)/); const requiredMatch = propConfig.match(/required:\s*(true|false)/); const defaultMatch = propConfig.match(/default:\s*([^,\n]+)/); props.push({ name: propName, type: typeMatch ? typeMatch[1] : 'any', required: requiredMatch ? requiredMatch[1] === 'true' : false, defaultValue: defaultMatch ? defaultMatch[1].trim() : undefined }); } } break; case 'angular': // Extract from @Input decorators const inputMatches = content.matchAll(/@Input\(\)\s*(\w+)(?::\s*([^;=\n]+))?/g); for (const match of inputMatches) { props.push({ name: match[1], type: match[2]?.trim() || 'any', required: true }); } break; } return props; } /** * Extract React hooks usage */ static extractHooks(content, framework) { if (framework !== 'react') return []; const hooks = []; const hookPattern = /use\w+/g; const matches = content.match(hookPattern); if (matches) { hooks.push(...new Set(matches)); } return hooks; } /** * Extract state variables */ static extractState(content, framework) { const state = []; switch (framework) { case 'react': const useStateMatches = content.matchAll(/const\s*\[\s*(\w+),\s*set\w+\s*\]\s*=\s*useState/g); for (const match of useStateMatches) { state.push(match[1]); } break; case 'vue': const vueStateMatches = content.matchAll(/data\(\)\s*{[^}]*(\w+):/g); for (const match of vueStateMatches) { state.push(match[1]); } break; case 'angular': const ngStateMatches = content.matchAll(/private|public\s+(\w+)(?:\s*:\s*[^=\n;]+)?(?:\s*=|;)/g); for (const match of ngStateMatches) { state.push(match[1]); } break; } return state; } /** * Extract event handlers */ static extractEvents(content, framework) { const events = []; // Common event handler patterns const eventPatterns = [ /on\w+/g, /handle\w+/g, /@click|@input|@change|@submit/g, // Vue events /\(click\)|\(input\)|\(change\)|\(submit\)/g // Angular events ]; for (const pattern of eventPatterns) { const matches = content.match(pattern); if (matches) { events.push(...matches); } } return [...new Set(events)]; } /** * Extract lifecycle methods */ static extractLifecycleMethods(content, framework) { const lifecycle = []; switch (framework) { case 'react': const reactLifecycle = ['useEffect', 'useLayoutEffect', 'useMemo', 'useCallback']; for (const method of reactLifecycle) { if (content.includes(method)) { lifecycle.push(method); } } break; case 'vue': const vueLifecycle = ['mounted', 'created', 'updated', 'destroyed', 'beforeMount', 'beforeUpdate', 'beforeDestroy']; for (const method of vueLifecycle) { if (content.includes(method)) { lifecycle.push(method); } } break; case 'angular': const ngLifecycle = ['ngOnInit', 'ngOnDestroy', 'ngOnChanges', 'ngAfterViewInit']; for (const method of ngLifecycle) { if (content.includes(method)) { lifecycle.push(method); } } break; } return lifecycle; } /** * Extract component dependencies */ static extractDependencies(content) { const dependencies = []; const importMatches = content.matchAll(/import.*from\s+['"]([^'"]+)['"]/g); for (const match of importMatches) { dependencies.push(match[1]); } return dependencies; } /** * Calculate component complexity */ static calculateComplexity(content) { const lines = content.split('\n').length; const branches = (content.match(/if|switch|case|for|while|\?/g) || []).length; const functions = (content.match(/function|=>/g) || []).length; const complexityScore = lines * 0.1 + branches * 2 + functions; if (complexityScore < 20) return 'low'; if (complexityScore < 50) return 'medium'; return 'high'; } /** * Extract default value for a prop */ static extractDefaultValue(content, propName) { const defaultMatch = content.match(new RegExp(`${propName}\\s*=\\s*([^,\\n}]+)`)); return defaultMatch ? defaultMatch[1].trim() : undefined; } /** * Generate comprehensive test code */ static generateComponentTest(analysis, framework, testType, options) { const { componentName, props, hooks, events } = analysis; const { includeVisualTests, includeAccessibilityTests, mockDependencies } = options; let testCode = ''; // Imports if (framework === 'react') { testCode += `import React from 'react';\n`; testCode += `import { render, screen, fireEvent, waitFor } from '@testing-library/react';\n`; testCode += `import userEvent from '@testing-library/user-event';\n`; if (includeAccessibilityTests) { testCode += `import { axe, toHaveNoViolations } from 'jest-axe';\n`; } testCode += `import { ${componentName} } from './${componentName}';\n\n`; if (includeAccessibilityTests) { testCode += `expect.extend(toHaveNoViolations);\n\n`; } } // Mock dependencies if requested if (mockDependencies && analysis.dependencies.length > 0) { testCode += `// Mock dependencies\n`; for (const dep of analysis.dependencies) { if (!dep.startsWith('.') && !dep.startsWith('react')) { testCode += `jest.mock('${dep}');\n`; } } testCode += `\n`; } // Test suite testCode += `describe('${componentName}', () => {\n`; // Setup if (props.length > 0) { testCode += ` const defaultProps = {\n`; for (const prop of props) { const value = this.generateMockValue(prop.type, prop.defaultValue); testCode += ` ${prop.name}: ${value},\n`; } testCode += ` };\n\n`; } // Basic rendering test testCode += ` it('renders without crashing', () => {\n`; if (props.length > 0) { testCode += ` render(<${componentName} {...defaultProps} />);\n`; } else { testCode += ` render(<${componentName} />);\n`; } testCode += ` expect(screen.getByRole('main')).toBeInTheDocument();\n`; testCode += ` });\n\n`; // Props testing if (props.length > 0) { testCode += ` describe('Props', () => {\n`; for (const prop of props) { if (prop.required) { testCode += ` it('handles ${prop.name} prop correctly', () => {\n`; testCode += ` const testValue = ${this.generateMockValue(prop.type)};\n`; testCode += ` render(<${componentName} {...defaultProps} ${prop.name}={testValue} />);\n`; testCode += ` // Add specific assertions for ${prop.name}\n`; testCode += ` });\n\n`; } } testCode += ` });\n\n`; } // Event handling tests if (events.length > 0) { testCode += ` describe('Event Handling', () => {\n`; for (const event of events.slice(0, 3)) { // Limit to first 3 events testCode += ` it('handles ${event} correctly', async () => {\n`; testCode += ` const user = userEvent.setup();\n`; testCode += ` const mock${event} = jest.fn();\n`; if (props.length > 0) { testCode += ` render(<${componentName} {...defaultProps} ${event}={mock${event}} />);\n`; } else { testCode += ` render(<${componentName} ${event}={mock${event}} />);\n`; } testCode += ` \n`; testCode += ` // Trigger the event - adjust selector as needed\n`; testCode += ` const element = screen.getByRole('button'); // Adjust as needed\n`; testCode += ` await user.click(element);\n`; testCode += ` \n`; testCode += ` expect(mock${event}).toHaveBeenCalled();\n`; testCode += ` });\n\n`; } testCode += ` });\n\n`; } // State testing (for stateful components) if (hooks.includes('useState') || analysis.state.length > 0) { testCode += ` describe('State Management', () => {\n`; testCode += ` it('manages internal state correctly', async () => {\n`; testCode += ` const user = userEvent.setup();\n`; if (props.length > 0) { testCode += ` render(<${componentName} {...defaultProps} />);\n`; } else { testCode += ` render(<${componentName} />);\n`; } testCode += ` \n`; testCode += ` // Test state changes - adjust based on component behavior\n`; testCode += ` // Example: clicking a button that changes state\n`; testCode += ` // const button = screen.getByRole('button');\n`; testCode += ` // await user.click(button);\n`; testCode += ` // expect(screen.getByText(/new state/i)).toBeInTheDocument();\n`; testCode += ` });\n`; testCode += ` });\n\n`; } // Accessibility tests if (includeAccessibilityTests) { testCode += ` describe('Accessibility', () => {\n`; testCode += ` it('has no accessibility violations', async () => {\n`; if (props.length > 0) { testCode += ` const { container } = render(<${componentName} {...defaultProps} />);\n`; } else { testCode += ` const { container } = render(<${componentName} />);\n`; } testCode += ` const results = await axe(container);\n`; testCode += ` expect(results).toHaveNoViolations();\n`; testCode += ` });\n\n`; testCode += ` it('supports keyboard navigation', async () => {\n`; testCode += ` const user = userEvent.setup();\n`; if (props.length > 0) { testCode += ` render(<${componentName} {...defaultProps} />);\n`; } else { testCode += ` render(<${componentName} />);\n`; } testCode += ` \n`; testCode += ` // Test keyboard navigation\n`; testCode += ` await user.tab();\n`; testCode += ` // Add assertions for focus management\n`; testCode += ` });\n`; testCode += ` });\n\n`; } // Visual tests if (includeVisualTests) { testCode += ` describe('Visual Regression', () => {\n`; testCode += ` it('matches visual snapshot', () => {\n`; if (props.length > 0) { testCode += ` const { container } = render(<${componentName} {...defaultProps} />);\n`; } else { testCode += ` const { container } = render(<${componentName} />);\n`; } testCode += ` expect(container.firstChild).toMatchSnapshot();\n`; testCode += ` });\n`; testCode += ` });\n\n`; } // Error boundary tests for complex components if (analysis.complexity === 'high') { testCode += ` describe('Error Handling', () => {\n`; testCode += ` it('handles errors gracefully', () => {\n`; testCode += ` const mockError = jest.spyOn(console, 'error').mockImplementation(() => {});\n`; testCode += ` \n`; testCode += ` // Test error scenarios\n`; testCode += ` // render(<${componentName} {...defaultProps} errorProp={true} />);\n`; testCode += ` \n`; testCode += ` mockError.mockRestore();\n`; testCode += ` });\n`; testCode += ` });\n\n`; } testCode += `});\n`; return testCode; } /** * Generate mock values for different prop types */ static generateMockValue(type, defaultValue) { if (defaultValue) { return defaultValue; } switch (type.toLowerCase()) { case 'string': return '"test string"'; case 'number': return '42'; case 'boolean': return 'true'; case 'array': return '[]'; case 'object': return '{}'; case 'function': return 'jest.fn()'; case 'date': return 'new Date()'; default: if (type.includes('[]')) { return '[]'; } if (type.includes('{}') || type.includes('object')) { return '{}'; } if (type.includes('function') || type.includes('=>')) { return 'jest.fn()'; } return '"mock value"'; } } /** * Generate recommendations based on analysis */ static generateRecommendations(analysis, testType) { const recommendations = []; if (analysis.props.length > 5) { recommendations.push('Consider breaking down this component - it has many props'); } if (analysis.complexity === 'high') { recommendations.push('High complexity detected - consider splitting into smaller components'); recommendations.push('Add comprehensive error boundary tests'); } if (analysis.state.length > 3) { recommendations.push('Component has complex state - consider using state management library'); } if (analysis.events.length > 5) { recommendations.push('Many event handlers detected - ensure proper cleanup'); } if (analysis.hooks.length > 4) { recommendations.push('Consider creating custom hooks for reusable logic'); } if (testType === 'unit') { recommendations.push('Add integration tests to verify component interactions'); } recommendations.push('Consider adding visual regression tests for UI consistency'); recommendations.push('Add performance tests for complex components'); recommendations.push('Ensure all user interactions are covered by tests'); return recommendations; } } //# sourceMappingURL=component-testing.js.map