Files
english/.opencode/skills/chrome-devtools/scripts/lib/selector.js
2026-04-12 01:06:31 +07:00

179 lines
5.3 KiB
JavaScript

/**
* 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:',
'<script',
'onerror=',
'onload=',
'onclick=',
'onmouseover=',
'eval(',
'Function(',
'constructor(',
];
const lower = xpath.toLowerCase();
for (const pattern of dangerous) {
if (lower.includes(pattern.toLowerCase())) {
throw new Error(`Potential XPath injection detected: ${pattern}`);
}
}
// Additional validation: check for extremely long selectors (potential DoS)
if (xpath.length > 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<void>}
*/
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<void>}
*/
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<void>}
*/
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<ElementHandle|null>}
*/
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 <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;
}