mirror of
https://github.com/motajs/template.git
synced 2026-09-19 05:30:16 +08:00
test(06-08): cover FlagSystem, FaceManager handlers and RoleFaceBinder (stage 2)
- add system.test.ts covering the full FlagSystem public surface and warn code 111 - add faceManager.test.ts covering FaceManager registry plus Dir4/Dir8 handlers - add face.test.ts covering RoleFaceBinder malloc/bind/query and error codes 43/44
This commit is contained in:
parent
7394a2591c
commit
2cb9bba3ce
171
packages-user/data-base/src/flag/system.test.ts
Normal file
171
packages-user/data-base/src/flag/system.test.ts
Normal file
@ -0,0 +1,171 @@
|
||||
// 测试 data-base 全局 Flag 系统的公开接口行为(存读档归 06-09,此处不测)
|
||||
import { beforeAll, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
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;
|
||||
};
|
||||
Map.prototype.getOrInsert ??= function <K, V>(
|
||||
this: Map<K, V>,
|
||||
key: K,
|
||||
defaultValue: V
|
||||
): V {
|
||||
const existing = this.get(key);
|
||||
if (existing !== undefined) return existing;
|
||||
this.set(key, defaultValue);
|
||||
return defaultValue;
|
||||
};
|
||||
});
|
||||
|
||||
interface TestModules {
|
||||
FlagSystem: typeof import('./system').FlagSystem;
|
||||
logger: typeof import('@motajs/common').logger;
|
||||
}
|
||||
|
||||
let modules: TestModules;
|
||||
|
||||
beforeAll(async () => {
|
||||
vi.stubGlobal('main', { replayChecking: true });
|
||||
vi.stubGlobal('location', { origin: 'http://localhost' });
|
||||
const systemModule = await import('./system');
|
||||
const commonModule = await import('@motajs/common');
|
||||
modules = {
|
||||
FlagSystem: systemModule.FlagSystem,
|
||||
logger: commonModule.logger
|
||||
};
|
||||
});
|
||||
|
||||
/** 创建一个 Flag 系统及其中一个已插入字段,便于直接操作字段对象 */
|
||||
function createField<T>(key: PropertyKey, value: T) {
|
||||
const system = new modules.FlagSystem();
|
||||
return { system, field: system.insertField(key, value) };
|
||||
}
|
||||
|
||||
describe('FlagSystem field container', () => {
|
||||
// 验证新系统的字段初始未占用,插入字段后被占用
|
||||
it('tracks field occupancy', () => {
|
||||
const system = new modules.FlagSystem();
|
||||
expect(system.occupied('score')).toBe(false);
|
||||
|
||||
system.insertField('score', 1);
|
||||
|
||||
expect(system.occupied('score')).toBe(true);
|
||||
});
|
||||
|
||||
// 验证 insertField 返回的字段对象支持取值、赋值与结构化克隆
|
||||
it('returns a field exposing get, set and toStructured', () => {
|
||||
const { field } = createField('nested', { list: [1, 2] });
|
||||
|
||||
expect(field.get()).toEqual({ list: [1, 2] });
|
||||
|
||||
field.set({ list: [3] });
|
||||
|
||||
expect(field.get()).toEqual({ list: [3] });
|
||||
const structured = field.toStructured();
|
||||
expect(structured).toEqual({ list: [3] });
|
||||
expect(structured).not.toBe(field.get());
|
||||
});
|
||||
|
||||
// 验证 getField 对未知字段返回 null,getOrInsert 插入默认值且二次调用返回同一实例
|
||||
it('gets by key and reuses the same field instance', () => {
|
||||
const system = new modules.FlagSystem();
|
||||
expect(system.getField('missing')).toBeNull();
|
||||
|
||||
const first = system.getOrInsert('count', 5);
|
||||
const second = system.getOrInsert('count', 99);
|
||||
|
||||
expect(first).toBe(second);
|
||||
expect(first.get()).toBe(5);
|
||||
expect(system.getField('count')).toBe(first);
|
||||
});
|
||||
|
||||
// 验证 getOrInsertComputed 把 key 传给默认值函数且仅在字段缺失时执行一次
|
||||
it('computes the default only when the field is missing', () => {
|
||||
const system = new modules.FlagSystem();
|
||||
let calls = 0;
|
||||
const compute = (key: string) => {
|
||||
calls++;
|
||||
return `value:${key}`;
|
||||
};
|
||||
|
||||
const field = system.getOrInsertComputed('name', compute);
|
||||
|
||||
expect(field.get()).toBe('value:name');
|
||||
expect(system.getOrInsertComputed('name', compute)).toBe(field);
|
||||
expect(calls).toBe(1);
|
||||
});
|
||||
|
||||
// 验证 deleteField 移除字段并恢复未占用状态
|
||||
it('deletes a field', () => {
|
||||
const system = new modules.FlagSystem();
|
||||
system.insertField('temp', 1);
|
||||
|
||||
system.deleteField('temp');
|
||||
|
||||
expect(system.occupied('temp')).toBe(false);
|
||||
expect(system.getField('temp')).toBeNull();
|
||||
expect(system.getFieldValue('temp')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('FlagSystem value accessors', () => {
|
||||
// 验证 setFieldValue 覆盖已有值并创建缺失字段
|
||||
it('sets values creating or overriding fields', () => {
|
||||
const system = new modules.FlagSystem();
|
||||
system.setFieldValue('score', 1);
|
||||
system.setFieldValue('score', 9);
|
||||
|
||||
expect(system.getFieldValue<number>('score')).toBe(9);
|
||||
expect(system.occupied('score')).toBe(true);
|
||||
});
|
||||
|
||||
// 验证 addFieldValue 对数值字段累加
|
||||
it('accumulates numeric field values', () => {
|
||||
const system = new modules.FlagSystem();
|
||||
system.addFieldValue('score', 4);
|
||||
system.addFieldValue('score', 6);
|
||||
|
||||
expect(system.getFieldValue<number>('score')).toBe(10);
|
||||
});
|
||||
|
||||
// 验证对非数值字段调用 addFieldValue 时经 logger 观测告警码 111
|
||||
it('warns code 111 when adding to a non-numeric field', () => {
|
||||
const system = new modules.FlagSystem();
|
||||
system.setFieldValue('name', 'hero');
|
||||
|
||||
const result = modules.logger.catch(() =>
|
||||
system.addFieldValue('name', 1)
|
||||
);
|
||||
|
||||
expect(result.info.map(info => info.code)).toContain(111);
|
||||
expect(system.getFieldValue<string>('name')).toBe('hero');
|
||||
});
|
||||
|
||||
// 验证 getFieldValue 对未知字段返回 undefined
|
||||
it('returns undefined for an unknown value', () => {
|
||||
const system = new modules.FlagSystem();
|
||||
|
||||
expect(system.getFieldValue('missing')).toBeUndefined();
|
||||
});
|
||||
|
||||
// 验证 getFieldValueDefaults 返回默认值并以该值插入字段
|
||||
it('returns and inserts the default value', () => {
|
||||
const system = new modules.FlagSystem();
|
||||
|
||||
const value = system.getFieldValueDefaults('lives', 3);
|
||||
|
||||
expect(value).toBe(3);
|
||||
expect(system.occupied('lives')).toBe(true);
|
||||
expect(system.getFieldValue<number>('lives')).toBe(3);
|
||||
});
|
||||
});
|
||||
122
packages-user/data-common/src/common/face.test.ts
Normal file
122
packages-user/data-common/src/common/face.test.ts
Normal file
@ -0,0 +1,122 @@
|
||||
// 测试 L0 RoleFaceBinder 的朝向分配、绑定与查询(含码 43/44 告警)
|
||||
import { beforeAll, describe, expect, it, vi } from 'vitest';
|
||||
import { FaceDirection } from './types';
|
||||
|
||||
vi.hoisted(() => {
|
||||
vi.stubGlobal('main', { replayChecking: true });
|
||||
vi.stubGlobal('location', { origin: 'http://localhost' });
|
||||
});
|
||||
|
||||
interface TestModules {
|
||||
RoleFaceBinder: typeof import('./face').RoleFaceBinder;
|
||||
logger: typeof import('@motajs/common').logger;
|
||||
}
|
||||
|
||||
let modules: TestModules;
|
||||
|
||||
beforeAll(async () => {
|
||||
vi.stubGlobal('main', { replayChecking: true });
|
||||
vi.stubGlobal('location', { origin: 'http://localhost' });
|
||||
const faceModule = await import('./face');
|
||||
const commonModule = await import('@motajs/common');
|
||||
modules = {
|
||||
RoleFaceBinder: faceModule.RoleFaceBinder,
|
||||
logger: commonModule.logger
|
||||
};
|
||||
});
|
||||
|
||||
/** 创建一个朝向绑定器实例 */
|
||||
function createBinder() {
|
||||
return new modules.RoleFaceBinder();
|
||||
}
|
||||
|
||||
describe('RoleFaceBinder malloc and bind', () => {
|
||||
// 验证 malloc 建立主朝向后可查询主朝向及该朝向的图块
|
||||
it('registers the main face of a block', () => {
|
||||
const binder = createBinder();
|
||||
binder.malloc(1, FaceDirection.Down);
|
||||
|
||||
expect(binder.getMainFace(1)).toEqual({
|
||||
identifier: 1,
|
||||
face: FaceDirection.Down
|
||||
});
|
||||
expect(binder.getFaceDirection(1)).toBe(FaceDirection.Down);
|
||||
expect(binder.getFaceOf(1, FaceDirection.Down)).toEqual({
|
||||
identifier: 1,
|
||||
face: FaceDirection.Down
|
||||
});
|
||||
});
|
||||
|
||||
// 验证 bind 将另一朝向图块绑定到主图块并共享朝向映射
|
||||
it('binds a face block to its main block', () => {
|
||||
const binder = createBinder();
|
||||
binder.malloc(1, FaceDirection.Down);
|
||||
binder.bind(2, 1, FaceDirection.Up);
|
||||
|
||||
expect(binder.getMainFace(2)).toEqual({
|
||||
identifier: 2,
|
||||
face: FaceDirection.Down
|
||||
});
|
||||
expect(binder.getFaceDirection(2)).toBe(FaceDirection.Up);
|
||||
expect(binder.getFaceOf(1, FaceDirection.Up)).toEqual({
|
||||
identifier: 2,
|
||||
face: FaceDirection.Up
|
||||
});
|
||||
expect(binder.getFaceOf(2, FaceDirection.Down)).toEqual({
|
||||
identifier: 1,
|
||||
face: FaceDirection.Down
|
||||
});
|
||||
});
|
||||
|
||||
// 验证绑定未知主图块时经 logger 观测错误码 43 且不建立绑定
|
||||
it('warns code 43 for an unknown main block', () => {
|
||||
const binder = createBinder();
|
||||
|
||||
const result = modules.logger.catch(() =>
|
||||
binder.bind(2, 99, FaceDirection.Up)
|
||||
);
|
||||
|
||||
expect(result.info.map(info => info.code)).toContain(43);
|
||||
expect(binder.getMainFace(2)).toBeNull();
|
||||
expect(binder.getFaceDirection(2)).toBeUndefined();
|
||||
});
|
||||
|
||||
// 验证绑定与主朝向相同朝向时经 logger 观测错误码 44 且不建立绑定
|
||||
it('warns code 44 when binding the main direction', () => {
|
||||
const binder = createBinder();
|
||||
binder.malloc(1, FaceDirection.Down);
|
||||
|
||||
const result = modules.logger.catch(() =>
|
||||
binder.bind(2, 1, FaceDirection.Down)
|
||||
);
|
||||
|
||||
expect(result.info.map(info => info.code)).toContain(44);
|
||||
expect(binder.getMainFace(2)).toBeNull();
|
||||
expect(binder.getFaceDirection(2)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('RoleFaceBinder queries', () => {
|
||||
// 验证 getFaceOf 对未绑定朝向与未知图块均返回 null
|
||||
it('returns null for an unbound face or an unknown block', () => {
|
||||
const binder = createBinder();
|
||||
binder.malloc(1, FaceDirection.Down);
|
||||
|
||||
expect(binder.getFaceOf(1, FaceDirection.Up)).toBeNull();
|
||||
expect(binder.getFaceOf(99, FaceDirection.Down)).toBeNull();
|
||||
});
|
||||
|
||||
// 验证 getFaceDirection 对未注册图块返回 undefined
|
||||
it('returns undefined direction for an unknown block', () => {
|
||||
const binder = createBinder();
|
||||
|
||||
expect(binder.getFaceDirection(42)).toBeUndefined();
|
||||
});
|
||||
|
||||
// 验证 getMainFace 对未注册图块返回 null
|
||||
it('returns null main face for an unknown block', () => {
|
||||
const binder = createBinder();
|
||||
|
||||
expect(binder.getMainFace(42)).toBeNull();
|
||||
});
|
||||
});
|
||||
178
packages-user/data-common/src/common/faceManager.test.ts
Normal file
178
packages-user/data-common/src/common/faceManager.test.ts
Normal file
@ -0,0 +1,178 @@
|
||||
// 测试 L0 FaceManager 注册表与内建 4/8 方向处理器的方向语义
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { FaceDirection } from './types';
|
||||
import { Dir4FaceHandler, Dir8FaceHandler, FaceManager } from './faceManager';
|
||||
|
||||
/** 创建内建的 4 方向与 8 方向处理器 */
|
||||
function createHandler() {
|
||||
return {
|
||||
dir4: new Dir4FaceHandler(),
|
||||
dir8: new Dir8FaceHandler()
|
||||
};
|
||||
}
|
||||
|
||||
describe('FaceManager registry', () => {
|
||||
// 验证按数字 key 与字符串 id 注册后都能查回同一处理器
|
||||
it('returns the registered handler by group and id', () => {
|
||||
const manager = new FaceManager();
|
||||
const { dir4, dir8 } = createHandler();
|
||||
manager.register(1, dir8);
|
||||
manager.registerById('four', dir4);
|
||||
|
||||
expect(manager.get<FaceDirection>(1)).toBe(dir8);
|
||||
expect(manager.getById<FaceDirection>('four')).toBe(dir4);
|
||||
});
|
||||
|
||||
// 验证查询未注册的 key 或 id 时返回 null
|
||||
it('returns null for an unknown group and id', () => {
|
||||
const manager = new FaceManager();
|
||||
|
||||
expect(manager.get<FaceDirection>(9)).toBeNull();
|
||||
expect(manager.getById<FaceDirection>('missing')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dir8FaceHandler', () => {
|
||||
// 验证 degrade 对任意输入原样透传
|
||||
it('passes degrade through unchanged', () => {
|
||||
const { dir8 } = createHandler();
|
||||
|
||||
expect(dir8.degrade(FaceDirection.LeftUp)).toBe(FaceDirection.LeftUp);
|
||||
expect(dir8.degrade(123)).toBe(123);
|
||||
});
|
||||
|
||||
// 验证 movement 对全部八个方向及未知返回单步偏移量
|
||||
it('returns the single-step offset for every direction', () => {
|
||||
const { dir8 } = createHandler();
|
||||
|
||||
expect(dir8.movement(FaceDirection.Left)).toEqual({ x: -1, y: 0 });
|
||||
expect(dir8.movement(FaceDirection.Up)).toEqual({ x: 0, y: -1 });
|
||||
expect(dir8.movement(FaceDirection.Right)).toEqual({ x: 1, y: 0 });
|
||||
expect(dir8.movement(FaceDirection.Down)).toEqual({ x: 0, y: 1 });
|
||||
expect(dir8.movement(FaceDirection.LeftUp)).toEqual({ x: -1, y: -1 });
|
||||
expect(dir8.movement(FaceDirection.RightUp)).toEqual({ x: 1, y: -1 });
|
||||
expect(dir8.movement(FaceDirection.LeftDown)).toEqual({ x: -1, y: 1 });
|
||||
expect(dir8.movement(FaceDirection.RightDown)).toEqual({ x: 1, y: 1 });
|
||||
expect(dir8.movement(FaceDirection.Unknown)).toEqual({ x: 0, y: 0 });
|
||||
});
|
||||
|
||||
// 验证越界方向输入返回零偏移量
|
||||
it('returns the zero offset for an out-of-range direction', () => {
|
||||
const { dir8 } = createHandler();
|
||||
|
||||
expect(dir8.movement(999)).toEqual({ x: 0, y: 0 });
|
||||
});
|
||||
|
||||
// 验证 move 按步数缩放偏移量,负步数得到反向位移
|
||||
it('scales the offset by the step count including negatives', () => {
|
||||
const { dir8 } = createHandler();
|
||||
|
||||
expect(dir8.move(FaceDirection.Right, 3)).toEqual({ x: 3, y: 0 });
|
||||
expect(dir8.move(FaceDirection.RightUp, 2)).toEqual({ x: 2, y: -2 });
|
||||
expect(dir8.move(FaceDirection.RightUp, -2)).toEqual({ x: -2, y: 2 });
|
||||
});
|
||||
|
||||
// 验证 opposite 返回反方向,未知朝向返回未知
|
||||
it('returns the opposite direction including unknown', () => {
|
||||
const { dir8 } = createHandler();
|
||||
|
||||
expect(dir8.opposite(FaceDirection.Up)).toBe(FaceDirection.Down);
|
||||
expect(dir8.opposite(FaceDirection.Left)).toBe(FaceDirection.Right);
|
||||
expect(dir8.opposite(FaceDirection.LeftUp)).toBe(
|
||||
FaceDirection.RightDown
|
||||
);
|
||||
expect(dir8.opposite(FaceDirection.RightUp)).toBe(
|
||||
FaceDirection.LeftDown
|
||||
);
|
||||
expect(dir8.opposite(FaceDirection.Unknown)).toBe(
|
||||
FaceDirection.Unknown
|
||||
);
|
||||
});
|
||||
|
||||
// 验证 next 默认顺时针、传入参数时逆时针,未知朝向透传
|
||||
it('rotates clockwise by default and anticlockwise on request', () => {
|
||||
const { dir8 } = createHandler();
|
||||
|
||||
expect(dir8.next(FaceDirection.Up)).toBe(FaceDirection.RightUp);
|
||||
expect(dir8.next(FaceDirection.LeftUp)).toBe(FaceDirection.Up);
|
||||
expect(dir8.next(FaceDirection.Up, true)).toBe(FaceDirection.LeftUp);
|
||||
expect(dir8.next(FaceDirection.LeftUp, true)).toBe(FaceDirection.Left);
|
||||
expect(dir8.next(FaceDirection.Unknown)).toBe(FaceDirection.Unknown);
|
||||
expect(dir8.next(FaceDirection.Unknown, true)).toBe(
|
||||
FaceDirection.Unknown
|
||||
);
|
||||
});
|
||||
|
||||
// 验证 mapDirection 与 mapMovement 迭代全部九个方向且未知为零偏移
|
||||
it('maps every direction together with its movement', () => {
|
||||
const { dir8 } = createHandler();
|
||||
|
||||
expect([...dir8.mapDirection()]).toEqual([
|
||||
FaceDirection.Unknown,
|
||||
FaceDirection.Left,
|
||||
FaceDirection.Up,
|
||||
FaceDirection.Right,
|
||||
FaceDirection.Down,
|
||||
FaceDirection.LeftUp,
|
||||
FaceDirection.RightUp,
|
||||
FaceDirection.LeftDown,
|
||||
FaceDirection.RightDown
|
||||
]);
|
||||
|
||||
const movements = [...dir8.mapMovement()];
|
||||
expect(movements).toHaveLength(9);
|
||||
expect(movements[0]).toEqual([FaceDirection.Unknown, { x: 0, y: 0 }]);
|
||||
expect(movements[8]).toEqual([FaceDirection.RightDown, { x: 1, y: 1 }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dir4FaceHandler', () => {
|
||||
// 验证 degrade 把对角降级为对应正交方向,其余非法输入降级为未知
|
||||
it('degrades diagonals into orthogonal and the rest into unknown', () => {
|
||||
const { dir4 } = createHandler();
|
||||
|
||||
expect(dir4.degrade(FaceDirection.LeftUp)).toBe(FaceDirection.Left);
|
||||
expect(dir4.degrade(FaceDirection.LeftDown)).toBe(FaceDirection.Left);
|
||||
expect(dir4.degrade(FaceDirection.RightUp)).toBe(FaceDirection.Right);
|
||||
expect(dir4.degrade(FaceDirection.RightDown)).toBe(FaceDirection.Right);
|
||||
expect(dir4.degrade(FaceDirection.Left)).toBe(FaceDirection.Left);
|
||||
expect(dir4.degrade(FaceDirection.Up)).toBe(FaceDirection.Up);
|
||||
expect(dir4.degrade(FaceDirection.Unknown)).toBe(FaceDirection.Unknown);
|
||||
expect(dir4.degrade(999)).toBe(FaceDirection.Unknown);
|
||||
});
|
||||
|
||||
// 验证 movement、move、opposite 与 next 在四方向集合内正确工作
|
||||
it('computes movement, opposite and rotation on the four-way set', () => {
|
||||
const { dir4 } = createHandler();
|
||||
|
||||
expect(dir4.movement(FaceDirection.Left)).toEqual({ x: -1, y: 0 });
|
||||
expect(dir4.movement(FaceDirection.RightUp)).toEqual({ x: 1, y: 0 });
|
||||
expect(dir4.movement(999)).toEqual({ x: 0, y: 0 });
|
||||
expect(dir4.move(FaceDirection.Down, 2)).toEqual({ x: 0, y: 2 });
|
||||
expect(dir4.move(FaceDirection.Right, 3)).toEqual({ x: 3, y: 0 });
|
||||
expect(dir4.opposite(FaceDirection.Up)).toBe(FaceDirection.Down);
|
||||
expect(dir4.opposite(FaceDirection.Unknown)).toBe(
|
||||
FaceDirection.Unknown
|
||||
);
|
||||
expect(dir4.next(FaceDirection.Up)).toBe(FaceDirection.Right);
|
||||
expect(dir4.next(FaceDirection.Up, true)).toBe(FaceDirection.Left);
|
||||
expect(dir4.next(FaceDirection.Unknown)).toBe(FaceDirection.Unknown);
|
||||
});
|
||||
|
||||
// 验证 mapDirection 与 mapMovement 只迭代四个正交方向与未知
|
||||
it('maps only the four-way set plus unknown', () => {
|
||||
const { dir4 } = createHandler();
|
||||
|
||||
expect([...dir4.mapDirection()]).toEqual([
|
||||
FaceDirection.Unknown,
|
||||
FaceDirection.Left,
|
||||
FaceDirection.Up,
|
||||
FaceDirection.Right,
|
||||
FaceDirection.Down
|
||||
]);
|
||||
|
||||
const movements = [...dir4.mapMovement()];
|
||||
expect(movements).toHaveLength(5);
|
||||
expect(movements[0]).toEqual([FaceDirection.Unknown, { x: 0, y: 0 }]);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user