first commmit
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
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/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user