Files
mcn-short-video/project/短视频脚本创作/V1.0/mcn-work-shop/cdp-verify-batch.mjs
T
maogeigei b049cca766 feat(工作台): 三列表(账号/视频/AI写脚本)批量操作——左下角图标按钮+勾选批量删除
- 后端: dsh-data.js batchDelete(kind,ids)——accounts 软删(del_flag=1)+连坐清 persona/analysis/videos/source/analysis/rewrite_log; videos|rewrites 删视频行+关联(video_id 与 aweme_id 双键,兼容 video_id=null 历史行); server.js POST /api/dsh/batch-delete(kind 校验+空 ids 幂等)
- 前端: data-pages.js batchBarHtml/bindBatchBar + 三列表勾选列+左下角批量操作条; 图标版——app.js 新增 check-square/trash 图标, 批量模式图标右侧出 trash/x+全选+计数; style.css 批量样式+--danger
- 修复 P0: 账号连坐删除漏删 account_videos 行(孤儿残留), 已补并复验
- 验证: API 实测(删测试账号/视频连坐全清+非法kind+空ids) + CDP UI 33项PASS + 端到端删除 PASS(cdp-verify-batch.mjs / cdp-verify-batch-e2e.mjs)
2026-09-01 14:31:46 +08:00

136 lines
6.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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); });