init: initial commit
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const vm = require('node:vm');
|
||||
const path = require('node:path');
|
||||
function setup() {
|
||||
const storage = new Map();
|
||||
const logins = [], requests = [];
|
||||
let app;
|
||||
const wx = {
|
||||
getStorageSync: key => storage.get(key), setStorageSync: (key,value) => storage.set(key,value),
|
||||
removeStorageSync: key => storage.delete(key), getStorageInfoSync: () => ({keys:[...storage.keys()]}),
|
||||
login: options => logins.push(options), request: options => requests.push(options),
|
||||
getNetworkType: options => options.success({networkType:'wifi'}), onNetworkStatusChange(){},
|
||||
hideLoading(){}, showToast(){}
|
||||
};
|
||||
const ctx = {wx, getApp:()=>app, console, Promise, setTimeout, clearTimeout,
|
||||
module:{exports:{}}, require: name => require(path.join(__dirname,'../utils', name))};
|
||||
vm.runInNewContext(fs.readFileSync(path.join(__dirname,'../utils/request.js'),'utf8'),ctx);
|
||||
const request = ctx.module.exports;
|
||||
ctx.require = () => request;
|
||||
ctx.App = definition => {app=definition;};
|
||||
vm.runInNewContext('(function(){' + fs.readFileSync(path.join(__dirname,'../app.js'),'utf8') + '\n})()',ctx);
|
||||
return {app,wx,storage,logins,requests,api:request.api};
|
||||
}
|
||||
const flush = async()=> {for(let i=0;i<12;i++) await Promise.resolve();};
|
||||
async function finishLogin(env, token='fresh') {
|
||||
env.logins.at(-1).success({code:'wechat-code'});
|
||||
await flush();
|
||||
const login = env.requests.findLast(req=>req.url.endsWith('/wechatLogin'));
|
||||
assert.ok(login, 'login endpoint bypasses auth gate');
|
||||
login.success({statusCode:200,data:{token,userId:87}});
|
||||
await flush();
|
||||
}
|
||||
test('cold start refreshes even cached token and holds concurrent business requests until login completes',async()=>{
|
||||
const env=setup(); env.storage.set('token','old'); env.app.onLaunch();
|
||||
const first=env.api.coupleInfo(), second=env.api.diaryList();
|
||||
await flush(); assert.equal(env.logins.length,1); assert.equal(env.requests.length,0);
|
||||
await finishLogin(env);
|
||||
assert.equal(env.app.globalData.authState,'ready');
|
||||
const business=env.requests.filter(req=>!req.url.endsWith('/wechatLogin'));
|
||||
assert.equal(business.length,2); business.forEach(req=>{assert.equal(req.header.Authorization,'Bearer fresh');req.success({statusCode:200,data:[]});});
|
||||
await Promise.all([first,second]);
|
||||
});
|
||||
test('first launch without any stored token issues no anonymous business request',async()=>{
|
||||
const env=setup();env.app.onLaunch();const pending=env.api.periodList();
|
||||
await flush();assert.equal(env.requests.length,0);assert.equal(env.logins.length,1);
|
||||
await finishLogin(env);const business=env.requests.find(req=>req.url.endsWith('/periodList'));
|
||||
assert.equal(business.header.Authorization,'Bearer fresh');
|
||||
business.success({statusCode:200,data:{cycles:[],canAccess:true}});await pending;
|
||||
});
|
||||
test('login failure rejects waiting loads and explicit retry obtains a usable token',async()=>{
|
||||
const env=setup();env.app.onLaunch();
|
||||
const failed=assert.rejects(env.api.diaryList(),error=>error.authFailed===true);
|
||||
env.logins[0].success({});await failed;await flush();
|
||||
assert.equal(env.app.globalData.authState,'error');assert.equal(env.requests.length,0);
|
||||
await assert.rejects(env.app.ensureAuth());assert.equal(env.logins.length,1);
|
||||
const retry=env.app.silentLogin();await finishLogin(env);await retry;
|
||||
assert.equal(env.storage.get('token'),'fresh');
|
||||
});
|
||||
test('parallel 401 responses share token refresh and retry at most once',async()=>{
|
||||
const env=setup();env.app.onLaunch();await finishLogin(env,'first');
|
||||
const one=env.api.coupleInfo(),two=env.api.diaryList();await flush();
|
||||
const old=env.requests.filter(req=>!req.url.endsWith('/wechatLogin'));
|
||||
old.forEach(req=>req.success({statusCode:401,data:{error:'expired'}}));await flush();
|
||||
assert.equal(env.logins.length,2);await finishLogin(env,'second');
|
||||
const retried=env.requests.filter(req=>req.header.Authorization==='Bearer second');assert.equal(retried.length,2);
|
||||
const failed=assert.rejects(one,error=>error._statusCode===401);
|
||||
retried[0].success({statusCode:401,data:{error:'still unauthorized'}});retried[1].success({statusCode:200,data:[]});
|
||||
await failed;await two;assert.equal(env.logins.length,2);
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const vm = require('node:vm');
|
||||
const path = require('node:path');
|
||||
const root = path.join(__dirname, '..');
|
||||
function formatter() {
|
||||
const context = {module:{exports:{}}, require(){return {getImageUrl:p=>p ? 'media/' + p : ''};}};
|
||||
vm.runInNewContext(fs.readFileSync(path.join(root,'utils/journal.js'),'utf8'),context);
|
||||
return context.module.exports;
|
||||
}
|
||||
function records(api) {
|
||||
let definition; const routes=[];
|
||||
vm.runInNewContext(fs.readFileSync(path.join(root,'pages/discover/discover.js'),'utf8'), {
|
||||
Page:p=>definition=p, console:{error(){}}, setTimeout:f=>f(),
|
||||
wx:{getStorageSync:()=> 'token',navigateTo:p=>routes.push(p.url)},
|
||||
require(name) {
|
||||
if(name.endsWith('/request'))return {api};
|
||||
if(name.endsWith('/journal'))return formatter();
|
||||
if(name.endsWith('/layout'))return {getLayout:()=>({})};
|
||||
return {saveScrollPosition(){},getScrollPosition:()=>0};
|
||||
}
|
||||
});
|
||||
return {page:{...definition,data:{...definition.data},setData(v){Object.assign(this.data,v);}},routes};
|
||||
}
|
||||
test('journal handles both stored image formats, malformed images and text-only entries',()=>{
|
||||
const {diaryEntry}=formatter();
|
||||
for(const images of [['one.jpg','two.jpg'],'["one.jpg","two.jpg"]']) {
|
||||
const entry=diaryEntry({id:314,images,content:'a\n b',created_at:'2026-10-04 18:32:00'});
|
||||
assert.equal(entry.cover,'media/one.jpg');assert.equal(entry.imageCount,2);assert.equal(entry.summary,'a b');assert.equal(entry.dateLabel,'2026.10.04');assert.equal(entry.id,314);
|
||||
}
|
||||
for(const images of ['invalid','{}',null])assert.equal(diaryEntry({images}).cover,'');
|
||||
});
|
||||
test('records filters use fresh API data and open registered detail/publish destinations',async()=>{
|
||||
const {page,routes}=records({timelineList:async()=>[{id:3,event_date:'2026-10-12',title:'旅行'}],albumList:async()=>[{id:4,cover:'a.jpg',photo_count:2}],diaryList:async()=>({items:[{id:314,images:'["b.jpg"]'}],total:1})});
|
||||
await page.loadStats();assert.equal(page.data.entries[0].id,314);assert.equal(page.data.entries[0].cover,'media/b.jpg');assert.equal(page.data.memories[0].dateDay,'12');assert.equal(page.data.albums[0].cover,'media/a.jpg');assert.equal(page.data.loading,false);
|
||||
page.selectFilter({currentTarget:{dataset:{filter:'album'}}});assert.equal(page.data.activeFilter,'album');
|
||||
page.openJournalEntry({currentTarget:{dataset:{id:314}}});page.openAlbum({currentTarget:{dataset:{id:4}}});
|
||||
page.showPublish();page.createMemory();assert.equal(page.data.publishOpen,false);
|
||||
assert.deepEqual(routes,['/pages/diary_detail/diary_detail?id=314','/pages/album/album?album_id=4','/pages/publish/publish?type=timeline']);
|
||||
});
|
||||
test('refresh failure clears busy states and retry replaces old records with authoritative empty data',async()=>{
|
||||
let fail=true;
|
||||
const {page}=records({timelineList:async()=>[],albumList:async()=>[],diaryList:async()=>{if(fail)throw Error('offline');return {items:[],total:0};}});
|
||||
page.data.entries=[{id:'old'}];await page.onRefresh();assert.equal(page.data.loadError,true);assert.equal(page.data.refreshing,false);assert.equal(page.data.loading,false);
|
||||
fail=false;await page.loadStats();assert.equal(page.data.loadError,false);assert.equal(page.data.entries.length,0);
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const vm = require('node:vm');
|
||||
const {tabForPage, paths} = require('../utils/navigation');
|
||||
const root = path.join(__dirname, '..');
|
||||
test('only the three main pages mount a tab bar; every secondary page removes its registration and reserved space',()=>{
|
||||
const pages=JSON.parse(fs.readFileSync(path.join(root,'app.json'),'utf8')).pages;
|
||||
for(const page of pages){
|
||||
const main=Object.values(paths).includes(page);
|
||||
const markup=fs.readFileSync(path.join(root,page+'.wxml'),'utf8');
|
||||
const components=JSON.parse(fs.readFileSync(path.join(root,page+'.json'),'utf8')).usingComponents||{};
|
||||
assert.equal(/<app-tabbar\b/.test(markup),main,page);
|
||||
assert.equal(!!tabForPage(page),main,page);
|
||||
if(!main){assert.ok(!components['app-tabbar'],page);assert.ok(markup.includes('app-shell--secondary'),page);}
|
||||
}
|
||||
assert.equal(tabForPage('/pages/profile/profile?bind=1'),'profile');
|
||||
assert.equal(tabForPage('unknown'),'');
|
||||
});
|
||||
test('component selects the main page, hides on child routes and avoids reloading its current tab',()=>{
|
||||
let definition,route='pages/profile/profile';const navigations=[];
|
||||
vm.runInNewContext(fs.readFileSync(path.join(root,'components/app-tabbar/app-tabbar.js'),'utf8'),{
|
||||
Component:c=>definition=c,require:()=>({tabForPage,paths}),getCurrentPages:()=>[{route}],wx:{reLaunch:o=>navigations.push(o.url)}
|
||||
});
|
||||
const component={...definition.methods,data:{...definition.data},setData(v){Object.assign(this.data,v);}};
|
||||
definition.lifetimes.attached.call(component);assert.equal(component.data.active,'profile');
|
||||
component.navigate({currentTarget:{dataset:{tab:'profile'}}});assert.equal(navigations.length,0);
|
||||
component.navigate({currentTarget:{dataset:{tab:'records'}}});assert.deepEqual(navigations,['/pages/discover/discover']);
|
||||
route='pages/settings/settings';definition.pageLifetimes.show.call(component);assert.equal(component.data.active,'');
|
||||
const markup=fs.readFileSync(path.join(root,'components/app-tabbar/app-tabbar.wxml'),'utf8');
|
||||
assert.ok(markup.includes('wx:if="{{active}}"'));
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const vm = require('node:vm');
|
||||
const path = require('node:path');
|
||||
function load(file, wx) {
|
||||
const context = { module: { exports: {} }, wx, console, Promise, Date, require(name) { return require(path.join(__dirname, '../utils', name)); } };
|
||||
vm.runInNewContext(fs.readFileSync(path.join(__dirname, '..', file), 'utf8'), context);
|
||||
return context.module.exports;
|
||||
}
|
||||
test('GET business parameters and POST bodies use clean paths with auth', async () => {
|
||||
const calls=[];
|
||||
const {api}=load('utils/request.js', { getStorageSync:()=> 'test-token', hideLoading(){}, request(o){calls.push(o);o.success({statusCode:200,data:{items:[], pageSize:20}});} });
|
||||
const response = await api.diaryList({page:2,page_size:20});
|
||||
assert.equal(response.page_size,20);
|
||||
await api.periodVisibleSet({period_visible:0});
|
||||
assert.equal(calls[0].url,'https://past.sundynix.cn/api/diaryList?page=2&pageSize=20');
|
||||
assert.equal(calls[0].header.Authorization,'Bearer test-token');
|
||||
assert.equal(calls[1].url,'https://past.sundynix.cn/api/periodVisibleSet');
|
||||
assert.equal(calls[1].data.periodVisible,0);
|
||||
});
|
||||
test('multipart upload retains action and token when Android omits headers',async()=>{
|
||||
let upload;
|
||||
const {uploadFileCompat}=load('utils/request.js',{getStorageSync:()=> 'test-token',compressImage(o){o.fail();},uploadFile(o){upload=o;o.success({statusCode:200,data:'{"success":true,"path":"uploads/test.jpg"}'});} });
|
||||
await uploadFileCompat({url:'https://past.sundynix.cn/api/uploadPartnerAvatar',filePath:'test.jpg',name:'file',formData:{partner_id:87}});
|
||||
assert.equal(upload.url,'https://past.sundynix.cn/api/uploadPartnerAvatar?token=test-token');
|
||||
assert.equal(upload.formData.action,'uploadPartnerAvatar');
|
||||
assert.equal(upload.formData.token,'test-token');
|
||||
assert.equal(upload.formData.partnerId,87);
|
||||
assert.equal(upload.formData.partner_id,undefined);
|
||||
});
|
||||
test('navigation responds to phone statusbar and capsule geometry, with fallback',()=>{
|
||||
for(const device of [{status:20,top:24,height:32,width:360,left:263},{status:54,top:58,height:32,width:393,left:285}]){
|
||||
const {getLayout}=load('utils/layout.js',{getWindowInfo:()=>({statusBarHeight:device.status,windowWidth:device.width}),getMenuButtonBoundingClientRect:()=>({top:device.top,height:device.height,left:device.left})});
|
||||
const layout=getLayout();assert.equal(layout.navHeight,device.status+40);assert.equal(layout.navSide,device.width-device.left+12);
|
||||
}
|
||||
const {getLayout}=load('utils/layout.js',{});assert.equal(getLayout().navHeight,64);
|
||||
});
|
||||
test('failed homepage load ends the recent-record spinner',async()=>{
|
||||
let definition;
|
||||
const api={coupleInfo:async()=>{throw { _statusCode:404 };}};
|
||||
vm.runInNewContext(fs.readFileSync(path.join(__dirname,'../pages/index/index.js'),'utf8'),{
|
||||
Page(p){definition=p;},require(name){return name.endsWith('/request')?{api,getImageUrl:v=>v}:name.endsWith('/layout')?{getLayout:()=>({})}:{saveScrollPosition(){},getScrollPosition(){return 0;}};},
|
||||
wx:{getStorageSync:()=> 'test-token'}, console:{error(){}}, Date, setTimeout
|
||||
});
|
||||
const page={...definition,data:{...definition.data},setData(data){Object.assign(this.data,data);}};
|
||||
await page.loadData();
|
||||
assert.equal(page.data.homeError,true);assert.equal(page.data.statsLoading,false);assert.equal(page.data.statsError,true);assert.equal(page._loadingData,false);
|
||||
});
|
||||
Reference in New Issue
Block a user