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

136 lines
3.5 KiB
JavaScript

/**
* Jest Setup - Mock Phaser and browser APIs
*/
// Mock Phaser BEFORE it's imported
jest.mock('phaser', () => ({
Physics: {
Arcade: {
DYNAMIC_BODY: 0,
Sprite: class MockSprite {
constructor(scene, x, y, texture) {
this.scene = scene;
this.x = x;
this.y = y;
this.texture = texture;
this.body = {
allowGravity: false,
setSize: jest.fn(),
setOffset: jest.fn()
};
this.setScale = jest.fn();
this.setInteractive = jest.fn();
this.on = jest.fn();
this.setPosition = jest.fn(() => true);
this.setFlipX = jest.fn();
this.setTint = jest.fn();
this.clearTint = jest.fn();
// Stateful setData/getData so tests can read back what they wrote
this._data = {};
this.setData = jest.fn((key, value) => { this._data[key] = value; });
this.getData = jest.fn((key) => this._data[key] ?? null);
this.pulse = null;
}
destroy() {} // no-op so Unit.destroy() can safely call super.destroy()
static enable(scene, object) {
object.body = { allowGravity: false };
}
}
}
},
Math: {
Angle: {
BetweenPoints: (a, b) => Math.atan2(b.y - a.y, b.x - a.x)
},
RadToDeg: rad => {
let deg = rad * (180 / Math.PI);
while (deg < 0) deg += 360;
return deg % 360;
},
Distance: {
BetweenPoints: (a, b) => Math.sqrt(Math.pow(b.x - a.x, 2) + Math.pow(b.y - a.y, 2))
},
Clamp: (v, min, max) => Math.max(min, Math.min(max, v))
},
Display: {
Color: {
GetColor32: (r, g, b, a) => (r << 24) | (g << 16) | (b << 8) | a
}
},
Tweens: {
Tween: class MockTween {
constructor(config) {
this.config = config;
}
getValue() { return 200; }
stop() {}
},
addCounter: config => {
const tween = {
getValue: () => 200,
stop: () => {}
};
// Fire onUpdate immediately so selection tests see setTint called
if (config.onUpdate) config.onUpdate(tween);
return tween;
}
},
Events: {
EventEmitter: class MockEventEmitter {
constructor() {
this.listeners = {};
}
on(event, fn) {
if (!this.listeners[event]) this.listeners[event] = [];
this.listeners[event].push(fn);
}
emit(event, ...args) {
if (this.listeners[event]) {
this.listeners[event].forEach(fn => fn(...args));
}
}
}
},
GameObjects: {
Zone: class MockZone {
constructor(scene, x, y, width, height) {
this.scene = scene;
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.body = { setCircle: jest.fn(), checkCollision: { none: false } };
}
destroy() {}
}
}
}));
// Mock XState
jest.mock('xstate', () => ({
createMachine: jest.fn(config => ({ config })),
interpret: jest.fn(machine => ({
machine,
start: jest.fn(),
send: jest.fn(),
stop: jest.fn(),
state: { value: 'IDLING' }
})),
assign: jest.fn(fn => fn)
}));
// Mock EasyStar
jest.mock('easystarjs', () => {
return jest.fn().mockImplementation(() => ({
setGrid: jest.fn(),
setIterationsPerCalculation: jest.fn(),
findPath: jest.fn((x, y, toX, toY, callback) => {
setTimeout(() => callback([{ x, y }, { x: toX, y: toY }]), 0);
}),
setTileAtXY: jest.fn()
}));
});
// Suppress console errors during tests
console.error = jest.fn();