826233e10c
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
97 lines
4.5 KiB
TypeScript
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);
|
|
});
|