diff --git a/src/index.ts b/src/index.ts index ea74990..1bc0511 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,7 +20,7 @@ export { parseCommand, parseCommandSchema, validateCommand, parseCommandWithSche export type { CommandRunner, CommandRunnerHandler, CommandRunnerContext, PromptEvent, CommandRunnerEvents } from './utils/command'; export { createCommandRegistry, registerCommand, unregisterCommand, hasCommand, getCommand, runCommand, runCommandParsed, createCommandRunnerContext, type CommandRegistry, type CommandRunnerContextExport } from './utils/command'; -export type { Entity, EntityAccessor } from './utils/entity'; +export type { Entity } from './utils/entity'; export { createEntityCollection } from './utils/entity'; export type { RNG } from './utils/rng'; diff --git a/tests/core/game.test.ts b/tests/core/game.test.ts index 0a9378e..bc1498a 100644 --- a/tests/core/game.test.ts +++ b/tests/core/game.test.ts @@ -1,71 +1,67 @@ import { describe, it, expect } from 'vitest'; -import { createGameContext, createGameCommand, IGameContext } from '../../src/core/game'; -import { createCommandRegistry, parseCommandSchema, type CommandRegistry } from '../../src/utils/command'; +import { createGameContext, createGameCommand, createGameCommandRegistry, IGameContext } from '../../src/core/game'; +import { Entity } from '../../src/utils/entity'; +import type { PromptEvent } from '../../src/utils/command'; type MyState = { score: number; round: number; }; -type MyContext = IGameContext & { - state: MyState; -}; - describe('createGameContext', () => { - it('should create a game context with empty parts and regions', () => { - const registry = createCommandRegistry(); + it('should create a game context with state', () => { + const { registry } = createGameCommandRegistry(); const ctx = createGameContext(registry); - expect(ctx.parts.collection.value).toEqual({}); - expect(ctx.regions.collection.value).toEqual({}); + expect(ctx.state).not.toBeNull(); + expect(ctx.state.value).toBeDefined(); }); it('should wire commands to the context', () => { - const registry = createCommandRegistry(); + const { registry } = createGameCommandRegistry(); const ctx = createGameContext(registry); expect(ctx.commands).not.toBeNull(); expect(ctx.commands.registry).toBe(registry); - expect(ctx.commands.context).toBe(ctx); + expect(ctx.commands.context).toBe(ctx.state); }); it('should accept initial state as an object', () => { - const registry = createCommandRegistry(); - const ctx = createGameContext(registry, { - state: { score: 0, round: 1 }, + const { registry } = createGameCommandRegistry(); + const ctx = createGameContext(registry, { + score: 0, + round: 1, }); - expect(ctx.state.score).toBe(0); - expect(ctx.state.round).toBe(1); + expect(ctx.state.value.score).toBe(0); + expect(ctx.state.value.round).toBe(1); }); it('should accept initial state as a factory function', () => { - const registry = createCommandRegistry(); - const ctx = createGameContext(registry, () => ({ - state: { score: 10, round: 3 }, + const { registry } = createGameCommandRegistry(); + const ctx = createGameContext(registry, () => ({ + score: 10, + round: 3, })); - expect(ctx.state.score).toBe(10); - expect(ctx.state.round).toBe(3); + expect(ctx.state.value.score).toBe(10); + expect(ctx.state.value.round).toBe(3); }); - it('should forward prompt events to the prompts queue', async () => { - const registry = createCommandRegistry(); + it('should forward prompt events via listener', async () => { + const { registry } = createGameCommandRegistry(); const ctx = createGameContext(registry); - const schema = parseCommandSchema('test '); - registry.set('test', { - schema, - run: async function () { - return this.prompt('prompt '); - }, + createGameCommand(registry, 'test ', async function () { + return this.prompt('prompt '); }); + const promptPromise = new Promise(resolve => { + ctx.commands.on('prompt', resolve); + }); const runPromise = ctx.commands.run('test hello'); - await new Promise((r) => setTimeout(r, 0)); - - const promptEvent = await ctx.prompts.pop(); + const promptEvent = await promptPromise; expect(promptEvent).not.toBeNull(); expect(promptEvent.schema.name).toBe('prompt'); @@ -80,94 +76,44 @@ describe('createGameContext', () => { }); describe('createGameCommand', () => { - it('should create a command from a string schema', () => { - const cmd = createGameCommand('test ', async function () { - return 1; - }); - - expect(cmd.schema.name).toBe('test'); - expect(cmd.schema.params[0].name).toBe('a'); - expect(cmd.schema.params[0].required).toBe(true); - }); - - it('should create a command from a CommandSchema object', () => { - const schema = parseCommandSchema('foo [y]'); - const cmd = createGameCommand(schema, async function () { - return 2; - }); - - expect(cmd.schema.name).toBe('foo'); - expect(cmd.schema.params[0].name).toBe('x'); - expect(cmd.schema.params[0].required).toBe(true); - expect(cmd.schema.params[1].name).toBe('y'); - expect(cmd.schema.params[1].required).toBe(false); - }); - it('should run a command with access to game context', async () => { - const registry = createCommandRegistry(); - const ctx = createGameContext(registry); + const { registry } = createGameCommandRegistry>(); + const ctx = createGameContext(registry, { marker: '' }); - const addRegion = createGameCommand('add-region ', async function (cmd) { + createGameCommand(registry, 'set-marker ', async function (cmd) { const id = cmd.params[0] as string; - this.context.regions.add({ id, axes: [], children: [] }); + this.context.produce(state => { + state.marker = id; + }); return id; }); - registry.set('add-region', addRegion); - - const result = await ctx.commands.run('add-region board'); + const result = await ctx.commands.run('set-marker board'); expect(result.success).toBe(true); if (result.success) { expect(result.result).toBe('board'); } - expect(ctx.regions.get('board')).not.toBeNull(); - }); - - it('should run a command that adds parts', async () => { - const registry = createCommandRegistry(); - const ctx = createGameContext(registry); - - ctx.regions.add({ id: 'zone', axes: [], children: [] }); - - const addPart = createGameCommand('add-part ', async function (cmd) { - const id = cmd.params[0] as string; - const part = { - id, - sides: 1, - side: 0, - region: this.context.regions.get('zone'), - position: [0], - }; - this.context.parts.add(part); - return id; - }); - - registry.set('add-part', addPart); - - const result = await ctx.commands.run('add-part piece-1'); - expect(result.success).toBe(true); - if (result.success) { - expect(result.result).toBe('piece-1'); - } - expect(ctx.parts.get('piece-1')).not.toBeNull(); + expect(ctx.state.value.marker).toBe('board'); }); it('should run a typed command with extended context', async () => { - const registry = createCommandRegistry(); + const { registry } = createGameCommandRegistry(); - const addScore = createGameCommand( + createGameCommand( + registry, 'add-score ', async function (cmd) { const amount = cmd.params[0] as number; - this.context.state.score += amount; - return this.context.state.score; + this.context.produce(state => { + state.score += amount; + }); + return this.context.value.score; } ); - registry.set('add-score', addScore); - - const ctx = createGameContext(registry, () => ({ - state: { score: 0, round: 1 }, + const ctx = createGameContext(registry, () => ({ + score: 0, + round: 1, })); const result = await ctx.commands.run('add-score 5'); @@ -175,6 +121,17 @@ describe('createGameCommand', () => { if (result.success) { expect(result.result).toBe(5); } - expect(ctx.state.score).toBe(5); + expect(ctx.state.value.score).toBe(5); + }); + + it('should return error for unknown command', async () => { + const { registry } = createGameCommandRegistry(); + const ctx = createGameContext(registry); + + const result = await ctx.commands.run('nonexistent'); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error).toContain('nonexistent'); + } }); }); diff --git a/tests/core/region.test.ts b/tests/core/region.test.ts index 906bd56..e73f1ac 100644 --- a/tests/core/region.test.ts +++ b/tests/core/region.test.ts @@ -1,233 +1,189 @@ import { describe, it, expect } from 'vitest'; import { applyAlign, shuffle, type Region, type RegionAxis } from '../../src/core/region'; import { createRNG } from '../../src/utils/rng'; -import { createEntityCollection } from '../../src/utils/entity'; +import { entity, Entity } from '../../src/utils/entity'; import { type Part } from '../../src/core/part'; describe('Region', () => { - function createPart(id: string, position: number[]): Part { - const collection = createEntityCollection(); - const part: Part = { - id, - sides: 1, - side: 0, - region: { id: 'region1', value: {} as Region }, - position: [...position] - }; - collection.add(part); - return part; - } - - function createRegion(axes: RegionAxis[], parts: Part[]): Region { - const region: Region = { + function createTestRegion(axes: RegionAxis[], parts: Part[]): Entity { + const partEntities = parts.map(p => entity(p.id, p)); + return entity('region1', { id: 'region1', axes: [...axes], - children: parts.map(p => ({ id: p.id, value: p })) - }; - return region; + children: partEntities, + }); } describe('applyAlign', () => { it('should do nothing with empty region', () => { - const region = createRegion([{ name: 'x', min: 0, align: 'start' }], []); + const region = createTestRegion([{ name: 'x', min: 0, align: 'start' }], []); applyAlign(region); - expect(region.children).toHaveLength(0); + expect(region.value.children).toHaveLength(0); }); it('should align parts to start on first axis', () => { - const part1 = createPart('p1', [5, 10]); - const part2 = createPart('p2', [7, 20]); - const part3 = createPart('p3', [2, 30]); - - const region = createRegion( + const part1: Part = { id: 'p1', region: null as any, position: [5, 10] }; + const part2: Part = { id: 'p2', region: null as any, position: [7, 20] }; + const part3: Part = { id: 'p3', region: null as any, position: [2, 30] }; + + const region = createTestRegion( [{ name: 'x', min: 0, align: 'start' }, { name: 'y' }], [part1, part2, part3] ); - + applyAlign(region); - - // 排序后应该是 part3(2), part1(5), part2(7) -> 对齐到 0, 1, 2 (第一轴) - expect(region.children[0].value.position[0]).toBe(0); - expect(region.children[1].value.position[0]).toBe(1); - expect(region.children[2].value.position[0]).toBe(2); - // 第二轴保持不变 - expect(region.children[0].value.position[1]).toBe(30); - expect(region.children[1].value.position[1]).toBe(10); - expect(region.children[2].value.position[1]).toBe(20); + + expect(region.value.children[0].value.position[0]).toBe(0); + expect(region.value.children[1].value.position[0]).toBe(1); + expect(region.value.children[2].value.position[0]).toBe(2); + expect(region.value.children[0].value.position[1]).toBe(30); + expect(region.value.children[1].value.position[1]).toBe(10); + expect(region.value.children[2].value.position[1]).toBe(20); }); it('should align parts to start with custom min', () => { - const part1 = createPart('p1', [5, 100]); - const part2 = createPart('p2', [7, 200]); - - const region = createRegion( + const part1: Part = { id: 'p1', region: null as any, position: [5, 100] }; + const part2: Part = { id: 'p2', region: null as any, position: [7, 200] }; + + const region = createTestRegion( [{ name: 'x', min: 10, align: 'start' }, { name: 'y' }], [part1, part2] ); - + applyAlign(region); - - expect(region.children[0].value.position[0]).toBe(10); - expect(region.children[1].value.position[0]).toBe(11); - // 第二轴保持不变 - expect(region.children[0].value.position[1]).toBe(100); - expect(region.children[1].value.position[1]).toBe(200); + + expect(region.value.children[0].value.position[0]).toBe(10); + expect(region.value.children[1].value.position[0]).toBe(11); + expect(region.value.children[0].value.position[1]).toBe(100); + expect(region.value.children[1].value.position[1]).toBe(200); }); it('should align parts to end on first axis', () => { - const part1 = createPart('p1', [2, 50]); - const part2 = createPart('p2', [4, 60]); - const part3 = createPart('p3', [1, 70]); - - const region = createRegion( + const part1: Part = { id: 'p1', region: null as any, position: [2, 50] }; + const part2: Part = { id: 'p2', region: null as any, position: [4, 60] }; + const part3: Part = { id: 'p3', region: null as any, position: [1, 70] }; + + const region = createTestRegion( [{ name: 'x', max: 10, align: 'end' }, { name: 'y' }], [part1, part2, part3] ); - + applyAlign(region); - - // 3 个部分,对齐到 end(max=10),应该是 8, 9, 10 - expect(region.children[0].value.position[0]).toBe(8); - expect(region.children[1].value.position[0]).toBe(9); - expect(region.children[2].value.position[0]).toBe(10); + + expect(region.value.children[0].value.position[0]).toBe(8); + expect(region.value.children[1].value.position[0]).toBe(9); + expect(region.value.children[2].value.position[0]).toBe(10); }); it('should align parts to center on first axis', () => { - const part1 = createPart('p1', [0, 5]); - const part2 = createPart('p2', [1, 6]); - const part3 = createPart('p3', [2, 7]); - - const region = createRegion( + const part1: Part = { id: 'p1', region: null as any, position: [0, 5] }; + const part2: Part = { id: 'p2', region: null as any, position: [1, 6] }; + const part3: Part = { id: 'p3', region: null as any, position: [2, 7] }; + + const region = createTestRegion( [{ name: 'x', min: 0, max: 10, align: 'center' }, { name: 'y' }], [part1, part2, part3] ); - + applyAlign(region); - - // 中心是 5,3 个部分应该是 4, 5, 6 - expect(region.children[0].value.position[0]).toBe(4); - expect(region.children[1].value.position[0]).toBe(5); - expect(region.children[2].value.position[0]).toBe(6); + + expect(region.value.children[0].value.position[0]).toBe(4); + expect(region.value.children[1].value.position[0]).toBe(5); + expect(region.value.children[2].value.position[0]).toBe(6); }); it('should handle even count center alignment', () => { - const part1 = createPart('p1', [0, 10]); - const part2 = createPart('p2', [1, 20]); - - const region = createRegion( + const part1: Part = { id: 'p1', region: null as any, position: [0, 10] }; + const part2: Part = { id: 'p2', region: null as any, position: [1, 20] }; + + const region = createTestRegion( [{ name: 'x', min: 0, max: 10, align: 'center' }, { name: 'y' }], [part1, part2] ); - + applyAlign(region); - - // 中心是 5,2 个部分应该是 4.5, 5.5 - expect(region.children[0].value.position[0]).toBe(4.5); - expect(region.children[1].value.position[0]).toBe(5.5); + + expect(region.value.children[0].value.position[0]).toBe(4.5); + expect(region.value.children[1].value.position[0]).toBe(5.5); }); it('should sort children by position on current axis', () => { - const part1 = createPart('p1', [5, 100]); - const part2 = createPart('p2', [1, 200]); - const part3 = createPart('p3', [3, 300]); - - const region = createRegion( + const part1: Part = { id: 'p1', region: null as any, position: [5, 100] }; + const part2: Part = { id: 'p2', region: null as any, position: [1, 200] }; + const part3: Part = { id: 'p3', region: null as any, position: [3, 300] }; + + const region = createTestRegion( [{ name: 'x', min: 0, align: 'start' }, { name: 'y' }], [part1, part2, part3] ); - + applyAlign(region); - - // children 应该按第一轴位置排序 - expect(region.children[0].value.id).toBe('p2'); - expect(region.children[1].value.id).toBe('p3'); - expect(region.children[2].value.id).toBe('p1'); + + expect(region.value.children[0].value.id).toBe('p2'); + expect(region.value.children[1].value.id).toBe('p3'); + expect(region.value.children[2].value.id).toBe('p1'); }); it('should align on multiple axes', () => { - const part1 = createPart('p1', [5, 10]); - const part2 = createPart('p2', [7, 20]); - const part3 = createPart('p3', [2, 30]); - - const region = createRegion( + const part1: Part = { id: 'p1', region: null as any, position: [5, 10] }; + const part2: Part = { id: 'p2', region: null as any, position: [7, 20] }; + const part3: Part = { id: 'p3', region: null as any, position: [2, 30] }; + + const region = createTestRegion( [ { name: 'x', min: 0, align: 'start' }, { name: 'y', min: 0, align: 'start' } ], [part1, part2, part3] ); - + applyAlign(region); - - // X 轴对齐: - // 唯一位置值:[2, 5, 7] -> 映射到 [0, 1, 2] - // part3: 2->0, part1: 5->1, part2: 7->2 - // 结果:part3=[0,30], part1=[1,10], part2=[2,20] - // - // Y 轴对齐: - // 唯一位置值:[10, 20, 30] -> 映射到 [0, 1, 2] - // part1: 10->0, part2: 20->1, part3: 30->2 - // 最终:part1=[1,0], part2=[2,1], part3=[0,2] - - const positions = region.children.map(c => ({ + + const positions = region.value.children.map(c => ({ id: c.value.id, position: c.value.position })); - - // children 按位置排序后的顺序 + expect(positions[0].id).toBe('p3'); expect(positions[0].position).toEqual([0, 2]); - + expect(positions[1].id).toBe('p1'); expect(positions[1].position).toEqual([1, 0]); - + expect(positions[2].id).toBe('p2'); expect(positions[2].position).toEqual([2, 1]); }); it('should align 4 elements on rectangle corners', () => { - // 4 个元素放在矩形的四个角:(0,0), (10,0), (10,1), (0,1) - // 期望:保持矩形布局,只是紧凑到 (0,0), (1,0), (1,1), (0,1) - const part1 = createPart('p1', [0, 0]); // 左下角 - const part2 = createPart('p2', [10, 0]); // 右下角 - const part3 = createPart('p3', [10, 1]); // 右上角 - const part4 = createPart('p4', [0, 1]); // 左上角 - - const region = createRegion( + const part1: Part = { id: 'p1', region: null as any, position: [0, 0] }; + const part2: Part = { id: 'p2', region: null as any, position: [10, 0] }; + const part3: Part = { id: 'p3', region: null as any, position: [10, 1] }; + const part4: Part = { id: 'p4', region: null as any, position: [0, 1] }; + + const region = createTestRegion( [ { name: 'x', min: 0, max: 10, align: 'start' }, { name: 'y', min: 0, max: 10, align: 'start' } ], [part1, part2, part3, part4] ); - + applyAlign(region); - - // X 轴对齐: - // 唯一位置值:[0, 10] -> 映射到 [0, 1] - // part1: 0->0, part4: 0->0, part2: 10->1, part3: 10->1 - // 结果:part1=[0,0], part4=[0,1], part2=[1,0], part3=[1,1] - // - // Y 轴对齐: - // 唯一位置值:[0, 1] -> 映射到 [0, 1] (已经是紧凑的) - // part1: 0->0, part2: 0->0, part4: 1->1, part3: 1->1 - // 最终:part1=[0,0], part2=[1,0], part4=[0,1], part3=[1,1] - - const positions = region.children.map(c => ({ + + const positions = region.value.children.map(c => ({ id: c.value.id, position: c.value.position })); - - // children 按位置排序后的顺序:(0,0), (0,1), (1,0), (1,1) + expect(positions[0].id).toBe('p1'); expect(positions[0].position).toEqual([0, 0]); - + expect(positions[1].id).toBe('p4'); expect(positions[1].position).toEqual([0, 1]); - + expect(positions[2].id).toBe('p2'); expect(positions[2].position).toEqual([1, 0]); - + expect(positions[3].id).toBe('p3'); expect(positions[3].position).toEqual([1, 1]); }); @@ -235,37 +191,35 @@ describe('Region', () => { describe('shuffle', () => { it('should do nothing with empty region', () => { - const region = createRegion([], []); + const region = createTestRegion([], []); const rng = createRNG(42); shuffle(region, rng); - expect(region.children).toHaveLength(0); + expect(region.value.children).toHaveLength(0); }); it('should do nothing with single part', () => { - const part = createPart('p1', [0, 0, 0]); - const region = createRegion([], [part]); + const part: Part = { id: 'p1', region: null as any, position: [0, 0, 0] }; + const region = createTestRegion([], [part]); const rng = createRNG(42); shuffle(region, rng); - expect(region.children[0].value.position).toEqual([0, 0, 0]); + expect(region.value.children[0].value.position).toEqual([0, 0, 0]); }); it('should shuffle positions of multiple parts', () => { - const part1 = createPart('p1', [0, 100]); - const part2 = createPart('p2', [1, 200]); - const part3 = createPart('p3', [2, 300]); - - const region = createRegion([], [part1, part2, part3]); + const part1: Part = { id: 'p1', region: null as any, position: [0, 100] }; + const part2: Part = { id: 'p2', region: null as any, position: [1, 200] }; + const part3: Part = { id: 'p3', region: null as any, position: [2, 300] }; + + const region = createTestRegion([], [part1, part2, part3]); const rng = createRNG(42); - - const originalPositions = region.children.map(c => [...c.value.position]); + + const originalPositions = region.value.children.map(c => [...c.value.position]); shuffle(region, rng); - - // 位置应该被交换 - const newPositions = region.children.map(c => c.value.position); - - // 验证所有原始位置仍然存在(只是被交换了) + + const newPositions = region.value.children.map(c => c.value.position); + originalPositions.forEach(origPos => { - const found = newPositions.some(newPos => + const found = newPositions.some(newPos => newPos[0] === origPos[0] && newPos[1] === origPos[1] ); expect(found).toBe(true); @@ -274,50 +228,48 @@ describe('Region', () => { it('should be deterministic with same seed', () => { const createRegionForTest = () => { - const part1 = createPart('p1', [0, 10]); - const part2 = createPart('p2', [1, 20]); - const part3 = createPart('p3', [2, 30]); - return createRegion([], [part1, part2, part3]); + const part1: Part = { id: 'p1', region: null as any, position: [0, 10] }; + const part2: Part = { id: 'p2', region: null as any, position: [1, 20] }; + const part3: Part = { id: 'p3', region: null as any, position: [2, 30] }; + return createTestRegion([], [part1, part2, part3]); }; - const region1 = createRegionForTest(); - const region2 = createRegionForTest(); - + const setup1 = createRegionForTest(); + const setup2 = createRegionForTest(); + const rng1 = createRNG(42); const rng2 = createRNG(42); - - shuffle(region1, rng1); - shuffle(region2, rng2); - - const positions1 = region1.children.map(c => c.value.position); - const positions2 = region2.children.map(c => c.value.position); - + + shuffle(setup1, rng1); + shuffle(setup2, rng2); + + const positions1 = setup1.value.children.map(c => c.value.position); + const positions2 = setup2.value.children.map(c => c.value.position); + expect(positions1).toEqual(positions2); }); it('should produce different results with different seeds', () => { const createRegionForTest = () => { - const part1 = createPart('p1', [0, 10]); - const part2 = createPart('p2', [1, 20]); - const part3 = createPart('p3', [2, 30]); - const part4 = createPart('p4', [3, 40]); - const part5 = createPart('p5', [4, 50]); - return createRegion([], [part1, part2, part3, part4, part5]); + const part1: Part = { id: 'p1', region: null as any, position: [0, 10] }; + const part2: Part = { id: 'p2', region: null as any, position: [1, 20] }; + const part3: Part = { id: 'p3', region: null as any, position: [2, 30] }; + const part4: Part = { id: 'p4', region: null as any, position: [3, 40] }; + const part5: Part = { id: 'p5', region: null as any, position: [4, 50] }; + return createTestRegion([], [part1, part2, part3, part4, part5]); }; const results = new Set(); - - // 尝试多个种子,确保大多数产生不同结果 + for (let seed = 1; seed <= 10; seed++) { - const region = createRegionForTest(); + const setup = createRegionForTest(); const rng = createRNG(seed); - shuffle(region, rng); - - const positions = JSON.stringify(region.children.map(c => c.value.position)); + shuffle(setup, rng); + + const positions = JSON.stringify(setup.value.children.map(c => c.value.position)); results.add(positions); } - - // 10 个种子中至少应该有 5 个不同的结果 + expect(results.size).toBeGreaterThan(5); }); }); diff --git a/tests/utils/entity.test.ts b/tests/utils/entity.test.ts index 571d957..76a79fe 100644 --- a/tests/utils/entity.test.ts +++ b/tests/utils/entity.test.ts @@ -1,10 +1,11 @@ import { describe, it, expect } from 'vitest'; -import { createEntityCollection, type Entity } from '../../src/utils/entity'; +import { createEntityCollection, Entity, entity } from '../../src/utils/entity'; -interface TestEntity extends Entity { +type TestEntity = { + id: string; name: string; value: number; -} +}; describe('createEntityCollection', () => { it('should create empty collection', () => { @@ -14,12 +15,12 @@ describe('createEntityCollection', () => { it('should add single entity', () => { const collection = createEntityCollection(); - const entity: TestEntity = { id: 'e1', name: 'Entity 1', value: 10 }; - - collection.add(entity); - + const testEntity: TestEntity = { id: 'e1', name: 'Entity 1', value: 10 }; + + collection.add(testEntity); + expect(collection.collection.value).toHaveProperty('e1'); - expect(collection.get('e1').value).toEqual(entity); + expect(collection.get('e1').value).toEqual(testEntity); }); it('should add multiple entities', () => { @@ -27,9 +28,9 @@ describe('createEntityCollection', () => { const entity1: TestEntity = { id: 'e1', name: 'Entity 1', value: 10 }; const entity2: TestEntity = { id: 'e2', name: 'Entity 2', value: 20 }; const entity3: TestEntity = { id: 'e3', name: 'Entity 3', value: 30 }; - + collection.add(entity1, entity2, entity3); - + expect(Object.keys(collection.collection.value)).toHaveLength(3); expect(collection.get('e1').value.name).toBe('Entity 1'); expect(collection.get('e2').value.name).toBe('Entity 2'); @@ -40,10 +41,10 @@ describe('createEntityCollection', () => { const collection = createEntityCollection(); const entity1: TestEntity = { id: 'e1', name: 'Entity 1', value: 10 }; const entity2: TestEntity = { id: 'e2', name: 'Entity 2', value: 20 }; - + collection.add(entity1, entity2); collection.remove('e1'); - + expect(Object.keys(collection.collection.value)).toHaveLength(1); expect(collection.collection.value).not.toHaveProperty('e1'); expect(collection.collection.value).toHaveProperty('e2'); @@ -54,64 +55,81 @@ describe('createEntityCollection', () => { const entity1: TestEntity = { id: 'e1', name: 'Entity 1', value: 10 }; const entity2: TestEntity = { id: 'e2', name: 'Entity 2', value: 20 }; const entity3: TestEntity = { id: 'e3', name: 'Entity 3', value: 30 }; - + collection.add(entity1, entity2, entity3); collection.remove('e1', 'e3'); - + expect(Object.keys(collection.collection.value)).toHaveLength(1); expect(collection.collection.value).toHaveProperty('e2'); }); it('should update entity via accessor', () => { const collection = createEntityCollection(); - const entity: TestEntity = { id: 'e1', name: 'Entity 1', value: 10 }; - - collection.add(entity); - + const testEntity: TestEntity = { id: 'e1', name: 'Entity 1', value: 10 }; + + collection.add(testEntity); + const accessor = collection.get('e1'); - accessor.value = { ...entity, value: 100, name: 'Updated' }; - + accessor.value = { ...testEntity, value: 100, name: 'Updated' }; + expect(collection.get('e1').value.value).toBe(100); expect(collection.get('e1').value.name).toBe('Updated'); }); it('should return undefined for non-existent entity', () => { const collection = createEntityCollection(); - - expect(collection.get('nonexistent').value).toBeUndefined(); + + expect(collection.get('nonexistent')).toBeUndefined(); }); it('should have correct accessor id', () => { const collection = createEntityCollection(); - const entity: TestEntity = { id: 'e1', name: 'Entity 1', value: 10 }; - - collection.add(entity); - + const testEntity: TestEntity = { id: 'e1', name: 'Entity 1', value: 10 }; + + collection.add(testEntity); + const accessor = collection.get('e1'); expect(accessor.id).toBe('e1'); }); it('should handle removing non-existent entity', () => { const collection = createEntityCollection(); - const entity: TestEntity = { id: 'e1', name: 'Entity 1', value: 10 }; - - collection.add(entity); + const testEntity: TestEntity = { id: 'e1', name: 'Entity 1', value: 10 }; + + collection.add(testEntity); collection.remove('nonexistent'); - + expect(Object.keys(collection.collection.value)).toHaveLength(1); }); it('should work with reactive updates', () => { const collection = createEntityCollection(); - const entity: TestEntity = { id: 'e1', name: 'Entity 1', value: 10 }; - - collection.add(entity); - - // 验证 accessor 可以正确获取和设置值 + const testEntity: TestEntity = { id: 'e1', name: 'Entity 1', value: 10 }; + + collection.add(testEntity); + const accessor = collection.get('e1'); expect(accessor.value.value).toBe(10); - - accessor.value = { ...entity, value: 50 }; + + accessor.value = { ...testEntity, value: 50 }; expect(accessor.value.value).toBe(50); }); }); + +describe('Entity', () => { + it('should create entity with id and value', () => { + const e = entity('test', { count: 1 }); + expect(e.id).toBe('test'); + expect(e.value.count).toBe(1); + }); + + it('should produce immutable updates', () => { + const e = entity('test', { count: 1, items: [1, 2, 3] }); + e.produce(draft => { + draft.count = 2; + draft.items.push(4); + }); + expect(e.value.count).toBe(2); + expect(e.value.items).toEqual([1, 2, 3, 4]); + }); +});