browser-debugger-cli
Version:
DevTools telemetry in your terminal. For humans and agents. Direct WebSocket to Chrome's debugging port.
239 lines (214 loc) • 6.73 kB
JavaScript
/**
* React-compatible event handling for form interactions.
*
* React uses synthetic events and doesn't detect direct DOM manipulation.
* This module provides JavaScript snippets that can be injected via Runtime.evaluate
* to properly trigger React's event system.
*/
/**
* JavaScript function to fill an input element in a React-compatible way.
*
* This approach:
* 1. Uses native property setters to bypass React's value tracking
* 2. Dispatches input/change events that React listens for
* 3. Properly handles focus/blur for form validation
*
* @remarks
* Works with React, Vue, Angular, and vanilla JS applications.
*/
export const REACT_FILL_SCRIPT = `
(function(selector, value, options) {
const allMatches = document.querySelectorAll(selector);
if (allMatches.length === 0) {
return {
success: false,
error: 'Element not found',
selector: selector
};
}
let el;
const index = options.index;
// If index is provided, use it directly (0-based)
if (typeof index === 'number' && index >= 0) {
if (index >= allMatches.length) {
return {
success: false,
error: 'Index out of range',
selector: selector,
matchCount: allMatches.length,
requestedIndex: index,
suggestion: 'Use --index between 0 and ' + (allMatches.length - 1)
};
}
el = allMatches[index];
} else {
el = allMatches[0];
}
const tagName = el.tagName.toLowerCase();
const inputType = el.type?.toLowerCase();
const isFillable = (
tagName === 'input' ||
tagName === 'textarea' ||
tagName === 'select' ||
el.isContentEditable
);
if (!isFillable) {
return {
success: false,
error: 'Element is not fillable',
elementType: tagName,
suggestion: 'Only input, textarea, select, and contenteditable elements can be filled'
};
}
el.focus();
if (tagName === 'select') {
el.value = value;
el.dispatchEvent(new Event('change', { bubbles: true }));
} else if (inputType === 'checkbox' || inputType === 'radio') {
const shouldCheck = value === 'true' || value === true;
el.checked = shouldCheck;
el.dispatchEvent(new Event('change', { bubbles: true }));
} else if (inputType === 'file') {
return {
success: false,
error: 'File inputs require CDP DOM.setFileInputFiles method',
suggestion: 'Use: bdg cdp DOM.setFileInputFiles --params {\\"files\\":[\\"path\\"]}'
};
} else if (el.isContentEditable) {
el.textContent = value;
el.dispatchEvent(new Event('input', { bubbles: true }));
} else {
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
'value'
)?.set;
const nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLTextAreaElement.prototype,
'value'
)?.set;
const setter = tagName === 'textarea'
? nativeTextAreaValueSetter
: nativeInputValueSetter;
if (setter) {
setter.call(el, value);
} else {
el.value = value;
}
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
}
if (options.blur !== false) {
el.blur();
el.dispatchEvent(new FocusEvent('focusout', { bubbles: true }));
}
return {
success: true,
selector: selector,
value: el.value || el.textContent,
elementType: tagName,
inputType: inputType || null,
checked: el.checked || undefined
};
})
`;
/**
* JavaScript function to click an element.
*
* @remarks
* Handles both direct selector matching and indexed selection.
* When index is provided, selects the nth matching element (0-based).
* When selector matches multiple elements without index, prioritizes visible ones.
*/
export const CLICK_ELEMENT_SCRIPT = `
(function(selector, index) {
const allMatches = document.querySelectorAll(selector);
if (allMatches.length === 0) {
return {
success: false,
error: 'Element not found',
selector: selector
};
}
let el;
// If index is provided, use it directly (0-based)
if (typeof index === 'number' && index >= 0) {
if (index >= allMatches.length) {
return {
success: false,
error: 'Index out of range',
selector: selector,
matchCount: allMatches.length,
requestedIndex: index,
suggestion: 'Use --index between 0 and ' + (allMatches.length - 1)
};
}
el = allMatches[index];
} else if (allMatches.length === 1) {
// Single match - use it directly
el = allMatches[0];
} else {
// Multiple matches without index - find first visible one
el = allMatches[0];
for (const candidate of allMatches) {
const style = window.getComputedStyle(candidate);
const rect = candidate.getBoundingClientRect();
const hasSize = rect.width > 0 && rect.height > 0;
const isDisplayed = style.display !== 'none' && style.visibility !== 'hidden';
const isOpaque = parseFloat(style.opacity) > 0;
const isPositioned = candidate.offsetParent !== null || style.position === 'fixed';
const isVisible = hasSize && isDisplayed && isOpaque && isPositioned;
if (isVisible) {
el = candidate;
break;
}
}
}
const tagName = el.tagName.toLowerCase();
const isClickable = (
tagName === 'button' ||
tagName === 'a' ||
tagName === 'input' ||
el.onclick !== null ||
el.getAttribute('role') === 'button' ||
window.getComputedStyle(el).cursor === 'pointer'
);
if (!isClickable) {
console.warn('Warning: Element may not be clickable:', el);
}
el.scrollIntoView({ behavior: 'auto', block: 'center' });
el.click();
return {
success: true,
selector: selector,
elementType: tagName,
clickable: isClickable,
matchCount: allMatches.length,
selectedIndex: typeof index === 'number' ? index : undefined
};
})
`;
/**
* Type guard for FillResult.
*
* @param value - Value to check
* @returns True if value is a valid FillResult
*/
export function isFillResult(value) {
if (typeof value !== 'object' || value === null)
return false;
const obj = value;
return typeof obj['success'] === 'boolean';
}
/**
* Type guard for ClickResult.
*
* @param value - Value to check
* @returns True if value is a valid ClickResult
*/
export function isClickResult(value) {
if (typeof value !== 'object' || value === null)
return false;
const obj = value;
return typeof obj['success'] === 'boolean';
}
//# sourceMappingURL=reactEventHelpers.js.map