diff --git a/package.json b/package.json index 6ceeba1c71..4afcda7bcc 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "@commitlint/core": "~8.3.5", "@commitlint/prompt": "~8.3.5", "@testing-library/react": "~10.1.0", - "@testing-library/react-hooks": "~3.2.1", + "@testing-library/react-hooks": "~3.3.0", "@types/enzyme": "^3.10.5", "@types/jest": "~25.2.0", "@types/lodash": "^4.14.150", diff --git a/src/__tests__/patients/care-plans/AddCarePlanModal.test.tsx b/src/__tests__/patients/care-plans/AddCarePlanModal.test.tsx new file mode 100644 index 0000000000..e62b486275 --- /dev/null +++ b/src/__tests__/patients/care-plans/AddCarePlanModal.test.tsx @@ -0,0 +1,120 @@ +import '../../../__mocks__/matchMediaMock' +import { Modal } from '@hospitalrun/components' +import { mount } from 'enzyme' +import { createMemoryHistory } from 'history' +import React from 'react' +import { act } from 'react-dom/test-utils' +import { Provider } from 'react-redux' +import { Router } from 'react-router-dom' +import createMockStore from 'redux-mock-store' +import thunk from 'redux-thunk' + +import { CarePlanIntent, CarePlanStatus } from '../../../model/CarePlan' +import Patient from '../../../model/Patient' +import AddCarePlanModal from '../../../patients/care-plans/AddCarePlanModal' +import CarePlanForm from '../../../patients/care-plans/CarePlanForm' +import * as patientSlice from '../../../patients/patient-slice' +import { RootState } from '../../../store' + +const mockStore = createMockStore([thunk]) + +describe('Add Care Plan Modal', () => { + const patient = { + id: 'patientId', + diagnoses: [{ id: '123', name: 'some name', diagnosisDate: new Date().toISOString() }], + carePlans: [ + { + id: '123', + title: 'some title', + description: 'some description', + diagnosisId: '123', + startDate: new Date().toISOString(), + endDate: new Date().toISOString(), + status: CarePlanStatus.Active, + intent: CarePlanIntent.Proposal, + }, + ], + } as Patient + + const carePlanError = { + title: 'some care plan error', + } + + const onCloseSpy = jest.fn() + const setup = () => { + const store = mockStore({ patient: { patient, carePlanError } } as any) + const history = createMemoryHistory() + const wrapper = mount( + + + + + , + ) + + wrapper.update() + return { wrapper } + } + + it('should render a modal', () => { + const { wrapper } = setup() + + const modal = wrapper.find(Modal) + + expect(modal).toHaveLength(1) + + const successButton = modal.prop('successButton') + const cancelButton = modal.prop('closeButton') + expect(modal.prop('title')).toEqual('patient.carePlan.new') + expect(successButton?.children).toEqual('patient.carePlan.new') + expect(successButton?.icon).toEqual('add') + expect(cancelButton?.children).toEqual('actions.cancel') + }) + + it('should render the care plan form', () => { + const { wrapper } = setup() + + const carePlanForm = wrapper.find(CarePlanForm) + expect(carePlanForm).toHaveLength(1) + expect(carePlanForm.prop('carePlanError')).toEqual(carePlanError) + expect(carePlanForm.prop('patient')).toEqual(patient) + }) + + it('should dispatch add care plan when the save button is clicked', async () => { + const { wrapper } = setup() + jest.spyOn(patientSlice, 'addCarePlan') + + act(() => { + const carePlanForm = wrapper.find(CarePlanForm) + const onChange = carePlanForm.prop('onChange') as any + onChange(patient.carePlans[0]) + }) + wrapper.update() + + await act(async () => { + const modal = wrapper.find(Modal) + const successButton = modal.prop('successButton') + const onClick = successButton?.onClick as any + await onClick() + }) + + expect(patientSlice.addCarePlan).toHaveBeenCalledTimes(1) + expect(patientSlice.addCarePlan).toHaveBeenCalledWith(patient.id, patient.carePlans[0]) + }) + + it('should call the on close function when the cancel button is clicked', () => { + const { wrapper } = setup() + + const modal = wrapper.find(Modal) + + expect(modal).toHaveLength(1) + + act(() => { + const cancelButton = modal.prop('closeButton') + const onClick = cancelButton?.onClick as any + onClick() + }) + + expect(onCloseSpy).toHaveBeenCalledTimes(1) + }) +}) diff --git a/src/__tests__/patients/care-plans/CarePlanForm.test.tsx b/src/__tests__/patients/care-plans/CarePlanForm.test.tsx new file mode 100644 index 0000000000..20f83dd4c8 --- /dev/null +++ b/src/__tests__/patients/care-plans/CarePlanForm.test.tsx @@ -0,0 +1,319 @@ +import '../../../__mocks__/matchMediaMock' +import { Alert } from '@hospitalrun/components' +import { addDays } from 'date-fns' +import { mount } from 'enzyme' +import React from 'react' +import { act } from 'react-dom/test-utils' + +import CarePlan, { CarePlanIntent, CarePlanStatus } from '../../../model/CarePlan' +import Diagnosis from '../../../model/Diagnosis' +import Patient from '../../../model/Patient' +import CarePlanForm from '../../../patients/care-plans/CarePlanForm' + +describe('Care Plan Form', () => { + let onCarePlanChangeSpy: any + const diagnosis: Diagnosis = { + id: '123', + name: 'some diagnosis name', + diagnosisDate: new Date().toISOString(), + } + const carePlan: CarePlan = { + id: 'id', + title: 'title', + description: 'description', + status: CarePlanStatus.Active, + intent: CarePlanIntent.Option, + startDate: new Date().toISOString(), + endDate: new Date().toISOString(), + diagnosisId: diagnosis.id, + createdOn: new Date().toISOString(), + note: 'note', + } + const setup = (disabled = false, initializeCarePlan = true, error?: any) => { + onCarePlanChangeSpy = jest.fn() + const mockPatient = { id: '123', diagnoses: [diagnosis] } as Patient + const wrapper = mount( + , + ) + return { wrapper } + } + + it('should render a title input', () => { + const { wrapper } = setup() + + const titleInput = wrapper.findWhere((w) => w.prop('name') === 'title') + + expect(titleInput).toHaveLength(1) + expect(titleInput.prop('patient.carePlan.title')) + expect(titleInput.prop('isRequired')).toBeTruthy() + expect(titleInput.prop('value')).toEqual(carePlan.title) + }) + + it('should call the on change handler when condition changes', () => { + const expectedNewTitle = 'some new title' + const { wrapper } = setup(false, false) + act(() => { + const titleInput = wrapper.findWhere((w) => w.prop('name') === 'title') + const onChange = titleInput.prop('onChange') as any + onChange({ currentTarget: { value: expectedNewTitle } }) + }) + + expect(onCarePlanChangeSpy).toHaveBeenCalledWith({ title: expectedNewTitle }) + }) + + it('should render a description input', () => { + const { wrapper } = setup() + + const descriptionInput = wrapper.findWhere((w) => w.prop('name') === 'description') + + expect(descriptionInput).toHaveLength(1) + expect(descriptionInput.prop('patient.carePlan.description')) + expect(descriptionInput.prop('isRequired')).toBeTruthy() + expect(descriptionInput.prop('value')).toEqual(carePlan.description) + }) + + it('should call the on change handler when condition changes', () => { + const expectedNewDescription = 'some new description' + const { wrapper } = setup(false, false) + act(() => { + const descriptionInput = wrapper.findWhere((w) => w.prop('name') === 'description') + const onChange = descriptionInput.prop('onChange') as any + onChange({ currentTarget: { value: expectedNewDescription } }) + }) + + expect(onCarePlanChangeSpy).toHaveBeenCalledWith({ description: expectedNewDescription }) + }) + + it('should render a condition selector with the diagnoses from the patient', () => { + const { wrapper } = setup() + + const conditionSelector = wrapper.findWhere((w) => w.prop('name') === 'condition') + + expect(conditionSelector).toHaveLength(1) + expect(conditionSelector.prop('patient.carePlan.condition')) + expect(conditionSelector.prop('isRequired')).toBeTruthy() + expect(conditionSelector.prop('value')).toEqual(carePlan.diagnosisId) + expect(conditionSelector.prop('options')).toEqual([ + { value: diagnosis.id, label: diagnosis.name }, + ]) + }) + + it('should call the on change handler when condition changes', () => { + const expectedNewCondition = 'some new condition' + const { wrapper } = setup(false, false) + act(() => { + const conditionSelector = wrapper.findWhere((w) => w.prop('name') === 'condition') + const onChange = conditionSelector.prop('onChange') as any + onChange({ currentTarget: { value: expectedNewCondition } }) + }) + + expect(onCarePlanChangeSpy).toHaveBeenCalledWith({ diagnosisId: expectedNewCondition }) + }) + + it('should render a status selector', () => { + const { wrapper } = setup() + + const statusSelector = wrapper.findWhere((w) => w.prop('name') === 'status') + + expect(statusSelector).toHaveLength(1) + expect(statusSelector.prop('patient.carePlan.status')) + expect(statusSelector.prop('isRequired')).toBeTruthy() + expect(statusSelector.prop('value')).toEqual(carePlan.status) + expect(statusSelector.prop('options')).toEqual( + Object.values(CarePlanStatus).map((v) => ({ label: v, value: v })), + ) + }) + + it('should call the on change handler when status changes', () => { + const expectedNewStatus = CarePlanStatus.Revoked + const { wrapper } = setup(false, false) + act(() => { + const statusSelector = wrapper.findWhere((w) => w.prop('name') === 'status') + const onChange = statusSelector.prop('onChange') as any + onChange({ currentTarget: { value: expectedNewStatus } }) + }) + + expect(onCarePlanChangeSpy).toHaveBeenCalledWith({ status: expectedNewStatus }) + }) + + it('should render an intent selector', () => { + const { wrapper } = setup() + + const intentSelector = wrapper.findWhere((w) => w.prop('name') === 'intent') + + expect(intentSelector).toHaveLength(1) + expect(intentSelector.prop('patient.carePlan.intent')) + expect(intentSelector.prop('isRequired')).toBeTruthy() + expect(intentSelector.prop('value')).toEqual(carePlan.intent) + expect(intentSelector.prop('options')).toEqual( + Object.values(CarePlanIntent).map((v) => ({ label: v, value: v })), + ) + }) + + it('should call the on change handler when intent changes', () => { + const newIntent = CarePlanIntent.Proposal + const { wrapper } = setup(false, false) + act(() => { + const intentSelector = wrapper.findWhere((w) => w.prop('name') === 'intent') + const onChange = intentSelector.prop('onChange') as any + onChange({ currentTarget: { value: newIntent } }) + }) + + expect(onCarePlanChangeSpy).toHaveBeenCalledWith({ intent: newIntent }) + }) + + it('should render a start date picker', () => { + const { wrapper } = setup() + + const startDatePicker = wrapper.findWhere((w) => w.prop('name') === 'startDate') + + expect(startDatePicker).toHaveLength(1) + expect(startDatePicker.prop('patient.carePlan.startDate')) + expect(startDatePicker.prop('isRequired')).toBeTruthy() + expect(startDatePicker.prop('value')).toEqual(new Date(carePlan.startDate)) + }) + + it('should call the on change handler when start date changes', () => { + const expectedNewStartDate = addDays(1, new Date().getDate()) + const { wrapper } = setup(false, false) + + const startDatePicker = wrapper.findWhere((w) => w.prop('name') === 'startDate') + act(() => { + const onChange = startDatePicker.prop('onChange') as any + onChange(expectedNewStartDate) + }) + + expect(onCarePlanChangeSpy).toHaveBeenCalledWith({ + startDate: expectedNewStartDate.toISOString(), + }) + }) + + it('should render an end date picker', () => { + const { wrapper } = setup() + + const endDatePicker = wrapper.findWhere((w) => w.prop('name') === 'endDate') + + expect(endDatePicker).toHaveLength(1) + expect(endDatePicker.prop('patient.carePlan.endDate')) + expect(endDatePicker.prop('isRequired')).toBeTruthy() + expect(endDatePicker.prop('value')).toEqual(new Date(carePlan.endDate)) + }) + + it('should call the on change handler when end date changes', () => { + const expectedNewEndDate = addDays(1, new Date().getDate()) + const { wrapper } = setup(false, false) + + const endDatePicker = wrapper.findWhere((w) => w.prop('name') === 'endDate') + act(() => { + const onChange = endDatePicker.prop('onChange') as any + onChange(expectedNewEndDate) + }) + + expect(onCarePlanChangeSpy).toHaveBeenCalledWith({ + endDate: expectedNewEndDate.toISOString(), + }) + }) + + it('should render a note input', () => { + const { wrapper } = setup() + + const noteInput = wrapper.findWhere((w) => w.prop('name') === 'note') + expect(noteInput).toHaveLength(1) + expect(noteInput.prop('patient.carePlan.note')) + expect(noteInput.prop('isRequired')).toBeTruthy() + expect(noteInput.prop('value')).toEqual(carePlan.note) + }) + + it('should call the on change handler when note changes', () => { + const expectedNewNote = 'some new note' + const { wrapper } = setup(false, false) + + const noteInput = wrapper.findWhere((w) => w.prop('name') === 'note') + act(() => { + const onChange = noteInput.prop('onChange') as any + onChange({ currentTarget: { value: expectedNewNote } }) + }) + + expect(onCarePlanChangeSpy).toHaveBeenCalledWith({ note: expectedNewNote }) + }) + + it('should render all of the fields as disabled if the form is disabled', () => { + const { wrapper } = setup(true) + const titleInput = wrapper.findWhere((w) => w.prop('name') === 'title') + const descriptionInput = wrapper.findWhere((w) => w.prop('name') === 'description') + const conditionSelector = wrapper.findWhere((w) => w.prop('name') === 'condition') + const statusSelector = wrapper.findWhere((w) => w.prop('name') === 'status') + const intentSelector = wrapper.findWhere((w) => w.prop('name') === 'intent') + const startDatePicker = wrapper.findWhere((w) => w.prop('name') === 'startDate') + const endDatePicker = wrapper.findWhere((w) => w.prop('name') === 'endDate') + const noteInput = wrapper.findWhere((w) => w.prop('name') === 'note') + + expect(titleInput.prop('isEditable')).toBeFalsy() + expect(descriptionInput.prop('isEditable')).toBeFalsy() + expect(conditionSelector.prop('isEditable')).toBeFalsy() + expect(statusSelector.prop('isEditable')).toBeFalsy() + expect(intentSelector.prop('isEditable')).toBeFalsy() + expect(startDatePicker.prop('isEditable')).toBeFalsy() + expect(endDatePicker.prop('isEditable')).toBeFalsy() + expect(noteInput.prop('isEditable')).toBeFalsy() + }) + + it('should render the form fields in an error state', () => { + const expectedError = { + message: 'some error message', + title: 'some title error', + description: 'some description error', + status: 'some status error', + intent: 'some intent error', + startDate: 'some start date error', + endDate: 'some end date error', + note: 'some note error', + condition: 'some condition error', + } + + const { wrapper } = setup(false, false, expectedError) + + const alert = wrapper.find(Alert) + const titleInput = wrapper.findWhere((w) => w.prop('name') === 'title') + const descriptionInput = wrapper.findWhere((w) => w.prop('name') === 'description') + const conditionSelector = wrapper.findWhere((w) => w.prop('name') === 'condition') + const statusSelector = wrapper.findWhere((w) => w.prop('name') === 'status') + const intentSelector = wrapper.findWhere((w) => w.prop('name') === 'intent') + const startDatePicker = wrapper.findWhere((w) => w.prop('name') === 'startDate') + const endDatePicker = wrapper.findWhere((w) => w.prop('name') === 'endDate') + const noteInput = wrapper.findWhere((w) => w.prop('name') === 'note') + + expect(alert).toHaveLength(1) + expect(alert.prop('message')).toEqual(expectedError.message) + + expect(titleInput.prop('isInvalid')).toBeTruthy() + expect(titleInput.prop('feedback')).toEqual(expectedError.title) + + expect(descriptionInput.prop('isInvalid')).toBeTruthy() + expect(descriptionInput.prop('feedback')).toEqual(expectedError.description) + + expect(conditionSelector.prop('isInvalid')).toBeTruthy() + expect(conditionSelector.prop('feedback')).toEqual(expectedError.condition) + + expect(statusSelector.prop('isInvalid')).toBeTruthy() + expect(statusSelector.prop('feedback')).toEqual(expectedError.status) + + expect(intentSelector.prop('isInvalid')).toBeTruthy() + expect(intentSelector.prop('feedback')).toEqual(expectedError.intent) + + expect(startDatePicker.prop('isInvalid')).toBeTruthy() + expect(startDatePicker.prop('feedback')).toEqual(expectedError.startDate) + + expect(endDatePicker.prop('isInvalid')).toBeTruthy() + expect(endDatePicker.prop('feedback')).toEqual(expectedError.endDate) + + expect(noteInput.prop('isInvalid')).toBeTruthy() + expect(noteInput.prop('feedback')).toEqual(expectedError.note) + }) +}) diff --git a/src/__tests__/patients/care-plans/CarePlanTab.test.tsx b/src/__tests__/patients/care-plans/CarePlanTab.test.tsx new file mode 100644 index 0000000000..1a202bc841 --- /dev/null +++ b/src/__tests__/patients/care-plans/CarePlanTab.test.tsx @@ -0,0 +1,101 @@ +import '../../../__mocks__/matchMediaMock' +import { Button } from '@hospitalrun/components' +import { mount } from 'enzyme' +import { createMemoryHistory } from 'history' +import React from 'react' +import { act } from 'react-dom/test-utils' +import { Provider } from 'react-redux' +import { Router } from 'react-router-dom' +import createMockStore from 'redux-mock-store' +import thunk from 'redux-thunk' + +import Permissions from '../../../model/Permissions' +import AddCarePlanModal from '../../../patients/care-plans/AddCarePlanModal' +import CarePlanTab from '../../../patients/care-plans/CarePlanTab' +import CarePlanTable from '../../../patients/care-plans/CarePlanTable' +import ViewCarePlan from '../../../patients/care-plans/ViewCarePlan' +import { RootState } from '../../../store' + +const mockStore = createMockStore([thunk]) + +describe('Care Plan Tab', () => { + const patient = { + id: 'patientId', + } + + const setup = (route: string, permissions: Permissions[]) => { + const store = mockStore({ patient: { patient }, user: { permissions } } as any) + const history = createMemoryHistory() + history.push(route) + const wrapper = mount( + + + + + , + ) + + wrapper.update() + return { wrapper, history } + } + + it('should render an add care plan button if user has correct permissions', () => { + const { wrapper } = setup('/patients/123/care-plans', [Permissions.AddCarePlan]) + + const addNewButton = wrapper.find(Button).at(0) + expect(addNewButton).toHaveLength(1) + expect(addNewButton.text().trim()).toEqual('patient.carePlan.new') + }) + + it('should open the add care plan modal on click', () => { + const { wrapper } = setup('/patients/123/care-plans', [Permissions.AddCarePlan]) + + act(() => { + const addNewButton = wrapper.find(Button).at(0) + const onClick = addNewButton.prop('onClick') as any + onClick() + }) + wrapper.update() + + const modal = wrapper.find(AddCarePlanModal) + expect(modal.prop('show')).toBeTruthy() + }) + + it('should close the modal when the close button is clicked', () => { + const { wrapper } = setup('/patients/123/care-plans', [Permissions.AddCarePlan]) + + act(() => { + const addNewButton = wrapper.find(Button).at(0) + const onClick = addNewButton.prop('onClick') as any + onClick() + }) + wrapper.update() + + act(() => { + const modal = wrapper.find(AddCarePlanModal) + const onClose = modal.prop('onCloseButtonClick') as any + onClose() + }) + wrapper.update() + + expect(wrapper.find(AddCarePlanModal).prop('show')).toBeFalsy() + }) + + it('should not render care plan button if user does not have permissions', () => { + const { wrapper } = setup('/patients/123/care-plans', []) + + expect(wrapper.find(Button)).toHaveLength(0) + }) + + it('should render the care plans table when on /patient/:id/care-plans', () => { + const { wrapper } = setup('/patients/123/care-plans', []) + + expect(wrapper.find(CarePlanTable)).toHaveLength(1) + }) + + it('should render the care plan view when on /patient/:id/care-plans/:carePlanId', () => { + const { wrapper } = setup('/patients/123/care-plans/456', []) + + expect(wrapper.find(ViewCarePlan)).toHaveLength(1) + }) +}) diff --git a/src/__tests__/patients/care-plans/CarePlanTable.test.tsx b/src/__tests__/patients/care-plans/CarePlanTable.test.tsx new file mode 100644 index 0000000000..ce705f2a59 --- /dev/null +++ b/src/__tests__/patients/care-plans/CarePlanTable.test.tsx @@ -0,0 +1,89 @@ +import '../../../__mocks__/matchMediaMock' +import { Button } from '@hospitalrun/components' +import { mount } from 'enzyme' +import { createMemoryHistory } from 'history' +import React from 'react' +import { act } from 'react-dom/test-utils' +import { Provider } from 'react-redux' +import { Router } from 'react-router-dom' +import createMockStore from 'redux-mock-store' +import thunk from 'redux-thunk' + +import CarePlan, { CarePlanIntent, CarePlanStatus } from '../../../model/CarePlan' +import Patient from '../../../model/Patient' +import CarePlanTable from '../../../patients/care-plans/CarePlanTable' +import { RootState } from '../../../store' + +const mockStore = createMockStore([thunk]) + +describe('Care Plan Table', () => { + const carePlan: CarePlan = { + id: 'id', + title: 'title', + description: 'description', + status: CarePlanStatus.Active, + intent: CarePlanIntent.Option, + startDate: new Date(2020, 6, 3).toISOString(), + endDate: new Date(2020, 6, 5).toISOString(), + diagnosisId: 'some id', + createdOn: new Date().toISOString(), + note: 'note', + } + const patient = { + id: 'patientId', + diagnoses: [{ id: '123', name: 'some name', diagnosisDate: new Date().toISOString() }], + carePlans: [carePlan], + } as Patient + + const setup = () => { + const store = mockStore({ patient: { patient } } as any) + const history = createMemoryHistory() + history.push(`/patients/${patient.id}/care-plans/${patient.carePlans[0].id}`) + const wrapper = mount( + + + + + , + ) + + return { wrapper, history } + } + + it('should render a table', () => { + const { wrapper } = setup() + + const table = wrapper.find('table') + const tableHeader = table.find('thead') + const headers = tableHeader.find('th') + const body = table.find('tbody') + const columns = body.find('tr').find('td') + + expect(headers.at(0).text()).toEqual('patient.carePlan.title') + expect(headers.at(1).text()).toEqual('patient.carePlan.startDate') + expect(headers.at(2).text()).toEqual('patient.carePlan.endDate') + expect(headers.at(3).text()).toEqual('patient.carePlan.status') + expect(headers.at(4).text()).toEqual('actions.label') + + expect(columns.at(0).text()).toEqual(carePlan.title) + expect(columns.at(1).text()).toEqual('2020-07-03') + expect(columns.at(2).text()).toEqual('2020-07-05') + expect(columns.at(3).text()).toEqual(carePlan.status) + expect(columns.at(4).find('button')).toHaveLength(1) + }) + + it('should navigate to the care plan view when the view details button is clicked', () => { + const { wrapper, history } = setup() + + const table = wrapper.find('table') + const body = table.find('tbody') + const columns = body.find('tr').find('td') + + act(() => { + const onClick = columns.at(4).find(Button).prop('onClick') as any + onClick() + }) + + expect(history.location.pathname).toEqual(`/patients/${patient.id}/care-plans/${carePlan.id}`) + }) +}) diff --git a/src/__tests__/patients/care-plans/ViewCarePlan.test.tsx b/src/__tests__/patients/care-plans/ViewCarePlan.test.tsx new file mode 100644 index 0000000000..558b2773cf --- /dev/null +++ b/src/__tests__/patients/care-plans/ViewCarePlan.test.tsx @@ -0,0 +1,55 @@ +import '../../../__mocks__/matchMediaMock' +import { mount } from 'enzyme' +import { createMemoryHistory } from 'history' +import React from 'react' +import { Provider } from 'react-redux' +import { Route, Router } from 'react-router-dom' +import createMockStore from 'redux-mock-store' +import thunk from 'redux-thunk' + +import Patient from '../../../model/Patient' +import CarePlanForm from '../../../patients/care-plans/CarePlanForm' +import ViewCarePlan from '../../../patients/care-plans/ViewCarePlan' +import { RootState } from '../../../store' + +const mockStore = createMockStore([thunk]) + +describe('View Care Plan', () => { + const patient = { + id: 'patientId', + diagnoses: [{ id: '123', name: 'some name', diagnosisDate: new Date().toISOString() }], + carePlans: [{ id: '123', title: 'some title' }], + } as Patient + + const setup = () => { + const store = mockStore({ patient: { patient } } as any) + const history = createMemoryHistory() + history.push(`/patients/${patient.id}/care-plans/${patient.carePlans[0].id}`) + const wrapper = mount( + + + + + + + , + ) + + return { wrapper } + } + + it('should render the care plan title', () => { + const { wrapper } = setup() + + expect(wrapper.find('h2').text()).toEqual(patient.carePlans[0].title) + }) + + it('should render a care plan form with the correct data', () => { + const { wrapper } = setup() + + const carePlanForm = wrapper.find(CarePlanForm) + expect(carePlanForm).toHaveLength(1) + expect(carePlanForm.prop('carePlan')).toEqual(patient.carePlans[0]) + expect(carePlanForm.prop('patient')).toEqual(patient) + }) +}) diff --git a/src/__tests__/patients/patient-slice.test.ts b/src/__tests__/patients/patient-slice.test.ts index f49ca7caa1..9ce0c2ddf6 100644 --- a/src/__tests__/patients/patient-slice.test.ts +++ b/src/__tests__/patients/patient-slice.test.ts @@ -1,34 +1,37 @@ import '../../__mocks__/matchMediaMock' -import { addDays } from 'date-fns' +import { addDays, subDays } from 'date-fns' import { AnyAction } from 'redux' import createMockStore from 'redux-mock-store' import thunk from 'redux-thunk' import PatientRepository from '../../clients/db/PatientRepository' import Allergy from '../../model/Allergy' +import CarePlan, { CarePlanIntent, CarePlanStatus } from '../../model/CarePlan' import Diagnosis from '../../model/Diagnosis' import Patient from '../../model/Patient' import RelatedPerson from '../../model/RelatedPerson' import patient, { - fetchPatientStart, - fetchPatientSuccess, - fetchPatient, - createPatientStart, - createPatientSuccess, - createPatient, - updatePatientStart, - updatePatientSuccess, - updatePatient, - addRelatedPerson, - addDiagnosis, addAllergy, - removeRelatedPerson, - updatePatientError, - createPatientError, addAllergyError, + addCarePlan, + addDiagnosis, addDiagnosisError, + addRelatedPerson, addRelatedPersonError, + createPatient, + createPatientError, + createPatientStart, + createPatientSuccess, + fetchPatient, + fetchPatientStart, + fetchPatientSuccess, + removeRelatedPerson, + updatePatient, + updatePatientError, + updatePatientStart, + updatePatientSuccess, + addCarePlanError, } from '../../patients/patient-slice' import { RootState } from '../../store' import * as uuid from '../../util/uuid' @@ -637,4 +640,90 @@ describe('patients slice', () => { expect(onSuccessSpy).not.toHaveBeenCalled() }) }) + + describe('add care plan', () => { + it('should add a care plan', async () => { + const expectedCarePlanId = 'expected id' + const store = mockStore() + const expectedPatientId = '123' + + const expectedPatient = { + id: expectedPatientId, + givenName: 'some name', + } as Patient + + const expectedCarePlan = { + id: 'some id', + title: 'care plan title', + description: 'care plan description', + status: CarePlanStatus.Completed, + intent: CarePlanIntent.Proposal, + startDate: new Date().toISOString(), + endDate: new Date().toISOString(), + createdOn: new Date(Date.now()).toISOString(), + diagnosisId: 'some diagnosis id', + note: 'care plan note', + } as CarePlan + + const expectedUpdatedPatient = { + ...expectedPatient, + carePlans: [{ ...expectedCarePlan, id: expectedCarePlanId }], + } as Patient + + const findPatientSpy = jest + .spyOn(PatientRepository, 'find') + .mockResolvedValue(expectedPatient) + jest.spyOn(uuid, 'uuid').mockReturnValue(expectedCarePlanId) + jest.spyOn(PatientRepository, 'saveOrUpdate').mockResolvedValue(expectedUpdatedPatient) + const onSuccessSpy = jest.fn() + + await store.dispatch(addCarePlan(expectedPatientId, expectedCarePlan, onSuccessSpy)) + + expect(findPatientSpy).toHaveBeenCalledWith(expectedPatientId) + expect(store.getActions()[1]).toEqual(updatePatientSuccess(expectedUpdatedPatient)) + expect(onSuccessSpy).toHaveBeenCalledWith(expectedUpdatedPatient) + }) + + it('should validate the required fields', async () => { + const expectedError = { + message: 'patient.carePlan.error.unableToAdd', + title: 'patient.carePlan.error.titleRequired', + description: 'patient.carePlan.error.descriptionRequired', + status: 'patient.carePlan.error.statusRequired', + intent: 'patient.carePlan.error.intentRequired', + startDate: 'patient.carePlan.error.startDateRequired', + endDate: 'patient.carePlan.error.endDateRequired', + condition: 'patient.carePlan.error.conditionRequired', + note: 'patient.carePlan.error.noteRequired', + } + const store = mockStore() + const expectedCarePlan = {} as CarePlan + const onSuccessSpy = jest.fn() + + await store.dispatch(addCarePlan('some id', expectedCarePlan, onSuccessSpy)) + + expect(store.getActions()[0]).toEqual(addCarePlanError(expectedError)) + expect(onSuccessSpy).not.toHaveBeenCalled() + }) + + it('should validate that start date is before end date', async () => { + const store = mockStore() + const expectedCarePlan = { + startDate: new Date().toISOString(), + endDate: subDays(new Date(), 1).toISOString(), + } as CarePlan + const onSuccessSpy = jest.fn() + + await store.dispatch(addCarePlan('some id', expectedCarePlan, onSuccessSpy)) + + expect(store.getActions()[0]).toEqual( + addCarePlanError( + expect.objectContaining({ + endDate: 'patient.carePlan.error.endDateMustBeAfterStartDate', + }), + ), + ) + expect(onSuccessSpy).not.toHaveBeenCalled() + }) + }) }) diff --git a/src/__tests__/patients/view/ViewPatient.test.tsx b/src/__tests__/patients/view/ViewPatient.test.tsx index 71207f49bd..3cc651b1e3 100644 --- a/src/__tests__/patients/view/ViewPatient.test.tsx +++ b/src/__tests__/patients/view/ViewPatient.test.tsx @@ -18,6 +18,8 @@ import Permissions from '../../../model/Permissions' import * as ButtonBarProvider from '../../../page-header/ButtonBarProvider' import * as titleUtil from '../../../page-header/useTitle' import Allergies from '../../../patients/allergies/Allergies' +import AppointmentsList from '../../../patients/appointments/AppointmentsList' +import CarePlanTab from '../../../patients/care-plans/CarePlanTab' import Diagnoses from '../../../patients/diagnoses/Diagnoses' import GeneralInformation from '../../../patients/GeneralInformation' import LabsTab from '../../../patients/labs/LabsTab' @@ -59,6 +61,7 @@ describe('ViewPatient', () => { store = mockStore({ patient: { patient }, user: { permissions }, + appointments: { appointments: [] }, } as any) history.push('/patients/123') @@ -132,7 +135,7 @@ describe('ViewPatient', () => { const tabs = tabsHeader.find(Tab) expect(tabsHeader).toHaveLength(1) - expect(tabs).toHaveLength(7) + expect(tabs).toHaveLength(8) expect(tabs.at(0).prop('label')).toEqual('patient.generalInformation') expect(tabs.at(1).prop('label')).toEqual('patient.relatedPersons.label') expect(tabs.at(2).prop('label')).toEqual('scheduling.appointments.label') @@ -140,6 +143,7 @@ describe('ViewPatient', () => { expect(tabs.at(4).prop('label')).toEqual('patient.diagnoses.label') expect(tabs.at(5).prop('label')).toEqual('patient.notes.label') expect(tabs.at(6).prop('label')).toEqual('patient.labs.label') + expect(tabs.at(7).prop('label')).toEqual('patient.carePlan.label') }) it('should mark the general information tab as active and render the general information component when route is /patients/:id', async () => { @@ -197,6 +201,30 @@ describe('ViewPatient', () => { expect(relatedPersonTab.prop('patient')).toEqual(patient) }) + it('should mark the appointments tab as active when it is clicked and render the appointments tab component when route is /patients/:id/appointments', async () => { + let wrapper: any + await act(async () => { + wrapper = await setup() + }) + + await act(async () => { + const tabsHeader = wrapper.find(TabsHeader) + const tabs = tabsHeader.find(Tab) + tabs.at(2).prop('onClick')() + }) + + wrapper.update() + + const tabsHeader = wrapper.find(TabsHeader) + const tabs = tabsHeader.find(Tab) + const appointmentsTab = wrapper.find(AppointmentsList) + + expect(history.location.pathname).toEqual(`/patients/${patient.id}/appointments`) + expect(tabs.at(2).prop('active')).toBeTruthy() + expect(appointmentsTab).toHaveLength(1) + expect(appointmentsTab.prop('patientId')).toEqual(patient.id) + }) + it('should mark the allergies tab as active when it is clicked and render the allergies component when route is /patients/:id/allergies', async () => { let wrapper: any await act(async () => { @@ -292,4 +320,27 @@ describe('ViewPatient', () => { expect(labsTab).toHaveLength(1) expect(labsTab.prop('patientId')).toEqual(patient.id) }) + + it('should mark the care plans tab as active when it is clicked and render the care plan tab component when route is /patients/:id/care-plans', async () => { + let wrapper: any + await act(async () => { + wrapper = await setup() + }) + + await act(async () => { + const tabsHeader = wrapper.find(TabsHeader) + const tabs = tabsHeader.find(Tab) + tabs.at(7).prop('onClick')() + }) + + wrapper.update() + + const tabsHeader = wrapper.find(TabsHeader) + const tabs = tabsHeader.find(Tab) + const carePlansTab = wrapper.find(CarePlanTab) + + expect(history.location.pathname).toEqual(`/patients/${patient.id}/care-plans`) + expect(tabs.at(7).prop('active')).toBeTruthy() + expect(carePlansTab).toHaveLength(1) + }) }) diff --git a/src/components/input/SelectWithLableFormGroup.tsx b/src/components/input/SelectWithLableFormGroup.tsx index be9874aca1..fff067037c 100644 --- a/src/components/input/SelectWithLableFormGroup.tsx +++ b/src/components/input/SelectWithLableFormGroup.tsx @@ -10,18 +10,37 @@ interface Props { value: string label: string name: string + isRequired?: boolean isEditable?: boolean options: Option[] onChange?: (event: React.ChangeEvent) => void + feedback?: string + isInvalid?: boolean } const SelectWithLabelFormGroup = (props: Props) => { - const { value, label, name, isEditable, options, onChange } = props + const { + value, + label, + name, + isEditable, + options, + onChange, + isRequired, + feedback, + isInvalid, + } = props const id = `${name}Select` return (
-
) diff --git a/src/user/user-slice.ts b/src/user/user-slice.ts index afcb489b76..cd25a963c6 100644 --- a/src/user/user-slice.ts +++ b/src/user/user-slice.ts @@ -25,6 +25,8 @@ const initialState: UserState = { Permissions.ViewIncident, Permissions.ViewIncidents, Permissions.ReportIncident, + Permissions.AddCarePlan, + Permissions.ReadCarePlan, ], user: { id: 'some-hardcoded-id',