Files
octoturge 826233e10c Add player position tracking relay + rendering (Phase 7b)
Relays the mod's throttled player_positions roster over a new
/ws/players/:serverId gateway (Redis pub/sub + snapshot key so a
tab connecting between mod flushes isn't empty), gated per-server
by a new playerPositionsVisible admin toggle independent of the
mod's own tracking config. Frontend renders the roster as map
markers with a show/hide toggle and online count. Covered by unit
tests (players.test.ts, ws-gateway.test.ts, admin.test.ts) and a
new e2e spec that plays the real mod WS protocol from inside a
browser context.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015tKdPZt78zbPUZMXWzKEKt
2026-08-09 20:07:11 +02:00

97 lines
4.5 KiB
TypeScript

// Covers Phase 7b's player-position relay end to end: a simulated mod connection sends a
// player_positions roster over the real WS gateway, and a real browser tab — subscribed via
// /ws/players/:serverId — is expected to render it as a map marker and update the online count.
// Nothing here is seeded via SQL (unlike markers.spec.ts's terrain footprint) since there's
// nothing to seed: the roster only exists once the WS protocol actually runs, which is exactly
// the gap a headless unit test (ws-gateway.test.ts) can't close — it never proves a real browser
// on the other end of players-gateway.ts's Redis pub/sub actually receives and renders it.
import { test, expect, type Page } from "@playwright/test";
import { readE2eState, API_PORT } from "../config";
const state = readE2eState();
async function waitForMapReady(page: Page) {
await page.goto("/");
await expect(page.getByTestId("players-toggle")).toBeVisible();
await page.locator("#map.leaflet-container").waitFor();
}
/**
* Opens a throwaway WS connection *inside the browser context* (not Node-side) — this page
* already has other in-app sockets open (chat, players), but a raw WebSocket a test script opens
* here is entirely independent of those; Chromium has no trouble holding several at once. Playing
* the mod's own wire protocol (see ws-gateway.ts's doc comment) rather than reaching into
* Redis/Postgres directly is the point: it's what proves the real WS handler path works, not just
* whatever a lower-level test harness could fake.
*/
async function sendAsMod(page: Page, port: number, token: string, message: object) {
await page.evaluate(
([port, token, message]) => {
return new Promise<void>((resolve, reject) => {
const ws = new WebSocket(`ws://localhost:${port}/ws`);
ws.onopen = () => ws.send(JSON.stringify({ type: "hello", token }));
ws.onmessage = (ev) => {
const parsed = JSON.parse(ev.data as string);
if (parsed.type === "hello_ack" && parsed.ok) {
ws.send(JSON.stringify(message));
setTimeout(() => {
ws.close();
resolve();
}, 200);
} else if (parsed.type === "hello_ack" && !parsed.ok) {
reject(new Error(`hello rejected: ${parsed.error}`));
}
};
ws.onerror = () => reject(new Error("mod ws connection failed"));
});
},
[port, token, message] as const,
);
}
test("a player roster sent over the mod WS protocol shows up as a marker on the map", async ({ page }) => {
await waitForMapReady(page);
await expect(page.getByTestId("players-online-count")).toHaveText("0 online");
await sendAsMod(page, API_PORT, state.serverToken, {
type: "player_positions",
dimension: 0,
players: [{ uuid: "e2e-player-1", username: "E2EPlayer", x: 5, y: 64, z: 5 }],
});
await expect(page.getByTestId("players-online-count")).toHaveText("1 online");
await expect(page.locator(".leaflet-tooltip", { hasText: "E2EPlayer" })).toBeVisible();
});
test("unchecking 'show' hides player markers without dropping the roster count", async ({ page }) => {
await waitForMapReady(page);
await sendAsMod(page, API_PORT, state.serverToken, {
type: "player_positions",
dimension: 0,
players: [{ uuid: "e2e-player-2", username: "ToggleTest", x: 1, y: 64, z: 1 }],
});
await expect(page.getByTestId("players-online-count")).toHaveText("1 online");
await expect(page.locator(".leaflet-tooltip", { hasText: "ToggleTest" })).toBeVisible();
await page.getByTestId("players-toggle").uncheck();
await expect(page.locator(".leaflet-tooltip", { hasText: "ToggleTest" })).toHaveCount(0);
// The roster itself is untouched by the visibility toggle — only the map layer is.
await expect(page.getByTestId("players-online-count")).toHaveText("1 online");
});
test("an empty roster (everyone logged out) clears existing markers", async ({ page }) => {
await waitForMapReady(page);
await sendAsMod(page, API_PORT, state.serverToken, {
type: "player_positions",
dimension: 0,
players: [{ uuid: "e2e-player-3", username: "LoggingOut", x: 2, y: 64, z: 2 }],
});
await expect(page.locator(".leaflet-tooltip", { hasText: "LoggingOut" })).toBeVisible();
await sendAsMod(page, API_PORT, state.serverToken, { type: "player_positions", dimension: 0, players: [] });
await expect(page.getByTestId("players-online-count")).toHaveText("0 online");
await expect(page.locator(".leaflet-tooltip", { hasText: "LoggingOut" })).toHaveCount(0);
});