Address the two non-blocking review notes on PR #252: - The re-injected non-leaf parent (e.g. #1710) is technically clickable, not 'never offered'; correct the MappingRow comment to state selecting it is a no-op (only leaves resolve). - Extract the per-row option-building glue into a pure, unit-tested helper comboboxCategoriesForTarget (4 cases: null/leaf pass-through by reference, non-leaf append, unresolvable stale id).
177 lines
5.8 KiB
TypeScript
177 lines
5.8 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import {
|
|
taxonomyToComboboxCategories,
|
|
findTaxonomyCategory,
|
|
comboboxCategoriesForTarget,
|
|
} from "./migrationTargets";
|
|
import type { Category } from "../../shared/types";
|
|
import {
|
|
getTaxonomyV1,
|
|
type TaxonomyNode,
|
|
} from "../../services/categoryTaxonomyService";
|
|
|
|
function node(
|
|
id: number,
|
|
name: string,
|
|
children: TaxonomyNode[] = [],
|
|
type: "expense" | "income" | "transfer" = "expense",
|
|
): TaxonomyNode {
|
|
return {
|
|
id,
|
|
name,
|
|
i18n_key: `categoriesSeed.test.${id}`,
|
|
type,
|
|
color: "#123456",
|
|
sort_order: 1,
|
|
children,
|
|
};
|
|
}
|
|
|
|
describe("taxonomyToComboboxCategories (leaves only)", () => {
|
|
it("returns [] for empty input", () => {
|
|
expect(taxonomyToComboboxCategories([])).toEqual([]);
|
|
});
|
|
|
|
it("keeps only leaves, dropping every intermediate parent, in DFS order", () => {
|
|
const roots = [
|
|
node(1000, "Revenus", [
|
|
node(1010, "Emploi", [node(1011, "Paie"), node(1012, "Prime")]),
|
|
]),
|
|
node(1100, "Alimentation", [node(1111, "Épicerie")]),
|
|
];
|
|
const out = taxonomyToComboboxCategories(roots);
|
|
// parents 1000/1010/1100 dropped; leaves kept in depth-first reading order
|
|
expect(out.map((c) => c.id)).toEqual([1011, 1012, 1111]);
|
|
});
|
|
|
|
it("flattens leaves: parent_id undefined + running sort_order (flat list)", () => {
|
|
const roots = [
|
|
node(1000, "Revenus", [
|
|
node(1010, "Emploi", [node(1011, "Paie"), node(1012, "Prime")]),
|
|
]),
|
|
node(1100, "Alimentation", [node(1111, "Épicerie")]),
|
|
];
|
|
const out = taxonomyToComboboxCategories(roots);
|
|
expect(out.every((c) => c.parent_id === undefined)).toBe(true);
|
|
expect(out.map((c) => c.sort_order)).toEqual([0, 1, 2]);
|
|
});
|
|
|
|
it("marks every returned row inputable (all leaves)", () => {
|
|
const roots = [
|
|
node(1700, "Loisirs", [
|
|
node(1710, "Divertissement", [node(1711, "Cinéma")]),
|
|
]),
|
|
];
|
|
const out = taxonomyToComboboxCategories(roots);
|
|
expect(out.map((c) => c.id)).toEqual([1711]); // 1700 + 1710 parents dropped
|
|
expect(out.every((c) => c.is_inputable)).toBe(true);
|
|
});
|
|
|
|
it("copies id/name/type/color/i18n_key and defaults is_active + created_at", () => {
|
|
const [c] = taxonomyToComboboxCategories([
|
|
node(1111, "Épicerie", [], "expense"),
|
|
]);
|
|
expect(c).toMatchObject({
|
|
id: 1111,
|
|
name: "Épicerie",
|
|
type: "expense",
|
|
color: "#123456",
|
|
i18n_key: "categoriesSeed.test.1111",
|
|
is_active: true,
|
|
is_inputable: true,
|
|
});
|
|
expect(typeof c.created_at).toBe("string");
|
|
});
|
|
|
|
it("adapts the real v1 taxonomy: 112 leaves, ids unique, NO non-leaf parent offered", () => {
|
|
const out = taxonomyToComboboxCategories(getTaxonomyV1().roots);
|
|
expect(out.length).toBe(112); // leaves only (was 150 incl. 38 parents)
|
|
const ids = new Set(out.map((c) => c.id));
|
|
expect(ids.size).toBe(out.length);
|
|
// The non-leaf parent 1710 "Divertissement" must NOT be a selectable option.
|
|
expect(out.find((c) => c.id === 1710)).toBeUndefined();
|
|
// A known leaf is present and inputable.
|
|
expect(out.find((c) => c.id === 1111)!.is_inputable).toBe(true);
|
|
expect(out.every((c) => c.is_inputable)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("findTaxonomyCategory", () => {
|
|
const roots = [
|
|
node(1700, "Loisirs", [
|
|
node(1710, "Divertissement", [node(1711, "Cinéma")]),
|
|
]),
|
|
];
|
|
|
|
it("resolves a non-leaf parent by id (to display a current target)", () => {
|
|
const c = findTaxonomyCategory(roots, 1710);
|
|
expect(c).toMatchObject({
|
|
id: 1710,
|
|
name: "Divertissement",
|
|
is_inputable: false,
|
|
});
|
|
expect(c!.parent_id).toBeUndefined();
|
|
});
|
|
|
|
it("resolves a leaf too", () => {
|
|
expect(findTaxonomyCategory(roots, 1711)).toMatchObject({
|
|
id: 1711,
|
|
is_inputable: true,
|
|
});
|
|
});
|
|
|
|
it("returns null for an unknown id", () => {
|
|
expect(findTaxonomyCategory(roots, 9999)).toBeNull();
|
|
});
|
|
|
|
it("finds the real low-confidence default target 1710 in the bundled taxonomy", () => {
|
|
const c = findTaxonomyCategory(getTaxonomyV1().roots, 1710);
|
|
expect(c).toMatchObject({ id: 1710, is_inputable: false });
|
|
});
|
|
});
|
|
|
|
describe("comboboxCategoriesForTarget", () => {
|
|
const leaf = (id: number): Category => ({
|
|
id,
|
|
name: `Leaf ${id}`,
|
|
parent_id: undefined,
|
|
type: "expense",
|
|
is_active: true,
|
|
is_inputable: true,
|
|
sort_order: id,
|
|
created_at: "",
|
|
});
|
|
const leaves = [leaf(1111), leaf(1121), leaf(1131)];
|
|
const parent1710: Category = {
|
|
id: 1710,
|
|
name: "Divertissement",
|
|
parent_id: undefined,
|
|
type: "expense",
|
|
is_active: true,
|
|
is_inputable: false,
|
|
sort_order: Number.MAX_SAFE_INTEGER,
|
|
created_at: "",
|
|
};
|
|
const resolveTarget = (id: number) => (id === 1710 ? parent1710 : null);
|
|
|
|
it("returns the leaves list unchanged (same reference) when target is null", () => {
|
|
expect(comboboxCategoriesForTarget(leaves, null, resolveTarget)).toBe(leaves);
|
|
});
|
|
|
|
it("returns the leaves list unchanged (same reference) when target is already a leaf", () => {
|
|
expect(comboboxCategoriesForTarget(leaves, 1121, resolveTarget)).toBe(leaves);
|
|
});
|
|
|
|
it("appends the resolved non-leaf parent when the target is a parent absent from leaves", () => {
|
|
const out = comboboxCategoriesForTarget(leaves, 1710, resolveTarget);
|
|
expect(out).toHaveLength(leaves.length + 1);
|
|
expect(out[out.length - 1]).toMatchObject({ id: 1710, is_inputable: false });
|
|
// originals preserved, in order
|
|
expect(out.slice(0, 3).map((c) => c.id)).toEqual([1111, 1121, 1131]);
|
|
});
|
|
|
|
it("returns the leaves list unchanged when the non-leaf target cannot be resolved", () => {
|
|
// targetId absent from leaves AND resolver returns null (stale id) -> no injection
|
|
expect(comboboxCategoriesForTarget(leaves, 9999, resolveTarget)).toBe(leaves);
|
|
});
|
|
});
|