'use strict'; const assert = require('node:assert/strict'); const SS = require('../js/skin-score.js'); const UI = require('../js/report-ui.js'); let count = 0; function check(age, firm, vita, stab, tier, adjustment = 'none') { const r = SS.recommendProduct(age, { firm, vita, stab }); assert.equal(r.tier, tier, `age=${age}; scores=${firm}/${vita}/${stab}`); assert.equal(r.adjustment, adjustment); assert.equal(r.version, '2026-10-16-display-skin-age-v2'); if (tier) assert(Math.abs(r.tier - r.baseTier) <= 1); count++; } // Explicit decision-table expectations, including fractional ages and inclusive endpoints. for (const [age, normal, low, high] of [ [24, 1, 1, 1], [31.99, 1, 1, 1], [32, 1, 2, 1], [34.99, 1, 2, 1], [35, 2, 2, 1], [36, 2, 2, 1], [37, 2, 2, 1], [38, 2, 2, 1], [38.01, 2, 2, 2], [46.99, 2, 2, 2], [47, 2, 3, 2], [49.99, 2, 3, 2], [50, 3, 3, 2], [51, 3, 3, 2], [52, 3, 3, 2], [53, 3, 3, 2], [53.01, 3, 3, 3], [80, 3, 3, 3] ]) { check(age, 8, 8, 8, normal); check(age, 7.99, 10, 10, low, low > normal ? 'up' : 'none'); check(age, 9, 9, 7, high, high < normal ? 'down' : 'none'); check(age, 9, 7, 9, high, high < normal ? 'down' : 'none'); } // Thresholds are compared before display rounding, not to 1- or 2-decimal scores. for (const age of [32, 34.99, 47, 49.99]) { const base = age < 35 ? 1 : 2; check(age, 7.99999, 10, 10, base + 1, 'up'); check(age, 8, 10, 10, base); check(age, null, 10, 10, base); } for (const age of [35, 38, 50, 53]) { const base = age < 50 ? 2 : 3; check(age, 8.99999, 10, 10, base); check(age, 9, 8.99999, 8.99999, base); check(age, 9, null, null, base); check(age, null, 10, 10, base); check(age, 9, null, 9, base - 1, 'down'); check(age, 9, 9, null, base - 1, 'down'); } check(37, 9, 7, 9, 1, 'down'); // Only dullness: the third dimension may be arbitrarily low. check(35, 8.99, 10, 10, 2); // Firmness is necessary, other two alone do not downgrade. check(24, null, null, null, 1); check(40, null, null, null, 2); for (const invalid of [null, undefined, NaN, Infinity, -Infinity, 0, -5, '37', {}]) { check(invalid, 10, 10, 10, null, 'unavailable'); } const raw = JSON.parse(JSON.stringify(SS.MOCK_RESULT)); raw.skin_age = { value: 24 }; for (const realAge of [24, 40, 60, undefined]) { const vm = SS.buildViewModel(raw, SS.MOCK_SIDE, { realAge }); const age=realAge==null?24:Math.max(realAge-5,Math.min(realAge+5,24)); assert.equal(vm.essence.tier, SS.recommendProduct(age,vm.recommendation.scores).tier); assert.equal(vm.recommendation.skinAge, age); assert(Math.abs(vm.recommendation.scores.firm - 7.69) < 1e-12); assert.equal(UI.build(vm, raw).product.tier,vm.essence.tier); assert.equal(vm.skinAgeDisplay, realAge == null ? 24 : Math.max(realAge - 5, Math.min(realAge + 5, 24))); count++; } delete raw.skin_age; const missing = SS.buildViewModel(raw, SS.MOCK_SIDE, { realAge: 60 }); assert.equal(missing.group, null); assert.equal(missing.essence.tier, null); assert.equal(UI.build(missing, raw).product, null); assert(missing.recommendation.reason.includes('未获取有效展示肌龄')); count++; console.log(`PASS: ${count} recommendation cases; unchanged boundary/threshold rules, display-age input and missing-age handling`);