index.js•86.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);
}