Files
restitution/tests/Map_Player.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

381 lines
12 KiB
JavaScript

/**
* Map_Player + Interface tests — M1.2: Disable legacy pointers, add F-key spawn
*
* Tests:
* 1. F-key spawns unit in scene
* 2. Spawned unit is selectable (physics body present for SelectionSystem)
* 3. Interface.init(false) does NOT create pathfinder or wire pointer events
*/
// ── Module-level mocks (before any imports) ───────────────────────
jest.mock('PhaserClasses/CustomConstants', () => ({
__esModule: true,
default: { TINTS: { RED: 0xff0000, BLUE: 0x0000ff, GREEN: 0x00ff00 } },
}));
jest.mock('Systems/SystemOrchestrator.js', () => ({
__esModule: true,
default: jest.fn().mockImplementation(() => ({
init: jest.fn(),
initPathfinding: jest.fn(),
initControlPoints: jest.fn(),
update: jest.fn(),
shutdown: jest.fn(),
registerBuilding: jest.fn((building, config) => ({
building,
config,
getState: jest.fn(() => 'ACTIVE'),
destroy: jest.fn(),
})),
unregisterBuilding: jest.fn(),
systems: {
selection: null,
pathfinding: null,
combat: { },
economy: {
initPlayer: jest.fn(),
events: {
on: jest.fn(),
},
},
network: null,
EntityStateMachine: {},
BuildingStateMachine: {},
ControlPointStateMachine: {},
},
})),
}));
jest.mock('Systems/NetworkSystem.js', () => ({
NetworkSystemClient: jest.fn(),
}));
// Mock entity skins to avoid real Phaser sprite construction
const createMockSkin = (scene, tile) => ({
scene,
tile,
x: (tile?.x ?? 0) * 32,
y: (tile?.y ?? 0) * 32,
name: 'test-unit',
body: null, // set by physics.enable
setScale: jest.fn().mockReturnThis(),
setData: jest.fn().mockReturnThis(),
setName: jest.fn().mockReturnThis(),
select: jest.fn(),
unSelect: jest.fn(),
destroy: jest.fn(),
});
jest.mock('Entities/skins/ukrainian-infantry', () => ({
__esModule: true,
default: jest.fn().mockImplementation(createMockSkin),
}));
jest.mock('Entities/skins/russian-infantry', () => ({
__esModule: true,
default: jest.fn().mockImplementation(createMockSkin),
}));
jest.mock('Entities/skins/russian-tank', () => ({
__esModule: true,
default: jest.fn().mockImplementation(createMockSkin),
}));
// ── Imports (after mocks) ─────────────────────────────────────────
import Map_Player from 'Scenes/Map_Player';
import Interface from 'PhaserClasses/interface';
// ── Helpers ───────────────────────────────────────────────────────
/**
* Build a mock Phaser scene with the surface area that create() + spawnTestUnit need.
*/
function buildMockScene() {
const keyboardHandlers = {};
const inputOnHandlers = {};
const mockScene = {
// Phaser.Scene basics
key: 'Map_Player',
scene: { key: 'Map_Player' },
game: { colyseus: null },
// Camera
cameras: {
main: {
setBounds: jest.fn(),
zoomTo: jest.fn(),
centerOn: jest.fn(),
get zoom() { return 1; },
},
},
// Input
input: {
setDefaultCursor: jest.fn(),
keyboard: {
addKey: jest.fn(() => ({ isDown: false })),
on: jest.fn((event, cb) => {
keyboardHandlers[event] = cb;
}),
_handlers: keyboardHandlers,
_fire: (event) => {
if (keyboardHandlers[event]) keyboardHandlers[event]();
},
},
on: jest.fn((event, cb) => {
inputOnHandlers[event] = cb;
}),
_handlers: inputOnHandlers,
_fire: (event, ...args) => {
if (inputOnHandlers[event]) inputOnHandlers[event](...args);
},
},
// Add game objects
add: {
container: jest.fn(() => ({
setName: jest.fn().mockReturnThis(),
add: jest.fn(),
setScrollFactor: jest.fn().mockReturnThis(),
setDepth: jest.fn().mockReturnThis(),
setPosition: jest.fn().mockReturnThis(),
setVisible: jest.fn().mockReturnThis(),
setAlpha: jest.fn().mockReturnThis(),
destroy: jest.fn(),
active: true,
})),
rectangle: jest.fn(() => ({
setStrokeStyle: jest.fn().mockReturnThis(),
setOrigin: jest.fn().mockReturnThis(),
setPosition: jest.fn().mockReturnThis(),
setScrollFactor: jest.fn().mockReturnThis(),
setInteractive: jest.fn().mockReturnThis(),
setFillStyle: jest.fn().mockReturnThis(),
setAlpha: jest.fn().mockReturnThis(),
setDepth: jest.fn().mockReturnThis(),
setVisible: jest.fn().mockReturnThis(),
on: jest.fn().mockReturnThis(),
destroy: jest.fn(),
active: true,
})),
graphics: jest.fn(() => ({
setDepth: jest.fn().mockReturnThis(),
setAlpha: jest.fn().mockReturnThis(),
clear: jest.fn(),
fillStyle: jest.fn(),
fillRect: jest.fn(),
lineStyle: jest.fn(),
strokeRect: jest.fn(),
get active() { return true; },
})),
text: jest.fn(() => ({
setScrollFactor: jest.fn().mockReturnThis(),
setOrigin: jest.fn().mockReturnThis(),
setDepth: jest.fn().mockReturnThis(),
setText: jest.fn().mockReturnThis(),
setVisible: jest.fn().mockReturnThis(),
setAlpha: jest.fn().mockReturnThis(),
setPosition: jest.fn().mockReturnThis(),
destroy: jest.fn(),
get active() { return true; },
})),
sprite: jest.fn(() => ({
setScale: jest.fn().mockReturnThis(),
setPosition: jest.fn().mockReturnThis(),
setVisible: jest.fn().mockReturnThis(),
play: jest.fn(),
setOrigin: jest.fn().mockReturnThis(),
setAlpha: jest.fn().mockReturnThis(),
setScrollFactor: jest.fn().mockReturnThis(),
setDepth: jest.fn().mockReturnThis(),
setDisplaySize: jest.fn().mockReturnThis(),
setTint: jest.fn().mockReturnThis(),
destroy: jest.fn(),
active: true,
})),
existing: jest.fn(),
},
// Physics
physics: {
world: { enableBody: jest.fn() },
overlapRect: jest.fn(() => []),
add: {
existing: jest.fn(),
},
},
// Events
events: {
on: jest.fn(),
emit: jest.fn(),
off: jest.fn(),
},
// Tweens
tweens: {
addCounter: jest.fn((config) => {
const tween = { getValue: () => 200, stop: jest.fn() };
if (config.onUpdate) config.onUpdate(tween);
return tween;
}),
},
// Animations
anims: {
create: jest.fn(),
generateFrameNumbers: jest.fn(() => []),
},
// Make (tilemap factory)
make: {
tilemap: jest.fn(() => ({
addTilesetImage: jest.fn(() => ({})),
createLayer: jest.fn(() => ({
setCollisionByProperty: jest.fn().mockReturnThis(),
setDepth: jest.fn().mockReturnThis(),
getTileAtWorldXY: jest.fn(() => ({ x: 5, y: 5, index: 0, z: 0 })),
getTilesWithinShape: jest.fn(() => [{ x: 0, y: 0 }]),
tileToWorldXY: jest.fn((x, y) => ({ x: x * 32, y: y * 32 })),
})),
widthInPixels: 640,
heightInPixels: 480,
worldToTileXY: jest.fn(() => ({ x: 0, y: 0 })),
tileToWorldXY: jest.fn(() => ({ x: 0, y: 0 })),
})),
},
// Scene-specific properties (added by create)
map: null,
groundLayer: null,
rockLayer: null,
goodGuys: null,
infantry: null,
interface: null,
orchestrator: {
registerBuilding: jest.fn((building, config) => ({
building,
config,
getState: jest.fn(() => 'ACTIVE'),
destroy: jest.fn(),
})),
unregisterBuilding: jest.fn(),
systems: {},
},
tints: {},
};
return mockScene;
}
// ── Tests ─────────────────────────────────────────────────────────
describe('Map_Player — F-key spawn', () => {
let scene;
let mapPlayer;
beforeEach(() => {
jest.clearAllMocks();
scene = buildMockScene();
// Instantiate Map_Player with our mock scene
mapPlayer = new Map_Player();
// Monkey-patch the scene reference (Phaser does this internally)
mapPlayer.scene = scene;
// Attach scene properties that create() would set
Object.assign(mapPlayer, {
add: scene.add,
input: scene.input,
cameras: scene.cameras,
make: scene.make,
physics: scene.physics,
events: scene.events,
tweens: scene.tweens,
anims: scene.anims,
game: scene.game,
});
// Stub createMap to set the layers
mapPlayer.createMap = jest.fn(function () {
this.map = this.make.tilemap({ key: 'test1' });
this.groundLayer = this.map.createLayer('Floor', {}, 0, 0);
this.rockLayer = this.map.createLayer('Rocks', {}, 0, 0).setCollisionByProperty({ collides: true }).setDepth(10);
});
// Stub createInfantry to do nothing (test F-key spawn instead)
mapPlayer.createInfantry = jest.fn();
mapPlayer.createFriendlyPlatoon = jest.fn();
mapPlayer.createFriendlyInfantry = jest.fn();
});
test('F-key spawns unit in scene', () => {
// Call create() — this registers the F-key handler
mapPlayer.create();
// Verify F-key handler was registered
expect(scene.input.keyboard.on).toHaveBeenCalledWith(
'keydown-F',
expect.any(Function),
);
// Simulate F keypress
scene.input.keyboard._fire('keydown-F');
// spawnTestUnit should have created an infantry and added it to physics
// The mock Ukrainian_Rifle constructor was called
const Ukrainian_Rifle = require('Entities/skins/ukrainian-infantry').default;
expect(Ukrainian_Rifle).toHaveBeenCalled();
// The unit should have been added to the physics group via physics.add.existing
expect(scene.physics.add.existing).toHaveBeenCalled();
});
test('spawned unit is selectable', () => {
mapPlayer.create();
// Simulate F keypress
scene.input.keyboard._fire('keydown-F');
// The spawned unit should have a physics body so SelectionSystem can find it
// Mock the unit's physics body
const Ukrainian_Rifle = require('Entities/skins/ukrainian-infantry').default;
// Get the last created unit mock
const lastCall = Ukrainian_Rifle.mock.results[Ukrainian_Rifle.mock.results.length - 1];
const unit = lastCall ? lastCall.value : null;
expect(unit).toBeDefined();
// Unit must have a body (set by physics.add.existing or scene.physics.world.enableBody)
// Verify physics.add.existing was called with the unit
expect(scene.physics.add.existing).toHaveBeenCalledWith(unit);
});
});
describe('Interface — init(false)', () => {
test('init(false) does NOT wire pointer DOWN/MOVE/UP or create pathfinder', () => {
const scene = buildMockScene();
// createCamera() needs scene.map to be set for setBounds
scene.map = { widthInPixels: 640, heightInPixels: 480 };
const iface = new Interface(scene);
// Call init with useLegacyPointers=false
iface.init(false);
// Should NOT have registered pointer DOWN/MOVE/UP
const inputCalls = scene.input.on.mock.calls.map((c) => c[0]);
expect(inputCalls).not.toContain('pointerdown');
expect(inputCalls).not.toContain('pointermove');
expect(inputCalls).not.toContain('pointerup');
// Should still register POINTER_WHEEL (for zoom)
const wheelEvent = 'wheel';
expect(inputCalls).toContain(wheelEvent);
// Should NOT have a pathfinder
expect(iface.pathfinder).toBeUndefined();
});
});