174 lines
7.7 KiB
JavaScript
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/);
|
|
});
|
|
}
|