fix(ui): F8-06 calculator polish — always 3-decimal display, mono font, input cap
Tests · UI / test (push) Has been cancelled
Tests · Integration / integration (pull_request) Successful in 1m48s
Tests · Go / test (pull_request) Successful in 2m3s
Tests · UI / test (pull_request) Successful in 2m28s

Owner feedback round 2 on PR #61:

- Pad every read-only calculator value to three decimals: tech labels,
  derived results (mass, speed, attack, defence, bombing, cargo
  capacity), planet MAT, planet build-rate, modernization cost, and
  the full-cargo capacity label all read as "1.000" instead of "1",
  matching the goal-seek back-solved input and the report. Drops
  thousands grouping so the same `fmt()` string also embeds cleanly
  in the read-only `<input type="number">` cell.
- Switch label and input styling onto the existing `--font-mono`
  token (right-aligned, tabular-nums) so columns line up vertically
  across rows like a financial table.
- Refuse a fourth decimal as the user types in every calculator
  number input (DWSC blocks, tech, MAT, custom load, lock value,
  modernization target tech): the `oninput` truncates the input text
  past three decimal digits and explicitly writes the truncated
  value back through `bind:value`, so Svelte's later reactive flush
  cannot undo the cap.
- Doc + tests follow the rule (five new vitest cases covering the
  3-decimal label format, the input cap on each input class, and
  the integer-padding rule for derived results).
This commit is contained in:
Ilia Denisov
2026-05-26 18:43:32 +02:00
parent cbf7f65916
commit cc4727a32e
4 changed files with 150 additions and 23 deletions
+81 -12
View File
@@ -401,15 +401,12 @@ describe("calculator-tab", () => {
});
test("renders unoverridden tech as a 3-decimal label (matches the report)", () => {
// Player drive tech 1.2 → "1.200" via the shared ceil3 formatter.
// Player drive tech 1.2 → "1.200" via the shared ceil3 formatter,
// always padded to three decimals (calculator labels are column-
// aligned with the report).
const ui = mount();
expect(ui.getByTestId("calculator-tech-value-drive")).toHaveTextContent(
"1.2",
);
// Stable column-aligned formatting (3 decimals) is what the report
// uses, so the tech labels read consistently.
const tech = ui.getByTestId("calculator-tech-value-drive");
expect(tech.textContent ?? "").toMatch(/^1\.20?0?$/);
expect((tech.textContent ?? "").trim()).toBe("1.200");
});
test("planet MAT label renders through the 3-decimal formatter", () => {
@@ -419,12 +416,84 @@ describe("calculator-tab", () => {
report: makeReport({ planets: [LOCAL_PLANET] }),
selection,
});
// Planet MAT is 100 → "100" through the shared formatter; the
// Planet MAT is 100 → "100.000" through the shared formatter; the
// label is monospaced + right-aligned via the existing `.mat-val`
// rule. Formatting check: no stray fractional digits on integers.
expect(
ui.getByTestId("calculator-planet-mat-value"),
).toHaveTextContent("100");
// rule. Integer MAT pads to three decimals like every other label.
const mat = ui.getByTestId("calculator-planet-mat-value");
expect((mat.textContent ?? "").trim()).toBe("100.000");
});
test("derived results pad to three decimals (integer empty mass)", async () => {
// Integer-valued outputs read with the same trailing zeros as
// fractional ones — column-aligned tabular display.
const ui = mount();
await setBlock(ui, "drive", 10);
await setBlock(ui, "shields", 5);
await setBlock(ui, "cargo", 5);
const mass = ui.getByTestId("calculator-out-emptyMass");
expect((mass.textContent ?? "").trim()).toBe("20.000");
});
test("number inputs refuse a fourth decimal as the user types", async () => {
const selection = new SelectionStore();
selection.selectPlanet(17);
const ui = mount({
report: makeReport({ planets: [LOCAL_PLANET] }),
selection,
});
// MAT input: typed "12.3456" must clamp to "12.345" on input.
await fireEvent.click(ui.getByTestId("calculator-mat-override"));
const mat = ui.getByTestId("calculator-planet-mat") as HTMLInputElement;
await fireEvent.input(mat, { target: { value: "12.3456" } });
expect(mat.value).toBe("12.345");
expect(mat.valueAsNumber).toBeCloseTo(12.345, 9);
// Custom-load input on a ship with a non-zero cargo: typed
// "1.2345" must clamp to "1.234".
await setBlock(ui, "drive", 10);
await setBlock(ui, "shields", 5);
await setBlock(ui, "cargo", 5);
await fireEvent.click(ui.getByTestId("calculator-load-custom"));
const load = ui.getByTestId("calculator-custom-load") as HTMLInputElement;
await fireEvent.input(load, { target: { value: "1.2345" } });
expect(load.value).toBe("1.234");
});
test("tech and target-tech inputs cap at three decimals", async () => {
const ui = mount();
// Tech override input.
await fireEvent.click(ui.getByTestId("calculator-tech-override-drive"));
const tech = ui.getByTestId("calculator-tech-drive") as HTMLInputElement;
await fireEvent.input(tech, { target: { value: "2.9999" } });
expect(tech.value).toBe("2.999");
// Modernization target tech input.
await fireEvent.click(ui.getByTestId("calculator-mode-modernization"));
const target = ui.getByTestId(
"calculator-target-drive",
) as HTMLInputElement;
await fireEvent.input(target, { target: { value: "3.1416" } });
expect(target.value).toBe("3.141");
});
test("lock value input caps at three decimals", async () => {
const ui = mount();
await setBlock(ui, "drive", 10);
await setBlock(ui, "shields", 5);
await setBlock(ui, "cargo", 5);
await fireEvent.click(ui.getByTestId("calculator-lock-attack"));
const lock = ui.getByTestId(
"calculator-locked-attack",
) as HTMLInputElement;
await fireEvent.input(lock, { target: { value: "0.1234" } });
expect(lock.value).toBe("0.123");
});
test("ship-block input caps at three decimals", async () => {
const ui = mount();
const drive = ui.getByTestId("calculator-block-drive") as HTMLInputElement;
await fireEvent.input(drive, { target: { value: "1.2345" } });
expect(drive.value).toBe("1.234");
});
test("lock spinner step is replaced by ArrowUp/ArrowDown (±0.001)", async () => {