Skip to main content
Glama
useQuestionnaireForm.test.tsx12.2 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import type { Questionnaire, QuestionnaireItem, QuestionnaireResponse, QuestionnaireResponseItem, } from '@medplum/fhirtypes'; import { MockClient } from '@medplum/mock'; import { act, renderHook } from '@testing-library/react'; import { MedplumProvider } from '../MedplumProvider/MedplumProvider'; import type { QuestionnaireFormLoadedState, QuestionnaireFormPaginationState } from './useQuestionnaireForm'; import { useQuestionnaireForm } from './useQuestionnaireForm'; describe('useQuestionnaireForm', () => { const medplum = new MockClient(); const wrapper: React.JSXElementConstructor<{ children: React.ReactNode; }> = ({ children }) => <MedplumProvider medplum={medplum}>{children}</MedplumProvider>; test('Pass by value', async () => { const questionnaire = { resourceType: 'Questionnaire', id: 'test', status: 'active', item: [ { type: 'string', linkId: 'test-item', text: 'Test Item', }, ], } as const; const onChange = jest.fn(); const { result } = renderHook(() => useQuestionnaireForm({ questionnaire, onChange }), { wrapper }); expect(onChange).toHaveBeenCalledTimes(1); expect(result.current).toMatchObject({ loading: false, questionnaire, questionnaireResponse: expect.objectContaining({ resourceType: 'QuestionnaireResponse', status: 'in-progress', questionnaire: 'Questionnaire/test', item: [ expect.objectContaining({ linkId: 'test-item', text: 'Test Item', }), ], }), }); onChange.mockClear(); const formState = result.current as QuestionnaireFormLoadedState; await act(async () => { formState.onChangeAnswer([], questionnaire.item[0], [{ valueString: 'Test Answer' }]); }); expect(onChange).toHaveBeenCalledTimes(1); const updatedState = result.current as QuestionnaireFormLoadedState; expect(updatedState.questionnaireResponse.item?.[0]).toMatchObject({ linkId: 'test-item', text: 'Test Item', answer: [{ valueString: 'Test Answer' }], }); }); test('Start with existing response', () => { const questionnaire: Questionnaire = { resourceType: 'Questionnaire', id: 'test', status: 'active', item: [ { type: 'string', linkId: 'test-item', text: 'Test Item', }, ], }; const defaultValue: QuestionnaireResponse = { resourceType: 'QuestionnaireResponse', status: 'in-progress', questionnaire: 'Questionnaire/test', item: [ { linkId: 'test-item', text: 'Test Item', answer: [{ valueString: 'Existing Answer' }], }, ], }; const { result } = renderHook(() => useQuestionnaireForm({ questionnaire, defaultValue }), { wrapper }); expect(result.current).toMatchObject({ loading: false, questionnaire, questionnaireResponse: expect.objectContaining(defaultValue), }); }); test('Pagination', async () => { const questionnaire = { resourceType: 'Questionnaire', id: 'pages-example', title: 'Pages Example', item: [ { linkId: 'group1', text: 'Group 1', type: 'group', item: [ { linkId: 'question1', text: 'Question 1', type: 'string', }, ], extension: [ { url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl', valueCodeableConcept: { coding: [ { system: 'http://hl7.org/fhir/questionnaire-item-control', code: 'page', }, ], }, }, ], }, { linkId: 'group2', text: 'Group 2', type: 'group', item: [ { linkId: 'question2', text: 'Question 2', type: 'reference', }, ], }, ], } as const; const { result } = renderHook(() => useQuestionnaireForm({ questionnaire }), { wrapper }); expect(result.current).toMatchObject({ loading: false, pagination: true }); // Should be on the first page const formState = result.current as QuestionnaireFormPaginationState; expect(formState.items[0].linkId).toBe('group1'); // Move to the next page await act(async () => { formState.onNextPage(); }); // Should now be on the second page const updatedState = result.current as QuestionnaireFormLoadedState; expect(updatedState.items[0].linkId).toBe('group2'); // Move back to the first page await act(async () => { formState.onPrevPage(); }); // Should be back on the first page const backToFirstPageState = result.current as QuestionnaireFormLoadedState; expect(backToFirstPageState.items[0].linkId).toBe('group1'); }); test('Repeatable group', async () => { const questionnaire = { resourceType: 'Questionnaire', id: 'test', status: 'active', item: [ { type: 'group', linkId: 'test-group', text: 'Test Group', repeats: true, item: [ { type: 'string', linkId: 'test-item', text: 'Test Item', }, ], }, ], }; const { result } = renderHook(() => useQuestionnaireForm({ questionnaire }), { wrapper }); expect(result.current).toMatchObject({ loading: false }); const formState = result.current as QuestionnaireFormLoadedState; expect(formState.items).toHaveLength(1); expect(formState.questionnaireResponse.item).toHaveLength(1); await act(async () => { formState.onAddGroup([], questionnaire.item[0] as QuestionnaireItem); }); const updatedState = result.current as QuestionnaireFormLoadedState; expect(updatedState.items).toHaveLength(1); expect(updatedState.questionnaireResponse.item).toHaveLength(2); }); test('Repeatable groups should maintain separate answers for each group instance', async () => { const questionnaire: Questionnaire = { resourceType: 'Questionnaire', id: 'test', status: 'active', item: [ { linkId: 'group1', text: 'Group 1', type: 'group', repeats: true, item: [ { linkId: 'question1', text: 'Question 1', type: 'string', }, ], }, ], }; const { result } = renderHook(() => useQuestionnaireForm({ questionnaire }), { wrapper }); expect(result.current).toMatchObject({ loading: false }); const formState = result.current as QuestionnaireFormLoadedState; expect(formState.items).toHaveLength(1); expect(formState.questionnaireResponse.item).toHaveLength(1); // Add a second group await act(async () => { formState.onAddGroup([], questionnaire.item?.[0] as QuestionnaireItem); }); const updatedState = result.current as QuestionnaireFormLoadedState; expect(updatedState.questionnaireResponse.item).toHaveLength(2); // Get the two group instances const responseItems = updatedState.questionnaireResponse.item; expect(responseItems).toBeDefined(); expect(responseItems).toHaveLength(2); const group1 = responseItems?.[0] as QuestionnaireResponseItem; const group2 = responseItems?.[1] as QuestionnaireResponseItem; // Answer question in first group await act(async () => { const questionItem = questionnaire.item?.[0]?.item?.[0] as QuestionnaireItem; updatedState.onChangeAnswer([group1], questionItem, [{ valueString: 'Answer 1' }]); }); // Answer question in second group await act(async () => { const questionItem = questionnaire.item?.[0]?.item?.[0] as QuestionnaireItem; updatedState.onChangeAnswer([group2], questionItem, [{ valueString: 'Answer 2' }]); }); const finalState = result.current as QuestionnaireFormLoadedState; // Verify that each group has its own answer const finalResponseItems = finalState.questionnaireResponse.item; expect(finalResponseItems).toBeDefined(); expect(finalResponseItems).toHaveLength(2); const finalGroup1Answer = finalResponseItems?.[0]?.item?.[0]?.answer; const finalGroup2Answer = finalResponseItems?.[1]?.item?.[0]?.answer; expect(finalGroup1Answer).toHaveLength(1); expect(finalGroup2Answer).toHaveLength(1); expect(finalGroup1Answer?.[0]?.valueString).toBe('Answer 1'); expect(finalGroup2Answer?.[0]?.valueString).toBe('Answer 2'); }); test('Repeatable answer', async () => { const questionnaire = { resourceType: 'Questionnaire', id: 'test', status: 'active', item: [ { type: 'string', linkId: 'test-item', text: 'Test Item', repeats: true, }, ], } as const; const { result } = renderHook(() => useQuestionnaireForm({ questionnaire }), { wrapper }); expect(result.current).toMatchObject({ loading: false }); const formState1 = result.current as QuestionnaireFormLoadedState; expect(formState1.items).toHaveLength(1); expect(formState1.questionnaireResponse.item).toHaveLength(1); expect(formState1.questionnaireResponse.item?.[0]?.answer).toBeUndefined(); await act(async () => { formState1.onChangeAnswer([], questionnaire.item[0], [{ valueString: 'Test Answer' }]); }); const formState2 = result.current as QuestionnaireFormLoadedState; expect(formState2.questionnaireResponse.item?.[0]).toMatchObject({ linkId: 'test-item', text: 'Test Item', answer: [{ valueString: 'Test Answer' }], }); await act(async () => { formState2.onAddAnswer([], questionnaire.item[0] as QuestionnaireItem); }); const updatedState = result.current as QuestionnaireFormLoadedState; expect(updatedState.items).toHaveLength(1); expect(updatedState.questionnaireResponse.item).toHaveLength(1); expect(updatedState.questionnaireResponse.item?.[0]?.answer).toHaveLength(2); }); test('Signature functionality', async () => { const questionnaire = { resourceType: 'Questionnaire', id: 'test', status: 'active', item: [ { type: 'string', linkId: 'test-item', text: 'Test Item', }, ], } as const; const { result } = renderHook(() => useQuestionnaireForm({ questionnaire }), { wrapper }); expect(result.current).toMatchObject({ loading: false }); const formState = result.current as QuestionnaireFormLoadedState; expect(formState.questionnaireResponse.extension).toBeUndefined(); const signature = { type: [ { system: 'urn:iso-astm:E1762-95:2013', code: '1.2.840.10065.1.12.1.1', display: "Author's Signature", }, ], when: '2023-01-01T00:00:00Z', who: { reference: 'Practitioner/test' }, data: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==', }; await act(async () => { formState.onChangeSignature(signature); }); const signedState = result.current as QuestionnaireFormLoadedState; expect(signedState.questionnaireResponse.extension).toHaveLength(1); expect(signedState.questionnaireResponse.extension?.[0]).toMatchObject({ url: 'http://hl7.org/fhir/StructureDefinition/questionnaireresponse-signature', valueSignature: signature, }); await act(async () => { formState.onChangeSignature(undefined); }); const unsignedState = result.current as QuestionnaireFormLoadedState; expect(unsignedState.questionnaireResponse.extension).toEqual([]); }); });

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