144 lines
15 KiB
JavaScript
144 lines
15 KiB
JavaScript
const { popupDependency } = require('./popup-animation-test-support.cjs');
|
|
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'));
|
|
const rewards=prefab.find(n=>n._name==='rewards'), originalRewards=original.find(n=>n._name==='rewards');
|
|
assert.equal(rewards._trs.array[0],originalRewards._trs.array[0]);
|
|
assert.deepEqual(rewards._trs.array.slice(7),originalRewards._trs.array.slice(7));
|
|
assert.equal(prefab.find(n=>n._name==='header')._contentSize.height,444);
|
|
assert.ok(!prefab.some(n=>n._name==='copy'||n._name==='claimButton'));
|
|
const ended=prefab.find(n=>n._name==='EndedCopy');assert.ok(ended);
|
|
const image=prefab[ended._components[0].__id__];assert.equal(image.__type__,'cc.Sprite');
|
|
const src='assets/coin_madness/texture/ui/ended_copy.png',dest='assets/pyramid/art/settlement/ended_copy.png';
|
|
assert.deepEqual(fs.readFileSync(src),fs.readFileSync(dest));
|
|
const srcMeta=JSON.parse(fs.readFileSync(src+'.meta')),dstMeta=JSON.parse(fs.readFileSync(dest+'.meta'));
|
|
assert.notEqual(srcMeta.uuid,dstMeta.uuid);assert.notEqual(srcMeta.subMetas.ended_copy.uuid,dstMeta.subMetas.ended_copy.uuid);
|
|
assert.equal(image._spriteFrame.__uuid__,dstMeta.subMetas.ended_copy.uuid);
|
|
assert.equal(dstMeta.subMetas.ended_copy.rawTextureUuid,dstMeta.uuid);
|
|
const topRow=prefab.find(n=>n._name==='topRow');
|
|
const worldY=(node,offset)=>{let y=offset;while(node){y=y*node._trs.array[8]+node._trs.array[1];node=prefab[node._parent?.__id__];}return y;};
|
|
assert.ok(worldY(ended,-ended._contentSize.height/2)>worldY(topRow,topRow._contentSize.height/2),'arrival message stays above the translated reward rows');
|
|
function refs(o){if(!o||typeof o!=='object')return;if('__id__'in o)assert.ok(prefab[o.__id__]);else Object.values(o).forEach(refs);}refs(prefab);
|
|
const atlas=JSON.parse(fs.readFileSync('assets/common/publicFont.plist.meta'));
|
|
assert.equal(prefab.find(n=>n.numberAtlas).numberAtlas.__uuid__,atlas.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,require:popupDependency};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']);
|
|
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/common/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 deposited goods and image-based hours/minutes; close fires once',()=>{
|
|
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('closeButton').handlers.click();f.panel.close();assert.equal(f.closed(),1);
|
|
});
|
|
test('settlement has no claim handler and close carries no deposit flag',()=>{
|
|
const f=fixture(),actions=[];f.show([], (...args)=>actions.push(args));
|
|
assert.equal(typeof f.panel.claim,'undefined');f.panel.close();f.panel.close();assert.deepEqual(actions,[[]]);
|
|
});
|
|
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;}getChildByName(name){return this.children.find(n=>n.name===name)||this;}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);},loadAsset:(bundle,path,type,done)=>bundle.load(path,type,done),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:()=>({canCollect:()=>true,isRunning:()=>true,markShown(){},completePreparation(){},enqueue:(key,priority,run)=>queued.push({key,priority,run})})};
|
|
const context={exports:{},cc,require:name=>popupDependency(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('deposited notices display sequentially without a second claim or reward animation',async()=>{
|
|
const f=homeFixture(),goods=[{id:1001,count:100},{id:2001,count:3}];
|
|
f.setNotices([{noticeId:'p1',goods},{noticeId:'p2',goods}]);f.view.queueSettlement();f.run();await flush();
|
|
assert.deepEqual(f.shown,['p1']);assert.equal(f.done(),0);await f.close();
|
|
assert.deepEqual(f.shown,['p1','p2']);await f.close();assert.equal(f.done(),1);
|
|
assert.equal(f.claims(),0);assert.equal(f.animations.length,0);assert.equal(f.releases(),1);
|
|
});
|
|
test('teardown while showing an arrived notice retains it for the next home',async()=>{
|
|
const f=homeFixture();f.setNotices([{noticeId:'p',goods:[{id:1001,count:100}]}]);f.view.queueSettlement();f.run();await flush();
|
|
const popup=f.view.settlementRoot;f.view.finishSettlement();assert.equal(popup.destroyed,true);assert.equal(f.done(),1);
|
|
await f.close();assert.equal(f.done(),1);assert.equal(f.service.settlements().length,1);
|
|
f.view.queueSettlement();f.run();await flush();assert.deepEqual(f.shown,['p','p']);await f.close();assert.equal(f.done(),2);
|
|
});
|
|
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);
|
|
await f.close();assert.deepEqual(f.shown,['p1','p2']);await 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']);await 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);
|
|
});
|