From 7f252936694c82c54c700feb7927f0d2a33dd1fb Mon Sep 17 00:00:00 2001 From: le king fu Date: Mon, 19 Oct 2026 20:59:02 -0400 Subject: [PATCH] test(categories): pin the rules the review of #393 left green (#393) Review 25437, point 2: the loop named by its own smallest member, not by a smaller id under it (RM1); the page hands the window every category, not the tree (RM11); a category that is its own parent stays out of the budget, and displaysAsRoot says so (RM15). RM7 (depth counted on active categories at the service) is pinned in categoryService.test.ts by 1a36d4e, RM2 by be4b923. Co-Authored-By: Claude Opus 5.5 --- src/hooks/useBudget.test.ts | 5 ++++ src/pages/CategoriesPage.render.test.tsx | 29 ++++++++++++++++++++++++ src/utils/categoryHierarchy.test.ts | 9 ++++++++ 3 files changed, 43 insertions(+) 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)];