Files
juzi/h5/test/camera-flip-browser.mjs
T

122 lines
7.9 KiB
JavaScript

// Canvas cameras and intercepted auth; no personal images, writes or paid API.
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { execSync } from 'node:child_process';
import { fileURLToPath, pathToFileURL } from 'node:url';
const { chromium } = await import(pathToFileURL(execSync('npm root -g', { encoding: 'utf8' }).trim() + '/playwright/index.mjs').href);
const browser = await chromium.launch({ args: ['--autoplay-policy=no-user-gesture-required'] });
try {
const page = await browser.newPage({ viewport: { width: 375, height: 726 } });
const errors = [];
page.on('pageerror', e => errors.push(e.message));
await page.route('**/api/**', route => {
assert.equal(route.request().method(), 'GET', 'no account/report writes');
return route.fulfill({ json: { ok: true, user: { id: 'camera-fixture', kind: 'browser' } } });
});
const source = fs.readFileSync(fileURLToPath(new URL('../js/app.js', import.meta.url)), 'utf8');
await page.route('**/js/app.js*', route => route.fulfill({ contentType: 'text/javascript', body: source.replace(/\}\)\(\);\s*$/, `
window.__cameraTest = {
enter: function() { showScreen('s-cam'); startCamera(); renderCamStep(); },
stop: stopCamera,
capture: function(value) { shutterLock = value; },
startDetection: function() { modelsOk = true; detectDisabled = false; IS_MOCK = false; maybeStartDetect(); },
state: function() { return { live: camLive, busy: camBusy, facing: camFacing, fallback: camFallback }; }
};
})();`) }));
await page.addInitScript(() => {
const nativeTimeout = window.setTimeout.bind(window);
window.setTimeout = (fn, ms, ...args) => nativeTimeout(fn, window.__shortCameraTimeout && ms === 12000 ? 200 : ms, ...args);
window.__calls = []; window.__delay = 0; window.__fail = false; window.__ignoreFacing = false;
window.__streams = []; window.__frameAt = 0;
navigator.mediaDevices.getUserMedia = async constraints => {
const facing = constraints.video.facingMode.ideal;
window.__calls.push(facing);
const fail = window.__fail, delay = window.__delay;
window.__fail = false;
await new Promise(r => setTimeout(r, delay));
if (fail) throw new Error('fixture camera unavailable');
const canvas = document.createElement('canvas'); canvas.width = 640; canvas.height = 480;
const ctx = canvas.getContext('2d');
ctx.fillStyle = facing === 'user' ? '#316a94' : '#d18e48'; ctx.fillRect(0, 0, 640, 480);
const stream = canvas.captureStream(20), track = stream.getVideoTracks()[0];
const settings = track.getSettings.bind(track);
track.getSettings = () => ({ ...settings(), facingMode: window.__ignoreFacing ? 'user' : facing });
const timer = setInterval(() => ctx.fillRect(0, 0, 640, 480), 40);
const stop = track.stop.bind(track); track.stop = () => { clearInterval(timer); stop(); };
window.__streams.push(stream);
return stream;
};
document.addEventListener('DOMContentLoaded', () => {
document.querySelector('#cam-video').addEventListener('loadeddata', () => { window.__frameAt = performance.now(); });
});
});
await page.goto((process.env.H5_BASE_URL || 'http://127.0.0.1:8080/') + '?mock=1');
await page.evaluate(() => window.__cameraTest.enter());
const live = () => page.waitForFunction(() => window.__cameraTest.state().live);
await live();
for (const delay of [0, 1000]) {
await page.evaluate(delay => { window.__delay = delay; document.querySelector('.cam-flip').click(); }, delay);
assert(await page.locator('#cam-frozen').isVisible(), 'frozen preview during switching');
const count = await page.evaluate(() => window.__calls.length);
await page.evaluate(() => document.querySelector('.cam-flip').click());
assert.equal(await page.evaluate(() => window.__calls.length), count, 'rapid click ignored');
await live();
const extra = await page.evaluate(() => performance.now() - window.__frameAt);
assert(extra < 500, `first-frame to visible latency ${extra}ms, no fixed 900ms wait`);
assert(!(await page.locator('#cam-frozen').isVisible()));
console.log('flip delay', delay, 'first-frame to visible', Math.round(extra), 'ms');
}
const before = await page.evaluate(() => window.__cameraTest.state().facing);
await page.evaluate(() => { window.__delay = 0; window.__fail = true; document.querySelector('.cam-flip').click(); });
await live();
assert.equal(await page.evaluate(() => window.__cameraTest.state().facing), before, 'failed flip restores camera');
assert.equal(await page.evaluate(() => window.__cameraTest.state().fallback), false);
await page.evaluate(() => { window.__ignoreFacing = true; document.querySelector('.cam-flip').click(); });
await live();
assert.equal(await page.evaluate(() => window.__cameraTest.state().facing), before, 'ignored facing is detected/restored');
await page.evaluate(() => {
window.__ignoreFacing = false; window.__shortCameraTimeout = true; window.__delay = 600;
document.querySelector('.cam-flip').click();
window.__delay = 0; // restoration succeeds while the original request is still pending
});
await live();
assert.equal(await page.evaluate(() => window.__cameraTest.state().facing), before, 'timeout restores original camera');
await page.waitForTimeout(700);
assert(await page.evaluate(() => window.__streams.slice(0, -2).every(s => s.getTracks().every(t => t.readyState === 'ended'))));
await page.evaluate(() => { window.__shortCameraTimeout = false; });
const count = await page.evaluate(() => window.__calls.length);
await page.evaluate(() => { window.__cameraTest.capture(true); document.querySelector('.cam-flip').click(); window.__cameraTest.capture(false); });
assert.equal(await page.evaluate(() => window.__calls.length), count, 'capture blocks flip');
// Pending inference from the old camera must neither update UI nor fork loops.
await page.evaluate(() => {
window.__detectCalls = 0; window.__concurrent = 0; window.__maxConcurrent = 0;
window.faceapi = {
nets: { tinyFaceDetector: { loadFromUri: () => Promise.resolve() }, faceLandmark68Net: { loadFromUri: () => Promise.resolve() } },
TinyFaceDetectorOptions: function() {},
detectAllFaces: function() { return { withFaceLandmarks: function() {
window.__detectCalls++; window.__concurrent++;
window.__maxConcurrent = Math.max(window.__maxConcurrent, window.__concurrent);
return new Promise(resolve => { window.__resolveDetect = () => { window.__concurrent--; resolve([]); }; });
} }; }
};
window.__cameraTest.startDetection();
});
await page.waitForFunction(() => window.__detectCalls === 1);
await page.evaluate(() => { window.__ignoreFacing = false; document.querySelector('.cam-flip').click(); });
await live();
await page.waitForTimeout(600);
assert.equal(await page.evaluate(() => window.__detectCalls), 1, 'do not overlap old inference');
await page.evaluate(() => window.__resolveDetect());
await page.waitForFunction(() => window.__detectCalls === 2);
await page.evaluate(() => window.__resolveDetect());
await page.waitForFunction(() => window.__detectCalls === 3);
assert.equal(await page.evaluate(() => window.__maxConcurrent), 1, 'single detection loop');
await page.evaluate(() => { window.__delay = 600; document.querySelector('.cam-flip').click(); window.__cameraTest.stop(); });
await page.waitForTimeout(900);
assert.equal(await page.evaluate(() => window.__cameraTest.state().live), false, 'late stream cannot revive stopped camera');
assert(await page.evaluate(() => window.__streams.every(s => s.getTracks().every(t => t.readyState === 'ended'))), 'all streams released');
assert.equal(await page.evaluate(() => document.querySelector('#cam-frozen').width), 0, 'frozen face released');
assert.deepEqual(errors, []);
console.log('PASS camera flip: first frame, frozen preview, rapid clicks, recovery/timeout, capture lock, stale inference, cleanup');
} finally { await browser.close(); }