132 lines
3.4 KiB
JavaScript
132 lines
3.4 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Get DOM snapshot with selectors
|
|
* Usage: node snapshot.js [--url https://example.com] [--output snapshot.json]
|
|
*/
|
|
import { getBrowser, getPage, closeBrowser, parseArgs, outputJSON, outputError } from './lib/browser.js';
|
|
import fs from 'fs/promises';
|
|
|
|
async function snapshot() {
|
|
const args = parseArgs(process.argv.slice(2));
|
|
|
|
try {
|
|
const browser = await getBrowser({
|
|
headless: args.headless !== 'false'
|
|
});
|
|
|
|
const page = await getPage(browser);
|
|
|
|
// Navigate if URL provided
|
|
if (args.url) {
|
|
await page.goto(args.url, {
|
|
waitUntil: args['wait-until'] || 'networkidle2'
|
|
});
|
|
}
|
|
|
|
// Get interactive elements with metadata
|
|
const elements = await page.evaluate(() => {
|
|
const interactiveSelectors = [
|
|
'a[href]',
|
|
'button',
|
|
'input',
|
|
'textarea',
|
|
'select',
|
|
'[onclick]',
|
|
'[role="button"]',
|
|
'[role="link"]',
|
|
'[contenteditable]'
|
|
];
|
|
|
|
const elements = [];
|
|
const selector = interactiveSelectors.join(', ');
|
|
const nodes = document.querySelectorAll(selector);
|
|
|
|
nodes.forEach((el, index) => {
|
|
const rect = el.getBoundingClientRect();
|
|
|
|
// Generate unique selector
|
|
let uniqueSelector = '';
|
|
if (el.id) {
|
|
uniqueSelector = `#${el.id}`;
|
|
} else if (el.className) {
|
|
const classes = Array.from(el.classList).join('.');
|
|
uniqueSelector = `${el.tagName.toLowerCase()}.${classes}`;
|
|
} else {
|
|
uniqueSelector = el.tagName.toLowerCase();
|
|
}
|
|
|
|
elements.push({
|
|
index: index,
|
|
tagName: el.tagName.toLowerCase(),
|
|
type: el.type || null,
|
|
id: el.id || null,
|
|
className: el.className || null,
|
|
name: el.name || null,
|
|
value: el.value || null,
|
|
text: el.textContent?.trim().substring(0, 100) || null,
|
|
href: el.href || null,
|
|
selector: uniqueSelector,
|
|
xpath: getXPath(el),
|
|
visible: rect.width > 0 && rect.height > 0,
|
|
position: {
|
|
x: rect.x,
|
|
y: rect.y,
|
|
width: rect.width,
|
|
height: rect.height
|
|
}
|
|
});
|
|
});
|
|
|
|
function getXPath(element) {
|
|
if (element.id) {
|
|
return `//*[@id="${element.id}"]`;
|
|
}
|
|
if (element === document.body) {
|
|
return '/html/body';
|
|
}
|
|
let ix = 0;
|
|
const siblings = element.parentNode?.childNodes || [];
|
|
for (let i = 0; i < siblings.length; i++) {
|
|
const sibling = siblings[i];
|
|
if (sibling === element) {
|
|
return getXPath(element.parentNode) + '/' + element.tagName.toLowerCase() + '[' + (ix + 1) + ']';
|
|
}
|
|
if (sibling.nodeType === 1 && sibling.tagName === element.tagName) {
|
|
ix++;
|
|
}
|
|
}
|
|
return '';
|
|
}
|
|
|
|
return elements;
|
|
});
|
|
|
|
const result = {
|
|
success: true,
|
|
url: page.url(),
|
|
title: await page.title(),
|
|
elementCount: elements.length,
|
|
elements: elements
|
|
};
|
|
|
|
if (args.output) {
|
|
await fs.writeFile(args.output, JSON.stringify(result, null, 2));
|
|
outputJSON({
|
|
success: true,
|
|
output: args.output,
|
|
elementCount: elements.length
|
|
});
|
|
} else {
|
|
outputJSON(result);
|
|
}
|
|
|
|
if (args.close !== 'false') {
|
|
await closeBrowser();
|
|
}
|
|
} catch (error) {
|
|
outputError(error);
|
|
}
|
|
}
|
|
|
|
snapshot();
|