const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const vm = require('node:vm'); const ts = require(process.env.TYPESCRIPT_PATH || 'typescript'); const root = path.resolve(__dirname, '../..'); const bridgeFile = 'assets/Script/module/MiniProgramBenefitsBridge.ts'; const panelFile = 'assets/mini_program_benefits/script/MiniProgramBenefitsPanel.ts'; const tick = () => new Promise(resolve => setImmediate(resolve)); function deferred() { let resolve, reject; const promise = new Promise((a,b) => { resolve=a; reject=b; }); return {promise,resolve,reject}; } function environment() { const destroyed = [], afterDraw = [], assetLoads = [], bundleLoads = [], releases = []; const classes = {}; class Component { schedule() {} unscheduleAllCallbacks() {} } class Color { fromHEX(value) { this.hex = value; return this; } } Color.WHITE = new Color().fromHEX('#FFFFFF'); class Graphics { roundRect() {} fill() {} circle() {} } class Label { } Label.HorizontalAlign = {CENTER:1}; Label.VerticalAlign = {CENTER:1}; Label.Overflow = {SHRINK:2}; class Sprite { } Sprite.SizeMode = {CUSTOM:0}; class Node { constructor(name) { this.name = name; this.children=[]; this.components=[]; this.events={}; this.active=true; this.valid=true; this.x=this.y=0; this.scale=1; this.width=720; this.height=1280; } set parent(node) { this._parent=node; if(node) node.children.push(this); } get parent() { return this._parent; } setPosition(x,y) { this.x=x; this.y=y; } setContentSize(w,h) { if(typeof w === 'object') {this.width=w.width;this.height=w.height;} else {this.width=w;this.height=h;} } getContentSize() { return {width:this.width,height:this.height}; } addComponent(type) { const C = typeof type === 'string' ? classes[type] : type; const c=new C(); c.node=this; this.components.push(c); return c; } getComponent(type) { const C=typeof type==='string'?classes[type]:type;return C?this.components.find(c=>c instanceof C):null; } getChildByName(name) { return this.children.find(node=>node.name===name && node.valid); } on(name, callback) { this.events[name]=callback; } off(name, callback) { if(this.events[name]===callback)delete this.events[name]; } destroy() { if(this.pending) return; this.pending=true; destroyed.push(this); } } Node.EventType = {TOUCH_END:'touchend'}; const bundle = { getInfoWithPath:()=>false, load(name,type,callback) { assetLoads.push({name,callback}); }, releaseAll() { releases.push('assets'); } }; const info = {uid:'player',token:'token',level:1,freezeAmount:5,hammerAmount:6,magicAmount:7}; const storage = {}; const cc = {Node,Component,Color,Graphics,Label,Sprite,Prefab:class {},SpriteFrame:class {},BlockInputEvents:class {}, tween:target=>({to(){return this},delay(){return this},call(){return this},start(){return this}}),Tween:{stopAllByTarget(){}}, _decorator:{property:()=>()=>{},ccclass:value=>{if(typeof value==="function"){classes[value.name]=value;return value;}return C=>{classes[value]=C;return C;};}}, color:(r,g,b)=>({r,g,b}),sys:{platform:'wechat',WECHAT_GAME:'wechat',localStorage:{getItem:key=>storage[key]||null,setItem:(key,value)=>{storage[key]=value;}}}, js:{getClassByName:name=>classes[name]}, view:{getVisibleSize:()=>({width:720,height:1280})}, find(route,node) { if(route==='Canvas') return null; return route.split('/').reduce((n,key)=>n&&n.getChildByName(key),node); }, isValid:(node,strict)=>!!node && node.valid && (!strict || !node.pending), Director:{EVENT_AFTER_DRAW:'draw'},director:{once(event,cb) {afterDraw.push(cb);}}, assetManager:{getBundle:()=>null,loadBundle(name,cb) {bundleLoads.push(cb);},removeBundle() {releases.push('bundle');}}, fx:{GameConfig:{GM_INFO:info},StorageMessage:{setStorage:(key,value)=>{storage[key]=value;}}}, }; const wx = {getSystemInfoSync:()=>({platform:'android',version:'test',SDKVersion:'test'}),showToast() {},getAccountInfoSync:()=>({miniProgram:{envVersion:'release'}})}; const testEnvironment = {allowed:false,version:'体验版',baseUrl:'https://test.example'}; function load(file) { const output = ts.transpileModule(fs.readFileSync(path.join(root,file),'utf8'),{compilerOptions:{target:ts.ScriptTarget.ES2017,module:ts.ModuleKind.CommonJS,experimentalDecorators:true}}).outputText; const module={exports:{}}; vm.runInNewContext(output,{module,exports:module.exports,require:name=>{ if(name==='../seven_day_gift/SevenDayGiftBootstrapApi')return {default:{getTestEnvironment:()=>testEnvironment}}; if(name==='../LoadingCatAnimation')return {default:{play:node=>{node.active=true;return true},stop:node=>{node.active=false}}}; if(name==='./MiniProgramBenefitsReward')return {default:class {request(){throw Error('Unexpected network adapter call in UI test');}}}; if(name.endsWith('/MiniProgramBenefitsBridge'))return {default:Bridge}; if(name.endsWith('/HomeButtonVibration'))return {vibrateHomeButton() {}}; throw new Error('Unexpected dependency: '+name); },cc,wx,window:{wx},console,setTimeout,clearTimeout,Promise,Set},{filename:file}); return module.exports.default; } const Bridge=load(bridgeFile), bridge=Bridge.instance; bridge.onLogin({_id:'player'}); const Panel=load(panelFile); function instantiatePrefab(name='BenefitsWindow') { const a=JSON.parse(fs.readFileSync(path.join(root,'assets/mini_program_benefits/prefab',name+'.prefab'),'utf8')); const nodes={};a.forEach((v,i)=>{if(v.__type__==='cc.Node'){const n=nodes[i]=new Node(v._name);n.setContentSize(v._contentSize);n.setPosition(v._trs.array[0],v._trs.array[1]);n.scale=v._trs.array[7];n.active=v._active;n.opacity=v._opacity;}}); for(const [i,n] of Object.entries(nodes)){const v=a[i];if(v._parent)n.parent=nodes[v._parent.__id__];for(const r of v._components){const c=a[r.__id__];if(c.__type__==='cc.Label')n.addComponent(Label).string=c._string;}} if(name==='BenefitsWindow'){const p=nodes[1].addComponent(Panel);p.mainPrefab={prefabName:'BenefitsMain'};p.favoritePrefab={prefabName:'BenefitsFavoriteGuide'};p.desktopPrefab={prefabName:'BenefitsDesktopGuide'};p.rewardPrefab={prefabName:'BenefitsReward'};} return nodes[1]; } cc.instantiate=asset=>instantiatePrefab(asset&&asset.prefabName||'BenefitsWindow'); function draw() { while(destroyed.length) { const node=destroyed.shift(); node.valid=false; node.children.forEach(child=>child.destroy()); node.components.forEach(c=>c.onDestroy&&c.onDestroy()); if(node.parent) node.parent.children=node.parent.children.filter(child=>child!==node); } afterDraw.splice(0).forEach(callback=>callback()); } function attach() { const home=new Node('Canvas'); const load=new Node('Load');load.parent=home; const top=new Node('Top');top.parent=load; const shop=new Node('shop');shop.parent=top;shop.setPosition(-280,250); const entry=new Node('chengxu');entry.parent=top;entry.setPosition(-280,30);entry.setContentSize(178,165); bridge.attach(home);return home; } return {bridge,Panel,Node,info,storage,wx,cc,attach,draw,bundle,bundleLoads,assetLoads,releases,classes,load}; } const frame = {getOriginalSize:()=>({width:100,height:100}),getRect:()=>({width:100,height:100})}; test('runtime test eligibility preserves server claims, receipts and other accounts',()=>{ const e=environment(),b=e.bridge; b.getTestEnvironment=()=>({allowed:true}); b.onLogin({_id:'player',miniProgramWelfare:{favorite:true,desktop:false}}); b.state.favorite_entry.receiptId='keep';b.state.favorite_entry.deliveryPending=true; e.storage['mini_program_benefits_eligibility_v1:other']='untouched'; b.setTestEligibility('desktop_entry');assert.ok(b.state.desktop_entry.eligibleAt); assert.equal(b.state.favorite_entry.claimedAt,1); b.setTestEligibility();assert.equal(b.state.desktop_entry.eligibleAt,0); assert.equal(b.state.favorite_entry.receiptId,'keep');assert.equal(b.state.favorite_entry.deliveryPending,true); assert.equal(e.info.miniProgramWelfare.favorite,true); assert.equal(e.storage['mini_program_benefits_eligibility_v1:other'],'untouched'); }); test('runtime test changes reject production, missing login, active sessions and failed saves',()=>{ const e=environment(),b=e.bridge; assert.throws(()=>b.setTestEligibility('desktop_entry'),/测试服/); b.getTestEnvironment=()=>({allowed:true});b.uid='';assert.throws(()=>b.setTestEligibility(),/登录/); b.uid='player';b.session={};assert.throws(()=>b.setTestEligibility(),/关闭/);b.session=null; assert.throws(()=>b.setTestEligibility('invalid'),/未知/); e.cc.sys.localStorage.setItem=()=>{throw Error('disk full');}; assert.throws(()=>b.setTestEligibility('desktop_entry'),/disk full/); assert.equal(b.state.desktop_entry.eligibleAt,0); }); test('claim cache reset preserves balances, eligibility, server flags and other accounts',()=>{ const e=environment(),b=e.bridge; e.cc.fx.StorageMessage.getStorage=key=>e.storage[key]; b.getTestEnvironment=()=>({allowed:true}); b.onLogin({_id:'player',miniProgramWelfare:{favorite:true,desktop:false}}); b.state.favorite_entry={eligibleAt:123,claimedAt:456,receiptId:'old',deliveryPending:true}; e.storage.prop={freezeAmount:9,hammerAmount:8,magicAmount:7,timestamp:12,welfareCredits:{'welfare:player:favorite':true,'welfare:player:desktop':true,'welfare:other:favorite':true}}; b.clearTestClaimCache(); assert.equal(e.storage.prop.freezeAmount,9);assert.equal(e.storage.prop.timestamp,12); assert.deepEqual(Object.keys(e.storage.prop.welfareCredits),['welfare:other:favorite']); assert.equal(b.state.favorite_entry.eligibleAt,123);assert.equal(b.state.favorite_entry.claimedAt,1); assert.equal(b.state.favorite_entry.receiptId,undefined);assert.equal(b.state.favorite_entry.deliveryPending,undefined); assert.equal(e.info.miniProgramWelfare.favorite,true); }); test('claim cache reset rejects production, missing login, sessions and outstanding requests',()=>{ const e=environment(),b=e.bridge; assert.throws(()=>b.clearTestClaimCache(),/测试服/); b.getTestEnvironment=()=>({allowed:true});b.uid='';assert.throws(()=>b.clearTestClaimCache(),/登录/); b.uid='player';b.session={};assert.throws(()=>b.clearTestClaimCache(),/关闭/);b.session=null; b.rewardRequests=1;assert.throws(()=>b.clearTestClaimCache(),/请求完成/);b.rewardRequests=0; e.cc.fx.StorageMessage.getStorage=()=>({welfareCredits:{'welfare:player:favorite':true}}); e.cc.fx.StorageMessage.setStorage=()=>{throw Error('disk full')};b.state.favorite_entry.receiptId='keep'; assert.throws(()=>b.clearTestClaimCache(),/disk full/);assert.equal(b.state.favorite_entry.receiptId,'keep'); }); test('runtime test bundle mounts only on isolated test server and ignores detached load',()=>{ const e=environment(),b=e.bridge;b.getTestEnvironment=()=>({allowed:true}); const home=e.attach();assert.equal(e.bundleLoads.length,0);b.mountTestPanel(home);assert.equal(e.bundleLoads.length,1);b.detach(); e.bundleLoads[0](null,e.bundle);assert.equal(e.assetLoads.length,0);assert.equal(b.testNode,null); }); test('close animation waits before closing and repeated taps do not start another close',()=>{ const e=environment(),panel=new e.Panel();panel.node=new e.Node('panel'); panel.motion=new e.Node('motion');panel.mask=new e.Node('mask'); let closed=0;const completions=[];panel.host={close:()=>closed++}; e.cc.tween=()=>({to(){return this},call(cb){completions.push(cb);return this},start(){return this}}); panel.closeWithAnimation();panel.closeWithAnimation(); assert.equal(closed,0);assert.equal(completions.length,1);assert.equal(panel.closing,true); completions[0]();assert.equal(closed,1); }); test('entry loader appears immediately and disappears after successful loading without touching the shared template',async()=>{ const e=environment(),home=e.attach();e.cc.macro={MAX_ZINDEX:32767}; const template=new e.Node('Loading');template.parent=home;template.active=false; const instantiate=e.cc.instantiate;e.cc.instantiate=asset=>asset===template?new e.Node('Loading'):instantiate(asset); const pending=e.bridge.open();const loading=home.getChildByName('BenefitsLoading'); assert.ok(loading&&loading.active);assert.equal(template.active,false); e.bundleLoads[0](null,e.bundle);await tick();e.assetLoads.forEach(item=>item.callback(null,frame));await pending; assert.equal(loading.active,false);assert.equal(template.active,false); }); test('entry loader closes on cancellation or bundle failure',async()=>{ for(const cancel of [true,false]){ const e=environment(),home=e.attach();e.cc.macro={MAX_ZINDEX:32767}; const template=new e.Node('Loading');template.parent=home;e.cc.instantiate=()=>new e.Node('Loading'); const pending=e.bridge.open(),loading=home.getChildByName('BenefitsLoading'); if(cancel){e.bridge.close();assert.equal(loading.active,false);e.bundleLoads[0](null,e.bundle)} else e.bundleLoads[0](new Error('download failed')); await pending;assert.equal(loading.active,false); } }); function receiptPanel(e) { const panel=new e.Panel();panel.node=new e.Node('panel');panel.render=()=>{}; const state={favorite_entry:{eligibleAt:1}};let shown=0,acks=0; const host={getState:()=>state,refresh:async()=>{},claim:async task=>{state[task]={claimedAt:1,receiptId:task,acknowledged:false}}, showReward:async count=>{shown+=count},acknowledge:async task=>{acks++;state[task].acknowledged=true}}; panel.showRewardAnimation=count=>host.showReward(count);panel.host=host;return {panel,host,state,shown:()=>shown,acks:()=>acks}; } test('claim plays the bundle reward animation and confirms after presentation',async()=>{ const e=environment(),p=receiptPanel(e); await p.panel.claim('favorite_entry'); assert.equal(p.panel.page,'list');assert.equal(p.shown(),1);assert.equal(p.acks(),1); assert.equal(p.state.favorite_entry.acknowledged,true);assert.equal(p.panel.renderReward,undefined); }); test('animation load failure leaves the receipt pending; refresh retries without a second claim',async()=>{ const e=environment(),p=receiptPanel(e);p.host.showReward=async()=>{throw Error('animation load failed')}; await p.panel.claim('favorite_entry');assert.equal(p.acks(),0);assert.equal(p.panel.page,'list'); assert.match(p.panel.message,/animation load failed/); let shown=0;p.host.showReward=async()=>{shown++};await p.panel.refresh(); assert.equal(shown,1);assert.equal(p.acks(),1); }); test('ack retry does not replay an already shown animation in the same panel session',async()=>{ const e=environment(),p=receiptPanel(e);const ack=p.host.acknowledge; p.host.acknowledge=async()=>{throw Error('save failed')};await p.panel.claim('favorite_entry'); assert.equal(p.shown(),1);p.host.acknowledge=ack;await p.panel.refresh(); assert.equal(p.shown(),1);assert.equal(p.acks(),1); }); test('multiple pending receipts are shown together rather than replacing one another',async()=>{ const e=environment(),p=receiptPanel(e);p.state.favorite_entry={claimedAt:1,receiptId:'a'}; p.state.desktop_entry={claimedAt:1,receiptId:'b'};const calls=[];p.host.showReward=async count=>calls.push(count); await p.panel.refresh();assert.deepEqual(calls,[2]);assert.equal(p.acks(),2); }); test('scene-owned chengxu keeps its sprite and position across detach and reattach',()=>{ const e=environment(),home=e.attach(),entry=e.bridge.entryNode; e.bridge.detach();e.draw();assert.equal(entry.valid,true);assert.equal(entry.active,false); const sprite=entry.addComponent(e.cc.Sprite);sprite.spriteFrame=frame; entry.setPosition(123,-456);e.cc.resources={load(){throw Error('Scene artwork must not reload')}}; e.bridge.attach(home);e.bridge.attach(home); assert.equal(e.bridge.entryNode,entry);assert.equal(entry.x,123);assert.equal(entry.y,-456); assert.equal(entry.children.filter(n=>n.valid&&!n.pending&&n.name==='red').length,1); let opens=0;e.bridge.open=()=>{opens++};entry.events.touchend();assert.equal(opens,1); e.bridge.detach();e.draw();assert.equal(entry.valid,true);assert.equal(sprite.spriteFrame,frame); assert.equal(entry.events.touchend,undefined); }); test('entry artwork holds one reference and releases it after the entry is destroyed',()=>{ const e=environment(),loads=[]; e.cc.resources={load:(name,type,callback)=>loads.push({name,callback})}; e.attach();assert.equal(e.bundleLoads.length,0); assert.equal(loads[0].name,'texture/mini-program-benefits-entry'); const art={refs:0,addRef(){this.refs++;return this},decRef(){this.refs--;return this}}; loads[0].callback(null,art);assert.equal(art.refs,1); assert.equal(e.bridge.entryVisual.getComponent(e.cc.Sprite).spriteFrame,art); e.bridge.detach();assert.equal(art.refs,1);e.draw();assert.equal(art.refs,0); }); test('late entry artwork after leaving HomeScene is released without mounting a sprite',()=>{ const e=environment(),loads=[]; e.cc.resources={load:(name,type,callback)=>loads.push(callback)}; e.attach();e.bridge.detach();e.draw(); const art={refs:0,addRef(){this.refs++;return this},decRef(){this.refs--;return this}}; loads[0](null,art);assert.equal(art.refs,0);assert.equal(e.bridge.entryFrame,null); }); test('never loads UI bundle at login/attach; first-level gate and red dot follow current state',()=>{ const e=environment(); e.info.level=0;e.attach();assert.equal(e.bundleLoads.length,0);assert.equal(e.bridge.entryNode.active,false); e.info.level=1;e.bridge.state={favorite_entry:{eligibleAt:10}};e.bridge.changed(); assert.equal(e.bridge.entryNode.active,true);assert.equal(e.bridge.entryRed.active,true); e.bridge.state={favorite_entry:{claimedAt:10,acknowledged:true},desktop_entry:{claimedAt:20,acknowledged:true}};e.bridge.changed(); assert.equal(e.bridge.entryNode.active,false); }); test('closing during bundle download releases the late bundle without starting texture loads',async()=>{ const e=environment();e.attach();const opening=e.bridge.open();e.bridge.close();e.bundleLoads[0](null,e.bundle);await opening; assert.equal(e.assetLoads.length,0);assert.deepEqual(e.releases,['assets','bundle']);assert.equal(e.bridge.session,null); }); test('cancelled prefab load releases before reopening; prefab failure permits retry',async()=>{ const e=environment();e.attach();const first=e.bridge.open();e.bundleLoads[0](null,e.bundle);await tick(); e.bridge.close();const second=e.bridge.open();e.assetLoads[0].callback(new Error('prefab failed'));await first;await tick(); assert.equal(e.bundleLoads.length,2);assert.deepEqual(e.releases,['assets','bundle']); e.bridge.close();e.bundleLoads[1](null,e.bundle);await second;assert.equal(e.bridge.session,null); }); test('mounted panel destroys listeners/nodes before textures; HomeScene exit unloads bundle',async()=>{ const e=environment();e.bridge.refresh=()=>Promise.resolve();const home=e.attach(); const opening=e.bridge.open();e.bundleLoads[0](null,e.bundle);await tick();e.assetLoads.forEach(item=>item.callback(null,frame));await opening; assert.equal(e.bridge.listeners.length,1);assert.equal(e.releases.length,0); e.bridge.detach(home);assert.equal(e.releases.length,0);e.draw();await tick(); assert.equal(e.bridge.listeners.length,0);assert.deepEqual(e.releases,['assets','bundle']);assert.equal(e.bridge.session,null); }); test('entry observations are bounded and never qualify a task or store query values',()=>{ const e=environment(); for(let i=0;i<35;i++)e.bridge.capture({scene:1089,query:{secret:'do-not-store'}},'show'); assert.equal(e.bridge.getDiagnostics().length,24); assert.equal(JSON.stringify(e.bridge.getDiagnostics()).includes('do-not-store'),false); assert.ok(Object.values(e.bridge.getState()).every(item=>!item.eligibleAt&&!item.claimedAt)); assert.equal(Object.keys(e.storage).length,0); }); test('non-WeChat builds do not create an entry, load a bundle or capture eligibility',()=>{ const e=environment();e.cc.sys.platform='douyin';e.attach();e.bridge.capture({scene:90001},'show'); assert.equal(e.bridge.entryNode,null);assert.equal(e.bundleLoads.length,0);assert.equal(e.bridge.getDiagnostics().length,0); }); test('chengxu follows xinshou in HomeScene and preserves the scene position without overlapping other entries',()=>{ const e=environment(); const scene=JSON.parse(fs.readFileSync(path.join(root,'assets/Scene/HomeScene.fire'),'utf8')); const source=scene.find(n=>n.__type__==='cc.Node'&&n._name==='Top'&&n._children.length>10); const home=new e.Node('Canvas');home.setContentSize(1080,1920); const load=new e.Node('Load');load.parent=home; const top=new e.Node('Top');top.parent=load;top.setPosition(source._trs.array[0],source._trs.array[1]); for(const ref of source._children){const data=scene[ref.__id__];const n=new e.Node(data._name);n.parent=top;n.setContentSize(data._contentSize);n.setPosition(data._trs.array[0],data._trs.array[1]);} const configured=top.getChildByName('chengxu');const x=configured.x,y=configured.y; e.bridge.attach(home);const entry=e.bridge.entryNode; assert.equal(entry,configured);assert.equal(entry.x,x);assert.equal(entry.y,y); assert.equal(top.children[top.children.indexOf(entry)-1].name,'xinshou'); assert.equal(top.children.some(n=>n.name==='miniProgramBenefits'),false); for(const n of top.children.filter(n=>n!==entry&&Math.abs(n.x-entry.x)<70))assert.ok(Math.abs(n.y-entry.y)>(n.height+entry.height)/2, n.name); assert.ok(top.y+entry.y-entry.height/2>=-960); assert.ok(entry.x-entry.width/2>=-540); }); test('bundle icons own unique UUIDs and no feature bundle is statically pulled into main',()=>{ const meta=JSON.parse(fs.readFileSync(path.join(root,'assets/mini_program_benefits.meta'),'utf8')); assert.equal(meta.isBundle,true);assert.equal(meta.compressionType.wechatgame,'subpackage'); const game=JSON.parse(fs.readFileSync(path.join(root,'build-templates/wechatgame/game.json'),'utf8')); assert.deepEqual(game.subpackages.filter(item=>item.name===meta.bundleName),[{name:meta.bundleName,root:'subpackages/'+meta.bundleName}]); for(const [name,original] of [['freeze','dongjie'],['hammer','chuizi'],['magic','mofabang']]){ const a=JSON.parse(fs.readFileSync(path.join(root,'assets/mini_program_benefits/icons',name+'.png.meta'),'utf8')); const b=JSON.parse(fs.readFileSync(path.join(root,'assets/jungle_treasure/img',original+'.png.meta'),'utf8')); assert.notEqual(a.uuid,b.uuid);assert.notEqual(Object.values(a.subMetas)[0].uuid,Object.values(b.subMetas)[0].uuid); } const source=fs.readFileSync(path.join(root,bridgeFile),'utf8'); assert.equal(/^import .*mini_program_benefits/m.test(source),false); assert.equal(/^import .*jungle_treasure|^import .*shop\//m.test(fs.readFileSync(path.join(root,panelFile),'utf8')),false); }); test('qualification uses the same latest entry as the panel instead of stale launch options',()=>{ const e=environment();e.wx.getEnterOptionsSync=()=>({scene:1104}); e.bridge.capture({scene:1011},'cold');assert.ok(e.bridge.getState().favorite_entry.eligibleAt); assert.equal(e.bridge.getDiagnostics()[0].scene,1104); const other=environment();other.wx.getEnterOptionsSync=()=>({scene:1089}); other.bridge.capture({scene:1104},'show');assert.ok(Object.values(other.bridge.getState()).every(item=>!item.eligibleAt)); const fallback=environment();fallback.wx.getEnterOptionsSync=()=>{throw Error('unsupported');}; fallback.bridge.capture({scene:1104},'show');assert.ok(fallback.bridge.getState().favorite_entry.eligibleAt); }); test('release, unknown version and missing account API never load phone test bundle',()=>{ for(const env of ['release','unknown',undefined]){ const e=environment();e.wx.getAccountInfoSync=()=>({miniProgram:{envVersion:env}});e.attach();assert.equal(e.bundleLoads.length,0); } const e=environment();e.wx.getAccountInfoSync=()=>{throw Error('unavailable');};e.attach();assert.equal(e.bundleLoads.length,0); }); test('receipt stays pending until the player dismisses the independent reward overlay',async()=>{ const e=environment(),p=receiptPanel(e),wait=deferred();p.panel.showRewardAnimation=()=>wait.promise; const claim=p.panel.claim('favorite_entry');await tick();assert.equal(p.acks(),0);assert.equal(p.panel.busy,true); wait.resolve();await claim;assert.equal(p.acks(),1);assert.equal(p.panel.busy,false); }); test('cancelled independent reward presentation leaves receipt available for retry',async()=>{ const e=environment(),p=receiptPanel(e);p.panel.showRewardAnimation=async()=>{throw Error('presentation cancelled');}; await p.panel.claim('favorite_entry');assert.equal(p.acks(),0);assert.equal(p.state.favorite_entry.acknowledged,false); }); test('benefits entry depends on its own state even while other activities are pending',()=>{ const e=environment();const home=e.attach(); home.getComponent=()=>({isBenefitsEntryLayoutReady:()=>false}); e.bridge.state={favorite_entry:{eligibleAt:1}};e.bridge.refreshEntry(); assert.equal(e.bridge.entryNode.active,true);assert.equal(e.bridge.entryRed.active,true); e.bridge.state={favorite_entry:{claimedAt:1,acknowledged:true},desktop_entry:{claimedAt:1,acknowledged:true}}; e.bridge.refreshEntry();assert.equal(e.bridge.entryNode.active,false); e.bridge.state.desktop_entry.acknowledged=false; e.bridge.refreshEntry();assert.equal(e.bridge.entryNode.active,true); }); test('real home uses a separate receipt store and never mounts the reset button',()=>{ const e=environment(); e.wx.getAccountInfoSync=()=>({miniProgram:{envVersion:'trial'}}); e.bridge.onLogin({_id:'player'}); assert.equal(e.bridge.getRewardStorageKey(),'mini_program_benefits_rewards_v1:player'); e.attach();assert.equal(e.bundleLoads.length,0); }); test('all five editable prefabs reference only bundle assets and valid local node ids',()=>{ const dir=path.join(root,'assets/mini_program_benefits'),ids=new Set(['eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432']); function metas(d){for(const item of fs.readdirSync(d,{withFileTypes:true})){const p=path.join(d,item.name);if(item.isDirectory())metas(p);else if(p.endsWith('.meta')){const m=JSON.parse(fs.readFileSync(p));ids.add(m.uuid);for(const sub of Object.values(m.subMetas||{}))ids.add(sub.uuid);}}}metas(dir); for(const name of ['BenefitsWindow','BenefitsMain','BenefitsFavoriteGuide','BenefitsDesktopGuide','BenefitsReward']){ const a=JSON.parse(fs.readFileSync(path.join(dir,'prefab',name+'.prefab'))); function visit(v){if(!v||typeof v!=='object')return;if(v.__id__!==undefined)assert.ok(a[v.__id__],name+' node reference');if(v.__uuid__)assert.ok(ids.has(v.__uuid__),name+' foreign asset '+v.__uuid__);Object.values(v).forEach(visit);}visit(a); assert.equal(a[0].__type__,'cc.Prefab');assert.ok(a.some(n=>n.__type__==='cc.Sprite')); } const panel=fs.readFileSync(path.join(root,panelFile),'utf8');assert.equal(panel.includes('new cc.Node'),false);assert.equal(panel.includes('cc.Graphics'),false); const bridge=fs.readFileSync(path.join(root,bridgeFile),'utf8');assert.ok(bridge.includes('bundle.load("prefab/BenefitsWindow", cc.Prefab')); }); test('prefab task buttons update existing nodes without rebuilding editable layout',()=>{ const e=environment();const node=e.cc.instantiate({prefabName:'BenefitsWindow'}),p=node.getComponent('MiniProgramBenefitsPanel');p.refresh=async()=>{};p.playOpenAnimation=()=>{}; let state={};const host={getUid:()=>"player",bindRewardApi(){},getRewardStorageKey:()=> 'test',getState:()=>state,subscribe:()=>()=>{},close(){}};p.init(host); const row=p.views.list.getChildByName('Task0'),view=row.getChildByName('View');assert.equal(view.active,true); state={favorite_entry:{eligibleAt:1}};p.render();assert.equal(view.active,false);assert.equal(row.getChildByName('Claim').active,true); state.favorite_entry.claimedAt=2;p.render();assert.equal(row.getChildByName('Claimed').active,true);assert.equal(p.views.list.getChildByName('Task0'),row); }); test('production login ignores old local claims and hides completed welfare after cache clear',()=>{ const e=environment(); e.storage[e.bridge.getRewardStorageKey()]=JSON.stringify({favorite_entry:{claimedAt:123},desktop_entry:{claimedAt:123}}); e.bridge.onLogin({_id:'player',miniProgramWelfare:{favorite:false,desktop:false}}); assert.equal(e.bridge.getState().favorite_entry.claimedAt,0); e.bridge.capture({scene:1023},'show');assert.ok(e.bridge.getState().desktop_entry.eligibleAt); Object.keys(e.storage).forEach(key=>delete e.storage[key]); e.bridge.onLogin({_id:'player',miniProgramWelfare:{favorite:true,desktop:true}}); const home=e.attach();e.bridge.refreshEntry(); assert.equal(e.cc.find('Load/Top/chengxu',home).active,false); assert.equal(e.bridge.getState().favorite_entry.acknowledged,true); }); test('production eligibility survives a new session and remains isolated by account',()=>{ const first=environment();first.bridge.uid="";first.bridge.pendingEntries={}; first.bridge.capture({scene:1104},'cold'); first.bridge.onLogin({_id:'alice',miniProgramWelfare:{favorite:false,desktop:false}}); first.bridge.capture({scene:1023},'show'); const next=environment();Object.assign(next.storage,first.storage); next.bridge.capture({scene:1001},'cold'); next.bridge.onLogin({_id:'alice',miniProgramWelfare:{favorite:false,desktop:false}}); assert.ok(next.bridge.getState().favorite_entry.eligibleAt); assert.ok(next.bridge.getState().desktop_entry.eligibleAt); assert.equal(next.bridge.getState().favorite_entry.claimedAt,0); next.bridge.onLogin({_id:'bob',miniProgramWelfare:{favorite:false,desktop:false}}); assert.equal(next.bridge.getState().favorite_entry.eligibleAt,0); next.bridge.onLogin({_id:'alice',miniProgramWelfare:{favorite:true,desktop:true}}); assert.ok(next.bridge.getState().favorite_entry.claimedAt); assert.ok(next.bridge.getState().desktop_entry.claimedAt); const home=next.attach();assert.equal(next.cc.find('Load/Top/chengxu',home).active,false); }); test('production eligibility ignores malformed cache and reports failed persistence',()=>{ const e=environment(); e.storage['mini_program_benefits_eligibility_v1:alice']='{"favorite_entry":"yes","desktop_entry":-1}'; e.bridge.onLogin({_id:'alice',miniProgramWelfare:{favorite:false,desktop:false}}); assert.equal(e.bridge.getState().favorite_entry.eligibleAt,0); e.cc.sys.localStorage.setItem=()=>{throw Error('full')}; e.bridge.capture({scene:1104},'show'); assert.equal(e.bridge.getState().favorite_entry.eligibleAt,0); }); test('activity close keeps the shared test bundle alive until leaving home',async()=>{ const e=environment(),b=e.bridge;b.getTestEnvironment=()=>({allowed:true}); e.classes.BenefitsRuntimeTest=class {initialize(host){this.host=host;}}; e.cc.instantiate=()=>new e.Node('BenefitsRuntimeTest'); const home=e.attach();assert.equal(e.bundleLoads.length,0);b.mountTestPanel(home);e.bundleLoads[0](null,e.bundle); assert.equal(e.assetLoads[0].name,'test/BenefitsRuntimeTest');e.assetLoads[0].callback(null,{}); assert.ok(b.testNode);assert.equal(b.bundleUsers,1); const opening=b.open();b.close();e.bundleLoads[1](null,e.bundle);await opening; assert.equal(b.bundleUsers,1);assert.deepEqual(e.releases,[]); b.detach();e.draw();assert.equal(b.bundleUsers,0);assert.deepEqual(e.releases,['assets','bundle']); }); test('both real entry qualifications captured before login survive and preserve server claims',()=>{ const e=environment(),b=e.bridge;b.uid='';e.info.level=0; b.capture({scene:1104},'cold');b.capture({scene:1023},'show');assert.equal(Object.keys(e.storage).length,0); b.onLogin({_id:'player',miniProgramWelfare:{favorite:true,desktop:false}});e.attach(); assert.ok(b.state.favorite_entry.eligibleAt);assert.ok(b.state.desktop_entry.eligibleAt); assert.equal(b.state.favorite_entry.claimedAt,1);assert.equal(b.entryNode.active,false); e.info.level=1;b.refreshEntry();assert.equal(b.entryNode.active,true);assert.equal(b.entryRed.active,true); b.state.favorite_entry.receiptId='keep';b.capture({scene:1104},'show');assert.equal(b.state.favorite_entry.receiptId,'keep'); }); test('account switch rejects delayed bridge responses without replacing new account state',async()=>{ const e=environment(),b=e.bridge,pending=deferred();b.session={api:{request:()=>pending.promise}}; const request=b.refresh();b.onLogin({_id:'other',miniProgramWelfare:{favorite:true,desktop:true}}); pending.resolve({state:{favorite_entry:{claimedAt:0}}});await assert.rejects(request,/账号/); assert.equal(b.getUid(),'other');assert.equal(b.state.favorite_entry.claimedAt,1);assert.equal(b.state.desktop_entry.claimedAt,1); });