214 lines
14 KiB
JavaScript
214 lines
14 KiB
JavaScript
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__)<root._children.findIndex(r=>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]<right._trs.array[1]);
|
|
assert.equal(a[p.matchingReward.__id__].digits.length,10);
|
|
assert.equal(a[p.progressRoster.__id__]._parent.__id__,p.pages[2].__id__);
|
|
});
|
|
|
|
test('progress owns the full-screen cloud route, image reward and authored 5/7/9 stage clouds',()=>{
|
|
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]);
|
|
});
|