Files
ucvl-home-vision/test_ui_logic.js
T

75 lines
6.5 KiB
JavaScript
Raw Normal View History

// Run actual UI functions in isolated contexts; no live accounts or browser state.
const test=require('node:test'),assert=require('node:assert/strict'),vm=require('node:vm'),fs=require('node:fs');
const source=name=>fs.readFileSync('web/'+name,'utf8');
function section(file,start,end){const s=source(file),a=s.indexOf(start),b=s.indexOf(end,a+start.length);assert.ok(a>=0&&b>a);return s.slice(a,b);}
const apiSource=section('app.js','async function api(','function toast(');
const refreshSource=section('app.js','async function refresh(','async function auth(');
function context(extra={}){return vm.createContext({sessionGeneration:0,refreshGeneration:0,authGeneration:0,loggedIn:true,currentUser:{id:'a'},state:{},Error,...extra});}
function deferred(){let resolve,reject;const promise=new Promise((a,b)=>{resolve=a;reject=b;});return {promise,resolve,reject};}
test('late successful response cannot restore private data after account switch',async()=>{
const pending=deferred(),c=context({fetch:()=>pending.promise});vm.runInContext(apiSource,c);
const request=c.api('/api/family');c.sessionGeneration++;c.currentUser={id:'b'};
pending.resolve({ok:true,json:async()=>({privatePerson:'previous account'})});
await assert.rejects(request,e=>e.stale===true);
});
test('old unauthorized response cannot log out a new session',async()=>{
const pending=deferred();let clears=0;const c=context({fetch:()=>pending.promise,clearPrivateView:()=>clears++,auth:()=>{}});vm.runInContext(apiSource,c);
const request=c.api('/api/state');c.sessionGeneration++;
pending.resolve({ok:false,status:401,json:async()=>({error:'old session'})});
await assert.rejects(request,e=>e.stale===true);assert.equal(clears,0);
});
test('API errors retain conflict status for explicit reload workflow',async()=>{
const c=context({fetch:async()=>({ok:false,status:409,json:async()=>({error:'家谱已被更新'})})});vm.runInContext(apiSource,c);
await assert.rejects(c.api('/api/family/person',{}),e=>e.status===409);
});
test('late state refresh cannot overwrite newer state',async()=>{
const first=deferred(),second=deferred(),requests=[first,second];let renders=0;
const c=context({api:()=>requests.shift().promise,render:()=>renders++});vm.runInContext(refreshSource,c);
const a=c.refresh(),b=c.refresh();second.resolve({version:'new',user:{id:'a'}});await b;
first.resolve({version:'old',user:{id:'a'}});await a;
assert.equal(c.state.version,'new');assert.equal(renders,1);
});
test('late refresh failure cannot cover a newer successful screen',async()=>{
const first=deferred(),second=deferred(),requests=[first,second];let banners=0;
const c=context({api:()=>requests.shift().promise,render:()=>{},$:()=>{banners++;return {};}});vm.runInContext(refreshSource,c);
const a=c.refresh(),b=c.refresh();second.resolve({user:{id:'a'}});await b;first.reject(Error('old failure'));await a;assert.equal(banners,0);
});
test('another tab changing the shared login clears the previous private view',async()=>{
let clears=0;const c=context({api:async()=>({user:{id:'b'},cameras:[]}),render:()=>{},
clearPrivateView:()=>{clears++;c.loggedIn=false;c.currentUser=null;}});
vm.runInContext(refreshSource,c);await c.refresh();assert.equal(clears,1);assert.equal(c.currentUser.id,'b');assert.equal(c.loggedIn,true);
});
test('expired session clears private view when session endpoint itself returns 200',async()=>{
const fields=new Map();const $=s=>{if(!fields.has(s))fields.set(s,{elements:{username:{}},showModal(){this.open=true;}});return fields.get(s);};
let clears=0;const c=context({api:async()=>({authenticated:false,user:null,setupRequired:false}),$,
clearPrivateView:()=>{clears++;c.currentUser=null;c.loggedIn=false;},applyPermissions:()=>{},refresh:()=>{},toast:()=>{}});
vm.runInContext(section('app.js','async function auth(','function openSite('),c);await c.auth();
assert.equal(clears,1);assert.equal(c.loggedIn,false);assert.equal($('#auth-dialog').open,true);
});
test('archived person reached from an event is revealed before graph location',()=>{
const toggle={checked:false};let rendered=false,located='';
const c=context({family:{people:[{id:'ancestor',archived:true}]},$:()=>toggle,window:{innerWidth:1200},document:{},
renderFamily:()=>{assert.equal(toggle.checked,true);rendered=true;},familyMap:{locate:id=>located=id}});
vm.runInContext(section('family.js','function selectFamilyPerson(','function openPerson('),c);c.selectFamilyPerson('ancestor',true);
assert.ok(rendered);assert.equal(c.familyView,'tree');assert.equal(located,'ancestor');
});
test('event default save preserves published state and exposes explicit withdrawal',()=>{
const controls=new Map(),form={dataset:{},querySelector:s=>{if(!controls.has(s))controls.set(s,{});return controls.get(s);}};
const elements=new Map([['#event-form',form]]);const $=s=>{if(!elements.has(s))elements.set(s,{showModal(){},close(){}});return elements.get(s);};
const c=context({family:{canEdit:true,revision:3,people:[],events:[{id:'published',status:'published',personIds:[]}]},$,fillForm:()=>{},initDateFields:()=>{},esc:String});
vm.runInContext(section('family-events.js','function editFamilyEvent(',"$('#event-form').addEventListener"),c);
c.editFamilyEvent('published');assert.equal(form.querySelector('[data-save-event]').value,'published');assert.equal(form.querySelector('[data-save-event]').textContent,'保存修改');assert.equal(form.querySelector('[data-publish-event]').hidden,true);assert.equal(form.querySelector('[data-withdraw-event]').hidden,false);
c.editFamilyEvent();assert.equal(form.querySelector('[data-save-event]').value,'draft');assert.equal(form.querySelector('[data-withdraw-event]').hidden,true);
});
test('event date validation never clears a separately edited person death date',()=>{
const c=context({$:()=>{throw Error('unexpected person form access');},syncLifeStatus:()=>{throw Error('unexpected life state change');},dateHint:()=>{}});
vm.runInContext(section('date-fields.js','function syncDateFields(','for(const field of $$'),c);
c.syncDateFields(true,{id:'event-form',querySelectorAll:()=>[]});
});
test('older family request errors cannot repopulate logged-out view',async()=>{
const pending=deferred();let messages=0;const c=context({familyGeneration:0,api:()=>pending.promise,$:()=>{messages++;return {};}});
vm.runInContext(section('family.js','async function loadFamily(','function relationText('),c);
const p=c.loadFamily();c.loggedIn=false;c.familyGeneration++;pending.reject(Error('late error'));assert.equal(await p,false);assert.equal(messages,0);
});