feat(t5): Auswahlgruppe mit Haken, Karten-Variante, Plus/Minus (T5a A6)

- ChoiceGroup nach Spec 10.2: gewählt mit Haken und 2-px-Tintenrahmen,
  „Nicht geprüft“ gestrichelt; neue Variante appearance="cards" mit Icon
  und Beschreibung (Winter-Ergebnis, Darstellung, Behördenmeldung).
- Eigenbau-Radios in Behördenmeldung, Phänologie-Bericht und
  Zeilen-Ausrichtung durch ChoiceGroup ersetzt.
- Button pressed → aria-pressed (Fett/Kursiv, „Aufbau als Liste“);
  Druck-Versatz nur mit motion-full.
- NumberStepper (ganze Schritte) für Winter-Waben, Milbenzahl (neu und
  bearbeiten), Weiselzellen, Entnahme/Zählbestand, Startposition; Entnahme
  nur 1 bis Restmenge, Startposition höchstens 1000.
- Baukasten-Wächter: Radios nur in ChoiceGroup, aria-pressed nur in
  Button und Kalender, auch in Objektform und .ts-Dateien.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick.RosenloecherandClaude Opus 5.5 committed 2026-10-16 18:44:31 +02:00
1 parent 6ac9eef93b
commit 208e9baaf0
30 files changed
+809 -198

No files matched your search

@@ -1,9 +1,10 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import { render, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { StockCard } from "./stock-card";
import { fetchStock } from "@/lib/stock/api";
import type { BatchStock } from "@/lib/stock/types";
import { correctStock, fetchStock, withdrawStock } from "@/lib/stock/api";
import type { BatchStock, StockBatchPath } from "@/lib/stock/types";
vi.mock("@/lib/stock/api", () => ({
fetchStock: vi.fn(),
@@ -11,17 +12,17 @@ vi.mock("@/lib/stock/api", () => ({
correctStock: vi.fn(),
}));
function renderCard(canBook = true) {
function renderCard(canBook = true, unit = "Gläser", path: StockBatchPath = "bottling-batches") {
const client = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
return render(
<QueryClientProvider client={client}>
<StockCard
path="bottling-batches"
path={path}
id="b1"
initialLabel="Abgefüllt"
unit="Gläser"
unit={unit}
canBook={canBook}
/>
</QueryClientProvider>,
@@ -107,4 +108,61 @@ describe("StockCard", () => {
screen.queryByRole("button", { name: /Inventur/ }),
).not.toBeInTheDocument();
});
it("stock card withdraw and counted stock use NumberStepper", async () => {
vi.mocked(fetchStock).mockResolvedValue(stock(80));
vi.mocked(withdrawStock).mockResolvedValue(undefined as never);
vi.mocked(correctStock).mockResolvedValue(undefined as never);
renderCard();
await userEvent.click(await screen.findByRole("button", { name: /Entnehmen/ }));
let dialog = await screen.findByRole("dialog");
const quantity = within(dialog).getByLabelText("Anzahl (Gläser)");
expect(quantity).toHaveValue("1");
expect(quantity).toHaveAttribute("inputmode", "numeric");
await userEvent.click(within(dialog).getByRole("button", { name: /Eins mehr/ }));
await userEvent.click(within(dialog).getByRole("button", { name: "Entnahme buchen" }));
expect(vi.mocked(withdrawStock).mock.calls[0][2]).toMatchObject({ quantity: 2 });
await userEvent.click(await screen.findByRole("button", { name: /Inventur korrigieren/ }));
dialog = await screen.findByRole("dialog");
const counted = within(dialog).getByLabelText("Gezählt (Gläser)");
expect(counted).toHaveValue("80");
await userEvent.click(within(dialog).getByRole("button", { name: /Eins weniger/ }));
expect(within(dialog).getByText(/Differenz: -1/)).toBeInTheDocument();
});
it("keeps a typed field for amounts in grams", async () => {
vi.mocked(fetchStock).mockResolvedValue(stock(500));
renderCard(true, "g", "raw-material-lots");
await userEvent.click(await screen.findByRole("button", { name: /Entnehmen/ }));
const dialog = await screen.findByRole("dialog");
expect(within(dialog).getByLabelText("Anzahl (g)")).toHaveAttribute("type", "number");
expect(within(dialog).queryByRole("button", { name: /mehr$/ })).toBeNull();
});
it("does not book an empty withdrawal", async () => {
vi.mocked(fetchStock).mockResolvedValue(stock(80));
renderCard();
await userEvent.click(await screen.findByRole("button", { name: /Entnehmen/ }));
const dialog = await screen.findByRole("dialog");
await userEvent.clear(within(dialog).getByLabelText("Anzahl (Gläser)"));
expect(within(dialog).getByRole("button", { name: "Entnahme buchen" })).toBeDisabled();
});
it("does not book a withdrawal of 0 or more than is left", async () => {
vi.mocked(fetchStock).mockResolvedValue(stock(80));
renderCard();
await userEvent.click(await screen.findByRole("button", { name: /Entnehmen/ }));
const dialog = await screen.findByRole("dialog");
const quantity = within(dialog).getByLabelText("Anzahl (Gläser)");
const book = within(dialog).getByRole("button", { name: "Entnahme buchen" });
await userEvent.clear(quantity);
await userEvent.type(quantity, "0");
expect(book).toBeDisabled();
await userEvent.clear(quantity);
await userEvent.type(quantity, "80");
expect(book).toBeEnabled();
});
});