MatchMaster/tools/tests/mini-program-benefits-client.test.cjs
2026-09-18 11:49:52 +08:00

484 lines
32 KiB
JavaScript

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};
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);
});