// Vitest component coverage for the Phase 16 cargo-routes // subsection of the planet inspector. Drives the component against // a real `OrderDraftStore` (with `fake-indexeddb` standing in for // the browser IDB factory) and a stub `MapPickService` whose // `pick(...)` resolves to a script-controlled answer. The tests // assert the four-slot rendering, the picker invocation, the // per-(source, loadType) collapse rule, and the cancel path. import "@testing-library/jest-dom/vitest"; import "fake-indexeddb/auto"; import { fireEvent, render, waitFor } from "@testing-library/svelte"; import { afterEach, beforeEach, describe, expect, test } from "vitest"; import { i18n } from "../src/lib/i18n/index.svelte"; import type { ReportPlanet, ReportRoute } from "../src/api/game-state"; import CargoRoutes from "../src/lib/inspectors/planet/cargo-routes.svelte"; import { MAP_PICK_CONTEXT_KEY, MapPickService, type MapPickRequest, } from "../src/lib/map-pick.svelte"; import { ORDER_DRAFT_CONTEXT_KEY, OrderDraftStore, } from "../src/sync/order-draft.svelte"; import { IDBCache } from "../src/platform/store/idb-cache"; import { openGalaxyDB, type GalaxyDB } from "../src/platform/store/idb"; import type { Cache } from "../src/platform/store/index"; import type { IDBPDatabase } from "idb"; const GAME_ID = "11111111-2222-3333-4444-555555555555"; let db: IDBPDatabase; let dbName: string; let cache: Cache; let draft: OrderDraftStore; beforeEach(async () => { dbName = `galaxy-cargo-${crypto.randomUUID()}`; db = await openGalaxyDB(dbName); cache = new IDBCache(db); draft = new OrderDraftStore(); await draft.init({ cache, gameId: GAME_ID }); i18n.resetForTests("en"); }); afterEach(async () => { draft.dispose(); db.close(); await new Promise((resolve) => { const req = indexedDB.deleteDatabase(dbName); req.onsuccess = () => resolve(); req.onerror = () => resolve(); req.onblocked = () => resolve(); }); }); function makePlanet( overrides: Partial & Pick, ): ReportPlanet { return { name: `Planet-${overrides.number}`, x: 0, y: 0, kind: "local", owner: null, size: 100, resources: 1, industryStockpile: 0, materialsStockpile: 0, industry: 0, population: 0, colonists: 0, production: null, freeIndustry: 0, ...overrides, }; } interface PickInvocation { request: MapPickRequest; resolve: (id: number | null) => void; } class StubPickService extends MapPickService { invocations: PickInvocation[] = []; override pick(request: MapPickRequest): Promise { this.active = true; return new Promise((resolve) => { this.invocations.push({ request, resolve: (id) => { this.active = false; resolve(id); }, }); }); } override cancel(): void { const inv = this.invocations.shift(); inv?.resolve(null); } } function mount( planet: ReportPlanet, planets: ReportPlanet[], routes: ReportRoute[] = [], localPlayerDrive = 2, mapWidth = 4000, mapHeight = 4000, ) { const pick = new StubPickService(); const context = new Map([ [ORDER_DRAFT_CONTEXT_KEY, draft], [MAP_PICK_CONTEXT_KEY, pick], ]); const ui = render(CargoRoutes, { props: { planet, routes, planets, mapWidth, mapHeight, localPlayerDrive, }, context, }); return { ui, pick }; } describe("planet inspector — cargo routes", () => { test("renders four slots in COL/CAP/MAT/EMP order", () => { const { ui, pick } = mount( makePlanet({ number: 1, name: "Earth", x: 100, y: 100 }), [makePlanet({ number: 1, name: "Earth", x: 100, y: 100 })], ); const slots = ui.container.querySelectorAll( "[data-testid^='inspector-planet-cargo-slot-']", ); const slotIds = Array.from(slots).map((el) => el.getAttribute("data-testid"), ); // Each slot generates several test ids (label + body items); // pick the row data-testid (slot itself, no suffix). const rowIds = slotIds.filter((id) => /^inspector-planet-cargo-slot-(col|cap|mat|emp)$/.test(id ?? ""), ); expect(rowIds).toEqual([ "inspector-planet-cargo-slot-col", "inspector-planet-cargo-slot-cap", "inspector-planet-cargo-slot-mat", "inspector-planet-cargo-slot-emp", ]); }); test("an empty slot exposes the Add button and the (no route) marker", () => { const { ui, pick } = mount( makePlanet({ number: 1, name: "Earth", x: 100, y: 100 }), [makePlanet({ number: 1, name: "Earth", x: 100, y: 100 })], ); expect( ui.getByTestId("inspector-planet-cargo-slot-col-empty"), ).toBeInTheDocument(); expect( ui.getByTestId("inspector-planet-cargo-slot-col-add"), ).toBeInTheDocument(); expect( ui.queryByTestId("inspector-planet-cargo-slot-col-edit"), ).toBeNull(); }); test("a filled slot shows the destination name plus Edit and Remove", () => { const { ui, pick } = mount( makePlanet({ number: 1, name: "Earth", x: 100, y: 100 }), [ makePlanet({ number: 1, name: "Earth", x: 100, y: 100 }), makePlanet({ number: 2, name: "Mars", x: 150, y: 100 }), ], [ { sourcePlanetNumber: 1, entries: [{ loadType: "COL", destinationPlanetNumber: 2 }], }, ], ); expect( ui.getByTestId("inspector-planet-cargo-slot-col-destination"), ).toHaveTextContent("Mars"); expect( ui.getByTestId("inspector-planet-cargo-slot-col-edit"), ).toBeInTheDocument(); expect( ui.getByTestId("inspector-planet-cargo-slot-col-remove"), ).toBeInTheDocument(); expect( ui.queryByTestId("inspector-planet-cargo-slot-col-add"), ).toBeNull(); }); test("Add opens pick mode with the reach-filtered set", async () => { // Reach = 40 * 2 = 80. Mars is 50 away (in reach), Pluto is // 200 away (out of reach). const { ui, pick } = mount( makePlanet({ number: 1, name: "Earth", x: 100, y: 100 }), [ makePlanet({ number: 1, name: "Earth", x: 100, y: 100 }), makePlanet({ number: 2, name: "Mars", x: 150, y: 100 }), makePlanet({ number: 3, name: "Pluto", x: 300, y: 100 }), ], [], 2, ); await fireEvent.click(ui.getByTestId("inspector-planet-cargo-slot-col-add")); await waitFor(() => expect(pick.invocations.length).toBe(1)); const invocation = pick.invocations[0]!; expect(invocation.request.sourcePlanetNumber).toBe(1); expect(Array.from(invocation.request.reachableIds).sort()).toEqual([2]); expect( ui.getByTestId("inspector-planet-cargo-pick-prompt"), ).toBeInTheDocument(); }); test("a successful pick emits setCargoRoute and closes the prompt", async () => { const { ui, pick } = mount( makePlanet({ number: 1, name: "Earth", x: 100, y: 100 }), [ makePlanet({ number: 1, name: "Earth", x: 100, y: 100 }), makePlanet({ number: 2, name: "Mars", x: 150, y: 100 }), ], [], 2, ); await fireEvent.click(ui.getByTestId("inspector-planet-cargo-slot-cap-add")); await waitFor(() => expect(pick.invocations.length).toBe(1)); pick.invocations[0]!.resolve(2); await waitFor(() => expect(draft.commands).toHaveLength(1)); const cmd = draft.commands[0]!; expect(cmd.kind).toBe("setCargoRoute"); if (cmd.kind !== "setCargoRoute") return; expect(cmd.sourcePlanetNumber).toBe(1); expect(cmd.destinationPlanetNumber).toBe(2); expect(cmd.loadType).toBe("CAP"); await waitFor(() => expect( ui.queryByTestId("inspector-planet-cargo-pick-prompt"), ).toBeNull(), ); }); test("cancel resolves null and emits no command", async () => { const { ui, pick } = mount( makePlanet({ number: 1, name: "Earth", x: 100, y: 100 }), [ makePlanet({ number: 1, name: "Earth", x: 100, y: 100 }), makePlanet({ number: 2, name: "Mars", x: 150, y: 100 }), ], ); await fireEvent.click(ui.getByTestId("inspector-planet-cargo-slot-mat-add")); await waitFor(() => expect(pick.invocations.length).toBe(1)); pick.invocations[0]!.resolve(null); await waitFor(() => expect( ui.queryByTestId("inspector-planet-cargo-pick-prompt"), ).toBeNull(), ); expect(draft.commands).toHaveLength(0); }); test("Remove emits removeCargoRoute for the slot", async () => { const { ui, pick } = mount( makePlanet({ number: 1, name: "Earth", x: 100, y: 100 }), [ makePlanet({ number: 1, name: "Earth", x: 100, y: 100 }), makePlanet({ number: 2, name: "Mars", x: 150, y: 100 }), ], [ { sourcePlanetNumber: 1, entries: [{ loadType: "EMP", destinationPlanetNumber: 2 }], }, ], ); await fireEvent.click( ui.getByTestId("inspector-planet-cargo-slot-emp-remove"), ); await waitFor(() => expect(draft.commands).toHaveLength(1)); const cmd = draft.commands[0]!; expect(cmd.kind).toBe("removeCargoRoute"); if (cmd.kind !== "removeCargoRoute") return; expect(cmd.sourcePlanetNumber).toBe(1); expect(cmd.loadType).toBe("EMP"); }); test("Edit replaces the existing setCargoRoute via collapse rule", async () => { const { ui, pick } = mount( makePlanet({ number: 1, name: "Earth", x: 100, y: 100 }), [ makePlanet({ number: 1, name: "Earth", x: 100, y: 100 }), makePlanet({ number: 2, name: "Mars", x: 150, y: 100 }), makePlanet({ number: 3, name: "Vesta", x: 100, y: 150 }), ], [ { sourcePlanetNumber: 1, entries: [{ loadType: "COL", destinationPlanetNumber: 2 }], }, ], ); await fireEvent.click( ui.getByTestId("inspector-planet-cargo-slot-col-edit"), ); await waitFor(() => expect(pick.invocations.length).toBe(1)); pick.invocations[0]!.resolve(3); await waitFor(() => expect(draft.commands).toHaveLength(1)); // Then a second edit to a different planet — collapse keeps a // single row. await fireEvent.click( ui.getByTestId("inspector-planet-cargo-slot-col-edit"), ); await waitFor(() => expect(pick.invocations.length).toBe(2)); pick.invocations[1]!.resolve(2); await waitFor(() => expect(draft.commands).toHaveLength(1)); const cmd = draft.commands[0]!; expect(cmd.kind).toBe("setCargoRoute"); if (cmd.kind !== "setCargoRoute") return; expect(cmd.destinationPlanetNumber).toBe(2); }); test("different load-types coexist without collapsing each other", async () => { const { ui, pick } = mount( makePlanet({ number: 1, name: "Earth", x: 100, y: 100 }), [ makePlanet({ number: 1, name: "Earth", x: 100, y: 100 }), makePlanet({ number: 2, name: "Mars", x: 150, y: 100 }), ], ); await fireEvent.click(ui.getByTestId("inspector-planet-cargo-slot-col-add")); await waitFor(() => expect(pick.invocations.length).toBe(1)); pick.invocations[0]!.resolve(2); await waitFor(() => expect(draft.commands).toHaveLength(1)); await fireEvent.click(ui.getByTestId("inspector-planet-cargo-slot-cap-add")); await waitFor(() => expect(pick.invocations.length).toBe(2)); pick.invocations[1]!.resolve(2); await waitFor(() => expect(draft.commands).toHaveLength(2)); const types = draft.commands .filter((c) => c.kind === "setCargoRoute") .map((c) => (c.kind === "setCargoRoute" ? c.loadType : "")) .sort(); expect(types).toEqual(["CAP", "COL"]); }); test("no_destinations message appears when reach is positive but every planet is out of range", () => { const { ui, pick } = mount( makePlanet({ number: 1, name: "Earth", x: 100, y: 100 }), [ makePlanet({ number: 1, name: "Earth", x: 100, y: 100 }), makePlanet({ number: 2, name: "Pluto", x: 5000, y: 5000 }), ], [], 0.1, // reach 4 — far less than 5000 distance ); expect( ui.getByTestId("inspector-planet-cargo-no-destinations"), ).toBeInTheDocument(); }); });