const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const vm = require('node:vm'); const ts = require('typescript'); function fixture() { let now = 10000, data = {available:true, period:{enabled:true,startsAt:9000,endsAt:11000}, run:null}; const calls = {sync:0,play:0,close:0,start:0,prefab:0}; let listener; const service = {data:()=>data, run:()=>bridge.currentRun(data), stage:()=>{const run=service.run();return run&&run.stages.at(-1);}, now:()=>now, error:'', pending:()=>false, elimination:()=>null, hasPendingLoss:()=>false, unseenResult:()=>false, resultKey:()=>'', acknowledgeResult(){}, dismissExpiryPresentation(){ const item=service.elimination(); if(item?.after?.status==='expired')service.acknowledgeElimination(item.key); if(service.run()?.status==='expired')service.acknowledgeResult(); }, subscribe(fn){listener=fn;return ()=>{};}, sync:async()=>{calls.sync++;return true;}, prepareMatch:async()=>{calls.start++;return false;}}; class Component {schedule(){} unschedule(){} scheduleOnce(fn,delay){(this.scheduled||(this.scheduled=[])).push({fn,delay});}} const cc = {_decorator:{ccclass:c=>c,property:()=>()=>{}}, Component, Node:class{}, Label:class{}, Sprite:class{}, SpriteFrame:class{}, String, isValid:()=>true, warn(){}, view:{getVisibleSize:()=>({width:720,height:1280})}, fx:{GameConfig:{GM_INFO:{userId:1001,coin:0}}}}; function load(file, imports) { imports = {'./module/Config/ActivityScheduleService': {default:{active:()=>null,hasSchedule:()=>false,now:()=>now}}, ...imports}; const exports={}; vm.runInNewContext(ts.transpileModule(fs.readFileSync(file,'utf8'),{compilerOptions:{module:ts.ModuleKind.CommonJS, target:ts.ScriptTarget.ES2017,experimentalDecorators:true}}).outputText, {exports,cc,Date:{now:()=>now},console,setTimeout,clearTimeout,require:n=>{assert(n in imports,n);return imports[n];}}); return exports.default; } const bridge=load('assets/Script/module/Config/CloudRiseService.ts',{'../Pay/Utils':{default:{}}}); bridge.install(service); const Panel=load('assets/cloud_rise_home/scripts/CloudRisePanel.ts',{'./CloudRiseRuntime':{default:service}, '../../Script/module/Config/CloudRiseService':{default:bridge}, './CloudRiseNumberToImage':{default:class{}}, './CloudRisePage':{default:class{}}, './CloudRiseCounter':{default:class{}},'../../Script/Sdk/MiniGameSdk':{MiniGameSdk:{API:{showToast(){}}}}}); const node=()=>({active:true,setContentSize(){},getChildByName(){return null;},width:720,height:1280,events:{},on(type,fn,target,capture){this.events[type]={fn,target,capture};if(type==='click')this.click=fn;},off(){}}); function panel(mode='overview', beforeInitialize) { const p=new Panel();p.openPopup=()=>{};p.closePopup=close=>close();p.node=node();p.background=node();p.design=node(); p.pages=Array.from({length:5},node);p.pages[3]=null;p.buttons=Array.from({length:6},node); for(const key of ['overviewDescription','progressArt','progressCount','survivorCount','progressPool', 'playAction','resultArt','resultTitle','reward','resultDescription','totalReward','resultAction','errorLabel']) p[key]={string:'',node:node()}; p.failureMessage=node();p.expiredMessage=node();p.modalBackground=node();p.progressBackground=node();p.progressRoster=node();p.progressPool.setValue=()=>{};p.renderProgressRoster=()=>{};p.prepareProgressRoster=()=>{};p.continueHint=node();p.rewardGroup=node();p.overviewCopy=['open','waiting','completed','expired','failed','closed'];p.clocks=[{string:''}]; p.loadedStage=service.stage()?.stage||1;p.matchingPage=node();p.stagePage=node(); if(beforeInitialize)beforeInitialize(p); p.initialize(mode,false,()=>calls.close++,()=>calls.play++);return p; } function run(status='playing') { data.available=false; data.run={id:'r',status,stage:1,expiresAt:12000,stages:[{stage:1,status:status==='waiting'?'won':'playing', rewardSaved:true,reward:100,success_num:0,target:5,survivors:100,pool:10000,opponents:[]}]}; } return {bridge,service,calls,panel,run,cc,setNow:n=>{now=n;},setData:d=>{data=d;},get data(){return data;},emit:()=>listener(),load}; } test('overview uses only the authored Start artwork without caption overlays',()=>{ const a=JSON.parse(fs.readFileSync('assets/cloud_rise_home/prefab/CloudRise.prefab')); const binding=a.find(o=>o.pages&&o.buttons),button=a[binding.buttons[1].__id__]; assert.equal(binding.overviewAction,undefined); assert(!button._children.some(r=>['Caption','CaptionCover'].includes(a[r.__id__]._name))); const f=fixture(),p=f.panel();assert(p.buttons[1].active); for(const status of ['playing','waiting']) {f.run(status);p.render();assert(p.buttons[1].active);} assert.equal(f.calls.start,0); }); function continuationFixture() { const f=fixture(),popups=[],notices=[];let match=async()=>true; const View=f.load('assets/Script/CloudRiseView.ts',{ './module/Config/CloudRiseService':{default:f.bridge}, './Sdk/MiniGameSdk':{MiniGameSdk:{API:{showToast:s=>notices.push(s)}}}, '../home_popup_queue/HomePopupQueue':{default:{get:()=>({enqueue:(key,priority,show)=>popups.push({key,priority,show})})}}}); const view=new View();view.node={};view.entry={}; const host={getChildByName:()=>({getComponent:()=>view}),addChild(){},getComponent:()=>null};view.host=host; f.cc.macro={MAX_ZINDEX:10000};f.cc.instantiate=()=>({destroy(){},getComponent:()=>({preparePage:async()=>{},initialize(){},match:()=>match()})}); f.bridge.pagePrefab=async()=>({});f.bridge.pageFragment=async()=>({}); return {...f,View,view,host,popups,notices,setMatch:fn=>match=fn}; } test('Next never automatically matches another stage, including waiting stages 1 and 2',async()=>{ for(const stage of [1,2]) { const f=continuationFixture();f.run('waiting');f.data.run.stage=stage; let starts=0;f.setMatch(async()=>{starts++;f.data.run.stage++;f.data.run.status='playing';return true;}); assert.equal(await f.View.continueFromNextLevel(f.host),true); assert.equal(starts,0);assert.equal(f.data.run.stage,stage); assert.equal(f.view.overlay,null);assert.equal(f.view.busy,false); } for(const status of ['playing','completed','expired']) { const f=continuationFixture();f.run(status);let starts=0;f.setMatch(async()=>{starts++;return true;}); assert.equal(await f.View.continueFromNextLevel(f.host),true);assert.equal(starts,0); } }); test('failed result sync remains retryable without ever starting a stage',async()=>{ const f=continuationFixture();f.run('waiting');let starts=0; f.setMatch(async()=>{starts++;return true;}); f.service.sync=async()=>false; assert.equal(await f.View.continueFromNextLevel(f.host),true); f.service.sync=async()=>{throw Error('offline');}; assert.equal(await f.View.continueFromNextLevel(f.host),true); f.service.sync=async()=>true; assert.equal(await f.View.continueFromNextLevel(f.host),true); assert.equal(starts,0);assert.equal(f.data.run.status,'waiting'); }); test('Next presents delayed stage 1/2 rewards without auto-matching when the motion queue is empty',async()=>{ for(const stage of [1,2]) { const f=continuationFixture();f.run('playing');f.data.run.stage=stage;f.data.run.stages[0].stage=stage; let unseen=false,close,matched=0,syncs=0;const order=[]; f.service.unseenResult=()=>unseen; f.service.sync=async()=>{ if(++syncs===1) {f.data.run.status='waiting';f.data.run.stages[0].status='won';unseen=true;} return true; }; f.cc.instantiate=()=>({destroy(){},getComponent:()=>({preparePage:async()=>{}, initialize:(mode,inGame,onClose)=>{order.push(mode);if(mode==='result')close=onClose;}, acknowledge:()=>{unseen=false;order.push('ack');}, match:async()=>{matched++;f.data.run.stage++;f.data.run.status='playing';return true;}})}); const next=f.View.continueFromNextLevel(f.host); for(let i=0;i<30&&!close;i++)await Promise.resolve(); assert.equal(typeof close,'function');assert.equal(matched,0);assert.deepEqual(order,['result']); close();assert.equal(await next,true);assert.equal(matched,0);assert.deepEqual(order,['result','ack']); } }); test('an unseen reward whose prefab fails to load blocks stage advancement and remains retryable',async()=>{ const f=continuationFixture();f.run('waiting');f.service.unseenResult=()=>true; f.bridge.pagePrefab=async()=>{throw Error('offline');};let matched=0;f.setMatch(async()=>{matched++;return true;}); assert.equal(await f.View.continueFromNextLevel(f.host),false);assert.equal(matched,0); assert.equal(f.service.unseenResult(),true);assert.equal(f.view.overlay,null); }); test('returning to a fresh home never queues an already settled waiting-stage overview',async()=>{ for(const stage of [1,2,3]) { const f=continuationFixture();f.run(stage===3?'completed':'waiting');f.data.run.stage=stage; f.view.refresh();f.view.refresh();assert.equal(f.popups.length,0);assert.equal(f.calls.start,0); await f.view.open('overview');assert.equal(!!f.view.overlay,stage!==3,'completed entry stays closed'); } }); test('home timeout closes the entry and consumes presentation without loading a stage, including return from game',async()=>{ for(const number of [1,2,3]) { const f=continuationFixture();f.run('expired');f.data.run.stage=number;f.data.run.stages[0].stage=number; let item={key:'expiry',after:{stage:number,status:'expired'}},seen=false; f.service.elimination=()=>item;f.service.acknowledgeElimination=()=>item=null; f.service.unseenResult=()=>!seen;f.service.acknowledgeResult=()=>seen=true; f.bridge.allowsAutomaticResults=()=>true; f.bridge.pagePrefab=f.bridge.pageFragment=()=>assert.fail('home timeout must not load animation resources'); f.view.map={};f.view.refresh();assert.equal(item,null);assert.equal(seen,true,'ordinary gameplay quietly consumes activity expiry'); f.view.map=null;f.view.refresh();f.view.refresh(); assert.equal(f.view.entry.active,false);assert.equal(item,null);assert(seen);assert.equal(f.popups.length,0); await f.view.showElimination();await f.view.open('result'); assert.equal(f.view.overlay,null);assert.equal(f.notices.length,0); } }); test('a home progress page closes quietly at expiry and still syncs server state',async()=>{ const f=continuationFixture();f.run();f.setNow(12000);f.view.expiryPending=true; let destroyed=0,acknowledged=0; f.view.overlay={destroy(){destroyed++;}};f.view.panel={acknowledge(){acknowledged++;}}; f.bridge.sync=async()=>{f.calls.sync++;return true;}; f.view.showElimination=()=>assert.fail('home expiry must not animate'); await f.view.showExpiry(); assert.equal(destroyed,1);assert.equal(acknowledged,1);assert.equal(f.view.overlay,null); assert.equal(f.view.entry.active,false);assert.equal(f.view.expiryPending,false);assert.equal(f.calls.sync,1); }); test('an already queued home expiry finishes its popup slot without showing an animation',async()=>{ const f=continuationFixture();f.run(); let pending=true,item=null,seen=true; f.service.hasPendingLoss=()=>pending;f.service.elimination=()=>item; f.service.acknowledgeElimination=()=>item=null; f.service.unseenResult=()=>!seen;f.service.acknowledgeResult=()=>seen=true; f.bridge.sync=async()=>{pending=false;f.data.run.status='expired';item={key:'expired',after:{status:'expired'}};seen=false;return true;}; f.bridge.pagePrefab=f.bridge.pageFragment=()=>assert.fail('late timeout must not load a stage'); f.view.refresh();assert.equal(f.popups.length,1); let done=false;f.popups[0].show(()=>done=true);await new Promise(setImmediate); assert(done);assert.equal(item,null);assert(seen);assert.equal(f.view.overlay,null); }); test('home still presents genuinely unseen rewards and pending win/loss animations',()=>{ for(const outcome of ['reward','won','lost']) { const f=continuationFixture();f.run(outcome==='lost'?'failed':'waiting'); if(outcome==='reward') {f.service.unseenResult=()=>true;f.service.resultKey=()=>'r:1:waiting';f.bridge.allowsAutomaticResults=()=>true;} else f.service.elimination=()=>({key:'r:1:'+outcome,after:{stage:1,status:outcome}}); f.view.refresh();f.view.refresh(); assert.deepEqual(f.popups.map(p=>p.key),[outcome==='reward'?'cloudRiseResult':'cloudRiseElimination']); } }); test('final cloud movement opens victory before Next continues and waits for close',async()=>{ const f=continuationFixture();f.run('waiting');const order=[];let close,ended=false; let item={key:'r:1:5',after:{stage:1,status:'won'}}; f.service.elimination=()=>item;f.service.acknowledgeElimination=()=>{item=null;order.push('ack-motion');}; f.service.unseenResult=()=>true; f.cc.instantiate=()=>({destroy:()=>order.push('destroy'),getComponent:()=>({preparePage:async()=>{}, initialize:(mode,inGame,onClose)=>{assert.equal(mode,'elimination');close=onClose;}, playElimination:async()=>{order.push('motion');return true;}, openResult:()=>order.push('victory'),acknowledge:()=>order.push('ack-result')})}); const task=f.view.showElimination().then(()=>ended=true); for(let i=0;i<20&&!order.includes('victory');i++)await Promise.resolve(); assert.deepEqual(order,['motion','ack-motion','victory']);assert.equal(ended,false); assert(f.view.overlay.zIndex > f.cc.macro.MAX_ZINDEX - 2, 'activity settlement covers the settings popup'); assert(f.view.overlay.zIndex < f.cc.macro.MAX_ZINDEX, 'loading overlay remains above settlement'); close();await task; assert.deepEqual(order,['motion','ack-motion','victory','ack-result','destroy']);assert.equal(f.view.overlay,null); }); test('an abandoned loss waits in gameplay and is queued once on HomeScene, including late responses',async()=>{ for(const late of [false,true]) { const f=continuationFixture();f.run('failed');f.data.run.stages[0].status='lost'; let item=late?null:{key:'r:1:lost',after:{stage:1,status:'lost'}}; f.service.elimination=()=>item;f.view.layoutEntry=()=>{};f.view.map={}; f.view.refresh();assert.equal(f.popups.length,0,'gameplay must not auto-play the loss'); f.view.map=null;f.view.refresh(); if(late){assert.equal(f.popups.length,0);item={key:'r:1:lost',after:{stage:1,status:'lost'}};} f.view.refresh();f.view.refresh();assert.deepEqual(f.popups.map(p=>p.key),['cloudRiseElimination']); let shown=0,done=0;f.view.showElimination=async()=>{shown++;item=null;}; f.popups[0].show(()=>done++);await new Promise(setImmediate); assert.equal(shown,1);assert.equal(done,1);f.view.refresh();assert.equal(f.popups.length,1); } }); test('loss movement immediately opens the failure state and closing prevents another home result popup',async()=>{ const f=continuationFixture();f.run('failed');f.data.run.stages[0].status='lost'; const order=[];let close,ended=false,unseen=true,item={key:'r:1:lost',after:{stage:1,status:'lost'}}; f.service.elimination=()=>item;f.service.acknowledgeElimination=()=>{item=null;order.push('ack-motion');}; f.service.unseenResult=()=>unseen; f.cc.instantiate=()=>({destroy:()=>order.push('destroy'),getComponent:()=>({preparePage:async()=>{}, initialize:(mode,inGame,onClose)=>{close=onClose;},playElimination:async()=>{order.push('motion');return true;}, openResult:()=>order.push('failure'),acknowledge:()=>{unseen=false;order.push('ack-result');}})}); const task=f.view.showElimination().then(()=>ended=true); for(let i=0;i<20&&!order.includes('failure');i++)await Promise.resolve(); assert.deepEqual(order,['motion','ack-motion','failure']);assert.equal(ended,false); close();await task;f.view.refresh(); assert.equal(unseen,false);assert.equal(f.popups.length,0);assert.equal(f.view.overlay,null); }); test('failure result uses the cloud progress scene, hides counters, retains survivors, and allows tap dismissal',()=>{ const f=fixture();f.run('failed');f.data.run.stages[0].status='lost';const p=f.panel('overview');p.mode='elimination'; p.failureMessage={active:false};p.progressStatsContents=Array.from({length:5},()=>({active:true})); p.progressRoster={active:true};p.modalBackground={active:true};p.fitProgressBackground=()=>{}; let pool;p.progressPool={setValue:v=>pool=v};p.busy=true; p.openResult(); assert.equal(p.mode,'result');assert.equal(p.busy,false);assert.equal(p.buttons[0].active,false); assert.equal(p.pages[2].active,true);assert.equal(p.pages[3],null);assert.equal(p.modalBackground.active,false); assert.equal(p.failureMessage.active,true);assert.equal(p.progressRoster.active,true); assert(p.progressStatsContents.every(n=>!n.active));assert.equal(pool,10000); assert.equal(p.clocks[0].string,'00:00:02','failed state retains the personal deadline'); }); test('legacy flags do not gate enrollment; open, playing and waiting share the description art',()=>{ const f=fixture();f.data.period.enabled=false;f.data.period.status='inactive'; assert.equal(f.bridge.registrationOpen(),true);assert.equal(f.bridge.shouldPreload(f.data),true); const p=f.panel();p.overviewDescriptionImage={active:false};p.render(); assert.equal(p.overviewDescriptionImage.active,true); assert.equal(p.buttons[1].active,true); for(const status of ['playing','waiting','completed','failed','expired']) { f.run(status);p.render(); const active=['playing','waiting'].includes(status); assert.equal(p.overviewDescriptionImage.active,active); assert.equal(f.bridge.visible(),active); } }); test('entry follows registration boundaries and rejects stale historical runs',()=>{ const f=fixture();assert.equal(f.bridge.visible(),true); f.setNow(11000);assert.equal(f.bridge.visible(),false); f.setNow(8000);assert.equal(f.bridge.visible(),false); for(const status of ['completed','failed','expired']) { f.run(status);f.setNow(10000);assert.equal(f.bridge.visible(),false);f.setNow(13000);assert.equal(f.bridge.visible(),false); } f.run('completed');f.data.run.stages[0].status='won';f.data.run.stages[0].rewardSaved=false; assert.equal(f.bridge.visible(),false,'terminal runs never expose the home entry'); assert.equal(f.bridge.shouldPreload(f.data),true,'unsaved rewards still retry independently'); }); test('personal playing and waiting stay accessible after signup closes, only until personal expiry',()=>{ for(const status of ['playing','waiting']) { const f=fixture();f.run(status);f.setNow(11000);assert.equal(f.bridge.visible(),true); f.setNow(12000);assert.equal(f.bridge.visible(),false); } }); test('open registration page removes start at the exact deadline, even offline; sync retries are throttled',()=>{ const f=fixture(),p=f.panel();assert.equal(p.buttons[1].active,true); f.setNow(11000);p.tick();assert.equal(p.buttons[1].active,true);assert.equal(p.overviewDescription.string,''); p.buttons[1].click();assert.equal(f.calls.start,0); const count=f.calls.sync;for(let i=0;i<10;i++)p.tick();assert.equal(f.calls.sync,count); f.setNow(41000);p.tick();assert.equal(f.calls.sync,count+1); }); test('registration closure preserves manual next-stage action for an enrolled player',async()=>{ const f=fixture();f.run('waiting');const p=f.panel();f.setNow(11000);p.tick(); assert.equal(p.buttons[1].active,true);assert.equal(p.mode,'overview'); p.preparePage=async()=>{};p.buttons[1].click();for(let i=0;i<6;i++)await Promise.resolve();assert.equal(f.calls.start,1); }); test('personal expiry hands waiting/progress/roster pages to their owner without acknowledging a result',()=>{ for(const mode of ['overview','progress','participants']) { const f=fixture();f.run(mode==='overview'?'waiting':'playing');const p=f.panel(); let expired=0;p.onExpired=()=>expired++; p.mode=mode;f.setNow(12000);p.tick(); assert.equal(expired,1);assert.equal(p.mode,mode); assert.equal(f.calls.play,0); assert.equal(f.calls.close,0); } }); test('server closure updates a page immediately',()=>{ const f=fixture(),p=f.panel();f.setData(null);f.emit(); assert.equal(p.overviewDescription.string,'');assert.equal(p.buttons[1].active,true); }); test('home opening rechecks eligibility before loading activity resources',async()=>{ const f=fixture();f.setData(null); f.bridge.pagePrefab=async()=>{f.calls.prefab++;}; const View=f.load('assets/Script/CloudRiseView.ts',{ './module/Config/CloudRiseService':{default:f.bridge},'./Sdk/MiniGameSdk':{MiniGameSdk:{API:{showToast(){}}}}, '../home_popup_queue/HomePopupQueue':{default:{}}}); const v=new View();v.node={};await v.open();assert.equal(f.calls.prefab,0);assert.equal(v.overlay,null); }); test('locked home entry shows the configured level instead of loading or opening the activity',async()=>{ for(const completed of [103,109]) { const f=continuationFixture();f.data.period.unlockLevel=110;f.cc.fx.GameConfig.GM_INFO.level=completed; let loaded=0;f.bridge.pagePrefab=async()=>{loaded++;return {};}; assert.equal(f.bridge.visible(),true);assert.equal(f.bridge.shouldPreload(f.data),false); await f.view.open(); assert.deepEqual(f.notices,['月光宝盒活动通过第110关后解锁']); assert.equal(loaded,0);assert.equal(f.view.overlay,null);assert.equal(f.view.busy,false); } }); test('passing the unlock level opens the activity and enrolled runs remain accessible after a threshold edit',async()=>{ for(const enrolled of [false,true]) { const f=continuationFixture();f.data.period.unlockLevel=110; f.cc.fx.GameConfig.GM_INFO.level=enrolled?103:110; if(enrolled)f.run('waiting'); assert.equal(f.bridge.lockedUntilLevel(),0);assert.equal(f.bridge.shouldPreload(f.data),true); await f.view.open();assert.ok(f.view.overlay);assert.deepEqual(f.notices,[]); } }); test('home entry uses the refreshed unlock level and rechecks it after resource loading',async()=>{ const f=continuationFixture();f.data.period.unlockLevel=100;f.cc.fx.GameConfig.GM_INFO.level=103; f.service.sync=async()=>{f.data.period.unlockLevel=110;return true;}; await f.view.open();assert.equal(f.view.overlay,null);assert.deepEqual(f.notices,['月光宝盒活动通过第110关后解锁']); f.service.sync=async()=>true;f.data.period.unlockLevel=100;f.notices.length=0; f.bridge.pagePrefab=async()=>{f.data.period.unlockLevel=120;return {};}; await f.view.open();assert.equal(f.view.overlay,null);assert.deepEqual(f.notices,['月光宝盒活动通过第120关后解锁']); }); test('idle home no longer polls private status at boundaries, every 30 seconds or on foreground',async()=>{ const f=fixture(); const View=f.load('assets/Script/CloudRiseView.ts',{ './module/Config/CloudRiseService':{default:f.bridge},'./Sdk/MiniGameSdk':{MiniGameSdk:{API:{showToast(){}}}}, '../home_popup_queue/HomePopupQueue':{default:{}}}); const v=new View();let visible;v.refresh=()=>{visible=f.bridge.visible();}; v.tick();await Promise.resolve();assert.equal(visible,true); v.tick();assert.equal(f.calls.sync,0); f.setNow(11000);v.tick();await Promise.resolve();assert.equal(visible,false);assert.equal(f.calls.sync,0); f.setNow(41000);v.tick();await Promise.resolve();assert.equal(f.calls.sync,0); v.onShow();await Promise.resolve();assert.equal(f.calls.sync,0); }); test('all stage timeouts reuse the same overlay until the final tap without loading or instantiating another page',async()=>{ for(const number of [1,2,3]) { const f=continuationFixture();f.cc.warn=error=>{throw error;};f.run();f.data.run.stage=number;f.data.run.stages[0].stage=number; const p=f.panel('progress');f.setNow(12000); let destroyed=0,item=null,release,seen=false,syncs=0; const overlay={destroy(){destroyed++;}};f.view.overlay=overlay;f.view.panel=p; f.view.map={node:{},pause:true};f.view.priorPause=false;f.view.expiryPending=true; f.cc.instantiate=()=>assert.fail('must retain the current stage'); f.bridge.pagePrefab=f.bridge.pageFragment=()=>assert.fail('must not reload the stage'); f.service.elimination=()=>item;f.service.acknowledgeElimination=()=>item=null; f.service.unseenResult=()=>!seen;f.service.acknowledgeResult=()=>seen=true; f.service.sync=async()=>{syncs++;f.data.run.status='expired';f.data.run.stages[0].status='expired';item={key:'expired',after:{stage:number,status:'expired'}};return true;}; f.bridge.sync=f.service.sync; p.playElimination=()=>new Promise(resolve=>release=()=>resolve(true)); const task=f.view.showExpiry();await new Promise(setImmediate); assert.equal(f.view.overlay,overlay);assert.equal(f.view.panel,p);assert.equal(destroyed,0); assert.equal(p.mode,'elimination');assert.equal(f.view.map.pause,true); await f.view.showExpiry();assert.equal(syncs,1,'polling cannot start a second animation'); release();await new Promise(setImmediate); assert.equal(p.mode,'result');assert.equal(p.failureMessage.active,true);assert.equal(destroyed,0); touch(p,'touchstart');touch(p,'touchend');await task; assert.equal(destroyed,1);assert.equal(f.view.expiryPending,false);assert.equal(f.view.map.pause,false); } }); test('expiry synchronization failure retains the current page and remains retryable',async()=>{ const f=continuationFixture();f.run();f.setNow(12000);f.view.expiryPending=true; let destroyed=0;const overlay={destroy(){destroyed++;}};f.view.overlay=overlay;f.service.sync=async()=>false; f.view.map={node:{},pause:true};f.view.priorPause=false; await f.view.showExpiry();assert.equal(destroyed,0);assert.equal(f.view.overlay,overlay);assert.equal(f.view.map.pause,true); assert.equal(f.view.expiryPending,true);assert.equal(f.view.busy,false); }); test('personal challenge and pending work still sync in the background and on foreground',async()=>{ for(const kind of ['playing','waiting','pending','unsaved','result','overlay']) { const f=continuationFixture();f.view.refresh=()=>{}; if(['playing','waiting'].includes(kind))f.run(kind); if(kind==='pending')f.service.pending=()=>true; if(kind==='unsaved'){f.run('completed');Object.assign(f.data.run.stages[0],{status:'won',rewardSaved:false});} if(kind==='result')f.service.unseenResult=()=>true; if(kind==='overlay')f.view.overlay={}; f.view.tick();await Promise.resolve();assert.equal(f.calls.sync,1,kind); f.view.tick();assert.equal(f.calls.sync,1,kind); f.view.onShow();await Promise.resolve();assert.equal(f.calls.sync,2,kind); f.setNow(41000);f.view.tick();await Promise.resolve();assert.equal(f.calls.sync,3,kind); f.setData(null);f.service.pending=()=>false;f.service.unseenResult=()=>false;f.view.overlay=null; f.setNow(71000);f.view.tick();assert.equal(f.calls.sync,3,'polling stops after personal work finishes'); } }); test('an event ending while resources load cannot open a stale registration page',async()=>{ const f=fixture();let release; f.bridge.pagePrefab=()=>new Promise(resolve=>{release=resolve;}); const View=f.load('assets/Script/CloudRiseView.ts',{ './module/Config/CloudRiseService':{default:f.bridge},'./Sdk/MiniGameSdk':{MiniGameSdk:{API:{showToast(){}}}}, '../home_popup_queue/HomePopupQueue':{default:{}}}); const v=new View();v.node={};const task=v.open(); for(let i=0;i<5&&!release;i++)await Promise.resolve(); assert.equal(typeof release,'function');f.setNow(11000);release({});await task;assert.equal(v.overlay,null); }); test('overview uses the configured next pool and freezes an enrolled stage pool',()=>{ const f=fixture(),p=f.panel();let amount;p.overviewPool={setValue:value=>{amount=value;}}; f.data.pools=[12000,18000,24000];p.render();assert.equal(amount,12000); f.run('waiting');p.render();assert.equal(amount,18000); f.run('playing');p.render();assert.equal(amount,10000,'current run keeps its own pool'); assert.match(p.clocks[0].string,/^\d{2}:\d{2}:\d{2}$/); }); test('image number row reuses five slots, centers with coin, and rejects more than five digits',()=>{ class Node { constructor(){this.children=[];this.width=0;this.height=0;this.scale=1;this.x=0;} addComponent(){return {};} getComponent(){return {setMaterial(){}};} addChild(n){n.parent=this;this.children.push(n);} setContentSize(w,h){this.width=w;this.height=h;} setPosition(x,y){this.x=x;this.y=y;} removeFromParent(){this.parent.children=this.parent.children.filter(n=>n!==this);} destroy(){} } class Sprite{}Sprite.SizeMode={CUSTOM:0}; const cc={_decorator:{ccclass:c=>c,executeInEditMode:c=>c,property:(...args)=>args.length>1?undefined:()=>{}},Node,Sprite,SpriteFrame:class{}, Material:class {static getBuiltinMaterial(){return {};}},Component:class{},Color:{WHITE:'white'},color:(r,g,b)=>[r,g,b]}; const exports={};vm.runInNewContext(ts.transpileModule(fs.readFileSync('assets/cloud_rise_home/scripts/CloudRiseNumberToImage.ts','utf8'), {compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2017,experimentalDecorators:true}}).outputText,{exports,cc}); const n=new exports.default();n.node=new Node();n.node.width=380;n.coin=new Node();n.coin.width=74;n.content=new Node(); for(let i=0;i<5;i++)n.content.addChild(new Node()); n.digits=Array.from({length:10},()=>({getRect:()=>({width:35,height:39})})); for(const value of [10000,15000,20000,9,0,99999]) { n.setValue(value);assert.equal(n.content.children.length,5); assert.equal(n.content.children.filter(child=>child.active).length,String(value).length); const left=n.coin.x-n.coin.width*n.coin.scale/2; const right=n.content.x+(String(value).length*n.digitHeight*35/39*n.widthScale+(String(value).length-1)+8)*n.content.scale; assert(Math.abs(left+right)<1e-6);assert(right<=190); const child=n.content.children[0];n.setValue(value);assert.equal(n.content.children[0],child,'unchanged value reuses nodes'); } n.setValue(100000);assert.equal(n.content.active,false);assert.equal(n.coin.active,false);assert.equal(n.content.children.length,5); n.setValue(10000);assert.equal(n.content.active,true);assert.equal(n.coin.active,true); }); test('a settled previous period cannot replace the new signup page, clock, pool or stage checks',async()=>{ for(const status of ['expired','failed','completed']) { const f=fixture();f.run(status);const historical=f.data.run; historical.periodId='p1';historical.expiresAt=9000; f.data.period.periodId='p2';f.data.available=true;f.data.pools=[12000,18000,24000]; const p=f.panel('result');let pool;p.overviewPool={setValue:value=>pool=value};p.render(); assert.equal(f.bridge.run(),null);assert.equal(f.data.run,historical,'keep the raw record for recovery'); assert.equal(p.mode,'overview');assert.equal(p.buttons[1].active,true);assert.equal(pool,12000); assert.equal(p.clocks[0].string,'00:00:01','count down the new registration window'); p.tick();assert.equal(p.mode,'overview','old expiry must not reopen'); p.preparePage=async()=>{};p.buttons[1].click();for(let i=0;i<6;i++)await Promise.resolve();assert.equal(f.calls.start,1); } }); test('a previous personal challenge and an unsaved old award still take priority over new signup',()=>{ for(const status of ['playing','waiting','expired']) { const f=fixture();f.run(status);f.data.run.periodId='p1';f.data.period.periodId='p2'; if(status==='expired')Object.assign(f.data.run.stages[0],{status:'won',rewardSaved:false}); assert.equal(f.bridge.run(),f.data.run); } }); test('enrolled mainline hides the entry while the home entry stays available', () => { const f=fixture();f.run(); const View=f.load('assets/Script/CloudRiseView.ts',{ './module/Config/CloudRiseService':{default:f.bridge},'./Sdk/MiniGameSdk':{MiniGameSdk:{API:{showToast(){}}}}, '../home_popup_queue/HomePopupQueue':{default:{}}}); const view=new View();view.node={};view.entry={};view.map={};view.refresh(); assert.equal(view.entry.active,false); view.map=null;view.displayedCoin=0;view.refresh();assert.equal(view.entry.active,true); }); function touch(panel,type) { const binding=panel.node.events[type];let stopped=false; assert.equal(binding.capture,true,'result taps intercept children before their buttons'); binding.fn.call(binding.target,{stopPropagation(){stopped=true;}}); return stopped; } test('solo and shared victories use the same artwork with the number of other winners',()=>{ for(const survivors of [1,2,43]) { const f=fixture();f.run('waiting');const p=f.panel(); p.victoryPage={};p.modalBackground={};p.design.scale=1;p.victoryShade={setContentSize(){}}; p.fitProgressBackground=()=>{};p.renderProgressStage=()=>{};p.renderProgressRoster=()=>{}; p.victoryReward={node:{},setValue(){}};p.victoryPending={node:{}}; p.victoryShared={active:false};p.victorySolo={node:{active:true}};p.victoryOthers={};p.victorySlots=Array.from({length:100},()=>({setPosition(){}}));p.avatar=()=>{}; p.victoryKey='r:1';const stage=f.data.run.stages[0];stage.survivors=survivors; p.renderVictory(stage); assert.equal(p.victoryShared.active,true); assert.equal(p.victoryOthers.string,String(survivors-1)); } }); test('intermediate wins open overview but final victory closes after the manual tap',()=>{ for(const stage of [1,2,3]) { const f=fixture();f.run(stage===3?'completed':'waiting'); f.data.run.stage=stage;Object.assign(f.data.run.stages[0],{stage,status:'won'}); let ack=0;f.service.acknowledgeResult=()=>ack++; const p=f.panel('result');p.match=async()=>{f.calls.start++;return true;}; assert.equal(p.buttons[0].active,false); assert.equal(touch(p,'touchstart'),true);assert.equal(touch(p,'touchend'),true); if(stage===3) {assert.equal(f.calls.close,1);assert.equal(ack,1);assert.equal(f.calls.start,0);continue;} assert.equal(p.mode,'overview');assert.equal(ack,1);assert.equal(f.calls.close,0);assert.equal(f.calls.start,0); assert.equal(p.buttons[1].active,true); touch(p,'touchend');f.emit();assert.equal(f.calls.start,0);assert.equal(ack,1); p.buttons[1].click();assert.equal(f.calls.start,stage===3?0:1); } }); test('animation-time touches and cancelled gestures cannot dismiss the result',()=>{ const f=fixture();f.run('waiting');const p=f.panel('result'); let ack=0;f.service.acknowledgeResult=()=>ack++; p.busy=true;touch(p,'touchstart');touch(p,'touchend');assert.equal(p.mode,'result'); touch(p,'touchstart');p.busy=false;touch(p,'touchend');assert.equal(p.mode,'result','holding through animation completion is not a fresh tap'); touch(p,'touchstart');touch(p,'touchcancel');touch(p,'touchend');assert.equal(ack,0); touch(p,'touchstart');touch(p,'touchend');assert.equal(p.mode,'overview');assert.equal(ack,1); }); test('loss and expiry results close by tapping after their presentation',()=>{ for(const status of ['failed','expired']) { const f=fixture();f.run(status);f.data.run.stages[0].status='lost'; if(status==='expired')f.setNow(12000); const p=f.panel('result');assert.equal(p.buttons[0].active,false); touch(p,'touchstart');touch(p,'touchend');assert.equal(f.calls.close,1); } }); test('closing an already displayed waiting overview does not queue it again on the same home',()=>{ const f=continuationFixture();f.run('waiting');f.view.panel={acknowledge(){}}; f.view.overlay={destroy(){}};f.view.close();f.view.refresh(); assert.equal(f.popups.length,0);assert.equal(f.calls.start,0); }); test('Next stays pending through victory dismissal and overview, with optional manual enrollment',async()=>{ for(const manual of [false,true]) { const f=continuationFixture();f.run('waiting');f.view.map={};let unseen=true,finished=false; f.service.unseenResult=()=>unseen;f.service.acknowledgeResult=()=>{unseen=false;}; const p=f.panel('overview'); p.match=async()=>{f.calls.start++;f.data.run.stage=2;f.data.run.status='playing';return true;}; f.cc.instantiate=()=>({destroy(){},getComponent:()=>p}); const next=f.View.continueFromNextLevel(f.host).then(value=>{finished=true;return value;}); for(let i=0;i<30&&p.mode!=='result';i++)await Promise.resolve(); assert.equal(p.mode,'result');assert.equal(finished,false); touch(p,'touchstart');touch(p,'touchend');await Promise.resolve(); assert.equal(p.mode,'overview');assert.equal(unseen,false);assert.equal(finished,false); assert.equal(f.calls.start,0); if(manual)p.buttons[1].click(); p.buttons[0].click();assert.equal(await next,true); assert.equal(f.calls.start,manual?1:0);assert.equal(f.data.run.stage,manual?2:1); } }); test('elimination -> reward -> overview restores actual visible controls for stages 1 and 2',()=>{ for(const stage of [1,2]) { const f=fixture();f.run('waiting');f.data.run.stage=stage;f.data.run.stages[0].stage=stage; const p=f.panel('overview');p.fitProgressBackground=()=>{};p.show('elimination'); assert(p.buttons.every(b=>!b.active));p.openResult();touch(p,'touchstart');touch(p,'touchend'); assert.equal(p.pages[0].active,true);assert.equal(p.buttons[1].active,true); assert.equal(p.buttons[0].active,true);assert.equal(p.buttons[2].active,false); let matched=0;p.match=async()=>{matched++;return true;}; if(p.buttons[1].active)p.buttons[1].click();assert.equal(matched,1); } }); test('failure waits for a fresh tap before releasing pending retry/home presentation, including after polling',async()=>{ for(const inGame of [true,false]) { const f=continuationFixture();f.run('failed');f.data.run.stages[0].status='lost'; f.view.map=inGame?{}:null;let unseen=true,resumed=false; f.service.unseenResult=()=>unseen;f.service.acknowledgeResult=()=>{unseen=false;}; const p=f.panel('overview');f.cc.instantiate=()=>({destroy(){},getComponent:()=>p}); const task=f.view.showElimination().then(()=>resumed=true); for(let i=0;i<30&&p.mode!=='result';i++)await Promise.resolve(); assert.equal(p.mode,'result');assert.equal(resumed,false); f.emit();p.render();assert.equal((p.scheduled||[]).length,0); f.setNow(11000);f.emit();await Promise.resolve(); assert.equal(resumed,false);assert.equal(unseen,true); touch(p,'touchstart');touch(p,'touchend');await task; assert.equal(resumed,true);assert.equal(unseen,false);assert.equal(f.view.overlay,null); f.view.refresh();assert.equal(f.popups.length,0); } }); test('neither victory nor loss schedules automatic dismissal',()=>{ const f=fixture();f.run('waiting');const win=f.panel('result');assert.equal(win.scheduled,undefined); f.data.run.status='failed';f.data.run.stages[0].status='lost';const lose=f.panel('result'); assert.equal(lose.scheduled,undefined);assert.equal(f.calls.close,0); }); test('public schedule shows new periods without hiding an unfinished personal challenge',()=>{ const f=fixture(), current={periodId:'p1'}, next={periodId:'p2'}; f.data.period.periodId='p1'; assert.equal(f.bridge.homeEntryVisible(current,true,10000),true); assert.equal(f.bridge.homeEntryVisible(null,true,10000),false,'a successful empty schedule closes public registration'); assert.equal(f.bridge.homeEntryVisible(null,false,10000),false,'private registration data cannot replace a missing public schedule'); for(const status of ['playing','waiting','completed','failed','expired']) { f.run(status);f.data.run.periodId='p1'; const active=['playing','waiting'].includes(status); assert.equal(f.bridge.homeEntryVisible(current,true,10000),active); assert.equal(f.bridge.homeEntryVisible(null,true,10000),active); assert.equal(f.bridge.homeEntryVisible(null,false,10000),active,'an enrolled personal run survives a public schedule failure'); assert.equal(f.bridge.homeEntryVisible(next,true,10000),true); assert.equal(f.bridge.homeEntryVisible(current,true,13000),false,'personal expiry must be respected'); } }); test('home icon appears at cached start without relogin or a successful private status request',async()=>{ const f=fixture();f.setData(null); f.cc.fx.GameConfig.GM_INFO.level=100; f.cc.fx.GameConfig.GM_INFO.token='test-token'; f.cc.fx.StorageMessage={getStorage:()=>null,setStorage(){}}; let reply; const schedules=f.load('assets/Script/module/Config/ActivityScheduleService.ts',{'../Pay/Utils':{default:{ apiBaseUrl:()=> 'test/',POST:(_path,_body,callback)=>{reply=callback;}}}}); const task=schedules.sync();reply({code:1,data:{schemaVersion:1,serverNow:10000,windowEndsAt:100000,refreshAt:40000, activities:[{activityId:'cloudRise',periodId:'new',configVersion:'v1',startsAt:11000,endsAt:20000,unlockLevel:100}]}}); await task; const View=f.load('assets/Script/CloudRiseView.ts',{ './module/Config/CloudRiseService':{default:f.bridge}, './module/Config/ActivityScheduleService':{default:schedules}, './Sdk/MiniGameSdk':{MiniGameSdk:{API:{showToast(){}}}},'../home_popup_queue/HomePopupQueue':{default:{}}}); const view=new View();view.node={};view.entry={};view.displayedCoin=0; const unsubscribe=schedules.subscribe(()=>view.tick()); view.tick();assert.equal(view.entry.active,false);const initial=f.calls.sync; f.setNow(11000);schedules.tick();assert.equal(view.entry.active,true);assert.equal(f.calls.sync,initial); f.setNow(20000);schedules.tick();assert.equal(view.entry.active,false);unsubscribe(); assert.equal(f.calls.sync,0,'public transitions never trigger a private status request'); }); test('home opening does not load a stale activity when private status synchronization fails',async()=>{ const f=continuationFixture();f.service.sync=async()=>false; let loaded=0;f.bridge.pagePrefab=async()=>{loaded++;return {};}; await f.view.open();assert.equal(loaded,0);assert.equal(f.view.overlay,null); assert.deepEqual(f.notices,['活动信息待同步,请重试']);assert.equal(f.view.busy,false); }); test('an already earned pending result remains presentable when the status request is offline',async()=>{ const f=continuationFixture();f.run('completed'); f.service.sync=async()=>false;f.service.unseenResult=()=>true; await f.view.open('result');assert(f.view.overlay);assert.deepEqual(f.notices,[]); }); test('home progress uses close, round completion uses blank-area confirmation, and count taps do nothing',async()=>{ const f=fixture();f.run();const p=f.panel('progress'); assert.equal(p.buttons[0].active,true);assert.equal(p.continueHint.active,false);assert.equal(p.buttons[2].active,false); p.buttons[2].click();assert.equal(p.mode,'progress');touch(p,'touchstart');touch(p,'touchend');assert.equal(f.calls.close,0); p.buttons[0].click();assert.equal(f.calls.close,1); p.show('elimination');let finished=false;const task=p.waitForProgressContinue().then(()=>finished=true); assert.equal(p.buttons[0].active,false);assert.equal(p.continueHint.active,true); p.buttons[0].click();assert.equal(f.calls.close,1);assert.equal(finished,false); const chest={};p.rewardChest=chest;p.onResultTouchStart({target:chest});p.onResultTouchEnd({target:chest}); await Promise.resolve();assert.equal(finished,false,'chest bubble taps must not advance a progress page'); touch(p,'touchstart');touch(p,'touchend');await task;assert.equal(finished,true);assert.equal(p.continueHint.active,false); }); test('victory renders every real survivor once and packs the roster into its available area',()=>{ for(const count of [1,2,8,43,100]) { const f=fixture();f.run('waiting');const p=f.panel();const stage=f.data.run.stages[0];stage.status='won';stage.survivors=count; stage.opponents=Array.from({length:99},(_,i)=>({username:'Player'+i,alive:i({parent,setPosition(x,y){this.x=x;this.y=y;}}); f.cc.instantiate=make;p.victorySlots=Array.from({length:8},make);p.avatar=(slot,player)=>slot.player=player; p.victoryPage={};p.victoryShade={setContentSize(){}};p.victoryReward={node:{},setValue(){}}; p.victoryPending={node:{}};p.victoryShared={};p.victoryOthers={};p.victoryKey='r:1';p.fitProgressBackground=()=>{}; p.renderVictory(stage);const active=p.victorySlots.filter(n=>n.active); assert.equal(active.length,count);assert.equal(p.victoryOthers.string,String(count-1)); assert.equal(new Set(active.map(n=>n.player.username)).size,count); assert(active.slice(1).every(n=>Math.abs(n.x)+70*n.scale<=400&&n.y-70*n.scale>=-630)); } }); test('a non-final clear keeps the caller paused until the progress page receives a tap',async()=>{ const f=continuationFixture();f.run('playing');const p=f.panel('overview');let resumed=false; let item={key:'r:1:2',after:{stage:1,status:'playing'}}; f.service.elimination=()=>item;f.service.acknowledgeElimination=()=>item=null; p.playElimination=async()=>true;f.cc.instantiate=()=>({destroy(){},getComponent:()=>p}); const task=f.view.showElimination().then(()=>resumed=true); for(let i=0;i<30&&!p.continueResolver;i++)await Promise.resolve(); assert(p.continueResolver);assert.equal(p.mode,'elimination');assert.equal(resumed,false); assert.equal(p.continueHint.active,true);assert.equal(p.buttons[0].active,false); f.emit();assert.equal(resumed,false);touch(p,'touchstart');touch(p,'touchend');await task; assert.equal(resumed,true);assert.equal(f.view.overlay,null);assert.equal(item,null); }); test('a pending abandon response reserves the first home popup until elimination is dismissed',async()=>{ const f=continuationFixture();f.run();f.view.layoutEntry=()=>{}; let pending=true,item=null,finishSync,closeAnimation,done=false,shown=false; f.service.hasPendingLoss=()=>pending;f.service.elimination=()=>item; f.bridge.sync=()=>new Promise(resolve=>finishSync=()=>{pending=false;item={key:'loss'};resolve(true);}); f.view.showElimination=()=>new Promise(resolve=>{shown=true;closeAnimation=()=>{item=null;resolve();};}); f.view.refresh();f.view.refresh();assert.equal(f.popups.length,1);assert.equal(f.popups[0].priority,0); f.popups[0].show(()=>done=true);assert.equal(shown,false);assert.equal(done,false); finishSync();await new Promise(setImmediate);assert.equal(shown,true);assert.equal(done,false); f.view.refresh();assert.equal(f.popups.length,1); closeAnimation();await new Promise(setImmediate);assert.equal(done,true); }); test('activity guide blocks continuation and its close restores the same progress page',()=>{ const f=fixture();f.run();const p=f.panel('progress');p.design.scale=1;p.design.childrenCount=8; p.activityGuide={active:false,children:[],setSiblingIndex(i){this.sibling=i;}};p.guideShade={setContentSize(){}}; p.continueResolver=()=>{throw Error('guide must not continue gameplay');}; p.showGuide(true);assert.equal(p.canContinue(),false);assert.equal(p.continueHint.active,false); const event={stopPropagation(){this.stopped=true;}}; p.onResultTouchEnd(event);assert.equal(p.activityGuide.active,true,'opening tap cannot also close help'); p.onResultTouchStart(event);p.onResultTouchCancel(event);p.onResultTouchEnd(event);assert.equal(p.activityGuide.active,true); p.onResultTouchStart(event);p.onResultTouchEnd(event);assert.equal(p.activityGuide.active,false);assert(event.stopped); assert.equal(p.mode,'progress');assert.equal(p.canContinue(),true);assert.equal(p.continueHint.active,true); assert.equal(f.calls.close,0);assert.equal(f.calls.play,0); }); test('home entry hides below unlock level, including a previously opened personal run',()=>{ const f=fixture(),period={periodId:'p',unlockLevel:100};f.data.period=period; for(const enrolled of [false,true]){ if(enrolled)f.run(); f.cc.fx.GameConfig.GM_INFO.level=99; assert.equal(f.bridge.homeEntryVisible(period,true,10000),false); assert.equal(f.bridge.homeEntryVisible(null,false,10000),false); f.cc.fx.GameConfig.GM_INFO.level=100; assert.equal(f.bridge.homeEntryVisible(period,true,10000),true); } }); test('home countdown follows the personal deadline, refreshes on resume and expires with the entry',()=>{ const f=fixture();f.cc.fx.GameConfig.GM_INFO.level=100; const period={periodId:'p',unlockLevel:100,endsAt:10000+86400000};let hasSchedule=true; const View=f.load('assets/Script/CloudRiseView.ts',{ './module/Config/CloudRiseService':{default:f.bridge}, './module/Config/ActivityScheduleService':{default:{ active:()=>hasSchedule&&f.service.now()hasSchedule,now:()=>f.service.now()}}, './Sdk/MiniGameSdk':{MiniGameSdk:{}},'../home_popup_queue/HomePopupQueue':{default:{}} }); const view=new View();view.node={};view.entry={};view.entryCountdown={string:''};view.displayedCoin=0; view.refresh();assert.equal(view.entryCountdown.string,'24:00:00');assert.equal(view.entry.active,true); f.setNow(11000);view.refresh();assert.equal(view.entryCountdown.string,'23:59:59'); f.run();f.data.run.expiresAt=10000+3600000;hasSchedule=false; view.refresh();assert.equal(view.entryCountdown.string,'00:59:59');assert.equal(view.entry.active,true); f.setNow(10000+1800000);view.refresh();assert.equal(view.entryCountdown.string,'00:30:00','resume calculates from the deadline'); f.setNow(f.data.run.expiresAt);view.refresh();assert.equal(view.entry.active,false);assert.equal(view.entryCountdown.string,'00:00:00'); assert.equal(f.calls.sync,0,'updating the clock makes no activity request'); }); test('guide parts pop top to bottom and closing cancels all pending animations',()=>{ const f=fixture(),p=f.panel(),actions=[],stops=[]; p.design.scale=1;p.design.childrenCount=8;p.guideShade={setContentSize(){}}; const parts=Array.from({length:7},(_,i)=>({name:'GuidePart0'+(i+1),scale:1})); p.activityGuide={active:false,children:[{name:'Shade'},{name:'GuideContent',scale:0.85,children:parts}],setSiblingIndex(){}}; f.cc.Tween={stopAllByTarget:n=>stops.push(n)}; f.cc.tween=n=>{const action={node:n};actions.push(action);return { delay(t){action.delay=t;return this;},to(t,props,options){Object.assign(action,{duration:t,props,options});return this;},start(){} };}; p.showGuide(true);assert.equal(actions.length,7);assert(parts.every(n=>n.scale===0)); actions.forEach((a,i)=>{assert.equal(a.delay,(i+1)*0.3);assert.equal(a.options.easing,'backOut');}); p.showGuide(false);assert.equal(stops.length,14);assert(parts.every(n=>n.scale===1)); p.showGuide(true);assert.equal(actions.length,14);assert(parts.every(n=>n.scale===0)); }); test('expiry during a mainline board skips activity settlement on Next and direct finish paths for all stages',async()=>{ for(const stage of [1,2,3])for(const entry of ['next','finish']){ const f=continuationFixture();f.run();f.data.run.stage=stage;f.data.run.stages[0].stage=stage;f.view.map={}; let item=null,seen=true;f.service.elimination=()=>item;f.service.unseenResult=()=>!seen; f.service.acknowledgeElimination=()=>item=null;f.service.acknowledgeResult=()=>seen=true; f.service.sync=async()=>{f.data.available=false;f.data.run.status='expired';f.data.run.stages[0].status='lost';item={key:'expiry',after:{stage,status:'expired'}};seen=false;return true;}; f.view.showElimination=()=>assert.fail('expired activity must not open settlement over a completed board'); f.bridge.pagePrefab=f.bridge.pageFragment=()=>assert.fail('must not load expired result resources'); if(entry==='next')assert.equal(await f.View.continueFromNextLevel(f.host),true);else await f.View.playElimination(f.host); assert.equal(item,null);assert.equal(seen,true);assert.equal(f.view.overlay,null);assert.deepEqual(f.notices,[]); } });