Files
nexarch/web/portal/app/ModuleGrid.render.test.tsx
T

191 lines
8.4 KiB
TypeScript

import { describe, expect, it, vi, beforeEach } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { I18nProvider, MODULE_REGISTRY } from "@nexarch/shl";
import "./messages";
import { MODULE_ICONS } from "./moduleIcons";
const fetchMock = vi.fn();
let tokenPromise: Promise<string>;
vi.mock("../lib/http", () => ({
getSession: () => ({
token: () => tokenPromise,
identity: () => ({ userId: "u-1", tenant: "acme", role: "user" }),
fetch: (...a: unknown[]) => fetchMock(...a),
}),
getWorkflowSession: () => ({ token: () => Promise.reject(new Error("kein workflow-token")) }),
}));
import { ModuleGrid } from "./ModuleGrid";
const mods = [
{ key: "dms", group: "work", label: "Dokumente", description: "Dokumente verwalten.", href: "http://localhost:3101", permission: "p" },
{ key: "workflow", group: "work", label: "Aufgaben & Freigaben", description: "Freigaben.", href: "http://localhost:3103", permission: "p" },
{ key: "rbac_admin", group: "admin", label: "Rollen & Rechte", description: "Rechte.", href: "http://localhost:3112", permission: "p" },
];
function reply(modules: unknown, worklist: unknown) {
fetchMock.mockImplementation(async (url: string) => {
if (url === "/api/visible-modules") return new Response(JSON.stringify({ modules }), { status: 200 });
if (url === "/api/worklist") return worklist ? new Response(JSON.stringify(worklist), { status: 200 }) : new Response("", { status: 500 });
return new Response("", { status: 404 });
});
}
function renderGrid(locale: "de" | "en" = "de") {
return render(<I18nProvider initialLocale={locale}><ModuleGrid /></I18nProvider>);
}
beforeEach(() => {
fetchMock.mockReset();
tokenPromise = Promise.resolve("t");
});
describe("Icons", () => {
it("jedes Modul der Registry hat ein eigenes Icon", () => {
for (const m of MODULE_REGISTRY.filter((x) => x.key !== "portal")) expect(MODULE_ICONS[m.key], m.key).toBeTruthy();
});
});
describe("ModuleGrid (gerendert)", () => {
it("zeigt zuerst den Skeleton mit Statustext, danach die Kacheln", async () => {
let release!: (v: string) => void;
tokenPromise = new Promise((r) => (release = r));
reply(mods, { approvals: 5, tasks: 0, remindersDue: 2 });
renderGrid();
expect(screen.getByRole("status").textContent).toBe("Übersicht wird geladen");
release("t");
expect(await screen.findByRole("link", { name: /Dokumente/ })).toBeTruthy();
expect(screen.queryByText("Übersicht wird geladen")).toBeNull();
});
it("Badge zeigt Text mit Einheit, Gruppen sind Ueberschriften", async () => {
reply(mods, { approvals: 5, tasks: 0, remindersDue: 2 });
renderGrid();
const wf = await screen.findByRole("link", { name: /Aufgaben & Freigaben/ });
expect(wf.textContent).toContain("5 offen");
expect(screen.getByRole("heading", { name: /Arbeiten/ })).toBeTruthy();
expect(screen.getByRole("heading", { name: /Verwalten/ })).toBeTruthy();
expect(screen.getByRole("heading", { name: "Heute" })).toBeTruthy();
});
it("Ausfall der Worklist blendet 'Heute' aus, Kacheln bleiben", async () => {
reply(mods, null);
renderGrid();
await screen.findByRole("link", { name: /Dokumente/ });
expect(screen.queryByRole("heading", { name: "Heute" })).toBeNull();
});
it("Teilausfall einer Zaehlerquelle wird als nicht verfuegbar benannt", async () => {
reply(mods, { approvals: 1, tasks: null, remindersDue: 0 });
renderGrid();
expect(await screen.findByText(/Meine Aufgaben \(derzeit nicht verfügbar\)/)).toBeTruthy();
});
it("Fehler zeigt Meldung mit 'Erneut laden', das den Abruf wiederholt", async () => {
const user = userEvent.setup();
fetchMock.mockRejectedValueOnce(new Error("netz")).mockRejectedValueOnce(new Error("netz"));
renderGrid();
const alert = await screen.findByRole("alert");
expect(alert.textContent).toContain("konnte nicht geladen werden");
reply(mods, { approvals: 0, tasks: 0, remindersDue: 0 });
await user.click(screen.getByRole("button", { name: "Erneut laden" }));
expect(await screen.findByRole("link", { name: /Dokumente/ })).toBeTruthy();
});
it("leere Modulliste zeigt Hinweis auf die Administration", async () => {
reply([], null);
renderGrid();
expect(await screen.findByText(/Berechtigungen vergibt Ihre Administration/)).toBeTruthy();
});
it("englische Sprache uebersetzt Gruppen und Badge", async () => {
reply(mods, { approvals: 3, tasks: 0, remindersDue: 0 });
renderGrid("en");
const wf = await screen.findByRole("link", { name: /Aufgaben & Freigaben/ });
expect(wf.textContent).toContain("3 open");
expect(screen.getByRole("heading", { name: /Work/ })).toBeTruthy();
});
});
import { orderGroups } from "./ModuleGrid";
describe("orderGroups", () => {
it("sortiert nach Anzahl sichtbarer Module, bei Gleichstand work/admin/audit", () => {
const g = (key: string, n: number) => ({ key, modules: new Array(n).fill(0) });
expect(orderGroups([g("work", 1), g("admin", 1), g("audit", 1)]).map((x) => x.key)).toEqual(["work", "admin", "audit"]);
expect(orderGroups([g("work", 1), g("audit", 3)]).map((x) => x.key)).toEqual(["audit", "work"]);
});
});
describe("Favoriten, Zuletzt benutzt und Einklappen", () => {
beforeEach(() => window.localStorage.clear());
it("Stern markiert Favorit, zeigt Abschnitt und meldet es per Live-Region", async () => {
const user = userEvent.setup();
reply(mods, { approvals: 0, tasks: 0, remindersDue: 0 });
renderGrid();
const star = await screen.findByRole("button", { name: /Dokumente als Favorit markieren/ });
expect(star.getAttribute("aria-pressed")).toBe("false");
await user.click(star);
expect(screen.getByRole("heading", { name: "Favoriten" })).toBeTruthy();
expect(window.localStorage.getItem("nexarch-portal-favorites")).toBe('["dms"]');
expect(screen.getAllByRole("status").some((el) => el.textContent === "Dokumente zu Favoriten hinzugefügt")).toBe(true);
expect(screen.getAllByRole("button", { name: /Dokumente als Favorit entfernen/ })[0].getAttribute("aria-pressed")).toBe("true");
});
it("entzogenes Modul verschwindet aus den Favoriten", async () => {
window.localStorage.setItem("nexarch-portal-favorites", '["dms","ai"]');
reply(mods, null);
renderGrid();
expect(await screen.findByRole("heading", { name: "Favoriten" })).toBeTruthy();
expect(JSON.parse(window.localStorage.getItem("nexarch-portal-favorites") ?? "[]")).toContain("ai");
expect(screen.getAllByRole("button", { name: /als Favorit entfernen/ })).toHaveLength(2);
});
it("Klick auf eine Kachel landet unter 'Zuletzt benutzt'", async () => {
const user = userEvent.setup();
reply(mods, null);
renderGrid();
const link = await screen.findByRole("link", { name: /Rollen & Rechte/ });
link.addEventListener("click", (e) => e.preventDefault());
await user.click(link);
expect(window.localStorage.getItem("nexarch-portal-recent")).toBe('["rbac_admin"]');
expect(screen.getByRole("heading", { name: "Zuletzt benutzt" })).toBeTruthy();
});
it("Gruppe laesst sich ein- und ausklappen und merkt sich den Zustand", async () => {
const user = userEvent.setup();
reply(mods, null);
renderGrid();
const toggle = await screen.findByRole("button", { name: /Verwalten/ });
expect(toggle.getAttribute("aria-expanded")).toBe("true");
await user.click(toggle);
expect(toggle.getAttribute("aria-expanded")).toBe("false");
expect(document.getElementById("list-admin")?.hidden).toBe(true);
expect(window.localStorage.getItem("nexarch-portal-collapsed")).toBe('["admin"]');
});
});
describe("Befehlsleiste im Portal", () => {
it("Strg+K oeffnet die Palette, '/' ausserhalb von Eingabefeldern ebenfalls", async () => {
const user = userEvent.setup();
reply(mods, null);
renderGrid();
await screen.findByRole("link", { name: /Dokumente/ });
await user.keyboard("{Control>}k{/Control}");
expect(await screen.findByRole("combobox")).toBeTruthy();
await user.keyboard("{Escape}");
expect(screen.queryByRole("combobox")).toBeNull();
await user.keyboard("/");
expect(await screen.findByRole("combobox")).toBeTruthy();
});
it("Ausloeser-Button nennt das Kuerzel", async () => {
reply(mods, null);
renderGrid();
const btn = await screen.findByRole("button", { name: /Modul oder Dokument suchen/ });
expect(btn.textContent).toContain("Strg K");
});
});