Simpl-Resultat/src/components/categories-migration/migrationTargets.test.ts
le king fu ce2793fc2c
All checks were successful
PR Check / rust (pull_request) Successful in 22m29s
PR Check / frontend (pull_request) Successful in 2m30s
refactor(categories): migration target picker lists leaves only
The v1 target combobox in the migration preview now offers only leaf
categories (the inputable end-categories), not intermediate parents, so a
transaction is never mapped to a grouping bucket. Leaves render as a flat,
un-indented list in taxonomy order.

A low-confidence default can still point a v2 category at a non-leaf parent
(e.g. 'Divertissement' #1710); MappingRow re-injects that current target for
its single row via findTaxonomyCategory so the input stays populated, while
new picks remain leaves-only. Adds a findTaxonomyCategory pure helper + tests
(adapter now returns 112 leaves, not the 150-node full tree).
2026-07-04 20:40:29 -04:00

129 lines
4 KiB
TypeScript

import { describe, it, expect } from "vitest";
import {
taxonomyToComboboxCategories,
findTaxonomyCategory,
} from "./migrationTargets";
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 });
});
});