From 611bbcaad15c20eb4bc6d0a5e7f839551d1636b2 Mon Sep 17 00:00:00 2001 From: Alex Tan Date: Tue, 14 Jul 2020 12:16:38 -0400 Subject: [PATCH 1/6] feat(navbar): add feature to display currently logged in user name in profile dropdown --- .../shared/components/navbar/Navbar.test.tsx | 14 ++++++++++++-- src/shared/components/navbar/Navbar.tsx | 9 ++++++++- src/shared/locales/enUs/translations/user/index.ts | 1 + 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/__tests__/shared/components/navbar/Navbar.test.tsx b/src/__tests__/shared/components/navbar/Navbar.test.tsx index d3c7b78d5d..82720f57fa 100644 --- a/src/__tests__/shared/components/navbar/Navbar.test.tsx +++ b/src/__tests__/shared/components/navbar/Navbar.test.tsx @@ -170,13 +170,22 @@ describe('Navbar', () => { }) describe('account', () => { - it('should render an account link list', () => { + it("should render a link with the user's identification", () => { const wrapper = setup(allPermissions) const hospitalRunNavbar = wrapper.find(HospitalRunNavbar) const accountLinkList = hospitalRunNavbar.find('.nav-account') const { children } = accountLinkList.first().props() as any - expect(children[0].props.children).toEqual([undefined, 'settings.label']) + expect(children[0].props.children).not.toBeUndefined() + }) + + it('should render a setting link list', () => { + const wrapper = setup(allPermissions) + const hospitalRunNavbar = wrapper.find(HospitalRunNavbar) + const accountLinkList = hospitalRunNavbar.find('.nav-account') + const { children } = accountLinkList.first().props() as any + + expect(children[1].props.children).toEqual([undefined, 'settings.label']) }) it('should navigate to /settings when the list option is selected', () => { @@ -187,6 +196,7 @@ describe('Navbar', () => { act(() => { children[0].props.onClick() + children[1].props.onClick() }) expect(history.location.pathname).toEqual('/settings') diff --git a/src/shared/components/navbar/Navbar.tsx b/src/shared/components/navbar/Navbar.tsx index e77262ce25..73a2caef7c 100644 --- a/src/shared/components/navbar/Navbar.tsx +++ b/src/shared/components/navbar/Navbar.tsx @@ -12,7 +12,7 @@ const Navbar = () => { const dispatch = useDispatch() const history = useHistory() const { t } = useTranslator() - const { permissions } = useSelector((state: RootState) => state.user) + const { permissions, user } = useSelector((state: RootState) => state.user) const navigateTo = (location: string) => { history.push(location) @@ -98,6 +98,13 @@ const Navbar = () => { type: 'link-list-icon', alignRight: true, children: [ + { + type: 'link', + label: `${t('user.login.success')}${user?.givenName} ${user?.familyName}`, + onClick: () => { + navigateTo('/settings') + }, + }, { type: 'link', label: t('settings.label'), diff --git a/src/shared/locales/enUs/translations/user/index.ts b/src/shared/locales/enUs/translations/user/index.ts index 0d3ea73660..5227461210 100644 --- a/src/shared/locales/enUs/translations/user/index.ts +++ b/src/shared/locales/enUs/translations/user/index.ts @@ -13,6 +13,7 @@ export default { required: 'Password is required.', }, }, + success: 'Currently signed in as ', }, }, } From bc5b903c8debf9cb05bbaec73391d3f3582ad28f Mon Sep 17 00:00:00 2001 From: Alex Tan Date: Thu, 16 Jul 2020 10:28:21 -0400 Subject: [PATCH 2/6] fix(navbar): add test for rendering username and rename signed-in translation key more clearly fix #2236 --- .../shared/components/navbar/Navbar.test.tsx | 16 ++++++++++++---- src/shared/components/navbar/Navbar.tsx | 4 +++- .../locales/enUs/translations/user/index.ts | 2 +- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/__tests__/shared/components/navbar/Navbar.test.tsx b/src/__tests__/shared/components/navbar/Navbar.test.tsx index 82720f57fa..6e550be95f 100644 --- a/src/__tests__/shared/components/navbar/Navbar.test.tsx +++ b/src/__tests__/shared/components/navbar/Navbar.test.tsx @@ -11,6 +11,7 @@ import thunk from 'redux-thunk' import Navbar from '../../../../shared/components/navbar/Navbar' import Permissions from '../../../../shared/model/Permissions' +import User from '../../../../shared/model/User' import { RootState } from '../../../../shared/store' const mockStore = createMockStore([thunk]) @@ -18,10 +19,10 @@ const mockStore = createMockStore([thunk]) describe('Navbar', () => { const history = createMemoryHistory() - const setup = (permissions: Permissions[]) => { + const setup = (permissions: Permissions[], user?: User) => { const store = mockStore({ title: '', - user: { permissions }, + user: { permissions, user }, } as any) const wrapper = mount( @@ -34,6 +35,11 @@ describe('Navbar', () => { return wrapper } + const userName = { + givenName: 'givenName', + familyName: 'familyName', + } as User + const allPermissions = [ Permissions.ReadPatients, Permissions.WritePatients, @@ -171,12 +177,14 @@ describe('Navbar', () => { describe('account', () => { it("should render a link with the user's identification", () => { - const wrapper = setup(allPermissions) + const expectedUserName = `user.login.currentlySignedInAs ${userName.givenName} ${userName.familyName}` + + const wrapper = setup(allPermissions, userName) const hospitalRunNavbar = wrapper.find(HospitalRunNavbar) const accountLinkList = hospitalRunNavbar.find('.nav-account') const { children } = accountLinkList.first().props() as any - expect(children[0].props.children).not.toBeUndefined() + expect(children[0].props.children).toEqual([undefined, expectedUserName]) }) it('should render a setting link list', () => { diff --git a/src/shared/components/navbar/Navbar.tsx b/src/shared/components/navbar/Navbar.tsx index 73a2caef7c..fb6107bd35 100644 --- a/src/shared/components/navbar/Navbar.tsx +++ b/src/shared/components/navbar/Navbar.tsx @@ -100,7 +100,9 @@ const Navbar = () => { children: [ { type: 'link', - label: `${t('user.login.success')}${user?.givenName} ${user?.familyName}`, + label: `${t('user.login.currentlySignedInAs')} ${user?.givenName} ${ + user?.familyName + }`, onClick: () => { navigateTo('/settings') }, diff --git a/src/shared/locales/enUs/translations/user/index.ts b/src/shared/locales/enUs/translations/user/index.ts index 5227461210..4911c39b66 100644 --- a/src/shared/locales/enUs/translations/user/index.ts +++ b/src/shared/locales/enUs/translations/user/index.ts @@ -13,7 +13,7 @@ export default { required: 'Password is required.', }, }, - success: 'Currently signed in as ', + currentlySignedInAs: 'Currently signed in as', }, }, } From a7db0ebb00a6d762dfea3f31160fd8dc64729fad Mon Sep 17 00:00:00 2001 From: Alex Tan Date: Thu, 23 Jul 2020 17:46:07 -0400 Subject: [PATCH 3/6] feat(imaging): create basic imaging module add permissions to request new imaging and view all imagings; add links to sidebar and navbar for creating new imaging and viewing imagings --- src/HospitalRun.tsx | 2 + src/imagings/Imagings.tsx | 40 +++++ src/imagings/ViewImagings.tsx | 117 ++++++++++++++ src/imagings/imaging-slice.ts | 103 ++++++++++++ src/imagings/imagings-slice.ts | 66 ++++++++ src/imagings/requests/NewImagingRequest.tsx | 152 ++++++++++++++++++ src/shared/components/Sidebar.tsx | 53 ++++++ src/shared/components/navbar/Navbar.tsx | 9 +- src/shared/components/navbar/pageMap.tsx | 12 ++ src/shared/config/pouchdb.ts | 6 + src/shared/db/ImagingRepository.ts | 18 +++ .../enUs/translations/imagings/index.ts | 35 ++++ src/shared/locales/enUs/translations/index.ts | 2 + src/shared/model/Imaging.ts | 13 ++ src/shared/model/Permissions.ts | 2 + src/shared/store/index.ts | 4 + src/user/user-slice.ts | 2 + 17 files changed, 635 insertions(+), 1 deletion(-) create mode 100644 src/imagings/Imagings.tsx create mode 100644 src/imagings/ViewImagings.tsx create mode 100644 src/imagings/imaging-slice.ts create mode 100644 src/imagings/imagings-slice.ts create mode 100644 src/imagings/requests/NewImagingRequest.tsx create mode 100644 src/shared/db/ImagingRepository.ts create mode 100644 src/shared/locales/enUs/translations/imagings/index.ts create mode 100644 src/shared/model/Imaging.ts diff --git a/src/HospitalRun.tsx b/src/HospitalRun.tsx index 6e9a9b406c..e64d545462 100644 --- a/src/HospitalRun.tsx +++ b/src/HospitalRun.tsx @@ -4,6 +4,7 @@ import { useSelector } from 'react-redux' import { Redirect, Route, Switch } from 'react-router-dom' import Dashboard from './dashboard/Dashboard' +import Imagings from './imagings/Imagings' import Incidents from './incidents/Incidents' import Labs from './labs/Labs' import Breadcrumbs from './page-header/breadcrumbs/Breadcrumbs' @@ -53,6 +54,7 @@ const HospitalRun = () => { + diff --git a/src/imagings/Imagings.tsx b/src/imagings/Imagings.tsx new file mode 100644 index 0000000000..0db7deacc8 --- /dev/null +++ b/src/imagings/Imagings.tsx @@ -0,0 +1,40 @@ +import React from 'react' +import { useSelector } from 'react-redux' +import { Switch } from 'react-router-dom' + +import useAddBreadcrumbs from '../page-header/breadcrumbs/useAddBreadcrumbs' +import PrivateRoute from '../shared/components/PrivateRoute' +import Permissions from '../shared/model/Permissions' +import { RootState } from '../shared/store' +import NewImagingRequest from './requests/NewImagingRequest' +import ImagingRequests from './ViewImagings' + +const Imagings = () => { + const { permissions } = useSelector((state: RootState) => state.user) + const breadcrumbs = [ + { + i18nKey: 'imagings.label', + location: '/imagings', + }, + ] + useAddBreadcrumbs(breadcrumbs, true) + + return ( + + + + + ) +} + +export default Imagings diff --git a/src/imagings/ViewImagings.tsx b/src/imagings/ViewImagings.tsx new file mode 100644 index 0000000000..46e41a428b --- /dev/null +++ b/src/imagings/ViewImagings.tsx @@ -0,0 +1,117 @@ +import { Button, Table } from '@hospitalrun/components' +import format from 'date-fns/format' +import React, { useState, useEffect, useCallback } from 'react' +import { useSelector, useDispatch } from 'react-redux' +import { useHistory } from 'react-router-dom' + +import { useButtonToolbarSetter } from '../page-header/button-toolbar/ButtonBarProvider' +import useTitle from '../page-header/title/useTitle' +// import SelectWithLabelFormGroup, { +// Option, +// } from '../shared/components/input/SelectWithLableFormGroup' +import useDebounce from '../shared/hooks/useDebounce' +import useTranslator from '../shared/hooks/useTranslator' +import Permissions from '../shared/model/Permissions' +import { RootState } from '../shared/store' +import { extractUsername } from '../shared/util/extractUsername' +import { searchImagings } from './imagings-slice' + +type ImagingFilter = 'requested' | 'completed' | 'canceled' | 'all' + +const ViewImagings = () => { + const { t } = useTranslator() + const history = useHistory() + const setButtons = useButtonToolbarSetter() + useTitle(t('imagings.label')) + + const { permissions } = useSelector((state: RootState) => state.user) + const dispatch = useDispatch() + const { imagings } = useSelector((state: RootState) => state.imagings) + const [searchFilter, setSearchFilter] = useState('all') + + const debouncedSearchText = useDebounce(' ', 500) + + const getButtons = useCallback(() => { + const buttons: React.ReactNode[] = [] + + if (permissions.includes(Permissions.RequestImaging)) { + buttons.push( + , + ) + } + + return buttons + }, [permissions, history, t]) + + useEffect(() => { + setSearchFilter('all' as ImagingFilter) + }, []) + + useEffect(() => { + dispatch(searchImagings(debouncedSearchText, searchFilter)) + }, [dispatch, debouncedSearchText, searchFilter]) + + useEffect(() => { + setButtons(getButtons()) + return () => { + setButtons([]) + } + }, [dispatch, getButtons, setButtons]) + + // const filterOptions: Option[] = [ + // { label: t('imagings.status.requested'), value: 'requested' }, + // { label: t('imagings.status.completed'), value: 'completed' }, + // { label: t('imagings.status.canceled'), value: 'canceled' }, + // { label: t('imagings.filter.all'), value: 'all' }, + // ] + return ( + <> + {/*
+
+ value === searchFilter)} + onChange={(values) => setSearchFilter(values[0] as ImagingFilter)} + isEditable + /> +
+
*/} + +
+ row.id} + columns={[ + { label: t('imagings.imaging.code'), key: 'code' }, + { label: t('imagings.imaging.type'), key: 'type' }, + { + label: t('imagings.imaging.requestedOn'), + key: 'requestedOn', + formatter: (row) => + row.requestedOn ? format(new Date(row.requestedOn), 'yyyy-MM-dd hh:mm a') : '', + }, + { label: t('imagings.imaging.patient'), key: 'patient' }, + { + label: t('imagings.imaging.requestedBy'), + key: 'requestedBy', + formatter: (row) => extractUsername(row.requestedBy), + }, // need to be formated later + { label: t('imagings.imaging.status'), key: 'status' }, + ]} + data={imagings} + /> + + + ) +} + +export default ViewImagings diff --git a/src/imagings/imaging-slice.ts b/src/imagings/imaging-slice.ts new file mode 100644 index 0000000000..5be2c06c03 --- /dev/null +++ b/src/imagings/imaging-slice.ts @@ -0,0 +1,103 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit' + +import ImagingRepository from '../shared/db/ImagingRepository' +import Imaging from '../shared/model/Imaging' +import Patient from '../shared/model/Patient' +import { AppThunk } from '../shared/store' + +interface Error { + patient?: string + type?: string + status?: string + message?: string +} + +interface ImagingState { + error: Error + imaging?: Imaging + patient?: Patient + status: 'loading' | 'error' | 'completed' +} + +const statusType: string[] = ['requested', 'completed', 'canceled'] + +const initialState: ImagingState = { + error: {}, + imaging: undefined, + patient: undefined, + status: 'loading', +} + +function start(state: ImagingState) { + state.status = 'loading' +} + +function finish(state: ImagingState, { payload }: PayloadAction) { + state.status = 'completed' + state.imaging = payload + state.error = {} +} + +function error(state: ImagingState, { payload }: PayloadAction) { + state.status = 'error' + state.error = payload +} + +const imagingSlice = createSlice({ + name: 'imaging', + initialState, + reducers: { + requestImagingStart: start, + requestImagingSuccess: finish, + requestImagingError: error, + }, +}) + +export const { + requestImagingStart, + requestImagingSuccess, + requestImagingError, +} = imagingSlice.actions + +const validateImagingRequest = (newImaging: Imaging): Error => { + const imagingRequestError: Error = {} + if (!newImaging.patient) { + imagingRequestError.patient = 'imagings.requests.error.patientRequired' + } + + if (!newImaging.type) { + imagingRequestError.type = 'imagings.requests.error.typeRequired' + } + + if (!newImaging.status) { + imagingRequestError.status = 'imagings.requests.error.statusRequired' + } else if (!statusType.includes(newImaging.status)) { + imagingRequestError.status = 'imagings.requests.error.incorrectStatus' + } + + return imagingRequestError +} + +export const requestImaging = ( + newImaging: Imaging, + onSuccess?: (imaging: Imaging) => void, +): AppThunk => async (dispatch, getState) => { + dispatch(requestImagingStart()) + + const imagingRequestError = validateImagingRequest(newImaging) + if (Object.keys(imagingRequestError).length > 0) { + imagingRequestError.message = 'imagings.requests.error.unableToRequest' + dispatch(requestImagingError(imagingRequestError)) + } else { + newImaging.requestedOn = new Date(Date.now().valueOf()).toISOString() + newImaging.requestedBy = getState().user.user?.id || '' + const requestedImaging = await ImagingRepository.save(newImaging) + dispatch(requestImagingSuccess(requestedImaging)) + + if (onSuccess) { + onSuccess(requestedImaging) + } + } +} + +export default imagingSlice.reducer diff --git a/src/imagings/imagings-slice.ts b/src/imagings/imagings-slice.ts new file mode 100644 index 0000000000..e58fd2fc0d --- /dev/null +++ b/src/imagings/imagings-slice.ts @@ -0,0 +1,66 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit' + +import ImagingRepository from '../shared/db/ImagingRepository' +import SortRequest from '../shared/db/SortRequest' +import Imaging from '../shared/model/Imaging' +import { AppThunk } from '../shared/store' + +interface ImagingsState { + isLoading: boolean + imagings: Imaging[] + statusFilter: status +} + +type status = 'requested' | 'completed' | 'canceled' | 'all' + +const defaultSortRequest: SortRequest = { + sorts: [ + { + field: 'requestedOn', + direction: 'desc', + }, + ], +} + +const initialState: ImagingsState = { + isLoading: false, + imagings: [], + statusFilter: 'all', +} + +const startLoading = (state: ImagingsState) => { + state.isLoading = true +} + +const imagingsSlice = createSlice({ + name: 'imagings', + initialState, + reducers: { + fetchImagingsStart: startLoading, + fetchImagingsSuccess(state, { payload }: PayloadAction) { + state.isLoading = false + state.imagings = payload + }, + }, +}) +export const { fetchImagingsStart, fetchImagingsSuccess } = imagingsSlice.actions + +export const searchImagings = (text: string, status: status): AppThunk => async (dispatch) => { + dispatch(fetchImagingsStart()) + + let imagings + + if (text.trim() === '' && status === initialState.statusFilter) { + imagings = await ImagingRepository.findAll(defaultSortRequest) + } else { + imagings = await ImagingRepository.search({ + text, + status, + defaultSortRequest, + }) + } + + dispatch(fetchImagingsSuccess(imagings)) +} + +export default imagingsSlice.reducer diff --git a/src/imagings/requests/NewImagingRequest.tsx b/src/imagings/requests/NewImagingRequest.tsx new file mode 100644 index 0000000000..3cc442fe43 --- /dev/null +++ b/src/imagings/requests/NewImagingRequest.tsx @@ -0,0 +1,152 @@ +import { Typeahead, Label, Button, Alert } from '@hospitalrun/components' +import React, { useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { useHistory } from 'react-router-dom' + +import useAddBreadcrumbs from '../../page-header/breadcrumbs/useAddBreadcrumbs' +import useTitle from '../../page-header/title/useTitle' +import SelectWithLabelFormGroup, { + Option, +} from '../../shared/components/input/SelectWithLableFormGroup' +import TextFieldWithLabelFormGroup from '../../shared/components/input/TextFieldWithLabelFormGroup' +import TextInputWithLabelFormGroup from '../../shared/components/input/TextInputWithLabelFormGroup' +import PatientRepository from '../../shared/db/PatientRepository' +import useTranslator from '../../shared/hooks/useTranslator' +import Imaging from '../../shared/model/Imaging' +import Patient from '../../shared/model/Patient' +import { RootState } from '../../shared/store' +import { requestImaging } from '../imaging-slice' + +const NewImagingRequest = () => { + const { t } = useTranslator() + const dispatch = useDispatch() + const history = useHistory() + useTitle(t('imagings.requests.new')) + const { status, error } = useSelector((state: RootState) => state.imaging) + + const statusOptions: Option[] = [ + { label: t('imagings.status.requested'), value: 'requested' }, + { label: t('imagings.status.completed'), value: 'completed' }, + { label: t('imagings.status.canceled'), value: 'canceled' }, + ] + + const [newImagingRequest, setNewImagingRequest] = useState({ + patient: '', + type: '', + notes: '', + status: '', + }) + + const breadcrumbs = [ + { + i18nKey: 'imagings.requests.new', + location: `/imagings/new`, + }, + ] + useAddBreadcrumbs(breadcrumbs) + + const onPatientChange = (patient: Patient) => { + setNewImagingRequest((previousNewImagingRequest) => ({ + ...previousNewImagingRequest, + patient: patient.fullName as string, + })) + } + + const onImagingTypeChange = (event: React.ChangeEvent) => { + const type = event.currentTarget.value + setNewImagingRequest((previousNewImagingRequest) => ({ + ...previousNewImagingRequest, + type, + })) + } + + const onStatusChange = (value: string) => { + setNewImagingRequest((previousNewImagingRequest) => ({ + ...previousNewImagingRequest, + status: value, + })) + } + + const onNoteChange = (event: React.ChangeEvent) => { + const notes = event.currentTarget.value + setNewImagingRequest((previousNewImagingRequest) => ({ + ...previousNewImagingRequest, + notes, + })) + } + + const onSave = async () => { + const newImaging = newImagingRequest as Imaging + const onSuccess = () => { + history.push(`/imagings`) + } + + dispatch(requestImaging(newImaging, onSuccess)) + } + + const onCancel = () => { + history.push('/imagings') + } + + return ( + <> + {status === 'error' && ( + + )} +
+
+
+ + onStatusChange(values[0])} + /> +
+ +
+
+
+ + +
+
+ + + ) +} + +export default NewImagingRequest diff --git a/src/shared/components/Sidebar.tsx b/src/shared/components/Sidebar.tsx index cc48668548..8e412614a7 100644 --- a/src/shared/components/Sidebar.tsx +++ b/src/shared/components/Sidebar.tsx @@ -44,6 +44,8 @@ const Sidebar = () => { ? 'labs' : splittedPath[1].includes('incidents') ? 'incidents' + : splittedPath[1].includes('imagings') + ? 'imagings' : 'none', ) @@ -297,6 +299,56 @@ const Sidebar = () => { ) + const getImagingLinks = () => ( + <> + { + navigateTo('/imagings') + setExpansion('imagings') + }} + className="nav-item" + style={listItemStyle} + > + + {!sidebarCollapsed && t('imagings.label')} + + {splittedPath[1].includes('imagings') && expandedItem === 'imagings' && ( + + {permissions.includes(Permissions.RequestImaging) && ( + navigateTo('/imagings/new')} + active={splittedPath[1].includes('imagings') && splittedPath.length > 2} + > + + {!sidebarCollapsed && t('imagings.requests.new')} + + )} + {permissions.includes(Permissions.ViewImagings) && ( + navigateTo('/imagings')} + active={splittedPath[1].includes('imagings') && splittedPath.length < 3} + > + + {!sidebarCollapsed && t('imagings.requests.label')} + + )} + + )} + + ) + return ( diff --git a/src/shared/components/navbar/Navbar.tsx b/src/shared/components/navbar/Navbar.tsx index d51390b192..e9c5d79c28 100644 --- a/src/shared/components/navbar/Navbar.tsx +++ b/src/shared/components/navbar/Navbar.tsx @@ -22,6 +22,7 @@ const Navbar = () => { 'scheduling.appointments.new', 'labs.requests.new', 'incidents.reports.new', + 'imagings.requests.new', 'settings.label', ] @@ -43,7 +44,13 @@ const Navbar = () => { const hambergerPages = Object.keys(pageMap).map((key) => pageMap[key]) // For Desktop, add shortcuts menu - const addPages = [pageMap.newPatient, pageMap.newAppointment, pageMap.newLab, pageMap.newIncident] + const addPages = [ + pageMap.newPatient, + pageMap.newAppointment, + pageMap.newLab, + pageMap.newIncident, + pageMap.newImaging, + ] return ( { + constructor() { + super('imaging', relationalDb) + } + + async save(entity: Imaging): Promise { + const imagingCode = generateCode('I') + entity.code = imagingCode + return super.save(entity) + } +} + +export default new ImagingRepository() diff --git a/src/shared/locales/enUs/translations/imagings/index.ts b/src/shared/locales/enUs/translations/imagings/index.ts new file mode 100644 index 0000000000..b1d86aa4b5 --- /dev/null +++ b/src/shared/locales/enUs/translations/imagings/index.ts @@ -0,0 +1,35 @@ +export default { + imagings: { + label: 'Imagings', + status: { + requested: 'Requested', + completed: 'Completed', + canceled: 'Canceled', + }, + requests: { + label: 'Imaging Requests', + new: 'New Imaging Request', + view: 'View Imaging', + cancel: 'Cancel Imaging', + complete: 'Complete Imaging', + error: { + unableToRequest: 'Unable to create new imaging request.', + incorrectStatus: 'Incorrect Status', + typeRequired: 'Type is required.', + statusRequired: 'Status is required.', + patientRequired: 'Patient name is required.', + }, + }, + imaging: { + code: 'Imaging Code', + status: 'Status', + type: 'Type', + notes: 'Notes', + requestedOn: 'Requested On', + completedOn: 'Completed On', + canceledOn: 'Canceled On', + requestedBy: 'Requested By', + patient: 'Patient', + }, + }, +} diff --git a/src/shared/locales/enUs/translations/index.ts b/src/shared/locales/enUs/translations/index.ts index ec5eee0dd3..3039d0c06d 100644 --- a/src/shared/locales/enUs/translations/index.ts +++ b/src/shared/locales/enUs/translations/index.ts @@ -1,6 +1,7 @@ import actions from './actions' import bloodType from './blood-type' import dashboard from './dashboard' +import imagings from './imagings' import incidents from './incidents' import labs from './labs' import networkStatus from './network-status' @@ -26,4 +27,5 @@ export default { ...settings, ...user, ...bloodType, + ...imagings, } diff --git a/src/shared/model/Imaging.ts b/src/shared/model/Imaging.ts new file mode 100644 index 0000000000..e423d983fd --- /dev/null +++ b/src/shared/model/Imaging.ts @@ -0,0 +1,13 @@ +import AbstractDBModel from './AbstractDBModel' + +export default interface Imaging extends AbstractDBModel { + code: string + patient: string + type: string + status: 'requested' | 'completed' | 'canceled' + requestedOn: string + requestedBy: string // will be the currently logged in user's id + completedOn?: string + canceledOn?: string + notes?: string +} diff --git a/src/shared/model/Permissions.ts b/src/shared/model/Permissions.ts index f9c6d2ac1f..917ca2d6fd 100644 --- a/src/shared/model/Permissions.ts +++ b/src/shared/model/Permissions.ts @@ -17,6 +17,8 @@ enum Permissions { ResolveIncident = 'resolve:incident', AddCarePlan = 'write:care_plan', ReadCarePlan = 'read:care_plan', + RequestImaging = 'write:imaging', + ViewImagings = 'read:imagings', } export default Permissions diff --git a/src/shared/store/index.ts b/src/shared/store/index.ts index 4ac0fa9576..68ace1c86f 100644 --- a/src/shared/store/index.ts +++ b/src/shared/store/index.ts @@ -1,6 +1,8 @@ import { configureStore, combineReducers, Action } from '@reduxjs/toolkit' import ReduxThunk, { ThunkAction } from 'redux-thunk' +import imaging from '../../imagings/imaging-slice' +import imagings from '../../imagings/imagings-slice' import incident from '../../incidents/incident-slice' import incidents from '../../incidents/incidents-slice' import lab from '../../labs/lab-slice' @@ -27,6 +29,8 @@ const reducer = combineReducers({ incident, incidents, labs, + imagings, + imaging, }) const store = configureStore({ diff --git a/src/user/user-slice.ts b/src/user/user-slice.ts index 660df6262a..388a84ee18 100644 --- a/src/user/user-slice.ts +++ b/src/user/user-slice.ts @@ -38,6 +38,8 @@ const initialState: UserState = { Permissions.ResolveIncident, Permissions.AddCarePlan, Permissions.ReadCarePlan, + Permissions.ViewImagings, + Permissions.RequestImaging, ], } From 4b49f72ea56375b7363372f0e9f558b13ed5e7cd Mon Sep 17 00:00:00 2001 From: Alex Tan Date: Tue, 28 Jul 2020 10:20:28 -0400 Subject: [PATCH 4/6] feat(imaging): add imagings unit tests and removed unused component fix #2242 --- src/__tests__/HospitalRun.test.tsx | 50 ++++ src/__tests__/imagings/Imagings.test.tsx | 74 +++++ src/__tests__/imagings/ViewImagings.test.tsx | 156 +++++++++++ src/__tests__/imagings/imaging-slice.test.ts | 122 +++++++++ src/__tests__/imagings/imagings-slice.test.ts | 146 ++++++++++ .../requests/NewImagingRequest.test.tsx | 259 ++++++++++++++++++ .../shared/components/Sidebar.test.tsx | 111 ++++++++ .../shared/components/navbar/Navbar.test.tsx | 7 +- .../shared/db/ImagingRepository.test.ts | 84 ++++++ src/imagings/ViewImagings.tsx | 22 -- src/imagings/requests/NewImagingRequest.tsx | 1 + src/shared/db/ImagingRepository.ts | 31 +++ 12 files changed, 1040 insertions(+), 23 deletions(-) create mode 100644 src/__tests__/imagings/Imagings.test.tsx create mode 100644 src/__tests__/imagings/ViewImagings.test.tsx create mode 100644 src/__tests__/imagings/imaging-slice.test.ts create mode 100644 src/__tests__/imagings/imagings-slice.test.ts create mode 100644 src/__tests__/imagings/requests/NewImagingRequest.test.tsx create mode 100644 src/__tests__/shared/db/ImagingRepository.test.ts diff --git a/src/__tests__/HospitalRun.test.tsx b/src/__tests__/HospitalRun.test.tsx index ef18ef3bb2..042f8b8560 100644 --- a/src/__tests__/HospitalRun.test.tsx +++ b/src/__tests__/HospitalRun.test.tsx @@ -9,11 +9,13 @@ import thunk from 'redux-thunk' import Dashboard from '../dashboard/Dashboard' import HospitalRun from '../HospitalRun' +import ViewImagings from '../imagings/ViewImagings' import Incidents from '../incidents/Incidents' import ViewLabs from '../labs/ViewLabs' import { addBreadcrumbs } from '../page-header/breadcrumbs/breadcrumbs-slice' import Appointments from '../scheduling/appointments/Appointments' import Settings from '../settings/Settings' +import ImagingRepository from '../shared/db/ImagingRepository' import LabRepository from '../shared/db/LabRepository' import Permissions from '../shared/model/Permissions' import { RootState } from '../shared/store' @@ -170,6 +172,54 @@ describe('HospitalRun', () => { }) }) + describe('/imagings', () => { + it('should render the Imagings component when /imagings is accessed', async () => { + jest.spyOn(ImagingRepository, 'findAll').mockResolvedValue([]) + const store = mockStore({ + title: 'test', + user: { user: { id: '123' }, permissions: [Permissions.ViewImagings] }, + imagings: { imagings: [] }, + breadcrumbs: { breadcrumbs: [] }, + components: { sidebarCollapsed: false }, + } as any) + + let wrapper: any + await act(async () => { + wrapper = await mount( + + + + + , + ) + }) + wrapper.update() + + expect(wrapper.find(ViewImagings)).toHaveLength(1) + }) + + it('should render the dashboard if the user does not have permissions to view imagings', () => { + jest.spyOn(LabRepository, 'findAll').mockResolvedValue([]) + const store = mockStore({ + title: 'test', + user: { user: { id: '123' }, permissions: [] }, + breadcrumbs: { breadcrumbs: [] }, + components: { sidebarCollapsed: false }, + } as any) + + const wrapper = mount( + + + + + , + ) + + expect(wrapper.find(ViewImagings)).toHaveLength(0) + expect(wrapper.find(Dashboard)).toHaveLength(1) + }) + }) + describe('/settings', () => { it('should render the Settings component when /settings is accessed', async () => { const store = mockStore({ diff --git a/src/__tests__/imagings/Imagings.test.tsx b/src/__tests__/imagings/Imagings.test.tsx new file mode 100644 index 0000000000..e258cc9fdf --- /dev/null +++ b/src/__tests__/imagings/Imagings.test.tsx @@ -0,0 +1,74 @@ +import { mount } from 'enzyme' +import React from 'react' +import { Provider } from 'react-redux' +import { MemoryRouter } from 'react-router-dom' +import createMockStore from 'redux-mock-store' +import thunk from 'redux-thunk' + +import Imagings from '../../imagings/Imagings' +import NewImagingRequest from '../../imagings/requests/NewImagingRequest' +import ImagingRepository from '../../shared/db/ImagingRepository' +import PatientRepository from '../../shared/db/PatientRepository' +import Imaging from '../../shared/model/Imaging' +import Patient from '../../shared/model/Patient' +import Permissions from '../../shared/model/Permissions' +import { RootState } from '../../shared/store' + +const mockStore = createMockStore([thunk]) + +describe('Imagings', () => { + jest.spyOn(ImagingRepository, 'findAll').mockResolvedValue([]) + jest + .spyOn(ImagingRepository, 'find') + .mockResolvedValue({ id: '1234', requestedOn: new Date().toISOString() } as Imaging) + jest + .spyOn(PatientRepository, 'find') + .mockResolvedValue({ id: '12345', fullName: 'test test' } as Patient) + + describe('routing', () => { + describe('/imagings/new', () => { + it('should render the new imaging request screen when /imagings/new is accessed', () => { + const store = mockStore({ + title: 'test', + user: { permissions: [Permissions.RequestImaging] }, + breadcrumbs: { breadcrumbs: [] }, + components: { sidebarCollapsed: false }, + imaging: { + imaging: { id: 'imagingId', patient: 'patient' } as Imaging, + patient: { id: 'patientId', fullName: 'some name' }, + error: {}, + }, + } as any) + + const wrapper = mount( + + + + + , + ) + + expect(wrapper.find(NewImagingRequest)).toHaveLength(1) + }) + + it('should not navigate to /imagings/new if the user does not have RequestLab permissions', () => { + const store = mockStore({ + title: 'test', + user: { permissions: [] }, + breadcrumbs: { breadcrumbs: [] }, + components: { sidebarCollapsed: false }, + } as any) + + const wrapper = mount( + + + + + , + ) + + expect(wrapper.find(NewImagingRequest)).toHaveLength(0) + }) + }) + }) +}) diff --git a/src/__tests__/imagings/ViewImagings.test.tsx b/src/__tests__/imagings/ViewImagings.test.tsx new file mode 100644 index 0000000000..fec576b52b --- /dev/null +++ b/src/__tests__/imagings/ViewImagings.test.tsx @@ -0,0 +1,156 @@ +import { Table } from '@hospitalrun/components' +import { act } from '@testing-library/react' +import { mount, ReactWrapper } from 'enzyme' +import { createMemoryHistory } from 'history' +import React from 'react' +import { Provider } from 'react-redux' +import { Router } from 'react-router-dom' +import createMockStore from 'redux-mock-store' +import thunk from 'redux-thunk' + +import ViewImagings from '../../imagings/ViewImagings' +import * as ButtonBarProvider from '../../page-header/button-toolbar/ButtonBarProvider' +import * as titleUtil from '../../page-header/title/useTitle' +import ImagingRepository from '../../shared/db/ImagingRepository' +import Imaging from '../../shared/model/Imaging' +import Permissions from '../../shared/model/Permissions' +import { RootState } from '../../shared/store' + +const mockStore = createMockStore([thunk]) + +describe('View Imagings', () => { + describe('title', () => { + let titleSpy: any + beforeEach(async () => { + const store = mockStore({ + title: '', + user: { permissions: [Permissions.ViewImagings, Permissions.RequestLab] }, + imagings: { imagings: [] }, + } as any) + titleSpy = jest.spyOn(titleUtil, 'default') + jest.spyOn(ImagingRepository, 'findAll').mockResolvedValue([]) + await act(async () => { + await mount( + + + + + , + ) + }) + }) + + it('should have the title', () => { + expect(titleSpy).toHaveBeenCalledWith('imagings.label') + }) + }) + + describe('button bar', () => { + it('should display button to add new imaging request', async () => { + const store = mockStore({ + title: '', + user: { permissions: [Permissions.ViewImagings, Permissions.RequestImaging] }, + imagings: { imagings: [] }, + } as any) + const setButtonToolBarSpy = jest.fn() + jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter').mockReturnValue(setButtonToolBarSpy) + jest.spyOn(ImagingRepository, 'findAll').mockResolvedValue([]) + await act(async () => { + await mount( + + + + + , + ) + }) + + const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0] + expect((actualButtons[0] as any).props.children).toEqual('imagings.requests.new') + }) + + it('should not display button to add new imaging request if the user does not have permissions', async () => { + const store = mockStore({ + title: '', + user: { permissions: [Permissions.ViewImagings] }, + imagings: { imagings: [] }, + } as any) + const setButtonToolBarSpy = jest.fn() + jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter').mockReturnValue(setButtonToolBarSpy) + jest.spyOn(ImagingRepository, 'findAll').mockResolvedValue([]) + await act(async () => { + await mount( + + + + + , + ) + }) + + const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0] + expect(actualButtons).toEqual([]) + }) + }) + + describe('table', () => { + let wrapper: ReactWrapper + let history: any + const expectedImaging = { + code: 'I-1234', + id: '1234', + type: 'imaging type', + patient: 'patient', + status: 'requested', + requestedOn: '2020-03-30T04:43:20.102Z', + requestedBy: 'some user', + } as Imaging + + beforeEach(async () => { + const store = mockStore({ + title: '', + user: { permissions: [Permissions.ViewImagings, Permissions.RequestImaging] }, + imagings: { imagings: [expectedImaging] }, + } as any) + history = createMemoryHistory() + + jest.spyOn(ImagingRepository, 'findAll').mockResolvedValue([expectedImaging]) + await act(async () => { + wrapper = await mount( + + + + + , + ) + }) + + wrapper.update() + }) + + it('should render a table with data', () => { + const table = wrapper.find(Table) + const columns = table.prop('columns') + expect(columns[0]).toEqual( + expect.objectContaining({ label: 'imagings.imaging.code', key: 'code' }), + ) + expect(columns[1]).toEqual( + expect.objectContaining({ label: 'imagings.imaging.type', key: 'type' }), + ) + expect(columns[2]).toEqual( + expect.objectContaining({ label: 'imagings.imaging.requestedOn', key: 'requestedOn' }), + ) + expect(columns[3]).toEqual( + expect.objectContaining({ label: 'imagings.imaging.patient', key: 'patient' }), + ) + expect(columns[4]).toEqual( + expect.objectContaining({ label: 'imagings.imaging.requestedBy', key: 'requestedBy' }), + ) + expect(columns[5]).toEqual( + expect.objectContaining({ label: 'imagings.imaging.status', key: 'status' }), + ) + + expect(table.prop('data')).toEqual([expectedImaging]) + }) + }) +}) diff --git a/src/__tests__/imagings/imaging-slice.test.ts b/src/__tests__/imagings/imaging-slice.test.ts new file mode 100644 index 0000000000..9a674ae194 --- /dev/null +++ b/src/__tests__/imagings/imaging-slice.test.ts @@ -0,0 +1,122 @@ +import createMockStore from 'redux-mock-store' +import thunk from 'redux-thunk' + +import imagingSlice, { + requestImaging, + requestImagingStart, + requestImagingSuccess, + requestImagingError, +} from '../../imagings/imaging-slice' +import ImagingRepository from '../../shared/db/ImagingRepository' +import Imaging from '../../shared/model/Imaging' +import { RootState } from '../../shared/store' + +const mockStore = createMockStore([thunk]) + +describe('imaging slice', () => { + describe('reducers', () => { + describe('requestImagingStart', () => { + it('should set status to loading', async () => { + const imagingStore = imagingSlice(undefined, requestImagingStart()) + + expect(imagingStore.status).toEqual('loading') + }) + }) + + describe('requestImagingSuccess', () => { + it('should set the imaging and status to success', () => { + const expectedImaging = { id: 'imagingId' } as Imaging + + const imagingStore = imagingSlice(undefined, requestImagingSuccess(expectedImaging)) + + expect(imagingStore.status).toEqual('completed') + expect(imagingStore.imaging).toEqual(expectedImaging) + }) + }) + + describe('requestImagingError', () => { + const expectedError = { message: 'some message', result: 'some result error' } + + const imagingStore = imagingSlice(undefined, requestImagingError(expectedError)) + + expect(imagingStore.status).toEqual('error') + expect(imagingStore.error).toEqual(expectedError) + }) + + describe('request imaging', () => { + const mockImaging = { + id: 'imagingId', + type: 'imagingType', + patient: 'patient', + status: 'requested', + } as Imaging + let imagingRepositorySaveSpy: any + + beforeEach(() => { + jest.restoreAllMocks() + Date.now = jest.fn().mockReturnValue(new Date().valueOf()) + imagingRepositorySaveSpy = jest + .spyOn(ImagingRepository, 'save') + .mockResolvedValue(mockImaging) + }) + + it('should request a new imaging', async () => { + const store = mockStore({ + user: { + user: { + id: '1234', + }, + }, + } as any) + + const expectedRequestedImaging = { + ...mockImaging, + requestedOn: new Date(Date.now()).toISOString(), + requestedBy: store.getState().user.user.id, + } as Imaging + + await store.dispatch(requestImaging(mockImaging)) + + const actions = store.getActions() + + expect(actions[0]).toEqual(requestImagingStart()) + expect(imagingRepositorySaveSpy).toHaveBeenCalledWith(expectedRequestedImaging) + expect(actions[1]).toEqual(requestImagingSuccess(expectedRequestedImaging)) + }) + + it('should execute the onSuccess callback if provided', async () => { + const store = mockStore({ + user: { + user: { + id: 'fake id', + }, + }, + } as any) + const onSuccessSpy = jest.fn() + + await store.dispatch(requestImaging(mockImaging, onSuccessSpy)) + expect(onSuccessSpy).toHaveBeenCalledWith(mockImaging) + }) + + it('should validate that the imaging can be requested', async () => { + const store = mockStore() + const onSuccessSpy = jest.fn() + await store.dispatch(requestImaging({} as Imaging, onSuccessSpy)) + + const actions = store.getActions() + + expect(actions[0]).toEqual(requestImagingStart()) + expect(actions[1]).toEqual( + requestImagingError({ + patient: 'imagings.requests.error.patientRequired', + type: 'imagings.requests.error.typeRequired', + status: 'imagings.requests.error.statusRequired', + message: 'imagings.requests.error.unableToRequest', + }), + ) + expect(imagingRepositorySaveSpy).not.toHaveBeenCalled() + expect(onSuccessSpy).not.toHaveBeenCalled() + }) + }) + }) +}) diff --git a/src/__tests__/imagings/imagings-slice.test.ts b/src/__tests__/imagings/imagings-slice.test.ts new file mode 100644 index 0000000000..e4b5447d59 --- /dev/null +++ b/src/__tests__/imagings/imagings-slice.test.ts @@ -0,0 +1,146 @@ +import { AnyAction } from 'redux' +import { mocked } from 'ts-jest/utils' + +import imagings, { + fetchImagingsStart, + fetchImagingsSuccess, + searchImagings, +} from '../../imagings/imagings-slice' +import ImagingRepository from '../../shared/db/ImagingRepository' +import SortRequest from '../../shared/db/SortRequest' +import Imaging from '../../shared/model/Imaging' + +interface SearchContainer { + text: string + status: 'requested' | 'completed' | 'canceled' | 'all' + defaultSortRequest: SortRequest +} + +const defaultSortRequest: SortRequest = { + sorts: [ + { + field: 'requestedOn', + direction: 'desc', + }, + ], +} + +const expectedSearchObject: SearchContainer = { + text: 'search string', + status: 'all', + defaultSortRequest, +} + +describe('imagings slice', () => { + beforeEach(() => { + jest.resetAllMocks() + }) + + describe('imagings reducer', () => { + it('should create the proper intial state with empty imagings array', () => { + const imagingsStore = imagings(undefined, {} as AnyAction) + expect(imagingsStore.isLoading).toBeFalsy() + expect(imagingsStore.imagings).toHaveLength(0) + expect(imagingsStore.statusFilter).toEqual('all') + }) + + it('it should handle the FETCH_IMAGINGS_SUCCESS action', () => { + const expectedImagings = [{ id: '1234' }] + const imagingsStore = imagings(undefined, { + type: fetchImagingsSuccess.type, + payload: expectedImagings, + }) + + expect(imagingsStore.isLoading).toBeFalsy() + expect(imagingsStore.imagings).toEqual(expectedImagings) + }) + }) + + describe('searchImagings', () => { + it('should dispatch the FETCH_IMAGINGS_START action', async () => { + const dispatch = jest.fn() + const getState = jest.fn() + + await searchImagings('search string', 'all')(dispatch, getState, null) + + expect(dispatch).toHaveBeenCalledWith({ type: fetchImagingsStart.type }) + }) + + it('should call the ImagingRepository search method with the correct search criteria', async () => { + const dispatch = jest.fn() + const getState = jest.fn() + jest.spyOn(ImagingRepository, 'search') + + await searchImagings(expectedSearchObject.text, expectedSearchObject.status)( + dispatch, + getState, + null, + ) + + expect(ImagingRepository.search).toHaveBeenCalledWith(expectedSearchObject) + }) + + it('should call the ImagingRepository findAll method if there is no string text and status is set to all', async () => { + const dispatch = jest.fn() + const getState = jest.fn() + jest.spyOn(ImagingRepository, 'findAll') + + await searchImagings('', expectedSearchObject.status)(dispatch, getState, null) + + expect(ImagingRepository.findAll).toHaveBeenCalledTimes(1) + }) + + it('should dispatch the FETCH_IMAGINGS_SUCCESS action', async () => { + const dispatch = jest.fn() + const getState = jest.fn() + + const expectedImagings = [ + { + type: 'text', + }, + ] as Imaging[] + + const mockedImagingRepository = mocked(ImagingRepository, true) + mockedImagingRepository.search.mockResolvedValue(expectedImagings) + + await searchImagings(expectedSearchObject.text, expectedSearchObject.status)( + dispatch, + getState, + null, + ) + + expect(dispatch).toHaveBeenLastCalledWith({ + type: fetchImagingsSuccess.type, + payload: expectedImagings, + }) + }) + }) + + describe('sort Request', () => { + it('should have called findAll with sort request in searchImagings method', async () => { + const dispatch = jest.fn() + const getState = jest.fn() + jest.spyOn(ImagingRepository, 'findAll') + + await searchImagings('', expectedSearchObject.status)(dispatch, getState, null) + + expect(ImagingRepository.findAll).toHaveBeenCalledWith( + expectedSearchObject.defaultSortRequest, + ) + }) + + it('should include sorts in the search criteria', async () => { + const dispatch = jest.fn() + const getState = jest.fn() + jest.spyOn(ImagingRepository, 'search') + + await searchImagings(expectedSearchObject.text, expectedSearchObject.status)( + dispatch, + getState, + null, + ) + + expect(ImagingRepository.search).toHaveBeenCalledWith(expectedSearchObject) + }) + }) +}) diff --git a/src/__tests__/imagings/requests/NewImagingRequest.test.tsx b/src/__tests__/imagings/requests/NewImagingRequest.test.tsx new file mode 100644 index 0000000000..e91cc69cd4 --- /dev/null +++ b/src/__tests__/imagings/requests/NewImagingRequest.test.tsx @@ -0,0 +1,259 @@ +import { Button, Typeahead, Label, Alert } from '@hospitalrun/components' +import { mount, ReactWrapper } 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 NewImagingRequest from '../../../imagings/requests/NewImagingRequest' +import * as titleUtil from '../../../page-header/title/useTitle' +import SelectWithLabelFormGroup from '../../../shared/components/input/SelectWithLableFormGroup' +import TextFieldWithLabelFormGroup from '../../../shared/components/input/TextFieldWithLabelFormGroup' +import TextInputWithLabelFormGroup from '../../../shared/components/input/TextInputWithLabelFormGroup' +import Imaging from '../../../shared/model/Imaging' +import Patient from '../../../shared/model/Patient' +import { RootState } from '../../../shared/store' + +const mockStore = createMockStore([thunk]) + +describe('New Imaging Request', () => { + describe('title and breadcrumbs', () => { + let titleSpy: any + const history = createMemoryHistory() + + beforeEach(() => { + const store = mockStore({ title: '', imaging: { status: 'loading', error: {} } } as any) + titleSpy = jest.spyOn(titleUtil, 'default') + history.push('/imagings/new') + + mount( + + + + + , + ) + }) + + it('should have New Imaging Request as the title', () => { + expect(titleSpy).toHaveBeenCalledWith('imagings.requests.new') + }) + }) + + describe('form layout', () => { + let wrapper: ReactWrapper + const history = createMemoryHistory() + + beforeEach(() => { + const store = mockStore({ title: '', imaging: { status: 'loading', error: {} } } as any) + history.push('/imagings/new') + + wrapper = mount( + + + + + , + ) + }) + + it('should render a patient typeahead', () => { + const typeaheadDiv = wrapper.find('.patient-typeahead') + + expect(typeaheadDiv).toBeDefined() + + const label = typeaheadDiv.find(Label) + const typeahead = typeaheadDiv.find(Typeahead) + + expect(label).toBeDefined() + expect(label.prop('text')).toEqual('imagings.imaging.patient') + expect(typeahead).toBeDefined() + expect(typeahead.prop('placeholder')).toEqual('imagings.imaging.patient') + expect(typeahead.prop('searchAccessor')).toEqual('fullName') + }) + + it('should render a type input box', () => { + const typeInputBox = wrapper.find(TextInputWithLabelFormGroup) + + expect(typeInputBox).toBeDefined() + expect(typeInputBox.prop('label')).toEqual('imagings.imaging.type') + expect(typeInputBox.prop('isRequired')).toBeTruthy() + expect(typeInputBox.prop('isEditable')).toBeTruthy() + }) + + it('should render a status types select', () => { + const statusTypesSelect = wrapper.find(SelectWithLabelFormGroup) + + expect(statusTypesSelect).toBeDefined() + expect(statusTypesSelect.prop('label')).toEqual('imagings.imaging.status') + expect(statusTypesSelect.prop('isRequired')).toBeTruthy() + expect(statusTypesSelect.prop('isEditable')).toBeTruthy() + expect(statusTypesSelect.prop('options')).toHaveLength(3) + expect(statusTypesSelect.prop('options')[0].label).toEqual('imagings.status.requested') + expect(statusTypesSelect.prop('options')[0].value).toEqual('requested') + expect(statusTypesSelect.prop('options')[1].label).toEqual('imagings.status.completed') + expect(statusTypesSelect.prop('options')[1].value).toEqual('completed') + expect(statusTypesSelect.prop('options')[2].label).toEqual('imagings.status.canceled') + expect(statusTypesSelect.prop('options')[2].value).toEqual('canceled') + }) + + it('should render a notes text field', () => { + const notesTextField = wrapper.find(TextFieldWithLabelFormGroup) + + expect(notesTextField).toBeDefined() + expect(notesTextField.prop('label')).toEqual('imagings.imaging.notes') + expect(notesTextField.prop('isRequired')).toBeFalsy() + expect(notesTextField.prop('isEditable')).toBeTruthy() + }) + + it('should render a save button', () => { + const saveButton = wrapper.find(Button).at(0) + expect(saveButton).toBeDefined() + expect(saveButton.text().trim()).toEqual('actions.save') + }) + + it('should render a cancel button', () => { + const cancelButton = wrapper.find(Button).at(1) + expect(cancelButton).toBeDefined() + expect(cancelButton.text().trim()).toEqual('actions.cancel') + }) + }) + + describe('errors', () => { + let wrapper: ReactWrapper + const history = createMemoryHistory() + const error = { + message: 'some message', + patient: 'some patient message', + type: 'some type error', + status: 'status type error', + } + + beforeEach(() => { + history.push('/imagings/new') + const store = mockStore({ title: '', imaging: { status: 'error', error } } as any) + wrapper = mount( + + + + + , + ) + }) + + it('should display errors', () => { + const alert = wrapper.find(Alert) + const typeInput = wrapper.find(TextInputWithLabelFormGroup) + const patientTypeahead = wrapper.find(Typeahead) + + expect(alert.prop('message')).toEqual(error.message) + expect(alert.prop('title')).toEqual('states.error') + expect(alert.prop('color')).toEqual('danger') + + expect(patientTypeahead.prop('isInvalid')).toBeTruthy() + + expect(typeInput.prop('feedback')).toEqual(error.type) + expect(typeInput.prop('isInvalid')).toBeTruthy() + }) + }) + + describe('on cancel', () => { + let wrapper: ReactWrapper + const history = createMemoryHistory() + + beforeEach(() => { + history.push('/imagings/new') + const store = mockStore({ title: '', imaging: { status: 'loading', error: {} } } as any) + wrapper = mount( + + + + + , + ) + }) + + it('should navigate back to /imagings', () => { + const cancelButton = wrapper.find(Button).at(1) + + act(() => { + const onClick = cancelButton.prop('onClick') as any + onClick({} as React.MouseEvent) + }) + + expect(history.location.pathname).toEqual('/imagings') + }) + }) + + describe('on save', () => { + let wrapper: ReactWrapper + const history = createMemoryHistory() + const expectedDate = new Date() + const expectedImaging = { + patient: 'patient', + type: 'expected type', + status: 'requested', + notes: 'expected notes', + id: '1234', + requestedOn: expectedDate.toISOString(), + } as Imaging + + beforeEach(() => { + jest.resetAllMocks() + Date.now = jest.fn(() => expectedDate.valueOf()) + + history.push('/imagings') + const store = mockStore({ + title: '', + imaging: { status: 'loading', error: {} }, + user: { user: { id: '1234' } }, + } as any) + wrapper = mount( + + + + + , + ) + }) + + it('should save the imaging request and navigate to "/imagings"', async () => { + const patientTypeahead = wrapper.find(Typeahead) + await act(async () => { + const onChange = patientTypeahead.prop('onChange') + await onChange([{ fullName: expectedImaging.patient }] as Patient[]) + }) + + const typeInput = wrapper.find(TextInputWithLabelFormGroup) + act(() => { + const onChange = typeInput.prop('onChange') as any + onChange({ currentTarget: { value: expectedImaging.type } }) + }) + + const statusSelect = wrapper.find(SelectWithLabelFormGroup) + act(() => { + const onChange = statusSelect.prop('onChange') as any + onChange({ currentTarget: { value: expectedImaging.status } }) + }) + + const notesTextField = wrapper.find(TextFieldWithLabelFormGroup) + act(() => { + const onChange = notesTextField.prop('onChange') as any + onChange({ currentTarget: { value: expectedImaging.notes } }) + }) + wrapper.update() + + const saveButton = wrapper.find(Button).at(0) + const onClick = saveButton.prop('onClick') as any + expect(saveButton.text().trim()).toEqual('actions.save') + await act(async () => { + await onClick() + }) + + expect(history.location.pathname).toEqual(`/imagings`) + }) + }) +}) diff --git a/src/__tests__/shared/components/Sidebar.test.tsx b/src/__tests__/shared/components/Sidebar.test.tsx index 34e78818fd..5b0e7ca989 100644 --- a/src/__tests__/shared/components/Sidebar.test.tsx +++ b/src/__tests__/shared/components/Sidebar.test.tsx @@ -32,6 +32,8 @@ describe('Sidebar', () => { Permissions.ViewIncidents, Permissions.ViewIncident, Permissions.ReportIncident, + Permissions.RequestImaging, + Permissions.ViewImagings, ] const store = mockStore({ components: { sidebarCollapsed: false }, @@ -532,4 +534,113 @@ describe('Sidebar', () => { expect(history.location.pathname).toEqual('/incidents') }) }) + + describe('imagings links', () => { + it('should render the main imagings link', () => { + const wrapper = setup('/imagings') + + const listItems = wrapper.find(ListItem) + + expect(listItems.at(6).text().trim()).toEqual('imagings.label') + }) + + it('should render the new imaging request link', () => { + const wrapper = setup('/imagings') + + const listItems = wrapper.find(ListItem) + + expect(listItems.at(7).text().trim()).toEqual('imagings.requests.new') + }) + + it('should not render the new imaging request link when user does not have the request imagings privileges', () => { + const wrapper = setupNoPermissions('/imagings') + + const listItems = wrapper.find(ListItem) + + listItems.forEach((_, i) => { + expect(listItems.at(i).text().trim()).not.toEqual('imagings.requests.new') + }) + }) + + it('should render the imagings list link', () => { + const wrapper = setup('/imagings') + + const listItems = wrapper.find(ListItem) + + expect(listItems.at(8).text().trim()).toEqual('imagings.requests.label') + }) + + it('should not render the imagings list link when user does not have the view imagings privileges', () => { + const wrapper = setupNoPermissions('/imagings') + + const listItems = wrapper.find(ListItem) + + listItems.forEach((_, i) => { + expect(listItems.at(i).text().trim()).not.toEqual('imagings.requests.label') + }) + }) + + it('main imagings link should be active when the current path is /imagings', () => { + const wrapper = setup('/imagings') + + const listItems = wrapper.find(ListItem) + + expect(listItems.at(6).prop('active')).toBeTruthy() + }) + + it('should navigate to /imagings when the main imagings link is clicked', () => { + const wrapper = setup('/') + + const listItems = wrapper.find(ListItem) + + act(() => { + const onClick = listItems.at(6).prop('onClick') as any + onClick() + }) + + expect(history.location.pathname).toEqual('/imagings') + }) + + it('new imaging request link should be active when the current path is /imagings/new', () => { + const wrapper = setup('/imagings/new') + + const listItems = wrapper.find(ListItem) + + expect(listItems.at(7).prop('active')).toBeTruthy() + }) + + it('should navigate to /imagings/new when the new imaging link is clicked', () => { + const wrapper = setup('/imagings') + + const listItems = wrapper.find(ListItem) + + act(() => { + const onClick = listItems.at(7).prop('onClick') as any + onClick() + }) + + expect(history.location.pathname).toEqual('/imagings/new') + }) + + it('imagings list link should be active when the current path is /imagings', () => { + const wrapper = setup('/imagings') + + const listItems = wrapper.find(ListItem) + + expect(listItems.at(8).prop('active')).toBeTruthy() + }) + + it('should navigate to /imagings when the imagings list link is clicked', () => { + const wrapper = setup('/imagings/new') + + const listItems = wrapper.find(ListItem) + + act(() => { + const onClick = listItems.at(8).prop('onClick') as any + onClick() + }) + + expect(history.location.pathname).toEqual('/imagings') + }) + }) }) diff --git a/src/__tests__/shared/components/navbar/Navbar.test.tsx b/src/__tests__/shared/components/navbar/Navbar.test.tsx index d0884a7883..f2afeb206d 100644 --- a/src/__tests__/shared/components/navbar/Navbar.test.tsx +++ b/src/__tests__/shared/components/navbar/Navbar.test.tsx @@ -56,6 +56,8 @@ describe('Navbar', () => { Permissions.ViewIncidents, Permissions.ViewIncident, Permissions.ReportIncident, + Permissions.RequestImaging, + Permissions.ViewImagings, ] describe('hamberger', () => { @@ -78,7 +80,7 @@ describe('Navbar', () => { }) it('should not show an item if user does not have a permission', () => { - // exclude labs and incidents permissions + // exclude labs, incidents, and imagings permissions const wrapper = setup(cloneDeep(allPermissions).slice(0, 6)) const hospitalRunNavbar = wrapper.find(HospitalRunNavbar) const hamberger = hospitalRunNavbar.find('.nav-hamberger') @@ -89,6 +91,8 @@ describe('Navbar', () => { 'labs.requests.label', 'incidents.reports.new', 'incidents.reports.label', + 'imagings.requests.new', + 'imagings.requests.label', ] children.forEach((option: any) => { @@ -151,6 +155,7 @@ describe('Navbar', () => { children.forEach((option: any) => { expect(option.props.children).not.toEqual('labs.requests.new') expect(option.props.children).not.toEqual('incidents.requests.new') + expect(option.props.children).not.toEqual('imagings.requests.new') }) }) }) diff --git a/src/__tests__/shared/db/ImagingRepository.test.ts b/src/__tests__/shared/db/ImagingRepository.test.ts new file mode 100644 index 0000000000..16b980e25f --- /dev/null +++ b/src/__tests__/shared/db/ImagingRepository.test.ts @@ -0,0 +1,84 @@ +import shortid from 'shortid' + +import { relationalDb } from '../../../shared/config/pouchdb' +import ImagingRepository from '../../../shared/db/ImagingRepository' +import Imaging from '../../../shared/model/Imaging' + +const uuidV4Regex = /^[A-F\d]{8}-[A-F\d]{4}-4[A-F\d]{3}-[89AB][A-F\d]{3}-[A-F\d]{12}$/i + +async function removeAllDocs() { + const docs = await relationalDb.rel.find('imaging') + docs.imagings.forEach(async (d: any) => { + await relationalDb.rel.del('imaging', d) + }) +} + +describe('imaging repository', () => { + describe('find', () => { + afterEach(async () => { + await removeAllDocs() + }) + + it('should return a imaging with the correct data', async () => { + await relationalDb.rel.save('imaging', { _id: 'id1111' }) + const expectedImaging = await relationalDb.rel.save('imaging', { id: 'id2222' }) + + const actualImaging = await ImagingRepository.find('id2222') + + expect(actualImaging).toBeDefined() + expect(actualImaging.id).toEqual(expectedImaging.id) + }) + }) + + describe('save', () => { + afterEach(async () => { + await removeAllDocs() + }) + + it('should generate an id that is a uuid for the imaging', async () => { + const newImaging = await ImagingRepository.save({ + patient: '123', + type: 'test', + status: 'status' as string, + notes: 'some notes', + } as Imaging) + + expect(uuidV4Regex.test(newImaging.id)).toBeTruthy() + }) + + it('should generate a imaging code', async () => { + const newImaging = await ImagingRepository.save({ + code: 'somecode', + patient: '123', + type: 'test', + status: 'status' as string, + notes: 'some notes', + } as Imaging) + + expect(shortid.isValid(newImaging.code)).toBeTruthy() + }) + + it('should generate a timestamp for created date and last updated date', async () => { + const newImaging = await ImagingRepository.save({ + patient: '123', + type: 'test', + status: 'status' as string, + notes: 'some notes', + } as Imaging) + + expect(newImaging.createdAt).toBeDefined() + expect(newImaging.updatedAt).toBeDefined() + }) + + it('should override the created date and last updated date even if one was passed in', async () => { + const unexpectedTime = new Date(2020, 2, 1).toISOString() + const newImaging = await ImagingRepository.save({ + createdAt: unexpectedTime, + updatedAt: unexpectedTime, + } as Imaging) + + expect(newImaging.createdAt).not.toEqual(unexpectedTime) + expect(newImaging.updatedAt).not.toEqual(unexpectedTime) + }) + }) +}) diff --git a/src/imagings/ViewImagings.tsx b/src/imagings/ViewImagings.tsx index 46e41a428b..3c484e0778 100644 --- a/src/imagings/ViewImagings.tsx +++ b/src/imagings/ViewImagings.tsx @@ -6,9 +6,6 @@ import { useHistory } from 'react-router-dom' import { useButtonToolbarSetter } from '../page-header/button-toolbar/ButtonBarProvider' import useTitle from '../page-header/title/useTitle' -// import SelectWithLabelFormGroup, { -// Option, -// } from '../shared/components/input/SelectWithLableFormGroup' import useDebounce from '../shared/hooks/useDebounce' import useTranslator from '../shared/hooks/useTranslator' import Permissions from '../shared/model/Permissions' @@ -66,27 +63,8 @@ const ViewImagings = () => { } }, [dispatch, getButtons, setButtons]) - // const filterOptions: Option[] = [ - // { label: t('imagings.status.requested'), value: 'requested' }, - // { label: t('imagings.status.completed'), value: 'completed' }, - // { label: t('imagings.status.canceled'), value: 'canceled' }, - // { label: t('imagings.filter.all'), value: 'all' }, - // ] return ( <> - {/*
-
- value === searchFilter)} - onChange={(values) => setSearchFilter(values[0] as ImagingFilter)} - isEditable - /> -
-
*/} -
row.id} diff --git a/src/imagings/requests/NewImagingRequest.tsx b/src/imagings/requests/NewImagingRequest.tsx index 3cc442fe43..9741417c12 100644 --- a/src/imagings/requests/NewImagingRequest.tsx +++ b/src/imagings/requests/NewImagingRequest.tsx @@ -123,6 +123,7 @@ const NewImagingRequest = () => { options={statusOptions} isRequired isEditable + defaultSelected={statusOptions.filter(({ value }) => value === newImagingRequest.status)} onChange={(values) => onStatusChange(values[0])} />
diff --git a/src/shared/db/ImagingRepository.ts b/src/shared/db/ImagingRepository.ts index 33056ea32d..147e023494 100644 --- a/src/shared/db/ImagingRepository.ts +++ b/src/shared/db/ImagingRepository.ts @@ -2,12 +2,43 @@ import { relationalDb } from '../config/pouchdb' import Imaging from '../model/Imaging' import generateCode from '../util/generateCode' import Repository from './Repository' +import SortRequest from './SortRequest' + +interface SearchContainer { + text: string + status: 'requested' | 'completed' | 'canceled' | 'all' + defaultSortRequest: SortRequest +} class ImagingRepository extends Repository { constructor() { super('imaging', relationalDb) } + async search(container: SearchContainer): Promise { + const searchValue = { $regex: RegExp(container.text, 'i') } + const selector = { + $and: [ + { + $or: [ + { + 'data.type': searchValue, + }, + { + 'data.code': searchValue, + }, + ], + }, + ...(container.status !== 'all' ? [{ 'data.status': container.status }] : [undefined]), + ].filter((x) => x !== undefined), + sorts: container.defaultSortRequest, + } + + return super.search({ + selector, + }) + } + async save(entity: Imaging): Promise { const imagingCode = generateCode('I') entity.code = imagingCode From c6c18cc38561d5e18f71b8dfa7dd5c68ae146a2a Mon Sep 17 00:00:00 2001 From: Alex Tan Date: Tue, 28 Jul 2020 10:59:37 -0400 Subject: [PATCH 5/6] fix(imaging): remove unused component fix #2242 --- src/imagings/ViewImagings.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/imagings/ViewImagings.tsx b/src/imagings/ViewImagings.tsx index 3c484e0778..3bf1eca9fa 100644 --- a/src/imagings/ViewImagings.tsx +++ b/src/imagings/ViewImagings.tsx @@ -6,7 +6,6 @@ import { useHistory } from 'react-router-dom' import { useButtonToolbarSetter } from '../page-header/button-toolbar/ButtonBarProvider' import useTitle from '../page-header/title/useTitle' -import useDebounce from '../shared/hooks/useDebounce' import useTranslator from '../shared/hooks/useTranslator' import Permissions from '../shared/model/Permissions' import { RootState } from '../shared/store' @@ -26,8 +25,6 @@ const ViewImagings = () => { const { imagings } = useSelector((state: RootState) => state.imagings) const [searchFilter, setSearchFilter] = useState('all') - const debouncedSearchText = useDebounce(' ', 500) - const getButtons = useCallback(() => { const buttons: React.ReactNode[] = [] @@ -53,8 +50,8 @@ const ViewImagings = () => { }, []) useEffect(() => { - dispatch(searchImagings(debouncedSearchText, searchFilter)) - }, [dispatch, debouncedSearchText, searchFilter]) + dispatch(searchImagings(' ', searchFilter)) + }, [dispatch, searchFilter]) useEffect(() => { setButtons(getButtons()) From f811cb9708933c4cb7a9cd3d55134c9e74097007 Mon Sep 17 00:00:00 2001 From: Alex Tan Date: Mon, 3 Aug 2020 14:12:58 -0400 Subject: [PATCH 6/6] feat(imaging): create setup function for the whole test suiteand replace route name to 'imaging' fix #2242 --- src/HospitalRun.tsx | 2 +- src/__tests__/HospitalRun.test.tsx | 8 +- src/__tests__/imagings/Imagings.test.tsx | 6 +- src/__tests__/imagings/ViewImagings.test.tsx | 147 ++++++-------- .../requests/NewImagingRequest.test.tsx | 179 ++++++++---------- .../shared/components/Sidebar.test.tsx | 16 +- src/imagings/Imagings.tsx | 8 +- src/imagings/ViewImagings.tsx | 4 +- src/imagings/requests/NewImagingRequest.tsx | 6 +- src/shared/components/Sidebar.tsx | 16 +- src/shared/components/navbar/pageMap.tsx | 4 +- .../enUs/translations/imagings/index.ts | 1 + 12 files changed, 169 insertions(+), 228 deletions(-) diff --git a/src/HospitalRun.tsx b/src/HospitalRun.tsx index e64d545462..a67923d623 100644 --- a/src/HospitalRun.tsx +++ b/src/HospitalRun.tsx @@ -54,7 +54,7 @@ const HospitalRun = () => { - +
diff --git a/src/__tests__/HospitalRun.test.tsx b/src/__tests__/HospitalRun.test.tsx index 042f8b8560..15d1b0fdae 100644 --- a/src/__tests__/HospitalRun.test.tsx +++ b/src/__tests__/HospitalRun.test.tsx @@ -172,8 +172,8 @@ describe('HospitalRun', () => { }) }) - describe('/imagings', () => { - it('should render the Imagings component when /imagings is accessed', async () => { + describe('/imaging', () => { + it('should render the Imagings component when /imaging is accessed', async () => { jest.spyOn(ImagingRepository, 'findAll').mockResolvedValue([]) const store = mockStore({ title: 'test', @@ -187,7 +187,7 @@ describe('HospitalRun', () => { await act(async () => { wrapper = await mount( - + , @@ -209,7 +209,7 @@ describe('HospitalRun', () => { const wrapper = mount( - + , diff --git a/src/__tests__/imagings/Imagings.test.tsx b/src/__tests__/imagings/Imagings.test.tsx index e258cc9fdf..eb3b03db63 100644 --- a/src/__tests__/imagings/Imagings.test.tsx +++ b/src/__tests__/imagings/Imagings.test.tsx @@ -26,8 +26,8 @@ describe('Imagings', () => { .mockResolvedValue({ id: '12345', fullName: 'test test' } as Patient) describe('routing', () => { - describe('/imagings/new', () => { - it('should render the new imaging request screen when /imagings/new is accessed', () => { + describe('/imaging/new', () => { + it('should render the new imaging request screen when /imaging/new is accessed', () => { const store = mockStore({ title: 'test', user: { permissions: [Permissions.RequestImaging] }, @@ -42,7 +42,7 @@ describe('Imagings', () => { const wrapper = mount( - + , diff --git a/src/__tests__/imagings/ViewImagings.test.tsx b/src/__tests__/imagings/ViewImagings.test.tsx index fec576b52b..94ac8b53fb 100644 --- a/src/__tests__/imagings/ViewImagings.test.tsx +++ b/src/__tests__/imagings/ViewImagings.test.tsx @@ -4,11 +4,12 @@ import { mount, ReactWrapper } from 'enzyme' import { createMemoryHistory } from 'history' import React from 'react' import { Provider } from 'react-redux' -import { Router } from 'react-router-dom' +import { Route, Router } from 'react-router-dom' import createMockStore from 'redux-mock-store' import thunk from 'redux-thunk' import ViewImagings from '../../imagings/ViewImagings' +import * as breadcrumbUtil from '../../page-header/breadcrumbs/useAddBreadcrumbs' import * as ButtonBarProvider from '../../page-header/button-toolbar/ButtonBarProvider' import * as titleUtil from '../../page-header/title/useTitle' import ImagingRepository from '../../shared/db/ImagingRepository' @@ -19,74 +20,72 @@ import { RootState } from '../../shared/store' const mockStore = createMockStore([thunk]) describe('View Imagings', () => { - describe('title', () => { - let titleSpy: any - beforeEach(async () => { - const store = mockStore({ - title: '', - user: { permissions: [Permissions.ViewImagings, Permissions.RequestLab] }, - imagings: { imagings: [] }, - } as any) - titleSpy = jest.spyOn(titleUtil, 'default') - jest.spyOn(ImagingRepository, 'findAll').mockResolvedValue([]) - await act(async () => { - await mount( + let history: any + let setButtonToolBarSpy: any + const expectedDate = new Date(2020, 5, 3, 19, 48) + const expectedImaging = { + code: 'I-1234', + id: '1234', + type: 'imaging type', + patient: 'patient', + status: 'requested', + requestedOn: expectedDate.toISOString(), + requestedBy: 'some user', + } as Imaging + + const setup = async (permissions: Permissions[], mockImagings?: any) => { + jest.resetAllMocks() + jest.spyOn(breadcrumbUtil, 'default') + setButtonToolBarSpy = jest.fn() + jest.spyOn(titleUtil, 'default') + jest.spyOn(ImagingRepository, 'findAll').mockResolvedValue(mockImagings) + jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter').mockReturnValue(setButtonToolBarSpy) + + history = createMemoryHistory() + history.push(`/imaging`) + + const store = mockStore({ + title: '', + user: { permissions }, + imagings: { imagings: mockImagings }, + } as any) + + let wrapper: any + await act(async () => { + wrapper = await mount( + - - + + + + - , - ) - }) + + , + ) }) - it('should have the title', () => { - expect(titleSpy).toHaveBeenCalledWith('imagings.label') + wrapper.update() + return wrapper as ReactWrapper + } + + describe('title', () => { + it('should have the title', async () => { + await setup([Permissions.ViewImagings], []) + expect(titleUtil.default).toHaveBeenCalledWith('imagings.label') }) }) describe('button bar', () => { it('should display button to add new imaging request', async () => { - const store = mockStore({ - title: '', - user: { permissions: [Permissions.ViewImagings, Permissions.RequestImaging] }, - imagings: { imagings: [] }, - } as any) - const setButtonToolBarSpy = jest.fn() - jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter').mockReturnValue(setButtonToolBarSpy) - jest.spyOn(ImagingRepository, 'findAll').mockResolvedValue([]) - await act(async () => { - await mount( - - - - - , - ) - }) + await setup([Permissions.ViewImagings, Permissions.RequestImaging], []) const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0] expect((actualButtons[0] as any).props.children).toEqual('imagings.requests.new') }) it('should not display button to add new imaging request if the user does not have permissions', async () => { - const store = mockStore({ - title: '', - user: { permissions: [Permissions.ViewImagings] }, - imagings: { imagings: [] }, - } as any) - const setButtonToolBarSpy = jest.fn() - jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter').mockReturnValue(setButtonToolBarSpy) - jest.spyOn(ImagingRepository, 'findAll').mockResolvedValue([]) - await act(async () => { - await mount( - - - - - , - ) - }) + await setup([Permissions.ViewImagings], []) const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0] expect(actualButtons).toEqual([]) @@ -94,41 +93,11 @@ describe('View Imagings', () => { }) describe('table', () => { - let wrapper: ReactWrapper - let history: any - const expectedImaging = { - code: 'I-1234', - id: '1234', - type: 'imaging type', - patient: 'patient', - status: 'requested', - requestedOn: '2020-03-30T04:43:20.102Z', - requestedBy: 'some user', - } as Imaging - - beforeEach(async () => { - const store = mockStore({ - title: '', - user: { permissions: [Permissions.ViewImagings, Permissions.RequestImaging] }, - imagings: { imagings: [expectedImaging] }, - } as any) - history = createMemoryHistory() - - jest.spyOn(ImagingRepository, 'findAll').mockResolvedValue([expectedImaging]) - await act(async () => { - wrapper = await mount( - - - - - , - ) - }) - - wrapper.update() - }) - - it('should render a table with data', () => { + it('should render a table with data', async () => { + const wrapper = await setup( + [Permissions.ViewIncident, Permissions.RequestImaging], + [expectedImaging], + ) const table = wrapper.find(Table) const columns = table.prop('columns') expect(columns[0]).toEqual( diff --git a/src/__tests__/imagings/requests/NewImagingRequest.test.tsx b/src/__tests__/imagings/requests/NewImagingRequest.test.tsx index e91cc69cd4..a05bfde5a4 100644 --- a/src/__tests__/imagings/requests/NewImagingRequest.test.tsx +++ b/src/__tests__/imagings/requests/NewImagingRequest.test.tsx @@ -4,15 +4,18 @@ 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 { Router, Route } from 'react-router-dom' import createMockStore from 'redux-mock-store' import thunk from 'redux-thunk' import NewImagingRequest from '../../../imagings/requests/NewImagingRequest' +import * as breadcrumbUtil from '../../../page-header/breadcrumbs/useAddBreadcrumbs' +import * as ButtonBarProvider from '../../../page-header/button-toolbar/ButtonBarProvider' import * as titleUtil from '../../../page-header/title/useTitle' import SelectWithLabelFormGroup from '../../../shared/components/input/SelectWithLableFormGroup' import TextFieldWithLabelFormGroup from '../../../shared/components/input/TextFieldWithLabelFormGroup' import TextInputWithLabelFormGroup from '../../../shared/components/input/TextInputWithLabelFormGroup' +import ImagingRepository from '../../../shared/db/ImagingRepository' import Imaging from '../../../shared/model/Imaging' import Patient from '../../../shared/model/Patient' import { RootState } from '../../../shared/store' @@ -20,47 +23,55 @@ import { RootState } from '../../../shared/store' const mockStore = createMockStore([thunk]) describe('New Imaging Request', () => { - describe('title and breadcrumbs', () => { - let titleSpy: any - const history = createMemoryHistory() - - beforeEach(() => { - const store = mockStore({ title: '', imaging: { status: 'loading', error: {} } } as any) - titleSpy = jest.spyOn(titleUtil, 'default') - history.push('/imagings/new') - - mount( - - - - - , + let history: any + let setButtonToolBarSpy: any + + const setup = async (status: string, error: any = {}) => { + jest.resetAllMocks() + jest.spyOn(breadcrumbUtil, 'default') + setButtonToolBarSpy = jest.fn() + jest.spyOn(titleUtil, 'default') + jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter').mockReturnValue(setButtonToolBarSpy) + + history = createMemoryHistory() + history.push(`/imaging/new`) + const store = mockStore({ + title: '', + user: { user: { id: '1234' } }, + imaging: { + status, + error, + }, + } as any) + + let wrapper: any + await act(async () => { + wrapper = await mount( + + + + + + + + + , ) }) + wrapper.update() + return wrapper as ReactWrapper + } - it('should have New Imaging Request as the title', () => { - expect(titleSpy).toHaveBeenCalledWith('imagings.requests.new') + describe('title and breadcrumbs', () => { + it('should have New Imaging Request as the title', async () => { + await setup('loading', {}) + expect(titleUtil.default).toHaveBeenCalledWith('imagings.requests.new') }) }) describe('form layout', () => { - let wrapper: ReactWrapper - const history = createMemoryHistory() - - beforeEach(() => { - const store = mockStore({ title: '', imaging: { status: 'loading', error: {} } } as any) - history.push('/imagings/new') - - wrapper = mount( - - - - - , - ) - }) - - it('should render a patient typeahead', () => { + it('should render a patient typeahead', async () => { + const wrapper = await setup('loading', {}) const typeaheadDiv = wrapper.find('.patient-typeahead') expect(typeaheadDiv).toBeDefined() @@ -75,7 +86,8 @@ describe('New Imaging Request', () => { expect(typeahead.prop('searchAccessor')).toEqual('fullName') }) - it('should render a type input box', () => { + it('should render a type input box', async () => { + const wrapper = await setup('loading', {}) const typeInputBox = wrapper.find(TextInputWithLabelFormGroup) expect(typeInputBox).toBeDefined() @@ -84,7 +96,8 @@ describe('New Imaging Request', () => { expect(typeInputBox.prop('isEditable')).toBeTruthy() }) - it('should render a status types select', () => { + it('should render a status types select', async () => { + const wrapper = await setup('loading', {}) const statusTypesSelect = wrapper.find(SelectWithLabelFormGroup) expect(statusTypesSelect).toBeDefined() @@ -100,7 +113,8 @@ describe('New Imaging Request', () => { expect(statusTypesSelect.prop('options')[2].value).toEqual('canceled') }) - it('should render a notes text field', () => { + it('should render a notes text field', async () => { + const wrapper = await setup('loading', {}) const notesTextField = wrapper.find(TextFieldWithLabelFormGroup) expect(notesTextField).toBeDefined() @@ -109,13 +123,15 @@ describe('New Imaging Request', () => { expect(notesTextField.prop('isEditable')).toBeTruthy() }) - it('should render a save button', () => { + it('should render a save button', async () => { + const wrapper = await setup('loading', {}) const saveButton = wrapper.find(Button).at(0) expect(saveButton).toBeDefined() expect(saveButton.text().trim()).toEqual('actions.save') }) - it('should render a cancel button', () => { + it('should render a cancel button', async () => { + const wrapper = await setup('loading', {}) const cancelButton = wrapper.find(Button).at(1) expect(cancelButton).toBeDefined() expect(cancelButton.text().trim()).toEqual('actions.cancel') @@ -123,8 +139,6 @@ describe('New Imaging Request', () => { }) describe('errors', () => { - let wrapper: ReactWrapper - const history = createMemoryHistory() const error = { message: 'some message', patient: 'some patient message', @@ -132,19 +146,8 @@ describe('New Imaging Request', () => { status: 'status type error', } - beforeEach(() => { - history.push('/imagings/new') - const store = mockStore({ title: '', imaging: { status: 'error', error } } as any) - wrapper = mount( - - - - - , - ) - }) - - it('should display errors', () => { + it('should display errors', async () => { + const wrapper = await setup('error', error) const alert = wrapper.find(Alert) const typeInput = wrapper.find(TextInputWithLabelFormGroup) const patientTypeahead = wrapper.find(Typeahead) @@ -161,22 +164,8 @@ describe('New Imaging Request', () => { }) describe('on cancel', () => { - let wrapper: ReactWrapper - const history = createMemoryHistory() - - beforeEach(() => { - history.push('/imagings/new') - const store = mockStore({ title: '', imaging: { status: 'loading', error: {} } } as any) - wrapper = mount( - - - - - , - ) - }) - - it('should navigate back to /imagings', () => { + it('should navigate back to /imaging', async () => { + const wrapper = await setup('loading', {}) const cancelButton = wrapper.find(Button).at(1) act(() => { @@ -184,43 +173,25 @@ describe('New Imaging Request', () => { onClick({} as React.MouseEvent) }) - expect(history.location.pathname).toEqual('/imagings') + expect(history.location.pathname).toEqual('/imaging') }) }) describe('on save', () => { - let wrapper: ReactWrapper - const history = createMemoryHistory() - const expectedDate = new Date() - const expectedImaging = { - patient: 'patient', - type: 'expected type', - status: 'requested', - notes: 'expected notes', - id: '1234', - requestedOn: expectedDate.toISOString(), - } as Imaging - - beforeEach(() => { - jest.resetAllMocks() - Date.now = jest.fn(() => expectedDate.valueOf()) - - history.push('/imagings') - const store = mockStore({ - title: '', - imaging: { status: 'loading', error: {} }, - user: { user: { id: '1234' } }, - } as any) - wrapper = mount( - - - - - , - ) - }) + it('should save the imaging request and navigate to "/imaging"', async () => { + const expectedDate = new Date() + const expectedImaging = { + patient: 'patient', + type: 'expected type', + status: 'requested', + notes: 'expected notes', + id: '1234', + requestedOn: expectedDate.toISOString(), + } as Imaging + + const wrapper = await setup('loading', {}) + jest.spyOn(ImagingRepository, 'save').mockResolvedValue({ ...expectedImaging }) - it('should save the imaging request and navigate to "/imagings"', async () => { const patientTypeahead = wrapper.find(Typeahead) await act(async () => { const onChange = patientTypeahead.prop('onChange') @@ -253,7 +224,7 @@ describe('New Imaging Request', () => { await onClick() }) - expect(history.location.pathname).toEqual(`/imagings`) + expect(history.location.pathname).toEqual(`/imaging/new`) }) }) }) diff --git a/src/__tests__/shared/components/Sidebar.test.tsx b/src/__tests__/shared/components/Sidebar.test.tsx index 5b0e7ca989..fd32f5c6d8 100644 --- a/src/__tests__/shared/components/Sidebar.test.tsx +++ b/src/__tests__/shared/components/Sidebar.test.tsx @@ -537,7 +537,7 @@ describe('Sidebar', () => { describe('imagings links', () => { it('should render the main imagings link', () => { - const wrapper = setup('/imagings') + const wrapper = setup('/imaging') const listItems = wrapper.find(ListItem) @@ -552,7 +552,7 @@ describe('Sidebar', () => { expect(listItems.at(7).text().trim()).toEqual('imagings.requests.new') }) - it('should not render the new imaging request link when user does not have the request imagings privileges', () => { + it('should not render the new imaging request link when user does not have the request imaging privileges', () => { const wrapper = setupNoPermissions('/imagings') const listItems = wrapper.find(ListItem) @@ -588,7 +588,7 @@ describe('Sidebar', () => { expect(listItems.at(6).prop('active')).toBeTruthy() }) - it('should navigate to /imagings when the main imagings link is clicked', () => { + it('should navigate to /imaging when the main imagings link is clicked', () => { const wrapper = setup('/') const listItems = wrapper.find(ListItem) @@ -598,7 +598,7 @@ describe('Sidebar', () => { onClick() }) - expect(history.location.pathname).toEqual('/imagings') + expect(history.location.pathname).toEqual('/imaging') }) it('new imaging request link should be active when the current path is /imagings/new', () => { @@ -609,7 +609,7 @@ describe('Sidebar', () => { expect(listItems.at(7).prop('active')).toBeTruthy() }) - it('should navigate to /imagings/new when the new imaging link is clicked', () => { + it('should navigate to /imaging/new when the new imaging link is clicked', () => { const wrapper = setup('/imagings') const listItems = wrapper.find(ListItem) @@ -619,7 +619,7 @@ describe('Sidebar', () => { onClick() }) - expect(history.location.pathname).toEqual('/imagings/new') + expect(history.location.pathname).toEqual('/imaging/new') }) it('imagings list link should be active when the current path is /imagings', () => { @@ -630,7 +630,7 @@ describe('Sidebar', () => { expect(listItems.at(8).prop('active')).toBeTruthy() }) - it('should navigate to /imagings when the imagings list link is clicked', () => { + it('should navigate to /imaging when the imagings list link is clicked', () => { const wrapper = setup('/imagings/new') const listItems = wrapper.find(ListItem) @@ -640,7 +640,7 @@ describe('Sidebar', () => { onClick() }) - expect(history.location.pathname).toEqual('/imagings') + expect(history.location.pathname).toEqual('/imaging') }) }) }) diff --git a/src/imagings/Imagings.tsx b/src/imagings/Imagings.tsx index 0db7deacc8..ce9bc893e3 100644 --- a/src/imagings/Imagings.tsx +++ b/src/imagings/Imagings.tsx @@ -13,8 +13,8 @@ const Imagings = () => { const { permissions } = useSelector((state: RootState) => state.user) const breadcrumbs = [ { - i18nKey: 'imagings.label', - location: '/imagings', + i18nKey: 'imagings.imaging.label', + location: '/imaging', }, ] useAddBreadcrumbs(breadcrumbs, true) @@ -24,13 +24,13 @@ const Imagings = () => { diff --git a/src/imagings/ViewImagings.tsx b/src/imagings/ViewImagings.tsx index 3bf1eca9fa..ffc5317566 100644 --- a/src/imagings/ViewImagings.tsx +++ b/src/imagings/ViewImagings.tsx @@ -32,7 +32,7 @@ const ViewImagings = () => { buttons.push(