feat(h5): calculate integer total from weighted dimensions across reports and admin
This commit is contained in:
@@ -108,7 +108,7 @@ function main() {
|
||||
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);
|
||||
okNum(tag + '.F 综合得分(4:3:3)', vm.total, Math.round(tr.dims.firm.S * 4 + tr.dims.vita.S * 3 + tr.dims.stab.S * 3));
|
||||
|
||||
// 六分类 P/S/档位 + 文案逐字
|
||||
for (var k in CAT_COLS) {
|
||||
|
||||
@@ -46,6 +46,9 @@ try {
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.report_saved, true);
|
||||
assert(result.vm && result.presentation);
|
||||
const expectedTotal = Math.round(result.vm.dims.firm.S * 4 + result.vm.dims.vita.S * 3 + result.vm.dims.stab.S * 3);
|
||||
assert.equal(result.vm.total, expectedTotal);
|
||||
assert.notEqual(expectedTotal, result.result.score_info.total_score, 'fixture distinguishes weighted score from API total');
|
||||
assert(result.vm.skinAgeRaw < 32);
|
||||
assert.equal(result.vm.skinAgeDisplay, result.vm.skinAgeRaw);
|
||||
assert.equal(result.vm.essence.tier, 1); // Raw age, never actual60.
|
||||
@@ -55,24 +58,32 @@ try {
|
||||
assert.deepEqual(adminRecord.recommendation, result.vm.recommendation);
|
||||
assert.equal(adminRecord.skin_age, result.vm.skinAgeRaw);
|
||||
assert.equal(adminRecord.skin_age_display, result.vm.skinAgeDisplay);
|
||||
assert.equal(adminRecord.total_score, expectedTotal);
|
||||
assert.equal(adminRecord.total_score_rule, 'dimensions-433-v1');
|
||||
for (const key of ['firm', 'vita', 'stab']) assert.equal(adminRecord.dims[key].S, result.vm.dims[key].S);
|
||||
const list = await (await api.get(base + 'api/admin/records')).json();
|
||||
assert.equal(list.items[0].skin_age, result.vm.skinAgeRaw);
|
||||
assert.equal(list.items[0].skin_age_display, result.vm.skinAgeRaw);
|
||||
assert.deepEqual(list.items[0].recommendation, result.vm.recommendation);
|
||||
assert.equal(list.items[0].total_score, expectedTotal);
|
||||
const csv = await (await api.get(base + 'api/admin/export')).text();
|
||||
assert(csv.includes('原始肌龄(推荐依据),前端展示肌龄'));
|
||||
assert(csv.includes(result.vm.recommendation.version));
|
||||
assert(csv.includes('综合得分(4:3:3)'));
|
||||
assert.equal(csv.trim().split('\n')[1].split(',')[12], String(expectedTotal));
|
||||
const snapshot = await (await api.get(base + 'api/reports/' + result.record_id)).json();
|
||||
assert.equal(snapshot.report.photos.length, 3);
|
||||
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.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 } });
|
||||
desktop.on('pageerror', e => errors.push(e.message));
|
||||
await desktop.goto(base + 'admin.html');
|
||||
const row = desktop.locator('#tbl-records tbody tr').first();
|
||||
await row.waitFor();
|
||||
assert.equal(await row.locator('td').nth(6).textContent(), String(expectedTotal));
|
||||
assert((await row.textContent()).includes('原始 ' + result.vm.skinAgeRaw + '岁'));
|
||||
assert((await row.textContent()).includes('展示 ' + result.vm.skinAgeRaw + '岁'));
|
||||
assert((await row.textContent()).includes("1'me 主场精华 I(25+)"));
|
||||
@@ -92,6 +103,9 @@ try {
|
||||
await row.click();
|
||||
await desktop.waitForFunction(() => document.querySelector('#d-body').textContent.includes('推荐依据'));
|
||||
const detailText = await desktop.locator('#d-body').textContent();
|
||||
assert(detailText.includes('综合得分 ' + expectedTotal));
|
||||
assert(detailText.includes('紧塑力×4+透亮感×3+稳韧力×3'));
|
||||
if (process.env.H5_ARTIFACT_DIR) await desktop.screenshot({ path: path.join(process.env.H5_ARTIFACT_DIR, 'admin-weighted-detail.png') });
|
||||
assert(detailText.includes(result.vm.recommendation.reason));
|
||||
assert(detailText.includes('前端展示肌龄 ' + result.vm.skinAgeRaw + '岁'));
|
||||
for (const view of result.presentation.views) assert(detailText.includes(view.text));
|
||||
@@ -123,6 +137,11 @@ try {
|
||||
await page.click('[data-action="history"]');
|
||||
await page.click('[data-report-id]');
|
||||
await page.waitForSelector('#s-report.active');
|
||||
assert.equal(await page.locator('#stat-score b').textContent(), String(expectedTotal));
|
||||
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') });
|
||||
}
|
||||
await page.waitForFunction(() => [...document.querySelectorAll('.ditem-img')].every(i => !i.hidden && i.naturalWidth));
|
||||
const imagesBefore = await page.locator('.ditem-img').evaluateAll(images => images.map(i => i.src));
|
||||
const copyBefore = await page.locator('.ditem-para').allTextContents();
|
||||
@@ -147,6 +166,7 @@ try {
|
||||
legacy.presentation.product = { ...ReportUI.products[1], image: 'assets/img/ui-20261009/product-35.png' };
|
||||
legacy.presentation.views[0].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: ['最差项约束'] };
|
||||
delete legacy.vm.recommendation;
|
||||
fs.writeFileSync(snapshotFile, JSON.stringify(legacy));
|
||||
@@ -154,6 +174,7 @@ try {
|
||||
delete record.presentation;
|
||||
delete record.recommendation;
|
||||
record.skin_age_display = 57;
|
||||
record.total_score = 12;
|
||||
fs.writeFileSync(recordFile, JSON.stringify(record) + '\n');
|
||||
const storedBytes = fs.readFileSync(snapshotFile, 'utf8');
|
||||
const recordBytes = fs.readFileSync(recordFile, 'utf8');
|
||||
@@ -161,6 +182,12 @@ try {
|
||||
await page.click('[data-report-id]');
|
||||
await page.waitForSelector('#s-report.active');
|
||||
assert.equal(await page.locator('#stat-age b').textContent(), String(result.vm.skinAgeRaw), 'old saved display clamp must not override original skin age');
|
||||
assert.equal(await page.locator('#stat-score b').textContent(), String(expectedTotal), 'old H5 report uses saved dimensions, not saved upstream total');
|
||||
const legacyList = await (await api.get(base + 'api/admin/records')).json();
|
||||
assert.equal(legacyList.items[0].total_score, expectedTotal);
|
||||
const legacyDetail = await (await api.get(base + 'api/admin/records/' + result.record_id)).json();
|
||||
assert.equal(legacyDetail.record.total_score, expectedTotal);
|
||||
assert.equal((await (await api.get(base + 'api/admin/export')).text()).trim().split('\n')[1].split(',')[12], String(expectedTotal));
|
||||
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));
|
||||
@@ -171,6 +198,21 @@ try {
|
||||
assert((await desktop.locator('#d-body').textContent()).includes('前端展示肌龄 ' + result.vm.skinAgeRaw + '岁'));
|
||||
assert.equal(fs.readFileSync(snapshotFile, 'utf8'), storedBytes);
|
||||
assert.equal(fs.readFileSync(recordFile, 'utf8'), recordBytes);
|
||||
// An incomplete set must show no total everywhere, not fall back to the API score.
|
||||
const missingVm = JSON.parse(JSON.stringify(legacy.vm)); missingVm.dims.stab.S = null;
|
||||
const missingRec = JSON.parse(JSON.stringify(record)); missingRec.dims.stab.S = null;
|
||||
fs.writeFileSync(snapshotFile, JSON.stringify({ ...legacy, vm: missingVm }));
|
||||
fs.writeFileSync(recordFile, JSON.stringify(missingRec) + '\n');
|
||||
await page.reload(); await page.waitForSelector('#s-splash.active');
|
||||
await page.click('[data-action="history"]'); await page.click('[data-report-id]');
|
||||
await page.waitForSelector('#s-report.active');
|
||||
assert.equal(await page.locator('#stat-score b').textContent(), '--');
|
||||
assert.equal((await (await api.get(base + 'api/admin/records')).json()).items[0].total_score, null);
|
||||
assert.equal((await (await api.get(base + 'api/admin/records/' + result.record_id)).json()).record.total_score, null);
|
||||
await desktop.goto(base + 'admin.html');
|
||||
assert.equal(await desktop.locator('#tbl-records tbody tr').first().locator('td').nth(6).textContent(), '--');
|
||||
assert.equal((await (await api.get(base + 'api/admin/export')).text()).trim().split('\n')[1].split(',')[12], '');
|
||||
fs.writeFileSync(recordFile, recordBytes);
|
||||
// Missing skin age must not leave the previous product visible in the H5.
|
||||
delete legacy.payload.result.skin_age;
|
||||
const SkinScore = require('../js/skin-score.js');
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
'use strict';
|
||||
const assert = require('node:assert/strict');
|
||||
const Score = require('../js/skin-score');
|
||||
const dims = (firm, vita, stab) => ({ firm: { S: firm }, vita: { S: vita }, stab: { S: stab } });
|
||||
for (const [scores, total] of [
|
||||
[[8, 9, 8.5], 85], [[10, 10, 10], 100], [[7, 7, 7], 70],
|
||||
[[10, 7, 7], 82], [[7, 10, 7], 79], [[7, 7, 10], 79],
|
||||
[[8.01, 8.82, 8], 83], [[7.69, 8.72, 9.1], 84]
|
||||
]) assert.equal(Score.weightedTotal(dims(...scores)), total, scores.join('/'));
|
||||
for (const invalid of [null, undefined, NaN, Infinity, -Infinity, '9', -1, 11]) {
|
||||
for (const key of ['firm', 'vita', 'stab']) {
|
||||
const d = dims(8, 9, 8.5); d[key].S = invalid;
|
||||
assert.equal(Score.weightedTotal(d), null, 'no reweighting invalid/missing ' + key);
|
||||
}
|
||||
}
|
||||
for (const d of [null, {}, { firm: { S: 9 }, vita: { S: 8 } }]) assert.equal(Score.weightedTotal(d), null);
|
||||
const input = JSON.parse(JSON.stringify(Score.MOCK_RESULT));
|
||||
const before = JSON.stringify(input);
|
||||
const expected = Score.buildViewModel(input, Score.MOCK_SIDE, {});
|
||||
assert.equal(expected.totalRule, 'dimensions-433-v1');
|
||||
assert.equal(JSON.stringify(input), before, 'preserve raw upstream data');
|
||||
for (const value of [0, 100, null, undefined]) {
|
||||
const r = JSON.parse(JSON.stringify(input));
|
||||
if (value === undefined) delete r.score_info.total_score; else r.score_info.total_score = value;
|
||||
const changed = Score.buildViewModel(r, Score.MOCK_SIDE, {});
|
||||
assert.equal(changed.total, expected.total, 'API total has no influence');
|
||||
assert.deepEqual(changed.dims, expected.dims);
|
||||
assert.deepEqual(changed.recommendation, expected.recommendation);
|
||||
}
|
||||
assert.equal(Score.buildViewModel({}, null, {}).total, null);
|
||||
console.log('PASS weighted total: 4:3:3, integer/half rounding, missing/invalid dimensions, API total ignored, source/scores/recommendation unchanged');
|
||||
Reference in New Issue
Block a user