@probolabs/playwright 0.1.0 → 0.1.3

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.js ADDED
@@ -0,0 +1,1177 @@
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 = {\r\n CLICKABLE: \"CLICKABLE\", // button, link, toggle switch, checkbox, radio, dropdowns, clickable divs\r\n FILLABLE: \"FILLABLE\", // input, textarea content_editable, date picker??\r\n SELECTABLE: \"SELECTABLE\", // select\r\n NON_INTERACTIVE_ELEMENT: 'NON_INTERACTIVE_ELEMENT',\r\n };\r\n\r\n class ElementInfo {\r\n constructor(element, index, {tag, type, text, html, xpath, css_selector, bounding_box}) {\r\n this.index = index.toString();\r\n this.tag = tag;\r\n this.type = type;\r\n this.text = text;\r\n this.html = html;\r\n this.xpath = xpath;\r\n this.css_selector = css_selector;\r\n this.bounding_box = bounding_box;\r\n this.element = element;\r\n this.depth = -1;\r\n }\r\n\r\n getSelector() {\r\n return this.xpath ? this.xpath : this.css_selector;\r\n }\r\n\r\n getDepth() {\r\n if (this.depth >= 0) {\r\n return this.depth;\r\n }\r\n \r\n this.depth = 0;\r\n let currentElement = this.element;\r\n \r\n while (currentElement.nodeType === Node.ELEMENT_NODE) { \r\n this.depth++;\r\n if (currentElement.assignedSlot) {\r\n currentElement = currentElement.assignedSlot;\r\n }\r\n else {\r\n currentElement = currentElement.parentNode;\r\n // Check if we're at a shadow root\r\n if (currentElement.nodeType !== Node.ELEMENT_NODE && currentElement.getRootNode() instanceof ShadowRoot) {\r\n // Get the shadow root's host element\r\n currentElement = currentElement.getRootNode().host; \r\n }\r\n }\r\n }\r\n \r\n return this.depth;\r\n }\r\n }\n\n // import { realpath } from \"fs\";\r\n\r\n function getAllDocumentElementsIncludingShadow(selectors, root = document) {\r\n const elements = Array.from(root.querySelectorAll(selectors));\r\n\r\n root.querySelectorAll('*').forEach(el => {\r\n if (el.shadowRoot) {\r\n elements.push(...getAllDocumentElementsIncludingShadow(selectors, el.shadowRoot));\r\n }\r\n });\r\n return elements;\r\n }\r\n\r\n function getAllFrames(root = document) {\r\n const result = [root];\r\n const frames = getAllDocumentElementsIncludingShadow('frame, iframe', root); \r\n frames.forEach(frame => {\r\n try {\r\n const frameDocument = frame.contentDocument;\r\n if (frameDocument) {\r\n result.push(frameDocument);\r\n }\r\n } catch (e) {\r\n // Skip cross-origin frames\r\n console.warn('Could not access frame content:', e.message);\r\n }\r\n });\r\n\r\n return result;\r\n }\r\n\r\n function getAllElementsIncludingShadow(selectors, root = document) {\r\n const elements = [];\r\n\r\n getAllFrames(root).forEach(doc => {\r\n elements.push(...getAllDocumentElementsIncludingShadow(selectors, doc));\r\n });\r\n\r\n return elements;\r\n }\r\n\r\n /**\r\n * Deeply searches through DOM trees including Shadow DOM and frames/iframes\r\n * @param {string} selector - CSS selector to search for\r\n * @param {Document|Element} [root=document] - Starting point for the search\r\n * @param {Object} [options] - Search options\r\n * @param {boolean} [options.searchShadow=true] - Whether to search Shadow DOM\r\n * @param {boolean} [options.searchFrames=true] - Whether to search frames/iframes\r\n * @returns {Element[]} Array of found elements\r\n \r\n function getAllElementsIncludingShadow(selector, root = document, options = {}) {\r\n const {\r\n searchShadow = true,\r\n searchFrames = true\r\n } = options;\r\n\r\n const results = new Set();\r\n \r\n // Helper to check if an element is valid and not yet found\r\n const addIfValid = (element) => {\r\n if (element && !results.has(element)) {\r\n results.add(element);\r\n }\r\n };\r\n\r\n // Helper to process a single document or element\r\n function processNode(node) {\r\n // Search regular DOM\r\n node.querySelectorAll(selector).forEach(addIfValid);\r\n\r\n if (searchShadow) {\r\n // Search all shadow roots\r\n const treeWalker = document.createTreeWalker(\r\n node,\r\n NodeFilter.SHOW_ELEMENT,\r\n {\r\n acceptNode: (element) => {\r\n return element.shadowRoot ? \r\n NodeFilter.FILTER_ACCEPT : \r\n NodeFilter.FILTER_SKIP;\r\n }\r\n }\r\n );\r\n\r\n while (treeWalker.nextNode()) {\r\n const element = treeWalker.currentNode;\r\n if (element.shadowRoot) {\r\n // Search within shadow root\r\n element.shadowRoot.querySelectorAll(selector).forEach(addIfValid);\r\n // Recursively process the shadow root for nested shadow DOMs\r\n processNode(element.shadowRoot);\r\n }\r\n }\r\n }\r\n\r\n if (searchFrames) {\r\n // Search frames and iframes\r\n const frames = node.querySelectorAll('frame, iframe');\r\n frames.forEach(frame => {\r\n try {\r\n const frameDocument = frame.contentDocument;\r\n if (frameDocument) {\r\n processNode(frameDocument);\r\n }\r\n } catch (e) {\r\n // Skip cross-origin frames\r\n console.warn('Could not access frame content:', e.message);\r\n }\r\n });\r\n }\r\n }\r\n\r\n // Start processing from the root\r\n processNode(root);\r\n\r\n return Array.from(results);\r\n }\r\n */\r\n // <div x=1 y=2 role='combobox'> </div>\r\n function findDropdowns() {\r\n const dropdowns = [];\r\n \r\n // Native select elements\r\n dropdowns.push(...getAllElementsIncludingShadow('select'));\r\n \r\n // Elements with dropdown roles that don't have <input>..</input>\r\n const roleElements = getAllElementsIncludingShadow('[role=\"combobox\"], [role=\"listbox\"], [role=\"dropdown\"], [role=\"option\"]').filter(el => {\r\n return el.tagName.toLowerCase() !== 'input' || ![\"button\", \"checkbox\", \"radio\"].includes(el.getAttribute(\"type\"));\r\n });\r\n dropdowns.push(...roleElements);\r\n \r\n // Common dropdown class patterns\r\n const dropdownPattern = /.*(dropdown|select|combobox).*/i;\r\n const elements = getAllElementsIncludingShadow('*');\r\n const dropdownClasses = Array.from(elements).filter(el => {\r\n const hasDropdownClass = dropdownPattern.test(el.className);\r\n const validTag = ['li', 'ul', 'span', 'div', 'p'].includes(el.tagName.toLowerCase());\r\n // const comboboxTag = el.tagName.toLowerCase().includes('combobox-item');\r\n const style = window.getComputedStyle(el); \r\n const result = hasDropdownClass && validTag && style.cursor === 'pointer';\r\n return result;\r\n });\r\n \r\n dropdowns.push(...dropdownClasses);\r\n \r\n // Elements with aria-haspopup attribute\r\n dropdowns.push(...getAllElementsIncludingShadow('[aria-haspopup=\"true\"], [aria-haspopup=\"listbox\"]'));\r\n\r\n dropdowns.push(...getAllElementsIncludingShadow('nav ul li'));\r\n\r\n return dropdowns;\r\n }\r\n\r\n function findClickables() {\r\n const clickables = [];\r\n \r\n const checkboxPattern = /checkbox/i;\r\n // Collect all clickable elements first\r\n const nativeLinks = [...getAllElementsIncludingShadow('a[href]')];\r\n const nativeButtons = [...getAllElementsIncludingShadow('button')];\r\n const inputButtons = [...getAllElementsIncludingShadow('input[type=\"button\"], input[type=\"submit\"], input[type=\"reset\"]')];\r\n const roleButtons = [...getAllElementsIncludingShadow('[role=\"button\"]')];\r\n // const tabbable = [...getAllElementsIncludingShadow('[tabindex=\"0\"]')];\r\n const clickHandlers = [...getAllElementsIncludingShadow('[onclick]')];\r\n const dropdowns = findDropdowns();\r\n const nativeCheckboxes = [...getAllElementsIncludingShadow('input[type=\"checkbox\"]')]; \r\n const fauxCheckboxes = getAllElementsIncludingShadow('*').filter(el => {\r\n if (checkboxPattern.test(el.className)) {\r\n const realCheckboxes = getAllElementsIncludingShadow('input[type=\"checkbox\"]', el);\r\n if (realCheckboxes.length === 1) {\r\n const boundingRect = realCheckboxes[0].getBoundingClientRect();\r\n return boundingRect.width <= 1 && boundingRect.height <= 1 \r\n }\r\n }\r\n return false;\r\n });\r\n const nativeRadios = [...getAllElementsIncludingShadow('input[type=\"radio\"]')];\r\n const toggles = findToggles();\r\n const pointerElements = findElementsWithPointer();\r\n // Add all elements at once\r\n clickables.push(\r\n ...nativeLinks,\r\n ...nativeButtons,\r\n ...inputButtons,\r\n ...roleButtons,\r\n // ...tabbable,\r\n ...clickHandlers,\r\n ...dropdowns,\r\n ...nativeCheckboxes,\r\n ...fauxCheckboxes,\r\n ...nativeRadios,\r\n ...toggles,\r\n ...pointerElements\r\n );\r\n\r\n // Only uniquify once at the end\r\n return clickables; // Let findElements handle the uniquification\r\n }\r\n\r\n function findToggles() {\r\n const toggles = [];\r\n const checkboxes = getAllElementsIncludingShadow('input[type=\"checkbox\"]');\r\n const togglePattern = /switch|toggle|slider/i;\r\n\r\n checkboxes.forEach(checkbox => {\r\n let isToggle = false;\r\n\r\n // Check the checkbox itself\r\n if (togglePattern.test(checkbox.className) || togglePattern.test(checkbox.getAttribute('role') || '')) {\r\n isToggle = true;\r\n }\r\n\r\n // Check parent elements (up to 3 levels)\r\n if (!isToggle) {\r\n let element = checkbox;\r\n for (let i = 0; i < 3; i++) {\r\n const parent = element.parentElement;\r\n if (!parent) break;\r\n\r\n const className = parent.className || '';\r\n const role = parent.getAttribute('role') || '';\r\n\r\n if (togglePattern.test(className) || togglePattern.test(role)) {\r\n isToggle = true;\r\n break;\r\n }\r\n element = parent;\r\n }\r\n }\r\n\r\n // Check next sibling\r\n if (!isToggle) {\r\n const nextSibling = checkbox.nextElementSibling;\r\n if (nextSibling) {\r\n const className = nextSibling.className || '';\r\n const role = nextSibling.getAttribute('role') || '';\r\n if (togglePattern.test(className) || togglePattern.test(role)) {\r\n isToggle = true;\r\n }\r\n }\r\n }\r\n\r\n if (isToggle) {\r\n toggles.push(checkbox);\r\n }\r\n });\r\n\r\n return toggles;\r\n }\r\n\r\n function findNonInteractiveElements() {\r\n // Get all elements in the document\r\n const all = Array.from(getAllElementsIncludingShadow('*'));\r\n \r\n // Filter elements based on Python implementation rules\r\n return all.filter(element => {\r\n if (!element.firstElementChild) {\r\n const tag = element.tagName.toLowerCase(); \r\n if (!['select', 'button', 'a'].includes(tag)) {\r\n const validTags = ['p', 'span', 'div', 'input', 'textarea'].includes(tag) || /^h\\d$/.test(tag) || /text/.test(tag);\r\n const boundingRect = element.getBoundingClientRect();\r\n return validTags && boundingRect.height > 1 && boundingRect.width > 1;\r\n }\r\n }\r\n return false;\r\n });\r\n }\r\n\r\n\r\n\r\n // export function findNonInteractiveElements() {\r\n // const all = [];\r\n // try {\r\n // const elements = getAllElementsIncludingShadow('*');\r\n // all.push(...elements);\r\n // } catch (e) {\r\n // console.warn('Error getting elements:', e);\r\n // }\r\n \r\n // console.debug('Total elements found:', all.length);\r\n \r\n // return all.filter(element => {\r\n // try {\r\n // const tag = element.tagName.toLowerCase(); \r\n\r\n // // Special handling for input elements\r\n // if (tag === 'input' || tag === 'textarea') {\r\n // const boundingRect = element.getBoundingClientRect();\r\n // const value = element.value || '';\r\n // const placeholder = element.placeholder || '';\r\n // return boundingRect.height > 1 && \r\n // boundingRect.width > 1 && \r\n // (value.trim() !== '' || placeholder.trim() !== '');\r\n // }\r\n\r\n \r\n // // Check if it's a valid tag for text content\r\n // const validTags = ['p', 'span', 'div', 'label', 'th', 'td', 'li', 'button', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'a', 'select'].includes(tag) || \r\n // /^h\\d$/.test(tag) || \r\n // /text/.test(tag);\r\n\r\n // const boundingRect = element.getBoundingClientRect();\r\n\r\n // // Get direct text content, excluding child element text\r\n // let directText = '';\r\n // for (const node of element.childNodes) {\r\n // // Only include text nodes (nodeType 3)\r\n // if (node.nodeType === 3) {\r\n // directText += node.textContent || '';\r\n // }\r\n // }\r\n \r\n // // If no direct text and it's a table cell or heading, check label content\r\n // if (!directText.trim() && (tag === 'th' || tag === 'td' || tag === 'h1')) {\r\n // const labels = element.getElementsByTagName('label');\r\n // for (const label of labels) {\r\n // directText += label.textContent || '';\r\n // }\r\n // }\r\n\r\n // // If still no text and it's a heading, get all text content\r\n // if (!directText.trim() && tag === 'h1') {\r\n // directText = element.textContent || '';\r\n // }\r\n\r\n // directText = directText.trim();\r\n\r\n // // Debug logging\r\n // if (directText) {\r\n // console.debugg('Text element found:', {\r\n // tag,\r\n // text: directText,\r\n // dimensions: boundingRect,\r\n // element\r\n // });\r\n // }\r\n\r\n // return validTags && \r\n // boundingRect.height > 1 && \r\n // boundingRect.width > 1 && \r\n // directText !== '';\r\n \r\n // } catch (e) {\r\n // console.warn('Error processing element:', e);\r\n // return false;\r\n // }\r\n // });\r\n // }\r\n\r\n\r\n\r\n\r\n\r\n function findElementsWithPointer() {\r\n const elements = [];\r\n const allElements = getAllElementsIncludingShadow('*');\r\n \r\n console.log('Checking elements with pointer style...');\r\n \r\n allElements.forEach(element => {\r\n // Skip SVG elements for now\r\n if (element instanceof SVGElement || element.tagName.toLowerCase() === 'svg') {\r\n return;\r\n }\r\n \r\n const style = window.getComputedStyle(element);\r\n if (style.cursor === 'pointer') {\r\n elements.push(element);\r\n }\r\n });\r\n \r\n console.log(`Found ${elements.length} elements with pointer cursor`);\r\n return elements;\r\n }\r\n\r\n function findCheckables() {\r\n const elements = [];\r\n\r\n elements.push(...getAllElementsIncludingShadow('input[type=\"checkbox\"]'));\r\n elements.push(...getAllElementsIncludingShadow('input[type=\"radio\"]'));\r\n const all_elements = getAllElementsIncludingShadow('label');\r\n const radioClasses = Array.from(all_elements).filter(el => {\r\n return /.*radio.*/i.test(el.className); \r\n });\r\n elements.push(...radioClasses);\r\n return elements;\r\n }\r\n\r\n function findFillables() {\r\n const elements = [];\r\n\r\n const inputs = [...getAllElementsIncludingShadow('input:not([type=\"radio\"]):not([type=\"checkbox\"])')];\r\n console.log('Found inputs:', inputs.length, inputs);\r\n elements.push(...inputs);\r\n \r\n const textareas = [...getAllElementsIncludingShadow('textarea')];\r\n console.log('Found textareas:', textareas.length);\r\n elements.push(...textareas);\r\n \r\n const editables = [...getAllElementsIncludingShadow('[contenteditable=\"true\"]')];\r\n console.log('Found editables:', editables.length);\r\n elements.push(...editables);\r\n\r\n return elements;\r\n }\n\n /**\r\n * Finds the first element matching a CSS selector, traversing Shadow DOM if necessary\r\n * @param {string} selector - CSS selector to search for\r\n * @param {Element} [root=document] - Root element to start searching from\r\n * @returns {Element|null} - The first matching element or null if not found\r\n */\r\n function querySelectorShadow(selector, root = document) {\r\n // First try to find in light DOM\r\n let element = root.querySelector(selector);\r\n if (element) return element;\r\n \r\n // Get all elements with shadow root\r\n const shadowElements = Array.from(root.querySelectorAll('*'))\r\n .filter(el => el.shadowRoot);\r\n \r\n // Search through each shadow root until we find a match\r\n for (const el of shadowElements) {\r\n element = querySelectorShadow(selector, el.shadowRoot);\r\n if (element) return element;\r\n }\r\n \r\n return null;\r\n }\r\n\r\n const getElementByXPathOrCssSelector = (element_info) => {\r\n let element;\r\n\r\n if (element_info.xpath) { //try xpath if exists\r\n element = document.evaluate(\r\n element_info.xpath, \r\n document, \r\n null, \r\n XPathResult.FIRST_ORDERED_NODE_TYPE, \r\n null\r\n ).singleNodeValue;\r\n \r\n if (!element) {\r\n console.warn('Failed to find element with xpath:', element_info.xpath);\r\n }\r\n }\r\n else { //try CSS selector\r\n element = querySelectorShadow(element_info.css_selector);\r\n // console.log('found element by CSS elector: ', element);\r\n if (!element) {\r\n console.warn('Failed to find element with CSS selector:', element_info.css_selector);\r\n }\r\n }\r\n\r\n return element;\r\n };\r\n\r\n function generateXPath(element) {\r\n if (!element || element.getRootNode() instanceof ShadowRoot) return '';\r\n \r\n // If element has an id, use that (it's unique and shorter)\r\n if (element.id) {\r\n return `//*[@id=\"${element.id}\"]`;\r\n }\r\n \r\n const parts = [];\r\n let current = element;\r\n \r\n while (current && current.nodeType === Node.ELEMENT_NODE) {\r\n let index = 1;\r\n let sibling = current.previousSibling;\r\n \r\n while (sibling) {\r\n if (sibling.nodeType === Node.ELEMENT_NODE && sibling.tagName === current.tagName) {\r\n index++;\r\n }\r\n sibling = sibling.previousSibling;\r\n }\r\n \r\n const tagName = current.tagName.toLowerCase();\r\n parts.unshift(`${tagName}[${index}]`);\r\n current = current.parentNode;\r\n }\r\n \r\n return '/' + parts.join('/');\r\n }\r\n\r\n function isDecendent(parent, child) {\r\n let element = child;\r\n while (element.nodeType === Node.ELEMENT_NODE) { \r\n \r\n if (element.assignedSlot) {\r\n element = element.assignedSlot;\r\n }\r\n else {\r\n element = element.parentNode;\r\n // Check if we're at a shadow root\r\n if (element.nodeType !== Node.ELEMENT_NODE && element.getRootNode() instanceof ShadowRoot) {\r\n // Get the shadow root's host element\r\n element = element.getRootNode().host; \r\n }\r\n }\r\n if (element === parent)\r\n return true;\r\n }\r\n return false;\r\n }\r\n\r\n function generateCssPath(element) {\r\n const path = [];\r\n while (element.nodeType === Node.ELEMENT_NODE) { \r\n let selector = element.nodeName.toLowerCase();\r\n \r\n // if (element.id) {\r\n // //escape special characters\r\n // const normalized_id = element.id.replace(/[:;.#()[\\]!@$%^&*]/g, '\\\\$&');\r\n // selector = `#${normalized_id}`;\r\n // path.unshift(selector);\r\n // break;\r\n // } \r\n \r\n let sibling = element;\r\n let nth = 1;\r\n while (sibling = sibling.previousElementSibling) {\r\n if (sibling.nodeName.toLowerCase() === selector) nth++;\r\n }\r\n sibling = element;\r\n let singleChild = true;\r\n while (sibling = sibling.nextElementSibling) {\r\n if (sibling.nodeName.toLowerCase() === selector) {\r\n singleChild = false;\r\n break;\r\n }\r\n }\r\n if (nth > 1 || !singleChild) selector += `:nth-of-type(${nth})`;\r\n \r\n path.unshift(selector);\r\n\r\n if (element.assignedSlot) {\r\n element = element.assignedSlot;\r\n }\r\n else {\r\n element = element.parentNode;\r\n // Check if we're at a shadow root\r\n if (element.nodeType !== Node.ELEMENT_NODE && element.getRootNode() instanceof ShadowRoot) {\r\n // Get the shadow root's host element\r\n element = element.getRootNode().host; \r\n }\r\n }\r\n }\r\n return path.join(' > ');\r\n }\r\n\r\n function cleanHTML(rawHTML) {\r\n const parser = new DOMParser();\r\n const doc = parser.parseFromString(rawHTML, \"text/html\");\r\n\r\n function cleanElement(element) {\r\n const allowedAttributes = new Set([\r\n \"role\",\r\n \"type\",\r\n \"class\",\r\n \"href\",\r\n \"alt\",\r\n \"title\",\r\n \"readonly\",\r\n \"checked\",\r\n \"enabled\",\r\n \"disabled\",\r\n ]);\r\n\r\n [...element.attributes].forEach(attr => {\r\n const name = attr.name.toLowerCase();\r\n const value = attr.value;\r\n\r\n const isTestAttribute = /^(testid|test-id|data-test-id)$/.test(name);\r\n const isDataAttribute = name.startsWith(\"data-\") && value;\r\n const isBooleanAttribute = [\"readonly\", \"checked\", \"enabled\", \"disabled\"].includes(name);\r\n\r\n if (!allowedAttributes.has(name) && !isDataAttribute && !isTestAttribute && !isBooleanAttribute) {\r\n element.removeAttribute(name);\r\n }\r\n });\r\n\r\n // Handle SVG content - more aggressive replacement\r\n if (element.tagName.toLowerCase() === \"svg\") {\r\n // Remove all attributes except class and role\r\n [...element.attributes].forEach(attr => {\r\n const name = attr.name.toLowerCase();\r\n if (name !== \"class\" && name !== \"role\") {\r\n element.removeAttribute(name);\r\n }\r\n });\r\n element.innerHTML = \"CONTENT REMOVED\";\r\n } else {\r\n // Recursively clean child elements\r\n Array.from(element.children).forEach(cleanElement);\r\n }\r\n\r\n // Only remove empty elements that aren't semantic or icon elements\r\n const keepEmptyElements = ['i', 'span', 'svg', 'button', 'input'];\r\n if (!keepEmptyElements.includes(element.tagName.toLowerCase()) && \r\n !element.children.length && \r\n !element.textContent.trim()) {\r\n element.remove();\r\n }\r\n }\r\n\r\n // Process all elements in the document body\r\n Array.from(doc.body.children).forEach(cleanElement);\r\n return doc.body.innerHTML;\r\n }\r\n\r\n function getElementInfo(element, index) {\r\n\r\n const xpath = generateXPath(element);\r\n const css_selector = generateCssPath(element);\r\n\r\n // Return element info with pre-calculated values\r\n return new ElementInfo(element, index, {\r\n tag: element.tagName.toLowerCase(),\r\n type: element.type || '',\r\n text: element.innerText, //getTextContent(element),\r\n html: cleanHTML(element.outerHTML),\r\n xpath: xpath,\r\n css_selector: css_selector,\r\n bounding_box: element.getBoundingClientRect()\r\n });\r\n }\r\n\r\n\r\n\r\n\r\n const filterZeroDimensions = (elementInfo) => {\r\n const rect = elementInfo.bounding_box;\r\n //single pixel elements are typically faux controls and should be filtered too\r\n const hasSize = rect.width > 1 && rect.height > 1;\r\n const style = window.getComputedStyle(elementInfo.element);\r\n const isVisible = style.display !== 'none' && style.visibility !== 'hidden';\r\n \r\n if (!hasSize || !isVisible) {\r\n // if (elementInfo.element.isConnected) {\r\n // console.log('Filtered out invisible/zero-size element:', {\r\n // tag: elementInfo.tag,\r\n // xpath: elementInfo.xpath,\r\n // element: elementInfo.element,\r\n // hasSize,\r\n // isVisible,\r\n // dimensions: rect\r\n // });\r\n // }\r\n return false;\r\n }\r\n return true;\r\n };\r\n\r\n function uniquifyElements(elements) {\r\n const seen = new Set();\r\n console.log(`Starting uniquification with ${elements.length} elements`);\r\n // First filter out elements with zero dimensions\r\n const nonZeroElements = elements.filter(filterZeroDimensions);\r\n // sort by CSS selector depth so parents are processed first\r\n nonZeroElements.sort((a, b) => a.getDepth() - b.getDepth());\r\n console.log(`After dimension filtering: ${nonZeroElements.length} elements remain (${elements.length - nonZeroElements.length} removed)`);\r\n \r\n // nonZeroElements.forEach(element_info => seen.add(element_info.css_selector));\r\n \r\n // nonZeroElements.forEach(info => { \r\n // console.log(`Element ${info.index}:`, info); \r\n // });\r\n \r\n const filteredByParent = nonZeroElements.filter(element_info => {\r\n const parent = findClosestParent(seen, element_info);\r\n const keep = parent == null || shouldKeepNestedElement(element_info, parent);\r\n // console.log(\"node \", element_info.index, \": keep=\", keep, \" parent=\", parent);\r\n // if (!keep && !element_info.xpath) {\r\n // console.log(\"Filtered out element \", element_info,\" because it's a nested element of \", parent);\r\n // }\r\n if (keep)\r\n seen.add(element_info.css_selector);\r\n\r\n return keep;\r\n });\r\n\r\n console.log(`After parent/child filtering: ${filteredByParent.length} elements remain (${nonZeroElements.length - filteredByParent.length} removed)`);\r\n\r\n // Final overlap filtering\r\n const filteredResults = filteredByParent.filter(element => {\r\n // Look for any element that came BEFORE this one in the array\r\n const hasEarlierOverlap = filteredByParent.some(other => {\r\n // Only check elements that came before (lower index)\r\n if (filteredByParent.indexOf(other) >= filteredByParent.indexOf(element)) {\r\n return false;\r\n }\r\n \r\n return areElementsOverlapping(element, other);\r\n });\r\n\r\n // Keep element if it has no earlier overlapping elements\r\n return !hasEarlierOverlap;\r\n });\r\n \r\n console.log(`After filtering: ${filteredResults.length} (${filteredByParent.length - filteredResults.length} removed by overlap)`);\r\n \r\n const nonOverlaidElements = filteredResults.filter(element => {\r\n return !isOverlaid(element);\r\n });\r\n\r\n console.log(`Final elements after overlay removal: ${nonOverlaidElements.length} (${filteredResults.length - nonOverlaidElements.length} removed)`);\r\n\r\n //remove bogus links\r\n // const finalResults = filteredResults.filter(elementInfo => {\r\n // //ignore non-link elements\r\n // if (elementInfo.tag !== 'a')\r\n // return true;\r\n // const special_link = elementInfo.element.getAttribute('data-special-link');\r\n // return !special_link || special_link!=='true';\r\n // });\r\n\r\n // further cleanup of of the element html to remove any element that we have already seen\r\n \r\n \r\n // for debugging purposes, add a data-probolabs_index attribute to the element\r\n // filteredResults.forEach((elementInfo, index) => {\r\n // // elementInfo.index = index.toString();\r\n // const foundElement = elementInfo.element; //getElementByXPathOrCssSelector(element);\r\n // if (foundElement) {\r\n // foundElement.dataset.probolabs_index = index.toString();\r\n // }\r\n // });\r\n\r\n // final path cleanup the html\r\n // filteredResults.forEach(elementInfo => {\r\n // const foundElement = elementInfo.element; //getElementByXPathOrCssSelector(element);\r\n // if (foundElement) {\r\n // // const parser = new DOMParser();\r\n // // const doc = parser.parseFromString(foundElement.outerHTML, \"text/html\");\r\n // // doc.querySelectorAll('[data-probolabs_index]').forEach(el => {\r\n // // if (el.dataset.probolabs_index !== element.index) {\r\n // // el.remove();\r\n // // }\r\n // // });\r\n // // // Get the first element from the processed document\r\n // // const container = doc.body.firstElementChild;\r\n // const clone = foundElement.cloneNode(false); // false = don't clone children\r\n // elementInfo.element.html = clone.outerHTML;\r\n // }\r\n // });\r\n\r\n return nonOverlaidElements;\r\n\r\n\r\n }\r\n\r\n\r\n\r\n const areElementsOverlapping = (element1, element2) => {\r\n if (element1.css_selector === element2.css_selector) {\r\n return true;\r\n }\r\n \r\n const box1 = element1.bounding_box;\r\n const box2 = element2.bounding_box;\r\n \r\n return box1.x === box2.x &&\r\n box1.y === box2.y &&\r\n box1.width === box2.width &&\r\n box1.height === box2.height;\r\n // element1.text === element2.text &&\r\n // element2.tag === 'a';\r\n };\r\n\r\n function findClosestParent(seen, element_info) { \r\n // //Use element child/parent queries\r\n // let parent = element_info.element.parentNode;\r\n // if (parent.getRootNode() instanceof ShadowRoot) {\r\n // // Get the shadow root's host element\r\n // parent = parent.getRootNode().host; \r\n // }\r\n\r\n // while (parent.nodeType === Node.ELEMENT_NODE) { \r\n // const css_selector = generateCssPath(parent);\r\n // if (seen.has(css_selector)) {\r\n // console.log(\"element \", element_info, \" closest parent is \", parent)\r\n // return parent; \r\n // }\r\n // parent = parent.parentNode;\r\n // if (parent.getRootNode() instanceof ShadowRoot) {\r\n // // Get the shadow root's host element\r\n // parent = parent.getRootNode().host; \r\n // }\r\n // }\r\n\r\n // Split the xpath into segments\r\n const segments = element_info.css_selector.split(' > ');\r\n \r\n // Try increasingly shorter paths until we find one in the seen set\r\n for (let i = segments.length - 1; i > 0; i--) {\r\n const parentPath = segments.slice(0, i).join(' > ');\r\n if (seen.has(parentPath)) {\r\n return parentPath;\r\n }\r\n }\r\n\r\n return null;\r\n }\r\n\r\n function shouldKeepNestedElement(elementInfo, parentPath) {\r\n let result = false;\r\n const parentSegments = parentPath.split(' > ');\r\n\r\n const isParentLink = /^a(:nth-of-type\\(\\d+\\))?$/.test(parentSegments[parentSegments.length - 1]);\r\n if (isParentLink) {\r\n return false; \r\n }\r\n // If this is a checkbox/radio input\r\n if (elementInfo.tag === 'input' && \r\n (elementInfo.type === 'checkbox' || elementInfo.type === 'radio')) {\r\n \r\n // Check if parent is a label by looking at the parent xpath's last segment\r\n \r\n const isParentLabel = /^label(:nth-of-type\\(\\d+\\))?$/.test(parentSegments[parentSegments.length - 1]);\r\n \r\n // If parent is a label, don't keep the input (we'll keep the label instead)\r\n if (isParentLabel) {\r\n return false;\r\n }\r\n }\r\n \r\n // Keep all other form controls and dropdown items\r\n if (isFormControl(elementInfo) || isDropdownItem(elementInfo)) {\r\n result = true;\r\n }\r\n \r\n // console.log(`shouldKeepNestedElement: ${elementInfo.tag} ${elementInfo.text} ${elementInfo.xpath} -> ${parentXPath} -> ${result}`);\r\n return result;\r\n }\r\n\r\n function isOverlaid(elementInfo) {\r\n const element = elementInfo.element;\r\n const boundingRect = element.getBoundingClientRect();\r\n \r\n // for (let x of [boundingRect.left, boundingRect.left + boundingRect.width]) {\r\n // for (let y of [boundingRect.top, boundingRect.top + boundingRect.height]) {\r\n const elementAtPoint = element.ownerDocument.elementFromPoint(boundingRect.x + boundingRect.width/2, boundingRect.y + boundingRect.height/2); \r\n \r\n return elementAtPoint && elementAtPoint !== element && !isDecendent(element, elementAtPoint) && !isDecendent(elementAtPoint, element);\r\n // return true;\r\n // }\r\n // }\r\n // return false;\r\n }\r\n\r\n // Helper function to check if element is a form control\r\n function isFormControl(elementInfo) {\r\n return /^(input|select|textarea|button|label)$/i.test(elementInfo.tag);\r\n }\r\n\r\n const isDropdownItem = (elementInfo) => {\r\n const dropdownPatterns = [\r\n /dropdown[-_]?item/i, // matches: dropdown-item, dropdownitem, dropdown_item\r\n /menu[-_]?item/i, // matches: menu-item, menuitem, menu_item\r\n /dropdown[-_]?link/i, // matches: dropdown-link, dropdownlink, dropdown_link\r\n /list[-_]?item/i, // matches: list-item, listitem, list_item\r\n /select[-_]?item/i, // matches: select-item, selectitem, select_item \r\n ];\r\n\r\n const rolePatterns = [\r\n /menu[-_]?item/i, // matches: menuitem, menu-item\r\n /option/i, // matches: option\r\n /list[-_]?item/i, // matches: listitem, list-item\r\n /tree[-_]?item/i // matches: treeitem, tree-item\r\n ];\r\n\r\n const hasMatchingClass = elementInfo.element.className && \r\n dropdownPatterns.some(pattern => \r\n pattern.test(elementInfo.element.className)\r\n );\r\n\r\n const hasMatchingRole = elementInfo.element.getAttribute('role') && \r\n rolePatterns.some(pattern => \r\n pattern.test(elementInfo.element.getAttribute('role'))\r\n );\r\n\r\n return hasMatchingClass || hasMatchingRole;\r\n };\n\n const highlight = {\r\n execute: async function(elementTypes, handleScroll=false) {\r\n const elements = await findElements(elementTypes);\r\n highlightElements(elements);\r\n return elements;\r\n },\r\n\r\n unexecute: function(handleScroll=false) {\r\n unhighlightElements(handleScroll);\r\n },\r\n\r\n generateJSON: async function() {\r\n const json = {};\r\n await Promise.all(Object.values(ElementTag).map(async elementType => {\r\n const elements = await findElements(elementType);\r\n json[elementType] = elements;\r\n }));\r\n\r\n // Serialize the JSON object\r\n const jsonString = JSON.stringify(json, null, 4); // Pretty print with 4 spaces\r\n\r\n console.log(`JSON: ${jsonString}`);\r\n return jsonString;\r\n },\r\n\r\n getElementInfo\r\n };\r\n\r\n\r\n function unhighlightElements(handleScroll=false) {\r\n const documents = getAllFrames();\r\n documents.forEach(doc => {\r\n const overlay = doc.getElementById('highlight-overlay');\r\n if (overlay) {\r\n if (handleScroll) {\r\n // Remove event listeners\r\n doc.removeEventListener('scroll', overlay.scrollHandler, true);\r\n doc.removeEventListener('resize', overlay.resizeHandler);\r\n }\r\n overlay.remove();\r\n }\r\n });\r\n }\r\n\r\n\r\n\r\n\r\n async function findElements(elementTypes) {\r\n const typesArray = Array.isArray(elementTypes) ? elementTypes : [elementTypes];\r\n console.log('Starting element search for types:', typesArray);\r\n\r\n const elements = [];\r\n typesArray.forEach(elementType => {\r\n if (elementType === ElementTag.FILLABLE) {\r\n elements.push(...findFillables());\r\n }\r\n if (elementType === ElementTag.SELECTABLE) {\r\n elements.push(...findDropdowns());\r\n }\r\n if (elementType === ElementTag.CLICKABLE) {\r\n elements.push(...findClickables());\r\n elements.push(...findToggles());\r\n elements.push(...findCheckables());\r\n }\r\n if (elementType === ElementTag.NON_INTERACTIVE_ELEMENT) {\r\n elements.push(...findNonInteractiveElements());\r\n }\r\n });\r\n\r\n // console.log('Before uniquify:', elements.length);\r\n const elementsWithInfo = elements.map((element, index) => \r\n getElementInfo(element, index)\r\n );\r\n \r\n const uniqueElements = uniquifyElements(elementsWithInfo);\r\n console.log(`Found ${uniqueElements.length} elements:`);\r\n const visibleElements = uniqueElements.filter(elementInfo => \r\n getComputedStyle(elementInfo.element).display !== 'none'\r\n );\r\n console.log(`Out of which ${visibleElements.length} elements are visible:`);\r\n visibleElements.forEach(info => {\r\n console.log(`Element ${info.index}:`, info);\r\n });\r\n \r\n return visibleElements;\r\n }\r\n\r\n // elements is an array of objects with index, xpath\r\n function highlightElements(elements) {\r\n // console.log('Starting highlight for elements:', elements);\r\n \r\n // Create overlay if it doesn't exist and store it in a dictionary\r\n const documents = getAllFrames(); \r\n let overlays = {};\r\n documents.forEach(doc => {\r\n let overlay = doc.getElementById('highlight-overlay');\r\n if (!overlay) {\r\n overlay = doc.createElement('div');\r\n overlay.id = 'highlight-overlay';\r\n overlay.style.cssText = `\r\n position: fixed;\r\n top: 0;\r\n left: 0;\r\n width: 100%;\r\n height: 100%;\r\n pointer-events: none;\r\n z-index: 10000;\r\n `;\r\n doc.body.appendChild(overlay);\r\n }\r\n overlays[doc.documentURI] = overlay;\r\n });\r\n \r\n\r\n const updateHighlights = (doc = null) => {\r\n if (doc) {\r\n overlays[doc.documentURI].innerHTML = '';\r\n } else {\r\n Object.values(overlays).forEach(overlay => { overlay.innerHTML = ''; });\r\n } \r\n elements.forEach(elementInfo => {\r\n // console.log('updateHighlights-Processing elementInfo:', elementInfo);\r\n let element = elementInfo.element; //getElementByXPathOrCssSelector(elementInfo);\r\n if (!element) {\r\n element = getElementByXPathOrCssSelector(elementInfo);\r\n if (!element)\r\n return;\r\n }\r\n \r\n //if highlights requested for a specific doc, skip unrelated elements\r\n if (doc && element.ownerDocument !== doc) {\r\n console.log(\"skipped element \", element, \" since it doesn't belong to document \", doc);\r\n return;\r\n }\r\n\r\n const rect = element.getBoundingClientRect();\r\n // console.log('Element rect:', elementInfo.tag, rect);\r\n \r\n if (rect.width === 0 || rect.height === 0) {\r\n console.warn('Element has zero dimensions:', elementInfo);\r\n return;\r\n }\r\n \r\n // Create border highlight (red rectangle)\r\n // use ownerDocument to support iframes/frames\r\n const highlight = element.ownerDocument.createElement('div');\r\n highlight.style.cssText = `\r\n position: fixed;\r\n left: ${rect.x}px;\r\n top: ${rect.y}px;\r\n width: ${rect.width}px;\r\n height: ${rect.height}px;\r\n border: 1px solid rgb(255, 0, 0);\r\n transition: all 0.2s ease-in-out;\r\n `;\r\n\r\n // Create index label container - now positioned to the right and slightly up\r\n const labelContainer = element.ownerDocument.createElement('div');\r\n labelContainer.style.cssText = `\r\n position: absolute;\r\n right: -10px; /* Offset to the right */\r\n top: -10px; /* Offset upwards */\r\n padding: 4px;\r\n background-color: rgba(255, 255, 0, 0.6);\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n `;\r\n\r\n const text = element.ownerDocument.createElement('span');\r\n text.style.cssText = `\r\n color: rgb(0, 0, 0, 0.8);\r\n font-family: 'Courier New', Courier, monospace;\r\n font-size: 12px;\r\n font-weight: bold;\r\n line-height: 1;\r\n `;\r\n text.textContent = elementInfo.index;\r\n \r\n labelContainer.appendChild(text);\r\n highlight.appendChild(labelContainer); \r\n overlays[element.ownerDocument.documentURI].appendChild(highlight);\r\n });\r\n };\r\n\r\n // Initial highlight\r\n updateHighlights();\r\n\r\n documents.forEach(doc => {\r\n // Update highlights on scroll and resize\r\n console.log('registering scroll and resize handlers for document: ', doc);\r\n const scrollHandler = () => {\r\n requestAnimationFrame(() => updateHighlights(doc));\r\n };\r\n const resizeHandler = () => {\r\n updateHighlights(doc);\r\n };\r\n doc.addEventListener('scroll', scrollHandler, true);\r\n doc.addEventListener('resize', resizeHandler);\r\n // Store event handlers for cleanup\r\n overlays[doc.documentURI].scrollHandler = scrollHandler;\r\n overlays[doc.documentURI].resizeHandler = resizeHandler;\r\n }); \r\n }\r\n\r\n // function unexecute() {\r\n // unhighlightElements();\r\n // }\r\n\r\n // Make it available globally for both Extension and Playwright\r\n if (typeof window !== 'undefined') {\r\n window.ProboLabs = {\r\n ElementTag,\r\n highlight,\r\n unhighlightElements,\r\n findElements,\r\n highlightElements,\r\n getElementInfo\r\n };\r\n }\n\n exports.findElements = findElements;\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
+ const ElementTag = {
3
+ CLICKABLE: "CLICKABLE", // button, link, toggle switch, checkbox, radio, dropdowns, clickable divs
4
+ FILLABLE: "FILLABLE", // input, textarea content_editable, date picker??
5
+ SELECTABLE: "SELECTABLE", // select
6
+ NON_INTERACTIVE_ELEMENT: 'NON_INTERACTIVE_ELEMENT',
7
+ };
8
+
9
+ var ProboLogLevel;
10
+ (function (ProboLogLevel) {
11
+ ProboLogLevel["DEBUG"] = "DEBUG";
12
+ ProboLogLevel["INFO"] = "INFO";
13
+ ProboLogLevel["LOG"] = "LOG";
14
+ ProboLogLevel["WARN"] = "WARN";
15
+ ProboLogLevel["ERROR"] = "ERROR";
16
+ })(ProboLogLevel || (ProboLogLevel = {}));
17
+ // Add a severity map for comparison
18
+ const LogLevelSeverity = {
19
+ [ProboLogLevel.DEBUG]: 0,
20
+ [ProboLogLevel.INFO]: 1,
21
+ [ProboLogLevel.LOG]: 2,
22
+ [ProboLogLevel.WARN]: 3,
23
+ [ProboLogLevel.ERROR]: 4,
24
+ };
25
+ class ProboLogger {
26
+ constructor(prefix, logLevel = ProboLogLevel.LOG) {
27
+ this.prefix = prefix;
28
+ this.logLevel = logLevel;
29
+ }
30
+ setLogLevel(level) {
31
+ this.logLevel = level;
32
+ console.log(`[${this.prefix}-INFO] Log level set to: ${ProboLogLevel[level]}`);
33
+ }
34
+ debug(...args) {
35
+ this.msg(ProboLogLevel.DEBUG, ...args);
36
+ }
37
+ info(...args) {
38
+ this.msg(ProboLogLevel.INFO, ...args);
39
+ }
40
+ log(...args) {
41
+ this.msg(ProboLogLevel.LOG, ...args);
42
+ }
43
+ warn(...args) {
44
+ this.msg(ProboLogLevel.WARN, ...args);
45
+ }
46
+ error(...args) {
47
+ this.msg(ProboLogLevel.ERROR, ...args);
48
+ }
49
+ msg(logLevel, ...args) {
50
+ if (LogLevelSeverity[logLevel] >= LogLevelSeverity[this.logLevel]) {
51
+ const now = new Date();
52
+ const timestamp = now.toLocaleString('en-GB', {
53
+ day: '2-digit',
54
+ month: 'short',
55
+ year: 'numeric',
56
+ hour: '2-digit',
57
+ minute: '2-digit',
58
+ second: '2-digit',
59
+ hour12: false
60
+ }).replace(',', '');
61
+ // const stack = new Error().stack;
62
+ // const callerLine = stack?.split('\n')[3];
63
+ // const callerInfo = callerLine?.match(/at\s+(?:(.+)\s+\()?(.+):(\d+):(\d+)\)?/);
64
+ // const [, fnName, file, line] = callerInfo || [];
65
+ // const fileInfo = file ? `${file.split('/').pop()}:${line}` : '';
66
+ // const functionInfo = fnName ? `${fnName}` : '';
67
+ // console.log(`[${timestamp}] ${logLevel} [${this.prefix}.${fileInfo}${functionInfo ? ` ${functionInfo}` : ''}]`, ...args);
68
+ console.log(`[${timestamp}] ${logLevel} [${this.prefix}]`, ...args);
69
+ }
70
+ }
71
+ }
72
+ const proboLogger = new ProboLogger('probolib');
73
+
74
+ // Utility functions for enhanced Playwright interactions
75
+ async function waitForNavigationToSettle(page, options = {}) {
76
+ const startTime = Date.now();
77
+ let navigationCount = 0;
78
+ let lastNavigationTime = null;
79
+ const onFrameNavigated = (frame) => {
80
+ if (frame === page.mainFrame()) {
81
+ navigationCount++;
82
+ lastNavigationTime = Date.now();
83
+ proboLogger.debug(`Navigation ${navigationCount} detected at +${(lastNavigationTime - startTime) / 1000}s`);
84
+ }
85
+ };
86
+ try {
87
+ page.on('framenavigated', onFrameNavigated);
88
+ await page.waitForTimeout(options.initialWait || 1500);
89
+ proboLogger.debug(`After initial wait (${(Date.now() - startTime) / 1000}s), navigation_count: ${navigationCount}`);
90
+ if (navigationCount > 0) {
91
+ while (true) {
92
+ const currentTime = Date.now();
93
+ if (lastNavigationTime && (currentTime - lastNavigationTime) > (options.navigationTimeout || 5000)) {
94
+ break;
95
+ }
96
+ if ((currentTime - startTime) > (options.totalTimeout || 10000)) {
97
+ break;
98
+ }
99
+ await page.waitForTimeout(100);
100
+ }
101
+ await page.waitForLoadState('domcontentloaded', { timeout: options.totalTimeout || 10000 });
102
+ proboLogger.debug(`Navigation settled in ${(Date.now() - startTime) / 1000}s`);
103
+ return true;
104
+ }
105
+ proboLogger.debug(`No navigation detected (${(Date.now() - startTime) / 1000}s)`);
106
+ return false;
107
+ }
108
+ finally {
109
+ page.removeListener('framenavigated', onFrameNavigated);
110
+ proboLogger.debug(`Navigation listener removed at ${(Date.now() - startTime) / 1000}s`);
111
+ }
112
+ }
113
+ async function stabilizePage(page) {
114
+ const startTime = Date.now();
115
+ proboLogger.debug('Starting page stabilization...');
116
+ await waitForNavigationToSettle(page);
117
+ const afterNavTime = Date.now();
118
+ proboLogger.debug(`Navigation phase: ${(afterNavTime - startTime) / 1000}s`);
119
+ await scrollToBottomRight(page);
120
+ const afterScrollTime = Date.now();
121
+ proboLogger.debug(`Scroll phase: ${(afterScrollTime - afterNavTime) / 1000}s`);
122
+ await waitForMutationsToSettle(page);
123
+ const totalTime = (Date.now() - startTime) / 1000;
124
+ proboLogger.debug(`Page stabilization complete in ${totalTime}s`);
125
+ }
126
+ async function enhancedClick(page, selector, options = {}) {
127
+ await stabilizePage(page);
128
+ proboLogger.debug(`Clicking element: ${selector}`);
129
+ await page.click(selector, { noWaitAfter: false, ...options });
130
+ await stabilizePage(page);
131
+ }
132
+ async function selectDropdownOption(page, selector, value) {
133
+ const locator = page.locator(selector);
134
+ const tagName = await locator.evaluate(el => el.tagName.toLowerCase());
135
+ const role = await locator.getAttribute('role');
136
+ if (tagName === 'option' || role === 'option') {
137
+ proboLogger.debug('selectDropdownOption: option tag/role detected');
138
+ await locator.click();
139
+ }
140
+ else if (tagName === 'select') {
141
+ // Handle native select element
142
+ proboLogger.debug('selectDropdownOption: simple select tag');
143
+ try {
144
+ // First try direct selectOption
145
+ await page.selectOption(selector, value);
146
+ }
147
+ catch (error) {
148
+ proboLogger.debug('selectDropdownOption: direct selectOption failed, trying alternative approach');
149
+ // If direct selection fails, try force-selecting
150
+ await page.evaluate(({ sel, val }) => {
151
+ const element = document.querySelector(sel);
152
+ if (element) {
153
+ const event = new Event('change', { bubbles: true });
154
+ element.value = Array.isArray(val) ? val[0] : val;
155
+ element.dispatchEvent(event);
156
+ }
157
+ }, { sel: selector, val: value });
158
+ }
159
+ }
160
+ else {
161
+ // Handle custom dropdown (non-select element)
162
+ //look for child <select> if exists
163
+ let listboxLocator = locator.locator('select');
164
+ let count = await listboxLocator.count();
165
+ if (count > 1)
166
+ throw new Error(`selectDropdownOption: ambiguous action found ${count} <select> elements`);
167
+ if (count === 1) { //delegate to child <select>
168
+ proboLogger.debug("selectDropdownOption: identified child element with <select>");
169
+ await listboxLocator.selectOption(value);
170
+ }
171
+ else { //no child <select> found
172
+ //click the original locator to expose the real listbox and options
173
+ proboLogger.debug("selectDropdownOption: clicking container element, looking for child element with role listbox");
174
+ await locator.click();
175
+ let container = locator;
176
+ count = 0;
177
+ if (role !== 'listbox') {
178
+ //search up to 7 levels above original locator
179
+ for (let i = 0; i < 7; i++) {
180
+ listboxLocator = container.getByRole('listbox');
181
+ count = await listboxLocator.count();
182
+ if (count >= 1)
183
+ break;
184
+ else {
185
+ proboLogger.debug(`selectDropdownOption: iteration #${i}: no listbox found`);
186
+ container = container.locator('xpath=..');
187
+ }
188
+ }
189
+ }
190
+ else
191
+ listboxLocator = container;
192
+ if (count !== 1)
193
+ throw new Error(`selectDropdownOption: found ${count} listbox locators`);
194
+ proboLogger.debug(`selectDropdownOption: clicking options ${value}`);
195
+ // Handle both string and array values
196
+ const values = Array.isArray(value) ? value : [value];
197
+ for (const val of values) {
198
+ const optionLocator = listboxLocator
199
+ .getByRole('option')
200
+ .getByText(new RegExp(`^${val}$`, 'i'));
201
+ count = await optionLocator.count();
202
+ if (count !== 1)
203
+ throw new Error(`selectDropdownOption: found ${count} option locators`);
204
+ await optionLocator.click();
205
+ }
206
+ }
207
+ }
208
+ }
209
+ async function getElementValue(page, selector) {
210
+ const element = page.locator(selector).first();
211
+ return await element.textContent() || '';
212
+ }
213
+ async function waitForMutationsToSettle(page) {
214
+ const startTime = Date.now();
215
+ await page.waitForTimeout(500);
216
+ proboLogger.debug(`Mutations settled in ${(Date.now() - startTime) / 1000}s`);
217
+ }
218
+ async function scrollToBottomRight(page) {
219
+ const startTime = Date.now();
220
+ proboLogger.debug("Starting page scroll...");
221
+ let lastHeight = await page.evaluate(() => document.documentElement.scrollHeight);
222
+ let lastWidth = await page.evaluate(() => document.documentElement.scrollWidth);
223
+ let iterationCount = 0;
224
+ while (true) {
225
+ iterationCount++;
226
+ let smoothingSteps = 0;
227
+ const initScrollY = await page.evaluate(() => window.scrollY);
228
+ const initScrollX = await page.evaluate(() => window.scrollX);
229
+ const maxScrollY = await page.evaluate(() => document.documentElement.scrollHeight);
230
+ const maxScrollX = await page.evaluate(() => document.documentElement.scrollWidth);
231
+ const maxClientY = await page.evaluate(() => document.documentElement.clientHeight);
232
+ const maxClientX = await page.evaluate(() => document.documentElement.clientWidth);
233
+ let currScrollX = initScrollX;
234
+ while (currScrollX <= (maxScrollX - maxClientX)) {
235
+ let currScrollY = initScrollY;
236
+ while (currScrollY <= (maxScrollY - maxClientY)) {
237
+ currScrollY += maxClientY;
238
+ await page.evaluate(({ x, y }) => window.scrollTo(x, y), { x: currScrollX, y: currScrollY });
239
+ await page.waitForTimeout(50);
240
+ smoothingSteps++;
241
+ }
242
+ currScrollX += maxClientX;
243
+ }
244
+ proboLogger.debug(`Iteration ${iterationCount}: ${smoothingSteps} smoothing steps in ${(Date.now() - startTime) / 1000}s`);
245
+ const newHeight = await page.evaluate(() => document.documentElement.scrollHeight);
246
+ const newWidth = await page.evaluate(() => document.documentElement.scrollWidth);
247
+ if (newHeight === lastHeight && newWidth === lastWidth) {
248
+ break;
249
+ }
250
+ proboLogger.debug(`Page dimensions updated at ${(Date.now() - startTime) / 1000}s, repeating scroll`);
251
+ lastHeight = newHeight;
252
+ lastWidth = newWidth;
253
+ }
254
+ await page.waitForTimeout(200);
255
+ await page.evaluate(() => window.scrollTo(0, 0));
256
+ await page.waitForTimeout(50);
257
+ proboLogger.debug(`Scroll completed in ${(Date.now() - startTime) / 1000}s`);
258
+ }
259
+ const PlaywrightAction = {
260
+ VISIT_BASE_URL: 'VISIT_BASE_URL',
261
+ VISIT_URL: 'VISIT_URL',
262
+ CLICK: 'CLICK',
263
+ FILL_IN: 'FILL_IN',
264
+ SELECT_DROPDOWN: 'SELECT_DROPDOWN',
265
+ SELECT_MULTIPLE_DROPDOWN: 'SELECT_MULTIPLE_DROPDOWN',
266
+ CHECK_CHECKBOX: 'CHECK_CHECKBOX',
267
+ SELECT_RADIO: 'SELECT_RADIO',
268
+ TOGGLE_SWITCH: 'TOGGLE_SWITCH',
269
+ TYPE_KEYS: 'TYPE_KEYS',
270
+ VALIDATE_EXACT_VALUE: 'VALIDATE_EXACT_VALUE',
271
+ VALIDATE_CONTAINS_VALUE: 'VALIDATE_CONTAINS_VALUE',
272
+ VALIDATE_URL: 'VALIDATE_URL'
273
+ };
274
+ const executePlaywrightAction = async (page, action, value, element_css_selector) => {
275
+ proboLogger.info('Executing playwright action:', { action, value, element_css_selector });
276
+ try {
277
+ switch (action) {
278
+ case PlaywrightAction.VISIT_BASE_URL:
279
+ await page.goto(value, { waitUntil: 'domcontentloaded' });
280
+ await stabilizePage(page);
281
+ break;
282
+ case PlaywrightAction.VISIT_URL:
283
+ await page.goto(value, { waitUntil: 'domcontentloaded' });
284
+ await stabilizePage(page);
285
+ break;
286
+ case PlaywrightAction.CLICK:
287
+ const element = page.locator(element_css_selector);
288
+ const tagName = await element.evaluate(el => el.tagName.toLowerCase());
289
+ if (tagName === 'select') {
290
+ // If it's a select element, use selectDropdownOption instead of click
291
+ await selectDropdownOption(page, element_css_selector, value);
292
+ }
293
+ else {
294
+ await enhancedClick(page, element_css_selector);
295
+ }
296
+ break;
297
+ case PlaywrightAction.FILL_IN:
298
+ await enhancedClick(page, element_css_selector);
299
+ await page.fill(element_css_selector, value);
300
+ break;
301
+ case PlaywrightAction.SELECT_DROPDOWN:
302
+ await stabilizePage(page);
303
+ await selectDropdownOption(page, element_css_selector, value);
304
+ break;
305
+ case PlaywrightAction.SELECT_MULTIPLE_DROPDOWN:
306
+ let options;
307
+ if (value.startsWith('[')) {
308
+ // Handle array-like string: "['option1', 'option2']" or '["option1", "option2"]'
309
+ try {
310
+ options = JSON.parse(value);
311
+ }
312
+ catch (_a) {
313
+ // Handle case where quotes might be inconsistent
314
+ options = value.slice(1, -1).split(',')
315
+ .map(opt => opt.trim().replace(/^['"]|['"]$/g, ''));
316
+ }
317
+ }
318
+ else {
319
+ // Handle comma-separated string: "option1, option2"
320
+ options = value.split(',').map(opt => opt.trim());
321
+ }
322
+ await selectDropdownOption(page, element_css_selector, options);
323
+ break;
324
+ case PlaywrightAction.CHECK_CHECKBOX:
325
+ await stabilizePage(page);
326
+ await page.setChecked(element_css_selector, value.toLowerCase() === 'true');
327
+ break;
328
+ case PlaywrightAction.SELECT_RADIO:
329
+ case PlaywrightAction.TOGGLE_SWITCH:
330
+ await enhancedClick(page, element_css_selector);
331
+ break;
332
+ case PlaywrightAction.TYPE_KEYS:
333
+ await stabilizePage(page);
334
+ await page.locator(element_css_selector).pressSequentially(value);
335
+ break;
336
+ case PlaywrightAction.VALIDATE_EXACT_VALUE:
337
+ await stabilizePage(page);
338
+ const exactValue = await getElementValue(page, element_css_selector);
339
+ if (exactValue !== value) {
340
+ proboLogger.info(`Validation FAIL: expected ${value} but got ${exactValue}`);
341
+ return false;
342
+ }
343
+ proboLogger.info('Validation PASS');
344
+ return true;
345
+ case PlaywrightAction.VALIDATE_CONTAINS_VALUE:
346
+ await stabilizePage(page);
347
+ const containsValue = await getElementValue(page, element_css_selector);
348
+ if (!containsValue.includes(value)) {
349
+ proboLogger.info(`Validation FAIL: expected ${value} to be contained in ${containsValue}`);
350
+ return false;
351
+ }
352
+ proboLogger.info('Validation PASS');
353
+ return true;
354
+ case PlaywrightAction.VALIDATE_URL:
355
+ await stabilizePage(page);
356
+ const currentUrl = page.url();
357
+ if (currentUrl !== value) {
358
+ proboLogger.info(`Validation FAIL: expected url ${value} but got ${currentUrl}`);
359
+ return false;
360
+ }
361
+ proboLogger.info('Validation PASS');
362
+ return true;
363
+ default:
364
+ throw new Error(`Unknown action: ${action}`);
365
+ }
366
+ return true;
367
+ }
368
+ catch (error) {
369
+ proboLogger.error(`Failed to execute action ${action}:`, error);
370
+ throw error;
371
+ }
372
+ };
373
+
374
+ class Highlighter {
375
+ constructor(enableConsoleLogs = true) {
376
+ this.enableConsoleLogs = enableConsoleLogs;
377
+ }
378
+ async ensureHighlighterScript(page, maxRetries = 3) {
379
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
380
+ try {
381
+ const scriptExists = await page.evaluate(`typeof window.ProboLabs?.highlight?.execute === 'function'`);
382
+ if (!scriptExists) {
383
+ proboLogger.debug('Injecting highlighter script...');
384
+ await page.evaluate(highlighterCode);
385
+ // Verify the script was injected correctly
386
+ const verified = await page.evaluate(`
387
+ //console.log('ProboLabs global:', window.ProboLabs);
388
+ typeof window.ProboLabs?.highlight?.execute === 'function'
389
+ `);
390
+ proboLogger.debug('Script injection verified:', verified);
391
+ }
392
+ return; // Success - exit the function
393
+ }
394
+ catch (error) {
395
+ if (attempt === maxRetries - 1) {
396
+ throw error;
397
+ }
398
+ proboLogger.debug(`Script injection attempt ${attempt + 1} failed, retrying after delay...`);
399
+ await new Promise(resolve => setTimeout(resolve, 100));
400
+ }
401
+ }
402
+ }
403
+ async highlightElements(page, elementTag) {
404
+ proboLogger.debug('highlightElements called with:', elementTag);
405
+ await this.ensureHighlighterScript(page);
406
+ // Execute the highlight function and await its result
407
+ const result = await page.evaluate(async (tag) => {
408
+ var _a, _b;
409
+ //proboLogger.debug('Browser: Starting highlight execution with tag:', tag);
410
+ if (!((_b = (_a = window.ProboLabs) === null || _a === void 0 ? void 0 : _a.highlight) === null || _b === void 0 ? void 0 : _b.execute)) {
411
+ console.error('Browser: ProboLabs.highlight.execute is not available!');
412
+ return null;
413
+ }
414
+ const elements = await window.ProboLabs.highlight.execute([tag]);
415
+ //proboLogger.debug('Browser: Found elements:', elements);
416
+ return elements;
417
+ }, elementTag);
418
+ return result;
419
+ }
420
+ async unhighlightElements(page) {
421
+ proboLogger.debug('unhighlightElements called');
422
+ await this.ensureHighlighterScript(page);
423
+ await page.evaluate(() => {
424
+ var _a, _b;
425
+ (_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();
426
+ });
427
+ }
428
+ async highlightElement(page, element_css_selector, element_index) {
429
+ await this.ensureHighlighterScript(page);
430
+ proboLogger.debug('Highlighting element with:', { element_css_selector, element_index });
431
+ await page.evaluate(({ css_selector, index }) => {
432
+ const proboLabs = window.ProboLabs;
433
+ if (!proboLabs) {
434
+ proboLogger.warn('ProboLabs not initialized');
435
+ return;
436
+ }
437
+ // Create ElementInfo object for the element
438
+ const elementInfo = {
439
+ css_selector: css_selector,
440
+ index: index
441
+ };
442
+ // Call highlightElements directly
443
+ proboLabs.highlightElements([elementInfo]);
444
+ }, {
445
+ css_selector: element_css_selector,
446
+ index: element_index
447
+ });
448
+ }
449
+ }
450
+
451
+ function getDefaultExportFromCjs (x) {
452
+ return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
453
+ }
454
+
455
+ var retry$2 = {};
456
+
457
+ var retry_operation;
458
+ var hasRequiredRetry_operation;
459
+
460
+ function requireRetry_operation () {
461
+ if (hasRequiredRetry_operation) return retry_operation;
462
+ hasRequiredRetry_operation = 1;
463
+ function RetryOperation(timeouts, options) {
464
+ // Compatibility for the old (timeouts, retryForever) signature
465
+ if (typeof options === 'boolean') {
466
+ options = { forever: options };
467
+ }
468
+
469
+ this._originalTimeouts = JSON.parse(JSON.stringify(timeouts));
470
+ this._timeouts = timeouts;
471
+ this._options = options || {};
472
+ this._maxRetryTime = options && options.maxRetryTime || Infinity;
473
+ this._fn = null;
474
+ this._errors = [];
475
+ this._attempts = 1;
476
+ this._operationTimeout = null;
477
+ this._operationTimeoutCb = null;
478
+ this._timeout = null;
479
+ this._operationStart = null;
480
+ this._timer = null;
481
+
482
+ if (this._options.forever) {
483
+ this._cachedTimeouts = this._timeouts.slice(0);
484
+ }
485
+ }
486
+ retry_operation = RetryOperation;
487
+
488
+ RetryOperation.prototype.reset = function() {
489
+ this._attempts = 1;
490
+ this._timeouts = this._originalTimeouts.slice(0);
491
+ };
492
+
493
+ RetryOperation.prototype.stop = function() {
494
+ if (this._timeout) {
495
+ clearTimeout(this._timeout);
496
+ }
497
+ if (this._timer) {
498
+ clearTimeout(this._timer);
499
+ }
500
+
501
+ this._timeouts = [];
502
+ this._cachedTimeouts = null;
503
+ };
504
+
505
+ RetryOperation.prototype.retry = function(err) {
506
+ if (this._timeout) {
507
+ clearTimeout(this._timeout);
508
+ }
509
+
510
+ if (!err) {
511
+ return false;
512
+ }
513
+ var currentTime = new Date().getTime();
514
+ if (err && currentTime - this._operationStart >= this._maxRetryTime) {
515
+ this._errors.push(err);
516
+ this._errors.unshift(new Error('RetryOperation timeout occurred'));
517
+ return false;
518
+ }
519
+
520
+ this._errors.push(err);
521
+
522
+ var timeout = this._timeouts.shift();
523
+ if (timeout === undefined) {
524
+ if (this._cachedTimeouts) {
525
+ // retry forever, only keep last error
526
+ this._errors.splice(0, this._errors.length - 1);
527
+ timeout = this._cachedTimeouts.slice(-1);
528
+ } else {
529
+ return false;
530
+ }
531
+ }
532
+
533
+ var self = this;
534
+ this._timer = setTimeout(function() {
535
+ self._attempts++;
536
+
537
+ if (self._operationTimeoutCb) {
538
+ self._timeout = setTimeout(function() {
539
+ self._operationTimeoutCb(self._attempts);
540
+ }, self._operationTimeout);
541
+
542
+ if (self._options.unref) {
543
+ self._timeout.unref();
544
+ }
545
+ }
546
+
547
+ self._fn(self._attempts);
548
+ }, timeout);
549
+
550
+ if (this._options.unref) {
551
+ this._timer.unref();
552
+ }
553
+
554
+ return true;
555
+ };
556
+
557
+ RetryOperation.prototype.attempt = function(fn, timeoutOps) {
558
+ this._fn = fn;
559
+
560
+ if (timeoutOps) {
561
+ if (timeoutOps.timeout) {
562
+ this._operationTimeout = timeoutOps.timeout;
563
+ }
564
+ if (timeoutOps.cb) {
565
+ this._operationTimeoutCb = timeoutOps.cb;
566
+ }
567
+ }
568
+
569
+ var self = this;
570
+ if (this._operationTimeoutCb) {
571
+ this._timeout = setTimeout(function() {
572
+ self._operationTimeoutCb();
573
+ }, self._operationTimeout);
574
+ }
575
+
576
+ this._operationStart = new Date().getTime();
577
+
578
+ this._fn(this._attempts);
579
+ };
580
+
581
+ RetryOperation.prototype.try = function(fn) {
582
+ console.log('Using RetryOperation.try() is deprecated');
583
+ this.attempt(fn);
584
+ };
585
+
586
+ RetryOperation.prototype.start = function(fn) {
587
+ console.log('Using RetryOperation.start() is deprecated');
588
+ this.attempt(fn);
589
+ };
590
+
591
+ RetryOperation.prototype.start = RetryOperation.prototype.try;
592
+
593
+ RetryOperation.prototype.errors = function() {
594
+ return this._errors;
595
+ };
596
+
597
+ RetryOperation.prototype.attempts = function() {
598
+ return this._attempts;
599
+ };
600
+
601
+ RetryOperation.prototype.mainError = function() {
602
+ if (this._errors.length === 0) {
603
+ return null;
604
+ }
605
+
606
+ var counts = {};
607
+ var mainError = null;
608
+ var mainErrorCount = 0;
609
+
610
+ for (var i = 0; i < this._errors.length; i++) {
611
+ var error = this._errors[i];
612
+ var message = error.message;
613
+ var count = (counts[message] || 0) + 1;
614
+
615
+ counts[message] = count;
616
+
617
+ if (count >= mainErrorCount) {
618
+ mainError = error;
619
+ mainErrorCount = count;
620
+ }
621
+ }
622
+
623
+ return mainError;
624
+ };
625
+ return retry_operation;
626
+ }
627
+
628
+ var hasRequiredRetry$1;
629
+
630
+ function requireRetry$1 () {
631
+ if (hasRequiredRetry$1) return retry$2;
632
+ hasRequiredRetry$1 = 1;
633
+ (function (exports) {
634
+ var RetryOperation = requireRetry_operation();
635
+
636
+ exports.operation = function(options) {
637
+ var timeouts = exports.timeouts(options);
638
+ return new RetryOperation(timeouts, {
639
+ forever: options && (options.forever || options.retries === Infinity),
640
+ unref: options && options.unref,
641
+ maxRetryTime: options && options.maxRetryTime
642
+ });
643
+ };
644
+
645
+ exports.timeouts = function(options) {
646
+ if (options instanceof Array) {
647
+ return [].concat(options);
648
+ }
649
+
650
+ var opts = {
651
+ retries: 10,
652
+ factor: 2,
653
+ minTimeout: 1 * 1000,
654
+ maxTimeout: Infinity,
655
+ randomize: false
656
+ };
657
+ for (var key in options) {
658
+ opts[key] = options[key];
659
+ }
660
+
661
+ if (opts.minTimeout > opts.maxTimeout) {
662
+ throw new Error('minTimeout is greater than maxTimeout');
663
+ }
664
+
665
+ var timeouts = [];
666
+ for (var i = 0; i < opts.retries; i++) {
667
+ timeouts.push(this.createTimeout(i, opts));
668
+ }
669
+
670
+ if (options && options.forever && !timeouts.length) {
671
+ timeouts.push(this.createTimeout(i, opts));
672
+ }
673
+
674
+ // sort the array numerically ascending
675
+ timeouts.sort(function(a,b) {
676
+ return a - b;
677
+ });
678
+
679
+ return timeouts;
680
+ };
681
+
682
+ exports.createTimeout = function(attempt, opts) {
683
+ var random = (opts.randomize)
684
+ ? (Math.random() + 1)
685
+ : 1;
686
+
687
+ var timeout = Math.round(random * Math.max(opts.minTimeout, 1) * Math.pow(opts.factor, attempt));
688
+ timeout = Math.min(timeout, opts.maxTimeout);
689
+
690
+ return timeout;
691
+ };
692
+
693
+ exports.wrap = function(obj, options, methods) {
694
+ if (options instanceof Array) {
695
+ methods = options;
696
+ options = null;
697
+ }
698
+
699
+ if (!methods) {
700
+ methods = [];
701
+ for (var key in obj) {
702
+ if (typeof obj[key] === 'function') {
703
+ methods.push(key);
704
+ }
705
+ }
706
+ }
707
+
708
+ for (var i = 0; i < methods.length; i++) {
709
+ var method = methods[i];
710
+ var original = obj[method];
711
+
712
+ obj[method] = function retryWrapper(original) {
713
+ var op = exports.operation(options);
714
+ var args = Array.prototype.slice.call(arguments, 1);
715
+ var callback = args.pop();
716
+
717
+ args.push(function(err) {
718
+ if (op.retry(err)) {
719
+ return;
720
+ }
721
+ if (err) {
722
+ arguments[0] = op.mainError();
723
+ }
724
+ callback.apply(this, arguments);
725
+ });
726
+
727
+ op.attempt(function() {
728
+ original.apply(obj, args);
729
+ });
730
+ }.bind(obj, original);
731
+ obj[method].options = options;
732
+ }
733
+ };
734
+ } (retry$2));
735
+ return retry$2;
736
+ }
737
+
738
+ var retry$1;
739
+ var hasRequiredRetry;
740
+
741
+ function requireRetry () {
742
+ if (hasRequiredRetry) return retry$1;
743
+ hasRequiredRetry = 1;
744
+ retry$1 = requireRetry$1();
745
+ return retry$1;
746
+ }
747
+
748
+ var retryExports = requireRetry();
749
+ var retry = /*@__PURE__*/getDefaultExportFromCjs(retryExports);
750
+
751
+ const objectToString = Object.prototype.toString;
752
+
753
+ const isError = value => objectToString.call(value) === '[object Error]';
754
+
755
+ const errorMessages = new Set([
756
+ 'network error', // Chrome
757
+ 'Failed to fetch', // Chrome
758
+ 'NetworkError when attempting to fetch resource.', // Firefox
759
+ 'The Internet connection appears to be offline.', // Safari 16
760
+ 'Load failed', // Safari 17+
761
+ 'Network request failed', // `cross-fetch`
762
+ 'fetch failed', // Undici (Node.js)
763
+ 'terminated', // Undici (Node.js)
764
+ ]);
765
+
766
+ function isNetworkError(error) {
767
+ const isValid = error
768
+ && isError(error)
769
+ && error.name === 'TypeError'
770
+ && typeof error.message === 'string';
771
+
772
+ if (!isValid) {
773
+ return false;
774
+ }
775
+
776
+ // We do an extra check for Safari 17+ as it has a very generic error message.
777
+ // Network errors in Safari have no stack.
778
+ if (error.message === 'Load failed') {
779
+ return error.stack === undefined;
780
+ }
781
+
782
+ return errorMessages.has(error.message);
783
+ }
784
+
785
+ class AbortError extends Error {
786
+ constructor(message) {
787
+ super();
788
+
789
+ if (message instanceof Error) {
790
+ this.originalError = message;
791
+ ({message} = message);
792
+ } else {
793
+ this.originalError = new Error(message);
794
+ this.originalError.stack = this.stack;
795
+ }
796
+
797
+ this.name = 'AbortError';
798
+ this.message = message;
799
+ }
800
+ }
801
+
802
+ const decorateErrorWithCounts = (error, attemptNumber, options) => {
803
+ // Minus 1 from attemptNumber because the first attempt does not count as a retry
804
+ const retriesLeft = options.retries - (attemptNumber - 1);
805
+
806
+ error.attemptNumber = attemptNumber;
807
+ error.retriesLeft = retriesLeft;
808
+ return error;
809
+ };
810
+
811
+ async function pRetry(input, options) {
812
+ return new Promise((resolve, reject) => {
813
+ options = {...options};
814
+ options.onFailedAttempt ??= () => {};
815
+ options.shouldRetry ??= () => true;
816
+ options.retries ??= 10;
817
+
818
+ const operation = retry.operation(options);
819
+
820
+ const abortHandler = () => {
821
+ operation.stop();
822
+ reject(options.signal?.reason);
823
+ };
824
+
825
+ if (options.signal && !options.signal.aborted) {
826
+ options.signal.addEventListener('abort', abortHandler, {once: true});
827
+ }
828
+
829
+ const cleanUp = () => {
830
+ options.signal?.removeEventListener('abort', abortHandler);
831
+ operation.stop();
832
+ };
833
+
834
+ operation.attempt(async attemptNumber => {
835
+ try {
836
+ const result = await input(attemptNumber);
837
+ cleanUp();
838
+ resolve(result);
839
+ } catch (error) {
840
+ try {
841
+ if (!(error instanceof Error)) {
842
+ throw new TypeError(`Non-error was thrown: "${error}". You should only throw errors.`);
843
+ }
844
+
845
+ if (error instanceof AbortError) {
846
+ throw error.originalError;
847
+ }
848
+
849
+ if (error instanceof TypeError && !isNetworkError(error)) {
850
+ throw error;
851
+ }
852
+
853
+ decorateErrorWithCounts(error, attemptNumber, options);
854
+
855
+ if (!(await options.shouldRetry(error))) {
856
+ operation.stop();
857
+ reject(error);
858
+ }
859
+
860
+ await options.onFailedAttempt(error);
861
+
862
+ if (!operation.retry(error)) {
863
+ throw operation.mainError();
864
+ }
865
+ } catch (finalError) {
866
+ decorateErrorWithCounts(finalError, attemptNumber, options);
867
+ cleanUp();
868
+ reject(finalError);
869
+ }
870
+ }
871
+ });
872
+ });
873
+ }
874
+
875
+ class ApiError extends Error {
876
+ constructor(status, message, data) {
877
+ super(message);
878
+ this.status = status;
879
+ this.data = data;
880
+ this.name = 'ApiError';
881
+ // Remove stack trace for cleaner error messages
882
+ this.stack = undefined;
883
+ }
884
+ toString() {
885
+ return `${this.message} (Status: ${this.status})`;
886
+ }
887
+ }
888
+ class ApiClient {
889
+ constructor(apiUrl, token = 'b31793f81a1f58b8a153c86a5fdf4df0e5179c51', // Default to demo token
890
+ maxRetries = 3, initialBackoff = 1000) {
891
+ this.apiUrl = apiUrl;
892
+ this.token = token;
893
+ this.maxRetries = maxRetries;
894
+ this.initialBackoff = initialBackoff;
895
+ }
896
+ async handleResponse(response) {
897
+ var _a;
898
+ try {
899
+ const data = await response.json();
900
+ if (!response.ok) {
901
+ switch (response.status) {
902
+ case 401:
903
+ throw new ApiError(401, 'Unauthorized - Invalid or missing authentication token');
904
+ case 403:
905
+ throw new ApiError(403, 'Forbidden - You do not have permission to perform this action');
906
+ case 400:
907
+ throw new ApiError(400, 'Bad Request', data);
908
+ case 404:
909
+ throw new ApiError(404, 'Not Found', data);
910
+ case 500:
911
+ throw new ApiError(500, 'Internal Server Error', data);
912
+ default:
913
+ throw new ApiError(response.status, `API Error: ${data.error || 'Unknown error'}`, data);
914
+ }
915
+ }
916
+ return data;
917
+ }
918
+ catch (error) {
919
+ // Only throw the original error if it's not a network error
920
+ if (!((_a = error.message) === null || _a === void 0 ? void 0 : _a.includes('fetch failed'))) {
921
+ throw error;
922
+ }
923
+ throw new ApiError(0, 'Network error: fetch failed');
924
+ }
925
+ }
926
+ getHeaders() {
927
+ const headers = {
928
+ 'Content-Type': 'application/json',
929
+ };
930
+ // Always include token in headers now that we have a default
931
+ headers['Authorization'] = `Token ${this.token}`;
932
+ return headers;
933
+ }
934
+ async createStep(options) {
935
+ return pRetry(async () => {
936
+ const response = await fetch(`${this.apiUrl}/step-runners/`, {
937
+ method: 'POST',
938
+ headers: this.getHeaders(),
939
+ body: JSON.stringify({
940
+ step_id: options.stepIdFromServer,
941
+ scenario_name: options.scenarioName,
942
+ step_prompt: options.stepPrompt,
943
+ initial_screenshot: options.initial_screenshot_url,
944
+ initial_html_content: options.initial_html_content,
945
+ use_cache: options.use_cache,
946
+ }),
947
+ });
948
+ const data = await this.handleResponse(response);
949
+ return data.step.id;
950
+ }, {
951
+ retries: this.maxRetries,
952
+ minTimeout: this.initialBackoff,
953
+ onFailedAttempt: error => {
954
+ console.log(`[probolib-LOG] API call failed, attempt ${error.attemptNumber} of ${error.retriesLeft + error.attemptNumber}...`);
955
+ }
956
+ });
957
+ }
958
+ async resolveNextInstruction(stepId, instruction) {
959
+ return pRetry(async () => {
960
+ const response = await fetch(`${this.apiUrl}/step-runners/${stepId}/run/`, {
961
+ method: 'POST',
962
+ headers: this.getHeaders(),
963
+ body: JSON.stringify({ executed_instruction: instruction }),
964
+ });
965
+ const data = await this.handleResponse(response);
966
+ return data.instruction;
967
+ }, {
968
+ retries: this.maxRetries,
969
+ minTimeout: this.initialBackoff,
970
+ onFailedAttempt: error => {
971
+ console.log(`[probolib-LOG] API call failed, attempt ${error.attemptNumber} of ${error.retriesLeft + error.attemptNumber}...`);
972
+ }
973
+ });
974
+ }
975
+ async uploadScreenshot(screenshot_bytes) {
976
+ return pRetry(async () => {
977
+ const response = await fetch(`${this.apiUrl}/upload-screenshots/`, {
978
+ method: 'POST',
979
+ headers: {
980
+ 'Authorization': `Token ${this.token}`
981
+ },
982
+ body: screenshot_bytes,
983
+ });
984
+ const data = await this.handleResponse(response);
985
+ return data.screenshot_url;
986
+ }, {
987
+ retries: this.maxRetries,
988
+ minTimeout: this.initialBackoff,
989
+ onFailedAttempt: error => {
990
+ console.log(`[probolib-LOG] API call failed, attempt ${error.attemptNumber} of ${error.retriesLeft + error.attemptNumber}...`);
991
+ }
992
+ });
993
+ }
994
+ }
995
+
996
+ const DEMO_TOKEN = 'b31793f81a1f58b8a153c86a5fdf4df0e5179c51';
997
+ const retryOptions = {
998
+ retries: 3,
999
+ minTimeout: 1000,
1000
+ onFailedAttempt: (error) => {
1001
+ proboLogger.warn(`Page operation failed, attempt ${error.attemptNumber} of ${error.retriesLeft + error.attemptNumber}...`);
1002
+ }
1003
+ };
1004
+ class Probo {
1005
+ constructor({ scenarioName, token, apiUrl, enableConsoleLogs = false, debugLevel = ProboLogLevel.LOG }) {
1006
+ this.highlighter = new Highlighter(enableConsoleLogs);
1007
+ this.apiClient = new ApiClient(apiUrl, token);
1008
+ this.enableConsoleLogs = enableConsoleLogs;
1009
+ this.scenarioName = scenarioName;
1010
+ proboLogger.setLogLevel(debugLevel);
1011
+ proboLogger.log(`Initializing: scenarioName: ${scenarioName}, apiUrl: ${apiUrl}, enableConsoleLogs: ${enableConsoleLogs}, debugLevel: ${debugLevel}`);
1012
+ }
1013
+ async runStep(page, stepPrompt, stepIdFromServer, // Make optional
1014
+ useCache = true) {
1015
+ proboLogger.log(`runStep: #${stepIdFromServer} - ${stepPrompt}. pageUrl: ${page.url()}`);
1016
+ this.setupConsoleLogs(page);
1017
+ const stepId = await this._handleStepCreation(page, stepPrompt, stepIdFromServer, useCache);
1018
+ proboLogger.debug('Step ID:', stepId);
1019
+ let instruction = null;
1020
+ // Main execution loop
1021
+ while (true) {
1022
+ try {
1023
+ // Get next instruction from server
1024
+ const nextInstruction = await this.apiClient.resolveNextInstruction(stepId, instruction);
1025
+ proboLogger.debug('Next Instruction from server:', nextInstruction);
1026
+ // Exit conditions
1027
+ if (nextInstruction.what_to_do === 'do_nothing') {
1028
+ if (nextInstruction.args.success) {
1029
+ proboLogger.log(`Reasoning: ${nextInstruction.args.message}`);
1030
+ proboLogger.info('Step completed successfully');
1031
+ return nextInstruction.args.status;
1032
+ }
1033
+ else {
1034
+ throw new Error(`Step failed: ${nextInstruction.args.message}`);
1035
+ }
1036
+ break;
1037
+ }
1038
+ // Handle different instruction types
1039
+ switch (nextInstruction.what_to_do) {
1040
+ case 'highlight_candidate_elements':
1041
+ proboLogger.debug('Highlighting candidate elements:', nextInstruction.args.element_type);
1042
+ const highlighted_elements = await this.highlightElements(page, nextInstruction.args.element_type);
1043
+ proboLogger.debug(`Highlighted ${highlighted_elements.length} elements`);
1044
+ const candidate_elements_screenshot_url = await this.screenshot(page);
1045
+ // proboLogger.log('candidate_elements_screenshot_url:', candidate_elements_screenshot_url);
1046
+ const executed_instruction = {
1047
+ what_to_do: 'highlight_candidate_elements',
1048
+ args: {
1049
+ element_type: nextInstruction.args.element_type
1050
+ },
1051
+ result: {
1052
+ highlighted_elements: highlighted_elements,
1053
+ candidate_elements_screenshot_url: candidate_elements_screenshot_url
1054
+ }
1055
+ };
1056
+ proboLogger.debug('Executed Instruction:', executed_instruction);
1057
+ instruction = executed_instruction;
1058
+ break;
1059
+ case 'perform_action':
1060
+ instruction = await this._handlePerformAction(page, nextInstruction);
1061
+ break;
1062
+ default:
1063
+ throw new Error(`Unknown instruction type: ${nextInstruction.what_to_do}`);
1064
+ }
1065
+ }
1066
+ catch (error) {
1067
+ proboLogger.error('Error during step execution:', error);
1068
+ throw error;
1069
+ }
1070
+ }
1071
+ }
1072
+ async _handleStepCreation(page, stepPrompt, stepIdFromServer, useCache) {
1073
+ proboLogger.debug(`Taking initial screenshot from the page ${page.url()}`);
1074
+ await stabilizePage(page);
1075
+ const initial_screenshot_url = await pRetry(() => this.screenshot(page), retryOptions);
1076
+ proboLogger.debug(`Taking initial html content from the page ${page.url()}`);
1077
+ const initial_html_content = await pRetry(async () => {
1078
+ try {
1079
+ return await page.content();
1080
+ }
1081
+ catch (error) {
1082
+ console.log('Error caught:', {
1083
+ name: error.name,
1084
+ message: error.message,
1085
+ code: error.code,
1086
+ constructor: error.constructor.name,
1087
+ prototype: Object.getPrototypeOf(error).constructor.name
1088
+ });
1089
+ throw error; // Re-throw to trigger retry
1090
+ }
1091
+ }, retryOptions);
1092
+ return await this.apiClient.createStep({
1093
+ stepIdFromServer,
1094
+ scenarioName: this.scenarioName,
1095
+ stepPrompt: stepPrompt,
1096
+ initial_screenshot_url,
1097
+ initial_html_content,
1098
+ use_cache: useCache
1099
+ });
1100
+ }
1101
+ setupConsoleLogs(page) {
1102
+ if (this.enableConsoleLogs) {
1103
+ page.on('console', msg => {
1104
+ const type = msg.type();
1105
+ const text = msg.text();
1106
+ proboLogger.log(`[Browser-${type}]: ${text}`);
1107
+ });
1108
+ }
1109
+ }
1110
+ async highlightElements(page, elementTag) {
1111
+ return this.highlighter.highlightElements(page, elementTag);
1112
+ }
1113
+ async unhighlightElements(page) {
1114
+ return this.highlighter.unhighlightElements(page);
1115
+ }
1116
+ async highlightElement(page, element_css_selector, element_index) {
1117
+ return this.highlighter.highlightElement(page, element_css_selector, element_index);
1118
+ }
1119
+ async screenshot(page) {
1120
+ const screenshot_bytes = await page.screenshot({ fullPage: true, animations: 'disabled' });
1121
+ // make an api call to upload the screenshot to cloudinary
1122
+ const screenshot_url = await this.apiClient.uploadScreenshot(screenshot_bytes);
1123
+ return screenshot_url;
1124
+ }
1125
+ async _handlePerformAction(page, nextInstruction) {
1126
+ proboLogger.debug('Handling perform action:', nextInstruction);
1127
+ const action = nextInstruction.args.action;
1128
+ const value = nextInstruction.args.value;
1129
+ const element_css_selector = nextInstruction.args.element_css_selector;
1130
+ const element_index = nextInstruction.args.element_index;
1131
+ if (action !== PlaywrightAction.VISIT_URL) {
1132
+ await this.unhighlightElements(page);
1133
+ proboLogger.debug('Unhighlighted elements');
1134
+ await this.highlightElement(page, element_css_selector, element_index);
1135
+ proboLogger.debug('Highlighted element');
1136
+ }
1137
+ const pre_action_screenshot_url = await this.screenshot(page);
1138
+ const step_status = await executePlaywrightAction(page, action, value, element_css_selector);
1139
+ await this.unhighlightElements(page);
1140
+ proboLogger.debug('UnHighlighted element');
1141
+ await waitForNavigationToSettle(page, { initialWait: 500 });
1142
+ const post_action_screenshot_url = await this.screenshot(page);
1143
+ const post_html_content = await pRetry(async () => {
1144
+ try {
1145
+ return await page.content();
1146
+ }
1147
+ catch (error) {
1148
+ console.log('Error caught:', {
1149
+ name: error.name,
1150
+ message: error.message,
1151
+ code: error.code,
1152
+ constructor: error.constructor.name,
1153
+ prototype: Object.getPrototypeOf(error).constructor.name
1154
+ });
1155
+ throw error; // Re-throw to trigger retry
1156
+ }
1157
+ }, retryOptions);
1158
+ const executed_instruction = {
1159
+ what_to_do: 'perform_action',
1160
+ args: {
1161
+ action: action,
1162
+ value: value,
1163
+ element_css_selector: element_css_selector
1164
+ },
1165
+ result: {
1166
+ pre_action_screenshot_url: pre_action_screenshot_url,
1167
+ post_action_screenshot_url: post_action_screenshot_url,
1168
+ post_html_content: post_html_content,
1169
+ validation_status: step_status !== null && step_status !== void 0 ? step_status : true
1170
+ }
1171
+ };
1172
+ return executed_instruction;
1173
+ }
1174
+ }
1175
+
1176
+ export { DEMO_TOKEN, ElementTag, Probo, ProboLogLevel };
1177
+ //# sourceMappingURL=index.js.map