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 = [], storage = new Map(); let bridge, failBundle = false, hold = false; const runtime = { subscribe() {}, sync: async () => { calls.runtime++; return true; }, data: () => data, now: () => Date.now(), pending:()=>false, elimination:()=>null, unseenResult: () => false, error: '' }; const cc = { warn() {}, Prefab: class {}, fx: { GameConfig: { GM_INFO: info }, StorageMessage:{getStorage:key=>storage.get(key)} }, assetManager: { loadBundle(name, done) { calls.bundle++; assert(['cloud_rise_home','art','matching'].includes(name)); if (failBundle) return done(new Error('offline')); if (name === 'cloud_rise_home') 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/index"); 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,storage,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(); await f.bridge.pageFragment('matching'); assert.deepEqual(f.calls,{status:1,bundle:3,prefab:2,runtime:1,retained:2}); }); 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('login recovers a challenge from another device without any local journal',async()=>{ const data=state();data.available=false;data.period=null; data.run={periodId:'old',status:'playing',expiresAt:Date.now()+10000,stages:[]}; const f=fixture(data);assert.equal(f.bridge.needsPersonalSync(),false); f.bridge.afterLogin();await f.bridge.sync(); assert.equal(f.bridge.run(),data.run);assert.equal(f.bridge.needsPersonalSync(),true); assert.equal(f.bridge.homeEntryVisible(null,true,Date.now()),true); }); test('cold home retries local queued operations, unfinished runs and unsaved awards only',()=>{ const f=fixture(null); for(const saved of [ {queue:[{action:'finish'}]}, {attempt:{runId:'r'}}, {eliminations:[{key:'r'}]}, {snapshot:{run:{status:'playing',expiresAt:0,stages:[]}}}, {snapshot:{run:{status:'completed',stages:[{status:'won',rewardSaved:false}]}}} ]) { f.storage.set('cloudRise:1001',saved);assert.equal(f.bridge.needsPersonalSync(),true); } f.storage.set('cloudRise:1001',{snapshot:{run:{status:'failed',stages:[]}},queue:[]}); assert.equal(f.bridge.needsPersonalSync(),false); f.info.userId=2002;assert.equal(f.bridge.needsPersonalSync(),false,'another account cannot inherit pending work'); }); test('public entry preloads matching assets once without requesting private status or enrolling',async()=>{ const f=fixture(state()), period={periodId:'p1',unlockLevel:100};f.info.level=99; f.bridge.preloadPublicEntry(period);assert.equal(f.calls.bundle,0); f.info.level=100;f.bridge.preloadPublicEntry(period);f.bridge.preloadPublicEntry(period); for(let i=0;i<20;i++)await Promise.resolve(); assert.deepEqual(f.calls,{status:0,bundle:3,prefab:2,runtime:0,retained:2}); }); 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('all reward displays reuse five authored sprite nodes when the amount changes', () => { const objects = JSON.parse(fs.readFileSync('assets/cloud_rise_home/prefab/CloudRise.prefab')); const renderers = objects.filter(o => o.digits && o.content && o.coin); assert.equal(renderers.length, 1); 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_home/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'); } } });