Files
KateLegend2_proj/tests/data/ConfigMgr.test.ts
T
2026-05-06 08:17:32 +08:00

125 lines
5.6 KiB
TypeScript

import { ConfigMgr, MapJsonLoader } from '@data/ConfigMgr';
import { EnemyType, ItemType, WeaponType } from '@data/Interfaces';
// Import the real JSON delivered by task 2.1 — if these files are malformed
// the test suite will catch it on CI before any Cocos editor run.
import enemies from '../../assets/resources/configs/enemies.json';
import items from '../../assets/resources/configs/items.json';
import weapons from '../../assets/resources/configs/weapons.json';
import levels from '../../assets/resources/configs/levels.json';
import bosses from '../../assets/resources/configs/bosses.json';
import stories from '../../assets/resources/configs/stories.json';
function makeLoader(overrides: Partial<Record<string, unknown>> = {}) {
const base: Record<string, unknown> = {
'configs/enemies': enemies,
'configs/items': items,
'configs/weapons': weapons,
'configs/levels': levels,
'configs/bosses': bosses,
'configs/stories': stories,
};
return new MapJsonLoader({ ...base, ...overrides });
}
describe('ConfigMgr — happy path with delivered JSON', () => {
it('loads and validates the chapter-1 bundle', async () => {
const mgr = new ConfigMgr(makeLoader());
const bundle = await mgr.load();
expect(bundle.enemies.length).toBe(4);
expect(bundle.items.length).toBe(5);
expect(bundle.weapons.length).toBe(2);
expect(bundle.levels.length).toBe(5);
expect(bundle.bosses.length).toBe(1);
expect(bundle.stories.length).toBe(1);
});
it('resolves every enemy, item, weapon, level, boss, and story by id', async () => {
const mgr = new ConfigMgr(makeLoader());
await mgr.load();
expect(mgr.enemy(EnemyType.QingRen).displayName).toBe('青忍');
expect(mgr.item(ItemType.CrystalJade).displayName).toBe('水晶玉');
expect(mgr.weapon(WeaponType.NinjaSword).canParry).toBe(true);
expect(mgr.level('1-5').objective.kind).toBe('defeat_boss');
expect(mgr.boss('shuang_huan_fang').phases.length).toBe(3);
expect(mgr.story('chapter_1_intro').pages.length).toBe(3);
});
it('throws when accessed before load()', () => {
const mgr = new ConfigMgr(makeLoader());
expect(() => mgr.enemy(EnemyType.QingRen)).toThrow(/load\(\)/);
});
});
describe('ConfigMgr — validation rejects malformed configs', () => {
it('rejects a config bundle that contains the forbidden "casual" token (req 13.6)', async () => {
const polluted = JSON.parse(JSON.stringify(levels));
polluted[0].displayName = 'casual'; // inject disallowed token
const mgr = new ConfigMgr(makeLoader({ 'configs/levels': polluted }));
await expect(mgr.load()).rejects.toThrow(/casual/);
});
it('rejects an enemy entry missing required fields', async () => {
const bad = [{ id: 'qing_ren' }];
const mgr = new ConfigMgr(makeLoader({ 'configs/enemies': bad }));
await expect(mgr.load()).rejects.toThrow(/missing field/);
});
it('rejects an enemy with an unknown EnemyType id', async () => {
const bad = JSON.parse(JSON.stringify(enemies));
bad[0].id = 'not_a_real_enemy';
const mgr = new ConfigMgr(makeLoader({ 'configs/enemies': bad }));
await expect(mgr.load()).rejects.toThrow(/EnemyType/);
});
it('rejects a level referencing an unknown boss', async () => {
const bad = JSON.parse(JSON.stringify(levels));
bad[4].objective.bossId = 'ghost_boss';
const mgr = new ConfigMgr(makeLoader({ 'configs/levels': bad }));
await expect(mgr.load()).rejects.toThrow(/unknown boss/);
});
it('rejects a level spawn referencing an unknown enemy', async () => {
const bad = JSON.parse(JSON.stringify(levels));
bad[0].enemySpawns[0].type = 'white_ninja';
const mgr = new ConfigMgr(makeLoader({ 'configs/levels': bad }));
await expect(mgr.load()).rejects.toThrow(/unknown enemy/);
});
it('rejects boss phases that are not monotonically descending', async () => {
const bad = JSON.parse(JSON.stringify(bosses));
bad[0].phases = [
{ hpThreshold: 0.33, mode: 'a', actionIntervalSec: 1 },
{ hpThreshold: 1.0, mode: 'b', actionIntervalSec: 1 },
];
const mgr = new ConfigMgr(makeLoader({ 'configs/bosses': bad }));
await expect(mgr.load()).rejects.toThrow(/descending hpThreshold/);
});
it('rejects a story with fewer than 3 pages (req 19.2)', async () => {
const bad = JSON.parse(JSON.stringify(stories));
bad[0].pages = bad[0].pages.slice(0, 2);
const mgr = new ConfigMgr(makeLoader({ 'configs/stories': bad }));
await expect(mgr.load()).rejects.toThrow(/≥3 pages/);
});
it('rejects a story whose maxDurationSec exceeds the 30s budget (req 19.1)', async () => {
const bad = JSON.parse(JSON.stringify(stories));
bad[0].maxDurationSec = 45;
const mgr = new ConfigMgr(makeLoader({ 'configs/stories': bad }));
await expect(mgr.load()).rejects.toThrow(/30s budget/);
});
it('rejects a story with non-contiguous page indices', async () => {
const bad = JSON.parse(JSON.stringify(stories));
bad[0].pages[1].index = 5;
const mgr = new ConfigMgr(makeLoader({ 'configs/stories': bad }));
await expect(mgr.load()).rejects.toThrow(/contiguous/);
});
it('rejects an empty enemies list', async () => {
const mgr = new ConfigMgr(makeLoader({ 'configs/enemies': [] }));
await expect(mgr.load()).rejects.toThrow(/enemies list is empty/);
});
});