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
JavaScript
// 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;
}
}