// SplitButton — the Répartition button of a transaction row (#369). No jsdom: // react-dom/server renders the markup in vitest's node environment, and calling // the component as a function (UpsellCtas.render.test.tsx pattern) hands over // the button's props, so a click runs without a DOM. import { describe, it, expect, vi } from "vitest"; import type { ReactElement } from "react"; import { renderToStaticMarkup } from "react-dom/server"; import fr from "../../i18n/locales/fr.json"; import en from "../../i18n/locales/en.json"; vi.mock("react-i18next", async () => (await import("../../test/i18nMock")).i18nMock()); import SplitButton from "./SplitButton"; import type { SplitAccess } from "../../shared/splitGate"; import type { TransactionRow } from "../../shared/types"; const row = (isSplit: boolean): TransactionRow => ({ id: isSplit ? 1 : 2, date: "2026-09-12", description: isSplit ? "COSTCO 123" : "IGA 456", amount: -120, category_id: 7, category_name: "Épicerie", category_color: null, source_name: "Exemple Desjardins", notes: null, is_manually_categorized: true, is_split: isSplit, }); const ACCESSES: SplitAccess[] = ["open", "locked", "pending"]; const LOCK = "lucide-lock"; const ORANGE = "text-orange-500"; const markup = (access: SplitAccess, isSplit = false) => renderToStaticMarkup( , ); /** One click on the button, through its own props: which callback ran. */ function click(access: SplitAccess, isSplit: boolean) { const onOpenSplit = vi.fn(); const onOpenUpsell = vi.fn(); const button = SplitButton({ row: row(isSplit), access, onOpenSplit, onOpenUpsell }) as ReactElement<{ onClick: () => void; }>; button.props.onClick(); return { split: onOpenSplit.mock.calls.length, upsell: onOpenUpsell.mock.calls.length }; } describe("SplitButton — what it shows (#369)", () => { it("shows the lock in the locked state only", () => { expect(markup("locked")).toContain(LOCK); expect(markup("open")).not.toContain(LOCK); expect(markup("pending")).not.toContain(LOCK); }); it("keeps the orange of a split row in every state, and only for a split row", () => { for (const access of ACCESSES) { expect(markup(access, true), access).toContain(ORANGE); expect(markup(access, false), access).not.toContain(ORANGE); } }); it("is disabled while the license is not ready, and only then", () => { // The attribute itself: the class also names the `disabled:` variants. expect(markup("pending")).toContain('disabled=""'); expect(markup("open")).not.toContain('disabled=""'); expect(markup("locked")).not.toContain('disabled=""'); }); it("names the edition in the locked title, keeps the usual title otherwise", () => { expect(markup("locked")).toContain('title="t(transactions.splitLocked[tier=t(license.editions.base)])"'); expect(markup("open")).toContain('title="t(transactions.splitAdjustment)"'); expect(markup("pending")).toContain('title="t(transactions.splitAdjustment)"'); }); }); describe("SplitButton — what a click does (#369)", () => { // Every row: the decision never depends on the row being split already. it("locked: opens the upsell, never the split window, on a split row and on a plain one", () => { expect(click("locked", true)).toEqual({ split: 0, upsell: 1 }); expect(click("locked", false)).toEqual({ split: 0, upsell: 1 }); }); it("open: opens the split window, never the upsell", () => { expect(click("open", true)).toEqual({ split: 1, upsell: 0 }); expect(click("open", false)).toEqual({ split: 1, upsell: 0 }); }); it("pending: opens nothing", () => { expect(click("pending", true)).toEqual({ split: 0, upsell: 0 }); expect(click("pending", false)).toEqual({ split: 0, upsell: 0 }); }); }); // The i18n mock renders any key: a key missing from one locale would only show // up in the app (the raw key in English, or English in French). describe("transactions.splitLocked in both locales", () => { it("exists in fr and en and interpolates the edition", () => { for (const messages of [fr, en]) { expect(messages.transactions.splitLocked).toContain("{{tier}}"); expect(messages.transactions.splitLocked).not.toBe(messages.transactions.splitAdjustment); } }); });