/* fog-clip.js 的单元测试(node 直接跑,无依赖)。 * 用法:node internal/web/static/js/fog-clip.test.js * * 这里不比对字符串,而是把生成的 path 数据**解析回圆弧**再验几何: * 拼错一个数字(半径、圆心、绕向)都能被抓住,而「看起来差不多」的字符串比对做不到。 */ const fs = require('fs'); const path = require('path'); const vm = require('vm'); const src = fs.readFileSync(path.join(__dirname, 'fog-clip.js'), 'utf8'); const mod = {}; const sandbox = { window: {}, module: mod }; vm.createContext(sandbox); vm.runInContext(src, sandbox); const api = sandbox.window.rondFogClip; let pass = 0, fail = 0; function ok(cond, label, extra) { if (cond) { pass++; console.log(' ok ' + label); } else { fail++; console.log(' FAIL ' + label + (extra ? '\n ' + extra : '')); } } // 把 path("M…a…a…Z M…a…a…Z") 解析成 [{cx, cy, r, sweep}] // 只认本模块生成的那种形式:M x y 后跟两段 `a r r 0 1 s dx dy` function parsePath(v) { const m = /^path\("(.*)"\)$/.exec(v); if (!m) return null; const out = []; const subs = m[1].split('M').slice(1); for (const s of subs) { const t = s.match(/^([-\d.]+) ([-\d.]+)a([-\d.]+) ([-\d.]+) 0 1 ([01]) ([-\d.]+) 0a([-\d.]+) ([-\d.]+) 0 1 ([01]) ([-\d.]+) 0Z$/); if (!t) return null; const x0 = +t[1], y = +t[2], r1 = +t[3], r2 = +t[4], sw1 = +t[5], dx1 = +t[6]; const r3 = +t[7], r4 = +t[8], sw2 = +t[9], dx2 = +t[10]; if (r1 !== r2 || r3 !== r4 || r1 !== r3) return null; // 两个半径必须一致 if (Math.abs(dx1 - 2 * r1) > 0.05 || Math.abs(dx2 + 2 * r1) > 0.05) return null; out.push({ cx: x0 + r1, cy: y, r: r1, sweep: [sw1, sw2] }); } return out; } function near(a, b) { return Math.abs(a - b) < 0.15; } console.log('=== 一个圆:几何反解必须与输入一致 ==='); (function () { const v = api.pathFor([{ x: 100, y: 200, r: 50 }]); const cs = parsePath(v); ok(cs && cs.length === 1, '应解析出 1 个子路径', v); if (!cs || !cs.length) return; ok(near(cs[0].cx, 100) && near(cs[0].cy, 200) && near(cs[0].r, 50), '圆心半径应为 (100,200) r=50', JSON.stringify(cs[0])); })(); console.log('=== 多个圆:每个圆一段子路径,且同绕向(并集的前提)==='); (function () { const input = [{ x: 100, y: 200, r: 50 }, { x: 300, y: 400, r: 25 }, { x: 140, y: 210, r: 50 }]; const v = api.pathFor(input); const cs = parsePath(v); ok(cs && cs.length === 3, '应解析出 3 个子路径', v); if (!cs || cs.length !== 3) return; input.forEach((c, i) => { ok(near(cs[i].cx, c.x) && near(cs[i].cy, c.y) && near(cs[i].r, c.r), '第 ' + (i + 1) + ' 个圆与原输入一致', JSON.stringify(cs[i]) + ' vs ' + JSON.stringify(c)); }); // 绕向必须全都一样:两段圆弧 sweep 相同。若某个圆反向,重叠区会被 nonzero 规则抵消成洞。 const allSame = cs.every(c => c.sweep[0] === cs[0].sweep[0] && c.sweep[1] === cs[0].sweep[1]); ok(allSame, '所有子路径绕向一致(否则重叠处会被挖成洞)', JSON.stringify(cs.map(c => c.sweep))); // 第 1、3 个圆是重叠的,仍应各是一段完整子路径(不合并、不挖洞) ok(cs.length === 3, '重叠的两个圆不会被合并或抵消'); })(); console.log('=== 退化输入 ==='); (function () { ok(api.pathFor([]) === '', '空数组 → 空串(调用方据此不改样式)'); ok(api.pathFor(null) === '', 'null → 空串'); ok(api.pathFor(undefined) === '', 'undefined → 空串'); // 半径 0 会让路径退化,必须夹到 >= 1px const v = api.pathFor([{ x: 10, y: 10, r: 0 }]); const cs = parsePath(v); ok(cs && cs.length === 1 && cs[0].r >= 1, '半径 0 被夹到 >= 1px', v); // 负半径同理(拖拽时可能算出负数) const v2 = api.pathFor([{ x: 10, y: 10, r: -5 }]); const cs2 = parsePath(v2); ok(cs2 && cs2.length === 1 && cs2[0].r >= 1, '负半径被夹到 >= 1px', v2); })(); console.log('=== 大量圆:个数与顺序不能错位 ==='); (function () { const input = []; for (let i = 0; i < 20; i++) input.push({ x: i * 10, y: 500 - i * 3, r: 4 + i }); const cs = parsePath(api.pathFor(input)); ok(cs && cs.length === 20, '20 个圆 → 20 个子路径'); if (!cs || cs.length !== 20) return; let bad = -1; input.forEach((c, i) => { if (bad < 0 && !(near(cs[i].cx, c.x) && near(cs[i].cy, c.y) && near(cs[i].r, c.r))) bad = i; }); ok(bad < 0, '20 个圆的顺序与数值都对应上' + (bad >= 0 ? '(第 ' + bad + ' 个错位)' : '')); })(); console.log('\n=== 汇总 ==='); console.log(' pass=' + pass + ' fail=' + fail); process.exit(fail === 0 ? 0 : 1);