// 模拟点击测试:俊希「AI创作」界面链路(#/accounts → AI创作弹窗 → AI 选题生成) // 覆盖测试用例 TC-09 的界面交互环节(真实模拟用户点击,非仅文字步骤) // 2026-09-01 更新:按钮/弹窗标题已由「AI写脚本」统一改名为「AI创作」(cdp-test 断言同步) // // 前置: // 1. 工作台服务运行中(node server.js,默认端口 8900;可传参指定) // 2. 浏览器 9223 调试实例已启动(browser-harness 调试实例) // 3. 账号列表(hot_accounts)含「俊希」 // 用法: // node cdp-test-junxi.mjs # 只验证到「选题生成」,不触发创作任务(安全模式) // node cdp-test-junxi.mjs --confirm # 选题生成后继续点「确认执行」,真实触发创作任务 // node cdp-test-junxi.mjs --port 8900 # 指定工作台端口(默认 8900) // 说明:脚本在 9223 实例新开标签页操作,不干扰用户已打开的页面;结束后自动关闭新开标签页。 const argPort = process.argv.indexOf('--port'); const PORT = argPort > -1 && process.argv[argPort + 1] ? process.argv[argPort + 1] : '8900'; const CONFIRM = process.argv.includes('--confirm'); const BASE = 'http://localhost:' + PORT; // 1) 连接 9223 调试实例 const list = await fetch('http://localhost:9223/json').then((r) => r.json()); const page = list.find((t) => t.type === 'page'); if (!page) { console.log('ERR: 9223 无可用页面,请先启动 browser-harness 调试实例'); process.exit(1); } const ws = new WebSocket(page.webSocketDebuggerUrl); let id = 0; const pending = new Map(); const send = (method, params = {}) => new Promise((res, rej) => { const i = ++id; pending.set(i, { res, rej }); ws.send(JSON.stringify({ id: i, method, params })); }); ws.onmessage = (e) => { const m = JSON.parse(e.data); if (m.id && pending.has(m.id)) { const p = pending.get(m.id); pending.delete(m.id); m.error ? p.rej(new Error(m.error.message)) : p.res(m.result); } }; await new Promise((r) => (ws.onopen = r)); const ev = async (expr) => { const r = await send('Runtime.evaluate', { expression: expr, returnByValue: true, awaitPromise: true }); if (r.exceptionDetails) return { __err: (r.exceptionDetails.exception?.description || r.exceptionDetails.text).slice(0, 300) }; return r.result.value; }; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); await send('Page.enable'); await send('Runtime.enable'); // 2) 新开标签页打开账号列表(不干扰现有页面) const { targetId } = await send('Target.createTarget', { url: BASE + '/#/accounts' }); await sleep(2500); let targetPage = list2 => list2.find((t) => t.type === 'page' && t.url.includes('localhost:' + PORT) && t.id === targetId); let pages = await fetch('http://localhost:9223/json').then((r) => r.json()); let tp = targetPage(pages); if (!tp) { console.log('ERR: 新开标签页未找到 ' + PORT + ' 页面'); process.exit(1); } // 切换 WS 到新页面 const ws2 = new WebSocket(tp.webSocketDebuggerUrl); let id2 = 0; const pending2 = new Map(); const send2 = (method, params = {}) => new Promise((res, rej) => { const i = ++id2; pending2.set(i, { res, rej }); ws2.send(JSON.stringify({ id: i, method, params })); }); ws2.onmessage = (e) => { const m = JSON.parse(e.data); if (m.id && pending2.has(m.id)) { const p = pending2.get(m.id); pending2.delete(m.id); m.error ? p.rej(new Error(m.error.message)) : p.res(m.result); } }; await new Promise((r) => (ws2.onopen = r)); const ev2 = async (expr) => { const r = await send2('Runtime.evaluate', { expression: expr, returnByValue: true, awaitPromise: true }); if (r.exceptionDetails) return { __err: (r.exceptionDetails.exception?.description || r.exceptionDetails.text).slice(0, 300) }; return r.result.value; }; const sleep2 = (ms) => new Promise((r) => setTimeout(r, ms)); await send2('Page.enable'); await send2('Runtime.enable'); await send2('Page.reload', { ignoreCache: true }); await sleep2(3500); // 3) 账号列表页:定位俊希行「AI写脚本」按钮 const findBtn = await ev2(`(() => { const btns = [...document.querySelectorAll('[data-create]')]; const b = btns.find((x) => x.dataset.create === '俊希'); if (!b) return { found: false, total: btns.length, names: btns.slice(0, 20).map((x) => x.dataset.create) }; return { found: true, accId: b.dataset.accId, txt: b.textContent.trim() }; })()`); console.log('【Step1 账号列表定位俊希】', JSON.stringify(findBtn)); if (!findBtn.found) { console.log('FAIL: 账号列表未找到俊希「AI创作」按钮(若账号未关注,请先到周榜/账号列表关注俊希)'); await send('Target.closeTarget', { targetId }); ws.close(); ws2.close(); process.exit(1); } // 4) 模拟点击「AI创作」→ 轮询等待弹窗出现(confirmAiScript 先 await persona 接口再建弹窗,需等待) await ev2(`(() => { const b = [...document.querySelectorAll('[data-create]')].find((x) => x.dataset.create === '俊希'); b.click(); return true; })()`); const modal = await ev2(`(async () => { for (let i = 0; i < 24; i++) { // 只认非 hidden 且含 #aiTopicGen 的弹窗(index.html 有 hidden 的 #settingsModal 干扰) const m = [...document.querySelectorAll('.modal-mask')].find((x) => !x.hidden && x.querySelector('#aiTopicGen')); if (m) { return { open: true, title: m.querySelector('h3')?.textContent.trim() || '', desc: m.querySelector('.dim')?.textContent.trim() || '', hasTopicInput: !!m.querySelector('#aiTopicInput'), hasGenBtn: !!m.querySelector('#aiTopicGen'), hasOk: !!m.querySelector('[data-ok]') }; } await new Promise((r) => setTimeout(r, 500)); } return { open: false }; })()`); console.log('【Step2 AI创作弹窗】', JSON.stringify(modal, null, 1)); const descOk = modal.open && modal.title.includes('AI创作') && modal.hasTopicInput && modal.hasGenBtn; const branch = modal.open && (modal.desc || '').includes('账号设定'); // 09-01 修正:有 persona 时 desc=「基于当前账号设定…」;无 persona 时才提示先分析账号设定 const firstTime = modal.open && /先.*账号设定|暂无账号设定|尚未.*设定/.test(modal.desc || ''); console.log(firstTime ? ' → 文案分支:先进行账号设定分析(首次场景)' : branch ? ' → 文案分支:基于当前账号设定(有设定场景)' : ' → 文案分支:未识别(需人工确认)'); // 5) 模拟点击「AI」选题按钮 → 轮询选题输入框(Dify 约 50s) await ev2(`(() => { const g = document.querySelector('#aiTopicGen'); g.click(); return true; })()`); console.log('【Step3 AI 选题生成中…(最长等待 100s)】'); let topics = ''; const t0 = Date.now(); while (Date.now() - t0 < 100000) { await sleep2(3000); topics = (await ev2(`(() => document.querySelector('#aiTopicInput')?.value || '')()`)) || ''; if (topics && !topics.includes('(未生成内容)')) break; } console.log('【Step4 选题结果】'); console.log(topics || '(超时未返回)'); const topicOk = topics.split('\n').filter((l) => l.trim()).length >= 3 || /选题|方向/.test(topics); // 6) --confirm 模式:确认执行触发创作任务 if (CONFIRM) { console.log('【Step5 --confirm】点击确认执行,触发创作任务…'); await ev2(`(() => { document.querySelector('[data-ok]').click(); return true; })()`); await sleep2(1500); const toast = await ev2(`(() => document.querySelector('.toast')?.textContent || '')()`); console.log(' toast:', toast || '(未捕获)'); } else { console.log('【Step5 安全模式】未触发创作任务(加 --confirm 参数可真实触发)'); await ev2(`(() => { document.querySelector('[data-cancel]')?.click(); return true; })()`); } // 7) 汇总 console.log('\n===== 测试结果 ====='); console.log('Step1 定位俊希按钮:', findBtn.found ? 'PASS' : 'FAIL'); console.log('Step2 弹窗结构:', descOk ? 'PASS' : 'FAIL'); console.log('Step3-4 AI 选题:', topicOk ? 'PASS' : 'CHECK'); console.log(CONFIRM ? 'Step5 任务触发: 已提交(到左侧会话栏查看)' : 'Step5 任务触发: 未触发(安全模式)'); // 关闭新开标签页 await send('Target.closeTarget', { targetId }); ws.close(); ws2.close(); process.exit(descOk && findBtn.found ? 0 : 1);