test(06-03): cover Enemy special CRUD and clone/copyFrom (stage 2)

- enemy.test.ts: addSpecial/get/has/iterate, duplicate warn 96, delete by object/code, clone/copyFrom independence
- special.test.ts: clone value independence and deepEqualsTo code/value rules, NonePropertySpecial clone
This commit is contained in:
unanmed 2026-09-14 14:35:24 +08:00
parent 965868c219
commit ff752713b9
2 changed files with 185 additions and 1 deletions

View File

@ -1,6 +1,6 @@
// 测试 Enemy 数据模型:属性单元、特殊属性增删查、克隆与复制行为
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
import { type IEnemy } from './types';
import { type IEnemy, type ISpecial } from './types';
vi.hoisted(() => {
vi.stubGlobal('main', { replayChecking: true });
@ -67,6 +67,19 @@ function createEnemy(
);
}
/**
* 构造一个可序列化的数值特殊属性,仅使用内联配置
* @param code 特殊属性代码
* @param value 特殊属性数值
*/
function createSpecial(code: number, value: number): ISpecial<number> {
return new modules.CommonSerializableSpecial<number>(code, value, {
getSpecialName: () => `special-${code}`,
getDescription: () => `description-${code}`,
fromLegacyEnemy: () => value
});
}
describe('Enemy attribute units', () => {
// 验证 getAttribute 按单个属性键返回构造时写入的初值
it('reads each attribute key independently', () => {
@ -112,3 +125,107 @@ describe('Enemy attribute units', () => {
expect(enemy.cloneAttributes()).not.toBe(enemy.cloneAttributes());
});
});
describe('Enemy special composition', () => {
// 验证 addSpecial 后可按代码查询、判断存在并迭代取出
it('adds a special and resolves it by code', () => {
const enemy = createEnemy();
const special = createSpecial(6, 3);
enemy.addSpecial(special);
expect(enemy.hasSpecial(6)).toBe(true);
expect(enemy.getSpecial<number>(6)).toBe(special);
expect([...enemy.iterateSpecials()].map(item => item.code)).toEqual([
6
]);
expect(enemy.hasSpecial(7)).toBe(false);
expect(enemy.getSpecial(7)).toBeNull();
});
// 验证重复 code 告警 96 且不替换既有特殊属性
it('warns 96 and keeps the existing special on a duplicate code', () => {
const enemy = createEnemy();
enemy.addSpecial(createSpecial(6, 3));
const result = modules.logger.catch(() =>
enemy.addSpecial(createSpecial(6, 9))
);
expect(result.info.map(info => info.code)).toContain(96);
expect(enemy.getSpecial<number>(6)!.getValue()).toBe(3);
expect([...enemy.iterateSpecials()]).toHaveLength(1);
});
// 验证 deleteSpecial 支持按对象与按代码删除且未知代码安全无操作
it('deletes specials by object or code and ignores unknown codes', () => {
const enemy = createEnemy();
const first = createSpecial(6, 3);
enemy.addSpecial(first);
enemy.addSpecial(createSpecial(7, 5));
enemy.deleteSpecial(first);
expect(enemy.hasSpecial(6)).toBe(false);
expect(enemy.getSpecial(6)).toBeNull();
enemy.deleteSpecial(7);
enemy.deleteSpecial(99);
expect([...enemy.iterateSpecials()]).toHaveLength(0);
});
// 验证 clone 复制属性与克隆后的特殊属性且与来源互相独立
it('clones attributes and specials into an independent enemy', () => {
const source = createEnemy('source', 1, {
hp: 20,
atk: 8,
tags: ['base']
});
source.addSpecial(createSpecial(6, 3));
const clone = source.clone();
expect(clone).not.toBe(source);
expect(clone.id).toBe('source');
expect(clone.code).toBe(1);
expect(clone.getSpecial<number>(6)).not.toBe(source.getSpecial(6));
expect(clone.getAttribute('tags')).not.toBe(
source.getAttribute('tags')
);
clone.setAttribute('hp', 99);
clone.getSpecial<number>(6)!.setValue(9);
clone.getAttribute('tags').push('cloned');
expect(source.getAttribute('hp')).toBe(20);
expect(source.getSpecial<number>(6)!.getValue()).toBe(3);
expect(source.getAttribute('tags')).toEqual(['base']);
});
// 验证 copyFrom 以来源的属性与克隆后的特殊属性整体替换当前对象
it('copies attributes and specials from another enemy', () => {
const target = createEnemy('target', 2, { hp: 1, atk: 1, tags: [] });
target.addSpecial(createSpecial(7, 5));
const source = createEnemy('source', 1, {
hp: 77,
atk: 5,
tags: ['copied']
});
source.addSpecial(createSpecial(6, 3));
target.copyFrom(source);
expect(target.id).toBe('target');
expect(target.code).toBe(2);
expect(target.getAttribute('hp')).toBe(77);
expect(target.getAttribute('tags')).toEqual(['copied']);
expect(target.hasSpecial(7)).toBe(false);
expect(target.getSpecial<number>(6)).not.toBe(source.getSpecial(6));
expect(target.getSpecial<number>(6)!.getValue()).toBe(3);
source.setAttribute('hp', 1);
expect(target.getAttribute('hp')).toBe(77);
});
});

View File

@ -1,6 +1,7 @@
// 测试 special 数据模型:可序列化特殊属性的数值/名称/描述单元,以及无属性特殊属性
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
import { type ICommonSpecialConfig } from './special';
import { type ISpecial } from './types';
vi.hoisted(() => {
vi.stubGlobal('main', { replayChecking: true });
@ -62,6 +63,30 @@ function makeNoneConfig(): ICommonSpecialConfig<void> {
};
}
/**
* 构造一个内联配置的可序列化数值特殊属性实例
* @param code 特殊属性代码
* @param value 特殊属性数值
*/
function createValueSpecial(code: number, value: number): ISpecial<number> {
return modules.defineCommonSerializableSpecial<number>(
code,
value,
makeCommonConfig()
)(undefined as never);
}
/**
* 构造一个内联配置的无属性特殊属性实例
* @param code 特殊属性代码
*/
function createNoneSpecial(code: number): ISpecial<void> {
return modules.defineNonePropertySpecial(
code,
makeNoneConfig()
)(undefined as never);
}
describe('CommonSerializableSpecial units', () => {
// 验证 setValue 更新数值且 getValue 返回最新数值
it('writes and reads the serializable value', () => {
@ -134,3 +159,45 @@ describe('NonePropertySpecial units', () => {
expect(special.getDescription()).toBe('怪物首先攻击。');
});
});
describe('Special clone and deep equality', () => {
// 验证可序列化特殊属性的 clone 深拷贝数值且与来源互相独立
it('clones a serializable special without aliasing its value', () => {
const source = createValueSpecial(6, 3);
const clone = source.clone();
expect(clone).not.toBe(source);
expect(clone.code).toBe(6);
expect(clone.getValue()).toBe(3);
clone.setValue(9);
expect(source.getValue()).toBe(3);
expect(clone.getValue()).toBe(9);
});
// 验证可序列化特殊属性按 code 与深比较数值判定相等
it('compares serializable specials by code and value', () => {
expect(
createValueSpecial(6, 3).deepEqualsTo(createValueSpecial(6, 3))
).toBe(true);
expect(
createValueSpecial(6, 3).deepEqualsTo(createValueSpecial(6, 5))
).toBe(false);
expect(
createValueSpecial(6, 3).deepEqualsTo(createValueSpecial(7, 3))
).toBe(false);
});
// 验证无属性特殊属性的 clone 独立且 deepEqualsTo 仅按 code 判定
it('clones a none-property special and compares by code only', () => {
const source = createNoneSpecial(1);
const clone = source.clone();
expect(clone).not.toBe(source);
expect(clone.code).toBe(1);
expect(clone.getValue()).toBeUndefined();
expect(clone.deepEqualsTo(source)).toBe(true);
expect(clone.deepEqualsTo(createNoneSpecial(2))).toBe(false);
});
});