Skip to main content
Glama
QuestionnaireForm.test.tsx87 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { getAllQuestionnaireAnswers, getQuestionnaireAnswers } from '@medplum/core'; import type { Extension, Questionnaire, QuestionnaireResponse } from '@medplum/fhirtypes'; import { MockClient } from '@medplum/mock'; import { MedplumProvider, QUESTIONNAIRE_SIGNATURE_REQUIRED_URL, QuestionnaireItemType } from '@medplum/react-hooks'; import { randomUUID } from 'crypto'; import each from 'jest-each'; import { MemoryRouter } from 'react-router'; import { act, fireEvent, render, screen } from '../test-utils/render'; import type { QuestionnaireFormProps } from './QuestionnaireForm'; import { QuestionnaireForm } from './QuestionnaireForm'; const medplum = new MockClient(); jest.mock('signature_pad', () => { return jest.fn().mockImplementation(() => ({ fromDataURL: jest.fn().mockResolvedValue(undefined), clear: jest.fn(), addEventListener: jest.fn(), removeEventListener: jest.fn(), toDataURL: jest.fn(() => 'data:image/png;base64,signature-data'), })); }); const pageExtension: Extension[] = [ { url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl', valueCodeableConcept: { coding: [ { system: 'http://hl7.org/fhir/questionnaire-item-control', code: 'page', }, ], }, }, ]; async function setup(args: QuestionnaireFormProps): Promise<void> { await act(async () => { render( <MemoryRouter> <MedplumProvider medplum={medplum}> <QuestionnaireForm {...args} /> </MedplumProvider> </MemoryRouter> ); }); } describe('QuestionnaireForm', () => { beforeEach(() => { jest.useFakeTimers(); }); afterEach(async () => { await act(async () => { jest.runOnlyPendingTimers(); }); jest.useRealTimers(); }); test('Renders empty', async () => { await setup({ questionnaire: { resourceType: 'Questionnaire', status: 'active', }, onSubmit: jest.fn(), }); expect(screen.getByTestId('questionnaire-form')).toBeInTheDocument(); }); test('Display text', async () => { await setup({ questionnaire: { resourceType: 'Questionnaire', status: 'active', item: [ { linkId: 'display', text: 'Hello world', type: QuestionnaireItemType.display, }, ], }, onSubmit: jest.fn(), }); expect(screen.getByText('Hello world')).toBeInTheDocument(); }); test('Groups', async () => { const onChange = jest.fn(); const onSubmit = jest.fn(); await setup({ questionnaire: { resourceType: 'Questionnaire', status: 'active', item: [ { linkId: 'group1', text: 'Group 1', type: QuestionnaireItemType.group, item: [ { linkId: 'question1', text: 'Question 1', type: QuestionnaireItemType.string, }, { linkId: 'question2', text: 'Question 2', type: QuestionnaireItemType.string, }, ], }, { linkId: 'group2', text: 'Group 2', type: QuestionnaireItemType.group, item: [ { linkId: 'question3', text: 'Question 3', type: QuestionnaireItemType.string, }, { linkId: 'question4', text: 'Question 4', type: QuestionnaireItemType.string, }, { linkId: 'question5', text: 'Question 5', type: QuestionnaireItemType.boolean, }, ], }, ], }, onChange, onSubmit, }); expect(screen.getByTestId('questionnaire-form')).toBeInTheDocument(); expect(screen.getByText('Group 1')).toBeDefined(); expect(screen.getByText('Group 2')).toBeDefined(); onChange.mockClear(); await act(async () => { fireEvent.change(screen.getByLabelText('Question 1'), { target: { value: 'a1' } }); }); expect(onChange).toHaveBeenCalledTimes(1); await act(async () => { fireEvent.change(screen.getByLabelText('Question 2'), { target: { value: 'a2' } }); }); await act(async () => { fireEvent.change(screen.getByLabelText('Question 3'), { target: { value: 'a3' } }); }); await act(async () => { fireEvent.change(screen.getByLabelText('Question 4'), { target: { value: 'a4' } }); }); await act(async () => { fireEvent.click(screen.getByLabelText('Question 5')); }); await act(async () => { fireEvent.click(screen.getByText('Submit')); }); expect(onSubmit).toHaveBeenCalled(); const response = onSubmit.mock.calls[0][0]; expect(response.resourceType).toBe('QuestionnaireResponse'); expect(response.status).toBe('completed'); expect(response.item).toHaveLength(2); expect(response.item[0].item).toHaveLength(2); expect(response.item[0].item[0].linkId).toBe('question1'); expect(response.item[0].item[0].text).toBe('Question 1'); expect(response.item[1].item[2].linkId).toBe('question5'); expect(response.item[1].item[2].text).toBe('Question 5'); const answers = getQuestionnaireAnswers(response); expect(answers['question1']).toMatchObject({ valueString: 'a1' }); expect(answers['question2']).toMatchObject({ valueString: 'a2' }); expect(answers['question3']).toMatchObject({ valueString: 'a3' }); expect(answers['question4']).toMatchObject({ valueString: 'a4' }); expect(answers['question5']).toMatchObject({ valueBoolean: true }); }); test('Groups with QuestionnaireResponse', async () => { const onSubmit = jest.fn(); await setup({ questionnaire: { resourceType: 'Questionnaire', status: 'active', item: [ { linkId: 'group1', text: 'Group 1', type: QuestionnaireItemType.group, item: [ { linkId: 'question1', text: 'Question 1', type: QuestionnaireItemType.string, }, { linkId: 'question2', text: 'Question 2', type: QuestionnaireItemType.string, }, ], }, { linkId: 'group2', text: 'Group 2', type: QuestionnaireItemType.group, item: [ { linkId: 'question3', text: 'Question 3', type: QuestionnaireItemType.string, }, { linkId: 'question4', text: 'Question 4', type: QuestionnaireItemType.string, }, { linkId: 'question5', text: 'Question 5', type: QuestionnaireItemType.boolean, }, ], }, ], }, questionnaireResponse: { resourceType: 'QuestionnaireResponse', status: 'in-progress', item: [ { id: 'id-2', linkId: 'group1', text: 'Group 1', item: [ { id: 'id-3', linkId: 'question1', text: 'Question 1', answer: [ { valueString: 'a1', }, ], }, { id: 'id-4', linkId: 'question2', text: 'Question 2', answer: [ { valueString: 'a2', }, ], }, ], answer: [], }, { id: 'id-5', linkId: 'group2', text: 'Group 2', item: [ { id: 'id-6', linkId: 'question3', text: 'Question 3', answer: [ { valueString: 'a3', }, ], }, { id: 'id-7', linkId: 'question4', text: 'Question 4', answer: [ { valueString: 'a4', }, ], }, { id: 'id-8', linkId: 'question5', text: 'Question 5', answer: [ { valueBoolean: true, }, ], }, ], answer: [], }, ], }, onSubmit, }); expect(screen.getByTestId('questionnaire-form')).toBeInTheDocument(); expect(screen.getByText('Group 1')).toBeDefined(); expect(screen.getByText('Group 2')).toBeDefined(); await act(async () => { fireEvent.click(screen.getByText('Submit')); }); expect(onSubmit).toHaveBeenCalled(); const responseAuto = onSubmit.mock.calls[0][0]; expect(responseAuto.resourceType).toBe('QuestionnaireResponse'); expect(responseAuto.status).toBe('completed'); const answersAuto = getQuestionnaireAnswers(responseAuto); expect(answersAuto['question1']).toMatchObject({ valueString: 'a1' }); expect(answersAuto['question2']).toMatchObject({ valueString: 'a2' }); expect(answersAuto['question3']).toMatchObject({ valueString: 'a3' }); expect(answersAuto['question4']).toMatchObject({ valueString: 'a4' }); expect(answersAuto['question5']).toMatchObject({ valueBoolean: true }); await act(async () => { fireEvent.change(screen.getByLabelText('Question 1'), { target: { value: 'a11' } }); }); await act(async () => { fireEvent.change(screen.getByLabelText('Question 2'), { target: { value: 'a22' } }); }); await act(async () => { fireEvent.change(screen.getByLabelText('Question 3'), { target: { value: 'a33' } }); }); await act(async () => { fireEvent.change(screen.getByLabelText('Question 4'), { target: { value: 'a44' } }); }); await act(async () => { fireEvent.click(screen.getByLabelText('Question 5')); }); await act(async () => { fireEvent.click(screen.getByText('Submit')); }); const responseManual = onSubmit.mock.calls[1][0]; const answersManual = getQuestionnaireAnswers(responseManual); expect(answersManual['question1']).toMatchObject({ valueString: 'a11' }); expect(answersManual['question2']).toMatchObject({ valueString: 'a22' }); expect(answersManual['question3']).toMatchObject({ valueString: 'a33' }); expect(answersManual['question4']).toMatchObject({ valueString: 'a44' }); expect(answersManual['question5']).toMatchObject({ valueBoolean: false }); }); test('Handles submit', async () => { const onSubmit = jest.fn(); await setup({ questionnaire: { resourceType: 'Questionnaire', url: 'https://example.com/Questionnaire/123', status: 'active', item: [ { linkId: 'q1', type: QuestionnaireItemType.string, text: 'q1', }, { linkId: 'q2', type: QuestionnaireItemType.integer, text: 'q2', }, { linkId: 'q3', type: QuestionnaireItemType.date, text: 'q3', }, { linkId: '', // Silently ignore missing linkId type: QuestionnaireItemType.string, text: 'q4', }, { linkId: 'q5', type: '' as unknown as 'string', // Silently ignore missing type text: 'q5', }, { linkId: 'q6', type: QuestionnaireItemType.string, text: 'q6', initial: [ { valueString: 'initial answer', }, ], }, { linkId: 'q7', type: QuestionnaireItemType.boolean, text: 'q7', }, { linkId: 'q8', type: QuestionnaireItemType.boolean, text: 'q8', initial: [ { valueBoolean: true, }, ], }, ], }, onSubmit, }); expect(screen.getByTestId('questionnaire-form')).toBeInTheDocument(); expect(screen.queryByLabelText('q4')).toBeFalsy(); expect(screen.queryByLabelText('q5')).toBeFalsy(); expect((screen.queryByLabelText('q7') as HTMLInputElement).checked).toBe(false); expect((screen.queryByLabelText('q8') as HTMLInputElement).checked).toBe(true); await act(async () => { fireEvent.change(screen.getByLabelText('q1'), { target: { value: 'a1' } }); }); await act(async () => { fireEvent.change(screen.getByLabelText('q2'), { target: { value: '2' } }); }); await act(async () => { fireEvent.change(screen.getByLabelText('q3'), { target: { value: '2023-03-03' } }); }); await act(async () => { fireEvent.click(screen.getByText('Submit')); }); expect(onSubmit).toHaveBeenCalled(); const response = onSubmit.mock.calls[0][0]; const answers = getQuestionnaireAnswers(response); expect(response.resourceType).toBe('QuestionnaireResponse'); expect(response.status).toBe('completed'); expect(response.questionnaire).toBe('https://example.com/Questionnaire/123'); expect(answers['q1']).toMatchObject({ valueString: 'a1' }); expect(answers['q2']).toMatchObject({ valueInteger: 2 }); expect(answers['q3']).toMatchObject({ valueDate: '2023-03-03' }); expect(answers['q6']).toMatchObject({ valueString: 'initial answer' }); }); test('Handles submit (empty)', async () => { const onSubmit = jest.fn(); await setup({ questionnaire: { resourceType: 'Questionnaire', id: '456', status: 'active', }, onSubmit, }); expect(screen.getByTestId('questionnaire-form')).toBeInTheDocument(); await act(async () => { fireEvent.click(screen.getByText('Submit')); }); expect(onSubmit).toHaveBeenCalled(); const response = onSubmit.mock.calls[0][0]; expect(response.resourceType).toBe('QuestionnaireResponse'); expect(response.status).toBe('completed'); expect(response.questionnaire).toBe('Questionnaire/456'); expect(response.authored).toBeDefined(); expect(response.source).toBeDefined(); }); each([ [QuestionnaireItemType.decimal, 'number', '123.456'], [QuestionnaireItemType.integer, 'number', '123'], [QuestionnaireItemType.date, 'date', '2020-01-01'], [QuestionnaireItemType.dateTime, 'datetime-local', '2020-01-01T12:01:01.000'], [QuestionnaireItemType.time, 'time', '12:01:01'], [QuestionnaireItemType.string, 'text', 'hello'], [QuestionnaireItemType.text, 'textarea', 'lorem ipsum'], [QuestionnaireItemType.url, 'text', 'https://example.com/'], [QuestionnaireItemType.quantity, 'number', '123'], ]).test('%s question', async (propertyType: QuestionnaireItemType, inputType: string, value: string) => { await setup({ questionnaire: { resourceType: 'Questionnaire', status: 'active', item: [ { linkId: 'q1', type: propertyType, text: 'q1', }, ], }, onSubmit: jest.fn(), }); const input = screen.getByLabelText('q1') as HTMLInputElement; expect(input).toBeInTheDocument(); if (inputType !== 'date' && inputType !== 'datetime-local') { // JSDOM does not support date or datetime-local expect(input.type).toEqual(inputType); } await act(async () => { fireEvent.change(input, { target: { value } }); }); expect(input.value).toBe(value); }); test('Boolean input', async () => { await setup({ questionnaire: { resourceType: 'Questionnaire', status: 'active', item: [ { linkId: 'q1', type: QuestionnaireItemType.boolean, text: 'q1', }, ], }, onSubmit: jest.fn(), }); const input = screen.getByLabelText('q1') as HTMLInputElement; expect(input).toBeInTheDocument(); await act(async () => { fireEvent.click(input); }); expect(input.checked).toBe(true); }); test('Choice input', async () => { const onSubmit = jest.fn(); await setup({ questionnaire: { resourceType: 'Questionnaire', status: 'active', item: [ { linkId: 'q1', type: QuestionnaireItemType.choice, text: 'q1', answerOption: [ { valueString: 'a1', }, { valueString: 'a2', }, ], }, ], }, onSubmit, }); expect(screen.getByText('q1')).toBeInTheDocument(); expect(screen.getByText('a1')).toBeInTheDocument(); expect(screen.getByText('a2')).toBeInTheDocument(); await act(async () => { fireEvent.click(screen.getByLabelText('a1')); }); await act(async () => { fireEvent.click(screen.getByText('Submit')); }); const response1 = onSubmit.mock.calls[0][0]; const answers1 = getQuestionnaireAnswers(response1); expect(answers1['q1']).toMatchObject({ valueString: 'a1' }); await act(async () => { fireEvent.click(screen.getByLabelText('a2')); }); await act(async () => { fireEvent.click(screen.getByText('Submit')); }); const response2 = onSubmit.mock.calls[1][0]; const answers2 = getQuestionnaireAnswers(response2); expect(answers2['q1']).toMatchObject({ valueString: 'a2' }); }); test('Choice valueReference default value', async () => { const onSubmit = jest.fn(); await setup({ questionnaire: { resourceType: 'Questionnaire', status: 'active', item: [ { linkId: 'q1', type: QuestionnaireItemType.choice, text: 'q1', answerOption: [ { valueString: 'a1', }, { valueString: 'a2', }, { valueReference: { reference: 'Patient/123', }, }, { valueReference: { reference: 'Organization/123', }, }, ], initial: [ { valueReference: { reference: 'Organization/123', }, }, ], }, ], }, onSubmit, }); const radioButton1 = screen.getByLabelText('a1'); expect(radioButton1).toBeInTheDocument(); expect((radioButton1 as HTMLInputElement).checked).toBe(false); const radioButton2 = screen.getByLabelText('Organization/123'); expect(radioButton2).toBeInTheDocument(); expect((radioButton2 as HTMLInputElement).checked).toBe(true); }); test('Choice valueCoding default value', async () => { const onSubmit = jest.fn(); await setup({ questionnaire: { resourceType: 'Questionnaire', status: 'active', item: [ { linkId: 'q1', type: QuestionnaireItemType.choice, text: 'q1', answerOption: [ { valueString: 'a1', }, { valueString: 'a2', }, { valueCoding: { code: 'patient', }, }, { valueCoding: { code: 'organization', }, }, ], initial: [ { valueCoding: { code: 'patient', }, }, ], }, ], }, onSubmit, }); const radioButton1 = screen.getByLabelText('a1'); expect(radioButton1).toBeInTheDocument(); expect((radioButton1 as HTMLInputElement).checked).toBe(false); const radioButton2 = screen.getByLabelText('patient'); expect(radioButton2).toBeInTheDocument(); expect((radioButton2 as HTMLInputElement).checked).toBe(true); }); test('Open choice input', async () => { await setup({ questionnaire: { resourceType: 'Questionnaire', status: 'active', item: [ { linkId: 'q1', type: QuestionnaireItemType.openChoice, text: 'q1', answerOption: [ { valueString: 'a1', }, { valueString: 'a2', }, ], }, ], }, onSubmit: jest.fn(), }); expect(screen.getByText('q1')).toBeInTheDocument(); expect(screen.getByText('a1')).toBeInTheDocument(); expect(screen.getByText('a2')).toBeInTheDocument(); }); test('Attachment input', async () => { const questionnaire: Questionnaire = { resourceType: 'Questionnaire', status: 'active', id: randomUUID(), item: [ { linkId: 'q1', type: QuestionnaireItemType.attachment, text: 'q1', }, ], }; const expectedResponse: QuestionnaireResponse = { resourceType: 'QuestionnaireResponse', status: 'completed', questionnaire: 'Questionnaire/' + questionnaire.id, source: { display: 'Alice Smith', reference: 'Practitioner/123', }, item: [ { linkId: 'q1', text: 'q1', answer: [ { valueAttachment: { title: 'hello.txt', contentType: 'text/plain', url: expect.stringContaining('https://example.com/binary/'), }, }, ], }, ], }; const onSubmit = jest.fn(); await setup({ questionnaire, onSubmit }); const input = screen.getByText('Upload...'); expect(input).toBeInTheDocument(); await act(async () => { const files = [new File(['hello'], 'hello.txt', { type: 'text/plain' })]; fireEvent.change(screen.getByTestId('upload-file-input'), { target: { files }, }); }); expect(screen.getByText('hello.txt')).toBeInTheDocument(); await act(async () => { fireEvent.click(screen.getByText('Submit')); }); const submittedData = onSubmit.mock.calls[0][0]; expect(submittedData.item[0].answer).toEqual(expectedResponse?.item?.[0].answer); expect(submittedData.item[0].text).toEqual(expectedResponse?.item?.[0].text); expect(submittedData.item[0].linkId).toEqual(expectedResponse?.item?.[0].linkId); }); test('Reference input', async () => { const onSubmit = jest.fn(); await setup({ questionnaire: { resourceType: 'Questionnaire', status: 'active', item: [ { linkId: 'q1', type: QuestionnaireItemType.reference, text: 'q1', }, ], }, onSubmit, }); const input = screen.getByPlaceholderText('Resource Type'); expect(input).toBeInTheDocument(); await act(async () => { fireEvent.click(screen.getByText('Submit')); }); expect(onSubmit).toHaveBeenCalled(); }); test('Drop down choice input', async () => { const onSubmit = jest.fn(); await setup({ questionnaire: { resourceType: 'Questionnaire', status: 'active', item: [ { linkId: 'q1', type: QuestionnaireItemType.choice, extension: [ { url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl', valueCodeableConcept: { coding: [ { system: 'http://hl7.org/fhir/questionnaire-item-control', code: 'drop-down', display: 'Drop down', }, ], text: 'Drop down', }, }, ], text: 'q1', answerOption: [ { valueString: 'a1', }, { valueString: 'a2', }, ], }, ], }, onSubmit, }); expect(screen.getByText('q1')).toBeInTheDocument(); const dropDown = screen.getByLabelText('q1'); expect(dropDown).toBeInTheDocument(); expect(dropDown).toBeInstanceOf(HTMLSelectElement); await act(async () => { // fireEvent.click(screen.getByLabelText('a1')); fireEvent.change(dropDown, { target: { value: 'a1' } }); }); await act(async () => { fireEvent.click(screen.getByText('Submit')); }); const response1 = onSubmit.mock.calls[0][0]; const answers1 = getQuestionnaireAnswers(response1); expect(answers1['q1']).toMatchObject({ valueString: 'a1' }); await act(async () => { fireEvent.change(dropDown, { target: { value: 'a2' } }); }); await act(async () => { fireEvent.click(screen.getByText('Submit')); }); const response2 = onSubmit.mock.calls[1][0]; const answers2 = getQuestionnaireAnswers(response2); expect(answers2['q1']).toMatchObject({ valueString: 'a2' }); await act(async () => { fireEvent.change(dropDown, { target: { value: '' } }); }); await act(async () => { fireEvent.click(screen.getByText('Submit')); }); const response3 = onSubmit.mock.calls[2][0]; const answers3 = getQuestionnaireAnswers(response3); expect(answers3['q1']).toMatchObject({}); }); test('referenceResource extension with valueCodeableConcept', async () => { const onSubmit = jest.fn(); await setup({ questionnaire: { resourceType: 'Questionnaire', status: 'active', item: [ { linkId: 'q1', type: QuestionnaireItemType.reference, extension: [ { url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-referenceResource', valueCodeableConcept: { coding: [ { system: 'http://hl7.org/fhir/fhir-types', display: 'Patient', code: 'Patient', }, { system: 'http://hl7.org/fhir/fhir-types', display: 'Organization', code: 'Organization', }, ], }, }, ], }, ], }, onSubmit, }); expect(screen.getByText('Patient')).toBeInTheDocument(); await act(async () => { fireEvent.click(screen.getByText('Patient')); }); }); test('referenceResource extension with valueCode', async () => { const onSubmit = jest.fn(); await setup({ questionnaire: { resourceType: 'Questionnaire', status: 'active', item: [ { linkId: 'q1', type: QuestionnaireItemType.reference, extension: [ { url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-referenceResource', valueCode: 'Patient', }, ], }, ], }, onSubmit, }); expect(screen.queryByText('Patient')).not.toBeInTheDocument(); }); test('Drop down choice input default value', async () => { const onSubmit = jest.fn(); await setup({ questionnaire: { resourceType: 'Questionnaire', status: 'active', item: [ { linkId: 'q1', type: QuestionnaireItemType.choice, extension: [ { url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl', valueCodeableConcept: { coding: [ { system: 'http://hl7.org/fhir/questionnaire-item-control', code: 'drop-down', display: 'Drop down', }, ], text: 'Drop down', }, }, ], text: 'q1', answerOption: [ { valueString: 'a1', }, { valueString: 'a2', }, ], initial: [ { valueString: 'a2', }, ], }, ], }, onSubmit, }); expect(screen.getByText('q1')).toBeInTheDocument(); const dropDown = screen.getByLabelText('q1'); expect(dropDown).toBeInTheDocument(); expect(dropDown).toBeInstanceOf(HTMLSelectElement); expect((dropDown as HTMLSelectElement).value).toBe('a2'); }); test('Drop down choice input default reference value', async () => { const onSubmit = jest.fn(); await setup({ questionnaire: { resourceType: 'Questionnaire', status: 'active', item: [ { linkId: 'q1', type: QuestionnaireItemType.choice, extension: [ { url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl', valueCodeableConcept: { coding: [ { system: 'http://hl7.org/fhir/questionnaire-item-control', code: 'drop-down', display: 'Drop down', }, ], text: 'Drop down', }, }, ], text: 'q1', answerOption: [ { valueString: 'a1', }, { valueReference: { reference: 'Organization/123', display: 'Test Organization', }, }, ], initial: [ { valueReference: { reference: 'Organization/123', display: 'Test Organization', }, }, ], }, ], }, onSubmit, }); expect(screen.getByText('q1')).toBeInTheDocument(); const dropDown = screen.getByLabelText('q1'); expect(dropDown).toBeInTheDocument(); expect(dropDown).toBeInstanceOf(HTMLSelectElement); expect((dropDown as HTMLSelectElement).value).toBe('Test Organization'); }); test('Page Sequence', async () => { const visibleQuestion = 'Visible Question'; const hiddenQuestion = 'Hidden Question'; await setup({ questionnaire: { id: 'groups-example', resourceType: 'Questionnaire', status: 'active', title: 'Groups Example', item: [ { linkId: 'q1', type: QuestionnaireItemType.group, text: 'Visible Sequence', item: [ { linkId: 'question1', text: visibleQuestion, type: 'string', required: true, }, { linkId: 'question2-string', text: 'visible question 2', type: 'string', }, ], extension: pageExtension, }, { linkId: 'q2', type: QuestionnaireItemType.group, text: 'Hidden Sequence', item: [ { linkId: 'question2', text: hiddenQuestion, type: 'string', }, ], extension: pageExtension, }, ], }, onSubmit: jest.fn(), }); const visibleQuestionInput = screen.getByLabelText(visibleQuestion + ' *'); expect(visibleQuestionInput).toBeInTheDocument(); const nextButton = screen.getByText('Next'); expect(nextButton).toBeInTheDocument(); // Try to click "Next" without answering the question. // This should fail, because the question is required. await act(async () => { fireEvent.click(nextButton); }); expect(visibleQuestionInput).toBeInTheDocument(); fireEvent.change(visibleQuestionInput, { target: { value: 'Test Value' } }); expect((visibleQuestionInput as HTMLInputElement).value).toBe('Test Value'); const question2StringInput = screen.getByLabelText('visible question 2'); fireEvent.change(question2StringInput, { target: { value: 'Test Value for Question2-String' } }); expect((question2StringInput as HTMLInputElement).value).toBe('Test Value for Question2-String'); await act(async () => { fireEvent.click(nextButton); }); // Check that the texts for visibleQuestion and question2-string are no longer in the document. expect(screen.queryByText(visibleQuestion)).not.toBeInTheDocument(); expect(screen.queryByText('visible question 2')).not.toBeInTheDocument(); // Check that the hidden text is now visible. expect(screen.getByText(hiddenQuestion)).toBeInTheDocument(); await act(async () => { fireEvent.click(screen.getByText('Back')); }); // Check that the values in the visibleQuestion and question2-string inputs are still the same. const updatedVisibleQuestionInput = screen.getByLabelText(visibleQuestion + ' *') as HTMLInputElement; expect(updatedVisibleQuestionInput.value).toBe('Test Value'); const updatedQuestion2StringInput = screen.getByLabelText('visible question 2') as HTMLInputElement; expect(updatedQuestion2StringInput.value).toBe('Test Value for Question2-String'); }); test('Page Sequence with non page items in root', async () => { const visibleQuestion = 'Visible Question'; const hiddenQuestion = 'Hidden Question'; await setup({ questionnaire: { id: 'groups-example', resourceType: 'Questionnaire', status: 'active', title: 'Groups Example', item: [ { linkId: 'q1', type: QuestionnaireItemType.group, text: 'Visible Sequence', item: [ { linkId: 'question1', text: visibleQuestion, type: 'string', }, ], extension: pageExtension, }, { linkId: 'q2', text: 'Question Choice', type: 'choice', answerOption: [ { valueString: 'Yes', }, { valueString: 'No', }, ], }, { linkId: 'q3', type: QuestionnaireItemType.group, text: 'Hidden Sequence', item: [ { linkId: 'question2', text: hiddenQuestion, type: 'string', }, ], extension: pageExtension, }, ], }, onSubmit: jest.fn(), }); // The form should render expect(screen.getByText(visibleQuestion)).toBeInTheDocument(); // The hidden text should be hidden expect(screen.queryByText(hiddenQuestion)).not.toBeInTheDocument(); await act(async () => { fireEvent.click(screen.getByText('Next')); }); expect(screen.queryByText(visibleQuestion)).not.toBeInTheDocument(); // The hidden text should still not be visible expect(screen.queryByText(hiddenQuestion)).not.toBeInTheDocument(); expect(screen.getByText('Yes')).toBeInTheDocument(); expect(screen.getByText('Back')).toBeInTheDocument(); await act(async () => { fireEvent.click(screen.getByText('Next')); }); // The hidden text should now be visible expect(screen.getByText(hiddenQuestion)).toBeInTheDocument(); expect(screen.getByText('Back')).toBeInTheDocument(); }); test('Value Set Choice', async () => { const onSubmit = jest.fn(); await setup({ questionnaire: { resourceType: 'Questionnaire', status: 'active', id: 'value-set-example', title: 'Valueset', item: [ { linkId: 'q1', text: 'Value Set', type: 'choice', answerValueSet: 'http://example.com/valueset', }, ], }, onSubmit, }); const input = screen.getByRole('searchbox') as HTMLInputElement; expect(input).toBeInTheDocument(); await act(async () => { fireEvent.change(input, { target: { value: 'Test' } }); }); // Wait for the drop down await act(async () => { jest.advanceTimersByTime(1000); }); // Press the down arrow await act(async () => { fireEvent.keyDown(input, { key: 'ArrowDown', code: 'ArrowDown' }); }); // Press "Enter" await act(async () => { fireEvent.keyDown(input, { key: 'Enter', code: 'Enter', charCode: 13 }); }); await act(async () => { fireEvent.click(screen.getByText('Submit')); }); expect(onSubmit).toHaveBeenCalled(); const response = onSubmit.mock.calls[0][0]; const answer = getQuestionnaireAnswers(response); expect(answer['q1']).toMatchObject({ valueCoding: { code: 'test-code', display: 'Test Display', system: 'x' }, }); }); test('Dropdown with no value set or options', async () => { await setup({ questionnaire: { resourceType: 'Questionnaire', status: 'active', id: 'no-options-dropdown', title: 'No Options Dropdown Example', item: [ { linkId: 'choices', text: 'Choices', type: 'choice', extension: [ { url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl', valueCodeableConcept: { coding: [ { system: 'http://hl7.org/fhir/questionnaire-item-control', code: 'drop-down', display: 'Drop down', }, ], text: 'Drop down', }, }, ], }, ], }, onSubmit: jest.fn(), }); expect(screen.getByPlaceholderText('No Answers Defined')).toBeInTheDocument(); }); test('Value Set Checkbox', async () => { const onSubmit = jest.fn(); // Mock the value set expansion to return more than 30 items const mockValueSet = { resourceType: 'ValueSet', expansion: { contains: Array.from({ length: 35 }, (_, i) => ({ system: 'x', code: `test-code-${i}`, display: `Test Display ${i}`, })), }, }; // Mock the medplum client's valueSetExpand method medplum.valueSetExpand = jest.fn().mockResolvedValue(mockValueSet); await setup({ questionnaire: { resourceType: 'Questionnaire', status: 'active', id: 'value-set-checkbox', title: 'Value Set Checkbox', item: [ { linkId: 'q1', text: 'Value Set Checkbox', type: 'choice', answerValueSet: 'http://example.com/valueset', extension: [ { url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl', valueCodeableConcept: { coding: [ { system: 'http://hl7.org/fhir/questionnaire-item-control', code: 'check-box', display: 'Check Box', }, ], }, }, ], }, ], }, onSubmit, }); // Wait for value set to load await act(async () => { jest.advanceTimersByTime(1000); }); // Check that checkboxes are rendered const checkboxes = screen.getAllByRole('checkbox'); expect(checkboxes.length).toBeGreaterThan(0); // Verify that the cutoff message is shown const cutoffMessage = screen.getByText(/Showing first 30 options/); expect(cutoffMessage).toBeInTheDocument(); // Click the first checkbox await act(async () => { fireEvent.click(checkboxes[0]); }); // Click the second checkbox await act(async () => { fireEvent.click(checkboxes[1]); }); // Click the third checkbox await act(async () => { fireEvent.click(checkboxes[2]); }); await act(async () => { fireEvent.click(screen.getByText('Submit')); }); expect(onSubmit).toHaveBeenCalled(); const response = onSubmit.mock.calls[0][0]; const answer = getAllQuestionnaireAnswers(response); expect(answer['q1']).toBeDefined(); expect(answer['q1'][0]).toMatchObject({ valueCoding: { code: 'test-code-0', display: 'Test Display 0', system: 'x' }, }); expect(answer['q1'][2]).toMatchObject({ valueCoding: { code: 'test-code-2', display: 'Test Display 2', system: 'x' }, }); }); test('Value Set Radio Button', async () => { const onSubmit = jest.fn(); await setup({ questionnaire: { resourceType: 'Questionnaire', status: 'active', id: 'value-set-radio', title: 'Value Set Radio', item: [ { linkId: 'q1', text: 'Value Set Radio', type: 'choice', answerValueSet: 'http://example.com/valueset', extension: [ { url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl', valueCodeableConcept: { coding: [ { system: 'http://hl7.org/fhir/questionnaire-item-control', code: 'radio-button', display: 'Radio Button', }, ], }, }, ], }, ], }, onSubmit, }); // Wait for value set to load await act(async () => { jest.advanceTimersByTime(1000); }); // Check that radio buttons are rendered const radioButtons = screen.getAllByRole('radio'); expect(radioButtons.length).toBeGreaterThan(0); // Click the first radio button await act(async () => { fireEvent.click(radioButtons[0]); }); await act(async () => { fireEvent.click(screen.getByText('Submit')); }); expect(onSubmit).toHaveBeenCalled(); const response = onSubmit.mock.calls[0][0]; const answer = getQuestionnaireAnswers(response); expect(answer['q1']).toMatchObject({ valueCoding: { code: 'test-code-0', display: 'Test Display 0', system: 'x' }, }); }); test('Non-Value Set Checkbox', async () => { const onSubmit = jest.fn(); await setup({ questionnaire: { resourceType: 'Questionnaire', status: 'active', id: 'non-valueset-checkbox', title: 'Non-Value Set Checkbox', item: [ { linkId: 'q1', text: 'Non-Value Set Checkbox', type: 'choice', repeats: true, answerOption: [ { valueString: 'Option 1', }, { valueString: 'Option 2', }, { valueString: 'Option 3', }, ], extension: [ { url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl', valueCodeableConcept: { coding: [ { system: 'http://hl7.org/fhir/questionnaire-item-control', code: 'check-box', display: 'Check Box', }, ], }, }, ], }, ], }, onSubmit, }); // Check that checkboxes are rendered const checkboxes = screen.getAllByRole('checkbox'); expect(checkboxes.length).toBe(3); // Select multiple options await act(async () => { fireEvent.click(checkboxes[0]); // Option 1 fireEvent.click(checkboxes[2]); // Option 3 }); await act(async () => { fireEvent.click(screen.getByText('Submit')); }); expect(onSubmit).toHaveBeenCalled(); const response = onSubmit.mock.calls[0][0]; expect(response.item[0].answer).toHaveLength(2); expect(response.item[0].answer[0]).toMatchObject({ valueString: 'Option 1' }); expect(response.item[0].answer[1]).toMatchObject({ valueString: 'Option 3' }); }); test('Checkbox Selection and Deselection', async () => { const onSubmit = jest.fn(); await setup({ questionnaire: { resourceType: 'Questionnaire', status: 'active', id: 'checkbox-selection-deselection', title: 'Checkbox Selection and Deselection', item: [ { linkId: 'q1', text: 'Select Options', type: 'choice', repeats: true, answerOption: [ { valueString: 'Option 1', }, { valueString: 'Option 2', }, { valueString: 'Option 3', }, ], extension: [ { url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl', valueCodeableConcept: { coding: [ { system: 'http://hl7.org/fhir/questionnaire-item-control', code: 'check-box', display: 'Check Box', }, ], }, }, ], }, ], }, onSubmit, }); // Check that checkboxes are rendered const checkboxes = screen.getAllByRole('checkbox'); expect(checkboxes.length).toBe(3); // Select all options await act(async () => { checkboxes.forEach((checkbox) => { fireEvent.click(checkbox); }); }); // Submit with all options selected await act(async () => { fireEvent.click(screen.getByText('Submit')); }); // Verify all options are selected in the response expect(onSubmit).toHaveBeenCalledTimes(1); const response1 = onSubmit.mock.calls[0][0]; expect(response1.item[0].answer).toHaveLength(3); expect(response1.item[0].answer).toEqual( expect.arrayContaining([{ valueString: 'Option 1' }, { valueString: 'Option 2' }, { valueString: 'Option 3' }]) ); // Deselect all options await act(async () => { checkboxes.forEach((checkbox) => { fireEvent.click(checkbox); }); }); // Submit with no options selected await act(async () => { fireEvent.click(screen.getByText('Submit')); }); // Verify no options are selected in the response expect(onSubmit).toHaveBeenCalledTimes(2); const response2 = onSubmit.mock.calls[1][0]; expect(response2.item[0].answer).toHaveLength(1); expect(response2.item[0].answer).toEqual(expect.arrayContaining([{}])); }); test('Multi-Select Dropdown Value Set', async () => { const onSubmit = jest.fn(); // Mock the value set expansion to return multiple items const mockValueSet = { resourceType: 'ValueSet', expansion: { contains: Array.from({ length: 5 }, (_, i) => ({ system: 'x', code: `test-code-${i}`, display: `Test Display ${i}`, })), }, }; // Mock the medplum client's valueSetExpand method medplum.valueSetExpand = jest.fn().mockResolvedValue(mockValueSet); await setup({ questionnaire: { resourceType: 'Questionnaire', status: 'active', id: 'multi-select-dropdown-valueset', title: 'Multi-Select Dropdown Value Set', item: [ { linkId: 'q1', text: 'Multi-Select Dropdown Value Set', type: 'choice', repeats: true, answerValueSet: 'http://example.com/valueset', extension: [ { url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl', valueCodeableConcept: { coding: [ { system: 'http://hl7.org/fhir/questionnaire-item-control', code: 'drop-down', display: 'Drop down', }, ], }, }, ], }, ], }, onSubmit, }); // Wait for value set to load await act(async () => { jest.advanceTimersByTime(1000); }); const searchInput = screen.getByPlaceholderText('Select items'); expect(searchInput).toBeInTheDocument(); // Select first option await act(async () => { fireEvent.change(searchInput, { target: { value: 'Test Display 0' } }); }); // Wait for options to load await act(async () => { jest.advanceTimersByTime(1000); }); // Select the first option await act(async () => { fireEvent.keyDown(searchInput, { key: 'ArrowDown', code: 'ArrowDown' }); fireEvent.keyDown(searchInput, { key: 'Enter', code: 'Enter' }); }); // Clear input for second selection await act(async () => { fireEvent.change(searchInput, { target: { value: '' } }); }); // Select second option await act(async () => { fireEvent.change(searchInput, { target: { value: 'Test Display 2' } }); }); // Wait for options to load await act(async () => { jest.advanceTimersByTime(1000); }); // Select the second option (press ArrowDown twice to get to the third option) await act(async () => { fireEvent.keyDown(searchInput, { key: 'ArrowDown', code: 'ArrowDown' }); fireEvent.keyDown(searchInput, { key: 'ArrowDown', code: 'ArrowDown' }); fireEvent.keyDown(searchInput, { key: 'Enter', code: 'Enter' }); }); // Submit the form await act(async () => { fireEvent.click(screen.getByText('Submit')); }); expect(onSubmit).toHaveBeenCalled(); const response = onSubmit.mock.calls[0][0]; expect(response.item[0].answer).toHaveLength(2); expect(response.item[0].answer[0]).toMatchObject({ valueCoding: { code: 'test-code-0', display: 'Test Display 0', system: 'x' }, }); expect(response.item[0].answer[1]).toMatchObject({ valueCoding: { code: 'test-code-2', display: 'Test Display 2', system: 'x' }, }); }); test('Repeated Choice Dropdown', async () => { await setup({ questionnaire: { resourceType: 'Questionnaire', status: 'active', id: 'default-values', title: 'Default Values Example', item: [ { id: 'choice', linkId: 'choice', text: 'choice', type: 'choice', answerOption: [ { valueString: 'Yes', }, { valueString: 'No', }, ], repeats: true, extension: [ { url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl', valueCodeableConcept: { coding: [ { system: 'http://hl7.org/fhir/questionnaire-item-control', code: 'drop-down', display: 'Drop down', }, ], text: 'Drop down', }, }, ], }, ], }, onSubmit: jest.fn(), }); const dropDown = screen.getByText('choice'); await act(async () => { fireEvent.click(dropDown); }); await act(async () => { fireEvent.change(dropDown, { target: 'Yes' }); }); await act(async () => { fireEvent.change(dropDown, { target: 'No' }); }); }); test('Conditional question', async () => { await setup({ questionnaire: { resourceType: 'Questionnaire', status: 'active', id: 'enable-when', title: 'Enable When Example', item: [ { linkId: 'q1', text: 'Question 1', type: 'choice', answerOption: [ { valueString: 'Yes', }, { valueString: 'No', }, ], }, { linkId: 'q2', type: 'display', text: 'Hidden Text', enableWhen: [ { question: 'q1', operator: '=', answerString: 'Yes', }, ], }, ], }, onSubmit: jest.fn(), }); // The form should render expect(screen.getByText('Question 1')).toBeInTheDocument(); // The hidden text should be hidden expect(screen.queryByText('Hidden Text')).not.toBeInTheDocument(); // Click on "No" await act(async () => { fireEvent.click(screen.getByLabelText('No')); }); // The hidden text should still be hidden expect(screen.queryByText('Hidden Text')).not.toBeInTheDocument(); // Click on "Yes" await act(async () => { fireEvent.click(screen.getByLabelText('Yes')); }); // Now the hidden text should be visible expect(screen.queryByText('Hidden Text')).toBeInTheDocument(); }); test('Multi Select', async () => { await setup({ questionnaire: { resourceType: 'Questionnaire', status: 'active', id: 'multi-select', title: 'Multi Select Example', item: [ { linkId: 'group1', type: 'group', text: 'Group 1', item: [ { linkId: 'q1', type: QuestionnaireItemType.choice, repeats: true, extension: [ { url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl', valueCodeableConcept: { coding: [ { system: 'http://hl7.org/fhir/questionnaire-item-control', code: 'drop-down', display: 'Drop down', }, ], text: 'Drop down', }, }, ], text: 'q1', answerOption: [ { valueString: 'a1', }, { valueString: 'a2', }, ], }, ], extension: pageExtension, }, { linkId: 'group2', type: 'group', text: 'Group 2', extension: pageExtension, item: [ { linkId: 'string', type: 'string', text: 'string', }, ], }, ], }, onSubmit: jest.fn(), }); expect(screen.getByText('q1')).toBeInTheDocument(); const searchInput = screen.getByPlaceholderText('Select items'); expect(searchInput).toBeInTheDocument(); expect(searchInput).toBeInstanceOf(HTMLInputElement); await act(async () => { fireEvent.change(searchInput, { target: { value: 'a1' } }); }); await act(async () => { fireEvent.click(screen.getByText('Next')); }); await act(async () => { fireEvent.click(screen.getByText('Back')); }); const searchInput1 = screen.getByPlaceholderText('Select items'); expect(searchInput1).toBeInTheDocument(); }); test('Multi Select shows with no data', async () => { await setup({ questionnaire: { resourceType: 'Questionnaire', status: 'active', item: [ { linkId: 'q1', type: QuestionnaireItemType.choice, repeats: true, extension: [ { url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl', valueCodeableConcept: { coding: [ { system: 'http://hl7.org/fhir/questionnaire-item-control', code: 'drop-down', display: 'Drop down', }, ], text: 'Drop down', }, }, ], text: 'q1', answerOption: [], }, ], }, onSubmit: jest.fn(), }); expect(screen.getByText('q1')).toBeInTheDocument(); const searchInput = screen.getByPlaceholderText('No Answers Defined'); expect(searchInput).toBeInTheDocument(); expect(searchInput).toBeInstanceOf(HTMLInputElement); }); test('Multi Select Code', async () => { await setup({ questionnaire: { resourceType: 'Questionnaire', status: 'active', id: 'default-values', title: 'Default Values Example', item: [ { id: 'choice', linkId: 'choice', text: 'choice', type: 'choice', answerOption: [ { valueString: 'Yes', }, { valueString: 'No', }, ], extension: [ { url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl', valueCodeableConcept: { coding: [ { system: 'http://hl7.org/fhir/questionnaire-item-control', code: 'multi-select', display: 'Multi Select', }, ], text: 'Multi Select', }, }, ], }, ], }, onSubmit: jest.fn(), }); const dropDown = screen.getByText('choice'); await act(async () => { fireEvent.click(dropDown); }); await act(async () => { fireEvent.change(dropDown, { target: 'Yes' }); }); await act(async () => { fireEvent.change(dropDown, { target: 'No' }); }); }); test('Multi Select Code shows with no data', async () => { await setup({ questionnaire: { resourceType: 'Questionnaire', status: 'active', item: [ { linkId: 'q1', type: QuestionnaireItemType.choice, extension: [ { url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl', valueCodeableConcept: { coding: [ { system: 'http://hl7.org/fhir/questionnaire-item-control', code: 'multi-select', display: 'Multi Select', }, ], text: 'Multi Select', }, }, ], text: 'q1', answerOption: [], }, ], }, onSubmit: jest.fn(), }); expect(screen.getByText('q1')).toBeInTheDocument(); const searchInput = screen.getByPlaceholderText('No Answers Defined'); expect(searchInput).toBeInTheDocument(); expect(searchInput).toBeInstanceOf(HTMLInputElement); }); test('Nested repeat', async () => { const onSubmit = jest.fn(); await setup({ questionnaire: { resourceType: 'Questionnaire', status: 'active', id: 'pages-example', title: 'Pages Example', item: [ { linkId: 'group1', type: 'group', text: 'Outside Group', repeats: true, item: [ { linkId: 'group2', type: 'group', text: 'Inside Group', repeats: true, item: [ { linkId: 'question1', type: 'string', text: 'question1', }, ], }, ], }, ], }, onSubmit, }); expect(screen.getByText('Outside Group')).toBeInTheDocument(); const outsideGroupButton = screen.getAllByText('Add Group: Outside Group'); const insideGroupButton = screen.getAllByText('Add Group: Inside Group'); expect(outsideGroupButton).toHaveLength(1); expect(insideGroupButton).toHaveLength(1); await act(async () => { fireEvent.click(outsideGroupButton[0]); }); expect(screen.getAllByText('Outside Group').length).toBe(2); expect(screen.getAllByText('Inside Group').length).toBe(2); const stringInputs = screen.getAllByText('question1'); expect(stringInputs).toHaveLength(2); await act(async () => { fireEvent.change(screen.getByLabelText('question1'), { target: { value: 'answer1' } }); }); await act(async () => { fireEvent.click(screen.getByText('Submit')); }); expect(onSubmit).toHaveBeenCalled(); const response = onSubmit.mock.calls[0][0]; expect(response.item[0].item[0].item[0].answer[0].valueString).toEqual('answer1'); }); test('repeatableQuestion', async () => { await setup({ questionnaire: { resourceType: 'Questionnaire', status: 'active', id: 'repeatable-when', title: 'repeatable Questionnaire', item: [ { linkId: 'question1', text: 'Question 1', type: 'string', repeats: true, }, { linkId: 'group1', text: 'Question Group', type: 'group', repeats: true, item: [ { linkId: 'question2', text: 'Question 2', type: 'string', }, ], }, ], }, onSubmit: jest.fn(), }); await act(async () => { fireEvent.change(screen.getByLabelText('Question 1'), { target: { value: 'answer' } }); }); await act(async () => { fireEvent.click(screen.getByText('Add Group: Question Group')); }); expect(screen.getAllByText('Question Group').length).toBe(2); expect(screen.getAllByText('Question 2').length).toBe(2); }); test('No Answers Defined', async () => { await setup({ questionnaire: { resourceType: 'Questionnaire', status: 'active', id: 'no-answers', title: 'No Answers Example', item: [ { linkId: 'choices', text: 'Choices', type: 'choice', extension: [ { url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl', valueCodeableConcept: { coding: [ { system: 'http://hl7.org/fhir/questionnaire-item-control', code: 'drop-down', display: 'Drop down', }, ], text: 'Drop down', }, }, ], }, ], }, onSubmit: jest.fn(), }); expect(screen.getByPlaceholderText('No Answers Defined')).toBeInTheDocument(); }); test('Empty Array of Answer Options', async () => { await setup({ questionnaire: { resourceType: 'Questionnaire', status: 'active', id: 'empty-answers', title: 'Empty Answers Example', item: [ { linkId: 'choices', text: 'Choices', type: 'choice', answerOption: [], extension: [ { url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl', valueCodeableConcept: { coding: [ { system: 'http://hl7.org/fhir/questionnaire-item-control', code: 'drop-down', display: 'Drop down', }, ], text: 'Drop down', }, }, ], }, ], }, onSubmit: jest.fn(), }); expect(screen.getByPlaceholderText('No Answers Defined')).toBeInTheDocument(); }); test('Empty Answer Options for Radio', async () => { await setup({ questionnaire: { resourceType: 'Questionnaire', status: 'active', id: 'empty-radio', title: 'Empty Radio Example', item: [ { linkId: 'choices', text: 'Choices', type: 'choice', answerOption: [], }, ], }, onSubmit: jest.fn(), }); expect(screen.getByPlaceholderText('No Answers Defined')).toBeInTheDocument(); }); test('Reference filter', async () => { await setup({ questionnaire: { resourceType: 'Questionnaire', status: 'active', id: 'reference-filter', item: [ { linkId: 'q1', type: 'reference', text: 'Question', extension: [ { url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-referenceResource', valueCode: 'Observation', }, { url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-referenceFilter', valueString: 'subject=$subj', }, ], }, ], }, subject: { reference: 'Patient/123' }, onSubmit: jest.fn(), }); // Add a spy on medplum.searchResources const searchResources = jest.spyOn(medplum, 'searchResources'); // Get the search input const input = screen.getByRole('searchbox') as HTMLInputElement; // Enter "Simpson" await act(async () => { fireEvent.change(input, { target: { value: 'Test' } }); }); // Wait for the drop down await act(async () => { jest.advanceTimersByTime(1000); }); expect(screen.getByText('Test 1')).toBeDefined(); expect(searchResources).toHaveBeenCalledTimes(1); expect(searchResources.mock.calls[0][0]).toBe('Observation'); expect(searchResources.mock.calls[0][1]).toBeInstanceOf(URLSearchParams); const params = searchResources.mock.calls[0][1] as URLSearchParams; expect(params.get('subject')).toBe('Patient/123'); expect(params.get('code')).toBe('Test'); }); test('Questionnaire CalculatedExpression with boolean field', async () => { const onSubmit = jest.fn(); await setup({ questionnaire: { resourceType: 'Questionnaire', status: 'active', id: 'temperature-threshold', title: 'Temperature Threshold Check', item: [ { id: 'id-1', linkId: 'q1', type: 'string', text: 'Fahrenheit', }, { id: 'id-2', linkId: 'q2', type: 'boolean', text: 'Is temperature over 120?', extension: [ { url: 'http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-calculatedExpression', valueExpression: { language: 'text/fhirpath', expression: "iif(%resource.item.where(linkId='q1').answer.value.empty(), false, (%resource.item.where(linkId='q1').answer.value.toDecimal() > 120))", }, }, ], }, ], }, onSubmit, }); await act(async () => { fireEvent.change(screen.getByLabelText('Fahrenheit'), { target: { value: '125' } }); }); await act(async () => { fireEvent.click(screen.getByText('Submit')); }); expect(onSubmit).toHaveBeenCalled(); const response = onSubmit.mock.calls[0][0]; const answers = getQuestionnaireAnswers(response); expect(answers['q1']).toMatchObject({ valueString: '125' }); expect(answers['q2']).toMatchObject({ valueBoolean: true }); await act(async () => { fireEvent.change(screen.getByLabelText('Fahrenheit'), { target: { value: '100' } }); }); await act(async () => { fireEvent.click(screen.getByText('Submit')); }); const response2 = onSubmit.mock.calls[1][0]; const answers2 = getQuestionnaireAnswers(response2); expect(answers2['q1']).toMatchObject({ valueString: '100' }); expect(answers2['q2']).toMatchObject({ valueBoolean: false }); await act(async () => { fireEvent.change(screen.getByLabelText('Fahrenheit'), { target: { value: '' } }); }); await act(async () => { fireEvent.click(screen.getByText('Submit')); }); const response3 = onSubmit.mock.calls[2][0]; const answers3 = getQuestionnaireAnswers(response3); expect(answers3['q1']).toBeUndefined(); expect(answers3['q2']).toMatchObject({ valueBoolean: false }); }); test('Questionnaire CalculatedExpression failed to evaluate expression', async () => { const onSubmit = jest.fn(); await setup({ questionnaire: { resourceType: 'Questionnaire', status: 'active', id: 'temperature-threshold', title: 'Temperature Threshold Check', item: [ { id: 'id-1', linkId: 'q1', type: 'string', text: 'Fahrenheit', }, { id: 'id-2', linkId: 'q2', type: 'boolean', text: 'Is temperature over 120?', extension: [ { url: 'http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-calculatedExpression', valueExpression: { language: 'text/fhirpath', expression: '%fail', }, }, ], }, ], }, onSubmit, }); await act(async () => { fireEvent.change(screen.getByLabelText('Fahrenheit'), { target: { value: '125' } }); }); expect( screen.getByText('Expression evaluation failed: FhirPathError on "%fail": Error: Undefined variable %fail') ).toBeInTheDocument(); }); test('Questionnaire CalculatedExpression with nested groups', async () => { const onSubmit = jest.fn(); await setup({ questionnaire: { resourceType: 'Questionnaire', status: 'active', id: 'temperature-conversion', title: 'Temperature Conversion', item: [ { id: 'id-6', linkId: 'g6', type: 'group', text: 'Temperature Group', item: [ { id: 'id-1', linkId: 'q1', type: 'decimal', text: 'Fahrenheit', }, { id: 'id-2', linkId: 'q2', type: 'decimal', text: 'Celsius', extension: [ { url: 'http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-calculatedExpression', valueExpression: { language: 'text/fhirpath', expression: "iif(%resource.item.where(linkId='g6').item.where(linkId='q1').answer.value.empty(), '', ((%resource.item.where(linkId='g6').item.where(linkId='q1').answer.value - 32) * 5 / 9).round(2))", }, }, ], }, { id: 'id-3', linkId: 'q3', type: 'decimal', text: 'Kelvin', extension: [ { url: 'http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-calculatedExpression', valueExpression: { language: 'text/fhirpath', expression: "iif(%resource.item.where(linkId='g6').item.where(linkId='q1').answer.value.empty(), '', (((%resource.item.where(linkId='g6').item.where(linkId='q1').answer.value - 32) * 5 / 9) + 273.15).round(2))", }, }, ], }, ], }, ], }, onSubmit, }); await act(async () => { fireEvent.change(screen.getByLabelText('Fahrenheit'), { target: { value: '100' } }); }); await act(async () => { fireEvent.click(screen.getByText('Submit')); }); expect(onSubmit).toHaveBeenCalled(); const response = onSubmit.mock.calls[0][0]; const answers = getQuestionnaireAnswers(response); expect(answers['q1']).toMatchObject({ valueDecimal: 100 }); // Original Fahrenheit value expect(answers['q2']).toMatchObject({ valueDecimal: 37.78 }); // Calculated Celsius expect(answers['q3']).toMatchObject({ valueDecimal: 310.93 }); // Calculated Kelvin }); test('Questionnaire CalculatedExpression with nested groups and QuestionnaireResponse', async () => { const onSubmit = jest.fn(); await setup({ questionnaire: { resourceType: 'Questionnaire', status: 'active', id: 'temperature-conversion', title: 'Temperature Conversion', item: [ { id: 'id-6', linkId: 'g6', type: 'group', text: 'Temperature Group', item: [ { id: 'id-1', linkId: 'q1', type: 'decimal', text: 'Fahrenheit', }, { id: 'id-2', linkId: 'q2', type: 'decimal', text: 'Celsius', extension: [ { url: 'http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-calculatedExpression', valueExpression: { language: 'text/fhirpath', expression: "iif(%resource.item.where(linkId='g6').item.where(linkId='q1').answer.value.empty(), '', ((%resource.item.where(linkId='g6').item.where(linkId='q1').answer.value - 32) * 5 / 9).round(2))", }, }, ], }, { id: 'id-3', linkId: 'q3', type: 'decimal', text: 'Kelvin', extension: [ { url: 'http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-calculatedExpression', valueExpression: { language: 'text/fhirpath', expression: "iif(%resource.item.where(linkId='g6').item.where(linkId='q1').answer.value.empty(), '', (((%resource.item.where(linkId='g6').item.where(linkId='q1').answer.value - 32) * 5 / 9) + 273.15).round(2))", }, }, ], }, ], }, ], }, questionnaireResponse: { resourceType: 'QuestionnaireResponse', status: 'in-progress', item: [ { id: 'id-76', linkId: 'g6', text: 'Temperature Group', item: [ { id: 'id-77', linkId: 'q1', text: 'Fahrenheit', answer: [ { valueDecimal: 100, }, ], }, ], answer: [], }, ], }, onSubmit, }); await act(async () => { fireEvent.click(screen.getByText('Submit')); }); expect(onSubmit).toHaveBeenCalled(); const response = onSubmit.mock.calls[0][0]; const answers = getQuestionnaireAnswers(response); expect(answers['q1']).toMatchObject({ valueDecimal: 100 }); // Original Fahrenheit value expect(answers['q2']).toMatchObject({ valueDecimal: 37.78 }); // Calculated Celsius expect(answers['q3']).toMatchObject({ valueDecimal: 310.93 }); // Calculated Kelvin }); test('Required radio button choice validation', async () => { const onSubmit = jest.fn(); await setup({ questionnaire: { resourceType: 'Questionnaire', status: 'active', item: [ { linkId: 'required-radio', text: 'Required Radio Choice', type: QuestionnaireItemType.choice, required: true, answerOption: [{ valueString: 'Option 1' }, { valueString: 'Option 2' }, { valueString: 'Option 3' }], }, ], }, onSubmit, }); const radioGroup = screen.getByRole('radiogroup'); expect(radioGroup).toBeInTheDocument(); await act(async () => { fireEvent.click(screen.getByText('Submit')); }); expect(onSubmit).not.toHaveBeenCalled(); const radioOption = screen.getByLabelText('Option 1'); await act(async () => { fireEvent.click(radioOption); }); await act(async () => { fireEvent.click(screen.getByText('Submit')); }); expect(onSubmit).toHaveBeenCalled(); const response = onSubmit.mock.calls[0][0]; expect(response.item[0].answer[0].valueString).toBe('Option 1'); }); test('Required dropdown choice validation', async () => { const onSubmit = jest.fn(); await setup({ questionnaire: { resourceType: 'Questionnaire', status: 'active', item: [ { linkId: 'required-dropdown', text: 'Required Dropdown Choice', type: QuestionnaireItemType.choice, required: true, answerOption: [{ valueString: 'Option A' }, { valueString: 'Option B' }, { valueString: 'Option C' }], extension: [ { url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl', valueCodeableConcept: { coding: [ { system: 'http://hl7.org/fhir/questionnaire-item-control', code: 'drop-down', }, ], }, }, ], }, ], }, onSubmit, }); const dropdown = screen.getByLabelText('Required Dropdown Choice *'); expect(dropdown).toBeInTheDocument(); expect(dropdown).toHaveAttribute('required'); await act(async () => { fireEvent.click(screen.getByText('Submit')); }); expect(onSubmit).not.toHaveBeenCalled(); await act(async () => { fireEvent.change(dropdown, { target: { value: 'Option A' } }); }); await act(async () => { fireEvent.click(screen.getByText('Submit')); }); expect(onSubmit).toHaveBeenCalled(); const response = onSubmit.mock.calls[0][0]; expect(response.item[0].answer[0].valueString).toBe('Option A'); }); test('Required value set dropdown validation', async () => { const onSubmit = jest.fn(); await setup({ questionnaire: { resourceType: 'Questionnaire', status: 'active', item: [ { linkId: 'required-valueset-dropdown', text: 'Required Value Set Dropdown', type: QuestionnaireItemType.choice, required: true, answerValueSet: 'http://example.com/valueset', extension: [ { url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl', valueCodeableConcept: { coding: [ { system: 'http://hl7.org/fhir/questionnaire-item-control', code: 'drop-down', }, ], }, }, ], }, ], }, onSubmit, }); const autocomplete = screen.getByRole('searchbox'); expect(autocomplete).toBeInTheDocument(); await act(async () => { fireEvent.click(screen.getByText('Submit')); }); await act(async () => { fireEvent.change(autocomplete, { target: { value: 'Test' } }); }); await act(async () => { fireEvent.click(screen.getByText('Submit')); }); expect(onSubmit).toHaveBeenCalled(); }); test('Required value set radio button validation', async () => { const onSubmit = jest.fn(); await setup({ questionnaire: { resourceType: 'Questionnaire', status: 'active', item: [ { linkId: 'required-valueset-radio', text: 'Required Value Set Radio', type: QuestionnaireItemType.choice, required: true, answerValueSet: 'http://example.com/valueset', extension: [ { url: 'http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl', valueCodeableConcept: { coding: [ { system: 'http://hl7.org/fhir/questionnaire-item-control', code: 'radio-button', }, ], }, }, ], }, ], }, onSubmit, }); await act(async () => { jest.runAllTimers(); }); const radioButtons = screen.getAllByRole('radio'); expect(radioButtons.length).toBeGreaterThan(0); await act(async () => { fireEvent.click(screen.getByText('Submit')); }); expect(onSubmit).not.toHaveBeenCalled(); await act(async () => { fireEvent.click(radioButtons[0]); }); await act(async () => { fireEvent.click(screen.getByText('Submit')); }); expect(onSubmit).toHaveBeenCalled(); }); test('Required boolean field validation', async () => { const onSubmit = jest.fn(); await setup({ questionnaire: { resourceType: 'Questionnaire', status: 'active', item: [ { linkId: 'required-boolean', text: 'Required Boolean Field', type: QuestionnaireItemType.boolean, required: true, }, ], }, onSubmit, }); const checkbox = screen.getByRole('checkbox'); expect(checkbox).toBeInTheDocument(); expect(checkbox).toHaveAttribute('required'); await act(async () => { fireEvent.click(screen.getByText('Submit')); }); expect(onSubmit).not.toHaveBeenCalled(); await act(async () => { fireEvent.click(checkbox); }); expect(checkbox).toBeChecked(); await act(async () => { fireEvent.click(screen.getByText('Submit')); }); expect(onSubmit).toHaveBeenCalled(); const response = onSubmit.mock.calls[0][0]; expect(response.item[0].answer[0].valueBoolean).toBe(true); }); describe('Signature validation', () => { const signatureRequiredQuestionnaire: Questionnaire = { resourceType: 'Questionnaire', status: 'active', extension: [ { url: QUESTIONNAIRE_SIGNATURE_REQUIRED_URL, valueCodeableConcept: { coding: [ { system: 'urn:iso-astm:E1762-95:2013', code: '1.2.840.10065.1.12.1.1', display: "Author's Signature", }, ], }, }, ], item: [ { linkId: 'question1', text: 'Question 1', type: 'string', }, ], }; test('Renders signature input when signature is required', async () => { await setup({ questionnaire: signatureRequiredQuestionnaire, onSubmit: jest.fn(), }); expect(screen.getByLabelText('Signature input area')).toBeInTheDocument(); expect(screen.queryByText('Signature is required.')).not.toBeInTheDocument(); }); test('Does not render signature input when signature is not required', async () => { await setup({ questionnaire: { resourceType: 'Questionnaire', status: 'active', item: [ { linkId: 'question1', text: 'Question 1', type: 'string', }, ], }, onSubmit: jest.fn(), }); expect(screen.queryByLabelText('Signature input area')).not.toBeInTheDocument(); }); test('Shows error message when submit is attempted without signature', async () => { const onSubmit = jest.fn(); await setup({ questionnaire: signatureRequiredQuestionnaire, onSubmit, }); expect(screen.queryByText('Signature is required.')).not.toBeInTheDocument(); await act(async () => { fireEvent.click(screen.getByText('Submit')); }); expect(screen.getByText('Signature is required.')).toBeInTheDocument(); expect(onSubmit).not.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