@probolabs/playwright 1.5.0 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -1
- package/dist/cli.js +56 -45
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +193 -219
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +193 -219
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/test-suite-runner.d.ts.map +1 -1
- package/export-readme-template.md +49 -0
- package/package.json +4 -3
package/dist/index.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const highlighterCode = "(function (global, factory) {\n typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :\n typeof define === 'function' && define.amd ? define(['exports'], factory) :\n (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.ProboLabs = {}));\n})(this, (function (exports) { 'use strict';\n\n /**\n * Element tag constants for different types of interactive elements\n */\n const ElementTag = {\n CLICKABLE: \"CLICKABLE\",\n FILLABLE: \"FILLABLE\",\n SELECTABLE: \"SELECTABLE\",\n NON_INTERACTIVE_ELEMENT: 'NON_INTERACTIVE_ELEMENT',\n };\n\n var ApplyAIStatus;\n (function (ApplyAIStatus) {\n ApplyAIStatus[\"PREPARE_START\"] = \"PREPARE_START\";\n ApplyAIStatus[\"PREPARE_SUCCESS\"] = \"PREPARE_SUCCESS\";\n ApplyAIStatus[\"PREPARE_ERROR\"] = \"PREPARE_ERROR\";\n ApplyAIStatus[\"SEND_START\"] = \"SEND_START\";\n ApplyAIStatus[\"SEND_SUCCESS\"] = \"SEND_SUCCESS\";\n ApplyAIStatus[\"SEND_ERROR\"] = \"SEND_ERROR\";\n ApplyAIStatus[\"APPLY_AI_ERROR\"] = \"APPLY_AI_ERROR\";\n ApplyAIStatus[\"APPLY_AI_CANCELLED\"] = \"APPLY_AI_CANCELLED\";\n ApplyAIStatus[\"SUMMARY_COMPLETED\"] = \"SUMMARY_COMPLETED\";\n ApplyAIStatus[\"SUMMARY_ERROR\"] = \"SUMMARY_ERROR\";\n })(ApplyAIStatus || (ApplyAIStatus = {}));\n var ReplayStatus;\n (function (ReplayStatus) {\n ReplayStatus[\"REPLAY_START\"] = \"REPLAY_START\";\n ReplayStatus[\"REPLAY_SUCCESS\"] = \"REPLAY_SUCCESS\";\n ReplayStatus[\"REPLAY_ERROR\"] = \"REPLAY_ERROR\";\n ReplayStatus[\"REPLAY_CANCELLED\"] = \"REPLAY_CANCELLED\";\n })(ReplayStatus || (ReplayStatus = {}));\n\n // Action constants\n var PlaywrightAction;\n (function (PlaywrightAction) {\n PlaywrightAction[\"VISIT_BASE_URL\"] = \"VISIT_BASE_URL\";\n PlaywrightAction[\"VISIT_URL\"] = \"VISIT_URL\";\n PlaywrightAction[\"CLICK\"] = \"CLICK\";\n PlaywrightAction[\"FILL_IN\"] = \"FILL_IN\";\n PlaywrightAction[\"SELECT_DROPDOWN\"] = \"SELECT_DROPDOWN\";\n PlaywrightAction[\"SELECT_MULTIPLE_DROPDOWN\"] = \"SELECT_MULTIPLE_DROPDOWN\";\n PlaywrightAction[\"CHECK_CHECKBOX\"] = \"CHECK_CHECKBOX\";\n PlaywrightAction[\"SELECT_RADIO\"] = \"SELECT_RADIO\";\n PlaywrightAction[\"TOGGLE_SWITCH\"] = \"TOGGLE_SWITCH\";\n PlaywrightAction[\"SET_SLIDER\"] = \"SET_SLIDER\";\n PlaywrightAction[\"TYPE_KEYS\"] = \"TYPE_KEYS\";\n PlaywrightAction[\"HOVER\"] = \"HOVER\";\n PlaywrightAction[\"ASSERT_EXACT_VALUE\"] = \"ASSERT_EXACT_VALUE\";\n PlaywrightAction[\"ASSERT_CONTAINS_VALUE\"] = \"ASSERT_CONTAINS_VALUE\";\n PlaywrightAction[\"ASSERT_URL\"] = \"ASSERT_URL\";\n PlaywrightAction[\"SCROLL_TO_ELEMENT\"] = \"SCROLL_TO_ELEMENT\";\n PlaywrightAction[\"EXTRACT_VALUE\"] = \"EXTRACT_VALUE\";\n PlaywrightAction[\"ASK_AI\"] = \"ASK_AI\";\n PlaywrightAction[\"EXECUTE_SCRIPT\"] = \"EXECUTE_SCRIPT\";\n PlaywrightAction[\"UPLOAD_FILES\"] = \"UPLOAD_FILES\";\n PlaywrightAction[\"WAIT_FOR\"] = \"WAIT_FOR\";\n PlaywrightAction[\"WAIT_FOR_OTP\"] = \"WAIT_FOR_OTP\";\n PlaywrightAction[\"GEN_TOTP\"] = \"GEN_TOTP\";\n // Sequence marker actions (not real interactions, used only for grouping)\n PlaywrightAction[\"SEQUENCE_START\"] = \"SEQUENCE_START\";\n PlaywrightAction[\"SEQUENCE_END\"] = \"SEQUENCE_END\";\n })(PlaywrightAction || (PlaywrightAction = {}));\n\n /**\n * Checks if any string in the array matches the given regular expression pattern.\n *\n * @param array - Array of strings to test.\n * @param pattern - Regular expression to test against each string.\n * @returns true if at least one string matches the pattern, false otherwise.\n */\n function testArray(array, pattern) {\n return array.some(item => pattern.test(item));\n }\n /**\n * Determines if an element is clickable\n *\n * @param element The DOM element to check\n * @returns boolean indicating if the element is fillable\n */\n function isClickableElement(element) {\n if (!element)\n return false;\n let depth = 0;\n while (depth < 5 && element && element.nodeType === Node.ELEMENT_NODE) {\n if (isClickableElementHelper(element) === IsClickable.YES)\n return true;\n if (isClickableElementHelper(element) === IsClickable.NO)\n return false;\n // if maybe, continue searching up to 5 levels up the DOM tree\n element = element.parentNode;\n depth++;\n }\n return false;\n }\n // clickable element detection result\n var IsClickable;\n (function (IsClickable) {\n IsClickable[\"YES\"] = \"YES\";\n IsClickable[\"NO\"] = \"NO\";\n IsClickable[\"MAYBE\"] = \"MAYBE\";\n })(IsClickable || (IsClickable = {}));\n function isClickableElementHelper(element) {\n var _a, _b;\n if (!element)\n return IsClickable.NO;\n //check for tag name\n const tagName = element.tagName.toLowerCase();\n const clickableTags = [\n 'a', 'button',\n ];\n if (clickableTags.includes(tagName))\n return IsClickable.YES;\n //check for clickable <input>\n const inputType = (_a = element.type) === null || _a === void 0 ? void 0 : _a.toLowerCase();\n const clickableTypes = [\n 'button', 'submit', 'reset', 'checkbox', 'radio',\n ];\n const ariaAutocompleteValues = [\n 'list', 'both',\n ];\n if (tagName === 'input') {\n if (clickableTypes.includes(inputType) || ariaAutocompleteValues.includes((_b = element.getAttribute('aria-autocomplete')) !== null && _b !== void 0 ? _b : ''))\n return IsClickable.YES;\n if (['date', 'number', 'range'].includes(inputType))\n return IsClickable.NO; //don't record the click as a change event will be generated for elements that generate an input change event\n }\n // check for cursor type\n const style = window.getComputedStyle(element);\n if (style.cursor === 'pointer')\n return IsClickable.YES;\n // check for attributes\n const clickableRoles = [\n 'button', 'combobox', 'listbox', 'dropdown', 'option', 'menu', 'menuitem',\n 'navigation', 'checkbox', 'switch', 'toggle', 'slider', 'textbox', 'listitem',\n 'presentation',\n ];\n const ariaPopupValues = [\n 'true', 'listbox', 'menu',\n ];\n if (element.hasAttribute('onclick') ||\n clickableRoles.includes(element.getAttribute('role') || '') ||\n ariaPopupValues.includes(element.getAttribute('aria-haspopup') || ''))\n return IsClickable.YES;\n // check for tabindex (means element is focusable and therefore clickable)\n if (parseInt(element.getAttribute('tabindex') || '-1') >= 0)\n return IsClickable.YES;\n // extract class names\n const classNames = Array.from(element.classList);\n // check for checkbox/radio-like class name - TODO: check if can be removed\n const checkboxPattern = /checkbox|switch|toggle|slider/i;\n if (testArray(classNames, checkboxPattern))\n return IsClickable.YES;\n // check for Material UI class names\n const muiClickableClassPattern = /MuiButton|MuiIconButton|MuiChip|MuiMenuItem|MuiListItem|MuiInputBase|MuiOutlinedInput|MuiSelect|MuiAutocomplete|MuiToggleButton|MuiBackdrop-root|MuiBackdrop-invisible/;\n if (testArray(classNames, muiClickableClassPattern))\n return IsClickable.YES;\n // check for SalesForce class names\n const sfClassPattern = /slds-button|slds-dropdown|slds-combobox|slds-picklist|slds-tabs|slds-pill|slds-action|slds-row-action|slds-context-bar|slds-input|slds-rich-text-area|slds-radio|slds-checkbox|slds-toggle|slds-link|slds-accordion|slds-tree/;\n if (testArray(classNames, sfClassPattern))\n return IsClickable.YES;\n // check for chart dots\n const chartClickableClassPattern = /recharts-dot/;\n if (testArray(classNames, chartClickableClassPattern))\n return IsClickable.YES;\n // check for React component classes\n const reactClickableClassPattern = /react-select|ant-select|rc-select|react-dropdown|react-autocomplete|react-datepicker|react-modal|react-tooltip|react-popover|react-menu|react-tabs|react-accordion|react-collapse|react-toggle|react-switch|react-checkbox|react-radio|react-button|react-link|react-card|react-list-item|react-menu-item|react-option|react-tab|react-panel|react-drawer|react-sidebar|react-nav|react-breadcrumb|react-pagination|react-stepper|react-wizard|react-carousel|react-slider|react-range|react-progress|react-badge|react-chip|react-tag|react-avatar|react-icon|react-fab|react-speed-dial|react-floating|react-sticky|react-affix|react-backdrop|react-overlay|react-portal|react-transition|react-animate|react-spring|react-framer|react-gesture|react-drag|react-drop|react-sortable|react-resizable|react-split|react-grid|react-table|react-datagrid|react-tree|react-treeview|react-file|react-upload|react-cropper|react-image|react-gallery|react-lightbox|react-player|react-video|react-audio|react-chart|react-graph|react-diagram|react-flow|react-d3|react-plotly|react-vega|react-vis|react-nivo|react-recharts|react-victory|react-echarts|react-highcharts|react-google-charts|react-fusioncharts|react-apexcharts|react-chartjs|react-chartkick|react-sparklines|react-trend|react-smooth|react-animated|react-lottie|react-spring|react-framer-motion|react-pose|react-motion|react-transition-group|react-router|react-navigation/i;\n if (testArray(classNames, reactClickableClassPattern))\n return IsClickable.YES;\n //check for cloudinary class names\n const cloudinaryClickableClassPattern = /cld-combobox|cld-upload-button|cld-controls|cld-player|cld-tab|cld-menu-item|cld-close|cld-play|cld-pause|cld-fullscreen|cld-browse|cld-cancel|cld-retry/;\n if (testArray(classNames, cloudinaryClickableClassPattern))\n return IsClickable.YES;\n return IsClickable.MAYBE;\n }\n function getParentNode(element) {\n if (!element || element.nodeType !== Node.ELEMENT_NODE)\n return null;\n let parent = null;\n // SF is using slots and shadow DOM heavily\n // However, there might be slots in the light DOM which shouldn't be traversed\n if (element.assignedSlot && element.getRootNode() instanceof ShadowRoot)\n parent = element.assignedSlot;\n else\n parent = element.parentNode;\n // Check if we're at a shadow root\n if (parent && parent.nodeType !== Node.ELEMENT_NODE && parent.getRootNode() instanceof ShadowRoot)\n parent = parent.getRootNode().host;\n return parent;\n }\n function getElementDepth(element) {\n let depth = 0;\n let currentElement = element;\n while ((currentElement === null || currentElement === void 0 ? void 0 : currentElement.nodeType) === Node.ELEMENT_NODE) {\n depth++;\n currentElement = getParentNode(currentElement);\n }\n return depth;\n }\n\n // WebSocketsMessageType enum for WebSocket and event message types shared across the app\n var WebSocketsMessageType;\n (function (WebSocketsMessageType) {\n WebSocketsMessageType[\"INTERACTION_APPLY_AI_PREPARE_START\"] = \"INTERACTION_APPLY_AI_PREPARE_START\";\n WebSocketsMessageType[\"INTERACTION_APPLY_AI_PREPARE_SUCCESS\"] = \"INTERACTION_APPLY_AI_PREPARE_SUCCESS\";\n WebSocketsMessageType[\"INTERACTION_APPLY_AI_PREPARE_ERROR\"] = \"INTERACTION_APPLY_AI_PREPARE_ERROR\";\n WebSocketsMessageType[\"INTERACTION_APPLY_AI_SEND_TO_LLM_START\"] = \"INTERACTION_APPLY_AI_SEND_TO_LLM_START\";\n WebSocketsMessageType[\"INTERACTION_APPLY_AI_SEND_TO_LLM_SUCCESS\"] = \"INTERACTION_APPLY_AI_SEND_TO_LLM_SUCCESS\";\n WebSocketsMessageType[\"INTERACTION_APPLY_AI_SEND_TO_LLM_ERROR\"] = \"INTERACTION_APPLY_AI_SEND_TO_LLM_ERROR\";\n WebSocketsMessageType[\"INTERACTION_REPLAY_START\"] = \"INTERACTION_REPLAY_START\";\n WebSocketsMessageType[\"INTERACTION_REPLAY_SUCCESS\"] = \"INTERACTION_REPLAY_SUCCESS\";\n WebSocketsMessageType[\"INTERACTION_REPLAY_ERROR\"] = \"INTERACTION_REPLAY_ERROR\";\n WebSocketsMessageType[\"INTERACTION_REPLAY_CANCELLED\"] = \"INTERACTION_REPLAY_CANCELLED\";\n WebSocketsMessageType[\"INTERACTION_APPLY_AI_CANCELLED\"] = \"INTERACTION_APPLY_AI_CANCELLED\";\n WebSocketsMessageType[\"INTERACTION_APPLY_AI_ERROR\"] = \"INTERACTION_APPLY_AI_ERROR\";\n WebSocketsMessageType[\"INTERACTION_APPLY_AI_SUMMARY_COMPLETED\"] = \"INTERACTION_APPLY_AI_SUMMARY_COMPLETED\";\n WebSocketsMessageType[\"INTERACTION_APPLY_AI_SUMMARY_ERROR\"] = \"INTERACTION_APPLY_AI_SUMMARY_ERROR\";\n WebSocketsMessageType[\"OTP_RETRIEVED\"] = \"OTP_RETRIEVED\";\n WebSocketsMessageType[\"TEST_SUITE_RUN_START\"] = \"TEST_SUITE_RUN_START\";\n WebSocketsMessageType[\"TEST_SUITE_RUN_LOG\"] = \"TEST_SUITE_RUN_LOG\";\n WebSocketsMessageType[\"TEST_SUITE_RUN_COMPLETE\"] = \"TEST_SUITE_RUN_COMPLETE\";\n WebSocketsMessageType[\"TEST_SUITE_RUN_ERROR\"] = \"TEST_SUITE_RUN_ERROR\";\n WebSocketsMessageType[\"TEST_SUITE_RUN_REPORTER_EVENT\"] = \"TEST_SUITE_RUN_REPORTER_EVENT\";\n })(WebSocketsMessageType || (WebSocketsMessageType = {}));\n\n /**\n * Logging levels for Probo\n */\n var ProboLogLevel;\n (function (ProboLogLevel) {\n ProboLogLevel[\"DEBUG\"] = \"DEBUG\";\n ProboLogLevel[\"INFO\"] = \"INFO\";\n ProboLogLevel[\"LOG\"] = \"LOG\";\n ProboLogLevel[\"WARN\"] = \"WARN\";\n ProboLogLevel[\"ERROR\"] = \"ERROR\";\n })(ProboLogLevel || (ProboLogLevel = {}));\n const logLevelOrder = {\n [ProboLogLevel.DEBUG]: 0,\n [ProboLogLevel.INFO]: 1,\n [ProboLogLevel.LOG]: 2,\n [ProboLogLevel.WARN]: 3,\n [ProboLogLevel.ERROR]: 4,\n };\n class ProboLogger {\n constructor(prefix, level = ProboLogLevel.INFO) {\n this.prefix = prefix;\n this.level = level;\n }\n setLogLevel(level) {\n console.log(`[${this.prefix}] Setting log level to: ${level} (was: ${this.level})`);\n this.level = level;\n }\n shouldLog(level) {\n return logLevelOrder[level] >= logLevelOrder[this.level];\n }\n preamble(level) {\n const now = new Date();\n const hours = String(now.getHours()).padStart(2, '0');\n const minutes = String(now.getMinutes()).padStart(2, '0');\n const seconds = String(now.getSeconds()).padStart(2, '0');\n const milliseconds = String(now.getMilliseconds()).padStart(3, '0');\n return `[${hours}:${minutes}:${seconds}.${milliseconds}] [${this.prefix}] [${level}]`;\n }\n debug(...args) { if (this.shouldLog(ProboLogLevel.DEBUG))\n console.debug(this.preamble(ProboLogLevel.DEBUG), ...args); }\n info(...args) { if (this.shouldLog(ProboLogLevel.INFO))\n console.info(this.preamble(ProboLogLevel.INFO), ...args); }\n log(...args) { if (this.shouldLog(ProboLogLevel.LOG))\n console.log(this.preamble(ProboLogLevel.LOG), ...args); }\n warn(...args) { if (this.shouldLog(ProboLogLevel.WARN))\n console.warn(this.preamble(ProboLogLevel.WARN), ...args); }\n error(...args) { if (this.shouldLog(ProboLogLevel.ERROR))\n console.error(this.preamble(ProboLogLevel.ERROR), ...args); }\n }\n new ProboLogger('fetch');\n function doubleQuoteString(str) {\n if (!str)\n return '';\n return `\"${str.replace(/\"/g, '\\\\\"')}\"`;\n }\n\n new ProboLogger('apiclient');\n\n /**\n * Available AI models for LLM operations\n */\n var AIModel;\n (function (AIModel) {\n AIModel[\"AZURE_GPT4\"] = \"azure-gpt4\";\n AIModel[\"AZURE_GPT4_MINI\"] = \"azure-gpt4-mini\";\n AIModel[\"GEMINI_1_5_FLASH\"] = \"gemini-1.5-flash\";\n AIModel[\"GEMINI_2_5_FLASH\"] = \"gemini-2.5-flash\";\n AIModel[\"GPT4\"] = \"gpt4\";\n AIModel[\"GPT4_MINI\"] = \"gpt4-mini\";\n AIModel[\"CLAUDE_3_5\"] = \"claude-3.5\";\n AIModel[\"CLAUDE_SONNET_4_5\"] = \"claude-sonnet-4.5\";\n AIModel[\"CLAUDE_HAIKU_4_5\"] = \"claude-haiku-4.5\";\n AIModel[\"CLAUDE_OPUS_4_1\"] = \"claude-opus-4.1\";\n AIModel[\"GROK_2\"] = \"grok-2\";\n AIModel[\"LLAMA_4_SCOUT\"] = \"llama-4-scout\";\n AIModel[\"DEEPSEEK_V3\"] = \"deepseek-v3\";\n })(AIModel || (AIModel = {}));\n\n const highlighterLogger = new ProboLogger('highlighter');\n\n // Get text content of an element (recursively including child elements)\n function getElementText(element) {\n if (!element) return '';\n return element.textContent || '';\n }\n\n // Get placeholder text of an element\n function getElementPlaceholder(element) {\n if (!element) return '';\n return element.placeholder || '';\n }\n\n // Get CSS selector of an element (not recursively, just for a single element)\n function getElementCssSelector(element) {\n if (!(element instanceof Element)) return '';\n \n const tag = element.tagName.toLowerCase();\n let selector = tag;\n \n // Find nth-of-type for elements without ID\n let nth = 1;\n let sibling = element.previousElementSibling;\n while (sibling) {\n if (sibling.nodeName.toLowerCase() === tag) nth++;\n sibling = sibling.previousElementSibling;\n }\n if (nth > 1 || element.nextElementSibling) selector += `:nth-of-type(${nth})`;\n \n return selector;\n }\n\n // Helper function to check if element is a form control\n function isFormControl(elementInfo) {\n return /^(input|select|textarea|button|label)$/i.test(elementInfo.tag);\n }\n\n // Helper function to check if element is a dropdown item\n const isDropdownItem = (elementInfo) => {\n const dropdownPatterns = [\n /dropdown[-_]?item/i, // matches: dropdown-item, dropdownitem, dropdown_item\n /menu[-_]?item/i, // matches: menu-item, menuitem, menu_item\n /dropdown[-_]?link/i, // matches: dropdown-link, dropdownlink, dropdown_link\n /list[-_]?item/i, // matches: list-item, listitem, list_item\n /select[-_]?item/i, // matches: select-item, selectitem, select_item \n ];\n\n const rolePatterns = [\n /menu[-_]?item/i, // matches: menuitem, menu-item\n /option/i, // matches: option\n /list[-_]?item/i, // matches: listitem, list-item\n /tree[-_]?item/i // matches: treeitem, tree-item\n ];\n\n const hasMatchingClass = elementInfo.element.className && \n dropdownPatterns.some(pattern => \n pattern.test(elementInfo.element.className)\n );\n\n const hasMatchingRole = elementInfo.element.getAttribute('role') && \n rolePatterns.some(pattern => \n pattern.test(elementInfo.element.getAttribute('role'))\n );\n\n return hasMatchingClass || hasMatchingRole;\n };\n\n /**\n * Element Query Utilities\n */\n\n function getAllDocumentElementsIncludingShadow(selectors, root = document) {\n const elements = Array.from(root.querySelectorAll(selectors));\n\n root.querySelectorAll('*').forEach(el => {\n if (el.shadowRoot) {\n elements.push(...getAllDocumentElementsIncludingShadow(selectors, el.shadowRoot));\n }\n });\n return elements;\n }\n\n function getAllFrames(root = document) {\n const result = [root];\n const frames = getAllDocumentElementsIncludingShadow('frame, iframe', root); \n frames.forEach(frame => {\n try {\n const frameDocument = frame.contentDocument || frame.contentWindow.document;\n if (frameDocument) {\n result.push(frameDocument);\n }\n } catch (e) {\n // Skip cross-origin frames\n highlighterLogger.warn('Could not access frame content:', e.message);\n }\n });\n\n return result;\n }\n\n function getAllElementsIncludingShadow(selectors, root = document) {\n const elements = [];\n\n getAllFrames(root).forEach(doc => {\n elements.push(...getAllDocumentElementsIncludingShadow(selectors, doc));\n });\n\n return elements;\n }\n\n /**\n * Finds the first element matching a CSS selector, traversing Shadow DOM if necessary\n * @param {string} selector - CSS selector to search for\n * @param {Element} [root=document] - Root element to start searching from\n * @returns {Element|null} - The first matching element or null if not found\n */\n function robustQuerySelector(selector, root = document, all = false) {\n // First try to find in light DOM\n let elements = all ? root.querySelectorAll(selector) : root.querySelector(selector);\n if (elements) return elements;\n \n // Get all shadow roots\n const shadowElements = Array.from(root.querySelectorAll('*'))\n .filter(el => el.shadowRoot);\n \n // Search through each shadow root until we find a match\n for (const el of shadowElements) {\n elements = robustQuerySelector(selector, el.shadowRoot, all);\n if (elements) return elements;\n }\n \n return all ? [] : null;\n }\n\n\n function isDecendent(parent, child) {\n let element = child;\n while (element && element !== parent && element.nodeType === Node.ELEMENT_NODE) { \n element = getParentNode(element); \n }\n return element === parent;\n }\n\n function generateXPath(element) {\n return '/'+extractElementPath(element).map(item => `${item.tagName}${item.onlyChild ? '' : `[${item.index}]`}`).join('/');\n }\n\n function generateCssPath(element) {\n return extractElementPath(element).map(item => `${item.tagName}:nth-of-type(${item.index})`).join(' > ');\n }\n\n // unified path generation for xpath and css selector\n function extractElementPath(element) {\n if (!element) {\n highlighterLogger.error('ERROR: No element provided to generatePath');\n return [];\n }\n const path = [];\n // traversing up the DOM tree\n while (element && element.nodeType === Node.ELEMENT_NODE) { \n let tagName = element.nodeName.toLowerCase();\n \n let sibling = element;\n let index = 1;\n \n while (sibling = sibling.previousElementSibling) {\n if (sibling.nodeName.toLowerCase() === tagName) index++;\n }\n sibling = element;\n \n let onlyChild = (index === 1);\n while (onlyChild && (sibling = sibling.nextElementSibling)) {\n if (sibling.nodeName.toLowerCase() === tagName) onlyChild = false;\n }\n \n // add a tuple with tagName, index (nth), and onlyChild \n path.unshift({\n tagName: tagName,\n index: index,\n onlyChild: onlyChild \n }); \n\n element = getParentNode(element);\n }\n \n return path;\n }\n\n function cleanHTML(rawHTML) {\n const parser = new DOMParser();\n const doc = parser.parseFromString(rawHTML, \"text/html\");\n\n function cleanElement(element) {\n const allowedAttributes = new Set([\n \"role\",\n \"type\",\n \"class\",\n \"href\",\n \"alt\",\n \"title\",\n \"readonly\",\n \"checked\",\n \"enabled\",\n \"disabled\",\n ]);\n\n [...element.attributes].forEach(attr => {\n const name = attr.name.toLowerCase();\n const value = attr.value;\n\n const isTestAttribute = /^(testid|test-id|data-test-id)$/.test(name);\n const isDataAttribute = name.startsWith(\"data-\") && value;\n const isBooleanAttribute = [\"readonly\", \"checked\", \"enabled\", \"disabled\"].includes(name);\n\n if (!allowedAttributes.has(name) && !isDataAttribute && !isTestAttribute && !isBooleanAttribute) {\n element.removeAttribute(name);\n }\n });\n\n // Handle SVG content - more aggressive replacement\n if (element.tagName.toLowerCase() === \"svg\") {\n // Remove all attributes except class and role\n [...element.attributes].forEach(attr => {\n const name = attr.name.toLowerCase();\n if (name !== \"class\" && name !== \"role\") {\n element.removeAttribute(name);\n }\n });\n element.innerHTML = \"CONTENT REMOVED\";\n } else {\n // Recursively clean child elements\n Array.from(element.children).forEach(cleanElement);\n }\n\n // Only remove empty elements that aren't semantic or icon elements\n const keepEmptyElements = ['i', 'span', 'svg', 'button', 'input'];\n if (!keepEmptyElements.includes(element.tagName.toLowerCase()) && \n !element.children.length && \n !element.textContent.trim()) {\n element.remove();\n }\n }\n\n // Process all elements in the document body\n Array.from(doc.body.children).forEach(cleanElement);\n return doc.body.innerHTML;\n }\n\n function getContainingIFrame(element) {\n // If not in an iframe, return null\n if (element.ownerDocument.defaultView === window.top) {\n return null;\n }\n \n // Try to find the iframe in the parent document that contains our element\n try {\n const parentDocument = element.ownerDocument.defaultView.parent.document;\n const iframes = parentDocument.querySelectorAll('iframe');\n \n for (const iframe of iframes) {\n if (iframe.contentWindow === element.ownerDocument.defaultView) {\n return iframe;\n }\n }\n } catch (e) {\n // Cross-origin restriction\n return \"Cross-origin iframe - cannot access details\";\n }\n \n return null;\n }\n\n function getAriaLabelledByText(elementInfo, includeHidden=true) {\n if (!elementInfo.element.hasAttribute('aria-labelledby')) return '';\n\n const ids = elementInfo.element.getAttribute('aria-labelledby').split(/\\s+/);\n let labelText = '';\n\n //locate root (document or iFrame document if element is contained in an iframe)\n let root = document;\n if (elementInfo.iframe_selector) { \n const frames = getAllDocumentElementsIncludingShadow('iframe', document);\n \n // Iterate over all frames and compare their CSS selectors\n for (const frame of frames) {\n const selector = generateCssPath(frame);\n if (selector === elementInfo.iframe_selector) {\n root = frame.contentDocument || frame.contentWindow.document; \n break;\n }\n } \n }\n\n ids.forEach(id => {\n const el = robustQuerySelector(`#${CSS.escape(id)}`, root);\n if (el) {\n if (includeHidden || el.offsetParent !== null || getComputedStyle(el).display !== 'none') {\n labelText += el.textContent.trim() + ' ';\n }\n }\n });\n\n return labelText.trim();\n }\n\n\n\n const filterZeroDimensions = (elementInfo) => {\n const rect = elementInfo.bounding_box;\n //single pixel elements are typically faux controls and should be filtered too\n const hasSize = rect.width > 1 && rect.height > 1;\n const style = window.getComputedStyle(elementInfo.element);\n const isVisible = style.display !== 'none' && style.visibility !== 'hidden';\n \n if (!hasSize || !isVisible) {\n \n return false;\n }\n return true;\n };\n\n function filterNestedElements(nonZeroElements, parentCandidates, shouldKeepNestedElementFn) {\n /**\n * Filter elements based on parent/child relationships.\n * @param {Array} nonZeroElements - Elements sorted by depth (parents first)\n * @param {Set} parentCandidates - Set of all candidate parent CSS selectors (all nonZeroElements)\n * @param {Function} shouldKeepNestedElementFn - Function(element_info, parentPath, parentElementInfo) => boolean\n * @returns {Array} Filtered elements\n */\n return nonZeroElements.filter(element_info => {\n const parent = findClosestParent(parentCandidates, element_info);\n \n if (parent == null) {\n // No parent found, keep element\n return true;\n }\n \n // Find parent element_info\n const parentElementInfo = nonZeroElements.find(e => e.css_selector === parent);\n \n // Use custom function to determine if we should keep nested element\n return shouldKeepNestedElementFn(element_info, parent, parentElementInfo);\n });\n }\n\n function getDirectHandlers(element) {\n /**\n * Get direct handlers for an element (no bubbling check).\n * Returns array of handler objects.\n */\n const handlers = [];\n \n // Check onclick\n if (element.hasAttribute('onclick') || element.onclick) {\n handlers.push({\n type: 'onclick',\n source: element.hasAttribute('onclick') ? 'attribute' : 'property',\n value: element.getAttribute('onclick') || 'function'\n });\n }\n \n // Check React Fiber\n const reactKey = Object.keys(element).find(key => \n key.startsWith('__reactFiber') || key.startsWith('__reactInternalInstance')\n );\n if (reactKey) {\n const fiber = element[reactKey];\n if (fiber?.memoizedProps?.onClick || fiber?.pendingProps?.onClick) {\n handlers.push({\n type: 'react-onClick',\n source: 'react-fiber',\n hasMemoized: !!fiber?.memoizedProps?.onClick,\n hasPending: !!fiber?.pendingProps?.onClick\n });\n }\n }\n \n return handlers;\n }\n\n function getElementHandlersWithBubbling(element, depth = 0) {\n /**\n * Get handler info including bubbling (recursive DOM traversal).\n * Uses actual DOM relationships (parentElement) instead of CSS selector parsing.\n * Cursor uses getComputedStyle() which already includes inheritance.\n * @param {HTMLElement} element - The element to check\n * @param {number} depth - Recursion depth (safety limit)\n */\n // Safety check: null/undefined element\n if (!element) {\n return { handlers: [], cursorPointer: false, handlerSource: 'none' };\n }\n \n // Safety check: prevent infinite recursion (circular parent references)\n if (depth > 100) {\n throw new Error(`Recursion depth limit exceeded (${depth}) in getElementHandlersWithBubbling. Possible circular parent reference. Element: ${element.tagName || 'unknown'}`);\n }\n \n const directHandlers = getDirectHandlers(element);\n \n // If element has direct handlers, return them\n if (directHandlers.length > 0) {\n // Cursor: getComputedStyle already includes inheritance!\n let cursorPointer = false;\n try {\n const style = window.getComputedStyle(element);\n cursorPointer = style.cursor === 'pointer';\n } catch (e) {\n // getComputedStyle can fail in edge cases (e.g., detached elements)\n // Default to false\n }\n \n return {\n handlers: directHandlers,\n cursorPointer: cursorPointer,\n handlerSource: 'direct'\n };\n }\n \n // No direct handlers - check parent for bubbled handlers (using DOM, not CSS selectors)\n if (element.parentElement) {\n const parentResult = getElementHandlersWithBubbling(element.parentElement, depth + 1);\n if (parentResult.handlers.length > 0) {\n // Parent has handlers that would bubble to this element\n // Cursor: getComputedStyle already includes inheritance!\n let cursorPointer = false;\n try {\n const style = window.getComputedStyle(element);\n cursorPointer = style.cursor === 'pointer';\n } catch (e) {\n // getComputedStyle can fail in edge cases (e.g., detached elements)\n // Default to false\n }\n \n return {\n handlers: parentResult.handlers.map(h => ({\n ...h,\n source: 'bubbled'\n })),\n cursorPointer: cursorPointer,\n handlerSource: 'bubbled'\n };\n }\n }\n \n // No handlers at all\n let cursorPointer = false;\n try {\n const style = window.getComputedStyle(element);\n cursorPointer = style.cursor === 'pointer';\n } catch (e) {\n // getComputedStyle can fail in edge cases (e.g., detached elements)\n // Default to false\n }\n \n return {\n handlers: [],\n cursorPointer: cursorPointer,\n handlerSource: 'none'\n };\n }\n\n function normalizeHandlers(handlers) {\n /**\n * Normalize handlers for comparison (ignore source field, deduplicate).\n * Returns sorted array of unique normalized handler objects.\n */\n const seen = new Set();\n const normalized = [];\n \n for (const h of handlers) {\n // Safety check: skip malformed handlers\n if (!h || !h.type) continue;\n \n const key = h.type === 'react-onClick' \n ? 'react-onClick' \n : `${h.type}:${h.value || 'function'}`;\n \n if (!seen.has(key)) {\n seen.add(key);\n normalized.push({\n type: h.type,\n value: h.type === 'react-onClick' ? undefined : (h.value || 'function')\n });\n }\n }\n \n return normalized.sort((a, b) => {\n if (a.type !== b.type) return a.type.localeCompare(b.type);\n return (a.value || '').localeCompare(b.value || '');\n });\n }\n\n function compareHandlersForUniquify(childHandlers, parentHandlers) {\n /**\n * Compare handlers between child and parent for uniquification.\n * Returns true if handlers are different (should keep child), false if same (can unify).\n */\n const childNormalized = normalizeHandlers(childHandlers.handlers);\n const parentNormalized = normalizeHandlers(parentHandlers.handlers);\n \n const handlersEqual = JSON.stringify(childNormalized) === JSON.stringify(parentNormalized);\n const cursorEqual = childHandlers.cursorPointer === parentHandlers.cursorPointer;\n \n // If handlers or cursor differ, keep child (don't unify)\n return !handlersEqual || !cursorEqual;\n }\n\n function uniquifyElements(elements) {\n highlighterLogger.debug(`Starting uniquification with ${elements.length} elements`);\n\n // Filter out testing infrastructure elements first\n const filteredInfrastructure = elements.filter(element_info => {\n // Skip the highlight-overlay element completely - it's part of the testing infrastructure\n if (element_info.element.id === 'highlight-overlay' || \n (element_info.css_selector && element_info.css_selector.includes('#highlight-overlay'))) {\n highlighterLogger.debug('Filtered out testing infrastructure element:', element_info.css_selector);\n return false;\n }\n \n // Filter out UI framework container/manager elements\n const el = element_info.element;\n // UI framework container checks - generic detection for any framework\n if ((el.getAttribute('data-rendered-by') || \n el.getAttribute('data-reactroot') || \n el.getAttribute('ng-version') || \n el.getAttribute('data-component-id') ||\n el.getAttribute('data-root') ||\n el.getAttribute('data-framework')) && \n (el.className && \n typeof el.className === 'string' && \n (el.className.includes('Container') || \n el.className.includes('container') || \n el.className.includes('Manager') || \n el.className.includes('manager')))) {\n highlighterLogger.debug('Filtered out UI framework container element:', element_info.css_selector);\n return false;\n }\n \n // Direct filter for framework container elements that shouldn't be interactive\n // Consolidating multiple container detection patterns into one efficient check\n const isFullViewport = element_info.bounding_box && \n element_info.bounding_box.x <= 5 && \n element_info.bounding_box.y <= 5 && \n element_info.bounding_box.width >= (window.innerWidth * 0.95) && \n element_info.bounding_box.height >= (window.innerHeight * 0.95);\n \n // Empty content check\n const isEmpty = !el.innerText || el.innerText.trim() === '';\n \n // Check if it's a framework container element\n if (element_info.element.tagName === 'DIV' && \n isFullViewport && \n isEmpty && \n (\n // Pattern matching for root containers\n (element_info.xpath && \n (element_info.xpath.match(/^\\/html\\[\\d+\\]\\/body\\[\\d+\\]\\/div\\[\\d+\\]\\/div\\[\\d+\\]$/) || \n element_info.xpath.match(/^\\/\\/\\*\\[@id='[^']+'\\]\\/div\\[\\d+\\]$/))) ||\n \n // Simple DOM structure\n (element_info.css_selector.split(' > ').length <= 4 && element_info.depth <= 5) ||\n \n // Empty or container-like classes\n (!el.className || el.className === '' || \n (typeof el.className === 'string' && \n (el.className.includes('overlay') || \n el.className.includes('container') || \n el.className.includes('wrapper'))))\n )) {\n highlighterLogger.debug('Filtered out framework container element:', element_info.css_selector);\n return false;\n }\n \n return true;\n });\n\n // First filter out elements with zero dimensions\n const nonZeroElements = filteredInfrastructure.filter(filterZeroDimensions);\n // sort by CSS selector depth so parents are processed first\n nonZeroElements.sort((a, b) => a.depth - b.depth);\n highlighterLogger.debug(`After dimension filtering: ${nonZeroElements.length} elements remain (${elements.length - nonZeroElements.length} removed)`);\n \n // Create parent candidate set with ALL nonZeroElements (before filtering)\n // This allows findClosestParent to find any element as a parent, even if it gets filtered out later\n const parentCandidates = new Set(nonZeroElements.map(e => e.css_selector));\n \n // Filter nested elements using handler comparison and standard logic\n const filteredByParent = filterNestedElements(nonZeroElements, parentCandidates, (element_info, parent, parentElementInfo) => {\n return shouldKeepNestedElement(element_info, parent, parentElementInfo);\n });\n\n highlighterLogger.debug(`After parent/child filtering: ${filteredByParent.length} elements remain (${nonZeroElements.length - filteredByParent.length} removed)`);\n\n // Final overlap filtering\n const filteredResults = filteredByParent.filter(element => {\n\n // Look for any element that came BEFORE this one in the array\n const hasEarlierOverlap = filteredByParent.some(other => {\n // Only check elements that came before (lower index)\n if (filteredByParent.indexOf(other) >= filteredByParent.indexOf(element)) {\n return false;\n }\n \n const isOverlapping = areElementsOverlapping(element, other); \n return isOverlapping;\n }); \n\n // Keep element if it has no earlier overlapping elements\n return !hasEarlierOverlap;\n });\n \n \n \n // Check for overlay removal\n highlighterLogger.debug(`After filtering: ${filteredResults.length} (${filteredByParent.length - filteredResults.length} removed by overlap)`);\n \n const nonOverlaidElements = filteredResults.filter(element => {\n return !isOverlaid(element);\n });\n\n highlighterLogger.debug(`Final elements after overlay removal: ${nonOverlaidElements.length} (${filteredResults.length - nonOverlaidElements.length} removed)`);\n \n return nonOverlaidElements;\n\n }\n\n\n\n const areElementsOverlapping = (element1, element2) => {\n if (element1.css_selector === element2.css_selector) {\n return true;\n }\n \n const box1 = element1.bounding_box;\n const box2 = element2.bounding_box;\n \n return box1.x === box2.x &&\n box1.y === box2.y &&\n box1.width === box2.width &&\n box1.height === box2.height;\n // element1.text === element2.text &&\n // element2.tag === 'a';\n };\n\n function findClosestParent(parentCandidates, element_info) { \n // Split the CSS selector into segments\n const segments = element_info.css_selector.split(' > ');\n \n // Try increasingly shorter paths until we find one in the parentCandidates set\n for (let i = segments.length - 1; i > 0; i--) {\n const parentPath = segments.slice(0, i).join(' > ');\n if (parentCandidates.has(parentPath)) {\n return parentPath;\n }\n }\n\n return null;\n }\n\n function shouldKeepNestedElement(elementInfo, parentPath, parentElementInfo) {\n // Check special cases FIRST (before handler comparison)\n const parentSegments = parentPath.split(' > ');\n\n // If parent is a link, don't keep child\n const isParentLink = /^a(:nth-of-type\\(\\d+\\))?$/.test(parentSegments[parentSegments.length - 1]);\n if (isParentLink) {\n return false; \n }\n \n // If this is a checkbox/radio input inside a label, don't keep the input (keep label instead)\n if (elementInfo.tag === 'input' && \n (elementInfo.type === 'checkbox' || elementInfo.type === 'radio')) {\n const isParentLabel = /^label(:nth-of-type\\(\\d+\\))?$/.test(parentSegments[parentSegments.length - 1]);\n if (isParentLabel) {\n return false;\n }\n }\n \n // Now check if handlers differ (only if special cases don't apply)\n if (parentElementInfo) {\n const childHandlers = getElementHandlersWithBubbling(elementInfo.element);\n const parentHandlers = getElementHandlersWithBubbling(parentElementInfo.element);\n \n // If handlers differ, keep child (don't unify)\n if (compareHandlersForUniquify(childHandlers, parentHandlers)) {\n return true;\n }\n }\n \n return isFormControl(elementInfo) || isDropdownItem(elementInfo) || isTableCell(elementInfo);\n }\n\n function isTableCell(elementInfo) {\n const element = elementInfo.element;\n if(!element || !(element instanceof HTMLElement)) {\n return false;\n }\n const validTags = new Set(['td', 'th']);\n const validRoles = new Set(['cell', 'gridcell', 'columnheader', 'rowheader']);\n \n const tag = element.tagName.toLowerCase();\n const role = element.getAttribute('role')?.toLowerCase();\n\n if (validTags.has(tag) || (role && validRoles.has(role))) {\n return true;\n }\n return false;\n \n }\n\n function isOverlaid(elementInfo) {\n const element = elementInfo.element;\n const boundingRect = elementInfo.bounding_box;\n \n\n \n \n // Create a diagnostic logging function that only logs when needed\n const diagnosticLog = (...args) => {\n { // set to true for debugging\n highlighterLogger.debug('[OVERLAY-DEBUG]', ...args);\n }\n };\n\n // Special handling for tooltips\n if (elementInfo.element.className && typeof elementInfo.element.className === 'string' && \n elementInfo.element.className.includes('tooltip')) {\n diagnosticLog('Element is a tooltip, not considering it overlaid');\n return false;\n }\n \n \n \n // Get element at the center point to check if it's covered by a popup/modal\n const middleX = boundingRect.x + boundingRect.width/2;\n const middleY = boundingRect.y + boundingRect.height/2;\n const elementAtMiddle = element.ownerDocument.elementFromPoint(middleX, middleY);\n \n if (elementAtMiddle && \n elementAtMiddle !== element && \n !isDecendent(element, elementAtMiddle) && \n !isDecendent(elementAtMiddle, element)) {\n\n \n return true;\n }\n \n \n return false;\n \n }\n\n /**\n * Checks if an element is scrollable (has scrollable content)\n * \n * @param element - The element to check\n * @returns boolean indicating if the element is scrollable\n */\n /* export function isScrollableContainer(element) {\n if (!element) return false;\n \n const style = window.getComputedStyle(element);\n \n // Reliable way to detect if an element has scrollbars or is scrollable\n const hasScrollHeight = element.scrollHeight > element.clientHeight;\n const hasScrollWidth = element.scrollWidth > element.clientWidth;\n \n // Check actual style properties\n const hasOverflowY = style.overflowY === 'auto' || \n style.overflowY === 'scroll' || \n style.overflowY === 'overlay';\n const hasOverflowX = style.overflowX === 'auto' || \n style.overflowX === 'scroll' || \n style.overflowX === 'overlay';\n \n // Check common class names and attributes for scrollable containers across frameworks\n const hasScrollClasses = element.classList.contains('scroll') || \n element.classList.contains('scrollable') ||\n element.classList.contains('overflow') ||\n element.classList.contains('overflow-auto') ||\n element.classList.contains('overflow-scroll') ||\n element.getAttribute('data-scrollable') === 'true';\n \n // Check for height/max-height constraints that often indicate scrolling content\n const hasHeightConstraint = style.maxHeight && \n style.maxHeight !== 'none' && \n style.maxHeight !== 'auto';\n \n // An element is scrollable if it has:\n // 1. Actual scrollbars in use (most reliable check) OR\n // 2. Overflow styles allowing scrolling AND content that would require scrolling\n return (hasScrollHeight && hasOverflowY) || \n (hasScrollWidth && hasOverflowX) ||\n (hasScrollClasses && (hasScrollHeight || hasScrollWidth)) ||\n (hasHeightConstraint && hasScrollHeight);\n } */\n\n function isScrollableContainer(el) {\n if (!el || el === document.body) return false;\n\n const style = getComputedStyle(el);\n\n const canScroll = (overflowProp) => {\n return overflowProp === 'auto' ||\n overflowProp === 'scroll' ||\n overflowProp === 'overlay';\n };\n\n //check if the element can scroll on either axis\n if (!canScroll(style.overflowY) && !canScroll(style.overflowX)) return false;\n return el.scrollHeight - el.clientHeight > 1 || el.scrollWidth - el.clientWidth > 1;\n }\n /**\n * Detects scrollable containers that are ancestors of the target element\n * \n * This function traverses up the DOM tree from the target element and identifies\n * all scrollable containers (elements that have scrollable content).\n * \n * @param target - The target element to start the search from\n * @returns Array of objects with selector and scroll properties\n */\n function detectScrollableContainers(target) {\n const scrollableContainers = [];\n \n if (!target) {\n return scrollableContainers;\n }\n \n highlighterLogger.debug('🔍 [detectScrollableContainers] Starting detection for target:', target.tagName, target.id, target.className);\n \n // Detect if target is inside an iframe\n const iframe = getContainingIFrame(target);\n const iframe_selector = iframe ? generateCssPath(iframe) : \"\";\n \n highlighterLogger.debug('🔍 [detectScrollableContainers] Iframe context:', iframe ? 'inside iframe' : 'main document', 'selector:', iframe_selector);\n \n // Start from the target element and traverse up the DOM tree\n let currentElement = target;\n let depth = 0;\n const MAX_DEPTH = 15; // Limit traversal depth to avoid infinite loops\n \n while (currentElement && currentElement.nodeType === Node.ELEMENT_NODE && depth < MAX_DEPTH) { \n // Check if the current element is scrollable\n if (isScrollableContainer(currentElement)) {\n highlighterLogger.debug('🔍 [detectScrollableContainers] Found scrollable container at depth', depth, ':', currentElement.tagName, currentElement.id, currentElement.className);\n \n const container = {\n containerEl: currentElement,\n selector: generateCssPath(currentElement),\n iframe_selector: iframe_selector,\n scrollTop: currentElement.scrollTop,\n scrollLeft: currentElement.scrollLeft,\n scrollHeight: currentElement.scrollHeight,\n scrollWidth: currentElement.scrollWidth,\n clientHeight: currentElement.clientHeight,\n clientWidth: currentElement.clientWidth\n };\n \n scrollableContainers.push(container);\n }\n \n // Move to parent element\n currentElement = getParentNode(currentElement);\n \n depth++;\n }\n \n highlighterLogger.debug('🔍 [detectScrollableContainers] Detection complete. Found', scrollableContainers.length, 'scrollable containers');\n return scrollableContainers;\n }\n\n class DOMSerializer {\n constructor(options = {}) {\n this.options = {\n includeStyles: true,\n includeScripts: false, // Security consideration\n includeFrames: true,\n includeShadowDOM: true,\n maxDepth: 50,\n ...options\n };\n this.serializedFrames = new Map();\n this.shadowRoots = new Map();\n }\n \n /**\n * Serialize a complete document or element\n */\n serialize(rootElement = document) {\n try {\n const serialized = {\n type: 'document',\n doctype: this.serializeDoctype(rootElement),\n documentElement: this.serializeElement(rootElement.documentElement || rootElement),\n frames: [],\n timestamp: Date.now(),\n url: rootElement.URL || window.location?.href,\n metadata: {\n title: rootElement.title,\n charset: rootElement.characterSet,\n contentType: rootElement.contentType\n }\n };\n \n // Serialize frames and iframes if enabled\n if (this.options.includeFrames) {\n serialized.frames = this.serializeFrames(rootElement);\n }\n \n return serialized;\n } catch (error) {\n console.error('Serialization error:', error);\n throw new Error(`DOM serialization failed: ${error.message}`);\n }\n }\n \n /**\n * Serialize document type declaration\n */\n serializeDoctype(doc) {\n if (!doc.doctype) return null;\n \n return {\n name: doc.doctype.name,\n publicId: doc.doctype.publicId,\n systemId: doc.doctype.systemId\n };\n }\n \n /**\n * Serialize an individual element and its children\n */\n serializeElement(element, depth = 0) {\n if (depth > this.options.maxDepth) {\n return { type: 'text', content: '<!-- Max depth exceeded -->' };\n }\n \n const nodeType = element.nodeType;\n \n switch (nodeType) {\n case Node.ELEMENT_NODE:\n return this.serializeElementNode(element, depth);\n case Node.TEXT_NODE:\n return this.serializeTextNode(element);\n case Node.COMMENT_NODE:\n return this.serializeCommentNode(element);\n case Node.DOCUMENT_FRAGMENT_NODE:\n return this.serializeDocumentFragment(element, depth);\n default:\n return null;\n }\n }\n \n /**\n * Serialize element node with attributes and children\n */\n serializeElementNode(element, depth) {\n const tagName = element.tagName.toLowerCase();\n \n // Skip script tags for security unless explicitly enabled\n if (tagName === 'script' && !this.options.includeScripts) {\n return { type: 'comment', content: '<!-- Script tag removed for security -->' };\n }\n \n const serialized = {\n type: 'element',\n tagName: tagName,\n attributes: this.serializeAttributes(element),\n children: [],\n shadowRoot: null\n };\n \n // Handle Shadow DOM\n if (this.options.includeShadowDOM && element.shadowRoot) {\n serialized.shadowRoot = this.serializeShadowRoot(element.shadowRoot, depth + 1);\n }\n \n // Handle special elements\n if (tagName === 'iframe' || tagName === 'frame') {\n serialized.frameData = this.serializeFrameElement(element);\n }\n \n // Serialize children\n for (const child of element.childNodes) {\n const serializedChild = this.serializeElement(child, depth + 1);\n if (serializedChild) {\n serialized.children.push(serializedChild);\n }\n }\n \n // Include computed styles if enabled\n if (this.options.includeStyles && element.nodeType === Node.ELEMENT_NODE) {\n serialized.computedStyle = this.serializeComputedStyle(element);\n }\n \n return serialized;\n }\n \n /**\n * Serialize element attributes\n */\n serializeAttributes(element) {\n const attributes = {};\n \n if (element.attributes) {\n for (const attr of element.attributes) {\n attributes[attr.name] = attr.value;\n }\n }\n \n return attributes;\n }\n \n /**\n * Serialize computed styles\n */\n serializeComputedStyle(element) {\n try {\n const computedStyle = window.getComputedStyle(element);\n const styles = {};\n \n // Only serialize non-default values to reduce size\n const importantStyles = [\n 'display', 'position', 'width', 'height', 'margin', 'padding',\n 'border', 'background', 'color', 'font-family', 'font-size',\n 'text-align', 'visibility', 'z-index', 'transform'\n ];\n \n for (const prop of importantStyles) {\n const value = computedStyle.getPropertyValue(prop);\n if (value && value !== 'initial' && value !== 'normal') {\n styles[prop] = value;\n }\n }\n \n return styles;\n } catch (error) {\n return {};\n }\n }\n \n /**\n * Serialize text node\n */\n serializeTextNode(node) {\n return {\n type: 'text',\n content: node.textContent\n };\n }\n \n /**\n * Serialize comment node\n */\n serializeCommentNode(node) {\n return {\n type: 'comment',\n content: node.textContent\n };\n }\n \n /**\n * Serialize document fragment\n */\n serializeDocumentFragment(fragment, depth) {\n const serialized = {\n type: 'fragment',\n children: []\n };\n \n for (const child of fragment.childNodes) {\n const serializedChild = this.serializeElement(child, depth + 1);\n if (serializedChild) {\n serialized.children.push(serializedChild);\n }\n }\n \n return serialized;\n }\n \n /**\n * Serialize Shadow DOM\n */\n serializeShadowRoot(shadowRoot, depth) {\n const serialized = {\n type: 'shadowRoot',\n mode: shadowRoot.mode,\n children: []\n };\n \n for (const child of shadowRoot.childNodes) {\n const serializedChild = this.serializeElement(child, depth + 1);\n if (serializedChild) {\n serialized.children.push(serializedChild);\n }\n }\n \n return serialized;\n }\n \n /**\n * Serialize frame/iframe elements\n */\n serializeFrameElement(frameElement) {\n const frameData = {\n src: frameElement.src,\n name: frameElement.name,\n id: frameElement.id,\n sandbox: frameElement.sandbox?.toString() || '',\n allowfullscreen: frameElement.allowFullscreen\n };\n \n // Try to access frame content (may fail due to CORS)\n try {\n const frameDoc = frameElement.contentDocument;\n if (frameDoc && this.options.includeFrames) {\n frameData.content = this.serialize(frameDoc);\n }\n } catch (error) {\n frameData.accessError = 'Cross-origin frame content not accessible';\n }\n \n return frameData;\n }\n \n /**\n * Serialize all frames in document\n */\n serializeFrames(doc) {\n const frames = [];\n const frameElements = doc.querySelectorAll('iframe, frame');\n \n for (const frameElement of frameElements) {\n try {\n const frameDoc = frameElement.contentDocument;\n if (frameDoc) {\n frames.push({\n element: this.serializeElement(frameElement),\n content: this.serialize(frameDoc)\n });\n }\n } catch (error) {\n frames.push({\n element: this.serializeElement(frameElement),\n error: 'Frame content not accessible'\n });\n }\n }\n \n return frames;\n }\n \n /**\n * Deserialize serialized DOM data back to DOM nodes\n */\n deserialize(serializedData, targetDocument = document) {\n try {\n if (serializedData.type === 'document') {\n return this.deserializeDocument(serializedData, targetDocument);\n } else {\n return this.deserializeElement(serializedData, targetDocument);\n }\n } catch (error) {\n console.error('Deserialization error:', error);\n throw new Error(`DOM deserialization failed: ${error.message}`);\n }\n }\n \n /**\n * Deserialize complete document\n */\n deserializeDocument(serializedDoc, targetDoc) {\n // Create new document if needed\n const doc = targetDoc || document.implementation.createHTMLDocument();\n \n // Set doctype if present\n if (serializedDoc.doctype) {\n const doctype = document.implementation.createDocumentType(\n serializedDoc.doctype.name,\n serializedDoc.doctype.publicId,\n serializedDoc.doctype.systemId\n );\n doc.replaceChild(doctype, doc.doctype);\n }\n \n // Deserialize document element\n if (serializedDoc.documentElement) {\n const newDocElement = this.deserializeElement(serializedDoc.documentElement, doc);\n doc.replaceChild(newDocElement, doc.documentElement);\n }\n \n // Handle metadata\n if (serializedDoc.metadata) {\n doc.title = serializedDoc.metadata.title || '';\n }\n \n return doc;\n }\n \n /**\n * Deserialize individual element\n */\n deserializeElement(serializedNode, doc) {\n switch (serializedNode.type) {\n case 'element':\n return this.deserializeElementNode(serializedNode, doc);\n case 'text':\n return doc.createTextNode(serializedNode.content);\n case 'comment':\n return doc.createComment(serializedNode.content);\n case 'fragment':\n return this.deserializeDocumentFragment(serializedNode, doc);\n case 'shadowRoot':\n // Shadow roots are handled during element creation\n return null;\n default:\n return null;\n }\n }\n \n /**\n * Deserialize element node\n */\n deserializeElementNode(serializedElement, doc) {\n const element = doc.createElement(serializedElement.tagName);\n \n // Set attributes\n if (serializedElement.attributes) {\n for (const [name, value] of Object.entries(serializedElement.attributes)) {\n try {\n element.setAttribute(name, value);\n } catch (error) {\n console.warn(`Failed to set attribute ${name}:`, error);\n }\n }\n }\n \n // Apply computed styles if available\n if (serializedElement.computedStyle && this.options.includeStyles) {\n for (const [prop, value] of Object.entries(serializedElement.computedStyle)) {\n try {\n element.style.setProperty(prop, value);\n } catch (error) {\n console.warn(`Failed to set style ${prop}:`, error);\n }\n }\n }\n \n // Create shadow root if present\n if (serializedElement.shadowRoot && element.attachShadow) {\n try {\n const shadowRoot = element.attachShadow({ \n mode: serializedElement.shadowRoot.mode || 'open' \n });\n \n // Deserialize shadow root children\n for (const child of serializedElement.shadowRoot.children) {\n const childElement = this.deserializeElement(child, doc);\n if (childElement) {\n shadowRoot.appendChild(childElement);\n }\n }\n } catch (error) {\n console.warn('Failed to create shadow root:', error);\n }\n }\n \n // Deserialize children\n if (serializedElement.children) {\n for (const child of serializedElement.children) {\n const childElement = this.deserializeElement(child, doc);\n if (childElement) {\n element.appendChild(childElement);\n }\n }\n }\n \n // Handle frame content\n if (serializedElement.frameData && serializedElement.frameData.content) {\n // Frame content deserialization would happen after the frame loads\n element.addEventListener('load', () => {\n try {\n const frameDoc = element.contentDocument;\n if (frameDoc) {\n this.deserializeDocument(serializedElement.frameData.content, frameDoc);\n }\n } catch (error) {\n console.warn('Failed to deserialize frame content:', error);\n }\n });\n }\n \n return element;\n }\n \n /**\n * Deserialize document fragment\n */\n deserializeDocumentFragment(serializedFragment, doc) {\n const fragment = doc.createDocumentFragment();\n \n if (serializedFragment.children) {\n for (const child of serializedFragment.children) {\n const childElement = this.deserializeElement(child, doc);\n if (childElement) {\n fragment.appendChild(childElement);\n }\n }\n }\n \n return fragment;\n }\n }\n \n // Usage example and utility functions\n class DOMUtils {\n /**\n * Create serializer with common presets\n */\n static createSerializer(preset = 'default') {\n const presets = {\n default: {\n includeStyles: true,\n includeScripts: false,\n includeFrames: true,\n includeShadowDOM: true\n },\n minimal: {\n includeStyles: false,\n includeScripts: false,\n includeFrames: false,\n includeShadowDOM: false\n },\n complete: {\n includeStyles: true,\n includeScripts: true,\n includeFrames: true,\n includeShadowDOM: true\n },\n secure: {\n includeStyles: true,\n includeScripts: false,\n includeFrames: false,\n includeShadowDOM: true\n }\n };\n \n return new DOMSerializer(presets[preset] || presets.default);\n }\n \n /**\n * Serialize DOM to JSON string\n */\n static serializeToJSON(element, options) {\n const serializer = new DOMSerializer(options);\n const serialized = serializer.serialize(element);\n return JSON.stringify(serialized, null, 2);\n }\n \n /**\n * Deserialize from JSON string\n */\n static deserializeFromJSON(jsonString, targetDocument) {\n const serialized = JSON.parse(jsonString);\n const serializer = new DOMSerializer();\n return serializer.deserialize(serialized, targetDocument);\n }\n \n /**\n * Clone DOM with full fidelity including Shadow DOM\n */\n static deepClone(element, options) {\n const serializer = new DOMSerializer(options);\n const serialized = serializer.serialize(element);\n return serializer.deserialize(serialized, element.ownerDocument);\n }\n \n /**\n * Compare two DOM structures\n */\n static compare(element1, element2, options) {\n const serializer = new DOMSerializer(options);\n const serialized1 = serializer.serialize(element1);\n const serialized2 = serializer.serialize(element2);\n \n return JSON.stringify(serialized1) === JSON.stringify(serialized2);\n }\n }\n \n /*\n // Export for use\n if (typeof module !== 'undefined' && module.exports) {\n module.exports = { DOMSerializer, DOMUtils };\n } else if (typeof window !== 'undefined') {\n window.DOMSerializer = DOMSerializer;\n window.DOMUtils = DOMUtils;\n }\n */\n\n /* Usage Examples:\n \n // Basic serialization\n const serializer = new DOMSerializer();\n const serialized = serializer.serialize(document);\n console.log(JSON.stringify(serialized, null, 2));\n \n // Deserialize back to DOM\n const clonedDoc = serializer.deserialize(serialized);\n \n // Using presets\n const minimalSerializer = DOMUtils.createSerializer('minimal');\n const secureSerializer = DOMUtils.createSerializer('secure');\n \n // Serialize specific element with Shadow DOM\n const customElement = document.querySelector('my-custom-element');\n const serializedElement = serializer.serialize(customElement);\n \n // JSON utilities\n const jsonString = DOMUtils.serializeToJSON(document.body);\n const restored = DOMUtils.deserializeFromJSON(jsonString);\n \n // Deep clone with Shadow DOM support\n const clone = DOMUtils.deepClone(document.body, { includeShadowDOM: true });\n \n */\n\n function serializeNodeToJSON(nodeElement) {\n return DOMUtils.serializeToJSON(nodeElement, {includeStyles: false});\n }\n\n function deserializeNodeFromJSON(jsonString) {\n return DOMUtils.deserializeFromJSON(jsonString);\n }\n\n function findDropdowns() {\n const dropdowns = [];\n \n // Native select elements\n dropdowns.push(...getAllElementsIncludingShadow('select'));\n \n // Elements with dropdown roles that don't have <input>..</input>\n const roleElements = getAllElementsIncludingShadow('[role=\"combobox\"], [role=\"listbox\"], [role=\"dropdown\"], [role=\"option\"], [role=\"menu\"], [role=\"menuitem\"]').filter(el => {\n return el.tagName.toLowerCase() !== 'input' || ![\"button\", \"checkbox\", \"radio\"].includes(el.getAttribute(\"type\"));\n });\n dropdowns.push(...roleElements);\n \n // Common dropdown class patterns\n const dropdownPattern = /.*(dropdown|select|combobox|menu).*/i;\n const elements = getAllElementsIncludingShadow('*');\n const dropdownClasses = Array.from(elements).filter(el => {\n const hasDropdownClass = dropdownPattern.test(el.className);\n const validTag = ['li', 'ul', 'span', 'div', 'p', 'a', 'button'].includes(el.tagName.toLowerCase());\n const style = window.getComputedStyle(el); \n const result = hasDropdownClass && validTag && (style.cursor === 'pointer' || el.tagName.toLowerCase() === 'a' || el.tagName.toLowerCase() === 'button');\n return result;\n });\n \n dropdowns.push(...dropdownClasses);\n \n // Elements with aria-haspopup attribute\n dropdowns.push(...getAllElementsIncludingShadow('[aria-haspopup=\"true\"], [aria-haspopup=\"listbox\"], [aria-haspopup=\"menu\"]'));\n\n // Improve navigation element detection\n // Semantic nav elements with list items\n dropdowns.push(...getAllElementsIncludingShadow('nav ul li, nav ol li'));\n \n // Navigation elements in common design patterns\n dropdowns.push(...getAllElementsIncludingShadow('header a, .header a, .nav a, .navigation a, .menu a, .sidebar a, aside a'));\n \n // Elements in primary navigation areas with common attributes\n dropdowns.push(...getAllElementsIncludingShadow('[role=\"navigation\"] a, [aria-label*=\"navigation\"] a, [aria-label*=\"menu\"] a'));\n\n return dropdowns;\n }\n\n function findClickables() {\n const clickables = [];\n \n const checkboxPattern = /checkbox/i;\n // Collect all clickable elements first\n const nativeLinks = [...getAllElementsIncludingShadow('a')];\n const nativeButtons = [...getAllElementsIncludingShadow('button')];\n const inputButtons = [...getAllElementsIncludingShadow('input[type=\"button\"], input[type=\"submit\"], input[type=\"reset\"]')];\n const roleButtons = [...getAllElementsIncludingShadow('[role=\"button\"]')];\n // const tabbable = [...getAllElementsIncludingShadow('[tabindex=\"0\"]')];\n const clickHandlers = [...getAllElementsIncludingShadow('[onclick]')];\n const dropdowns = findDropdowns();\n const nativeCheckboxes = [...getAllElementsIncludingShadow('input[type=\"checkbox\"]')]; \n const fauxCheckboxes = getAllElementsIncludingShadow('*').filter(el => {\n if (checkboxPattern.test(el.className)) {\n const realCheckboxes = getAllElementsIncludingShadow('input[type=\"checkbox\"]', el);\n if (realCheckboxes.length === 1) {\n const boundingRect = realCheckboxes[0].getBoundingClientRect();\n return boundingRect.width <= 1 && boundingRect.height <= 1 \n }\n }\n return false;\n });\n const nativeRadios = [...getAllElementsIncludingShadow('input[type=\"radio\"]')];\n const toggles = findToggles();\n const pointerElements = findElementsWithPointer();\n // Add all elements at once\n clickables.push(\n ...nativeLinks,\n ...nativeButtons,\n ...inputButtons,\n ...roleButtons,\n // ...tabbable,\n ...clickHandlers,\n ...dropdowns,\n ...nativeCheckboxes,\n ...fauxCheckboxes,\n ...nativeRadios,\n ...toggles,\n ...pointerElements\n );\n\n // Only uniquify once at the end\n return clickables; // Let findElements handle the uniquification\n }\n\n function findToggles() {\n const toggles = [];\n const checkboxes = getAllElementsIncludingShadow('input[type=\"checkbox\"]');\n const togglePattern = /switch|toggle|slider/i;\n\n checkboxes.forEach(checkbox => {\n let isToggle = false;\n\n // Check the checkbox itself\n if (togglePattern.test(checkbox.className) || togglePattern.test(checkbox.getAttribute('role') || '')) {\n isToggle = true;\n }\n\n // Check parent elements (up to 3 levels)\n if (!isToggle) {\n let element = checkbox;\n for (let i = 0; i < 3; i++) {\n const parent = element.parentElement;\n if (!parent) break;\n\n const className = parent.className || '';\n const role = parent.getAttribute('role') || '';\n\n if (togglePattern.test(className) || togglePattern.test(role)) {\n isToggle = true;\n break;\n }\n element = parent;\n }\n }\n\n // Check next sibling\n if (!isToggle) {\n const nextSibling = checkbox.nextElementSibling;\n if (nextSibling) {\n const className = nextSibling.className || '';\n const role = nextSibling.getAttribute('role') || '';\n if (togglePattern.test(className) || togglePattern.test(role)) {\n isToggle = true;\n }\n }\n }\n\n if (isToggle) {\n toggles.push(checkbox);\n }\n });\n\n return toggles;\n }\n\n function findNonInteractiveElements() {\n // Get all elements in the document\n const all = Array.from(getAllElementsIncludingShadow('*'));\n \n // Filter elements based on Python implementation rules\n return all.filter(element => {\n if (!element.firstElementChild) {\n const tag = element.tagName.toLowerCase(); \n if (!['select', 'button', 'a'].includes(tag)) {\n const validTags = ['p', 'span', 'div', 'input', 'textarea','td','th'].includes(tag) || /^h\\d$/.test(tag) || /text/.test(tag);\n const boundingRect = element.getBoundingClientRect();\n return validTags && boundingRect.height > 1 && boundingRect.width > 1;\n }\n }\n return false;\n });\n }\n\n\n\n function findElementsWithPointer() {\n const elements = [];\n const allElements = getAllElementsIncludingShadow('*');\n \n allElements.forEach(element => {\n const style = window.getComputedStyle(element);\n const hasPointer = style.cursor === 'pointer';\n \n if (hasPointer) {\n elements.push(element);\n }\n });\n \n highlighterLogger.debug(`Found ${elements.length} elements with pointer cursor`);\n return elements;\n }\n\n function findCheckables() {\n const elements = [];\n\n elements.push(...getAllElementsIncludingShadow('input[type=\"checkbox\"]'));\n elements.push(...getAllElementsIncludingShadow('input[type=\"radio\"]'));\n const all_elements = getAllElementsIncludingShadow('label');\n const radioClasses = Array.from(all_elements).filter(el => {\n return /.*radio.*/i.test(el.className); \n });\n elements.push(...radioClasses);\n return elements;\n }\n\n function findFillables() {\n const elements = [];\n\n const inputs = [...getAllElementsIncludingShadow('input:not([type=\"radio\"]):not([type=\"checkbox\"])')];\n highlighterLogger.debug('Found inputs:', inputs.length, inputs);\n elements.push(...inputs);\n \n const textareas = [...getAllElementsIncludingShadow('textarea')];\n highlighterLogger.debug('Found textareas:', textareas.length);\n elements.push(...textareas);\n \n const editables = [...getAllElementsIncludingShadow('[contenteditable=\"true\"]')];\n highlighterLogger.debug('Found editables:', editables.length);\n elements.push(...editables);\n\n return elements;\n }\n\n // maximum number of characters in a logical name\n const MAX_LOGICAL_NAME_LENGTH = 25;\n // minimum number of characters in text content to be considered a text element\n const MIN_TEXT_LENGTH = 3;\n /**\n * Query elements by CSS selector with :has-text(\"...\") pseudo-selector\n * @param {string} selector - CSS selector with optional :has-text(\"...\") pseudo-selector\n * @param {Element|Document} context - Context element to query within (defaults to document)\n * @returns {Element[]} Array of matched elements\n * @example\n * // Find all buttons with text \"Submit\"\n * querySelectorAllByText('button:has-text(\"Submit\")')\n * \n * // Find element with specific role and text\n * querySelectorAllByText('[role=\"button\"]:has-text(\"Click me\")')\n * \n * // Complex selector with descendants\n * querySelectorAllByText('button:has-text(\"foo\") > div > ul')\n * \n * // Multiple components\n * querySelectorAllByText('.container > button:has-text(\"Submit\")')\n * \n * // Multiple text filters\n * querySelectorAllByText('button:has-text(\"foo\") > div:has-text(\"bar\") > span')\n */\n function querySelectorAllByText(selector, context = document) {\n \t// Local helper function to compare two strings by normalizing whitespace and ignoring case\n \t// mimic the behavior of Playwright's :has-text() selector modifier\n \tfunction pwHasText(str1, str2) {\n \t\tif (!str1 || !str2) return false;\n \t\t\n \t\t// Normalize whitespace by replacing multiple spaces with single space and trim\n \t\tconst normalize = (str) => cleanText(str.toLowerCase());\n \t\t\n \t\tconst normalized1 = normalize(str1);\n \t\tconst normalized2 = normalize(str2);\n \t\t\n \t\treturn normalized1.includes(normalized2);\n \t}\n \t\n \t// Check if selector contains :has-text() pseudo-selector\n \tif (!selector.includes(':has-text(')) {\n \t\t// No :has-text pseudo-selector, use regular querySelectorAll\n \t\treturn Array.from(robustQuerySelector(selector, context, true));\n \t}\n \t\n \t// Split selector by combinators while preserving them\n \t// Matches: >, +, ~, or space (descendant combinator)\n \tconst parts = [];\n \tlet currentPart = '';\n \tlet inBracket = false;\n \tlet inTextPseudo = false;\n \tlet inTextQuotes = false;\n \tlet i = 0;\n \t\n \twhile (i < selector.length) {\n \t\tconst char = selector[i];\n \t\t\n \t\t// Track :has-text(\"...\") to avoid splitting inside it\n \t\tif (selector.substring(i, i + 10) === ':has-text(') {\n \t\t\tinTextPseudo = true;\n \t\t\tinTextQuotes = false;\n \t\t}\n \t\tif (inTextPseudo && char === '\"' && !inTextQuotes) {\n \t\t\tinTextQuotes = true;\n \t\t} else if (inTextPseudo && char === '\"' && inTextQuotes) {\n \t\t\t// Check if this quote is escaped\n \t\t\tif (i > 0 && selector[i - 1] === '\\\\') ; else {\n \t\t\t\tinTextQuotes = false;\n \t\t\t}\n \t\t}\n \t\tif (inTextPseudo && char === ')' && !inTextQuotes) {\n \t\t\tinTextPseudo = false;\n \t\t\tcurrentPart += char;\n \t\t\ti++;\n \t\t\tcontinue;\n \t\t}\n \t\t\n \t\t// Track brackets to avoid splitting on combinators inside attribute selectors\n \t\tif (char === '[') inBracket = true;\n \t\tif (char === ']') inBracket = false;\n \t\t\n \t\t// Check for combinators (not inside brackets or :has-text())\n \t\tif (!inBracket && !inTextPseudo && (char === '>' || char === '+' || char === '~')) {\n \t\t\tif (currentPart.trim()) parts.push({ selector: currentPart.trim(), combinator: null });\n \t\t\tparts.push({ selector: null, combinator: char.trim() });\n \t\t\tcurrentPart = '';\n \t\t\ti++;\n \t\t\tcontinue;\n \t\t}\n \t\t\n \t\t// Check for space combinator (descendant)\n \t\tif (!inBracket && !inTextPseudo && char === ' ') {\n \t\t\t// Skip multiple spaces\n \t\t\twhile (i < selector.length && selector[i] === ' ') i++;\n \t\t\t// Check if next char is a combinator (>, +, ~)\n \t\t\tif (i < selector.length && !'><+~'.includes(selector[i])) {\n \t\t\t\tif (currentPart.trim()) {\n \t\t\t\t\tparts.push({ selector: currentPart.trim(), combinator: null });\n \t\t\t\t\tparts.push({ selector: null, combinator: ' ' });\n \t\t\t\t\tcurrentPart = '';\n \t\t\t\t}\n \t\t\t}\n \t\t\tcontinue;\n \t\t}\n \t\t\n \t\tcurrentPart += char;\n \t\ti++;\n \t}\n \tif (currentPart.trim()) parts.push({ selector: currentPart.trim(), combinator: null });\n \t\n \t// Filter out combinator-only entries and rebuild as selector+combinator pairs\n \tconst selectorParts = [];\n \tfor (let j = 0; j < parts.length; j++) {\n \t\tif (parts[j].selector) {\n \t\t\tconst combinator = (j + 1 < parts.length && parts[j + 1].combinator) ? parts[j + 1].combinator : null;\n \t\t\t// Extract text value if present\n \t\t\tconst textMatch = parts[j].selector.match(/:has-text\\(\"((?:[^\"\\\\]|\\\\.)*)\"\\)/);\n \t\t\tconst textValue = textMatch ? textMatch[1] : null;\n \t\t\tconst selectorWithoutText = textMatch ? parts[j].selector.replace(/:has-text\\(\"((?:[^\"\\\\]|\\\\.)*)\"\\)/, '') : parts[j].selector;\n \t\t\t\n \t\t\tselectorParts.push({ \n \t\t\t\tselector: parts[j].selector,\n \t\t\t\tselectorWithoutText,\n \t\t\t\ttextValue,\n \t\t\t\tcombinator \n \t\t\t});\n \t\t}\n \t}\n \t\n \t// Process selector parts sequentially, applying text filters as we go\n \tlet currentElements = [context];\n \t\n \tfor (let partIndex = 0; partIndex < selectorParts.length; partIndex++) {\n \t\tconst part = selectorParts[partIndex];\n \t\tconst nextElements = [];\n \t\t\n \t\tfor (const ctx of currentElements) {\n \t\t\tlet candidates;\n \t\t\t\n \t\t\tif (partIndex === 0 && ctx === context) {\n \t\t\t\t// First part, query from document/context\n \t\t\t\tconst querySelector = part.selectorWithoutText.trim() || '*';\n \t\t\t\tcandidates = Array.from(robustQuerySelector(querySelector, ctx, true));\n \t\t\t\thighlighterLogger.debug(`querySelectorAllByText: first part, querySelector=${querySelector}, candidates=`, candidates);\n \t\t\t} \n \t\t\telse {\n \t\t\t\t// Subsequent parts, use combinator\n \t\t\t\tconst prevPart = selectorParts[partIndex - 1];\n \t\t\t\tconst combinator = prevPart.combinator;\n \t\t\t\tconst querySelector = part.selectorWithoutText.trim() || '*';\n \t\t\t\t\n \t\t\t\tif (combinator === '>') {\n \t\t\t\t\t// Direct children\n \t\t\t\t\tcandidates = Array.from(ctx.children).filter(child => {\n \t\t\t\t\t\treturn child.matches(querySelector);\n \t\t\t\t\t});\n \t\t\t\t} else if (combinator === '+') {\n \t\t\t\t\t// Next sibling\n \t\t\t\t\tconst sibling = ctx.nextElementSibling;\n \t\t\t\t\tcandidates = (sibling && sibling.matches(querySelector)) ? [sibling] : [];\n \t\t\t\t} else if (combinator === '~') {\n \t\t\t\t\t// Following siblings\n \t\t\t\t\tcandidates = [];\n \t\t\t\t\tlet sibling = ctx.nextElementSibling;\n \t\t\t\t\twhile (sibling) {\n \t\t\t\t\t\tif (sibling.matches(querySelector)) {\n \t\t\t\t\t\t\tcandidates.push(sibling);\n \t\t\t\t\t\t}\n \t\t\t\t\t\tsibling = sibling.nextElementSibling;\n \t\t\t\t\t}\n \t\t\t\t} \n \t\t\t\telse {\n \t\t\t\t\t// Descendant (space)\n \t\t\t\t\tcandidates = Array.from(robustQuerySelector(querySelector, ctx, true));\n \t\t\t\t}\n \t\t\t}\n \t\t\t\n \t\t\t// Apply text filter if present\n \t\t\tif (part.textValue !== null) {\n \t\t\t\tcandidates = candidates.filter(el => pwHasText(getElementText(el), part.textValue));\n \t\t\t}\n \t\t\t\n \t\t\tnextElements.push(...candidates);\n \t\t}\n \t\t\n \t\tcurrentElements = nextElements;\n \t\t\n \t\t// If no matches, bail early\n \t\tif (currentElements.length === 0) {\n \t\t\treturn [];\n \t\t}\n \t}\n \t\n \treturn currentElements;\n }\n\n const cleanText = (text) => {\n \treturn text.replace(/\\s+/g, ' ').trim();\n };\n\n // Helper function to get name selector\n const getNameSelector = (element) => {\n \tconst isClickable = isClickableElement(element);\n \tconst text = cleanText(getElementText(element));\n \tconst title = element.getAttribute('title');\n \tconst name = element.getAttribute('name');\n \tconst ariaLabel = element.getAttribute('aria-label');\n \tif (!text && !title && !name && !ariaLabel) throw new Error('No name attribute provided');\n \tif (text && isClickable) return `:has-text(${doubleQuoteString(text)})`;\n \tif (title) return `[title=${doubleQuoteString(title)}]`;\n \tif (ariaLabel) return `[aria-label=${doubleQuoteString(ariaLabel)}]`;\n \treturn `[name=${doubleQuoteString(name)}]`; \n };\n\n const getLogicalName = (element) => {\n \tconst isClickable = isClickableElement(element);\n \tconst raw_text = cleanText(getElementText(element));\n \tconst text = raw_text && raw_text.length >= MIN_TEXT_LENGTH ? raw_text : '';\n \tconst title = element.getAttribute('title');\n \tconst name = element.getAttribute('name');\n \tconst ariaLabel = element.getAttribute('aria-label');\n \tconst role = element.getAttribute('role');\n \tconst logicalName = title || name || ariaLabel;\n \tconst useText = text && isClickable;\n \tconst finalLogicalName = useText ? text : logicalName;\n \treturn {\n \t\thasLogicalName: (useText || logicalName) && finalLogicalName.length < MAX_LOGICAL_NAME_LENGTH && !role?.startsWith('row'),\n \t\tlogicalName: finalLogicalName,\n \t\tuseText: useText\n \t};\n };\n\n\n /**\n * Individual strategy functions\n */\n\n function tryRoleStrategy(element) { \n \tconst role = element.getAttribute('role');\n \tconst tag = element.tagName.toLowerCase();\n \tconst { hasLogicalName } = getLogicalName(element);\n \tif (role && hasLogicalName) {\n \t\treturn { \n \t\t\tstrategy: 'role', \n \t\t\tselector: `[role=${doubleQuoteString(role)}]${getNameSelector(element)}`\n \t\t};\n \t}\n \t\n \t// Tag-based role (button, a, input, etc.) + name\n \tif (['button', 'a', 'input', 'textarea', 'select'].includes(tag) && hasLogicalName) {\n \t\treturn { \n \t\t\tstrategy: 'role', \n \t\t\tselector: `${tag}${getNameSelector(element)}`\n \t\t};\n \t}\n \t\n \treturn null;\n }\n\n function tryPlaceholderStrategy(element) {\n \tconst placeholder = element.getAttribute('placeholder');\n \tif (placeholder) {\n \t\treturn { \n \t\t\tstrategy: 'placeholder', \n \t\t\tselector: `[placeholder=${doubleQuoteString(placeholder)}]` \n \t\t};\n \t}\n \treturn null;\n }\n\n function tryLabelStrategy(element) {\n \tconst tag = element.tagName.toLowerCase();\n \tif (['input', 'textarea', 'select'].includes(tag)) {\n \t\tconst id = element.id;\n \t\tif (id) {\n \t\t\tconst labels = robustQuerySelector(`label[for=${doubleQuoteString(id)}]`, document, true);\n \t\t\tif (labels.length === 1) { //make sure id is unique\n \t\t\t\tconst labelText = cleanText(getElementText(labels[0]));\n \t\t\t\tif (labelText) {\n \t\t\t\t\tconst labelSelector = `label:has-text(${doubleQuoteString(labelText)})`;\n \t\t\t\t\ttry {\n \t\t\t\t\t\tconst els = querySelectorAllByText(labelSelector, document);\n \t\t\t\t\t\tif (els.length === 1) {\n \t\t\t\t\t\t\treturn { \n \t\t\t\t\t\t\t\tstrategy: 'label for',\n \t\t\t\t\t\t\t\tlabel_selector: labelSelector\n \t\t\t\t\t\t\t};\n \t\t\t\t\t\t}\n \t\t\t\t\t} catch (error) {\n \t\t\t\t\t\thighlighterLogger.error(`internal error: while querying label selector ${labelSelector} for element ${element.outerHTML}`, error);\n \t\t\t\t\t\tthrow error;\n \t\t\t\t\t}\n \t\t\t\t}\n \t\t\t}\n \t\t}\n \t}\n \treturn null;\n }\n\n function tryAriaLabelledByStrategy(element) {\n \tconst arialabeledBy = element.getAttribute('aria-labelledby');\n \tconst tag = element.tagName.toLowerCase();\n \t\n \tif (arialabeledBy) {\n \t\tconst labels = robustQuerySelector(`#${CSS.escape(arialabeledBy)}`, document, true);\n \t\tif (labels.length === 1) {\n \t\t\tconst labelText = cleanText(getElementText(labels[0]));\n \t\t\tif (labelText) { //verify the label text is unique\n \t\t\t\tconst labelSelector = `${tag}:has-text(${doubleQuoteString(labelText)})`;\n \t\t\t\ttry {\n \t\t\t\t\tconst els = querySelectorAllByText(labelSelector, document);\n \t\t\t\t\tif (els.length === 1) {\n \t\t\t\t\t\treturn { \n \t\t\t\t\t\t\tstrategy: 'label by', \n \t\t\t\t\t\t\tlabel_selector: labelSelector\n \t\t\t\t\t\t};\n \t\t\t\t\t}\n \t\t\t\t} catch (error) {\n \t\t\t\t\thighlighterLogger.error(`internal error: while querying aria-labelledby selector ${labelSelector} for element ${element.outerHTML}`, error);\n \t\t\t\t\tthrow error;\n \t\t\t\t}\n \t\t\t}\n \t\t}\n \t}\n \treturn null;\n }\n\n // Only use text strategy if element is clickable/interactive \n function tryNameStrategy(element) {\t\n \tconst { hasLogicalName, useText } = getLogicalName(element);\n \tconst classes = Array.from(element.classList).map(cls => CSS.escape(cls));\n \t\n \t// Only use :has-text() selector if element is clickable/interactive and therefore unlikely to change text content\n \tif (useText && classes.length > 0) {\n \t\treturn { \n \t\t\tstrategy: 'name', \n \t\t\tselector: `.${classes.join('.')}${getNameSelector(element)}` \n \t\t\t// selector: `${getNameSelector(element)}`\n \t\t\t// selector: `${element.tagName.toLowerCase()}${getNameSelector(element)}`\n \t\t};\n \t}\n \t// Otherwise use name strategy if element has a logical name (name, title, aria-label, etc.)\n \telse if (!useText && hasLogicalName) {\n \t\treturn { \n \t\t\tstrategy: 'name', \n \t\t\tselector: `${getNameSelector(element)}`\n \t\t};\n \t}\n \treturn null;\n }\n\n function tryDataTestIdStrategy(element) {\n \tconst dataTestId = element.getAttribute('data-testid');\n \tif (dataTestId) { //check if the data-testid is unique\n \t\tconst matchedElements = robustQuerySelector(`[data-testid=${doubleQuoteString(dataTestId)}]`, document, true);\n \t\tif (matchedElements.length === 1) {\n \t\t\treturn { \n \t\t\t\tstrategy: 'data-testid',\t\t\t\n \t\t\t\tselector: `[data-testid=${doubleQuoteString(dataTestId)}]` \n \t\t\t};\n \t\t}\n \t}\n \treturn null;\n }\n\n function tryIdStrategy(element) {\n \tconst id = element.getAttribute('id');\n \tif (id && !/:|\\s|\\\\/.test(id)) { //avoid using id if it contain unusual characters (react creates ids like this: \":r:42\")\n \t\t//check if the id is unique by querying the id selector\n \t\tconst matchedElements = robustQuerySelector(`#${CSS.escape(id)}`, document, true);\t\n \t\tif (matchedElements.length === 1) {\n \t\t\treturn { \n \t\t\t\tstrategy: 'id', \n \t\t\t\tselector: `#${CSS.escape(id)}` \n \t\t\t};\n \t\t}\n \t}\n \treturn null;\n }\n\n // Try all strategies on an element in order of precedence\n function attemptStrategiesOnElement(element) {\n \tconst strategies = [\n \t\ttryRoleStrategy, \t\t\n \t\ttryAriaLabelledByStrategy,\n \t\ttryLabelStrategy,\n \t\ttryDataTestIdStrategy,\n \t\ttryPlaceholderStrategy,\n \t\ttryNameStrategy,\n \t\ttryIdStrategy,\t\t\n \t\t// tryClassStrategy\n \t];\n \t\n \tfor (const strategy of strategies) {\n \t\tconst result = strategy(element); \n \t\tif (result) { \n \t\t\treturn result;\n \t\t}\n \t}\n \treturn null;\n }\n\n /**\n * Generate a smart selector for an element\n * @param {Element} element - The element to generate a locator for\n * @param {string} childPath - The child path to generate a locator for\n * @returns {Object} The selector for the element\n */\n function generateSmartSelector(element, childPath = '') {\n \t// Terminate recursion\n \tif (!element || element.nodeType !== Node.ELEMENT_NODE) {\n \t\treturn {\n \t\t\tstrategy: 'css',\n \t\t\tselector: childPath\n \t\t};\n \t}\n \t\n \t// Try strategies 1-6 on current element\n \tconst result = attemptStrategiesOnElement(element);\n \tconst is_label = result?.strategy === 'label for' || result?.strategy === 'label by';\n\n \tconsole.log('result', result);\n \tif (result) { // found a strategy\t\t\t\n \t\t// If we have a child path, concatenate parent strategy result with child CSS path \n \t\tlet extendedResult = result; \n \t\tif (childPath) {\n \t\t\tconst selector = is_label ? `${childPath}` : `${result?.selector} > ${childPath}`;\n \t\t\textendedResult = {\n \t\t\t\t...extendedResult,\n \t\t\t\tselector: selector,\n \t\t\t\tstrategy: `${extendedResult.strategy} (concatenated with child CSS path)`\n \t\t\t};\n \t\t} \t\t\n \t\t// check if the combined selector is unique and if not add the index to the strategy\n \t\tif (!is_label) { //label element is already checked for uniqueness\n \t\t\ttry {\n \t\t\t\tconst matchedElements = querySelectorAllByText(extendedResult?.selector); //query full selector for uniqueness checking \n \t\t\t\thighlighterLogger.debug(`querySelectorAllByText(${extendedResult?.selector}) matched ${matchedElements.length} elements`);\n \t\t\t\thighlighterLogger.debug(matchedElements);\n \t\t\t\tif (matchedElements.length > 1) {\n \t\t\t\t\tconst childElement = childPath ? robustQuerySelector(`:scope > ${childPath}`, element) : element;\n \t\t\t\t\tconst index = matchedElements.findIndex(el => el === childElement);\n \t\t\t\t\tif (index === -1) {\n \t\t\t\t\t\thighlighterLogger.error('internal error: Element not found in matched elements', element, result?.selector, matchedElements);\n \t\t\t\t\t\treturn null;\n \t\t\t\t\t}\n \t\t\t\t\textendedResult = {\n \t\t\t\t\t\t...extendedResult,\n \t\t\t\t\t\tstrategy: `${extendedResult.strategy} (by index)`,\n \t\t\t\t\t\tindex,\n \t\t\t\t\t}; \n \t\t\t\t}\n \t\t\t} catch (error) {\n \t\t\t\thighlighterLogger.error(`internal error: while checking if selector ${result?.selector} is unique for element ${element.outerHTML}`, error);\n \t\t\t\tthrow error;\n \t\t\t}\t\t\t\n \t\t}\n \t\treturn extendedResult;\n \t}\n \t\n \t// Get CSS selector for current element and build child path \n \tlet newChildPath;\n \t// didn't find strategy\n \tconst elementSelector = getElementCssSelector(element);\n \tnewChildPath = childPath ? `${elementSelector} > ${childPath}` : elementSelector;\n \tconst numComponents = newChildPath.split(' > ').length;\n \tif (numComponents > 2) { //require at least 2 components for css strategy\n \t\t// check if the combined selector is unique\n \t\tconst matchedElements = robustQuerySelector(newChildPath, document, true);\n \t\tif (matchedElements.length === 1) {\n \t\t\treturn {\n \t\t\t\tstrategy: 'css',\t\t\t\t\n \t\t\t\tselector: newChildPath\n \t\t\t};\n \t\t}\n \t}\n\n \t// Recursively try parent element\n \treturn generateSmartSelector(getParentNode(element), newChildPath);\n }\n\n const findElementByInfo = (elementInfo, useSmartSelectors = false) => {\n \thighlighterLogger.debug('findElementByInfo:', elementInfo);\n \tif (useSmartSelectors) {\n \t return findElementBySelectors(elementInfo.smart_iframe_selector, elementInfo.smart_selector, true);\n \t}\n \treturn findElementBySelectors(elementInfo.iframe_selector, elementInfo.css_selector, false);\n };\n\n const compareSelectors = (selector1, selector2, useSmartSelectors = false) => {\n \tif (useSmartSelectors) {\n \t\treturn JSON.stringify(selector1) === JSON.stringify(selector2);\n \t}\n \treturn selector1 === selector2;\n };\n\n const findElementBySelectors = (iframeSelector, elementSelector, useSmartSelectors = false) => {\n \tlet element;\n \t\n \t// first find the iframe that contains the element\n \tif (iframeSelector) { \n \t const frames = getAllDocumentElementsIncludingShadow('iframe', document);\n \t \n \t // Iterate over all frames and compare their CSS selectors\n \t for (const frame of frames) {\n \t\tconst selector = useSmartSelectors ? generateSmartSelector(frame) : generateCssPath(frame);\n \t\tif (compareSelectors(selector, iframeSelector, useSmartSelectors)) {\n \t\t const frameDocument = frame.contentDocument || frame.contentWindow.document;\n \t\t element = useSmartSelectors ? findElementBySmartSelector(elementSelector, frameDocument) : robustQuerySelector(elementSelector, frameDocument);\t\t \n \t\t break;\n \t\t} \n \t }\t}\n \telse {\t\t\n \t\telement = useSmartSelectors ? findElementBySmartSelector(elementSelector, document) : robustQuerySelector(elementSelector, document);\t\t\n \t}\n \t \n \tif (element) {\n \t\thighlighterLogger.debug('findElementBySelectors: found element ', element);\n \t}\n \telse {\n \t highlighterLogger.warn('findElementBySelectors: failed to find element with CSS selector:', JSON.stringify(elementSelector));\n \t}\n \n \treturn element;\n };\n \n function findElementBySmartSelector(smartSelector, root = document) {\t\n \thighlighterLogger.debug(`findElementBySmartSelector: called with smartSelector=${JSON.stringify(smartSelector)}`);\n \tlet combinedSelector;\n \tconst mainStrategy = smartSelector.strategy.split(' ')[0]; //extract the main strategy from the full strategy string\n \tswitch(mainStrategy) {\n case 'role': \n case 'placeholder':\n case 'name':\n case 'data-testid':\n case 'id':\n case 'css':\n case 'class':\n \t\t highlighterLogger.debug(`findElementBySmartSelector: strategy='${smartSelector.strategy}', selector=${smartSelector?.selector}, index=${smartSelector?.index}`);\n if (typeof smartSelector?.index === 'number') {\n return querySelectorAllByText(smartSelector.selector, root)[smartSelector.index];\n }\n else {\n return querySelectorAllByText(smartSelector.selector, root)[0];\n }\t\t \n case 'label':\n \t\t\tif (smartSelector.strategy === 'label for') {\n \t\t\t\thighlighterLogger.debug(`findElementBySmartSelector: strategy='label for', label_selector=${smartSelector.label_selector}`);\n \t\t\t\tconst label_for = querySelectorAllByText(smartSelector.label_selector, root)[0];\n \t\t\t\tcombinedSelector = smartSelector?.selector ? `#${label_for.getAttribute('for')} > ${smartSelector.selector}` : `#${label_for.getAttribute('for')}`;\n \t\t\t\treturn robustQuerySelector(combinedSelector, root);\n \t\t\t}\n \t\t\telse if (smartSelector.strategy === 'label by') {\n \t\t\t\thighlighterLogger.debug(`findElementBySmartSelector: strategy='label by', label_selector=${smartSelector.label_selector}`);\n \t\t\t\tconst label_by = querySelectorAllByText(smartSelector.label_selector, root)[0];\n \t\t\t\tcombinedSelector = smartSelector?.selector ? `[aria-labelledby=\"${label_by.getAttribute('id')}\"] > ${smartSelector.selector}` : `[aria-labelledby=\"${label_by.getAttribute('id')}\"]`;\n \t\t\t\treturn robustQuerySelector(combinedSelector, root);\n \t\t\t}\n \t\t\telse {\n \t\t\t\thighlighterLogger.error(`findElementBySmartSelector: unsupported label strategy: ${smartSelector.strategy}`);\n \t\t\t\treturn null;\n \t\t\t} \n default:\n highlighterLogger.error(`findElementBySmartSelector: Unsupported smart selector strategy: ${smartSelector.strategy}`);\n return null;\n }\n }\n\n function getElementInfo(element, index) { \t\t\t\n \tconst iframe = getContainingIFrame(element); \n \tconst iframe_selector = iframe ? generateCssPath(iframe) : \"\";\t\n \tconst smart_iframe_selector = iframe ? generateSmartSelector(iframe) : null;\n \t\n \t// Return element info with pre-calculated values\n \treturn {\n \t\tindex: index ?? -1,\n \t\ttag: element.tagName.toLowerCase(),\n \t\ttype: element.type || '',\n \t\ttext: getElementText(element) || getElementPlaceholder(element),\n \t\thtml: cleanHTML(element.outerHTML),\n \t\txpath: generateXPath(element),\n \t\tcss_selector: generateCssPath(element),\n \t\tbounding_box: element.getBoundingClientRect(),\t\t\n \t\tiframe_selector: iframe_selector,\t\t\n \t\tsmart_selector: generateSmartSelector(element),\n \t\tsmart_iframe_selector: smart_iframe_selector,\n \t\telement: element,\n \t\tdepth: getElementDepth(element)\n \t};\n }\n\n /**\n * Checks if a point is inside a bounding box\n * \n * @param point The point to check\n * @param box The bounding box\n * @returns boolean indicating if the point is inside the box\n */\n function isPointInsideBox(point, box) {\n return point.x >= box.x &&\n point.x <= box.x + box.width &&\n point.y >= box.y &&\n point.y <= box.y + box.height;\n }\n\n /**\n * Calculates the overlap area between two bounding boxes\n * \n * @param box1 First bounding box\n * @param box2 Second bounding box\n * @returns The overlap area\n */\n function calculateOverlap(box1, box2) {\n const xOverlap = Math.max(0,\n Math.min(box1.x + box1.width, box2.x + box2.width) -\n Math.max(box1.x, box2.x)\n );\n const yOverlap = Math.max(0,\n Math.min(box1.y + box1.height, box2.y + box2.height) -\n Math.max(box1.y, box2.y)\n );\n return xOverlap * yOverlap;\n }\n\n /**\n * Finds an exact match between candidate elements and the actual interaction element\n * \n * @param candidate_elements Array of candidate element infos\n * @param actualInteractionElementInfo The actual interaction element info\n * @returns The matching candidate element info, or null if no match is found\n */\n function findExactMatch(candidate_elements, actualInteractionElementInfo) {\n if (!actualInteractionElementInfo.element) {\n return null;\n }\n\n const exactMatch = candidate_elements.find(elementInfo => \n elementInfo.element && elementInfo.element === actualInteractionElementInfo.element\n );\n \n if (exactMatch) {\n highlighterLogger.debug('✅ Found exact element match:', {\n matchedElement: exactMatch.element?.tagName,\n matchedElementClass: exactMatch.element?.className,\n index: exactMatch.index\n });\n return exactMatch;\n }\n \n return null;\n }\n\n /**\n * Finds a match by traversing up the parent elements\n * \n * @param candidate_elements Array of candidate element infos\n * @param actualInteractionElementInfo The actual interaction element info\n * @returns The matching candidate element info, or null if no match is found\n */\n function findParentMatch(candidate_elements, actualInteractionElementInfo) {\n if (!actualInteractionElementInfo.element) {\n return null;\n }\n\n let element = actualInteractionElementInfo.element;\n while (element.parentElement) {\n element = element.parentElement;\n const parentMatch = candidate_elements.find(candidate => \n candidate.element && candidate.element === element\n );\n \n if (parentMatch) {\n highlighterLogger.debug('✅ Found parent element match:', {\n matchedElement: parentMatch.element?.tagName,\n matchedElementClass: parentMatch.element?.className,\n index: parentMatch.index,\n depth: element.tagName\n });\n return parentMatch;\n }\n \n // Stop if we hit another candidate element\n if (candidate_elements.some(candidate => \n candidate.element && candidate.element === element\n )) {\n highlighterLogger.debug('⚠️ Stopped parent search - hit another candidate element:', element.tagName);\n break;\n }\n }\n \n return null;\n }\n\n /**\n * Finds a match based on spatial relationships between elements\n * \n * @param candidate_elements Array of candidate element infos\n * @param actualInteractionElementInfo The actual interaction element info\n * @returns The matching candidate element info, or null if no match is found\n */\n function findSpatialMatch(candidate_elements, actualInteractionElementInfo) {\n if (!actualInteractionElementInfo.element || !actualInteractionElementInfo.bounding_box) {\n return null;\n }\n\n const actualBox = actualInteractionElementInfo.bounding_box;\n let bestMatch = null;\n let bestScore = 0;\n\n for (const candidateInfo of candidate_elements) {\n if (!candidateInfo.bounding_box) continue;\n \n const candidateBox = candidateInfo.bounding_box;\n let score = 0;\n\n // Check if actual element is contained within candidate\n if (isPointInsideBox({ x: actualBox.x, y: actualBox.y }, candidateBox) &&\n isPointInsideBox({ x: actualBox.x + actualBox.width, y: actualBox.y + actualBox.height }, candidateBox)) {\n score += 100; // High score for containment\n }\n\n // Calculate overlap area as a factor\n const overlap = calculateOverlap(actualBox, candidateBox);\n score += overlap;\n\n // Consider proximity if no containment\n if (score === 0) {\n const distance = Math.sqrt(\n Math.pow((actualBox.x + actualBox.width/2) - (candidateBox.x + candidateBox.width/2), 2) +\n Math.pow((actualBox.y + actualBox.height/2) - (candidateBox.y + candidateBox.height/2), 2)\n );\n // Convert distance to a score (closer = higher score)\n score = 1000 / (distance + 1);\n }\n\n if (score > bestScore) {\n bestScore = score;\n bestMatch = candidateInfo;\n highlighterLogger.debug('📏 New best spatial match:', {\n element: candidateInfo.element?.tagName,\n class: candidateInfo.element?.className,\n index: candidateInfo.index,\n score: score\n });\n }\n }\n\n if (bestMatch) {\n highlighterLogger.debug('✅ Final spatial match selected:', {\n element: bestMatch.element?.tagName,\n class: bestMatch.element?.className,\n index: bestMatch.index,\n finalScore: bestScore\n });\n return bestMatch;\n }\n\n return null;\n }\n\n /**\n * Finds a matching candidate element for an actual interaction element\n * \n * @param candidate_elements Array of candidate element infos\n * @param actualInteractionElementInfo The actual interaction element info\n * @returns The matching candidate element info, or null if no match is found\n */\n function findMatchingCandidateElementInfo(candidate_elements, actualInteractionElementInfo) {\n if (!actualInteractionElementInfo.element || !actualInteractionElementInfo.bounding_box) {\n highlighterLogger.error('❌ Missing required properties in actualInteractionElementInfo');\n return null;\n }\n\n highlighterLogger.debug('🔍 Starting element matching for:', {\n clickedElement: actualInteractionElementInfo.element.tagName,\n clickedElementClass: actualInteractionElementInfo.element.className,\n totalCandidates: candidate_elements.length\n });\n\n // First try exact element match\n const exactMatch = findExactMatch(candidate_elements, actualInteractionElementInfo);\n if (exactMatch) {\n return exactMatch;\n }\n highlighterLogger.debug('❌ No exact element match found, trying parent matching...');\n\n // Try finding closest clickable parent\n const parentMatch = findParentMatch(candidate_elements, actualInteractionElementInfo);\n if (parentMatch) {\n return parentMatch;\n }\n highlighterLogger.debug('❌ No parent match found, falling back to spatial matching...');\n\n // If no exact or parent match, look for spatial relationships\n const spatialMatch = findSpatialMatch(candidate_elements, actualInteractionElementInfo);\n if (spatialMatch) {\n return spatialMatch;\n }\n\n highlighterLogger.error('❌ No matching element found for actual interaction element:', actualInteractionElementInfo);\n return null;\n }\n\n const highlight = {\n execute: async function(elementTypes, handleScroll=false) {\n const elements = await findElements(elementTypes);\n highlightElements(elements, false,handleScroll);\n return elements;\n },\n\n unexecute: function(handleScroll=false) {\n unhighlightElements(handleScroll);\n },\n\n generateJSON: async function() {\n const json = {};\n\n // Capture viewport dimensions\n const viewportData = {\n width: window.innerWidth,\n height: window.innerHeight,\n documentWidth: document.documentElement.clientWidth,\n documentHeight: document.documentElement.clientHeight,\n timestamp: new Date().toISOString()\n };\n\n // Add viewport data to the JSON output\n json.viewport = viewportData;\n\n\n await Promise.all(Object.values(ElementTag).map(async elementType => {\n const elements = await findElements(elementType);\n json[elementType] = elements;\n }));\n\n // Serialize the JSON object\n const jsonString = JSON.stringify(json, null, 4); // Pretty print with 4 spaces\n\n highlighterLogger.log(`JSON: ${jsonString}`);\n return jsonString;\n },\n\n getElementInfo\n };\n\n\n function unhighlightElements(handleScroll=false) {\n const documents = getAllFrames();\n documents.forEach(doc => {\n const overlay = doc.getElementById('highlight-overlay');\n if (overlay) {\n if (handleScroll) {\n // Remove event listeners\n doc.removeEventListener('scroll', overlay.scrollHandler, true);\n doc.removeEventListener('resize', overlay.resizeHandler);\n }\n overlay.remove();\n }\n });\n }\n\n\n async function findElements(elementTypes, verbose=true) {\n const typesArray = Array.isArray(elementTypes) ? elementTypes : [elementTypes];\n highlighterLogger.debug('Starting element search for types:', typesArray);\n\n const elements = [];\n typesArray.forEach(elementType => {\n if (elementType === ElementTag.FILLABLE) {\n elements.push(...findFillables());\n }\n if (elementType === ElementTag.SELECTABLE) {\n elements.push(...findDropdowns());\n }\n if (elementType === ElementTag.CLICKABLE) {\n elements.push(...findClickables());\n elements.push(...findToggles());\n elements.push(...findCheckables());\n }\n if (elementType === ElementTag.NON_INTERACTIVE_ELEMENT) {\n elements.push(...findNonInteractiveElements());\n }\n });\n\n // console.log('Before uniquify:', elements.length);\n const elementsWithInfo = elements.map((element, index) => \n getElementInfo(element, index)\n );\n\n \n \n const uniqueElements = uniquifyElements(elementsWithInfo);\n highlighterLogger.debug(`Found ${uniqueElements.length} elements:`);\n \n // More comprehensive visibility check\n const visibleElements = uniqueElements.filter(elementInfo => {\n const el = elementInfo.element;\n const style = getComputedStyle(el);\n \n // Check various style properties that affect visibility\n if (style.display === 'none' || \n style.visibility === 'hidden') {\n return false;\n }\n \n // Check if element has non-zero dimensions\n const rect = el.getBoundingClientRect();\n if (rect.width === 0 || rect.height === 0) {\n return false;\n }\n \n // Check if element is within viewport\n if (rect.bottom < 0 || \n rect.top > window.innerHeight || \n rect.right < 0 || \n rect.left > window.innerWidth) {\n // Element is outside viewport, but still might be valid \n // if user scrolls to it, so we'll include it\n return true;\n }\n \n return true;\n });\n \n highlighterLogger.debug(`Out of which ${visibleElements.length} elements are visible:`);\n if (verbose) {\n visibleElements.forEach(info => {\n highlighterLogger.debug(`Element ${info.index}:`, info);\n });\n }\n \n return visibleElements;\n }\n\n // elements is an array of objects with index, xpath\n function highlightElements(elements, enableSmartSelectors=false, handleScroll=false) {\n // console.log('[highlightElements] called with', elements.length, 'elements');\n // Create overlay if it doesn't exist and store it in a dictionary\n const documents = getAllFrames(); \n let overlays = {};\n documents.forEach(doc => {\n let overlay = doc.getElementById('highlight-overlay');\n if (!overlay) {\n overlay = doc.createElement('div');\n overlay.id = 'highlight-overlay';\n overlay.style.cssText = `\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n pointer-events: none;\n z-index: 2147483647;\n `;\n doc.body.appendChild(overlay);\n // console.log('[highlightElements] Created overlay in document:', doc);\n }\n overlays[doc.documentURI] = overlay;\n });\n \n\n const updateHighlights = (doc = null) => {\n if (doc) {\n overlays[doc.documentURI].innerHTML = '';\n } else {\n Object.values(overlays).forEach(overlay => { overlay.innerHTML = ''; });\n } \n elements.forEach((elementInfo, idx) => {\n //console.log(`[highlightElements] Processing element ${idx}:`, elementInfo.tag, elementInfo.css_selector, elementInfo.bounding_box);\n let element = elementInfo.element;\n if (!element) {\n element = findElementByInfo(elementInfo, enableSmartSelectors);\n if (!element) {\n highlighterLogger.warn('[highlightElements] Could not find element for:', elementInfo);\n return;\n }\n }\n //if highlights requested for a specific doc, skip unrelated elements\n if (doc && element.ownerDocument !== doc) {\n highlighterLogger.debug(\"[highlightElements] Skipped element since it doesn't belong to document\", doc);\n return;\n }\n const rect = element.getBoundingClientRect();\n if (rect.width === 0 || rect.height === 0) {\n highlighterLogger.warn('[highlightElements] Element has zero dimensions:', elementInfo);\n return;\n }\n // Create border highlight (red rectangle)\n // use ownerDocument to support iframes/frames\n const highlight = element.ownerDocument.createElement('div');\n highlight.style.cssText = `\n position: fixed;\n left: ${rect.x}px;\n top: ${rect.y}px;\n width: ${rect.width}px;\n height: ${rect.height}px;\n border: 1px solid rgb(255, 0, 0);\n transition: all 0.2s ease-in-out;\n `;\n // Create index label container - now positioned to the right and slightly up\n const labelContainer = element.ownerDocument.createElement('div');\n labelContainer.style.cssText = `\n position: absolute;\n right: -10px; /* Offset to the right */\n top: -10px; /* Offset upwards */\n padding: 4px;\n background-color: rgba(255, 255, 0, 0.6);\n display: flex;\n align-items: center;\n justify-content: center;\n `;\n const text = element.ownerDocument.createElement('span');\n text.style.cssText = `\n color: rgb(0, 0, 0, 0.8);\n font-family: 'Courier New', Courier, monospace;\n font-size: 12px;\n font-weight: bold;\n line-height: 1;\n `;\n text.textContent = elementInfo.index.toString();\n labelContainer.appendChild(text);\n highlight.appendChild(labelContainer); \n overlays[element.ownerDocument.documentURI].appendChild(highlight);\n \n });\n };\n\n // Initial highlight\n updateHighlights();\n\n if (handleScroll) {\n documents.forEach(doc => {\n // Update highlights on scroll and resize\n highlighterLogger.debug('registering scroll and resize handlers for document: ', doc);\n const scrollHandler = () => {\n requestAnimationFrame(() => updateHighlights(doc));\n };\n const resizeHandler = () => {\n updateHighlights(doc);\n };\n doc.addEventListener('scroll', scrollHandler, true);\n doc.addEventListener('resize', resizeHandler);\n // Store event handlers for cleanup\n overlays[doc.documentURI].scrollHandler = scrollHandler;\n overlays[doc.documentURI].resizeHandler = resizeHandler;\n }); \n }\n }\n\n // function unexecute() {\n // unhighlightElements();\n // }\n\n // Make it available globally for both Extension and Playwright\n if (typeof window !== 'undefined') {\n function stripElementRefs(elementInfo) {\n if (!elementInfo) return null;\n const { element, ...rest } = elementInfo;\n return rest;\n }\n\n window.ProboLabs = window.ProboLabs || {};\n\n // --- Caching State ---\n window.ProboLabs.candidates = [];\n window.ProboLabs.actual = null;\n window.ProboLabs.matchingCandidate = null;\n\n // --- Methods ---\n /**\n * Find and cache candidate elements of a given type (e.g., 'CLICKABLE').\n * NOTE: This function is async and must be awaited from Playwright/Node.\n */\n window.ProboLabs.findAndCacheCandidateElements = async function(elementType) {\n //console.log('[ProboLabs] findAndCacheCandidateElements called with:', elementType);\n const found = await findElements(elementType);\n window.ProboLabs.candidates = found;\n // console.log('[ProboLabs] candidates set to:', found, 'type:', typeof found, 'isArray:', Array.isArray(found));\n return found.length;\n };\n\n window.ProboLabs.findAndCacheActualElement = function(iframeSelector, elementSelector, isHover=false, useSmartSelectors=false) {\n // console.log('[ProboLabs] findAndCacheActualElement called with:', cssSelector, iframeSelector);\n highlighterLogger.debug(`[ProboLabs] findAndCacheActualElement called with: iframeSelector=${iframeSelector}, elementSelector=${elementSelector}, useSmartSelectors=${useSmartSelectors}`);\n let el = findElementBySelectors(iframeSelector, elementSelector, useSmartSelectors);\n if(isHover) {\n const visibleElement = findClosestVisibleElement(el);\n if (visibleElement) {\n el = visibleElement;\n }\n }\n if (!el) {\n window.ProboLabs.actual = null;\n // console.log('[ProboLabs] actual set to null');\n return false;\n }\n window.ProboLabs.actual = getElementInfo(el, -1);\n // console.log('[ProboLabs] actual set to:', window.ProboLabs.actual);\n return true;\n };\n\n window.ProboLabs.findAndCacheMatchingCandidate = function() {\n // console.log('[ProboLabs] findAndCacheMatchingCandidate called');\n if (!window.ProboLabs.candidates.length || !window.ProboLabs.actual) {\n window.ProboLabs.matchingCandidate = null;\n // console.log('[ProboLabs] matchingCandidate set to null');\n return false;\n }\n window.ProboLabs.matchingCandidate = findMatchingCandidateElementInfo(window.ProboLabs.candidates, window.ProboLabs.actual);\n // console.log('[ProboLabs] matchingCandidate set to:', window.ProboLabs.matchingCandidate);\n return !!window.ProboLabs.matchingCandidate;\n };\n\n window.ProboLabs.highlightCachedElements = function(which) {\n let elements = [];\n if (which === 'candidates') elements = window.ProboLabs.candidates;\n if (which === 'actual' && window.ProboLabs.actual) elements = [window.ProboLabs.actual];\n if (which === 'matching' && window.ProboLabs.matchingCandidate) elements = [window.ProboLabs.matchingCandidate];\n highlighterLogger.debug(`[ProboLabs] highlightCachedElements ${which} with ${elements.length} elements`);\n highlightElements(elements);\n };\n\n window.ProboLabs.unhighlight = function() {\n // console.log('[ProboLabs] unhighlight called');\n unhighlightElements();\n };\n\n window.ProboLabs.reset = function() {\n highlighterLogger.debug('[ProboLabs] reset called');\n window.ProboLabs.candidates = [];\n window.ProboLabs.actual = null;\n window.ProboLabs.matchingCandidate = null;\n unhighlightElements();\n };\n\n window.ProboLabs.getCandidates = function() {\n // console.log('[ProboLabs] getCandidates called. candidates:', window.ProboLabs.candidates, 'type:', typeof window.ProboLabs.candidates, 'isArray:', Array.isArray(window.ProboLabs.candidates));\n const arr = Array.isArray(window.ProboLabs.candidates) ? window.ProboLabs.candidates : [];\n return arr.map(stripElementRefs);\n };\n window.ProboLabs.getActual = function() {\n return stripElementRefs(window.ProboLabs.actual);\n };\n window.ProboLabs.getMatchingCandidate = function() {\n return stripElementRefs(window.ProboLabs.matchingCandidate);\n };\n\n window.ProboLabs.setLoggerDebugLevel = function(debugLevel) {\n highlighterLogger.setLogLevel(debugLevel);\n };\n\n // Retain existing API for backward compatibility\n window.ProboLabs.ElementTag = ElementTag;\n window.ProboLabs.highlightElements = highlightElements;\n window.ProboLabs.unhighlightElements = unhighlightElements;\n window.ProboLabs.findElements = findElements;\n window.ProboLabs.getElementInfo = getElementInfo;\n window.ProboLabs.highlight = window.ProboLabs.highlight;\n window.ProboLabs.unhighlight = window.ProboLabs.unhighlight;\n\n // --- Utility Functions ---\n function findClosestVisibleElement(element) {\n let current = element;\n while (current) {\n const style = window.getComputedStyle(current);\n if (\n style &&\n style.display !== 'none' &&\n style.visibility !== 'hidden' &&\n current.offsetWidth > 0 &&\n current.offsetHeight > 0\n ) {\n return current;\n }\n if (!current.parentElement || current === document.body) break;\n current = current.parentElement;\n }\n return null;\n }\n }\n\n exports.deserializeNodeFromJSON = deserializeNodeFromJSON;\n exports.detectScrollableContainers = detectScrollableContainers;\n exports.findElementByInfo = findElementByInfo;\n exports.findElementBySelectors = findElementBySelectors;\n exports.findElements = findElements;\n exports.generateCssPath = generateCssPath;\n exports.generateSmartSelector = generateSmartSelector;\n exports.getAriaLabelledByText = getAriaLabelledByText;\n exports.getContainingIFrame = getContainingIFrame;\n exports.getElementInfo = getElementInfo;\n exports.highlight = highlight;\n exports.highlightElements = highlightElements;\n exports.isScrollableContainer = isScrollableContainer;\n exports.serializeNodeToJSON = serializeNodeToJSON;\n exports.unhighlightElements = unhighlightElements;\n\n}));\n//# sourceMappingURL=probolabs.umd.js.map\n";
|
|
1
|
+
const highlighterCode = "(function (global, factory) {\n typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :\n typeof define === 'function' && define.amd ? define(['exports'], factory) :\n (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.ProboLabs = {}));\n})(this, (function (exports) { 'use strict';\n\n /**\n * Element tag constants for different types of interactive elements\n */\n const ElementTag = {\n CLICKABLE: \"CLICKABLE\",\n FILLABLE: \"FILLABLE\",\n SELECTABLE: \"SELECTABLE\",\n NON_INTERACTIVE_ELEMENT: 'NON_INTERACTIVE_ELEMENT',\n };\n\n var ApplyAIStatus;\n (function (ApplyAIStatus) {\n ApplyAIStatus[\"PREPARE_START\"] = \"PREPARE_START\";\n ApplyAIStatus[\"PREPARE_SUCCESS\"] = \"PREPARE_SUCCESS\";\n ApplyAIStatus[\"PREPARE_ERROR\"] = \"PREPARE_ERROR\";\n ApplyAIStatus[\"SEND_START\"] = \"SEND_START\";\n ApplyAIStatus[\"SEND_SUCCESS\"] = \"SEND_SUCCESS\";\n ApplyAIStatus[\"SEND_ERROR\"] = \"SEND_ERROR\";\n ApplyAIStatus[\"APPLY_AI_ERROR\"] = \"APPLY_AI_ERROR\";\n ApplyAIStatus[\"APPLY_AI_CANCELLED\"] = \"APPLY_AI_CANCELLED\";\n ApplyAIStatus[\"SUMMARY_COMPLETED\"] = \"SUMMARY_COMPLETED\";\n ApplyAIStatus[\"SUMMARY_ERROR\"] = \"SUMMARY_ERROR\";\n })(ApplyAIStatus || (ApplyAIStatus = {}));\n var ReplayStatus;\n (function (ReplayStatus) {\n ReplayStatus[\"REPLAY_START\"] = \"REPLAY_START\";\n ReplayStatus[\"REPLAY_SUCCESS\"] = \"REPLAY_SUCCESS\";\n ReplayStatus[\"REPLAY_ERROR\"] = \"REPLAY_ERROR\";\n ReplayStatus[\"REPLAY_CANCELLED\"] = \"REPLAY_CANCELLED\";\n })(ReplayStatus || (ReplayStatus = {}));\n\n // Action constants\n var PlaywrightAction;\n (function (PlaywrightAction) {\n PlaywrightAction[\"VISIT_BASE_URL\"] = \"VISIT_BASE_URL\";\n PlaywrightAction[\"VISIT_URL\"] = \"VISIT_URL\";\n PlaywrightAction[\"CLICK\"] = \"CLICK\";\n PlaywrightAction[\"FILL_IN\"] = \"FILL_IN\";\n PlaywrightAction[\"SELECT_DROPDOWN\"] = \"SELECT_DROPDOWN\";\n PlaywrightAction[\"SELECT_MULTIPLE_DROPDOWN\"] = \"SELECT_MULTIPLE_DROPDOWN\";\n PlaywrightAction[\"CHECK_CHECKBOX\"] = \"CHECK_CHECKBOX\";\n PlaywrightAction[\"SELECT_RADIO\"] = \"SELECT_RADIO\";\n PlaywrightAction[\"TOGGLE_SWITCH\"] = \"TOGGLE_SWITCH\";\n PlaywrightAction[\"SET_SLIDER\"] = \"SET_SLIDER\";\n PlaywrightAction[\"TYPE_KEYS\"] = \"TYPE_KEYS\";\n PlaywrightAction[\"HOVER\"] = \"HOVER\";\n PlaywrightAction[\"ASSERT_EXACT_VALUE\"] = \"ASSERT_EXACT_VALUE\";\n PlaywrightAction[\"ASSERT_CONTAINS_VALUE\"] = \"ASSERT_CONTAINS_VALUE\";\n PlaywrightAction[\"ASSERT_URL\"] = \"ASSERT_URL\";\n PlaywrightAction[\"SCROLL_TO_ELEMENT\"] = \"SCROLL_TO_ELEMENT\";\n PlaywrightAction[\"EXTRACT_VALUE\"] = \"EXTRACT_VALUE\";\n PlaywrightAction[\"ASK_AI\"] = \"ASK_AI\";\n PlaywrightAction[\"EXECUTE_SCRIPT\"] = \"EXECUTE_SCRIPT\";\n PlaywrightAction[\"UPLOAD_FILES\"] = \"UPLOAD_FILES\";\n PlaywrightAction[\"WAIT_FOR\"] = \"WAIT_FOR\";\n PlaywrightAction[\"WAIT_FOR_OTP\"] = \"WAIT_FOR_OTP\";\n PlaywrightAction[\"GEN_TOTP\"] = \"GEN_TOTP\";\n // Sequence marker actions (not real interactions, used only for grouping)\n PlaywrightAction[\"SEQUENCE_START\"] = \"SEQUENCE_START\";\n PlaywrightAction[\"SEQUENCE_END\"] = \"SEQUENCE_END\";\n })(PlaywrightAction || (PlaywrightAction = {}));\n\n /**\n * Checks if any string in the array matches the given regular expression pattern.\n *\n * @param array - Array of strings to test.\n * @param pattern - Regular expression to test against each string.\n * @returns true if at least one string matches the pattern, false otherwise.\n */\n function testArray(array, pattern) {\n return array.some(item => pattern.test(item));\n }\n /**\n * Determines if an element is fillable (can accept text input)\n *\n * @param element The DOM element to check\n * @returns boolean indicating if the element is fillable\n */\n function isFillableElement(element) {\n var _a;\n if (!element)\n return false;\n // Check if it's an input element\n if (element.tagName === 'INPUT') {\n const inputType = (_a = element.type) === null || _a === void 0 ? void 0 : _a.toLowerCase();\n const isOTPInput = element.getAttribute('data-input-otp') === 'true' ||\n element.getAttribute('autocomplete') === 'one-time-code';\n // if it's an OTP input, don't consider it fillable as it's handled by OTP logic\n if (isOTPInput)\n return false;\n // Include text, password, email, number, tel, url, search, date, datetime-local, month, week, time\n const fillableTypes = [\n 'text', 'password', 'email', 'number', 'tel', 'url', 'search',\n 'date', 'datetime-local', 'month', 'week', 'time'\n ];\n return fillableTypes.includes(inputType);\n }\n // Check if it's a textarea\n if (element.tagName === 'TEXTAREA') {\n return true;\n }\n // Check if it's a contenteditable element\n if (element.getAttribute('contenteditable') === 'true') {\n return true;\n }\n return false;\n }\n /**\n * Determines if an element is clickable\n *\n * @param element The DOM element to check\n * @returns boolean indicating if the element is fillable\n */\n function isClickableElement(element) {\n if (!element)\n return false;\n let depth = 0;\n while (depth < 5 && element && element.nodeType === Node.ELEMENT_NODE) {\n if (isClickableElementHelper(element) === IsClickable.YES)\n return true;\n if (isClickableElementHelper(element) === IsClickable.NO)\n return false;\n // if maybe, continue searching up to 5 levels up the DOM tree\n element = element.parentNode;\n depth++;\n }\n return false;\n }\n // clickable element detection result\n var IsClickable;\n (function (IsClickable) {\n IsClickable[\"YES\"] = \"YES\";\n IsClickable[\"NO\"] = \"NO\";\n IsClickable[\"MAYBE\"] = \"MAYBE\";\n })(IsClickable || (IsClickable = {}));\n function isClickableElementHelper(element) {\n var _a, _b;\n if (!element)\n return IsClickable.NO;\n //check for tag name\n const tagName = element.tagName.toLowerCase();\n const clickableTags = [\n 'a', 'button',\n ];\n if (clickableTags.includes(tagName))\n return IsClickable.YES;\n //check for clickable <input>\n const inputType = (_a = element.type) === null || _a === void 0 ? void 0 : _a.toLowerCase();\n const clickableTypes = [\n 'button', 'submit', 'reset', 'checkbox', 'radio',\n ];\n const ariaAutocompleteValues = [\n 'list', 'both',\n ];\n if (tagName === 'input') {\n if (clickableTypes.includes(inputType) || ariaAutocompleteValues.includes((_b = element.getAttribute('aria-autocomplete')) !== null && _b !== void 0 ? _b : ''))\n return IsClickable.YES;\n if (['date', 'number', 'range'].includes(inputType))\n return IsClickable.NO; //don't record the click as a change event will be generated for elements that generate an input change event\n }\n // check for cursor type\n const style = window.getComputedStyle(element);\n if (style.cursor === 'pointer')\n return IsClickable.YES;\n // check for attributes\n const clickableRoles = [\n 'button', 'combobox', 'listbox', 'dropdown', 'option', 'menu', 'menuitem',\n 'navigation', 'checkbox', 'switch', 'toggle', 'slider', 'textbox', 'listitem',\n 'presentation',\n ];\n const ariaPopupValues = [\n 'true', 'listbox', 'menu',\n ];\n if (element.hasAttribute('onclick') ||\n clickableRoles.includes(element.getAttribute('role') || '') ||\n ariaPopupValues.includes(element.getAttribute('aria-haspopup') || ''))\n return IsClickable.YES;\n // check for tabindex (means element is focusable and therefore clickable)\n if (parseInt(element.getAttribute('tabindex') || '-1') >= 0)\n return IsClickable.YES;\n // extract class names\n const classNames = Array.from(element.classList);\n // check for checkbox/radio-like class name - TODO: check if can be removed\n const checkboxPattern = /checkbox|switch|toggle|slider/i;\n if (testArray(classNames, checkboxPattern))\n return IsClickable.YES;\n // check for Material UI class names\n const muiClickableClassPattern = /MuiButton|MuiIconButton|MuiChip|MuiMenuItem|MuiListItem|MuiInputBase|MuiOutlinedInput|MuiSelect|MuiAutocomplete|MuiToggleButton|MuiBackdrop-root|MuiBackdrop-invisible/;\n if (testArray(classNames, muiClickableClassPattern))\n return IsClickable.YES;\n // check for SalesForce class names\n const sfClassPattern = /slds-button|slds-dropdown|slds-combobox|slds-picklist|slds-tabs|slds-pill|slds-action|slds-row-action|slds-context-bar|slds-input|slds-rich-text-area|slds-radio|slds-checkbox|slds-toggle|slds-link|slds-accordion|slds-tree/;\n if (testArray(classNames, sfClassPattern))\n return IsClickable.YES;\n // check for chart dots\n const chartClickableClassPattern = /recharts-dot/;\n if (testArray(classNames, chartClickableClassPattern))\n return IsClickable.YES;\n // check for React component classes\n const reactClickableClassPattern = /react-select|ant-select|rc-select|react-dropdown|react-autocomplete|react-datepicker|react-modal|react-tooltip|react-popover|react-menu|react-tabs|react-accordion|react-collapse|react-toggle|react-switch|react-checkbox|react-radio|react-button|react-link|react-card|react-list-item|react-menu-item|react-option|react-tab|react-panel|react-drawer|react-sidebar|react-nav|react-breadcrumb|react-pagination|react-stepper|react-wizard|react-carousel|react-slider|react-range|react-progress|react-badge|react-chip|react-tag|react-avatar|react-icon|react-fab|react-speed-dial|react-floating|react-sticky|react-affix|react-backdrop|react-overlay|react-portal|react-transition|react-animate|react-spring|react-framer|react-gesture|react-drag|react-drop|react-sortable|react-resizable|react-split|react-grid|react-table|react-datagrid|react-tree|react-treeview|react-file|react-upload|react-cropper|react-image|react-gallery|react-lightbox|react-player|react-video|react-audio|react-chart|react-graph|react-diagram|react-flow|react-d3|react-plotly|react-vega|react-vis|react-nivo|react-recharts|react-victory|react-echarts|react-highcharts|react-google-charts|react-fusioncharts|react-apexcharts|react-chartjs|react-chartkick|react-sparklines|react-trend|react-smooth|react-animated|react-lottie|react-spring|react-framer-motion|react-pose|react-motion|react-transition-group|react-router|react-navigation/i;\n if (testArray(classNames, reactClickableClassPattern))\n return IsClickable.YES;\n //check for cloudinary class names\n const cloudinaryClickableClassPattern = /cld-combobox|cld-upload-button|cld-controls|cld-player|cld-tab|cld-menu-item|cld-close|cld-play|cld-pause|cld-fullscreen|cld-browse|cld-cancel|cld-retry/;\n if (testArray(classNames, cloudinaryClickableClassPattern))\n return IsClickable.YES;\n return IsClickable.MAYBE;\n }\n function getParentNode(element) {\n if (!element || element.nodeType !== Node.ELEMENT_NODE)\n return null;\n let parent = null;\n // SF is using slots and shadow DOM heavily\n // However, there might be slots in the light DOM which shouldn't be traversed\n if (element.assignedSlot && element.getRootNode() instanceof ShadowRoot)\n parent = element.assignedSlot;\n else\n parent = element.parentNode;\n // Check if we're at a shadow root\n if (parent && parent.nodeType !== Node.ELEMENT_NODE && parent.getRootNode() instanceof ShadowRoot)\n parent = parent.getRootNode().host;\n return parent;\n }\n function getElementDepth(element) {\n let depth = 0;\n let currentElement = element;\n while ((currentElement === null || currentElement === void 0 ? void 0 : currentElement.nodeType) === Node.ELEMENT_NODE) {\n depth++;\n currentElement = getParentNode(currentElement);\n }\n return depth;\n }\n\n // WebSocketsMessageType enum for WebSocket and event message types shared across the app\n var WebSocketsMessageType;\n (function (WebSocketsMessageType) {\n WebSocketsMessageType[\"INTERACTION_APPLY_AI_PREPARE_START\"] = \"INTERACTION_APPLY_AI_PREPARE_START\";\n WebSocketsMessageType[\"INTERACTION_APPLY_AI_PREPARE_SUCCESS\"] = \"INTERACTION_APPLY_AI_PREPARE_SUCCESS\";\n WebSocketsMessageType[\"INTERACTION_APPLY_AI_PREPARE_ERROR\"] = \"INTERACTION_APPLY_AI_PREPARE_ERROR\";\n WebSocketsMessageType[\"INTERACTION_APPLY_AI_SEND_TO_LLM_START\"] = \"INTERACTION_APPLY_AI_SEND_TO_LLM_START\";\n WebSocketsMessageType[\"INTERACTION_APPLY_AI_SEND_TO_LLM_SUCCESS\"] = \"INTERACTION_APPLY_AI_SEND_TO_LLM_SUCCESS\";\n WebSocketsMessageType[\"INTERACTION_APPLY_AI_SEND_TO_LLM_ERROR\"] = \"INTERACTION_APPLY_AI_SEND_TO_LLM_ERROR\";\n WebSocketsMessageType[\"INTERACTION_REPLAY_START\"] = \"INTERACTION_REPLAY_START\";\n WebSocketsMessageType[\"INTERACTION_REPLAY_SUCCESS\"] = \"INTERACTION_REPLAY_SUCCESS\";\n WebSocketsMessageType[\"INTERACTION_REPLAY_ERROR\"] = \"INTERACTION_REPLAY_ERROR\";\n WebSocketsMessageType[\"INTERACTION_REPLAY_CANCELLED\"] = \"INTERACTION_REPLAY_CANCELLED\";\n WebSocketsMessageType[\"INTERACTION_APPLY_AI_CANCELLED\"] = \"INTERACTION_APPLY_AI_CANCELLED\";\n WebSocketsMessageType[\"INTERACTION_APPLY_AI_ERROR\"] = \"INTERACTION_APPLY_AI_ERROR\";\n WebSocketsMessageType[\"INTERACTION_APPLY_AI_SUMMARY_COMPLETED\"] = \"INTERACTION_APPLY_AI_SUMMARY_COMPLETED\";\n WebSocketsMessageType[\"INTERACTION_APPLY_AI_SUMMARY_ERROR\"] = \"INTERACTION_APPLY_AI_SUMMARY_ERROR\";\n WebSocketsMessageType[\"OTP_RETRIEVED\"] = \"OTP_RETRIEVED\";\n WebSocketsMessageType[\"TEST_SUITE_RUN_START\"] = \"TEST_SUITE_RUN_START\";\n WebSocketsMessageType[\"TEST_SUITE_RUN_LOG\"] = \"TEST_SUITE_RUN_LOG\";\n WebSocketsMessageType[\"TEST_SUITE_RUN_COMPLETE\"] = \"TEST_SUITE_RUN_COMPLETE\";\n WebSocketsMessageType[\"TEST_SUITE_RUN_ERROR\"] = \"TEST_SUITE_RUN_ERROR\";\n WebSocketsMessageType[\"TEST_SUITE_RUN_REPORTER_EVENT\"] = \"TEST_SUITE_RUN_REPORTER_EVENT\";\n })(WebSocketsMessageType || (WebSocketsMessageType = {}));\n\n /**\n * Logging levels for Probo\n */\n var ProboLogLevel;\n (function (ProboLogLevel) {\n ProboLogLevel[\"DEBUG\"] = \"DEBUG\";\n ProboLogLevel[\"INFO\"] = \"INFO\";\n ProboLogLevel[\"LOG\"] = \"LOG\";\n ProboLogLevel[\"WARN\"] = \"WARN\";\n ProboLogLevel[\"ERROR\"] = \"ERROR\";\n })(ProboLogLevel || (ProboLogLevel = {}));\n const logLevelOrder = {\n [ProboLogLevel.DEBUG]: 0,\n [ProboLogLevel.INFO]: 1,\n [ProboLogLevel.LOG]: 2,\n [ProboLogLevel.WARN]: 3,\n [ProboLogLevel.ERROR]: 4,\n };\n class ProboLogger {\n constructor(prefix, level = ProboLogLevel.INFO) {\n this.prefix = prefix;\n this.level = level;\n }\n setLogLevel(level) {\n console.log(`[${this.prefix}] Setting log level to: ${level} (was: ${this.level})`);\n this.level = level;\n }\n shouldLog(level) {\n return logLevelOrder[level] >= logLevelOrder[this.level];\n }\n preamble(level) {\n const now = new Date();\n const hours = String(now.getHours()).padStart(2, '0');\n const minutes = String(now.getMinutes()).padStart(2, '0');\n const seconds = String(now.getSeconds()).padStart(2, '0');\n const milliseconds = String(now.getMilliseconds()).padStart(3, '0');\n return `[${hours}:${minutes}:${seconds}.${milliseconds}] [${this.prefix}] [${level}]`;\n }\n debug(...args) { if (this.shouldLog(ProboLogLevel.DEBUG))\n console.debug(this.preamble(ProboLogLevel.DEBUG), ...args); }\n info(...args) { if (this.shouldLog(ProboLogLevel.INFO))\n console.info(this.preamble(ProboLogLevel.INFO), ...args); }\n log(...args) { if (this.shouldLog(ProboLogLevel.LOG))\n console.log(this.preamble(ProboLogLevel.LOG), ...args); }\n warn(...args) { if (this.shouldLog(ProboLogLevel.WARN))\n console.warn(this.preamble(ProboLogLevel.WARN), ...args); }\n error(...args) { if (this.shouldLog(ProboLogLevel.ERROR))\n console.error(this.preamble(ProboLogLevel.ERROR), ...args); }\n }\n new ProboLogger('fetch');\n function doubleQuoteString(str) {\n if (!str)\n return '';\n return `\"${str.replace(/\"/g, '\\\\\"')}\"`;\n }\n\n new ProboLogger('apiclient');\n\n /**\n * Available AI models for LLM operations\n */\n var AIModel;\n (function (AIModel) {\n AIModel[\"AZURE_GPT4\"] = \"azure-gpt4\";\n AIModel[\"AZURE_GPT4_MINI\"] = \"azure-gpt4-mini\";\n AIModel[\"GEMINI_1_5_FLASH\"] = \"gemini-1.5-flash\";\n AIModel[\"GEMINI_2_5_FLASH\"] = \"gemini-2.5-flash\";\n AIModel[\"GPT4\"] = \"gpt4\";\n AIModel[\"GPT4_MINI\"] = \"gpt4-mini\";\n AIModel[\"CLAUDE_3_5\"] = \"claude-3.5\";\n AIModel[\"CLAUDE_SONNET_4_5\"] = \"claude-sonnet-4.5\";\n AIModel[\"CLAUDE_HAIKU_4_5\"] = \"claude-haiku-4.5\";\n AIModel[\"CLAUDE_OPUS_4_1\"] = \"claude-opus-4.1\";\n AIModel[\"GROK_2\"] = \"grok-2\";\n AIModel[\"LLAMA_4_SCOUT\"] = \"llama-4-scout\";\n AIModel[\"DEEPSEEK_V3\"] = \"deepseek-v3\";\n })(AIModel || (AIModel = {}));\n\n const highlighterLogger = new ProboLogger('highlighter');\n\n // Get text content of an element (recursively including child elements)\n function getElementText(element) {\n if (!element) return '';\n return element.textContent || '';\n }\n\n // Get placeholder text of an element\n function getElementPlaceholder(element) {\n if (!element) return '';\n return element.placeholder || '';\n }\n\n // Get CSS selector of an element (not recursively, just for a single element)\n function getElementCssSelector(element) {\n if (!(element instanceof Element)) return '';\n \n const tag = element.tagName.toLowerCase();\n let selector = tag;\n \n // Find nth-of-type for elements without ID\n let nth = 1;\n let sibling = element.previousElementSibling;\n while (sibling) {\n if (sibling.nodeName.toLowerCase() === tag) nth++;\n sibling = sibling.previousElementSibling;\n }\n if (nth > 1 || element.nextElementSibling) selector += `:nth-of-type(${nth})`;\n \n return selector;\n }\n\n // Helper function to check if element is a form control\n function isFormControl(elementInfo) {\n return /^(input|select|textarea|button|label)$/i.test(elementInfo.tag);\n }\n\n // Helper function to check if element is a dropdown item\n const isDropdownItem = (elementInfo) => {\n const dropdownPatterns = [\n /dropdown[-_]?item/i, // matches: dropdown-item, dropdownitem, dropdown_item\n /menu[-_]?item/i, // matches: menu-item, menuitem, menu_item\n /dropdown[-_]?link/i, // matches: dropdown-link, dropdownlink, dropdown_link\n /list[-_]?item/i, // matches: list-item, listitem, list_item\n /select[-_]?item/i, // matches: select-item, selectitem, select_item \n ];\n\n const rolePatterns = [\n /menu[-_]?item/i, // matches: menuitem, menu-item\n /option/i, // matches: option\n /list[-_]?item/i, // matches: listitem, list-item\n /tree[-_]?item/i // matches: treeitem, tree-item\n ];\n\n const hasMatchingClass = elementInfo.element.className && \n dropdownPatterns.some(pattern => \n pattern.test(elementInfo.element.className)\n );\n\n const hasMatchingRole = elementInfo.element.getAttribute('role') && \n rolePatterns.some(pattern => \n pattern.test(elementInfo.element.getAttribute('role'))\n );\n\n return hasMatchingClass || hasMatchingRole;\n };\n\n /**\n * Element Query Utilities\n */\n\n function getAllDocumentElementsIncludingShadow(selectors, root = document) {\n const elements = Array.from(root.querySelectorAll(selectors));\n\n root.querySelectorAll('*').forEach(el => {\n if (el.shadowRoot) {\n elements.push(...getAllDocumentElementsIncludingShadow(selectors, el.shadowRoot));\n }\n });\n return elements;\n }\n\n function getAllFrames(root = document) {\n const result = [root];\n const frames = getAllDocumentElementsIncludingShadow('frame, iframe', root); \n frames.forEach(frame => {\n try {\n const frameDocument = frame.contentDocument || frame.contentWindow.document;\n if (frameDocument) {\n result.push(frameDocument);\n }\n } catch (e) {\n // Skip cross-origin frames\n highlighterLogger.warn('Could not access frame content:', e.message);\n }\n });\n\n return result;\n }\n\n function getAllElementsIncludingShadow(selectors, root = document) {\n const elements = [];\n\n getAllFrames(root).forEach(doc => {\n elements.push(...getAllDocumentElementsIncludingShadow(selectors, doc));\n });\n\n return elements;\n }\n\n /**\n * Finds the first element matching a CSS selector, traversing Shadow DOM if necessary\n * @param {string} selector - CSS selector to search for\n * @param {Element} [root=document] - Root element to start searching from\n * @returns {Element|null} - The first matching element or null if not found\n */\n function robustQuerySelector(selector, root = document, all = false) {\n // First try to find in light DOM\n let elements = all ? root.querySelectorAll(selector) : root.querySelector(selector);\n if (elements) return elements;\n \n // Get all shadow roots\n const shadowElements = Array.from(root.querySelectorAll('*'))\n .filter(el => el.shadowRoot);\n \n // Search through each shadow root until we find a match\n for (const el of shadowElements) {\n elements = robustQuerySelector(selector, el.shadowRoot, all);\n if (elements) return elements;\n }\n \n return all ? [] : null;\n }\n\n\n function isDecendent(parent, child) {\n let element = child;\n while (element && element !== parent && element.nodeType === Node.ELEMENT_NODE) { \n element = getParentNode(element); \n }\n return element === parent;\n }\n\n function generateXPath(element) {\n return '/'+extractElementPath(element).map(item => `${item.tagName}${item.onlyChild ? '' : `[${item.index}]`}`).join('/');\n }\n\n function generateCssPath(element) {\n return extractElementPath(element).map(item => `${item.tagName}:nth-of-type(${item.index})`).join(' > ');\n }\n\n // unified path generation for xpath and css selector\n function extractElementPath(element) {\n if (!element) {\n highlighterLogger.error('ERROR: No element provided to generatePath');\n return [];\n }\n const path = [];\n // traversing up the DOM tree\n while (element && element.nodeType === Node.ELEMENT_NODE) { \n let tagName = element.nodeName.toLowerCase();\n \n let sibling = element;\n let index = 1;\n \n while (sibling = sibling.previousElementSibling) {\n if (sibling.nodeName.toLowerCase() === tagName) index++;\n }\n sibling = element;\n \n let onlyChild = (index === 1);\n while (onlyChild && (sibling = sibling.nextElementSibling)) {\n if (sibling.nodeName.toLowerCase() === tagName) onlyChild = false;\n }\n \n // add a tuple with tagName, index (nth), and onlyChild \n path.unshift({\n tagName: tagName,\n index: index,\n onlyChild: onlyChild \n }); \n\n element = getParentNode(element);\n }\n \n return path;\n }\n\n function cleanHTML(rawHTML) {\n const parser = new DOMParser();\n const doc = parser.parseFromString(rawHTML, \"text/html\");\n\n function cleanElement(element) {\n const allowedAttributes = new Set([\n \"role\",\n \"type\",\n \"class\",\n \"href\",\n \"alt\",\n \"title\",\n \"readonly\",\n \"checked\",\n \"enabled\",\n \"disabled\",\n ]);\n\n [...element.attributes].forEach(attr => {\n const name = attr.name.toLowerCase();\n const value = attr.value;\n\n const isTestAttribute = /^(testid|test-id|data-test-id)$/.test(name);\n const isDataAttribute = name.startsWith(\"data-\") && value;\n const isBooleanAttribute = [\"readonly\", \"checked\", \"enabled\", \"disabled\"].includes(name);\n\n if (!allowedAttributes.has(name) && !isDataAttribute && !isTestAttribute && !isBooleanAttribute) {\n element.removeAttribute(name);\n }\n });\n\n // Handle SVG content - more aggressive replacement\n if (element.tagName.toLowerCase() === \"svg\") {\n // Remove all attributes except class and role\n [...element.attributes].forEach(attr => {\n const name = attr.name.toLowerCase();\n if (name !== \"class\" && name !== \"role\") {\n element.removeAttribute(name);\n }\n });\n element.innerHTML = \"CONTENT REMOVED\";\n } else {\n // Recursively clean child elements\n Array.from(element.children).forEach(cleanElement);\n }\n\n // Only remove empty elements that aren't semantic or icon elements\n const keepEmptyElements = ['i', 'span', 'svg', 'button', 'input'];\n if (!keepEmptyElements.includes(element.tagName.toLowerCase()) && \n !element.children.length && \n !element.textContent.trim()) {\n element.remove();\n }\n }\n\n // Process all elements in the document body\n Array.from(doc.body.children).forEach(cleanElement);\n return doc.body.innerHTML;\n }\n\n function getContainingIFrame(element) {\n // If not in an iframe, return null\n if (element.ownerDocument.defaultView === window.top) {\n return null;\n }\n \n // Try to find the iframe in the parent document that contains our element\n try {\n const parentDocument = element.ownerDocument.defaultView.parent.document;\n const iframes = parentDocument.querySelectorAll('iframe');\n \n for (const iframe of iframes) {\n if (iframe.contentWindow === element.ownerDocument.defaultView) {\n return iframe;\n }\n }\n } catch (e) {\n // Cross-origin restriction\n return \"Cross-origin iframe - cannot access details\";\n }\n \n return null;\n }\n\n function getAriaLabelledByText(elementInfo, includeHidden=true) {\n if (!elementInfo.element.hasAttribute('aria-labelledby')) return '';\n\n const ids = elementInfo.element.getAttribute('aria-labelledby').split(/\\s+/);\n let labelText = '';\n\n //locate root (document or iFrame document if element is contained in an iframe)\n let root = document;\n if (elementInfo.iframe_selector) { \n const frames = getAllDocumentElementsIncludingShadow('iframe', document);\n \n // Iterate over all frames and compare their CSS selectors\n for (const frame of frames) {\n const selector = generateCssPath(frame);\n if (selector === elementInfo.iframe_selector) {\n root = frame.contentDocument || frame.contentWindow.document; \n break;\n }\n } \n }\n\n ids.forEach(id => {\n const el = robustQuerySelector(`#${CSS.escape(id)}`, root);\n if (el) {\n if (includeHidden || el.offsetParent !== null || getComputedStyle(el).display !== 'none') {\n labelText += el.textContent.trim() + ' ';\n }\n }\n });\n\n return labelText.trim();\n }\n\n\n\n const filterZeroDimensions = (elementInfo) => {\n const rect = elementInfo.bounding_box;\n //single pixel elements are typically faux controls and should be filtered too\n const hasSize = rect.width > 1 && rect.height > 1;\n const style = window.getComputedStyle(elementInfo.element);\n const isVisible = style.display !== 'none' && style.visibility !== 'hidden';\n \n if (!hasSize || !isVisible) {\n \n return false;\n }\n return true;\n };\n\n function filterNestedElements(nonZeroElements, parentCandidates, shouldKeepNestedElementFn) {\n /**\n * Filter elements based on parent/child relationships.\n * @param {Array} nonZeroElements - Elements sorted by depth (parents first)\n * @param {Set} parentCandidates - Set of all candidate parent CSS selectors (all nonZeroElements)\n * @param {Function} shouldKeepNestedElementFn - Function(element_info, parentPath, parentElementInfo) => boolean\n * @returns {Array} Filtered elements\n */\n return nonZeroElements.filter(element_info => {\n const parent = findClosestParent(parentCandidates, element_info);\n \n if (parent == null) {\n // No parent found, keep element\n return true;\n }\n \n // Find parent element_info\n const parentElementInfo = nonZeroElements.find(e => e.css_selector === parent);\n \n // Use custom function to determine if we should keep nested element\n return shouldKeepNestedElementFn(element_info, parent, parentElementInfo);\n });\n }\n\n function getDirectHandlers(element) {\n /**\n * Get direct handlers for an element (no bubbling check).\n * Returns array of handler objects.\n */\n const handlers = [];\n \n // Check onclick\n if (element.hasAttribute('onclick') || element.onclick) {\n handlers.push({\n type: 'onclick',\n source: element.hasAttribute('onclick') ? 'attribute' : 'property',\n value: element.getAttribute('onclick') || 'function'\n });\n }\n \n // Check React Fiber\n const reactKey = Object.keys(element).find(key => \n key.startsWith('__reactFiber') || key.startsWith('__reactInternalInstance')\n );\n if (reactKey) {\n const fiber = element[reactKey];\n if (fiber?.memoizedProps?.onClick || fiber?.pendingProps?.onClick) {\n handlers.push({\n type: 'react-onClick',\n source: 'react-fiber',\n hasMemoized: !!fiber?.memoizedProps?.onClick,\n hasPending: !!fiber?.pendingProps?.onClick\n });\n }\n }\n \n return handlers;\n }\n\n function getElementHandlersWithBubbling(element, depth = 0) {\n /**\n * Get handler info including bubbling (recursive DOM traversal).\n * Uses actual DOM relationships (parentElement) instead of CSS selector parsing.\n * Cursor uses getComputedStyle() which already includes inheritance.\n * @param {HTMLElement} element - The element to check\n * @param {number} depth - Recursion depth (safety limit)\n */\n // Safety check: null/undefined element\n if (!element) {\n return { handlers: [], cursorPointer: false, handlerSource: 'none' };\n }\n \n // Safety check: prevent infinite recursion (circular parent references)\n if (depth > 100) {\n throw new Error(`Recursion depth limit exceeded (${depth}) in getElementHandlersWithBubbling. Possible circular parent reference. Element: ${element.tagName || 'unknown'}`);\n }\n \n const directHandlers = getDirectHandlers(element);\n \n // If element has direct handlers, return them\n if (directHandlers.length > 0) {\n // Cursor: getComputedStyle already includes inheritance!\n let cursorPointer = false;\n try {\n const style = window.getComputedStyle(element);\n cursorPointer = style.cursor === 'pointer';\n } catch (e) {\n // getComputedStyle can fail in edge cases (e.g., detached elements)\n // Default to false\n }\n \n return {\n handlers: directHandlers,\n cursorPointer: cursorPointer,\n handlerSource: 'direct'\n };\n }\n \n // No direct handlers - check parent for bubbled handlers (using DOM, not CSS selectors)\n if (element.parentElement) {\n const parentResult = getElementHandlersWithBubbling(element.parentElement, depth + 1);\n if (parentResult.handlers.length > 0) {\n // Parent has handlers that would bubble to this element\n // Cursor: getComputedStyle already includes inheritance!\n let cursorPointer = false;\n try {\n const style = window.getComputedStyle(element);\n cursorPointer = style.cursor === 'pointer';\n } catch (e) {\n // getComputedStyle can fail in edge cases (e.g., detached elements)\n // Default to false\n }\n \n return {\n handlers: parentResult.handlers.map(h => ({\n ...h,\n source: 'bubbled'\n })),\n cursorPointer: cursorPointer,\n handlerSource: 'bubbled'\n };\n }\n }\n \n // No handlers at all\n let cursorPointer = false;\n try {\n const style = window.getComputedStyle(element);\n cursorPointer = style.cursor === 'pointer';\n } catch (e) {\n // getComputedStyle can fail in edge cases (e.g., detached elements)\n // Default to false\n }\n \n return {\n handlers: [],\n cursorPointer: cursorPointer,\n handlerSource: 'none'\n };\n }\n\n function normalizeHandlers(handlers) {\n /**\n * Normalize handlers for comparison (ignore source field, deduplicate).\n * Returns sorted array of unique normalized handler objects.\n */\n const seen = new Set();\n const normalized = [];\n \n for (const h of handlers) {\n // Safety check: skip malformed handlers\n if (!h || !h.type) continue;\n \n const key = h.type === 'react-onClick' \n ? 'react-onClick' \n : `${h.type}:${h.value || 'function'}`;\n \n if (!seen.has(key)) {\n seen.add(key);\n normalized.push({\n type: h.type,\n value: h.type === 'react-onClick' ? undefined : (h.value || 'function')\n });\n }\n }\n \n return normalized.sort((a, b) => {\n if (a.type !== b.type) return a.type.localeCompare(b.type);\n return (a.value || '').localeCompare(b.value || '');\n });\n }\n\n function compareHandlersForUniquify(childHandlers, parentHandlers) {\n /**\n * Compare handlers between child and parent for uniquification.\n * Returns true if handlers are different (should keep child), false if same (can unify).\n */\n const childNormalized = normalizeHandlers(childHandlers.handlers);\n const parentNormalized = normalizeHandlers(parentHandlers.handlers);\n \n const handlersEqual = JSON.stringify(childNormalized) === JSON.stringify(parentNormalized);\n const cursorEqual = childHandlers.cursorPointer === parentHandlers.cursorPointer;\n \n // If handlers or cursor differ, keep child (don't unify)\n return !handlersEqual || !cursorEqual;\n }\n\n function uniquifyElements(elements) {\n highlighterLogger.debug(`Starting uniquification with ${elements.length} elements`);\n\n // Filter out testing infrastructure elements first\n const filteredInfrastructure = elements.filter(element_info => {\n // Skip the highlight-overlay element completely - it's part of the testing infrastructure\n if (element_info.element.id === 'highlight-overlay' || \n (element_info.css_selector && element_info.css_selector.includes('#highlight-overlay'))) {\n highlighterLogger.debug('Filtered out testing infrastructure element:', element_info.css_selector);\n return false;\n }\n \n // Filter out UI framework container/manager elements\n const el = element_info.element;\n // UI framework container checks - generic detection for any framework\n if ((el.getAttribute('data-rendered-by') || \n el.getAttribute('data-reactroot') || \n el.getAttribute('ng-version') || \n el.getAttribute('data-component-id') ||\n el.getAttribute('data-root') ||\n el.getAttribute('data-framework')) && \n (el.className && \n typeof el.className === 'string' && \n (el.className.includes('Container') || \n el.className.includes('container') || \n el.className.includes('Manager') || \n el.className.includes('manager')))) {\n highlighterLogger.debug('Filtered out UI framework container element:', element_info.css_selector);\n return false;\n }\n \n // Direct filter for framework container elements that shouldn't be interactive\n // Consolidating multiple container detection patterns into one efficient check\n const isFullViewport = element_info.bounding_box && \n element_info.bounding_box.x <= 5 && \n element_info.bounding_box.y <= 5 && \n element_info.bounding_box.width >= (window.innerWidth * 0.95) && \n element_info.bounding_box.height >= (window.innerHeight * 0.95);\n \n // Empty content check\n const isEmpty = !el.innerText || el.innerText.trim() === '';\n \n // Check if it's a framework container element\n if (element_info.element.tagName === 'DIV' && \n isFullViewport && \n isEmpty && \n (\n // Pattern matching for root containers\n (element_info.xpath && \n (element_info.xpath.match(/^\\/html\\[\\d+\\]\\/body\\[\\d+\\]\\/div\\[\\d+\\]\\/div\\[\\d+\\]$/) || \n element_info.xpath.match(/^\\/\\/\\*\\[@id='[^']+'\\]\\/div\\[\\d+\\]$/))) ||\n \n // Simple DOM structure\n (element_info.css_selector.split(' > ').length <= 4 && element_info.depth <= 5) ||\n \n // Empty or container-like classes\n (!el.className || el.className === '' || \n (typeof el.className === 'string' && \n (el.className.includes('overlay') || \n el.className.includes('container') || \n el.className.includes('wrapper'))))\n )) {\n highlighterLogger.debug('Filtered out framework container element:', element_info.css_selector);\n return false;\n }\n \n return true;\n });\n\n // First filter out elements with zero dimensions\n const nonZeroElements = filteredInfrastructure.filter(filterZeroDimensions);\n // sort by CSS selector depth so parents are processed first\n nonZeroElements.sort((a, b) => a.depth - b.depth);\n highlighterLogger.debug(`After dimension filtering: ${nonZeroElements.length} elements remain (${elements.length - nonZeroElements.length} removed)`);\n \n // Create parent candidate set with ALL nonZeroElements (before filtering)\n // This allows findClosestParent to find any element as a parent, even if it gets filtered out later\n const parentCandidates = new Set(nonZeroElements.map(e => e.css_selector));\n \n // Filter nested elements using handler comparison and standard logic\n const filteredByParent = filterNestedElements(nonZeroElements, parentCandidates, (element_info, parent, parentElementInfo) => {\n return shouldKeepNestedElement(element_info, parent, parentElementInfo);\n });\n\n highlighterLogger.debug(`After parent/child filtering: ${filteredByParent.length} elements remain (${nonZeroElements.length - filteredByParent.length} removed)`);\n\n // Final overlap filtering\n const filteredResults = filteredByParent.filter(element => {\n\n // Look for any element that came BEFORE this one in the array\n const hasEarlierOverlap = filteredByParent.some(other => {\n // Only check elements that came before (lower index)\n if (filteredByParent.indexOf(other) >= filteredByParent.indexOf(element)) {\n return false;\n }\n \n const isOverlapping = areElementsOverlapping(element, other); \n return isOverlapping;\n }); \n\n // Keep element if it has no earlier overlapping elements\n return !hasEarlierOverlap;\n });\n \n \n \n // Check for overlay removal\n highlighterLogger.debug(`After filtering: ${filteredResults.length} (${filteredByParent.length - filteredResults.length} removed by overlap)`);\n \n const nonOverlaidElements = filteredResults.filter(element => {\n return !isOverlaid(element);\n });\n\n highlighterLogger.debug(`Final elements after overlay removal: ${nonOverlaidElements.length} (${filteredResults.length - nonOverlaidElements.length} removed)`);\n \n return nonOverlaidElements;\n\n }\n\n\n\n const areElementsOverlapping = (element1, element2) => {\n if (element1.css_selector === element2.css_selector) {\n return true;\n }\n \n const box1 = element1.bounding_box;\n const box2 = element2.bounding_box;\n \n return box1.x === box2.x &&\n box1.y === box2.y &&\n box1.width === box2.width &&\n box1.height === box2.height;\n // element1.text === element2.text &&\n // element2.tag === 'a';\n };\n\n function findClosestParent(parentCandidates, element_info) { \n // Split the CSS selector into segments\n const segments = element_info.css_selector.split(' > ');\n \n // Try increasingly shorter paths until we find one in the parentCandidates set\n for (let i = segments.length - 1; i > 0; i--) {\n const parentPath = segments.slice(0, i).join(' > ');\n if (parentCandidates.has(parentPath)) {\n return parentPath;\n }\n }\n\n return null;\n }\n\n function shouldKeepNestedElement(elementInfo, parentPath, parentElementInfo) {\n // Check special cases FIRST (before handler comparison)\n const parentSegments = parentPath.split(' > ');\n\n // If parent is a link, don't keep child\n const isParentLink = /^a(:nth-of-type\\(\\d+\\))?$/.test(parentSegments[parentSegments.length - 1]);\n if (isParentLink) {\n return false; \n }\n \n // If this is a checkbox/radio input inside a label, don't keep the input (keep label instead)\n if (elementInfo.tag === 'input' && \n (elementInfo.type === 'checkbox' || elementInfo.type === 'radio')) {\n const isParentLabel = /^label(:nth-of-type\\(\\d+\\))?$/.test(parentSegments[parentSegments.length - 1]);\n if (isParentLabel) {\n return false;\n }\n }\n \n // Now check if handlers differ (only if special cases don't apply)\n if (parentElementInfo) {\n const childHandlers = getElementHandlersWithBubbling(elementInfo.element);\n const parentHandlers = getElementHandlersWithBubbling(parentElementInfo.element);\n \n // If handlers differ, keep child (don't unify)\n if (compareHandlersForUniquify(childHandlers, parentHandlers)) {\n return true;\n }\n }\n \n return isFormControl(elementInfo) || isDropdownItem(elementInfo) || isTableCell(elementInfo);\n }\n\n function isTableCell(elementInfo) {\n const element = elementInfo.element;\n if(!element || !(element instanceof HTMLElement)) {\n return false;\n }\n const validTags = new Set(['td', 'th']);\n const validRoles = new Set(['cell', 'gridcell', 'columnheader', 'rowheader']);\n \n const tag = element.tagName.toLowerCase();\n const role = element.getAttribute('role')?.toLowerCase();\n\n if (validTags.has(tag) || (role && validRoles.has(role))) {\n return true;\n }\n return false;\n \n }\n\n function isOverlaid(elementInfo) {\n const element = elementInfo.element;\n const boundingRect = elementInfo.bounding_box;\n \n\n \n \n // Create a diagnostic logging function that only logs when needed\n const diagnosticLog = (...args) => {\n { // set to true for debugging\n highlighterLogger.debug('[OVERLAY-DEBUG]', ...args);\n }\n };\n\n // Special handling for tooltips\n if (elementInfo.element.className && typeof elementInfo.element.className === 'string' && \n elementInfo.element.className.includes('tooltip')) {\n diagnosticLog('Element is a tooltip, not considering it overlaid');\n return false;\n }\n \n \n \n // Get element at the center point to check if it's covered by a popup/modal\n const middleX = boundingRect.x + boundingRect.width/2;\n const middleY = boundingRect.y + boundingRect.height/2;\n const elementAtMiddle = element.ownerDocument.elementFromPoint(middleX, middleY);\n \n if (elementAtMiddle && \n elementAtMiddle !== element && \n !isDecendent(element, elementAtMiddle) && \n !isDecendent(elementAtMiddle, element)) {\n\n \n return true;\n }\n \n \n return false;\n \n }\n\n /**\n * Checks if an element is scrollable (has scrollable content)\n * \n * @param element - The element to check\n * @returns boolean indicating if the element is scrollable\n */\n /* export function isScrollableContainer(element) {\n if (!element) return false;\n \n const style = window.getComputedStyle(element);\n \n // Reliable way to detect if an element has scrollbars or is scrollable\n const hasScrollHeight = element.scrollHeight > element.clientHeight;\n const hasScrollWidth = element.scrollWidth > element.clientWidth;\n \n // Check actual style properties\n const hasOverflowY = style.overflowY === 'auto' || \n style.overflowY === 'scroll' || \n style.overflowY === 'overlay';\n const hasOverflowX = style.overflowX === 'auto' || \n style.overflowX === 'scroll' || \n style.overflowX === 'overlay';\n \n // Check common class names and attributes for scrollable containers across frameworks\n const hasScrollClasses = element.classList.contains('scroll') || \n element.classList.contains('scrollable') ||\n element.classList.contains('overflow') ||\n element.classList.contains('overflow-auto') ||\n element.classList.contains('overflow-scroll') ||\n element.getAttribute('data-scrollable') === 'true';\n \n // Check for height/max-height constraints that often indicate scrolling content\n const hasHeightConstraint = style.maxHeight && \n style.maxHeight !== 'none' && \n style.maxHeight !== 'auto';\n \n // An element is scrollable if it has:\n // 1. Actual scrollbars in use (most reliable check) OR\n // 2. Overflow styles allowing scrolling AND content that would require scrolling\n return (hasScrollHeight && hasOverflowY) || \n (hasScrollWidth && hasOverflowX) ||\n (hasScrollClasses && (hasScrollHeight || hasScrollWidth)) ||\n (hasHeightConstraint && hasScrollHeight);\n } */\n\n function isScrollableContainer(el) {\n if (!el || el === document.body) return false;\n\n const style = getComputedStyle(el);\n\n const canScroll = (overflowProp) => {\n return overflowProp === 'auto' ||\n overflowProp === 'scroll' ||\n overflowProp === 'overlay';\n };\n\n //check if the element can scroll on either axis\n if (!canScroll(style.overflowY) && !canScroll(style.overflowX)) return false;\n return el.scrollHeight - el.clientHeight > 1 || el.scrollWidth - el.clientWidth > 1;\n }\n /**\n * Detects scrollable containers that are ancestors of the target element\n * \n * This function traverses up the DOM tree from the target element and identifies\n * all scrollable containers (elements that have scrollable content).\n * \n * @param target - The target element to start the search from\n * @returns Array of objects with selector and scroll properties\n */\n function detectScrollableContainers(target) {\n const scrollableContainers = [];\n \n if (!target) {\n return scrollableContainers;\n }\n \n highlighterLogger.debug('🔍 [detectScrollableContainers] Starting detection for target:', target.tagName, target.id, target.className);\n \n // Detect if target is inside an iframe\n const iframe = getContainingIFrame(target);\n const iframe_selector = iframe ? generateCssPath(iframe) : \"\";\n \n highlighterLogger.debug('🔍 [detectScrollableContainers] Iframe context:', iframe ? 'inside iframe' : 'main document', 'selector:', iframe_selector);\n \n // Start from the target element and traverse up the DOM tree\n let currentElement = target;\n let depth = 0;\n const MAX_DEPTH = 15; // Limit traversal depth to avoid infinite loops\n \n while (currentElement && currentElement.nodeType === Node.ELEMENT_NODE && depth < MAX_DEPTH) { \n // Check if the current element is scrollable\n if (isScrollableContainer(currentElement)) {\n highlighterLogger.debug('🔍 [detectScrollableContainers] Found scrollable container at depth', depth, ':', currentElement.tagName, currentElement.id, currentElement.className);\n \n const container = {\n containerEl: currentElement,\n selector: generateCssPath(currentElement),\n iframe_selector: iframe_selector,\n scrollTop: currentElement.scrollTop,\n scrollLeft: currentElement.scrollLeft,\n scrollHeight: currentElement.scrollHeight,\n scrollWidth: currentElement.scrollWidth,\n clientHeight: currentElement.clientHeight,\n clientWidth: currentElement.clientWidth\n };\n \n scrollableContainers.push(container);\n }\n \n // Move to parent element\n currentElement = getParentNode(currentElement);\n \n depth++;\n }\n \n highlighterLogger.debug('🔍 [detectScrollableContainers] Detection complete. Found', scrollableContainers.length, 'scrollable containers');\n return scrollableContainers;\n }\n\n class DOMSerializer {\n constructor(options = {}) {\n this.options = {\n includeStyles: true,\n includeScripts: false, // Security consideration\n includeFrames: true,\n includeShadowDOM: true,\n maxDepth: 50,\n ...options\n };\n this.serializedFrames = new Map();\n this.shadowRoots = new Map();\n }\n \n /**\n * Serialize a complete document or element\n */\n serialize(rootElement = document) {\n try {\n const serialized = {\n type: 'document',\n doctype: this.serializeDoctype(rootElement),\n documentElement: this.serializeElement(rootElement.documentElement || rootElement),\n frames: [],\n timestamp: Date.now(),\n url: rootElement.URL || window.location?.href,\n metadata: {\n title: rootElement.title,\n charset: rootElement.characterSet,\n contentType: rootElement.contentType\n }\n };\n \n // Serialize frames and iframes if enabled\n if (this.options.includeFrames) {\n serialized.frames = this.serializeFrames(rootElement);\n }\n \n return serialized;\n } catch (error) {\n console.error('Serialization error:', error);\n throw new Error(`DOM serialization failed: ${error.message}`);\n }\n }\n \n /**\n * Serialize document type declaration\n */\n serializeDoctype(doc) {\n if (!doc.doctype) return null;\n \n return {\n name: doc.doctype.name,\n publicId: doc.doctype.publicId,\n systemId: doc.doctype.systemId\n };\n }\n \n /**\n * Serialize an individual element and its children\n */\n serializeElement(element, depth = 0) {\n if (depth > this.options.maxDepth) {\n return { type: 'text', content: '<!-- Max depth exceeded -->' };\n }\n \n const nodeType = element.nodeType;\n \n switch (nodeType) {\n case Node.ELEMENT_NODE:\n return this.serializeElementNode(element, depth);\n case Node.TEXT_NODE:\n return this.serializeTextNode(element);\n case Node.COMMENT_NODE:\n return this.serializeCommentNode(element);\n case Node.DOCUMENT_FRAGMENT_NODE:\n return this.serializeDocumentFragment(element, depth);\n default:\n return null;\n }\n }\n \n /**\n * Serialize element node with attributes and children\n */\n serializeElementNode(element, depth) {\n const tagName = element.tagName.toLowerCase();\n \n // Skip script tags for security unless explicitly enabled\n if (tagName === 'script' && !this.options.includeScripts) {\n return { type: 'comment', content: '<!-- Script tag removed for security -->' };\n }\n \n const serialized = {\n type: 'element',\n tagName: tagName,\n attributes: this.serializeAttributes(element),\n children: [],\n shadowRoot: null\n };\n \n // Handle Shadow DOM\n if (this.options.includeShadowDOM && element.shadowRoot) {\n serialized.shadowRoot = this.serializeShadowRoot(element.shadowRoot, depth + 1);\n }\n \n // Handle special elements\n if (tagName === 'iframe' || tagName === 'frame') {\n serialized.frameData = this.serializeFrameElement(element);\n }\n \n // Serialize children\n for (const child of element.childNodes) {\n const serializedChild = this.serializeElement(child, depth + 1);\n if (serializedChild) {\n serialized.children.push(serializedChild);\n }\n }\n \n // Include computed styles if enabled\n if (this.options.includeStyles && element.nodeType === Node.ELEMENT_NODE) {\n serialized.computedStyle = this.serializeComputedStyle(element);\n }\n \n return serialized;\n }\n \n /**\n * Serialize element attributes\n */\n serializeAttributes(element) {\n const attributes = {};\n \n if (element.attributes) {\n for (const attr of element.attributes) {\n attributes[attr.name] = attr.value;\n }\n }\n \n return attributes;\n }\n \n /**\n * Serialize computed styles\n */\n serializeComputedStyle(element) {\n try {\n const computedStyle = window.getComputedStyle(element);\n const styles = {};\n \n // Only serialize non-default values to reduce size\n const importantStyles = [\n 'display', 'position', 'width', 'height', 'margin', 'padding',\n 'border', 'background', 'color', 'font-family', 'font-size',\n 'text-align', 'visibility', 'z-index', 'transform'\n ];\n \n for (const prop of importantStyles) {\n const value = computedStyle.getPropertyValue(prop);\n if (value && value !== 'initial' && value !== 'normal') {\n styles[prop] = value;\n }\n }\n \n return styles;\n } catch (error) {\n return {};\n }\n }\n \n /**\n * Serialize text node\n */\n serializeTextNode(node) {\n return {\n type: 'text',\n content: node.textContent\n };\n }\n \n /**\n * Serialize comment node\n */\n serializeCommentNode(node) {\n return {\n type: 'comment',\n content: node.textContent\n };\n }\n \n /**\n * Serialize document fragment\n */\n serializeDocumentFragment(fragment, depth) {\n const serialized = {\n type: 'fragment',\n children: []\n };\n \n for (const child of fragment.childNodes) {\n const serializedChild = this.serializeElement(child, depth + 1);\n if (serializedChild) {\n serialized.children.push(serializedChild);\n }\n }\n \n return serialized;\n }\n \n /**\n * Serialize Shadow DOM\n */\n serializeShadowRoot(shadowRoot, depth) {\n const serialized = {\n type: 'shadowRoot',\n mode: shadowRoot.mode,\n children: []\n };\n \n for (const child of shadowRoot.childNodes) {\n const serializedChild = this.serializeElement(child, depth + 1);\n if (serializedChild) {\n serialized.children.push(serializedChild);\n }\n }\n \n return serialized;\n }\n \n /**\n * Serialize frame/iframe elements\n */\n serializeFrameElement(frameElement) {\n const frameData = {\n src: frameElement.src,\n name: frameElement.name,\n id: frameElement.id,\n sandbox: frameElement.sandbox?.toString() || '',\n allowfullscreen: frameElement.allowFullscreen\n };\n \n // Try to access frame content (may fail due to CORS)\n try {\n const frameDoc = frameElement.contentDocument;\n if (frameDoc && this.options.includeFrames) {\n frameData.content = this.serialize(frameDoc);\n }\n } catch (error) {\n frameData.accessError = 'Cross-origin frame content not accessible';\n }\n \n return frameData;\n }\n \n /**\n * Serialize all frames in document\n */\n serializeFrames(doc) {\n const frames = [];\n const frameElements = doc.querySelectorAll('iframe, frame');\n \n for (const frameElement of frameElements) {\n try {\n const frameDoc = frameElement.contentDocument;\n if (frameDoc) {\n frames.push({\n element: this.serializeElement(frameElement),\n content: this.serialize(frameDoc)\n });\n }\n } catch (error) {\n frames.push({\n element: this.serializeElement(frameElement),\n error: 'Frame content not accessible'\n });\n }\n }\n \n return frames;\n }\n \n /**\n * Deserialize serialized DOM data back to DOM nodes\n */\n deserialize(serializedData, targetDocument = document) {\n try {\n if (serializedData.type === 'document') {\n return this.deserializeDocument(serializedData, targetDocument);\n } else {\n return this.deserializeElement(serializedData, targetDocument);\n }\n } catch (error) {\n console.error('Deserialization error:', error);\n throw new Error(`DOM deserialization failed: ${error.message}`);\n }\n }\n \n /**\n * Deserialize complete document\n */\n deserializeDocument(serializedDoc, targetDoc) {\n // Create new document if needed\n const doc = targetDoc || document.implementation.createHTMLDocument();\n \n // Set doctype if present\n if (serializedDoc.doctype) {\n const doctype = document.implementation.createDocumentType(\n serializedDoc.doctype.name,\n serializedDoc.doctype.publicId,\n serializedDoc.doctype.systemId\n );\n doc.replaceChild(doctype, doc.doctype);\n }\n \n // Deserialize document element\n if (serializedDoc.documentElement) {\n const newDocElement = this.deserializeElement(serializedDoc.documentElement, doc);\n doc.replaceChild(newDocElement, doc.documentElement);\n }\n \n // Handle metadata\n if (serializedDoc.metadata) {\n doc.title = serializedDoc.metadata.title || '';\n }\n \n return doc;\n }\n \n /**\n * Deserialize individual element\n */\n deserializeElement(serializedNode, doc) {\n switch (serializedNode.type) {\n case 'element':\n return this.deserializeElementNode(serializedNode, doc);\n case 'text':\n return doc.createTextNode(serializedNode.content);\n case 'comment':\n return doc.createComment(serializedNode.content);\n case 'fragment':\n return this.deserializeDocumentFragment(serializedNode, doc);\n case 'shadowRoot':\n // Shadow roots are handled during element creation\n return null;\n default:\n return null;\n }\n }\n \n /**\n * Deserialize element node\n */\n deserializeElementNode(serializedElement, doc) {\n const element = doc.createElement(serializedElement.tagName);\n \n // Set attributes\n if (serializedElement.attributes) {\n for (const [name, value] of Object.entries(serializedElement.attributes)) {\n try {\n element.setAttribute(name, value);\n } catch (error) {\n console.warn(`Failed to set attribute ${name}:`, error);\n }\n }\n }\n \n // Apply computed styles if available\n if (serializedElement.computedStyle && this.options.includeStyles) {\n for (const [prop, value] of Object.entries(serializedElement.computedStyle)) {\n try {\n element.style.setProperty(prop, value);\n } catch (error) {\n console.warn(`Failed to set style ${prop}:`, error);\n }\n }\n }\n \n // Create shadow root if present\n if (serializedElement.shadowRoot && element.attachShadow) {\n try {\n const shadowRoot = element.attachShadow({ \n mode: serializedElement.shadowRoot.mode || 'open' \n });\n \n // Deserialize shadow root children\n for (const child of serializedElement.shadowRoot.children) {\n const childElement = this.deserializeElement(child, doc);\n if (childElement) {\n shadowRoot.appendChild(childElement);\n }\n }\n } catch (error) {\n console.warn('Failed to create shadow root:', error);\n }\n }\n \n // Deserialize children\n if (serializedElement.children) {\n for (const child of serializedElement.children) {\n const childElement = this.deserializeElement(child, doc);\n if (childElement) {\n element.appendChild(childElement);\n }\n }\n }\n \n // Handle frame content\n if (serializedElement.frameData && serializedElement.frameData.content) {\n // Frame content deserialization would happen after the frame loads\n element.addEventListener('load', () => {\n try {\n const frameDoc = element.contentDocument;\n if (frameDoc) {\n this.deserializeDocument(serializedElement.frameData.content, frameDoc);\n }\n } catch (error) {\n console.warn('Failed to deserialize frame content:', error);\n }\n });\n }\n \n return element;\n }\n \n /**\n * Deserialize document fragment\n */\n deserializeDocumentFragment(serializedFragment, doc) {\n const fragment = doc.createDocumentFragment();\n \n if (serializedFragment.children) {\n for (const child of serializedFragment.children) {\n const childElement = this.deserializeElement(child, doc);\n if (childElement) {\n fragment.appendChild(childElement);\n }\n }\n }\n \n return fragment;\n }\n }\n \n // Usage example and utility functions\n class DOMUtils {\n /**\n * Create serializer with common presets\n */\n static createSerializer(preset = 'default') {\n const presets = {\n default: {\n includeStyles: true,\n includeScripts: false,\n includeFrames: true,\n includeShadowDOM: true\n },\n minimal: {\n includeStyles: false,\n includeScripts: false,\n includeFrames: false,\n includeShadowDOM: false\n },\n complete: {\n includeStyles: true,\n includeScripts: true,\n includeFrames: true,\n includeShadowDOM: true\n },\n secure: {\n includeStyles: true,\n includeScripts: false,\n includeFrames: false,\n includeShadowDOM: true\n }\n };\n \n return new DOMSerializer(presets[preset] || presets.default);\n }\n \n /**\n * Serialize DOM to JSON string\n */\n static serializeToJSON(element, options) {\n const serializer = new DOMSerializer(options);\n const serialized = serializer.serialize(element);\n return JSON.stringify(serialized, null, 2);\n }\n \n /**\n * Deserialize from JSON string\n */\n static deserializeFromJSON(jsonString, targetDocument) {\n const serialized = JSON.parse(jsonString);\n const serializer = new DOMSerializer();\n return serializer.deserialize(serialized, targetDocument);\n }\n \n /**\n * Clone DOM with full fidelity including Shadow DOM\n */\n static deepClone(element, options) {\n const serializer = new DOMSerializer(options);\n const serialized = serializer.serialize(element);\n return serializer.deserialize(serialized, element.ownerDocument);\n }\n \n /**\n * Compare two DOM structures\n */\n static compare(element1, element2, options) {\n const serializer = new DOMSerializer(options);\n const serialized1 = serializer.serialize(element1);\n const serialized2 = serializer.serialize(element2);\n \n return JSON.stringify(serialized1) === JSON.stringify(serialized2);\n }\n }\n \n /*\n // Export for use\n if (typeof module !== 'undefined' && module.exports) {\n module.exports = { DOMSerializer, DOMUtils };\n } else if (typeof window !== 'undefined') {\n window.DOMSerializer = DOMSerializer;\n window.DOMUtils = DOMUtils;\n }\n */\n\n /* Usage Examples:\n \n // Basic serialization\n const serializer = new DOMSerializer();\n const serialized = serializer.serialize(document);\n console.log(JSON.stringify(serialized, null, 2));\n \n // Deserialize back to DOM\n const clonedDoc = serializer.deserialize(serialized);\n \n // Using presets\n const minimalSerializer = DOMUtils.createSerializer('minimal');\n const secureSerializer = DOMUtils.createSerializer('secure');\n \n // Serialize specific element with Shadow DOM\n const customElement = document.querySelector('my-custom-element');\n const serializedElement = serializer.serialize(customElement);\n \n // JSON utilities\n const jsonString = DOMUtils.serializeToJSON(document.body);\n const restored = DOMUtils.deserializeFromJSON(jsonString);\n \n // Deep clone with Shadow DOM support\n const clone = DOMUtils.deepClone(document.body, { includeShadowDOM: true });\n \n */\n\n function serializeNodeToJSON(nodeElement) {\n return DOMUtils.serializeToJSON(nodeElement, {includeStyles: false});\n }\n\n function deserializeNodeFromJSON(jsonString) {\n return DOMUtils.deserializeFromJSON(jsonString);\n }\n\n function findDropdowns() {\n const dropdowns = [];\n \n // Native select elements\n dropdowns.push(...getAllElementsIncludingShadow('select'));\n \n // Elements with dropdown roles that don't have <input>..</input>\n const roleElements = getAllElementsIncludingShadow('[role=\"combobox\"], [role=\"listbox\"], [role=\"dropdown\"], [role=\"option\"], [role=\"menu\"], [role=\"menuitem\"]').filter(el => {\n return el.tagName.toLowerCase() !== 'input' || ![\"button\", \"checkbox\", \"radio\"].includes(el.getAttribute(\"type\"));\n });\n dropdowns.push(...roleElements);\n \n // Common dropdown class patterns\n const dropdownPattern = /.*(dropdown|select|combobox|menu).*/i;\n const elements = getAllElementsIncludingShadow('*');\n const dropdownClasses = Array.from(elements).filter(el => {\n const hasDropdownClass = dropdownPattern.test(el.className);\n const validTag = ['li', 'ul', 'span', 'div', 'p', 'a', 'button'].includes(el.tagName.toLowerCase());\n const style = window.getComputedStyle(el); \n const result = hasDropdownClass && validTag && (style.cursor === 'pointer' || el.tagName.toLowerCase() === 'a' || el.tagName.toLowerCase() === 'button');\n return result;\n });\n \n dropdowns.push(...dropdownClasses);\n \n // Elements with aria-haspopup attribute\n dropdowns.push(...getAllElementsIncludingShadow('[aria-haspopup=\"true\"], [aria-haspopup=\"listbox\"], [aria-haspopup=\"menu\"]'));\n\n // Improve navigation element detection\n // Semantic nav elements with list items\n dropdowns.push(...getAllElementsIncludingShadow('nav ul li, nav ol li'));\n \n // Navigation elements in common design patterns\n dropdowns.push(...getAllElementsIncludingShadow('header a, .header a, .nav a, .navigation a, .menu a, .sidebar a, aside a'));\n \n // Elements in primary navigation areas with common attributes\n dropdowns.push(...getAllElementsIncludingShadow('[role=\"navigation\"] a, [aria-label*=\"navigation\"] a, [aria-label*=\"menu\"] a'));\n\n return dropdowns;\n }\n\n function findClickables() {\n const clickables = [];\n \n const checkboxPattern = /checkbox/i;\n // Collect all clickable elements first\n const nativeLinks = [...getAllElementsIncludingShadow('a')];\n const nativeButtons = [...getAllElementsIncludingShadow('button')];\n const inputButtons = [...getAllElementsIncludingShadow('input[type=\"button\"], input[type=\"submit\"], input[type=\"reset\"]')];\n const roleButtons = [...getAllElementsIncludingShadow('[role=\"button\"]')];\n // const tabbable = [...getAllElementsIncludingShadow('[tabindex=\"0\"]')];\n const clickHandlers = [...getAllElementsIncludingShadow('[onclick]')];\n const dropdowns = findDropdowns();\n const nativeCheckboxes = [...getAllElementsIncludingShadow('input[type=\"checkbox\"]')]; \n const fauxCheckboxes = getAllElementsIncludingShadow('*').filter(el => {\n if (checkboxPattern.test(el.className)) {\n const realCheckboxes = getAllElementsIncludingShadow('input[type=\"checkbox\"]', el);\n if (realCheckboxes.length === 1) {\n const boundingRect = realCheckboxes[0].getBoundingClientRect();\n return boundingRect.width <= 1 && boundingRect.height <= 1 \n }\n }\n return false;\n });\n const nativeRadios = [...getAllElementsIncludingShadow('input[type=\"radio\"]')];\n const toggles = findToggles();\n const pointerElements = findElementsWithPointer();\n // Add all elements at once\n clickables.push(\n ...nativeLinks,\n ...nativeButtons,\n ...inputButtons,\n ...roleButtons,\n // ...tabbable,\n ...clickHandlers,\n ...dropdowns,\n ...nativeCheckboxes,\n ...fauxCheckboxes,\n ...nativeRadios,\n ...toggles,\n ...pointerElements\n );\n\n // Only uniquify once at the end\n return clickables; // Let findElements handle the uniquification\n }\n\n function findToggles() {\n const toggles = [];\n const checkboxes = getAllElementsIncludingShadow('input[type=\"checkbox\"]');\n const togglePattern = /switch|toggle|slider/i;\n\n checkboxes.forEach(checkbox => {\n let isToggle = false;\n\n // Check the checkbox itself\n if (togglePattern.test(checkbox.className) || togglePattern.test(checkbox.getAttribute('role') || '')) {\n isToggle = true;\n }\n\n // Check parent elements (up to 3 levels)\n if (!isToggle) {\n let element = checkbox;\n for (let i = 0; i < 3; i++) {\n const parent = element.parentElement;\n if (!parent) break;\n\n const className = parent.className || '';\n const role = parent.getAttribute('role') || '';\n\n if (togglePattern.test(className) || togglePattern.test(role)) {\n isToggle = true;\n break;\n }\n element = parent;\n }\n }\n\n // Check next sibling\n if (!isToggle) {\n const nextSibling = checkbox.nextElementSibling;\n if (nextSibling) {\n const className = nextSibling.className || '';\n const role = nextSibling.getAttribute('role') || '';\n if (togglePattern.test(className) || togglePattern.test(role)) {\n isToggle = true;\n }\n }\n }\n\n if (isToggle) {\n toggles.push(checkbox);\n }\n });\n\n return toggles;\n }\n\n function findNonInteractiveElements() {\n // Get all elements in the document\n const all = Array.from(getAllElementsIncludingShadow('*'));\n \n // Filter elements based on Python implementation rules\n return all.filter(element => {\n if (!element.firstElementChild) {\n const tag = element.tagName.toLowerCase(); \n if (!['select', 'button', 'a'].includes(tag)) {\n const validTags = ['p', 'span', 'div', 'input', 'textarea','td','th'].includes(tag) || /^h\\d$/.test(tag) || /text/.test(tag);\n const boundingRect = element.getBoundingClientRect();\n return validTags && boundingRect.height > 1 && boundingRect.width > 1;\n }\n }\n return false;\n });\n }\n\n\n\n function findElementsWithPointer() {\n const elements = [];\n const allElements = getAllElementsIncludingShadow('*');\n \n allElements.forEach(element => {\n const style = window.getComputedStyle(element);\n const hasPointer = style.cursor === 'pointer';\n \n if (hasPointer) {\n elements.push(element);\n }\n });\n \n highlighterLogger.debug(`Found ${elements.length} elements with pointer cursor`);\n return elements;\n }\n\n function findCheckables() {\n const elements = [];\n\n elements.push(...getAllElementsIncludingShadow('input[type=\"checkbox\"]'));\n elements.push(...getAllElementsIncludingShadow('input[type=\"radio\"]'));\n const all_elements = getAllElementsIncludingShadow('label');\n const radioClasses = Array.from(all_elements).filter(el => {\n return /.*radio.*/i.test(el.className); \n });\n elements.push(...radioClasses);\n return elements;\n }\n\n function findFillables() {\n const elements = [];\n\n const inputs = [...getAllElementsIncludingShadow('input:not([type=\"radio\"]):not([type=\"checkbox\"])')];\n highlighterLogger.debug('Found inputs:', inputs.length, inputs);\n elements.push(...inputs);\n \n const textareas = [...getAllElementsIncludingShadow('textarea')];\n highlighterLogger.debug('Found textareas:', textareas.length);\n elements.push(...textareas);\n \n const editables = [...getAllElementsIncludingShadow('[contenteditable=\"true\"]')];\n highlighterLogger.debug('Found editables:', editables.length);\n elements.push(...editables);\n\n return elements;\n }\n\n // maximum number of characters in a logical name\n const MAX_LOGICAL_NAME_LENGTH = 40;\n // minimum number of characters in text content to be considered a text element\n const MIN_TEXT_LENGTH = 3;\n /**\n * Query elements by CSS selector with :has-text(\"...\") pseudo-selector\n * @param {string} selector - CSS selector with optional :has-text(\"...\") pseudo-selector\n * @param {Element|Document} context - Context element to query within (defaults to document)\n * @returns {Element[]} Array of matched elements\n * @example\n * // Find all buttons with text \"Submit\"\n * querySelectorAllByText('button:has-text(\"Submit\")')\n * \n * // Find element with specific role and text\n * querySelectorAllByText('[role=\"button\"]:has-text(\"Click me\")')\n * \n * // Complex selector with descendants\n * querySelectorAllByText('button:has-text(\"foo\") > div > ul')\n * \n * // Multiple components\n * querySelectorAllByText('.container > button:has-text(\"Submit\")')\n * \n * // Multiple text filters\n * querySelectorAllByText('button:has-text(\"foo\") > div:has-text(\"bar\") > span')\n */\n function querySelectorAllByText(selector, context = document) {\n \t// Local helper function to compare two strings by normalizing whitespace and ignoring case\n \t// mimic the behavior of Playwright's :has-text() selector modifier\n \tfunction pwHasText(str1, str2) {\n \t\tif (!str1 || !str2) return false;\n \t\t\n \t\t// Normalize whitespace by replacing multiple spaces with single space and trim\n \t\tconst normalize = (str) => cleanText(str.toLowerCase());\n \t\t\n \t\tconst normalized1 = normalize(str1);\n \t\tconst normalized2 = normalize(str2);\n \t\t\n \t\treturn normalized1.includes(normalized2);\n \t}\n \t\n \t// Check if selector contains :has-text() pseudo-selector\n \tif (!selector.includes(':has-text(')) {\n \t\t// No :has-text pseudo-selector, use regular querySelectorAll\n \t\treturn Array.from(robustQuerySelector(selector, context, true));\n \t}\n \t\n \t// Split selector by combinators while preserving them\n \t// Matches: >, +, ~, or space (descendant combinator)\n \tconst parts = [];\n \tlet currentPart = '';\n \tlet inBracket = false;\n \tlet inTextPseudo = false;\n \tlet inTextQuotes = false;\n \tlet i = 0;\n \t\n \twhile (i < selector.length) {\n \t\tconst char = selector[i];\n \t\t\n \t\t// Track :has-text(\"...\") to avoid splitting inside it\n \t\tif (selector.substring(i, i + 10) === ':has-text(') {\n \t\t\tinTextPseudo = true;\n \t\t\tinTextQuotes = false;\n \t\t}\n \t\tif (inTextPseudo && char === '\"' && !inTextQuotes) {\n \t\t\tinTextQuotes = true;\n \t\t} else if (inTextPseudo && char === '\"' && inTextQuotes) {\n \t\t\t// Check if this quote is escaped\n \t\t\tif (i > 0 && selector[i - 1] === '\\\\') ; else {\n \t\t\t\tinTextQuotes = false;\n \t\t\t}\n \t\t}\n \t\tif (inTextPseudo && char === ')' && !inTextQuotes) {\n \t\t\tinTextPseudo = false;\n \t\t\tcurrentPart += char;\n \t\t\ti++;\n \t\t\tcontinue;\n \t\t}\n \t\t\n \t\t// Track brackets to avoid splitting on combinators inside attribute selectors\n \t\tif (char === '[') inBracket = true;\n \t\tif (char === ']') inBracket = false;\n \t\t\n \t\t// Check for combinators (not inside brackets or :has-text())\n \t\tif (!inBracket && !inTextPseudo && (char === '>' || char === '+' || char === '~')) {\n \t\t\tif (currentPart.trim()) parts.push({ selector: currentPart.trim(), combinator: null });\n \t\t\tparts.push({ selector: null, combinator: char.trim() });\n \t\t\tcurrentPart = '';\n \t\t\ti++;\n \t\t\tcontinue;\n \t\t}\n \t\t\n \t\t// Check for space combinator (descendant)\n \t\tif (!inBracket && !inTextPseudo && char === ' ') {\n \t\t\t// Skip multiple spaces\n \t\t\twhile (i < selector.length && selector[i] === ' ') i++;\n \t\t\t// Check if next char is a combinator (>, +, ~)\n \t\t\tif (i < selector.length && !'><+~'.includes(selector[i])) {\n \t\t\t\tif (currentPart.trim()) {\n \t\t\t\t\tparts.push({ selector: currentPart.trim(), combinator: null });\n \t\t\t\t\tparts.push({ selector: null, combinator: ' ' });\n \t\t\t\t\tcurrentPart = '';\n \t\t\t\t}\n \t\t\t}\n \t\t\tcontinue;\n \t\t}\n \t\t\n \t\tcurrentPart += char;\n \t\ti++;\n \t}\n \tif (currentPart.trim()) parts.push({ selector: currentPart.trim(), combinator: null });\n \t\n \t// Filter out combinator-only entries and rebuild as selector+combinator pairs\n \tconst selectorParts = [];\n \tfor (let j = 0; j < parts.length; j++) {\n \t\tif (parts[j].selector) {\n \t\t\tconst combinator = (j + 1 < parts.length && parts[j + 1].combinator) ? parts[j + 1].combinator : null;\n \t\t\t// Extract text value if present\n \t\t\tconst textMatch = parts[j].selector.match(/:has-text\\(\"((?:[^\"\\\\]|\\\\.)*)\"\\)/);\n \t\t\tconst textValue = textMatch ? textMatch[1] : null;\n \t\t\tconst selectorWithoutText = textMatch ? parts[j].selector.replace(/:has-text\\(\"((?:[^\"\\\\]|\\\\.)*)\"\\)/, '') : parts[j].selector;\n \t\t\t\n \t\t\tselectorParts.push({ \n \t\t\t\tselector: parts[j].selector,\n \t\t\t\tselectorWithoutText,\n \t\t\t\ttextValue,\n \t\t\t\tcombinator \n \t\t\t});\n \t\t}\n \t}\n \t\n \t// Process selector parts sequentially, applying text filters as we go\n \tlet currentElements = [context];\n \t\n \tfor (let partIndex = 0; partIndex < selectorParts.length; partIndex++) {\n \t\tconst part = selectorParts[partIndex];\n \t\tconst nextElements = [];\n \t\t\n \t\tfor (const ctx of currentElements) {\n \t\t\tlet candidates;\n \t\t\t\n \t\t\tif (partIndex === 0 && ctx === context) {\n \t\t\t\t// First part, query from document/context\n \t\t\t\tconst querySelector = part.selectorWithoutText.trim() || '*';\n \t\t\t\tcandidates = Array.from(robustQuerySelector(querySelector, ctx, true));\n \t\t\t\thighlighterLogger.debug(`querySelectorAllByText: first part, querySelector=${querySelector}, candidates=`, candidates);\n \t\t\t} \n \t\t\telse {\n \t\t\t\t// Subsequent parts, use combinator\n \t\t\t\tconst prevPart = selectorParts[partIndex - 1];\n \t\t\t\tconst combinator = prevPart.combinator;\n \t\t\t\tconst querySelector = part.selectorWithoutText.trim() || '*';\n \t\t\t\t\n \t\t\t\tif (combinator === '>') {\n \t\t\t\t\t// Direct children\n \t\t\t\t\tcandidates = Array.from(ctx.children).filter(child => {\n \t\t\t\t\t\treturn child.matches(querySelector);\n \t\t\t\t\t});\n \t\t\t\t} else if (combinator === '+') {\n \t\t\t\t\t// Next sibling\n \t\t\t\t\tconst sibling = ctx.nextElementSibling;\n \t\t\t\t\tcandidates = (sibling && sibling.matches(querySelector)) ? [sibling] : [];\n \t\t\t\t} else if (combinator === '~') {\n \t\t\t\t\t// Following siblings\n \t\t\t\t\tcandidates = [];\n \t\t\t\t\tlet sibling = ctx.nextElementSibling;\n \t\t\t\t\twhile (sibling) {\n \t\t\t\t\t\tif (sibling.matches(querySelector)) {\n \t\t\t\t\t\t\tcandidates.push(sibling);\n \t\t\t\t\t\t}\n \t\t\t\t\t\tsibling = sibling.nextElementSibling;\n \t\t\t\t\t}\n \t\t\t\t} \n \t\t\t\telse {\n \t\t\t\t\t// Descendant (space)\n \t\t\t\t\tcandidates = Array.from(robustQuerySelector(querySelector, ctx, true));\n \t\t\t\t}\n \t\t\t}\n \t\t\t\n \t\t\t// Apply text filter if present\n \t\t\tif (part.textValue !== null) {\n \t\t\t\tcandidates = candidates.filter(el => pwHasText(getElementText(el), part.textValue));\n \t\t\t}\n \t\t\t\n \t\t\tnextElements.push(...candidates);\n \t\t}\n \t\t\n \t\tcurrentElements = nextElements;\n \t\t\n \t\t// If no matches, bail early\n \t\tif (currentElements.length === 0) {\n \t\t\treturn [];\n \t\t}\n \t}\n \t\n \treturn currentElements;\n }\n\n const cleanText = (text) => {\n \treturn text.replace(/\\s+/g, ' ').trim();\n };\n\n // Helper function to get name properties (logicalName and selector)\n // Priority: text (if clickable and not input) -> name -> title -> aria-label\n const getNameProps = (element) => {\n \tconst isClickable = isClickableElement(element);\n \tconst text = cleanText(getElementText(element));\n \tconst name = element.getAttribute('name');\n \tconst title = element.getAttribute('title');\n \tconst ariaLabel = element.getAttribute('aria-label');\n \tconst placeholder = element.getAttribute('placeholder');\n \tconst role = element.getAttribute('role');\n \t\n \tlet logicalName = '';\n \tlet selector = '';\n \tlet useText = false;\n \t\n \t// if the role starts with 'row', return empty logicalName and selector\n \tif (role?.startsWith('row')) {\n \t\treturn {\n \t\t\tlogicalName: '',\n \t\t\tselector: '',\n \t\t\tuseText: false\n \t\t};\n \t}\n \t// Priority: text (if clickable and not input) -> name -> title -> aria-label\n \tif (text && text.length >= MIN_TEXT_LENGTH && text.length <= MAX_LOGICAL_NAME_LENGTH && isClickable && !isFillableElement(element)) {\n \t\tlogicalName = text;\n \t\tselector = `:has-text(${doubleQuoteString(text)})`;\n \t\tuseText = true;\n \t} else if (name && name.length <= MAX_LOGICAL_NAME_LENGTH) {\n \t\tlogicalName = name;\n \t\tselector = `[name=${doubleQuoteString(name)}]`;\n \t} else if (title && title.length <= MAX_LOGICAL_NAME_LENGTH) {\n \t\tlogicalName = title;\n \t\tselector = `[title=${doubleQuoteString(title)}]`;\n \t} else if (ariaLabel && ariaLabel.length <= MAX_LOGICAL_NAME_LENGTH) {\n \t\tlogicalName = ariaLabel;\n \t\tselector = `[aria-label=${doubleQuoteString(ariaLabel)}]`;\n \t} else if (placeholder && placeholder.length <= MAX_LOGICAL_NAME_LENGTH) {\n \t\tlogicalName = placeholder;\n \t\tselector = `[placeholder=${doubleQuoteString(placeholder)}]`;\n \t}\n \t\n \treturn {\n \t\tlogicalName,\n \t\tselector,\n \t\tuseText\n \t};\n };\n\n\n /**\n * Individual strategy functions\n */\n\n function tryRoleStrategy(element) { \n \tconst role = element.getAttribute('role');\n \tconst tag = element.tagName.toLowerCase();\n \tconst { logicalName, selector } = getNameProps(element);\n \tif (logicalName) {\n \t\t// Role-based strategy\n \t\tif (role) {\n \t\t\treturn { \n \t\t\t\tstrategy: 'role', \n \t\t\t\tselector: `[role=${doubleQuoteString(role)}]${selector}`\n \t\t\t};\n \t\t}\n \t\n \t\t// Tag-based role (button, a, input, etc.) + name\n \t\tif (['button', 'a', 'input', 'textarea', 'select'].includes(tag)) {\n \t\t\treturn { \n \t\t\t\tstrategy: 'role', \n \t\t\t\tselector: `${tag}${selector}`\n \t\t\t};\n \t\t}\n \t}\n \t\n \treturn null;\n }\n\n function tryLabelStrategy(element) {\n \tconst tag = element.tagName.toLowerCase();\n \tif (['input', 'textarea', 'select'].includes(tag)) {\n \t\tconst id = element.id;\n \t\tif (id) {\n \t\t\tconst labels = robustQuerySelector(`label[for=${doubleQuoteString(id)}]`, document, true);\n \t\t\tif (labels.length === 1) { //make sure id is unique\n \t\t\t\tconst labelText = cleanText(getElementText(labels[0]));\n \t\t\t\tif (labelText) {\n \t\t\t\t\tconst labelSelector = `label:has-text(${doubleQuoteString(labelText)})`;\n \t\t\t\t\ttry {\n \t\t\t\t\t\tconst els = querySelectorAllByText(labelSelector, document);\n \t\t\t\t\t\tif (els.length === 1) {\n \t\t\t\t\t\t\treturn { \n \t\t\t\t\t\t\t\tstrategy: 'label for',\n \t\t\t\t\t\t\t\tlabel_selector: labelSelector\n \t\t\t\t\t\t\t};\n \t\t\t\t\t\t}\n \t\t\t\t\t} catch (error) {\n \t\t\t\t\t\thighlighterLogger.error(`internal error: while querying label selector ${labelSelector} for element ${element.outerHTML}`, error);\n \t\t\t\t\t\tthrow error;\n \t\t\t\t\t}\n \t\t\t\t}\n \t\t\t}\n \t\t}\n \t}\n \treturn null;\n }\n\n function tryAriaLabelledByStrategy(element) {\n \tconst arialabeledBy = element.getAttribute('aria-labelledby');\n \tconst tag = element.tagName.toLowerCase();\n \t\n \tif (arialabeledBy) {\n \t\tconst labels = robustQuerySelector(`#${CSS.escape(arialabeledBy)}`, document, true);\n \t\tif (labels.length === 1) {\n \t\t\tconst labelText = cleanText(getElementText(labels[0]));\n \t\t\tif (labelText) { //verify the label text is unique\n \t\t\t\tconst labelSelector = `${tag}:has-text(${doubleQuoteString(labelText)})`;\n \t\t\t\ttry {\n \t\t\t\t\tconst els = querySelectorAllByText(labelSelector, document);\n \t\t\t\t\tif (els.length === 1) {\n \t\t\t\t\t\treturn { \n \t\t\t\t\t\t\tstrategy: 'label by', \n \t\t\t\t\t\t\tlabel_selector: labelSelector\n \t\t\t\t\t\t};\n \t\t\t\t\t}\n \t\t\t\t} catch (error) {\n \t\t\t\t\thighlighterLogger.error(`internal error: while querying aria-labelledby selector ${labelSelector} for element ${element.outerHTML}`, error);\n \t\t\t\t\tthrow error;\n \t\t\t\t}\n \t\t\t}\n \t\t}\n \t}\n \treturn null;\n }\n\n function tryNameStrategy(element) {\t\n \tlet { logicalName, selector, useText } = getNameProps(element);\n\n \t//if text wrap the selector in brackets since there is no other attribute used\n \tif (useText) {\n \t\tconst tag = element.tagName.toLowerCase();\n \t\tselector = `${tag}${selector}`;\n \t}\n \t\n \tif (logicalName) {\n \t\treturn { \n \t\t\tstrategy: 'name', \n \t\t\tselector: selector\n \t\t};\n \t}\n \treturn null;}\n\n function tryDataTestIdStrategy(element) {\n \tconst dataTestId = element.getAttribute('data-testid');\n \tif (dataTestId) { //check if the data-testid is unique\n \t\tconst matchedElements = robustQuerySelector(`[data-testid=${doubleQuoteString(dataTestId)}]`, document, true);\n \t\tif (matchedElements.length === 1) {\n \t\t\treturn { \n \t\t\t\tstrategy: 'data-testid',\t\t\t\n \t\t\t\tselector: `[data-testid=${doubleQuoteString(dataTestId)}]` \n \t\t\t};\n \t\t}\n \t}\n \treturn null;\n }\n\n function tryIdStrategy(element) {\n \tconst id = element.getAttribute('id');\n \tif (id && !/:|\\s|\\\\/.test(id)) { //avoid using id if it contain unusual characters (react creates ids like this: \":r:42\")\n \t\t//check if the id is unique by querying the id selector\n \t\tconst matchedElements = robustQuerySelector(`#${CSS.escape(id)}`, document, true);\t\n \t\tif (matchedElements.length === 1) {\n \t\t\treturn { \n \t\t\t\tstrategy: 'id', \n \t\t\t\tselector: `#${CSS.escape(id)}` \n \t\t\t};\n \t\t}\n \t}\n \treturn null;\n }\n\n // Try all strategies on an element in order of precedence\n function attemptStrategiesOnElement(element) {\n \tconst strategies = [\n \t\ttryRoleStrategy, \t\t\n \t\ttryAriaLabelledByStrategy,\n \t\ttryLabelStrategy,\n \t\ttryDataTestIdStrategy,\n \t\ttryNameStrategy,\n \t\t// tryPlaceholderStrategy,\n \t\ttryIdStrategy,\t\t\n \t\t// tryClassStrategy\n \t];\n \t\n \tfor (const strategy of strategies) {\n \t\tconst result = strategy(element); \n \t\tif (result) { \n \t\t\treturn result;\n \t\t}\n \t}\n \treturn null;\n }\n\n /**\n * Generate a smart selector for an element\n * @param {Element} element - The element to generate a locator for\n * @param {string} childPath - The child path to generate a locator for\n * @returns {Object} The selector for the element\n */\n function generateSmartSelector(element, childPath = '') {\n \t// Terminate recursion\n \tif (!element || element.nodeType !== Node.ELEMENT_NODE) {\n \t\treturn {\n \t\t\tstrategy: 'css',\n \t\t\tselector: childPath\n \t\t};\n \t}\n \t\n \t// Try strategies 1-6 on current element\n \tconst result = attemptStrategiesOnElement(element);\n \tconst is_label = result?.strategy === 'label for' || result?.strategy === 'label by';\n\n \tconsole.log('result', result);\n \tif (result) { // found a strategy\t\t\t\n \t\t// If we have a child path, concatenate parent strategy result with child CSS path \n \t\tlet extendedResult = result; \n \t\tif (childPath) {\n \t\t\tconst selector = is_label ? `${childPath}` : `${result?.selector} > ${childPath}`;\n \t\t\textendedResult = {\n \t\t\t\t...extendedResult,\n \t\t\t\tselector: selector,\n \t\t\t\tstrategy: `${extendedResult.strategy} (concatenated with child CSS path)`\n \t\t\t};\n \t\t} \t\t\n \t\t// check if the combined selector is unique and if not add the index to the strategy\n \t\tif (!is_label) { //label element is already checked for uniqueness\n \t\t\ttry {\n \t\t\t\tconst matchedElements = querySelectorAllByText(extendedResult?.selector); //query full selector for uniqueness checking \n \t\t\t\thighlighterLogger.debug(`querySelectorAllByText(${extendedResult?.selector}) matched ${matchedElements.length} elements`);\n \t\t\t\thighlighterLogger.debug(matchedElements);\n \t\t\t\tif (matchedElements.length > 1) {\n \t\t\t\t\tconst childElement = childPath ? robustQuerySelector(`:scope > ${childPath}`, element) : element;\n \t\t\t\t\tconst index = matchedElements.findIndex(el => el === childElement);\n \t\t\t\t\tif (index === -1) {\n \t\t\t\t\t\thighlighterLogger.error('internal error: Element not found in matched elements', element, result?.selector, matchedElements);\n \t\t\t\t\t\treturn null;\n \t\t\t\t\t}\n \t\t\t\t\textendedResult = {\n \t\t\t\t\t\t...extendedResult,\n \t\t\t\t\t\tstrategy: `${extendedResult.strategy} (by index)`,\n \t\t\t\t\t\tindex,\n \t\t\t\t\t}; \n \t\t\t\t}\n \t\t\t} catch (error) {\n \t\t\t\thighlighterLogger.error(`internal error: while checking if selector ${result?.selector} is unique for element ${element.outerHTML}`, error);\n \t\t\t\tthrow error;\n \t\t\t}\t\t\t\n \t\t}\n \t\treturn extendedResult;\n \t}\n \t\n \t// Get CSS selector for current element and build child path \n \tlet newChildPath;\n \t// didn't find strategy\n \tconst elementSelector = getElementCssSelector(element);\n \tnewChildPath = childPath ? `${elementSelector} > ${childPath}` : elementSelector;\n \tconst numComponents = newChildPath.split(' > ').length;\n \tif (numComponents > 2) { //require at least 2 components for css strategy\n \t\t// check if the combined selector is unique\n \t\tconst matchedElements = robustQuerySelector(newChildPath, document, true);\n \t\tif (matchedElements.length === 1) {\n \t\t\treturn {\n \t\t\t\tstrategy: 'css',\t\t\t\t\n \t\t\t\tselector: newChildPath\n \t\t\t};\n \t\t}\n \t}\n\n \t// Recursively try parent element\n \treturn generateSmartSelector(getParentNode(element), newChildPath);\n }\n\n const findElementByInfo = (elementInfo, useSmartSelectors = false) => {\n \thighlighterLogger.debug('findElementByInfo:', elementInfo);\n \tif (useSmartSelectors) {\n \t return findElementBySelectors(elementInfo.smart_iframe_selector, elementInfo.smart_selector, true);\n \t}\n \treturn findElementBySelectors(elementInfo.iframe_selector, elementInfo.css_selector, false);\n };\n\n const compareSelectors = (selector1, selector2, useSmartSelectors = false) => {\n \tif (useSmartSelectors) {\n \t\treturn JSON.stringify(selector1) === JSON.stringify(selector2);\n \t}\n \treturn selector1 === selector2;\n };\n\n const findElementBySelectors = (iframeSelector, elementSelector, useSmartSelectors = false) => {\n \tlet element;\n \t\n \t// first find the iframe that contains the element\n \tif (iframeSelector) { \n \t const frames = getAllDocumentElementsIncludingShadow('iframe', document);\n \t \n \t // Iterate over all frames and compare their CSS selectors\n \t for (const frame of frames) {\n \t\tconst selector = useSmartSelectors ? generateSmartSelector(frame) : generateCssPath(frame);\n \t\tif (compareSelectors(selector, iframeSelector, useSmartSelectors)) {\n \t\t const frameDocument = frame.contentDocument || frame.contentWindow.document;\n \t\t element = useSmartSelectors ? findElementBySmartSelector(elementSelector, frameDocument) : robustQuerySelector(elementSelector, frameDocument);\t\t \n \t\t break;\n \t\t} \n \t }\t}\n \telse {\t\t\n \t\telement = useSmartSelectors ? findElementBySmartSelector(elementSelector, document) : robustQuerySelector(elementSelector, document);\t\t\n \t}\n \t \n \tif (element) {\n \t\thighlighterLogger.debug('findElementBySelectors: found element ', element);\n \t}\n \telse {\n \t highlighterLogger.warn('findElementBySelectors: failed to find element with CSS selector:', JSON.stringify(elementSelector));\n \t}\n \n \treturn element;\n };\n \n function findElementBySmartSelector(smartSelector, root = document) {\t\n \thighlighterLogger.debug(`findElementBySmartSelector: called with smartSelector=${JSON.stringify(smartSelector)}`);\n \tlet combinedSelector;\n \tconst mainStrategy = smartSelector.strategy.split(' ')[0]; //extract the main strategy from the full strategy string\n \tswitch(mainStrategy) {\n case 'role': \n case 'placeholder':\n case 'name':\n case 'data-testid':\n case 'id':\n case 'css':\n case 'class':\n \t\t highlighterLogger.debug(`findElementBySmartSelector: strategy='${smartSelector.strategy}', selector=${smartSelector?.selector}, index=${smartSelector?.index}`);\n if (typeof smartSelector?.index === 'number') {\n return querySelectorAllByText(smartSelector.selector, root)[smartSelector.index];\n }\n else {\n return querySelectorAllByText(smartSelector.selector, root)[0];\n }\t\t \n case 'label':\n \t\t\tif (smartSelector.strategy === 'label for') {\n \t\t\t\thighlighterLogger.debug(`findElementBySmartSelector: strategy='label for', label_selector=${smartSelector.label_selector}`);\n \t\t\t\tconst label_for = querySelectorAllByText(smartSelector.label_selector, root)[0];\n \t\t\t\tcombinedSelector = smartSelector?.selector ? `#${label_for.getAttribute('for')} > ${smartSelector.selector}` : `#${label_for.getAttribute('for')}`;\n \t\t\t\treturn robustQuerySelector(combinedSelector, root);\n \t\t\t}\n \t\t\telse if (smartSelector.strategy === 'label by') {\n \t\t\t\thighlighterLogger.debug(`findElementBySmartSelector: strategy='label by', label_selector=${smartSelector.label_selector}`);\n \t\t\t\tconst label_by = querySelectorAllByText(smartSelector.label_selector, root)[0];\n \t\t\t\tcombinedSelector = smartSelector?.selector ? `[aria-labelledby=\"${label_by.getAttribute('id')}\"] > ${smartSelector.selector}` : `[aria-labelledby=\"${label_by.getAttribute('id')}\"]`;\n \t\t\t\treturn robustQuerySelector(combinedSelector, root);\n \t\t\t}\n \t\t\telse {\n \t\t\t\thighlighterLogger.error(`findElementBySmartSelector: unsupported label strategy: ${smartSelector.strategy}`);\n \t\t\t\treturn null;\n \t\t\t} \n default:\n highlighterLogger.error(`findElementBySmartSelector: Unsupported smart selector strategy: ${smartSelector.strategy}`);\n return null;\n }\n }\n\n function getElementInfo(element, index) { \t\t\t\n \tconst iframe = getContainingIFrame(element); \n \tconst iframe_selector = iframe ? generateCssPath(iframe) : \"\";\t\n \tconst smart_iframe_selector = iframe ? generateSmartSelector(iframe) : null;\n \t\n \t// Return element info with pre-calculated values\n \treturn {\n \t\tindex: index ?? -1,\n \t\ttag: element.tagName.toLowerCase(),\n \t\ttype: element.type || '',\n \t\ttext: getElementText(element) || getElementPlaceholder(element),\n \t\thtml: cleanHTML(element.outerHTML),\n \t\txpath: generateXPath(element),\n \t\tcss_selector: generateCssPath(element),\n \t\tbounding_box: element.getBoundingClientRect(),\t\t\n \t\tiframe_selector: iframe_selector,\t\t\n \t\tsmart_selector: generateSmartSelector(element),\n \t\tsmart_iframe_selector: smart_iframe_selector,\n \t\telement: element,\n \t\tdepth: getElementDepth(element)\n \t};\n }\n\n /**\n * Checks if a point is inside a bounding box\n * \n * @param point The point to check\n * @param box The bounding box\n * @returns boolean indicating if the point is inside the box\n */\n function isPointInsideBox(point, box) {\n return point.x >= box.x &&\n point.x <= box.x + box.width &&\n point.y >= box.y &&\n point.y <= box.y + box.height;\n }\n\n /**\n * Calculates the overlap area between two bounding boxes\n * \n * @param box1 First bounding box\n * @param box2 Second bounding box\n * @returns The overlap area\n */\n function calculateOverlap(box1, box2) {\n const xOverlap = Math.max(0,\n Math.min(box1.x + box1.width, box2.x + box2.width) -\n Math.max(box1.x, box2.x)\n );\n const yOverlap = Math.max(0,\n Math.min(box1.y + box1.height, box2.y + box2.height) -\n Math.max(box1.y, box2.y)\n );\n return xOverlap * yOverlap;\n }\n\n /**\n * Finds an exact match between candidate elements and the actual interaction element\n * \n * @param candidate_elements Array of candidate element infos\n * @param actualInteractionElementInfo The actual interaction element info\n * @returns The matching candidate element info, or null if no match is found\n */\n function findExactMatch(candidate_elements, actualInteractionElementInfo) {\n if (!actualInteractionElementInfo.element) {\n return null;\n }\n\n const exactMatch = candidate_elements.find(elementInfo => \n elementInfo.element && elementInfo.element === actualInteractionElementInfo.element\n );\n \n if (exactMatch) {\n highlighterLogger.debug('✅ Found exact element match:', {\n matchedElement: exactMatch.element?.tagName,\n matchedElementClass: exactMatch.element?.className,\n index: exactMatch.index\n });\n return exactMatch;\n }\n \n return null;\n }\n\n /**\n * Finds a match by traversing up the parent elements\n * \n * @param candidate_elements Array of candidate element infos\n * @param actualInteractionElementInfo The actual interaction element info\n * @returns The matching candidate element info, or null if no match is found\n */\n function findParentMatch(candidate_elements, actualInteractionElementInfo) {\n if (!actualInteractionElementInfo.element) {\n return null;\n }\n\n let element = actualInteractionElementInfo.element;\n while (element.parentElement) {\n element = element.parentElement;\n const parentMatch = candidate_elements.find(candidate => \n candidate.element && candidate.element === element\n );\n \n if (parentMatch) {\n highlighterLogger.debug('✅ Found parent element match:', {\n matchedElement: parentMatch.element?.tagName,\n matchedElementClass: parentMatch.element?.className,\n index: parentMatch.index,\n depth: element.tagName\n });\n return parentMatch;\n }\n \n // Stop if we hit another candidate element\n if (candidate_elements.some(candidate => \n candidate.element && candidate.element === element\n )) {\n highlighterLogger.debug('⚠️ Stopped parent search - hit another candidate element:', element.tagName);\n break;\n }\n }\n \n return null;\n }\n\n /**\n * Finds a match based on spatial relationships between elements\n * \n * @param candidate_elements Array of candidate element infos\n * @param actualInteractionElementInfo The actual interaction element info\n * @returns The matching candidate element info, or null if no match is found\n */\n function findSpatialMatch(candidate_elements, actualInteractionElementInfo) {\n if (!actualInteractionElementInfo.element || !actualInteractionElementInfo.bounding_box) {\n return null;\n }\n\n const actualBox = actualInteractionElementInfo.bounding_box;\n let bestMatch = null;\n let bestScore = 0;\n\n for (const candidateInfo of candidate_elements) {\n if (!candidateInfo.bounding_box) continue;\n \n const candidateBox = candidateInfo.bounding_box;\n let score = 0;\n\n // Check if actual element is contained within candidate\n if (isPointInsideBox({ x: actualBox.x, y: actualBox.y }, candidateBox) &&\n isPointInsideBox({ x: actualBox.x + actualBox.width, y: actualBox.y + actualBox.height }, candidateBox)) {\n score += 100; // High score for containment\n }\n\n // Calculate overlap area as a factor\n const overlap = calculateOverlap(actualBox, candidateBox);\n score += overlap;\n\n // Consider proximity if no containment\n if (score === 0) {\n const distance = Math.sqrt(\n Math.pow((actualBox.x + actualBox.width/2) - (candidateBox.x + candidateBox.width/2), 2) +\n Math.pow((actualBox.y + actualBox.height/2) - (candidateBox.y + candidateBox.height/2), 2)\n );\n // Convert distance to a score (closer = higher score)\n score = 1000 / (distance + 1);\n }\n\n if (score > bestScore) {\n bestScore = score;\n bestMatch = candidateInfo;\n highlighterLogger.debug('📏 New best spatial match:', {\n element: candidateInfo.element?.tagName,\n class: candidateInfo.element?.className,\n index: candidateInfo.index,\n score: score\n });\n }\n }\n\n if (bestMatch) {\n highlighterLogger.debug('✅ Final spatial match selected:', {\n element: bestMatch.element?.tagName,\n class: bestMatch.element?.className,\n index: bestMatch.index,\n finalScore: bestScore\n });\n return bestMatch;\n }\n\n return null;\n }\n\n /**\n * Finds a matching candidate element for an actual interaction element\n * \n * @param candidate_elements Array of candidate element infos\n * @param actualInteractionElementInfo The actual interaction element info\n * @returns The matching candidate element info, or null if no match is found\n */\n function findMatchingCandidateElementInfo(candidate_elements, actualInteractionElementInfo) {\n if (!actualInteractionElementInfo.element || !actualInteractionElementInfo.bounding_box) {\n highlighterLogger.error('❌ Missing required properties in actualInteractionElementInfo');\n return null;\n }\n\n highlighterLogger.debug('🔍 Starting element matching for:', {\n clickedElement: actualInteractionElementInfo.element.tagName,\n clickedElementClass: actualInteractionElementInfo.element.className,\n totalCandidates: candidate_elements.length\n });\n\n // First try exact element match\n const exactMatch = findExactMatch(candidate_elements, actualInteractionElementInfo);\n if (exactMatch) {\n return exactMatch;\n }\n highlighterLogger.debug('❌ No exact element match found, trying parent matching...');\n\n // Try finding closest clickable parent\n const parentMatch = findParentMatch(candidate_elements, actualInteractionElementInfo);\n if (parentMatch) {\n return parentMatch;\n }\n highlighterLogger.debug('❌ No parent match found, falling back to spatial matching...');\n\n // If no exact or parent match, look for spatial relationships\n const spatialMatch = findSpatialMatch(candidate_elements, actualInteractionElementInfo);\n if (spatialMatch) {\n return spatialMatch;\n }\n\n highlighterLogger.error('❌ No matching element found for actual interaction element:', actualInteractionElementInfo);\n return null;\n }\n\n const highlight = {\n execute: async function(elementTypes, handleScroll=false) {\n const elements = await findElements(elementTypes);\n highlightElements(elements, false,handleScroll);\n return elements;\n },\n\n unexecute: function(handleScroll=false) {\n unhighlightElements(handleScroll);\n },\n\n generateJSON: async function() {\n const json = {};\n\n // Capture viewport dimensions\n const viewportData = {\n width: window.innerWidth,\n height: window.innerHeight,\n documentWidth: document.documentElement.clientWidth,\n documentHeight: document.documentElement.clientHeight,\n timestamp: new Date().toISOString()\n };\n\n // Add viewport data to the JSON output\n json.viewport = viewportData;\n\n\n await Promise.all(Object.values(ElementTag).map(async elementType => {\n const elements = await findElements(elementType);\n json[elementType] = elements;\n }));\n\n // Serialize the JSON object\n const jsonString = JSON.stringify(json, null, 4); // Pretty print with 4 spaces\n\n highlighterLogger.log(`JSON: ${jsonString}`);\n return jsonString;\n },\n\n getElementInfo\n };\n\n\n function unhighlightElements(handleScroll=false) {\n const documents = getAllFrames();\n documents.forEach(doc => {\n const overlay = doc.getElementById('highlight-overlay');\n if (overlay) {\n if (handleScroll) {\n // Remove event listeners\n doc.removeEventListener('scroll', overlay.scrollHandler, true);\n doc.removeEventListener('resize', overlay.resizeHandler);\n }\n overlay.remove();\n }\n });\n }\n\n\n async function findElements(elementTypes, verbose=true) {\n const typesArray = Array.isArray(elementTypes) ? elementTypes : [elementTypes];\n highlighterLogger.debug('Starting element search for types:', typesArray);\n\n const elements = [];\n typesArray.forEach(elementType => {\n if (elementType === ElementTag.FILLABLE) {\n elements.push(...findFillables());\n }\n if (elementType === ElementTag.SELECTABLE) {\n elements.push(...findDropdowns());\n }\n if (elementType === ElementTag.CLICKABLE) {\n elements.push(...findClickables());\n elements.push(...findToggles());\n elements.push(...findCheckables());\n }\n if (elementType === ElementTag.NON_INTERACTIVE_ELEMENT) {\n elements.push(...findNonInteractiveElements());\n }\n });\n\n // console.log('Before uniquify:', elements.length);\n const elementsWithInfo = elements.map((element, index) => \n getElementInfo(element, index)\n );\n\n \n \n const uniqueElements = uniquifyElements(elementsWithInfo);\n highlighterLogger.debug(`Found ${uniqueElements.length} elements:`);\n \n // More comprehensive visibility check\n const visibleElements = uniqueElements.filter(elementInfo => {\n const el = elementInfo.element;\n const style = getComputedStyle(el);\n \n // Check various style properties that affect visibility\n if (style.display === 'none' || \n style.visibility === 'hidden') {\n return false;\n }\n \n // Check if element has non-zero dimensions\n const rect = el.getBoundingClientRect();\n if (rect.width === 0 || rect.height === 0) {\n return false;\n }\n \n // Check if element is within viewport\n if (rect.bottom < 0 || \n rect.top > window.innerHeight || \n rect.right < 0 || \n rect.left > window.innerWidth) {\n // Element is outside viewport, but still might be valid \n // if user scrolls to it, so we'll include it\n return true;\n }\n \n return true;\n });\n \n highlighterLogger.debug(`Out of which ${visibleElements.length} elements are visible:`);\n if (verbose) {\n visibleElements.forEach(info => {\n highlighterLogger.debug(`Element ${info.index}:`, info);\n });\n }\n \n return visibleElements;\n }\n\n // elements is an array of objects with index, xpath\n function highlightElements(elements, enableSmartSelectors=false, handleScroll=false) {\n // console.log('[highlightElements] called with', elements.length, 'elements');\n // Create overlay if it doesn't exist and store it in a dictionary\n const documents = getAllFrames(); \n let overlays = {};\n documents.forEach(doc => {\n let overlay = doc.getElementById('highlight-overlay');\n if (!overlay) {\n overlay = doc.createElement('div');\n overlay.id = 'highlight-overlay';\n overlay.style.cssText = `\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n pointer-events: none;\n z-index: 2147483647;\n `;\n doc.body.appendChild(overlay);\n // console.log('[highlightElements] Created overlay in document:', doc);\n }\n overlays[doc.documentURI] = overlay;\n });\n \n\n const updateHighlights = (doc = null) => {\n if (doc) {\n overlays[doc.documentURI].innerHTML = '';\n } else {\n Object.values(overlays).forEach(overlay => { overlay.innerHTML = ''; });\n } \n elements.forEach((elementInfo, idx) => {\n //console.log(`[highlightElements] Processing element ${idx}:`, elementInfo.tag, elementInfo.css_selector, elementInfo.bounding_box);\n let element = elementInfo.element;\n if (!element) {\n element = findElementByInfo(elementInfo, enableSmartSelectors);\n if (!element) {\n highlighterLogger.warn('[highlightElements] Could not find element for:', elementInfo);\n return;\n }\n }\n //if highlights requested for a specific doc, skip unrelated elements\n if (doc && element.ownerDocument !== doc) {\n highlighterLogger.debug(\"[highlightElements] Skipped element since it doesn't belong to document\", doc);\n return;\n }\n const rect = element.getBoundingClientRect();\n if (rect.width === 0 || rect.height === 0) {\n highlighterLogger.warn('[highlightElements] Element has zero dimensions:', elementInfo);\n return;\n }\n // Create border highlight (red rectangle)\n // use ownerDocument to support iframes/frames\n const highlight = element.ownerDocument.createElement('div');\n highlight.style.cssText = `\n position: fixed;\n left: ${rect.x}px;\n top: ${rect.y}px;\n width: ${rect.width}px;\n height: ${rect.height}px;\n border: 1px solid rgb(255, 0, 0);\n transition: all 0.2s ease-in-out;\n `;\n // Create index label container - now positioned to the right and slightly up\n const labelContainer = element.ownerDocument.createElement('div');\n labelContainer.style.cssText = `\n position: absolute;\n right: -10px; /* Offset to the right */\n top: -10px; /* Offset upwards */\n padding: 4px;\n background-color: rgba(255, 255, 0, 0.6);\n display: flex;\n align-items: center;\n justify-content: center;\n `;\n const text = element.ownerDocument.createElement('span');\n text.style.cssText = `\n color: rgb(0, 0, 0, 0.8);\n font-family: 'Courier New', Courier, monospace;\n font-size: 12px;\n font-weight: bold;\n line-height: 1;\n `;\n text.textContent = elementInfo.index.toString();\n labelContainer.appendChild(text);\n highlight.appendChild(labelContainer); \n overlays[element.ownerDocument.documentURI].appendChild(highlight);\n \n });\n };\n\n // Initial highlight\n updateHighlights();\n\n if (handleScroll) {\n documents.forEach(doc => {\n // Update highlights on scroll and resize\n highlighterLogger.debug('registering scroll and resize handlers for document: ', doc);\n const scrollHandler = () => {\n requestAnimationFrame(() => updateHighlights(doc));\n };\n const resizeHandler = () => {\n updateHighlights(doc);\n };\n doc.addEventListener('scroll', scrollHandler, true);\n doc.addEventListener('resize', resizeHandler);\n // Store event handlers for cleanup\n overlays[doc.documentURI].scrollHandler = scrollHandler;\n overlays[doc.documentURI].resizeHandler = resizeHandler;\n }); \n }\n }\n\n // function unexecute() {\n // unhighlightElements();\n // }\n\n // Make it available globally for both Extension and Playwright\n if (typeof window !== 'undefined') {\n function stripElementRefs(elementInfo) {\n if (!elementInfo) return null;\n const { element, ...rest } = elementInfo;\n return rest;\n }\n\n window.ProboLabs = window.ProboLabs || {};\n\n // --- Caching State ---\n window.ProboLabs.candidates = [];\n window.ProboLabs.actual = null;\n window.ProboLabs.matchingCandidate = null;\n\n // --- Methods ---\n /**\n * Find and cache candidate elements of a given type (e.g., 'CLICKABLE').\n * NOTE: This function is async and must be awaited from Playwright/Node.\n */\n window.ProboLabs.findAndCacheCandidateElements = async function(elementType) {\n //console.log('[ProboLabs] findAndCacheCandidateElements called with:', elementType);\n const found = await findElements(elementType);\n window.ProboLabs.candidates = found;\n // console.log('[ProboLabs] candidates set to:', found, 'type:', typeof found, 'isArray:', Array.isArray(found));\n return found.length;\n };\n\n window.ProboLabs.findAndCacheActualElement = function(iframeSelector, elementSelector, isHover=false, useSmartSelectors=false) {\n // console.log('[ProboLabs] findAndCacheActualElement called with:', cssSelector, iframeSelector);\n highlighterLogger.debug(`[ProboLabs] findAndCacheActualElement called with: iframeSelector=${iframeSelector}, elementSelector=${elementSelector}, useSmartSelectors=${useSmartSelectors}`);\n let el = findElementBySelectors(iframeSelector, elementSelector, useSmartSelectors);\n if(isHover) {\n const visibleElement = findClosestVisibleElement(el);\n if (visibleElement) {\n el = visibleElement;\n }\n }\n if (!el) {\n window.ProboLabs.actual = null;\n // console.log('[ProboLabs] actual set to null');\n return false;\n }\n window.ProboLabs.actual = getElementInfo(el, -1);\n // console.log('[ProboLabs] actual set to:', window.ProboLabs.actual);\n return true;\n };\n\n window.ProboLabs.findAndCacheMatchingCandidate = function() {\n // console.log('[ProboLabs] findAndCacheMatchingCandidate called');\n if (!window.ProboLabs.candidates.length || !window.ProboLabs.actual) {\n window.ProboLabs.matchingCandidate = null;\n // console.log('[ProboLabs] matchingCandidate set to null');\n return false;\n }\n window.ProboLabs.matchingCandidate = findMatchingCandidateElementInfo(window.ProboLabs.candidates, window.ProboLabs.actual);\n // console.log('[ProboLabs] matchingCandidate set to:', window.ProboLabs.matchingCandidate);\n return !!window.ProboLabs.matchingCandidate;\n };\n\n window.ProboLabs.highlightCachedElements = function(which) {\n let elements = [];\n if (which === 'candidates') elements = window.ProboLabs.candidates;\n if (which === 'actual' && window.ProboLabs.actual) elements = [window.ProboLabs.actual];\n if (which === 'matching' && window.ProboLabs.matchingCandidate) elements = [window.ProboLabs.matchingCandidate];\n highlighterLogger.debug(`[ProboLabs] highlightCachedElements ${which} with ${elements.length} elements`);\n highlightElements(elements);\n };\n\n window.ProboLabs.unhighlight = function() {\n // console.log('[ProboLabs] unhighlight called');\n unhighlightElements();\n };\n\n window.ProboLabs.reset = function() {\n highlighterLogger.debug('[ProboLabs] reset called');\n window.ProboLabs.candidates = [];\n window.ProboLabs.actual = null;\n window.ProboLabs.matchingCandidate = null;\n unhighlightElements();\n };\n\n window.ProboLabs.getCandidates = function() {\n // console.log('[ProboLabs] getCandidates called. candidates:', window.ProboLabs.candidates, 'type:', typeof window.ProboLabs.candidates, 'isArray:', Array.isArray(window.ProboLabs.candidates));\n const arr = Array.isArray(window.ProboLabs.candidates) ? window.ProboLabs.candidates : [];\n return arr.map(stripElementRefs);\n };\n window.ProboLabs.getActual = function() {\n return stripElementRefs(window.ProboLabs.actual);\n };\n window.ProboLabs.getMatchingCandidate = function() {\n return stripElementRefs(window.ProboLabs.matchingCandidate);\n };\n\n window.ProboLabs.setLoggerDebugLevel = function(debugLevel) {\n highlighterLogger.setLogLevel(debugLevel);\n };\n\n // Retain existing API for backward compatibility\n window.ProboLabs.ElementTag = ElementTag;\n window.ProboLabs.highlightElements = highlightElements;\n window.ProboLabs.unhighlightElements = unhighlightElements;\n window.ProboLabs.findElements = findElements;\n window.ProboLabs.getElementInfo = getElementInfo;\n window.ProboLabs.highlight = window.ProboLabs.highlight;\n window.ProboLabs.unhighlight = window.ProboLabs.unhighlight;\n\n // --- Utility Functions ---\n function findClosestVisibleElement(element) {\n let current = element;\n while (current) {\n const style = window.getComputedStyle(current);\n if (\n style &&\n style.display !== 'none' &&\n style.visibility !== 'hidden' &&\n current.offsetWidth > 0 &&\n current.offsetHeight > 0\n ) {\n return current;\n }\n if (!current.parentElement || current === document.body) break;\n current = current.parentElement;\n }\n return null;\n }\n }\n\n exports.deserializeNodeFromJSON = deserializeNodeFromJSON;\n exports.detectScrollableContainers = detectScrollableContainers;\n exports.findElementByInfo = findElementByInfo;\n exports.findElementBySelectors = findElementBySelectors;\n exports.findElements = findElements;\n exports.generateCssPath = generateCssPath;\n exports.generateSmartSelector = generateSmartSelector;\n exports.getAriaLabelledByText = getAriaLabelledByText;\n exports.getContainingIFrame = getContainingIFrame;\n exports.getElementInfo = getElementInfo;\n exports.highlight = highlight;\n exports.highlightElements = highlightElements;\n exports.isScrollableContainer = isScrollableContainer;\n exports.serializeNodeToJSON = serializeNodeToJSON;\n exports.unhighlightElements = unhighlightElements;\n\n}));\n//# sourceMappingURL=probolabs.umd.js.map\n";
|
|
2
2
|
(function (global, factory) {
|
|
3
3
|
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('fs'), require('path'), require('os'), require('child_process'), require('util'), require('adm-zip'), require('node-fetch')) :
|
|
4
4
|
typeof define === 'function' && define.amd ? define(['exports', 'fs', 'path', 'os', 'child_process', 'util', 'adm-zip', 'node-fetch'], factory) :
|
|
@@ -3223,8 +3223,8 @@ export default class ProboReporter implements Reporter {
|
|
|
3223
3223
|
}
|
|
3224
3224
|
NavTracker.instance = null;
|
|
3225
3225
|
|
|
3226
|
-
//! otpauth 9.
|
|
3227
|
-
//! noble-hashes
|
|
3226
|
+
//! otpauth 9.5.0 | (c) Héctor Molinero Fernández | MIT | https://github.com/hectorm/otpauth
|
|
3227
|
+
//! noble-hashes 2.0.1 | (c) Paul Miller | MIT | https://github.com/paulmillr/noble-hashes
|
|
3228
3228
|
/// <reference types="./otpauth.d.ts" />
|
|
3229
3229
|
// @ts-nocheck
|
|
3230
3230
|
/**
|
|
@@ -3247,24 +3247,29 @@ export default class ProboReporter implements Reporter {
|
|
|
3247
3247
|
/**
|
|
3248
3248
|
* Utilities for hex, bytes, CSPRNG.
|
|
3249
3249
|
* @module
|
|
3250
|
-
*/ /*! noble-hashes - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
|
3251
|
-
// node.js versions earlier than v19 don't declare it in global scope.
|
|
3252
|
-
// For node.js, package.json#exports field mapping rewrites import
|
|
3253
|
-
// from `crypto` to `cryptoNode`, which imports native module.
|
|
3254
|
-
// Makes the utils un-importable in browsers without a bundler.
|
|
3255
|
-
// Once node.js 18 is deprecated (2025-04-30), we can just drop the import.
|
|
3256
|
-
/** Checks if something is Uint8Array. Be careful: nodejs Buffer will return true. */ function isBytes(a) {
|
|
3250
|
+
*/ /*! noble-hashes - MIT License (c) 2022 Paul Miller (paulmillr.com) */ /** Checks if something is Uint8Array. Be careful: nodejs Buffer will return true. */ function isBytes(a) {
|
|
3257
3251
|
return a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === 'Uint8Array';
|
|
3258
3252
|
}
|
|
3259
|
-
/** Asserts something is positive integer. */ function anumber(n) {
|
|
3260
|
-
if (!Number.isSafeInteger(n) || n < 0)
|
|
3253
|
+
/** Asserts something is positive integer. */ function anumber(n, title = '') {
|
|
3254
|
+
if (!Number.isSafeInteger(n) || n < 0) {
|
|
3255
|
+
const prefix = title && `"${title}" `;
|
|
3256
|
+
throw new Error(`${prefix}expected integer >= 0, got ${n}`);
|
|
3257
|
+
}
|
|
3261
3258
|
}
|
|
3262
|
-
/** Asserts something is Uint8Array. */ function abytes(
|
|
3263
|
-
|
|
3264
|
-
|
|
3259
|
+
/** Asserts something is Uint8Array. */ function abytes(value, length, title = '') {
|
|
3260
|
+
const bytes = isBytes(value);
|
|
3261
|
+
const len = value?.length;
|
|
3262
|
+
const needsLen = length !== undefined;
|
|
3263
|
+
if (!bytes || needsLen && len !== length) {
|
|
3264
|
+
const prefix = title && `"${title}" `;
|
|
3265
|
+
const ofLen = needsLen ? ` of length ${length}` : '';
|
|
3266
|
+
const got = bytes ? `length=${len}` : `type=${typeof value}`;
|
|
3267
|
+
throw new Error(prefix + 'expected Uint8Array' + ofLen + ', got ' + got);
|
|
3268
|
+
}
|
|
3269
|
+
return value;
|
|
3265
3270
|
}
|
|
3266
3271
|
/** Asserts something is hash */ function ahash(h) {
|
|
3267
|
-
if (typeof h !== 'function' || typeof h.create !== 'function') throw new Error('Hash
|
|
3272
|
+
if (typeof h !== 'function' || typeof h.create !== 'function') throw new Error('Hash must wrapped by utils.createHasher');
|
|
3268
3273
|
anumber(h.outputLen);
|
|
3269
3274
|
anumber(h.blockLen);
|
|
3270
3275
|
}
|
|
@@ -3273,10 +3278,10 @@ export default class ProboReporter implements Reporter {
|
|
|
3273
3278
|
if (checkFinished && instance.finished) throw new Error('Hash#digest() has already been called');
|
|
3274
3279
|
}
|
|
3275
3280
|
/** Asserts output is properly-sized byte array */ function aoutput(out, instance) {
|
|
3276
|
-
abytes(out);
|
|
3281
|
+
abytes(out, undefined, 'digestInto() output');
|
|
3277
3282
|
const min = instance.outputLen;
|
|
3278
3283
|
if (out.length < min) {
|
|
3279
|
-
throw new Error('digestInto()
|
|
3284
|
+
throw new Error('"digestInto() output" expected to be of length >=' + min);
|
|
3280
3285
|
}
|
|
3281
3286
|
}
|
|
3282
3287
|
/** Cast u8 / u16 / u32 to u32. */ function u32(arr) {
|
|
@@ -3309,34 +3314,32 @@ export default class ProboReporter implements Reporter {
|
|
|
3309
3314
|
return arr;
|
|
3310
3315
|
}
|
|
3311
3316
|
const swap32IfBE = isLE ? (u)=>u : byteSwap32;
|
|
3312
|
-
/**
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
*/ function utf8ToBytes(str) {
|
|
3316
|
-
if (typeof str !== 'string') throw new Error('string expected');
|
|
3317
|
-
return new Uint8Array(new TextEncoder().encode(str)); // https://bugzil.la/1681809
|
|
3318
|
-
}
|
|
3319
|
-
/**
|
|
3320
|
-
* Normalizes (non-hex) string or Uint8Array to Uint8Array.
|
|
3321
|
-
* Warning: when Uint8Array is passed, it would NOT get copied.
|
|
3322
|
-
* Keep in mind for future mutable operations.
|
|
3323
|
-
*/ function toBytes(data) {
|
|
3324
|
-
if (typeof data === 'string') data = utf8ToBytes(data);
|
|
3325
|
-
abytes(data);
|
|
3326
|
-
return data;
|
|
3327
|
-
}
|
|
3328
|
-
/** For runtime check if class implements interface */ class Hash {
|
|
3329
|
-
}
|
|
3330
|
-
/** Wraps hash function, creating an interface on top of it */ function createHasher(hashCons) {
|
|
3331
|
-
const hashC = (msg)=>hashCons().update(toBytes(msg)).digest();
|
|
3332
|
-
const tmp = hashCons();
|
|
3317
|
+
/** Creates function with outputLen, blockLen, create properties from a class constructor. */ function createHasher(hashCons, info = {}) {
|
|
3318
|
+
const hashC = (msg, opts)=>hashCons(opts).update(msg).digest();
|
|
3319
|
+
const tmp = hashCons(undefined);
|
|
3333
3320
|
hashC.outputLen = tmp.outputLen;
|
|
3334
3321
|
hashC.blockLen = tmp.blockLen;
|
|
3335
|
-
hashC.create = ()=>hashCons();
|
|
3336
|
-
|
|
3322
|
+
hashC.create = (opts)=>hashCons(opts);
|
|
3323
|
+
Object.assign(hashC, info);
|
|
3324
|
+
return Object.freeze(hashC);
|
|
3337
3325
|
}
|
|
3326
|
+
/** Creates OID opts for NIST hashes, with prefix 06 09 60 86 48 01 65 03 04 02. */ const oidNist = (suffix)=>({
|
|
3327
|
+
oid: Uint8Array.from([
|
|
3328
|
+
0x06,
|
|
3329
|
+
0x09,
|
|
3330
|
+
0x60,
|
|
3331
|
+
0x86,
|
|
3332
|
+
0x48,
|
|
3333
|
+
0x01,
|
|
3334
|
+
0x65,
|
|
3335
|
+
0x03,
|
|
3336
|
+
0x04,
|
|
3337
|
+
0x02,
|
|
3338
|
+
suffix
|
|
3339
|
+
])
|
|
3340
|
+
});
|
|
3338
3341
|
|
|
3339
|
-
class HMAC
|
|
3342
|
+
/** Internal class for HMAC. */ class _HMAC {
|
|
3340
3343
|
update(buf) {
|
|
3341
3344
|
aexists(this);
|
|
3342
3345
|
this.iHash.update(buf);
|
|
@@ -3344,7 +3347,7 @@ export default class ProboReporter implements Reporter {
|
|
|
3344
3347
|
}
|
|
3345
3348
|
digestInto(out) {
|
|
3346
3349
|
aexists(this);
|
|
3347
|
-
abytes(out, this.outputLen);
|
|
3350
|
+
abytes(out, this.outputLen, 'output');
|
|
3348
3351
|
this.finished = true;
|
|
3349
3352
|
this.iHash.digestInto(out);
|
|
3350
3353
|
this.oHash.update(out);
|
|
@@ -3377,12 +3380,11 @@ export default class ProboReporter implements Reporter {
|
|
|
3377
3380
|
this.oHash.destroy();
|
|
3378
3381
|
this.iHash.destroy();
|
|
3379
3382
|
}
|
|
3380
|
-
constructor(hash,
|
|
3381
|
-
super();
|
|
3383
|
+
constructor(hash, key){
|
|
3382
3384
|
this.finished = false;
|
|
3383
3385
|
this.destroyed = false;
|
|
3384
3386
|
ahash(hash);
|
|
3385
|
-
|
|
3387
|
+
abytes(key, undefined, 'key');
|
|
3386
3388
|
this.iHash = hash.create();
|
|
3387
3389
|
if (typeof this.iHash.update !== 'function') throw new Error('Expected instance of class which extends utils.Hash');
|
|
3388
3390
|
this.blockLen = this.iHash.blockLen;
|
|
@@ -3410,20 +3412,9 @@ export default class ProboReporter implements Reporter {
|
|
|
3410
3412
|
* import { hmac } from '@noble/hashes/hmac';
|
|
3411
3413
|
* import { sha256 } from '@noble/hashes/sha2';
|
|
3412
3414
|
* const mac1 = hmac(sha256, 'key', 'message');
|
|
3413
|
-
*/ const hmac = (hash, key, message)=>new
|
|
3414
|
-
hmac.create = (hash, key)=>new
|
|
3415
|
-
|
|
3416
|
-
/** Polyfill for Safari 14. https://caniuse.com/mdn-javascript_builtins_dataview_setbiguint64 */ function setBigUint64(view, byteOffset, value, isLE) {
|
|
3417
|
-
if (typeof view.setBigUint64 === 'function') return view.setBigUint64(byteOffset, value, isLE);
|
|
3418
|
-
const _32n = BigInt(32);
|
|
3419
|
-
const _u32_max = BigInt(0xffffffff);
|
|
3420
|
-
const wh = Number(value >> _32n & _u32_max);
|
|
3421
|
-
const wl = Number(value & _u32_max);
|
|
3422
|
-
const h = isLE ? 4 : 0;
|
|
3423
|
-
const l = isLE ? 0 : 4;
|
|
3424
|
-
view.setUint32(byteOffset + h, wh, isLE);
|
|
3425
|
-
view.setUint32(byteOffset + l, wl, isLE);
|
|
3426
|
-
}
|
|
3415
|
+
*/ const hmac = (hash, key, message)=>new _HMAC(hash, key).update(message).digest();
|
|
3416
|
+
hmac.create = (hash, key)=>new _HMAC(hash, key);
|
|
3417
|
+
|
|
3427
3418
|
/** Choice: a ? b : c */ function Chi(a, b, c) {
|
|
3428
3419
|
return a & b ^ ~a & c;
|
|
3429
3420
|
}
|
|
@@ -3433,10 +3424,9 @@ export default class ProboReporter implements Reporter {
|
|
|
3433
3424
|
/**
|
|
3434
3425
|
* Merkle-Damgard hash construction base class.
|
|
3435
3426
|
* Could be used to create MD5, RIPEMD, SHA1, SHA2.
|
|
3436
|
-
*/ class HashMD
|
|
3427
|
+
*/ class HashMD {
|
|
3437
3428
|
update(data) {
|
|
3438
3429
|
aexists(this);
|
|
3439
|
-
data = toBytes(data);
|
|
3440
3430
|
abytes(data);
|
|
3441
3431
|
const { view, buffer, blockLen } = this;
|
|
3442
3432
|
const len = data.length;
|
|
@@ -3483,12 +3473,12 @@ export default class ProboReporter implements Reporter {
|
|
|
3483
3473
|
// Note: sha512 requires length to be 128bit integer, but length in JS will overflow before that
|
|
3484
3474
|
// You need to write around 2 exabytes (u64_max / 8 / (1024**6)) for this to happen.
|
|
3485
3475
|
// So we just write lowest 64 bits of that value.
|
|
3486
|
-
setBigUint64(
|
|
3476
|
+
view.setBigUint64(blockLen - 8, BigInt(this.length * 8), isLE);
|
|
3487
3477
|
this.process(view, 0);
|
|
3488
3478
|
const oview = createView(out);
|
|
3489
3479
|
const len = this.outputLen;
|
|
3490
|
-
// NOTE: we do division by 4 later, which
|
|
3491
|
-
if (len % 4) throw new Error('_sha2: outputLen
|
|
3480
|
+
// NOTE: we do division by 4 later, which must be fused in single op with modulo by JIT
|
|
3481
|
+
if (len % 4) throw new Error('_sha2: outputLen must be aligned to 32bit');
|
|
3492
3482
|
const outLen = len / 4;
|
|
3493
3483
|
const state = this.get();
|
|
3494
3484
|
if (outLen > state.length) throw new Error('_sha2: outputLen bigger than state');
|
|
@@ -3516,7 +3506,6 @@ export default class ProboReporter implements Reporter {
|
|
|
3516
3506
|
return this._cloneInto();
|
|
3517
3507
|
}
|
|
3518
3508
|
constructor(blockLen, outputLen, padOffset, isLE){
|
|
3519
|
-
super();
|
|
3520
3509
|
this.finished = false;
|
|
3521
3510
|
this.length = 0;
|
|
3522
3511
|
this.pos = 0;
|
|
@@ -3598,7 +3587,7 @@ export default class ProboReporter implements Reporter {
|
|
|
3598
3587
|
]);
|
|
3599
3588
|
// Reusable temporary buffer
|
|
3600
3589
|
const SHA1_W = /* @__PURE__ */ new Uint32Array(80);
|
|
3601
|
-
/** SHA1 legacy hash class. */ class
|
|
3590
|
+
/** Internal SHA1 legacy hash class. */ class _SHA1 extends HashMD {
|
|
3602
3591
|
get() {
|
|
3603
3592
|
const { A, B, C, D, E } = this;
|
|
3604
3593
|
return [
|
|
@@ -3659,15 +3648,10 @@ export default class ProboReporter implements Reporter {
|
|
|
3659
3648
|
clean(this.buffer);
|
|
3660
3649
|
}
|
|
3661
3650
|
constructor(){
|
|
3662
|
-
super(64, 20, 8, false);
|
|
3663
|
-
this.A = SHA1_IV[0] | 0;
|
|
3664
|
-
this.B = SHA1_IV[1] | 0;
|
|
3665
|
-
this.C = SHA1_IV[2] | 0;
|
|
3666
|
-
this.D = SHA1_IV[3] | 0;
|
|
3667
|
-
this.E = SHA1_IV[4] | 0;
|
|
3651
|
+
super(64, 20, 8, false), this.A = SHA1_IV[0] | 0, this.B = SHA1_IV[1] | 0, this.C = SHA1_IV[2] | 0, this.D = SHA1_IV[3] | 0, this.E = SHA1_IV[4] | 0;
|
|
3668
3652
|
}
|
|
3669
3653
|
}
|
|
3670
|
-
/** SHA1 (RFC 3174) legacy hash function. It was cryptographically broken. */ const sha1 = /* @__PURE__ */ createHasher(()=>new
|
|
3654
|
+
/** SHA1 (RFC 3174) legacy hash function. It was cryptographically broken. */ const sha1 = /* @__PURE__ */ createHasher(()=>new _SHA1());
|
|
3671
3655
|
|
|
3672
3656
|
/**
|
|
3673
3657
|
* Internal helpers for u64. BigUint64Array is too slow as per 2025, so we implement it using Uint32Array.
|
|
@@ -3804,7 +3788,7 @@ export default class ProboReporter implements Reporter {
|
|
|
3804
3788
|
0xc67178f2
|
|
3805
3789
|
]);
|
|
3806
3790
|
/** Reusable temporary buffer. "W" comes straight from spec. */ const SHA256_W = /* @__PURE__ */ new Uint32Array(64);
|
|
3807
|
-
class
|
|
3791
|
+
/** Internal 32-byte base SHA2 hash class. */ class SHA2_32B extends HashMD {
|
|
3808
3792
|
get() {
|
|
3809
3793
|
const { A, B, C, D, E, F, G, H } = this;
|
|
3810
3794
|
return [
|
|
@@ -3873,31 +3857,20 @@ export default class ProboReporter implements Reporter {
|
|
|
3873
3857
|
this.set(0, 0, 0, 0, 0, 0, 0, 0);
|
|
3874
3858
|
clean(this.buffer);
|
|
3875
3859
|
}
|
|
3876
|
-
constructor(outputLen
|
|
3860
|
+
constructor(outputLen){
|
|
3877
3861
|
super(64, outputLen, 8, false);
|
|
3878
|
-
|
|
3862
|
+
}
|
|
3863
|
+
}
|
|
3864
|
+
/** Internal SHA2-256 hash class. */ class _SHA256 extends SHA2_32B {
|
|
3865
|
+
constructor(){
|
|
3866
|
+
super(32), // We cannot use array here since array allows indexing by variable
|
|
3879
3867
|
// which means optimizer/compiler cannot use registers.
|
|
3880
|
-
this.A = SHA256_IV[0] | 0;
|
|
3881
|
-
this.B = SHA256_IV[1] | 0;
|
|
3882
|
-
this.C = SHA256_IV[2] | 0;
|
|
3883
|
-
this.D = SHA256_IV[3] | 0;
|
|
3884
|
-
this.E = SHA256_IV[4] | 0;
|
|
3885
|
-
this.F = SHA256_IV[5] | 0;
|
|
3886
|
-
this.G = SHA256_IV[6] | 0;
|
|
3887
|
-
this.H = SHA256_IV[7] | 0;
|
|
3868
|
+
this.A = SHA256_IV[0] | 0, this.B = SHA256_IV[1] | 0, this.C = SHA256_IV[2] | 0, this.D = SHA256_IV[3] | 0, this.E = SHA256_IV[4] | 0, this.F = SHA256_IV[5] | 0, this.G = SHA256_IV[6] | 0, this.H = SHA256_IV[7] | 0;
|
|
3888
3869
|
}
|
|
3889
3870
|
}
|
|
3890
|
-
class
|
|
3871
|
+
/** Internal SHA2-224 hash class. */ class _SHA224 extends SHA2_32B {
|
|
3891
3872
|
constructor(){
|
|
3892
|
-
super(28);
|
|
3893
|
-
this.A = SHA224_IV[0] | 0;
|
|
3894
|
-
this.B = SHA224_IV[1] | 0;
|
|
3895
|
-
this.C = SHA224_IV[2] | 0;
|
|
3896
|
-
this.D = SHA224_IV[3] | 0;
|
|
3897
|
-
this.E = SHA224_IV[4] | 0;
|
|
3898
|
-
this.F = SHA224_IV[5] | 0;
|
|
3899
|
-
this.G = SHA224_IV[6] | 0;
|
|
3900
|
-
this.H = SHA224_IV[7] | 0;
|
|
3873
|
+
super(28), this.A = SHA224_IV[0] | 0, this.B = SHA224_IV[1] | 0, this.C = SHA224_IV[2] | 0, this.D = SHA224_IV[3] | 0, this.E = SHA224_IV[4] | 0, this.F = SHA224_IV[5] | 0, this.G = SHA224_IV[6] | 0, this.H = SHA224_IV[7] | 0;
|
|
3901
3874
|
}
|
|
3902
3875
|
}
|
|
3903
3876
|
// SHA2-512 is slower than sha256 in js because u64 operations are slow.
|
|
@@ -3991,7 +3964,7 @@ export default class ProboReporter implements Reporter {
|
|
|
3991
3964
|
// Reusable temporary buffers
|
|
3992
3965
|
const SHA512_W_H = /* @__PURE__ */ new Uint32Array(80);
|
|
3993
3966
|
const SHA512_W_L = /* @__PURE__ */ new Uint32Array(80);
|
|
3994
|
-
class
|
|
3967
|
+
/** Internal 64-byte base SHA2 hash class. */ class SHA2_64B extends HashMD {
|
|
3995
3968
|
// prettier-ignore
|
|
3996
3969
|
get() {
|
|
3997
3970
|
const { Ah, Al, Bh, Bl, Ch, Cl, Dh, Dl, Eh, El, Fh, Fl, Gh, Gl, Hh, Hl } = this;
|
|
@@ -4110,60 +4083,31 @@ export default class ProboReporter implements Reporter {
|
|
|
4110
4083
|
clean(this.buffer);
|
|
4111
4084
|
this.set(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
|
|
4112
4085
|
}
|
|
4113
|
-
constructor(outputLen
|
|
4086
|
+
constructor(outputLen){
|
|
4114
4087
|
super(128, outputLen, 16, false);
|
|
4115
|
-
// We cannot use array here since array allows indexing by variable
|
|
4116
|
-
// which means optimizer/compiler cannot use registers.
|
|
4117
|
-
// h -- high 32 bits, l -- low 32 bits
|
|
4118
|
-
this.Ah = SHA512_IV[0] | 0;
|
|
4119
|
-
this.Al = SHA512_IV[1] | 0;
|
|
4120
|
-
this.Bh = SHA512_IV[2] | 0;
|
|
4121
|
-
this.Bl = SHA512_IV[3] | 0;
|
|
4122
|
-
this.Ch = SHA512_IV[4] | 0;
|
|
4123
|
-
this.Cl = SHA512_IV[5] | 0;
|
|
4124
|
-
this.Dh = SHA512_IV[6] | 0;
|
|
4125
|
-
this.Dl = SHA512_IV[7] | 0;
|
|
4126
|
-
this.Eh = SHA512_IV[8] | 0;
|
|
4127
|
-
this.El = SHA512_IV[9] | 0;
|
|
4128
|
-
this.Fh = SHA512_IV[10] | 0;
|
|
4129
|
-
this.Fl = SHA512_IV[11] | 0;
|
|
4130
|
-
this.Gh = SHA512_IV[12] | 0;
|
|
4131
|
-
this.Gl = SHA512_IV[13] | 0;
|
|
4132
|
-
this.Hh = SHA512_IV[14] | 0;
|
|
4133
|
-
this.Hl = SHA512_IV[15] | 0;
|
|
4134
4088
|
}
|
|
4135
4089
|
}
|
|
4136
|
-
class
|
|
4090
|
+
/** Internal SHA2-512 hash class. */ class _SHA512 extends SHA2_64B {
|
|
4091
|
+
constructor(){
|
|
4092
|
+
super(64), this.Ah = SHA512_IV[0] | 0, this.Al = SHA512_IV[1] | 0, this.Bh = SHA512_IV[2] | 0, this.Bl = SHA512_IV[3] | 0, this.Ch = SHA512_IV[4] | 0, this.Cl = SHA512_IV[5] | 0, this.Dh = SHA512_IV[6] | 0, this.Dl = SHA512_IV[7] | 0, this.Eh = SHA512_IV[8] | 0, this.El = SHA512_IV[9] | 0, this.Fh = SHA512_IV[10] | 0, this.Fl = SHA512_IV[11] | 0, this.Gh = SHA512_IV[12] | 0, this.Gl = SHA512_IV[13] | 0, this.Hh = SHA512_IV[14] | 0, this.Hl = SHA512_IV[15] | 0;
|
|
4093
|
+
}
|
|
4094
|
+
}
|
|
4095
|
+
/** Internal SHA2-384 hash class. */ class _SHA384 extends SHA2_64B {
|
|
4137
4096
|
constructor(){
|
|
4138
|
-
super(48);
|
|
4139
|
-
this.Ah = SHA384_IV[0] | 0;
|
|
4140
|
-
this.Al = SHA384_IV[1] | 0;
|
|
4141
|
-
this.Bh = SHA384_IV[2] | 0;
|
|
4142
|
-
this.Bl = SHA384_IV[3] | 0;
|
|
4143
|
-
this.Ch = SHA384_IV[4] | 0;
|
|
4144
|
-
this.Cl = SHA384_IV[5] | 0;
|
|
4145
|
-
this.Dh = SHA384_IV[6] | 0;
|
|
4146
|
-
this.Dl = SHA384_IV[7] | 0;
|
|
4147
|
-
this.Eh = SHA384_IV[8] | 0;
|
|
4148
|
-
this.El = SHA384_IV[9] | 0;
|
|
4149
|
-
this.Fh = SHA384_IV[10] | 0;
|
|
4150
|
-
this.Fl = SHA384_IV[11] | 0;
|
|
4151
|
-
this.Gh = SHA384_IV[12] | 0;
|
|
4152
|
-
this.Gl = SHA384_IV[13] | 0;
|
|
4153
|
-
this.Hh = SHA384_IV[14] | 0;
|
|
4154
|
-
this.Hl = SHA384_IV[15] | 0;
|
|
4097
|
+
super(48), this.Ah = SHA384_IV[0] | 0, this.Al = SHA384_IV[1] | 0, this.Bh = SHA384_IV[2] | 0, this.Bl = SHA384_IV[3] | 0, this.Ch = SHA384_IV[4] | 0, this.Cl = SHA384_IV[5] | 0, this.Dh = SHA384_IV[6] | 0, this.Dl = SHA384_IV[7] | 0, this.Eh = SHA384_IV[8] | 0, this.El = SHA384_IV[9] | 0, this.Fh = SHA384_IV[10] | 0, this.Fl = SHA384_IV[11] | 0, this.Gh = SHA384_IV[12] | 0, this.Gl = SHA384_IV[13] | 0, this.Hh = SHA384_IV[14] | 0, this.Hl = SHA384_IV[15] | 0;
|
|
4155
4098
|
}
|
|
4156
4099
|
}
|
|
4157
4100
|
/**
|
|
4158
|
-
* SHA2-256 hash function from RFC 4634.
|
|
4101
|
+
* SHA2-256 hash function from RFC 4634. In JS it's the fastest: even faster than Blake3. Some info:
|
|
4159
4102
|
*
|
|
4160
|
-
*
|
|
4161
|
-
*
|
|
4162
|
-
*
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
/** SHA2-
|
|
4166
|
-
/** SHA2-
|
|
4103
|
+
* - Trying 2^128 hashes would get 50% chance of collision, using birthday attack.
|
|
4104
|
+
* - BTC network is doing 2^70 hashes/sec (2^95 hashes/year) as per 2025.
|
|
4105
|
+
* - Each sha256 hash is executing 2^18 bit operations.
|
|
4106
|
+
* - Good 2024 ASICs can do 200Th/sec with 3500 watts of power, corresponding to 2^36 hashes/joule.
|
|
4107
|
+
*/ const sha256 = /* @__PURE__ */ createHasher(()=>new _SHA256(), /* @__PURE__ */ oidNist(0x01));
|
|
4108
|
+
/** SHA2-224 hash function from RFC 4634 */ const sha224 = /* @__PURE__ */ createHasher(()=>new _SHA224(), /* @__PURE__ */ oidNist(0x04));
|
|
4109
|
+
/** SHA2-512 hash function from RFC 4634. */ const sha512 = /* @__PURE__ */ createHasher(()=>new _SHA512(), /* @__PURE__ */ oidNist(0x03));
|
|
4110
|
+
/** SHA2-384 hash function from RFC 4634. */ const sha384 = /* @__PURE__ */ createHasher(()=>new _SHA384(), /* @__PURE__ */ oidNist(0x02));
|
|
4167
4111
|
|
|
4168
4112
|
// No __PURE__ annotations in sha3 header:
|
|
4169
4113
|
// EVERYTHING is in fact used on every export.
|
|
@@ -4176,7 +4120,7 @@ export default class ProboReporter implements Reporter {
|
|
|
4176
4120
|
const _0x71n = BigInt(0x71);
|
|
4177
4121
|
const SHA3_PI = [];
|
|
4178
4122
|
const SHA3_ROTL = [];
|
|
4179
|
-
const _SHA3_IOTA = [];
|
|
4123
|
+
const _SHA3_IOTA = []; // no pure annotation: var is always used
|
|
4180
4124
|
for(let round = 0, R = _1n, x = 1, y = 0; round < 24; round++){
|
|
4181
4125
|
// Pi
|
|
4182
4126
|
[x, y] = [
|
|
@@ -4190,7 +4134,7 @@ export default class ProboReporter implements Reporter {
|
|
|
4190
4134
|
let t = _0n;
|
|
4191
4135
|
for(let j = 0; j < 7; j++){
|
|
4192
4136
|
R = (R << _1n ^ (R >> _7n) * _0x71n) % _256n;
|
|
4193
|
-
if (R & _2n) t ^= _1n << (_1n <<
|
|
4137
|
+
if (R & _2n) t ^= _1n << (_1n << BigInt(j)) - _1n;
|
|
4194
4138
|
}
|
|
4195
4139
|
_SHA3_IOTA.push(t);
|
|
4196
4140
|
}
|
|
@@ -4242,7 +4186,7 @@ export default class ProboReporter implements Reporter {
|
|
|
4242
4186
|
}
|
|
4243
4187
|
clean(B);
|
|
4244
4188
|
}
|
|
4245
|
-
/** Keccak sponge function. */ class Keccak
|
|
4189
|
+
/** Keccak sponge function. */ class Keccak {
|
|
4246
4190
|
clone() {
|
|
4247
4191
|
return this._cloneInto();
|
|
4248
4192
|
}
|
|
@@ -4255,7 +4199,6 @@ export default class ProboReporter implements Reporter {
|
|
|
4255
4199
|
}
|
|
4256
4200
|
update(data) {
|
|
4257
4201
|
aexists(this);
|
|
4258
|
-
data = toBytes(data);
|
|
4259
4202
|
abytes(data);
|
|
4260
4203
|
const { blockLen, state } = this;
|
|
4261
4204
|
const len = data.length;
|
|
@@ -4331,7 +4274,6 @@ export default class ProboReporter implements Reporter {
|
|
|
4331
4274
|
}
|
|
4332
4275
|
// NOTE: we accept arguments in bytes instead of bits here.
|
|
4333
4276
|
constructor(blockLen, suffix, outputLen, enableXOF = false, rounds = 24){
|
|
4334
|
-
super();
|
|
4335
4277
|
this.pos = 0;
|
|
4336
4278
|
this.posOut = 0;
|
|
4337
4279
|
this.finished = false;
|
|
@@ -4343,7 +4285,7 @@ export default class ProboReporter implements Reporter {
|
|
|
4343
4285
|
this.enableXOF = enableXOF;
|
|
4344
4286
|
this.rounds = rounds;
|
|
4345
4287
|
// Can be passed from user as dkLen
|
|
4346
|
-
anumber(outputLen);
|
|
4288
|
+
anumber(outputLen, 'outputLen');
|
|
4347
4289
|
// 1600 = 5x5 matrix of 64bit. 1600 bits === 200 bytes
|
|
4348
4290
|
// 0 < blockLen < 200
|
|
4349
4291
|
if (!(0 < blockLen && blockLen < 200)) throw new Error('only keccak-f1600 function is supported');
|
|
@@ -4351,11 +4293,11 @@ export default class ProboReporter implements Reporter {
|
|
|
4351
4293
|
this.state32 = u32(this.state);
|
|
4352
4294
|
}
|
|
4353
4295
|
}
|
|
4354
|
-
const
|
|
4355
|
-
/** SHA3-224 hash function. */ const sha3_224 = /* @__PURE__ */ (
|
|
4356
|
-
/** SHA3-256 hash function. Different from keccak-256. */ const sha3_256 = /* @__PURE__ */ (
|
|
4357
|
-
/** SHA3-384 hash function. */ const sha3_384 = /* @__PURE__ */ (
|
|
4358
|
-
/** SHA3-512 hash function. */ const sha3_512 = /* @__PURE__ */ (
|
|
4296
|
+
const genKeccak = (suffix, blockLen, outputLen, info = {})=>createHasher(()=>new Keccak(blockLen, suffix, outputLen), info);
|
|
4297
|
+
/** SHA3-224 hash function. */ const sha3_224 = /* @__PURE__ */ genKeccak(0x06, 144, 28, /* @__PURE__ */ oidNist(0x07));
|
|
4298
|
+
/** SHA3-256 hash function. Different from keccak-256. */ const sha3_256 = /* @__PURE__ */ genKeccak(0x06, 136, 32, /* @__PURE__ */ oidNist(0x08));
|
|
4299
|
+
/** SHA3-384 hash function. */ const sha3_384 = /* @__PURE__ */ genKeccak(0x06, 104, 48, /* @__PURE__ */ oidNist(0x09));
|
|
4300
|
+
/** SHA3-512 hash function. */ const sha3_512 = /* @__PURE__ */ genKeccak(0x06, 72, 64, /* @__PURE__ */ oidNist(0x0a));
|
|
4359
4301
|
|
|
4360
4302
|
/**
|
|
4361
4303
|
* "globalThis" ponyfill.
|
|
@@ -4759,9 +4701,14 @@ export default class ProboReporter implements Reporter {
|
|
|
4759
4701
|
* @param {string} [config.algorithm='SHA1'] HMAC hashing algorithm.
|
|
4760
4702
|
* @param {number} [config.digits=6] Token length.
|
|
4761
4703
|
* @param {number} [config.counter=0] Counter value.
|
|
4704
|
+
* @param {(algorithm: string, key: Uint8Array, message: Uint8Array) => Uint8Array} [config.hmac] Custom HMAC function.
|
|
4762
4705
|
* @returns {string} Token.
|
|
4763
|
-
*/ static generate({ secret, algorithm = HOTP.defaults.algorithm, digits = HOTP.defaults.digits, counter = HOTP.defaults.counter }) {
|
|
4764
|
-
const
|
|
4706
|
+
*/ static generate({ secret, algorithm = HOTP.defaults.algorithm, digits = HOTP.defaults.digits, counter = HOTP.defaults.counter, hmac = hmacDigest }) {
|
|
4707
|
+
const message = uintDecode(counter);
|
|
4708
|
+
const digest = hmac(algorithm, secret.bytes, message);
|
|
4709
|
+
if (!digest?.byteLength || digest.byteLength < 19) {
|
|
4710
|
+
throw new TypeError("Return value must be at least 19 bytes");
|
|
4711
|
+
}
|
|
4765
4712
|
const offset = digest[digest.byteLength - 1] & 15;
|
|
4766
4713
|
const otp = ((digest[offset] & 127) << 24 | (digest[offset + 1] & 255) << 16 | (digest[offset + 2] & 255) << 8 | digest[offset + 3] & 255) % 10 ** digits;
|
|
4767
4714
|
return otp.toString().padStart(digits, "0");
|
|
@@ -4776,7 +4723,8 @@ export default class ProboReporter implements Reporter {
|
|
|
4776
4723
|
secret: this.secret,
|
|
4777
4724
|
algorithm: this.algorithm,
|
|
4778
4725
|
digits: this.digits,
|
|
4779
|
-
counter
|
|
4726
|
+
counter,
|
|
4727
|
+
hmac: this.hmac
|
|
4780
4728
|
});
|
|
4781
4729
|
}
|
|
4782
4730
|
/**
|
|
@@ -4788,8 +4736,9 @@ export default class ProboReporter implements Reporter {
|
|
|
4788
4736
|
* @param {number} [config.digits=6] Token length.
|
|
4789
4737
|
* @param {number} [config.counter=0] Counter value.
|
|
4790
4738
|
* @param {number} [config.window=1] Window of counter values to test.
|
|
4739
|
+
* @param {(algorithm: string, key: Uint8Array, message: Uint8Array) => Uint8Array} [config.hmac] Custom HMAC function.
|
|
4791
4740
|
* @returns {number|null} Token delta or null if it is not found in the search window, in which case it should be considered invalid.
|
|
4792
|
-
*/ static validate({ token, secret, algorithm, digits = HOTP.defaults.digits, counter = HOTP.defaults.counter, window = HOTP.defaults.window }) {
|
|
4741
|
+
*/ static validate({ token, secret, algorithm, digits = HOTP.defaults.digits, counter = HOTP.defaults.counter, window = HOTP.defaults.window, hmac = hmacDigest }) {
|
|
4793
4742
|
// Return early if the token length does not match the digit number.
|
|
4794
4743
|
if (token.length !== digits) return null;
|
|
4795
4744
|
let delta = null;
|
|
@@ -4798,7 +4747,8 @@ export default class ProboReporter implements Reporter {
|
|
|
4798
4747
|
secret,
|
|
4799
4748
|
algorithm,
|
|
4800
4749
|
digits,
|
|
4801
|
-
counter: i
|
|
4750
|
+
counter: i,
|
|
4751
|
+
hmac
|
|
4802
4752
|
});
|
|
4803
4753
|
if (timingSafeEqual(token, generatedToken)) {
|
|
4804
4754
|
delta = i - counter;
|
|
@@ -4827,7 +4777,8 @@ export default class ProboReporter implements Reporter {
|
|
|
4827
4777
|
algorithm: this.algorithm,
|
|
4828
4778
|
digits: this.digits,
|
|
4829
4779
|
counter,
|
|
4830
|
-
window
|
|
4780
|
+
window,
|
|
4781
|
+
hmac: this.hmac
|
|
4831
4782
|
});
|
|
4832
4783
|
}
|
|
4833
4784
|
/**
|
|
@@ -4847,7 +4798,8 @@ export default class ProboReporter implements Reporter {
|
|
|
4847
4798
|
* @param {string} [config.algorithm='SHA1'] HMAC hashing algorithm.
|
|
4848
4799
|
* @param {number} [config.digits=6] Token length.
|
|
4849
4800
|
* @param {number} [config.counter=0] Initial counter value.
|
|
4850
|
-
|
|
4801
|
+
* @param {(algorithm: string, key: Uint8Array, message: Uint8Array) => Uint8Array} [config.hmac] Custom HMAC function.
|
|
4802
|
+
*/ constructor({ issuer = HOTP.defaults.issuer, label = HOTP.defaults.label, issuerInLabel = HOTP.defaults.issuerInLabel, secret = new Secret(), algorithm = HOTP.defaults.algorithm, digits = HOTP.defaults.digits, counter = HOTP.defaults.counter, hmac } = {}){
|
|
4851
4803
|
/**
|
|
4852
4804
|
* Account provider.
|
|
4853
4805
|
* @type {string}
|
|
@@ -4867,7 +4819,7 @@ export default class ProboReporter implements Reporter {
|
|
|
4867
4819
|
/**
|
|
4868
4820
|
* HMAC hashing algorithm.
|
|
4869
4821
|
* @type {string}
|
|
4870
|
-
*/ this.algorithm = canonicalizeAlgorithm(algorithm);
|
|
4822
|
+
*/ this.algorithm = hmac ? algorithm : canonicalizeAlgorithm(algorithm);
|
|
4871
4823
|
/**
|
|
4872
4824
|
* Token length.
|
|
4873
4825
|
* @type {number}
|
|
@@ -4876,6 +4828,10 @@ export default class ProboReporter implements Reporter {
|
|
|
4876
4828
|
* Initial counter value.
|
|
4877
4829
|
* @type {number}
|
|
4878
4830
|
*/ this.counter = counter;
|
|
4831
|
+
/**
|
|
4832
|
+
* Custom HMAC function.
|
|
4833
|
+
* @type {((algorithm: string, key: Uint8Array, message: Uint8Array) => Uint8Array)|undefined}
|
|
4834
|
+
*/ this.hmac = hmac;
|
|
4879
4835
|
}
|
|
4880
4836
|
}
|
|
4881
4837
|
|
|
@@ -4953,8 +4909,9 @@ export default class ProboReporter implements Reporter {
|
|
|
4953
4909
|
* @param {number} [config.digits=6] Token length.
|
|
4954
4910
|
* @param {number} [config.period=30] Token time-step duration.
|
|
4955
4911
|
* @param {number} [config.timestamp=Date.now] Timestamp value in milliseconds.
|
|
4912
|
+
* @param {(algorithm: string, key: Uint8Array, message: Uint8Array) => Uint8Array} [config.hmac] Custom HMAC function.
|
|
4956
4913
|
* @returns {string} Token.
|
|
4957
|
-
*/ static generate({ secret, algorithm, digits, period = TOTP.defaults.period, timestamp = Date.now() }) {
|
|
4914
|
+
*/ static generate({ secret, algorithm, digits, period = TOTP.defaults.period, timestamp = Date.now(), hmac }) {
|
|
4958
4915
|
return HOTP.generate({
|
|
4959
4916
|
secret,
|
|
4960
4917
|
algorithm,
|
|
@@ -4962,7 +4919,8 @@ export default class ProboReporter implements Reporter {
|
|
|
4962
4919
|
counter: TOTP.counter({
|
|
4963
4920
|
period,
|
|
4964
4921
|
timestamp
|
|
4965
|
-
})
|
|
4922
|
+
}),
|
|
4923
|
+
hmac
|
|
4966
4924
|
});
|
|
4967
4925
|
}
|
|
4968
4926
|
/**
|
|
@@ -4976,7 +4934,8 @@ export default class ProboReporter implements Reporter {
|
|
|
4976
4934
|
algorithm: this.algorithm,
|
|
4977
4935
|
digits: this.digits,
|
|
4978
4936
|
period: this.period,
|
|
4979
|
-
timestamp
|
|
4937
|
+
timestamp,
|
|
4938
|
+
hmac: this.hmac
|
|
4980
4939
|
});
|
|
4981
4940
|
}
|
|
4982
4941
|
/**
|
|
@@ -4989,8 +4948,9 @@ export default class ProboReporter implements Reporter {
|
|
|
4989
4948
|
* @param {number} [config.period=30] Token time-step duration.
|
|
4990
4949
|
* @param {number} [config.timestamp=Date.now] Timestamp value in milliseconds.
|
|
4991
4950
|
* @param {number} [config.window=1] Window of counter values to test.
|
|
4951
|
+
* @param {(algorithm: string, key: Uint8Array, message: Uint8Array) => Uint8Array} [config.hmac] Custom HMAC function.
|
|
4992
4952
|
* @returns {number|null} Token delta or null if it is not found in the search window, in which case it should be considered invalid.
|
|
4993
|
-
*/ static validate({ token, secret, algorithm, digits, period = TOTP.defaults.period, timestamp = Date.now(), window }) {
|
|
4953
|
+
*/ static validate({ token, secret, algorithm, digits, period = TOTP.defaults.period, timestamp = Date.now(), window, hmac }) {
|
|
4994
4954
|
return HOTP.validate({
|
|
4995
4955
|
token,
|
|
4996
4956
|
secret,
|
|
@@ -5000,7 +4960,8 @@ export default class ProboReporter implements Reporter {
|
|
|
5000
4960
|
period,
|
|
5001
4961
|
timestamp
|
|
5002
4962
|
}),
|
|
5003
|
-
window
|
|
4963
|
+
window,
|
|
4964
|
+
hmac
|
|
5004
4965
|
});
|
|
5005
4966
|
}
|
|
5006
4967
|
/**
|
|
@@ -5018,7 +4979,8 @@ export default class ProboReporter implements Reporter {
|
|
|
5018
4979
|
digits: this.digits,
|
|
5019
4980
|
period: this.period,
|
|
5020
4981
|
timestamp,
|
|
5021
|
-
window
|
|
4982
|
+
window,
|
|
4983
|
+
hmac: this.hmac
|
|
5022
4984
|
});
|
|
5023
4985
|
}
|
|
5024
4986
|
/**
|
|
@@ -5038,7 +5000,8 @@ export default class ProboReporter implements Reporter {
|
|
|
5038
5000
|
* @param {string} [config.algorithm='SHA1'] HMAC hashing algorithm.
|
|
5039
5001
|
* @param {number} [config.digits=6] Token length.
|
|
5040
5002
|
* @param {number} [config.period=30] Token time-step duration.
|
|
5041
|
-
|
|
5003
|
+
* @param {(algorithm: string, key: Uint8Array, message: Uint8Array) => Uint8Array} [config.hmac] Custom HMAC function.
|
|
5004
|
+
*/ constructor({ issuer = TOTP.defaults.issuer, label = TOTP.defaults.label, issuerInLabel = TOTP.defaults.issuerInLabel, secret = new Secret(), algorithm = TOTP.defaults.algorithm, digits = TOTP.defaults.digits, period = TOTP.defaults.period, hmac } = {}){
|
|
5042
5005
|
/**
|
|
5043
5006
|
* Account provider.
|
|
5044
5007
|
* @type {string}
|
|
@@ -5058,7 +5021,7 @@ export default class ProboReporter implements Reporter {
|
|
|
5058
5021
|
/**
|
|
5059
5022
|
* HMAC hashing algorithm.
|
|
5060
5023
|
* @type {string}
|
|
5061
|
-
*/ this.algorithm = canonicalizeAlgorithm(algorithm);
|
|
5024
|
+
*/ this.algorithm = hmac ? algorithm : canonicalizeAlgorithm(algorithm);
|
|
5062
5025
|
/**
|
|
5063
5026
|
* Token length.
|
|
5064
5027
|
* @type {number}
|
|
@@ -5067,6 +5030,10 @@ export default class ProboReporter implements Reporter {
|
|
|
5067
5030
|
* Token time-step duration.
|
|
5068
5031
|
* @type {number}
|
|
5069
5032
|
*/ this.period = period;
|
|
5033
|
+
/**
|
|
5034
|
+
* Custom HMAC function.
|
|
5035
|
+
* @type {((algorithm: string, key: Uint8Array, message: Uint8Array) => Uint8Array)|undefined}
|
|
5036
|
+
*/ this.hmac = hmac;
|
|
5070
5037
|
}
|
|
5071
5038
|
}
|
|
5072
5039
|
|
|
@@ -6377,6 +6344,57 @@ export default class ProboReporter implements Reporter {
|
|
|
6377
6344
|
}
|
|
6378
6345
|
|
|
6379
6346
|
const execAsync = util.promisify(child_process.exec);
|
|
6347
|
+
/** Inlined README for exported projects and download zip (keep in sync with export-readme-template.md) */
|
|
6348
|
+
const EXPORT_README_TEMPLATE = `# Probo Labs - Playwright Test Runner
|
|
6349
|
+
|
|
6350
|
+
This package allows you to easily run automated tests using Playwright, **without needing administrator rights**.
|
|
6351
|
+
|
|
6352
|
+
## 📁 Included Files
|
|
6353
|
+
|
|
6354
|
+
- \`tests/<YourTest>.spec.ts\` – your Playwright test file(s).
|
|
6355
|
+
- \`playwright.config.ts\` _(optional)_ – your custom Playwright configuration file.
|
|
6356
|
+
|
|
6357
|
+
## ✅ How to Use
|
|
6358
|
+
|
|
6359
|
+
1. npm install
|
|
6360
|
+
2. npx playwright install chromium
|
|
6361
|
+
3. Set the required environment variable \`PROBO_API_KEY\` (and optionally \`PROBO_API_ENDPOINT\`, which defaults to \`https://api.probolabs.ai\`):
|
|
6362
|
+
- **Windows (PowerShell)**
|
|
6363
|
+
\`\`\`
|
|
6364
|
+
setx PROBO_API_KEY "<your-api-key>"
|
|
6365
|
+
\`\`\`
|
|
6366
|
+
Restart your terminal (or log out and back in) so the new value loads.
|
|
6367
|
+
- **macOS / Linux (bash, zsh, etc.)**
|
|
6368
|
+
\`\`\`
|
|
6369
|
+
export PROBO_API_KEY="<your-api-key>"
|
|
6370
|
+
\`\`\`
|
|
6371
|
+
Add that line to your shell profile (e.g., \`~/.bashrc\`, \`~/.zshrc\`) to persist it. To use a different API endpoint, set \`PROBO_API_ENDPOINT\` as well.
|
|
6372
|
+
For a one-off run: \`PROBO_API_KEY="<your-api-key>" npm test\`
|
|
6373
|
+
4. Run the single generated test:
|
|
6374
|
+
\`\`\`
|
|
6375
|
+
npm test
|
|
6376
|
+
\`\`\`
|
|
6377
|
+
5. Run the full test suite:
|
|
6378
|
+
\`\`\`
|
|
6379
|
+
npx playwright test
|
|
6380
|
+
\`\`\`
|
|
6381
|
+
|
|
6382
|
+
## 🔐 Setting Secrets in CI/CD
|
|
6383
|
+
|
|
6384
|
+
When using secrets in your tests, you need to configure them in your CI/CD platform. Here's how to set them up for different platforms:
|
|
6385
|
+
|
|
6386
|
+
### GitHub Actions
|
|
6387
|
+
Go to your repository → **Settings** → **Secrets and variables** → **Actions** → **New repository secret**
|
|
6388
|
+
|
|
6389
|
+
### GitLab CI/CD
|
|
6390
|
+
Go to your project → **Settings** → **CI/CD** → **Variables** → **Expand** → **Add variable**
|
|
6391
|
+
|
|
6392
|
+
### Azure DevOps
|
|
6393
|
+
Go to your project → **Pipelines** → **Library** → **Variable group** → **+ Variable group**
|
|
6394
|
+
|
|
6395
|
+
### Jenkins
|
|
6396
|
+
Go to your Jenkins instance → **Manage Jenkins** → **Manage Credentials** → **Credentials Store**
|
|
6397
|
+
`;
|
|
6380
6398
|
/**
|
|
6381
6399
|
* Ensures a directory exists, creating it recursively if needed
|
|
6382
6400
|
*/
|
|
@@ -7275,51 +7293,7 @@ export default class ProboReporter implements Reporter {
|
|
|
7275
7293
|
if (codeGenResult.scenarios.length === 0) {
|
|
7276
7294
|
throw new Error(`No scenarios found in project "${codeGenResult.projectName}" (ID: ${projectId}). Cannot generate test files.`);
|
|
7277
7295
|
}
|
|
7278
|
-
//
|
|
7279
|
-
if (fs__namespace.existsSync(projectDir)) {
|
|
7280
|
-
try {
|
|
7281
|
-
const nodeModulesPath = path__namespace.join(projectDir, 'node_modules');
|
|
7282
|
-
const hasNodeModules = fs__namespace.existsSync(nodeModulesPath);
|
|
7283
|
-
// Temporarily move node_modules out of the way if it exists
|
|
7284
|
-
let tempNodeModulesPath = null;
|
|
7285
|
-
if (hasNodeModules) {
|
|
7286
|
-
tempNodeModulesPath = path__namespace.join(projectDir, '..', `node_modules.temp.${projectId}`);
|
|
7287
|
-
// Remove temp directory if it exists from a previous failed run
|
|
7288
|
-
if (fs__namespace.existsSync(tempNodeModulesPath)) {
|
|
7289
|
-
fs__namespace.rmSync(tempNodeModulesPath, { recursive: true, force: true });
|
|
7290
|
-
}
|
|
7291
|
-
fs__namespace.renameSync(nodeModulesPath, tempNodeModulesPath);
|
|
7292
|
-
console.log(`📦 Preserved node_modules temporarily`);
|
|
7293
|
-
}
|
|
7294
|
-
// Delete everything in the directory
|
|
7295
|
-
const entries = fs__namespace.readdirSync(projectDir, { withFileTypes: true });
|
|
7296
|
-
for (const entry of entries) {
|
|
7297
|
-
const entryPath = path__namespace.join(projectDir, entry.name);
|
|
7298
|
-
try {
|
|
7299
|
-
if (entry.isDirectory()) {
|
|
7300
|
-
fs__namespace.rmSync(entryPath, { recursive: true, force: true });
|
|
7301
|
-
}
|
|
7302
|
-
else {
|
|
7303
|
-
fs__namespace.unlinkSync(entryPath);
|
|
7304
|
-
}
|
|
7305
|
-
}
|
|
7306
|
-
catch (error) {
|
|
7307
|
-
console.warn(`⚠️ Failed to delete ${entry.name}:`, error);
|
|
7308
|
-
}
|
|
7309
|
-
}
|
|
7310
|
-
console.log(`🗑️ Cleaned project directory (preserved node_modules)`);
|
|
7311
|
-
// Move node_modules back
|
|
7312
|
-
if (hasNodeModules && tempNodeModulesPath) {
|
|
7313
|
-
fs__namespace.renameSync(tempNodeModulesPath, nodeModulesPath);
|
|
7314
|
-
console.log(`📦 Restored node_modules`);
|
|
7315
|
-
}
|
|
7316
|
-
}
|
|
7317
|
-
catch (error) {
|
|
7318
|
-
console.warn(`⚠️ Failed to clean project directory ${projectDir}:`, error);
|
|
7319
|
-
// Continue anyway - we'll overwrite files as needed
|
|
7320
|
-
}
|
|
7321
|
-
}
|
|
7322
|
-
// Ensure directories exist (will recreate after deletion)
|
|
7296
|
+
// Ensure directories exist; overwrite generated files only (do not delete project dir)
|
|
7323
7297
|
ensureDirectoryExists(projectDir);
|
|
7324
7298
|
const testsDir = path__namespace.join(projectDir, 'tests');
|
|
7325
7299
|
ensureDirectoryExists(testsDir);
|
|
@@ -7341,6 +7315,10 @@ export default class ProboReporter implements Reporter {
|
|
|
7341
7315
|
if (includeReporter) {
|
|
7342
7316
|
await this.generateProboReporter(projectDir);
|
|
7343
7317
|
}
|
|
7318
|
+
// Write README.md (same content as download zip; inlined so it works when bundled)
|
|
7319
|
+
const readmePath = path__namespace.join(projectDir, 'README.md');
|
|
7320
|
+
fs__namespace.writeFileSync(readmePath, EXPORT_README_TEMPLATE, 'utf-8');
|
|
7321
|
+
console.log(`✅ Generated README: ${readmePath}`);
|
|
7344
7322
|
}
|
|
7345
7323
|
/**
|
|
7346
7324
|
* Export project code to a directory.
|
|
@@ -7586,11 +7564,7 @@ export default class ProboReporter implements Reporter {
|
|
|
7586
7564
|
proboLogger.error("API key wasn't provided. Pass 'token' or set PROBO_API_KEY");
|
|
7587
7565
|
throw new Error('Probo API key not provided');
|
|
7588
7566
|
}
|
|
7589
|
-
const apiEndPoint = apiUrl || process.env.PROBO_API_ENDPOINT;
|
|
7590
|
-
if (!apiEndPoint) {
|
|
7591
|
-
proboLogger.error("API endpoint wasn't provided. Pass 'apiUrl' or set PROBO_API_ENDPOINT");
|
|
7592
|
-
throw new Error('Probo API endpoint not provided');
|
|
7593
|
-
}
|
|
7567
|
+
const apiEndPoint = apiUrl || process.env.PROBO_API_ENDPOINT || 'https://api.probolabs.ai';
|
|
7594
7568
|
this.highlighter = new Highlighter(enableSmartSelectors, enableConsoleLogs);
|
|
7595
7569
|
this.apiClient = new ApiClient(apiEndPoint, apiKey);
|
|
7596
7570
|
this.debugLevel = debugLevel;
|