Files
mcn-short-video/project/短视频脚本创作/V1.0/mcn-workshop/cdp-verify-batch.mjs
T

135 lines
6.4 KiB
JavaScript
Raw Normal View History

/* 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); });