279 lines
27 KiB
JavaScript
279 lines
27 KiB
JavaScript
const test=require('node:test'),assert=require('node:assert/strict'),fs=require('node:fs'),path=require('node:path'),vm=require('node:vm'),ts=require('typescript');
|
|
const backend=path.resolve(__dirname,'../../server/laf-cloud/functions');
|
|
const clone=v=>v==null?v:structuredClone(v), plain=v=>JSON.parse(JSON.stringify(v));
|
|
function load(file,deps={},globals={}) { const exports={};vm.runInNewContext(ts.transpileModule(fs.readFileSync(file,'utf8'),{compilerOptions:{target:99,module:1,experimentalDecorators:true}}).outputText,{exports,require:n=>deps[n],console,Date,process:{env:{PASSCHECKTIME_ID:'clock'}},...globals});return exports; }
|
|
const model=load('assets/Script/module/Config/BattlePassModel.ts');
|
|
const settlementModel=load('assets/Script/module/Config/BattlePassSettlementModel.ts',{'./BattlePassModel':model});
|
|
const config=JSON.parse(fs.readFileSync('assets/resources/Json/PASS_CHECK.json')).Sheet1;
|
|
test('archived season retains new tiers and excludes claimed rewards with the current table',()=>{
|
|
const stage=model.normalizePass({time:'100',experience:config.slice(0,40).reduce((sum,row)=>sum+row.token,0),tier:30,activate:true,free:Array(39).fill(0),passCheck:Array(39).fill(0)},config);
|
|
const snapshot={stage:plain(stage)};
|
|
const restored=model.normalizePass(snapshot.stage,config);
|
|
assert.equal(restored.unlocked,40);
|
|
assert.equal(restored.free[37],0);
|
|
assert.equal(model.canClaimPass(restored,39,false),true);
|
|
assert.deepEqual(plain(settlementModel.settlementRewards(snapshot,config)),[{type:'coin',count:1200},{type:'infinite_health',count:1800}]);
|
|
assert.deepEqual(plain(settlementModel.settlementClaimFlags(snapshot,config)).free.slice(0,40),Array(40).fill(0));
|
|
});
|
|
function fixture() {
|
|
const now=1800000000000,end=now-1000,tables={users:[{_id:'u',openid:'o',token:'t',coinAmount:10,freezeAmount:1,hammerAmount:2,magicAmount:3,userPowerTime:0,passCheck:JSON.stringify({2:{time:String(end),experience:4,free:[0,1,1],passCheck:[1,1,1],activate:false}})}],order:[],idcount:[{_id:'clock',passcheckTime:end-model.PASS_PERIOD}]};
|
|
let clock=now,drop=false;
|
|
class Clock extends Date {static now(){return clock;}}
|
|
const get=(o,key)=>key.split('.').reduce((v,k)=>v?.[k],o);
|
|
const match=(row,query)=>Object.entries(query).every(([k,v])=> k==='$expr'?clock<new Date(v.$lt[1]).getTime():v===null?get(row,k)==null:JSON.stringify(get(row,k))===JSON.stringify(v));
|
|
const coll=name=>({async findOne(q){return clone(tables[name].find(r=>match(r,q)));},async updateOne(q,u){const r=tables[name].find(r=>match(r,q));if(!r)return{modifiedCount:0};for(const[k,v]of Object.entries(u.$set||{})){const keys=k.split('.');let o=r;for(const key of keys.slice(0,-1))o=o[key]||(o[key]={});o[keys.at(-1)]=clone(v);}if(drop&&u.$set?.timestamp){drop=false;throw Error('response lost after commit');}return{modifiedCount:1};}});
|
|
const db={collection:name=>({where:q=>({getOne:async()=>({data:await coll(name).findOne(q)}),get:async()=>({data:clone(tables[name].filter(r=>match(r,q)))}),update:async values=>({updated:(await coll(name).updateOne(q,{$set:values})).modifiedCount})})})};
|
|
const deps={'@lafjs/cloud':{default:{database:()=>db,mongo:{db:{collection:coll}}}},'@/Utils':{default:{checkToken:(a,b)=>a===b}}};
|
|
const s=load(path.join(backend,'passCheckSettlement.ts'),deps,{Date:Clock});deps['@/passCheckSettlement']=s;
|
|
const upgrade=load(path.join(backend,'passCheckUpgrade.ts'),deps,{Date:Clock});deps['@/passCheckUpgrade']=upgrade;
|
|
const legacy=load(path.join(backend,'passCheck.ts'),deps,{Date:Clock}).default;
|
|
const call=body=>s.default({body:{uid:'u',token:'t',...body}});
|
|
return{tables,s,upgrade,legacy,call,end,now,advance:n=>clock=n,drop:()=>drop=true,user:()=>tables.users[0]};
|
|
}
|
|
test('expiration freezes progress/ownership without awarding offline players',async()=>{const f=fixture();const r=await f.call({action:'read'});assert.equal(r.code,1);assert.equal(r.data.rows.length,1);assert.equal(r.data.rows[0].stage.experience,4);assert.equal(f.user().coinAmount,10);f.user().passCheck=JSON.stringify({2:{time:String(f.end),experience:999}});const again=await f.call({action:'read'});assert.equal(again.data.rows[0].stage.experience,4);});
|
|
test('latest client config computes permitted lanes and merges quantities including duration',()=>{const row={stage:{time:'1',experience:4,free:[0,1,1],passCheck:[1,1,1]}};const reward=plain(settlementModel.settlementRewards(row,config));assert.deepEqual(reward,[{type:'freeze',count:1},{type:'hammer',count:1}]);assert.deepEqual(plain(settlementModel.settlementRewards(row,config,true)),[{type:'coin',count:300},{type:'freeze',count:4},{type:'magic',count:2}]);const timed=[{token:0,free:'3001:2',paid:'3003:1'}];assert.deepEqual(plain(settlementModel.settlementRewards({stage:{time:'1',experience:0,activate:true}},timed)),[{type:'infinite_health',count:5400}]);});
|
|
test('18 tier cannot receive level 36+, 30 tier can',()=>{const stage={time:'1',experience:100000,activate:true,tier:18};const basic=settlementModel.settlementRewards({stage},config),limited=settlementModel.settlementRewards({stage},config.slice(0,35));assert.deepEqual(plain(basic),plain(limited));assert.notDeepEqual(plain(basic),plain(settlementModel.settlementRewards({stage:{...stage,tier:30}},config)));});
|
|
test('parallel close and claim commits resources exactly once',async()=>{const f=fixture();const body={action:'claim',end:f.end,rewards:[{type:'coin',count:15},{type:'infinite_health',count:900}]};const r=await Promise.all([f.call(body),f.call(body)]);assert.ok(r.every(x=>x.code===1));assert.equal(f.user().coinAmount,25);assert.equal(f.user().userPowerTime,Math.floor(f.now/1000)+900);assert.equal(r.filter(x=>x.data.alreadyGranted).length,1);});
|
|
test('lost response after commit is recoverable without repeating the delta',async()=>{const f=fixture();f.drop();const body={action:'claim',end:f.end,rewards:[{type:'hammer',count:2}]};assert.equal((await f.call(body)).code,500);assert.equal(f.user().hammerAmount,4);const r=await f.call(body);assert.equal(r.code,1);assert.equal(r.data.alreadyGranted,true);assert.equal(r.data.resources.hammerAmount,4);assert.equal(f.user().hammerAmount,4);});
|
|
test('invalid rewards and tokens never mutate balances or receipt',async()=>{const f=fixture();assert.equal((await f.call({action:'claim',end:f.end,token:'bad',rewards:[]})).code,0);for(const rewards of [[{type:'coin',count:-1}],[{type:'foo',count:1}],[{type:'coin',count:1},{type:'coin',count:1}]])assert.equal((await f.call({action:'claim',end:f.end,rewards})).code,400);assert.equal(f.user().coinAmount,10);assert.equal(f.user().passSettlements[f.end].claimed,false);});
|
|
test('empty notice is once per period; render acknowledgment does not add resources',async()=>{const f=fixture();await f.call({action:'empty_seen',end:f.end});const r=await f.call({action:'read'});assert.equal(r.data.rows[0].emptySeen,true);assert.equal(f.user().coinAmount,10);assert.equal(settlementModel.settlementSteps(r.data.rows,f.end,f.now).length,1,'unbought offer remains');});
|
|
test('offer acknowledgment is idempotent only for original view; new session cannot reopen',async()=>{const f=fixture(),offerToken='a_repeatable_view_token';for(let i=0;i<2;i++)assert.equal((await f.call({action:'offer_shown',end:f.end,offerToken})).code,1);assert.equal((await f.call({action:'offer_shown',end:f.end,offerToken:'a_different_view_token'})).code,410);const data=(await f.call({action:'read'})).data;assert.equal(settlementModel.settlementSteps(data.rows,f.end,f.now).length,1,'unclaimed reward survives');assert.equal(data.rows[0].offerToken,undefined);f.user().token='new';assert.equal((await f.call({action:'offer_shown',end:f.end,offerToken,token:'new'})).code,410);});
|
|
test('decline is permanent and blocks order creation; empty close does not decline',async()=>{const f=fixture(),offerToken='a_repeatable_view_token';await f.call({action:'offer_shown',end:f.end,offerToken});assert.equal(f.upgrade.passOrderFields(f.user(),{itemid:'battlepass',passVersion:2,passEnd:f.end,passOfferToken:offerToken}).passPostSeason,true);await f.call({action:'decline',end:f.end});assert.throws(()=>f.upgrade.passOrderFields(f.user(),{itemid:'battlepass',passVersion:2,passEnd:f.end,passOfferToken:offerToken}));});
|
|
test('paid post-season order survives process loss and grants once; duplicate payment requires review',async()=>{const f=fixture();await f.call({action:'read'});f.tables.order.push({outTradeNo:'paid',openid:'o',itemid:'battlepass',passPostSeason:true,passEnd:String(f.end),state:1});const body={action:'purchase_claim',end:f.end,outTradeNo:'paid',rewards:[{type:'coin',count:300}]};assert.equal((await f.call(body)).code,1);assert.equal((await f.call(body)).data.alreadyGranted,true);assert.equal(f.user().coinAmount,310);f.tables.order.push({...f.tables.order[0],outTradeNo:'second',passGranted:false,state:1});assert.equal((await f.call({...body,outTradeNo:'second'})).code,410);assert.equal(f.tables.order[1].passReview,'duplicate_post_season_payment');assert.equal(f.user().coinAmount,310);});
|
|
test('offline multiple periods are ordered oldest first, only latest has a purchase opportunity',async()=>{const f=fixture();f.user().passCheckUpgrade=JSON.stringify({[f.end-model.PASS_PERIOD]:{time:String(f.end-model.PASS_PERIOD),experience:8}});const r=await f.call({action:'read'});assert.deepEqual(plain(r.data.rows.map(r=>r.end)),[f.end-model.PASS_PERIOD,f.end]);assert.equal((await f.call({action:'offer_shown',end:f.end-model.PASS_PERIOD,offerToken:'an_older_period_token'})).code,410);});
|
|
test('clock preserves the millisecond origin and advances exact 30-day periods at cutoff',async()=>{const f=fixture();assert.equal(await f.s.currentPassStart(),f.end);f.advance(f.end+model.PASS_PERIOD);assert.equal(await f.s.currentPassStart(),f.end+model.PASS_PERIOD);});
|
|
test('ordinary saves at and after cutoff are rejected, leaving the frozen progress unchanged',async()=>{const f=fixture();const r=await f.upgrade.default({body:{uid:'u',token:'t',action:'save',revision:0,passCheck:JSON.stringify({2:{time:String(f.end),experience:999}}),legacy:'{}'}});assert.equal(r.code,410);assert.equal(f.user().passSettlements[f.end].stage.experience,4);const old=await f.legacy({body:{uid:'u',token:'t',action:'save',passCheck:JSON.stringify({2:{time:String(f.end),experience:999}})}});assert.equal(old.code,410);});
|
|
|
|
function uiFixture() {
|
|
const calls=[],scheduled=[],cc={_decorator:{ccclass:T=>T},Component:class{scheduleOnce(fn){scheduled.push(fn);}},log:msg=>calls.push({log:msg}),isValid:n=>!!n&&!n.destroyed,fx:{GameConfig:{PASS_CHECK:config}}};
|
|
let reject=false,animation,requestError;
|
|
const utils={settlementRequest:async(action,data)=>{calls.push({action,data});if(requestError)throw requestError;return{data:{row:{...view.row,emptySeen:true,offer:action==='offer_shown'?'shown':view.row.offer}}};},commitPassSettlement:async()=>{calls.push({action:'claim'});if(reject)throw{msg:'network'};return{data:{row:{...view.row,claimed:true,emptySeen:true},rewards:[{type:'coin',count:100}]}};}};
|
|
const C=load('assets/Script/BattlePassSettlement.ts',{'./module/Pay/Utils':{default:utils},'./module/Config/BattlePassSettlementModel':settlementModel,'./Sdk/MiniGameSdk':{MiniGameSdk:{API:{showToast:msg=>calls.push({error:msg})}}}},{cc}).default;
|
|
const view=new C();view.root={};view.panel={active:true};view.node={getComponent:()=>({updateCoin(){},updatePower(){},openRewardWindow:(r,_unused,done)=>{animation=done;calls.push({animation:r});}})};
|
|
view.show=(mode,rewards)=>{view.mode=mode;calls.push({show:mode,rewards});};view.offerOrNext=()=>calls.push({afterAnimation:true});
|
|
view.row={end:Date.now()-1000,stage:{time:String(Date.now()-1000),experience:0,free:[1],passCheck:[1]},offer:'unused'};
|
|
return {view,calls,scheduled,fail:()=>reject=true,failRequest:error=>requestError=error,complete:()=>animation()};
|
|
}
|
|
test('figure 3 close uses claim, keeps the queue while its reward animation is open',async()=>{const f=uiFixture();f.view.mode='claim';f.view.close();await new Promise(setImmediate);assert.equal(f.calls.filter(c=>c.action==='claim').length,1);assert.equal(f.view.busy,true);assert.equal(f.view.panel.active,false);assert.equal(f.calls.some(c=>c.afterAnimation),false);f.complete();assert.equal(f.view.busy,false);assert.equal(f.calls.some(c=>c.afterAnimation),true);});
|
|
test('claim failure retains the popup and permits retry without displaying an animation',async()=>{const f=uiFixture();f.fail();await f.view.claim();assert.equal(f.view.busy,false);assert.equal(f.view.panel.active,true);assert.equal(f.calls.some(c=>c.animation),false);assert.equal(f.calls.some(c=>c.afterAnimation),false);});
|
|
test('empty notice acknowledgment is scheduled after rendering and close proceeds to the offer',async()=>{const f=uiFixture();f.view.rows=[{...f.view.row,stage:{...f.view.row.stage,free:[0]}}];f.view.next();assert.equal(f.calls[0].show,'empty');assert.equal(f.calls.some(c=>c.action==='empty_seen'),false);await f.scheduled[0]();assert.equal(f.calls[1].action,'empty_seen');f.view.close();assert.equal(f.calls.some(c=>c.afterAnimation),true);});
|
|
test('ordinary dismissal of the confirmation only returns to the underlying offer',()=>{const f=uiFixture(),confirmation={active:true};f.view.mode='offer';f.view.confirm=confirmation;f.view.close();assert.equal(confirmation.active,false);assert.equal(f.view.confirm,null);assert.equal(f.calls.length,0);});
|
|
|
|
test('ineligible post-season offer is skipped silently without blocking the queue',async()=>{
|
|
for(const msg of ['不符合上期购买条件','该期购买机会已使用']) {
|
|
const f=uiFixture();let advanced=0;f.view.next=()=>advanced++;f.view.busy=true;
|
|
f.failRequest({code:410,msg});await f.view.markOffer();
|
|
assert.equal(f.view.busy,false);assert.equal(advanced,1);
|
|
assert.equal(f.calls.some(c=>c.error),false);
|
|
}
|
|
});
|
|
test('eligible offer is shown after acknowledgment and network errors retry while hidden',async()=>{
|
|
const f=uiFixture();let advanced=0;f.view.next=()=>advanced++;
|
|
await f.view.markOffer();assert.equal(f.view.row.offer,'shown');assert.equal(advanced,0);
|
|
f.view.busy=true;f.failRequest({code:500,msg:'网络异常'});await f.view.markOffer();
|
|
assert.equal(f.view.busy,true);assert.equal(advanced,0);assert.equal(f.scheduled.length,1);
|
|
assert.equal(f.calls.filter(c=>c.log).length,1);
|
|
});
|
|
test('returning home repeatedly never flashes an offer rejected by the server',async()=>{
|
|
for(let home=0;home<3;home++) {
|
|
const f=uiFixture();f.view.latest=f.view.row.end;f.view.serverTime=Date.now();
|
|
f.view.root.active=true;let advanced=0;f.view.next=()=>advanced++;
|
|
f.failRequest({code:410,msg:'不符合上期购买条件'});
|
|
Object.getPrototypeOf(f.view).offerOrNext.call(f.view);
|
|
assert.equal(f.view.root.active,false);assert.equal(f.calls.some(c=>c.show),false);
|
|
await new Promise(setImmediate);
|
|
assert.equal(advanced,1);assert.equal(f.calls.some(c=>c.show||c.error),false);
|
|
}
|
|
});
|
|
test('eligible offer waits for server response; leaving the scene discards the response',async()=>{
|
|
for(const leave of [false,true]) {
|
|
const f=uiFixture();f.view.latest=f.view.row.end;f.view.serverTime=Date.now();
|
|
Object.getPrototypeOf(f.view).offerOrNext.call(f.view);
|
|
assert.equal(f.view.root.active,false);assert.equal(f.calls.some(c=>c.show),false);
|
|
if(leave)f.view.node.destroyed=true;
|
|
await new Promise(setImmediate);
|
|
assert.equal(f.calls.filter(c=>c.show==='offer').length,leave?0:1);
|
|
}
|
|
});
|
|
test('locally ineligible offers make no API request and display no popup or toast',()=>{
|
|
for(const patch of [{stage:{activate:true}},{offer:'shown'},{purchase:'paid'},{end:1}]) {
|
|
const f=uiFixture();f.view.latest=f.view.row.end;f.view.serverTime=Date.now();
|
|
f.view.row={...f.view.row,...patch};let advanced=0;f.view.next=()=>advanced++;
|
|
Object.getPrototypeOf(f.view).offerOrNext.call(f.view);
|
|
assert.equal(advanced,1);assert.equal(f.calls.length,0);assert.equal(f.scheduled.length,0);
|
|
}
|
|
});
|
|
|
|
test('claim flags keep unpaid lanes available for late entitlement recovery',()=>{
|
|
const snapshot={stage:{time:'1',experience:4,free:[0,1,1],passCheck:[1,1,1]}};
|
|
const flags=plain(settlementModel.settlementClaimFlags(snapshot,config));
|
|
assert.deepEqual(flags.free.slice(0,3),[0,0,0]);
|
|
assert.deepEqual(flags.passCheck.slice(0,3),[1,1,1]);
|
|
assert.deepEqual(plain(settlementModel.settlementClaimFlags(snapshot,config,true)).passCheck.slice(0,3),[0,0,0]);
|
|
});
|
|
test('settlement reserves the queue through completion, then restores original priority order',()=>{
|
|
const cc={_decorator:{ccclass:T=>T},Component:class{},error:()=>{}};
|
|
const Queue=load('assets/home_popup_queue/HomePopupQueue.ts',{}, {cc}).default;
|
|
const queue=new Queue(),events=[];let finish;
|
|
queue.hasVisiblePopup=()=>false;
|
|
queue.enqueue('intro',60,done=>{events.push('intro');done();});
|
|
queue.enqueue('daily',10,done=>{events.push('daily');done();});
|
|
queue.enqueue('battlePassSettlement',-100,done=>{events.push('settlement');finish=done;});
|
|
queue.update();queue.update();assert.deepEqual(events,['settlement']);assert.equal(queue.has('battlePassSettlement'),true);
|
|
finish();queue.update();queue.update();assert.deepEqual(events,['settlement','daily','intro']);
|
|
assert.equal(queue.has('battlePassSettlement'),false);
|
|
});
|
|
|
|
function prefabTree(file) {
|
|
const records=JSON.parse(fs.readFileSync(file));
|
|
function node(id) {
|
|
const source=records[id],events={};
|
|
return {name:source._name,active:source._active,width:source._contentSize.width,height:source._contentSize.height,
|
|
x:source._trs.array[0],y:source._trs.array[1],scaleX:source._trs.array[7],scaleY:source._trs.array[8],
|
|
children:source._children.map(c=>node(c.__id__)),source,records,
|
|
getChildByName(name){return this.children.find(c=>c.name===name);},on(name,cb){events[name]=cb;},
|
|
emit(name){events[name]?.();},destroy(){this.destroyed=true;}};
|
|
}
|
|
return node(records[0].data.__id__);
|
|
}
|
|
|
|
function homeSettlementFixture() {
|
|
const source=ts.createSourceFile('JiaZai.ts',fs.readFileSync('assets/Script/JiaZai.ts','utf8'),99,true);
|
|
const members=source.statements.find(ts.isClassDeclaration).members;
|
|
const method=name=>members.find(n=>n.name?.getText(source)===name);
|
|
const listeners=[],runs=[],passReads=[],cc={_decorator:{ccclass:T=>T},Component:class{},error:assert.fail,
|
|
isValid:v=>!!v&&!v.destroyed,fx:{GameConfig:{isGameplayEntryPreparing:()=>false,
|
|
GM_INFO:{level:30,canIos:true},getFeatureUnlockLevel:()=>21}},
|
|
game:{EVENT_SHOW:'show',on(event,fn,owner){listeners.push({event,fn,owner});},
|
|
off(event,fn,owner){const i=listeners.findIndex(l=>l.event===event&&l.fn===fn&&l.owner===owner);if(i>=0)listeners.splice(i,1);}}};
|
|
const Queue=load('assets/home_popup_queue/HomePopupQueue.ts',{}, {cc}).default;
|
|
class Settlement { run(done){runs.push({view:this,done});} }
|
|
// Exercise the actual lifecycle statements, without unrelated homepage UI setup.
|
|
const lifecycle=name=>name+'(){'+method(name).body.statements.filter(s=>s.getText(source).includes('this.checkBattlePassSettlement')).map(s=>s.getText(source)).join('\n')+'}';
|
|
const exports={};vm.runInNewContext(ts.transpileModule('export class Home {'+method('checkBattlePassSettlement').getText(source)+method('checkAndSetPlayerPassLevel').getText(source)+lifecycle('start')+lifecycle('onDestroy')+'}',{compilerOptions:{target:99,module:1}}).outputText,
|
|
{exports,cc,HomePopupQueue:Queue,BattlePassSettlement:Settlement,canClaimPass:model.canClaimPass,
|
|
Utils:{getPassCheckInfo:callback=>passReads.push(callback)}});
|
|
const create=()=>{
|
|
const home=new exports.Home(),components=[];
|
|
home.isHomeRuntimeAlive=()=>!home.destroyed;
|
|
const red={active:true},button={active:false,getChildByName:name=>name==='red'?red:null};
|
|
const top={getChildByName:name=>name==='passBtn'?button:null},load={getChildByName:name=>name==='Top'?top:null};
|
|
home.node={activeInHierarchy:true,getChildByName:name=>name==='Load'?load:null,getComponent:T=>components.find(c=>c instanceof T&&!c.destroyed),addComponent:T=>{const c=new T();c.node=home.node;components.push(c);return c;}};
|
|
home.start();const queue=Queue.get(home.node);queue.hasVisiblePopup=()=>false;
|
|
return {home,queue,button,red};
|
|
};
|
|
return {create,runs,listeners,passReads,cc,source,method,show:()=>listeners.slice().forEach(l=>l.fn.call(l.owner))};
|
|
}
|
|
|
|
test('finishing old-season settlement refreshes the hidden home entry and current-season progress without reentering',()=>{
|
|
const f=homeSettlementFixture(),{queue,button,red}=f.create();
|
|
f.cc.fx.GameConfig.GM_INFO.getProgressLevel=35;
|
|
queue.update();assert.equal(button.active,false);assert.equal(f.passReads.length,0);
|
|
f.runs[0].done();assert.equal(f.passReads.length,1);
|
|
const start=Date.now()-1000;
|
|
f.passReads[0]({code:1,data:{time:start,passCheck:JSON.stringify({
|
|
1:{time:'0',free:[],passCheck:[]},
|
|
2:{time:String(start+model.PASS_PERIOD),progressLevel:1,progress:1,activate:false,free:[],passCheck:[]}
|
|
})}});
|
|
assert.equal(button.active,true);assert.equal(red.active,false);
|
|
assert.equal(f.cc.fx.GameConfig.GM_INFO.getProgressLevel,1);
|
|
assert.equal(f.cc.fx.GameConfig.GM_INFO.getItemType,2);
|
|
assert.equal(queue.has('battlePassSettlement'),false);
|
|
});
|
|
|
|
test('settlement completion releases the queue without refreshing a hidden or departing homepage',()=>{
|
|
for(const state of ['destroyed','inactive','enteringGame']) {
|
|
const f=homeSettlementFixture(),{home,queue}=f.create();queue.update();
|
|
if(state==='destroyed')home.destroyed=true;
|
|
if(state==='inactive')home.node.activeInHierarchy=false;
|
|
if(state==='enteringGame')f.cc.fx.GameConfig.isGameplayEntryPreparing=()=>true;
|
|
f.runs[0].done();assert.equal(f.passReads.length,0);
|
|
assert.equal(queue.has('battlePassSettlement'),false);
|
|
}
|
|
});
|
|
test('battle-pass checks use the same start and foreground hooks as gold miner, not level refresh or a new onShow',()=>{
|
|
const f=homeSettlementFixture();
|
|
assert.match(f.method('start').getText(f.source),/this\.checkBattlePassSettlement\(\)/);
|
|
assert.match(f.method('start').getText(f.source),/cc\.game\.on\(cc\.game\.EVENT_SHOW, this\.checkBattlePassSettlement, this\)/);
|
|
assert.equal(f.method('onShow'),undefined);
|
|
for(const name of ['onLoad','checkAndSetPlayerPassLevel'])assert.doesNotMatch(f.method(name).getText(f.source),/battlePassSettlement|checkBattlePassSettlement/);
|
|
for(let visit=0;visit<3;visit++) {
|
|
const {home,queue}=f.create();queue.update();assert.equal(f.runs.length,visit+1);
|
|
f.runs.at(-1).done();home.onDestroy();home.destroyed=true;
|
|
assert.equal(f.listeners.length,0);
|
|
}
|
|
f.show();assert.equal(f.runs.length,3);
|
|
});
|
|
test('foreground checks deduplicate queued/running settlement and recheck after completion',()=>{
|
|
const f=homeSettlementFixture(),{queue}=f.create();
|
|
f.show();f.show();queue.update();assert.equal(f.runs.length,1);
|
|
f.show();queue.update();assert.equal(f.runs.length,1);
|
|
// Cocos destruction is deferred; a completed component must not be reused.
|
|
f.runs[0].view.destroyed=true;f.runs[0].done();
|
|
f.show();queue.update();assert.equal(f.runs.length,2);
|
|
assert.notEqual(f.runs[0].view,f.runs[1].view);
|
|
});
|
|
test('home settlement check skips inactive/leaving scenes, including queued tasks',()=>{
|
|
const f=homeSettlementFixture(),{home,queue}=f.create();
|
|
home.node.activeInHierarchy=false;queue.update();assert.equal(f.runs.length,0);
|
|
f.show();assert.equal(queue.has('battlePassSettlement'),false);
|
|
home.node.activeInHierarchy=true;f.cc.fx.GameConfig.isGameplayEntryPreparing=()=>true;
|
|
f.show();assert.equal(queue.has('battlePassSettlement'),false);
|
|
f.cc.fx.GameConfig.isGameplayEntryPreparing=()=>false;
|
|
f.show();home.destroyed=true;queue.update();assert.equal(f.runs.length,0);
|
|
});
|
|
const find=(path,root)=>path.split('/').reduce((n,p)=>n.getChildByName(p),root);
|
|
test('figures 1/2/3 match figure 5 title size and the offer uses the glossy green button',()=>{
|
|
const panel=prefabTree('assets/passCheck/prefab/PassSettlement.prefab').getChildByName('panel');
|
|
const prefab=JSON.parse(fs.readFileSync('assets/shop/prefab/buyActivate.prefab'));
|
|
const title=prefab.find(n=>n.__type__==='cc.Node'&&n._name==='tongpiaozi');
|
|
assert.equal(prefab[title._components[0].__id__]._sizeMode,0,'custom size must not reset to texture dimensions');
|
|
for(const mode of ['offer','empty','claim']){
|
|
const drawn=find(mode+'/title',panel);
|
|
assert.equal(drawn.width,title._contentSize.width);assert.equal(drawn.height,title._contentSize.height);
|
|
}
|
|
const atlas=JSON.parse(fs.readFileSync('assets/shop/img2/shopUI.plist.meta')).subMetas;
|
|
const button=find('offer/buyButton',panel);
|
|
const sprite=button.records[button.source._components[0].__id__];
|
|
assert.equal(sprite._spriteFrame.__uuid__,atlas['giftBtnBg.png'].uuid);
|
|
});
|
|
test('prefab reward slots and confirmation are reused without overwriting editor positions',()=>{
|
|
const cc={_decorator:{ccclass:T=>T},Component:class{},find};
|
|
const digits={numberToImageNodesNumber:(value,w,x,p,n)=>n.value=value,durationToImageNodes:(value,n)=>n.value=value};
|
|
const C=load('assets/Script/BattlePassSettlement.ts',{'./NumberToImage':{default:digits}},{cc}).default;
|
|
const view=new C();view.root=prefabTree('assets/passCheck/prefab/PassSettlement.prefab');view.root.active=false;view.panel=view.root.getChildByName('panel');
|
|
const title=find('offer/title',view.panel),slot=find('offer/rewards/topRow/slot1',view.panel),amount=find('item/amount',slot);
|
|
title.x=123;slot.y=-89;amount.x=12;
|
|
view.show('offer',[{type:'hammer',count:7}]);
|
|
assert.equal(view.root.active,true);
|
|
assert.equal(title.x,123);assert.equal(slot.y,-89);assert.equal(amount.x,12);assert.equal(amount.value,7);
|
|
assert.equal(find('offer/rewards/topRow/slot2',view.panel).active,false);
|
|
assert.deepEqual(find('item/icon',slot).children.filter(n=>n.active).map(n=>n.name),['hammer']);
|
|
const confirmation=view.panel.getChildByName('confirmation');
|
|
view.close();assert.equal(confirmation.active,true);view.close();assert.equal(confirmation.active,false);
|
|
view.close();assert.equal(view.confirm,confirmation,'reopening must reuse the authored node');
|
|
view.show('claim',[{type:'coin',count:1}]);assert.equal(confirmation.active,false);
|
|
assert.equal(view.panel.getChildByName('offer').active,false);assert.equal(view.panel.getChildByName('claim').active,true);
|
|
});
|
|
test('unlock uses the prefab tier and preserves edited artwork transforms',async()=>{
|
|
for(const tier of [18,30]) {
|
|
const root=prefabTree('assets/passCheck/prefab/PassUnlock.prefab'),card=find('content/'+(tier===18?'vip':'svip')+'/cardAnimation/card',root);
|
|
card.x=95;card.scaleX=1.25;
|
|
const cc={isValid:()=>true,instantiate:()=>root,Node:{EventType:{TOUCH_END:'touchend'}},tween:()=>({to(){return this;},delay(){return this;},call(fn){fn();return this;},start(){}})};
|
|
const Art=load('assets/Script/BattlePassArt.ts',{}, {cc}).default;
|
|
Art.load=async()=>{};let closed=0;await Art.unlock({width:1080,height:1920},tier,()=>closed++);
|
|
assert.equal(find('content/vip',root).active,tier===18);assert.equal(find('content/svip',root).active,tier===30);
|
|
assert.equal(card.x,95);assert.equal(card.scaleX,1.25);
|
|
root.getChildByName('shade').emit('touchend');assert.equal(closed,1);assert.equal(root.destroyed,true);
|
|
}
|
|
});
|
|
test('popup prefabs have valid object references and no fixed-copy Labels or runtime node construction',()=>{
|
|
for(const name of ['PassSettlement','PassUnlock']) {
|
|
const records=JSON.parse(fs.readFileSync('assets/passCheck/prefab/'+name+'.prefab'));
|
|
const visit=v=>{if(!v||typeof v!=='object')return;if('__id__'in v)assert.ok(records[v.__id__]);Object.values(v).forEach(visit);};
|
|
records.forEach(visit);assert.equal(records.some(r=>r.__type__==='cc.Label'),false);
|
|
}
|
|
assert.doesNotMatch(fs.readFileSync('assets/Script/BattlePassSettlement.ts','utf8'),/new cc\.Node|addComponent\(cc\.(Graphics|Sprite|Button)/);
|
|
assert.doesNotMatch(fs.readFileSync('assets/Script/BattlePassArt.ts','utf8').split('static async unlock')[1],/new cc\.Node|this\.image\(/);
|
|
});
|