Files
juzi/h5/test/regression.js
T
Ubuntu 6c634e3a66 feat: 真机反馈 11 项(下拉层级/侧脸蒙版/报告改版/护理建议 V2.3/水分图)
1. 城市下拉浮层 z-index 提至 50,不再被肤质轮播圆点压盖
2. 倒计时音频已更新(用户重生成,解码验证 2.62s)
3. 侧脸步蒙版鼻向修正:2/3 右脸鼻朝右、3/3 左脸鼻朝左(原与设计稿 06/07 相反)
4. 无进展提示上移至 bottom:24%(引导 chips 上方,不再压快门)
5. 相册选择器「拍照」项为 iOS/安卓系统行为,web 无法移除(详见回复)
6. 报告页顶栏移除,返回/历史改悬浮
7. 肌龄小字按产品要求不再展示(钳制留痕仍随数据返回)
8. 关注行对齐设计稿:前缀灰 + 维度名深色 500
9. 三维度排版对齐设计稿:去掉档位徽章,最低分标红
10. 肤色水润图改用 water_area(接口水分图),REPORT_MAPS 与 mock fixture 同步更新
11. 报告「综合分析」「生活方式建议」通栏移除,新增「护理建议」卡(V2.3 §8:肤质定制/最差2分类/精华/生活方式 五段,§8.5.1 无内部标注;skin-score 实现 8.6/8.7 全量文案库,回归新增 §8.8 两组样例逐字断言 86/86 绿)
e2e 全量绿(含真实 API:水分图/红区合成图挂载、护理建议五段)
2026-09-27 11:15:54 +08:00

152 lines
9.6 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 = {};
/* ─── V2.3 §8.8 护理建议成品真值(五段,逐字) ─── */
var V23_SKIN = { case1: '混合偏干肌肤', case2: '干性肌肤' };
var V23_EXPECT = {
case1: [
{ tag: '肤质', text: '你的日常肤质为混合偏干肌肤:T区状态尚可,但两颊及面部外侧偏干,易出现紧绷与细纹。建议分区护理:两颊侧重保湿滋养,选用滋润型乳液/面霜,T区轻量保湿即可,避免全脸统一用厚重面霜。干燥会放大纹理粗糙感,建议先将两颊水合度提上来再做角质调理。' },
{ tag: '皱纹', text: '加强纹路分级管理,重点针对眼周、法令纹等动态纹高发区域,使用视黄醇/胜肽类淡纹精华并配合点按手法,遏制静态纹转化。' },
{ tag: '光滑', text: '加强细腻度管理,避免过度清洁破坏角质屏障,重点关注毛孔显著区域,配合低浓度酸类(如水杨酸)周期护理与柔润精华。' },
{ tag: '精华', text: '建议搭配使用35+支撑精华:专为轻熟肌打造,提升弹韧表现、维持面部轮廓清晰度,改善疲态并延缓衰老速度。' },
{ tag: '生活方式', text: '保持侧卧改仰卧的睡姿习惯,减少枕巾摩擦;控制屏幕时间,缓解眼周疲劳' }
],
case2: [
{ tag: '肤质', text: '你的日常肤质为干性肌肤:全脸皮脂分泌偏少,角质层保水能力弱,易紧绷起屑。护理以高效保湿与封闭锁水为核心,洁面选用温和低泡产品,避免频繁去角质,面霜可选择含神经酰胺、角鲨烷等封闭性成分的配方。' },
{ tag: '皱纹', text: '加强纹路分级管理,重点针对眼周、法令纹等动态纹高发区域,使用视黄醇/胜肽类淡纹精华并配合点按手法,遏制静态纹转化。' },
{ tag: '轮廓', text: '强化轮廓紧致管理,重点针对面中、苹果肌与下颌线支持带区域,配合向上提升按摩手法,促进真皮层胶原再生与微循环。' },
{ tag: '精华', text: '建议搭配使用50+重塑精华:针对熟龄肌松弛与轮廓流失问题,深层重塑支撑结构,改善明显松弛与纹路,重建面部紧实与弹性。' },
{ tag: '生活方式', text: '保持侧卧改仰卧的睡姿习惯,减少枕巾摩擦;控制屏幕时间,缓解眼周疲劳' }
]
};
/* ─── 断言计数与输出 ─── */
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);
// V2.3 §8 护理建议五段(真值:§8.8 样例;daily_skin_type 按样例取,与 summary 口径的肤质参数不同)
var vm23 = SkinScore.buildViewModel(fixture.result, fixture.side, { realAge: tr.profile.realAge, skinType: V23_SKIN[tag] });
okStr(tag + '.BA 护理建议·五段成品', JSON.stringify(vm23.careAdvice), JSON.stringify(V23_EXPECT[tag]));
}
// 登记项清理提示:已知差异键本轮全部命中为差异;若某键实际通过则提示移除登记
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();