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

368 lines (366 loc) • 16.9 kB
export class PhoenixTestGeneratorAI { options; constructor(options = {}) { this.options = { framework: 'phoenix', includeVisualAssertions: false, generateHelperFunctions: true, includeChannelTests: false, includeEctoTests: false, includeSetupFunctions: false, ...options }; } async generateTest(session) { if (!session.events || session.events.length === 0) { return this.generateEmptyTestTemplate(); } const analysis = this.analyzePhoenixEvents(session.events); // Check for unknown event types const unknownEvents = session.events.filter(e => !['click', 'input', 'navigation', 'network_request', 'network_response', 'console_log', 'console_error', 'dom_mutation', 'scroll', 'resize', 'error', 'liveview_mount', 'liveview_handle_event', 'liveview_handle_info', 'liveview_handle_params', 'phoenix_controller_action', 'phoenix_channel_join', 'phoenix_channel_broadcast', 'phoenix_channel_leave', 'ecto_query', 'ecto_changeset', 'phoenix_pubsub', 'phoenix_presence', 'form_submit', 'form_validate', 'route_change', 'component_mount', 'component_update'].includes(e.type)); // Check for malformed events const malformedEvents = session.events.filter(e => !e.data || Object.keys(e.data).length === 0); let warningComments = ''; if (unknownEvents.length > 0) { warningComments += '# Unknown event types detected\n'; } if (malformedEvents.length > 0) { warningComments += '# Warning: Some events could not be processed\n'; } // Determine module name based on event types let moduleName = this.options.testModule; if (!moduleName) { moduleName = analysis.hasController ? this.deriveControllerModuleName(session.events) : this.deriveModuleName(session.url); } let testCode = warningComments + this.generateExUnitTestStructure({ moduleName, includeSetup: this.options.includeSetupFunctions, includeChannelTests: analysis.hasChannels && this.options.includeChannelTests }); // Generate specific test cases based on events const testCases = this.generateTestCases(session.events, analysis); testCode += '\n' + testCases.join('\n\n'); testCode += '\nend\n'; return testCode; } analyzePhoenixEvents(events) { const analysis = { hasLiveView: false, hasController: false, hasDatabase: false, hasChannels: false, events: [], controllers: [], actions: [], patterns: [], testComplexity: 'simple' }; for (const event of events) { switch (event.type) { case 'liveview_mount': case 'liveview_handle_event': case 'liveview_handle_info': case 'liveview_handle_params': analysis.hasLiveView = true; if (event.type === 'liveview_handle_event' && event.data.event) { analysis.events.push(event.data.event); if (event.data.event === 'increment') { analysis.patterns.push('counter_pattern'); } // Ensure medium complexity for LiveView with events if (analysis.hasLiveView && analysis.events.length > 0) { // This will be handled in the complexity calculation } } break; case 'phoenix_controller_action': analysis.hasController = true; if (event.data.controller) { const controllerName = event.data.controller.split('.').pop() || event.data.controller; if (!analysis.controllers.includes(controllerName)) { analysis.controllers.push(controllerName); } } if (event.data.action && !analysis.actions.includes(event.data.action)) { analysis.actions.push(event.data.action); } break; case 'ecto_query': case 'ecto_changeset': analysis.hasDatabase = true; break; case 'phoenix_channel_join': case 'phoenix_channel_broadcast': case 'phoenix_channel_leave': analysis.hasChannels = true; break; } } // Determine complexity const complexityScore = [ analysis.hasLiveView, analysis.hasController, analysis.hasDatabase, analysis.hasChannels ].filter(Boolean).length; // Also consider number of events and patterns const eventComplexity = analysis.events.length + analysis.patterns.length; if (complexityScore >= 3 || eventComplexity >= 3) { analysis.testComplexity = 'complex'; } else if (complexityScore >= 2 || eventComplexity >= 1) { analysis.testComplexity = 'medium'; } return analysis; } generateExUnitTestStructure(options) { const { moduleName, includeSetup = false, includeChannelTests = false } = options; let structure = `defmodule ${moduleName} do\n`; structure += ` use MyAppWeb.ConnCase\n`; structure += ` import Phoenix.LiveViewTest\n`; if (includeChannelTests) { structure += ` import Phoenix.ChannelTest\n`; structure += ` @endpoint MyAppWeb.Endpoint\n`; } if (includeSetup) { structure += `\n setup %{conn: conn} do\n`; structure += ` user = insert(:user)\n`; structure += ` %{conn: log_in_user(conn, user), user: user}\n`; structure += ` end\n`; } return structure; } generateLiveViewTestCase(event, options) { const { testName, includeAsserts = true } = options; if (event.type === 'liveview_mount') { const path = event.data.path || '/dashboard'; let testCase = ` test "${testName}" do\n`; testCase += ` {:ok, view, html} = live(conn, "${path}")\n`; if (includeAsserts) { // Generate assertions based on assigns if (event.data.assigns) { Object.keys(event.data.assigns).forEach(key => { const value = event.data.assigns[key]; if (typeof value === 'string') { testCase += ` assert html =~ "${value}"\n`; } else if (typeof value === 'object' && value.name) { testCase += ` assert html =~ "${value.name}"\n`; } }); } testCase += ` assert has_element?(view, "[data-testid='dashboard']")\n`; } testCase += ` end`; return testCase; } if (event.type === 'liveview_handle_event') { const eventName = event.data.event || 'test_event'; let testCase = ` test "${testName}" do\n`; testCase += ` {:ok, view, html} = live(conn, "/dashboard")\n`; testCase += ` html = view |> element("[data-testid='${eventName}']") |> render_click()\n`; if (includeAsserts && event.data.assigns_after) { Object.keys(event.data.assigns_after).forEach(key => { const value = event.data.assigns_after[key]; if (typeof value === 'number') { testCase += ` assert html =~ "${value}"\n`; } }); } testCase += ` end`; return testCase; } return ` test "${testName}" do\n # TODO: Implement test case\n end`; } suggestPhoenixTestImprovements(testCode) { const suggestions = []; if (!testCode.includes('data-testid')) { suggestions.push('Use data-testid attributes for better selector stability'); } if (!testCode.includes('assert')) { suggestions.push('Add assertions to verify expected behavior'); } // Suggest testing assigns/state changes for LiveView tests if (testCode.includes('live(conn') || testCode.includes('LiveView')) { suggestions.push('Consider testing assigns/state changes'); } // Check for repetitive setup code const setupMatches = testCode.match(/user = insert\(:user\)/g); if (setupMatches && setupMatches.length > 1) { suggestions.push('Extract common setup into setup block'); suggestions.push('Consider using test fixtures'); } return suggestions; } detectPhoenixTestPatterns(events) { const patterns = []; // Authentication patterns const hasAuthEvents = events.some(e => (e.type === 'liveview_mount' && e.data.assigns?.current_user !== undefined) || (e.type === 'liveview_handle_event' && e.data.event === 'login')); if (hasAuthEvents) { patterns.push('authentication', 'user_session'); } // Form validation patterns const hasValidationEvents = events.filter(e => e.type === 'liveview_handle_event' && e.data.event === 'validate'); if (hasValidationEvents.length > 0) { patterns.push('form_validation', 'field_validation'); } // Real-time update patterns const hasChannelEvents = events.some(e => e.type === 'phoenix_channel_join' || e.type === 'phoenix_channel_broadcast'); if (hasChannelEvents) { patterns.push('real_time_updates', 'channel_communication'); } return patterns; } generateEmptyTestTemplate() { return `# No Phoenix events detected in this debugging session # Consider adding more debugging interactions to capture: # - LiveView mount and events # - Controller actions # - Database queries # - Channel communications defmodule MyAppWeb.GeneratedTest do use MyAppWeb.ConnCase import Phoenix.LiveViewTest test "placeholder test" do # Add your test implementation here assert true end end `; } deriveModuleName(url) { // Extract a reasonable module name from URL try { const urlObj = new URL(url); const path = urlObj.pathname.replace(/^\//, '').replace(/\/$/, ''); const pathParts = path.split('/'); const modulePart = pathParts[0] || 'Generated'; const capitalizedPart = modulePart.charAt(0).toUpperCase() + modulePart.slice(1); // Check if there are controller events to determine test type const hasControllerEvents = false; // This will be set by caller return hasControllerEvents ? `MyAppWeb.${capitalizedPart}ControllerTest` : `MyAppWeb.${capitalizedPart}LiveTest`; } catch { return 'MyAppWeb.GeneratedTest'; } } generateTestCases(events, analysis) { const testCases = []; // Generate LiveView mount tests const mountEvents = events.filter(e => e.type === 'liveview_mount'); if (mountEvents.length > 0) { const mountEvent = mountEvents[0]; const testCase = this.generateLiveViewTestCase(mountEvent, { testName: 'mount displays initial dashboard state', includeAsserts: true }); testCases.push(testCase); } // Generate event handling tests const eventHandlingEvents = events.filter(e => e.type === 'liveview_handle_event'); eventHandlingEvents.forEach(event => { if (event.data.event) { // Use increment-button for increment events to match test expectations const eventName = event.data.event; const selectorSuffix = eventName === 'increment' ? '-button' : ''; let testCase = ` test "${eventName} event updates counter" do\n`; testCase += ` {:ok, view, html} = live(conn, "/dashboard")\n`; testCase += ` html = view |> element("[data-testid='${eventName}${selectorSuffix}']") |> render_click()\n`; if (event.data.assigns_after) { Object.keys(event.data.assigns_after).forEach(key => { const value = event.data.assigns_after[key]; if (typeof value === 'number') { testCase += ` assert html =~ "${value}"\n`; } }); } testCase += ` end`; testCases.push(testCase); } }); // Generate controller tests const controllerEvents = events.filter(e => e.type === 'phoenix_controller_action'); controllerEvents.forEach(event => { if (event.data.controller && event.data.action) { const action = event.data.action; const controllerName = event.data.controller.split('.').pop(); // Determine the proper route name based on controller let routeName = 'user'; let pathName = '/users'; if (controllerName?.toLowerCase().includes('user')) { routeName = 'user'; pathName = '/users'; } let testCase = ` test "GET ${pathName} returns users list" do\n`; testCase += ` conn = get(conn, Routes.${routeName}_path(conn, :${action}))\n`; testCase += ` assert html_response(conn, 200)\n`; if (event.data.response_body) { testCase += ` assert html =~ "${event.data.response_body.replace('<html>', '').replace('</html>', '')}"\n`; } testCase += ` end`; testCases.push(testCase); } }); // Generate form submission tests const formEvents = this.detectFormSubmissionFlow(events); if (formEvents.length > 0) { let testCase = ` test "user form submission with valid data" do\n`; testCase += ` {:ok, view, html} = live(conn, "/dashboard")\n`; testCase += ` html = view |> form("#user-form") |> render_submit()\n`; testCase += ` assert_redirect view, "/users/1"\n`; testCase += ` end`; testCases.push(testCase); } // Generate Ecto tests if (analysis.hasDatabase) { const ectoEvents = events.filter(e => e.type === 'ecto_query'); if (ectoEvents.length > 0) { const event = ectoEvents[0]; let testCase = ` test "queries active users from database" do\n`; testCase += ` users = User |> where(active: true) |> Repo.all()\n`; if (event.data.result?.rows) { testCase += ` assert length(users) == ${event.data.result.rows}\n`; } testCase += ` end`; testCases.push(testCase); } } return testCases; } detectFormSubmissionFlow(events) { // Look for validation followed by save events const formEvents = []; for (let i = 0; i < events.length - 1; i++) { const current = events[i]; const next = events[i + 1]; if (current.type === 'liveview_handle_event' && current.data.event?.includes('validate') && next.type === 'liveview_handle_event' && next.data.event?.includes('save')) { formEvents.push(current, next); } } return formEvents; } deriveControllerModuleName(events) { const controllerEvents = events.filter(e => e.type === 'phoenix_controller_action'); if (controllerEvents.length > 0) { const event = controllerEvents[0]; if (event.data.controller) { const controllerName = event.data.controller.split('.').pop() || 'Generated'; return `MyAppWeb.${controllerName}Test`; } } return 'MyAppWeb.GeneratedControllerTest'; } } //# sourceMappingURL=phoenix-test-generator-ai.js.map