Files
juzi/h5/test/regression.js
T
Ubuntu ba3b346fce feat: 报告页 V2.2 检测可视化 + 评分引擎审计 + 真值回归/e2e 闭环
- server.js: face_maps 改为白名单 5 图层完整 dataURL 透传(REPORT_MAPS,按魔数判 mime),marks 坐标随 result 原样透传;实测 return_marks 不支持毛孔/黑头等扩展标记
- skin-score.js: 按需求文档 V2.2 逐字审计修正 9 处(黑眼圈 dark_circle_severity、oil_desc/spot_flag §7.2 拼接、skintone 中文映射、轮廓预警档前缀、痘痘 heavy_note、50+精华全文、综合文案数值格式),契约引用 V2.0→V2.2
- app.js/index.html/css: 报告页 6 分类检测可视化(轮廓=正脸实拍、皱纹=正脸压暗+皱纹增强线、光滑=正脸+色斑/痣/痤疮标记、肤色=色沉图、敏感=红区图、痘痘=正脸+闭口/痤疮标记),数据驱动 VIZ_CAT 配置,优雅降级
- h5/test/: Excel 两组三视图真值回归 84/84(含案例2 AY 列扁平化数据重建 fixture)
- h5/assets/mock/: 真实案例1 报告页 fixture(坐标与照片同比例缩放),?mock=1&screen=report 直渲
- e2e: Chrome 无头闭环 55/55(mock 全流程/直渲/真实 API 降级链路),截图存档 preview/e2e-20260926
- 自测: skin-score 83/83,fixture 冒烟 35/35
2026-09-26 22:35:02 +08:00

