// 端到端用户走查:进站→逛大师墙→大师详情→和大师聊→文旅→路线→报名→注册→养分身→登出。 // 每步截图 + 抓控制台报错/失败请求。用法:node scripts/journey.mjs // 前置:chrome 带 --remote-debugging-port=9222 已打开 http://localhost:5173/ import WebSocket from 'ws'; import { writeFileSync } from 'node:fs'; const OUT = process.argv[2]; const BASE = 'http://localhost:5173'; const wait = (ms) => new Promise((r) => setTimeout(r, ms)); const uniqEmail = `walk${Date.now()}@test.com`; async function targetWs() { for (let i = 0; i < 30; i++) { const ts = await (await fetch('http://localhost:9222/json/list')).json(); const t = ts.find((x) => x.type === 'page' && /localhost:5173/.test(x.url)); if (t) return t.webSocketDebuggerUrl; await wait(300); } throw new Error('no 5173 target'); } const report = []; let curStep = 'init'; const errs = []; // {step, kind, detail} async function main() { const ws = await targetWs(); const sock = new WebSocket(ws, { perMessageDeflate: false, maxPayload: 64 * 1024 * 1024 }); let id = 0; const pending = new Map(); const send = (method, params = {}) => new Promise((resolve) => { const mid = ++id; pending.set(mid, resolve); sock.send(JSON.stringify({ id: mid, method, params })); }); await new Promise((r) => sock.on('open', r)); sock.on('message', (buf) => { const m = JSON.parse(buf.toString()); if (m.id && pending.has(m.id)) { pending.get(m.id)(m.result); pending.delete(m.id); return; } if (m.method === 'Runtime.consoleAPICalled' && (m.params.type === 'error')) { const txt = (m.params.args || []).map((a) => a.value || a.description || '').join(' ').slice(0, 160); if (txt) errs.push({ step: curStep, kind: 'console.error', detail: txt }); } if (m.method === 'Runtime.exceptionThrown') { errs.push({ step: curStep, kind: 'exception', detail: (m.params.exceptionDetails?.exception?.description || m.params.exceptionDetails?.text || '').slice(0, 160) }); } if (m.method === 'Network.responseReceived') { const st = m.params.response?.status, u = m.params.response?.url || ''; if (st >= 400 && /\/api\//.test(u)) errs.push({ step: curStep, kind: 'http ' + st, detail: u.replace('http://localhost:3701', '') }); } }); await send('Page.enable'); await send('Runtime.enable'); await send('Network.enable'); const evalJs = async (expr) => (await send('Runtime.evaluate', { expression: expr, returnByValue: true }))?.result?.value; const nav = async (path) => { await send('Page.navigate', { url: BASE + path }); await wait(2600); }; const clickText = (txt) => evalJs(`(()=>{const t=${JSON.stringify(txt)}.replace(/\\s+/g,'');const el=[...document.querySelectorAll('a,button,[role=button],label')].find(e=>(e.textContent||'').replace(/\\s+/g,'').includes(t));if(el){el.click();return true}return false})()`); const clickSel = (sel) => evalJs(`(()=>{const el=document.querySelector(${JSON.stringify(sel)});if(el){el.click();return true}return false})()`); const fill = (ph, v) => evalJs(`(()=>{const el=[...document.querySelectorAll('input,textarea')].find(e=>(e.placeholder||'').includes(${JSON.stringify(ph)}));if(el){el.value=${JSON.stringify(v)};el.dispatchEvent(new Event('input',{bubbles:true}));el.dispatchEvent(new Event('change',{bubbles:true}));return true}return false})()`); const info = () => evalJs(`JSON.stringify({path:location.pathname, h1:document.querySelector('h1')?.textContent?.trim().slice(0,24), authed:!!localStorage.getItem('dm_token'), nav:[...document.querySelectorAll('header a,header button')].map(e=>e.textContent.trim()).filter(Boolean).slice(0,8)})`); const shot = async (name) => { const { data } = await send('Page.captureScreenshot', { format: 'png' }); writeFileSync(`${OUT}/${name}.png`, Buffer.from(data, 'base64')); }; const step = async (label, fn) => { curStep = label; const before = errs.length; await fn(); const i = JSON.parse(await info() || '{}'); report.push({ label, ...i, newErrs: errs.length - before }); }; // ── 走查 ── await step('01-进站落广场', async () => { await nav('/'); await shot('01-plaza'); }); await step('02-点大师卡', async () => { await clickSel('a[href^="/masters/"]'); await wait(2200); await shot('02-master'); }); await step('03-和大师聊(点)', async () => { await clickSel('a[href^="/me/"]'); await wait(2500); await shot('03-chat-open'); }); await step('04-输入框可用(本地无key不真发)', async () => { await fill('说点什么', '你好,介绍下你自己'); await wait(600); await shot('04-chat-typed'); }); await step('05-返回并进大师文旅', async () => { await nav('/tours'); await shot('05-tours'); }); await step('06-看路线详情', async () => { await clickSel('a[href^="/tours/"]'); await wait(2400); await shot('06-tour-detail'); }); await step('07-填报名交定金', async () => { await fill('您的称呼', '走查客'); await fill('手机号', '13800008888'); await clickText('报名·交预定金锁位'); await wait(400); await clickText('报名并支付预定金'); await wait(2800); await shot('07-booked'); }); await step('08-去登录页', async () => { await nav('/login'); await shot('08-login'); }); await step('09-注册新号', async () => { await clickText('去注册'); await wait(600); await fill('you@example.com', uniqEmail); await fill('至少 6 位', 'walk123456'); await evalJs(`(()=>{const el=[...document.querySelectorAll('input')].find(e=>/昵称|名字|称呼|name/i.test(e.placeholder||''));if(el){el.value='走查用户';el.dispatchEvent(new Event('input',{bubbles:true}))}})()`); await clickText('注册'); await wait(3000); await shot('09-after-register'); }); await step('10-养分身/工作台', async () => { await clickText('养我的分身'); await wait(2600); await shot('10-dashboard'); }); await step('11-我的分身导航', async () => { await clickText('我的分身'); await wait(2200); await shot('11-mine'); }); await step('12-登出', async () => { await clickText('退出'); await wait(2200); await shot('12-logout'); }); writeFileSync(`${OUT}/report.json`, JSON.stringify({ report, errs }, null, 2)); console.log('=== 走查步骤 ==='); for (const r of report) console.log(`${r.label} → path:${r.path} authed:${r.authed} h1:"${r.h1 || ''}" 本步报错:${r.newErrs}`); console.log('\n=== 报错/失败请求汇总 ==='); if (!errs.length) console.log('(无)'); for (const e of errs) console.log(`[${e.step}] ${e.kind}: ${e.detail}`); sock.close(); process.exit(0); } main().catch((e) => { console.error(e); process.exit(1); });