62 lines
8.2 KiB
JavaScript
62 lines
8.2 KiB
JavaScript
const test=require('node:test'),assert=require('node:assert/strict'),fs=require('node:fs'),vm=require('node:vm'),{parse}=require('acorn');
|
|
const C=require('../src/core.js');
|
|
const source=fs.readFileSync('src/app.js','utf8'),names=['version','parseStored','localStored','writeProgress','initStore','saveRecord'],functions=parse(source,{ecmaVersion:2017}).body[0].expression.callee.body.body.filter(n=>n.type==='FunctionDeclaration'&&names.includes(n.id.name)).map(n=>source.slice(n.start,n.end)).join('\n');
|
|
function store({local={},native={},readFail=false,writeFail=false,delay}={}){
|
|
const written=[],messages=[],localData={...local};
|
|
const storage={getItem:key=>localData[key]||null,setItem:(key,value)=>{if(writeFail)throw Error('denied');localData[key]=value;written.push({key,data:JSON.parse(value)});}};
|
|
const api={getStorage:async({key})=>{if(key==='shanhe-progress-v2'&&delay)await delay;if(key==='shanhe-progress-v2'&&readFail)throw Error('offline');return {data:native[key]||null};},setStorage:async value=>{if(writeFail)throw Error('denied');written.push({key:value.key,data:JSON.parse(value.data)});}};
|
|
const box={C,Math,JSON,Object,Error,Promise,records:{},triumphs:{},storeReadable:true,storeReady:false,storageNative:false,miniTool:null,saveChain:Promise.resolve(),window:{localStorage:storage,xhs:native===false?null:{launchOptions:{miniToolEnv:{buildVersion:9460001}},miniTool:api}},updateUI(){},renderJournal(){},toast:msg=>messages.push(msg),$:()=>({classList:{contains:()=>false},hasAttribute:()=>false})};
|
|
vm.createContext(box);vm.runInContext(functions,box);return {box,written,messages,localData};
|
|
}
|
|
const serialized=value=>JSON.stringify(value);
|
|
const plain=value=>JSON.parse(JSON.stringify(value));
|
|
test('每档分数跨弹性取最高,旧无吸附档位纪录归入原难度且不推断通关',()=>{
|
|
const stats=C.journeyStats('100000',{'100000:4:0:s0':300,'100000:4:3:s0':650,'100000:4:1:s1':500,'100000:1':900,'100000:4:1:s2':700,'110000:4:1:s0':9999},{});
|
|
assert.deepEqual(stats,{best:900,scores:[650,500,900],wins:[false,false,false]});
|
|
});
|
|
test('零分已挑战与从未挑战分别显示,非法分数和奖杯不进入战绩',()=>{
|
|
assert.deepEqual(C.journeyStats('110000',{'110000:4:1:s0':0,'110000:4:1:s1':Infinity,'110000:4:1:s2':-5},{'110000:s0':'true','110000:s1':1,'110000:s2':true,'110000:s3':true}),{best:0,scores:[0,null,null],wins:[false,false,true]});
|
|
assert.deepEqual(C.safeTriumphs([]),{});assert.deepEqual(C.safeTriumphs(null),{});
|
|
});
|
|
test('通关按关卡和吸附难度点亮,改变弹性和重复通关均不增加奖杯',()=>{
|
|
let progress={};for(let bounce=0;bounce<4;bounce++)progress=C.finishProgress(progress,'100000',0,bounce,100+bounce,true);
|
|
assert.equal(Object.keys(progress.triumphs).length,1);assert.equal(C.journeyStats('100000',progress.records,progress.triumphs).best,103);
|
|
progress=C.finishProgress(progress,'100000',1,0,50,true);progress=C.finishProgress(progress,'100000',2,3,80,true);assert.deepEqual(C.journeyStats('100000',progress.records,progress.triumphs).wins,[true,true,true]);
|
|
assert.deepEqual(C.journeyStats('110000',progress.records,progress.triumphs).wins,[false,false,false]);
|
|
});
|
|
test('主动结束、失败仅记录分数,不能点亮奖杯;低分不覆盖原最高分',()=>{
|
|
let progress=C.finishProgress({},'110000',2,1,500,false);progress=C.finishProgress(progress,'110000',2,1,50,false);assert.deepEqual(C.journeyStats('110000',progress.records,progress.triumphs),{best:500,scores:[null,null,500],wins:[false,false,false]});
|
|
progress=C.finishProgress(progress,'110000',2,1,50,true);assert.equal(progress.records['110000:4:1:s2'],500);assert.equal(progress.triumphs['110000:s2'],true);
|
|
for(const args of [['110000',3,1,100,true],['110000',1,9,100,true],['bad',1,1,100,true],['110000',1,1,NaN,true]])assert.deepEqual(C.finishProgress({},...args),{records:{},triumphs:{}});
|
|
});
|
|
test('全部35关三档通关独立累计105杯,全国、27省内与7县市区关均覆盖',()=>{
|
|
const box={window:{}};vm.runInNewContext(fs.readFileSync('src/maps.js','utf8'),box);const levels=box.window.MAP_DATA.levels;assert.equal(levels.length,35);assert.equal(levels.filter(l=>l.kind==='prefecture').length,27);let progress={};
|
|
for(const l of levels)for(let snap=0;snap<3;snap++)progress=C.finishProgress(progress,l.id,snap,1,Number(l.id)+snap,true);
|
|
assert.equal(Object.keys(progress.triumphs).length,105);for(const l of levels)assert.deepEqual(C.journeyStats(l.id,progress.records,progress.triumphs).wins,[true,true,true]);
|
|
});
|
|
test('合并多来源进度保留最高分与已获奖杯,不修改输入且不接收任意字段',()=>{
|
|
const a={records:{'100000:4:1:s0':50},triumphs:{'100000:s0':true}},b={records:{'100000:4:1:s0':40,'110000:4:1:s1':100},triumphs:{'100000:s0':false,'110000:s1':true,bad:true}};
|
|
assert.deepEqual(C.mergeProgress(a,b),{records:{'100000:4:1:s0':50,'110000:4:1:s1':100},triumphs:{'100000:s0':true,'110000:s1':true}});assert.deepEqual(a,{records:{'100000:4:1:s0':50},triumphs:{'100000:s0':true}});
|
|
});
|
|
test('迁移本地与原生旧高分和新奖杯,统一原子保存到新key且保留旧key',async()=>{
|
|
const local={'shanhe-records-v1':serialized({'100000:4':900}),'shanhe-progress-v2':serialized({records:{'100000:4:1:s0':500},triumphs:{'100000:s0':true}})},native={'shanhe-records-v1':serialized({'100000:4':800}),'shanhe-progress-v2':serialized({records:{'110000:4:1:s1':600},triumphs:{'110000:s1':true}})};
|
|
const a=store({local,native});a.box.storePromise=a.box.initStore();await a.box.storePromise;assert.equal(a.box.records['100000:4'],900);assert.equal(a.box.records['110000:4:1:s1'],600);assert.equal(Object.keys(a.box.triumphs).length,2);assert.equal(a.written.length,1);assert.equal(a.written[0].key,'shanhe-progress-v2');assert.deepEqual(a.written[0].data,plain({records:a.box.records,triumphs:a.box.triumphs}));assert.equal(a.localData['shanhe-records-v1'],local['shanhe-records-v1']);
|
|
});
|
|
test('异步加载期间结束一局,不会被迟到的旧数据覆盖,新分数与奖杯持久化',async()=>{
|
|
let release;const delay=new Promise(resolve=>{release=resolve;}),a=store({delay,native:{'shanhe-progress-v2':serialized({records:{'110000:4:1:s1':999},triumphs:{'110000:s1':true}})}});
|
|
a.box.storePromise=a.box.initStore();a.box.game={level:{id:'100000'},snapIndex:0,bounceIndex:2,score:1234,endReason:'won'};a.box.saveRecord();release();await a.box.saveChain;
|
|
const final=a.written.at(-1).data;assert.equal(final.records['100000:4:2:s0'],1234);assert.equal(final.records['110000:4:1:s1'],999);assert.equal(final.triumphs['100000:s0'],true);assert.equal(final.triumphs['110000:s1'],true);
|
|
});
|
|
test('只有引擎won结果发杯,暂停结束和堆顶失败不会误发;刷新恢复',async()=>{
|
|
const a=store({native:false});a.box.storePromise=a.box.initStore();await a.box.storePromise;
|
|
for(const endReason of ['stopped','lost','won']){a.box.game={level:{id:'110000'},snapIndex:1,bounceIndex:3,score:400,endReason};a.box.saveRecord();await a.box.saveChain;assert.equal(a.box.triumphs['110000:s1']===true,endReason==='won');}
|
|
const b=store({native:false,local:a.localData});b.box.storePromise=b.box.initStore();await b.box.storePromise;assert.equal(b.box.triumphs['110000:s1'],true);assert.equal(b.box.records['110000:4:3:s1'],400);
|
|
});
|
|
test('原生战绩读取失败时不覆盖未读缓存,内存仍记录新奖杯并说明限制',async()=>{
|
|
const a=store({readFail:true});a.box.storePromise=a.box.initStore();await a.box.storePromise;a.box.game={level:{id:'100000'},snapIndex:0,bounceIndex:1,score:500,endReason:'won'};a.box.saveRecord();await a.box.saveChain;
|
|
assert.equal(a.written.length,0);assert.equal(a.box.triumphs['100000:s0'],true);assert.ok(a.messages.some(m=>m.includes('读取暂不可用')));assert.ok(a.messages.some(m=>m.includes('存储暂不可用')));
|
|
});
|
|
test('写入失败不会阻塞游玩或清除内存成绩,失败有可读提示',async()=>{
|
|
for(const native of [false,{}]){const a=store({native,writeFail:true});a.box.storePromise=a.box.initStore();await a.box.storePromise;a.box.game={level:{id:'100000'},snapIndex:2,bounceIndex:0,score:800,endReason:'won'};a.box.saveRecord();await a.box.saveChain;assert.equal(a.box.records['100000:4:0:s2'],800);assert.equal(a.box.triumphs['100000:s2'],true);assert.ok(a.messages.some(m=>m.includes('存储暂不可用')));}
|
|
});
|