@probolabs/playwright 0.4.11 → 0.4.12

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.
@@ -0,0 +1,644 @@
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 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\"]').filter(el => {\r\n return el.tagName.toLowerCase() != 'input';\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 || comboboxTag) && 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 return getAllElementsIncludingShadow('input[type=\"checkbox\"]', el).length === 1;\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 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 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 // Get text content with spaces between elements\r\n function getTextContent(element) {\r\n const walker = document.createTreeWalker(\r\n element,\r\n NodeFilter.SHOW_TEXT,\r\n null,\r\n false\r\n );\r\n\r\n let text = '';\r\n let node;\r\n\r\n while (node = walker.nextNode()) {\r\n const trimmedText = node.textContent.trim();\r\n if (trimmedText) {\r\n // Add space if there's already text\r\n if (text) {\r\n text += ' ';\r\n }\r\n text += trimmedText;\r\n }\r\n }\r\n\r\n return text;\r\n }\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: 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.y); \r\n //TODO: might need to replace contains() with a shadow DOM aware implementation\r\n return elementAtPoint && elementAtPoint !== element && !element.contains(elementAtPoint) && !elementAtPoint.contains(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) {\r\n const elements = await findElements(elementTypes);\r\n highlightElements(elements);\r\n return elements;\r\n },\r\n\r\n unexecute: function() {\r\n const documents = getAllFrames();\r\n documents.forEach(doc => {\r\n const overlay = doc.getElementById('highlight-overlay');\r\n if (overlay) {\r\n overlay.remove();\r\n }\r\n });\r\n },\r\n\r\n getElementInfo\r\n };\r\n\r\n\r\n function unhighlightElements() {\r\n const documents = getAllFrames();\r\n documents.forEach(doc => {\r\n const overlay = doc.getElementById('highlight-overlay');\r\n if (overlay) {\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.URL] = overlay;\r\n });\r\n \r\n\r\n const updateHighlights = () => {\r\n // 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 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.URL].appendChild(highlight);\r\n });\r\n };\r\n\r\n // Initial highlight\r\n updateHighlights();\r\n\r\n // Update highlights on scroll and resize\r\n const scrollHandler = () => {\r\n requestAnimationFrame(updateHighlights);\r\n };\r\n \r\n window.addEventListener('scroll', scrollHandler, true);\r\n window.addEventListener('resize', updateHighlights);\r\n\r\n // Store event handlers for cleanup\r\n Object.values(overlays).forEach(overlay => {\r\n overlay.scrollHandler = scrollHandler;\r\n overlay.updateHighlights = updateHighlights;\r\n }); \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
+ (function (global, factory) {
3
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
4
+ typeof define === 'function' && define.amd ? define(['exports'], factory) :
5
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.ProboPlaywright = {}));
6
+ })(this, (function (exports) { 'use strict';
7
+
8
+ const ElementTag = {
9
+ CLICKABLE: "CLICKABLE", // button, link, toggle switch, checkbox, radio, dropdowns, clickable divs
10
+ FILLABLE: "FILLABLE", // input, textarea content_editable, date picker??
11
+ SELECTABLE: "SELECTABLE", // select
12
+ NON_INTERACTIVE_ELEMENT: 'NON_INTERACTIVE_ELEMENT',
13
+ };
14
+
15
+ exports.ProboLogLevel = void 0;
16
+ (function (ProboLogLevel) {
17
+ ProboLogLevel[ProboLogLevel["DEBUG"] = 0] = "DEBUG";
18
+ ProboLogLevel[ProboLogLevel["INFO"] = 1] = "INFO";
19
+ ProboLogLevel[ProboLogLevel["LOG"] = 2] = "LOG";
20
+ ProboLogLevel[ProboLogLevel["WARN"] = 3] = "WARN";
21
+ ProboLogLevel[ProboLogLevel["ERROR"] = 4] = "ERROR";
22
+ })(exports.ProboLogLevel || (exports.ProboLogLevel = {}));
23
+ class ProboLogger {
24
+ constructor(prefix, logLevel = exports.ProboLogLevel.LOG) {
25
+ this.prefix = prefix;
26
+ this.logLevel = logLevel;
27
+ }
28
+ setLogLevel(level) {
29
+ this.logLevel = level;
30
+ console.log(`[${this.prefix}-INFO] Log level set to: ${exports.ProboLogLevel[level]}`);
31
+ }
32
+ debug(...args) {
33
+ this.msg(exports.ProboLogLevel.DEBUG, ...args);
34
+ }
35
+ info(...args) {
36
+ this.msg(exports.ProboLogLevel.INFO, ...args);
37
+ }
38
+ log(...args) {
39
+ this.msg(exports.ProboLogLevel.LOG, ...args);
40
+ }
41
+ warn(...args) {
42
+ this.msg(exports.ProboLogLevel.WARN, ...args);
43
+ }
44
+ error(...args) {
45
+ this.msg(exports.ProboLogLevel.ERROR, ...args);
46
+ }
47
+ msg(logLevel, ...args) {
48
+ if (logLevel >= this.logLevel) {
49
+ console.log(`[${this.prefix}-${exports.ProboLogLevel[logLevel]}]`, ...args);
50
+ }
51
+ }
52
+ }
53
+ const proboLogger = new ProboLogger('probolib');
54
+
55
+ // Utility functions for enhanced Playwright interactions
56
+ async function handleNavigation(page, url, navigationTimeout = 5000, totalTimeout = 10000) {
57
+ let navigationCount = 0;
58
+ let lastNavigationTime = null;
59
+ // Declare the function outside try block so it's accessible in finally
60
+ const onFrameNavigated = (frame) => {
61
+ if (frame === page.mainFrame()) {
62
+ navigationCount++;
63
+ lastNavigationTime = Date.now();
64
+ proboLogger.debug(`Navigation ${navigationCount} detected at ${lastNavigationTime}`);
65
+ }
66
+ };
67
+ try {
68
+ // Setup navigation tracking
69
+ page.on('framenavigated', onFrameNavigated);
70
+ await page.goto(url, {
71
+ waitUntil: 'domcontentloaded',
72
+ timeout: totalTimeout
73
+ });
74
+ // Final wait only for DOM, not network
75
+ if (navigationCount > 0) {
76
+ proboLogger.debug(`Total navigations detected: ${navigationCount}`);
77
+ await page.waitForLoadState('domcontentloaded');
78
+ // Add scroll to bottom to trigger lazy loading
79
+ await scrollToBottomRight(page);
80
+ }
81
+ }
82
+ finally {
83
+ page.removeListener('framenavigated', onFrameNavigated);
84
+ proboLogger.debug('Navigation listener removed');
85
+ }
86
+ }
87
+ async function enhancedClick(page, selector, options = {}) {
88
+ // Track initial scroll position
89
+ let initScrollY = await page.evaluate(() => window.scrollY);
90
+ let initScrollX = await page.evaluate(() => window.scrollX);
91
+ // Perform the click
92
+ await page.click(selector, { noWaitAfter: false, ...options });
93
+ // Monitor for any scrolling that might occur due to the click
94
+ let scrollingDetected = false;
95
+ while (true) {
96
+ const currScrollX = await page.evaluate(() => window.scrollX);
97
+ const currScrollY = await page.evaluate(() => window.scrollY);
98
+ if (currScrollY !== initScrollY || currScrollX !== initScrollX) {
99
+ proboLogger.debug('Click detected scrolling, waiting 50ms for stabilization...');
100
+ scrollingDetected = true;
101
+ await page.waitForTimeout(50);
102
+ initScrollX = currScrollX;
103
+ initScrollY = currScrollY;
104
+ }
105
+ else {
106
+ break;
107
+ }
108
+ }
109
+ // If scrolling occurred, reset to top
110
+ if (scrollingDetected) {
111
+ await page.evaluate(() => window.scrollTo(0, 0));
112
+ await page.waitForTimeout(50); // Small delay between scrolls
113
+ }
114
+ }
115
+ async function selectDropdownOption(page, selector, value) {
116
+ // Get the element's tag name
117
+ const locator = page.locator(selector);
118
+ const tagName = await locator.evaluate(el => el.tagName.toLowerCase());
119
+ if (tagName === 'select') {
120
+ // Handle native select element
121
+ proboLogger.debug('selectDropdownOption: simple select tag');
122
+ await page.selectOption(selector, value);
123
+ }
124
+ else { // not a native select
125
+ try {
126
+ // Look for child <select> if exists
127
+ const listboxSelector = locator.locator('select');
128
+ const count = await listboxSelector.count();
129
+ if (count === 1) { // delegate to child <select>
130
+ proboLogger.debug('Identified child element with tag select');
131
+ await listboxSelector.selectOption(value);
132
+ }
133
+ else {
134
+ // Click the original locator to expose the real listbox and options
135
+ proboLogger.debug('Clicking container element, looking for child element with role listbox');
136
+ await locator.click();
137
+ const listbox = locator.getByRole('listbox');
138
+ proboLogger.debug(`Clicking option ${value}`);
139
+ // Handle both string and array values
140
+ const values = Array.isArray(value) ? value : [value];
141
+ for (const val of values) {
142
+ const optionSelector = listbox
143
+ .getByRole('option')
144
+ .getByText(new RegExp(`^${val.toLowerCase()}$`, 'i'));
145
+ const optionCount = await optionSelector.count();
146
+ if (optionCount !== 1) {
147
+ throw new Error(`Found ${optionCount} option selectors for value: ${val}`);
148
+ }
149
+ await optionSelector.click();
150
+ }
151
+ }
152
+ }
153
+ catch (error) {
154
+ throw new Error(`Failed to select option '${value}' from custom dropdown: ${error}`);
155
+ }
156
+ }
157
+ }
158
+ async function getElementValue(page, selector) {
159
+ // TODO: Implement getting all text contents
160
+ proboLogger.log("TODO: Implement getting all text contents");
161
+ return "TODO: Implement getting all text contents";
162
+ }
163
+ async function waitForMutationsToSettle(page) {
164
+ // TODO: Implement mutation observer
165
+ //proboLogger.log("TODO: Implement mutation observer");
166
+ await page.waitForTimeout(500);
167
+ }
168
+ async function scrollToBottomRight(page) {
169
+ proboLogger.debug("Scrolling to bottom of page...");
170
+ // Get initial page dimensions
171
+ let lastHeight = await page.evaluate(() => document.documentElement.scrollHeight);
172
+ let lastWidth = await page.evaluate(() => document.documentElement.scrollWidth);
173
+ while (true) {
174
+ let smoothingSteps = 0;
175
+ // Get current scroll positions and dimensions
176
+ const initScrollY = await page.evaluate(() => window.scrollY);
177
+ const initScrollX = await page.evaluate(() => window.scrollX);
178
+ const maxScrollY = await page.evaluate(() => document.documentElement.scrollHeight);
179
+ const maxScrollX = await page.evaluate(() => document.documentElement.scrollWidth);
180
+ const maxClientY = await page.evaluate(() => document.documentElement.clientHeight);
181
+ const maxClientX = await page.evaluate(() => document.documentElement.clientWidth);
182
+ // Scroll both horizontally and vertically in steps
183
+ let currScrollX = initScrollX;
184
+ while (currScrollX <= (maxScrollX - maxClientX)) {
185
+ let currScrollY = initScrollY;
186
+ while (currScrollY <= (maxScrollY - maxClientY)) {
187
+ currScrollY += maxClientY;
188
+ await page.evaluate(({ x, y }) => window.scrollTo(x, y), { x: currScrollX, y: currScrollY });
189
+ await page.waitForTimeout(50); // Small delay between scrolls
190
+ smoothingSteps++;
191
+ }
192
+ currScrollX += maxClientX;
193
+ }
194
+ proboLogger.debug(`Performed ${smoothingSteps} smoothing steps while scrolling`);
195
+ // Check if content was lazy loaded (dimensions changed)
196
+ const newHeight = await page.evaluate(() => document.documentElement.scrollHeight);
197
+ const newWidth = await page.evaluate(() => document.documentElement.scrollWidth);
198
+ if (newHeight === lastHeight && newWidth === lastWidth) {
199
+ break;
200
+ }
201
+ proboLogger.debug("Page dimensions updated, repeating smoothing steps with new dimensions");
202
+ lastHeight = newHeight;
203
+ lastWidth = newWidth;
204
+ }
205
+ // Reset scroll position to top left
206
+ await page.waitForTimeout(200);
207
+ await page.evaluate(() => window.scrollTo(0, 0));
208
+ await page.waitForTimeout(50);
209
+ }
210
+ const PlaywrightAction = {
211
+ VISIT_BASE_URL: 'VISIT_BASE_URL',
212
+ VISIT_URL: 'VISIT_URL',
213
+ CLICK: 'CLICK',
214
+ FILL_IN: 'FILL_IN',
215
+ SELECT_DROPDOWN: 'SELECT_DROPDOWN',
216
+ SELECT_MULTIPLE_DROPDOWN: 'SELECT_MULTIPLE_DROPDOWN',
217
+ CHECK_CHECKBOX: 'CHECK_CHECKBOX',
218
+ SELECT_RADIO: 'SELECT_RADIO',
219
+ TOGGLE_SWITCH: 'TOGGLE_SWITCH',
220
+ PRESS_KEY: 'PRESS_KEY',
221
+ VALIDATE_EXACT_VALUE: 'VALIDATE_EXACT_VALUE',
222
+ VALIDATE_CONTAINS_VALUE: 'VALIDATE_CONTAINS_VALUE',
223
+ VALIDATE_URL: 'VALIDATE_URL'
224
+ };
225
+ const executePlaywrightAction = async (page, action, value, element_css_selector) => {
226
+ proboLogger.info('Executing playwright action:', { action, value, element_css_selector });
227
+ try {
228
+ switch (action) {
229
+ case PlaywrightAction.VISIT_BASE_URL:
230
+ case PlaywrightAction.VISIT_URL:
231
+ await handleNavigation(page, value);
232
+ break;
233
+ case PlaywrightAction.CLICK:
234
+ await enhancedClick(page, element_css_selector);
235
+ break;
236
+ case PlaywrightAction.FILL_IN:
237
+ await enhancedClick(page, element_css_selector);
238
+ await page.fill(element_css_selector, value);
239
+ break;
240
+ case PlaywrightAction.SELECT_DROPDOWN:
241
+ await selectDropdownOption(page, element_css_selector, value);
242
+ break;
243
+ case PlaywrightAction.SELECT_MULTIPLE_DROPDOWN:
244
+ const options = value.split(',').map(opt => opt.trim());
245
+ await selectDropdownOption(page, element_css_selector, options);
246
+ break;
247
+ case PlaywrightAction.CHECK_CHECKBOX:
248
+ const checked = value.toLowerCase() === 'true';
249
+ await page.setChecked(element_css_selector, checked);
250
+ break;
251
+ case PlaywrightAction.SELECT_RADIO:
252
+ case PlaywrightAction.TOGGLE_SWITCH:
253
+ await enhancedClick(page, element_css_selector);
254
+ break;
255
+ case PlaywrightAction.PRESS_KEY:
256
+ await page.press(element_css_selector, value);
257
+ break;
258
+ case PlaywrightAction.VALIDATE_EXACT_VALUE:
259
+ const exactValue = await getElementValue(page, element_css_selector);
260
+ if (exactValue !== value) {
261
+ proboLogger.info(`Validation FAIL: expected ${value} but got ${exactValue}`);
262
+ return false;
263
+ }
264
+ proboLogger.info('Validation PASS');
265
+ return true;
266
+ case PlaywrightAction.VALIDATE_CONTAINS_VALUE:
267
+ const containsValue = await getElementValue(page, element_css_selector);
268
+ if (!containsValue.includes(value)) {
269
+ proboLogger.info(`Validation FAIL: expected ${value} to be contained in ${containsValue}`);
270
+ return false;
271
+ }
272
+ proboLogger.info('Validation PASS');
273
+ return true;
274
+ case PlaywrightAction.VALIDATE_URL:
275
+ const currentUrl = page.url();
276
+ if (currentUrl !== value) {
277
+ proboLogger.info(`Validation FAIL: expected url ${value} but got ${currentUrl}`);
278
+ return false;
279
+ }
280
+ proboLogger.info('Validation PASS');
281
+ return true;
282
+ default:
283
+ throw new Error(`Unknown action: ${action}`);
284
+ }
285
+ // Wait for mutations to settle after any action
286
+ await waitForMutationsToSettle(page);
287
+ return true;
288
+ }
289
+ catch (error) {
290
+ proboLogger.error(`Failed to execute action ${action}:`, error);
291
+ throw error;
292
+ }
293
+ };
294
+
295
+ class Highlighter {
296
+ constructor(enableConsoleLogs = true) {
297
+ this.enableConsoleLogs = enableConsoleLogs;
298
+ }
299
+ async ensureHighlighterScript(page, maxRetries = 3) {
300
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
301
+ try {
302
+ const scriptExists = await page.evaluate(`typeof window.ProboLabs?.highlight?.execute === 'function'`);
303
+ if (!scriptExists) {
304
+ proboLogger.debug('Injecting highlighter script...');
305
+ await page.evaluate(highlighterCode);
306
+ // Verify the script was injected correctly
307
+ const verified = await page.evaluate(`
308
+ //console.log('ProboLabs global:', window.ProboLabs);
309
+ typeof window.ProboLabs?.highlight?.execute === 'function'
310
+ `);
311
+ proboLogger.debug('Script injection verified:', verified);
312
+ }
313
+ return; // Success - exit the function
314
+ }
315
+ catch (error) {
316
+ if (attempt === maxRetries - 1) {
317
+ throw error;
318
+ }
319
+ proboLogger.debug(`Script injection attempt ${attempt + 1} failed, retrying after delay...`);
320
+ await new Promise(resolve => setTimeout(resolve, 100));
321
+ }
322
+ }
323
+ }
324
+ async highlightElements(page, elementTag) {
325
+ proboLogger.debug('highlightElements called with:', elementTag);
326
+ await this.ensureHighlighterScript(page);
327
+ // Execute the highlight function and await its result
328
+ const result = await page.evaluate(async (tag) => {
329
+ var _a, _b;
330
+ //proboLogger.debug('Browser: Starting highlight execution with tag:', tag);
331
+ if (!((_b = (_a = window.ProboLabs) === null || _a === void 0 ? void 0 : _a.highlight) === null || _b === void 0 ? void 0 : _b.execute)) {
332
+ console.error('Browser: ProboLabs.highlight.execute is not available!');
333
+ return null;
334
+ }
335
+ const elements = await window.ProboLabs.highlight.execute([tag]);
336
+ //proboLogger.debug('Browser: Found elements:', elements);
337
+ return elements;
338
+ }, elementTag);
339
+ return result;
340
+ }
341
+ async unhighlightElements(page) {
342
+ proboLogger.debug('unhighlightElements called');
343
+ await this.ensureHighlighterScript(page);
344
+ await page.evaluate(() => {
345
+ var _a, _b;
346
+ (_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();
347
+ });
348
+ }
349
+ async highlightElement(page, element_css_selector, element_index) {
350
+ await this.ensureHighlighterScript(page);
351
+ proboLogger.debug('Highlighting element with:', { element_css_selector, element_index });
352
+ await page.evaluate(({ css_selector, index }) => {
353
+ const proboLabs = window.ProboLabs;
354
+ if (!proboLabs) {
355
+ proboLogger.warn('ProboLabs not initialized');
356
+ return;
357
+ }
358
+ // Create ElementInfo object for the element
359
+ const elementInfo = {
360
+ css_selector: css_selector,
361
+ index: index
362
+ };
363
+ // Call highlightElements directly
364
+ proboLabs.highlightElements([elementInfo]);
365
+ }, {
366
+ css_selector: element_css_selector,
367
+ index: element_index
368
+ });
369
+ }
370
+ }
371
+
372
+ class ApiError extends Error {
373
+ constructor(status, message, data) {
374
+ super(message);
375
+ this.status = status;
376
+ this.data = data;
377
+ this.name = 'ApiError';
378
+ // Remove stack trace for cleaner error messages
379
+ this.stack = undefined;
380
+ }
381
+ toString() {
382
+ return `${this.message} (Status: ${this.status})`;
383
+ }
384
+ }
385
+ class ApiClient {
386
+ constructor(apiUrl, token = 'b31793f81a1f58b8a153c86a5fdf4df0e5179c51', // Default to demo token
387
+ maxRetries = 3, initialBackoff = 1000) {
388
+ this.apiUrl = apiUrl;
389
+ this.token = token;
390
+ this.maxRetries = maxRetries;
391
+ this.initialBackoff = initialBackoff;
392
+ }
393
+ async handleResponse(response) {
394
+ var _a;
395
+ try {
396
+ const data = await response.json();
397
+ if (!response.ok) {
398
+ switch (response.status) {
399
+ case 401:
400
+ throw new ApiError(401, 'Unauthorized - Invalid or missing authentication token');
401
+ case 403:
402
+ throw new ApiError(403, 'Forbidden - You do not have permission to perform this action');
403
+ case 400:
404
+ throw new ApiError(400, 'Bad Request', data);
405
+ case 404:
406
+ throw new ApiError(404, 'Not Found', data);
407
+ case 500:
408
+ throw new ApiError(500, 'Internal Server Error', data);
409
+ default:
410
+ throw new ApiError(response.status, `API Error: ${data.error || 'Unknown error'}`, data);
411
+ }
412
+ }
413
+ return data;
414
+ }
415
+ catch (error) {
416
+ // Only throw the original error if it's not a network error
417
+ if (!((_a = error.message) === null || _a === void 0 ? void 0 : _a.includes('fetch failed'))) {
418
+ throw error;
419
+ }
420
+ throw new ApiError(0, 'Network error: fetch failed');
421
+ }
422
+ }
423
+ getHeaders() {
424
+ const headers = {
425
+ 'Content-Type': 'application/json',
426
+ };
427
+ // Always include token in headers now that we have a default
428
+ headers['Authorization'] = `Token ${this.token}`;
429
+ return headers;
430
+ }
431
+ async withRetry(operation) {
432
+ var _a;
433
+ for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
434
+ try {
435
+ return await operation();
436
+ }
437
+ catch (error) {
438
+ // Only retry on network errors
439
+ if (!((_a = error.message) === null || _a === void 0 ? void 0 : _a.includes('Network error: fetch failed'))) {
440
+ throw error;
441
+ }
442
+ // On last attempt, throw the error
443
+ if (attempt === this.maxRetries) {
444
+ throw error;
445
+ }
446
+ // Otherwise, wait and retry
447
+ console.log(`[probolib-LOG] Network error, retrying (${attempt}/${this.maxRetries}). Waiting ${this.initialBackoff * Math.pow(2, attempt - 1)}ms...`);
448
+ await new Promise(resolve => setTimeout(resolve, this.initialBackoff * Math.pow(2, attempt - 1)));
449
+ }
450
+ }
451
+ // TypeScript needs this, but it will never be reached
452
+ throw new Error('Unreachable');
453
+ }
454
+ async createStep(options) {
455
+ return this.withRetry(async () => {
456
+ const response = await fetch(`${this.apiUrl}/step-runners/`, {
457
+ method: 'POST',
458
+ headers: this.getHeaders(),
459
+ body: JSON.stringify({
460
+ step_id: options.stepIdFromServer,
461
+ scenario_name: options.scenarioName,
462
+ step_prompt: options.stepPrompt,
463
+ initial_screenshot: options.initial_screenshot_url,
464
+ initial_html_content: options.initial_html_content,
465
+ use_cache: options.use_cache,
466
+ }),
467
+ });
468
+ const data = await this.handleResponse(response);
469
+ return data.step.id;
470
+ });
471
+ }
472
+ async resolveNextInstruction(stepId, instruction) {
473
+ return this.withRetry(async () => {
474
+ const response = await fetch(`${this.apiUrl}/step-runners/${stepId}/run/`, {
475
+ method: 'POST',
476
+ headers: this.getHeaders(),
477
+ body: JSON.stringify({ executed_instruction: instruction }),
478
+ });
479
+ const data = await this.handleResponse(response);
480
+ return data.instruction;
481
+ });
482
+ }
483
+ async uploadScreenshot(screenshot_bytes) {
484
+ return this.withRetry(async () => {
485
+ const response = await fetch(`${this.apiUrl}/upload-screenshots/`, {
486
+ method: 'POST',
487
+ headers: {
488
+ 'Authorization': `Token ${this.token}`
489
+ },
490
+ body: screenshot_bytes,
491
+ });
492
+ const data = await this.handleResponse(response);
493
+ return data.screenshot_url;
494
+ });
495
+ }
496
+ }
497
+
498
+ const DEMO_TOKEN = 'b31793f81a1f58b8a153c86a5fdf4df0e5179c51';
499
+ class Probo {
500
+ constructor({ scenarioName, token, apiUrl, enableConsoleLogs = false }) {
501
+ this.highlighter = new Highlighter(enableConsoleLogs);
502
+ this.apiClient = new ApiClient(apiUrl, token);
503
+ this.enableConsoleLogs = enableConsoleLogs;
504
+ this.scenarioName = scenarioName;
505
+ proboLogger.log('Initializing: ', scenarioName, token, apiUrl, enableConsoleLogs);
506
+ }
507
+ async runStep(page, stepPrompt, stepIdFromServer, useCache = true) {
508
+ proboLogger.log(`runStep: ${stepPrompt}`);
509
+ this.setupConsoleLogs(page);
510
+ const stepId = await this._handleStepCreation(page, stepPrompt, stepIdFromServer, useCache);
511
+ proboLogger.debug('Step ID:', stepId);
512
+ let instruction = null;
513
+ // Main execution loop
514
+ while (true) {
515
+ try {
516
+ // Get next instruction from server
517
+ const nextInstruction = await this.apiClient.resolveNextInstruction(stepId, instruction);
518
+ proboLogger.debug('Next Instruction from server:', nextInstruction);
519
+ // Exit conditions
520
+ if (nextInstruction.what_to_do === 'do_nothing') {
521
+ if (nextInstruction.args.success) {
522
+ proboLogger.log(`Reasoning: ${nextInstruction.args.message}`);
523
+ proboLogger.info('Step completed successfully');
524
+ return nextInstruction.args.status;
525
+ }
526
+ else {
527
+ throw new Error(`Step failed: ${nextInstruction.args.message}`);
528
+ }
529
+ break;
530
+ }
531
+ // Handle different instruction types
532
+ switch (nextInstruction.what_to_do) {
533
+ case 'highlight_candidate_elements':
534
+ proboLogger.debug('Highlighting candidate elements:', nextInstruction.args.element_type);
535
+ const highlighted_elements = await this.highlightElements(page, nextInstruction.args.element_type);
536
+ proboLogger.debug(`Highlighted ${highlighted_elements.length} elements`);
537
+ const candidate_elements_screenshot_url = await this.screenshot(page);
538
+ // proboLogger.log('candidate_elements_screenshot_url:', candidate_elements_screenshot_url);
539
+ const executed_instruction = {
540
+ what_to_do: 'highlight_candidate_elements',
541
+ args: {
542
+ element_type: nextInstruction.args.element_type
543
+ },
544
+ result: {
545
+ highlighted_elements: highlighted_elements,
546
+ candidate_elements_screenshot_url: candidate_elements_screenshot_url
547
+ }
548
+ };
549
+ proboLogger.debug('Executed Instruction:', executed_instruction);
550
+ instruction = executed_instruction;
551
+ break;
552
+ case 'perform_action':
553
+ instruction = await this._handlePerformAction(page, nextInstruction);
554
+ break;
555
+ default:
556
+ throw new Error(`Unknown instruction type: ${nextInstruction.what_to_do}`);
557
+ }
558
+ }
559
+ catch (error) {
560
+ proboLogger.error('Error during step execution:', error);
561
+ throw error;
562
+ }
563
+ }
564
+ }
565
+ async _handleStepCreation(page, stepPrompt, stepIdFromServer, useCache = false) {
566
+ const initial_screenshot_url = await this.screenshot(page);
567
+ const initial_html_content = await page.content();
568
+ const stepId = await this.apiClient.createStep({
569
+ stepIdFromServer: stepIdFromServer,
570
+ scenarioName: this.scenarioName,
571
+ stepPrompt: stepPrompt,
572
+ initial_screenshot_url: initial_screenshot_url,
573
+ initial_html_content: initial_html_content,
574
+ use_cache: useCache
575
+ });
576
+ // proboLogger.log('Step ID:', stepId);
577
+ return stepId;
578
+ }
579
+ setupConsoleLogs(page) {
580
+ if (this.enableConsoleLogs) {
581
+ page.on('console', msg => {
582
+ const type = msg.type();
583
+ const text = msg.text();
584
+ proboLogger.log(`[Browser-${type}]: ${text}`);
585
+ });
586
+ }
587
+ }
588
+ async highlightElements(page, elementTag) {
589
+ return this.highlighter.highlightElements(page, elementTag);
590
+ }
591
+ async unhighlightElements(page) {
592
+ return this.highlighter.unhighlightElements(page);
593
+ }
594
+ async highlightElement(page, element_css_selector, element_index) {
595
+ return this.highlighter.highlightElement(page, element_css_selector, element_index);
596
+ }
597
+ async screenshot(page) {
598
+ const screenshot_bytes = await page.screenshot({ fullPage: true, animations: 'disabled' });
599
+ // make an api call to upload the screenshot to cloudinary
600
+ const screenshot_url = await this.apiClient.uploadScreenshot(screenshot_bytes);
601
+ return screenshot_url;
602
+ }
603
+ async _handlePerformAction(page, nextInstruction) {
604
+ proboLogger.debug('Handling perform action:', nextInstruction);
605
+ const action = nextInstruction.args.action;
606
+ const value = nextInstruction.args.value;
607
+ const element_css_selector = nextInstruction.args.element_css_selector;
608
+ const element_index = nextInstruction.args.element_index;
609
+ if (action !== PlaywrightAction.VISIT_URL) {
610
+ await this.unhighlightElements(page);
611
+ proboLogger.debug('Unhighlighted elements');
612
+ await this.highlightElement(page, element_css_selector, element_index);
613
+ proboLogger.debug('Highlighted element');
614
+ }
615
+ const pre_action_screenshot_url = await this.screenshot(page);
616
+ const step_status = await executePlaywrightAction(page, action, value, element_css_selector);
617
+ await this.unhighlightElements(page);
618
+ proboLogger.debug('UnHighlighted element');
619
+ const post_action_screenshot_url = await this.screenshot(page);
620
+ const executed_instruction = {
621
+ what_to_do: 'perform_action',
622
+ args: {
623
+ action: action,
624
+ value: value,
625
+ element_css_selector: element_css_selector
626
+ },
627
+ result: {
628
+ pre_action_screenshot_url: pre_action_screenshot_url,
629
+ post_action_screenshot_url: post_action_screenshot_url,
630
+ post_html_content: await page.content(),
631
+ validation_status: step_status !== null && step_status !== void 0 ? step_status : true
632
+ }
633
+ };
634
+ return executed_instruction;
635
+ }
636
+ }
637
+
638
+ exports.DEMO_TOKEN = DEMO_TOKEN;
639
+ exports.ElementTag = ElementTag;
640
+ exports.Probo = Probo;
641
+
642
+ Object.defineProperty(exports, '__esModule', { value: true });
643
+
644
+ }));