const test=require('node:test'),assert=require('node:assert/strict'),fs=require('node:fs'),vm=require('node:vm'),ts=require('typescript'); const left=['shop','yicon','redeemBtn','passBtn','jungle','xinshou','chengxu','cloudRise']; const right=['rank','gacha','day','hammer']; function fixture(){ const exports={}; vm.runInNewContext(ts.transpileModule(fs.readFileSync('assets/Script/HomeActivityEntryLayout.ts','utf8'),{ compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2017}}).outputText,{exports}); const a=JSON.parse(fs.readFileSync('assets/Scene/HomeScene.fire')),nodes=new Map();let writes=0; for(let i=0;in.name===name);},setPosition(x,y){this.x=x;this.y=y;writes++;}}); } for(const [i,n]of nodes){n.children=a[i]._children.map(r=>nodes.get(r.__id__));n.parent=nodes.get(a[i]._parent?.__id__);} const top=[...nodes.values()].find(n=>n.name==='Top'&&n.parent.name==='Load'); const gift=top.getChildByName('xinshou'); const frame=JSON.parse(fs.readFileSync('assets/UI/UI/start/startUI2.plist.meta')).subMetas['cloudRise.png']; const cloud={...gift,name:'cloudRise',width:frame.rawWidth,height:frame.rawHeight,children:gift.children.filter(n=>['New Sprite','time'].includes(n.name))}; top.children.push(cloud); const layout=exports.default; function box(n){const b=layout.extent(n),scale=top.parent.scaleY;return {top:n.y+b.top*scale,bottom:n.y+b.bottom*scale};} function check(){ const all=[...left,...right].map(n=>top.getChildByName(n)).filter(n=>n.active); for(let i=0;i300)continue; const a=box(p),b=box(q);assert(a.bottom>=b.top+15.99||b.bottom>=a.top+15.99,`${p.name} overlaps ${q.name}`); } const footer=top.parent.getChildByName('shezhiBtn'),bound=footer.y+footer.height*(1-footer.anchorY)*top.parent.scaleY-top.y; for(const n of all)assert(box(n).bottom>=bound+15.99,`${n.name} covers footer: ${box(n).bottom} < ${bound+16}`); } return {layout,top,check,get writes(){return writes;}}; } test('every visibility combination fits without overlap, including both columns and bottom controls',()=>{ const f=fixture(); for(const scale of [1,0.85]){ f.top.parent.scaleY=scale; for(let mask=0;mask<4096;mask++){ [...left,...right].forEach((name,i)=>f.top.getChildByName(name).active=!!(mask&(1<{ const f=fixture();[...left,...right].forEach(name=>f.top.getChildByName(name).active=name!=='redeemBtn'); f.layout.refresh(f.top);f.check(); const benefit=f.top.getChildByName('chengxu'),cloud=f.top.getChildByName('cloudRise'); assert.notDeepEqual([benefit.x,benefit.y],[cloud.x,cloud.y]); assert.equal(cloud.x,f.top.getChildByName('rank').x,'overflow uses the free right slot'); const old={x:benefit.x,y:benefit.y};benefit.active=false;f.layout.refresh(f.top);f.check(); // The countdown makes CloudRise taller than benefits; only reclaim a slot if the whole entry fits. f.top.getChildByName('xinshou').active=false;f.layout.refresh(f.top);f.check(); assert.equal(cloud.x,old.x);assert(cloud.y>old.y); const writes=f.writes;for(let i=0;i<20;i++)f.layout.refresh(f.top);assert.equal(f.writes,writes,'unchanged frames do not reposition buttons'); benefit.active=true;f.layout.refresh(f.top);f.check(); }); test('home lays out late in the frame after asynchronous entry visibility changes',()=>{ const source=fs.readFileSync('assets/Script/HomeActivitySchedule.ts','utf8');let arranged; const exports={},cc={Component:class{},_decorator:{ccclass:c=>c}}; vm.runInNewContext(ts.transpileModule(source,{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2017,experimentalDecorators:true}}).outputText, {exports,cc,require:name=>({default:name.endsWith('HomeActivityEntryLayout')?{refresh:top=>arranged=top}:{}})}); const f=fixture(),home=new exports.default();home.node={getChildByName:()=>f.top.parent};home.lateUpdate();assert.equal(arranged,f.top); assert(!fs.readFileSync('assets/Script/CloudRiseView.ts','utf8').includes('private layoutEntry()'),'CloudRise no longer writes competing positions'); assert(!fs.readFileSync('assets/Script/JiaZai.ts','utf8').includes('benefitsEntryOffset'),'benefits no longer follows a separate layout'); });