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
JavaScript
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