f7109af55c
Two follow-up fixes after the initial Phase 19 landing:
1. The IncomingGroup dashed trajectory was drawn between raw
(x1, y1) and (x2, y2) world coordinates. On torus wrap mode
this took the long way around when origin and destination
sat near opposite seams. The line now picks endpoints via
`torusShortestDelta` so the segment crosses the seam when
that's the shorter visual path. The interpolated clickable
point follows the same unwrapped vector. The same helper
fixes the in-hyperspace position for local / foreign groups.
2. On-planet local and foreign groups previously rendered as
small offset points next to every populated planet, which
turned the canvas into noise as soon as a player held more
than a handful of planets. The map no longer renders any
in-orbit group; the planet inspector grows a compact
"stationed ship groups" subsection
(`lib/inspectors/planet/ship-groups.svelte`) that lists
each in-orbit group as a row of `<race> · <class> · <count>
ships · <mass>`. Race attribution: LocalGroup → the player's
race, OtherGroup on a foreign-owned planet → the planet's
owner, OtherGroup elsewhere → "foreign" placeholder. Rows
are non-interactive in Phase 19; Phase 21+ will deep-link
into the ship-groups table view with a (planet, race) filter.
Tests:
- `state-binding-groups.test.ts` swaps the on-planet rendering
expectation for the new "no map primitive" rule, and adds a
regression that asserts the incoming line crosses the torus
seam via `torusShortestDelta`.
- new `inspector-planet-ship-groups.test.ts` covers row
composition, the destination-mismatch filter, the
in-hyperspace exclusion, the foreign-planet owner fallback,
and the empty-state collapse.
- `inspector-planet.test.ts` and `inspector-ship-group.spec.ts`
pick up the new prop chain (`localShipGroups`,
`otherShipGroups`, `localRace`).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
424 lines
12 KiB
TypeScript
424 lines
12 KiB
TypeScript
// 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>): 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,
|
|
localShipGroups: [],
|
|
otherShipGroups: [],
|
|
localRace: "",
|
|
},
|
|
});
|
|
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,
|
|
localShipGroups: [],
|
|
otherShipGroups: [],
|
|
localRace: "",
|
|
},
|
|
});
|
|
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,
|
|
localShipGroups: [],
|
|
otherShipGroups: [],
|
|
localRace: "",
|
|
},
|
|
});
|
|
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,
|
|
localShipGroups: [],
|
|
otherShipGroups: [],
|
|
localRace: "",
|
|
},
|
|
});
|
|
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,
|
|
localShipGroups: [],
|
|
otherShipGroups: [],
|
|
localRace: "",
|
|
},
|
|
});
|
|
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<unknown, unknown>([
|
|
[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,
|
|
localShipGroups: [],
|
|
otherShipGroups: [],
|
|
localRace: "",
|
|
},
|
|
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<unknown, unknown>([
|
|
[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,
|
|
localShipGroups: [],
|
|
otherShipGroups: [],
|
|
localRace: "",
|
|
},
|
|
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,
|
|
localShipGroups: [],
|
|
otherShipGroups: [],
|
|
localRace: "",
|
|
},
|
|
});
|
|
// 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");
|
|
});
|
|
});
|