// This program has been developed by students from the bachelor Computer Science at Utrecht // University within the Software Project course. // © Copyright Utrecht University (Department of Information and Computing Sciences) import { describe, it, beforeEach } from '@jest/globals'; import { screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { renderWithProviders } from '../../../../test-utils/test-utils.tsx'; import NormNode, { NormReduce, type NormNodeData, NormConnectionSource, NormConnectionTarget } 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' import { NormNodeDefaults } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/NormNode.default.ts'; import { BasicBeliefNodeDefaults } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.default.ts'; import BasicBeliefNode, { BasicBeliefConnectionSource } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.tsx'; describe('NormNode', () => { let user: ReturnType; beforeEach(() => { 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: {...JSON.parse(JSON.stringify(NormNodeDefaults))}, }; 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: { ...JSON.parse(JSON.stringify(NormNodeDefaults)), 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: { ...JSON.parse(JSON.stringify(NormNodeDefaults)), label: 'Test Norm', droppable: true, conditions: [], norm: '', hasReduce: true, critical: false }, }; renderWithProviders( ); const 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: { ...JSON.parse(JSON.stringify(NormNodeDefaults)), 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: { ...JSON.parse(JSON.stringify(NormNodeDefaults)), 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: { ...JSON.parse(JSON.stringify(NormNodeDefaults)), 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: { ...JSON.parse(JSON.stringify(NormNodeDefaults)), 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: { ...JSON.parse(JSON.stringify(NormNodeDefaults)), 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: { ...JSON.parse(JSON.stringify(NormNodeDefaults)), 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 condition: Node = { id: "belief-1", type: 'basic_belief', position: {x: 10, y: 10}, data: { ...JSON.parse(JSON.stringify(BasicBeliefNodeDefaults)) } } const normNode: Node = { id: 'norm-1', type: 'norm', position: { x: 0, y: 0 }, data: { ...JSON.parse(JSON.stringify(NormNodeDefaults)), label: 'Safety Norm', droppable: true, norm: 'Never harm humans', hasReduce: true, condition: "belief-1" }, }; const allNodes: Node[] = [normNode, condition]; const result = NormReduce(normNode, allNodes); expect(result).toEqual({ id: 'norm-1', label: 'Safety Norm', norm: 'Never harm humans', critical: false, condition: { id: "belief-1", keyword: "" }, }); }); it('should reduce multiple norm nodes independently', () => { const norm1: Node = { id: 'norm-1', type: 'norm', position: { x: 0, y: 0 }, data: { ...JSON.parse(JSON.stringify(NormNodeDefaults)), label: 'Norm 1', droppable: true, norm: 'Be helpful', hasReduce: true, }, }; const norm2: Node = { id: 'norm-2', type: 'norm', position: { x: 100, y: 0 }, data: { ...JSON.parse(JSON.stringify(NormNodeDefaults)), 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: { ...JSON.parse(JSON.stringify(NormNodeDefaults)), 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: { ...JSON.parse(JSON.stringify(NormNodeDefaults)), 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: { ...JSON.parse(JSON.stringify(NormNodeDefaults)), label: 'Test Norm', droppable: true, norm: 'Test', hasReduce: true, }, }; const phaseNode: Node = { id: 'phase-1', type: 'phase', position: { x: 100, y: 0 }, data: { ...JSON.parse(JSON.stringify(NormNodeDefaults)), label: 'Phase 1', droppable: true, children: [], hasReduce: true, }, }; expect(() => { NormConnectionSource(normNode, phaseNode.id); }).not.toThrow(); }); it('should handle connection when norm is target', () => { const normNode: Node = { id: 'norm-1', type: 'norm', position: { x: 0, y: 0 }, data: { ...JSON.parse(JSON.stringify(NormNodeDefaults)), label: 'Test Norm', droppable: true, norm: 'Test', hasReduce: true, }, }; const phaseNode: Node = { id: 'phase-1', type: 'phase', position: { x: 100, y: 0 }, data: { ...JSON.parse(JSON.stringify(NormNodeDefaults)), label: 'Phase 1', droppable: true, children: [], hasReduce: true, }, }; expect(() => { NormConnectionTarget(normNode, phaseNode.id); }).not.toThrow(); }); it('should handle self-connection', () => { const normNode: Node = { id: 'norm-1', type: 'norm', position: { x: 0, y: 0 }, data: { ...NormNodeDefaults, label: 'Test Norm', droppable: true, norm: 'Test', hasReduce: true, }, }; expect(() => { NormConnectionTarget(normNode, normNode.id); NormConnectionSource(normNode, normNode.id); }).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: { ...JSON.parse(JSON.stringify(NormNodeDefaults)), 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 properly update the store when editing critical checkbox', async () => { const mockNode: Node = { id: 'norm-1', type: 'norm', position: { x: 0, y: 0 }, data: { ...JSON.parse(JSON.stringify(NormNodeDefaults)), label: 'Test Norm', droppable: true, norm: '', hasReduce: true, critical: false, }, }; useFlowStore.setState({ nodes: [mockNode], edges: [], }); renderWithProviders( ); 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(''); }); }); 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: { ...JSON.parse(JSON.stringify(NormNodeDefaults)), 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: { ...JSON.parse(JSON.stringify(NormNodeDefaults)), 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: { ...NormNodeDefaults, label: 'Test Norm', droppable: true, norm: 'haa haa fuyaaah - link', hasReduce: true, }, }; useFlowStore.setState({ nodes: [mockNode], edges: [], }); renderWithProviders( ); const input = screen.getByPlaceholderText('Pepper should ...'); expect(input).toBeDefined() await user.type(input, 'a{enter}'); await waitFor(() => { expect(useFlowStore.getState().nodes[0].data.norm).toBe('haa haa fuyaaah - linka'); }); await user.type(input, 'b{enter}'); await waitFor(() => { expect(useFlowStore.getState().nodes[0].data.norm).toBe('haa haa fuyaaah - linkab'); }); await user.type(input, 'c{enter}'); await waitFor(() => { expect(useFlowStore.getState().nodes[0].data.norm).toBe('haa haa fuyaaah - linkabc'); }); }); }); describe('Integration beliefs', () => { it('should update visually when adding beliefs', async () => { // Setup state const mockNode: Node = { id: 'norm-1', type: 'norm', position: { x: 0, y: 0 }, data: { ...JSON.parse(JSON.stringify(NormNodeDefaults)), label: 'Test Norm', droppable: true, norm: 'haa haa fuyaaah - link', hasReduce: true, } }; const mockBelief: Node = { id: 'basic_belief-1', type: 'basic_belief', position: {x:100, y:100}, data: { ...JSON.parse(JSON.stringify(BasicBeliefNodeDefaults)) } }; useFlowStore.setState({ nodes: [mockNode, mockBelief], edges: [], }); // Simulate connecting NormConnectionTarget(mockNode, mockBelief.id); BasicBeliefConnectionSource(mockBelief, mockNode.id) renderWithProviders(
); await waitFor(() => { expect(screen.getByTestId('norm-condition-information')).toBeInTheDocument(); }); }); it('should update the data when adding beliefs', async () => { // Setup state const mockNode: Node = { id: 'norm-1', type: 'norm', position: { x: 0, y: 0 }, data: { ...JSON.parse(JSON.stringify(NormNodeDefaults)), label: 'Test Norm', droppable: true, norm: 'haa haa fuyaaah - link', hasReduce: true, } }; const mockBelief1: Node = { id: 'basic_belief-1', type: 'basic_belief', position: {x:100, y:100}, data: { ...JSON.parse(JSON.stringify(BasicBeliefNodeDefaults)) } }; useFlowStore.setState({ nodes: [mockNode, mockBelief1], edges: [], }); // Simulate connecting useFlowStore.getState().onConnect({ source: 'basic_belief-1', target: 'norm-1', sourceHandle: null, targetHandle: null, }); const state = useFlowStore.getState(); const updatedNorm = state.nodes.find(n => n.id === 'norm-1'); expect(updatedNorm?.data.condition).toEqual("basic_belief-1"); }); }); });