'use strict'; const { test } = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'), path = require('node:path'), vm = require('node:vm'); const root = path.resolve(__dirname, '..'); const file = path.join(root, 'pages/camnative/camnative.js'); function setup(options = {}) { let definition, clock = 1000; const timers = [], contexts = [], photos = [], renderCallbacks = []; const platform = { createCameraContext(id) { const record = { id, starts: [], stops: [], callback: null }; record.context = { onCameraFrame(callback) { record.callback = callback; return { start(value) { record.starts.push(value); if (options.startSuccess) value.success(); }, stop(value) { record.stops.push(value); } }; }, takePhoto(value) { photos.push(value); } }; if (options.noFrames) delete record.context.onCameraFrame; contexts.push(record); return record.context; }, ...options.my }; vm.runInNewContext(fs.readFileSync(file, 'utf8'), { Page(value) { definition = value; }, my: platform, Date: { now: () => clock }, require(name) { if (name === '../../utils/ar-camera') return require('../utils/ar-camera'); if (name === '../../utils/development') return { nativeFaceProbeEnabled: () => true }; throw Error('Probe must not access accounts, uploads or external services: ' + name); }, setTimeout(fn, delay) { const row = { fn, delay, active: true }; timers.push(row); return row; }, clearTimeout(row) { if (row) row.active = false; } }); const page = Object.assign({}, definition, { data: JSON.parse(JSON.stringify(definition.data)), setData(value, callback) { Object.assign(this.data, value); if (callback) { if (options.deferRender) renderCallbacks.push(callback); else callback(); } } }); page.onLoad(); page.onShow(); page.onReady(); return { page, contexts, photos, timers, renderCallbacks, timer(delay) { const row = timers.find(value => value.active && value.delay === delay); assert(row, 'missing timer ' + delay); row.active = false; clock += delay; row.fn(); }, advance(ms) { clock += ms; }, event(session) { return { currentTarget: { id: 'native-camera-' + session, dataset: { cameraSession: session } } }; } }; } test('copy toolbar and initial version survive missing system geometry and a tall status bar', () => { const fallback = setup(); assert.equal(fallback.page.data.probeVersion, 'CAM-NATIVE-20261102-04'); assert.equal(fallback.page.data.toolbarTop, 112); const notched = setup({ my: { getSystemInfoSync: () => ({ statusBarHeight: 64, safeArea: { top: 68 }, titleBarHeight: 48 }) } }); assert(notched.page.data.toolbarTop >= 68 + 48 + 12); const failed = setup({ my: { getSystemInfoSync() { throw Error('unavailable'); } } }); assert.equal(failed.page.data.toolbarTop, 112); }); function raw() { return { width: 2, height: 2, data: new Uint8Array(16), timestamp: 123 }; } test('native diagnostic requests front, remounts with unique IDs, and rejects previous component events', () => { const f = setup(); assert.equal(f.page.data.devicePosition, 'front'); assert.equal(f.contexts[0].id, 'native-camera-1'); f.page.onFlip(); assert.equal(f.page.data.devicePosition, 'back'); assert.equal(f.contexts[1].id, 'native-camera-2'); assert.equal(f.contexts[0].stops.length, 1); f.contexts[0].callback(raw()); f.page.onNativeReady(f.event(1)); f.page.onNativeError(f.event(1)); f.page.onNativeStop(f.event(1)); assert.equal(f.page.data.frameCount, 0); assert.equal(f.page.data.nativeReady, false); assert.equal(f.page.data.cameraEnabled, true); f.page.onNativeReady(f.event(2)); assert.equal(f.page.data.nativeReady, true); }); test('listener start success never masquerades as a frame; 3s and 10s deadlines stop an empty stream', () => { const f = setup({ startSuccess: true }); assert.match(f.page.data.startStatus, /调用成功/); assert.equal(f.page.data.frameCount, 0); f.timer(3000); assert.match(f.page.data.frameStatus, /3秒未收到/); f.timer(10000); assert.match(f.page.data.frameStatus, /10秒仍未收到/); assert.equal(f.contexts[0].stops.length, 1); f.contexts[0].callback(raw()); assert.equal(f.page.data.frameCount, 0); }); test('only dimensions plus real bytes count as raw frames, metadata never retains pixel contents, stop discards late callbacks', () => { const f = setup(); f.contexts[0].callback({ width: 2, height: 2, data: 'not-confirmed-raw' }); assert.equal(f.page.data.frameCount, 0); assert.equal(f.page.data.invalidFrameCount, 1); f.advance(400); f.contexts[0].callback(raw()); assert.equal(f.page.data.frameCount, 1); assert.equal(f.page.data.frameCallbacks, 2); assert.match(f.page.data.frameInfo, /"byteLength":16/); assert(!f.page.data.frameInfo.includes('not-confirmed-raw')); assert(!f.timers.some(row => row.active && (row.delay === 3000 || row.delay === 10000))); assert.equal(f.page.latestFrame, undefined); f.page.onStopFrames(); f.contexts[0].callback(raw()); assert.equal(f.page.data.frameCount, 1); assert.equal(f.contexts[0].stops.length, 1); }); test('deferred mount callbacks cannot create an old camera after hide or replace a newer session', () => { const f = setup({ deferRender: true }); const staleMount = f.renderCallbacks.shift(); f.page.onHide(); f.page.onShow(); const currentMount = f.renderCallbacks.shift(); staleMount(); assert.equal(f.contexts.length, 0); currentMount(); assert.equal(f.contexts.length, 1); assert.equal(f.contexts[0].id, 'native-camera-2'); f.page.onHide(); f.contexts[0].starts[0].success(); f.contexts[0].callback(raw()); assert.equal(f.page.data.frameCount, 0); assert.equal(f.page.data.cameraEnabled, false); }); test('native photo exports stay serialized across hide/show and old callbacks cannot publish private previews', () => { const f = setup(); f.page.onTakePhoto(); f.page.onTakePhoto(); f.page.onFlip(); assert.equal(f.photos.length, 1); assert.equal(f.page.data.devicePosition, 'front'); f.page.onHide(); f.page.onShow(); f.page.onTakePhoto(); assert.equal(f.photos.length, 1); assert.equal(f.page.data.photoBusy, true); f.photos[0].success({ tempImagePath: 'old-hidden-photo' }); assert.equal(f.page.data.photoPath, ''); assert.equal(f.page.data.photoBusy, false); f.page.onTakePhoto(); assert.equal(f.photos.length, 2); f.photos[0].complete(); assert.equal(f.page.data.photoBusy, true); f.photos[1].success({ tempImagePath: 'current-local-photo' }); f.photos[1].complete(); assert.equal(f.page.data.photoPath, 'current-local-photo'); assert.equal(f.page.data.photoBusy, false); f.page.onClearPhoto(); assert.equal(f.page.data.photoPath, ''); }); test('photo timeout does not unlock a noncancelable native export, and unload suppresses its final callback', () => { const f = setup(); f.page.onStopFrames(); f.page.onTakePhoto(); f.timer(10000); assert.match(f.page.data.photoStatus, /10秒未返回/); assert.equal(f.page.data.photoBusy, true); f.page.onTakePhoto(); assert.equal(f.photos.length, 1); f.page.onUnload(); f.photos[0].success({ tempImagePath: 'late-photo' }); f.photos[0].complete(); assert.equal(f.page.data.photoPath, ''); assert(!f.timers.some(row => row.active)); }); test('a native export that times out in the background remains locked and reports timeout on return', () => { const f = setup(); f.page.onStopFrames(); f.page.onTakePhoto(); f.page.onHide(); f.timer(10000); f.page.onShow(); assert.equal(f.page.data.photoBusy, true); assert.match(f.page.data.photoStatus, /10秒未返回/); f.page.onTakePhoto(); assert.equal(f.photos.length, 1); f.photos[0].fail({ errorMessage: '原操作已结束' }); assert.equal(f.page.data.photoBusy, false); assert.equal(f.page.data.photoPath, ''); }); test('missing runtime APIs report an explicit failure, and diagnostic source has no business or network dependency', () => { const missing = setup({ my: { createCameraContext: undefined } }); assert.match(missing.page.data.contextStatus, /缺少 my.createCameraContext/); const noFrames = setup({ noFrames: true }); assert.match(noFrames.page.data.frameStatus, /缺少 onCameraFrame/); noFrames.page.onTakePhoto(); assert.equal(noFrames.photos.length, 1); const source = fs.readFileSync(file, 'utf8'); assert.doesNotMatch(source, /\bgetApp\(|my\.(?:request|uploadFile|setStorage|setStorageSync|chooseImage)\b/); const markup = fs.readFileSync(path.join(root, 'pages/camnative/camnative.axml'), 'utf8'); assert.match(markup, /device-position="{{devicePosition}}"/); for (const match of markup.matchAll(/\bon[A-Z]\w*="(\w+)"/g)) assert.equal(typeof noFrames.page[match[1]], 'function', match[1]); }); test('copy includes fresh scalar counters and only permitted metadata, never paths, pixels, coordinates or native error text', () => { const copied = []; const f = setup({ startSuccess: true, my: { SDKVersion: '2.8.0', getSystemInfoSync: () => ({ brand: 'TestPhone', system: 'Android 15', version: '11.2.0', userId: 'PRIVATE_ACCOUNT', token: 'PRIVATE_TOKEN' }), setClipboard(value) { copied.push(value.text); value.success(); } } }); assert.equal(copied.length, 0); assert.equal(f.page.data.diagnosticText, ''); for (let index = 0; index < 8; index++) f.contexts[0].callback({ ...raw(), pixelFormat: 'rgba', format: 'PRIVATE_NATIVE_FORMAT', face: { points: ['PRIVATE_FACE_POINTS'] }, private: 'PRIVATE_FRAME_FIELD' }); // UI updates are throttled; an explicit report still reads all eight callbacks. assert.equal(f.page.data.frameCount, 1); f.page.onTakePhoto(); f.photos[0].success({ tempImagePath: 'PRIVATE_PHOTO_PATH', private: 'PRIVATE_PHOTO_METADATA' }); f.page.setData({ cameraStatus: 'PRIVATE_CAMERA_ERROR', frameInfo: 'PRIVATE_UNSAFE_FRAME_INFO' }); f.page.onStopFrames(); f.contexts[0].stops[0].fail({ errorCode: 7, errorMessage: 'PRIVATE_ERROR_TEXT', code: 'PRIVATE_OTHER_CODE' }); f.page.onCopyDiagnostics(); assert.equal(copied.length, 1); const report = copied[0]; assert.match(report, /^CAM-NATIVE-20261102-04/); assert.match(report, /system.brand=TestPhone/); assert.match(report, /runtime.SDKVersion=2.8.0/); assert.match(report, /rawFrames=8/); assert.match(report, /frame.width=2; frame.height=2; frame.byteLength=16/); assert.match(report, /frame.pixelFormat=RGBA; frame.format=unrecognized/); assert.match(report, /context.takePhoto=true; context.onCameraFrame=true/); assert.match(report, /listener.start.state=success/); assert.match(report, /listener.stop.state=failure; listener.stop.code=7/); assert.match(report, /requestedPosition=front/); assert.match(report, /actualPosition=not_verified/); assert.match(report, /photo.success=true; photo.returnedPath=true; photo.previewPresent=true/); assert.doesNotMatch(report, /PRIVATE_|tempImagePath|landmark|points|timestamp/); assert.equal(f.page.data.diagnosticText, report); assert.match(f.page.data.copyStatus, /已复制/); }); test('generating selectable diagnostics and missing clipboard fallback have no camera, capture or clipboard side effects', () => { const f = setup(); const contexts = f.contexts.length, starts = f.contexts[0].starts.length, timers = f.timers.length; assert.equal(f.page.data.diagnosticText, ''); f.page.onGenerateDiagnostics(); assert.match(f.page.data.diagnosticText, /^CAM-NATIVE-20261102-04/); assert.match(f.page.data.copyStatus, /可长按/); f.page.onCopyDiagnostics(); assert.match(f.page.data.copyStatus, /缺少剪贴板 API/); assert.equal(f.contexts.length, contexts); assert.equal(f.contexts[0].starts.length, starts); assert.equal(f.contexts[0].stops.length, 0); assert.equal(f.photos.length, 0); assert.equal(f.timers.length, timers); const markup = fs.readFileSync(path.join(root, 'pages/camnative/camnative.axml'), 'utf8'); assert.match(markup, /class="native-toolbar(?:\s|")/); assert.match(markup, /onTap="onGenerateDiagnostics"/); assert.match(markup, /selectable="{{true}}"/); const css = fs.readFileSync(path.join(root, 'pages/camnative/camnative.acss'), 'utf8'); assert.match(css, /\.native-toolbar\s*\{[^}]*position:fixed/); const height = Number(/\.native-toolbar\s*\{[^}]*height:(\d+)rpx/.exec(css)[1]); const padding = Number(/\.native-test\s*\{\s*padding:(\d+)rpx/.exec(css)[1]); assert(padding > height, 'fixed toolbar must leave room above the page title'); }); test('clipboard failure and synchronous throws keep selectable safe text, and arbitrary string error codes are withheld', () => { const f = setup({ my: { setClipboard(value) { value.fail({ errorMessage: 'PRIVATE_CLIPBOARD_ERROR' }); } } }); f.contexts[0].starts[0].fail({ code: 'PRIVATE_CODE', errorMessage: 'PRIVATE_NATIVE_ERROR' }); f.page.onCopyDiagnostics(); assert.match(f.page.data.copyStatus, /复制失败/); assert.match(f.page.data.diagnosticText, /listener.start.state=failure; listener.start.code=unavailable/); assert.doesNotMatch(f.page.data.diagnosticText + f.page.data.copyStatus, /PRIVATE_/); const throwing = setup({ my: { setClipboard() { throw new Error('PRIVATE_THROW'); } } }); throwing.page.onCopyDiagnostics(); assert.match(throwing.page.data.copyStatus, /复制失败/); assert.match(throwing.page.data.diagnosticText, /^CAM-NATIVE-20261102-04/); assert.doesNotMatch(throwing.page.data.copyStatus, /PRIVATE_/); });