feat(report): unify display grades and add contour side photos
This commit is contained in:
@@ -18,7 +18,7 @@ assert.deepEqual(result.cat.vars.acne_scoring.types[1].sources,['left','right'])
|
||||
assert.equal(result.vm.dims.stab.S,9.61);assert(result.view.text.includes('丘疹1处'));
|
||||
assert.equal(run([2,0,0,0,3],[5,1,0,0,1],[3,1,0,0,4],80).cat.P,70,'front score mapping still limits score');
|
||||
const heavy=run([0,0,0,0,0],[0,0,1,0,0],[0,0,0,1,0]);
|
||||
assert.equal(heavy.cat.P,90);assert.equal(heavy.view.level,'改善');
|
||||
assert.equal(heavy.cat.P,90);assert.equal(heavy.view.level,'优秀');
|
||||
assert(heavy.view.text.includes('脓包1处')&&heavy.view.text.includes('结节1处'));
|
||||
assert(heavy.view.text.includes('含脓包、结节'));
|
||||
const empty=run([null,null,null,null,null],[null,null,null,null,null],[null,null,null,null,null]);
|
||||
|
||||
@@ -24,9 +24,10 @@ check([79.999,79.998,79.997,99,99,99,99],'firm',['wrinkles','contour','color']);
|
||||
check([null,null,50,null,60,null,null],'firm',['color','water']);
|
||||
check([null,null,null,null,null,null,null],null,[]);
|
||||
const x=check([99,98,79.99,99,90,99,99],'vita',['color','water','wrinkles']);
|
||||
assert.equal(x.ui.views.find(v=>v.key==='color').level,'改善');assert.equal(x.ui.views.find(v=>v.key==='water').level,'优秀');
|
||||
assert.equal(x.ui.views.find(v=>v.key==='color').level,'优秀');assert.equal(x.ui.views.find(v=>v.key==='water').level,'优秀');
|
||||
assert.equal(x.ui.careRule,'seven-lowest-three-focus-first-v1');
|
||||
const heavy=sample(Object.fromEntries(keys.map(k=>[k,95])),'stab');heavy.vm.cats.acne.vars.acne_counts.acne_nodule=1;heavy.vm.cats.acne.vars.acne_cnt=1;
|
||||
assert.equal(UI.build(heavy.vm,{}).views.find(v=>v.key==='acne').level,'改善');
|
||||
assert.equal(UI.build(heavy.vm,{}).views.find(v=>v.key==='acne').level,'优秀');
|
||||
assert(UI.build(heavy.vm,{}).views.find(v=>v.key==='acne').text.includes('结节'));
|
||||
assert(!Copy.care.wrinkles['改善'].lifestyle.startsWith('保证睡眠'));
|
||||
console.log('PASS care: global lowest3, focus-first selected only, two/one/no focus members, tie/document order, raw precision, missing, independent color/water, acne guard, exact copy and immutable scores');
|
||||
|
||||
@@ -8,7 +8,7 @@ const templates={
|
||||
优秀:'检测到当前肌肤色素分 88,无明显色素沉积,{note};肤色均匀透亮。'
|
||||
};
|
||||
let checked=0;
|
||||
for(const [P,level] of [[79.99,'改善'],[80,'良好'],[89.99,'良好'],[90,'优秀'],[100,'优秀']]){
|
||||
for(const [P,level] of [[30,'改善'],[40,'良好'],[60,'良好'],[65,'优秀'],[79.99,'优秀'],[100,'优秀']]){
|
||||
for(const severity of [0,1,2,3,null,undefined,-1,4,1.5,'2',NaN,Infinity]){
|
||||
const result=structuredClone(SS.MOCK_RESULT);
|
||||
result.score_info.melanin_score=88;
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
// Browser-only fixtures, no writes/paid detection. Full photo opens in the
|
||||
// existing zoom overlay while thumbnail cropping is CSS-only.
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import {execSync} from 'node:child_process';
|
||||
import {pathToFileURL} from 'node:url';
|
||||
const {chromium}=await import(pathToFileURL(execSync('npm root -g',{encoding:'utf8'}).trim()+'/playwright/index.mjs').href);
|
||||
const browser=await chromium.launch();
|
||||
try {
|
||||
const page=await browser.newPage({viewport:{width:375,height:1000},isMobile:true,hasTouch:true});
|
||||
const errors=[];page.on('pageerror',e=>errors.push(e.message));
|
||||
const source=fs.readFileSync(new URL('../js/app.js',import.meta.url),'utf8');
|
||||
await page.route('**/api/**',r=>r.fulfill({json:{ok:true,user:{kind:'browser'}}}));
|
||||
await page.route('**/js/app.js*',r=>r.fulfill({contentType:'text/javascript',body:source.replace(/\}\)\(\);\s*$/,`
|
||||
window.__sideTest=function(ph){
|
||||
var fx=window.MOCK_FIXTURE,vm=SkinScore.buildViewModel(fx.result,fx.side,{});
|
||||
renderReport(vm,fx.photos[0],null,{result:fx.result},ph);showScreen('s-report');
|
||||
};})();`)}));
|
||||
await page.goto((process.env.H5_BASE_URL||'http://127.0.0.1:8080/')+'?mock=1&screen=report');
|
||||
const photo=color=>'data:image/svg+xml,'+encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="320" height="240"><rect width="320" height="240" fill="'+color+'"/></svg>');
|
||||
const left=photo('red'),right=photo('blue');
|
||||
for(const width of [320,375,414,480]) {
|
||||
await page.setViewportSize({width,height:1000});
|
||||
await page.evaluate(ph=>window.__sideTest(ph),[null,left,right]);
|
||||
await page.waitForFunction(()=>[...document.querySelectorAll('#contour-sides figure')].every(f=>!f.hidden&&f.querySelector('img').naturalWidth===320));
|
||||
const figures=page.locator('#contour-sides figure');
|
||||
assert.equal(await figures.count(),2);
|
||||
assert.equal((await page.locator('#contour-sides').textContent()).trim(),'','no visible left/right captions');
|
||||
assert.equal(await page.locator('#contour-left').getAttribute('src'),left);
|
||||
assert.equal(await page.locator('#contour-right').getAttribute('src'),right);
|
||||
await page.waitForFunction(()=>document.querySelector('#ditem-img1').naturalWidth>0);
|
||||
const crop=await page.locator('#ditem-img1').evaluate(img=>{
|
||||
const i=img.getBoundingClientRect(),f=img.parentElement.getBoundingClientRect();
|
||||
const other=document.querySelector('#ditem-img2').parentElement.getBoundingClientRect();
|
||||
return{ratio:f.height/i.height,top:i.top-f.top,width:i.width-f.width,other:other.height/i.height,overflow:getComputedStyle(img.parentElement).overflow};
|
||||
});
|
||||
assert(Math.abs(crop.ratio-.9)<.002);assert(Math.abs(crop.top)<1);assert(Math.abs(crop.width)<1);
|
||||
assert(Math.abs(crop.other-1)<.002);assert.equal(crop.overflow,'hidden');
|
||||
const offset=await page.locator('#ditem-img1').getAttribute('data-content-offset');
|
||||
assert(offset!=null&&Number(offset)>=0&&Number(offset)<=.090001);
|
||||
const fullFront=await page.locator('#ditem-img1').getAttribute('src');
|
||||
await page.locator('#ditem-img1').click();
|
||||
assert.equal(await page.locator('#zoom-img').getAttribute('src'),fullFront,'front zoom still uses uncropped source');
|
||||
await page.locator('#p-zoom').click();
|
||||
const geo=await page.locator('.contour-side-photo').first().evaluate(e=>{const r=e.getBoundingClientRect();return{w:r.width,h:r.height,fit:getComputedStyle(e.querySelector('img')).objectFit};});
|
||||
assert(Math.abs(geo.h/geo.w-92.25/104.35)<.002);assert.equal(geo.fit,'cover');
|
||||
for(const img of await page.locator('.contour-side-photo img').all()) {
|
||||
const fill=await img.evaluate(e=>{
|
||||
const m=new DOMMatrix(getComputedStyle(e).transform),r=e.getBoundingClientRect(),b=e.parentElement.getBoundingClientRect();
|
||||
return{sx:m.a,sy:m.d,shift:m.f/b.height,top:r.top-b.top,bottom:r.bottom-b.bottom};
|
||||
});
|
||||
assert(Math.abs(fill.sx-1.3)<.001);assert(Math.abs(fill.sy-1.3)<.001);assert(Math.abs(fill.shift-.15)<.001);
|
||||
assert(Math.abs(fill.top)<1&&fill.bottom>0,'zoomed fill covers frame without blank top edge');
|
||||
}
|
||||
const spacing=await page.locator('#contour-sides').evaluate(e=>{
|
||||
const frame=document.querySelector('.contour-front-frame'),f=frame.getBoundingClientRect(),s=e.getBoundingClientRect();
|
||||
return{u:f.width/215,horizontal:parseFloat(getComputedStyle(e).columnGap),vertical:s.top-f.bottom};
|
||||
});
|
||||
assert(Math.abs(spacing.horizontal/spacing.u-6.3)<.1);
|
||||
assert(Math.abs(spacing.vertical/spacing.u-7.56)<.1);
|
||||
await page.getByRole('button',{name:'查看左脸完整大图'}).click();
|
||||
assert(await page.locator('#p-zoom').evaluate(e=>e.classList.contains('show')));
|
||||
assert.equal(await page.locator('#zoom-img').getAttribute('src'),left);
|
||||
assert.equal(await page.locator('#zoom-img').evaluate(e=>e.naturalWidth/e.naturalHeight),4/3);
|
||||
await page.locator('#p-zoom').click();
|
||||
await page.getByRole('button',{name:'查看右脸完整大图'}).click();
|
||||
assert.equal(await page.locator('#zoom-img').getAttribute('src'),right);
|
||||
await page.keyboard.press('Escape');
|
||||
assert(!await page.locator('#p-zoom').evaluate(e=>e.classList.contains('show')));
|
||||
await page.locator('.card-detail').first().locator('[data-tab="1"]').click();
|
||||
await page.waitForTimeout(400);
|
||||
assert(await page.locator('#contour-sides').evaluate(e=>{
|
||||
const r=e.getBoundingClientRect(),v=e.closest('.detail-viewport').getBoundingClientRect();
|
||||
return e.closest('.dpanel').getAttribute('aria-hidden')==='true'&&r.right<=v.left+1;
|
||||
}),'contour thumbnails remain clipped outside the wrinkle tab');
|
||||
}
|
||||
await page.evaluate(ph=>window.__sideTest(ph),[null,left,null]);
|
||||
await page.waitForFunction(()=>!document.querySelector('#contour-left').closest('figure').hidden);
|
||||
assert(await page.locator('#contour-right').evaluate(e=>e.closest('figure').hidden&&!e.hasAttribute('src')));
|
||||
await page.evaluate(()=>window.__sideTest([null,null,null]));
|
||||
assert(await page.locator('#contour-sides').evaluate(e=>e.hidden));
|
||||
assert.equal(await page.locator('#contour-sides img[src]').count(),0,'new report clears previous side photos');
|
||||
await page.evaluate(()=>window.__sideTest([null,'data:image/jpeg;base64,broken',null]));
|
||||
await page.waitForFunction(()=>!document.querySelector('#contour-left').hasAttribute('src'));
|
||||
assert(await page.locator('#contour-sides').evaluate(e=>e.hidden));
|
||||
assert.deepEqual(errors,[]);
|
||||
console.log('PASS contour side photos: 4 widths, preserved thumbnail heights and 30% tighter gaps, left/right sources, full-aspect zoom, close/Escape, tab isolation, one/missing/broken photos and stale-photo cleanup');
|
||||
} finally {await browser.close();}
|
||||
@@ -36,11 +36,12 @@ assert(!text.includes('分区明细') && !text.includes('标准化长度'));
|
||||
const approved = {
|
||||
'预警': '全脸共检测到皱纹 12 条、细纹 34 条,面中、口周、眼周等区域已出现静态纹倾向,纹路已呈现固化趋势,并非临时性干纹。',
|
||||
'改善': '全脸共检测到皱纹 12 条、细纹 34 条,面部整体平整,仅眼周或口周有细微的干纹,无明显静态深纹,纹路面积占比相对较低。',
|
||||
'良好': '面中、口周、眼周等各区域均无明显皱纹沟壑,无静态深纹与粗纹,各类纹路面积均处于极低水平,面部整体平整细腻。'
|
||||
'良好': '全脸共检测到皱纹 12 条、细纹 34 条;整体纹路状态较好,建议维持日常淡纹与保湿护理。'
|
||||
};
|
||||
for (const level of Object.keys(approved)) {
|
||||
const fixture = JSON.parse(modelBefore);
|
||||
fixture.cats.wrinkles.level = level;
|
||||
fixture.cats.wrinkles.P = { '预警': 20, '改善': 50, '良好': 90 }[level];
|
||||
fixture.cats.wrinkles.vars.wr_cnt = 12;
|
||||
fixture.cats.wrinkles.vars.fl_cnt = 34;
|
||||
assert.equal(ReportUI.build(fixture, result).views.find(view => view.key === 'wrinkles').text, approved[level]);
|
||||
@@ -49,6 +50,7 @@ assert.equal(JSON.stringify(model), modelBefore);
|
||||
assert.equal(JSON.stringify(result), rawBefore);
|
||||
const excellent = JSON.parse(JSON.stringify(model));
|
||||
excellent.cats.smooth.level = '良好'; // Legacy level name maps to new "优秀".
|
||||
excellent.cats.smooth.P = 90;
|
||||
excellent.cats.smooth.vars.pore_cnt = 7;
|
||||
excellent.cats.smooth.vars.blackhead_cnt = 2;
|
||||
let smooth = ReportUI.build(excellent, result).views.find(view => view.key === 'smooth');
|
||||
@@ -67,6 +69,13 @@ assert(crop.width < 1600 && crop.height < 1800);
|
||||
assert(crop.left <= 1479 && crop.top <= 1133);
|
||||
assert(crop.left + crop.width >= 2595 && crop.top + crop.height >= 2249);
|
||||
assert(Math.abs(crop.width / crop.height - 215 / 278) < 1e-8);
|
||||
const moved = sandbox.vizShiftContentDown(crop,{left:1479,top:1133,width:1116,height:1116},.09);
|
||||
assert(Math.abs(moved.top-(crop.top-crop.height*.09))<1e-8);
|
||||
assert.equal(moved.width,crop.width);assert.equal(moved.height,crop.height);assert.equal(moved.left,crop.left);
|
||||
assert(moved.top>=0&&moved.top+moved.height>=2249);
|
||||
assert.equal(sandbox.vizShiftContentDown(crop,null,.09),crop);
|
||||
const edgeCrop=sandbox.vizFaceCrop(1000,1000,{left:100,top:0,width:300,height:300});
|
||||
assert.equal(sandbox.vizShiftContentDown(edgeCrop,{left:100,top:0,width:300,height:300},.09).top,0);
|
||||
for (const rect of [null, { left: NaN }, { left: 0, top: 0, width: -1, height: 20 },
|
||||
{ left: 3900, top: 0, width: 1000, height: 1000 }, { left: 0, top: 0, width: '100', height: 100 }]) {
|
||||
const full = sandbox.vizFaceCrop(4000, 3000, rect);
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
'use strict';
|
||||
const assert = require('node:assert/strict');
|
||||
const SS = require('../js/skin-score'), UI = require('../js/report-ui'), Copy = require('../js/report-copy');
|
||||
for (const [S, level] of [[7,'改善'],[7.94,'改善'],[7.95,'良好'],[8,'良好'],[8.94,'良好'],[8.95,'优秀'],[9,'优秀'],[9.3,'优秀'],[10,'优秀'],[null,null],[NaN,null]]) {
|
||||
assert.equal(UI.levelForScore(S), level);
|
||||
}
|
||||
const keys = ['contour','wrinkles','color','smooth','water','sens','acne'];
|
||||
for (const [P, level] of [[0,'改善'],[20,'改善'],[31.49,'改善'],[31.5,'良好'],[50,'良好'],[64.83,'良好'],[65,'优秀'],[76,'优秀'],[79,'优秀'],[100,'优秀'],[null,null]]) {
|
||||
const r = structuredClone(SS.MOCK_RESULT), vm = SS.buildViewModel(r, SS.MOCK_SIDE, {});
|
||||
for (const key of ['contour','wrinkles','smooth','sens','acne']) vm.cats[key].P = P;
|
||||
Object.assign(vm.cats.tone.vars, {melanin:P,color_score:P,water_score:P,water_sev:0});
|
||||
vm.cats.tone.vars.tone_scoring.color_score = P;
|
||||
const before = JSON.stringify({vm,r}), ui = UI.build(vm,r);
|
||||
for (const view of ui.views) {
|
||||
assert.equal(view.level,level,view.key+' P='+P);
|
||||
assert.deepEqual(view.advice,level ? Copy.care[view.key][level] : null);
|
||||
assert.equal(view.S,UI.scoreOfP(P));
|
||||
}
|
||||
assert.equal(JSON.stringify({vm,r}),before);
|
||||
assert.equal(ui.gradeRule,UI.gradeRule);
|
||||
for (const care of ui.care) assert.equal(care.level,level);
|
||||
assert.deepEqual(ui.views.map(v=>v.key),keys);
|
||||
}
|
||||
const r = structuredClone(SS.MOCK_RESULT);
|
||||
r.eye_pouch_severity = {value:2};
|
||||
r.score_info.wrinkle_score = 76;
|
||||
const vm = SS.buildViewModel(r,SS.MOCK_SIDE,{}), ui = UI.build(vm,r);
|
||||
assert.equal(UI.dimensionScore(vm.dims.firm.S),'9.3');
|
||||
assert.equal(ui.views.find(v=>v.key==='contour').level,'优秀');
|
||||
assert(ui.views.find(v=>v.key==='contour').text.includes('眼周无明显眼袋问题'),'contour copy remains original by user request');
|
||||
const old = structuredClone(ui);
|
||||
delete old.gradeRule;
|
||||
old.views[0].level='改善';old.views[0].text='旧轮廓文案';
|
||||
old.product={tier:3,title:'历史推荐不改'};
|
||||
const oldBytes=JSON.stringify(old), vmBytes=JSON.stringify(vm);
|
||||
const display=UI.forDisplay(vm,r,old);
|
||||
assert.equal(display.views[0].level,'优秀');
|
||||
assert.deepEqual(display.product,old.product);
|
||||
assert.deepEqual(display.care.map(v=>v.key),old.care.map(v=>v.key));
|
||||
assert.equal(JSON.stringify(old),oldBytes);assert.equal(JSON.stringify(vm),vmBytes);
|
||||
assert.equal(UI.forDisplay(vm,r,ui),ui);
|
||||
console.log('PASS: seven views/advice use displayed 8/9 grades; rounding, missing, 9.3 case, unchanged contour copy, immutable history and product/order');
|
||||
@@ -0,0 +1,32 @@
|
||||
'use strict';
|
||||
const assert=require('node:assert/strict'), SS=require('../js/skin-score'), UI=require('../js/report-ui'), Copy=require('../js/report-copy');
|
||||
const Original=require('./fixtures/report-copy-before-two-fixes.cjs');
|
||||
// Strict scope guard: every care template and 20 other conclusion templates
|
||||
// must be byte-for-byte identical to the released baseline.
|
||||
assert.deepEqual(Copy.care,Original.care);
|
||||
for(const key of Object.keys(Original.conclusion)) for(const level of Object.keys(Original.conclusion[key])) {
|
||||
if(key==='wrinkles'&&level==='优秀') continue;
|
||||
assert.equal(Copy.conclusion[key][level],Original.conclusion[key][level],key+' '+level+' must stay original');
|
||||
}
|
||||
const r=structuredClone(SS.MOCK_RESULT), vm=SS.buildViewModel(r,SS.MOCK_SIDE,{});
|
||||
vm.cats.wrinkles.P=76;Object.assign(vm.cats.wrinkles.vars,{wr_cnt:10,fl_cnt:10});
|
||||
vm.cats.acne.P=95;
|
||||
Object.assign(vm.cats.acne.vars,{acne_cnt:2,acne_detail:'丘疹2处',acne_counts_complete:true,acne_counts:{closed_comedones:0,acne:2,acne_pustule:0,acne_nodule:0,acne_mark:0}});
|
||||
const before=JSON.stringify({vm,r}),ui=UI.build(vm,r),view=k=>ui.views.find(v=>v.key===k);
|
||||
assert.equal(view('wrinkles').text,'全脸共检测到皱纹 10 条、细纹 10 条;整体纹路状态较好,建议维持日常淡纹与保湿护理。');
|
||||
assert.equal(view('acne').text,'检测到 2 处皮损(丘疹2处),局部皮损仍需关注。');
|
||||
assert.equal(view('acne').level,'优秀');
|
||||
assert.equal(JSON.stringify({vm,r}),before);
|
||||
const saved={gradeRule:UI.gradeRule,copyRule:'retired',product:{tier:3,title:'旧推荐'},care:ui.views.slice().reverse().map(v=>({name:v.name,text:'旧护理'}))};
|
||||
const bytes=JSON.stringify(saved),display=UI.forDisplay(vm,r,saved);
|
||||
assert.equal(display.copyRule,UI.copyRule);assert.deepEqual(display.product,saved.product);
|
||||
assert.deepEqual(display.care.map(v=>v.name),saved.care.map(v=>v.name));
|
||||
assert.equal(JSON.stringify(saved),bytes);
|
||||
for(const item of display.care){const v=view(item.key);assert.equal(item.level,v.level);assert.equal(item.text,Copy.care[item.key][v.level].care+Copy.care[item.key][v.level].lifestyle);}
|
||||
assert.equal(UI.forDisplay(vm,r,{...saved,care:saved.care.slice(0,2)}).care.length,2);
|
||||
const unknown={name:'未知旧项目',text:'保留原文'};
|
||||
assert.deepEqual(UI.forDisplay(vm,r,{...saved,care:[unknown]}).care[0],unknown);
|
||||
const missing=structuredClone(vm);Object.assign(missing.cats.wrinkles.vars,{wr_cnt:null,fl_cnt:null});
|
||||
const text=UI.build(missing,{}).views.find(v=>v.key==='wrinkles').text;
|
||||
assert(text.includes('计数未获取'));assert(!text.includes('皱纹 0 条'));assert(!/[{}]|undefined|NaN/.test(text));
|
||||
console.log('PASS strict two-copy scope: all care and 20 other templates unchanged; excellent wrinkles, detected acne, legacy care mapping and immutable data');
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
// Generated from reference/AI测肤报告页话术修改.md; run tools/build_report_copy.py.
|
||||
(function(root,factory){if(typeof module==="object"&&module.exports)module.exports=factory();else root.ReportCopy=factory();})(typeof window!=="undefined"?window:this,function(){return {
|
||||
"version": "2026-10-09",
|
||||
"conclusion": {
|
||||
"contour": {
|
||||
"改善": "面部轮廓紧致度有待提升,下颌线清晰度不足,左右下颌线角度分别为左 {la}°/ 右 {ra}°;眼袋明显、黑眼圈轻/中/重度(根据分数给出判断);中面部容积略欠饱满,肌肤支撑力偏弱。",
|
||||
"良好": "面部轮廓清晰,下颌线较为清晰,左右下颌线角度左 {la}°/ 右 {ra}°;眼周眼袋初显、黑眼圈轻/中/重度(根据分数给出判断);中面部容积无明显流失;肌肤处于健康状态,支撑力表现良好。",
|
||||
"优秀": "面部轮廓紧致立体,下颌线清晰;眼周无明显眼袋问题、黑眼圈轻/中/重度(根据分数给出判断);中面部容积饱满,肌肤支撑力充足。"
|
||||
},
|
||||
"wrinkles": {
|
||||
"改善": "全脸共检测到皱纹 {wr_cnt} 条、细纹 {fl_cnt} 条,面中、口周、眼周等区域已出现静态纹倾向,纹路已呈现固化趋势,并非临时性干纹。",
|
||||
"良好": "全脸共检测到皱纹 {wr_cnt} 条、细纹 {fl_cnt} 条,面部整体平整,仅眼周或口周有细微的干纹,无明显静态深纹,纹路面积占比相对较低。",
|
||||
"优秀": "面中、口周、眼周等各区域均无明显皱纹沟壑,无静态深纹与粗纹,各类纹路面积均处于极低水平,面部整体平整细腻。"
|
||||
},
|
||||
"color": {
|
||||
"改善": "检测到当前肌肤色素分 {melanin},存在色素沉着较显著,{dark_circle_note},整体透亮感不足。",
|
||||
"良好": "检测到当前肌肤色素分 {melanin},局部有色素沉积;{dark_circle_note},整体状态稳定,局部透亮感稍弱。",
|
||||
"优秀": "检测到当前肌肤色素分 {melanin},无明显色素沉积,{dark_circle_note};肤色均匀透亮。"
|
||||
},
|
||||
"smooth": {
|
||||
"改善": "当前肌肤平滑度还有提升空间,检测到粗大毛孔 {pore_cnt} 颗、黑头 {blackhead_cnt} 颗,肤感整体细腻度有提升空间,纹理不均。",
|
||||
"良好": "肌肤整体较为平滑,局部区域细腻度不足;检测到粗大毛孔 {pore_cnt} 颗、黑头 {blackhead_cnt} 颗,有轻微角质粗糙感。",
|
||||
"优秀": "肌肤质感表现优异,表面细腻平滑,角质层排列整齐,无明显粗大毛孔与黑头。"
|
||||
},
|
||||
"water": {
|
||||
"改善": "水分分 {water_score};缺水面积 {water_area}%,{water_severity_note}肌肤水润度有待提升,建议加强补水与保湿。",
|
||||
"良好": "水分分 {water_score};缺水面积 {water_area}%、肌肤含水量处于健康水平,整体水润度良好。",
|
||||
"优秀": "水分分 {water_score};角质层含水量充足,肌肤屏障锁水能力强,表面饱满水润,无干燥紧绷表现。"
|
||||
},
|
||||
"sens": {
|
||||
"改善": "泛红面积达 {sens_area}%,肌肤泛红明显,敏感区域可见泛红;对外界刺激耐受度不足,容易受外界因素影响产生状态波动。",
|
||||
"良好": "泛红面积 {sens_area}%,肌肤存在局部轻微泛红;对外界环境有良好的耐受度,仅在接触强刺激物时偶有轻微反应。",
|
||||
"优秀": "泛红面积 {sens_area}%,无明显泛红、敏感迹象;对外界刺激耐受度佳,肌肤处于稳定的健康状态。"
|
||||
},
|
||||
"acne": {
|
||||
"改善": "检测到 {acne_cnt} 处皮损({acne_detail}){heavy_note},肌肤状态不稳定,毛孔易堵塞,皮脂分泌波动明显。",
|
||||
"良好": "检测到 {acne_cnt} 处皮损({acne_detail}),以闭口粉刺、丘疹为主,无明显红肿痘痘;皮脂分泌基本正常,毛孔偶有轻微堵塞。",
|
||||
"优秀": "未检出明显痘痘、粉刺或闭口,无明显红肿、脓包迹象。"
|
||||
}
|
||||
},
|
||||
"care": {
|
||||
"contour": {
|
||||
"改善": {
|
||||
"care": "重点进行轮廓紧致管理,关注面中、苹果肌与下颌线区域。日常增加紧致精华步骤,配合由下向上的轻柔提升按摩,帮助肌肤维持弹润和支撑感,让轮廓看起来更紧实。",
|
||||
"lifestyle": "保证充足、规律的睡眠,让肌肤在夜间充分修护;坚持防晒,减少紫外线带来的老化影响;减少熬夜。"
|
||||
},
|
||||
"良好": {
|
||||
"care": "关注面中、苹果肌、下颌线等容易显疲态区域的支撑感。日常可叠加紧致精华步骤,并配合向上提升按摩手法,预防疲态引起的松弛与垮塌累积,稳住面部支撑线。",
|
||||
"lifestyle": "日常注意防晒,保持规律作息;坚持适度运动,帮助肌肤维持活力。"
|
||||
},
|
||||
"优秀": {
|
||||
"care": "保持现有紧致抗衰护理节奏,日常叠加使用提亮精华,坚持防晒,加强光损伤防护与状态管理,持续关注面中疲态与轮廓支撑状态,让紧致状态更稳定、更持久。",
|
||||
"lifestyle": "坚持防晒与规律作息,均衡饮食,保证优质蛋白、新鲜蔬果和健康脂肪摄入,适度运动。"
|
||||
}
|
||||
},
|
||||
"wrinkles": {
|
||||
"改善": {
|
||||
"care": "重视纹路管理,日常可使用含有肽类、胶原类成分的淡纹精华,针对眼周、法令纹等动态纹高发区域配合点按手法,遏制静态纹转化,帮肌肤恢复平滑细腻的状态。",
|
||||
"lifestyle": "注意睡姿,减少面部长期受压;减少夸张表情,避免揉眼、托腮等表情性挤压习惯,减少纹路加深。"
|
||||
},
|
||||
"良好": {
|
||||
"care": "加强日常淡纹与肌肤弹润护理,叠加使用含肽类、胶原类成分的淡纹精华,提升肌肤水润度与饱满感,维持肤表平滑。",
|
||||
"lifestyle": "减少夸张表情反复牵拉,睡前加强保湿修护。"
|
||||
},
|
||||
"优秀": {
|
||||
"care": "以预防初纹为核心,搭配抗初老型精华,提前建立抗氧化/紧致护理习惯,延缓第一道静态纹出现。",
|
||||
"lifestyle": "保持良好作息与运动习惯,坚持防晒,减轻光老化与糖化负担,维持肌肤稳定状态。"
|
||||
}
|
||||
},
|
||||
"color": {
|
||||
"改善": {
|
||||
"care": "重点关照暗沉与色素易沉积区域,强化透亮与肤色均匀,严格防晒,配合温和焕亮或抗氧化精华帮助均匀肤色与透亮感。",
|
||||
"lifestyle": "日常加强防晒与抗氧化护理,减少熬夜与压力引发的暗沉,适当增加富含维C的蔬果摄入。"
|
||||
},
|
||||
"良好": {
|
||||
"care": "加强日常光泽管理与局部暗沉护理,做好防晒与抗氧化打底;叠加使用温和焕亮精华,稳住当前肤色状态并提升整体通透感。",
|
||||
"lifestyle": "持续做好防晒,保持作息规律,避免波动性色沉,守护均匀透亮感。"
|
||||
},
|
||||
"优秀": {
|
||||
"care": "维持现有防晒与抗氧化护肤习惯,叠加使用温和焕亮精华,巩固肤色均匀度与透亮感。",
|
||||
"lifestyle": "坚持日常防晒,保持均衡饮食支持皮肤稳态。"
|
||||
}
|
||||
},
|
||||
"smooth": {
|
||||
"改善": {
|
||||
"care": "关注毛孔可见度与肤表粗糙感改善,加强角质管理,提升肌肤平滑细腻度。",
|
||||
"lifestyle": "减少高糖高脂饮食,降低皮脂分泌刺激,定期温和调理角质,避免用手挤压黑头。"
|
||||
},
|
||||
"良好": {
|
||||
"care": "加强基础清洁与角质管理,关注鼻翼、下颌等易堵塞部位,提升毛孔通畅度与肌肤细腻度。",
|
||||
"lifestyle": "温水洁面避免烫洗,保持床品清洁减少局部刺激。"
|
||||
},
|
||||
"优秀": {
|
||||
"care": "维持当前温和护理节奏,维护角质层健康与肌肤屏障稳定状态。",
|
||||
"lifestyle": "保持规律清洁与保湿习惯,避免频繁更换功效类产品,减少肌肤适应负担。"
|
||||
}
|
||||
},
|
||||
"water": {
|
||||
"改善": {
|
||||
"care": "强化屏障锁水修复,注意水油平衡,每周2-3次深度补水,深度改善肌肤缺水状态。",
|
||||
"lifestyle": "每日足量饮水,补充身体水分,改善干燥环境湿度,避免长时间空调直吹。"
|
||||
},
|
||||
"良好": {
|
||||
"care": "日常需注重水油平衡调理,避免过度清洁损伤皮肤屏障,注意为肌肤持续补水。",
|
||||
"lifestyle": "每日足量饮水,环境干燥时及时增加肌肤水分补充。"
|
||||
},
|
||||
"优秀": {
|
||||
"care": "持续为皮肤补水与保湿,避免过度清洁与频繁去角质,巩固肌肤水润度与水油平衡。",
|
||||
"lifestyle": "坚持多喝水与规律作息,季节交替时提前调整保湿方案。"
|
||||
}
|
||||
},
|
||||
"sens": {
|
||||
"改善": {
|
||||
"care": "关注屏障修复与舒缓,精简护肤流程,选择温和洁面方式,修复受损角质层。若状态持续无明显好转,可寻求专业护肤建议。",
|
||||
"lifestyle": "减少外界刺激,如酒精、香精、高浓度酸类精华等成分,避免冷热骤变刺激。"
|
||||
},
|
||||
"良好": {
|
||||
"care": "保持温和护理节奏,注意防晒与环境防护,逐步提升皮肤耐受度。",
|
||||
"lifestyle": "避免冷热骤变刺激肌肤屏障,运动后及时清洁降温。"
|
||||
},
|
||||
"优秀": {
|
||||
"care": "持续规律护理并按季节阶段性调整,巩固皮肤耐受稳态。",
|
||||
"lifestyle": "保持健康作息与均衡饮食,维持皮肤屏障稳态。"
|
||||
}
|
||||
},
|
||||
"acne": {
|
||||
"改善": {
|
||||
"care": "优先针对性调理痘区肌肤状态,精简护肤流程,避免频繁更换护肤方案与叠加功效成分;严重时及时就医。",
|
||||
"lifestyle": "减少高糖高脂饮食与熬夜,避免手挤痘痘;保持床品、毛巾清洁,运动后及时清洁,减少汗液与皮脂堆积。"
|
||||
},
|
||||
"良好": {
|
||||
"care": "保持温和清洁与肌肤炎症调理,关注T区、下颌等易出油区域,提升毛孔通畅度,同时注意防晒与保湿。",
|
||||
"lifestyle": "保持规律作息与清淡饮食,运动后及时清洁,减少汗液与皮脂堆积。"
|
||||
},
|
||||
"优秀": {
|
||||
"care": "坚持定期清洁,维持水油平衡与毛孔通畅,巩固皮肤耐受稳态。",
|
||||
"lifestyle": "保持健康作息与均衡饮食,维持皮肤屏障健康稳定。"
|
||||
}
|
||||
}
|
||||
}
|
||||
};});
|
||||
@@ -41,6 +41,10 @@ try{
|
||||
return{heading:rect('.prod-title'),care:rect('.card-care'),product:rect('.card-product'),info:rect('.prod-info'),title:rect('#product-title'),name:rect('#product-name'),image:rect('#product-image'),badge:rect('.prod-badge'),disclaimer:rect('.report-disclaimer'),align:getComputedStyle(document.querySelector('.prod-title')).textAlign,headingFont:parseFloat(getComputedStyle(document.querySelector('.prod-title')).fontSize)};
|
||||
});
|
||||
assert.equal(geo.align,'center');assert(geo.heading.bottom<=geo.care.y+1);
|
||||
const divider=await p.locator('.product-section-heading').evaluate(e=>{const s=getComputedStyle(e,'::before');return{width:s.borderTopWidth,color:s.borderTopColor,left:s.left,right:s.right};});
|
||||
assert.equal(divider.width,'1px');assert.equal(divider.color,'rgb(221, 221, 221)');assert.equal(divider.left,'0px');assert.equal(divider.right,'0px');
|
||||
const careGap=await p.locator('.product-section-heading').evaluate(e=>({gap:parseFloat(getComputedStyle(e).marginTop),u:document.querySelector('.viz-frame').getBoundingClientRect().width/215}));
|
||||
assert(Math.abs(careGap.gap-careGap.u*24.0625)<.1,'care/product whitespace reduced by 30%');
|
||||
assert(Math.abs(geo.heading.w-595/1080*width)<.1,'Figma text group width scales with preview');
|
||||
const stabilityFont=await p.locator('.dimension-stability-title').evaluate(e=>parseFloat(getComputedStyle(e).fontSize));
|
||||
assert(Math.abs(geo.headingFont-stabilityFont)<.01,'bottom title matches stability heading font size at every width');
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
'use strict';
|
||||
// Input is private, sanitized real-case data outside Git. No HTTP writes,
|
||||
// upstream API calls, scoring recalculation or production database access.
|
||||
const fs=require('node:fs'), assert=require('node:assert/strict'), UI=require('../js/report-ui');
|
||||
const input=process.env.H5_COPY_CASE_FILE;
|
||||
if (!input) throw Error('Set H5_COPY_CASE_FILE to private JSON outside the repository');
|
||||
const cases=JSON.parse(fs.readFileSync(input,'utf8'));
|
||||
let views=0, careItems=0;
|
||||
for(const c of cases) {
|
||||
const bytes=JSON.stringify(c), out=UI.forDisplay(c.vm,c.result,c.saved);
|
||||
assert.equal(JSON.stringify(c),bytes,c.id+' immutable input');
|
||||
assert.deepEqual(out.product,c.saved.product,c.id+' saved recommendation');
|
||||
assert.deepEqual(out.care.map(v=>v.name),c.saved.care.map(v=>v.name),c.id+' care selection/order');
|
||||
assert.equal(out.views.length,7);
|
||||
for(const v of out.views) {
|
||||
views++;
|
||||
assert.equal(v.level,UI.levelForScore(v.S),c.id+' '+v.key);
|
||||
assert(!/[{}]|undefined|NaN/.test(v.text),c.id+' unresolved copy');
|
||||
if(v.key==='wrinkles'&&v.level==='优秀') {
|
||||
const vars=c.vm.cats.wrinkles.vars;
|
||||
if(vars.wr_cnt!=null) assert(v.text.includes('皱纹 '+vars.wr_cnt+' 条'));
|
||||
if(vars.fl_cnt!=null) assert(v.text.includes('细纹 '+vars.fl_cnt+' 条'));
|
||||
assert(!/无静态深纹|均无明显皱纹|固化趋势/.test(v.text));
|
||||
}
|
||||
if(v.key==='acne'&&c.vm.cats.acne.vars.acne_cnt>0) {
|
||||
assert(v.text.includes('局部皮损仍需关注')); assert(!v.text.includes('未检出'));
|
||||
}
|
||||
}
|
||||
for(const item of out.care) {
|
||||
careItems++;
|
||||
const v=out.views.find(v=>v.key===item.key);
|
||||
assert(v,c.id+' legacy name unresolved: '+item.name);
|
||||
assert.equal(item.level,v.level);
|
||||
assert.equal(item.text,v.advice.care+v.advice.lifestyle);
|
||||
}
|
||||
}
|
||||
console.log(`PASS ${cases.length} real cases, ${views} views, ${careItems} care items: score-based grades, factual copy, legacy care names, immutable scores/product/order`);
|
||||
@@ -68,6 +68,9 @@ try {
|
||||
assert.equal(adminRecord.skin_age_display, result.vm.skinAgeDisplay);
|
||||
assert.equal(adminRecord.total_score, expectedTotal);
|
||||
assert.equal(adminRecord.total_score_rule, 'dimensions-433-v1');
|
||||
assert.equal(adminRecord.grade_rule, ReportUI.gradeRule);
|
||||
for (const dim of Object.values(adminRecord.dims)) assert.equal(dim.level,ReportUI.internalLevel(dim.S));
|
||||
for (const item of Object.values(adminRecord.sub_scores)) assert.equal(item.level,ReportUI.internalLevel(item.S));
|
||||
assert.deepEqual(adminRecord.acne_scoring,result.vm.cats.acne.vars.acne_scoring);
|
||||
assert.deepEqual(adminRecord.tone_scoring,result.vm.cats.tone.vars.tone_scoring);
|
||||
assert.equal(adminRecord.vita_score_rule,'independent-color-water-smooth-min-v1');
|
||||
@@ -94,6 +97,8 @@ try {
|
||||
assert.deepEqual(snapshot.report.presentation, result.presentation);
|
||||
assert.deepEqual(snapshot.report.payload.result.face_maps, result.result.face_maps);
|
||||
assert.equal(snapshot.report.photos[0], fixture.photos[0]);
|
||||
assert.equal(snapshot.report.photos[1],fixture.photos[0]); // This HTTP test submits the same fixture for all three views.
|
||||
assert.equal(snapshot.report.photos[2],fixture.photos[0]);
|
||||
assert.equal(snapshot.report.vm.total, expectedTotal);
|
||||
assert.equal(JSON.parse(fs.readFileSync(path.join(dataRoot, 'records.jsonl'), 'utf8').trim()).total_score, expectedTotal, 'new records persist weighted score');
|
||||
const desktop = await browser.newPage({ viewport: { width: 1600, height: 1000 } });
|
||||
@@ -106,7 +111,7 @@ try {
|
||||
assert((await row.textContent()).includes('展示 ' + expectedAge + '岁'));
|
||||
assert((await row.textContent()).includes("1'me 主场精华 I(25+)"));
|
||||
for (const key of ['firm', 'vita', 'stab']) {
|
||||
assert((await row.textContent()).includes(ReportUI.dimNames[key] + ' ' + ReportUI.dimensionScore(result.vm.dims[key].S) + ' ' + ReportUI.levelMap[result.vm.dims[key].level]));
|
||||
assert((await row.textContent()).includes(ReportUI.dimNames[key] + ' ' + ReportUI.dimensionScore(result.vm.dims[key].S) + ' ' + ReportUI.levelForScore(result.vm.dims[key].S)));
|
||||
}
|
||||
assert.deepEqual(await desktop.locator('#f-dimlevel option').allTextContents(), ['不限档位', '改善', '良好', '优秀']);
|
||||
if (process.env.H5_ARTIFACT_DIR) {
|
||||
@@ -114,10 +119,11 @@ try {
|
||||
await desktop.screenshot({ path: path.join(process.env.H5_ARTIFACT_DIR, 'admin-list.png'), fullPage: true });
|
||||
}
|
||||
await desktop.selectOption('#f-dim', 'firm');
|
||||
await desktop.selectOption('#f-dimlevel', adminRecord.dims.firm.level);
|
||||
await desktop.selectOption('#f-dimlevel', { value: adminRecord.dims.firm.level });
|
||||
const filtered = desktop.waitForResponse(r => r.url().includes('/api/admin/records?'));
|
||||
await desktop.click('#btn-search');
|
||||
assert.equal((await (await filtered).json()).total, 1);
|
||||
const filteredResponse = await filtered;
|
||||
assert.equal((await filteredResponse.json()).total, 1, filteredResponse.url() + ' expected grade=' + adminRecord.dims.firm.level);
|
||||
await row.click();
|
||||
await desktop.waitForFunction(() => document.querySelector('#d-body').textContent.includes('推荐依据'));
|
||||
const detailText = await desktop.locator('#d-body').textContent();
|
||||
@@ -163,6 +169,9 @@ try {
|
||||
await page.click('[data-report-id]');
|
||||
await page.waitForSelector('#s-report.active');
|
||||
assert.equal(await page.locator('#stat-score b').textContent(), String(expectedTotal));
|
||||
await page.waitForFunction(()=>[...document.querySelectorAll('#contour-sides figure')].every(f=>!f.hidden));
|
||||
assert.equal(await page.locator('#contour-left').getAttribute('src'),snapshot.report.photos[1]);
|
||||
assert.equal(await page.locator('#contour-right').getAttribute('src'),snapshot.report.photos[2]);
|
||||
if (process.env.H5_ARTIFACT_DIR) {
|
||||
await page.waitForTimeout(400); // Capture after the report's entrance fade.
|
||||
await page.screenshot({ path: path.join(process.env.H5_ARTIFACT_DIR, 'report-weighted-total.png') });
|
||||
@@ -190,6 +199,9 @@ try {
|
||||
const legacy = JSON.parse(fs.readFileSync(snapshotFile, 'utf8'));
|
||||
legacy.presentation.product = { ...ReportUI.products[1], image: 'assets/img/ui-20261009/product-35.png' };
|
||||
legacy.presentation.views[0].text = '历史报告原文,不应按新文案重新计算。';
|
||||
delete legacy.presentation.gradeRule; // Old copy must regrade for display only.
|
||||
delete legacy.presentation.copyRule;
|
||||
for (const care of legacy.presentation.care) { delete care.key; delete care.level; care.text='旧护理待刷新'; }
|
||||
legacy.vm.skinAgeDisplay = 57; // Old snapshot retains the retired display clamp.
|
||||
legacy.vm.total = 12; // Old stored upstream total must not control the current display.
|
||||
legacy.vm.essence = { name: '35+支撑精华', text: '旧版推荐', triggers: ['最差项约束'] };
|
||||
@@ -214,15 +226,20 @@ try {
|
||||
const legacyDetail = await (await api.get(base + 'api/admin/records/' + result.record_id)).json();
|
||||
assert.equal(legacyDetail.record.skin_age_display, expectedAge);
|
||||
assert.equal(legacyDetail.record.total_score, expectedTotal);
|
||||
assert.equal(legacyDetail.record.presentation.gradeRule,ReportUI.gradeRule);
|
||||
assert.equal(legacyDetail.record.presentation.copyRule,ReportUI.copyRule);
|
||||
for (const item of legacyDetail.record.presentation.care) { assert(item.key); assert(item.level); assert(!item.text.includes('旧护理待刷新')); }
|
||||
for (const view of legacyDetail.record.presentation.views) assert.equal(view.level,ReportUI.levelForScore(view.S));
|
||||
const legacyCsv=(await (await api.get(base + 'api/admin/export')).text()).trim().split('\n')[1].split(',');
|
||||
assert.equal(legacyCsv[12], String(expectedTotal));
|
||||
assert.equal(legacyCsv[14], String(expectedAge));
|
||||
assert.equal(await page.locator('#product-title').textContent(), ReportUI.products[1].title);
|
||||
assert.equal(await page.locator('#product-image').getAttribute('src'), ReportUI.products[1].image, 'refresh artwork crop, never the historical recommendation/copy');
|
||||
assert((await page.locator('.ditem-para').allTextContents()).includes(legacy.presentation.views[0].text));
|
||||
assert((await page.locator('.ditem-para').allTextContents()).includes(ReportUI.forDisplay(legacy.vm,legacy.payload.result,legacy.presentation).views[0].text));
|
||||
assert(!(await page.locator('#care-list').textContent()).includes('旧护理待刷新'));
|
||||
await desktop.goto(base + 'admin.html');
|
||||
await desktop.locator('#tbl-records tbody tr').first().click();
|
||||
await desktop.waitForFunction(() => document.querySelector('#d-body').textContent.includes('历史报告原文'));
|
||||
await desktop.waitForFunction(text => document.querySelector('#d-body').textContent.includes(text), legacyDetail.record.presentation.views[0].text);
|
||||
assert((await desktop.locator('#d-body').textContent()).includes('旧版规则(历史记录)'));
|
||||
assert((await desktop.locator('#d-body').textContent()).includes('前端展示肌龄 ' + expectedAge + '岁'));
|
||||
assert.equal(fs.readFileSync(snapshotFile, 'utf8'), storedBytes);
|
||||
|
||||
@@ -11,12 +11,12 @@ const example=run(97);assert.equal(example.vm.cats.tone.P,77.9);assert.equal(exa
|
||||
assert(!/含水量偏低|缺水比较严重|水润度不足/.test(example.water.text));assert(example.water.text.includes('97'));
|
||||
assert.deepEqual(example.water.advice,Copy.care.water['优秀']);
|
||||
assert(!example.presentation.care.some(v=>v.name==='水分'),'healthy water97 should not displace a weaker item in global bottom three');
|
||||
for(const[p,level]of[[0,'改善'],[79.99,'改善'],[80,'良好'],[89.99,'良好'],[90,'优秀'],[100,'优秀']]){
|
||||
for(const[p,level]of[[0,'改善'],[30,'改善'],[40,'良好'],[60,'良好'],[65,'优秀'],[79.99,'优秀'],[80,'优秀'],[90,'优秀'],[100,'优秀']]){
|
||||
for(const pigment of[0,50,100]){const x=run(p,3,pigment,0);assert.equal(x.water.level,level);assert.equal(x.water.P,p);assert.deepEqual(x.water.advice,Copy.care.water[level]);}
|
||||
}
|
||||
const crossed=run(97,70);assert.equal(crossed.water.P,30);assert.equal(crossed.water.level,'改善');assert(crossed.water.text.includes('分数与缺水指标不一致'));assert.equal(crossed.vm.cats.tone.vars.tone_scoring.water_adjusted_score,30);
|
||||
assert(run(50,70).water.text.includes('缺水程度较高'));
|
||||
assert(!run(50,3).water.text.includes('程度较高'));
|
||||
assert(!run(50,null).water.text.includes('严重度'));
|
||||
assert(run(20,70).water.text.includes('缺水程度较高'));
|
||||
assert(!run(20,3).water.text.includes('程度较高'));
|
||||
assert(!run(20,null).water.text.includes('严重度'));
|
||||
for(const score of[null,undefined,-1,101,NaN,Infinity]){const x=run(score);assert.equal(x.water.valid,false);assert.equal(x.water.advice,null);assert(!x.presentation.care.some(v=>v.name==='水分'));}
|
||||
console.log('PASS water: 97 vs combined77.9, independent water copy/advice, 80/90 boundaries, true severity, existing cross-check, missing/invalid, scoring immutable');
|
||||
console.log('PASS water: independent copy/advice, displayed 8/9 boundaries, true severity, existing cross-check, missing/invalid, scoring immutable');
|
||||
|
||||
Reference in New Issue
Block a user