Skip to main content
Glama

Automation Script Generator MCP Server

index.js86.8 kB
#!/usr/bin/env node /** * Automation Script Generator MCP Server * * This MCP server provides tools for automated test code generation: * - Fetches test scenarios from Notion * - Analyzes repository patterns * - Generates WDIO test files (features, steps, pages, components) * - Reviews and enhances generated code */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js'; import fs from 'fs-extra'; import path from 'path'; import dotenv from 'dotenv'; import Ajv from 'ajv'; import { glob } from 'glob'; // Import modular components import { ConfigManager } from './src/config.js'; import { FileAnalyzer } from './src/file-analyzer.js'; import { CodeGenerator } from './src/code-generator.js'; import { SmartAnalyzer } from './src/smart-analyzer.js'; import { PatternExtractor } from './src/pattern-extractor.js'; import { CacheManager, PerformanceMonitor } from './src/cache-manager.js'; // Load environment variables dotenv.config(); // Initialize JSON Schema validator const ajv = new Ajv({ allErrors: true }); class AutomationScriptGenerator { constructor() { // Initialize configuration management this.config = new ConfigManager(); // Initialize performance monitoring this.performanceMonitor = new PerformanceMonitor(); this.cacheManager = new CacheManager(this.config); // Initialize modular components this.fileAnalyzer = new FileAnalyzer(this.config); this.codeGenerator = new CodeGenerator(this.config); this.smartAnalyzer = new SmartAnalyzer(this.config, this.fileAnalyzer); this.patternExtractor = new PatternExtractor(this.config); this.server = new Server( { name: 'automation-script-generator', version: '1.0.0', }, { capabilities: { tools: {}, }, } ); // Store tool schemas for validation this.toolSchemas = {}; this.setupToolHandlers(); } /** * Validate arguments against the tool's input schema * @param {string} toolName - Name of the tool * @param {object} args - Arguments to validate * @throws {McpError} If validation fails */ validateArgs(toolName, args) { const schema = this.toolSchemas[toolName]; if (!schema) { throw new McpError( ErrorCode.InvalidParams, `No schema found for tool: ${toolName}` ); } const validate = ajv.compile(schema); const valid = validate(args); if (!valid) { const errors = validate.errors .map(err => `${err.instancePath || 'root'}: ${err.message}`) .join(', '); throw new McpError( ErrorCode.InvalidParams, `Validation failed for tool ${toolName}: ${errors}` ); } } setupToolHandlers() { // Define tool configurations with schemas const toolConfigs = [ { name: 'process_test_scenario', description: 'Process a test scenario provided directly by the user and generate complete WDIO test files', inputSchema: { type: 'object', properties: { scenario_title: { type: 'string', description: 'Title of the test scenario', minLength: 1, }, tags: { type: 'array', items: { type: 'string' }, description: 'Test ID tags for the scenario (e.g., ["@login", "@smoke", "@TEST-001"])', }, gherkin_syntax: { type: 'string', description: 'Complete Gherkin syntax with Given/When/Then steps', minLength: 1, }, selectors: { type: 'object', description: 'UI element selectors as key-value pairs (e.g., {"usernameInput": "#username", "loginButton": "[data-testid=login-btn]"})', }, data_items: { type: 'object', description: 'Test data items and configurations (optional)', }, output_directory: { type: 'string', description: 'Base directory where all generated files should be saved', minLength: 1, }, repo_path: { type: 'string', description: 'Path to existing repository for pattern analysis (optional)', }, }, required: ['scenario_title', 'gherkin_syntax', 'selectors', 'output_directory'], additionalProperties: false, }, }, { name: 'analyze_repository_patterns', description: 'Read and analyze repository for existing patterns, formats, and validation rules', inputSchema: { type: 'object', properties: { repo_path: { type: 'string', description: 'Path to the repository to analyze', }, pattern_types: { type: 'array', items: { type: 'string' }, description: 'Types of patterns to analyze (features, steps, pages, components)', default: ['features', 'steps', 'pages', 'components'], }, }, required: ['repo_path'], additionalProperties: false, }, }, { name: 'generate_feature_file', description: 'Generate WDIO feature file with Gherkin syntax (Given, When, Then)', inputSchema: { type: 'object', properties: { scenario_title: { type: 'string', description: 'Title of the test scenario', minLength: 1, }, gherkin_syntax: { type: 'string', description: 'Gherkin syntax content for the feature', minLength: 1, }, tags: { type: 'array', items: { type: 'string' }, description: 'Test ID tags for the scenario', }, output_path: { type: 'string', description: 'Path where the feature file should be saved', minLength: 1, }, }, required: ['scenario_title', 'gherkin_syntax', 'output_path'], additionalProperties: false, }, }, { name: 'generate_steps_file', description: 'Generate WDIO step definitions file with functions to execute Gherkin syntax', inputSchema: { type: 'object', properties: { scenario_title: { type: 'string', description: 'Title of the test scenario', minLength: 1, }, gherkin_syntax: { type: 'string', description: 'Gherkin syntax to generate steps for', minLength: 1, }, selectors: { type: 'object', description: 'Selectors for UI elements', }, existing_steps: { type: 'array', items: { type: 'string' }, description: 'List of existing step functions to reuse', }, output_path: { type: 'string', description: 'Path where the steps file should be saved', minLength: 1, }, }, required: ['scenario_title', 'gherkin_syntax', 'output_path'], additionalProperties: false, }, }, { name: 'generate_page_file', description: 'Generate WDIO page object file with general functions and element selectors', inputSchema: { type: 'object', properties: { scenario_title: { type: 'string', description: 'Title of the test scenario', minLength: 1, }, selectors: { type: 'object', description: 'Selectors for each UI element', }, page_functions: { type: 'array', items: { type: 'string' }, description: 'List of page functions needed', }, output_path: { type: 'string', description: 'Path where the page file should be saved', minLength: 1, }, }, required: ['scenario_title', 'selectors', 'output_path'], additionalProperties: false, }, }, { name: 'generate_component_file', description: 'Generate component file with collection of data items (only if needed)', inputSchema: { type: 'object', properties: { scenario_title: { type: 'string', description: 'Title of the test scenario', minLength: 1, }, data_items: { type: 'object', description: 'Collection of data items for the component', }, output_path: { type: 'string', description: 'Path where the component file should be saved', minLength: 1, }, }, required: ['scenario_title', 'data_items', 'output_path'], additionalProperties: false, }, }, { name: 'review_and_enhance_code', description: 'Review generated code and enhance it with improvements, documentation, validation, POM patterns, and reusable functions', inputSchema: { type: 'object', properties: { file_paths: { type: 'array', items: { type: 'string', minLength: 1 }, description: 'Paths to the generated files to review', minItems: 1, }, review_criteria: { type: 'array', items: { type: 'string', enum: ['docs', 'pom', 'functions', 'utils', 'format', 'existing_steps'] }, description: 'Specific review criteria (docs, pom, functions, utils, format, existing_steps)', default: ['docs', 'pom', 'functions', 'utils', 'format', 'existing_steps'], }, repo_path: { type: 'string', description: 'Repository path for checking existing functions and patterns', }, }, required: ['file_paths'], additionalProperties: false, }, }, ]; // Store schemas for validation toolConfigs.forEach(config => { this.toolSchemas[config.name] = config.inputSchema; }); this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: toolConfigs, }; }); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { // Validate arguments against schema this.validateArgs(name, args || {}); switch (name) { case 'process_test_scenario': return await this.processTestScenario(args); case 'analyze_repository_patterns': return await this.analyzeRepositoryPatterns(args); case 'generate_feature_file': return await this.generateFeatureFile(args); case 'generate_steps_file': return await this.generateStepsFile(args); case 'generate_page_file': return await this.generatePageFile(args); case 'generate_component_file': return await this.generateComponentFile(args); case 'review_and_enhance_code': return await this.reviewAndEnhanceCode(args); default: throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${name}` ); } } catch (error) { if (error instanceof McpError) { throw error; } throw new McpError( ErrorCode.InternalError, `Error executing tool ${name}: ${error.message}` ); } }); } async processTestScenario(args) { const { scenario_title, tags = [], gherkin_syntax, selectors, data_items = null, output_directory, repo_path } = args; try { const results = { scenario_title, tags, files_generated: [], files_updated: [], analysis: null, decisions: [] }; // Step 1: Analyze repository patterns if repo_path provided if (repo_path) { console.error(`Analyzing repository patterns at: ${repo_path}`); const analysisResult = await this.analyzeRepositoryPatterns({ repo_path, pattern_types: ['features', 'steps', 'pages', 'components'] }); results.analysis = analysisResult.content[0].text; } // Step 2: Analyze existing files and determine strategy const fileStrategy = await this.analyzeExistingFiles( output_directory, scenario_title, gherkin_syntax, selectors, repo_path ); results.decisions = fileStrategy.decisions; // Step 3: Process files based on strategy await this.executeFileStrategy(fileStrategy, { scenario_title, tags, gherkin_syntax, selectors, data_items, output_directory, repo_path }); results.files_generated = fileStrategy.filesToCreate; results.files_updated = fileStrategy.filesToUpdate; // Step 4: Review and enhance all processed files console.error('Reviewing and enhancing generated/updated code...'); const allFiles = [...results.files_generated, ...results.files_updated]; if (allFiles.length > 0) { await this.reviewAndEnhanceCode({ file_paths: allFiles, review_criteria: ['docs', 'pom', 'functions', 'utils', 'format', 'existing_steps'], repo_path }); } return { content: [ { type: 'text', text: this.formatProcessingResults(results), }, ], }; } catch (error) { throw new Error(`Failed to process test scenario: ${error.message}`); } } /** * Analyze existing files and determine the best strategy for new test scenario */ async analyzeExistingFiles(output_directory, scenario_title, gherkin_syntax, selectors, repo_path) { const strategy = { decisions: [], filesToCreate: [], filesToUpdate: [], similarFeatures: [], matchingPages: [], existingSteps: [] }; try { // Check if output directory exists const dirExists = await fs.pathExists(output_directory); if (!dirExists) { strategy.decisions.push('Output directory does not exist - will create new files'); return this.createNewFilesStrategy(output_directory, scenario_title); } // Analyze existing feature files const existingFeatures = await this.findSimilarFeatures(output_directory, scenario_title, gherkin_syntax); strategy.similarFeatures = existingFeatures; // Analyze existing page objects const matchingPages = await this.findMatchingPageObjects(output_directory, selectors); strategy.matchingPages = matchingPages; // Analyze existing step definitions const existingSteps = await this.findReusableSteps(output_directory, gherkin_syntax, repo_path); strategy.existingSteps = existingSteps; // Determine strategy based on analysis if (existingFeatures.length > 0) { // Similar feature found - update existing strategy.decisions.push(`Found similar feature: ${existingFeatures[0].file} - will add new scenario`); strategy.filesToUpdate.push(existingFeatures[0].path); // Check if we need to update corresponding steps file const stepsFile = existingFeatures[0].path.replace(/\.feature$/, '.steps.js').replace('/features/', '/step-definitions/'); if (await fs.pathExists(stepsFile)) { strategy.filesToUpdate.push(stepsFile); strategy.decisions.push(`Will update corresponding steps file: ${path.basename(stepsFile)}`); } } else { // No similar feature - create new feature file const featureFileName = this.generateFileName(scenario_title, 'feature'); const featurePath = path.join(output_directory, 'features', featureFileName); strategy.filesToCreate.push(featurePath); strategy.decisions.push('No similar feature found - will create new feature file'); } // Handle page objects if (matchingPages.length > 0) { // Update existing page object with new selectors strategy.filesToUpdate.push(matchingPages[0].path); strategy.decisions.push(`Found matching page object: ${matchingPages[0].file} - will add new selectors`); } else { // Create new page object const pageFileName = this.generateFileName(scenario_title, 'page'); const pagePath = path.join(output_directory, 'pageobjects', pageFileName); strategy.filesToCreate.push(pagePath); strategy.decisions.push('No matching page object found - will create new page file'); } // Handle step definitions if (strategy.filesToUpdate.some(f => f.includes('.steps.js'))) { // Already updating steps file with feature strategy.decisions.push('Step definitions will be updated with feature scenarios'); } else { // Create new steps file const stepsFileName = this.generateFileName(scenario_title, 'steps'); const stepsPath = path.join(output_directory, 'step-definitions', stepsFileName); strategy.filesToCreate.push(stepsPath); strategy.decisions.push('Will create new step definitions file'); } return strategy; } catch (error) { console.warn(`Error analyzing existing files: ${error.message}`); strategy.decisions.push('Analysis failed - defaulting to create new files'); return this.createNewFilesStrategy(output_directory, scenario_title); } } /** * Find similar existing feature files based on title and content */ async findSimilarFeatures(output_directory, scenario_title, gherkin_syntax) { const features = []; const featuresDir = path.join(output_directory, 'features'); try { if (!await fs.pathExists(featuresDir)) return features; const files = await fs.readdir(featuresDir); const featureFiles = files.filter(f => f.endsWith('.feature')); for (const file of featureFiles) { const filePath = path.join(featuresDir, file); const content = await fs.readFile(filePath, 'utf8'); // Calculate similarity based on: // 1. Title similarity // 2. Common keywords in Gherkin // 3. Feature context const similarity = this.calculateFeatureSimilarity( scenario_title, gherkin_syntax, content ); if (similarity > 0.6) { // 60% similarity threshold features.push({ file, path: filePath, similarity, content }); } } // Sort by similarity score return features.sort((a, b) => b.similarity - a.similarity); } catch (error) { console.warn(`Error finding similar features: ${error.message}`); return features; } } /** * Find matching page objects based on selectors */ async findMatchingPageObjects(output_directory, selectors) { const pages = []; const pagesDir = path.join(output_directory, 'pageobjects'); try { if (!await fs.pathExists(pagesDir)) return pages; const files = await fs.readdir(pagesDir); const pageFiles = files.filter(f => f.endsWith('.page.js')); for (const file of pageFiles) { const filePath = path.join(pagesDir, file); const content = await fs.readFile(filePath, 'utf8'); // Check for matching selectors const matchingSelectors = this.findMatchingSelectors(selectors, content); if (matchingSelectors.length > 0) { pages.push({ file, path: filePath, matchingSelectors, content }); } } return pages; } catch (error) { console.warn(`Error finding matching page objects: ${error.message}`); return pages; } } /** * Find reusable step definitions */ async findReusableSteps(output_directory, gherkin_syntax, repo_path) { const reusableSteps = []; const stepsDir = path.join(output_directory, 'step-definitions'); try { const searchDirs = [stepsDir]; if (repo_path) { searchDirs.push(path.join(repo_path, 'step-definitions')); searchDirs.push(path.join(repo_path, 'steps')); } const newSteps = this.parseGherkinSteps(gherkin_syntax); for (const dir of searchDirs) { if (!await fs.pathExists(dir)) continue; const files = await fs.readdir(dir); const stepFiles = files.filter(f => f.endsWith('.steps.js') || f.endsWith('.step.js')); for (const file of stepFiles) { const filePath = path.join(dir, file); const content = await fs.readFile(filePath, 'utf8'); const existingSteps = this.extractStepDefinitions(content); // Find matching steps for (const newStep of newSteps) { const normalizedNewStep = this.normalizeStepText(newStep); for (const existingStep of existingSteps) { const normalizedExistingStep = this.normalizeStepText(existingStep); if (this.calculateStepSimilarity(normalizedNewStep, normalizedExistingStep) > 0.8) { reusableSteps.push({ newStep, existingStep, file: filePath, similarity: this.calculateStepSimilarity(normalizedNewStep, normalizedExistingStep) }); } } } } } return reusableSteps; } catch (error) { console.warn(`Error finding reusable steps: ${error.message}`); return reusableSteps; } } /** * Execute the determined file strategy */ async executeFileStrategy(strategy, params) { const { scenario_title, tags, gherkin_syntax, selectors, data_items, output_directory, repo_path } = params; // Create new files for (const filePath of strategy.filesToCreate) { if (filePath.endsWith('.feature')) { await this.generateFeatureFile({ scenario_title, gherkin_syntax, tags, output_path: filePath }); } else if (filePath.endsWith('.steps.js')) { const existing_steps = repo_path ? await this.extractExistingSteps(repo_path) : []; await this.generateStepsFile({ scenario_title, gherkin_syntax, selectors, existing_steps, output_path: filePath }); } else if (filePath.endsWith('.page.js')) { const page_functions = this.extractPageFunctionsFromGherkin(gherkin_syntax); await this.generatePageFile({ scenario_title, selectors, page_functions, output_path: filePath }); } else if (filePath.endsWith('.data.js') && data_items) { await this.generateComponentFile({ scenario_title, data_items, output_path: filePath }); } } // Update existing files for (const filePath of strategy.filesToUpdate) { if (filePath.endsWith('.feature')) { await this.updateFeatureFile(filePath, scenario_title, gherkin_syntax, tags); } else if (filePath.endsWith('.steps.js')) { await this.updateStepsFile(filePath, gherkin_syntax, selectors, strategy.existingSteps); } else if (filePath.endsWith('.page.js')) { await this.updatePageFile(filePath, selectors); } } } /** * Create strategy for new files when no existing files found */ createNewFilesStrategy(output_directory, scenario_title) { const featureFileName = this.generateFileName(scenario_title, 'feature'); const stepsFileName = this.generateFileName(scenario_title, 'steps'); const pageFileName = this.generateFileName(scenario_title, 'page'); return { decisions: ['No existing files found - creating new test suite'], filesToCreate: [ path.join(output_directory, 'features', featureFileName), path.join(output_directory, 'step-definitions', stepsFileName), path.join(output_directory, 'pageobjects', pageFileName) ], filesToUpdate: [], similarFeatures: [], matchingPages: [], existingSteps: [] }; } /** * Calculate similarity between features based on title and content */ calculateFeatureSimilarity(scenario_title, gherkin_syntax, existingContent) { const titleWords = scenario_title.toLowerCase().split(/\s+/); const gherkinWords = gherkin_syntax.toLowerCase().split(/\s+/); const existingWords = existingContent.toLowerCase().split(/\s+/); // Calculate title similarity const titleMatches = titleWords.filter(word => existingWords.includes(word)).length; const titleSimilarity = titleMatches / titleWords.length; // Calculate content similarity const contentMatches = gherkinWords.filter(word => existingWords.includes(word)).length; const contentSimilarity = contentMatches / gherkinWords.length; // Weighted average (title 40%, content 60%) return (titleSimilarity * 0.4) + (contentSimilarity * 0.6); } /** * Find matching selectors in existing page object */ findMatchingSelectors(newSelectors, pageContent) { const matches = []; for (const [key, selector] of Object.entries(newSelectors)) { // Check if selector or similar selector exists if (pageContent.includes(selector) || pageContent.includes(key) || pageContent.includes(this.toCamelCase(key))) { matches.push(key); } } return matches; } /** * Normalize step text for comparison */ normalizeStepText(stepText) { return stepText .replace(/^(Given|When|Then|And|But)\s+/i, '') .replace(/['"]/g, '') .replace(/\s+/g, ' ') .trim() .toLowerCase(); } /** * Calculate similarity between step texts */ calculateStepSimilarity(step1, step2) { const words1 = step1.split(/\s+/); const words2 = step2.split(/\s+/); const commonWords = words1.filter(word => words2.includes(word)); return (commonWords.length * 2) / (words1.length + words2.length); } /** * Update existing feature file with new scenario */ async updateFeatureFile(filePath, scenario_title, gherkin_syntax, tags) { try { const existingContent = await fs.readFile(filePath, 'utf8'); // Add new scenario to existing feature const tagString = tags.map(tag => ` @${tag.replace('@', '')}`).join(' '); const newScenario = `\n${tagString ? tagString + '\n' : ''} Scenario: ${scenario_title}\n` + gherkin_syntax.split('\n').map(line => ` ${line.trim()}`).join('\n') + '\n'; const updatedContent = existingContent.trim() + '\n' + newScenario; await fs.writeFile(filePath, updatedContent); console.error(`Updated feature file: ${filePath}`); } catch (error) { throw new Error(`Failed to update feature file: ${error.message}`); } } /** * Update existing steps file with new step definitions */ async updateStepsFile(filePath, gherkin_syntax, selectors, existingSteps) { try { const existingContent = await fs.readFile(filePath, 'utf8'); const newSteps = this.parseGherkinSteps(gherkin_syntax); const newStepDefinitions = []; for (const step of newSteps) { const normalizedStep = this.normalizeStepText(step); const isReusable = existingSteps.some(existing => this.calculateStepSimilarity(normalizedStep, this.normalizeStepText(existing.existingStep)) > 0.8 ); if (!isReusable) { const stepType = step.split(' ')[0]; const stepText = step.substring(stepType.length).trim(); newStepDefinitions.push(`\n${stepType}('${stepText}', async () => { ${this.generateStepImplementation(step, stepText, stepType, selectors)} });`); } } if (newStepDefinitions.length > 0) { const updatedContent = existingContent.trim() + '\n' + newStepDefinitions.join('\n'); await fs.writeFile(filePath, updatedContent); console.error(`Updated steps file: ${filePath} with ${newStepDefinitions.length} new step definitions`); } } catch (error) { throw new Error(`Failed to update steps file: ${error.message}`); } } /** * Update existing page object with new selectors */ async updatePageFile(filePath, selectors) { try { const existingContent = await fs.readFile(filePath, 'utf8'); const newSelectors = []; for (const [key, selector] of Object.entries(selectors)) { const camelCaseKey = this.toCamelCase(key); // Check if selector already exists if (!existingContent.includes(camelCaseKey) && !existingContent.includes(selector)) { newSelectors.push(`\n get ${camelCaseKey}() { return $('${selector}'); }`); } } if (newSelectors.length > 0) { // Find the position to insert new selectors (before the closing brace) const insertPosition = existingContent.lastIndexOf('}'); const updatedContent = existingContent.slice(0, insertPosition) + newSelectors.join('') + '\n' + existingContent.slice(insertPosition); await fs.writeFile(filePath, updatedContent); console.error(`Updated page file: ${filePath} with ${newSelectors.length} new selectors`); } } catch (error) { throw new Error(`Failed to update page file: ${error.message}`); } } /** * Format processing results for display */ formatProcessingResults(results) { let output = `✅ Successfully processed test scenario: "${results.scenario_title}"\n\n`; // Show decisions made if (results.decisions.length > 0) { output += `🤔 Analysis & Decisions:\n${results.decisions.map(d => ` • ${d}`).join('\n')}\n\n`; } // Show files created if (results.files_generated.length > 0) { output += `📁 New Files Created:\n${results.files_generated.map(f => ` • ${f}`).join('\n')}\n\n`; } // Show files updated if (results.files_updated.length > 0) { output += `📝 Existing Files Updated:\n${results.files_updated.map(f => ` • ${f}`).join('\n')}\n\n`; } output += `🏷️ Tags: ${results.tags.join(', ')}\n\n`; output += `📋 Summary:\n`; output += ` • Smart file analysis and strategy determination\n`; output += ` • ${results.files_generated.length > 0 ? 'Feature file with Gherkin syntax\n • ' : ''}`; output += ` • ${results.files_generated.length > 0 ? 'Step definitions with WDIO functions\n • ' : ''}`; output += ` • ${results.files_generated.length > 0 ? 'Page Object Model with selectors\n • ' : ''}`; output += ` • Code review and enhancement applied\n\n`; if (results.analysis) { output += `🔍 Repository Analysis:\n${results.analysis}\n\n`; } output += `🚀 All files are ready for use with WDIO framework!`; return output; } async analyzeRepositoryPatterns(args) { const { repo_path, pattern_types = ['features', 'steps', 'pages', 'components'] } = args; try { const analysis = { patterns: {}, existing_functions: {}, conventions: {}, utils: {} }; for (const patternType of pattern_types) { const patterns = await this.scanForPatterns(repo_path, patternType); analysis.patterns[patternType] = patterns; } // Scan for existing utility functions analysis.utils = await this.scanForUtils(repo_path); // Extract naming conventions analysis.conventions = await this.extractNamingConventions(repo_path); return { content: [ { type: 'text', text: `Repository analysis complete:\n\n${JSON.stringify(analysis, null, 2)}`, }, ], }; } catch (error) { throw new Error(`Failed to analyze repository patterns: ${error.message}`); } } async generateFeatureFile(args) { const { scenario_title, gherkin_syntax, tags = [], output_path } = args; try { const featureContent = this.buildFeatureFileContent(scenario_title, gherkin_syntax, tags); await fs.ensureDir(path.dirname(output_path)); await fs.writeFile(output_path, featureContent); return { content: [ { type: 'text', text: `Feature file generated successfully at: ${output_path}\n\nContent:\n${featureContent}`, }, ], }; } catch (error) { throw new Error(`Failed to generate feature file: ${error.message}`); } } async generateStepsFile(args) { const { scenario_title, gherkin_syntax, selectors = {}, existing_steps = [], output_path } = args; try { const stepsContent = this.buildStepsFileContent(scenario_title, gherkin_syntax, selectors, existing_steps); await fs.ensureDir(path.dirname(output_path)); await fs.writeFile(output_path, stepsContent); return { content: [ { type: 'text', text: `Steps file generated successfully at: ${output_path}\n\nContent:\n${stepsContent}`, }, ], }; } catch (error) { throw new Error(`Failed to generate steps file: ${error.message}`); } } async generatePageFile(args) { const { scenario_title, selectors, page_functions = [], output_path } = args; try { const pageContent = this.buildPageFileContent(scenario_title, selectors, page_functions); await fs.ensureDir(path.dirname(output_path)); await fs.writeFile(output_path, pageContent); return { content: [ { type: 'text', text: `Page file generated successfully at: ${output_path}\n\nContent:\n${pageContent}`, }, ], }; } catch (error) { throw new Error(`Failed to generate page file: ${error.message}`); } } async generateComponentFile(args) { const { scenario_title, data_items, output_path } = args; try { const componentContent = this.buildComponentFileContent(scenario_title, data_items); await fs.ensureDir(path.dirname(output_path)); await fs.writeFile(output_path, componentContent); return { content: [ { type: 'text', text: `Component file generated successfully at: ${output_path}\n\nContent:\n${componentContent}`, }, ], }; } catch (error) { throw new Error(`Failed to generate component file: ${error.message}`); } } async reviewAndEnhanceCode(args) { const { file_paths, review_criteria = ['docs', 'pom', 'functions', 'utils', 'format', 'existing_steps'], repo_path } = args; try { const reviews = []; for (const filePath of file_paths) { const content = await fs.readFile(filePath, 'utf8'); const review = await this.performCodeReview(content, review_criteria, repo_path, filePath); reviews.push({ file: filePath, review }); // Apply enhancements if (review.enhanced_content) { await fs.writeFile(filePath, review.enhanced_content); } } return { content: [ { type: 'text', text: `Code review and enhancement complete:\n\n${JSON.stringify(reviews, null, 2)}`, }, ], }; } catch (error) { throw new Error(`Failed to review and enhance code: ${error.message}`); } } // Helper methods for content generation buildFeatureFileContent(scenario_title, gherkin_syntax, tags) { const tagString = tags.map(tag => `@${tag}`).join(' '); return `${tagString ? tagString + '\n' : ''}Feature: ${scenario_title} ${gherkin_syntax} `; } buildStepsFileContent(scenario_title, gherkin_syntax, selectors, existing_steps) { const steps = this.parseGherkinSteps(gherkin_syntax); const imports = this.generateStepImports(selectors); const stepDefinitions = this.generateStepDefinitions(steps, selectors, existing_steps); return `${imports} /** * Step definitions for ${scenario_title} * Generated from Gherkin syntax */ ${stepDefinitions} `; } buildPageFileContent(scenario_title, selectors, page_functions) { const className = this.toPascalCase(scenario_title.replace(/[^a-zA-Z0-9]/g, '')) + 'Page'; const selectorMethods = this.generateSelectorMethods(selectors); const pageMethods = this.generatePageMethods(page_functions); return `/** * Page Object Model for ${scenario_title} * Contains selectors and page-specific functions */ class ${className} { // Element selectors ${selectorMethods} // Page functions ${pageMethods} } module.exports = ${className}; `; } buildComponentFileContent(scenario_title, data_items) { const componentName = this.toCamelCase(scenario_title.replace(/[^a-zA-Z0-9]/g, '')) + 'Data'; const dataStructure = JSON.stringify(data_items, null, 2); return `/** * Data component for ${scenario_title} * Contains test data items and configurations */ const ${componentName} = ${dataStructure}; module.exports = ${componentName}; `; } // Utility methods generateFileName(scenario_title, type) { const cleanTitle = scenario_title .toLowerCase() .replace(/[^a-z0-9]/g, '-') .replace(/-+/g, '-') .replace(/^-|-$/g, ''); const extensions = { feature: '.feature', steps: '.steps.js', page: '.page.js', data: '.data.js' }; return `${cleanTitle}${extensions[type] || '.js'}`; } async extractExistingSteps(repo_path) { const existing_steps = []; try { const stepFiles = await this.findFilesByPattern(repo_path, 'steps'); for (const file of stepFiles) { const content = await fs.readFile(file, 'utf8'); const steps = this.extractStepDefinitions(content); existing_steps.push(...steps); } } catch (error) { console.warn(`Could not extract existing steps: ${error.message}`); } return existing_steps; } extractPageFunctionsFromGherkin(gherkin_syntax) { const functions = []; const lines = gherkin_syntax.split('\n'); for (const line of lines) { const trimmed = line.trim(); if (trimmed.match(/^(Given|When|Then|And|But)/)) { // Extract potential function names from steps const words = trimmed.split(' ').slice(1); // Remove Given/When/Then const functionName = words .join(' ') .replace(/['"]/g, '') .replace(/\s+/g, '_') .toLowerCase(); if (functionName.length > 3) { functions.push(functionName); } } } return [...new Set(functions)]; // Remove duplicates } extractStepDefinitions(content) { const steps = []; const stepRegex = /(Given|When|Then|And|But)\(['"`]([^'"`]+)['"`]/g; let match; while ((match = stepRegex.exec(content)) !== null) { steps.push(match[2]); // The step text } return steps; } parseSelectorsFromNotion(selectorsText) { try { return JSON.parse(selectorsText); } catch { return {}; } } parseDataItemsFromNotion(dataItemsText) { try { return JSON.parse(dataItemsText); } catch { return {}; } } async scanForPatterns(repo_path, patternType) { const patterns = []; try { const files = await this.findFilesByPattern(repo_path, patternType); for (const file of files) { try { const content = await fs.readFile(file, 'utf8'); const extractedPatterns = this.extractPatterns(content, patternType); if (extractedPatterns && Object.keys(extractedPatterns).length > 0) { patterns.push({ file: path.relative(repo_path, file), patterns: extractedPatterns }); } } catch (fileError) { console.warn(`Could not read file ${file}: ${fileError.message}`); } } } catch (error) { console.warn(`Could not scan for ${patternType} patterns: ${error.message}`); } return patterns; } async scanForUtils(repo_path) { const utils = {}; try { const utilFiles = await this.findFilesByPattern(repo_path, 'utils'); for (const file of utilFiles) { try { const content = await fs.readFile(file, 'utf8'); const fileName = path.basename(file, path.extname(file)); utils[fileName] = { filePath: path.relative(repo_path, file), functions: this.extractUtilFunctions(content), exports: this.extractExportedFunctions(content), dependencies: this.extractDependencies(content) }; } catch (fileError) { console.warn(`Could not read utility file ${file}: ${fileError.message}`); } } } catch (error) { console.warn(`Could not scan for utils: ${error.message}`); } return utils; } async extractNamingConventions(repo_path) { const conventions = { fileNaming: 'kebab-case', classNaming: 'PascalCase', functionNaming: 'camelCase', variableNaming: 'camelCase', constantNaming: 'UPPER_CASE', confidence: {} }; try { const allFiles = await this.findFilesByPattern(repo_path, 'all'); const samples = { files: [], classes: [], functions: [], variables: [], constants: [] }; // Sample files for analysis for (const file of allFiles.slice(0, 20)) { // Limit to 20 files for performance try { const fileName = path.basename(file, path.extname(file)); samples.files.push(fileName); const content = await fs.readFile(file, 'utf8'); // Extract class names const classMatches = content.match(/class\s+(\w+)/g) || []; samples.classes.push(...classMatches.map(match => match.replace('class ', ''))); // Extract function names const functionMatches = content.match(/(?:function\s+|const\s+|let\s+|var\s+)(\w+)/g) || []; samples.functions.push(...functionMatches.map(match => match.replace(/(?:function\s+|const\s+|let\s+|var\s+)/, '') )); // Extract variable names (simplified) const variableMatches = content.match(/(?:const|let|var)\s+([a-z]\w*)/g) || []; samples.variables.push(...variableMatches.map(match => match.replace(/(?:const|let|var)\s+/, '') )); // Extract constants (UPPER_CASE pattern) const constantMatches = content.match(/(?:const|let|var)\s+([A-Z][A-Z_]*)/g) || []; samples.constants.push(...constantMatches.map(match => match.replace(/(?:const|let|var)\s+/, '') )); } catch (fileError) { // Skip files that can't be read continue; } } // Analyze naming patterns conventions.fileNaming = this.detectNamingPattern(samples.files, ['kebab-case', 'camelCase', 'snake_case']); conventions.classNaming = this.detectNamingPattern(samples.classes, ['PascalCase', 'camelCase']); conventions.functionNaming = this.detectNamingPattern(samples.functions, ['camelCase', 'snake_case']); conventions.variableNaming = this.detectNamingPattern(samples.variables, ['camelCase', 'snake_case']); conventions.constantNaming = this.detectNamingPattern(samples.constants, ['UPPER_CASE', 'camelCase']); // Calculate confidence scores conventions.confidence = { fileNaming: this.calculateConfidence(samples.files, conventions.fileNaming), classNaming: this.calculateConfidence(samples.classes, conventions.classNaming), functionNaming: this.calculateConfidence(samples.functions, conventions.functionNaming), variableNaming: this.calculateConfidence(samples.variables, conventions.variableNaming), constantNaming: this.calculateConfidence(samples.constants, conventions.constantNaming) }; } catch (error) { console.warn(`Error extracting naming conventions: ${error.message}`); } return conventions; } detectNamingPattern(samples, patterns) { const scores = {}; patterns.forEach(pattern => { scores[pattern] = 0; samples.forEach(sample => { if (this.matchesNamingPattern(sample, pattern)) { scores[pattern]++; } }); }); // Return pattern with highest score return Object.keys(scores).reduce((a, b) => scores[a] > scores[b] ? a : b) || patterns[0]; } matchesNamingPattern(name, pattern) { switch (pattern) { case 'kebab-case': return /^[a-z][a-z0-9-]*$/.test(name) && name.includes('-'); case 'camelCase': return /^[a-z][a-zA-Z0-9]*$/.test(name) && /[A-Z]/.test(name); case 'PascalCase': return /^[A-Z][a-zA-Z0-9]*$/.test(name); case 'snake_case': return /^[a-z][a-z0-9_]*$/.test(name) && name.includes('_'); case 'UPPER_CASE': return /^[A-Z][A-Z0-9_]*$/.test(name); default: return false; } } calculateConfidence(samples, pattern) { if (samples.length === 0) return 0; const matches = samples.filter(sample => this.matchesNamingPattern(sample, pattern)).length; return Math.round((matches / samples.length) * 100); } async findFilesByPattern(repo_path, patternType) { const extensions = ['.js', '.ts', '.feature']; const patterns = { features: ['**/features/**/*.feature', '**/feature/**/*.feature', '**/*.feature'], steps: ['**/steps/**/*.{js,ts}', '**/step_definitions/**/*.{js,ts}', '**/step-definitions/**/*.{js,ts}'], pages: ['**/pages/**/*.{js,ts}', '**/page_objects/**/*.{js,ts}', '**/pageobjects/**/*.{js,ts}'], components: ['**/components/**/*.{js,ts}', '**/data/**/*.{js,ts}'], utils: ['**/utils/**/*.{js,ts}', '**/helpers/**/*.{js,ts}'] }; const files = []; try { const searchPatterns = patterns[patternType] || [`**/*.{js,ts,feature}`]; for (const pattern of searchPatterns) { const matches = await glob(pattern, { cwd: repo_path, absolute: true, ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.git/**'] }); files.push(...matches); } // Remove duplicates and return unique files return [...new Set(files)]; } catch (error) { console.warn(`Error finding files for pattern ${patternType}: ${error.message}`); return []; } } extractPatterns(content, patternType) { const patterns = {}; try { switch (patternType) { case 'features': patterns.features = this.extractFeaturePatterns(content); break; case 'steps': patterns.stepDefinitions = this.extractStepPatterns(content); break; case 'pages': patterns.pageObjects = this.extractPagePatterns(content); break; case 'components': patterns.dataComponents = this.extractDataPatterns(content); break; case 'utils': patterns.utilities = this.extractUtilityPatterns(content); break; default: patterns.general = this.extractGeneralPatterns(content); } } catch (error) { console.warn(`Error extracting patterns for ${patternType}: ${error.message}`); } return patterns; } extractFeaturePatterns(content) { const patterns = { scenarios: [], tags: [], steps: [] }; // Extract feature scenarios const scenarioMatches = content.match(/Scenario:\s*(.+)/g) || []; patterns.scenarios = scenarioMatches.map(match => match.replace('Scenario:', '').trim()); // Extract tags const tagMatches = content.match(/@[\w-]+/g) || []; patterns.tags = [...new Set(tagMatches)]; // Extract step patterns const stepMatches = content.match(/(Given|When|Then|And|But)\s+(.+)/g) || []; patterns.steps = stepMatches.map(step => step.trim()); return patterns; } extractStepPatterns(content) { const patterns = { stepDefinitions: [], imports: [], exports: [] }; // Extract step definitions (Given, When, Then) const stepDefMatches = content.match(/(Given|When|Then|And|But)\(/g) || []; patterns.stepDefinitions = stepDefMatches.map(match => match.replace('(', '').trim()); // Extract import statements const importMatches = content.match(/import\s+.*?from\s+['"](.+?)['"];?/g) || []; patterns.imports = importMatches; // Extract function names const functionMatches = content.match(/(?:function\s+|const\s+|let\s+|var\s+)(\w+)/g) || []; patterns.exports = functionMatches.map(match => match.replace(/(?:function\s+|const\s+|let\s+|var\s+)/, '').trim() ); return patterns; } extractPagePatterns(content) { const patterns = { selectors: {}, methods: [], className: null }; // Extract class name const classMatch = content.match(/class\s+(\w+)/); patterns.className = classMatch ? classMatch[1] : null; // Extract selectors (various patterns) const selectorMatches = content.match(/['"]([#.\[].+?)['"]|['"](data-testid.+?)['"]/g) || []; selectorMatches.forEach(selector => { const cleanSelector = selector.replace(/['"]/g, ''); patterns.selectors[cleanSelector] = cleanSelector; }); // Extract method names const methodMatches = content.match(/(\w+)\s*\([^)]*\)\s*{/g) || []; patterns.methods = methodMatches.map(match => match.replace(/\s*\([^)]*\)\s*{/, '').trim() ); return patterns; } extractDataPatterns(content) { const patterns = { dataObjects: [], constants: [], exports: [] }; // Extract exported objects const exportMatches = content.match(/export\s+(?:const|let|var)\s+(\w+)/g) || []; patterns.exports = exportMatches.map(match => match.replace(/export\s+(?:const|let|var)\s+/, '').trim() ); // Extract object literals const objectMatches = content.match(/(\w+)\s*=\s*{/g) || []; patterns.dataObjects = objectMatches.map(match => match.replace(/\s*=\s*{/, '').trim() ); return patterns; } extractUtilityPatterns(content) { const patterns = { functions: [], classes: [], exports: [] }; // Extract function declarations const functionMatches = content.match(/(?:function\s+|const\s+\w+\s*=\s*(?:async\s+)?(?:function|\([^)]*\)\s*=>))(\w+)/g) || []; patterns.functions = functionMatches.map(match => { const functionName = match.match(/(\w+)/); return functionName ? functionName[1] : ''; }).filter(name => name); // Extract class declarations const classMatches = content.match(/class\s+(\w+)/g) || []; patterns.classes = classMatches.map(match => match.replace('class ', '').trim()); return patterns; } extractGeneralPatterns(content) { return { imports: content.match(/import\s+.*?from\s+['"](.+?)['"];?/g) || [], exports: content.match(/export\s+.*?/g) || [], functions: content.match(/(?:function\s+|const\s+\w+\s*=\s*(?:async\s+)?(?:function|\([^)]*\)\s*=>))(\w+)/g) || [] }; } extractUtilFunctions(content) { const functions = []; try { // Extract function declarations const functionRegex = /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\([^)]*\)\s*{/g; let match; while ((match = functionRegex.exec(content)) !== null) { functions.push({ name: match[1], type: 'function', isAsync: match[0].includes('async'), isExported: match[0].includes('export') }); } // Extract arrow functions const arrowRegex = /(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*=>/g; while ((match = arrowRegex.exec(content)) !== null) { functions.push({ name: match[1], type: 'arrow', isAsync: match[0].includes('async'), isExported: match[0].includes('export') }); } // Extract class methods const methodRegex = /(?:async\s+)?(\w+)\s*\([^)]*\)\s*{/g; const classContent = content.match(/class\s+\w+[^{]*{([^}]*)}/s); if (classContent) { while ((match = methodRegex.exec(classContent[1])) !== null) { if (match[1] !== 'constructor' && !functions.some(f => f.name === match[1])) { functions.push({ name: match[1], type: 'method', isAsync: match[0].includes('async'), isExported: false }); } } } } catch (error) { console.warn(`Error extracting utility functions: ${error.message}`); } return functions; } extractExportedFunctions(content) { const exports = []; try { // Extract named exports const namedExportRegex = /export\s*{\s*([^}]+)\s*}/g; let match; while ((match = namedExportRegex.exec(content)) !== null) { const exportNames = match[1].split(',').map(name => name.trim().split(' as ')[0]); exports.push(...exportNames); } // Extract default exports const defaultExportRegex = /export\s+default\s+(\w+)/g; while ((match = defaultExportRegex.exec(content)) !== null) { exports.push(match[1]); } // Extract direct exports const directExportRegex = /export\s+(?:const|let|var|function|class)\s+(\w+)/g; while ((match = directExportRegex.exec(content)) !== null) { exports.push(match[1]); } } catch (error) { console.warn(`Error extracting exported functions: ${error.message}`); } return [...new Set(exports)]; } extractDependencies(content) { const dependencies = []; try { // Extract import statements const importRegex = /import\s+.*?from\s+['"]([^'"]+)['"]/g; let match; while ((match = importRegex.exec(content)) !== null) { dependencies.push(match[1]); } // Extract require statements const requireRegex = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g; while ((match = requireRegex.exec(content)) !== null) { dependencies.push(match[1]); } } catch (error) { console.warn(`Error extracting dependencies: ${error.message}`); } return [...new Set(dependencies)]; } parseGherkinSteps(gherkin_syntax) { const steps = []; const lines = gherkin_syntax.split('\n'); for (const line of lines) { const trimmed = line.trim(); if (trimmed.match(/^(Given|When|Then|And|But)/)) { steps.push(trimmed); } } return steps; } generateStepImports(selectors) { return `const { Given, When, Then } = require('@wdio/cucumber-framework');`; } generateStepDefinitions(steps, selectors, existing_steps) { return steps.map(step => { const stepType = step.split(' ')[0]; const stepText = step.substring(stepType.length).trim(); return `${stepType}('${stepText}', async () => { ${this.generateStepImplementation(step, stepText, stepType, selectors)} });`; }).join('\n\n'); } generateSelectorMethods(selectors) { return Object.keys(selectors).map(key => { return ` get ${this.toCamelCase(key)}() { return $('${selectors[key]}'); }`; }).join('\n\n'); } generatePageMethods(page_functions) { return page_functions.map(func => { const methodName = this.toCamelCase(func); return ` async ${methodName}() { ${this.generatePageMethodImplementation(func, methodName)} }`; }).join('\n\n'); } generateStepImplementation(step, stepText, stepType, selectors = {}) { const implementation = []; const lowerStep = step.toLowerCase(); const lowerText = stepText.toLowerCase(); // Analyze step content and generate appropriate implementation if (stepType === 'Given') { if (lowerText.includes('on') && (lowerText.includes('page') || lowerText.includes('login') || lowerText.includes('home'))) { implementation.push(' await browser.url(\'/\');'); implementation.push(' await browser.waitUntil(() => browser.getTitle().then(title => title.length > 0));'); } else if (lowerText.includes('logged in') || lowerText.includes('authenticated')) { implementation.push(' // Assume user is already logged in - set up session'); implementation.push(' await browser.setCookies([{ name: \'session\', value: \'authenticated-user\' }]);'); } else { implementation.push(' // Setup precondition'); implementation.push(` console.log('Setting up: ${stepText}');`); } } else if (stepType === 'When') { if (lowerText.includes('click') || lowerText.includes('press')) { const element = this.extractElementFromStep(stepText, selectors); implementation.push(` await ${element}.waitForClickable();`); implementation.push(` await ${element}.click();`); } else if (lowerText.includes('enter') || lowerText.includes('type') || lowerText.includes('fill')) { const element = this.extractElementFromStep(stepText, selectors); const value = this.extractValueFromStep(stepText); implementation.push(` await ${element}.waitForDisplayed();`); implementation.push(` await ${element}.setValue('${value}');`); } else if (lowerText.includes('select') || lowerText.includes('choose')) { const element = this.extractElementFromStep(stepText, selectors); const value = this.extractValueFromStep(stepText); implementation.push(` await ${element}.waitForDisplayed();`); implementation.push(` await ${element}.selectByVisibleText('${value}');`); } else if (lowerText.includes('navigate') || lowerText.includes('go to')) { const url = this.extractUrlFromStep(stepText); implementation.push(` await browser.url('${url}');`); } else { implementation.push(' // Perform action'); implementation.push(` console.log('Executing: ${stepText}');`); } } else if (stepType === 'Then') { if (lowerText.includes('see') || lowerText.includes('displayed') || lowerText.includes('visible')) { const element = this.extractElementFromStep(stepText, selectors); implementation.push(` await expect(${element}).toBeDisplayed();`); } else if (lowerText.includes('text') || lowerText.includes('contains')) { const element = this.extractElementFromStep(stepText, selectors); const expectedText = this.extractValueFromStep(stepText); implementation.push(` await expect(${element}).toHaveText('${expectedText}');`); } else if (lowerText.includes('redirect') || lowerText.includes('url')) { const expectedUrl = this.extractUrlFromStep(stepText); implementation.push(` await expect(browser).toHaveUrl('${expectedUrl}');`); } else if (lowerText.includes('enabled') || lowerText.includes('clickable')) { const element = this.extractElementFromStep(stepText, selectors); implementation.push(` await expect(${element}).toBeEnabled();`); } else { implementation.push(' // Verify result'); implementation.push(` console.log('Verifying: ${stepText}');`); } } else { // Handle And/But by copying logic from previous step type implementation.push(' // Continue previous action pattern'); implementation.push(` console.log('Continuing: ${stepText}');`); } return implementation.join('\n'); } generatePageMethodImplementation(func, methodName) { const implementation = []; const lowerFunc = func.toLowerCase(); if (lowerFunc.includes('login')) { implementation.push(' await this.usernameInput.setValue(username);'); implementation.push(' await this.passwordInput.setValue(password);'); implementation.push(' await this.loginButton.click();'); implementation.push(' await browser.waitUntil(() => browser.getUrl().then(url => !url.includes(\'/login\')));'); } else if (lowerFunc.includes('fill') || lowerFunc.includes('enter')) { implementation.push(' // Fill form with provided data'); implementation.push(' for (const [field, value] of Object.entries(data)) {'); implementation.push(' const element = this[`${field}Input`] || this[field];'); implementation.push(' if (element) {'); implementation.push(' await element.setValue(value);'); implementation.push(' }'); implementation.push(' }'); } else if (lowerFunc.includes('submit') || lowerFunc.includes('save')) { implementation.push(' await this.submitButton.click();'); implementation.push(' await browser.waitUntil(() => this.successMessage.isDisplayed());'); } else if (lowerFunc.includes('wait')) { implementation.push(' await browser.waitUntil(() => this.pageTitle.isDisplayed());'); } else if (lowerFunc.includes('validate') || lowerFunc.includes('verify')) { implementation.push(' await expect(this.pageTitle).toBeDisplayed();'); implementation.push(' return await this.pageTitle.getText();'); } else { implementation.push(` // Implement ${func} functionality`); implementation.push(' console.log(\'Method executed successfully\');'); } return implementation.join('\n'); } extractElementFromStep(stepText, selectors = {}) { const words = stepText.toLowerCase().split(' '); // Try to find matching selector for (const [key, selector] of Object.entries(selectors)) { const keyWords = key.toLowerCase().replace(/([A-Z])/g, ' $1').trim().split(' '); if (keyWords.some(word => words.includes(word))) { return `$(\"${selector}\")`; } } // Fallback patterns if (words.includes('button')) return '$(\".btn, button, [type=submit]\")'; if (words.includes('input') || words.includes('field')) return '$("input, textarea")'; if (words.includes('username')) return '$("[data-testid=username], #username, [name=username]")'; if (words.includes('password')) return '$("[data-testid=password], #password, [name=password]")'; if (words.includes('email')) return '$("[data-testid=email], #email, [name=email]")'; if (words.includes('message')) return '$(".message, .alert, .notification")'; if (words.includes('title')) return '$("h1, h2, .title")'; return '$(".element")'; // Generic fallback } extractValueFromStep(stepText) { // Extract quoted values const quotedMatch = stepText.match(/["']([^"']+)["']/); if (quotedMatch) return quotedMatch[1]; // Extract common values if (stepText.toLowerCase().includes('valid')) return 'valid_value'; if (stepText.toLowerCase().includes('test')) return 'test_value'; return 'sample_value'; } extractUrlFromStep(stepText) { // Extract URL patterns if (stepText.includes('dashboard')) return '/dashboard'; if (stepText.includes('login')) return '/login'; if (stepText.includes('profile')) return '/profile'; if (stepText.includes('home')) return '/'; return '/page'; } async performCodeReview(content, review_criteria, repo_path, filePath) { const review = { issues: [], suggestions: [], enhanced_content: content, score: 100, improvements: [] }; try { // Get existing patterns from repository if available let repoPatterns = {}; if (repo_path) { try { const analysisResult = await this.analyzeRepositoryPatterns({ repo_path, pattern_types: ['features', 'steps', 'pages', 'components'] }); repoPatterns = JSON.parse(analysisResult.content[0].text).patterns || {}; } catch (error) { console.warn(`Could not analyze repository patterns: ${error.message}`); } } // Perform review based on criteria for (const criteria of review_criteria) { switch (criteria) { case 'docs': this.reviewDocumentation(content, review, filePath); break; case 'pom': this.reviewPageObjectModel(content, review, filePath); break; case 'functions': this.reviewFunctionQuality(content, review, filePath); break; case 'utils': this.reviewUtilityUsage(content, review, repoPatterns); break; case 'format': this.reviewCodeFormatting(content, review); break; case 'existing_steps': this.reviewStepReuse(content, review, repoPatterns); break; } } // Apply enhancements to content review.enhanced_content = this.applyEnhancements(content, review.improvements); // Calculate final score review.score = Math.max(0, 100 - (review.issues.length * 10) - (review.suggestions.length * 5)); } catch (error) { review.issues.push(`Review process failed: ${error.message}`); } return review; } reviewDocumentation(content, review, filePath) { const fileExt = path.extname(filePath); if (fileExt === '.js' || fileExt === '.ts') { // Check for JSDoc comments const functionRegex = /(?:export\s+)?(?:async\s+)?function\s+(\w+)/g; const functions = []; let match; while ((match = functionRegex.exec(content)) !== null) { functions.push(match[1]); } functions.forEach(funcName => { const jsdocPattern = new RegExp(`\\/\\*\\*[\\s\\S]*?\\*\\/\\s*(?:export\\s+)?(?:async\\s+)?function\\s+${funcName}`, 'g'); if (!jsdocPattern.test(content)) { review.issues.push(`Missing JSDoc documentation for function: ${funcName}`); review.improvements.push({ type: 'add_jsdoc', function: funcName, template: `/**\n * ${funcName} - Description needed\n * @param {*} param - Parameter description\n * @returns {*} Return description\n */` }); } }); // Check for class documentation const classMatches = content.match(/class\s+(\w+)/g); if (classMatches) { classMatches.forEach(classMatch => { const className = classMatch.replace('class ', ''); const classDocPattern = new RegExp(`\\/\\*\\*[\\s\\S]*?\\*\\/\\s*class\\s+${className}`, 'g'); if (!classDocPattern.test(content)) { review.suggestions.push(`Consider adding class documentation for: ${className}`); } }); } } } reviewPageObjectModel(content, review, filePath) { if (filePath.includes('page') || filePath.includes('Page')) { // Check for POM patterns if (!content.includes('class ')) { review.issues.push('Page Object should use class-based structure'); review.improvements.push({ type: 'convert_to_class', suggestion: 'Convert to class-based Page Object Model' }); } // Check for proper selector organization if (!content.includes('get ') && content.includes('[')) { review.suggestions.push('Consider using getter methods for selectors'); review.improvements.push({ type: 'add_getters', suggestion: 'Add getter methods for better selector encapsulation' }); } // Check for action methods const actionMethods = ['click', 'type', 'select', 'wait']; const hasActionMethods = actionMethods.some(action => content.includes(action)); if (!hasActionMethods) { review.suggestions.push('Consider adding action methods (click, type, select, wait)'); } } } reviewFunctionQuality(content, review, filePath) { // Check for function length const functions = content.match(/function[^{]*{[^}]*}/g) || []; functions.forEach(func => { const lines = func.split('\n').length; if (lines > 50) { review.issues.push('Function is too long (>50 lines) - consider breaking it down'); } }); // Check for error handling if (content.includes('await ') && !content.includes('try') && !content.includes('catch')) { review.issues.push('Async functions should include error handling (try/catch)'); review.improvements.push({ type: 'add_error_handling', suggestion: 'Wrap async operations in try/catch blocks' }); } // Check for proper return statements const functionBodies = content.match(/function[^{]*{([^}]*)}/g) || []; functionBodies.forEach(body => { if (!body.includes('return') && !body.includes('void')) { review.suggestions.push('Consider adding explicit return statements'); } }); } reviewUtilityUsage(content, review, repoPatterns) { // Check if utility functions from repo could be reused if (repoPatterns.utils) { Object.keys(repoPatterns.utils).forEach(utilFile => { const utils = repoPatterns.utils[utilFile]; if (utils.functions) { utils.functions.forEach(func => { if (content.includes(func.name) && !content.includes(`import`)) { review.suggestions.push(`Consider importing existing utility function: ${func.name} from ${utilFile}`); } }); } }); } // Check for repeated code patterns const codeBlocks = content.split('\n').filter(line => line.trim().length > 0); const duplicateLines = codeBlocks.filter((line, index) => codeBlocks.indexOf(line) !== index && line.trim().length > 10 ); if (duplicateLines.length > 0) { review.suggestions.push('Consider extracting repeated code into utility functions'); } } reviewCodeFormatting(content, review) { // Check for consistent indentation const lines = content.split('\n'); const indentationTypes = { spaces: 0, tabs: 0 }; lines.forEach(line => { if (line.startsWith(' ')) indentationTypes.spaces++; if (line.startsWith('\t')) indentationTypes.tabs++; }); if (indentationTypes.spaces > 0 && indentationTypes.tabs > 0) { review.issues.push('Inconsistent indentation - mixing spaces and tabs'); } // Check for trailing whitespace const hasTrailingWhitespace = lines.some(line => line.match(/\s+$/)); if (hasTrailingWhitespace) { review.suggestions.push('Remove trailing whitespace'); } // Check for consistent quotes const singleQuotes = (content.match(/'/g) || []).length; const doubleQuotes = (content.match(/"/g) || []).length; if (singleQuotes > 0 && doubleQuotes > 0 && Math.abs(singleQuotes - doubleQuotes) > 10) { review.suggestions.push('Consider using consistent quote style throughout the file'); } } reviewStepReuse(content, review, repoPatterns) { if (repoPatterns.steps) { // Check for existing step definitions that could be reused repoPatterns.steps.forEach(stepFile => { if (stepFile.patterns && stepFile.patterns.stepDefinitions) { stepFile.patterns.stepDefinitions.forEach(existingStep => { const stepPattern = existingStep.toLowerCase(); const contentLower = content.toLowerCase(); if (contentLower.includes('given') || contentLower.includes('when') || contentLower.includes('then')) { if (contentLower.includes(stepPattern.replace(/[()]/g, ''))) { review.suggestions.push(`Consider reusing existing step definition: ${existingStep} from ${stepFile.file}`); } } }); } }); } } applyEnhancements(content, improvements) { let enhancedContent = content; improvements.forEach(improvement => { switch (improvement.type) { case 'add_jsdoc': // Add JSDoc before function const funcPattern = new RegExp(`((?:export\\s+)?(?:async\\s+)?function\\s+${improvement.function})`, 'g'); enhancedContent = enhancedContent.replace(funcPattern, `${improvement.template}\n$1`); break; case 'add_error_handling': // This would be complex to implement automatically break; case 'convert_to_class': // This would require significant code transformation break; case 'add_getters': // This would require analyzing selectors and creating getter methods break; } }); return enhancedContent; } // String utility methods toCamelCase(str) { return str.replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) => { return index === 0 ? word.toLowerCase() : word.toUpperCase(); }).replace(/\s+/g, ''); } toPascalCase(str) { return str.replace(/(?:^\w|[A-Z]|\b\w)/g, (word) => { return word.toUpperCase(); }).replace(/\s+/g, ''); } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('Automation Script Generator MCP server running on stdio'); } } const server = new AutomationScriptGenerator(); // Export for testing export { AutomationScriptGenerator }; // Only run if this file is executed directly if (import.meta.url === `file://${process.argv[1]}`) { server.run().catch(console.error); }

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/raymondsambur/automation-script-generator'

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