import { render, screen, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { DraftsCard } from "@/components/dashboard/DraftsCard"; import { ToastProvider } from "@/components/ui/Toast"; import type { Entwurf } from "@/lib/entwuerfe"; const openWizard = vi.fn(); const deleteHireDraft = vi.fn<(id: string) => Promise<{ success: boolean; error?: string }>>(async () => ({ success: true, })); const refresh = vi.fn(); vi.mock("@/components/hire/HireWizardContext", () => ({ useHireWizard: () => ({ openWizard }) })); vi.mock("@/actions/hireDrafts", () => ({ deleteHireDraft: (id: string) => deleteHireDraft(id) })); vi.mock("next/navigation", () => ({ useRouter: () => ({ refresh, push: vi.fn() }) })); // Seit die Auswahl in der Glocke auch die Entwürfe steuert, stehen hier // fremde Zeilen. Sie dürfen aussehen wie das, was sie sind: zum Ansehen. // Böte die Karte an ihnen „Fortsetzen", liefe die Person durch den ganzen // Assistenten und bekäme am Ende eine Fehlermeldung aus der Datenbank. function entwurf(teil: Partial = {}): Entwurf { return { id: "d1", step: 2, payload: { firstName: "Manuel", lastName: "Aigner" }, updated_at: "2026-09-09T08:00:00.000Z", vonMir: true, autor: "Max Stubhan", gesperrtVon: null, ...teil, }; } const fremd = entwurf({ id: "d2", payload: { firstName: "Eva", lastName: "Novak" }, vonMir: false, autor: "Anna Berger" }); function zeige(drafts: Entwurf[]) { return render( ); } /** Die Zeile zu einem Namen — die Karte zeigt mehrere nebeneinander. */ function zeile(name: string) { return screen.getByText(name).closest("li") as HTMLElement; } beforeEach(() => { openWizard.mockClear(); deleteHireDraft.mockClear(); deleteHireDraft.mockResolvedValue({ success: true }); refresh.mockClear(); }); describe("DraftsCard", () => { it("zeigt zu eigenen Entwürfen Fortsetzen und Löschen", () => { zeige([entwurf()]); const z = zeile("Manuel Aigner"); expect(within(z).getByRole("button", { name: "Fortsetzen" })).toBeInTheDocument(); expect(within(z).getByRole("button", { name: "Entwurf löschen" })).toBeInTheDocument(); }); it("sagt an eigenen Entwürfen: von mir", () => { // Der eigene Name stünde an jeder Zeile und sagte nichts — man kennt ihn. zeige([entwurf()]); const z = zeile("Manuel Aigner"); expect(within(z).getByText("von mir")).toBeInTheDocument(); expect(within(z).queryByText(/Max Stubhan/)).not.toBeInTheDocument(); }); it("nennt an fremden Entwürfen, von wem sie stammen", () => { zeige([fremd]); expect(within(zeile("Eva Novak")).getByText("von Anna Berger")).toBeInTheDocument(); }); it("lässt auch fremde Entwürfe fortsetzen", () => { // Der Sinn der ganzen Freigabe: wer vertreten wird, soll die angefangene // Einstellung zu Ende bringen können. zeige([fremd]); expect(within(zeile("Eva Novak")).getByRole("button", { name: "Fortsetzen" })).toBeInTheDocument(); }); it("bietet an fremden Entwürfen kein Löschen", () => { // Die Regel liesse es zu — sie muss, weil der Assistent den Entwurf nach // dem Anlegen selbst wegräumt. Eine Schaltfläche, die fremde halbfertige // Arbeit wegwirft, gehört trotzdem nicht neben eine, die sie fortsetzt. zeige([fremd]); expect(within(zeile("Eva Novak")).queryByRole("button", { name: "Entwurf löschen" })).not.toBeInTheDocument(); }); it("nimmt Fortsetzen weg, solange jemand anderes drin ist", () => { zeige([entwurf({ gesperrtVon: "Bernd Huber" })]); expect(within(zeile("Manuel Aigner")).queryByRole("button", { name: "Fortsetzen" })).not.toBeInTheDocument(); }); it("sagt, wer den gesperrten Entwurf gerade bearbeitet", () => { // Eine graue Schaltfläche ohne Erklärung liest sich wie ein Fehler, und // die nächste Handlung wäre, es gleich noch einmal zu versuchen. zeige([entwurf({ gesperrtVon: "Bernd Huber" })]); expect(within(zeile("Manuel Aigner")).getByText(/Bernd Huber bearbeitet gerade/)).toBeInTheDocument(); }); it("sperrt auch das Löschen des eigenen Entwurfs, solange jemand drin ist", () => { // Sonst risse man jemandem den Entwurf unter den Händen weg. zeige([entwurf({ gesperrtVon: "Bernd Huber" })]); expect(within(zeile("Manuel Aigner")).getByRole("button", { name: "Entwurf löschen" })).toBeDisabled(); }); it("trennt die Zeilen: freie bedienbar, gesperrte nicht", () => { // Beide nebeneinander — der Fall, in dem eine Verwechslung teuer wird. zeige([entwurf(), entwurf({ id: "d9", payload: { firstName: "Eva", lastName: "Novak" }, gesperrtVon: "Bernd Huber" })]); expect(within(zeile("Manuel Aigner")).getByRole("button", { name: "Fortsetzen" })).toBeInTheDocument(); expect(within(zeile("Eva Novak")).queryByRole("button", { name: "Fortsetzen" })).not.toBeInTheDocument(); }); it("setzt beim Klick genau den angeklickten Entwurf fort", () => { const user = userEvent.setup(); zeige([entwurf(), fremd]); return user.click(within(zeile("Manuel Aigner")).getByRole("button", { name: "Fortsetzen" })).then(() => { expect(openWizard).toHaveBeenCalledWith({ draftId: "d1" }); }); }); it("löscht genau den angeklickten Entwurf", async () => { // Zwei eigene nebeneinander: eine Karte, die immer den ersten löscht, // fiele mit nur einer Zeile nicht auf. const user = userEvent.setup(); zeige([entwurf(), entwurf({ id: "d3", payload: { firstName: "Lena", lastName: "Wolf" } })]); await user.click(within(zeile("Lena Wolf")).getByRole("button", { name: "Entwurf löschen" })); expect(deleteHireDraft).toHaveBeenCalledWith("d3"); }); it("meldet den Fehler, wenn das Löschen abgewiesen wird", async () => { deleteHireDraft.mockResolvedValue({ success: false, error: "Gehört jemand anderem." }); const user = userEvent.setup(); zeige([entwurf()]); await user.click(screen.getByRole("button", { name: "Entwurf löschen" })); expect(await screen.findByText("Gehört jemand anderem.")).toBeInTheDocument(); }); it("bleibt bei leerer Liste unsichtbar", () => { // Eine Überschrift über nichts ist eine Zeile, die jeden Tag gelesen und // jeden Tag verworfen wird. // Nicht auf einen leeren Container prüfen: der ToastProvider legt // seinen eigenen Kasten an, und der stünde da, egal was die Karte tut. zeige([]); expect(screen.queryByRole("heading", { name: /Entwürfe/ })).not.toBeInTheDocument(); expect(screen.queryByRole("list")).not.toBeInTheDocument(); }); });