122 lines
7.9 KiB
JavaScript
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(); }
|