mirror of
https://github.com/motajs/template.git
synced 2026-09-25 20:10:17 +08:00
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:
parent
e03fd3cb69
commit
965868c219
114
packages-user/data-base/src/enemy/enemy.test.ts
Normal file
114
packages-user/data-base/src/enemy/enemy.test.ts
Normal 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());
|
||||
});
|
||||
});
|
||||
136
packages-user/data-base/src/enemy/special.test.ts
Normal file
136
packages-user/data-base/src/enemy/special.test.ts
Normal 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('怪物首先攻击。');
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user