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
781 lines • 37.4 kB
JavaScript
import { BaseHandler } from './base-handler.js';
export class AdvancedFormInteractionHandler extends BaseHandler {
constructor() {
super();
this.tools = this.getTools();
}
getToolName() {
return 'advanced-form-interaction';
}
getToolDescription() {
return 'Advanced form interactions with dynamic elements, file uploads, and smart validation';
}
getTools() {
return [
{
name: 'smart_form_interaction',
description: 'Intelligent form filling with adaptation to complex form states',
inputSchema: {
type: 'object',
properties: {
sessionId: { type: 'string', description: 'Debug session ID' },
formContext: {
type: 'object',
properties: {
formSelector: { type: 'string', description: 'CSS selector for the form' },
fields: {
type: 'array',
items: {
type: 'object',
properties: {
selector: { type: 'string' },
type: { type: 'string', enum: ['text', 'email', 'password', 'select', 'checkbox', 'radio', 'file', 'textarea'] },
value: { type: 'string' },
options: { type: 'array', items: { type: 'string' } },
files: { type: 'array', items: { type: 'string' } },
required: { type: 'boolean' }
},
required: ['selector', 'type']
}
},
submitSelector: { type: 'string', description: 'Submit button selector' },
validationStrategy: { type: 'string', enum: ['immediate', 'onSubmit', 'none'], default: 'onSubmit' }
},
required: ['fields']
},
options: {
type: 'object',
properties: {
skipValidation: { type: 'boolean', default: false },
waitForResponse: { type: 'boolean', default: true },
captureErrors: { type: 'boolean', default: true }
}
}
},
required: ['sessionId', 'formContext']
}
},
{
name: 'handle_dynamic_elements',
description: 'Advanced handling of dynamic form elements like dropdowns, modals, overlays',
inputSchema: {
type: 'object',
properties: {
sessionId: { type: 'string', description: 'Debug session ID' },
elementConfig: {
type: 'object',
properties: {
selector: { type: 'string', description: 'Element selector' },
type: { type: 'string', enum: ['dropdown', 'modal', 'autocomplete', 'datepicker', 'overlay'] },
trigger: { type: 'string', description: 'Selector to trigger the element' },
waitForOptions: { type: 'boolean', default: true },
optionSelector: { type: 'string', description: 'Selector for options/items' },
valueSelector: { type: 'string', description: 'Selector for the value to select' }
},
required: ['selector', 'type']
},
action: {
type: 'object',
properties: {
type: { type: 'string', enum: ['open', 'select', 'close', 'search'] },
value: { type: 'string', description: 'Value to select or search for' },
index: { type: 'number', description: 'Index of option to select' }
},
required: ['type']
}
},
required: ['sessionId', 'elementConfig', 'action']
}
},
{
name: 'simulate_file_upload',
description: 'Simulate file uploads with various file types and validation',
inputSchema: {
type: 'object',
properties: {
sessionId: { type: 'string', description: 'Debug session ID' },
fileInputSelector: { type: 'string', description: 'File input selector' },
files: {
type: 'array',
items: {
type: 'object',
properties: {
name: { type: 'string', description: 'File name' },
content: { type: 'string', description: 'File content (base64 or text)' },
mimeType: { type: 'string', description: 'MIME type' },
size: { type: 'number', description: 'File size in bytes' }
},
required: ['name', 'content', 'mimeType']
}
},
uploadContext: {
type: 'object',
properties: {
multiple: { type: 'boolean', default: false },
acceptedTypes: { type: 'array', items: { type: 'string' } },
maxSize: { type: 'number', description: 'Max file size in bytes' },
progressSelector: { type: 'string', description: 'Upload progress indicator selector' }
}
}
},
required: ['sessionId', 'fileInputSelector', 'files']
}
},
{
name: 'wait_for_form_state',
description: 'Smart waiting for form state changes, validation, and dynamic updates',
inputSchema: {
type: 'object',
properties: {
sessionId: { type: 'string', description: 'Debug session ID' },
expectedState: {
type: 'object',
properties: {
formValid: { type: 'boolean', description: 'Whether form should be valid' },
fieldsPopulated: { type: 'array', items: { type: 'string' } },
errorsPresent: { type: 'boolean', description: 'Whether validation errors should be present' },
submitEnabled: { type: 'boolean', description: 'Whether submit button should be enabled' },
customCondition: { type: 'string', description: 'Custom JavaScript condition to evaluate' }
}
},
timeout: { type: 'number', default: 30000, description: 'Maximum wait time' },
pollInterval: { type: 'number', default: 500, description: 'State check interval' }
},
required: ['sessionId', 'expectedState']
}
},
{
name: 'extract_form_validation_errors',
description: 'Extract and analyze form validation errors and messages',
inputSchema: {
type: 'object',
properties: {
sessionId: { type: 'string', description: 'Debug session ID' },
formSelector: { type: 'string', description: 'Form selector' },
errorSelectors: {
type: 'object',
properties: {
fieldErrors: { type: 'string', description: 'Selector for field-level errors' },
generalErrors: { type: 'string', description: 'Selector for general form errors' },
successMessages: { type: 'string', description: 'Selector for success messages' }
}
}
},
required: ['sessionId']
}
},
{
name: 'create_form_test_scenario',
description: 'Create comprehensive form testing scenarios with validation testing',
inputSchema: {
type: 'object',
properties: {
scenarioName: { type: 'string', description: 'Name for the test scenario' },
formConfig: {
type: 'object',
properties: {
url: { type: 'string', description: 'URL containing the form' },
formSelector: { type: 'string', description: 'Form selector' },
fields: { type: 'array', items: { type: 'object' } },
submitSelector: { type: 'string' }
},
required: ['url', 'fields']
},
testCases: {
type: 'array',
items: {
type: 'object',
properties: {
name: { type: 'string' },
description: { type: 'string' },
data: { type: 'object' },
expectedResult: { type: 'string', enum: ['success', 'validation_error', 'server_error'] },
validationRules: { type: 'object' }
},
required: ['name', 'data', 'expectedResult']
}
}
},
required: ['scenarioName', 'formConfig', 'testCases']
}
}
];
}
async handle(tool, params, sessions) {
try {
switch (tool) {
case 'smart_form_interaction':
return await this.smartFormInteraction(params, sessions);
case 'handle_dynamic_elements':
return await this.handleDynamicElements(params, sessions);
case 'simulate_file_upload':
return await this.simulateFileUpload(params, sessions);
case 'wait_for_form_state':
return await this.waitForFormState(params, sessions);
case 'extract_form_validation_errors':
return await this.extractFormValidationErrors(params, sessions);
case 'create_form_test_scenario':
return await this.createFormTestScenario(params);
default:
throw new Error(`Unknown tool: ${tool}`);
}
}
catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
tool,
timestamp: new Date().toISOString()
};
}
}
async smartFormInteraction(params, sessions) {
const { sessionId, formContext, options = {} } = params;
if (!sessions.has(sessionId)) {
throw new Error(`Session ${sessionId} not found`);
}
const session = sessions.get(sessionId);
const page = session.page;
const results = [];
try {
// Wait for form to be ready
if (formContext.formSelector) {
await page.waitForSelector(formContext.formSelector, { timeout: 10000 });
}
// Process each field
for (const field of formContext.fields) {
try {
const fieldResult = await this.processFormField(page, field);
results.push({
field: field.selector,
type: field.type,
status: 'success',
result: fieldResult
});
}
catch (fieldError) {
results.push({
field: field.selector,
type: field.type,
status: 'error',
error: fieldError instanceof Error ? fieldError.message : 'Unknown error'
});
if (field.required && !options.skipValidation) {
throw new Error(`Required field ${field.selector} failed: ${fieldError instanceof Error ? fieldError.message : 'Unknown error'}`);
}
}
}
// Handle form submission if specified
let submitResult = null;
if (formContext.submitSelector) {
try {
await page.click(formContext.submitSelector);
if (options.waitForResponse) {
// Wait for either success or error response
await Promise.race([
page.waitForNavigation({ timeout: 10000 }).catch(() => null),
page.waitForSelector('[role="alert"], .error, .success', { timeout: 10000 }).catch(() => null)
]);
}
submitResult = {
submitted: true,
currentUrl: page.url(),
timestamp: new Date().toISOString()
};
}
catch (submitError) {
submitResult = {
submitted: false,
error: submitError instanceof Error ? submitError.message : 'Unknown error'
};
}
}
// Capture validation errors if requested
let validationErrors = null;
if (options.captureErrors) {
validationErrors = await this.captureValidationErrors(page);
}
const successfulFields = results.filter(r => r.status === 'success').length;
const failedFields = results.filter(r => r.status === 'error').length;
return {
success: true,
formProcessed: true,
fieldsProcessed: results.length,
successfulFields,
failedFields,
results,
submitResult,
validationErrors,
currentUrl: page.url(),
message: `Form interaction completed: ${successfulFields}/${results.length} fields processed successfully`
};
}
catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
partialResults: results,
fieldsProcessed: results.length,
message: 'Form interaction failed during processing'
};
}
}
async processFormField(page, field) {
await page.waitForSelector(field.selector, { timeout: 10000 });
switch (field.type) {
case 'text':
case 'email':
case 'password':
case 'textarea':
await page.fill(field.selector, field.value || '');
return { value: field.value, filled: true };
case 'select':
if (field.value) {
await page.selectOption(field.selector, field.value);
}
else if (field.options && field.options.length > 0) {
await page.selectOption(field.selector, field.options[0]);
}
return { selected: field.value || field.options?.[0] };
case 'checkbox':
const isChecked = await page.isChecked(field.selector);
const shouldCheck = field.value === 'true' || field.value === 'checked';
if (isChecked !== shouldCheck) {
await page.click(field.selector);
}
return { checked: shouldCheck, wasToggled: isChecked !== shouldCheck };
case 'radio':
await page.click(field.selector);
return { selected: true };
case 'file':
if (field.files && field.files.length > 0) {
await page.setInputFiles(field.selector, field.files);
return { filesUploaded: field.files };
}
return { message: 'No files specified for upload' };
default:
throw new Error(`Unsupported field type: ${field.type}`);
}
}
async handleDynamicElements(params, sessions) {
const { sessionId, elementConfig, action } = params;
if (!sessions.has(sessionId)) {
throw new Error(`Session ${sessionId} not found`);
}
const session = sessions.get(sessionId);
const page = session.page;
try {
switch (elementConfig.type) {
case 'dropdown':
return await this.handleDropdown(page, elementConfig, action);
case 'modal':
return await this.handleModal(page, elementConfig, action);
case 'autocomplete':
return await this.handleAutocomplete(page, elementConfig, action);
case 'datepicker':
return await this.handleDatepicker(page, elementConfig, action);
case 'overlay':
return await this.handleOverlay(page, elementConfig, action);
default:
throw new Error(`Unsupported dynamic element type: ${elementConfig.type}`);
}
}
catch (error) {
return {
success: false,
elementType: elementConfig.type,
action: action.type,
error: error instanceof Error ? error.message : 'Unknown error',
message: `Failed to handle ${elementConfig.type} element`
};
}
}
async handleDropdown(page, config, action) {
switch (action.type) {
case 'open':
if (config.trigger) {
await page.click(config.trigger);
}
else {
await page.click(config.selector);
}
if (config.waitForOptions) {
await page.waitForSelector(config.optionSelector || `${config.selector} option`, { timeout: 5000 });
}
return { opened: true, selector: config.selector };
case 'select':
if (action.value) {
await page.selectOption(config.selector, action.value);
}
else if (typeof action.index === 'number') {
const options = await page.$$(`${config.selector} option`);
if (options[action.index]) {
await options[action.index].click();
}
}
return { selected: action.value || `index:${action.index}` };
case 'close':
// Try common close methods
await Promise.race([
page.keyboard.press('Escape').catch(() => null),
page.click('body').catch(() => null)
]);
return { closed: true };
default:
throw new Error(`Unsupported dropdown action: ${action.type}`);
}
}
async handleModal(page, config, action) {
switch (action.type) {
case 'open':
if (config.trigger) {
await page.click(config.trigger);
await page.waitForSelector(config.selector, { timeout: 10000 });
}
return { opened: true, modalVisible: true };
case 'close':
// Try multiple close strategies
const closeStrategies = [
() => page.click(`${config.selector} [data-close], ${config.selector} .close, ${config.selector} .modal-close`),
() => page.keyboard.press('Escape'),
() => page.click(`${config.selector}-backdrop, .modal-backdrop, .overlay-backdrop`)
];
for (const strategy of closeStrategies) {
try {
await strategy();
await page.waitForSelector(config.selector, { state: 'hidden', timeout: 3000 });
return { closed: true, method: 'successful' };
}
catch {
continue;
}
}
throw new Error('Could not close modal with any strategy');
default:
throw new Error(`Unsupported modal action: ${action.type}`);
}
}
async handleAutocomplete(page, config, action) {
switch (action.type) {
case 'search':
await page.fill(config.selector, action.value || '');
await page.waitForSelector(config.optionSelector || `${config.selector}-options`, { timeout: 5000 });
const options = await page.$$eval(config.optionSelector || `${config.selector}-options > *`, elements => elements.map(el => el.textContent?.trim()).filter(Boolean));
return { searched: action.value, optionsFound: options.length, options };
case 'select':
if (action.value) {
await page.fill(config.selector, action.value);
await page.waitForSelector(config.optionSelector || `${config.selector}-options`, { timeout: 5000 });
await page.click(`${config.optionSelector || `${config.selector}-options`} [data-value="${action.value}"], ${config.optionSelector || `${config.selector}-options`} :text("${action.value}")`);
}
return { selected: action.value };
default:
throw new Error(`Unsupported autocomplete action: ${action.type}`);
}
}
async handleDatepicker(page, config, action) {
// Basic datepicker handling - can be extended for specific libraries
switch (action.type) {
case 'open':
await page.click(config.selector);
await page.waitForSelector('.datepicker, .date-picker, [data-datepicker]', { timeout: 5000 });
return { opened: true };
case 'select':
if (action.value) {
// Try direct input first
try {
await page.fill(config.selector, action.value);
return { dateSet: action.value, method: 'direct-input' };
}
catch {
// Fall back to datepicker interaction
await page.click(config.selector);
// This would need to be customized for specific datepicker libraries
return { dateSet: action.value, method: 'datepicker-interaction' };
}
}
break;
default:
throw new Error(`Unsupported datepicker action: ${action.type}`);
}
}
async handleOverlay(page, config, action) {
switch (action.type) {
case 'close':
await page.click('.overlay-backdrop, .backdrop, body');
await page.waitForSelector(config.selector, { state: 'hidden', timeout: 5000 });
return { closed: true };
default:
throw new Error(`Unsupported overlay action: ${action.type}`);
}
}
async simulateFileUpload(params, sessions) {
const { sessionId, fileInputSelector, files, uploadContext = {} } = params;
if (!sessions.has(sessionId)) {
throw new Error(`Session ${sessionId} not found`);
}
const session = sessions.get(sessionId);
const page = session.page;
try {
await page.waitForSelector(fileInputSelector, { timeout: 10000 });
// Create temporary files for upload simulation
const tempFiles = [];
for (const file of files) {
// In a real implementation, you'd create actual temp files
// For now, we'll simulate the file upload process
tempFiles.push(`/tmp/simulated-${file.name}`);
}
// Simulate file selection
await page.setInputFiles(fileInputSelector, tempFiles);
// Wait for upload progress if specified
if (uploadContext.progressSelector) {
try {
await page.waitForSelector(uploadContext.progressSelector, { timeout: 5000 });
// Monitor upload progress
const uploadComplete = await page.waitForFunction((selector) => {
const progress = document.querySelector(selector);
return progress && (progress.textContent?.includes('100%') ||
progress.textContent?.includes('complete') ||
progress.getAttribute('aria-valuenow') === '100');
}, uploadContext.progressSelector, { timeout: 30000 });
}
catch {
// Progress monitoring failed, continue anyway
}
}
return {
success: true,
filesUploaded: files.length,
fileNames: files.map((f) => f.name),
totalSize: files.reduce((sum, f) => sum + (f.size || 0), 0),
uploadContext: uploadContext,
message: `Successfully simulated upload of ${files.length} files`
};
}
catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
fileInputSelector,
attemptedFiles: files.length,
message: 'File upload simulation failed'
};
}
}
async waitForFormState(params, sessions) {
const { sessionId, expectedState, timeout = 30000, pollInterval = 500 } = params;
if (!sessions.has(sessionId)) {
throw new Error(`Session ${sessionId} not found`);
}
const session = sessions.get(sessionId);
const page = session.page;
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
try {
const currentState = await this.evaluateFormState(page, expectedState);
if (this.stateMatches(currentState, expectedState)) {
return {
success: true,
stateReached: true,
currentState,
waitTime: Date.now() - startTime,
message: 'Expected form state reached'
};
}
await page.waitForTimeout(pollInterval);
}
catch (error) {
// Continue polling on errors
await page.waitForTimeout(pollInterval);
}
}
// Timeout reached
const finalState = await this.evaluateFormState(page, expectedState);
return {
success: false,
stateReached: false,
currentState: finalState,
expectedState,
waitTime: timeout,
error: 'Timeout waiting for expected form state',
message: 'Expected form state not reached within timeout'
};
}
async evaluateFormState(page, expectedState) {
return await page.evaluate((expected) => {
const state = {};
// Check form validity
if (expected.formValid !== undefined) {
const forms = document.querySelectorAll('form');
state.formValid = Array.from(forms).some(form => form.checkValidity());
}
// Check if fields are populated
if (expected.fieldsPopulated) {
state.fieldsPopulated = expected.fieldsPopulated.map((selector) => {
const element = document.querySelector(selector);
return {
selector,
populated: element ? !!element.value : false,
value: element?.value || null
};
});
}
// Check for validation errors
if (expected.errorsPresent !== undefined) {
const errorElements = document.querySelectorAll('[role="alert"], .error, .field-error, .form-error');
state.errorsPresent = errorElements.length > 0;
state.errorCount = errorElements.length;
}
// Check submit button state
if (expected.submitEnabled !== undefined) {
const submitButtons = document.querySelectorAll('[type="submit"], .submit-btn, .form-submit');
state.submitEnabled = Array.from(submitButtons).some(btn => !btn.disabled);
}
// Evaluate custom condition
if (expected.customCondition) {
try {
state.customConditionResult = eval(expected.customCondition);
}
catch (error) {
state.customConditionError = error.message;
}
}
return state;
}, expectedState);
}
stateMatches(currentState, expectedState) {
for (const [key, expectedValue] of Object.entries(expectedState)) {
if (key === 'fieldsPopulated') {
const currentFields = currentState.fieldsPopulated || [];
const allPopulated = currentFields.every((field) => field.populated);
if (!allPopulated)
return false;
}
else if (key === 'customCondition') {
if (!currentState.customConditionResult)
return false;
}
else if (currentState[key] !== expectedValue) {
return false;
}
}
return true;
}
async extractFormValidationErrors(params, sessions) {
const { sessionId, formSelector, errorSelectors = {} } = params;
if (!sessions.has(sessionId)) {
throw new Error(`Session ${sessionId} not found`);
}
const session = sessions.get(sessionId);
const page = session.page;
try {
const errors = await page.evaluate((selectors) => {
const result = {
fieldErrors: [],
generalErrors: [],
successMessages: [],
formValid: true
};
// Extract field-level errors
const fieldErrorSelector = selectors.fieldErrors || '[role="alert"], .field-error, .error';
const fieldErrors = document.querySelectorAll(fieldErrorSelector);
result.fieldErrors = Array.from(fieldErrors).map(el => ({
text: el.textContent?.trim(),
selector: el.tagName.toLowerCase() + (el.className ? '.' + el.className.split(' ').join('.') : ''),
visible: el.offsetParent !== null
}));
// Extract general form errors
const generalErrorSelector = selectors.generalErrors || '.form-error, .alert-error';
const generalErrors = document.querySelectorAll(generalErrorSelector);
result.generalErrors = Array.from(generalErrors).map(el => ({
text: el.textContent?.trim(),
selector: el.tagName.toLowerCase() + (el.className ? '.' + el.className.split(' ').join('.') : ''),
visible: el.offsetParent !== null
}));
// Extract success messages
const successSelector = selectors.successMessages || '.success, .alert-success';
const successMessages = document.querySelectorAll(successSelector);
result.successMessages = Array.from(successMessages).map(el => ({
text: el.textContent?.trim(),
selector: el.tagName.toLowerCase() + (el.className ? '.' + el.className.split(' ').join('.') : ''),
visible: el.offsetParent !== null
}));
// Check overall form validity
const forms = document.querySelectorAll('form');
result.formValid = Array.from(forms).every(form => form.checkValidity());
return result;
}, errorSelectors);
return {
success: true,
errors,
hasErrors: errors.fieldErrors.length > 0 || errors.generalErrors.length > 0,
hasSuccessMessages: errors.successMessages.length > 0,
formValid: errors.formValid,
summary: {
fieldErrors: errors.fieldErrors.length,
generalErrors: errors.generalErrors.length,
successMessages: errors.successMessages.length
},
message: `Extracted ${errors.fieldErrors.length} field errors, ${errors.generalErrors.length} general errors, ${errors.successMessages.length} success messages`
};
}
catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
message: 'Failed to extract form validation errors'
};
}
}
async createFormTestScenario(params) {
const { scenarioName, formConfig, testCases } = params;
// Store test scenario for later execution
const scenario = {
name: scenarioName,
formConfig,
testCases,
created: new Date().toISOString()
};
// In a real implementation, you'd store this in a database or file
console.log(`Form test scenario '${scenarioName}' created with ${testCases.length} test cases`);
return {
success: true,
scenarioName,
testCases: testCases.length,
formConfig: {
url: formConfig.url,
fields: formConfig.fields.length,
hasSubmit: !!formConfig.submitSelector
},
message: `Form test scenario '${scenarioName}' created successfully with ${testCases.length} test cases`
};
}
async captureValidationErrors(page) {
return await page.evaluate(() => {
const errors = [];
// Common error selectors
const errorSelectors = [
'[role="alert"]',
'.error',
'.field-error',
'.form-error',
'.invalid-feedback',
'.help-block.error'
];
for (const selector of errorSelectors) {
const elements = document.querySelectorAll(selector);
Array.from(elements).forEach(el => {
if (el.textContent?.trim()) {
errors.push({
text: el.textContent.trim(),
selector,
visible: el.offsetParent !== null
});
}
});
}
return errors;
});
}
}
//# sourceMappingURL=advanced-form-interaction-handler.js.map