From 5f50cb97f314c3fb0cb50cd8e7a71a5cb8766b77 Mon Sep 17 00:00:00 2001 From: Maximilian Stubhan Date: Thu, 13 Aug 2026 17:57:10 +0200 Subject: [PATCH] Let the history say what an address was before MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HR reported it from testing: change someone's address and their history shows "Geänderte Felder: Adresse, Ort" — the new address is on the Stammdaten tab, the old one is nowhere. It was recorded, but only in the audit log, which is a different page sorted by time and actor rather than by person. So you had to already know what you were looking for to find out whether an address had ever changed, let alone what it used to be. The field-by-field diff was being built anyway and written to the audit log. employee_history now carries the same list, and the person's history renders it as an expandable Feld / Vorher / Nachher table — the same table the audit log uses, lifted into a shared component so the two views don't drift into reading differently. It expands with
, so the values are in the page: findable with Ctrl+F, present when printed, no script involved. The duplication with audit_log is deliberate. A person's history should be readable on its own, including after the log is eventually thinned by a retention rule. Rows written before today stay without values. They could only be reconstructed from the audit log, and the link is not reliable — no key, only a timestamp and a person. Honestly empty beats plausibly wrong. The migration was generated from the live function definition rather than retyped, and the diff is four lines: two column lists, two value lists. It carries a self-check that raises if either insert failed to pick up the new column, and it was rehearsed inside a rolled-back transaction against real data first — the probe confirmed the old street name lands in the history row. Co-Authored-By: Claude Opus 5 --- components/audit/AuditDetail.tsx | 34 +--- components/employees/tabs/HistorieTab.tsx | 31 +++ components/ui/AenderungsTabelle.tsx | 42 ++++ lib/supabase/types.ts | 3 + ...20260813120000_history_carries_changes.sql | 179 ++++++++++++++++++ tests/components/HistorieTab.test.tsx | 78 ++++++++ 6 files changed, 336 insertions(+), 31 deletions(-) create mode 100644 components/ui/AenderungsTabelle.tsx create mode 100644 supabase/migrations/20260813120000_history_carries_changes.sql create mode 100644 tests/components/HistorieTab.test.tsx diff --git a/components/audit/AuditDetail.tsx b/components/audit/AuditDetail.tsx index 815d628..9a66b54 100644 --- a/components/audit/AuditDetail.tsx +++ b/components/audit/AuditDetail.tsx @@ -2,6 +2,7 @@ import Link from "next/link"; import { useState } from "react"; +import { AenderungsTabelle } from "@/components/ui/AenderungsTabelle"; import { SlideOver } from "@/components/ui/SlideOver"; import { actionBadgeStyle } from "@/lib/colors"; import type { AuditChange } from "@/lib/supabase/types"; @@ -33,14 +34,6 @@ const zeitFormat = new Intl.DateTimeFormat("de-AT", { timeZone: "Europe/Vienna", }); -/** Leerer Wert heisst „war nicht gesetzt“ — und das ist eine Aussage. */ -function Wert({ text, art }: { text: string | null; art: "vorher" | "nachher" }) { - if (text === null || text === "") { - return leer; - } - return {text}; -} - export function AuditDetail({ eintrag }: { eintrag: AuditEintrag }) { const [offen, setOffen] = useState(false); const anzahl = eintrag.changes?.length ?? 0; @@ -101,29 +94,8 @@ export function AuditDetail({ eintrag }: { eintrag: AuditEintrag }) {

Geänderte Felder

