Files
ucvl-home-vision/test_daily_flow.js
T

70 lines
5.5 KiB
JavaScript

const test=require('node:test'),assert=require('node:assert/strict'),fs=require('node:fs'),vm=require('node:vm');
const source=name=>fs.readFileSync('web/'+name,'utf8');
function part(file,start,end){const s=source(file);return s.slice(s.indexOf(start),s.indexOf(end,s.indexOf(start)+start.length));}
const devices=require('./web/device-model.js');
const areas=[{id:'floor',name:'一楼'},{id:'room',name:'卧室',parentId:'floor'}];
const lamp={name:'台灯',deviceType:'灯',siteId:'room',brand:'小米',managerName:'爸爸',tags:['阅读']};
test('device search combines area, type, brand, responsible person and tags',()=>{
for(const q of ['卧室 灯','一楼 小米','爸爸 阅读',' 卧室 台灯 '])assert.ok(devices.matches(lamp,q,areas),q);
assert.ok(!devices.matches(lamp,'厨房 灯',areas));assert.ok(devices.matches(lamp,'',areas));
});
test('grouping views retain devices and their location',()=>{
const before=JSON.stringify(lamp);
for(const dimensions of [['space'],['type','space'],['manager','type'],['owner','tag']])assert.equal(devices.groups([lamp],dimensions,areas).flatMap(g=>g.items).length,1);
assert.equal(JSON.stringify(lamp),before);
});
test('editor reset clears old identities and revisions after editing',()=>{
const fields=[{name:'id',value:'old'},{name:'familyRevision',value:'9'},{name:'revision',value:'3'}];
let resets=0;const c=vm.createContext({});vm.runInContext(part('app.js','function resetEditorForm(','function formValues('),c);
c.resetEditorForm({reset(){resets++;},querySelectorAll(){return fields;}});
assert.equal(resets,1);assert.deepEqual(fields.map(f=>f.value),['','0','0']);
});
test('back buttons follow place, device and family hierarchy',()=>{
const c=vm.createContext({});vm.runInContext(part('steward.js','function scopeReturn(','function selectedDevice('),c);
for(const [page,device,target] of [['overview',null,'households'],['live',{},'objects'],['sites',null,'objects'],['family',null,'heritage'],['heritage-records',null,'heritage'],['journal',null,'overview']])assert.equal(c.scopeReturn(page,device).page,target);
});
function settingsContext(admin){
const nodes={};const c=vm.createContext({settingsSection:'preferences',isPlatformAdmin:()=>admin,$:id=>nodes[id]??={},$$:()=>[]});
vm.runInContext(part('personalization.js','function showSettingsSection(','document.addEventListener'),c);return {c,nodes};
}
test('settings show one section at a time',()=>{
const {c,nodes}=settingsContext(true);
for(const section of ['preferences','baidu','brand','backup']){c.showSettingsSection(section);assert.equal(Object.values(nodes).filter(n=>!n.hidden).length,1);assert.equal(c.settingsSection,section);}
});
test('members cannot select platform settings',()=>{
const {c,nodes}=settingsContext(false);for(const section of ['baidu','brand','backup','unknown']){c.showSettingsSection(section);assert.equal(c.settingsSection,'preferences');assert.equal(nodes['#backup-panel'].hidden,true);}
});
function journalContext(){
const fields=Object.fromEntries(['text','occurredOn','category','visibility'].map(k=>[k,{value:k==='visibility'?'private':''}]));
const nodes={'#journal-form':{elements:fields}};
const c=vm.createContext({journalSelectedMedia:new Set(),journalBaseline:'',journalUpload:false,journalTracks:null,journalSaving:false,journalPage:3,confirm:()=>false,toast:()=>{},$:id=>nodes[id]??={value:''}});
vm.runInContext(part('journal.js','function journalDraftSignature(','const journalId='),c);return {c,fields,nodes};
}
test('saved entry filter reset clears searches and dates but retains privacy',()=>{
const {c,fields,nodes}=journalContext();for(const id of ['journal-search','journal-category','journal-from','journal-to','journal-person'])c.$('#'+id).value='old';
c.resetJournalFilters('mine');assert.equal(c.journalPage,0);assert.equal(nodes['#journal-search'].value,'');assert.equal(nodes['#journal-person-label'].hidden,true);assert.equal(fields.visibility.value,'private');
});
test('dirty drafts and pending saves are protected',()=>{
const {c,fields}=journalContext();c.journalBaseline=c.journalDraftSignature();assert.equal(c.journalHasDraft(),false);
fields.text.value='未保存';assert.equal(c.journalHasDraft(),true);assert.equal(c.confirmJournalDiscard(),false);
c.confirm=()=>true;assert.equal(c.confirmJournalDiscard(),true);c.journalSaving=true;assert.equal(c.confirmJournalDiscard(),false);
});
test('empty feed explains private visibility and filtered emptiness',()=>{
const {c}=journalContext();c.$('#journal-mode').value='feed';assert.match(c.journalEmptyMessage(),/仅自己可见/);
c.$('#journal-search').value='missing';assert.match(c.journalEmptyMessage(),/清除筛选/);
});
test('mobile navigation toggles an accessible control without changing scope',()=>{
let opened=false;const button={setAttribute(k,v){this[k]=v;},textContent:''};
const c=vm.createContext({$:id=>id==='.sidebar'?{classList:{toggle(k,v){opened=v;}}}:button});
vm.runInContext(part('app.js','function setMobileMenu(',"$('#mobile-nav-toggle').onclick"),c);
c.setMobileMenu(true);assert.equal(opened,true);assert.equal(button['aria-expanded'],'true');
c.setMobileMenu(false);assert.equal(opened,false);assert.equal(button['aria-expanded'],'false');
});
test('mobile forms use readable inputs, touch targets and bounded dialogs',()=>{
const css=source('chinese-theme.css'),html=require('./test-support/ui-source')();
assert.match(css,/font-size:16px!important;min-height:44px/);
assert.match(css,/max-height:90dvh/);assert.match(css,/sidebar:not\(\.mobile-open\)>nav/);
assert.match(html,/aria-controls="main-navigation"/);
});