feat: require couple binding and refine miniapp settings

Gate record features and storage entry points with invitation guidance; enforce record access in the backend. Add homepage greetings, right-swipe profile dismissal, and move the smoking switch to settings. Simplify API base configuration and remove the unused legacy tab bar component.
This commit is contained in:
2026-11-04 21:27:03 +08:00
parent 41bcb793da
commit b854d2873e
67 changed files with 430 additions and 227 deletions
+56
View File
@@ -0,0 +1,56 @@
const test=require('node:test'), assert=require('node:assert/strict'), path=require('node:path');
const createLoader=require('./load-module.cjs');
const root=path.join(__dirname,'..');
function env(cid='') {
const modals=[],routes=[],storage={coupleId:cid};let page,component;
const app={globalData:{},ensureAuth:async()=>{}};
const wx={getStorageSync:k=>storage[k],removeStorageSync:k=>delete storage[k],showModal:o=>modals.push(o),switchTab:o=>routes.push(o.url),navigateTo:o=>routes.push(o.url),showToast(){}};
const load=createLoader({wx,console,Date,Promise,setTimeout,clearTimeout,setInterval,clearInterval,getApp:()=>app,Page:p=>page=p,Component:c=>component=c,getCurrentPages:()=>[{route:'pages/index/index',data:{}}]}, {'../../utils/request':{api:{},getImageUrl:v=>v},'../../utils/page-cache':{withDataCache:p=>p}});
return {wx,modals,routes,storage,app,load,page:()=>({...page,data:{...page.data},setData(v){Object.assign(this.data,v)}}),component:()=>component};
}
test('unbound entry modal is shared and confirmation opens the existing invitation panel',()=>{
const e=env(),guard=e.load(path.join(root,'utils/record-access.js'));
assert.equal(guard.requireRecordAccess(),false);guard.requireRecordAccess();assert.equal(e.modals.length,1);
const modal=e.modals[0];assert.equal(modal.confirmText,'立即邀请');modal.success({confirm:true});modal.complete();
assert.equal(e.app.globalData.openProfileBind,true);assert.deepEqual(e.routes,['/pages/profile/profile']);
e.storage.coupleId='couple';assert.equal(guard.requireRecordAccess(),true);
});
test('homepage record links and profile feature shortcuts reject unbound users before navigation',()=>{
for(const [file,methods] of [['index/index',['showMore','writeDiary','goToAlbum','goToCheckin']],['profile/profile',['openStatistic']]]) {
for(const method of methods){const e=env();e.load(path.join(root,'pages',file+'.js'));const p=e.page();p[method]({currentTarget:{dataset:{key:'diary'}}});assert.equal(e.routes.length,0);assert.equal(e.modals.length,1,method);}
}
});
test('record tab rejects unbound navigation and allows bound navigation',()=>{
for(const cid of ['', 'couple']){const e=env(cid);e.load(path.join(root,'custom-tab-bar/index.js'));const c=e.component();const p={...c.methods,setData(){}};p.navigate({currentTarget:{dataset:{tab:'records'}}});assert.equal(e.routes.length,cid?1:0);assert.equal(e.modals.length,cid?0:1);}
});
test('all protected direct links avoid loading records when unbound and cancellation returns home',async()=>{
const pages=['discover/discover','timeline/timeline','punch/punch','diary/diary','albums/albums','album/album','lovewords/lovewords','publish/publish','checkin-detail/checkin-detail','timeline-detail/timeline-detail','period/period','diary_detail/diary_detail','diary_edit/diary_edit','pet/pet','pet/achievements','smoking-detail/smoking-detail','storage/storage'];
for(const file of pages){const e=env();e.load(path.join(root,'pages',file+'.js'));const p=e.page();await p.onLoad({});await p.onShow();assert.equal(p.data.recordAccessBlocked,true,file);assert.equal(e.modals.length,1,file);e.modals[0].success({cancel:true});e.modals[0].complete();assert.deepEqual(e.routes,['/pages/index/index']);}
});
test('cold direct entry waits for login before deciding binding and loading the page',async()=>{
const e=env();let loaded=0,shown=0,finish;
e.app.ensureAuth=()=>new Promise(resolve=>finish=()=>{e.storage.coupleId='bound';resolve();});
const guard=e.load(path.join(root,'utils/record-access.js'));
const p=guard.withRecordAccess({onLoad(){loaded++;},onShow(){shown++;},setData(v){this.data={...this.data,...v};}});
const load=p.onLoad(),show=p.onShow();assert.equal(loaded,0);finish();await Promise.all([load,show]);assert.equal(loaded,1);assert.equal(shown,1);assert.equal(p.data.recordAccessBlocked,false);assert.equal(e.modals.length,0);
e.storage.coupleId='';await p.onShow();assert.equal(shown,1);assert.equal(e.modals.length,1);
});
test('authoritative backend denial clears stale binding and offers invitation',async()=>{
const e=env('stale-couple');
e.wx.request=o=>o.success({statusCode:403,data:{error:'请先绑定情侣,再使用记录功能',code:10002}});
const {api}=e.load(path.join(root,'utils/request.js'));
await assert.rejects(api.diaryAdd({content:'blocked'}),error=>error.code===10002);
assert.equal(e.storage.coupleId,undefined);assert.equal(e.modals.length,1);assert.equal(e.routes.length,0);
});
test('more and storage entries require binding while bound navigation stays available',()=>{
for(const cid of ['', 'couple']) {
for(const method of ['openStatistic','goStorage','goToSettings']) {
const e=env(cid);e.load(path.join(root,'pages/profile/profile.js'));const p=e.page();
p[method]({currentTarget:{dataset:{key:'settings'}}});
assert.equal(e.modals.length,cid?0:1,method);assert.equal(e.routes.length,cid?1:0,method);
if(cid) assert.equal(e.routes[0],method==='goStorage'?'/pages/storage/storage':'/pages/settings/settings');
}
}
});