505 lines
17 KiB
TypeScript
505 lines
17 KiB
TypeScript
// 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) // TODO: FIX THIS TEST :)))
|
|
|
|
expect(actualResult).toEqual(expectedResult)
|
|
});
|
|
})
|
|
}); |