{anzahl > 0 ? ( -
- - - - - - - - - - {eintrag.changes!.map((c, i) => ( - - - - - - ))} - -
FeldVorherNachher
{c.feld} - - - -
+
+
) : ( // Kein Aufzählungszeichen für „nichts da“: der Grund ist wichtig, diff --git a/components/employees/tabs/HistorieTab.tsx b/components/employees/tabs/HistorieTab.tsx index 0ec4707..cb3b241 100644 --- a/components/employees/tabs/HistorieTab.tsx +++ b/components/employees/tabs/HistorieTab.tsx @@ -1,9 +1,22 @@ +import { AenderungsTabelle } from "@/components/ui/AenderungsTabelle"; import { actionBadgeStyle } from "@/lib/colors"; import { fmtDate, todayIso } from "@/lib/format"; import type { Database } from "@/lib/supabase/types"; type HistoryRow = Database["public"]["Tables"]["employee_history"]["Row"]; +// Die Geschichte einer Person — und zwar aufklappbar bis auf die Werte. +// +// Vorher stand hier nur „Geänderte Felder: Adresse, Ort". Damit liess sich +// zwar sehen, *dass* jemand die Anschrift angefasst hat, aber nicht, was +// vorher dort stand. Die alte Adresse lag allein im Protokoll, und das ist +// eine andere Seite, nach Zeitpunkt sortiert statt nach Person — man hätte +// also erst wissen müssen, wonach man sucht. +// +// Aufgeklappt wird mit
, nicht mit einem Zustand im Browser: die +// Werte stehen dann schon in der Seite, sind durchsuchbar (Strg+F) und im +// Ausdruck sichtbar, und es braucht kein Skript dafür. + export function HistorieTab({ history }: { history: HistoryRow[] }) { const today = todayIso(); @@ -15,6 +28,7 @@ export function HistorieTab({ history }: { history: HistoryRow[] }) {
    {history.map((h) => { const isFuture = h.event_date > today; + const changes = h.changes ?? []; return (
  • @@ -27,6 +41,23 @@ export function HistorieTab({ history }: { history: HistoryRow[] }) { )}

    {h.description}

    + + {changes.length > 0 && ( +
    + + + {changes.length} {changes.length === 1 ? "Feld" : "Felder"} im Detail + +
    + +
    +
    + )}
  • ); })} diff --git a/components/ui/AenderungsTabelle.tsx b/components/ui/AenderungsTabelle.tsx new file mode 100644 index 0000000..3dd2a91 --- /dev/null +++ b/components/ui/AenderungsTabelle.tsx @@ -0,0 +1,42 @@ +import type { AuditChange } from "@/lib/supabase/types"; + +// Was sich geändert hat, feldweise — im Protokoll und in der Historie einer +// Person dieselbe Darstellung. Zwei Ansichten derselben Sache verschieden zu +// setzen, hiesse dass jemand sie zweimal lesen lernen muss. + +/** Leerer Wert heisst „war nicht gesetzt“ — und das ist eine Aussage. */ +function Wert({ text, art }: { text: string | null; art: "vorher" | "nachher" }) { + if (text === null || text === "") { + return leer; + } + return {text}; +} + +export function AenderungsTabelle({ changes }: { changes: AuditChange[] }) { + return ( +
    + + + + + + + + + + {changes.map((c, i) => ( + + + + + + ))} + +
    FeldVorherNachher
    {c.feld} + + + +
    +
    + ); +} diff --git a/lib/supabase/types.ts b/lib/supabase/types.ts index ddefbbf..e7e42a8 100644 --- a/lib/supabase/types.ts +++ b/lib/supabase/types.ts @@ -238,6 +238,8 @@ export type Database = { event_date: string; event_type: HistoryEventType; description: string; + /** Feldweise Änderungen — dieselbe Form wie audit_log.changes. */ + changes: AuditChange[] | null; created_at: string; }; Insert: { @@ -246,6 +248,7 @@ export type Database = { event_date: string; event_type: HistoryEventType; description: string; + changes?: AuditChange[] | null; created_at?: string; }; Update: Partial; diff --git a/supabase/migrations/20260813120000_history_carries_changes.sql b/supabase/migrations/20260813120000_history_carries_changes.sql new file mode 100644 index 0000000..b38368f --- /dev/null +++ b/supabase/migrations/20260813120000_history_carries_changes.sql @@ -0,0 +1,179 @@ +-- Die Historie trägt die Werte mit, nicht nur die Feldnamen. +-- +-- Beim Testen fiel auf: ändert jemand eine Adresse, steht in der Historie der +-- Person nur "Geänderte Felder: Adresse, Ort". Die alte Adresse ist nirgends +-- zu sehen — sie steckt allein im Audit-Log, und das ist eine andere Seite, +-- nach Zeitpunkt und handelnder Person sortiert statt nach Person. Wer wissen +-- will, ob eine Anschrift je geändert wurde und wie sie vorher lautete, kommt +-- also nicht hin, obwohl die Anwendung es weiß. +-- +-- Die Feldliste wird beim Ändern ohnehin gebaut (app_aenderung) und ins +-- Audit-Log geschrieben. Sie wandert jetzt zusätzlich in die Historienzeile. +-- Das ist bewusst redundant: die Historie ist die Geschichte *einer Person* +-- und soll für sich allein lesbar sein — auch dann noch, wenn das Protokoll +-- irgendwann nach Aufbewahrungsfrist ausgedünnt wird. +-- +-- Alte Zeilen bleiben ohne Werte. Nachliefern ließe sich das nur aus dem +-- Audit-Log, und die Zuordnung dorthin ist nicht eindeutig (kein Schlüssel, +-- nur Zeitpunkt und Person). Lieber ehrlich leer als falsch verknüpft. + +alter table employee_history add column if not exists changes jsonb; + +comment on column employee_history.changes is + 'Feldweise Änderungen als [{feld, vorher, nachher}] — dieselbe Form wie audit_log.changes. Null bei Ereignissen ohne Einzelfelder (Eintritt, Austritt, Import) und bei Zeilen von vor dieser Migration.'; + +CREATE OR REPLACE FUNCTION public.change_employee_data(payload jsonb) + RETURNS void + LANGUAGE plpgsql + SET search_path TO 'public', 'pg_temp' +AS $function$ +declare + v_employee_id uuid := (payload->>'employee_id')::uuid; + v_effective_date date := coalesce(nullif(payload->>'effective_date', '')::date, current_date); + v_old employees%rowtype; + v_name text; + v_person_changes jsonb := '[]'::jsonb; + v_contract_changes jsonb := '[]'::jsonb; + v_person jsonb := payload->'person'; + v_contract jsonb := payload->'contract'; + v_role jsonb := payload->'role'; + v_immediate boolean; + v_new_work_days text[]; + v_new_title_prefix text[]; + v_new_title_suffix text[]; +begin + perform require_hr_admin(); + select * into v_old from employees where id = v_employee_id; + v_name := v_old.first_name || ' ' || v_old.last_name; + v_immediate := v_effective_date <= current_date; + + -- Der `?`-Test bleibt: ein fehlender Schlüssel heisst „nicht übermittelt", + -- nicht „geleert". Ohne ihn würde jedes nicht gesendete Feld als Änderung + -- auf null gemeldet. + if v_person ? 'first_name' then v_person_changes := app_aenderung(v_person_changes, 'Vorname', v_old.first_name, v_person->>'first_name'); end if; + if v_person ? 'last_name' then v_person_changes := app_aenderung(v_person_changes, 'Nachname', v_old.last_name, v_person->>'last_name'); end if; + if v_person ? 'gender' then v_person_changes := app_aenderung(v_person_changes, 'Geschlecht', v_old.gender::text, v_person->>'gender'); end if; + -- Datumswerte über ::date::text vergleichen, damit „2026-8-3" und + -- „2026-08-03" nicht als Änderung gelten. + if v_person ? 'birth_date' then v_person_changes := app_aenderung(v_person_changes, 'Geburtsdatum', v_old.birth_date::text, (nullif(v_person->>'birth_date','')::date)::text); end if; + if v_person ? 'sv_nummer' then v_person_changes := app_aenderung(v_person_changes, 'SV-Nummer', v_old.sv_nummer, v_person->>'sv_nummer'); end if; + if v_person ? 'nationality' then v_person_changes := app_aenderung(v_person_changes, 'Staatsbürgerschaft', v_old.nationality, v_person->>'nationality'); end if; + if v_person ? 'address' then v_person_changes := app_aenderung(v_person_changes, 'Adresse', v_old.address, v_person->>'address'); end if; + if v_person ? 'postal_code' then v_person_changes := app_aenderung(v_person_changes, 'Postleitzahl', v_old.postal_code, v_person->>'postal_code'); end if; + if v_person ? 'city' then v_person_changes := app_aenderung(v_person_changes, 'Ort', v_old.city, v_person->>'city'); end if; + if v_person ? 'address_country' then v_person_changes := app_aenderung(v_person_changes, 'Land', v_old.address_country, v_person->>'address_country'); end if; + if v_person ? 'email' then v_person_changes := app_aenderung(v_person_changes, 'E-Mail', v_old.email, v_person->>'email'); end if; + if v_person ? 'phone' then v_person_changes := app_aenderung(v_person_changes, 'Telefon', v_old.phone, v_person->>'phone'); end if; + if v_person ? 'emergency_contact_name' then v_person_changes := app_aenderung(v_person_changes, 'Notfallkontakt', v_old.emergency_contact_name, v_person->>'emergency_contact_name'); end if; + if v_person ? 'emergency_contact_phone' then v_person_changes := app_aenderung(v_person_changes, 'Notfallkontakt Telefon', v_old.emergency_contact_phone, v_person->>'emergency_contact_phone'); end if; + if v_person ? 'emergency_contact_relation' then v_person_changes := app_aenderung(v_person_changes, 'Notfallkontakt Verhältnis', v_old.emergency_contact_relation, v_person->>'emergency_contact_relation'); end if; + + if v_person ? 'title_prefix' then + v_new_title_prefix := coalesce((select array_agg(elem) from jsonb_array_elements_text(v_person->'title_prefix') elem), '{}'); + v_person_changes := app_aenderung(v_person_changes, 'Titel (vorangestellt)', + array_to_string(v_old.title_prefix, ', '), array_to_string(v_new_title_prefix, ', ')); + end if; + if v_person ? 'title_suffix' then + v_new_title_suffix := coalesce((select array_agg(elem) from jsonb_array_elements_text(v_person->'title_suffix') elem), '{}'); + v_person_changes := app_aenderung(v_person_changes, 'Titel (nachgestellt)', + array_to_string(v_old.title_suffix, ', '), array_to_string(v_new_title_suffix, ', ')); + end if; + + if v_contract ? 'employment_type' then v_contract_changes := app_aenderung(v_contract_changes, 'Beschäftigungsausmaß', v_old.employment_type::text, v_contract->>'employment_type'); end if; + -- Über ::numeric::text, damit „38.50" und „38.5" gleich zählen. + if v_contract ? 'weekly_hours' then v_contract_changes := app_aenderung(v_contract_changes, 'Wochenstunden', v_old.weekly_hours::text, (nullif(v_contract->>'weekly_hours','')::numeric)::text); end if; + if v_contract ? 'contract_type' then v_contract_changes := app_aenderung(v_contract_changes, 'Vertragsart', v_old.contract_type::text, v_contract->>'contract_type'); end if; + if v_contract ? 'contract_end_date' then v_contract_changes := app_aenderung(v_contract_changes, 'Befristet bis', v_old.contract_end_date::text, (nullif(v_contract->>'contract_end_date','')::date)::text); end if; + + if v_role ? 'worker_type' then v_contract_changes := app_aenderung(v_contract_changes, 'Angestellte:r/Arbeiter:in', v_old.worker_type::text, v_role->>'worker_type'); end if; + if v_role ? 'collective_agreement' then v_contract_changes := app_aenderung(v_contract_changes, 'Kollektivvertrag', v_old.collective_agreement::text, v_role->>'collective_agreement'); end if; + if v_role ? 'work_days' then + v_new_work_days := coalesce((select array_agg(elem) from jsonb_array_elements_text(v_role->'work_days') elem), '{}'); + v_contract_changes := app_aenderung(v_contract_changes, 'Arbeitstage', + array_to_string(v_old.work_days, ', '), array_to_string(v_new_work_days, ', ')); + end if; + if v_role ? 'is_betriebsrat' then v_contract_changes := app_aenderung(v_contract_changes, 'Betriebsrat', v_old.is_betriebsrat::text, v_role->>'is_betriebsrat'); end if; + if v_role ? 'has_dienstwagen' then v_contract_changes := app_aenderung(v_contract_changes, 'Dienstwagen', v_old.has_dienstwagen::text, v_role->>'has_dienstwagen'); end if; + if v_role ? 'is_laterale_fuehrung' then v_contract_changes := app_aenderung(v_contract_changes, 'Laterale Führung', v_old.is_laterale_fuehrung::text, v_role->>'is_laterale_fuehrung'); end if; + if v_role ? 'is_c_level' then v_contract_changes := app_aenderung(v_contract_changes, 'C-Level', v_old.is_c_level::text, v_role->>'is_c_level'); end if; + if v_role ? 'dienstwagen_art' then v_contract_changes := app_aenderung(v_contract_changes, 'Dienstwagen Antrieb', v_old.dienstwagen_art, nullif(v_role->>'dienstwagen_art', '')); end if; + + if v_immediate then + update employees set + first_name = coalesce(v_person->>'first_name', first_name), + last_name = coalesce(v_person->>'last_name', last_name), + gender = coalesce((v_person->>'gender')::gender_type, gender), + birth_date = coalesce((v_person->>'birth_date')::date, birth_date), + sv_nummer = coalesce(v_person->>'sv_nummer', sv_nummer), + nationality = coalesce(v_person->>'nationality', nationality), + address = coalesce(v_person->>'address', address), + postal_code = coalesce(v_person->>'postal_code', postal_code), + city = coalesce(v_person->>'city', city), + address_country = coalesce(v_person->>'address_country', address_country), + email = coalesce(v_person->>'email', email), + phone = coalesce(v_person->>'phone', phone), + emergency_contact_name = case when v_person ? 'emergency_contact_name' then nullif(v_person->>'emergency_contact_name', '') else emergency_contact_name end, + emergency_contact_phone = case when v_person ? 'emergency_contact_phone' then nullif(v_person->>'emergency_contact_phone', '') else emergency_contact_phone end, + emergency_contact_relation = case when v_person ? 'emergency_contact_relation' then nullif(v_person->>'emergency_contact_relation', '') else emergency_contact_relation end, + title_prefix = case when v_person ? 'title_prefix' then v_new_title_prefix else title_prefix end, + title_suffix = case when v_person ? 'title_suffix' then v_new_title_suffix else title_suffix end, + employment_type = coalesce((v_contract->>'employment_type')::employment_type, employment_type), + weekly_hours = coalesce((v_contract->>'weekly_hours')::numeric, weekly_hours), + contract_type = coalesce((v_contract->>'contract_type')::contract_type, contract_type), + contract_end_date = case when v_contract ? 'contract_end_date' then nullif(v_contract->>'contract_end_date','')::date else contract_end_date end, + worker_type = coalesce((v_role->>'worker_type')::worker_type, worker_type), + collective_agreement = coalesce((v_role->>'collective_agreement')::collective_agreement, collective_agreement), + work_days = case when v_role ? 'work_days' then v_new_work_days else work_days end, + is_betriebsrat = coalesce((v_role->>'is_betriebsrat')::boolean, is_betriebsrat), + has_dienstwagen = coalesce((v_role->>'has_dienstwagen')::boolean, has_dienstwagen), + is_laterale_fuehrung = coalesce((v_role->>'is_laterale_fuehrung')::boolean, is_laterale_fuehrung), + is_c_level = coalesce((v_role->>'is_c_level')::boolean, is_c_level), + dienstwagen_art = case + when coalesce((v_role->>'has_dienstwagen')::boolean, has_dienstwagen) then + coalesce(nullif(v_role->>'dienstwagen_art', ''), dienstwagen_art, 'Verbrenner') + else null + end + where id = v_employee_id; + elsif jsonb_array_length(v_person_changes) > 0 or jsonb_array_length(v_contract_changes) > 0 then + insert into pending_org_changes (employee_id, change_type, effective_date, payload) + values (v_employee_id, 'contract_change', v_effective_date, payload); + end if; + + if jsonb_array_length(v_person_changes) > 0 then + insert into employee_history (employee_id, event_date, event_type, description, changes) + values (v_employee_id, v_effective_date, 'Stammdatenänderung', + 'Geänderte Felder: ' || app_aenderungsfelder(v_person_changes) || ', wirksam ab ' || v_effective_date, v_person_changes); + insert into audit_log (actor_user_id, actor_name, action, target_label, target_employee_id, details, changes) + values (app_current_user_id(), current_actor_name(), 'Stammdatenänderung', v_name, v_employee_id, + app_aenderungsfelder(v_person_changes) || ', wirksam ab ' || v_effective_date, v_person_changes); + end if; + + if jsonb_array_length(v_contract_changes) > 0 then + insert into employee_history (employee_id, event_date, event_type, description, changes) + values (v_employee_id, v_effective_date, 'Vertragsänderung', + 'Geänderte Felder: ' || app_aenderungsfelder(v_contract_changes) || ', wirksam ab ' || v_effective_date, v_contract_changes); + insert into audit_log (actor_user_id, actor_name, action, target_label, target_employee_id, details, changes) + values (app_current_user_id(), current_actor_name(), 'Vertragsänderung', v_name, v_employee_id, + app_aenderungsfelder(v_contract_changes) || ', wirksam ab ' || v_effective_date, v_contract_changes); + end if; +end; +$function$; + + +-- Selbstprüfung: schlägt die Migration fehl, statt still nichts zu tun. +do $$ +declare + v_def text := pg_get_functiondef('public.change_employee_data(jsonb)'::regprocedure); +begin + if not exists ( + select 1 from information_schema.columns + where table_schema = 'public' and table_name = 'employee_history' and column_name = 'changes' + ) then + raise exception 'employee_history.changes fehlt'; + end if; + + if (length(v_def) - length(replace(v_def, 'description, changes)', ''))) / length('description, changes)') <> 2 then + raise exception 'change_employee_data schreibt changes nicht in beide Historien-Einträge'; + end if; +end +$$; diff --git a/tests/components/HistorieTab.test.tsx b/tests/components/HistorieTab.test.tsx new file mode 100644 index 0000000..ba6ca38 --- /dev/null +++ b/tests/components/HistorieTab.test.tsx @@ -0,0 +1,78 @@ +import { render, screen, within } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { HistorieTab } from "@/components/employees/tabs/HistorieTab"; +import type { Database } from "@/lib/supabase/types"; + +type HistoryRow = Database["public"]["Tables"]["employee_history"]["Row"]; + +// Der Anlass: eine Adressänderung war in der Historie der Person nur als +// „Geänderte Felder: Adresse" zu sehen. Die alte Anschrift stand allein im +// Protokoll — einer nach Zeitpunkt sortierten Seite, auf der man erst wissen +// muss, wonach man sucht. Damit war nicht nachvollziehbar, ob und wie sich +// eine Adresse je geändert hat. + +function eintrag(teil: Partial): HistoryRow { + return { + id: "h1", + employee_id: "e1", + event_date: "2026-03-02", + event_type: "Stammdatenänderung", + description: "Geänderte Felder: Adresse, Ort, wirksam ab 2026-03-02", + changes: null, + created_at: "2026-03-02T08:00:00.000Z", + ...teil, + }; +} + +const ADRESSWECHSEL = eintrag({ + changes: [ + { feld: "Adresse", vorher: "Feldweg 66", nachher: "Bahnhofstrasse 5" }, + { feld: "Ort", vorher: "Brno", nachher: "Linz" }, + ], +}); + +describe("HistorieTab", () => { + it("zeigt die alte Anschrift, nicht nur die neue", () => { + render(); + expect(screen.getByText("Feldweg 66")).toBeInTheDocument(); + expect(screen.getByText("Bahnhofstrasse 5")).toBeInTheDocument(); + }); + + it("stellt Vorher und Nachher je Feld gegenüber", () => { + render(); + const zeile = screen.getByText("Adresse").closest("tr")!; + const zellen = within(zeile).getAllByRole("cell"); + expect(zellen[1]).toHaveTextContent("Feldweg 66"); + expect(zellen[2]).toHaveTextContent("Bahnhofstrasse 5"); + }); + + it("nennt die Zahl der Felder am Aufklapper", () => { + render(); + expect(screen.getByText(/2 Felder im Detail/)).toBeInTheDocument(); + }); + + it("benennt ein einzelnes Feld im Singular", () => { + render(); + expect(screen.getByText(/1 Feld im Detail/)).toBeInTheDocument(); + }); + + it("macht aus einem leeren Vorher-Wert eine Aussage statt einer Lücke", () => { + render(); + const zeile = screen.getByText("Telefon").closest("tr")!; + expect(within(zeile).getAllByRole("cell")[1]).toHaveTextContent("leer"); + }); + + it("hängt keinen Aufklapper an Ereignisse ohne Feldwerte", () => { + // Eintritt, Austritt und die Zeilen von vor dieser Erweiterung haben + // keine Werte. Ein leerer Aufklapper würde einen Fehler vermuten lassen. + render(); + expect(screen.getByText("Eintritt als Elektrotechniker:in")).toBeInTheDocument(); + expect(screen.queryByText(/im Detail/)).not.toBeInTheDocument(); + expect(document.querySelector("details")).toBeNull(); + }); + + it("bleibt bei leerer Historie bei einem Satz", () => { + render(); + expect(screen.getByText("Keine Historieneinträge vorhanden.")).toBeInTheDocument(); + }); +});