Skip to main content
Glama
SchedulePage.test.tsx8.99 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { MantineProvider } from '@mantine/core'; import { Notifications } from '@mantine/notifications'; import { MockClient } from '@medplum/mock'; import { MedplumProvider } from '@medplum/react'; import { act, render, screen, waitFor } from '@testing-library/react'; import { MemoryRouter } from 'react-router'; import { describe, expect, test, vi, beforeEach } from 'vitest'; import type { Appointment, Practitioner, Schedule, Slot } from '@medplum/fhirtypes'; import { SchedulePage } from './SchedulePage'; describe('SchedulePage', () => { let medplum: MockClient; let mockPractitioner: Practitioner; let mockSchedule: Schedule; beforeEach(async () => { medplum = new MockClient(); vi.clearAllMocks(); mockPractitioner = { resourceType: 'Practitioner', id: 'practitioner-1', name: [{ given: ['Dr.'], family: 'Smith' }], }; mockSchedule = { resourceType: 'Schedule', id: 'schedule-1', actor: [{ reference: 'Practitioner/practitioner-1' }], active: true, }; // Mock window.innerHeight for calendar height calculation Object.defineProperty(window, 'innerHeight', { writable: true, configurable: true, value: 800, }); // Create practitioner resource in MockClient so getReferenceString works await medplum.createResource(mockPractitioner); medplum.getProfile = vi.fn().mockResolvedValue(mockPractitioner); medplum.searchOne = vi.fn().mockResolvedValue(mockSchedule); medplum.searchResources = vi.fn().mockResolvedValue([]); }); const setup = (): ReturnType<typeof render> => { return render( <MemoryRouter> <MedplumProvider medplum={medplum}> <MantineProvider> <Notifications /> <SchedulePage /> </MantineProvider> </MedplumProvider> </MemoryRouter> ); }; describe('Initial Rendering', () => { test('returns null when schedule is not loaded', async () => { medplum.searchOne = vi.fn().mockResolvedValue(undefined); medplum.createResource = vi.fn().mockResolvedValue(mockSchedule); await act(async () => { setup(); }); // Component should return null until schedule is loaded await waitFor(() => { expect(medplum.searchOne).toHaveBeenCalled(); }); }); test('loads existing schedule for practitioner', async () => { await act(async () => { setup(); }); await waitFor(() => { expect(medplum.searchOne).toHaveBeenCalledWith('Schedule', expect.any(Object)); }); }); test('creates schedule if one does not exist', async () => { medplum.searchOne = vi.fn().mockResolvedValue(undefined); medplum.createResource = vi.fn().mockResolvedValue(mockSchedule); await act(async () => { setup(); }); await waitFor(() => { expect(medplum.createResource).toHaveBeenCalledWith( expect.objectContaining({ resourceType: 'Schedule', active: true, }) ); }); }); test('renders calendar when schedule is loaded', async () => { await act(async () => { setup(); }); await waitFor(() => { // Calendar should be rendered (check for Today button in toolbar) expect(screen.getByText('Today')).toBeInTheDocument(); }); }); }); describe('Toolbar', () => { test('renders toolbar with navigation buttons', async () => { await act(async () => { setup(); }); await waitFor(() => { expect(screen.getByText('Today')).toBeInTheDocument(); // Check for navigation buttons (they contain chevron icons) const buttons = screen.getAllByRole('button'); expect(buttons.length).toBeGreaterThan(0); }); }); test('renders view switcher with Month, Week, Day options', async () => { await act(async () => { setup(); }); await waitFor(() => { expect(screen.getByText('Month')).toBeInTheDocument(); expect(screen.getByText('Week')).toBeInTheDocument(); expect(screen.getByText('Day')).toBeInTheDocument(); }); }); test('displays current month/year in title for non-day views', async () => { await act(async () => { setup(); }); await waitFor(() => { // Check for month/year format (e.g., "January 2024") const title = screen.getByText(/\w+\s+\d{4}/); expect(title).toBeInTheDocument(); }); }); }); describe('Calendar Events', () => { test('searches for slots when date range is set', async () => { const mockSlots: Slot[] = [ { resourceType: 'Slot', id: 'slot-1', schedule: { reference: 'Schedule/schedule-1' }, status: 'free', start: '2024-01-15T10:00:00Z', end: '2024-01-15T10:30:00Z', }, ]; medplum.searchResources = vi.fn().mockImplementation((resourceType: string) => { if (resourceType === 'Slot') { return Promise.resolve(mockSlots); } return Promise.resolve([]); }); await act(async () => { setup(); }); // Wait for calendar to render and trigger range change await waitFor(() => { expect(screen.getByText('Today')).toBeInTheDocument(); }); // The calendar will trigger range change which calls refreshEvents // This may take some time, so we check if searchResources was called with Slot await waitFor( () => { const slotCalls = (medplum.searchResources as ReturnType<typeof vi.fn>).mock.calls.filter( (call) => call[0] === 'Slot' ); expect(slotCalls.length).toBeGreaterThan(0); }, { timeout: 5000 } ); }); test('searches for appointments when date range is set', async () => { const mockAppointments: Appointment[] = [ { resourceType: 'Appointment', id: 'appt-1', status: 'booked', start: '2024-01-15T10:00:00Z', end: '2024-01-15T10:30:00Z', participant: [ { actor: { reference: 'Practitioner/practitioner-1' }, status: 'accepted', }, ], }, ]; medplum.searchResources = vi.fn().mockImplementation((resourceType: string) => { if (resourceType === 'Appointment') { return Promise.resolve(mockAppointments); } return Promise.resolve([]); }); await act(async () => { setup(); }); // Wait for calendar to render await waitFor(() => { expect(screen.getByText('Today')).toBeInTheDocument(); }); // Check if searchResources was called with Appointment await waitFor( () => { const appointmentCalls = (medplum.searchResources as ReturnType<typeof vi.fn>).mock.calls.filter( (call) => call[0] === 'Appointment' ); expect(appointmentCalls.length).toBeGreaterThan(0); }, { timeout: 5000 } ); }); }); describe('Slot Selection', () => { test('opens drawer when slot is selected', async () => { await act(async () => { setup(); }); await waitFor(() => { expect(screen.getByText('Today')).toBeInTheDocument(); }); // The drawer should not be visible initially expect(screen.queryByText('New Calendar Event')).not.toBeInTheDocument(); }); }); describe('View Changes', () => { test('renders view switcher buttons', async () => { await act(async () => { setup(); }); await waitFor(() => { expect(screen.getByText('Month')).toBeInTheDocument(); expect(screen.getByText('Week')).toBeInTheDocument(); expect(screen.getByText('Day')).toBeInTheDocument(); }); }); }); describe('Error Handling', () => { test('handles schedule search error gracefully', async () => { medplum.searchOne = vi.fn().mockRejectedValue(new Error('Search failed')); await act(async () => { setup(); }); await waitFor(() => { expect(medplum.searchOne).toHaveBeenCalled(); }); // Component should handle error (may return null or show error state) // The exact behavior depends on error handling implementation }); test('handles schedule creation error gracefully', async () => { medplum.searchOne = vi.fn().mockResolvedValue(undefined); medplum.createResource = vi.fn().mockRejectedValue(new Error('Creation failed')); await act(async () => { setup(); }); await waitFor(() => { expect(medplum.createResource).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