diff --git a/src/components/labs/__tests__/lab-unit-input.browser.test.tsx b/src/components/labs/__tests__/lab-unit-input.browser.test.tsx new file mode 100644 index 000000000..d11dff4cf --- /dev/null +++ b/src/components/labs/__tests__/lab-unit-input.browser.test.tsx @@ -0,0 +1,56 @@ +import { useState } from "react"; +import { describe, expect, it } from "vitest"; +import { userEvent } from "vitest/browser"; + +import { renderWithApp } from "../../../../tests/browser/render"; +import { LabUnitInput } from "../lab-unit-input"; + +/** + * The unit field as a form holds it, typed into key by key. A unit that + * starts with a 1 (10^9/L, 1/min) passes through "1" on the way, and that + * keystroke must not turn into the "no unit" choice. + */ +function Controlled({ initial = "" }: { initial?: string }) { + const [unit, setUnit] = useState(initial); + return ( + <> + + + {unit} + + ); +} + +describe(" typed into", () => { + it("keeps a unit that starts with a 1", async () => { + const { screen } = await renderWithApp(); + await userEvent.type(screen.getByLabelText("Unit"), "10^9/L"); + await expect + .element(screen.getByTestId("unit")) + .toHaveTextContent("10^9/L"); + await expect + .element(screen.getByRole("checkbox", { name: /no unit/i })) + .not.toBeChecked(); + }); + + it("shows a stored unity as the ticked choice and gives the field back on unticking", async () => { + const { screen } = await renderWithApp(); + const box = screen.getByRole("checkbox", { name: /no unit/i }); + await expect.element(box).toBeChecked(); + await expect.element(screen.getByLabelText("Unit")).toBeDisabled(); + await box.click(); + await expect.element(screen.getByLabelText("Unit")).toBeEnabled(); + await userEvent.type(screen.getByLabelText("Unit"), "mmol/L"); + await expect + .element(screen.getByTestId("unit")) + .toHaveTextContent("mmol/L"); + }); + + it("sets the unity when the box is ticked", async () => { + const { screen } = await renderWithApp(); + await screen.getByRole("checkbox", { name: /no unit/i }).click(); + await expect + .poll(() => screen.getByTestId("unit").element().textContent) + .toBe("1"); + }); +}); diff --git a/src/components/labs/lab-unit-input.tsx b/src/components/labs/lab-unit-input.tsx index cdbc5d1df..7545f791e 100644 --- a/src/components/labs/lab-unit-input.tsx +++ b/src/components/labs/lab-unit-input.tsx @@ -1,6 +1,6 @@ "use client"; -import { useId, useRef } from "react"; +import { useId, useRef, useState } from "react"; import { Checkbox } from "@/components/ui/checkbox"; import { Input } from "@/components/ui/input"; @@ -48,8 +48,9 @@ interface LabUnitInputProps { * The unit of a lab value, with an explicit "no unit" choice for a ratio or an * index. Ticking it sets the unit to the UCUM unity `"1"`, which every write * path accepts as a real unit; leaving the field empty is still refused, so a - * unit an extraction dropped is never taken for a value that has none. Typing - * `1` by hand is the same choice and ticks the box. + * unit an extraction dropped is never taken for a value that has none. Only + * the box makes the choice: a unit typed key by key passes through "1" on its + * way to `10^9/L`, and that keystroke stays text. */ export function LabUnitInput({ id, @@ -64,7 +65,9 @@ export function LabUnitInput({ const { t } = useTranslations(); const noneId = useId(); const hintId = `${noneId}-hint`; - const none = allowNoUnit && isDimensionlessUnit(value); + // Set when a keystroke, not the box, left the field at "1". + const [typedOne, setTypedOne] = useState(false); + const none = allowNoUnit && isDimensionlessUnit(value) && !typedOne; // What was typed before the box was ticked, so unticking gives it back. const typed = useRef(none ? "" : value); @@ -74,6 +77,7 @@ export function LabUnitInput({ value={none ? "" : value} onChange={(e) => { typed.current = e.target.value; + setTypedOne(isDimensionlessUnit(e.target.value)); onChange(e.target.value); }} placeholder={none ? t("labs.unit.none") : placeholder} @@ -95,15 +99,16 @@ export function LabUnitInput({ + onCheckedChange={(state) => { + setTypedOne(false); onChange( state === true ? DIMENSIONLESS_UNIT : isDimensionlessUnit(typed.current) ? "" : typed.current, - ) - } + ); + }} aria-describedby={hintId} className="mt-0.5" />