Files
MatchMaster/tools/test-weekly-quests.cjs
T

429 lines
29 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 { stripTypeScriptTypes } = require('node:module');
const root = path.resolve(__dirname, '..');
const prefab = JSON.parse(fs.readFileSync(path.join(root, 'assets/action_bundle/prefab/dailyQuests.prefab'), 'utf8'));
const source = fs.readFileSync(path.join(root, 'assets/action_bundle/script/DailyQuests.ts'), 'utf8')
.replace(/^import .*;\r?$/gm, '').replace(/^\s*@(?:ccclass|property[^\r\n]*)\r?$/gm, '')
.replace('export default class DailyQuests', 'class DailyQuests');
const trackerSource = fs.readFileSync(path.join(root, 'assets/Script/module/Task/DailyTaskProgressTracker.ts'), 'utf8')
.replace(/^export type /gm, 'type ').replace('export default class', 'class');
const code = stripTypeScriptTypes(trackerSource + '\n' + source) + '\nglobalThis.DailyQuests = DailyQuests;';
class Node {
constructor(o) {
this.name = o._name; this.active = o._active; this.width = o._contentSize.width; this.height = o._contentSize.height;
this.x = o._trs.array[0]; this.y = o._trs.array[1]; this.scaleX = o._trs.array[7]; this.scaleY = o._trs.array[8];
this.angle = o._eulerAngles.z; this.anchorX = o._anchorPoint.x; this.anchorY = o._anchorPoint.y;
this.children = []; this.components = {};
}
set scale(value) { this.scaleX=value; this.scaleY=value; }
get scale() {return this.scaleX;}
get activeInHierarchy() { return !this.destroyed && this.active && (!this.parent || this.parent.activeInHierarchy); }
on(type,handler,target,capture) { (this.listeners ||= []).push({type,handler,target,capture}); }
off(type,handler,target,capture) {this.listeners=this.listeners.filter(item=>item.handler!==handler || item.capture!==capture);}
setContentSize(width,height) {this.width=width;this.height=height;}
getBoundingBoxToWorld() {
let x=this.x,y=this.y,parent=this.parent;
while(parent){x=x*parent.scaleX+parent.x;y=y*(parent.scaleY || 1)+parent.y;parent=parent.parent;}
return {contains:point=>Math.abs(point.x-x)<=this.width*this.scaleX/2 && Math.abs(point.y-y)<=this.height*(this.scaleY||1)/2};
}
getChildByName(name) { return this.children.find(node => node.name === name); }
getComponent(type) { return this.components[type] || null; }
addChild(node) { assert.ok(node, 'addChild must receive a node'); this.children.push(node); }
setSiblingIndex() {}
setPosition(x, y) { this.x = x; this.y = y; }
}
function build(id) {
const o = prefab[id], node = new Node(o);
for (const ref of o._components) {
const component=structuredClone(prefab[ref.__id__]);
if(component.clickEvents) component.clickEvents=component.clickEvents.map(event=>event.__id__ == null ? event : structuredClone(prefab[event.__id__]));
node.components[component.__type__]=component;
}
if (node.components['cc.Layout']) node.components['cc.Layout'].updateLayout = () => {
const active=node.children.filter(child=>child.active);
node.width=active.reduce((sum,child)=>sum+child.width*child.scaleX,0)+Math.max(0,active.length-1)*(node.components['cc.Layout'].spacingX ?? node.components['cc.Layout']['_N$spacingX']);
};
node.children = o._children.map(ref => build(ref.__id__)); node.children.forEach(child=>child.parent=node); return node;
}
function clone(node) {
const copy = Object.assign(Object.create(Node.prototype), node);
copy.components = Object.fromEntries(Object.entries(node.components).map(([key, value]) => [key, { ...value }])); copy.children = node.children.map(clone); return copy;
}
function setup(stars = 15) {
const requests = [], toasts = [], tweens = [];
// Advance animation time deterministically, including the gap between loops.
// Only started tweens run; stopping one prevents any later property changes.
class Tween {
constructor(target) { this.target = target; this.steps = []; this.index = 0; this.elapsed = 0; this.cancelled = false; }
to(duration, properties, options = {}) { this.steps.push({ duration, properties, options }); return this; }
delay(duration) { this.steps.push({ duration }); return this; }
repeatForever(tween) { this.steps.push(...tween.steps); this.repeating = true; return this; }
start() { tweens.push(this); return this; }
stop() { this.cancelled = true; return this; }
advance(seconds) {
while (seconds > 1e-9 && !this.cancelled && this.index < this.steps.length) {
const step = this.steps[this.index];
this.initial ||= Object.fromEntries(Object.keys(step.properties || {}).map(key => [key, this.target[key]]));
const consumed = Math.min(seconds, step.duration - this.elapsed);
this.elapsed += consumed; seconds -= consumed;
const progress = Math.min(1, this.elapsed / step.duration);
const eased = step.options?.easing === 'sineInOut' ? (1 - Math.cos(Math.PI * progress)) / 2 : progress;
for (const [key, end] of Object.entries(step.properties || {})) this.target[key] = this.initial[key] + (end - this.initial[key]) * eased;
if (this.elapsed + 1e-9 < step.duration) return;
if (step.properties) Object.assign(this.target, step.properties);
this.index++; this.elapsed = 0; this.initial = null;
if (this.repeating && this.index === this.steps.length) this.index = 0;
}
}
static stopAllByTarget(target) { tweens.filter(tween => tween.target === target).forEach(tween => tween.stop()); }
}
const cc = { Component: class {}, Node, Tween, tween: target => new Tween(target), Label: {toString:()=> 'cc.Label',Overflow:{SHRINK:2}}, RichText: 'cc.RichText', Sprite: {toString:()=> 'cc.Sprite',Type:{SLICED:1},SizeMode:{CUSTOM:0,TRIMMED:1}},
ProgressBar: 'cc.ProgressBar', Button: 'cc.Button', Layout: 'cc.Layout', _decorator: {}, Camera:{findCamera:()=>null},
isValid: node => !!node && !node.destroyed, instantiate: clone, color: (r,g,b) => ({r,g,b}), error: message => { throw Error(message); },
fx: { GameConfig: { GM_INFO: {} } } };
cc.Node.EventType={TOUCH_START:'touchstart'};
cc.find=()=>null;
const context = { cc, Date, console, setTimeout, Utils: { getDailyQuestReward: (id, callback, scope) => requests.push([id, response => {
if (response.code === 1) cc.fx.GameConfig.GM_INFO.tasks = response.data.task;
callback(response);
}, scope]) },
MiniGameSdk: { API: { showToast: text => toasts.push(text) } } };
cc.fx.GameTool = {getDailyTaskServerTime: () => Date.now(), claimDailyQuestReward: context.Utils.getDailyQuestReward};
cc.fx.GameTool.getCachedDailyQuests = () => null;
vm.runInNewContext(code, context);
const daily = new context.DailyQuests(); daily.node = build(1);
daily.content = daily.node.getChildByName('bg').getChildByName('content'); daily.touchStart = true;
daily.rewardIcons = ['xinCoin', 'ice', 'hammer', 'magic', 'skyLine'].map(name => {
const meta = JSON.parse(fs.readFileSync(path.join(root, `assets/res/public/${name}.png.meta`), 'utf8')).subMetas[name];
return { getRect: () => ({ width: meta.width, height: meta.height }) };
});
daily.openLoad = () => {}; daily.closeLoad = () => {};
// Reward presentation phases are covered by test-daily-reward-animation.cjs.
daily.playRewardAnimation = (rewards, complete) => complete();
daily.scheduleOnce = callback => { daily.timeout = callback; };
daily.unschedule = callback => { if (daily.timeout === callback) daily.timeout = null; };
const data = { version: 2, weeklyStars: stars, dailyTasks: [], weeklyRewards: [35,70,105,140].map(target => ({
rewardId: `weekly_${target}`, target, state: stars >= target ? 1 : 0,
rewards: target === 140 ? [{type:'infinite_rocket',count:900},{type:'infinite_hourglass',count:900},{type:'coin',count:1500}]
: [{type:'hammer',count:1},{type:'infinite_health',count:900}]
})) };
cc.fx.GameConfig.GM_INFO.tasks = data;
const weekly = daily.node.getChildByName('bg').getChildByName('weeklyRewards');
const preview = weekly.getChildByName('preview');
daily.onLoad();daily.renderWeeklyRewards(data);
return {daily,data,weekly,preview,requests,toasts,cc,tweens,
advance(seconds) { tweens.forEach(tween => tween.advance(seconds)); } };
}
test('weekly chests pivot at top center without changing their resting visual bounds', () => {
const {weekly}=setup(0),sizes=[78,90,100,110];
weekly.getChildByName('tiers').children.forEach((tier,index)=>{
const chest=tier.getChildByName('chest');
assert.equal(chest.anchorX,0.5);assert.equal(chest.anchorY,1);assert.equal(chest.angle,0);
assert.equal(chest.width,sizes[index]);assert.equal(chest.height,sizes[index]);
assert.equal(chest.scaleX,1);assert.equal(chest.scaleY,1);
// Before moving the anchor, each centered icon was positioned at (0, 10).
const left=chest.x-chest.anchorX*chest.width*chest.scaleX;
const bottom=chest.y-chest.anchorY*chest.height*chest.scaleY;
assert.equal(left,-sizes[index]/2);assert.equal(bottom,10-sizes[index]/2);
assert.equal(bottom+chest.height*chest.scaleY,10+sizes[index]/2);
});
});
test('only ready chest icons swing gently left and right, with two seconds at rest between loops', () => {
const h=setup(0),tiers=h.weekly.getChildByName('tiers').children;
h.data.weeklyRewards.forEach((reward,index)=>reward.state=[0,1,2,1][index]);
h.daily.renderWeeklyRewards(h.data);
const chests=tiers.map(tier=>tier.getChildByName('chest'));
const stationary=tiers.flatMap(tier=>[tier,tier.getChildByName('target'),tier.getChildByName('claimable'),tier.getChildByName('claimed')]);
const transforms=stationary.map(node=>[node.x,node.y,node.angle,node.scaleX,node.scaleY]);
assert.deepEqual(h.tweens.map(tween=>tween.target),[chests[1],chests[3]]);
for(let loop=0;loop<3;loop++){
h.advance(0.06);assert.ok(chests[1].angle>0 && chests[1].angle<6);
h.advance(0.06);assert.equal(chests[1].angle,6);
h.advance(0.24);assert.equal(chests[1].angle,-6);
h.advance(0.12);assert.equal(chests[1].angle,0);
h.advance(1);assert.equal(chests[1].angle,0);
h.advance(0.99);assert.equal(chests[1].angle,0);
h.advance(0.01);assert.equal(chests[1].angle,0);
assert.equal(chests[0].angle,0);assert.equal(chests[2].angle,0);
assert.deepEqual(stationary.map(node=>[node.x,node.y,node.angle,node.scaleX,node.scaleY]),transforms);
}
h.advance(0.12);assert.equal(chests[1].angle,6);assert.equal(chests[3].angle,6);
assert.equal(h.tweens.length,2,'loops reuse the original tweens');
assert.deepEqual(h.data.weeklyRewards.map(reward=>reward.state),[0,1,2,1]);
});
test('repeated rendering keeps the current swing and pause instead of restarting or stacking tweens', () => {
const h=setup(35),chest=h.weekly.getChildByName('tiers').children[0].getChildByName('chest');
const tween=h.tweens[0];
h.advance(0.06);const angle=chest.angle;
for(let i=0;i<3;i++) h.daily.renderWeeklyRewards(h.data);
assert.equal(h.tweens.length,1);assert.equal(h.tweens[0],tween);assert.equal(chest.angle,angle);
h.advance(0.06);assert.equal(chest.angle,6);
h.advance(0.36);assert.equal(chest.angle,0);
h.advance(1);h.daily.renderWeeklyRewards(h.data);
h.advance(0.99);assert.equal(chest.angle,0);
h.advance(0.07);assert.ok(chest.angle>0 && chest.angle<6);
assert.equal(h.tweens.length,1);
});
for(const state of [0,2]) test(`changing a ready reward to state ${state} stops its swing and restores the authored angle`, () => {
const h=setup(0),chest=h.weekly.getChildByName('tiers').children[0].getChildByName('chest');
chest.angle=11;h.data.weeklyRewards[0].state=1;h.daily.renderWeeklyRewards(h.data);
h.advance(0.12);assert.equal(chest.angle,17);
h.data.weeklyRewards[0].state=state;h.daily.renderWeeklyRewards(h.data);
assert.equal(chest.angle,11);assert.equal(h.tweens[0].cancelled,true);
h.advance(10);assert.equal(chest.angle,11);
assert.equal(h.daily.weeklyChestAnimations.length,0);
h.data.weeklyRewards[0].state=1;h.daily.renderWeeklyRewards(h.data);
h.advance(0.12);assert.equal(chest.angle,17);assert.equal(h.tweens.length,2);
});
test('removing one reward stops only its icon, and empty weekly data stops the remaining loops', () => {
const h=setup(140),tiers=h.weekly.getChildByName('tiers').children;
const chests=tiers.map(tier=>tier.getChildByName('chest'));
h.advance(0.12);h.data.weeklyRewards.shift();h.daily.renderWeeklyRewards(h.data);
assert.equal(tiers[0].active,false);assert.equal(chests[0].angle,0);assert.equal(h.tweens[0].cancelled,true);
assert.equal(h.daily.weeklyChestAnimations.length,3);
h.advance(0.24);assert.equal(chests[0].angle,0);assert.equal(chests[1].angle,-6);
h.data.weeklyRewards=[];h.daily.renderWeeklyRewards(h.data);
assert.equal(h.weekly.active,false);assert.equal(h.daily.weeklyChestAnimations.length,0);
assert.ok(h.tweens.every(tween=>tween.cancelled));
h.advance(10);assert.ok(chests.every(chest=>chest.angle===0));
});
test('a removed weekly section releases all running chest animations', () => {
const h=setup(140),chests=h.weekly.getChildByName('tiers').children.map(tier=>tier.getChildByName('chest'));
h.advance(0.12);
h.weekly.parent.children=h.weekly.parent.children.filter(node=>node!==h.weekly);
h.daily.renderWeeklyRewards(h.data);
assert.ok(h.tweens.every(tween=>tween.cancelled));assert.equal(h.daily.weeklyChestAnimations.length,0);
h.advance(10);assert.ok(chests.every(chest=>chest.angle===0));
});
test('disabling a panel restores its icons, inactive rendering stays stopped, and reopening starts fresh loops', () => {
const h=setup(140),chests=h.weekly.getChildByName('tiers').children.map(tier=>tier.getChildByName('chest'));
h.advance(0.12);h.daily.node.active=false;h.daily.onDisable();h.daily.onDisable();
assert.ok(h.tweens.every(tween=>tween.cancelled));assert.equal(h.daily.weeklyChestAnimations.length,0);
h.daily.renderWeeklyRewards(h.data);h.advance(10);
assert.equal(h.tweens.length,4);assert.ok(chests.every(chest=>chest.angle===0));
h.daily.node.active=true;h.daily.renderWeeklyRewards(h.data);h.daily.renderWeeklyRewards(h.data);
assert.equal(h.tweens.length,8);assert.equal(h.daily.weeklyChestAnimations.length,4);
h.advance(0.12);assert.ok(chests.every(chest=>chest.angle===6));
});
test('destroying a panel stops its loops without cancelling unrelated tweens on the same icon', () => {
const h=setup(35),chest=h.weekly.getChildByName('tiers').children[0].getChildByName('chest');
const unrelated=h.cc.tween(chest).to(1,{opacity:100}).start();
h.advance(0.12);h.daily.onDestroy();h.daily.onDestroy();
assert.equal(h.tweens[0].cancelled,true);assert.equal(unrelated.cancelled,false);
assert.equal(h.daily.weeklyChestAnimations.length,0);assert.equal(chest.angle,0);
h.advance(1);assert.equal(chest.angle,0);assert.equal(chest.opacity,100);
});
test('weekly prefab references, nine-slice background and check assets are valid', () => {
function walk(value) {
if (!value || typeof value !== 'object') return;
if ('__id__' in value) assert.ok(prefab[value.__id__]);
Object.values(value).forEach(walk);
}
walk(prefab);
const {weekly,preview}=setup();
assert.equal(preview.getComponent('cc.Sprite')._type,1);
const pop=JSON.parse(fs.readFileSync(path.join(root,'assets/action_bundle/img/dailyQuest/pop.png.meta'),'utf8'));
for (const key of ['borderLeft','borderRight','borderTop','borderBottom']) assert.equal(pop.subMetas.pop[key],28);
assert.equal(preview.getChildByName('close'),undefined);
assert.equal(preview.getChildByName('btnClaim'),undefined);
const meta=JSON.parse(fs.readFileSync(path.join(root,'assets/action_bundle/img/dailyQuest/dagou.png.meta'),'utf8'));
for(const tier of weekly.getChildByName('tiers').children){
const check=tier.getChildByName('claimed');assert.ok(check.width>0 && check.width<=tier.getChildByName('chest').width);assert.ok(check.height>0 && check.height<=tier.getChildByName('chest').height);
assert.equal(check.getComponent('cc.Sprite')._spriteFrame.__uuid__,meta.subMetas.dagou.uuid);
assert.equal(tier.getComponent('cc.Button').clickEvents[0].handler,'selectWeeklyReward');
}
});
test('weekly progress and total do not create rows in the daily list',()=>{
const {daily,weekly,preview}=setup(15);
assert.equal(weekly.getChildByName('progress').getComponent('cc.ProgressBar').progress,15/140);
assert.equal(weekly.getChildByName('current').getChildByName('count').getComponent('cc.Label').string,'15');
assert.equal(daily.content.children.length,1);assert.equal(preview.active,false);
});
test('clicking an unreached chest only shows its rewards',()=>{
const {daily,weekly,preview,requests}=setup(15);daily.selectWeeklyReward(null,'weekly_35');
assert.equal(preview.active,true);assert.equal(weekly.getChildByName('previewDismiss'),undefined);
const items=preview.getChildByName('rewards').children.filter(node=>node.active);
assert.equal(items.length,2);assert.ok(items.every(node=>!node.getChildByName('stars').active));
assert.equal(items[1].getChildByName('count').getComponent('cc.Label').string,'15分钟');
assert.equal(requests.length,0);
});
test('clicking a ready chest claims immediately and adds the check only after success',()=>{
const {daily,data,weekly,preview,requests,cc,tweens,advance}=setup(105);
const tier=weekly.getChildByName('tiers').getChildByName('weekly_105');
const check=tier.getChildByName('claimed'),chest=tier.getChildByName('chest');
const click=tier.getComponent('cc.Button').clickEvents[0];
advance(0.12);assert.equal(chest.angle,6);assert.equal(tier.angle,0);assert.equal(check.angle,0);
let granted=0;
daily.openReward=(id,rewards)=>{assert.equal(id,'weekly_105');assert.equal(rewards.length,2);granted++;};
daily[click.handler](null,click.customEventData);daily[click.handler](null,click.customEventData);
assert.equal(requests.length,1);assert.equal(requests[0][0],'weekly_105');assert.equal(requests[0][2],'weekly');
assert.equal(preview.active,false);assert.equal(check.active,false);
const result=structuredClone(data);result.weeklyRewards[2].state=2;
requests[0][1]({code:1,data:{rewards:result.weeklyRewards[2].rewards,task:result}});
assert.equal(granted,1);assert.equal(check.active,true);assert.equal(daily.touchStart,true);
assert.equal(chest.angle,0);assert.equal(tweens.find(tween=>tween.target===chest).cancelled,true);
advance(2.6);assert.equal(chest.angle,0);assert.equal(check.angle,0);
assert.equal(daily.weeklyChestAnimations.length,2,'other claimable rewards keep swinging');
daily.selectWeeklyReward(null,'weekly_105');assert.equal(requests.length,1);assert.equal(preview.active,true);
});
test('clicking an already claimed chest only previews rewards',()=>{
const {daily,data,weekly,preview,requests}=setup(140);data.weeklyRewards[3].state=2;daily.renderWeeklyRewards(data);
daily.selectWeeklyReward(null,'weekly_140');
assert.equal(preview.active,true);assert.equal(requests.length,0);
assert.equal(weekly.getChildByName('tiers').getChildByName('weekly_140').getChildByName('claimed').active,true);
assert.equal(preview.getChildByName('rewards').children.filter(node=>node.active).length,3);
});
test('capture listener closes outside touches without a dismiss overlay and preserves inside touches',()=>{
const {daily,weekly,preview}=setup(0);daily.selectWeeklyReward(null,'weekly_35');
const listener=daily.node.listeners[0];assert.equal(listener.capture,true);assert.equal(listener.type,'touchstart');
let stopped=0;
const inside={getLocation:()=>({x:daily.node.x+preview.x,y:daily.node.y+weekly.y+preview.y}),stopPropagation:()=>stopped++};
listener.handler.call(listener.target,inside);assert.equal(preview.active,true);assert.equal(stopped,0);
const outside={getLocation:()=>({x:daily.node.x-500,y:daily.node.y+600}),stopPropagation:()=>stopped++};
listener.handler.call(listener.target,outside);assert.equal(preview.active,false);assert.equal(stopped,1);
daily.selectWeeklyReward(null,'weekly_70');assert.equal(preview.active,true);
daily.onDestroy();assert.equal(daily.node.listeners.length,0);
});
test('failed requests allow retry and slow requests do not mark a chest claimed',()=>{
const {daily,weekly,requests,toasts,tweens,advance}=setup(35);daily.selectWeeklyReward(null,'weekly_35');
requests[0][1]({code:0,msg:'领取失败'});
const check=weekly.getChildByName('tiers').getChildByName('weekly_35').getChildByName('claimed');
assert.equal(check.active,false);assert.equal(daily.touchStart,true);assert.deepEqual(toasts,['领取失败']);
daily.selectWeeklyReward(null,'weekly_35');assert.equal(requests.length,2);daily.timeout();
assert.equal(daily.touchStart,true);assert.equal(check.active,false);
assert.equal(tweens.length,1);assert.equal(tweens[0].cancelled,false);
advance(0.12);assert.equal(weekly.getChildByName('tiers').getChildByName('weekly_35').getChildByName('chest').angle,6);
});
test('edge previews stay inside the panel and reuse editor-created reward nodes',()=>{
const {daily,weekly,preview,cc}=setup(0);const items=preview.getChildByName('rewards').children,original=items.slice();
cc.instantiate=()=>{throw Error('weekly rewards must not instantiate nodes');};
const tier=weekly.getChildByName('tiers').children[0];tier.x=-120;daily.renderWeeklyRewards(cc.fx.GameConfig.GM_INFO.tasks);assert.equal(tier.x,-120);
daily.selectWeeklyReward(null,'weekly_140');assert.equal(preview.width,508);assert.equal(preview.x,136);assert.equal(preview.getChildByName('arrow').x,204);
assert.equal(items.filter(node=>node.active).length,3);
daily.selectWeeklyReward(null,'weekly_35');assert.equal(items.filter(node=>node.active).length,2);
items.forEach((node,index)=>assert.equal(node,original[index]));
});
test('progress remains bounded and empty weekly data hides the section',()=>{
const {daily,data,weekly}=setup(0);assert.equal(weekly.getChildByName('progress').getComponent('cc.ProgressBar').progress,0);
data.weeklyStars=999;daily.renderWeeklyRewards(data);assert.equal(weekly.getChildByName('progress').getComponent('cc.ProgressBar').progress,1);
data.weeklyRewards=[];daily.renderWeeklyRewards(data);assert.equal(weekly.active,false);
});
test('five weekly rewards reuse editor slots in centered rows with a fixed arrow tip',()=>{
const {daily,data,preview,cc}=setup(0);
const rewards=[{type:'coin',count:2000},{type:'infinite_health',count:3600},{type:'freeze',count:1},{type:'magic_wand',count:1},{type:'hammer',count:1}];
data.weeklyRewards[3].rewards=rewards;
cc.instantiate=()=>{throw Error('preview must not create reward nodes');};
daily.selectWeeklyReward(null,'weekly_140');
const list=preview.getChildByName('rewards'),slots=list.children.filter(n=>n.active);
assert.equal(slots.length,5);assert.equal(preview.width,508);assert.equal(preview.height,360);
assert.deepEqual(slots.map(n=>[n.x,n.y]),[[-164,90],[0,90],[164,90],[-82,-90],[82,-90]]);
assert.equal(preview.y+preview.getChildByName('arrow').y,25);
assert.deepEqual(slots.map(n=>n.getChildByName('count').getComponent('cc.Label').string),['2000','60分钟','×1','×1','×1']);
daily.selectWeeklyReward(null,'weekly_35');
assert.equal(preview.height,180);assert.equal(preview.y,124);assert.equal(list.children.filter(n=>n.active).length,2);
});
test('daily reward icons match the coin height without stretching or resizing the fish can',()=>{
const {daily,cc}=setup(0);
const list=daily.content.getChildByName('taskItem').getChildByName('rewards');
const stars=list.children[0].getChildByName('stars');
const originalStars=[stars.width,stars.height,stars.scaleX,stars.scaleY];
const coinRect=daily.rewardIcons[0].getRect();
for(const [index,type] of ['coin','freeze','hammer','magic_wand','infinite_health'].entries()){
// Reuse the same node, including after the much larger heart/wand assets.
daily.renderTaskRewards(list,{stars:2,rewards:[{type,count:1}]});
const icon=list.children[1].getChildByName('icon'),rect=daily.rewardIcons[index].getRect();
assert.ok(Math.abs(icon.height*icon.scaleY-coinRect.height)<1e-8,`${type} matches the outer coin height`);
assert.equal(icon.width/icon.height,rect.width/rect.height,`${type} retains its trimmed aspect ratio`);
assert.equal(icon.scaleX,icon.scaleY);
assert.equal(icon.getComponent('cc.Sprite').sizeMode,cc.Sprite.SizeMode.TRIMMED);
assert.equal(icon.getComponent('cc.Sprite').trim,true);
assert.deepEqual([stars.width,stars.height,stars.scaleX,stars.scaleY],originalStars);
if(type==='coin') assert.equal(icon.scale,1,'the existing coin size is unchanged');
}
daily.renderTaskRewards(list,{stars:2,rewards:[{type:'rocket',count:1}]});
const icon=list.children[1].getChildByName('icon');
assert.equal(icon.getComponent('cc.Sprite').spriteFrame,null);
assert.equal(icon.scale,1,'missing assets do not retain a previous reward scale');
daily.renderTaskRewards(list,{stars:2,rewards:[{type:'coin',count:100}]});
assert.equal(icon.scale,1);
assert.equal(icon.height,coinRect.height);
});
test('preview icons match the outer coin height and retain count labels without action buttons',()=>{
const {daily,preview}=setup(0);daily.selectWeeklyReward(null,'weekly_35');
const rewards=preview.getChildByName('rewards');assert.equal(rewards.scaleX,1);
assert.equal(preview.width,344);assert.equal(preview.height,180);
for(const slot of rewards.children){
assert.equal(slot.scaleX,1);
const icon=slot.getChildByName('icon');
assert.equal(icon.scaleX,icon.scaleY);
const frame=icon.getComponent('cc.Sprite').spriteFrame;
if(frame){
assert.ok(Math.abs(icon.height*icon.scaleY-daily.rewardIcons[0].getRect().height)<1e-8);
assert.equal(icon.width/icon.height,frame.getRect().width/frame.getRect().height);
}
assert.equal(slot.getChildByName('count').getComponent('cc.Label').fontSize,40);
assert.equal(slot.getChildByName('plus').active,false);
}
assert.equal(preview.getChildByName('btnClaim'),undefined);assert.equal(preview.getChildByName('close'),undefined);
});
test('missing reward images keep empty icon slots and quantities without text substitutes',()=>{
const {daily,preview}=setup(0);
daily.selectWeeklyReward(null,'weekly_35');
daily.selectWeeklyReward(null,'weekly_140');
const items=preview.getChildByName('rewards').children;
for(const slot of items.slice(0,2)){
assert.equal(slot.active,true);
assert.equal(slot.getChildByName('icon').active,true);
assert.equal(slot.getChildByName('icon').getComponent('cc.Sprite').spriteFrame,null);
assert.equal(slot.getChildByName('rewardName').active,false);
assert.equal(slot.getChildByName('rewardName').getComponent('cc.Label').string,'');
assert.equal(slot.getChildByName('count').getComponent('cc.Label').string,'15分钟');
}
daily.rewardIcons[8]={getRect:()=>({width:100,height:200})};
daily.selectWeeklyReward(null,'weekly_140');
assert.equal(items[0].getChildByName('icon').getComponent('cc.Sprite').spriteFrame,daily.rewardIcons[8]);
});
test('runtime layout repairs the old 0.82 x 0.8 scaling without creating nodes',()=>{
const {daily,preview,cc}=setup(0),rewards=preview.getChildByName('rewards');
rewards.scale=0.82;
rewards.children.forEach(slot=>{slot.scale=0.8;slot.getChildByName('icon').setContentSize(90,94);});
cc.instantiate=()=>{throw Error('no runtime nodes');};
daily.selectWeeklyReward(null,'weekly_35');
assert.equal(rewards.scale,1);assert.equal(preview.width,344);
for(const slot of rewards.children){
assert.equal(slot.scale,1);
const icon=slot.getChildByName('icon');
if(icon.getComponent('cc.Sprite').spriteFrame) assert.ok(Math.abs(icon.height*icon.scaleY-daily.rewardIcons[0].getRect().height)<1e-8);
}
daily.selectWeeklyReward(null,'weekly_140');assert.equal(preview.width,508);
assert.equal(preview.getComponent('cc.Sprite').type,1);
});
test('outside hit testing converts camera screen coordinates before checking the preview',()=>{
const {daily,weekly,preview,cc}=setup(0);daily.selectWeeklyReward(null,'weekly_35');
const insideWorld={x:daily.node.x+preview.x,y:daily.node.y+weekly.y+preview.y};
cc.Camera.findCamera=()=>({getScreenToWorldPoint:(point,out)=>{out.x=point.x+700;out.y=point.y-300;return out;}});
let stopped=0;
daily.onWeeklyPreviewOutsideTouch({getLocation:()=>({x:insideWorld.x-700,y:insideWorld.y+300}),stopPropagation:()=>stopped++});
assert.equal(preview.active,true);assert.equal(stopped,0);
daily.onWeeklyPreviewOutsideTouch({getLocation:()=>({x:-1000,y:-1000}),stopPropagation:()=>stopped++});
assert.equal(preview.active,false);assert.equal(stopped,1);
});