/* ============================================================ * test_travel_citytour_export.js —— 游山玩水 · 单文件 HTML 导出器单元测试 * * 测什么:tc_export.js 的 build() 是纯字符串拼装(不碰 DOM), * 所以可以在 Node 里直接跑,把导出的 HTML 当成字符串做结构断言。 * * 覆盖: * 1. report_spec 为空时必须抛错(否则又会出现「静默留白」) * 2. 12 个区块、hero、目录、卡片数、表格 pick_id、清单、地图 SVG、页脚 都要在 * 3. 不得出现 undefined / [object Object] / / 内 const SKILL_ID = 'sh_2d_car_hike' const SKILL_PUBLIC = path.join(ROOT, 'public', 'tools', 'thought_lab', 'labs', 'travel_citytour', 'skills', SKILL_ID) const SKILL_SERVER = path.join(ROOT, 'src', 'server', 'thought_lab', 'labs', 'travel_citytour', 'skills', SKILL_ID) const readJson = (p) => JSON.parse(fs.readFileSync(p, 'utf-8')) const exporter = require(path.join(SKILL_PUBLIC, 'tc_export.js')) let pass = 0 const fails = [] function ok(name, cond, extra) { if (cond) { pass++; console.log(' ✔ ' + name) } else { fails.push(name + (extra ? ' → ' + extra : '')); console.log(' ✘ ' + name + (extra ? ' → ' + extra : '')) } } function count(hay, needle) { let n = 0, i = 0 while (true) { const j = hay.indexOf(needle, i) if (j < 0) break n++; i = j + needle.length } return n } const CSS = fs.readFileSync(path.join(SKILL_PUBLIC, 'tc_export.css'), 'utf-8') /* ---------- 1. 空 spec 必须抛错 ---------- */ console.log('\n[1] 空 report_spec') let threw = false try { exporter.build({ summary: { title: 'x' } }, {}, { css: CSS }) } catch (e) { threw = true } ok('sections 缺失时主动抛错(不静默留白)', threw) /* ---------- 2. 真实结果:结构完整性 ---------- */ console.log('\n[2] 真实生成结果(last_result.json)') const env = readJson(path.join(SKILL_SERVER, 'data', 'last_result.json')) const spec = readJson(path.join(SKILL_SERVER, 'report_spec.json')) const payload = env.payload const html = exporter.build(payload, spec, { css: CSS, meta: env.meta, exportedAt: '2026-09-29 15:30:00' }) ok('产出完整 HTML 文档', html.startsWith('') && html.trim().endsWith('')) ok('lang=zh-CN', html.indexOf('') > 0) ok('标题取自 payload.summary.title', html.indexOf('' + payload.summary.title + ' · 行程报告') > 0) ok('样式已内联(