/* cdp-verify-footer.mjs — 验证账号列表渲染修复(09-01) * 背景:上一轮误加裸赋值 accReset(严格模式 ReferenceError)+ list-footer 未包账号列表 * 验证:①账号列表页渲染成功(无 console 异常)②批量操作条与分页同一行(.list-footer 内) * ③三列表均含 .list-footer ④批量模式切换正常 * 运行:node cdp-verify-footer.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 main() { // 找 8900 标签页 const tabs = await (await fetch(CDP + '/json')).json(); const tab = tabs.find((x) => x.url.includes('localhost:8900')) || tabs.find((x) => x.type === 'page'); if (!tab) throw new Error('no 8900 tab found'); const ws = await getWs(tab.id); await cdp(ws, 'Page.enable'); await cdp(ws, 'Runtime.enable'); await cdp(ws, 'Log.enable'); const errors = []; const onErr = (ev) => { const m = JSON.parse(ev.data); if (m.method === 'Runtime.exceptionThrown') errors.push(JSON.stringify(m.params.exceptionDetails).slice(0, 200)); }; const ws2 = new WebSocket(ws); await new Promise((r) => { ws2.onopen = r; }); ws2.onmessage = onErr; let pass = 0, fail = 0; const check = (name, cond) => { cond ? pass++ : fail++; console.log((cond ? 'PASS' : 'FAIL') + ' | ' + name); }; // 打开账号列表 await cdp(ws, 'Page.navigate', { url: 'http://localhost:8900/?v=verify#/accounts' }); await sleep(2500); const acc = await evalJs(ws, `(() => { const root = document.querySelector('#view, main, .view, #app'); const page = root ? root.innerHTML : document.body.innerHTML; return { hasError: /页面渲染失败|not defined|ReferenceError/.test(page), hasTable: !!document.querySelector('.table'), hasRows: document.querySelectorAll('.table tbody tr').length, footer: !!document.querySelector('.list-footer'), footerBoth: !!document.querySelector('.list-footer .list-batchbar') && !!document.querySelector('.list-footer .pager'), batchBtn: !!document.querySelector('[data-batch-open]'), title: document.querySelector('.page-title')?.textContent || '' }; })()`); check('账号列表渲染无错误文案', !acc.hasError); check('账号列表表格存在', acc.hasTable); check('列表有数据行', acc.hasRows > 0); check('.list-footer 存在(批量+分页同行容器)', acc.footer); check('.list-footer 内含批量条和分页器', acc.footerBoth); check('批量操作图标按钮存在', acc.batchBtn); console.log(' 页面标题:', acc.title, '| 行数:', acc.hasRows); // 点批量操作图标 → 验证批量模式 + 同行不破坏 await evalJs(ws, `document.querySelector('[data-batch-open]').click()`); await sleep(600); const bm = await evalJs(ws, `(() => ({ cbs: document.querySelectorAll('.batch-cb').length, delBtn: !!document.querySelector('[data-batch-del]'), cancelBtn: !!document.querySelector('[data-batch-cancel]'), footerStill: !!document.querySelector('.list-footer .list-batchbar') && !!document.querySelector('.list-footer .pager'), rowHasCb: !!document.querySelector('.table tbody .cb-cell input.batch-cb') }))()`); check('批量模式每行有勾选框', bm.cbs > 0 && bm.rowHasCb); check('删除(trash)图标按钮出现', bm.delBtn); check('取消(x)图标按钮出现', bm.cancelBtn); check('批量模式下批量条+分页仍同行', bm.footerStill); // 取消恢复 await evalJs(ws, `document.querySelector('[data-batch-cancel]').click()`); await sleep(500); const back = await evalJs(ws, `({ cbs: document.querySelectorAll('.batch-cb').length, footer: !!document.querySelector('.list-footer') })`); check('取消后恢复常态(无勾选框)', back.cbs === 0); check('取消后 footer 仍在', back.footer); // 顺手验证另外两个列表的 footer for (const [name, hash, sel] of [ ['账号详情视频列表', '#/account/3', '.list-footer'], ['AI写脚本列表', '#/rewrites', '.list-footer'] ]) { await cdp(ws, 'Page.navigate', { url: 'http://localhost:8900/?v=verify' + hash }); await sleep(2000); const r = await evalJs(ws, `(() => { const f = document.querySelector('.list-footer'); return { f: !!f, both: f ? !!f.querySelector('.list-batchbar') && !!f.querySelector('.pager') : false, err: /页面渲染失败|not defined/.test(document.body.innerHTML) }; })()`); check(name + ' .list-footer 存在', r.f); check(name + ' 批量条+分页同行', r.both); check(name + ' 无渲染错误', !r.err); } console.log('--- console 异常:', errors.length ? errors.join('\n') : '无'); if (errors.length) fail++; check('无 console 异常', errors.length === 0); console.log(`\n结果: ${pass} PASS / ${fail} FAIL`); ws2.close(); process.exit(fail ? 1 : 0); } main().catch((e) => { console.error('ERROR:', e.message); process.exit(1); });