// TransactionTable — the getting-started target (#399). No jsdom: // react-dom/server renders in vitest's node environment, and the markup says // which element carries the highlight: a wrapper around the category of the // first row shown, CategoryCombobox itself left as it is. import { describe, it, expect, vi } from "vitest"; import { renderToStaticMarkup } from "react-dom/server"; vi.mock("react-i18next", async () => (await import("../../test/i18nMock")).i18nMock()); import TransactionTable, { TRANSACTION_CATEGORY_TARGET } from "./TransactionTable"; import type { Category, TransactionRow } from "../../shared/types"; import type { SplitAccess } from "../../shared/splitGate"; const HIGHLIGHT = 'data-onboarding-highlight="true"'; const count = (html: string, needle: string) => html.split(needle).length - 1; const GROCERIES: Category = { id: 7, name: "Épicerie", type: "expense", is_active: true, is_inputable: true, sort_order: 1, created_at: "", }; const row = ( id: number, description: string, category: Category | null = null, isSplit = false, ): TransactionRow => ({ id, date: "2026-09-12", description, amount: -42.5, category_id: category?.id ?? null, category_name: category?.name ?? null, category_color: null, source_name: "Exemple Desjardins", notes: null, is_manually_categorized: false, is_split: isSplit, }); /** The guided entry: uncategorized rows only. */ const UNCATEGORIZED = [row(1, "IGA 123"), row(2, "METRO 456"), row(3, "SAQ 789")]; const markup = (rows: TransactionRow[], onboardingTarget?: boolean, splitAccess: SplitAccess = "open") => renderToStaticMarkup( undefined)} onLoadSplitChildren={vi.fn(async () => [])} onSaveSplit={vi.fn(async () => undefined)} onDeleteSplit={vi.fn(async () => undefined)} splitAccess={splitAccess} onboardingTarget={onboardingTarget} />, ); /** The rows of the table's body, in order. */ const bodyRows = (html: string) => html.split(" new RegExp( `
` + `
]*role="combobox"[^>]*value="${value.replace(/[()[\]]/g, "\\$&")}"[^>]*/>
`, ); describe("TransactionTable — getting-started target (#399)", () => { it("highlights exactly one element: a wrapper around the first row's category", () => { const html = markup(UNCATEGORIZED, true); expect(count(html, HIGHLIGHT)).toBe(1); const [first, ...others] = bodyRows(html); expect(first).toContain("IGA 123"); expect(first).toMatch(ringedCategory("t(transactions.table.noCategory)")); expect(others).toHaveLength(2); for (const other of others) { expect(other).not.toContain("data-onboarding"); // Every row has the same wrapper, without the ring. expect(other).toContain('
{ const html = markup([row(1, "IGA 123", GROCERIES), row(2, "METRO 456")], true); expect(count(html, HIGHLIGHT)).toBe(1); const [first] = bodyRows(html); expect(first).toMatch(new RegExp(`${ringedCategory("Épicerie").source} { expect(markup(UNCATEGORIZED)).not.toContain("data-onboarding"); expect(markup(UNCATEGORIZED, false)).not.toContain("data-onboarding"); }); it("highlights nothing without a row: the card stays, without a ring", () => { const html = markup([], true); expect(html).toContain("t(transactions.noTransactions)"); expect(html).not.toContain("data-onboarding"); }); }); describe("TransactionTable — the Répartition button (#369)", () => { /** A split row, then a plain one, both categorized (the button needs a category). */ const CATEGORIZED = [row(1, "COSTCO 123", GROCERIES, true), row(2, "IGA 456", GROCERIES)]; /** The rows' split buttons, in order (their title names them). */ const splitButtons = (html: string) => html.match(/