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:'', elimination:()=>null, unseenResult:()=>false, resultKey:()=>'', acknowledgeResult(){}, subscribe(fn){listener=fn;return ()=>{};}, sync:async()=>{calls.sync++;return true;}, startStage:async()=>{calls.start++;return true;}}; 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) { 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{}}}); 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.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','overviewAction','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.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('authored overview caption is bound and renders enrollment, next-stage and closed states',()=>{ const a=JSON.parse(fs.readFileSync('assets/cloud_rise_home/prefab/CloudRise.prefab')); const binding=a.find(o=>o.pages&&o.buttons),ref=binding.overviewAction; const label=ref&&a[ref.__id__]; const f=fixture(),cover={active:false};let p; assert.doesNotThrow(()=>{p=f.panel('overview',panel=>{ panel.overviewAction=label?{string:label._string,node:{active:a[label.node.__id__]._active}}:null; panel.buttons[1].getChildByName=name=>name==='CaptionCover'?cover:null; });},'opening the authored prefab must not fail in initialize/render'); assert.equal(label.__type__,'cc.Label'); const button=a[binding.buttons[1].__id__],caption=a[label.node.__id__]; assert.equal(caption._parent.__id__,binding.buttons[1].__id__); const children=button._children.map(r=>a[r.__id__]); assert(children.indexOf(caption)>children.findIndex(n=>n._name==='CaptionCover')); assert.equal(p.overviewAction.string,'开始');assert.equal(p.overviewAction.node.active,false);assert.equal(cover.active,false); for(const stage of [1,2]){ f.run('waiting');f.data.run.stage=stage;p.render(); assert.equal(p.overviewAction.string,'开始下一阶段');assert.equal(p.overviewAction.node.active,true);assert.equal(cover.active,true); } f.run('completed');p.render();assert.equal(p.overviewAction.string,'知道了'); }); 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,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),false); f.service.sync=async()=>{throw Error('offline');}; assert.equal(await f.View.continueFromNextLevel(f.host),false); 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 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 Promise.resolve();await Promise.resolve(); 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 and avatars, 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,false); 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.overviewAction.string,'开始'); 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.overviewAction.string,'开始'); f.setNow(11000);p.tick();assert.equal(p.overviewAction.string,'知道了');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.overviewAction.string,'开始下一阶段');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 switches waiting/progress/roster pages to timeout without server response',()=>{ for(const mode of ['overview','progress','participants']) { const f=fixture();f.run(mode==='overview'?'waiting':'playing');const p=f.panel(); p.mode=mode;f.setNow(12000);p.tick(); assert.equal(p.mode,'result');assert.equal(p.expiredMessage.active,true);assert.equal(p.failureMessage.active,false); assert.equal(f.calls.play,0); touch(p,'touchstart');touch(p,'touchend');assert.equal(f.calls.close,1); } }); 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.overviewAction.string,'知道了'); }); 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('home refreshes on elapsed deadlines, polls new events and resyncs on returning to 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);const initial=f.calls.sync; v.tick();assert.equal(f.calls.sync,initial); f.setNow(11000);v.tick();await Promise.resolve();assert.equal(visible,false);assert.equal(f.calls.sync,initial+1); f.setNow(41000);v.tick();await Promise.resolve();assert.equal(f.calls.sync,initial+2); v.onShow();await Promise.resolve();assert.equal(f.calls.sync,initial+3); }); 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.overviewAction.string,'开始');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=[]; 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.overviewAction.string,stage===3?'知道了':'开始下一阶段'); 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); });