Merge branch 'feat/basic-belief-nodes' into 'demo'

Create a basic belief node that can be used in further stages to create inferred belief and be inputs for other nodes.

See merge request ics/sp/2025/n25b/pepperplus-ui!32
This commit was merged in pull request #32.
This commit is contained in:
Pim Hutting
2026-01-06 14:29:13 +00:00
8 changed files with 1000 additions and 19 deletions

View File

@@ -248,3 +248,11 @@ button.no-button {
text-decoration: underline;
}
}
.flex-center-x {
display: flex;
justify-content: center; /* horizontal centering */
text-align: center; /* center multi-line text */
width: 100%; /* allow it to stretch */
flex-wrap: wrap; /* optional: let text wrap naturally */
}

View File

@@ -71,6 +71,11 @@
filter: drop-shadow(0 0 0.25rem red);
}
.node-basic_belief {
outline: plum solid 2pt;
filter: drop-shadow(0 0 0.25rem plum);
}
.draggable-node {
padding: 3px 10px;
background-color: canvas;
@@ -126,3 +131,11 @@
outline: red solid 2pt;
filter: drop-shadow(0 0 0.25rem red);
}
.draggable-node-basic_belief {
padding: 3px 10px;
background-color: canvas;
border-radius: 5pt;
outline: plum solid 2pt;
filter: drop-shadow(0 0 0.25rem plum);
}

View File

