@ichigo_san/graphing
Version:
A lightweight UML-style diagram editor built with React Flow and Tailwind CSS
434 lines (357 loc) • 12.9 kB
JavaScript
import {
// Route Patterns
L_PATTERNS,
S_PATTERNS,
getLPatterns,
getSPatterns,
getAllPatterns,
isLPattern,
isSPattern,
// Routing Algorithms
calculateOrthogonalRoute,
calculatePatternRoute,
calculateStraightRoute,
snapPointToGrid,
calculateRouteWithObstacles,
hasCollisions,
segmentIntersectsRect,
linesIntersect,
// Routing Pipeline
routeOrthogonalEdge,
validateRoutingInputs,
generateRoutingMetadata,
calculateRouteDistance,
updateRoute,
hasNodesMovedSignificantly,
optimizeRoute,
createRoutingContext
} from '../moduleC.js';
import { SIDES } from '../edgeTypes.js';
describe('Module C - Routing Pipeline', () => {
// Test data
const mockSourceNode = {
id: 'source',
position: { x: 100, y: 100 },
width: 80,
height: 60,
shape: 'rect'
};
const mockTargetNode = {
id: 'target',
position: { x: 300, y: 200 },
width: 100,
height: 80,
shape: 'rect'
};
const mockTransform = {
x: 0,
y: 0,
k: 1
};
const mockGridConfig = {
enabled: true,
size: 20,
tolerance: 5
};
describe('Route Patterns', () => {
test('L_PATTERNS should contain patterns for all side combinations', () => {
expect(L_PATTERNS[SIDES.NORTH]).toBeDefined();
expect(L_PATTERNS[SIDES.SOUTH]).toBeDefined();
expect(L_PATTERNS[SIDES.EAST]).toBeDefined();
expect(L_PATTERNS[SIDES.WEST]).toBeDefined();
});
test('S_PATTERNS should contain patterns for same-side connections', () => {
expect(S_PATTERNS[SIDES.NORTH][SIDES.NORTH]).toBeDefined();
expect(S_PATTERNS[SIDES.SOUTH][SIDES.SOUTH]).toBeDefined();
expect(S_PATTERNS[SIDES.EAST][SIDES.EAST]).toBeDefined();
expect(S_PATTERNS[SIDES.WEST][SIDES.WEST]).toBeDefined();
});
test('getLPatterns should return patterns for valid side combinations', () => {
const patterns = getLPatterns(SIDES.NORTH, SIDES.SOUTH);
expect(patterns).toHaveLength(1);
expect(patterns[0].name).toBe('L-NS');
});
test('getSPatterns should return patterns for same-side connections', () => {
const patterns = getSPatterns(SIDES.NORTH, SIDES.NORTH);
expect(patterns).toHaveLength(1);
expect(patterns[0].name).toBe('S-NN');
});
test('getAllPatterns should return both L and S patterns', () => {
const patterns = getAllPatterns(SIDES.NORTH, SIDES.SOUTH);
expect(patterns.length).toBeGreaterThan(0);
});
test('isLPattern should correctly identify L patterns', () => {
const lPattern = { name: 'L-NS' };
const sPattern = { name: 'S-NN' };
expect(isLPattern(lPattern)).toBe(true);
expect(isLPattern(sPattern)).toBe(false);
});
test('isSPattern should correctly identify S patterns', () => {
const lPattern = { name: 'L-NS' };
const sPattern = { name: 'S-NN' };
expect(isSPattern(sPattern)).toBe(true);
expect(isSPattern(lPattern)).toBe(false);
});
});
describe('Routing Algorithms', () => {
test('calculateStraightRoute should return simple two-point route', () => {
const sourcePoint = { x: 0, y: 0 };
const targetPoint = { x: 100, y: 100 };
const result = calculateStraightRoute(sourcePoint, targetPoint);
expect(result.success).toBe(true);
expect(result.points).toHaveLength(2);
expect(result.pattern).toBe('straight');
});
test('snapPointToGrid should snap points within tolerance', () => {
const point = { x: 35, y: 45 };
const gridConfig = { size: 20, tolerance: 5 };
const snapped = snapPointToGrid(point, gridConfig);
expect(snapped.x).toBe(40);
expect(snapped.y).toBe(40);
});
test('snapPointToGrid should not snap points outside tolerance', () => {
const point = { x: 35, y: 45 };
const gridConfig = { size: 20, tolerance: 2 };
const snapped = snapPointToGrid(point, gridConfig);
expect(snapped.x).toBe(35);
expect(snapped.y).toBe(45);
});
test('linesIntersect should detect intersection', () => {
const line1 = { x1: 0, y1: 0, x2: 10, y2: 10 };
const line2 = { x1: 0, y1: 10, x2: 10, y2: 0 };
// These lines should intersect at (5, 5)
expect(linesIntersect(line1, line2)).toBe(true);
});
test('linesIntersect should detect intersection with different test data', () => {
const line1 = { x1: 0, y1: 0, x2: 5, y2: 5 };
const line2 = { x1: 0, y1: 5, x2: 5, y2: 0 };
expect(linesIntersect(line1, line2)).toBe(true);
});
test('linesIntersect should detect intersection with simpler test data', () => {
const line1 = { x1: 0, y1: 0, x2: 2, y2: 2 };
const line2 = { x1: 0, y1: 2, x2: 2, y2: 0 };
expect(linesIntersect(line1, line2)).toBe(true);
});
test('linesIntersect should not detect intersection for parallel lines', () => {
const line1 = { x1: 0, y1: 0, x2: 10, y2: 0 };
const line2 = { x1: 0, y1: 10, x2: 10, y2: 10 };
expect(linesIntersect(line1, line2)).toBe(false);
});
test('segmentIntersectsRect should detect intersection', () => {
const segment = { x1: 0, y1: 0, x2: 10, y2: 10 };
const rect = { x: 5, y: 5, width: 10, height: 10 };
expect(segmentIntersectsRect(segment, rect)).toBe(true);
});
test('hasCollisions should detect route collisions', () => {
const points = [
{ x: 0, y: 0 },
{ x: 10, y: 0 },
{ x: 10, y: 10 }
];
const obstacles = [
{ x: 5, y: 5, width: 10, height: 10 }
];
expect(hasCollisions(points, obstacles)).toBe(true);
});
test('hasCollisions should not detect collisions when none exist', () => {
const points = [
{ x: 0, y: 0 },
{ x: 10, y: 0 },
{ x: 10, y: 10 }
];
const obstacles = [
{ x: 20, y: 20, width: 10, height: 10 }
];
expect(hasCollisions(points, obstacles)).toBe(false);
});
});
describe('Routing Pipeline', () => {
test('validateRoutingInputs should validate correct inputs', () => {
const context = {
sourceNode: mockSourceNode,
targetNode: mockTargetNode,
transform: mockTransform
};
const result = validateRoutingInputs(context);
expect(result.valid).toBe(true);
});
test('validateRoutingInputs should reject invalid inputs', () => {
const context = {
sourceNode: null,
targetNode: mockTargetNode,
transform: mockTransform
};
const result = validateRoutingInputs(context);
expect(result.valid).toBe(false);
expect(result.error).toBeDefined();
});
test('calculateRouteDistance should calculate correct distance', () => {
const points = [
{ x: 0, y: 0 },
{ x: 3, y: 4 },
{ x: 6, y: 8 }
];
const distance = calculateRouteDistance(points);
expect(distance).toBe(10); // 5 + 5
});
test('calculateRouteDistance should return 0 for single point', () => {
const points = [{ x: 0, y: 0 }];
const distance = calculateRouteDistance(points);
expect(distance).toBe(0);
});
test('generateRoutingMetadata should create valid metadata', () => {
const context = {
sourceNode: mockSourceNode,
targetNode: mockTargetNode,
obstacles: [],
avoidObstacles: true
};
const routeResult = {
pattern: 'L-NS',
points: [{ x: 0, y: 0 }, { x: 10, y: 10 }]
};
const modelPoints = [{ x: 0, y: 0 }, { x: 10, y: 10 }];
const metadata = generateRoutingMetadata(context, routeResult, modelPoints);
expect(metadata.totalDistance).toBeGreaterThan(0);
expect(metadata.segmentCount).toBe(1);
expect(metadata.pattern).toBe('L-NS');
expect(metadata.obstacleCount).toBe(0);
expect(metadata.obstacleAvoidance).toBe(true);
expect(metadata.timestamp).toBeDefined();
});
test('hasNodesMovedSignificantly should detect significant movement', () => {
const route = {
success: true,
modelPoints: [
{ x: 100, y: 100 },
{ x: 300, y: 200 }
]
};
const movedSourceNode = { position: { x: 120, y: 100 } };
const movedTargetNode = { position: { x: 300, y: 200 } };
const context = { gridConfig: { tolerance: 5 } };
expect(hasNodesMovedSignificantly(route, movedSourceNode, movedTargetNode, context)).toBe(true);
});
test('hasNodesMovedSignificantly should not detect insignificant movement', () => {
const route = {
success: true,
modelPoints: [
{ x: 100, y: 100 },
{ x: 300, y: 200 }
]
};
const movedSourceNode = { position: { x: 102, y: 100 } };
const movedTargetNode = { position: { x: 300, y: 200 } };
const context = { gridConfig: { tolerance: 5 } };
expect(hasNodesMovedSignificantly(route, movedSourceNode, movedTargetNode, context)).toBe(false);
});
test('optimizeRoute should remove redundant points', () => {
const route = {
success: true,
modelPoints: [
{ x: 0, y: 0 },
{ x: 5, y: 5 },
{ x: 10, y: 10 }
],
viewPoints: [
{ x: 0, y: 0 },
{ x: 5, y: 5 },
{ x: 10, y: 10 }
],
metadata: {}
};
const context = {
transform: mockTransform,
gridConfig: { size: 20 }
};
const optimized = optimizeRoute(route, context);
expect(optimized.modelPoints.length).toBeLessThan(route.modelPoints.length);
expect(optimized.metadata.optimized).toBe(true);
});
test('createRoutingContext should create valid context', () => {
const context = createRoutingContext(
mockSourceNode,
mockTargetNode,
mockTransform,
{
sourcePort: { side: SIDES.NORTH },
targetPort: { side: SIDES.SOUTH },
gridConfig: mockGridConfig,
obstacles: [],
avoidObstacles: true
}
);
expect(context.sourceNode).toBe(mockSourceNode);
expect(context.targetNode).toBe(mockTargetNode);
expect(context.transform).toBe(mockTransform);
expect(context.sourcePort.side).toBe(SIDES.NORTH);
expect(context.targetPort.side).toBe(SIDES.SOUTH);
expect(context.gridConfig).toBe(mockGridConfig);
expect(context.obstacles).toEqual([]);
expect(context.avoidObstacles).toBe(true);
});
test('routeOrthogonalEdge should return valid route result', () => {
const context = createRoutingContext(
mockSourceNode,
mockTargetNode,
mockTransform,
{
sourcePort: { side: SIDES.NORTH },
targetPort: { side: SIDES.SOUTH },
gridConfig: mockGridConfig
}
);
const result = routeOrthogonalEdge(context);
expect(result.success).toBe(true);
expect(result.modelPoints.length).toBeGreaterThan(0);
expect(result.viewPoints.length).toBeGreaterThan(0);
expect(result.pattern).toBeDefined();
expect(result.metadata).toBeDefined();
});
test('routeOrthogonalEdge should handle invalid inputs gracefully', () => {
const context = createRoutingContext(
null, // Invalid source node
mockTargetNode,
mockTransform
);
const result = routeOrthogonalEdge(context);
expect(result.success).toBe(false);
expect(result.modelPoints).toEqual([]);
expect(result.viewPoints).toEqual([]);
expect(result.metadata.error).toBeDefined();
});
test('updateRoute should update route when nodes move significantly', () => {
const originalRoute = {
success: true,
modelPoints: [
{ x: 100, y: 100 },
{ x: 300, y: 200 }
],
viewPoints: [
{ x: 100, y: 100 },
{ x: 300, y: 200 }
],
pattern: 'L-NS',
sides: [SIDES.NORTH, SIDES.SOUTH],
metadata: {}
};
const movedSourceNode = {
...mockSourceNode,
position: { x: 150, y: 150 }
};
const movedTargetNode = {
...mockTargetNode,
position: { x: 350, y: 250 }
};
const context = createRoutingContext(
movedSourceNode,
movedTargetNode,
mockTransform,
{ gridConfig: mockGridConfig }
);
const updatedRoute = updateRoute(originalRoute, movedSourceNode, movedTargetNode, context);
expect(updatedRoute.success).toBe(true);
expect(updatedRoute.modelPoints.length).toBeGreaterThan(0);
});
});
});