Files
alpenwerk-hr/tests/components/NotesBell.test.tsx
Maximilian Stubhan e8e675fd07
All checks were successful
CI / Lint, Typen, Tests, Build (push) Successful in 11m36s
CI / Migrationen auf leerer Datenbank (push) Successful in 10m10s
Turn the note filter around: yours by default, colleagues added
Yesterday's version had it the other way — everyone visible, untick to
hide. The decision from the business side is the opposite: you see your
own notes, and you tick the colleagues you also want. So note_mutes
becomes note_subscriptions and the predicate flips from `not exists` to
`exists`.

The existing rows are not carried over. The meaning inverts rather than
the sign: converting faithfully ("everyone except the muted") would write
almost the whole roster into the new table and reproduce exactly the state
the change is meant to end. Anyone opening the setting tomorrow would
think it had not taken effect. The table is a day old; what is lost is a
few ticks from trying it out.

What this costs is worth saying plainly: the silent case that could not
happen under exceptions can happen now. Do not tick a colleague and you
will not see her follow-ups — not while she is on holiday either. That is
the flip side of the decision, and it is written down in the migration
rather than discovered later.

Each note now says who wrote it. Own notes read "von mir" rather than
repeating your own name, which would sit on every second line and tell
nobody anything. The flag is computed on the server: the user id is
already there, and threading it through four components for one word is a
poor trade. The counter on the button follows the same turn — "+2" for
what you added, nothing when you added nothing.

Verified: 21 tests, five mutation-checked (restoring `not exists`,
dropping the own-notes clause, inverting the default, hiding the author,
and printing your own name instead of "von mir" each turn them red). 489
tests, typecheck, lint, schema drift and build clean. The migration is
reviewed but not run — no reachable database here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 11:36:38 +02:00

170 lines
7.0 KiB
TypeScript

