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'); const base = 'assets/Script/module/Config/CloudRiseService.ts'; function fixture(data) { const calls = { status: 0, bundle: 0, prefab: 0, runtime: 0, retained: 0 }; const info = { uid: 'u', userId: 1001, otherLevel: 0 }, pending = []; let bridge, failBundle = false, hold = false; const runtime = { subscribe() {}, sync: async () => { calls.runtime++; return true; }, data: () => data, now: () => Date.now(), error: '' }; const cc = { warn() {}, Prefab: class {}, fx: { GameConfig: { GM_INFO: info } }, assetManager: { loadBundle(name, done) { calls.bundle++; assert.equal(name, 'cloud_rise'); if (failBundle) return done(new Error('offline')); bridge.install(runtime); done(null, { load(path, type, cb) { calls.prefab++; assert.equal(path, 'prefab/CloudRise'); cb(null, { addRef() { calls.retained++; } }); } }); } } }; const exports = {}; vm.runInNewContext(ts.transpileModule(fs.readFileSync(base,'utf8'), {compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2017}}).outputText, {exports,cc,Date,console,require(name) { if(name.includes('Utils')) return {default:{POST(url,body,done) {assert.equal(url,"cloudRise/cloudRise"); calls.status++; assert.equal(body.uid,'1001'); assert.equal(body.action,'status'); if(hold) pending.push(done); else done(data ? {code:1,data} : {code:0,data:null,msg:"活动未开启"});}}}; return {default:{apply() {}}}; }}); bridge = exports.default; return {bridge,calls,info,pending,hold(){hold=true;},failBundle(value){failBundle=value;}}; } const state = () => ({serverNow:Date.now(),available:true,period:{enabled:true,startsAt:Date.now()-1000,endsAt:Date.now()+100000},run:null}); test('login preloads bundle and prefab only with fresh eligible server status', async () => { const f=fixture(state()); assert.equal(await f.bridge.sync(),true); await f.bridge.pagePrefab(); await f.bridge.pagePrefab(); assert.deepEqual(f.calls,{status:1,bundle:1,prefab:1,runtime:1,retained:1}); }); test('closed, future, exhausted and unknown eligibility never automatically load the bundle',async()=>{ const closed=state();closed.period.endsAt=Date.now()-1; const future=state();future.period.startsAt=Date.now()+10000; const used=state();used.available=false; for(const data of [closed,future,used]) {const f=fixture(data);await f.bridge.sync();assert.equal(f.calls.bundle,0);assert.equal(f.calls.prefab,0);} }); test('an active personal deadline permits preloading after registration closes, including waiting',async()=>{ for(const status of ['playing','waiting']) { const data=state();data.available=false;data.period.endsAt=Date.now()-1; data.run={status,expiresAt:Date.now()+10000};const f=fixture(data);await f.bridge.sync();assert.equal(f.calls.bundle,1); } const data=state();data.available=false;data.run={status:'expired',expiresAt:Date.now()-1}; const f=fixture(data);await f.bridge.sync();assert.equal(f.calls.bundle,0); }); test('login is nonblocking, coalesces in-flight checks and ignores results for a switched account',async()=>{ const f=fixture(state());f.hold();assert.equal(f.bridge.afterLogin(),undefined); const pending=f.bridge.sync();assert.equal(f.calls.status,1);f.info.userId=2002;f.pending[0]({code:1,data:state()}); assert.equal(await pending,false);assert.equal(f.calls.bundle,0); }); test('failed background bundle load can be retried without creating a match',async()=>{ const f=fixture(state());f.failBundle(true);assert.equal(await f.bridge.sync(),false); f.failBundle(false);assert.equal(await f.bridge.sync(),true);assert.equal(f.calls.bundle,2); }); test('prefab owns five pages, editable tracks, participant row, buttons and component references',()=>{ const path='assets/cloud_rise/prefab/CloudRise.prefab', objects=JSON.parse(fs.readFileSync(path)); const properties=objects.find(o=>o.rowTemplate&&o.overviewAction); assert(properties);assert.equal(properties.pages.length,5);assert.equal(properties.buttons.length,6); assert.deepEqual(properties.pages.map(r=>objects[r.__id__]._name),['Overview','Matching','Progress','Result','Participants']); assert.deepEqual(properties.tracks.map(r=>objects[r.__id__]._children.length),[5,7,9]); assert.equal(objects[properties.rowTemplate.__id__]._children.length,4); function references(value) { if(!value||typeof value!=='object')return; if('__id__' in value)assert(objects[value.__id__],`broken reference ${value.__id__}`); Object.values(value).forEach(references); } objects.forEach(references); const panel=fs.readFileSync('assets/cloud_rise/scripts/CloudRisePanel.ts','utf8'); assert(!panel.includes('new cc.Node'));assert(!panel.includes('addComponent(')); const bridge=fs.readFileSync(base,'utf8');assert(!/import.+cloud_rise/.test(bridge)); assert(fs.readFileSync('assets/Script/GameManager.ts','utf8').includes('CloudRiseService.afterLogin()')); }); test('victory overlays progress with four authored reveal groups, native reward digits and eight avatar slots',()=>{ const objects=JSON.parse(fs.readFileSync('assets/cloud_rise/prefab/CloudRise.prefab')); const p=objects.find(o=>o.rowTemplate&&o.overviewAction),victory=objects[p.victoryPage.__id__]; assert.deepEqual(p.victoryGroups.map(r=>objects[r.__id__]._name),['VictoryTitle','VictoryChest','VictoryCoins','VictoryPeople']); assert.equal(p.victorySlots.length,8); const root=objects[victory._parent.__id__]; assert(root._children.findIndex(r=>r.__id__===p.victoryPage.__id__)r.__id__===p.buttons[0].__id__)); const reward=objects[p.victoryReward.__id__]; assert.equal(reward.sourceArt,true);assert.equal(reward.showCoin,false);assert.equal(reward.digits.length,10); assert.equal(objects[p.victoryShade.__id__]._parent.__id__,p.victoryPage.__id__); }); test('all reward displays reuse five authored sprite nodes when the amount changes', () => { const objects = JSON.parse(fs.readFileSync('assets/cloud_rise/prefab/CloudRise.prefab')); const renderers = objects.filter(o => o.digits && o.content && o.coin); assert.equal(renderers.length, 4); for (const renderer of renderers) { const children = objects[renderer.content.__id__]._children.map(r => objects[r.__id__]); assert.deepEqual(children.map(n => n._name), ['Digit0', 'Digit1', 'Digit2', 'Digit3', 'Digit4']); assert(children.every(n => n._children.length === 0 && n._components.some(r => objects[r.__id__].__type__ === 'cc.Sprite'))); } const builtin = {}; const cc = { _decorator: { ccclass: c => c, executeInEditMode: c => c, property: (...args) => args.length >= 2 ? undefined : () => {} }, Component: class {}, SpriteFrame: class {}, Material: class { static getBuiltinMaterial() { return builtin; } }, Node: class {}, Sprite: { SizeMode: { CUSTOM: 0 } }, Color: { WHITE: {} }, v2: (x,y) => ({x,y}), Vec4: class { constructor(x,y,z,w) {Object.assign(this,{x,y,z,w});} } }; const exports = {}; vm.runInNewContext(ts.transpileModule(fs.readFileSync('assets/cloud_rise/scripts/CloudRiseNumberToImage.ts','utf8'), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2017, experimentalDecorators: true } }).outputText, { cc, exports }); const display = new exports.default(); display.node = { width: 400 }; display.coin = { width: 60 }; display.glyphMaterial = {}; display.digits = Array.from({length:10}, (_,digit) => ({ digit, getRect: () => ({x:digit*30,y:0,width:25,height:29}), getTexture: () => ({width:256,height:256}) })); const slots = Array.from({length:5}, () => { const sprite = { setMaterial(index, material) { this.material = material; }, getMaterial: () => ({ setProperty() {} }) }; return { getComponent: () => sprite, setContentSize() {}, setPosition(x,y) { this.x=x;this.y=y; }, sprite }; }); display.content = {children: slots}; for (const amount of [10000, 250, 0, 99999, 15000]) { display.setValue(amount); assert.equal(display.content.children, slots); assert.equal(slots.filter(n => n.active).map(n => n.sprite.spriteFrame.digit).join(''), String(amount)); assert(slots.filter(n => n.active).every((n,i,active) => !i || n.x > active[i-1].x)); } display.sourceArt = true; display.update(); assert(slots.every(n => n.sprite.material === builtin), 'original art replaces the old outline material'); const replacement = { getRect: () => ({width:23,height:51}) }; display.digits[1] = replacement; display.update(); assert.equal(slots[0].sprite.spriteFrame, replacement, 'changing frames refreshes an unchanged amount'); display.sourceArt = false; display.glyphMaterial = null; display.update(); assert(slots.every(n => n.sprite.material === builtin)); for (const renderer of renderers.filter(r => r.sourceArt)) { for (const ref of objects[renderer.content.__id__]._children) { const sprite = objects[ref.__id__]._components.map(r => objects[r.__id__]).find(o => o.__type__ === 'cc.Sprite'); assert.equal(sprite._materials[0].__uuid__, 'eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432'); } } }); test('a won but unsaved reward loads the activity even after the personal deadline',async()=>{ const data=state();data.available=false; data.run={status:'completed',expiresAt:Date.now()-1000,stages:[{status:'won',rewardSaved:false}]}; const f=fixture(data);await f.bridge.sync();assert.equal(f.calls.bundle,1); }); test('closed response does not preload or display the activity',async()=>{ const f=fixture(null); assert.equal(await f.bridge.sync(),false); assert.equal(f.bridge.error,'活动未开启'); assert.equal(f.bridge.visible(),false); assert.equal(f.calls.bundle,0); assert.equal(f.calls.prefab,0); }); test('popup prefab preserves home behind a scrim and binds the designed clock and image prize',()=>{ const objects=JSON.parse(fs.readFileSync('assets/cloud_rise/prefab/CloudRise.prefab','utf8')); const controller=objects.find(o=>o.rowTemplate&&o.overviewAction); const background=objects[controller.background.__id__]; assert(background._components.some(r=>objects[r.__id__].fillOpacity===155)); assert(!background._components.some(r=>objects[r.__id__].__type__==='cc.Sprite')); const overview=objects[controller.pages[0].__id__]; const children=overview._children.map(r=>objects[r.__id__]); const panel=children.find(o=>o._name==='PanelTop');assert.equal(panel._contentSize.width,902);assert.equal(panel._contentSize.height,1366); const clock=children.find(o=>o._name==='ClockBackground');assert.equal(clock._contentSize.width,284);assert.equal(clock._contentSize.height,56); assert(clock._children.some(r=>objects[r.__id__]._name==='ClockIcon')); const prize=objects[controller.overviewPool.__id__];assert.equal(prize.digits.length,10);assert.equal(prize.sourceArt,true);assert.equal(prize.glyphMaterial,null);assert(prize.coin);assert(prize.content); }); test('matching owns full-screen artwork, dynamic prize and three perspective slots above the cloud',()=>{ const a=JSON.parse(fs.readFileSync('assets/cloud_rise/prefab/CloudRise.prefab','utf8')); const p=a.find(o=>o.pages);const page=a[p.pages[1].__id__]; assert.equal(page._name,'Matching');assert.notEqual(p.pages[1].__id__,p.pages[2].__id__); const names=page._children.map(r=>a[r.__id__]._name); for(const name of ['Background','Title','Moon','Branch','PrizeCloud','Chest','MatchingReward','PromptBackground','AvatarCloud'])assert(names.includes(name),name); assert.equal(a[p.matchingBackground.__id__]._contentSize.height,2200); assert.equal(p.matchingSlots.length,3); const [left,right,front]=p.matchingSlots.map(r=>a[r.__id__]); assert(front._trs.array[7]>left._trs.array[7]);assert(front._trs.array[1]{ const a=JSON.parse(fs.readFileSync('assets/cloud_rise/prefab/CloudRise.prefab','utf8')); const p=a.find(o=>o.pages),page=a[p.pages[2].__id__]; assert.equal(a[p.progressBackground.__id__]._parent.__id__,p.pages[2].__id__); assert.equal(page._children[0].__id__,p.progressBackground.__id__); assert.equal(a[p.progressPool.__id__].digits.length,10); const stepMeta=JSON.parse(fs.readFileSync('assets/cloud_rise/images/progress_step.png.meta','utf8')); const stepFrame=stepMeta.subMetas.progress_step.uuid; p.tracks.forEach((ref,i)=>{ const track=a[ref.__id__];assert.equal(track._children.length,[5,7,9][i]); let previousY=-Infinity; for(const child of track._children){ const step=a[child.__id__];assert(step._trs.array[1]>previousY);previousY=step._trs.array[1]; assert(step._components.some(c=>a[c.__id__]._spriteFrame?.__uuid__===stepFrame)); assert(!step._children.some(c=>a[c.__id__]._name==='Check'),'avatars show position instead of checkmarks'); assert.equal(step._opacity,255,'upcoming clouds remain fully visible'); } }); assert(a[p.buttons[2].__id__]._components.some(c=>a[c.__id__].__type__==='cc.Button')); const cloud=page._children.map(r=>a[r.__id__]).find(n=>n._name==='StartingCloud'); assert(cloud);assert.equal(cloud._children.length,0); assert(!cloud._components.some(r=>a[r.__id__].__type__==='cc.Button'),'starting cloud is decorative only'); assert(!('playAction' in p)); }); test('legacy disabled flag cannot prevent eligible bundle loading',async()=>{ const data=state();data.period.enabled=false;const f=fixture(data);await f.bridge.sync();assert.equal(f.calls.bundle,1); }); test('overview uses the supplied description art and retains no terminal-state description',()=>{ const objects=JSON.parse(fs.readFileSync('assets/cloud_rise/prefab/CloudRise.prefab')); const panel=objects.find(o=>o.overviewDescriptionImage); assert(panel);const node=objects[panel.overviewDescriptionImage.__id__]; assert.equal(node._name,'DescriptionImage');assert.equal(node._active,true); assert(node._components.some(ref=>objects[ref.__id__].__type__==='cc.Sprite')); assert.deepEqual(panel.overviewCopy.slice(2),['','','','']); assert.equal(panel.overviewCopy[0],panel.overviewCopy[1]); });