code-generator.jsā¢22.2 kB
/**
* Code Generator Module
* Handles all code generation for features, steps, pages, and components
*/
import fs from 'fs-extra';
import path from 'path';
export class CodeGenerator {
constructor(config) {
this.config = config;
}
// Generate complete file content based on type
async generateFileContent(type, data) {
switch (type) {
case 'feature':
return this.buildFeatureFileContent(data.scenario_title, data.gherkin_syntax, data.tags);
case 'steps':
return this.buildStepsFileContent(data.scenario_title, data.gherkin_syntax, data.selectors, data.existing_steps);
case 'page':
return this.buildPageFileContent(data.scenario_title, data.selectors, data.page_functions);
case 'component':
return this.buildComponentFileContent(data.scenario_title, data.data_items);
default:
throw new Error(`Unknown file type: ${type}`);
}
}
buildFeatureFileContent(scenario_title, gherkin_syntax, tags = []) {
const indent = this.getIndentation();
let content = '';
// Add metadata if configured
if (this.config.get('fileGeneration.includeMetadata')) {
content += `# Generated on: ${new Date().toISOString()}\n`;
content += `# Scenario: ${scenario_title}\n\n`;
}
// Extract feature name from gherkin or use scenario title
const featureMatch = gherkin_syntax.match(/Feature:\s*(.+)/);
const featureName = featureMatch ? featureMatch[1].trim() : scenario_title;
content += `Feature: ${featureName}\n`;
// Add feature-level tags
if (tags.length > 0) {
content += `\n${tags.join(' ')}\n`;
}
// Add the gherkin content (remove duplicate Feature line if present)
let gherkinContent = gherkin_syntax;
if (featureMatch) {
gherkinContent = gherkin_syntax.replace(/Feature:\s*.+\n?/, '');
}
// Ensure proper indentation
const lines = gherkinContent.split('\n');
const formattedLines = lines.map(line => {
line = line.trim();
if (!line) return '';
if (line.startsWith('Scenario') || line.startsWith('Background') || line.startsWith('Examples')) {
return `\n${indent}${line}`;
} else if (line.startsWith('@')) {
return `${indent}${line}`;
} else if (line.match(/^\s*(Given|When|Then|And|But)/)) {
return `${indent}${indent}${line}`;
} else if (line.startsWith('|')) {
return `${indent}${indent}${indent}${line}`;
} else {
return `${indent}${line}`;
}
});
content += formattedLines.join('\n');
return this.formatCode(content, 'gherkin');
}
buildStepsFileContent(scenario_title, gherkin_syntax, selectors = {}, existing_steps = []) {
const indent = this.getIndentation();
let content = '';
// Add header and imports
if (this.config.get('fileGeneration.includeMetadata')) {
content += this.generateFileHeader('Step Definitions', scenario_title);
}
content += `import { Given, When, Then } from '@wdio/cucumber-framework';\n\n`;
// Add JSDoc if configured
if (this.config.shouldEnforceJSDoc()) {
content += `/**\n * Step definitions for ${scenario_title}\n */\n\n`;
}
// Extract and generate step definitions
const steps = this.parseGherkinSteps(gherkin_syntax);
const stepDefinitions = this.generateStepDefinitions(steps, selectors, existing_steps);
content += stepDefinitions;
return this.formatCode(content, 'javascript');
}
buildPageFileContent(scenario_title, selectors = {}, page_functions = []) {
const indent = this.getIndentation();
const className = this.generateClassName(scenario_title);
let content = '';
// Add header
if (this.config.get('fileGeneration.includeMetadata')) {
content += this.generateFileHeader('Page Object', scenario_title);
}
// Add JSDoc for class
if (this.config.shouldEnforceJSDoc()) {
content += `/**\n * Page Object for ${scenario_title}\n */\n`;
}
content += `class ${className} {\n`;
// Generate selector getters
if (Object.keys(selectors).length > 0) {
content += `${indent}// Selectors\n`;
content += this.generateSelectorMethods(selectors);
content += '\n';
}
// Generate page methods
if (page_functions.length > 0) {
content += `${indent}// Actions\n`;
content += this.generatePageMethods(page_functions);
}
content += '}\n\n';
content += `export default new ${className}();\n`;
return this.formatCode(content, 'javascript');
}
buildComponentFileContent(scenario_title, data_items = {}) {
let content = '';
// Add header
if (this.config.get('fileGeneration.includeMetadata')) {
content += this.generateFileHeader('Test Data Component', scenario_title);
}
// Add JSDoc
if (this.config.shouldEnforceJSDoc()) {
content += `/**\n * Test data for ${scenario_title}\n */\n\n`;
}
// Generate data objects
Object.keys(data_items).forEach(key => {
const dataObject = data_items[key];
content += `export const ${key} = {\n`;
Object.keys(dataObject).forEach(prop => {
const value = typeof dataObject[prop] === 'string' ? `'${dataObject[prop]}'` : dataObject[prop];
content += ` ${prop}: ${value},\n`;
});
content += '};\n\n';
});
// Add utility functions for data manipulation
content += this.generateDataUtilityFunctions(data_items);
return this.formatCode(content, 'javascript');
}
generateFileHeader(fileType, scenario_title) {
const timestamp = new Date().toISOString();
return `/**\n * ${fileType} - ${scenario_title}\n * Generated on: ${timestamp}\n * \n * This file was auto-generated by MCP Automation Script Generator\n */\n\n`;
}
generateClassName(scenario_title) {
return scenario_title
.replace(/[^a-zA-Z0-9\s]/g, '')
.split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join('') + 'Page';
}
parseGherkinSteps(gherkin_syntax) {
const stepRegex = /(Given|When|Then|And|But)\s+(.+)/g;
const steps = [];
let match;
while ((match = stepRegex.exec(gherkin_syntax)) !== null) {
steps.push({
type: match[1],
text: match[2].trim(),
full: match[0].trim()
});
}
return steps;
}
generateStepDefinitions(steps, selectors = {}, existing_steps = []) {
const stepDefs = [];
const processedSteps = new Set();
steps.forEach(step => {
// Avoid duplicate step definitions
const stepSignature = `${step.type}('${step.text}'`;
if (processedSteps.has(stepSignature)) {
return;
}
processedSteps.add(stepSignature);
// Check if step already exists
const isReusable = existing_steps.some(existing =>
this.calculateStepSimilarity(step.text, existing) > this.config.getStepSimilarityThreshold()
);
if (!isReusable) {
const implementation = this.generateStepImplementation(step.full, step.text, step.type, selectors);
let stepDef = `${step.type}('${step.text}', async () => {\n`;
stepDef += implementation;
stepDef += '\n});';
stepDefs.push(stepDef);
}
});
return stepDefs.join('\n\n');
}
generateStepImplementation(step, stepText, stepType, selectors = {}) {
const indent = this.getIndentation();
const implementation = [];
const lowerStep = step.toLowerCase();
const lowerText = stepText.toLowerCase();
// Add error handling wrapper if configured
const useErrorHandling = this.config.get('codeQuality.enforceErrorHandling');
if (useErrorHandling) {
implementation.push(`${indent}try {`);
}
const baseIndent = useErrorHandling ? indent + indent : indent;
// Generate implementation based on step type and content
if (stepType === 'Given') {
this.generateGivenImplementation(implementation, lowerText, baseIndent, selectors);
} else if (stepType === 'When') {
this.generateWhenImplementation(implementation, lowerText, baseIndent, selectors, stepText);
} else if (stepType === 'Then') {
this.generateThenImplementation(implementation, lowerText, baseIndent, selectors, stepText);
} else {
// Handle And/But by analyzing context
implementation.push(`${baseIndent}// Continue previous action pattern`);
implementation.push(`${baseIndent}console.log('Continuing: ${stepText}');`);
}
if (useErrorHandling) {
implementation.push(`${indent}} catch (error) {`);
implementation.push(`${indent}${indent}console.error('Step failed:', error);`);
implementation.push(`${indent}${indent}throw error;`);
implementation.push(`${indent}}`);
}
return implementation.join('\n');
}
generateGivenImplementation(implementation, lowerText, indent, selectors) {
if (lowerText.includes('on') && (lowerText.includes('page') || lowerText.includes('login') || lowerText.includes('home'))) {
implementation.push(`${indent}await browser.url('/');`);
implementation.push(`${indent}await browser.waitUntil(() => browser.getTitle().then(title => title.length > 0));`);
} else if (lowerText.includes('logged in') || lowerText.includes('authenticated')) {
implementation.push(`${indent}// Assume user is already logged in - set up session`);
implementation.push(`${indent}await browser.setCookies([{ name: 'session', value: 'authenticated-user' }]);`);
} else if (lowerText.includes('data') || lowerText.includes('exists')) {
implementation.push(`${indent}// Setup test data`);
implementation.push(`${indent}console.log('Setting up test data...');`);
} else {
implementation.push(`${indent}// Setup precondition`);
implementation.push(`${indent}console.log('Setting up precondition');`);
}
}
generateWhenImplementation(implementation, lowerText, indent, selectors, stepText) {
if (lowerText.includes('click') || lowerText.includes('press')) {
const element = this.extractElementFromStep(stepText, selectors);
implementation.push(`${indent}await ${element}.waitForClickable();`);
implementation.push(`${indent}await ${element}.click();`);
} else if (lowerText.includes('enter') || lowerText.includes('type') || lowerText.includes('fill') || lowerText.includes('input')) {
const element = this.extractElementFromStep(stepText, selectors);
const value = this.extractValueFromStep(stepText);
implementation.push(`${indent}await ${element}.waitForDisplayed();`);
implementation.push(`${indent}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(`${indent}await ${element}.waitForDisplayed();`);
implementation.push(`${indent}await ${element}.selectByVisibleText('${value}');`);
} else if (lowerText.includes('navigate') || lowerText.includes('go to')) {
const url = this.extractUrlFromStep(stepText);
implementation.push(`${indent}await browser.url('${url}');`);
} else if (lowerText.includes('wait')) {
const element = this.extractElementFromStep(stepText, selectors);
implementation.push(`${indent}await ${element}.waitForDisplayed();`);
} else {
implementation.push(`${indent}// Perform action`);
implementation.push(`${indent}console.log('Executing action');`);
}
}
generateThenImplementation(implementation, lowerText, indent, selectors, stepText) {
if (lowerText.includes('see') || lowerText.includes('displayed') || lowerText.includes('visible')) {
const element = this.extractElementFromStep(stepText, selectors);
implementation.push(`${indent}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(`${indent}await expect(${element}).toHaveText('${expectedText}');`);
} else if (lowerText.includes('redirect') || lowerText.includes('url')) {
const expectedUrl = this.extractUrlFromStep(stepText);
implementation.push(`${indent}await expect(browser).toHaveUrl('${expectedUrl}');`);
} else if (lowerText.includes('enabled') || lowerText.includes('clickable')) {
const element = this.extractElementFromStep(stepText, selectors);
implementation.push(`${indent}await expect(${element}).toBeEnabled();`);
} else if (lowerText.includes('count') || lowerText.includes('number')) {
const element = this.extractElementFromStep(stepText, selectors);
implementation.push(`${indent}const elements = await $$(${element});`);
implementation.push(`${indent}await expect(elements).toHaveLength(expectedCount);`);
} else {
implementation.push(`${indent}// Verify result`);
implementation.push(`${indent}console.log('Verifying result');`);
}
}
generateSelectorMethods(selectors) {
const indent = this.getIndentation();
return Object.keys(selectors).map(key => {
const selectorValue = selectors[key];
const methodName = this.toCamelCase(key);
let method = `${indent}get ${methodName}() {\n`;
method += `${indent}${indent}return $('${selectorValue}');\n`;
method += `${indent}}\n`;
return method;
}).join('\n');
}
generatePageMethods(page_functions) {
const indent = this.getIndentation();
return page_functions.map(func => {
const methodName = this.toCamelCase(func);
let method = '';
if (this.config.shouldEnforceJSDoc()) {
method += `${indent}/**\n${indent} * ${func}\n${indent} */\n`;
}
method += `${indent}async ${methodName}() {\n`;
method += this.generatePageMethodImplementation(func, methodName);
method += `${indent}}\n`;
return method;
}).join('\n\n');
}
generatePageMethodImplementation(func, methodName) {
const indent = this.getIndentation();
const implementation = [];
const lowerFunc = func.toLowerCase();
if (lowerFunc.includes('login')) {
implementation.push(`${indent}${indent}await this.usernameInput.setValue(username);`);
implementation.push(`${indent}${indent}await this.passwordInput.setValue(password);`);
implementation.push(`${indent}${indent}await this.loginButton.click();`);
implementation.push(`${indent}${indent}await browser.waitUntil(() => browser.getUrl().then(url => !url.includes('/login')));`);
} else if (lowerFunc.includes('fill') || lowerFunc.includes('enter')) {
implementation.push(`${indent}${indent}// Fill form with provided data`);
implementation.push(`${indent}${indent}for (const [field, value] of Object.entries(data)) {`);
implementation.push(`${indent}${indent}${indent}const element = this[\`\${field}Input\`] || this[field];`);
implementation.push(`${indent}${indent}${indent}if (element) {`);
implementation.push(`${indent}${indent}${indent}${indent}await element.setValue(value);`);
implementation.push(`${indent}${indent}${indent}}`);
implementation.push(`${indent}${indent}}`);
} else if (lowerFunc.includes('submit') || lowerFunc.includes('save')) {
implementation.push(`${indent}${indent}await this.submitButton.click();`);
implementation.push(`${indent}${indent}await browser.waitUntil(() => this.successMessage.isDisplayed());`);
} else if (lowerFunc.includes('wait')) {
implementation.push(`${indent}${indent}await browser.waitUntil(() => this.pageTitle.isDisplayed());`);
} else if (lowerFunc.includes('validate') || lowerFunc.includes('verify')) {
implementation.push(`${indent}${indent}await expect(this.pageTitle).toBeDisplayed();`);
implementation.push(`${indent}${indent}return await this.pageTitle.getText();`);
} else {
implementation.push(`${indent}${indent}// Implement ${func} functionality`);
implementation.push(`${indent}${indent}console.log('Method executed successfully');`);
}
return implementation.join('\n');
}
generateDataUtilityFunctions(data_items) {
let content = '// Utility functions for test data\n\n';
content += `export function getRandomTestData(dataSet) {\n`;
content += ` const keys = Object.keys(dataSet);\n`;
content += ` const randomKey = keys[Math.floor(Math.random() * keys.length)];\n`;
content += ` return dataSet[randomKey];\n`;
content += `}\n\n`;
content += `export function validateTestData(data, requiredFields = []) {\n`;
content += ` return requiredFields.every(field => data.hasOwnProperty(field) && data[field] !== null);\n`;
content += `}\n`;
return content;
}
// Utility methods
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")';
}
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';
}
calculateStepSimilarity(step1, step2) {
const normalized1 = this.normalizeStepText(step1);
const normalized2 = this.normalizeStepText(step2);
const words1 = normalized1.split(' ');
const words2 = normalized2.split(' ');
const commonWords = words1.filter(word => words2.includes(word));
const totalWords = Math.max(words1.length, words2.length);
return commonWords.length / totalWords;
}
normalizeStepText(stepText) {
return stepText
.toLowerCase()
.replace(/['"]/g, '')
.replace(/\d+/g, 'NUMBER')
.replace(/[^\w\s]/g, ' ')
.replace(/\s+/g, ' ')
.trim();
}
toCamelCase(str) {
return str
.replace(/[^a-zA-Z0-9]/g, ' ')
.split(' ')
.map((word, index) =>
index === 0
? word.toLowerCase()
: word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
)
.join('');
}
getIndentation() {
const config = this.config.getIndentationConfig();
const char = config.type === 'tabs' ? '\t' : ' ';
return char.repeat(config.type === 'tabs' ? 1 : config.size);
}
formatCode(content, language) {
// Apply basic formatting based on configuration
const lines = content.split('\n');
const formattedLines = lines.map(line => {
// Remove trailing whitespace
return line.trimEnd();
});
// Add consistent line endings
return formattedLines.join('\n').trim() + '\n';
}
}