import { beforeEach, describe, expect, it, vi } from 'vitest'; import { Tree } from './tree'; import { TREE_CHANGE_EVENT, type TreeChangeEvent } from './tree.types'; const MARKUP = ` `; function setup(): HTMLElement { document.body.innerHTML = MARKUP; return document.body.firstElementChild as HTMLElement; } function checkbox(root: HTMLElement, value: string): HTMLInputElement { return root .querySelector(`[data-c42-tree-item][data-value="${value}"]`)! .querySelector(':scope > [data-c42-tree-row] [data-c42-tree-checkbox]')!; } function changeCheckbox(box: HTMLInputElement, checked: boolean): void { box.checked = checked; box.dispatchEvent(new Event('change', { bubbles: true })); } describe('Tree', () => { beforeEach(() => { document.body.innerHTML = ''; }); it('wires tree/treeitem/group roles and collapses groups by default', () => { const root = setup(); new Tree(root); expect(root.getAttribute('role')).toBe('tree'); const parent = root.querySelector('[data-value="p1"]')!; expect(parent.getAttribute('role')).toBe('treeitem'); expect(parent.getAttribute('aria-expanded')).toBe('false'); expect(root.querySelector('[data-c42-tree-group]')!.hasAttribute('hidden')).toBe(true); }); it('expands and collapses on toggle click', () => { const root = setup(); new Tree(root); const parent = root.querySelector('[data-value="p1"]')!; parent.querySelector('[data-c42-tree-toggle]')!.click(); expect(parent.getAttribute('aria-expanded')).toBe('true'); expect(root.querySelector('[data-c42-tree-group]')!.hasAttribute('hidden')).toBe(false); }); it('checking a parent checks all children', () => { const root = setup(); const tree = new Tree(root); changeCheckbox(checkbox(root, 'p1'), true); expect(checkbox(root, 'c1').checked).toBe(true); expect(checkbox(root, 'c2').checked).toBe(true); expect(tree.value).toEqual(['p1', 'c1', 'c2']); }); it('parent becomes indeterminate when only some children are checked', () => { const root = setup(); const tree = new Tree(root); changeCheckbox(checkbox(root, 'c1'), true); expect(checkbox(root, 'p1').indeterminate).toBe(true); expect(checkbox(root, 'p1').checked).toBe(false); expect(tree.value).toEqual(['c1']); }); it('parent becomes checked when all children are checked', () => { const root = setup(); new Tree(root); changeCheckbox(checkbox(root, 'c1'), true); changeCheckbox(checkbox(root, 'c2'), true); expect(checkbox(root, 'p1').checked).toBe(true); expect(checkbox(root, 'p1').indeterminate).toBe(false); }); it('treeSelection=false keeps checkboxes independent', () => { const root = setup(); const tree = new Tree(root, { treeSelection: false }); changeCheckbox(checkbox(root, 'p1'), true); expect(checkbox(root, 'c1').checked).toBe(false); expect(tree.value).toEqual(['p1']); }); it('expandAll expands groups on init', () => { const root = setup(); new Tree(root, { expandAll: true }); expect(root.querySelector('[data-value="p1"]')!.getAttribute('aria-expanded')).toBe('true'); }); it('expands a collapsed node with ArrowRight', () => { const root = setup(); new Tree(root); const parent = root.querySelector('[data-value="p1"]')!; checkbox(root, 'p1').focus(); root.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true })); expect(parent.getAttribute('aria-expanded')).toBe('true'); }); it('emits change events with selected values', () => { const root = setup(); const tree = new Tree(root); const spy = vi.fn(); tree.on(TREE_CHANGE_EVENT, spy); changeCheckbox(checkbox(root, 'c1'), true); expect(spy).toHaveBeenCalledOnce(); expect(spy.mock.calls[0][0].detail.values).toEqual(['c1']); }); });