Skip to main content
Glama

Automation Script Generator MCP Server

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'; } }

Latest Blog Posts

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