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']);
});
});