864 lines
48 KiB
JavaScript
864 lines
48 KiB
JavaScript
// Run: node --test tools/test-first-game-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 parse(file) {
|
|
return ts.createSourceFile(file, fs.readFileSync(path.join(root, file), 'utf8'), ts.ScriptTarget.Latest, true);
|
|
}
|
|
function loadClass(file, names, globals) {
|
|
const source = parse(file);
|
|
const declaration = source.statements.find(ts.isClassDeclaration);
|
|
const members = names.map(name => {
|
|
const member = declaration.members.find(node => node.name && node.name.getText(source) === name);
|
|
assert.ok(member, `${file}: ${name}`);
|
|
return member.getText(source);
|
|
});
|
|
const code = `class ${declaration.name.text} { ${members.join('\n')} }\n${declaration.name.text};`;
|
|
return vm.runInNewContext(ts.transpileModule(code, {
|
|
compilerOptions: { target: ts.ScriptTarget.ES2017 },
|
|
}).outputText, globals);
|
|
}
|
|
const flush = async () => { for (let i = 0; i < 15; i++) await Promise.resolve(); };
|
|
function deferred() {
|
|
let resolve, reject;
|
|
const promise = new Promise((yes, no) => { resolve = yes; reject = no; });
|
|
return { promise, resolve, reject };
|
|
}
|
|
|
|
function fixture() {
|
|
const state = { scene: { name: 'HomeScene' }, eligible: true, events: [], timers: [], errors: [], preloads: [], holds: 0,
|
|
downloads: deferred(), blocks: deferred(), bundleCallbacks: [], jsonCallbacks: [], logs: [] };
|
|
const controller = {
|
|
openLoad() { state.loading = true; },
|
|
closeLoad() { state.loading = false; },
|
|
playGameEntryTransition(force) { assert.equal(force, true); state.events.push('up'); },
|
|
};
|
|
const cc = {
|
|
warn() {}, isValid: value => !!value && !value.destroyed, fx: {},
|
|
find: () => ({ getComponent: () => controller }),
|
|
sys: { platform: 'web', WECHAT_GAME: 'wechat' },
|
|
assetManager: { loadBundle(name, options, done) { assert.equal(options.priority, 10); state.bundleCallbacks.push(done); },
|
|
loadAny(request, options, done) {
|
|
assert.equal(request.bundle, 'custom'); assert.match(request.path, /^Json\/level/);
|
|
assert.equal(options.priority, 10); state.jsonCallbacks.push(done);
|
|
}, bundles: { find: () => ({ preloadScene(name, options, done) {
|
|
assert.equal(options.preset, 'scene'); assert.equal(options.priority, 10);
|
|
state.preloads.push(done);
|
|
} }) } },
|
|
director: {
|
|
getScene: () => state.scene,
|
|
preloadScene: (name, progress, done) => state.preloads.push(done),
|
|
loadScene: (name, done) => {
|
|
if (state.sceneError) { done(state.sceneError); return true; }
|
|
if (state.rejected) return false;
|
|
state.scene = { name }; state.events.push('loadScene'); done(null); return true;
|
|
},
|
|
},
|
|
};
|
|
const GameTool = {
|
|
maxLevel: () => false,
|
|
shouldPreloadGameplayPopupInstances: () => state.eligible,
|
|
areGameplayPopupSubpackagesDownloaded: () => true,
|
|
ensureGameplayPopupSubpackagesDownloaded: () => { state.events.push('download-check'); return state.downloads.promise; },
|
|
warmupHomeGameplayPopupAssets: () => { state.events.push('home-popups'); return Promise.resolve(true); },
|
|
predownloadGameplayPopupSubpackages: () => state.events.push('disk'),
|
|
getMissingGameplayPopupSubpackages: () => ['pause'],
|
|
beginGameplayPopupGameSceneReloadRetention: () => state.eligible,
|
|
finishGameplayPopupGameSceneReloadRetention: () => state.events.push('retention-finished'),
|
|
};
|
|
const BlockAssetManager = { instance: {
|
|
holdBackgroundDownloads() {
|
|
state.holds++; let released = false;
|
|
return () => { if (!released) { released = true; state.holds--; } };
|
|
},
|
|
startBackgroundCache: () => state.events.push('background'),
|
|
} };
|
|
const Config = loadClass('assets/Script/module/Config/GameConfig.ts', [
|
|
'firstGameplayEntryComplete', 'firstGameplayEntryPending', 'sceneLoadRequest', 'cancelBlockLoading',
|
|
'gameplayScenePreload', 'gameplaySceneForegroundPreload', 'pendingEntryDownloadRelease', 'takeEntryDownloadRelease', 'preloadGameplayScene',
|
|
'timeGameplayStage', 'beginGameplayEntryPreparation', 'gameplayEntryStartedAt', 'isGameplayEntryPreparing',
|
|
'levelLoadRequest', 'LEVEL_INFO_init', 'applyLevelData', 'loadLevelFromBundle',
|
|
'shouldPrepareFirstGameplayEntry', 'createBlockLoading', 'enterGameSceneWhenReady',
|
|
], { cc, GameTool, BlockAssetManager, console: { log: (...args) => state.logs.push(args.join(' ')) }, setTimeout: (done, delay) => state.timers.push({ done, delay }) });
|
|
Config.BLOCK_INFO = [];
|
|
Config.GM_INFO = { level: 0, otherLevel: 0 };
|
|
Config.prepareBlockWindow = () => { state.events.push('blocks'); return state.blocks.promise; };
|
|
Config.warmupHomeBlockAssets = () => state.blocks.promise;
|
|
Config.showBlockLoadError = (error, retry) => state.errors.push({ error, retry });
|
|
require('./new-player-ab-test-support.cjs').withNewPlayerConfig(Config);
|
|
cc.fx = { GameConfig: Config, GameTool };
|
|
return { state, cc, Config, BlockAssetManager };
|
|
}
|
|
|
|
async function resourcesReady(state) {
|
|
state.downloads.resolve(true); state.blocks.resolve(true); state.preloads.at(-1)(null); await flush();
|
|
}
|
|
|
|
test('entry prepares disk packages, blocks and scene in parallel; popup Prefabs never gate it', async () => {
|
|
const { state, Config } = fixture();
|
|
Config.enterGameSceneWhenReady();
|
|
assert.equal(state.loading, true);
|
|
assert.equal(state.preloads.length, 1);
|
|
assert.ok(state.events.includes('blocks'));
|
|
assert.equal(state.holds, 1);
|
|
await resourcesReady(state);
|
|
assert.equal(state.events.includes('home-popups'), false);
|
|
assert.equal(state.loading, false);
|
|
assert.equal(state.timers[0].delay, 1000, 'preserve the existing transition animation duration');
|
|
state.timers[0].done();
|
|
assert.equal(Config.firstGameplayEntryPending, true);
|
|
assert.equal(Config.firstGameplayEntryComplete, false);
|
|
assert.equal(state.holds, 1, 'scene launch is not map readiness');
|
|
Config.takeEntryDownloadRelease()();
|
|
assert.equal(state.holds, 0);
|
|
});
|
|
|
|
for (const mode of ['low', 'later-home', 'restart']) {
|
|
test(`${mode}: no popup barrier and caller transition delay remains unchanged`, async () => {
|
|
const { state, Config } = fixture();
|
|
if (mode === 'low') state.eligible = false;
|
|
if (mode === 'later-home') Config.firstGameplayEntryComplete = true;
|
|
if (mode === 'restart') state.scene.name = 'GameScene';
|
|
Config.enterGameSceneWhenReady(27); await resourcesReady(state);
|
|
assert.equal(state.timers[0].delay, 27); state.timers[0].done();
|
|
assert.equal(Config.firstGameplayEntryPending, false);
|
|
assert.equal(state.events.includes('home-popups'), false);
|
|
assert.equal(state.events.includes('retention-finished'), mode === 'restart');
|
|
});
|
|
}
|
|
|
|
for (const stage of ['downloads', 'blocks', 'loadScene', 'rejected']) {
|
|
test(`${stage} failure releases background hold and permits retry`, async () => {
|
|
const { state, Config } = fixture();
|
|
Config.enterGameSceneWhenReady();
|
|
state.downloads.resolve(stage !== 'downloads');
|
|
if (stage === 'blocks') state.blocks.reject(new Error('blocks')); else state.blocks.resolve(true);
|
|
state.preloads[0](stage === 'preload' ? new Error('preload') : null); await flush();
|
|
if (stage === 'loadScene') state.sceneError = new Error('scene');
|
|
if (stage === 'rejected') state.rejected = true;
|
|
if (state.timers.length) state.timers[0].done();
|
|
assert.equal(state.errors.length, 1); assert.equal(state.holds, 0);
|
|
assert.equal(Config.firstGameplayEntryComplete, false);
|
|
state.sceneError = null; state.rejected = false;
|
|
state.blocks = deferred(); state.downloads = deferred();
|
|
state.errors[0].retry(); await resourcesReady(state); state.timers.at(-1).done();
|
|
assert.equal(state.scene.name, 'GameScene'); assert.equal(state.holds, 1);
|
|
});
|
|
}
|
|
|
|
test('foreground scene preparation does not wait for the lower-priority Home request', async () => {
|
|
const { state, Config } = fixture();
|
|
const early = Config.preloadGameplayScene(); Config.enterGameSceneWhenReady();
|
|
assert.equal(state.preloads.length, 2); await resourcesReady(state);
|
|
state.timers[0].done(); assert.equal(state.scene.name, 'GameScene');
|
|
state.preloads[0](null); await early;
|
|
Config.preloadGameplayScene(); assert.equal(state.preloads.length, 3); state.preloads[2](null);
|
|
});
|
|
|
|
test('superseded or departed entry cannot close a newer Loading or leak its hold', async () => {
|
|
const { state, Config } = fixture();
|
|
Config.enterGameSceneWhenReady(); const old = state.blocks;
|
|
state.blocks = deferred(); Config.enterGameSceneWhenReady();
|
|
assert.equal(state.holds, 1);
|
|
old.resolve(true); state.downloads.resolve(true); state.preloads[0](null); await flush();
|
|
assert.equal(state.loading, true); assert.equal(state.holds, 1);
|
|
state.scene = { name: 'OtherScene' }; state.blocks.resolve(true); await flush();
|
|
assert.equal(state.holds, 0); assert.equal(state.events.includes('up'), false);
|
|
});
|
|
|
|
test('old Map release cannot unpause a new entry', async () => {
|
|
const { state, Config } = fixture();
|
|
Config.enterGameSceneWhenReady(); await resourcesReady(state); state.timers[0].done();
|
|
const oldMapRelease = Config.takeEntryDownloadRelease();
|
|
Config.enterGameSceneWhenReady(); assert.equal(state.holds, 2);
|
|
oldMapRelease(); oldMapRelease(); assert.equal(state.holds, 1);
|
|
Config.takeEntryDownloadRelease()(); assert.equal(state.holds, 0);
|
|
});
|
|
|
|
for (const high of [true, false]) {
|
|
test(`Home idle policy high=${high}: only high tier preloads GameScene, no high popup load`, async () => {
|
|
const { state, cc, Config, BlockAssetManager } = fixture(); state.eligible = high;
|
|
const Home = loadClass('assets/Script/JiaZai.ts', ['startHomeGameplayWarmup'], { cc, BlockAssetManager, console: { log() {} } });
|
|
Object.assign(new Home(), { node: {}, homeGameplayWarmupPending: true, isHomeRuntimeAlive: () => true, scheduleOnce() {}, unschedule() {}, flushHomeAutomaticTasks() {} }).startHomeGameplayWarmup();
|
|
assert.equal(state.events[0], 'disk');
|
|
assert.equal(state.preloads.length, high ? 1 : 0, 'scene starts before block warmup completes');
|
|
state.blocks.resolve(true); await flush();
|
|
assert.equal(state.preloads.length, high ? 1 : 0);
|
|
if (high) { state.preloads[0](null); await flush(); }
|
|
assert.equal(state.events.includes('home-popups'), !high, 'low helper only warms Pause Bundle');
|
|
assert.ok(state.events.includes('background'));
|
|
});
|
|
}
|
|
|
|
function mapFixture() {
|
|
const context = fixture(); const { state, cc, Config } = context;
|
|
state.scene.name = 'GameScene'; state.popupCallbacks = []; state.frames = [];
|
|
const Manager = loadClass('assets/Script/SceneManager.ts', ['warmupGameplayPopupBundles'], { cc });
|
|
const manager = Object.assign(new Manager(), {
|
|
node: { getChildByName: () => ({}) }, isPopupRuntimeAlive: () => !state.destroyed,
|
|
scheduleOnce: done => state.frames.push(done),
|
|
scheduleGameplayIntroPredownload: () => state.events.push('intro-idle'),
|
|
loadDynamicPopup(name, bundle, prefab, parent, done, showLoading) {
|
|
assert.equal(showLoading, false); state.popupCallbacks.push({ name, done });
|
|
},
|
|
releaseDynamicPopup: () => assert.fail('warmup failure must not clear a concurrent user open/retry'),
|
|
});
|
|
const Map = loadClass('assets/Script/Map.ts', [
|
|
'beginGameEntry', 'finishFirstGameplayEntry', 'resumeEntryBackgroundDownloads', 'onDestroy',
|
|
], { cc });
|
|
const map = Object.assign(new Map(), {
|
|
node: { parent: { parent: { parent: { getChildByName: () => ({ active: false }) } } } },
|
|
SceneManager: manager, isMapRuntimeAlive: () => !state.destroyed,
|
|
ensureWinLoaded: done => state.popupCallbacks.push({ name: 'win', done }),
|
|
playEntryReveal: done => { state.events.push('down'); state.revealed = done; },
|
|
showEnterNewModeIfNeeded: () => state.events.push('new-mode'), scheduleOnce: done => state.frames.push(done),
|
|
releaseWinBundle: () => {}, releaseBlockAssets: () => {},
|
|
});
|
|
Map._instance = map;
|
|
Config.firstGameplayEntryPending = true;
|
|
Config.pendingEntryDownloadRelease = context.BlockAssetManager.instance.holdBackgroundDownloads(); map.beginGameEntry();
|
|
const source = parse('assets/Script/Map.ts'); let initializer;
|
|
function visit(node) {
|
|
if (ts.isVariableDeclaration(node) && node.name.getText(source) === 'schedulePopupWarmup') initializer = node.initializer.getText(source);
|
|
ts.forEachChild(node, visit);
|
|
}
|
|
visit(source); assert.ok(initializer);
|
|
const code = ts.transpileModule(`(function () {
|
|
let wallReady = false, blockReady = false, popupWarmupScheduled = false;
|
|
const schedulePopupWarmup = ${initializer};
|
|
return { wall() { wallReady = true; schedulePopupWarmup(); }, block() { blockReady = true; schedulePopupWarmup(); } };
|
|
})`, { compilerOptions: { target: ts.ScriptTarget.ES2017 } }).outputText;
|
|
return { ...context, map, ready: vm.runInNewContext(code, { cc }).call(map) };
|
|
}
|
|
|
|
test('map readiness reveals immediately; popup warmup follows pause, win, propWindow, newMode, lose across frames', () => {
|
|
const { state, ready, Config } = mapFixture();
|
|
ready.wall(); assert.equal(state.holds, 1); assert.equal(state.events.includes('down'), false);
|
|
ready.block(); assert.equal(state.holds, 0); assert.equal(Config.firstGameplayEntryComplete, true);
|
|
assert.equal(state.events.at(-1), 'down'); assert.equal(state.popupCallbacks.length, 0);
|
|
const order = [];
|
|
for (let i = 0; i < 5; i++) {
|
|
state.frames.shift()(); const pending = state.popupCallbacks.shift(); order.push(pending.name);
|
|
pending.done(null); assert.equal(state.popupCallbacks.length, 0, 'next popup must wait another frame');
|
|
}
|
|
assert.deepEqual(order, ['pause', 'win', 'propWindow', 'newMode', 'lose']);
|
|
state.revealed(); assert.equal(state.events.at(-1), 'new-mode');
|
|
});
|
|
|
|
for (const failure of ['pause', 'win']) {
|
|
test(`${failure} warmup failure does not block gameplay or remaining popups`, () => {
|
|
const { state, ready, Config } = mapFixture(); ready.wall(); ready.block();
|
|
for (let i = 0; i < 5; i++) {
|
|
state.frames.shift()(); const pending = state.popupCallbacks.shift();
|
|
pending.done(pending.name === failure ? new Error('warmup') : null);
|
|
}
|
|
assert.equal(Config.firstGameplayEntryComplete, true); assert.equal(state.errors.length, 0);
|
|
});
|
|
}
|
|
|
|
for (const reason of ['memory', 'destroy']) {
|
|
test(`${reason} stops subsequent popup warmup even without a completion callback`, () => {
|
|
const { state, ready } = mapFixture(); ready.wall(); ready.block(); state.frames.shift()();
|
|
if (reason === 'memory') state.eligible = false; else state.destroyed = true;
|
|
state.popupCallbacks.shift().done(null);
|
|
while (state.frames.length) state.frames.shift()();
|
|
assert.equal(state.popupCallbacks.length, 0); assert.equal(state.holds, 0);
|
|
});
|
|
}
|
|
|
|
test('background hold blocks only queued background work and supports nested/idempotent release', () => {
|
|
const Manager = loadClass('assets/Script/module/Tool/BlockAssetManager.ts', [
|
|
'entryDownloadHolds', 'holdBackgroundDownloads', 'pumpDownloadQueue',
|
|
], {});
|
|
Manager.FOREGROUND_DOWNLOAD_LIMIT = 10; Manager.BACKGROUND_CONCURRENCY = 8;
|
|
const started = [];
|
|
const manager = Object.assign(new Manager(), { foregroundLoads: 0, activeFileDownloads: 0,
|
|
activeBackgroundDownloads: 0, downloadQueue: [], startUserDownload: job => started.push(job.path) });
|
|
const a = manager.holdBackgroundDownloads(), b = manager.holdBackgroundDownloads();
|
|
manager.downloadQueue = [{ path: 'bulk', priority: -1, background: true }, { path: 'current', priority: 1, background: false }];
|
|
manager.pumpDownloadQueue(); assert.deepEqual(started, ['current']);
|
|
a(); a(); assert.deepEqual(started, ['current']); b(); assert.deepEqual(started, ['current', 'bulk']);
|
|
assert.equal(manager.entryDownloadHolds, 0);
|
|
});
|
|
|
|
test('Map destruction before readiness releases its hold; late map callbacks cannot warm popups', () => {
|
|
const { state, map, ready, Config } = mapFixture();
|
|
assert.equal(state.holds, 1);
|
|
state.destroyed = true; map.onDestroy(); map.onDestroy();
|
|
ready.wall(); ready.block();
|
|
assert.equal(state.holds, 0); assert.equal(state.frames.length, 0);
|
|
assert.equal(Config.firstGameplayEntryPending, false);
|
|
});
|
|
|
|
test('memory downgrade before map readiness still reveals the map but skips Prefab warmup', () => {
|
|
const { state, ready, Config } = mapFixture(); state.eligible = false;
|
|
ready.wall(); assert.equal(state.events.includes('down'), false);
|
|
ready.block(); state.frames.shift()();
|
|
assert.equal(Config.firstGameplayEntryComplete, true);
|
|
assert.equal(state.holds, 0); assert.deepEqual(state.events.slice(-2), ['down', 'intro-idle']);
|
|
assert.equal(state.popupCallbacks.length, 0);
|
|
});
|
|
|
|
test('JSON pending does not delay package downloads or foreground scene preparation; no duplicate entry task', async () => {
|
|
const { state, Config } = fixture(); Config.LEVEL_INFO_init(true, 0, false);
|
|
assert.equal(state.preloads.length, 1); assert.equal(state.holds, 1);
|
|
assert.ok(state.events.includes('download-check')); assert.equal(state.events.includes('blocks'), false);
|
|
state.bundleCallbacks[0](null, {});
|
|
assert.equal(state.events.includes('blocks'), false);
|
|
state.jsonCallbacks[0](null, { json: { BLOCK_INFO: [[]], LEVEL_INFO: [{ id: 1 }], WALL_INFO: [] } });
|
|
assert.equal(state.preloads.length, 1); assert.equal(state.events.filter(e => e === 'download-check').length, 1);
|
|
state.downloads.resolve(true); state.blocks.resolve(true); await flush();
|
|
state.timers[0].done(); assert.equal(state.scene.name, 'GameScene', 'must launch while foreground prefetch is still pending');
|
|
state.preloads[0](null); await flush();
|
|
assert.equal(state.logs.some(line => line.includes('[进关耗时]')), false, 'entry timing logs are disabled');
|
|
});
|
|
|
|
for (const failure of ['custom', 'json', 'departed']) {
|
|
test(`${failure} during early preparation releases the pause and cannot launch a stale scene`, () => {
|
|
const { state, Config } = fixture(); Config.LEVEL_INFO_init(true, 0, false);
|
|
if (failure === 'departed') state.scene = { name: 'OtherScene' };
|
|
state.bundleCallbacks[0](failure === 'custom' ? new Error('custom') : null, {
|
|
getInfoWithPath: () => ({}) });
|
|
if (failure === 'json') state.jsonCallbacks[0](new Error('json'));
|
|
assert.equal(state.holds, 0); assert.equal(state.timers.length, 0);
|
|
});
|
|
}
|
|
|
|
test('prefetch failure does not fail the entry: the actual scene load owns retry/error reporting', async () => {
|
|
const { state, Config } = fixture(); Config.enterGameSceneWhenReady();
|
|
state.preloads[0](new Error('prefetch')); await flush(); assert.equal(state.errors.length, 0);
|
|
state.downloads.resolve(true); state.blocks.resolve(true); await flush(); state.timers[0].done();
|
|
assert.equal(state.scene.name, 'GameScene'); assert.equal(state.errors.length, 0);
|
|
});
|
|
|
|
test('late custom callback from an older entry cannot release the newer entry hold', () => {
|
|
const { state, Config } = fixture();
|
|
Config.LEVEL_INFO_init(true, 0, false); Config.LEVEL_INFO_init(true, 0, false);
|
|
assert.equal(state.holds, 1);
|
|
state.bundleCallbacks[0](new Error('stale'));
|
|
assert.equal(state.holds, 1); assert.equal(state.errors.length, 0);
|
|
state.bundleCallbacks[1](new Error('current'));
|
|
assert.equal(state.holds, 0); assert.equal(state.errors.length, 1);
|
|
});
|
|
|
|
test('entry preparation leaves Home resource scheduling untouched', () => {
|
|
const { state, cc, Config } = fixture();
|
|
cc.find = () => assert.fail('entry preparation must not take control of Home resource tasks');
|
|
const entry = Config.beginGameplayEntryPreparation();
|
|
assert.equal(state.holds, 1);
|
|
assert.ok(state.events.includes('download-check'));
|
|
assert.equal(state.preloads.length, 1);
|
|
entry.release();
|
|
assert.equal(state.holds, 0);
|
|
});
|
|
|
|
function homeFixture(high = true) {
|
|
const context = fixture();
|
|
const { state, cc, Config, BlockAssetManager } = context;
|
|
Object.assign(state, { eligible: high, homeFrames: [], homeBundleLoads: [], homePrefabLoads: [],
|
|
bundles: new Map(), toasts: [], opened: [], automatic: [], bundleDependencies: {},
|
|
delayedBundles: new Set(), pendingBundles: [], warnings: [] });
|
|
Object.assign(Config.GM_INFO, { level: 20, canIos: true, first: false, otherLevel: 0 });
|
|
cc.Prefab = class {};
|
|
cc.game = { setFrameRate() {} };
|
|
cc.assetManager.getBundle = name => state.bundles.get(name);
|
|
cc.assetManager.loadBundle = (name, options, done) => {
|
|
state.homeBundleLoads.push({ name, priority: options.priority });
|
|
const finish = (error = null) => {
|
|
if (error) return done(error);
|
|
let bundle = state.bundles.get(name);
|
|
if (!bundle) {
|
|
bundle = { name, deps: state.bundleDependencies[name] || [],
|
|
load: (path, type, loaded) => {
|
|
assert.equal(type, cc.Prefab);
|
|
state.homePrefabLoads.push({ path, done: loaded, priority: options.priority });
|
|
} };
|
|
state.bundles.set(name, bundle);
|
|
}
|
|
done(null, bundle);
|
|
};
|
|
if (state.delayedBundles.has(name)) state.pendingBundles.push({ name, finish });
|
|
else finish();
|
|
};
|
|
cc.assetManager.loadAny = (request, options, done) => {
|
|
assert.ok(state.bundles.get(request.bundle), 'loadBundle must finish before loading its Prefab');
|
|
assert.equal(request.type, cc.Prefab);
|
|
state.homePrefabLoads.push({ path: request.path, done, priority: options.priority });
|
|
};
|
|
cc.fx.GameTool.predownloadGameplayPopupSubpackages = () => {
|
|
state.events.push('disk'); return state.downloads.promise;
|
|
};
|
|
cc.fx.GameTool.getWechatGameVersion = () => '正式版';
|
|
cc.fx.GameTool.shushu_Track = () => {};
|
|
cc.warn = (...args) => state.warnings.push(args);
|
|
cc.error = (...args) => state.warnings.push(args);
|
|
const globals = { cc, BlockAssetManager, console: { log() {}, error() {} }, LQCollideSystem: {},
|
|
initProvinceLocator: () => state.automatic.push('province'),
|
|
wx: { getLaunchOptionsSync: () => ({ query: state.launchQuery || {} }), getSystemInfoSync: () => ({ platform: 'android' }) },
|
|
MiniGameSdk: { API: { showToast: text => state.toasts.push(text) } },
|
|
NumberToImage: { numberToImageNodes5: () => state.automatic.push('coin-images') },
|
|
setTimeout: (done, delay) => state.timers.push({ done, delay }),
|
|
Utils: { getShopDouble: callback => { state.shopResponse = callback; } } };
|
|
const Home = loadClass('assets/Script/JiaZai.ts', [
|
|
'homeGameplayWarmupPending', 'homeAutomaticTasks', 'homeAutomaticFlushScheduled', 'homePopupOpenRequest',
|
|
'isHomeRuntimeAlive', 'queueHomeAutomaticTask', 'flushHomeAutomaticTasks', 'startHomeGameplayWarmup',
|
|
'preloadHomePopupPrefabs', 'loadHomePopupPrefab', 'ensureHomePopupPrefab', 'onLoad',
|
|
'isHomeBundleReady', 'loadHomeBundleWithDependencies',
|
|
'openShop', 'openMonthlyCard', 'openRewardWindow', 'openPassCheck', 'getShareInfo',
|
|
'getOrder', 'buyMonthCard', 'openStarter_pack', 'startGame',
|
|
'loadJungleTreasurePrefab', 'showJungleTreasure', 'loadJungleOverPrefab', 'showJungleOver',
|
|
], globals);
|
|
const node = { getChildByName: () => node, addChild() {} };
|
|
const home = Object.assign(new Home(), {
|
|
node, scheduleOnce: (done, delay) => state.homeFrames.push({ done, delay }),
|
|
unschedule: done => { state.homeFrames = state.homeFrames.filter(item => item.done !== done); },
|
|
isCareerDisabledByMemoryWarning: () => false, vibrateButtonClick() {},
|
|
deferHomePopupWhileTransfer: () => false,
|
|
});
|
|
for (const name of ['ensureLoadingOnTop', 'registerCareerMemoryWarning', 'updateWucaiTransferVisibility',
|
|
'applyStartUI2', 'refreshStaminaDisplay', 'closeLoad', 'checkShare', 'closeAvatar', 'setShareInfo',
|
|
'checkTasks', 'specialLevelShow', 'endlessLevelShow', 'passCheckBuyState', 'checkAndSetPlayerPassLevel',
|
|
'initializeJungleTreasureEntryFromLogin', 'setStarterPackHomeButtonVisible', 'checkStarter_pack',
|
|
'getSRank', 'popUpPassCheck', 'onGames', 'getOrder', 'monthH']) {
|
|
home[name] = () => state.automatic.push(name);
|
|
}
|
|
return { ...context, Home, home, globals,
|
|
finishBundle(name, error = null) {
|
|
const index = state.pendingBundles.findIndex(item => item.name === name);
|
|
assert.ok(index >= 0, `pending dependency: ${name}`);
|
|
state.pendingBundles.splice(index, 1)[0].finish(error);
|
|
},
|
|
frame() {
|
|
const index = state.homeFrames.findIndex(item => item.delay < 1);
|
|
assert.ok(index >= 0, 'one queued frame must exist');
|
|
state.homeFrames.splice(index, 1)[0].done();
|
|
},
|
|
async ready() {
|
|
state.downloads.resolve(true); state.blocks.resolve(true);
|
|
for (const done of state.preloads) done(null);
|
|
await flush();
|
|
},
|
|
};
|
|
}
|
|
|
|
for (const cached of [false, true]) {
|
|
test(`Home startup cached=${cached}: gameplay preparation precedes optional Prefabs and Career`, async () => {
|
|
const f = homeFixture();
|
|
if (cached) {
|
|
const request = f.home.loadHomePopupPrefab('shop', 'prefab/shop0917', 'cachedShopPrefab', -10);
|
|
f.state.homePrefabLoads.shift().done(null, {}); await request;
|
|
f.state.homeBundleLoads = [];
|
|
}
|
|
f.home.onLoad();
|
|
assert.equal(f.state.homeBundleLoads.length, 0);
|
|
assert.equal(f.state.automatic.includes('getSRank'), false);
|
|
assert.equal(f.state.automatic.includes('closeAvatar'), false);
|
|
f.frame();
|
|
assert.ok(f.state.events.includes('disk'));
|
|
assert.equal(f.state.preloads.length, 1);
|
|
f.state.downloads.resolve(true); f.state.preloads[0](null); await flush();
|
|
assert.equal(f.state.homePrefabLoads.length, 0, 'block preparation is still pending');
|
|
f.state.blocks.resolve(true); await flush();
|
|
while (f.state.homeFrames.some(item => item.delay < 1)) f.frame();
|
|
assert.ok(f.state.homePrefabLoads.some(item => item.path === 'prefab/Share'));
|
|
assert.equal(f.state.homePrefabLoads.some(item => item.path === 'prefab/shop0917'), !cached);
|
|
assert.ok(f.state.automatic.includes('getSRank'));
|
|
});
|
|
}
|
|
|
|
test('Home automatic tasks wait through entry, resume on failure, and stop on scene departure', async () => {
|
|
const f = homeFixture();
|
|
f.home.queueHomeAutomaticTask('one', () => f.state.automatic.push('one'));
|
|
f.home.queueHomeAutomaticTask('two', () => f.state.automatic.push('two'));
|
|
f.home.startHomeGameplayWarmup();
|
|
const entry = f.Config.beginGameplayEntryPreparation();
|
|
await f.ready(); f.frame();
|
|
assert.deepEqual(f.state.automatic, []);
|
|
entry.release(); f.frame();
|
|
assert.deepEqual(f.state.automatic, ['one']);
|
|
f.state.scene = { name: 'GameScene' }; f.home.node.destroyed = true;
|
|
f.frame();
|
|
assert.deepEqual(f.state.automatic, ['one']);
|
|
});
|
|
|
|
for (const failure of ['rejection', 'timeout']) {
|
|
test(`Home warmup ${failure} does not leave automatic features waiting forever`, async () => {
|
|
const f = homeFixture();
|
|
f.home.queueHomeAutomaticTask('home', () => f.state.automatic.push('home'));
|
|
f.home.startHomeGameplayWarmup();
|
|
if (failure === 'timeout') f.state.homeFrames.find(item => item.delay === 15).done();
|
|
else { f.state.blocks.reject(new Error('offline')); await f.ready(); }
|
|
f.frame();
|
|
assert.deepEqual(f.state.automatic, ['home']);
|
|
assert.equal(f.Config.isGameplayEntryPreparing(), false);
|
|
});
|
|
}
|
|
|
|
test('only unstarted automatic tasks are deferred; an in-flight Prefab still completes and caches after Home destruction', async () => {
|
|
const f = homeFixture();
|
|
const request = f.home.loadHomePopupPrefab('shop', 'prefab/shop0917', 'cachedShopPrefab', -10);
|
|
f.home.node.destroyed = true; f.state.scene = { name: 'GameScene' };
|
|
const prefab = {};
|
|
f.state.homePrefabLoads[0].done(null, prefab);
|
|
assert.equal(await request, prefab);
|
|
assert.equal(f.Home.cachedShopPrefab, prefab);
|
|
});
|
|
|
|
test('a cached shop does not skip the three other missing shop Prefabs', async () => {
|
|
const f = homeFixture();
|
|
f.home.loadHomePopupPrefab('shop', 'prefab/shop0917', 'cachedShopPrefab', -10);
|
|
f.state.homePrefabLoads.shift().done(null, {}); await flush();
|
|
f.home.homeGameplayWarmupPending = false;
|
|
f.home.preloadHomePopupPrefabs();
|
|
while (f.state.homeFrames.some(item => item.delay < 1)) f.frame();
|
|
const requests = f.state.homePrefabLoads.map(item => item.path);
|
|
assert.equal(requests.includes('prefab/shop0917'), false);
|
|
for (const name of ['RewardWindow', 'monthlyCard', 'passCheck']) assert.ok(requests.includes('prefab/' + name));
|
|
for (const request of f.state.homePrefabLoads) {
|
|
const paid = ['newbieGift', 'monthlyCard', 'passCheck'].some(name => request.path === 'prefab/' + name);
|
|
assert.equal(request.priority, request.path === 'prefab/RewardWindow' ? 2 : paid ? 1 : -10);
|
|
}
|
|
});
|
|
|
|
for (const method of ['openShop', 'openMonthlyCard', 'openRewardWindow', 'openPassCheck']) {
|
|
test(`${method}: a user request loads immediately during warmup, then opens only after its Prefab is ready`, async () => {
|
|
const f = homeFixture();
|
|
f.home.queueHomeAutomaticTask('background', () => assert.fail('automatic task started too early'));
|
|
f.home[method] = () => f.state.opened.push(method);
|
|
f.Home.prototype[method].call(f.home);
|
|
assert.equal(f.state.homePrefabLoads.length, 1);
|
|
assert.equal(f.state.homePrefabLoads[0].priority, 5);
|
|
assert.deepEqual(f.state.opened, []);
|
|
f.state.homePrefabLoads[0].done(null, {}); await flush();
|
|
assert.deepEqual(f.state.opened, [method]);
|
|
assert.equal(f.home.homeGameplayWarmupPending, true);
|
|
});
|
|
}
|
|
|
|
test('late manual popup completion cannot open over entry or touch its Loading, but remains cached', async () => {
|
|
const f = homeFixture();
|
|
f.home.ensureHomePopupPrefab('shop', 'prefab/shop0917', 'cachedShopPrefab', () => assert.fail('late popup opened'));
|
|
const entry = f.Config.beginGameplayEntryPreparation(); f.state.loading = true;
|
|
f.state.homePrefabLoads[0].done(null, {}); await flush();
|
|
assert.ok(f.Home.cachedShopPrefab);
|
|
assert.equal(f.state.loading, true);
|
|
entry.release();
|
|
});
|
|
|
|
test('failed manual popup load can be retried and never instantiates a missing asset', async () => {
|
|
const f = homeFixture();
|
|
const open = () => f.state.opened.push('shop');
|
|
f.home.ensureHomePopupPrefab('shop', 'prefab/shop0917', 'cachedShopPrefab', open);
|
|
f.state.homePrefabLoads[0].done(new Error('offline')); await flush();
|
|
assert.equal(f.state.opened.length, 0);
|
|
f.home.ensureHomePopupPrefab('shop', 'prefab/shop0917', 'cachedShopPrefab', open);
|
|
f.state.homePrefabLoads[1].done(null, {}); await flush();
|
|
assert.deepEqual(f.state.opened, ['shop']);
|
|
});
|
|
|
|
test('a bundle released before Prefab completion is never installed into the Home cache', async () => {
|
|
const f = homeFixture();
|
|
const request = f.home.loadHomePopupPrefab('shop', 'prefab/shop0917', 'cachedShopPrefab', 5);
|
|
f.state.bundles.delete('shop');
|
|
f.state.homePrefabLoads[0].done(null, {});
|
|
await assert.rejects(request, /预制体已失效/);
|
|
assert.equal(f.Home.cachedShopPrefab, undefined);
|
|
});
|
|
|
|
test('share entry selects the actual level immediately but defers the automatic invitation window', () => {
|
|
const f = homeFixture();
|
|
f.cc.sys.platform = 'wechat'; f.Config.GM_INFO.uid = 'self';
|
|
f.state.launchQuery = { level: '51', uid: 'friend' };
|
|
f.home.openShare = () => f.state.opened.push('share');
|
|
f.home.getShareInfo();
|
|
assert.equal(f.Config.GM_INFO.otherLevel, 51);
|
|
assert.equal(f.Config.GM_INFO.otherUid, 'friend');
|
|
assert.equal(f.state.opened.length, 0);
|
|
f.home.homeGameplayWarmupPending = false; f.home.flushHomeAutomaticTasks(); f.frame();
|
|
assert.deepEqual(f.state.opened, ['share']);
|
|
});
|
|
|
|
test('newer user popup requests supersede old UI callbacks without losing either Prefab cache', async () => {
|
|
const f = homeFixture();
|
|
f.home.ensureHomePopupPrefab('shop', 'prefab/shop0917', 'cachedShopPrefab', () => f.state.opened.push('shop'));
|
|
f.home.ensureHomePopupPrefab('shop', 'prefab/monthlyCard', 'cachedMonthlyCardPrefab', () => f.state.opened.push('card'));
|
|
f.state.homePrefabLoads[0].done(null, {}); f.state.homePrefabLoads[1].done(null, {}); await flush();
|
|
assert.deepEqual(f.state.opened, ['card']);
|
|
assert.ok(f.Home.cachedShopPrefab && f.Home.cachedMonthlyCardPrefab);
|
|
});
|
|
|
|
test('Home level warmup uses the same priority-aware JSON API as manual entry', async () => {
|
|
const { state, Config } = fixture();
|
|
const request = Config.loadLevelFromBundle(51);
|
|
assert.equal(state.jsonCallbacks.length, 0);
|
|
state.bundleCallbacks[0](null, {});
|
|
const json = { BLOCK_INFO: [[]] };
|
|
state.jsonCallbacks[0](null, { json });
|
|
assert.equal(await request, json);
|
|
});
|
|
|
|
for (const high of [false, true]) {
|
|
test(`Home tier high=${high}: RewardWindow loads first and order recovery waits for its success`, async () => {
|
|
const f = homeFixture(high);
|
|
f.home.onLoad(); f.frame();
|
|
assert.equal(f.state.automatic.includes('getOrder'), false, 'phase one still has precedence');
|
|
await f.ready(); f.state.automatic = [];
|
|
f.frame();
|
|
assert.deepEqual(f.state.automatic, []);
|
|
assert.equal(f.state.homePrefabLoads.length, 1);
|
|
assert.equal(f.state.homePrefabLoads[0].path, 'prefab/RewardWindow');
|
|
assert.equal(f.state.homePrefabLoads[0].priority, 2);
|
|
while (f.state.homeFrames.some(item => item.delay < 1)) f.frame();
|
|
const paths = f.state.homePrefabLoads.map(item => item.path);
|
|
assert.deepEqual(paths.slice(0, 5), ['RewardWindow', 'newbieGift', 'monthlyCard', 'passCheck', 'shop'].map(n => 'prefab/' + n));
|
|
assert.equal(f.state.automatic.includes('getOrder'), false, 'no recovery while reward images are pending');
|
|
f.state.homePrefabLoads.find(item => item.path === 'prefab/RewardWindow').done(null, {});
|
|
await flush();
|
|
while (f.state.homeFrames.some(item => item.delay < 1)) f.frame();
|
|
assert.equal(f.state.automatic.filter(name => name === 'getOrder').length, 1);
|
|
assert.ok(f.state.automatic.indexOf('checkStarter_pack') < f.state.automatic.indexOf('closeAvatar'));
|
|
});
|
|
}
|
|
|
|
test('RewardWindow failure leaves recovery unstarted and the pending orders untouched; a later preload can retry', async () => {
|
|
const f = homeFixture();
|
|
const pending = [{ itemid: 'gold_1', outTradeNo: 'pending-order' }];
|
|
f.Config.GM_INFO.allOutTradeNo = pending;
|
|
f.home.homeGameplayWarmupPending = false;
|
|
f.home.preloadHomePopupPrefabs(); f.frame();
|
|
f.state.homePrefabLoads[0].done(new Error('reward images offline'), null); await flush();
|
|
while (f.state.homeFrames.some(item => item.delay < 1)) f.frame();
|
|
assert.equal(f.state.automatic.includes('getOrder'), false);
|
|
assert.equal(f.Config.GM_INFO.allOutTradeNo, pending);
|
|
f.home.preloadHomePopupPrefabs(); f.frame();
|
|
const rewards = f.state.homePrefabLoads.filter(item => item.path === 'prefab/RewardWindow');
|
|
assert.equal(rewards.length, 2);
|
|
rewards[1].done(null, {}); await flush(); f.frame();
|
|
assert.equal(f.state.automatic.filter(name => name === 'getOrder').length, 1);
|
|
});
|
|
|
|
for (const departed of [false, true]) {
|
|
test(`late RewardWindow success does not start recovery over game entry, departed=${departed}`, async () => {
|
|
const f = homeFixture();
|
|
f.home.homeGameplayWarmupPending = false;
|
|
f.home.preloadHomePopupPrefabs(); f.frame();
|
|
f.Config.isGameplayEntryPreparing = () => true;
|
|
if (departed) { f.home.node.destroyed = true; f.state.scene = { name: 'GameScene' }; }
|
|
const prefab = {}; f.state.homePrefabLoads[0].done(null, prefab); await flush();
|
|
f.frame();
|
|
assert.equal(f.Home.cachedRewardPrefab, prefab);
|
|
assert.equal(f.state.automatic.includes('getOrder'), false);
|
|
if (!departed) {
|
|
f.Config.isGameplayEntryPreparing = () => false;
|
|
f.frame();
|
|
assert.equal(f.state.automatic.filter(name => name === 'getOrder').length, 1);
|
|
assert.equal(f.state.automatic[0], 'getOrder', 'recovery still has priority over unstarted gift tasks');
|
|
}
|
|
});
|
|
}
|
|
|
|
test('Home priorities reorder only unstarted tasks and keep equal priorities in insertion order', () => {
|
|
const f = homeFixture();
|
|
for (const [name, priority] of [['normal', 0], ['paid-a', 1], ['order', 2], ['paid-b', 1]]) {
|
|
f.home.queueHomeAutomaticTask(name, () => f.state.automatic.push(name), priority);
|
|
}
|
|
f.home.homeGameplayWarmupPending = false; f.home.flushHomeAutomaticTasks();
|
|
while (f.state.homeFrames.some(item => item.delay < 1)) f.frame();
|
|
assert.deepEqual(f.state.automatic, ['order', 'paid-a', 'paid-b', 'normal']);
|
|
});
|
|
|
|
test('Home Prefab waits for nested bundle dependencies and forwards request priority', async () => {
|
|
const f = homeFixture();
|
|
f.state.bundleDependencies = { action_bundle: ['shop'], shop: ['UI'] };
|
|
f.state.delayedBundles.add('UI');
|
|
const request = f.home.loadHomePopupPrefab('action_bundle', 'prefab/newbieGift', 'cachedActionPrefab', 5);
|
|
assert.deepEqual(f.state.homeBundleLoads.map(item => item.name), ['action_bundle', 'shop', 'UI']);
|
|
assert.ok(f.state.homeBundleLoads.every(item => item.priority === 5));
|
|
assert.equal(f.state.homePrefabLoads.length, 0, 'registered root does not mean its dependencies are ready');
|
|
f.finishBundle('UI');
|
|
assert.equal(f.state.homePrefabLoads.length, 1);
|
|
const prefab = {}; f.state.homePrefabLoads[0].done(null, prefab);
|
|
assert.equal(await request, prefab);
|
|
});
|
|
|
|
test('failed Home dependency prevents Prefab parsing; the next request can retry', async () => {
|
|
const f = homeFixture();
|
|
f.state.bundleDependencies = { shop: ['UI'] }; f.state.delayedBundles.add('UI');
|
|
const request = f.home.loadHomePopupPrefab('shop', 'prefab/RewardWindow', 'cachedRewardPrefab', 1);
|
|
const failed = assert.rejects(request, /dependency offline/);
|
|
f.finishBundle('UI', new Error('dependency offline')); await failed;
|
|
assert.equal(f.state.homePrefabLoads.length, 0);
|
|
assert.equal(f.Home.cachedRewardPrefab, undefined);
|
|
const retry = f.home.loadHomePopupPrefab('shop', 'prefab/RewardWindow', 'cachedRewardPrefab', 5);
|
|
f.finishBundle('UI'); const prefab = {};
|
|
f.state.homePrefabLoads[0].done(null, prefab); assert.equal(await retry, prefab);
|
|
});
|
|
|
|
test('Home cache fast path cannot bypass a missing dependency', async () => {
|
|
const f = homeFixture();
|
|
f.state.bundles.set('shop', { name: 'shop', deps: ['UI'] });
|
|
f.Home.cachedShopPrefab = {}; f.state.delayedBundles.add('UI');
|
|
const opened = f.home.ensureHomePopupPrefab('shop', 'prefab/shop0917', 'cachedShopPrefab', () => f.state.opened.push('shop'));
|
|
assert.equal(opened, false); assert.deepEqual(f.state.opened, []);
|
|
assert.equal(f.state.homePrefabLoads.length, 0);
|
|
f.finishBundle('UI'); f.state.homePrefabLoads[0].done(null, {}); await flush();
|
|
assert.deepEqual(f.state.opened, ['shop']);
|
|
});
|
|
|
|
test('Home dependency removed during Prefab loading cannot leave a usable-looking cache', async () => {
|
|
const f = homeFixture(); f.state.bundleDependencies = { shop: ['UI'] };
|
|
const request = f.home.loadHomePopupPrefab('shop', 'prefab/shop0917', 'cachedShopPrefab', 1);
|
|
const failed = assert.rejects(request, /预制体已失效/);
|
|
f.state.bundles.delete('UI'); f.state.homePrefabLoads[0].done(null, {}); await failed;
|
|
assert.equal(f.Home.cachedShopPrefab, undefined);
|
|
});
|
|
|
|
test('Home dependency traversal handles repeated and cyclic bundle references', async () => {
|
|
const f = homeFixture(); f.state.bundleDependencies = { shop: ['action_bundle'], action_bundle: ['shop'] };
|
|
const request = f.home.loadHomePopupPrefab('shop', 'prefab/shop0917', 'cachedShopPrefab', 1);
|
|
assert.deepEqual(f.state.homeBundleLoads.map(item => item.name), ['shop', 'action_bundle']);
|
|
f.state.homePrefabLoads[0].done(null, {}); await request;
|
|
});
|
|
|
|
for (const stage of ['dependency', 'prefab']) {
|
|
test(`newbie gift callback at ${stage} cannot create nodes after Home destruction`, () => {
|
|
const f = homeFixture();
|
|
f.globals.starterPackRemaining = () => 3600;
|
|
f.state.bundleDependencies = { action_bundle: ['shop'] }; f.state.delayedBundles.add('shop');
|
|
f.cc.instantiate = () => assert.fail('late gift created a node');
|
|
// Exercise the popup body after queue dispatch; the fixture does not load HomePopupQueue.
|
|
f.Home.prototype.openStarter_pack.call(f.home, undefined, undefined, true);
|
|
if (stage === 'prefab') f.finishBundle('shop');
|
|
f.home.node.destroyed = true; f.state.scene = { name: 'GameScene' };
|
|
if (stage === 'dependency') {
|
|
f.finishBundle('shop'); assert.equal(f.state.homePrefabLoads.length, 0);
|
|
} else {
|
|
const prefab = {}; f.state.homePrefabLoads[0].done(null, prefab);
|
|
assert.equal(f.Home.cachedActionPrefab, prefab, 'completed asset can still be cached');
|
|
}
|
|
});
|
|
}
|
|
|
|
for (const stage of ['not-started', 'shop-request', 'first-order']) {
|
|
test(`startGame preserves pending orders while recovery is ${stage}`, () => {
|
|
const f = homeFixture(); f.cc.sys.platform = 'wechat';
|
|
const orders = [{ itemid: 'gold_1', outTradeNo: 'order-a', goodsPrice: 600 },
|
|
{ itemid: 'gold_2', outTradeNo: 'order-b', goodsPrice: 3600 }];
|
|
f.Config.GM_INFO.allOutTradeNo = orders; f.Config.GM_INFO.hp = 1;
|
|
f.cc.fx.AudioManager = { _instance: { playEffect() {} } };
|
|
f.cc.fx.GameTool.getUserPowerTime = () => false;
|
|
f.globals.wx.offShow = f.globals.wx.offHide = () => {};
|
|
f.home.node.getComponent = () => ({ _touch: true, setTouch() {} });
|
|
f.home.playGameEntryTransition = () => {};
|
|
let entries = 0;
|
|
f.Config.LEVEL_INFO_init = () => { entries++; f.Config.isGameplayEntryPreparing = () => true; };
|
|
const requests = [], grants = [], callbacks = [];
|
|
f.globals.Utils.setPayInfo = (done, order) => { requests.push(order); callbacks.push(done); };
|
|
f.globals.MiniGameSdk.API.yinli_Pay = () => {};
|
|
f.cc.fx.GameTool.shopBuy = id => grants.push(id);
|
|
f.home.openLoad = f.home.closeLoad = f.home.updateCoin = () => {};
|
|
if (stage !== 'not-started') f.Home.prototype.getOrder.call(f.home);
|
|
if (stage === 'first-order') f.state.shopResponse({ code: 1, data: { shopDouble: {} } });
|
|
f.home.startGame();
|
|
assert.equal(entries, 1);
|
|
assert.equal(f.Config.GM_INFO.allOutTradeNo, orders, 'entering gameplay must not discard the recovery list');
|
|
assert.equal(f.Config.GM_INFO.allOutTradeNo.length, 2);
|
|
f.home.node.destroyed = true; f.state.scene = { name: 'GameScene' };
|
|
if (stage === 'not-started') {
|
|
assert.deepEqual(requests, []);
|
|
return;
|
|
}
|
|
if (stage === 'shop-request') f.state.shopResponse({ code: 1, data: { shopDouble: {} } });
|
|
while (callbacks.length) callbacks.shift()({ code: 1 });
|
|
assert.deepEqual(requests, ['order-a', 'order-b']);
|
|
assert.deepEqual(grants, ['gold_1', 'gold_2']);
|
|
assert.equal(f.Config.GM_INFO.allOutTradeNo.length, 0);
|
|
});
|
|
}
|
|
|
|
for (const departed of [false, true]) {
|
|
test(`order recovery callback preserves fulfillment without touching entry/old Home UI, departed=${departed}`, () => {
|
|
const f = homeFixture(); f.cc.sys.platform = 'wechat';
|
|
f.Config.GM_INFO.allOutTradeNo = [{ itemid: 'gold_1', outTradeNo: 'paid-order', goodsPrice: 600 }];
|
|
let payDone, uiCalls = 0; const grants = [];
|
|
f.globals.Utils.setPayInfo = done => { payDone = done; };
|
|
f.globals.MiniGameSdk.API.yinli_Pay = () => {};
|
|
f.cc.fx.GameTool.shopBuy = (id, compensate) => grants.push({ id, compensate });
|
|
f.home.openLoad = f.home.closeLoad = f.home.updateCoin = () => uiCalls++;
|
|
f.Home.prototype.getOrder.call(f.home);
|
|
f.state.shopResponse({ code: 1, data: { shopDouble: {} } });
|
|
assert.equal(uiCalls, 1);
|
|
if (departed) { f.home.node.destroyed = true; f.state.scene = { name: 'GameScene' }; }
|
|
else f.Config.isGameplayEntryPreparing = () => true;
|
|
payDone({ code: 1 });
|
|
assert.deepEqual(grants, [{ id: 'gold_1', compensate: true }]);
|
|
assert.equal(uiCalls, 1, 'late payment must not close entry Loading or update old coin nodes');
|
|
assert.equal(f.state.automatic.includes('coin-images'), false);
|
|
assert.equal(f.Config.GM_INFO.allOutTradeNo.length, 0);
|
|
});
|
|
}
|
|
|
|
test('monthly-card fulfillment does not dereference a missing Canvas during scene replacement', async () => {
|
|
const f = homeFixture(); let complete, grants = 0;
|
|
f.globals.Utils.setMonthlyCard = (type, done) => { complete = done; };
|
|
f.cc.fx.GameTool.shopBuy = () => grants++;
|
|
f.cc.fx.GameTool.getMonthlyCardValidityDays = () => Promise.resolve({ days: 30, time: Date.now() });
|
|
f.cc.fx.GameTool.setUserHealth = () => {};
|
|
f.cc.fx.StorageMessage = { setStorage() {} };
|
|
f.Home.prototype.buyMonthCard.call(f.home, 'month_Card', 'month-order');
|
|
f.cc.find = () => null; f.state.scene = { name: 'GameScene' }; f.home.node.destroyed = true;
|
|
complete({ code: 1 }); await flush();
|
|
assert.equal(grants, 1);
|
|
assert.equal(f.state.automatic.includes('closeLoad'), false);
|
|
});
|
|
|
|
for (const departed of [false, true]) {
|
|
for (const stage of ['dependency', 'prefab']) {
|
|
for (const failed of [false, true]) {
|
|
test(`jungle late ${stage} callback cannot touch Home/entry UI, departed=${departed}, failed=${failed}`, () => {
|
|
const f = homeFixture();
|
|
f.home.jungleTreasureServerData = {};
|
|
f.state.bundleDependencies = { jungle_treasure: ['UI'] }; f.state.delayedBundles.add('UI');
|
|
f.home.showJungleTreasure = () => f.state.opened.push('jungle');
|
|
f.home.loadJungleTreasurePrefab(true);
|
|
if (stage === 'prefab') f.finishBundle('UI');
|
|
if (departed) { f.home.node.destroyed = true; f.state.scene = { name: 'GameScene' }; }
|
|
else f.Config.isGameplayEntryPreparing = () => true;
|
|
const error = failed ? new Error('offline') : null;
|
|
if (stage === 'dependency') {
|
|
f.finishBundle('UI', error);
|
|
assert.equal(f.state.homePrefabLoads.length, 0);
|
|
} else {
|
|
const prefab = failed ? null : {};
|
|
f.state.homePrefabLoads[0].done(error, prefab);
|
|
if (!failed) assert.equal(f.Home.cachedJungleTreasurePrefab, prefab);
|
|
}
|
|
assert.equal(f.home.jungleTreasureLoading, false);
|
|
assert.equal(f.home.jungleTreasureOpenAfterLoad, false);
|
|
assert.deepEqual(f.state.opened, []);
|
|
assert.equal(f.state.automatic.includes('closeLoad'), false);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
test('cached jungle settlement bundle still waits for its missing dependencies', () => {
|
|
const f = homeFixture();
|
|
f.state.bundles.set('jungle_treasure', { deps: ['UI'], load: (path, type, done) => {
|
|
f.state.homePrefabLoads.push({ path, done });
|
|
} });
|
|
f.Home.cachedJungleOverPrefab = {}; f.state.delayedBundles.add('UI');
|
|
f.home.showJungleOver = () => f.state.opened.push('settlement');
|
|
f.home.loadJungleOverPrefab();
|
|
assert.equal(f.state.homePrefabLoads.length, 0); assert.deepEqual(f.state.opened, []);
|
|
f.finishBundle('UI');
|
|
assert.equal(f.state.homePrefabLoads[0].path, 'prefab/jungleOver');
|
|
f.state.homePrefabLoads[0].done(null, {});
|
|
assert.deepEqual(f.state.opened, ['settlement']);
|
|
});
|