diff --git a/src/hooks/useTransactions.test.ts b/src/hooks/useTransactions.test.ts index 312bad4..8f6df11 100644 --- a/src/hooks/useTransactions.test.ts +++ b/src/hooks/useTransactions.test.ts @@ -16,6 +16,7 @@ import { initTransactionsState, showsEmptyState, GUIDED_ENTRY_FILTERS, + type TransactionsAction, type TransactionsState, } from "./useTransactions"; import type { TransactionPageResult } from "../shared/types"; @@ -170,6 +171,32 @@ describe("transactionsReducer — the guided entry's fallback (#399)", () => { expect(after.guidedFallback).toBe(false); }); + // The actions the hook really sends around the first page: `fetchData` sends + // SET_LOADING then SET_ERROR(null) before each read, and the mount effect's + // categories, sources and count land before or after that page. None of + // them may disarm the fallback (review of #399). + it.each([ + ["the mount reads land first", ["mount", "fetch", "page"]], + ["the first page lands first", ["fetch", "page", "mount"]], + ] as const)("the hook's real sequence still falls back (%s)", (_name, order) => { + const sent: Record<(typeof order)[number], TransactionsAction[]> = { + fetch: [ + { type: "SET_LOADING", payload: true }, + { type: "SET_ERROR", payload: null }, + ], + mount: [ + { type: "SET_CATEGORIES", payload: [] }, + { type: "SET_SOURCES", payload: [] }, + { type: "SET_TOTAL_IN_DB", payload: 12 }, + ], + page: [{ type: "SET_PAGE_RESULT", payload: page(0) }], + }; + const after = order.flatMap((group) => sent[group]).reduce(transactionsReducer, guided()); + expect(after.filters.uncategorizedOnly).toBe(false); + expect(after.filters.dateFrom).toBeNull(); + expect(after.isLoading).toBe(true); + }); + it("falls back once: an empty page after the fallback keeps the filters, so nothing reads again", () => { const fellBack = result(guided(), 0); const again = result(fellBack, 0); diff --git a/src/pages/TransactionsPage.render.test.tsx b/src/pages/TransactionsPage.render.test.tsx index 1ad3fde..04ba739 100644 --- a/src/pages/TransactionsPage.render.test.tsx +++ b/src/pages/TransactionsPage.render.test.tsx @@ -8,7 +8,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import { renderToStaticMarkup } from "react-dom/server"; import { MemoryRouter } from "react-router-dom"; -import type { TransactionRow } from "../shared/types"; +import type { TransactionFilters, TransactionRow } from "../shared/types"; // The page's services pull the database: stub the connection. vi.mock("../services/db", () => ({ getDb: vi.fn(), withTransaction: vi.fn() })); @@ -21,7 +21,12 @@ vi.mock("react-i18next", () => ({ })); vi.mock("../components/shared/PageHelp", () => ({ PageHelp: () => null })); -const harness = vi.hoisted(() => ({ onboarding: null as unknown, rows: [] as TransactionRow[] })); +const harness = vi.hoisted(() => ({ + onboarding: null as unknown, + rows: [] as TransactionRow[], + /** Filters a later dispatch would have set (the fallback, a filter picked by hand). */ + filters: null as Partial | null, +})); vi.mock("../contexts/OnboardingContext", () => ({ useOnboarding: () => harness.onboarding })); vi.mock("../hooks/useTransactions", async (importOriginal) => { @@ -30,7 +35,8 @@ vi.mock("../hooks/useTransactions", async (importOriginal) => { ...actual, useTransactions: (...args: Parameters) => { const hook = actual.useTransactions(...args); - return { ...hook, state: { ...hook.state, rows: harness.rows } }; + const filters = harness.filters ? { ...hook.state.filters, ...harness.filters } : hook.state.filters; + return { ...hook, state: { ...hook.state, rows: harness.rows, filters } }; }, }; }); @@ -122,6 +128,7 @@ const activePeriod = (html: string) => beforeEach(() => { harness.onboarding = path(IMPORTED); harness.rows = []; + harness.filters = null; }); describe("TransactionsPage — the guided entry (#399)", () => { @@ -165,6 +172,19 @@ describe("TransactionsPage — the step card (#399)", () => { expect(sentence(render())).toBe("t(onboarding.card.transactions.all[])"); }); + // The sentence follows the list's filter, not the navigation state. + it("the guided entry after its fallback: the sentence for every transaction", () => { + harness.filters = { uncategorizedOnly: false }; + expect(sentence(render(GUIDED))).toBe("t(onboarding.card.transactions.all[])"); + }); + + it("an ordinary visit with 'Uncategorized' picked by hand: the uncategorized sentence", () => { + harness.filters = { uncategorizedOnly: true }; + expect(sentence(render())).toBe( + "t(onboarding.card.transactions.uncategorized[filter=t(transactions.filters.uncategorized)])", + ); + }); + it("the first row's category is the one target", () => { harness.rows = [row(1, "IGA 123"), row(2, "METRO 456")]; const html = render(GUIDED);