whisper-money/resources/js/lib/category-tree.test.ts

102 lines
3.4 KiB
TypeScript

import { Category } from '@/types/category';
import { describe, expect, it } from 'vitest';
import {
categorySelectionState,
toggleCategorySelection,
} from './category-tree';
function category(id: string, parent_id: string | null): Category {
return {
id,
name: id,
icon: 'Tag',
color: 'gray',
type: 'expense',
cashflow_direction: 'hidden',
parent_id,
} as Category;
}
// food ──┬─ groceries ── coffee
// └─ restaurants
// drinks (root)
const categories: Category[] = [
category('food', null),
category('groceries', 'food'),
category('restaurants', 'food'),
category('coffee', 'groceries'),
category('drinks', null),
];
const stateOf = (id: string, selected: string[]) =>
categorySelectionState(id, new Set(selected), categories);
describe('categorySelectionState', () => {
it('marks a category and its descendants checked when the parent is selected', () => {
expect(stateOf('food', ['food'])).toBe('checked');
expect(stateOf('groceries', ['food'])).toBe('checked');
expect(stateOf('coffee', ['food'])).toBe('checked');
expect(stateOf('drinks', ['food'])).toBe('unchecked');
});
it('marks ancestors indeterminate when only a descendant is selected', () => {
expect(stateOf('food', ['coffee'])).toBe('indeterminate');
expect(stateOf('groceries', ['coffee'])).toBe('indeterminate');
expect(stateOf('coffee', ['coffee'])).toBe('checked');
});
});
describe('toggleCategorySelection', () => {
it('selecting a parent collapses to a single id covering the subtree', () => {
expect(toggleCategorySelection([], 'food', categories)).toEqual([
'food',
]);
});
it('unselecting one child keeps its siblings selected', () => {
const result = toggleCategorySelection(
['food'],
'restaurants',
categories,
);
expect(result).not.toContain('food');
expect(result).not.toContain('restaurants');
expect(stateOf('groceries', result)).toBe('checked');
expect(stateOf('restaurants', result)).toBe('unchecked');
expect(stateOf('food', result)).toBe('indeterminate');
});
it('re-selecting a sibling does not auto-collapse to the parent, but clicking the parent does', () => {
const partial = toggleCategorySelection(
['food'],
'restaurants',
categories,
);
const reselected = toggleCategorySelection(
partial,
'restaurants',
categories,
);
// Both children selected individually; the parent is not auto-added.
expect(reselected).not.toContain('food');
expect(stateOf('groceries', reselected)).toBe('checked');
expect(stateOf('restaurants', reselected)).toBe('checked');
// Clicking the parent collapses the whole subtree to a single id.
expect(toggleCategorySelection(reselected, 'food', categories)).toEqual(
['food'],
);
});
it('selecting a single child never pulls in its parent', () => {
const result = toggleCategorySelection([], 'coffee', categories);
expect(result).toEqual(['coffee']);
expect(stateOf('groceries', result)).toBe('indeterminate');
expect(stateOf('food', result)).toBe('indeterminate');
});
});