Files
nexarch/web/workflow-tasks/app/TaskBoard.test.tsx
patrickandClaude Sonnet 5 56ab1a2d6d
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (pull_request) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (pull_request) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (pull_request) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (pull_request) Canceled after 0s
CI / Web (dms-browser) (pull_request) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (push) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Canceled after 0s
CI / Web (dms-browser) (push) Canceled after 0s
CI / Web (pdf-viewer) (push) Canceled after 0s
CI / Web (shl) (push) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (push) Canceled after 0s
Supply-Chain-Scan (npm) / npm-audit (push) Canceled after 0s
CI / Web (pdf-viewer) (pull_request) Canceled after 0s
CI / Web (shl) (pull_request) Canceled after 0s
Core-Schnittstellen-Vertragstests / contract-tests (pull_request) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (pull_request) Canceled after 0s
Supply-Chain-Scan (npm) / npm-audit (pull_request) Canceled after 0s
Core-Pflichttest-Gate / pflichttest-gate (pull_request) Canceled after 0s
RBAC-Rollenhierarchie-Pflichtgate / rbac-policy-invariant (pull_request) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (pull_request) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Canceled after 0s
TSK-04: aufgaben oberflaeche
Neue additive status-Spalte (open/in_progress/done) auf tasks,
status.go (SetStatus/ByStatus/GetStatus/ListAll) haelt completed_at
synchron fuer TSK-03s escalation.Store. Neuer HTTP-Layer taskapi
(GET /tasks, PATCH /tasks/{id}/status), Tenant-Kontext-Durchsetzung
wie backchannelapi (VOR-06). Neue Next.js-Oberflaeche
web/workflow-tasks (Liste + Kanban, Drag&Drop + Tastaturbedienung).
tasks-a11y-fixture fuer den echten Browser-Test, kein Produktions-
binary vorhanden das geaendert werden musste.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-26 23:26:56 +02:00

113 lines
5.6 KiB
TypeScript

