@probolabs/playwright 0.4.17 → 0.4.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/index.d.ts +56 -60
- package/dist/index.js +1238 -1024
- package/dist/index.js.map +1 -1
- package/dist/types/actions.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/probo-logger.d.ts.map +1 -0
- package/package.json +5 -4
- package/dist/types/api-client.d.ts.map +0 -1
- package/dist/types/utils.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,58 +1,63 @@
|
|
|
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}) {\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 }\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 if (currentElement.assignedSlot) {\n currentElement = currentElement.assignedSlot;\n }\n else {\n currentElement = currentElement.parentNode;\n // Check if we're at a shadow root\n if (currentElement && currentElement.nodeType !== Node.ELEMENT_NODE && currentElement.getRootNode() instanceof ShadowRoot) {\n // Get the shadow root's host element\n currentElement = currentElement.getRootNode().host; \n }\n }\n }\n \n return this.depth;\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'].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 let element;\n\n console.log('getElementByXPathOrCssSelector:', element_info);\n // if (element_info.xpath) { //try xpath if exists\n // element = document.evaluate(\n // element_info.xpath, \n // document, \n // null, \n // XPathResult.FIRST_ORDERED_NODE_TYPE, \n // null\n // ).singleNodeValue;\n \n // if (!element) {\n // console.warn('Failed to find element with xpath:', element_info.xpath);\n // }\n // }\n // else { //try CSS selector\n if (element_info.iframe_selector) {\n console.log('elementInfo with iframe: ', element_info);\n const frames = getAllDocumentElementsIncludingShadow('iframe');\n \n // Iterate over all frames and compare their CSS selectors\n for (const frame of frames) {\n const cssSelector = generateCssPath(frame);\n if (cssSelector === element_info.iframe_selector) {\n const frameDocument = frame.contentDocument || frame.contentWindow.document;\n element = querySelectorShadow(element_info.css_selector, frameDocument);\n console.log('found element ', element);\n break;\n } \n } }\n else\n element = querySelectorShadow(element_info.css_selector);\n // console.log('found element by CSS elector: ', element);\n if (!element) {\n console.warn('Failed to find element with CSS selector:', element_info.css_selector);\n }\n // }\n\n return element;\n };\n\n function generateXPath(element) {\n if (!element || element.getRootNode() instanceof ShadowRoot) return '';\n \n // If element has an id, use that (it's unique and shorter)\n if (element.id) {\n return `//*[@id=\"${element.id}\"]`;\n }\n \n const parts = [];\n let current = element;\n \n while (current && current.nodeType === Node.ELEMENT_NODE) {\n let index = 1;\n let sibling = current.previousSibling;\n \n while (sibling) {\n if (sibling.nodeType === Node.ELEMENT_NODE && sibling.tagName === current.tagName) {\n index++;\n }\n sibling = sibling.previousSibling;\n }\n \n const tagName = current.tagName.toLowerCase();\n parts.unshift(`${tagName}[${index}]`);\n current = current.parentNode;\n }\n \n return '/' + parts.join('/');\n }\n\n function isDecendent(parent, child) {\n let element = child;\n while (element.nodeType === Node.ELEMENT_NODE) { \n \n if (element.assignedSlot) {\n element = element.assignedSlot;\n }\n else {\n element = element.parentNode;\n // Check if we're at a shadow root\n if (element && element.nodeType !== Node.ELEMENT_NODE && element.getRootNode() instanceof ShadowRoot) {\n // Get the shadow root's host element\n element = element.getRootNode().host; \n }\n }\n if (element === parent)\n return true;\n }\n return false;\n }\n\n function generateCssPath(element) {\n if (!element) {\n console.error('ERROR: No element provided to generateCssPath returning empty string');\n return '';\n }\n const path = [];\n // console.group('Generating CSS path for:', element);\n while (element && element.nodeType === Node.ELEMENT_NODE) { \n let selector = element.nodeName.toLowerCase();\n // console.log('Element:', selector, element);\n \n // if (element.id) {\n // //escape special characters\n // const normalized_id = element.id.replace(/[:;.#()[\\]!@$%^&*]/g, '\\\\$&');\n // selector = `#${normalized_id}`;\n // path.unshift(selector);\n // break;\n // } \n \n let sibling = element;\n let nth = 1;\n while (sibling = sibling.previousElementSibling) {\n if (sibling.nodeName.toLowerCase() === selector) nth++;\n }\n sibling = element;\n while (sibling = sibling.nextElementSibling) {\n if (sibling.nodeName.toLowerCase() === selector) {\n break;\n }\n }\n selector += `:nth-of-type(${nth})`;\n \n \n path.unshift(selector);\n //console.log(` Current path: ${path.join(' > ')}`);\n\n if (element.assignedSlot) {\n element = element.assignedSlot;\n // console.log(' Moving to assigned slot');\n }\n else {\n element = element.parentNode;\n // console.log(' Moving to parent:', element);\n\n // Check if we're at a shadow root\n if (element && element.nodeType !== Node.ELEMENT_NODE && element.getRootNode() instanceof ShadowRoot) {\n console.log(' Found shadow root, moving to host');\n // Get the shadow root's host element\n element = element.getRootNode().host; \n }\n }\n }\n \n // console.log('Final selector:', path.join(' > '));\n // console.groupEnd();\n return path.join(' > ');\n }\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\n const xpath = generateXPath(element);\n const css_selector = generateCssPath(element);\n\n const iframe = getContainingIframe(element);\n const iframe_selector = iframe ? generateCssPath(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 });\n }\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 // if (elementInfo.element.isConnected) {\n // console.log('Filtered out invisible/zero-size element:', {\n // tag: elementInfo.tag,\n // xpath: elementInfo.xpath,\n // element: elementInfo.element,\n // hasSize,\n // isVisible,\n // dimensions: rect\n // });\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 // //Use element child/parent queries\n // let parent = element_info.element.parentNode;\n // if (parent.getRootNode() instanceof ShadowRoot) {\n // // Get the shadow root's host element\n // parent = parent.getRootNode().host; \n // }\n\n // while (parent.nodeType === Node.ELEMENT_NODE) { \n // const css_selector = generateCssPath(parent);\n // if (seen.has(css_selector)) {\n // console.log(\"element \", element_info, \" closest parent is \", parent)\n // return parent; \n // }\n // parent = parent.parentNode;\n // if (parent.getRootNode() instanceof ShadowRoot) {\n // // Get the shadow root's host element\n // parent = parent.getRootNode().host; \n // }\n // }\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 // Add detailed logging for overlaid elements with formatted output\n console.log('[OVERLAY-DEBUG]', JSON.stringify({\n originalElement: {\n selector: elementInfo.css_selector,\n rect: {\n x: boundingRect.x,\n y: boundingRect.y,\n width: boundingRect.width,\n height: boundingRect.height,\n top: boundingRect.top,\n right: boundingRect.right,\n bottom: boundingRect.bottom,\n left: boundingRect.left\n }\n },\n overlayingElement: {\n selector: generateCssPath(elementAtMiddle),\n rect: {\n x: elementAtMiddle.getBoundingClientRect().x,\n y: elementAtMiddle.getBoundingClientRect().y,\n width: elementAtMiddle.getBoundingClientRect().width,\n height: elementAtMiddle.getBoundingClientRect().height,\n top: elementAtMiddle.getBoundingClientRect().top,\n right: elementAtMiddle.getBoundingClientRect().right,\n bottom: elementAtMiddle.getBoundingClientRect().bottom,\n left: elementAtMiddle.getBoundingClientRect().left\n }\n },\n middlePoint: { x: middleX, y: middleY }\n }, null, 2));\n\n console.log('[OVERLAY-REMOVED]', elementInfo.css_selector, elementInfo.bounding_box, 'elementAtMiddle:', elementAtMiddle, 'elementAtMiddle selector:', generateCssPath(elementAtMiddle));\n return true;\n }\n \n // Check specifically if the element at middle is a popup/modal\n // if (elementAtMiddle && isElementOrAncestorPopup(elementAtMiddle)) {\n // diagnosticLog('Element at middle is a popup/modal, element IS overlaid');\n // return true; // It's under a popup, so it is overlaid\n // }\n return false;\n // }\n // return false;\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('Starting highlight for elements:', elements);\n \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 }\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 => {\n // console.log('updateHighlights-Processing elementInfo:', elementInfo);\n let element = elementInfo.element; //getElementByXPathOrCssSelector(elementInfo);\n if (!element) {\n element = getElementByXPathOrCssSelector(elementInfo);\n if (!element)\n return;\n }\n \n //if highlights requested for a specific doc, skip unrelated elements\n if (doc && element.ownerDocument !== doc) {\n console.log(\"skipped element \", element, \" since it doesn't belong to document \", doc);\n return;\n }\n\n const rect = element.getBoundingClientRect();\n // console.log('Element rect:', elementInfo.tag, rect);\n \n if (rect.width === 0 || rect.height === 0) {\n console.warn('Element has zero dimensions:', elementInfo);\n return;\n }\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\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\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 \n labelContainer.appendChild(text);\n highlight.appendChild(labelContainer); \n overlays[element.ownerDocument.documentURI].appendChild(highlight);\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 window.ProboLabs = {\n ElementTag,\n highlight,\n unhighlightElements,\n findElements,\n highlightElements,\n getElementInfo\n };\n }\n\n exports.findElements = findElements;\n exports.getElementInfo = getElementInfo;\n exports.highlight = highlight;\n exports.highlightElements = highlightElements;\n exports.unhighlightElements = unhighlightElements;\n\n Object.defineProperty(exports, '__esModule', { value: true });\n\n}));\n//# sourceMappingURL=probolabs.umd.js.map\n";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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 if (currentElement.assignedSlot) {\n currentElement = currentElement.assignedSlot;\n }\n else {\n currentElement = currentElement.parentNode;\n // Check if we're at a shadow root\n if (currentElement && currentElement.nodeType !== Node.ELEMENT_NODE && currentElement.getRootNode() instanceof ShadowRoot) {\n // Get the shadow root's host element\n currentElement = currentElement.getRootNode().host; \n }\n }\n }\n \n return this.depth;\n }\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'].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 function generateXPath(element) {\n if (!element || element.getRootNode() instanceof ShadowRoot) return '';\n \n // If element has an id, use that (it's unique and shorter)\n if (element.id) {\n return `//*[@id=\"${element.id}\"]`;\n }\n \n const parts = [];\n let current = element;\n \n while (current && current.nodeType === Node.ELEMENT_NODE) {\n let index = 1;\n let sibling = current.previousSibling;\n \n while (sibling) {\n if (sibling.nodeType === Node.ELEMENT_NODE && sibling.tagName === current.tagName) {\n index++;\n }\n sibling = sibling.previousSibling;\n }\n \n const tagName = current.tagName.toLowerCase();\n parts.unshift(`${tagName}[${index}]`);\n current = current.parentNode;\n }\n \n return '/' + parts.join('/');\n }\n\n function isDecendent(parent, child) {\n let element = child;\n while (element.nodeType === Node.ELEMENT_NODE) { \n \n if (element.assignedSlot) {\n element = element.assignedSlot;\n }\n else {\n element = element.parentNode;\n // Check if we're at a shadow root\n if (element && element.nodeType !== Node.ELEMENT_NODE && element.getRootNode() instanceof ShadowRoot) {\n // Get the shadow root's host element\n element = element.getRootNode().host; \n }\n }\n if (element === parent)\n return true;\n }\n return false;\n }\n\n function generateCssPath(element) {\n if (!element) {\n console.error('ERROR: No element provided to generateCssPath returning empty string');\n return '';\n }\n const path = [];\n // console.group('Generating CSS path for:', element);\n while (element && element.nodeType === Node.ELEMENT_NODE) { \n let selector = element.nodeName.toLowerCase();\n \n \n let sibling = element;\n let nth = 1;\n while (sibling = sibling.previousElementSibling) {\n if (sibling.nodeName.toLowerCase() === selector) nth++;\n }\n sibling = element;\n while (sibling = sibling.nextElementSibling) {\n if (sibling.nodeName.toLowerCase() === selector) {\n break;\n }\n }\n selector += `:nth-of-type(${nth})`;\n \n \n path.unshift(selector);\n //console.log(` Current path: ${path.join(' > ')}`);\n\n if (element.assignedSlot) {\n element = element.assignedSlot;\n // console.log(' Moving to assigned slot');\n }\n else {\n element = element.parentNode;\n // console.log(' Moving to parent:', element);\n\n // Check if we're at a shadow root\n if (element && element.nodeType !== Node.ELEMENT_NODE && element.getRootNode() instanceof ShadowRoot) {\n console.log(' Found shadow root, moving to host');\n // Get the shadow root's host element\n element = element.getRootNode().host; \n }\n }\n }\n \n // console.log('Final selector:', path.join(' > '));\n // console.groupEnd();\n return path.join(' > ');\n }\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\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(`#${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 && depth < MAX_DEPTH) {\n // Skip if we've reached the document body or html element\n if (currentElement === document.body || currentElement === document.documentElement) {\n break;\n }\n \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 if (currentElement.assignedSlot) {\n // Handle Shadow DOM\n currentElement = currentElement.assignedSlot;\n } else {\n currentElement = currentElement.parentElement;\n \n // Handle Shadow Root boundaries\n if (currentElement) {\n const rootNode = currentElement.getRootNode();\n if (rootNode instanceof ShadowRoot) {\n currentElement = rootNode.host;\n }\n }\n }\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.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";
|
|
2
|
+
var ApplyAIStatus;
|
|
3
|
+
(function (ApplyAIStatus) {
|
|
4
|
+
ApplyAIStatus["PREPARE_START"] = "PREPARE_START";
|
|
5
|
+
ApplyAIStatus["PREPARE_SUCCESS"] = "PREPARE_SUCCESS";
|
|
6
|
+
ApplyAIStatus["PREPARE_ERROR"] = "PREPARE_ERROR";
|
|
7
|
+
ApplyAIStatus["SEND_START"] = "SEND_START";
|
|
8
|
+
ApplyAIStatus["SEND_SUCCESS"] = "SEND_SUCCESS";
|
|
9
|
+
ApplyAIStatus["SEND_ERROR"] = "SEND_ERROR";
|
|
10
|
+
ApplyAIStatus["APPLY_AI_CANCELLED"] = "APPLY_AI_CANCELLED";
|
|
11
|
+
ApplyAIStatus["SUMMARY_COMPLETED"] = "SUMMARY_COMPLETED";
|
|
12
|
+
ApplyAIStatus["SUMMARY_ERROR"] = "SUMMARY_ERROR";
|
|
13
|
+
})(ApplyAIStatus || (ApplyAIStatus = {}));
|
|
14
|
+
var ReplayStatus;
|
|
15
|
+
(function (ReplayStatus) {
|
|
16
|
+
ReplayStatus["REPLAY_START"] = "REPLAY_START";
|
|
17
|
+
ReplayStatus["REPLAY_SUCCESS"] = "REPLAY_SUCCESS";
|
|
18
|
+
ReplayStatus["REPLAY_ERROR"] = "REPLAY_ERROR";
|
|
19
|
+
ReplayStatus["REPLAY_CANCELLED"] = "REPLAY_CANCELLED";
|
|
20
|
+
})(ReplayStatus || (ReplayStatus = {}));
|
|
21
|
+
|
|
22
|
+
// WebSocketsMessageType enum for WebSocket and event message types shared across the app
|
|
23
|
+
var WebSocketsMessageType;
|
|
24
|
+
(function (WebSocketsMessageType) {
|
|
25
|
+
WebSocketsMessageType["INTERACTION_APPLY_AI_PREPARE_START"] = "INTERACTION_APPLY_AI_PREPARE_START";
|
|
26
|
+
WebSocketsMessageType["INTERACTION_APPLY_AI_PREPARE_SUCCESS"] = "INTERACTION_APPLY_AI_PREPARE_SUCCESS";
|
|
27
|
+
WebSocketsMessageType["INTERACTION_APPLY_AI_PREPARE_ERROR"] = "INTERACTION_APPLY_AI_PREPARE_ERROR";
|
|
28
|
+
WebSocketsMessageType["INTERACTION_APPLY_AI_SEND_TO_LLM_START"] = "INTERACTION_APPLY_AI_SEND_TO_LLM_START";
|
|
29
|
+
WebSocketsMessageType["INTERACTION_APPLY_AI_SEND_TO_LLM_SUCCESS"] = "INTERACTION_APPLY_AI_SEND_TO_LLM_SUCCESS";
|
|
30
|
+
WebSocketsMessageType["INTERACTION_APPLY_AI_SEND_TO_LLM_ERROR"] = "INTERACTION_APPLY_AI_SEND_TO_LLM_ERROR";
|
|
31
|
+
WebSocketsMessageType["INTERACTION_REPLAY_START"] = "INTERACTION_REPLAY_START";
|
|
32
|
+
WebSocketsMessageType["INTERACTION_REPLAY_SUCCESS"] = "INTERACTION_REPLAY_SUCCESS";
|
|
33
|
+
WebSocketsMessageType["INTERACTION_REPLAY_ERROR"] = "INTERACTION_REPLAY_ERROR";
|
|
34
|
+
WebSocketsMessageType["INTERACTION_REPLAY_CANCELLED"] = "INTERACTION_REPLAY_CANCELLED";
|
|
35
|
+
WebSocketsMessageType["INTERACTION_APPLY_AI_CANCELLED"] = "INTERACTION_APPLY_AI_CANCELLED";
|
|
36
|
+
WebSocketsMessageType["INTERACTION_STEP_CREATED"] = "INTERACTION_STEP_CREATED";
|
|
37
|
+
WebSocketsMessageType["INTERACTION_APPLY_AI_SUMMARY_COMPLETED"] = "INTERACTION_APPLY_AI_SUMMARY_COMPLETED";
|
|
38
|
+
WebSocketsMessageType["INTERACTION_APPLY_AI_SUMMARY_ERROR"] = "INTERACTION_APPLY_AI_SUMMARY_ERROR";
|
|
39
|
+
})(WebSocketsMessageType || (WebSocketsMessageType = {}));
|
|
10
40
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (this.depth >= 0) {
|
|
32
|
-
return this.depth;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
this.depth = 0;
|
|
36
|
-
let currentElement = this.element;
|
|
37
|
-
|
|
38
|
-
while (currentElement.nodeType === Node.ELEMENT_NODE) {
|
|
39
|
-
this.depth++;
|
|
40
|
-
if (currentElement.assignedSlot) {
|
|
41
|
-
currentElement = currentElement.assignedSlot;
|
|
42
|
-
}
|
|
43
|
-
else {
|
|
44
|
-
currentElement = currentElement.parentNode;
|
|
45
|
-
// Check if we're at a shadow root
|
|
46
|
-
if (currentElement && currentElement.nodeType !== Node.ELEMENT_NODE && currentElement.getRootNode() instanceof ShadowRoot) {
|
|
47
|
-
// Get the shadow root's host element
|
|
48
|
-
currentElement = currentElement.getRootNode().host;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return this.depth;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
41
|
+
// Action constants
|
|
42
|
+
var PlaywrightAction;
|
|
43
|
+
(function (PlaywrightAction) {
|
|
44
|
+
PlaywrightAction["VISIT_BASE_URL"] = "VISIT_BASE_URL";
|
|
45
|
+
PlaywrightAction["VISIT_URL"] = "VISIT_URL";
|
|
46
|
+
PlaywrightAction["CLICK"] = "CLICK";
|
|
47
|
+
PlaywrightAction["FILL_IN"] = "FILL_IN";
|
|
48
|
+
PlaywrightAction["SELECT_DROPDOWN"] = "SELECT_DROPDOWN";
|
|
49
|
+
PlaywrightAction["SELECT_MULTIPLE_DROPDOWN"] = "SELECT_MULTIPLE_DROPDOWN";
|
|
50
|
+
PlaywrightAction["CHECK_CHECKBOX"] = "CHECK_CHECKBOX";
|
|
51
|
+
PlaywrightAction["SELECT_RADIO"] = "SELECT_RADIO";
|
|
52
|
+
PlaywrightAction["TOGGLE_SWITCH"] = "TOGGLE_SWITCH";
|
|
53
|
+
PlaywrightAction["TYPE_KEYS"] = "TYPE_KEYS";
|
|
54
|
+
PlaywrightAction["HOVER"] = "HOVER";
|
|
55
|
+
PlaywrightAction["VALIDATE_EXACT_VALUE"] = "VALIDATE_EXACT_VALUE";
|
|
56
|
+
PlaywrightAction["VALIDATE_CONTAINS_VALUE"] = "VALIDATE_CONTAINS_VALUE";
|
|
57
|
+
PlaywrightAction["VALIDATE_URL"] = "VALIDATE_URL";
|
|
58
|
+
PlaywrightAction["VALIDATE"] = "VALIDATE";
|
|
59
|
+
PlaywrightAction["SCROLL_TO_ELEMENT"] = "SCROLL_TO_ELEMENT";
|
|
60
|
+
})(PlaywrightAction || (PlaywrightAction = {}));
|
|
56
61
|
|
|
57
62
|
/**
|
|
58
63
|
* Logging levels for Probo
|
|
@@ -65,75 +70,91 @@ var ProboLogLevel;
|
|
|
65
70
|
ProboLogLevel["WARN"] = "WARN";
|
|
66
71
|
ProboLogLevel["ERROR"] = "ERROR";
|
|
67
72
|
})(ProboLogLevel || (ProboLogLevel = {}));
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
[ProboLogLevel.
|
|
71
|
-
[ProboLogLevel.
|
|
72
|
-
[ProboLogLevel.
|
|
73
|
-
[ProboLogLevel.
|
|
74
|
-
[ProboLogLevel.ERROR]: 'error',
|
|
73
|
+
const logLevelOrder = {
|
|
74
|
+
[ProboLogLevel.DEBUG]: 0,
|
|
75
|
+
[ProboLogLevel.INFO]: 1,
|
|
76
|
+
[ProboLogLevel.LOG]: 2,
|
|
77
|
+
[ProboLogLevel.WARN]: 3,
|
|
78
|
+
[ProboLogLevel.ERROR]: 4,
|
|
75
79
|
};
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
transports: []
|
|
81
|
-
});
|
|
82
|
-
/**
|
|
83
|
-
* Configure logging transports and level.
|
|
84
|
-
* @param options.logToConsole - Enable or disable console logging (default: true)
|
|
85
|
-
* @param options.logToFile - Enable or disable file logging (default: true)
|
|
86
|
-
* @param options.level - Logging level (default: INFO)
|
|
87
|
-
* @param options.filePath - Path for file transport (default: '/tmp/probo.log')
|
|
88
|
-
*/
|
|
89
|
-
function configureLogger(options) {
|
|
90
|
-
const { logToConsole = true, logToFile = true, level = ProboLogLevel.INFO, filePath = '/tmp/probo.log', } = options || {};
|
|
91
|
-
// Set global level
|
|
92
|
-
logger.level = levelMap[level];
|
|
93
|
-
// Console transport
|
|
94
|
-
const consoleTransport = logger.transports.find(t => t instanceof winston.transports.Console);
|
|
95
|
-
if (logToConsole) {
|
|
96
|
-
if (!consoleTransport) {
|
|
97
|
-
logger.add(new winston.transports.Console());
|
|
98
|
-
}
|
|
80
|
+
class ProboLogger {
|
|
81
|
+
constructor(prefix, level = ProboLogLevel.INFO) {
|
|
82
|
+
this.prefix = prefix;
|
|
83
|
+
this.level = level;
|
|
99
84
|
}
|
|
100
|
-
|
|
101
|
-
|
|
85
|
+
setLogLevel(level) {
|
|
86
|
+
this.level = level;
|
|
102
87
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (logToFile) {
|
|
106
|
-
if (!fileTransport) {
|
|
107
|
-
logger.add(new winston.transports.File({ filename: filePath }));
|
|
108
|
-
}
|
|
88
|
+
shouldLog(level) {
|
|
89
|
+
return logLevelOrder[level] >= logLevelOrder[this.level];
|
|
109
90
|
}
|
|
110
|
-
|
|
111
|
-
|
|
91
|
+
preamble() {
|
|
92
|
+
const now = new Date();
|
|
93
|
+
const hours = String(now.getHours()).padStart(2, '0');
|
|
94
|
+
const minutes = String(now.getMinutes()).padStart(2, '0');
|
|
95
|
+
const seconds = String(now.getSeconds()).padStart(2, '0');
|
|
96
|
+
return `[${hours}:${minutes}:${seconds}] [${this.prefix}]`;
|
|
112
97
|
}
|
|
98
|
+
debug(...args) { if (this.shouldLog(ProboLogLevel.DEBUG))
|
|
99
|
+
console.debug(this.preamble(), ...args); }
|
|
100
|
+
info(...args) { if (this.shouldLog(ProboLogLevel.INFO))
|
|
101
|
+
console.info(this.preamble(), ...args); }
|
|
102
|
+
log(...args) { if (this.shouldLog(ProboLogLevel.LOG))
|
|
103
|
+
console.log(this.preamble(), ...args); }
|
|
104
|
+
warn(...args) { if (this.shouldLog(ProboLogLevel.WARN))
|
|
105
|
+
console.warn(this.preamble(), ...args); }
|
|
106
|
+
error(...args) { if (this.shouldLog(ProboLogLevel.ERROR))
|
|
107
|
+
console.error(this.preamble(), ...args); }
|
|
113
108
|
}
|
|
114
|
-
// Apply defaults: both console and file, INFO level
|
|
115
|
-
configureLogger();
|
|
116
109
|
/**
|
|
117
|
-
*
|
|
110
|
+
* Interfaces for element information (unchanged)
|
|
118
111
|
*/
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
112
|
+
/* export interface ElementInfo {
|
|
113
|
+
index: string;
|
|
114
|
+
tag: string;
|
|
115
|
+
type: string;
|
|
116
|
+
text: string;
|
|
117
|
+
html: string;
|
|
118
|
+
xpath: string;
|
|
119
|
+
css_selector: string;
|
|
120
|
+
bounding_box: {
|
|
121
|
+
x: number;
|
|
122
|
+
y: number;
|
|
123
|
+
width: number;
|
|
124
|
+
height: number;
|
|
125
|
+
top: number;
|
|
126
|
+
right: number;
|
|
127
|
+
bottom: number;
|
|
128
|
+
left: number;
|
|
129
|
+
};
|
|
130
|
+
iframe_selector: string;
|
|
131
|
+
element: any;
|
|
132
|
+
depth?: number;
|
|
133
|
+
getSelector(): string;
|
|
134
|
+
getDepth(): number;
|
|
134
135
|
}
|
|
135
|
-
|
|
136
|
-
|
|
136
|
+
*/
|
|
137
|
+
/* export interface CleanElementInfo {
|
|
138
|
+
index: string;
|
|
139
|
+
tag: string;
|
|
140
|
+
type: string;
|
|
141
|
+
text: string;
|
|
142
|
+
html: string;
|
|
143
|
+
xpath: string;
|
|
144
|
+
css_selector: string;
|
|
145
|
+
iframe_selector: string;
|
|
146
|
+
bounding_box: {
|
|
147
|
+
x: number;
|
|
148
|
+
y: number;
|
|
149
|
+
width: number;
|
|
150
|
+
height: number;
|
|
151
|
+
top: number;
|
|
152
|
+
right: number;
|
|
153
|
+
bottom: number;
|
|
154
|
+
left: number;
|
|
155
|
+
};
|
|
156
|
+
depth: number;
|
|
157
|
+
} */
|
|
137
158
|
// Element cleaner logging
|
|
138
159
|
const elementLogger = new ProboLogger('element-cleaner');
|
|
139
160
|
/**
|
|
@@ -179,602 +200,8 @@ function cleanupInstructionElements(instruction) {
|
|
|
179
200
|
return cleaned;
|
|
180
201
|
}
|
|
181
202
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
VISIT_BASE_URL: 'VISIT_BASE_URL',
|
|
185
|
-
VISIT_URL: 'VISIT_URL',
|
|
186
|
-
CLICK: 'CLICK',
|
|
187
|
-
FILL_IN: 'FILL_IN',
|
|
188
|
-
SELECT_DROPDOWN: 'SELECT_DROPDOWN',
|
|
189
|
-
SELECT_MULTIPLE_DROPDOWN: 'SELECT_MULTIPLE_DROPDOWN',
|
|
190
|
-
CHECK_CHECKBOX: 'CHECK_CHECKBOX',
|
|
191
|
-
SELECT_RADIO: 'SELECT_RADIO',
|
|
192
|
-
TOGGLE_SWITCH: 'TOGGLE_SWITCH',
|
|
193
|
-
TYPE_KEYS: 'TYPE_KEYS',
|
|
194
|
-
HOVER: 'HOVER',
|
|
195
|
-
VALIDATE_EXACT_VALUE: 'VALIDATE_EXACT_VALUE',
|
|
196
|
-
VALIDATE_CONTAINS_VALUE: 'VALIDATE_CONTAINS_VALUE',
|
|
197
|
-
VALIDATE_URL: 'VALIDATE_URL',
|
|
198
|
-
};
|
|
199
|
-
const SPECIAL_KEYS = [
|
|
200
|
-
'Enter', 'Tab', 'Escape', 'Backspace', 'Delete',
|
|
201
|
-
'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown',
|
|
202
|
-
'Home', 'End', 'PageUp', 'PageDown',
|
|
203
|
-
'Insert', 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12',
|
|
204
|
-
'Shift', 'Control', 'Alt', 'Meta',
|
|
205
|
-
'CapsLock', 'NumLock', 'ScrollLock',
|
|
206
|
-
'Pause', 'PrintScreen', 'ContextMenu',
|
|
207
|
-
'AudioVolumeUp', 'AudioVolumeDown', 'AudioVolumeMute',
|
|
208
|
-
'MediaTrackNext', 'MediaTrackPrevious', 'MediaStop', 'MediaPlayPause',
|
|
209
|
-
'BrowserBack', 'BrowserForward', 'BrowserRefresh', 'BrowserFavorites'
|
|
210
|
-
];
|
|
211
|
-
/**
|
|
212
|
-
* Handle potential navigation exactly like Python's handle_potential_navigation
|
|
213
|
-
*/
|
|
214
|
-
async function handlePotentialNavigation(page, locator = null, value = null, options = {}) {
|
|
215
|
-
const { initialTimeout = 5000, navigationTimeout = 7000, globalTimeout = 15000, } = options;
|
|
216
|
-
const startTime = Date.now();
|
|
217
|
-
let navigationCount = 0;
|
|
218
|
-
let lastNavTime = null;
|
|
219
|
-
const onFrameNav = (frame) => {
|
|
220
|
-
if (frame === page.mainFrame()) {
|
|
221
|
-
navigationCount++;
|
|
222
|
-
lastNavTime = Date.now();
|
|
223
|
-
proboLogger$1.debug(`DEBUG_NAV[${((Date.now() - startTime) / 1000).toFixed(3)}s]: navigation detected (count=${navigationCount})`);
|
|
224
|
-
}
|
|
225
|
-
};
|
|
226
|
-
proboLogger$1.debug(`DEBUG_NAV[0.000s]: Starting navigation detection`);
|
|
227
|
-
page.on("framenavigated", onFrameNav);
|
|
228
|
-
try {
|
|
229
|
-
if (locator) {
|
|
230
|
-
proboLogger$1.debug(`DEBUG_NAV: Executing click(${value})`);
|
|
231
|
-
await clickAtPosition(page, locator, value);
|
|
232
|
-
}
|
|
233
|
-
// wait for any initial nav to fire
|
|
234
|
-
await page.waitForTimeout(initialTimeout);
|
|
235
|
-
proboLogger$1.debug(`DEBUG_NAV[${((Date.now() - startTime) / 1000).toFixed(3)}s]: After initial wait, count=${navigationCount}`);
|
|
236
|
-
if (navigationCount > 0) {
|
|
237
|
-
// loop until either per-nav or global timeout
|
|
238
|
-
while (true) {
|
|
239
|
-
const now = Date.now();
|
|
240
|
-
if (lastNavTime !== null &&
|
|
241
|
-
now - lastNavTime > navigationTimeout) {
|
|
242
|
-
proboLogger$1.debug(`DEBUG_NAV[${((now - startTime) / 1000).toFixed(3)}s]: per‐navigation timeout reached`);
|
|
243
|
-
break;
|
|
244
|
-
}
|
|
245
|
-
if (now - startTime > globalTimeout) {
|
|
246
|
-
proboLogger$1.debug(`DEBUG_NAV[${((now - startTime) / 1000).toFixed(3)}s]: overall timeout reached`);
|
|
247
|
-
break;
|
|
248
|
-
}
|
|
249
|
-
await page.waitForTimeout(500);
|
|
250
|
-
}
|
|
251
|
-
// now wait for load + idle
|
|
252
|
-
proboLogger$1.debug(`DEBUG_NAV: waiting for load state`);
|
|
253
|
-
await page.waitForLoadState("load", { timeout: globalTimeout });
|
|
254
|
-
proboLogger$1.debug(`DEBUG_NAV: waiting for networkidle`);
|
|
255
|
-
try {
|
|
256
|
-
// shorter idle‐wait so we don't hang the full globalTimeout here
|
|
257
|
-
await page.waitForLoadState("networkidle", {
|
|
258
|
-
timeout: navigationTimeout,
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
catch (_a) {
|
|
262
|
-
proboLogger$1.debug(`DEBUG_NAV: networkidle not reached in ${navigationTimeout}ms, proceeding anyway`);
|
|
263
|
-
}
|
|
264
|
-
await scrollToBottomRight(page);
|
|
265
|
-
proboLogger$1.debug(`DEBUG_NAV: done`);
|
|
266
|
-
return true;
|
|
267
|
-
}
|
|
268
|
-
proboLogger$1.debug(`DEBUG_NAV: no navigation detected`);
|
|
269
|
-
return false;
|
|
270
|
-
}
|
|
271
|
-
finally {
|
|
272
|
-
page.removeListener("framenavigated", onFrameNav);
|
|
273
|
-
proboLogger$1.debug(`DEBUG_NAV: listener removed`);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
/**
|
|
277
|
-
* Scroll entire page to bottom-right, triggering lazy-loaded content
|
|
278
|
-
*/
|
|
279
|
-
async function scrollToBottomRight(page) {
|
|
280
|
-
const startTime = performance.now();
|
|
281
|
-
proboLogger$1.debug(`Starting scroll to bottom-right`);
|
|
282
|
-
let lastHeight = await page.evaluate(() => document.documentElement.scrollHeight);
|
|
283
|
-
let lastWidth = await page.evaluate(() => document.documentElement.scrollWidth);
|
|
284
|
-
let smoothingSteps = 0;
|
|
285
|
-
while (true) {
|
|
286
|
-
const initY = await page.evaluate(() => window.scrollY);
|
|
287
|
-
const initX = await page.evaluate(() => window.scrollX);
|
|
288
|
-
const clientHeight = await page.evaluate(() => document.documentElement.clientHeight);
|
|
289
|
-
const maxHeight = await page.evaluate(() => document.documentElement.scrollHeight);
|
|
290
|
-
const clientWidth = await page.evaluate(() => document.documentElement.clientWidth);
|
|
291
|
-
const maxWidth = await page.evaluate(() => document.documentElement.scrollWidth);
|
|
292
|
-
let currX = initX;
|
|
293
|
-
while (currX < maxWidth - clientWidth) {
|
|
294
|
-
let currY = initY;
|
|
295
|
-
while (currY < maxHeight - clientHeight) {
|
|
296
|
-
currY += clientHeight;
|
|
297
|
-
await page.evaluate(([x, y]) => window.scrollTo(x, y), [currX, currY]);
|
|
298
|
-
await page.waitForTimeout(50);
|
|
299
|
-
smoothingSteps++;
|
|
300
|
-
}
|
|
301
|
-
currX += clientWidth;
|
|
302
|
-
}
|
|
303
|
-
proboLogger$1.debug(`performed ${smoothingSteps} smoothing steps while scrolling`);
|
|
304
|
-
const newHeight = await page.evaluate(() => document.documentElement.scrollHeight);
|
|
305
|
-
const newWidth = await page.evaluate(() => document.documentElement.scrollWidth);
|
|
306
|
-
if (newHeight === lastHeight && newWidth === lastWidth)
|
|
307
|
-
break;
|
|
308
|
-
proboLogger$1.debug(`page dimensions updated, repeating scroll`);
|
|
309
|
-
lastHeight = newHeight;
|
|
310
|
-
lastWidth = newWidth;
|
|
311
|
-
}
|
|
312
|
-
if (smoothingSteps > 0) {
|
|
313
|
-
await page.waitForTimeout(200);
|
|
314
|
-
await page.evaluate('window.scrollTo(0, 0)');
|
|
315
|
-
await page.waitForTimeout(50);
|
|
316
|
-
}
|
|
317
|
-
proboLogger$1.debug(`Scroll completed in ${(performance.now() - startTime).toFixed(3)}ms`);
|
|
318
|
-
}
|
|
319
|
-
/**
|
|
320
|
-
* Wait for DOM mutations to settle using MutationObserver logic
|
|
321
|
-
*/
|
|
322
|
-
async function waitForMutationsToSettle(page, mutationTimeout = 1500, initTimeout = 2000) {
|
|
323
|
-
const startTime = Date.now();
|
|
324
|
-
proboLogger$1.debug(`Starting mutation settlement (initTimeout=${initTimeout}, mutationTimeout=${mutationTimeout})`);
|
|
325
|
-
const result = await page.evaluate(async ({ mutationTimeout, initTimeout }) => {
|
|
326
|
-
async function blockUntilStable(targetNode, options = { childList: true, subtree: true }) {
|
|
327
|
-
return new Promise((resolve) => {
|
|
328
|
-
let initTimerExpired = false;
|
|
329
|
-
let mutationsOccurred = false;
|
|
330
|
-
const observerTimers = new Set();
|
|
331
|
-
window.setTimeout(() => {
|
|
332
|
-
initTimerExpired = true;
|
|
333
|
-
if (observerTimers.size === 0)
|
|
334
|
-
resolve(mutationsOccurred);
|
|
335
|
-
}, initTimeout);
|
|
336
|
-
const observer = new MutationObserver(() => {
|
|
337
|
-
mutationsOccurred = true;
|
|
338
|
-
observerTimers.forEach((t) => clearTimeout(t));
|
|
339
|
-
observerTimers.clear();
|
|
340
|
-
const t = window.setTimeout(() => {
|
|
341
|
-
observerTimers.delete(t);
|
|
342
|
-
if (initTimerExpired && observerTimers.size === 0) {
|
|
343
|
-
observer.disconnect();
|
|
344
|
-
resolve(mutationsOccurred);
|
|
345
|
-
}
|
|
346
|
-
}, mutationTimeout);
|
|
347
|
-
observerTimers.add(t);
|
|
348
|
-
});
|
|
349
|
-
observer.observe(targetNode, options);
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
return blockUntilStable(document.body);
|
|
353
|
-
}, { mutationTimeout, initTimeout });
|
|
354
|
-
const total = ((Date.now() - startTime) / 1000).toFixed(2);
|
|
355
|
-
proboLogger$1.debug(result
|
|
356
|
-
? `Mutations settled. Took ${total}s`
|
|
357
|
-
: `No mutations observed. Took ${total}s`);
|
|
358
|
-
return result;
|
|
359
|
-
}
|
|
360
|
-
/**
|
|
361
|
-
* Get element text value: allTextContents + innerText fallback
|
|
362
|
-
*/
|
|
363
|
-
async function getElementValue(page, locator) {
|
|
364
|
-
const texts = await locator.allTextContents();
|
|
365
|
-
let allText = texts.join('').trim();
|
|
366
|
-
if (!allText) {
|
|
367
|
-
allText = await locator.evaluate((el) => el.innerText);
|
|
368
|
-
}
|
|
369
|
-
proboLogger$1.debug(`getElementValue: [${allText}]`);
|
|
370
|
-
return allText;
|
|
371
|
-
}
|
|
372
|
-
/**
|
|
373
|
-
* Select dropdown option: native <select>, <option>, child select, or ARIA listbox
|
|
374
|
-
*/
|
|
375
|
-
async function selectDropdownOption(page, locator, value) {
|
|
376
|
-
const tagName = await (locator === null || locator === void 0 ? void 0 : locator.evaluate((el) => el.tagName.toLowerCase()));
|
|
377
|
-
const role = await (locator === null || locator === void 0 ? void 0 : locator.getAttribute('role'));
|
|
378
|
-
if (tagName === 'option' || role === 'option') {
|
|
379
|
-
proboLogger$1.debug('selectDropdownOption: option role detected');
|
|
380
|
-
await (locator === null || locator === void 0 ? void 0 : locator.click());
|
|
381
|
-
}
|
|
382
|
-
else if (tagName === 'select') {
|
|
383
|
-
proboLogger$1.debug('selectDropdownOption: simple select tag detected');
|
|
384
|
-
try {
|
|
385
|
-
await (locator === null || locator === void 0 ? void 0 : locator.selectOption(value));
|
|
386
|
-
}
|
|
387
|
-
catch (_a) {
|
|
388
|
-
proboLogger$1.debug('selectDropdownOption: manual change event fallback');
|
|
389
|
-
const handle = locator ? await locator.elementHandle() : null;
|
|
390
|
-
await page.evaluate(({ h, val }) => {
|
|
391
|
-
const el = h;
|
|
392
|
-
if (el) {
|
|
393
|
-
el.value = Array.isArray(val) ? val[0] : val;
|
|
394
|
-
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
395
|
-
}
|
|
396
|
-
}, { h: handle, val: value });
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
else {
|
|
400
|
-
proboLogger$1.debug('selectDropdownOption: custom dropdown path');
|
|
401
|
-
let listbox = locator.locator('select');
|
|
402
|
-
let count = await listbox.count();
|
|
403
|
-
if (count > 1)
|
|
404
|
-
throw new Error(`selectDropdownOption: ambiguous <select> count=${count}`);
|
|
405
|
-
if (count === 1) {
|
|
406
|
-
proboLogger$1.debug('selectDropdownOption: child <select> found');
|
|
407
|
-
await listbox.selectOption(value);
|
|
408
|
-
return;
|
|
409
|
-
}
|
|
410
|
-
await locator.click();
|
|
411
|
-
let container = locator;
|
|
412
|
-
count = 0;
|
|
413
|
-
if (role !== 'listbox') {
|
|
414
|
-
for (let i = 0; i < 7; i++) {
|
|
415
|
-
listbox = container.getByRole('listbox');
|
|
416
|
-
count = await listbox.count();
|
|
417
|
-
if (count >= 1)
|
|
418
|
-
break;
|
|
419
|
-
proboLogger$1.debug(`selectDropdownOption: iteration #${i} no listbox found`);
|
|
420
|
-
container = container.locator('xpath=..');
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
else {
|
|
424
|
-
listbox = container;
|
|
425
|
-
count = await listbox.count();
|
|
426
|
-
}
|
|
427
|
-
if (count !== 1)
|
|
428
|
-
throw new Error(`selectDropdownOption: found ${count} listbox locators`);
|
|
429
|
-
const vals = Array.isArray(value) ? value : [value];
|
|
430
|
-
for (const val of vals) {
|
|
431
|
-
const option = listbox.getByRole('option').getByText(new RegExp(`^${val}$`, 'i'));
|
|
432
|
-
const optCount = await option.count();
|
|
433
|
-
if (optCount !== 1)
|
|
434
|
-
throw new Error(`selectDropdownOption: ${optCount} options for '${val}'`);
|
|
435
|
-
await option.click();
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
/**
|
|
440
|
-
* Execute a given Playwright action, mirroring Python's _perform_action
|
|
441
|
-
*/
|
|
442
|
-
async function executePlaywrightAction(page, action, value, iframe_selector, element_css_selector) {
|
|
443
|
-
proboLogger$1.info(`performing Action: ${action} Value: ${value}`);
|
|
444
|
-
try {
|
|
445
|
-
if (action === PlaywrightAction.VISIT_BASE_URL || action === PlaywrightAction.VISIT_URL) {
|
|
446
|
-
await page.goto(value, { waitUntil: 'load' });
|
|
447
|
-
await handlePotentialNavigation(page, null);
|
|
448
|
-
}
|
|
449
|
-
else {
|
|
450
|
-
let locator = undefined;
|
|
451
|
-
if (iframe_selector)
|
|
452
|
-
locator = page.frameLocator(iframe_selector).locator(element_css_selector);
|
|
453
|
-
else
|
|
454
|
-
locator = page.locator(element_css_selector);
|
|
455
|
-
switch (action) {
|
|
456
|
-
case PlaywrightAction.CLICK:
|
|
457
|
-
await handlePotentialNavigation(page, locator, value);
|
|
458
|
-
break;
|
|
459
|
-
case PlaywrightAction.FILL_IN:
|
|
460
|
-
// await locator.click();
|
|
461
|
-
await locator.fill(value);
|
|
462
|
-
break;
|
|
463
|
-
case PlaywrightAction.TYPE_KEYS:
|
|
464
|
-
// Check if the value contains any special keys
|
|
465
|
-
const specialKey = SPECIAL_KEYS.find(key => value.includes(key));
|
|
466
|
-
if (specialKey) {
|
|
467
|
-
// If it's a single special key, just press it
|
|
468
|
-
if (value === specialKey) {
|
|
469
|
-
await (locator === null || locator === void 0 ? void 0 : locator.press(specialKey));
|
|
470
|
-
}
|
|
471
|
-
else {
|
|
472
|
-
// Handle combinations like 'Control+A' or 'Shift+ArrowRight'
|
|
473
|
-
const parts = value.split('+');
|
|
474
|
-
if (parts.length === 2 && SPECIAL_KEYS.includes(parts[0]) && SPECIAL_KEYS.includes(parts[1])) {
|
|
475
|
-
await (locator === null || locator === void 0 ? void 0 : locator.press(value));
|
|
476
|
-
}
|
|
477
|
-
else {
|
|
478
|
-
// If it's a mix of special keys and text, use pressSequentially
|
|
479
|
-
await (locator === null || locator === void 0 ? void 0 : locator.pressSequentially(value));
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
else {
|
|
484
|
-
// No special keys, just type normally
|
|
485
|
-
await (locator === null || locator === void 0 ? void 0 : locator.pressSequentially(value));
|
|
486
|
-
}
|
|
487
|
-
break;
|
|
488
|
-
case PlaywrightAction.SELECT_DROPDOWN:
|
|
489
|
-
await selectDropdownOption(page, locator, value);
|
|
490
|
-
break;
|
|
491
|
-
case PlaywrightAction.SELECT_MULTIPLE_DROPDOWN:
|
|
492
|
-
let optsArr;
|
|
493
|
-
if (value.startsWith('[')) {
|
|
494
|
-
try {
|
|
495
|
-
optsArr = JSON.parse(value);
|
|
496
|
-
}
|
|
497
|
-
catch (_a) {
|
|
498
|
-
optsArr = value.slice(1, -1).split(',').map(o => o.trim());
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
else {
|
|
502
|
-
optsArr = value.split(',').map(o => o.trim());
|
|
503
|
-
}
|
|
504
|
-
await (locator === null || locator === void 0 ? void 0 : locator.selectOption(optsArr));
|
|
505
|
-
break;
|
|
506
|
-
case PlaywrightAction.CHECK_CHECKBOX:
|
|
507
|
-
await (locator === null || locator === void 0 ? void 0 : locator.setChecked(value.toLowerCase() === 'true'));
|
|
508
|
-
break;
|
|
509
|
-
case PlaywrightAction.SELECT_RADIO:
|
|
510
|
-
case PlaywrightAction.TOGGLE_SWITCH:
|
|
511
|
-
await (locator === null || locator === void 0 ? void 0 : locator.click());
|
|
512
|
-
break;
|
|
513
|
-
case PlaywrightAction.HOVER:
|
|
514
|
-
await (locator === null || locator === void 0 ? void 0 : locator.hover({ noWaitAfter: false }));
|
|
515
|
-
await page.waitForTimeout(100); // short delay for hover to take effect
|
|
516
|
-
break;
|
|
517
|
-
case PlaywrightAction.VALIDATE_EXACT_VALUE:
|
|
518
|
-
const actualExact = await getElementValue(page, locator);
|
|
519
|
-
proboLogger$1.debug(`actual value is [${actualExact}]`);
|
|
520
|
-
if (actualExact !== value) {
|
|
521
|
-
proboLogger$1.info(`Validation *FAIL* expected '${value}' but got '${actualExact}'`);
|
|
522
|
-
return false;
|
|
523
|
-
}
|
|
524
|
-
proboLogger$1.info('Validation *PASS*');
|
|
525
|
-
break;
|
|
526
|
-
case PlaywrightAction.VALIDATE_CONTAINS_VALUE:
|
|
527
|
-
const actualContains = await getElementValue(page, locator);
|
|
528
|
-
proboLogger$1.debug(`actual value is [${actualContains}]`);
|
|
529
|
-
if (!actualContains.includes(value)) {
|
|
530
|
-
proboLogger$1.info(`Validation *FAIL* expected '${value}' to be contained in '${actualContains}'`);
|
|
531
|
-
return false;
|
|
532
|
-
}
|
|
533
|
-
proboLogger$1.info('Validation *PASS*');
|
|
534
|
-
break;
|
|
535
|
-
case PlaywrightAction.VALIDATE_URL:
|
|
536
|
-
const currUrl = page.url();
|
|
537
|
-
if (currUrl !== value) {
|
|
538
|
-
proboLogger$1.info(`Validation *FAIL* expected url '${value}' while is '${currUrl}'`);
|
|
539
|
-
return false;
|
|
540
|
-
}
|
|
541
|
-
proboLogger$1.info('Validation *PASS*');
|
|
542
|
-
break;
|
|
543
|
-
default:
|
|
544
|
-
throw new Error(`Unknown action: ${action}`);
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
return true;
|
|
548
|
-
}
|
|
549
|
-
catch (e) {
|
|
550
|
-
proboLogger$1.debug(`***ERROR failed to execute action ${action}: ${e}`);
|
|
551
|
-
throw e;
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
/**
|
|
555
|
-
* Execute a given Playwright action using native Playwright functions where possible
|
|
556
|
-
*/
|
|
557
|
-
async function executeCachedPlaywrightAction(page, action, value, iframe_selector, element_css_selector) {
|
|
558
|
-
proboLogger$1.log(`performing Cached Action: ${action} Value: ${value} on iframe: ${iframe_selector} locator: ${element_css_selector}`);
|
|
559
|
-
try {
|
|
560
|
-
let locator = undefined;
|
|
561
|
-
if (iframe_selector)
|
|
562
|
-
locator = page.frameLocator(iframe_selector).locator(element_css_selector);
|
|
563
|
-
else
|
|
564
|
-
locator = page.locator(element_css_selector);
|
|
565
|
-
switch (action) {
|
|
566
|
-
case PlaywrightAction.VISIT_BASE_URL:
|
|
567
|
-
case PlaywrightAction.VISIT_URL:
|
|
568
|
-
await page.goto(value, { waitUntil: 'networkidle' });
|
|
569
|
-
break;
|
|
570
|
-
case PlaywrightAction.CLICK:
|
|
571
|
-
await clickAtPosition(page, locator, value);
|
|
572
|
-
await handlePotentialNavigation(page);
|
|
573
|
-
break;
|
|
574
|
-
case PlaywrightAction.FILL_IN:
|
|
575
|
-
// await locator.click();
|
|
576
|
-
await locator.fill(value);
|
|
577
|
-
break;
|
|
578
|
-
case PlaywrightAction.TYPE_KEYS:
|
|
579
|
-
// Check if the value contains any special keys
|
|
580
|
-
const specialKey = SPECIAL_KEYS.find(key => value.includes(key));
|
|
581
|
-
if (specialKey) {
|
|
582
|
-
// If it's a single special key, just press it
|
|
583
|
-
if (value === specialKey) {
|
|
584
|
-
await locator.press(specialKey);
|
|
585
|
-
}
|
|
586
|
-
else {
|
|
587
|
-
// Handle combinations like 'Control+A' or 'Shift+ArrowRight'
|
|
588
|
-
const parts = value.split('+');
|
|
589
|
-
if (parts.length === 2 && SPECIAL_KEYS.includes(parts[0]) && SPECIAL_KEYS.includes(parts[1])) {
|
|
590
|
-
await locator.press(value);
|
|
591
|
-
}
|
|
592
|
-
else {
|
|
593
|
-
// If it's a mix of special keys and text, use pressSequentially
|
|
594
|
-
await locator.pressSequentially(value);
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
else {
|
|
599
|
-
// No special keys, just type normally
|
|
600
|
-
await locator.pressSequentially(value);
|
|
601
|
-
}
|
|
602
|
-
break;
|
|
603
|
-
case PlaywrightAction.SELECT_DROPDOWN:
|
|
604
|
-
await locator.selectOption(value);
|
|
605
|
-
break;
|
|
606
|
-
case PlaywrightAction.SELECT_MULTIPLE_DROPDOWN:
|
|
607
|
-
let optsArr;
|
|
608
|
-
if (value.startsWith('[')) {
|
|
609
|
-
try {
|
|
610
|
-
optsArr = JSON.parse(value);
|
|
611
|
-
}
|
|
612
|
-
catch (_a) {
|
|
613
|
-
optsArr = value.slice(1, -1).split(',').map(o => o.trim());
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
else {
|
|
617
|
-
optsArr = value.split(',').map(o => o.trim());
|
|
618
|
-
}
|
|
619
|
-
await locator.selectOption(optsArr);
|
|
620
|
-
break;
|
|
621
|
-
case PlaywrightAction.CHECK_CHECKBOX:
|
|
622
|
-
await locator.setChecked(value.toLowerCase() === 'true');
|
|
623
|
-
break;
|
|
624
|
-
case PlaywrightAction.SELECT_RADIO:
|
|
625
|
-
case PlaywrightAction.TOGGLE_SWITCH:
|
|
626
|
-
await locator.click();
|
|
627
|
-
break;
|
|
628
|
-
case PlaywrightAction.HOVER:
|
|
629
|
-
await (locator === null || locator === void 0 ? void 0 : locator.hover({ noWaitAfter: false }));
|
|
630
|
-
await page.waitForTimeout(100); // short delay for hover to take effect
|
|
631
|
-
break;
|
|
632
|
-
case PlaywrightAction.VALIDATE_EXACT_VALUE:
|
|
633
|
-
const actualExact = await getElementValue(page, locator);
|
|
634
|
-
proboLogger$1.debug(`actual value is [${actualExact}]`);
|
|
635
|
-
if (actualExact !== value) {
|
|
636
|
-
proboLogger$1.info(`Validation *FAIL* expected '${value}' but got '${actualExact}'`);
|
|
637
|
-
return false;
|
|
638
|
-
}
|
|
639
|
-
proboLogger$1.info('Validation *PASS*');
|
|
640
|
-
break;
|
|
641
|
-
case PlaywrightAction.VALIDATE_CONTAINS_VALUE:
|
|
642
|
-
const actualContains = await getElementValue(page, locator);
|
|
643
|
-
proboLogger$1.debug(`actual value is [${actualContains}]`);
|
|
644
|
-
if (!actualContains.includes(value)) {
|
|
645
|
-
proboLogger$1.info(`Validation *FAIL* expected '${value}' to be contained in '${actualContains}'`);
|
|
646
|
-
return false;
|
|
647
|
-
}
|
|
648
|
-
proboLogger$1.info('Validation *PASS*');
|
|
649
|
-
break;
|
|
650
|
-
case PlaywrightAction.VALIDATE_URL:
|
|
651
|
-
const currUrl = page.url();
|
|
652
|
-
if (currUrl !== value) {
|
|
653
|
-
proboLogger$1.info(`Validation *FAIL* expected url '${value}' while is '${currUrl}'`);
|
|
654
|
-
return false;
|
|
655
|
-
}
|
|
656
|
-
proboLogger$1.info('Validation *PASS*');
|
|
657
|
-
break;
|
|
658
|
-
default:
|
|
659
|
-
throw new Error(`Unknown action: ${action}`);
|
|
660
|
-
}
|
|
661
|
-
return true;
|
|
662
|
-
}
|
|
663
|
-
catch (e) {
|
|
664
|
-
proboLogger$1.debug(`***ERROR failed to execute cached action ${action}: ${e}`);
|
|
665
|
-
throw e;
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
var ClickPosition;
|
|
669
|
-
(function (ClickPosition) {
|
|
670
|
-
ClickPosition["LEFT"] = "LEFT";
|
|
671
|
-
ClickPosition["RIGHT"] = "RIGHT";
|
|
672
|
-
ClickPosition["CENTER"] = "CENTER";
|
|
673
|
-
})(ClickPosition || (ClickPosition = {}));
|
|
674
|
-
/**
|
|
675
|
-
* Click on an element at a specific position
|
|
676
|
-
*/
|
|
677
|
-
async function clickAtPosition(page, locator, clickPosition) {
|
|
678
|
-
if (!clickPosition || clickPosition === ClickPosition.CENTER) {
|
|
679
|
-
await locator.click({ noWaitAfter: false });
|
|
680
|
-
return;
|
|
681
|
-
}
|
|
682
|
-
const boundingBox = await locator.boundingBox();
|
|
683
|
-
if (boundingBox) {
|
|
684
|
-
if (clickPosition === ClickPosition.LEFT) {
|
|
685
|
-
await page.mouse.click(boundingBox.x + 10, boundingBox.y + boundingBox.height / 2);
|
|
686
|
-
}
|
|
687
|
-
else if (clickPosition === ClickPosition.RIGHT) {
|
|
688
|
-
await page.mouse.click(boundingBox.x + boundingBox.width - 10, boundingBox.y + boundingBox.height / 2);
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
class Highlighter {
|
|
694
|
-
constructor(enableConsoleLogs = true) {
|
|
695
|
-
this.enableConsoleLogs = enableConsoleLogs;
|
|
696
|
-
}
|
|
697
|
-
async ensureHighlighterScript(page, maxRetries = 3) {
|
|
698
|
-
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
699
|
-
try {
|
|
700
|
-
const scriptExists = await page.evaluate(`typeof window.ProboLabs?.highlight?.execute === 'function'`);
|
|
701
|
-
if (!scriptExists) {
|
|
702
|
-
proboLogger$1.debug('Injecting highlighter script...');
|
|
703
|
-
await page.evaluate(highlighterCode);
|
|
704
|
-
// Verify the script was injected correctly
|
|
705
|
-
const verified = await page.evaluate(`
|
|
706
|
-
//console.log('ProboLabs global:', window.ProboLabs);
|
|
707
|
-
typeof window.ProboLabs?.highlight?.execute === 'function'
|
|
708
|
-
`);
|
|
709
|
-
proboLogger$1.debug('Script injection verified:', verified);
|
|
710
|
-
}
|
|
711
|
-
return; // Success - exit the function
|
|
712
|
-
}
|
|
713
|
-
catch (error) {
|
|
714
|
-
if (attempt === maxRetries - 1) {
|
|
715
|
-
throw error;
|
|
716
|
-
}
|
|
717
|
-
proboLogger$1.debug(`Script injection attempt ${attempt + 1} failed, retrying after delay...`);
|
|
718
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
async highlightElements(page, elementTags) {
|
|
723
|
-
proboLogger$1.debug('highlightElements called with:', elementTags);
|
|
724
|
-
await this.ensureHighlighterScript(page);
|
|
725
|
-
// Execute the highlight function and await its result
|
|
726
|
-
const result = await page.evaluate(async (tags) => {
|
|
727
|
-
var _a, _b;
|
|
728
|
-
//proboLogger.debug('Browser: Starting highlight execution with tag:', tag);
|
|
729
|
-
if (!((_b = (_a = window.ProboLabs) === null || _a === void 0 ? void 0 : _a.highlight) === null || _b === void 0 ? void 0 : _b.execute)) {
|
|
730
|
-
console.error('Browser: ProboLabs.highlight.execute is not available!');
|
|
731
|
-
return null;
|
|
732
|
-
}
|
|
733
|
-
const elements = await window.ProboLabs.highlight.execute(tags);
|
|
734
|
-
//proboLogger.debug('Browser: Found elements:', elements);
|
|
735
|
-
return elements;
|
|
736
|
-
}, elementTags);
|
|
737
|
-
// proboLogger.debug("highlighted elements =>");
|
|
738
|
-
// for (let i = 0; i < result.length; i++) {
|
|
739
|
-
// proboLogger.debug(result[i]);
|
|
740
|
-
// };
|
|
741
|
-
return result;
|
|
742
|
-
}
|
|
743
|
-
async unhighlightElements(page) {
|
|
744
|
-
proboLogger$1.debug('unhighlightElements called');
|
|
745
|
-
await this.ensureHighlighterScript(page);
|
|
746
|
-
await page.evaluate(() => {
|
|
747
|
-
var _a, _b;
|
|
748
|
-
(_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();
|
|
749
|
-
});
|
|
750
|
-
}
|
|
751
|
-
async highlightElement(page, element_css_selector, iframe_selector, element_index) {
|
|
752
|
-
await this.ensureHighlighterScript(page);
|
|
753
|
-
proboLogger$1.debug('Highlighting element with:', { element_css_selector, iframe_selector, element_index });
|
|
754
|
-
await page.evaluate(({ css_selector, iframe_selector, index }) => {
|
|
755
|
-
const proboLabs = window.ProboLabs;
|
|
756
|
-
if (!proboLabs) {
|
|
757
|
-
proboLogger$1.warn('ProboLabs not initialized');
|
|
758
|
-
return;
|
|
759
|
-
}
|
|
760
|
-
// Create ElementInfo object for the element
|
|
761
|
-
const elementInfo = {
|
|
762
|
-
css_selector: css_selector,
|
|
763
|
-
iframe_selector: iframe_selector,
|
|
764
|
-
index: index
|
|
765
|
-
};
|
|
766
|
-
// Call highlightElements directly
|
|
767
|
-
proboLabs.highlightElements([elementInfo]);
|
|
768
|
-
}, {
|
|
769
|
-
css_selector: element_css_selector,
|
|
770
|
-
iframe_selector: iframe_selector,
|
|
771
|
-
index: element_index
|
|
772
|
-
});
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
function getDefaultExportFromCjs (x) {
|
|
777
|
-
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
|
|
203
|
+
function getDefaultExportFromCjs (x) {
|
|
204
|
+
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
|
|
778
205
|
}
|
|
779
206
|
|
|
780
207
|
var retry$2 = {};
|
|
@@ -1039,336 +466,1120 @@ function requireRetry$1 () {
|
|
|
1039
466
|
var args = Array.prototype.slice.call(arguments, 1);
|
|
1040
467
|
var callback = args.pop();
|
|
1041
468
|
|
|
1042
|
-
args.push(function(err) {
|
|
1043
|
-
if (op.retry(err)) {
|
|
1044
|
-
return;
|
|
1045
|
-
}
|
|
1046
|
-
if (err) {
|
|
1047
|
-
arguments[0] = op.mainError();
|
|
1048
|
-
}
|
|
1049
|
-
callback.apply(this, arguments);
|
|
1050
|
-
});
|
|
469
|
+
args.push(function(err) {
|
|
470
|
+
if (op.retry(err)) {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
if (err) {
|
|
474
|
+
arguments[0] = op.mainError();
|
|
475
|
+
}
|
|
476
|
+
callback.apply(this, arguments);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
op.attempt(function() {
|
|
480
|
+
original.apply(obj, args);
|
|
481
|
+
});
|
|
482
|
+
}.bind(obj, original);
|
|
483
|
+
obj[method].options = options;
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
} (retry$2));
|
|
487
|
+
return retry$2;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
var retry$1;
|
|
491
|
+
var hasRequiredRetry;
|
|
492
|
+
|
|
493
|
+
function requireRetry () {
|
|
494
|
+
if (hasRequiredRetry) return retry$1;
|
|
495
|
+
hasRequiredRetry = 1;
|
|
496
|
+
retry$1 = requireRetry$1();
|
|
497
|
+
return retry$1;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
var retryExports = requireRetry();
|
|
501
|
+
var retry = /*@__PURE__*/getDefaultExportFromCjs(retryExports);
|
|
502
|
+
|
|
503
|
+
const objectToString = Object.prototype.toString;
|
|
504
|
+
|
|
505
|
+
const isError = value => objectToString.call(value) === '[object Error]';
|
|
506
|
+
|
|
507
|
+
const errorMessages = new Set([
|
|
508
|
+
'network error', // Chrome
|
|
509
|
+
'Failed to fetch', // Chrome
|
|
510
|
+
'NetworkError when attempting to fetch resource.', // Firefox
|
|
511
|
+
'The Internet connection appears to be offline.', // Safari 16
|
|
512
|
+
'Load failed', // Safari 17+
|
|
513
|
+
'Network request failed', // `cross-fetch`
|
|
514
|
+
'fetch failed', // Undici (Node.js)
|
|
515
|
+
'terminated', // Undici (Node.js)
|
|
516
|
+
]);
|
|
517
|
+
|
|
518
|
+
function isNetworkError(error) {
|
|
519
|
+
const isValid = error
|
|
520
|
+
&& isError(error)
|
|
521
|
+
&& error.name === 'TypeError'
|
|
522
|
+
&& typeof error.message === 'string';
|
|
523
|
+
|
|
524
|
+
if (!isValid) {
|
|
525
|
+
return false;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// We do an extra check for Safari 17+ as it has a very generic error message.
|
|
529
|
+
// Network errors in Safari have no stack.
|
|
530
|
+
if (error.message === 'Load failed') {
|
|
531
|
+
return error.stack === undefined;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return errorMessages.has(error.message);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
class AbortError extends Error {
|
|
538
|
+
constructor(message) {
|
|
539
|
+
super();
|
|
540
|
+
|
|
541
|
+
if (message instanceof Error) {
|
|
542
|
+
this.originalError = message;
|
|
543
|
+
({message} = message);
|
|
544
|
+
} else {
|
|
545
|
+
this.originalError = new Error(message);
|
|
546
|
+
this.originalError.stack = this.stack;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
this.name = 'AbortError';
|
|
550
|
+
this.message = message;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const decorateErrorWithCounts = (error, attemptNumber, options) => {
|
|
555
|
+
// Minus 1 from attemptNumber because the first attempt does not count as a retry
|
|
556
|
+
const retriesLeft = options.retries - (attemptNumber - 1);
|
|
557
|
+
|
|
558
|
+
error.attemptNumber = attemptNumber;
|
|
559
|
+
error.retriesLeft = retriesLeft;
|
|
560
|
+
return error;
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
async function pRetry(input, options) {
|
|
564
|
+
return new Promise((resolve, reject) => {
|
|
565
|
+
options = {...options};
|
|
566
|
+
options.onFailedAttempt ??= () => {};
|
|
567
|
+
options.shouldRetry ??= () => true;
|
|
568
|
+
options.retries ??= 10;
|
|
569
|
+
|
|
570
|
+
const operation = retry.operation(options);
|
|
571
|
+
|
|
572
|
+
const abortHandler = () => {
|
|
573
|
+
operation.stop();
|
|
574
|
+
reject(options.signal?.reason);
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
if (options.signal && !options.signal.aborted) {
|
|
578
|
+
options.signal.addEventListener('abort', abortHandler, {once: true});
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const cleanUp = () => {
|
|
582
|
+
options.signal?.removeEventListener('abort', abortHandler);
|
|
583
|
+
operation.stop();
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
operation.attempt(async attemptNumber => {
|
|
587
|
+
try {
|
|
588
|
+
const result = await input(attemptNumber);
|
|
589
|
+
cleanUp();
|
|
590
|
+
resolve(result);
|
|
591
|
+
} catch (error) {
|
|
592
|
+
try {
|
|
593
|
+
if (!(error instanceof Error)) {
|
|
594
|
+
throw new TypeError(`Non-error was thrown: "${error}". You should only throw errors.`);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (error instanceof AbortError) {
|
|
598
|
+
throw error.originalError;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (error instanceof TypeError && !isNetworkError(error)) {
|
|
602
|
+
throw error;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
decorateErrorWithCounts(error, attemptNumber, options);
|
|
606
|
+
|
|
607
|
+
if (!(await options.shouldRetry(error))) {
|
|
608
|
+
operation.stop();
|
|
609
|
+
reject(error);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
await options.onFailedAttempt(error);
|
|
613
|
+
|
|
614
|
+
if (!operation.retry(error)) {
|
|
615
|
+
throw operation.mainError();
|
|
616
|
+
}
|
|
617
|
+
} catch (finalError) {
|
|
618
|
+
decorateErrorWithCounts(finalError, attemptNumber, options);
|
|
619
|
+
cleanUp();
|
|
620
|
+
reject(finalError);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const apiLogger = new ProboLogger('apiclient');
|
|
628
|
+
class ApiError extends Error {
|
|
629
|
+
constructor(status, message, data) {
|
|
630
|
+
super(message);
|
|
631
|
+
this.status = status;
|
|
632
|
+
this.data = data;
|
|
633
|
+
this.name = 'ApiError';
|
|
634
|
+
// Remove stack trace for cleaner error messages
|
|
635
|
+
this.stack = undefined;
|
|
636
|
+
}
|
|
637
|
+
toString() {
|
|
638
|
+
return `${this.message} (Status: ${this.status})`;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
class ApiClient {
|
|
642
|
+
constructor(apiUrl, token, maxRetries = 3, initialBackoff = 1000) {
|
|
643
|
+
this.apiUrl = apiUrl;
|
|
644
|
+
this.token = token;
|
|
645
|
+
this.maxRetries = maxRetries;
|
|
646
|
+
this.initialBackoff = initialBackoff;
|
|
647
|
+
}
|
|
648
|
+
async handleResponse(response) {
|
|
649
|
+
var _a;
|
|
650
|
+
try {
|
|
651
|
+
const data = await response.json();
|
|
652
|
+
if (!response.ok) {
|
|
653
|
+
switch (response.status) {
|
|
654
|
+
case 401:
|
|
655
|
+
throw new ApiError(401, 'Unauthorized - Invalid or missing authentication token');
|
|
656
|
+
case 403:
|
|
657
|
+
throw new ApiError(403, 'Forbidden - You do not have permission to perform this action');
|
|
658
|
+
case 400:
|
|
659
|
+
throw new ApiError(400, 'Bad Request', data);
|
|
660
|
+
case 404:
|
|
661
|
+
throw new ApiError(404, 'Not Found', data);
|
|
662
|
+
case 500:
|
|
663
|
+
throw new ApiError(500, 'Internal Server Error', data);
|
|
664
|
+
default:
|
|
665
|
+
throw new ApiError(response.status, `API Error: ${data.error || 'Unknown error'}`, data);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return data;
|
|
669
|
+
}
|
|
670
|
+
catch (error) {
|
|
671
|
+
// Only throw the original error if it's not a network error
|
|
672
|
+
if (!((_a = error.message) === null || _a === void 0 ? void 0 : _a.includes('fetch failed'))) {
|
|
673
|
+
throw error;
|
|
674
|
+
}
|
|
675
|
+
throw new ApiError(0, 'Network error: fetch failed');
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
getHeaders() {
|
|
679
|
+
const headers = {
|
|
680
|
+
'Content-Type': 'application/json',
|
|
681
|
+
};
|
|
682
|
+
// Always include token in headers now that we have a default
|
|
683
|
+
headers['Authorization'] = `Token ${this.token}`;
|
|
684
|
+
return headers;
|
|
685
|
+
}
|
|
686
|
+
async createStep(options) {
|
|
687
|
+
apiLogger.debug('creating step ', options.stepPrompt);
|
|
688
|
+
return pRetry(async () => {
|
|
689
|
+
const response = await fetch(`${this.apiUrl}/step-runners/`, {
|
|
690
|
+
method: 'POST',
|
|
691
|
+
headers: this.getHeaders(),
|
|
692
|
+
body: JSON.stringify({
|
|
693
|
+
step_id: options.stepIdFromServer,
|
|
694
|
+
scenario_name: options.scenarioName,
|
|
695
|
+
step_prompt: options.stepPrompt,
|
|
696
|
+
initial_screenshot: options.initial_screenshot_url,
|
|
697
|
+
initial_html_content: options.initial_html_content,
|
|
698
|
+
use_cache: options.use_cache,
|
|
699
|
+
}),
|
|
700
|
+
});
|
|
701
|
+
const data = await this.handleResponse(response);
|
|
702
|
+
return data.step.id;
|
|
703
|
+
}, {
|
|
704
|
+
retries: this.maxRetries,
|
|
705
|
+
minTimeout: this.initialBackoff,
|
|
706
|
+
onFailedAttempt: error => {
|
|
707
|
+
apiLogger.warn(`API call failed, attempt ${error.attemptNumber} of ${error.retriesLeft + error.attemptNumber}...`);
|
|
708
|
+
}
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
async patchStep(stepId, fields) {
|
|
712
|
+
// Use PATCH /steps/:id/ endpoint
|
|
713
|
+
apiLogger.debug(`patching step #${stepId}`);
|
|
714
|
+
return pRetry(async () => {
|
|
715
|
+
const response = await fetch(`${this.apiUrl}/steps/${stepId}/`, {
|
|
716
|
+
method: 'PATCH',
|
|
717
|
+
headers: this.getHeaders(),
|
|
718
|
+
body: JSON.stringify(fields)
|
|
719
|
+
});
|
|
720
|
+
const data = await this.handleResponse(response);
|
|
721
|
+
return;
|
|
722
|
+
}, {
|
|
723
|
+
retries: this.maxRetries,
|
|
724
|
+
minTimeout: this.initialBackoff,
|
|
725
|
+
onFailedAttempt: error => {
|
|
726
|
+
apiLogger.warn(`API call failed, attempt ${error.attemptNumber} of ${error.retriesLeft + error.attemptNumber}...`);
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
async resolveNextInstruction(stepId, instruction, aiModel) {
|
|
731
|
+
apiLogger.debug(`resolving next instruction: ${instruction}`);
|
|
732
|
+
return pRetry(async () => {
|
|
733
|
+
apiLogger.debug(`API client: Resolving next instruction for step ${stepId}`);
|
|
734
|
+
const cleanInstruction = cleanupInstructionElements(instruction);
|
|
735
|
+
const response = await fetch(`${this.apiUrl}/step-runners/${stepId}/run/`, {
|
|
736
|
+
method: 'POST',
|
|
737
|
+
headers: this.getHeaders(),
|
|
738
|
+
body: JSON.stringify({
|
|
739
|
+
executed_instruction: cleanInstruction,
|
|
740
|
+
ai_model: aiModel
|
|
741
|
+
}),
|
|
742
|
+
});
|
|
743
|
+
const data = await this.handleResponse(response);
|
|
744
|
+
return data.instruction;
|
|
745
|
+
}, {
|
|
746
|
+
retries: this.maxRetries,
|
|
747
|
+
minTimeout: this.initialBackoff,
|
|
748
|
+
onFailedAttempt: error => {
|
|
749
|
+
apiLogger.warn(`API call failed, attempt ${error.attemptNumber} of ${error.retriesLeft + error.attemptNumber}...`);
|
|
750
|
+
}
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
async uploadScreenshot(screenshot_bytes) {
|
|
754
|
+
return pRetry(async () => {
|
|
755
|
+
const response = await fetch(`${this.apiUrl}/upload-screenshots/`, {
|
|
756
|
+
method: 'POST',
|
|
757
|
+
headers: {
|
|
758
|
+
'Authorization': `Token ${this.token}`
|
|
759
|
+
},
|
|
760
|
+
body: screenshot_bytes,
|
|
761
|
+
});
|
|
762
|
+
const data = await this.handleResponse(response);
|
|
763
|
+
return data.screenshot_url;
|
|
764
|
+
}, {
|
|
765
|
+
retries: this.maxRetries,
|
|
766
|
+
minTimeout: this.initialBackoff,
|
|
767
|
+
onFailedAttempt: error => {
|
|
768
|
+
apiLogger.warn(`API call failed, attempt ${error.attemptNumber} of ${error.retriesLeft + error.attemptNumber}...`);
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
async findStepByPrompt(prompt, scenarioName) {
|
|
773
|
+
apiLogger.debug(`Finding step by prompt: ${prompt} and scenario: ${scenarioName}`);
|
|
774
|
+
return pRetry(async () => {
|
|
775
|
+
const response = await fetch(`${this.apiUrl}/step-runners/find-step-by-prompt/`, {
|
|
776
|
+
method: 'POST',
|
|
777
|
+
headers: this.getHeaders(),
|
|
778
|
+
body: JSON.stringify({
|
|
779
|
+
prompt: prompt,
|
|
780
|
+
scenario_name: scenarioName
|
|
781
|
+
}),
|
|
782
|
+
});
|
|
783
|
+
try {
|
|
784
|
+
const data = await this.handleResponse(response);
|
|
785
|
+
return {
|
|
786
|
+
step: data.step,
|
|
787
|
+
total_count: data.total_count
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
catch (error) {
|
|
791
|
+
// If we get a 404, the step doesn't exist
|
|
792
|
+
if (error instanceof ApiError && error.status === 404) {
|
|
793
|
+
return null;
|
|
794
|
+
}
|
|
795
|
+
// For any other error, rethrow
|
|
796
|
+
throw error;
|
|
797
|
+
}
|
|
798
|
+
}, {
|
|
799
|
+
retries: this.maxRetries,
|
|
800
|
+
minTimeout: this.initialBackoff,
|
|
801
|
+
onFailedAttempt: error => {
|
|
802
|
+
apiLogger.warn(`API call failed, attempt ${error.attemptNumber} of ${error.retriesLeft + error.attemptNumber}...`);
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
async resetStep(stepId) {
|
|
807
|
+
return pRetry(async () => {
|
|
808
|
+
const response = await fetch(`${this.apiUrl}/steps/${stepId}/reset/`, {
|
|
809
|
+
method: 'POST',
|
|
810
|
+
headers: this.getHeaders(),
|
|
811
|
+
});
|
|
812
|
+
const data = await this.handleResponse(response);
|
|
813
|
+
return data;
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
async interactionToStep(scenarioName, interaction, position = -1) {
|
|
817
|
+
// Use POST /interaction-to-step/ endpoint
|
|
818
|
+
apiLogger.debug(`converting interaction #${interaction.interactionId} to step`);
|
|
819
|
+
return pRetry(async () => {
|
|
820
|
+
const response = await fetch(`${this.apiUrl}/interaction-to-step/`, {
|
|
821
|
+
method: 'POST',
|
|
822
|
+
headers: this.getHeaders(),
|
|
823
|
+
body: JSON.stringify({
|
|
824
|
+
scenario_name: scenarioName,
|
|
825
|
+
position,
|
|
826
|
+
interaction_id: interaction.interactionId,
|
|
827
|
+
action: interaction.action,
|
|
828
|
+
argument: interaction.argument,
|
|
829
|
+
element_css_selector: interaction.elementInfo.css_selector,
|
|
830
|
+
iframe_selector: interaction.elementInfo.iframe_selector,
|
|
831
|
+
prompt: interaction.nativeDescription,
|
|
832
|
+
vanilla_prompt: interaction.nativeDescription,
|
|
833
|
+
is_vanilla_prompt_robust: interaction.isNativeDescriptionElaborate || false,
|
|
834
|
+
target_element_name: interaction.nativeName,
|
|
835
|
+
target_element_info: interaction.elementInfo,
|
|
836
|
+
url: interaction.url,
|
|
837
|
+
})
|
|
838
|
+
});
|
|
839
|
+
const data = await this.handleResponse(response);
|
|
840
|
+
return [data.result.step_id, data.result.matched_step, data.scenario_id];
|
|
841
|
+
}, {
|
|
842
|
+
retries: this.maxRetries,
|
|
843
|
+
minTimeout: this.initialBackoff,
|
|
844
|
+
onFailedAttempt: error => {
|
|
845
|
+
apiLogger.warn(`API call failed, attempt ${error.attemptNumber} of ${error.retriesLeft + error.attemptNumber}...`);
|
|
846
|
+
}
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
async actionToPrompt(action2promptInput, aiModel) {
|
|
850
|
+
// Use POST /action-to-prompt/ endpoint
|
|
851
|
+
apiLogger.debug(`running action2prompt for step #${action2promptInput.step_id}`);
|
|
852
|
+
return pRetry(async () => {
|
|
853
|
+
const response = await fetch(`${this.apiUrl}/steps/${action2promptInput.step_id}/action_to_prompt/`, {
|
|
854
|
+
method: 'POST',
|
|
855
|
+
headers: this.getHeaders(),
|
|
856
|
+
body: JSON.stringify({ ...action2promptInput, 'model': aiModel })
|
|
857
|
+
});
|
|
858
|
+
const data = await this.handleResponse(response);
|
|
859
|
+
return data;
|
|
860
|
+
}, {
|
|
861
|
+
retries: this.maxRetries,
|
|
862
|
+
minTimeout: this.initialBackoff,
|
|
863
|
+
onFailedAttempt: error => {
|
|
864
|
+
apiLogger.warn(`API call failed, attempt ${error.attemptNumber} of ${error.retriesLeft + error.attemptNumber}...`);
|
|
865
|
+
}
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
async summarizeScenario(scenarioId, aiModel) {
|
|
869
|
+
const response = await fetch(`${this.apiUrl}/api/scenarios/${scenarioId}/summary`, {
|
|
870
|
+
method: 'POST',
|
|
871
|
+
headers: this.getHeaders(),
|
|
872
|
+
body: JSON.stringify({ 'model': aiModel })
|
|
873
|
+
});
|
|
874
|
+
const data = await response.json();
|
|
875
|
+
if (!response.ok) {
|
|
876
|
+
throw new Error(data.error || 'Failed to summarize scenario');
|
|
877
|
+
}
|
|
878
|
+
return data;
|
|
879
|
+
}
|
|
880
|
+
} /* ApiClient */
|
|
881
|
+
|
|
882
|
+
// Default logger instance
|
|
883
|
+
const proboLogger = new ProboLogger('probolib');
|
|
1051
884
|
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
885
|
+
const SPECIAL_KEYS = [
|
|
886
|
+
'Enter', 'Tab', 'Escape', 'Backspace', 'Delete',
|
|
887
|
+
'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown',
|
|
888
|
+
'Home', 'End', 'PageUp', 'PageDown',
|
|
889
|
+
'Insert', 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12',
|
|
890
|
+
'Shift', 'Control', 'Alt', 'Meta',
|
|
891
|
+
'CapsLock', 'NumLock', 'ScrollLock',
|
|
892
|
+
'Pause', 'PrintScreen', 'ContextMenu',
|
|
893
|
+
'AudioVolumeUp', 'AudioVolumeDown', 'AudioVolumeMute',
|
|
894
|
+
'MediaTrackNext', 'MediaTrackPrevious', 'MediaStop', 'MediaPlayPause',
|
|
895
|
+
'BrowserBack', 'BrowserForward', 'BrowserRefresh', 'BrowserFavorites'
|
|
896
|
+
];
|
|
897
|
+
/**
|
|
898
|
+
* Handle potential navigation exactly like Python's handle_potential_navigation
|
|
899
|
+
*/
|
|
900
|
+
async function handlePotentialNavigation(page, locator = null, value = null, options = {}) {
|
|
901
|
+
const { initialTimeout = 5000, navigationTimeout = 7000, globalTimeout = 15000, } = options;
|
|
902
|
+
const startTime = Date.now();
|
|
903
|
+
let navigationCount = 0;
|
|
904
|
+
let lastNavTime = null;
|
|
905
|
+
const onFrameNav = (frame) => {
|
|
906
|
+
if (frame === page.mainFrame()) {
|
|
907
|
+
navigationCount++;
|
|
908
|
+
lastNavTime = Date.now();
|
|
909
|
+
proboLogger.debug(`DEBUG_NAV[${((Date.now() - startTime) / 1000).toFixed(3)}s]: navigation detected (count=${navigationCount})`);
|
|
910
|
+
}
|
|
911
|
+
};
|
|
912
|
+
proboLogger.debug(`DEBUG_NAV[0.000s]: Starting navigation detection`);
|
|
913
|
+
page.on("framenavigated", onFrameNav);
|
|
914
|
+
try {
|
|
915
|
+
if (locator) {
|
|
916
|
+
proboLogger.debug(`DEBUG_NAV: Executing click(${value})`);
|
|
917
|
+
await clickAtPosition(page, locator, value);
|
|
918
|
+
}
|
|
919
|
+
// wait for any initial nav to fire
|
|
920
|
+
await page.waitForTimeout(initialTimeout);
|
|
921
|
+
proboLogger.debug(`DEBUG_NAV[${((Date.now() - startTime) / 1000).toFixed(3)}s]: After initial wait, count=${navigationCount}`);
|
|
922
|
+
if (navigationCount > 0) {
|
|
923
|
+
// loop until either per-nav or global timeout
|
|
924
|
+
while (true) {
|
|
925
|
+
const now = Date.now();
|
|
926
|
+
if (lastNavTime !== null &&
|
|
927
|
+
now - lastNavTime > navigationTimeout) {
|
|
928
|
+
proboLogger.debug(`DEBUG_NAV[${((now - startTime) / 1000).toFixed(3)}s]: per‐navigation timeout reached`);
|
|
929
|
+
break;
|
|
930
|
+
}
|
|
931
|
+
if (now - startTime > globalTimeout) {
|
|
932
|
+
proboLogger.debug(`DEBUG_NAV[${((now - startTime) / 1000).toFixed(3)}s]: overall timeout reached`);
|
|
933
|
+
break;
|
|
934
|
+
}
|
|
935
|
+
await page.waitForTimeout(500);
|
|
936
|
+
}
|
|
937
|
+
// now wait for load + idle
|
|
938
|
+
proboLogger.debug(`DEBUG_NAV: waiting for load state`);
|
|
939
|
+
await page.waitForLoadState("load", { timeout: globalTimeout });
|
|
940
|
+
proboLogger.debug(`DEBUG_NAV: waiting for networkidle`);
|
|
941
|
+
try {
|
|
942
|
+
// shorter idle‐wait so we don't hang the full globalTimeout here
|
|
943
|
+
await page.waitForLoadState("networkidle", {
|
|
944
|
+
timeout: navigationTimeout,
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
catch (_a) {
|
|
948
|
+
proboLogger.debug(`DEBUG_NAV: networkidle not reached in ${navigationTimeout}ms, proceeding anyway`);
|
|
949
|
+
}
|
|
950
|
+
await scrollToBottomRight(page);
|
|
951
|
+
proboLogger.debug(`DEBUG_NAV: done`);
|
|
952
|
+
return true;
|
|
953
|
+
}
|
|
954
|
+
proboLogger.debug(`DEBUG_NAV: no navigation detected`);
|
|
955
|
+
return false;
|
|
956
|
+
}
|
|
957
|
+
finally {
|
|
958
|
+
page.removeListener("framenavigated", onFrameNav);
|
|
959
|
+
proboLogger.debug(`DEBUG_NAV: listener removed`);
|
|
960
|
+
}
|
|
1061
961
|
}
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
962
|
+
/**
|
|
963
|
+
* Scroll entire page to bottom-right, triggering lazy-loaded content
|
|
964
|
+
*/
|
|
965
|
+
async function scrollToBottomRight(page) {
|
|
966
|
+
const startTime = performance.now();
|
|
967
|
+
proboLogger.debug(`Starting scroll to bottom-right`);
|
|
968
|
+
let lastHeight = await page.evaluate(() => document.documentElement.scrollHeight);
|
|
969
|
+
let lastWidth = await page.evaluate(() => document.documentElement.scrollWidth);
|
|
970
|
+
let smoothingSteps = 0;
|
|
971
|
+
while (true) {
|
|
972
|
+
const initY = await page.evaluate(() => window.scrollY);
|
|
973
|
+
const initX = await page.evaluate(() => window.scrollX);
|
|
974
|
+
const clientHeight = await page.evaluate(() => document.documentElement.clientHeight);
|
|
975
|
+
const maxHeight = await page.evaluate(() => document.documentElement.scrollHeight);
|
|
976
|
+
const clientWidth = await page.evaluate(() => document.documentElement.clientWidth);
|
|
977
|
+
const maxWidth = await page.evaluate(() => document.documentElement.scrollWidth);
|
|
978
|
+
let currX = initX;
|
|
979
|
+
while (currX < maxWidth - clientWidth) {
|
|
980
|
+
let currY = initY;
|
|
981
|
+
while (currY < maxHeight - clientHeight) {
|
|
982
|
+
currY += clientHeight;
|
|
983
|
+
await page.evaluate(([x, y]) => window.scrollTo(x, y), [currX, currY]);
|
|
984
|
+
await page.waitForTimeout(50);
|
|
985
|
+
smoothingSteps++;
|
|
986
|
+
}
|
|
987
|
+
currX += clientWidth;
|
|
988
|
+
}
|
|
989
|
+
proboLogger.debug(`performed ${smoothingSteps} smoothing steps while scrolling`);
|
|
990
|
+
const newHeight = await page.evaluate(() => document.documentElement.scrollHeight);
|
|
991
|
+
const newWidth = await page.evaluate(() => document.documentElement.scrollWidth);
|
|
992
|
+
if (newHeight === lastHeight && newWidth === lastWidth)
|
|
993
|
+
break;
|
|
994
|
+
proboLogger.debug(`page dimensions updated, repeating scroll`);
|
|
995
|
+
lastHeight = newHeight;
|
|
996
|
+
lastWidth = newWidth;
|
|
997
|
+
}
|
|
998
|
+
if (smoothingSteps > 0) {
|
|
999
|
+
await page.waitForTimeout(200);
|
|
1000
|
+
await page.evaluate('window.scrollTo(0, 0)');
|
|
1001
|
+
await page.waitForTimeout(50);
|
|
1002
|
+
}
|
|
1003
|
+
proboLogger.debug(`Scroll completed in ${(performance.now() - startTime).toFixed(3)}ms`);
|
|
1071
1004
|
}
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
const
|
|
1077
|
-
|
|
1078
|
-
const
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1005
|
+
/**
|
|
1006
|
+
* Wait for DOM mutations to settle using MutationObserver logic
|
|
1007
|
+
*/
|
|
1008
|
+
async function waitForMutationsToSettle(page, mutationTimeout = 1500, initTimeout = 2000) {
|
|
1009
|
+
const startTime = Date.now();
|
|
1010
|
+
proboLogger.debug(`Starting mutation settlement (initTimeout=${initTimeout}, mutationTimeout=${mutationTimeout})`);
|
|
1011
|
+
const result = await page.evaluate(async ({ mutationTimeout, initTimeout }) => {
|
|
1012
|
+
async function blockUntilStable(targetNode, options = { childList: true, subtree: true }) {
|
|
1013
|
+
return new Promise((resolve) => {
|
|
1014
|
+
let initTimerExpired = false;
|
|
1015
|
+
let mutationsOccurred = false;
|
|
1016
|
+
const observerTimers = new Set();
|
|
1017
|
+
window.setTimeout(() => {
|
|
1018
|
+
initTimerExpired = true;
|
|
1019
|
+
if (observerTimers.size === 0)
|
|
1020
|
+
resolve(mutationsOccurred);
|
|
1021
|
+
}, initTimeout);
|
|
1022
|
+
const observer = new MutationObserver(() => {
|
|
1023
|
+
mutationsOccurred = true;
|
|
1024
|
+
observerTimers.forEach((t) => clearTimeout(t));
|
|
1025
|
+
observerTimers.clear();
|
|
1026
|
+
const t = window.setTimeout(() => {
|
|
1027
|
+
observerTimers.delete(t);
|
|
1028
|
+
if (initTimerExpired && observerTimers.size === 0) {
|
|
1029
|
+
observer.disconnect();
|
|
1030
|
+
resolve(mutationsOccurred);
|
|
1031
|
+
}
|
|
1032
|
+
}, mutationTimeout);
|
|
1033
|
+
observerTimers.add(t);
|
|
1034
|
+
});
|
|
1035
|
+
observer.observe(targetNode, options);
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
return blockUntilStable(document.body);
|
|
1039
|
+
}, { mutationTimeout, initTimeout });
|
|
1040
|
+
const total = ((Date.now() - startTime) / 1000).toFixed(2);
|
|
1041
|
+
proboLogger.debug(result
|
|
1042
|
+
? `Mutations settled. Took ${total}s`
|
|
1043
|
+
: `No mutations observed. Took ${total}s`);
|
|
1044
|
+
return result;
|
|
1108
1045
|
}
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1046
|
+
/**
|
|
1047
|
+
* Get element text value: allTextContents + innerText fallback
|
|
1048
|
+
*/
|
|
1049
|
+
async function getElementValue(page, locator) {
|
|
1050
|
+
const texts = await locator.allTextContents();
|
|
1051
|
+
let allText = texts.join('').trim();
|
|
1052
|
+
if (!allText) {
|
|
1053
|
+
allText = await locator.evaluate((el) => el.innerText);
|
|
1054
|
+
}
|
|
1055
|
+
if (!allText) {
|
|
1056
|
+
allText = await locator.inputValue();
|
|
1057
|
+
}
|
|
1058
|
+
proboLogger.debug(`getElementValue: [${allText}]`);
|
|
1059
|
+
return allText;
|
|
1060
|
+
}
|
|
1061
|
+
/**
|
|
1062
|
+
* Select dropdown option: native <select>, <option>, child select, or ARIA listbox
|
|
1063
|
+
*/
|
|
1064
|
+
async function selectDropdownOption(page, locator, value) {
|
|
1065
|
+
const tagName = await (locator === null || locator === void 0 ? void 0 : locator.evaluate((el) => el.tagName.toLowerCase()));
|
|
1066
|
+
const role = await (locator === null || locator === void 0 ? void 0 : locator.getAttribute('role'));
|
|
1067
|
+
if (tagName === 'option' || role === 'option') {
|
|
1068
|
+
proboLogger.debug('selectDropdownOption: option role detected');
|
|
1069
|
+
await (locator === null || locator === void 0 ? void 0 : locator.click());
|
|
1070
|
+
}
|
|
1071
|
+
else if (tagName === 'select') {
|
|
1072
|
+
proboLogger.debug('selectDropdownOption: simple select tag detected');
|
|
1073
|
+
try {
|
|
1074
|
+
await (locator === null || locator === void 0 ? void 0 : locator.selectOption(value));
|
|
1075
|
+
}
|
|
1076
|
+
catch (_a) {
|
|
1077
|
+
proboLogger.debug('selectDropdownOption: manual change event fallback');
|
|
1078
|
+
const handle = locator ? await locator.elementHandle() : null;
|
|
1079
|
+
await page.evaluate(({ h, val }) => {
|
|
1080
|
+
const el = h;
|
|
1081
|
+
if (el) {
|
|
1082
|
+
el.value = Array.isArray(val) ? val[0] : val;
|
|
1083
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1084
|
+
}
|
|
1085
|
+
}, { h: handle, val: value });
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
else {
|
|
1089
|
+
proboLogger.debug('selectDropdownOption: custom dropdown path');
|
|
1090
|
+
let listbox = locator.locator('select');
|
|
1091
|
+
let count = await listbox.count();
|
|
1092
|
+
if (count > 1)
|
|
1093
|
+
throw new Error(`selectDropdownOption: ambiguous <select> count=${count}`);
|
|
1094
|
+
if (count === 1) {
|
|
1095
|
+
proboLogger.debug('selectDropdownOption: child <select> found');
|
|
1096
|
+
await listbox.selectOption(value);
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
await locator.click();
|
|
1100
|
+
let container = locator;
|
|
1101
|
+
count = 0;
|
|
1102
|
+
if (role !== 'listbox') {
|
|
1103
|
+
for (let i = 0; i < 7; i++) {
|
|
1104
|
+
listbox = container.getByRole('listbox');
|
|
1105
|
+
count = await listbox.count();
|
|
1106
|
+
if (count >= 1)
|
|
1107
|
+
break;
|
|
1108
|
+
proboLogger.debug(`selectDropdownOption: iteration #${i} no listbox found`);
|
|
1109
|
+
container = container.locator('xpath=..');
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
else {
|
|
1113
|
+
listbox = container;
|
|
1114
|
+
count = await listbox.count();
|
|
1115
|
+
}
|
|
1116
|
+
if (count !== 1)
|
|
1117
|
+
throw new Error(`selectDropdownOption: found ${count} listbox locators`);
|
|
1118
|
+
const vals = Array.isArray(value) ? value : [value];
|
|
1119
|
+
for (const val of vals) {
|
|
1120
|
+
const option = listbox.getByRole('option').getByText(new RegExp(`^${val}$`, 'i'));
|
|
1121
|
+
const optCount = await option.count();
|
|
1122
|
+
if (optCount !== 1)
|
|
1123
|
+
throw new Error(`selectDropdownOption: ${optCount} options for '${val}'`);
|
|
1124
|
+
await option.click();
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
/**
|
|
1129
|
+
* Execute a given Playwright action, mirroring Python's _perform_action
|
|
1130
|
+
*/
|
|
1131
|
+
async function executePlaywrightAction(page, action, value, iframe_selector, element_css_selector) {
|
|
1132
|
+
proboLogger.info(`performing Action: ${action} Value: ${value}`);
|
|
1133
|
+
try {
|
|
1134
|
+
if (action === PlaywrightAction.VISIT_BASE_URL || action === PlaywrightAction.VISIT_URL) {
|
|
1135
|
+
await page.goto(value, { waitUntil: 'load' });
|
|
1136
|
+
await handlePotentialNavigation(page, null);
|
|
1137
|
+
}
|
|
1138
|
+
else {
|
|
1139
|
+
let locator = undefined;
|
|
1140
|
+
if (iframe_selector)
|
|
1141
|
+
locator = page.frameLocator(iframe_selector).locator(element_css_selector);
|
|
1142
|
+
else
|
|
1143
|
+
locator = page.locator(element_css_selector);
|
|
1144
|
+
switch (action) {
|
|
1145
|
+
case PlaywrightAction.CLICK:
|
|
1146
|
+
await handlePotentialNavigation(page, locator, value);
|
|
1147
|
+
break;
|
|
1148
|
+
case PlaywrightAction.FILL_IN:
|
|
1149
|
+
// await locator.click();
|
|
1150
|
+
await locator.fill(value);
|
|
1151
|
+
break;
|
|
1152
|
+
case PlaywrightAction.TYPE_KEYS:
|
|
1153
|
+
// Check if the value contains any special keys
|
|
1154
|
+
const specialKey = SPECIAL_KEYS.find(key => value.includes(key));
|
|
1155
|
+
if (specialKey) {
|
|
1156
|
+
// If it's a single special key, just press it
|
|
1157
|
+
if (value === specialKey) {
|
|
1158
|
+
await (locator === null || locator === void 0 ? void 0 : locator.press(specialKey));
|
|
1159
|
+
}
|
|
1160
|
+
else {
|
|
1161
|
+
// Handle combinations like 'Control+A' or 'Shift+ArrowRight'
|
|
1162
|
+
const parts = value.split('+');
|
|
1163
|
+
if (parts.length === 2 && SPECIAL_KEYS.includes(parts[0]) && SPECIAL_KEYS.includes(parts[1])) {
|
|
1164
|
+
await (locator === null || locator === void 0 ? void 0 : locator.press(value));
|
|
1165
|
+
}
|
|
1166
|
+
else {
|
|
1167
|
+
// If it's a mix of special keys and text, use pressSequentially
|
|
1168
|
+
await (locator === null || locator === void 0 ? void 0 : locator.pressSequentially(value));
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
else {
|
|
1173
|
+
// No special keys, just type normally
|
|
1174
|
+
await (locator === null || locator === void 0 ? void 0 : locator.pressSequentially(value));
|
|
1175
|
+
}
|
|
1176
|
+
break;
|
|
1177
|
+
case PlaywrightAction.SELECT_DROPDOWN:
|
|
1178
|
+
await selectDropdownOption(page, locator, value);
|
|
1179
|
+
break;
|
|
1180
|
+
case PlaywrightAction.SELECT_MULTIPLE_DROPDOWN:
|
|
1181
|
+
let optsArr;
|
|
1182
|
+
if (value.startsWith('[')) {
|
|
1183
|
+
try {
|
|
1184
|
+
optsArr = JSON.parse(value);
|
|
1185
|
+
}
|
|
1186
|
+
catch (_a) {
|
|
1187
|
+
optsArr = value.slice(1, -1).split(',').map(o => o.trim());
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
else {
|
|
1191
|
+
optsArr = value.split(',').map(o => o.trim());
|
|
1192
|
+
}
|
|
1193
|
+
await (locator === null || locator === void 0 ? void 0 : locator.selectOption(optsArr));
|
|
1194
|
+
break;
|
|
1195
|
+
case PlaywrightAction.CHECK_CHECKBOX:
|
|
1196
|
+
await (locator === null || locator === void 0 ? void 0 : locator.setChecked(value.toLowerCase() === 'true'));
|
|
1197
|
+
break;
|
|
1198
|
+
case PlaywrightAction.SELECT_RADIO:
|
|
1199
|
+
case PlaywrightAction.TOGGLE_SWITCH:
|
|
1200
|
+
await (locator === null || locator === void 0 ? void 0 : locator.click());
|
|
1201
|
+
break;
|
|
1202
|
+
case PlaywrightAction.HOVER:
|
|
1203
|
+
if (await locator.isVisible()) {
|
|
1204
|
+
await (locator === null || locator === void 0 ? void 0 : locator.hover({ noWaitAfter: false }));
|
|
1205
|
+
await page.waitForTimeout(100); // short delay for hover to take effect
|
|
1206
|
+
}
|
|
1207
|
+
break;
|
|
1208
|
+
case PlaywrightAction.VALIDATE_EXACT_VALUE:
|
|
1209
|
+
const actualExact = await getElementValue(page, locator);
|
|
1210
|
+
proboLogger.debug(`actual value is [${actualExact}]`);
|
|
1211
|
+
if (actualExact !== value) {
|
|
1212
|
+
proboLogger.info(`Validation *FAIL* expected '${value}' but got '${actualExact}'`);
|
|
1213
|
+
return false;
|
|
1214
|
+
}
|
|
1215
|
+
proboLogger.info('Validation *PASS*');
|
|
1216
|
+
break;
|
|
1217
|
+
case PlaywrightAction.VALIDATE_CONTAINS_VALUE:
|
|
1218
|
+
case PlaywrightAction.VALIDATE:
|
|
1219
|
+
const actualContains = await getElementValue(page, locator);
|
|
1220
|
+
proboLogger.debug(`actual value is [${actualContains}]`);
|
|
1221
|
+
if (!actualContains.includes(value)) {
|
|
1222
|
+
proboLogger.info(`Validation *FAIL* expected '${value}' to be contained in '${actualContains}'`);
|
|
1223
|
+
return false;
|
|
1224
|
+
}
|
|
1225
|
+
proboLogger.info('Validation *PASS*');
|
|
1226
|
+
break;
|
|
1227
|
+
case PlaywrightAction.VALIDATE_URL:
|
|
1228
|
+
const currUrl = page.url();
|
|
1229
|
+
if (currUrl !== value) {
|
|
1230
|
+
proboLogger.info(`Validation *FAIL* expected url '${value}' while is '${currUrl}'`);
|
|
1231
|
+
return false;
|
|
1232
|
+
}
|
|
1233
|
+
proboLogger.info('Validation *PASS*');
|
|
1234
|
+
break;
|
|
1235
|
+
case PlaywrightAction.SCROLL_TO_ELEMENT:
|
|
1236
|
+
// Restore exact scroll positions from recording
|
|
1237
|
+
const scrollData = JSON.parse(value);
|
|
1238
|
+
try {
|
|
1239
|
+
proboLogger.debug('🔄 Restoring scroll position for container:', locator, 'scrollTop:', scrollData.scrollTop, 'scrollLeft:', scrollData.scrollLeft);
|
|
1240
|
+
await locator.evaluate((el, scrollData) => {
|
|
1241
|
+
el.scrollTop = scrollData.scrollTop;
|
|
1242
|
+
el.scrollLeft = scrollData.scrollLeft;
|
|
1243
|
+
}, { scrollTop: scrollData.scrollTop, scrollLeft: scrollData.scrollLeft }, { timeout: 2000 });
|
|
1244
|
+
}
|
|
1245
|
+
catch (e) {
|
|
1246
|
+
proboLogger.error('🔄 Failed to restore scroll position for container:', locator, 'scrollTop:', scrollData.scrollTop, 'scrollLeft:', scrollData.scrollLeft, 'error:', e);
|
|
1247
|
+
}
|
|
1248
|
+
await page.waitForTimeout(500);
|
|
1249
|
+
break;
|
|
1250
|
+
default:
|
|
1251
|
+
throw new Error(`Unknown action: ${action}`);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
return true;
|
|
1255
|
+
}
|
|
1256
|
+
catch (e) {
|
|
1257
|
+
proboLogger.debug(`***ERROR failed to execute action ${action}: ${e}`);
|
|
1258
|
+
throw e;
|
|
1259
|
+
}
|
|
1125
1260
|
}
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1261
|
+
/**
|
|
1262
|
+
* Execute a given Playwright action using native Playwright functions where possible
|
|
1263
|
+
*/
|
|
1264
|
+
async function executeCachedPlaywrightAction(page, action, value, iframe_selector, element_css_selector) {
|
|
1265
|
+
proboLogger.log(`performing Cached Action: ${action} Value: ${value} on iframe: ${iframe_selector} locator: ${element_css_selector}`);
|
|
1266
|
+
try {
|
|
1267
|
+
let locator = undefined;
|
|
1268
|
+
if (iframe_selector)
|
|
1269
|
+
locator = page.frameLocator(iframe_selector).locator(element_css_selector);
|
|
1270
|
+
else
|
|
1271
|
+
locator = page.locator(element_css_selector);
|
|
1272
|
+
switch (action) {
|
|
1273
|
+
case PlaywrightAction.VISIT_BASE_URL:
|
|
1274
|
+
case PlaywrightAction.VISIT_URL:
|
|
1275
|
+
await page.goto(value, { waitUntil: 'networkidle' });
|
|
1276
|
+
break;
|
|
1277
|
+
case PlaywrightAction.CLICK:
|
|
1278
|
+
await clickAtPosition(page, locator, value);
|
|
1279
|
+
await handlePotentialNavigation(page);
|
|
1280
|
+
break;
|
|
1281
|
+
case PlaywrightAction.FILL_IN:
|
|
1282
|
+
// await locator.click();
|
|
1283
|
+
await locator.fill(value);
|
|
1284
|
+
break;
|
|
1285
|
+
case PlaywrightAction.TYPE_KEYS:
|
|
1286
|
+
// Check if the value contains any special keys
|
|
1287
|
+
const specialKey = SPECIAL_KEYS.find(key => value.includes(key));
|
|
1288
|
+
if (specialKey) {
|
|
1289
|
+
// If it's a single special key, just press it
|
|
1290
|
+
if (value === specialKey) {
|
|
1291
|
+
await locator.press(specialKey);
|
|
1292
|
+
}
|
|
1293
|
+
else {
|
|
1294
|
+
// Handle combinations like 'Control+A' or 'Shift+ArrowRight'
|
|
1295
|
+
const parts = value.split('+');
|
|
1296
|
+
if (parts.length === 2 && SPECIAL_KEYS.includes(parts[0]) && SPECIAL_KEYS.includes(parts[1])) {
|
|
1297
|
+
await locator.press(value);
|
|
1298
|
+
}
|
|
1299
|
+
else {
|
|
1300
|
+
// If it's a mix of special keys and text, use pressSequentially
|
|
1301
|
+
await locator.pressSequentially(value);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
else {
|
|
1306
|
+
// No special keys, just type normally
|
|
1307
|
+
await locator.pressSequentially(value);
|
|
1308
|
+
}
|
|
1309
|
+
break;
|
|
1310
|
+
case PlaywrightAction.SELECT_DROPDOWN:
|
|
1311
|
+
await locator.selectOption(value);
|
|
1312
|
+
break;
|
|
1313
|
+
case PlaywrightAction.SELECT_MULTIPLE_DROPDOWN:
|
|
1314
|
+
let optsArr;
|
|
1315
|
+
if (value.startsWith('[')) {
|
|
1316
|
+
try {
|
|
1317
|
+
optsArr = JSON.parse(value);
|
|
1318
|
+
}
|
|
1319
|
+
catch (_a) {
|
|
1320
|
+
optsArr = value.slice(1, -1).split(',').map(o => o.trim());
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
else {
|
|
1324
|
+
optsArr = value.split(',').map(o => o.trim());
|
|
1325
|
+
}
|
|
1326
|
+
await locator.selectOption(optsArr);
|
|
1327
|
+
break;
|
|
1328
|
+
case PlaywrightAction.CHECK_CHECKBOX:
|
|
1329
|
+
await locator.setChecked(value.toLowerCase() === 'true');
|
|
1330
|
+
break;
|
|
1331
|
+
case PlaywrightAction.SELECT_RADIO:
|
|
1332
|
+
case PlaywrightAction.TOGGLE_SWITCH:
|
|
1333
|
+
await locator.click();
|
|
1334
|
+
break;
|
|
1335
|
+
case PlaywrightAction.HOVER:
|
|
1336
|
+
if (await locator.isVisible()) {
|
|
1337
|
+
await (locator === null || locator === void 0 ? void 0 : locator.hover({ noWaitAfter: false }));
|
|
1338
|
+
await page.waitForTimeout(100); // short delay for hover to take effect
|
|
1339
|
+
}
|
|
1340
|
+
break;
|
|
1341
|
+
case PlaywrightAction.VALIDATE_EXACT_VALUE:
|
|
1342
|
+
const actualExact = await getElementValue(page, locator);
|
|
1343
|
+
proboLogger.debug(`actual value is [${actualExact}]`);
|
|
1344
|
+
if (actualExact !== value) {
|
|
1345
|
+
proboLogger.info(`Validation *FAIL* expected '${value}' but got '${actualExact}'`);
|
|
1346
|
+
return false;
|
|
1347
|
+
}
|
|
1348
|
+
proboLogger.info('Validation *PASS*');
|
|
1349
|
+
break;
|
|
1350
|
+
case PlaywrightAction.VALIDATE_CONTAINS_VALUE:
|
|
1351
|
+
case PlaywrightAction.VALIDATE:
|
|
1352
|
+
const actualContains = await getElementValue(page, locator);
|
|
1353
|
+
proboLogger.debug(`actual value is [${actualContains}]`);
|
|
1354
|
+
if (!actualContains.includes(value)) {
|
|
1355
|
+
proboLogger.info(`Validation *FAIL* expected '${value}' to be contained in '${actualContains}'`);
|
|
1356
|
+
return false;
|
|
1357
|
+
}
|
|
1358
|
+
proboLogger.info('Validation *PASS*');
|
|
1359
|
+
break;
|
|
1360
|
+
case PlaywrightAction.VALIDATE_URL:
|
|
1361
|
+
const currUrl = page.url();
|
|
1362
|
+
if (currUrl !== value) {
|
|
1363
|
+
proboLogger.info(`Validation *FAIL* expected url '${value}' while is '${currUrl}'`);
|
|
1364
|
+
return false;
|
|
1365
|
+
}
|
|
1366
|
+
proboLogger.info('Validation *PASS*');
|
|
1367
|
+
break;
|
|
1368
|
+
default:
|
|
1369
|
+
throw new Error(`Unknown action: ${action}`);
|
|
1370
|
+
}
|
|
1371
|
+
return true;
|
|
1372
|
+
}
|
|
1373
|
+
catch (e) {
|
|
1374
|
+
proboLogger.debug(`***ERROR failed to execute cached action ${action}: ${e}`);
|
|
1375
|
+
throw e;
|
|
1376
|
+
}
|
|
1198
1377
|
}
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1378
|
+
var ClickPosition;
|
|
1379
|
+
(function (ClickPosition) {
|
|
1380
|
+
ClickPosition["LEFT"] = "LEFT";
|
|
1381
|
+
ClickPosition["RIGHT"] = "RIGHT";
|
|
1382
|
+
ClickPosition["CENTER"] = "CENTER";
|
|
1383
|
+
})(ClickPosition || (ClickPosition = {}));
|
|
1384
|
+
/**
|
|
1385
|
+
* Click on an element at a specific position
|
|
1386
|
+
*/
|
|
1387
|
+
async function clickAtPosition(page, locator, clickPosition) {
|
|
1388
|
+
if (!clickPosition || clickPosition === ClickPosition.CENTER) {
|
|
1389
|
+
await locator.click({ noWaitAfter: false });
|
|
1390
|
+
return;
|
|
1208
1391
|
}
|
|
1209
|
-
|
|
1210
|
-
|
|
1392
|
+
const boundingBox = await locator.boundingBox();
|
|
1393
|
+
if (boundingBox) {
|
|
1394
|
+
if (clickPosition === ClickPosition.LEFT) {
|
|
1395
|
+
// await page.mouse.click(boundingBox.x + 10, boundingBox.y + boundingBox.height/2);
|
|
1396
|
+
// mouse click returns immediately, use locator click instead (position relative to top left corner)
|
|
1397
|
+
await locator.click({ noWaitAfter: false, position: { x: 10, y: boundingBox.height / 2 } });
|
|
1398
|
+
}
|
|
1399
|
+
else if (clickPosition === ClickPosition.RIGHT) {
|
|
1400
|
+
// await page.mouse.click(boundingBox.x + boundingBox.width - 10, boundingBox.y + boundingBox.height/2);
|
|
1401
|
+
// mouse click returns immediately, use locator click instead (position relative to top left corner)
|
|
1402
|
+
await locator.click({ noWaitAfter: false, position: { x: boundingBox.width - 10, y: boundingBox.height / 2 } });
|
|
1403
|
+
}
|
|
1211
1404
|
}
|
|
1212
1405
|
}
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
this.
|
|
1217
|
-
this.maxRetries = maxRetries;
|
|
1218
|
-
this.initialBackoff = initialBackoff;
|
|
1406
|
+
|
|
1407
|
+
class Highlighter {
|
|
1408
|
+
constructor(enableConsoleLogs = true) {
|
|
1409
|
+
this.enableConsoleLogs = enableConsoleLogs;
|
|
1219
1410
|
}
|
|
1220
|
-
async
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
throw new ApiError(404, 'Not Found', data);
|
|
1234
|
-
case 500:
|
|
1235
|
-
throw new ApiError(500, 'Internal Server Error', data);
|
|
1236
|
-
default:
|
|
1237
|
-
throw new ApiError(response.status, `API Error: ${data.error || 'Unknown error'}`, data);
|
|
1411
|
+
async ensureHighlighterScript(page, maxRetries = 3) {
|
|
1412
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
1413
|
+
try {
|
|
1414
|
+
const scriptExists = await page.evaluate(`typeof window.ProboLabs?.highlight?.execute === 'function'`);
|
|
1415
|
+
if (!scriptExists) {
|
|
1416
|
+
proboLogger.debug('Injecting highlighter script...');
|
|
1417
|
+
await page.evaluate(highlighterCode);
|
|
1418
|
+
// Verify the script was injected correctly
|
|
1419
|
+
const verified = await page.evaluate(`
|
|
1420
|
+
//console.log('ProboLabs global:', window.ProboLabs);
|
|
1421
|
+
typeof window.ProboLabs?.highlight?.execute === 'function'
|
|
1422
|
+
`);
|
|
1423
|
+
proboLogger.debug('Script injection verified:', verified);
|
|
1238
1424
|
}
|
|
1425
|
+
return; // Success - exit the function
|
|
1239
1426
|
}
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1427
|
+
catch (error) {
|
|
1428
|
+
if (attempt === maxRetries - 1) {
|
|
1429
|
+
throw error;
|
|
1430
|
+
}
|
|
1431
|
+
proboLogger.debug(`Script injection attempt ${attempt + 1} failed, retrying after delay...`);
|
|
1432
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
1246
1433
|
}
|
|
1247
|
-
throw new ApiError(0, 'Network error: fetch failed');
|
|
1248
1434
|
}
|
|
1249
1435
|
}
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
return pRetry(async () => {
|
|
1261
|
-
const response = await fetch(`${this.apiUrl}/step-runners/`, {
|
|
1262
|
-
method: 'POST',
|
|
1263
|
-
headers: this.getHeaders(),
|
|
1264
|
-
body: JSON.stringify({
|
|
1265
|
-
step_id: options.stepIdFromServer,
|
|
1266
|
-
scenario_name: options.scenarioName,
|
|
1267
|
-
step_prompt: options.stepPrompt,
|
|
1268
|
-
initial_screenshot: options.initial_screenshot_url,
|
|
1269
|
-
initial_html_content: options.initial_html_content,
|
|
1270
|
-
use_cache: options.use_cache,
|
|
1271
|
-
}),
|
|
1272
|
-
});
|
|
1273
|
-
const data = await this.handleResponse(response);
|
|
1274
|
-
return data.step.id;
|
|
1275
|
-
}, {
|
|
1276
|
-
retries: this.maxRetries,
|
|
1277
|
-
minTimeout: this.initialBackoff,
|
|
1278
|
-
onFailedAttempt: error => {
|
|
1279
|
-
proboLogger$1.warn(`API call failed, attempt ${error.attemptNumber} of ${error.retriesLeft + error.attemptNumber}...`);
|
|
1436
|
+
async highlightElements(page, elementTags) {
|
|
1437
|
+
proboLogger.debug('highlightElements called with:', elementTags);
|
|
1438
|
+
await this.ensureHighlighterScript(page);
|
|
1439
|
+
// Execute the highlight function and await its result
|
|
1440
|
+
const result = await page.evaluate(async (tags) => {
|
|
1441
|
+
var _a, _b;
|
|
1442
|
+
//proboLogger.debug('Browser: Starting highlight execution with tag:', tag);
|
|
1443
|
+
if (!((_b = (_a = window.ProboLabs) === null || _a === void 0 ? void 0 : _a.highlight) === null || _b === void 0 ? void 0 : _b.execute)) {
|
|
1444
|
+
console.error('Browser: ProboLabs.highlight.execute is not available!');
|
|
1445
|
+
return null;
|
|
1280
1446
|
}
|
|
1447
|
+
const elements = await window.ProboLabs.highlight.execute(tags);
|
|
1448
|
+
//proboLogger.debug('Browser: Found elements:', elements);
|
|
1449
|
+
return elements;
|
|
1450
|
+
}, elementTags);
|
|
1451
|
+
// proboLogger.debug("highlighted elements =>");
|
|
1452
|
+
// for (let i = 0; i < result.length; i++) {
|
|
1453
|
+
// proboLogger.debug(result[i]);
|
|
1454
|
+
// };
|
|
1455
|
+
return result;
|
|
1456
|
+
}
|
|
1457
|
+
async unhighlightElements(page) {
|
|
1458
|
+
proboLogger.debug('unhighlightElements called');
|
|
1459
|
+
await this.ensureHighlighterScript(page);
|
|
1460
|
+
await page.evaluate(() => {
|
|
1461
|
+
var _a, _b;
|
|
1462
|
+
(_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();
|
|
1281
1463
|
});
|
|
1282
1464
|
}
|
|
1283
|
-
async
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
const
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
body: JSON.stringify({
|
|
1292
|
-
executed_instruction: cleanInstruction,
|
|
1293
|
-
ai_model: aiModel
|
|
1294
|
-
}),
|
|
1295
|
-
});
|
|
1296
|
-
const data = await this.handleResponse(response);
|
|
1297
|
-
return data.instruction;
|
|
1298
|
-
}, {
|
|
1299
|
-
retries: this.maxRetries,
|
|
1300
|
-
minTimeout: this.initialBackoff,
|
|
1301
|
-
onFailedAttempt: error => {
|
|
1302
|
-
proboLogger$1.warn(`API call failed, attempt ${error.attemptNumber} of ${error.retriesLeft + error.attemptNumber}...`);
|
|
1465
|
+
async highlightElement(page, element_css_selector, iframe_selector, element_index) {
|
|
1466
|
+
await this.ensureHighlighterScript(page);
|
|
1467
|
+
proboLogger.debug('Highlighting element with:', { element_css_selector, iframe_selector, element_index });
|
|
1468
|
+
await page.evaluate(({ css_selector, iframe_selector, index }) => {
|
|
1469
|
+
const proboLabs = window.ProboLabs;
|
|
1470
|
+
if (!proboLabs) {
|
|
1471
|
+
proboLogger.warn('ProboLabs not initialized');
|
|
1472
|
+
return;
|
|
1303
1473
|
}
|
|
1474
|
+
// Create ElementInfo object for the element
|
|
1475
|
+
const elementInfo = {
|
|
1476
|
+
css_selector: css_selector,
|
|
1477
|
+
iframe_selector: iframe_selector,
|
|
1478
|
+
index: index
|
|
1479
|
+
};
|
|
1480
|
+
// Call highlightElements directly
|
|
1481
|
+
proboLabs.highlightElements([elementInfo]);
|
|
1482
|
+
}, {
|
|
1483
|
+
css_selector: element_css_selector,
|
|
1484
|
+
iframe_selector: iframe_selector,
|
|
1485
|
+
index: element_index
|
|
1304
1486
|
});
|
|
1305
1487
|
}
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1488
|
+
// --- New Clean Caching API ---
|
|
1489
|
+
/**
|
|
1490
|
+
* Find and cache candidate elements of a given type (e.g., 'CLICKABLE').
|
|
1491
|
+
* Returns the number of candidates found.
|
|
1492
|
+
*/
|
|
1493
|
+
async findAndCacheCandidateElements(page, elementTypes) {
|
|
1494
|
+
await this.ensureHighlighterScript(page);
|
|
1495
|
+
const result = await page.evaluate(async (types) => {
|
|
1496
|
+
var _a, _b;
|
|
1497
|
+
return await ((_b = (_a = window.ProboLabs) === null || _a === void 0 ? void 0 : _a.findAndCacheCandidateElements) === null || _b === void 0 ? void 0 : _b.call(_a, types));
|
|
1498
|
+
}, elementTypes);
|
|
1499
|
+
return result !== null && result !== void 0 ? result : 0;
|
|
1500
|
+
}
|
|
1501
|
+
/**
|
|
1502
|
+
* Find and cache the actual interaction element by CSS and iframe selector.
|
|
1503
|
+
* Returns true if found, false otherwise.
|
|
1504
|
+
*/
|
|
1505
|
+
async findAndCacheActualElement(page, cssSelector, iframeSelector, isHover = false) {
|
|
1506
|
+
await this.ensureHighlighterScript(page);
|
|
1507
|
+
const result = await page.evaluate((params) => {
|
|
1508
|
+
var _a, _b;
|
|
1509
|
+
return (_b = (_a = window.ProboLabs) === null || _a === void 0 ? void 0 : _a.findAndCacheActualElement) === null || _b === void 0 ? void 0 : _b.call(_a, params.css, params.iframe, params.isHover);
|
|
1510
|
+
}, { css: cssSelector, iframe: iframeSelector, isHover: isHover });
|
|
1511
|
+
return result !== null && result !== void 0 ? result : false;
|
|
1512
|
+
}
|
|
1513
|
+
/**
|
|
1514
|
+
* Find and cache the best matching candidate for the actual element.
|
|
1515
|
+
* Returns true if a match was found, false otherwise.
|
|
1516
|
+
*/
|
|
1517
|
+
async findAndCacheMatchingCandidate(page) {
|
|
1518
|
+
await this.ensureHighlighterScript(page);
|
|
1519
|
+
const result = await page.evaluate(() => {
|
|
1520
|
+
var _a, _b;
|
|
1521
|
+
return (_b = (_a = window.ProboLabs) === null || _a === void 0 ? void 0 : _a.findAndCacheMatchingCandidate) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
1323
1522
|
});
|
|
1523
|
+
return result !== null && result !== void 0 ? result : false;
|
|
1324
1524
|
}
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
// If we get a 404, the step doesn't exist
|
|
1345
|
-
if (error instanceof ApiError && error.status === 404) {
|
|
1346
|
-
return null;
|
|
1347
|
-
}
|
|
1348
|
-
// For any other error, rethrow
|
|
1349
|
-
throw error;
|
|
1350
|
-
}
|
|
1351
|
-
}, {
|
|
1352
|
-
retries: this.maxRetries,
|
|
1353
|
-
minTimeout: this.initialBackoff,
|
|
1354
|
-
onFailedAttempt: error => {
|
|
1355
|
-
proboLogger$1.warn(`API call failed, attempt ${error.attemptNumber} of ${error.retriesLeft + error.attemptNumber}...`);
|
|
1356
|
-
}
|
|
1525
|
+
/**
|
|
1526
|
+
* Highlight the cached candidates, actual, or matching candidate.
|
|
1527
|
+
* @param which 'candidates' | 'actual' | 'matching'
|
|
1528
|
+
*/
|
|
1529
|
+
async highlightCachedElements(page, which) {
|
|
1530
|
+
await this.ensureHighlighterScript(page);
|
|
1531
|
+
await page.evaluate((w) => {
|
|
1532
|
+
var _a, _b;
|
|
1533
|
+
(_b = (_a = window.ProboLabs) === null || _a === void 0 ? void 0 : _a.highlightCachedElements) === null || _b === void 0 ? void 0 : _b.call(_a, w);
|
|
1534
|
+
}, which);
|
|
1535
|
+
}
|
|
1536
|
+
/**
|
|
1537
|
+
* Remove all highlights.
|
|
1538
|
+
*/
|
|
1539
|
+
async unhighlightCached(page) {
|
|
1540
|
+
await this.ensureHighlighterScript(page);
|
|
1541
|
+
await page.evaluate(() => {
|
|
1542
|
+
var _a, _b;
|
|
1543
|
+
(_b = (_a = window.ProboLabs) === null || _a === void 0 ? void 0 : _a.unhighlight) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
1357
1544
|
});
|
|
1358
1545
|
}
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1546
|
+
/**
|
|
1547
|
+
* Reset/clear all cached state.
|
|
1548
|
+
*/
|
|
1549
|
+
async resetCached(page) {
|
|
1550
|
+
await this.ensureHighlighterScript(page);
|
|
1551
|
+
await page.evaluate(() => {
|
|
1552
|
+
var _a, _b;
|
|
1553
|
+
(_b = (_a = window.ProboLabs) === null || _a === void 0 ? void 0 : _a.reset) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1556
|
+
/**
|
|
1557
|
+
* Get serializable info for Node/Playwright (candidates, actual, matchingCandidate)
|
|
1558
|
+
*/
|
|
1559
|
+
async getCandidatesCached(page) {
|
|
1560
|
+
await this.ensureHighlighterScript(page);
|
|
1561
|
+
return await page.evaluate(() => {
|
|
1562
|
+
var _a, _b;
|
|
1563
|
+
return (_b = (_a = window.ProboLabs) === null || _a === void 0 ? void 0 : _a.getCandidates) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
async getActualCached(page) {
|
|
1567
|
+
await this.ensureHighlighterScript(page);
|
|
1568
|
+
return await page.evaluate(() => {
|
|
1569
|
+
var _a, _b;
|
|
1570
|
+
return (_b = (_a = window.ProboLabs) === null || _a === void 0 ? void 0 : _a.getActual) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
async getMatchingCandidateCached(page) {
|
|
1574
|
+
await this.ensureHighlighterScript(page);
|
|
1575
|
+
return await page.evaluate(() => {
|
|
1576
|
+
var _a, _b;
|
|
1577
|
+
return (_b = (_a = window.ProboLabs) === null || _a === void 0 ? void 0 : _a.getMatchingCandidate) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
1367
1578
|
});
|
|
1368
1579
|
}
|
|
1369
1580
|
}
|
|
1370
1581
|
|
|
1371
|
-
const proboLogger = new ProboLogger('probo-playwright');
|
|
1582
|
+
// const proboLogger = new ProboLogger('probo-playwright');
|
|
1372
1583
|
/**
|
|
1373
1584
|
* Available AI models for LLM operations
|
|
1374
1585
|
*/
|
|
@@ -1397,7 +1608,8 @@ const retryOptions = {
|
|
|
1397
1608
|
class Probo {
|
|
1398
1609
|
constructor({ scenarioName, token = '', apiUrl = '', enableConsoleLogs = false, logToConsole = true, logToFile = false, debugLevel = ProboLogLevel.INFO, aiModel = AIModel.DEFAULT_AI_MODEL }) {
|
|
1399
1610
|
// Configure logger transports and level
|
|
1400
|
-
configureLogger({ logToConsole, logToFile, level: debugLevel });
|
|
1611
|
+
// configureLogger({ logToConsole, logToFile, level: debugLevel });
|
|
1612
|
+
proboLogger.setLogLevel(debugLevel);
|
|
1401
1613
|
const apiKey = token || process.env.PROBO_API_KEY;
|
|
1402
1614
|
if (!apiKey) {
|
|
1403
1615
|
proboLogger.error("API key wasn't provided. Pass 'token' or set PROBO_API_KEY");
|
|
@@ -1416,7 +1628,7 @@ class Probo {
|
|
|
1416
1628
|
proboLogger.info(`Initializing: scenario=${scenarioName}, apiUrl=${apiEndPoint}, ` +
|
|
1417
1629
|
`enableConsoleLogs=${enableConsoleLogs}, debugLevel=${debugLevel}, aiModel=${aiModel}`);
|
|
1418
1630
|
}
|
|
1419
|
-
async runStep(page, stepPrompt, options = { useCache: true, stepIdFromServer: undefined, aiModel: this.aiModel }) {
|
|
1631
|
+
async runStep(page, stepPrompt, argument = null, options = { useCache: true, stepIdFromServer: undefined, aiModel: this.aiModel }) {
|
|
1420
1632
|
// Use the aiModel from options if provided, otherwise use the one from constructor
|
|
1421
1633
|
const aiModelToUse = options.aiModel !== undefined ? options.aiModel : this.aiModel;
|
|
1422
1634
|
proboLogger.log(`runStep: ${options.stepIdFromServer ? '#' + options.stepIdFromServer + ' - ' : ''}${stepPrompt}, aiModel: ${aiModelToUse}, pageUrl: ${page.url()}`);
|
|
@@ -1424,7 +1636,7 @@ class Probo {
|
|
|
1424
1636
|
// First check if the step exists in the database
|
|
1425
1637
|
let stepId;
|
|
1426
1638
|
if (options.useCache) {
|
|
1427
|
-
const isCachedStep = await this._handleCachedStep(page, stepPrompt);
|
|
1639
|
+
const isCachedStep = await this._handleCachedStep(page, stepPrompt, argument);
|
|
1428
1640
|
if (isCachedStep) {
|
|
1429
1641
|
proboLogger.debug('performed cached step!');
|
|
1430
1642
|
return true;
|
|
@@ -1477,7 +1689,7 @@ class Probo {
|
|
|
1477
1689
|
instruction = executed_instruction;
|
|
1478
1690
|
break;
|
|
1479
1691
|
case 'perform_action':
|
|
1480
|
-
instruction = await this._handlePerformAction(page, nextInstruction);
|
|
1692
|
+
instruction = await this._handlePerformAction(page, nextInstruction, argument);
|
|
1481
1693
|
break;
|
|
1482
1694
|
default:
|
|
1483
1695
|
throw new Error(`Unknown instruction type: ${nextInstruction.what_to_do}`);
|
|
@@ -1489,12 +1701,13 @@ class Probo {
|
|
|
1489
1701
|
}
|
|
1490
1702
|
}
|
|
1491
1703
|
}
|
|
1492
|
-
async _handleCachedStep(page, stepPrompt) {
|
|
1704
|
+
async _handleCachedStep(page, stepPrompt, argument) {
|
|
1493
1705
|
proboLogger.debug(`Checking if step exists in database: ${stepPrompt}`);
|
|
1494
1706
|
const result = await this.apiClient.findStepByPrompt(stepPrompt, this.scenarioName);
|
|
1495
1707
|
if (result) {
|
|
1496
|
-
|
|
1497
|
-
proboLogger.
|
|
1708
|
+
const actionArgument = argument !== null && argument !== void 0 ? argument : result.step.argument;
|
|
1709
|
+
proboLogger.log(`Found existing step with ID: ${result.step.id} going to perform action: ${result.step.action} with value: ${actionArgument}`);
|
|
1710
|
+
proboLogger.debug(`Step in the DB: #${result.step.id} status: ${result.step.status} action: ${result.step.action} argument: ${actionArgument} locator: ${result.step.element_css_selector}`);
|
|
1498
1711
|
if (result.step.status !== 'EXECUTED') {
|
|
1499
1712
|
proboLogger.debug(`Step ${result.step.id} is not executed, returning false`);
|
|
1500
1713
|
return false;
|
|
@@ -1503,7 +1716,7 @@ class Probo {
|
|
|
1503
1716
|
const element_css_selector = result.step.element_css_selector;
|
|
1504
1717
|
const iframe_selector = result.step.iframe_selector;
|
|
1505
1718
|
try {
|
|
1506
|
-
await executeCachedPlaywrightAction(page, result.step.action,
|
|
1719
|
+
await executeCachedPlaywrightAction(page, result.step.action, actionArgument, iframe_selector, element_css_selector);
|
|
1507
1720
|
}
|
|
1508
1721
|
catch (error) {
|
|
1509
1722
|
proboLogger.error(`Error executing action for step ${result.step.id} going to reset the step`);
|
|
@@ -1571,15 +1784,15 @@ class Probo {
|
|
|
1571
1784
|
proboLogger.debug(`taking screenshot of current page: ${page.url()}`);
|
|
1572
1785
|
// await page.evaluate(() => document.fonts?.ready.catch(() => {}));
|
|
1573
1786
|
const screenshot_bytes = await page.screenshot({ fullPage: true, animations: 'disabled' });
|
|
1574
|
-
// make an api call to upload the screenshot to
|
|
1575
|
-
proboLogger.debug('uploading image data to
|
|
1787
|
+
// make an api call to upload the screenshot to cloud
|
|
1788
|
+
proboLogger.debug('uploading image data to cloud');
|
|
1576
1789
|
const screenshot_url = await this.apiClient.uploadScreenshot(screenshot_bytes);
|
|
1577
1790
|
return screenshot_url;
|
|
1578
1791
|
}
|
|
1579
|
-
async _handlePerformAction(page, nextInstruction) {
|
|
1792
|
+
async _handlePerformAction(page, nextInstruction, argument) {
|
|
1580
1793
|
proboLogger.debug('Handling perform action:', nextInstruction);
|
|
1581
1794
|
const action = nextInstruction.args.action;
|
|
1582
|
-
const value = nextInstruction.args.value;
|
|
1795
|
+
const value = argument !== null && argument !== void 0 ? argument : nextInstruction.args.value;
|
|
1583
1796
|
const element_css_selector = nextInstruction.args.element_css_selector;
|
|
1584
1797
|
const iframe_selector = nextInstruction.args.iframe_selector;
|
|
1585
1798
|
const element_index = nextInstruction.args.element_index;
|
|
@@ -1628,6 +1841,7 @@ class Probo {
|
|
|
1628
1841
|
return executed_instruction;
|
|
1629
1842
|
}
|
|
1630
1843
|
}
|
|
1844
|
+
// export const highlighterCode = '';
|
|
1631
1845
|
|
|
1632
|
-
export { AIModel,
|
|
1846
|
+
export { AIModel, Highlighter, PlaywrightAction, Probo, ProboLogLevel, executeCachedPlaywrightAction, executePlaywrightAction };
|
|
1633
1847
|
//# sourceMappingURL=index.js.map
|