Files
tripsense/tools/verify-route-map-followup.cjs
T
tamakyi a7fb670181 修正追加轮验收脚本的截图取景
#planExtra 默认 hidden 导致 #routeMap 尺寸 0x0、截图落到页面顶部; 显式移除 hidden 并校验 rect。降级方位示意图会画出 142km 外的出发地, 把市区内 9 个点压成一坨, 验收造数据只给地名不给坐标, 让 SVG 按目的地跨度取景。
2026-11-06 10:00:14 +08:00

255 lines
10 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.
'use strict';
/**
* 端到端验收:AI 追加轮之后「行程路线示意」不能只剩一个点。
*
* 用真实无头 Chrome 打开站点,注入一份 mock 分析结果与两版方案正文,
* 直接调用页面里的 refreshRouteFromText / drawRouteMap,读真实 DOM:
* - 初版:多点点
* - 复现修复前(nearby 清空、不继承):只剩 1 个
* - 修复后(nearby 沿用 + 继承上一版坐标):恢复多点
* - 切换回第 1 版:路线图跟着版本重绘
*
* 用法:node tools/verify-route-map-followup.cjs
*/
const { spawn } = require('child_process');
const fs = require('fs');
const path = require('path');
const os = require('os');
const OUT = path.join(__dirname, '..', 'preview');
const CDP_PORT = 9361;
const CHROME = 'C:/Program Files/Google/Chrome/Application/chrome.exe';
function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
class CDP {
constructor(ws) { this.ws = ws; this.id = 0; this.waits = new Map(); }
static async connect(url) {
const ws = new WebSocket(url);
await new Promise((res, rej) => { ws.onopen = res; ws.onerror = rej; });
const c = new CDP(ws);
ws.onmessage = (e) => {
const m = JSON.parse(e.data);
if (m.id && c.waits.has(m.id)) { c.waits.get(m.id)(m); c.waits.delete(m.id); }
};
return c;
}
send(method, params = {}, sessionId) {
const id = ++this.id;
return new Promise((res) => {
this.waits.set(id, res);
this.ws.send(JSON.stringify({ id, method, params, sessionId }));
});
}
}
const LAT = 24.32;
const LON = 109.42;
const pt = (name, i) => ({
name,
lat: +(LAT + (i % 5) * 0.004).toFixed(4),
lon: +(LON + Math.floor(i / 5) * 0.004).toFixed(4),
type: '地点',
});
const SIGHT_NAMES = ['龙潭公园', '马鞍山公园', '窑埠古镇', '柳侯公园', '程阳八寨景区'];
const FOOD_NAMES = ['螺蛳粉老店', '青云菜市', '五星步行街', '水上公交码头', '龙潭公园餐厅'];
const NEARBY = {
area: '柳州市',
radiusKm: 8,
sights: SIGHT_NAMES.map((n, i) => pt(n, i)),
food: FOOD_NAMES.map((n, i) => pt(n, i + 5)),
};
// 不给出发地坐标:降级方位示意图会画出 142km 外的起点,把市区内 9 个目的地
// 压成一坨、标签全叠在一起。这是既有的取景行为,与本次修复无关;验收截图专注
// 「追加轮是否仍有多个点」,所以只给地名,让 SVG 按目的地跨度取景。
const ANALYSIS = {
origin: { name: '河池市金城江区' },
candidates: [{ id: 'd1', name: '柳州市', lat: LAT, lon: LON, score: 9.1 }],
timeRange: { start: '2026-11-20', end: '2026-11-21', days: 2 },
dataNotes: [],
};
const V1 = `# 柳州两日行程
## 方案总览
两天都在柳州市区与近郊,节奏适中。
## 逐日安排
### 第 1 天
上午去 **龙潭公园**、**马鞍山公园**,中午在 **螺蛳粉老店** 吃饭;
下午逛 **窑埠古镇**、**柳侯公园**,傍晚到 **五星步行街** 与 **青云菜市**。
### 第 2 天
去 **程阳八寨景区**、**水上公交码头**,晚上在 **龙潭公园餐厅** 收尾。
`;
const V2 = `# 柳州两日行程(第 2 版:少走路)
## 方案总览
按「少走路」重排:第 1 天压缩成市区一圈,第 2 天只留一个远点。
## 逐日安排
### 第 1 天
上午去龙潭公园、马鞍山公园,中午在螺蛳粉老店吃饭;
下午逛窑埠古镇与柳侯公园,傍晚到五星步行街。
### 第 2 天
只保留程阳八寨景区,删掉水上公交码头与青云菜市(走路太多)。
`;
let server = null;
let chrome = null;
(async () => {
if (!fs.existsSync(CHROME)) { console.error('找不到 Chrome:' + CHROME); process.exit(1); }
fs.mkdirSync(OUT, { recursive: true });
// ---- 服务端(端口可能被占用自动顺延,从启动日志里读真实端口)
server = spawn(process.execPath, ['server.js'], {
cwd: path.join(__dirname, '..'),
stdio: ['ignore', 'pipe', 'pipe'],
});
let banner = '';
server.stdout.on('data', (d) => { banner += d.toString(); });
server.stderr.on('data', (d) => { banner += d.toString(); });
await sleep(2800);
const m = banner.match(/http:\/\/127\.0\.0\.1:(\d+)/);
if (!m) { console.error('服务端未打印监听地址:' + banner); process.exit(1); }
const base = `http://127.0.0.1:${m[1]}`;
console.log('服务端 =', base);
// ---- 无头 Chrome + CDP
chrome = spawn(CHROME, [
'--headless=new', '--disable-gpu', '--hide-scrollbars',
`--remote-debugging-port=${CDP_PORT}`,
'--user-data-dir=' + path.join(os.tmpdir(), 'tripverifyroute' + Date.now()),
'about:blank',
], { stdio: 'ignore' });
let wsUrl = null;
for (let i = 0; i < 30 && !wsUrl; i += 1) {
await sleep(500);
try {
const res = await fetch(`http://127.0.0.1:${CDP_PORT}/json/version`);
wsUrl = (await res.json()).webSocketDebuggerUrl;
} catch (e) { /* 还没起来 */ }
}
if (!wsUrl) { console.error('Chrome 未启动'); process.exit(1); }
const browser = await CDP.connect(wsUrl);
const { result: { targetId } } = await browser.send('Target.createTarget', { url: 'about:blank' });
const { result: { sessionId } } = await browser.send('Target.attachToTarget', { targetId, flatten: true });
const S = (method, params) => browser.send(method, params, sessionId);
await S('Page.enable');
await S('Runtime.enable');
await S('Emulation.setDeviceMetricsOverride', { width: 1280, height: 1400, deviceScaleFactor: 1, mobile: false });
await S('Page.navigate', { url: base });
await sleep(2500);
const evalJs = async (expr) => {
const r = await S('Runtime.evaluate', { expression: expr, awaitPromise: true, returnByValue: true });
if (r.result?.exceptionDetails) throw new Error(JSON.stringify(r.result.exceptionDetails).slice(0, 500));
return r.result?.result?.value;
};
// 初始化 mock 状态;强制走「方位示意」分支,保证 SVG 里能数到编号点
await evalJs(`(() => {
state.analysis = ${JSON.stringify(ANALYSIS)};
state.selected = new Set(['d1']);
state.nearby = ${JSON.stringify(NEARBY)};
state.ai.amapKey = '';
if (!state.config) state.config = {};
state.config.features = { ...(state.config.features || {}), amapRoad: false };
// #planExtra 默认 hidden(runPlan 成功才移除),不拆掉它 #routeMap 尺寸是 0×0,
// 截图会落到页面顶部。这里显式显示,保证截到真实的路线图。
const pe = document.querySelector('#planExtra');
if (pe) pe.classList.remove('hidden');
if (typeof gotoStep === 'function') gotoStep(3);
window.__V1 = ${JSON.stringify(V1)};
window.__V2 = ${JSON.stringify(V2)};
window.__ver1 = { n: 1, text: window.__V1, routePoints: null, routeMissing: [] };
window.__ver2 = { n: 2, text: window.__V2, routePoints: null, routeMissing: [] };
return true;
})()`);
const readMap = `(() => ({
points: (state.routePoints || []).length,
dots: document.querySelectorAll('#routeMap .rm-idx').length,
note: document.querySelector('#routeNote')?.textContent || '',
hasSvg: !!document.querySelector('#routeMap .route-svg'),
}))()`;
// 1) 初版
const first = await evalJs(`(async () => {
await refreshRouteFromText(window.__V1, null, window.__ver1);
await drawRouteMap();
return ${readMap};
})()`);
console.log('初版 →', first);
// 2) 复现修复前:追加轮把 nearby 清空、不继承上一版坐标
const broken = await evalJs(`(async () => {
state.nearby = null;
await refreshRouteFromText(window.__V2, null, null);
await drawRouteMap();
return ${readMap};
})()`);
console.log('修复前追加轮 →', broken);
// 3) 修复后:nearby 沿用 + 继承上一版坐标
const fixed = await evalJs(`(async () => {
state.nearby = ${JSON.stringify(NEARBY)};
await refreshRouteFromText(window.__V2, window.__ver1.routePoints, window.__ver2);
await drawRouteMap();
return ${readMap};
})()`);
console.log('修复后追加轮 →', fixed);
// 4) 通过真实下拉事件切回第 1 版,路线图应跟着重绘
await evalJs(`(() => {
state.planVersions = [window.__ver1, window.__ver2];
state.viewing = 1;
renderVersionBar();
const sel = document.querySelector('#versionSelect');
sel.value = 'v0';
sel.dispatchEvent(new Event('change', { bubbles: true }));
return true;
})()`);
await sleep(400);
const switched = await evalJs(readMap);
console.log('切回第 1 版 →', switched);
// 截图:路线图区域
const box = await evalJs(`(() => {
const r = document.querySelector('#routeMap').getBoundingClientRect();
return { x: r.x + scrollX, y: r.y + scrollY, width: Math.max(r.width, 640), height: Math.max(r.height, 380), rectW: r.width, rectH: r.height };
})()`);
console.log('路线图区域 =', box);
if (!box.rectW || !box.rectH) throw new Error('#routeMap 尺寸为 0,可能仍在隐藏容器里');
const shot = await S('Page.captureScreenshot', {
format: 'png', captureBeyondViewport: true,
clip: { ...box, scale: 1 },
});
const shotPath = path.join(OUT, 'preview-route-followup.png');
fs.writeFileSync(shotPath, Buffer.from(shot.result.data, 'base64'));
console.log('截图已保存 preview/preview-route-followup.png');
const fails = [];
if (first.dots < 2) fails.push('初版路线图点数不足');
if (broken.dots !== 1) fails.push('未能复现「只剩一个点」(期望 1,实际 ' + broken.dots + ')');
if (fixed.dots < 2) fails.push('修复后追加轮仍只有一个点');
if (fixed.points <= broken.points) fails.push('修复后路线点数没有增加');
if (switched.dots < 2) fails.push('切换版本后路线图未重绘');
if (fails.length) {
console.error('\n失败:' + fails.join(';'));
process.exitCode = 1;
} else {
console.log('\n✅ 端到端验收通过:追加轮仍有多个点,版本切换会重绘');
}
})().catch((e) => {
console.error(e.stack || e.message);
process.exitCode = 1;
}).finally(() => {
if (chrome) { try { chrome.kill(); } catch (e) { /* ignore */ } }
if (server) { try { server.kill(); } catch (e) { /* ignore */ } }
});