// Real local detector + synthetic video containing existing three-view photos. // Auth/analysis are in-memory fixtures: no account/report writes or paid calls. import assert from 'node:assert/strict'; import fs from 'node:fs'; import { execSync } from 'node:child_process'; import { pathToFileURL, fileURLToPath } from 'node:url'; const { chromium } = await import(pathToFileURL(execSync('npm root -g', { encoding: 'utf8' }).trim() + '/playwright/index.mjs').href); const base = process.env.H5_BASE_URL || 'http://127.0.0.1:8080/'; const browser = await chromium.launch({ headless: true }); try { const page = await browser.newPage({ viewport: { width: 375, height: 726 } }); page.setDefaultTimeout(60000); const errors = [], failedRequests = []; page.on('pageerror', e => errors.push(e.message)); page.on('requestfailed', request => failedRequests.push({ path: new URL(request.url()).pathname, error: request.failure().errorText })); await page.route('**/api/auth/session', route => route.fulfill({ json: { ok: true, user: { id: 'local-camera-fixture', kind: 'browser' } } })); for (const name of ['C3_180.jpg', 'C3_135.jpg', 'C3_-135.jpg']) { const body = fs.readFileSync(fileURLToPath(new URL('../../reference/facepp-skinanalyze/examples/images/' + name, import.meta.url))); await page.route('**/sample-' + name, route => route.fulfill({ contentType: 'image/jpeg', body })); } await page.addInitScript(() => { const canvas = document.createElement('canvas'); canvas.width = 800; canvas.height = 600; let currentImage, offsetX = 0; function draw() { if (!currentImage) return; const c = canvas.getContext('2d'); c.fillStyle = '#111'; c.fillRect(0, 0, canvas.width, canvas.height); c.drawImage(currentImage, offsetX, 0, canvas.width, canvas.height); } setInterval(draw, 100); window.__cameraFixture = { canvas, set: async name => { const image = new Image(); image.src = 'sample-' + name; await image.decode(); // Simulate a person following the centering prompt, not an off-center // landscape source stretched into a portrait camera frame. let nextOffset = 0; if (window.faceapi && faceapi.nets.faceLandmark68Net.isLoaded) { const faces = await faceapi.detectAllFaces(image, new faceapi.TinyFaceDetectorOptions({ inputSize: 416, scoreThreshold: .5 })); if (faces.length === 1) nextOffset = canvas.width / 2 - (faces[0].box.x + faces[0].box.width / 2) * canvas.width / image.naturalWidth; } currentImage = image; offsetX = nextOffset; draw(); } }; navigator.mediaDevices.getUserMedia = async () => { await window.__cameraFixture.set('C3_180.jpg'); return canvas.captureStream(10); }; }); await page.route('**/api/skin-analyze', async route => { const fixture = await page.evaluate(() => ({ ok: true, result: SkinScore.MOCK_RESULT, side: SkinScore.MOCK_SIDE, report_saved: true })); await route.fulfill({ json: fixture }); }); await page.goto(base); await page.click('[data-action="open-privacy"]'); assert.equal(await page.title(), "1'me肌肤状态检测"); 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"]'); try { await page.waitForFunction(() => window.faceapi && faceapi.nets.faceLandmark68Net.isLoaded && document.querySelector('#s-cam').classList.contains('cam-live')); } catch (error) { console.log(await page.evaluate(() => ({ status: document.querySelector('#cam-status').textContent, hint: document.querySelector('#cam-hint').textContent, toast: document.querySelector('#toast').textContent, width: document.querySelector('#cam-video').videoWidth, classes: document.querySelector('#s-cam').className, engine: !!window.faceapi })), errors, failedRequests); throw error; } assert.equal(await page.locator('#s-cam.cam-manual').count(), 0, 'real automatic pipeline, not manual bypass'); await page.evaluate(() => { window.__holdSamples = []; window.__hints = []; window.__evaluations = []; const evaluate = CaptureQuality.evaluate; CaptureQuality.evaluate = function(d, options) { const result = evaluate(d, options); window.__evaluations.push({ target: options.target, ok: result.ok, hint: result.hint }); return result; }; const hold = CaptureQuality.hold; CaptureQuality.hold = function(state, obs, now) { const result = hold(state, obs, now); window.__holdSamples.push({ gap: state ? now - state.lastAt : null, frames: result.state && result.state.frames, ready: result.ready, obs }); if (window.__holdSamples.length > 8) window.__holdSamples.shift(); return result; }; new MutationObserver(() => { window.__hints.push(document.querySelector('#cam-hint').textContent); if (window.__hints.length > 20) window.__hints.shift(); }).observe(document.querySelector('#cam-hint'), { childList: true, subtree: true }); }); try { await page.waitForFunction(() => document.querySelector('#cam-step').textContent.includes('2/3')); } catch (error) { console.log(await page.evaluate(async () => { const v = document.querySelector('#cam-video'); const faces = await faceapi.detectAllFaces(v, new faceapi.TinyFaceDetectorOptions({ inputSize: 416, scoreThreshold: .5 })).withFaceLandmarks(); return { status: document.querySelector('#cam-status').textContent, hint: document.querySelector('#cam-hint').textContent, step: document.querySelector('#cam-step').textContent, size: [v.videoWidth, v.videoHeight, v.clientWidth, v.clientHeight], ratio: window.__dbgRatio, faces: faces.map(d => ({ score: d.detection.score, pose: HeadPose.estimate(d.landmarks.positions, false), box: d.detection.box })), holds: window.__holdSamples, hints: window.__hints }; }).then(value => JSON.stringify(value, null, 2)), errors); throw error; } await page.waitForFunction(() => window.__evaluations.some(row => row.target === 'right' && !row.ok)); assert((await page.locator('#cam-step').textContent()).includes('2/3'), 'front photo cannot pass the side gate'); await page.evaluate(() => window.__cameraFixture.set('C3_135.jpg')); await page.waitForFunction(() => document.querySelector('#cam-step').textContent.includes('3/3')); await page.waitForFunction(() => window.__evaluations.some(row => row.target === 'left' && !row.ok)); assert((await page.locator('#cam-step').textContent()).includes('3/3'), 'wrong-direction side cannot pass'); const sent = page.waitForRequest(r => r.url().endsWith('/api/skin-analyze')); await page.evaluate(() => window.__cameraFixture.set('C3_-135.jpg')); const body = (await sent).postDataJSON(); assert.equal(await page.locator('#scan-photo').count(), 0, 'live waiting does not display the captured face'); assert.equal(await page.locator('#scan-artwork').count(), 1, 'waiting retains original PSD head'); await page.waitForSelector('#s-report.active'); assert.equal(await page.locator('#p-analyzing.show').count(), 0, 'waiting is cleared after success'); for (const key of ['front', 'left', 'right']) { const row = body.capture_diagnostics.views[key]; assert.equal(row.mode, 'live'); assert.equal(row.direction_checked, true); assert(row.score >= .4 && row.luma >= 65 && row.luma <= 235); assert(Math.abs(row.roll) <= 15); } assert(Math.abs(body.capture_diagnostics.views.front.yaw) <= 15); assert(body.capture_diagnostics.views.left.yaw <= -30); assert(body.capture_diagnostics.views.right.yaw >= 30); assert(await page.evaluate(() => document.querySelector('#cam-video').srcObject === null), 'camera released after success'); assert.deepEqual(errors, []); console.log('PASS: actual local models + automatic three-view capture + final JPEG revalidation; front/wrong-direction frames blocked locally; all three approved samples recorded; camera released; no paid call or backend writes'); } finally { await browser.close(); }