fix(report): recommend by measured skin age and align admin results
This commit is contained in:
@@ -8,6 +8,9 @@ import http from 'node:http';
|
||||
import { spawn, execSync } from 'node:child_process';
|
||||
import { once } from 'node:events';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import { createRequire } from 'node:module';
|
||||
const require = createRequire(import.meta.url);
|
||||
const ReportUI = require('../js/report-ui.js');
|
||||
const { chromium } = await import(pathToFileURL(execSync('npm root -g', { encoding: 'utf8' }).trim() + '/playwright/index.mjs').href);
|
||||
const h5 = fileURLToPath(new URL('../', import.meta.url));
|
||||
const dataRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'juzi-report-test-'));
|
||||
@@ -40,15 +43,68 @@ try {
|
||||
await page.click('[data-action="login-browser"]');
|
||||
await page.waitForSelector('#s-splash.active');
|
||||
const api = page.context().request;
|
||||
const result = await (await api.post(base + 'api/skin-analyze', { data: { front_image: fixture.photos[0], left_image: fixture.photos[0], right_image: fixture.photos[0], profile: { real_age: 28, daily_skin_type: '干性肌肤' } } })).json();
|
||||
const result = await (await api.post(base + 'api/skin-analyze', { data: { front_image: fixture.photos[0], left_image: fixture.photos[0], right_image: fixture.photos[0], profile: { real_age: 60, daily_skin_type: '干性肌肤' } } })).json();
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.report_saved, true);
|
||||
assert(result.vm && result.presentation);
|
||||
assert(result.vm.skinAgeRaw < 32);
|
||||
assert.equal(result.vm.skinAgeDisplay, 57);
|
||||
assert.equal(result.vm.essence.tier, 1); // Raw age, not actual60 or display57.
|
||||
assert.equal(result.presentation.product.tier, 1);
|
||||
const adminRecord = (await (await api.get(base + 'api/admin/records/' + result.record_id)).json()).record;
|
||||
assert.deepEqual(adminRecord.presentation, result.presentation);
|
||||
assert.deepEqual(adminRecord.recommendation, result.vm.recommendation);
|
||||
assert.equal(adminRecord.skin_age, result.vm.skinAgeRaw);
|
||||
assert.equal(adminRecord.skin_age_display, result.vm.skinAgeDisplay);
|
||||
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, 57);
|
||||
assert.deepEqual(list.items[0].recommendation, result.vm.recommendation);
|
||||
const csv = await (await api.get(base + 'api/admin/export')).text();
|
||||
assert(csv.includes('原始肌龄(推荐依据),前端展示肌龄'));
|
||||
assert(csv.includes(result.vm.recommendation.version));
|
||||
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]);
|
||||
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((await row.textContent()).includes('原始 ' + result.vm.skinAgeRaw + '岁'));
|
||||
assert((await row.textContent()).includes('展示 57岁'));
|
||||
assert((await row.textContent()).includes("1'me 主场精华 I(25+)"));
|
||||
for (const key of ['firm', 'vita', 'stab']) {
|
||||
assert((await row.textContent()).includes(ReportUI.dimNames[key] + ' ' + result.vm.dims[key].S.toFixed(1) + ' ' + ReportUI.levelMap[result.vm.dims[key].level]));
|
||||
}
|
||||
assert.deepEqual(await desktop.locator('#f-dimlevel option').allTextContents(), ['不限档位', '改善', '良好', '优秀']);
|
||||
if (process.env.H5_ARTIFACT_DIR) {
|
||||
fs.mkdirSync(process.env.H5_ARTIFACT_DIR, { recursive: true });
|
||||
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);
|
||||
const filtered = desktop.waitForResponse(r => r.url().includes('/api/admin/records?'));
|
||||
await desktop.click('#btn-search');
|
||||
assert.equal((await (await filtered).json()).total, 1);
|
||||
await row.click();
|
||||
await desktop.waitForFunction(() => document.querySelector('#d-body').textContent.includes('推荐依据'));
|
||||
const detailText = await desktop.locator('#d-body').textContent();
|
||||
assert(detailText.includes(result.vm.recommendation.reason));
|
||||
assert(detailText.includes('前端展示肌龄 57岁'));
|
||||
for (const view of result.presentation.views) assert(detailText.includes(view.text));
|
||||
for (const care of result.presentation.care) assert(detailText.includes(care.text));
|
||||
if (process.env.H5_ARTIFACT_DIR) {
|
||||
await desktop.locator('.d-sec').filter({ hasText: '使用原始肌龄' }).scrollIntoViewIfNeeded();
|
||||
await desktop.screenshot({ path: path.join(process.env.H5_ARTIFACT_DIR, 'admin-recommendation.png') });
|
||||
}
|
||||
await desktop.click('#btn-close');
|
||||
await desktop.click('[data-tab="stats"]');
|
||||
await desktop.waitForFunction(() => document.querySelector('#dim-dist').textContent.includes('透亮感'));
|
||||
assert(!(await desktop.locator('#dim-dist').textContent()).includes('预警'));
|
||||
// A separate browser cannot access this user's report, even knowing its ID.
|
||||
const other = await browser.newContext();
|
||||
assert.equal((await other.request.get(base + 'api/reports/' + result.record_id)).status(), 401);
|
||||
@@ -78,12 +134,55 @@ try {
|
||||
await page.waitForFunction(() => [...document.querySelectorAll('.ditem-img')].every(i => !i.hidden && i.naturalWidth));
|
||||
assert.deepEqual(await page.locator('.ditem-img').evaluateAll(images => images.map(i => i.src)), imagesBefore);
|
||||
assert.deepEqual(await page.locator('.ditem-para').allTextContents(), copyBefore);
|
||||
assert.equal(await page.locator('#product-title').textContent(), result.presentation.product.title);
|
||||
await page.goBack();
|
||||
await page.waitForSelector('#s-history.active');
|
||||
await page.goBack();
|
||||
await page.waitForSelector('#s-splash.active');
|
||||
// Simulate an old, immutable report whose original product was II, even though
|
||||
// today's rule would choose I. Admin must reuse its saved frontend copy, too.
|
||||
const recordFile = path.join(dataRoot, 'records.jsonl');
|
||||
const record = JSON.parse(fs.readFileSync(recordFile, 'utf8').trim());
|
||||
const snapshotFile = path.join(dataRoot, 'accounts/reports', record.owner_id, result.record_id + '.json');
|
||||
const legacy = JSON.parse(fs.readFileSync(snapshotFile, 'utf8'));
|
||||
legacy.presentation.product = ReportUI.products[1];
|
||||
legacy.presentation.views[0].text = '历史报告原文,不应按新文案重新计算。';
|
||||
legacy.vm.essence = { name: '35+支撑精华', text: '旧版推荐', triggers: ['最差项约束'] };
|
||||
delete legacy.vm.recommendation;
|
||||
fs.writeFileSync(snapshotFile, JSON.stringify(legacy));
|
||||
record.essence = legacy.vm.essence;
|
||||
delete record.presentation;
|
||||
delete record.recommendation;
|
||||
delete record.skin_age_display;
|
||||
fs.writeFileSync(recordFile, JSON.stringify(record) + '\n');
|
||||
const storedBytes = fs.readFileSync(snapshotFile, 'utf8');
|
||||
const recordBytes = fs.readFileSync(recordFile, 'utf8');
|
||||
await page.click('[data-action="history"]');
|
||||
await page.click('[data-report-id]');
|
||||
await page.waitForSelector('#s-report.active');
|
||||
assert.equal(await page.locator('#product-title').textContent(), ReportUI.products[1].title);
|
||||
assert((await page.locator('.ditem-para').allTextContents()).includes(legacy.presentation.views[0].text));
|
||||
await desktop.goto(base + 'admin.html');
|
||||
await desktop.locator('#tbl-records tbody tr').first().click();
|
||||
await desktop.waitForFunction(() => document.querySelector('#d-body').textContent.includes('历史报告原文'));
|
||||
assert((await desktop.locator('#d-body').textContent()).includes('旧版规则(历史记录)'));
|
||||
assert.equal(fs.readFileSync(snapshotFile, 'utf8'), storedBytes);
|
||||
assert.equal(fs.readFileSync(recordFile, 'utf8'), 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');
|
||||
legacy.vm = SkinScore.buildViewModel(legacy.payload.result, legacy.payload.side, { realAge: 60 });
|
||||
legacy.presentation = ReportUI.build(legacy.vm, legacy.payload.result);
|
||||
fs.writeFileSync(snapshotFile, JSON.stringify(legacy));
|
||||
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('.card-product').isVisible(), false);
|
||||
assert.equal(await page.locator('#product-unavailable').isVisible(), true);
|
||||
assert.deepEqual(errors, []);
|
||||
console.log('PASS: authenticated analyze → full snapshot → backend history → reload → seven identical images/copy; owner isolation, private files, browser back');
|
||||
console.log('PASS: raw-age recommendation; admin/H5 scores, ages, product, seven-view copy, care, CSV, filters; saved/reloaded and legacy immutable snapshots; missing-age UI; owner isolation/private files/back');
|
||||
} finally {
|
||||
if (browser) await browser.close();
|
||||
child.kill('SIGTERM');
|
||||
|
||||
Reference in New Issue
Block a user