2026-10-04 16:21:40 +08:00
|
|
|
const test=require('node:test'),assert=require('node:assert/strict'),fs=require('node:fs'),vm=require('node:vm');
|
2026-10-05 20:23:05 +08:00
|
|
|
const code=fs.readFileSync('web/steward.js','utf8'),html=require('./test-support/ui-source')(),app=fs.readFileSync('web/app.js','utf8');
|
2026-10-04 16:21:40 +08:00
|
|
|
const part=(s,a,b)=>s.slice(s.indexOf(a),s.indexOf(b,s.indexOf(a)+a.length));
|
|
|
|
|
test('navigation starts at places and contains no global camera functions',()=>{
|
2026-10-04 20:37:14 +08:00
|
|
|
const nav=html.match(/<nav[^>]*aria-label="主导航">([\s\S]*?)<\/nav>/)[1];
|
2026-10-04 16:21:40 +08:00
|
|
|
assert.doesNotMatch(nav,/data-page="(?:live|playback)"/);
|
|
|
|
|
assert.match(nav,/scope-navigation/);assert.match(app,/activePage = 'households'/);
|
|
|
|
|
assert.match(html,/aria-label="设备功能"/);
|
|
|
|
|
});
|
|
|
|
|
test('one device exposes its lenses while place wall exposes all authorized lenses',()=>{
|
|
|
|
|
const c=vm.createContext({deviceContext:null});vm.runInContext(part(code,'function deviceCameras(','function enterDevice('),c);
|
|
|
|
|
const cameras=[{id:'1',assetId:'a'},{id:'2',assetId:'a'},{id:'3',assetId:'b'}];
|
|
|
|
|
assert.equal(c.deviceCameras(cameras).length,3);
|
|
|
|
|
assert.deepEqual(c.deviceCameras(cameras,{source:'camera',id:'a'}).map(x=>x.id),['1','2']);
|
|
|
|
|
assert.equal(c.deviceCameras(cameras,{source:'camera',id:'missing'}).length,0);
|
|
|
|
|
});
|
|
|
|
|
test('recorder relation is explicit and generic devices have no video operations',()=>{
|
|
|
|
|
const c=vm.createContext({deviceContext:null});vm.runInContext(part(code,'function deviceCameras(','function enterDevice('),c);
|
|
|
|
|
const cameras=[{id:'1',recorderId:'r'},{id:'2',archiveRecorderId:'r'},{id:'3',assetId:'r'}];
|
|
|
|
|
assert.equal(c.deviceCameras(cameras,{source:'recorder',id:'r'}).length,2);
|
|
|
|
|
assert.equal(c.deviceCameras(cameras,{source:'device',id:'r'}).length,0);
|
|
|
|
|
});
|
|
|
|
|
test('switching devices cancels prior playback before changing context',()=>{
|
|
|
|
|
const order=[],c=vm.createContext({state:{cameras:[]},ZhaoDevices:{catalog:()=>[{source:'camera',id:'a'}]},
|
|
|
|
|
$:()=>({value:''}),stopPlayers:()=>order.push('stop'),resetPlayback:()=>order.push('reset'),closePtz:()=>order.push('ptz'),
|
|
|
|
|
deviceCameras:()=>[{}],showPage:page=>order.push(page),toast:()=>{throw Error('not expected');}});
|
|
|
|
|
vm.runInContext(part(code,'function enterDevice(','function renderSteward('),c);c.enterDevice('camera','a');
|
|
|
|
|
assert.deepEqual(order,['stop','reset','ptz','live']);assert.equal(c.deviceContext.id,'a');
|
|
|
|
|
});
|
|
|
|
|
test('an unknown or unauthorized device cannot open a player',()=>{
|
|
|
|
|
let warned=0;const c=vm.createContext({state:{},ZhaoDevices:{catalog:()=>[]},toast:()=>warned++,stopPlayers:()=>{throw Error('must not open');}});
|
|
|
|
|
vm.runInContext(part(code,'function enterDevice(','function renderSteward('),c);c.enterDevice('camera','foreign');assert.equal(warned,1);
|
|
|
|
|
});
|
|
|
|
|
test('late heritage response cannot populate a different view',async()=>{
|
|
|
|
|
let finish,writes=0;const c=vm.createContext({heritageGeneration:0,heritageKind:'culture',activePage:'heritage-records',
|
|
|
|
|
$:()=>({replaceChildren(){},hidden:false,textContent:''}),api:()=>new Promise(r=>finish=r),renderHeritageRecords:()=>writes++});
|
|
|
|
|
const source=fs.readFileSync('web/heritage.js','utf8');
|
|
|
|
|
vm.runInContext(part(source,'async function loadHeritage(','function renderHeritageRecords('),c);
|
|
|
|
|
const pending=c.loadHeritage();c.heritageGeneration++;c.activePage='households';finish({entries:['private']});await pending;assert.equal(writes,0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('family timeline entry preserves its explicitly selected view',()=>{
|
|
|
|
|
assert.match(code,/familyView=b.dataset.familySection;showPage\('family',\{keepFamilyView:true\}\)/);
|
|
|
|
|
assert.match(app,/loadFamily\(!options.keepFamilyView\)/);
|
|
|
|
|
});
|
|
|
|
|
test('culture and property entry requires its separate permission',()=>{
|
|
|
|
|
const c=vm.createContext({isAdmin:()=>false,currentUser:{familyAccess:'read',heritageAccess:'none'},currentHome:()=>({name:'赵府'}),esc:s=>s,$:()=>output}),output={innerHTML:''};
|
|
|
|
|
vm.runInContext(part(code,'function renderHeritageHome(','function resetSteward('),c);c.renderHeritageHome();
|
|
|
|
|
assert.match(output.innerHTML,/data-family-section/);assert.doesNotMatch(output.innerHTML,/data-heritage-kind/);
|
|
|
|
|
c.currentUser.heritageAccess='read';c.renderHeritageHome();assert.match(output.innerHTML,/data-heritage-kind/);
|
|
|
|
|
});
|
2026-10-05 18:23:25 +08:00
|
|
|
|
|
|
|
|
test('video device cards expose a single configuration entry with correct scope',()=>{
|
|
|
|
|
const devices=fs.readFileSync('web/devices.js','utf8');
|
|
|
|
|
const c=vm.createContext({state:{cameras:[]},isAdmin:()=>true,esc:s=>String(s)});
|
|
|
|
|
vm.runInContext(part(devices,'function videoDeviceActions(','function deviceCard('),c);
|
|
|
|
|
const camera={source:'camera',id:'a'};
|
|
|
|
|
assert.match(c.videoDeviceActions(camera),/data-view-device="a".*data-device-source="camera".*data-open-device-tab="cameras"/);
|
|
|
|
|
assert.doesNotMatch(c.videoDeviceActions(camera),/查看画面/);
|
|
|
|
|
c.state.cameras=[{assetId:'a'}];assert.match(c.videoDeviceActions(camera),/查看画面/);
|
|
|
|
|
c.isAdmin=()=>false;assert.doesNotMatch(c.videoDeviceActions(camera),/配置镜头/);
|
|
|
|
|
assert.equal(c.videoDeviceActions({source:'device',id:'lamp'}),'');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('monitor wall offers viewing without duplicate device management',()=>{
|
|
|
|
|
const live=part(app,'function renderLive()','function updateLiveStatus(');
|
|
|
|
|
assert.doesNotMatch(live,/data-edit-camera|data-camera-settings|new-camera/);
|
|
|
|
|
assert.match(live,/data-view-device/);
|
|
|
|
|
const section=part(html,'<section id="page-live"','<section id="page-playback"');
|
|
|
|
|
assert.doesNotMatch(section,/data-action="new-camera"/);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('camera form submits the configured ONVIF port',()=>{
|
|
|
|
|
assert.match(html,/name="onvifPort" type="number" value="80" min="1" max="65535"/);
|
|
|
|
|
assert.match(app,/onvifPort:Number\(d.onvifPort\)/);
|
|
|
|
|
assert.match(app,/port:Number\(f.elements.onvifPort.value\)/);
|
|
|
|
|
});
|