// Vitest component coverage for the read-only planet inspector. // Each kind has a dedicated case so the per-kind field gating // (which fields are present, which are hidden) is verified // explicitly. The component is purely presentational, so the tests // drive it with synthetic `ReportPlanet` literals — no store. import "@testing-library/jest-dom/vitest"; import "fake-indexeddb/auto"; import { fireEvent, render } from "@testing-library/svelte"; import { beforeEach, describe, expect, test } from "vitest"; import { i18n } from "../src/lib/i18n/index.svelte"; import type { ReportPlanet } from "../src/api/game-state"; import Planet from "../src/lib/inspectors/planet.svelte"; import { ORDER_DRAFT_CONTEXT_KEY, OrderDraftStore, } from "../src/sync/order-draft.svelte"; import { IDBCache } from "../src/platform/store/idb-cache"; import { openGalaxyDB } from "../src/platform/store/idb"; beforeEach(() => { i18n.resetForTests("en"); }); function makePlanet(overrides: Partial): ReportPlanet { return { number: 0, name: "", x: 0, y: 0, kind: "local", owner: null, size: null, resources: null, industryStockpile: null, materialsStockpile: null, industry: null, population: null, colonists: null, production: null, freeIndustry: null, ...overrides, }; } describe("planet inspector", () => { test("local planet renders the full economy field set", () => { const ui = render(Planet, { props: { planet: makePlanet({ number: 7, name: "Home World", kind: "local", x: 100.25, y: 200, size: 1000, resources: 10, population: 950, colonists: 50, industry: 800, industryStockpile: 12.5, materialsStockpile: 30, production: "Drive", freeIndustry: 187.5, }), localShipClass: [], routes: [], planets: [], mapWidth: 1, mapHeight: 1, localPlayerDrive: 0, }, }); const section = ui.getByTestId("inspector-planet"); expect(section).toHaveAttribute("data-planet-id", "7"); expect(section).toHaveAttribute("data-planet-kind", "local"); expect(ui.getByTestId("inspector-planet-name")).toHaveTextContent( "Home World", ); expect(ui.getByTestId("inspector-planet-kind")).toHaveTextContent( "your planet", ); expect( ui.getByTestId("inspector-planet-field-coordinates"), ).toHaveTextContent("(100.25, 200)"); expect(ui.getByTestId("inspector-planet-field-size")).toHaveTextContent( "size", ); expect( ui.getByTestId("inspector-planet-field-natural_resources"), ).toHaveTextContent("10"); expect( ui.getByTestId("inspector-planet-field-population"), ).toHaveTextContent("950"); expect( ui.getByTestId("inspector-planet-field-colonists"), ).toHaveTextContent("50"); expect( ui.getByTestId("inspector-planet-field-industry"), ).toHaveTextContent("800"); expect( ui.getByTestId("inspector-planet-field-industry_stockpile"), ).toHaveTextContent("12.5"); expect( ui.getByTestId("inspector-planet-field-materials_stockpile"), ).toHaveTextContent("30"); // Phase 15: the static "current production" row is replaced by // the interactive Production component for owned planets. expect(ui.queryByTestId("inspector-planet-field-production")).toBeNull(); expect(ui.getByTestId("inspector-planet-production")).toBeInTheDocument(); expect( ui.getByTestId("inspector-planet-field-free_industry"), ).toHaveTextContent("187.5"); expect(ui.queryByTestId("inspector-planet-field-owner")).toBeNull(); expect(ui.queryByTestId("inspector-planet-no-data")).toBeNull(); }); test("other-race planet shows the owner row", () => { const ui = render(Planet, { props: { planet: makePlanet({ number: 9, name: "Far Away", kind: "other", owner: "Federation", size: 700, resources: 5, population: 500, colonists: 12, industry: 400, industryStockpile: 5, materialsStockpile: 8, production: "weapons", freeIndustry: 75, }), localShipClass: [], routes: [], planets: [], mapWidth: 1, mapHeight: 1, localPlayerDrive: 0, }, }); expect(ui.getByTestId("inspector-planet-kind")).toHaveTextContent( "other race planet", ); expect(ui.getByTestId("inspector-planet-field-owner")).toHaveTextContent( "Federation", ); expect( ui.getByTestId("inspector-planet-field-population"), ).toHaveTextContent("500"); // Non-local planets keep the read-only production row. expect( ui.getByTestId("inspector-planet-field-production"), ).toHaveTextContent("weapons"); expect(ui.queryByTestId("inspector-planet-production")).toBeNull(); }); test("uninhabited planet hides population, industry, and production rows", () => { const ui = render(Planet, { props: { planet: makePlanet({ number: 3, name: "Bare Rock", kind: "uninhabited", size: 250, resources: 1.5, industryStockpile: 0, materialsStockpile: 0, }), localShipClass: [], routes: [], planets: [], mapWidth: 1, mapHeight: 1, localPlayerDrive: 0, }, }); expect(ui.getByTestId("inspector-planet-kind")).toHaveTextContent( "uninhabited planet", ); expect(ui.getByTestId("inspector-planet-name")).toHaveTextContent( "Bare Rock", ); expect(ui.getByTestId("inspector-planet-field-size")).toHaveTextContent( "250", ); expect( ui.getByTestId("inspector-planet-field-natural_resources"), ).toHaveTextContent("1.5"); expect(ui.queryByTestId("inspector-planet-field-population")).toBeNull(); expect(ui.queryByTestId("inspector-planet-field-colonists")).toBeNull(); expect(ui.queryByTestId("inspector-planet-field-industry")).toBeNull(); expect(ui.queryByTestId("inspector-planet-field-production")).toBeNull(); expect(ui.queryByTestId("inspector-planet-field-free_industry")).toBeNull(); expect(ui.queryByTestId("inspector-planet-field-owner")).toBeNull(); }); test("unidentified planet shows the no-data hint and only coordinates", () => { const ui = render(Planet, { props: { planet: makePlanet({ number: 42, kind: "unidentified", x: 1234, y: -5, }), localShipClass: [], routes: [], planets: [], mapWidth: 1, mapHeight: 1, localPlayerDrive: 0, }, }); expect(ui.getByTestId("inspector-planet-kind")).toHaveTextContent( "unidentified planet", ); expect(ui.queryByTestId("inspector-planet-name")).toBeNull(); expect(ui.getByTestId("inspector-planet-no-data")).toHaveTextContent( "no data", ); expect( ui.getByTestId("inspector-planet-field-coordinates"), ).toHaveTextContent("(1,234, -5)"); expect(ui.queryByTestId("inspector-planet-field-size")).toBeNull(); expect(ui.queryByTestId("inspector-planet-field-natural_resources")).toBeNull(); }); test("Rename action is hidden for non-local planets", () => { const ui = render(Planet, { props: { planet: makePlanet({ number: 9, name: "Far", kind: "other", owner: "Federation", size: 100, resources: 5, }), localShipClass: [], routes: [], planets: [], mapWidth: 1, mapHeight: 1, localPlayerDrive: 0, }, }); expect(ui.queryByTestId("inspector-planet-rename-action")).toBeNull(); }); test("Rename action opens an inline editor and validates locally", async () => { const dbName = `galaxy-rename-${crypto.randomUUID()}`; const db = await openGalaxyDB(dbName); const cache = new IDBCache(db); const draft = new OrderDraftStore(); await draft.init({ cache, gameId: "00000000-0000-0000-0000-000000000abc" }); const context = new Map([ [ORDER_DRAFT_CONTEXT_KEY, draft], ]); const ui = render(Planet, { props: { planet: makePlanet({ number: 7, name: "Earth", kind: "local", size: 100, resources: 5, population: 100, colonists: 0, industry: 0, industryStockpile: 0, materialsStockpile: 0, production: "Drive", freeIndustry: 0, }), localShipClass: [], routes: [], planets: [], mapWidth: 1, mapHeight: 1, localPlayerDrive: 0, }, context, }); const action = ui.getByTestId("inspector-planet-rename-action"); await fireEvent.click(action); const input = ui.getByTestId("inspector-planet-rename-input") as HTMLInputElement; expect(input.value).toBe("Earth"); const confirm = ui.getByTestId("inspector-planet-rename-confirm"); expect(confirm).not.toBeDisabled(); await fireEvent.input(input, { target: { value: " " } }); expect(ui.getByTestId("inspector-planet-rename-error")).toBeVisible(); expect(confirm).toBeDisabled(); await fireEvent.input(input, { target: { value: "New Earth!" } }); // Whitespace inside disallowed expect(ui.getByTestId("inspector-planet-rename-error")).toBeVisible(); expect(confirm).toBeDisabled(); await fireEvent.input(input, { target: { value: "Mars-2" } }); expect(ui.queryByTestId("inspector-planet-rename-error")).toBeNull(); expect(confirm).not.toBeDisabled(); await fireEvent.click(confirm); expect(draft.commands).toHaveLength(1); const cmd = draft.commands[0]!; expect(cmd.kind).toBe("planetRename"); if (cmd.kind !== "planetRename") return; expect(cmd.planetNumber).toBe(7); expect(cmd.name).toBe("Mars-2"); draft.dispose(); db.close(); }); test("Cancel closes the editor without adding to the draft", async () => { const dbName = `galaxy-rename-${crypto.randomUUID()}`; const db = await openGalaxyDB(dbName); const cache = new IDBCache(db); const draft = new OrderDraftStore(); await draft.init({ cache, gameId: "00000000-0000-0000-0000-000000000abc" }); const context = new Map([ [ORDER_DRAFT_CONTEXT_KEY, draft], ]); const ui = render(Planet, { props: { planet: makePlanet({ number: 1, name: "Earth", kind: "local", size: 100, resources: 5, population: 1, colonists: 0, industry: 0, industryStockpile: 0, materialsStockpile: 0, production: "Drive", freeIndustry: 0, }), localShipClass: [], routes: [], planets: [], mapWidth: 1, mapHeight: 1, localPlayerDrive: 0, }, context, }); await fireEvent.click(ui.getByTestId("inspector-planet-rename-action")); await fireEvent.click(ui.getByTestId("inspector-planet-rename-cancel")); expect(ui.queryByTestId("inspector-planet-rename")).toBeNull(); expect(draft.commands).toEqual([]); draft.dispose(); db.close(); }); test("non-local planets fall back to the localised production placeholder", () => { const ui = render(Planet, { props: { planet: makePlanet({ number: 5, name: "Idle", kind: "other", owner: "Drift", size: 800, resources: 1, population: 1, colonists: 0, industry: 0, industryStockpile: 0, materialsStockpile: 0, production: "", freeIndustry: 0, }), localShipClass: [], routes: [], planets: [], mapWidth: 1, mapHeight: 1, localPlayerDrive: 0, }, }); // Empty production strings collapse to the localised "none" // placeholder on the read-only path. The local-planet branch // owns the production surface via the interactive component // instead and is covered by `inspector-planet-production.test.ts`. expect( ui.getByTestId("inspector-planet-field-production"), ).toHaveTextContent("none"); }); });