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