import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; import { buildUniverseMap } from "./build-universe-map.mjs"; const html = readFileSync(new URL("./star-map.html", import.meta.url), "utf8"); const startScript = readFileSync(new URL("./start-webserver.cmd", import.meta.url), "utf8"); function extractJsonScript(id) { const pattern = new RegExp(``); const match = html.match(pattern); assert.ok(match, `missing JSON script #${id}`); return JSON.parse(match[1]); } test("universe import builds a grid map from universe.ini", async () => { const data = await buildUniverseMap({ universeDir: "C:/PROJECTS/REM/REM-Mod/DATA/UNIVERSE" }); assert.equal(data.source, "universe.ini"); assert.equal(data.systems.length, 58); assert.ok(data.connections.length >= 80, "expected direct jump connections from system INIs"); assert.deepEqual(data.grid, { minCol: 0, minRow: 0, maxCol: 16, maxRow: 16, cellSize: 72, margin: 72 }); const newYork = data.systems.find((system) => system.id === "li01"); assert.ok(newYork, "missing Li01"); assert.equal(newYork.name, "New York"); assert.equal(newYork.gridX, 7); assert.equal(newYork.gridY, 8); assert.equal(newYork.x, 576); assert.equal(newYork.y, 648); assert.ok(data.connections.some((connection) => connection.from === "li01" && connection.to === "li02")); }); test("template contains the systems and connections from universe.ini", () => { const data = extractJsonScript("initial-map-data"); assert.equal(data.source, "universe.ini"); assert.equal(data.systems.length, 58, "expected active systems from universe.ini"); assert.ok(data.connections.length >= 80, "expected direct route set from system INIs"); assert.ok(data.grid, "expected grid metadata"); for (const name of ["Tau-37", "Leeds", "New Tokyo", "Sigma-19", "New Berlin", "Omicron Prime"]) { assert.ok(data.systems.some((system) => system.name === name), `missing ${name}`); } for (const connection of data.connections) { assert.ok(data.systems.some((system) => system.id === connection.from), `missing source ${connection.from}`); assert.ok(data.systems.some((system) => system.id === connection.to), `missing target ${connection.to}`); } }); test("editor UI exposes creation, deletion, linking, notes, and background controls", () => { assert.match(html, /data-mode="add"/, "missing add-system mode"); assert.match(html, /data-mode="connect"/, "missing connection mode"); assert.match(html, /id="deleteSelected"/, "missing delete action"); assert.match(html, /id="systemNote"/, "missing per-system note field"); assert.match(html, /id="backgroundColor"/, "missing configurable background"); assert.match(html, /id="systemColor"/, "missing per-system color control"); assert.match(html, /id="saveRemote"/, "missing persistent save action"); assert.match(html, /id="loadRemote"/, "missing persistent load action"); assert.doesNotMatch(html, /id="snapToGrid"/, "grid snapping must not be optional"); assert.match(html, /function snapPointToGrid/, "missing grid snapping behavior"); assert.doesNotMatch(html, /snapToGrid\.checked/, "grid snapping must always be active"); assert.match(html, /function createSystem/, "missing system creation behavior"); assert.match(html, /function createConnection/, "missing connection creation behavior"); assert.match(html, /async function saveRemoteMap/, "missing server save behavior"); assert.match(html, /async function loadRemoteMap/, "missing server load behavior"); }); test("system dragging has a forgiving hit target and stable pointer handlers", () => { assert.match(html, /createElementNS\("http:\/\/www\.w3\.org\/2000\/svg", "circle"\)[\s\S]*?class", "system-hit"/, "missing circular system drag hit target"); assert.doesNotMatch(html, /createElementNS\("http:\/\/www\.w3\.org\/2000\/svg", "rect"\)[\s\S]*?class", "system-hit"/, "system labels must not be covered by click hit rects"); assert.match(html, /function stopLabelEvent/, "missing label event blocker"); assert.match(html, /label\.addEventListener\("click", stopLabelEvent\)/, "labels should swallow clicks without selecting objects"); assert.match(html, /function handleDragMove/, "missing shared drag move handler"); assert.match(html, /window\.addEventListener\("pointermove", handleDragMove\)/, "missing window pointermove handler"); assert.match(html, /window\.addEventListener\("pointerup", handleDragEnd\)/, "missing window pointerup handler"); }); test("map supports zoom and easier connection selection", () => { assert.match(html, /id="zoomIn"/, "missing zoom-in action"); assert.match(html, /id="zoomOut"/, "missing zoom-out action"); assert.match(html, /id="resetZoom"/, "missing reset zoom action"); assert.match(html, /function zoomMap/, "missing zoom behavior"); assert.match(html, /svg\.addEventListener\("wheel"/, "missing wheel zoom"); assert.match(html, /<\/g>\s*<\/g>/, "connection hit layer should sit above visible lines"); assert.match(html, /\.connection\s*\{[\s\S]*?pointer-events:\s*none;/, "visible connections should not block hit lines"); assert.match(html, /\.connection-hit\s*\{[\s\S]*?stroke-width:\s*28;/, "connection hit target should be wide"); }); test("start script launches the local server and opens the browser", () => { assert.match(startScript, /node.*server\.mjs/i, "start script must launch server.mjs with node"); assert.match(startScript, /127\.0\.0\.1:3000/i, "start script must open the local app URL"); assert.match(startScript, /Start-Process/i, "start script must open the browser"); });