Files
pepperplus-ui/test/pages/visProgPage/visualProgrammingUI/nodes/NormNode.test.tsx

952 lines
26 KiB
TypeScript

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<typeof userEvent.setup>;
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(
<NormNode
id={mockNode.id}
type={mockNode.type as string}
data={mockNode.data as any}
selected={false}
isConnectable={true}
zIndex={0}
dragging={false}
selectable={true}
deletable={true}
draggable={true}
positionAbsoluteX={0}
positionAbsoluteY={0}
/>
);
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(
<NormNode
id={mockNode.id}
type={mockNode.type as string}
data={mockNode.data as any}
selected={false}
isConnectable={true}
zIndex={0}
dragging={false}
selectable={true}
deletable={true}
draggable={true}
positionAbsoluteX={0}
positionAbsoluteY={0}
/>
);
const input = screen.getByDisplayValue('Be respectful to humans');
expect(input).toBeInTheDocument();
});
it('should render with selected state', () => {
const mockNode: Node<NormNodeData> = {
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(
<NormNode
id={mockNode.id}
type={mockNode.type as string}
data={mockNode.data as any}
selected={false}
isConnectable={true}
zIndex={0}
dragging={false}
selectable={true}
deletable={true}
draggable={true}
positionAbsoluteX={0}
positionAbsoluteY={0}
/>
);
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(
<NormNode
id={mockNode.id}
type={mockNode.type as string}
data={mockNode.data as any}
selected={false}
isConnectable={true}
zIndex={0}
dragging={false}
selectable={true}
deletable={true}
draggable={true}
positionAbsoluteX={0}
positionAbsoluteY={0}
/>
);
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(
<NormNode
id={mockNode.id}
type={mockNode.type as string}
data={mockNode.data as any}
selected={false}
isConnectable={true}
zIndex={0}
dragging={false}
selectable={true}
deletable={true}
draggable={true}
positionAbsoluteX={0}
positionAbsoluteY={0}
/>
);
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(
<NormNode
id={mockNode.id}
type={mockNode.type as string}
data={mockNode.data as any}
selected={false}
isConnectable={true}
zIndex={0}
dragging={false}
selectable={true}
deletable={true}
draggable={true}
positionAbsoluteX={0}
positionAbsoluteY={0}
/>
);
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(
<NormNode
id={mockNode.id}
type={mockNode.type as string}
data={mockNode.data as any}
selected={false}
isConnectable={true}
zIndex={0}
dragging={false}
selectable={true}
deletable={true}
draggable={true}
positionAbsoluteX={0}
positionAbsoluteY={0}
/>
);
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(
<NormNode
id={mockNode.id}
type={mockNode.type as string}
data={mockNode.data as any}
selected={false}
isConnectable={true}
zIndex={0}
dragging={false}
selectable={true}
deletable={true}
draggable={true}
positionAbsoluteX={0}
positionAbsoluteY={0}
/>
);
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(
<NormNode
id={mockNode.id}
type={mockNode.type as string}
data={mockNode.data as any}
selected={false}
isConnectable={true}
zIndex={0}
dragging={false}
selectable={true}
deletable={true}
draggable={true}
positionAbsoluteX={0}
positionAbsoluteY={0}
/>
);
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(
<NormNode
id={mockNode.id}
type={mockNode.type as string}
data={mockNode.data as any}
selected={false}
isConnectable={true}
zIndex={0}
dragging={false}
selectable={true}
deletable={true}
draggable={true}
positionAbsoluteX={0}
positionAbsoluteY={0}
/>
);
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(
<NormNode
id={mockNode.id}
type={mockNode.type as string}
data={mockNode.data as any}
selected={false}
isConnectable={true}
zIndex={0}
dragging={false}
selectable={true}
deletable={true}
draggable={true}
positionAbsoluteX={0}
positionAbsoluteY={0}
/>
);
const checkbox = screen.getByLabelText('Critical:');
await user.click(checkbox);
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('');
expect(state.nodes[0].data.critical).toBe(true);
});
});
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(
<NormNode
id={norm1.id}
type={norm1.type as string}
data={norm1.data as any}
selected={false}
isConnectable={true}
zIndex={0}
dragging={false}
selectable={true}
deletable={true}
draggable={true}
positionAbsoluteX={0}
positionAbsoluteY={0}
/>
);
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(
<NormNode
id={mockNode.id}
type={mockNode.type as string}
data={mockNode.data as any}
selected={false}
isConnectable={true}
zIndex={0}
dragging={false}
selectable={true}
deletable={true}
draggable={true}
positionAbsoluteX={0}
positionAbsoluteY={0}
/>
);
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(
<div>
<NormNode
id={mockNode.id}
type={mockNode.type as string}
data={mockNode.data as any}
selected={false}
isConnectable={true}
zIndex={0}
dragging={false}
selectable={true}
deletable={true}
draggable={true}
positionAbsoluteX={0}
positionAbsoluteY={0}
/>
<BasicBeliefNode
id={mockBelief.id}
type={mockBelief.type as string}
data={mockBelief.data as any}
selected={false}
isConnectable={true}
zIndex={0}
dragging={false}
selectable={true}
deletable={true}
draggable={true}
positionAbsoluteX={0}
positionAbsoluteY={0}
/>
</div>
);
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");
});
});
});