/** * Shared selector parsing and validation library * Supports CSS and XPath selectors with security validation */ /** * Parse and validate selector * @param {string} selector - CSS or XPath selector * @returns {{type: 'css'|'xpath', selector: string}} * @throws {Error} If XPath contains injection patterns */ export function parseSelector(selector) { if (!selector || typeof selector !== 'string') { throw new Error('Selector must be a non-empty string'); } // Detect XPath selectors if (selector.startsWith('/') || selector.startsWith('(//')) { // XPath injection prevention validateXPath(selector); return { type: 'xpath', selector }; } // CSS selector return { type: 'css', selector }; } /** * Validate XPath selector for security * @param {string} xpath - XPath expression to validate * @throws {Error} If XPath contains dangerous patterns */ function validateXPath(xpath) { const dangerous = [ 'javascript:', ' 1000) { throw new Error('XPath selector too long (max 1000 characters)'); } } /** * Wait for element based on selector type * @param {Object} page - Puppeteer page instance * @param {{type: string, selector: string}} parsed - Parsed selector * @param {Object} options - Wait options (visible, timeout) * @returns {Promise} */ export async function waitForElement(page, parsed, options = {}) { const defaultOptions = { visible: true, timeout: 5000, ...options }; if (parsed.type === 'xpath') { // Use locator API for XPath (Puppeteer v24+) const locator = page.locator(`::-p-xpath(${parsed.selector})`); // setVisibility and setTimeout are the locator options await locator .setVisibility(defaultOptions.visible ? 'visible' : null) .setTimeout(defaultOptions.timeout) .wait(); } else { await page.waitForSelector(parsed.selector, defaultOptions); } } /** * Click element based on selector type * @param {Object} page - Puppeteer page instance * @param {{type: string, selector: string}} parsed - Parsed selector * @returns {Promise} */ export async function clickElement(page, parsed) { if (parsed.type === 'xpath') { // Use locator API for XPath (Puppeteer v24+) const locator = page.locator(`::-p-xpath(${parsed.selector})`); await locator.click(); } else { await page.click(parsed.selector); } } /** * Type into element based on selector type * @param {Object} page - Puppeteer page instance * @param {{type: string, selector: string}} parsed - Parsed selector * @param {string} value - Text to type * @param {Object} options - Type options (delay, clear) * @returns {Promise} */ export async function typeIntoElement(page, parsed, value, options = {}) { if (parsed.type === 'xpath') { // Use locator API for XPath (Puppeteer v24+) const locator = page.locator(`::-p-xpath(${parsed.selector})`); // Clear if requested if (options.clear) { await locator.fill(''); } await locator.fill(value); } else { // CSS selector if (options.clear) { await page.$eval(parsed.selector, el => el.value = ''); } await page.type(parsed.selector, value, { delay: options.delay || 0 }); } } /** * Get element handle based on selector type * @param {Object} page - Puppeteer page instance * @param {{type: string, selector: string}} parsed - Parsed selector * @returns {Promise} */ export async function getElement(page, parsed) { if (parsed.type === 'xpath') { // For XPath, use page.evaluate with XPath evaluation // This returns the first matching element const element = await page.evaluateHandle((xpath) => { const result = document.evaluate( xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null ); return result.singleNodeValue; }, parsed.selector); // Convert JSHandle to ElementHandle const elementHandle = element.asElement(); return elementHandle; } else { return await page.$(parsed.selector); } } /** * Get enhanced error message for selector failures * @param {Error} error - Original error * @param {string} selector - Selector that failed * @returns {Error} Enhanced error with troubleshooting tips */ export function enhanceError(error, selector) { if (error.message.includes('waiting for selector') || error.message.includes('waiting for XPath') || error.message.includes('No node found')) { error.message += '\n\nTroubleshooting:\n' + '1. Use snapshot.js to find correct selector: node snapshot.js --url \n' + '2. Try XPath selector: //button[text()="Click"] or //button[contains(text(),"Click")]\n' + '3. Check element is visible on page (not display:none or hidden)\n' + '4. Increase --timeout value: --timeout 10000\n' + '5. Change wait strategy: --wait-until load or --wait-until domcontentloaded'; } return error; }