@probolabs/playwright 1.0.16 → 1.0.19
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 -0
- package/dist/fixtures.cjs +272 -0
- package/dist/fixtures.cjs.map +1 -0
- package/dist/fixtures.js +178 -9
- package/dist/fixtures.js.map +1 -1
- package/dist/index.cjs +4928 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +115 -58
- package/dist/index.js +2320 -242
- package/dist/index.js.map +1 -1
- package/dist/types/fixtures.d.ts.map +1 -1
- package/dist/types/highlight.d.ts.map +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/replay-utils.d.ts.map +1 -1
- package/package.json +6 -5
package/dist/index.js
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 const ElementTag = {\n CLICKABLE: \"CLICKABLE\", // button, link, toggle switch, checkbox, radio, dropdowns, clickable divs\n FILLABLE: \"FILLABLE\", // input, textarea content_editable, date picker??\n SELECTABLE: \"SELECTABLE\", // select\n NON_INTERACTIVE_ELEMENT: 'NON_INTERACTIVE_ELEMENT',\n };\n\n class ElementInfo {\n constructor(element, index, {tag, type, text, html, xpath, css_selector, bounding_box, iframe_selector, short_css_selector, short_iframe_selector}) {\n this.index = index.toString();\n this.tag = tag;\n this.type = type;\n this.text = text;\n this.html = html;\n this.xpath = xpath;\n this.css_selector = css_selector;\n this.bounding_box = bounding_box;\n this.iframe_selector = iframe_selector;\n this.element = element;\n this.depth = -1;\n this.short_css_selector = short_css_selector;\n this.short_iframe_selector = short_iframe_selector;\n }\n\n getSelector() {\n return this.xpath ? this.xpath : this.css_selector;\n }\n\n getDepth() {\n if (this.depth >= 0) {\n return this.depth;\n }\n \n this.depth = 0;\n let currentElement = this.element;\n \n while (currentElement.nodeType === Node.ELEMENT_NODE) { \n this.depth++;\n currentElement = getParentNode(currentElement);\n }\n \n return this.depth;\n }\n }\n\n function getParentNode(element) {\n if (!element || element.nodeType !== Node.ELEMENT_NODE) return null;\n \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 \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\n return parent;\n }\n\n // License: MIT\n // Author: Anton Medvedev <anton@medv.io>\n // Source: https://github.com/antonmedv/finder\n const acceptedAttrNames = new Set(['role', 'name', 'aria-label', 'rel', 'href']);\n /** Check if attribute name and value are word-like. */\n function attr(name, value) {\n let nameIsOk = acceptedAttrNames.has(name);\n nameIsOk ||= name.startsWith('data-') && wordLike(name);\n let valueIsOk = wordLike(value) && value.length < 100;\n valueIsOk ||= value.startsWith('#') && wordLike(value.slice(1));\n return nameIsOk && valueIsOk;\n }\n /** Check if id name is word-like. */\n function idName(name) {\n return wordLike(name);\n }\n /** Check if class name is word-like. */\n function className(name) {\n return wordLike(name);\n }\n /** Check if tag name is word-like. */\n function tagName(name) {\n return true;\n }\n /** Finds unique CSS selectors for the given element. */\n function finder(input, options) {\n if (input.nodeType !== Node.ELEMENT_NODE) {\n throw new Error(`Can't generate CSS selector for non-element node type.`);\n }\n if (input.tagName.toLowerCase() === 'html') {\n return 'html';\n }\n const defaults = {\n root: document.body,\n idName: idName,\n className: className,\n tagName: tagName,\n attr: attr,\n timeoutMs: 1000,\n seedMinLength: 3,\n optimizedMinLength: 2,\n maxNumberOfPathChecks: Infinity,\n };\n const startTime = new Date();\n const config = { ...defaults, ...options };\n const rootDocument = findRootDocument(config.root, defaults);\n let foundPath;\n let count = 0;\n for (const candidate of search(input, config, rootDocument)) {\n const elapsedTimeMs = new Date().getTime() - startTime.getTime();\n if (elapsedTimeMs > config.timeoutMs ||\n count >= config.maxNumberOfPathChecks) {\n const fPath = fallback(input, rootDocument);\n if (!fPath) {\n throw new Error(`Timeout: Can't find a unique selector after ${config.timeoutMs}ms`);\n }\n return selector(fPath);\n }\n count++;\n if (unique(candidate, rootDocument)) {\n foundPath = candidate;\n break;\n }\n }\n if (!foundPath) {\n throw new Error(`Selector was not found.`);\n }\n const optimized = [\n ...optimize(foundPath, input, config, rootDocument, startTime),\n ];\n optimized.sort(byPenalty);\n if (optimized.length > 0) {\n return selector(optimized[0]);\n }\n return selector(foundPath);\n }\n function* search(input, config, rootDocument) {\n const stack = [];\n let paths = [];\n let current = input;\n let i = 0;\n while (current && current !== rootDocument) {\n const level = tie(current, config);\n for (const node of level) {\n node.level = i;\n }\n stack.push(level);\n current = current.parentElement;\n i++;\n paths.push(...combinations(stack));\n if (i >= config.seedMinLength) {\n paths.sort(byPenalty);\n for (const candidate of paths) {\n yield candidate;\n }\n paths = [];\n }\n }\n paths.sort(byPenalty);\n for (const candidate of paths) {\n yield candidate;\n }\n }\n function wordLike(name) {\n if (/^[a-z\\-]{3,}$/i.test(name)) {\n const words = name.split(/-|[A-Z]/);\n for (const word of words) {\n if (word.length <= 2) {\n return false;\n }\n if (/[^aeiou]{4,}/i.test(word)) {\n return false;\n }\n }\n return true;\n }\n return false;\n }\n function tie(element, config) {\n const level = [];\n const elementId = element.getAttribute('id');\n if (elementId && config.idName(elementId)) {\n level.push({\n name: '#' + CSS.escape(elementId),\n penalty: 0,\n });\n }\n for (let i = 0; i < element.classList.length; i++) {\n const name = element.classList[i];\n if (config.className(name)) {\n level.push({\n name: '.' + CSS.escape(name),\n penalty: 1,\n });\n }\n }\n for (let i = 0; i < element.attributes.length; i++) {\n const attr = element.attributes[i];\n if (config.attr(attr.name, attr.value)) {\n level.push({\n name: `[${CSS.escape(attr.name)}=\"${CSS.escape(attr.value)}\"]`,\n penalty: 2,\n });\n }\n }\n const tagName = element.tagName.toLowerCase();\n if (config.tagName(tagName)) {\n level.push({\n name: tagName,\n penalty: 5,\n });\n const index = indexOf(element, tagName);\n if (index !== undefined) {\n level.push({\n name: nthOfType(tagName, index),\n penalty: 10,\n });\n }\n }\n const nth = indexOf(element);\n if (nth !== undefined) {\n level.push({\n name: nthChild(tagName, nth),\n penalty: 50,\n });\n }\n return level;\n }\n function selector(path) {\n let node = path[0];\n let query = node.name;\n for (let i = 1; i < path.length; i++) {\n const level = path[i].level || 0;\n if (node.level === level - 1) {\n query = `${path[i].name} > ${query}`;\n }\n else {\n query = `${path[i].name} ${query}`;\n }\n node = path[i];\n }\n return query;\n }\n function penalty(path) {\n return path.map((node) => node.penalty).reduce((acc, i) => acc + i, 0);\n }\n function byPenalty(a, b) {\n return penalty(a) - penalty(b);\n }\n function indexOf(input, tagName) {\n const parent = input.parentNode;\n if (!parent) {\n return undefined;\n }\n let child = parent.firstChild;\n if (!child) {\n return undefined;\n }\n let i = 0;\n while (child) {\n if (child.nodeType === Node.ELEMENT_NODE &&\n (tagName === undefined ||\n child.tagName.toLowerCase() === tagName)) {\n i++;\n }\n if (child === input) {\n break;\n }\n child = child.nextSibling;\n }\n return i;\n }\n function fallback(input, rootDocument) {\n let i = 0;\n let current = input;\n const path = [];\n while (current && current !== rootDocument) {\n const tagName = current.tagName.toLowerCase();\n const index = indexOf(current, tagName);\n if (index === undefined) {\n return;\n }\n path.push({\n name: nthOfType(tagName, index),\n penalty: NaN,\n level: i,\n });\n current = current.parentElement;\n i++;\n }\n if (unique(path, rootDocument)) {\n return path;\n }\n }\n function nthChild(tagName, index) {\n if (tagName === 'html') {\n return 'html';\n }\n return `${tagName}:nth-child(${index})`;\n }\n function nthOfType(tagName, index) {\n if (tagName === 'html') {\n return 'html';\n }\n return `${tagName}:nth-of-type(${index})`;\n }\n function* combinations(stack, path = []) {\n if (stack.length > 0) {\n for (let node of stack[0]) {\n yield* combinations(stack.slice(1, stack.length), path.concat(node));\n }\n }\n else {\n yield path;\n }\n }\n function findRootDocument(rootNode, defaults) {\n if (rootNode.nodeType === Node.DOCUMENT_NODE) {\n return rootNode;\n }\n if (rootNode === defaults.root) {\n return rootNode.ownerDocument;\n }\n return rootNode;\n }\n function unique(path, rootDocument) {\n const css = selector(path);\n switch (rootDocument.querySelectorAll(css).length) {\n case 0:\n throw new Error(`Can't select any node with this selector: ${css}`);\n case 1:\n return true;\n default:\n return false;\n }\n }\n function* optimize(path, input, config, rootDocument, startTime) {\n if (path.length > 2 && path.length > config.optimizedMinLength) {\n for (let i = 1; i < path.length - 1; i++) {\n const elapsedTimeMs = new Date().getTime() - startTime.getTime();\n if (elapsedTimeMs > config.timeoutMs) {\n return;\n }\n const newPath = [...path];\n newPath.splice(i, 1);\n if (unique(newPath, rootDocument) &&\n rootDocument.querySelector(selector(newPath)) === input) {\n yield newPath;\n yield* optimize(newPath, input, config, rootDocument, startTime);\n }\n }\n }\n }\n\n // import { realpath } from \"fs\";\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 console.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 * Deeply searches through DOM trees including Shadow DOM and frames/iframes\n * @param {string} selector - CSS selector to search for\n * @param {Document|Element} [root=document] - Starting point for the search\n * @param {Object} [options] - Search options\n * @param {boolean} [options.searchShadow=true] - Whether to search Shadow DOM\n * @param {boolean} [options.searchFrames=true] - Whether to search frames/iframes\n * @returns {Element[]} Array of found elements\n \n function getAllElementsIncludingShadow(selector, root = document, options = {}) {\n const {\n searchShadow = true,\n searchFrames = true\n } = options;\n\n const results = new Set();\n \n // Helper to check if an element is valid and not yet found\n const addIfValid = (element) => {\n if (element && !results.has(element)) {\n results.add(element);\n }\n };\n\n // Helper to process a single document or element\n function processNode(node) {\n // Search regular DOM\n node.querySelectorAll(selector).forEach(addIfValid);\n\n if (searchShadow) {\n // Search all shadow roots\n const treeWalker = document.createTreeWalker(\n node,\n NodeFilter.SHOW_ELEMENT,\n {\n acceptNode: (element) => {\n return element.shadowRoot ? \n NodeFilter.FILTER_ACCEPT : \n NodeFilter.FILTER_SKIP;\n }\n }\n );\n\n while (treeWalker.nextNode()) {\n const element = treeWalker.currentNode;\n if (element.shadowRoot) {\n // Search within shadow root\n element.shadowRoot.querySelectorAll(selector).forEach(addIfValid);\n // Recursively process the shadow root for nested shadow DOMs\n processNode(element.shadowRoot);\n }\n }\n }\n\n if (searchFrames) {\n // Search frames and iframes\n const frames = node.querySelectorAll('frame, iframe');\n frames.forEach(frame => {\n try {\n const frameDocument = frame.contentDocument;\n if (frameDocument) {\n processNode(frameDocument);\n }\n } catch (e) {\n // Skip cross-origin frames\n console.warn('Could not access frame content:', e.message);\n }\n });\n }\n }\n\n // Start processing from the root\n processNode(root);\n\n return Array.from(results);\n }\n */\n // <div x=1 y=2 role='combobox'> </div>\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 // export function findNonInteractiveElements() {\n // const all = [];\n // try {\n // const elements = getAllElementsIncludingShadow('*');\n // all.push(...elements);\n // } catch (e) {\n // console.warn('Error getting elements:', e);\n // }\n \n // console.debug('Total elements found:', all.length);\n \n // return all.filter(element => {\n // try {\n // const tag = element.tagName.toLowerCase(); \n\n // // Special handling for input elements\n // if (tag === 'input' || tag === 'textarea') {\n // const boundingRect = element.getBoundingClientRect();\n // const value = element.value || '';\n // const placeholder = element.placeholder || '';\n // return boundingRect.height > 1 && \n // boundingRect.width > 1 && \n // (value.trim() !== '' || placeholder.trim() !== '');\n // }\n\n \n // // Check if it's a valid tag for text content\n // const validTags = ['p', 'span', 'div', 'label', 'th', 'td', 'li', 'button', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'a', 'select'].includes(tag) || \n // /^h\\d$/.test(tag) || \n // /text/.test(tag);\n\n // const boundingRect = element.getBoundingClientRect();\n\n // // Get direct text content, excluding child element text\n // let directText = '';\n // for (const node of element.childNodes) {\n // // Only include text nodes (nodeType 3)\n // if (node.nodeType === 3) {\n // directText += node.textContent || '';\n // }\n // }\n \n // // If no direct text and it's a table cell or heading, check label content\n // if (!directText.trim() && (tag === 'th' || tag === 'td' || tag === 'h1')) {\n // const labels = element.getElementsByTagName('label');\n // for (const label of labels) {\n // directText += label.textContent || '';\n // }\n // }\n\n // // If still no text and it's a heading, get all text content\n // if (!directText.trim() && tag === 'h1') {\n // directText = element.textContent || '';\n // }\n\n // directText = directText.trim();\n\n // // Debug logging\n // if (directText) {\n // console.debugg('Text element found:', {\n // tag,\n // text: directText,\n // dimensions: boundingRect,\n // element\n // });\n // }\n\n // return validTags && \n // boundingRect.height > 1 && \n // boundingRect.width > 1 && \n // directText !== '';\n \n // } catch (e) {\n // console.warn('Error processing element:', e);\n // return false;\n // }\n // });\n // }\n\n\n\n\n\n function findElementsWithPointer() {\n const elements = [];\n const allElements = getAllElementsIncludingShadow('*');\n \n console.log('Checking elements with pointer style...');\n \n allElements.forEach(element => {\n // Skip SVG elements for now\n if (element instanceof SVGElement || element.tagName.toLowerCase() === 'svg') {\n return;\n }\n \n const style = window.getComputedStyle(element);\n if (style.cursor === 'pointer') {\n elements.push(element);\n }\n });\n \n console.log(`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 console.log('Found inputs:', inputs.length, inputs);\n elements.push(...inputs);\n \n const textareas = [...getAllElementsIncludingShadow('textarea')];\n console.log('Found textareas:', textareas.length);\n elements.push(...textareas);\n \n const editables = [...getAllElementsIncludingShadow('[contenteditable=\"true\"]')];\n console.log('Found editables:', editables.length);\n elements.push(...editables);\n\n return elements;\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 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 * 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 querySelectorShadow(selector, root = document) {\n // First try to find in light DOM\n let element = root.querySelector(selector);\n if (element) return element;\n \n // Get all elements with shadow root\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 element = querySelectorShadow(selector, el.shadowRoot);\n if (element) return element;\n }\n \n return null;\n }\n\n const getElementByXPathOrCssSelector = (element_info) => {\n console.log('getElementByXPathOrCssSelector:', element_info);\n\n findElement(document, element_info.iframe_selector, element_info.css_selector);\n };\n\n const findElement = (root, iframeSelector, cssSelector) => {\n let element;\n \n if (iframeSelector) { \n const frames = getAllDocumentElementsIncludingShadow('iframe', root);\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 === iframeSelector) {\n const frameDocument = frame.contentDocument || frame.contentWindow.document;\n element = querySelectorShadow(cssSelector, frameDocument);\n console.log('found element ', element);\n break;\n } \n } }\n else\n element = querySelectorShadow(cssSelector, root);\n \n if (!element) {\n console.warn('Failed to find element with CSS selector:', cssSelector);\n }\n\n return element;\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 function extractElementPath(element) {\n if (!element) {\n console.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 getElementInfo(element, index) {\n // Get text content with spaces between elements\n /* function getTextContent(element) {\n const walker = document.createTreeWalker(\n element,\n NodeFilter.SHOW_TEXT,\n null,\n false\n );\n\n let text = '';\n let node;\n\n while (node = walker.nextNode()) {\n const trimmedText = node.textContent.trim();\n if (trimmedText) {\n // Add space if there's already text\n if (text) {\n text += ' ';\n }\n text += trimmedText;\n }\n }\n\n return text;\n } */\n \n const xpath = generateXPath(element);\n const css_selector = generateCssPath(element);\n //disabled since it's blocking event handling in recorder\n const short_css_selector = ''; //getRobustSelector(element);\n\n const iframe = getContainingIframe(element); \n const iframe_selector = iframe ? generateCssPath(iframe) : \"\";\n //disabled since it's blocking event handling in recorder\n const short_iframe_selector = ''; //iframe ? getRobustSelector(iframe) : \"\";\n\n // Return element info with pre-calculated values\n return new ElementInfo(element, index, {\n tag: element.tagName.toLowerCase(),\n type: element.type || '',\n text: element.innerText || element.placeholder || '', //getTextContent(element),\n html: cleanHTML(element.outerHTML),\n xpath: xpath,\n css_selector: css_selector,\n bounding_box: element.getBoundingClientRect(),\n iframe_selector: iframe_selector,\n short_css_selector: short_css_selector,\n short_iframe_selector: short_iframe_selector\n });\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 = querySelectorShadow(`#${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\n\n function uniquifyElements(elements) {\n const seen = new Set();\n\n console.log(`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 console.log('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 console.log('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 console.log('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.getDepth() - b.getDepth());\n console.log(`After dimension filtering: ${nonZeroElements.length} elements remain (${elements.length - nonZeroElements.length} removed)`);\n \n const filteredByParent = nonZeroElements.filter(element_info => {\n\n const parent = findClosestParent(seen, element_info);\n const keep = parent == null || shouldKeepNestedElement(element_info, parent);\n // console.log(\"node \", element_info.index, \": keep=\", keep, \" parent=\", parent);\n // if (!keep && !element_info.xpath) {\n // console.log(\"Filtered out element \", element_info,\" because it's a nested element of \", parent);\n // }\n if (keep)\n seen.add(element_info.css_selector);\n\n return keep;\n });\n\n console.log(`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 console.log(`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 console.log(`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(seen, element_info) { \n \n // Split the xpath into segments\n const segments = element_info.css_selector.split(' > ');\n \n // Try increasingly shorter paths until we find one in the seen set\n for (let i = segments.length - 1; i > 0; i--) {\n const parentPath = segments.slice(0, i).join(' > ');\n if (seen.has(parentPath)) {\n return parentPath;\n }\n }\n\n return null;\n }\n\n function shouldKeepNestedElement(elementInfo, parentPath) {\n let result = false;\n const parentSegments = parentPath.split(' > ');\n\n const isParentLink = /^a(:nth-of-type\\(\\d+\\))?$/.test(parentSegments[parentSegments.length - 1]);\n if (isParentLink) {\n return false; \n }\n // If this is a checkbox/radio input\n if (elementInfo.tag === 'input' && \n (elementInfo.type === 'checkbox' || elementInfo.type === 'radio')) {\n \n // Check if parent is a label by looking at the parent xpath's last segment\n \n const isParentLabel = /^label(:nth-of-type\\(\\d+\\))?$/.test(parentSegments[parentSegments.length - 1]);\n \n // If parent is a label, don't keep the input (we'll keep the label instead)\n if (isParentLabel) {\n return false;\n }\n }\n \n // Keep all other form controls and dropdown items\n if (isFormControl(elementInfo) || isDropdownItem(elementInfo)) {\n result = true;\n }\n\n if(isTableCell(elementInfo)) {\n result = true;\n }\n \n \n // console.log(`shouldKeepNestedElement: ${elementInfo.tag} ${elementInfo.text} ${elementInfo.xpath} -> ${parentXPath} -> ${result}`);\n return result;\n }\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 console.log('[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\n /**\n * Get the “best” short, unique, and robust CSS selector for an element.\n * \n * @param {Element} element\n * @returns {string} A selector guaranteed to find exactly that element in its context\n */\n function getRobustSelector(element) {\n // 1. Figure out the real “root” (iframe doc, shadow root, or main doc)\n const root = (() => {\n const rootNode = element.getRootNode();\n if (rootNode instanceof ShadowRoot) {\n return rootNode;\n }\n return element.ownerDocument;\n })();\n\n // 2. Options to bias toward stable attrs and away from auto-generated classes\n const options = {\n root,\n // only use data-*, id or aria-label by default\n attr(name, value) {\n if (name === 'id' || name.startsWith('data-') || name === 'aria-label') {\n return true;\n }\n return false;\n },\n // skip framework junk\n filter(name, value) {\n if (name.startsWith('ng-') || name.startsWith('_ngcontent') || /^p-/.test(name)) {\n return false;\n }\n return true;\n },\n // let finder try really short seeds\n seedMinLength: 1,\n optimizedMinLength: 1,\n };\n\n let selector;\n try {\n selector = finder(element, options);\n // 3. Verify it really works in the context\n const found = root.querySelectorAll(selector);\n if (found.length !== 1 || found[0] !== element) {\n throw new Error('not unique or not found');\n }\n return selector;\n } catch (err) {\n // 4. Fallback: full path (you already have this utility)\n console.warn('[getRobustSelector] finder failed, falling back to full path:', err);\n return generateCssPath(element); // you’d import or define this elsewhere\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 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 /**\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 console.log('🔍 [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 console.log('🔍 [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 = 10; // 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 console.log('🔍 [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 console.log('🔍 [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 /**\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 console.log('✅ 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 console.log('✅ 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 console.log('⚠️ 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 console.log('📏 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 console.log('✅ 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 console.error('❌ Missing required properties in actualInteractionElementInfo');\n return null;\n }\n\n console.log('🔍 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 console.log('❌ 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 console.log('❌ 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 console.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, 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 console.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\n\n async function findElements(elementTypes, verbose=true) {\n const typesArray = Array.isArray(elementTypes) ? elementTypes : [elementTypes];\n console.log('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 console.log(`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 console.log(`Out of which ${visibleElements.length} elements are visible:`);\n if (verbose) {\n visibleElements.forEach(info => {\n console.log(`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, 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; //getElementByXPathOrCssSelector(elementInfo);\n if (!element) {\n element = getElementByXPathOrCssSelector(elementInfo);\n if (!element) {\n console.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 console.log(\"[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 console.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;\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 console.log('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(cssSelector, iframeSelector, isHover=false) {\n // console.log('[ProboLabs] findAndCacheActualElement called with:', cssSelector, iframeSelector);\n let el = findElement(document, iframeSelector, cssSelector);\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 console.log(`[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 console.log('[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 // 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.ElementInfo = ElementInfo;\n exports.ElementTag = ElementTag;\n exports.deserializeNodeFromJSON = deserializeNodeFromJSON;\n exports.detectScrollableContainers = detectScrollableContainers;\n exports.findElement = findElement;\n exports.findElements = findElements;\n exports.generateCssPath = generateCssPath;\n exports.getAriaLabelledByText = getAriaLabelledByText;\n exports.getContainingIframe = getContainingIframe;\n exports.getElementInfo = getElementInfo;\n exports.getParentNode = getParentNode;\n exports.getRobustSelector = getRobustSelector;\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 })(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_STEP_CREATED\"] = \"INTERACTION_STEP_CREATED\";\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 || (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 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\n\n function uniquifyElements(elements) {\n const seen = new Set();\n\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 const filteredByParent = nonZeroElements.filter(element_info => {\n\n const parent = findClosestParent(seen, element_info);\n const keep = parent == null || shouldKeepNestedElement(element_info, parent);\n // console.log(\"node \", element_info.index, \": keep=\", keep, \" parent=\", parent);\n // if (!keep && !element_info.xpath) {\n // console.log(\"Filtered out element \", element_info,\" because it's a nested element of \", parent);\n // }\n if (keep)\n seen.add(element_info.css_selector);\n\n return keep;\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(seen, element_info) { \n \n // Split the xpath into segments\n const segments = element_info.css_selector.split(' > ');\n \n // Try increasingly shorter paths until we find one in the seen set\n for (let i = segments.length - 1; i > 0; i--) {\n const parentPath = segments.slice(0, i).join(' > ');\n if (seen.has(parentPath)) {\n return parentPath;\n }\n }\n\n return null;\n }\n\n function shouldKeepNestedElement(elementInfo, parentPath) {\n let result = false;\n const parentSegments = parentPath.split(' > ');\n\n const isParentLink = /^a(:nth-of-type\\(\\d+\\))?$/.test(parentSegments[parentSegments.length - 1]);\n if (isParentLink) {\n return false; \n }\n // If this is a checkbox/radio input\n if (elementInfo.tag === 'input' && \n (elementInfo.type === 'checkbox' || elementInfo.type === 'radio')) {\n \n // Check if parent is a label by looking at the parent xpath's last segment\n \n const isParentLabel = /^label(:nth-of-type\\(\\d+\\))?$/.test(parentSegments[parentSegments.length - 1]);\n \n // If parent is a label, don't keep the input (we'll keep the label instead)\n if (isParentLabel) {\n return false;\n }\n }\n \n // Keep all other form controls and dropdown items\n if (isFormControl(elementInfo) || isDropdownItem(elementInfo)) {\n result = true;\n }\n\n if(isTableCell(elementInfo)) {\n result = true;\n }\n \n \n // console.log(`shouldKeepNestedElement: ${elementInfo.tag} ${elementInfo.text} ${elementInfo.xpath} -> ${parentXPath} -> ${result}`);\n return result;\n }\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 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 /**\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 = 10; // 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 highlighterLogger.debug('Checking elements with pointer style...');\n \n allElements.forEach(element => {\n // Skip SVG elements for now\n if (element instanceof SVGElement || element.tagName.toLowerCase() === 'svg') {\n return;\n }\n \n const style = window.getComputedStyle(element);\n if (style.cursor === 'pointer') {\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 \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 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) 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 text = cleanText(getElementText(element));\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 = text || title || name || ariaLabel;\n \treturn {\n \t\thasLogicalName: logicalName && logicalName.length < MAX_LOGICAL_NAME_LENGTH && !role?.startsWith('row'),\n \t\tlogicalName: logicalName\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, logicalName } = 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 } = getLogicalName(element);\n \tconst classes = Array.from(element.classList).map(cls => CSS.escape(cls));\n \t\n \t// Only use name strategy if element is clickable/interactive \n \tif (isClickableElement(element) && hasLogicalName && classes.length > 0) {\n \t\treturn { \n \t\t\tstrategy: 'name', \n \t\t\tselector: `.${classes.join('.')}${getNameSelector(element)}` \n \t\t\t// selector: `${element.tagName.toLowerCase()}${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) {\n \t\treturn { \n \t\t\tstrategy: 'data-testid',\t\t\t\n \t\t\tselector: `[data-testid=${doubleQuoteString(dataTestId)}]` \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\treturn { \n \t\t\tstrategy: 'id', \n \t\t\tselector: `#${CSS.escape(id)}` \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\ttryNameStrategy,\n \t\ttryLabelStrategy,\n \t\ttryDataTestIdStrategy,\n \t\ttryPlaceholderStrategy,\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 \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(result?.selector); \n \t\t\t\thighlighterLogger.debug(`querySelectorAllByText(${result?.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 index = matchedElements.findIndex(el => el === element);\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 matchedElements = robustQuerySelector(newChildPath, document, true);\n \tif (matchedElements.length === 1) {\n \t\treturn {\n \t\t\tstrategy: 'css',\t\t\t\t\n \t\t\tselector: newChildPath\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
|
/**
|
|
3
3
|
* Element tag constants for different types of interactive elements
|
|
4
4
|
*/
|
|
@@ -55,6 +55,7 @@ var PlaywrightAction;
|
|
|
55
55
|
PlaywrightAction["UPLOAD_FILES"] = "UPLOAD_FILES";
|
|
56
56
|
PlaywrightAction["WAIT_FOR"] = "WAIT_FOR";
|
|
57
57
|
PlaywrightAction["WAIT_FOR_OTP"] = "WAIT_FOR_OTP";
|
|
58
|
+
PlaywrightAction["GEN_TOTP"] = "GEN_TOTP";
|
|
58
59
|
})(PlaywrightAction || (PlaywrightAction = {}));
|
|
59
60
|
|
|
60
61
|
/**
|
|
@@ -85,6 +86,13 @@ function resolveElementTag(action) {
|
|
|
85
86
|
throw new Error(`Unknown action: ${action}`);
|
|
86
87
|
}
|
|
87
88
|
}
|
|
89
|
+
// clickable element detection result
|
|
90
|
+
var IsClickable;
|
|
91
|
+
(function (IsClickable) {
|
|
92
|
+
IsClickable["YES"] = "YES";
|
|
93
|
+
IsClickable["NO"] = "NO";
|
|
94
|
+
IsClickable["MAYBE"] = "MAYBE";
|
|
95
|
+
})(IsClickable || (IsClickable = {}));
|
|
88
96
|
|
|
89
97
|
// WebSocketsMessageType enum for WebSocket and event message types shared across the app
|
|
90
98
|
var WebSocketsMessageType;
|
|
@@ -131,122 +139,30 @@ class ProboLogger {
|
|
|
131
139
|
this.level = level;
|
|
132
140
|
}
|
|
133
141
|
setLogLevel(level) {
|
|
142
|
+
console.log(`[${this.prefix}] Setting log level to: ${level} (was: ${this.level})`);
|
|
134
143
|
this.level = level;
|
|
135
144
|
}
|
|
136
145
|
shouldLog(level) {
|
|
137
146
|
return logLevelOrder[level] >= logLevelOrder[this.level];
|
|
138
147
|
}
|
|
139
|
-
preamble() {
|
|
148
|
+
preamble(level) {
|
|
140
149
|
const now = new Date();
|
|
141
150
|
const hours = String(now.getHours()).padStart(2, '0');
|
|
142
151
|
const minutes = String(now.getMinutes()).padStart(2, '0');
|
|
143
152
|
const seconds = String(now.getSeconds()).padStart(2, '0');
|
|
144
153
|
const milliseconds = String(now.getMilliseconds()).padStart(3, '0');
|
|
145
|
-
return `[${hours}:${minutes}:${seconds}.${milliseconds}] [${this.prefix}]`;
|
|
154
|
+
return `[${hours}:${minutes}:${seconds}.${milliseconds}] [${this.prefix}] [${level}]`;
|
|
146
155
|
}
|
|
147
156
|
debug(...args) { if (this.shouldLog(ProboLogLevel.DEBUG))
|
|
148
|
-
console.debug(this.preamble(), ...args); }
|
|
157
|
+
console.debug(this.preamble(ProboLogLevel.DEBUG), ...args); }
|
|
149
158
|
info(...args) { if (this.shouldLog(ProboLogLevel.INFO))
|
|
150
|
-
console.info(this.preamble(), ...args); }
|
|
159
|
+
console.info(this.preamble(ProboLogLevel.INFO), ...args); }
|
|
151
160
|
log(...args) { if (this.shouldLog(ProboLogLevel.LOG))
|
|
152
|
-
console.log(this.preamble(), ...args); }
|
|
161
|
+
console.log(this.preamble(ProboLogLevel.LOG), ...args); }
|
|
153
162
|
warn(...args) { if (this.shouldLog(ProboLogLevel.WARN))
|
|
154
|
-
console.warn(this.preamble(), ...args); }
|
|
163
|
+
console.warn(this.preamble(ProboLogLevel.WARN), ...args); }
|
|
155
164
|
error(...args) { if (this.shouldLog(ProboLogLevel.ERROR))
|
|
156
|
-
console.error(this.preamble(), ...args); }
|
|
157
|
-
}
|
|
158
|
-
/**
|
|
159
|
-
* Interfaces for element information (unchanged)
|
|
160
|
-
*/
|
|
161
|
-
/* export interface ElementInfo {
|
|
162
|
-
index: string;
|
|
163
|
-
tag: string;
|
|
164
|
-
type: string;
|
|
165
|
-
text: string;
|
|
166
|
-
html: string;
|
|
167
|
-
xpath: string;
|
|
168
|
-
css_selector: string;
|
|
169
|
-
bounding_box: {
|
|
170
|
-
x: number;
|
|
171
|
-
y: number;
|
|
172
|
-
width: number;
|
|
173
|
-
height: number;
|
|
174
|
-
top: number;
|
|
175
|
-
right: number;
|
|
176
|
-
bottom: number;
|
|
177
|
-
left: number;
|
|
178
|
-
};
|
|
179
|
-
iframe_selector: string;
|
|
180
|
-
element: any;
|
|
181
|
-
depth?: number;
|
|
182
|
-
getSelector(): string;
|
|
183
|
-
getDepth(): number;
|
|
184
|
-
}
|
|
185
|
-
*/
|
|
186
|
-
/* export interface CleanElementInfo {
|
|
187
|
-
index: string;
|
|
188
|
-
tag: string;
|
|
189
|
-
type: string;
|
|
190
|
-
text: string;
|
|
191
|
-
html: string;
|
|
192
|
-
xpath: string;
|
|
193
|
-
css_selector: string;
|
|
194
|
-
iframe_selector: string;
|
|
195
|
-
bounding_box: {
|
|
196
|
-
x: number;
|
|
197
|
-
y: number;
|
|
198
|
-
width: number;
|
|
199
|
-
height: number;
|
|
200
|
-
top: number;
|
|
201
|
-
right: number;
|
|
202
|
-
bottom: number;
|
|
203
|
-
left: number;
|
|
204
|
-
};
|
|
205
|
-
depth: number;
|
|
206
|
-
} */
|
|
207
|
-
// Element cleaner logging
|
|
208
|
-
const elementLogger = new ProboLogger('element-cleaner');
|
|
209
|
-
/**
|
|
210
|
-
* Cleans and returns a minimal element info structure.
|
|
211
|
-
*/
|
|
212
|
-
function cleanupElementInfo(elementInfo) {
|
|
213
|
-
var _a;
|
|
214
|
-
elementLogger.debug(`Cleaning up element info for ${elementInfo.tag} at index ${elementInfo.index}`);
|
|
215
|
-
const depth = (_a = elementInfo.depth) !== null && _a !== void 0 ? _a : elementInfo.getDepth();
|
|
216
|
-
const cleanEl = {
|
|
217
|
-
index: elementInfo.index,
|
|
218
|
-
tag: elementInfo.tag,
|
|
219
|
-
type: elementInfo.type,
|
|
220
|
-
text: elementInfo.text,
|
|
221
|
-
html: elementInfo.html,
|
|
222
|
-
xpath: elementInfo.xpath,
|
|
223
|
-
css_selector: elementInfo.css_selector,
|
|
224
|
-
iframe_selector: elementInfo.iframe_selector,
|
|
225
|
-
bounding_box: elementInfo.bounding_box,
|
|
226
|
-
depth
|
|
227
|
-
};
|
|
228
|
-
elementLogger.debug(`Cleaned element: ${JSON.stringify(cleanEl)}`);
|
|
229
|
-
return cleanEl;
|
|
230
|
-
}
|
|
231
|
-
/**
|
|
232
|
-
* Cleans highlighted elements in an instruction payload.
|
|
233
|
-
*/
|
|
234
|
-
function cleanupInstructionElements(instruction) {
|
|
235
|
-
var _a;
|
|
236
|
-
if (!((_a = instruction === null || instruction === void 0 ? void 0 : instruction.result) === null || _a === void 0 ? void 0 : _a.highlighted_elements)) {
|
|
237
|
-
elementLogger.debug('No highlighted elements to clean');
|
|
238
|
-
return instruction;
|
|
239
|
-
}
|
|
240
|
-
elementLogger.debug(`Cleaning ${instruction.result.highlighted_elements.length} highlighted elements`);
|
|
241
|
-
const cleaned = {
|
|
242
|
-
...instruction,
|
|
243
|
-
result: {
|
|
244
|
-
...instruction.result,
|
|
245
|
-
highlighted_elements: instruction.result.highlighted_elements.map((el) => cleanupElementInfo(el))
|
|
246
|
-
}
|
|
247
|
-
};
|
|
248
|
-
elementLogger.debug('Instruction cleaning completed');
|
|
249
|
-
return cleaned;
|
|
165
|
+
console.error(this.preamble(ProboLogLevel.ERROR), ...args); }
|
|
250
166
|
}
|
|
251
167
|
function matchRegex(str, regex) {
|
|
252
168
|
//parse the regex string
|
|
@@ -286,6 +202,49 @@ const setupBrowserConsoleLogs = (page, enableConsoleLogs, logger = null) => {
|
|
|
286
202
|
};
|
|
287
203
|
page.on('console', listener);
|
|
288
204
|
};
|
|
205
|
+
/**
|
|
206
|
+
* Safely interpolates template literals in a string using the provided context.
|
|
207
|
+
* Similar to JavaScript template literals, but executed safely at runtime.
|
|
208
|
+
* Recursively interpolates until no more template literals remain.
|
|
209
|
+
*
|
|
210
|
+
* This function only interpolates the argument string itself, not the entire context.
|
|
211
|
+
* When a template literal resolves to another string containing template literals,
|
|
212
|
+
* it recursively interpolates that result.
|
|
213
|
+
*
|
|
214
|
+
* @param str The string containing template literal syntax (e.g., "Hello ${name}")
|
|
215
|
+
* @param context The context object containing variables for interpolation
|
|
216
|
+
* @param maxDepth Maximum recursion depth to prevent infinite loops (default: 10)
|
|
217
|
+
* @returns The interpolated string, or the original string if no interpolation is needed
|
|
218
|
+
*/
|
|
219
|
+
function interpolateTemplate(str, context, maxDepth = 10) {
|
|
220
|
+
if (typeof str !== 'string' || !str.includes('${')) {
|
|
221
|
+
return str;
|
|
222
|
+
}
|
|
223
|
+
if (maxDepth <= 0) {
|
|
224
|
+
console.warn('⚠️ Maximum interpolation depth reached, returning partially interpolated string');
|
|
225
|
+
return str;
|
|
226
|
+
}
|
|
227
|
+
try {
|
|
228
|
+
// Escape backticks in the template to prevent template literal injection
|
|
229
|
+
const escapedTemplate = str.replace(/\\/g, '\\\\').replace(/`/g, '\\`');
|
|
230
|
+
// Create a safe template execution function
|
|
231
|
+
const compiled = new Function('ctx', `
|
|
232
|
+
const {${Object.keys(context).join(',')}} = ctx;
|
|
233
|
+
return \`${escapedTemplate}\`;
|
|
234
|
+
`);
|
|
235
|
+
const rendered = compiled(context);
|
|
236
|
+
// If the result still contains template literals, recursively interpolate
|
|
237
|
+
// This handles cases like: Employee_Name = '${firstName + middleName}' and then assert_1 = '${Employee_Name}'
|
|
238
|
+
if (rendered.includes('${') && rendered !== str) {
|
|
239
|
+
return interpolateTemplate(rendered, context, maxDepth - 1);
|
|
240
|
+
}
|
|
241
|
+
return rendered;
|
|
242
|
+
}
|
|
243
|
+
catch (e) {
|
|
244
|
+
console.warn('⚠️ Template evaluation failed, using original string:', e);
|
|
245
|
+
return str;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
289
248
|
|
|
290
249
|
function getDefaultExportFromCjs (x) {
|
|
291
250
|
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
|
|
@@ -883,23 +842,26 @@ class ApiClient {
|
|
|
883
842
|
return;
|
|
884
843
|
});
|
|
885
844
|
}
|
|
886
|
-
async resolveNextInstruction(stepId, instruction, aiModel) {
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
return data.instruction;
|
|
845
|
+
/* async resolveNextInstruction(stepId: string, instruction: Instruction | null, aiModel?: string) {
|
|
846
|
+
apiLogger.debug(`resolving next instruction: ${instruction}`);
|
|
847
|
+
return this.requestWithRetry('resolveNextInstruction', async () => {
|
|
848
|
+
apiLogger.debug(`API client: Resolving next instruction for step ${stepId}`);
|
|
849
|
+
|
|
850
|
+
const cleanInstruction = cleanupInstructionElements(instruction);
|
|
851
|
+
|
|
852
|
+
const response = await fetch(`${this.apiUrl}/step-runners/${stepId}/run/`, {
|
|
853
|
+
method: 'POST',
|
|
854
|
+
headers: this.getHeaders(),
|
|
855
|
+
body: JSON.stringify({
|
|
856
|
+
executed_instruction: cleanInstruction,
|
|
857
|
+
ai_model: aiModel
|
|
858
|
+
}),
|
|
901
859
|
});
|
|
902
|
-
|
|
860
|
+
|
|
861
|
+
const data = await this.handleResponse(response);
|
|
862
|
+
return data.instruction;
|
|
863
|
+
});
|
|
864
|
+
} */
|
|
903
865
|
async uploadScreenshot(screenshot_bytes) {
|
|
904
866
|
return this.requestWithRetry('uploadScreenshot', async () => {
|
|
905
867
|
const response = await fetch(`${this.apiUrl}/upload-screenshots/`, {
|
|
@@ -968,7 +930,7 @@ class ApiClient {
|
|
|
968
930
|
// Backend will create new step or update existing one based on interaction_id
|
|
969
931
|
apiLogger.debug(`converting interaction #${interaction.interactionId} to step`);
|
|
970
932
|
return this.requestWithRetry('interactionToStep', async () => {
|
|
971
|
-
var _a, _b;
|
|
933
|
+
var _a, _b, _c, _d;
|
|
972
934
|
const response = await fetch(`${this.apiUrl}/interaction-to-step/`, {
|
|
973
935
|
method: 'POST',
|
|
974
936
|
headers: this.getHeaders(),
|
|
@@ -980,6 +942,8 @@ class ApiClient {
|
|
|
980
942
|
argument: interaction.argument,
|
|
981
943
|
element_css_selector: ((_a = interaction.elementInfo) === null || _a === void 0 ? void 0 : _a.css_selector) || '',
|
|
982
944
|
iframe_selector: ((_b = interaction.elementInfo) === null || _b === void 0 ? void 0 : _b.iframe_selector) || '',
|
|
945
|
+
smart_selector: ((_c = interaction.elementInfo) === null || _c === void 0 ? void 0 : _c.smart_selector) || null,
|
|
946
|
+
smart_iframe_selector: ((_d = interaction.elementInfo) === null || _d === void 0 ? void 0 : _d.smart_iframe_selector) || null,
|
|
983
947
|
prompt: interaction.nativeDescription,
|
|
984
948
|
vanilla_prompt: interaction.nativeDescription,
|
|
985
949
|
is_vanilla_prompt_robust: interaction.isNativeDescriptionElaborate || false,
|
|
@@ -1079,6 +1043,9 @@ var AIModel;
|
|
|
1079
1043
|
AIModel["GPT4"] = "gpt4";
|
|
1080
1044
|
AIModel["GPT4_MINI"] = "gpt4-mini";
|
|
1081
1045
|
AIModel["CLAUDE_3_5"] = "claude-3.5";
|
|
1046
|
+
AIModel["CLAUDE_SONNET_4_5"] = "claude-sonnet-4.5";
|
|
1047
|
+
AIModel["CLAUDE_HAIKU_4_5"] = "claude-haiku-4.5";
|
|
1048
|
+
AIModel["CLAUDE_OPUS_4_1"] = "claude-opus-4.1";
|
|
1082
1049
|
AIModel["GROK_2"] = "grok-2";
|
|
1083
1050
|
AIModel["LLAMA_4_SCOUT"] = "llama-4-scout";
|
|
1084
1051
|
AIModel["DEEPSEEK_V3"] = "deepseek-v3";
|
|
@@ -1104,7 +1071,7 @@ const DEFAULT_PLAYWRIGHT_TIMEOUT_CONFIG = {
|
|
|
1104
1071
|
};
|
|
1105
1072
|
|
|
1106
1073
|
// Default logger instance
|
|
1107
|
-
const proboLogger = new ProboLogger('
|
|
1074
|
+
const proboLogger = new ProboLogger('proboLib');
|
|
1108
1075
|
|
|
1109
1076
|
/**
|
|
1110
1077
|
* Wait for DOM mutations to settle using MutationObserver logic
|
|
@@ -1183,8 +1150,10 @@ async function findClosestVisibleElement(locator) {
|
|
|
1183
1150
|
}
|
|
1184
1151
|
|
|
1185
1152
|
class Highlighter {
|
|
1186
|
-
constructor(enableConsoleLogs = true) {
|
|
1153
|
+
constructor(enableSmartSelectors = false, enableConsoleLogs = true, debugLevel = ProboLogLevel.INFO) {
|
|
1154
|
+
this.enableSmartSelectors = enableSmartSelectors;
|
|
1187
1155
|
this.enableConsoleLogs = enableConsoleLogs;
|
|
1156
|
+
this.debugLevel = debugLevel;
|
|
1188
1157
|
}
|
|
1189
1158
|
async ensureHighlighterScript(page, maxRetries = 3) {
|
|
1190
1159
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
@@ -1199,6 +1168,7 @@ class Highlighter {
|
|
|
1199
1168
|
typeof window.ProboLabs?.highlight?.execute === 'function'
|
|
1200
1169
|
`);
|
|
1201
1170
|
proboLogger.debug('Script injection verified:', verified);
|
|
1171
|
+
await page.evaluate(`window.ProboLabs.setLoggerDebugLevel('${this.debugLevel}');`);
|
|
1202
1172
|
}
|
|
1203
1173
|
return; // Success - exit the function
|
|
1204
1174
|
}
|
|
@@ -1240,10 +1210,10 @@ class Highlighter {
|
|
|
1240
1210
|
(_b = (_a = window === null || window === void 0 ? void 0 : window.ProboLabs) === null || _a === void 0 ? void 0 : _a.highlight) === null || _b === void 0 ? void 0 : _b.unexecute();
|
|
1241
1211
|
});
|
|
1242
1212
|
}
|
|
1243
|
-
async highlightElement(page, element_css_selector, iframe_selector, element_index) {
|
|
1213
|
+
async highlightElement(page, element_css_selector, iframe_selector, smart_selector, smart_iframe_selector, element_index) {
|
|
1244
1214
|
await this.ensureHighlighterScript(page);
|
|
1245
|
-
proboLogger.debug('Highlighting element with:', { element_css_selector, iframe_selector, element_index });
|
|
1246
|
-
await page.evaluate(({ css_selector, iframe_selector, index }) => {
|
|
1215
|
+
proboLogger.debug('Highlighting element with:', { element_css_selector, iframe_selector, smart_selector, smart_iframe_selector, enableSmartSelectors: this.enableSmartSelectors, element_index });
|
|
1216
|
+
await page.evaluate(({ css_selector, iframe_selector, smart_selector, smart_iframe_selector, index }) => {
|
|
1247
1217
|
const proboLabs = window.ProboLabs;
|
|
1248
1218
|
if (!proboLabs) {
|
|
1249
1219
|
proboLogger.warn('ProboLabs not initialized');
|
|
@@ -1253,13 +1223,17 @@ class Highlighter {
|
|
|
1253
1223
|
const elementInfo = {
|
|
1254
1224
|
css_selector: css_selector,
|
|
1255
1225
|
iframe_selector: iframe_selector,
|
|
1226
|
+
smart_selector: smart_selector,
|
|
1227
|
+
smart_iframe_selector: smart_iframe_selector,
|
|
1256
1228
|
index: index
|
|
1257
1229
|
};
|
|
1258
1230
|
// Call highlightElements directly
|
|
1259
|
-
proboLabs.highlightElements([elementInfo]);
|
|
1231
|
+
proboLabs.highlightElements([elementInfo], this.enableSmartSelectors);
|
|
1260
1232
|
}, {
|
|
1261
1233
|
css_selector: element_css_selector,
|
|
1262
1234
|
iframe_selector: iframe_selector,
|
|
1235
|
+
smart_selector: smart_selector,
|
|
1236
|
+
smart_iframe_selector: smart_iframe_selector,
|
|
1263
1237
|
index: element_index
|
|
1264
1238
|
});
|
|
1265
1239
|
}
|
|
@@ -1280,12 +1254,12 @@ class Highlighter {
|
|
|
1280
1254
|
* Find and cache the actual interaction element by CSS and iframe selector.
|
|
1281
1255
|
* Returns true if found, false otherwise.
|
|
1282
1256
|
*/
|
|
1283
|
-
async findAndCacheActualElement(page,
|
|
1257
|
+
async findAndCacheActualElement(page, iframeSelector, elementSelector, isHover = false) {
|
|
1284
1258
|
await this.ensureHighlighterScript(page);
|
|
1285
1259
|
const result = await page.evaluate((params) => {
|
|
1286
1260
|
var _a, _b;
|
|
1287
|
-
return (_b = (_a = window.ProboLabs) === null || _a === void 0 ? void 0 : _a.findAndCacheActualElement) === null || _b === void 0 ? void 0 : _b.call(_a, params.
|
|
1288
|
-
}, {
|
|
1261
|
+
return (_b = (_a = window.ProboLabs) === null || _a === void 0 ? void 0 : _a.findAndCacheActualElement) === null || _b === void 0 ? void 0 : _b.call(_a, params.iframeSelector, params.elementSelector, params.isHover, params.useSmartSelectors);
|
|
1262
|
+
}, { iframeSelector: iframeSelector, elementSelector: elementSelector, isHover: isHover, useSmartSelectors: this.enableSmartSelectors });
|
|
1289
1263
|
return result !== null && result !== void 0 ? result : false;
|
|
1290
1264
|
}
|
|
1291
1265
|
/**
|
|
@@ -1806,7 +1780,7 @@ class NavTracker {
|
|
|
1806
1780
|
this.inflightGraceMs = (_g = options.inflightGraceMs) !== null && _g !== void 0 ? _g : 4000;
|
|
1807
1781
|
this.waitForStabilityVerbose = (_h = options.waitForStabilityVerbose) !== null && _h !== void 0 ? _h : false;
|
|
1808
1782
|
proboLogger.debug(`NavTracker constructor set values: quietTimeout=${this.waitForStabilityQuietTimeout}, initialDelay=${this.waitForStabilityInitialDelay}, globalTimeout=${this.waitForStabilityGlobalTimeout}, verbose=${this.waitForStabilityVerbose}`);
|
|
1809
|
-
this.instanceId = Math.random().toString(36).
|
|
1783
|
+
this.instanceId = Math.random().toString(36).substring(2, 11);
|
|
1810
1784
|
// Initialize timestamps
|
|
1811
1785
|
const now = Date.now();
|
|
1812
1786
|
this.lastHardNavAt = now;
|
|
@@ -2080,22 +2054,1871 @@ class NavTracker {
|
|
|
2080
2054
|
}
|
|
2081
2055
|
NavTracker.instance = null;
|
|
2082
2056
|
|
|
2057
|
+
//! otpauth 9.4.1 | (c) Héctor Molinero Fernández | MIT | https://github.com/hectorm/otpauth
|
|
2058
|
+
//! noble-hashes 1.8.0 | (c) Paul Miller | MIT | https://github.com/paulmillr/noble-hashes
|
|
2059
|
+
/// <reference types="./otpauth.d.ts" />
|
|
2060
|
+
// @ts-nocheck
|
|
2061
|
+
/**
|
|
2062
|
+
* Converts an integer to an Uint8Array.
|
|
2063
|
+
* @param {number} num Integer.
|
|
2064
|
+
* @returns {Uint8Array} Uint8Array.
|
|
2065
|
+
*/ const uintDecode = (num)=>{
|
|
2066
|
+
const buf = new ArrayBuffer(8);
|
|
2067
|
+
const arr = new Uint8Array(buf);
|
|
2068
|
+
let acc = num;
|
|
2069
|
+
for(let i = 7; i >= 0; i--){
|
|
2070
|
+
if (acc === 0) break;
|
|
2071
|
+
arr[i] = acc & 255;
|
|
2072
|
+
acc -= arr[i];
|
|
2073
|
+
acc /= 256;
|
|
2074
|
+
}
|
|
2075
|
+
return arr;
|
|
2076
|
+
};
|
|
2077
|
+
|
|
2078
|
+
/**
|
|
2079
|
+
* Utilities for hex, bytes, CSPRNG.
|
|
2080
|
+
* @module
|
|
2081
|
+
*/ /*! noble-hashes - MIT License (c) 2022 Paul Miller (paulmillr.com) */ // We use WebCrypto aka globalThis.crypto, which exists in browsers and node.js 16+.
|
|
2082
|
+
// node.js versions earlier than v19 don't declare it in global scope.
|
|
2083
|
+
// For node.js, package.json#exports field mapping rewrites import
|
|
2084
|
+
// from `crypto` to `cryptoNode`, which imports native module.
|
|
2085
|
+
// Makes the utils un-importable in browsers without a bundler.
|
|
2086
|
+
// Once node.js 18 is deprecated (2025-04-30), we can just drop the import.
|
|
2087
|
+
/** Checks if something is Uint8Array. Be careful: nodejs Buffer will return true. */ function isBytes(a) {
|
|
2088
|
+
return a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === 'Uint8Array';
|
|
2089
|
+
}
|
|
2090
|
+
/** Asserts something is positive integer. */ function anumber(n) {
|
|
2091
|
+
if (!Number.isSafeInteger(n) || n < 0) throw new Error('positive integer expected, got ' + n);
|
|
2092
|
+
}
|
|
2093
|
+
/** Asserts something is Uint8Array. */ function abytes(b, ...lengths) {
|
|
2094
|
+
if (!isBytes(b)) throw new Error('Uint8Array expected');
|
|
2095
|
+
if (lengths.length > 0 && !lengths.includes(b.length)) throw new Error('Uint8Array expected of length ' + lengths + ', got length=' + b.length);
|
|
2096
|
+
}
|
|
2097
|
+
/** Asserts something is hash */ function ahash(h) {
|
|
2098
|
+
if (typeof h !== 'function' || typeof h.create !== 'function') throw new Error('Hash should be wrapped by utils.createHasher');
|
|
2099
|
+
anumber(h.outputLen);
|
|
2100
|
+
anumber(h.blockLen);
|
|
2101
|
+
}
|
|
2102
|
+
/** Asserts a hash instance has not been destroyed / finished */ function aexists(instance, checkFinished = true) {
|
|
2103
|
+
if (instance.destroyed) throw new Error('Hash instance has been destroyed');
|
|
2104
|
+
if (checkFinished && instance.finished) throw new Error('Hash#digest() has already been called');
|
|
2105
|
+
}
|
|
2106
|
+
/** Asserts output is properly-sized byte array */ function aoutput(out, instance) {
|
|
2107
|
+
abytes(out);
|
|
2108
|
+
const min = instance.outputLen;
|
|
2109
|
+
if (out.length < min) {
|
|
2110
|
+
throw new Error('digestInto() expects output buffer of length at least ' + min);
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
/** Cast u8 / u16 / u32 to u32. */ function u32(arr) {
|
|
2114
|
+
return new Uint32Array(arr.buffer, arr.byteOffset, Math.floor(arr.byteLength / 4));
|
|
2115
|
+
}
|
|
2116
|
+
/** Zeroize a byte array. Warning: JS provides no guarantees. */ function clean(...arrays) {
|
|
2117
|
+
for(let i = 0; i < arrays.length; i++){
|
|
2118
|
+
arrays[i].fill(0);
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
/** Create DataView of an array for easy byte-level manipulation. */ function createView(arr) {
|
|
2122
|
+
return new DataView(arr.buffer, arr.byteOffset, arr.byteLength);
|
|
2123
|
+
}
|
|
2124
|
+
/** The rotate right (circular right shift) operation for uint32 */ function rotr(word, shift) {
|
|
2125
|
+
return word << 32 - shift | word >>> shift;
|
|
2126
|
+
}
|
|
2127
|
+
/** The rotate left (circular left shift) operation for uint32 */ function rotl(word, shift) {
|
|
2128
|
+
return word << shift | word >>> 32 - shift >>> 0;
|
|
2129
|
+
}
|
|
2130
|
+
/** Is current platform little-endian? Most are. Big-Endian platform: IBM */ const isLE = /* @__PURE__ */ (()=>new Uint8Array(new Uint32Array([
|
|
2131
|
+
0x11223344
|
|
2132
|
+
]).buffer)[0] === 0x44)();
|
|
2133
|
+
/** The byte swap operation for uint32 */ function byteSwap(word) {
|
|
2134
|
+
return word << 24 & 0xff000000 | word << 8 & 0xff0000 | word >>> 8 & 0xff00 | word >>> 24 & 0xff;
|
|
2135
|
+
}
|
|
2136
|
+
/** In place byte swap for Uint32Array */ function byteSwap32(arr) {
|
|
2137
|
+
for(let i = 0; i < arr.length; i++){
|
|
2138
|
+
arr[i] = byteSwap(arr[i]);
|
|
2139
|
+
}
|
|
2140
|
+
return arr;
|
|
2141
|
+
}
|
|
2142
|
+
const swap32IfBE = isLE ? (u)=>u : byteSwap32;
|
|
2143
|
+
/**
|
|
2144
|
+
* Converts string to bytes using UTF8 encoding.
|
|
2145
|
+
* @example utf8ToBytes('abc') // Uint8Array.from([97, 98, 99])
|
|
2146
|
+
*/ function utf8ToBytes(str) {
|
|
2147
|
+
if (typeof str !== 'string') throw new Error('string expected');
|
|
2148
|
+
return new Uint8Array(new TextEncoder().encode(str)); // https://bugzil.la/1681809
|
|
2149
|
+
}
|
|
2150
|
+
/**
|
|
2151
|
+
* Normalizes (non-hex) string or Uint8Array to Uint8Array.
|
|
2152
|
+
* Warning: when Uint8Array is passed, it would NOT get copied.
|
|
2153
|
+
* Keep in mind for future mutable operations.
|
|
2154
|
+
*/ function toBytes(data) {
|
|
2155
|
+
if (typeof data === 'string') data = utf8ToBytes(data);
|
|
2156
|
+
abytes(data);
|
|
2157
|
+
return data;
|
|
2158
|
+
}
|
|
2159
|
+
/** For runtime check if class implements interface */ class Hash {
|
|
2160
|
+
}
|
|
2161
|
+
/** Wraps hash function, creating an interface on top of it */ function createHasher(hashCons) {
|
|
2162
|
+
const hashC = (msg)=>hashCons().update(toBytes(msg)).digest();
|
|
2163
|
+
const tmp = hashCons();
|
|
2164
|
+
hashC.outputLen = tmp.outputLen;
|
|
2165
|
+
hashC.blockLen = tmp.blockLen;
|
|
2166
|
+
hashC.create = ()=>hashCons();
|
|
2167
|
+
return hashC;
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
class HMAC extends Hash {
|
|
2171
|
+
update(buf) {
|
|
2172
|
+
aexists(this);
|
|
2173
|
+
this.iHash.update(buf);
|
|
2174
|
+
return this;
|
|
2175
|
+
}
|
|
2176
|
+
digestInto(out) {
|
|
2177
|
+
aexists(this);
|
|
2178
|
+
abytes(out, this.outputLen);
|
|
2179
|
+
this.finished = true;
|
|
2180
|
+
this.iHash.digestInto(out);
|
|
2181
|
+
this.oHash.update(out);
|
|
2182
|
+
this.oHash.digestInto(out);
|
|
2183
|
+
this.destroy();
|
|
2184
|
+
}
|
|
2185
|
+
digest() {
|
|
2186
|
+
const out = new Uint8Array(this.oHash.outputLen);
|
|
2187
|
+
this.digestInto(out);
|
|
2188
|
+
return out;
|
|
2189
|
+
}
|
|
2190
|
+
_cloneInto(to) {
|
|
2191
|
+
// Create new instance without calling constructor since key already in state and we don't know it.
|
|
2192
|
+
to || (to = Object.create(Object.getPrototypeOf(this), {}));
|
|
2193
|
+
const { oHash, iHash, finished, destroyed, blockLen, outputLen } = this;
|
|
2194
|
+
to = to;
|
|
2195
|
+
to.finished = finished;
|
|
2196
|
+
to.destroyed = destroyed;
|
|
2197
|
+
to.blockLen = blockLen;
|
|
2198
|
+
to.outputLen = outputLen;
|
|
2199
|
+
to.oHash = oHash._cloneInto(to.oHash);
|
|
2200
|
+
to.iHash = iHash._cloneInto(to.iHash);
|
|
2201
|
+
return to;
|
|
2202
|
+
}
|
|
2203
|
+
clone() {
|
|
2204
|
+
return this._cloneInto();
|
|
2205
|
+
}
|
|
2206
|
+
destroy() {
|
|
2207
|
+
this.destroyed = true;
|
|
2208
|
+
this.oHash.destroy();
|
|
2209
|
+
this.iHash.destroy();
|
|
2210
|
+
}
|
|
2211
|
+
constructor(hash, _key){
|
|
2212
|
+
super();
|
|
2213
|
+
this.finished = false;
|
|
2214
|
+
this.destroyed = false;
|
|
2215
|
+
ahash(hash);
|
|
2216
|
+
const key = toBytes(_key);
|
|
2217
|
+
this.iHash = hash.create();
|
|
2218
|
+
if (typeof this.iHash.update !== 'function') throw new Error('Expected instance of class which extends utils.Hash');
|
|
2219
|
+
this.blockLen = this.iHash.blockLen;
|
|
2220
|
+
this.outputLen = this.iHash.outputLen;
|
|
2221
|
+
const blockLen = this.blockLen;
|
|
2222
|
+
const pad = new Uint8Array(blockLen);
|
|
2223
|
+
// blockLen can be bigger than outputLen
|
|
2224
|
+
pad.set(key.length > blockLen ? hash.create().update(key).digest() : key);
|
|
2225
|
+
for(let i = 0; i < pad.length; i++)pad[i] ^= 0x36;
|
|
2226
|
+
this.iHash.update(pad);
|
|
2227
|
+
// By doing update (processing of first block) of outer hash here we can re-use it between multiple calls via clone
|
|
2228
|
+
this.oHash = hash.create();
|
|
2229
|
+
// Undo internal XOR && apply outer XOR
|
|
2230
|
+
for(let i = 0; i < pad.length; i++)pad[i] ^= 0x36 ^ 0x5c;
|
|
2231
|
+
this.oHash.update(pad);
|
|
2232
|
+
clean(pad);
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
/**
|
|
2236
|
+
* HMAC: RFC2104 message authentication code.
|
|
2237
|
+
* @param hash - function that would be used e.g. sha256
|
|
2238
|
+
* @param key - message key
|
|
2239
|
+
* @param message - message data
|
|
2240
|
+
* @example
|
|
2241
|
+
* import { hmac } from '@noble/hashes/hmac';
|
|
2242
|
+
* import { sha256 } from '@noble/hashes/sha2';
|
|
2243
|
+
* const mac1 = hmac(sha256, 'key', 'message');
|
|
2244
|
+
*/ const hmac = (hash, key, message)=>new HMAC(hash, key).update(message).digest();
|
|
2245
|
+
hmac.create = (hash, key)=>new HMAC(hash, key);
|
|
2246
|
+
|
|
2247
|
+
/** Polyfill for Safari 14. https://caniuse.com/mdn-javascript_builtins_dataview_setbiguint64 */ function setBigUint64(view, byteOffset, value, isLE) {
|
|
2248
|
+
if (typeof view.setBigUint64 === 'function') return view.setBigUint64(byteOffset, value, isLE);
|
|
2249
|
+
const _32n = BigInt(32);
|
|
2250
|
+
const _u32_max = BigInt(0xffffffff);
|
|
2251
|
+
const wh = Number(value >> _32n & _u32_max);
|
|
2252
|
+
const wl = Number(value & _u32_max);
|
|
2253
|
+
const h = isLE ? 4 : 0;
|
|
2254
|
+
const l = isLE ? 0 : 4;
|
|
2255
|
+
view.setUint32(byteOffset + h, wh, isLE);
|
|
2256
|
+
view.setUint32(byteOffset + l, wl, isLE);
|
|
2257
|
+
}
|
|
2258
|
+
/** Choice: a ? b : c */ function Chi(a, b, c) {
|
|
2259
|
+
return a & b ^ ~a & c;
|
|
2260
|
+
}
|
|
2261
|
+
/** Majority function, true if any two inputs is true. */ function Maj(a, b, c) {
|
|
2262
|
+
return a & b ^ a & c ^ b & c;
|
|
2263
|
+
}
|
|
2264
|
+
/**
|
|
2265
|
+
* Merkle-Damgard hash construction base class.
|
|
2266
|
+
* Could be used to create MD5, RIPEMD, SHA1, SHA2.
|
|
2267
|
+
*/ class HashMD extends Hash {
|
|
2268
|
+
update(data) {
|
|
2269
|
+
aexists(this);
|
|
2270
|
+
data = toBytes(data);
|
|
2271
|
+
abytes(data);
|
|
2272
|
+
const { view, buffer, blockLen } = this;
|
|
2273
|
+
const len = data.length;
|
|
2274
|
+
for(let pos = 0; pos < len;){
|
|
2275
|
+
const take = Math.min(blockLen - this.pos, len - pos);
|
|
2276
|
+
// Fast path: we have at least one block in input, cast it to view and process
|
|
2277
|
+
if (take === blockLen) {
|
|
2278
|
+
const dataView = createView(data);
|
|
2279
|
+
for(; blockLen <= len - pos; pos += blockLen)this.process(dataView, pos);
|
|
2280
|
+
continue;
|
|
2281
|
+
}
|
|
2282
|
+
buffer.set(data.subarray(pos, pos + take), this.pos);
|
|
2283
|
+
this.pos += take;
|
|
2284
|
+
pos += take;
|
|
2285
|
+
if (this.pos === blockLen) {
|
|
2286
|
+
this.process(view, 0);
|
|
2287
|
+
this.pos = 0;
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
this.length += data.length;
|
|
2291
|
+
this.roundClean();
|
|
2292
|
+
return this;
|
|
2293
|
+
}
|
|
2294
|
+
digestInto(out) {
|
|
2295
|
+
aexists(this);
|
|
2296
|
+
aoutput(out, this);
|
|
2297
|
+
this.finished = true;
|
|
2298
|
+
// Padding
|
|
2299
|
+
// We can avoid allocation of buffer for padding completely if it
|
|
2300
|
+
// was previously not allocated here. But it won't change performance.
|
|
2301
|
+
const { buffer, view, blockLen, isLE } = this;
|
|
2302
|
+
let { pos } = this;
|
|
2303
|
+
// append the bit '1' to the message
|
|
2304
|
+
buffer[pos++] = 0b10000000;
|
|
2305
|
+
clean(this.buffer.subarray(pos));
|
|
2306
|
+
// we have less than padOffset left in buffer, so we cannot put length in
|
|
2307
|
+
// current block, need process it and pad again
|
|
2308
|
+
if (this.padOffset > blockLen - pos) {
|
|
2309
|
+
this.process(view, 0);
|
|
2310
|
+
pos = 0;
|
|
2311
|
+
}
|
|
2312
|
+
// Pad until full block byte with zeros
|
|
2313
|
+
for(let i = pos; i < blockLen; i++)buffer[i] = 0;
|
|
2314
|
+
// Note: sha512 requires length to be 128bit integer, but length in JS will overflow before that
|
|
2315
|
+
// You need to write around 2 exabytes (u64_max / 8 / (1024**6)) for this to happen.
|
|
2316
|
+
// So we just write lowest 64 bits of that value.
|
|
2317
|
+
setBigUint64(view, blockLen - 8, BigInt(this.length * 8), isLE);
|
|
2318
|
+
this.process(view, 0);
|
|
2319
|
+
const oview = createView(out);
|
|
2320
|
+
const len = this.outputLen;
|
|
2321
|
+
// NOTE: we do division by 4 later, which should be fused in single op with modulo by JIT
|
|
2322
|
+
if (len % 4) throw new Error('_sha2: outputLen should be aligned to 32bit');
|
|
2323
|
+
const outLen = len / 4;
|
|
2324
|
+
const state = this.get();
|
|
2325
|
+
if (outLen > state.length) throw new Error('_sha2: outputLen bigger than state');
|
|
2326
|
+
for(let i = 0; i < outLen; i++)oview.setUint32(4 * i, state[i], isLE);
|
|
2327
|
+
}
|
|
2328
|
+
digest() {
|
|
2329
|
+
const { buffer, outputLen } = this;
|
|
2330
|
+
this.digestInto(buffer);
|
|
2331
|
+
const res = buffer.slice(0, outputLen);
|
|
2332
|
+
this.destroy();
|
|
2333
|
+
return res;
|
|
2334
|
+
}
|
|
2335
|
+
_cloneInto(to) {
|
|
2336
|
+
to || (to = new this.constructor());
|
|
2337
|
+
to.set(...this.get());
|
|
2338
|
+
const { blockLen, buffer, length, finished, destroyed, pos } = this;
|
|
2339
|
+
to.destroyed = destroyed;
|
|
2340
|
+
to.finished = finished;
|
|
2341
|
+
to.length = length;
|
|
2342
|
+
to.pos = pos;
|
|
2343
|
+
if (length % blockLen) to.buffer.set(buffer);
|
|
2344
|
+
return to;
|
|
2345
|
+
}
|
|
2346
|
+
clone() {
|
|
2347
|
+
return this._cloneInto();
|
|
2348
|
+
}
|
|
2349
|
+
constructor(blockLen, outputLen, padOffset, isLE){
|
|
2350
|
+
super();
|
|
2351
|
+
this.finished = false;
|
|
2352
|
+
this.length = 0;
|
|
2353
|
+
this.pos = 0;
|
|
2354
|
+
this.destroyed = false;
|
|
2355
|
+
this.blockLen = blockLen;
|
|
2356
|
+
this.outputLen = outputLen;
|
|
2357
|
+
this.padOffset = padOffset;
|
|
2358
|
+
this.isLE = isLE;
|
|
2359
|
+
this.buffer = new Uint8Array(blockLen);
|
|
2360
|
+
this.view = createView(this.buffer);
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
/**
|
|
2364
|
+
* Initial SHA-2 state: fractional parts of square roots of first 16 primes 2..53.
|
|
2365
|
+
* Check out `test/misc/sha2-gen-iv.js` for recomputation guide.
|
|
2366
|
+
*/ /** Initial SHA256 state. Bits 0..32 of frac part of sqrt of primes 2..19 */ const SHA256_IV = /* @__PURE__ */ Uint32Array.from([
|
|
2367
|
+
0x6a09e667,
|
|
2368
|
+
0xbb67ae85,
|
|
2369
|
+
0x3c6ef372,
|
|
2370
|
+
0xa54ff53a,
|
|
2371
|
+
0x510e527f,
|
|
2372
|
+
0x9b05688c,
|
|
2373
|
+
0x1f83d9ab,
|
|
2374
|
+
0x5be0cd19
|
|
2375
|
+
]);
|
|
2376
|
+
/** Initial SHA224 state. Bits 32..64 of frac part of sqrt of primes 23..53 */ const SHA224_IV = /* @__PURE__ */ Uint32Array.from([
|
|
2377
|
+
0xc1059ed8,
|
|
2378
|
+
0x367cd507,
|
|
2379
|
+
0x3070dd17,
|
|
2380
|
+
0xf70e5939,
|
|
2381
|
+
0xffc00b31,
|
|
2382
|
+
0x68581511,
|
|
2383
|
+
0x64f98fa7,
|
|
2384
|
+
0xbefa4fa4
|
|
2385
|
+
]);
|
|
2386
|
+
/** Initial SHA384 state. Bits 0..64 of frac part of sqrt of primes 23..53 */ const SHA384_IV = /* @__PURE__ */ Uint32Array.from([
|
|
2387
|
+
0xcbbb9d5d,
|
|
2388
|
+
0xc1059ed8,
|
|
2389
|
+
0x629a292a,
|
|
2390
|
+
0x367cd507,
|
|
2391
|
+
0x9159015a,
|
|
2392
|
+
0x3070dd17,
|
|
2393
|
+
0x152fecd8,
|
|
2394
|
+
0xf70e5939,
|
|
2395
|
+
0x67332667,
|
|
2396
|
+
0xffc00b31,
|
|
2397
|
+
0x8eb44a87,
|
|
2398
|
+
0x68581511,
|
|
2399
|
+
0xdb0c2e0d,
|
|
2400
|
+
0x64f98fa7,
|
|
2401
|
+
0x47b5481d,
|
|
2402
|
+
0xbefa4fa4
|
|
2403
|
+
]);
|
|
2404
|
+
/** Initial SHA512 state. Bits 0..64 of frac part of sqrt of primes 2..19 */ const SHA512_IV = /* @__PURE__ */ Uint32Array.from([
|
|
2405
|
+
0x6a09e667,
|
|
2406
|
+
0xf3bcc908,
|
|
2407
|
+
0xbb67ae85,
|
|
2408
|
+
0x84caa73b,
|
|
2409
|
+
0x3c6ef372,
|
|
2410
|
+
0xfe94f82b,
|
|
2411
|
+
0xa54ff53a,
|
|
2412
|
+
0x5f1d36f1,
|
|
2413
|
+
0x510e527f,
|
|
2414
|
+
0xade682d1,
|
|
2415
|
+
0x9b05688c,
|
|
2416
|
+
0x2b3e6c1f,
|
|
2417
|
+
0x1f83d9ab,
|
|
2418
|
+
0xfb41bd6b,
|
|
2419
|
+
0x5be0cd19,
|
|
2420
|
+
0x137e2179
|
|
2421
|
+
]);
|
|
2422
|
+
|
|
2423
|
+
/** Initial SHA1 state */ const SHA1_IV = /* @__PURE__ */ Uint32Array.from([
|
|
2424
|
+
0x67452301,
|
|
2425
|
+
0xefcdab89,
|
|
2426
|
+
0x98badcfe,
|
|
2427
|
+
0x10325476,
|
|
2428
|
+
0xc3d2e1f0
|
|
2429
|
+
]);
|
|
2430
|
+
// Reusable temporary buffer
|
|
2431
|
+
const SHA1_W = /* @__PURE__ */ new Uint32Array(80);
|
|
2432
|
+
/** SHA1 legacy hash class. */ class SHA1 extends HashMD {
|
|
2433
|
+
get() {
|
|
2434
|
+
const { A, B, C, D, E } = this;
|
|
2435
|
+
return [
|
|
2436
|
+
A,
|
|
2437
|
+
B,
|
|
2438
|
+
C,
|
|
2439
|
+
D,
|
|
2440
|
+
E
|
|
2441
|
+
];
|
|
2442
|
+
}
|
|
2443
|
+
set(A, B, C, D, E) {
|
|
2444
|
+
this.A = A | 0;
|
|
2445
|
+
this.B = B | 0;
|
|
2446
|
+
this.C = C | 0;
|
|
2447
|
+
this.D = D | 0;
|
|
2448
|
+
this.E = E | 0;
|
|
2449
|
+
}
|
|
2450
|
+
process(view, offset) {
|
|
2451
|
+
for(let i = 0; i < 16; i++, offset += 4)SHA1_W[i] = view.getUint32(offset, false);
|
|
2452
|
+
for(let i = 16; i < 80; i++)SHA1_W[i] = rotl(SHA1_W[i - 3] ^ SHA1_W[i - 8] ^ SHA1_W[i - 14] ^ SHA1_W[i - 16], 1);
|
|
2453
|
+
// Compression function main loop, 80 rounds
|
|
2454
|
+
let { A, B, C, D, E } = this;
|
|
2455
|
+
for(let i = 0; i < 80; i++){
|
|
2456
|
+
let F, K;
|
|
2457
|
+
if (i < 20) {
|
|
2458
|
+
F = Chi(B, C, D);
|
|
2459
|
+
K = 0x5a827999;
|
|
2460
|
+
} else if (i < 40) {
|
|
2461
|
+
F = B ^ C ^ D;
|
|
2462
|
+
K = 0x6ed9eba1;
|
|
2463
|
+
} else if (i < 60) {
|
|
2464
|
+
F = Maj(B, C, D);
|
|
2465
|
+
K = 0x8f1bbcdc;
|
|
2466
|
+
} else {
|
|
2467
|
+
F = B ^ C ^ D;
|
|
2468
|
+
K = 0xca62c1d6;
|
|
2469
|
+
}
|
|
2470
|
+
const T = rotl(A, 5) + F + E + K + SHA1_W[i] | 0;
|
|
2471
|
+
E = D;
|
|
2472
|
+
D = C;
|
|
2473
|
+
C = rotl(B, 30);
|
|
2474
|
+
B = A;
|
|
2475
|
+
A = T;
|
|
2476
|
+
}
|
|
2477
|
+
// Add the compressed chunk to the current hash value
|
|
2478
|
+
A = A + this.A | 0;
|
|
2479
|
+
B = B + this.B | 0;
|
|
2480
|
+
C = C + this.C | 0;
|
|
2481
|
+
D = D + this.D | 0;
|
|
2482
|
+
E = E + this.E | 0;
|
|
2483
|
+
this.set(A, B, C, D, E);
|
|
2484
|
+
}
|
|
2485
|
+
roundClean() {
|
|
2486
|
+
clean(SHA1_W);
|
|
2487
|
+
}
|
|
2488
|
+
destroy() {
|
|
2489
|
+
this.set(0, 0, 0, 0, 0);
|
|
2490
|
+
clean(this.buffer);
|
|
2491
|
+
}
|
|
2492
|
+
constructor(){
|
|
2493
|
+
super(64, 20, 8, false);
|
|
2494
|
+
this.A = SHA1_IV[0] | 0;
|
|
2495
|
+
this.B = SHA1_IV[1] | 0;
|
|
2496
|
+
this.C = SHA1_IV[2] | 0;
|
|
2497
|
+
this.D = SHA1_IV[3] | 0;
|
|
2498
|
+
this.E = SHA1_IV[4] | 0;
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
/** SHA1 (RFC 3174) legacy hash function. It was cryptographically broken. */ const sha1 = /* @__PURE__ */ createHasher(()=>new SHA1());
|
|
2502
|
+
|
|
2503
|
+
/**
|
|
2504
|
+
* Internal helpers for u64. BigUint64Array is too slow as per 2025, so we implement it using Uint32Array.
|
|
2505
|
+
* @todo re-check https://issues.chromium.org/issues/42212588
|
|
2506
|
+
* @module
|
|
2507
|
+
*/ const U32_MASK64 = /* @__PURE__ */ BigInt(2 ** 32 - 1);
|
|
2508
|
+
const _32n = /* @__PURE__ */ BigInt(32);
|
|
2509
|
+
function fromBig(n, le = false) {
|
|
2510
|
+
if (le) return {
|
|
2511
|
+
h: Number(n & U32_MASK64),
|
|
2512
|
+
l: Number(n >> _32n & U32_MASK64)
|
|
2513
|
+
};
|
|
2514
|
+
return {
|
|
2515
|
+
h: Number(n >> _32n & U32_MASK64) | 0,
|
|
2516
|
+
l: Number(n & U32_MASK64) | 0
|
|
2517
|
+
};
|
|
2518
|
+
}
|
|
2519
|
+
function split(lst, le = false) {
|
|
2520
|
+
const len = lst.length;
|
|
2521
|
+
let Ah = new Uint32Array(len);
|
|
2522
|
+
let Al = new Uint32Array(len);
|
|
2523
|
+
for(let i = 0; i < len; i++){
|
|
2524
|
+
const { h, l } = fromBig(lst[i], le);
|
|
2525
|
+
[Ah[i], Al[i]] = [
|
|
2526
|
+
h,
|
|
2527
|
+
l
|
|
2528
|
+
];
|
|
2529
|
+
}
|
|
2530
|
+
return [
|
|
2531
|
+
Ah,
|
|
2532
|
+
Al
|
|
2533
|
+
];
|
|
2534
|
+
}
|
|
2535
|
+
// for Shift in [0, 32)
|
|
2536
|
+
const shrSH = (h, _l, s)=>h >>> s;
|
|
2537
|
+
const shrSL = (h, l, s)=>h << 32 - s | l >>> s;
|
|
2538
|
+
// Right rotate for Shift in [1, 32)
|
|
2539
|
+
const rotrSH = (h, l, s)=>h >>> s | l << 32 - s;
|
|
2540
|
+
const rotrSL = (h, l, s)=>h << 32 - s | l >>> s;
|
|
2541
|
+
// Right rotate for Shift in (32, 64), NOTE: 32 is special case.
|
|
2542
|
+
const rotrBH = (h, l, s)=>h << 64 - s | l >>> s - 32;
|
|
2543
|
+
const rotrBL = (h, l, s)=>h >>> s - 32 | l << 64 - s;
|
|
2544
|
+
// Left rotate for Shift in [1, 32)
|
|
2545
|
+
const rotlSH = (h, l, s)=>h << s | l >>> 32 - s;
|
|
2546
|
+
const rotlSL = (h, l, s)=>l << s | h >>> 32 - s;
|
|
2547
|
+
// Left rotate for Shift in (32, 64), NOTE: 32 is special case.
|
|
2548
|
+
const rotlBH = (h, l, s)=>l << s - 32 | h >>> 64 - s;
|
|
2549
|
+
const rotlBL = (h, l, s)=>h << s - 32 | l >>> 64 - s;
|
|
2550
|
+
// JS uses 32-bit signed integers for bitwise operations which means we cannot
|
|
2551
|
+
// simple take carry out of low bit sum by shift, we need to use division.
|
|
2552
|
+
function add(Ah, Al, Bh, Bl) {
|
|
2553
|
+
const l = (Al >>> 0) + (Bl >>> 0);
|
|
2554
|
+
return {
|
|
2555
|
+
h: Ah + Bh + (l / 2 ** 32 | 0) | 0,
|
|
2556
|
+
l: l | 0
|
|
2557
|
+
};
|
|
2558
|
+
}
|
|
2559
|
+
// Addition with more than 2 elements
|
|
2560
|
+
const add3L = (Al, Bl, Cl)=>(Al >>> 0) + (Bl >>> 0) + (Cl >>> 0);
|
|
2561
|
+
const add3H = (low, Ah, Bh, Ch)=>Ah + Bh + Ch + (low / 2 ** 32 | 0) | 0;
|
|
2562
|
+
const add4L = (Al, Bl, Cl, Dl)=>(Al >>> 0) + (Bl >>> 0) + (Cl >>> 0) + (Dl >>> 0);
|
|
2563
|
+
const add4H = (low, Ah, Bh, Ch, Dh)=>Ah + Bh + Ch + Dh + (low / 2 ** 32 | 0) | 0;
|
|
2564
|
+
const add5L = (Al, Bl, Cl, Dl, El)=>(Al >>> 0) + (Bl >>> 0) + (Cl >>> 0) + (Dl >>> 0) + (El >>> 0);
|
|
2565
|
+
const add5H = (low, Ah, Bh, Ch, Dh, Eh)=>Ah + Bh + Ch + Dh + Eh + (low / 2 ** 32 | 0) | 0;
|
|
2566
|
+
|
|
2567
|
+
/**
|
|
2568
|
+
* Round constants:
|
|
2569
|
+
* First 32 bits of fractional parts of the cube roots of the first 64 primes 2..311)
|
|
2570
|
+
*/ // prettier-ignore
|
|
2571
|
+
const SHA256_K = /* @__PURE__ */ Uint32Array.from([
|
|
2572
|
+
0x428a2f98,
|
|
2573
|
+
0x71374491,
|
|
2574
|
+
0xb5c0fbcf,
|
|
2575
|
+
0xe9b5dba5,
|
|
2576
|
+
0x3956c25b,
|
|
2577
|
+
0x59f111f1,
|
|
2578
|
+
0x923f82a4,
|
|
2579
|
+
0xab1c5ed5,
|
|
2580
|
+
0xd807aa98,
|
|
2581
|
+
0x12835b01,
|
|
2582
|
+
0x243185be,
|
|
2583
|
+
0x550c7dc3,
|
|
2584
|
+
0x72be5d74,
|
|
2585
|
+
0x80deb1fe,
|
|
2586
|
+
0x9bdc06a7,
|
|
2587
|
+
0xc19bf174,
|
|
2588
|
+
0xe49b69c1,
|
|
2589
|
+
0xefbe4786,
|
|
2590
|
+
0x0fc19dc6,
|
|
2591
|
+
0x240ca1cc,
|
|
2592
|
+
0x2de92c6f,
|
|
2593
|
+
0x4a7484aa,
|
|
2594
|
+
0x5cb0a9dc,
|
|
2595
|
+
0x76f988da,
|
|
2596
|
+
0x983e5152,
|
|
2597
|
+
0xa831c66d,
|
|
2598
|
+
0xb00327c8,
|
|
2599
|
+
0xbf597fc7,
|
|
2600
|
+
0xc6e00bf3,
|
|
2601
|
+
0xd5a79147,
|
|
2602
|
+
0x06ca6351,
|
|
2603
|
+
0x14292967,
|
|
2604
|
+
0x27b70a85,
|
|
2605
|
+
0x2e1b2138,
|
|
2606
|
+
0x4d2c6dfc,
|
|
2607
|
+
0x53380d13,
|
|
2608
|
+
0x650a7354,
|
|
2609
|
+
0x766a0abb,
|
|
2610
|
+
0x81c2c92e,
|
|
2611
|
+
0x92722c85,
|
|
2612
|
+
0xa2bfe8a1,
|
|
2613
|
+
0xa81a664b,
|
|
2614
|
+
0xc24b8b70,
|
|
2615
|
+
0xc76c51a3,
|
|
2616
|
+
0xd192e819,
|
|
2617
|
+
0xd6990624,
|
|
2618
|
+
0xf40e3585,
|
|
2619
|
+
0x106aa070,
|
|
2620
|
+
0x19a4c116,
|
|
2621
|
+
0x1e376c08,
|
|
2622
|
+
0x2748774c,
|
|
2623
|
+
0x34b0bcb5,
|
|
2624
|
+
0x391c0cb3,
|
|
2625
|
+
0x4ed8aa4a,
|
|
2626
|
+
0x5b9cca4f,
|
|
2627
|
+
0x682e6ff3,
|
|
2628
|
+
0x748f82ee,
|
|
2629
|
+
0x78a5636f,
|
|
2630
|
+
0x84c87814,
|
|
2631
|
+
0x8cc70208,
|
|
2632
|
+
0x90befffa,
|
|
2633
|
+
0xa4506ceb,
|
|
2634
|
+
0xbef9a3f7,
|
|
2635
|
+
0xc67178f2
|
|
2636
|
+
]);
|
|
2637
|
+
/** Reusable temporary buffer. "W" comes straight from spec. */ const SHA256_W = /* @__PURE__ */ new Uint32Array(64);
|
|
2638
|
+
class SHA256 extends HashMD {
|
|
2639
|
+
get() {
|
|
2640
|
+
const { A, B, C, D, E, F, G, H } = this;
|
|
2641
|
+
return [
|
|
2642
|
+
A,
|
|
2643
|
+
B,
|
|
2644
|
+
C,
|
|
2645
|
+
D,
|
|
2646
|
+
E,
|
|
2647
|
+
F,
|
|
2648
|
+
G,
|
|
2649
|
+
H
|
|
2650
|
+
];
|
|
2651
|
+
}
|
|
2652
|
+
// prettier-ignore
|
|
2653
|
+
set(A, B, C, D, E, F, G, H) {
|
|
2654
|
+
this.A = A | 0;
|
|
2655
|
+
this.B = B | 0;
|
|
2656
|
+
this.C = C | 0;
|
|
2657
|
+
this.D = D | 0;
|
|
2658
|
+
this.E = E | 0;
|
|
2659
|
+
this.F = F | 0;
|
|
2660
|
+
this.G = G | 0;
|
|
2661
|
+
this.H = H | 0;
|
|
2662
|
+
}
|
|
2663
|
+
process(view, offset) {
|
|
2664
|
+
// Extend the first 16 words into the remaining 48 words w[16..63] of the message schedule array
|
|
2665
|
+
for(let i = 0; i < 16; i++, offset += 4)SHA256_W[i] = view.getUint32(offset, false);
|
|
2666
|
+
for(let i = 16; i < 64; i++){
|
|
2667
|
+
const W15 = SHA256_W[i - 15];
|
|
2668
|
+
const W2 = SHA256_W[i - 2];
|
|
2669
|
+
const s0 = rotr(W15, 7) ^ rotr(W15, 18) ^ W15 >>> 3;
|
|
2670
|
+
const s1 = rotr(W2, 17) ^ rotr(W2, 19) ^ W2 >>> 10;
|
|
2671
|
+
SHA256_W[i] = s1 + SHA256_W[i - 7] + s0 + SHA256_W[i - 16] | 0;
|
|
2672
|
+
}
|
|
2673
|
+
// Compression function main loop, 64 rounds
|
|
2674
|
+
let { A, B, C, D, E, F, G, H } = this;
|
|
2675
|
+
for(let i = 0; i < 64; i++){
|
|
2676
|
+
const sigma1 = rotr(E, 6) ^ rotr(E, 11) ^ rotr(E, 25);
|
|
2677
|
+
const T1 = H + sigma1 + Chi(E, F, G) + SHA256_K[i] + SHA256_W[i] | 0;
|
|
2678
|
+
const sigma0 = rotr(A, 2) ^ rotr(A, 13) ^ rotr(A, 22);
|
|
2679
|
+
const T2 = sigma0 + Maj(A, B, C) | 0;
|
|
2680
|
+
H = G;
|
|
2681
|
+
G = F;
|
|
2682
|
+
F = E;
|
|
2683
|
+
E = D + T1 | 0;
|
|
2684
|
+
D = C;
|
|
2685
|
+
C = B;
|
|
2686
|
+
B = A;
|
|
2687
|
+
A = T1 + T2 | 0;
|
|
2688
|
+
}
|
|
2689
|
+
// Add the compressed chunk to the current hash value
|
|
2690
|
+
A = A + this.A | 0;
|
|
2691
|
+
B = B + this.B | 0;
|
|
2692
|
+
C = C + this.C | 0;
|
|
2693
|
+
D = D + this.D | 0;
|
|
2694
|
+
E = E + this.E | 0;
|
|
2695
|
+
F = F + this.F | 0;
|
|
2696
|
+
G = G + this.G | 0;
|
|
2697
|
+
H = H + this.H | 0;
|
|
2698
|
+
this.set(A, B, C, D, E, F, G, H);
|
|
2699
|
+
}
|
|
2700
|
+
roundClean() {
|
|
2701
|
+
clean(SHA256_W);
|
|
2702
|
+
}
|
|
2703
|
+
destroy() {
|
|
2704
|
+
this.set(0, 0, 0, 0, 0, 0, 0, 0);
|
|
2705
|
+
clean(this.buffer);
|
|
2706
|
+
}
|
|
2707
|
+
constructor(outputLen = 32){
|
|
2708
|
+
super(64, outputLen, 8, false);
|
|
2709
|
+
// We cannot use array here since array allows indexing by variable
|
|
2710
|
+
// which means optimizer/compiler cannot use registers.
|
|
2711
|
+
this.A = SHA256_IV[0] | 0;
|
|
2712
|
+
this.B = SHA256_IV[1] | 0;
|
|
2713
|
+
this.C = SHA256_IV[2] | 0;
|
|
2714
|
+
this.D = SHA256_IV[3] | 0;
|
|
2715
|
+
this.E = SHA256_IV[4] | 0;
|
|
2716
|
+
this.F = SHA256_IV[5] | 0;
|
|
2717
|
+
this.G = SHA256_IV[6] | 0;
|
|
2718
|
+
this.H = SHA256_IV[7] | 0;
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
class SHA224 extends SHA256 {
|
|
2722
|
+
constructor(){
|
|
2723
|
+
super(28);
|
|
2724
|
+
this.A = SHA224_IV[0] | 0;
|
|
2725
|
+
this.B = SHA224_IV[1] | 0;
|
|
2726
|
+
this.C = SHA224_IV[2] | 0;
|
|
2727
|
+
this.D = SHA224_IV[3] | 0;
|
|
2728
|
+
this.E = SHA224_IV[4] | 0;
|
|
2729
|
+
this.F = SHA224_IV[5] | 0;
|
|
2730
|
+
this.G = SHA224_IV[6] | 0;
|
|
2731
|
+
this.H = SHA224_IV[7] | 0;
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
// SHA2-512 is slower than sha256 in js because u64 operations are slow.
|
|
2735
|
+
// Round contants
|
|
2736
|
+
// First 32 bits of the fractional parts of the cube roots of the first 80 primes 2..409
|
|
2737
|
+
// prettier-ignore
|
|
2738
|
+
const K512 = /* @__PURE__ */ (()=>split([
|
|
2739
|
+
'0x428a2f98d728ae22',
|
|
2740
|
+
'0x7137449123ef65cd',
|
|
2741
|
+
'0xb5c0fbcfec4d3b2f',
|
|
2742
|
+
'0xe9b5dba58189dbbc',
|
|
2743
|
+
'0x3956c25bf348b538',
|
|
2744
|
+
'0x59f111f1b605d019',
|
|
2745
|
+
'0x923f82a4af194f9b',
|
|
2746
|
+
'0xab1c5ed5da6d8118',
|
|
2747
|
+
'0xd807aa98a3030242',
|
|
2748
|
+
'0x12835b0145706fbe',
|
|
2749
|
+
'0x243185be4ee4b28c',
|
|
2750
|
+
'0x550c7dc3d5ffb4e2',
|
|
2751
|
+
'0x72be5d74f27b896f',
|
|
2752
|
+
'0x80deb1fe3b1696b1',
|
|
2753
|
+
'0x9bdc06a725c71235',
|
|
2754
|
+
'0xc19bf174cf692694',
|
|
2755
|
+
'0xe49b69c19ef14ad2',
|
|
2756
|
+
'0xefbe4786384f25e3',
|
|
2757
|
+
'0x0fc19dc68b8cd5b5',
|
|
2758
|
+
'0x240ca1cc77ac9c65',
|
|
2759
|
+
'0x2de92c6f592b0275',
|
|
2760
|
+
'0x4a7484aa6ea6e483',
|
|
2761
|
+
'0x5cb0a9dcbd41fbd4',
|
|
2762
|
+
'0x76f988da831153b5',
|
|
2763
|
+
'0x983e5152ee66dfab',
|
|
2764
|
+
'0xa831c66d2db43210',
|
|
2765
|
+
'0xb00327c898fb213f',
|
|
2766
|
+
'0xbf597fc7beef0ee4',
|
|
2767
|
+
'0xc6e00bf33da88fc2',
|
|
2768
|
+
'0xd5a79147930aa725',
|
|
2769
|
+
'0x06ca6351e003826f',
|
|
2770
|
+
'0x142929670a0e6e70',
|
|
2771
|
+
'0x27b70a8546d22ffc',
|
|
2772
|
+
'0x2e1b21385c26c926',
|
|
2773
|
+
'0x4d2c6dfc5ac42aed',
|
|
2774
|
+
'0x53380d139d95b3df',
|
|
2775
|
+
'0x650a73548baf63de',
|
|
2776
|
+
'0x766a0abb3c77b2a8',
|
|
2777
|
+
'0x81c2c92e47edaee6',
|
|
2778
|
+
'0x92722c851482353b',
|
|
2779
|
+
'0xa2bfe8a14cf10364',
|
|
2780
|
+
'0xa81a664bbc423001',
|
|
2781
|
+
'0xc24b8b70d0f89791',
|
|
2782
|
+
'0xc76c51a30654be30',
|
|
2783
|
+
'0xd192e819d6ef5218',
|
|
2784
|
+
'0xd69906245565a910',
|
|
2785
|
+
'0xf40e35855771202a',
|
|
2786
|
+
'0x106aa07032bbd1b8',
|
|
2787
|
+
'0x19a4c116b8d2d0c8',
|
|
2788
|
+
'0x1e376c085141ab53',
|
|
2789
|
+
'0x2748774cdf8eeb99',
|
|
2790
|
+
'0x34b0bcb5e19b48a8',
|
|
2791
|
+
'0x391c0cb3c5c95a63',
|
|
2792
|
+
'0x4ed8aa4ae3418acb',
|
|
2793
|
+
'0x5b9cca4f7763e373',
|
|
2794
|
+
'0x682e6ff3d6b2b8a3',
|
|
2795
|
+
'0x748f82ee5defb2fc',
|
|
2796
|
+
'0x78a5636f43172f60',
|
|
2797
|
+
'0x84c87814a1f0ab72',
|
|
2798
|
+
'0x8cc702081a6439ec',
|
|
2799
|
+
'0x90befffa23631e28',
|
|
2800
|
+
'0xa4506cebde82bde9',
|
|
2801
|
+
'0xbef9a3f7b2c67915',
|
|
2802
|
+
'0xc67178f2e372532b',
|
|
2803
|
+
'0xca273eceea26619c',
|
|
2804
|
+
'0xd186b8c721c0c207',
|
|
2805
|
+
'0xeada7dd6cde0eb1e',
|
|
2806
|
+
'0xf57d4f7fee6ed178',
|
|
2807
|
+
'0x06f067aa72176fba',
|
|
2808
|
+
'0x0a637dc5a2c898a6',
|
|
2809
|
+
'0x113f9804bef90dae',
|
|
2810
|
+
'0x1b710b35131c471b',
|
|
2811
|
+
'0x28db77f523047d84',
|
|
2812
|
+
'0x32caab7b40c72493',
|
|
2813
|
+
'0x3c9ebe0a15c9bebc',
|
|
2814
|
+
'0x431d67c49c100d4c',
|
|
2815
|
+
'0x4cc5d4becb3e42b6',
|
|
2816
|
+
'0x597f299cfc657e2a',
|
|
2817
|
+
'0x5fcb6fab3ad6faec',
|
|
2818
|
+
'0x6c44198c4a475817'
|
|
2819
|
+
].map((n)=>BigInt(n))))();
|
|
2820
|
+
const SHA512_Kh = /* @__PURE__ */ (()=>K512[0])();
|
|
2821
|
+
const SHA512_Kl = /* @__PURE__ */ (()=>K512[1])();
|
|
2822
|
+
// Reusable temporary buffers
|
|
2823
|
+
const SHA512_W_H = /* @__PURE__ */ new Uint32Array(80);
|
|
2824
|
+
const SHA512_W_L = /* @__PURE__ */ new Uint32Array(80);
|
|
2825
|
+
class SHA512 extends HashMD {
|
|
2826
|
+
// prettier-ignore
|
|
2827
|
+
get() {
|
|
2828
|
+
const { Ah, Al, Bh, Bl, Ch, Cl, Dh, Dl, Eh, El, Fh, Fl, Gh, Gl, Hh, Hl } = this;
|
|
2829
|
+
return [
|
|
2830
|
+
Ah,
|
|
2831
|
+
Al,
|
|
2832
|
+
Bh,
|
|
2833
|
+
Bl,
|
|
2834
|
+
Ch,
|
|
2835
|
+
Cl,
|
|
2836
|
+
Dh,
|
|
2837
|
+
Dl,
|
|
2838
|
+
Eh,
|
|
2839
|
+
El,
|
|
2840
|
+
Fh,
|
|
2841
|
+
Fl,
|
|
2842
|
+
Gh,
|
|
2843
|
+
Gl,
|
|
2844
|
+
Hh,
|
|
2845
|
+
Hl
|
|
2846
|
+
];
|
|
2847
|
+
}
|
|
2848
|
+
// prettier-ignore
|
|
2849
|
+
set(Ah, Al, Bh, Bl, Ch, Cl, Dh, Dl, Eh, El, Fh, Fl, Gh, Gl, Hh, Hl) {
|
|
2850
|
+
this.Ah = Ah | 0;
|
|
2851
|
+
this.Al = Al | 0;
|
|
2852
|
+
this.Bh = Bh | 0;
|
|
2853
|
+
this.Bl = Bl | 0;
|
|
2854
|
+
this.Ch = Ch | 0;
|
|
2855
|
+
this.Cl = Cl | 0;
|
|
2856
|
+
this.Dh = Dh | 0;
|
|
2857
|
+
this.Dl = Dl | 0;
|
|
2858
|
+
this.Eh = Eh | 0;
|
|
2859
|
+
this.El = El | 0;
|
|
2860
|
+
this.Fh = Fh | 0;
|
|
2861
|
+
this.Fl = Fl | 0;
|
|
2862
|
+
this.Gh = Gh | 0;
|
|
2863
|
+
this.Gl = Gl | 0;
|
|
2864
|
+
this.Hh = Hh | 0;
|
|
2865
|
+
this.Hl = Hl | 0;
|
|
2866
|
+
}
|
|
2867
|
+
process(view, offset) {
|
|
2868
|
+
// Extend the first 16 words into the remaining 64 words w[16..79] of the message schedule array
|
|
2869
|
+
for(let i = 0; i < 16; i++, offset += 4){
|
|
2870
|
+
SHA512_W_H[i] = view.getUint32(offset);
|
|
2871
|
+
SHA512_W_L[i] = view.getUint32(offset += 4);
|
|
2872
|
+
}
|
|
2873
|
+
for(let i = 16; i < 80; i++){
|
|
2874
|
+
// s0 := (w[i-15] rightrotate 1) xor (w[i-15] rightrotate 8) xor (w[i-15] rightshift 7)
|
|
2875
|
+
const W15h = SHA512_W_H[i - 15] | 0;
|
|
2876
|
+
const W15l = SHA512_W_L[i - 15] | 0;
|
|
2877
|
+
const s0h = rotrSH(W15h, W15l, 1) ^ rotrSH(W15h, W15l, 8) ^ shrSH(W15h, W15l, 7);
|
|
2878
|
+
const s0l = rotrSL(W15h, W15l, 1) ^ rotrSL(W15h, W15l, 8) ^ shrSL(W15h, W15l, 7);
|
|
2879
|
+
// s1 := (w[i-2] rightrotate 19) xor (w[i-2] rightrotate 61) xor (w[i-2] rightshift 6)
|
|
2880
|
+
const W2h = SHA512_W_H[i - 2] | 0;
|
|
2881
|
+
const W2l = SHA512_W_L[i - 2] | 0;
|
|
2882
|
+
const s1h = rotrSH(W2h, W2l, 19) ^ rotrBH(W2h, W2l, 61) ^ shrSH(W2h, W2l, 6);
|
|
2883
|
+
const s1l = rotrSL(W2h, W2l, 19) ^ rotrBL(W2h, W2l, 61) ^ shrSL(W2h, W2l, 6);
|
|
2884
|
+
// SHA256_W[i] = s0 + s1 + SHA256_W[i - 7] + SHA256_W[i - 16];
|
|
2885
|
+
const SUMl = add4L(s0l, s1l, SHA512_W_L[i - 7], SHA512_W_L[i - 16]);
|
|
2886
|
+
const SUMh = add4H(SUMl, s0h, s1h, SHA512_W_H[i - 7], SHA512_W_H[i - 16]);
|
|
2887
|
+
SHA512_W_H[i] = SUMh | 0;
|
|
2888
|
+
SHA512_W_L[i] = SUMl | 0;
|
|
2889
|
+
}
|
|
2890
|
+
let { Ah, Al, Bh, Bl, Ch, Cl, Dh, Dl, Eh, El, Fh, Fl, Gh, Gl, Hh, Hl } = this;
|
|
2891
|
+
// Compression function main loop, 80 rounds
|
|
2892
|
+
for(let i = 0; i < 80; i++){
|
|
2893
|
+
// S1 := (e rightrotate 14) xor (e rightrotate 18) xor (e rightrotate 41)
|
|
2894
|
+
const sigma1h = rotrSH(Eh, El, 14) ^ rotrSH(Eh, El, 18) ^ rotrBH(Eh, El, 41);
|
|
2895
|
+
const sigma1l = rotrSL(Eh, El, 14) ^ rotrSL(Eh, El, 18) ^ rotrBL(Eh, El, 41);
|
|
2896
|
+
//const T1 = (H + sigma1 + Chi(E, F, G) + SHA256_K[i] + SHA256_W[i]) | 0;
|
|
2897
|
+
const CHIh = Eh & Fh ^ ~Eh & Gh;
|
|
2898
|
+
const CHIl = El & Fl ^ ~El & Gl;
|
|
2899
|
+
// T1 = H + sigma1 + Chi(E, F, G) + SHA512_K[i] + SHA512_W[i]
|
|
2900
|
+
// prettier-ignore
|
|
2901
|
+
const T1ll = add5L(Hl, sigma1l, CHIl, SHA512_Kl[i], SHA512_W_L[i]);
|
|
2902
|
+
const T1h = add5H(T1ll, Hh, sigma1h, CHIh, SHA512_Kh[i], SHA512_W_H[i]);
|
|
2903
|
+
const T1l = T1ll | 0;
|
|
2904
|
+
// S0 := (a rightrotate 28) xor (a rightrotate 34) xor (a rightrotate 39)
|
|
2905
|
+
const sigma0h = rotrSH(Ah, Al, 28) ^ rotrBH(Ah, Al, 34) ^ rotrBH(Ah, Al, 39);
|
|
2906
|
+
const sigma0l = rotrSL(Ah, Al, 28) ^ rotrBL(Ah, Al, 34) ^ rotrBL(Ah, Al, 39);
|
|
2907
|
+
const MAJh = Ah & Bh ^ Ah & Ch ^ Bh & Ch;
|
|
2908
|
+
const MAJl = Al & Bl ^ Al & Cl ^ Bl & Cl;
|
|
2909
|
+
Hh = Gh | 0;
|
|
2910
|
+
Hl = Gl | 0;
|
|
2911
|
+
Gh = Fh | 0;
|
|
2912
|
+
Gl = Fl | 0;
|
|
2913
|
+
Fh = Eh | 0;
|
|
2914
|
+
Fl = El | 0;
|
|
2915
|
+
({ h: Eh, l: El } = add(Dh | 0, Dl | 0, T1h | 0, T1l | 0));
|
|
2916
|
+
Dh = Ch | 0;
|
|
2917
|
+
Dl = Cl | 0;
|
|
2918
|
+
Ch = Bh | 0;
|
|
2919
|
+
Cl = Bl | 0;
|
|
2920
|
+
Bh = Ah | 0;
|
|
2921
|
+
Bl = Al | 0;
|
|
2922
|
+
const All = add3L(T1l, sigma0l, MAJl);
|
|
2923
|
+
Ah = add3H(All, T1h, sigma0h, MAJh);
|
|
2924
|
+
Al = All | 0;
|
|
2925
|
+
}
|
|
2926
|
+
// Add the compressed chunk to the current hash value
|
|
2927
|
+
({ h: Ah, l: Al } = add(this.Ah | 0, this.Al | 0, Ah | 0, Al | 0));
|
|
2928
|
+
({ h: Bh, l: Bl } = add(this.Bh | 0, this.Bl | 0, Bh | 0, Bl | 0));
|
|
2929
|
+
({ h: Ch, l: Cl } = add(this.Ch | 0, this.Cl | 0, Ch | 0, Cl | 0));
|
|
2930
|
+
({ h: Dh, l: Dl } = add(this.Dh | 0, this.Dl | 0, Dh | 0, Dl | 0));
|
|
2931
|
+
({ h: Eh, l: El } = add(this.Eh | 0, this.El | 0, Eh | 0, El | 0));
|
|
2932
|
+
({ h: Fh, l: Fl } = add(this.Fh | 0, this.Fl | 0, Fh | 0, Fl | 0));
|
|
2933
|
+
({ h: Gh, l: Gl } = add(this.Gh | 0, this.Gl | 0, Gh | 0, Gl | 0));
|
|
2934
|
+
({ h: Hh, l: Hl } = add(this.Hh | 0, this.Hl | 0, Hh | 0, Hl | 0));
|
|
2935
|
+
this.set(Ah, Al, Bh, Bl, Ch, Cl, Dh, Dl, Eh, El, Fh, Fl, Gh, Gl, Hh, Hl);
|
|
2936
|
+
}
|
|
2937
|
+
roundClean() {
|
|
2938
|
+
clean(SHA512_W_H, SHA512_W_L);
|
|
2939
|
+
}
|
|
2940
|
+
destroy() {
|
|
2941
|
+
clean(this.buffer);
|
|
2942
|
+
this.set(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
|
|
2943
|
+
}
|
|
2944
|
+
constructor(outputLen = 64){
|
|
2945
|
+
super(128, outputLen, 16, false);
|
|
2946
|
+
// We cannot use array here since array allows indexing by variable
|
|
2947
|
+
// which means optimizer/compiler cannot use registers.
|
|
2948
|
+
// h -- high 32 bits, l -- low 32 bits
|
|
2949
|
+
this.Ah = SHA512_IV[0] | 0;
|
|
2950
|
+
this.Al = SHA512_IV[1] | 0;
|
|
2951
|
+
this.Bh = SHA512_IV[2] | 0;
|
|
2952
|
+
this.Bl = SHA512_IV[3] | 0;
|
|
2953
|
+
this.Ch = SHA512_IV[4] | 0;
|
|
2954
|
+
this.Cl = SHA512_IV[5] | 0;
|
|
2955
|
+
this.Dh = SHA512_IV[6] | 0;
|
|
2956
|
+
this.Dl = SHA512_IV[7] | 0;
|
|
2957
|
+
this.Eh = SHA512_IV[8] | 0;
|
|
2958
|
+
this.El = SHA512_IV[9] | 0;
|
|
2959
|
+
this.Fh = SHA512_IV[10] | 0;
|
|
2960
|
+
this.Fl = SHA512_IV[11] | 0;
|
|
2961
|
+
this.Gh = SHA512_IV[12] | 0;
|
|
2962
|
+
this.Gl = SHA512_IV[13] | 0;
|
|
2963
|
+
this.Hh = SHA512_IV[14] | 0;
|
|
2964
|
+
this.Hl = SHA512_IV[15] | 0;
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
class SHA384 extends SHA512 {
|
|
2968
|
+
constructor(){
|
|
2969
|
+
super(48);
|
|
2970
|
+
this.Ah = SHA384_IV[0] | 0;
|
|
2971
|
+
this.Al = SHA384_IV[1] | 0;
|
|
2972
|
+
this.Bh = SHA384_IV[2] | 0;
|
|
2973
|
+
this.Bl = SHA384_IV[3] | 0;
|
|
2974
|
+
this.Ch = SHA384_IV[4] | 0;
|
|
2975
|
+
this.Cl = SHA384_IV[5] | 0;
|
|
2976
|
+
this.Dh = SHA384_IV[6] | 0;
|
|
2977
|
+
this.Dl = SHA384_IV[7] | 0;
|
|
2978
|
+
this.Eh = SHA384_IV[8] | 0;
|
|
2979
|
+
this.El = SHA384_IV[9] | 0;
|
|
2980
|
+
this.Fh = SHA384_IV[10] | 0;
|
|
2981
|
+
this.Fl = SHA384_IV[11] | 0;
|
|
2982
|
+
this.Gh = SHA384_IV[12] | 0;
|
|
2983
|
+
this.Gl = SHA384_IV[13] | 0;
|
|
2984
|
+
this.Hh = SHA384_IV[14] | 0;
|
|
2985
|
+
this.Hl = SHA384_IV[15] | 0;
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
/**
|
|
2989
|
+
* SHA2-256 hash function from RFC 4634.
|
|
2990
|
+
*
|
|
2991
|
+
* It is the fastest JS hash, even faster than Blake3.
|
|
2992
|
+
* To break sha256 using birthday attack, attackers need to try 2^128 hashes.
|
|
2993
|
+
* BTC network is doing 2^70 hashes/sec (2^95 hashes/year) as per 2025.
|
|
2994
|
+
*/ const sha256 = /* @__PURE__ */ createHasher(()=>new SHA256());
|
|
2995
|
+
/** SHA2-224 hash function from RFC 4634 */ const sha224 = /* @__PURE__ */ createHasher(()=>new SHA224());
|
|
2996
|
+
/** SHA2-512 hash function from RFC 4634. */ const sha512 = /* @__PURE__ */ createHasher(()=>new SHA512());
|
|
2997
|
+
/** SHA2-384 hash function from RFC 4634. */ const sha384 = /* @__PURE__ */ createHasher(()=>new SHA384());
|
|
2998
|
+
|
|
2999
|
+
// No __PURE__ annotations in sha3 header:
|
|
3000
|
+
// EVERYTHING is in fact used on every export.
|
|
3001
|
+
// Various per round constants calculations
|
|
3002
|
+
const _0n = BigInt(0);
|
|
3003
|
+
const _1n = BigInt(1);
|
|
3004
|
+
const _2n = BigInt(2);
|
|
3005
|
+
const _7n = BigInt(7);
|
|
3006
|
+
const _256n = BigInt(256);
|
|
3007
|
+
const _0x71n = BigInt(0x71);
|
|
3008
|
+
const SHA3_PI = [];
|
|
3009
|
+
const SHA3_ROTL = [];
|
|
3010
|
+
const _SHA3_IOTA = [];
|
|
3011
|
+
for(let round = 0, R = _1n, x = 1, y = 0; round < 24; round++){
|
|
3012
|
+
// Pi
|
|
3013
|
+
[x, y] = [
|
|
3014
|
+
y,
|
|
3015
|
+
(2 * x + 3 * y) % 5
|
|
3016
|
+
];
|
|
3017
|
+
SHA3_PI.push(2 * (5 * y + x));
|
|
3018
|
+
// Rotational
|
|
3019
|
+
SHA3_ROTL.push((round + 1) * (round + 2) / 2 % 64);
|
|
3020
|
+
// Iota
|
|
3021
|
+
let t = _0n;
|
|
3022
|
+
for(let j = 0; j < 7; j++){
|
|
3023
|
+
R = (R << _1n ^ (R >> _7n) * _0x71n) % _256n;
|
|
3024
|
+
if (R & _2n) t ^= _1n << (_1n << /* @__PURE__ */ BigInt(j)) - _1n;
|
|
3025
|
+
}
|
|
3026
|
+
_SHA3_IOTA.push(t);
|
|
3027
|
+
}
|
|
3028
|
+
const IOTAS = split(_SHA3_IOTA, true);
|
|
3029
|
+
const SHA3_IOTA_H = IOTAS[0];
|
|
3030
|
+
const SHA3_IOTA_L = IOTAS[1];
|
|
3031
|
+
// Left rotation (without 0, 32, 64)
|
|
3032
|
+
const rotlH = (h, l, s)=>s > 32 ? rotlBH(h, l, s) : rotlSH(h, l, s);
|
|
3033
|
+
const rotlL = (h, l, s)=>s > 32 ? rotlBL(h, l, s) : rotlSL(h, l, s);
|
|
3034
|
+
/** `keccakf1600` internal function, additionally allows to adjust round count. */ function keccakP(s, rounds = 24) {
|
|
3035
|
+
const B = new Uint32Array(5 * 2);
|
|
3036
|
+
// NOTE: all indices are x2 since we store state as u32 instead of u64 (bigints to slow in js)
|
|
3037
|
+
for(let round = 24 - rounds; round < 24; round++){
|
|
3038
|
+
// Theta θ
|
|
3039
|
+
for(let x = 0; x < 10; x++)B[x] = s[x] ^ s[x + 10] ^ s[x + 20] ^ s[x + 30] ^ s[x + 40];
|
|
3040
|
+
for(let x = 0; x < 10; x += 2){
|
|
3041
|
+
const idx1 = (x + 8) % 10;
|
|
3042
|
+
const idx0 = (x + 2) % 10;
|
|
3043
|
+
const B0 = B[idx0];
|
|
3044
|
+
const B1 = B[idx0 + 1];
|
|
3045
|
+
const Th = rotlH(B0, B1, 1) ^ B[idx1];
|
|
3046
|
+
const Tl = rotlL(B0, B1, 1) ^ B[idx1 + 1];
|
|
3047
|
+
for(let y = 0; y < 50; y += 10){
|
|
3048
|
+
s[x + y] ^= Th;
|
|
3049
|
+
s[x + y + 1] ^= Tl;
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
// Rho (ρ) and Pi (π)
|
|
3053
|
+
let curH = s[2];
|
|
3054
|
+
let curL = s[3];
|
|
3055
|
+
for(let t = 0; t < 24; t++){
|
|
3056
|
+
const shift = SHA3_ROTL[t];
|
|
3057
|
+
const Th = rotlH(curH, curL, shift);
|
|
3058
|
+
const Tl = rotlL(curH, curL, shift);
|
|
3059
|
+
const PI = SHA3_PI[t];
|
|
3060
|
+
curH = s[PI];
|
|
3061
|
+
curL = s[PI + 1];
|
|
3062
|
+
s[PI] = Th;
|
|
3063
|
+
s[PI + 1] = Tl;
|
|
3064
|
+
}
|
|
3065
|
+
// Chi (χ)
|
|
3066
|
+
for(let y = 0; y < 50; y += 10){
|
|
3067
|
+
for(let x = 0; x < 10; x++)B[x] = s[y + x];
|
|
3068
|
+
for(let x = 0; x < 10; x++)s[y + x] ^= ~B[(x + 2) % 10] & B[(x + 4) % 10];
|
|
3069
|
+
}
|
|
3070
|
+
// Iota (ι)
|
|
3071
|
+
s[0] ^= SHA3_IOTA_H[round];
|
|
3072
|
+
s[1] ^= SHA3_IOTA_L[round];
|
|
3073
|
+
}
|
|
3074
|
+
clean(B);
|
|
3075
|
+
}
|
|
3076
|
+
/** Keccak sponge function. */ class Keccak extends Hash {
|
|
3077
|
+
clone() {
|
|
3078
|
+
return this._cloneInto();
|
|
3079
|
+
}
|
|
3080
|
+
keccak() {
|
|
3081
|
+
swap32IfBE(this.state32);
|
|
3082
|
+
keccakP(this.state32, this.rounds);
|
|
3083
|
+
swap32IfBE(this.state32);
|
|
3084
|
+
this.posOut = 0;
|
|
3085
|
+
this.pos = 0;
|
|
3086
|
+
}
|
|
3087
|
+
update(data) {
|
|
3088
|
+
aexists(this);
|
|
3089
|
+
data = toBytes(data);
|
|
3090
|
+
abytes(data);
|
|
3091
|
+
const { blockLen, state } = this;
|
|
3092
|
+
const len = data.length;
|
|
3093
|
+
for(let pos = 0; pos < len;){
|
|
3094
|
+
const take = Math.min(blockLen - this.pos, len - pos);
|
|
3095
|
+
for(let i = 0; i < take; i++)state[this.pos++] ^= data[pos++];
|
|
3096
|
+
if (this.pos === blockLen) this.keccak();
|
|
3097
|
+
}
|
|
3098
|
+
return this;
|
|
3099
|
+
}
|
|
3100
|
+
finish() {
|
|
3101
|
+
if (this.finished) return;
|
|
3102
|
+
this.finished = true;
|
|
3103
|
+
const { state, suffix, pos, blockLen } = this;
|
|
3104
|
+
// Do the padding
|
|
3105
|
+
state[pos] ^= suffix;
|
|
3106
|
+
if ((suffix & 0x80) !== 0 && pos === blockLen - 1) this.keccak();
|
|
3107
|
+
state[blockLen - 1] ^= 0x80;
|
|
3108
|
+
this.keccak();
|
|
3109
|
+
}
|
|
3110
|
+
writeInto(out) {
|
|
3111
|
+
aexists(this, false);
|
|
3112
|
+
abytes(out);
|
|
3113
|
+
this.finish();
|
|
3114
|
+
const bufferOut = this.state;
|
|
3115
|
+
const { blockLen } = this;
|
|
3116
|
+
for(let pos = 0, len = out.length; pos < len;){
|
|
3117
|
+
if (this.posOut >= blockLen) this.keccak();
|
|
3118
|
+
const take = Math.min(blockLen - this.posOut, len - pos);
|
|
3119
|
+
out.set(bufferOut.subarray(this.posOut, this.posOut + take), pos);
|
|
3120
|
+
this.posOut += take;
|
|
3121
|
+
pos += take;
|
|
3122
|
+
}
|
|
3123
|
+
return out;
|
|
3124
|
+
}
|
|
3125
|
+
xofInto(out) {
|
|
3126
|
+
// Sha3/Keccak usage with XOF is probably mistake, only SHAKE instances can do XOF
|
|
3127
|
+
if (!this.enableXOF) throw new Error('XOF is not possible for this instance');
|
|
3128
|
+
return this.writeInto(out);
|
|
3129
|
+
}
|
|
3130
|
+
xof(bytes) {
|
|
3131
|
+
anumber(bytes);
|
|
3132
|
+
return this.xofInto(new Uint8Array(bytes));
|
|
3133
|
+
}
|
|
3134
|
+
digestInto(out) {
|
|
3135
|
+
aoutput(out, this);
|
|
3136
|
+
if (this.finished) throw new Error('digest() was already called');
|
|
3137
|
+
this.writeInto(out);
|
|
3138
|
+
this.destroy();
|
|
3139
|
+
return out;
|
|
3140
|
+
}
|
|
3141
|
+
digest() {
|
|
3142
|
+
return this.digestInto(new Uint8Array(this.outputLen));
|
|
3143
|
+
}
|
|
3144
|
+
destroy() {
|
|
3145
|
+
this.destroyed = true;
|
|
3146
|
+
clean(this.state);
|
|
3147
|
+
}
|
|
3148
|
+
_cloneInto(to) {
|
|
3149
|
+
const { blockLen, suffix, outputLen, rounds, enableXOF } = this;
|
|
3150
|
+
to || (to = new Keccak(blockLen, suffix, outputLen, enableXOF, rounds));
|
|
3151
|
+
to.state32.set(this.state32);
|
|
3152
|
+
to.pos = this.pos;
|
|
3153
|
+
to.posOut = this.posOut;
|
|
3154
|
+
to.finished = this.finished;
|
|
3155
|
+
to.rounds = rounds;
|
|
3156
|
+
// Suffix can change in cSHAKE
|
|
3157
|
+
to.suffix = suffix;
|
|
3158
|
+
to.outputLen = outputLen;
|
|
3159
|
+
to.enableXOF = enableXOF;
|
|
3160
|
+
to.destroyed = this.destroyed;
|
|
3161
|
+
return to;
|
|
3162
|
+
}
|
|
3163
|
+
// NOTE: we accept arguments in bytes instead of bits here.
|
|
3164
|
+
constructor(blockLen, suffix, outputLen, enableXOF = false, rounds = 24){
|
|
3165
|
+
super();
|
|
3166
|
+
this.pos = 0;
|
|
3167
|
+
this.posOut = 0;
|
|
3168
|
+
this.finished = false;
|
|
3169
|
+
this.destroyed = false;
|
|
3170
|
+
this.enableXOF = false;
|
|
3171
|
+
this.blockLen = blockLen;
|
|
3172
|
+
this.suffix = suffix;
|
|
3173
|
+
this.outputLen = outputLen;
|
|
3174
|
+
this.enableXOF = enableXOF;
|
|
3175
|
+
this.rounds = rounds;
|
|
3176
|
+
// Can be passed from user as dkLen
|
|
3177
|
+
anumber(outputLen);
|
|
3178
|
+
// 1600 = 5x5 matrix of 64bit. 1600 bits === 200 bytes
|
|
3179
|
+
// 0 < blockLen < 200
|
|
3180
|
+
if (!(0 < blockLen && blockLen < 200)) throw new Error('only keccak-f1600 function is supported');
|
|
3181
|
+
this.state = new Uint8Array(200);
|
|
3182
|
+
this.state32 = u32(this.state);
|
|
3183
|
+
}
|
|
3184
|
+
}
|
|
3185
|
+
const gen = (suffix, blockLen, outputLen)=>createHasher(()=>new Keccak(blockLen, suffix, outputLen));
|
|
3186
|
+
/** SHA3-224 hash function. */ const sha3_224 = /* @__PURE__ */ (()=>gen(0x06, 144, 224 / 8))();
|
|
3187
|
+
/** SHA3-256 hash function. Different from keccak-256. */ const sha3_256 = /* @__PURE__ */ (()=>gen(0x06, 136, 256 / 8))();
|
|
3188
|
+
/** SHA3-384 hash function. */ const sha3_384 = /* @__PURE__ */ (()=>gen(0x06, 104, 384 / 8))();
|
|
3189
|
+
/** SHA3-512 hash function. */ const sha3_512 = /* @__PURE__ */ (()=>gen(0x06, 72, 512 / 8))();
|
|
3190
|
+
|
|
3191
|
+
/**
|
|
3192
|
+
* "globalThis" ponyfill.
|
|
3193
|
+
* @see [A horrifying globalThis polyfill in universal JavaScript](https://mathiasbynens.be/notes/globalthis)
|
|
3194
|
+
* @type {Object.<string, *>}
|
|
3195
|
+
*/ const globalScope = (()=>{
|
|
3196
|
+
if (typeof globalThis === "object") return globalThis;
|
|
3197
|
+
else {
|
|
3198
|
+
Object.defineProperty(Object.prototype, "__GLOBALTHIS__", {
|
|
3199
|
+
get () {
|
|
3200
|
+
return this;
|
|
3201
|
+
},
|
|
3202
|
+
configurable: true
|
|
3203
|
+
});
|
|
3204
|
+
try {
|
|
3205
|
+
// @ts-expect-error
|
|
3206
|
+
// eslint-disable-next-line no-undef
|
|
3207
|
+
if (typeof __GLOBALTHIS__ !== "undefined") return __GLOBALTHIS__;
|
|
3208
|
+
} finally{
|
|
3209
|
+
// @ts-expect-error
|
|
3210
|
+
delete Object.prototype.__GLOBALTHIS__;
|
|
3211
|
+
}
|
|
3212
|
+
}
|
|
3213
|
+
// Still unable to determine "globalThis", fall back to a naive method.
|
|
3214
|
+
if (typeof self !== "undefined") return self;
|
|
3215
|
+
else if (typeof window !== "undefined") return window;
|
|
3216
|
+
else if (typeof global !== "undefined") return global;
|
|
3217
|
+
return undefined;
|
|
3218
|
+
})();
|
|
3219
|
+
|
|
3220
|
+
/**
|
|
3221
|
+
* @noble/hashes hash functions.
|
|
3222
|
+
* @type {Object.<string, sha1|sha224|sha256|sha384|sha512|sha3_224|sha3_256|sha3_384|sha3_512>}
|
|
3223
|
+
*/ const nobleHashes = {
|
|
3224
|
+
SHA1: sha1,
|
|
3225
|
+
SHA224: sha224,
|
|
3226
|
+
SHA256: sha256,
|
|
3227
|
+
SHA384: sha384,
|
|
3228
|
+
SHA512: sha512,
|
|
3229
|
+
"SHA3-224": sha3_224,
|
|
3230
|
+
"SHA3-256": sha3_256,
|
|
3231
|
+
"SHA3-384": sha3_384,
|
|
3232
|
+
"SHA3-512": sha3_512
|
|
3233
|
+
};
|
|
3234
|
+
/**
|
|
3235
|
+
* Canonicalizes a hash algorithm name.
|
|
3236
|
+
* @param {string} algorithm Hash algorithm name.
|
|
3237
|
+
* @returns {"SHA1"|"SHA224"|"SHA256"|"SHA384"|"SHA512"|"SHA3-224"|"SHA3-256"|"SHA3-384"|"SHA3-512"} Canonicalized hash algorithm name.
|
|
3238
|
+
*/ const canonicalizeAlgorithm = (algorithm)=>{
|
|
3239
|
+
switch(true){
|
|
3240
|
+
case /^(?:SHA-?1|SSL3-SHA1)$/i.test(algorithm):
|
|
3241
|
+
return "SHA1";
|
|
3242
|
+
case /^SHA(?:2?-)?224$/i.test(algorithm):
|
|
3243
|
+
return "SHA224";
|
|
3244
|
+
case /^SHA(?:2?-)?256$/i.test(algorithm):
|
|
3245
|
+
return "SHA256";
|
|
3246
|
+
case /^SHA(?:2?-)?384$/i.test(algorithm):
|
|
3247
|
+
return "SHA384";
|
|
3248
|
+
case /^SHA(?:2?-)?512$/i.test(algorithm):
|
|
3249
|
+
return "SHA512";
|
|
3250
|
+
case /^SHA3-224$/i.test(algorithm):
|
|
3251
|
+
return "SHA3-224";
|
|
3252
|
+
case /^SHA3-256$/i.test(algorithm):
|
|
3253
|
+
return "SHA3-256";
|
|
3254
|
+
case /^SHA3-384$/i.test(algorithm):
|
|
3255
|
+
return "SHA3-384";
|
|
3256
|
+
case /^SHA3-512$/i.test(algorithm):
|
|
3257
|
+
return "SHA3-512";
|
|
3258
|
+
default:
|
|
3259
|
+
throw new TypeError(`Unknown hash algorithm: ${algorithm}`);
|
|
3260
|
+
}
|
|
3261
|
+
};
|
|
3262
|
+
/**
|
|
3263
|
+
* Calculates an HMAC digest.
|
|
3264
|
+
* @param {string} algorithm Algorithm.
|
|
3265
|
+
* @param {Uint8Array} key Key.
|
|
3266
|
+
* @param {Uint8Array} message Message.
|
|
3267
|
+
* @returns {Uint8Array} Digest.
|
|
3268
|
+
*/ const hmacDigest = (algorithm, key, message)=>{
|
|
3269
|
+
if (hmac) {
|
|
3270
|
+
const hash = nobleHashes[algorithm] ?? nobleHashes[canonicalizeAlgorithm(algorithm)];
|
|
3271
|
+
return hmac(hash, key, message);
|
|
3272
|
+
} else {
|
|
3273
|
+
throw new Error("Missing HMAC function");
|
|
3274
|
+
}
|
|
3275
|
+
};
|
|
3276
|
+
|
|
3277
|
+
/**
|
|
3278
|
+
* RFC 4648 base32 alphabet without pad.
|
|
3279
|
+
* @type {string}
|
|
3280
|
+
*/ const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
3281
|
+
/**
|
|
3282
|
+
* Converts a base32 string to an Uint8Array (RFC 4648).
|
|
3283
|
+
* @see [LinusU/base32-decode](https://github.com/LinusU/base32-decode)
|
|
3284
|
+
* @param {string} str Base32 string.
|
|
3285
|
+
* @returns {Uint8Array} Uint8Array.
|
|
3286
|
+
*/ const base32Decode = (str)=>{
|
|
3287
|
+
// Remove spaces (although they are not allowed by the spec, some issuers add them for readability).
|
|
3288
|
+
str = str.replace(/ /g, "");
|
|
3289
|
+
// Canonicalize to all upper case and remove padding if it exists.
|
|
3290
|
+
let end = str.length;
|
|
3291
|
+
while(str[end - 1] === "=")--end;
|
|
3292
|
+
str = (end < str.length ? str.substring(0, end) : str).toUpperCase();
|
|
3293
|
+
const buf = new ArrayBuffer(str.length * 5 / 8 | 0);
|
|
3294
|
+
const arr = new Uint8Array(buf);
|
|
3295
|
+
let bits = 0;
|
|
3296
|
+
let value = 0;
|
|
3297
|
+
let index = 0;
|
|
3298
|
+
for(let i = 0; i < str.length; i++){
|
|
3299
|
+
const idx = ALPHABET.indexOf(str[i]);
|
|
3300
|
+
if (idx === -1) throw new TypeError(`Invalid character found: ${str[i]}`);
|
|
3301
|
+
value = value << 5 | idx;
|
|
3302
|
+
bits += 5;
|
|
3303
|
+
if (bits >= 8) {
|
|
3304
|
+
bits -= 8;
|
|
3305
|
+
arr[index++] = value >>> bits;
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
return arr;
|
|
3309
|
+
};
|
|
3310
|
+
/**
|
|
3311
|
+
* Converts an Uint8Array to a base32 string (RFC 4648).
|
|
3312
|
+
* @see [LinusU/base32-encode](https://github.com/LinusU/base32-encode)
|
|
3313
|
+
* @param {Uint8Array} arr Uint8Array.
|
|
3314
|
+
* @returns {string} Base32 string.
|
|
3315
|
+
*/ const base32Encode = (arr)=>{
|
|
3316
|
+
let bits = 0;
|
|
3317
|
+
let value = 0;
|
|
3318
|
+
let str = "";
|
|
3319
|
+
for(let i = 0; i < arr.length; i++){
|
|
3320
|
+
value = value << 8 | arr[i];
|
|
3321
|
+
bits += 8;
|
|
3322
|
+
while(bits >= 5){
|
|
3323
|
+
str += ALPHABET[value >>> bits - 5 & 31];
|
|
3324
|
+
bits -= 5;
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3327
|
+
if (bits > 0) {
|
|
3328
|
+
str += ALPHABET[value << 5 - bits & 31];
|
|
3329
|
+
}
|
|
3330
|
+
return str;
|
|
3331
|
+
};
|
|
3332
|
+
|
|
3333
|
+
/**
|
|
3334
|
+
* Converts a hexadecimal string to an Uint8Array.
|
|
3335
|
+
* @param {string} str Hexadecimal string.
|
|
3336
|
+
* @returns {Uint8Array} Uint8Array.
|
|
3337
|
+
*/ const hexDecode = (str)=>{
|
|
3338
|
+
// Remove spaces (although they are not allowed by the spec, some issuers add them for readability).
|
|
3339
|
+
str = str.replace(/ /g, "");
|
|
3340
|
+
const buf = new ArrayBuffer(str.length / 2);
|
|
3341
|
+
const arr = new Uint8Array(buf);
|
|
3342
|
+
for(let i = 0; i < str.length; i += 2){
|
|
3343
|
+
arr[i / 2] = parseInt(str.substring(i, i + 2), 16);
|
|
3344
|
+
}
|
|
3345
|
+
return arr;
|
|
3346
|
+
};
|
|
3347
|
+
/**
|
|
3348
|
+
* Converts an Uint8Array to a hexadecimal string.
|
|
3349
|
+
* @param {Uint8Array} arr Uint8Array.
|
|
3350
|
+
* @returns {string} Hexadecimal string.
|
|
3351
|
+
*/ const hexEncode = (arr)=>{
|
|
3352
|
+
let str = "";
|
|
3353
|
+
for(let i = 0; i < arr.length; i++){
|
|
3354
|
+
const hex = arr[i].toString(16);
|
|
3355
|
+
if (hex.length === 1) str += "0";
|
|
3356
|
+
str += hex;
|
|
3357
|
+
}
|
|
3358
|
+
return str.toUpperCase();
|
|
3359
|
+
};
|
|
3360
|
+
|
|
3361
|
+
/**
|
|
3362
|
+
* Converts a Latin-1 string to an Uint8Array.
|
|
3363
|
+
* @param {string} str Latin-1 string.
|
|
3364
|
+
* @returns {Uint8Array} Uint8Array.
|
|
3365
|
+
*/ const latin1Decode = (str)=>{
|
|
3366
|
+
const buf = new ArrayBuffer(str.length);
|
|
3367
|
+
const arr = new Uint8Array(buf);
|
|
3368
|
+
for(let i = 0; i < str.length; i++){
|
|
3369
|
+
arr[i] = str.charCodeAt(i) & 0xff;
|
|
3370
|
+
}
|
|
3371
|
+
return arr;
|
|
3372
|
+
};
|
|
3373
|
+
/**
|
|
3374
|
+
* Converts an Uint8Array to a Latin-1 string.
|
|
3375
|
+
* @param {Uint8Array} arr Uint8Array.
|
|
3376
|
+
* @returns {string} Latin-1 string.
|
|
3377
|
+
*/ const latin1Encode = (arr)=>{
|
|
3378
|
+
let str = "";
|
|
3379
|
+
for(let i = 0; i < arr.length; i++){
|
|
3380
|
+
str += String.fromCharCode(arr[i]);
|
|
3381
|
+
}
|
|
3382
|
+
return str;
|
|
3383
|
+
};
|
|
3384
|
+
|
|
3385
|
+
/**
|
|
3386
|
+
* TextEncoder instance.
|
|
3387
|
+
* @type {TextEncoder|null}
|
|
3388
|
+
*/ const ENCODER = globalScope.TextEncoder ? new globalScope.TextEncoder() : null;
|
|
3389
|
+
/**
|
|
3390
|
+
* TextDecoder instance.
|
|
3391
|
+
* @type {TextDecoder|null}
|
|
3392
|
+
*/ const DECODER = globalScope.TextDecoder ? new globalScope.TextDecoder() : null;
|
|
3393
|
+
/**
|
|
3394
|
+
* Converts an UTF-8 string to an Uint8Array.
|
|
3395
|
+
* @param {string} str String.
|
|
3396
|
+
* @returns {Uint8Array} Uint8Array.
|
|
3397
|
+
*/ const utf8Decode = (str)=>{
|
|
3398
|
+
if (!ENCODER) {
|
|
3399
|
+
throw new Error("Encoding API not available");
|
|
3400
|
+
}
|
|
3401
|
+
return ENCODER.encode(str);
|
|
3402
|
+
};
|
|
3403
|
+
/**
|
|
3404
|
+
* Converts an Uint8Array to an UTF-8 string.
|
|
3405
|
+
* @param {Uint8Array} arr Uint8Array.
|
|
3406
|
+
* @returns {string} String.
|
|
3407
|
+
*/ const utf8Encode = (arr)=>{
|
|
3408
|
+
if (!DECODER) {
|
|
3409
|
+
throw new Error("Encoding API not available");
|
|
3410
|
+
}
|
|
3411
|
+
return DECODER.decode(arr);
|
|
3412
|
+
};
|
|
3413
|
+
|
|
3414
|
+
/**
|
|
3415
|
+
* Returns random bytes.
|
|
3416
|
+
* @param {number} size Size.
|
|
3417
|
+
* @returns {Uint8Array} Random bytes.
|
|
3418
|
+
*/ const randomBytes = (size)=>{
|
|
3419
|
+
if (globalScope.crypto?.getRandomValues) {
|
|
3420
|
+
return globalScope.crypto.getRandomValues(new Uint8Array(size));
|
|
3421
|
+
} else {
|
|
3422
|
+
throw new Error("Cryptography API not available");
|
|
3423
|
+
}
|
|
3424
|
+
};
|
|
3425
|
+
|
|
3426
|
+
/**
|
|
3427
|
+
* OTP secret key.
|
|
3428
|
+
*/ class Secret {
|
|
3429
|
+
/**
|
|
3430
|
+
* Converts a Latin-1 string to a Secret object.
|
|
3431
|
+
* @param {string} str Latin-1 string.
|
|
3432
|
+
* @returns {Secret} Secret object.
|
|
3433
|
+
*/ static fromLatin1(str) {
|
|
3434
|
+
return new Secret({
|
|
3435
|
+
buffer: latin1Decode(str).buffer
|
|
3436
|
+
});
|
|
3437
|
+
}
|
|
3438
|
+
/**
|
|
3439
|
+
* Converts an UTF-8 string to a Secret object.
|
|
3440
|
+
* @param {string} str UTF-8 string.
|
|
3441
|
+
* @returns {Secret} Secret object.
|
|
3442
|
+
*/ static fromUTF8(str) {
|
|
3443
|
+
return new Secret({
|
|
3444
|
+
buffer: utf8Decode(str).buffer
|
|
3445
|
+
});
|
|
3446
|
+
}
|
|
3447
|
+
/**
|
|
3448
|
+
* Converts a base32 string to a Secret object.
|
|
3449
|
+
* @param {string} str Base32 string.
|
|
3450
|
+
* @returns {Secret} Secret object.
|
|
3451
|
+
*/ static fromBase32(str) {
|
|
3452
|
+
return new Secret({
|
|
3453
|
+
buffer: base32Decode(str).buffer
|
|
3454
|
+
});
|
|
3455
|
+
}
|
|
3456
|
+
/**
|
|
3457
|
+
* Converts a hexadecimal string to a Secret object.
|
|
3458
|
+
* @param {string} str Hexadecimal string.
|
|
3459
|
+
* @returns {Secret} Secret object.
|
|
3460
|
+
*/ static fromHex(str) {
|
|
3461
|
+
return new Secret({
|
|
3462
|
+
buffer: hexDecode(str).buffer
|
|
3463
|
+
});
|
|
3464
|
+
}
|
|
3465
|
+
/**
|
|
3466
|
+
* Secret key buffer.
|
|
3467
|
+
* @deprecated For backward compatibility, the "bytes" property should be used instead.
|
|
3468
|
+
* @type {ArrayBufferLike}
|
|
3469
|
+
*/ get buffer() {
|
|
3470
|
+
return this.bytes.buffer;
|
|
3471
|
+
}
|
|
3472
|
+
/**
|
|
3473
|
+
* Latin-1 string representation of secret key.
|
|
3474
|
+
* @type {string}
|
|
3475
|
+
*/ get latin1() {
|
|
3476
|
+
Object.defineProperty(this, "latin1", {
|
|
3477
|
+
enumerable: true,
|
|
3478
|
+
writable: false,
|
|
3479
|
+
configurable: false,
|
|
3480
|
+
value: latin1Encode(this.bytes)
|
|
3481
|
+
});
|
|
3482
|
+
return this.latin1;
|
|
3483
|
+
}
|
|
3484
|
+
/**
|
|
3485
|
+
* UTF-8 string representation of secret key.
|
|
3486
|
+
* @type {string}
|
|
3487
|
+
*/ get utf8() {
|
|
3488
|
+
Object.defineProperty(this, "utf8", {
|
|
3489
|
+
enumerable: true,
|
|
3490
|
+
writable: false,
|
|
3491
|
+
configurable: false,
|
|
3492
|
+
value: utf8Encode(this.bytes)
|
|
3493
|
+
});
|
|
3494
|
+
return this.utf8;
|
|
3495
|
+
}
|
|
3496
|
+
/**
|
|
3497
|
+
* Base32 string representation of secret key.
|
|
3498
|
+
* @type {string}
|
|
3499
|
+
*/ get base32() {
|
|
3500
|
+
Object.defineProperty(this, "base32", {
|
|
3501
|
+
enumerable: true,
|
|
3502
|
+
writable: false,
|
|
3503
|
+
configurable: false,
|
|
3504
|
+
value: base32Encode(this.bytes)
|
|
3505
|
+
});
|
|
3506
|
+
return this.base32;
|
|
3507
|
+
}
|
|
3508
|
+
/**
|
|
3509
|
+
* Hexadecimal string representation of secret key.
|
|
3510
|
+
* @type {string}
|
|
3511
|
+
*/ get hex() {
|
|
3512
|
+
Object.defineProperty(this, "hex", {
|
|
3513
|
+
enumerable: true,
|
|
3514
|
+
writable: false,
|
|
3515
|
+
configurable: false,
|
|
3516
|
+
value: hexEncode(this.bytes)
|
|
3517
|
+
});
|
|
3518
|
+
return this.hex;
|
|
3519
|
+
}
|
|
3520
|
+
/**
|
|
3521
|
+
* Creates a secret key object.
|
|
3522
|
+
* @param {Object} [config] Configuration options.
|
|
3523
|
+
* @param {ArrayBufferLike} [config.buffer] Secret key buffer.
|
|
3524
|
+
* @param {number} [config.size=20] Number of random bytes to generate, ignored if 'buffer' is provided.
|
|
3525
|
+
*/ constructor({ buffer, size = 20 } = {}){
|
|
3526
|
+
/**
|
|
3527
|
+
* Secret key.
|
|
3528
|
+
* @type {Uint8Array}
|
|
3529
|
+
* @readonly
|
|
3530
|
+
*/ this.bytes = typeof buffer === "undefined" ? randomBytes(size) : new Uint8Array(buffer);
|
|
3531
|
+
// Prevent the "bytes" property from being modified.
|
|
3532
|
+
Object.defineProperty(this, "bytes", {
|
|
3533
|
+
enumerable: true,
|
|
3534
|
+
writable: false,
|
|
3535
|
+
configurable: false,
|
|
3536
|
+
value: this.bytes
|
|
3537
|
+
});
|
|
3538
|
+
}
|
|
3539
|
+
}
|
|
3540
|
+
|
|
3541
|
+
/**
|
|
3542
|
+
* Returns true if a is equal to b, without leaking timing information that would allow an attacker to guess one of the values.
|
|
3543
|
+
* @param {string} a String a.
|
|
3544
|
+
* @param {string} b String b.
|
|
3545
|
+
* @returns {boolean} Equality result.
|
|
3546
|
+
*/ const timingSafeEqual = (a, b)=>{
|
|
3547
|
+
{
|
|
3548
|
+
if (a.length !== b.length) {
|
|
3549
|
+
throw new TypeError("Input strings must have the same length");
|
|
3550
|
+
}
|
|
3551
|
+
let i = -1;
|
|
3552
|
+
let out = 0;
|
|
3553
|
+
while(++i < a.length){
|
|
3554
|
+
out |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
3555
|
+
}
|
|
3556
|
+
return out === 0;
|
|
3557
|
+
}
|
|
3558
|
+
};
|
|
3559
|
+
|
|
3560
|
+
/**
|
|
3561
|
+
* HOTP: An HMAC-based One-time Password Algorithm.
|
|
3562
|
+
* @see [RFC 4226](https://datatracker.ietf.org/doc/html/rfc4226)
|
|
3563
|
+
*/ class HOTP {
|
|
3564
|
+
/**
|
|
3565
|
+
* Default configuration.
|
|
3566
|
+
* @type {{
|
|
3567
|
+
* issuer: string,
|
|
3568
|
+
* label: string,
|
|
3569
|
+
* issuerInLabel: boolean,
|
|
3570
|
+
* algorithm: string,
|
|
3571
|
+
* digits: number,
|
|
3572
|
+
* counter: number
|
|
3573
|
+
* window: number
|
|
3574
|
+
* }}
|
|
3575
|
+
*/ static get defaults() {
|
|
3576
|
+
return {
|
|
3577
|
+
issuer: "",
|
|
3578
|
+
label: "OTPAuth",
|
|
3579
|
+
issuerInLabel: true,
|
|
3580
|
+
algorithm: "SHA1",
|
|
3581
|
+
digits: 6,
|
|
3582
|
+
counter: 0,
|
|
3583
|
+
window: 1
|
|
3584
|
+
};
|
|
3585
|
+
}
|
|
3586
|
+
/**
|
|
3587
|
+
* Generates an HOTP token.
|
|
3588
|
+
* @param {Object} config Configuration options.
|
|
3589
|
+
* @param {Secret} config.secret Secret key.
|
|
3590
|
+
* @param {string} [config.algorithm='SHA1'] HMAC hashing algorithm.
|
|
3591
|
+
* @param {number} [config.digits=6] Token length.
|
|
3592
|
+
* @param {number} [config.counter=0] Counter value.
|
|
3593
|
+
* @returns {string} Token.
|
|
3594
|
+
*/ static generate({ secret, algorithm = HOTP.defaults.algorithm, digits = HOTP.defaults.digits, counter = HOTP.defaults.counter }) {
|
|
3595
|
+
const digest = hmacDigest(algorithm, secret.bytes, uintDecode(counter));
|
|
3596
|
+
const offset = digest[digest.byteLength - 1] & 15;
|
|
3597
|
+
const otp = ((digest[offset] & 127) << 24 | (digest[offset + 1] & 255) << 16 | (digest[offset + 2] & 255) << 8 | digest[offset + 3] & 255) % 10 ** digits;
|
|
3598
|
+
return otp.toString().padStart(digits, "0");
|
|
3599
|
+
}
|
|
3600
|
+
/**
|
|
3601
|
+
* Generates an HOTP token.
|
|
3602
|
+
* @param {Object} [config] Configuration options.
|
|
3603
|
+
* @param {number} [config.counter=this.counter++] Counter value.
|
|
3604
|
+
* @returns {string} Token.
|
|
3605
|
+
*/ generate({ counter = this.counter++ } = {}) {
|
|
3606
|
+
return HOTP.generate({
|
|
3607
|
+
secret: this.secret,
|
|
3608
|
+
algorithm: this.algorithm,
|
|
3609
|
+
digits: this.digits,
|
|
3610
|
+
counter
|
|
3611
|
+
});
|
|
3612
|
+
}
|
|
3613
|
+
/**
|
|
3614
|
+
* Validates an HOTP token.
|
|
3615
|
+
* @param {Object} config Configuration options.
|
|
3616
|
+
* @param {string} config.token Token value.
|
|
3617
|
+
* @param {Secret} config.secret Secret key.
|
|
3618
|
+
* @param {string} [config.algorithm='SHA1'] HMAC hashing algorithm.
|
|
3619
|
+
* @param {number} [config.digits=6] Token length.
|
|
3620
|
+
* @param {number} [config.counter=0] Counter value.
|
|
3621
|
+
* @param {number} [config.window=1] Window of counter values to test.
|
|
3622
|
+
* @returns {number|null} Token delta or null if it is not found in the search window, in which case it should be considered invalid.
|
|
3623
|
+
*/ static validate({ token, secret, algorithm, digits = HOTP.defaults.digits, counter = HOTP.defaults.counter, window = HOTP.defaults.window }) {
|
|
3624
|
+
// Return early if the token length does not match the digit number.
|
|
3625
|
+
if (token.length !== digits) return null;
|
|
3626
|
+
let delta = null;
|
|
3627
|
+
const check = (/** @type {number} */ i)=>{
|
|
3628
|
+
const generatedToken = HOTP.generate({
|
|
3629
|
+
secret,
|
|
3630
|
+
algorithm,
|
|
3631
|
+
digits,
|
|
3632
|
+
counter: i
|
|
3633
|
+
});
|
|
3634
|
+
if (timingSafeEqual(token, generatedToken)) {
|
|
3635
|
+
delta = i - counter;
|
|
3636
|
+
}
|
|
3637
|
+
};
|
|
3638
|
+
check(counter);
|
|
3639
|
+
for(let i = 1; i <= window && delta === null; ++i){
|
|
3640
|
+
check(counter - i);
|
|
3641
|
+
if (delta !== null) break;
|
|
3642
|
+
check(counter + i);
|
|
3643
|
+
if (delta !== null) break;
|
|
3644
|
+
}
|
|
3645
|
+
return delta;
|
|
3646
|
+
}
|
|
3647
|
+
/**
|
|
3648
|
+
* Validates an HOTP token.
|
|
3649
|
+
* @param {Object} config Configuration options.
|
|
3650
|
+
* @param {string} config.token Token value.
|
|
3651
|
+
* @param {number} [config.counter=this.counter] Counter value.
|
|
3652
|
+
* @param {number} [config.window=1] Window of counter values to test.
|
|
3653
|
+
* @returns {number|null} Token delta or null if it is not found in the search window, in which case it should be considered invalid.
|
|
3654
|
+
*/ validate({ token, counter = this.counter, window }) {
|
|
3655
|
+
return HOTP.validate({
|
|
3656
|
+
token,
|
|
3657
|
+
secret: this.secret,
|
|
3658
|
+
algorithm: this.algorithm,
|
|
3659
|
+
digits: this.digits,
|
|
3660
|
+
counter,
|
|
3661
|
+
window
|
|
3662
|
+
});
|
|
3663
|
+
}
|
|
3664
|
+
/**
|
|
3665
|
+
* Returns a Google Authenticator key URI.
|
|
3666
|
+
* @returns {string} URI.
|
|
3667
|
+
*/ toString() {
|
|
3668
|
+
const e = encodeURIComponent;
|
|
3669
|
+
return "otpauth://hotp/" + `${this.issuer.length > 0 ? this.issuerInLabel ? `${e(this.issuer)}:${e(this.label)}?issuer=${e(this.issuer)}&` : `${e(this.label)}?issuer=${e(this.issuer)}&` : `${e(this.label)}?`}` + `secret=${e(this.secret.base32)}&` + `algorithm=${e(this.algorithm)}&` + `digits=${e(this.digits)}&` + `counter=${e(this.counter)}`;
|
|
3670
|
+
}
|
|
3671
|
+
/**
|
|
3672
|
+
* Creates an HOTP object.
|
|
3673
|
+
* @param {Object} [config] Configuration options.
|
|
3674
|
+
* @param {string} [config.issuer=''] Account provider.
|
|
3675
|
+
* @param {string} [config.label='OTPAuth'] Account label.
|
|
3676
|
+
* @param {boolean} [config.issuerInLabel=true] Include issuer prefix in label.
|
|
3677
|
+
* @param {Secret|string} [config.secret=Secret] Secret key.
|
|
3678
|
+
* @param {string} [config.algorithm='SHA1'] HMAC hashing algorithm.
|
|
3679
|
+
* @param {number} [config.digits=6] Token length.
|
|
3680
|
+
* @param {number} [config.counter=0] Initial counter value.
|
|
3681
|
+
*/ 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 } = {}){
|
|
3682
|
+
/**
|
|
3683
|
+
* Account provider.
|
|
3684
|
+
* @type {string}
|
|
3685
|
+
*/ this.issuer = issuer;
|
|
3686
|
+
/**
|
|
3687
|
+
* Account label.
|
|
3688
|
+
* @type {string}
|
|
3689
|
+
*/ this.label = label;
|
|
3690
|
+
/**
|
|
3691
|
+
* Include issuer prefix in label.
|
|
3692
|
+
* @type {boolean}
|
|
3693
|
+
*/ this.issuerInLabel = issuerInLabel;
|
|
3694
|
+
/**
|
|
3695
|
+
* Secret key.
|
|
3696
|
+
* @type {Secret}
|
|
3697
|
+
*/ this.secret = typeof secret === "string" ? Secret.fromBase32(secret) : secret;
|
|
3698
|
+
/**
|
|
3699
|
+
* HMAC hashing algorithm.
|
|
3700
|
+
* @type {string}
|
|
3701
|
+
*/ this.algorithm = canonicalizeAlgorithm(algorithm);
|
|
3702
|
+
/**
|
|
3703
|
+
* Token length.
|
|
3704
|
+
* @type {number}
|
|
3705
|
+
*/ this.digits = digits;
|
|
3706
|
+
/**
|
|
3707
|
+
* Initial counter value.
|
|
3708
|
+
* @type {number}
|
|
3709
|
+
*/ this.counter = counter;
|
|
3710
|
+
}
|
|
3711
|
+
}
|
|
3712
|
+
|
|
3713
|
+
/**
|
|
3714
|
+
* TOTP: Time-Based One-Time Password Algorithm.
|
|
3715
|
+
* @see [RFC 6238](https://datatracker.ietf.org/doc/html/rfc6238)
|
|
3716
|
+
*/ class TOTP {
|
|
3717
|
+
/**
|
|
3718
|
+
* Default configuration.
|
|
3719
|
+
* @type {{
|
|
3720
|
+
* issuer: string,
|
|
3721
|
+
* label: string,
|
|
3722
|
+
* issuerInLabel: boolean,
|
|
3723
|
+
* algorithm: string,
|
|
3724
|
+
* digits: number,
|
|
3725
|
+
* period: number
|
|
3726
|
+
* window: number
|
|
3727
|
+
* }}
|
|
3728
|
+
*/ static get defaults() {
|
|
3729
|
+
return {
|
|
3730
|
+
issuer: "",
|
|
3731
|
+
label: "OTPAuth",
|
|
3732
|
+
issuerInLabel: true,
|
|
3733
|
+
algorithm: "SHA1",
|
|
3734
|
+
digits: 6,
|
|
3735
|
+
period: 30,
|
|
3736
|
+
window: 1
|
|
3737
|
+
};
|
|
3738
|
+
}
|
|
3739
|
+
/**
|
|
3740
|
+
* Calculates the counter. i.e. the number of periods since timestamp 0.
|
|
3741
|
+
* @param {Object} [config] Configuration options.
|
|
3742
|
+
* @param {number} [config.period=30] Token time-step duration.
|
|
3743
|
+
* @param {number} [config.timestamp=Date.now] Timestamp value in milliseconds.
|
|
3744
|
+
* @returns {number} Counter.
|
|
3745
|
+
*/ static counter({ period = TOTP.defaults.period, timestamp = Date.now() } = {}) {
|
|
3746
|
+
return Math.floor(timestamp / 1000 / period);
|
|
3747
|
+
}
|
|
3748
|
+
/**
|
|
3749
|
+
* Calculates the counter. i.e. the number of periods since timestamp 0.
|
|
3750
|
+
* @param {Object} [config] Configuration options.
|
|
3751
|
+
* @param {number} [config.timestamp=Date.now] Timestamp value in milliseconds.
|
|
3752
|
+
* @returns {number} Counter.
|
|
3753
|
+
*/ counter({ timestamp = Date.now() } = {}) {
|
|
3754
|
+
return TOTP.counter({
|
|
3755
|
+
period: this.period,
|
|
3756
|
+
timestamp
|
|
3757
|
+
});
|
|
3758
|
+
}
|
|
3759
|
+
/**
|
|
3760
|
+
* Calculates the remaining time in milliseconds until the next token is generated.
|
|
3761
|
+
* @param {Object} [config] Configuration options.
|
|
3762
|
+
* @param {number} [config.period=30] Token time-step duration.
|
|
3763
|
+
* @param {number} [config.timestamp=Date.now] Timestamp value in milliseconds.
|
|
3764
|
+
* @returns {number} counter.
|
|
3765
|
+
*/ static remaining({ period = TOTP.defaults.period, timestamp = Date.now() } = {}) {
|
|
3766
|
+
return period * 1000 - timestamp % (period * 1000);
|
|
3767
|
+
}
|
|
3768
|
+
/**
|
|
3769
|
+
* Calculates the remaining time in milliseconds until the next token is generated.
|
|
3770
|
+
* @param {Object} [config] Configuration options.
|
|
3771
|
+
* @param {number} [config.timestamp=Date.now] Timestamp value in milliseconds.
|
|
3772
|
+
* @returns {number} counter.
|
|
3773
|
+
*/ remaining({ timestamp = Date.now() } = {}) {
|
|
3774
|
+
return TOTP.remaining({
|
|
3775
|
+
period: this.period,
|
|
3776
|
+
timestamp
|
|
3777
|
+
});
|
|
3778
|
+
}
|
|
3779
|
+
/**
|
|
3780
|
+
* Generates a TOTP token.
|
|
3781
|
+
* @param {Object} config Configuration options.
|
|
3782
|
+
* @param {Secret} config.secret Secret key.
|
|
3783
|
+
* @param {string} [config.algorithm='SHA1'] HMAC hashing algorithm.
|
|
3784
|
+
* @param {number} [config.digits=6] Token length.
|
|
3785
|
+
* @param {number} [config.period=30] Token time-step duration.
|
|
3786
|
+
* @param {number} [config.timestamp=Date.now] Timestamp value in milliseconds.
|
|
3787
|
+
* @returns {string} Token.
|
|
3788
|
+
*/ static generate({ secret, algorithm, digits, period = TOTP.defaults.period, timestamp = Date.now() }) {
|
|
3789
|
+
return HOTP.generate({
|
|
3790
|
+
secret,
|
|
3791
|
+
algorithm,
|
|
3792
|
+
digits,
|
|
3793
|
+
counter: TOTP.counter({
|
|
3794
|
+
period,
|
|
3795
|
+
timestamp
|
|
3796
|
+
})
|
|
3797
|
+
});
|
|
3798
|
+
}
|
|
3799
|
+
/**
|
|
3800
|
+
* Generates a TOTP token.
|
|
3801
|
+
* @param {Object} [config] Configuration options.
|
|
3802
|
+
* @param {number} [config.timestamp=Date.now] Timestamp value in milliseconds.
|
|
3803
|
+
* @returns {string} Token.
|
|
3804
|
+
*/ generate({ timestamp = Date.now() } = {}) {
|
|
3805
|
+
return TOTP.generate({
|
|
3806
|
+
secret: this.secret,
|
|
3807
|
+
algorithm: this.algorithm,
|
|
3808
|
+
digits: this.digits,
|
|
3809
|
+
period: this.period,
|
|
3810
|
+
timestamp
|
|
3811
|
+
});
|
|
3812
|
+
}
|
|
3813
|
+
/**
|
|
3814
|
+
* Validates a TOTP token.
|
|
3815
|
+
* @param {Object} config Configuration options.
|
|
3816
|
+
* @param {string} config.token Token value.
|
|
3817
|
+
* @param {Secret} config.secret Secret key.
|
|
3818
|
+
* @param {string} [config.algorithm='SHA1'] HMAC hashing algorithm.
|
|
3819
|
+
* @param {number} [config.digits=6] Token length.
|
|
3820
|
+
* @param {number} [config.period=30] Token time-step duration.
|
|
3821
|
+
* @param {number} [config.timestamp=Date.now] Timestamp value in milliseconds.
|
|
3822
|
+
* @param {number} [config.window=1] Window of counter values to test.
|
|
3823
|
+
* @returns {number|null} Token delta or null if it is not found in the search window, in which case it should be considered invalid.
|
|
3824
|
+
*/ static validate({ token, secret, algorithm, digits, period = TOTP.defaults.period, timestamp = Date.now(), window }) {
|
|
3825
|
+
return HOTP.validate({
|
|
3826
|
+
token,
|
|
3827
|
+
secret,
|
|
3828
|
+
algorithm,
|
|
3829
|
+
digits,
|
|
3830
|
+
counter: TOTP.counter({
|
|
3831
|
+
period,
|
|
3832
|
+
timestamp
|
|
3833
|
+
}),
|
|
3834
|
+
window
|
|
3835
|
+
});
|
|
3836
|
+
}
|
|
3837
|
+
/**
|
|
3838
|
+
* Validates a TOTP token.
|
|
3839
|
+
* @param {Object} config Configuration options.
|
|
3840
|
+
* @param {string} config.token Token value.
|
|
3841
|
+
* @param {number} [config.timestamp=Date.now] Timestamp value in milliseconds.
|
|
3842
|
+
* @param {number} [config.window=1] Window of counter values to test.
|
|
3843
|
+
* @returns {number|null} Token delta or null if it is not found in the search window, in which case it should be considered invalid.
|
|
3844
|
+
*/ validate({ token, timestamp, window }) {
|
|
3845
|
+
return TOTP.validate({
|
|
3846
|
+
token,
|
|
3847
|
+
secret: this.secret,
|
|
3848
|
+
algorithm: this.algorithm,
|
|
3849
|
+
digits: this.digits,
|
|
3850
|
+
period: this.period,
|
|
3851
|
+
timestamp,
|
|
3852
|
+
window
|
|
3853
|
+
});
|
|
3854
|
+
}
|
|
3855
|
+
/**
|
|
3856
|
+
* Returns a Google Authenticator key URI.
|
|
3857
|
+
* @returns {string} URI.
|
|
3858
|
+
*/ toString() {
|
|
3859
|
+
const e = encodeURIComponent;
|
|
3860
|
+
return "otpauth://totp/" + `${this.issuer.length > 0 ? this.issuerInLabel ? `${e(this.issuer)}:${e(this.label)}?issuer=${e(this.issuer)}&` : `${e(this.label)}?issuer=${e(this.issuer)}&` : `${e(this.label)}?`}` + `secret=${e(this.secret.base32)}&` + `algorithm=${e(this.algorithm)}&` + `digits=${e(this.digits)}&` + `period=${e(this.period)}`;
|
|
3861
|
+
}
|
|
3862
|
+
/**
|
|
3863
|
+
* Creates a TOTP object.
|
|
3864
|
+
* @param {Object} [config] Configuration options.
|
|
3865
|
+
* @param {string} [config.issuer=''] Account provider.
|
|
3866
|
+
* @param {string} [config.label='OTPAuth'] Account label.
|
|
3867
|
+
* @param {boolean} [config.issuerInLabel=true] Include issuer prefix in label.
|
|
3868
|
+
* @param {Secret|string} [config.secret=Secret] Secret key.
|
|
3869
|
+
* @param {string} [config.algorithm='SHA1'] HMAC hashing algorithm.
|
|
3870
|
+
* @param {number} [config.digits=6] Token length.
|
|
3871
|
+
* @param {number} [config.period=30] Token time-step duration.
|
|
3872
|
+
*/ 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 } = {}){
|
|
3873
|
+
/**
|
|
3874
|
+
* Account provider.
|
|
3875
|
+
* @type {string}
|
|
3876
|
+
*/ this.issuer = issuer;
|
|
3877
|
+
/**
|
|
3878
|
+
* Account label.
|
|
3879
|
+
* @type {string}
|
|
3880
|
+
*/ this.label = label;
|
|
3881
|
+
/**
|
|
3882
|
+
* Include issuer prefix in label.
|
|
3883
|
+
* @type {boolean}
|
|
3884
|
+
*/ this.issuerInLabel = issuerInLabel;
|
|
3885
|
+
/**
|
|
3886
|
+
* Secret key.
|
|
3887
|
+
* @type {Secret}
|
|
3888
|
+
*/ this.secret = typeof secret === "string" ? Secret.fromBase32(secret) : secret;
|
|
3889
|
+
/**
|
|
3890
|
+
* HMAC hashing algorithm.
|
|
3891
|
+
* @type {string}
|
|
3892
|
+
*/ this.algorithm = canonicalizeAlgorithm(algorithm);
|
|
3893
|
+
/**
|
|
3894
|
+
* Token length.
|
|
3895
|
+
* @type {number}
|
|
3896
|
+
*/ this.digits = digits;
|
|
3897
|
+
/**
|
|
3898
|
+
* Token time-step duration.
|
|
3899
|
+
* @type {number}
|
|
3900
|
+
*/ this.period = period;
|
|
3901
|
+
}
|
|
3902
|
+
}
|
|
3903
|
+
|
|
2083
3904
|
class ProboPlaywright {
|
|
2084
|
-
constructor(timeoutConfig = {}, page = null) {
|
|
3905
|
+
constructor({ enableSmartSelectors = false, timeoutConfig = {}, debugLevel = ProboLogLevel.INFO }, page = null) {
|
|
2085
3906
|
this.page = null;
|
|
2086
|
-
|
|
3907
|
+
this.params = {};
|
|
3908
|
+
this.enableSmartSelectors = enableSmartSelectors;
|
|
2087
3909
|
this.timeoutConfig = {
|
|
2088
3910
|
...DEFAULT_PLAYWRIGHT_TIMEOUT_CONFIG,
|
|
2089
3911
|
...timeoutConfig
|
|
2090
3912
|
};
|
|
2091
3913
|
this.setPage(page);
|
|
3914
|
+
proboLogger.setLogLevel(debugLevel);
|
|
2092
3915
|
}
|
|
2093
3916
|
/**
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
3917
|
+
* Sets the Playwright page instance for this ProboPlaywright instance.
|
|
3918
|
+
* Also applies the configured default navigation and action timeouts to the page.
|
|
3919
|
+
*
|
|
3920
|
+
* @param page - The Playwright Page instance to use, or null to unset.
|
|
3921
|
+
*/
|
|
2099
3922
|
setPage(page) {
|
|
2100
3923
|
this.page = page;
|
|
2101
3924
|
if (this.page) {
|
|
@@ -2104,18 +3927,49 @@ class ProboPlaywright {
|
|
|
2104
3927
|
}
|
|
2105
3928
|
}
|
|
2106
3929
|
/**
|
|
2107
|
-
*
|
|
2108
|
-
*
|
|
2109
|
-
*
|
|
2110
|
-
* @param params - Configuration object containing element selectors, action type, arguments, and display options
|
|
2111
|
-
* @returns Promise that resolves to a result object for extract actions, or void for other actions
|
|
2112
|
-
* @throws Error if element is not found or validation fails
|
|
3930
|
+
* Sets the parameters object for template literal interpolation
|
|
3931
|
+
* Stores a reference to the params object so mutations are automatically reflected
|
|
3932
|
+
* @param params The parameters object containing values to use for interpolation
|
|
2113
3933
|
*/
|
|
3934
|
+
setParams(params) {
|
|
3935
|
+
this.params = params;
|
|
3936
|
+
}
|
|
3937
|
+
/**
|
|
3938
|
+
* Interpolates a string using the current params and optional additional context
|
|
3939
|
+
* @param str The string to interpolate (may contain ${variable} syntax)
|
|
3940
|
+
* @param additionalContext Optional additional context to merge with params
|
|
3941
|
+
* @returns The interpolated string
|
|
3942
|
+
*/
|
|
3943
|
+
interpolate(str, additionalContext = {}) {
|
|
3944
|
+
const context = { ...this.params, ...additionalContext };
|
|
3945
|
+
return interpolateTemplate(str, context);
|
|
3946
|
+
}
|
|
3947
|
+
/**
|
|
3948
|
+
* Executes a single step in the test scenario with the specified action on the target element.
|
|
3949
|
+
* Handles iframe navigation, element highlighting, and various Playwright actions like click, fill, validate, etc.
|
|
3950
|
+
*
|
|
3951
|
+
* @param params - Configuration object containing element selectors, action type, arguments, and display options
|
|
3952
|
+
* @returns Promise that resolves to a string for extract actions, boolean for assert actions, or void for other actions
|
|
3953
|
+
* @throws Error if element is not found or validation fails
|
|
3954
|
+
*/
|
|
2114
3955
|
async runStep(params) {
|
|
2115
|
-
|
|
3956
|
+
let { action, argument = '', iframeSelector = '', elementSelector = '', smartSelector = null, smartIFrameSelector = null, annotation = '', } = params;
|
|
2116
3957
|
// 0. Check that page is set
|
|
2117
3958
|
if (!this.page) {
|
|
2118
|
-
throw new Error('
|
|
3959
|
+
throw new Error('Page is not set');
|
|
3960
|
+
}
|
|
3961
|
+
// Interpolate argument if it's a string with template literals
|
|
3962
|
+
if (typeof argument === 'string' && argument.includes('${')) {
|
|
3963
|
+
argument = this.interpolate(argument);
|
|
3964
|
+
}
|
|
3965
|
+
else if (Array.isArray(argument)) {
|
|
3966
|
+
// Handle array arguments (e.g., for UPLOAD_FILES or SELECT_DROPDOWN)
|
|
3967
|
+
argument = argument.map((arg) => {
|
|
3968
|
+
if (typeof arg === 'string' && arg.includes('${')) {
|
|
3969
|
+
return this.interpolate(arg);
|
|
3970
|
+
}
|
|
3971
|
+
return arg;
|
|
3972
|
+
});
|
|
2119
3973
|
}
|
|
2120
3974
|
// 1. Check if we need to visit a url
|
|
2121
3975
|
if (action === PlaywrightAction.VISIT_URL || action === PlaywrightAction.VISIT_BASE_URL) {
|
|
@@ -2147,10 +4001,11 @@ class ProboPlaywright {
|
|
|
2147
4001
|
const startTime = Date.now();
|
|
2148
4002
|
let locator;
|
|
2149
4003
|
if (iframeSelector && iframeSelector.length > 0) {
|
|
2150
|
-
|
|
4004
|
+
const frameLocator = await this.getLocator(iframeSelector, smartIFrameSelector, true);
|
|
4005
|
+
locator = await this.getLocator(elementSelector, smartSelector, false, frameLocator);
|
|
2151
4006
|
}
|
|
2152
4007
|
else {
|
|
2153
|
-
locator = this.
|
|
4008
|
+
locator = await this.getLocator(elementSelector, smartSelector);
|
|
2154
4009
|
}
|
|
2155
4010
|
// Fail fast: immediately validate that the element exists for non-wait actions
|
|
2156
4011
|
const locator_timeout = (action === PlaywrightAction.WAIT_FOR) ? params.timeout || 10000 : this.timeoutConfig.playwrightLocatorTimeout;
|
|
@@ -2158,7 +4013,25 @@ class ProboPlaywright {
|
|
|
2158
4013
|
await locator.waitFor({ state: 'attached', timeout: locator_timeout });
|
|
2159
4014
|
}
|
|
2160
4015
|
catch (e) {
|
|
2161
|
-
|
|
4016
|
+
if (this.enableSmartSelectors) { //fallback to CSS selector
|
|
4017
|
+
try {
|
|
4018
|
+
proboLogger.warn(`Element not found with smart selector: ${JSON.stringify(smartSelector)} ${smartIFrameSelector ? `with iframe smart selector: ${JSON.stringify(smartIFrameSelector)}` : ''}. Falling back to CSS selector`);
|
|
4019
|
+
if (iframeSelector && iframeSelector.length > 0) {
|
|
4020
|
+
const frameLocator = await this.getLocatorOrFrame(iframeSelector, true);
|
|
4021
|
+
locator = await this.getLocatorOrFrame(elementSelector, false, frameLocator);
|
|
4022
|
+
}
|
|
4023
|
+
else {
|
|
4024
|
+
locator = await this.getLocatorOrFrame(elementSelector, false);
|
|
4025
|
+
}
|
|
4026
|
+
await locator.waitFor({ state: 'attached', timeout: 200 }); //only small timeout is needed as we've already waited for the locator_timeout
|
|
4027
|
+
}
|
|
4028
|
+
catch (e) {
|
|
4029
|
+
throw new Error(`Element not found with CSS selector: ${elementSelector} ${iframeSelector ? `in iframe: ${iframeSelector}` : ''} after ${locator_timeout}ms`);
|
|
4030
|
+
}
|
|
4031
|
+
}
|
|
4032
|
+
else {
|
|
4033
|
+
throw new Error(`Element not found with CSS selector: ${elementSelector} ${iframeSelector ? `in iframe: ${iframeSelector}` : ''} after ${locator_timeout}ms`);
|
|
4034
|
+
}
|
|
2162
4035
|
}
|
|
2163
4036
|
if (action === PlaywrightAction.HOVER) {
|
|
2164
4037
|
const visibleLocator = await findClosestVisibleElement(locator);
|
|
@@ -2177,13 +4050,13 @@ class ProboPlaywright {
|
|
|
2177
4050
|
case PlaywrightAction.CLICK:
|
|
2178
4051
|
case PlaywrightAction.CHECK_CHECKBOX:
|
|
2179
4052
|
case PlaywrightAction.SELECT_RADIO:
|
|
2180
|
-
await this.
|
|
4053
|
+
await this.robustMouseAction(locator, 'click');
|
|
2181
4054
|
break;
|
|
2182
4055
|
case PlaywrightAction.FILL_IN:
|
|
2183
4056
|
await this.robustFill(locator, argument);
|
|
2184
4057
|
break;
|
|
2185
4058
|
case PlaywrightAction.SELECT_DROPDOWN:
|
|
2186
|
-
await locator.selectOption(argument);
|
|
4059
|
+
await locator.selectOption(argument, { timeout: this.timeoutConfig.playwrightActionTimeout });
|
|
2187
4060
|
break;
|
|
2188
4061
|
case PlaywrightAction.SET_SLIDER:
|
|
2189
4062
|
await this.setSliderValue(locator, argument);
|
|
@@ -2192,11 +4065,31 @@ class ProboPlaywright {
|
|
|
2192
4065
|
// till we figure out how to get the inbox name we will wait for ANY OTP in all inboxes
|
|
2193
4066
|
const otp = await OTP.waitForOTP({ checkRecentMessagesSinceMs: 120000 });
|
|
2194
4067
|
if (otp) {
|
|
2195
|
-
|
|
2196
|
-
await locator.fill(otp);
|
|
4068
|
+
proboLogger.log(`✅ OTP found: ${otp}`);
|
|
4069
|
+
await locator.fill(otp, { timeout: this.timeoutConfig.playwrightActionTimeout });
|
|
2197
4070
|
}
|
|
2198
4071
|
else {
|
|
2199
|
-
|
|
4072
|
+
proboLogger.log(`❌ OTP not found`);
|
|
4073
|
+
}
|
|
4074
|
+
break;
|
|
4075
|
+
case PlaywrightAction.GEN_TOTP:
|
|
4076
|
+
// Use secret from argument and auxiliary config (digits/algorithm) from totpConfig
|
|
4077
|
+
const totpAux = params.totpConfig;
|
|
4078
|
+
const secretArg = params.argument;
|
|
4079
|
+
if (secretArg && totpAux) {
|
|
4080
|
+
try {
|
|
4081
|
+
const totpCode = this.generateOTP(secretArg, totpAux.digits || 6, totpAux.algorithm || 'SHA1');
|
|
4082
|
+
proboLogger.log(`✅ TOTP generated`);
|
|
4083
|
+
await locator.fill(totpCode, { timeout: this.timeoutConfig.playwrightActionTimeout });
|
|
4084
|
+
}
|
|
4085
|
+
catch (error) {
|
|
4086
|
+
proboLogger.error(`❌ TOTP generation failed: ${error}`);
|
|
4087
|
+
throw new Error(`TOTP generation failed: ${error}`);
|
|
4088
|
+
}
|
|
4089
|
+
}
|
|
4090
|
+
else {
|
|
4091
|
+
proboLogger.log(`❌ Missing TOTP argument or config`);
|
|
4092
|
+
throw new Error(`Missing TOTP argument or config`);
|
|
2200
4093
|
}
|
|
2201
4094
|
break;
|
|
2202
4095
|
case PlaywrightAction.ASSERT_CONTAINS_VALUE:
|
|
@@ -2204,38 +4097,39 @@ class ProboPlaywright {
|
|
|
2204
4097
|
if (!matchRegex(containerText, argument)) {
|
|
2205
4098
|
throw new Error(`Validation failed. Expected text "${containerText}" to match "${argument}".`);
|
|
2206
4099
|
}
|
|
2207
|
-
|
|
4100
|
+
return true; // Return true for successful assertion
|
|
2208
4101
|
case PlaywrightAction.ASSERT_EXACT_VALUE:
|
|
2209
4102
|
const actualText = await this.getTextValue(locator);
|
|
2210
4103
|
if (actualText !== argument) {
|
|
2211
4104
|
throw new Error(`Validation failed. Expected text "${argument}", but got "${actualText}".`);
|
|
2212
4105
|
}
|
|
2213
|
-
|
|
4106
|
+
return true; // Return true for successful assertion
|
|
2214
4107
|
case PlaywrightAction.HOVER:
|
|
2215
|
-
//console.log('HOVER', locator);
|
|
2216
4108
|
if (locator) {
|
|
2217
|
-
|
|
2218
|
-
|
|
4109
|
+
await this.robustMouseAction(locator, 'hover');
|
|
4110
|
+
}
|
|
4111
|
+
else {
|
|
4112
|
+
throw new Error('not executing HOVER because no visible ancestor found');
|
|
2219
4113
|
}
|
|
2220
4114
|
break;
|
|
2221
4115
|
case PlaywrightAction.SCROLL_TO_ELEMENT:
|
|
2222
4116
|
// Restore exact scroll positions from recording
|
|
2223
4117
|
const scrollData = JSON.parse(argument);
|
|
2224
4118
|
try {
|
|
2225
|
-
|
|
4119
|
+
proboLogger.log('🔄 Restoring scroll position for container:', locator, 'scrollTop:', scrollData.scrollTop, 'scrollLeft:', scrollData.scrollLeft);
|
|
2226
4120
|
await locator.evaluate((el, scrollData) => {
|
|
2227
4121
|
// el.scrollTop = scrollData.scrollTop;
|
|
2228
4122
|
// el.scrollLeft = scrollData.scrollLeft;
|
|
2229
4123
|
el.scrollTo({ left: scrollData.scrollLeft, top: scrollData.scrollTop, behavior: 'smooth' });
|
|
2230
|
-
}, { scrollTop: scrollData.scrollTop, scrollLeft: scrollData.scrollLeft }, { timeout:
|
|
4124
|
+
}, { scrollTop: scrollData.scrollTop, scrollLeft: scrollData.scrollLeft }, { timeout: this.timeoutConfig.playwrightActionTimeout });
|
|
2231
4125
|
}
|
|
2232
4126
|
catch (e) {
|
|
2233
|
-
|
|
4127
|
+
proboLogger.error('🔄 Failed to restore scroll position for container:', locator, 'scrollTop:', scrollData.scrollTop, 'scrollLeft:', scrollData.scrollLeft, 'error:', e);
|
|
2234
4128
|
}
|
|
2235
4129
|
await this.page.waitForTimeout(500);
|
|
2236
4130
|
break;
|
|
2237
4131
|
case PlaywrightAction.UPLOAD_FILES:
|
|
2238
|
-
await locator.setInputFiles(argument);
|
|
4132
|
+
await locator.setInputFiles(argument, { timeout: this.timeoutConfig.playwrightActionTimeout });
|
|
2239
4133
|
break;
|
|
2240
4134
|
case PlaywrightAction.EXTRACT_VALUE:
|
|
2241
4135
|
let extractedText = await this.getTextValue(locator);
|
|
@@ -2256,7 +4150,7 @@ class ProboPlaywright {
|
|
|
2256
4150
|
// Check if the text matches (using the same logic as ASSERT_CONTAINS_VALUE)
|
|
2257
4151
|
if (matchRegex(currentText, expectedText)) {
|
|
2258
4152
|
textMatches = true;
|
|
2259
|
-
|
|
4153
|
+
proboLogger.log(`✅ Wait for text completed successfully. Found: "${currentText}"`);
|
|
2260
4154
|
}
|
|
2261
4155
|
else {
|
|
2262
4156
|
// Text doesn't match yet, wait for the polling interval
|
|
@@ -2286,12 +4180,105 @@ class ProboPlaywright {
|
|
|
2286
4180
|
}
|
|
2287
4181
|
}
|
|
2288
4182
|
/**
|
|
2289
|
-
*
|
|
2290
|
-
*
|
|
2291
|
-
*
|
|
2292
|
-
* @param
|
|
2293
|
-
* @
|
|
4183
|
+
* Public method to generate TOTP code using the provided secret, digits, and algorithm
|
|
4184
|
+
* @param secret - The TOTP secret (base32 encoded)
|
|
4185
|
+
* @param digits - Number of digits in the TOTP code (default: 6)
|
|
4186
|
+
* @param algorithm - Hash algorithm to use (default: 'SHA1')
|
|
4187
|
+
* @returns The generated TOTP code
|
|
2294
4188
|
*/
|
|
4189
|
+
generateOTP(secret, digits = 6, algorithm = 'SHA1') {
|
|
4190
|
+
const otp = new TOTP({
|
|
4191
|
+
algorithm,
|
|
4192
|
+
digits,
|
|
4193
|
+
secret,
|
|
4194
|
+
});
|
|
4195
|
+
return otp.generate();
|
|
4196
|
+
}
|
|
4197
|
+
async getLocator(elementSelector, smartSelector, getIFrameLocator = false, frameLocator = null) {
|
|
4198
|
+
if (!this.page) {
|
|
4199
|
+
throw new Error('Page is not set');
|
|
4200
|
+
}
|
|
4201
|
+
if (this.enableSmartSelectors && smartSelector && Object.keys(smartSelector).length > 0) {
|
|
4202
|
+
const { strategy, selector, index, label_selector } = smartSelector;
|
|
4203
|
+
proboLogger.debug(`getLocator: smartSelector=${JSON.stringify(smartSelector)}`);
|
|
4204
|
+
let labelLocator;
|
|
4205
|
+
let combinedSelector;
|
|
4206
|
+
let id;
|
|
4207
|
+
const mainStrategy = smartSelector.strategy.split(' ')[0]; //extract the main strategy from the full strategy string
|
|
4208
|
+
switch (mainStrategy) {
|
|
4209
|
+
case 'role':
|
|
4210
|
+
case 'placeholder':
|
|
4211
|
+
case 'name':
|
|
4212
|
+
case 'data-testid':
|
|
4213
|
+
case 'id':
|
|
4214
|
+
case 'css':
|
|
4215
|
+
case 'class':
|
|
4216
|
+
proboLogger.debug(`getLocator: strategy=${mainStrategy}, index=${index}`);
|
|
4217
|
+
if (typeof index === 'number') {
|
|
4218
|
+
const matchedLocators = await this.getLocatorOrFrame(selector, getIFrameLocator, frameLocator);
|
|
4219
|
+
if (!getIFrameLocator) {
|
|
4220
|
+
proboLogger.debug(`getLocator: matched ${await matchedLocators.count()} elements`);
|
|
4221
|
+
for (const [index, element] of (await matchedLocators.all()).entries()) {
|
|
4222
|
+
// Get outerHTML
|
|
4223
|
+
const html = await element.evaluate(el => el.outerHTML);
|
|
4224
|
+
proboLogger.debug(`[${index}] ${html}`);
|
|
4225
|
+
}
|
|
4226
|
+
}
|
|
4227
|
+
return matchedLocators.nth(index);
|
|
4228
|
+
}
|
|
4229
|
+
else {
|
|
4230
|
+
return this.getLocatorOrFrame(selector, getIFrameLocator, frameLocator);
|
|
4231
|
+
}
|
|
4232
|
+
case 'label':
|
|
4233
|
+
if (smartSelector.strategy === 'label for') {
|
|
4234
|
+
proboLogger.debug(`getLocator: strategy=${smartSelector.strategy}, label_selector=${label_selector}`);
|
|
4235
|
+
labelLocator = this.getLocatorOrFrame(label_selector, false, frameLocator);
|
|
4236
|
+
id = await labelLocator.getAttribute('for');
|
|
4237
|
+
proboLogger.debug(`getLocator: labelLocator.getAttribute('for')=${id}`);
|
|
4238
|
+
combinedSelector = selector ? `#${id} > ${selector}` : `#${id}`;
|
|
4239
|
+
proboLogger.debug(`getLocator: combinedSelector=${combinedSelector}`);
|
|
4240
|
+
return this.getLocatorOrFrame(combinedSelector, getIFrameLocator, frameLocator);
|
|
4241
|
+
}
|
|
4242
|
+
else if (smartSelector.strategy === 'label by') {
|
|
4243
|
+
proboLogger.debug(`getLocator: strategy=${smartSelector.strategy}, label_selector=${label_selector}`);
|
|
4244
|
+
labelLocator = this.getLocatorOrFrame(label_selector, false, frameLocator);
|
|
4245
|
+
id = await labelLocator.getAttribute('id');
|
|
4246
|
+
proboLogger.debug(`getLocator: labelLocator.getAttribute('id')=${id}`);
|
|
4247
|
+
combinedSelector = selector ? `[aria-labelledby="${id}"] > ${selector}` : `[aria-labelledby="${id}"]`;
|
|
4248
|
+
proboLogger.debug(`getLocator: combinedSelector=${combinedSelector}`);
|
|
4249
|
+
return this.getLocatorOrFrame(combinedSelector, getIFrameLocator, frameLocator);
|
|
4250
|
+
}
|
|
4251
|
+
else {
|
|
4252
|
+
throw new Error(`Unsupported label strategy: ${smartSelector.strategy}`);
|
|
4253
|
+
}
|
|
4254
|
+
default:
|
|
4255
|
+
throw new Error(`Unsupported smart selector strategy: ${strategy}`);
|
|
4256
|
+
}
|
|
4257
|
+
}
|
|
4258
|
+
// fallback to standard locator
|
|
4259
|
+
return this.getLocatorOrFrame(elementSelector, getIFrameLocator, frameLocator);
|
|
4260
|
+
}
|
|
4261
|
+
getLocatorOrFrame(selector, getIFrameLocator, frameLocator = null) {
|
|
4262
|
+
if (!this.page) {
|
|
4263
|
+
throw new Error('Page is not set');
|
|
4264
|
+
}
|
|
4265
|
+
if (getIFrameLocator) {
|
|
4266
|
+
proboLogger.debug(`getLocatorOrFrame: getting frame locator for selector=${selector}`);
|
|
4267
|
+
if (frameLocator) {
|
|
4268
|
+
throw new Error(`getLocatorOrFrame: frameLocator must be null when getIFrameLocator is true`);
|
|
4269
|
+
}
|
|
4270
|
+
return this.page.frameLocator(selector);
|
|
4271
|
+
}
|
|
4272
|
+
proboLogger.debug(`getLocatorOrFrame: ${frameLocator ? `frameLocator.locator("${selector}")` : `page.locator("${selector}")`}`);
|
|
4273
|
+
return frameLocator ? frameLocator.locator(selector) : this.page.locator(selector);
|
|
4274
|
+
}
|
|
4275
|
+
/**
|
|
4276
|
+
* Creates a visual highlight overlay on the target element with optional annotation text.
|
|
4277
|
+
* The highlight appears as a red border around the element and can include descriptive text.
|
|
4278
|
+
*
|
|
4279
|
+
* @param locator - The Playwright locator for the element to highlight
|
|
4280
|
+
* @param annotation - Optional text annotation to display above/below the highlighted element
|
|
4281
|
+
*/
|
|
2295
4282
|
async highlight(locator, annotation = null) {
|
|
2296
4283
|
try {
|
|
2297
4284
|
await locator.evaluate((el) => {
|
|
@@ -2322,7 +4309,7 @@ class ProboPlaywright {
|
|
|
2322
4309
|
}, { timeout: 500 });
|
|
2323
4310
|
}
|
|
2324
4311
|
catch (e) {
|
|
2325
|
-
|
|
4312
|
+
proboLogger.log('highlight: failed to run locator.evaluate()', e);
|
|
2326
4313
|
}
|
|
2327
4314
|
if (annotation) {
|
|
2328
4315
|
await locator.evaluate((el, annotation) => {
|
|
@@ -2356,11 +4343,11 @@ class ProboPlaywright {
|
|
|
2356
4343
|
}
|
|
2357
4344
|
;
|
|
2358
4345
|
/**
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
4346
|
+
* Removes the highlight overlay from the target element.
|
|
4347
|
+
* Cleans up the visual highlighting created by the highlight method.
|
|
4348
|
+
*
|
|
4349
|
+
* @param locator - The Playwright locator for the element to unhighlight
|
|
4350
|
+
*/
|
|
2364
4351
|
async unhighlight(locator) {
|
|
2365
4352
|
try {
|
|
2366
4353
|
await locator.evaluate((el) => {
|
|
@@ -2371,48 +4358,48 @@ class ProboPlaywright {
|
|
|
2371
4358
|
}, { timeout: 500 });
|
|
2372
4359
|
}
|
|
2373
4360
|
catch (e) {
|
|
2374
|
-
|
|
4361
|
+
proboLogger.log('unhighlight: failed to run locator.evaluate()', e);
|
|
2375
4362
|
}
|
|
2376
4363
|
}
|
|
2377
4364
|
;
|
|
2378
4365
|
/**
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
4366
|
+
* Attempts to fill a form field with the specified value using multiple fallback strategies.
|
|
4367
|
+
* First tries the standard fill method, then falls back to click + type if needed.
|
|
4368
|
+
*
|
|
4369
|
+
* @param locator - The Playwright locator for the input element
|
|
4370
|
+
* @param value - The text value to fill into the input field
|
|
4371
|
+
*/
|
|
2385
4372
|
async robustFill(locator, value) {
|
|
2386
4373
|
if (!this.page) {
|
|
2387
|
-
throw new Error('
|
|
4374
|
+
throw new Error('Page is not set');
|
|
2388
4375
|
}
|
|
2389
4376
|
try {
|
|
2390
|
-
await locator.fill(value);
|
|
4377
|
+
await locator.fill(value, { timeout: this.timeoutConfig.playwrightActionTimeout });
|
|
2391
4378
|
return;
|
|
2392
4379
|
}
|
|
2393
4380
|
catch (err) {
|
|
2394
|
-
|
|
4381
|
+
proboLogger.warn('robustFill: failed to run locator.fill(), trying fallback', err);
|
|
2395
4382
|
}
|
|
2396
4383
|
// fallback: click and type
|
|
2397
4384
|
try {
|
|
2398
|
-
await locator.focus();
|
|
4385
|
+
await locator.focus({ timeout: this.timeoutConfig.playwrightActionTimeout });
|
|
2399
4386
|
await this.page.keyboard.type(value);
|
|
2400
4387
|
return;
|
|
2401
4388
|
}
|
|
2402
4389
|
catch (err) {
|
|
2403
|
-
|
|
4390
|
+
proboLogger.warn('robustFill: failed to run locator.focus() and page.keyboard.type()', err);
|
|
2404
4391
|
}
|
|
2405
4392
|
}
|
|
2406
4393
|
;
|
|
2407
4394
|
async robustTypeKeys(value) {
|
|
2408
4395
|
if (!this.page) {
|
|
2409
|
-
throw new Error('
|
|
4396
|
+
throw new Error('Page is not set');
|
|
2410
4397
|
}
|
|
2411
4398
|
/* try {
|
|
2412
|
-
|
|
2413
|
-
|
|
4399
|
+
await locator.press(value);
|
|
4400
|
+
return;
|
|
2414
4401
|
} catch (err) {
|
|
2415
|
-
|
|
4402
|
+
proboLogger.warn('robustTypeKeys: failed to run locator.type()', err);
|
|
2416
4403
|
} */
|
|
2417
4404
|
// fallback: click and type
|
|
2418
4405
|
try {
|
|
@@ -2421,69 +4408,98 @@ class ProboPlaywright {
|
|
|
2421
4408
|
return;
|
|
2422
4409
|
}
|
|
2423
4410
|
catch (err) {
|
|
2424
|
-
|
|
4411
|
+
proboLogger.warn('robustTypeKeys: failed to run page.keyboard.type()', err);
|
|
2425
4412
|
}
|
|
2426
4413
|
}
|
|
2427
4414
|
/**
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
4415
|
+
* Performs a robust mouse action (click or hover) using multiple fallback strategies.
|
|
4416
|
+
* Attempts standard click first, then mouse click at center coordinates, and finally native DOM events.
|
|
4417
|
+
*
|
|
4418
|
+
* @param locator - The Playwright locator for the element to click
|
|
4419
|
+
* @param action - The mouse action to perform ('click' or 'hover')
|
|
4420
|
+
* @throws Error if all mouse action methods fail
|
|
4421
|
+
*/
|
|
4422
|
+
async robustMouseAction(locator, action) {
|
|
2435
4423
|
if (!this.page) {
|
|
2436
|
-
throw new Error('
|
|
4424
|
+
throw new Error('Page is not set');
|
|
2437
4425
|
}
|
|
2438
4426
|
// start with a standard click
|
|
2439
4427
|
try {
|
|
2440
|
-
|
|
4428
|
+
if (action === 'click') {
|
|
4429
|
+
await locator.click({ noWaitAfter: false, timeout: this.timeoutConfig.playwrightActionTimeout });
|
|
4430
|
+
}
|
|
4431
|
+
else if (action === 'hover') {
|
|
4432
|
+
await locator.hover({ timeout: this.timeoutConfig.playwrightActionTimeout });
|
|
4433
|
+
}
|
|
4434
|
+
else {
|
|
4435
|
+
throw new Error(`Unsupported mouse action: ${action}`);
|
|
4436
|
+
}
|
|
2441
4437
|
return;
|
|
2442
4438
|
}
|
|
2443
4439
|
catch (err) {
|
|
2444
|
-
|
|
4440
|
+
proboLogger.warn(`robustMouseAction: failed to run locator.${action}(), trying mouse.${action}()`);
|
|
2445
4441
|
}
|
|
2446
4442
|
// try clicking using mouse at the center of the element
|
|
2447
4443
|
try {
|
|
2448
4444
|
const bbox = await locator.boundingBox({ timeout: this.timeoutConfig.playwrightLocatorTimeout });
|
|
2449
4445
|
if (bbox) {
|
|
2450
|
-
|
|
4446
|
+
if (action === 'click') {
|
|
4447
|
+
await this.page.mouse.click(bbox.x + bbox.width / 2, bbox.y + bbox.height / 2);
|
|
4448
|
+
}
|
|
4449
|
+
else if (action === 'hover') {
|
|
4450
|
+
await this.page.mouse.move(bbox.x + bbox.width / 2, bbox.y + bbox.height / 2);
|
|
4451
|
+
}
|
|
2451
4452
|
return;
|
|
2452
4453
|
}
|
|
2453
4454
|
else {
|
|
2454
|
-
|
|
4455
|
+
proboLogger.warn('robustMouseAction: bounding box not found');
|
|
2455
4456
|
}
|
|
2456
4457
|
}
|
|
2457
4458
|
catch (err2) {
|
|
2458
|
-
|
|
4459
|
+
proboLogger.warn(`robustMouseAction: failed to run page.mouse.${action}(), trying dispatching an event`);
|
|
2459
4460
|
}
|
|
2460
4461
|
// fallback: dispatch native mouse events manually
|
|
2461
4462
|
try {
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
4463
|
+
if (action === 'click') {
|
|
4464
|
+
await locator.evaluate((el) => {
|
|
4465
|
+
['mousedown', 'mouseup', 'click'].forEach(type => {
|
|
4466
|
+
const event = new MouseEvent(type, {
|
|
4467
|
+
bubbles: true,
|
|
4468
|
+
cancelable: true,
|
|
4469
|
+
view: window
|
|
4470
|
+
});
|
|
4471
|
+
el.dispatchEvent(event);
|
|
2468
4472
|
});
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
4473
|
+
}, { timeout: this.timeoutConfig.playwrightActionTimeout });
|
|
4474
|
+
}
|
|
4475
|
+
else if (action === 'hover') {
|
|
4476
|
+
await locator.evaluate((el) => {
|
|
4477
|
+
['mouseenter', 'mouseover', 'focusin', 'focus'].forEach(type => {
|
|
4478
|
+
const event = new MouseEvent(type, {
|
|
4479
|
+
bubbles: true,
|
|
4480
|
+
cancelable: true,
|
|
4481
|
+
view: window
|
|
4482
|
+
});
|
|
4483
|
+
el.dispatchEvent(event);
|
|
4484
|
+
});
|
|
4485
|
+
}, { timeout: this.timeoutConfig.playwrightActionTimeout });
|
|
4486
|
+
}
|
|
2472
4487
|
}
|
|
2473
4488
|
catch (err3) {
|
|
2474
|
-
|
|
2475
|
-
|
|
4489
|
+
proboLogger.error(`robustMouseAction: all ${action} methods failed:`);
|
|
4490
|
+
// not re-throwing playwright errors as they are long and not useful for the user
|
|
4491
|
+
throw new Error(`robustMouseAction: all ${action} methods failed`);
|
|
2476
4492
|
}
|
|
2477
4493
|
}
|
|
2478
4494
|
;
|
|
2479
4495
|
/**
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
4496
|
+
* Extracts text content from an element using multiple strategies.
|
|
4497
|
+
* Tries textContent first, then inputValue, and finally looks for nested input elements.
|
|
4498
|
+
* Returns normalized and trimmed text for consistent comparison.
|
|
4499
|
+
*
|
|
4500
|
+
* @param locator - The Playwright locator for the element to extract text from
|
|
4501
|
+
* @returns Normalized text content with consistent whitespace handling
|
|
4502
|
+
*/
|
|
2487
4503
|
async getTextValue(locator) {
|
|
2488
4504
|
let textValue = await locator.textContent();
|
|
2489
4505
|
if (!textValue) {
|
|
@@ -2491,7 +4507,7 @@ class ProboPlaywright {
|
|
|
2491
4507
|
textValue = await locator.inputValue();
|
|
2492
4508
|
}
|
|
2493
4509
|
catch (err) {
|
|
2494
|
-
|
|
4510
|
+
// proboLogger.warn('getTextValue: failed to run locator.inputValue()', err);
|
|
2495
4511
|
}
|
|
2496
4512
|
}
|
|
2497
4513
|
if (!textValue) {
|
|
@@ -2499,7 +4515,7 @@ class ProboPlaywright {
|
|
|
2499
4515
|
textValue = await locator.locator('input').inputValue();
|
|
2500
4516
|
}
|
|
2501
4517
|
catch (err) {
|
|
2502
|
-
|
|
4518
|
+
// proboLogger.warn('getTextValue: failed to run locator.locator("input").inputValue()', err);
|
|
2503
4519
|
}
|
|
2504
4520
|
}
|
|
2505
4521
|
if (!textValue) {
|
|
@@ -2510,16 +4526,31 @@ class ProboPlaywright {
|
|
|
2510
4526
|
}
|
|
2511
4527
|
;
|
|
2512
4528
|
async setSliderValue(locator, value) {
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
4529
|
+
try {
|
|
4530
|
+
await locator.fill(value, { timeout: this.timeoutConfig.playwrightActionTimeout });
|
|
4531
|
+
return;
|
|
4532
|
+
}
|
|
4533
|
+
catch (err) {
|
|
4534
|
+
proboLogger.warn('setSliderValue: failed to run locator.fill(), trying fallback', err);
|
|
4535
|
+
}
|
|
4536
|
+
// fallback
|
|
4537
|
+
try {
|
|
4538
|
+
await locator.focus();
|
|
4539
|
+
await locator.evaluate((el, value) => {
|
|
4540
|
+
el.value = value;
|
|
4541
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
4542
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
4543
|
+
}, value, { timeout: this.timeoutConfig.playwrightActionTimeout });
|
|
4544
|
+
}
|
|
4545
|
+
catch (err) {
|
|
4546
|
+
proboLogger.error('setSliderValue: failed to run locator.evaluate()', err);
|
|
4547
|
+
}
|
|
2518
4548
|
}
|
|
2519
4549
|
} /* class ProboPlaywright */
|
|
2520
4550
|
|
|
2521
4551
|
class Probo {
|
|
2522
|
-
constructor({ scenarioName, token = '', apiUrl = '', enableConsoleLogs = false, logToConsole = true, logToFile = false, debugLevel = ProboLogLevel.INFO, aiModel = AIModel.AZURE_GPT4_MINI, timeoutConfig = {} }) {
|
|
4552
|
+
constructor({ scenarioName, token = '', apiUrl = '', enableConsoleLogs = false, enableSmartSelectors = false, logToConsole = true, logToFile = false, debugLevel = ProboLogLevel.INFO, aiModel = AIModel.AZURE_GPT4_MINI, timeoutConfig = {} }) {
|
|
4553
|
+
this.params = {};
|
|
2523
4554
|
// Configure logger transports and level
|
|
2524
4555
|
// configureLogger({ logToConsole, logToFile, level: debugLevel });
|
|
2525
4556
|
proboLogger.setLogLevel(debugLevel);
|
|
@@ -2533,26 +4564,50 @@ class Probo {
|
|
|
2533
4564
|
proboLogger.error("API endpoint wasn't provided. Pass 'apiUrl' or set PROBO_API_ENDPOINT");
|
|
2534
4565
|
throw new Error('Probo API endpoint not provided');
|
|
2535
4566
|
}
|
|
2536
|
-
this.highlighter = new Highlighter(enableConsoleLogs);
|
|
4567
|
+
this.highlighter = new Highlighter(enableSmartSelectors, enableConsoleLogs);
|
|
2537
4568
|
this.apiClient = new ApiClient(apiEndPoint, apiKey);
|
|
4569
|
+
this.debugLevel = debugLevel;
|
|
2538
4570
|
this.enableConsoleLogs = enableConsoleLogs;
|
|
2539
4571
|
this.scenarioName = scenarioName;
|
|
2540
4572
|
this.aiModel = aiModel;
|
|
4573
|
+
this.enableSmartSelectors = enableSmartSelectors;
|
|
2541
4574
|
this.timeoutConfig = { ...DEFAULT_PLAYWRIGHT_TIMEOUT_CONFIG, ...timeoutConfig };
|
|
2542
4575
|
// set the log level for the api client
|
|
2543
|
-
apiLogger.setLogLevel(debugLevel);
|
|
4576
|
+
apiLogger.setLogLevel(this.debugLevel);
|
|
2544
4577
|
proboLogger.info(`Initializing: scenario=${scenarioName}, apiUrl=${apiEndPoint}, ` +
|
|
2545
4578
|
`enableConsoleLogs=${enableConsoleLogs}, debugLevel=${debugLevel}, aiModel=${aiModel}`);
|
|
2546
4579
|
}
|
|
2547
|
-
|
|
4580
|
+
/**
|
|
4581
|
+
* Sets the parameters object for template literal interpolation
|
|
4582
|
+
* Stores a reference to the params object so mutations are automatically reflected
|
|
4583
|
+
* @param params The parameters object containing values to use for interpolation
|
|
4584
|
+
*/
|
|
4585
|
+
setParams(params) {
|
|
4586
|
+
this.params = params;
|
|
4587
|
+
}
|
|
4588
|
+
/**
|
|
4589
|
+
* Interpolates a string using the current params and optional additional context
|
|
4590
|
+
* @param str The string to interpolate (may contain ${variable} syntax)
|
|
4591
|
+
* @param additionalContext Optional additional context to merge with params
|
|
4592
|
+
* @returns The interpolated string
|
|
4593
|
+
*/
|
|
4594
|
+
interpolate(str, additionalContext = {}) {
|
|
4595
|
+
const context = { ...this.params, ...additionalContext };
|
|
4596
|
+
return interpolateTemplate(str, context);
|
|
4597
|
+
}
|
|
4598
|
+
async askAI(page, question, options, assertAnswer = '') {
|
|
2548
4599
|
var _a, _b;
|
|
2549
|
-
const response = await this.askAIHelper(page, question, options);
|
|
4600
|
+
const response = await this.askAIHelper(page, question, options, assertAnswer);
|
|
2550
4601
|
if ((_a = response === null || response === void 0 ? void 0 : response.result) === null || _a === void 0 ? void 0 : _a.error) {
|
|
2551
4602
|
throw new Error(response.result.error);
|
|
2552
4603
|
}
|
|
2553
4604
|
return (_b = response === null || response === void 0 ? void 0 : response.result) === null || _b === void 0 ? void 0 : _b.answer;
|
|
2554
4605
|
}
|
|
2555
4606
|
async runStep(page, stepPrompt, argument = null, options = { aiModel: this.aiModel, timeoutConfig: this.timeoutConfig }) {
|
|
4607
|
+
// Interpolate argument if it's a string with template literals
|
|
4608
|
+
if (typeof argument === 'string' && argument.includes('${')) {
|
|
4609
|
+
argument = this.interpolate(argument);
|
|
4610
|
+
}
|
|
2556
4611
|
const runStepStartTime = Date.now();
|
|
2557
4612
|
try {
|
|
2558
4613
|
// Determine which AI model to use for this step
|
|
@@ -2623,7 +4678,7 @@ class Probo {
|
|
|
2623
4678
|
proboLogger.debug(`⏱️ findBestCandidateElement took ${findBestCandidateTime}ms`);
|
|
2624
4679
|
proboLogger.debug(`AI selected candidate element at index: ${index}`);
|
|
2625
4680
|
// STEP 8: Find the actual element object from the candidates
|
|
2626
|
-
const actualElement = candidate_elements.find(element =>
|
|
4681
|
+
const actualElement = candidate_elements.find(element => element.index === index);
|
|
2627
4682
|
if (!actualElement) {
|
|
2628
4683
|
throw new Error(`No candidate element found with index ${index}. Available indices: ${candidate_elements.map(e => e.index).join(', ')}`);
|
|
2629
4684
|
}
|
|
@@ -2662,11 +4717,17 @@ class Probo {
|
|
|
2662
4717
|
proboLogger.log(`Found existing step with ID: ${result.id} going to perform action: ${result.action} with value: ${actionArgument}`);
|
|
2663
4718
|
try {
|
|
2664
4719
|
// Create ProboPlaywright instance with the page
|
|
2665
|
-
const proboPlaywright = new ProboPlaywright(
|
|
4720
|
+
const proboPlaywright = new ProboPlaywright({
|
|
4721
|
+
enableSmartSelectors: this.enableSmartSelectors,
|
|
4722
|
+
debugLevel: this.debugLevel,
|
|
4723
|
+
timeoutConfig: this.timeoutConfig
|
|
4724
|
+
}, page);
|
|
2666
4725
|
// Call runStep with the cached action
|
|
2667
4726
|
const runStepResult = await proboPlaywright.runStep({
|
|
2668
4727
|
iframeSelector: result.iframe_selector,
|
|
2669
4728
|
elementSelector: result.element_css_selector,
|
|
4729
|
+
smartSelector: result.smart_selector,
|
|
4730
|
+
smartIFrameSelector: result.smart_iframe_selector,
|
|
2670
4731
|
action: result.action,
|
|
2671
4732
|
argument: actionArgument,
|
|
2672
4733
|
});
|
|
@@ -2710,8 +4771,8 @@ class Probo {
|
|
|
2710
4771
|
async unhighlightElements(page) {
|
|
2711
4772
|
return this.highlighter.unhighlightElements(page);
|
|
2712
4773
|
}
|
|
2713
|
-
async highlightElement(page, element_css_selector, iframe_selector, element_index) {
|
|
2714
|
-
return this.highlighter.highlightElement(page, element_css_selector, iframe_selector, element_index);
|
|
4774
|
+
async highlightElement(page, element_css_selector, iframe_selector, smart_selector, smart_iframe_selector, element_index) {
|
|
4775
|
+
return this.highlighter.highlightElement(page, element_css_selector, iframe_selector, smart_selector, smart_iframe_selector, element_index);
|
|
2715
4776
|
}
|
|
2716
4777
|
async waitForMutationsToSettle(page, timeout, initTimeout) {
|
|
2717
4778
|
const mutationTimeout = timeout !== null && timeout !== void 0 ? timeout : DEFAULT_PLAYWRIGHT_TIMEOUT_CONFIG.mutationsTimeout;
|
|
@@ -2721,7 +4782,7 @@ class Probo {
|
|
|
2721
4782
|
async screenshot(page) {
|
|
2722
4783
|
proboLogger.debug(`taking screenshot of current page: ${page.url()}`);
|
|
2723
4784
|
// await page.evaluate(() => document.fonts?.ready.catch(() => {}));
|
|
2724
|
-
const screenshot_bytes = await page.screenshot({ fullPage: true, animations: 'disabled' });
|
|
4785
|
+
const screenshot_bytes = await page.screenshot({ fullPage: true, animations: 'disabled', timeout: 10000 });
|
|
2725
4786
|
// make an api call to upload the screenshot to cloud
|
|
2726
4787
|
proboLogger.debug('uploading image data to cloud');
|
|
2727
4788
|
const screenshot_url = await this.apiClient.uploadScreenshot(screenshot_bytes);
|
|
@@ -2732,17 +4793,25 @@ class Probo {
|
|
|
2732
4793
|
const element_css_selector = actualElement.css_selector;
|
|
2733
4794
|
const iframe_selector = actualElement.iframe_selector;
|
|
2734
4795
|
const element_index = actualElement.index;
|
|
2735
|
-
|
|
4796
|
+
const smart_selector = actualElement.smart_selector;
|
|
4797
|
+
const smart_iframe_selector = actualElement.smart_iframe_selector;
|
|
4798
|
+
proboLogger.debug('Handling perform action:', { action, element_css_selector, iframe_selector, smart_selector, smart_iframe_selector, element_index });
|
|
2736
4799
|
if (action !== PlaywrightAction.VISIT_URL) {
|
|
2737
4800
|
await this.unhighlightElements(page);
|
|
2738
4801
|
proboLogger.debug('Unhighlighted elements');
|
|
2739
|
-
await this.highlightElement(page, element_css_selector, iframe_selector, element_index);
|
|
4802
|
+
await this.highlightElement(page, element_css_selector, iframe_selector, smart_selector, smart_iframe_selector, element_index);
|
|
2740
4803
|
proboLogger.debug('Highlighted element');
|
|
2741
4804
|
}
|
|
2742
4805
|
const pre_action_screenshot_url = await this.screenshot(page);
|
|
2743
|
-
const returnValue = await new ProboPlaywright(
|
|
4806
|
+
const returnValue = await new ProboPlaywright({
|
|
4807
|
+
enableSmartSelectors: this.enableSmartSelectors,
|
|
4808
|
+
debugLevel: this.debugLevel,
|
|
4809
|
+
timeoutConfig: this.timeoutConfig
|
|
4810
|
+
}, page).runStep({
|
|
2744
4811
|
iframeSelector: iframe_selector,
|
|
2745
4812
|
elementSelector: element_css_selector,
|
|
4813
|
+
smartSelector: smart_selector,
|
|
4814
|
+
smartIFrameSelector: smart_iframe_selector,
|
|
2746
4815
|
action: action,
|
|
2747
4816
|
argument: value,
|
|
2748
4817
|
highlightTimeout: 0
|
|
@@ -2756,7 +4825,8 @@ class Probo {
|
|
|
2756
4825
|
});
|
|
2757
4826
|
return returnValue;
|
|
2758
4827
|
}
|
|
2759
|
-
async askAIHelper(page, question, options) {
|
|
4828
|
+
async askAIHelper(page, question, options, assertAnswer = '') {
|
|
4829
|
+
var _a, _b;
|
|
2760
4830
|
// Set default value for createStep to true if not provided
|
|
2761
4831
|
const createStep = (options === null || options === void 0 ? void 0 : options.createStep) !== undefined ? options.createStep : true;
|
|
2762
4832
|
const stepId = options === null || options === void 0 ? void 0 : options.stepId;
|
|
@@ -2824,6 +4894,14 @@ class Probo {
|
|
|
2824
4894
|
proboLogger.debug(`✅ [askAI] Step created successfully with ID: ${stepId}`);
|
|
2825
4895
|
}
|
|
2826
4896
|
// Return the answer from the result, or the reasoning if no answer
|
|
4897
|
+
if (assertAnswer) {
|
|
4898
|
+
const actualAnswer = (_b = (_a = serverResponse === null || serverResponse === void 0 ? void 0 : serverResponse.result) === null || _a === void 0 ? void 0 : _a.answer) === null || _b === void 0 ? void 0 : _b.toString();
|
|
4899
|
+
const interpolatedAssertAnswer = this.interpolate(assertAnswer);
|
|
4900
|
+
if (actualAnswer !== interpolatedAssertAnswer) {
|
|
4901
|
+
proboLogger.error(`❌ [askAI] Expected answer to be ${interpolatedAssertAnswer}, but got ${actualAnswer}`);
|
|
4902
|
+
throw new Error(`Expected answer to be ${interpolatedAssertAnswer}, but got ${actualAnswer}`);
|
|
4903
|
+
}
|
|
4904
|
+
}
|
|
2827
4905
|
return serverResponse;
|
|
2828
4906
|
}
|
|
2829
4907
|
catch (error) {
|
|
@@ -2833,5 +4911,5 @@ class Probo {
|
|
|
2833
4911
|
}
|
|
2834
4912
|
}
|
|
2835
4913
|
|
|
2836
|
-
export { Highlighter, NavTracker, OTP, PlaywrightAction, Probo, ProboLogLevel, ProboPlaywright, findClosestVisibleElement };
|
|
4914
|
+
export { AIModel, Highlighter, NavTracker, OTP, PlaywrightAction, Probo, ProboLogLevel, ProboPlaywright, findClosestVisibleElement };
|
|
2837
4915
|
//# sourceMappingURL=index.js.map
|