Skip to content
This repository has been archived by the owner on Jan 9, 2023. It is now read-only.

Commit

Permalink
feat(incidents): add ability to resolve incidents (#2222)
Browse files Browse the repository at this point in the history
  • Loading branch information
blestab committed Jul 12, 2020
1 parent b51999a commit 5e95a9c
Show file tree
Hide file tree
Showing 12 changed files with 268 additions and 19 deletions.
64 changes: 64 additions & 0 deletions src/__tests__/incidents/incident-slice.test.ts
Expand Up @@ -12,6 +12,9 @@ import incident, {
fetchIncidentStart,
fetchIncidentSuccess,
fetchIncident,
resolveIncident,
resolveIncidentStart,
resolveIncidentSuccess,
} from '../../incidents/incident-slice'
import IncidentRepository from '../../shared/db/IncidentRepository'
import Incident from '../../shared/model/Incident'
Expand Down Expand Up @@ -70,6 +73,11 @@ describe('incident slice', () => {
expect(incidentStore.status).toEqual('loading')
})

it('should handle resolve incident start', () => {
const incidentStore = incident(undefined, resolveIncidentStart())
expect(incidentStore.status).toEqual('loading')
})

it('should handle fetch incident success', () => {
const expectedIncident = {
id: '1234',
Expand All @@ -80,6 +88,18 @@ describe('incident slice', () => {
expect(incidentStore.status).toEqual('completed')
expect(incidentStore.incident).toEqual(expectedIncident)
})

it('should handle resolve incident success', () => {
const expectedIncident = {
id: '1234',
resolvedOn: new Date(Date.now()).toISOString(),
status: 'resolved',
} as Incident

const incidentStore = incident(undefined, resolveIncidentSuccess(expectedIncident))
expect(incidentStore.status).toEqual('completed')
expect(incidentStore.incident).toEqual(expectedIncident)
})
})

describe('report incident', () => {
Expand Down Expand Up @@ -202,4 +222,48 @@ describe('incident slice', () => {
expect(store.getActions()[1]).toEqual(fetchIncidentSuccess(expectedIncident))
})
})

describe('resolve incident', () => {
const expectedDate = new Date()
const mockIncident = {
id: '123',
description: 'description',
date: expectedDate.toISOString(),
department: 'some department',
category: 'category',
categoryItem: 'categoryItem',
status: 'reported',
} as Incident
const expectedResolvedIncident = {
...mockIncident,
resolvedOn: expectedDate.toISOString(),
status: 'resolved',
} as Incident
let incidentRepositorySaveOrUpdateSpy: any

beforeEach(() => {
Date.now = jest.fn().mockReturnValue(expectedDate.valueOf())
incidentRepositorySaveOrUpdateSpy = jest
.spyOn(IncidentRepository, 'saveOrUpdate')
.mockResolvedValue(expectedResolvedIncident)
})

it('should resolve an incident', async () => {
const store = mockStore()

await store.dispatch(resolveIncident(mockIncident))

expect(store.getActions()[0]).toEqual(resolveIncidentStart())
expect(incidentRepositorySaveOrUpdateSpy).toHaveBeenCalledWith(expectedResolvedIncident)
expect(store.getActions()[1]).toEqual(resolveIncidentSuccess(expectedResolvedIncident))
})

it('should call on success callback if provided', async () => {
const store = mockStore()
const onSuccessSpy = jest.fn()
await store.dispatch(resolveIncident(mockIncident, onSuccessSpy))

expect(onSuccessSpy).toHaveBeenCalledWith(expectedResolvedIncident)
})
})
})
102 changes: 90 additions & 12 deletions src/__tests__/incidents/view/ViewIncident.test.tsx
@@ -1,3 +1,4 @@
import { Button } from '@hospitalrun/components'
import { act } from '@testing-library/react'
import { mount } from 'enzyme'
import { createMemoryHistory } from 'history'
Expand All @@ -20,6 +21,8 @@ const mockStore = createMockStore<RootState, any>([thunk])

