diff --git a/src/hooks/useBudget.test.ts b/src/hooks/useBudget.test.ts index 77cad06..6043aae 100644 --- a/src/hooks/useBudget.test.ts +++ b/src/hooks/useBudget.test.ts @@ -359,6 +359,11 @@ describe("buildBudgetYearRows — a category whose parent is deleted or gone (#3 ); expect(rows.some((r) => r.category_id === 7 || r.category_id === 8)).toBe(false); }); + + it("hides a category that is its own parent too, a loop of one member", () => { + const rows = buildBudgetYearRows([...CATEGORIES, cat(7, "Gifts", "expense", { parent_id: 7 })], [entry(7, 40)], []); + expect(rows.some((r) => r.category_id === 7)).toBe(false); + }); }); describe("buildBudgetYearRows + multi-level collapse (issue #289 end-to-end)", () => { diff --git a/src/pages/CategoriesPage.render.test.tsx b/src/pages/CategoriesPage.render.test.tsx index 80df5bd..55fb941 100644 --- a/src/pages/CategoriesPage.render.test.tsx +++ b/src/pages/CategoriesPage.render.test.tsx @@ -202,6 +202,35 @@ describe("CategoriesPage — « Vérifier la hiérarchie » (#393)", () => { expect(render()).toContain("t(categories.hierarchy.errors.too_deep)"); }); + it("hands the window every category, not the tree: an orphan in a loop is named", () => { + const node = (id: number, name: string, parent_id: number | null) => ({ + id, + name, + parent_id, + color: null, + icon: null, + type: "expense", + is_active: true, + is_inputable: true, + sort_order: id, + keyword_count: 0, + i18n_key: null, + children: [], + }); + // 3 <-> 5: no root reaches them, so the tree holds root 1 (and 10 under it) only. + const categories = [node(1, "Logement", null), node(10, "Loyer", 1), node(3, "Cadeaux", 5), node(5, "Fêtes", 3)]; + harness.state = { + isLoading: false, + categories, + tree: [{ ...categories[0], children: [categories[1]] }], + hierarchyCheck: { orphans: [{ id: 3, reason: "cycle" }], error: null }, + }; + const html = render(); + expect(html).toContain(">Cadeaux

"); + expect(html).toContain('aria-label="t(categories.hierarchy.newParentFor[name=Cadeaux])"'); + expect(html).not.toContain(">—

"); + }); + it("translates a refused save of the form by its key, and shows any other failure as is", () => { harness.state = { error: { key: "categories.hierarchy.errors.parent_has_transactions" } }; expect(render()).toContain(">t(categories.hierarchy.errors.parent_has_transactions)"); diff --git a/src/utils/categoryHierarchy.test.ts b/src/utils/categoryHierarchy.test.ts index de853a6..11b537a 100644 --- a/src/utils/categoryHierarchy.test.ts +++ b/src/utils/categoryHierarchy.test.ts @@ -113,6 +113,10 @@ describe("displaysAsRoot", () => { expect(displaysAsRoot({ id: 10, parent_id: 1 }, ids)).toBe(false); expect(displaysAsRoot({ id: 1, parent_id: 2 }, ids)).toBe(false); }); + + it("is not a root as its own parent: that one-member loop stays hidden, like the tree hides it", () => { + expect(displaysAsRoot({ id: 10, parent_id: 10 }, ids)).toBe(false); + }); }); describe("isActiveRow", () => { @@ -139,6 +143,11 @@ describe("findOrphans (#393)", () => { expect(findOrphans(rows)).toEqual([{ id: 3, reason: "cycle" }]); }); + it("names the loop by its own smallest member, never by a smaller id that hangs under it", () => { + // 5 <-> 8, 2 under 5: reattaching 2 would leave 5 and 8 hidden. + expect(findOrphans([r(2, 5), r(5, 8), r(8, 5), r(1, null)])).toEqual([{ id: 5, reason: "cycle" }]); + }); + it("lists a three-member cycle once, by its smallest id, whatever the order of the rows", () => { // 6 -> 4 -> 9 -> 6, 12 under 9. const rows = [r(6, 4), r(4, 9), r(9, 6), r(12, 9), r(1, null)];