更新最新代码

This commit is contained in:
CodeBuddy
2026-10-26 17:32:47 +08:00
commit 0f5dc01674
315 changed files with 42640 additions and 0 deletions
+101
View File
@@ -0,0 +1,101 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const path = require('node:path');
const createLoader = require('./load-module.cjs');
function setup() {
const storage = {userId: 1, coupleId: 2, token: 'token'};
const requests = [];
let now = 1000000;
class Clock extends Date { static now() { return now; } }
const wx = {getStorageSync: key => storage[key], setStorageSync: (key,value)=>storage[key]=value, request: options => requests.push(options), hideLoading(){}, showToast(){}};
const definitions = [];
const load = createLoader({wx, console, Promise, Date: Clock, Page: definition=>definitions.push(definition)});
const cache = load(path.join(__dirname, '../utils/data-cache.js'));
const {api} = load(path.join(__dirname, '../utils/request.js'));
const {withDataCache} = load(path.join(__dirname, '../utils/page-cache.js'));
const flush = async () => { for(let i=0;i<30;i++) await Promise.resolve(); };
const respond = (index, data, statusCode=200) => requests[index].success({statusCode, data});
return {load, definitions, storage, requests, cache, api, withDataCache, flush, respond, advance: ms=>now+=ms};
}
test('identical reads share a request, return independent objects and expire', async()=>{
const e=setup(); const one=e.api.diaryList({page:1,page_size:20}), two=e.api.diaryList({page_size:20,page:1});
await e.flush(); assert.equal(e.requests.length,1);
e.respond(0,{items:[{title:'old'}]});const [a,b]=await Promise.all([one,two]);
a.items[0].title='formatted';assert.equal(b.items[0].title,'old');
assert.equal((await e.api.diaryList({page:1,page_size:20})).items[0].title,'old');assert.equal(e.requests.length,1);
const different=e.api.diaryList({page:2,page_size:20});await e.flush();assert.equal(e.requests.length,2);e.respond(1,{items:[]});await different;
e.advance(45001);const expired=e.api.diaryList({page:1,page_size:20});await e.flush();assert.equal(e.requests.length,3);e.respond(2,{items:[]});await expired;
});
test('successful mutations invalidate related reads; unrelated and failed writes retain cache',async()=>{
const e=setup();const diary=e.api.diaryList(),album=e.api.albumList();await e.flush();e.respond(0,{items:[]});e.respond(1,[]);await Promise.all([diary,album]);
const failed=e.api.diaryUpdate({id:1});await e.flush();e.respond(2,{error:'failed'},500);await assert.rejects(failed);
await e.api.diaryList();assert.equal(e.requests.length,3);
const edit=e.api.diaryUpdate({id:1});await e.flush();e.respond(3,{success:true});await edit;
await e.api.albumList();assert.equal(e.requests.length,4);
const updated=e.api.diaryList();await e.flush();assert.equal(e.requests.length,5);e.respond(4,{items:[{id:1}]});await updated;
});
test('scope changes, errors and invalidation while reading never reuse stale results',async()=>{
const e=setup();const first=e.api.diaryList();await e.flush();e.cache.invalidate(['diaryList']);e.respond(0,{items:[{id:'old'}]});await first;
const fresh=e.api.diaryList();await e.flush();assert.equal(e.requests.length,2);e.respond(1,{items:[]});await fresh;
e.storage.userId=3;const other=e.api.diaryList();await e.flush();assert.equal(e.requests.length,3);e.respond(2,{error:'offline'},503);await assert.rejects(other);
const retry=e.api.diaryList();await e.flush();assert.equal(e.requests.length,4);e.respond(3,{items:[]});await retry;
});
test('each upper page observes changes independently and pull refresh bypasses cached reads',async()=>{
const e=setup();let loads=0;
function page() {
const definition=e.withDataCache({data:{loading:true}, async loadData(){loads++;this.data.items=await e.api.diaryList();}, async onPullDownRefresh(){await this.loadData();}}, {loadData:['diaryList']});
return {...definition,data:{...definition.data}};
}
const a=page(),b=page();let pending=Promise.all([a.loadData(),a.loadData(),b.loadData()]);await e.flush();assert.equal(e.requests.length,1);e.respond(0,{items:[]});await pending;
await Promise.all([a.loadData(),b.loadData()]);assert.equal(loads,2);
e.cache.mutation('diaryUpdate',{success:true});pending=Promise.all([a.loadData(),b.loadData()]);await e.flush();assert.equal(e.requests.length,2);e.respond(1,{items:[{id:1}]});await pending;assert.equal(loads,4);
pending=a.onPullDownRefresh();await e.flush();assert.equal(e.requests.length,3);e.respond(2,{items:[]});await pending;
e.advance(45001);pending=b.loadData();await e.flush();assert.equal(e.requests.length,4);e.respond(3,{items:[]});await pending;
});
test('permission reads remain live and longer profile TTL does not extend list TTL',async()=>{
const e=setup();let p=e.api.periodList();await e.flush();e.respond(0,{cycles:[]});await p;
p=e.api.periodList();await e.flush();e.respond(1,{cycles:[]});await p;assert.equal(e.requests.length,2);
p=e.api.coupleInfo();await e.flush();e.respond(2,{partner1:{id:1}});await p;e.advance(45001);await e.api.coupleInfo();assert.equal(e.requests.length,3);
});
test('actual diary detail keeps data on return and updates after editing in a child page',async()=>{
const e=setup();e.load(path.join(__dirname,'../pages/diary_detail/diary_detail.js'));
const definition=e.definitions[0];
const page={...definition,data:{...definition.data,diaryId:7},setData(values){Object.assign(this.data,values);}};
let pending=Promise.all([page.loadDiary(),page.loadComments()]);await e.flush();
assert.equal(e.requests.length,2);e.respond(0,{id:7,title:'before',images:[]});e.respond(1,[]);await pending;
page.onShow();await e.flush();assert.equal(e.requests.length,2);assert.equal(page.data.diary.title,'before');
pending=e.api.diaryUpdate({id:7,title:'after'});await e.flush();e.respond(2,{success:true});await pending;
page.onShow();await e.flush();assert.equal(e.requests.length,5);assert.equal(page.data.loading,false);
e.respond(3,{id:7,title:'after',images:[]});e.respond(4,[]);
await Promise.all([page.loadDiary(),page.loadComments()]);assert.equal(page.data.diary.title,'after');
});
test('manual refresh during an existing load waits and then fetches the newest data',async()=>{
const e=setup();const definition=e.withDataCache({data:{},async loadData(){this.data.result=await e.api.diaryList();},onRefresh(){return this.loadData();}},{loadData:['diaryList']});
const page={...definition,data:{}};const old=page.loadData();await e.flush();
const refresh=page.onRefresh();e.respond(0,{items:[{id:'old'}]});await old;await e.flush();
assert.equal(e.requests.length,2);e.respond(1,{items:[{id:'new'}]});await refresh;
assert.equal(page.data.result.items[0].id,'new');
});
test('switching back to records resets scroll while retaining cached content without a loader',async()=>{
const e=setup();e.load(path.join(__dirname,'../pages/discover/discover.js'));
const definition=e.definitions[0];const updates=[];let tabSyncs=0;
const page={...definition,data:{...definition.data},getTabBar:()=>({syncActive(){tabSyncs++;}}),setData(values, callback){updates.push(values);Object.assign(this.data,values);if(callback)callback();}};
const pending=page.loadStats();await e.flush();e.respond(0,[]);e.respond(1,[]);e.respond(2,{items:[{id:7,content:'hello'}],total:1});await pending;
page.onShow();await e.flush();
page.onScroll({detail:{scrollTop:600}});page._savePosBeforeLeave();assert.equal(e.storage.tabScrollPositions.discover.scrollTop,600);
const {resetScrollOnTabShow}=e.load(path.join(__dirname,'../utils/scroll-pos.js'));
resetScrollOnTabShow({data:{scrollTop:0}},'profile');
updates.length=0;page.onShow();await e.flush();assert.equal(tabSyncs,2);assert.equal(e.requests.length,3);
assert.equal(page.data.entries[0].id,7);assert.ok(updates.every(value=>value.loading!==true));
assert.deepEqual(updates.filter(value=>'scrollTop' in value).map(value=>value.scrollTop),[600,0]);
assert.equal(page._scrollTop,0);assert.equal(e.storage.tabScrollPositions.discover.scrollTop,0);
page.onScroll({detail:{scrollTop:250}});updates.length=0;page.onShow();await e.flush();
assert.ok(updates.every(value=>!('scrollTop' in value)));assert.equal(page._scrollTop,250);assert.equal(e.requests.length,3);
});
test('photo metadata updates invalidate album and photo caches',async()=>{
const e=setup();let pending=Promise.all([e.api.photos(7),e.api.albumList()]);await e.flush();e.respond(0,[]);e.respond(1,[]);await pending;
pending=e.api.photoUpdate({id:1,taken_at:'',location:'公园',activity:'散步'});await e.flush();e.respond(2,{success:true});await pending;
pending=Promise.all([e.api.photos(7),e.api.albumList()]);await e.flush();assert.equal(e.requests.length,5);e.respond(3,[]);e.respond(4,[]);await pending;
});