feat(h5): apply fourth feedback and tighten local capture validation
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
// Isolated backend/browser/fixture upstream: no production data or paid API.
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
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 CaptureQuality = require('../js/capture-quality');
|
||||
const ReportUI = require('../js/report-ui');
|
||||
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-fourth-test-'));
|
||||
const artifacts = process.env.H5_ARTIFACT_DIR;
|
||||
const child = spawn(process.execPath, ['-e', `
|
||||
const fs=require('fs'), vm=require('vm'), w={};
|
||||
vm.runInNewContext(fs.readFileSync('assets/mock/report-fixture.js','utf8'),{window:w});
|
||||
const f=w.MOCK_FIXTURE;
|
||||
f.side.right.right_jawline_info.right_jawline_quality=3;
|
||||
global.fetch=async()=>({ok:true,status:200,json:async()=>({result:f.result,face_rectangle:f.face_rectangle,left_side_result:f.side.left,right_side_result:f.side.right})});
|
||||
require('./server.js');
|
||||
`], { cwd: h5, env: { ...process.env, PORT: '18104', HOST: '127.0.0.1', JUZI_DATA_ROOT: dataRoot,
|
||||
SKIN_API_KEY: 'fixture', SKIN_API_SECRET: 'fixture', PUBLIC_BASE_URL: '', WECHAT_APP_ID: '', WECHAT_APP_SECRET: '' }, stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
let browser;
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => reject(Error('test startup timeout')), 10000);
|
||||
child.stdout.on('data', b => { if (b.toString().includes('已启动')) { clearTimeout(timeout); resolve(); } });
|
||||
child.once('exit', code => { clearTimeout(timeout); reject(Error('server exited ' + code)); });
|
||||
});
|
||||
const base = 'http://127.0.0.1:18104/';
|
||||
browser = await chromium.launch({ headless: true });
|
||||
const page = await browser.newPage({ viewport: { width: 375, height: 726 }, isMobile: true, hasTouch: true });
|
||||
const errors = [], requests = [];
|
||||
page.on('pageerror', e => errors.push(e.message));
|
||||
page.on('request', r => requests.push(new URL(r.url()).pathname));
|
||||
await page.addInitScript(() => {
|
||||
navigator.mediaDevices.getUserMedia = () => Promise.reject(new DOMException('fixture denial', 'NotAllowedError'));
|
||||
window.__scanIterations = [];
|
||||
document.addEventListener('animationiteration', e => { if (e.target.classList.contains('scan-beam')) window.__scanIterations.push(e.elapsedTime); });
|
||||
});
|
||||
await page.route('**/vendor/faceapi/face-api.js', route => route.abort());
|
||||
await page.goto(base); await page.waitForLoadState('networkidle');
|
||||
assert.equal((await page.locator('[data-action="login-browser"]').textContent()).trim(), '游客登录');
|
||||
assert(!requests.some(p => /scan-(base|face)|\.woff2$|faceapi/.test(p)), 'no heavy/scanning entry requests');
|
||||
await page.check('#login-consent'); await page.click('[data-action="login-browser"]');
|
||||
await page.waitForSelector('#p-login-success.show');
|
||||
assert((await page.locator('#login-success-message').textContent()).includes('当前使用:游客登录'));
|
||||
await page.click('[data-action="login-confirm"]');
|
||||
assert.equal(await page.locator('#s-splash [data-auth-status]').textContent(), '游客已登录');
|
||||
const api = page.context().request;
|
||||
await page.addScriptTag({ url: base + 'assets/mock/report-fixture.js' });
|
||||
const fixture = await page.evaluate(() => window.MOCK_FIXTURE);
|
||||
const result = await (await api.post(base + 'api/skin-analyze', { data: {
|
||||
front_image: fixture.photos[0], left_image: fixture.photos[1], right_image: fixture.photos[2],
|
||||
profile: { real_age: 30 }, capture_diagnostics: { version: CaptureQuality.config.version,
|
||||
views: { front: { mode: 'live', yaw: 2, luma: 120 }, left: { mode: 'file', direction_checked: false }, right: { mode: 'unchecked', reason: 'model-unavailable' } }, ignore: 'discard' }
|
||||
} })).json();
|
||||
assert(result.ok && result.report_saved, 'provider quality=3 must NOT block or request a re-shoot');
|
||||
assert(result.vm.notes.some(n => n.includes('quality=3')));
|
||||
const admin = (await (await api.get(base + 'api/admin/records/' + result.record_id)).json()).record;
|
||||
assert.equal(admin.capture_quality.status, 'invalid');
|
||||
assert.equal(admin.capture_quality.views.right.quality, 3);
|
||||
assert.equal(admin.capture_diagnostics.version, CaptureQuality.config.version);
|
||||
assert.equal(admin.capture_diagnostics.policy.pose.SIDE_YAW_MIN, 35);
|
||||
assert.equal(admin.capture_diagnostics.ignore, undefined);
|
||||
assert(admin.notes.some(n => n.includes('quality=3')));
|
||||
const wrinkle = admin.presentation.views.find(view => view.key === 'wrinkles');
|
||||
assert(!wrinkle.text.includes('分区明细') && !wrinkle.text.includes('标准化长度'));
|
||||
assert.equal(wrinkle.details.length, 12);
|
||||
const csv = await (await api.get(base + 'api/admin/export')).text();
|
||||
assert(csv.includes('侧脸质量状态,前端拍摄参数版本'));
|
||||
assert(csv.includes('invalid,' + CaptureQuality.config.version));
|
||||
const before = (await api.get(base + 'api/reports/' + result.record_id));
|
||||
const snapshotBefore = await before.text();
|
||||
await page.click('[data-action="history"]'); await page.click('[data-report-id]');
|
||||
await page.waitForSelector('#s-report.active');
|
||||
assert(await page.locator('#dims-note').isVisible());
|
||||
assert.equal(await page.locator('#vm-notes').isVisible(), false);
|
||||
assert(!(await page.locator('#s-report').textContent()).includes('建议重拍'));
|
||||
const expected = ReportUI.focusOf(result.vm);
|
||||
assert.equal(await page.locator('.dims-note-l1').textContent(), '近期优先关注:' + expected.name);
|
||||
assert.equal(await page.locator('.dims-note-l2').textContent(), expected.tip);
|
||||
for (const width of [320, 375, 390, 414, 480]) {
|
||||
await page.setViewportSize({ width, height: 844 });
|
||||
const geometry = await page.locator('#dims-note').evaluate(el => ({ width: el.clientWidth, scroll: el.scrollWidth, bottom: el.getBoundingClientRect().bottom, next: document.querySelector('.dimension-title').getBoundingClientRect().top }));
|
||||
assert(geometry.scroll <= geometry.width && geometry.bottom <= geometry.next, 'focus wraps without overlap at ' + width);
|
||||
}
|
||||
if (artifacts) { fs.mkdirSync(artifacts, { recursive: true }); await page.setViewportSize({ width: 375, height: 726 }); await page.screenshot({ path: path.join(artifacts, '01-focus-history.png') }); }
|
||||
assert.equal(await (await api.get(base + 'api/reports/' + result.record_id)).text(), snapshotBefore, 'display changes must not mutate historical report');
|
||||
const desktop = await browser.newPage({ viewport: { width: 1500, height: 1000 } });
|
||||
desktop.on('pageerror', e => errors.push(e.message));
|
||||
await desktop.goto(base + 'admin.html');
|
||||
await desktop.waitForFunction(() => document.querySelector('#tbl-records').textContent.includes('侧脸质量异常'));
|
||||
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('quality=3'));
|
||||
const wrinkleTable = desktop.locator('.d-sec').filter({ hasText: '皱纹分区原始指标(仅后台复核' });
|
||||
assert.equal(await wrinkleTable.locator('tbody tr').count(), 12);
|
||||
assert.equal(await wrinkleTable.locator('thead th').count(), 10);
|
||||
assert((await wrinkleTable.textContent()).includes('标准化长度'));
|
||||
if (artifacts) await desktop.screenshot({ path: path.join(artifacts, '02-admin-quality.png') });
|
||||
// A fast intercepted analysis response still waits for one actual CSS cycle.
|
||||
await page.goto(base); await page.click('[data-action="open-privacy"]');
|
||||
await page.click('#privacy-check'); await page.click('[data-action="agree-continue"]');
|
||||
for (const selector of ['#pick-birth', '#pick-gender', '#pick-region']) {
|
||||
await page.click(selector); await page.click('.ms-mobile-select.ms-show .ms-ensure'); await page.waitForTimeout(400);
|
||||
}
|
||||
await page.click('[data-action="submit-profile"]'); await page.click('[data-action="start-camera"]');
|
||||
let sent;
|
||||
await page.route('**/api/skin-analyze', route => { sent = route.request().postDataJSON(); return route.fulfill({ json: result }); });
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
await page.waitForFunction(n => document.querySelector('#cam-step').textContent.includes(n + '/3'), i);
|
||||
await page.waitForTimeout(1600);
|
||||
await page.setInputFiles('#cam-file', path.join(h5, '../reference/facepp-skinanalyze/examples/images/C3_180.jpg'));
|
||||
}
|
||||
await page.waitForSelector('#p-analyzing.show');
|
||||
await page.waitForTimeout(300);
|
||||
assert(await page.locator('#p-analyzing').isVisible(), 'fast response must not cut off cycle');
|
||||
if (artifacts) await page.screenshot({ path: path.join(artifacts, '03-scan-motion.png') });
|
||||
await page.waitForSelector('#s-report.active');
|
||||
assert(await page.evaluate(() => window.__scanIterations.some(seconds => seconds >= 1)), 'one actual complete 1000ms CSS cycle, independent of Playwright observation latency');
|
||||
assert(sent && sent.capture_diagnostics.version === CaptureQuality.config.version);
|
||||
assert.equal(sent.capture_diagnostics.views.front.mode, 'unchecked');
|
||||
assert.equal(await page.locator('#p-analyzing.scan-running').count(), 0);
|
||||
assert.equal((await (await api.get(base + 'api/reports')).json()).total, 1, 'intercepted flow creates no extra report');
|
||||
assert.deepEqual(errors, []);
|
||||
console.log('PASS: guest login/status; provider quality=3 remains successful/saved/backend-only; calibration/admin/CSV; focus historical immutability/five widths; real CSS cycle gates fast report; fallback trace; no JS errors/paid calls');
|
||||
} finally {
|
||||
if (browser) await browser.close();
|
||||
const stopped = once(child, 'exit'); child.kill(); await stopped;
|
||||
fs.rmSync(dataRoot, { recursive: true, force: true });
|
||||
}
|
||||
Reference in New Issue
Block a user