Files
restitution/tests/unit/ControlPointManager.test.js
kaykayyali 8fc45968b5 M2.3 + M2.4 + TeamManager integration: projectile sprites, death handling, build menu, production panel, building placer
- ProjectileSprite.js: physics arcade sprite, faction tint, off-screen culling
- CombatSystem: refactored enemy selection to use TeamManager instead of legacy containers
- Death handling: DYING alpha tween (500ms), smoke puff (300ms), unit:killed event, cleanup
- TeamManager: centralized team registry replacing goodGuys/badGuys containers
- HealthBarSystem, ResourceBar, CaptureProgressUI, BuildMenu, BuildingPlacer, BuildingRenderer, ProductionPanel
- Map_Player: wired new subsystems, removed legacy container creation
- Tests: ProjectileSprite (4), DeathHandling (13), CombatSystem updated

47 tests passed at dev time (M2.3), 158/158 at dev time (M2.4)
2026-06-01 05:18:33 +00:00

188 lines
5.8 KiB
JavaScript

/**
* ControlPointManager.test.js — Tests for placing CPs and wiring to EconomySystem.
*/
// Mock xstate before CPStateMachine imports it
jest.mock('xstate', () => ({
createMachine: jest.fn((config) => ({ config, id: config.id })),
interpret: jest.fn((machine) => ({
machine,
start: jest.fn(),
send: jest.fn(),
stop: jest.fn(),
state: {
value: 'NEUTRAL',
context: { owner: null, captureProgress: 0, unitsInRadius: {} },
},
})),
assign: jest.fn((fn) => fn),
}));
import ControlPointManager from 'Systems/ControlPointManager.js';
// ── Helpers ───────────────────────────────────────────────────────
function mockTilemap() {
return {
tileToWorldXY: jest.fn((tx, ty) => ({ x: tx * 32, y: ty * 32 })),
tileWidth: 32,
tileHeight: 32,
};
}
function mockScene() {
return {
add: {
zone: jest.fn((x, y) => ({
x: x ?? 0,
y: y ?? 0,
setName: jest.fn().mockReturnThis(),
destroy: jest.fn(),
body: { setCircle: jest.fn(), setOffset: jest.fn() },
})),
},
physics: {
world: { enableBody: jest.fn() },
},
events: { on: jest.fn(), emit: jest.fn(), off: jest.fn() },
};
}
function mockEconomy() {
return {
addIncome: jest.fn(),
getResources: jest.fn(),
initPlayer: jest.fn(),
};
}
// ── Tests ─────────────────────────────────────────────────────────
describe('ControlPointManager', () => {
let manager;
let scene;
let tilemap;
let economy;
beforeEach(() => {
scene = mockScene();
tilemap = mockTilemap();
economy = mockEconomy();
manager = new ControlPointManager(scene, tilemap, economy);
});
afterEach(() => {
if (manager) manager.destroy();
});
// ── constructor ─────────────────────────────────────────────────
describe('constructor', () => {
test('creates 4 control points', () => {
expect(manager.controlPoints.length).toBe(4);
});
test('accepts optional teamManager parameter', () => {
const tm = { getAllUnitsGrouped: jest.fn() };
const mgr = new ControlPointManager(scene, tilemap, economy, tm);
expect(mgr.teamManager).toBe(tm);
});
test('falls back to scene.teamManager when no teamManager passed', () => {
scene.teamManager = { getAllUnitsGrouped: jest.fn() };
const mgr = new ControlPointManager(scene, tilemap, economy);
expect(mgr.teamManager).toBe(scene.teamManager);
});
test('CPs are at clearing centers converted to world coords', () => {
const expectedTiles = [
{ tx: 32, ty: 32 },
{ tx: 96, ty: 32 },
{ tx: 32, ty: 96 },
{ tx: 96, ty: 96 },
];
for (let i = 0; i < 4; i++) {
const cp = manager.controlPoints[i];
expect(cp.zone.x).toBe(expectedTiles[i].tx * 32);
expect(cp.zone.y).toBe(expectedTiles[i].ty * 32);
}
});
test('tileToWorldXY is called for each CP', () => {
expect(tilemap.tileToWorldXY).toHaveBeenCalledTimes(4);
expect(tilemap.tileToWorldXY).toHaveBeenCalledWith(32, 32);
expect(tilemap.tileToWorldXY).toHaveBeenCalledWith(96, 32);
expect(tilemap.tileToWorldXY).toHaveBeenCalledWith(32, 96);
expect(tilemap.tileToWorldXY).toHaveBeenCalledWith(96, 96);
});
test('each CP has type=controlPoint, captureTime=60000, radius=5 tiles', () => {
for (const cp of manager.controlPoints) {
expect(cp.type).toBe('controlPoint');
expect(cp.captureTime).toBe(60000);
expect(cp.radiusTiles).toBe(5);
}
});
});
// ── update ──────────────────────────────────────────────────────
describe('update', () => {
test('ticks every CP', () => {
const tickSpies = manager.controlPoints.map((cp) =>
jest.spyOn(cp, 'tick').mockImplementation(() => {}),
);
manager.update(1000, 16);
// tick should be called with time and delta, not necessarily scene
for (const spy of tickSpies) {
expect(spy).toHaveBeenCalledWith(1000, 16, scene);
}
});
});
// ── CP income (tick-driven CAPTURED check) ─────────────────────
describe('CP income', () => {
test('each CP is wired with the economy system on construction', () => {
for (const cp of manager.controlPoints) {
expect(cp.economySystem).toBe(economy);
}
});
test('does NOT call addIncome when state is not CAPTURED', () => {
const cp = manager.controlPoints[0];
cp.getState = jest.fn(() => 'NEUTRAL');
manager.update(1000, 1000);
expect(economy.addIncome).not.toHaveBeenCalled();
});
test('does NOT call addIncome when owner is null', () => {
const cp = manager.controlPoints[0];
cp.getState = jest.fn(() => 'CAPTURED');
cp.getOwner = jest.fn(() => null);
manager.update(1000, 1000);
expect(economy.addIncome).not.toHaveBeenCalled();
});
});
// ── destroy → stop generating ───────────────────────────────────
describe('destroy', () => {
test('destroys all CPs and clears the array', () => {
const destroySpies = manager.controlPoints.map((cp) =>
jest.spyOn(cp, 'destroy').mockImplementation(() => {}),
);
manager.destroy();
for (const spy of destroySpies) {
expect(spy).toHaveBeenCalled();
}
expect(manager.controlPoints.length).toBe(0);
});
});
});