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, base64UrlToBytes, bytesToBase64Url, decodeTreeState, decodeAdaptiveBitset, encodeAdaptiveBitset, getBit, loadManifest, setBit, validateManifestEvolution } = await import('../src/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('can initially select every selectable node', () => { const { tree } = createTree({ initiallySelected: true }); assert.deepEqual(tree.getSelectedIds(), ['parent', 'first', 'second']); }); 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/ ); }); test('bit operations use least-significant-bit-first byte ordering', () => { const bytes = new Uint8Array(2); setBit(bytes, 0, true); setBit(bytes, 7, true); setBit(bytes, 8, true); assert.deepEqual(Array.from(bytes), [129, 1]); assert.equal(getBit(bytes, 0), true); assert.equal(getBit(bytes, 7), true); assert.equal(getBit(bytes, 8), true); assert.equal(getBit(bytes, 16), false); setBit(bytes, 7, false); assert.deepEqual(Array.from(bytes), [1, 1]); }); test('Base64URL encoding is unpadded, URL-safe, and round trips', () => { const bytes = Uint8Array.from([251, 255, 0]); const encoded = bytesToBase64Url(bytes); assert.equal(encoded, '-_8A'); assert.deepEqual(Array.from(base64UrlToBytes(encoded)), Array.from(bytes)); assert.equal(bytesToBase64Url(new Uint8Array()), ''); assert.throws(() => base64UrlToBytes('not valid!'), /Invalid Base64URL/); }); test('adaptive bitsets choose sparse enabled and disabled representations', () => { const mostlyDisabled = new Map(Array.from( { length: 628 }, (_, slot) => [slot, slot === 2 || slot === 400] )); const sparseEnabled = encodeAdaptiveBitset(mostlyDisabled, 628); assert.equal(sparseEnabled[0], 1); const enabled = decodeAdaptiveBitset(sparseEnabled, 628); assert.equal(enabled(2), true); assert.equal(enabled(3), false); assert.equal(enabled(400), true); const nearlyAllEnabled = new Map(Array.from({ length: 100 }, (_, slot) => [slot, slot !== 7])); const sparseDisabled = encodeAdaptiveBitset(nearlyAllEnabled, 100); assert.equal(sparseDisabled[0], 2); const enabledExcept = decodeAdaptiveBitset(sparseDisabled, 105); assert.equal(enabledExcept(6), true); assert.equal(enabledExcept(7), false); assert.equal(enabledExcept(99), true); assert.equal(enabledExcept(100), false); }); test('loads text and JSON manifests while retaining tombstone slots', () => { const text = loadManifest('# tree-state-manifest\nroot\n!deleted\nroot>child\n'); assert.equal(text.slots.length, 3); assert.equal(text.slots[1].deleted, true); assert.equal(text.pathToSlot.get('root>child'), 2); const json = loadManifest({ schema: 1, nodes: ['renamed', null, 'renamed>child', 'new'] }); assert.equal(json.pathToSlot.get('renamed>child'), 2); assert.equal(json.pathToSlot.get('new'), 3); assert.throws(() => loadManifest({ schema: 1, nodes: ['same', 'same'] }), /Duplicate/); }); test('manifest evolution rejects slot removal and tombstone reuse', () => { const original = { schema: 1, nodes: ['root', null, 'root>child'] }; assert.equal(validateManifestEvolution(original, { schema: 1, nodes: ['root', null, 'root>child', 'new'] }).valid, true); assert.match(validateManifestEvolution(original, { schema: 1, nodes: ['root', 'reused', 'root>child'] }).errors[0], /cannot be reused/); assert.match(validateManifestEvolution(original, { schema: 1, nodes: ['root'] }).errors[0], /cannot be removed/); }); test('serializes and restores checked and expanded state through a fragment', () => { const manifest = loadManifest({ schema: 1, nodes: ['parent', 'parent>first', null, 'parent>second'] }); const first = createTree(); checkbox(first.container, 'first').click(); first.container.querySelector('.checkbox-tree__toggle').click(); const fragment = first.tree.serializeStateToFragment(manifest); assert.equal(fragment, '#v=1&c=AAI&x=AAE'); const second = createTree({ manifest }); const result = second.tree.restoreStateFromLocation(manifest, { hash: fragment }); assert.equal(result.applied, true); assert.deepEqual(second.tree.getSelectedIds(), ['first']); assert.equal(second.container.querySelector('.checkbox-tree__toggle').getAttribute('aria-expanded'), 'true'); }); test('compatible manifest changes preserve slots and default appended nodes', () => { const oldManifest = loadManifest({ schema: 1, nodes: ['parent', 'parent>first', 'parent>second'] }); const first = createTree(); checkbox(first.container, 'first').click(); const fragment = first.tree.serializeStateToFragment(oldManifest); const evolved = loadManifest({ schema: 1, nodes: ['parent', 'parent>first', null, 'parent>second', 'future'] }); const second = createTree(); const result = second.tree.restoreStateFromLocation(evolved, { hash: fragment }); assert.equal(result.applied, true); assert.deepEqual(second.tree.getSelectedIds(), ['first']); assert.deepEqual(second.tree.validateManifest(evolved), { valid: true, unregisteredPaths: [], missingPaths: ['future'] }); }); test('malformed, oversized, and unsupported URL state fails safely', () => { const manifest = loadManifest({ schema: 1, nodes: ['parent', 'parent>first', 'parent>second'] }); assert.equal(decodeTreeState('#v=2&c=AQ', manifest).applied, false); assert.equal(decodeTreeState('#v=1&c=!', manifest).applied, false); assert.equal(decodeTreeState('#v=1&c=Aw', manifest).applied, false); const { tree } = createTree({ initiallySelected: true }); const result = tree.restoreStateFromLocation(manifest, { hash: '#v=1&c=!' }); assert.equal(result.applied, false); assert.deepEqual(tree.getSelectedIds(), ['parent', 'first', 'second']); });