Files
restitution/gameServer/tests/inputHandler.test.ts
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

194 lines
5.3 KiB
TypeScript

/**
* Tests for inputHandler — pure functions that process client input messages
* and delegate to UnitManager. Extracted from GameRoom for testability,
* same pattern as roomLogic.ts.
*/
import { handleInput, ClientMessage } from "../src/rooms/inputHandler";
import { UnitManager, UnitRecord } from "../src/systems/UnitManager";
function freshManager(): UnitManager {
return new UnitManager();
}
describe("handleInput - spawnUnit", () => {
it("delegates to UnitManager.spawnUnit and returns the unit", () => {
const mgr = freshManager();
const msg: ClientMessage = {
type: "spawnUnit",
unitType: "tank",
position: { x: 50, y: 50 },
team: "ukraine",
};
const result = handleInput(mgr, "p1", msg) as UnitRecord;
expect(result.ownerId).toBe("p1");
expect(result.type).toBe("tank");
expect(result.position).toEqual({ x: 50, y: 50 });
expect(result.team).toBe("ukraine");
});
it("uses infantry if unitType is infantry", () => {
const mgr = freshManager();
const msg: ClientMessage = {
type: "spawnUnit",
unitType: "infantry",
position: { x: 0, y: 0 },
team: "russia",
};
const result = handleInput(mgr, "p2", msg) as UnitRecord;
expect(result.type).toBe("infantry");
expect(result.health.max).toBe(100);
});
});
describe("handleInput - moveUnit", () => {
it("delegates to UnitManager.moveUnit", () => {
const mgr = freshManager();
const unit = mgr.spawnUnit("p1", "tank", { x: 0, y: 0 }, "ukraine");
const msg: ClientMessage = {
type: "moveUnit",
unitId: unit.id,
path: [{ x: 10, y: 0 }, { x: 10, y: 10 }],
};
const result = handleInput(mgr, "p1", msg) as UnitRecord;
expect(result.state).toBe("MOVING");
expect(result.path).toEqual(msg.path);
});
});
describe("handleInput - attackUnit", () => {
it("delegates to UnitManager.attackUnit", () => {
const mgr = freshManager();
const attacker = mgr.spawnUnit("p1", "tank", { x: 0, y: 0 }, "ukraine");
const target = mgr.spawnUnit("p2", "infantry", { x: 50, y: 50 }, "russia");
const msg: ClientMessage = {
type: "attackUnit",
unitId: attacker.id,
targetId: target.id,
};
const result = handleInput(mgr, "p1", msg) as UnitRecord;
expect(result.state).toBe("ATTACKING");
expect(result.targetId).toBe(target.id);
});
});
describe("handleInput - damageUnit", () => {
it("delegates to UnitManager.damageUnit", () => {
const mgr = freshManager();
const unit = mgr.spawnUnit("p1", "tank", { x: 0, y: 0 }, "ukraine");
const msg: ClientMessage = {
type: "damageUnit",
unitId: unit.id,
amount: 30,
};
const result = handleInput(mgr, "p1", msg) as UnitRecord;
expect(result.health.current).toBe(120);
});
});
describe("handleInput - removeDeadUnits", () => {
it("delegates to UnitManager.removeDeadUnits", () => {
const mgr = freshManager();
const unit = mgr.spawnUnit("p1", "tank", { x: 0, y: 0 }, "ukraine");
mgr.damageUnit(unit.id, 999);
const msg: ClientMessage = {
type: "removeDeadUnits",
};
const result = handleInput(mgr, "p1", msg) as string[];
expect(result).toEqual([unit.id]);
expect(mgr.getUnit(unit.id)).toBeUndefined();
});
});
describe("handleInput - applyEvent", () => {
it("applies a UnitEvent to a unit", () => {
const mgr = freshManager();
const unit = mgr.spawnUnit("p1", "tank", { x: 0, y: 0 }, "ukraine");
const msg: ClientMessage = {
type: "applyEvent",
unitId: unit.id,
event: "MOVE",
};
const result = handleInput(mgr, "p1", msg) as UnitRecord;
expect(result.state).toBe("MOVING");
});
});
describe("handleInput - unknown type", () => {
it("returns null for unrecognized message type", () => {
const mgr = freshManager();
const msg = { type: "unknownCommand" } as ClientMessage;
expect(handleInput(mgr, "p1", msg)).toBeNull();
});
});
describe("handleInput - getUnitsInRange", () => {
it("returns units in range for the given team", () => {
const mgr = freshManager();
mgr.spawnUnit("p2", "infantry", { x: 5, y: 5 }, "russia");
mgr.spawnUnit("p2", "infantry", { x: 500, y: 500 }, "russia");
const msg: ClientMessage = {
type: "getUnitsInRange",
position: { x: 0, y: 0 },
range: 10,
team: "russia",
};
const result = handleInput(mgr, "p1", msg) as UnitRecord[];
expect(result.length).toBe(1);
});
});
describe("handleInput - full client flow", () => {
it("spawn → move → damage → cleanup", () => {
const mgr = freshManager();
// spawn
const spawned = handleInput(mgr, "p1", {
type: "spawnUnit",
unitType: "tank",
position: { x: 0, y: 0 },
team: "ukraine",
}) as UnitRecord;
expect(spawned.state).toBe("IDLING");
// move
const moved = handleInput(mgr, "p1", {
type: "moveUnit",
unitId: spawned.id,
path: [{ x: 100, y: 100 }],
}) as UnitRecord;
expect(moved.state).toBe("MOVING");
// damage to kill
const damaged = handleInput(mgr, "p1", {
type: "damageUnit",
unitId: spawned.id,
amount: 200,
}) as UnitRecord;
expect(damaged.state).toBe("DYING");
// cleanup
const cleaned = handleInput(mgr, "p1", {
type: "removeDeadUnits",
}) as string[];
expect(cleaned).toContain(spawned.id);
});
});