// Run: node --test tools/test-wucai-entry.cjs 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('typescript'); const root = path.resolve(__dirname, '..'); function loadClass(file, members, globals = {}) { let text = fs.readFileSync(path.join(root, file), 'utf8'); // Isolate the changed adapter from an unrelated pre-existing invalid character earlier in Utils. if (file.endsWith('/Utils.ts')) text = 'class Utils {\n' + text.slice(text.indexOf(' static wucaiTransfer(')); const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true); const declaration = source.statements.find(ts.isClassDeclaration); const body = members ? members.map(name => { const member = declaration.members.find(node => node.name?.getText(source) === name); assert.ok(member, name); return member.getText(source); }).join('\n') : declaration.members.map(node => node.getText(source)).join('\n'); const name = declaration.name.text; return vm.runInNewContext(ts.transpileModule(`class ${name} { ${body} }\n${name};`, { compilerOptions: { target: ts.ScriptTarget.ES2017 }, }).outputText, globals); } const entry = () => loadClass('assets/Script/module/WucaiEntry.ts'); const flush = async () => { for (let i = 0; i < 8; i++) await Promise.resolve(); }; test('cold launch and its paired onShow create only one opportunity', () => { const Entry = entry(); const options = { query: { from: 'wucai' }, scene: 1037 }; Entry.captureLaunch(options); const first = Entry.loginEntryId; assert.ok(first); assert.equal(Entry.captureShow(options), false); assert.equal(Entry.loginEntryId, first); assert.equal(Entry.pendingEntryId, ''); }); test('each later external jump gets a new opportunity; ordinary resume does not', () => { const Entry = entry(); Entry.captureLaunch({ query: {} }); Entry.onHide(); assert.equal(Entry.captureShow({ query: { from: 'wucai' }, scene: 1037 }), true); const first = Entry.pendingEntryId; Entry.onHide(); assert.equal(Entry.captureShow({ query: { from: 'wucai' }, scene: 1089 }), false); assert.equal(Entry.pendingEntryId, first, 'a pending game-scene entry survives ordinary resume'); Entry.onHide(); assert.equal(Entry.captureShow({ query: { from: 'wucai' }, scene: 1037 }), true); assert.notEqual(Entry.pendingEntryId, first); }); test('internal restart reuses the entry, and the post-choice restart is an ordinary login', () => { const Entry = entry(); const query = Object.fromEntries(new URLSearchParams(Entry.restartPath('entry-1'))); Entry.captureLaunch({ query }); assert.equal(Entry.loginEntryId, 'entry-1'); Entry.onHide(); assert.equal(Entry.captureShow({ query, scene: 1037 }), false); Entry.captureLaunch({ query: Object.fromEntries(new URLSearchParams(Entry.restartPath())) }); assert.equal(Entry.loginEntryId, ''); assert.equal(Entry.pendingEntryId, ''); }); function homeFixture() { const state = { alive: true, preparingGame: false, reboots: [], cancelled: 0, loading: false, toasts: [] }; let resolve, reject; const saving = new Promise((yes, no) => { resolve = yes; reject = no; }); const Entry = entry(); Entry.pendingEntryId = 'jump-1'; const Utils = { prepareWucaiTransfer: () => { state.prepares = (state.prepares || 0) + 1; return saving; }, cancelWucaiTransferPreparation: () => state.cancelled++ }; class Node { setContentSize() {} addComponent() {} destroy() { this.destroyed = true; } } const cc = { Node, BlockInputEvents: {}, macro: { MAX_ZINDEX: 999 }, isValid: node => !node.destroyed, fx: { GameConfig: { isGameplayEntryPreparing: () => state.preparingGame } } }; const MiniGameSdk = { API: { reboot: (path, fail) => state.reboots.push({ path, fail }), showToast: msg => state.toasts.push(msg) } }; const Home = loadClass('assets/Script/JiaZai.ts', ['preparingWucaiTransfer', 'deferredWucaiEntryId', 'hasPendingWucaiEntry', 'resumeWucaiEntry'], { cc, Utils, WucaiEntry: Entry, MiniGameSdk, }); const home = new Home(); home.node = { getContentSize: () => ({}), addChild: node => { state.blocker = node; } }; home.isHomeRuntimeAlive = () => state.alive; home.openLoad = () => { state.loading = true; }; home.closeLoad = () => { state.loading = false; }; home.resumeDeferredHomePopups = () => { state.popupsResumed = true; }; home.flushHomeAutomaticTasks = () => { state.tasksResumed = true; }; return { state, home, Entry, resolve, reject }; } test('home entry blocks input and waits for saves before restarting with the same opportunity', async () => { const { state, home, resolve } = homeFixture(); home.resumeWucaiEntry(); home.resumeWucaiEntry(); assert.equal(state.prepares, 1); assert.ok(state.blocker); assert.equal(state.loading, true); assert.equal(state.reboots.length, 0); resolve(); await flush(); assert.equal(state.reboots.length, 1); assert.equal(state.reboots[0].path, '?from=wucai&wucai_entry_id=jump-1'); }); test('a jump in gameplay waits for HomeScene without touching saves or restarting the round', async () => { const { state, home, Entry, resolve } = homeFixture(); state.alive = false; home.resumeWucaiEntry(); assert.equal(state.prepares, undefined); assert.equal(Entry.pendingEntryId, 'jump-1'); state.alive = true; home.resumeWucaiEntry(); resolve(); await flush(); assert.equal(state.reboots.length, 1); }); test('save failure or restart failure releases input, retains the entry, and reports failure', async () => { for (const kind of ['save', 'restart']) { const { state, home, Entry, resolve, reject } = homeFixture(); home.resumeWucaiEntry(); if (kind === 'save') reject({ msg: '保存结果未确认' }); else { resolve(); await flush(); state.reboots[0].fail(); } await flush(); assert.equal(state.cancelled, 1); assert.equal(state.blocker.destroyed, true); assert.equal(state.loading, false); assert.equal(home.preparingWucaiTransfer, false); assert.equal(Entry.pendingEntryId, 'jump-1'); assert.equal(home.hasPendingWucaiEntry(), false, 'failed preparation must allow normal play and saves to recover'); assert.equal(state.popupsResumed, true); assert.equal(state.tasksResumed, true); assert.equal(state.toasts.length, 1); if (kind === 'save') assert.equal(state.toasts[0], '保存结果未确认'); } }); test('lifecycle processes a hot jump only at home, and later normal resumes retain deferred jumps', () => { const Entry = entry(); Entry.captureLaunch({ query: {} }); const state = { scene: 'GameScene', resumes: 0 }; const cc = { fx: { GameConfig: { GM_INFO: {} } }, audioEngine: { resumeAll() {} }, game: { resume() {} }, director: { getScene: () => ({ name: state.scene }) }, find: () => ({ getComponent: () => ({ resumeWucaiEntry: () => state.resumes++ }) }) }; const Manager = loadClass('assets/Script/GameManager.ts', ['onShow', 'updateWucaiEntry'], { cc, WucaiEntry: Entry, MiniProgramBenefitsBridge: { instance: { capture() {} } }, }); const manager = new Manager(); Entry.onHide(); manager.onShow({ query: { from: 'wucai' }, scene: 1037 }); assert.equal(state.resumes, 0); assert.ok(Entry.pendingEntryId); state.scene = 'HomeScene'; Entry.onHide(); manager.onShow({ query: {}, scene: 1089 }); assert.equal(state.resumes, 1); }); test('choice submission carries the round id and reloads without opening another round', () => { const state = { reboots: [], saved: [] }; const gm = { uid: 'u1', wucai_type: 1, wucai_entry_id: 'round-2', wucai_preview: { levelAmount: 4 }, wucai_current_preview: { levelAmount: 7 }, coin: 99 }; const MiniGameSdk = { API: { reboot: path => state.reboots.push(path), showToast() {}, shushu_TrackWucaiTransfer() {}, shushu_userSetTransferredFromWucai() {} } }; const Utils = loadClass('assets/Script/module/Pay/Utils.ts', ['wucaiTransfer'], { cc: { fx: { GameConfig: { GM_INFO: gm }, StorageMessage: { setStorage: (...args) => state.saved.push(args) } } }, MiniGameSdk, }); Utils.POST = (path, body, callback) => { assert.equal(path, 'wucaiTransfer'); assert.equal(body.wucai_entry_id, 'round-2'); callback({ code: 1, data: { wucai_type: 2, wucai_entry_id: 'round-2', wucai_reward_granted: true, wucai_reward_popup_pending: false } }); }; Utils.wucaiTransfer('choose_wucai'); assert.equal(gm.wucai_reward_granted, true); assert.equal(gm.wucai_reward_popup_pending, false); assert.equal(gm.coin, 99, 'client does not locally award another migration gift'); assert.deepEqual(state.reboots, ['?wucai_resume=1']); });