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

781 lines • 37.4 kB
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