From ba9d82cf00e42b3f85548e6b5671fdbac2a926e4 Mon Sep 17 00:00:00 2001 From: samuelmale Date: Mon, 14 Oct 2024 23:32:49 +0300 Subject: [PATCH 1/2] Fix support of marking fields as unspecified --- .../rfe-forms/sample_unspecified-form.json | 39 ++++ .../unspecified/unspecified.component.tsx | 11 +- .../inputs/unspecified/unspecified.test.tsx | 192 +++++++++++++----- .../field/form-field-renderer.component.tsx | 4 + src/utils/common-utils.ts | 1 + 5 files changed, 193 insertions(+), 54 deletions(-) create mode 100644 __mocks__/forms/rfe-forms/sample_unspecified-form.json diff --git a/__mocks__/forms/rfe-forms/sample_unspecified-form.json b/__mocks__/forms/rfe-forms/sample_unspecified-form.json new file mode 100644 index 00000000..7182c005 --- /dev/null +++ b/__mocks__/forms/rfe-forms/sample_unspecified-form.json @@ -0,0 +1,39 @@ +{ + "name": "Sample Unspecified Form", + "pages": [ + { + "label": "Page 1", + "sections": [ + { + "label": "Section 1", + "isExpanded": "true", + "questions": [ + { + "label": "Body Weight", + "type": "obs", + "questionOptions": { + "rendering": "number", + "concept": "560555AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "conceptMappings": [ + { + "type": "CIEL", + "value": "160555" + } + ] + }, + "id": "bodyWeight", + "validators": [], + "required": true, + "unspecified": true + } + ] + } + ] + } + ], + "availableIntents": [], + "processor": "EncounterFormProcessor", + "uuid": "na24c540-cc83-43bc-978f-c1ef180a597f", + "referencedForms": [], + "encounterType": "b9c1f50f-f77d-42e2-ad2a-d29304dde2fv" +} diff --git a/src/components/inputs/unspecified/unspecified.component.tsx b/src/components/inputs/unspecified/unspecified.component.tsx index f5c50e20..4be4fd46 100644 --- a/src/components/inputs/unspecified/unspecified.component.tsx +++ b/src/components/inputs/unspecified/unspecified.component.tsx @@ -7,7 +7,7 @@ import { isTrue } from '../../../utils/boolean-utils'; import styles from './unspecified.scss'; import { useFormProviderContext } from '../../../provider/form-provider'; -import { isViewMode } from '../../../utils/common-utils'; +import { clearSubmission, isViewMode } from '../../../utils/common-utils'; interface UnspecifiedFieldProps { field: FormField; @@ -40,14 +40,17 @@ const UnspecifiedField: React.FC = ({ field, fieldValue, (value) => { const rendering = field.questionOptions.rendering; if (value.target.checked) { - const emptyValue = rendering === 'checkbox' ? [] : ''; - field.meta.submission = { ...field.meta.submission, unspecified: true }; - updateFormField({ ...field }); setIsUnspecified(true); + const emptyValue = rendering === 'checkbox' ? [] : ''; + clearSubmission(field); + field.meta.submission.unspecified = true; + updateFormField(field); setFieldValue(emptyValue); onAfterChange(emptyValue); } else { setIsUnspecified(false); + field.meta.submission.unspecified = false; + updateFormField(field); } }, [field.questionOptions.rendering], diff --git a/src/components/inputs/unspecified/unspecified.test.tsx b/src/components/inputs/unspecified/unspecified.test.tsx index 3fe607da..d7635e55 100644 --- a/src/components/inputs/unspecified/unspecified.test.tsx +++ b/src/components/inputs/unspecified/unspecified.test.tsx @@ -1,74 +1,166 @@ import React from 'react'; -import dayjs from 'dayjs'; -import { fireEvent, render, screen } from '@testing-library/react'; -import { OpenmrsDatePicker } from '@openmrs/esm-framework'; -import { type FormField } from '../../../types'; -import { findTextOrDateInput } from '../../../utils/test-utils'; - -const mockOpenmrsDatePicker = jest.mocked(OpenmrsDatePicker); - -mockOpenmrsDatePicker.mockImplementation(({ id, labelText, value, onChange }) => { - return ( - <> - - onChange(new Date(evt.target.value))} - /> - - ); +import { act, render, screen } from '@testing-library/react'; +import { usePatient, useSession } from '@openmrs/esm-framework'; +import { type FormSchema, type SessionMode } from '../../../types'; +import { findNumberInput } from '../../../utils/test-utils'; +import unspecifiedForm from '../../../../__mocks__/forms/rfe-forms/sample_unspecified-form.json'; +import { FormEngine } from '../../..'; +import { mockPatient } from '../../../../__mocks__/patient.mock'; +import { mockSessionDataResponse } from '../../../../__mocks__/session.mock'; +import userEvent from '@testing-library/user-event'; +import * as api from '../../../api'; + +const mockUsePatient = jest.mocked(usePatient); +const mockUseSession = jest.mocked(useSession); + +global.ResizeObserver = require('resize-observer-polyfill'); + +jest.mock('../../../api', () => { + const originalModule = jest.requireActual('../../../api'); + return { + ...originalModule, + getPreviousEncounter: jest.fn().mockImplementation(() => Promise.resolve(null)), + getConcept: jest.fn().mockImplementation(() => Promise.resolve(null)), + saveEncounter: jest.fn(), + }; }); -const question: FormField = { - label: 'Visit Date', - type: 'obs', - datePickerFormat: 'calendar', - questionOptions: { - rendering: 'date', - concept: '163260AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', - }, - id: 'visit-date', -}; +jest.mock('../../../hooks/useConcepts', () => ({ + useConcepts: jest.fn().mockImplementation((references: Set) => { + return { + isLoading: false, + concepts: [], + error: undefined, + }; + }), +})); -const renderForm = (initialValues) => { - render(<>); +jest.mock('../../../hooks/useEncounterRole', () => ({ + useEncounterRole: jest.fn().mockReturnValue({ + isLoading: false, + encounterRole: { name: 'Clinician', uuid: 'clinician-uuid' }, + error: undefined, + }), +})); + +jest.mock('../../../hooks/useEncounter', () => ({ + useEncounter: jest.fn().mockImplementation((formJson: FormSchema) => { + return { + encounter: formJson.encounter + ? { + uuid: 'encounter-uuid', + obs: [], + } + : null, + isLoading: false, + error: undefined, + }; + }), +})); + +const renderForm = async (mode: SessionMode = 'enter') => { + await act(async () => { + render( + , + ); + }); }; -describe.skip('Unspecified', () => { - it('Should toggle the "Unspecified" checkbox on click', async () => { - // setup - renderForm({}); +describe('Unspecified', () => { + const user = userEvent.setup(); + + beforeEach(() => { + Object.defineProperty(window, 'i18next', { + writable: true, + configurable: true, + value: { + language: 'en', + t: jest.fn(), + }, + }); + + mockUsePatient.mockImplementation(() => ({ + patient: mockPatient, + isLoading: false, + error: undefined, + patientUuid: mockPatient.id, + })); + + mockUseSession.mockImplementation(() => mockSessionDataResponse.data); + }); + + it('Should clear field value when the "Unspecified" checkbox is clicked', async () => { + //setup + await renderForm(); const unspecifiedCheckbox = screen.getByRole('checkbox', { name: /Unspecified/ }); + const bodyWeightField = await findNumberInput(screen, 'Body Weight *'); // assert initial state expect(unspecifiedCheckbox).not.toBeChecked(); + expect(bodyWeightField.value).toBe(''); - // assert checked - fireEvent.click(unspecifiedCheckbox); - expect(unspecifiedCheckbox).toBeChecked(); + await user.type(bodyWeightField, '55'); - // assert unchecked - fireEvent.click(unspecifiedCheckbox); - expect(unspecifiedCheckbox).not.toBeChecked(); + // assert new value + expect(bodyWeightField.value).toBe('55'); + + // mark as unspecified + await user.click(unspecifiedCheckbox); + expect(unspecifiedCheckbox).toBeChecked(); + expect(bodyWeightField.value).toBe(''); }); - it('Should clear field value when the "Unspecified" checkbox is clicked', async () => { + it('Should bypass form validation when the "Unspecified" checkbox is clicked', async () => { //setup - renderForm({}); + const mockSaveEncounter = jest.spyOn(api, 'saveEncounter'); + await renderForm(); const unspecifiedCheckbox = screen.getByRole('checkbox', { name: /Unspecified/ }); - const visitDateField = await findTextOrDateInput(screen, 'Visit Date'); + const bodyWeightField = await findNumberInput(screen, 'Body Weight *'); // assert initial state expect(unspecifiedCheckbox).not.toBeChecked(); - expect(visitDateField.value).toBe(''); + expect(bodyWeightField.value).toBe(''); + + // attempt to submit the form + await user.click(screen.getByRole('button', { name: /Save/ })); + expect(screen.getByText(/Field is mandatory/)).toBeInTheDocument(); + expect(mockSaveEncounter).not.toHaveBeenCalled(); + + // mark as unspecified + await user.click(unspecifiedCheckbox); + expect(unspecifiedCheckbox).toBeChecked(); + expect(bodyWeightField.value).toBe(''); + + // submit the form again + await user.click(screen.getByRole('button', { name: /Save/ })); + expect(mockSaveEncounter).toHaveBeenCalled(); + }); - fireEvent.change(visitDateField, { target: { value: '2023-09-09T00:00:00.000Z' } }); + it('Should mark fields with null values as unspecified when in edit mode', async () => { + // setup + await renderForm('edit'); + const unspecifiedCheckbox = screen.getByRole('checkbox', { name: /Unspecified/ }); + const bodyWeightField = await findNumberInput(screen, 'Body Weight *'); - // assert checked - fireEvent.click(unspecifiedCheckbox); + // assert initial state expect(unspecifiedCheckbox).toBeChecked(); - //TODO : Fix this test case - - https://openmrs.atlassian.net/browse/O3-3479s - // expect(visitDateField.value).toBe(''); + expect(bodyWeightField.value).toBe(''); + }); + + it('Should not display the unspecified checkbox in view mode', async () => { + // setup + await renderForm('view'); + + try { + screen.getByRole('checkbox', { name: /Unspecified/ }); + fail('Unspecified checkbox should not be displayed'); + } catch (error) { + expect(error).toBeDefined(); + } }); }); diff --git a/src/components/renderer/field/form-field-renderer.component.tsx b/src/components/renderer/field/form-field-renderer.component.tsx index 7abec1b7..b649fb55 100644 --- a/src/components/renderer/field/form-field-renderer.component.tsx +++ b/src/components/renderer/field/form-field-renderer.component.tsx @@ -105,6 +105,10 @@ export const FormFieldRenderer = ({ fieldId, valueAdapter, repeatOptions }: Form if (field.meta.submission?.warnings) { setWarnings(field.meta.submission.warnings); } + if (field.meta.submission?.unspecified) { + setErrors([]); + removeInvalidField(field.id); + } }, [field.meta.submission]); const onAfterChange = (value: any) => { diff --git a/src/utils/common-utils.ts b/src/utils/common-utils.ts index ea84f559..659f1460 100644 --- a/src/utils/common-utils.ts +++ b/src/utils/common-utils.ts @@ -30,6 +30,7 @@ export function clearSubmission(field: FormField) { field.meta = { ...(field.meta || {}), submission: {} }; } field.meta.submission = { + ...field.meta.submission, voidedValue: null, newValue: null, }; From 7afaf636708362aaa20a5f088eed8ecce680df6d Mon Sep 17 00:00:00 2001 From: samuelmale Date: Mon, 14 Oct 2024 23:57:43 +0300 Subject: [PATCH 2/2] fixup --- src/components/inputs/unspecified/unspecified.component.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/inputs/unspecified/unspecified.component.tsx b/src/components/inputs/unspecified/unspecified.component.tsx index 4be4fd46..53e28621 100644 --- a/src/components/inputs/unspecified/unspecified.component.tsx +++ b/src/components/inputs/unspecified/unspecified.component.tsx @@ -29,12 +29,12 @@ const UnspecifiedField: React.FC = ({ field, fieldValue, }, []); useEffect(() => { - if (field.meta.submission?.unspecified && field.meta.submission.newValue) { + if (field.meta.submission?.unspecified && (field.meta.submission.newValue || !isEmpty(fieldValue))) { setIsUnspecified(false); field.meta.submission.unspecified = false; updateFormField(field); } - }, [field.meta?.submission]); + }, [field.meta?.submission, fieldValue]); const handleOnChange = useCallback( (value) => {