Files
ucvl-home-vision/test_personalization.js
T

57 lines
5.4 KiB
JavaScript

const test=require('node:test'),assert=require('node:assert/strict'),vm=require('node:vm'),fs=require('node:fs');
const source=fs.readFileSync('web/personalization.js','utf8'),homes=fs.readFileSync('web/households.js','utf8');
const part=(s,a,b)=>s.slice(s.indexOf(a),s.indexOf(b,s.indexOf(a)+a.length));
function preferences(){const c=vm.createContext({pageInfo:{},document:{documentElement:{dataset:{}}}});vm.runInContext(source.slice(0,source.indexOf('function applyBranding(')),c);return c;}
test('unset preferences inherit platform defaults and unknown values cannot become CSS',()=>{
const c=preferences();const values=c.effectiveAppearance({theme:'javascript:alert(1)',layout:'top'},{theme:'ink'});
assert.equal(values.theme,'ink');assert.equal(values.layout,'top');assert.equal(values.density,'comfortable');
c.applyAppearance({theme:'clean',font:'sans'});assert.equal(c.document.documentElement.dataset.theme,'clean');
assert.equal(c.document.documentElement.dataset.font,'sans');
});
test('explicit personal appearance is preserved when the platform default changes',()=>{
const c=preferences();assert.equal(c.effectiveAppearance({theme:'paper'},{theme:'ink'}).theme,'paper');
assert.equal(c.effectiveAppearance({theme:'default'},{theme:'ink'}).theme,'ink');
});
test('leaving settings invalidates outstanding work and restores the saved appearance',()=>{
let applied=0,c=vm.createContext({appearanceGeneration:3,settingsTimer:7,clearTimeout:()=>{},applyAppearance:()=>applied++});
vm.runInContext(part(source,'function leaveSettings()',"$('#preferences-form')"),c);c.leaveSettings();
assert.equal(c.appearanceGeneration,4);assert.equal(c.settingsTimer,null);assert.equal(applied,1);
});
test('a late backup response cannot disclose details after leaving settings',async()=>{
let finish,writes=0;const c=vm.createContext({appearanceGeneration:1,activePage:'settings',api:()=>new Promise(r=>finish=r),renderBackup:()=>writes++});
vm.runInContext(part(source,'async function pollBackup()',"$('#backup-form')"),c);
const pending=c.pollBackup();c.appearanceGeneration++;c.activePage='households';finish({private:true});await pending;assert.equal(writes,0);
});
test('late backup save or run cannot update a newly opened account',async()=>{
let save,finish,writes=0;const button={},elements={enabled:{checked:true},time:{value:'03:00'},keep:{value:'14'},networkPath:{value:''}},node={addEventListener:(name,fn)=>save=fn};
const c=vm.createContext({appearanceGeneration:1,backupRevision:0,$:()=>node,fillBackup:()=>writes++,pollBackup:()=>writes++,api:()=>new Promise(r=>finish=r)});
vm.runInContext(source.slice(source.indexOf("$('#backup-form').addEventListener")),c);
const pending=save({preventDefault(){},currentTarget:{elements,querySelector:()=>button}});c.appearanceGeneration++;finish({});await pending;assert.equal(writes,0);
const run=node.onclick();c.appearanceGeneration++;finish({});await run;assert.equal(writes,0);
});
test('backup messages distinguish local-only, network copy, and failure',()=>{
const c=vm.createContext({});vm.runInContext(part(source,'function backupLabel(','function renderBackup('),c);
assert.match(c.backupLabel({localOk:true,networkOk:false}),/本机/);assert.doesNotMatch(c.backupLabel({localOk:true}),/网络|云端/);
assert.match(c.backupLabel({error:'offline',localOk:true}),/未完成/);assert.match(c.backupLabel({networkOk:true}),/网络目录/);
});
test('place cards group by city without exposing an inline photo payload',()=>{
const out={innerHTML:''},filter={};const c=vm.createContext({state:{familyId:'a',households:[{id:'a',name:'Home',province:'Province',city:'City A',members:1,sites:1,coverUrl:'/api/households/cover?id=a',coverData:'PRIVATE-PHOTO'},{id:'b',name:'Office',city:'City B',kind:'office',members:1,sites:0}]},
activePage:'households',setOptions:()=>{},$:s=>s==='#household-list'?out:filter,isAdmin:()=>false,esc:s=>String(s),scopeKind:h=>h.kind||'home'});
vm.runInContext(part(homes,'function renderHouseholds(','async function switchHousehold('),c);c.renderHouseholds();
assert.match(out.innerHTML,/City A/);assert.match(out.innerHTML,/City B/);assert.match(out.innerHTML,/\/api\/households\/cover\?id=a/);
assert.doesNotMatch(out.innerHTML,/PRIVATE-PHOTO|data-edit-household/);
});
test('publication text is rendered as text and the export omits private state',()=>{
const apply=part(source,'function applyBranding(','async function loadBranding(');assert.doesNotMatch(apply,/innerHTML/);
const payload=part(source,'function brandPayload()',"\n$('#brand-form').addEventListener");assert.doesNotMatch(payload,/password|currentUser|backupRevision|networkPath/);
assert.match(source,/digital-steward-appearance/);
});
test('image conversion resizes oversized output and closes its decoded bitmap',async()=>{
let closed=0,draws=0;const bitmap={width:1600,height:800,close(){closed++;}},canvas={width:0,height:0,getContext:()=>({drawImage(){draws++;}}),toDataURL:()=>draws===1?'data:image/png;base64,'+'A'.repeat(3000000):'data:image/png;base64,AA=='};
const c=vm.createContext({createImageBitmap:async()=>bitmap,document:{createElement:()=>canvas}});
vm.runInContext(part(source,'async function readImage(','for(const [id,type,max]'),c);
await assert.rejects(c.readImage({type:'image/svg+xml',size:20}),/PNG/);
assert.equal(await c.readImage({type:'image/png',size:100},960),'data:image/png;base64,AA==');
assert.equal(canvas.width,768);assert.equal(canvas.height,384);assert.equal(closed,1);
});