Files
MatchMaster/tools/test-level-resource-entry.cjs

208 lines
15 KiB
JavaScript

const test = require('node:test'), assert = require('node:assert/strict');
const { loadMembers, loadModule } = require('./new-player-ab-test-support.cjs');
const file = 'assets/Script/JiaZai.ts';
const flush = async () => { for (let i = 0; i < 20; i++) await Promise.resolve(); };
for (const sceneName of ['HomeScene', 'GameScene']) {
test(`level read failure deduplicates modal, invalidates entry and confirms Home from ${sceneName}`, () => {
const modals = [], launches = [], scene = { name: sceneName }, transition = { active: true };
let closed = 0, released = 0, touch = false;
const home = { isShow: false, closeLoad() { closed++; } };
const canvas = { getComponent: () => home, getChildByName: () => transition };
const cc = { warn() {}, find: path => path === 'Canvas' ? canvas : { getComponent: () => ({ setTouch: value => touch = value }) },
director: { getScene: () => scene, loadScene(name, done) { launches.push(name); scene.name = name; done(); return true; } } };
const Config = loadMembers('assets/Script/module/Config/GameConfig.ts',
['showLevelReadError', 'levelReadModalOpen', 'levelLoadRequest', 'sceneLoadRequest', 'homeWarmupRequest'],
{ cc, wx: { showModal: options => modals.push(options) } });
Config.GM_INFO = { level: 300, hp: 5 };
Config.cancelBlockLoading = () => closed++;
Config.takeEntryDownloadRelease = () => () => released++;
Config.showBlockLoadError = () => assert.fail('native modal expected');
Config.showLevelReadError(Error('offline')); Config.showLevelReadError(Error('offline'));
assert.equal(modals.length, 1); assert.equal(launches.length, 0);
assert.equal(Config.levelLoadRequest, 1); assert.equal(Config.sceneLoadRequest, 1);
assert.equal(released, 1); assert.equal(modals[0].content, '关卡读取失败,请连接网络!');
assert.equal(modals[0].showCancel, false); assert.equal(modals[0].confirmText, '确定');
modals[0].success({ confirm: true }); modals[0].success({ confirm: true });
assert.equal(scene.name, 'HomeScene'); assert.equal(Config.levelReadModalOpen, false);
assert.deepEqual(Config.GM_INFO, { level: 300, hp: 5 });
if (sceneName === 'HomeScene') { assert.equal(touch, true); assert.equal(transition.active, false); assert.equal(home.isShow, true); }
else assert.deepEqual(launches, ['HomeScene']);
Config.showLevelReadError(Error('still offline')); assert.equal(modals.length, 2);
});
}
function entryFixture() {
let resolve, calls = 0, launches = 0;
const ready = new Promise(yes => resolve = yes), scene = { name: 'HomeScene' };
const cc = { isValid: n => !!n && !n.destroyed, warn() {}, director: { getScene: () => scene }, fx: {
GameConfig: { GM_INFO: { level: 2040, hp: 5, otherLevel: 0 } },
GameTool: { getWechatGameVersion: () => '正式版', getUserPowerTime: () => true, maxLevel: () => true },
AudioManager: { _instance: { playEffect() {} } },
} };
const Home = loadMembers(file, ['startGame', 'cancelLevelStart', 'cancelLevelStartOnTouch'], {
cc, MiniGameSdk: { API: { showToast() {} } },
LevelResources: { allReady: () => false, ensureAll: () => { calls++; return ready; } },
});
const home = new Home();
Object.assign(home, { node: { name: 'HomeScene' }, enabled: true, custom: { string: '' }, endlessStartIntent: 0,
waitingForLevels: false, preparingWucaiTransfer: false, hasPendingWucaiEntry: () => false, vibrateButtonClick() {} });
const click = () => Home.prototype.startGame.call(home);
home.startGame = () => launches++;
return { home, click, resolve, scene, get calls() { return calls; }, get launches() { return launches; } };
}
test('infinite start deduplicates clicks and enters exactly once after complete pool', async () => {
const f = entryFixture(); f.click(); f.click(); assert.equal(f.calls, 1); assert.equal(f.launches, 0);
f.resolve(); await flush(); assert.equal(f.launches, 1);
});
test('another Home action, destruction or scene departure cancels automatic entry without cancelling preparation', async () => {
for (const cancel of [f => f.home.cancelLevelStartOnTouch({ target: { name: 'shop', parent: f.home.node } }),
f => f.home.node.destroyed = true, f => f.scene.name = 'GameScene']) {
const f = entryFixture(); f.click(); cancel(f); f.resolve(); await flush(); assert.equal(f.launches, 0);
}
});
test('a repeated touch on Start does not cancel its pending intent', async () => {
const f = entryFixture(); f.click();
f.home.cancelLevelStartOnTouch({ target: { name: 'label', parent: { name: 'startBtn', parent: f.home.node } } });
f.resolve(); await flush(); assert.equal(f.launches, 1);
});
test('dynamic maxLevel preserves completed progress and clamps only the random candidate range', () => {
const cc = { fx: { GameConfig: { GM_INFO: { level: 2060 } } } };
const Tool = loadMembers('assets/Script/module/Tool/GameTool.ts', ['maxLevel', 'getPurelyRandom'], { cc });
const tool = new Tool(); tool.MAX_NORMAL_LEVEL = 2040;
assert.equal(tool.maxLevel(), true); assert.equal(cc.fx.GameConfig.GM_INFO.level, 2060);
for (let n = 0; n < 100; n++) assert(tool.getPurelyRandom() <= 2010);
tool.MAX_NORMAL_LEVEL = 2080; assert.equal(tool.maxLevel(), false); assert.equal(cc.fx.GameConfig.GM_INFO.level + 1, 2061);
});
test('adapter applies login policy once, leaves preview local, and routes help to A', async () => {
const calls = [], info = { version: '1.0.96', level: 59, otherLevel: 0 };
const runtime = { beginLogin: data => { calls.push(data); return { maxLevel: 2060 }; },
limits: () => ({ levelGroup: 'B' }), load: async (id, group) => { calls.push([id, group]); return { data: { id, group } }; } };
const cc = { sys: { platform: 'wechat', WECHAT_GAME: 'wechat' }, fx: { GameTool: {}, GameConfig: { GM_INFO: info } } };
const window = { __levelResources: runtime };
const Adapter = loadModule('assets/Script/module/Config/LevelResources.ts', {}, { window, cc }).default;
Adapter.beginLogin({ maxLevel: 2060, bLevelCount: 80, levelGroup: 'B' }, info.version);
assert.equal(cc.fx.GameTool.MAX_NORMAL_LEVEL, 2060);
assert.equal((await Adapter.load(60, true, () => assert.fail('local fallback'))).group, 'B');
assert.equal((await Adapter.load(60, false, () => assert.fail('local fallback'))).group, 'A');
delete window.__levelResources; cc.sys.platform = 'web';
assert.equal(await Adapter.load(3000, true, async () => 'local preview'), 'local preview');
});
function scenePerfFixture(enabled = true) {
let time = 0; const rows = [], calls = [];
const perf = require('../packages/level-resources/perf').create({ enabled, now: () => time,
output: line => rows.push(JSON.parse(line.slice('[LevelPerf] '.length))) });
const oldPrototype = { destroy() { assert.equal(this.name, 'HomeScene'); calls.push('destroy'); time += 5; return 9; } };
const old = Object.assign(Object.create(oldPrototype), { name: 'HomeScene' });
const scenePrototype = {
_load() { assert.equal(this.name, 'GameScene'); calls.push('initialize'); time += 3; },
_activate() { calls.push('onLoad/onEnable'); time += 11; }
};
const scene = Object.assign(Object.create(scenePrototype), { name: 'GameScene' });
const releaseManager = { _autoRelease() { calls.push('release'); time += 4; } };
const object = { _deferredDestroy() { calls.push('onDestroy'); time += 7; } };
const director = { getScene: () => old, runSceneImmediate(incoming, before, launched) {
const target = incoming.scene || incoming;
target._load(); time += 2; releaseManager._autoRelease(); assert.equal(old.destroy(), 9);
object._deferredDestroy(); if (before) before(); target._activate();
if (launched) launched(null, target); return 42;
} };
const cc = { director, assetManager: { _releaseManager: releaseManager }, Object: object };
const Adapter = loadModule('assets/Script/module/Config/LevelResources.ts', {}, { cc, window: { __levelPerf: perf } }).default;
const originals = { run: director.runSceneImmediate, release: releaseManager._autoRelease, destroy: object._deferredDestroy };
const restored = () => {
assert.equal(director.runSceneImmediate, originals.run);
assert.equal(releaseManager._autoRelease, originals.release); assert.equal(object._deferredDestroy, originals.destroy);
assert.equal(Object.hasOwn(scene, '_load'), false); assert.equal(Object.hasOwn(scene, '_activate'), false);
assert.equal(Object.hasOwn(old, 'destroy'), false);
};
return { Adapter, cc, perf, director, scene, rows, calls, restored, advance: ms => time += ms };
}
test('scene resource diagnostics follow tagged dependencies, preserve callbacks and exclude unrelated loads', () => {
const f = scenePerfFixture(), manager = f.cc.assetManager, callbacks = {}, options = { preset: 'scene' };
let tagged, callbackCount = 0, progressCount = 0;
manager.loadAny = function (request, opts, progress) { assert.equal(this, manager); tagged = opts; progress(1, 2, {}); return 17; };
manager.packManager = { load(item, opts, callback) { assert.equal(this, manager.packManager); callbacks.fetch = callback; return 18; } };
manager.downloader = { download(id, url, type, opts, callback) { callbacks.download = callback; return 19; } };
manager.parser = { parse(id, file, type, opts, callback) { callbacks.parse = callback; return 20; } };
manager.pipeline = { async(task) { callbacks.dependencies = () => task.onComplete.call(task, null); return 21; } };
const originals = [manager.loadAny, manager.packManager.load, manager.downloader.download, manager.parser.parse, manager.pipeline.async];
const stop = f.Adapter.traceSceneLoad('GameScene', { requestId: 91 });
assert.equal(manager.loadAny({ scene: 'GameScene' }, options, () => progressCount++, () => {}), 17);
assert.equal(progressCount, 1); assert.equal(options.__levelSceneTrace__, undefined, 'caller options stay untouched');
const item = { id: 'image@native', uuid: 'image', url: 'subpackages/main/image.png?secret=hidden', ext: '.png', info: { path: 'game/image' }, config: { name: 'main' } };
const done = function (error, value) { assert.equal(this, callbacks); assert.equal(error, null); assert.equal(value, 42); callbackCount++; };
assert.equal(manager.packManager.load(item, { ...tagged }, done), 18);
assert.equal(manager.downloader.download(item.id, item.url, '.png', { ...tagged }, done), 19);
f.advance(300); callbacks.download.call(callbacks, null, 42); callbacks.fetch.call(callbacks, null, 42);
assert.equal(manager.parser.parse(item.id, {}, '.png', { ...tagged }, done), 20);
f.advance(50); callbacks.parse.call(callbacks, null, 42);
const task = { input: [{ uuid: 'dep' }], options: { ...tagged }, onComplete(error) { assert.equal(this, task); assert.equal(error, null); callbackCount++; } };
const complete = task.onComplete;
assert.equal(manager.pipeline.async(task), 21); f.advance(25); callbacks.dependencies();
assert.equal(task.onComplete, complete);
manager.parser.parse('unrelated', {}, 'import', {}, () => callbackCount++); callbacks.parse();
assert.equal(callbackCount, 5);
stop({ status: 'ok' }); f.restored();
assert.deepEqual([manager.loadAny, manager.packManager.load, manager.downloader.download, manager.parser.parse, manager.pipeline.async], originals);
const summary = f.rows.find(row => row.event === 'summary' && row.scene);
assert.equal(summary.metrics['scene.resource.fetch'].maxMs, 300);
assert.equal(summary.metrics['scene.resource.download'].maxMs, 300);
assert.equal(summary.metrics['scene.resource.parse'].count, 1);
assert.equal(summary.metrics['scene.resource.parse'].maxMs, 50);
assert.equal(summary.metrics['scene.resource.dependencies'].maxMs, 25);
assert.equal(summary.scene['scene.resource.parse'][0].path, 'game/image');
assert(!JSON.stringify(f.rows).includes('secret'));
assert(!f.rows.some(row => row.event === 'begin' && row.stage.startsWith('scene.resource.')));
});
test('scene resource diagnostics close pending spans on failure and preserve late callbacks', () => {
const f = scenePerfFixture(), manager = f.cc.assetManager;
let tagged, callback, calls = 0;
manager.loadAny = (_request, options) => { tagged = options; };
manager.parser = { parse(_id, _file, _type, _options, done) { callback = done; } };
const original = manager.parser.parse, stop = f.Adapter.traceSceneLoad('GameScene');
manager.loadAny({ scene: 'GameScene' }, {}, null, () => {});
manager.parser.parse('slow', {}, 'import', tagged, () => calls++);
f.advance(3000); f.perf.flush();
const pending = f.rows.find(row => row.event === 'summary').pending.find(row => row.stage === 'scene.resource.parse');
assert.equal(pending.resource, 'slow'); assert.equal(pending.elapsedMs, 3000);
stop({ status: 'error' }); assert.equal(manager.parser.parse, original);
callback(null, {}); assert.equal(calls, 1);
f.perf.flush(); assert.equal(f.rows.filter(row => row.event === 'summary').at(-1).pendingCount, 0);
});
test('scene diagnostics split resource waiting and engine phases without changing callbacks or order', () => {
const f = scenePerfFixture(), done = f.Adapter.traceSceneLoad('GameScene', { requestId: 7 });
f.advance(100); let callbackCount = 0;
assert.equal(f.director.runSceneImmediate({ scene: f.scene }, () => f.calls.push('before'), (error, scene) => {
assert.equal(error, null); assert.equal(scene, f.scene); callbackCount++; done({ status: 'ok' });
}), 42);
done(); f.restored(); assert.equal(callbackCount, 1);
assert.deepEqual(f.calls, ['initialize', 'release', 'destroy', 'onDestroy', 'before', 'onLoad/onEnable']);
const ends = f.rows.filter(row => row.event === 'end');
const expected = { resourceLoad: 100, 'initialize.sync': 3, 'releaseAssets.sync': 4,
'destroyOldScene.sync': 5, 'deferredDestroy.sync': 7, 'activate.sync': 11, 'switch.sync': 32 };
assert.equal(ends.length, Object.keys(expected).length);
for (const [part, ms] of Object.entries(expected)) {
const row = ends.find(row => row.stage === 'scene.GameScene.' + part);
assert.equal(row.ms, ms); assert.equal(row.requestId, undefined); // request identity is in its paired begin.
assert.equal(row.status, 'ok');
}
});
test('scene diagnostics restore hooks on load rejection and thrown lifecycle, and disabled logging installs nothing', () => {
const rejected = scenePerfFixture(), stop = rejected.Adapter.traceSceneLoad('GameScene');
stop({ status: 'rejected' }); rejected.restored();
assert(rejected.rows.some(row => row.event === 'end' && row.status === 'rejected'));
const f = scenePerfFixture(); const sentinel = Error('lifecycle failed');
f.scene._activate = () => { throw sentinel; };
const originalActivate = f.scene._activate;
const finish = f.Adapter.traceSceneLoad('GameScene');
assert.throws(() => f.director.runSceneImmediate(f.scene), error => error === sentinel); finish({ status: 'error' });
assert.equal(f.scene._activate, originalActivate); delete f.scene._activate; f.restored();
assert(f.rows.some(row => row.stage === 'scene.GameScene.activate.sync' && row.status === 'error'));
const disabled = scenePerfFixture(false); disabled.Adapter.traceSceneLoad('GameScene')(); disabled.restored(); assert.equal(disabled.rows.length, 0);
});