import test from 'node:test'; import assert from 'node:assert/strict'; import { applyInitialVisibility, autoEngineOptions, resolveAutoLayout, resolveInteractions } from './auto-layout-policy.js'; import { createOrgChart } from './index.js'; import { engineTree, type RenderContext } from './render.js'; import type { Organization, OrganizationTreeNode } from '../data/index.js'; const organization = (id: string, parentId: string | null): Organization => ({ id, parentId, name: id, type: parentId ? 'team' : 'root', leaderId: null, displayOrder: 0 }); const organizations = [organization('root', null), organization('a', 'root'), organization('b', 'root'), organization('a1', 'a'), organization('a2', 'a1')]; test('each independent layout policy selects compatible installed engine options', () => { assert.deepEqual(autoEngineOptions(resolveAutoLayout()), { direction: 't2b', visibleLevel: 999 }); assert.deepEqual(autoEngineOptions(resolveAutoLayout({ type: 'lr', initialVisibleDepth: 1 })), { direction: 'l2r', visibleLevel: 999 }); for (const depth of [2, 3, 4] as const) assert.deepEqual(autoEngineOptions(resolveAutoLayout({ type: 'hybrid', hybridStartDepth: depth })), { direction: 't2b', visibleLevel: 999, verticalLevel: depth }); }); test('collapse, Zoom and Pan have independent validated defaults', () => { assert.deepEqual(resolveInteractions(), { collapse: true, zoom: true, pan: true }); assert.deepEqual(resolveInteractions({ collapse: false }), { collapse: false, zoom: true, pan: true }); assert.deepEqual(resolveInteractions({ zoom: false }), { collapse: true, zoom: false, pan: true }); assert.deepEqual(resolveInteractions({ pan: false }), { collapse: true, zoom: true, pan: false }); assert.throws(() => resolveInteractions({ zoom: 0 as unknown as boolean }), /interactions.zoom must be boolean/); }); test('incompatible per-group/global/directional combinations fail explicitly', () => { assert.throws(() => resolveAutoLayout({ type: 'lr', hybridOrganizationIds: ['a'] }), /supported only with TB/); assert.throws(() => resolveAutoLayout({ type: 'hybrid', hybridOrganizationIds: ['a'] }), /supported only with TB/); assert.throws(() => resolveAutoLayout({ initialVisibleDepth: 0 as 1 }), /initialVisibleDepth/); assert.throws(() => resolveAutoLayout({ hybridStartDepth: 5 as 2 }), /hybridStartDepth/); assert.throws(() => resolveAutoLayout({ hybridOrganizationIds: [''] }), /nonempty/); const input = ['a', 'a']; const resolved = resolveAutoLayout({ hybridOrganizationIds: input }); input.push('b'); assert.deepEqual(resolved.hybridOrganizationIds, ['a']); assert.ok(Object.isFrozen(resolved.hybridOrganizationIds)); }); test('initial depth is one-based and user expansion survives subsequent renders and new data', () => { const collapsed = new Set(), initialized = new Set(); applyInitialVisibility(organizations, collapsed, initialized, 2); assert.deepEqual([...collapsed], ['a', 'b', 'a1', 'a2']); collapsed.delete('a'); applyInitialVisibility([...organizations, organization('b1', 'b')], collapsed, initialized, 2); assert.equal(collapsed.has('a'), false); assert.equal(collapsed.has('b1'), true); applyInitialVisibility(organizations, collapsed, initialized, 'all', true); assert.equal(collapsed.size, 0); collapsed.add('a1'); applyInitialVisibility(organizations.filter(item => item.id !== 'a2'), collapsed, initialized, 'all'); assert.equal(collapsed.has('a1'), true); assert.equal(initialized.has('a2'), false); }); test('Depth 1 projection keeps independent company roots while collapsed descendants stay out of DOM', () => { const roots: OrganizationTreeNode[] = [{ organization: organizations[0]!, children: [{ organization: organizations[1]!, children: [{ organization: organizations[3]!, children: [] }] }] }]; const collapsed = new Set(), initialized = new Set(); applyInitialVisibility(organizations, collapsed, initialized, 1); const context = { orderedOrganizations: [], readOnly: true, collapsedOrganizations: collapsed, autoLayout: resolveAutoLayout() } as unknown as RenderContext; const tree = engineTree(roots, 'initial', context); assert.equal(tree.length, 1); assert.equal(tree[0]!.children, undefined); assert.deepEqual(context.orderedOrganizations.map(item => item.id), ['root']); }); test('per-group Hybrid projection marks only the selected parent and leaves source immutable', () => { const roots: OrganizationTreeNode[] = [{ organization: organizations[0]!, children: [{ organization: organizations[1]!, children: [{ organization: organizations[3]!, children: [] }] }, { organization: organizations[2]!, children: [] }] }]; const before = structuredClone(roots); const context = { orderedOrganizations: [], readOnly: true, autoLayout: resolveAutoLayout({ hybridOrganizationIds: ['a'] }) } as unknown as RenderContext; const tree = engineTree(roots, 'groups', context); assert.equal(tree[0]!.hybrid, undefined); assert.equal(tree[0]!.level, 1); assert.equal(tree[0]!.children![0]!.hybrid, true); assert.equal(tree[0]!.children![0]!.level, 2); assert.equal(tree[0]!.children![0]!.children![0]!.level, 3); assert.equal(tree[0]!.children![1]!.hybrid, undefined); assert.deepEqual(roots, before); }); test('Fixed ignores Auto configuration and facade defaults keep existing TB fully expanded', () => { const options = { dataSource: { load: async () => ({}) }, stylesheetUrls: { engine: '/engine.css', core: '/style.css' } }; const fixed = createOrgChart({ ...options, autoLayout: { type: 'invalid' as 'tb' } }); assert.equal(fixed.getAutoLayout().type, 'tb'); fixed.destroy(); const expanded = createOrgChart({ ...options, layoutMode: 'auto' }); assert.equal(expanded.getAutoLayout().initialVisibleDepth, 'all'); assert.equal(expanded.getAutoLayout().type, 'tb'); expanded.destroy(); });