// HierarchyCheckModal — what the « Vérifier la hiérarchie » window shows (#393). // No jsdom: react-dom/server renders in vitest's node environment. A closed // CategoryCombobox renders its input only, without its portal (#429). import { describe, it, expect, vi } from "vitest"; import { renderToStaticMarkup } from "react-dom/server"; vi.mock("react-i18next", async () => (await import("../../test/i18nMock")).i18nMock()); import HierarchyCheckModal from "./HierarchyCheckModal"; import type { CategoryTreeNode } from "../../shared/types"; import type { OrphanCategory } from "../../utils/categoryHierarchy"; const node = (id: number, parent_id: number | null, name: string, i18n_key: string | null = null): CategoryTreeNode => ({ id, name, parent_id, color: null, icon: null, type: "expense", is_active: true, is_inputable: true, sort_order: id, keyword_count: 0, i18n_key, children: [], }); // 1 is a root; 3 <-> 5 is a cycle; 20's parent is gone. const CATEGORIES = [ node(1, null, "Logement", "categoriesSeed.housing"), node(3, 5, "Épicerie"), node(5, 3, "Alimentation"), node(20, 99, "Cadeaux"), ]; const ORPHANS: OrphanCategory[] = [ { id: 3, reason: "cycle" }, { id: 20, reason: "missing_parent" }, ]; const render = (props: { orphans: OrphanCategory[] | null; error?: string | null; isSaving?: boolean }) => renderToStaticMarkup( , ); const comboboxes = (html: string) => html.match(/]*role="combobox"[^>]*>/g) ?? []; describe("HierarchyCheckModal (#393)", () => { it("is a modal dialog named after the button, with its explanation and a close button", () => { const html = render({ orphans: [] }); expect(html).toContain('role="dialog"'); expect(html).toContain('aria-modal="true"'); expect(html).toContain('aria-label="t(categories.hierarchy.check)"'); expect(html).toContain("t(categories.hierarchy.intro)"); expect(html).toContain("t(common.close)"); }); it("says the check runs while the orphans are not known", () => { const html = render({ orphans: null }); expect(html).toContain("t(categories.hierarchy.checking)"); expect(html).not.toContain("t(categories.hierarchy.none)"); expect(comboboxes(html)).toHaveLength(0); }); it("says « aucune » for a sound hierarchy, with no row", () => { const html = render({ orphans: [] }); expect(html).toContain("t(categories.hierarchy.none)"); expect(html).not.toContain("categories.hierarchy.count"); expect(html).not.toContain("t(categories.hierarchy.reattach)"); }); it("counts the orphans, and gives each row its name, its reason, a new parent and « Rattacher »", () => { const html = render({ orphans: ORPHANS }); expect(html).toContain("t(categories.hierarchy.count[count=2])"); expect(html).toContain(">Épicerie

"); expect(html).toContain(">Cadeaux

"); expect(html).toContain("t(categories.hierarchy.reasons.cycle)"); expect(html).toContain("t(categories.hierarchy.reasons.missing_parent)"); expect(html.split("t(categories.hierarchy.reattach)")).toHaveLength(3); const inputs = comboboxes(html); expect(inputs).toHaveLength(2); expect(inputs[0]).toContain('aria-label="t(categories.hierarchy.newParentFor[name=Épicerie])"'); expect(inputs[1]).toContain('aria-label="t(categories.hierarchy.newParentFor[name=Cadeaux])"'); }); it("preselects the root on every row", () => { for (const input of comboboxes(render({ orphans: ORPHANS }))) { expect(input).toContain('value="t(categories.hierarchy.root)"'); } }); it("names a seed category in the app's language", () => { const html = render({ orphans: [{ id: 1, reason: "inactive_parent" }] }); expect(html).toContain(">t(categoriesSeed.housing[defaultValue=Logement])

"); expect(html).toContain("t(categories.hierarchy.reasons.inactive_parent)"); }); it("shows the translated error above the list, as an alert", () => { const html = render({ orphans: ORPHANS, error: "Ce parent a des transactions" }); expect(html).toContain('role="alert"'); expect(html.indexOf("Ce parent a des transactions")).toBeLessThan(html.indexOf("categories.hierarchy.count")); }); it("shows a failed check alone, without the « checking » line", () => { const html = render({ orphans: null, error: "database is locked" }); expect(html).toContain("database is locked"); expect(html).not.toContain("t(categories.hierarchy.checking)"); }); it("disables « Rattacher » while a reattachment runs", () => { const html = render({ orphans: ORPHANS, isSaving: true }); const buttons = html.match(/]*>t\(categories\.hierarchy\.reattach\)<\/button>/g) ?? []; expect(buttons).toHaveLength(2); for (const button of buttons) expect(button).toContain('disabled=""'); expect(render({ orphans: ORPHANS })).not.toContain('disabled=""'); }); });