@probolabs/playwright 0.4.12 → 0.4.14
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/README.md +136 -136
- package/dist/index.d.ts +37 -4
- package/dist/index.js +165 -135
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/utils.d.ts.map +1 -1
- package/package.json +3 -2
- package/dist/actions.d.ts +0 -24
- package/dist/actions.d.ts.map +0 -1
- package/dist/actions.js +0 -241
- package/dist/api-client.d.ts +0 -32
- package/dist/api-client.d.ts.map +0 -1
- package/dist/api-client.js +0 -125
- package/dist/highlight.d.ts +0 -28
- package/dist/highlight.d.ts.map +0 -1
- package/dist/highlight.js +0 -79
- package/dist/index.d.ts.map +0 -1
- package/dist/probo-playwright.esm.js +0 -63
- package/dist/probo-playwright.umd.js +0 -644
- package/dist/src/actions.d.ts +0 -18
- package/dist/src/api-client.d.ts +0 -13
- package/dist/src/highlight.d.ts +0 -28
- package/dist/src/index.d.ts +0 -20
- package/dist/src/utils.d.ts +0 -20
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/dist/utils.d.ts +0 -21
- package/dist/utils.d.ts.map +0 -1
- package/dist/utils.js +0 -39
package/dist/index.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
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, iframe_selector}) {\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.iframe_selector = iframe_selector;\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 && 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 || frame.contentWindow.document;\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\"], [role=\"menu\"], [role=\"menuitem\"]').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|menu).*/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', 'a', 'button'].includes(el.tagName.toLowerCase());\r\n const style = window.getComputedStyle(el); \r\n const result = hasDropdownClass && validTag && (style.cursor === 'pointer' || el.tagName.toLowerCase() === 'a' || el.tagName.toLowerCase() === 'button');\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\"], [aria-haspopup=\"menu\"]'));\r\n\r\n // Improve navigation element detection\r\n // Semantic nav elements with list items\r\n dropdowns.push(...getAllElementsIncludingShadow('nav ul li, nav ol li'));\r\n \r\n // Navigation elements in common design patterns\r\n dropdowns.push(...getAllElementsIncludingShadow('header a, .header a, .nav a, .navigation a, .menu a, .sidebar a, aside a'));\r\n \r\n // Elements in primary navigation areas with common attributes\r\n dropdowns.push(...getAllElementsIncludingShadow('[role=\"navigation\"] a, [aria-label*=\"navigation\"] a, [aria-label*=\"menu\"] a'));\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')];\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 // 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 };\r\n\r\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 console.log('getElementByXPathOrCssSelector:', element_info);\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 if (element_info.iframe_selector) {\r\n console.log('elementInfo with iframe: ', element_info);\r\n const frames = getAllDocumentElementsIncludingShadow('iframe');\r\n \r\n // Iterate over all frames and compare their CSS selectors\r\n for (const frame of frames) {\r\n const cssSelector = generateCssPath(frame);\r\n if (cssSelector === element_info.iframe_selector) {\r\n const frameDocument = frame.contentDocument || frame.contentWindow.document;\r\n element = querySelectorShadow(element_info.css_selector, frameDocument);\r\n console.log('found element ', element);\r\n break;\r\n } \r\n } }\r\n else\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 && 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 if (!element) {\r\n console.error('ERROR: No element provided to generateCssPath returning empty string');\r\n return '';\r\n }\r\n const path = [];\r\n // console.group('Generating CSS path for:', element);\r\n while (element && element.nodeType === Node.ELEMENT_NODE) { \r\n let selector = element.nodeName.toLowerCase();\r\n // console.log('Element:', selector, element);\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 while (sibling = sibling.nextElementSibling) {\r\n if (sibling.nodeName.toLowerCase() === selector) {\r\n break;\r\n }\r\n }\r\n selector += `:nth-of-type(${nth})`;\r\n \r\n \r\n path.unshift(selector);\r\n //console.log(` Current path: ${path.join(' > ')}`);\r\n\r\n if (element.assignedSlot) {\r\n element = element.assignedSlot;\r\n // console.log(' Moving to assigned slot');\r\n }\r\n else {\r\n element = element.parentNode;\r\n // console.log(' Moving to parent:', element);\r\n\r\n // Check if we're at a shadow root\r\n if (element && element.nodeType !== Node.ELEMENT_NODE && element.getRootNode() instanceof ShadowRoot) {\r\n console.log(' Found shadow root, moving to host');\r\n // Get the shadow root's host element\r\n element = element.getRootNode().host; \r\n }\r\n }\r\n }\r\n \r\n // console.log('Final selector:', path.join(' > '));\r\n // console.groupEnd();\r\n return path.join(' > ');\r\n }\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 getContainingIframe(element) {\r\n // If not in an iframe, return null\r\n if (element.ownerDocument.defaultView === window.top) {\r\n return null;\r\n }\r\n \r\n // Try to find the iframe in the parent document that contains our element\r\n try {\r\n const parentDocument = element.ownerDocument.defaultView.parent.document;\r\n const iframes = parentDocument.querySelectorAll('iframe');\r\n \r\n for (const iframe of iframes) {\r\n if (iframe.contentWindow === element.ownerDocument.defaultView) {\r\n return iframe;\r\n }\r\n }\r\n } catch (e) {\r\n // Cross-origin restriction\r\n return \"Cross-origin iframe - cannot access details\";\r\n }\r\n \r\n return null;\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 const iframe = getContainingIframe(element);\r\n const iframe_selector = iframe ? generateCssPath(iframe) : \"\";\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 || element.placeholder || '', //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 iframe_selector: iframe_selector\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\r\n\r\n function uniquifyElements(elements) {\r\n const seen = new Set();\r\n\r\n console.log(`Starting uniquification with ${elements.length} elements`);\r\n\r\n // Filter out testing infrastructure elements first\r\n const filteredInfrastructure = elements.filter(element_info => {\r\n // Skip the highlight-overlay element completely - it's part of the testing infrastructure\r\n if (element_info.element.id === 'highlight-overlay' || \r\n (element_info.css_selector && element_info.css_selector.includes('#highlight-overlay'))) {\r\n console.log('Filtered out testing infrastructure element:', element_info.css_selector);\r\n return false;\r\n }\r\n \r\n // Filter out UI framework container/manager elements\r\n const el = element_info.element;\r\n // UI framework container checks - generic detection for any framework\r\n if ((el.getAttribute('data-rendered-by') || \r\n el.getAttribute('data-reactroot') || \r\n el.getAttribute('ng-version') || \r\n el.getAttribute('data-component-id') ||\r\n el.getAttribute('data-root') ||\r\n el.getAttribute('data-framework')) && \r\n (el.className && \r\n typeof el.className === 'string' && \r\n (el.className.includes('Container') || \r\n el.className.includes('container') || \r\n el.className.includes('Manager') || \r\n el.className.includes('manager')))) {\r\n console.log('Filtered out UI framework container element:', element_info.css_selector);\r\n return false;\r\n }\r\n \r\n // Direct filter for framework container elements that shouldn't be interactive\r\n // Consolidating multiple container detection patterns into one efficient check\r\n const isFullViewport = element_info.bounding_box && \r\n element_info.bounding_box.x <= 5 && \r\n element_info.bounding_box.y <= 5 && \r\n element_info.bounding_box.width >= (window.innerWidth * 0.95) && \r\n element_info.bounding_box.height >= (window.innerHeight * 0.95);\r\n \r\n // Empty content check\r\n const isEmpty = !el.innerText || el.innerText.trim() === '';\r\n \r\n // Check if it's a framework container element\r\n if (element_info.element.tagName === 'DIV' && \r\n isFullViewport && \r\n isEmpty && \r\n (\r\n // Pattern matching for root containers\r\n (element_info.xpath && \r\n (element_info.xpath.match(/^\\/html\\[\\d+\\]\\/body\\[\\d+\\]\\/div\\[\\d+\\]\\/div\\[\\d+\\]$/) || \r\n element_info.xpath.match(/^\\/\\/\\*\\[@id='[^']+'\\]\\/div\\[\\d+\\]$/))) ||\r\n \r\n // Simple DOM structure\r\n (element_info.css_selector.split(' > ').length <= 4 && element_info.depth <= 5) ||\r\n \r\n // Empty or container-like classes\r\n (!el.className || el.className === '' || \r\n (typeof el.className === 'string' && \r\n (el.className.includes('overlay') || \r\n el.className.includes('container') || \r\n el.className.includes('wrapper'))))\r\n )) {\r\n console.log('Filtered out framework container element:', element_info.css_selector);\r\n return false;\r\n }\r\n \r\n return true;\r\n });\r\n\r\n // First filter out elements with zero dimensions\r\n const nonZeroElements = filteredInfrastructure.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 const filteredByParent = nonZeroElements.filter(element_info => {\r\n\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\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 const isOverlapping = areElementsOverlapping(element, other); \r\n return isOverlapping;\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 \r\n \r\n // Check for overlay removal\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 return nonOverlaidElements;\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 if(isTableCell(elementInfo)) {\r\n result = true;\r\n }\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\r\n function isTableCell(elementInfo) {\r\n const element = elementInfo.element;\r\n if(!element || !(element instanceof HTMLElement)) {\r\n return false;\r\n }\r\n const validTags = new Set(['td', 'th']);\r\n const validRoles = new Set(['cell', 'gridcell', 'columnheader', 'rowheader']);\r\n \r\n const tag = element.tagName.toLowerCase();\r\n const role = element.getAttribute('role')?.toLowerCase();\r\n\r\n if (validTags.has(tag) || (role && validRoles.has(role))) {\r\n return true;\r\n }\r\n return false;\r\n \r\n }\r\n\r\n function isOverlaid(elementInfo) {\r\n const element = elementInfo.element;\r\n const boundingRect = elementInfo.bounding_box;\r\n \r\n\r\n \r\n \r\n // Create a diagnostic logging function that only logs when needed\r\n const diagnosticLog = (...args) => {\r\n { // set to true for debugging\r\n console.log('[OVERLAY-DEBUG]', ...args);\r\n }\r\n };\r\n\r\n // Special handling for tooltips\r\n if (elementInfo.element.className && typeof elementInfo.element.className === 'string' && \r\n elementInfo.element.className.includes('tooltip')) {\r\n diagnosticLog('Element is a tooltip, not considering it overlaid');\r\n return false;\r\n }\r\n \r\n \r\n \r\n // Get element at the center point to check if it's covered by a popup/modal\r\n const middleX = boundingRect.x + boundingRect.width/2;\r\n const middleY = boundingRect.y + boundingRect.height/2;\r\n const elementAtMiddle = element.ownerDocument.elementFromPoint(middleX, middleY);\r\n \r\n if (elementAtMiddle && \r\n elementAtMiddle !== element && \r\n !isDecendent(element, elementAtMiddle) && \r\n !isDecendent(elementAtMiddle, element)) {\r\n\r\n // Add detailed logging for overlaid elements with formatted output\r\n console.log('[OVERLAY-DEBUG]', JSON.stringify({\r\n originalElement: {\r\n selector: elementInfo.css_selector,\r\n rect: {\r\n x: boundingRect.x,\r\n y: boundingRect.y,\r\n width: boundingRect.width,\r\n height: boundingRect.height,\r\n top: boundingRect.top,\r\n right: boundingRect.right,\r\n bottom: boundingRect.bottom,\r\n left: boundingRect.left\r\n }\r\n },\r\n overlayingElement: {\r\n selector: generateCssPath(elementAtMiddle),\r\n rect: {\r\n x: elementAtMiddle.getBoundingClientRect().x,\r\n y: elementAtMiddle.getBoundingClientRect().y,\r\n width: elementAtMiddle.getBoundingClientRect().width,\r\n height: elementAtMiddle.getBoundingClientRect().height,\r\n top: elementAtMiddle.getBoundingClientRect().top,\r\n right: elementAtMiddle.getBoundingClientRect().right,\r\n bottom: elementAtMiddle.getBoundingClientRect().bottom,\r\n left: elementAtMiddle.getBoundingClientRect().left\r\n }\r\n },\r\n middlePoint: { x: middleX, y: middleY }\r\n }, null, 2));\r\n\r\n console.log('[OVERLAY-REMOVED]', elementInfo.css_selector, elementInfo.bounding_box, 'elementAtMiddle:', elementAtMiddle, 'elementAtMiddle selector:', generateCssPath(elementAtMiddle));\r\n return true;\r\n }\r\n \r\n // Check specifically if the element at middle is a popup/modal\r\n // if (elementAtMiddle && isElementOrAncestorPopup(elementAtMiddle)) {\r\n // diagnosticLog('Element at middle is a popup/modal, element IS overlaid');\r\n // return true; // It's under a popup, so it is overlaid\r\n // }\r\n return false;\r\n // }\r\n // return false;\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, handleScroll);\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\r\n // Capture viewport dimensions\r\n const viewportData = {\r\n width: window.innerWidth,\r\n height: window.innerHeight,\r\n documentWidth: document.documentElement.clientWidth,\r\n documentHeight: document.documentElement.clientHeight,\r\n timestamp: new Date().toISOString()\r\n };\r\n\r\n // Add viewport data to the JSON output\r\n json.viewport = viewportData;\r\n\r\n\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, verbose=true) {\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 \r\n // More comprehensive visibility check\r\n const visibleElements = uniqueElements.filter(elementInfo => {\r\n const el = elementInfo.element;\r\n const style = getComputedStyle(el);\r\n \r\n // Check various style properties that affect visibility\r\n if (style.display === 'none' || \r\n style.visibility === 'hidden' || \r\n parseFloat(style.opacity) === 0) {\r\n return false;\r\n }\r\n \r\n // Check if element has non-zero dimensions\r\n const rect = el.getBoundingClientRect();\r\n if (rect.width === 0 || rect.height === 0) {\r\n return false;\r\n }\r\n \r\n // Check if element is within viewport\r\n if (rect.bottom < 0 || \r\n rect.top > window.innerHeight || \r\n rect.right < 0 || \r\n rect.left > window.innerWidth) {\r\n // Element is outside viewport, but still might be valid \r\n // if user scrolls to it, so we'll include it\r\n return true;\r\n }\r\n \r\n return true;\r\n });\r\n \r\n console.log(`Out of which ${visibleElements.length} elements are visible:`);\r\n if (verbose) {\r\n visibleElements.forEach(info => {\r\n console.log(`Element ${info.index}:`, info);\r\n });\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, handleScroll=false) {\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: 2147483647;\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 if (handleScroll) {\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\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.getElementInfo = getElementInfo;\n exports.highlight = highlight;\n exports.highlightElements = highlightElements;\n exports.unhighlightElements = unhighlightElements;\n\n Object.defineProperty(exports, '__esModule', { value: true });\n\n}));\n//# sourceMappingURL=probolabs.umd.js.map\n";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
const highlighterCode = "(function (global, factory) {\n typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :\n typeof define === 'function' && define.amd ? define(['exports'], factory) :\n (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.ProboLabs = {}));\n})(this, (function (exports) { 'use strict';\n\n const ElementTag = {\n CLICKABLE: \"CLICKABLE\", // button, link, toggle switch, checkbox, radio, dropdowns, clickable divs\n FILLABLE: \"FILLABLE\", // input, textarea content_editable, date picker??\n SELECTABLE: \"SELECTABLE\", // select\n NON_INTERACTIVE_ELEMENT: 'NON_INTERACTIVE_ELEMENT',\n };\n\n class ElementInfo {\n constructor(element, index, {tag, type, text, html, xpath, css_selector, bounding_box, iframe_selector}) {\n this.index = index.toString();\n this.tag = tag;\n this.type = type;\n this.text = text;\n this.html = html;\n this.xpath = xpath;\n this.css_selector = css_selector;\n this.bounding_box = bounding_box;\n this.iframe_selector = iframe_selector;\n this.element = element;\n this.depth = -1;\n }\n\n getSelector() {\n return this.xpath ? this.xpath : this.css_selector;\n }\n\n getDepth() {\n if (this.depth >= 0) {\n return this.depth;\n }\n \n this.depth = 0;\n let currentElement = this.element;\n \n while (currentElement.nodeType === Node.ELEMENT_NODE) { \n this.depth++;\n if (currentElement.assignedSlot) {\n currentElement = currentElement.assignedSlot;\n }\n else {\n currentElement = currentElement.parentNode;\n // Check if we're at a shadow root\n if (currentElement && currentElement.nodeType !== Node.ELEMENT_NODE && currentElement.getRootNode() instanceof ShadowRoot) {\n // Get the shadow root's host element\n currentElement = currentElement.getRootNode().host; \n }\n }\n }\n \n return this.depth;\n }\n }\n\n // import { realpath } from \"fs\";\n\n function getAllDocumentElementsIncludingShadow(selectors, root = document) {\n const elements = Array.from(root.querySelectorAll(selectors));\n\n root.querySelectorAll('*').forEach(el => {\n if (el.shadowRoot) {\n elements.push(...getAllDocumentElementsIncludingShadow(selectors, el.shadowRoot));\n }\n });\n return elements;\n }\n\n function getAllFrames(root = document) {\n const result = [root];\n const frames = getAllDocumentElementsIncludingShadow('frame, iframe', root); \n frames.forEach(frame => {\n try {\n const frameDocument = frame.contentDocument || frame.contentWindow.document;\n if (frameDocument) {\n result.push(frameDocument);\n }\n } catch (e) {\n // Skip cross-origin frames\n console.warn('Could not access frame content:', e.message);\n }\n });\n\n return result;\n }\n\n function getAllElementsIncludingShadow(selectors, root = document) {\n const elements = [];\n\n getAllFrames(root).forEach(doc => {\n elements.push(...getAllDocumentElementsIncludingShadow(selectors, doc));\n });\n\n return elements;\n }\n\n /**\n * Deeply searches through DOM trees including Shadow DOM and frames/iframes\n * @param {string} selector - CSS selector to search for\n * @param {Document|Element} [root=document] - Starting point for the search\n * @param {Object} [options] - Search options\n * @param {boolean} [options.searchShadow=true] - Whether to search Shadow DOM\n * @param {boolean} [options.searchFrames=true] - Whether to search frames/iframes\n * @returns {Element[]} Array of found elements\n \n function getAllElementsIncludingShadow(selector, root = document, options = {}) {\n const {\n searchShadow = true,\n searchFrames = true\n } = options;\n\n const results = new Set();\n \n // Helper to check if an element is valid and not yet found\n const addIfValid = (element) => {\n if (element && !results.has(element)) {\n results.add(element);\n }\n };\n\n // Helper to process a single document or element\n function processNode(node) {\n // Search regular DOM\n node.querySelectorAll(selector).forEach(addIfValid);\n\n if (searchShadow) {\n // Search all shadow roots\n const treeWalker = document.createTreeWalker(\n node,\n NodeFilter.SHOW_ELEMENT,\n {\n acceptNode: (element) => {\n return element.shadowRoot ? \n NodeFilter.FILTER_ACCEPT : \n NodeFilter.FILTER_SKIP;\n }\n }\n );\n\n while (treeWalker.nextNode()) {\n const element = treeWalker.currentNode;\n if (element.shadowRoot) {\n // Search within shadow root\n element.shadowRoot.querySelectorAll(selector).forEach(addIfValid);\n // Recursively process the shadow root for nested shadow DOMs\n processNode(element.shadowRoot);\n }\n }\n }\n\n if (searchFrames) {\n // Search frames and iframes\n const frames = node.querySelectorAll('frame, iframe');\n frames.forEach(frame => {\n try {\n const frameDocument = frame.contentDocument;\n if (frameDocument) {\n processNode(frameDocument);\n }\n } catch (e) {\n // Skip cross-origin frames\n console.warn('Could not access frame content:', e.message);\n }\n });\n }\n }\n\n // Start processing from the root\n processNode(root);\n\n return Array.from(results);\n }\n */\n // <div x=1 y=2 role='combobox'> </div>\n function findDropdowns() {\n const dropdowns = [];\n \n // Native select elements\n dropdowns.push(...getAllElementsIncludingShadow('select'));\n \n // Elements with dropdown roles that don't have <input>..</input>\n const roleElements = getAllElementsIncludingShadow('[role=\"combobox\"], [role=\"listbox\"], [role=\"dropdown\"], [role=\"option\"], [role=\"menu\"], [role=\"menuitem\"]').filter(el => {\n return el.tagName.toLowerCase() !== 'input' || ![\"button\", \"checkbox\", \"radio\"].includes(el.getAttribute(\"type\"));\n });\n dropdowns.push(...roleElements);\n \n // Common dropdown class patterns\n const dropdownPattern = /.*(dropdown|select|combobox|menu).*/i;\n const elements = getAllElementsIncludingShadow('*');\n const dropdownClasses = Array.from(elements).filter(el => {\n const hasDropdownClass = dropdownPattern.test(el.className);\n const validTag = ['li', 'ul', 'span', 'div', 'p', 'a', 'button'].includes(el.tagName.toLowerCase());\n const style = window.getComputedStyle(el); \n const result = hasDropdownClass && validTag && (style.cursor === 'pointer' || el.tagName.toLowerCase() === 'a' || el.tagName.toLowerCase() === 'button');\n return result;\n });\n \n dropdowns.push(...dropdownClasses);\n \n // Elements with aria-haspopup attribute\n dropdowns.push(...getAllElementsIncludingShadow('[aria-haspopup=\"true\"], [aria-haspopup=\"listbox\"], [aria-haspopup=\"menu\"]'));\n\n // Improve navigation element detection\n // Semantic nav elements with list items\n dropdowns.push(...getAllElementsIncludingShadow('nav ul li, nav ol li'));\n \n // Navigation elements in common design patterns\n dropdowns.push(...getAllElementsIncludingShadow('header a, .header a, .nav a, .navigation a, .menu a, .sidebar a, aside a'));\n \n // Elements in primary navigation areas with common attributes\n dropdowns.push(...getAllElementsIncludingShadow('[role=\"navigation\"] a, [aria-label*=\"navigation\"] a, [aria-label*=\"menu\"] a'));\n\n return dropdowns;\n }\n\n function findClickables() {\n const clickables = [];\n \n const checkboxPattern = /checkbox/i;\n // Collect all clickable elements first\n const nativeLinks = [...getAllElementsIncludingShadow('a')];\n const nativeButtons = [...getAllElementsIncludingShadow('button')];\n const inputButtons = [...getAllElementsIncludingShadow('input[type=\"button\"], input[type=\"submit\"], input[type=\"reset\"]')];\n const roleButtons = [...getAllElementsIncludingShadow('[role=\"button\"]')];\n // const tabbable = [...getAllElementsIncludingShadow('[tabindex=\"0\"]')];\n const clickHandlers = [...getAllElementsIncludingShadow('[onclick]')];\n const dropdowns = findDropdowns();\n const nativeCheckboxes = [...getAllElementsIncludingShadow('input[type=\"checkbox\"]')]; \n const fauxCheckboxes = getAllElementsIncludingShadow('*').filter(el => {\n if (checkboxPattern.test(el.className)) {\n const realCheckboxes = getAllElementsIncludingShadow('input[type=\"checkbox\"]', el);\n if (realCheckboxes.length === 1) {\n const boundingRect = realCheckboxes[0].getBoundingClientRect();\n return boundingRect.width <= 1 && boundingRect.height <= 1 \n }\n }\n return false;\n });\n const nativeRadios = [...getAllElementsIncludingShadow('input[type=\"radio\"]')];\n const toggles = findToggles();\n const pointerElements = findElementsWithPointer();\n // Add all elements at once\n clickables.push(\n ...nativeLinks,\n ...nativeButtons,\n ...inputButtons,\n ...roleButtons,\n // ...tabbable,\n ...clickHandlers,\n ...dropdowns,\n ...nativeCheckboxes,\n ...fauxCheckboxes,\n ...nativeRadios,\n ...toggles,\n ...pointerElements\n );\n\n // Only uniquify once at the end\n return clickables; // Let findElements handle the uniquification\n }\n\n function findToggles() {\n const toggles = [];\n const checkboxes = getAllElementsIncludingShadow('input[type=\"checkbox\"]');\n const togglePattern = /switch|toggle|slider/i;\n\n checkboxes.forEach(checkbox => {\n let isToggle = false;\n\n // Check the checkbox itself\n if (togglePattern.test(checkbox.className) || togglePattern.test(checkbox.getAttribute('role') || '')) {\n isToggle = true;\n }\n\n // Check parent elements (up to 3 levels)\n if (!isToggle) {\n let element = checkbox;\n for (let i = 0; i < 3; i++) {\n const parent = element.parentElement;\n if (!parent) break;\n\n const className = parent.className || '';\n const role = parent.getAttribute('role') || '';\n\n if (togglePattern.test(className) || togglePattern.test(role)) {\n isToggle = true;\n break;\n }\n element = parent;\n }\n }\n\n // Check next sibling\n if (!isToggle) {\n const nextSibling = checkbox.nextElementSibling;\n if (nextSibling) {\n const className = nextSibling.className || '';\n const role = nextSibling.getAttribute('role') || '';\n if (togglePattern.test(className) || togglePattern.test(role)) {\n isToggle = true;\n }\n }\n }\n\n if (isToggle) {\n toggles.push(checkbox);\n }\n });\n\n return toggles;\n }\n\n function findNonInteractiveElements() {\n // Get all elements in the document\n const all = Array.from(getAllElementsIncludingShadow('*'));\n \n // Filter elements based on Python implementation rules\n return all.filter(element => {\n if (!element.firstElementChild) {\n const tag = element.tagName.toLowerCase(); \n if (!['select', 'button', 'a'].includes(tag)) {\n const validTags = ['p', 'span', 'div', 'input', 'textarea'].includes(tag) || /^h\\d$/.test(tag) || /text/.test(tag);\n const boundingRect = element.getBoundingClientRect();\n return validTags && boundingRect.height > 1 && boundingRect.width > 1;\n }\n }\n return false;\n });\n }\n\n\n\n // export function findNonInteractiveElements() {\n // const all = [];\n // try {\n // const elements = getAllElementsIncludingShadow('*');\n // all.push(...elements);\n // } catch (e) {\n // console.warn('Error getting elements:', e);\n // }\n \n // console.debug('Total elements found:', all.length);\n \n // return all.filter(element => {\n // try {\n // const tag = element.tagName.toLowerCase(); \n\n // // Special handling for input elements\n // if (tag === 'input' || tag === 'textarea') {\n // const boundingRect = element.getBoundingClientRect();\n // const value = element.value || '';\n // const placeholder = element.placeholder || '';\n // return boundingRect.height > 1 && \n // boundingRect.width > 1 && \n // (value.trim() !== '' || placeholder.trim() !== '');\n // }\n\n \n // // Check if it's a valid tag for text content\n // const validTags = ['p', 'span', 'div', 'label', 'th', 'td', 'li', 'button', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'a', 'select'].includes(tag) || \n // /^h\\d$/.test(tag) || \n // /text/.test(tag);\n\n // const boundingRect = element.getBoundingClientRect();\n\n // // Get direct text content, excluding child element text\n // let directText = '';\n // for (const node of element.childNodes) {\n // // Only include text nodes (nodeType 3)\n // if (node.nodeType === 3) {\n // directText += node.textContent || '';\n // }\n // }\n \n // // If no direct text and it's a table cell or heading, check label content\n // if (!directText.trim() && (tag === 'th' || tag === 'td' || tag === 'h1')) {\n // const labels = element.getElementsByTagName('label');\n // for (const label of labels) {\n // directText += label.textContent || '';\n // }\n // }\n\n // // If still no text and it's a heading, get all text content\n // if (!directText.trim() && tag === 'h1') {\n // directText = element.textContent || '';\n // }\n\n // directText = directText.trim();\n\n // // Debug logging\n // if (directText) {\n // console.debugg('Text element found:', {\n // tag,\n // text: directText,\n // dimensions: boundingRect,\n // element\n // });\n // }\n\n // return validTags && \n // boundingRect.height > 1 && \n // boundingRect.width > 1 && \n // directText !== '';\n \n // } catch (e) {\n // console.warn('Error processing element:', e);\n // return false;\n // }\n // });\n // }\n\n\n\n\n\n function findElementsWithPointer() {\n const elements = [];\n const allElements = getAllElementsIncludingShadow('*');\n \n console.log('Checking elements with pointer style...');\n \n allElements.forEach(element => {\n // Skip SVG elements for now\n if (element instanceof SVGElement || element.tagName.toLowerCase() === 'svg') {\n return;\n }\n \n const style = window.getComputedStyle(element);\n if (style.cursor === 'pointer') {\n elements.push(element);\n }\n });\n \n console.log(`Found ${elements.length} elements with pointer cursor`);\n return elements;\n }\n\n function findCheckables() {\n const elements = [];\n\n elements.push(...getAllElementsIncludingShadow('input[type=\"checkbox\"]'));\n elements.push(...getAllElementsIncludingShadow('input[type=\"radio\"]'));\n const all_elements = getAllElementsIncludingShadow('label');\n const radioClasses = Array.from(all_elements).filter(el => {\n return /.*radio.*/i.test(el.className); \n });\n elements.push(...radioClasses);\n return elements;\n }\n\n function findFillables() {\n const elements = [];\n\n const inputs = [...getAllElementsIncludingShadow('input:not([type=\"radio\"]):not([type=\"checkbox\"])')];\n console.log('Found inputs:', inputs.length, inputs);\n elements.push(...inputs);\n \n const textareas = [...getAllElementsIncludingShadow('textarea')];\n console.log('Found textareas:', textareas.length);\n elements.push(...textareas);\n \n const editables = [...getAllElementsIncludingShadow('[contenteditable=\"true\"]')];\n console.log('Found editables:', editables.length);\n elements.push(...editables);\n\n return elements;\n }\n\n // Helper function to check if element is a form control\n function isFormControl(elementInfo) {\n return /^(input|select|textarea|button|label)$/i.test(elementInfo.tag);\n }\n\n const isDropdownItem = (elementInfo) => {\n const dropdownPatterns = [\n /dropdown[-_]?item/i, // matches: dropdown-item, dropdownitem, dropdown_item\n /menu[-_]?item/i, // matches: menu-item, menuitem, menu_item\n /dropdown[-_]?link/i, // matches: dropdown-link, dropdownlink, dropdown_link\n /list[-_]?item/i, // matches: list-item, listitem, list_item\n /select[-_]?item/i, // matches: select-item, selectitem, select_item \n ];\n\n const rolePatterns = [\n /menu[-_]?item/i, // matches: menuitem, menu-item\n /option/i, // matches: option\n /list[-_]?item/i, // matches: listitem, list-item\n /tree[-_]?item/i // matches: treeitem, tree-item\n ];\n\n const hasMatchingClass = elementInfo.element.className && \n dropdownPatterns.some(pattern => \n pattern.test(elementInfo.element.className)\n );\n\n const hasMatchingRole = elementInfo.element.getAttribute('role') && \n rolePatterns.some(pattern => \n pattern.test(elementInfo.element.getAttribute('role'))\n );\n\n return hasMatchingClass || hasMatchingRole;\n };\n\n /**\n * Finds the first element matching a CSS selector, traversing Shadow DOM if necessary\n * @param {string} selector - CSS selector to search for\n * @param {Element} [root=document] - Root element to start searching from\n * @returns {Element|null} - The first matching element or null if not found\n */\n function querySelectorShadow(selector, root = document) {\n // First try to find in light DOM\n let element = root.querySelector(selector);\n if (element) return element;\n \n // Get all elements with shadow root\n const shadowElements = Array.from(root.querySelectorAll('*'))\n .filter(el => el.shadowRoot);\n \n // Search through each shadow root until we find a match\n for (const el of shadowElements) {\n element = querySelectorShadow(selector, el.shadowRoot);\n if (element) return element;\n }\n \n return null;\n }\n\n const getElementByXPathOrCssSelector = (element_info) => {\n let element;\n\n console.log('getElementByXPathOrCssSelector:', element_info);\n // if (element_info.xpath) { //try xpath if exists\n // element = document.evaluate(\n // element_info.xpath, \n // document, \n // null, \n // XPathResult.FIRST_ORDERED_NODE_TYPE, \n // null\n // ).singleNodeValue;\n \n // if (!element) {\n // console.warn('Failed to find element with xpath:', element_info.xpath);\n // }\n // }\n // else { //try CSS selector\n if (element_info.iframe_selector) {\n console.log('elementInfo with iframe: ', element_info);\n const frames = getAllDocumentElementsIncludingShadow('iframe');\n \n // Iterate over all frames and compare their CSS selectors\n for (const frame of frames) {\n const cssSelector = generateCssPath(frame);\n if (cssSelector === element_info.iframe_selector) {\n const frameDocument = frame.contentDocument || frame.contentWindow.document;\n element = querySelectorShadow(element_info.css_selector, frameDocument);\n console.log('found element ', element);\n break;\n } \n } }\n else\n element = querySelectorShadow(element_info.css_selector);\n // console.log('found element by CSS elector: ', element);\n if (!element) {\n console.warn('Failed to find element with CSS selector:', element_info.css_selector);\n }\n // }\n\n return element;\n };\n\n function generateXPath(element) {\n if (!element || element.getRootNode() instanceof ShadowRoot) return '';\n \n // If element has an id, use that (it's unique and shorter)\n if (element.id) {\n return `//*[@id=\"${element.id}\"]`;\n }\n \n const parts = [];\n let current = element;\n \n while (current && current.nodeType === Node.ELEMENT_NODE) {\n let index = 1;\n let sibling = current.previousSibling;\n \n while (sibling) {\n if (sibling.nodeType === Node.ELEMENT_NODE && sibling.tagName === current.tagName) {\n index++;\n }\n sibling = sibling.previousSibling;\n }\n \n const tagName = current.tagName.toLowerCase();\n parts.unshift(`${tagName}[${index}]`);\n current = current.parentNode;\n }\n \n return '/' + parts.join('/');\n }\n\n function isDecendent(parent, child) {\n let element = child;\n while (element.nodeType === Node.ELEMENT_NODE) { \n \n if (element.assignedSlot) {\n element = element.assignedSlot;\n }\n else {\n element = element.parentNode;\n // Check if we're at a shadow root\n if (element && element.nodeType !== Node.ELEMENT_NODE && element.getRootNode() instanceof ShadowRoot) {\n // Get the shadow root's host element\n element = element.getRootNode().host; \n }\n }\n if (element === parent)\n return true;\n }\n return false;\n }\n\n function generateCssPath(element) {\n if (!element) {\n console.error('ERROR: No element provided to generateCssPath returning empty string');\n return '';\n }\n const path = [];\n // console.group('Generating CSS path for:', element);\n while (element && element.nodeType === Node.ELEMENT_NODE) { \n let selector = element.nodeName.toLowerCase();\n // console.log('Element:', selector, element);\n \n // if (element.id) {\n // //escape special characters\n // const normalized_id = element.id.replace(/[:;.#()[\\]!@$%^&*]/g, '\\\\$&');\n // selector = `#${normalized_id}`;\n // path.unshift(selector);\n // break;\n // } \n \n let sibling = element;\n let nth = 1;\n while (sibling = sibling.previousElementSibling) {\n if (sibling.nodeName.toLowerCase() === selector) nth++;\n }\n sibling = element;\n while (sibling = sibling.nextElementSibling) {\n if (sibling.nodeName.toLowerCase() === selector) {\n break;\n }\n }\n selector += `:nth-of-type(${nth})`;\n \n \n path.unshift(selector);\n //console.log(` Current path: ${path.join(' > ')}`);\n\n if (element.assignedSlot) {\n element = element.assignedSlot;\n // console.log(' Moving to assigned slot');\n }\n else {\n element = element.parentNode;\n // console.log(' Moving to parent:', element);\n\n // Check if we're at a shadow root\n if (element && element.nodeType !== Node.ELEMENT_NODE && element.getRootNode() instanceof ShadowRoot) {\n console.log(' Found shadow root, moving to host');\n // Get the shadow root's host element\n element = element.getRootNode().host; \n }\n }\n }\n \n // console.log('Final selector:', path.join(' > '));\n // console.groupEnd();\n return path.join(' > ');\n }\n\n\n function cleanHTML(rawHTML) {\n const parser = new DOMParser();\n const doc = parser.parseFromString(rawHTML, \"text/html\");\n\n function cleanElement(element) {\n const allowedAttributes = new Set([\n \"role\",\n \"type\",\n \"class\",\n \"href\",\n \"alt\",\n \"title\",\n \"readonly\",\n \"checked\",\n \"enabled\",\n \"disabled\",\n ]);\n\n [...element.attributes].forEach(attr => {\n const name = attr.name.toLowerCase();\n const value = attr.value;\n\n const isTestAttribute = /^(testid|test-id|data-test-id)$/.test(name);\n const isDataAttribute = name.startsWith(\"data-\") && value;\n const isBooleanAttribute = [\"readonly\", \"checked\", \"enabled\", \"disabled\"].includes(name);\n\n if (!allowedAttributes.has(name) && !isDataAttribute && !isTestAttribute && !isBooleanAttribute) {\n element.removeAttribute(name);\n }\n });\n\n // Handle SVG content - more aggressive replacement\n if (element.tagName.toLowerCase() === \"svg\") {\n // Remove all attributes except class and role\n [...element.attributes].forEach(attr => {\n const name = attr.name.toLowerCase();\n if (name !== \"class\" && name !== \"role\") {\n element.removeAttribute(name);\n }\n });\n element.innerHTML = \"CONTENT REMOVED\";\n } else {\n // Recursively clean child elements\n Array.from(element.children).forEach(cleanElement);\n }\n\n // Only remove empty elements that aren't semantic or icon elements\n const keepEmptyElements = ['i', 'span', 'svg', 'button', 'input'];\n if (!keepEmptyElements.includes(element.tagName.toLowerCase()) && \n !element.children.length && \n !element.textContent.trim()) {\n element.remove();\n }\n }\n\n // Process all elements in the document body\n Array.from(doc.body.children).forEach(cleanElement);\n return doc.body.innerHTML;\n }\n\n function getContainingIframe(element) {\n // If not in an iframe, return null\n if (element.ownerDocument.defaultView === window.top) {\n return null;\n }\n \n // Try to find the iframe in the parent document that contains our element\n try {\n const parentDocument = element.ownerDocument.defaultView.parent.document;\n const iframes = parentDocument.querySelectorAll('iframe');\n \n for (const iframe of iframes) {\n if (iframe.contentWindow === element.ownerDocument.defaultView) {\n return iframe;\n }\n }\n } catch (e) {\n // Cross-origin restriction\n return \"Cross-origin iframe - cannot access details\";\n }\n \n return null;\n }\n\n function getElementInfo(element, index) {\n\n const xpath = generateXPath(element);\n const css_selector = generateCssPath(element);\n\n const iframe = getContainingIframe(element);\n const iframe_selector = iframe ? generateCssPath(iframe) : \"\";\n\n // Return element info with pre-calculated values\n return new ElementInfo(element, index, {\n tag: element.tagName.toLowerCase(),\n type: element.type || '',\n text: element.innerText || element.placeholder || '', //getTextContent(element),\n html: cleanHTML(element.outerHTML),\n xpath: xpath,\n css_selector: css_selector,\n bounding_box: element.getBoundingClientRect(),\n iframe_selector: iframe_selector\n });\n }\n\n\n\n\n const filterZeroDimensions = (elementInfo) => {\n const rect = elementInfo.bounding_box;\n //single pixel elements are typically faux controls and should be filtered too\n const hasSize = rect.width > 1 && rect.height > 1;\n const style = window.getComputedStyle(elementInfo.element);\n const isVisible = style.display !== 'none' && style.visibility !== 'hidden';\n \n if (!hasSize || !isVisible) {\n // if (elementInfo.element.isConnected) {\n // console.log('Filtered out invisible/zero-size element:', {\n // tag: elementInfo.tag,\n // xpath: elementInfo.xpath,\n // element: elementInfo.element,\n // hasSize,\n // isVisible,\n // dimensions: rect\n // });\n // }\n return false;\n }\n return true;\n };\n\n\n\n function uniquifyElements(elements) {\n const seen = new Set();\n\n console.log(`Starting uniquification with ${elements.length} elements`);\n\n // Filter out testing infrastructure elements first\n const filteredInfrastructure = elements.filter(element_info => {\n // Skip the highlight-overlay element completely - it's part of the testing infrastructure\n if (element_info.element.id === 'highlight-overlay' || \n (element_info.css_selector && element_info.css_selector.includes('#highlight-overlay'))) {\n console.log('Filtered out testing infrastructure element:', element_info.css_selector);\n return false;\n }\n \n // Filter out UI framework container/manager elements\n const el = element_info.element;\n // UI framework container checks - generic detection for any framework\n if ((el.getAttribute('data-rendered-by') || \n el.getAttribute('data-reactroot') || \n el.getAttribute('ng-version') || \n el.getAttribute('data-component-id') ||\n el.getAttribute('data-root') ||\n el.getAttribute('data-framework')) && \n (el.className && \n typeof el.className === 'string' && \n (el.className.includes('Container') || \n el.className.includes('container') || \n el.className.includes('Manager') || \n el.className.includes('manager')))) {\n console.log('Filtered out UI framework container element:', element_info.css_selector);\n return false;\n }\n \n // Direct filter for framework container elements that shouldn't be interactive\n // Consolidating multiple container detection patterns into one efficient check\n const isFullViewport = element_info.bounding_box && \n element_info.bounding_box.x <= 5 && \n element_info.bounding_box.y <= 5 && \n element_info.bounding_box.width >= (window.innerWidth * 0.95) && \n element_info.bounding_box.height >= (window.innerHeight * 0.95);\n \n // Empty content check\n const isEmpty = !el.innerText || el.innerText.trim() === '';\n \n // Check if it's a framework container element\n if (element_info.element.tagName === 'DIV' && \n isFullViewport && \n isEmpty && \n (\n // Pattern matching for root containers\n (element_info.xpath && \n (element_info.xpath.match(/^\\/html\\[\\d+\\]\\/body\\[\\d+\\]\\/div\\[\\d+\\]\\/div\\[\\d+\\]$/) || \n element_info.xpath.match(/^\\/\\/\\*\\[@id='[^']+'\\]\\/div\\[\\d+\\]$/))) ||\n \n // Simple DOM structure\n (element_info.css_selector.split(' > ').length <= 4 && element_info.depth <= 5) ||\n \n // Empty or container-like classes\n (!el.className || el.className === '' || \n (typeof el.className === 'string' && \n (el.className.includes('overlay') || \n el.className.includes('container') || \n el.className.includes('wrapper'))))\n )) {\n console.log('Filtered out framework container element:', element_info.css_selector);\n return false;\n }\n \n return true;\n });\n\n // First filter out elements with zero dimensions\n const nonZeroElements = filteredInfrastructure.filter(filterZeroDimensions);\n // sort by CSS selector depth so parents are processed first\n nonZeroElements.sort((a, b) => a.getDepth() - b.getDepth());\n console.log(`After dimension filtering: ${nonZeroElements.length} elements remain (${elements.length - nonZeroElements.length} removed)`);\n \n const filteredByParent = nonZeroElements.filter(element_info => {\n\n const parent = findClosestParent(seen, element_info);\n const keep = parent == null || shouldKeepNestedElement(element_info, parent);\n // console.log(\"node \", element_info.index, \": keep=\", keep, \" parent=\", parent);\n // if (!keep && !element_info.xpath) {\n // console.log(\"Filtered out element \", element_info,\" because it's a nested element of \", parent);\n // }\n if (keep)\n seen.add(element_info.css_selector);\n\n return keep;\n });\n\n console.log(`After parent/child filtering: ${filteredByParent.length} elements remain (${nonZeroElements.length - filteredByParent.length} removed)`);\n\n // Final overlap filtering\n const filteredResults = filteredByParent.filter(element => {\n\n // Look for any element that came BEFORE this one in the array\n const hasEarlierOverlap = filteredByParent.some(other => {\n // Only check elements that came before (lower index)\n if (filteredByParent.indexOf(other) >= filteredByParent.indexOf(element)) {\n return false;\n }\n \n const isOverlapping = areElementsOverlapping(element, other); \n return isOverlapping;\n }); \n\n // Keep element if it has no earlier overlapping elements\n return !hasEarlierOverlap;\n });\n \n \n \n // Check for overlay removal\n console.log(`After filtering: ${filteredResults.length} (${filteredByParent.length - filteredResults.length} removed by overlap)`);\n \n const nonOverlaidElements = filteredResults.filter(element => {\n return !isOverlaid(element);\n });\n\n console.log(`Final elements after overlay removal: ${nonOverlaidElements.length} (${filteredResults.length - nonOverlaidElements.length} removed)`);\n \n return nonOverlaidElements;\n\n }\n\n\n\n const areElementsOverlapping = (element1, element2) => {\n if (element1.css_selector === element2.css_selector) {\n return true;\n }\n \n const box1 = element1.bounding_box;\n const box2 = element2.bounding_box;\n \n return box1.x === box2.x &&\n box1.y === box2.y &&\n box1.width === box2.width &&\n box1.height === box2.height;\n // element1.text === element2.text &&\n // element2.tag === 'a';\n };\n\n function findClosestParent(seen, element_info) { \n // //Use element child/parent queries\n // let parent = element_info.element.parentNode;\n // if (parent.getRootNode() instanceof ShadowRoot) {\n // // Get the shadow root's host element\n // parent = parent.getRootNode().host; \n // }\n\n // while (parent.nodeType === Node.ELEMENT_NODE) { \n // const css_selector = generateCssPath(parent);\n // if (seen.has(css_selector)) {\n // console.log(\"element \", element_info, \" closest parent is \", parent)\n // return parent; \n // }\n // parent = parent.parentNode;\n // if (parent.getRootNode() instanceof ShadowRoot) {\n // // Get the shadow root's host element\n // parent = parent.getRootNode().host; \n // }\n // }\n\n // Split the xpath into segments\n const segments = element_info.css_selector.split(' > ');\n \n // Try increasingly shorter paths until we find one in the seen set\n for (let i = segments.length - 1; i > 0; i--) {\n const parentPath = segments.slice(0, i).join(' > ');\n if (seen.has(parentPath)) {\n return parentPath;\n }\n }\n\n return null;\n }\n\n function shouldKeepNestedElement(elementInfo, parentPath) {\n let result = false;\n const parentSegments = parentPath.split(' > ');\n\n const isParentLink = /^a(:nth-of-type\\(\\d+\\))?$/.test(parentSegments[parentSegments.length - 1]);\n if (isParentLink) {\n return false; \n }\n // If this is a checkbox/radio input\n if (elementInfo.tag === 'input' && \n (elementInfo.type === 'checkbox' || elementInfo.type === 'radio')) {\n \n // Check if parent is a label by looking at the parent xpath's last segment\n \n const isParentLabel = /^label(:nth-of-type\\(\\d+\\))?$/.test(parentSegments[parentSegments.length - 1]);\n \n // If parent is a label, don't keep the input (we'll keep the label instead)\n if (isParentLabel) {\n return false;\n }\n }\n \n // Keep all other form controls and dropdown items\n if (isFormControl(elementInfo) || isDropdownItem(elementInfo)) {\n result = true;\n }\n\n if(isTableCell(elementInfo)) {\n result = true;\n }\n \n \n // console.log(`shouldKeepNestedElement: ${elementInfo.tag} ${elementInfo.text} ${elementInfo.xpath} -> ${parentXPath} -> ${result}`);\n return result;\n }\n\n\n function isTableCell(elementInfo) {\n const element = elementInfo.element;\n if(!element || !(element instanceof HTMLElement)) {\n return false;\n }\n const validTags = new Set(['td', 'th']);\n const validRoles = new Set(['cell', 'gridcell', 'columnheader', 'rowheader']);\n \n const tag = element.tagName.toLowerCase();\n const role = element.getAttribute('role')?.toLowerCase();\n\n if (validTags.has(tag) || (role && validRoles.has(role))) {\n return true;\n }\n return false;\n \n }\n\n function isOverlaid(elementInfo) {\n const element = elementInfo.element;\n const boundingRect = elementInfo.bounding_box;\n \n\n \n \n // Create a diagnostic logging function that only logs when needed\n const diagnosticLog = (...args) => {\n { // set to true for debugging\n console.log('[OVERLAY-DEBUG]', ...args);\n }\n };\n\n // Special handling for tooltips\n if (elementInfo.element.className && typeof elementInfo.element.className === 'string' && \n elementInfo.element.className.includes('tooltip')) {\n diagnosticLog('Element is a tooltip, not considering it overlaid');\n return false;\n }\n \n \n \n // Get element at the center point to check if it's covered by a popup/modal\n const middleX = boundingRect.x + boundingRect.width/2;\n const middleY = boundingRect.y + boundingRect.height/2;\n const elementAtMiddle = element.ownerDocument.elementFromPoint(middleX, middleY);\n \n if (elementAtMiddle && \n elementAtMiddle !== element && \n !isDecendent(element, elementAtMiddle) && \n !isDecendent(elementAtMiddle, element)) {\n\n // Add detailed logging for overlaid elements with formatted output\n console.log('[OVERLAY-DEBUG]', JSON.stringify({\n originalElement: {\n selector: elementInfo.css_selector,\n rect: {\n x: boundingRect.x,\n y: boundingRect.y,\n width: boundingRect.width,\n height: boundingRect.height,\n top: boundingRect.top,\n right: boundingRect.right,\n bottom: boundingRect.bottom,\n left: boundingRect.left\n }\n },\n overlayingElement: {\n selector: generateCssPath(elementAtMiddle),\n rect: {\n x: elementAtMiddle.getBoundingClientRect().x,\n y: elementAtMiddle.getBoundingClientRect().y,\n width: elementAtMiddle.getBoundingClientRect().width,\n height: elementAtMiddle.getBoundingClientRect().height,\n top: elementAtMiddle.getBoundingClientRect().top,\n right: elementAtMiddle.getBoundingClientRect().right,\n bottom: elementAtMiddle.getBoundingClientRect().bottom,\n left: elementAtMiddle.getBoundingClientRect().left\n }\n },\n middlePoint: { x: middleX, y: middleY }\n }, null, 2));\n\n console.log('[OVERLAY-REMOVED]', elementInfo.css_selector, elementInfo.bounding_box, 'elementAtMiddle:', elementAtMiddle, 'elementAtMiddle selector:', generateCssPath(elementAtMiddle));\n return true;\n }\n \n // Check specifically if the element at middle is a popup/modal\n // if (elementAtMiddle && isElementOrAncestorPopup(elementAtMiddle)) {\n // diagnosticLog('Element at middle is a popup/modal, element IS overlaid');\n // return true; // It's under a popup, so it is overlaid\n // }\n return false;\n // }\n // return false;\n }\n\n const highlight = {\n execute: async function(elementTypes, handleScroll=false) {\n const elements = await findElements(elementTypes);\n highlightElements(elements, handleScroll);\n return elements;\n },\n\n unexecute: function(handleScroll=false) {\n unhighlightElements(handleScroll);\n },\n\n generateJSON: async function() {\n const json = {};\n\n // Capture viewport dimensions\n const viewportData = {\n width: window.innerWidth,\n height: window.innerHeight,\n documentWidth: document.documentElement.clientWidth,\n documentHeight: document.documentElement.clientHeight,\n timestamp: new Date().toISOString()\n };\n\n // Add viewport data to the JSON output\n json.viewport = viewportData;\n\n\n await Promise.all(Object.values(ElementTag).map(async elementType => {\n const elements = await findElements(elementType);\n json[elementType] = elements;\n }));\n\n // Serialize the JSON object\n const jsonString = JSON.stringify(json, null, 4); // Pretty print with 4 spaces\n\n console.log(`JSON: ${jsonString}`);\n return jsonString;\n },\n\n getElementInfo\n };\n\n\n function unhighlightElements(handleScroll=false) {\n const documents = getAllFrames();\n documents.forEach(doc => {\n const overlay = doc.getElementById('highlight-overlay');\n if (overlay) {\n if (handleScroll) {\n // Remove event listeners\n doc.removeEventListener('scroll', overlay.scrollHandler, true);\n doc.removeEventListener('resize', overlay.resizeHandler);\n }\n overlay.remove();\n }\n });\n }\n\n\n\n\n async function findElements(elementTypes, verbose=true) {\n const typesArray = Array.isArray(elementTypes) ? elementTypes : [elementTypes];\n console.log('Starting element search for types:', typesArray);\n\n const elements = [];\n typesArray.forEach(elementType => {\n if (elementType === ElementTag.FILLABLE) {\n elements.push(...findFillables());\n }\n if (elementType === ElementTag.SELECTABLE) {\n elements.push(...findDropdowns());\n }\n if (elementType === ElementTag.CLICKABLE) {\n elements.push(...findClickables());\n elements.push(...findToggles());\n elements.push(...findCheckables());\n }\n if (elementType === ElementTag.NON_INTERACTIVE_ELEMENT) {\n elements.push(...findNonInteractiveElements());\n }\n });\n\n // console.log('Before uniquify:', elements.length);\n const elementsWithInfo = elements.map((element, index) => \n getElementInfo(element, index)\n );\n\n \n \n const uniqueElements = uniquifyElements(elementsWithInfo);\n console.log(`Found ${uniqueElements.length} elements:`);\n \n // More comprehensive visibility check\n const visibleElements = uniqueElements.filter(elementInfo => {\n const el = elementInfo.element;\n const style = getComputedStyle(el);\n \n // Check various style properties that affect visibility\n if (style.display === 'none' || \n style.visibility === 'hidden') {\n return false;\n }\n \n // Check if element has non-zero dimensions\n const rect = el.getBoundingClientRect();\n if (rect.width === 0 || rect.height === 0) {\n return false;\n }\n \n // Check if element is within viewport\n if (rect.bottom < 0 || \n rect.top > window.innerHeight || \n rect.right < 0 || \n rect.left > window.innerWidth) {\n // Element is outside viewport, but still might be valid \n // if user scrolls to it, so we'll include it\n return true;\n }\n \n return true;\n });\n \n console.log(`Out of which ${visibleElements.length} elements are visible:`);\n if (verbose) {\n visibleElements.forEach(info => {\n console.log(`Element ${info.index}:`, info);\n });\n }\n \n return visibleElements;\n }\n\n // elements is an array of objects with index, xpath\n function highlightElements(elements, handleScroll=false) {\n // console.log('Starting highlight for elements:', elements);\n \n // Create overlay if it doesn't exist and store it in a dictionary\n const documents = getAllFrames(); \n let overlays = {};\n documents.forEach(doc => {\n let overlay = doc.getElementById('highlight-overlay');\n if (!overlay) {\n overlay = doc.createElement('div');\n overlay.id = 'highlight-overlay';\n overlay.style.cssText = `\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n pointer-events: none;\n z-index: 2147483647;\n `;\n doc.body.appendChild(overlay);\n }\n overlays[doc.documentURI] = overlay;\n });\n \n\n const updateHighlights = (doc = null) => {\n if (doc) {\n overlays[doc.documentURI].innerHTML = '';\n } else {\n Object.values(overlays).forEach(overlay => { overlay.innerHTML = ''; });\n } \n elements.forEach(elementInfo => {\n // console.log('updateHighlights-Processing elementInfo:', elementInfo);\n let element = elementInfo.element; //getElementByXPathOrCssSelector(elementInfo);\n if (!element) {\n element = getElementByXPathOrCssSelector(elementInfo);\n if (!element)\n return;\n }\n \n //if highlights requested for a specific doc, skip unrelated elements\n if (doc && element.ownerDocument !== doc) {\n console.log(\"skipped element \", element, \" since it doesn't belong to document \", doc);\n return;\n }\n\n const rect = element.getBoundingClientRect();\n // console.log('Element rect:', elementInfo.tag, rect);\n \n if (rect.width === 0 || rect.height === 0) {\n console.warn('Element has zero dimensions:', elementInfo);\n return;\n }\n \n // Create border highlight (red rectangle)\n // use ownerDocument to support iframes/frames\n const highlight = element.ownerDocument.createElement('div');\n highlight.style.cssText = `\n position: fixed;\n left: ${rect.x}px;\n top: ${rect.y}px;\n width: ${rect.width}px;\n height: ${rect.height}px;\n border: 1px solid rgb(255, 0, 0);\n transition: all 0.2s ease-in-out;\n `;\n\n // Create index label container - now positioned to the right and slightly up\n const labelContainer = element.ownerDocument.createElement('div');\n labelContainer.style.cssText = `\n position: absolute;\n right: -10px; /* Offset to the right */\n top: -10px; /* Offset upwards */\n padding: 4px;\n background-color: rgba(255, 255, 0, 0.6);\n display: flex;\n align-items: center;\n justify-content: center;\n `;\n\n const text = element.ownerDocument.createElement('span');\n text.style.cssText = `\n color: rgb(0, 0, 0, 0.8);\n font-family: 'Courier New', Courier, monospace;\n font-size: 12px;\n font-weight: bold;\n line-height: 1;\n `;\n text.textContent = elementInfo.index;\n \n labelContainer.appendChild(text);\n highlight.appendChild(labelContainer); \n overlays[element.ownerDocument.documentURI].appendChild(highlight);\n });\n };\n\n // Initial highlight\n updateHighlights();\n\n if (handleScroll) {\n documents.forEach(doc => {\n // Update highlights on scroll and resize\n console.log('registering scroll and resize handlers for document: ', doc);\n const scrollHandler = () => {\n requestAnimationFrame(() => updateHighlights(doc));\n };\n const resizeHandler = () => {\n updateHighlights(doc);\n };\n doc.addEventListener('scroll', scrollHandler, true);\n doc.addEventListener('resize', resizeHandler);\n // Store event handlers for cleanup\n overlays[doc.documentURI].scrollHandler = scrollHandler;\n overlays[doc.documentURI].resizeHandler = resizeHandler;\n }); \n }\n }\n\n // function unexecute() {\n // unhighlightElements();\n // }\n\n // Make it available globally for both Extension and Playwright\n if (typeof window !== 'undefined') {\n window.ProboLabs = {\n ElementTag,\n highlight,\n unhighlightElements,\n findElements,\n highlightElements,\n getElementInfo\n };\n }\n\n exports.findElements = findElements;\n exports.getElementInfo = getElementInfo;\n exports.highlight = highlight;\n exports.highlightElements = highlightElements;\n exports.unhighlightElements = unhighlightElements;\n\n Object.defineProperty(exports, '__esModule', { value: true });\n\n}));\n//# sourceMappingURL=probolabs.umd.js.map\n";
|
|
2
|
+
import winston from 'winston';
|
|
3
|
+
|
|
4
|
+
const ElementTag = {
|
|
5
|
+
CLICKABLE: "CLICKABLE", // button, link, toggle switch, checkbox, radio, dropdowns, clickable divs
|
|
6
|
+
FILLABLE: "FILLABLE", // input, textarea content_editable, date picker??
|
|
7
|
+
SELECTABLE: "SELECTABLE", // select
|
|
8
|
+
NON_INTERACTIVE_ELEMENT: 'NON_INTERACTIVE_ELEMENT',
|
|
7
9
|
};
|
|
8
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Logging levels for Probo
|
|
13
|
+
*/
|
|
9
14
|
var ProboLogLevel;
|
|
10
15
|
(function (ProboLogLevel) {
|
|
11
16
|
ProboLogLevel["DEBUG"] = "DEBUG";
|
|
@@ -14,67 +19,85 @@ var ProboLogLevel;
|
|
|
14
19
|
ProboLogLevel["WARN"] = "WARN";
|
|
15
20
|
ProboLogLevel["ERROR"] = "ERROR";
|
|
16
21
|
})(ProboLogLevel || (ProboLogLevel = {}));
|
|
17
|
-
//
|
|
18
|
-
const
|
|
19
|
-
[ProboLogLevel.DEBUG]:
|
|
20
|
-
[ProboLogLevel.INFO]:
|
|
21
|
-
[ProboLogLevel.LOG]:
|
|
22
|
-
[ProboLogLevel.WARN]:
|
|
23
|
-
[ProboLogLevel.ERROR]:
|
|
22
|
+
// Map ProboLogLevel to Winston levels
|
|
23
|
+
const levelMap = {
|
|
24
|
+
[ProboLogLevel.DEBUG]: 'debug',
|
|
25
|
+
[ProboLogLevel.INFO]: 'info',
|
|
26
|
+
[ProboLogLevel.LOG]: 'info',
|
|
27
|
+
[ProboLogLevel.WARN]: 'warn',
|
|
28
|
+
[ProboLogLevel.ERROR]: 'error',
|
|
24
29
|
};
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
30
|
+
// Create a single Winston logger instance without transports initially
|
|
31
|
+
const logger = winston.createLogger({
|
|
32
|
+
level: levelMap[ProboLogLevel.INFO],
|
|
33
|
+
format: winston.format.combine(winston.format.timestamp(), winston.format.printf(({ timestamp, level, message }) => `[${timestamp}] ${level.toUpperCase()}: ${message}`)),
|
|
34
|
+
transports: []
|
|
35
|
+
});
|
|
36
|
+
/**
|
|
37
|
+
* Configure logging transports and level.
|
|
38
|
+
* @param options.logToConsole - Enable or disable console logging (default: true)
|
|
39
|
+
* @param options.logToFile - Enable or disable file logging (default: true)
|
|
40
|
+
* @param options.level - Logging level (default: INFO)
|
|
41
|
+
* @param options.filePath - Path for file transport (default: '/tmp/probo.log')
|
|
42
|
+
*/
|
|
43
|
+
function configureLogger(options) {
|
|
44
|
+
const { logToConsole = true, logToFile = true, level = ProboLogLevel.INFO, filePath = '/tmp/probo.log', } = options || {};
|
|
45
|
+
// Set global level
|
|
46
|
+
logger.level = levelMap[level];
|
|
47
|
+
// Console transport
|
|
48
|
+
const consoleTransport = logger.transports.find(t => t instanceof winston.transports.Console);
|
|
49
|
+
if (logToConsole) {
|
|
50
|
+
if (!consoleTransport) {
|
|
51
|
+
logger.add(new winston.transports.Console());
|
|
52
|
+
}
|
|
36
53
|
}
|
|
37
|
-
|
|
38
|
-
|
|
54
|
+
else if (consoleTransport) {
|
|
55
|
+
logger.remove(consoleTransport);
|
|
39
56
|
}
|
|
40
|
-
|
|
41
|
-
|
|
57
|
+
// File transport
|
|
58
|
+
const fileTransport = logger.transports.find(t => t instanceof winston.transports.File);
|
|
59
|
+
if (logToFile) {
|
|
60
|
+
if (!fileTransport) {
|
|
61
|
+
logger.add(new winston.transports.File({ filename: filePath }));
|
|
62
|
+
}
|
|
42
63
|
}
|
|
43
|
-
|
|
44
|
-
|
|
64
|
+
else if (fileTransport) {
|
|
65
|
+
logger.remove(fileTransport);
|
|
45
66
|
}
|
|
46
|
-
|
|
47
|
-
|
|
67
|
+
}
|
|
68
|
+
// Apply defaults: both console and file, INFO level
|
|
69
|
+
configureLogger();
|
|
70
|
+
/**
|
|
71
|
+
* Simple logger wrapper that prefixes messages.
|
|
72
|
+
*/
|
|
73
|
+
class ProboLogger {
|
|
74
|
+
constructor(prefix) {
|
|
75
|
+
this.prefix = prefix;
|
|
48
76
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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.padEnd(5, ' ')} [${this.prefix}]`, ...args);
|
|
68
|
-
}
|
|
77
|
+
_logInternal(level, ...args) {
|
|
78
|
+
const msg = args
|
|
79
|
+
.map(a => (typeof a === 'object' ? JSON.stringify(a) : a))
|
|
80
|
+
.join(' ');
|
|
81
|
+
logger.log({ level: levelMap[level], message: `[${this.prefix}] ${msg}` });
|
|
69
82
|
}
|
|
83
|
+
debug(...args) { this._logInternal(ProboLogLevel.DEBUG, ...args); }
|
|
84
|
+
info(...args) { this._logInternal(ProboLogLevel.INFO, ...args); }
|
|
85
|
+
log(...args) { this._logInternal(ProboLogLevel.LOG, ...args); }
|
|
86
|
+
warn(...args) { this._logInternal(ProboLogLevel.WARN, ...args); }
|
|
87
|
+
error(...args) { this._logInternal(ProboLogLevel.ERROR, ...args); }
|
|
70
88
|
}
|
|
71
|
-
|
|
89
|
+
// Default logger instance
|
|
90
|
+
const proboLogger$1 = new ProboLogger('probolib');
|
|
91
|
+
// Element cleaner logging
|
|
72
92
|
const elementLogger = new ProboLogger('element-cleaner');
|
|
93
|
+
/**
|
|
94
|
+
* Cleans and returns a minimal element info structure.
|
|
95
|
+
*/
|
|
73
96
|
function cleanupElementInfo(elementInfo) {
|
|
74
97
|
var _a;
|
|
75
|
-
elementLogger.debug(`Cleaning up element info for ${elementInfo.tag}
|
|
98
|
+
elementLogger.debug(`Cleaning up element info for ${elementInfo.tag} at index ${elementInfo.index}`);
|
|
76
99
|
const depth = (_a = elementInfo.depth) !== null && _a !== void 0 ? _a : elementInfo.getDepth();
|
|
77
|
-
const
|
|
100
|
+
const cleanEl = {
|
|
78
101
|
index: elementInfo.index,
|
|
79
102
|
tag: elementInfo.tag,
|
|
80
103
|
type: elementInfo.type,
|
|
@@ -84,11 +107,14 @@ function cleanupElementInfo(elementInfo) {
|
|
|
84
107
|
css_selector: elementInfo.css_selector,
|
|
85
108
|
iframe_selector: elementInfo.iframe_selector,
|
|
86
109
|
bounding_box: elementInfo.bounding_box,
|
|
87
|
-
depth
|
|
110
|
+
depth
|
|
88
111
|
};
|
|
89
|
-
elementLogger.debug(`Cleaned element
|
|
90
|
-
return
|
|
112
|
+
elementLogger.debug(`Cleaned element: ${JSON.stringify(cleanEl)}`);
|
|
113
|
+
return cleanEl;
|
|
91
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* Cleans highlighted elements in an instruction payload.
|
|
117
|
+
*/
|
|
92
118
|
function cleanupInstructionElements(instruction) {
|
|
93
119
|
var _a;
|
|
94
120
|
if (!((_a = instruction === null || instruction === void 0 ? void 0 : instruction.result) === null || _a === void 0 ? void 0 : _a.highlighted_elements)) {
|
|
@@ -96,15 +122,15 @@ function cleanupInstructionElements(instruction) {
|
|
|
96
122
|
return instruction;
|
|
97
123
|
}
|
|
98
124
|
elementLogger.debug(`Cleaning ${instruction.result.highlighted_elements.length} highlighted elements`);
|
|
99
|
-
const
|
|
125
|
+
const cleaned = {
|
|
100
126
|
...instruction,
|
|
101
127
|
result: {
|
|
102
128
|
...instruction.result,
|
|
103
|
-
highlighted_elements: instruction.result.highlighted_elements.map((
|
|
129
|
+
highlighted_elements: instruction.result.highlighted_elements.map((el) => cleanupElementInfo(el))
|
|
104
130
|
}
|
|
105
131
|
};
|
|
106
132
|
elementLogger.debug('Instruction cleaning completed');
|
|
107
|
-
return
|
|
133
|
+
return cleaned;
|
|
108
134
|
}
|
|
109
135
|
|
|
110
136
|
// Action constants
|
|
@@ -148,38 +174,38 @@ async function handlePotentialNavigation(page, locator = null, value = null, opt
|
|
|
148
174
|
if (frame === page.mainFrame()) {
|
|
149
175
|
navigationCount++;
|
|
150
176
|
lastNavTime = Date.now();
|
|
151
|
-
proboLogger.debug(`DEBUG_NAV[${((Date.now() - startTime) / 1000).toFixed(3)}s]: navigation detected (count=${navigationCount})`);
|
|
177
|
+
proboLogger$1.debug(`DEBUG_NAV[${((Date.now() - startTime) / 1000).toFixed(3)}s]: navigation detected (count=${navigationCount})`);
|
|
152
178
|
}
|
|
153
179
|
};
|
|
154
|
-
proboLogger.debug(`DEBUG_NAV[0.000s]: Starting navigation detection`);
|
|
180
|
+
proboLogger$1.debug(`DEBUG_NAV[0.000s]: Starting navigation detection`);
|
|
155
181
|
page.on("framenavigated", onFrameNav);
|
|
156
182
|
try {
|
|
157
183
|
if (locator) {
|
|
158
|
-
proboLogger.debug(`DEBUG_NAV: Executing click(${value})`);
|
|
184
|
+
proboLogger$1.debug(`DEBUG_NAV: Executing click(${value})`);
|
|
159
185
|
await clickAtPosition(page, locator, value);
|
|
160
186
|
}
|
|
161
187
|
// wait for any initial nav to fire
|
|
162
188
|
await page.waitForTimeout(initialTimeout);
|
|
163
|
-
proboLogger.debug(`DEBUG_NAV[${((Date.now() - startTime) / 1000).toFixed(3)}s]: After initial wait, count=${navigationCount}`);
|
|
189
|
+
proboLogger$1.debug(`DEBUG_NAV[${((Date.now() - startTime) / 1000).toFixed(3)}s]: After initial wait, count=${navigationCount}`);
|
|
164
190
|
if (navigationCount > 0) {
|
|
165
191
|
// loop until either per-nav or global timeout
|
|
166
192
|
while (true) {
|
|
167
193
|
const now = Date.now();
|
|
168
194
|
if (lastNavTime !== null &&
|
|
169
195
|
now - lastNavTime > navigationTimeout) {
|
|
170
|
-
proboLogger.debug(`DEBUG_NAV[${((now - startTime) / 1000).toFixed(3)}s]: per‐navigation timeout reached`);
|
|
196
|
+
proboLogger$1.debug(`DEBUG_NAV[${((now - startTime) / 1000).toFixed(3)}s]: per‐navigation timeout reached`);
|
|
171
197
|
break;
|
|
172
198
|
}
|
|
173
199
|
if (now - startTime > globalTimeout) {
|
|
174
|
-
proboLogger.debug(`DEBUG_NAV[${((now - startTime) / 1000).toFixed(3)}s]: overall timeout reached`);
|
|
200
|
+
proboLogger$1.debug(`DEBUG_NAV[${((now - startTime) / 1000).toFixed(3)}s]: overall timeout reached`);
|
|
175
201
|
break;
|
|
176
202
|
}
|
|
177
203
|
await page.waitForTimeout(500);
|
|
178
204
|
}
|
|
179
205
|
// now wait for load + idle
|
|
180
|
-
proboLogger.debug(`DEBUG_NAV: waiting for load state`);
|
|
206
|
+
proboLogger$1.debug(`DEBUG_NAV: waiting for load state`);
|
|
181
207
|
await page.waitForLoadState("load", { timeout: globalTimeout });
|
|
182
|
-
proboLogger.debug(`DEBUG_NAV: waiting for networkidle`);
|
|
208
|
+
proboLogger$1.debug(`DEBUG_NAV: waiting for networkidle`);
|
|
183
209
|
try {
|
|
184
210
|
// shorter idle‐wait so we don't hang the full globalTimeout here
|
|
185
211
|
await page.waitForLoadState("networkidle", {
|
|
@@ -187,18 +213,18 @@ async function handlePotentialNavigation(page, locator = null, value = null, opt
|
|
|
187
213
|
});
|
|
188
214
|
}
|
|
189
215
|
catch (_a) {
|
|
190
|
-
proboLogger.debug(`DEBUG_NAV: networkidle not reached in ${navigationTimeout}ms, proceeding anyway`);
|
|
216
|
+
proboLogger$1.debug(`DEBUG_NAV: networkidle not reached in ${navigationTimeout}ms, proceeding anyway`);
|
|
191
217
|
}
|
|
192
218
|
await scrollToBottomRight(page);
|
|
193
|
-
proboLogger.debug(`DEBUG_NAV: done`);
|
|
219
|
+
proboLogger$1.debug(`DEBUG_NAV: done`);
|
|
194
220
|
return true;
|
|
195
221
|
}
|
|
196
|
-
proboLogger.debug(`DEBUG_NAV: no navigation detected`);
|
|
222
|
+
proboLogger$1.debug(`DEBUG_NAV: no navigation detected`);
|
|
197
223
|
return false;
|
|
198
224
|
}
|
|
199
225
|
finally {
|
|
200
226
|
page.removeListener("framenavigated", onFrameNav);
|
|
201
|
-
proboLogger.debug(`DEBUG_NAV: listener removed`);
|
|
227
|
+
proboLogger$1.debug(`DEBUG_NAV: listener removed`);
|
|
202
228
|
}
|
|
203
229
|
}
|
|
204
230
|
/**
|
|
@@ -206,7 +232,7 @@ async function handlePotentialNavigation(page, locator = null, value = null, opt
|
|
|
206
232
|
*/
|
|
207
233
|
async function scrollToBottomRight(page) {
|
|
208
234
|
const startTime = performance.now();
|
|
209
|
-
proboLogger.debug(`Starting scroll to bottom-right`);
|
|
235
|
+
proboLogger$1.debug(`Starting scroll to bottom-right`);
|
|
210
236
|
let lastHeight = await page.evaluate(() => document.documentElement.scrollHeight);
|
|
211
237
|
let lastWidth = await page.evaluate(() => document.documentElement.scrollWidth);
|
|
212
238
|
let smoothingSteps = 0;
|
|
@@ -228,12 +254,12 @@ async function scrollToBottomRight(page) {
|
|
|
228
254
|
}
|
|
229
255
|
currX += clientWidth;
|
|
230
256
|
}
|
|
231
|
-
proboLogger.debug(`performed ${smoothingSteps} smoothing steps while scrolling`);
|
|
257
|
+
proboLogger$1.debug(`performed ${smoothingSteps} smoothing steps while scrolling`);
|
|
232
258
|
const newHeight = await page.evaluate(() => document.documentElement.scrollHeight);
|
|
233
259
|
const newWidth = await page.evaluate(() => document.documentElement.scrollWidth);
|
|
234
260
|
if (newHeight === lastHeight && newWidth === lastWidth)
|
|
235
261
|
break;
|
|
236
|
-
proboLogger.debug(`page dimensions updated, repeating scroll`);
|
|
262
|
+
proboLogger$1.debug(`page dimensions updated, repeating scroll`);
|
|
237
263
|
lastHeight = newHeight;
|
|
238
264
|
lastWidth = newWidth;
|
|
239
265
|
}
|
|
@@ -242,14 +268,14 @@ async function scrollToBottomRight(page) {
|
|
|
242
268
|
await page.evaluate('window.scrollTo(0, 0)');
|
|
243
269
|
await page.waitForTimeout(50);
|
|
244
270
|
}
|
|
245
|
-
proboLogger.debug(`Scroll completed in ${(performance.now() - startTime).toFixed(3)}ms`);
|
|
271
|
+
proboLogger$1.debug(`Scroll completed in ${(performance.now() - startTime).toFixed(3)}ms`);
|
|
246
272
|
}
|
|
247
273
|
/**
|
|
248
274
|
* Wait for DOM mutations to settle using MutationObserver logic
|
|
249
275
|
*/
|
|
250
276
|
async function waitForMutationsToSettle(page, mutationTimeout = 1500, initTimeout = 2000) {
|
|
251
277
|
const startTime = Date.now();
|
|
252
|
-
proboLogger.debug(`Starting mutation settlement (initTimeout=${initTimeout}, mutationTimeout=${mutationTimeout})`);
|
|
278
|
+
proboLogger$1.debug(`Starting mutation settlement (initTimeout=${initTimeout}, mutationTimeout=${mutationTimeout})`);
|
|
253
279
|
const result = await page.evaluate(async ({ mutationTimeout, initTimeout }) => {
|
|
254
280
|
async function blockUntilStable(targetNode, options = { childList: true, subtree: true }) {
|
|
255
281
|
return new Promise((resolve) => {
|
|
@@ -280,7 +306,7 @@ async function waitForMutationsToSettle(page, mutationTimeout = 1500, initTimeou
|
|
|
280
306
|
return blockUntilStable(document.body);
|
|
281
307
|
}, { mutationTimeout, initTimeout });
|
|
282
308
|
const total = ((Date.now() - startTime) / 1000).toFixed(2);
|
|
283
|
-
proboLogger.debug(result
|
|
309
|
+
proboLogger$1.debug(result
|
|
284
310
|
? `Mutations settled. Took ${total}s`
|
|
285
311
|
: `No mutations observed. Took ${total}s`);
|
|
286
312
|
return result;
|
|
@@ -294,7 +320,7 @@ async function getElementValue(page, locator) {
|
|
|
294
320
|
if (!allText) {
|
|
295
321
|
allText = await locator.evaluate((el) => el.innerText);
|
|
296
322
|
}
|
|
297
|
-
proboLogger.debug(`getElementValue: [${allText}]`);
|
|
323
|
+
proboLogger$1.debug(`getElementValue: [${allText}]`);
|
|
298
324
|
return allText;
|
|
299
325
|
}
|
|
300
326
|
/**
|
|
@@ -304,16 +330,16 @@ async function selectDropdownOption(page, locator, value) {
|
|
|
304
330
|
const tagName = await (locator === null || locator === void 0 ? void 0 : locator.evaluate((el) => el.tagName.toLowerCase()));
|
|
305
331
|
const role = await (locator === null || locator === void 0 ? void 0 : locator.getAttribute('role'));
|
|
306
332
|
if (tagName === 'option' || role === 'option') {
|
|
307
|
-
proboLogger.debug('selectDropdownOption: option role detected');
|
|
333
|
+
proboLogger$1.debug('selectDropdownOption: option role detected');
|
|
308
334
|
await (locator === null || locator === void 0 ? void 0 : locator.click());
|
|
309
335
|
}
|
|
310
336
|
else if (tagName === 'select') {
|
|
311
|
-
proboLogger.debug('selectDropdownOption: simple select tag detected');
|
|
337
|
+
proboLogger$1.debug('selectDropdownOption: simple select tag detected');
|
|
312
338
|
try {
|
|
313
339
|
await (locator === null || locator === void 0 ? void 0 : locator.selectOption(value));
|
|
314
340
|
}
|
|
315
341
|
catch (_a) {
|
|
316
|
-
proboLogger.debug('selectDropdownOption: manual change event fallback');
|
|
342
|
+
proboLogger$1.debug('selectDropdownOption: manual change event fallback');
|
|
317
343
|
const handle = locator ? await locator.elementHandle() : null;
|
|
318
344
|
await page.evaluate(({ h, val }) => {
|
|
319
345
|
const el = h;
|
|
@@ -325,13 +351,13 @@ async function selectDropdownOption(page, locator, value) {
|
|
|
325
351
|
}
|
|
326
352
|
}
|
|
327
353
|
else {
|
|
328
|
-
proboLogger.debug('selectDropdownOption: custom dropdown path');
|
|
354
|
+
proboLogger$1.debug('selectDropdownOption: custom dropdown path');
|
|
329
355
|
let listbox = locator.locator('select');
|
|
330
356
|
let count = await listbox.count();
|
|
331
357
|
if (count > 1)
|
|
332
358
|
throw new Error(`selectDropdownOption: ambiguous <select> count=${count}`);
|
|
333
359
|
if (count === 1) {
|
|
334
|
-
proboLogger.debug('selectDropdownOption: child <select> found');
|
|
360
|
+
proboLogger$1.debug('selectDropdownOption: child <select> found');
|
|
335
361
|
await listbox.selectOption(value);
|
|
336
362
|
return;
|
|
337
363
|
}
|
|
@@ -344,7 +370,7 @@ async function selectDropdownOption(page, locator, value) {
|
|
|
344
370
|
count = await listbox.count();
|
|
345
371
|
if (count >= 1)
|
|
346
372
|
break;
|
|
347
|
-
proboLogger.debug(`selectDropdownOption: iteration #${i} no listbox found`);
|
|
373
|
+
proboLogger$1.debug(`selectDropdownOption: iteration #${i} no listbox found`);
|
|
348
374
|
container = container.locator('xpath=..');
|
|
349
375
|
}
|
|
350
376
|
}
|
|
@@ -368,7 +394,7 @@ async function selectDropdownOption(page, locator, value) {
|
|
|
368
394
|
* Execute a given Playwright action, mirroring Python's _perform_action
|
|
369
395
|
*/
|
|
370
396
|
async function executePlaywrightAction(page, action, value, iframe_selector, element_css_selector) {
|
|
371
|
-
proboLogger.info(`performing Action: ${action} Value: ${value}`);
|
|
397
|
+
proboLogger$1.info(`performing Action: ${action} Value: ${value}`);
|
|
372
398
|
try {
|
|
373
399
|
if (action === PlaywrightAction.VISIT_BASE_URL || action === PlaywrightAction.VISIT_URL) {
|
|
374
400
|
await page.goto(value, { waitUntil: 'load' });
|
|
@@ -444,29 +470,29 @@ async function executePlaywrightAction(page, action, value, iframe_selector, ele
|
|
|
444
470
|
break;
|
|
445
471
|
case PlaywrightAction.VALIDATE_EXACT_VALUE:
|
|
446
472
|
const actualExact = await getElementValue(page, locator);
|
|
447
|
-
proboLogger.debug(`actual value is [${actualExact}]`);
|
|
473
|
+
proboLogger$1.debug(`actual value is [${actualExact}]`);
|
|
448
474
|
if (actualExact !== value) {
|
|
449
|
-
proboLogger.info(`Validation *FAIL* expected '${value}' but got '${actualExact}'`);
|
|
475
|
+
proboLogger$1.info(`Validation *FAIL* expected '${value}' but got '${actualExact}'`);
|
|
450
476
|
return false;
|
|
451
477
|
}
|
|
452
|
-
proboLogger.info('Validation *PASS*');
|
|
478
|
+
proboLogger$1.info('Validation *PASS*');
|
|
453
479
|
break;
|
|
454
480
|
case PlaywrightAction.VALIDATE_CONTAINS_VALUE:
|
|
455
481
|
const actualContains = await getElementValue(page, locator);
|
|
456
|
-
proboLogger.debug(`actual value is [${actualContains}]`);
|
|
482
|
+
proboLogger$1.debug(`actual value is [${actualContains}]`);
|
|
457
483
|
if (!actualContains.includes(value)) {
|
|
458
|
-
proboLogger.info(`Validation *FAIL* expected '${value}' to be contained in '${actualContains}'`);
|
|
484
|
+
proboLogger$1.info(`Validation *FAIL* expected '${value}' to be contained in '${actualContains}'`);
|
|
459
485
|
return false;
|
|
460
486
|
}
|
|
461
|
-
proboLogger.info('Validation *PASS*');
|
|
487
|
+
proboLogger$1.info('Validation *PASS*');
|
|
462
488
|
break;
|
|
463
489
|
case PlaywrightAction.VALIDATE_URL:
|
|
464
490
|
const currUrl = page.url();
|
|
465
491
|
if (currUrl !== value) {
|
|
466
|
-
proboLogger.info(`Validation *FAIL* expected url '${value}' while is '${currUrl}'`);
|
|
492
|
+
proboLogger$1.info(`Validation *FAIL* expected url '${value}' while is '${currUrl}'`);
|
|
467
493
|
return false;
|
|
468
494
|
}
|
|
469
|
-
proboLogger.info('Validation *PASS*');
|
|
495
|
+
proboLogger$1.info('Validation *PASS*');
|
|
470
496
|
break;
|
|
471
497
|
default:
|
|
472
498
|
throw new Error(`Unknown action: ${action}`);
|
|
@@ -475,7 +501,7 @@ async function executePlaywrightAction(page, action, value, iframe_selector, ele
|
|
|
475
501
|
return true;
|
|
476
502
|
}
|
|
477
503
|
catch (e) {
|
|
478
|
-
proboLogger.debug(`***ERROR failed to execute action ${action}: ${e}`);
|
|
504
|
+
proboLogger$1.debug(`***ERROR failed to execute action ${action}: ${e}`);
|
|
479
505
|
throw e;
|
|
480
506
|
}
|
|
481
507
|
}
|
|
@@ -483,7 +509,7 @@ async function executePlaywrightAction(page, action, value, iframe_selector, ele
|
|
|
483
509
|
* Execute a given Playwright action using native Playwright functions where possible
|
|
484
510
|
*/
|
|
485
511
|
async function executeCachedPlaywrightAction(page, action, value, iframe_selector, element_css_selector) {
|
|
486
|
-
proboLogger.log(`performing Cached Action: ${action} Value: ${value} on iframe: ${iframe_selector} locator: ${element_css_selector}`);
|
|
512
|
+
proboLogger$1.log(`performing Cached Action: ${action} Value: ${value} on iframe: ${iframe_selector} locator: ${element_css_selector}`);
|
|
487
513
|
try {
|
|
488
514
|
let locator = undefined;
|
|
489
515
|
if (iframe_selector)
|
|
@@ -559,29 +585,29 @@ async function executeCachedPlaywrightAction(page, action, value, iframe_selecto
|
|
|
559
585
|
break;
|
|
560
586
|
case PlaywrightAction.VALIDATE_EXACT_VALUE:
|
|
561
587
|
const actualExact = await getElementValue(page, locator);
|
|
562
|
-
proboLogger.debug(`actual value is [${actualExact}]`);
|
|
588
|
+
proboLogger$1.debug(`actual value is [${actualExact}]`);
|
|
563
589
|
if (actualExact !== value) {
|
|
564
|
-
proboLogger.info(`Validation *FAIL* expected '${value}' but got '${actualExact}'`);
|
|
590
|
+
proboLogger$1.info(`Validation *FAIL* expected '${value}' but got '${actualExact}'`);
|
|
565
591
|
return false;
|
|
566
592
|
}
|
|
567
|
-
proboLogger.info('Validation *PASS*');
|
|
593
|
+
proboLogger$1.info('Validation *PASS*');
|
|
568
594
|
break;
|
|
569
595
|
case PlaywrightAction.VALIDATE_CONTAINS_VALUE:
|
|
570
596
|
const actualContains = await getElementValue(page, locator);
|
|
571
|
-
proboLogger.debug(`actual value is [${actualContains}]`);
|
|
597
|
+
proboLogger$1.debug(`actual value is [${actualContains}]`);
|
|
572
598
|
if (!actualContains.includes(value)) {
|
|
573
|
-
proboLogger.info(`Validation *FAIL* expected '${value}' to be contained in '${actualContains}'`);
|
|
599
|
+
proboLogger$1.info(`Validation *FAIL* expected '${value}' to be contained in '${actualContains}'`);
|
|
574
600
|
return false;
|
|
575
601
|
}
|
|
576
|
-
proboLogger.info('Validation *PASS*');
|
|
602
|
+
proboLogger$1.info('Validation *PASS*');
|
|
577
603
|
break;
|
|
578
604
|
case PlaywrightAction.VALIDATE_URL:
|
|
579
605
|
const currUrl = page.url();
|
|
580
606
|
if (currUrl !== value) {
|
|
581
|
-
proboLogger.info(`Validation *FAIL* expected url '${value}' while is '${currUrl}'`);
|
|
607
|
+
proboLogger$1.info(`Validation *FAIL* expected url '${value}' while is '${currUrl}'`);
|
|
582
608
|
return false;
|
|
583
609
|
}
|
|
584
|
-
proboLogger.info('Validation *PASS*');
|
|
610
|
+
proboLogger$1.info('Validation *PASS*');
|
|
585
611
|
break;
|
|
586
612
|
default:
|
|
587
613
|
throw new Error(`Unknown action: ${action}`);
|
|
@@ -589,7 +615,7 @@ async function executeCachedPlaywrightAction(page, action, value, iframe_selecto
|
|
|
589
615
|
return true;
|
|
590
616
|
}
|
|
591
617
|
catch (e) {
|
|
592
|
-
proboLogger.debug(`***ERROR failed to execute cached action ${action}: ${e}`);
|
|
618
|
+
proboLogger$1.debug(`***ERROR failed to execute cached action ${action}: ${e}`);
|
|
593
619
|
throw e;
|
|
594
620
|
}
|
|
595
621
|
}
|
|
@@ -627,14 +653,14 @@ class Highlighter {
|
|
|
627
653
|
try {
|
|
628
654
|
const scriptExists = await page.evaluate(`typeof window.ProboLabs?.highlight?.execute === 'function'`);
|
|
629
655
|
if (!scriptExists) {
|
|
630
|
-
proboLogger.debug('Injecting highlighter script...');
|
|
656
|
+
proboLogger$1.debug('Injecting highlighter script...');
|
|
631
657
|
await page.evaluate(highlighterCode);
|
|
632
658
|
// Verify the script was injected correctly
|
|
633
|
-
const verified = await page.evaluate(`
|
|
634
|
-
//console.log('ProboLabs global:', window.ProboLabs);
|
|
635
|
-
typeof window.ProboLabs?.highlight?.execute === 'function'
|
|
659
|
+
const verified = await page.evaluate(`
|
|
660
|
+
//console.log('ProboLabs global:', window.ProboLabs);
|
|
661
|
+
typeof window.ProboLabs?.highlight?.execute === 'function'
|
|
636
662
|
`);
|
|
637
|
-
proboLogger.debug('Script injection verified:', verified);
|
|
663
|
+
proboLogger$1.debug('Script injection verified:', verified);
|
|
638
664
|
}
|
|
639
665
|
return; // Success - exit the function
|
|
640
666
|
}
|
|
@@ -642,13 +668,13 @@ class Highlighter {
|
|
|
642
668
|
if (attempt === maxRetries - 1) {
|
|
643
669
|
throw error;
|
|
644
670
|
}
|
|
645
|
-
proboLogger.debug(`Script injection attempt ${attempt + 1} failed, retrying after delay...`);
|
|
671
|
+
proboLogger$1.debug(`Script injection attempt ${attempt + 1} failed, retrying after delay...`);
|
|
646
672
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
647
673
|
}
|
|
648
674
|
}
|
|
649
675
|
}
|
|
650
676
|
async highlightElements(page, elementTags) {
|
|
651
|
-
proboLogger.debug('highlightElements called with:', elementTags);
|
|
677
|
+
proboLogger$1.debug('highlightElements called with:', elementTags);
|
|
652
678
|
await this.ensureHighlighterScript(page);
|
|
653
679
|
// Execute the highlight function and await its result
|
|
654
680
|
const result = await page.evaluate(async (tags) => {
|
|
@@ -669,7 +695,7 @@ class Highlighter {
|
|
|
669
695
|
return result;
|
|
670
696
|
}
|
|
671
697
|
async unhighlightElements(page) {
|
|
672
|
-
proboLogger.debug('unhighlightElements called');
|
|
698
|
+
proboLogger$1.debug('unhighlightElements called');
|
|
673
699
|
await this.ensureHighlighterScript(page);
|
|
674
700
|
await page.evaluate(() => {
|
|
675
701
|
var _a, _b;
|
|
@@ -678,11 +704,11 @@ class Highlighter {
|
|
|
678
704
|
}
|
|
679
705
|
async highlightElement(page, element_css_selector, iframe_selector, element_index) {
|
|
680
706
|
await this.ensureHighlighterScript(page);
|
|
681
|
-
proboLogger.debug('Highlighting element with:', { element_css_selector, iframe_selector, element_index });
|
|
707
|
+
proboLogger$1.debug('Highlighting element with:', { element_css_selector, iframe_selector, element_index });
|
|
682
708
|
await page.evaluate(({ css_selector, iframe_selector, index }) => {
|
|
683
709
|
const proboLabs = window.ProboLabs;
|
|
684
710
|
if (!proboLabs) {
|
|
685
|
-
proboLogger.warn('ProboLabs not initialized');
|
|
711
|
+
proboLogger$1.warn('ProboLabs not initialized');
|
|
686
712
|
return;
|
|
687
713
|
}
|
|
688
714
|
// Create ElementInfo object for the element
|
|
@@ -1184,7 +1210,7 @@ class ApiClient {
|
|
|
1184
1210
|
return headers;
|
|
1185
1211
|
}
|
|
1186
1212
|
async createStep(options) {
|
|
1187
|
-
proboLogger.debug('creating step ', options.stepPrompt);
|
|
1213
|
+
proboLogger$1.debug('creating step ', options.stepPrompt);
|
|
1188
1214
|
return pRetry(async () => {
|
|
1189
1215
|
const response = await fetch(`${this.apiUrl}/step-runners/`, {
|
|
1190
1216
|
method: 'POST',
|
|
@@ -1204,14 +1230,14 @@ class ApiClient {
|
|
|
1204
1230
|
retries: this.maxRetries,
|
|
1205
1231
|
minTimeout: this.initialBackoff,
|
|
1206
1232
|
onFailedAttempt: error => {
|
|
1207
|
-
proboLogger.warn(`API call failed, attempt ${error.attemptNumber} of ${error.retriesLeft + error.attemptNumber}...`);
|
|
1233
|
+
proboLogger$1.warn(`API call failed, attempt ${error.attemptNumber} of ${error.retriesLeft + error.attemptNumber}...`);
|
|
1208
1234
|
}
|
|
1209
1235
|
});
|
|
1210
1236
|
}
|
|
1211
1237
|
async resolveNextInstruction(stepId, instruction, aiModel) {
|
|
1212
|
-
proboLogger.debug(`resolving next instruction: ${instruction}`);
|
|
1238
|
+
proboLogger$1.debug(`resolving next instruction: ${instruction}`);
|
|
1213
1239
|
return pRetry(async () => {
|
|
1214
|
-
proboLogger.debug(`API client: Resolving next instruction for step ${stepId}`);
|
|
1240
|
+
proboLogger$1.debug(`API client: Resolving next instruction for step ${stepId}`);
|
|
1215
1241
|
const cleanInstruction = cleanupInstructionElements(instruction);
|
|
1216
1242
|
const response = await fetch(`${this.apiUrl}/step-runners/${stepId}/run/`, {
|
|
1217
1243
|
method: 'POST',
|
|
@@ -1227,7 +1253,7 @@ class ApiClient {
|
|
|
1227
1253
|
retries: this.maxRetries,
|
|
1228
1254
|
minTimeout: this.initialBackoff,
|
|
1229
1255
|
onFailedAttempt: error => {
|
|
1230
|
-
proboLogger.warn(`API call failed, attempt ${error.attemptNumber} of ${error.retriesLeft + error.attemptNumber}...`);
|
|
1256
|
+
proboLogger$1.warn(`API call failed, attempt ${error.attemptNumber} of ${error.retriesLeft + error.attemptNumber}...`);
|
|
1231
1257
|
}
|
|
1232
1258
|
});
|
|
1233
1259
|
}
|
|
@@ -1246,12 +1272,12 @@ class ApiClient {
|
|
|
1246
1272
|
retries: this.maxRetries,
|
|
1247
1273
|
minTimeout: this.initialBackoff,
|
|
1248
1274
|
onFailedAttempt: error => {
|
|
1249
|
-
proboLogger.warn(`API call failed, attempt ${error.attemptNumber} of ${error.retriesLeft + error.attemptNumber}...`);
|
|
1275
|
+
proboLogger$1.warn(`API call failed, attempt ${error.attemptNumber} of ${error.retriesLeft + error.attemptNumber}...`);
|
|
1250
1276
|
}
|
|
1251
1277
|
});
|
|
1252
1278
|
}
|
|
1253
1279
|
async findStepByPrompt(prompt, scenarioName) {
|
|
1254
|
-
proboLogger.debug(`Finding step by prompt: ${prompt} and scenario: ${scenarioName}`);
|
|
1280
|
+
proboLogger$1.debug(`Finding step by prompt: ${prompt} and scenario: ${scenarioName}`);
|
|
1255
1281
|
return pRetry(async () => {
|
|
1256
1282
|
const response = await fetch(`${this.apiUrl}/step-runners/find-step-by-prompt/`, {
|
|
1257
1283
|
method: 'POST',
|
|
@@ -1280,7 +1306,7 @@ class ApiClient {
|
|
|
1280
1306
|
retries: this.maxRetries,
|
|
1281
1307
|
minTimeout: this.initialBackoff,
|
|
1282
1308
|
onFailedAttempt: error => {
|
|
1283
|
-
proboLogger.warn(`API call failed, attempt ${error.attemptNumber} of ${error.retriesLeft + error.attemptNumber}...`);
|
|
1309
|
+
proboLogger$1.warn(`API call failed, attempt ${error.attemptNumber} of ${error.retriesLeft + error.attemptNumber}...`);
|
|
1284
1310
|
}
|
|
1285
1311
|
});
|
|
1286
1312
|
}
|
|
@@ -1296,6 +1322,7 @@ class ApiClient {
|
|
|
1296
1322
|
}
|
|
1297
1323
|
}
|
|
1298
1324
|
|
|
1325
|
+
const proboLogger = new ProboLogger('probo-playwright');
|
|
1299
1326
|
/**
|
|
1300
1327
|
* Available AI models for LLM operations
|
|
1301
1328
|
*/
|
|
@@ -1317,28 +1344,31 @@ const retryOptions = {
|
|
|
1317
1344
|
retries: 3,
|
|
1318
1345
|
minTimeout: 1000,
|
|
1319
1346
|
onFailedAttempt: (error) => {
|
|
1320
|
-
proboLogger.warn(`Page operation failed, attempt ${error.attemptNumber} of ${error.retriesLeft +
|
|
1347
|
+
proboLogger.warn(`Page operation failed, attempt ${error.attemptNumber} of ${error.retriesLeft +
|
|
1348
|
+
error.attemptNumber}...`);
|
|
1321
1349
|
}
|
|
1322
1350
|
};
|
|
1323
1351
|
class Probo {
|
|
1324
|
-
constructor({ scenarioName, token = '', apiUrl = '', enableConsoleLogs = false, debugLevel = ProboLogLevel.
|
|
1352
|
+
constructor({ scenarioName, token = '', apiUrl = '', enableConsoleLogs = false, logToConsole = true, logToFile = false, debugLevel = ProboLogLevel.INFO, aiModel = AIModel.DEFAULT_AI_MODEL }) {
|
|
1353
|
+
// Configure logger transports and level
|
|
1354
|
+
configureLogger({ logToConsole, logToFile, level: debugLevel });
|
|
1325
1355
|
const apiKey = token || process.env.PROBO_API_KEY;
|
|
1326
1356
|
if (!apiKey) {
|
|
1327
|
-
proboLogger.error("API key wasn't provided.
|
|
1328
|
-
throw Error('Probo API key not provided');
|
|
1357
|
+
proboLogger.error("API key wasn't provided. Pass 'token' or set PROBO_API_KEY");
|
|
1358
|
+
throw new Error('Probo API key not provided');
|
|
1329
1359
|
}
|
|
1330
1360
|
const apiEndPoint = apiUrl || process.env.PROBO_API_ENDPOINT;
|
|
1331
1361
|
if (!apiEndPoint) {
|
|
1332
|
-
proboLogger.error("API endpoint wasn't provided.
|
|
1333
|
-
throw Error('Probo API endpoint not provided');
|
|
1362
|
+
proboLogger.error("API endpoint wasn't provided. Pass 'apiUrl' or set PROBO_API_ENDPOINT");
|
|
1363
|
+
throw new Error('Probo API endpoint not provided');
|
|
1334
1364
|
}
|
|
1335
1365
|
this.highlighter = new Highlighter(enableConsoleLogs);
|
|
1336
1366
|
this.apiClient = new ApiClient(apiEndPoint, apiKey);
|
|
1337
1367
|
this.enableConsoleLogs = enableConsoleLogs;
|
|
1338
1368
|
this.scenarioName = scenarioName;
|
|
1339
1369
|
this.aiModel = aiModel;
|
|
1340
|
-
proboLogger.
|
|
1341
|
-
|
|
1370
|
+
proboLogger.info(`Initializing: scenario=${scenarioName}, apiUrl=${apiEndPoint}, ` +
|
|
1371
|
+
`enableConsoleLogs=${enableConsoleLogs}, debugLevel=${debugLevel}, aiModel=${aiModel}`);
|
|
1342
1372
|
}
|
|
1343
1373
|
async runStep(page, stepPrompt, options = { useCache: true, stepIdFromServer: undefined, aiModel: this.aiModel }) {
|
|
1344
1374
|
// Use the aiModel from options if provided, otherwise use the one from constructor
|
|
@@ -1450,7 +1480,7 @@ class Probo {
|
|
|
1450
1480
|
return await page.content();
|
|
1451
1481
|
}
|
|
1452
1482
|
catch (error) {
|
|
1453
|
-
console.
|
|
1483
|
+
console.error('Error caught:', {
|
|
1454
1484
|
name: error.name,
|
|
1455
1485
|
message: error.message,
|
|
1456
1486
|
code: error.code,
|
|
@@ -1520,7 +1550,7 @@ class Probo {
|
|
|
1520
1550
|
return await page.content();
|
|
1521
1551
|
}
|
|
1522
1552
|
catch (error) {
|
|
1523
|
-
console.
|
|
1553
|
+
console.error('Error caught:', {
|
|
1524
1554
|
name: error.name,
|
|
1525
1555
|
message: error.message,
|
|
1526
1556
|
code: error.code,
|
|
@@ -1549,5 +1579,5 @@ class Probo {
|
|
|
1549
1579
|
}
|
|
1550
1580
|
}
|
|
1551
1581
|
|
|
1552
|
-
export { AIModel, ElementTag, Probo, ProboLogLevel };
|
|
1582
|
+
export { AIModel, ElementTag, Probo, ProboLogLevel, ProboLogger, configureLogger };
|
|
1553
1583
|
//# sourceMappingURL=index.js.map
|