Files
restitution/tests/EconomySystem.test.js
kaykayyali 3fc29f728e feat: Colyseus authoritative server + invite-code lobby
- Replace socket.io relay with Colyseus 0.15 authoritative server
- GameRoom with GameState schema (players, units, resources)
- Pure TS services: CombatResolver, EconomyService, PathfindingService, UnitManager
- POST /api/create-room → 4-char invite code
- React/MUI LobbyScreen: Create (shows code + START GAME) / Join by code
- ColyseusClient: joinOrCreate/join by room type = invite code
- Nginx: static assets direct, all else proxied to Colyseus (WS upgrade)
- Content-hashed JS bundles for Cloudflare cache-busting
- 1-player lobbies: START GAME button bypasses 2-player wait
2026-05-30 02:49:20 +00:00

162 lines
5.3 KiB
JavaScript

/**
* EconomySystem Unit Tests
*/
import EconomySystem from '../src/systems/EconomySystem';
// Mock Phaser Scene
const createMockScene = () => ({
events: {
emit: jest.fn(),
on: jest.fn()
}
});
describe('EconomySystem', () => {
let scene;
let economy;
beforeEach(() => {
scene = createMockScene();
economy = new EconomySystem(scene);
});
describe('initPlayer', () => {
it('should initialize player with default resources', () => {
economy.initPlayer('player1');
const resources = economy.getResources('player1');
expect(resources.fuel).toBe(100);
expect(resources.ammo).toBe(100);
expect(resources.capturePoints).toBe(0);
});
it('should initialize player with custom resources', () => {
economy.initPlayer('player1', { fuel: 200, ammo: 50, capturePoints: 10 });
const resources = economy.getResources('player1');
expect(resources.fuel).toBe(200);
expect(resources.ammo).toBe(50);
expect(resources.capturePoints).toBe(10);
});
});
describe('canAfford', () => {
beforeEach(() => {
economy.initPlayer('player1', { fuel: 100, ammo: 50 });
});
it('should return true when player has enough resources', () => {
expect(economy.canAfford('player1', { fuel: 50, ammo: 25 })).toBe(true);
});
it('should return false when player lacks fuel', () => {
expect(economy.canAfford('player1', { fuel: 150, ammo: 25 })).toBe(false);
});
it('should return false when player lacks ammo', () => {
expect(economy.canAfford('player1', { fuel: 50, ammo: 100 })).toBe(false);
});
it('should return false for non-existent player', () => {
expect(economy.canAfford('player2', { fuel: 10 })).toBe(false);
});
});
describe('deduct', () => {
beforeEach(() => {
economy.initPlayer('player1', { fuel: 100, ammo: 50 });
});
it('should deduct resources and return true', () => {
const result = economy.deduct('player1', { fuel: 30, ammo: 20 });
const resources = economy.getResources('player1');
expect(result).toBe(true);
expect(resources.fuel).toBe(70);
expect(resources.ammo).toBe(30);
});
it('should not deduct and return false when insufficient resources', () => {
const result = economy.deduct('player1', { fuel: 150, ammo: 20 });
const resources = economy.getResources('player1');
expect(result).toBe(false);
expect(resources.fuel).toBe(100); // Unchanged
expect(resources.ammo).toBe(50); // Unchanged
});
it('should emit economy:purchaseFailed on insufficient resources', () => {
// Production code emits on economy.events (internal EventEmitter), not scene.events
const emitSpy = jest.spyOn(economy.events, 'emit');
economy.deduct('player1', { fuel: 150, ammo: 20 });
expect(emitSpy).toHaveBeenCalledWith(
'economy:purchaseFailed',
expect.objectContaining({ playerId: 'player1', reason: expect.any(String) })
);
});
});
describe('addIncome', () => {
it('should add income to player resources', () => {
economy.initPlayer('player1', { fuel: 100, ammo: 50 });
economy.addIncome('player1', { fuel: 25, ammo: 10, capturePoints: 5 });
const resources = economy.getResources('player1');
expect(resources.fuel).toBe(125);
expect(resources.ammo).toBe(60);
expect(resources.capturePoints).toBe(5);
});
it('should auto-initialize player if not exists', () => {
// Auto-init gives DEFAULT_STARTING_RESOURCES (fuel:100) + income (fuel:50) = 150
economy.addIncome('player2', { fuel: 50 });
const resources = economy.getResources('player2');
expect(resources.fuel).toBe(150);
});
it('should emit economy:incomeReceived and economy:updated', () => {
// Production code emits on economy.events (internal EventEmitter)
const emitSpy = jest.spyOn(economy.events, 'emit');
economy.initPlayer('player1');
economy.addIncome('player1', { fuel: 10 });
expect(emitSpy).toHaveBeenCalledWith(
'economy:incomeReceived',
expect.objectContaining({ playerId: 'player1' })
);
expect(emitSpy).toHaveBeenCalledWith(
'economy:updated',
expect.objectContaining({ playerId: 'player1' })
);
});
});
describe('update', () => {
it('should track elapsed time for income tick guard', () => {
// update() is a guard only — external systems call addIncome().
// Verify update() doesn't throw and advances _lastTick.
expect(() => economy.update(1000)).not.toThrow();
expect(economy._lastTick).toBe(1000);
// Second call at 1500ms — not enough time passed since last tick (1000)
economy._lastTick = 0; // reset
expect(() => economy.update(500)).not.toThrow();
expect(economy._lastTick).toBe(0); // guard didn't fire
});
it('should not fire tick before 1000ms', () => {
// At 500ms, guard not triggered
economy._lastTick = 0;
const emitSpy = jest.spyOn(economy.events, 'emit');
economy.update(500);
// Update is a pure guard — no events are emitted by update() itself.
// External systems call addIncome() which does the event emission.
expect(economy._lastTick).toBe(0);
});
});
});