import React from 'react'; import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; import userEvent from '@testing-library/user-event'; import { type FetchResponse, getDefaultsFromConfigSchema, openmrsFetch, showSnackbar, useConfig, } from '@openmrs/esm-framework'; import { screen } from '@testing-library/react'; import { mockQueueAltPriorities, mockQueueEntryAlice, mockQueueEntryBrian, mockPreviousQueueEntryAlice, mockQueueEntryNoPriorities, mockQueueEntryNoStatuses, mockQueues, mockQueuesForComboBox, mockQueueNoPriorities, mockQueueNoStatuses, } from '__mocks__'; import { renderWithSwr } from 'tools'; import { type ConfigObject, configSchema } from '../config-schema'; import DeleteQueueEntryModal from './delete-queue-entry.modal'; import QueueEntryActionModal from './queue-entry-actions-modal.component'; import UndoTransitionQueueEntryModal from './undo-transition-queue-entry.modal'; const mockOpenmrsFetch = vi.mocked(openmrsFetch); const mockUseConfig = vi.mocked(useConfig); const mockUseQueues = vi.fn().mockReturnValue({ queues: mockQueues }); vi.mock('../hooks/useQueues', () => ({ useQueues: () => mockUseQueues(), })); vi.mock('../create-queue-entry/hooks/useQueueLocations', () => ({ useQueueLocations: vi.fn(() => ({ queueLocations: [], isLoading: false, error: undefined })), })); vi.mock('../hooks/useQueueEntries', () => { return { useMutateQueueEntries: vi.fn().mockReturnValue({ mutateQueueEntries: vi.fn(), }), }; }); beforeEach(() => { mockUseConfig.mockReturnValue({ ...getDefaultsFromConfigSchema(configSchema), showQueueNumber: true, showPriorityComment: true, showTransitionDateTime: true, priorityConfigs: [ { conceptUuid: 'f4620bfa-3625-4883-bd3f-84c2cce14470', style: null, color: 'green', }, { conceptUuid: 'dc3492ef-24a5-4fd9-b58d-4fd2acf7071f', style: null, color: 'orange', }, ], concepts: { ...getDefaultsFromConfigSchema(configSchema).concepts, defaultPriorityConceptUuid: 'f4620bfa-3625-4883-bd3f-84c2cce14470', defaultStatusConceptUuid: '51ae5e4d-b72b-4912-bf31-a17efb690aeb', defaultTransitionStatus: 'ca7494ae-437f-4fd0-8aae-b88b9a2ba47d', }, } as ConfigObject); }); describe('UndoTransitionQueueEntryModal', () => { const queueEntry = mockQueueEntryAlice; it('has a cancel button that closes the modal', async () => { const closeModal = vi.fn(); const user = userEvent.setup(); renderWithSwr(); const cancelButton = screen.getByText('Cancel'); await user.click(cancelButton); expect(closeModal).toHaveBeenCalled(); }); it('has an working submit button', async () => { mockOpenmrsFetch.mockResolvedValue({ status: 200, data: [], } as unknown as FetchResponse); const user = userEvent.setup(); renderWithSwr( {}} />); const submitButton = screen.getByRole('button', { name: /Undo transition/ }); expect(submitButton).toBeEnabled(); await user.click(submitButton); expect(mockOpenmrsFetch).toHaveBeenCalled(); expect(showSnackbar).toHaveBeenCalled(); }); }); describe('VoidQueueEntryModal', () => { const queueEntry = mockQueueEntryAlice; it('has a cancel button that closes the modal', async () => { const closeModal = vi.fn(); const user = userEvent.setup(); renderWithSwr(); const cancelButton = screen.getByText('Cancel'); await user.click(cancelButton); expect(closeModal).toHaveBeenCalled(); }); it('has an working submit button', async () => { mockOpenmrsFetch.mockResolvedValue({ status: 200, data: [], } as unknown as FetchResponse); const user = userEvent.setup(); renderWithSwr( {}} />); const submitButton = screen.getByRole('button', { name: /Delete queue entry/ }); expect(submitButton).toBeEnabled(); await user.click(submitButton); expect(mockOpenmrsFetch).toHaveBeenCalled(); expect(showSnackbar).toHaveBeenCalled(); }); }); describe('QueueEntryActionModal', () => { const defaultProps = { queueEntry: mockQueueEntryAlice, closeModal: vi.fn(), modalParams: { modalTitle: 'Test Modal', modalInstruction: 'Test instruction', submitButtonText: 'Submit', submitSuccessTitle: 'Success', submitSuccessText: 'Operation completed', submitFailureTitle: 'Submission Failed', submitAction: vi.fn(), disableSubmit: vi.fn().mockReturnValue(false), isEdit: false, showQueuePicker: true, showStatusPicker: true, }, }; it('renders with correct content', () => { renderWithSwr(); expect(screen.getByText('Test instruction')).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Submit' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument(); }); it('closes modal and shows warning snackbar when submission fails with already-ended error', async () => { const mockSubmitAction = vi.fn().mockRejectedValue({ responseBody: { error: { message: 'Cannot transition a queue entry that has already ended', }, }, }); const closeModal = vi.fn(); const user = userEvent.setup(); renderWithSwr( , ); const submitButton = screen.getByRole('button', { name: 'Submit' }); await user.click(submitButton); expect(showSnackbar).toHaveBeenCalledWith( expect.objectContaining({ kind: 'warning', title: 'Queue entry is no longer active', }), ); expect(closeModal).toHaveBeenCalled(); }); it('shows inline error notification when submission fails with duplicate error', async () => { const mockSubmitAction = vi.fn().mockRejectedValue({ responseBody: { error: { message: '[queue.entry.duplicate.patient]', }, }, }); const user = userEvent.setup(); renderWithSwr( , ); const submitButton = screen.getByRole('button', { name: 'Submit' }); await user.click(submitButton); expect(await screen.findByText('This patient is already in the selected queue.')).toBeInTheDocument(); expect(screen.getByText('Patient already in queue')).toBeInTheDocument(); }); it('shows inline error notification when submission fails with generic error', async () => { const mockSubmitAction = vi.fn().mockRejectedValue({ message: 'Network error occurred', }); const user = userEvent.setup(); renderWithSwr( , ); const submitButton = screen.getByRole('button', { name: 'Submit' }); await user.click(submitButton); expect(await screen.findByText('Network error occurred')).toBeInTheDocument(); expect(screen.getByText('Submission Failed')).toBeInTheDocument(); }); it('clears error when user changes queue selection', async () => { const mockSubmitAction = vi.fn().mockRejectedValue({ message: 'Test error', }); const user = userEvent.setup(); renderWithSwr( , ); const submitButton = screen.getByRole('button', { name: 'Submit' }); await user.click(submitButton); expect(await screen.findByText('Test error')).toBeInTheDocument(); const firstQueueOption = screen.getByRole('radio', { name: 'Triage - Triage' }); await user.click(firstQueueOption); expect(screen.queryByText('Test error')).not.toBeInTheDocument(); }); it('clears error when user changes priority', async () => { const mockSubmitAction = vi.fn().mockRejectedValue({ message: 'Test error', }); const user = userEvent.setup(); renderWithSwr( , ); const submitButton = screen.getByRole('button', { name: 'Submit' }); await user.click(submitButton); expect(await screen.findByText('Test error')).toBeInTheDocument(); const priorityOption = screen.getByRole('radio', { name: 'Non urgent' }); await user.click(priorityOption); expect(screen.queryByText('Test error')).not.toBeInTheDocument(); }); it('clears error when user changes status', async () => { const mockSubmitAction = vi.fn().mockRejectedValue({ message: 'Test error', }); const user = userEvent.setup(); renderWithSwr( , ); const submitButton = screen.getByRole('button', { name: 'Submit' }); await user.click(submitButton); expect(await screen.findByText('Test error')).toBeInTheDocument(); const statusOption = screen.getByRole('radio', { name: 'Waiting' }); await user.click(statusOption); expect(screen.queryByText('Test error')).not.toBeInTheDocument(); }); it('submits form successfully without errors', async () => { const mockSubmitAction = vi.fn().mockResolvedValue({ status: 200 }); const closeModal = vi.fn(); const user = userEvent.setup(); renderWithSwr( , ); const submitButton = screen.getByRole('button', { name: 'Submit' }); await user.click(submitButton); expect(mockSubmitAction).toHaveBeenCalled(); expect(closeModal).toHaveBeenCalled(); }); it('closes modal when cancel button is clicked', async () => { const closeModal = vi.fn(); const user = userEvent.setup(); renderWithSwr(); const cancelButton = screen.getByRole('button', { name: 'Cancel' }); await user.click(cancelButton); expect(closeModal).toHaveBeenCalled(); }); it('allows user to close error notification', async () => { const mockSubmitAction = vi.fn().mockRejectedValue({ message: 'Test error', }); const user = userEvent.setup(); renderWithSwr( , ); const submitButton = screen.getByRole('button', { name: 'Submit' }); await user.click(submitButton); const errorNotification = await screen.findByText('Test error'); expect(errorNotification).toBeInTheDocument(); const closeButton = screen.getByRole('button', { name: 'close notification' }); await user.click(closeButton); expect(screen.queryByText('Test error')).not.toBeInTheDocument(); }); it('shows error when server returns non-200 status', async () => { const mockSubmitAction = vi.fn().mockResolvedValue({ status: 201 }); const user = userEvent.setup(); renderWithSwr( , ); await user.click(screen.getByRole('button', { name: 'Submit' })); expect(await screen.findByText('Unexpected Server Response')).toBeInTheDocument(); }); it('initializes transition date from queue entry when isEdit is true', async () => { const mockSubmitAction = vi.fn().mockResolvedValue({ status: 200 }); const user = userEvent.setup(); renderWithSwr( , ); const nowCheckbox = screen.getByRole('checkbox', { name: 'Now' }); await user.click(nowCheckbox); await user.click(screen.getByRole('button', { name: 'Submit' })); const [, formState] = mockSubmitAction.mock.calls[0]; expect(formState.transitionDate.getTime()).toBe(new Date(mockQueueEntryAlice.startedAt).getTime()); }); describe('empty configuration states', () => { afterEach(() => { mockUseQueues.mockReturnValue({ queues: mockQueues }); }); it('shows no-statuses configured notification when no statuses are configured', () => { mockUseQueues.mockReturnValue({ queues: [mockQueueNoStatuses] }); renderWithSwr(); expect(screen.getByText(/no status configured/i)).toBeInTheDocument(); }); it('shows no-priorities configured notification when no priorities are configured', () => { mockUseQueues.mockReturnValue({ queues: [mockQueueNoPriorities] }); renderWithSwr(); expect(screen.getByText(/no priorities configured/i)).toBeInTheDocument(); }); it('switching to a queue without current priority falls back to first allowed priority', async () => { mockUseQueues.mockReturnValue({ queues: [mockQueueAltPriorities, ...mockQueues] }); const user = userEvent.setup(); renderWithSwr(); const altQueueOption = screen.getByRole('radio', { name: /AltPriority/i }); await user.click(altQueueOption); expect(screen.getByRole('radio', { name: /Urgent/i })).toBeChecked(); }); }); it('passes updated comment to submitAction', async () => { const mockSubmitAction = vi.fn().mockResolvedValue({ status: 200 }); const user = userEvent.setup(); renderWithSwr( , ); const comment = screen.getByPlaceholderText('Enter comment here'); await user.clear(comment); await user.type(comment, 'New comment'); await user.click(screen.getByRole('button', { name: 'Submit' })); const [, formState] = mockSubmitAction.mock.calls[0]; expect(formState.priorityComment).toBe('New comment'); }); }); describe('QueueEntryActionModal - ComboBox behavior with many queues', () => { const defaultProps = { queueEntry: mockQueueEntryAlice, closeModal: vi.fn(), modalParams: { modalTitle: 'Test Modal', modalInstruction: 'Test instruction', submitButtonText: 'Submit', submitSuccessTitle: 'Success', submitSuccessText: 'Operation completed', submitFailureTitle: 'Submission Failed', submitAction: vi.fn().mockResolvedValue({ status: 200 }), disableSubmit: vi.fn().mockReturnValue(false), isEdit: false, showQueuePicker: true, showStatusPicker: false, }, }; beforeEach(() => { mockUseQueues.mockReturnValue({ queues: mockQueuesForComboBox }); }); afterEach(() => { mockUseQueues.mockReturnValue({ queues: mockQueues }); }); it('renders a searchable ComboBox instead of radio buttons when there are more than 8 queues', () => { renderWithSwr(); // Should render ComboBox for queue selection expect(screen.getByRole('combobox', { name: /service location/i })).toBeInTheDocument(); expect(screen.getByText('Service location')).toBeInTheDocument(); // Should NOT render radio buttons for queue selection (no radiogroup with queue name) expect(screen.queryByRole('radio', { name: /Triage - Main Hospital/i })).not.toBeInTheDocument(); }); it('displays the current queue with "(Current)" suffix in the ComboBox', () => { renderWithSwr(); const combobox = screen.getByRole('combobox', { name: /service location/i }); // mockQueueEntryAlice is in mockQueueSurgery expect(combobox).toHaveValue('Surgery - Surgery (Current)'); }); it('filters queues by name when user types in the search box', async () => { const user = userEvent.setup(); renderWithSwr(); const combobox = screen.getByRole('combobox', { name: /service location/i }); await user.clear(combobox); await user.type(combobox, 'Cardio'); // Should show Cardiology option expect(screen.getByRole('option', { name: 'Cardiology - Heart Center' })).toBeInTheDocument(); // Should not show unrelated queues expect(screen.queryByRole('option', { name: /Pharmacy/i })).not.toBeInTheDocument(); expect(screen.queryByRole('option', { name: /Pediatrics/i })).not.toBeInTheDocument(); }); it('filters queues by location name when user types in the search box', async () => { const user = userEvent.setup(); renderWithSwr(); const combobox = screen.getByRole('combobox', { name: /service location/i }); await user.clear(combobox); await user.type(combobox, 'Heart Center'); // Should show queue at Heart Center location expect(screen.getByRole('option', { name: 'Cardiology - Heart Center' })).toBeInTheDocument(); // Should not show queues at other locations expect(screen.queryByRole('option', { name: /Main Hospital/i })).not.toBeInTheDocument(); }); it('allows user to select a queue from filtered results', async () => { const mockSubmitAction = vi.fn().mockResolvedValue({ status: 200 }); const user = userEvent.setup(); renderWithSwr( , ); const combobox = screen.getByRole('combobox', { name: /service location/i }); await user.clear(combobox); await user.type(combobox, 'Pediatrics'); const option = screen.getByRole('option', { name: 'Pediatrics - Children Wing' }); await user.click(option); // Verify the selection is reflected in the combobox expect(combobox).toHaveValue('Pediatrics - Children Wing'); // Submit and verify the selected queue is passed const submitButton = screen.getByRole('button', { name: 'Submit' }); await user.click(submitButton); expect(mockSubmitAction).toHaveBeenCalled(); }); it('clears submission error when user selects a different queue via ComboBox', async () => { const mockSubmitAction = vi.fn().mockRejectedValueOnce({ message: 'Test error' }); const user = userEvent.setup(); renderWithSwr( , ); // Trigger an error const submitButton = screen.getByRole('button', { name: 'Submit' }); await user.click(submitButton); expect(await screen.findByText('Test error')).toBeInTheDocument(); // Select a different queue const combobox = screen.getByRole('combobox', { name: /service location/i }); await user.clear(combobox); await user.type(combobox, 'Laboratory'); const option = screen.getByRole('option', { name: 'Laboratory - Main Hospital' }); await user.click(option); // Error should be cleared expect(screen.queryByText('Test error')).not.toBeInTheDocument(); }); it('performs case-insensitive search', async () => { const user = userEvent.setup(); renderWithSwr(); const combobox = screen.getByRole('combobox', { name: /service location/i }); await user.clear(combobox); await user.type(combobox, 'RADIOLOGY'); expect(screen.getByRole('option', { name: 'Radiology - Imaging Center' })).toBeInTheDocument(); }); }); describe('QueueEntryActionModal - time validation with minute precision', () => { const baseModalParams = { modalTitle: 'Edit Queue Entry', submitButtonText: 'Submit', submitSuccessTitle: 'Success', submitSuccessText: 'Updated', submitFailureTitle: 'Failed', submitAction: vi.fn().mockResolvedValue({ status: 200 }), disableSubmit: vi.fn().mockReturnValue(false), isEdit: true, showQueuePicker: true, showStatusPicker: true, }; it('should allow time entry matching previous entry minute when seconds are non-zero', async () => { const queueEntryWithSeconds: typeof mockQueueEntryAlice = { ...mockQueueEntryAlice, startedAt: '2024-01-02T05:32:00.000+0000', previousQueueEntry: { ...mockPreviousQueueEntryAlice, startedAt: '2024-01-02T05:31:59.000+0000', }, }; const user = userEvent.setup(); renderWithSwr( , ); const nowCheckbox = screen.getByRole('checkbox', { name: 'Now' }); await user.click(nowCheckbox); const timeInput = screen.getByRole('textbox', { name: 'Time' }); await user.clear(timeInput); const prevDate = new Date('2024-01-02T05:31:59.000+0000'); const hours = prevDate.getHours() % 12 || 12; const formattedHours = hours.toString().padStart(2, '0'); const formattedMinutes = prevDate.getMinutes().toString().padStart(2, '0'); await user.type(timeInput, `${formattedHours}:${formattedMinutes}`); expect(screen.queryByText(/cannot be before start of previous queue entry/i)).not.toBeInTheDocument(); }); it('should show error when time is strictly before the truncated previous entry minute', async () => { const queueEntryWithSeconds: typeof mockQueueEntryAlice = { ...mockQueueEntryAlice, startedAt: '2024-01-02T05:32:00.000+0000', previousQueueEntry: { ...mockPreviousQueueEntryAlice, startedAt: '2024-01-02T05:31:59.000+0000', }, }; const user = userEvent.setup(); renderWithSwr( , ); const nowCheckbox = screen.getByRole('checkbox', { name: 'Now' }); await user.click(nowCheckbox); const timeInput = screen.getByRole('textbox', { name: 'Time' }); await user.clear(timeInput); const beforeDate = new Date('2024-01-02T05:30:00.000+0000'); const hours = beforeDate.getHours() % 12 || 12; const formattedHours = hours.toString().padStart(2, '0'); const formattedMinutes = beforeDate.getMinutes().toString().padStart(2, '0'); await user.type(timeInput, `${formattedHours}:${formattedMinutes}`); expect(await screen.findByText(/cannot be before start of previous queue entry/i)).toBeInTheDocument(); }); it('should skip previous entry validation when previousQueueEntry is null', async () => { const user = userEvent.setup(); renderWithSwr( , ); const nowCheckbox = screen.getByRole('checkbox', { name: 'Now' }); await user.click(nowCheckbox); const timeInput = screen.getByRole('textbox', { name: 'Time' }); await user.clear(timeInput); await user.type(timeInput, '12:00'); expect(screen.queryByText(/cannot be before start of previous queue entry/i)).not.toBeInTheDocument(); }); });