fix: incorrect phase reduction order
This commit is contained in:
committed by
Pim Hutting
parent
9c80391fea
commit
9b3414ba98
@@ -3,6 +3,9 @@ import useFlowStore from '../../../../src/pages/VisProgPage/visualProgrammingUI/
|
||||
import { mockReactFlow } from '../../../setupFlowTests.ts';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
beforeAll(() => {
|
||||
mockReactFlow();
|
||||
});
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import type { Node, Edge, Connection } from '@xyflow/react'
|
||||
import useFlowStore from '../../../../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores';
|
||||
import type { PhaseNodeData } from "../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/PhaseNode";
|
||||
import { getByTestId, render } from '@testing-library/react';
|
||||
import type {PhaseNode, PhaseNodeData} from "../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/PhaseNode";
|
||||
import {act, getByTestId, render} from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import VisProgPage from '../../../../../src/pages/VisProgPage/VisProg';
|
||||
import {mockReactFlow} from "../../../../setupFlowTests.ts";
|
||||
|
||||
|
||||
class ResizeObserver {
|
||||
@@ -98,4 +100,195 @@ describe('PhaseNode', () => {
|
||||
expect(p1_data.children.length == 1);
|
||||
expect(p2_data.children.length == 2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// --| Helper functions |--
|
||||
|
||||
function createPhaseNode(
|
||||
id: string,
|
||||
overrides: Partial<PhaseNodeData> = {},
|
||||
): Node<PhaseNodeData> {
|
||||
return {
|
||||
id: id,
|
||||
type: 'phase',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
label: 'Phase',
|
||||
droppable: true,
|
||||
children: [],
|
||||
hasReduce: true,
|
||||
nextPhaseId: null,
|
||||
isFirstPhase: false,
|
||||
...overrides,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function createNode(id: string, type: string): Node {
|
||||
return {
|
||||
id: id,
|
||||
type: type,
|
||||
position: { x: 0, y: 0 },
|
||||
data: {},
|
||||
}
|
||||
}
|
||||
|
||||
function connect(source: string, target: string): Connection {
|
||||
return {
|
||||
source: source,
|
||||
target: target,
|
||||
sourceHandle: null,
|
||||
targetHandle: null
|
||||
};
|
||||
}
|
||||
|
||||
function edge(source: string, target: string): Edge {
|
||||
return {
|
||||
id: `${source}-${target}`,
|
||||
source: source,
|
||||
target: target,
|
||||
}
|
||||
}
|
||||
|
||||
// --| Connection Tests |--
|
||||
|
||||
describe('PhaseNode Connection logic', () => {
|
||||
beforeAll(() => {
|
||||
mockReactFlow();
|
||||
});
|
||||
|
||||
describe('PhaseConnections', () => {
|
||||
test('connecting start => phase sets isFirstPhase to true', () => {
|
||||
const phase = createPhaseNode('phase-1')
|
||||
const start = createNode('start', 'start')
|
||||
|
||||
useFlowStore.setState({ nodes: [phase, start] })
|
||||
|
||||
// verify it starts of false
|
||||
expect(phase.data.isFirstPhase).toBe(false);
|
||||
|
||||
act(() => {
|
||||
useFlowStore.getState().onConnect(connect('start', 'phase-1'))
|
||||
})
|
||||
|
||||
const updatedPhase = useFlowStore
|
||||
.getState()
|
||||
.nodes.find((n) => n.id === 'phase-1') as PhaseNode
|
||||
|
||||
expect(updatedPhase.data.isFirstPhase).toBe(true)
|
||||
})
|
||||
|
||||
test('connecting task => phase adds child', () => {
|
||||
const phase = createPhaseNode('phase-1')
|
||||
const norm = createNode('norm-1', 'norm')
|
||||
|
||||
useFlowStore.setState({ nodes: [phase, norm] })
|
||||
|
||||
act(() => {
|
||||
useFlowStore.getState().onConnect(connect('norm-1', 'phase-1'))
|
||||
})
|
||||
|
||||
const updatedPhase = useFlowStore
|
||||
.getState()
|
||||
.nodes.find((n) => n.id === 'phase-1') as PhaseNode
|
||||
|
||||
expect(updatedPhase.data.children).toEqual(['norm-1'])
|
||||
})
|
||||
|
||||
test('connecting phase => phase sets nextPhaseId', () => {
|
||||
const p1 = createPhaseNode('phase-1')
|
||||
const p2 = createPhaseNode('phase-2')
|
||||
|
||||
useFlowStore.setState({ nodes: [p1, p2] })
|
||||
|
||||
act(() => {
|
||||
useFlowStore.getState().onConnect(connect('phase-1', 'phase-2'))
|
||||
})
|
||||
|
||||
const updatedP1 = useFlowStore
|
||||
.getState()
|
||||
.nodes.find((n) => n.id === 'phase-1') as PhaseNode
|
||||
|
||||
expect(updatedP1.data.nextPhaseId).toBe('phase-2')
|
||||
})
|
||||
|
||||
test('connecting phase to end => phase sets nextPhaseId to "end"', () => {
|
||||
const phase = createPhaseNode('phase-1')
|
||||
const end = createNode('end', 'end')
|
||||
|
||||
useFlowStore.setState({ nodes: [phase, end] })
|
||||
|
||||
act(() => {
|
||||
useFlowStore.getState().onConnect(connect('phase-1', 'end'))
|
||||
})
|
||||
|
||||
const updatedPhase = useFlowStore
|
||||
.getState()
|
||||
.nodes.find((n) => n.id === 'phase-1') as PhaseNode
|
||||
|
||||
expect(updatedPhase.data.nextPhaseId).toBe('end')
|
||||
})
|
||||
})
|
||||
|
||||
describe('PhaseDisconnections', () => {
|
||||
test('disconnecting task => phase removes child', () => {
|
||||
const phase = createPhaseNode('phase-1', { children: ['norm-1'] })
|
||||
const norm = createNode('norm-1', 'norm')
|
||||
|
||||
useFlowStore.setState({
|
||||
nodes: [phase, norm],
|
||||
edges: [edge('norm-1', 'phase-1')]
|
||||
})
|
||||
|
||||
act(() => {
|
||||
useFlowStore.getState().onEdgesDelete([edge('norm-1', 'phase-1')])
|
||||
})
|
||||
|
||||
const updatedPhase = useFlowStore
|
||||
.getState()
|
||||
.nodes.find((n) => n.id === 'phase-1') as PhaseNode
|
||||
|
||||
expect(updatedPhase.data.children).toEqual([])
|
||||
})
|
||||
|
||||
test('disconnecting start => phase sets isFirstPhase to false', () => {
|
||||
const phase = createPhaseNode('phase-1', { isFirstPhase: true })
|
||||
const start = createNode('start', 'start')
|
||||
|
||||
useFlowStore.setState({
|
||||
nodes: [phase, start],
|
||||
edges: [edge('start', 'phase-1')]
|
||||
})
|
||||
|
||||
act(() => {
|
||||
useFlowStore.getState().onEdgesDelete([edge('start', 'phase-1')])
|
||||
})
|
||||
|
||||
const updatedPhase = useFlowStore
|
||||
.getState()
|
||||
.nodes.find((n) => n.id === 'phase-1') as PhaseNode
|
||||
|
||||
expect(updatedPhase.data.isFirstPhase).toBe(false)
|
||||
})
|
||||
|
||||
test('disconnecting phase => phase sets nextPhaseId to null', () => {
|
||||
const p1 = createPhaseNode('phase-1', { nextPhaseId: 'phase-2' })
|
||||
const p2 = createPhaseNode('phase-2')
|
||||
|
||||
useFlowStore.setState({
|
||||
nodes: [p1, p2],
|
||||
edges: [edge('phase-1', 'phase-2')]
|
||||
})
|
||||
|
||||
act(() => {
|
||||
useFlowStore.getState().onEdgesDelete([edge('phase-1', 'phase-2')])
|
||||
})
|
||||
|
||||
const updatedP1 = useFlowStore
|
||||
.getState()
|
||||
.nodes.find((n) => n.id === 'phase-1') as PhaseNode
|
||||
|
||||
expect(updatedP1.data.nextPhaseId).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -13,19 +13,17 @@ describe('NormNode', () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
function createNode(id: string, type: string, position: XYPosition, data: Record<string, unknown>, deletable?: boolean) {
|
||||
function createNode(id: string, type: string, position: XYPosition, data: Record<string, unknown>, deletable? : boolean) {
|
||||
const defaultData = NodeDefaults[type as keyof typeof NodeDefaults]
|
||||
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
position,
|
||||
deletable,
|
||||
data: {
|
||||
...defaultData,
|
||||
...data,
|
||||
},
|
||||
id: id,
|
||||
type: type,
|
||||
position: position,
|
||||
data: {...defaultData, ...data},
|
||||
deletable: deletable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
@@ -47,34 +45,34 @@ describe('NormNode', () => {
|
||||
|
||||
describe('Rendering', () => {
|
||||
test.each(getAllTypes())('it should render %s node with the default data', (nodeType) => {
|
||||
const lengthBefore = screen.getAllByText(/.*/).length;
|
||||
const lengthBefore = screen.getAllByText(/.*/).length;
|
||||
|
||||
const newNode = createNode(nodeType + "1", nodeType, {x: 200, y:200}, {});
|
||||
const newNode = createNode(nodeType + "1", nodeType, {x: 200, y:200}, {});
|
||||
|
||||
const found = Object.entries(NodeTypes).find(([t]) => t === nodeType);
|
||||
const uiElement = found ? found[1] : null;
|
||||
const found = Object.entries(NodeTypes).find(([t]) => t === nodeType);
|
||||
const uiElement = found ? found[1] : null;
|
||||
|
||||
expect(uiElement).not.toBeNull();
|
||||
const props = {
|
||||
id: newNode.id,
|
||||
type: newNode.type as string,
|
||||
data: newNode.data as any,
|
||||
selected: false,
|
||||
isConnectable: true,
|
||||
zIndex: 0,
|
||||
dragging: false,
|
||||
selectable: true,
|
||||
deletable: true,
|
||||
draggable: true,
|
||||
positionAbsoluteX: 0,
|
||||
positionAbsoluteY: 0,
|
||||
};
|
||||
expect(uiElement).not.toBeNull();
|
||||
const props = {
|
||||
id: newNode.id,
|
||||
type: newNode.type as string,
|
||||
data: newNode.data as any,
|
||||
selected: false,
|
||||
isConnectable: true,
|
||||
zIndex: 0,
|
||||
dragging: false,
|
||||
selectable: true,
|
||||
deletable: true,
|
||||
draggable: true,
|
||||
positionAbsoluteX: 0,
|
||||
positionAbsoluteY: 0,
|
||||
};
|
||||
|
||||
renderWithProviders(createElement(uiElement as React.ComponentType<any>, props));
|
||||
const lengthAfter = screen.getAllByText(/.*/).length;
|
||||
renderWithProviders(createElement(uiElement as React.ComponentType<any>, props));
|
||||
const lengthAfter = screen.getAllByText(/.*/).length;
|
||||
|
||||
expect(lengthBefore + 1 === lengthAfter);
|
||||
});
|
||||
expect(lengthBefore + 1 === lengthAfter);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
110
test/utils/orderPhaseNodes.test.ts
Normal file
110
test/utils/orderPhaseNodes.test.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import type {PhaseNode} from "../../src/pages/VisProgPage/visualProgrammingUI/nodes/PhaseNode.tsx";
|
||||
import orderPhaseNodeArray from "../../src/utils/orderPhaseNodes.ts";
|
||||
|
||||
function createPhaseNode(
|
||||
id: string,
|
||||
isFirst: boolean = false,
|
||||
nextPhaseId: string | null = null
|
||||
): PhaseNode {
|
||||
return {
|
||||
id: id,
|
||||
type: 'phase',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
label: 'Phase',
|
||||
droppable: true,
|
||||
children: [],
|
||||
hasReduce: true,
|
||||
nextPhaseId: nextPhaseId,
|
||||
isFirstPhase: isFirst,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
describe("orderPhaseNodes", () => {
|
||||
test.each([
|
||||
{
|
||||
testCase: {
|
||||
testName: "Throws correct error when there is no first phase (empty input array)",
|
||||
input: [],
|
||||
expected: "No phaseNode with isFirstObject = true found"
|
||||
}
|
||||
},{
|
||||
testCase: {
|
||||
testName: "Throws correct error when there is no first phase",
|
||||
input: [
|
||||
createPhaseNode("phase-1", false, "phase-2"),
|
||||
createPhaseNode("phase-2", false, "phase-3"),
|
||||
createPhaseNode("phase-3", false, "end")
|
||||
],
|
||||
expected: "No phaseNode with isFirstObject = true found"
|
||||
}
|
||||
},{
|
||||
testCase: {
|
||||
testName: "Throws correct error when the program doesn't lead to an end node (missing phase-phase connection)",
|
||||
input: [
|
||||
createPhaseNode("phase-1", true, "phase-2"),
|
||||
createPhaseNode("phase-2", false, null),
|
||||
createPhaseNode("phase-3", false, "end")
|
||||
],
|
||||
expected: "Incomplete phase sequence, program does not reach the end node"
|
||||
}
|
||||
},{
|
||||
testCase: {
|
||||
testName: "Throws correct error when the program doesn't lead to an end node (missing phase-end connection)",
|
||||
input: [
|
||||
createPhaseNode("phase-1", true, "phase-2"),
|
||||
createPhaseNode("phase-2", false, "phase-3"),
|
||||
createPhaseNode("phase-3", false, null)
|
||||
],
|
||||
expected: "Incomplete phase sequence, program does not reach the end node"
|
||||
}
|
||||
},{
|
||||
testCase: {
|
||||
testName: "Throws correct error when the program leads to a non-existent phase",
|
||||
input: [
|
||||
createPhaseNode("phase-1", true, "phase-2"),
|
||||
createPhaseNode("phase-2", false, "phase-3"),
|
||||
createPhaseNode("phase-3", false, "phase-4")
|
||||
],
|
||||
expected: "Incomplete phase sequence, phaseNode with id \"phase-4\" not found"
|
||||
}
|
||||
}
|
||||
])(`Error Handling: $testCase.testName`, ({testCase}) => {
|
||||
expect(() => { orderPhaseNodeArray(testCase.input) }).toThrow(testCase.expected);
|
||||
})
|
||||
test.each([
|
||||
{
|
||||
testCase: {
|
||||
testName: "Already correctly ordered phases stay ordered",
|
||||
input: [
|
||||
createPhaseNode("phase-1", true, "phase-2"),
|
||||
createPhaseNode("phase-2", false, "phase-3"),
|
||||
createPhaseNode("phase-3", false, "end")
|
||||
],
|
||||
expected: [
|
||||
createPhaseNode("phase-1", true, "phase-2"),
|
||||
createPhaseNode("phase-2", false, "phase-3"),
|
||||
createPhaseNode("phase-3", false, "end")
|
||||
]
|
||||
}
|
||||
},{
|
||||
testCase: {
|
||||
testName: "Incorrectly ordered phases get ordered correctly",
|
||||
input: [
|
||||
createPhaseNode("phase-3", false, "end"),
|
||||
createPhaseNode("phase-1", true, "phase-2"),
|
||||
createPhaseNode("phase-2", false, "phase-3"),
|
||||
],
|
||||
expected: [
|
||||
createPhaseNode("phase-1", true, "phase-2"),
|
||||
createPhaseNode("phase-2", false, "phase-3"),
|
||||
createPhaseNode("phase-3", false, "end")
|
||||
]
|
||||
}
|
||||
}
|
||||
])(`Functional: $testCase.testName`, ({testCase}) => {
|
||||
const output = orderPhaseNodeArray(testCase.input);
|
||||
expect(output).toEqual(testCase.expected);
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user