fix deep cloning bug where phases don't have their own children but store references #27

Merged
9828273 merged 12 commits from fix/deep-clone-data into dev 2025-12-09 14:55:47 +00:00
8 changed files with 153 additions and 94 deletions
Showing only changes of commit a95fbd15e6 - Show all commits

View File

@@ -69,6 +69,7 @@ export const NodeConnects = {
export const NodeDeletes = { export const NodeDeletes = {
start: () => false, start: () => false,
end: () => false, end: () => false,
test: () => false, // Used for coverage of universal/ undefined nodes
} }
/** /**
@@ -79,4 +80,5 @@ export const NodesInPhase = {
start: () => false, start: () => false,
end: () => false, end: () => false,
phase: () => false, phase: () => false,
test: () => false, // Used for coverage of universal/ undefined nodes
} }

View File

@@ -40,14 +40,11 @@ export default function EndNode(props: NodeProps<EndNode>) {
/** /**
* Functionality for reducing this node into its more compact json program * Functionality for reducing this node into its more compact json program
* @param node the node to reduce * @param node the node to reduce
* @param nodes all nodes present * @param _nodes all nodes present
* @returns Dictionary, {id: node.id} * @returns Dictionary, {id: node.id}
*/ */
export function EndReduce(node: Node, nodes: Node[]) { export function EndReduce(node: Node, _nodes: Node[]) {
// Replace this for nodes functionality // Replace this for nodes functionality
if (nodes.length <= -1) {
console.warn("Impossible nodes length in EndReduce")
}
return { return {
id: node.id id: node.id
} }
@@ -55,13 +52,9 @@ export function EndReduce(node: Node, nodes: Node[]) {
/** /**
* Any connection functionality that should get called when a connection is made to this node type (end) * Any connection functionality that should get called when a connection is made to this node type (end)
* @param thisNode the node of which the functionality gets called * @param _thisNode the node of which the functionality gets called
* @param otherNode the other node which has connected * @param _otherNode the other node which has connected
* @param isThisSource whether this node is the one that is the source of the connection * @param _isThisSource whether this node is the one that is the source of the connection
*/ */
export function EndConnects(thisNode: Node, otherNode: Node, isThisSource: boolean) { export function EndConnects(_thisNode: Node, _otherNode: Node, _isThisSource: boolean) {
// Replace this for connection logic
if (thisNode == undefined && otherNode == undefined && isThisSource == false) {
console.warn("Impossible node connection called in EndConnects")
}
} }

View File

@@ -77,13 +77,9 @@ export default function GoalNode(props: NodeProps<GoalNode>) {
/** /**
* Reduces each Goal, including its children down into its relevant data. * Reduces each Goal, including its children down into its relevant data.
* @param node: The Node Properties of this node. * @param node: The Node Properties of this node.
* @param nodes: all the nodes in the graph * @param _nodes: all the nodes in the graph
*/ */
export function GoalReduce(node: Node, nodes: Node[]) { export function GoalReduce(node: Node, _nodes: Node[]) {
// Replace this for nodes functionality
if (nodes.length <= -1) {
console.warn("Impossible nodes length in GoalReduce")
}
const data = node.data as GoalNodeData; const data = node.data as GoalNodeData;
return { return {
id: node.id, id: node.id,
@@ -93,9 +89,6 @@ export function GoalReduce(node: Node, nodes: Node[]) {
} }
} }
export function GoalConnects(thisNode: Node, otherNode: Node, isThisSource: boolean) { export function GoalConnects(_thisNode: Node, _otherNode: Node, _isThisSource: boolean) {
// Replace this for connection logic // Replace this for connection logic
if (thisNode == undefined && otherNode == undefined && isThisSource == false) {
console.warn("Impossible node connection called in EndConnects")
}
} }

View File

@@ -61,13 +61,9 @@ export default function NormNode(props: NodeProps<NormNode>) {
/** /**
* Reduces each Norm, including its children down into its relevant data. * Reduces each Norm, including its children down into its relevant data.
* @param node: The Node Properties of this node. * @param node: The Node Properties of this node.
* @param nodes: all the nodes in the graph * @param _nodes: all the nodes in the graph
*/ */
export function NormReduce(node: Node, nodes: Node[]) { export function NormReduce(node: Node, _nodes: Node[]) {
// Replace this for nodes functionality
if (nodes.length <= -1) {
console.warn("Impossible nodes length in NormReduce")
}
const data = node.data as NormNodeData; const data = node.data as NormNodeData;
return { return {
id: node.id, id: node.id,
@@ -76,9 +72,5 @@ export function NormReduce(node: Node, nodes: Node[]) {
} }
} }
export function NormConnects(thisNode: Node, otherNode: Node, isThisSource: boolean) { export function NormConnects(_thisNode: Node, _otherNode: Node, _isThisSource: boolean) {
// Replace this for connection logic
if (thisNode == undefined && otherNode == undefined && isThisSource == false) {
console.warn("Impossible node connection called in EndConnects")
}
} }

View File

@@ -78,8 +78,10 @@ export function PhaseReduce(node: Node, nodes: Node[]) {
.filter(([t]) => !nodesNotInPhase.includes(t)) .filter(([t]) => !nodesNotInPhase.includes(t))
.map(([t]) => t); .map(([t]) => t);
// children nodes // children nodes - make sure to check for empty arrays
const childrenNodes = nodes.filter((node) => data.children.includes(node.id)); let childrenNodes: any[] = [];
if (data.children)
childrenNodes = nodes.filter((node) => data.children.includes(node.id));
// Build the result object // Build the result object
const result: Record<string, unknown> = { const result: Record<string, unknown> = {

View File

@@ -40,14 +40,11 @@ export default function StartNode(props: NodeProps<StartNode>) {
/** /**
* The reduce function for this node type. * The reduce function for this node type.
* @param node this node * @param node this node
* @param nodes all the nodes in the graph * @param _nodes all the nodes in the graph
* @returns a reduced structure of this node * @returns a reduced structure of this node
*/ */
export function StartReduce(node: Node, nodes: Node[]) { export function StartReduce(node: Node, _nodes: Node[]) {
// Replace this for nodes functionality // Replace this for nodes functionality
if (nodes.length <= -1) {
console.warn("Impossible nodes length in StartReduce")
}
return { return {
id: node.id id: node.id
} }
@@ -55,13 +52,9 @@ export function StartReduce(node: Node, nodes: Node[]) {
/** /**
* This function is called whenever a connection is made with this node type (start) * This function is called whenever a connection is made with this node type (start)
* @param thisNode the node of this node type which function is called * @param _thisNode the node of this node type which function is called
* @param otherNode the other node which was part of the connection * @param _otherNode the other node which was part of the connection
* @param isThisSource whether this instance of the node was the source in the connection, true = yes. * @param _isThisSource whether this instance of the node was the source in the connection, true = yes.
*/ */
export function StartConnects(thisNode: Node, otherNode: Node, isThisSource: boolean) { export function StartConnects(_thisNode: Node, _otherNode: Node, _isThisSource: boolean) {
// Replace this for connection logic
if (thisNode == undefined && otherNode == undefined && isThisSource == false) {
console.warn("Impossible node connection called in EndConnects")
}
} }

View File

@@ -68,13 +68,9 @@ export default function TriggerNode(props: NodeProps<TriggerNode>) {
/** /**
* Reduces each Trigger, including its children down into its relevant data. * Reduces each Trigger, including its children down into its relevant data.
* @param node: The Node Properties of this node. * @param node: The Node Properties of this node.
* @param nodes: all the nodes in the graph. * @param _nodes: all the nodes in the graph.
*/ */
export function TriggerReduce(node: Node, nodes: Node[]) { export function TriggerReduce(node: Node, _nodes: Node[]) {
// Replace this for nodes functionality
if (nodes.length <= -1) {
console.warn("Impossible nodes length in TriggerReduce")
}
const data = node.data as TriggerNodeData; const data = node.data as TriggerNodeData;
return { return {
label: data.label, label: data.label,
@@ -84,15 +80,12 @@ export function TriggerReduce(node: Node, nodes: Node[]) {
/** /**
* This function is called whenever a connection is made with this node type (trigger) * This function is called whenever a connection is made with this node type (trigger)
* @param thisNode the node of this node type which function is called * @param _thisNode the node of this node type which function is called
* @param otherNode the other node which was part of the connection * @param _otherNode the other node which was part of the connection
* @param isThisSource whether this instance of the node was the source in the connection, true = yes. * @param _isThisSource whether this instance of the node was the source in the connection, true = yes.
*/ */
export function TriggerConnects(thisNode: Node, otherNode: Node, isThisSource: boolean) { export function TriggerConnects(_thisNode: Node, _otherNode: Node, _isThisSource: boolean) {
// Replace this for connection logic
if (thisNode == undefined && otherNode == undefined && isThisSource == false) {
console.warn("Impossible node connection called in EndConnects")
}
} }
// Definitions for the possible triggers, being keywords and emotions // Definitions for the possible triggers, being keywords and emotions

View File

@@ -2,15 +2,19 @@ import { describe, beforeEach } from '@jest/globals';
import { screen } from '@testing-library/react'; import { screen } from '@testing-library/react';
import { renderWithProviders, resetFlowStore } from '../.././/./../../test-utils/test-utils'; import { renderWithProviders, resetFlowStore } from '../.././/./../../test-utils/test-utils';
import type { XYPosition } from '@xyflow/react'; import type { XYPosition } from '@xyflow/react';
import { NodeTypes, NodeDefaults } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/NodeRegistry'; import { NodeTypes, NodeDefaults, NodeConnects, NodeReduces, NodesInPhase } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/NodeRegistry';
import '@testing-library/jest-dom' import '@testing-library/jest-dom'
import { createElement } from 'react';
import useFlowStore from '../../../../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores';
describe('NormNode', () => { describe('NormNode', () => {
// let user: ReturnType<typeof userEvent.setup>; beforeEach(() => {
resetFlowStore();
jest.clearAllMocks();
});
// Copied from VisStores. 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] const defaultData = NodeDefaults[type as keyof typeof NodeDefaults]
const newData = { const newData = {
id: id, id: id,
@@ -23,13 +27,27 @@ describe('NormNode', () => {
} }
beforeEach(() => { /**
resetFlowStore(); * Reduces the graph into its phases' information and recursively calls their reducing function
// user = userEvent.setup(); */
function graphReducer() {
const { nodes } = useFlowStore.getState();
return nodes
.filter((n) => n.type == 'phase')
.map((n) => {
const reducer = NodeReduces['phase'];
return reducer(n, nodes)
}); });
}
function getAllTypes() {
return Object.entries(NodeTypes).map(([t])=>t)
}
describe('Rendering', () => { describe('Rendering', () => {
test.each([Object.entries(NodeTypes)].map(([t])=>t))('it should render each node with the default data', (nodeType) => { test.each(getAllTypes())('it should render %s node with the default data', (nodeType) => {
const lengthBefore = screen.getAllByText(/.*/).length;
let newNode = createNode(nodeType + "1", nodeType, {x: 200, y:200}, {}) let newNode = createNode(nodeType + "1", nodeType, {x: 200, y:200}, {})
let uiElement = Object.entries(NodeTypes).find(([t])=>t==nodeType)?.[1]!; let uiElement = Object.entries(NodeTypes).find(([t])=>t==nodeType)?.[1]!;
let props = { let props = {
@@ -45,11 +63,84 @@ describe('NormNode', () => {
draggable:true, draggable:true,
positionAbsoluteX:0, positionAbsoluteX:0,
positionAbsoluteY:0,} positionAbsoluteY:0,}
renderWithProviders(uiElement(props));
const elements = screen.queryAllByText((content, ) => renderWithProviders(createElement(uiElement as React.ComponentType<any>, props));
content.toLowerCase().includes(nodeType.toLowerCase()) const lengthAfter = screen.getAllByText(/.*/).length;
);
expect(elements.length).toBeGreaterThan(0); expect(lengthBefore + 1 == lengthAfter)
});
});
describe('Connecting', () => {
test.each(getAllTypes())('it should call the connect function when %s node is connected', (nodeType) => {
// Create two nodes - one of the current type and one to connect to
const sourceNode = createNode('source-1', nodeType, {x: 100, y: 100}, {});
const targetNode = createNode('target-1', 'end', {x: 300, y: 100}, {});
// Add nodes to store
useFlowStore.setState({ nodes: [sourceNode, targetNode] });
// Spy on the connect functions
const sourceConnectSpy = jest.spyOn(NodeConnects, nodeType as keyof typeof NodeConnects);
const targetConnectSpy = jest.spyOn(NodeConnects, 'end');
// Simulate connection
useFlowStore.getState().onConnect({
source: 'source-1',
target: 'target-1',
sourceHandle: null,
targetHandle: null,
});
// Verify the connect functions were called
expect(sourceConnectSpy).toHaveBeenCalledWith(sourceNode, targetNode, true);
expect(targetConnectSpy).toHaveBeenCalledWith(targetNode, sourceNode, false);
sourceConnectSpy.mockRestore();
targetConnectSpy.mockRestore();
});
});
describe('Reducing', () => {
test.each(getAllTypes())('it should correctly call/ not call the reduce function when %s node is in a phase', (nodeType) => {
// Create a phase node and a node of the current type
const phaseNode = createNode('phase-1', 'phase', {x: 200, y: 100}, { label: 'Test Phase', children: [] });
const testNode = createNode('node-1', nodeType, {x: 100, y: 100}, {});
// Add the test node as a child of the phase
(phaseNode.data as any).children.push(testNode.id);
// Add nodes to store
useFlowStore.setState({ nodes: [phaseNode, testNode] });
// Spy on the reduce functions
const phaseReduceSpy = jest.spyOn(NodeReduces, 'phase');
const nodeReduceSpy = jest.spyOn(NodeReduces, nodeType as keyof typeof NodeReduces);
// Simulate reducing - using the graphReducer
const result = graphReducer();
// Verify the reduce functions were called
expect(phaseReduceSpy).toHaveBeenCalledWith(phaseNode, [phaseNode, testNode]);
// Check if this node type is in NodesInPhase and returns false
const nodesInPhaseFunc = NodesInPhase[nodeType as keyof typeof NodesInPhase];
if (nodesInPhaseFunc && nodesInPhaseFunc() === false && nodeType !== 'phase') {
// Node is NOT in phase, so it should NOT be called
expect(nodeReduceSpy).not.toHaveBeenCalled();
} else {
// Node IS in phase, so it SHOULD be called
expect(nodeReduceSpy).toHaveBeenCalled();
}
// Verify the correct structure is present using NodesInPhase
expect(result).toHaveLength(nodeType !== 'phase' ? 1 : 2);
expect(result[0]).toHaveProperty('id', 'phase-1');
expect(result[0]).toHaveProperty('label', 'Test Phase');
// Restore mocks
phaseReduceSpy.mockRestore();
nodeReduceSpy.mockRestore();
}); });
}); });
}); });