fix(ui): F8-06 calculator polish — always 3-decimal display, mono font, input cap
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:
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user