MatchMaster/tools/test-career-loading.cjs

174 lines
7.7 KiB
JavaScript

// Run: node --test tools/test-career-loading.cjs
// Set JIAZAI_TEST_REF=HEAD to reproduce the regression against committed source.
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(process.env.TYPESCRIPT_PATH || 'typescript');
const root = path.resolve(__dirname, '..');
const filename = 'assets/Script/JiaZai.ts';
const source = process.env.JIAZAI_TEST_REF
? require('node:child_process').execFileSync('git', ['show', `${process.env.JIAZAI_TEST_REF}:${filename}`], { cwd: root, encoding: 'utf8' })
: fs.readFileSync(path.join(root, filename), 'utf8');
const parsed = ts.createSourceFile(filename, source, ts.ScriptTarget.Latest, true);
const declaration = parsed.statements.find(node => ts.isClassDeclaration(node) && node.name.text === 'JiaZai');
const methods = ['LoadCareer', 'getSRank', 'isHomeBundleReady', 'loadHomeBundleWithDependencies'].map(name => {
const method = declaration.members.find(node => ts.isMethodDeclaration(node) && node.name.getText(parsed) === name);
assert.ok(method, `JiaZai.${name} must exist`);
return method.getText(parsed);
});
const compiled = ts.transpileModule(`class Subject { ${methods.join('\n')} }\nSubject;`, {
compilerOptions: { target: ts.ScriptTarget.ES2017 },
}).outputText;
function fixture(cachedAction = false) {
const state = { bundles: new Map(), bundleLoads: [], prefabLoads: [], errors: [], requests: 0, instances: 0,
lowMemory: false, dependencies: {}, bundleErrors: {} };
const pendingAction = [];
const action = { name: 'action_bundle' };
if (cachedAction) state.bundles.set(action.name, action);
const cc = {
Prefab: class {},
warn: (...args) => state.errors.push(args.map(String).join(' ')),
isValid: node => !!node && node.valid !== false,
error: (...args) => state.errors.push(args.map(String).join(' ')),
instantiate: prefab => { state.instances++; return { prefab }; },
assetManager: {
getBundle: name => state.bundles.get(name),
loadBundle(name, options, done) {
state.bundleLoads.push(name);
if (state.bundleErrors[name]) return done(state.bundleErrors[name]);
if (name === 'action_bundle') return pendingAction.push(done);
const bundle = {
name, deps: state.dependencies[name] || [],
load(asset, type, loaded) {
state.prefabLoads.push(asset);
assert.equal(type, cc.Prefab);
// Model Cocos refusing to resolve cross-bundle prefab dependencies.
if (!state.bundles.has('action_bundle')) {
return loaded(new Error('Please load bundle action_bundle first'));
}
loaded(null, { asset });
},
};
state.bundles.set(name, bundle);
done(null, bundle);
},
},
};
const Subject = vm.runInNewContext(compiled, { cc, Utils: { getSRank: () => { state.requests++; } } });
const subject = Object.assign(new Subject(), {
node: { valid: true }, RankNode: null, isShow: true,
careerLoadGeneration: 0, _sRankRequestId: 0,
isCareerDisabledByMemoryWarning: () => state.lowMemory,
});
return {
subject, state,
finishAction(error = null) {
assert.ok(pendingAction.length, 'action_bundle must have a pending load');
if (!error) state.bundles.set(action.name, action);
for (const done of pendingAction.splice(0)) done(error, error ? undefined : action);
},
};
}
test('cold first entry waits for action_bundle before loading rankGame and requesting ranks', () => {
const { subject, state, finishAction } = fixture();
subject.getSRank(true);
assert.deepEqual(state.bundleLoads, ['action_bundle']);
assert.deepEqual(state.prefabLoads, []);
assert.equal(state.instances, 0);
assert.equal(state.requests, 0);
finishAction();
assert.deepEqual(state.bundleLoads, ['action_bundle', 'career']);
assert.deepEqual(state.prefabLoads, ['prefab/rankGame']);
assert.equal(state.instances, 1);
assert.equal(state.requests, 1);
assert.deepEqual(state.errors, []);
});
test('cached action_bundle allows the rank load to continue immediately', () => {
const { subject, state } = fixture(true);
subject.getSRank(true);
assert.deepEqual(state.bundleLoads, ['career']);
assert.deepEqual(state.prefabLoads, ['prefab/rankGame']);
assert.equal(state.instances, 1);
assert.equal(state.requests, 1);
assert.deepEqual(state.errors, []);
});
test('failed action_bundle load reports the error and does not continue', () => {
const { subject, state, finishAction } = fixture();
subject.getSRank(true);
finishAction(new Error('dependency download failed'));
assert.deepEqual(state.bundleLoads, ['action_bundle']);
assert.deepEqual(state.prefabLoads, []);
assert.equal(state.instances, 0);
assert.equal(state.requests, 0);
assert.match(state.errors.join('\n'), /dependency download failed/);
});
for (const [name, invalidate] of [
['destroyed node', ({ subject }) => { subject.node.valid = false; }],
['missing node', ({ subject }) => { subject.node = null; }],
['new load generation', ({ subject }) => { subject.careerLoadGeneration++; }],
['memory warning', ({ state }) => { state.lowMemory = true; }],
]) {
test(`action_bundle completion cannot continue after ${name}`, () => {
const context = fixture();
context.subject.getSRank(true);
invalidate(context);
context.finishAction();
assert.deepEqual(context.state.bundleLoads, ['action_bundle']);
assert.deepEqual(context.state.prefabLoads, []);
assert.equal(context.state.instances, 0);
assert.equal(context.state.requests, 0);
});
}
test('getSRank(false) still only preloads; showing the preloaded rank requests once', () => {
const { subject, state, finishAction } = fixture();
subject.getSRank(false);
assert.equal(state.requests, 0);
finishAction();
assert.equal(state.instances, 1);
assert.equal(state.requests, 0);
subject.getSRank(true);
assert.equal(state.requests, 1);
assert.equal(state.instances, 1);
assert.deepEqual(state.bundleLoads, ['action_bundle', 'career']);
});
test('memory warning before entry prevents all rank loading', () => {
const { subject, state } = fixture();
state.lowMemory = true;
subject.getSRank(true);
assert.deepEqual(state.bundleLoads, []);
assert.equal(state.requests, 0);
});
test('cached action_bundle still waits for its own declared dependencies before Career', () => {
const { subject, state } = fixture(true);
state.bundles.get('action_bundle').deps = ['shop'];
state.dependencies.shop = ['UI'];
subject.getSRank(true);
assert.deepEqual(state.bundleLoads, ['shop', 'UI', 'career']);
assert.equal(state.instances, 1);
assert.deepEqual(state.errors, []);
});
for (const owner of ['action_bundle', 'career']) {
test(`${owner} dependency failure never parses or instantiates Career`, () => {
const { subject, state } = fixture(true);
if (owner === 'action_bundle') state.bundles.get(owner).deps = ['shop'];
else state.dependencies.career = ['shop'];
state.bundleErrors.shop = new Error('shop dependency offline');
subject.getSRank(true);
assert.deepEqual(state.prefabLoads, []);
assert.equal(state.instances, 0);
assert.equal(state.requests, 0);
assert.match(state.errors.join('\n'), /shop dependency offline/);
});
}