import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { NotesBell, type Kollege } from "@/components/shell/NotesBell";
import { ToastProvider } from "@/components/ui/Toast";
import type { OpenNote } from "@/lib/notes";
const setNotizSichtbarkeit = vi.fn(async () => ({ success: true }) as { success: boolean; error?: string });
const refresh = vi.fn();
vi.mock("@/actions/notes", () => ({ setNotizSichtbarkeit: (...a: unknown[]) => setNotizSichtbarkeit(...(a as [])) }));
vi.mock("@/actions/employees", () => ({ completeEmployeeNote: vi.fn(async () => ({ success: true })) }));
vi.mock("next/navigation", () => ({ useRouter: () => ({ refresh, push: vi.fn() }) }));
// Die Einstellung steckt in der Glocke, weil sie dort gebraucht wird: wer
// eine fremde Wiedervorlage sieht, die ihn nichts angeht, will sie an Ort
// und Stelle abwählen können — nicht auf einer Einstellungsseite.
const KOLLEGEN: Kollege[] = [
{ id: "a", name: "Anna Berger", sichtbar: true },
{ id: "b", name: "Bernd Huber", sichtbar: false },
];
// Die Vorgabe ist die eigene Person: ohne Haken sieht man nur sich selbst.
function notiz(teil: Partial<OpenNote> = {}): OpenNote {
return {
id: "n1",
employee_id: "e1",
author_user_id: "a",
author_name: "Anna Berger",
category: "Wiedervorlage",
note_text: "Dienstzettel nachfassen",
due_date: "2026-09-20",
done: false,
done_by: null,
done_at: null,
created_at: "2026-09-01T08:00:00.000Z",
employeeName: "Manuel Aigner",
vonMir: false,
...teil,
} as OpenNote;
}
function zeige(kollegen = KOLLEGEN, notes = [notiz()]) {
return render(
<ToastProvider>
<NotesBell notes={notes} kollegen={kollegen} />
</ToastProvider>
);
}
async function oeffneEinstellung(user: ReturnType<typeof userEvent.setup>) {
await user.click(screen.getByRole("button", { name: /Meine Notizen/i }));
await user.click(screen.getByRole("button", { name: /Kolleg:innen/i }));
}
beforeEach(() => {
setNotizSichtbarkeit.mockClear();
setNotizSichtbarkeit.mockResolvedValue({ success: true });
refresh.mockClear();
});
describe("Sichtbarkeit der Kolleg:innen", () => {
it("zeigt die Einstellung erst auf Klick", async () => {
const user = userEvent.setup();
zeige();
await user.click(screen.getByRole("button", { name: /Meine Notizen/i }));
expect(screen.queryByRole("checkbox", { name: "Anna Berger" })).not.toBeInTheDocument();
await user.click(screen.getByRole("button", { name: /Kolleg:innen/i }));
expect(screen.getByRole("checkbox", { name: "Anna Berger" })).toBeInTheDocument();
});
it("spiegelt den gespeicherten Stand in den Haken", async () => {
const user = userEvent.setup();
zeige();
await oeffneEinstellung(user);
expect(screen.getByRole("checkbox", { name: "Anna Berger" })).toBeChecked();
expect(screen.getByRole("checkbox", { name: "Bernd Huber" })).not.toBeChecked();
});
it("meldet beim Abwählen die Kennung und den neuen Stand", async () => {
// sichtbar: false heisst jetzt „Abo entfernen", nicht „ausblenden".
const user = userEvent.setup();
zeige();
await oeffneEinstellung(user);
await user.click(screen.getByRole("checkbox", { name: "Anna Berger" }));
expect(setNotizSichtbarkeit).toHaveBeenCalledWith({ kollegeId: "a", sichtbar: false });
expect(screen.getByRole("checkbox", { name: "Anna Berger" })).not.toBeChecked();
});
it("wählt beim zweiten Klick wieder an", async () => {
const user = userEvent.setup();
zeige();
await oeffneEinstellung(user);
await user.click(screen.getByRole("checkbox", { name: "Bernd Huber" }));
expect(setNotizSichtbarkeit).toHaveBeenCalledWith({ kollegeId: "b", sichtbar: true });
expect(screen.getByRole("checkbox", { name: "Bernd Huber" })).toBeChecked();
});
it("dreht den Haken zurück, wenn das Speichern scheitert", async () => {
// Der Haken springt sofort um, damit sich das Durchgehen der Liste nicht
// zäh anfühlt. Bleibt er stehen, obwohl nichts gespeichert wurde, zeigt
// die Einstellung etwas anderes als die Datenbank.
setNotizSichtbarkeit.mockResolvedValue({ success: false, error: "Nicht gespeichert." });
const user = userEvent.setup();
zeige();
await oeffneEinstellung(user);
await user.click(screen.getByRole("checkbox", { name: "Anna Berger" }));
expect(screen.getByRole("checkbox", { name: "Anna Berger" })).toBeChecked();
expect(await screen.findByText("Nicht gespeichert.")).toBeInTheDocument();
});
it("nennt die Zahl der Hinzugewählten nur, wenn welche da sind", async () => {
const user = userEvent.setup();
const { unmount } = zeige();
await user.click(screen.getByRole("button", { name: /Meine Notizen/i }));
expect(screen.getByRole("button", { name: /Kolleg:innen/i })).toHaveTextContent("+1");
unmount();
// Niemand hinzugewählt: die Vorgabe. Eine „+0" wäre bei jedem
// Aufschlagen zu lesen und sagte nichts.
zeige(KOLLEGEN.map((k) => ({ ...k, sichtbar: false })));
await user.click(screen.getByRole("button", { name: /Meine Notizen/i }));
expect(screen.getByRole("button", { name: /Kolleg:innen/i })).not.toHaveTextContent("+");
});
it("nennt an jeder Notiz den Verfasser", async () => {
// Sichtbar sind die eigenen und die hinzugewählten — ohne diese Zeile
// wäre nicht zu unterscheiden, welche davon man gerade liest.
const user = userEvent.setup();
zeige(KOLLEGEN, [notiz({ author_name: "Anna Berger" })]);
await user.click(screen.getByRole("button", { name: /Meine Notizen/i }));
expect(screen.getByText("von Anna Berger")).toBeInTheDocument();
});
it("sagt bei eigenen Notizen: von mir", async () => {
// Der eigene Name stünde an jeder zweiten Zeile und sagte nichts — man
// kennt ihn.
const user = userEvent.setup();
zeige(KOLLEGEN, [notiz({ vonMir: true, author_name: "Max Stubhan" })]);
await user.click(screen.getByRole("button", { name: /Meine Notizen/i }));
expect(screen.getByText("von mir")).toBeInTheDocument();
expect(screen.queryByText(/Max Stubhan/)).not.toBeInTheDocument();
});
it("kommt ohne Kolleg:innen zurecht", async () => {
// Eine Personalabteilung aus einer Person: die Liste ist leer, und ein
// Kasten ohne Inhalt wäre eine Sackgasse.
const user = userEvent.setup();
zeige([]);
await oeffneEinstellung(user);
expect(screen.getByText(/Keine weiteren HR-Kolleg:innen/i)).toBeInTheDocument();
});
it("lässt die Notizen selbst stehen, während die Einstellung offen ist", async () => {
// Die Einstellung schiebt sich über die Liste, sie ersetzt sie nicht:
// wer abwählt, will sehen, was daraufhin verschwindet.
const user = userEvent.setup();
zeige();
await oeffneEinstellung(user);
expect(screen.getByText("Dienstzettel nachfassen")).toBeInTheDocument();
expect(screen.getByRole("checkbox", { name: "Anna Berger" })).toBeInTheDocument();
});
});