Files
shanhe-puzzle/tests/records.test.cjs
T
codex-bot 4845826deb
Shanhe source and offline package checks / verify (pull_request) Failing after 5s
feat: sync Shanhe 2.2 source and add remote package verification
2026-11-01 17:13:46 +08:00

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('存储暂不可用')));}
});