// 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, 30); 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 }); }