Files
MatchMaster/tools/test-pyramid-settlement.cjs
T
2026-10-12 15:00:49 +08:00

150 lines
16 KiB
JavaScript

const test=require('node:test'),assert=require('node:assert/strict'),fs=require('fs'),path=require('path'),vm=require('vm'),ts=require('typescript');
const prefab=JSON.parse(fs.readFileSync('assets/pyramid/prefab/PyramidSettlement.prefab'));
test('settlement uses the October artwork and only references local pyramid images',()=>{
const frames=new Set();function scan(dir){for(const e of fs.readdirSync(dir,{withFileTypes:true})){const p=path.join(dir,e.name);if(e.isDirectory())scan(p);else if(p.endsWith('.meta')){const m=JSON.parse(fs.readFileSync(p));frames.add(m.uuid);for(const s of Object.values(m.subMetas||{}))frames.add(s.uuid);}}}scan('assets/pyramid');
for(const sprite of prefab.filter(n=>n.__type__==='cc.Sprite'))assert.ok(frames.has(sprite._spriteFrame.__uuid__),sprite._spriteFrame.__uuid__);
const panel=prefab.find(n=>n._name==='panel');assert.deepEqual(panel._children.map(r=>prefab[r.__id__]._name),['claim']);
const original=JSON.parse(fs.readFileSync('assets/passCheck/prefab/PassSettlement.prefab'));
assert.deepEqual(prefab.find(n=>n._name==='rewards')._trs,original.find(n=>n._name==='rewards')._trs);
assert.equal(prefab.find(n=>n._name==='header')._contentSize.height,444);
const message=prefab.find(n=>n.__type__==='cc.Label');assert.equal(message._string,'活动已结束,\n您还有未领取的奖励如下:');
const title=prefab.find(n=>n._name==='title');assert.equal(prefab[title._components[0].__id__]._spriteFrame.__uuid__,JSON.parse(fs.readFileSync('assets/pyramid/art/title.png.meta')).subMetas.title.uuid);
for(const source of ['assets/common/publicFont.png','assets/common/publicFont.plist','assets/res/move/jibijj.png','assets/passCheck/img/settlement/reward_card.png','assets/passCheck/img/settlement/btn_claim.png']){
const dest='assets/pyramid/art/settlement/'+path.basename(source);assert.deepEqual(fs.readFileSync(source),fs.readFileSync(dest));
assert.notEqual(JSON.parse(fs.readFileSync(source+'.meta')).uuid,JSON.parse(fs.readFileSync(dest+'.meta')).uuid);
}
const atlas=JSON.parse(fs.readFileSync('assets/pyramid/art/settlement/publicFont.plist.meta'));
assert.equal(atlas.rawTextureUuid,JSON.parse(fs.readFileSync('assets/pyramid/art/settlement/publicFont.png.meta')).uuid);
});
function fixture(){
class Node{constructor(name){this.name=name;this.children=[];this.handlers={};this.width=1080;this.height=1920;this.active=true;this.scaleX=this.scaleY=name==='item'?0.7:1;}addChild(n){n.parent=this;this.children.push(n);}removeAllChildren(){this.children=[];}getChildByName(name){return this.children.find(n=>n.name===name);}setScale(s){this.scaleX=this.scaleY=s;}on(event,fn,owner){this.handlers[event]=()=>fn.call(owner);}addComponent(C){const c=new C();c.node=this;return c;}setContentSize(w,h){this.width=w;this.height=h;}}
class Sprite{}Sprite.SizeMode={CUSTOM:0};const cc={Node,Sprite,SpriteAtlas:class{},Component:class{},_decorator:{ccclass:c=>c,property:()=>()=>{}},systemEvent:{on(){},off(){}},SystemEvent:{EventType:{KEY_DOWN:'key'}},macro:{KEY:{escape:27}}};
const context={exports:{},cc};vm.runInNewContext(ts.transpileModule(fs.readFileSync('assets/pyramid/scripts/PyramidSettlement.ts','utf8'),{compilerOptions:{target:ts.ScriptTarget.ES2017,module:ts.ModuleKind.CommonJS,experimentalDecorators:true}}).outputText,context);
const panel=new context.exports.default(),root=new Node('PyramidSettlement');root.parent=new Node('host');
function children(n,names){for(const name of names)n.addChild(new Node(name));}
children(root,['panel']);const p=root.getChildByName('panel');children(p,['claim']);const claim=p.getChildByName('claim');children(claim,['rewards','closeButton','claimButton']);
const rewards=claim.getChildByName('rewards');children(rewards,['topRow','bottomRow']);
for(let i=0;i<5;i++){const cell=new Node('slot'+i);rewards.children[i<3?0:1].addChild(cell);children(cell,['item']);children(cell.children[0],['icon','amount']);children(cell.children[0].children[0],['coin','freeze','hammer','magic','infinite_health']);}
const atlas=JSON.parse(fs.readFileSync('assets/pyramid/art/settlement/publicFont.plist.meta'));
panel.node=root;panel.numberAtlas={getSpriteFrame:name=>({name,getRect:()=>atlas.subMetas[name+'.png']})};let closed=0;
return {panel,claim,rewards,show:(goods,callback=()=>closed++)=>panel.show({goods},callback),closed:()=>closed};
}
test('settlement shows aggregated goods and image-based hours/minutes; claim blocks repeated clicks',()=>{
const f=fixture();f.show([{id:1001,count:200},{id:2001,count:3},{id:2005,count:(27*60+25)*60}]);
const slots=f.rewards.children.flatMap(r=>r.children);assert.equal(slots[3].active,false);
const icon=slots[2].getChildByName('item').getChildByName('icon');assert.equal(icon.children.find(n=>n.active).name,'infinite_health');
const amount=slots[2].getChildByName('item').getChildByName('amount');
assert.deepEqual(amount.children.filter(n=>n.y>0).map(n=>n.name),['big_2','big_7','big_xiaoshi']);
assert.deepEqual(amount.children.filter(n=>n.y<0).map(n=>n.name),['big_2','big_5','big_fenzhong']);
assert.equal(amount.scaleX,1);assert.equal(amount.x,0);
assert.ok(amount.children.every(n=>n.height===45));
for(const count of [3600,25*60]){
const single=fixture();single.show([{id:2005,count}]);
const digits=single.rewards.children[0].children[0].getChildByName('item').getChildByName('amount').children;
assert.ok(digits.every(n=>n.y===0));
assert.equal(digits[digits.length-1].name,count===3600?'big_xiaoshi':'big_fenzhong');
}
f.claim.getChildByName('claimButton').handlers.click();f.claim.getChildByName('closeButton').handlers.click();assert.equal(f.closed(),1);
});
test('close never claims; pending claim blocks close and failed claim can be retried',async()=>{
const f=fixture(),actions=[];let reject;
f.show([],claim=>{actions.push(claim);if(claim)return new Promise((_,r)=>reject=r);});
const pending=f.panel.claim();f.panel.close();f.panel.claim();assert.deepEqual(actions,[true]);
reject(Error('offline'));await pending;f.panel.close();assert.deepEqual(actions,[true,false]);
const retry=fixture();let calls=0;retry.show([],()=>{if(++calls===1)throw Error('offline');});
await retry.panel.claim();await retry.panel.claim();assert.equal(calls,2);
});
test('highest tier fits full coin/tool counts and two-line stamina without shrinking its icon',()=>{
const f=fixture(),goods=[{id:1001,count:82800},{id:2001,count:262},{id:2002,count:137},{id:2003,count:132},{id:2005,count:1635*60}];
const original=JSON.stringify(goods);f.show(goods);assert.equal(JSON.stringify(goods),original);
const slots=f.rewards.children.flatMap(r=>r.children);
assert.deepEqual(slots.map(s=>s.children[0].children[0].children.find(n=>n.active).name),['coin','freeze','magic','hammer','infinite_health']);
for(const [i,s]of slots.entries()){
const amount=s.children[0].getChildByName('amount');assert.equal(amount.scaleX,1);
const bounds=amount.children.map(n=>[n.x-n.width/2,n.x+n.width/2]);
assert.ok((Math.max(...bounds.map(b=>b[1]))-Math.min(...bounds.map(b=>b[0])))*0.7<=180);
if(i>0&&i<4)assert.equal(amount.children[0].name,'big_cheng');
}
const item=slots[4].children[0],amount=item.getChildByName('amount'),icon=item.getChildByName('icon').getChildByName('infinite_health');
assert.equal(icon.scaleX,1);assert.equal(icon.y,58);
assert.deepEqual(amount.children.filter(n=>n.y>0).map(n=>n.name),['big_2','big_7','big_xiaoshi']);
assert.deepEqual(amount.children.filter(n=>n.y<0).map(n=>n.name),['big_1','big_5','big_fenzhong']);
f.show(goods);assert.equal(amount.children.length,6,'reusing the popup must not duplicate digits');
});
function homeFixture(){
let notices=[],reads=0,loads=0,releases=0,done=0,claims=0,failClaim=false,failLoad=false,failReward=false,close=null,rewardClose=null,settled=true,now=10,syncs=0;const queued=[],shown=[],scheduled=[],animations=[];
class Node{constructor(name){this.name=name;this.children=[];this.width=1080;this.height=1920;}addChild(n){n.parent=this;this.children.push(n);}setContentSize(){}setPosition(){}addComponent(){}destroy(){this.destroyed=true;}getComponent(type){
if(type==='PyramidSettlement')return{show:(notice,callback)=>{shown.push(notice.noticeId);close=callback;}};
if(type==='Reward')return{init:(rewards,callback,durationUnits)=>{assert.ok(this.parent.children.filter(n=>n.name==='PyramidSettlement').every(n=>n.destroyed&&n.active===false));animations.push({rewards,durationUnits});rewardClose=callback;}};
return null;
}}
const state={periodId:'p',startAt:0,endAt:20,status:'active'};
const service={data:()=>state,now:()=>now,sync:async()=>{syncs++;},pendingDelivery:async()=>false,settlements:()=>notices.slice(),readSettlements:async()=>{reads++;return settled;},
claimSettlement:async notice=>{claims++;if(failClaim)throw Error('offline');return notice;},
dismissSettlement:id=>{notices=notices.filter(n=>n.noticeId!==id);},acquireResources:async()=>()=>releases++,bundle:async()=>({load:(p,t,cb)=>{loads++;cb(failLoad?Error('offline'):null,'PyramidSettlement');}})};
const cc={Node,Component:class{scheduleOnce(fn,seconds){scheduled.push({fn,seconds});}unschedule(fn){for(let i=scheduled.length-1;i>=0;i--)if(scheduled[i].fn===fn)scheduled.splice(i,1);}},fx:{GameConfig:{GM_INFO:{uid:'u',token:'t'}}},Prefab:class{},BlockInputEvents:class{},_decorator:{ccclass:c=>c},view:{getVisibleSize:()=>({width:1080,height:1920})},isValid:n=>!!n&&!n.destroyed,warn(){},instantiate:()=>new Node('PyramidSettlement')};
cc.instantiate=name=>new Node(name);
cc.assetManager={getBundle:()=>({load:(p,t,cb)=>{assert.equal(p,'prefab/RewardWindow');cb(failReward?Error('offline'):null,'RewardWindow');}})};
const queue={get:()=>({enqueue:(key,priority,run)=>queued.push({key,priority,run})})};
const context={exports:{},cc,require:name=>({default:name.includes('PyramidService')?service:name.includes('HomePopupQueue')?queue:{}})};
vm.runInNewContext(ts.transpileModule(fs.readFileSync('assets/Script/PyramidView.ts','utf8'),{compilerOptions:{target:ts.ScriptTarget.ES2017,module:ts.ModuleKind.CommonJS,experimentalDecorators:true}}).outputText,context);
const view=new context.exports.default();view.node=new Node('home');view.entry={active:false};
view.account='u:t';view.render=()=>{};view.syncPaymentLoading=()=>{};view.refreshCountdown=()=>{};view.queueDailyReminder=()=>{};
return{view,shown,queued,service,scheduled,animations,setSettled:b=>settled=b,setNow:n=>now=n,syncs:()=>syncs,setNotices:n=>notices=n.map(row=>({goods:[],...row})),setFail:b=>failLoad=b,setRewardFail:b=>failReward=b,setClaimFail:b=>failClaim=b,claims:()=>claims,reads:()=>reads,loads:()=>loads,releases:()=>releases,done:()=>done,close:()=>close(false),claim:()=>close(true),closeReward:()=>rewardClose(),run:()=>queued.shift().run(()=>done++),retry:()=>scheduled.shift().fn.call(view)};
}
const flush=()=>new Promise(resolve=>setImmediate(resolve));
test('settlement closes before battle-pass rewards and holds the queue until each animation finishes',async()=>{
const f=homeFixture(),goods=[{id:1001,count:82800},{id:2001,count:262},{id:2002,count:137},{id:2003,count:132},{id:2005,count:98100}];
f.setNotices([{noticeId:'p1',goods},{noticeId:'p2',goods}]);f.view.queueSettlement();f.run();await flush();
await f.claim();await f.claim();assert.equal(f.claims(),1);assert.equal(f.animations.length,1);assert.deepEqual(f.shown,['p1']);assert.equal(f.done(),0);assert.equal(f.releases(),0);
assert.equal(f.animations[0].durationUnits,true);
assert.deepEqual(JSON.parse(JSON.stringify(f.animations[0].rewards)),goods.map((g,i)=>({type:['coin','freeze','hammer','magic','infinite_health'][i],count:g.count})));
assert.equal(f.service.settlements().length,2);f.closeReward();assert.deepEqual(f.shown,['p1','p2']);assert.equal(f.service.settlements().length,1);
f.closeReward();assert.equal(f.service.settlements().length,1,'duplicate animation callbacks must not dismiss the next notice');
await f.claim();assert.equal(f.animations.length,2);f.closeReward();assert.equal(f.done(),1);assert.equal(f.releases(),1);assert.equal(f.service.settlements().length,0);
assert.equal(f.reads(),1,'presentation must not request another credit');
});
test('reward loading failure retains notice; teardown during animation destroys it and releases queue once',async()=>{
const f=homeFixture();f.setNotices([{noticeId:'p',goods:[{id:1001,count:100}]}]);f.setRewardFail(true);f.view.queueSettlement();f.run();await flush();
assert.equal(f.shown.length,0);assert.equal(f.service.settlements().length,1);assert.equal(f.done(),1);
f.setRewardFail(false);f.view.queueSettlement();f.run();await flush();await f.claim();const animation=f.view.settlementRoot;
f.view.finishSettlement();assert.equal(animation.destroyed,true);assert.equal(f.done(),2);assert.equal(f.releases(),2);
f.closeReward();assert.equal(f.done(),2);assert.equal(f.service.settlements().length,1);
});
test('close skips deposit and animation; failed claim retains popup until a successful retry',async()=>{
const f=homeFixture();f.setNotices([{noticeId:'p',goods:[{id:1001,count:20}]}]);f.view.queueSettlement();f.run();await flush();
f.setClaimFail(true);await assert.rejects(f.claim());assert.equal(f.view.settlementRoot.destroyed,undefined);assert.equal(f.animations.length,0);assert.equal(f.done(),0);
f.setClaimFail(false);await f.claim();assert.equal(f.animations.length,1);f.closeReward();assert.equal(f.done(),1);
const closed=homeFixture();closed.setNotices([{noticeId:'p',goods:[{id:1001,count:20}]}]);closed.view.queueSettlement();closed.run();await flush();
await closed.close();assert.equal(closed.claims(),0);assert.equal(closed.animations.length,0);assert.equal(closed.done(),1);
});
test('each home refresh checks expired notices without an entry and closes without granting',async()=>{
const f=homeFixture();f.setNotices([{noticeId:'p1'},{noticeId:'p2'}]);f.view.queueSettlement();f.view.queueSettlement();assert.equal(f.queued.length,1);assert.equal(f.reads(),0);
f.run();await flush();assert.equal(f.reads(),1);assert.deepEqual(f.shown,['p1']);assert.equal(f.done(),0);
f.close();assert.deepEqual(f.shown,['p1','p2']);f.close();assert.equal(f.done(),1);assert.equal(f.releases(),1);
f.view.queueSettlement();assert.equal(f.queued.length,1);f.run();await flush();assert.equal(f.reads(),2);assert.equal(f.loads(),1);
});
test('empty refresh skips the bundle; load failure retains the returned receipt for next home',async()=>{
const empty=homeFixture();empty.view.queueSettlement();empty.run();await flush();assert.equal(empty.loads(),0);assert.equal(empty.done(),1);
const f=homeFixture();f.setNotices([{noticeId:'p'}]);f.setFail(true);f.view.queueSettlement();f.run();await flush();assert.equal(f.done(),1);assert.equal(f.releases(),1);assert.equal(f.service.settlements().length,1);
f.setFail(false);f.view.queueSettlement();f.run();await flush();assert.equal(f.reads(),2);assert.deepEqual(f.shown,['p']);f.close();assert.equal(f.releases(),2);
});
test('home stay does not poll; expiry and foreground trigger settlement reads without relogin',async()=>{
const f=homeFixture();f.view.update(.5);await flush();f.run();await flush();assert.equal(f.reads(),1);
for(let i=0;i<20;i++)f.view.update(.5);assert.equal(f.queued.length,0);
f.setNow(20);f.view.update(.5);await flush();f.run();await flush();assert.equal(f.reads(),2);
for(let i=0;i<20;i++)f.view.update(.5);assert.equal(f.queued.length,0);
f.view.wake();f.view.update(0);await flush();f.run();await flush();assert.equal(f.reads(),3);
f.view.onEnable();f.view.update(0);await flush();f.run();await flush();assert.equal(f.reads(),4);
});
test('settling retries use gold miner delays, are bounded, and do not refresh activity info',async()=>{
const f=homeFixture();f.setSettled(false);f.view.update(.5);await flush();f.run();await flush();
for(const seconds of [2,3,5,7,10,15,25,30,50]){
assert.equal(f.scheduled[0].seconds,seconds);f.retry();f.view.update(0);f.run();await flush();
}
assert.equal(f.reads(),10);assert.equal(f.scheduled.length,0);assert.equal(f.syncs(),1);
f.view.wake();f.view.update(0);await flush();f.run();await flush();assert.equal(f.scheduled[0].seconds,2);
f.setSettled(true);f.retry();f.view.update(0);f.run();await flush();assert.equal(f.scheduled.length,0);
});