import { expect, test, type Page } from "@playwright/test"; /** * Mission planning map: an editor enables the map on a mission, places an * Arma icon marker (with text), a hatched rectangle, and a line, edits the * selection from the panel, and everything survives a reload. Seeds its own * map + mission through cookie-authed REST (needs the test DB's superuser dev). */ test.describe("Mission planning map", () => { let mapId: number | null = null; let missionId: number | null = null; async function login(page: Page) { const res = await page.request.post("/api/users/login", { data: { username: "dev", password: "Test123" }, }); expect(res.ok(), `login failed: ${res.status()}`).toBe(true); } async function markers(page: Page) { const res = await page.request.get( `/api/mission-map-markers?where[mission][equals]=${missionId}&depth=0&limit=100&sort=id`, ); expect(res.ok()).toBe(true); return ((await res.json()).docs ?? []) as { id: number; shape: string; markerType: string | null; color: string; brush: string | null; text: string | null; points: number[][] | null; }[]; } test.afterAll(async ({ browser }) => { const page = await browser.newPage(); await login(page); if (missionId) await page.request.delete(`/api/missions/${missionId}`); if (mapId) await page.request.delete(`/api/maps/${mapId}`); await page.close(); }); test("enable, place icon/area/line, edit, persist", async ({ page }) => { await login(page); // Tours would overlay the page; opt this browser out before any page runs. await page.addInitScript(() => window.localStorage.setItem("ptf:tours:opted-out", "1")); const mapRes = await page.request.post("/api/maps", { data: { name: "E2E Planning Map", worldSizeWidth: 8192, worldSizeHeight: 8192, basemapMode: "image", isActive: true, }, }); expect(mapRes.ok(), await mapRes.text()).toBe(true); mapId = (await mapRes.json()).doc.id; const meRes = await page.request.get("/api/users/me"); const meId = (await meRes.json()).user.id as number; const missionRes = await page.request.post("/api/missions", { data: { name: "E2E Planning Op", codeName: "E2E-PLAN", summary: "Planning map e2e mission", operationType: "main", classification: { map: mapId, missionType: "PvE", startDateTime: new Date(Date.now() + 3 * 86_400_000).toISOString(), estimatedDuration: 60, }, ownershipAndStatus: { authors: [meId], status: "Scheduled", visibility: "unit" }, briefing: [], gameDetails: { serverDetails: { serverIp: "127.0.0.1", serverPort: 2302 } }, missionRoles: { maxPlayers: 32, providedRoles: [] }, }, }); expect(missionRes.ok(), await missionRes.text()).toBe(true); missionId = (await missionRes.json()).doc.id; await page.goto(`/intelligence/missions/${missionId}`); await page.getByRole("button", { name: "Enable planning map" }).click(); const section = page.locator("section[aria-label='Mission planning map']"); await expect(section).toBeVisible(); const map = section.locator(".leaflet-container"); await expect(map).toBeVisible(); await page.waitForTimeout(1000); // Panel edits can scroll the page, so measure the map before every use. const at = async (fx: number, fy: number) => { await map.scrollIntoViewIfNeeded(); const box = (await map.boundingBox())!; return { x: box.x + box.width * fx, y: box.y + box.height * fy }; }; // 1. NATO icon (OPFOR armor), red, with text. await section.getByRole("radio", { name: "Icon" }).click(); await section.getByRole("button", { name: "Marker type" }).click(); await page.getByPlaceholder("Search markers...").fill("o_armor"); await page.locator("button[title*='(o_armor)']").first().click(); await section.getByRole("button", { name: "Red", exact: true }).click(); await section.getByPlaceholder("Label shown beside the icon").fill("ENEMY ARMOR"); let p = await at(0.55, 0.35); await page.mouse.click(p.x, p.y); await expect(section.locator(".arma-marker-text", { hasText: "ENEMY ARMOR" })).toBeVisible(); // 2. Rectangle with the Grid brush, drawn corner to corner. await section.getByRole("radio", { name: "Rectangle" }).click(); await section.getByRole("combobox").click(); await page.getByRole("option", { name: "Grid", exact: true }).click(); // Radix Select blocks pointer events on until it finishes closing. await expect(page.locator("body")).not.toHaveCSS("pointer-events", "none"); const a = await at(0.3, 0.5); const b = await at(0.5, 0.7); await page.mouse.move(a.x, a.y); await page.mouse.down(); await page.mouse.move(b.x, b.y, { steps: 8 }); await page.mouse.up(); await expect(section.getByText("Selected // Rectangle")).toBeVisible(); // 3. Line: click, click, double-click. await section.getByRole("radio", { name: "Line" }).click(); for (const [fx, fy] of [ [0.2, 0.3], [0.35, 0.25], ]) { p = await at(fx, fy); await page.mouse.click(p.x, p.y); await page.waitForTimeout(150); } p = await at(0.45, 0.35); await page.mouse.dblclick(p.x, p.y); await expect(section.getByText("Selected // Line")).toBeVisible(); await expect .poll(async () => (await markers(page)).map((m) => m.shape).sort()) .toEqual(["icon", "polyline", "rectangle"]); const saved = await markers(page); const icon = saved.find((m) => m.shape === "icon")!; expect(icon).toMatchObject({ markerType: "o_armor", color: "ColorRed", text: "ENEMY ARMOR" }); expect(saved.find((m) => m.shape === "rectangle")?.brush).toBe("Grid"); expect(saved.find((m) => m.shape === "polyline")?.points).toHaveLength(3); // 4. Select the icon and edit its text from the panel (debounced save). await section.getByRole("radio", { name: "Select" }).click(); await section.locator(".arma-marker-text", { hasText: "ENEMY ARMOR" }).click(); await expect(section.getByText("Selected // Icon")).toBeVisible(); await section.getByPlaceholder("Label shown beside the icon").fill("T-72 PLT"); await expect .poll(async () => (await markers(page)).find((m) => m.id === icon.id)?.text) .toBe("T-72 PLT"); await section.screenshot({ path: "test-results/mission-planning-map.png" }); // 5. Reload: the plan persists and the readout counts it. await page.reload(); const reloaded = page.locator("section[aria-label='Mission planning map']"); await expect(reloaded.locator(".arma-marker-text", { hasText: "T-72 PLT" })).toBeVisible(); const footer = reloaded.locator("div.border-t.bg-black\\/60"); await expect(footer).toContainText("1Icons"); await expect(footer).toContainText("1Areas"); await expect(footer).toContainText("1Lines"); // 6. Delete key removes the selected marker. await reloaded.locator(".arma-marker-text", { hasText: "T-72 PLT" }).click(); await page.keyboard.press("Delete"); await expect.poll(async () => (await markers(page)).length).toBe(2); }); });