const test=require('node:test'),assert=require('node:assert/strict'),fs=require('node:fs'),vm=require('node:vm'); const code=fs.readFileSync('web/steward.js','utf8'),html=require('./test-support/ui-source')(),app=fs.readFileSync('web/app.js','utf8'); 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',()=>{ const nav=html.match(/]*aria-label="主导航">([\s\S]*?)<\/nav>/)[1]; 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/); }); 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,'
{ 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\)/); });