Let the truncated Anstehend list open
Some checks failed
CI / Lint, Typen, Tests, Build (push) Failing after 5m35s
CI / Migrationen auf leerer Datenbank (push) Successful in 10m14s

"… und 5 weitere Ereignisse in diesem Zeitraum" was a sentence to read.
It is now a button: click it and the rest unfolds in place, click again
and the card goes back to eight rows.

Until now the only way to see the remaining entries was to narrow the
period or the kinds — which changes the question rather than the answer.

This happens in the browser, not through the URL, unlike the period and
kind filters. Those go through the address because a longer period brings
rows into play that were never loaded; here every entry in the period is
already on the page and the eight was purely presentational. A round trip
would mean waiting for data that is already there, plus a history entry
for something nobody wants to go back to.

The list moved into its own component so the state has somewhere to live.
KIND_LABEL and the item type moved with it, since they only describe this
list.

The button only appears when there is something to unfold, and once open
it offers the way back — otherwise the card stays long for the rest of the
session because somebody looked once.

Verified: 10 tests, five mutation-checked (ignoring the state, dropping
the way back, showing the button with nothing to unfold, losing the
singular, dropping aria-expanded each turn them red). 487 tests,
typecheck and build clean. Not seen in a browser — login goes through the
company account and the database is unreachable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-09 20:20:19 +02:00
parent 99b1df9735
commit eb25369d1d
3 changed files with 219 additions and 68 deletions

View File

@@ -0,0 +1,101 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it } from "vitest";
import { AnstehendListe, VORSCHAU, type AnstehendEintrag } from "@/components/dashboard/AnstehendListe";
// Die Karte zeigt acht Zeilen und darunter, wie viele es insgesamt sind.
// Diese Zeile war lange ein Satz zum Lesen; jetzt ist sie ein Knopf. Wer
// alle sehen will, soll nicht den Zeitraum weiter stellen müssen — das
// änderte die Frage, nicht nur die Antwort.
const HEUTE = "2026-09-09";
function eintrag(nr: number, teil: Partial<AnstehendEintrag> = {}): AnstehendEintrag {
return {
id: `i${nr}`,
employeeId: `e${nr}`,
label: `Person ${nr}`,
date: `2026-10-${String(nr).padStart(2, "0")}`,
kind: "hire",
...teil,
};
}
const viele = Array.from({ length: 13 }, (_, i) => eintrag(i + 1));
function zeige(eintraege: AnstehendEintrag[]) {
return render(<AnstehendListe eintraege={eintraege} today={HEUTE} leerText="Nichts steht an." />);
}
describe("AnstehendListe", () => {
it("zeigt zunächst nur die Vorschau", () => {
zeige(viele);
expect(screen.getAllByRole("listitem")).toHaveLength(VORSCHAU);
expect(screen.getByText("Person 8")).toBeInTheDocument();
expect(screen.queryByText("Person 9")).not.toBeInTheDocument();
});
it("nennt im Knopf, wie viele noch kommen", () => {
zeige(viele);
expect(screen.getByRole("button", { name: /und 5 weitere Ereignisse anzeigen/i })).toBeInTheDocument();
});
it("klappt auf Klick alle auf", async () => {
const user = userEvent.setup();
zeige(viele);
await user.click(screen.getByRole("button", { name: /anzeigen/i }));
expect(screen.getAllByRole("listitem")).toHaveLength(13);
expect(screen.getByText("Person 13")).toBeInTheDocument();
});
it("führt wieder zurück", async () => {
// Ohne den Weg zurück bliebe die Karte für den Rest der Sitzung lang,
// auch wenn jemand nur einmal nachsehen wollte.
const user = userEvent.setup();
zeige(viele);
await user.click(screen.getByRole("button", { name: /anzeigen/i }));
await user.click(screen.getByRole("button", { name: /Weniger anzeigen/i }));
expect(screen.getAllByRole("listitem")).toHaveLength(VORSCHAU);
});
it("meldet den Zustand an Hilfsmittel", async () => {
const user = userEvent.setup();
zeige(viele);
const knopf = screen.getByRole("button", { name: /anzeigen/i });
expect(knopf).toHaveAttribute("aria-expanded", "false");
await user.click(knopf);
expect(screen.getByRole("button", { name: /Weniger/i })).toHaveAttribute("aria-expanded", "true");
});
it("zeigt keinen Knopf, wenn ohnehin alles zu sehen ist", () => {
// Ein Knopf, der nichts aufklappt, ist ein Versprechen ohne Inhalt.
zeige(viele.slice(0, VORSCHAU));
expect(screen.queryByRole("button")).not.toBeInTheDocument();
});
it("sagt bei einem einzelnen Ereignis Einzahl", () => {
zeige(viele.slice(0, VORSCHAU + 1));
expect(screen.getByRole("button", { name: /und 1 weiteres Ereignis anzeigen/i })).toBeInTheDocument();
});
it("zeigt den übergebenen Text, wenn nichts ansteht", () => {
zeige([]);
expect(screen.getByText("Nichts steht an.")).toBeInTheDocument();
expect(screen.queryByRole("button")).not.toBeInTheDocument();
});
it("kennzeichnet überfällige Wiedervorlagen", () => {
// Nur Wiedervorlagen können überfällig sein: die anderen Arten haben
// eine untere Grenze, eine offene Aufgabe nicht.
zeige([eintrag(1, { kind: "note", date: "2026-08-01", hinweis: "Dienstzettel" })]);
expect(screen.getByText(/überfällig/)).toBeInTheDocument();
});
it("verlinkt auf die Akte, nicht auf die Notiz", () => {
zeige([eintrag(1, { kind: "note", employeeId: "e42" })]);
expect(screen.getByRole("link")).toHaveAttribute("href", "/employees/e42");
});
});