feat: added endpoint
ref:N25B-400
This commit is contained in:
@@ -3,6 +3,9 @@ import useFlowStore from '../../../../src/pages/VisProgPage/visualProgrammingUI/
|
||||
import { mockReactFlow } from '../../../setupFlowTests.ts';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
beforeAll(() => {
|
||||
mockReactFlow();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,743 @@
|
||||
// BasicBeliefNode.test.tsx
|
||||
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';
|
||||
import BasicBeliefNode, { type BasicBeliefNodeData } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode';
|
||||
import useFlowStore from '../../../../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores';
|
||||
import type { Node } from '@xyflow/react';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
describe('BasicBeliefNode', () => {
|
||||
let user: ReturnType<typeof userEvent.setup>;
|
||||
|
||||
beforeEach(() => {
|
||||
user = userEvent.setup();
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render the basic belief node with keyword type by default', () => {
|
||||
const mockNode: Node<BasicBeliefNodeData> = {
|
||||
id: 'belief-1',
|
||||
type: 'basic_belief',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
label: 'Belief',
|
||||
droppable: true,
|
||||
belief: { type: 'keyword', id: 'help', value: 'help', label: 'Keyword said:' },
|
||||
hasReduce: true,
|
||||
},
|
||||
};
|
||||
|
||||
renderWithProviders(
|
||||
<BasicBeliefNode
|
||||
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.getByText('Belief:')).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue('Keyword said:')).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue('help')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render with semantic belief type', () => {
|
||||
const mockNode: Node<BasicBeliefNodeData> = {
|
||||
id: 'belief-2',
|
||||
type: 'basic_belief',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
label: 'Belief',
|
||||
droppable: true,
|
||||
belief: { type: 'semantic', id: 'test', value: 'test value', label: 'Detected with LLM:' },
|
||||
hasReduce: true,
|
||||
},
|
||||
};
|
||||
|
||||
renderWithProviders(
|
||||
<BasicBeliefNode
|
||||
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.getByDisplayValue('Detected with LLM:')).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue('test value')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render with object belief type', () => {
|
||||
const mockNode: Node<BasicBeliefNodeData> = {
|
||||
id: 'belief-3',
|
||||
type: 'basic_belief',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
label: 'Belief',
|
||||
droppable: true,
|
||||
belief: { type: 'object', id: 'obj1', value: 'cup', label: 'Object found:' },
|
||||
hasReduce: true,
|
||||
},
|
||||
};
|
||||
|
||||
renderWithProviders(
|
||||
<BasicBeliefNode
|
||||
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.getByDisplayValue('Object found:')).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue('cup')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render with emotion belief type and select dropdown', () => {
|
||||
const mockNode: Node<BasicBeliefNodeData> = {
|
||||
id: 'belief-4',
|
||||
type: 'basic_belief',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
label: 'Belief',
|
||||
droppable: true,
|
||||
belief: { type: 'emotion', id: 'em1', value: 'happy', label: 'Emotion recognised:' },
|
||||
hasReduce: true,
|
||||
},
|
||||
};
|
||||
|
||||
renderWithProviders(
|
||||
<BasicBeliefNode
|
||||
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.getByDisplayValue('Emotion recognised:')).toBeInTheDocument();
|
||||
// For emotion type, we should check that the select has the correct value selected
|
||||
const selectElement = screen.getByDisplayValue('Happy');
|
||||
expect(selectElement).toBeInTheDocument();
|
||||
expect((selectElement as HTMLSelectElement).value).toBe('happy');
|
||||
});
|
||||
|
||||
it('should render emotion dropdown with all emotion options', () => {
|
||||
const mockNode: Node<BasicBeliefNodeData> = {
|
||||
id: 'belief-5',
|
||||
type: 'basic_belief',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
label: 'Belief',
|
||||
droppable: true,
|
||||
belief: { type: 'emotion', id: 'em1', value: 'happy', label: 'Emotion recognised:' },
|
||||
hasReduce: true,
|
||||
},
|
||||
};
|
||||
|
||||
renderWithProviders(
|
||||
<BasicBeliefNode
|
||||
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 selectElement = screen.getByDisplayValue('Happy');
|
||||
expect(selectElement).toBeInTheDocument();
|
||||
|
||||
// Check that all emotion options are present
|
||||
expect(screen.getByText('Happy')).toBeInTheDocument();
|
||||
expect(screen.getByText('Angry')).toBeInTheDocument();
|
||||
expect(screen.getByText('Sad')).toBeInTheDocument();
|
||||
expect(screen.getByText('Cheerful')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render without wrapping quotes for object type', () => {
|
||||
const mockNode: Node<BasicBeliefNodeData> = {
|
||||
id: 'belief-6',
|
||||
type: 'basic_belief',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
label: 'Belief',
|
||||
droppable: true,
|
||||
belief: { type: 'object', id: 'obj1', value: 'chair', label: 'Object found:' },
|
||||
hasReduce: true,
|
||||
},
|
||||
};
|
||||
|
||||
renderWithProviders(
|
||||
<BasicBeliefNode
|
||||
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}
|
||||
/>
|
||||
);
|
||||
|
||||
// Object type should not have wrapping quotes
|
||||
const inputs = screen.getAllByDisplayValue('chair');
|
||||
expect(inputs.length).toBe(1); // Only the text input, no extra quote elements
|
||||
});
|
||||
});
|
||||
|
||||
describe('User Interactions', () => {
|
||||
it('should update belief type when select is changed', async () => {
|
||||
const mockNode: Node<BasicBeliefNodeData> = {
|
||||
id: 'belief-1',
|
||||
type: 'basic_belief',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
label: 'Belief',
|
||||
droppable: true,
|
||||
belief: { type: 'keyword', id: 'kw1', value: 'hello', label: 'Keyword said:' },
|
||||
hasReduce: true,
|
||||
},
|
||||
};
|
||||
|
||||
useFlowStore.setState({
|
||||
nodes: [mockNode],
|
||||
edges: [],
|
||||
});
|
||||
|
||||
renderWithProviders(
|
||||
<BasicBeliefNode
|
||||
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 select = screen.getByDisplayValue('Keyword said:');
|
||||
await user.selectOptions(select, 'semantic');
|
||||
|
||||
await waitFor(() => {
|
||||
const state = useFlowStore.getState();
|
||||
const updatedNode = state.nodes.find(n => n.id === 'belief-1') as Node<BasicBeliefNodeData>;
|
||||
expect(updatedNode?.data.belief.type).toBe('semantic');
|
||||
// Note: The component doesn't update the label when changing type
|
||||
// So we can't test for label change
|
||||
});
|
||||
});
|
||||
|
||||
it('should update text value when typing for keyword type', async () => {
|
||||
const mockNode: Node<BasicBeliefNodeData> = {
|
||||
id: 'belief-1',
|
||||
type: 'basic_belief',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
label: 'Belief',
|
||||
droppable: true,
|
||||
belief: { type: 'keyword', id: 'kw1', value: '', label: 'Keyword said:' },
|
||||
hasReduce: true,
|
||||
},
|
||||
};
|
||||
|
||||
useFlowStore.setState({
|
||||
nodes: [mockNode],
|
||||
edges: [],
|
||||
});
|
||||
|
||||
renderWithProviders(
|
||||
<BasicBeliefNode
|
||||
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('keyword...');
|
||||
await user.type(input, 'help me{enter}');
|
||||
|
||||
await waitFor(() => {
|
||||
const state = useFlowStore.getState();
|
||||
const updatedNode = state.nodes.find(n => n.id === 'belief-1') as Node<BasicBeliefNodeData>;
|
||||
expect(updatedNode?.data.belief.value).toBe('help me');
|
||||
});
|
||||
});
|
||||
|
||||
it('should update text value when typing for semantic type', async () => {
|
||||
const mockNode: Node<BasicBeliefNodeData> = {
|
||||
id: 'belief-1',
|
||||
type: 'basic_belief',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
label: 'Belief',
|
||||
droppable: true,
|
||||
belief: { type: 'semantic', id: 'sem1', value: 'initial', label: 'Detected with LLM:' },
|
||||
hasReduce: true,
|
||||
},
|
||||
};
|
||||
|
||||
useFlowStore.setState({
|
||||
nodes: [mockNode],
|
||||
edges: [],
|
||||
});
|
||||
|
||||
renderWithProviders(
|
||||
<BasicBeliefNode
|
||||
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') as HTMLInputElement;
|
||||
|
||||
// Clear the input
|
||||
for (let i = 0; i < 'initial'.length; i++) {
|
||||
await user.type(input, '{backspace}');
|
||||
}
|
||||
await user.type(input, 'new semantic value{enter}');
|
||||
|
||||
await waitFor(() => {
|
||||
const state = useFlowStore.getState();
|
||||
const updatedNode = state.nodes.find(n => n.id === 'belief-1') as Node<BasicBeliefNodeData>;
|
||||
expect(updatedNode?.data.belief.value).toBe('new semantic value');
|
||||
});
|
||||
});
|
||||
|
||||
it('should update emotion value when selecting from dropdown', async () => {
|
||||
const mockNode: Node<BasicBeliefNodeData> = {
|
||||
id: 'belief-1',
|
||||
type: 'basic_belief',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
label: 'Belief',
|
||||
droppable: true,
|
||||
belief: { type: 'emotion', id: 'em1', value: 'happy', label: 'Emotion recognised:' },
|
||||
hasReduce: true,
|
||||
},
|
||||
};
|
||||
|
||||
useFlowStore.setState({
|
||||
nodes: [mockNode],
|
||||
edges: [],
|
||||
});
|
||||
|
||||
renderWithProviders(
|
||||
<BasicBeliefNode
|
||||
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 select = screen.getByDisplayValue('Happy');
|
||||
await user.selectOptions(select, 'sad');
|
||||
|
||||
await waitFor(() => {
|
||||
const state = useFlowStore.getState();
|
||||
const updatedNode = state.nodes.find(n => n.id === 'belief-1') as Node<BasicBeliefNodeData>;
|
||||
expect(updatedNode?.data.belief.value).toBe('sad');
|
||||
});
|
||||
});
|
||||
|
||||
it('should preserve value when switching between text-based belief types', async () => {
|
||||
const mockNode: Node<BasicBeliefNodeData> = {
|
||||
id: 'belief-1',
|
||||
type: 'basic_belief',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
label: 'Belief',
|
||||
droppable: true,
|
||||
belief: { type: 'keyword', id: 'kw1', value: 'test value', label: 'Keyword said:' },
|
||||
hasReduce: true,
|
||||
},
|
||||
};
|
||||
|
||||
useFlowStore.setState({
|
||||
nodes: [mockNode],
|
||||
edges: [],
|
||||
});
|
||||
|
||||
renderWithProviders(
|
||||
<BasicBeliefNode
|
||||
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}
|
||||
/>
|
||||
);
|
||||
|
||||
// Switch from keyword to semantic
|
||||
const typeSelect = screen.getByDisplayValue('Keyword said:');
|
||||
await user.selectOptions(typeSelect, 'semantic');
|
||||
|
||||
await waitFor(() => {
|
||||
const state = useFlowStore.getState();
|
||||
const updatedNode = state.nodes.find(n => n.id === 'belief-1') as Node<BasicBeliefNodeData>;
|
||||
expect(updatedNode?.data.belief.type).toBe('semantic');
|
||||
expect(updatedNode?.data.belief.value).toBe('test value'); // Value should be preserved
|
||||
});
|
||||
});
|
||||
|
||||
it('should automatically choose the first option when switching to emotion type, and carry on to the text values', async () => {
|
||||
const mockNode: Node<BasicBeliefNodeData> = {
|
||||
id: 'belief-1',
|
||||
type: 'basic_belief',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
label: 'Belief',
|
||||
droppable: true,
|
||||
belief: { type: 'keyword', id: 'kw1', value: 'some text', label: 'Keyword said:' },
|
||||
hasReduce: true,
|
||||
},
|
||||
};
|
||||
|
||||
useFlowStore.setState({
|
||||
nodes: [mockNode],
|
||||
edges: [],
|
||||
});
|
||||
|
||||
renderWithProviders(
|
||||
<BasicBeliefNode
|
||||
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}
|
||||
/>
|
||||
);
|
||||
|
||||
// Switch from keyword to emotion
|
||||
const typeSelect = screen.getByDisplayValue('Keyword said:');
|
||||
await user.selectOptions(typeSelect, 'emotion');
|
||||
|
||||
await waitFor(() => {
|
||||
const state = useFlowStore.getState();
|
||||
const updatedNode = state.nodes.find(n => n.id === 'belief-1') as Node<BasicBeliefNodeData>;
|
||||
expect(updatedNode?.data.belief.type).toBe('emotion');
|
||||
// The component doesn't reset the value when changing types
|
||||
// So it keeps the old value even though it doesn't make sense for emotion type
|
||||
expect(updatedNode?.data.belief.value).toBe('Happy');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ... rest of the tests remain the same, just fixing the Integration with Store section ...
|
||||
|
||||
describe('Integration with Store', () => {
|
||||
it('should properly update the store when changing belief value', async () => {
|
||||
const mockNode: Node<BasicBeliefNodeData> = {
|
||||
id: 'belief-1',
|
||||
type: 'basic_belief',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
label: 'Belief',
|
||||
droppable: true,
|
||||
belief: { type: 'keyword', id: 'kw1', value: '', label: 'Keyword said:' },
|
||||
hasReduce: true,
|
||||
},
|
||||
};
|
||||
|
||||
useFlowStore.setState({
|
||||
nodes: [mockNode],
|
||||
edges: [],
|
||||
});
|
||||
|
||||
renderWithProviders(
|
||||
<BasicBeliefNode
|
||||
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('keyword...');
|
||||
await user.type(input, 'emergency{enter}');
|
||||
|
||||
await waitFor(() => {
|
||||
const state = useFlowStore.getState();
|
||||
expect(state.nodes).toHaveLength(1);
|
||||
expect(state.nodes[0].id).toBe('belief-1');
|
||||
const beliefData = state.nodes[0].data as BasicBeliefNodeData;
|
||||
expect(beliefData.belief.value).toBe('emergency');
|
||||
expect(beliefData.belief.type).toBe('keyword');
|
||||
});
|
||||
});
|
||||
|
||||
it('should properly update the store when changing belief type', async () => {
|
||||
const mockNode: Node<BasicBeliefNodeData> = {
|
||||
id: 'belief-1',
|
||||
type: 'basic_belief',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
label: 'Belief',
|
||||
droppable: true,
|
||||
belief: { type: 'keyword', id: 'kw1', value: 'test', label: 'Keyword said:' },
|
||||
hasReduce: true,
|
||||
},
|
||||
};
|
||||
|
||||
useFlowStore.setState({
|
||||
nodes: [mockNode],
|
||||
edges: [],
|
||||
});
|
||||
|
||||
renderWithProviders(
|
||||
<BasicBeliefNode
|
||||
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 select = screen.getByDisplayValue('Keyword said:');
|
||||
await user.selectOptions(select, 'object');
|
||||
|
||||
await waitFor(() => {
|
||||
const state = useFlowStore.getState();
|
||||
const beliefData = state.nodes[0].data as BasicBeliefNodeData;
|
||||
expect(beliefData.belief.type).toBe('object');
|
||||
// Note: The component doesn't update the label when changing type
|
||||
expect(beliefData.belief.value).toBe('test'); // Value should be preserved
|
||||
});
|
||||
});
|
||||
|
||||
it('should not affect other nodes when updating one belief node', async () => {
|
||||
const belief1: Node<BasicBeliefNodeData> = {
|
||||
id: 'belief-1',
|
||||
type: 'basic_belief',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
label: 'Belief 1',
|
||||
droppable: true,
|
||||
belief: { type: 'keyword', id: 'kw1', value: 'hello', label: 'Keyword said:' },
|
||||
hasReduce: true,
|
||||
},
|
||||
};
|
||||
|
||||
const belief2: Node<BasicBeliefNodeData> = {
|
||||
id: 'belief-2',
|
||||
type: 'basic_belief',
|
||||
position: { x: 100, y: 0 },
|
||||
data: {
|
||||
label: 'Belief 2',
|
||||
droppable: true,
|
||||
belief: { type: 'object', id: 'obj1', value: 'chair', label: 'Object found:' },
|
||||
hasReduce: true,
|
||||
},
|
||||
};
|
||||
|
||||
useFlowStore.setState({
|
||||
nodes: [belief1, belief2],
|
||||
edges: [],
|
||||
});
|
||||
|
||||
renderWithProviders(
|
||||
<BasicBeliefNode
|
||||
id={belief1.id}
|
||||
type={belief1.type as string}
|
||||
data={belief1.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('hello') as HTMLInputElement;
|
||||
|
||||
// Clear the input
|
||||
for (let i = 0; i < 'hello'.length; i++) {
|
||||
await user.type(input, '{backspace}');
|
||||
}
|
||||
await user.type(input, 'goodbye{enter}');
|
||||
|
||||
await waitFor(() => {
|
||||
const state = useFlowStore.getState();
|
||||
const updatedBelief1 = state.nodes.find(n => n.id === 'belief-1') as Node<BasicBeliefNodeData>;
|
||||
const unchangedBelief2 = state.nodes.find(n => n.id === 'belief-2') as Node<BasicBeliefNodeData>;
|
||||
|
||||
expect(updatedBelief1.data.belief.value).toBe('goodbye');
|
||||
expect(unchangedBelief2.data.belief.value).toBe('chair');
|
||||
expect(unchangedBelief2.data.belief.type).toBe('object');
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle multiple rapid updates to belief value', async () => {
|
||||
const mockNode: Node<BasicBeliefNodeData> = {
|
||||
id: 'belief-1',
|
||||
type: 'basic_belief',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
label: 'Belief',
|
||||
droppable: true,
|
||||
belief: { type: 'semantic', id: 'sem1', value: 'initial', label: 'Detected with LLM:' },
|
||||
hasReduce: true,
|
||||
},
|
||||
};
|
||||
|
||||
useFlowStore.setState({
|
||||
nodes: [mockNode],
|
||||
edges: [],
|
||||
});
|
||||
|
||||
renderWithProviders(
|
||||
<BasicBeliefNode
|
||||
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') as HTMLInputElement;
|
||||
|
||||
await user.type(input, '1');
|
||||
await waitFor(() => {
|
||||
const state = useFlowStore.getState();
|
||||
const nodeData = state.nodes[0].data as BasicBeliefNodeData;
|
||||
expect(nodeData.belief.value).toBe('initial');
|
||||
});
|
||||
|
||||
await user.type(input, '2');
|
||||
await waitFor(() => {
|
||||
const state = useFlowStore.getState();
|
||||
const nodeData = state.nodes[0].data as BasicBeliefNodeData;
|
||||
expect(nodeData.belief.value).toBe('initial');
|
||||
});
|
||||
|
||||
await user.type(input, '{enter}');
|
||||
await waitFor(() => {
|
||||
const state = useFlowStore.getState();
|
||||
const nodeData = state.nodes[0].data as BasicBeliefNodeData;
|
||||
expect(nodeData.belief.value).toBe('initial12');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,10 @@
|
||||
import type { Node, Edge, Connection } from '@xyflow/react'
|
||||
import useFlowStore from '../../../../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores';
|
||||
import type { PhaseNodeData } from "../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/PhaseNode";
|
||||
import { getByTestId, render } from '@testing-library/react';
|
||||
import type {PhaseNode, PhaseNodeData} from "../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/PhaseNode";
|
||||
import {act, getByTestId, render} from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import VisProgPage from '../../../../../src/pages/VisProgPage/VisProg';
|
||||
import {mockReactFlow} from "../../../../setupFlowTests.ts";
|
||||
|
||||
|
||||
class ResizeObserver {
|
||||
@@ -98,4 +100,195 @@ describe('PhaseNode', () => {
|
||||
expect(p1_data.children.length == 1);
|
||||
expect(p2_data.children.length == 2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// --| Helper functions |--
|
||||
|
||||
function createPhaseNode(
|
||||
id: string,
|
||||
overrides: Partial<PhaseNodeData> = {},
|
||||
): Node<PhaseNodeData> {
|
||||
return {
|
||||
id: id,
|
||||
type: 'phase',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
label: 'Phase',
|
||||
droppable: true,
|
||||
children: [],
|
||||
hasReduce: true,
|
||||
nextPhaseId: null,
|
||||
isFirstPhase: false,
|
||||
...overrides,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function createNode(id: string, type: string): Node {
|
||||
return {
|
||||
id: id,
|
||||
type: type,
|
||||
position: { x: 0, y: 0 },
|
||||
data: {},
|
||||
}
|
||||
}
|
||||
|
||||
function connect(source: string, target: string): Connection {
|
||||
return {
|
||||
source: source,
|
||||
target: target,
|
||||
sourceHandle: null,
|
||||
targetHandle: null
|
||||
};
|
||||
}
|
||||
|
||||
function edge(source: string, target: string): Edge {
|
||||
return {
|
||||
id: `${source}-${target}`,
|
||||
source: source,
|
||||
target: target,
|
||||
}
|
||||
}
|
||||
|
||||
// --| Connection Tests |--
|
||||
|
||||
describe('PhaseNode Connection logic', () => {
|
||||
beforeAll(() => {
|
||||
mockReactFlow();
|
||||
});
|
||||
|
||||
describe('PhaseConnections', () => {
|
||||
test('connecting start => phase sets isFirstPhase to true', () => {
|
||||
const phase = createPhaseNode('phase-1')
|
||||
const start = createNode('start', 'start')
|
||||
|
||||
useFlowStore.setState({ nodes: [phase, start] })
|
||||
|
||||
// verify it starts of false
|
||||
expect(phase.data.isFirstPhase).toBe(false);
|
||||
|
||||
act(() => {
|
||||
useFlowStore.getState().onConnect(connect('start', 'phase-1'))
|
||||
})
|
||||
|
||||
const updatedPhase = useFlowStore
|
||||
.getState()
|
||||
.nodes.find((n) => n.id === 'phase-1') as PhaseNode
|
||||
|
||||
expect(updatedPhase.data.isFirstPhase).toBe(true)
|
||||
})
|
||||
|
||||
test('connecting task => phase adds child', () => {
|
||||
const phase = createPhaseNode('phase-1')
|
||||
const norm = createNode('norm-1', 'norm')
|
||||
|
||||
useFlowStore.setState({ nodes: [phase, norm] })
|
||||
|
||||
act(() => {
|
||||
useFlowStore.getState().onConnect(connect('norm-1', 'phase-1'))
|
||||
})
|
||||
|
||||
const updatedPhase = useFlowStore
|
||||
.getState()
|
||||
.nodes.find((n) => n.id === 'phase-1') as PhaseNode
|
||||
|
||||
expect(updatedPhase.data.children).toEqual(['norm-1'])
|
||||
})
|
||||
|
||||
test('connecting phase => phase sets nextPhaseId', () => {
|
||||
const p1 = createPhaseNode('phase-1')
|
||||
const p2 = createPhaseNode('phase-2')
|
||||
|
||||
useFlowStore.setState({ nodes: [p1, p2] })
|
||||
|
||||
act(() => {
|
||||
useFlowStore.getState().onConnect(connect('phase-1', 'phase-2'))
|
||||
})
|
||||
|
||||
const updatedP1 = useFlowStore
|
||||
.getState()
|
||||
.nodes.find((n) => n.id === 'phase-1') as PhaseNode
|
||||
|
||||
expect(updatedP1.data.nextPhaseId).toBe('phase-2')
|
||||
})
|
||||
|
||||
test('connecting phase to end => phase sets nextPhaseId to "end"', () => {
|
||||
const phase = createPhaseNode('phase-1')
|
||||
const end = createNode('end', 'end')
|
||||
|
||||
useFlowStore.setState({ nodes: [phase, end] })
|
||||
|
||||
act(() => {
|
||||
useFlowStore.getState().onConnect(connect('phase-1', 'end'))
|
||||
})
|
||||
|
||||
const updatedPhase = useFlowStore
|
||||
.getState()
|
||||
.nodes.find((n) => n.id === 'phase-1') as PhaseNode
|
||||
|
||||
expect(updatedPhase.data.nextPhaseId).toBe('end')
|
||||
})
|
||||
})
|
||||
|
||||
describe('PhaseDisconnections', () => {
|
||||
test('disconnecting task => phase removes child', () => {
|
||||
const phase = createPhaseNode('phase-1', { children: ['norm-1'] })
|
||||
const norm = createNode('norm-1', 'norm')
|
||||
|
||||
useFlowStore.setState({
|
||||
nodes: [phase, norm],
|
||||
edges: [edge('norm-1', 'phase-1')]
|
||||
})
|
||||
|
||||
act(() => {
|
||||
useFlowStore.getState().onEdgesDelete([edge('norm-1', 'phase-1')])
|
||||
})
|
||||
|
||||
const updatedPhase = useFlowStore
|
||||
.getState()
|
||||
.nodes.find((n) => n.id === 'phase-1') as PhaseNode
|
||||
|
||||
expect(updatedPhase.data.children).toEqual([])
|
||||
})
|
||||
|
||||
test('disconnecting start => phase sets isFirstPhase to false', () => {
|
||||
const phase = createPhaseNode('phase-1', { isFirstPhase: true })
|
||||
const start = createNode('start', 'start')
|
||||
|
||||
useFlowStore.setState({
|
||||
nodes: [phase, start],
|
||||
edges: [edge('start', 'phase-1')]
|
||||
})
|
||||
|
||||
act(() => {
|
||||
useFlowStore.getState().onEdgesDelete([edge('start', 'phase-1')])
|
||||
})
|
||||
|
||||
const updatedPhase = useFlowStore
|
||||
.getState()
|
||||
.nodes.find((n) => n.id === 'phase-1') as PhaseNode
|
||||
|
||||
expect(updatedPhase.data.isFirstPhase).toBe(false)
|
||||
})
|
||||
|
||||
test('disconnecting phase => phase sets nextPhaseId to null', () => {
|
||||
const p1 = createPhaseNode('phase-1', { nextPhaseId: 'phase-2' })
|
||||
const p2 = createPhaseNode('phase-2')
|
||||
|
||||
useFlowStore.setState({
|
||||
nodes: [p1, p2],
|
||||
edges: [edge('phase-1', 'phase-2')]
|
||||
})
|
||||
|
||||
act(() => {
|
||||
useFlowStore.getState().onEdgesDelete([edge('phase-1', 'phase-2')])
|
||||
})
|
||||
|
||||
const updatedP1 = useFlowStore
|
||||
.getState()
|
||||
.nodes.find((n) => n.id === 'phase-1') as PhaseNode
|
||||
|
||||
expect(updatedP1.data.nextPhaseId).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -8,22 +8,22 @@ import { createElement } from 'react';
|
||||
import useFlowStore from '../../../../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores';
|
||||
|
||||
|
||||
describe('NormNode', () => {
|
||||
describe('Universal Nodes', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
function createNode(id: string, type: string, position: XYPosition, data: Record<string, unknown>, deletable?: boolean) {
|
||||
function createNode(id: string, type: string, position: XYPosition, data: Record<string, unknown>, deletable? : boolean) {
|
||||
const defaultData = NodeDefaults[type as keyof typeof NodeDefaults]
|
||||
const newData = {
|
||||
id: id,
|
||||
type: type,
|
||||
position: position,
|
||||
data: data,
|
||||
deletable: deletable,
|
||||
|
||||
return {
|
||||
id: id,
|
||||
type: type,
|
||||
position: position,
|
||||
data: {...defaultData, ...data},
|
||||
deletable: deletable
|
||||
}
|
||||
return {...defaultData, ...newData}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
@@ -45,34 +45,34 @@ describe('NormNode', () => {
|
||||
|
||||
describe('Rendering', () => {
|
||||
test.each(getAllTypes())('it should render %s node with the default data', (nodeType) => {
|
||||
const lengthBefore = screen.getAllByText(/.*/).length;
|
||||
const lengthBefore = screen.getAllByText(/.*/).length;
|
||||
|
||||
const newNode = createNode(nodeType + "1", nodeType, {x: 200, y:200}, {});
|
||||
const newNode = createNode(nodeType + "1", nodeType, {x: 200, y:200}, {});
|
||||
|
||||
const found = Object.entries(NodeTypes).find(([t]) => t === nodeType);
|
||||
const uiElement = found ? found[1] : null;
|
||||
const found = Object.entries(NodeTypes).find(([t]) => t === nodeType);
|
||||
const uiElement = found ? found[1] : null;
|
||||
|
||||
expect(uiElement).not.toBeNull();
|
||||
const 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,
|
||||
};
|
||||
expect(uiElement).not.toBeNull();
|
||||
const 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(createElement(uiElement as React.ComponentType<any>, props));
|
||||
const lengthAfter = screen.getAllByText(/.*/).length;
|
||||
renderWithProviders(createElement(uiElement as React.ComponentType<any>, props));
|
||||
const lengthAfter = screen.getAllByText(/.*/).length;
|
||||
|
||||
expect(lengthBefore + 1 === lengthAfter);
|
||||
});
|
||||
expect(lengthBefore + 1 === lengthAfter);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
110
test/utils/orderPhaseNodes.test.ts
Normal file
110
test/utils/orderPhaseNodes.test.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import type {PhaseNode} from "../../src/pages/VisProgPage/visualProgrammingUI/nodes/PhaseNode.tsx";
|
||||
import orderPhaseNodeArray from "../../src/utils/orderPhaseNodes.ts";
|
||||
|
||||
function createPhaseNode(
|
||||
id: string,
|
||||
isFirst: boolean = false,
|
||||
nextPhaseId: string | null = null
|
||||
): PhaseNode {
|
||||
return {
|
||||
id: id,
|
||||
type: 'phase',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
label: 'Phase',
|
||||
droppable: true,
|
||||
children: [],
|
||||
hasReduce: true,
|
||||
nextPhaseId: nextPhaseId,
|
||||
isFirstPhase: isFirst,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
describe("orderPhaseNodes", () => {
|
||||
test.each([
|
||||
{
|
||||
testCase: {
|
||||
testName: "Throws correct error when there is no first phase (empty input array)",
|
||||
input: [],
|
||||
expected: "No phaseNode with isFirstObject = true found"
|
||||
}
|
||||
},{
|
||||
testCase: {
|
||||
testName: "Throws correct error when there is no first phase",
|
||||
input: [
|
||||
createPhaseNode("phase-1", false, "phase-2"),
|
||||
createPhaseNode("phase-2", false, "phase-3"),
|
||||
createPhaseNode("phase-3", false, "end")
|
||||
],
|
||||
expected: "No phaseNode with isFirstObject = true found"
|
||||
}
|
||||
},{
|
||||
testCase: {
|
||||
testName: "Throws correct error when the program doesn't lead to an end node (missing phase-phase connection)",
|
||||
input: [
|
||||
createPhaseNode("phase-1", true, "phase-2"),
|
||||
createPhaseNode("phase-2", false, null),
|
||||
createPhaseNode("phase-3", false, "end")
|
||||
],
|
||||
expected: "Incomplete phase sequence, program does not reach the end node"
|
||||
}
|
||||
},{
|
||||
testCase: {
|
||||
testName: "Throws correct error when the program doesn't lead to an end node (missing phase-end connection)",
|
||||
input: [
|
||||
createPhaseNode("phase-1", true, "phase-2"),
|
||||
createPhaseNode("phase-2", false, "phase-3"),
|
||||
createPhaseNode("phase-3", false, null)
|
||||
],
|
||||
expected: "Incomplete phase sequence, program does not reach the end node"
|
||||
}
|
||||
},{
|
||||
testCase: {
|
||||
testName: "Throws correct error when the program leads to a non-existent phase",
|
||||
input: [
|
||||
createPhaseNode("phase-1", true, "phase-2"),
|
||||
createPhaseNode("phase-2", false, "phase-3"),
|
||||
createPhaseNode("phase-3", false, "phase-4")
|
||||
],
|
||||
expected: "Incomplete phase sequence, phaseNode with id \"phase-4\" not found"
|
||||
}
|
||||
}
|
||||
])(`Error Handling: $testCase.testName`, ({testCase}) => {
|
||||
expect(() => { orderPhaseNodeArray(testCase.input) }).toThrow(testCase.expected);
|
||||
})
|
||||
test.each([
|
||||
{
|
||||
testCase: {
|
||||
testName: "Already correctly ordered phases stay ordered",
|
||||
input: [
|
||||
createPhaseNode("phase-1", true, "phase-2"),
|
||||
createPhaseNode("phase-2", false, "phase-3"),
|
||||
createPhaseNode("phase-3", false, "end")
|
||||
],
|
||||
expected: [
|
||||
createPhaseNode("phase-1", true, "phase-2"),
|
||||
createPhaseNode("phase-2", false, "phase-3"),
|
||||
createPhaseNode("phase-3", false, "end")
|
||||
]
|
||||
}
|
||||
},{
|
||||
testCase: {
|
||||
testName: "Incorrectly ordered phases get ordered correctly",
|
||||
input: [
|
||||
createPhaseNode("phase-3", false, "end"),
|
||||
createPhaseNode("phase-1", true, "phase-2"),
|
||||
createPhaseNode("phase-2", false, "phase-3"),
|
||||
],
|
||||
expected: [
|
||||
createPhaseNode("phase-1", true, "phase-2"),
|
||||
createPhaseNode("phase-2", false, "phase-3"),
|
||||
createPhaseNode("phase-3", false, "end")
|
||||
]
|
||||
}
|
||||
}
|
||||
])(`Functional: $testCase.testName`, ({testCase}) => {
|
||||
const output = orderPhaseNodeArray(testCase.input);
|
||||
expect(output).toEqual(testCase.expected);
|
||||
})
|
||||
})
|
||||
@@ -85,14 +85,30 @@ describe('useProgramStore', () => {
|
||||
).toThrow('phase with id:"missing-phase" not found');
|
||||
});
|
||||
|
||||
// this test should be at the bottom to avoid conflicts with the previous tests
|
||||
it('should clone program state when setting it (no shared references should exist)', () => {
|
||||
useProgramStore.getState().setProgramState(mockProgram);
|
||||
const changeableMockProgram: ReducedProgram = {
|
||||
phases: [
|
||||
{
|
||||
id: 'phase-1',
|
||||
norms: [{ id: 'norm-1' }],
|
||||
goals: [{ id: 'goal-1' }],
|
||||
triggers: [{ id: 'trigger-1' }],
|
||||
},
|
||||
{
|
||||
id: 'phase-2',
|
||||
norms: [{ id: 'norm-2' }],
|
||||
goals: [{ id: 'goal-2' }],
|
||||
triggers: [{ id: 'trigger-2' }],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
useProgramStore.getState().setProgramState(changeableMockProgram);
|
||||
|
||||
const storedProgram = useProgramStore.getState().getProgramState();
|
||||
|
||||
// mutate original
|
||||
(mockProgram.phases[0].norms as any[]).push({ id: 'norm-mutated' });
|
||||
(changeableMockProgram.phases[0].norms as any[]).push({ id: 'norm-mutated' });
|
||||
|
||||
// store should NOT change
|
||||
expect(storedProgram.phases[0]['norms']).toHaveLength(1);
|
||||
|
||||
Reference in New Issue
Block a user