197 lines
11 KiB
JavaScript
197 lines
11 KiB
JavaScript
// Run: node --test tools/test-gameplay-intro-loading.cjs
|
|
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const vm = require('node:vm');
|
|
const ts = require('typescript');
|
|
|
|
function loadClass(file, names, globals) {
|
|
const source = ts.createSourceFile(file, fs.readFileSync(file, 'utf8'), ts.ScriptTarget.Latest, true);
|
|
const declaration = source.statements.find(ts.isClassDeclaration);
|
|
const members = names.map(name => declaration.members.find(m => m.name && m.name.getText(source) === name).getText(source));
|
|
return vm.runInNewContext(ts.transpileModule(`class ${declaration.name.text} { ${members.join('\n')} }\n${declaration.name.text}`, {
|
|
compilerOptions: { target: ts.ScriptTarget.ES2017 },
|
|
}).outputText, globals);
|
|
}
|
|
const flush = async () => { for (let i = 0; i < 5; i++) await Promise.resolve(); };
|
|
|
|
function fixture() {
|
|
const state = { blocksBusy: false, entryBusy: false, destroyed: false, requests: [], schedules: [], timers: [], bundles: new Map(), released: [] };
|
|
const pipeline = () => ({ pipes: [], insert(fn, index) { this.pipes.splice(index, 0, fn); } });
|
|
const cc = {
|
|
sys: { platform: 'wechat', WECHAT_GAME: 'wechat' }, fx: {}, warn() {},
|
|
Node: { EventType: { TOUCH_END: 'touchend' } },
|
|
isValid: node => !!node && !node.destroyed,
|
|
assetManager: {
|
|
pipeline: pipeline(), fetchPipeline: pipeline(),
|
|
getBundle: name => state.bundles.get(name),
|
|
removeBundle: bundle => state.bundles.delete(bundle.name),
|
|
loadBundle: () => assert.fail('background predownload must not load a Bundle'),
|
|
},
|
|
};
|
|
const wx = { preDownloadSubpackage: options => state.requests.push(options) };
|
|
const BlockAssetManager = { instance: { hasPendingLoads: () => state.blocksBusy } };
|
|
const mod = { exports: {} };
|
|
vm.runInNewContext(ts.transpileModule(fs.readFileSync('assets/Script/module/Tool/GameTool.ts', 'utf8'), {
|
|
compilerOptions: { target: ts.ScriptTarget.ES2017, module: ts.ModuleKind.CommonJS },
|
|
}).outputText, {
|
|
module: mod, exports: mod.exports, cc, wx, console: { log() {} },
|
|
require: name => name.endsWith('/BlockAssetManager') ? { default: BlockAssetManager } : { default: {}, MiniGameSdk: {} },
|
|
});
|
|
const tool = cc.fx.GameTool = mod.exports.GameTool;
|
|
tool.trackResourceLoads();
|
|
cc.fx.GameConfig = { isGameplayEntryPreparing: () => state.entryBusy };
|
|
const Manager = loadClass('assets/Script/SceneManager.ts', [
|
|
'activeInstance', 'scheduleGameplayIntroPredownload', 'warmupGameplayPopupBundles',
|
|
'releaseDynamicPopup', 'releaseDynamicPopupBundle', 'releaseDynamicPopupBundleIfUnused',
|
|
'releaseOrphanedPopupBundle', 'onDestroy',
|
|
], { cc, wx, console: { log() {} }, setTimeout: fn => state.timers.push(fn) });
|
|
const manager = Object.assign(new Manager(), {
|
|
node: { getChildByName: () => null, off() {} },
|
|
dynamicPopupStates: {}, dynamicPopupLoads: {},
|
|
isPopupRuntimeAlive: () => !state.destroyed,
|
|
scheduleOnce: (fn, delay) => state.schedules.push({ fn, delay }),
|
|
finishDynamicPopupLoad() {}, unbindLosePanel() {}, unbindPropWindowPanel() {},
|
|
clearDynamicPopupRetry() {}, clearFlyCoins() {}, disposeJungleTreasure() {},
|
|
});
|
|
tool.shouldRetainGameplayPopupBundlesForGameSceneReload = () => true;
|
|
Manager.activeInstance = manager;
|
|
const tick = () => { assert.ok(state.schedules.length); state.schedules.shift().fn(); };
|
|
const bundle = name => {
|
|
const result = { name, releaseAll: () => state.released.push(name) };
|
|
state.bundles.set(name, result); return result;
|
|
};
|
|
return { state, cc, wx, tool, manager, Manager, tick, bundle };
|
|
}
|
|
|
|
test('resource tracking preserves pipeline input/results and drains success and failure exactly once', () => {
|
|
const { cc, tool } = fixture();
|
|
tool.trackResourceLoads();
|
|
for (const pipeline of [cc.assetManager.pipeline, cc.assetManager.fetchPipeline]) {
|
|
assert.equal(pipeline.pipes.length, 1, 'tracking is installed once');
|
|
let callbackArgs;
|
|
const task = { input: ['asset'], onComplete(...args) { callbackArgs = args; } };
|
|
pipeline.pipes[0](task, () => assert.equal(task.output, task.input));
|
|
assert.equal(tool.hasPendingGameplayResourceLoads(), true);
|
|
const error = new Error('download failed');
|
|
task.onComplete(error, 'result');
|
|
assert.deepEqual(callbackArgs, [error, 'result']);
|
|
assert.equal(tool.hasPendingGameplayResourceLoads(), false);
|
|
}
|
|
});
|
|
|
|
test('Block idle check includes queued, in-flight, decoding and whole background batches', () => {
|
|
const Block = loadClass('assets/Script/module/Tool/BlockAssetManager.ts', ['hasPendingLoads'], {});
|
|
const block = Object.assign(new Block(), {
|
|
foregroundLoads: 0, entryDownloadHolds: 0, backgroundRunning: false,
|
|
loading: new Map(), fileDownloads: new Map(), downloadQueue: [],
|
|
});
|
|
assert.equal(block.hasPendingLoads(), false);
|
|
for (const key of ['foregroundLoads', 'entryDownloadHolds']) {
|
|
block[key] = 1; assert.equal(block.hasPendingLoads(), true); block[key] = 0;
|
|
}
|
|
for (const key of ['loading', 'fileDownloads']) {
|
|
block[key].set('pending', {}); assert.equal(block.hasPendingLoads(), true); block[key].clear();
|
|
}
|
|
block.backgroundRunning = true; assert.equal(block.hasPendingLoads(), true); block.backgroundRunning = false;
|
|
block.downloadQueue.push({}); assert.equal(block.hasPendingLoads(), true);
|
|
});
|
|
|
|
test('intro waits for block queue, engine loading, popup downloads, user popup and stable idle', async () => {
|
|
const { state, cc, tool, manager, tick } = fixture();
|
|
manager.scheduleGameplayIntroPredownload();
|
|
manager.scheduleGameplayIntroPredownload();
|
|
assert.equal(state.schedules.length, 1);
|
|
state.blocksBusy = true; tick(); tick();
|
|
state.blocksBusy = false;
|
|
const task = { input: 'queued asset' };
|
|
cc.assetManager.pipeline.pipes[0](task, () => {}); tick();
|
|
task.onComplete(null, {});
|
|
tool._gameplayPopupSubpackagePreloadPromise = {}; tick();
|
|
tool._gameplayPopupSubpackagePreloadPromise = null;
|
|
tool._gameplayPopupMemoryWarmupPromise = {}; tick();
|
|
tool._gameplayPopupMemoryWarmupPromise = null;
|
|
manager.dynamicPopupLoads.pause = true; tick();
|
|
delete manager.dynamicPopupLoads.pause;
|
|
state.entryBusy = true; tick(); state.entryBusy = false;
|
|
tick(); assert.equal(state.requests.length, 0);
|
|
tick(); assert.equal(state.requests.length, 1);
|
|
assert.equal(state.requests[0].name, 'gameplay_intro');
|
|
state.requests[0].success(); await flush();
|
|
assert.equal(tool.isGameplayIntroDownloaded(), true);
|
|
assert.equal(state.bundles.size, 0, 'disk caching does not register a Bundle');
|
|
await tool.predownloadGameplayIntro();
|
|
assert.equal(state.requests.length, 1, 'successful session cache avoids duplicate download checks');
|
|
});
|
|
|
|
test('failure retries only after a delay and another idle check; leaving scene cancels pending work', async () => {
|
|
const { state, manager, tick } = fixture();
|
|
manager.scheduleGameplayIntroPredownload(); tick(); tick();
|
|
state.requests[0].fail(new Error('offline')); await flush();
|
|
assert.equal(state.schedules[0].delay, 10);
|
|
state.blocksBusy = true; tick(); tick();
|
|
assert.equal(state.requests.length, 1);
|
|
state.blocksBusy = false; tick(); tick();
|
|
assert.equal(state.requests.length, 2);
|
|
state.destroyed = true; state.requests[1].fail(new Error('offline')); await flush();
|
|
assert.equal(state.schedules.length, 0);
|
|
});
|
|
|
|
test('departing before idle never starts a download; old SDK does not execute a subpackage', () => {
|
|
const f = fixture(); f.manager.scheduleGameplayIntroPredownload();
|
|
f.state.destroyed = true; f.tick(); assert.equal(f.state.requests.length, 0);
|
|
const old = fixture(); delete old.wx.preDownloadSubpackage;
|
|
old.wx.loadSubpackage = () => assert.fail('must not execute scripts as background predownload');
|
|
old.manager.scheduleGameplayIntroPredownload(); assert.equal(old.state.schedules.length, 0);
|
|
});
|
|
|
|
test('concurrent callers share one download, and cached Bundle needs no predownload', async () => {
|
|
const { tool, state, bundle } = fixture();
|
|
const first = tool.predownloadGameplayIntro();
|
|
assert.equal(tool.predownloadGameplayIntro(), first);
|
|
state.requests[0].success(); assert.equal(await first, true);
|
|
const second = fixture(); second.bundle('gameplay_intro');
|
|
assert.equal(await second.tool.predownloadGameplayIntro(), true);
|
|
assert.equal(second.state.requests.length, 0);
|
|
});
|
|
|
|
test('scene reload releases intro resources while retaining ordinary popup bundles', () => {
|
|
const { state, manager, bundle } = fixture();
|
|
for (const [name, bundleName] of [['gameplayIntro', 'gameplay_intro'], ['pause', 'pause']]) {
|
|
const root = { active: true, removeFromParent() {}, destroy() { this.destroyed = true; } };
|
|
manager.dynamicPopupStates[name] = { bundleName, bundle: bundle(bundleName), root, callbacks: [], requestId: 1 };
|
|
}
|
|
manager.onDestroy();
|
|
assert.deepEqual(state.released, ['gameplay_intro']);
|
|
assert.equal(state.bundles.has('pause'), true);
|
|
assert.equal(state.bundles.has('gameplay_intro'), false);
|
|
});
|
|
|
|
test('unowned intro bundle is cleaned even with retention enabled and no popup state', () => {
|
|
const { state, manager, bundle } = fixture(); bundle('gameplay_intro'); manager.onDestroy();
|
|
assert.deepEqual(state.released, ['gameplay_intro']);
|
|
});
|
|
|
|
for (const owned of [false, true]) {
|
|
test(`late old-scene intro callback respects actual new-scene ownership: ${owned}`, () => {
|
|
const { state, manager, Manager, bundle } = fixture(); const intro = bundle('gameplay_intro');
|
|
if (owned) manager.dynamicPopupStates.gameplayIntro = { bundleName: 'gameplay_intro', loading: true };
|
|
Manager.releaseOrphanedPopupBundle('gameplay_intro', intro); state.timers.shift()();
|
|
assert.equal(state.bundles.has('gameplay_intro'), owned);
|
|
});
|
|
}
|
|
|
|
for (const mode of ['skip', 'cached', 'load', 'failure']) {
|
|
test(`low-memory warmup signals completion so idle download can start: ${mode}`, () => {
|
|
const { cc, tool, manager, bundle } = fixture(); let calls = 0, done;
|
|
tool.isIOSDevice = () => mode !== 'skip';
|
|
if (mode === 'cached') bundle('pause');
|
|
cc.assetManager.loadBundle = (name, callback) => { assert.equal(name, 'pause'); done = callback; };
|
|
manager.warmupGameplayPopupBundles(false, null, error => {
|
|
assert.equal(!!error, mode === 'failure'); calls++;
|
|
});
|
|
if (done) { assert.equal(calls, 0); done(mode === 'failure' ? new Error('offline') : null, {}); }
|
|
assert.equal(calls, 1);
|
|
});
|
|
}
|