// 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 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; const mockOnSave = jest.fn(); beforeEach(() => { user = userEvent.setup(); jest.clearAllMocks(); }); const defaultPlan: Plan = { id: 'plan-1', name: 'Test Plan', steps: [], }; 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', type: 'gesture' as const }, ], }; const renderDialog = (props: Partial> = {}) => { const defaultProps = { plan: undefined, onSave: mockOnSave, description: undefined, }; return renderWithProviders(); }; 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(); }); }); });