UNPKG

@furystack/shades-common-components

Version:

Common UI components for FuryStack Shades

341 lines 16 kB
import { using } from '@furystack/utils'; import { describe, expect, it } from 'vitest'; import { TreeService } from './tree-service.js'; const getChildren = (node) => node.children ?? []; describe('TreeService', () => { const createTreeData = () => [ { id: 1, name: 'Root 1', children: [ { id: 11, name: 'Child 1-1', children: [ { id: 111, name: 'Leaf 1-1-1' }, { id: 112, name: 'Leaf 1-1-2' }, ], }, { id: 12, name: 'Child 1-2' }, ], }, { id: 2, name: 'Root 2' }, { id: 3, name: 'Root 3', children: [{ id: 31, name: 'Child 3-1' }], }, ]; const createTestService = () => { const rootItems = createTreeData(); const service = new TreeService({ getChildren, }); service.rootItems.setValue(rootItems); service.updateFlattenedNodes(); return { service, rootItems }; }; describe('flattenedNodes', () => { it('should flatten only root items when none are expanded', () => { const { service, rootItems } = createTestService(); using(service, () => { const flattened = service.flattenedNodes.getValue(); expect(flattened.length).toBe(3); expect(flattened[0].item).toBe(rootItems[0]); expect(flattened[0].level).toBe(0); expect(flattened[0].hasChildren).toBe(true); expect(flattened[0].isExpanded).toBe(false); expect(flattened[1].item).toBe(rootItems[1]); expect(flattened[1].hasChildren).toBe(false); expect(flattened[2].item).toBe(rootItems[2]); }); }); it('should flatten children when a node is expanded', () => { const { service, rootItems } = createTestService(); using(service, () => { service.expand(rootItems[0]); const flattened = service.flattenedNodes.getValue(); expect(flattened.length).toBe(5); expect(flattened[0].item).toBe(rootItems[0]); expect(flattened[0].isExpanded).toBe(true); expect(flattened[1].item).toBe(rootItems[0].children[0]); expect(flattened[1].level).toBe(1); expect(flattened[2].item).toBe(rootItems[0].children[1]); expect(flattened[2].level).toBe(1); expect(flattened[3].item).toBe(rootItems[1]); expect(flattened[4].item).toBe(rootItems[2]); }); }); it('should flatten deeply nested children', () => { const { service, rootItems } = createTestService(); using(service, () => { service.expand(rootItems[0]); service.expand(rootItems[0].children[0]); const flattened = service.flattenedNodes.getValue(); expect(flattened.length).toBe(7); expect(flattened[2].item.id).toBe(111); expect(flattened[2].level).toBe(2); expect(flattened[3].item.id).toBe(112); expect(flattened[3].level).toBe(2); }); }); it('should sync items with flattened nodes', () => { const { service, rootItems } = createTestService(); using(service, () => { service.expand(rootItems[0]); const items = service.items.getValue(); const flattened = service.flattenedNodes.getValue(); expect(items.length).toBe(flattened.length); for (let i = 0; i < items.length; i++) { expect(items[i]).toBe(flattened[i].item); } }); }); }); describe('expand/collapse', () => { it('should expand a node', () => { const { service, rootItems } = createTestService(); using(service, () => { service.expand(rootItems[0]); expect(service.isExpanded(rootItems[0])).toBe(true); }); }); it('should collapse a node', () => { const { service, rootItems } = createTestService(); using(service, () => { service.expand(rootItems[0]); service.collapse(rootItems[0]); expect(service.isExpanded(rootItems[0])).toBe(false); }); }); it('should toggle expand on a collapsed node with children', () => { const { service, rootItems } = createTestService(); using(service, () => { service.toggleExpanded(rootItems[0]); expect(service.isExpanded(rootItems[0])).toBe(true); }); }); it('should toggle collapse on an expanded node', () => { const { service, rootItems } = createTestService(); using(service, () => { service.expand(rootItems[0]); service.toggleExpanded(rootItems[0]); expect(service.isExpanded(rootItems[0])).toBe(false); }); }); it('should not expand a leaf node', () => { const { service, rootItems } = createTestService(); using(service, () => { service.toggleExpanded(rootItems[1]); expect(service.isExpanded(rootItems[1])).toBe(false); }); }); it('should hide children when collapsing a node', () => { const { service, rootItems } = createTestService(); using(service, () => { service.expand(rootItems[0]); expect(service.flattenedNodes.getValue().length).toBe(5); service.collapse(rootItems[0]); expect(service.flattenedNodes.getValue().length).toBe(3); }); }); }); describe('getParent', () => { it('should return undefined for root items', () => { const { service, rootItems } = createTestService(); using(service, () => { expect(service.getParent(rootItems[0])).toBeUndefined(); expect(service.getParent(rootItems[1])).toBeUndefined(); }); }); it('should return the parent of a child node', () => { const { service, rootItems } = createTestService(); using(service, () => { const parent = service.getParent(rootItems[0].children[0]); expect(parent).toBe(rootItems[0]); }); }); it('should return the parent of a deeply nested node', () => { const { service, rootItems } = createTestService(); using(service, () => { const parent = service.getParent(rootItems[0].children[0].children[0]); expect(parent).toBe(rootItems[0].children[0]); }); }); }); describe('getNodeInfo', () => { it('should return node info for a visible item', () => { const { service, rootItems } = createTestService(); using(service, () => { const info = service.getNodeInfo(rootItems[0]); expect(info).toBeDefined(); expect(info?.level).toBe(0); expect(info?.hasChildren).toBe(true); expect(info?.isExpanded).toBe(false); }); }); it('should return undefined for a hidden item', () => { const { service, rootItems } = createTestService(); using(service, () => { const info = service.getNodeInfo(rootItems[0].children[0]); expect(info).toBeUndefined(); }); }); }); describe('handleKeyDown - tree navigation', () => { it('should expand a collapsed node on ArrowRight', () => { const { service, rootItems } = createTestService(); using(service, () => { service.hasFocus.setValue(true); service.focusedItem.setValue(rootItems[0]); service.handleKeyDown(new KeyboardEvent('keydown', { key: 'ArrowRight' })); expect(service.isExpanded(rootItems[0])).toBe(true); }); }); it('should not move focus on ArrowRight when already expanded (delegated to spatial navigation)', () => { const { service, rootItems } = createTestService(); using(service, () => { service.hasFocus.setValue(true); service.expand(rootItems[0]); service.focusedItem.setValue(rootItems[0]); const ev = new KeyboardEvent('keydown', { key: 'ArrowRight', cancelable: true }); service.handleKeyDown(ev); expect(service.focusedItem.getValue()).toBe(rootItems[0]); }); }); it('should do nothing on ArrowRight on a leaf node', () => { const { service, rootItems } = createTestService(); using(service, () => { service.hasFocus.setValue(true); service.focusedItem.setValue(rootItems[1]); service.handleKeyDown(new KeyboardEvent('keydown', { key: 'ArrowRight' })); expect(service.focusedItem.getValue()).toBe(rootItems[1]); }); }); it('should collapse an expanded node on ArrowLeft', () => { const { service, rootItems } = createTestService(); using(service, () => { service.hasFocus.setValue(true); service.expand(rootItems[0]); service.focusedItem.setValue(rootItems[0]); service.handleKeyDown(new KeyboardEvent('keydown', { key: 'ArrowLeft' })); expect(service.isExpanded(rootItems[0])).toBe(false); }); }); it('should move focus to parent on ArrowLeft when node is collapsed', () => { const { service, rootItems } = createTestService(); using(service, () => { service.hasFocus.setValue(true); service.expand(rootItems[0]); service.focusedItem.setValue(rootItems[0].children[0]); service.handleKeyDown(new KeyboardEvent('keydown', { key: 'ArrowLeft' })); expect(service.focusedItem.getValue()).toBe(rootItems[0]); }); }); it('should do nothing on ArrowLeft on a root node that is collapsed', () => { const { service, rootItems } = createTestService(); using(service, () => { service.hasFocus.setValue(true); service.focusedItem.setValue(rootItems[1]); service.handleKeyDown(new KeyboardEvent('keydown', { key: 'ArrowLeft' })); expect(service.focusedItem.getValue()).toBe(rootItems[1]); }); }); it('should not handle keys when not focused', () => { const { service, rootItems } = createTestService(); using(service, () => { service.hasFocus.setValue(false); service.focusedItem.setValue(rootItems[0]); service.handleKeyDown(new KeyboardEvent('keydown', { key: 'ArrowRight' })); expect(service.isExpanded(rootItems[0])).toBe(false); }); }); }); describe('inherited ListService keyboard navigation', () => { it('should not handle ArrowDown (delegated to spatial navigation)', () => { const { service, rootItems } = createTestService(); using(service, () => { service.hasFocus.setValue(true); service.focusedItem.setValue(rootItems[0]); const ev = new KeyboardEvent('keydown', { key: 'ArrowDown', cancelable: true }); service.handleKeyDown(ev); expect(service.focusedItem.getValue()).toBe(rootItems[0]); }); }); it('should not handle ArrowUp (delegated to spatial navigation)', () => { const { service, rootItems } = createTestService(); using(service, () => { service.hasFocus.setValue(true); service.focusedItem.setValue(rootItems[1]); const ev = new KeyboardEvent('keydown', { key: 'ArrowUp', cancelable: true }); service.handleKeyDown(ev); expect(service.focusedItem.getValue()).toBe(rootItems[1]); }); }); it('should handle Home to move focus to first item', () => { const { service, rootItems } = createTestService(); using(service, () => { service.hasFocus.setValue(true); service.focusedItem.setValue(rootItems[2]); service.handleKeyDown(new KeyboardEvent('keydown', { key: 'Home' })); expect(service.focusedItem.getValue()).toBe(rootItems[0]); }); }); it('should handle End to move focus to last item', () => { const { service, rootItems } = createTestService(); using(service, () => { service.hasFocus.setValue(true); service.focusedItem.setValue(rootItems[0]); service.handleKeyDown(new KeyboardEvent('keydown', { key: 'End' })); expect(service.focusedItem.getValue()).toBe(rootItems[2]); }); }); it('should handle Space to toggle selection of focused item', () => { const { service, rootItems } = createTestService(); using(service, () => { service.hasFocus.setValue(true); service.focusedItem.setValue(rootItems[0]); service.handleKeyDown(new KeyboardEvent('keydown', { key: ' ' })); expect(service.selection.getValue()).toContain(rootItems[0]); service.handleKeyDown(new KeyboardEvent('keydown', { key: ' ' })); expect(service.selection.getValue()).not.toContain(rootItems[0]); }); }); it('should handle Escape to clear selection and search term', () => { const { service, rootItems } = createTestService(); using(service, () => { service.hasFocus.setValue(true); service.selection.setValue([rootItems[0]]); service.searchTerm.setValue('test'); service.handleKeyDown(new KeyboardEvent('keydown', { key: 'Escape' })); expect(service.selection.getValue()).toEqual([]); expect(service.searchTerm.getValue()).toBe(''); }); }); }); describe('handleItemDoubleClick', () => { it('should toggle expansion on double-click of a node with children', () => { const { service, rootItems } = createTestService(); using(service, () => { service.handleItemDoubleClick(rootItems[0]); expect(service.isExpanded(rootItems[0])).toBe(true); service.handleItemDoubleClick(rootItems[0]); expect(service.isExpanded(rootItems[0])).toBe(false); }); }); it('should not expand on double-click of a leaf node', () => { const { service, rootItems } = createTestService(); using(service, () => { service.handleItemDoubleClick(rootItems[1]); expect(service.isExpanded(rootItems[1])).toBe(false); }); }); }); describe('dispose', () => { it('should dispose all observables', () => { const { service } = createTestService(); expect(() => service[Symbol.dispose]()).not.toThrow(); }); }); }); //# sourceMappingURL=tree-service.spec.js.map