fix(h5): center score stack, lock profile scroll and gate turn guidance
This commit is contained in:
@@ -6,7 +6,7 @@ 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 });
|
||||
const browser = await chromium.launch({ headless: true, args: ['--autoplay-policy=no-user-gesture-required'] });
|
||||
try {
|
||||
const page = await browser.newPage({ viewport: { width: 375, height: 726 } });
|
||||
page.setDefaultTimeout(60000);
|
||||
@@ -19,6 +19,37 @@ try {
|
||||
await page.route('**/sample-' + name, route => route.fulfill({ contentType: 'image/jpeg', body }));
|
||||
}
|
||||
await page.addInitScript(() => {
|
||||
// Record actual decoded audio starts, not preload requests. Include the
|
||||
// visible capture step so an early "turn left" instruction is detectable.
|
||||
const ids = new WeakMap(), buffers = new WeakMap(), originalFetch = window.fetch;
|
||||
window.__voice = [];
|
||||
window.fetch = function(url, options) {
|
||||
const match = String(url).match(/voice\/([a-z0-9-]+)\.mp3/);
|
||||
return originalFetch.call(this, url, options).then(response => {
|
||||
if (match) {
|
||||
const original = response.arrayBuffer.bind(response);
|
||||
response.arrayBuffer = () => original().then(data => { ids.set(data, match[1]); return data; });
|
||||
}
|
||||
return response;
|
||||
});
|
||||
};
|
||||
const OriginalAudioContext = window.AudioContext || window.webkitAudioContext;
|
||||
function AudioRecorder(...args) {
|
||||
const context = new OriginalAudioContext(...args), decode = context.decodeAudioData.bind(context);
|
||||
context.decodeAudioData = (data, ok, error) => decode(data, buffer => { buffers.set(buffer, ids.get(data)); if (ok) ok(buffer); }, error);
|
||||
const create = context.createBufferSource.bind(context);
|
||||
context.createBufferSource = () => {
|
||||
const source = create(), start = source.start.bind(source);
|
||||
source.start = (...args) => {
|
||||
window.__voice.push({ id: buffers.get(source.buffer), at: Date.now(), duration: source.buffer.duration * 1000, step: document.querySelector('#cam-step').textContent });
|
||||
return start(...args);
|
||||
};
|
||||
return source;
|
||||
};
|
||||
return context;
|
||||
}
|
||||
AudioRecorder.prototype = OriginalAudioContext.prototype;
|
||||
window.AudioContext = window.webkitAudioContext = AudioRecorder;
|
||||
const canvas = document.createElement('canvas'); canvas.width = 800; canvas.height = 600;
|
||||
let currentImage, offsetX = 0;
|
||||
function draw() {
|
||||
@@ -39,7 +70,7 @@ try {
|
||||
currentImage = image; offsetX = nextOffset; draw();
|
||||
} };
|
||||
navigator.mediaDevices.getUserMedia = async () => {
|
||||
await window.__cameraFixture.set('C3_180.jpg');
|
||||
await window.__cameraFixture.set('C3_135.jpg'); // invalid front: must not prompt a next-step turn
|
||||
return canvas.captureStream(10);
|
||||
};
|
||||
});
|
||||
@@ -78,6 +109,12 @@ try {
|
||||
};
|
||||
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 });
|
||||
});
|
||||
await page.evaluate(() => window.__cameraFixture.set('C3_135.jpg'));
|
||||
await page.waitForFunction(() => window.__evaluations.some(row => row.target === 'front' && !row.ok && row.hint === '请正对屏幕'));
|
||||
await page.waitForFunction(() => window.__voice.some(row => row.id === 'hint-front'));
|
||||
assert((await page.locator('#cam-step').textContent()).includes('1/3'), 'invalid frontal angle never advances');
|
||||
assert.equal(await page.evaluate(() => window.__voice.some(row => /^hint-turn-|^step[23]-|^shot-done$/.test(row.id))), false, 'no turn/next-step/completion audio before a successful front photo');
|
||||
await page.evaluate(() => window.__cameraFixture.set('C3_180.jpg'));
|
||||
try { await page.waitForFunction(() => document.querySelector('#cam-step').textContent.includes('2/3')); }
|
||||
catch (error) {
|
||||
console.log(await page.evaluate(async () => {
|
||||
@@ -88,6 +125,11 @@ try {
|
||||
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.__voice.some(row => row.id === 'step2-rightface'));
|
||||
const firstVoice = await page.evaluate(() => window.__voice);
|
||||
const completion = firstVoice.find(row => row.id === 'shot-done'), turn = firstVoice.find(row => row.id === 'step2-rightface');
|
||||
assert(completion && turn && completion.step.includes('1/3') && turn.step.includes('2/3'));
|
||||
assert(turn.at >= completion.at + completion.duration - 100, 'turn instruction waits for captured-front completion speech to finish');
|
||||
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'));
|
||||
@@ -112,5 +154,5 @@ try {
|
||||
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');
|
||||
console.log('PASS: actual local models + automatic three-view capture + final JPEG revalidation; invalid front stays on step 1 with neutral audio, no early turn; actual decoded shot-done finishes before side guidance; wrong-direction frames blocked; approved samples/camera cleanup; no paid call/backend writes');
|
||||
} finally { await browser.close(); }
|
||||
|
||||
Reference in New Issue
Block a user