// BudgetTable and the getting-started card's target (#400). No jsdom: // react-dom/server renders in vitest's node environment. The collapse hook is // replaced by the real pure helpers over a state the test sets (the real hook // hydrates in an effect, which a static render never runs), and the // subtotal-position preference is read from a stubbed localStorage. What is // pinned is where the one highlight lands: "Expand all" or this month's cell // of the first editable row shown. import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { renderToStaticMarkup } from "react-dom/server"; import type { BudgetYearRow } from "../../shared/types"; import type { CollapseAccessors } from "../../utils/collapsibleRows"; vi.mock("react-i18next", () => ({ useTranslation: () => ({ t: (k: string) => `t(${k})`, i18n: { language: "fr" } }), })); const harness = vi.hoisted(() => ({ /** Keys of the open groups (`p:`): the grid opens collapsed. */ expanded: new Set(), })); vi.mock("../../hooks/useCollapsibleGroups", async () => { const { collapsibleKeys, isCollapsedFor, visibleRows } = await import("../../utils/collapsibleRows"); return { useCollapsibleGroups: (_key: string | null, acc: CollapseAccessors) => { const isCollapsed = (row: T) => isCollapsedFor(harness.expanded, acc.keyOf(row), false); return { visible: (rows: T[]) => visibleRows(rows, acc, isCollapsed), isCollapsed, toggle: () => undefined, expandAll: () => undefined, collapseAll: () => undefined, allExpanded: (rows: T[]) => { const keys = collapsibleKeys(rows, acc); return keys.length > 0 && keys.every((key) => harness.expanded.has(key)); }, groupCount: (rows: T[]) => collapsibleKeys(rows, acc).length, }; }, }; }); import BudgetTable, { BUDGET_CELL_TARGET, BUDGET_EXPAND_ALL_TARGET } from "./BudgetTable"; const leaf = ( category_id: number, category_name: string, category_type: BudgetYearRow["category_type"], parent_id: number | null = null, ): BudgetYearRow => ({ category_id, category_name, category_color: "#9ca3af", category_type, parent_id, is_parent: false, depth: parent_id === null ? 0 : 1, months: Array(12).fill(0) as number[], annual: 0, previousYearTotal: 0, }); const group = ( category_id: number, category_name: string, category_type: BudgetYearRow["category_type"], ): BudgetYearRow => ({ ...leaf(category_id, category_name, category_type), is_parent: true }); /** Two groups, in the order `buildBudgetYearRows` emits them: income first. */ const GROUPED: BudgetYearRow[] = [ group(1, "Revenus", "income"), leaf(2, "Salaire", "income", 1), group(10, "Logement", "expense"), leaf(11, "Loyer", "expense", 10), leaf(12, "Électricité", "expense", 10), ]; /** A category without sub-category stays on screen while the groups are collapsed. */ const WITH_STANDALONE: BudgetYearRow[] = [...GROUPED, leaf(30, "Divers", "expense")]; /** No group at all. */ const FLAT: BudgetYearRow[] = [leaf(30, "Divers", "expense"), leaf(31, "Cadeaux", "expense")]; const MARCH = 2; const DECEMBER = 11; const render = (rows: BudgetYearRow[], onboardingTarget?: boolean) => renderToStaticMarkup( undefined} onSplitEvenly={() => undefined} onboardingTarget={onboardingTarget} />, ); const HIGHLIGHT = 'data-onboarding-highlight="true"'; const count = (html: string, needle: string) => html.split(needle).length - 1; /** The one highlighted control: its target key and its inner markup. */ function highlighted(html: string): { target: string; inner: string } { expect(count(html, HIGHLIGHT)).toBe(1); const match = html.match(/