feat(org-chart): add core cards and fixed-layout integration
This commit is contained in:
1 parent
d18e3c3380
commit
61c6f8d1f1
161 files changed
+13044
-1
No files matched your search
@@ -0,0 +1,133 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { selectAffiliationSummary, selectMajorOrganizationTotals, selectOrganizationPositionCounts, selectStatusGroups } from './content-selectors.js';
|
||||
import { calculateSummary } from './summary.js';
|
||||
import type { OrgChartDataset } from './types.js';
|
||||
|
||||
function fixture(): OrgChartDataset {
|
||||
return {
|
||||
schemaVersion: '0.1-draft',
|
||||
organizations: [
|
||||
{ id: 'root', parentId: null, name: '가상 본부', type: 'root', leaderId: 'leader', displayOrder: 0 },
|
||||
{ id: 'team', parentId: 'root', name: '가상 팀', type: 'team', leaderId: null, displayOrder: 1 },
|
||||
{ id: 'empty', parentId: 'root', name: '빈 가상 팀', type: 'team', leaderId: null, displayOrder: 2 },
|
||||
],
|
||||
employees: [
|
||||
{ id: 'leader', organizationId: 'root', affiliationId: 'first', name: '가상 가', position: '대표', jobTitle: '본부장', profileImage: null, workStatus: 'active' },
|
||||
{ id: 'sent', organizationId: 'team', affiliationId: 'second', name: '가상 나', position: '책임', jobTitle: null, profileImage: null, workStatus: 'dispatched' },
|
||||
{ id: 'leave', organizationId: 'team', affiliationId: 'first', name: '가상 다', position: '책임', jobTitle: null, profileImage: null, workStatus: 'onLeave' },
|
||||
],
|
||||
affiliations: [{ id: 'first', name: '가상 관계사 가' }, { id: 'second', name: '가상 관계사 나' }, { id: 'zero', name: '가상 관계사 다' }],
|
||||
metadata: { asOfDate: '2026-10-12', dataVersion: 'selector-test' },
|
||||
};
|
||||
}
|
||||
|
||||
test('position matrix uses direct primary membership, includes leaders and all work statuses', () => {
|
||||
const data = fixture();
|
||||
const matrix = selectOrganizationPositionCounts(data);
|
||||
const summary = calculateSummary(data);
|
||||
assert.deepEqual([...matrix.get('root')!], [['대표', 1]]);
|
||||
assert.deepEqual([...matrix.get('team')!], [['책임', 2]]);
|
||||
assert.equal(matrix.get('empty')!.size, 0);
|
||||
assert.equal(matrix.get('root')!.get('책임'), undefined);
|
||||
assert.equal(summary.raw.byOrganization.root!.inclusive, 3);
|
||||
for (const organization of data.organizations) {
|
||||
assert.equal([...matrix.get(organization.id)!.values()].reduce((sum, count) => sum + count, 0), summary.raw.byOrganization[organization.id]!.direct);
|
||||
}
|
||||
for (const [position, count] of Object.entries(summary.raw.byPosition)) {
|
||||
assert.equal([...matrix.values()].reduce((sum, row) => sum + (row.get(position) ?? 0), 0), count);
|
||||
}
|
||||
assert.equal([...matrix.values()].flatMap(row => [...row.values()]).reduce((sum, count) => sum + count, 0), summary.raw.total);
|
||||
assert.equal(summary.aggregation.status, 'unavailable');
|
||||
});
|
||||
|
||||
test('duplicate IDs cannot inflate matrix or status groups and original Employee references survive', () => {
|
||||
const data = fixture();
|
||||
data.employees.push({ ...data.employees[1]!, organizationId: 'root', position: '대표' });
|
||||
const summary = calculateSummary(data);
|
||||
const matrix = selectOrganizationPositionCounts(data);
|
||||
assert.equal(matrix.get('team')!.get('책임'), 2);
|
||||
assert.equal(matrix.get('root')!.get('대표'), 1);
|
||||
const groups = selectStatusGroups(data, summary);
|
||||
assert.equal(groups[0]!.employees.length, 1);
|
||||
assert.strictEqual(groups[0]!.employees[0], data.employees[1]);
|
||||
assert.strictEqual(groups[1]!.employees[0], data.employees[2]);
|
||||
assert.equal(summary.raw.total, 3);
|
||||
});
|
||||
|
||||
test('opaque prototype-like organization IDs and position strings remain ordinary matrix keys', () => {
|
||||
const data = fixture();
|
||||
data.organizations[1]!.id = '__proto__';
|
||||
data.employees[1]!.organizationId = '__proto__';
|
||||
data.employees[2]!.organizationId = '__proto__';
|
||||
data.employees[1]!.position = 'constructor';
|
||||
data.employees[2]!.position = '__proto__';
|
||||
const row = selectOrganizationPositionCounts(data).get('__proto__')!;
|
||||
assert.equal(row.get('constructor'), 1);
|
||||
assert.equal(row.get('__proto__'), 1);
|
||||
});
|
||||
|
||||
test('affiliation summary preserves declared order, zero entries and original affiliation objects', () => {
|
||||
const data = fixture();
|
||||
const summary = calculateSummary(data);
|
||||
const rows = selectAffiliationSummary(data, summary);
|
||||
assert.deepEqual(rows.map(row => [row.affiliation.id, row.count]), [['first', 2], ['second', 1], ['zero', 0]]);
|
||||
assert.strictEqual(rows[2]!.affiliation, data.affiliations[2]);
|
||||
assert.equal(rows.reduce((sum, row) => sum + row.count, 0), summary.raw.total);
|
||||
});
|
||||
|
||||
test('status flags independently hide groups without changing Raw Count or affiliation totals', () => {
|
||||
const data = fixture();
|
||||
const summary = calculateSummary(data);
|
||||
data.presentation = {
|
||||
title: '가상 조직도', description: null, hubText: null, organizationOrder: [],
|
||||
groups: { showEmployees: false, showSummary: false, showAffiliations: false, showDispatched: false, showOnLeave: true },
|
||||
};
|
||||
assert.deepEqual(selectStatusGroups(data, summary).map(group => group.status), ['onLeave']);
|
||||
data.presentation.groups.showDispatched = true;
|
||||
data.presentation.groups.showOnLeave = false;
|
||||
assert.deepEqual(selectStatusGroups(data, summary).map(group => group.status), ['dispatched']);
|
||||
data.presentation.groups.showDispatched = false;
|
||||
assert.deepEqual(selectStatusGroups(data, summary), []);
|
||||
assert.deepEqual(calculateSummary(data), summary);
|
||||
assert.equal(selectAffiliationSummary(data, summary).length, 3);
|
||||
});
|
||||
|
||||
test('zero employees retains all organization/affiliation rows and both empty status groups', () => {
|
||||
const data = fixture();
|
||||
data.employees = [];
|
||||
data.organizations[0]!.leaderId = null;
|
||||
const summary = calculateSummary(data);
|
||||
assert.equal(selectOrganizationPositionCounts(data).size, 3);
|
||||
assert.ok([...selectOrganizationPositionCounts(data).values()].every(row => row.size === 0));
|
||||
assert.ok(selectAffiliationSummary(data, summary).every(row => row.count === 0));
|
||||
assert.deepEqual(selectStatusGroups(data, summary).map(group => [group.status, group.count, group.employees.length]), [['dispatched', 0, 0], ['onLeave', 0, 0]]);
|
||||
});
|
||||
|
||||
test('new JSON snapshot updates matrix, affiliations and status groups without a cached count', () => {
|
||||
const data = fixture();
|
||||
const before = calculateSummary(data);
|
||||
data.employees[1]!.organizationId = 'root';
|
||||
data.employees[1]!.position = '수석';
|
||||
data.employees[1]!.affiliationId = 'zero';
|
||||
data.employees[1]!.workStatus = 'active';
|
||||
const after = calculateSummary(data);
|
||||
const matrix = selectOrganizationPositionCounts(data);
|
||||
assert.equal(matrix.get('root')!.get('수석'), 1);
|
||||
assert.equal(matrix.get('team')!.get('책임'), 1);
|
||||
assert.deepEqual(selectAffiliationSummary(data, after).map(row => row.count), [2, 0, 1]);
|
||||
assert.equal(selectStatusGroups(data, after)[0]!.employees.length, 0);
|
||||
assert.equal(before.raw.byStatus.dispatched, 1);
|
||||
assert.equal(after.raw.total, before.raw.total);
|
||||
});
|
||||
|
||||
test('major organization totals: root direct members then inclusive root children, summing to the raw total', () => {
|
||||
const data = fixture();
|
||||
data.organizations.push({ id: 'cell', parentId: 'team', name: '가상 셀', type: 'cell', leaderId: null, displayOrder: 3 });
|
||||
data.employees.push({ id: 'member', organizationId: 'cell', affiliationId: 'zero', name: '가상 라', position: '선임', jobTitle: null, profileImage: null, workStatus: 'active' });
|
||||
const summary = calculateSummary(data);
|
||||
const rows = selectMajorOrganizationTotals(data, summary);
|
||||
assert.deepEqual(rows.map(row => [row.organization.id, row.scope, row.count]), [['root', 'direct', 1], ['team', 'inclusive', 3], ['empty', 'inclusive', 0]]);
|
||||
assert.equal(rows.reduce((sum, row) => sum + row.count, 0), summary.raw.total);
|
||||
assert.equal(rows.some(row => row.organization.id === 'cell'), false);
|
||||
});
|
||||
Reference in new issue
Block a user