Files
restitution/tests/unit/EntityStateMachine.test.js
root 2e07519648 Refactor: Component-based architecture + 10 sub-systems
- Implemented 10 sub-systems (Economy, Pathfinding, Combat, Selection, Network, Map, Entity/Building/ControlPoint state machines, Orchestrator)
- Refactored Custom_Entity.js → Unit.js with 5 components (health, owner, inventory, movement, combat)
- Added Jest test suite with 100+ tests (EconomySystem 100%, EntityStateMachine 100%, PathfindingSystem 99%, Unit.js 72%)
- All webpack builds pass (0 errors)
- BMAD-auto team-respawn flow: 10 parallel sub-agents implemented systems

Architecture: Phaser 3 + XState + socket.io + EasyStar
Mode: team-respawn
Model: custom/ollama-cloud-pro
2026-05-29 22:13:44 +00:00

235 lines
7.4 KiB
JavaScript

/**
* EntityStateMachine.test.js — Tests for state transitions, event sending, lifecycle.
*/
import EntityStateMachine from 'Systems/EntityStateMachine.js';
describe('EntityStateMachine', () => {
let mockEntity;
let machineConfig;
beforeEach(() => {
mockEntity = {
name: 'testEntity',
x: 100,
y: 200,
rotation: 0,
};
machineConfig = {
id: 'entity',
initial: 'IDLING',
context: {},
states: {
IDLING: {
entry: ['playIdleAnim'],
on: { MOVE: 'MOVING', DIE: 'DYING' },
},
MOVING: {
entry: ['playMoveAnim'],
on: { ARRIVED: 'IDLING', DIE: 'DYING' },
},
DYING: {
type: 'final',
},
},
};
});
// ── Constructor ─────────────────────────────────────────────────
describe('constructor', () => {
test('stores entity and machine config', () => {
const esm = new EntityStateMachine(mockEntity, machineConfig);
expect(esm.entity).toBe(mockEntity);
expect(esm.machineConfig).toBe(machineConfig);
expect(esm.service).toBeNull();
});
test('default initial state is IDLING', () => {
const esm = new EntityStateMachine(mockEntity, machineConfig);
expect(esm._currentState).toBe('IDLING');
expect(esm.getState()).toBe('IDLING');
});
});
// ── send ────────────────────────────────────────────────────────
describe('send', () => {
test('sends event to service when available', () => {
const esm = new EntityStateMachine(mockEntity, machineConfig);
const mockSend = jest.fn();
esm.service = {
send: mockSend,
state: { value: 'IDLING' },
};
esm.send('MOVE');
expect(mockSend).toHaveBeenCalledWith({ type: 'MOVE' });
});
test('sends event with context', () => {
const esm = new EntityStateMachine(mockEntity, machineConfig);
const mockSend = jest.fn();
esm.service = {
send: mockSend,
state: { value: 'IDLING' },
};
esm.send('MOVE', { x: 10, y: 20 });
expect(mockSend).toHaveBeenCalledWith({ type: 'MOVE', x: 10, y: 20 });
});
test('does not throw when service is null', () => {
const esm = new EntityStateMachine(mockEntity, machineConfig);
expect(() => esm.send('MOVE')).not.toThrow();
});
test('does not throw when service has no send method', () => {
const esm = new EntityStateMachine(mockEntity, machineConfig);
esm.service = {};
expect(() => esm.send('MOVE')).not.toThrow();
});
});
// ── getState ────────────────────────────────────────────────────
describe('getState', () => {
test('returns service state value when available', () => {
const esm = new EntityStateMachine(mockEntity, machineConfig);
esm.service = {
state: { value: 'MOVING' },
send: jest.fn(),
};
expect(esm.getState()).toBe('MOVING');
});
test('falls back to _currentState when service is null', () => {
const esm = new EntityStateMachine(mockEntity, machineConfig);
esm._currentState = 'ATTACKING';
expect(esm.getState()).toBe('ATTACKING');
});
test('falls back to _currentState when service.state is null', () => {
const esm = new EntityStateMachine(mockEntity, machineConfig);
esm.service = {
state: null,
send: jest.fn(),
};
esm._currentState = 'DYING';
expect(esm.getState()).toBe('DYING');
});
});
// ── state transitions ──────────────────────────────────────────
describe('state transitions', () => {
test('starts in IDLING', () => {
const esm = new EntityStateMachine(mockEntity, machineConfig);
expect(esm.getState()).toBe('IDLING');
});
test('can simulate state changes via send', () => {
const esm = new EntityStateMachine(mockEntity, machineConfig);
const mockSend = jest.fn();
// Simulate a service that tracks state
let currentState = 'IDLING';
const stateMap = {
IDLING: { MOVE: 'MOVING', DIE: 'DYING' },
MOVING: { ARRIVED: 'IDLING', DIE: 'DYING' },
DYING: {},
};
esm.service = {
send: (event) => {
mockSend(event);
const transitions = stateMap[currentState];
if (transitions && transitions[event.type]) {
currentState = transitions[event.type];
}
},
state: { value: currentState },
};
expect(esm.getState()).toBe('IDLING');
esm.send('MOVE');
expect(currentState).toBe('MOVING');
});
});
// ── tick ────────────────────────────────────────────────────────
describe('tick', () => {
test('does not throw when called', () => {
const esm = new EntityStateMachine(mockEntity, machineConfig);
expect(() => esm.tick(1000, 16)).not.toThrow();
});
test('can be called multiple times without side effects', () => {
const esm = new EntityStateMachine(mockEntity, machineConfig);
for (let i = 0; i < 10; i++) {
expect(() => esm.tick(i * 100, 16)).not.toThrow();
}
});
});
// ── destroy ─────────────────────────────────────────────────────
describe('destroy', () => {
test('stops service if it has a stop method', () => {
const esm = new EntityStateMachine(mockEntity, machineConfig);
const mockStop = jest.fn();
esm.service = {
stop: mockStop,
send: jest.fn(),
};
esm.destroy();
expect(mockStop).toHaveBeenCalledTimes(1);
expect(esm.service).toBeNull();
expect(esm.entity).toBeNull();
});
test('handles service without stop method gracefully', () => {
const esm = new EntityStateMachine(mockEntity, machineConfig);
esm.service = { send: jest.fn() };
expect(() => esm.destroy()).not.toThrow();
expect(esm.service).toBeNull();
});
test('handles null service gracefully', () => {
const esm = new EntityStateMachine(mockEntity, machineConfig);
expect(() => esm.destroy()).not.toThrow();
});
});
// ── edge cases ──────────────────────────────────────────────────
describe('edge cases', () => {
test('handles empty machineConfig', () => {
const esm = new EntityStateMachine(mockEntity, {});
expect(esm.machineConfig).toEqual({});
expect(esm.getState()).toBe('IDLING');
});
test('handles rapid send calls', () => {
const esm = new EntityStateMachine(mockEntity, machineConfig);
const events = [];
esm.service = {
send: (e) => events.push(e.type),
state: { value: 'IDLING' },
};
esm.send('MOVE');
esm.send('DIE');
esm.send('ARRIVED');
expect(events).toEqual(['MOVE', 'DIE', 'ARRIVED']);
});
});
});