@@ -46,6 +46,8 @@ import TriggerNode, {
TriggerReduce
} from "./nodes/TriggerNode";
import { TriggerNodeDefaults } from "./nodes/TriggerNode.default";
import BasicBeliefNode, { BasicBeliefConnectionSource, BasicBeliefConnectionTarget, BasicBeliefDisconnectionSource, BasicBeliefDisconnectionTarget, BasicBeliefReduce } from "./nodes/BasicBeliefNode";
import { BasicBeliefNodeDefaults } from "./nodes/BasicBeliefNode.default";
/**
* Registered node types in the visual programming system.
@@ -60,6 +62,7 @@ export const NodeTypes = {
norm: NormNode,
goal: GoalNode,
trigger: TriggerNode,
basic_belief: BasicBeliefNode,
};
/**
@@ -74,6 +77,7 @@ export const NodeDefaults = {
norm: NormNodeDefaults,
goal: GoalNodeDefaults,
trigger: TriggerNodeDefaults,
basic_belief: BasicBeliefNodeDefaults,
};
@@ -90,6 +94,7 @@ export const NodeReduces = {
norm: NormReduce,
goal: GoalReduce,
trigger: TriggerReduce,
basic_belief: BasicBeliefReduce,
}
@@ -107,6 +112,7 @@ export const NodeConnections = {
norm: NormConnectionTarget,
goal: GoalConnectionTarget,
trigger: TriggerConnectionTarget,
basic_belief: BasicBeliefConnectionTarget,
},
Sources: {
start: StartConnectionSource,
@@ -115,6 +121,7 @@ export const NodeConnections = {
norm: NormConnectionSource,
goal: GoalConnectionSource,
trigger: TriggerConnectionSource,
basic_belief: BasicBeliefConnectionSource
}
}
@@ -132,6 +139,7 @@ export const NodeDisconnections = {
norm: NormDisconnectionTarget,
goal: GoalDisconnectionTarget,
trigger: TriggerDisconnectionTarget,
basic_belief: BasicBeliefDisconnectionTarget,
},
Sources: {
start: StartDisconnectionSource,
@@ -140,6 +148,7 @@ export const NodeDisconnections = {
norm: NormDisconnectionSource,
goal: GoalDisconnectionSource,
trigger: TriggerDisconnectionSource,
basic_belief: BasicBeliefDisconnectionSource,
},
}
@@ -151,7 +160,6 @@ export const NodeDisconnections = {
export const NodeDeletes = {
start: () => false,
end: () => false,
test: () => false, // Used for coverage of universal/ undefined nodes
}
/**
@@ -164,5 +172,5 @@ export const NodesInPhase = {
start: () => false,
end: () => false,
phase: () => false,
test: () => false, // Used for coverage of universal/ undefined nodes
basic_belief: () => false,
}

View File

@@ -28,17 +28,19 @@ import { UndoRedo } from "./EditorUndoRedo.ts";
* @param deletable - Optional flag to indicate if the node can be deleted (can be deleted by default).
* @returns A fully initialized Node object ready to be added to the flow.
*/
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,
function createNode(id: string, type: string, position: XYPosition, data: Record<string, unknown>, deletable?: boolean) {
const defaultData = NodeDefaults[type as keyof typeof NodeDefaults]
return {
id,
type,
position,
deletable,
data: {
...defaultData,
...data,
},
}
}
return {...defaultData, ...newData}
}
//* Initial nodes, created by using createNode. */
const initialNodes : Node[] = [

View File

@@ -0,0 +1,12 @@
import type { BasicBeliefNodeData } from "./BasicBeliefNode";
/**
* Default data for this node
*/
export const BasicBeliefNodeDefaults: BasicBeliefNodeData = {
label: "Belief",
droppable: true,
belief: {type: "keyword", id: "help", value: "help", label: "Keyword said:"},
hasReduce: true,
};

View File

@@ -0,0 +1,193 @@
import {
Handle,
type NodeProps,
Position,
type Node,
} from '@xyflow/react';
import { Toolbar } from '../components/NodeComponents';
import styles from '../../VisProg.module.css';
import useFlowStore from '../VisProgStores';
import { TextField } from '../../../../components/TextField';
/**
* The default data structure for a BasicBelief node
*
* Represents configuration for a node that activates when a specific condition is met,
* such as keywords being spoken or emotions detected.
*
* @property label: the display label of this BasicBelief node.
* @property droppable: Whether this node can be dropped from the toolbar (default: true).
* @property BasicBeliefType - The type of BasicBelief ("keywords" or a custom string).
* @property BasicBeliefs - The list of keyword BasicBeliefs (if applicable).
* @property hasReduce - Whether this node supports reduction logic.
*/
export type BasicBeliefNodeData = {
label: string;
droppable: boolean;
belief: BasicBeliefType;
hasReduce: boolean;
};
// These are all the types a basic belief could be.
type BasicBeliefType = Keyword | Semantic | DetectedObject | Emotion
type Keyword = { type: "keyword", id: string, value: string, label: "Keyword said:"};
type Semantic = { type: "semantic", id: string, value: string, label: "Detected with LLM:"};
type DetectedObject = { type: "object", id: string, value: string, label: "Object found:"};
type Emotion = { type: "emotion", id: string, value: string, label: "Emotion recognised:"};
export type BasicBeliefNode = Node<BasicBeliefNodeData>
/**
* This function is called whenever a connection is made with this node type as the target
* @param _thisNode the node of this node type which function is called
* @param _sourceNodeId the source of the received connection
*/
export function BasicBeliefConnectionTarget(_thisNode: Node, _sourceNodeId: string) {
// no additional connection logic exists yet
}
/**
* This function is called whenever a connection is made with this node type as the source
* @param _thisNode the node of this node type which function is called
* @param _targetNodeId the target of the created connection
*/
export function BasicBeliefConnectionSource(_thisNode: Node, _targetNodeId: string) {
// no additional connection logic exists yet
}
/**
* This function is called whenever a connection is disconnected with this node type as the target
* @param _thisNode the node of this node type which function is called
* @param _sourceNodeId the source of the disconnected connection
*/
export function BasicBeliefDisconnectionTarget(_thisNode: Node, _sourceNodeId: string) {
// no additional connection logic exists yet
}
/**
* This function is called whenever a connection is disconnected with this node type as the source
* @param _thisNode the node of this node type which function is called
* @param _targetNodeId the target of the diconnected connection
*/
export function BasicBeliefDisconnectionSource(_thisNode: Node, _targetNodeId: string) {
// no additional connection logic exists yet
}
/**
* Defines how a BasicBelief node should be rendered
* @param props - Node properties provided by React Flow, including `id` and `data`.
* @returns The rendered BasicBeliefNode React element (React.JSX.Element).
*/
export default function BasicBeliefNode(props: NodeProps<BasicBeliefNode>) {
const data = props.data;
const {updateNodeData} = useFlowStore();
const updateValue = (value: string) => updateNodeData(props.id, {...data, belief: {...data.belief, value: value}});
const label_input_id = `basic_belief_${props.id}_label_input`;
type BeliefString = BasicBeliefType["type"];
function updateBeliefType(newType: BeliefString) {
updateNodeData(props.id, {
...data,
belief: {
...data.belief,
type: newType,
value:
newType === "emotion"
? emotionOptions[0]
: data.belief.value,
},
});
}
// Use this
const emotionOptions = ["Happy", "Angry", "Sad", "Cheerful"]
let placeholder = ""
let wrapping = ""
switch (props.data.belief.type) {
case ("keyword"):
placeholder = "keyword..."
wrapping = '"'
break;
case ("semantic"):
placeholder = "word..."
wrapping = '"'
break;
case ("object"):
placeholder = "object..."
break;
case ("emotion"):
// TODO: emotion should probably be a drop-down menu rather than a string
// So this placeholder won't hold for always
placeholder = "emotion..."
break;
default:
break;
}
return (
<>
<Toolbar nodeId={props.id} allowDelete={true}/>
<div className={`${styles.defaultNode} ${styles.nodeBasicBelief /*TODO: Change this*/}`}>
<div className={"flex-center-x gap-sm"}>
<label htmlFor={label_input_id}>Belief:</label>
</div>
<div className={"flex-row gap-sm"}>
<select
value={data.belief.type}
onChange={(e) => updateBeliefType(e.target.value as BeliefString)}
>
<option value="keyword">Keyword said:</option>
<option value="semantic">Detected with LLM:</option>
<option value="object">Object found:</option>
<option value="emotion">Emotion recognised:</option>
</select>
{wrapping}
{data.belief.type === "emotion" && (
<select
value={data.belief.value}
onChange={(e) => updateValue(e.target.value)}
>
{emotionOptions.map((emotion) => (
<option key={emotion} value={emotion.toLowerCase()}>
{emotion}
</option>
))}
</select>
)}
{data.belief.type !== "emotion" &&
(<TextField
id={label_input_id}
value={data.belief.value}
setValue={updateValue}
placeholder={placeholder}
/>)}
{wrapping}
</div>
<Handle type="source" position={Position.Right} id="source"/>
</div>
</>
);
};
/**
* Reduces each BasicBelief, including its children down into its core data.
* @param node - The BasicBelief node to reduce.
* @param _nodes - The list of all nodes in the current flow graph.
* @returns A simplified object containing the node label and its list of BasicBeliefs.
*/
export function BasicBeliefReduce(node: Node, _nodes: Node[]) {
const data = node.data as BasicBeliefNodeData;
return {
id: node.id,
type: data.belief.type,
value: data.belief.value
}
}

View File

@@ -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');
});
});
});
});

View File

@@ -15,14 +15,16 @@ describe('NormNode', () => {
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,
type,
position,
deletable,
data: {
...defaultData,
...data,
},
}
return {...defaultData, ...newData}
}