Skip to main content
Glama
ResourceForm.test.tsx16 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { HTTP_HL7_ORG, createReference, deepClone, indexStructureDefinitionBundle, loadDataType } from '@medplum/core'; import { readJson } from '@medplum/definitions'; import type { Bot, Bundle, Condition, Observation, OperationOutcome, Patient, Specimen, StructureDefinition, } from '@medplum/fhirtypes'; import { HomerObservation1, MockClient } from '@medplum/mock'; import { MedplumProvider } from '@medplum/react-hooks'; import { convertIsoToLocal, convertLocalToIso } from '../DateTimeInput/DateTimeInput.utils'; import { act, fireEvent, render, screen, within } from '../test-utils/render'; import type { ResourceFormProps } from './ResourceForm'; import { ResourceForm } from './ResourceForm'; const medplum = new MockClient(); describe('ResourceForm', () => { let USCoreStructureDefinitions: StructureDefinition[]; beforeAll(() => { indexStructureDefinitionBundle(readJson('fhir/r4/profiles-types.json') as Bundle); indexStructureDefinitionBundle(readJson('fhir/r4/profiles-resources.json') as Bundle); USCoreStructureDefinitions = readJson('fhir/r4/testing/uscore-v5.0.1-structuredefinitions.json'); }); beforeEach(() => { jest.useFakeTimers(); }); afterEach(async () => { await act(async () => { jest.runOnlyPendingTimers(); }); jest.useRealTimers(); }); async function setup(props: ResourceFormProps, medplumClient?: MockClient): Promise<void> { await act(async () => { render( <MedplumProvider medplum={medplumClient ?? medplum}> <ResourceForm {...props} /> </MedplumProvider> ); }); } test('Error on missing resource type', async () => { const onSubmit = jest.fn(); await setup({ defaultValue: {}, onSubmit, }); }); test('Renders empty Practitioner form', async () => { const onSubmit = jest.fn(); await setup({ defaultValue: { resourceType: 'Practitioner', }, onSubmit, }); expect(await screen.findByText('Resource Type')).toBeInTheDocument(); const control = screen.getByText('Resource Type'); expect(control).toBeDefined(); }); test('Renders Practitioner resource', async () => { const onSubmit = jest.fn(); await setup({ defaultValue: { reference: 'Practitioner/123', }, onSubmit, }); expect(await screen.findByText('Resource Type')).toBeInTheDocument(); const control = screen.getByText('Resource Type'); expect(control).toBeDefined(); }); test('Submit Practitioner', async () => { const onSubmit = jest.fn(); await setup({ defaultValue: { resourceType: 'Practitioner', }, onSubmit, }); expect(await screen.findByText('Resource Type')).toBeInTheDocument(); await act(async () => { fireEvent.click(screen.getByText('Create')); }); expect(onSubmit).toHaveBeenCalled(); }); test('Renders empty Observation form', async () => { const onSubmit = jest.fn(); await setup({ defaultValue: { resourceType: 'Observation', } as Observation, onSubmit, }); expect(await screen.findByText('Resource Type')).toBeInTheDocument(); const control = screen.getByText('Resource Type'); expect(control).toBeDefined(); }); test('Renders Observation resource', async () => { const onSubmit = jest.fn(); await setup({ defaultValue: createReference(HomerObservation1), onSubmit, }); expect(await screen.findByText('Resource Type')).toBeInTheDocument(); const control = screen.getByText('Resource Type'); expect(control).toBeDefined(); }); test('Submit Observation', async () => { await medplum.requestSchema('Observation'); const onSubmit = jest.fn(); await setup({ defaultValue: { resourceType: 'Observation', valueQuantity: { value: 1, unit: 'kg', }, } as Observation, onSubmit, }); expect(await screen.findByText('Resource Type')).toBeInTheDocument(); // Change the value[x] from Quantity to string // and set a value await act(async () => { fireEvent.change(screen.getByDisplayValue('Quantity'), { target: { value: 'string' }, }); }); await act(async () => { fireEvent.change(screen.getByTestId('value[x]'), { target: { value: 'hello' }, }); }); await act(async () => { fireEvent.click(screen.getByText('Create')); }); expect(onSubmit).toHaveBeenCalled(); const result = onSubmit.mock.calls[0][0]; expect(result.resourceType).toBe('Observation'); expect(result.valueQuantity).toBeUndefined(); expect(result.valueString).toBe('hello'); }); test('Patch', async () => { const onSubmit = jest.fn(); const onPatch = jest.fn(); await setup({ defaultValue: { resourceType: 'Practitioner', id: 'xyz', }, onSubmit, onPatch, }); const moreActions = screen.getByLabelText('More actions'); expect(moreActions).toBeDefined(); await act(async () => { fireEvent.click(moreActions); }); const patchButton = await screen.findByText('Patch'); expect(patchButton).toBeInTheDocument(); await act(async () => { fireEvent.click(patchButton); }); expect(onSubmit).not.toHaveBeenCalled(); expect(onPatch).toHaveBeenCalled(); }); test('Delete', async () => { const onSubmit = jest.fn(); const onDelete = jest.fn(); await setup({ defaultValue: { resourceType: 'Practitioner', id: 'xyz', }, onSubmit, onDelete, }); const moreActions = screen.getByLabelText('More actions'); expect(moreActions).toBeDefined(); await act(async () => { fireEvent.click(moreActions); }); const deleteButton = await screen.findByText('Delete'); expect(deleteButton).toBeInTheDocument(); await act(async () => { fireEvent.click(deleteButton); }); expect(onSubmit).not.toHaveBeenCalled(); expect(onDelete).toHaveBeenCalled(); }); test('Change Specimen.collection.collectedDateTime', async () => { const date = new Date(); date.setMilliseconds(0); // datetime-local does not support milliseconds const localString = convertIsoToLocal(date.toISOString()); const isoString = convertLocalToIso(localString); const onSubmit = jest.fn(); await setup({ defaultValue: { resourceType: 'Specimen' }, onSubmit }); expect(await screen.findByText('Resource Type')).toBeInTheDocument(); await act(async () => { fireEvent.change(screen.getByTestId('collected[x]'), { target: { value: localString } }); }); await act(async () => { fireEvent.click(screen.getByText('Create')); }); expect(onSubmit).toHaveBeenCalled(); const result = onSubmit.mock.calls[0][0] as Specimen; expect(result.resourceType).toBe('Specimen'); expect(result.collection).toBeDefined(); expect(result.collection?.collectedDateTime).toBe(isoString); }); test('Change boolean', async () => { const onSubmit = jest.fn(); await setup({ defaultValue: { resourceType: 'Patient' }, onSubmit }); expect(await screen.findByText('Resource Type')).toBeInTheDocument(); await act(async () => { fireEvent.click(screen.getByLabelText('Active')); }); await act(async () => { fireEvent.click(screen.getByText('Create')); }); expect(onSubmit).toHaveBeenCalled(); const patient = onSubmit.mock.calls[0][0] as Patient; expect(patient.resourceType).toBe('Patient'); expect(patient.active).toBe(true); }); test('Clear choice-of-type string', async () => { const onSubmit = jest.fn(); await setup({ defaultValue: { resourceType: 'Bot', id: '123', cronString: '0 0 0 0 0 0' }, onSubmit, }); const cronStringInput = await screen.findByTestId('cron[x]'); expect(cronStringInput).toBeInTheDocument(); await act(async () => { fireEvent.change(cronStringInput, { target: { value: '' } }); }); await act(async () => { fireEvent.click(screen.getByText('Update')); }); expect(onSubmit).toHaveBeenCalled(); const bot = onSubmit.mock.calls[0][0] as Bot; expect(bot.resourceType).toBe('Bot'); expect(bot.cronString).toBeUndefined(); }); test('Remove CodeableConcept', async () => { const onSubmit = jest.fn(); await setup({ defaultValue: { resourceType: 'Condition', id: '123', clinicalStatus: { coding: [{ system: 'http://system.com', code: 'foo' }] }, verificationStatus: { coding: [{ system: 'http://system.com', code: 'bar' }] }, }, onSubmit, }); const fooSpan = await screen.findByText('foo'); expect(fooSpan).toBeInTheDocument(); const fooClearButton = fooSpan.nextSibling as HTMLElement; expect(fooClearButton).toBeDefined(); await act(async () => { fireEvent.click(fooClearButton); }); await act(async () => { fireEvent.click(screen.getByText('Update')); }); expect(onSubmit).toHaveBeenCalled(); const condition = onSubmit.mock.calls[0][0] as Condition; expect(condition.resourceType).toBe('Condition'); expect(condition.clinicalStatus).toBeUndefined(); }); test('With profileUrl specified', async () => { const profileUrl = `${HTTP_HL7_ORG}/fhir/us/core/StructureDefinition/us-core-implantable-device`; const profilesToLoad = [profileUrl, `${HTTP_HL7_ORG}/fhir/us/core/StructureDefinition/us-core-patient`]; for (const url of profilesToLoad) { const sd = USCoreStructureDefinitions.find((sd) => sd.url === url); if (!sd) { fail(`could not find structure definition for ${url}`); } loadDataType(sd); } const onSubmit = jest.fn(); const mockedMedplum = new MockClient(); const fakeRequestProfileSchema = jest.fn(async (_profileUrl: string) => {}); mockedMedplum.requestProfileSchema = fakeRequestProfileSchema; await setup({ defaultValue: { resourceType: 'Device' }, profileUrl, onSubmit }, mockedMedplum); expect(fakeRequestProfileSchema).toHaveBeenCalledTimes(1); }); describe('US Core Patient', () => { const profileUrl = `${HTTP_HL7_ORG}/fhir/us/core/StructureDefinition/us-core-patient`; const raceExtensionUrl = `${HTTP_HL7_ORG}/fhir/us/core/StructureDefinition/us-core-race`; const ethnicityExtensionUrl = `${HTTP_HL7_ORG}/fhir/us/core/StructureDefinition/us-core-ethnicity`; const profileUrls = [ profileUrl, raceExtensionUrl, ethnicityExtensionUrl, `${HTTP_HL7_ORG}/fhir/us/core/StructureDefinition/us-core-birthsex`, `${HTTP_HL7_ORG}/fhir/us/core/StructureDefinition/us-core-genderIdentity`, ]; const fakeRequestProfileSchema = jest.fn(async (_profileUrl: string) => {}); beforeAll(() => { for (const url of profileUrls) { const sd = USCoreStructureDefinitions.find((sd) => sd.url === url); if (!sd) { fail(`could not find structure definition for ${url}`); } loadDataType(sd); } }); test('add extensions', async () => { const onSubmit = jest.fn(); const mockedMedplum = new MockClient(); mockedMedplum.requestProfileSchema = fakeRequestProfileSchema; const initialValue: Patient = { resourceType: 'Patient', name: [ { given: ['Lisa'], family: 'Simpson', use: 'usual', }, ], gender: 'female', identifier: [ { system: 'http://name.ly', value: 'lisa-123', }, ], }; const expectedValue = deepClone(initialValue); expectedValue.extension = []; await setup({ defaultValue: initialValue, profileUrl, onSubmit }, mockedMedplum); const raceExtension = screen.getByTestId('slice-race'); await act(async () => { fireEvent.click(within(raceExtension).getByText('Add Race')); }); await act(async () => { fireEvent.click(within(raceExtension).getByText('Add OMB Category')); }); const ombCategoryInput = within(within(raceExtension).getByTestId('slice-ombCategory')).getByRole('searchbox'); await act(async () => { fireEvent.focus(ombCategoryInput); }); await act(async () => { fireEvent.change(ombCategoryInput, { target: { value: 'custom-omb-category-value' } }); }); expect(await screen.findByText('+ Create custom-omb-category-value')).toBeInTheDocument(); await act(async () => { fireEvent.click(screen.getByText('+ Create custom-omb-category-value')); }); expect(await screen.findByText('custom-omb-category-value')).toBeInTheDocument(); await act(async () => { const textInput = within(within(raceExtension).getByTestId('slice-text')).getByTestId('value[x]'); fireEvent.change(textInput, { target: { value: 'This is a text value' }, }); }); // Just clicking add, but not filling in a value should not add it to the final value await act(async () => { fireEvent.click(within(raceExtension).getByText('Add Detailed')); }); expectedValue.extension.push({ extension: [ { url: 'ombCategory', valueCoding: { code: 'custom-omb-category-value', display: 'custom-omb-category-value', }, }, { url: 'text', valueString: 'This is a text value', }, ], url: raceExtensionUrl, }); await act(async () => { fireEvent.click(screen.getByText('Create')); }); expect(onSubmit).toHaveBeenCalledWith(expectedValue); }); test('Array-aware error messages on primitive field', async () => { const onSubmit = jest.fn(); const mockedMedplum = new MockClient(); mockedMedplum.requestProfileSchema = fakeRequestProfileSchema; const defaultValue: Patient = { resourceType: 'Patient', identifier: [{ system: 'http://system.com', value: 'foo' }], name: [{ given: ['Matt'] }, { prefix: ['Sir'] }], gender: 'male', meta: { profile: ['http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient'], }, link: [ { other: { reference: 'Patient/5bf4658e-b598-45a1-b575-a896206ae4e0', display: 'Matt' } } as any, { other: { reference: 'Patient/5bf4658e-b598-45a1-b575-a896206ae4e0', display: 'Matt' }, type: 'replaced-by', }, ], }; const outcome: OperationOutcome = { resourceType: 'OperationOutcome', issue: [ { severity: 'error', code: 'structure', details: { text: 'Missing required property', }, expression: ['Patient.link[0].type'], }, ], }; await setup({ defaultValue, profileUrl, onSubmit, outcome }, mockedMedplum); const typeInputs = screen.getAllByText('Type'); expect(typeInputs).toHaveLength(2); // Patient.link[0].type has error const typeLabel1 = typeInputs[0]; if (typeLabel1.parentElement === null) { fail('typeLabel1.parentElement is null'); } expect(within(typeLabel1.parentElement).queryByText('Missing required property')).toBeInTheDocument(); // Patient.link[1].type has NO error const typeLabel2 = typeInputs[1]; if (typeLabel2.parentElement === null) { fail('typeLabel2.parentElement is null'); } expect(within(typeLabel2.parentElement).queryByText('Missing required property')).not.toBeInTheDocument(); }); }); });

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