test(06-03): add Enemy attribute and special unit tests (stage 1)

- enemy.test.ts: attribute get/set/add independence and cloneAttributes deep copy
- special.test.ts: CommonSerializableSpecial value/name/description and NonePropertySpecial units
- fixtures inline, legacy and save/load excluded per D-30/D-32
This commit is contained in:
unanmed 2026-09-14 14:32:40 +08:00
parent e03fd3cb69
commit 965868c219
2 changed files with 250 additions and 0 deletions

View File

@ -0,0 +1,114 @@
// 测试 Enemy 数据模型:属性单元、特殊属性增删查、克隆与复制行为
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
import { type IEnemy } from './types';
vi.hoisted(() => {
vi.stubGlobal('main', { replayChecking: true });
vi.stubGlobal('location', { origin: 'http://localhost' });
Map.prototype.getOrInsertComputed ??= function <K, V>(
this: Map<K, V>,
key: K,
callback: (key: K) => V
): V {
const existing = this.get(key);
if (existing !== undefined) return existing;
const value = callback(key);
this.set(key, value);
return value;
};
});
interface TestModules {
Enemy: typeof import('./enemy').Enemy;
CommonSerializableSpecial: typeof import('./special').CommonSerializableSpecial;
logger: typeof import('@motajs/common').logger;
}
let modules: TestModules;
beforeAll(async () => {
vi.stubGlobal('main', { replayChecking: true });
vi.stubGlobal('location', { origin: 'http://localhost' });
const enemyModule = await import('./enemy');
const specialModule = await import('./special');
const commonModule = await import('@motajs/common');
modules = {
Enemy: enemyModule.Enemy,
CommonSerializableSpecial: specialModule.CommonSerializableSpecial,
logger: commonModule.logger
};
});
afterAll(() => {
vi.unstubAllGlobals();
});
interface IEnemyTestAttr {
hp: number;
atk: number;
tags: string[];
}
/**
* 构造一个带合成属性的怪物对象
* @param id 怪物标识符
* @param code 怪物图块数字
* @param attributes 初始属性
*/
function createEnemy(
id = 'enemy-1',
code = 1,
attributes: IEnemyTestAttr = { hp: 20, atk: 8, tags: ['base'] }
): IEnemy<IEnemyTestAttr> {
return new modules.Enemy<IEnemyTestAttr>(
id,
code,
structuredClone(attributes)
);
}
describe('Enemy attribute units', () => {
// 验证 getAttribute 按单个属性键返回构造时写入的初值
it('reads each attribute key independently', () => {
const enemy = createEnemy();
expect(enemy.getAttribute('hp')).toBe(20);
expect(enemy.getAttribute('atk')).toBe(8);
expect(enemy.getAttribute('tags')).toEqual(['base']);
});
// 验证 setAttribute 只修改指定键且不影响其它属性
it('updates only the selected attribute key', () => {
const enemy = createEnemy();
enemy.setAttribute('hp', 35);
expect(enemy.getAttribute('hp')).toBe(35);
expect(enemy.getAttribute('atk')).toBe(8);
expect(enemy.getAttribute('tags')).toEqual(['base']);
});
// 验证 addAttribute 对数字键做增减且支持负值
it('adds positive and negative deltas to a numeric key', () => {
const enemy = createEnemy();
enemy.addAttribute('hp', 5);
enemy.addAttribute('atk', -3);
expect(enemy.getAttribute('hp')).toBe(25);
expect(enemy.getAttribute('atk')).toBe(5);
});
// 验证 cloneAttributes 返回深拷贝而非内部属性的别名
it('returns a deep copy instead of an internal alias', () => {
const enemy = createEnemy();
const cloned = enemy.cloneAttributes();
cloned.hp = 999;
cloned.tags.push('mutated');
expect(enemy.getAttribute('hp')).toBe(20);
expect(enemy.getAttribute('tags')).toEqual(['base']);
expect(enemy.cloneAttributes()).not.toBe(enemy.cloneAttributes());
});
});

