82 lines
5.4 KiB
JavaScript
82 lines
5.4 KiB
JavaScript
'use strict';
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const vm = require('node:vm');
|
|
const path = require('node:path');
|
|
const source = fs.readFileSync(path.join(__dirname, '../js/app.js'), 'utf8');
|
|
function extract(name) { return source.split('/* ' + name + '-begin */')[1].split('/* ' + name + '-end */')[0]; }
|
|
const context = {};
|
|
vm.runInNewContext(extract('camera-outline-pure') + extract('camera-haptic-pure') + extract('camera-hint-stage-pure'), context);
|
|
for (const hint of ['请向左转头', '请向右转头']) {
|
|
assert.equal(context.captureStageHint(hint, 'front', false), '请正对屏幕');
|
|
assert.equal(context.captureStageHint(hint, 'front', true), '请正对屏幕');
|
|
assert.equal(context.captureStageHint(hint, 'right', false), '请正对屏幕');
|
|
assert.equal(context.captureStageHint(hint, 'right', true), hint);
|
|
}
|
|
assert.equal(context.captureStageHint('请靠近一点', 'front', false), '请靠近一点');
|
|
const geometry = { window: {} };
|
|
vm.runInNewContext(fs.readFileSync(path.join(__dirname, '../js/camera-geometry.js'), 'utf8'), geometry);
|
|
const original = JSON.stringify(geometry.window.CameraGeometry);
|
|
for (const row of geometry.window.CameraGeometry) {
|
|
const rounded = context.smoothCameraOutline(row.outline);
|
|
assert(rounded.includes('Q') && rounded.endsWith('Z'));
|
|
assert(!/NaN|Infinity/.test(rounded));
|
|
// Curve endpoints/control points remain within the exported contour bounds.
|
|
const oldNumbers = row.outline.match(/[-+]?\d*\.?\d+/g).map(Number);
|
|
const newNumbers = rounded.match(/[-+]?\d*\.?\d+/g).map(Number);
|
|
for (let axis = 0; axis < 2; axis++) {
|
|
const oldAxis = oldNumbers.filter((_, i) => i % 2 === axis), newAxis = newNumbers.filter((_, i) => i % 2 === axis);
|
|
assert(Math.min(...newAxis) >= Math.min(...oldAxis) - .001);
|
|
assert(Math.max(...newAxis) <= Math.max(...oldAxis) + .001);
|
|
assert(Math.abs(Math.min(...newAxis) - Math.min(...oldAxis)) <= 2);
|
|
assert(Math.abs(Math.max(...newAxis) - Math.max(...oldAxis)) <= 2);
|
|
}
|
|
}
|
|
assert.equal(JSON.stringify(geometry.window.CameraGeometry), original);
|
|
for (const value of [null, '', 'M--Z', 'M1,2L1,2Z', 'M0,0L0,0L0,0Z', 'M0,0Q1,2 3,4Z']) assert.equal(context.smoothCameraOutline(value), value);
|
|
for (const nav of [null, {}, { vibrate: false }, { vibrate: () => false }, { vibrate: () => { throw Error('denied'); } }]) assert.equal(context.captureHaptic(nav), false);
|
|
let pattern;
|
|
assert(context.captureHaptic({ vibrate: p => { pattern = Array.from(p); return true; } }));
|
|
assert.deepEqual(pattern, [70, 40, 70]);
|
|
|
|
// Exercise the actual readiness state machine; only camera/pose inputs and clock
|
|
// are stubs; the relaxed shared hold policy now requires 450ms/2 frames.
|
|
let now = 10000, buzzes = 0, captures = 0;
|
|
const state = {
|
|
missFrames: 0, MISS_FRAMES_MAX: 3, camVideo: { videoHeight: 600 }, FACE_NEAR: .30, FACE_FAR: .68,
|
|
faceOffsetX: () => 0, CENTER_TOL: .18, CAM_STEPS: [{ target: 'front' }], camStep: 0,
|
|
HeadPose: { check: () => ({ ok: true }) }, sideYawFlip: false, faceLuma: () => 100,
|
|
LUMA_DARK: 65, LUMA_BRIGHT: 235, window: {}, setMask: () => {}, setHint: () => {},
|
|
okSince: 0, readyHapticAt: 0, OK_HOLD_MS: 450, Date: { now: () => now },
|
|
holdState: null, CaptureQuality: {
|
|
evaluate: d => ({ ok: d.detection.box.height >= 100, hint: '保持不动', observation: { yaw: 0, pitch: 0, center_x: .5, center_y: .5, ratio: .5 } }),
|
|
hold: require('../js/capture-quality').hold
|
|
},
|
|
navigator: { vibrate: () => { buzzes++; return true; } }, doCapture: () => { captures++; }
|
|
};
|
|
vm.runInNewContext(extract('camera-haptic-pure') + source.slice(source.indexOf(' function onDetect(d)'), source.indexOf(' /* ─── 拍照流程')), state);
|
|
const face = { detection: { box: { height: 250 } }, landmarks: { positions: [] } };
|
|
state.onDetect(face); assert.equal(buzzes, 1); assert.equal(captures, 0);
|
|
now += 200; state.onDetect(face); assert.equal(buzzes, 1); assert.equal(captures, 0);
|
|
now += 250; state.onDetect(face); assert.equal(buzzes, 1); assert.equal(captures, 1);
|
|
now += 100; state.onDetect({ ...face, detection: { box: { height: 20 } } });
|
|
now += 100; state.onDetect(face); assert.equal(buzzes, 1, 'jitter must not repeatedly buzz');
|
|
now += 2100; state.onDetect({ ...face, detection: { box: { height: 20 } } });
|
|
state.onDetect(face); assert.equal(buzzes, 2);
|
|
state.navigator.vibrate = () => { throw Error('blocked'); };
|
|
now += 300; state.onDetect(face);
|
|
now += 150; state.onDetect(face); assert.equal(captures, 2, 'vibration failure must not prevent capture');
|
|
for (const manual of [true, false]) {
|
|
let manualBuzzes = 0, scheduled = [];
|
|
const shutter = { shutterLock: false, camLive: true, capturing: false, camSeq: 1,
|
|
isManualMode: () => manual, navigator: { vibrate: () => { manualBuzzes++; throw Error('blocked'); } },
|
|
clearTimeout: () => {}, nudgeTimer: null, hideNudge: () => {},
|
|
$: () => ({ classList: { add: () => {} } }), setTimeout: (_, ms) => scheduled.push(ms) };
|
|
vm.runInNewContext(extract('camera-haptic-pure') + source.slice(source.indexOf(' function doCapture()'), source.indexOf(' // 三张拍完:按视角重排')), shutter);
|
|
shutter.doCapture();
|
|
assert.equal(manualBuzzes, manual ? 1 : 0, 'auto must not double buzz; manual must retain its cue');
|
|
assert(shutter.shutterLock && shutter.capturing);
|
|
assert.deepEqual(scheduled, [300], 'blocked haptic must still schedule camera capture');
|
|
}
|
|
console.log('PASS: three bounded rounded contours; haptic safety, pre-capture cue, jitter throttling; relaxed 450ms/2-frame hold');
|