feat(org-chart): implement expandable layout and multi-company navigation

Integrate P5-1B through P5-4 shared Core layout, collapse, zoom/pan/fit, company roots and keyboard accessibility. Include React/HTML examples, virtual 20/100/300 employee fixtures, focused checks and the additive data contract.
This commit is contained in:
shkim4 committed 2026-10-12 21:59:09 +09:00
1 parent a92913d6bc
commit 064517ec46
30 files changed
+7532 -183

No files matched your search

@@ -0,0 +1,45 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { validateOrgChartDataset, normalizeOrgChartDataset } from '../data/index.js';
import { resolveExpandedCompanies, validateTopNodes } from './expanded-company.js';
import { createOrgChart } from './index.js';
test('fixed JSON fixtures have exact unique counts, valid ancestry and company memberships', () => {
for (const [size, count, companies, depth] of [['small', 20, 3, 2], ['medium', 100, 5, 3], ['large', 300, 7, 4]] as const) {
const input = JSON.parse(readFileSync(`examples/org-chart/data-expanded-${size}.json`, 'utf8'));
const validation = validateOrgChartDataset(input);
assert.equal(validation.valid, true, JSON.stringify(validation));
if (!validation.valid) continue;
const data = validation.data, normalized = normalizeOrgChartDataset(data);
const resolved = resolveExpandedCompanies(data, normalized.roots, 'hanmac');
assert.equal(data.employees.length, count);
assert.equal(new Set(data.employees.map(employee => employee.id)).size, count);
assert.equal(resolved.companyIds.length, companies);
assert.equal(resolved.issues.length, 0);
for (const employee of data.employees) assert.equal(resolved.companies.get(employee.organizationId), employee.affiliationId);
let maxDepth = 0;
const visit = (nodes: typeof normalized.roots, level: number) => { maxDepth = Math.max(maxDepth, level); nodes.forEach(node => { if (node.children.length) visit(node.children, level + 1); }); };
visit(normalized.roots, 0); assert.equal(maxDepth, depth);
assert.equal(validateTopNodes(data, normalized.roots, resolved.companies, { hanmac: { title: '회장', display: 'card' } }).length, 0);
assert.ok(validateTopNodes(data, normalized.roots, resolved.companies, { hanmac: { employeeId: 'missing' } }).length);
assert.ok(validateTopNodes(data, normalized.roots, resolved.companies, { hanmac: { profileImage: 'javascript:alert(1)' } }).length);
}
});
test('inherited identity stays stable; cross-company ancestry fails instead of mixing companies', () => {
const data = JSON.parse(readFileSync('examples/org-chart/data-expanded-small.json', 'utf8'));
delete data.organizations[1].companyId;
let roots = normalizeOrgChartDataset(data).roots;
assert.equal(resolveExpandedCompanies(data, roots, 'saman').companies.get(data.organizations[1].id), 'hanmac');
data.organizations[1].companyId = 'saman'; roots = normalizeOrgChartDataset(data).roots;
assert.equal(resolveExpandedCompanies(data, roots, 'hanmac').issues[0]?.code, 'COMPANY_HIERARCHY');
});
test('invalid Auto view configuration fails early, leaving Fixed untouched', () => {
const options = { dataSource: { load: async () => ({}) }, stylesheetUrls: { engine: '/engine.css', core: '/style.css' } };
assert.throws(() => createOrgChart({ ...options, layoutMode: 'auto', selectedCompanyIds: ['unknown'] }), /selectedCompanyIds/);
assert.throws(() => createOrgChart({ ...options, layoutMode: 'auto', zoom: { min: 2 } }), /zoom/);
const fixed = createOrgChart({ ...options, layoutMode: 'fixed', selectedCompanyIds: ['unknown'], companyTopNodes: { unknown: { title: 'legacy' } } });
assert.equal(fixed.getViewportState(), undefined); fixed.destroy();
});