Each line of the adjustment form chose its category in a native select showing raw names. It now takes the compact CategoryCombobox of the split window: typing filters, hierarchical order, seed names translated. A new line still takes the first category of the list. D2: the hook reads the Transactions list, getAllCategories (active and open to input), instead of its own query on every active category. The list feeds two pickers only, the form and the split window of the Adjustments page; the detail view names a line's category from the entries' join, so a line saved on a category hidden from input keeps its name there and its value, its field empty until changed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
85 lines
3 KiB
TypeScript
85 lines
3 KiB
TypeScript
// useAdjustments — the categories an adjustment line can pick (#429, D2).
|
|
//
|
|
// 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 mount effect. The services are mocked; the base answers a raw
|
|
// query with every active category, so a hook that read the base itself,
|
|
// instead of the Transactions list, would show it.
|
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
|
|
vi.mock("react", async (orig) => (await import("../test/hookHarness")).fakeReact(await orig()));
|
|
const { getAllCategories, getAllAdjustments, select } = vi.hoisted(() => ({
|
|
getAllCategories: vi.fn(),
|
|
getAllAdjustments: vi.fn(),
|
|
select: vi.fn(),
|
|
}));
|
|
vi.mock("../services/transactionService", () => ({ getAllCategories }));
|
|
vi.mock("../services/adjustmentService", () => ({
|
|
getAllAdjustments,
|
|
getEntriesByAdjustmentId: vi.fn(),
|
|
createAdjustment: vi.fn(),
|
|
updateAdjustment: vi.fn(),
|
|
deleteAdjustment: vi.fn(),
|
|
createEntry: vi.fn(),
|
|
updateEntry: vi.fn(),
|
|
deleteEntry: vi.fn(),
|
|
}));
|
|
vi.mock("../services/db", () => ({ getDb: vi.fn(async () => ({ select })) }));
|
|
|
|
import { hookHarness } from "../test/hookHarness";
|
|
import { useAdjustments } from "./useAdjustments";
|
|
import type { Category } from "../shared/types";
|
|
|
|
const harness = hookHarness();
|
|
|
|
const category = (id: number, name: string, isInputable: boolean): Category => ({
|
|
id,
|
|
name,
|
|
type: "expense",
|
|
is_active: true,
|
|
is_inputable: isInputable,
|
|
sort_order: id,
|
|
created_at: "",
|
|
});
|
|
|
|
/** The Transactions list: active and open to input. */
|
|
const INPUTABLE = [category(1, "Épicerie", true), category(2, "Loyer", true)];
|
|
/** Every active category, a parent hidden from input included. */
|
|
const ACTIVE = [category(3, "Alimentation", false), ...INPUTABLE];
|
|
|
|
/** Lets the loaders' promises settle. */
|
|
const flush = async () => {
|
|
for (let i = 0; i < 10; i++) await Promise.resolve();
|
|
};
|
|
|
|
beforeEach(() => {
|
|
harness.clear();
|
|
getAllCategories.mockReset();
|
|
getAllCategories.mockResolvedValue(INPUTABLE);
|
|
getAllAdjustments.mockReset();
|
|
getAllAdjustments.mockResolvedValue([]);
|
|
select.mockReset();
|
|
select.mockResolvedValue(ACTIVE);
|
|
});
|
|
|
|
describe("useAdjustments — categories of a line (#429, D2)", () => {
|
|
it("offers the Transactions list, active and open to input, not every active category", async () => {
|
|
harness.render(useAdjustments);
|
|
harness.commit();
|
|
await flush();
|
|
const { state } = harness.render(useAdjustments);
|
|
expect(state.categories).toBe(INPUTABLE);
|
|
expect(getAllCategories).toHaveBeenCalledTimes(1);
|
|
expect(select).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("reports a failed read as the page's error, with no category", async () => {
|
|
getAllCategories.mockRejectedValue(new Error("database is locked"));
|
|
harness.render(useAdjustments);
|
|
harness.commit();
|
|
await flush();
|
|
const { state } = harness.render(useAdjustments);
|
|
expect(state.categories).toEqual([]);
|
|
expect(state.error).toBe("database is locked");
|
|
});
|
|
});
|