Skip to main content
Glama

MCP Cypress Page Object & Test Generator

by jprealini
index.js35.8 kB
#!/usr/bin/env node import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" import { z } from "zod" import puppeteer from 'puppeteer' import * as cheerio from 'cheerio' import fs from 'fs/promises' import path from 'path' // Create the MCP server instance const server = new McpServer({ name: "Cypress Generator MCP", version: "1.0.0" }) // Here starts code generated using Copilot: // Utility functions for file operations class CypressFileManager { constructor() { this.workspaceRoot = null this.cypressConfig = null } async detectWorkspace(startPath = process.cwd()) { let currentPath = startPath while (currentPath !== path.dirname(currentPath)) { const cypressConfigJs = path.join(currentPath, 'cypress.config.js') const cypressConfigTs = path.join(currentPath, 'cypress.config.ts') const packageJson = path.join(currentPath, 'package.json') if (await this.fileExists(cypressConfigJs) || await this.fileExists(cypressConfigTs)) { this.workspaceRoot = currentPath await this.loadCypressConfig(currentPath) return currentPath } // Also check if it's a Node.js project with Cypress in dependencies if (await this.fileExists(packageJson)) { try { const packageContent = await fs.readFile(packageJson, 'utf8') const packageData = JSON.parse(packageContent) const hasCypress = packageData.dependencies?.cypress || packageData.devDependencies?.cypress || packageData.dependencies?.['@cypress/react'] || packageData.devDependencies?.['@cypress/react'] if (hasCypress) { this.workspaceRoot = currentPath return currentPath } } catch (error) { // Continue searching } } currentPath = path.dirname(currentPath) } throw new Error('No valid Cypress project found. Make sure you are in a directory with cypress.config.js/ts or a package.json with Cypress as a dependency.') } async loadCypressConfig(workspaceRoot) { const configJsPath = path.join(workspaceRoot, 'cypress.config.js') const configTsPath = path.join(workspaceRoot, 'cypress.config.ts') try { let configPath = null if (await this.fileExists(configJsPath)) { configPath = configJsPath } else if (await this.fileExists(configTsPath)) { configPath = configTsPath } if (configPath) { // For now, we'll use default paths. In a more sophisticated version, // we could dynamically import and parse the config this.cypressConfig = { e2e: { specPattern: 'cypress/e2e/**/*.cy.{js,ts}', supportFile: 'cypress/support/e2e.js' }, component: { specPattern: 'cypress/component/**/*.cy.{js,ts}' } } } } catch (error) { console.warn('Could not load Cypress config, using defaults:', error.message) this.cypressConfig = { e2e: { specPattern: 'cypress/e2e/**/*.cy.{js,ts}', supportFile: 'cypress/support/e2e.js' } } } } async fileExists(filePath) { try { await fs.access(filePath) return true } catch { return false } } async ensureDirectoryStructure(workspaceRoot) { const directories = [ path.join(workspaceRoot, 'cypress'), path.join(workspaceRoot, 'cypress', 'pages'), path.join(workspaceRoot, 'cypress', 'e2e'), path.join(workspaceRoot, 'cypress', 'e2e', 'tests'), path.join(workspaceRoot, 'cypress', 'support'), path.join(workspaceRoot, 'cypress', 'fixtures') ] for (const dir of directories) { await fs.mkdir(dir, { recursive: true }) } } async createPageObject(workspaceRoot, url, pageObjectMeta) { const { featureName, classCode } = pageObjectMeta const fileName = `${featureName}.js` const filePath = path.join(workspaceRoot, 'cypress', 'pages', fileName) if (await this.fileExists(filePath)) { const timestamp = new Date().toISOString().replace(/[:.]/g, '-') const backupPath = path.join(workspaceRoot, 'cypress', 'pages', `${featureName}.backup.${timestamp}.js`) await fs.copyFile(filePath, backupPath) } await fs.writeFile(filePath, classCode, 'utf8') return filePath } async createTestFile(workspaceRoot, url, testCode, featureName) { const fileName = `${featureName}.cy.js` const filePath = path.join(workspaceRoot, 'cypress', 'e2e', 'tests', fileName) if (await this.fileExists(filePath)) { const timestamp = new Date().toISOString().replace(/[:.]/g, '-') const backupPath = path.join(workspaceRoot, 'cypress', 'e2e', 'tests', `${featureName}.backup.${timestamp}.cy.js`) await fs.copyFile(filePath, backupPath) } await fs.writeFile(filePath, testCode, 'utf8') return filePath } sanitizeFileName(name) { return name.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase() } async createIndexFile(workspaceRoot) { const indexPath = path.join(workspaceRoot, 'cypress', 'pages', 'index.js') // Get all page object files const pagesDir = path.join(workspaceRoot, 'cypress', 'pages') const files = await fs.readdir(pagesDir) const pageFiles = files.filter(file => file.endsWith('.js') && file !== 'index.js') const imports = pageFiles.map(file => { const className = file.replace('.js', '') return `export { ${className} } from './${file.replace('.js', '')}'` }).join('\n') const indexContent = `// Auto-generated index file for page objects ${imports} ` await fs.writeFile(indexPath, indexContent, 'utf8') return indexPath } } // Function to generate Cypress Page Object class with locators and action methods function generatePageObjectClass($, url, customFeatureName = null) { const featureName = customFeatureName || getFeatureName($, url) const className = featureName.charAt(0).toUpperCase() + featureName.slice(1) + 'Page' const elements = [] const getters = [] const valueGetters = [] const interactionMethods = [] const workflowMethods = [] // Removed bulk generic methods per user request; we keep individual element action methods only. let elementCounter = 1 // Track generated element names for test consistency and generic grouping const elementMeta = [] // BUTTONS $('button').each((_, element) => { const $el = $(element) const text = $el.text().trim() const id = $el.attr('id') const classNameAttr = $el.attr('class') const dataTestId = $el.attr('data-testid') let locator = '' let elementName = '' if (dataTestId) { locator = `cy.get('[data-testid="${dataTestId}"]')` elementName = `button_${dataTestId.replace(/[^a-zA-Z0-9]/g, '_')}` } else if (id) { locator = `cy.get('#${id}')` elementName = `button_${id}` } else if (text) { locator = `cy.contains('button', '${text}')` elementName = `button_${text.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase()}` } else if (classNameAttr) { locator = `cy.get('button.${classNameAttr.split(' ')[0]}')` elementName = `button_${classNameAttr.split(' ')[0]}` } else { locator = `cy.get('button').eq(${elementCounter - 1})` elementName = `button_${elementCounter}` } elements.push(` ${elementName}: () => ${locator}`) getters.push(` get ${elementName.charAt(0).toUpperCase() + elementName.slice(1)}() { return this.#elements.${elementName}() }`) interactionMethods.push(` click${elementName.charAt(0).toUpperCase() + elementName.slice(1)}() { return this.#elements.${elementName}().click() }`) valueGetters.push(` getText${elementName.charAt(0).toUpperCase() + elementName.slice(1)}() { return this.#elements.${elementName}().invoke('text') }`) elementMeta.push({ type: 'button', elementName }) elementCounter++ }) // INPUTS $('input').each((_, element) => { const $el = $(element) const type = $el.attr('type') || 'text' const id = $el.attr('id') const name = $el.attr('name') const placeholder = $el.attr('placeholder') const dataTestId = $el.attr('data-testid') let locator = '' let elementName = '' if (dataTestId) { locator = `cy.get('[data-testid="${dataTestId}"]')` elementName = `input_${dataTestId.replace(/[^a-zA-Z0-9]/g, '_')}` } else if (id) { locator = `cy.get('#${id}')` elementName = `input_${id}` } else if (name) { locator = `cy.get('input[name="${name}"]')` elementName = `input_${name}` } else if (placeholder) { locator = `cy.get('input[placeholder="${placeholder}"]')` elementName = `input_${placeholder.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase()}` } else { locator = `cy.get('input[type="${type}"]').eq(${elementCounter - 1})` elementName = `input_${type}_${elementCounter}` } elements.push(` ${elementName}: () => ${locator}`) getters.push(` get ${elementName.charAt(0).toUpperCase() + elementName.slice(1)}() { return this.#elements.${elementName}() }`) if (type === 'checkbox' || type === 'radio') { interactionMethods.push(` check${elementName.charAt(0).toUpperCase() + elementName.slice(1)}() { return this.#elements.${elementName}().check() }`) interactionMethods.push(` uncheck${elementName.charAt(0).toUpperCase() + elementName.slice(1)}() { return this.#elements.${elementName}().uncheck() }`) valueGetters.push(` isChecked${elementName.charAt(0).toUpperCase() + elementName.slice(1)}() { return this.#elements.${elementName}().should('have.prop', 'checked') }`) } else { interactionMethods.push(` type${elementName.charAt(0).toUpperCase() + elementName.slice(1)}(text) { return this.#elements.${elementName}().type(text) }`) interactionMethods.push(` clear${elementName.charAt(0).toUpperCase() + elementName.slice(1)}() { return this.#elements.${elementName}().clear() }`) valueGetters.push(` getValue${elementName.charAt(0).toUpperCase() + elementName.slice(1)}() { return this.#elements.${elementName}().invoke('val') }`) } elementMeta.push({ type, elementName }) elementCounter++ }) // LINKS $('a').each((_, element) => { const $el = $(element) const text = $el.text().trim() const href = $el.attr('href') const id = $el.attr('id') const dataTestId = $el.attr('data-testid') let locator = '' let elementName = '' if (dataTestId) { locator = `cy.get('[data-testid="${dataTestId}"]')` elementName = `link_${dataTestId.replace(/[^a-zA-Z0-9]/g, '_')}` } else if (id) { locator = `cy.get('#${id}')` elementName = `link_${id}` } else if (text) { locator = `cy.contains('a', '${text}')` elementName = `link_${text.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase()}` } else if (href) { locator = `cy.get('a[href="${href}"]')` elementName = `link_${href.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase()}` } else { locator = `cy.get('a').eq(${elementCounter - 1})` elementName = `link_${elementCounter}` } elements.push(` ${elementName}: () => ${locator}`) getters.push(` get ${elementName.charAt(0).toUpperCase() + elementName.slice(1)}() { return this.#elements.${elementName}() }`) interactionMethods.push(` click${elementName.charAt(0).toUpperCase() + elementName.slice(1)}() { return this.#elements.${elementName}().click() }`) valueGetters.push(` getText${elementName.charAt(0).toUpperCase() + elementName.slice(1)}() { return this.#elements.${elementName}().invoke('text') }`) elementMeta.push({ type: 'link', elementName }) elementCounter++ }) // SELECTS $('select').each((_, element) => { const $el = $(element) const id = $el.attr('id') const name = $el.attr('name') const dataTestId = $el.attr('data-testid') let locator = '' let elementName = '' if (dataTestId) { locator = `cy.get('[data-testid="${dataTestId}"]')` elementName = `select_${dataTestId.replace(/[^a-zA-Z0-9]/g, '_')}` } else if (id) { locator = `cy.get('#${id}')` elementName = `select_${id}` } else if (name) { locator = `cy.get('select[name="${name}"]')` elementName = `select_${name}` } else { locator = `cy.get('select').eq(${elementCounter - 1})` elementName = `select_${elementCounter}` } elements.push(` ${elementName}: () => ${locator}`) getters.push(` get ${elementName.charAt(0).toUpperCase() + elementName.slice(1)}() { return this.#elements.${elementName}() }`) interactionMethods.push(` select${elementName.charAt(0).toUpperCase() + elementName.slice(1)}(value) { return this.#elements.${elementName}().select(value) }`) valueGetters.push(` getValue${elementName.charAt(0).toUpperCase() + elementName.slice(1)}() { return this.#elements.${elementName}().invoke('val') }`) elementMeta.push({ type: 'select', elementName }) elementCounter++ }) // TEXTAREAS $('textarea').each((_, element) => { const $el = $(element) const id = $el.attr('id') const name = $el.attr('name') const placeholder = $el.attr('placeholder') const dataTestId = $el.attr('data-testid') let locator = '' let elementName = '' if (dataTestId) { locator = `cy.get('[data-testid="${dataTestId}"]')` elementName = `textarea_${dataTestId.replace(/[^a-zA-Z0-9]/g, '_')}` } else if (id) { locator = `cy.get('#${id}')` elementName = `textarea_${id}` } else if (name) { locator = `cy.get('textarea[name="${name}"]')` elementName = `textarea_${name}` } else if (placeholder) { locator = `cy.get('textarea[placeholder="${placeholder}"]')` elementName = `textarea_${placeholder.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase()}` } else { locator = `cy.get('textarea').eq(${elementCounter - 1})` elementName = `textarea_${elementCounter}` } elements.push(` ${elementName}: () => ${locator}`) getters.push(` get ${elementName.charAt(0).toUpperCase() + elementName.slice(1)}() { return this.#elements.${elementName}() }`) interactionMethods.push(` type${elementName.charAt(0).toUpperCase() + elementName.slice(1)}(text) { return this.#elements.${elementName}().type(text) }`) interactionMethods.push(` clear${elementName.charAt(0).toUpperCase() + elementName.slice(1)}() { return this.#elements.${elementName}().clear() }`) valueGetters.push(` getValue${elementName.charAt(0).toUpperCase() + elementName.slice(1)}() { return this.#elements.${elementName}().invoke('val') }`) elementMeta.push({ type: 'textarea', elementName }) elementCounter++ }) // (Meta map previously used for bulk methods retained only if needed in future; not required now.) const metaMap = elementMeta.reduce((acc, m) => { acc[m.elementName] = m.type; return acc }, {}) // Generate workflow methods based on detected elements const hasLoginForm = $('form').length > 0 && ($('input[type="password"]').length > 0 || $('input[name*="password"]').length > 0) const hasSearchForm = $('input[type="search"]').length > 0 || $('input[placeholder*="search"]').length > 0 const hasSubmitButton = $('button[type="submit"]').length > 0 || $('input[type="submit"]').length > 0 const hasRegistrationForm = $('form').filter((_, f) => { const txt = $(f).text().toLowerCase() return txt.includes('register') || txt.includes('signup') || txt.includes('create account') || txt.includes('sign up') }).length > 0 if (hasLoginForm) { workflowMethods.push(` // Login workflow login(username, password) { const usernameInput = this.getInputUsername ? this.getInputUsername() : this.getInputEmail() const passwordInput = this.getInputPassword() const submitButton = this.getButtonSubmit ? this.getButtonSubmit() : this.getButtonLogin() if (usernameInput) usernameInput.type(username) if (passwordInput) passwordInput.type(password) if (submitButton) submitButton.click() return this }`) } if (hasSearchForm) { workflowMethods.push(` // Search workflow search(query) { const searchInput = this.getInputSearch ? this.getInputSearch() : this.getInputQuery() const searchButton = this.getButtonSearch ? this.getButtonSearch() : this.getButtonSubmit() if (searchInput) searchInput.type(query) if (searchButton) searchButton.click() return this }`) } if (hasRegistrationForm) { workflowMethods.push(` // Registration workflow register(user, email, password) { const userInput = this.getInputUsername ? this.getInputUsername() : (this.getInputUser ? this.getInputUser() : (this.getInputName ? this.getInputName() : this.getInputEmail ? this.getInputEmail() : null)) const emailInput = this.getInputEmail ? this.getInputEmail() : null const passwordInput = this.getInputPassword ? this.getInputPassword() : (this.getInputPass ? this.getInputPass() : null) const submitButton = this.getButtonRegister ? this.getButtonRegister() : (this.getButtonSignup ? this.getButtonSignup() : (this.getButtonSubmit ? this.getButtonSubmit() : null)) if (userInput) userInput.type(user) if (emailInput && email) emailInput.type(email) if (passwordInput) passwordInput.type(password) if (submitButton) submitButton.click() return this }`) } // Add common workflow methods workflowMethods.push(` // Navigation workflow navigateToHome() { const homeLink = this.getLinkHome ? this.getLinkHome() : this.getLinkLogo() if (homeLink) homeLink.click() return this } // Form submission workflow submitForm() { const submitButton = this.getButtonSubmit ? this.getButtonSubmit() : this.getButtonLogin() if (submitButton) submitButton.click() return this } // Wait for page load waitForPageLoad() { cy.wait(1000) // Adjust as needed return this } // Verify page loaded verifyPageLoaded() { cy.url().should('include', '${new URL(url).hostname}') return this }`) return { classCode: `export class ${className} {\n // Private elements\n #elements = {\n${elements.join(',\n')}\n }\n\n // Element meta (currently not used for bulk actions)\n #meta = ${JSON.stringify(metaMap, null, 2)}\n\n // Public getters\n${getters.join('\n')}\n\n // Value/State getters\n${valueGetters.join('\n')}\n\n // Interaction methods (per-element actions)\n${interactionMethods.join('\n')}\n\n // Workflow methods\n${workflowMethods.join('\n')}\n}\n`, className, featureName, elementMeta } } // Helper function to generate class name from URL function generateClassName(url) { const urlObj = new URL(url) const hostname = urlObj.hostname.replace(/[^a-zA-Z0-9]/g, '') const pathname = urlObj.pathname.replace(/[^a-zA-Z0-9]/g, '') let className = hostname.charAt(0).toUpperCase() + hostname.slice(1) if (pathname && pathname !== '/') { className += pathname.charAt(0).toUpperCase() + pathname.slice(1) } return `${className}Page` } // Utility: Infer a feature/page name from HTML or URL function getFeatureName($, url) { // Try form name/id let name = $('form').attr('name') || $('form').attr('id') if (name) return sanitizeFeatureName(name) // Try legend inside form let legend = $('form legend').first().text().trim() if (legend) return sanitizeFeatureName(legend) // Try h1/h2 let h1 = $('h1').first().text().trim() if (h1) return sanitizeFeatureName(h1) let h2 = $('h2').first().text().trim() if (h2) return sanitizeFeatureName(h2) // Try page title let title = $('title').first().text().trim() if (title) return sanitizeFeatureName(title) // Try common keywords in URL or path const urlObj = new URL(url) const pathParts = urlObj.pathname.split('/').filter(Boolean) const keywords = ['login', 'register', 'signup', 'signin', 'user', 'profile', 'dashboard', 'settings', 'admin', 'account', 'reset', 'forgot', 'password', 'contact', 'about', 'home'] for (const part of pathParts) { for (const keyword of keywords) { if (part.toLowerCase().includes(keyword)) { return keyword } } } for (const keyword of keywords) { if (urlObj.hostname.toLowerCase().includes(keyword)) { return keyword } } // Fallback: use hostname + first path part if (pathParts.length > 0) { return sanitizeFeatureName(pathParts[0]) } return 'page' } function sanitizeFeatureName(name) { return name.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '') } // Function to generate Cypress test cases function generateCypressTests($, pageObjectMeta, url) { const { className, featureName, elementMeta } = pageObjectMeta const elementTests = [] const formTests = [] const workflowTests = [] // Map elementName to type for easy lookup const elementTypeMap = Object.fromEntries(elementMeta.map(e => [e.elementName, e.type])) // Helper to capitalize const cap = s => s.charAt(0).toUpperCase() + s.slice(1) // --- Element-level tests --- for (const meta of elementMeta) { const { type, elementName } = meta if (type === 'button' || type === 'link') { elementTests.push(`\n it('should click ${elementName}', () => {\n page.click${cap(elementName)}()\n // Add assertions based on expected behavior\n })`) elementTests.push(`\n it('should get text of ${elementName}', () => {\n page.getText${cap(elementName)}().should('be.a', 'string')\n })`) } else if (type === 'checkbox' || type === 'radio') { elementTests.push(`\n it('should check ${elementName}', () => {\n page.check${cap(elementName)}()\n page.isChecked${cap(elementName)}()\n })`) elementTests.push(`\n it('should uncheck ${elementName}', () => {\n page.uncheck${cap(elementName)}()\n // Should be unchecked\n })`) } else if (type === 'select' || type === 'textarea' || type === 'input') { elementTests.push(`\n it('should type in ${elementName}', () => {\n page.type${cap(elementName)}('test input')\n page.getValue${cap(elementName)}().should('eq', 'test input')\n })`) elementTests.push(`\n it('should clear ${elementName}', () => {\n page.type${cap(elementName)}('test')\n page.clear${cap(elementName)}()\n page.getValue${cap(elementName)}().should('eq', '')\n })`) } } // --- Form-level tests --- $('form').each((formIdx, form) => { const $form = $(form) // Find all input/select/textarea in this form const fields = [] $form.find('input,select,textarea').each((_, el) => { const $el = $(el) let base = '' if ($el.is('input')) { const type = $el.attr('type') || 'text' if (type === 'checkbox' || type === 'radio') { base = `input_${$el.attr('id') || $el.attr('name') || $el.attr('data-testid') || type}` } else { base = `input_${$el.attr('id') || $el.attr('name') || $el.attr('data-testid') || type}` } } else if ($el.is('select')) { base = `select_${$el.attr('id') || $el.attr('name') || $el.attr('data-testid') || 'select'}` } else if ($el.is('textarea')) { base = `textarea_${$el.attr('id') || $el.attr('name') || $el.attr('data-testid') || 'textarea'}` } // Normalize base = base.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase() // Find the closest match in elementMeta const match = elementMeta.find(e => e.elementName.startsWith(base)) if (match) { fields.push({ ...match, elementName: match.elementName }) } }) // Find submit button in form let submitBtn = null $form.find('button[type="submit"],input[type="submit"]').each((_, el) => { const $el = $(el) let base = '' if ($el.is('button')) { base = `button_${$el.attr('id') || $el.text().trim().replace(/[^a-zA-Z0-9]/g, '_').toLowerCase() || 'submit'}` } else { base = `input_${$el.attr('id') || $el.attr('name') || $el.attr('data-testid') || 'submit'}` } base = base.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase() const match = elementMeta.find(e => e.elementName.startsWith(base)) if (match) submitBtn = match.elementName }) // --- Positive test --- if (fields.length && submitBtn) { const validData = fields.map(f => { if (f.type === 'checkbox' || f.type === 'radio') return `page.check${cap(f.elementName)}()` return `page.type${cap(f.elementName)}('valid_${f.elementName}')` }).join('\n ') formTests.push(`\n it('should submit form with valid data', () => {\n ${validData}\n page.click${cap(submitBtn)}()\n // Add assertions for successful submission\n })`) } // --- Negative tests: missing required fields --- fields.forEach(f => { if (f.type === 'checkbox' || f.type === 'radio') { formTests.push(`\n it('should show error if ${f.elementName} is not checked', () => {\n // Do not check\n page.click${cap(submitBtn)}()\n // Add assertions for error\n })`) } else { formTests.push(`\n it('should show error if ${f.elementName} is empty', () => {\n ${fields.filter(ff => ff.elementName !== f.elementName).map(ff => ff.type === 'checkbox' || ff.type === 'radio' ? `page.check${cap(ff.elementName)}()` : `page.type${cap(ff.elementName)}('valid_${ff.elementName}')`).join('\n ')}\n // Leave ${f.elementName} empty\n page.click${cap(submitBtn)}()\n // Add assertions for error\n })`) } }) // --- Edge case tests --- fields.forEach(f => { if (f.type !== 'checkbox' && f.type !== 'radio') { formTests.push(`\n it('should handle long input for ${f.elementName}', () => {\n page.type${cap(f.elementName)}('a'.repeat(1000))\n page.click${cap(submitBtn)}()\n // Add assertions for edge case\n })`) formTests.push(`\n it('should handle special characters for ${f.elementName}', () => {\n page.type${cap(f.elementName)}('!@#$%^&*()_+-=[]{}|;:,.<>?')\n page.click${cap(submitBtn)}()\n // Add assertions for edge case\n })`) } }) }) // --- Workflow tests (login, register, search, etc) --- // If the page object has a login/search/register method, generate workflow tests if (typeof pageObjectMeta.classCode === 'string') { if (/login\s*\(/.test(pageObjectMeta.classCode)) { workflowTests.push(`\n describe('Login Workflow', () => {\n it('should login with valid credentials', () => {\n page.login('validuser', 'validpassword')\n // Add assertions for successful login\n })\n it('should show error with invalid credentials', () => {\n page.login('invaliduser', 'wrongpassword')\n // Add assertions for error\n })\n it('should show error with empty username', () => {\n page.login('', 'password')\n // Add assertions for error\n })\n it('should show error with empty password', () => {\n page.login('username', '')\n // Add assertions for error\n })\n })`) } if (/search\s*\(/.test(pageObjectMeta.classCode)) { workflowTests.push(`\n describe('Search Workflow', () => {\n it('should search with valid query', () => {\n page.search('test query')\n // Add assertions for search results\n })\n it('should handle empty search query', () => {\n page.search('')\n // Add assertions for empty search\n })\n })`) } if (/register\s*\(/.test(pageObjectMeta.classCode)) { workflowTests.push(`\n describe('Register Workflow', () => {\n it('should register with valid data', () => {\n page.register('user', 'email@example.com', 'password')\n // Add assertions for successful registration\n })\n it('should show error with invalid data', () => {\n page.register('', '', '')\n // Add assertions for error\n })\n })`) } } // --- Compose the test file --- return `import { ${className} } from '../pages/${featureName}'\n\ndescribe('${className} Tests', () => {\n let page\n beforeEach(() => {\n cy.visit('${url}')\n page = new ${className}()\n })\n describe('Element Interactions', () => {\n${elementTests.join('\n')}\n })\n describe('Form Submission', () => {\n${formTests.join('\n')}\n })\n${workflowTests.join('\n')}\n // Add more negative/error/edge case tests as needed\n})` } // Copilot generated code ends here // Tools (updated to registerTool API replacing deprecated server.tool style) server.registerTool( 'createCypressFiles', { title: 'Create Cypress Files', description: 'Create Page Object and Test files directly in the Cypress project', inputSchema: { url: z.string().describe('URL of the web page'), workspacePath: z.string().optional().describe('Workspace path (optional, it is detected automatically if not provided)'), pageObjectName: z.string().optional().describe('Custom name for the page object (optional)') } }, async ({ url, workspacePath, pageObjectName }) => { const fileManager = new CypressFileManager() try { const workspaceRoot = await fileManager.detectWorkspace(workspacePath) await fileManager.ensureDirectoryStructure(workspaceRoot) const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox'] }) const page = await browser.newPage() await page.goto(url, { waitUntil: 'networkidle2' }) const html = await page.content() await browser.close() const $ = cheerio.load(html) const pageObjectMeta = generatePageObjectClass($, url, pageObjectName) const cypressTests = generateCypressTests($, pageObjectMeta, url) const pageObjectPath = await fileManager.createPageObject(workspaceRoot, url, pageObjectMeta) const testFilePath = await fileManager.createTestFile(workspaceRoot, url, cypressTests, pageObjectMeta.featureName) const indexPath = await fileManager.createIndexFile(workspaceRoot) return { content: [ { type: 'text', text: `✅ Files created successfully:\n\n📄 Page Object: ${pageObjectPath}\n🧪 Test File: ${testFilePath}\n📋 Index File: ${indexPath}\n\nWorkspace detected: ${workspaceRoot}\n\nNow you can import the page object in your tests using:\nimport { ${pageObjectMeta.className} } from '../pages/${pageObjectMeta.featureName}'` } ] } } catch (error) { return { content: [ { type: 'text', text: `❌ Error creating Cypress files: ${error instanceof Error ? error.message : 'Unknown error'}\n\nMake sure you are in a directory with a valid Cypress project.` } ] } } } ) server.registerTool( 'generateLocator', { title: 'Generate Locator & Tests', description: 'Generate Cypress Page Object and Tests', inputSchema: { url: z.string().describe('Web page URL') } }, async ({ url }) => { try { const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox'] }) const page = await browser.newPage() await page.goto(url, { waitUntil: 'networkidle2' }) const html = await page.content() await browser.close() const $ = cheerio.load(html) const pageObjectMeta = generatePageObjectClass($, url) const className = generateClassName(url) const cypressTests = generateCypressTests($, pageObjectMeta, url) return { content: [ { type: 'text', text: `// ===== PAGE OBJECT CLASS =====\n// Save this as: ${className}.ts\n\n${pageObjectMeta.classCode}\n\n// ===== CYPRESS TESTS =====\n// Save this as: ${className}.cy.ts\n\n${cypressTests}` } ] } } catch (error) { return { content: [ { type: 'text', text: `Error generating Page Object and Tests: ${error instanceof Error ? error.message : 'Unknown error'}` } ] } } } ) // Create transport with proper error handling let transport try { transport = new StdioServerTransport() } catch (error) { console.error('Failed to create StdioServerTransport:', error) // Fallback transport transport = { onclose: undefined, onerror: undefined, onmessage: undefined, start: async () => {}, close: async () => {}, send: async (message) => {} } } // Wrap in async function to avoid top-level await (async () => { await server.connect(transport) })()

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/jprealini/cypress-mcp'

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