describe('View Incident', () => {
const expectedDate = new Date(2020, 5, 1, 19, 48)
const expectedResolveDate = new Date()
let incidentRepositorySaveSpy: any
let history: any
const expectedIncident = {
id: '1234',
Expand All @@ -34,11 +37,15 @@ describe('View Incident', () => {
date: expectedDate.toISOString(),
} as Incident

const setup = async (permissions: Permissions[]) => {
const setup = async (mockIncident: Incident, permissions: Permissions[]) => {
jest.resetAllMocks()
Date.now = jest.fn(() => expectedResolveDate.valueOf())
jest.spyOn(breadcrumbUtil, 'default')
jest.spyOn(titleUtil, 'default')
jest.spyOn(IncidentRepository, 'find').mockResolvedValue(expectedIncident)
incidentRepositorySaveSpy = jest
.spyOn(IncidentRepository, 'saveOrUpdate')
.mockResolvedValue(expectedIncident)

history = createMemoryHistory()
history.push(`/incidents/1234`)
Expand All @@ -49,7 +56,7 @@ describe('View Incident', () => {
permissions,
},
incident: {
incident: expectedIncident,
incident: mockIncident,
},
} as any)

Expand All @@ -73,81 +80,152 @@ describe('View Incident', () => {

describe('layout', () => {
it('should set the title', async () => {
await setup([Permissions.ViewIncident])
await setup(expectedIncident, [Permissions.ViewIncident])

expect(titleUtil.default).toHaveBeenCalledWith(expectedIncident.code)
})

it('should set the breadcrumbs properly', async () => {
await setup([Permissions.ViewIncident])
await setup(expectedIncident, [Permissions.ViewIncident])

expect(breadcrumbUtil.default).toHaveBeenCalledWith([
{ i18nKey: expectedIncident.code, location: '/incidents/1234' },
])
})

it('should render the date of incident', async () => {
const wrapper = await setup([Permissions.ViewIncident])
const wrapper = await setup(expectedIncident, [Permissions.ViewIncident])

const dateOfIncidentFormGroup = wrapper.find('.incident-date')
expect(dateOfIncidentFormGroup.find('h4').text()).toEqual('incidents.reports.dateOfIncident')
expect(dateOfIncidentFormGroup.find('h5').text()).toEqual('2020-06-01 07:48 PM')
})

it('should render the status', async () => {
const wrapper = await setup([Permissions.ViewIncident])
const wrapper = await setup(expectedIncident, [Permissions.ViewIncident])

const dateOfIncidentFormGroup = wrapper.find('.incident-status')
expect(dateOfIncidentFormGroup.find('h4').text()).toEqual('incidents.reports.status')
expect(dateOfIncidentFormGroup.find('h5').text()).toEqual(expectedIncident.status)
})

it('should render the reported by', async () => {
const wrapper = await setup([Permissions.ViewIncident])
const wrapper = await setup(expectedIncident, [Permissions.ViewIncident])

const dateOfIncidentFormGroup = wrapper.find('.incident-reported-by')
expect(dateOfIncidentFormGroup.find('h4').text()).toEqual('incidents.reports.reportedBy')
expect(dateOfIncidentFormGroup.find('h5').text()).toEqual(expectedIncident.reportedBy)
})

it('should render the reported on', async () => {
const wrapper = await setup([Permissions.ViewIncident])
const wrapper = await setup(expectedIncident, [Permissions.ViewIncident])

const dateOfIncidentFormGroup = wrapper.find('.incident-reported-on')
expect(dateOfIncidentFormGroup.find('h4').text()).toEqual('incidents.reports.reportedOn')
expect(dateOfIncidentFormGroup.find('h5').text()).toEqual('2020-06-01 07:48 PM')
})

it('should render the resolved on if incident status is resolved', async () => {
const mockIncident = {
...expectedIncident,
status: 'resolved',
resolvedOn: '2020-07-10 06:33 PM',
} as Incident
const wrapper = await setup(mockIncident, [Permissions.ViewIncident])

const dateOfResolutionFormGroup = wrapper.find('.incident-resolved-on')
expect(dateOfResolutionFormGroup.find('h4').text()).toEqual('incidents.reports.resolvedOn')
expect(dateOfResolutionFormGroup.find('h5').text()).toEqual('2020-07-10 06:33 PM')
})

it('should not render the resolved on if incident status is not resolved', async () => {
const wrapper = await setup(expectedIncident, [Permissions.ViewIncident])

const completedOn = wrapper.find('.incident-resolved-on')
expect(completedOn).toHaveLength(0)
})

it('should render the department', async () => {
const wrapper = await setup([Permissions.ViewIncident])
const wrapper = await setup(expectedIncident, [Permissions.ViewIncident])

const departmentInput = wrapper.findWhere((w: any) => w.prop('name') === 'department')
expect(departmentInput.prop('label')).toEqual('incidents.reports.department')
expect(departmentInput.prop('value')).toEqual(expectedIncident.department)
})

it('should render the category', async () => {
const wrapper = await setup([Permissions.ViewIncident])
const wrapper = await setup(expectedIncident, [Permissions.ViewIncident])

const categoryInput = wrapper.findWhere((w: any) => w.prop('name') === 'category')
expect(categoryInput.prop('label')).toEqual('incidents.reports.category')
expect(categoryInput.prop('value')).toEqual(expectedIncident.category)
})

it('should render the category item', async () => {
const wrapper = await setup([Permissions.ViewIncident])
const wrapper = await setup(expectedIncident, [Permissions.ViewIncident])

const categoryItemInput = wrapper.findWhere((w: any) => w.prop('name') === 'categoryItem')
expect(categoryItemInput.prop('label')).toEqual('incidents.reports.categoryItem')
expect(categoryItemInput.prop('value')).toEqual(expectedIncident.categoryItem)
})

it('should render the description', async () => {
const wrapper = await setup([Permissions.ViewIncident])
const wrapper = await setup(expectedIncident, [Permissions.ViewIncident])

const descriptionTextInput = wrapper.findWhere((w: any) => w.prop('name') === 'description')
expect(descriptionTextInput.prop('label')).toEqual('incidents.reports.description')
expect(descriptionTextInput.prop('value')).toEqual(expectedIncident.description)
})

it('should display a resolve incident button if the incident is in a reported state', async () => {
const wrapper = await setup(expectedIncident, [
Permissions.ViewIncident,
Permissions.ResolveIncident,
])

const buttons = wrapper.find(Button)
expect(buttons.at(0).text().trim()).toEqual('incidents.reports.resolve')
})

it('should not display a resolve incident button if the user has no access ResolveIncident access', async () => {
const wrapper = await setup(expectedIncident, [Permissions.ViewIncident])

const resolveButton = wrapper.find(Button)
expect(resolveButton).toHaveLength(0)
})

it('should not display a resolve incident button if the incident is resolved', async () => {
const mockIncident = { ...expectedIncident, status: 'resolved' } as Incident
const wrapper = await setup(mockIncident, [Permissions.ViewIncident])

const resolveButton = wrapper.find(Button)
expect(resolveButton).toHaveLength(0)
})
})

describe('on resolve', () => {
it('should mark the status as resolved and fill in the resolved date with the current time', async () => {
const wrapper = await setup(expectedIncident, [
Permissions.ViewIncident,
Permissions.ResolveIncident,
])

const resolveButton = wrapper.find(Button).at(0)
await act(async () => {
const onClick = resolveButton.prop('onClick')
await onClick()
})
wrapper.update()

expect(incidentRepositorySaveSpy).toHaveBeenCalledTimes(1)
expect(incidentRepositorySaveSpy).toHaveBeenCalledWith(
expect.objectContaining({
...expectedIncident,
status: 'resolved',
resolvedOn: expectedResolveDate.toISOString(),
}),
)
expect(history.location.pathname).toEqual('/incidents')
})
})
})
13 changes: 13 additions & 0 deletions src/__tests__/shared/utils/extractUsername.test.ts
@@ -0,0 +1,13 @@
import { extractUsername } from '../../../shared/util/extractUsername'

describe('extract username util', () => {
it('should extract the string after the last : in a given string', () => {
const extractedName = extractUsername('org.couchdb.user:username')
expect(extractedName).toMatch('username')
})

it('should return the string if string does not contain a : ', () => {
const extractedName = extractUsername('username')
expect(extractedName).toMatch('username')
})
})
1 change: 1 addition & 0 deletions src/incidents/IncidentFilter.ts
@@ -1,5 +1,6 @@
enum IncidentFilter {
reported = 'reported',
resolved = 'resolved',
all = 'all',
}

Expand Down
23 changes: 23 additions & 0 deletions src/incidents/incident-slice.ts
Expand Up @@ -51,6 +51,8 @@ const incidentSlice = createSlice({
reportIncidentStart: start,
reportIncidentSuccess: finish,
reportIncidentError: error,
resolveIncidentStart: start,
resolveIncidentSuccess: finish,
},
})

Expand All @@ -60,6 +62,8 @@ export const {
reportIncidentStart,
reportIncidentSuccess,
reportIncidentError,
resolveIncidentStart,
resolveIncidentSuccess,
} = incidentSlice.actions

export const fetchIncident = (id: string): AppThunk => async (dispatch) => {
Expand Down Expand Up @@ -120,4 +124,23 @@ export const reportIncident = (
}
}

export const resolveIncident = (
incidentToComplete: Incident,
onSuccess?: (incidentToComplete: Incident) => void,
): AppThunk => async (dispatch) => {
dispatch(resolveIncidentStart())

const resolvedIncident = await IncidentRepository.saveOrUpdate({
...incidentToComplete,
resolvedOn: new Date(Date.now().valueOf()).toISOString(),
status: 'resolved',
})

dispatch(resolveIncidentSuccess(resolvedIncident))

if (onSuccess) {
onSuccess(resolvedIncident)
}
}

export default incidentSlice.reducer
8 changes: 6 additions & 2 deletions src/incidents/list/ViewIncidents.tsx
Expand Up @@ -11,6 +11,7 @@ import SelectWithLabelFormGroup, {
} from '../../shared/components/input/SelectWithLableFormGroup'
import useTranslator from '../../shared/hooks/useTranslator'
import { RootState } from '../../shared/store'
import { extractUsername } from '../../shared/util/extractUsername'
import IncidentFilter from '../IncidentFilter'
import { searchIncidents } from '../incidents-slice'

Expand All @@ -21,7 +22,10 @@ const ViewIncidents = () => {
useTitle(t('incidents.reports.label'))
const [searchFilter, setSearchFilter] = useState(IncidentFilter.reported)
const { incidents } = useSelector((state: RootState) => state.incidents)

const viewIncidents = incidents.map((row) => ({
...row,
reportedBy: extractUsername(row.reportedBy),
}))
const setButtonToolBar = useButtonToolbarSetter()
useEffect(() => {
setButtonToolBar([
Expand Down Expand Up @@ -67,7 +71,7 @@ const ViewIncidents = () => {
<div className="row">
<Table
getID={(row) => row.id}
data={incidents}
data={viewIncidents}
columns={[
{ label: t('incidents.reports.code'), key: 'code' },
{
Expand Down

1 comment on commit 5e95a9c

@vercel
Copy link

@vercel vercel bot commented on 5e95a9c Jul 12, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.