255 lines
8.0 KiB
JavaScript
255 lines
8.0 KiB
JavaScript
const { chromium } = require('playwright');
|
||
const fs = require('fs').promises;
|
||
const path = require('path');
|
||
|
||
async function getAXTreeForUrl(url, browser) {
|
||
// 创建新页面
|
||
const page = await browser.newPage();
|
||
const result = {
|
||
axtree: "",
|
||
idToSelector: {},
|
||
error: null
|
||
};
|
||
|
||
try {
|
||
// 访问页面并等待加载
|
||
await page.goto(url, {
|
||
waitUntil: 'domcontentloaded',
|
||
timeout: 30000
|
||
});
|
||
|
||
await page.waitForTimeout(5000);
|
||
|
||
const clickedButtons = new Set();
|
||
|
||
const expandButtons = async () => {
|
||
console.log('开始寻找可展开按钮...');
|
||
const buttons = await page.$$('button[aria-label*="Expand"], button[aria-label*="Open"], button[aria-expanded="false"]');
|
||
console.log(`找到 ${buttons.length} 个折叠按钮`);
|
||
|
||
let newButtonsFound = false;
|
||
|
||
for (const button of buttons) {
|
||
try {
|
||
// 使用更可靠的滚动方法
|
||
await page.evaluate(element => {
|
||
// 使用JavaScript的scrollIntoView,更直接且兼容性更好
|
||
element.scrollIntoView({behavior: 'smooth', block: 'center'});
|
||
}, button);
|
||
|
||
const ariaLabel = await button.getAttribute('aria-label');
|
||
if (!clickedButtons.has(ariaLabel)) {
|
||
console.log(`点击新按钮: ${ariaLabel}`);
|
||
await button.click();
|
||
clickedButtons.add(ariaLabel);
|
||
newButtonsFound = true;
|
||
await page.waitForTimeout(200);
|
||
}
|
||
} catch (e) {
|
||
console.log(`点击失败: ${e.message}`);
|
||
}
|
||
}
|
||
|
||
return newButtonsFound;
|
||
};
|
||
|
||
let iteration = 1;
|
||
while (true) {
|
||
console.log(`\n第 ${iteration} 次查找...`);
|
||
const foundNewButtons = await expandButtons();
|
||
|
||
if (!foundNewButtons) {
|
||
console.log('没有发现新的可展开按钮,结束查找');
|
||
break;
|
||
}
|
||
|
||
console.log(`已点击按钮数量: ${clickedButtons.size}`);
|
||
await page.waitForTimeout(500);
|
||
iteration++;
|
||
}
|
||
|
||
// await page.waitForTimeout(5000);
|
||
|
||
// 获取 AXTree
|
||
const axTree = await page.accessibility.snapshot({ interestingOnly: false });
|
||
|
||
// 用于存储 id 到 selector 的映射
|
||
let idCounter = 1;
|
||
const idToSelector = {};
|
||
const nodeParents = new Map();
|
||
|
||
// 用于构建文本形式的 AXTree
|
||
let axtreeText = [];
|
||
|
||
function traverse(node, depth = 0, parent = null) {
|
||
nodeParents.set(node, parent);
|
||
|
||
if (((node.role === 'none' || node.role === 'generic' || node.role === 'InlineTextBox') &&
|
||
!node.name &&
|
||
!node.focusable &&
|
||
!node.focused &&
|
||
node.expanded === undefined) ||
|
||
node.role === 'InlineTextBox'
|
||
) {
|
||
if (node.children?.length > 0) {
|
||
for (const child of node.children) {
|
||
traverse(child, depth, node);
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
|
||
const currentId = idCounter++;
|
||
|
||
let selectorParts = [`role=${node.role}`];
|
||
if (node.name) {
|
||
selectorParts.push(`[name="${node.name}"]`);
|
||
}
|
||
|
||
if (node.selected) selectorParts.push('[selected=true]');
|
||
if (node.checked !== undefined) selectorParts.push(`[checked=${node.checked}]`);
|
||
if (node.pressed !== undefined) selectorParts.push(`[pressed=${node.pressed}]`);
|
||
|
||
if (parent && parent.role !== 'WebArea') {
|
||
let parentSelector = `role=${parent.role}`;
|
||
if (parent.name) {
|
||
parentSelector += `[name="${parent.name}"]`;
|
||
}
|
||
selectorParts.unshift(`${parentSelector} >>`);
|
||
}
|
||
|
||
if (parent?.children) {
|
||
const siblingIndex = parent.children.findIndex(child => child === node);
|
||
if (siblingIndex !== -1) {
|
||
selectorParts.push(`:nth-match(${siblingIndex + 1})`);
|
||
}
|
||
}
|
||
|
||
idToSelector[currentId] = selectorParts.join(' ');
|
||
|
||
// 收集所有可能的属性
|
||
let props = [];
|
||
if (node.focusable) props.push('focusable');
|
||
if (node.focused) props.push('focused');
|
||
if (node.expanded !== undefined) props.push(`expanded=${node.expanded}`);
|
||
if (node.selected) props.push('selected');
|
||
if (node.checked !== undefined) props.push(`checked=${node.checked}`);
|
||
if (node.disabled) props.push('disabled');
|
||
if (node.required) props.push('required');
|
||
if (node.pressed !== undefined) props.push(`pressed=${node.pressed}`);
|
||
|
||
// 判断元素是否可点击
|
||
const clickableRoles = ['button', 'link', 'menuitem', 'tab', 'checkbox', 'radio', 'switch', 'option'];
|
||
const isClickable = clickableRoles.includes(node.role) ||
|
||
node.focusable ||
|
||
node.role === 'generic' && node.name && node.focusable;
|
||
if (isClickable) props.push('clickable');
|
||
|
||
const indent = ' '.repeat(depth * 4);
|
||
const nodeLine = `${indent}[${currentId}] ${node.role} '${node.name || ''}'${props.length > 0 ? ' (' + props.join(', ') + ')' : ''}`;
|
||
axtreeText.push(nodeLine);
|
||
|
||
if (node.children?.length > 0) {
|
||
for (const child of node.children) {
|
||
traverse(child, depth + 1, node);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 添加根节点信息
|
||
let rootProps = [];
|
||
if (axTree.focusable) rootProps.push('focusable=True');
|
||
if (axTree.focused) rootProps.push('focused');
|
||
axtreeText.push(`Root${axTree.role ? axTree.role : 'WebArea'} '${axTree.name || ''}'${rootProps.length > 0 ? ', ' + rootProps.join(', ') : ''}`);
|
||
|
||
// 遍历 AXTree
|
||
if (axTree.children?.length > 0) {
|
||
for (const child of axTree.children) {
|
||
traverse(child, 1, axTree);
|
||
}
|
||
}
|
||
|
||
result.axtree = axtreeText.join('\n');
|
||
result.idToSelector = idToSelector;
|
||
|
||
} catch (error) {
|
||
result.error = error.message;
|
||
console.error(`Error processing URL ${url}:`, error);
|
||
} finally {
|
||
await page.close();
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
async function processUrls() {
|
||
try {
|
||
// 读取输入文件
|
||
const inputData = JSON.parse(
|
||
await fs.readFile('path/processed_3.json', 'utf8')
|
||
);
|
||
|
||
// 启动浏览器
|
||
const browser = await chromium.launch({ headless: true });
|
||
|
||
// 收集所有唯一的 URL
|
||
const uniqueUrls = new Set();
|
||
for (const item of Object.values(inputData)) {
|
||
if (item.shortestPathsMeta) {
|
||
for (const meta of item.shortestPathsMeta) {
|
||
if (meta.chainUrls) {
|
||
meta.chainUrls.forEach(url => uniqueUrls.add(url));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 存储结果的对象
|
||
const results = {};
|
||
|
||
// 处理每个 URL
|
||
let processed = 0;
|
||
const total = uniqueUrls.size;
|
||
|
||
for (const url of uniqueUrls) {
|
||
processed++;
|
||
console.log(`\n========== 处理 URL ${processed}/${total} ==========`);
|
||
console.log(`URL: ${url}`);
|
||
|
||
const result = await getAXTreeForUrl(url, browser);
|
||
results[url] = result;
|
||
|
||
// 打印 AXTree 信息
|
||
if (result.error) {
|
||
console.log(`获取 AXTree 失败: ${result.error}`);
|
||
} else {
|
||
console.log(`AXTree 获取成功,包含 ${Object.keys(result.idToSelector).length} 个节点`);
|
||
console.log(`AXTree 预览 (前5行):`);
|
||
const previewLines = result.axtree.split('\n').slice(0, 5);
|
||
console.log(previewLines.join('\n'));
|
||
if (result.axtree.split('\n').length > 5) {
|
||
console.log('... (更多内容已省略)');
|
||
}
|
||
}
|
||
console.log(`========== URL ${processed}/${total} 处理完成 ==========\n`);
|
||
}
|
||
|
||
// 关闭浏览器
|
||
await browser.close();
|
||
|
||
// 写入结果文件
|
||
await fs.writeFile(
|
||
'path/processed_3_axtree.json',
|
||
JSON.stringify(results, null, 2)
|
||
);
|
||
|
||
console.log('Processing complete. Results saved to path/processed_3_axtree.json');
|
||
|
||
} catch (error) {
|
||
console.error('Error in main process:', error);
|
||
}
|
||
}
|
||
|
||
// 运行主程序
|
||
processUrls();
|