diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..96d3c81 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "liveServer.settings.root": "/web" +} diff --git a/test/checkbox-tree.test.mjs b/test/checkbox-tree.test.mjs new file mode 100644 index 0000000..7ad0a04 --- /dev/null +++ b/test/checkbox-tree.test.mjs @@ -0,0 +1,100 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { JSDOM } from 'jsdom'; + +const dom = new JSDOM('
', { url: 'https://example.test/' }); +globalThis.window = dom.window; +globalThis.document = dom.window.document; +globalThis.Element = dom.window.Element; +globalThis.localStorage = dom.window.localStorage; + +const { CheckboxTree } = await import('../web/lib/checkbox-tree/checkbox-tree.js'); + +function createTree(options = {}) { + const container = document.createElement('div'); + document.body.replaceChildren(container); + const tree = new CheckboxTree(container, options); + tree.setData([ + { + id: 'parent', + label: 'Parent', + metadata: { kind: 'parent' }, + children: [ + { id: 'first', label: 'First', metadata: { kind: 'leaf' } }, + { id: 'second', label: 'Second', metadata: { kind: 'leaf' } } + ] + } + ]); + return { container, tree }; +} + +function checkbox(container, id) { + return container.querySelector(`.checkbox-tree__checkbox[data-node-id="${id}"]`); +} + +test('selecting a parent selects all descendants and emits selected nodes', () => { + let change; + const { container, tree } = createTree({ onSelectionChange: event => { change = event; } }); + + const parent = checkbox(container, 'parent'); + parent.checked = true; + parent.dispatchEvent(new window.Event('change', { bubbles: true })); + + assert.deepEqual(tree.getSelectedIds(), ['parent', 'first', 'second']); + assert.deepEqual(change.selectedIds, ['parent', 'first', 'second']); + assert.equal(change.selectedNodes[1].metadata.kind, 'leaf'); +}); + +test('partial child selection makes the parent indeterminate', () => { + const { container, tree } = createTree(); + const first = checkbox(container, 'first'); + first.checked = true; + first.dispatchEvent(new window.Event('change', { bubbles: true })); + + const parent = checkbox(container, 'parent'); + assert.equal(parent.checked, false); + assert.equal(parent.indeterminate, true); + assert.deepEqual(tree.getSelectedIds(), ['first']); +}); + +test('persists selection and collapsed branches per configured key', () => { + localStorage.clear(); + const first = createTree({ storageKey: 'example-tree' }); + checkbox(first.container, 'first').click(); + + const toggle = first.container.querySelector('.checkbox-tree__toggle'); + toggle.click(); + assert.equal(toggle.getAttribute('aria-expanded'), 'true'); + + const second = createTree({ storageKey: 'example-tree' }); + assert.deepEqual(second.tree.getSelectedIds(), ['first']); + assert.equal(second.container.querySelector('.checkbox-tree__toggle').getAttribute('aria-expanded'), 'true'); +}); + +test('supports independent instances and removes listeners on destroy', () => { + const firstContainer = document.createElement('div'); + const secondContainer = document.createElement('div'); + document.body.replaceChildren(firstContainer, secondContainer); + const first = new CheckboxTree(firstContainer); + const second = new CheckboxTree(secondContainer); + const data = [{ id: 'node', label: 'Node' }]; + first.setData(data); + second.setData(data); + + checkbox(firstContainer, 'node').click(); + assert.deepEqual(first.getSelectedIds(), ['node']); + assert.deepEqual(second.getSelectedIds(), []); + + first.destroy(); + assert.equal(firstContainer.childElementCount, 0); + assert.equal(firstContainer.classList.contains('checkbox-tree'), false); +}); + +test('rejects duplicate node ids', () => { + const container = document.createElement('div'); + const tree = new CheckboxTree(container); + assert.throws( + () => tree.setData([{ id: 'same', label: 'One' }, { id: 'same', label: 'Two' }]), + /Duplicate CheckboxTree node id/ + ); +}); diff --git a/web/index.html b/web/index.html index 024b8fc..f6a5e36 100644 --- a/web/index.html +++ b/web/index.html @@ -8,6 +8,7 @@ + @@ -55,4 +56,4 @@ -