diff --git a/src/pages/VisProgPage/VisProg.tsx b/src/pages/VisProgPage/VisProg.tsx
index 4b8944c..8208a70 100644
--- a/src/pages/VisProgPage/VisProg.tsx
+++ b/src/pages/VisProgPage/VisProg.tsx
@@ -10,12 +10,13 @@ import '@xyflow/react/dist/style.css';
import {useShallow} from 'zustand/react/shallow';
import {
- StartNode,
- EndNode,
- PhaseNode,
- NormNode
+ StartNodeComponent,
+ EndNodeComponent,
+ PhaseNodeComponent,
+ NormNodeComponent
} from './visualProgrammingUI/components/NodeDefinitions.tsx';
import {DndToolbar} from './visualProgrammingUI/components/DragDropSidebar.tsx';
+import graphReducer from "./visualProgrammingUI/GraphReducer.ts";
import useFlowStore from './visualProgrammingUI/VisProgStores.tsx';
import type {FlowState} from './visualProgrammingUI/VisProgTypes.tsx';
import styles from './VisProg.module.css'
@@ -26,10 +27,10 @@ import styles from './VisProg.module.css'
* contains the types of all nodes that are available in the editor
*/
const NODE_TYPES = {
- start: StartNode,
- end: EndNode,
- phase: PhaseNode,
- norm: NormNode
+ start: StartNodeComponent,
+ end: EndNodeComponent,
+ phase: PhaseNodeComponent,
+ norm: NormNodeComponent
};
/**
@@ -123,6 +124,12 @@ function VisualProgrammingUI() {
);
}
+// currently outputs the prepared program to the console
+function runProgram() {
+ const program = graphReducer();
+ console.log(program);
+}
+
/**
* houses the entire page, so also UI elements
* that are not a part of the Visual Programming UI
@@ -132,6 +139,7 @@ function VisProgPage() {
return (
<>
+
>
)
}
diff --git a/src/pages/VisProgPage/visualProgrammingUI/GraphReducer.ts b/src/pages/VisProgPage/visualProgrammingUI/GraphReducer.ts
new file mode 100644
index 0000000..138eb82
--- /dev/null
+++ b/src/pages/VisProgPage/visualProgrammingUI/GraphReducer.ts
@@ -0,0 +1,188 @@
+import {
+ type Edge,
+ getIncomers,
+ getOutgoers
+} from '@xyflow/react';
+import useFlowStore from "./VisProgStores.tsx";
+import type {
+ BehaviorProgram,
+ GoalData,
+ GoalReducer,
+ GraphPreprocessor,
+ NormData,
+ NormReducer,
+ OrderedPhases,
+ Phase,
+ PhaseReducer,
+ PreparedGraph,
+ PreparedPhase
+} from "./GraphReducerTypes.ts";
+import type {
+ AppNode,
+ GoalNode,
+ NormNode,
+ PhaseNode
+} from "./VisProgTypes.tsx";
+
+/**
+ * Reduces the current graph inside the visual programming editor into a BehaviorProgram
+ *
+ * @param {GraphPreprocessor} graphPreprocessor
+ * @param {PhaseReducer} phaseReducer
+ * @param {NormReducer} normReducer
+ * @param {GoalReducer} goalReducer
+ * @returns {BehaviorProgram}
+ */
+export default function graphReducer(
+ graphPreprocessor: GraphPreprocessor = defaultGraphPreprocessor,
+ phaseReducer: PhaseReducer = defaultPhaseReducer,
+ normReducer: NormReducer = defaultNormReducer,
+ goalReducer: GoalReducer = defaultGoalReducer
+) : BehaviorProgram {
+ const nodes: AppNode[] = useFlowStore.getState().nodes;
+ const edges: Edge[] = useFlowStore.getState().edges;
+ const preparedGraph: PreparedGraph = graphPreprocessor(nodes, edges);
+
+ return preparedGraph.map((preparedPhase: PreparedPhase) : Phase =>
+ phaseReducer(
+ preparedPhase,
+ normReducer,
+ goalReducer
+ ));
+};
+
+/**
+ * reduces a single preparedPhase to a Phase object
+ * the Phase object describes a single phase in a BehaviorProgram
+ *
+ * @param {PreparedPhase} phase
+ * @param {NormReducer} normReducer
+ * @param {GoalReducer} goalReducer
+ * @returns {Phase}
+ */
+export function defaultPhaseReducer(
+ phase: PreparedPhase,
+ normReducer: NormReducer = defaultNormReducer,
+ goalReducer: GoalReducer = defaultGoalReducer
+) : Phase {
+ return {
+ id: phase.phaseNode.id,
+ name: phase.phaseNode.data.label,
+ nextPhaseId: phase.nextPhaseId,
+ phaseData: {
+ norms: phase.connectedNorms.map(normReducer),
+ goals: phase.connectedGoals.map(goalReducer)
+ }
+ }
+}
+
+/**
+ * the default implementation of the goalNode reducer function
+ *
+ * @param {GoalNode} node
+ * @returns {GoalData}
+ */
+function defaultGoalReducer(node: GoalNode) : GoalData {
+ return {
+ id: node.id,
+ name: node.data.label,
+ value: node.data.value
+ }
+}
+
+/**
+ * the default implementation of the normNode reducer function
+ *
+ * @param {NormNode} node
+ * @returns {NormData}
+ */
+function defaultNormReducer(node: NormNode) :NormData {
+ return {
+ id: node.id,
+ name: node.data.label,
+ value: node.data.value
+ }
+}
+
+// Graph preprocessing functions:
+
+/**
+ * Preprocesses the provide state of the behavior editor graph, preparing it for further processing in
+ * the graphReducer function
+ *
+ * @param {AppNode[]} nodes
+ * @param {Edge[]} edges
+ * @returns {PreparedGraph}
+ */
+export function defaultGraphPreprocessor(nodes: AppNode[], edges: Edge[]) : PreparedGraph {
+ const norms : NormNode[] = nodes.filter((node) => node.type === 'norm') as NormNode[];
+ const goals : GoalNode[] = nodes.filter((node) => node.type === 'goal') as GoalNode[];
+ const orderedPhases : OrderedPhases = orderPhases(nodes, edges);
+
+ return orderedPhases.phaseNodes.map((phase: PhaseNode) : PreparedPhase => {
+ const nextPhase = orderedPhases.connections.get(phase.id);
+ return {
+ phaseNode: phase,
+ nextPhaseId: nextPhase as string,
+ connectedNorms: getIncomers({id: phase.id}, norms,edges),
+ connectedGoals: getIncomers({id: phase.id}, goals,edges)
+ };
+ });
+}
+
+/**
+ * orderPhases takes the state of the graph created by the editor and turns it into an OrderedPhases object.
+ *
+ * @param {AppNode[]} nodes
+ * @param {Edge[]} edges
+ * @returns {OrderedPhases}
+ */
+export function orderPhases(nodes: AppNode[],edges: Edge[]) : OrderedPhases {
+ // find the first Phase node
+ const phaseNodes : PhaseNode[] = nodes.filter((node) => node.type === 'phase') as PhaseNode[];
+ const startNodeIndex = nodes.findIndex((node : AppNode):boolean => {return (node.type === 'start');});
+ const firstPhaseNode = getOutgoers({ id: nodes[startNodeIndex].id },phaseNodes,edges);
+
+ // recursively adds the phase nodes to a list in the order they are connected in the graph
+ const nextPhase = (
+ currentIndex: number,
+ { phaseNodes: phases, connections: connections} : OrderedPhases
+ ) : OrderedPhases => {
+ // get the current phase and the next phases;
+ const currentPhase = phases[currentIndex];
+ const nextPhaseNodes = getOutgoers(currentPhase,phaseNodes,edges);
+ const nextNodes = getOutgoers(currentPhase,nodes, edges);
+
+ // handles adding of the next phase to the chain, and error handle if an invalid state is received
+ if (nextPhaseNodes.length === 1 && nextNodes.length === 1) {
+ connections.set(currentPhase.id, nextPhaseNodes[0].id);
+ return nextPhase(phases.push(nextPhaseNodes[0] as PhaseNode) - 1, {phaseNodes: phases, connections: connections});
+ } else {
+ // handle erroneous states
+ if (nextNodes.length === 0){
+ throw new Error(`| INVALID PROGRAM | the source handle of "${currentPhase.id}" doesn't have any outgoing connections`);
+ } else {
+ if (nextNodes.length > 1) {
+ throw new Error(`| INVALID PROGRAM | the source handle of "${currentPhase.id}" connects to too many targets`);
+ } else {
+ if (nextNodes[0].type === "end"){
+ connections.set(currentPhase.id, "end");
+ // returns the final output of the function
+ return { phaseNodes: phases, connections: connections};
+ } else {
+ throw new Error(`| INVALID PROGRAM | the node "${nextNodes[0].id}" that "${currentPhase.id}" connects to is not a phase or end node`);
+ }
+ }
+ }
+ }
+ }
+ // initializes the Map describing the connections between phase nodes
+ // we need this Map to make sure we preserve this information,
+ // so we don't need to do checks on the entire set of edges in further stages of the reduction algorithm
+ const connections : Map = new Map();
+
+ // returns an empty list if no phase nodes are present, otherwise returns an ordered list of phaseNodes
+ if (firstPhaseNode.length > 0) {
+ return nextPhase(0, {phaseNodes: [firstPhaseNode[0] as PhaseNode], connections: connections})
+ } else { return {phaseNodes: [], connections: connections} }
+}
\ No newline at end of file
diff --git a/src/pages/VisProgPage/visualProgrammingUI/GraphReducerTypes.ts b/src/pages/VisProgPage/visualProgrammingUI/GraphReducerTypes.ts
new file mode 100644
index 0000000..9151b56
--- /dev/null
+++ b/src/pages/VisProgPage/visualProgrammingUI/GraphReducerTypes.ts
@@ -0,0 +1,106 @@
+import type {Edge} from "@xyflow/react";
+import type {AppNode, GoalNode, NormNode, PhaseNode} from "./VisProgTypes.tsx";
+
+
+/**
+ * defines how a norm is represented in the simplified behavior program
+ */
+export type NormData = {
+ id: string;
+ name: string;
+ value: string;
+};
+
+/**
+ * defines how a goal is represented in the simplified behavior program
+ */
+export type GoalData = {
+ id: string;
+ name: string;
+ value: string;
+};
+
+/**
+ * definition of a PhaseData object, it contains all phaseData that is relevant
+ * for further processing and execution of a phase.
+ */
+export type PhaseData = {
+ norms: NormData[];
+ goals: GoalData[];
+};
+
+/**
+ * Describes a single phase within the simplified representation of a behavior program,
+ *
+ * Contains:
+ * - the id of the described phase,
+ * - the name of the described phase,
+ * - the id of the next phase in the user defined behavior program
+ * - the data property of the described phase node
+ *
+ * @NOTE at the moment the type definitions do not support branching programs,
+ * if branching of phases is to be supported in the future, the type definition for Phase has to be updated
+ */
+export type Phase = {
+ id: string;
+ name: string;
+ nextPhaseId: string;
+ phaseData: PhaseData;
+};
+
+/**
+ * Describes a simplified behavior program as a list of Phase objects
+ */
+export type BehaviorProgram = Phase[];
+
+
+
+export type NormReducer = (node: NormNode) => NormData;
+export type GoalReducer = (node: GoalNode) => GoalData;
+export type PhaseReducer = (
+ preparedPhase: PreparedPhase,
+ normReducer: NormReducer,
+ goalReducer: GoalReducer
+) => Phase;
+
+/**
+ * contains:
+ *
+ * - list of phases, sorted based on position in chain between the start and end node
+ * - a dictionary containing all outgoing connections,
+ * to other phase or end nodes, for each phase node uses the id of the source node as key
+ * and the id of the target node as value
+ *
+ */
+export type OrderedPhases = {
+ phaseNodes: PhaseNode[];
+ connections: Map;
+};
+
+/**
+ * A single prepared phase,
+ * contains:
+ * - the described phaseNode,
+ * - the id of the next phaseNode or "end" for the end node
+ * - a list of the normNodes that are connected to the described phase
+ * - a list of the goalNodes that are connected to the described phase
+ */
+export type PreparedPhase = {
+ phaseNode: PhaseNode;
+ nextPhaseId: string;
+ connectedNorms: NormNode[];
+ connectedGoals: GoalNode[];
+};
+
+/**
+ * a list of PreparedPhase objects,
+ * describes the preprocessed state of a program,
+ * before the contents of the node
+ */
+export type PreparedGraph = PreparedPhase[];
+
+export type GraphPreprocessor = (nodes: AppNode[], edges: Edge[]) => PreparedGraph;
+
+
+
+
diff --git a/src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx b/src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx
index 10d0142..e27fb28 100644
--- a/src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx
+++ b/src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx
@@ -20,7 +20,7 @@ const initialNodes = [
data: {label: 'start'}
},
{
- id: 'genericPhase',
+ id: 'phase-1',
type: 'phase',
position: {x: 0, y: 150},
data: {label: 'Generic Phase', number: 1},
@@ -38,9 +38,14 @@ const initialNodes = [
*/
const initialEdges = [
{
- id: 'start-end',
+ id: 'start-phase-1',
source: 'start',
- target: 'end'
+ target: 'phase-1',
+ },
+ {
+ id: 'phase-1-end',
+ source: 'phase-1',
+ target: 'end',
}
];
diff --git a/src/pages/VisProgPage/visualProgrammingUI/VisProgTypes.tsx b/src/pages/VisProgPage/visualProgrammingUI/VisProgTypes.tsx
index f5ede86..378f9be 100644
--- a/src/pages/VisProgPage/visualProgrammingUI/VisProgTypes.tsx
+++ b/src/pages/VisProgPage/visualProgrammingUI/VisProgTypes.tsx
@@ -7,12 +7,24 @@ import {
type OnReconnect,
} from '@xyflow/react';
+
+type defaultNodeData = {
+ label: string;
+};
+
+export type StartNode = Node;
+export type EndNode = Node;
+export type GoalNode = Node;
+export type NormNode = Node;
+export type PhaseNode = Node;
+
+
/**
* a type meant to house different node types, currently not used
* but will allow us to more clearly define nodeTypes when we implement
* computation of the Graph inside the ReactFlow editor
*/
-export type AppNode = Node;
+export type AppNode = Node | StartNode | EndNode | NormNode | GoalNode | PhaseNode;
/**
diff --git a/src/pages/VisProgPage/visualProgrammingUI/components/DragDropSidebar.tsx b/src/pages/VisProgPage/visualProgrammingUI/components/DragDropSidebar.tsx
index 383f72c..c9e1496 100644
--- a/src/pages/VisProgPage/visualProgrammingUI/components/DragDropSidebar.tsx
+++ b/src/pages/VisProgPage/visualProgrammingUI/components/DragDropSidebar.tsx
@@ -11,6 +11,8 @@ import {
} from 'react';
import useFlowStore from "../VisProgStores.tsx";
import styles from "../../VisProg.module.css"
+import type {AppNode, PhaseNode, NormNode} from "../VisProgTypes.tsx";
+
/**
@@ -68,28 +70,45 @@ function DraggableNode({className, children, nodeType, onDrop}: DraggableNodePro
// eslint-disable-next-line react-refresh/only-export-components
export function addNode(nodeType: string, position: XYPosition) {
const {setNodes} = useFlowStore.getState();
- const nds = useFlowStore.getState().nodes;
+ const nds : AppNode[] = useFlowStore.getState().nodes;
const newNode = () => {
switch (nodeType) {
case "phase":
{
- const phaseNumber = nds.filter((node) => node.type === 'phase').length;
- return {
+ const phaseNodes= nds.filter((node) => node.type === 'phase');
+ let phaseNumber;
+ if (phaseNodes.length > 0) {
+ const finalPhaseId : number = +(phaseNodes[phaseNodes.length - 1].id.split('-')[1]);
+ phaseNumber = finalPhaseId + 1;
+ } else {
+ phaseNumber = 1;
+ }
+ const phaseNode : PhaseNode = {
id: `phase-${phaseNumber}`,
type: nodeType,
position,
data: {label: 'new', number: phaseNumber},
- };
+ }
+ return phaseNode;
}
case "norm":
{
- const normNumber = nds.filter((node) => node.type === 'norm').length;
- return {
+ const normNodes= nds.filter((node) => node.type === 'norm');
+ let normNumber
+ if (normNodes.length > 0) {
+ const finalNormId : number = +(normNodes[normNodes.length - 1].id.split('-')[1]);
+ normNumber = finalNormId + 1;
+ } else {
+ normNumber = 1;
+ }
+
+ const normNode : NormNode = {
id: `norm-${normNumber}`,
type: nodeType,
position,
- data: {label: `new norm node`},
- };
+ data: {label: `new norm node`, value: "Pepper should be formal"},
+ }
+ return normNode;
}
default: {
throw new Error(`Node ${nodeType} not found`);
diff --git a/src/pages/VisProgPage/visualProgrammingUI/components/NodeDefinitions.tsx b/src/pages/VisProgPage/visualProgrammingUI/components/NodeDefinitions.tsx
index 63765b5..f74dd2b 100644
--- a/src/pages/VisProgPage/visualProgrammingUI/components/NodeDefinitions.tsx
+++ b/src/pages/VisProgPage/visualProgrammingUI/components/NodeDefinitions.tsx
@@ -1,32 +1,32 @@
-import {Handle, NodeToolbar, Position} from '@xyflow/react';
+import {Handle, type NodeProps, NodeToolbar, Position} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import styles from '../../VisProg.module.css';
import useFlowStore from "../VisProgStores.tsx";
+import type {
+ StartNode,
+ EndNode,
+ PhaseNode,
+ NormNode
+} from "../VisProgTypes.tsx";
-// Contains the datatypes for the data inside our NodeTypes
-// this has to be improved or adapted to suit our implementation for computing the graph
-// into a format that is useful for the Control Backend
-
-type defaultNodeData = {
- label: string;
-};
-
-type startNodeData = defaultNodeData;
-type endNodeData = defaultNodeData;
-type normNodeData = defaultNodeData;
-type phaseNodeData = defaultNodeData & {
- number: number;
-};
-
-export type nodeData = defaultNodeData | startNodeData | phaseNodeData | endNodeData;
-
-// Node Toolbar definition, contains node delete functionality
+//
type ToolbarProps = {
nodeId: string;
allowDelete: boolean;
};
+
+/**
+ * Node Toolbar definition:
+ * handles: node deleting functionality
+ * can be added to any custom node component as a React component
+ *
+ * @param {string} nodeId
+ * @param {boolean} allowDelete
+ * @returns {React.JSX.Element}
+ * @constructor
+ */
export function Toolbar({nodeId, allowDelete}: ToolbarProps) {
const {deleteNode} = useFlowStore();
@@ -42,14 +42,15 @@ export function Toolbar({nodeId, allowDelete}: ToolbarProps) {
// Definitions of Nodes
-// Start Node definition:
-
-type StartNodeProps = {
- id: string;
- data: startNodeData;
-};
-
-export const StartNode = ({id, data}: StartNodeProps) => {
+/**
+ * Start Node definition:
+ *
+ * @param {string} id
+ * @param {defaultNodeData} data
+ * @returns {React.JSX.Element}
+ * @constructor
+ */
+export const StartNodeComponent = ({id, data}: NodeProps) => {
return (
<>
@@ -62,14 +63,15 @@ export const StartNode = ({id, data}: StartNodeProps) => {
};
-// End node definition:
-
-type EndNodeProps = {
- id: string;
- data: endNodeData;
-};
-
-export const EndNode = ({id, data}: EndNodeProps) => {
+/**
+ * End node definition:
+ *
+ * @param {string} id
+ * @param {defaultNodeData} data
+ * @returns {React.JSX.Element}
+ * @constructor
+ */
+export const EndNodeComponent = ({id, data}: NodeProps) => {
return (
<>
@@ -82,14 +84,15 @@ export const EndNode = ({id, data}: EndNodeProps) => {
};
-// Phase node definition:
-
-type PhaseNodeProps = {
- id: string;
- data: phaseNodeData;
-};
-
-export const PhaseNode = ({id, data}: PhaseNodeProps) => {
+/**
+ * Phase node definition:
+ *
+ * @param {string} id
+ * @param {defaultNodeData & {number: number}} data
+ * @returns {React.JSX.Element}
+ * @constructor
+ */
+export const PhaseNodeComponent = ({id, data}: NodeProps) => {
return (
<>
@@ -104,14 +107,15 @@ export const PhaseNode = ({id, data}: PhaseNodeProps) => {
};
-// Norm node definition:
-
-type NormNodeProps = {
- id: string;
- data: normNodeData;
-};
-
-export const NormNode = ({id, data}: NormNodeProps) => {
+/**
+ * Norm node definition:
+ *
+ * @param {string} id
+ * @param {defaultNodeData & {value: string}} data
+ * @returns {React.JSX.Element}
+ * @constructor
+ */
+export const NormNodeComponent = ({id, data}: NodeProps) => {
return (
<>
diff --git a/test/pages/visProgPage/visualProgrammingUI/GraphReducer.test.ts b/test/pages/visProgPage/visualProgrammingUI/GraphReducer.test.ts
new file mode 100644
index 0000000..a907a58
--- /dev/null
+++ b/test/pages/visProgPage/visualProgrammingUI/GraphReducer.test.ts
@@ -0,0 +1,986 @@
+import type {Edge} from "@xyflow/react";
+import graphReducer, {
+ defaultGraphPreprocessor, defaultPhaseReducer,
+ orderPhases
+} from "../../../../src/pages/VisProgPage/visualProgrammingUI/GraphReducer.ts";
+import type {PreparedPhase} from "../../../../src/pages/VisProgPage/visualProgrammingUI/GraphReducerTypes.ts";
+import useFlowStore from "../../../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx";
+import type {AppNode} from "../../../../src/pages/VisProgPage/visualProgrammingUI/VisProgTypes.tsx";
+
+// sets of default values for nodes and edges to be used for test cases
+type FlowState = {
+ name: string;
+ nodes: AppNode[];
+ edges: Edge[];
+};
+
+// predefined graphs for testing:
+const onlyOnePhase : FlowState = {
+ name: "onlyOnePhase",
+ nodes: [
+ {
+ id: 'start',
+ type: 'start',
+ position: {x: 0, y: 0},
+ data: {label: 'start'}
+ },
+ {
+ id: 'phase-1',
+ type: 'phase',
+ position: {x: 0, y: 150},
+ data: {label: 'Generic Phase', number: 1},
+ },
+ {
+ id: 'end',
+ type: 'end',
+ position: {x: 0, y: 300},
+ data: {label: 'End'}
+ }
+ ],
+ edges:[
+ {
+ id: 'start-phase-1',
+ source: 'start',
+ target: 'phase-1',
+ },
+ {
+ id: 'phase-1-end',
+ source: 'phase-1',
+ target: 'end',
+ }
+ ]
+};
+const onlyThreePhases : FlowState = {
+ name: "onlyThreePhases",
+ nodes: [
+ {
+ id: 'start',
+ type: 'start',
+ position: {x: 0, y: 0},
+ data: {label: 'start'}
+ },
+ {
+ id: 'phase-1',
+ type: 'phase',
+ position: {x: 0, y: 150},
+ data: {label: 'Generic Phase', number: 1},
+ },
+ {
+ id: 'phase-3',
+ type: 'phase',
+ position: {x: 0, y: 150},
+ data: {label: 'Generic Phase', number: 3},
+ },
+ {
+ id: 'phase-2',
+ type: 'phase',
+ position: {x: 0, y: 150},
+ data: {label: 'Generic Phase', number: 2},
+ },
+ {
+ id: 'end',
+ type: 'end',
+ position: {x: 0, y: 300},
+ data: {label: 'End'}
+ }
+ ],
+ edges:[
+ {
+ id: 'start-phase-1',
+ source: 'start',
+ target: 'phase-1',
+ },
+ {
+ id: 'phase-1-phase-2',
+ source: 'phase-1',
+ target: 'phase-2',
+ },
+ {
+ id: 'phase-2-phase-3',
+ source: 'phase-2',
+ target: 'phase-3',
+ },
+ {
+ id: 'phase-3-end',
+ source: 'phase-3',
+ target: 'end',
+ }
+ ]
+};
+const onlySingleEdgeNorms : FlowState = {
+ name: "onlySingleEdgeNorms",
+ nodes: [
+ {
+ id: 'start',
+ type: 'start',
+ position: {x: 0, y: 0},
+ data: {label: 'start'}
+ },
+ {
+ id: 'phase-1',
+ type: 'phase',
+ position: {x: 0, y: 150},
+ data: {label: 'Generic Phase', number: 1},
+ },
+ {
+ id: 'norm-1',
+ type: 'norm',
+ position: {x: 0, y: 150},
+ data: {label: 'Generic Norm', value: "generic"},
+ },
+ {
+ id: 'norm-2',
+ type: 'norm',
+ position: {x: 0, y: 150},
+ data: {label: 'Generic Norm', value: "generic"},
+ },
+ {
+ id: 'phase-3',
+ type: 'phase',
+ position: {x: 0, y: 150},
+ data: {label: 'Generic Phase', number: 3},
+ },
+ {
+ id: 'phase-2',
+ type: 'phase',
+ position: {x: 0, y: 150},
+ data: {label: 'Generic Phase', number: 2},
+ },
+ {
+ id: 'end',
+ type: 'end',
+ position: {x: 0, y: 300},
+ data: {label: 'End'}
+ }
+ ],
+ edges:[
+ {
+ id: 'start-phase-1',
+ source: 'start',
+ target: 'phase-1',
+ },
+ {
+ id: 'norm-1-phase-2',
+ source: 'norm-1',
+ target: 'phase-2',
+ },
+ {
+ id: 'phase-1-phase-2',
+ source: 'phase-1',
+ target: 'phase-2',
+ },
+ {
+ id: 'phase-2-phase-3',
+ source: 'phase-2',
+ target: 'phase-3',
+ },
+ {
+ id: 'norm-2-phase-3',
+ source: 'norm-2',
+ target: 'phase-3',
+ },
+ {
+ id: 'phase-3-end',
+ source: 'phase-3',
+ target: 'end',
+ }
+ ]
+};
+const multiEdgeNorms : FlowState = {
+ name: "multiEdgeNorms",
+ nodes: [
+ {
+ id: 'start',
+ type: 'start',
+ position: {x: 0, y: 0},
+ data: {label: 'start'}
+ },
+ {
+ id: 'phase-1',
+ type: 'phase',
+ position: {x: 0, y: 150},
+ data: {label: 'Generic Phase', number: 1},
+ },
+ {
+ id: 'norm-1',
+ type: 'norm',
+ position: {x: 0, y: 150},
+ data: {label: 'Generic Norm', value: "generic"},
+ },
+ {
+ id: 'norm-2',
+ type: 'norm',
+ position: {x: 0, y: 150},
+ data: {label: 'Generic Norm', value: "generic"},
+ },
+ {
+ id: 'phase-3',
+ type: 'phase',
+ position: {x: 0, y: 150},
+ data: {label: 'Generic Phase', number: 3},
+ },
+ {
+ id: 'norm-3',
+ type: 'norm',
+ position: {x: 0, y: 150},
+ data: {label: 'Generic Norm', value: "generic"},
+ },
+ {
+ id: 'phase-2',
+ type: 'phase',
+ position: {x: 0, y: 150},
+ data: {label: 'Generic Phase', number: 2},
+ },
+ {
+ id: 'end',
+ type: 'end',
+ position: {x: 0, y: 300},
+ data: {label: 'End'}
+ }
+ ],
+ edges:[
+ {
+ id: 'start-phase-1',
+ source: 'start',
+ target: 'phase-1',
+ },
+ {
+ id: 'norm-1-phase-2',
+ source: 'norm-1',
+ target: 'phase-2',
+ },
+ {
+ id: 'norm-1-phase-3',
+ source: 'norm-1',
+ target: 'phase-3',
+ },
+ {
+ id: 'phase-1-phase-2',
+ source: 'phase-1',
+ target: 'phase-2',
+ },
+ {
+ id: 'norm-3-phase-1',
+ source: 'norm-3',
+ target: 'phase-1',
+ },
+ {
+ id: 'phase-2-phase-3',
+ source: 'phase-2',
+ target: 'phase-3',
+ },
+ {
+ id: 'norm-2-phase-3',
+ source: 'norm-2',
+ target: 'phase-3',
+ },
+ {
+ id: 'norm-2-phase-2',
+ source: 'norm-2',
+ target: 'phase-2',
+ },
+ {
+ id: 'phase-3-end',
+ source: 'phase-3',
+ target: 'end',
+ }
+ ]
+};
+const onlyStartEnd : FlowState = {
+ name: "onlyStartEnd",
+ nodes: [
+ {
+ id: 'start',
+ type: 'start',
+ position: {x: 0, y: 0},
+ data: {label: 'start'}
+ },
+ {
+ id: 'end',
+ type: 'end',
+ position: {x: 0, y: 300},
+ data: {label: 'End'}
+ }
+ ],
+ edges:[
+ {
+ id: 'start-end',
+ source: 'start',
+ target: 'end',
+ },
+ ]
+};
+
+// states that contain invalid programs for testing if correct errors are thrown:
+const phaseConnectsToInvalidNodeType : FlowState = {
+ name: "phaseConnectsToInvalidNodeType",
+ nodes: [
+ {
+ id: 'start',
+ type: 'start',
+ position: {x: 0, y: 0},
+ data: {label: 'start'}
+ },
+ {
+ id: 'phase-1',
+ type: 'phase',
+ position: {x: 0, y: 150},
+ data: {label: 'Generic Phase', number: 1},
+ },
+ {
+ id: 'default-1',
+ type: 'default',
+ position: {x: 0, y: 150},
+ data: {label: 'Generic Norm'},
+ },
+ {
+ id: 'end',
+ type: 'end',
+ position: {x: 0, y: 300},
+ data: {label: 'End'}
+ }
+ ],
+ edges:[
+ {
+ id: 'start-phase-1',
+ source: 'start',
+ target: 'phase-1',
+ },
+ {
+ id: 'phase-1-default-1',
+ source: 'phase-1',
+ target: 'default-1',
+ },
+ ]
+};
+const phaseHasNoOutgoingConnections : FlowState = {
+ name: "phaseHasNoOutgoingConnections",
+ nodes: [
+ {
+ id: 'start',
+ type: 'start',
+ position: {x: 0, y: 0},
+ data: {label: 'start'}
+ },
+ {
+ id: 'phase-1',
+ type: 'phase',
+ position: {x: 0, y: 150},
+ data: {label: 'Generic Phase', number: 1},
+ },
+ {
+ id: 'phase-2',
+ type: 'phase',
+ position: {x: 0, y: 150},
+ data: {label: 'Generic Phase', number: 2},
+ },
+ {
+ id: 'end',
+ type: 'end',
+ position: {x: 0, y: 300},
+ data: {label: 'End'}
+ }
+ ],
+ edges:[
+ {
+ id: 'start-phase-1',
+ source: 'start',
+ target: 'phase-1',
+ },
+ ]
+};
+const phaseHasTooManyOutgoingConnections : FlowState = {
+ name: "phaseHasTooManyOutgoingConnections",
+ nodes: [
+ {
+ id: 'start',
+ type: 'start',
+ position: {x: 0, y: 0},
+ data: {label: 'start'}
+ },
+ {
+ id: 'phase-1',
+ type: 'phase',
+ position: {x: 0, y: 150},
+ data: {label: 'Generic Phase', number: 1},
+ },
+ {
+ id: 'phase-2',
+ type: 'phase',
+ position: {x: 0, y: 150},
+ data: {label: 'Generic Phase', number: 2},
+ },
+ {
+ id: 'end',
+ type: 'end',
+ position: {x: 0, y: 300},
+ data: {label: 'End'}
+ }
+ ],
+ edges:[
+ {
+ id: 'start-phase-1',
+ source: 'start',
+ target: 'phase-1',
+ },
+ {
+ id: 'phase-1-phase-2',
+ source: 'phase-1',
+ target: 'phase-2',
+ },
+ {
+ id: 'phase-1-end',
+ source: 'phase-1',
+ target: 'end',
+ },
+ {
+ id: 'phase-2-end',
+ source: 'phase-2',
+ target: 'end',
+ },
+ ]
+};
+
+describe('Graph Reducer Tests', () => {
+ describe('defaultGraphPreprocessor', () => {
+ test.each([
+ {
+ state: onlyOnePhase,
+ expected: [
+ {
+ phaseNode: {
+ id: 'phase-1',
+ type: 'phase',
+ position: {x: 0, y: 150},
+ data: {label: 'Generic Phase', number: 1},
+ },
+ nextPhaseId: 'end',
+ connectedNorms: [],
+ connectedGoals: [],
+ }]
+ },
+ {
+ state: onlyThreePhases,
+ expected: [
+ {
+ phaseNode: {
+ id: 'phase-1',
+ type: 'phase',
+ position: {x: 0, y: 150},
+ data: {label: 'Generic Phase', number: 1},
+ },
+ nextPhaseId: 'phase-2',
+ connectedNorms: [],
+ connectedGoals: [],
+ },
+ {
+ phaseNode: {
+ id: 'phase-2',
+ type: 'phase',
+ position: {x: 0, y: 150},
+ data: {label: 'Generic Phase', number: 2},
+ },
+ nextPhaseId: 'phase-3',
+ connectedNorms: [],
+ connectedGoals: [],
+ },
+ {
+ phaseNode: {
+ id: 'phase-3',
+ type: 'phase',
+ position: {x: 0, y: 150},
+ data: {label: 'Generic Phase', number: 3},
+ },
+ nextPhaseId: 'end',
+ connectedNorms: [],
+ connectedGoals: [],
+ }]
+ },
+ {
+ state: onlySingleEdgeNorms,
+ expected: [
+ {
+ phaseNode: {
+ id: 'phase-1',
+ type: 'phase',
+ position: {x: 0, y: 150},
+ data: {label: 'Generic Phase', number: 1},
+ },
+ nextPhaseId: 'phase-2',
+ connectedNorms: [],
+ connectedGoals: [],
+ },
+ {
+ phaseNode: {
+ id: 'phase-2',
+ type: 'phase',
+ position: {x: 0, y: 150},
+ data: {label: 'Generic Phase', number: 2},
+ },
+ nextPhaseId: 'phase-3',
+ connectedNorms: [{
+ id: 'norm-1',
+ type: 'norm',
+ position: {x: 0, y: 150},
+ data: {label: 'Generic Norm', value: "generic"},
+ }],
+ connectedGoals: [],
+ },
+ {
+ phaseNode: {
+ id: 'phase-3',
+ type: 'phase',
+ position: {x: 0, y: 150},
+ data: {label: 'Generic Phase', number: 3},
+ },
+ nextPhaseId: 'end',
+ connectedNorms: [{
+ id: 'norm-2',
+ type: 'norm',
+ position: {x: 0, y: 150},
+ data: {label: 'Generic Norm', value: "generic"},
+ }],
+ connectedGoals: [],
+ }]
+ },
+ {
+ state: multiEdgeNorms,
+ expected: [
+ {
+ phaseNode: {
+ id: 'phase-1',
+ type: 'phase',
+ position: {x: 0, y: 150},
+ data: {label: 'Generic Phase', number: 1},
+ },
+ nextPhaseId: 'phase-2',
+ connectedNorms: [{
+ id: 'norm-3',
+ type: 'norm',
+ position: {x: 0, y: 150},
+ data: {label: 'Generic Norm', value: "generic"},
+ }],
+ connectedGoals: [],
+ },
+ {
+ phaseNode: {
+ id: 'phase-2',
+ type: 'phase',
+ position: {x: 0, y: 150},
+ data: {label: 'Generic Phase', number: 2},
+ },
+ nextPhaseId: 'phase-3',
+ connectedNorms: [{
+ id: 'norm-1',
+ type: 'norm',
+ position: {x: 0, y: 150},
+ data: {label: 'Generic Norm', value: "generic"},
+ },
+ {
+ id: 'norm-2',
+ type: 'norm',
+ position: {x: 0, y: 150},
+ data: {label: 'Generic Norm', value: "generic"},
+ }],
+ connectedGoals: [],
+ },
+ {
+ phaseNode: {
+ id: 'phase-3',
+ type: 'phase',
+ position: {x: 0, y: 150},
+ data: {label: 'Generic Phase', number: 3},
+ },
+ nextPhaseId: 'end',
+ connectedNorms: [{
+ id: 'norm-1',
+ type: 'norm',
+ position: {x: 0, y: 150},
+ data: {label: 'Generic Norm', value: "generic"},
+ },
+ {
+ id: 'norm-2',
+ type: 'norm',
+ position: {x: 0, y: 150},
+ data: {label: 'Generic Norm', value: "generic"},
+ }],
+ connectedGoals: [],
+ }]
+ },
+ {
+ state: onlyStartEnd,
+ expected: [],
+ }
+ ])(`tests state: $state.name`, ({state, expected}) => {
+ const output = defaultGraphPreprocessor(state.nodes, state.edges);
+ expect(output).toEqual(expected);
+ });
+ });
+ describe("orderPhases", () => {
+ test.each([
+ {
+ state: onlyOnePhase,
+ expected: {
+ phaseNodes: [{
+ id: 'phase-1',
+ type: 'phase',
+ position: {x: 0, y: 150},
+ data: {label: 'Generic Phase', number: 1},
+ }],
+ connections: new Map([["phase-1","end"]])
+ }
+ },
+ {
+ state: onlyThreePhases,
+ expected: {
+ phaseNodes: [
+ {
+ id: 'phase-1',
+ type: 'phase',
+ position: {x: 0, y: 150},
+ data: {label: 'Generic Phase', number: 1},
+ },
+ {
+ id: 'phase-2',
+ type: 'phase',
+ position: {x: 0, y: 150},
+ data: {label: 'Generic Phase', number: 2},
+ },
+ {
+ id: 'phase-3',
+ type: 'phase',
+ position: {x: 0, y: 150},
+ data: {label: 'Generic Phase', number: 3},
+ }],
+ connections: new Map([
+ ["phase-1","phase-2"],
+ ["phase-2","phase-3"],
+ ["phase-3","end"]
+ ])
+ }
+ },
+ {
+ state: onlySingleEdgeNorms,
+ expected: {
+ phaseNodes: [
+ {
+ id: 'phase-1',
+ type: 'phase',
+ position: {x: 0, y: 150},
+ data: {label: 'Generic Phase', number: 1},
+ },
+ {
+ id: 'phase-2',
+ type: 'phase',
+ position: {x: 0, y: 150},
+ data: {label: 'Generic Phase', number: 2},
+ },
+ {
+ id: 'phase-3',
+ type: 'phase',
+ position: {x: 0, y: 150},
+ data: {label: 'Generic Phase', number: 3},
+ }],
+ connections: new Map([
+ ["phase-1","phase-2"],
+ ["phase-2","phase-3"],
+ ["phase-3","end"]
+ ])
+ }
+ },
+ {
+ state: onlyStartEnd,
+ expected: {
+ phaseNodes: [],
+ connections: new Map()
+ }
+ }
+ ])(`tests state: $state.name`, ({state, expected}) => {
+ const output = orderPhases(state.nodes, state.edges);
+ expect(output.phaseNodes).toEqual(expected.phaseNodes);
+ expect(output.connections).toEqual(expected.connections);
+ });
+ test.each([
+ {
+ state: phaseConnectsToInvalidNodeType,
+ expected: new Error('| INVALID PROGRAM | the node "default-1" that "phase-1" connects to is not a phase or end node')
+ },
+ {
+ state: phaseHasNoOutgoingConnections,
+ expected: new Error('| INVALID PROGRAM | the source handle of "phase-1" doesn\'t have any outgoing connections')
+ },
+ {
+ state: phaseHasTooManyOutgoingConnections,
+ expected: new Error('| INVALID PROGRAM | the source handle of "phase-1" connects to too many targets')
+ }
+ ])(`tests erroneous state: $state.name`, ({state, expected}) => {
+ const testForError = () => {
+ orderPhases(state.nodes, state.edges);
+ };
+ expect(testForError).toThrow(expected);
+ })
+ })
+ describe("defaultPhaseReducer", () => {
+ test("phaseReducer handles empty norms and goals without failing", () => {
+ const input : PreparedPhase = {
+ phaseNode: {
+ id: 'phase-1',
+ type: 'phase',
+ position: {x: 0, y: 150},
+ data: {label: 'Generic Phase', number: 1},
+ },
+ nextPhaseId: 'end',
+ connectedNorms: [],
+ connectedGoals: [],
+ }
+ const output = defaultPhaseReducer(input);
+ expect(output).toEqual({
+ id: 'phase-1',
+ name: 'Generic Phase',
+ nextPhaseId: 'end',
+ phaseData: {
+ norms: [],
+ goals: []
+ }
+ });
+ });
+ test("defaultNormReducer reduces norms correctly", () => {
+ const input : PreparedPhase = {
+ phaseNode: {
+ id: 'phase-1',
+ type: 'phase',
+ position: {x: 0, y: 150},
+ data: {label: 'Generic Phase', number: 1},
+ },
+ nextPhaseId: 'end',
+ connectedNorms: [{
+ id: 'norm-1',
+ type: 'norm',
+ position: {x: 0, y: 150},
+ data: {label: 'Generic Norm', value: "generic"},
+ }],
+ connectedGoals: [],
+ }
+ const output = defaultPhaseReducer(input);
+ expect(output).toEqual({
+ id: 'phase-1',
+ name: 'Generic Phase',
+ nextPhaseId: 'end',
+ phaseData: {
+ norms: [{
+ id: 'norm-1',
+ name: 'Generic Norm',
+ value: "generic"
+ }],
+ goals: []
+ }
+ });
+ });
+ test("defaultGoalReducer reduces goals correctly", () => {
+ const input : PreparedPhase = {
+ phaseNode: {
+ id: 'phase-1',
+ type: 'phase',
+ position: {x: 0, y: 150},
+ data: {label: 'Generic Phase', number: 1},
+ },
+ nextPhaseId: 'end',
+ connectedNorms: [],
+ connectedGoals: [{
+ id: 'goal-1',
+ type: 'goal',
+ position: {x: 0, y: 150},
+ data: {label: 'Generic Goal', value: "generic"},
+ }],
+ }
+ const output = defaultPhaseReducer(input);
+ expect(output).toEqual({
+ id: 'phase-1',
+ name: 'Generic Phase',
+ nextPhaseId: 'end',
+ phaseData: {
+ norms: [],
+ goals: [{
+ id: 'goal-1',
+ name: 'Generic Goal',
+ value: "generic"
+ }]
+ }
+ });
+ });
+ })
+ describe("GraphReducer", () => {
+ test.each([
+ {
+ state: onlyOnePhase,
+ expected: [
+ {
+ id: 'phase-1',
+ name: 'Generic Phase',
+ nextPhaseId: 'end',
+ phaseData: {
+ norms: [],
+ goals: []
+ }
+ }]
+ },
+ {
+ state: onlyThreePhases,
+ expected: [
+ {
+ id: 'phase-1',
+ name: 'Generic Phase',
+ nextPhaseId: 'phase-2',
+ phaseData: {
+ norms: [],
+ goals: []
+ }
+ },
+ {
+ id: 'phase-2',
+ name: 'Generic Phase',
+ nextPhaseId: 'phase-3',
+ phaseData: {
+ norms: [],
+ goals: []
+ }
+ },
+ {
+ id: 'phase-3',
+ name: 'Generic Phase',
+ nextPhaseId: 'end',
+ phaseData: {
+ norms: [],
+ goals: []
+ }
+ }]
+ },
+ {
+ state: onlySingleEdgeNorms,
+ expected: [
+ {
+ id: 'phase-1',
+ name: 'Generic Phase',
+ nextPhaseId: 'phase-2',
+ phaseData: {
+ norms: [],
+ goals: []
+ }
+ },
+ {
+ id: 'phase-2',
+ name: 'Generic Phase',
+ nextPhaseId: 'phase-3',
+ phaseData: {
+ norms: [
+ {
+ id: 'norm-1',
+ name: 'Generic Norm',
+ value: "generic"
+ }
+ ],
+ goals: []
+ }
+ },
+ {
+ id: 'phase-3',
+ name: 'Generic Phase',
+ nextPhaseId: 'end',
+ phaseData: {
+ norms: [{
+ id: 'norm-2',
+ name: 'Generic Norm',
+ value: "generic"
+ }],
+ goals: []
+ }
+ }]
+ },
+ {
+ state: multiEdgeNorms,
+ expected: [
+ {
+ id: 'phase-1',
+ name: 'Generic Phase',
+ nextPhaseId: 'phase-2',
+ phaseData: {
+ norms: [{
+ id: 'norm-3',
+ name: 'Generic Norm',
+ value: "generic"
+ }],
+ goals: []
+ }
+ },
+ {
+ id: 'phase-2',
+ name: 'Generic Phase',
+ nextPhaseId: 'phase-3',
+ phaseData: {
+ norms: [
+ {
+ id: 'norm-1',
+ name: 'Generic Norm',
+ value: "generic"
+ },
+ {
+ id: 'norm-2',
+ name: 'Generic Norm',
+ value: "generic"
+ }
+ ],
+ goals: []
+ }
+ },
+ {
+ id: 'phase-3',
+ name: 'Generic Phase',
+ nextPhaseId: 'end',
+ phaseData: {
+ norms: [{
+ id: 'norm-1',
+ name: 'Generic Norm',
+ value: "generic"
+ },
+ {
+ id: 'norm-2',
+ name: 'Generic Norm',
+ value: "generic"
+ }],
+ goals: []
+ }
+ }]
+ },
+ {
+ state: onlyStartEnd,
+ expected: [],
+ }
+ ])("`tests state: $state.name`", ({state, expected}) => {
+ useFlowStore.setState({nodes: state.nodes, edges: state.edges});
+ const output = graphReducer(); // uses default reducers
+ expect(output).toEqual(expected);
+ })
+ // we run the test for correct error handling for the entire graph reducer as well,
+ // to make sure no errors occur before we intend to handle the errors ourselves
+ test.each([
+ {
+ state: phaseConnectsToInvalidNodeType,
+ expected: new Error('| INVALID PROGRAM | the node "default-1" that "phase-1" connects to is not a phase or end node')
+ },
+ {
+ state: phaseHasNoOutgoingConnections,
+ expected: new Error('| INVALID PROGRAM | the source handle of "phase-1" doesn\'t have any outgoing connections')
+ },
+ {
+ state: phaseHasTooManyOutgoingConnections,
+ expected: new Error('| INVALID PROGRAM | the source handle of "phase-1" connects to too many targets')
+ }
+ ])(`tests erroneous state: $state.name`, ({state, expected}) => {
+ useFlowStore.setState({nodes: state.nodes, edges: state.edges});
+ const testForError = () => {
+ graphReducer();
+ };
+ expect(testForError).toThrow(expected);
+ })
+ })
+});
\ No newline at end of file
diff --git a/test/pages/visProgPage/visualProgrammingUI/components/DragDropSidebar.test.tsx b/test/pages/visProgPage/visualProgrammingUI/components/DragDropSidebar.test.tsx
index ae9b88c..a92adb3 100644
--- a/test/pages/visProgPage/visualProgrammingUI/components/DragDropSidebar.test.tsx
+++ b/test/pages/visProgPage/visualProgrammingUI/components/DragDropSidebar.test.tsx
@@ -24,8 +24,8 @@ describe('Drag-and-Drop sidebar', () => {
})
const updatedState = useFlowStore.getState();
expect(updatedState.nodes.length).toBe(2);
- expect(updatedState.nodes[0].id).toBe(`${nodeType}-0`);
- expect(updatedState.nodes[1].id).toBe(`${nodeType}-1`);
+ expect(updatedState.nodes[0].id).toBe(`${nodeType}-1`);
+ expect(updatedState.nodes[1].id).toBe(`${nodeType}-2`);
});
test('throws error on unexpected node type', () => {
expect(() => addNode('I do not Exist', {x:100, y:100})).toThrow("Node I do not Exist not found");