const test=require('node:test'),assert=require('node:assert/strict'),fs=require('node:fs'); const charts=()=>import('./frontend/src/composables/charts.mjs'); test('dashboard counts physical devices once, not lenses; handles missing network metadata',async()=>{ const {summarizeDevices}=await charts(); assert.equal(summarizeDevices().total,0); const result=summarizeDevices({assets:[{id:'camera',networkAccess:'tailscale'}],cameras:[{assetId:'camera'},{assetId:'camera'}],recorders:[{networkAccess:'lan'}],devices:[{deviceType:'灯'}, {deviceType:'灯',networkAccess:'lan'}]}); assert.equal(result.total,4); assert.equal(result.types.find(x=>x.label==='灯').count,2); assert.equal(result.networks.find(x=>x.label==='待登记').count,1); }); test('chart aggregates remain numerical and empty charts are valid',async()=>{ const {bars,distribution}=await charts(); assert.deepEqual(bars().series[0].data,[]); assert.equal(distribution([{label:'a',count:-2}]).series[0].data[0].value,0); assert.equal(bars([{label:'a',count:'3'}]).series[0].data[0].value,3); }); test('network chart cannot draw phantom parent or cyclic connections',async()=>{ const {networkChart}=await charts(); const option=networkChart([{device:{id:'r',name:'路由',source:'device'},depth:0},{device:{id:'c',name:'摄像头',uplinkId:'r',networkHosts:['192.168.8.160']},depth:1},{device:{id:'x',name:'独立',uplinkId:'missing'},depth:0}]); assert.deepEqual(option.series[0].links,[{source:'r',target:'c'}]); assert.equal(option.series[0].data.length,3);assert.equal(option.series[0].data[1].targetId,'c'); assert.match(option.series[0].data[1].caption,/192\.168\.8\.160/); assert.equal(option.series[0].layout,'none'); }); test('platform graph only includes supplied authorized households and no device leakage',async()=>{ const {platformChart}=await charts(); const series=platformChart([{id:'one',name:'家庭A',deviceCount:2}],'本平台').series[0]; assert.deepEqual(series.data.map(x=>x.id),['platform','home:one']); assert.equal(series.data[1].targetId,'one');assert.equal(series.links.length,1); }); test('ECharts runtime is local, lazy, deterministic and releases resources',()=>{ const runtime=fs.readFileSync('frontend/src/composables/charts-runtime.js','utf8'),widget=fs.readFileSync('frontend/src/components/LocalChart.vue','utf8'); assert.match(runtime,/echarts\/core/);assert.doesNotMatch(runtime,/https?:\/\//); assert.match(widget,/import\('\.\.\/composables\/charts-runtime\.js'\)/); assert.match(widget,/animation: false/);assert.match(widget,/fingerprint !== last/); assert.match(widget,/renderMode: 'richText'/);assert.match(widget,/onBeforeUnmount/); assert.match(widget,/resizeObserver\?\.disconnect/);assert.match(widget,/chart\?\.dispose/); }); test('deployed entry and controller dependencies have no remote CDN references',()=>{ const html=fs.readFileSync('web/ui/index.html','utf8'); for(const match of html.matchAll(/(?:src|href)="([^"]+)"/g))assert.ok(match[1].startsWith('/'),match[1]); const scripts=JSON.parse(fs.readFileSync('frontend/src/compat/scripts.json','utf8')); for(const script of scripts){assert.ok(script.startsWith('/')&&!script.startsWith('//'));assert.ok(fs.existsSync('web'+script));} for(const file of ['frontend/src/main.js','frontend/src/dashboard.css','frontend/src/theme.css','frontend/src/bootstrap-polish.css'])assert.doesNotMatch(fs.readFileSync(file,'utf8'),/(?:@import\s+|(?:src|href)\s*=\s*|url\(\s*['"]?)https?:\/\//); });