feat(ui+legacy): F8-05 owner-feedback round 1 — inspector tweaks + parser
Tests · UI / test (push) Has been cancelled
Tests · UI / test (pull_request) Successful in 2m45s

Owner-reported polish on top of #48, plus a legacy-parser gap that
prevented verifying stationed ship groups against a real .REP fixture.

UI:
- Production: drop the empty `(production)` placeholder option. Owned
  planets always produce something, so the primary select now opens on
  `industry` by default when `planet.production` is null/unknown,
  keeping the row inside the four real production kinds at all times.
- Production: lock the row to a single line (no flex-wrap) and strip
  border + padding from the ✓/✗ buttons so the apply/cancel icons read
  as glyphs and the row no longer breaks into two visual rows for
  Research / Ship contexts where both selects are present.
- Cargo routes: the placeholder option is now an `<option disabled>`
  styled like a section header (greyed, italic) and reads "manage
  routes" instead of "cargo routes". The wording shifts the intent
  from a section label to an action prompt.

Legacy parser:
- F8-05 (#48 п.32) "Stationed ship groups" couldn't be verified against
  the dg fixture because the legacy `<Race> Groups` blocks (outside
  battles) and the `Unidentified Groups` block were dropped by the
  parser — both are now wired up. Foreign group rows parse the
  `# T D W S C T Q D P M` columns and resolve the destination against
  the parsed planet tables (rows with an invisible destination drop,
  matching the existing local-group convention). The legacy row
  carries no origin / range columns, so foreign groups surface as
  stationed at the destination.
- Smoke tests on every fixture extended with `otherGroups` and
  `unidentifiedGroups` counts. New focused unit test
  `TestParseOtherAndUnidentifiedGroups` covers the column layout, the
  drop-on-unknown-destination rule, and the `X Y`-only unidentified
  rows.
- `tools/local-dev/reports/dg/KNNTS039.json` and
  `tools/local-dev/reports/dg/KNNTS041.json` regenerated so the
  synthetic-loader fixtures carry the new arrays.
- README updated: the two sections move out of "Skipped sections" into
  a "Foreign and unidentified groups" block; package doc-comment
  reflects the broader scope.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Ilia Denisov
2026-05-27 15:21:55 +02:00
parent aee5f39a7e
commit cc4bc3c2b7
10 changed files with 46770 additions and 9291 deletions
@@ -124,13 +124,15 @@ function getTarget(
describe("planet inspector — production controls", () => {
test("renders the main select with localised options and ✓/✗ icons", () => {
// No production is set on the seeded planet → the select falls
// back to the documented `industry` default (an owned planet
// always produces something on the engine side, so there is no
// "(none)" placeholder option).
const ui = mountProduction(localPlanet({ number: 1 }));
const main = getMain(ui);
expect(main.value).toBe("");
expect(main.value).toBe("industry");
const labels = Array.from(main.options).map((o) => o.textContent?.trim());
// One placeholder + the four production kinds, in the documented order.
expect(labels).toEqual([
"(production)",
"industry",
"materials",
"research",
@@ -140,18 +142,20 @@ describe("planet inspector — production controls", () => {
expect(
ui.queryByTestId("inspector-planet-production-target"),
).toBeNull();
// The row is dirty against the seeded `production: null`, so
// both icon buttons are enabled — the player can either ✓ to
// confirm the default or ✗ to revert (back to industry again).
expect(
ui.getByTestId("inspector-planet-production-apply"),
).toBeDisabled();
).not.toBeDisabled();
expect(
ui.getByTestId("inspector-planet-production-cancel"),
).toBeDisabled();
).not.toBeDisabled();
});
test("Industry pick + ✓ emits a CAP setProductionType command", async () => {
test("Industry default + ✓ emits a CAP setProductionType command", async () => {
const ui = mountProduction(localPlanet({ number: 7 }));
const main = getMain(ui);
await fireEvent.change(main, { target: { value: "industry" } });
expect(getMain(ui).value).toBe("industry");
const apply = ui.getByTestId("inspector-planet-production-apply");
expect(apply).not.toBeDisabled();
await fireEvent.click(apply);
@@ -268,7 +272,7 @@ describe("planet inspector — production controls", () => {
test("active main derivation seeds the select from planet.production", () => {
const cases: ReadonlyArray<{
production: string | null;
expected: "" | "industry" | "materials" | "research" | "ship";
expected: "industry" | "materials" | "research" | "ship";
}> = [
{ production: "Capital", expected: "industry" },
{ production: "Material", expected: "materials" },
@@ -277,9 +281,11 @@ describe("planet inspector — production controls", () => {
{ production: "Shields", expected: "research" },
{ production: "Cargo", expected: "research" },
{ production: "Scout", expected: "ship" },
{ production: "-", expected: "" },
{ production: null, expected: "" },
{ production: "UnknownThing", expected: "" },
// Falls back to the documented `industry` default when the
// engine display string is missing or unrecognised.
{ production: "-", expected: "industry" },
{ production: null, expected: "industry" },
{ production: "UnknownThing", expected: "industry" },
];
for (const tc of cases) {
const ui = mountProduction(