- Political map country names were invisible because the label SVG had a zero-sized viewport, which some browsers never paint; size it to the world and guard it with a test. - Added a Devlog button showing the ten latest commits, generated into client/js/devlog_data.js by scripts/generate-devlog.js and refreshed by the pre-commit hook. - Worked through FIXME.md: the city button and a unit double-click now open the city, and the stat tooltip escapes the HUD so the unit panel cannot cover it. - The server creates its game at startup and clients only join it; the Create game screen and the configure_game path are gone, and testing is a --testing server flag. - A page on a loopback host signs in as adrien, claims France and gets the free, instant testing buttons, while the server grants free orders only to a loopback socket whose Origin is also loopback.
226 lines
7.2 KiB
JavaScript
226 lines
7.2 KiB
JavaScript
import { TestCase } from "./framework/test_case.js";
|
|
import { setupDom, teardownDom, FakeWebSocket } from "./framework/dom.js";
|
|
import { Network } from "../client/js/net.js";
|
|
|
|
export class NetTest extends TestCase {
|
|
async connected(url = "ws://localhost/ws") {
|
|
const env = await setupDom();
|
|
FakeWebSocket.reset();
|
|
const network = new Network();
|
|
const promise = network.connect(url);
|
|
const ws = FakeWebSocket.last;
|
|
ws.simulateOpen();
|
|
await promise;
|
|
return { env, network, ws };
|
|
}
|
|
|
|
async test_default_server_url_uses_the_page_origin() {
|
|
const env = await setupDom();
|
|
try {
|
|
this.assertEqual(new Network().defaultServerUrl(), "ws://game.example/ws");
|
|
} finally {
|
|
teardownDom(env);
|
|
}
|
|
}
|
|
|
|
async test_normalize_url_accepts_hosts_and_urls() {
|
|
const env = await setupDom();
|
|
try {
|
|
const network = new Network();
|
|
this.assertEqual(network.normalizeUrl("example.com"), "ws://example.com:27015/ws");
|
|
this.assertEqual(network.normalizeUrl("example.com", 1234), "ws://example.com:1234/ws");
|
|
this.assertEqual(network.normalizeUrl("http://example.com"), "ws://example.com/ws");
|
|
this.assertEqual(network.normalizeUrl("https://example.com/ws"), "wss://example.com/ws");
|
|
this.assertEqual(network.normalizeUrl("ws://example.com/socket"), "ws://example.com/socket");
|
|
} finally {
|
|
teardownDom(env);
|
|
}
|
|
}
|
|
|
|
async test_request_game_list_sends_and_receives() {
|
|
const { env, network, ws } = await this.connected();
|
|
try {
|
|
let games = null;
|
|
network.on("game_list_received", (value) => {
|
|
games = value;
|
|
});
|
|
const promise = network.requestGameList("ws://localhost/ws");
|
|
const socket = FakeWebSocket.last;
|
|
socket.simulateOpen();
|
|
await promise;
|
|
this.assertEqual(socket.sent[0], { t: "request_games" });
|
|
socket.simulateMessage({ t: "game_list", games: [{ name: "Game" }] });
|
|
this.assertEqual(games, [{ name: "Game" }]);
|
|
} finally {
|
|
teardownDom(env);
|
|
}
|
|
}
|
|
|
|
async test_login_success_emits_session_started() {
|
|
const env = await setupDom();
|
|
try {
|
|
FakeWebSocket.reset();
|
|
const network = new Network();
|
|
let started = null;
|
|
network.on("session_started", (message) => {
|
|
started = message;
|
|
});
|
|
const promise = network.joinGame("ws://localhost/ws", "Alice", "pw");
|
|
const ws = FakeWebSocket.last;
|
|
ws.simulateOpen();
|
|
await promise;
|
|
this.assertEqual(ws.lastSent(), { t: "login", name: "Alice", password: "pw" });
|
|
this.assertFalse(network.isClientConnected());
|
|
ws.simulateMessage({ t: "assigned", playerId: 7, admin: true, name: "Alice" });
|
|
this.assertEqual(started.playerId, 7);
|
|
this.assertEqual(network.localPlayerId, 7);
|
|
this.assertTrue(network.isAdmin);
|
|
this.assertTrue(network.isClientConnected());
|
|
} finally {
|
|
teardownDom(env);
|
|
}
|
|
}
|
|
|
|
async test_login_rejection_emits_session_failed() {
|
|
const { env, network, ws } = await this.connected();
|
|
try {
|
|
let failed = null;
|
|
network.on("session_failed", (message) => {
|
|
failed = message;
|
|
});
|
|
ws.simulateMessage({ t: "login_rejected", reason: "Wrong password for Alice." });
|
|
this.assertEqual(failed, "Wrong password for Alice.");
|
|
} finally {
|
|
teardownDom(env);
|
|
}
|
|
}
|
|
|
|
async test_players_and_game_state_events() {
|
|
const { env, network, ws } = await this.connected();
|
|
try {
|
|
let players = null;
|
|
let state = null;
|
|
network.on("players_updated", (value) => {
|
|
players = value;
|
|
});
|
|
network.on("game_state_received", (value) => {
|
|
state = value;
|
|
});
|
|
ws.simulateMessage({ t: "players", players: { 1: { id: 1, name: "A", admin: true } } });
|
|
this.assertEqual(players["1"].name, "A");
|
|
this.assertTrue(network.players["1"].admin);
|
|
ws.simulateMessage({ t: "game_state", state: { seed: 5, viewer: 0 } });
|
|
this.assertEqual(state.seed, 5);
|
|
} finally {
|
|
teardownDom(env);
|
|
}
|
|
}
|
|
|
|
async test_game_state_deltas_merge_into_the_last_full_state() {
|
|
const { env, network, ws } = await this.connected();
|
|
try {
|
|
let state = null;
|
|
network.on("game_state_received", (value) => {
|
|
state = value;
|
|
});
|
|
ws.simulateMessage({
|
|
t: "game_state",
|
|
state: { seed: 5, viewer: 0, territory: [[0, 0, 1]], explored: [[0, 0]], units: [] },
|
|
});
|
|
this.assertEqual(state.territory.length, 1, "the first state is used whole");
|
|
|
|
// A delta updates only what it carries and keeps the rest.
|
|
ws.simulateMessage({
|
|
t: "game_state",
|
|
state: { delta: true, units: [{ id: 1 }], exploredAdded: [[1, 0]] },
|
|
});
|
|
this.assertEqual(state.territory.length, 1, "unchanged territory is kept");
|
|
this.assertSize(state.units, 1, "the changed field is updated");
|
|
this.assertSize(state.explored, 2, "newly explored tiles are appended");
|
|
this.assertEqual(state.explored[1], [1, 0]);
|
|
this.assertNull(state.exploredAdded, "the temporary tail is dropped");
|
|
|
|
// A full state replaces the base.
|
|
ws.simulateMessage({
|
|
t: "game_state",
|
|
state: { seed: 6, viewer: 0, territory: [], explored: [], units: [] },
|
|
});
|
|
this.assertEqual(state.seed, 6, "a full state replaces the base");
|
|
} finally {
|
|
teardownDom(env);
|
|
}
|
|
}
|
|
|
|
async test_disconnected_without_failure_reports_it() {
|
|
const { env, network, ws } = await this.connected();
|
|
try {
|
|
let disconnected = false;
|
|
network.on("server_disconnected", () => {
|
|
disconnected = true;
|
|
});
|
|
ws.simulateClose();
|
|
this.assertTrue(disconnected);
|
|
} finally {
|
|
teardownDom(env);
|
|
}
|
|
}
|
|
|
|
async test_send_order_is_ignored_while_disconnected() {
|
|
const env = await setupDom();
|
|
try {
|
|
FakeWebSocket.reset();
|
|
const network = new Network();
|
|
network.sendOrder({ type: "move" });
|
|
this.assertEmpty(FakeWebSocket.instances, "no socket was opened");
|
|
network.close();
|
|
} finally {
|
|
teardownDom(env);
|
|
}
|
|
}
|
|
|
|
async test_join_game_sends_login() {
|
|
const env = await setupDom();
|
|
try {
|
|
FakeWebSocket.reset();
|
|
const network = new Network();
|
|
const promise = network.joinGame("ws://example.com/ws", "Bob", "pw");
|
|
const ws = FakeWebSocket.last;
|
|
this.assertEqual(ws.url, "ws://example.com/ws");
|
|
ws.simulateOpen();
|
|
await promise;
|
|
this.assertEqual(ws.lastSent(), { t: "login", name: "Bob", password: "pw" });
|
|
} finally {
|
|
teardownDom(env);
|
|
}
|
|
}
|
|
|
|
async test_unknown_messages_are_ignored() {
|
|
const { env, network, ws } = await this.connected();
|
|
try {
|
|
ws.simulateMessage({ t: "something-unknown" });
|
|
this.assertEqual(network.localPlayerId, 0);
|
|
} finally {
|
|
teardownDom(env);
|
|
}
|
|
}
|
|
|
|
async test_connection_error_rejects_the_connect_promise() {
|
|
const env = await setupDom();
|
|
try {
|
|
FakeWebSocket.reset();
|
|
const network = new Network();
|
|
const promise = network.connect("ws://localhost/ws");
|
|
FakeWebSocket.last.simulateError();
|
|
let failed = false;
|
|
try {
|
|
await promise;
|
|
} catch {
|
|
failed = true;
|
|
}
|
|
this.assertTrue(failed, "the connection promise rejected");
|
|
} finally {
|
|
teardownDom(env);
|
|
}
|
|
}
|
|
}
|