Files
juzi/h5/test/customer-feedback-fourth.mjs
T

141 lines
10 KiB
JavaScript

// 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 wechat = require('./wechat-fixture.cjs')(path.join(dataRoot, 'accounts'))[0];
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.title(), "1'me肌肤状态检测");
assert.equal(await page.locator('meta[property="og:title"]').getAttribute('content'), "1'me肌肤状态检测");
assert.equal((await page.locator('[data-action="guest-experience"]').textContent()).trim(), '游客登陆');
assert(!requests.some(p => /scan-(base|face)|\.woff2$|faceapi/.test(p)), 'no heavy/scanning entry requests');
await page.click('[data-action="guest-experience"]');
await page.waitForSelector('#s-splash.active');
assert.equal(await page.locator('#p-login-success.show').count(), 0);
assert.equal(await page.locator('#s-splash [data-auth-status]').textContent(), '游客模式');
await page.context().addCookies([{ name: 'juzi_session', value: wechat.token, url: base }]);
await page.evaluate(() => AccountClient.init()); // Persistence scenarios require WeChat.
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, 24);
assert.equal(admin.capture_diagnostics.policy.capture.minFrames, 2);
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 });
}