From c5d9b8342d0163ca558c57132c468df8156536b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Otgaar?= Date: Thu, 27 Nov 2025 17:14:19 +0100 Subject: [PATCH] chore: create new tests for the UI, namely normnode, and one for all nodes --- .../components/DragDropSidebar.tsx | 7 +- test/components/Logging/Logging.test.tsx | 12 +- .../components/DragDropSidebar.test.tsx | 115 ++- .../nodes/NormNode.test.tsx | 745 ++++++++++++++++++ .../nodes/UniversalNodes.test.tsx | 55 ++ test/test-utils/mocks.ts | 41 + test/test-utils/test-utils.tsx | 35 + 7 files changed, 999 insertions(+), 11 deletions(-) create mode 100644 test/pages/visProgPage/visualProgrammingUI/nodes/NormNode.test.tsx create mode 100644 test/pages/visProgPage/visualProgrammingUI/nodes/UniversalNodes.test.tsx create mode 100644 test/test-utils/mocks.ts create mode 100644 test/test-utils/test-utils.tsx diff --git a/src/pages/VisProgPage/visualProgrammingUI/components/DragDropSidebar.tsx b/src/pages/VisProgPage/visualProgrammingUI/components/DragDropSidebar.tsx index 97b563b..fb4857e 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/components/DragDropSidebar.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/components/DragDropSidebar.tsx @@ -37,7 +37,11 @@ function DraggableNode({ className, children, nodeType, onDrop }: DraggableNodeP }); return ( -
+
{children}
); @@ -120,6 +124,7 @@ export function DndToolbar() { {/* Maps over all the nodes that are droppable, and puts them in the panel */} {droppableNodes.map(({type, data}) => ( { render(); - expect(screen.getByText("Logs")).toBeInTheDocument(); - expect(screen.getByText("WARNING")).toBeInTheDocument(); - expect(screen.getByText("logging")).toBeInTheDocument(); - expect(screen.getByText("Ping")).toBeInTheDocument(); + expect(screen.getByText("Logs")).toBeDefined(); + expect(screen.getByText("WARNING")).toBeDefined(); + expect(screen.getByText("logging")).toBeDefined(); + expect(screen.getByText("Ping")).toBeDefined(); let timestamp = screen.queryByText("ABS TIME"); if (!timestamp) { @@ -141,7 +141,7 @@ describe("Logging component", () => { } await user.click(timestamp); - expect(screen.getByText("00:00:12.345")).toBeInTheDocument(); + expect(screen.getByText("00:00:12.345")).toBeDefined(); }); it("shows the scroll-to-bottom button after a manual scroll and scrolls when clicked", async () => { @@ -188,7 +188,7 @@ describe("Logging component", () => { logCell.set({...current, message: "Updated"}); }); - expect(screen.getByText("Updated")).toBeInTheDocument(); + expect(screen.getByText("Updated")).toBeDefined(); await waitFor(() => { expect(scrollSpy).toHaveBeenCalledTimes(1); }); diff --git a/test/pages/visProgPage/visualProgrammingUI/components/DragDropSidebar.test.tsx b/test/pages/visProgPage/visualProgrammingUI/components/DragDropSidebar.test.tsx index 70087ee..a17fde8 100644 --- a/test/pages/visProgPage/visualProgrammingUI/components/DragDropSidebar.test.tsx +++ b/test/pages/visProgPage/visualProgrammingUI/components/DragDropSidebar.test.tsx @@ -1,5 +1,112 @@ -describe('Not implemented', () => { - test('nothing yet', () => { - expect(true) - }); +import { getByTestId, render } from '@testing-library/react'; +import useFlowStore from '../../../../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores'; +import VisProgPage from '../../../../../src/pages/VisProgPage/VisProg'; +import userEvent from '@testing-library/user-event'; + + + +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]); + }, +})); + +// We will mock @xyflow/react so we control screenToFlowPosition +jest.mock('@xyflow/react', () => { + const actual = jest.requireActual('@xyflow/react'); + return { + ...actual, + useReactFlow: () => ({ + screenToFlowPosition: ({ x, y }: { x: number; y: number }) => ({ + x: x - 100, + y: y - 100, + }), + }), + }; }); + +// Reset Zustand state helper +function resetStore() { + useFlowStore.setState({ nodes: [], edges: [] }); +} + +describe("Drag & drop node creation", () => { + beforeEach(() => resetStore()); + + test("drops a phase node inside the canvas and adds it with transformed position", 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: () => {}, + }); + + + const phaseLabel = getByTestId(container, 'draggable-phase') + + await user.pointer([ + // touch the screen at element1 + {keys: '[TouchA>]', target: phaseLabel}, + // move the touch pointer to element2 + {pointerName: 'TouchA', coords: {x: 300, y: 250}}, + // release the touch pointer at the last position (element2) + {keys: '[/TouchA]'}, + ]); + + // Read the Zustand store + const { nodes } = useFlowStore.getState(); + + // --- Assertions --- + expect(nodes.length).toBe(1); + + const node = nodes[0]; + + expect(node.type).toBe("phase"); + expect(node.id).toBe("phase-1"); + + // screenToFlowPosition was mocked to subtract 100 + expect(node.position).toEqual({ + x: 200, + y: 150, + }); + }); +}); \ No newline at end of file diff --git a/test/pages/visProgPage/visualProgrammingUI/nodes/NormNode.test.tsx b/test/pages/visProgPage/visualProgrammingUI/nodes/NormNode.test.tsx new file mode 100644 index 0000000..9e3d049 --- /dev/null +++ b/test/pages/visProgPage/visualProgrammingUI/nodes/NormNode.test.tsx @@ -0,0 +1,745 @@ +import { describe, it, beforeEach } from '@jest/globals'; +import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { renderWithProviders, resetFlowStore } from '../.././/./../../test-utils/test-utils'; +import NormNode, { NormReduce, NormConnects, type NormNodeData } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/NormNode' +import useFlowStore from '../../../../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores'; +import type { Node } from '@xyflow/react'; +import '@testing-library/jest-dom' + + + +describe('NormNode', () => { + let user: ReturnType; + + beforeEach(() => { + resetFlowStore(); + user = userEvent.setup(); + }); + + describe('Rendering', () => { + it('should render the norm node with default data', () => { + const mockNode: Node = { + id: 'norm-1', + type: 'norm', + position: { x: 0, y: 0 }, + data: { + label: 'Test Norm', + droppable: true, + norm: '', + hasReduce: true, + }, + }; + + renderWithProviders( + + ); + + expect(screen.getByPlaceholderText('Pepper should ...')).toBeInTheDocument(); + }); + + it('should render with pre-populated norm text', () => { + const mockNode: Node = { + id: 'norm-1', + type: 'norm', + position: { x: 0, y: 0 }, + data: { + label: 'Test Norm', + droppable: true, + norm: 'Be respectful to humans', + hasReduce: true, + }, + }; + + renderWithProviders( + + ); + + const input = screen.getByDisplayValue('Be respectful to humans'); + expect(input).toBeInTheDocument(); + }); + + it('should render with selected state', () => { + const mockNode: Node = { + id: 'norm-1', + type: 'norm', + position: { x: 0, y: 0 }, + data: { + label: 'Test Norm', + droppable: true, + norm: '', + hasReduce: true, + }, + }; + + renderWithProviders( + + ); + + let norm = screen.getByText("Norm :") + expect(norm).toBeInTheDocument; + }); + + it('should render with dragging state', () => { + const mockNode: Node = { + id: 'norm-1', + type: 'norm', + position: { x: 0, y: 0 }, + data: { + label: 'Test Norm', + droppable: true, + norm: 'Dragged norm', + hasReduce: true, + }, + }; + + renderWithProviders( + + ); + + const input = screen.getByDisplayValue('Dragged norm'); + expect(input).toBeInTheDocument(); + }); + }); + + describe('User Interactions', () => { + it('should update norm text when user types in the input field', async () => { + const mockNode: Node = { + id: 'norm-1', + type: 'norm', + position: { x: 0, y: 0 }, + data: { + label: 'Test Norm', + droppable: true, + norm: '', + hasReduce: true, + }, + }; + + useFlowStore.setState({ + nodes: [mockNode], + edges: [], + }); + + renderWithProviders( + + ); + + const input = screen.getByPlaceholderText('Pepper should ...'); + await user.type(input, 'Be polite to guests{enter}'); + + await waitFor(() => { + const state = useFlowStore.getState(); + const updatedNode = state.nodes.find(n => n.id === 'norm-1'); + expect(updatedNode?.data.norm).toBe('Be polite to guests'); + }); + }); + + it('should handle clearing the norm text', async () => { + const mockNode: Node = { + id: 'norm-1', + type: 'norm', + position: { x: 0, y: 0 }, + data: { + label: 'Test Norm', + droppable: true, + norm: 'Initial norm text', + hasReduce: true, + }, + }; + + useFlowStore.setState({ + nodes: [mockNode], + edges: [], + }); + + renderWithProviders( + + ); + + const input = screen.getByDisplayValue('Initial norm text') as HTMLInputElement; + + // clearing the norm text is the same as just deleting all characters one by one + // TODO: FIGURE OUT A MORE EFFICIENT WAY, I"M SORRY, user.clear() just doesn't work:/ + for (let a = 0; a < 'Initial norm text'.length; a++){ + await user.type(input, '{backspace}') + } + await user.type(input,'{enter}') + + await waitFor(() => { + const state = useFlowStore.getState(); + const updatedNode = state.nodes.find(n => n.id === 'norm-1'); + expect(updatedNode?.data.norm).toBe(''); + }); + }); + + it('should update norm text multiple times', async () => { + const mockNode: Node = { + id: 'norm-1', + type: 'norm', + position: { x: 0, y: 0 }, + data: { + label: 'Test Norm', + droppable: true, + norm: '', + hasReduce: true, + }, + }; + + useFlowStore.setState({ + nodes: [mockNode], + edges: [], + }); + + renderWithProviders( + + ); + + const input = screen.getByPlaceholderText('Pepper should ...'); + await user.type(input, 'First norm{enter}'); + await waitFor(() => { + expect(useFlowStore.getState().nodes[0].data.norm).toBe('First norm'); + }); + + + // TODO: FIGURE OUT A MORE EFFICIENT WAY, I"M SORRY, user.clear() just doesn't work:/ + for (let a = 0; a < 'First norm'.length; a++){ + await user.type(input, '{backspace}') + } + + await user.type(input, 'Second norm{enter}'); + await waitFor(() => { + expect(useFlowStore.getState().nodes[0].data.norm).toBe('Second norm'); + }); + }); + + it('should handle special characters in norm text', async () => { + const mockNode: Node = { + id: 'norm-1', + type: 'norm', + position: { x: 0, y: 0 }, + data: { + label: 'Test Norm', + droppable: true, + norm: '', + hasReduce: true, + }, + }; + + useFlowStore.setState({ + nodes: [mockNode], + edges: [], + }); + + renderWithProviders( + + ); + + const input = screen.getByPlaceholderText('Pepper should ...'); + await user.type(input, "Don't harm & be nice!{enter}" ); + + await waitFor(() => { + expect(useFlowStore.getState().nodes[0].data.norm).toBe("Don't harm & be nice!"); + }); + }); + + it('should handle long norm text', async () => { + const longText = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'; + const mockNode: Node = { + id: 'norm-1', + type: 'norm', + position: { x: 0, y: 0 }, + data: { + label: 'Test Norm', + droppable: true, + norm: '', + hasReduce: true, + }, + }; + + useFlowStore.setState({ + nodes: [mockNode], + edges: [], + }); + + renderWithProviders( + + ); + + const input = screen.getByPlaceholderText('Pepper should ...'); + await user.type(input, longText); + await user.type(input, "{enter}") + + await waitFor(() => { + expect(useFlowStore.getState().nodes[0].data.norm).toBe(longText); + }); + }); + }); + + describe('NormReduce Function', () => { + it('should reduce a norm node to its essential data', () => { + const normNode: Node = { + id: 'norm-1', + type: 'norm', + position: { x: 0, y: 0 }, + data: { + label: 'Safety Norm', + droppable: true, + norm: 'Never harm humans', + hasReduce: true, + }, + }; + + const allNodes: Node[] = [normNode]; + const result = NormReduce(normNode, allNodes); + + expect(result).toEqual({ + id: 'norm-1', + label: 'Safety Norm', + norm: 'Never harm humans', + }); + }); + + it('should reduce multiple norm nodes independently', () => { + const norm1: Node = { + id: 'norm-1', + type: 'norm', + position: { x: 0, y: 0 }, + data: { + label: 'Norm 1', + droppable: true, + norm: 'Be helpful', + hasReduce: true, + }, + }; + + const norm2: Node = { + id: 'norm-2', + type: 'norm', + position: { x: 100, y: 0 }, + data: { + label: 'Norm 2', + droppable: true, + norm: 'Be honest', + hasReduce: true, + }, + }; + + const allNodes: Node[] = [norm1, norm2]; + + const result1 = NormReduce(norm1, allNodes); + const result2 = NormReduce(norm2, allNodes); + + expect(result1.id).toBe('norm-1'); + expect(result1.norm).toBe('Be helpful'); + expect(result2.id).toBe('norm-2'); + expect(result2.norm).toBe('Be honest'); + }); + + it('should handle empty norm text', () => { + const normNode: Node = { + id: 'norm-1', + type: 'norm', + position: { x: 0, y: 0 }, + data: { + label: 'Empty Norm', + droppable: true, + norm: '', + hasReduce: true, + }, + }; + + const result = NormReduce(normNode, [normNode]); + + expect(result.norm).toBe(''); + expect(result.id).toBe('norm-1'); + }); + + it('should preserve node label in reduction', () => { + const normNode: Node = { + id: 'norm-1', + type: 'norm', + position: { x: 0, y: 0 }, + data: { + label: 'Custom Label', + droppable: false, + norm: 'Test norm', + hasReduce: false, + }, + }; + + const result = NormReduce(normNode, [normNode]); + + expect(result.label).toBe('Custom Label'); + }); + }); + + describe('NormConnects Function', () => { + it('should handle connection without errors', () => { + 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: [], + hasReduce: true, + }, + }; + + expect(() => { + NormConnects(normNode, phaseNode, true); + }).not.toThrow(); + }); + + it('should handle connection when norm is target', () => { + 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: [], + hasReduce: true, + }, + }; + + expect(() => { + NormConnects(normNode, phaseNode, false); + }).not.toThrow(); + }); + + it('should handle self-connection', () => { + const normNode: Node = { + id: 'norm-1', + type: 'norm', + position: { x: 0, y: 0 }, + data: { + label: 'Test Norm', + droppable: true, + norm: 'Test', + hasReduce: true, + }, + }; + + expect(() => { + NormConnects(normNode, normNode, true); + }).not.toThrow(); + }); + }); + + describe('Integration with Store', () => { + it('should properly update the store when editing norm text', async () => { + const mockNode: Node = { + id: 'norm-1', + type: 'norm', + position: { x: 0, y: 0 }, + data: { + label: 'Test Norm', + droppable: true, + norm: '', + hasReduce: true, + }, + }; + + useFlowStore.setState({ + nodes: [mockNode], + edges: [], + }); + + renderWithProviders( + + ); + + const input = screen.getByPlaceholderText('Pepper should ...'); + + // TODO: FIGURE OUT A MORE EFFICIENT WAY, I"M SORRY, user.clear() just doesn't work:/ + for (let a = 0; a < 20; a++){ + await user.type(input, '{backspace}') + } + await user.type(input, 'New norm value{enter}'); + + await waitFor(() => { + const state = useFlowStore.getState(); + expect(state.nodes).toHaveLength(1); + expect(state.nodes[0].id).toBe('norm-1'); + expect(state.nodes[0].data.norm).toBe('New norm value'); + }); + }); + + it('should not affect other nodes when updating one norm node', async () => { + const norm1: Node = { + id: 'norm-1', + type: 'norm', + position: { x: 0, y: 0 }, + data: { + label: 'Norm 1', + droppable: true, + norm: 'Original norm 1', + hasReduce: true, + }, + }; + + const norm2: Node = { + id: 'norm-2', + type: 'norm', + position: { x: 100, y: 0 }, + data: { + label: 'Norm 2', + droppable: true, + norm: 'Original norm 2', + hasReduce: true, + }, + }; + + useFlowStore.setState({ + nodes: [norm1, norm2], + edges: [], + }); + + renderWithProviders( + + ); + + const input = screen.getByDisplayValue('Original norm 1') as HTMLInputElement; + + + // TODO: FIGURE OUT A MORE EFFICIENT WAY, I"M SORRY, user.clear() just doesn't work:/ + for (let a = 0; a < 20; a++){ + await user.type(input, '{backspace}') + } + await user.type(input, 'Updated norm 1{enter}'); + + await waitFor(() => { + const state = useFlowStore.getState(); + const updatedNorm1 = state.nodes.find(n => n.id === 'norm-1'); + const unchangedNorm2 = state.nodes.find(n => n.id === 'norm-2'); + + expect(updatedNorm1?.data.norm).toBe('Updated norm 1'); + expect(unchangedNorm2?.data.norm).toBe('Original norm 2'); + }); + }); + + it('should maintain data consistency with multiple rapid updates', async () => { + const mockNode: Node = { + id: 'norm-1', + type: 'norm', + position: { x: 0, y: 0 }, + data: { + label: 'Test Norm', + droppable: true, + norm: 'haa haa fuyaaah - link', + hasReduce: true, + }, + }; + + useFlowStore.setState({ + nodes: [mockNode], + edges: [], + }); + + renderWithProviders( + + ); + + const input = screen.getByPlaceholderText('Pepper should ...'); + + await user.type(input, 'a'); + await waitFor(() => { + expect(useFlowStore.getState().nodes[0].data.norm).toBe('haa haa fuyaaah - link'); + }); + + await user.type(input, 'b'); + await waitFor(() => { + expect(useFlowStore.getState().nodes[0].data.norm).toBe('haa haa fuyaaah - link'); + }); + + await user.type(input, 'c'); + await waitFor(() => { + expect(useFlowStore.getState().nodes[0].data.norm).toBe('haa haa fuyaaah - link'); + }, { timeout: 3000 }); + }); + }); +}); \ No newline at end of file diff --git a/test/pages/visProgPage/visualProgrammingUI/nodes/UniversalNodes.test.tsx b/test/pages/visProgPage/visualProgrammingUI/nodes/UniversalNodes.test.tsx new file mode 100644 index 0000000..7e1e9ca --- /dev/null +++ b/test/pages/visProgPage/visualProgrammingUI/nodes/UniversalNodes.test.tsx @@ -0,0 +1,55 @@ +import { describe, beforeEach } from '@jest/globals'; +import { screen } from '@testing-library/react'; +import { renderWithProviders, resetFlowStore } from '../.././/./../../test-utils/test-utils'; +import type { XYPosition } from '@xyflow/react'; +import { NodeTypes, NodeDefaults } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/NodeRegistry'; +import '@testing-library/jest-dom' + + +describe('NormNode', () => { + // let user: ReturnType; + + // Copied from VisStores. + function createNode(id: string, type: string, position: XYPosition, data: Record, deletable? : boolean) { + const defaultData = NodeDefaults[type as keyof typeof NodeDefaults] + const newData = { + id: id, + type: type, + position: position, + data: data, + deletable: deletable, + } + return {...defaultData, ...newData} + } + + + beforeEach(() => { + resetFlowStore(); + // user = userEvent.setup(); + }); + + describe('Rendering', () => { + test.each([Object.entries(NodeTypes)].map(([t])=>t))('it should render each node with the default data', (nodeType) => { + let newNode = createNode(nodeType + "1", nodeType, {x: 200, y:200}, {}) + let uiElement = Object.entries(NodeTypes).find(([t])=>t==nodeType)?.[1]!; + let 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(uiElement(props)); + const elements = screen.queryAllByText((content, ) => + content.toLowerCase().includes(nodeType.toLowerCase()) + ); + expect(elements.length).toBeGreaterThan(0); + }); + }); +}); \ No newline at end of file diff --git a/test/test-utils/mocks.ts b/test/test-utils/mocks.ts new file mode 100644 index 0000000..21971c1 --- /dev/null +++ b/test/test-utils/mocks.ts @@ -0,0 +1,41 @@ +import { jest } from '@jest/globals'; +import React from 'react'; +import '@testing-library/jest-dom'; + +/** + * Mock for @xyflow/react + * Provides simplified versions of React Flow hooks and components + */ +jest.mock('@xyflow/react', () => ({ + useReactFlow: jest.fn(() => ({ + screenToFlowPosition: jest.fn((pos: any) => pos), + getNode: jest.fn(), + getNodes: jest.fn(() => []), + getEdges: jest.fn(() => []), + setNodes: jest.fn(), + setEdges: jest.fn(), + })), + ReactFlowProvider: ({ children }: { children: React.ReactNode }) => + React.createElement('div', { 'data-testid': 'react-flow-provider' }, children), + ReactFlow: ({ children, ...props }: any) => + React.createElement('div', { 'data-testid': 'react-flow', ...props }, children), + Handle: ({ type, position, id }: any) => + React.createElement('div', { 'data-testid': `handle-${type}-${id}`, 'data-position': position }), + Panel: ({ children, position }: any) => + React.createElement('div', { 'data-testid': 'panel', 'data-position': position }, children), + Controls: () => React.createElement('div', { 'data-testid': 'controls' }), + Background: () => React.createElement('div', { 'data-testid': 'background' }), +})); + +/** + * Mock for @neodrag/react + * Simplifies drag behavior for testing + */ +jest.mock('@neodrag/react', () => ({ + useDraggable: jest.fn((ref: any, options?: any) => { + // Store the options so we can trigger them in tests + if (ref && ref.current) { + (ref.current as any)._dragOptions = options; + } + }), +})); \ No newline at end of file diff --git a/test/test-utils/test-utils.tsx b/test/test-utils/test-utils.tsx new file mode 100644 index 0000000..76878b9 --- /dev/null +++ b/test/test-utils/test-utils.tsx @@ -0,0 +1,35 @@ +// __tests__/utils/test-utils.tsx +import { render, type RenderOptions } from '@testing-library/react'; +import { type ReactElement, type ReactNode } from 'react'; +import { ReactFlowProvider } from '@xyflow/react'; +import useFlowStore from '../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores'; + +/** + * Custom render function that wraps components with necessary providers + * This ensures all components have access to ReactFlow context + */ +export function renderWithProviders( + ui: ReactElement, + options?: Omit +) { + function Wrapper({ children }: { children: ReactNode }) { + return {children}; + } + + return render(ui, { wrapper: Wrapper, ...options }); +} + +/** + * Helper to reset the Zustand store between tests + * This ensures test isolation + */ +export function resetFlowStore() { + useFlowStore.setState({ + nodes: [], + edges: [], + edgeReconnectSuccessful: true, + }); +} + +// Re-export everything from testing library +export * from '@testing-library/react'; \ No newline at end of file