135 lines
6.4 KiB
JavaScript
135 lines
6.4 KiB
JavaScript
/* cdp-verify-batch.mjs — 验证三列表(账号列表/视频列表/AI写脚本列表)批量操作 UI(09-01)
|
||||
|
|
* 覆盖点:①左下角「批量操作」图标(check-square)存在
|
|||
|
|
* ②点击后每行出现勾选框 + 删除(trash)/取消(x) 图标 + 全选 + 计数
|
|||
|
|
* ③勾选计数正确、全选正确、取消恢复常态
|
|||
|
|
* 方法:CDP 9223 → 复用 8900 标签页 → 模拟点击(不真实调用删除 API)
|
|||
|
|
* 运行:node cdp-verify-batch.mjs
|
|||
|
|
*/
|
|||
|
|
const CDP = 'http://127.0.0.1:9223';
|
|||
|
|
|
|||
|
|
async function getWs(targetId) {
|
|||
|
|
const tabs = await (await fetch(CDP + '/json')).json();
|
|||
|
|
const t = tabs.find((x) => x.id === targetId);
|
|||
|
|
if (!t) throw new Error('target not found: ' + targetId);
|
|||
|
|
return t.webSocketDebuggerUrl;
|
|||
|
|
}
|
|||
|
|
function cdp(ws, method, params = {}) {
|
|||
|
|
return new Promise((resolve, reject) => {
|
|||
|
|
const id = ++cdp._id;
|
|||
|
|
const w = new WebSocket(ws);
|
|||
|
|
const timer = setTimeout(() => { try { w.close(); } catch (e) {} reject(new Error('timeout: ' + method)); }, 30000);
|
|||
|
|
w.onopen = () => w.send(JSON.stringify({ id, method, params }));
|
|||
|
|
w.onmessage = (ev) => {
|
|||
|
|
const m = JSON.parse(ev.data);
|
|||
|
|
if (m.id !== id) return;
|
|||
|
|
clearTimeout(timer);
|
|||
|
|
try { w.close(); } catch (e) {}
|
|||
|
|
m.error ? reject(new Error(method + ': ' + JSON.stringify(m.error))) : resolve(m.result);
|
|||
|
|
};
|
|||
|
|
w.onerror = () => { clearTimeout(timer); reject(new Error('ws error: ' + method)); };
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
cdp._id = 0;
|
|||
|
|
|
|||
|
|
async function evalJs(ws, expr) {
|
|||
|
|
const r = await cdp(ws, 'Runtime.evaluate', { expression: expr, awaitPromise: true, returnByValue: true });
|
|||
|
|
if (r.exceptionDetails) throw new Error('eval err: ' + JSON.stringify(r.exceptionDetails).slice(0, 300));
|
|||
|
|
return r.result && r.result.value;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|||
|
|
|
|||
|
|
async function waitFor(ws, expr, timeout = 40000, label = '') {
|
|||
|
|
const t0 = Date.now();
|
|||
|
|
while (Date.now() - t0 < timeout) {
|
|||
|
|
try { if (await evalJs(ws, expr)) return true; } catch (e) {}
|
|||
|
|
await sleep(800);
|
|||
|
|
}
|
|||
|
|
throw new Error('waitFor timeout: ' + label + ' -> ' + expr);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let pass = 0, fail = 0;
|
|||
|
|
function ok(cond, label) {
|
|||
|
|
if (cond) { pass++; console.log(' ✅ ' + label); }
|
|||
|
|
else { fail++; console.log(' ❌ FAIL: ' + label); }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 对单个列表页执行批量操作 UI 全链路验证
|
|||
|
|
async function verifyList(ws, hash, label, rowSelector) {
|
|||
|
|
console.log(`\n【${label}】 ${hash}`);
|
|||
|
|
await evalJs(ws, `location.hash = '${hash}'`);
|
|||
|
|
await cdp(ws, 'Page.reload', { ignoreCache: true });
|
|||
|
|
await waitFor(ws, `!!document.querySelector('${rowSelector}')`, 40000, label + ' 列表渲染');
|
|||
|
|
await sleep(400);
|
|||
|
|
|
|||
|
|
// ① 常态:左下角批量操作图标按钮存在(含 svg 图标)
|
|||
|
|
const init = await evalJs(ws, `(() => {
|
|||
|
|
const bar = document.querySelector('.list-batchbar');
|
|||
|
|
const btn = bar && bar.querySelector('[data-batch-open]');
|
|||
|
|
return { hasBar: !!bar, hasBtn: !!btn, hasIcon: !!(btn && btn.querySelector('.ic')), btnText: btn ? btn.textContent.trim() : '' };
|
|||
|
|
})()`);
|
|||
|
|
ok(init.hasBar && init.hasBtn, `左下角批量操作条存在`);
|
|||
|
|
ok(init.hasIcon, `批量操作按钮为图标(非文字,文本="${init.btnText}")`);
|
|||
|
|
ok(init.btnText.trim() === '', `按钮无文字残留(纯图标)`);
|
|||
|
|
|
|||
|
|
// ② 点击批量操作图标 → 进入批量模式
|
|||
|
|
await evalJs(ws, `document.querySelector('[data-batch-open]').click()`);
|
|||
|
|
await waitFor(ws, `!!document.querySelector('.batch-cb-all')`, 20000, label + ' 批量模式进入');
|
|||
|
|
await sleep(300);
|
|||
|
|
const mode = await evalJs(ws, `(() => {
|
|||
|
|
const rows = document.querySelectorAll('tbody tr .batch-cb').length;
|
|||
|
|
const delBtn = document.querySelector('[data-batch-del]');
|
|||
|
|
const cancelBtn = document.querySelector('[data-batch-cancel]');
|
|||
|
|
return {
|
|||
|
|
rowCbs: rows,
|
|||
|
|
hasDel: !!delBtn, delIcon: !!(delBtn && delBtn.querySelector('.ic')),
|
|||
|
|
hasCancel: !!cancelBtn, cancelIcon: !!(cancelBtn && cancelBtn.querySelector('.ic')),
|
|||
|
|
hasAll: !!document.querySelector('.batch-cb-all'),
|
|||
|
|
delText: delBtn ? delBtn.textContent.trim() : '',
|
|||
|
|
};
|
|||
|
|
})()`);
|
|||
|
|
ok(mode.rowCbs > 0, `每行出现勾选框(${mode.rowCbs} 行)`);
|
|||
|
|
ok(mode.hasDel && mode.delIcon, `删除按钮为图标(trash)`);
|
|||
|
|
ok(mode.hasCancel && mode.cancelIcon, `取消按钮为图标(x)`);
|
|||
|
|
ok(mode.hasAll, `表头全选框存在`);
|
|||
|
|
ok(mode.delText === '', `删除按钮无文字残留(纯图标)`);
|
|||
|
|
|
|||
|
|
// ③ 勾选 2 行 → 计数更新
|
|||
|
|
await evalJs(ws, `(() => { const cbs = document.querySelectorAll('.batch-cb'); cbs[0].click(); cbs[1].click(); return true; })()`);
|
|||
|
|
await sleep(200);
|
|||
|
|
const cnt = await evalJs(ws, `document.querySelector('.batch-count-n').textContent`);
|
|||
|
|
ok(cnt === '2', `勾选 2 行计数显示 2(实际 ${cnt})`);
|
|||
|
|
|
|||
|
|
// ④ 全选 → 计数=行数
|
|||
|
|
await evalJs(ws, `document.querySelector('.batch-cb-all').click()`);
|
|||
|
|
await sleep(200);
|
|||
|
|
const cntAll = await evalJs(ws, `document.querySelector('.batch-count-n').textContent`);
|
|||
|
|
const totalRows = await evalJs(ws, `document.querySelectorAll('.batch-cb').length`);
|
|||
|
|
ok(cntAll === String(totalRows), `全选计数=行数 ${totalRows}(实际 ${cntAll})`);
|
|||
|
|
|
|||
|
|
// ⑤ 取消 → 恢复常态(无勾选框)
|
|||
|
|
await evalJs(ws, `document.querySelector('[data-batch-cancel]').click()`);
|
|||
|
|
await sleep(400);
|
|||
|
|
const back = await evalJs(ws, `(() => {
|
|||
|
|
const bar = document.querySelector('.list-batchbar');
|
|||
|
|
return { hasCbs: document.querySelectorAll('.batch-cb').length > 0, hasOpen: !!(bar && bar.querySelector('[data-batch-open]')) };
|
|||
|
|
})()`);
|
|||
|
|
ok(!back.hasCbs && back.hasOpen, `取消后恢复常态(勾选框消失,批量操作图标回归)`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function main() {
|
|||
|
|
const tabs = await (await fetch(CDP + '/json')).json();
|
|||
|
|
const wb = tabs.find((t) => t.url.includes('localhost:8900') || t.url.includes('127.0.0.1:8900'));
|
|||
|
|
if (!wb) throw new Error('未找到 8900 工作台标签页,请先在内置浏览器打开工作台');
|
|||
|
|
const ws = await getWs(wb.id);
|
|||
|
|
console.log('▶ 复用标签页:', wb.url);
|
|||
|
|
|
|||
|
|
await verifyList(ws, '#/accounts', '账号列表', 'tbody tr a.link');
|
|||
|
|
await verifyList(ws, '#/account/3', '账号详情-视频列表(俊希)', 'tbody tr .td-id a');
|
|||
|
|
await verifyList(ws, '#/rewrites', 'AI写脚本列表', 'tbody tr [data-rewrite]');
|
|||
|
|
|
|||
|
|
console.log(`\n==== ${fail === 0 ? '全部 PASS' : '存在 FAIL ' + fail + ' 项'}(PASS ${pass} / FAIL ${fail})====`);
|
|||
|
|
process.exit(fail === 0 ? 0 : 1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
main().catch((e) => { console.error('脚本异常:', e.message); process.exit(1); });
|