tree-edge.test.ts4.7 KBView on GitHub /**
* The family-tree line.
*
* These are assertions about GEOMETRY, not about a path string, so each one reads the numbers
* back out of the `d` attribute. The load-bearing property is the first describe: every child
* of one parent turns on the SAME y. It is the thing four different card heights used to break,
* and it is invisible in a unit test that only checks one edge at a time.
*/
import { getTreeEdgePath } from '@/modules/graph/tree-edge';
const OFFSET = 55;
/** Every `y` the path passes through, in order. */
function ysOf(path: string): number[] {
return [...path.matchAll(/[ML] ?-?[\d.]+,(-?[\d.]+)/g)].map((m) => Number(m[1]));
}
/** The one horizontal run: the y both of its ends share. */
function busYOf(path: string): number {
const ys = ysOf(path);
// The `L …,busY` that precedes the second corner — index 2 of the four straight points.
return ys[2] ?? Number.NaN;
}
describe('every child of one parent turns on the same line', () => {
// A parent whose bottom edge is at y=200, and three children on the rank below whose cards
// are 72, 150 and 210 tall — the spread that made smoothstep draw three separate runs.
const parentBottom = 200;
const childTop = 400;
const edges = [
getTreeEdgePath(
{ sourceX: 100, sourceY: childTop, targetX: 500, targetY: parentBottom },
{ offset: OFFSET },
),
getTreeEdgePath(
{ sourceX: 500, sourceY: childTop, targetX: 500, targetY: parentBottom },
{ offset: OFFSET },
),
getTreeEdgePath(
{ sourceX: 900, sourceY: childTop, targetX: 500, targetY: parentBottom },
{ offset: OFFSET },
),
];
it('puts the bus a fixed distance below the PARENT, not midway between each pair', () => {
const [left, , right] = edges;
expect(busYOf(left![0])).toBe(parentBottom + OFFSET);
expect(busYOf(right![0])).toBe(parentBottom + OFFSET);
});
it('agrees across siblings — which is the whole point', () => {
const [left, , right] = edges;
expect(busYOf(left![0])).toBe(busYOf(right![0]));
});
it('hangs the label on the bus, centred on the run', () => {
const [, labelX, labelY] = edges[0]!;
expect(labelX).toBe(300);
expect(labelY).toBe(parentBottom + OFFSET);
});
});
describe('a child directly beneath its parent', () => {
it('draws ONE straight segment — no jog out and back', () => {
const [path] = getTreeEdgePath(
{ sourceX: 500, sourceY: 400, targetX: 500, targetY: 200 },
{ offset: OFFSET },
);
expect(path).toBe('M 500,400 L 500,200');
});
it('tolerates the sub-pixel drift of two measured DOM boxes', () => {
// Card centres come off `measured` widths and land on fractions. Half a pixel of
// disagreement is not a reason to draw two corners.
const [path] = getTreeEdgePath(
{ sourceX: 500.4, sourceY: 400, targetX: 500, targetY: 200 },
{ offset: OFFSET },
);
expect(path).not.toContain('Q');
});
});
describe('direction', () => {
it('routes the same way when the SOURCE is the upper card', () => {
// A dragged node can put either end on top; `getFloatingEdgeParams` flips the anchors and
// the bus still belongs to whichever card is above.
const [path] = getTreeEdgePath(
{ sourceX: 500, sourceY: 200, targetX: 900, targetY: 400 },
{ offset: OFFSET },
);
expect(busYOf(path)).toBe(200 + OFFSET);
});
it('starts at the source anchor and ends at the target anchor, either way round', () => {
const [path] = getTreeEdgePath(
{ sourceX: 100, sourceY: 400, targetX: 500, targetY: 200 },
{ offset: OFFSET },
);
expect(path.startsWith('M 100,400')).toBe(true);
expect(path.endsWith('L 500,200')).toBe(true);
});
});
describe('cramped pairs', () => {
it('never routes the bus past the lower card', () => {
// Two cards dragged almost on top of each other: a 55px offset would put the horizontal run
// below the child it is connecting to, drawing a line through the card.
const [path] = getTreeEdgePath(
{ sourceX: 100, sourceY: 210, targetX: 500, targetY: 200 },
{ offset: OFFSET },
);
expect(busYOf(path)).toBe(210);
});
it('shrinks its corners rather than overshooting a short run', () => {
// A 4px horizontal run cannot hold two 8px corners. Every x in the path must stay inside
// the two endpoints, or the line visibly bulges past the cards it joins.
const [path] = getTreeEdgePath(
{ sourceX: 100, sourceY: 400, targetX: 104, targetY: 200 },
{ offset: OFFSET },
);
const xs = [...path.matchAll(/(-?[\d.]+),-?[\d.]+/g)].map((m) => Number(m[1]));
expect(Math.min(...xs)).toBeGreaterThanOrEqual(100);
expect(Math.max(...xs)).toBeLessThanOrEqual(104);
});
});