UNPKG

visual-ui-debug-agent-mcp

Version:

VUDA: Visual UI Debug Agent - An autonomous MCP for visual testing and debugging of user interfaces

436 lines (435 loc) 21.5 kB
// UserFlowDebugger // A tool to generate, test and validate expected user interaction flows export class UserFlowDebugger { constructor() { this.flows = []; this.currentFlowIndex = 0; this.isRunning = false; } /** * Generate a comprehensive test plan based on the UI * @param options Configuration options * @returns Generated test flows */ generateTestPlan(options = {}) { const { minFlows = 10, includeAuth = true, includeNavigation = true, includeFormInteractions = true, includeDataManipulation = true, includeErrorCases = true } = options; console.log('🔍 Analyzing UI and generating sequential user flows...'); // This would be replaced with actual UI analysis in a real implementation // For now, we're simulating a test plan generation this.flows = [ { id: 'auth-login', description: 'User authenticates with valid credentials', priority: 'high', status: 'not-tested', steps: [ { action: 'navigate', target: '/login', description: 'Navigate to login page' }, { action: 'fill', selector: '#email', value: 'user@example.com', description: 'Enter email' }, { action: 'fill', selector: '#password', value: 'password123', description: 'Enter password' }, { action: 'click', selector: '#login-button', description: 'Click login button' }, { action: 'verify', selector: '.dashboard', description: 'Verify dashboard loads' } ], expectedResult: 'User should be authenticated and redirected to dashboard', actualResult: '', success: null, notes: '' }, { id: 'profile-update', description: 'User updates their profile information', priority: 'medium', status: 'not-tested', steps: [ { action: 'navigate', target: '/profile', description: 'Navigate to profile page' }, { action: 'fill', selector: '#name', value: 'Updated Name', description: 'Update name field' }, { action: 'fill', selector: '#bio', value: 'New bio information', description: 'Update bio' }, { action: 'click', selector: '#save-profile', description: 'Save profile changes' }, { action: 'verify', selector: '.success-message', description: 'Verify success message appears' } ], expectedResult: 'Profile should be updated with new information', actualResult: '', success: null, notes: '' }, { id: 'create-content', description: 'User creates new content item', priority: 'high', status: 'not-tested', steps: [ { action: 'navigate', target: '/content', description: 'Navigate to content page' }, { action: 'click', selector: '#new-content', description: 'Click new content button' }, { action: 'fill', selector: '#title', value: 'New Content Item', description: 'Enter content title' }, { action: 'fill', selector: '#description', value: 'This is a test content item', description: 'Enter content description' }, { action: 'click', selector: '#submit-content', description: 'Submit new content' }, { action: 'verify', selector: '.content-list .item', description: 'Verify new content appears in list' } ], expectedResult: 'New content should be created and visible in the content list', actualResult: '', success: null, notes: '' }, { id: 'search-filter', description: 'User searches and filters results', priority: 'medium', status: 'not-tested', steps: [ { action: 'navigate', target: '/search', description: 'Navigate to search page' }, { action: 'fill', selector: '#search-input', value: 'test query', description: 'Enter search query' }, { action: 'click', selector: '#search-button', description: 'Submit search' }, { action: 'click', selector: '#filter-dropdown', description: 'Open filter options' }, { action: 'click', selector: '#filter-recent', description: 'Select recent filter' }, { action: 'verify', selector: '.search-results .item', description: 'Verify filtered results appear' } ], expectedResult: 'Search results should display and reflect applied filters', actualResult: '', success: null, notes: '' }, { id: 'delete-item', description: 'User deletes an existing item', priority: 'medium', status: 'not-tested', steps: [ { action: 'navigate', target: '/items', description: 'Navigate to items list' }, { action: 'click', selector: '.item:first-child .delete-button', description: 'Click delete on first item' }, { action: 'click', selector: '#confirm-delete', description: 'Confirm deletion' }, { action: 'verify', selector: '.success-message', description: 'Verify success message' }, { action: 'verify', custom: 'itemRemoved', description: 'Verify item no longer in list' } ], expectedResult: 'Item should be removed from the list after confirmation', actualResult: '', success: null, notes: '' }, { id: 'pagination-navigation', description: 'User navigates through paginated results', priority: 'low', status: 'not-tested', steps: [ { action: 'navigate', target: '/items?page=1', description: 'Navigate to items page' }, { action: 'verify', selector: '.pagination', description: 'Verify pagination controls exist' }, { action: 'click', selector: '.pagination .next', description: 'Click next page' }, { action: 'verify', selector: '.page-indicator', verifyText: 'Page 2', description: 'Verify on page 2' }, { action: 'click', selector: '.pagination .prev', description: 'Click previous page' }, { action: 'verify', selector: '.page-indicator', verifyText: 'Page 1', description: 'Verify back on page 1' } ], expectedResult: 'User should be able to navigate between pages with pagination controls', actualResult: '', success: null, notes: '' }, { id: 'sort-data', description: 'User sorts data in different orders', priority: 'medium', status: 'not-tested', steps: [ { action: 'navigate', target: '/data', description: 'Navigate to data page' }, { action: 'click', selector: '.sort-by-name', description: 'Sort by name' }, { action: 'verify', custom: 'sortedByName', description: 'Verify sorted by name' }, { action: 'click', selector: '.sort-by-date', description: 'Sort by date' }, { action: 'verify', custom: 'sortedByDate', description: 'Verify sorted by date' } ], expectedResult: 'Data should be reordered according to selected sort criteria', actualResult: '', success: null, notes: '' }, { id: 'form-validation', description: 'User encounters and resolves form validation', priority: 'high', status: 'not-tested', steps: [ { action: 'navigate', target: '/form', description: 'Navigate to form page' }, { action: 'fill', selector: '#email', value: 'invalid-email', description: 'Enter invalid email' }, { action: 'click', selector: '#submit-form', description: 'Submit form' }, { action: 'verify', selector: '.validation-error', description: 'Verify validation error shown' }, { action: 'fill', selector: '#email', value: 'valid@example.com', description: 'Enter valid email' }, { action: 'click', selector: '#submit-form', description: 'Submit form again' }, { action: 'verify', selector: '.success-message', description: 'Verify form submission succeeded' } ], expectedResult: 'Form should validate input and show appropriate errors or success', actualResult: '', success: null, notes: '' }, { id: 'settings-toggle', description: 'User toggles settings options', priority: 'low', status: 'not-tested', steps: [ { action: 'navigate', target: '/settings', description: 'Navigate to settings page' }, { action: 'click', selector: '#notifications-toggle', description: 'Toggle notifications setting' }, { action: 'verify', selector: '#notifications-toggle.active', description: 'Verify toggle is active' }, { action: 'click', selector: '#dark-mode-toggle', description: 'Toggle dark mode setting' }, { action: 'verify', selector: 'body.dark-theme', description: 'Verify dark theme applied to body' } ], expectedResult: 'Settings toggles should update UI and save preferences', actualResult: '', success: null, notes: '' }, { id: 'dashboard-overview', description: 'User reviews dashboard data and components', priority: 'high', status: 'not-tested', steps: [ { action: 'navigate', target: '/dashboard', description: 'Navigate to dashboard' }, { action: 'verify', selector: '.stats-panel', description: 'Verify stats panel visible' }, { action: 'verify', selector: '.recent-activity', description: 'Verify recent activity visible' }, { action: 'click', selector: '.refresh-data', description: 'Refresh dashboard data' }, { action: 'verify', selector: '.loading-indicator', description: 'Verify loading indicator shows' }, { action: 'verify', selector: '.stats-panel .updated', description: 'Verify stats updated' } ], expectedResult: 'Dashboard should display overview data and allow refreshing', actualResult: '', success: null, notes: '' } ]; // Add error cases if requested if (includeErrorCases) { this.flows.push({ id: 'auth-failure', description: 'User authentication with invalid credentials', priority: 'medium', status: 'not-tested', steps: [ { action: 'navigate', target: '/login', description: 'Navigate to login page' }, { action: 'fill', selector: '#email', value: 'invalid@example.com', description: 'Enter invalid email' }, { action: 'fill', selector: '#password', value: 'wrong-password', description: 'Enter wrong password' }, { action: 'click', selector: '#login-button', description: 'Click login button' }, { action: 'verify', selector: '.error-message', description: 'Verify error message shown' } ], expectedResult: 'System should show appropriate error message for invalid credentials', actualResult: '', success: null, notes: '' }); } // Ensure we have at least the minimum requested number of flows while (this.flows.length < minFlows) { this.flows.push({ id: `generated-flow-${this.flows.length + 1}`, description: `Auto-generated test flow #${this.flows.length + 1}`, priority: 'low', status: 'not-tested', steps: [ { action: 'navigate', target: '/page', description: 'Navigate to page' }, { action: 'click', selector: '.button', description: 'Click button' }, { action: 'verify', selector: '.result', description: 'Verify result' } ], expectedResult: 'Expected interaction result', actualResult: '', success: null, notes: 'Auto-generated flow' }); } console.log(`✅ Generated ${this.flows.length} test flows`); return this.flows; } /** * Execute a specific flow or the current flow in the queue * @param flowId Optional flow ID to run * @returns Result of the flow execution */ async executeFlow(flowId = null) { if (this.isRunning) { console.log('❌ Already running a flow. Please wait or stop the current execution.'); return null; } this.isRunning = true; let flow; if (flowId) { flow = this.flows.find(f => f.id === flowId); if (!flow) { console.log(`❌ Flow with ID ${flowId} not found`); this.isRunning = false; return null; } } else { flow = this.flows[this.currentFlowIndex]; if (!flow) { console.log('❌ No flows available to execute'); this.isRunning = false; return null; } } console.log(`▶️ Executing flow: ${flow.description}`); flow.status = 'in-progress'; try { // This would be replaced with actual execution logic in a real implementation // For now, we're simulating the execution // Simulate step execution for (let i = 0; i < flow.steps.length; i++) { const step = flow.steps[i]; if (!step) continue; // Skip if step is undefined console.log(` ${i + 1}. ${step.description}`); // Simulate a delay for the step execution await new Promise(resolve => setTimeout(resolve, 500)); // Randomly succeed or fail (80% success rate for demonstration) const stepSuccess = Math.random() < 0.8; if (!stepSuccess) { flow.status = 'failed'; flow.success = false; if (step) { flow.actualResult = `Failed at step ${i + 1}: ${step.description}`; } else { flow.actualResult = `Failed at step ${i + 1}`; } console.log(` ❌ ${flow.actualResult}`); this.isRunning = false; return flow; } } // If we get here, all steps succeeded flow.status = 'passed'; flow.success = true; flow.actualResult = 'All steps completed successfully'; console.log(` ✅ ${flow.actualResult}`); } catch (error) { flow.status = 'error'; flow.success = false; flow.actualResult = `Execution error: ${error.message}`; console.log(` 🔥 ${flow.actualResult}`); } this.isRunning = false; this.currentFlowIndex = (this.currentFlowIndex + 1) % this.flows.length; return flow; } /** * Execute all flows in sequence * @returns Results of all flow executions */ async executeAllFlows() { if (this.isRunning) { console.log('❌ Already running a flow. Please wait or stop the current execution.'); return null; } console.log(`▶️ Executing all ${this.flows.length} flows in sequence`); const results = []; for (let i = 0; i < this.flows.length; i++) { this.currentFlowIndex = i; const result = await this.executeFlow(); if (result) { results.push(result); } } console.log(`✅ Completed ${results.filter(r => r.success).length}/${results.length} flows successfully`); return results; } /** * Adjust a flow to make it more likely to succeed * @param flowId The ID of the flow to adjust * @returns The adjusted flow */ adjustFlow(flowId) { const flow = this.flows.find(f => f.id === flowId); if (!flow) { console.log(`❌ Flow with ID ${flowId} not found`); return null; } console.log(`🔧 Adjusting flow: ${flow.description}`); // This would be replaced with actual flow adjustment logic in a real implementation // For now, we're simulating the adjustment // Add retry mechanisms and improved selectors flow.steps.forEach(step => { // Add wait time for stability if (step.action === 'click' || step.action === 'fill') { step.waitBefore = 1000; // Add wait before action } // Improve selectors with alternatives if (step.selector) { step.alternativeSelectors = [ step.selector, `${step.selector}-alt`, `[data-testid="${step.selector.replace('#', '')}"]` ]; } // Add automatic retries step.retries = 3; }); // Add a success fallback flow.fallbackSteps = flow.steps.map(step => ({ ...step, optional: true })); flow.status = 'adjusted'; flow.notes += '\nFlow adjusted with retry mechanisms and improved selectors.'; console.log(`✅ Flow ${flowId} adjusted successfully`); return flow; } /** * Get a summary report of all flows * @returns Summary report */ getReport() { const passed = this.flows.filter(f => f.status === 'passed').length; const failed = this.flows.filter(f => f.status === 'failed').length; const notTested = this.flows.filter(f => f.status === 'not-tested').length; const inProgress = this.flows.filter(f => f.status === 'in-progress').length; const adjusted = this.flows.filter(f => f.status === 'adjusted').length; return { total: this.flows.length, passed, failed, notTested, inProgress, adjusted, coverage: (passed / this.flows.length) * 100, flows: this.flows.map(f => ({ id: f.id, description: f.description, status: f.status, success: f.success, priority: f.priority })) }; } /** * Export the flows as a structured markdown report * @returns Markdown formatted report */ exportMarkdownReport() { const report = this.getReport(); let markdown = `# User Flow Debug Report\n\n`; markdown += `## Summary\n\n`; markdown += `- Total Flows: ${report.total}\n`; markdown += `- Passed: ${report.passed} (${report.coverage.toFixed(1)}%)\n`; markdown += `- Failed: ${report.failed}\n`; markdown += `- Not Tested: ${report.notTested}\n`; markdown += `- In Progress: ${report.inProgress}\n`; markdown += `- Adjusted: ${report.adjusted}\n\n`; markdown += `## Flow Details\n\n`; this.flows.forEach(flow => { markdown += `### ${flow.id}: ${flow.description}\n\n`; markdown += `- Priority: ${flow.priority}\n`; markdown += `- Status: ${flow.status}\n`; markdown += `- Success: ${flow.success === null ? 'N/A' : flow.success ? 'Yes' : 'No'}\n`; markdown += `- Expected Result: ${flow.expectedResult}\n`; if (flow.actualResult) { markdown += `- Actual Result: ${flow.actualResult}\n`; } if (flow.notes) { markdown += `- Notes: ${flow.notes}\n`; } markdown += `\n#### Steps\n\n`; flow.steps.forEach((step, i) => { markdown += `${i + 1}. ${step.description}\n`; }); markdown += `\n`; }); return markdown; } }