UNPKG

@labnex/cli

Version:

CLI for Labnex, an AI-Powered Testing Automation Platform

627 lines 35 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ElementFinder = void 0; exports.findElementWithFallbacks = findElementWithFallbacks; exports.captureFocusedDomSnippet = captureFocusedDomSnippet; const extractHintedSelector_1 = require("./parserHelpers/extractHintedSelector"); const client_1 = require("../api/client"); // Fast and efficient element finding with AI assistance async function findElementWithFallbacks(page, currentFrame, addLog, selectorOrText, descriptiveTerm, originalStep = '', disableFallbacks = false, retryApiCallFn, index = 0) { if (!selectorOrText) { addLog('[findElementWithFallbacks] No selector provided'); return null; } const MAX_WAIT_TIME = 20000; // 20 seconds max const startTime = Date.now(); addLog(`[findElementWithFallbacks] Looking for: "${selectorOrText}" (${descriptiveTerm}) at index: ${index}`); // Extract selector hint if present const hintExtraction = (0, extractHintedSelector_1.extractHintedSelector)(selectorOrText); let primarySelector = hintExtraction.selectorValue || selectorOrText; const selectorType = hintExtraction.type || 'auto'; // Minimal cleanup - don't be too aggressive primarySelector = primarySelector.trim(); // --------------------------- // Smart-wait pre-pass: // Wait up to 3 s for any early fallback selector to appear. This helps with // React/Vue hydration where the DOM node is injected shortly after load. // We only attempt this once, and only for the first few (more specific) // strategies to avoid a long upfront delay. const earlyWaitStrategies = generateFallbackStrategies(primarySelector).slice(0, 15); for (const strat of earlyWaitStrategies) { try { const el = await waitForElement(currentFrame, strat.selector, 3000, strat.method); if (el) { const visible = await verifyElementVisibility(el); if (visible) { addLog(`[SmartWait] Found element via early wait (${strat.type})`); return el; } await el.dispose(); } } catch { } } // Try exact selector first without waiting let element = await tryFindElementImmediate(currentFrame, primarySelector, selectorType, addLog, index); if (element) { addLog(`[findElementWithFallbacks] ✓ Found element immediately`); return element; } // Temporarily disable AI assistance to bypass backend 500 error if (!disableFallbacks && retryApiCallFn && client_1.apiClient) { addLog('[findElementWithFallbacks] Element not found immediately. Requesting AI assistance...'); try { // Capture focused DOM context const domSnippet = await captureFocusedDomSnippet(page, currentFrame, primarySelector, addLog); const aiContext = { failedSelector: primarySelector, descriptiveTerm, pageUrl: page.url(), domSnippet, originalStep }; addLog('[findElementWithFallbacks] AI Request Payload:', JSON.stringify(aiContext, null, 2).substring(0, 500) + (JSON.stringify(aiContext).length > 500 ? "... (truncated)" : "")); const aiResponse = await retryApiCallFn(() => client_1.apiClient.getDynamicSelectorSuggestion(aiContext), 3, // maxRetries 1000, // baseDelayMs 'AI selector suggestion' // callDescription ); if (aiResponse.success && aiResponse.data) { const suggestedSelector = aiResponse.data.suggestedSelector; const suggestedStrategy = aiResponse.data.suggestedStrategy; const confidence = aiResponse.data.confidence; const reasoning = aiResponse.data.reasoning; addLog(`[AI] Suggested: ${suggestedSelector} (${suggestedStrategy}, confidence: ${confidence})`); if (reasoning) { addLog(`[AI] Reasoning: ${reasoning}`); } // Try the AI suggested selector with a short wait element = await waitForElement(currentFrame, suggestedSelector, 5000, // Increased wait for AI suggestion suggestedStrategy); if (element) { addLog('[AI] ✓ Successfully found element using AI suggestion'); return element; } // Try alternative selectors if provided const alternatives = aiResponse.data.alternativeSelectors; if (alternatives && Array.isArray(alternatives)) { for (const altSelector of alternatives) { element = await tryFindElementImmediate(currentFrame, altSelector, 'auto', addLog, index); if (element) { addLog(`[AI] ✓ Found element using alternative selector: ${altSelector}`); return element; } } } } else { addLog(`[AI] AI response failed or no suggestion: ${aiResponse.error || 'Unknown error'}`); } } catch (error) { addLog('[findElementWithFallbacks] AI assistance failed:', error.message); } } // Finally, try fallback strategies with short waits if (!disableFallbacks) { const fallbackStrategies = generateFallbackStrategies(primarySelector); for (const strategy of fallbackStrategies) { if (Date.now() - startTime > MAX_WAIT_TIME) { addLog(`[findElementWithFallbacks] Timeout reached after ${MAX_WAIT_TIME}ms`); break; } try { addLog(`[findElementWithFallbacks] Trying ${strategy.type}: "${strategy.selector}"`); const waitMs = strategy.type.includes('text') ? 5000 : 2000; element = await waitForElement(currentFrame, strategy.selector, waitMs, strategy.method); if (element) { const isVisible = await verifyElementVisibility(element); if (isVisible) { addLog(`[findElementWithFallbacks] ✓ Found element using ${strategy.type} strategy`); return element; } else { addLog(`[findElementWithFallbacks] Element found but not visible/interactable`); await element.dispose(); } } } catch (error) { addLog(`[findElementWithFallbacks] ${strategy.type} strategy failed: ${error.message}`); } } } // Dynamic DOM scan for login/sign-in if still not found try { const lowerPrimary = primarySelector.toLowerCase(); if (/(login|log in|sign in)/i.test(primarySelector)) { addLog('[DynamicScan] Performing broad scan for login/sign-in elements.'); const handle = await currentFrame.evaluateHandle(() => { const candidates = Array.from(document.querySelectorAll('a, button, [role="button"], input[type="button"], input[type="submit"]')); return candidates.find(el => { const txt = (el.innerText || el.textContent || '').toLowerCase().trim(); const href = el.getAttribute('href') || ''; const id = el.id || ''; const cls = el.className || ''; if (txt.includes('login') || txt.includes('log in') || txt.includes('sign in')) return el; if (href.toLowerCase().includes('login') || href.toLowerCase().includes('sign')) return el; if (id.toLowerCase().includes('login') || cls.toLowerCase().includes('login')) return el; return false; }) || null; }); const dynamicElem = handle.asElement(); if (dynamicElem) { addLog('[DynamicScan] ✓ Found element via dynamic scan fallback.'); return dynamicElem; } await handle.dispose(); } } catch (e) { addLog(`[DynamicScan] Error during dynamic scan: ${e.message}`); } // Interactive user click capture (non-headless only) try { // Robustly determine if the browser was launched in headless mode – in remote/CT environments // the _process property can be undefined which previously caused a TypeError. We fall back to // browser().process?.spawnargs when available. const spawnArgs = page.browser().process?.spawnargs || page.browser()._process?.spawnargs; const isInteractiveEnabled = Array.isArray(spawnArgs) ? !spawnArgs.includes('--headless') : false; if (isInteractiveEnabled && page.isClosed() === false) { addLog('[InteractiveCapture] No element found. Prompting user to click desired element in the browser.'); await page.evaluate((msg) => { // Inject simple overlay prompt if (document.getElementById('labnex-overlay')) return; const overlay = document.createElement('div'); overlay.id = 'labnex-overlay'; overlay.style.position = 'fixed'; overlay.style.top = '0'; overlay.style.left = '0'; overlay.style.right = '0'; overlay.style.padding = '10px'; overlay.style.background = 'rgba(0,0,0,0.8)'; overlay.style.color = '#fff'; overlay.style.fontSize = '16px'; overlay.style.zIndex = '2147483647'; overlay.style.textAlign = 'center'; overlay.innerText = msg; document.body.appendChild(overlay); }, `Please click the element for \"${descriptiveTerm}\"`); // Wait for click event and capture selector const selectorHandle = await page.evaluateHandle(() => { return new Promise((resolve) => { const handler = (ev) => { ev.preventDefault(); ev.stopPropagation(); const el = ev.target; // build simple selector let sel = ''; if (el.id) sel = `#${el.id}`; else if (el.getAttribute('data-testid')) sel = `[data-testid="${el.getAttribute('data-testid')}"]`; else if (el.className) sel = '.' + Array.from(el.classList).join('.'); else sel = el.tagName.toLowerCase(); document.removeEventListener('click', handler, true); const overlay = document.getElementById('labnex-overlay'); if (overlay) overlay.remove(); resolve(sel); }; document.addEventListener('click', handler, true); }); }); const userSelector = await selectorHandle.jsonValue(); await selectorHandle.dispose(); if (userSelector) { addLog(`[InteractiveCapture] User provided selector: ${userSelector}`); const el = await waitForElement(currentFrame, userSelector, 5000, userSelector.includes('//') ? 'xpath' : 'css'); if (el) return el; } } } catch (e) { addLog(`[InteractiveCapture] Error: ${e.message}`); } // If looking for submit and global flag indicates form submitted, treat as success try { if (/(submit|sign in|log in|login)/i.test(primarySelector)) { const submitted = await page.evaluate(() => !!window.__labnexSubmitted); if (submitted) { addLog('[SubmitSkip] Form submission already detected, skipping missing submit element.'); // Return dummy element handle (body) to signify success const bodyHandle = await currentFrame.$('body'); if (bodyHandle) return bodyHandle; } } } catch (e) { } addLog(`[findElementWithFallbacks] ✗ Failed to find element after all attempts`); return null; } // Try to find element immediately without waiting async function tryFindElementImmediate(frame, selector, selectorType, addLog, index = 0) { try { let element = null; // Clean and determine selector type let cleanSelector = selector.trim(); let isXPath = false; // Check for xpath:// prefix if (cleanSelector.startsWith('xpath://')) { isXPath = true; cleanSelector = cleanSelector.replace(/^xpath:\/\//, ''); addLog('[tryFindElementImmediate] XPath prefix detected, cleaned selector: ' + JSON.stringify(cleanSelector)); } // Check for explicit xpath type or XPath syntax else if (selectorType === 'xpath' || cleanSelector.includes('//') || cleanSelector.startsWith('/')) { isXPath = true; cleanSelector = cleanSelector.replace(/^xpath:/, ''); // Remove xpath: prefix if present addLog('[tryFindElementImmediate] XPath detected by syntax/type, cleaned selector: ' + JSON.stringify(cleanSelector)); } // Check for css:// prefix else if (cleanSelector.startsWith('css://')) { isXPath = false; cleanSelector = cleanSelector.replace(/^css:\/\//, ''); addLog('[tryFindElementImmediate] CSS prefix detected, cleaned selector: ' + JSON.stringify(cleanSelector)); } addLog('[tryFindElementImmediate] Using ' + (isXPath ? 'XPath' : 'CSS') + ' method for: ' + JSON.stringify(cleanSelector)); if (isXPath) { // Handle XPath query const elements = await frame.evaluateHandle((sel) => { const result = document.evaluate(sel, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); const nodes = []; for (let i = 0; i < result.snapshotLength; i++) { nodes.push(result.snapshotItem(i)); } return nodes; }, cleanSelector); addLog(`[tryFindElementImmediate] XPath query found ${await elements.evaluate(e => e.length)} elements`); if (await elements.evaluate(e => e.length) > 0) { element = await frame.$(`xpath=${cleanSelector}`); if (element && index > 0) { const allElements = await frame.evaluateHandle((sel) => { const result = document.evaluate(sel, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); const nodes = []; for (let i = 0; i < result.snapshotLength; i++) { nodes.push(result.snapshotItem(i)); } return nodes; }, cleanSelector); const allElementHandles = await allElements.evaluateHandle((nodes) => nodes); const allElementsArray = await allElementHandles.evaluate((nodes) => nodes.map((node, i) => i)); if (allElementsArray.length > index) { const indexedElement = await frame.evaluateHandle((nodes, idx) => nodes[idx], allElementHandles, index); element = indexedElement.asElement(); } await allElements.dispose(); await allElementHandles.dispose(); } } } else { // Handle CSS selector const elements = await frame.$$(cleanSelector); addLog(`[tryFindElementImmediate] CSS query found ${elements.length} elements`); if (elements.length > 0) { if (index >= 0 && index < elements.length) { element = elements[index]; } else { element = elements[0]; } } // Dispose of extra handles if any elements.forEach((el, i) => { if (i !== index && el !== element) el.dispose(); }); } return element; } catch (error) { addLog(`[tryFindElementImmediate] Error: ${error.message}`); return null; } } // Helper function to convert simple XPath expressions to CSS selectors function convertSimpleXPathToCSS(xpath) { // Handle common XPath patterns // //button[text()='Open Modal'] -> button (we'll handle text matching differently) if (xpath.match(/^\/\/button\[text\(\)=['"]([^'"]+)['"]\]$/)) { const match = xpath.match(/^\/\/button\[text\(\)=['"]([^'"]+)['"]\]$/); if (match) { // Return a selector that will be handled by text-based strategies later return null; // Let fallback strategies handle this } } // //button[contains(text(),'Open Modal')] -> similar approach if (xpath.match(/^\/\/button\[contains\(text\(\),['"]([^'"]+)['"]\)\]$/)) { return null; // Let fallback strategies handle this } // //button[@id='myBtn'] -> button#myBtn const idMatch = xpath.match(/^\/\/(\w+)\[@id=['"]([^'"]+)['"]\]$/); if (idMatch) { return `${idMatch[1]}#${idMatch[2]}`; } // //button[@class='btn'] -> button.btn const classMatch = xpath.match(/^\/\/(\w+)\[@class=['"]([^'"]+)['"]\]$/); if (classMatch) { return `${classMatch[1]}.${classMatch[2].replace(/\s+/g, '.')}`; } // //*[@id='myBtn'] -> #myBtn const anyIdMatch = xpath.match(/^\/\/\*\[@id=['"]([^'"]+)['"]\]$/); if (anyIdMatch) { return `#${anyIdMatch[1]}`; } return null; } // Generate fallback strategies function generateFallbackStrategies(primarySelector) { const strategies = []; // Clean selector for fallbacks const cleanSelector = primarySelector.replace(/^["']|["']$/g, '').trim(); // If it's already a good selector, try variations if (cleanSelector.includes('//')) { // XPath variations strategies.push({ type: 'xpath-original', selector: cleanSelector, method: 'xpath' }); // Extract text from XPath for CSS alternatives const textMatch = cleanSelector.match(/text\(\)=['"]([^'"]+)['"]/); if (textMatch) { const buttonText = textMatch[1]; // Add CSS alternatives for button text strategies.push({ type: 'css-button-id', selector: '#myBtn', method: 'css' }); // W3Schools specific strategies.push({ type: 'css-button-class', selector: 'button.w3-button', method: 'css' }); strategies.push({ type: 'css-button-generic', selector: 'button', method: 'css' }); } strategies.push({ type: 'xpath-contains-text', selector: `//*[contains(text(), "${cleanSelector.replace(/.*text\(\)=["']([^"']+)["'].*/, '$1')}")]`, method: 'xpath' }); } else if (cleanSelector.includes('#') || cleanSelector.includes('.') || cleanSelector.includes('[')) { // CSS selector strategies.push({ type: 'css-original', selector: cleanSelector, method: 'css' }); } else { // Try as various attributes strategies.push({ type: 'id', selector: `#${cleanSelector}`, method: 'css' }); strategies.push({ type: 'class', selector: `.${cleanSelector}`, method: 'css' }); strategies.push({ type: 'name', selector: `[name="${cleanSelector}"]`, method: 'css' }); strategies.push({ type: 'data-testid', selector: `[data-testid="${cleanSelector}"]`, method: 'css' }); strategies.push({ type: 'aria-label', selector: `[aria-label="${cleanSelector}"]`, method: 'css' }); // Attribute *contains* fallbacks (case-insensitive) const safeContains = cleanSelector.replace(/"/g, '\\"'); strategies.push({ type: 'id-contains', selector: `[id*="${safeContains}" i]`, method: 'css' }); strategies.push({ type: 'class-contains', selector: `[class*="${safeContains}" i]`, method: 'css' }); strategies.push({ type: 'data-testid-contains', selector: `[data-testid*="${safeContains}" i]`, method: 'css' }); // Also try kebab-case version (spaces->-, lowercased) if (safeContains.includes(' ')) { const kebab = safeContains.toLowerCase().replace(/\s+/g, '-'); strategies.push({ type: 'data-testid-kebab', selector: `[data-test*="${kebab}" i]`, method: 'css' }); strategies.push({ type: 'id-kebab-contains', selector: `[id*="${kebab}" i]`, method: 'css' }); strategies.push({ type: 'class-kebab-contains', selector: `[class*="${kebab}" i]`, method: 'css' }); } // Text-based XPath strategies.push({ type: 'exact-text', selector: `//*[normalize-space(text())="${cleanSelector}"]`, method: 'xpath' }); strategies.push({ type: 'contains-text', selector: `//*[contains(normalize-space(text()), "${cleanSelector}")]`, method: 'xpath' }); strategies.push({ type: 'button-text', selector: `//button[contains(text(), "${cleanSelector}")]`, method: 'xpath' }); // If the selector is a single word but may appear as two words (e.g., "Login" vs "Log In") if (/^[a-zA-Z]+$/.test(cleanSelector)) { const spaced = cleanSelector.replace(/([a-z])([A-Z])/g, '$1 $2'); // camelCase -> camel Case if (spaced !== cleanSelector) { strategies.push({ type: 'contains-text-spaced', selector: `//*[contains(normalize-space(text()), "${spaced}")]`, method: 'xpath' }); } } // If the selector has multiple words, require all words present (AND condition) const words = cleanSelector.split(/\s+/).filter(Boolean); if (words.length > 1) { const andContains = words.map(w => `contains(translate(normalize-space(text()), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), '${w.toLowerCase()}')`).join(' and '); strategies.push({ type: 'contains-all-words', selector: `//*[${andContains}]`, method: 'xpath' }); } // Add common href pattern for auth/login const loginRegex = /^(login|log in)$/i; const signInRegex = /^sign\s*-?\s*in$/i; if (loginRegex.test(cleanSelector) || signInRegex.test(cleanSelector)) { strategies.push({ type: 'href-login-path', selector: 'a[href*="/login" i], a[href*="auth" i][href*="login" i]', method: 'css' }); } } // Add W3Schools specific fallbacks for modal button if (cleanSelector.includes('Open Modal') || cleanSelector.includes('Modal')) { strategies.push({ type: 'w3schools-modal-id', selector: '#myBtn', method: 'css' }); strategies.push({ type: 'w3schools-modal-class', selector: 'button.w3-button', method: 'css' }); strategies.push({ type: 'w3schools-modal-class-green', selector: 'button.w3-button.w3-green', method: 'css' }); strategies.push({ type: 'w3schools-modal-class-blue', selector: 'button.w3-button.w3-blue', method: 'css' }); strategies.push({ type: 'w3schools-modal-onclick', selector: 'button[onclick*="modal"]', method: 'css' }); } // Add W3Schools specific fallbacks for close buttons if (cleanSelector.includes('close') || cleanSelector.includes('Close')) { strategies.push({ type: 'w3schools-close-class', selector: 'span.w3-button.w3-display-topright', method: 'css' }); strategies.push({ type: 'w3schools-close-onclick', selector: 'span[onclick*="display=\'none\']', method: 'css' }); strategies.push({ type: 'w3schools-close-symbol', selector: 'span.w3-xlarge', method: 'css' }); } // If the target looks like a login/sign-in button/link, add typical variants const loginRegex = /^(login|log in)$/i; const signInRegex = /^sign\s*-?\s*in$/i; if (loginRegex.test(cleanSelector) || signInRegex.test(cleanSelector)) { strategies.push({ type: 'href-login', selector: 'a[href*="login" i]', method: 'css' }); strategies.push({ type: 'href-signin', selector: 'a[href*="sign" i][href*="in" i]', method: 'css' }); strategies.push({ type: 'button-login', selector: 'button[id*="login" i], button[class*="login" i]', method: 'css' }); strategies.push({ type: 'button-signin', selector: 'button[id*="sign" i][id*="in" i], button[class*="sign" i][class*="in" i]', method: 'css' }); strategies.push({ type: 'xpath-login-text', selector: `//a[contains(translate(normalize-space(text()), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'login') or contains(translate(normalize-space(text()), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'log in') or contains(translate(normalize-space(text()), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'sign in')]`, method: 'xpath' }); } // ------------------------------------------------------------ // Synonym expansion – map common "username/user/email" terms // ------------------------------------------------------------ const lowerSel = cleanSelector.toLowerCase(); if (/(^|[^a-z])(user(name)?)([^a-z]|$)/i.test(lowerSel)) { const syns = [ { type: 'email-id', selector: '#email', method: 'css' }, { type: 'email-name', selector: '[name*="email" i]', method: 'css' }, { type: 'email-placeholder', selector: '[placeholder*="email" i]', method: 'css' }, { type: 'email-input', selector: 'input[type="email"]', method: 'css' }, ]; strategies.unshift(...syns.reverse()); } if (/(^|[^a-z])email([^a-z]|$)/i.test(lowerSel)) { const syns = [ { type: 'username-id', selector: '#username', method: 'css' }, { type: 'user-id', selector: '#user', method: 'css' }, { type: 'user-name', selector: '[name*="user" i]', method: 'css' }, { type: 'user-placeholder', selector: '[placeholder*="user" i]', method: 'css' }, ]; strategies.unshift(...syns.reverse()); } if (/locate the email input field/i.test(primarySelector)) { const syns = [ { type: 'email-input', selector: 'input[type="email"]', method: 'css' }, { type: 'email-id', selector: '#email', method: 'css' }, { type: 'email-name', selector: '[name="email" i]', method: 'css' }, { type: 'email-placeholder', selector: '[placeholder*="email" i]', method: 'css' }, ]; strategies.unshift(...syns.reverse()); } if (/locate the password input field/i.test(primarySelector)) { const syns = [ { type: 'password-input', selector: 'input[type="password"]', method: 'css' }, { type: 'password-id', selector: '#password', method: 'css' }, { type: 'password-name', selector: '[name="password" i]', method: 'css' }, { type: 'password-placeholder', selector: '[placeholder*="password" i]', method: 'css' }, ]; strategies.unshift(...syns.reverse()); } if (/^checkout$/i.test(cleanSelector)) { const syns = [ { type: 'checkout-id', selector: '#checkout', method: 'css' }, { type: 'checkout-data-test', selector: '[data-test*="checkout" i]', method: 'css' }, { type: 'checkout-text-ci', selector: `//*[contains(translate(normalize-space(text()), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'checkout')]`, method: 'xpath' } ]; strategies.unshift(...syns.reverse()); } if (/^finish$/i.test(cleanSelector)) { const syns = [ { type: 'finish-id', selector: '#finish', method: 'css' }, { type: 'finish-data-test', selector: '[data-test*="finish" i]', method: 'css' }, { type: 'finish-text-ci', selector: `//*[contains(translate(normalize-space(text()), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'finish')]`, method: 'xpath' } ]; strategies.unshift(...syns.reverse()); } return strategies; } // Verify element visibility efficiently async function verifyElementVisibility(element) { try { return await element.evaluate(el => { const style = window.getComputedStyle(el); const rect = el.getBoundingClientRect(); return style.display !== 'none' && style.visibility !== 'hidden' && parseFloat(style.opacity) > 0 && rect.width > 0 && rect.height > 0; }); } catch (error) { return false; } } // Fast element waiting with shorter timeout async function waitForElement(frame, selector, timeout, type = 'css') { const endTime = Date.now() + timeout; const pollInterval = 200; // Increased poll interval for better performance while (Date.now() < endTime) { try { let element = null; if (type === 'xpath') { // Try multiple approaches for XPath, similar to tryFindElementImmediate try { if ('$x' in frame && typeof frame.$x === 'function') { const elements = await frame.$x(selector); if (elements.length > 0) { element = elements[0]; } } else { // Fallback: use evaluateHandle with document.evaluate const elementHandle = await frame.evaluateHandle((xpath) => { const result = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); return result.singleNodeValue; }, selector); if (elementHandle) { const asElement = elementHandle.asElement(); if (asElement) { element = asElement; } else { await elementHandle.dispose(); } } } } catch (xpathError) { // Try CSS equivalent as fallback const cssEquivalent = convertSimpleXPathToCSS(selector); if (cssEquivalent) { element = await frame.$(cssEquivalent); } } } else { element = await frame.$(selector); } if (element) { const isConnected = await element.evaluate(el => el.isConnected); if (isConnected) { return element; } await element.dispose(); } } catch (error) { // Ignore errors during waiting } // Wait before next attempt await new Promise(resolve => setTimeout(resolve, pollInterval)); } return null; } // Enhanced DOM capture for better AI performance async function captureFocusedDomSnippet(page, currentFrame, failedSelector, addLog) { try { const domSnippet = await (currentFrame === page ? page : currentFrame).evaluate((selector) => { // Enhanced DOM capture with better context but limited size const buttons = Array.from(document.querySelectorAll('button')).slice(0, 10).map(b => `<button${b.id ? ` id="${b.id}"` : ''}${b.className ? ` class="${b.className}"` : ''}${b.onclick ? ` onclick="..."` : ''}>${b.textContent?.trim().substring(0, 50) || ''}</button>`); const inputs = Array.from(document.querySelectorAll('input')).slice(0, 5).map(i => `<input${i.id ? ` id="${i.id}"` : ''}${i.className ? ` class="${i.className}"` : ''}${i.type ? ` type="${i.type}"` : ''}${i.name ? ` name="${i.name}"` : ''}${i.placeholder ? ` placeholder="${i.placeholder}"` : ''}>`); const images = Array.from(document.querySelectorAll('img')).slice(0, 5).map(img => `<img${img.id ? ` id="${img.id}"` : ''}${img.className ? ` class="${img.className}"` : ''}${img.alt ? ` alt="${img.alt}"` : ''}${img.src ? ` src="${img.src.substring(0, 50)}..."` : ''}>`); const links = Array.from(document.querySelectorAll('a')).slice(0, 5).map(a => `<a${a.id ? ` id="${a.id}"` : ''}${a.className ? ` class="${a.className}"` : ''}${a.href ? ` href="${a.href.substring(0, 30)}..."` : ''}>${a.textContent?.trim().substring(0, 50) || ''}</a>`); const divs = Array.from(document.querySelectorAll('div[id], div[class*="gallery"], div[class*="trash"], div[class*="modal"], div[class*="popup"]')).slice(0, 5).map(d => `<div${d.id ? ` id="${d.id}"` : ''}${d.className ? ` class="${d.className}"` : ''}>${d.children.length > 0 ? '...' : (d.textContent?.trim().substring(0, 30) || '')}</div>`); const spans = Array.from(document.querySelectorAll('span[id], span[class]')).slice(0, 5).map(s => `<span${s.id ? ` id="${s.id}"` : ''}${s.className ? ` class="${s.className}"` : ''}>${s.textContent?.trim().substring(0, 30) || ''}</span>`); // Get title and URL for context const pageInfo = `Page: ${document.title} (${window.location.href})`; return `${pageInfo}\n\nButtons: ${buttons.join(', ')}\n\nInputs: ${inputs.join(', ')}\n\nImages: ${images.join(', ')}\n\nLinks: ${links.join(', ')}\n\nDivs: ${divs.join(', ')}\n\nSpans: ${spans.join(', ')}`; }, failedSelector); addLog(`[DOM Capture] Captured ${domSnippet.length} characters of DOM context`); return domSnippet; } catch (error) { addLog(`[DOM Capture] Error: ${error.message}`); return 'Failed to capture DOM snippet'; } } // Legacy class-based interface for backward compatibility class ElementFinder { constructor(page, frame, addLog, retryApiCallFn) { this.page = page; this.frame = frame; this.addLog = addLog; this.retryApiCallFn = retryApiCallFn; } async findElement(context, options = {}) { return await findElementWithFallbacks(this.page, this.frame, this.addLog, context.selector, context.descriptiveTerm, context.originalStep, !options.retryWithAI, this.retryApiCallFn, context.index || 0); } } exports.ElementFinder = ElementFinder; //# sourceMappingURL=elementFinderV2.js.map