// useTransactions — the reads of the mount stand alone (#384). // // React's hooks come from the shared harness (src/test/hookHarness.ts, #409): // the hook runs as a plain function, its reducer is applied, and `commit()` // runs its effects (the mount reads, the first page, the search debounce, // whose timer `unmount()` clears). The services are mocked. What is pinned: // one failed read no longer drops the others, a failed count leaves // `totalInDb` unknown (the regular layout, never the "import first" card), // and the first failure, in the order categories, sources, count, is the // page's error. import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; vi.mock("react", async (orig) => (await import("../test/hookHarness")).fakeReact(await orig())); const services = vi.hoisted(() => ({ getAllCategories: vi.fn(), getAllImportSources: vi.fn(), getTransactionCount: vi.fn(), getTransactionPage: vi.fn(), })); vi.mock("../services/transactionService", () => ({ ...services, updateTransactionCategory: vi.fn(), updateTransactionNotes: vi.fn(), autoCategorizeTransactions: vi.fn(), getSplitChildren: vi.fn(), saveSplitAdjustment: vi.fn(), deleteSplitAdjustment: vi.fn(), })); vi.mock("../services/categoryService", () => ({ createKeyword: vi.fn() })); import { hookHarness } from "../test/hookHarness"; import { useTransactions, showsEmptyState } from "./useTransactions"; import type { Category, ImportSource, TransactionPageResult } from "../shared/types"; const harness = hookHarness(); const CATEGORIES = [{ id: 1, name: "Épicerie" }] as Category[]; const SOURCES = [{ id: 7, name: "Desjardins" }] as ImportSource[]; const PAGE: TransactionPageResult = { rows: [], totalCount: 0, totalAmount: 0, incomeTotal: 0, expenseTotal: 0, }; /** Lets the mount reads and the first page settle. */ const flush = async () => { for (let i = 0; i < 10; i++) await Promise.resolve(); }; /** Mounts the hook, runs its effects, lets them settle, and renders again. */ async function mount() { harness.render(() => useTransactions()); harness.commit(); await flush(); return harness.render(() => useTransactions()).state; } /** The actions of one type the hook dispatched, in order. */ const sent = (type: string) => harness.dispatch.mock.calls .map(([action]) => action as { type: string; payload?: unknown }) .filter((a) => a.type === type); beforeEach(() => { harness.clear(); services.getAllCategories.mockReset().mockResolvedValue(CATEGORIES); services.getAllImportSources.mockReset().mockResolvedValue(SOURCES); services.getTransactionCount.mockReset().mockResolvedValue(0); services.getTransactionPage.mockReset().mockResolvedValue(PAGE); }); afterEach(() => { harness.unmount(); }); describe("useTransactions — the reads of the mount (#384)", () => { it("all three succeed: categories, sources and the count, no error", async () => { const state = await mount(); expect(state.categories).toBe(CATEGORIES); expect(state.sources).toBe(SOURCES); expect(state.totalInDb).toBe(0); expect(state.error).toBeNull(); expect(showsEmptyState(state)).toBe(true); }); it("a failed count still loads the categories and the sources, and leaves the count unknown", async () => { services.getTransactionCount.mockRejectedValue(new Error("count failed")); const state = await mount(); expect(state.categories).toBe(CATEGORIES); expect(state.sources).toBe(SOURCES); expect(state.totalInDb).toBeNull(); expect(sent("SET_TOTAL_IN_DB")).toEqual([]); expect(state.error).toBe("count failed"); // Unknown count: the regular layout, never the "import first" card. expect(showsEmptyState(state)).toBe(false); }); it("failed categories still load the sources and the count", async () => { services.getAllCategories.mockRejectedValue(new Error("categories failed")); services.getTransactionCount.mockResolvedValue(12); const state = await mount(); expect(state.categories).toEqual([]); expect(state.sources).toBe(SOURCES); expect(state.totalInDb).toBe(12); expect(state.error).toBe("categories failed"); }); it("several failures: the first one, in the order categories, sources, count, is the error", async () => { services.getAllImportSources.mockRejectedValue("sources failed"); services.getTransactionCount.mockRejectedValue(new Error("count failed")); const state = await mount(); expect(state.categories).toBe(CATEGORIES); expect(state.sources).toEqual([]); expect(state.totalInDb).toBeNull(); // A rejection that is not an Error is shown as its text. expect(state.error).toBe("sources failed"); expect(sent("SET_ERROR").filter((a) => a.payload !== null)).toHaveLength(1); }); });