Skip to main content
Glama
DoseSpotFavoritesPage.test.tsx15.5 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import type { CodeableConcept, MedicationKnowledge } from '@medplum/fhirtypes'; import { MockClient } from '@medplum/mock'; import { MedplumProvider } from '@medplum/react'; import { showNotification } from '@mantine/notifications'; import { describe, expect, test, beforeEach, vi } from 'vitest'; import { DoseSpotFavoritesPage } from './DoseSpotFavoritesPage'; import { act, fireEvent, render, screen, waitFor } from '../../test-utils/render'; import { useDoseSpotClinicFormulary } from '@medplum/dosespot-react'; import * as notifications from '../../utils/notifications'; // Mock the hooks and dependencies vi.mock('@medplum/dosespot-react', async () => { const actual = await vi.importActual('@medplum/dosespot-react'); return { ...actual, useDoseSpotClinicFormulary: vi.fn(), }; }); vi.mock('@mantine/notifications', () => ({ showNotification: vi.fn(), })); vi.mock('../../utils/notifications', () => ({ showErrorNotification: vi.fn(), })); describe('DoseSpotFavoritesPage', () => { let medplum: MockClient; let mockFormularyReturn: ReturnType<typeof useDoseSpotClinicFormulary>; beforeEach(() => { vi.clearAllMocks(); medplum = new MockClient(); mockFormularyReturn = { state: { selectedMedication: undefined, directions: undefined, }, searchMedications: vi.fn().mockResolvedValue([]), setSelectedMedication: vi.fn(), setSelectedMedicationDirections: vi.fn(), saveFavoriteMedication: vi.fn(), clear: vi.fn(), }; vi.mocked(useDoseSpotClinicFormulary).mockReturnValue(mockFormularyReturn); vi.spyOn(medplum, 'search').mockResolvedValue({ resourceType: 'Bundle', type: 'searchset', entry: [], total: 0, }); }); function setup(): void { render( <MedplumProvider medplum={medplum}> <DoseSpotFavoritesPage /> </MedplumProvider> ); } test('Renders page title and add button', () => { setup(); expect(screen.getByText('DoseSpot Medication Favorites')).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Add Favorite Medication' })).toBeInTheDocument(); }); test('Loads favorite medications on mount', async () => { const mockMedicationKnowledge: MedicationKnowledge = { resourceType: 'MedicationKnowledge', id: 'med-1', code: { text: 'Aspirin 325mg', coding: [ { system: 'http://www.nlm.nih.gov/research/umls/rxnorm', code: '1191', display: 'Aspirin 325 MG Oral Tablet', }, ], }, }; vi.spyOn(medplum, 'search').mockResolvedValue({ resourceType: 'Bundle', type: 'searchset', entry: [ { resource: { ...mockMedicationKnowledge, id: 'med-1' }, }, ], total: 1, }); setup(); await waitFor(() => { expect(medplum.search).toHaveBeenCalledWith('MedicationKnowledge', expect.stringContaining('code=')); }); }); test('Shows error notification when loading favorites fails', async () => { const error = new Error('Failed to load favorites'); vi.spyOn(medplum, 'search').mockRejectedValue(error); setup(); await waitFor(() => { expect(notifications.showErrorNotification).toHaveBeenCalledWith(error); }); }); test('Opens modal when Add Favorite Medication button is clicked', async () => { setup(); const addButton = screen.getByRole('button', { name: 'Add Favorite Medication' }); await act(async () => { fireEvent.click(addButton); }); await waitFor(() => { expect(screen.getByText('Medication')).toBeInTheDocument(); }); expect(screen.getByPlaceholderText('Search medications...')).toBeInTheDocument(); }); test('Closes modal when close button is clicked', async () => { setup(); // Wait for initial load to complete await waitFor(() => { expect(screen.getByText('No favorite medications found')).toBeInTheDocument(); }); const addButton = screen.getByRole('button', { name: 'Add Favorite Medication' }); await act(async () => { fireEvent.click(addButton); }); await waitFor(() => { expect(screen.getByText('Medication')).toBeInTheDocument(); }); // Find close button - it might be an X icon button or a button with aria-label const closeButtons = screen.getAllByRole('button'); const closeButton = closeButtons.find( (btn) => btn.getAttribute('aria-label')?.toLowerCase().includes('close') || btn.className.includes('mantine-Modal-close') || btn.querySelector('[class*="mantine-Modal-close"]') ); expect(closeButton).toBeDefined(); if (closeButton) { await act(async () => { fireEvent.click(closeButton); }); await waitFor(() => { expect(screen.queryByText('Medication')).not.toBeInTheDocument(); }); expect(mockFormularyReturn.clear).toHaveBeenCalled(); } }); test('Shows directions input when medication is selected', async () => { const mockMedication: CodeableConcept = { text: 'Aspirin 325mg', coding: [ { system: 'http://www.nlm.nih.gov/research/umls/rxnorm', code: '1191', display: 'Aspirin 325 MG Oral Tablet', }, ], }; // Create a new mock return with medication selected const formularyReturnWithMedication: ReturnType<typeof useDoseSpotClinicFormulary> = { ...mockFormularyReturn, state: { selectedMedication: mockMedication, directions: '', }, }; vi.mocked(useDoseSpotClinicFormulary).mockReturnValue(formularyReturnWithMedication); setup(); // Wait for initial load to complete await waitFor(() => { expect(screen.getByText('No favorite medications found')).toBeInTheDocument(); }); const addButton = screen.getByRole('button', { name: 'Add Favorite Medication' }); await act(async () => { fireEvent.click(addButton); }); await waitFor(() => { expect(screen.getByText('Medication')).toBeInTheDocument(); }); // The directions input should appear when a medication is selected // Since we're mocking the hook, we verify the component structure supports it expect(screen.getByPlaceholderText('Search medications...')).toBeInTheDocument(); }); test('Updates directions when text input changes', async () => { const mockMedication: CodeableConcept = { text: 'Aspirin 325mg', coding: [ { system: 'http://www.nlm.nih.gov/research/umls/rxnorm', code: '1191', display: 'Aspirin 325 MG Oral Tablet', }, ], }; // Create a new mock return with medication selected const formularyReturnWithMedication: ReturnType<typeof useDoseSpotClinicFormulary> = { ...mockFormularyReturn, state: { selectedMedication: mockMedication, directions: 'Take 1 tablet daily', }, }; vi.mocked(useDoseSpotClinicFormulary).mockReturnValue(formularyReturnWithMedication); setup(); // Wait for initial load to complete await waitFor(() => { expect(screen.getByText('No favorite medications found')).toBeInTheDocument(); }); const addButton = screen.getByRole('button', { name: 'Add Favorite Medication' }); await act(async () => { fireEvent.click(addButton); }); await waitFor(() => { expect(screen.getByText('Medication')).toBeInTheDocument(); }); // Verify the directions input exists and can be interacted with // Since we're mocking, we verify the component structure supports direction updates const directionsInputs = screen.queryAllByLabelText('Directions'); if (directionsInputs.length > 0) { const directionsInput = directionsInputs[0]; await act(async () => { fireEvent.change(directionsInput, { target: { value: 'Take 2 tablets daily' } }); }); expect(mockFormularyReturn.setSelectedMedicationDirections).toHaveBeenCalled(); } }); test('Add Favorite button is disabled when medication or directions are missing', async () => { setup(); const addButton = screen.getByRole('button', { name: 'Add Favorite Medication' }); await act(async () => { fireEvent.click(addButton); }); await waitFor(() => { expect(screen.getByRole('button', { name: 'Add Favorite' })).toBeDisabled(); }); }); test('Add Favorite button is enabled when medication and directions are provided', async () => { const mockMedication: CodeableConcept = { text: 'Aspirin 325mg', coding: [ { system: 'http://www.nlm.nih.gov/research/umls/rxnorm', code: '1191', display: 'Aspirin 325 MG Oral Tablet', }, ], }; mockFormularyReturn.state.selectedMedication = mockMedication; mockFormularyReturn.state.directions = 'Take 1 tablet daily'; vi.mocked(useDoseSpotClinicFormulary).mockReturnValue(mockFormularyReturn); setup(); const addButton = screen.getByRole('button', { name: 'Add Favorite Medication' }); await act(async () => { fireEvent.click(addButton); }); await waitFor(() => { const addFavoriteButton = screen.getByRole('button', { name: 'Add Favorite' }); expect(addFavoriteButton).not.toBeDisabled(); }); }); test('Successfully adds favorite medication', async () => { const mockMedication: CodeableConcept = { text: 'Aspirin 325mg', coding: [ { system: 'http://www.nlm.nih.gov/research/umls/rxnorm', code: '1191', display: 'Aspirin 325 MG Oral Tablet', }, ], }; const mockCreatedMedication: MedicationKnowledge = { resourceType: 'MedicationKnowledge', id: 'med-new', code: mockMedication, }; mockFormularyReturn.state.selectedMedication = mockMedication; mockFormularyReturn.state.directions = 'Take 1 tablet daily'; (mockFormularyReturn as any).saveFavoriteMedication = vi.fn().mockResolvedValue(mockCreatedMedication); vi.mocked(useDoseSpotClinicFormulary).mockReturnValue(mockFormularyReturn); setup(); const addButton = screen.getByRole('button', { name: 'Add Favorite Medication' }); await act(async () => { fireEvent.click(addButton); }); await waitFor(() => { expect(screen.getByRole('button', { name: 'Add Favorite' })).not.toBeDisabled(); }); const addFavoriteButton = screen.getByRole('button', { name: 'Add Favorite' }); await act(async () => { fireEvent.click(addFavoriteButton); }); await waitFor(() => { expect(mockFormularyReturn.saveFavoriteMedication).toHaveBeenCalled(); }); await waitFor(() => { expect(showNotification).toHaveBeenCalledWith({ title: 'Medication added to favorites', message: 'The medication has been added to your favorites', color: 'green', }); }); expect(mockFormularyReturn.clear).toHaveBeenCalled(); }); test('Shows error notification when adding favorite fails', async () => { const mockMedication: CodeableConcept = { text: 'Aspirin 325mg', coding: [ { system: 'http://www.nlm.nih.gov/research/umls/rxnorm', code: '1191', display: 'Aspirin 325 MG Oral Tablet', }, ], }; const error = new Error('Failed to save medication'); mockFormularyReturn.state.selectedMedication = mockMedication; mockFormularyReturn.state.directions = 'Take 1 tablet daily'; (mockFormularyReturn as any).saveFavoriteMedication = vi.fn().mockRejectedValue(error); vi.mocked(useDoseSpotClinicFormulary).mockReturnValue(mockFormularyReturn); setup(); const addButton = screen.getByRole('button', { name: 'Add Favorite Medication' }); await act(async () => { fireEvent.click(addButton); }); await waitFor(() => { expect(screen.getByRole('button', { name: 'Add Favorite' })).not.toBeDisabled(); }); const addFavoriteButton = screen.getByRole('button', { name: 'Add Favorite' }); await act(async () => { fireEvent.click(addFavoriteButton); }); await waitFor(() => { expect(notifications.showErrorNotification).toHaveBeenCalledWith( expect.objectContaining({ title: 'Error adding medication to favorites', }) ); }); expect(mockFormularyReturn.clear).toHaveBeenCalled(); }); test('Shows loading state while adding favorite', async () => { const mockMedication: CodeableConcept = { text: 'Aspirin 325mg', coding: [ { system: 'http://www.nlm.nih.gov/research/umls/rxnorm', code: '1191', display: 'Aspirin 325 MG Oral Tablet', }, ], }; const mockCreatedMedication: MedicationKnowledge = { resourceType: 'MedicationKnowledge', id: 'med-new', code: mockMedication, }; let resolveSave: ((value: MedicationKnowledge) => void) | undefined; const savePromise = new Promise<MedicationKnowledge>((resolve) => { resolveSave = () => resolve(mockCreatedMedication); }); mockFormularyReturn.state.selectedMedication = mockMedication; mockFormularyReturn.state.directions = 'Take 1 tablet daily'; (mockFormularyReturn as any).saveFavoriteMedication = vi.fn().mockReturnValue(savePromise); vi.mocked(useDoseSpotClinicFormulary).mockReturnValue(mockFormularyReturn); setup(); const addButton = screen.getByRole('button', { name: 'Add Favorite Medication' }); await act(async () => { fireEvent.click(addButton); }); await waitFor(() => { expect(screen.getByRole('button', { name: 'Add Favorite' })).not.toBeDisabled(); }); const addFavoriteButton = screen.getByRole('button', { name: 'Add Favorite' }); await act(async () => { fireEvent.click(addFavoriteButton); }); // Button should be disabled while loading await waitFor(() => { expect(addFavoriteButton).toBeDisabled(); }); // Resolve the promise resolveSave?.(mockCreatedMedication); await savePromise; await waitFor(() => { expect(addFavoriteButton).not.toBeDisabled(); }); }); test('Clears form when modal is closed', async () => { setup(); // Wait for initial load to complete await waitFor(() => { expect(screen.getByText('No favorite medications found')).toBeInTheDocument(); }); const addButton = screen.getByRole('button', { name: 'Add Favorite Medication' }); await act(async () => { fireEvent.click(addButton); }); await waitFor(() => { expect(screen.getByText('Medication')).toBeInTheDocument(); }); // Find close button - it might be an X icon button or a button with aria-label const closeButtons = screen.getAllByRole('button'); const closeButton = closeButtons.find( (btn) => btn.getAttribute('aria-label')?.toLowerCase().includes('close') || btn.className.includes('mantine-Modal-close') ); expect(closeButton).toBeDefined(); if (closeButton) { await act(async () => { fireEvent.click(closeButton); }); expect(mockFormularyReturn.clear).toHaveBeenCalled(); } }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/medplum/medplum'

If you have feedback or need assistance with the MCP directory API, please join our Discord server