Conditional Norms

This commit is contained in:
Björn Otgaar
2026-01-07 09:27:23 +00:00
committed by Gerla, J. (Justin)
parent 0ad2d5935f
commit 4e9a048c90
5 changed files with 270 additions and 35 deletions

View File

@@ -10,8 +10,9 @@ import 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>;
@@ -26,12 +27,7 @@ describe('NormNode', () => {
id: 'norm-1',
type: 'norm',
position: { x: 0, y: 0 },
data: {
label: 'Test Norm',
droppable: true,
norm: '',
hasReduce: true,
},
data: {...JSON.parse(JSON.stringify(NormNodeDefaults))},
};
renderWithProviders(
@@ -60,6 +56,7 @@ describe('NormNode', () => {
type: 'norm',
position: { x: 0, y: 0 },
data: {
...JSON.parse(JSON.stringify(NormNodeDefaults)),
label: 'Test Norm',
droppable: true,
norm: 'Be respectful to humans',
@@ -94,8 +91,10 @@ describe('NormNode', () => {
type: 'norm',
position: { x: 0, y: 0 },
data: {
...JSON.parse(JSON.stringify(NormNodeDefaults)),
label: 'Test Norm',
droppable: true,
conditions: [],
norm: '',
hasReduce: true,
critical: false
@@ -129,6 +128,7 @@ describe('NormNode', () => {
type: 'norm',
position: { x: 0, y: 0 },
data: {
...JSON.parse(JSON.stringify(NormNodeDefaults)),
label: 'Test Norm',
droppable: true,
norm: 'Dragged norm',
@@ -165,6 +165,7 @@ describe('NormNode', () => {
type: 'norm',
position: { x: 0, y: 0 },
data: {
...JSON.parse(JSON.stringify(NormNodeDefaults)),
label: 'Test Norm',
droppable: true,
norm: '',
@@ -210,6 +211,7 @@ describe('NormNode', () => {
type: 'norm',
position: { x: 0, y: 0 },
data: {
...JSON.parse(JSON.stringify(NormNodeDefaults)),
label: 'Test Norm',
droppable: true,
norm: 'Initial norm text',
@@ -261,6 +263,7 @@ describe('NormNode', () => {
type: 'norm',
position: { x: 0, y: 0 },
data: {
...JSON.parse(JSON.stringify(NormNodeDefaults)),
label: 'Test Norm',
droppable: true,
norm: '',
@@ -314,6 +317,7 @@ describe('NormNode', () => {
type: 'norm',
position: { x: 0, y: 0 },
data: {
...JSON.parse(JSON.stringify(NormNodeDefaults)),
label: 'Test Norm',
droppable: true,
norm: '',
@@ -358,6 +362,7 @@ describe('NormNode', () => {
type: 'norm',
position: { x: 0, y: 0 },
data: {
...JSON.parse(JSON.stringify(NormNodeDefaults)),
label: 'Test Norm',
droppable: true,
norm: '',
@@ -404,6 +409,7 @@ describe('NormNode', () => {
type: 'norm',
position: { x: 0, y: 0 },
data: {
...JSON.parse(JSON.stringify(NormNodeDefaults)),
label: 'Safety Norm',
droppable: true,
norm: 'Never harm humans',
@@ -418,6 +424,8 @@ describe('NormNode', () => {
id: 'norm-1',
label: 'Safety Norm',
norm: 'Never harm humans',
critical: false,
basic_beliefs: [],
});
});
@@ -427,6 +435,7 @@ describe('NormNode', () => {
type: 'norm',
position: { x: 0, y: 0 },
data: {
...JSON.parse(JSON.stringify(NormNodeDefaults)),
label: 'Norm 1',
droppable: true,
norm: 'Be helpful',
@@ -439,6 +448,7 @@ describe('NormNode', () => {
type: 'norm',
position: { x: 100, y: 0 },
data: {
...JSON.parse(JSON.stringify(NormNodeDefaults)),
label: 'Norm 2',
droppable: true,
norm: 'Be honest',
@@ -463,6 +473,7 @@ describe('NormNode', () => {
type: 'norm',
position: { x: 0, y: 0 },
data: {
...JSON.parse(JSON.stringify(NormNodeDefaults)),
label: 'Empty Norm',
droppable: true,
norm: '',
@@ -482,6 +493,7 @@ describe('NormNode', () => {
type: 'norm',
position: { x: 0, y: 0 },
data: {
...JSON.parse(JSON.stringify(NormNodeDefaults)),
label: 'Custom Label',
droppable: false,
norm: 'Test norm',
@@ -502,6 +514,7 @@ describe('NormNode', () => {
type: 'norm',
position: { x: 0, y: 0 },
data: {
...JSON.parse(JSON.stringify(NormNodeDefaults)),
label: 'Test Norm',
droppable: true,
norm: 'Test',
@@ -514,6 +527,7 @@ describe('NormNode', () => {
type: 'phase',
position: { x: 100, y: 0 },
data: {
...JSON.parse(JSON.stringify(NormNodeDefaults)),
label: 'Phase 1',
droppable: true,
children: [],
@@ -532,6 +546,7 @@ describe('NormNode', () => {
type: 'norm',
position: { x: 0, y: 0 },
data: {
...JSON.parse(JSON.stringify(NormNodeDefaults)),
label: 'Test Norm',
droppable: true,
norm: 'Test',
@@ -544,6 +559,7 @@ describe('NormNode', () => {
type: 'phase',
position: { x: 100, y: 0 },
data: {
...JSON.parse(JSON.stringify(NormNodeDefaults)),
label: 'Phase 1',
droppable: true,
children: [],
@@ -562,6 +578,7 @@ describe('NormNode', () => {
type: 'norm',
position: { x: 0, y: 0 },
data: {
...NormNodeDefaults,
label: 'Test Norm',
droppable: true,
norm: 'Test',
@@ -583,6 +600,7 @@ describe('NormNode', () => {
type: 'norm',
position: { x: 0, y: 0 },
data: {
...JSON.parse(JSON.stringify(NormNodeDefaults)),
label: 'Test Norm',
droppable: true,
norm: '',
@@ -634,6 +652,7 @@ describe('NormNode', () => {
type: 'norm',
position: { x: 0, y: 0 },
data: {
...JSON.parse(JSON.stringify(NormNodeDefaults)),
label: 'Test Norm',
droppable: true,
norm: '',
@@ -682,6 +701,7 @@ describe('NormNode', () => {
type: 'norm',
position: { x: 0, y: 0 },
data: {
...JSON.parse(JSON.stringify(NormNodeDefaults)),
label: 'Norm 1',
droppable: true,
norm: 'Original norm 1',
@@ -694,6 +714,7 @@ describe('NormNode', () => {
type: 'norm',
position: { x: 100, y: 0 },
data: {
...JSON.parse(JSON.stringify(NormNodeDefaults)),
label: 'Norm 2',
droppable: true,
norm: 'Original norm 2',
@@ -748,6 +769,7 @@ describe('NormNode', () => {
type: 'norm',
position: { x: 0, y: 0 },
data: {
...NormNodeDefaults,
label: 'Test Norm',
droppable: true,
norm: 'haa haa fuyaaah - link',
@@ -778,21 +800,154 @@ describe('NormNode', () => {
);
const input = screen.getByPlaceholderText('Pepper should ...');
expect(input).toBeDefined()
await user.type(input, 'a');
await user.type(input, 'a{enter}');
await waitFor(() => {
expect(useFlowStore.getState().nodes[0].data.norm).toBe('haa haa fuyaaah - link');
expect(useFlowStore.getState().nodes[0].data.norm).toBe('haa haa fuyaaah - linka');
});
await user.type(input, 'b');
await user.type(input, 'b{enter}');
await waitFor(() => {
expect(useFlowStore.getState().nodes[0].data.norm).toBe('haa haa fuyaaah - link');
expect(useFlowStore.getState().nodes[0].data.norm).toBe('haa haa fuyaaah - linkab');
});
await user.type(input, 'c');
await user.type(input, 'c{enter}');
await waitFor(() => {
expect(useFlowStore.getState().nodes[0].data.norm).toBe('haa haa fuyaaah - link');
}, { timeout: 3000 });
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))
}
};
const mockBelief2: Node = {
id: 'basic_belief-2',
type: 'basic_belief',
position: {x:300, y:300},
data: {
...JSON.parse(JSON.stringify(BasicBeliefNodeDefaults))
}
};
useFlowStore.setState({
nodes: [mockNode, mockBelief1, mockBelief2],
edges: [],
});
// Simulate connecting
useFlowStore.getState().onConnect({
source: 'basic_belief-1',
target: 'norm-1',
sourceHandle: null,
targetHandle: null,
});
useFlowStore.getState().onConnect({
source: 'basic_belief-2',
target: 'norm-1',
sourceHandle: null,
targetHandle: null,
});
const state = useFlowStore.getState();
const updatedNorm = state.nodes.find(n => n.id === 'norm-1');
expect(updatedNorm?.data.conditions).toEqual(["basic_belief-1", "basic_belief-2"]);
});
});
});

View File

@@ -8,7 +8,7 @@ import { createElement } from 'react';
import useFlowStore from '../../../../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores';
describe('NormNode', () => {
describe('Universal Nodes', () => {
beforeEach(() => {
jest.clearAllMocks();
});
@@ -107,6 +107,50 @@ describe('NormNode', () => {
});
});
describe('Disconnecting', () => {
test.each(getAllTypes())('it should remove the correct data when something is disconnected on a %s node.', (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', 'basic_belief', {x: 300, y: 100}, {});
// Add nodes to store
useFlowStore.setState({ nodes: [sourceNode, targetNode] });
// Spy on the connect functions
const sourceConnectSpy = jest.spyOn(NodeConnections.Sources, nodeType as keyof typeof NodeConnections.Sources);
const targetConnectSpy = jest.spyOn(NodeConnections.Targets, 'basic_belief');
// 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.id);
expect(targetConnectSpy).toHaveBeenCalledWith(targetNode, sourceNode.id);
// Find this connection, and delete it
const edge = useFlowStore.getState().edges[0];
useFlowStore.getState().onEdgesDelete([edge]);
// Find the nodes in the flow
const newSourceNode = useFlowStore.getState().nodes.find((node) => node.id == "source-1");
const newTargetNode = useFlowStore.getState().nodes.find((node) => node.id == "target-1");
// Expect them to be the same after deleting the edges
expect(newSourceNode).toBe(sourceNode);
expect(newTargetNode).toBe(targetNode);
// Restore our spies
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