117 lines
8.1 KiB
JavaScript
117 lines
8.1 KiB
JavaScript
// 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(); }
|