The budget grid and the Budget vs Actual report read active categories only and started from the categories without a parent, so a category whose parent was deactivated or no longer exists vanished from both, budget amounts included. It now stands at the top level, as in the tree and the category pickers (displaysAsRoot). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
451 lines
20 KiB
TypeScript
451 lines
20 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import type { BudgetYearRow, Category, BudgetEntry } from "../shared/types";
|
|
import { buildBudgetYearRows } from "./useBudget";
|
|
import { sumLeavesForType } from "../components/budget/budgetTableResults";
|
|
import { reorderRows } from "../utils/reorderRows";
|
|
import {
|
|
type CollapseAccessors,
|
|
visibleRows,
|
|
isCollapsedFor,
|
|
} from "../utils/collapsibleRows";
|
|
|
|
// The Budget grid's rows are built by `useBudget` (the "3rd builder" the
|
|
// abandoned v1 collapse plan ignored). Since issue #424 they are emitted
|
|
// DEPTH-FIRST: each parent row is followed directly by its whole subtree, so an
|
|
// expanded subcategory shows under its parent and `reorderRows` (subtotals at
|
|
// the bottom) attaches each subtotal to its own rows. The collapse (ancestor
|
|
// walk, issue #288) does not depend on the order.
|
|
|
|
function cat(
|
|
id: number,
|
|
name: string,
|
|
type: Category["type"],
|
|
opts: { parent_id?: number; is_inputable?: boolean; sort_order?: number } = {},
|
|
): Category {
|
|
return {
|
|
id,
|
|
name,
|
|
type,
|
|
parent_id: opts.parent_id,
|
|
color: "#000",
|
|
is_active: true,
|
|
is_inputable: opts.is_inputable ?? true,
|
|
sort_order: opts.sort_order ?? 0,
|
|
created_at: "",
|
|
};
|
|
}
|
|
|
|
// income leaf + a 3-level expense group:
|
|
// Housing (root, non-inputable)
|
|
// ├─ Rent (depth-1 leaf)
|
|
// └─ Utilities (depth-1 intermediate parent)
|
|
// ├─ Hydro (depth-2 leaf)
|
|
// └─ Internet (depth-2 leaf)
|
|
const CATEGORIES: Category[] = [
|
|
cat(10, "Salary", "income", { sort_order: 0 }),
|
|
cat(1, "Housing", "expense", { is_inputable: false, sort_order: 1 }),
|
|
cat(2, "Rent", "expense", { parent_id: 1, sort_order: 0 }),
|
|
cat(3, "Utilities", "expense", { parent_id: 1, is_inputable: false, sort_order: 1 }),
|
|
cat(4, "Hydro", "expense", { parent_id: 3, sort_order: 0 }),
|
|
cat(5, "Internet", "expense", { parent_id: 3, sort_order: 1 }),
|
|
];
|
|
|
|
function entry(category_id: number, amount: number, month = 1): BudgetEntry {
|
|
return { id: 0, category_id, year: 2026, month, amount, created_at: "", updated_at: "" };
|
|
}
|
|
|
|
// Same accessors BudgetTable declares inline — duplicated here (they are 4 trivial
|
|
// lambdas the issue mandates stay inline; a .tsx import into a node-env test would
|
|
// pull in JSX/lucide/react-i18next needlessly).
|
|
const ACCESSORS: CollapseAccessors<BudgetYearRow> = {
|
|
keyOf: (row) => `p:${row.category_id}`,
|
|
parentKeyOf: (row) => (row.parent_id != null ? `p:${row.parent_id}` : null),
|
|
depthOf: (row) => row.depth ?? 0,
|
|
isParent: (row) => row.is_parent,
|
|
};
|
|
|
|
/**
|
|
* Depth-first invariant: every non-root row comes after its parent row, and
|
|
* every row between a parent row and the end of its subtree is one of its
|
|
* descendants. A "(direct)" leaf carries its own category as parent: it is
|
|
* attached to the subtotal row of that category (the `p:` key of the grid).
|
|
*/
|
|
function expectDepthFirst(rows: BudgetYearRow[]) {
|
|
const parentIndex = new Map<number, number>();
|
|
rows.forEach((r, i) => {
|
|
if (r.is_parent) parentIndex.set(r.category_id, i);
|
|
});
|
|
// Ancestor ids of a row, climbing the parent subtotal rows.
|
|
const ancestorsOf = (r: BudgetYearRow): number[] => {
|
|
const out: number[] = [];
|
|
let pid = r.parent_id;
|
|
while (pid != null && !out.includes(pid)) {
|
|
out.push(pid);
|
|
const idx = parentIndex.get(pid);
|
|
if (idx === undefined) break;
|
|
pid = rows[idx].parent_id;
|
|
}
|
|
return out;
|
|
};
|
|
rows.forEach((r, i) => {
|
|
if (r.parent_id != null) {
|
|
const p = parentIndex.get(r.parent_id);
|
|
expect(p, `${r.category_name} has a parent row`).toBeDefined();
|
|
expect(p!, `${r.category_name} comes after its parent`).toBeLessThan(i);
|
|
}
|
|
});
|
|
rows.forEach((r, i) => {
|
|
if (!r.is_parent) return;
|
|
const depth = r.depth ?? 0;
|
|
// The subtree runs until the next row of this depth or shallower.
|
|
let j = i + 1;
|
|
while (j < rows.length && (rows[j].depth ?? 0) > depth) {
|
|
expect(ancestorsOf(rows[j]), `${rows[j].category_name} is under ${r.category_name}`).toContain(r.category_id);
|
|
j++;
|
|
}
|
|
// ...and every descendant of the parent lies inside that range.
|
|
rows.forEach((d, k) => {
|
|
if (k === i) return;
|
|
if (ancestorsOf(d).includes(r.category_id)) {
|
|
expect(k, `${d.category_name} lies inside the subtree of ${r.category_name}`).toBeGreaterThan(i);
|
|
expect(k, `${d.category_name} lies inside the subtree of ${r.category_name}`).toBeLessThan(j);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
const names = (rows: BudgetYearRow[]) => rows.map((r) => r.category_name);
|
|
|
|
// Shape of the "Revenus" group of a new profile (consolidated_schema.sql): one
|
|
// depth-1 leaf ("Autres revenus") and three intermediate parents.
|
|
const REVENUS: Category[] = [
|
|
cat(1000, "Revenus", "income", { is_inputable: false, sort_order: 1 }),
|
|
cat(1010, "Emploi", "income", { parent_id: 1000, is_inputable: false, sort_order: 1 }),
|
|
cat(1011, "Paie régulière", "income", { parent_id: 1010, sort_order: 1 }),
|
|
cat(1012, "Primes & bonus", "income", { parent_id: 1010, sort_order: 2 }),
|
|
cat(1013, "Travail autonome", "income", { parent_id: 1010, sort_order: 3 }),
|
|
cat(1020, "Gouvernemental", "income", { parent_id: 1000, is_inputable: false, sort_order: 2 }),
|
|
cat(1021, "Remboursement impôt", "income", { parent_id: 1020, sort_order: 1 }),
|
|
cat(1022, "Allocations familiales", "income", { parent_id: 1020, sort_order: 2 }),
|
|
cat(1023, "Crédits TPS/TVQ", "income", { parent_id: 1020, sort_order: 3 }),
|
|
cat(1024, "Assurance-emploi / RQAP", "income", { parent_id: 1020, sort_order: 4 }),
|
|
cat(1030, "Revenus de placement", "income", { parent_id: 1000, is_inputable: false, sort_order: 3 }),
|
|
cat(1031, "Intérêts & dividendes", "income", { parent_id: 1030, sort_order: 1 }),
|
|
cat(1032, "Gains en capital", "income", { parent_id: 1030, sort_order: 2 }),
|
|
cat(1033, "Revenus locatifs", "income", { parent_id: 1030, sort_order: 3 }),
|
|
cat(1090, "Autres revenus", "income", { parent_id: 1000, sort_order: 9 }),
|
|
];
|
|
|
|
// Shape of "Finances & obligations" of a new profile: leaves before AND after
|
|
// intermediate parents ("Retrait cash" sorts after "Impôts & taxes").
|
|
const FINANCES: Category[] = [
|
|
cat(1900, "Finances & obligations", "expense", { is_inputable: false, sort_order: 10 }),
|
|
cat(1910, "Frais bancaires", "expense", { parent_id: 1900, is_inputable: false, sort_order: 1 }),
|
|
cat(1911, "Frais de compte", "expense", { parent_id: 1910, sort_order: 1 }),
|
|
cat(1912, "Intérêts & frais de crédit", "expense", { parent_id: 1910, sort_order: 2 }),
|
|
cat(1913, "Frais de change", "expense", { parent_id: 1910, sort_order: 3 }),
|
|
cat(1920, "Impôts & taxes", "expense", { parent_id: 1900, is_inputable: false, sort_order: 2 }),
|
|
cat(1921, "Impôt fédéral", "expense", { parent_id: 1920, sort_order: 1 }),
|
|
cat(1922, "Impôt provincial", "expense", { parent_id: 1920, sort_order: 2 }),
|
|
cat(1923, "Acomptes provisionnels", "expense", { parent_id: 1920, sort_order: 3 }),
|
|
cat(1930, "Dons & cotisations", "expense", { parent_id: 1900, is_inputable: false, sort_order: 3 }),
|
|
cat(1931, "Dons de charité", "expense", { parent_id: 1930, sort_order: 1 }),
|
|
cat(1932, "Cotisations professionnelles", "expense", { parent_id: 1930, sort_order: 2 }),
|
|
cat(1933, "Cotisations syndicales", "expense", { parent_id: 1930, sort_order: 3 }),
|
|
cat(1940, "Cadeaux", "expense", { parent_id: 1900, sort_order: 4 }),
|
|
cat(1945, "Retrait cash", "expense", { parent_id: 1900, sort_order: 5 }),
|
|
cat(1946, "Achats divers non catégorisés", "expense", { parent_id: 1900, sort_order: 6 }),
|
|
];
|
|
|
|
// Shape of the older taxonomy (default profile): an inputable root, so a
|
|
// "(direct)" row, and one intermediate parent (Assurances, migration v7).
|
|
const RECURRENTES: Category[] = [
|
|
cat(2, "Dépenses récurrentes", "expense", { sort_order: 2 }),
|
|
cat(20, "Loyer", "expense", { parent_id: 2, sort_order: 1 }),
|
|
cat(21, "Électricité", "expense", { parent_id: 2, sort_order: 2 }),
|
|
cat(31, "Assurances", "expense", { parent_id: 2, is_inputable: false, sort_order: 12 }),
|
|
cat(310, "Assurance-auto", "expense", { parent_id: 31, sort_order: 1 }),
|
|
cat(311, "Assurance-habitation", "expense", { parent_id: 31, sort_order: 2 }),
|
|
cat(312, "Assurance-vie", "expense", { parent_id: 31, sort_order: 3 }),
|
|
cat(32, "Pharmacie", "expense", { parent_id: 2, sort_order: 13 }),
|
|
cat(33, "Taxes municipales", "expense", { parent_id: 2, sort_order: 14 }),
|
|
];
|
|
|
|
describe("buildBudgetYearRows — depth-first emission (issue #424)", () => {
|
|
it("emits income before expense, each group depth-first: Utilities is followed by its own leaves", () => {
|
|
const rows = buildBudgetYearRows(CATEGORIES, [], []);
|
|
// Salary (income) first; then Housing, its children sorted by name with
|
|
// leaves and sub-groups mixed: Rent, then Utilities and its leaves.
|
|
expect(rows.map((r) => r.category_id)).toEqual([10, 1, 2, 3, 4, 5]);
|
|
expectDepthFirst(rows);
|
|
});
|
|
|
|
it("Revenus: the depth-1 leaf sorts first by name, each sub-group carries its own leaves", () => {
|
|
const rows = buildBudgetYearRows(REVENUS, [], []);
|
|
expect(names(rows)).toEqual([
|
|
"Revenus",
|
|
"Autres revenus",
|
|
"Emploi",
|
|
"Paie régulière",
|
|
"Primes & bonus",
|
|
"Travail autonome",
|
|
"Gouvernemental",
|
|
"Allocations familiales",
|
|
"Assurance-emploi / RQAP",
|
|
"Crédits TPS/TVQ",
|
|
"Remboursement impôt",
|
|
"Revenus de placement",
|
|
"Gains en capital",
|
|
"Intérêts & dividendes",
|
|
"Revenus locatifs",
|
|
]);
|
|
expectDepthFirst(rows);
|
|
});
|
|
|
|
it("Finances & obligations: leaves before and after sub-groups, sorted by name together", () => {
|
|
const rows = buildBudgetYearRows(FINANCES, [], []);
|
|
expect(names(rows)).toEqual([
|
|
"Finances & obligations",
|
|
"Achats divers non catégorisés",
|
|
"Cadeaux",
|
|
"Dons & cotisations",
|
|
"Cotisations professionnelles",
|
|
"Cotisations syndicales",
|
|
"Dons de charité",
|
|
"Frais bancaires",
|
|
"Frais de change",
|
|
"Frais de compte",
|
|
"Intérêts & frais de crédit",
|
|
"Impôts & taxes",
|
|
"Acomptes provisionnels",
|
|
"Impôt fédéral",
|
|
"Impôt provincial",
|
|
"Retrait cash",
|
|
]);
|
|
expectDepthFirst(rows);
|
|
});
|
|
|
|
it("older taxonomy: the (direct) row leads, Assurances is followed by its three leaves", () => {
|
|
const rows = buildBudgetYearRows(RECURRENTES, [], []);
|
|
expect(names(rows)).toEqual([
|
|
"Dépenses récurrentes",
|
|
"Dépenses récurrentes (direct)",
|
|
"Assurances",
|
|
"Assurance-auto",
|
|
"Assurance-habitation",
|
|
"Assurance-vie",
|
|
"Électricité",
|
|
"Loyer",
|
|
"Pharmacie",
|
|
"Taxes municipales",
|
|
]);
|
|
expectDepthFirst(rows);
|
|
});
|
|
|
|
it("top groups are sorted by type, then sort_order, then name — whatever the input order", () => {
|
|
const all = [
|
|
...FINANCES,
|
|
...RECURRENTES,
|
|
...REVENUS,
|
|
cat(9000, "Divers", "expense", { sort_order: 2 }),
|
|
cat(9100, "Virements", "transfer", { sort_order: 0 }),
|
|
];
|
|
const rows = buildBudgetYearRows([...all].reverse(), [], []);
|
|
const roots = rows.filter((r) => (r.depth ?? 0) === 0).map((r) => r.category_name);
|
|
expect(roots).toEqual([
|
|
"Revenus", // income
|
|
"Dépenses récurrentes", // expense, sort_order 2, name before "Divers"
|
|
"Divers", // expense, sort_order 2 (lone leaf)
|
|
"Finances & obligations", // expense, sort_order 10
|
|
"Virements", // transfer, last despite sort_order 0
|
|
]);
|
|
expectDepthFirst(rows);
|
|
});
|
|
|
|
it("subtotals at the bottom: each subtotal follows its own rows (reorderRows on the builder output)", () => {
|
|
const rows = reorderRows(buildBudgetYearRows([...FINANCES, ...RECURRENTES], [], []), false);
|
|
expect(names(rows)).toEqual([
|
|
"Dépenses récurrentes (direct)",
|
|
"Assurance-auto",
|
|
"Assurance-habitation",
|
|
"Assurance-vie",
|
|
"Assurances",
|
|
"Électricité",
|
|
"Loyer",
|
|
"Pharmacie",
|
|
"Taxes municipales",
|
|
"Dépenses récurrentes",
|
|
"Achats divers non catégorisés",
|
|
"Cadeaux",
|
|
"Cotisations professionnelles",
|
|
"Cotisations syndicales",
|
|
"Dons de charité",
|
|
"Dons & cotisations",
|
|
"Frais de change",
|
|
"Frais de compte",
|
|
"Intérêts & frais de crédit",
|
|
"Frais bancaires",
|
|
"Acomptes provisionnels",
|
|
"Impôt fédéral",
|
|
"Impôt provincial",
|
|
"Impôts & taxes",
|
|
"Retrait cash",
|
|
"Finances & obligations",
|
|
]);
|
|
});
|
|
|
|
it("expanding a single intermediate parent shows its leaves right under it", () => {
|
|
const rows = buildBudgetYearRows(REVENUS, [], []);
|
|
const flipped = new Set<string>(["p:1000", "p:1010"]); // Revenus + Emploi expanded
|
|
const isCollapsed = (r: BudgetYearRow) => isCollapsedFor(flipped, ACCESSORS.keyOf(r), false);
|
|
const visible = visibleRows(rows, ACCESSORS, isCollapsed);
|
|
expect(names(visible)).toEqual([
|
|
"Revenus",
|
|
"Autres revenus",
|
|
"Emploi",
|
|
"Paie régulière",
|
|
"Primes & bonus",
|
|
"Travail autonome",
|
|
"Gouvernemental",
|
|
"Revenus de placement",
|
|
]);
|
|
});
|
|
|
|
it("subtotals are unchanged by the order: each subtotal sums its own leaves", () => {
|
|
const ENTRIES = [entry(1011, 100), entry(1012, 20), entry(1021, 7), entry(1033, 3), entry(1090, 1000)];
|
|
const rows = buildBudgetYearRows(REVENUS, ENTRIES, []);
|
|
const annualOf = (id: number) => rows.find((r) => r.category_id === id && r.is_parent)!.annual;
|
|
expect(annualOf(1000)).toBe(1130);
|
|
expect(annualOf(1010)).toBe(120);
|
|
expect(annualOf(1020)).toBe(7);
|
|
expect(annualOf(1030)).toBe(3);
|
|
});
|
|
});
|
|
|
|
describe("buildBudgetYearRows — a category whose parent is deleted or gone (#393)", () => {
|
|
// The budget reads active categories only: a deleted (inactive) parent is
|
|
// simply absent from the list, like a parent that no longer exists.
|
|
it("shows a leaf whose parent is absent as a root, with its budget amounts", () => {
|
|
const rows = buildBudgetYearRows(
|
|
[...CATEGORIES, cat(7, "Gifts", "expense", { parent_id: 99, sort_order: 5 })],
|
|
[entry(7, 40, 1), entry(7, 60, 2)],
|
|
[{ category_id: 7, actual: -80 }],
|
|
);
|
|
const gifts = rows.find((r) => r.category_id === 7);
|
|
expect(gifts).toMatchObject({ parent_id: null, depth: 0, is_parent: false, annual: 100, previousYearTotal: -80 });
|
|
expectDepthFirst(rows);
|
|
});
|
|
|
|
it("shows a parent whose own parent is absent as a root group, its children under it", () => {
|
|
const rows = buildBudgetYearRows(
|
|
[
|
|
...CATEGORIES,
|
|
cat(7, "Gifts", "expense", { parent_id: 99, is_inputable: false, sort_order: 5 }),
|
|
cat(8, "Flowers", "expense", { parent_id: 7, sort_order: 1 }),
|
|
],
|
|
[entry(8, 25)],
|
|
[],
|
|
);
|
|
expect(rows.find((r) => r.category_id === 7)).toMatchObject({ parent_id: null, depth: 0, is_parent: true, annual: 25 });
|
|
expect(rows.find((r) => r.category_id === 8)).toMatchObject({ parent_id: 7, depth: 1, annual: 25 });
|
|
expectDepthFirst(rows);
|
|
});
|
|
|
|
it("still hides the members of a parent_id cycle, which « Vérifier la hiérarchie » repairs", () => {
|
|
const rows = buildBudgetYearRows(
|
|
[...CATEGORIES, cat(7, "Gifts", "expense", { parent_id: 8 }), cat(8, "Flowers", "expense", { parent_id: 7 })],
|
|
[],
|
|
[],
|
|
);
|
|
expect(rows.some((r) => r.category_id === 7 || r.category_id === 8)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("buildBudgetYearRows + multi-level collapse (issue #289 end-to-end)", () => {
|
|
const rows = buildBudgetYearRows(CATEGORIES, [], []);
|
|
|
|
it("collapsed-by-default (empty set) shows only the roots", () => {
|
|
const isCollapsed = (r: BudgetYearRow) => isCollapsedFor(new Set<string>(), ACCESSORS.keyOf(r), false);
|
|
const visible = visibleRows(rows, ACCESSORS, isCollapsed);
|
|
// Salary (root leaf) + Housing (root subtotal); nothing beneath Housing.
|
|
expect(visible.map((r) => r.category_id)).toEqual([10, 1]);
|
|
});
|
|
|
|
it("expanding a root reveals its DIRECT children only — grandchildren stay folded under the still-collapsed intermediate", () => {
|
|
const flipped = new Set<string>(["p:1"]); // Housing expanded
|
|
const isCollapsed = (r: BudgetYearRow) => isCollapsedFor(flipped, ACCESSORS.keyOf(r), false);
|
|
const visible = visibleRows(rows, ACCESSORS, isCollapsed);
|
|
// Housing's direct children, by name: Rent leaf + Utilities subtotal.
|
|
// Hydro/Internet remain hidden under the collapsed Utilities.
|
|
expect(visible.map((r) => r.category_id)).toEqual([10, 1, 2, 3]);
|
|
});
|
|
});
|
|
|
|
describe("BudgetTable section subtotals stay on RAW rows (collapse is purely visual, issue #289)", () => {
|
|
const ENTRIES = [entry(2, 1000), entry(4, 100), entry(5, 50)];
|
|
const rows = buildBudgetYearRows(CATEGORIES, ENTRIES, []);
|
|
|
|
it("sums every expense LEAF regardless of collapse — folding a parent never moves a total", () => {
|
|
const rawExpense = sumLeavesForType(rows, "expense");
|
|
expect(rawExpense.annual).toBe(-1150); // -(Rent 1000 + Hydro 100 + Internet 50)
|
|
|
|
// If the grid mistakenly summed the collapse-visible rows, a fully-collapsed
|
|
// grid (only roots visible, and the expense root is a parent → excluded)
|
|
// would report 0. The grid feeds the RAW group to `sumLeavesForType`, so the
|
|
// section total is invariant under collapse; this asserts the two differ,
|
|
// which is exactly why raw rows must be used.
|
|
const isCollapsed = (r: BudgetYearRow) => isCollapsedFor(new Set<string>(), ACCESSORS.keyOf(r), false);
|
|
const collapsedVisible = visibleRows(rows, ACCESSORS, isCollapsed);
|
|
const visibleExpense = sumLeavesForType(collapsedVisible, "expense");
|
|
expect(visibleExpense.annual).toBe(0);
|
|
expect(rawExpense.annual).not.toBe(visibleExpense.annual);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Issue #353 — the profile-wide transaction count (`totalInDb`) drives the
|
|
// "import first" empty state of the Budget page. The exported reducer is
|
|
// exercised directly (no renderHook harness in this repo).
|
|
|
|
import { budgetReducer, initialBudgetState, showsEmptyState } from "./useBudget";
|
|
|
|
describe("budgetReducer — SET_TOTAL_IN_DB (issue #353)", () => {
|
|
it("starts unknown: totalInDb is null, which keeps the grid", () => {
|
|
const state = initialBudgetState();
|
|
expect(state.totalInDb).toBeNull();
|
|
expect(showsEmptyState(state)).toBe(false);
|
|
});
|
|
|
|
it("stores the count without touching year, rows, templates or accounts", () => {
|
|
const before = budgetReducer(
|
|
budgetReducer(initialBudgetState(), { type: "SET_YEAR", payload: 2024 }),
|
|
{ type: "SET_DATA", payload: { rows: buildBudgetYearRows(CATEGORIES, [entry(2, 1000)], []), templates: [] } },
|
|
);
|
|
const after = budgetReducer(before, { type: "SET_TOTAL_IN_DB", payload: 0 });
|
|
|
|
expect(after.totalInDb).toBe(0);
|
|
expect(after.year).toBe(2024);
|
|
expect(after.rows).toBe(before.rows);
|
|
expect(after.templates).toBe(before.templates);
|
|
expect(after.accounts).toBe(before.accounts);
|
|
expect({ ...after, totalInDb: before.totalInDb }).toEqual(before);
|
|
});
|
|
|
|
it("0 shows the empty state, then N > 0 renders the grid again", () => {
|
|
const empty = budgetReducer(initialBudgetState(), { type: "SET_TOTAL_IN_DB", payload: 0 });
|
|
expect(showsEmptyState(empty)).toBe(true);
|
|
|
|
const filled = budgetReducer(empty, { type: "SET_TOTAL_IN_DB", payload: 12 });
|
|
expect(filled.totalInDb).toBe(12);
|
|
expect(showsEmptyState(filled)).toBe(false);
|
|
});
|
|
|
|
it("the other actions preserve totalInDb", () => {
|
|
const known = budgetReducer(initialBudgetState(), { type: "SET_TOTAL_IN_DB", payload: 3 });
|
|
const after = budgetReducer(
|
|
budgetReducer(known, { type: "SET_YEAR", payload: 2025 }),
|
|
{ type: "SET_ERROR", payload: "boom" },
|
|
);
|
|
expect(after.totalInDb).toBe(3);
|
|
});
|
|
});
|