Files
MatchMaster/tools/test-daily-quests-loading-lifecycle.cjs

159 lines
7.7 KiB
JavaScript

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 file = path.join(__dirname, '../assets/Script/JiaZai.ts');
const source = ts.createSourceFile(file, fs.readFileSync(file, 'utf8'), ts.ScriptTarget.Latest, true);
const declaration = source.statements.find(ts.isClassDeclaration);
const methods = ['isHomeRuntimeAlive', 'openDailyQuests'].map(name => {
const method = declaration.members.find(item => item.name?.getText(source) === name);
assert.ok(method, name);
return method.getText(source);
}).join('\n');
function fixture(cached = false) {
const state = { scene: 'HomeScene', preparing: false, requests: 0, bundleLoads: 0, prefabLoads: 0,
created: 0, attached: 0, initialized: [], closed: 0, opened: 0, errors: [], toasts: [], canPresent: true, shown: 0 };
const pending = {};
const prefab = {};
const task = { daily: 'fixture' };
const response = { code: 1, data: { task } };
const cc = {
isValid: (value, strict) => !!value && !value.destroyed && !(strict && value.destroyPending),
director: { getScene: () => ({ name: state.scene }) }, Prefab: class {},
error: error => state.errors.push(error),
instantiate(asset) {
assert.equal(asset, prefab);
state.created++;
return { active: true, getComponent: () => ({ init: data => state.initialized.push(data) }) };
},
fx: { GameConfig: { GM_INFO: { level: 40 }, isNewPlayerB: () => false,
isGameplayEntryPreparing: () => state.preparing },
GameTool: { shushu_Track() {}, getDailyQuestsInfo(callback) { state.requests++; pending.response = callback; } } },
};
const Home = vm.runInNewContext(ts.transpileModule(`class JiaZai { ${methods} }; JiaZai;`, {
compilerOptions: { target: ts.ScriptTarget.ES2018 },
}).outputText, { cc, console: { log() {} }, MiniGameSdk: { API: { showToast: text => state.toasts.push(text) } },
HomePopupQueue: { get(node) {
assert.ok(cc.isValid(node, true), 'queue cannot be accessed through a destroyed node');
return { canPresent: () => state.canPresent, markShown: () => state.shown++ };
} } });
if (cached) Home.cachedDailyQuestsPrefab = prefab;
const home = new Home();
home.node = { addChild() { state.attached++; } };
home.vibrateButtonClick = () => {};
home.openLoad2 = () => state.opened++;
home.closeLoad = () => state.closed++;
home.loadHomeBundleWithDependencies = (_name, _priority, callback) => { state.bundleLoads++; pending.bundle = callback; };
const bundle = { load(_path, _type, callback) { state.prefabLoads++; pending.prefab = callback; } };
const finish = stage => {
if (stage === 'response') pending.response(response);
if (stage === 'bundle') pending.bundle(null, bundle);
if (stage === 'prefab') pending.prefab(null, prefab);
};
return { home, Home, state, pending, prefab, task, finish };
}
const invalidations = {
'node cleared': f => { f.home.node = null; },
'node destroyed': f => { f.home.node.destroyed = true; },
'node pending destruction': f => { f.home.node.destroyPending = true; },
'component destroyed': f => { f.home.destroyed = true; },
'component pending destruction': f => { f.home.destroyPending = true; },
'scene changed': f => { f.state.scene = 'GameScene'; },
'entering gameplay': f => { f.state.preparing = true; },
};
for (const stage of ['entry', 'response', 'bundle', 'prefab']) {
for (const [name, invalidate] of Object.entries(invalidations)) test(`${stage}: ${name} cannot open daily quests`, () => {
const f = fixture();
if (stage !== 'entry') f.home.openDailyQuests();
if (stage === 'bundle' || stage === 'prefab') f.finish('response');
if (stage === 'prefab') f.finish('bundle');
const closed = f.state.closed;
invalidate(f);
assert.doesNotThrow(() => stage === 'entry' ? f.home.openDailyQuests() : f.finish(stage));
assert.equal(f.state.created, 0);
assert.equal(f.state.attached, 0);
assert.equal(f.state.closed, closed, 'stale callbacks cannot touch loading UI');
if (stage === 'entry') assert.equal(f.state.requests, 0);
if (stage === 'response') assert.equal(f.state.bundleLoads, 0);
if (stage === 'bundle') assert.equal(f.state.prefabLoads, 0);
if (stage === 'prefab') assert.equal(f.Home.cachedDailyQuestsPrefab, f.prefab, 'completed resources remain reusable');
});
}
for (const cached of [true, false]) test(`live daily quests opens and reuses its node, cached=${cached}`, () => {
const f = fixture(cached);
f.home.openDailyQuests();
f.home.openDailyQuests();
assert.equal(f.state.requests, 1, 'pending API request is not duplicated');
f.finish('response');
if (!cached) { f.finish('bundle'); f.finish('prefab'); }
assert.equal(f.state.created, 1);
assert.equal(f.state.attached, 1);
assert.equal(f.state.initialized[0], f.task);
f.home.dailyQuestsNode.active = false;
f.home.openDailyQuests(); f.finish('response');
assert.equal(f.home.dailyQuestsNode.active, true);
assert.equal(f.state.created, 1);
assert.equal(f.state.initialized.length, 2);
});
for (const stage of ['response', 'bundle', 'prefab']) test(`${stage} failure allows a later retry`, () => {
const f = fixture();
f.home.openDailyQuests();
if (stage !== 'response') f.finish('response');
if (stage === 'prefab') f.finish('bundle');
if (stage === 'response') f.pending.response({ code: 0, data: null });
else f.pending[stage](new Error('offline'));
assert.equal(f.state.created, 0);
assert.equal(f.state.errors.length + f.state.toasts.length, 1);
f.home.openDailyQuests(); f.finish('response'); f.finish('bundle'); f.finish('prefab');
assert.equal(f.state.created, 1);
assert.equal(f.state.attached, 1);
});
for (const stage of ['entry', 'response', 'bundle', 'prefab']) {
for (const [name, invalidate] of Object.entries({ ...invalidations,
'queue no longer permits presentation': f => { f.state.canPresent = false; },
})) test(`automatic ${stage}: ${name} releases queue without marking shown`, () => {
const f = fixture();
let done = 0;
const open = () => f.home.openDailyQuests(undefined, undefined, undefined, () => done++);
if (stage !== 'entry') open();
if (stage === 'bundle' || stage === 'prefab') f.finish('response');
if (stage === 'prefab') f.finish('bundle');
invalidate(f);
if (stage === 'entry') open(); else f.finish(stage);
assert.equal(done, 1);
assert.equal(f.state.shown, 0);
assert.equal(f.state.created, 0);
});
}
for (const cached of [false, true]) test(`automatic success marks shown and releases queue, cached=${cached}`, () => {
const f = fixture(cached);
let done = 0;
f.home.openDailyQuests(undefined, undefined, undefined, () => done++);
f.finish('response');
if (!cached) { assert.equal(done, 0); f.finish('bundle'); f.finish('prefab'); }
assert.equal(f.state.attached, 1);
assert.equal(f.state.shown, 1);
assert.equal(done, 1);
});
for (const stage of ['response', 'bundle', 'prefab']) test(`automatic ${stage} failure releases queue`, () => {
const f = fixture();
let done = 0;
f.home.openDailyQuests(undefined, undefined, undefined, () => done++);
if (stage !== 'response') f.finish('response');
if (stage === 'prefab') f.finish('bundle');
if (stage === 'response') f.pending.response({ code: 0, data: null });
else f.pending[stage](new Error('offline'));
assert.equal(done, 1);
assert.equal(f.state.shown, 0);
});