195 lines
9.0 KiB
JavaScript
195 lines
9.0 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 read = file => fs.readFileSync(path.join(root, file), 'utf8');
|
|
const source = ts.createSourceFile('JiaZai.ts', read('assets/Script/JiaZai.ts'), ts.ScriptTarget.Latest, true);
|
|
const homeClass = source.statements.find(ts.isClassDeclaration);
|
|
const methods = ['isHomeRuntimeAlive', 'rewarded', 'monthH'].map(name => {
|
|
const member = homeClass.members.find(item => item.name?.getText(source) === name);
|
|
assert.ok(member, name);
|
|
return member.getText(source);
|
|
}).join('\n');
|
|
|
|
function compile(code, context) {
|
|
return vm.runInContext(ts.transpileModule(code, { compilerOptions: {
|
|
target: ts.ScriptTarget.ES2018, module: ts.ModuleKind.CommonJS, experimentalDecorators: true,
|
|
} }).outputText, context);
|
|
}
|
|
|
|
function fixture() {
|
|
const pending = {}, timers = [], stored = new Map(), tracked = [];
|
|
const state = { scene: 'HomeScene', preparing: false, coinRefreshes: 0, healthRefreshes: 0, opened: 0, wins: 0, healthWrites: 0 };
|
|
const info = { level: 30, coin: 100, hp: 8, hp_Max: 8, monthTime: 10 };
|
|
const node = () => ({ destroyed: false, active: false, children: [], getChildByName: () => null });
|
|
const cc = {
|
|
_decorator: { ccclass: value => value }, Component: class {},
|
|
isValid: value => !!value && !value.destroyed,
|
|
director: { getScene: () => ({ name: state.scene }) }, Color: { RED: 'red' },
|
|
tween: () => { const tween = { to: () => tween, start: () => tween }; return tween; },
|
|
fx: {
|
|
GameConfig: { GM_INFO: info, isNewPlayerB: () => false, isGameplayEntryPreparing: () => state.preparing },
|
|
StorageMessage: { getStorage: key => stored.get(key), setStorage: (key, value) => stored.set(key, value) },
|
|
GameTool: {
|
|
changeCoin: amount => { info.coin += amount; },
|
|
shushu_Track: (...args) => tracked.push(args),
|
|
setUserHealth: (_, callback) => { state.healthWrites++; callback({}); },
|
|
getHealth: () => {},
|
|
},
|
|
},
|
|
};
|
|
const context = vm.createContext({
|
|
exports: {}, cc, setTimeout: callback => timers.push(callback),
|
|
Utils: {
|
|
monthGetReward: callback => { pending.reward = callback; },
|
|
getMonthlyCard: callback => { pending.card = callback; },
|
|
},
|
|
NumberToImage: { numberToImageNodes: () => {} },
|
|
});
|
|
compile(read('assets/home_popup_queue/HomePopupQueue.ts'), context);
|
|
context.HomePopupQueue = context.exports.default;
|
|
const Home = compile(`class Home { ${methods} }; Home;`, context);
|
|
const home = new Home(), queue = new context.HomePopupQueue();
|
|
home.node = node();
|
|
home.node.getComponent = () => queue;
|
|
queue.node = home.node;
|
|
home.getcard = node();
|
|
const reward = node();
|
|
home.getcard.getChildByName = name => name === 'mcReward' ? reward : null;
|
|
home.cardTime = node();
|
|
home.deferHomePopupWhileTransfer = () => false;
|
|
home.updateCoin = () => state.coinRefreshes++;
|
|
home.setHealthInfo = () => state.healthRefreshes++;
|
|
home.startTimeCutDown = () => state.healthRefreshes++;
|
|
home.openMonthCard = () => { state.opened++; home.monthlyCardNode = { active: true }; };
|
|
home.openWin = () => state.wins++;
|
|
return { home, queue, state, info, pending, timers, stored, tracked };
|
|
}
|
|
|
|
const invalidate = {
|
|
'node cleared': f => { f.home.node = null; },
|
|
'node destroyed': f => { f.home.node.destroyed = true; },
|
|
'component destroyed': f => { f.home.destroyed = true; },
|
|
'entering gameplay': f => { f.state.preparing = true; },
|
|
'scene changed': f => { f.state.scene = 'GameScene'; },
|
|
};
|
|
const rewardResponse = { code: 1, data: { monthCardReward: 1 } };
|
|
|
|
for (const [name, leave] of Object.entries(invalidate)) {
|
|
test(`late monthly callbacks: ${name}`, () => {
|
|
const f = fixture();
|
|
f.home.rewarded(); f.home.monthH(); leave(f);
|
|
f.pending.reward(rewardResponse);
|
|
f.pending.card({ msg: '不在有效期' });
|
|
f.pending.card({ msg: '有效期内' });
|
|
assert.equal(f.info.coin, 600);
|
|
assert.equal(f.tracked.length, 1);
|
|
assert.equal(f.queue.tasks.length, 0);
|
|
assert.equal(f.state.coinRefreshes + f.state.opened + f.state.wins, 0);
|
|
});
|
|
|
|
test(`queued monthly popups: ${name}`, () => {
|
|
const f = fixture();
|
|
f.home.rewarded(); f.home.monthH();
|
|
f.pending.reward(rewardResponse); f.pending.card({ msg: '不在有效期' });
|
|
assert.equal(f.info.coin, 600);
|
|
assert.equal(f.queue.tasks.length, 2);
|
|
leave(f);
|
|
let finished = 0;
|
|
for (const task of f.queue.tasks) task.run(() => finished++);
|
|
assert.equal(finished, 2);
|
|
assert.equal(f.info.coin, 600);
|
|
assert.equal(f.home.getcard.active, false);
|
|
assert.equal(f.state.opened, 0);
|
|
assert.equal(f.stored.has('monthlyCardDate'), false);
|
|
});
|
|
|
|
test(`expired monthly UI timer: ${name}`, () => {
|
|
const f = fixture(); f.home.rewarded();
|
|
f.pending.reward({ code: 0, data: null }); leave(f);
|
|
f.timers.forEach(callback => callback());
|
|
assert.equal(f.info.hp_Max, 5);
|
|
assert.equal(f.info.hp, 5);
|
|
assert.equal(f.state.healthWrites, 1);
|
|
assert.equal(f.state.coinRefreshes + f.state.healthRefreshes, 0);
|
|
});
|
|
}
|
|
|
|
test('normal reward is credited before popup and is not credited again when displayed', () => {
|
|
const f = fixture(); f.home.rewarded(); f.pending.reward(rewardResponse);
|
|
assert.equal(f.info.coin, 600);
|
|
assert.equal(f.state.coinRefreshes, 1);
|
|
assert.equal(f.home.getcard.active, false);
|
|
let finished = 0;
|
|
f.queue.tasks[0].run(() => finished++);
|
|
assert.equal(f.home.getcard.active, true);
|
|
assert.equal(f.info.coin, 600);
|
|
assert.equal(f.tracked.length, 1);
|
|
assert.equal(finished, 1);
|
|
});
|
|
|
|
for (const shopOpen of [true, false]) {
|
|
test(`monthly daily reward refreshes an open shop only (open=${shopOpen})`, () => {
|
|
const f = fixture(), displayed = [];
|
|
const shop = { refreshJungleRewardDisplay: () => displayed.push(f.info.coin) };
|
|
const shopNode = { activeInHierarchy: shopOpen, getComponent: name => name === 'shop' ? shop : null };
|
|
f.home.node.getChildByName = name => name === 'shop' ? shopNode : null;
|
|
f.home.rewarded();
|
|
assert.deepEqual(displayed, []);
|
|
f.pending.reward(rewardResponse);
|
|
assert.equal(f.info.coin, 600);
|
|
assert.deepEqual(displayed, shopOpen ? [600] : []);
|
|
f.queue.tasks[0].run(() => {});
|
|
assert.deepEqual(displayed, shopOpen ? [600] : []);
|
|
assert.equal(f.info.coin, 600);
|
|
});
|
|
}
|
|
|
|
test('normal monthly popup records display date; already claimed reward does not credit coins', () => {
|
|
const f = fixture(); f.home.rewarded(); f.pending.reward({ code: 0, data: {} });
|
|
assert.equal(f.info.coin, 100);
|
|
f.home.monthH(); f.pending.card({ msg: '不在有效期' });
|
|
f.queue.tasks[0].run(() => {});
|
|
assert.equal(f.state.opened, 1);
|
|
assert.ok(f.stored.get('monthlyCardDate'));
|
|
});
|
|
|
|
test('missing reward UI does not prevent credit or block the queue', () => {
|
|
const f = fixture(); f.home.rewarded(); f.pending.reward(rewardResponse);
|
|
f.home.getcard = null;
|
|
let finished = 0; f.queue.tasks[0].run(() => finished++);
|
|
assert.equal(f.info.coin, 600);
|
|
assert.equal(finished, 1);
|
|
});
|
|
|
|
test('monthly card initializes at the home display scale, including first open and reuse in gameplay', () => {
|
|
const extract = (file, name) => {
|
|
const ast = ts.createSourceFile(file, read(file), ts.ScriptTarget.Latest, true);
|
|
return ast.statements.find(ts.isClassDeclaration).members.find(m => m.name?.getText(ast) === name).getText(ast);
|
|
};
|
|
const cc = { sys: { platform: 'web', WECHAT_GAME: 'wechat', BYTEDANCE_GAME: 'bytedance' },
|
|
fx: { GameTool: { checkExpiration: () => false, shushu_Track() {} } },
|
|
};
|
|
const context = vm.createContext({ cc });
|
|
const Card = compile(`class Card { ${extract('assets/Script/monthlyCard.ts', 'init')} }; Card;`, context);
|
|
const Scene = compile(`class SceneManager { ${extract('assets/Script/SceneManager.ts', 'openMonthlyCard')} }; SceneManager;`, context);
|
|
Scene.cachedMonthlyCardPrefab = {}; Scene.clearInvalidShopPrefabCaches = () => {};
|
|
const card = new Card(); card.node = { scale: 1, active: true, getComponent: () => card };
|
|
card.refreshCardState = () => {};
|
|
let created = 0;
|
|
cc.instantiate = () => { created++; return card.node; };
|
|
const scene = new Scene(); scene.node = { addChild() {} };
|
|
scene.openMonthlyCard('yiconBtn_level');
|
|
assert.equal(card.node.scale, 0.7);
|
|
card.node.active = false;
|
|
card.node.scale = 0.8;
|
|
scene.openMonthlyCard('yiconBtn_level');
|
|
assert.equal(card.node.scale, 0.7);
|
|
assert.equal(card.node.active, true);
|
|
assert.equal(created, 1);
|
|
});
|