Add verified camera settings, family version recovery and account self service

This commit is contained in:
Codex
2026-10-04 04:52:39 +08:00
parent 56a24c80e2
commit 96f0ef31fc
18 changed files with 969 additions and 37 deletions
+50
View File
@@ -72,3 +72,53 @@ test('older family request errors cannot repopulate logged-out view',async()=>{
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);
});
const historySource=section('family-history.js','async function showFamilyChange(','function prepareFamilyRestore(');
test('history prose is never translated as an enum value',()=>{
const c=context({personName:id=>'Person '+id});vm.runInContext(section('family-history.js','function historyValue(','async function openFamilyHistory('),c);
assert.equal(c.historyValue('body','published'),'published');assert.equal(c.historyValue('status','published'),'已发布');
assert.equal(c.historyValue('personIds',['a','b']),'Person a、Person b');assert.equal(c.historyValue('active',false),'否');
});
test('history detail from a previous selection cannot replace newer detail',async()=>{
const a=deferred(),b=deferred(),calls=[a,b],elements=new Map(),$=s=>{if(!elements.has(s))elements.set(s,{});return elements.get(s);};
const c=context({historyContext:{},historyDetailGeneration:0,historySelection:null,api:()=>calls.shift().promise,$,historyFields:{name:'Name'},esc:String,historyValue:(_,v)=>v});
vm.runInContext(historySource,c);const old=c.showFamilyChange(1),latest=c.showFamilyChange(2);
b.resolve({revision:2,before:null,after:{name:'New'}});await latest;const html=$('#history-detail').innerHTML;
a.resolve({revision:1,before:null,after:{name:'Old'}});await old;assert.equal($('#history-detail').innerHTML,html);assert.equal(c.historySelection.revision,2);
});
test('closing history prevents late private detail from rendering',async()=>{
const pending=deferred(),detail={};const c=context({historyContext:{},historyDetailGeneration:0,historySelection:null,api:()=>pending.promise,$:()=>detail});
vm.runInContext(historySource,c);const request=c.showFamilyChange(1);c.historyContext=null;c.historyDetailGeneration++;pending.resolve({after:{name:'Private'}});await request;
assert.equal(detail.innerHTML,undefined);assert.equal(c.historySelection,null);
});
test('password form requires matching, distinct and bounded new password',()=>{
const c=context();vm.runInContext(section('app.js','function validateNewPassword(','$(\'#account-password\').onclick='),c);
for(const data of [{newPassword:'different',confirmPassword:'mismatch'},{newPassword:'samepass',confirmPassword:'samepass',currentPassword:'samepass'},{newPassword:'short',confirmPassword:'short'},{newPassword:'x'.repeat(129),confirmPassword:'x'.repeat(129)}])assert.throws(()=>c.validateNewPassword(data));
assert.doesNotThrow(()=>c.validateNewPassword({newPassword:'new-password',confirmPassword:'new-password',currentPassword:'old-password'}));
});
test('history restore sends the reviewed revision and never the full text',async()=>{
let sent;const elements=new Map(),$=s=>{if(!elements.has(s))elements.set(s,{replaceChildren(){}});return elements.get(s);};
const c=context({historyContext:{revision:10},historySelection:{revision:3,before:{name:'Original'}},historyDetailGeneration:1,$,$$:()=>[],
api:async(path,data)=>{sent={path,data};},loadFamilyHistory:async()=>{},loadFamily:async()=>true,toast:()=>{}});
vm.runInContext(section('family-history.js','async function restoreFamilyChange(','$(\'#history-more\').onclick='),c);await c.restoreFamilyChange('before');
assert.equal(sent.path,'/api/family/restore');assert.equal(JSON.stringify(sent.data),JSON.stringify({changeRevision:3,side:'before',revision:10}));assert.equal(c.historySelection,null);
});
test('camera form sends only changed and advertised parameters',()=>{
const c=context();vm.runInContext(section('camera-settings.js','function changedCameraValues(','async function saveCameraSettings('),c);
const form={elements:{brightness:{value:'60'},dayNight:{value:'AUTO'},malicious:{value:'anything'}}};
const settings={imaging:{brightness:{value:50,min:0,max:100},dayNight:{value:'AUTO',choices:['AUTO','ON']}}};
assert.equal(JSON.stringify(c.changedCameraValues(form,'imaging',settings)),JSON.stringify({brightness:60}));
});
test('late camera settings cannot populate a closed or different camera dialog',async()=>{
const pending=deferred(),elements=new Map(),$=s=>{if(!elements.has(s))elements.set(s,{value:'main'});return elements.get(s);};let renders=0;
const c=context({cameraSettingsContext:{id:'old'},cameraSettingsGeneration:0,$,$$:()=>[],URLSearchParams,api:()=>pending.promise,renderCameraSettings:()=>renders++});
vm.runInContext(section('camera-settings.js','async function loadCameraSettings(','async function openCameraSettings('),c);const request=c.loadCameraSettings();c.cameraSettingsContext={id:'new'};c.cameraSettingsGeneration++;pending.resolve({device:{}});await request;assert.equal(renders,0);
});
test('ambiguous device save requires re-read before another write',async()=>{
const elements=new Map(),$=s=>{if(!elements.has(s))elements.set(s,{});return elements.get(s);};const buttons=[{disabled:false}];
const c=context({cameraSettingsContext:{id:'camera',quality:'main',settings:{revision:'old'}},cameraSettingsGeneration:0,$,$$:()=>buttons,
changedCameraValues:()=>({brightness:60}),api:async()=>{throw Error('connection lost');},toast:()=>{}});
vm.runInContext(section('camera-settings.js','async function saveCameraSettings(','for(const [id,section]'),c);await c.saveCameraSettings({},'imaging');
assert.equal(c.cameraSettingsContext.settings,null);assert.equal(buttons[0].disabled,true);assert.equal($('#camera-settings-reload').disabled,false);
});