Files
juzi/h5/test/customer-feedback-second.js
T

131 lines
9.1 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 = p; return true; } }));
assert.equal(pattern, 30);
// 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 = {
captureDiagnostics: [],
missFrames: 0, MISS_FRAMES_MAX: 3, FRONT_MISS_FRAMES_MAX: 6, frontFaceSeen:false, 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, OK_HOLD_MS: 450, Date: { now: () => now },
shutterLock: false, capturing: false, camBusy: false, camLive: true, currentScreen: 's-cam', document: { hidden: false },
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, 0); assert.equal(captures, 0);
now += 200; state.onDetect(face); assert.equal(buzzes, 0); assert.equal(captures, 0);
now += 250; state.onDetect(face); assert.equal(buzzes, 0); assert.equal(captures, 1);
now += 100; state.onDetect({ ...face, detection: { box: { height: 20 } } });
now += 100; state.onDetect(face); assert.equal(buzzes, 0, 'good angles must not buzz');
now += 2100; state.onDetect({ ...face, detection: { box: { height: 20 } } });
state.onDetect(face); assert.equal(buzzes, 0);
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,
currentScreen:'s-cam', VOICE:{countdown:'countdown-321'},
voice:{dropHint:()=>{},play:(_,opts)=>opts.onStart(3000),afterCurrent:cb=>cb()},
startCaptureCountdown:(_,duration,done)=>done(),
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, 0, 'neither auto nor manual shutter should buzz');
assert(shutter.shutterLock && shutter.capturing);
assert.deepEqual(scheduled, [300], 'blocked haptic must still schedule camera capture');
}
// Drive actual onDetect with angle-only failures, not UI hint string matching.
const events = [];
state.navigator.vibrate = p => { events.push({ p, at: now }); return true; };
state.CaptureQuality.evaluate = () => ({ ok: false, reason: 'angle', hint: '请正对屏幕' });
state.onDetect(face);
now += 999; state.onDetect(face); assert.equal(events.length, 0);
now += 1; state.onDetect(face); assert.equal(events.length, 1); assert.equal(events[0].p, 30);
for (let i = 0; i < 3; i++) { now += 1000; state.onDetect(face); }
assert.equal(events.length, 1, 'no repeat before 4 seconds');
now += 1000; state.onDetect(face); assert.equal(events.length, 2);
state.onDetect(null); assert.equal(events.at(-1).p, 0, 'missing face cancels vibration immediately');
const positives = () => events.filter(e => e.p > 0).length;
state.onDetect(face); now += 999; state.onDetect(face); assert.equal(positives(), 2, 'new continuous interval required');
state.CaptureQuality.evaluate = () => ({ ok: false, reason: 'quality', hint: '请靠近一点' });
now += 10000; state.onDetect(face); assert.equal(positives(), 2, 'distance/lighting failure does not buzz');
state.CaptureQuality.evaluate = () => ({ ok: false, reason: 'angle', hint: '请正对屏幕' });
state.onDetect(face); now += 1000; state.onDetect(face); assert.equal(positives(), 3);
state.CaptureQuality.evaluate = () => ({ ok: false, reason: 'quality', hint: '请到光线充足处' });
state.onDetect(face); assert.equal(events.at(-1).p, 0, 'valid angle cancels even when lighting fails');
for (const [key, value] of [['camBusy', true], ['shutterLock', true], ['capturing', true], ['camLive', false], ['currentScreen', 's-report']]) {
const previous = state[key]; state[key] = value;
state.CaptureQuality.evaluate = () => ({ ok: false, reason: 'angle' });
state.onDetect(face); now += 5000; state.onDetect(face);
assert.equal(positives(), 3, key + ' blocks reminders'); state[key] = previous;
}
state.document.hidden = true; state.onDetect(face); now += 5000; state.onDetect(face); assert.equal(positives(), 3);
state.document.hidden = false;
for (const nav of [{}, { vibrate: () => false }, { vibrate: () => { throw Error('blocked'); } }]) {
state.navigator = nav; state.resetAngleHaptic();
state.onDetect(face); now += 1000; assert.doesNotThrow(() => state.onDetect(face));
}
const missingHints=[];state.setHint=text=>missingHints.push(text);
state.resetAngleHaptic();state.frontFaceSeen=false;state.missFrames=0;state.CAM_STEPS[0].target='front';
const priorCaptures=captures;
for(let i=0;i<5;i++)state.onDetect(null);
assert.equal(missingHints.length,0,'initial front misses are quiet for five detections');
state.onDetect(null);assert.equal(missingHints.at(-1),'未检测到人脸');
missingHints.length=0;state.missFrames=0;state.frontFaceSeen=true;
state.onDetect(null);state.onDetect(null);assert.equal(missingHints.length,0);
state.onDetect(null);assert.equal(missingHints.at(-1),'未检测到人脸','after seeing a front face retain normal missing detection');
missingHints.length=0;state.missFrames=0;state.frontFaceSeen=false;state.CAM_STEPS[0].target='right';
state.onDetect(null);state.onDetect(null);assert.equal(missingHints.length,0);
state.onDetect(null);assert.equal(missingHints.at(-1),'未检测到人脸','side missing threshold unchanged');
assert.equal(captures,priorCaptures,'warmup never permits capture without a face');
console.log('PASS: initial-front missing tolerance6 vs normal3, no empty capture; angle haptics/lifecycle and unchanged 450ms/2-frame hold');