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> = {}) { const base: Record = { '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/); }); });