Datumsfelder lassen sich wieder tippen
In den Berichten hing jedes Datumsfeld unmittelbar an der Adresszeile: eine Aenderung hiess router.push, also neu laden und neu rendern. Ein <input type="date"> meldet beim Tippen der Jahreszahl aber viermal -- 0002, 0020, 0202, 2026 -- und die ersten drei loesten je eine Navigation aus, die das Feld auf den Stand aus der Adresse zuruecksetzte. Mitten im Tippen. Mit dem Kalender ging es, weil der in einem Zug ein fertiges Datum setzt; das war der Hinweis darauf, wo es klemmt. DateField haelt den Tippstand jetzt bei sich und meldet nur, was eine Aussage ist: ein vollstaendiges Datum mit vierstelliger Jahreszahl ab 1000, oder das Leeren des Feldes. Beim Verlassen wird nachgereicht, was liegengeblieben ist. Der Stichtag im Organigramm ist ein blankes input und benutzt dieselbe Regel ueber istMeldbaresDatum -- zwei Fassungen davon liefen hier erfahrungsgemaess auseinander. Der Abgleich mit dem Wert von aussen laeuft waehrend des Renderns, nicht in einem Effekt: der liefe erst nach dem Zeichnen, das Feld zeigte also fuer einen Bildaufbau den alten Stand -- und die Regel gegen setState im Effekt verbietet ihn aus genau diesem Grund. Die Felder in den Panels bleiben, wie sie sind: dort ist der Zustand lokal, es gibt keine Navigation und damit auch kein Zuruecksetzen.
This commit is contained in:
86
tests/components/DateField.test.tsx
Normal file
86
tests/components/DateField.test.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { DateField, istMeldbaresDatum } from "@/components/ui/Field";
|
||||
|
||||
// Der Anlass: in den Berichten hängt jedes Datumsfeld an der Adresszeile, eine
|
||||
// Änderung heisst also neu laden. Ein `<input type="date">` meldet beim Tippen
|
||||
// der Jahreszahl aber viermal — 0002, 0020, 0202, 2026 —, und die ersten drei
|
||||
// setzten das Feld mitten im Tippen zurück. Mit dem Kalender ging es, weil der
|
||||
// in einem Zug ein fertiges Datum setzt.
|
||||
|
||||
describe("istMeldbaresDatum", () => {
|
||||
it("nimmt ein vollständiges Datum an", () => {
|
||||
expect(istMeldbaresDatum("2026-09-30")).toBe(true);
|
||||
expect(istMeldbaresDatum("1980-01-01")).toBe(true);
|
||||
});
|
||||
|
||||
it("nimmt das leere Feld an, denn kein Datum ist auch eine Aussage", () => {
|
||||
expect(istMeldbaresDatum("")).toBe(true);
|
||||
});
|
||||
|
||||
it("weist die Zwischenstände beim Tippen ab", () => {
|
||||
for (const zwischen of ["0002-09-30", "0020-09-30", "0202-09-30"]) {
|
||||
expect(istMeldbaresDatum(zwischen), zwischen).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("weist Bruchstücke ab", () => {
|
||||
expect(istMeldbaresDatum("2026-09")).toBe(false);
|
||||
expect(istMeldbaresDatum("2026")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("DateField", () => {
|
||||
function feld(onChange: (v: string) => void, value = "") {
|
||||
render(<DateField label="Von" value={value} onChange={onChange} />);
|
||||
return screen.getByLabelText("Von") as HTMLInputElement;
|
||||
}
|
||||
|
||||
it("meldet den Zwischenstand nicht, zeigt ihn aber an", () => {
|
||||
const gemeldet = vi.fn();
|
||||
const input = feld(gemeldet);
|
||||
|
||||
fireEvent.change(input, { target: { value: "0002-09-30" } });
|
||||
expect(gemeldet).not.toHaveBeenCalled();
|
||||
// Der Tippstand bleibt stehen: würde das Feld ihn verwerfen, kämpfte es
|
||||
// gegen die Eingabe.
|
||||
expect(input.value).toBe("0002-09-30");
|
||||
});
|
||||
|
||||
it("meldet, sobald die Jahreszahl vollständig ist", () => {
|
||||
const gemeldet = vi.fn();
|
||||
const input = feld(gemeldet);
|
||||
|
||||
fireEvent.change(input, { target: { value: "0002-09-30" } });
|
||||
fireEvent.change(input, { target: { value: "2026-09-30" } });
|
||||
expect(gemeldet).toHaveBeenCalledExactlyOnceWith("2026-09-30");
|
||||
});
|
||||
|
||||
it("meldet das Leeren", () => {
|
||||
const gemeldet = vi.fn();
|
||||
const input = feld(gemeldet, "2026-09-30");
|
||||
fireEvent.change(input, { target: { value: "" } });
|
||||
expect(gemeldet).toHaveBeenCalledExactlyOnceWith("");
|
||||
});
|
||||
|
||||
it("reicht beim Verlassen nach, was liegengeblieben ist", () => {
|
||||
// Kommt in der Praxis kaum vor, ist aber der Rückfall: was nicht gemeldet
|
||||
// wurde, geht spätestens hier hinaus — sofern es eine Aussage ist.
|
||||
const gemeldet = vi.fn();
|
||||
const input = feld(gemeldet);
|
||||
fireEvent.change(input, { target: { value: "0002-09-30" } });
|
||||
fireEvent.blur(input);
|
||||
expect(gemeldet).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("übernimmt einen Wert, der von aussen kommt", () => {
|
||||
// „Zurücksetzen" in den Kriterien leert das Feld — der Tippstand darf
|
||||
// dann nicht stehenbleiben.
|
||||
const { rerender } = render(<DateField label="Von" value="2026-09-30" onChange={vi.fn()} />);
|
||||
const input = screen.getByLabelText("Von") as HTMLInputElement;
|
||||
expect(input.value).toBe("2026-09-30");
|
||||
|
||||
rerender(<DateField label="Von" value="" onChange={vi.fn()} />);
|
||||
expect(input.value).toBe("");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user