Files
digital-me-history/digital-me-backend/scripts/journey.mjs
T
admin 0593ff7e62
nginx serve /uploads direct (audio speed) / patch-nginx (push) Failing after 1m34s
数字我 Digital Me:全量入库(大文件走 Git LFS)
2026-10-10 16:24:43 +08:00

99 lines
6.6 KiB
JavaScript

// 端到端用户走查:进站→逛大师墙→大师详情→和大师聊→文旅→路线→报名→注册→养分身→登出。
// 每步截图 + 抓控制台报错/失败请求。用法:node scripts/journey.mjs <shotsDir>
// 前置: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); });