Add experiment logs to the monitoring page #48
@@ -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 */
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import VisProg from "./pages/VisProgPage/VisProg.tsx";
|
||||
import {useState} from "react";
|
||||
import Logging from "./components/Logging/Logging.tsx";
|
||||
|
||||
|
||||
function App(){
|
||||
const [showLogs, setShowLogs] = useState(false);
|
||||
|
||||
|
||||
@@ -1,25 +1,113 @@
|
||||
import { useState } from 'react'
|
||||
import React, { useState } from 'react';
|
||||
|
||||
/**
|
||||
* A minimal counter component that demonstrates basic React state handling.
|
||||
*
|
||||
* Maintains an internal count value and provides buttons to increment and reset it.
|
||||
*
|
||||
* @returns A JSX element rendering the counter UI.
|
||||
*/
|
||||
function Counter() {
|
||||
/** The current counter value. */
|
||||
const [count, setCount] = useState(0)
|
||||
export default function Counter() {
|
||||
const [customText, setCustomText] = useState("");
|
||||
const [selectedGesture, setSelectedGesture] = useState("animations/Stand/BodyTalk/Speaking/BodyTalk_1");
|
||||
|
||||
// Reusable helper to talk to the backend
|
||||
const send = async (type: string, context: string) => {
|
||||
try {
|
||||
await fetch("http://localhost:8000/button_pressed", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ type, context }),
|
||||
});
|
||||
// Backticks restored here:
|
||||
console.log(`Sent ${type}: ${context}`);
|
||||
} catch (err) {
|
||||
// Backticks restored here:
|
||||
console.error(`Error sending ${type}:`, err);
|
||||
}
|
||||
};
|
||||
|
||||
function list_generator(header: string, items: string[]) {
|
||||
return (
|
||||
<div style={{ flex: 1, minWidth: '200px' }}>
|
||||
<strong style={{ display: 'block', marginBottom: '10px', borderBottom: '1px solid #eee' }}>{header}</strong>
|
||||
<ul style={{ listStyle: 'none', padding: 0, margin: 0 }}>
|
||||
{items.map((item, index) => (
|
||||
<li key={index} style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: '8px',
|
||||
fontSize: '14px'
|
||||
}}>
|
||||
<span>{item}</span>
|
||||
<button
|
||||
onClick={() => send("override", "2")}
|
||||
style={{
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
padding: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
fontSize: '12px'
|
||||
}}
|
||||
title="Send Override 2"
|
||||
>
|
||||
▶
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<button onClick={() => setCount((count) => count + 1)}>
|
||||
count is {count}
|
||||
</button>
|
||||
<button className='reset' onClick={() => setCount(0)}>
|
||||
Reset Counter
|
||||
</button>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '20px', padding: '20px' }}>
|
||||
|
||||
<div style={{ display: 'flex', gap: '10px' }}>
|
||||
<input
|
||||
type="text"
|
||||
value={customText}
|
||||
onChange={(e) => setCustomText(e.target.value)}
|
||||
placeholder="Type something for Pepper to say..."
|
||||
style={{ padding: '8px', flex: 1 }}
|
||||
/>
|
||||
<button onClick={() => send("speech", customText)} style={{ padding: '8px 16px' }}>
|
||||
Say Custom Text
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '10px' }}>
|
||||
<button onClick={() => send("speech", "Hello, I'm Pepper")}>
|
||||
"Hello, I'm Pepper"
|
||||
</button>
|
||||
<button onClick={() => send("speech", "Could you repeat that please")}>
|
||||
"Repeat please"
|
||||
</button>
|
||||
<button onClick={() => send("speech", "Tell me something about yourself")}>
|
||||
"About yourself"
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
|
||||
<label htmlFor="gesture-select">Select Gesture:</label>
|
||||
<select
|
||||
id="gesture-select"
|
||||
value={selectedGesture}
|
||||
onChange={(e) => setSelectedGesture(e.target.value)}
|
||||
style={{ padding: '8px', borderRadius: '4px', flex: 1 }}
|
||||
>
|
||||
<option value="animations/Stand/BodyTalk/Speaking/BodyTalk_1">Body Talk 1</option>
|
||||
<option value="animations/Stand/Gestures/Thinking_8">Thinking "8"</option>
|
||||
<option value="animations/Stand/Gestures/Thinking_1">Thinking "1"</option>
|
||||
<option value="animations/Stand/Emotions/Positive/Happy_1">Happy</option>
|
||||
</select>
|
||||
<button onClick={() => send("gesture", selectedGesture)} style={{ padding: '8px 16px' }}>
|
||||
Actuate Gesture
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '30px', flexWrap: 'wrap' }}>
|
||||
{list_generator("Goals to complete:", ["Greet user", "Introduce self", "Ask for name"])}
|
||||
{list_generator("Conditional norms:", ["If {user_tired}, be less energetic", "If {play_mundo}, go where you please"])}
|
||||
{list_generator("Activate triggers:", ["If {user_thirsty}, offer drink", "If {name == Karen}, be triggered"])}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default Counter
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import styles from './MonitoringPage.module.css';
|
||||
|
||||
/**
|
||||
* HELPER: Unified sender function
|
||||
* In a real app, you might move this to a /services or /hooks folder
|
||||
*/
|
||||
const sendUserInterrupt = async (type: string, context: string) => {
|
||||
try {
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import React from 'react';
|
||||
import React, { use } from 'react';
|
||||
import styles from './MonitoringPage.module.css';
|
||||
import useProgramStore from "../../utils/programStore.ts";
|
||||
import { GestureControls, SpeechPresets, DirectSpeechInput, StatusList } from './Components';
|
||||
import { nextPhase } from ".//MonitoringPageAPI.ts"
|
||||
import { nextPhase, useExperimentLogger } from ".//MonitoringPageAPI.ts"
|
||||
|
||||
type Goal = { id?: string | number; description?: string; achieved?: boolean };
|
||||
type Trigger = { id?: string | number; label?: string ; achieved?: boolean };
|
||||
type Norm = { id?: string | number; norm?: string };
|
||||
|
||||
|
||||
const MonitoringPage: React.FC = () => {
|
||||
const getPhaseIds = useProgramStore((s) => s.getPhaseIds);
|
||||
const getNormsInPhase = useProgramStore((s) => s.getNormsInPhase);
|
||||
@@ -43,7 +42,7 @@ const MonitoringPage: React.FC = () => {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useExperimentLogger();
|
||||
return (
|
||||
<div className={styles.dashboardContainer}>
|
||||
{/* HEADER */}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const API_BASE = "http://localhost:8000/"; // Change depending on Pims interup agent/ correct endpoint
|
||||
import React, { useEffect } from 'react';
|
||||
const API_BASE = "http://localhost:8000"; // Change depending on Pims interup agent/ correct endpoint
|
||||
|
||||
|
||||
/**
|
||||
@@ -16,4 +17,37 @@ export async function nextPhase(): Promise<void> {
|
||||
if (!res.ok) {
|
||||
throw new Error("Failed to advance to next phase");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A hook that listens to the experiment stream and logs data to the console.
|
||||
* It does not render anything.
|
||||
*/
|
||||
export function useExperimentLogger() {
|
||||
useEffect(() => {
|
||||
console.log("Starting Experiment Stream listener...");
|
||||
|
||||
const eventSource = new EventSource(`${API_BASE}/experiment_stream`);
|
||||
|
||||
eventSource.onmessage = (event) => {
|
||||
try {
|
||||
const parsedData = JSON.parse(event.data);
|
||||
|
||||
console.log(" [Experiment Update Received] ", parsedData);
|
||||
|
||||
} catch (err) {
|
||||
console.warn("Received non-JSON experiment data:", event.data);
|
||||
}
|
||||
};
|
||||
|
||||
eventSource.onerror = (err) => {
|
||||
console.error("SSE Connection Error (Experiment Stream):", err);
|
||||
eventSource.close();
|
||||
};
|
||||
|
||||
return () => {
|
||||
console.log("Closing Experiment Stream listener...");
|
||||
eventSource.close();
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -9,8 +9,10 @@ import {
|
||||
import '@xyflow/react/dist/style.css';
|
||||
import {useEffect, useState} from "react";
|
||||
import {useShallow} from 'zustand/react/shallow';
|
||||
import orderPhaseNodeArray from "../../utils/orderPhaseNodes.ts";
|
||||
import useProgramStore from "../../utils/programStore.ts";
|
||||
import {DndToolbar} from './visualProgrammingUI/components/DragDropSidebar.tsx';
|
||||
import type {PhaseNode} from "./visualProgrammingUI/nodes/PhaseNode.tsx";
|
||||
import useFlowStore from './visualProgrammingUI/VisProgStores.tsx';
|
||||
import type {FlowState} from './visualProgrammingUI/VisProgTypes.tsx';
|
||||
import styles from './VisProg.module.css'
|
||||
@@ -167,14 +169,15 @@ function runProgramm() {
|
||||
*/
|
||||
function graphReducer() {
|
||||
const { nodes } = useFlowStore.getState();
|
||||
return nodes
|
||||
.filter((n) => n.type == 'phase')
|
||||
return orderPhaseNodeArray(nodes.filter((n) => n.type == 'phase') as PhaseNode [])
|
||||
.map((n) => {
|
||||
const reducer = NodeReduces['phase'];
|
||||
return reducer(n, nodes)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* houses the entire page, so also UI elements
|
||||
* that are not a part of the Visual Programming UI
|
||||
|
||||
@@ -39,10 +39,10 @@ export const UndoRedo = (
|
||||
* @param {BaseFlowState} state - the current state of the editor
|
||||
* @returns {FlowSnapshot} - returns a snapshot of the current editor state
|
||||
*/
|
||||
const getSnapshot = (state : BaseFlowState) : FlowSnapshot => ({
|
||||
const getSnapshot = (state : BaseFlowState) : FlowSnapshot => (structuredClone({
|
||||
nodes: state.nodes,
|
||||
edges: state.edges
|
||||
});
|
||||
}));
|
||||
|
||||
const initialState = config(set, get, api);
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -28,31 +28,31 @@ 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: {
|
||||
...JSON.parse(JSON.stringify(defaultData)),
|
||||
...data,
|
||||
},
|
||||
}
|
||||
}
|
||||
return {...defaultData, ...newData}
|
||||
}
|
||||
|
||||
|
||||
//* Initial nodes, created by using createNode. */
|
||||
const initialNodes : Node[] = [
|
||||
createNode('start', 'start', {x: 100, y: 100}, {label: "Start"}, false),
|
||||
createNode('end', 'end', {x: 500, y: 100}, {label: "End"}, false),
|
||||
createNode('phase-1', 'phase', {x:200, y:100}, {label: "Phase 1", children : []}),
|
||||
createNode('norms-1', 'norm', {x:-200, y:100}, {label: "Initial Norms", normList: ["Be a robot", "get good"], critical:false}),
|
||||
createNode(crypto.randomUUID(), 'phase', {x:200, y:100}, {label: "Phase 1", children : []}),
|
||||
createNode('norms-1', 'norm', {x:-200, y:100}, {label: "Initial Norms", normList: ["Be a robot", "get good"], critical:false}),
|
||||
];
|
||||
|
||||
// * Initial edges * /
|
||||
const initialEdges: Edge[] = [
|
||||
{ id: 'start-phase-1', source: 'start', target: 'phase-1' },
|
||||
{ id: 'phase-1-end', source: 'phase-1', target: 'end' },
|
||||
];
|
||||
const initialEdges: Edge[] = []; // no initial edges as edge connect events don't fire when using initial edges
|
||||
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import {
|
||||
Handle,
|
||||
useNodeConnections,
|
||||
type HandleType,
|
||||
type Position
|
||||
} from '@xyflow/react';
|
||||
|
||||
|
||||
const LimitedConnectionCountHandle = (props: {
|
||||
node_id: string,
|
||||
type: HandleType,
|
||||
position: Position,
|
||||
connection_count: number,
|
||||
id?: string
|
||||
}) => {
|
||||
const connections = useNodeConnections({
|
||||
id: props.node_id,
|
||||
handleType: props.type,
|
||||
handleId: props.id,
|
||||
});
|
||||
|
||||
return (
|
||||
<Handle
|
||||
{...props}
|
||||
isConnectable={connections.length < props.connection_count}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default LimitedConnectionCountHandle;
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import {
|
||||
Handle,
|
||||
type NodeProps,
|
||||
Position,
|
||||
type Node,
|
||||
} from '@xyflow/react';
|
||||
import LimitedConnectionCountHandle from "../components/CustomNodeHandles.tsx";
|
||||
import { Toolbar } from '../components/NodeComponents';
|
||||
import styles from '../../VisProg.module.css';
|
||||
|
||||
@@ -32,7 +32,13 @@ export default function EndNode(props: NodeProps<EndNode>) {
|
||||
<div className={"flex-row gap-sm"}>
|
||||
End
|
||||
</div>
|
||||
<Handle type="target" position={Position.Left} id="target"/>
|
||||
<LimitedConnectionCountHandle
|
||||
node_id={props.id}
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
connection_count={1}
|
||||
id="target"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { NormNodeData } from "./NormNode";
|
||||
export const NormNodeDefaults: NormNodeData = {
|
||||
label: "Norm Node",
|
||||
droppable: true,
|
||||
conditions: [],
|
||||
norm: "",
|
||||
hasReduce: true,
|
||||
critical: false,
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Toolbar } from '../components/NodeComponents';
|
||||
import styles from '../../VisProg.module.css';
|
||||
import { TextField } from '../../../../components/TextField';
|
||||
import useFlowStore from '../VisProgStores';
|
||||
import { BasicBeliefReduce } from './BasicBeliefNode';
|
||||
|
||||
/**
|
||||
* The default data dot a phase node
|
||||
@@ -19,6 +20,7 @@ import useFlowStore from '../VisProgStores';
|
||||
export type NormNodeData = {
|
||||
label: string;
|
||||
droppable: boolean;
|
||||
conditions: string[]; // List of (basic) belief nodes' ids.
|
||||
norm: string;
|
||||
hasReduce: boolean;
|
||||
critical: boolean;
|
||||
@@ -67,7 +69,14 @@ export default function NormNode(props: NodeProps<NormNode>) {
|
||||
onChange={(e) => setCritical(e.target.checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{data.conditions.length > 0 && (<div className={"flex-row gap-md align-center"} data-testid="norm-condition-information">
|
||||
<label htmlFor={checkbox_id}>{data.conditions.length} condition{data.conditions.length > 1 ? "s" : ""}/ belief{data.conditions.length > 1 ? "s" : ""} attached.</label>
|
||||
</div>)}
|
||||
|
||||
|
||||
<Handle type="source" position={Position.Right} id="norms"/>
|
||||
<Handle type="target" position={Position.Bottom} id="norms"/>
|
||||
</div>
|
||||
</>;
|
||||
};
|
||||
@@ -78,14 +87,29 @@ export default function NormNode(props: NodeProps<NormNode>) {
|
||||
* @param node The Node Properties of this node.
|
||||
* @param _nodes all the nodes in the graph
|
||||
*/
|
||||
export function NormReduce(node: Node, _nodes: Node[]) {
|
||||
export function NormReduce(node: Node, nodes: Node[]) {
|
||||
const data = node.data as NormNodeData;
|
||||
return {
|
||||
id: node.id,
|
||||
label: data.label,
|
||||
norm: data.norm,
|
||||
critical: data.critical,
|
||||
}
|
||||
|
||||
// conditions nodes - make sure to check for empty arrays
|
||||
let conditionNodes: Node[] = [];
|
||||
if (data.conditions)
|
||||
conditionNodes = nodes.filter((node) => data.conditions.includes(node.id));
|
||||
|
||||
// Build the result object
|
||||
const result: Record<string, unknown> = {
|
||||
id: node.id,
|
||||
label: data.label,
|
||||
norm: data.norm,
|
||||
critical: data.critical,
|
||||
};
|
||||
|
||||
// Go over our conditionNodes. They should either be Basic (OR TODO: Inferred)
|
||||
const reducer = BasicBeliefReduce;
|
||||
result["basic_beliefs"] = conditionNodes.map((condition) => reducer(condition, nodes))
|
||||
|
||||
// When the Inferred is being implemented, you should follow the same kind of structure that PhaseNode has,
|
||||
// dividing the conditions into basic and inferred, then calling the correct reducer on them.
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,7 +118,11 @@ export function NormReduce(node: Node, _nodes: Node[]) {
|
||||
* @param _sourceNodeId the source of the received connection
|
||||
*/
|
||||
export function NormConnectionTarget(_thisNode: Node, _sourceNodeId: string) {
|
||||
// no additional connection logic exists yet
|
||||
const data = _thisNode.data as NormNodeData;
|
||||
// If we got a belief connected, this is a condition for the norm.
|
||||
if ((useFlowStore.getState().nodes.find((node) => node.id === _sourceNodeId && node.type === 'basic_belief' /* TODO: Add the option for an inferred belief */))) {
|
||||
data.conditions.push(_sourceNodeId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -112,7 +140,11 @@ export function NormConnectionSource(_thisNode: Node, _targetNodeId: string) {
|
||||
* @param _sourceNodeId the source of the disconnected connection
|
||||
*/
|
||||
export function NormDisconnectionTarget(_thisNode: Node, _sourceNodeId: string) {
|
||||
// no additional connection logic exists yet
|
||||
const data = _thisNode.data as NormNodeData;
|
||||
// If we got a belief connected, this is a condition for the norm.
|
||||
if ((useFlowStore.getState().nodes.find((node) => node.id === _sourceNodeId && node.type === 'basic_belief' /* TODO: Add the option for an inferred belief */))) {
|
||||
data.conditions = data.conditions.filter(id => id != _sourceNodeId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,4 +8,6 @@ export const PhaseNodeDefaults: PhaseNodeData = {
|
||||
droppable: true,
|
||||
children: [],
|
||||
hasReduce: true,
|
||||
nextPhaseId: null,
|
||||
isFirstPhase: false,
|
||||
};
|
||||
@@ -9,6 +9,7 @@ import styles from '../../VisProg.module.css';
|
||||
import { NodeReduces, NodesInPhase, NodeTypes} from '../NodeRegistry';
|
||||
import useFlowStore from '../VisProgStores';
|
||||
import { TextField } from '../../../../components/TextField';
|
||||
import LimitedConnectionCountHandle from "../components/CustomNodeHandles.tsx";
|
||||
|
||||
/**
|
||||
* The default data dot a phase node
|
||||
@@ -16,12 +17,15 @@ import { TextField } from '../../../../components/TextField';
|
||||
* @param droppable: whether this node is droppable from the drop bar (initialized as true)
|
||||
* @param children: ID's of children of this node
|
||||
* @param hasReduce: whether this node has reducing functionality (true by default)
|
||||
* @param nextPhaseId:
|
||||
*/
|
||||
export type PhaseNodeData = {
|
||||
label: string;
|
||||
droppable: boolean;
|
||||
children: string[];
|
||||
hasReduce: boolean;
|
||||
nextPhaseId: string | "end" | null;
|
||||
isFirstPhase: boolean;
|
||||
};
|
||||
|
||||
export type PhaseNode = Node<PhaseNodeData>
|
||||
@@ -50,9 +54,21 @@ export default function PhaseNode(props: NodeProps<PhaseNode>) {
|
||||
placeholder={"Phase ..."}
|
||||
/>
|
||||
</div>
|
||||
<Handle type="target" position={Position.Left} id="target"/>
|
||||
<LimitedConnectionCountHandle
|
||||
node_id={props.id}
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
connection_count={1}
|
||||
id="target"
|
||||
/>
|
||||
<Handle type="target" position={Position.Bottom} id="norms"/>
|
||||
<Handle type="source" position={Position.Right} id="source"/>
|
||||
<LimitedConnectionCountHandle
|
||||
node_id={props.id}
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
connection_count={1}
|
||||
id="source"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
@@ -65,8 +81,8 @@ export default function PhaseNode(props: NodeProps<PhaseNode>) {
|
||||
* @returns A collection of all reduced nodes in this phase, starting with this phases' reduced data.
|
||||
*/
|
||||
export function PhaseReduce(node: Node, nodes: Node[]) {
|
||||
const thisnode = node as PhaseNode;
|
||||
const data = thisnode.data as PhaseNodeData;
|
||||
const thisNode = node as PhaseNode;
|
||||
const data = thisNode.data as PhaseNodeData;
|
||||
|
||||
// node typings that are not in phase
|
||||
const nodesNotInPhase: string[] = Object.entries(NodesInPhase)
|
||||
@@ -85,8 +101,8 @@ export function PhaseReduce(node: Node, nodes: Node[]) {
|
||||
|
||||
// Build the result object
|
||||
const result: Record<string, unknown> = {
|
||||
id: thisnode.id,
|
||||
label: data.label,
|
||||
id: thisNode.id,
|
||||
label: data.label,
|
||||
};
|
||||
|
||||
nodesInPhase.forEach((type) => {
|
||||
@@ -109,13 +125,19 @@ export function PhaseReduce(node: Node, nodes: Node[]) {
|
||||
* @param _sourceNodeId the source of the received connection
|
||||
*/
|
||||
export function PhaseConnectionTarget(_thisNode: Node, _sourceNodeId: string) {
|
||||
const node = _thisNode as PhaseNode
|
||||
const data = node.data as PhaseNodeData
|
||||
// we only add none phase nodes to the children
|
||||
if (!(useFlowStore.getState().nodes.find((node) => node.id === _sourceNodeId && node.type === 'phase'))) {
|
||||
data.children.push(_sourceNodeId)
|
||||
}
|
||||
const data = _thisNode.data as PhaseNodeData
|
||||
|
||||
const nodes = useFlowStore.getState().nodes;
|
||||
const sourceNode = nodes.find((node) => node.id === _sourceNodeId)!
|
||||
switch (sourceNode.type) {
|
||||
case "phase": break;
|
||||
case "start": data.isFirstPhase = true; break;
|
||||
// we only add none phase or start nodes to the children
|
||||
// endNodes cannot be the source of an outgoing connection
|
||||
// so we don't need to cover them with a special case
|
||||
// before handling the default behavior
|
||||
default: data.children.push(_sourceNodeId); break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,7 +146,19 @@ export function PhaseConnectionTarget(_thisNode: Node, _sourceNodeId: string) {
|
||||
* @param _targetNodeId the target of the created connection
|
||||
*/
|
||||
export function PhaseConnectionSource(_thisNode: Node, _targetNodeId: string) {
|
||||
// no additional connection logic exists yet
|
||||
const data = _thisNode.data as PhaseNodeData
|
||||
const nodes = useFlowStore.getState().nodes;
|
||||
|
||||
const targetNode = nodes.find((node) => node.id === _targetNodeId)
|
||||
if (!targetNode) {throw new Error("Source node not found")}
|
||||
|
||||
// we set the nextPhaseId to the next target's id if the target is a phaseNode,
|
||||
// or "end" if the target node is the end node
|
||||
switch (targetNode.type) {
|
||||
case 'phase': data.nextPhaseId = _targetNodeId; break;
|
||||
case 'end': data.nextPhaseId = "end"; break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -133,9 +167,23 @@ export function PhaseConnectionSource(_thisNode: Node, _targetNodeId: string) {
|
||||
* @param _sourceNodeId the source of the disconnected connection
|
||||
*/
|
||||
export function PhaseDisconnectionTarget(_thisNode: Node, _sourceNodeId: string) {
|
||||
const node = _thisNode as PhaseNode
|
||||
const data = node.data as PhaseNodeData
|
||||
data.children = data.children.filter((child) => { if (child != _sourceNodeId) return child; });
|
||||
const data = _thisNode.data as PhaseNodeData
|
||||
|
||||
const nodes = useFlowStore.getState().nodes;
|
||||
const sourceNode = nodes.find((node) => node.id === _sourceNodeId)
|
||||
const sourceType = sourceNode ? sourceNode.type : "deleted";
|
||||
switch (sourceType) {
|
||||
case "phase": break;
|
||||
case "start": data.isFirstPhase = false; break;
|
||||
// we only add none phase or start nodes to the children
|
||||
// endNodes cannot be the source of an outgoing connection
|
||||
// so we don't need to cover them with a special case
|
||||
// before handling the default behavior
|
||||
default:
|
||||
data.children = data.children.filter((child) => { if (child != _sourceNodeId) return child; });
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -144,5 +192,12 @@ export function PhaseDisconnectionTarget(_thisNode: Node, _sourceNodeId: string)
|
||||
* @param _targetNodeId the target of the diconnected connection
|
||||
*/
|
||||
export function PhaseDisconnectionSource(_thisNode: Node, _targetNodeId: string) {
|
||||
// no additional connection logic exists yet
|
||||
const data = _thisNode.data as PhaseNodeData
|
||||
const nodes = useFlowStore.getState().nodes;
|
||||
|
||||
// if the target is a phase or end node set the nextPhaseId to null,
|
||||
// as we are no longer connected to a subsequent phaseNode or to the endNode
|
||||
if (nodes.some((node) => node.id === _targetNodeId && ['phase', 'end'].includes(node.type!))){
|
||||
data.nextPhaseId = null;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import {
|
||||
Handle,
|
||||
type NodeProps,
|
||||
Position,
|
||||
type Node,
|
||||
} from '@xyflow/react';
|
||||
import LimitedConnectionCountHandle from "../components/CustomNodeHandles.tsx";
|
||||
import { Toolbar } from '../components/NodeComponents';
|
||||
import styles from '../../VisProg.module.css';
|
||||
|
||||
@@ -31,7 +31,13 @@ export default function StartNode(props: NodeProps<StartNode>) {
|
||||
<div className={"flex-row gap-sm"}>
|
||||
Start
|
||||
</div>
|
||||
<Handle type="source" position={Position.Right} id="source"/>
|
||||
<LimitedConnectionCountHandle
|
||||
node_id={props.id}
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
connection_count={1}
|
||||
id="source"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
40
src/utils/orderPhaseNodes.ts
Normal file
40
src/utils/orderPhaseNodes.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type {PhaseNode} from "../pages/VisProgPage/visualProgrammingUI/nodes/PhaseNode.tsx";
|
||||
|
||||
/**
|
||||
* takes an array of phaseNodes and orders them according to their nextPhaseId attributes,
|
||||
* starting with the phase that has isFirstPhase = true
|
||||
*
|
||||
* @param {PhaseNode[]} nodes an unordered phaseNode array
|
||||
* @returns {PhaseNode[]} the ordered phaseNode array
|
||||
*/
|
||||
export default function orderPhaseNodeArray(nodes: PhaseNode[]) : PhaseNode[] {
|
||||
// find the first phaseNode of the sequence
|
||||
const start = nodes.find(node => node.data.isFirstPhase);
|
||||
if (!start) {
|
||||
throw new Error('No phaseNode with isFirstObject = true found');
|
||||
}
|
||||
|
||||
// prepare for ordering of phaseNodes
|
||||
const orderedPhaseNodes: PhaseNode[] = [];
|
||||
const IdMap = new Map(nodes.map(node => [node.id, node]));
|
||||
let currentNode: PhaseNode | undefined = start;
|
||||
|
||||
// populate orderedPhaseNodes array with the phaseNodes in the correct order
|
||||
while (currentNode) {
|
||||
orderedPhaseNodes.push(currentNode);
|
||||
|
||||
if (!currentNode.data.nextPhaseId) {
|
||||
throw new Error("Incomplete phase sequence, program does not reach the end node");
|
||||
}
|
||||
|
||||
if (currentNode.data.nextPhaseId === "end") break;
|
||||
|
||||
currentNode = IdMap.get(currentNode.data.nextPhaseId);
|
||||
|
||||
if (!currentNode) {
|
||||
throw new Error(`Incomplete phase sequence, phaseNode with id "${orderedPhaseNodes.at(-1)?.data.nextPhaseId}" not found`);
|
||||
}
|
||||
}
|
||||
|
||||
return orderedPhaseNodes;
|
||||
}
|
||||
@@ -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