Skip to main content
Glama
useEncounterChart.test.tsx16.1 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { act, renderHook, waitFor } from '@testing-library/react'; import { MedplumProvider } from '@medplum/react'; import type { JSX } from 'react'; import type { Appointment, ChargeItem, Claim, ClinicalImpression, Encounter, Patient, Practitioner, Reference, Task, } from '@medplum/fhirtypes'; import { MockClient } from '@medplum/mock'; import { describe, expect, test, vi, beforeEach } from 'vitest'; import { useEncounterChart } from './useEncounterChart'; import { getChargeItemsForEncounter } from '../utils/chargeitems'; import { createClaimFromEncounter } from '../utils/claims'; import { showErrorNotification } from '../utils/notifications'; vi.mock('../utils/chargeitems'); vi.mock('../utils/claims'); vi.mock('../utils/notifications'); const mockEncounter: Encounter = { resourceType: 'Encounter', id: 'encounter-123', status: 'in-progress', class: { code: 'AMB', system: 'http://terminology.hl7.org/CodeSystem/v3-ActCode' }, subject: { reference: 'Patient/patient-123' }, participant: [ { individual: { reference: 'Practitioner/practitioner-123' }, }, ], appointment: [{ reference: 'Appointment/appointment-123' }], }; const mockPatient: Patient = { resourceType: 'Patient', id: 'patient-123', name: [{ given: ['Test'], family: 'Patient' }], }; const mockPractitioner: Practitioner = { resourceType: 'Practitioner', id: 'practitioner-123', name: [{ given: ['Dr.'], family: 'Test' }], }; const mockAppointment: Appointment = { resourceType: 'Appointment', id: 'appointment-123', status: 'booked', participant: [], }; const mockTask: Task = { resourceType: 'Task', id: 'task-123', status: 'in-progress', intent: 'order', encounter: { reference: 'Encounter/encounter-123' }, authoredOn: '2024-01-01T10:00:00Z', }; const mockClinicalImpression: ClinicalImpression = { resourceType: 'ClinicalImpression', id: 'clinical-impression-123', status: 'completed', subject: { reference: 'Patient/patient-123' }, encounter: { reference: 'Encounter/encounter-123' }, }; const mockChargeItem: ChargeItem = { resourceType: 'ChargeItem', id: 'charge-item-123', status: 'billable', subject: { reference: 'Patient/patient-123' }, code: { text: 'Test Charge' }, }; const mockClaim: Claim = { resourceType: 'Claim', id: 'claim-123', status: 'active', type: { coding: [{ code: 'professional' }] }, use: 'claim', created: new Date().toISOString(), patient: { reference: 'Patient/patient-123' }, provider: { reference: 'Practitioner/practitioner-123' }, priority: { coding: [{ code: 'normal' }] }, insurance: [], item: [ { sequence: 1, encounter: [{ reference: 'Encounter/encounter-123' }], productOrService: { coding: [{ code: 'CPT', system: 'http://terminology.hl7.org/CodeSystem/cpt' }], text: 'CPT Code', }, }, ], }; describe('useEncounterChart', () => { let medplum: MockClient; beforeEach(async () => { medplum = new MockClient(); vi.clearAllMocks(); }); const wrapper = ({ children }: { children: React.ReactNode }): JSX.Element => ( <MedplumProvider medplum={medplum}>{children}</MedplumProvider> ); test('returns initial state when no encounter provided', () => { const { result } = renderHook(() => useEncounterChart(undefined), { wrapper }); expect(result.current.encounter).toBeUndefined(); expect(result.current.claim).toBeUndefined(); expect(result.current.practitioner).toBeUndefined(); expect(result.current.tasks).toEqual([]); expect(result.current.clinicalImpression).toBeUndefined(); expect(result.current.chargeItems).toEqual([]); expect(result.current.appointment).toBeUndefined(); }); test('resolves encounter reference and loads data', async () => { // Create resources in MockClient const createdEncounter = await medplum.createResource(mockEncounter); await medplum.createResource(mockPractitioner); vi.mocked(getChargeItemsForEncounter).mockResolvedValue([mockChargeItem]); const encounterRef: Reference<Encounter> = { reference: `Encounter/${createdEncounter.id}` }; const { result } = renderHook(() => useEncounterChart(encounterRef), { wrapper }); await waitFor(() => { expect(result.current.encounter?.id).toBe(createdEncounter.id); }); await waitFor(() => { expect(result.current.chargeItems).toHaveLength(1); }); }); test('loads charge items for encounter', async () => { await medplum.createResource(mockEncounter); await medplum.createResource(mockPractitioner); vi.mocked(getChargeItemsForEncounter).mockResolvedValue([mockChargeItem]); const { result } = renderHook(() => useEncounterChart(mockEncounter), { wrapper }); await waitFor(() => { expect(result.current.chargeItems).toHaveLength(1); expect(result.current.chargeItems[0].id).toBe('charge-item-123'); }); }); test('fetches existing claim for encounter', async () => { const createdEncounter = await medplum.createResource(mockEncounter); await medplum.createResource(mockPractitioner); const claimWithEncounter: Claim = { ...mockClaim, item: [ { sequence: 1, encounter: [{ reference: `Encounter/${createdEncounter.id}` }], productOrService: { coding: [{ code: 'CPT', system: 'http://terminology.hl7.org/CodeSystem/cpt' }], text: 'CPT Code', }, }, ], }; await medplum.createResource(claimWithEncounter); vi.mocked(getChargeItemsForEncounter).mockResolvedValue([]); const { result } = renderHook(() => useEncounterChart(createdEncounter), { wrapper }); await waitFor(() => { expect(result.current.claim).toBeDefined(); }); }); test('fetches tasks for encounter', async () => { await medplum.createResource(mockEncounter); await medplum.createResource(mockPractitioner); await medplum.createResource(mockTask); vi.mocked(getChargeItemsForEncounter).mockResolvedValue([]); const { result } = renderHook(() => useEncounterChart(mockEncounter), { wrapper }); await waitFor(() => { expect(result.current.tasks).toHaveLength(1); expect(result.current.tasks[0].id).toBe('task-123'); }); }); test('sorts tasks by authoredOn date', async () => { const olderTask: Task = { ...mockTask, id: 'task-old', authoredOn: '2024-01-01T08:00:00Z', }; const newerTask: Task = { ...mockTask, id: 'task-new', authoredOn: '2024-01-01T12:00:00Z', }; await medplum.createResource(mockEncounter); await medplum.createResource(mockPractitioner); await medplum.createResource(newerTask); await medplum.createResource(olderTask); vi.mocked(getChargeItemsForEncounter).mockResolvedValue([]); const { result } = renderHook(() => useEncounterChart(mockEncounter), { wrapper }); await waitFor(() => { expect(result.current.tasks).toHaveLength(2); expect(result.current.tasks[0].id).toBe('task-old'); expect(result.current.tasks[1].id).toBe('task-new'); }); }); test('fetches clinical impression for encounter', async () => { await medplum.createResource(mockEncounter); await medplum.createResource(mockPractitioner); await medplum.createResource(mockClinicalImpression); vi.mocked(getChargeItemsForEncounter).mockResolvedValue([]); const { result } = renderHook(() => useEncounterChart(mockEncounter), { wrapper }); await waitFor(() => { expect(result.current.clinicalImpression?.id).toBe('clinical-impression-123'); }); }); test('fetches practitioner from encounter participant', async () => { await medplum.createResource(mockEncounter); await medplum.createResource(mockPractitioner); vi.mocked(getChargeItemsForEncounter).mockResolvedValue([]); const { result } = renderHook(() => useEncounterChart(mockEncounter), { wrapper }); await waitFor(() => { expect(result.current.practitioner?.id).toBe('practitioner-123'); }); }); test('fetches appointment from encounter', async () => { await medplum.createResource(mockEncounter); await medplum.createResource(mockPractitioner); await medplum.createResource(mockAppointment); vi.mocked(getChargeItemsForEncounter).mockResolvedValue([]); const { result } = renderHook(() => useEncounterChart(mockEncounter), { wrapper }); await waitFor(() => { expect(result.current.appointment?.id).toBe('appointment-123'); }); }); test('creates claim when all conditions are met', async () => { const newClaim: Claim = { resourceType: 'Claim', id: 'new-claim-123', status: 'active', type: { coding: [{ code: 'professional' }] }, use: 'claim', created: new Date().toISOString(), patient: { reference: 'Patient/patient-123' }, provider: { reference: 'Practitioner/practitioner-123' }, priority: { coding: [{ code: 'normal' }] }, insurance: [], }; await medplum.createResource(mockEncounter); await medplum.createResource(mockPractitioner); await medplum.createResource(mockPatient); vi.mocked(getChargeItemsForEncounter).mockResolvedValue([mockChargeItem]); vi.mocked(createClaimFromEncounter).mockResolvedValue(newClaim); const { result } = renderHook(() => useEncounterChart(mockEncounter, mockPatient), { wrapper }); // Wait for all prerequisites to be met await waitFor(() => { expect(result.current.chargeItems).toHaveLength(1); expect(result.current.practitioner).toBeDefined(); }); // Then wait for claim to be created await waitFor(() => { expect(result.current.claim?.id).toBe('new-claim-123'); }); expect(createClaimFromEncounter).toHaveBeenCalledWith(medplum, 'patient-123', 'encounter-123', 'practitioner-123', [ mockChargeItem, ]); }); test('does not create claim if one already exists', async () => { await medplum.createResource(mockEncounter); await medplum.createResource(mockPractitioner); await medplum.createResource(mockPatient); const createdClaim = await medplum.createResource(mockClaim); vi.mocked(getChargeItemsForEncounter).mockResolvedValue([mockChargeItem]); const { result } = renderHook(() => useEncounterChart(mockEncounter, mockPatient), { wrapper }); await waitFor(() => { expect(result.current.claim).toBeDefined(); }); // Verify the existing claim is found (may have different ID due to MockClient) expect(result.current.claim?.id).toBe(createdClaim.id); // Verify no new claim is created expect(createClaimFromEncounter).not.toHaveBeenCalled(); }); test('does not create claim if patient is missing', async () => { await medplum.createResource(mockEncounter); await medplum.createResource(mockPractitioner); vi.mocked(getChargeItemsForEncounter).mockResolvedValue([mockChargeItem]); const { result } = renderHook(() => useEncounterChart(mockEncounter), { wrapper }); // Wait for data to load await waitFor(() => { expect(result.current.chargeItems).toHaveLength(1); expect(result.current.practitioner).toBeDefined(); }); // Verify claim was not created and remains undefined expect(result.current.claim).toBeUndefined(); expect(createClaimFromEncounter).not.toHaveBeenCalled(); }); test('does not create claim if practitioner is missing', async () => { const encounterWithoutPractitioner: Encounter = { ...mockEncounter, participant: [], }; await medplum.createResource(encounterWithoutPractitioner); vi.mocked(getChargeItemsForEncounter).mockResolvedValue([mockChargeItem]); const { result } = renderHook(() => useEncounterChart(encounterWithoutPractitioner, mockPatient), { wrapper }); await waitFor(() => { expect(result.current.chargeItems).toHaveLength(1); }); // Verify no practitioner and no claim expect(result.current.practitioner).toBeUndefined(); expect(result.current.claim).toBeUndefined(); expect(createClaimFromEncounter).not.toHaveBeenCalled(); }); test('does not create claim if charge items are empty', async () => { await medplum.createResource(mockEncounter); await medplum.createResource(mockPractitioner); vi.mocked(getChargeItemsForEncounter).mockResolvedValue([]); const { result } = renderHook(() => useEncounterChart(mockEncounter, mockPatient), { wrapper }); await waitFor(() => { expect(result.current.practitioner).toBeDefined(); }); // Verify no charge items and no claim expect(result.current.chargeItems).toEqual([]); expect(result.current.claim).toBeUndefined(); expect(createClaimFromEncounter).not.toHaveBeenCalled(); }); test('handles charge item fetch errors gracefully', async () => { const error = new Error('Failed to fetch charge items'); await medplum.createResource(mockEncounter); await medplum.createResource(mockPractitioner); vi.mocked(getChargeItemsForEncounter).mockRejectedValue(error); const { result } = renderHook(() => useEncounterChart(mockEncounter), { wrapper }); await waitFor(() => { expect(showErrorNotification).toHaveBeenCalled(); }); // Hook should still function with empty charge items expect(result.current.encounter).toBeDefined(); expect(result.current.chargeItems).toEqual([]); }); test('handles search errors gracefully', async () => { const error = new Error('Search failed'); await medplum.createResource(mockEncounter); // Mock searchResources to fail medplum.searchResources = vi.fn().mockRejectedValue(error); vi.mocked(getChargeItemsForEncounter).mockResolvedValue([]); const { result } = renderHook(() => useEncounterChart(mockEncounter), { wrapper }); await waitFor(() => { expect(showErrorNotification).toHaveBeenCalled(); }); // Hook should still have the encounter expect(result.current.encounter?.id).toBe('encounter-123'); expect(result.current.tasks).toEqual([]); expect(result.current.clinicalImpression).toBeUndefined(); }); test('resolves patient reference when provided', async () => { await medplum.createResource(mockPatient); await medplum.createResource(mockEncounter); await medplum.createResource(mockPractitioner); vi.mocked(getChargeItemsForEncounter).mockResolvedValue([]); const patientRef: Reference<Patient> = { reference: 'Patient/patient-123' }; const { result } = renderHook(() => useEncounterChart(mockEncounter, patientRef), { wrapper }); await waitFor(() => { expect(result.current.encounter?.id).toBe('encounter-123'); }); }); test('allows manual state updates via setters', async () => { await medplum.createResource(mockEncounter); await medplum.createResource(mockPractitioner); vi.mocked(getChargeItemsForEncounter).mockResolvedValue([]); const { result } = renderHook(() => useEncounterChart(undefined), { wrapper }); expect(result.current.encounter).toBeUndefined(); const updatedEncounter: Encounter = { ...mockEncounter, status: 'finished', }; act(() => { result.current.setEncounter(updatedEncounter); }); expect(result.current.encounter?.status).toBe('finished'); }); test('updates claim via setter', async () => { await medplum.createResource(mockEncounter); await medplum.createResource(mockPractitioner); vi.mocked(getChargeItemsForEncounter).mockResolvedValue([]); const { result } = renderHook(() => useEncounterChart(mockEncounter), { wrapper }); await waitFor(() => { expect(result.current.encounter).toBeDefined(); }); expect(result.current.claim).toBeUndefined(); act(() => { result.current.setClaim(mockClaim); }); expect(result.current.claim?.id).toBe('claim-123'); }); });

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