From c077d75eb654747d5c6691369210942184325512 Mon Sep 17 00:00:00 2001 From: Jack Meyer Date: Wed, 4 Mar 2020 22:08:24 -0600 Subject: [PATCH] feat(appointments): add callbacks for appointment actions --- .../appointments/appointment-slice.test.ts | 78 +++++++++---------- .../edit/EditAppointment.test.tsx | 18 +++++ .../appointments/new/NewAppointment.test.tsx | 40 +++++++++- .../view/ViewAppointment.test.tsx | 25 ++++++ .../appointments/appointment-slice.ts | 42 +++++----- .../appointments/edit/EditAppointment.tsx | 6 +- .../appointments/new/NewAppointment.tsx | 8 +- .../appointments/view/ViewAppointment.tsx | 9 ++- 8 files changed, 159 insertions(+), 67 deletions(-) diff --git a/src/__tests__/scheduling/appointments/appointment-slice.test.ts b/src/__tests__/scheduling/appointments/appointment-slice.test.ts index ba46f6404d..43d7e6f20f 100644 --- a/src/__tests__/scheduling/appointments/appointment-slice.test.ts +++ b/src/__tests__/scheduling/appointments/appointment-slice.test.ts @@ -135,7 +135,7 @@ describe('appointment slice', () => { reason: 'reason', } as Appointment - await createAppointment(expectedAppointment, createMemoryHistory())(dispatch, getState, null) + await createAppointment(expectedAppointment)(dispatch, getState, null) expect(dispatch).toHaveBeenCalledWith({ type: createAppointmentStart.type }) }) @@ -155,18 +155,21 @@ describe('appointment slice', () => { reason: 'reason', } as Appointment - await createAppointment(expectedAppointment, createMemoryHistory())(dispatch, getState, null) + await createAppointment(expectedAppointment)(dispatch, getState, null) expect(appointmentRepositorySaveSpy).toHaveBeenCalled() expect(appointmentRepositorySaveSpy).toHaveBeenCalledWith(expectedAppointment) }) - it('should navigate the /appointments when an appointment is successfully created', async () => { + it('should call the onSuccess function', async () => { + const onSuccessSpy = jest.fn() jest.spyOn(AppointmentRepository, 'save') - mocked(AppointmentRepository, true).save.mockResolvedValue({ id: '123' } as Appointment) + const expectedSavedAppointment = { id: '123' } + mocked(AppointmentRepository, true).save.mockResolvedValue( + expectedSavedAppointment as Appointment, + ) const dispatch = jest.fn() const getState = jest.fn() - const history = createMemoryHistory() const expectedAppointment = { patientId: '123', @@ -177,9 +180,9 @@ describe('appointment slice', () => { reason: 'reason', } as Appointment - await createAppointment(expectedAppointment, history)(dispatch, getState, null) + await createAppointment(expectedAppointment, onSuccessSpy)(dispatch, getState, null) - expect(history.location.pathname).toEqual('/appointments') + expect(onSuccessSpy).toHaveBeenCalledWith(expectedSavedAppointment) }) }) @@ -247,22 +250,16 @@ describe('appointment slice', () => { describe('deleteAppointment()', () => { let deleteAppointmentSpy = jest.spyOn(AppointmentRepository, 'delete') - let toastSpy = jest.spyOn(components, 'Toast') beforeEach(() => { jest.resetAllMocks() deleteAppointmentSpy = jest.spyOn(AppointmentRepository, 'delete') - toastSpy = jest.spyOn(components, 'Toast') }) it('should dispatch the DELETE_APPOINTMENT_START action', async () => { const dispatch = jest.fn() const getState = jest.fn() - await deleteAppointment({ id: 'test1' } as Appointment, createMemoryHistory())( - dispatch, - getState, - null, - ) + await deleteAppointment({ id: 'test1' } as Appointment)(dispatch, getState, null) expect(dispatch).toHaveBeenCalledWith({ type: deleteAppointmentStart.type }) }) @@ -273,51 +270,31 @@ describe('appointment slice', () => { const dispatch = jest.fn() const getState = jest.fn() - await deleteAppointment(expectedAppointment, createMemoryHistory())(dispatch, getState, null) + await deleteAppointment(expectedAppointment)(dispatch, getState, null) expect(deleteAppointmentSpy).toHaveBeenCalledTimes(1) expect(deleteAppointmentSpy).toHaveBeenCalledWith(expectedAppointment) }) - it('should navigate to /appointments after deleting', async () => { - const history = createMemoryHistory() - const expectedAppointment = { id: 'appointmentId1' } as Appointment - + it('should call the onSuccess function after successfully deleting', async () => { + const onSuccessSpy = jest.fn() const dispatch = jest.fn() const getState = jest.fn() - await deleteAppointment(expectedAppointment, history)(dispatch, getState, null) - - expect(history.location.pathname).toEqual('/appointments') - }) - - it('should create a toast with a success message', async () => { - const dispatch = jest.fn() - const getState = jest.fn() - - await deleteAppointment({ id: 'test1' } as Appointment, createMemoryHistory())( + await deleteAppointment({ id: 'test1' } as Appointment, onSuccessSpy)( dispatch, getState, null, ) - expect(toastSpy).toHaveBeenCalledTimes(1) - expect(toastSpy).toHaveBeenLastCalledWith( - 'success', - 'states.success', - 'scheduling.appointments.successfullyDeleted', - ) + expect(onSuccessSpy).toHaveBeenCalled() }) it('should dispatch the DELETE_APPOINTMENT_SUCCESS action', async () => { const dispatch = jest.fn() const getState = jest.fn() - await deleteAppointment({ id: 'test1' } as Appointment, createMemoryHistory())( - dispatch, - getState, - null, - ) + await deleteAppointment({ id: 'test1' } as Appointment)(dispatch, getState, null) expect(dispatch).toHaveBeenCalledWith({ type: deleteAppointmentSuccess.type }) }) @@ -333,7 +310,7 @@ describe('appointment slice', () => { const mockedAppointmentRepository = mocked(AppointmentRepository, true) mockedAppointmentRepository.saveOrUpdate.mockResolvedValue(expectedAppointment) - await updateAppointment(expectedAppointment, createMemoryHistory())(dispatch, getState, null) + await updateAppointment(expectedAppointment)(dispatch, getState, null) expect(dispatch).toHaveBeenCalledWith({ type: updateAppointmentStart.type }) }) @@ -347,7 +324,7 @@ describe('appointment slice', () => { const mockedAppointmentRepository = mocked(AppointmentRepository, true) mockedAppointmentRepository.saveOrUpdate.mockResolvedValue(expectedAppointment) - await updateAppointment(expectedAppointment, createMemoryHistory())(dispatch, getState, null) + await updateAppointment(expectedAppointment)(dispatch, getState, null) expect(AppointmentRepository.saveOrUpdate).toHaveBeenCalledWith(expectedAppointment) }) @@ -361,12 +338,27 @@ describe('appointment slice', () => { const mockedAppointmentRepository = mocked(AppointmentRepository, true) mockedAppointmentRepository.saveOrUpdate.mockResolvedValue(expectedAppointment) - await updateAppointment(expectedAppointment, createMemoryHistory())(dispatch, getState, null) + await updateAppointment(expectedAppointment)(dispatch, getState, null) expect(dispatch).toHaveBeenCalledWith({ type: updateAppointmentSuccess.type, payload: expectedAppointment, }) }) + + it('should call on the onSuccess function after successfully updating', async () => { + const onSuccessSpy = jest.fn() + const dispatch = jest.fn() + const getState = jest.fn() + jest.spyOn(AppointmentRepository, 'saveOrUpdate') + const expectedAppointmentId = 'sliceId11' + const expectedAppointment = { id: expectedAppointmentId } as Appointment + const mockedAppointmentRepository = mocked(AppointmentRepository, true) + mockedAppointmentRepository.saveOrUpdate.mockResolvedValue(expectedAppointment) + + await updateAppointment(expectedAppointment, onSuccessSpy)(dispatch, getState, null) + + expect(onSuccessSpy).toHaveBeenCalledWith(expectedAppointment) + }) }) }) diff --git a/src/__tests__/scheduling/appointments/edit/EditAppointment.test.tsx b/src/__tests__/scheduling/appointments/edit/EditAppointment.test.tsx index ef5d059fbe..1deef6793c 100644 --- a/src/__tests__/scheduling/appointments/edit/EditAppointment.test.tsx +++ b/src/__tests__/scheduling/appointments/edit/EditAppointment.test.tsx @@ -184,6 +184,24 @@ describe('Edit Appointment', () => { ) }) + it('should navigate to /appointments/:id when save is successful', async () => { + let wrapper: any + await act(async () => { + wrapper = await setup() + }) + + wrapper.update() + + const saveButton = wrapper.find(Button).at(0) + const onClick = saveButton.prop('onClick') as any + + await act(async () => { + await onClick() + }) + + expect(history.location.pathname).toEqual('/appointments/123') + }) + it('should navigate to /appointments/:id when cancel is clicked', async () => { let wrapper: any await act(async () => { diff --git a/src/__tests__/scheduling/appointments/new/NewAppointment.test.tsx b/src/__tests__/scheduling/appointments/new/NewAppointment.test.tsx index b02fe24623..f797bee5b5 100644 --- a/src/__tests__/scheduling/appointments/new/NewAppointment.test.tsx +++ b/src/__tests__/scheduling/appointments/new/NewAppointment.test.tsx @@ -24,10 +24,13 @@ const mockStore = configureMockStore([thunk]) describe('New Appointment', () => { let history: MemoryHistory let store: MockStore + const expectedNewAppointment = { id: '123' } const setup = () => { jest.spyOn(AppointmentRepository, 'save') - mocked(AppointmentRepository, true).save.mockResolvedValue({ id: '123' } as Appointment) + mocked(AppointmentRepository, true).save.mockResolvedValue( + expectedNewAppointment as Appointment, + ) history = createMemoryHistory() store = mockStore({ @@ -154,6 +157,41 @@ describe('New Appointment', () => { expect(store.getActions()).toContainEqual(appointmentSlice.createAppointmentSuccess()) }) + it('should navigate to /appointments/:id when a new appointment is created', async () => { + let wrapper: any + await act(async () => { + wrapper = await setup() + }) + + const expectedAppointment = { + patientId: '123', + startDateTime: roundToNearestMinutes(new Date(), { nearestTo: 15 }).toISOString(), + endDateTime: addMinutes( + roundToNearestMinutes(new Date(), { nearestTo: 15 }), + 60, + ).toISOString(), + location: 'location', + reason: 'reason', + type: 'type', + } as Appointment + + act(() => { + const appointmentDetailForm = wrapper.find(AppointmentDetailForm) + const onFieldChange = appointmentDetailForm.prop('onFieldChange') + onFieldChange('patientId', expectedAppointment.patientId) + }) + wrapper.update() + const saveButton = wrapper.find(Button).at(0) + expect(saveButton.text().trim()).toEqual('actions.save') + const onClick = saveButton.prop('onClick') as any + + await act(async () => { + await onClick() + }) + + expect(history.location.pathname).toEqual(`/appointments/${expectedNewAppointment.id}`) + }) + it('should display an error if there is no patient id', async () => { let wrapper: any await act(async () => { diff --git a/src/__tests__/scheduling/appointments/view/ViewAppointment.test.tsx b/src/__tests__/scheduling/appointments/view/ViewAppointment.test.tsx index d1f69af573..7eabf0cfb8 100644 --- a/src/__tests__/scheduling/appointments/view/ViewAppointment.test.tsx +++ b/src/__tests__/scheduling/appointments/view/ViewAppointment.test.tsx @@ -12,6 +12,7 @@ import AppointmentRepository from 'clients/db/AppointmentRepository' import { mocked } from 'ts-jest/utils' import { act } from 'react-dom/test-utils' import { Spinner, Modal } from '@hospitalrun/components' +import * as components from '@hospitalrun/components' import AppointmentDetailForm from 'scheduling/appointments/AppointmentDetailForm' import PatientRepository from 'clients/db/PatientRepository' import Patient from 'model/Patient' @@ -261,5 +262,29 @@ describe('View Appointment', () => { expect(store.getActions()).toContainEqual(appointmentSlice.deleteAppointmentStart()) expect(store.getActions()).toContainEqual(appointmentSlice.deleteAppointmentSuccess()) }) + + it('should navigate to /appointments and display a message when delete is successful', async () => { + jest.spyOn(components, 'Toast') + const mockedComponents = mocked(components, true) + + let wrapper: any + await act(async () => { + wrapper = await setup(false, [Permissions.ReadAppointments, Permissions.DeleteAppointment]) + }) + + const deleteConfirmationModal = wrapper.find(Modal) + + await act(async () => { + await deleteConfirmationModal.prop('closeButton').onClick() + }) + wrapper.update() + + expect(history.location.pathname).toEqual('/appointments') + expect(mockedComponents.Toast).toHaveBeenCalledWith( + 'success', + 'states.success', + 'scheduling.appointments.successfullyDeleted', + ) + }) }) }) diff --git a/src/scheduling/appointments/appointment-slice.ts b/src/scheduling/appointments/appointment-slice.ts index 16ff88d4cf..e70ca7b354 100644 --- a/src/scheduling/appointments/appointment-slice.ts +++ b/src/scheduling/appointments/appointment-slice.ts @@ -71,36 +71,42 @@ export const fetchAppointment = (id: string): AppThunk => async (dispatch) => { dispatch(fetchAppointmentSuccess({ appointment, patient })) } -export const createAppointment = (appointment: Appointment, history: any): AppThunk => async ( - dispatch, -) => { +export const createAppointment = ( + appointment: Appointment, + onSuccess?: (appointment: Appointment) => void, +): AppThunk => async (dispatch) => { dispatch(createAppointmentStart()) - await AppointmentRepository.save(appointment) + const newAppointment = await AppointmentRepository.save(appointment) dispatch(createAppointmentSuccess()) - history.push('/appointments') + if (onSuccess) { + onSuccess(newAppointment) + } } -export const updateAppointment = (appointment: Appointment, history: any): AppThunk => async ( - dispatch, -) => { +export const updateAppointment = ( + appointment: Appointment, + onSuccess?: (appointment: Appointment) => void, +): AppThunk => async (dispatch) => { dispatch(updateAppointmentStart()) const updatedAppointment = await AppointmentRepository.saveOrUpdate(appointment) dispatch(updateAppointmentSuccess(updatedAppointment)) - history.push(`/appointments/${updatedAppointment.id}`) + + if (onSuccess) { + onSuccess(updatedAppointment) + } } -export const deleteAppointment = (appointment: Appointment, history: any): AppThunk => async ( - dispatch, -) => { +export const deleteAppointment = ( + appointment: Appointment, + onSuccess?: () => void, +): AppThunk => async (dispatch) => { dispatch(deleteAppointmentStart()) await AppointmentRepository.delete(appointment) - history.push('/appointments') - Toast( - 'success', - il8n.t('states.success'), - `${il8n.t('scheduling.appointments.successfullyDeleted')}`, - ) dispatch(deleteAppointmentSuccess()) + + if (onSuccess) { + onSuccess() + } } export default appointmentSlice.reducer diff --git a/src/scheduling/appointments/edit/EditAppointment.tsx b/src/scheduling/appointments/edit/EditAppointment.tsx index d2db65f976..e2d676b1ad 100644 --- a/src/scheduling/appointments/edit/EditAppointment.tsx +++ b/src/scheduling/appointments/edit/EditAppointment.tsx @@ -52,6 +52,10 @@ const EditAppointment = () => { history.push(`/appointments/${appointment.id}`) } + const onSaveSuccess = () => { + history.push(`/appointments/${appointment.id}`) + } + const onSave = () => { let newErrorMessage = '' if (isBefore(new Date(appointment.endDateTime), new Date(appointment.startDateTime))) { @@ -63,7 +67,7 @@ const EditAppointment = () => { return } - dispatch(updateAppointment(appointment as Appointment, history)) + dispatch(updateAppointment(appointment as Appointment, onSaveSuccess)) } const onFieldChange = (key: string, value: string | boolean) => { diff --git a/src/scheduling/appointments/new/NewAppointment.tsx b/src/scheduling/appointments/new/NewAppointment.tsx index df32be9fad..1fd6f00eb6 100644 --- a/src/scheduling/appointments/new/NewAppointment.tsx +++ b/src/scheduling/appointments/new/NewAppointment.tsx @@ -7,7 +7,7 @@ import { useDispatch } from 'react-redux' import Appointment from 'model/Appointment' import addMinutes from 'date-fns/addMinutes' import { isBefore } from 'date-fns' -import { Button } from '@hospitalrun/components' +import { Button, Toast } from '@hospitalrun/components' import useAddBreadcrumbs from '../../../breadcrumbs/useAddBreadcrumbs' import { createAppointment } from '../appointment-slice' import AppointmentDetailForm from '../AppointmentDetailForm' @@ -40,6 +40,10 @@ const NewAppointment = () => { history.push('/appointments') } + const onNewAppointmentSaveSuccess = (newAppointment: Appointment) => { + history.push(`/appointments/${newAppointment.id}`) + } + const onSave = () => { let newErrorMessage = '' if (!appointment.patientId) { @@ -54,7 +58,7 @@ const NewAppointment = () => { return } - dispatch(createAppointment(appointment as Appointment, history)) + dispatch(createAppointment(appointment as Appointment, onNewAppointmentSaveSuccess)) } const onFieldChange = (key: string, value: string | boolean) => { diff --git a/src/scheduling/appointments/view/ViewAppointment.tsx b/src/scheduling/appointments/view/ViewAppointment.tsx index 37a28fea2e..a262c8ddaa 100644 --- a/src/scheduling/appointments/view/ViewAppointment.tsx +++ b/src/scheduling/appointments/view/ViewAppointment.tsx @@ -3,7 +3,7 @@ import useTitle from 'page-header/useTitle' import { useSelector, useDispatch } from 'react-redux' import { RootState } from 'store' import { useParams, useHistory } from 'react-router' -import { Spinner, Button, Modal } from '@hospitalrun/components' +import { Spinner, Button, Modal, Toast } from '@hospitalrun/components' import { useTranslation } from 'react-i18next' import { useButtonToolbarSetter } from 'page-header/ButtonBarProvider' import Permissions from 'model/Permissions' @@ -29,8 +29,13 @@ const ViewAppointment = () => { setShowDeleteConfirmation(true) } + const onDeleteSuccess = () => { + history.push('/appointments') + Toast('success', t('states.success'), t('scheduling.appointments.successfullyDeleted')) + } + const onDeleteConfirmationButtonClick = () => { - dispatch(deleteAppointment(appointment, history)) + dispatch(deleteAppointment(appointment, onDeleteSuccess)) setShowDeleteConfirmation(false) }