70 lines
5.5 KiB
JavaScript
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=source('index.html');
|
|
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"/);
|
|
});
|