tierLayout.test.ts5.0 KBView on GitHub /**
* The fallback shape for a graph whose edges do not give it one.
*
* Every assertion here traces to one measured failure: a real 37-person chart with no reporting
* lines laid out as a single row 9,380px wide, which `fitView` rendered at ~8% zoom — two
* adjacent people read as one node. These tests are what stop that coming back.
*/
import { CARD_HEIGHT, CARD_WIDTH } from '@/modules/graph/layout-metrics';
import { tierLayout, tierOf } from '@/modules/graph/tierLayout';
import type { GraphNode, GraphNodeField } from '@zero/server/graph';
const SENIORITY: GraphNodeField = {
key=[redacted],
label: 'Seniority',
type: 'select',
options: ['c_level', 'vp', 'director', 'manager', 'ic'],
};
function node(id: string, seniority?: string, extra: Partial<GraphNode> = {}): GraphNode {
return {
id,
documentId: `doc_${id}`,
title: id,
fields: seniority ? { seniority } : {},
...extra,
};
}
const labelOf = (id: string) => id;
describe('tierOf', () => {
it("uses the field's own OPTION ORDER, so the schema author sets the vertical order", () => {
expect(tierOf(node('a', 'c_level'), SENIORITY)).toBe(0);
expect(tierOf(node('a', 'vp'), SENIORITY)).toBe(1);
expect(tierOf(node('a', 'ic'), SENIORITY)).toBe(4);
});
it('puts an UNRECORDED value BELOW every declared tier, never at the top', () => {
// "We have not recorded this person's seniority" must not read as "this person is the CEO".
expect(tierOf(node('a'), SENIORITY)).toBe(5);
expect(tierOf(node('a', 'founder'), SENIORITY)).toBe(5);
expect(tierOf(node('a', ''), SENIORITY)).toBe(5);
});
it('reads a BOUND field from the derived cache, like the node face does', () => {
const bound: GraphNodeField = { ...SENIORITY, from: 'person.seniority' };
expect(tierOf({ fields: {}, derived: { seniority: 'vp' } }, bound)).toBe(1);
});
it('takes the first value of a multi_select', () => {
expect(tierOf({ fields: { seniority: ['vp', 'ic'] } }, SENIORITY)).toBe(1);
});
it('puts everything in one tier when no field is named', () => {
expect(tierOf(node('a', 'vp'), undefined)).toBe(0);
});
});
describe('tierLayout', () => {
it('STACKS nodes instead of laying them in one row — the 9,380px bug', () => {
const nodes = Array.from({ length: 37 }, (_, i) =>
node(`n${i}`, ['c_level', 'vp', 'director', 'manager', 'ic'][i % 5]),
);
const pos = tierLayout({ nodes, tierField: SENIORITY, labelOf });
const ys = new Set(Object.values(pos).map((p) => p.y));
expect(ys.size).toBe(5);
const width =
Math.max(...Object.values(pos).map((p) => p.x)) -
Math.min(...Object.values(pos).map((p) => p.x));
// The old behaviour was 9,380px across. Eight per row is an order of magnitude narrower.
expect(width).toBeLessThan(3000);
});
it('puts the most senior tier at the TOP', () => {
const pos = tierLayout({
nodes: [node('ceo', 'c_level'), node('eng', 'ic'), node('vp', 'vp')],
tierField: SENIORITY,
labelOf,
});
expect(pos.ceo.y).toBeLessThan(pos.vp.y);
expect(pos.vp.y).toBeLessThan(pos.eng.y);
});
it('CENTRES each row against the widest, so it reads as a tree not a table', () => {
const pos = tierLayout({
nodes: [node('boss', 'c_level'), node('a', 'ic'), node('b', 'ic'), node('c', 'ic')],
tierField: SENIORITY,
labelOf,
});
const bottom = ['a', 'b', 'c'].map((id) => pos[id].x);
const bottomCentre = (Math.min(...bottom) + Math.max(...bottom)) / 2;
expect(pos.boss.x).toBeCloseTo(bottomCentre, 0);
});
it('never overlaps two cards', () => {
const nodes = Array.from({ length: 24 }, (_, i) => node(`n${i}`, i < 3 ? 'vp' : 'ic'));
const pos = tierLayout({ nodes, tierField: SENIORITY, labelOf });
const pts = Object.values(pos);
for (let i = 0; i < pts.length; i += 1) {
for (let j = i + 1; j < pts.length; j += 1) {
// Measured against the card's REAL height, not its `minHeight`. Checking 72px was the
// assumption that let the rows come out nearly touching in the first place.
const clash =
Math.abs(pts[i].x - pts[j].x) < CARD_WIDTH && Math.abs(pts[i].y - pts[j].y) < CARD_HEIGHT;
expect(clash).toBe(false);
}
}
});
it('is deterministic — two calls agree, so a re-render does not reshuffle', () => {
const nodes = [node('zoe', 'vp'), node('adam', 'vp'), node('mia', 'ic')];
expect(tierLayout({ nodes, tierField: SENIORITY, labelOf })).toEqual(
tierLayout({ nodes, tierField: SENIORITY, labelOf }),
);
});
it('orders within a tier by LABEL, not by insertion', () => {
const pos = tierLayout({
nodes: [node('zoe', 'vp'), node('adam', 'vp')],
tierField: SENIORITY,
labelOf,
});
expect(pos.adam.x).toBeLessThan(pos.zoe.x);
});
it('still lays out a graph whose field is absent — one row, honestly', () => {
const pos = tierLayout({ nodes: [node('a'), node('b')], labelOf });
expect(pos.a.y).toBe(pos.b.y);
expect(Object.keys(pos)).toHaveLength(2);
});
});