From 95397ceccce111af897c0bef6d9ba075fc20d839 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Otgaar?= Date: Thu, 4 Dec 2025 12:33:27 +0100 Subject: [PATCH] fix: fix the tests by simulating user actions rather than the function, and avoid the cyclic dependancy which was present ref: N25B-371 --- .../visualProgrammingUI/NodeRegistry.ts | 2 - .../components/DragDropSidebar.tsx | 42 +++++++++- .../visualProgrammingUI/nodes/PhaseNode.tsx | 1 - .../visualProgrammingUI/utils/AddNode.ts | 39 ---------- .../nodes/PhaseNode.test.tsx | 78 +++++++++++++++++-- 5 files changed, 111 insertions(+), 51 deletions(-) delete mode 100644 src/pages/VisProgPage/visualProgrammingUI/utils/AddNode.ts diff --git a/src/pages/VisProgPage/visualProgrammingUI/NodeRegistry.ts b/src/pages/VisProgPage/visualProgrammingUI/NodeRegistry.ts index 8812434..84b0ec5 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/NodeRegistry.ts +++ b/src/pages/VisProgPage/visualProgrammingUI/NodeRegistry.ts @@ -79,7 +79,6 @@ export const NodeConnects = { export const NodeDeletes = { start: () => false, end: () => false, - test: () => false, // Used for coverage of universal/ undefined nodes } /** @@ -92,5 +91,4 @@ export const NodesInPhase = { start: () => false, end: () => false, phase: () => false, - test: () => false, // Used for coverage of universal/ undefined nodes } \ No newline at end of file diff --git a/src/pages/VisProgPage/visualProgrammingUI/components/DragDropSidebar.tsx b/src/pages/VisProgPage/visualProgrammingUI/components/DragDropSidebar.tsx index 1d4d931..8440552 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/components/DragDropSidebar.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/components/DragDropSidebar.tsx @@ -1,9 +1,9 @@ import { useDraggable } from '@neodrag/react'; import { useReactFlow, type XYPosition } from '@xyflow/react'; import { type ReactNode, useCallback, useRef, useState } from 'react'; +import useFlowStore from '../VisProgStores'; import styles from '../../VisProg.module.css'; import { NodeDefaults, type NodeTypes } from '../NodeRegistry' -import addNode from '../utils/AddNode'; /** * Props for a draggable node within the drag-and-drop toolbar. @@ -57,6 +57,46 @@ function DraggableNode({ className, children, nodeType, onDrop }: DraggableNodeP ); } +/** + * Adds a new node to the flow graph. + * + * Handles: + * - Automatic node ID generation based on existing nodes of the same type. + * - Loading of default data from the `NodeDefaults` registry. + * - Integration with the flow store to update global node state. + * + * @param nodeType - The type of node to create (from `NodeTypes`). + * @param position - The XY position in the flow canvas where the node will appear. + */ +function addNode(nodeType: keyof typeof NodeTypes, position: XYPosition) { + const { nodes, setNodes } = useFlowStore.getState(); + + // Load any predefined data for this node type. + const defaultData = NodeDefaults[nodeType] ?? {} + + // Currently, we find out what the Id is by checking the last node and adding one. + const sameTypeNodes = nodes.filter((node) => node.type === nodeType); + const nextNumber = + sameTypeNodes.length > 0 + ? (() => { + const lastNode = sameTypeNodes[sameTypeNodes.length - 1]; + const parts = lastNode.id.split('-'); + const lastNum = Number(parts[1]); + return Number.isNaN(lastNum) ? sameTypeNodes.length + 1 : lastNum + 1; + })() + : 1; + const id = `${nodeType}-${nextNumber}`; + + // Create new node + const newNode = { + id: id, + type: nodeType, + position, + data: JSON.parse(JSON.stringify(defaultData)) + } + setNodes([...nodes, newNode]); +} + /** * The drag-and-drop toolbar component for the visual programming interface. * diff --git a/src/pages/VisProgPage/visualProgrammingUI/nodes/PhaseNode.tsx b/src/pages/VisProgPage/visualProgrammingUI/nodes/PhaseNode.tsx index 56c762c..c8ea2c0 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/nodes/PhaseNode.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/nodes/PhaseNode.tsx @@ -110,7 +110,6 @@ export function PhaseReduce(node: Node, nodes: Node[]) { * @param isThisSource whether this instance of the node was the source in the connection, true = yes. */ export function PhaseConnects(thisNode: Node, otherNode: Node, isThisSource: boolean) { - console.log("Connect functionality called.") const node = thisNode as PhaseNode const data = node.data as PhaseNodeData if (!isThisSource) diff --git a/src/pages/VisProgPage/visualProgrammingUI/utils/AddNode.ts b/src/pages/VisProgPage/visualProgrammingUI/utils/AddNode.ts deleted file mode 100644 index b73d46b..0000000 --- a/src/pages/VisProgPage/visualProgrammingUI/utils/AddNode.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { XYPosition } from "@xyflow/react"; -import { NodeDefaults, type NodeTypes } from "../NodeRegistry"; -import useFlowStore from "../VisProgStores"; - - -/** - * addNode — adds a new node to the flow using the unified class-based system. - * Keeps numbering logic for phase/norm nodes. - */ -export default function addNode(nodeType: keyof typeof NodeTypes, position: XYPosition) { - const { nodes, setNodes } = useFlowStore.getState(); - - // Find out if there's any default data about our ndoe - const defaultData = NodeDefaults[nodeType] ?? {} - - // Currently, we find out what the Id is by checking the last node and adding one - const sameTypeNodes = nodes.filter((node) => node.type === nodeType); - const nextNumber = - sameTypeNodes.length > 0 - ? (() => { - const lastNode = sameTypeNodes[sameTypeNodes.length - 1]; - const parts = lastNode.id.split('-'); - const lastNum = Number(parts[1]); - return Number.isNaN(lastNum) ? sameTypeNodes.length + 1 : lastNum + 1; - })() - : 1; - const id = `${nodeType}-${nextNumber}`; - - // Create new node - const newNode = { - id: id, - type: nodeType, - position, - // Deep copy using JSON because thats how things work: - // Ref: https://developer.mozilla.org/en-US/docs/Glossary/Deep_copy - data: JSON.parse(JSON.stringify(defaultData)) - } - setNodes([...nodes, newNode]); -} \ No newline at end of file diff --git a/test/pages/visProgPage/visualProgrammingUI/nodes/PhaseNode.test.tsx b/test/pages/visProgPage/visualProgrammingUI/nodes/PhaseNode.test.tsx index 006380c..01de131 100644 --- a/test/pages/visProgPage/visualProgrammingUI/nodes/PhaseNode.test.tsx +++ b/test/pages/visProgPage/visualProgrammingUI/nodes/PhaseNode.test.tsx @@ -1,16 +1,78 @@ import useFlowStore from '../../../../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores'; -import addNode from "../../../../../src/pages/VisProgPage/visualProgrammingUI/utils/AddNode"; import type { PhaseNodeData } from "../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/PhaseNode"; +import { getByTestId, render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import VisProgPage from '../../../../../src/pages/VisProgPage/VisProg'; +class ResizeObserver { + observe() {} + unobserve() {} + disconnect() {} +} +window.ResizeObserver = ResizeObserver; + +jest.mock('@neodrag/react', () => ({ + useDraggable: (ref: React.RefObject, options: any) => { + // We access the real useEffect from React to attach a listener + // This bridges the gap between the test's userEvent and the component's logic + const { useEffect } = jest.requireActual('react'); + + useEffect(() => { + const element = ref.current; + if (!element) return; + + // When the test fires a "pointerup" (end of click/drag), + // we manually trigger the library's onDragEnd callback. + const handlePointerUp = (e: PointerEvent) => { + if (options.onDragEnd) { + options.onDragEnd({ event: e }); + } + }; + + element.addEventListener('pointerup', handlePointerUp as EventListener); + return () => { + element.removeEventListener('pointerup', handlePointerUp as EventListener); + }; + }, [ref, options]); + }, +})); + describe('PhaseNode', () => { - it('each created phase gets its own children array, not the same reference ', () => { - // Create nodes - addNode("phase", {x:10,y:10}) - addNode("phase", {x:20,y:20}) - addNode("norm", {x:30,y:30}) - addNode("norm", {x:40,y:40}) - addNode("goal", {x:50,y:50}) + it('each created phase gets its own children array, not the same reference ', async () => { + const user = userEvent.setup(); + + const { container } = render(); + + // --- Mock ReactFlow bounding box --- + // Your DndToolbar checks these values: + const flowEl = container.querySelector('.react-flow'); + jest.spyOn(flowEl!, 'getBoundingClientRect').mockReturnValue({ + left: 0, + right: 800, + top: 0, + bottom: 600, + width: 800, + height: 600, + x: 0, + y: 0, + toJSON: () => {}, + }); + + // Find the draggable norm node in the toolbar + const phaseButton = getByTestId(container, 'draggable-phase') + + // Simulate dropping phase down in graph (twice) + for (let i = 0; i < 2; i++) { + await user.pointer([ + // touch the screen at element1 + {keys: '[TouchA>]', target: phaseButton}, + // move the touch pointer to element2 + {pointerName: 'TouchA', coords: {x: 300, y: 250}}, + // release the touch pointer at the last position (element2) + {keys: '[/TouchA]'}, + ]); + } // Find nodes const nodes = useFlowStore.getState().nodes;