Add hierarchical geography and cloud-hosted profile images

This commit is contained in:
Codex
2026-10-04 21:30:30 +08:00
parent a77fcc6671
commit 427fd51e14
27 changed files with 618 additions and 50 deletions
+20 -1
View File
@@ -36,7 +36,7 @@ test('backup messages distinguish local-only, network copy, and failure',()=>{
});
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'});
ZhaoGeography:require('./web/geography.js'),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/);
@@ -66,3 +66,22 @@ test('replacing or resetting an image invalidates older decode jobs',()=>{
const first=c.nextImageJob('logo'),second=c.nextImageJob('logo');assert.equal(c.imageJobCurrent('logo',first),false);assert.equal(c.imageJobCurrent('logo',second),true);
c.invalidateBrandImages();assert.equal(c.imageJobCurrent('logo',second),false);
});
test('cover accepts a large original but compresses below proxy request limits',async()=>{
let size=0;const canvas={getContext:()=>({drawImage(){}}),toDataURL:()=> 'data:image/png;base64,'+'A'.repeat(++size===1?1000000:500000)};
const c=vm.createContext({createImageBitmap:async()=>({width:8000,height:6000,close(){}}),document:{createElement:()=>canvas}});
vm.runInContext(part(source,'async function readImage(','for(const [id,type,max]'),c);
const result=await c.readImage({type:'image/jpeg',size:15*1024*1024},960);
assert.ok((result.length-22)*.75<600*1024);assert.ok(canvas.width<=960);
await assert.rejects(c.readImage({type:'image/jpeg',size:31*1024*1024},960),/30 MB/);
await assert.rejects(c.readImage({type:'image/jpeg',size:15*1024*1024},256),/5 MB/);
});
test('failed and still-processing covers block profile save instead of reporting success',async()=>{
let submit,calls=0;const error={textContent:''},f={querySelector:()=>error,addEventListener:(_,fn)=>submit=fn};
const jobs=new Map([['cover',{error:true}]]);
const c=vm.createContext({coverSaving:false,$:()=>f,imageJobs:jobs,imageJobPending:()=>false,api:async()=>{calls++;},formValues:()=>{throw Error('must not serialize failed image');}});
vm.runInContext(part(homes,"$('#household-form').addEventListener('submit'",'document.addEventListener'),c);
await submit({preventDefault(){},currentTarget:f});assert.equal(calls,0);assert.match(error.textContent,/封面未准备成功/);
c.imageJobPending=()=>true;await submit({preventDefault(){},currentTarget:f});assert.match(error.textContent,/仍在处理/);
});