UNPKG

browser-debugger-cli

Version:

DevTools telemetry in your terminal. For humans and agents. Direct WebSocket to Chrome's debugging port.

363 lines 15.8 kB
/** * Form interaction commands for filling inputs, clicking buttons, and submitting forms. */ import { DomElementResolver } from '../../commands/dom/DomElementResolver.js'; import { fillElement, clickElement, pressKeyElement, waitForActionStability, } from '../../commands/dom/formFillHelpers.js'; import { submitForm } from '../../commands/dom/formSubmitHelpers.js'; import { runCommand } from '../../commands/shared/CommandRunner.js'; import { jsonOption } from '../../commands/shared/commonOptions.js'; import { CommandError } from '../../ui/errors/index.js'; import { OutputFormatter } from '../../ui/formatting.js'; import { sessionMetadataMissingError, internalError } from '../../ui/messages/errors.js'; import { EXIT_CODES } from '../../utils/exitCodes.js'; import { filterDefined } from '../../utils/objects.js'; /** * Execute a function with an active CDP connection. * * Handles the full connection lifecycle: * 1. Validates active session * 2. Gets session metadata * 3. Verifies target exists * 4. Creates and connects CDP * 5. Executes callback * 6. Closes CDP connection (even on error) * * @param fn - Callback to execute with CDP connection * @returns Result from callback * @throws Error if session validation or connection fails * * @internal */ async function withCDPConnection(fn) { const { CDPConnection } = await import('../../connection/cdp.js'); const { validateActiveSession, getValidatedSessionMetadata, verifyTargetExists } = await import('../../commands/dom/evalHelpers.js'); validateActiveSession(); const metadata = getValidatedSessionMetadata(); const port = 9222; // Default port await verifyTargetExists(metadata, port); const cdp = new CDPConnection(); if (!metadata.webSocketDebuggerUrl) { const err = sessionMetadataMissingError('webSocketDebuggerUrl'); throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.SESSION_FILE_ERROR); } await cdp.connect(metadata.webSocketDebuggerUrl); try { return await fn(cdp, metadata); } finally { cdp.close(); } } /** * Register form interaction commands. * * @param program - Commander program instance * * @remarks * Registers the following commands: * - `bdg dom fill <selector> <value>` - Fill form fields * - `bdg dom click <selector>` - Click elements * - `bdg dom submit <selector>` - Submit forms with smart waiting */ export function registerFormInteractionCommands(program) { const domCommand = program.commands.find((cmd) => cmd.name() === 'dom'); if (!domCommand) { const err = internalError('DOM command group not found'); throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.SOFTWARE_ERROR); } domCommand .command('fill') .description('Fill a form field with a value (React-compatible, waits for stability)') .argument('<selectorOrIndex>', 'CSS selector or numeric index from query results (0-based)') .argument('<value>', 'Value to fill') .option('--index <n>', 'Element index if selector matches multiple (0-based)', parseInt) .option('--no-blur', 'Do not blur after filling (keeps focus on element)') .option('--no-wait', 'Skip waiting for network stability after fill') .addOption(jsonOption()) .action(async (selectorOrIndex, value, options) => { await runCommand(async () => { const target = await DomElementResolver.getInstance().resolve(selectorOrIndex, options.index); if (!target.success) { return { success: false, error: target.error ?? 'Failed to resolve element target', exitCode: target.exitCode ?? EXIT_CODES.INVALID_ARGUMENTS, ...(target.suggestion && { errorContext: { suggestion: target.suggestion } }), }; } return await withCDPConnection(async (cdp) => { const fillOptions = filterDefined({ index: target.index, blur: options.blur, }); const result = await fillElement(cdp, target.selector, value, fillOptions); if (!result.success) { return { success: false, error: result.error ?? 'Failed to fill element', exitCode: result.error?.includes('not found') ? EXIT_CODES.RESOURCE_NOT_FOUND : EXIT_CODES.INVALID_ARGUMENTS, errorContext: { suggestion: result.suggestion ?? 'Verify the selector matches a fillable element (input, textarea, select)', }, }; } if (options.wait !== false) { await waitForActionStability(cdp); } return { success: true, data: result }; }); }, options, formatFillOutput); }); domCommand .command('click') .description('Click an element and wait for stability (accepts selector or index)') .argument('<selectorOrIndex>', 'CSS selector or numeric index from query results (0-based)') .option('--index <n>', 'Element index if selector matches multiple (0-based)', parseInt) .option('--no-wait', 'Skip waiting for network stability after click') .addOption(jsonOption()) .action(async (selectorOrIndex, options) => { await runCommand(async () => { const target = await DomElementResolver.getInstance().resolve(selectorOrIndex, options.index); if (!target.success) { return { success: false, error: target.error ?? 'Failed to resolve element target', exitCode: target.exitCode ?? EXIT_CODES.INVALID_ARGUMENTS, ...(target.suggestion && { errorContext: { suggestion: target.suggestion } }), }; } return await withCDPConnection(async (cdp) => { const clickOptions = filterDefined({ index: target.index, }); const result = await clickElement(cdp, target.selector, clickOptions); if (!result.success) { return { success: false, error: result.error ?? 'Failed to click element', exitCode: result.error?.includes('not found') ? EXIT_CODES.RESOURCE_NOT_FOUND : EXIT_CODES.INVALID_ARGUMENTS, errorContext: { suggestion: result.suggestion ?? 'Verify the selector matches a clickable element', }, }; } if (options.wait !== false) { await waitForActionStability(cdp); } return { success: true, data: result }; }); }, options, formatClickOutput); }); domCommand .command('submit') .description('Submit a form by clicking submit button and waiting for completion') .argument('<selectorOrIndex>', 'CSS selector or numeric index from query results (0-based)') .option('--index <n>', 'Element index if selector matches multiple (0-based)', parseInt) .option('--wait-navigation', 'Wait for page navigation after submit') .option('--wait-network <ms>', 'Wait for network idle after submit (milliseconds)', '1000') .option('--timeout <ms>', 'Maximum time to wait (milliseconds)', '10000') .addOption(jsonOption()) .action(async (selectorOrIndex, options) => { await runCommand(async () => { const target = await DomElementResolver.getInstance().resolve(selectorOrIndex, options.index); if (!target.success) { return { success: false, error: target.error ?? 'Failed to resolve element target', exitCode: target.exitCode ?? EXIT_CODES.INVALID_ARGUMENTS, ...(target.suggestion && { errorContext: { suggestion: target.suggestion } }), }; } return await withCDPConnection(async (cdp) => { const submitOptions = filterDefined({ index: target.index, waitNavigation: options.waitNavigation, waitNetwork: parseInt(options.waitNetwork, 10), timeout: parseInt(options.timeout, 10), }); const result = await submitForm(cdp, target.selector, submitOptions); if (!result.success) { return { success: false, error: result.error ?? 'Failed to submit form', exitCode: result.error?.includes('not found') ? EXIT_CODES.RESOURCE_NOT_FOUND : result.error?.includes('Timeout') ? EXIT_CODES.CDP_TIMEOUT : EXIT_CODES.INVALID_ARGUMENTS, errorContext: { suggestion: result.suggestion ?? 'Verify the selector matches a form or submit button', }, }; } return { success: true, data: result }; }); }, options, formatSubmitOutput); }); domCommand .command('pressKey') .description('Press a key on an element (for Enter-to-submit, keyboard navigation)') .argument('<selectorOrIndex>', 'CSS selector or numeric index from query results (0-based)') .argument('<key>', 'Key to press (Enter, Tab, Escape, Space, ArrowUp, etc.)') .option('--index <n>', 'Element index if selector matches multiple (0-based)', parseInt) .option('--times <n>', 'Press key multiple times (default: 1)', parseInt) .option('--modifiers <mods>', 'Modifier keys: shift,ctrl,alt,meta (comma-separated)') .option('--no-wait', 'Skip waiting for network stability after key press') .addOption(jsonOption()) .action(async (selectorOrIndex, key, options) => { await runCommand(async () => { const target = await DomElementResolver.getInstance().resolve(selectorOrIndex, options.index); if (!target.success) { return { success: false, error: target.error ?? 'Failed to resolve element target', exitCode: target.exitCode ?? EXIT_CODES.INVALID_ARGUMENTS, ...(target.suggestion && { errorContext: { suggestion: target.suggestion } }), }; } return await withCDPConnection(async (cdp) => { const pressKeyOptions = filterDefined({ index: target.index, times: options.times, modifiers: options.modifiers, }); const result = await pressKeyElement(cdp, target.selector, key, pressKeyOptions); if (!result.success) { return { success: false, error: result.error ?? 'Failed to press key', exitCode: result.error?.includes('not found') ? EXIT_CODES.RESOURCE_NOT_FOUND : EXIT_CODES.INVALID_ARGUMENTS, errorContext: { suggestion: result.suggestion ?? 'Verify the selector matches a focusable element', }, }; } if (options.wait !== false) { await waitForActionStability(cdp); } return { success: true, data: result }; }); }, options, formatPressKeyOutput); }); } /** * Format fill command output for human-readable display. * * @param result - Fill result * @returns Formatted string */ function formatFillOutput(result) { const fmt = new OutputFormatter(); fmt.text('✓ Element Filled'); fmt.blank(); const details = [ ['Selector', result.selector ?? 'unknown'], ['Element Type', result.elementType ?? 'unknown'], ]; if (result.inputType) { details.push(['Input Type', result.inputType]); } if (result.checked !== undefined) { details.push(['Checked', result.checked ? 'true' : 'false']); } else if (result.value) { details.push(['Value', result.value]); } fmt.keyValueList(details, 15); return fmt.build(); } /** * Format click command output for human-readable display. * * @param result - Click result * @returns Formatted string */ function formatClickOutput(result) { const fmt = new OutputFormatter(); fmt.text('✓ Element Clicked'); fmt.blank(); fmt.keyValueList([ ['Selector', result.selector ?? 'unknown'], ['Element Type', result.elementType ?? 'unknown'], ['Clickable', result.clickable ? 'yes' : 'no (warning)'], ], 15); if (!result.clickable) { fmt.blank(); fmt.text('⚠ Warning: Element may not have a click handler'); } return fmt.build(); } /** * Format submit command output for human-readable display. * * @param result - Submit result * @returns Formatted string */ function formatSubmitOutput(result) { const fmt = new OutputFormatter(); fmt.text('✓ Form Submitted'); fmt.blank(); const details = [ ['Selector', result.selector ?? 'unknown'], ['Clicked', result.clicked ? 'yes' : 'no'], ]; if (result.networkRequests !== undefined) { details.push(['Network Requests', result.networkRequests.toString()]); } if (result.navigationOccurred !== undefined) { details.push(['Navigation', result.navigationOccurred ? 'yes' : 'no']); } if (result.waitTimeMs !== undefined) { details.push(['Wait Time', `${result.waitTimeMs}ms`]); } fmt.keyValueList(details, 20); fmt.blank(); fmt.text('Next steps:'); fmt.section('', [ 'bdg peek --network --last 10 Check network requests', 'bdg console --last 5 Check console messages', 'bdg status Check session state', ]); return fmt.build(); } /** * Format pressKey command output for human-readable display. * * @param result - Press key result * @returns Formatted string */ function formatPressKeyOutput(result) { const fmt = new OutputFormatter(); fmt.text('✓ Key Pressed'); fmt.blank(); const details = [ ['Key', result.key ?? 'unknown'], ['Selector', result.selector ?? 'unknown'], ['Element Type', result.elementType ?? 'unknown'], ]; if (result.times && result.times > 1) { details.push(['Times', result.times.toString()]); } if (result.modifiers && result.modifiers > 0) { const mods = []; if (result.modifiers & 1) mods.push('Shift'); if (result.modifiers & 2) mods.push('Ctrl'); if (result.modifiers & 4) mods.push('Alt'); if (result.modifiers & 8) mods.push('Meta'); details.push(['Modifiers', mods.join('+')]); } fmt.keyValueList(details, 15); return fmt.build(); } //# sourceMappingURL=formInteraction.js.map