View File

@ -0,0 +1,136 @@
// 测试 special 数据模型:可序列化特殊属性的数值/名称/描述单元,以及无属性特殊属性
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
import { type ICommonSpecialConfig } from './special';
vi.hoisted(() => {
vi.stubGlobal('main', { replayChecking: true });
vi.stubGlobal('location', { origin: 'http://localhost' });
Map.prototype.getOrInsertComputed ??= function <K, V>(
this: Map<K, V>,
key: K,
callback: (key: K) => V
): V {
const existing = this.get(key);
if (existing !== undefined) return existing;
const value = callback(key);
this.set(key, value);
return value;
};
});
interface TestModules {
defineCommonSerializableSpecial: typeof import('./special').defineCommonSerializableSpecial;
defineNonePropertySpecial: typeof import('./special').defineNonePropertySpecial;
}
let modules: TestModules;
beforeAll(async () => {
vi.stubGlobal('main', { replayChecking: true });
vi.stubGlobal('location', { origin: 'http://localhost' });
const specialModule = await import('./special');
modules = {
defineCommonSerializableSpecial:
specialModule.defineCommonSerializableSpecial,
defineNonePropertySpecial: specialModule.defineNonePropertySpecial
};
});
afterAll(() => {
vi.unstubAllGlobals();
});
/**
* 构造一个按当前数值生成名称与描述的可序列化特殊属性配置
*/
function makeCommonConfig(): ICommonSpecialConfig<number> {
return {
getSpecialName: special => `连击${special.getValue()}`,
getDescription: special => `怪物每回合攻击${special.getValue()}次。`,
fromLegacyEnemy: () => 0
};
}
/**
* 构造一个使用固定名称与描述的无属性特殊属性配置
*/
function makeNoneConfig(): ICommonSpecialConfig<void> {
return {
getSpecialName: () => '先攻',
getDescription: () => '怪物首先攻击。',
fromLegacyEnemy: () => undefined
};
}
describe('CommonSerializableSpecial units', () => {
// 验证 setValue 更新数值且 getValue 返回最新数值
it('writes and reads the serializable value', () => {
const creation = modules.defineCommonSerializableSpecial<number>(
6,
4,
makeCommonConfig()
);
const special = creation(undefined as never);
expect(special.getValue()).toBe(4);
special.setValue(5);
expect(special.getValue()).toBe(5);
});
// 验证 getSpecialName 委托给配置并反映当前数值
it('delegates the special name to the config with the current value', () => {
const creation = modules.defineCommonSerializableSpecial<number>(
6,
4,
makeCommonConfig()
);
const special = creation(undefined as never);
expect(special.getSpecialName()).toBe('连击4');
special.setValue(5);
expect(special.getSpecialName()).toBe('连击5');
});
// 验证 getDescription 委托给配置并反映当前数值
it('delegates the description to the config with the current value', () => {
const creation = modules.defineCommonSerializableSpecial<number>(
6,
2,
makeCommonConfig()
);
const special = creation(undefined as never);
expect(special.getDescription()).toBe('怪物每回合攻击2次。');
special.setValue(3);
expect(special.getDescription()).toBe('怪物每回合攻击3次。');
});
});
describe('NonePropertySpecial units', () => {
// 验证无属性特殊属性数值恒为 undefined 且 setValue 不产生效果
it('keeps an undefined value regardless of setValue', () => {
const creation = modules.defineNonePropertySpecial(1, makeNoneConfig());
const special = creation(undefined as never);
expect(special.getValue()).toBeUndefined();
special.setValue(undefined);
expect(special.getValue()).toBeUndefined();
});
// 验证无属性特殊属性使用配置提供的固定名称与描述
it('reports the fixed name and description from the config', () => {
const creation = modules.defineNonePropertySpecial(1, makeNoneConfig());
const special = creation(undefined as never);
expect(special.getSpecialName()).toBe('先攻');
expect(special.getDescription()).toBe('怪物首先攻击。');
});
});