import { describe, it, expect, vi, afterEach } from "vitest";
import { render, screen, fireEvent, waitFor, within } from "@testing-library/react";
import { ThemeProvider, I18nProvider, ToastProvider } from "@nexarch/shl";
import { TaskBoard } from "./TaskBoard";
import type { Task } from "../lib/api";
function renderWithProviders(ui: React.ReactElement) {
return render(
<ThemeProvider>
<I18nProvider initialLocale="de">
<ToastProvider>{ui}</ToastProvider>
</I18nProvider>
</ThemeProvider>
);
}
const past = new Date(Date.now() - 86400000).toISOString();
const future = new Date(Date.now() + 86400000 * 3).toISOString();
function makeTasks(): Task[] {
return [
{ ID: "t1", Title: "Ueberfaellig-Aufgabe", AssigneeType: "person", AssigneeID: "u1", Priority: "high", References: null, DueDate: past, Status: "open" },
{ ID: "t2", Title: "Zukunfts-Aufgabe", AssigneeType: "person", AssigneeID: "u1", Priority: "low", References: null, DueDate: future, Status: "open" },
{ ID: "t3", Title: "Erledigte-Aufgabe", AssigneeType: "person", AssigneeID: "u1", Priority: "normal", References: null, DueDate: past, Status: "done" },
];
}
describe("TaskBoard", () => {
const originalFetch = global.fetch;
afterEach(() => {
global.fetch = originalFetch;
});
// AK1/Pruefung2: ueberfaellige Aufgaben sind klar gekennzeichnet;
// erledigte Aufgaben gelten nie als ueberfaellig, auch mit vergangener
// Faelligkeit (siehe t3).
it("kennzeichnet ueberfaellige, nicht erledigte Aufgaben", async () => {
global.fetch = vi.fn(async () => new Response(JSON.stringify(makeTasks()), { status: 200 })) as typeof fetch;
renderWithProviders(<TaskBoard userId="alice" tenantSlug="acme" />);
await waitFor(() => expect(screen.getByTestId("task-list")).toBeInTheDocument());
const list = screen.getByTestId("task-list");
expect(within(list).getByText(/Ueberfaellig-Aufgabe/)).toHaveTextContent("(ueberfaellig)");
expect(within(list).getByText(/Zukunfts-Aufgabe/)).not.toHaveTextContent("(ueberfaellig)");
expect(within(list).getByText(/Erledigte-Aufgabe/)).not.toHaveTextContent("(ueberfaellig)");
});
// AK2/Pruefung1: Filterkombination (Prioritaet + nur ueberfaellige).
it("filtert nach Prioritaet und ueberfaellig-Status kombiniert", async () => {
global.fetch = vi.fn(async () => new Response(JSON.stringify(makeTasks()), { status: 200 })) as typeof fetch;
renderWithProviders(<TaskBoard userId="alice" tenantSlug="acme" />);
await waitFor(() => expect(screen.getByTestId("task-list")).toBeInTheDocument());
fireEvent.change(screen.getByLabelText("Prioritaet"), { target: { value: "high" } });
fireEvent.click(screen.getByLabelText(/Nur/));
const list = screen.getByTestId("task-list");
expect(within(list).getByText(/Ueberfaellig-Aufgabe/)).toBeInTheDocument();
expect(within(list).queryByText(/Zukunfts-Aufgabe/)).not.toBeInTheDocument();
expect(within(list).queryByText(/Erledigte-Aufgabe/)).not.toBeInTheDocument();
});
// AK4/Pruefung4: Statuswechsel in der Kanban-Ansicht schreibt ueber
// TSK-01 zurueck (hier: Tastaturaktion statt HTML5-DnD, gleichwertig,
// siehe Komponentenkommentar -- realer DnD-Event ist in jsdom nicht
// sinnvoll simulierbar).
it("verschiebt eine Aufgabe in der Kanban-Ansicht und schreibt den Status zurueck", async () => {
const statusCalls: { id: string; body: string }[] = [];
global.fetch = vi.fn(async (url: string | URL, init?: RequestInit) => {
const u = url.toString();
if (u.includes("/status")) {
const idMatch = u.match(/tasks\/([^/?]+)\/status/);
statusCalls.push({ id: idMatch ? idMatch[1] : "", body: init?.body as string });
return new Response(null, { status: 204 });
}
return new Response(JSON.stringify(makeTasks()), { status: 200 });
}) as typeof fetch;
renderWithProviders(<TaskBoard userId="alice" tenantSlug="acme" />);
await waitFor(() => expect(screen.getByTestId("task-list")).toBeInTheDocument());
fireEvent.click(screen.getByText("Kanban"));
await waitFor(() => expect(screen.getByRole("group", { name: "Spalte Offen" })).toBeInTheDocument());
fireEvent.click(screen.getByLabelText("Ueberfaellig-Aufgabe nach Erledigt verschieben"));
await waitFor(() => expect(statusCalls).toHaveLength(1));
expect(statusCalls[0].id).toBe("t1");
expect(JSON.parse(statusCalls[0].body)).toEqual({ status: "done" });
});
// AK5: Backend liefert bereits nur Aufgaben des aktuellen Mandanten
// (server-seitiger Filter in taskapi, siehe workflow/internal/taskapi
// Tests) -- hier nur geprueft, dass die Komponente die Backend-Liste
// unveraendert uebernimmt, keine eigene, umgehbare Client-Filterung
// dazwischenschaltet.
it("zeigt genau die vom Backend gelieferte Aufgabenliste ohne eigene Mandantenfilterung", async () => {
global.fetch = vi.fn(async () => new Response(JSON.stringify(makeTasks()), { status: 200 })) as typeof fetch;
renderWithProviders(<TaskBoard userId="alice" tenantSlug="acme" />);
await waitFor(() => expect(screen.getByTestId("task-list")).toBeInTheDocument());
const list = screen.getByTestId("task-list");
expect(within(list).getAllByRole("listitem")).toHaveLength(3);
});
it("zeigt eine Fehlermeldung, wenn der Mandant abgelehnt wird", async () => {
global.fetch = vi.fn(async () => new Response(JSON.stringify({ error: "nicht berechtigt" }), { status: 403 })) as typeof fetch;
renderWithProviders(<TaskBoard userId="alice" tenantSlug="fremd" />);
await waitFor(() => expect(screen.getByRole("alert")).toBeInTheDocument());
});
});