UNPKG

ai-debug-local-mcp

Version:

🎯 ENHANCED AI GUIDANCE v4.1.2: Dramatically improved tool descriptions help AI users choose the right tools instead of 'close enough' options. Ultra-fast keyboard automation (10x speed), universal recording, multi-ecosystem debugging support, and compreh

322 lines (314 loc) • 12.6 kB
export class TestGeneratorAI { options; constructor(options = {}) { this.options = { framework: 'playwright', aiModel: 'gpt-4', includeVisualAssertions: false, ...options }; } async generateTest(session) { this.validateSession(session); if (!session.events || session.events.length === 0) { return this.generateEmptyTestTemplate(); } // Filter out malformed events and add warnings const validEvents = []; const warnings = []; for (const event of session.events) { if (this.isValidEvent(event)) { validEvents.push(event); } else { warnings.push('// Warning: Skipped malformed event'); } } const testDescription = this.generateTestDescription(session); const testSteps = this.groupIntoSteps(validEvents); const assertions = this.generateAssertions(validEvents); let testCode = ''; switch (this.options.framework) { case 'jest': testCode = this.generateJestTest(testDescription, testSteps, assertions, session); break; case 'playwright': testCode = this.generatePlaywrightTest(testDescription, testSteps, assertions, session); break; case 'cypress': testCode = this.generateCypressTest(testDescription, testSteps, assertions, session); break; default: testCode = this.generatePlaywrightTest(testDescription, testSteps, assertions, session); } // Add warnings to the beginning if (warnings.length > 0) { testCode = warnings.join('\n') + '\n' + testCode; } return testCode; } analyzeUserInteractions(events) { return events .filter(event => event.type === 'click' || event.type === 'input') .map(event => ({ type: event.type, selector: event.data.selector || 'unknown', timestamp: event.timestamp, data: event.data })); } analyzeFormInputs(events) { return events .filter(event => event.type === 'input') .map(event => ({ selector: event.data.selector || 'unknown', value: event.data.value || '', inputType: event.data.inputType || 'text' })); } analyzeNavigations(events) { return events .filter(event => event.type === 'navigation') .map(event => ({ url: event.data.url || '', timestamp: event.timestamp, method: event.data.method || 'unknown' })); } detectCriticalPaths(events) { const paths = []; const hasFormSubmission = events.some(e => e.type === 'click' && e.data.element?.attributes?.type === 'submit'); const hasNavigation = events.some(e => e.type === 'navigation'); const hasFormInput = events.some(e => e.type === 'input'); if (hasFormInput && hasFormSubmission) { paths.push('form_submission'); } if (hasNavigation) { paths.push('navigation_flow'); } if (hasFormInput && events.some(e => e.data.inputType === 'email' || e.data.inputType === 'password')) { paths.push('authentication_flow'); } return paths; } optimizeSelectors(events) { const selectors = {}; events.forEach(event => { if (event.data.selector) { // For now, just use the original selector // In a real implementation, this would optimize based on element attributes selectors[event.data.selector] = event.data.selector; } }); return selectors; } groupIntoSteps(events) { const steps = []; // Simple grouping by event type for now const navigationEvents = events.filter(e => e.type === 'navigation'); const interactionEvents = events.filter(e => e.type === 'click' || e.type === 'input'); if (navigationEvents.length > 0) { steps.push({ description: 'Navigate to page', events: navigationEvents.slice(0, 1), assertions: [`Page should load successfully`] }); } if (interactionEvents.length > 0) { steps.push({ description: 'Complete form interactions', events: interactionEvents, assertions: [`Form should accept input`, `Buttons should be clickable`] }); } if (navigationEvents.length > 1) { steps.push({ description: 'Navigate to result page', events: navigationEvents.slice(1), assertions: [`Should redirect correctly`] }); } return steps; } validateSession(session) { if (!session) { throw new Error('Invalid session data: session is null or undefined'); } if (!session.events) { throw new Error('Invalid session data: events array is missing'); } if (!Array.isArray(session.events)) { throw new Error('Invalid session data: events must be an array'); } } analyzeTestIntent(metadata) { return { description: metadata.testIntent || 'Generated test', priority: this.determinePriority(metadata), categories: metadata.tags || ['general'] }; } suggestAdditionalTests(session) { const suggestions = []; const hasAuth = session.events.some(e => e.data.inputType === 'email' || e.data.inputType === 'password'); if (hasAuth) { suggestions.push('invalid_credentials_test'); suggestions.push('empty_form_validation_test'); suggestions.push('logout_flow_test'); } suggestions.push('error_handling_test'); suggestions.push('accessibility_test'); return suggestions; } generateTestDescription(session) { if (session.metadata?.testIntent) { return session.metadata.testIntent; } const hasLogin = session.events.some(e => e.data.inputType === 'email' || e.data.inputType === 'password'); const hasNavigation = session.events.some(e => e.type === 'navigation'); if (hasLogin && hasNavigation) { return 'should successfully log in with valid email and redirect to dashboard'; } return 'Generated test should complete user interaction flow'; } isValidEvent(event) { const validTypes = ['click', 'input', 'navigation', 'network_request', 'network_response', 'console_log', 'console_error', 'dom_mutation', 'scroll', 'resize', 'error']; return validTypes.includes(event.type) && event.data !== null && event.data !== undefined; } generateEmptyTestTemplate() { return `// No events recorded test('empty session', () => { // Add your test implementation });`; } generateAssertions(events) { const assertions = []; events.forEach(event => { switch (event.type) { case 'navigation': assertions.push(`expect(page.url()).toContain('${event.data.url}')`); break; case 'click': assertions.push(`expect(page.locator('${event.data.selector}')).toBeVisible()`); break; case 'input': assertions.push(`expect(page.locator('${event.data.selector}')).toHaveValue('${event.data.value}')`); break; } }); return assertions; } generateJestTest(description, steps, assertions, session) { const imports = `import { render, screen, fireEvent } from '@testing-library/react'; import { expect } from '@jest/globals';`; const metadata = session.metadata; const testName = metadata?.testIntent || description; return `${imports} describe('${metadata?.userStory || 'Generated Test Suite'}', () => { test('${testName}', async () => { // ${metadata?.tags?.join(', ') || 'Generated from debugging session'} ${steps.map(step => ` // ${step.description} ${step.events.map(event => this.generateJestAction(event)).join('\n')} `).join('\n')} // Assertions ${assertions.map(assertion => ` ${assertion};`).join('\n')} }); });`; } generatePlaywrightTest(description, steps, assertions, session) { const imports = `import { test, expect } from '@playwright/test';`; const metadata = session.metadata; const testName = metadata?.testIntent || description; return `${imports} test('${testName}', async ({ page }) => { // ${metadata?.userStory || 'Generated from debugging session'} // Tags: ${metadata?.tags?.join(', ') || 'none'} ${steps.map(step => ` // ${step.description} ${step.events.map(event => this.generatePlaywrightAction(event)).join('\n')} `).join('\n')} // Assertions ${assertions.map(assertion => ` ${assertion};`).join('\n')} });`; } generateCypressTest(description, steps, assertions, session) { const metadata = session.metadata; const testName = metadata?.testIntent || description; return `describe('${metadata?.userStory || 'Generated Test Suite'}', () => { it('${testName}', () => { // ${metadata?.tags?.join(', ') || 'Generated from debugging session'} ${steps.map(step => ` // ${step.description} ${step.events.map(event => this.generateCypressAction(event)).join('\n')} `).join('\n')} // Assertions ${assertions.map(assertion => ` ${this.convertToCypressAssertion(assertion)};`).join('\n')} }); });`; } generateJestAction(event) { switch (event.type) { case 'click': return ` fireEvent.click(screen.getByTestId('${event.data.selector?.replace('#', '')}'));`; case 'input': return ` fireEvent.change(screen.getByTestId('${event.data.selector?.replace('#', '')}'), { target: { value: '${event.data.value}' } });`; case 'navigation': return ` // Navigate to ${event.data.url}`; default: return ` // ${event.type} event`; } } generatePlaywrightAction(event) { switch (event.type) { case 'navigation': return ` await page.goto('${event.data.url}');`; case 'click': return ` await page.click('${event.data.selector}');`; case 'input': return ` await page.fill('${event.data.selector}', '${event.data.value}');`; default: return ` // ${event.type} event`; } } generateCypressAction(event) { switch (event.type) { case 'navigation': return ` cy.visit('${event.data.url}');`; case 'click': return ` cy.get('${event.data.selector}').click();`; case 'input': return ` cy.get('${event.data.selector}').type('${event.data.value}');`; default: return ` // ${event.type} event`; } } convertToCypressAssertion(assertion) { if (assertion.includes('toContain')) { const match = assertion.match(/'([^']+)'/); if (match) { return `cy.url().should('contain', '${match[1]}')`; } } if (assertion.includes('toBeVisible')) { const selector = assertion.match(/'([^']+)'/)?.[1]; return `cy.get('${selector}').should('be.visible')`; } if (assertion.includes('toHaveValue')) { const match = assertion.match(/'([^']+)'.*'([^']+)'/); if (match) { return `cy.get('${match[1]}').should('have.value', '${match[2]}')`; } } return `// ${assertion}`; } determinePriority(metadata) { const tags = metadata.tags || []; if (tags.includes('critical') || tags.includes('authentication')) { return 'critical'; } if (tags.includes('important') || tags.includes('core')) { return 'high'; } return 'medium'; } } //# sourceMappingURL=test-generator-ai.js.map