test: add tests for goal and triggers
This commit is contained in:
@@ -1,8 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
type NodeProps,
|
type NodeProps,
|
||||||
Position,
|
Position,
|
||||||
type Connection,
|
|
||||||
type Edge,
|
|
||||||
type Node,
|
type Node,
|
||||||
} from '@xyflow/react';
|
} from '@xyflow/react';
|
||||||
import { Toolbar } from '../components/NodeComponents';
|
import { Toolbar } from '../components/NodeComponents';
|
||||||
@@ -40,18 +38,6 @@ export type TriggerNodeData = {
|
|||||||
|
|
||||||
export type TriggerNode = Node<TriggerNodeData>
|
export type TriggerNode = Node<TriggerNodeData>
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines whether a Trigger node can connect to another node or edge.
|
|
||||||
*
|
|
||||||
* @param connection - The connection or edge being attempted to connect towards.
|
|
||||||
* @returns `true` if the connection is defined; otherwise, `false`.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export function TriggerNodeCanConnect(connection: Connection | Edge): boolean {
|
|
||||||
return (connection != undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines how a Trigger node should be rendered
|
* Defines how a Trigger node should be rendered
|
||||||
* @param props - Node properties provided by React Flow, including `id` and `data`.
|
* @param props - Node properties provided by React Flow, including `id` and `data`.
|
||||||
|
|||||||
@@ -0,0 +1,253 @@
|
|||||||
|
import { describe, it, beforeEach } from '@jest/globals';
|
||||||
|
import { screen, waitFor } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { renderWithProviders } from '../../../../test-utils/test-utils.tsx';
|
||||||
|
import GoalNode, { GoalReduce, type GoalNodeData } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/GoalNode';
|
||||||
|
import useFlowStore from '../../../../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores';
|
||||||
|
import type { Node } from '@xyflow/react';
|
||||||
|
import '@testing-library/jest-dom';
|
||||||
|
import { GoalNodeDefaults } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/GoalNode.default.ts';
|
||||||
|
import { defaultPlan } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/components/Plan.default.ts';
|
||||||
|
|
||||||
|
describe('GoalNode', () => {
|
||||||
|
let user: ReturnType<typeof userEvent.setup>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
user = userEvent.setup();
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the Goal node with default data', () => {
|
||||||
|
const mockNode: Node = {
|
||||||
|
id: 'goal-1',
|
||||||
|
type: 'goal',
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
data: { ...JSON.parse(JSON.stringify(GoalNodeDefaults)) },
|
||||||
|
};
|
||||||
|
|
||||||
|
renderWithProviders(
|
||||||
|
<GoalNode
|
||||||
|
id={mockNode.id}
|
||||||
|
type={mockNode.type as string}
|
||||||
|
data={mockNode.data as any}
|
||||||
|
selected={false}
|
||||||
|
isConnectable={true}
|
||||||
|
zIndex={0}
|
||||||
|
dragging={false}
|
||||||
|
selectable={true}
|
||||||
|
deletable={true}
|
||||||
|
draggable={true}
|
||||||
|
positionAbsoluteX={0}
|
||||||
|
positionAbsoluteY={0}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByPlaceholderText('To ...')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates goal name when user types and commits', async () => {
|
||||||
|
const mockNode: Node<GoalNodeData> = {
|
||||||
|
id: 'goal-2',
|
||||||
|
type: 'goal',
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
data: { ...JSON.parse(JSON.stringify(GoalNodeDefaults)), name: '' },
|
||||||
|
};
|
||||||
|
|
||||||
|
useFlowStore.setState({ nodes: [mockNode], edges: [] });
|
||||||
|
|
||||||
|
renderWithProviders(
|
||||||
|
<GoalNode
|
||||||
|
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('To ...');
|
||||||
|
|
||||||
|
await user.type(input, 'Save the world{enter}');
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const state = useFlowStore.getState();
|
||||||
|
const updated = state.nodes.find(n => n.id === 'goal-2');
|
||||||
|
expect(updated?.data.name).toBe('Save the world');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows plan message and disabled checked checkbox when plan does not iterate', () => {
|
||||||
|
const mockNode: Node<GoalNodeData> = {
|
||||||
|
id: 'goal-3',
|
||||||
|
type: 'goal',
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
data: { ...JSON.parse(JSON.stringify(GoalNodeDefaults)), plan: defaultPlan, name: 'G' },
|
||||||
|
};
|
||||||
|
|
||||||
|
useFlowStore.setState({ nodes: [mockNode], edges: [] });
|
||||||
|
|
||||||
|
renderWithProviders(
|
||||||
|
<GoalNode
|
||||||
|
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(/Will follow plan 'Default Plan' until all steps complete./i)).toBeInTheDocument();
|
||||||
|
|
||||||
|
const checkbox = screen.getByLabelText(/This plan always succeeds!/i) as HTMLInputElement;
|
||||||
|
expect(checkbox).toBeDisabled();
|
||||||
|
expect(checkbox.checked).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows toggling can_fail when plan iterates', async () => {
|
||||||
|
// plan with an llm-step will make DoesPlanIterate return true
|
||||||
|
const iterPlan = { ...defaultPlan, id: 'p-iter', steps: [{ id: 'a-1', type: 'llm', goal: 'do' }] } as any;
|
||||||
|
const mockNode: Node<GoalNodeData> = {
|
||||||
|
id: 'goal-4',
|
||||||
|
type: 'goal',
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
data: { ...JSON.parse(JSON.stringify(GoalNodeDefaults)), plan: iterPlan, name: 'Iterating' },
|
||||||
|
};
|
||||||
|
|
||||||
|
useFlowStore.setState({ nodes: [mockNode], edges: [] });
|
||||||
|
|
||||||
|
renderWithProviders(
|
||||||
|
<GoalNode
|
||||||
|
id={mockNode.id}
|
||||||
|
type={mockNode.type as string}
|
||||||
|
data={mockNode.data as any}
|
||||||
|
selected={false}
|
||||||
|
isConnectable={true}
|
||||||
|
zIndex={0}
|
||||||
|
dragging={false}
|
||||||
|
selectable={true}
|
||||||
|
deletable={true}
|
||||||
|
draggable={true}
|
||||||
|
positionAbsoluteX={0}
|
||||||
|
positionAbsoluteY={0}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkbox = screen.getByLabelText(/Check if this plan fails/i) as HTMLInputElement;
|
||||||
|
expect(checkbox).not.toBeDisabled();
|
||||||
|
expect(checkbox.checked).toBe(false);
|
||||||
|
|
||||||
|
await user.click(checkbox);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const state = useFlowStore.getState();
|
||||||
|
const updated = state.nodes.find(n => n.id === 'goal-4');
|
||||||
|
expect(updated?.data.can_fail).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disables the checkbox and shows description when plan includes a checking sub-goal', () => {
|
||||||
|
const childGoal: Node = {
|
||||||
|
id: 'child-1',
|
||||||
|
type: 'goal',
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
data: { ...JSON.parse(JSON.stringify(GoalNodeDefaults)), can_fail: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
const p = { ...defaultPlan, id: 'p-2', steps: [{ id: 'child-1', type: 'goal' } as any] } as any;
|
||||||
|
|
||||||
|
const mockNode: Node<GoalNodeData> = {
|
||||||
|
id: 'goal-5',
|
||||||
|
type: 'goal',
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
data: { ...JSON.parse(JSON.stringify(GoalNodeDefaults)), plan: p, name: 'HasCheck' },
|
||||||
|
};
|
||||||
|
|
||||||
|
useFlowStore.setState({ nodes: [mockNode, childGoal], edges: [] });
|
||||||
|
|
||||||
|
renderWithProviders(
|
||||||
|
<GoalNode
|
||||||
|
id={mockNode.id}
|
||||||
|
type={mockNode.type as string}
|
||||||
|
data={mockNode.data as any}
|
||||||
|
selected={false}
|
||||||
|
isConnectable={true}
|
||||||
|
zIndex={0}
|
||||||
|
dragging={false}
|
||||||
|
selectable={true}
|
||||||
|
deletable={true}
|
||||||
|
draggable={true}
|
||||||
|
positionAbsoluteX={0}
|
||||||
|
positionAbsoluteY={0}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkbox = screen.getByRole('checkbox') as HTMLInputElement;
|
||||||
|
expect(checkbox).toBeDisabled();
|
||||||
|
expect(checkbox.checked).toBe(true);
|
||||||
|
|
||||||
|
// description box should be visible because there's a checking subgoal
|
||||||
|
expect(screen.getByPlaceholderText('Describe the condition of this goal...')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reduces its data correctly (GoalReduce)', () => {
|
||||||
|
const childGoal: Node = {
|
||||||
|
id: 'child-2',
|
||||||
|
type: 'goal',
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
data: { ...JSON.parse(JSON.stringify(GoalNodeDefaults)), can_fail: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
const p = { ...defaultPlan, id: 'p-3', steps: [{ id: 'child-2', type: 'goal' } as any] } as any;
|
||||||
|
|
||||||
|
const mockNode: Node = {
|
||||||
|
id: 'goal-6',
|
||||||
|
type: 'goal',
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
data: { ...JSON.parse(JSON.stringify(GoalNodeDefaults)), plan: p, name: 'ReduceMe', description: 'desc', can_fail: false },
|
||||||
|
};
|
||||||
|
|
||||||
|
const reduced = GoalReduce(mockNode, [mockNode, childGoal]);
|
||||||
|
expect(reduced).toEqual({
|
||||||
|
id: 'goal-6',
|
||||||
|
name: 'ReduceMe',
|
||||||
|
description: 'desc',
|
||||||
|
can_fail: true,
|
||||||
|
plan: {
|
||||||
|
id: expect.anything(),
|
||||||
|
steps: expect.any(Array),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds a goal into a plan when a goal is connected to another', () => {
|
||||||
|
const source: Node = { id: 'g-src', type: 'goal', position: { x: 0, y: 0 }, data: { ...JSON.parse(JSON.stringify(GoalNodeDefaults)), name: 'Source' } };
|
||||||
|
const target: Node = { id: 'g-target', type: 'goal', position: { x: 0, y: 0 }, data: { ...JSON.parse(JSON.stringify(GoalNodeDefaults)), name: 'Target' } };
|
||||||
|
|
||||||
|
useFlowStore.setState({ nodes: [source, target], edges: [] });
|
||||||
|
|
||||||
|
// Simulate react-flow connect
|
||||||
|
useFlowStore.getState().onConnect({ source: 'g-src', target: 'g-target', sourceHandle: null, targetHandle: null });
|
||||||
|
|
||||||
|
const state = useFlowStore.getState();
|
||||||
|
const updatedTarget = state.nodes.find(n => n.id === 'g-target');
|
||||||
|
|
||||||
|
expect(updatedTarget?.data.plan).toBeDefined();
|
||||||
|
const plan = updatedTarget?.data.plan as any;
|
||||||
|
expect(plan.steps.length).toBe(1);
|
||||||
|
expect(plan.steps[0].id).toBe('g-src');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -3,7 +3,6 @@ import { screen } from '@testing-library/react';
|
|||||||
import { renderWithProviders } from '../../../../test-utils/test-utils.tsx';
|
import { renderWithProviders } from '../../../../test-utils/test-utils.tsx';
|
||||||
import TriggerNode, {
|
import TriggerNode, {
|
||||||
TriggerReduce,
|
TriggerReduce,
|
||||||
TriggerNodeCanConnect,
|
|
||||||
type TriggerNodeData,
|
type TriggerNodeData,
|
||||||
TriggerConnectionSource, TriggerConnectionTarget
|
TriggerConnectionSource, TriggerConnectionTarget
|
||||||
} from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/TriggerNode';
|
} from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/TriggerNode';
|
||||||
@@ -14,6 +13,8 @@ import { TriggerNodeDefaults } from '../../../../../src/pages/VisProgPage/visual
|
|||||||
import { BasicBeliefNodeDefaults } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.default.ts';
|
import { BasicBeliefNodeDefaults } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.default.ts';
|
||||||
import { defaultPlan } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/components/Plan.default.ts';
|
import { defaultPlan } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/components/Plan.default.ts';
|
||||||
import { NormNodeDefaults } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/NormNode.default.ts';
|
import { NormNodeDefaults } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/NormNode.default.ts';
|
||||||
|
import { GoalNodeDefaults } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/GoalNode.default.ts';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
|
||||||
describe('TriggerNode', () => {
|
describe('TriggerNode', () => {
|
||||||
|
|
||||||
@@ -134,10 +135,50 @@ describe('TriggerNode', () => {
|
|||||||
TriggerConnectionTarget(node1, node2.id);
|
TriggerConnectionTarget(node1, node2.id);
|
||||||
}).not.toThrow();
|
}).not.toThrow();
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should return true for TriggerNodeCanConnect if connection exists', () => {
|
|
||||||
const connection = { source: 'trigger-1', target: 'norm-1' };
|
describe('TriggerConnects Function', () => {
|
||||||
expect(TriggerNodeCanConnect(connection as any)).toBe(true);
|
it('should correctly remove a goal from the triggers plan after it has been disconnected', () => {
|
||||||
|
// first, define the goal node and trigger node.
|
||||||
|
const goal: Node = {
|
||||||
|
id: 'g-1',
|
||||||
|
type: 'goal',
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
data: { ...JSON.parse(JSON.stringify(GoalNodeDefaults)), name: 'Goal 1' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const trigger: Node<TriggerNodeData> = {
|
||||||
|
id: 'trigger-1',
|
||||||
|
type: 'trigger',
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
data: { ...JSON.parse(JSON.stringify(TriggerNodeDefaults)) },
|
||||||
|
};
|
||||||
|
|
||||||
|
// set initial store
|
||||||
|
useFlowStore.setState({ nodes: [goal, trigger], edges: [] });
|
||||||
|
|
||||||
|
// then, connect the goal to the trigger.
|
||||||
|
act(() => {
|
||||||
|
useFlowStore.getState().onConnect({ source: 'g-1', target: 'trigger-1', sourceHandle: null, targetHandle: null });
|
||||||
|
});
|
||||||
|
|
||||||
|
// expect the goal id to be part of a goal step of the plan.
|
||||||
|
let updatedTrigger = useFlowStore.getState().nodes.find((n) => n.id === 'trigger-1');
|
||||||
|
expect(updatedTrigger?.data.plan).toBeDefined();
|
||||||
|
const plan = updatedTrigger?.data.plan as any;
|
||||||
|
expect(plan.steps.find((s: any) => s.id === 'g-1')).toBeDefined();
|
||||||
|
|
||||||
|
// then, disconnect the goal from the trigger.
|
||||||
|
act(() => {
|
||||||
|
useFlowStore.getState().onEdgesDelete([{ id: 'g-1-trigger-1', source: 'g-1', target: 'trigger-1' } as any]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// finally, expect the goal id to NOT be part of the goal step of the plan.
|
||||||
|
updatedTrigger = useFlowStore.getState().nodes.find((n) => n.id === 'trigger-1');
|
||||||
|
const planAfter = updatedTrigger?.data.plan as any;
|
||||||
|
const stillHas = planAfter?.steps?.find((s: any) => s.id === 'g-1');
|
||||||
|
expect(stillHas).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user