131 lines
8.4 KiB
TypeScript
131 lines
8.4 KiB
TypeScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import fixture from '../../../../examples/org-chart/data-expanded-large.json';
|
|
import small from '../../../../examples/org-chart/data-expanded-small.json';
|
|
import medium from '../../../../examples/org-chart/data-expanded-medium.json';
|
|
import { companyColorPresets, type CompanyId } from '../company-color-presets.js';
|
|
import {
|
|
calculateSummary, createOrgChartExampleDataset, JsonOrgChartDataSource,
|
|
normalizeOrgChartDataset, snapshotOrgChartDataset, validateOrgChartDataset,
|
|
type OrgChartDataset, type OrgChartExampleEmployeeCount,
|
|
} from './index.js';
|
|
|
|
const base = fixture as OrgChartDataset;
|
|
const companies = Object.keys(companyColorPresets) as CompanyId[];
|
|
const sizes: OrgChartExampleEmployeeCount[] = [20, 100, 300];
|
|
|
|
function assertContract(dataset: OrgChartDataset) {
|
|
const result = validateOrgChartDataset(dataset);
|
|
assert.equal(result.valid, true, result.valid ? undefined : JSON.stringify(result.issues));
|
|
assert.equal(new Set(dataset.employees.map(employee => employee.id)).size, dataset.employees.length);
|
|
assert.equal(new Set(dataset.organizations.map(org => org.id)).size, dataset.organizations.length);
|
|
assert.equal(new Set(dataset.affiliations.map(affiliation => affiliation.id)).size, dataset.affiliations.length);
|
|
const summary = calculateSummary(dataset).raw;
|
|
assert.equal(summary.total, dataset.employees.length, 'leaders count once as employees');
|
|
const pending = normalizeOrgChartDataset(dataset).roots.map(node => ({ node, depth: 1, underCell: false }));
|
|
let maximumDepth = 0;
|
|
while (pending.length) {
|
|
const { node, depth, underCell } = pending.pop()!;
|
|
assert.ok(!(underCell && node.organization.type === 'team'), `team ${node.organization.id} must not descend from a cell`);
|
|
maximumDepth = Math.max(maximumDepth, depth);
|
|
pending.push(...node.children.map(child => ({ node: child, depth: depth + 1, underCell: underCell || node.organization.type === 'cell' })));
|
|
}
|
|
assert.ok(maximumDepth >= 3);
|
|
assert.ok(new Set(Object.values(summary.byOrganization).map(value => value.direct)).size > 1, 'unequal organization membership');
|
|
}
|
|
|
|
test('generated small/medium/large fixtures follow the hierarchy, retaining team -> cell examples', () => {
|
|
for (const dataset of [small, medium, fixture] as OrgChartDataset[]) assertContract(dataset);
|
|
const byId = new Map(base.organizations.map(org => [org.id, org]));
|
|
const cells = base.organizations.filter(org => org.type === 'cell');
|
|
assert.equal(cells.length, companies.length * 2);
|
|
for (const cell of cells) assert.equal(byId.get(cell.parentId!)!.type, 'team');
|
|
});
|
|
|
|
test('hierarchy regression check rejects a team below an indirect cell ancestor', () => {
|
|
const invalid = structuredClone(base);
|
|
const office = invalid.organizations.find(org => org.id === 'hanmac-office-sibling')!;
|
|
office.parentId = 'hanmac-cell-0';
|
|
invalid.organizations.find(org => org.id === 'hanmac-team-sibling')!.parentId = office.id;
|
|
assert.throws(() => assertContract(invalid), /must not descend from a cell/);
|
|
});
|
|
|
|
for (const employeeCount of sizes) {
|
|
test(`total ${employeeCount} contains exactly that many unique employees across all seven preset companies`, () => {
|
|
const dataset = createOrgChartExampleDataset(base, { scope: 'total', employeeCount });
|
|
assertContract(dataset);
|
|
assert.equal(dataset.employees.length, employeeCount);
|
|
assert.deepEqual(dataset.affiliations.map(item => item.id), companies);
|
|
for (const companyId of companies) assert.ok(dataset.employees.some(employee => employee.affiliationId === companyId));
|
|
});
|
|
test(`per-company ${employeeCount} supports every company, pairs, and the full seven-company selection`, () => {
|
|
for (const companyId of companies) {
|
|
const dataset = createOrgChartExampleDataset(base, { scope: 'per-company', employeeCount, companyIds: [companyId] });
|
|
assertContract(dataset);
|
|
assert.equal(dataset.employees.length, employeeCount);
|
|
assert.ok(dataset.organizations.every(org => org.companyId === companyId));
|
|
assert.ok(dataset.employees.every(employee => employee.affiliationId === companyId));
|
|
}
|
|
for (const companyIds of [['hanmac', 'saman'], companies] as CompanyId[][]) {
|
|
const dataset = createOrgChartExampleDataset(base, { scope: 'per-company', employeeCount, companyIds });
|
|
assertContract(dataset);
|
|
assert.equal(dataset.employees.length, employeeCount * companyIds.length);
|
|
for (const id of companyIds) assert.equal(dataset.employees.filter(employee => employee.affiliationId === id).length, employeeCount);
|
|
}
|
|
});
|
|
}
|
|
|
|
test('total company filtering preserves company allocation without redistribution or lost ancestors', () => {
|
|
for (const employeeCount of sizes) {
|
|
const complete = createOrgChartExampleDataset(base, { scope: 'total', employeeCount });
|
|
const selected = createOrgChartExampleDataset(base, { scope: 'total', employeeCount, companyIds: ['saman', 'hanmac'] });
|
|
assertContract(selected);
|
|
assert.deepEqual(selected.employees, complete.employees.filter(employee => ['hanmac', 'saman'].includes(employee.affiliationId)));
|
|
assert.deepEqual(selected.organizations.map(org => org.id), complete.organizations.filter(org => ['hanmac', 'saman'].includes(org.companyId!)).map(org => org.id));
|
|
}
|
|
});
|
|
|
|
test('total 300 reuses every P5-3 large employee record and the original organization IDs', () => {
|
|
const dataset = createOrgChartExampleDataset(base, { scope: 'total', employeeCount: 300 });
|
|
const originalById = new Map(base.employees.map(employee => [employee.id, employee]));
|
|
assert.equal(dataset.employees.length, base.employees.length);
|
|
for (const employee of dataset.employees) assert.deepEqual(employee, originalById.get(employee.id));
|
|
assert.deepEqual(dataset.organizations, base.organizations);
|
|
});
|
|
|
|
test('selections are deterministic sets and snapshots never mutate the source or another result', async () => {
|
|
const before = structuredClone(base);
|
|
const frozen = snapshotOrgChartDataset(base);
|
|
const first = createOrgChartExampleDataset(frozen, { scope: 'per-company', employeeCount: 100, companyIds: ['saman', 'hanmac', 'saman'] });
|
|
const equivalent = createOrgChartExampleDataset(frozen, { scope: 'per-company', employeeCount: 100, companyIds: ['hanmac', 'saman'] });
|
|
assert.deepEqual(first, equivalent);
|
|
assert.notEqual(first, equivalent);
|
|
assert.notEqual(first.employees[0], equivalent.employees[0]);
|
|
assert.ok(Object.isFrozen(first) && Object.isFrozen(first.organizations) && Object.isFrozen(first.employees[0]));
|
|
assert.throws(() => { first.employees[0]!.name = 'changed'; }, TypeError);
|
|
assert.deepEqual(base, before);
|
|
assert.deepEqual(await new JsonOrgChartDataSource(first).load(), first);
|
|
const single = createOrgChartExampleDataset(base, { scope: 'per-company', employeeCount: 100, companyIds: ['saman'] });
|
|
assert.deepEqual(single.employees, first.employees.filter(employee => employee.affiliationId === 'saman'));
|
|
});
|
|
|
|
test('expansion resolves source IDs that already use the generated employee namespace', () => {
|
|
const template = structuredClone(base);
|
|
const employee = template.employees.find(employee => employee.affiliationId === 'hanmac' && !template.organizations.some(org => org.leaderId === employee.id))!;
|
|
employee.id = 'p7-virtual-hanmac-050';
|
|
const dataset = createOrgChartExampleDataset(template, { scope: 'per-company', employeeCount: 100 });
|
|
assertContract(dataset);
|
|
assert.ok(dataset.employees.some(employee => employee.id === 'p7-virtual-hanmac-050-example'));
|
|
});
|
|
|
|
test('invalid scale, company IDs, empty view selection, and incomplete templates fail explicitly', () => {
|
|
assert.throws(() => createOrgChartExampleDataset(base, { scope: 'invalid' as 'total', employeeCount: 20 }), /scope/);
|
|
assert.throws(() => createOrgChartExampleDataset(base, { scope: 'total', employeeCount: 99 as 20 }), /employeeCount/);
|
|
assert.throws(() => createOrgChartExampleDataset(base, { scope: 'total', employeeCount: 20, companyIds: ['unknown' as CompanyId] }), /presets/);
|
|
assert.throws(() => createOrgChartExampleDataset(base, { scope: 'total', employeeCount: 20, companyIds: [] }), /empty view selection/);
|
|
const incomplete = structuredClone(base);
|
|
incomplete.employees = incomplete.employees.filter(employee => employee.affiliationId !== 'baron');
|
|
incomplete.organizations.filter(org => org.companyId === 'baron').forEach(org => { org.leaderId = null; });
|
|
assert.throws(() => createOrgChartExampleDataset(incomplete, { scope: 'total', employeeCount: 20 }), /all seven/);
|
|
});
|