Merge remote-tracking branch 'origin/demo' into feat/monitoringpage-pim
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
import {renderHook} from "@testing-library/react";
|
||||
import type {Connection} from "@xyflow/react";
|
||||
import {
|
||||
ruleResult,
|
||||
type RuleResult,
|
||||
useHandleRules
|
||||
} from "../../../../src/pages/VisProgPage/visualProgrammingUI/HandleRuleLogic.ts";
|
||||
import useFlowStore from "../../../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx";
|
||||
|
||||
describe('useHandleRules', () => {
|
||||
it('should register rules on mount and validate connection', () => {
|
||||
const rules = [() => ({ isSatisfied: true } as RuleResult)];
|
||||
|
||||
const { result } = renderHook(() => useHandleRules('node1', 'h1', 'target', rules));
|
||||
|
||||
// Confirm rules registered
|
||||
const storedRules = useFlowStore.getState().getTargetRules('node1', 'h1');
|
||||
expect(storedRules).toEqual(rules);
|
||||
|
||||
// Validate a connection
|
||||
const connection = { source: 'node2', sourceHandle: 'h2', target: 'node1', targetHandle: 'h1' };
|
||||
const validation = result.current(connection);
|
||||
expect(validation).toEqual(ruleResult.satisfied);
|
||||
});
|
||||
|
||||
it('should throw error if targetHandle missing', () => {
|
||||
const rules: any[] = [];
|
||||
const { result } = renderHook(() => useHandleRules('node1', 'h1', 'target', rules));
|
||||
|
||||
expect(() =>
|
||||
result.current({ source: 'a', target: 'b', targetHandle: null, sourceHandle: null })
|
||||
).toThrow('No target handle was provided');
|
||||
});
|
||||
});
|
||||
|
||||
describe('useHandleRules with multiple failed rules', () => {
|
||||
it('should return the first failed rule message and consider connectionCount', () => {
|
||||
// Mock rules for the target handle
|
||||
const failingRules = [
|
||||
(_conn: any, ctx: any) => {
|
||||
if (ctx.connectionCount >= 1) {
|
||||
return { isSatisfied: false, message: 'Max connections reached' } as RuleResult;
|
||||
}
|
||||
return { isSatisfied: true } as RuleResult;
|
||||
},
|
||||
() => ({ isSatisfied: false, message: 'Other rule failed' } as RuleResult),
|
||||
() => ({ isSatisfied: true } as RuleResult),
|
||||
];
|
||||
|
||||
// Register rules for the target handle
|
||||
useFlowStore.getState().registerRules('targetNode', 'targetHandle', failingRules);
|
||||
|
||||
// Add one existing edge to simulate connectionCount
|
||||
useFlowStore.setState({
|
||||
edges: [
|
||||
{
|
||||
id: 'edge-1',
|
||||
source: 'sourceNode',
|
||||
sourceHandle: 'sourceHandle',
|
||||
target: 'targetNode',
|
||||
targetHandle: 'targetHandle',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Create hook for a source node handle
|
||||
const rulesForSource = [
|
||||
(_c: Connection) => ({ isSatisfied: true } as RuleResult)
|
||||
];
|
||||
const { result } = renderHook(() =>
|
||||
useHandleRules('sourceNode', 'sourceHandle', 'source', rulesForSource)
|
||||
);
|
||||
|
||||
const connection = {
|
||||
source: 'sourceNode',
|
||||
sourceHandle: 'sourceHandle',
|
||||
target: 'targetNode',
|
||||
targetHandle: 'targetHandle',
|
||||
};
|
||||
|
||||
const validation = result.current(connection);
|
||||
|
||||
// Should fail with first failing rule message
|
||||
expect(validation).toEqual(ruleResult.notSatisfied('Max connections reached'));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
import {ruleResult} from "../../../../src/pages/VisProgPage/visualProgrammingUI/HandleRuleLogic.ts";
|
||||
import {
|
||||
allowOnlyConnectionsFromType,
|
||||
allowOnlyConnectionsFromHandle, noSelfConnections
|
||||
} from "../../../../src/pages/VisProgPage/visualProgrammingUI/HandleRules.ts";
|
||||
import useFlowStore from "../../../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx";
|
||||
|
||||
beforeEach(() => {
|
||||
useFlowStore.setState({
|
||||
nodes: [
|
||||
{ id: 'nodeA', type: 'typeA', position: { x: 0, y: 0 }, data: {} },
|
||||
{ id: 'nodeB', type: 'typeB', position: { x: 0, y: 0 }, data: {} },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe('allowOnlyConnectionsFromType', () => {
|
||||
it('should allow connection from allowed node type', () => {
|
||||
const rule = allowOnlyConnectionsFromType(['typeA']);
|
||||
const connection = { source: 'nodeA', sourceHandle: 'h1', target: 'nodeB', targetHandle: 'h2' };
|
||||
const context = { connectionCount: 0, source: { id: 'nodeA', handleId: 'h1' }, target: { id: 'nodeB', handleId: 'h2' } };
|
||||
|
||||
const result = rule(connection, context);
|
||||
expect(result).toEqual(ruleResult.satisfied);
|
||||
});
|
||||
|
||||
it('should not allow connection from disallowed node type', () => {
|
||||
const rule = allowOnlyConnectionsFromType(['typeA']);
|
||||
const connection = { source: 'nodeB', sourceHandle: 'h1', target: 'nodeA', targetHandle: 'h2' };
|
||||
const context = { connectionCount: 0, source: { id: 'nodeB', handleId: 'h1' }, target: { id: 'nodeA', handleId: 'h2' } };
|
||||
|
||||
const result = rule(connection, context);
|
||||
expect(result).toEqual(ruleResult.notSatisfied("the target doesn't allow connections from nodes with type: typeB"));
|
||||
});
|
||||
});
|
||||
describe('allowOnlyConnectionsFromHandle', () => {
|
||||
it('should allow connection from node with correct type and handle', () => {
|
||||
const rule = allowOnlyConnectionsFromHandle([{ nodeType: 'typeA', handleId: 'h1' }]);
|
||||
const connection = { source: 'nodeA', sourceHandle: 'h1', target: 'nodeB', targetHandle: 'h2' };
|
||||
const context = { connectionCount: 0, source: { id: 'nodeA', handleId: 'h1' }, target: { id: 'nodeB', handleId: 'h2' } };
|
||||
|
||||
const result = rule(connection, context);
|
||||
expect(result).toEqual(ruleResult.satisfied);
|
||||
});
|
||||
|
||||
it('should not allow connection from node with wrong handle', () => {
|
||||
const rule = allowOnlyConnectionsFromHandle([{ nodeType: 'typeA', handleId: 'h1' }]);
|
||||
const connection = { source: 'nodeA', sourceHandle: 'wrongHandle', target: 'nodeB', targetHandle: 'h2' };
|
||||
const context = { connectionCount: 0, source: { id: 'nodeA', handleId: 'wrongHandle' }, target: { id: 'nodeB', handleId: 'h2' } };
|
||||
|
||||
const result = rule(connection, context);
|
||||
expect(result).toEqual(ruleResult.notSatisfied("the target doesn't allow connections from nodes with type: typeA"));
|
||||
});
|
||||
|
||||
it('should not allow connection from node with wrong type', () => {
|
||||
const rule = allowOnlyConnectionsFromHandle([{ nodeType: 'typeA', handleId: 'h1' }]);
|
||||
const connection = { source: 'nodeB', sourceHandle: 'h1', target: 'nodeA', targetHandle: 'h2' };
|
||||
const context = { connectionCount: 0, source: { id: 'nodeB', handleId: 'h1' }, target: { id: 'nodeA', handleId: 'h2' } };
|
||||
|
||||
const result = rule(connection, context);
|
||||
expect(result).toEqual(ruleResult.notSatisfied("the target doesn't allow connections from nodes with type: typeB"));
|
||||
});
|
||||
});
|
||||
|
||||
describe('noSelfConnections', () => {
|
||||
it('should allow connection from node with other type and handle', () => {
|
||||
const rule = noSelfConnections;
|
||||
const connection = { source: 'nodeA', sourceHandle: 'h1', target: 'nodeB', targetHandle: 'h2' };
|
||||
const context = { connectionCount: 0, source: { id: 'nodeA', handleId: 'h1' }, target: { id: 'nodeB', handleId: 'h2' } };
|
||||
|
||||
const result = rule(connection, context);
|
||||
expect(result).toEqual(ruleResult.satisfied);
|
||||
});
|
||||
|
||||
it('should not allow connection from other handle on same node', () => {
|
||||
const rule = noSelfConnections;
|
||||
const connection = { source: 'nodeA', sourceHandle: 'h1', target: 'nodeA', targetHandle: 'h2' };
|
||||
const context = { connectionCount: 0, source: { id: 'nodeA', handleId: 'h1' }, target: { id: 'nodeB', handleId: 'h2' } };
|
||||
|
||||
const result = rule(connection, context);
|
||||
expect(result).toEqual(ruleResult.notSatisfied("nodes are not allowed to connect to themselves"));
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import {act} from '@testing-library/react';
|
||||
import type {Connection, Edge, Node} from "@xyflow/react";
|
||||
import type {HandleRule, RuleResult} from "../../../../src/pages/VisProgPage/visualProgrammingUI/HandleRuleLogic.ts";
|
||||
import { NodeDisconnections } from "../../../../src/pages/VisProgPage/visualProgrammingUI/NodeRegistry.ts";
|
||||
import type {PhaseNodeData} from "../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/PhaseNode.tsx";
|
||||
import useFlowStore from '../../../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx';
|
||||
@@ -594,5 +595,48 @@ describe('FlowStore Functionality', () => {
|
||||
expect(updatedState.nodes).toHaveLength(1);
|
||||
expect(updatedState.nodes[0]).toMatchObject(expected.node);
|
||||
})
|
||||
})
|
||||
describe('Handle Rule Registry', () => {
|
||||
it('should register and retrieve rules', () => {
|
||||
const mockRules: HandleRule[] = [() => ({ isSatisfied: true } as RuleResult)];
|
||||
|
||||
useFlowStore.getState().registerRules('node1', 'handleA', mockRules);
|
||||
const rules = useFlowStore.getState().getTargetRules('node1', 'handleA');
|
||||
|
||||
expect(rules).toEqual(mockRules);
|
||||
});
|
||||
|
||||
it('should warn and return empty array if rules are missing', () => {
|
||||
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
const rules = useFlowStore.getState().getTargetRules('missingNode', 'missingHandle');
|
||||
expect(rules).toEqual([]);
|
||||
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('No rules were registered'));
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should unregister a specific handle rule', () => {
|
||||
const mockRules: HandleRule[] = [() => ({ isSatisfied: true } as RuleResult)];
|
||||
useFlowStore.getState().registerRules('node1', 'handleA', mockRules);
|
||||
|
||||
useFlowStore.getState().unregisterHandleRules('node1', 'handleA');
|
||||
const rules = useFlowStore.getState().getTargetRules('node1', 'handleA');
|
||||
|
||||
expect(rules).toEqual([]);
|
||||
});
|
||||
|
||||
it('should unregister all rules for a node', () => {
|
||||
const mockRules: HandleRule[] = [() => ({ isSatisfied: true } as RuleResult)];
|
||||
useFlowStore.getState().registerRules('node1', 'handleA', mockRules);
|
||||
useFlowStore.getState().registerRules('node1', 'handleB', mockRules);
|
||||
useFlowStore.getState().registerRules('node2', 'handleC', mockRules);
|
||||
|
||||
useFlowStore.getState().unregisterNodeRules('node1');
|
||||
|
||||
expect(useFlowStore.getState().getTargetRules('node1', 'handleA')).toEqual([]);
|
||||
expect(useFlowStore.getState().getTargetRules('node1', 'handleB')).toEqual([]);
|
||||
expect(useFlowStore.getState().getTargetRules('node2', 'handleC')).toEqual(mockRules);
|
||||
});
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
@@ -95,7 +95,11 @@ describe("Drag & drop node creation", () => {
|
||||
const node = nodes[0];
|
||||
|
||||
expect(node.type).toBe("phase");
|
||||
expect(node.id).toBe("phase-1");
|
||||
|
||||
// UUID Expression
|
||||
expect(node.id).toMatch(
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
||||
);
|
||||
|
||||
// screenToFlowPosition was mocked to subtract 100
|
||||
expect(node.position).toEqual({
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import { useState } from 'react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { renderWithProviders, screen } from '../../../../test-utils/test-utils.tsx';
|
||||
import GestureValueEditor from '../../../../../src/pages/VisProgPage/visualProgrammingUI/components/GestureValueEditor';
|
||||
|
||||
function TestHarness({ initialValue = '', initialType=true, placeholder = 'Gesture name' } : { initialValue?: string, initialType?: boolean, placeholder?: string }) {
|
||||
const [value, setValue] = useState(initialValue);
|
||||
const [_, setType] = useState(initialType)
|
||||
return (
|
||||
<GestureValueEditor value={value} setValue={setValue} setType={setType} placeholder={placeholder} />
|
||||
);
|
||||
}
|
||||
|
||||
describe('GestureValueEditor', () => {
|
||||
let user: ReturnType<typeof userEvent.setup>;
|
||||
|
||||
beforeEach(() => {
|
||||
user = userEvent.setup();
|
||||
});
|
||||
|
||||
test('renders in tag mode by default and allows selecting a tag via button and select', async () => {
|
||||
renderWithProviders(<TestHarness />);
|
||||
|
||||
// Tag selector should be present
|
||||
const select = screen.getByTestId('tagSelectorTestID') as HTMLSelectElement;
|
||||
expect(select).toBeInTheDocument();
|
||||
expect(select.value).toBe('');
|
||||
|
||||
// Choose a tag via select
|
||||
await user.selectOptions(select, 'happy');
|
||||
expect(select.value).toBe('happy');
|
||||
|
||||
// The corresponding tag button should reflect the selection (have the selected class)
|
||||
const happyButton = screen.getByRole('button', { name: /happy/i });
|
||||
expect(happyButton).toBeInTheDocument();
|
||||
expect(happyButton.className).toMatch(/selected/);
|
||||
});
|
||||
|
||||
test('switches to single mode and shows suggestions list', async () => {
|
||||
renderWithProviders(<TestHarness initialValue={'happy'} />);
|
||||
|
||||
const singleButton = screen.getByRole('button', { name: /^single$/i });
|
||||
await user.click(singleButton);
|
||||
|
||||
// Input should be present with placeholder
|
||||
const input = screen.getByPlaceholderText('Gesture name') as HTMLInputElement;
|
||||
expect(input).toBeInTheDocument();
|
||||
|
||||
// Because switching to single populates suggestions, we expect at least one suggestion item
|
||||
const suggestion = await screen.findByText(/Listening_1/);
|
||||
expect(suggestion).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('typing filters suggestions and selecting a suggestion commits the value and hides the list', async () => {
|
||||
renderWithProviders(<TestHarness />);
|
||||
|
||||
// Switch to single mode
|
||||
await user.click(screen.getByRole('button', { name: /^single$/i }));
|
||||
const input = screen.getByPlaceholderText('Gesture name') as HTMLInputElement;
|
||||
|
||||
// Type a substring that matches some suggestions
|
||||
await user.type(input, 'Listening_2');
|
||||
|
||||
// The suggestion should appear and include the text we typed
|
||||
const matching = await screen.findByText(/Listening_2/);
|
||||
expect(matching).toBeInTheDocument();
|
||||
|
||||
// Click the suggestion
|
||||
await user.click(matching);
|
||||
|
||||
// After selecting, input should contain that suggestion and suggestions should be hidden
|
||||
expect(input.value).toContain('Listening_2');
|
||||
expect(screen.queryByText(/Listening_1/)).toBeNull();
|
||||
});
|
||||
|
||||
test('typing a non-matching string hides the suggestions list', async () => {
|
||||
renderWithProviders(<TestHarness />);
|
||||
await user.click(screen.getByRole('button', { name: /^single$/i }));
|
||||
const input = screen.getByPlaceholderText('Gesture name') as HTMLInputElement;
|
||||
|
||||
await user.type(input, 'no-match-zzz');
|
||||
|
||||
// There should be no suggestion that includes that gibberish
|
||||
expect(screen.queryByText(/no-match-zzz/)).toBeNull();
|
||||
});
|
||||
|
||||
test('switching back to tag mode clears value when it is not a valid tag and preserves it when it is', async () => {
|
||||
renderWithProviders(<TestHarness />);
|
||||
|
||||
// Switch to single mode and pick a suggestion (which is not a semantic tag)
|
||||
await user.click(screen.getByRole('button', { name: /^single$/i }));
|
||||
const input = screen.getByPlaceholderText('Gesture name') as HTMLInputElement;
|
||||
await user.type(input, 'Listening_3');
|
||||
const suggestion = await screen.findByText(/Listening_3/);
|
||||
await user.click(suggestion);
|
||||
|
||||
// Switch back to tag mode -> value should be cleared (not in tag list)
|
||||
await user.click(screen.getByRole('button', { name: /^tag$/i }));
|
||||
const select = screen.getByTestId('tagSelectorTestID') as HTMLSelectElement;
|
||||
expect(select.value).toBe('');
|
||||
|
||||
// Now pick a valid tag and switch to single then back to tag
|
||||
await user.selectOptions(select, 'happy');
|
||||
expect(select.value).toBe('happy');
|
||||
|
||||
// Switch to single and then back to tag; since 'happy' is a valid tag, it should remain
|
||||
await user.click(screen.getByRole('button', { name: /^single$/i }));
|
||||
await user.click(screen.getByRole('button', { name: /^tag$/i }));
|
||||
expect(select.value).toBe('happy');
|
||||
});
|
||||
|
||||
test('focus on input re-shows filtered suggestions when customValue is present', async () => {
|
||||
renderWithProviders(<TestHarness />);
|
||||
|
||||
// Switch to single mode and type to filter
|
||||
await user.click(screen.getByRole('button', { name: /^single$/i }));
|
||||
const input = screen.getByPlaceholderText('Gesture name') as HTMLInputElement;
|
||||
|
||||
await user.type(input, 'Listening_4');
|
||||
const found = await screen.findByText(/Listening_4/);
|
||||
expect(found).toBeInTheDocument();
|
||||
|
||||
// Blur the input
|
||||
input.blur();
|
||||
expect(found).toBeInTheDocument();
|
||||
|
||||
// Focus the input again and ensure the suggestions remain or reappear
|
||||
await user.click(input);
|
||||
const foundAgain = await screen.findByText(/Listening_4/);
|
||||
expect(foundAgain).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,505 @@
|
||||
// PlanEditorDialog.test.tsx
|
||||
import { describe, it, beforeEach, jest } from '@jest/globals';
|
||||
import { screen, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { renderWithProviders } from '../../../../test-utils/test-utils.tsx';
|
||||
import PlanEditorDialog from '../../../../../src/pages/VisProgPage/visualProgrammingUI/components/PlanEditor';
|
||||
import { PlanReduce, type Plan } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/components/Plan';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
// Mock structuredClone
|
||||
(globalThis as any).structuredClone = jest.fn((val) => JSON.parse(JSON.stringify(val)));
|
||||
|
||||
// UUID Regex for checking ID's
|
||||
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
|
||||
describe('PlanEditorDialog', () => {
|
||||
let user: ReturnType<typeof userEvent.setup>;
|
||||
const mockOnSave = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
user = userEvent.setup();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const defaultPlan: Plan = {
|
||||
id: 'plan-1',
|
||||
name: 'Test Plan',
|
||||
steps: [],
|
||||
};
|
||||
|
||||
const extendedPlan: Plan = {
|
||||
id: 'extended-plan-1',
|
||||
name: 'extended test plan',
|
||||
steps: [
|
||||
// Step 1: A wave tag gesture
|
||||
{
|
||||
id: 'firststep',
|
||||
type: 'gesture',
|
||||
isTag: true,
|
||||
gesture: "hello"
|
||||
},
|
||||
|
||||
// Step 2: A single tag gesture
|
||||
{
|
||||
id: 'secondstep',
|
||||
type: 'gesture',
|
||||
isTag: false,
|
||||
gesture: "somefolder/somegesture"
|
||||
},
|
||||
|
||||
// Step 3: A LLM action
|
||||
{
|
||||
id: 'thirdstep',
|
||||
type: 'llm',
|
||||
goal: 'ask the user something or whatever'
|
||||
},
|
||||
|
||||
// Step 4: A speech action
|
||||
{
|
||||
id: 'fourthstep',
|
||||
type: 'speech',
|
||||
text: "I'm a cyborg ninja :>"
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const planWithSteps: Plan = {
|
||||
id: 'plan-2',
|
||||
name: 'Existing Plan',
|
||||
steps: [
|
||||
{ id: 'step-1', text: 'Hello world', type: 'speech' as const },
|
||||
{ id: 'step-2', gesture: 'Wave', isTag:true, type: 'gesture' as const },
|
||||
],
|
||||
};
|
||||
|
||||
const renderDialog = (props: Partial<React.ComponentProps<typeof PlanEditorDialog>> = {}) => {
|
||||
const defaultProps = {
|
||||
plan: undefined,
|
||||
onSave: mockOnSave,
|
||||
description: undefined,
|
||||
};
|
||||
|
||||
return renderWithProviders(<PlanEditorDialog {...defaultProps} {...props} />);
|
||||
};
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should show "Create Plan" button when no plan is provided', () => {
|
||||
renderDialog();
|
||||
// The button should be visible
|
||||
expect(screen.getByRole('button', { name: 'Create Plan' })).toBeInTheDocument();
|
||||
// The dialog content should NOT be visible initially
|
||||
expect(screen.queryByText(/Add Action/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show "Edit Plan" button when a plan is provided', () => {
|
||||
renderDialog({ plan: defaultPlan });
|
||||
expect(screen.getByRole('button', { name: 'Edit Plan' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not show "Create Plan" button when a plan exists', () => {
|
||||
renderDialog({ plan: defaultPlan });
|
||||
// Query for the button text specifically, not dialog title
|
||||
expect(screen.queryByRole('button', { name: 'Create Plan' })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dialog Interactions', () => {
|
||||
it('should open dialog with "Create Plan" title when creating new plan', async () => {
|
||||
renderDialog();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Create Plan' }));
|
||||
|
||||
|
||||
// One for button, one for dialog.
|
||||
expect(screen.getAllByText('Create Plan').length).toEqual(2);
|
||||
});
|
||||
|
||||
it('should open dialog with "Edit Plan" title when editing existing plan', async () => {
|
||||
renderDialog({ plan: defaultPlan });
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Edit Plan' }));
|
||||
|
||||
// One for button, one for dialog
|
||||
expect(screen.getAllByText('Edit Plan').length).toEqual(2);
|
||||
});
|
||||
|
||||
it('should pre-fill plan name when editing', async () => {
|
||||
renderDialog({ plan: defaultPlan });
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Edit Plan' }));
|
||||
|
||||
const nameInput = screen.getByPlaceholderText('Plan name') as HTMLInputElement;
|
||||
expect(nameInput.value).toBe(defaultPlan.name);
|
||||
});
|
||||
|
||||
it('should close dialog when cancel button is clicked', async () => {
|
||||
renderDialog();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Create Plan' }));
|
||||
await user.click(screen.getByText('Cancel'));
|
||||
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Plan Creation', () => {
|
||||
it('should create a new plan with default values', async () => {
|
||||
renderDialog();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Create Plan' }));
|
||||
|
||||
// One for the button, one for the dialog
|
||||
expect(screen.getAllByText('Create Plan').length).toEqual(2);
|
||||
|
||||
const nameInput = screen.getByPlaceholderText('Plan name');
|
||||
expect(nameInput).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should auto-fill with description when provided', async () => {
|
||||
const description = 'Achieve world peace';
|
||||
renderDialog({ description });
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Create Plan' }));
|
||||
|
||||
// Check if plan name is pre-filled with description
|
||||
const nameInput = screen.getByPlaceholderText('Plan name') as HTMLInputElement;
|
||||
expect(nameInput.value).toBe(description);
|
||||
|
||||
// Check if action type is set to LLM
|
||||
const actionTypeSelect = screen.getByLabelText(/Action Type/i) as HTMLSelectElement;
|
||||
expect(actionTypeSelect.value).toBe('llm');
|
||||
|
||||
// Check if suggestion text is shown
|
||||
expect(screen.getByText('Filled in as a suggestion!')).toBeInTheDocument();
|
||||
expect(screen.getByText('Feel free to change!')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should allow changing plan name', async () => {
|
||||
renderDialog();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Create Plan' }));
|
||||
|
||||
const nameInput = screen.getByPlaceholderText('Plan name') as HTMLInputElement;
|
||||
const newName = 'My Custom Plan';
|
||||
|
||||
// Instead of clear(), select all text and type new value
|
||||
await user.click(nameInput);
|
||||
await user.keyboard('{Control>}a{/Control}'); // Select all (Ctrl+A)
|
||||
await user.keyboard(newName);
|
||||
|
||||
expect(nameInput.value).toBe(newName);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Action Management', () => {
|
||||
it('should add a speech action to the plan', async () => {
|
||||
renderDialog({ plan: defaultPlan });
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Edit Plan' }));
|
||||
|
||||
const actionTypeSelect = screen.getByLabelText(/Action Type/i);
|
||||
const actionValueInput = screen.getByPlaceholderText("Speech text")
|
||||
const addButton = screen.getByText('Add Step');
|
||||
|
||||
// Set up a speech action
|
||||
await user.selectOptions(actionTypeSelect, 'speech');
|
||||
await user.type(actionValueInput, 'Hello there!');
|
||||
|
||||
await user.click(addButton);
|
||||
|
||||
// Check if step was added
|
||||
expect(screen.getByText('speech:')).toBeInTheDocument();
|
||||
expect(screen.getByText('Hello there!')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should add a gesture action to the plan', async () => {
|
||||
renderDialog({ plan: defaultPlan });
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /edit plan/i }));
|
||||
|
||||
const actionTypeSelect = screen.getByLabelText(/Action Type/i);
|
||||
const addButton = screen.getByText('Add Step');
|
||||
|
||||
// Set up a gesture action
|
||||
await user.selectOptions(actionTypeSelect, 'gesture');
|
||||
|
||||
// Find the input field after type change
|
||||
const select = screen.getByTestId("tagSelectorTestID")
|
||||
const options = within(select).getAllByRole('option')
|
||||
|
||||
await user.selectOptions(select, options[1])
|
||||
await user.click(addButton);
|
||||
|
||||
// Check if step was added
|
||||
expect(screen.getByText('gesture:')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should add an LLM action to the plan', async () => {
|
||||
renderDialog({ plan: defaultPlan });
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Edit Plan' }));
|
||||
|
||||
const actionTypeSelect = screen.getByLabelText(/Action Type/i);
|
||||
const addButton = screen.getByText('Add Step');
|
||||
|
||||
// Set up an LLM action
|
||||
await user.selectOptions(actionTypeSelect, 'llm');
|
||||
|
||||
// Find the input field after type change
|
||||
const llmInput = screen.getByPlaceholderText(/LLM goal|text/i);
|
||||
await user.type(llmInput, 'Generate a story');
|
||||
|
||||
await user.click(addButton);
|
||||
|
||||
// Check if step was added
|
||||
expect(screen.getByText('llm:')).toBeInTheDocument();
|
||||
expect(screen.getByText('Generate a story')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should disable "Add Step" button when action value is empty', async () => {
|
||||
renderDialog({ plan: defaultPlan });
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Edit Plan' }));
|
||||
|
||||
const addButton = screen.getByText('Add Step');
|
||||
expect(addButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it('should reset action form after adding a step', async () => {
|
||||
renderDialog({ plan: defaultPlan });
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Edit Plan' }));
|
||||
|
||||
|
||||
const actionValueInput = screen.getByPlaceholderText("Speech text")
|
||||
const addButton = screen.getByText('Add Step');
|
||||
|
||||
await user.type(actionValueInput, 'Test speech');
|
||||
await user.click(addButton);
|
||||
|
||||
// Action value should be cleared
|
||||
expect(actionValueInput).toHaveValue('');
|
||||
// Action type should be reset to speech (default)
|
||||
const actionTypeSelect = screen.getByLabelText(/Action Type/i) as HTMLSelectElement;
|
||||
expect(actionTypeSelect.value).toBe('speech');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Step Management', () => {
|
||||
it('should show existing steps when editing a plan', async () => {
|
||||
renderDialog({ plan: planWithSteps });
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Edit Plan' }));
|
||||
|
||||
// Check if existing steps are shown
|
||||
expect(screen.getByText('speech:')).toBeInTheDocument();
|
||||
expect(screen.getByText('Hello world')).toBeInTheDocument();
|
||||
expect(screen.getByText('gesture:')).toBeInTheDocument();
|
||||
expect(screen.getByText('Wave')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show "No steps yet" message when plan has no steps', async () => {
|
||||
renderDialog({ plan: defaultPlan });
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Edit Plan' }));
|
||||
|
||||
expect(screen.getByText('No steps yet')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should remove a step when clicked', async () => {
|
||||
renderDialog({ plan: planWithSteps });
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Edit Plan' }));
|
||||
|
||||
// Initially have 2 steps
|
||||
expect(screen.getByText('speech:')).toBeInTheDocument();
|
||||
expect(screen.getByText('gesture:')).toBeInTheDocument();
|
||||
|
||||
// Click on the first step to remove it
|
||||
await user.click(screen.getByText('Hello world'));
|
||||
|
||||
// First step should be removed
|
||||
expect(screen.queryByText('Hello world')).not.toBeInTheDocument();
|
||||
// Second step should still exist
|
||||
expect(screen.getByText('Wave')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Save Functionality', () => {
|
||||
it('should call onSave with new plan when creating', async () => {
|
||||
renderDialog();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Create Plan' }));
|
||||
|
||||
// Set plan name
|
||||
const nameInput = screen.getByPlaceholderText('Plan name') as HTMLInputElement;
|
||||
await user.click(nameInput);
|
||||
await user.keyboard('{Control>}a{/Control}');
|
||||
await user.keyboard('My New Plan');
|
||||
|
||||
// Add a step
|
||||
const actionValueInput = screen.getByPlaceholderText(/text/i);
|
||||
await user.type(actionValueInput, 'First step');
|
||||
await user.click(screen.getByText('Add Step'));
|
||||
|
||||
// Save the plan
|
||||
await user.click(screen.getByText('Create'));
|
||||
|
||||
expect(mockOnSave).toHaveBeenCalledWith({
|
||||
id: expect.stringMatching(uuidRegex),
|
||||
name: 'My New Plan',
|
||||
steps: [
|
||||
{
|
||||
id: expect.stringMatching(uuidRegex),
|
||||
text: 'First step',
|
||||
type: 'speech',
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call onSave with updated plan when editing', async () => {
|
||||
renderDialog({ plan: defaultPlan });
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Edit Plan' }));
|
||||
|
||||
// Change plan name
|
||||
const nameInput = screen.getByPlaceholderText('Plan name') as HTMLInputElement;
|
||||
await user.click(nameInput);
|
||||
await user.keyboard('{Control>}a{/Control}');
|
||||
await user.keyboard('Updated Plan Name');
|
||||
|
||||
// Add a step
|
||||
const actionValueInput = screen.getByPlaceholderText(/text/i);
|
||||
await user.type(actionValueInput, 'New speech action');
|
||||
await user.click(screen.getByText('Add Step'));
|
||||
|
||||
// Save the plan
|
||||
await user.click(screen.getByText('Confirm'));
|
||||
|
||||
expect(mockOnSave).toHaveBeenCalledWith({
|
||||
id: defaultPlan.id,
|
||||
name: 'Updated Plan Name',
|
||||
steps: [
|
||||
{
|
||||
id: expect.stringMatching(uuidRegex),
|
||||
text: 'New speech action',
|
||||
type: 'speech',
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call onSave with undefined when reset button is clicked', async () => {
|
||||
renderDialog({ plan: defaultPlan });
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Edit Plan' }));
|
||||
await user.click(screen.getByText('Reset'));
|
||||
|
||||
expect(mockOnSave).toHaveBeenCalledWith(undefined);
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should disable save button when no draft plan exists', async () => {
|
||||
renderDialog();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Create Plan' }));
|
||||
|
||||
// The save button should be enabled since draftPlan exists after clicking Create Plan
|
||||
const saveButton = screen.getByText('Create');
|
||||
expect(saveButton).not.toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Step Indexing', () => {
|
||||
it('should show correct step numbers', async () => {
|
||||
renderDialog({ plan: defaultPlan });
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Edit Plan' }));
|
||||
|
||||
// Add multiple steps
|
||||
const actionValueInput = screen.getByPlaceholderText(/text/i);
|
||||
const addButton = screen.getByText('Add Step');
|
||||
|
||||
await user.type(actionValueInput, 'First');
|
||||
await user.click(addButton);
|
||||
|
||||
await user.type(actionValueInput, 'Second');
|
||||
await user.click(addButton);
|
||||
|
||||
await user.type(actionValueInput, 'Third');
|
||||
await user.click(addButton);
|
||||
|
||||
// Check step numbers
|
||||
expect(screen.getByText('1.')).toBeInTheDocument();
|
||||
expect(screen.getByText('2.')).toBeInTheDocument();
|
||||
expect(screen.getByText('3.')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Action Type Switching', () => {
|
||||
it('should update placeholder text when action type changes', async () => {
|
||||
renderDialog();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Create Plan' }));
|
||||
|
||||
const actionTypeSelect = screen.getByLabelText(/Action Type/i);
|
||||
|
||||
// Check speech placeholder
|
||||
await user.selectOptions(actionTypeSelect, 'speech');
|
||||
// The placeholder might be set dynamically, so we need to check the input
|
||||
const speechInput = screen.getByPlaceholderText(/text/i);
|
||||
expect(speechInput).toBeInTheDocument();
|
||||
|
||||
// Check gesture placeholder
|
||||
await user.selectOptions(actionTypeSelect, 'gesture');
|
||||
const gestureInput = screen.getByTestId("valueEditorTestID")
|
||||
expect(gestureInput).toBeInTheDocument();
|
||||
|
||||
// Check LLM placeholder
|
||||
await user.selectOptions(actionTypeSelect, 'llm');
|
||||
const llmInput = screen.getByPlaceholderText(/LLM|text/i);
|
||||
expect(llmInput).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Plan reducing', () => {
|
||||
it('should correctly reduce the plan given the elements of the plan', () => {
|
||||
const testplan = extendedPlan
|
||||
const expectedResult = {
|
||||
id: "extended-plan-1",
|
||||
steps: [
|
||||
{
|
||||
id: "firststep",
|
||||
gesture: {
|
||||
type: "tag",
|
||||
name: "hello"
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "secondstep",
|
||||
gesture: {
|
||||
type: "single",
|
||||
name: "somefolder/somegesture"
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "thirdstep",
|
||||
goal: "ask the user something or whatever"
|
||||
},
|
||||
{
|
||||
id: "fourthstep",
|
||||
text: "I'm a cyborg ninja :>"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const actualResult = PlanReduce(testplan)
|
||||
|
||||
expect(actualResult).toEqual(expectedResult)
|
||||
});
|
||||
})
|
||||
});
|
||||
@@ -14,7 +14,6 @@ describe('BasicBeliefNode', () => {
|
||||
beforeEach(() => {
|
||||
user = userEvent.setup();
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render the basic belief node with keyword type by default', () => {
|
||||
const mockNode: Node<BasicBeliefNodeData> = {
|
||||
@@ -59,7 +58,7 @@ describe('BasicBeliefNode', () => {
|
||||
data: {
|
||||
label: 'Belief',
|
||||
droppable: true,
|
||||
belief: { type: 'semantic', id: 'test', value: 'test value', label: 'Detected with LLM:' },
|
||||
belief: { type: 'semantic', id: 'test', value: 'test value', description: "test description", label: 'Detected with LLM:' },
|
||||
hasReduce: true,
|
||||
},
|
||||
};
|
||||
@@ -333,7 +332,7 @@ describe('BasicBeliefNode', () => {
|
||||
data: {
|
||||
label: 'Belief',
|
||||
droppable: true,
|
||||
belief: { type: 'semantic', id: 'sem1', value: 'initial', label: 'Detected with LLM:' },
|
||||
belief: { type: 'semantic', id: 'test', value: 'test value', description: "test description", label: 'Detected with LLM:' },
|
||||
hasReduce: true,
|
||||
},
|
||||
};
|
||||
@@ -360,10 +359,10 @@ describe('BasicBeliefNode', () => {
|
||||
/>
|
||||
);
|
||||
|
||||
const input = screen.getByDisplayValue('initial') as HTMLInputElement;
|
||||
const input = screen.getByDisplayValue('test value') as HTMLInputElement;
|
||||
|
||||
// Clear the input
|
||||
for (let i = 0; i < 'initial'.length; i++) {
|
||||
for (let i = 0; i < 'test value'.length; i++) {
|
||||
await user.type(input, '{backspace}');
|
||||
}
|
||||
await user.type(input, 'new semantic value{enter}');
|
||||
@@ -689,7 +688,7 @@ describe('BasicBeliefNode', () => {
|
||||
data: {
|
||||
label: 'Belief',
|
||||
droppable: true,
|
||||
belief: { type: 'semantic', id: 'sem1', value: 'initial', label: 'Detected with LLM:' },
|
||||
belief: { type: 'semantic', id: 'test', value: 'test value', description: "test description", label: 'Detected with LLM:' },
|
||||
hasReduce: true,
|
||||
},
|
||||
};
|
||||
@@ -716,27 +715,27 @@ describe('BasicBeliefNode', () => {
|
||||
/>
|
||||
);
|
||||
|
||||
const input = screen.getByDisplayValue('initial') as HTMLInputElement;
|
||||
const input = screen.getByDisplayValue('test value') 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');
|
||||
expect(nodeData.belief.value).toBe('test value');
|
||||
});
|
||||
|
||||
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');
|
||||
expect(nodeData.belief.value).toBe('test value');
|
||||
});
|
||||
|
||||
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');
|
||||
expect(nodeData.belief.value).toBe('test value12');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -404,6 +404,16 @@ describe('NormNode', () => {
|
||||
|
||||
describe('NormReduce Function', () => {
|
||||
it('should reduce a norm node to its essential data', () => {
|
||||
|
||||
const condition: Node = {
|
||||
id: "belief-1",
|
||||
type: 'basic_belief',
|
||||
position: {x: 10, y: 10},
|
||||
data: {
|
||||
...JSON.parse(JSON.stringify(BasicBeliefNodeDefaults))
|
||||
}
|
||||
}
|
||||
|
||||
const normNode: Node = {
|
||||
id: 'norm-1',
|
||||
type: 'norm',
|
||||
@@ -414,18 +424,21 @@ describe('NormNode', () => {
|
||||
droppable: true,
|
||||
norm: 'Never harm humans',
|
||||
hasReduce: true,
|
||||
condition: "belief-1"
|
||||
},
|
||||
};
|
||||
|
||||
const allNodes: Node[] = [normNode];
|
||||
const allNodes: Node[] = [normNode, condition];
|
||||
const result = NormReduce(normNode, allNodes);
|
||||
|
||||
expect(result).toEqual({
|
||||
id: 'norm-1',
|
||||
label: 'Safety Norm',
|
||||
norm: 'Never harm humans',
|
||||
critical: false,
|
||||
basic_beliefs: [],
|
||||
condition: {
|
||||
id: "belief-1",
|
||||
keyword: ""
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -917,17 +930,8 @@ describe('NormNode', () => {
|
||||
}
|
||||
};
|
||||
|
||||
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],
|
||||
nodes: [mockNode, mockBelief1],
|
||||
edges: [],
|
||||
});
|
||||
|
||||
@@ -938,16 +942,11 @@ describe('NormNode', () => {
|
||||
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"]);
|
||||
expect(updatedNorm?.data.condition).toEqual("basic_belief-1");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -78,8 +78,10 @@ describe('PhaseNode', () => {
|
||||
|
||||
// Find nodes
|
||||
const nodes = useFlowStore.getState().nodes;
|
||||
const p1 = nodes.find((x) => x.id === 'phase-1')!;
|
||||
const p2 = nodes.find((x) => x.id === 'phase-2')!;
|
||||
const phaseNodes = nodes.filter((x) => x.type === 'phase');
|
||||
const p1 = phaseNodes[0];
|
||||
const p2 = phaseNodes[1];
|
||||
|
||||
|
||||
// expect same value, not same reference
|
||||
expect(p1.data.children).not.toBe(p2.data.children);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { describe, it, beforeEach } from '@jest/globals';
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { renderWithProviders } from '../../../../test-utils/test-utils.tsx';
|
||||
import TriggerNode, {
|
||||
TriggerReduce,
|
||||
@@ -11,12 +10,15 @@ import TriggerNode, {
|
||||
import useFlowStore from '../../../../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores';
|
||||
import type { Node } from '@xyflow/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { TriggerNodeDefaults } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/TriggerNode.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 { NormNodeDefaults } from '../../../../../src/pages/VisProgPage/visualProgrammingUI/nodes/NormNode.default.ts';
|
||||
|
||||
describe('TriggerNode', () => {
|
||||
let user: ReturnType<typeof userEvent.setup>;
|
||||
|
||||
beforeEach(() => {
|
||||
user = userEvent.setup();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Rendering', () => {
|
||||
@@ -26,11 +28,7 @@ describe('TriggerNode', () => {
|
||||
type: 'trigger',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
label: 'Keyword Trigger',
|
||||
droppable: true,
|
||||
triggerType: 'keywords',
|
||||
triggers: [],
|
||||
hasReduce: true,
|
||||
...JSON.parse(JSON.stringify(TriggerNodeDefaults)),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -51,161 +49,58 @@ describe('TriggerNode', () => {
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText(/Triggers when the keyword is spoken/i)).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText('...')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render TriggerNode with emotion type', () => {
|
||||
const mockNode: Node<TriggerNodeData> = {
|
||||
id: 'trigger-2',
|
||||
type: 'trigger',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
label: 'Emotion Trigger',
|
||||
droppable: true,
|
||||
triggerType: 'emotion',
|
||||
triggers: [],
|
||||
hasReduce: true,
|
||||
},
|
||||
};
|
||||
|
||||
renderWithProviders(
|
||||
<TriggerNode
|
||||
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(/Emotion\?/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('User Interactions', () => {
|
||||
it('should add a new keyword', async () => {
|
||||
const mockNode: Node<TriggerNodeData> = {
|
||||
id: 'trigger-1',
|
||||
type: 'trigger',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
label: 'Keyword Trigger',
|
||||
droppable: true,
|
||||
triggerType: 'keywords',
|
||||
triggers: [],
|
||||
hasReduce: true,
|
||||
},
|
||||
};
|
||||
|
||||
useFlowStore.setState({ nodes: [mockNode], edges: [] });
|
||||
|
||||
renderWithProviders(
|
||||
<TriggerNode
|
||||
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('...');
|
||||
await user.type(input, 'hello{enter}');
|
||||
|
||||
await waitFor(() => {
|
||||
const node = useFlowStore.getState().nodes.find(n => n.id === 'trigger-1') as Node<TriggerNodeData> | undefined;
|
||||
expect(node?.data.triggers.length).toBe(1);
|
||||
expect(node?.data.triggers[0].keyword).toBe('hello');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('should remove a keyword when cleared', async () => {
|
||||
const mockNode: Node<TriggerNodeData> = {
|
||||
id: 'trigger-1',
|
||||
type: 'trigger',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
label: 'Keyword Trigger',
|
||||
droppable: true,
|
||||
triggerType: 'keywords',
|
||||
triggers: [{ id: 'kw1', keyword: 'hello' }],
|
||||
hasReduce: true,
|
||||
},
|
||||
};
|
||||
|
||||
useFlowStore.setState({ nodes: [mockNode], edges: [] });
|
||||
|
||||
renderWithProviders(
|
||||
<TriggerNode
|
||||
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('hello');
|
||||
for (let i = 0; i < 'hello'.length; i++) {
|
||||
await user.type(input, '{backspace}');
|
||||
}
|
||||
await user.type(input, '{enter}');
|
||||
|
||||
await waitFor(() => {
|
||||
const node = useFlowStore.getState().nodes.find(n => n.id === 'trigger-1') as Node<TriggerNodeData> | undefined;
|
||||
expect(node?.data.triggers.length).toBe(0);
|
||||
});
|
||||
|
||||
expect(screen.getByText(/Triggers when the condition is met/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Belief is currently/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Plan is currently/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('TriggerReduce Function', () => {
|
||||
it('should reduce a trigger node to its essential data', () => {
|
||||
const conditionNode: Node = {
|
||||
id: 'belief-1',
|
||||
type: 'basic_belief',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
...JSON.parse(JSON.stringify(BasicBeliefNodeDefaults)),
|
||||
},
|
||||
};
|
||||
|
||||
const triggerNode: Node = {
|
||||
id: 'trigger-1',
|
||||
type: 'trigger',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
label: 'Keyword Trigger',
|
||||
droppable: true,
|
||||
triggerType: 'keywords',
|
||||
triggers: [{ id: 'kw1', keyword: 'hello' }],
|
||||
hasReduce: true,
|
||||
...JSON.parse(JSON.stringify(TriggerNodeDefaults)),
|
||||
condition: "belief-1",
|
||||
plan: defaultPlan
|
||||
},
|
||||
};
|
||||
|
||||
const allNodes: Node[] = [triggerNode];
|
||||
const result = TriggerReduce(triggerNode, allNodes);
|
||||
useFlowStore.setState({
|
||||
nodes: [conditionNode, triggerNode],
|
||||
edges: [],
|
||||
});
|
||||
|
||||
useFlowStore.getState().onConnect({
|
||||
source: 'belief-1',
|
||||
target: 'trigger-1',
|
||||
sourceHandle: null,
|
||||
targetHandle: null,
|
||||
});
|
||||
|
||||
const result = TriggerReduce(triggerNode, useFlowStore.getState().nodes);
|
||||
|
||||
expect(result).toEqual({
|
||||
id: 'trigger-1',
|
||||
type: 'keywords',
|
||||
label: 'Keyword Trigger',
|
||||
keywords: [{ id: 'kw1', keyword: 'hello' }],
|
||||
});
|
||||
condition: {
|
||||
id: "belief-1",
|
||||
keyword: "",
|
||||
},
|
||||
plan: {
|
||||
id: expect.anything(),
|
||||
steps: [],
|
||||
},});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -217,11 +112,8 @@ describe('TriggerNode', () => {
|
||||
type: 'trigger',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
...JSON.parse(JSON.stringify(TriggerNodeDefaults)),
|
||||
label: 'Trigger 1',
|
||||
droppable: true,
|
||||
triggerType: 'keywords',
|
||||
triggers: [],
|
||||
hasReduce: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -230,10 +122,8 @@ describe('TriggerNode', () => {
|
||||
type: 'norm',
|
||||
position: { x: 100, y: 0 },
|
||||
data: {
|
||||
...JSON.parse(JSON.stringify(NormNodeDefaults)),
|
||||
label: 'Norm 1',
|
||||
droppable: true,
|
||||
norm: 'test',
|
||||
hasReduce: true,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -185,7 +185,7 @@ describe('Universal Nodes', () => {
|
||||
// Verify the correct structure is present using NodesInPhase
|
||||
expect(result).toHaveLength(nodeType !== 'phase' ? 1 : 2);
|
||||
expect(result[0]).toHaveProperty('id', 'phase-1');
|
||||
expect(result[0]).toHaveProperty('label', 'Test Phase');
|
||||
expect(result[0]).toHaveProperty('name', 'Test Phase');
|
||||
|
||||
// Restore mocks
|
||||
phaseReduceSpy.mockRestore();
|
||||
|
||||
@@ -66,6 +66,7 @@ export const mockReactFlow = () => {
|
||||
width: 200,
|
||||
height: 200,
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -77,7 +78,8 @@ beforeAll(() => {
|
||||
past: [],
|
||||
future: [],
|
||||
isBatchAction: false,
|
||||
edgeReconnectSuccessful: true
|
||||
edgeReconnectSuccessful: true,
|
||||
ruleRegistry: new Map()
|
||||
});
|
||||
});
|
||||
|
||||
@@ -89,7 +91,21 @@ afterEach(() => {
|
||||
past: [],
|
||||
future: [],
|
||||
isBatchAction: false,
|
||||
edgeReconnectSuccessful: true
|
||||
edgeReconnectSuccessful: true,
|
||||
ruleRegistry: new Map()
|
||||
});
|
||||
});
|
||||
|
||||
if (typeof HTMLDialogElement !== 'undefined') {
|
||||
if (!HTMLDialogElement.prototype.showModal) {
|
||||
HTMLDialogElement.prototype.showModal = function () {
|
||||
// basic behavior: mark as open
|
||||
this.setAttribute('open', '');
|
||||
};
|
||||
}
|
||||
if (!HTMLDialogElement.prototype.close) {
|
||||
HTMLDialogElement.prototype.close = function () {
|
||||
this.removeAttribute('open');
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user