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
JavaScript
/**
* 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