// This program has been developed by students from the bachelor Computer Science at Utrecht // University within the Software Project course. // © Copyright Utrecht University (Department of Information and Computing Sciences) 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/PlanEditor/Plan.default.ts'; describe('GoalNode', () => { let user: ReturnType; 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( ); expect(screen.getByPlaceholderText('To ...')).toBeInTheDocument(); }); it('updates goal name when user types and commits', async () => { const mockNode: Node = { id: 'goal-2', type: 'goal', position: { x: 0, y: 0 }, data: { ...JSON.parse(JSON.stringify(GoalNodeDefaults)), name: '' }, }; useFlowStore.setState({ nodes: [mockNode], edges: [] }); renderWithProviders( ); 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 = { 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( ); 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 = { 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( ); 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 = { 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( ); 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'); }); });