Skip to main content
Glama
QuestionnaireBuilder.test.tsx26.4 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { MockClient } from '@medplum/mock'; import { MedplumProvider, QuestionnaireItemType } from '@medplum/react-hooks'; import { act, fireEvent, render, screen } from '../test-utils/render'; import type { QuestionnaireBuilderProps } from './QuestionnaireBuilder'; import { QuestionnaireBuilder } from './QuestionnaireBuilder'; const medplum = new MockClient(); async function setup(args: QuestionnaireBuilderProps): Promise<void> { await act(async () => { render( <MedplumProvider medplum={medplum}> <QuestionnaireBuilder {...args} /> </MedplumProvider> ); }); } describe('QuestionnaireBuilder', () => { test('Renders empty', async () => { await setup({ questionnaire: { resourceType: 'Questionnaire', }, onSubmit: jest.fn(), }); expect(screen.getByTestId('questionnaire-form')).toBeDefined(); }); test('Render groups', async () => { await setup({ questionnaire: { resourceType: 'Questionnaire', item: [ { linkId: 'group1', text: 'Group 1', type: 'group', item: [ { linkId: 'question1', text: 'Question 1', type: 'string', }, { linkId: 'question2', text: 'Question 2', type: 'string', }, ], }, { linkId: 'group2', text: 'Group 2', type: 'group', item: [ { linkId: 'question3', text: 'Question 3', type: 'string', }, { linkId: 'question4', text: 'Question 4', type: 'string', }, ], }, ], }, onSubmit: jest.fn(), }); expect(screen.getByTestId('questionnaire-form')).toBeDefined(); expect(screen.getByText('Group 1')).toBeDefined(); expect(screen.getByText('Group 2')).toBeDefined(); }); test('Handles submit', async () => { const onSubmit = jest.fn(); await setup({ questionnaire: { resourceType: 'Questionnaire', item: [ { linkId: 'q1', type: QuestionnaireItemType.string, text: 'q1', }, { linkId: 'q2', type: QuestionnaireItemType.integer, text: 'q1', }, { 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', }, ], }, onSubmit, }); expect(screen.getByText('Save')).toBeDefined(); await act(async () => { fireEvent.click(screen.getByText('Save')); }); expect(onSubmit).toHaveBeenCalled(); }); test('Handles submit with required items', async () => { const onSubmit = jest.fn(); await setup({ questionnaire: { resourceType: 'Questionnaire', item: [ { linkId: 'q1', type: QuestionnaireItemType.string, required: true, text: 'q1', }, { linkId: 'q2', type: QuestionnaireItemType.integer, required: true, text: 'q1', }, { linkId: 'q3', type: QuestionnaireItemType.date, required: true, text: 'q3', }, ], }, onSubmit, }); expect(screen.getByText('Save')).toBeDefined(); await act(async () => { fireEvent.click(screen.getByText('Save')); }); expect(onSubmit).toHaveBeenCalled(); }); test('Handles AutoSave', async () => { const onSubmit = jest.fn(); await setup({ questionnaire: { resourceType: 'Questionnaire', item: [ { type: QuestionnaireItemType.string, linkId: 'question1', text: 'Question 1', }, ], }, onSubmit, autoSave: true, }); await act(async () => { fireEvent.click(screen.getByText('Add item')); }); expect(onSubmit).toHaveBeenCalled(); await act(async () => { fireEvent.click(screen.getByText('Question 1')); }); await act(async () => { fireEvent.click(screen.getAllByText('Remove')[0]); }); expect(onSubmit).toHaveBeenCalledTimes(2); await act(async () => { fireEvent.click(screen.getByText('Add group')); }); // Shouldn't autosave when adding a group expect(onSubmit).toHaveBeenCalledTimes(2); }); test('Sets ids', async () => { const onSubmit = jest.fn(); await setup({ questionnaire: { resourceType: 'Questionnaire', item: [ { linkId: 'q1', type: QuestionnaireItemType.choice, text: 'q1', answerOption: [ { valueString: 'a1', }, { valueString: 'a2', }, ], }, ], }, onSubmit, }); expect(screen.getByText('Save')).toBeDefined(); await act(async () => { fireEvent.click(screen.getByText('Save')); }); expect(onSubmit).toHaveBeenCalled(); const result = onSubmit.mock.calls[0][0]; expect(result.item[0].id).toBeDefined(); expect(result.item[0].answerOption[0].id).toBeDefined(); expect(result.item[0].answerOption[1].id).toBeDefined(); }); test('Edit a question text', async () => { const onSubmit = jest.fn(); await setup({ questionnaire: { resourceType: 'Questionnaire', item: [ { linkId: 'question1', text: 'Question 1', type: 'string', }, ], }, onSubmit, }); expect(screen.getByText('Question 1')).toBeDefined(); await act(async () => { fireEvent.click(screen.getByText('Question 1')); }); await act(async () => { fireEvent.change(screen.getByDisplayValue('Question 1'), { target: { value: 'Renamed' }, }); }); fireEvent.blur(screen.getByDisplayValue('Renamed')); await act(async () => { fireEvent.click(screen.getByText('Save')); }); expect(onSubmit).toHaveBeenCalled(); expect(onSubmit.mock.calls[0][0]).toMatchObject({ resourceType: 'Questionnaire', item: [ { linkId: 'question1', text: 'Renamed', type: 'string', }, ], }); }); test('Add item', async () => { const onSubmit = jest.fn(); await setup({ questionnaire: { resourceType: 'Questionnaire', title: 'My questionnaire', item: [ { linkId: 'question1', text: 'Question 1', type: 'string', }, ], }, onSubmit, }); await act(async () => { fireEvent.click(screen.getByText('My questionnaire')); }); await act(async () => { fireEvent.click(screen.getByText('Add item')); }); // Should not submit without autosave flag expect(onSubmit).not.toHaveBeenCalled(); await act(async () => { fireEvent.click(screen.getByText('Save')); }); expect(onSubmit).toHaveBeenCalled(); expect(onSubmit.mock.calls[0][0]).toMatchObject({ resourceType: 'Questionnaire', item: [ { linkId: 'question1', text: 'Question 1', type: 'string', }, { text: 'Question', type: 'string', }, ], }); }); test('Add item with existing linkId', async () => { const onSubmit = jest.fn(); await setup({ questionnaire: { resourceType: 'Questionnaire', title: 'My questionnaire', item: [ { id: 'id-100', linkId: 'q100', text: 'Question 1', type: 'string', }, ], }, onSubmit, }); await act(async () => { fireEvent.click(screen.getByText('My questionnaire')); }); await act(async () => { fireEvent.click(screen.getByText('Add item')); }); await act(async () => { fireEvent.click(screen.getByText('Save')); }); expect(onSubmit).toHaveBeenCalled(); expect(onSubmit.mock.calls[0][0]).toMatchObject({ resourceType: 'Questionnaire', item: [ { id: 'id-100', linkId: 'q100', text: 'Question 1', type: 'string', }, { id: 'id-101', linkId: 'q101', text: 'Question', type: 'string', }, ], }); }); test('Remove item', async () => { const onSubmit = jest.fn(); await setup({ questionnaire: { resourceType: 'Questionnaire', title: 'My questionnaire', item: [ { linkId: 'question1', text: 'Question 1', type: 'string', }, { linkId: 'question2', text: 'Question 2', type: 'string', }, ], }, onSubmit, }); await act(async () => { fireEvent.click(screen.getByText('Question 1')); }); await act(async () => { fireEvent.click(screen.getAllByText('Remove')[0]); }); await act(async () => { fireEvent.click(screen.getByText('Save')); }); expect(onSubmit).toHaveBeenCalled(); expect(onSubmit.mock.calls[0][0]).toMatchObject({ resourceType: 'Questionnaire', item: [ { linkId: 'question2', text: 'Question 2', type: 'string', }, ], }); }); test('Add group', async () => { const onSubmit = jest.fn(); await setup({ questionnaire: { resourceType: 'Questionnaire', title: 'My questionnaire', item: [ { linkId: 'question1', text: 'Question 1', type: 'string', }, ], }, onSubmit, }); await act(async () => { fireEvent.click(screen.getByText('My questionnaire')); }); await act(async () => { fireEvent.click(screen.getByText('Add group')); }); await act(async () => { fireEvent.click(screen.getByText('Save')); }); expect(onSubmit).toHaveBeenCalled(); expect(onSubmit.mock.calls[0][0]).toMatchObject({ resourceType: 'Questionnaire', item: [ { linkId: 'question1', text: 'Question 1', type: 'string', }, { type: 'group', }, ], }); }); test('Change title', async () => { const onSubmit = jest.fn(); await setup({ questionnaire: { resourceType: 'Questionnaire', title: 'My questionnaire', item: [ { linkId: 'question1', text: 'Question 1', type: 'string', }, ], }, onSubmit, }); await act(async () => { fireEvent.click(screen.getByText('My questionnaire')); }); await act(async () => { fireEvent.change(screen.getByDisplayValue('My questionnaire'), { target: { value: 'Renamed' }, }); }); fireEvent.blur(screen.getByDisplayValue('Renamed')); await act(async () => { fireEvent.click(screen.getByText('Save')); }); expect(onSubmit).toHaveBeenCalled(); expect(onSubmit.mock.calls[0][0]).toMatchObject({ resourceType: 'Questionnaire', title: 'Renamed', }); }); test('Add Reference Profiles', async () => { const onSubmit = jest.fn(); await setup({ questionnaire: { resourceType: 'Questionnaire', title: 'My References', item: [ { linkId: 'reference1', text: 'Reference 1', type: 'reference', }, ], }, onSubmit, }); await act(async () => { fireEvent.click(screen.getByText('Reference 1')); }); await act(async () => { fireEvent.click(screen.getByText('Add Resource Type')); }); await act(async () => { fireEvent.change(screen.getByPlaceholderText('Resource Type'), { target: { value: 'Patient' }, }); }); await act(async () => { fireEvent.click(screen.getByText('Add Resource Type')); }); await act(async () => { fireEvent.change(screen.getAllByPlaceholderText('Resource Type')[1], { target: { value: 'Organization' }, }); }); await act(async () => { fireEvent.change(screen.getByDisplayValue('Patient'), { target: { value: 'Practicitioner' }, }); }); expect(screen.getByDisplayValue('Organization')).toBeDefined(); expect(screen.getByDisplayValue('Practicitioner')).toBeDefined(); const removeLinks = screen.getAllByText('Remove'); expect(removeLinks.length).toEqual(3); await act(async () => { fireEvent.click(removeLinks[0]); }); }); test('Change linkId', async () => { const onSubmit = jest.fn(); await setup({ questionnaire: { resourceType: 'Questionnaire', title: 'My questionnaire', item: [ { linkId: 'question1', text: 'Question 1', type: 'string', }, ], }, onSubmit, }); await act(async () => { fireEvent.click(screen.getByText('Question 1')); }); await act(async () => { fireEvent.change(screen.getByDisplayValue('question1'), { target: { value: 'myNewLinkId' }, }); }); fireEvent.blur(screen.getByDisplayValue('myNewLinkId')); await act(async () => { fireEvent.click(screen.getByText('Save')); }); expect(onSubmit).toHaveBeenCalled(); expect(onSubmit.mock.calls[0][0]).toMatchObject({ resourceType: 'Questionnaire', item: [ { linkId: 'myNewLinkId', }, ], }); }); test('Hover on/off', async () => { await setup({ questionnaire: { resourceType: 'Questionnaire', title: 'My questionnaire', item: [ { id: 'question1', linkId: 'question1', text: 'Question 1', type: 'string', }, ], }, onSubmit: jest.fn(), }); expect(screen.getByTestId('question1')).not.toHaveClass('hovering'); await act(async () => { fireEvent.mouseOver(screen.getByText('Question 1')); }); expect(screen.getByTestId('question1')).toHaveClass('hovering'); await act(async () => { fireEvent.mouseOver(document.body); }); expect(screen.getByTestId('question1')).not.toHaveClass('hovering'); }); test('Add multiple choice', async () => { const onSubmit = jest.fn(); await setup({ questionnaire: { resourceType: 'Questionnaire', title: 'My questionnaire', item: [], }, onSubmit, }); // Add a new question await act(async () => { fireEvent.click(screen.getByText('Add item')); }); // Click on the question to start editing await act(async () => { fireEvent.click(screen.getByText('Question')); }); // Change the question type from "string" (default) to "choice" fireEvent.change(screen.getByDisplayValue('String'), { target: { value: 'choice' }, }); // Add a new choice await act(async () => { fireEvent.click(screen.getByText('Add choice')); }); // Change the question type from "integer" (default) to "string" fireEvent.change(screen.getByDisplayValue('integer'), { target: { value: 'string' }, }); // Change the text for the choice fireEvent.change(screen.getByTestId('value[x]'), { target: { value: 'foo bar' }, }); await act(async () => { fireEvent.click(screen.getByText('Save')); }); expect(onSubmit).toHaveBeenCalled(); expect(onSubmit.mock.calls[0][0]).toMatchObject({ resourceType: 'Questionnaire', item: [ { text: 'Question', type: 'choice', answerOption: [ { valueString: 'foo bar', }, ], }, ], }); }); test('Add Pages', async () => { const onSubmit = jest.fn(); await setup({ questionnaire: { resourceType: 'Questionnaire', title: 'My questionnaire', item: [], }, onSubmit, }); await act(async () => { fireEvent.click(screen.getByText('Add Page')); }); expect(screen.getByText('New Page')).toBeDefined(); }); test('Add Repeatable', async () => { await setup({ questionnaire: { resourceType: 'Questionnaire', title: 'My questionnaire', item: [ { id: 'question1', linkId: 'question1', text: 'Question 1', type: 'string', }, ], }, onSubmit: jest.fn(), }); await act(async () => { fireEvent.click(screen.getByText('Question 1')); }); await act(async () => { fireEvent.click(screen.getByText('Make Repeatable')); }); await act(async () => { fireEvent.click(screen.getByText('Remove Repeatable')); }); await act(async () => { fireEvent.click(screen.getByText('Make Repeatable')); }); }); test('Add Value Set', async () => { const onSubmit = jest.fn(); await setup({ questionnaire: { resourceType: 'Questionnaire', title: 'My questionnaire', item: [], }, onSubmit, }); // Add a new question await act(async () => { fireEvent.click(screen.getByText('Add item')); }); // Click on the question to start editing await act(async () => { fireEvent.click(screen.getByText('Question')); }); // Change the question type from "string" (default) to "choice" fireEvent.change(screen.getByDisplayValue('String'), { target: { value: 'choice' }, }); // Add a new choice await act(async () => { fireEvent.click(screen.getByText('Add choice')); }); // Change the question type from "integer" (default) to "string" fireEvent.change(screen.getByDisplayValue('integer'), { target: { value: 'string' }, }); // Change the text for the choice fireEvent.change(screen.getByTestId('value[x]'), { target: { value: 'foo bar' }, }); // Add a value set await act(async () => { fireEvent.click(screen.getByText('Add value set')); }); // Change the value set fireEvent.change(screen.getByDisplayValue(''), { target: { value: 'http://example.com' }, }); await act(async () => { fireEvent.click(screen.getByText('Save')); }); expect(onSubmit).toHaveBeenCalled(); expect(onSubmit.mock.calls[0][0]).toMatchObject({ resourceType: 'Questionnaire', item: [ { text: 'Question', type: 'choice', answerOption: [], answerValueSet: 'http://example.com', }, ], }); }); test('Move down', async () => { const onSubmit = jest.fn(); await setup({ questionnaire: { resourceType: 'Questionnaire', title: 'My questionnaire', item: [ { id: 'question1', linkId: 'question1', text: 'Question 1', type: 'string', }, { id: 'question2', linkId: 'question2', text: 'Question 2', type: 'string', }, { id: 'question3', linkId: 'question3', text: 'Question 3', type: 'string', }, ], }, onSubmit, }); await act(async () => { fireEvent.click(screen.getByText('Question 1')); }); await act(async () => { const downButtons = screen.getAllByTestId('down-button'); fireEvent.click(downButtons[0]); }); await act(async () => { fireEvent.click(screen.getByText('Save')); }); expect(onSubmit).toHaveBeenCalled(); expect(onSubmit.mock.calls[0][0]).toMatchObject({ resourceType: 'Questionnaire', item: [ { id: 'question2', linkId: 'question2', text: 'Question 2', type: 'string', }, { id: 'question1', linkId: 'question1', text: 'Question 1', type: 'string', }, { id: 'question3', linkId: 'question3', text: 'Question 3', type: 'string', }, ], }); }); test('Move Up', async () => { const onSubmit = jest.fn(); await setup({ questionnaire: { resourceType: 'Questionnaire', title: 'My questionnaire', item: [ { id: 'question1', linkId: 'question1', text: 'Question 1', type: 'string', }, { id: 'question2', linkId: 'question2', text: 'Question 2', type: 'string', }, { id: 'question3', linkId: 'question3', text: 'Question 3', type: 'string', }, ], }, onSubmit, }); await act(async () => { const upButtons = screen.getAllByTestId('up-button'); fireEvent.click(upButtons[1]); }); await act(async () => { fireEvent.click(screen.getByText('Save')); }); expect(onSubmit).toHaveBeenCalled(); expect(onSubmit.mock.calls[0][0]).toMatchObject({ resourceType: 'Questionnaire', item: [ { id: 'question1', linkId: 'question1', text: 'Question 1', type: 'string', }, { id: 'question3', linkId: 'question3', text: 'Question 3', type: 'string', }, { id: 'question2', linkId: 'question2', text: 'Question 2', type: 'string', }, ], }); }); test('Move Up with nested items', async () => { const onSubmit = jest.fn(); await setup({ questionnaire: { resourceType: 'Questionnaire', title: 'My questionnaire', item: [ { id: 'group', linkId: 'group', text: 'Group', type: 'group', item: [ { id: 'question1', linkId: 'question1', text: 'Question 1', type: 'string', }, { id: 'question2', linkId: 'question2', text: 'Question 2', type: 'string', }, { id: 'question3', linkId: 'question3', text: 'Question 3', type: 'string', }, ], }, ], }, onSubmit, }); await act(async () => { const upButtons = screen.getAllByTestId('up-button'); fireEvent.click(upButtons[1]); }); await act(async () => { fireEvent.click(screen.getByText('Save')); }); expect(onSubmit).toHaveBeenCalled(); expect(onSubmit.mock.calls[0][0]).toMatchObject({ resourceType: 'Questionnaire', item: [ { id: 'group', linkId: 'group', text: 'Group', type: 'group', item: [ { id: 'question1', linkId: 'question1', text: 'Question 1', type: 'string', }, { id: 'question3', linkId: 'question3', text: 'Question 3', type: 'string', }, { id: 'question2', linkId: 'question2', text: 'Question 2', type: 'string', }, ], }, ], }); }); test('Remove multiple choice', async () => { const onSubmit = jest.fn(); await setup({ questionnaire: { resourceType: 'Questionnaire', title: 'My questionnaire', item: [ { linkId: 'q1', type: QuestionnaireItemType.choice, text: 'My question', answerOption: [ { valueString: 'Answer 1', }, { valueString: 'Answer 2', }, { valueString: 'Answer 3', }, ], }, ], }, onSubmit, }); // Click on the question to start editing await act(async () => { fireEvent.click(screen.getByText('My question')); }); // Get all of the "Remove" links const removeLinks = screen.getAllByText('Remove'); expect(removeLinks.length).toEqual(4); // 1 question + 3 options // Remove "Answer 2", which is the 2nd remove link await act(async () => { fireEvent.click(removeLinks[1]); }); await act(async () => { fireEvent.click(screen.getByText('Save')); }); expect(onSubmit).toHaveBeenCalled(); expect(onSubmit.mock.calls[0][0]).toMatchObject({ resourceType: 'Questionnaire', item: [ { text: 'My question', type: 'choice', answerOption: [ { valueString: 'Answer 1', }, { valueString: 'Answer 3', }, ], }, ], }); }); });

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