129 lines
6.9 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.
/* ═════ 巨子测肤 H5 — 评分引擎真值回归(Excel 三视图表 × SkinScore)═════
* 用法:node h5/test/regression.js
* fixture 重建:python3 h5/test/build_fixtures.py(Excel 或参考 JSON 变更后需重建)
*
* 断言目标:docs/皮肤检测测试结果_三视图_20260926.xlsx「三视图检测汇总」第 3/4 行
* - 六分类 P(I/M/Q/U/Y/AC)、S(J/N/R/V/Z/AD)、档位(K/O/S/W/AA/AE)
* - 三维度 P/S/档位(AG..AO)、下颌线角度(AP/AQ)、产品组别(AT)、精华线(AU)、均分(AV)
* - 6 分类文案(L/P/T/X/AB/AF)、综合文案(AW)、生活方式(AX) 逐字一致
* - 浮点容差 0.05;文案逐字无容差
*
* 引擎与真值不一致时:不改 h5/js/skin-score.js(审计代理负责),差异登记在 KNOWN_DIFFS
* 并输出明细;未登记的新差异 → 退出码 1。登记项若转为通过(引擎已修复)会提示清理。
*/
'use strict';
var fs = require('fs');
var path = require('path');
var SkinScore = require(path.join(__dirname, '..', 'js', 'skin-score.js'));
/* ─── 已知差异登记:键 = 案例.列,值 = 差异原因(逐条说明)。当前为空:V2.2 审计后
引擎与两组真值全部一致;今后出现引擎/真值不一致时在此登记,未登记的新差异 → 退出码 1 ─── */
var KNOWN_DIFFS = {};
/* ─── 断言计数与输出 ─── */
var TOL = 0.05 + 1e-6; // 浮点容差 0.05(1e-6 仅抵二进制表示误差,如 98.65 vs 98.7)
var pass = 0, known = 0, fail = 0;
var failLines = [], knownLines = [];
function diffCtx(exp, act) { // 首个分歧点 ±30 字上下文
var i = 0, n = Math.min(exp.length, act.length);
while (i < n && exp[i] === act[i]) i++;
var lo = Math.max(0, i - 30);
return ' 分歧@' + i + ':\n 真值 …' + exp.slice(lo, i + 30) + '…\n 引擎 …' + act.slice(lo, i + 30) + '…';
}
function okNum(label, actual, expected) {
if (typeof actual === 'number' && typeof expected === 'number' && Math.abs(actual - expected) <= TOL) {
pass++; console.log(' ✓ ' + label + ' = ' + actual + (actual !== expected ? '(真值 ' + expected + ',容差内)' : ''));
} else {
fail++; failLines.push(label + ':引擎 ' + actual + ' ≠ 真值 ' + expected);
console.error(' ✗ ' + label + ':引擎 ' + actual + ' ≠ 真值 ' + expected);
}
}
function okStr(label, actual, expected, knownKey) {
if (actual === expected) { pass++; console.log(' ✓ ' + label); return; }
var reasons = knownKey ? KNOWN_DIFFS[knownKey] : null;
if (reasons) { // 已登记:已知差异,输出明细,不计失败
known++;
knownLines.push(knownKey);
console.log(' Δ ' + label + ' 已知差异(' + reasons.length + ' 条原因):');
for (var i = 0; i < reasons.length; i++) console.log(' 原因' + (i + 1) + ':' + reasons[i]);
console.log(diffCtx(String(expected), String(actual)));
} else {
fail++;
failLines.push(label + ':文案不一致\n' + diffCtx(String(expected), String(actual)));
console.error(' ✗ ' + label + ':文案不一致(未登记差异)\n' + diffCtx(String(expected), String(actual)));
}
}
/* ─── 列号映射:分类(文案列)与维度 ─── */
var CAT_COLS = { contour: 'I/J/K/L', wrinkles: 'M/N/O/P', smooth: 'Q/R/S/T', tone: 'U/V/W/X', sens: 'Y/Z/AA/AB', acne: 'AC/AD/AE/AF' };
var CAT_COPY_COL = { contour: 'L', wrinkles: 'P', smooth: 'T', tone: 'X', sens: 'AB', acne: 'AF' };
var DIM_COLS = { firm: 'AG/AH/AI', vita: 'AJ/AK/AL', stab: 'AM/AN/AO' };
/* ─── 主流程 ─── */
function loadJSON(name) {
return JSON.parse(fs.readFileSync(path.join(__dirname, 'fixtures', name), 'utf8'));
}
function main() {
var truth = loadJSON('truth.json');
var cases = [
{ tag: 'case1', fixture: loadJSON('case1.json'), truth: truth.case1 },
{ tag: 'case2', fixture: loadJSON('case2.json'), truth: truth.case2 }
];
for (var ci = 0; ci < cases.length; ci++) {
var tag = cases[ci].tag, fixture = cases[ci].fixture, tr = cases[ci].truth;
console.log('== ' + tag + '(realAge=' + tr.profile.realAge + ',skinType=' + tr.profile.skinType + ') ==');
var vm = SkinScore.buildViewModel(fixture.result, fixture.side, tr.profile);
// 下颌线角度 AP/AQ:fixture 保真校验(引擎消费角度后由 L 列逐字断言兜底)
okNum(tag + '.AP 左下颌线角度', fixture.side.left.left_jawline_info.left_jawline_angle, tr.jawline.left);
okNum(tag + '.AQ 右下颌线角度', fixture.side.right.right_jawline_info.right_jawline_angle, tr.jawline.right);
okNum(tag + '.E 肌肤年龄', vm.skinAgeRaw, tr.skinAge);
okNum(tag + '.F 总分', vm.total, tr.totalScore);
// 六分类 P/S/档位 + 文案逐字
for (var k in CAT_COLS) {
var cols = CAT_COLS[k].split('/'), c = vm.cats[k], t = tr.cats[k];
okNum(tag + '.' + cols[0] + ' ' + c.name + '·P', c.P, t.P);
okNum(tag + '.' + cols[1] + ' ' + c.name + '·S', c.S, t.S);
okStr(tag + '.' + cols[2] + ' ' + c.name + '·档位', c.level ? c.level + '档' : String(c.level), t.tier);
okStr(tag + '.' + CAT_COPY_COL[k] + ' ' + c.name + '·文案', c.text, t.copy, tag + '.' + CAT_COPY_COL[k]);
}
// 三维度 P/S/档位
for (var d in DIM_COLS) {
var dcols = DIM_COLS[d].split('/'), dd = vm.dims[d], dt = tr.dims[d];
okNum(tag + '.' + dcols[0] + ' ' + dd.name + '·P', dd.P, dt.P);
okNum(tag + '.' + dcols[1] + ' ' + dd.name + '·S', dd.S, dt.S);
okStr(tag + '.' + dcols[2] + ' ' + dd.name + '·档位', dd.level ? dd.level + '档' : String(dd.level), dt.tier);
}
// 组别/精华/均分/综合文案/生活方式
okStr(tag + '.AT 产品组别', String(vm.group), tr.group);
okStr(tag + '.AU 精华产品线', vm.essence.name, tr.essence);
okNum(tag + '.AV 六分类S均分', vm.avgS, tr.avgS);
okStr(tag + '.AW 综合文案', vm.summary, tr.summary, tag + '.AW');
okStr(tag + '.AX 生活方式', vm.lifestyle, tr.lifestyle);
}
// 登记项清理提示:已知差异键本轮全部命中为差异;若某键实际通过则提示移除登记
var knownHit = {}, i;
for (i = 0; i < knownLines.length; i++) knownHit[knownLines[i]] = 1;
for (var key in KNOWN_DIFFS) {
if (!knownHit[key]) console.log(' ◆ 提示:登记项 ' + key + ' 本轮通过(引擎或真值已变化),可从 KNOWN_DIFFS 移除');
}
console.log('----------------------------------------');
console.log('断言 ' + (pass + known + fail) + ' 条:通过 ' + pass + ',已知差异 ' + known + ',失败 ' + fail);
if (knownLines.length) console.log('已知差异明细:' + knownLines.join('、') + '(原因见上,待 skin-score.js 审计代理处理)');
if (failLines.length) {
console.error('失败明细:');
for (i = 0; i < failLines.length; i++) console.error(' ✗ ' + failLines[i]);
}
if (fail > 0) process.exit(1);
console.log(known > 0 ? '结论:全绿(仅剩已登记、已说明原因的已知差异)' : '结论:全绿');
}
main();