Review of PR #452 (comment 24129): the upgrade to Base checked that the upsell goes away, not that no split window appears in its place, so a locked click that also armed the split window (R14) left the suite green. Both edition changes now assert the other window stays closed, during the license refresh and after it. R14 now turns 1 test red, and its mirror (a split click that also arms the upsell) 1 test red. Refs #369 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
142 lines
5.6 KiB
TypeScript
142 lines
5.6 KiB
TypeScript
// TransactionTable — what the Répartition button opens (#369).
|
|
//
|
|
// A static render never shows a window: they open on a click, through the
|
|
// table's own state. React's hooks come from the shared harness
|
|
// (src/test/hookHarness.ts, #409): the table runs as a plain function, its
|
|
// state survives from one call to the next as between two renders of a mounted
|
|
// component, and the test clicks through the props the table hands to each
|
|
// SplitButton, then renders again to read what opened.
|
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
import { isValidElement, type ReactElement, type ReactNode } from "react";
|
|
|
|
vi.mock("react", async (orig) => (await import("../../test/hookHarness")).fakeReact(await orig()));
|
|
vi.mock("react-i18next", async () => (await import("../../test/i18nMock")).i18nMock());
|
|
|
|
import { hookHarness } from "../../test/hookHarness";
|
|
import TransactionTable from "./TransactionTable";
|
|
import SplitButton from "./SplitButton";
|
|
import SplitAdjustmentModal from "./SplitAdjustmentModal";
|
|
import UpsellDialog from "../shared/UpsellDialog";
|
|
import type { SplitAccess } from "../../shared/splitGate";
|
|
import type { Category, TransactionRow } from "../../shared/types";
|
|
|
|
const harness = hookHarness();
|
|
|
|
type Props = Record<string, unknown> & { children?: ReactNode };
|
|
|
|
function find(node: ReactNode, type: unknown): ReactElement<Props>[] {
|
|
if (Array.isArray(node)) return node.flatMap((n) => find(n, type));
|
|
if (!isValidElement(node)) return [];
|
|
const el = node as ReactElement<Props>;
|
|
return [...(el.type === type ? [el] : []), ...find(el.props.children, type)];
|
|
}
|
|
|
|
const GROCERIES: Category = {
|
|
id: 7,
|
|
name: "Épicerie",
|
|
type: "expense",
|
|
is_active: true,
|
|
is_inputable: true,
|
|
sort_order: 1,
|
|
created_at: "",
|
|
};
|
|
|
|
const row = (id: number, isSplit: boolean): TransactionRow => ({
|
|
id,
|
|
date: "2026-09-12",
|
|
description: `TX ${id}`,
|
|
amount: -42.5,
|
|
category_id: GROCERIES.id,
|
|
category_name: GROCERIES.name,
|
|
category_color: null,
|
|
source_name: "Exemple Desjardins",
|
|
notes: null,
|
|
is_manually_categorized: true,
|
|
is_split: isSplit,
|
|
});
|
|
|
|
/** A split row and a plain one: the button must behave the same on both. */
|
|
const ROWS = [row(1, true), row(2, false)];
|
|
|
|
/** One render of the mounted table, under the given access. */
|
|
const render = (splitAccess: SplitAccess) =>
|
|
harness.render(() =>
|
|
TransactionTable({
|
|
rows: ROWS,
|
|
sort: { column: "date", direction: "desc" },
|
|
categories: [GROCERIES],
|
|
onSort: vi.fn(),
|
|
onCategoryChange: vi.fn(),
|
|
onNotesChange: vi.fn(),
|
|
onAddKeyword: vi.fn(async () => undefined),
|
|
onLoadSplitChildren: vi.fn(async () => []),
|
|
onSaveSplit: vi.fn(async () => undefined),
|
|
onDeleteSplit: vi.fn(async () => undefined),
|
|
splitAccess,
|
|
}),
|
|
);
|
|
|
|
const splitButtons = (tree: ReactNode) => find(tree, SplitButton);
|
|
const upsells = (tree: ReactNode) => find(tree, UpsellDialog);
|
|
const splitWindows = (tree: ReactNode) => find(tree, SplitAdjustmentModal);
|
|
|
|
beforeEach(() => harness.clear());
|
|
|
|
describe("TransactionTable — the Répartition button's wiring (#369)", () => {
|
|
it("hands the table's access to every row's button, and opens nothing before a click", () => {
|
|
for (const access of ["open", "locked", "pending"] as const) {
|
|
harness.clear();
|
|
const tree = render(access);
|
|
const buttons = splitButtons(tree);
|
|
expect(buttons.map((b) => b.props.access)).toEqual([access, access]);
|
|
expect(buttons.map((b) => b.props.row)).toEqual(ROWS);
|
|
expect(upsells(tree)).toHaveLength(0);
|
|
expect(splitWindows(tree)).toHaveLength(0);
|
|
}
|
|
});
|
|
|
|
it("Free: the click opens the upsell for 'adjustments', never the split window, on any row", () => {
|
|
for (const index of [0, 1]) {
|
|
harness.clear();
|
|
(splitButtons(render("locked"))[index].props.onOpenUpsell as () => void)();
|
|
const opened = render("locked");
|
|
const dialogs = upsells(opened);
|
|
expect(dialogs, `row ${index}`).toHaveLength(1);
|
|
expect(dialogs[0].props.feature).toBe("adjustments");
|
|
expect(splitWindows(opened)).toHaveLength(0);
|
|
// The dialog's close hands control back to the table.
|
|
(dialogs[0].props.onClose as () => void)();
|
|
expect(upsells(render("locked"))).toHaveLength(0);
|
|
}
|
|
});
|
|
|
|
it("Base and Premium: the click opens the split window on its own row, no upsell", () => {
|
|
(splitButtons(render("open"))[1].props.onOpenSplit as () => void)();
|
|
const opened = render("open");
|
|
const windows = splitWindows(opened);
|
|
expect(windows).toHaveLength(1);
|
|
expect(windows[0].props.transaction).toBe(ROWS[1]);
|
|
expect(upsells(opened)).toHaveLength(0);
|
|
});
|
|
|
|
// One click arms one window: an edition change never brings up the other one.
|
|
it("an open split window survives a license refresh and closes once the edition is Free", () => {
|
|
(splitButtons(render("open"))[0].props.onOpenSplit as () => void)();
|
|
const refreshing = render("pending");
|
|
expect(splitWindows(refreshing)).toHaveLength(1);
|
|
expect(upsells(refreshing)).toHaveLength(0);
|
|
const downgraded = render("locked");
|
|
expect(splitWindows(downgraded)).toHaveLength(0);
|
|
expect(upsells(downgraded)).toHaveLength(0);
|
|
});
|
|
|
|
it("an open upsell survives a license refresh and goes away once the edition is Base", () => {
|
|
(splitButtons(render("locked"))[0].props.onOpenUpsell as () => void)();
|
|
const refreshing = render("pending");
|
|
expect(upsells(refreshing)).toHaveLength(1);
|
|
expect(splitWindows(refreshing)).toHaveLength(0);
|
|
const upgraded = render("open");
|
|
expect(upsells(upgraded)).toHaveLength(0);
|
|
expect(splitWindows(upgraded)).toHaveLength(0);
|
|
});
|
|
});
|