fix: edge-disconnections-are-not-reflected-in-reduced-program

This commit is contained in:
Gerla, J. (Justin)
2025-12-14 21:56:18 +00:00
committed by JobvAlewijk
parent 8149d67491
commit 58ab95eee1
15 changed files with 639 additions and 108 deletions

View File

@@ -1,4 +1,7 @@
import {act} from '@testing-library/react';
import type {Connection, Edge, Node} from "@xyflow/react";
import { NodeDisconnections } from "../../../../src/pages/VisProgPage/visualProgrammingUI/NodeRegistry.ts";
import type {PhaseNodeData} from "../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/PhaseNode.tsx";
import useFlowStore from '../../../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx';
import { mockReactFlow } from '../../../setupFlowTests.ts';
@@ -6,18 +9,187 @@ beforeAll(() => {
mockReactFlow();
});
// default state values for testing,
const normNode: Node = {
id: 'norm-1',
type: 'norm',
position: { x: 0, y: 0 },
data: {
label: 'Test Norm',
droppable: true,
norm: 'Test',
hasReduce: true,
},
};
const phaseNode: Node = {
id: 'phase-1',
type: 'phase',
position: { x: 100, y: 0 },
data: {
label: 'Phase 1',
droppable: true,
children: ["norm-1"],
hasReduce: true,
},
};
const testEdge: Edge = {
id: 'xy-edge__1-2',
source: 'norm-1',
target: 'phase-1',
sourceHandle: null,
targetHandle: null,
}
const testStateReconnectEnd = {
nodes: [phaseNode, normNode],
edges: [testEdge],
}
const phaseNodeUnconnected = {
id: 'phase-2',
type: 'phase',
position: { x: 100, y: 0 },
data: {
label: 'Phase 2',
droppable: true,
children: [],
hasReduce: true,
},
};
const testConnection: Connection = {
source: 'norm-1',
target: 'phase-2',
sourceHandle: null,
targetHandle: null,
}
const testStateOnConnect = {
nodes: [phaseNodeUnconnected, normNode],
edges: [],
}
describe('FlowStore Functionality', () => {
describe('Node changes', () => {
// currently just using a single function from the ReactFlow library,
// so testing would mean we are testing already tested behavior.
// if implementation gets modified tests should be added for custom behavior
});
describe('ReactFlow onEdgesDelete', () => {
test('Deleted edge is reflected in removed phaseNode child', () => {
const {onEdgesDelete} = useFlowStore.getState();
useFlowStore.setState({
nodes: [{
id: 'phase-1',
type: 'phase',
position: { x: 100, y: 0 },
data: {
label: 'Phase 1',
droppable: true,
children: ["norm-1"],
hasReduce: true,
},
},{
id: 'norm-1',
type: 'norm',
position: { x: 0, y: 0 },
data: {
label: 'Test Norm',
droppable: true,
norm: 'Test',
hasReduce: true,
},
}],
edges: [], // edges is empty as onEdgesDelete is triggered after the edges are deleted
})
act(() => {
onEdgesDelete([testEdge])
});
const outcome = useFlowStore.getState();
expect((outcome.nodes[0].data as PhaseNodeData).children.length).toBe(0);
})
test('Deleted edge is reflected in phaseNode,even if normNode was already deleted and caused edge removal', () => {
const { onEdgesDelete } = useFlowStore.getState();
useFlowStore.setState({
nodes: [{
id: 'phase-1',
type: 'phase',
position: { x: 100, y: 0 },
data: {
label: 'Phase 1',
droppable: true,
children: ["norm-1"],
hasReduce: true,
},
}],
edges: [], // edges is empty as onEdgesDelete is triggered after the edges are deleted
})
act(() => {
onEdgesDelete([testEdge]);
})
const outcome = useFlowStore.getState();
expect((outcome.nodes[0].data as PhaseNodeData).children.length).toBe(0);
})
test('edge removal resulting from deletion of targetNode calls only the connection function for the sourceNode', () => {
const { onEdgesDelete } = useFlowStore.getState();
useFlowStore.setState({
nodes: [{
id: 'norm-1',
type: 'norm',
position: { x: 0, y: 0 },
data: {
label: 'Test Norm',
droppable: true,
norm: 'Test',
hasReduce: true,
},
}],
edges: [], // edges is empty as onEdgesDelete is triggered after the edges are deleted
})
const targetDisconnectSpy = jest.spyOn(NodeDisconnections.Targets, 'phase');
const sourceDisconnectSpy = jest.spyOn(NodeDisconnections.Sources, 'norm');
act(() => {
onEdgesDelete([testEdge]);
})
expect(sourceDisconnectSpy).toHaveBeenCalledWith(normNode, 'phase-1');
expect(targetDisconnectSpy).not.toHaveBeenCalled();
sourceDisconnectSpy.mockRestore();
targetDisconnectSpy.mockRestore();
})
})
describe('Edge changes', () => {
// currently just using a single function from the ReactFlow library,
// so testing would mean we are testing already tested behavior.
// if implementation gets modified tests should be added for custom behavior
})
describe('ReactFlow onConnect', () => {
test('Adds connecting node to children of phaseNode', () => {
const {onConnect} = useFlowStore.getState();
useFlowStore.setState({
nodes: testStateOnConnect.nodes,
edges: testStateOnConnect.edges
})
act(() => {
onConnect(testConnection);
})
const outcome = useFlowStore.getState();
// phaseNode adds the normNode to its children
expect((outcome.nodes[0].data as PhaseNodeData).children).toEqual(['norm-1']);
})
test('adds an edge when onConnect is triggered', () => {
const {onConnect} = useFlowStore.getState();
@@ -39,6 +211,53 @@ describe('FlowStore Functionality', () => {
});
});
describe('ReactFlow onReconnect', () => {
test('PhaseNodes correctly change their children', () => {
const {onReconnect} = useFlowStore.getState();
useFlowStore.setState({
nodes: [{
id: 'phase-1',
type: 'phase',
position: { x: 100, y: 0 },
data: {
label: 'Phase 1',
droppable: true,
children: ["norm-1"],
hasReduce: true,
},
},{
id: 'phase-2',
type: 'phase',
position: { x: 100, y: 0 },
data: {
label: 'Phase 2',
droppable: true,
children: [],
hasReduce: true,
},
},{
id: 'norm-1',
type: 'norm',
position: { x: 0, y: 0 },
data: {
label: 'Test Norm',
droppable: true,
norm: 'Test',
hasReduce: true,
},
}],
edges: [testEdge],
})
act(() => {
onReconnect(testEdge, testConnection);
})
const outcome = useFlowStore.getState();
// phaseNodes lose and gain children when norm node's connection is changed from phaseNode to PhaseNodeUnconnected
expect((outcome.nodes[1].data as PhaseNodeData).children).toEqual(['norm-1']);
expect((outcome.nodes[0].data as PhaseNodeData).children).toEqual([]);
})
test('reconnects an existing edge when onReconnect is triggered', () => {
const {onReconnect} = useFlowStore.getState();
const oldEdge = {
@@ -93,36 +312,63 @@ describe('FlowStore Functionality', () => {
);
});
test('successfully removes edge if no successful reconnect occurred', () => {
const {onReconnectEnd} = useFlowStore.getState();
useFlowStore.setState({edgeReconnectSuccessful: false});
useFlowStore.setState({
edgeReconnectSuccessful: false,
edges: testStateReconnectEnd.edges,
nodes: testStateReconnectEnd.nodes
});
act(() => {
onReconnectEnd(null, {id: 'xy-edge__A-B'});
onReconnectEnd(null, testEdge);
});
const updatedState = useFlowStore.getState();
expect(updatedState.edgeReconnectSuccessful).toBe(true);
expect(updatedState.edges).toHaveLength(0);
expect(updatedState.nodes[0].data.children).toEqual([]);
});
test('does not remove reconnecting edge if successful reconnect occurred', () => {
const {onReconnectEnd} = useFlowStore.getState();
useFlowStore.setState({
edgeReconnectSuccessful: true,
edges: [testEdge],
nodes: [{
id: 'phase-1',
type: 'phase',
position: { x: 100, y: 0 },
data: {
label: 'Phase 1',
droppable: true,
children: ["norm-1"],
hasReduce: true,
},
},{
id: 'norm-1',
type: 'norm',
position: { x: 0, y: 0 },
data: {
label: 'Test Norm',
droppable: true,
norm: 'Test',
hasReduce: true,
},
}]
});
act(() => {
onReconnectEnd(null, {id: 'xy-edge__A-B'});
onReconnectEnd(null, testEdge);
});
const updatedState = useFlowStore.getState();
expect(updatedState.edgeReconnectSuccessful).toBe(true);
expect(updatedState.edges).toHaveLength(1);
expect(updatedState.edges).toMatchObject([
{
id: 'xy-edge__A-B',
source: 'A',
target: 'B'
}]
);
expect(updatedState.edges).toMatchObject([testEdge]);
expect(updatedState.nodes[0].data.children).toEqual(["norm-1"]);
});
});
describe('ReactFlow deleteNode', () => {