# Directory Structure ``` ├── .dockerignore ├── .gitignore ├── dist │ ├── index.d.ts │ ├── index.js │ ├── index.js.map │ ├── prompts │ │ ├── index.d.ts │ │ ├── index.js │ │ └── index.js.map │ ├── resources │ │ ├── docs.d.ts │ │ ├── docs.js │ │ ├── docs.js.map │ │ ├── templates.d.ts │ │ ├── templates.js │ │ └── templates.js.map │ ├── server.d.ts │ ├── server.js │ ├── server.js.map │ ├── tools │ │ ├── analyzer.d.ts │ │ ├── analyzer.js │ │ ├── analyzer.js.map │ │ ├── component-tester.d.ts │ │ ├── component-tester.js │ │ ├── component-tester.js.map │ │ ├── generator.d.ts │ │ ├── generator.js │ │ ├── generator.js.map │ │ ├── runner.d.ts │ │ ├── runner.js │ │ └── runner.js.map │ ├── transports │ │ ├── http.d.ts │ │ ├── http.js │ │ ├── http.js.map │ │ ├── stdio.d.ts │ │ ├── stdio.js │ │ └── stdio.js.map │ └── utils │ ├── command-executor.d.ts │ ├── command-executor.js │ ├── command-executor.js.map │ ├── test-config.d.ts │ ├── test-config.js │ ├── test-config.js.map │ ├── test-environment.d.ts │ ├── test-environment.js │ └── test-environment.js.map ├── docker-compose.yml ├── Dockerfile ├── LICENSE ├── package-lock.json ├── package.json ├── README.md ├── src │ ├── index.ts │ ├── prompts │ │ └── index.ts │ ├── resources │ │ ├── docs.ts │ │ └── templates.ts │ ├── server.ts │ ├── tools │ │ ├── analyzer.ts │ │ ├── component-tester.ts │ │ ├── generator.ts │ │ └── runner.ts │ ├── transports │ │ ├── http.ts │ │ └── stdio.ts │ └── utils │ ├── command-executor.ts │ ├── test-config.ts │ └── test-environment.ts ├── templates │ ├── cypress │ │ └── component.txt │ └── jest │ ├── component.txt │ └── unit.txt └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | node_modules 2 | ``` -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- ``` 1 | # Node.js 2 | node_modules 3 | npm-debug.log 4 | yarn-debug.log 5 | yarn-error.log 6 | 7 | # TypeScript output 8 | dist 9 | build 10 | 11 | # Version control 12 | .git 13 | .gitignore 14 | 15 | # Docker 16 | Dockerfile 17 | docker-compose.yml 18 | .dockerignore 19 | 20 | # Environment variables 21 | .env 22 | .env.* 23 | 24 | # IDE specific files 25 | .idea 26 | .vscode 27 | *.swp 28 | *.swo 29 | 30 | # Logs 31 | logs 32 | *.log 33 | 34 | # Test coverage 35 | coverage 36 | .nyc_output ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # MCP Frontend Testing Server 2 | 3 | ## Description 4 | 5 | This MCP server provides tools for frontend testing, including: 6 | 7 | - **Code Analysis**: Analyzes JavaScript/TypeScript code to determine appropriate testing strategies. 8 | - **Test Generation**: Generates unit and component tests for Jest and Cypress. 9 | - **Test Running**: Executes tests using Jest and Cypress and returns results. 10 | - **Component Testing**: Provides a tool specifically for testing React components. 11 | 12 | ## Getting Started 13 | 14 | ### Installation 15 | 16 | 1. **Clone the repository:** 17 | \`git clone <repository-url> mcp-frontend-testing\` 18 | 2. **Navigate to the project directory:** 19 | \`cd mcp-frontend-testing\` 20 | 3. **Install dependencies:** 21 | \`npm install\` 22 | 23 | ### Running the Server 24 | 25 | #### HTTP Transport 26 | 27 | \`\`\`bash 28 | # Build the server 29 | npm run build 30 | 31 | # Start the server with HTTP transport 32 | npm run start:http 33 | \`\`\` 34 | 35 | #### Stdio Transport 36 | 37 | \`\`\`bash 38 | # Build the server 39 | npm run build 40 | 41 | # Start the server with Stdio transport 42 | npm run start:stdio 43 | \`\`\` 44 | 45 | ## Usage 46 | 47 | ### Tools 48 | 49 | - **analyzeCode**: Analyzes code and returns analysis results. 50 | - **Parameters**: 51 | - \`code\` (string, required): The source code to analyze. 52 | - \`language\` (enum, optional): Language of the code (\`javascript\` | \`typescript\` | \`jsx\` | \`tsx\`, default: \`javascript\`). 53 | - **generateTest**: Generates test code based on source code and framework. 54 | - **Parameters**: 55 | - \`code\` (string, required): The source code to generate tests for. 56 | - \`framework\` (enum, required): Testing framework (\`jest\` | \`cypress\`). 57 | - \`type\` (enum, required): Type of test (\`unit\` | \`component\` | \`e2e\`). 58 | - \`language\` (enum, optional): Language of the code (\`javascript\` | \`typescript\` | \`jsx\` | \`tsx\`, default: \`javascript\`). 59 | - \`description\` (string, optional): Description of the test case. 60 | - **runTest**: Runs tests and returns results. 61 | - **Parameters**: 62 | - \`sourceCode\` (string, required): The source code being tested. 63 | - \`testCode\` (string, required): The test code to execute. 64 | - \`framework\` (enum, required): Testing framework (\`jest\` | \`cypress\`). 65 | - \`type\` (enum, required): Type of test (\`unit\` | \`component\` | \`e2e\`). 66 | - \`config\` (record, optional): Configuration object for test execution. 67 | - **testReactComponent**: Runs component tests specifically for React components. 68 | - **Parameters**: 69 | - \`componentCode\` (string, required): The source code of the React component. 70 | - \`testCode\` (string, optional): Test code for the component (auto-generated if not provided). 71 | - \`framework\` (enum, optional): Testing framework (\`jest\` | \`cypress\`, default: \`jest\`). 72 | - \`props\` (record, optional): Props to pass to the component during testing. 73 | - \`autoGenerateTest\` (boolean, optional): Automatically generate test code if not provided (default: \`true\`). 74 | 75 | ### Resources 76 | 77 | - **templates**: Provides test templates. 78 | - **URI**: \`templates://{framework}/{type}\` 79 | - **Parameters**: 80 | - \`framework\` (string, required): Testing framework (\`jest\` | \`cypress\`). 81 | - \`type\` (string, required): Type of template (\`unit\` | \`component\`). 82 | - **docs**: Provides documentation for testing frameworks. 83 | - **URI**: \`docs://{topic}\` 84 | - **Parameters**: 85 | - \`topic\` (string, required): Documentation topic (\`jest\` | \`cypress\` | \`react-testing-library\`). 86 | 87 | ## Deployment 88 | 89 | ### Docker 90 | 91 | Build and run the server using Docker: 92 | 93 | \`\`\`bash 94 | docker build -t mcp-frontend-testing . 95 | docker run -p 3000:3000 mcp-frontend-testing 96 | \`\`\` 97 | 98 | ### Cloud 99 | 100 | Deploy to cloud platforms like AWS Lambda, Google Cloud Run, or Azure Functions for serverless or containerized deployments. 101 | 102 | --- 103 | 104 | **Note**: This server is designed to be used with an MCP client to enable LLMs to perform frontend testing tasks. ``` -------------------------------------------------------------------------------- /templates/jest/unit.txt: -------------------------------------------------------------------------------- ``` 1 | // Jest unit test template 2 | describe('Unit test', () => { 3 | test('should work correctly', () => { 4 | // Arrange 5 | 6 | // Act 7 | 8 | // Assert 9 | expect(true).toBe(true); 10 | }); 11 | }); ``` -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- ```yaml 1 | version: '3.8' 2 | 3 | services: 4 | mcp-frontend-testing: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | image: mcp-frontend-testing 9 | container_name: mcp-frontend-testing 10 | ports: 11 | - "3000:3000" 12 | environment: 13 | - NODE_ENV=production 14 | restart: unless-stopped ``` -------------------------------------------------------------------------------- /templates/cypress/component.txt: -------------------------------------------------------------------------------- ``` 1 | // Cypress component test template 2 | import { mount } from '@cypress/react'; 3 | import Component from './Component'; 4 | 5 | describe('Component', () => { 6 | it('renders correctly', () => { 7 | // Arrange 8 | mount(<Component />); 9 | 10 | // Assert 11 | cy.contains(/example/i).should('be.visible'); 12 | }); 13 | }); ``` -------------------------------------------------------------------------------- /src/transports/stdio.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; 2 | import { createServer } from '../server.js'; 3 | 4 | export async function startStdioServer(): Promise<void> { 5 | const server = createServer(); 6 | const transport = new StdioServerTransport(); 7 | await server.connect(transport); 8 | } ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "esModuleInterop": true, 7 | "strict": true, 8 | "outDir": "dist", 9 | "sourceMap": true, 10 | "declaration": true, 11 | "jsx": "react-jsx", 12 | "lib": ["ES2022", "DOM"], 13 | "skipLibCheck": true 14 | }, 15 | "include": ["src/**/*"], 16 | "exclude": ["node_modules", "dist"] 17 | } ``` -------------------------------------------------------------------------------- /templates/jest/component.txt: -------------------------------------------------------------------------------- ``` 1 | // Jest React component test template 2 | import { render, screen } from '@testing-library/react'; 3 | import userEvent from '@testing-library/user-event'; 4 | import Component from './Component'; 5 | 6 | describe('Component', () => { 7 | test('renders correctly', () => { 8 | // Arrange 9 | render(<Component />); 10 | 11 | // Assert 12 | expect(screen.getByText(/example/i)).toBeInTheDocument(); 13 | }); 14 | }); ``` -------------------------------------------------------------------------------- /src/utils/command-executor.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { exec } from 'child_process'; 2 | import util from 'util'; 3 | 4 | const execPromise = util.promisify(exec); 5 | 6 | // Execute a command and return stdout 7 | export async function executeCommand(command: string, cwd: string): Promise<any> { 8 | return new Promise((resolve, reject) => { 9 | exec(command, { cwd }, (error, stdout, stderr) => { 10 | if (error) { 11 | reject(error); 12 | } 13 | resolve({ stdout, stderr }); 14 | }); 15 | }); 16 | } ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile 1 | # Build stage 2 | FROM node:20-alpine AS build 3 | 4 | # Set working directory 5 | WORKDIR /app 6 | 7 | # Copy package files for dependency installation 8 | COPY package.json package-lock.json ./ 9 | 10 | # Install dependencies 11 | RUN npm ci 12 | 13 | # Copy the rest of the application code 14 | COPY . . 15 | 16 | # Build the TypeScript application 17 | RUN npm run build 18 | 19 | # Production stage 20 | FROM node:20-alpine AS production 21 | 22 | # Set working directory 23 | WORKDIR /app 24 | 25 | # Copy package files 26 | COPY package.json package-lock.json ./ 27 | 28 | # Install only production dependencies 29 | RUN npm ci --production 30 | 31 | # Copy built application from build stage 32 | COPY --from=build /app/dist ./dist 33 | COPY --from=build /app/templates ./templates 34 | 35 | # Expose the HTTP port 36 | EXPOSE 3000 37 | 38 | # Set the default command to run the server with HTTP transport 39 | CMD ["node", "dist/index.js", "--transport=http"] ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { parseArgs } from 'node:util'; 2 | import { startHttpServer } from './transports/http.js'; 3 | import { startStdioServer } from './transports/stdio.js'; 4 | 5 | async function main() { 6 | // Parse command line arguments 7 | const { values } = parseArgs({ 8 | options: { 9 | transport: { 10 | type: 'string', 11 | short: 't', 12 | default: 'stdio' 13 | }, 14 | port: { 15 | type: 'string', 16 | short: 'p', 17 | default: '3000' 18 | } 19 | } 20 | }); 21 | 22 | // Start server with appropriate transport 23 | const transport = values.transport; 24 | if (transport === 'stdio') { 25 | await startStdioServer(); 26 | } else if (transport === 'http') { 27 | const port = parseInt(values.port); 28 | await startHttpServer(port); 29 | console.info(`HTTP server started on port ${port}`); 30 | } else { 31 | console.error(`Unknown transport: ${transport}`); 32 | process.exit(1); 33 | } 34 | } 35 | 36 | main().catch(err => { 37 | console.error('Error starting server:', err); 38 | process.exit(1); 39 | }); ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "mcp-frontend-testing", 3 | "version": "1.0.0", 4 | "description": "MCP server for frontend testing with Jest and Cypress", 5 | "main": "dist/index.js", 6 | "type": "module", 7 | "scripts": { 8 | "build": "tsc", 9 | "start": "node dist/index.js", 10 | "start:http": "node dist/index.js --transport=http", 11 | "start:stdio": "node dist/index.js --transport=stdio", 12 | "dev": "ts-node-esm src/index.ts", 13 | "test": "jest" 14 | }, 15 | "dependencies": { 16 | "@modelcontextprotocol/sdk": "1.10.2", 17 | "@testing-library/jest-dom": "^6.1.5", 18 | "@testing-library/react": "16.3.0", 19 | "@testing-library/user-event": "^14.5.1", 20 | "cors": "^2.8.5", 21 | "cypress": "14.3.2", 22 | "esbuild": "0.25.3", 23 | "express": "5.0.1", 24 | "jest": "^29.7.0", 25 | "jest-environment-jsdom": "^29.7.0", 26 | "react": "19.1.0", 27 | "react-dom": "19.1.0", 28 | "typescript": "5.8.3", 29 | "zod": "3.24.3" 30 | }, 31 | "devDependencies": { 32 | "@types/cors": "^2.8.17", 33 | "@types/express": "5.0.1", 34 | "@types/jest": "^29.5.11", 35 | "@types/node": "22.13.11", 36 | "@types/react": "19.1.2", 37 | "@types/react-dom": "19.1.2", 38 | "ts-node": "^10.9.1" 39 | } 40 | } 41 | ``` -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 2 | 3 | // Import resources 4 | import { registerTemplateResources } from './resources/templates.js'; 5 | import { registerDocResources } from './resources/docs.js'; 6 | 7 | // Import tools 8 | import { registerAnalyzerTool } from './tools/analyzer.js'; 9 | import { registerGeneratorTool } from './tools/generator.js'; 10 | import { registerRunnerTool } from './tools/runner.js'; 11 | import { registerComponentTesterTool } from './tools/component-tester.js'; 12 | 13 | // Import prompts 14 | import { registerPrompts } from './prompts/index.js'; 15 | 16 | export function createServer(): McpServer { 17 | // Create MCP server 18 | const server = new McpServer({ 19 | name: 'Frontend Testing Server', 20 | version: '1.0.0', 21 | description: 'MCP server for testing JavaScript/TypeScript code and React components' 22 | }); 23 | 24 | // Register resources 25 | registerTemplateResources(server); 26 | registerDocResources(server); 27 | 28 | // Register tools 29 | registerAnalyzerTool(server); 30 | registerGeneratorTool(server); 31 | registerRunnerTool(server); 32 | registerComponentTesterTool(server); 33 | 34 | // Register prompts 35 | registerPrompts(server); 36 | 37 | return server; 38 | } ``` -------------------------------------------------------------------------------- /src/prompts/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 2 | 3 | export function registerPrompts(server: McpServer): void { 4 | // Simplified implementation to avoid TypeScript errors 5 | // These prompts will be used by the LLM to generate test code 6 | 7 | // Unit test prompt 8 | server.prompt( 9 | 'create-unit-test', 10 | 'Create a unit test for the given code', 11 | () => ({ 12 | messages: [{ 13 | role: 'user', 14 | content: { 15 | type: 'text', 16 | text: 'Please create a unit test for the provided code.', 17 | }, 18 | }], 19 | }) 20 | ); 21 | 22 | // Component test prompt 23 | server.prompt( 24 | 'create-component-test', 25 | 'Create a test for a React component', 26 | () => ({ 27 | messages: [{ 28 | role: 'user', 29 | content: { 30 | type: 'text', 31 | text: 'Please create a test for this React component. Focus on testing the component\'s functionality, props, and user interactions.', 32 | }, 33 | }], 34 | }) 35 | ); 36 | 37 | // Fix failing test prompt 38 | server.prompt( 39 | 'fix-failing-test', 40 | 'Fix a failing test', 41 | () => ({ 42 | messages: [{ 43 | role: 'user', 44 | content: { 45 | type: 'text', 46 | text: 'I have a test that\'s failing. Please help me fix it by explaining what\'s wrong and providing a fixed version of the test.', 47 | }, 48 | }], 49 | }) 50 | ); 51 | } ``` -------------------------------------------------------------------------------- /src/transports/http.ts: -------------------------------------------------------------------------------- ```typescript 1 | import express from 'express'; 2 | import cors from 'cors'; 3 | import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; 4 | import { createServer } from '../server.js'; 5 | 6 | // Map to store active transports 7 | const transports = new Map<string, SSEServerTransport>(); 8 | 9 | export async function startHttpServer(port: number): Promise<void> { 10 | const app = express(); 11 | app.use(cors()); 12 | app.use(express.json()); 13 | 14 | // Health check endpoint 15 | app.get('/health', (req, res) => { 16 | res.json({ status: 'ok' }); 17 | }); 18 | 19 | // SSE endpoint 20 | app.get('/sse/:sessionId', async (req, res) => { 21 | const { sessionId } = req.params; 22 | res.setHeader('Content-Type', 'text/event-stream'); 23 | res.setHeader('Cache-Control', 'no-cache'); 24 | res.setHeader('Connection', 'keep-alive'); 25 | 26 | // Create server and transport 27 | const server = createServer(); 28 | const transport = new SSEServerTransport('/messages', res); 29 | transports.set(sessionId, transport); 30 | 31 | // Connect the transport to the server 32 | await server.connect(transport); 33 | 34 | // Remove transport when connection closes 35 | req.on('close', () => { 36 | transports.delete(sessionId); 37 | }); 38 | }); 39 | 40 | // Message endpoint 41 | app.post('/messages/:sessionId', async (req, res) => { 42 | const { sessionId } = req.params; 43 | const transport = transports.get(sessionId); 44 | 45 | if (!transport) { 46 | res.status(404).json({ error: 'Session not found' }); 47 | return; 48 | } 49 | 50 | await transport.handlePostMessage(req, res); 51 | }); 52 | 53 | // Start server 54 | return new Promise((resolve) => { 55 | app.listen(port, () => { 56 | resolve(); 57 | }); 58 | }); 59 | } ``` -------------------------------------------------------------------------------- /src/tools/runner.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 2 | import { z } from 'zod'; 3 | import { setupTestEnvironment, executeTest, cleanupTestEnvironment } from '../utils/test-environment.js'; 4 | import crypto from 'crypto'; 5 | import path from 'path'; 6 | import os from 'os'; 7 | import { executeCommand } from '../utils/command-executor.js'; 8 | 9 | export { executeCommand }; 10 | 11 | export function registerRunnerTool(server: McpServer): void { 12 | server.tool( 13 | 'runTest', 14 | { 15 | sourceCode: z.string(), 16 | testCode: z.string(), 17 | framework: z.enum(['jest', 'cypress']), 18 | type: z.enum(['unit', 'component', 'e2e']), 19 | config: z.record(z.any()).optional() 20 | }, 21 | async ({ sourceCode, testCode, framework, type, config }) => { 22 | try { 23 | // Create temporary test environment 24 | const testId = crypto.randomUUID(); 25 | const testDir = path.join(os.tmpdir(), 'mcp-test-server', testId); 26 | 27 | // Set up files 28 | await setupTestEnvironment(testDir, sourceCode, testCode, framework, type, config); 29 | 30 | // Install dependencies 31 | await executeCommand('npm install', testDir); 32 | 33 | try { 34 | // Run the test 35 | const results = await executeTest(testDir, framework, type); 36 | 37 | return { 38 | content: [{ 39 | type: 'text', 40 | text: JSON.stringify(results, null, 2) 41 | }] 42 | }; 43 | } finally { 44 | // Clean up 45 | await cleanupTestEnvironment(testDir); 46 | } 47 | } catch (error) { 48 | return { 49 | isError: true, 50 | content: [{ 51 | type: 'text', 52 | text: `Error running test: ${String(error)}` 53 | }] 54 | }; 55 | } 56 | } 57 | ); 58 | } ``` -------------------------------------------------------------------------------- /src/tools/component-tester.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 2 | import { z } from 'zod'; 3 | import { setupTestEnvironment, executeTest } from '../utils/test-environment.js'; 4 | import { generateTestCode } from './generator.js'; 5 | import { executeCommand } from '../utils/command-executor.js'; 6 | import crypto from 'crypto'; 7 | import path from 'path'; 8 | import os from 'os'; 9 | import { rm } from 'fs/promises'; 10 | 11 | export function registerComponentTesterTool(server: McpServer): void { 12 | server.tool( 13 | 'testReactComponent', 14 | { 15 | componentCode: z.string(), 16 | testCode: z.string().optional(), 17 | framework: z.enum(['jest', 'cypress']).default('jest'), 18 | props: z.record(z.any()).optional(), 19 | autoGenerateTest: z.boolean().default(true) 20 | }, 21 | async ({ componentCode, testCode, framework, props, autoGenerateTest }) => { 22 | try { 23 | // Determine language based on code 24 | const language = componentCode.includes('tsx') || componentCode.includes(':') ? 25 | 'tsx' : (componentCode.includes('jsx') ? 'jsx' : 'javascript'); 26 | 27 | // Generate test if not provided 28 | let finalTestCode = testCode; 29 | if (!finalTestCode && autoGenerateTest) { 30 | finalTestCode = await generateTestCode( 31 | componentCode, 32 | framework, 33 | 'component', 34 | language 35 | ); 36 | } 37 | 38 | if (!finalTestCode) { 39 | throw new Error('No test code provided or generated'); 40 | } 41 | 42 | // Create temporary test environment 43 | const testId = crypto.randomUUID(); 44 | const testDir = path.join(os.tmpdir(), 'mcp-test-server', testId); 45 | 46 | // Set up files for component testing 47 | await setupTestEnvironment(testDir, componentCode, finalTestCode, framework, 'component', props); 48 | 49 | // Install dependencies 50 | await executeCommand('npm install', testDir); 51 | 52 | try { 53 | // Run the test 54 | const results = await executeTest(testDir, framework, 'component'); 55 | 56 | return { 57 | content: [{ 58 | type: 'text', 59 | text: JSON.stringify(results, null, 2), 60 | }], 61 | }; 62 | } finally { 63 | // Clean up 64 | try { 65 | await rm(testDir, { recursive: true, force: true }); 66 | } catch (error) { 67 | console.error('Error cleaning up test environment:', error); 68 | } 69 | } 70 | } catch (error) { 71 | return { 72 | isError: true, 73 | content: [{ 74 | type: 'text', 75 | text: `Error testing component: ${String(error)}`, 76 | }], 77 | }; 78 | } 79 | } 80 | ); 81 | } ``` -------------------------------------------------------------------------------- /src/resources/templates.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'; 2 | import { readFile } from 'fs/promises'; 3 | import path from 'path'; 4 | import { fileURLToPath } from 'url'; 5 | 6 | // Get directory name 7 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 8 | const templatesDir = path.join(__dirname, '../../templates'); 9 | 10 | export function registerTemplateResources(server: McpServer): void { 11 | // Register template resources 12 | server.resource( 13 | 'templates', 14 | new ResourceTemplate('templates://{framework}/{type}', { 15 | list: async () => { 16 | return { 17 | resources: [ 18 | { 19 | uri: 'templates://jest/component', 20 | name: 'Jest Component Test Template', 21 | mimeType: 'text/plain', 22 | description: 'Template for Jest React component tests' 23 | }, 24 | { 25 | uri: 'templates://jest/unit', 26 | name: 'Jest Unit Test Template', 27 | mimeType: 'text/plain', 28 | description: 'Template for Jest unit tests' 29 | }, 30 | { 31 | uri: 'templates://cypress/component', 32 | name: 'Cypress Component Test Template', 33 | mimeType: 'text/plain', 34 | description: 'Template for Cypress component tests' 35 | } 36 | ] 37 | }; 38 | } 39 | }), 40 | async (uri, { framework, type }) => { 41 | try { 42 | // Try to load the template file 43 | const filePath = path.join(templatesDir, framework as string, `${type as string}.txt`); 44 | const content = await readFile(filePath, 'utf-8'); 45 | 46 | return { 47 | contents: [{ 48 | uri: uri.href, 49 | text: content 50 | }] 51 | }; 52 | } catch (error) { 53 | // Fallback to hardcoded templates if file not found 54 | const templates: Record<string, Record<string, string>> = { 55 | jest: { 56 | unit: ` 57 | // Jest unit test template 58 | describe('Unit test', () => { 59 | test('should work correctly', () => { 60 | // Arrange 61 | 62 | // Act 63 | 64 | // Assert 65 | expect(true).toBe(true); 66 | }); 67 | });`, 68 | component: ` 69 | // Jest React component test template 70 | import { render, screen } from '@testing-library/react'; 71 | import userEvent from '@testing-library/user-event'; 72 | import Component from './Component'; 73 | 74 | describe('Component', () => { 75 | test('renders correctly', () => { 76 | // Arrange 77 | render(<Component />); 78 | 79 | // Assert 80 | expect(screen.getByText(/example/i)).toBeInTheDocument(); 81 | }); 82 | });`, 83 | }, 84 | cypress: { 85 | component: ` 86 | // Cypress component test template 87 | import Component from './Component'; 88 | 89 | describe('Component', () => { 90 | it('renders correctly', () => { 91 | // Arrange 92 | cy.mount(<Component />); 93 | 94 | // Assert 95 | cy.contains(/example/i).should('be.visible'); 96 | }); 97 | });`, 98 | } 99 | }; 100 | 101 | const frameworkTemplates = templates[framework as keyof typeof templates]; 102 | const templateContent = frameworkTemplates && (type as string) in frameworkTemplates 103 | ? frameworkTemplates[type as string] 104 | : "Template not found"; 105 | 106 | return { 107 | contents: [{ 108 | uri: uri.href, 109 | text: templateContent 110 | }] 111 | }; 112 | } 113 | } 114 | ); 115 | 116 | // Template index 117 | server.resource( 118 | 'templates-index', 119 | 'templates://', 120 | async (uri) => { 121 | return { 122 | contents: [{ 123 | uri: uri.href, 124 | text: JSON.stringify({ 125 | frameworks: [ 126 | 'jest', 127 | 'cypress' 128 | ], 129 | types: [ 130 | 'unit', 131 | 'component' 132 | ] 133 | }, null, 2) 134 | }] 135 | }; 136 | } 137 | ); 138 | } ``` -------------------------------------------------------------------------------- /src/utils/test-config.ts: -------------------------------------------------------------------------------- ```typescript 1 | // Default Jest configuration for different test types 2 | export function getDefaultJestConfig( 3 | type: 'unit' | 'component' | 'e2e', 4 | isTypeScript: boolean 5 | ): string { 6 | const baseConfig: any = { 7 | transform: {}, 8 | testEnvironment: type === 'component' ? 'jsdom' : 'node', 9 | setupFilesAfterEnv: type === 'component' ? ['<rootDir>/setupTests.js'] : [], 10 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 11 | moduleDirectories: ['node_modules', '<rootDir>'], 12 | }; 13 | 14 | if (isTypeScript) { 15 | baseConfig.transform = {'\\.(ts|tsx)$': 'ts-jest'}; 16 | baseConfig.testRegex = '(/__tests__/.*|\\.(test|spec))\\.(ts|tsx)$'; 17 | baseConfig.moduleFileExtensions = ['ts', 'tsx', 'js', 'jsx', 'json', 'node']; 18 | } else { 19 | baseConfig.transform = {'\\.(js|jsx)$': 'babel-jest'}; 20 | baseConfig.testRegex = '(/__tests__/.*|\\.(test|spec))\\.(js|jsx)$'; 21 | baseConfig.moduleFileExtensions = ['js', 'jsx', 'json', 'node']; 22 | } 23 | 24 | return `export default ${JSON.stringify(baseConfig, null, 2)};`; 25 | } 26 | 27 | // Default Cypress configuration for different test types 28 | export function getDefaultCypressConfig( 29 | type: 'unit' | 'component' | 'e2e', 30 | isTypeScript: boolean 31 | ): string { 32 | let config; 33 | 34 | if (type === 'component') { 35 | config = { 36 | component: { 37 | devServer: { 38 | framework: 'react', 39 | bundler: 'vite', 40 | }, 41 | specPattern: isTypeScript ? '**/*.cy.{js,jsx,ts,tsx}' : '**/*.cy.{js,jsx}', 42 | }, 43 | }; 44 | } else { 45 | config = { 46 | e2e: { 47 | setupNodeEvents(on: any, config: any) { 48 | return config; 49 | }, 50 | specPattern: isTypeScript ? 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}' : 'cypress/e2e/**/*.cy.{js,jsx}', 51 | }, 52 | }; 53 | } 54 | 55 | return `import { defineConfig } from 'cypress' 56 | 57 | export default defineConfig(${JSON.stringify(config, null, 2)})`; 58 | } 59 | 60 | // Generate component wrappers for testing with props 61 | export function getComponentTestWrappers( 62 | componentName: string, 63 | isTypeScript: boolean 64 | ): { wrapperCode: string, importStatement: string } { 65 | const importStatement = isTypeScript 66 | ? `import React from 'react';` 67 | : `import React from 'react';`; 68 | 69 | const wrapperCode = isTypeScript 70 | ? `export const TestWrapper: React.FC = () => { 71 | return <${componentName} {...testProps} />; 72 | }; 73 | 74 | export default TestWrapper;` 75 | : `export const TestWrapper = () => { 76 | return <${componentName} {...testProps} />; 77 | }; 78 | 79 | export default TestWrapper;`; 80 | 81 | return { wrapperCode, importStatement }; 82 | } 83 | 84 | // Get dependencies for package.json based on test configuration 85 | export function getDependencies( 86 | framework: string, 87 | type: string, 88 | isTypeScript: boolean, 89 | isReact: boolean 90 | ): Record<string, string> { 91 | const dependencies: Record<string, string> = { 92 | // Common dependencies 93 | "jest": "^29.7.0", 94 | }; 95 | 96 | // TypeScript dependencies 97 | if (isTypeScript) { 98 | dependencies["typescript"] = "^5.3.3"; 99 | dependencies["ts-jest"] = "^29.1.1"; 100 | dependencies["@types/jest"] = "^29.5.11"; 101 | } 102 | 103 | // React dependencies 104 | if (isReact) { 105 | dependencies["react"] = "^18.2.0"; 106 | dependencies["react-dom"] = "^18.2.0"; 107 | 108 | if (isTypeScript) { 109 | dependencies["@types/react"] = "^18.2.42"; 110 | dependencies["@types/react-dom"] = "^18.2.17"; 111 | } 112 | } 113 | 114 | // Framework-specific dependencies 115 | if (framework === 'jest') { 116 | dependencies["jest-environment-jsdom"] = "^29.7.0"; 117 | 118 | if (type === 'component' || isReact) { 119 | dependencies["@testing-library/react"] = "^14.1.2"; 120 | dependencies["@testing-library/jest-dom"] = "^6.1.5"; 121 | dependencies["@testing-library/user-event"] = "^14.5.1"; 122 | } 123 | } else if (framework === 'cypress') { 124 | dependencies["cypress"] = "^13.6.1"; 125 | 126 | if (type === 'component' || isReact) { 127 | dependencies["@cypress/react"] = "^7.0.3"; 128 | } 129 | } 130 | 131 | // Build tools 132 | dependencies["esbuild"] = "^0.19.9"; 133 | 134 | return dependencies; 135 | } ``` -------------------------------------------------------------------------------- /src/resources/docs.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'; 2 | 3 | export function registerDocResources(server: McpServer): void { 4 | server.resource( 5 | 'docs', 6 | new ResourceTemplate('docs://{topic}', { 7 | list: async () => { 8 | return { 9 | resources: [ 10 | { 11 | uri: 'docs://jest', 12 | name: 'Jest Documentation', 13 | mimeType: 'text/plain', 14 | description: 'Documentation for Jest testing framework' 15 | }, 16 | { 17 | uri: 'docs://cypress', 18 | name: 'Cypress Documentation', 19 | mimeType: 'text/plain', 20 | description: 'Documentation for Cypress testing framework' 21 | }, 22 | { 23 | uri: 'docs://react-testing-library', 24 | name: 'React Testing Library Documentation', 25 | mimeType: 'text/plain', 26 | description: 'Documentation for React Testing Library' 27 | } 28 | ] 29 | }; 30 | } 31 | }), 32 | async (uri, { topic }) => { 33 | const docs: Record<string, string> = { 34 | jest: ` 35 | # Jest Documentation 36 | 37 | Jest is a JavaScript testing framework designed to ensure correctness of any JavaScript codebase. It allows you to write tests with an approachable, familiar and feature-rich API that gives you results quickly. 38 | 39 | ## Key Features 40 | 41 | - Zero config for most JavaScript projects 42 | - Snapshots for tracking large objects 43 | - Isolated test files to avoid sharing state 44 | - Powerful mocking library 45 | 46 | ## Basic Example 47 | 48 | \`\`\`javascript 49 | // sum.js 50 | function sum(a, b) { 51 | return a + b; 52 | } 53 | module.exports = sum; 54 | 55 | // sum.test.js 56 | const sum = require('./sum'); 57 | test('adds 1 + 2 to equal 3', () => { 58 | expect(sum(1, 2)).toBe(3); 59 | }); 60 | \`\`\` 61 | `, 62 | cypress: ` 63 | # Cypress Documentation 64 | 65 | Cypress is a next generation front end testing tool built for the modern web. It enables you to write faster, easier and more reliable tests. 66 | 67 | ## Key Features 68 | 69 | - Time Travel: Cypress takes snapshots as your tests run 70 | - Debuggability: Debug directly from familiar tools like Chrome DevTools 71 | - Automatic Waiting: Cypress automatically waits for commands and assertions 72 | - Real-time Reloads: Test is automatically reloaded when you make changes 73 | 74 | ## Basic Example 75 | 76 | \`\`\`javascript 77 | describe('My First Test', () => { 78 | it('clicks the link "type"', () => { 79 | cy.visit('https://example.cypress.io') 80 | cy.contains('type').click() 81 | cy.url().should('include', '/commands/actions') 82 | }) 83 | }) 84 | \`\`\` 85 | `, 86 | 'react-testing-library': ` 87 | # React Testing Library Documentation 88 | 89 | React Testing Library is a very light-weight solution for testing React components. It provides light utility functions on top of react-dom and react-dom/test-utils, encouraging better testing practices. 90 | 91 | ## Key Features 92 | 93 | - Works with actual DOM nodes 94 | - Focuses on testing from the user perspective 95 | - Encourages accessibility best practices 96 | - Simple and intuitive API 97 | 98 | ## Basic Example 99 | 100 | \`\`\`javascript 101 | import { render, screen } from '@testing-library/react'; 102 | import userEvent from '@testing-library/user-event'; 103 | import '@testing-library/jest-dom'; 104 | import Component from './Component.js'; 105 | 106 | test('loads and displays greeting', async () => { 107 | // Arrange 108 | render(<Component />) 109 | 110 | // Act 111 | await userEvent.click(screen.getByText('Load Greeting')) 112 | 113 | // Assert 114 | expect(screen.getByRole('heading')).toHaveTextContent('hello there') 115 | }) 116 | \`\`\` 117 | ` 118 | }; 119 | 120 | return { 121 | contents: [{ 122 | uri: uri.href, 123 | text: docs[topic as keyof typeof docs] || 'Documentation not found' 124 | }] 125 | }; 126 | } 127 | ); 128 | 129 | // Documentation index 130 | server.resource( 131 | 'docs-index', 132 | 'docs://', 133 | async (uri) => { 134 | return { 135 | contents: [{ 136 | uri: uri.href, 137 | text: JSON.stringify({ 138 | topics: [ 139 | 'jest', 140 | 'cypress', 141 | 'react-testing-library' 142 | ] 143 | }, null, 2) 144 | }] 145 | }; 146 | } 147 | ); 148 | } 149 | ``` -------------------------------------------------------------------------------- /src/tools/analyzer.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 2 | import { z } from 'zod'; 3 | 4 | export function registerAnalyzerTool(server: McpServer): void { 5 | server.tool( 6 | 'analyzeCode', 7 | { 8 | code: z.string(), 9 | language: z.enum(['javascript', 'typescript', 'jsx', 'tsx']).default('javascript') 10 | }, 11 | async ({ code, language }) => { 12 | try { 13 | // Analyze code to determine what kind of tests would be appropriate 14 | const analysis = performCodeAnalysis(code, language); 15 | return { 16 | content: [{ 17 | type: 'text', 18 | text: JSON.stringify(analysis, null, 2), 19 | }], 20 | }; 21 | } catch (error) { 22 | return { 23 | isError: true, 24 | content: [{ 25 | type: 'text', 26 | text: `Error analyzing code: ${String(error)}`, 27 | }], 28 | }; 29 | } 30 | } 31 | ); 32 | } 33 | 34 | // Helper function to analyze code 35 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 36 | export function performCodeAnalysis(code: string, language: string): any { 37 | const analysisResult: any = { 38 | codeType: {}, 39 | complexity: {}, 40 | recommendations: {} 41 | }; 42 | 43 | try { 44 | // Determine if the code is a React component 45 | const isReactComponent = code.includes('import React') || 46 | code.includes('from "react"') || 47 | code.includes("from 'react'") || 48 | code.includes('extends Component') || 49 | code.includes('React.Component') || 50 | ((code.includes('export') && code.includes('return')) && 51 | (code.includes('JSX.') || code.includes('<div') || code.includes('<>'))); 52 | 53 | // Check if it's a function or class 54 | const isClass = code.includes('class ') && code.includes('extends '); 55 | const isFunction = code.includes('function ') || code.includes('=>'); 56 | 57 | // Check if it uses hooks 58 | const usesHooks = code.includes('useState') || 59 | code.includes('useEffect') || 60 | code.includes('useContext') || 61 | code.includes('useReducer') || 62 | code.includes('useCallback') || 63 | code.includes('useMemo'); 64 | 65 | // Count imports to determine complexity 66 | const importMatches = code.match(/import .+ from .+/g); 67 | const imports = importMatches ? importMatches.length : 0; 68 | 69 | // Look for event handlers 70 | const hasEvents = code.includes('onClick') || 71 | code.includes('onChange') || 72 | code.includes('onSubmit') || 73 | code.includes('addEventListener'); 74 | 75 | // Look for async operations 76 | const hasAsync = code.includes('async ') || 77 | code.includes('await ') || 78 | code.includes('Promise') || 79 | code.includes('.then(') || 80 | code.includes('fetch('); 81 | 82 | const recommendedTestTypes: string[] = []; 83 | if (isReactComponent) { 84 | recommendedTestTypes.push('component'); 85 | if (hasEvents || hasAsync) { 86 | recommendedTestTypes.push('e2e'); 87 | } else { 88 | recommendedTestTypes.push('unit'); 89 | } 90 | } else { 91 | recommendedTestTypes.push('unit'); 92 | } 93 | 94 | // Recommend testing frameworks 95 | const recommendedFrameworks: string[] = []; 96 | if (isReactComponent) { 97 | recommendedFrameworks.push('jest'); 98 | if (hasEvents) { 99 | recommendedFrameworks.push('cypress'); 100 | } else { 101 | recommendedFrameworks.push('jest'); 102 | } 103 | } else { 104 | recommendedFrameworks.push('jest'); 105 | } 106 | 107 | analysisResult.codeType = { 108 | isReactComponent, 109 | isClass, 110 | isFunction, 111 | usesHooks, 112 | }; 113 | analysisResult.complexity = { 114 | imports, 115 | hasEvents, 116 | hasAsync 117 | }; 118 | analysisResult.recommendations = { 119 | testTypes: recommendedTestTypes, 120 | frameworks: recommendedFrameworks, 121 | priority: hasAsync ? 'high' : 'medium' 122 | }; 123 | } catch (error: any) { 124 | console.error(`Error during code analysis: ${error.message}`); 125 | } 126 | 127 | return analysisResult; 128 | } ``` -------------------------------------------------------------------------------- /src/tools/generator.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 2 | import { z } from 'zod'; 3 | import { performCodeAnalysis } from './analyzer.js'; 4 | 5 | export function registerGeneratorTool(server: McpServer): void { 6 | server.tool( 7 | 'generateTest', 8 | { 9 | code: z.string(), 10 | framework: z.enum(['jest', 'cypress']), 11 | type: z.enum(['unit', 'component', 'e2e']), 12 | language: z.enum(['javascript', 'typescript', 'jsx', 'tsx']).default('javascript'), 13 | description: z.string().optional() 14 | }, 15 | async ({ code, framework, type, language, description }) => { 16 | try { 17 | const testCode = generateTestCode(code, framework, type, language, description); 18 | return { 19 | content: [{ 20 | type: 'text', 21 | text: testCode, 22 | }], 23 | }; 24 | } catch (error) { 25 | return { 26 | isError: true, 27 | content: [{ 28 | type: 'text', 29 | text: `Error generating test: ${String(error)}`, 30 | }], 31 | }; 32 | } 33 | } 34 | ); 35 | } 36 | 37 | // Helper function to generate test code based on source code and parameters 38 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 39 | export function generateTestCode( 40 | sourceCode: string, 41 | framework: string, 42 | testType: string, 43 | language: string, 44 | description?: string 45 | ): string { 46 | // Analyze the code to better understand its structure 47 | const analysis = performCodeAnalysis(sourceCode, language); 48 | 49 | // Extract component or function name 50 | const nameMatch = sourceCode.match(/(?:function|class|const)\s+(\w+)/); 51 | const name = nameMatch ? nameMatch[1] : 'Component'; 52 | 53 | // Generate appropriate import statements based on framework and test type 54 | let imports = ''; 55 | let testCode = ''; 56 | 57 | if (framework === 'jest') { 58 | if (testType === 'unit') { 59 | imports = `// Import the module to test\n${language.includes('typescript') ? `import { ${name} } from './${name}';` : `const ${name} = require('./${name}');`}`; 60 | testCode = `describe('${name}', () => { 61 | test('${description || 'should work correctly'}', () => { 62 | // Arrange 63 | ${(analysis as any)?.codeType?.isFunction ? ` 64 | // Example test input 65 | const input = 'test'; 66 | 67 | // Act 68 | const result = ${name}(input); 69 | 70 | // Assert 71 | expect(result).toBeDefined();` : ` 72 | // Setup any required state 73 | 74 | // Act - perform the action 75 | 76 | // Assert - check the result 77 | expect(true).toBe(true);`} 78 | }); 79 | ${(analysis as any)?.complexity?.hasAsync ? ` 80 | test('handles async operations', async () => { 81 | // Arrange 82 | 83 | // Act 84 | const result = await ${name}(); 85 | 86 | // Assert 87 | expect(result).toBeDefined(); 88 | });` : ''} 89 | });`; 90 | } else if (testType === 'component') { 91 | imports = `import { render, screen${(analysis as any)?.complexity?.hasEvents ? ', fireEvent' : ''} } from '@testing-library/react';\n${(analysis as any)?.complexity?.hasEvents ? `import userEvent from '@testing-library/user-event';` : ''}\n${language.includes('typescript') ? `import { ${name} } from './${name}';` : `import { default as ${name} } from './${name}';`}`; 92 | testCode = `describe('${name}', () => { 93 | test('renders correctly', () => { 94 | // Arrange 95 | render(<${name} />); 96 | 97 | // Assert 98 | expect(screen.getByText(/content/i)).toBeInTheDocument(); 99 | }); 100 | ${(analysis as any).complexity?.hasEvents ? ` 101 | test('handles user interaction', async () => { 102 | // Arrange 103 | render(<${name} />); 104 | 105 | // Act 106 | await userEvent.click(screen.getByRole('button')); 107 | 108 | // Assert 109 | expect(screen.getByText(/result/i)).toBeInTheDocument(); 110 | });` : ''} 111 | ${(analysis as any)?.complexity?.hasAsync ? ` 112 | test('loads data asynchronously', async () => { 113 | // Arrange 114 | render(<${name} />); 115 | 116 | // Act - wait for async operation 117 | await screen.findByText(/loaded/i); 118 | 119 | // Assert 120 | expect(screen.getByText(/loaded/i)).toBeInTheDocument(); 121 | });` : ''} 122 | });`; 123 | } 124 | } else if (framework === 'cypress') { 125 | if (testType === 'component') { 126 | imports = `${language.includes('typescript') ? `import { ${name} } from './${name}';` : `import Component from './Component';`}`; 127 | testCode = `describe('${name}', () => { 128 | it('renders correctly', () => { 129 | // Arrange 130 | cy.mount(<${name} />); 131 | 132 | // Assert 133 | cy.contains(/content/i).should('be.visible'); 134 | }); 135 | ${(analysis as any)?.complexity?.hasEvents ? ` 136 | it('handles user interaction', () => { 137 | // Arrange 138 | cy.mount(<${name} />); 139 | 140 | // Act 141 | cy.get('button').click(); 142 | 143 | // Assert 144 | cy.contains(/result/i).should('be.visible'); 145 | });` : ''} 146 | ${(analysis as any)?.complexity?.hasAsync ? ` 147 | it('loads data asynchronously', () => { 148 | // Arrange 149 | cy.mount(<${name} />); 150 | 151 | // Assert - wait for async operation 152 | cy.contains(/loaded/i, { timeout: 10000 }).should('be.visible'); 153 | });` : ''} 154 | });`; 155 | } else if (testType === 'e2e') { 156 | imports = '// No imports needed for Cypress E2E tests'; 157 | testCode = `describe('${name} E2E Test', () => { 158 | beforeEach(() => { 159 | // Visit the page containing the component 160 | cy.visit('/'); 161 | }); 162 | 163 | it('${description || 'works correctly'}', () => { 164 | // Assert the component is rendered 165 | cy.contains(/content/i).should('be.visible'); 166 | ${(analysis as any)?.complexity?.hasEvents ? ` 167 | // Act - interact with the component 168 | cy.get('button').click(); 169 | 170 | // Assert the interaction worked 171 | cy.contains(/result/i).should('be.visible');` : ''} 172 | ${(analysis as any)?.complexity?.hasAsync ? ` 173 | // Assert async data loads correctly 174 | cy.contains(/loaded/i, { timeout: 10000 }).should('be.visible');` : ''} 175 | }); 176 | });`; 177 | } 178 | } 179 | 180 | // Combine imports and test code 181 | return `${imports}\n${testCode}`; 182 | } ``` -------------------------------------------------------------------------------- /src/utils/test-environment.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { mkdir, writeFile, rm } from 'fs/promises'; 2 | import path from 'path'; 3 | import { exec } from 'child_process'; 4 | import util from 'util'; 5 | import { getDefaultJestConfig, getDefaultCypressConfig, getComponentTestWrappers, getDependencies } from './test-config.js'; 6 | import { executeCommand } from './command-executor.js'; 7 | 8 | const execPromise = util.promisify(exec); 9 | 10 | // Set up a test environment with all necessary files 11 | export async function setupTestEnvironment( 12 | testDir: string, 13 | sourceCode: string, 14 | testCode: string, 15 | framework: 'jest' | 'cypress', 16 | type: 'unit' | 'component' | 'e2e', 17 | config?: Record<string, any> 18 | ): Promise<void> { 19 | // Create directory structure 20 | await mkdir(testDir, { recursive: true }); 21 | 22 | // Determine file extensions 23 | const isTypeScript = sourceCode.includes('typescript') || 24 | sourceCode.includes('tsx') || 25 | sourceCode.includes(':') || 26 | sourceCode.includes('interface'); 27 | const isReact = sourceCode.includes('React') || 28 | sourceCode.includes('react') || 29 | sourceCode.includes('JSX') || 30 | sourceCode.includes('<div') || 31 | sourceCode.includes('</'); 32 | 33 | const sourceExt = isTypeScript 34 | ? (isReact ? '.tsx' : '.ts') 35 | : (isReact ? '.jsx' : '.js'); 36 | 37 | const testExt = isTypeScript 38 | ? (framework === 'jest' ? '.test.tsx' : '.cy.tsx') 39 | : (framework === 'jest' ? '.test.jsx' : '.cy.jsx'); 40 | 41 | // Extract component or function name 42 | const nameMatch = sourceCode.match(/(?:function|class|const)\s+(\w+)/); 43 | const name = nameMatch ? nameMatch[1] : 'Component'; 44 | 45 | // Write source file 46 | await writeFile(path.join(testDir, `${name}${sourceExt}`), sourceCode); 47 | 48 | // Write test file 49 | await writeFile(path.join(testDir, `${name}${testExt}`), testCode); 50 | 51 | // Write configuration files 52 | if (framework === 'jest') { 53 | await writeFile( 54 | path.join(testDir, 'jest.config.js'), 55 | config?.jestConfig || getDefaultJestConfig(type, isTypeScript) as string 56 | ); 57 | 58 | // Setup for React testing 59 | if (isReact) { 60 | await writeFile( 61 | path.join(testDir, 'setupTests.js'), 62 | `import '@testing-library/jest-dom';` 63 | ); 64 | } 65 | } else if (framework === 'cypress') { 66 | await writeFile( 67 | path.join(testDir, 'cypress.config.js'), 68 | config?.cypressConfig || getDefaultCypressConfig(type, isTypeScript) as string 69 | ); 70 | 71 | // Create cypress directory structure for e2e tests 72 | if (type === 'e2e') { 73 | await mkdir(path.join(testDir, 'cypress', 'e2e'), { recursive: true }); 74 | await writeFile( 75 | path.join(testDir, 'cypress', 'e2e', `${name}.cy.js`), 76 | testCode 77 | ); 78 | } 79 | 80 | // Setup for component testing 81 | if (type === 'component') { 82 | await mkdir(path.join(testDir, 'cypress', 'support'), { recursive: true }); 83 | await writeFile( 84 | path.join(testDir, 'cypress', 'support', 'component.js'), 85 | `import { mount } from 'cypress/react18' 86 | import './commands' 87 | Cypress.Commands.add('mount', mount)` 88 | ); 89 | 90 | await writeFile( 91 | path.join(testDir, 'cypress', 'support', 'commands.js'), 92 | `// Custom commands go here` 93 | ); 94 | } 95 | } 96 | 97 | // Set up package.json 98 | await writeFile( 99 | path.join(testDir, 'package.json'), 100 | JSON.stringify({ 101 | name: 'mcp-test', 102 | version: '1.0.0', 103 | type: 'module', 104 | dependencies: getDependencies(framework, type, isTypeScript, isReact), 105 | scripts: { 106 | test: framework === 'jest' ? 'jest' : 'cypress run' 107 | } 108 | }, null, 2) 109 | ); 110 | 111 | // Create a basic index.html file for e2e tests 112 | if (framework === 'cypress' && type === 'e2e') { 113 | await writeFile( 114 | path.join(testDir, 'index.html'), 115 | `<!DOCTYPE html> 116 | <html> 117 | <head> 118 | <title>Test Page</title> 119 | </head> 120 | <body> 121 | <div id="root"></div> 122 | <script type="module" src="./index.js"></script> 123 | </body> 124 | </html>` 125 | ); 126 | 127 | await writeFile( 128 | path.join(testDir, 'index.js'), 129 | `import React from 'react'; 130 | import ReactDOM from 'react-dom/client'; 131 | import ${name} from './${name}'; 132 | 133 | ReactDOM.createRoot(document.getElementById('root')).render( 134 | <React.StrictMode> 135 | <${name} /> 136 | </React.StrictMode> 137 | );` 138 | ); 139 | } 140 | } 141 | 142 | // Set up environment specifically for component testing 143 | export async function setupComponentTestEnvironment( 144 | testDir: string, 145 | componentCode: string, 146 | testCode: string, 147 | framework: 'jest' | 'cypress', 148 | props?: Record<string, any> 149 | ): Promise<void> { 150 | // Determine if TypeScript 151 | const isTypeScript = componentCode.includes('typescript') || 152 | componentCode.includes('tsx') || 153 | componentCode.includes(':'); 154 | 155 | // Create directory structure 156 | await mkdir(testDir, { recursive: true }); 157 | 158 | // Extract component name 159 | const nameMatch = componentCode.match(/(?:function|class|const)\s+(\w+)/); 160 | const name = nameMatch ? nameMatch[1] : 'Component'; 161 | 162 | // Determine file extensions 163 | const sourceExt = isTypeScript ? '.tsx' : '.jsx'; 164 | const testExt = isTypeScript 165 | ? (framework === 'jest' ? '.test.tsx' : '.cy.tsx') 166 | : (framework === 'jest' ? '.test.jsx' : '.cy.jsx'); 167 | 168 | // Write component file 169 | await writeFile(path.join(testDir, `${name}${sourceExt}`), componentCode); 170 | 171 | // Write test file 172 | await writeFile(path.join(testDir, `${name}${testExt}`), testCode); 173 | 174 | // Create props file if props provided 175 | if (props) { 176 | await writeFile( 177 | path.join(testDir, 'props.json'), 178 | JSON.stringify(props, null, 2) 179 | ); 180 | 181 | // Create a wrapper component for the tests 182 | const { wrapperCode, importStatement } = getComponentTestWrappers(name, isTypeScript); 183 | 184 | await writeFile( 185 | path.join(testDir, `TestWrapper${sourceExt}`), 186 | `${importStatement} 187 | import ${name} from './${name}'; 188 | import testProps from './props.json'; 189 | 190 | ${wrapperCode}` 191 | ); 192 | } 193 | 194 | // Set up configuration 195 | if (framework === 'jest') { 196 | await writeFile( 197 | path.join(testDir, 'jest.config.js'), 198 | getDefaultJestConfig('component', isTypeScript) as string 199 | ); 200 | 201 | await writeFile( 202 | path.join(testDir, 'setupTests.js'), 203 | `import '@testing-library/jest-dom';` 204 | ); 205 | } else { 206 | await writeFile( 207 | path.join(testDir, 'cypress.config.js'), 208 | getDefaultCypressConfig('component', isTypeScript) as string 209 | ); 210 | 211 | await mkdir(path.join(testDir, 'cypress', 'support'), { recursive: true }); 212 | await writeFile( 213 | path.join(testDir, 'cypress', 'support', 'component.js'), 214 | `import { mount } from 'cypress/react18' 215 | import './commands' 216 | Cypress.Commands.add('mount', mount)` 217 | ); 218 | 219 | await writeFile( 220 | path.join(testDir, 'cypress', 'support', 'commands.js'), 221 | `// Custom commands go here` 222 | ); 223 | } 224 | 225 | // Set up package.json 226 | await writeFile( 227 | path.join(testDir, 'package.json'), 228 | JSON.stringify({ 229 | name: 'mcp-component-test', 230 | version: '1.0.0', 231 | type: 'module', 232 | dependencies: getDependencies(framework, 'component', isTypeScript, true), 233 | scripts: { 234 | test: framework === 'jest' ? 'jest' : 'cypress run-component' 235 | } 236 | }, null, 2) 237 | ); 238 | } 239 | 240 | // Execute tests and return results 241 | export async function executeTest( 242 | testDir: string, 243 | framework: 'jest' | 'cypress', 244 | type: 'unit' | 'component' | 'e2e' 245 | ): Promise<any> { 246 | // Change to test directory 247 | const cwd = process.cwd(); 248 | process.chdir(testDir); 249 | 250 | try { 251 | // Install dependencies 252 | console.info('Installing dependencies...'); 253 | await executeCommand('npm install --silent', testDir); 254 | 255 | // Run tests 256 | console.info(`Running ${framework} ${type} tests...`); 257 | let result; 258 | if (framework === 'jest') { 259 | result = await executeCommand('npx jest --json', testDir); 260 | return JSON.parse(result.stdout); 261 | } else if (framework === 'cypress') { 262 | if (type === 'component') { 263 | result = await executeCommand('npx cypress run-component --reporter json', testDir); 264 | } else { 265 | result = await executeCommand('npx cypress run --reporter json', testDir); 266 | } 267 | return result.stdout ? JSON.parse(result.stdout) : { success: false, error: result.stderr }; 268 | } 269 | } catch (error) { 270 | console.error('Test execution error:', error); 271 | return { 272 | success: false, 273 | error: error instanceof Error ? error.message : String(error) 274 | }; 275 | } finally { 276 | // Change back to original directory 277 | process.chdir(cwd); 278 | } 279 | } 280 | 281 | // Execute component tests 282 | export async function executeComponentTest( 283 | testDir: string, 284 | framework: 'jest' | 'cypress' 285 | ): Promise<any> { 286 | return executeTest(testDir, framework, 'component'); 287 | } 288 | 289 | // Clean up test environment 290 | export async function cleanupTestEnvironment(testDir: string): Promise<void> { 291 | try { 292 | await rm(testDir, { recursive: true, force: true }); 293 | } catch (error) { 294 | console.error('Error cleaning up test environment:', error); 295 | } 296 | } ```