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