diff --git a/.replit b/.replit deleted file mode 100644 index e5ab7812bc..0000000000 --- a/.replit +++ /dev/null @@ -1,2 +0,0 @@ -language = "nodejs" -run = "npm start" diff --git a/README.md b/README.md index 42928a8062..dd1ef4e355 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ ![Status](https://img.shields.io/badge/Status-developing-brightgree) [![Release](https://img.shields.io/github/release/HospitalRun/hospitalrun-frontend.svg)](https://github.com/HospitalRun/hospitalrun-frontend/releases) [![Version](https://img.shields.io/github/package-json/v/hospitalrun/hospitalrun-frontend)](https://github.com/HospitalRun/hospitalrun-frontend/releases) [![GitHub CI](https://github.com/HospitalRun/frontend/workflows/GitHub%20CI/badge.svg)](https://github.com/HospitalRun/frontend/actions) [![Coverage Status](https://coveralls.io/repos/github/HospitalRun/hospitalrun-frontend/badge.svg?branch=master)](https://coveralls.io/github/HospitalRun/hospitalrun-frontend?branch=master) [![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/HospitalRun/hospitalrun-frontend.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/HospitalRun/hospitalrun-frontend/context:javascript) ![Code scanning](https://github.com/HospitalRun/hospitalrun-frontend/workflows/Code%20scanning/badge.svg?branch=master) [![Documentation Status](https://readthedocs.org/projects/hospitalrun-frontend/badge/?version=latest)](https://hospitalrun-frontend.readthedocs.io) [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FHospitalRun%2Fhospitalrun-frontend.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2FHospitalRun%2Fhospitalrun-frontend?ref=badge_large) [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) -![dependabot](https://api.dependabot.com/badges/status?host=github&repo=HospitalRun/hospitalrun-frontend) [![Slack](https://hospitalrun-slack.herokuapp.com/badge.svg)](https://hospitalrun-slack.herokuapp.com) [![Run on Repl.it](https://repl.it/badge/github/HospitalRun/hospitalrun-frontend)](https://repl.it/github/HospitalRun/hospitalrun-frontend) +![dependabot](https://api.dependabot.com/badges/status?host=github&repo=HospitalRun/hospitalrun-frontend) [![Slack](https://hospitalrun-slack.herokuapp.com/badge.svg)](https://hospitalrun-slack.herokuapp.com) [![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/HospitalRun/hospitalrun-frontend) @@ -20,9 +20,9 @@ React frontend for [HospitalRun](http://hospitalrun.io/): free software for deve - How can I deploy 1.0.0-beta? - Where do I report a bug or request a feature? -- How can I contribute? (There are several other ways besides coding) -- What is the project structure? -- What is the application infrastructure? +- How can I contribute? (There are several other ways besides coding) +- What is the project structure? +- What is the application infrastructure? - Who is behind HospitalRun? etc. # Would you like to contribute? If yes... diff --git a/couchdb/local.ini b/couchdb/local.ini index 70c17e9c0f..9775ad68cb 100644 --- a/couchdb/local.ini +++ b/couchdb/local.ini @@ -4,6 +4,8 @@ users_db_security_editable = true [httpd] enable_cors = true +; Replace default WWW-Authenticate = Basic realm="administrator" +WWW-Authenticate = Other realm="app" [cors] origins = * diff --git a/nginx.conf b/nginx.conf index 2400141c1e..74aa917de7 100644 --- a/nginx.conf +++ b/nginx.conf @@ -5,6 +5,8 @@ server { root /usr/share/nginx/html; index index.html index.htm; try_files $uri /index.html; + # replace WWW-Authenticate header in response if authorization failed + more_set_headers -s 401 'WWW-Authenticate: Other realm="App"'; } error_page 500 502 503 504 /50x.html; diff --git a/package.json b/package.json index 8f1b944b9d..83ae8ec69c 100644 --- a/package.json +++ b/package.json @@ -5,16 +5,18 @@ "private": false, "license": "MIT", "dependencies": { - "@hospitalrun/components": "~1.16.0", + "@hospitalrun/components": "~3.0.0", "@reduxjs/toolkit": "~1.4.0", "@types/escape-string-regexp": "~2.0.1", + "@types/json2csv": "~5.0.1", "@types/pouchdb-find": "~6.3.4", "bootstrap": "~4.5.0", - "date-fns": "~2.15.0", + "date-fns": "~2.16.0", "escape-string-regexp": "~4.0.0", "i18next": "~19.7.0", "i18next-browser-languagedetector": "~6.0.0", "i18next-xhr-backend": "~3.2.2", + "json2csv": "~5.0.1", "lodash": "^4.17.15", "node-sass": "~4.14.0", "pouchdb": "~7.2.1", @@ -27,8 +29,8 @@ "react-bootstrap-typeahead": "~5.1.0", "react-dom": "~16.13.0", "react-i18next": "~11.7.0", - "react-query": "~2.5.13", - "react-query-devtools": "~2.4.2", + "react-query": "~2.23.0", + "react-query-devtools": "~2.5.0", "react-redux": "~7.2.0", "react-router": "~5.2.0", "react-router-dom": "~5.2.0", @@ -58,15 +60,15 @@ ], "devDependencies": { "@commitlint/cli": "~9.1.2", - "@commitlint/config-conventional": "~9.1.1", - "@commitlint/core": "~9.1.1", - "@commitlint/prompt": "~9.1.1", - "@testing-library/react": "~10.4.0", + "@commitlint/config-conventional": "~11.0.0", + "@commitlint/core": "~11.0.0", + "@commitlint/prompt": "~11.0.0", + "@testing-library/react": "~11.0.0", "@testing-library/react-hooks": "~3.4.1", "@types/enzyme": "^3.10.5", "@types/jest": "~26.0.0", "@types/lodash": "^4.14.150", - "@types/node": "~14.0.0", + "@types/node": "~14.11.1", "@types/pouchdb": "~6.4.0", "@types/react": "~16.9.17", "@types/react-dom": "~16.9.4", @@ -83,31 +85,31 @@ "commitizen": "~4.2.0", "commitlint-config-cz": "~0.13.0", "cross-env": "~7.0.0", - "cz-conventional-changelog": "~3.2.0", + "cz-conventional-changelog": "~3.3.0", "dateformat": "~3.0.3", "enzyme": "~3.11.0", "enzyme-adapter-react-16": "~1.15.2", "eslint": "~6.8.0", "eslint-config-airbnb": "~18.2.0", "eslint-config-prettier": "~6.11.0", - "eslint-import-resolver-typescript": "~2.2.0", + "eslint-import-resolver-typescript": "~2.3.0", "eslint-plugin-import": "~2.22.0", - "eslint-plugin-jest": "~23.20.0", + "eslint-plugin-jest": "~24.0.0", "eslint-plugin-jsx-a11y": "~6.3.0", "eslint-plugin-prettier": "~3.1.2", - "eslint-plugin-react": "~7.20.0", + "eslint-plugin-react": "~7.21.0", "eslint-plugin-react-hooks": "~4.1.0", "history": "4.10.1", - "husky": "~4.2.1", - "jest": "~24.9.0", - "lint-staged": "~10.2.0", + "husky": "~4.3.0", + "jest": "24.9.0", + "lint-staged": "~10.4.0", "memdown": "~5.1.0", "prettier": "~2.1.0", "redux-mock-store": "~1.5.4", "rimraf": "~3.0.2", "source-map-explorer": "^2.2.2", "standard-version": "~9.0.0", - "ts-jest": "~26.2.0" + "ts-jest": "~26.4.0" }, "scripts": { "analyze": "source-map-explorer 'build/static/js/*.js'", diff --git a/src/App.tsx b/src/App.tsx index 480178220e..0760e629c0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,6 +8,7 @@ import { BrowserRouter, Route, Switch } from 'react-router-dom' import HospitalRun from './HospitalRun' import Login from './login/Login' +import { TitleProvider } from './page-header/title/TitleContext' import { remoteDb } from './shared/config/pouchdb' import { getCurrentSession } from './user/user-slice' @@ -41,7 +42,9 @@ const App: React.FC = () => { }> - + + + diff --git a/src/HospitalRun.tsx b/src/HospitalRun.tsx index 118bafdeaf..31d8fe336e 100644 --- a/src/HospitalRun.tsx +++ b/src/HospitalRun.tsx @@ -11,6 +11,7 @@ import Medications from './medications/Medications' import Breadcrumbs from './page-header/breadcrumbs/Breadcrumbs' import { ButtonBarProvider } from './page-header/button-toolbar/ButtonBarProvider' import ButtonToolBar from './page-header/button-toolbar/ButtonToolBar' +import { useTitle } from './page-header/title/TitleContext' import Patients from './patients/Patients' import Appointments from './scheduling/appointments/Appointments' import Settings from './settings/Settings' @@ -20,7 +21,7 @@ import Sidebar from './shared/components/Sidebar' import { RootState } from './shared/store' const HospitalRun = () => { - const { title } = useSelector((state: RootState) => state.title) + const { title } = useTitle() const { sidebarCollapsed } = useSelector((state: RootState) => state.components) const { user } = useSelector((root: RootState) => root.user) @@ -33,7 +34,9 @@ const HospitalRun = () => {
- +
+ +
([thunk]) describe('HospitalRun', () => { + const setup = async (route: string, permissions: Permissions[] = []) => { + jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) + const store = mockStore({ + user: { user: { id: '123' }, permissions }, + appointments: { appointments: [] }, + medications: { medications: [] }, + labs: { labs: [] }, + imagings: { imagings: [] }, + breadcrumbs: { breadcrumbs: [] }, + components: { sidebarCollapsed: false }, + } as any) + const wrapper = mount( + + + + + + + , + ) + + await act(async () => { + wrapper.update() + }) + + return { wrapper: wrapper as ReactWrapper, store: store as any } + } + describe('routing', () => { describe('/appointments', () => { it('should render the appointments screen when /appointments is accessed', async () => { - const store = mockStore({ - title: 'test', - user: { user: { id: '123' }, permissions: [Permissions.ReadAppointments] }, - appointments: { appointments: [] }, - breadcrumbs: { breadcrumbs: [] }, - components: { sidebarCollapsed: false }, - } as any) - - const wrapper = mount( - - - - - , - ) - - await act(async () => { - wrapper.update() - }) + const permissions: Permissions[] = [Permissions.ReadAppointments] + const { wrapper, store } = await setup('/appointments', permissions) expect(wrapper.find(Appointments)).toHaveLength(1) @@ -59,22 +72,8 @@ describe('HospitalRun', () => { ) }) - it('should render the Dashboard when the user does not have read appointment privileges', () => { - const wrapper = mount( - - - - - , - ) - + it('should render the Dashboard when the user does not have read appointment privileges', async () => { + const { wrapper } = await setup('/appointments') expect(wrapper.find(Dashboard)).toHaveLength(1) }) }) @@ -82,45 +81,15 @@ describe('HospitalRun', () => { describe('/labs', () => { it('should render the Labs component when /labs is accessed', async () => { jest.spyOn(LabRepository, 'findAll').mockResolvedValue([]) - const store = mockStore({ - title: 'test', - user: { user: { id: '123' }, permissions: [Permissions.ViewLabs] }, - labs: { labs: [] }, - breadcrumbs: { breadcrumbs: [] }, - components: { sidebarCollapsed: false }, - } as any) - - let wrapper: any - await act(async () => { - wrapper = await mount( - - - - - , - ) - }) - wrapper.update() + const permissions: Permissions[] = [Permissions.ViewLabs] + const { wrapper } = await setup('/labs', permissions) expect(wrapper.find(ViewLabs)).toHaveLength(1) }) - it('should render the dashboard if the user does not have permissions to view labs', () => { + it('should render the dashboard if the user does not have permissions to view labs', async () => { 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( - - - - - , - ) + const { wrapper } = await setup('/labs') expect(wrapper.find(ViewLabs)).toHaveLength(0) expect(wrapper.find(Dashboard)).toHaveLength(1) @@ -129,46 +98,16 @@ describe('HospitalRun', () => { describe('/medications', () => { it('should render the Medications component when /medications is accessed', async () => { - jest.spyOn(MedicationRepository, 'findAll').mockResolvedValue([]) - const store = mockStore({ - title: 'test', - user: { user: { id: '123' }, permissions: [Permissions.ViewMedications] }, - medications: { medications: [] }, - breadcrumbs: { breadcrumbs: [] }, - components: { sidebarCollapsed: false }, - } as any) - - let wrapper: any - await act(async () => { - wrapper = await mount( - - - - - , - ) - }) - wrapper.update() + jest.spyOn(MedicationRepository, 'search').mockResolvedValue([]) + const permissions: Permissions[] = [Permissions.ViewMedications] + const { wrapper } = await setup('/medications', permissions) expect(wrapper.find(ViewMedications)).toHaveLength(1) }) - it('should render the dashboard if the user does not have permissions to view medications', () => { + it('should render the dashboard if the user does not have permissions to view medications', async () => { jest.spyOn(MedicationRepository, 'findAll').mockResolvedValue([]) - const store = mockStore({ - title: 'test', - user: { user: { id: '123' }, permissions: [] }, - breadcrumbs: { breadcrumbs: [] }, - components: { sidebarCollapsed: false }, - } as any) - - const wrapper = mount( - - - - - , - ) + const { wrapper } = await setup('/medications') expect(wrapper.find(ViewMedications)).toHaveLength(0) expect(wrapper.find(Dashboard)).toHaveLength(1) @@ -178,44 +117,15 @@ describe('HospitalRun', () => { describe('/incidents', () => { it('should render the Incidents component when /incidents is accessed', async () => { jest.spyOn(IncidentRepository, 'search').mockResolvedValue([]) - const store = mockStore({ - title: 'test', - user: { user: { id: '123' }, permissions: [Permissions.ViewIncidents] }, - breadcrumbs: { breadcrumbs: [] }, - components: { sidebarCollapsed: false }, - } as any) - - let wrapper: any - await act(async () => { - wrapper = await mount( - - - - - , - ) - }) - wrapper.update() + const permissions: Permissions[] = [Permissions.ViewIncidents] + const { wrapper } = await setup('/incidents', permissions) expect(wrapper.find(Incidents)).toHaveLength(1) }) - it('should render the dashboard if the user does not have permissions to view incidents', () => { + it('should render the dashboard if the user does not have permissions to view incidents', async () => { 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( - - - - - , - ) + const { wrapper } = await setup('/incidents') expect(wrapper.find(Incidents)).toHaveLength(0) expect(wrapper.find(Dashboard)).toHaveLength(1) @@ -224,46 +134,16 @@ describe('HospitalRun', () => { describe('/imaging', () => { it('should render the Imagings component when /imaging 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() + jest.spyOn(ImagingRepository, 'search').mockResolvedValue([]) + const permissions: Permissions[] = [Permissions.ViewImagings] + const { wrapper } = await setup('/imaging', permissions) expect(wrapper.find(ViewImagings)).toHaveLength(1) }) - it('should render the dashboard if the user does not have permissions to view imagings', () => { + it('should render the dashboard if the user does not have permissions to view imagings', async () => { 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( - - - - - , - ) + const { wrapper } = await setup('/imaging') expect(wrapper.find(ViewImagings)).toHaveLength(0) expect(wrapper.find(Dashboard)).toHaveLength(1) @@ -272,42 +152,16 @@ describe('HospitalRun', () => { describe('/settings', () => { it('should render the Settings component when /settings is accessed', async () => { - const store = mockStore({ - title: 'test', - user: { user: { id: '123' }, permissions: [] }, - breadcrumbs: { breadcrumbs: [] }, - components: { sidebarCollapsed: false }, - } as any) - - const wrapper = mount( - - - - - , - ) - + const { wrapper } = await setup('/settings') expect(wrapper.find(Settings)).toHaveLength(1) }) }) }) describe('layout', () => { - it('should render a Toaster', () => { - const wrapper = mount( - - - - - , - ) + it('should render a Toaster', async () => { + const permissions: Permissions[] = [Permissions.WritePatients] + const { wrapper } = await setup('/', permissions) expect(wrapper.find(Toaster)).toHaveLength(1) }) diff --git a/src/__tests__/imagings/Imagings.test.tsx b/src/__tests__/imagings/Imagings.test.tsx index eb3b03db63..6b1865dc0b 100644 --- a/src/__tests__/imagings/Imagings.test.tsx +++ b/src/__tests__/imagings/Imagings.test.tsx @@ -1,4 +1,4 @@ -import { mount } from 'enzyme' +import { mount, ReactWrapper } from 'enzyme' import React from 'react' import { Provider } from 'react-redux' import { MemoryRouter } from 'react-router-dom' @@ -7,6 +7,7 @@ import thunk from 'redux-thunk' import Imagings from '../../imagings/Imagings' import NewImagingRequest from '../../imagings/requests/NewImagingRequest' +import * as titleUtil from '../../page-header/title/TitleContext' import ImagingRepository from '../../shared/db/ImagingRepository' import PatientRepository from '../../shared/db/PatientRepository' import Imaging from '../../shared/model/Imaging' @@ -14,9 +15,11 @@ import Patient from '../../shared/model/Patient' import Permissions from '../../shared/model/Permissions' import { RootState } from '../../shared/store' +const { TitleProvider } = titleUtil const mockStore = createMockStore([thunk]) describe('Imagings', () => { + jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) jest.spyOn(ImagingRepository, 'findAll').mockResolvedValue([]) jest .spyOn(ImagingRepository, 'find') @@ -24,48 +27,41 @@ describe('Imagings', () => { jest .spyOn(PatientRepository, 'find') .mockResolvedValue({ id: '12345', fullName: 'test test' } as Patient) + const setup = (permissions: Permissions[], isNew = false) => { + const store = mockStore({ + user: { permissions: [permissions] }, + 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( + + + {isNew ? : } + + , + ) + wrapper.update() + return { wrapper: wrapper as ReactWrapper } + } describe('routing', () => { 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] }, - 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( - - - - - , - ) + it('should render the new imaging request screen when /imaging/new is accessed', async () => { + const permissions: Permissions[] = [Permissions.RequestImaging] + const { wrapper } = setup(permissions, true) 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( - - - - - , - ) + it('should not navigate to /imagings/new if the user does not have RequestImaging permissions', async () => { + const permissions: Permissions[] = [] + const { wrapper } = setup(permissions) expect(wrapper.find(NewImagingRequest)).toHaveLength(0) }) diff --git a/src/__tests__/imagings/ViewImagings.test.tsx b/src/__tests__/imagings/ViewImagings.test.tsx deleted file mode 100644 index f06f32e3cb..0000000000 --- a/src/__tests__/imagings/ViewImagings.test.tsx +++ /dev/null @@ -1,126 +0,0 @@ -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 { 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' -import Imaging from '../../shared/model/Imaging' -import Permissions from '../../shared/model/Permissions' -import { RootState } from '../../shared/store' - -const mockStore = createMockStore([thunk]) - -describe('View Imagings', () => { - 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', - fullName: 'full name', - 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( - - - - - - - - - , - ) - }) - - 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 () => { - 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 () => { - await setup([Permissions.ViewImagings], []) - - const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0] - expect(actualButtons).toEqual([]) - }) - }) - - describe('table', () => { - 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( - 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: 'fullName' }), - ) - 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/hooks/useImagingRequest.test.tsx b/src/__tests__/imagings/hooks/useImagingRequest.test.tsx new file mode 100644 index 0000000000..98b8b46d7d --- /dev/null +++ b/src/__tests__/imagings/hooks/useImagingRequest.test.tsx @@ -0,0 +1,32 @@ +import { renderHook, act } from '@testing-library/react-hooks' + +import useImagingRequest from '../../../imagings/hooks/useImagingRequest' +import ImagingRepository from '../../../shared/db/ImagingRepository' +import Imaging from '../../../shared/model/Imaging' +import waitUntilQueryIsSuccessful from '../../test-utils/wait-for-query.util' + +describe('useImagingRequest', () => { + it('should get an imaging request by id', async () => { + const expectedImagingId = 'some id' + const expectedImagingRequest = { + id: expectedImagingId, + patient: 'some patient', + visitId: 'visit id', + status: 'requested', + type: 'some type', + } as Imaging + jest.spyOn(ImagingRepository, 'find').mockResolvedValue(expectedImagingRequest) + + let actualData: any + await act(async () => { + const renderHookResult = renderHook(() => useImagingRequest(expectedImagingId)) + const { result } = renderHookResult + await waitUntilQueryIsSuccessful(renderHookResult) + actualData = result.current.data + }) + + expect(ImagingRepository.find).toHaveBeenCalledTimes(1) + expect(ImagingRepository.find).toBeCalledWith(expectedImagingId) + expect(actualData).toEqual(expectedImagingRequest) + }) +}) diff --git a/src/__tests__/imagings/hooks/useImagingSearch.test.tsx b/src/__tests__/imagings/hooks/useImagingSearch.test.tsx new file mode 100644 index 0000000000..8a50eeccee --- /dev/null +++ b/src/__tests__/imagings/hooks/useImagingSearch.test.tsx @@ -0,0 +1,43 @@ +import { act, renderHook } from '@testing-library/react-hooks' + +import useImagingSearch from '../../../imagings/hooks/useImagingSearch' +import ImagingSearchRequest from '../../../imagings/model/ImagingSearchRequest' +import ImagingRepository from '../../../shared/db/ImagingRepository' +import SortRequest from '../../../shared/db/SortRequest' +import Imaging from '../../../shared/model/Imaging' +import waitUntilQueryIsSuccessful from '../../test-utils/wait-for-query.util' + +const defaultSortRequest: SortRequest = { + sorts: [ + { + field: 'requestedOn', + direction: 'desc', + }, + ], +} + +describe('useImagingSearch', () => { + it('it should search imaging requests', async () => { + const expectedSearchRequest: ImagingSearchRequest = { + status: 'completed', + text: 'some search request', + } + const expectedImagingRequests = [{ id: 'some id' }] as Imaging[] + jest.spyOn(ImagingRepository, 'search').mockResolvedValue(expectedImagingRequests) + + let actualData: any + await act(async () => { + const renderHookResult = renderHook(() => useImagingSearch(expectedSearchRequest)) + const { result } = renderHookResult + await waitUntilQueryIsSuccessful(renderHookResult) + actualData = result.current.data + }) + + expect(ImagingRepository.search).toHaveBeenCalledTimes(1) + expect(ImagingRepository.search).toBeCalledWith({ + ...expectedSearchRequest, + defaultSortRequest, + }) + expect(actualData).toEqual(expectedImagingRequests) + }) +}) diff --git a/src/__tests__/imagings/hooks/useRequestImaging.test.tsx b/src/__tests__/imagings/hooks/useRequestImaging.test.tsx new file mode 100644 index 0000000000..ad7385bd9e --- /dev/null +++ b/src/__tests__/imagings/hooks/useRequestImaging.test.tsx @@ -0,0 +1,55 @@ +/* eslint-disable no-console */ + +import useRequestImaging from '../../../imagings/hooks/useRequestImaging' +import { ImagingRequestError } from '../../../imagings/util/validate-imaging-request' +import * as imagingRequestValidator from '../../../imagings/util/validate-imaging-request' +import ImagingRepository from '../../../shared/db/ImagingRepository' +import Imaging from '../../../shared/model/Imaging' +import executeMutation from '../../test-utils/use-mutation.util' + +describe('useReportIncident', () => { + beforeEach(() => { + jest.restoreAllMocks() + console.error = jest.fn() + }) + + it('should save the imaging request with correct data', async () => { + const expectedDate = new Date(Date.now()) + Date.now = jest.fn().mockReturnValue(expectedDate) + const givenImagingRequest = { + patient: 'some patient', + fullName: 'some full name', + status: 'requested', + type: 'some type', + notes: 'some notes', + visitId: 'some visit id', + } as Imaging + + const expectedImagingRequest = { + ...givenImagingRequest, + requestedOn: expectedDate.toISOString(), + requestedBy: 'test', + } as Imaging + jest.spyOn(ImagingRepository, 'save').mockResolvedValue(expectedImagingRequest) + + await executeMutation(() => useRequestImaging(), givenImagingRequest) + expect(ImagingRepository.save).toHaveBeenCalledTimes(1) + expect(ImagingRepository.save).toBeCalledWith(expectedImagingRequest) + }) + + it('should throw an error if validation fails', async () => { + const expectedImagingRequestError = { + patient: 'some patient error', + } as ImagingRequestError + + jest.spyOn(imagingRequestValidator, 'default').mockReturnValue(expectedImagingRequestError) + jest.spyOn(ImagingRepository, 'save').mockResolvedValue({} as Imaging) + + try { + await executeMutation(() => useRequestImaging(), {}) + } catch (e) { + expect(e).toEqual(expectedImagingRequestError) + expect(ImagingRepository.save).not.toHaveBeenCalled() + } + }) +}) diff --git a/src/__tests__/imagings/imaging-slice.test.ts b/src/__tests__/imagings/imaging-slice.test.ts deleted file mode 100644 index 9a674ae194..0000000000 --- a/src/__tests__/imagings/imaging-slice.test.ts +++ /dev/null @@ -1,122 +0,0 @@ -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 deleted file mode 100644 index e4b5447d59..0000000000 --- a/src/__tests__/imagings/imagings-slice.test.ts +++ /dev/null @@ -1,146 +0,0 @@ -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 index 6d21d587f5..b5abe42662 100644 --- a/src/__tests__/imagings/requests/NewImagingRequest.test.tsx +++ b/src/__tests__/imagings/requests/NewImagingRequest.test.tsx @@ -1,4 +1,4 @@ -import { Button, Typeahead, Label, Alert } from '@hospitalrun/components' +import { Button, Typeahead, Label } from '@hospitalrun/components' import { mount, ReactWrapper } from 'enzyme' import { createMemoryHistory } from 'history' import React from 'react' @@ -11,8 +11,8 @@ 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 * as titleUtil from '../../../page-header/title/TitleContext' +import SelectWithLabelFormGroup from '../../../shared/components/input/SelectWithLabelFormGroup' import TextFieldWithLabelFormGroup from '../../../shared/components/input/TextFieldWithLabelFormGroup' import TextInputWithLabelFormGroup from '../../../shared/components/input/TextInputWithLabelFormGroup' import ImagingRepository from '../../../shared/db/ImagingRepository' @@ -26,23 +26,16 @@ describe('New Imaging Request', () => { let history: any let setButtonToolBarSpy: any - const setup = async (status: string, error: any = {}) => { + const setup = async () => { jest.resetAllMocks() jest.spyOn(breadcrumbUtil, 'default') setButtonToolBarSpy = jest.fn() - jest.spyOn(titleUtil, 'default') + jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) 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) + const store = mockStore({} as any) let wrapper: any await act(async () => { @@ -51,27 +44,30 @@ describe('New Imaging Request', () => { - + + + , ) }) + wrapper.find(NewImagingRequest).props().updateTitle = jest.fn() wrapper.update() return wrapper as ReactWrapper } describe('title and breadcrumbs', () => { - it('should have New Imaging Request as the title', async () => { - await setup('loading', {}) - expect(titleUtil.default).toHaveBeenCalledWith('imagings.requests.new') + it('should have called the useUpdateTitle hook', async () => { + await setup() + expect(titleUtil.useUpdateTitle).toHaveBeenCalledTimes(1) }) }) describe('form layout', () => { it('should render a patient typeahead', async () => { - const wrapper = await setup('loading', {}) + const wrapper = await setup() const typeaheadDiv = wrapper.find('.patient-typeahead') expect(typeaheadDiv).toBeDefined() @@ -87,7 +83,7 @@ describe('New Imaging Request', () => { }) it('should render a dropdown list of visits', async () => { - const wrapper = await setup('loading', {}) + const wrapper = await setup() const visitsTypeSelect = wrapper.find('.visits').find(SelectWithLabelFormGroup) expect(visitsTypeSelect).toBeDefined() expect(visitsTypeSelect.prop('label')).toEqual('patient.visits.label') @@ -95,7 +91,7 @@ describe('New Imaging Request', () => { }) it('should render a type input box', async () => { - const wrapper = await setup('loading', {}) + const wrapper = await setup() const typeInputBox = wrapper.find(TextInputWithLabelFormGroup) expect(typeInputBox).toBeDefined() @@ -105,7 +101,7 @@ describe('New Imaging Request', () => { }) it('should render a status types select', async () => { - const wrapper = await setup('loading', {}) + const wrapper = await setup() const statusTypesSelect = wrapper.find('.imaging-status').find(SelectWithLabelFormGroup) expect(statusTypesSelect).toBeDefined() @@ -122,7 +118,7 @@ describe('New Imaging Request', () => { }) it('should render a notes text field', async () => { - const wrapper = await setup('loading', {}) + const wrapper = await setup() const notesTextField = wrapper.find(TextFieldWithLabelFormGroup) expect(notesTextField).toBeDefined() @@ -132,48 +128,23 @@ describe('New Imaging Request', () => { }) it('should render a save button', async () => { - const wrapper = await setup('loading', {}) + const wrapper = await setup() const saveButton = wrapper.find(Button).at(0) expect(saveButton).toBeDefined() expect(saveButton.text().trim()).toEqual('actions.save') }) it('should render a cancel button', async () => { - const wrapper = await setup('loading', {}) + const wrapper = await setup() const cancelButton = wrapper.find(Button).at(1) expect(cancelButton).toBeDefined() expect(cancelButton.text().trim()).toEqual('actions.cancel') }) }) - describe('errors', () => { - const error = { - message: 'some message', - patient: 'some patient message', - type: 'some type error', - status: 'status type error', - } - - 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) - - 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', () => { it('should navigate back to /imaging', async () => { - const wrapper = await setup('loading', {}) + const wrapper = await setup() const cancelButton = wrapper.find(Button).at(1) act(() => { @@ -198,7 +169,7 @@ describe('New Imaging Request', () => { requestedOn: expectedDate.toISOString(), } as Imaging - const wrapper = await setup('loading', {}) + const wrapper = await setup() jest.spyOn(ImagingRepository, 'save').mockResolvedValue({ ...expectedImaging }) const patientTypeahead = wrapper.find(Typeahead) diff --git a/src/__tests__/imagings/search/ImagingRequestTable.test.tsx b/src/__tests__/imagings/search/ImagingRequestTable.test.tsx new file mode 100644 index 0000000000..0f821a7708 --- /dev/null +++ b/src/__tests__/imagings/search/ImagingRequestTable.test.tsx @@ -0,0 +1,82 @@ +import { Table } from '@hospitalrun/components' +import { mount, ReactWrapper } from 'enzyme' +import React from 'react' +import { act } from 'react-dom/test-utils' + +import ImagingSearchRequest from '../../../imagings/model/ImagingSearchRequest' +import ImagingRequestTable from '../../../imagings/search/ImagingRequestTable' +import ImagingRepository from '../../../shared/db/ImagingRepository' +import SortRequest from '../../../shared/db/SortRequest' +import Imaging from '../../../shared/model/Imaging' + +const defaultSortRequest: SortRequest = { + sorts: [ + { + field: 'requestedOn', + direction: 'desc', + }, + ], +} + +describe('Imaging Request Table', () => { + const expectedImaging = { + code: 'I-1234', + id: '1234', + type: 'imaging type', + patient: 'patient', + fullName: 'full name', + status: 'requested', + requestedOn: new Date().toISOString(), + requestedBy: 'some user', + } as Imaging + const expectedImagings = [expectedImaging] + + const setup = async (searchRequest: ImagingSearchRequest) => { + jest.resetAllMocks() + jest.spyOn(ImagingRepository, 'search').mockResolvedValue(expectedImagings) + let wrapper: any + + await act(async () => { + wrapper = await mount() + }) + wrapper.update() + + return { wrapper: wrapper as ReactWrapper } + } + + it('should search for imaging requests ', async () => { + const expectedSearch: ImagingSearchRequest = { status: 'all', text: 'text' } + await setup(expectedSearch) + + expect(ImagingRepository.search).toHaveBeenCalledTimes(1) + expect(ImagingRepository.search).toHaveBeenCalledWith({ ...expectedSearch, defaultSortRequest }) + }) + + it('should render a table of imaging requests', async () => { + const expectedSearch: ImagingSearchRequest = { status: 'all', text: 'text' } + const { wrapper } = await setup(expectedSearch) + + 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: 'fullName' }), + ) + 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/util/validate-imaging-request.test.ts b/src/__tests__/imagings/util/validate-imaging-request.test.ts new file mode 100644 index 0000000000..af3f23e659 --- /dev/null +++ b/src/__tests__/imagings/util/validate-imaging-request.test.ts @@ -0,0 +1,17 @@ +import validateImagingRequest from '../../../imagings/util/validate-imaging-request' +import Imaging from '../../../shared/model/Imaging' + +describe('imaging request validator', () => { + it('should validate the required fields apart of an incident', async () => { + const newImagingRequest = {} as Imaging + const expectedError = { + patient: 'imagings.requests.error.patientRequired', + status: 'imagings.requests.error.statusRequired', + type: 'imagings.requests.error.typeRequired', + } + + const actualError = validateImagingRequest(newImagingRequest) + + expect(actualError).toEqual(expectedError) + }) +}) diff --git a/src/__tests__/incidents/Incidents.test.tsx b/src/__tests__/incidents/Incidents.test.tsx index 41d551c003..07f73e607b 100644 --- a/src/__tests__/incidents/Incidents.test.tsx +++ b/src/__tests__/incidents/Incidents.test.tsx @@ -9,6 +9,8 @@ import thunk from 'redux-thunk' import Incidents from '../../incidents/Incidents' import ReportIncident from '../../incidents/report/ReportIncident' import ViewIncident from '../../incidents/view/ViewIncident' +import VisualizeIncidents from '../../incidents/visualize/VisualizeIncidents' +import * as titleUtil from '../../page-header/title/TitleContext' import IncidentRepository from '../../shared/db/IncidentRepository' import Incident from '../../shared/model/Incident' import Permissions from '../../shared/model/Permissions' @@ -24,10 +26,10 @@ describe('Incidents', () => { date: new Date().toISOString(), reportedOn: new Date().toISOString(), } as Incident + jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) jest.spyOn(IncidentRepository, 'search').mockResolvedValue([]) jest.spyOn(IncidentRepository, 'find').mockResolvedValue(expectedIncident) const store = mockStore({ - title: 'test', user: { permissions }, breadcrumbs: { breadcrumbs: [] }, components: { sidebarCollapsed: false }, @@ -38,16 +40,26 @@ describe('Incidents', () => { wrapper = await mount( - + + + , ) }) + wrapper.find(Incidents).props().updateTitle = jest.fn() wrapper.update() return { wrapper: wrapper as ReactWrapper } } + describe('title', () => { + it('should have called the useUpdateTitle hook', async () => { + await setup([Permissions.ViewIncidents], '/incidents') + expect(titleUtil.useUpdateTitle).toHaveBeenCalledTimes(1) + }) + }) + describe('routing', () => { describe('/incidents/new', () => { it('should render the new incident screen when /incidents/new is accessed', async () => { @@ -63,6 +75,20 @@ describe('Incidents', () => { }) }) + describe('/incidents/visualize', () => { + it('should render the incident visualize screen when /incidents/visualize is accessed', async () => { + const { wrapper } = await setup([Permissions.ViewIncidentWidgets], '/incidents/visualize') + + expect(wrapper.find(VisualizeIncidents)).toHaveLength(1) + }) + + it('should not navigate to /incidents/visualize if the user does not have ViewIncidentWidgets permissions', async () => { + const { wrapper } = await setup([], '/incidents/visualize') + + expect(wrapper.find(VisualizeIncidents)).toHaveLength(0) + }) + }) + describe('/incidents/:id', () => { it('should render the view incident screen when /incidents/:id is accessed', async () => { const { wrapper } = await setup([Permissions.ViewIncident], '/incidents/1234') diff --git a/src/__tests__/incidents/list/ViewIncidents.test.tsx b/src/__tests__/incidents/list/ViewIncidents.test.tsx index e67c544f5f..db356f4074 100644 --- a/src/__tests__/incidents/list/ViewIncidents.test.tsx +++ b/src/__tests__/incidents/list/ViewIncidents.test.tsx @@ -12,7 +12,7 @@ import ViewIncidents from '../../../incidents/list/ViewIncidents' import ViewIncidentsTable from '../../../incidents/list/ViewIncidentsTable' 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 * as titleUtil from '../../../page-header/title/TitleContext' import IncidentRepository from '../../../shared/db/IncidentRepository' import Incident from '../../../shared/model/Incident' import Permissions from '../../../shared/model/Permissions' @@ -39,7 +39,7 @@ describe('View Incidents', () => { jest.resetAllMocks() jest.spyOn(breadcrumbUtil, 'default') setButtonToolBarSpy = jest.fn() - jest.spyOn(titleUtil, 'default') + jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter').mockReturnValue(setButtonToolBarSpy) jest.spyOn(IncidentRepository, 'findAll').mockResolvedValue(expectedIncidents) jest.spyOn(IncidentRepository, 'search').mockResolvedValue(expectedIncidents) @@ -47,7 +47,6 @@ describe('View Incidents', () => { history = createMemoryHistory() history.push(`/incidents`) const store = mockStore({ - title: '', user: { permissions, }, @@ -60,16 +59,25 @@ describe('View Incidents', () => { - + + + , ) }) + wrapper.find(ViewIncidents).props().updateTitle = jest.fn() wrapper.update() return { wrapper: wrapper as ReactWrapper } } + + it('should have called the useUpdateTitle hook', async () => { + await setup([Permissions.ViewIncidents]) + expect(titleUtil.useUpdateTitle).toHaveBeenCalledTimes(1) + }) + it('should filter incidents by status=reported on first load ', async () => { await setup([Permissions.ViewIncidents]) @@ -78,12 +86,6 @@ describe('View Incidents', () => { }) describe('layout', () => { - it('should set the title', async () => { - await setup([Permissions.ViewIncidents]) - - expect(titleUtil.default).toHaveBeenCalledWith('incidents.reports.label') - }) - it('should render a table with the incidents', async () => { const { wrapper } = await setup([Permissions.ViewIncidents]) const table = wrapper.find(ViewIncidentsTable) diff --git a/src/__tests__/incidents/list/ViewIncidentsTable.test.tsx b/src/__tests__/incidents/list/ViewIncidentsTable.test.tsx index d01ef96a52..c0168d6794 100644 --- a/src/__tests__/incidents/list/ViewIncidentsTable.test.tsx +++ b/src/__tests__/incidents/list/ViewIncidentsTable.test.tsx @@ -1,4 +1,5 @@ -import { Table } from '@hospitalrun/components' +import { Table, Dropdown } from '@hospitalrun/components' +import format from 'date-fns/format' import { mount, ReactWrapper } from 'enzyme' import { createMemoryHistory } from 'history' import React from 'react' @@ -6,7 +7,7 @@ import { act } from 'react-dom/test-utils' import { Router } from 'react-router' import IncidentFilter from '../../../incidents/IncidentFilter' -import ViewIncidentsTable from '../../../incidents/list/ViewIncidentsTable' +import ViewIncidentsTable, { populateExportData } from '../../../incidents/list/ViewIncidentsTable' import IncidentSearchRequest from '../../../incidents/model/IncidentSearchRequest' import IncidentRepository from '../../../shared/db/IncidentRepository' import Incident from '../../../shared/model/Incident' @@ -73,6 +74,58 @@ describe('View Incidents Table', () => { expect(incidentsTable.prop('actionsHeaderText')).toEqual('actions.label') }) + it('should display a download button', async () => { + const expectedIncidents: Incident[] = [ + { + id: 'incidentId1', + code: 'someCode', + date: new Date(2020, 7, 4, 0, 0, 0, 0).toISOString(), + reportedOn: new Date(2020, 8, 4, 0, 0, 0, 0).toISOString(), + reportedBy: 'com.test:user', + status: 'reported', + } as Incident, + ] + const { wrapper } = await setup({ status: IncidentFilter.all }, expectedIncidents) + + const dropDownButton = wrapper.find(Dropdown) + expect(dropDownButton.exists()).toBeTruthy() + }) + + it('should populate export data correctly', async () => { + const data = [ + { + category: 'asdf', + categoryItem: 'asdf', + code: 'I-eClU6OdkR', + createdAt: '2020-09-06T04:02:38.011Z', + date: '2020-09-06T04:02:32.855Z', + department: 'asdf', + description: 'asdf', + id: 'af9f968f-61d9-47c3-9321-5da3f381c38b', + reportedBy: 'some user', + reportedOn: '2020-09-06T04:02:38.011Z', + rev: '1-91d1ba60588b779c9554c7e20e15419c', + status: 'reported', + updatedAt: '2020-09-06T04:02:38.011Z', + }, + ] + + const expectedExportData = [ + { + code: 'I-eClU6OdkR', + date: format(new Date(data[0].date), 'yyyy-MM-dd hh:mm a'), + reportedBy: 'some user', + reportedOn: format(new Date(data[0].reportedOn), 'yyyy-MM-dd hh:mm a'), + status: 'reported', + }, + ] + + const exportData = [{}] + populateExportData(exportData, data) + + expect(exportData).toEqual(expectedExportData) + }) + it('should format the data correctly', async () => { const expectedIncidents: Incident[] = [ { diff --git a/src/__tests__/incidents/report/ReportIncident.test.tsx b/src/__tests__/incidents/report/ReportIncident.test.tsx index 4e3c2ee2da..6e16c55f8d 100644 --- a/src/__tests__/incidents/report/ReportIncident.test.tsx +++ b/src/__tests__/incidents/report/ReportIncident.test.tsx @@ -14,7 +14,7 @@ import ReportIncident from '../../../incidents/report/ReportIncident' import * as validationUtil from '../../../incidents/util/validate-incident' 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 * as titleUtil from '../../../page-header/title/TitleContext' import IncidentRepository from '../../../shared/db/IncidentRepository' import Incident from '../../../shared/model/Incident' import Permissions from '../../../shared/model/Permissions' @@ -34,13 +34,12 @@ describe('Report Incident', () => { const setup = async (permissions: Permissions[]) => { jest.spyOn(breadcrumbUtil, 'default') setButtonToolBarSpy = jest.fn() - jest.spyOn(titleUtil, 'default') + jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter').mockReturnValue(setButtonToolBarSpy) history = createMemoryHistory() history.push(`/incidents/new`) const store = mockStore({ - title: '', user: { permissions, user: { @@ -56,24 +55,28 @@ describe('Report Incident', () => { - + + + , ) }) + wrapper.find(ReportIncident).props().updateTitle = jest.fn() wrapper.update() return wrapper as ReactWrapper } - describe('layout', () => { - it('should set the title', async () => { + describe('title', () => { + it('should have called the useUpdateTitle hook', async () => { await setup([Permissions.ReportIncident]) - - expect(titleUtil.default).toHaveBeenCalledWith('incidents.reports.new') + expect(titleUtil.useUpdateTitle).toHaveBeenCalledTimes(1) }) + }) + describe('layout', () => { it('should set the breadcrumbs properly', async () => { await setup([Permissions.ReportIncident]) diff --git a/src/__tests__/incidents/view/ViewIncident.test.tsx b/src/__tests__/incidents/view/ViewIncident.test.tsx index 3d944ee096..b0706f7eb9 100644 --- a/src/__tests__/incidents/view/ViewIncident.test.tsx +++ b/src/__tests__/incidents/view/ViewIncident.test.tsx @@ -11,19 +11,20 @@ import ViewIncident from '../../../incidents/view/ViewIncident' import ViewIncidentDetails from '../../../incidents/view/ViewIncidentDetails' 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 * as titleUtil from '../../../page-header/title/TitleContext' import IncidentRepository from '../../../shared/db/IncidentRepository' import Incident from '../../../shared/model/Incident' import Permissions from '../../../shared/model/Permissions' import { RootState } from '../../../shared/store' +const { TitleProvider } = titleUtil const mockStore = createMockStore([thunk]) describe('View Incident', () => { const setup = async (permissions: Permissions[]) => { jest.resetAllMocks() + jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) jest.spyOn(breadcrumbUtil, 'default') - jest.spyOn(titleUtil, 'default') jest.spyOn(IncidentRepository, 'find').mockResolvedValue({ id: '1234', date: new Date().toISOString(), @@ -34,7 +35,6 @@ describe('View Incident', () => { history.push(`/incidents/1234`) const store = mockStore({ - title: '', user: { permissions, }, @@ -47,7 +47,9 @@ describe('View Incident', () => { - + + + @@ -58,18 +60,11 @@ describe('View Incident', () => { return { wrapper: wrapper as ReactWrapper, history } } - it('should render the title correctly', async () => { - await setup([Permissions.ViewIncident]) - - expect(titleUtil.default).toHaveBeenCalledTimes(1) - expect(titleUtil.default).toHaveBeenCalledWith('View Incident') - }) - it('should set the breadcrumbs properly', async () => { await setup([Permissions.ViewIncident]) expect(breadcrumbUtil.default).toHaveBeenCalledWith([ - { i18nKey: 'View Incident', location: '/incidents/1234' }, + { i18nKey: 'incidents.reports.view', location: '/incidents/1234' }, ]) }) diff --git a/src/__tests__/incidents/view/ViewIncidentDetails.test.tsx b/src/__tests__/incidents/view/ViewIncidentDetails.test.tsx index b00faa0f78..69619d1626 100644 --- a/src/__tests__/incidents/view/ViewIncidentDetails.test.tsx +++ b/src/__tests__/incidents/view/ViewIncidentDetails.test.tsx @@ -8,7 +8,6 @@ import { Router } from 'react-router' import ViewIncidentDetails from '../../../incidents/view/ViewIncidentDetails' 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 IncidentRepository from '../../../shared/db/IncidentRepository' import Incident from '../../../shared/model/Incident' import Permissions from '../../../shared/model/Permissions' @@ -35,7 +34,6 @@ describe('View Incident Details', () => { jest.resetAllMocks() Date.now = jest.fn(() => expectedResolveDate.valueOf()) jest.spyOn(breadcrumbUtil, 'default') - jest.spyOn(titleUtil, 'default') jest.spyOn(IncidentRepository, 'find').mockResolvedValue(mockIncident) incidentRepositorySaveSpy = jest .spyOn(IncidentRepository, 'saveOrUpdate') diff --git a/src/__tests__/labs/Labs.test.tsx b/src/__tests__/labs/Labs.test.tsx index 2dde1714c5..e8254db294 100644 --- a/src/__tests__/labs/Labs.test.tsx +++ b/src/__tests__/labs/Labs.test.tsx @@ -1,5 +1,4 @@ -import { act } from '@testing-library/react' -import { mount } from 'enzyme' +import { mount, ReactWrapper } from 'enzyme' import React from 'react' import { Provider } from 'react-redux' import { MemoryRouter } from 'react-router-dom' @@ -9,6 +8,7 @@ import thunk from 'redux-thunk' import Labs from '../../labs/Labs' import NewLabRequest from '../../labs/requests/NewLabRequest' import ViewLab from '../../labs/ViewLab' +import * as titleUtil from '../../page-header/title/TitleContext' import LabRepository from '../../shared/db/LabRepository' import PatientRepository from '../../shared/db/PatientRepository' import Lab from '../../shared/model/Lab' @@ -16,9 +16,11 @@ import Patient from '../../shared/model/Patient' import Permissions from '../../shared/model/Permissions' import { RootState } from '../../shared/store' +const { TitleProvider } = titleUtil const mockStore = createMockStore([thunk]) describe('Labs', () => { + jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) jest.spyOn(LabRepository, 'findAll').mockResolvedValue([]) jest .spyOn(LabRepository, 'find') @@ -26,48 +28,46 @@ describe('Labs', () => { jest .spyOn(PatientRepository, 'find') .mockResolvedValue({ id: '12345', fullName: 'test test' } as Patient) + const setup = (route: string, permissions: Permissions[] = []) => { + const store = mockStore({ + user: { permissions }, + breadcrumbs: { breadcrumbs: [] }, + components: { sidebarCollapsed: false }, + lab: { + lab: ({ + id: '1234', + patientId: 'patientId', + requestedOn: new Date().toISOString(), + } as unknown) as Lab, + patient: { id: 'patientId', fullName: 'some name' }, + error: {}, + }, + } as any) + + const wrapper = mount( + + + + + + + , + ) + wrapper.update() + return { wrapper: wrapper as ReactWrapper } + } describe('routing', () => { describe('/labs/new', () => { it('should render the new lab request screen when /labs/new is accessed', () => { - const store = mockStore({ - title: 'test', - user: { permissions: [Permissions.RequestLab] }, - breadcrumbs: { breadcrumbs: [] }, - components: { sidebarCollapsed: false }, - lab: { - lab: { id: 'labId', patientId: 'patientId' } as Lab, - patient: { id: 'patientId', fullName: 'some name' }, - error: {}, - }, - } as any) - - const wrapper = mount( - - - - - , - ) + const permissions: Permissions[] = [Permissions.RequestLab] + const { wrapper } = setup('/labs/new', permissions) expect(wrapper.find(NewLabRequest)).toHaveLength(1) }) it('should not navigate to /labs/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( - - - - - , - ) + const { wrapper } = setup('/labs/new') expect(wrapper.find(NewLabRequest)).toHaveLength(0) }) @@ -75,55 +75,17 @@ describe('Labs', () => { describe('/labs/:id', () => { it('should render the view lab screen when /labs/:id is accessed', async () => { - const store = mockStore({ - title: 'test', - user: { permissions: [Permissions.ViewLab] }, - breadcrumbs: { breadcrumbs: [] }, - components: { sidebarCollapsed: false }, - lab: { - lab: { - id: 'labId', - patientId: 'patientId', - requestedOn: new Date().toISOString(), - } as Lab, - patient: { id: 'patientId', fullName: 'some name' }, - error: {}, - }, - } as any) - - let wrapper: any - - await act(async () => { - wrapper = await mount( - - - - - , - ) + const permissions: Permissions[] = [Permissions.ViewLab] + const { wrapper } = setup('/labs/1234', permissions) - expect(wrapper.find(ViewLab)).toHaveLength(1) - }) + expect(wrapper.find(ViewLab)).toHaveLength(1) }) + }) - it('should not navigate to /labs/:id if the user does not have ViewLab permissions', async () => { - const store = mockStore({ - title: 'test', - user: { permissions: [] }, - breadcrumbs: { breadcrumbs: [] }, - components: { sidebarCollapsed: false }, - } as any) - - const wrapper = await mount( - - - - - , - ) + it('should not navigate to /labs/:id if the user does not have ViewLab permissions', async () => { + const { wrapper } = setup('/labs/1234') - expect(wrapper.find(ViewLab)).toHaveLength(0) - }) + expect(wrapper.find(ViewLab)).toHaveLength(0) }) }) }) diff --git a/src/__tests__/labs/ViewLab.test.tsx b/src/__tests__/labs/ViewLab.test.tsx index 9260d10ac7..baa9019ac5 100644 --- a/src/__tests__/labs/ViewLab.test.tsx +++ b/src/__tests__/labs/ViewLab.test.tsx @@ -1,7 +1,7 @@ import { Badge, Button, Alert } from '@hospitalrun/components' import { act } from '@testing-library/react' import format from 'date-fns/format' -import { mount } from 'enzyme' +import { mount, ReactWrapper } from 'enzyme' import { createMemoryHistory } from 'history' import React from 'react' import { Provider } from 'react-redux' @@ -11,7 +11,7 @@ import thunk from 'redux-thunk' import ViewLab from '../../labs/ViewLab' import * as ButtonBarProvider from '../../page-header/button-toolbar/ButtonBarProvider' -import * as titleUtil from '../../page-header/title/useTitle' +import * as titleUtil from '../../page-header/title/TitleContext' import TextFieldWithLabelFormGroup from '../../shared/components/input/TextFieldWithLabelFormGroup' import LabRepository from '../../shared/db/LabRepository' import PatientRepository from '../../shared/db/PatientRepository' @@ -31,19 +31,18 @@ describe('View Lab', () => { status: 'requested', patient: '1234', type: 'lab type', - notes: 'lab notes', + notes: ['lab notes'], requestedOn: '2020-03-30T04:43:20.102Z', } as Lab let setButtonToolBarSpy: any - let titleSpy: any let labRepositorySaveSpy: any const expectedDate = new Date() const setup = async (lab: Lab, permissions: Permissions[], error = {}) => { jest.resetAllMocks() Date.now = jest.fn(() => expectedDate.valueOf()) setButtonToolBarSpy = jest.fn() - titleSpy = jest.spyOn(titleUtil, 'default') + jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter').mockReturnValue(setButtonToolBarSpy) jest.spyOn(LabRepository, 'find').mockResolvedValue(lab) labRepositorySaveSpy = jest.spyOn(LabRepository, 'saveOrUpdate').mockResolvedValue(mockLab) @@ -52,7 +51,6 @@ describe('View Lab', () => { history = createMemoryHistory() history.push(`labs/${lab.id}`) const store = mockStore({ - title: '', user: { permissions, }, @@ -71,23 +69,26 @@ describe('View Lab', () => { - + + + , ) }) + wrapper.find(ViewLab).props().updateTitle = jest.fn() wrapper.update() return wrapper } - it('should set the title', async () => { - await setup(mockLab, [Permissions.ViewLab]) - - expect(titleSpy).toHaveBeenCalledWith( - `${mockLab.type} for ${mockPatient.fullName}(${mockLab.code})`, - ) + describe('title', () => { + it('should have called the useUpdateTitle hook', async () => { + const expectedLab = { ...mockLab } as Lab + await setup(expectedLab, [Permissions.ViewLab]) + expect(titleUtil.useUpdateTitle).toHaveBeenCalled() + }) }) describe('page content', () => { @@ -150,15 +151,31 @@ describe('View Lab', () => { expect(resultTextField.prop('value')).toEqual(expectedLab.result) }) - it('should display the notes in the notes text field', async () => { - const expectedLab = { ...mockLab, notes: 'expected notes' } as Lab + it('should display the past notes', async () => { + const expectedNotes = 'expected notes' + const expectedLab = { ...mockLab, notes: [expectedNotes] } as Lab + const wrapper = await setup(expectedLab, [Permissions.ViewLab]) + + const notes = wrapper.find('[data-test="note"]') + const pastNotesIndex = notes.reduce( + (result: number, item: ReactWrapper, index: number) => + item.text().trim() === expectedNotes ? index : result, + -1, + ) + + expect(pastNotesIndex).not.toBe(-1) + expect(notes.length).toBe(1) + }) + + it('should display the notes text field empty', async () => { + const expectedNotes = 'expected notes' + const expectedLab = { ...mockLab, notes: [expectedNotes] } as Lab const wrapper = await setup(expectedLab, [Permissions.ViewLab]) const notesTextField = wrapper.find(TextFieldWithLabelFormGroup).at(1) expect(notesTextField).toBeDefined() - expect(notesTextField.prop('label')).toEqual('labs.lab.notes') - expect(notesTextField.prop('value')).toEqual(expectedLab.notes) + expect(notesTextField.prop('value')).toEqual('') }) it('should display errors', async () => { @@ -188,9 +205,7 @@ describe('View Lab', () => { }) it('should display a update lab, complete lab, and cancel lab button if the lab is in a requested state', async () => { - const expectedLab = { ...mockLab, notes: 'expected notes' } as Lab - - const wrapper = await setup(expectedLab, [ + const wrapper = await setup(mockLab, [ Permissions.ViewLab, Permissions.CompleteLab, Permissions.CancelLab, @@ -254,6 +269,18 @@ describe('View Lab', () => { const updateButton = wrapper.find(Button) expect(updateButton).toHaveLength(0) }) + + it('should not display notes text field if the status is canceled', async () => { + const expectedLab = { ...mockLab, status: 'canceled' } as Lab + + const wrapper = await setup(expectedLab, [Permissions.ViewLab]) + + const textsField = wrapper.find(TextFieldWithLabelFormGroup) + const notesTextField = wrapper.find('notesTextField') + + expect(textsField.length).toBe(1) + expect(notesTextField).toHaveLength(0) + }) }) describe('completed lab request', () => { @@ -297,6 +324,22 @@ describe('View Lab', () => { const buttons = wrapper.find(Button) expect(buttons).toHaveLength(0) }) + + it('should not display notes text field if the status is completed', async () => { + const expectedLab = { ...mockLab, status: 'completed' } as Lab + + const wrapper = await setup(expectedLab, [ + Permissions.ViewLab, + Permissions.CompleteLab, + Permissions.CancelLab, + ]) + + const textsField = wrapper.find(TextFieldWithLabelFormGroup) + const notesTextField = wrapper.find('notesTextField') + + expect(textsField.length).toBe(1) + expect(notesTextField).toHaveLength(0) + }) }) }) @@ -304,7 +347,7 @@ describe('View Lab', () => { it('should update the lab with the new information', async () => { const wrapper = await setup(mockLab, [Permissions.ViewLab]) const expectedResult = 'expected result' - const expectedNotes = 'expected notes' + const newNotes = 'expected notes' const resultTextField = wrapper.find(TextFieldWithLabelFormGroup).at(0) act(() => { @@ -316,7 +359,7 @@ describe('View Lab', () => { const notesTextField = wrapper.find(TextFieldWithLabelFormGroup).at(1) act(() => { const onChange = notesTextField.prop('onChange') - onChange({ currentTarget: { value: expectedNotes } }) + onChange({ currentTarget: { value: newNotes } }) }) wrapper.update() const updateButton = wrapper.find(Button) @@ -325,6 +368,8 @@ describe('View Lab', () => { onClick() }) + const expectedNotes = mockLab.notes ? [...mockLab.notes, newNotes] : [newNotes] + expect(labRepositorySaveSpy).toHaveBeenCalledTimes(1) expect(labRepositorySaveSpy).toHaveBeenCalledWith( expect.objectContaining({ ...mockLab, result: expectedResult, notes: expectedNotes }), diff --git a/src/__tests__/labs/ViewLabs.test.tsx b/src/__tests__/labs/ViewLabs.test.tsx index fefdea666f..9a14691b4b 100644 --- a/src/__tests__/labs/ViewLabs.test.tsx +++ b/src/__tests__/labs/ViewLabs.test.tsx @@ -1,4 +1,4 @@ -import { TextInput, Select, Table } from '@hospitalrun/components' +import { Select, Table } from '@hospitalrun/components' import { act } from '@testing-library/react' import { mount, ReactWrapper } from 'enzyme' import { createMemoryHistory } from 'history' @@ -11,7 +11,7 @@ import thunk from 'redux-thunk' import * as labsSlice from '../../labs/labs-slice' import ViewLabs from '../../labs/ViewLabs' import * as ButtonBarProvider from '../../page-header/button-toolbar/ButtonBarProvider' -import * as titleUtil from '../../page-header/title/useTitle' +import * as titleUtil from '../../page-header/title/TitleContext' import LabRepository from '../../shared/db/LabRepository' import Lab from '../../shared/model/Lab' import Permissions from '../../shared/model/Permissions' @@ -19,75 +19,65 @@ import { RootState } from '../../shared/store' const mockStore = createMockStore([thunk]) -describe('View Labs', () => { - describe('title', () => { - let titleSpy: any - beforeEach(async () => { - const store = mockStore({ - title: '', - user: { permissions: [Permissions.ViewLabs, Permissions.RequestLab] }, - labs: { labs: [] }, - } as any) - titleSpy = jest.spyOn(titleUtil, 'default') - jest.spyOn(LabRepository, 'findAll').mockResolvedValue([]) - await act(async () => { - await mount( - - - - - , - ) - }) - }) - - it('should have the title', () => { - expect(titleSpy).toHaveBeenCalledWith('labs.label') - }) +let history: any +const expectedLab = { + code: 'L-1234', + id: '1234', + type: 'lab type', + patient: 'patientId', + status: 'requested', + requestedOn: new Date().toISOString(), +} as Lab + +const setup = (permissions: Permissions[] = [Permissions.ViewLabs, Permissions.RequestLab]) => { + const store = mockStore({ + user: { permissions }, + labs: { labs: [expectedLab] }, + } as any) + history = createMemoryHistory() + + jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) + jest.spyOn(LabRepository, 'findAll').mockResolvedValue([expectedLab]) + + const wrapper = mount( + + + + + + + , + ) + + wrapper.find(ViewLabs).props().updateTitle = jest.fn() + wrapper.update() + return { wrapper: wrapper as ReactWrapper } +} + +describe('title', () => { + it('should have called the useUpdateTitle hook', async () => { + setup() + expect(titleUtil.useUpdateTitle).toHaveBeenCalled() }) +}) +describe('View Labs', () => { describe('button bar', () => { it('should display button to add new lab request', async () => { - const store = mockStore({ - title: '', - user: { permissions: [Permissions.ViewLabs, Permissions.RequestLab] }, - labs: { labs: [] }, - } as any) const setButtonToolBarSpy = jest.fn() jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter').mockReturnValue(setButtonToolBarSpy) jest.spyOn(LabRepository, 'findAll').mockResolvedValue([]) - await act(async () => { - await mount( - - - - - , - ) - }) + setup() const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0] expect((actualButtons[0] as any).props.children).toEqual('labs.requests.new') }) it('should not display button to add new lab request if the user does not have permissions', async () => { - const store = mockStore({ - title: '', - user: { permissions: [Permissions.ViewLabs] }, - labs: { labs: [] }, - } as any) const setButtonToolBarSpy = jest.fn() jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter').mockReturnValue(setButtonToolBarSpy) jest.spyOn(LabRepository, 'findAll').mockResolvedValue([]) - await act(async () => { - await mount( - - - - - , - ) - }) + setup([]) const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0] expect(actualButtons).toEqual([]) @@ -95,40 +85,8 @@ describe('View Labs', () => { }) describe('table', () => { - let wrapper: ReactWrapper - let history: any - const expectedLab = { - code: 'L-1234', - id: '1234', - type: 'lab type', - patient: 'patientId', - status: 'requested', - requestedOn: '2020-03-30T04:43:20.102Z', - } as Lab - - beforeEach(async () => { - const store = mockStore({ - title: '', - user: { permissions: [Permissions.ViewLabs, Permissions.RequestLab] }, - labs: { labs: [expectedLab] }, - } as any) - history = createMemoryHistory() - - jest.spyOn(LabRepository, 'findAll').mockResolvedValue([expectedLab]) - await act(async () => { - wrapper = await mount( - - - - - , - ) - }) - - wrapper.update() - }) - it('should render a table with data', () => { + const { wrapper } = setup() const table = wrapper.find(Table) const columns = table.prop('columns') const actions = table.prop('actions') as any @@ -147,6 +105,7 @@ describe('View Labs', () => { }) it('should navigate to the lab when the view button is clicked', () => { + const { wrapper } = setup() const tr = wrapper.find('tr').at(1) act(() => { @@ -160,110 +119,53 @@ describe('View Labs', () => { describe('dropdown', () => { it('should search for labs when dropdown changes', () => { const searchLabsSpy = jest.spyOn(labsSlice, 'searchLabs') - let wrapper: ReactWrapper - let history: any - const expectedLab = { - id: '1234', - type: 'lab type', - patient: 'patientId', - status: 'requested', - requestedOn: '2020-03-30T04:43:20.102Z', - } as Lab + const { wrapper } = setup() - beforeEach(async () => { - const store = mockStore({ - title: '', - user: { permissions: [Permissions.ViewLabs, Permissions.RequestLab] }, - labs: { labs: [expectedLab] }, - } as any) - history = createMemoryHistory() + searchLabsSpy.mockClear() - await act(async () => { - wrapper = await mount( - - - - - , - ) - }) - - searchLabsSpy.mockClear() - - act(() => { - const onChange = wrapper.find(Select).prop('onChange') as any - onChange({ - target: { - value: 'requested', - }, - preventDefault: jest.fn(), - }) + act(() => { + const onChange = wrapper.find(Select).prop('onChange') as any + onChange({ + target: { + value: 'requested', + }, + preventDefault: jest.fn(), }) - - wrapper.update() - expect(searchLabsSpy).toHaveBeenCalledTimes(1) }) + + wrapper.update() + expect(searchLabsSpy).toHaveBeenCalledTimes(1) }) }) +}) - describe('search functionality', () => { - beforeEach(() => jest.useFakeTimers()) - - afterEach(() => jest.useRealTimers()) +describe('search functionality', () => { + beforeEach(() => jest.useFakeTimers()) - it('should search for labs after the search text has not changed for 500 milliseconds', () => { - const searchLabsSpy = jest.spyOn(labsSlice, 'searchLabs') - let wrapper: ReactWrapper - let history: any - const expectedLab = { - id: '1234', - type: 'lab type', - patient: 'patientId', - status: 'requested', - requestedOn: '2020-03-30T04:43:20.102Z', - } as Lab + afterEach(() => jest.useRealTimers()) - beforeEach(async () => { - const store = mockStore({ - title: '', - user: { permissions: [Permissions.ViewLabs, Permissions.RequestLab] }, - labs: { labs: [expectedLab] }, - } as any) - history = createMemoryHistory() + it('should search for labs after the search text has not changed for 500 milliseconds', async () => { + const searchLabsSpy = jest.spyOn(labsSlice, 'searchLabs') - jest.spyOn(LabRepository, 'findAll').mockResolvedValue([expectedLab]) - await act(async () => { - wrapper = await mount( - - - - - , - ) - }) + searchLabsSpy.mockClear() - searchLabsSpy.mockClear() - const expectedSearchText = 'search text' + beforeEach(async () => { + const { wrapper } = setup() - act(() => { - const onClick = wrapper.find(TextInput).prop('onChange') as any - onClick({ - target: { - value: expectedSearchText, - }, - preventDefault: jest.fn(), - }) - }) + searchLabsSpy.mockClear() - act(() => { - jest.advanceTimersByTime(500) + act(() => { + const onChange = wrapper.find(Select).prop('onChange') as any + onChange({ + target: { + value: 'requested', + }, + preventDefault: jest.fn(), }) - - wrapper.update() - - expect(searchLabsSpy).toHaveBeenCalledTimes(1) - expect(searchLabsSpy).toHaveBeenLastCalledWith(expectedSearchText) }) + + wrapper.update() + expect(searchLabsSpy).toHaveBeenCalledTimes(1) }) }) }) diff --git a/src/__tests__/labs/requests/NewLabRequest.test.tsx b/src/__tests__/labs/requests/NewLabRequest.test.tsx index 8723ce5515..a9175944b3 100644 --- a/src/__tests__/labs/requests/NewLabRequest.test.tsx +++ b/src/__tests__/labs/requests/NewLabRequest.test.tsx @@ -9,7 +9,7 @@ import createMockStore from 'redux-mock-store' import thunk from 'redux-thunk' import NewLabRequest from '../../../labs/requests/NewLabRequest' -import * as titleUtil from '../../../page-header/title/useTitle' +import * as titleUtil from '../../../page-header/title/TitleContext' import TextFieldWithLabelFormGroup from '../../../shared/components/input/TextFieldWithLabelFormGroup' import TextInputWithLabelFormGroup from '../../../shared/components/input/TextInputWithLabelFormGroup' import LabRepository from '../../../shared/db/LabRepository' @@ -21,47 +21,34 @@ import { RootState } from '../../../shared/store' const mockStore = createMockStore([thunk]) describe('New Lab Request', () => { - describe('title and breadcrumbs', () => { - let titleSpy: any + const setup = async (store = mockStore({ lab: { status: 'loading', error: {} } } as any)) => { const history = createMemoryHistory() + history.push(`/labs/new`) + jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) - beforeEach(() => { - const store = mockStore({ title: '', lab: { status: 'loading', error: {} } } as any) - titleSpy = jest.spyOn(titleUtil, 'default') - history.push('/labs/new') - - mount( - - + const wrapper: ReactWrapper = await mount( + + + - - , - ) - }) + + + , + ) - it('should have New Lab Request as the title', () => { - expect(titleSpy).toHaveBeenCalledWith('labs.requests.new') - }) - }) + wrapper.find(NewLabRequest).props().updateTitle = jest.fn() + wrapper.update() + return { wrapper } + } describe('form layout', () => { - let wrapper: ReactWrapper - const history = createMemoryHistory() - - beforeEach(() => { - const store = mockStore({ title: '', lab: { status: 'loading', error: {} } } as any) - history.push('/labs/new') - - wrapper = mount( - - - - - , - ) + it('should have called the useUpdateTitle hook', async () => { + await setup() + expect(titleUtil.useUpdateTitle).toHaveBeenCalledTimes(1) }) - it('should render a patient typeahead', () => { + it('should render a patient typeahead', async () => { + const { wrapper } = await setup() const typeaheadDiv = wrapper.find('.patient-typeahead') expect(typeaheadDiv).toBeDefined() @@ -76,7 +63,8 @@ describe('New Lab 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() const typeInputBox = wrapper.find(TextInputWithLabelFormGroup) expect(typeInputBox).toBeDefined() @@ -85,7 +73,8 @@ describe('New Lab Request', () => { expect(typeInputBox.prop('isEditable')).toBeTruthy() }) - it('should render a notes text field', () => { + it('should render a notes text field', async () => { + const { wrapper } = await setup() const notesTextField = wrapper.find(TextFieldWithLabelFormGroup) expect(notesTextField).toBeDefined() @@ -94,41 +83,31 @@ describe('New Lab Request', () => { expect(notesTextField.prop('isEditable')).toBeTruthy() }) - it('should render a save button', () => { + it('should render a save button', async () => { + const { wrapper } = await setup() const saveButton = wrapper.find(Button).at(0) expect(saveButton).toBeDefined() expect(saveButton.text().trim()).toEqual('labs.requests.save') }) - it('should render a cancel button', () => { + it('should render a cancel button', async () => { + const { wrapper } = await setup() 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() + describe('errors', async () => { const error = { message: 'some message', patient: 'some patient message', type: 'some type error', } + const store = mockStore({ lab: { status: 'error', error } } as any) + const { wrapper } = await setup(store) - beforeEach(() => { - history.push('/labs/new') - const store = mockStore({ title: '', lab: { status: 'error', error } } as any) - wrapper = mount( - - - - - , - ) - }) - - it('should display errors', () => { + it('should display errors', async () => { const alert = wrapper.find(Alert) const typeInput = wrapper.find(TextInputWithLabelFormGroup) const patientTypeahead = wrapper.find(Typeahead) @@ -144,21 +123,10 @@ describe('New Lab Request', () => { }) }) - describe('on cancel', () => { - let wrapper: ReactWrapper + describe('on cancel', async () => { const history = createMemoryHistory() - beforeEach(() => { - history.push('/labs/new') - const store = mockStore({ title: '', lab: { status: 'loading', error: {} } } as any) - wrapper = mount( - - - - - , - ) - }) + const { wrapper } = await setup() it('should navigate back to /labs', () => { const cancelButton = wrapper.find(Button).at(1) @@ -172,20 +140,24 @@ describe('New Lab Request', () => { }) }) - describe('on save', () => { - let wrapper: ReactWrapper + describe('on save', async () => { const history = createMemoryHistory() let labRepositorySaveSpy: any const expectedDate = new Date() + const expectedNotes = 'expected notes' const expectedLab = { patient: '12345', type: 'expected type', status: 'requested', - notes: 'expected notes', + notes: [expectedNotes], id: '1234', requestedOn: expectedDate.toISOString(), } as Lab - + const store = mockStore({ + lab: { status: 'loading', error: {} }, + user: { user: { id: 'fake id' } }, + } as any) + const { wrapper } = await setup(store) beforeEach(() => { jest.resetAllMocks() Date.now = jest.fn(() => expectedDate.valueOf()) @@ -194,20 +166,6 @@ describe('New Lab Request', () => { jest .spyOn(PatientRepository, 'search') .mockResolvedValue([{ id: expectedLab.patient, fullName: 'some full name' }] as Patient[]) - - history.push('/labs/new') - const store = mockStore({ - title: '', - lab: { status: 'loading', error: {} }, - user: { user: { id: 'fake id' } }, - } as any) - wrapper = mount( - - - - - , - ) }) it('should save the lab request and navigate to "/labs/:id"', async () => { @@ -226,7 +184,7 @@ describe('New Lab Request', () => { const notesTextField = wrapper.find(TextFieldWithLabelFormGroup) act(() => { const onChange = notesTextField.prop('onChange') as any - onChange({ currentTarget: { value: expectedLab.notes } }) + onChange({ currentTarget: { value: expectedNotes } }) }) wrapper.update() diff --git a/src/__tests__/login/Login.test.tsx b/src/__tests__/login/Login.test.tsx new file mode 100644 index 0000000000..df6e098b53 --- /dev/null +++ b/src/__tests__/login/Login.test.tsx @@ -0,0 +1,314 @@ +import { Alert } from '@hospitalrun/components' +import { mount, ReactWrapper } from 'enzyme' +import { createMemoryHistory } from 'history' +import React from 'react' +import Button from 'react-bootstrap/Button' +import { act } from 'react-dom/test-utils' +import { Provider } from 'react-redux' +import { Router } from 'react-router' +import createMockStore, { MockStore } from 'redux-mock-store' +import thunk from 'redux-thunk' + +import Login from '../../login/Login' +import TextInputWithLabelFormGroup from '../../shared/components/input/TextInputWithLabelFormGroup' +import { remoteDb } from '../../shared/config/pouchdb' +import User from '../../shared/model/User' +import { RootState } from '../../shared/store' + +const mockStore = createMockStore([thunk]) + +describe('Login', () => { + const history = createMemoryHistory() + let store: MockStore + + const setup = (storeValue: any = { loginError: {}, user: {} as User }) => { + history.push('/login') + store = mockStore(storeValue) + + const wrapper = mount( + + + + + , + ) + + wrapper.update() + return wrapper + } + + describe('Layout initial validations', () => { + it('should render a login form', async () => { + let wrapper: any + + await act(async () => { + wrapper = await setup() + }) + + const form = wrapper.find('form') + expect(form).toHaveLength(1) + }) + + it('should render a username and password input', async () => { + let wrapper: any + + await act(async () => { + wrapper = setup() + }) + + const user = wrapper.find(TextInputWithLabelFormGroup) + expect(user).toHaveLength(2) + }) + + it('should render a submit button', async () => { + let wrapper: any + await act(async () => { + wrapper = setup() + }) + + const button = wrapper.find(Button) + expect(button).toHaveLength(1) + }) + }) + + describe('Unable to login', () => { + it('should get field required error message if no username is provided', async () => { + let wrapper: any + + await act(async () => { + wrapper = await setup() + }) + + jest.spyOn(remoteDb, 'logIn') + + const password = wrapper.find('#passwordTextInput').at(0) + await act(async () => { + const onChange = password.prop('onChange') as any + await onChange({ currentTarget: { value: 'password' } }) + }) + + wrapper.update() + + const saveButton = wrapper.find({ type: 'submit' }).at(0) + + await act(async () => { + const onClick = saveButton.prop('onClick') as any + await onClick({ preventDefault: jest.fn() }) + }) + + wrapper.update() + + expect(remoteDb.logIn).toHaveBeenCalledWith('', 'password') + expect(history.location.pathname).toEqual('/login') + expect(store.getActions()).toContainEqual({ + type: 'user/loginError', + payload: { + message: 'user.login.error.message.required', + username: 'user.login.error.username.required', + password: 'user.login.error.password.required', + }, + }) + }) + + it('should show required username error message', async () => { + let wrapper: any + + await act(async () => { + wrapper = await setup({ + user: { + loginError: { + message: 'user.login.error.message.required', + username: 'user.login.error.username.required', + password: 'user.login.error.password.required', + }, + }, + } as any) + }) + + let password: ReactWrapper = wrapper.find('#passwordTextInput').at(0) + await act(async () => { + const onChange = password.prop('onChange') as any + await onChange({ currentTarget: { value: 'password' } }) + }) + + wrapper.update() + + const alert = wrapper.find(Alert) + const username = wrapper.find('#usernameTextInput') + password = wrapper.find('#passwordTextInput') + + const usernameFeedback = username.find('Feedback') + const passwordFeedback = password.find('Feedback') + + expect(alert.prop('message')).toEqual('user.login.error.message.required') + expect(usernameFeedback.hasClass('undefined')).not.toBeTruthy() + expect(passwordFeedback.hasClass('undefined')).toBeTruthy() + }) + + it('should get field required error message if no password is provided', async () => { + let wrapper: any + + await act(async () => { + wrapper = await setup() + }) + + jest.spyOn(remoteDb, 'logIn') + + const username = wrapper.find('#usernameTextInput').at(0) + await act(async () => { + const onChange = username.prop('onChange') as any + await onChange({ currentTarget: { value: 'username' } }) + }) + + wrapper.update() + + const saveButton = wrapper.find({ type: 'submit' }).at(0) + + await act(async () => { + const onClick = saveButton.prop('onClick') as any + await onClick({ preventDefault: jest.fn() }) + }) + + wrapper.update() + + expect(remoteDb.logIn).toHaveBeenCalledWith('username', '') + expect(history.location.pathname).toEqual('/login') + expect(store.getActions()).toContainEqual({ + type: 'user/loginError', + payload: { + message: 'user.login.error.message.required', + username: 'user.login.error.username.required', + password: 'user.login.error.password.required', + }, + }) + }) + + it('should show required password error message', async () => { + let wrapper: any + + await act(async () => { + wrapper = await setup({ + user: { + loginError: { + message: 'user.login.error.message.required', + username: 'user.login.error.username.required', + password: 'user.login.error.password.required', + }, + }, + } as any) + }) + + let username: ReactWrapper = wrapper.find('#usernameTextInput').at(0) + await act(async () => { + const onChange = username.prop('onChange') as any + await onChange({ currentTarget: { value: 'username' } }) + }) + + wrapper.update() + + const alert = wrapper.find(Alert) + const password = wrapper.find('#passwordTextInput').at(0) + username = wrapper.find('#usernameTextInput').at(0) + + const passwordFeedback = password.find('Feedback') + const usernameFeedback = username.find('Feedback') + + expect(alert.prop('message')).toEqual('user.login.error.message.required') + expect(passwordFeedback.hasClass('undefined')).not.toBeTruthy() + expect(usernameFeedback.hasClass('undefined')).toBeTruthy() + }) + + it('should get incorrect username or password error when incorrect username or password is provided', async () => { + let wrapper: any + + await act(async () => { + wrapper = await setup() + }) + + jest.spyOn(remoteDb, 'logIn').mockRejectedValue({ status: 401 }) + + const username = wrapper.find('#usernameTextInput').at(0) + await act(async () => { + const onChange = username.prop('onChange') as any + await onChange({ currentTarget: { value: 'username' } }) + }) + + const password = wrapper.find('#passwordTextInput').at(0) + await act(async () => { + const onChange = password.prop('onChange') as any + await onChange({ currentTarget: { value: 'password' } }) + }) + + wrapper.update() + + const saveButton = wrapper.find({ type: 'submit' }).at(0) + await act(async () => { + const onClick = saveButton.prop('onClick') as any + await onClick({ preventDefault: jest.fn() }) + }) + + wrapper.update() + + expect(remoteDb.logIn).toHaveBeenCalledWith('username', 'password') + expect(history.location.pathname).toEqual('/login') + expect(store.getActions()).toContainEqual({ + type: 'user/loginError', + payload: { + message: 'user.login.error.message.incorrect', + }, + }) + }) + }) + + describe('Sucessfully login', () => { + it('should log in if username and password is correct', async () => { + let wrapper: any + + await act(async () => { + wrapper = await setup() + }) + + jest.spyOn(remoteDb, 'logIn').mockResolvedValue({ + name: 'username', + ok: true, + roles: [], + }) + + jest.spyOn(remoteDb, 'getUser').mockResolvedValue({ + _id: 'userId', + metadata: { + givenName: 'test', + familyName: 'user', + }, + } as any) + + const username = wrapper.find('#usernameTextInput').at(0) + await act(async () => { + const onChange = username.prop('onChange') as any + await onChange({ currentTarget: { value: 'username' } }) + }) + + const password = wrapper.find('#passwordTextInput').at(0) + await act(async () => { + const onChange = password.prop('onChange') as any + await onChange({ currentTarget: { value: 'password' } }) + }) + + const saveButton = wrapper.find({ type: 'submit' }).at(0) + + await act(async () => { + const onClick = saveButton.prop('onClick') as any + await onClick({ preventDefault: jest.fn() }) + }) + + wrapper.update() + + expect(store.getActions()[0].payload.user).toEqual({ + id: 'userId', + givenName: 'test', + familyName: 'user', + }) + expect(store.getActions()[0].type).toEqual('user/loginSuccess') + }) + }) +}) diff --git a/src/__tests__/medications/Medications.test.tsx b/src/__tests__/medications/Medications.test.tsx index eb52b6ce2b..8bacbf0f05 100644 --- a/src/__tests__/medications/Medications.test.tsx +++ b/src/__tests__/medications/Medications.test.tsx @@ -9,6 +9,7 @@ import thunk from 'redux-thunk' import Medications from '../../medications/Medications' import NewMedicationRequest from '../../medications/requests/NewMedicationRequest' import ViewMedication from '../../medications/ViewMedication' +import { TitleProvider } from '../../page-header/title/TitleContext' import MedicationRepository from '../../shared/db/MedicationRepository' import PatientRepository from '../../shared/db/PatientRepository' import Medication from '../../shared/model/Medication' @@ -21,7 +22,7 @@ const mockStore = createMockStore([thunk]) describe('Medications', () => { const setup = (route: string, permissions: Array) => { jest.resetAllMocks() - jest.spyOn(MedicationRepository, 'findAll').mockResolvedValue([]) + jest.spyOn(MedicationRepository, 'search').mockResolvedValue([]) jest .spyOn(MedicationRepository, 'find') .mockResolvedValue({ id: '1234', requestedOn: new Date().toISOString() } as Medication) @@ -54,7 +55,9 @@ describe('Medications', () => { const wrapper = mount( - + + + , ) diff --git a/src/__tests__/medications/ViewMedication.test.tsx b/src/__tests__/medications/ViewMedication.test.tsx index 399d51ef8a..65ccdf8bd5 100644 --- a/src/__tests__/medications/ViewMedication.test.tsx +++ b/src/__tests__/medications/ViewMedication.test.tsx @@ -11,7 +11,7 @@ import thunk from 'redux-thunk' import ViewMedication from '../../medications/ViewMedication' import * as ButtonBarProvider from '../../page-header/button-toolbar/ButtonBarProvider' -import * as titleUtil from '../../page-header/title/useTitle' +import * as titleUtil from '../../page-header/title/TitleContext' import TextFieldWithLabelFormGroup from '../../shared/components/input/TextFieldWithLabelFormGroup' import MedicationRepository from '../../shared/db/MedicationRepository' import PatientRepository from '../../shared/db/PatientRepository' @@ -42,7 +42,7 @@ describe('View Medication', () => { jest.resetAllMocks() Date.now = jest.fn(() => expectedDate.valueOf()) const setButtonToolBarSpy = jest.fn() - const titleSpy = jest.spyOn(titleUtil, 'default') + jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter').mockReturnValue(setButtonToolBarSpy) jest.spyOn(MedicationRepository, 'find').mockResolvedValue(medication) const medicationRepositorySaveSpy = jest @@ -72,37 +72,29 @@ describe('View Medication', () => { - + + + , ) }) + wrapper.find(ViewMedication).props().updateTitle = jest.fn() wrapper.update() - return [ + return { wrapper, mockPatient, - { ...mockMedication, ...medication }, - titleSpy, + expectedMedication: { ...mockMedication, ...medication }, medicationRepositorySaveSpy, history, - ] + } } - it('should set the title', async () => { - const [, mockPatient, mockMedication, titleSpy] = await setup({} as Medication, [ - Permissions.ViewMedication, - ]) - - expect(titleSpy).toHaveBeenCalledWith( - `${mockMedication.medication} for ${mockPatient.fullName}`, - ) - }) - describe('page content', () => { it('should display the patient full name for the for', async () => { - const [wrapper, mockPatient] = await setup({} as Medication, [Permissions.ViewMedication]) + const { wrapper, mockPatient } = await setup({} as Medication, [Permissions.ViewMedication]) const forPatientDiv = wrapper.find('.for-patient') expect(forPatientDiv.find('h4').text().trim()).toEqual('medications.medication.for') @@ -110,7 +102,7 @@ describe('View Medication', () => { }) it('should display the medication ', async () => { - const [wrapper, , expectedMedication] = await setup({} as Medication, [ + const { wrapper, expectedMedication } = await setup({} as Medication, [ Permissions.ViewMedication, ]) const medicationTypeDiv = wrapper.find('.medication-medication') @@ -122,7 +114,7 @@ describe('View Medication', () => { }) it('should display the requested on date', async () => { - const [wrapper, , expectedMedication] = await setup({} as Medication, [ + const { wrapper, expectedMedication } = await setup({} as Medication, [ Permissions.ViewMedication, ]) const requestedOnDiv = wrapper.find('.requested-on') @@ -134,14 +126,14 @@ describe('View Medication', () => { }) it('should not display the canceled date if the medication is not canceled', async () => { - const [wrapper] = await setup({} as Medication, [Permissions.ViewMedication]) + const { wrapper } = await setup({} as Medication, [Permissions.ViewMedication]) const completedOnDiv = wrapper.find('.canceled-on') expect(completedOnDiv).toHaveLength(0) }) it('should display the notes in the notes text field', async () => { - const [wrapper, , expectedMedication] = await setup({} as Medication, [ + const { wrapper, expectedMedication } = await setup({} as Medication, [ Permissions.ViewMedication, ]) @@ -154,7 +146,7 @@ describe('View Medication', () => { describe('draft medication request', () => { it('should display a warning badge if the status is draft', async () => { - const [wrapper, , expectedMedication] = await setup({} as Medication, [ + const { wrapper, expectedMedication } = await setup({} as Medication, [ Permissions.ViewMedication, ]) const medicationStatusDiv = wrapper.find('.medication-status') @@ -168,7 +160,7 @@ describe('View Medication', () => { }) it('should display a update medication and cancel medication button if the medication is in a draft state', async () => { - const [wrapper] = await setup({} as Medication, [ + const { wrapper } = await setup({} as Medication, [ Permissions.ViewMedication, Permissions.CompleteMedication, Permissions.CancelMedication, @@ -183,7 +175,7 @@ describe('View Medication', () => { describe('canceled medication request', () => { it('should display a danger badge if the status is canceled', async () => { - const [wrapper, , expectedMedication] = await setup({ status: 'canceled' } as Medication, [ + const { wrapper, expectedMedication } = await setup({ status: 'canceled' } as Medication, [ Permissions.ViewMedication, ]) @@ -198,7 +190,7 @@ describe('View Medication', () => { }) it('should display the canceled on date if the medication request has been canceled', async () => { - const [wrapper, , expectedMedication] = await setup( + const { wrapper, expectedMedication } = await setup( { status: 'canceled', canceledOn: '2020-03-30T04:45:20.102Z', @@ -215,7 +207,7 @@ describe('View Medication', () => { }) it('should not display update and cancel button if the medication is canceled', async () => { - const [wrapper] = await setup( + const { wrapper } = await setup( { status: 'canceled', } as Medication, @@ -227,7 +219,7 @@ describe('View Medication', () => { }) it('should not display an update button if the medication is canceled', async () => { - const [wrapper] = await setup({ status: 'canceled' } as Medication, [ + const { wrapper } = await setup({ status: 'canceled' } as Medication, [ Permissions.ViewMedication, ]) @@ -239,9 +231,10 @@ describe('View Medication', () => { describe('on update', () => { it('should update the medication with the new information', async () => { - const [wrapper, , mockMedication, , medicationRepositorySaveSpy, history] = await setup({}, [ - Permissions.ViewMedication, - ]) + const { wrapper, expectedMedication, medicationRepositorySaveSpy, history } = await setup( + {}, + [Permissions.ViewMedication], + ) const expectedNotes = 'expected notes' const notesTextField = wrapper.find(TextFieldWithLabelFormGroup).at(0) @@ -256,10 +249,10 @@ describe('View Medication', () => { onClick() }) - expect(medicationRepositorySaveSpy).toHaveBeenCalledTimes(1) + expect(medicationRepositorySaveSpy).toHaveBeenCalled() expect(medicationRepositorySaveSpy).toHaveBeenCalledWith( expect.objectContaining({ - ...mockMedication, + ...expectedMedication, notes: expectedNotes, }), ) @@ -269,11 +262,10 @@ describe('View Medication', () => { describe('on cancel', () => { it('should mark the status as canceled and fill in the cancelled on date with the current time', async () => { - const [wrapper, , mockMedication, , medicationRepositorySaveSpy, history] = await setup({}, [ - Permissions.ViewMedication, - Permissions.CompleteMedication, - Permissions.CancelMedication, - ]) + const { wrapper, expectedMedication, medicationRepositorySaveSpy, history } = await setup( + {}, + [Permissions.ViewMedication, Permissions.CompleteMedication, Permissions.CancelMedication], + ) const cancelButton = wrapper.find(Button).at(1) await act(async () => { @@ -282,10 +274,10 @@ describe('View Medication', () => { }) wrapper.update() - expect(medicationRepositorySaveSpy).toHaveBeenCalledTimes(1) + expect(medicationRepositorySaveSpy).toHaveBeenCalled() expect(medicationRepositorySaveSpy).toHaveBeenCalledWith( expect.objectContaining({ - ...mockMedication, + ...expectedMedication, status: 'canceled', canceledOn: expectedDate.toISOString(), }), diff --git a/src/__tests__/medications/ViewMedications.test.tsx b/src/__tests__/medications/ViewMedications.test.tsx deleted file mode 100644 index 96c9c7d524..0000000000 --- a/src/__tests__/medications/ViewMedications.test.tsx +++ /dev/null @@ -1,190 +0,0 @@ -import { TextInput, Select, Table } from '@hospitalrun/components' -import { act } from '@testing-library/react' -import { mount } 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 * as medicationsSlice from '../../medications/medications-slice' -import ViewMedications from '../../medications/ViewMedications' -import * as ButtonBarProvider from '../../page-header/button-toolbar/ButtonBarProvider' -import * as titleUtil from '../../page-header/title/useTitle' -import MedicationRepository from '../../shared/db/MedicationRepository' -import Medication from '../../shared/model/Medication' -import Permissions from '../../shared/model/Permissions' -import { RootState } from '../../shared/store' - -const mockStore = createMockStore([thunk]) - -describe('View Medications', () => { - const setup = async (medication: Medication, permissions: Permissions[]) => { - let wrapper: any - const expectedMedication = ({ - id: '1234', - medication: 'medication', - patient: 'patientId', - status: 'draft', - intent: 'order', - priority: 'routine', - quantity: { value: 1, unit: 'unit' }, - requestedOn: '2020-03-30T04:43:20.102Z', - } as unknown) as Medication - const history = createMemoryHistory() - const store = mockStore({ - title: '', - user: { permissions }, - medications: { medications: [{ ...expectedMedication, ...medication }] }, - } as any) - const titleSpy = jest.spyOn(titleUtil, 'default') - const setButtonToolBarSpy = jest.fn() - const searchMedicationsSpy = jest.spyOn(medicationsSlice, 'searchMedications') - jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter').mockReturnValue(setButtonToolBarSpy) - jest.spyOn(MedicationRepository, 'findAll').mockResolvedValue([]) - - await act(async () => { - wrapper = await mount( - - - - - , - ) - }) - wrapper.update() - return [ - wrapper, - titleSpy, - setButtonToolBarSpy, - { ...expectedMedication, ...medication }, - history, - searchMedicationsSpy, - ] - } - - describe('title', () => { - it('should have the title', async () => { - const permissions: never[] = [] - const [, titleSpy] = await setup({} as Medication, permissions) - expect(titleSpy).toHaveBeenCalledWith('medications.label') - }) - }) - - describe('button bar', () => { - it('should display button to add new medication request', async () => { - const permissions = [Permissions.ViewMedications, Permissions.RequestMedication] - const [, , setButtonToolBarSpy] = await setup({} as Medication, permissions) - - const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0] - expect((actualButtons[0] as any).props.children).toEqual('medications.requests.new') - }) - - it('should not display button to add new medication request if the user does not have permissions', async () => { - const permissions: never[] = [] - const [, , setButtonToolBarSpy] = await setup({} as Medication, permissions) - - const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0] - expect(actualButtons).toEqual([]) - }) - }) - - describe('table', () => { - it('should render a table with data', async () => { - const permissions = [Permissions.ViewMedications] - const [wrapper, , , expectedMedication] = await setup({} as Medication, permissions) - const table = wrapper.find(Table) - const columns = table.prop('columns') - const actions = table.prop('actions') as any - expect(columns[0]).toEqual( - expect.objectContaining({ label: 'medications.medication.medication', key: 'medication' }), - ) - expect(columns[1]).toEqual( - expect.objectContaining({ label: 'medications.medication.priority', key: 'priority' }), - ) - expect(columns[2]).toEqual( - expect.objectContaining({ label: 'medications.medication.intent', key: 'intent' }), - ) - expect(columns[3]).toEqual( - expect.objectContaining({ - label: 'medications.medication.requestedOn', - key: 'requestedOn', - }), - ) - expect(columns[4]).toEqual( - expect.objectContaining({ label: 'medications.medication.status', key: 'status' }), - ) - - expect(actions[0]).toEqual(expect.objectContaining({ label: 'actions.view' })) - expect(table.prop('actionsHeaderText')).toEqual('actions.label') - expect(table.prop('data')).toEqual([expectedMedication]) - }) - - it('should navigate to the medication when the view button is clicked', async () => { - const permissions = [Permissions.ViewMedications] - const [wrapper, , , expectedMedication, history] = await setup({} as Medication, permissions) - const tr = wrapper.find('tr').at(1) - - act(() => { - const onClick = tr.find('button').prop('onClick') as any - onClick({ stopPropagation: jest.fn() }) - }) - expect(history.location.pathname).toEqual(`/medications/${expectedMedication.id}`) - }) - }) - - describe('dropdown', () => { - it('should search for medications when dropdown changes', async () => { - const permissions = [Permissions.ViewMedications] - const [wrapper, , , , , searchMedicationsSpy] = await setup({} as Medication, permissions) - - searchMedicationsSpy.mockClear() - - act(() => { - const onChange = wrapper.find(Select).prop('onChange') as any - onChange({ - target: { - value: 'draft', - }, - preventDefault: jest.fn(), - }) - }) - - wrapper.update() - expect(searchMedicationsSpy).toHaveBeenCalledTimes(1) - }) - }) - - describe('search functionality', () => { - beforeEach(() => jest.useFakeTimers()) - - afterEach(() => jest.useRealTimers()) - - it('should search for medications after the search text has not changed for 500 milliseconds', async () => { - const permissions = [Permissions.ViewMedications] - const [wrapper, , , , , searchMedicationsSpy] = await setup({} as Medication, permissions) - - searchMedicationsSpy.mockClear() - const expectedSearchText = 'search text' - - act(() => { - const onClick = wrapper.find(TextInput).at(0).prop('onChange') as any - onClick({ - target: { - value: expectedSearchText, - }, - preventDefault: jest.fn(), - }) - }) - - act(() => { - jest.advanceTimersByTime(500) - }) - - wrapper.update() - - expect(searchMedicationsSpy).toHaveBeenCalledTimes(1) - }) - }) -}) diff --git a/src/__tests__/medications/hooks/useMedicationSearch.test.tsx b/src/__tests__/medications/hooks/useMedicationSearch.test.tsx new file mode 100644 index 0000000000..570119e022 --- /dev/null +++ b/src/__tests__/medications/hooks/useMedicationSearch.test.tsx @@ -0,0 +1,43 @@ +import { act, renderHook } from '@testing-library/react-hooks' + +import useMedicationSearch from '../../../medications/hooks/useMedicationSearch' +import MedicationSearchRequest from '../../../medications/models/MedicationSearchRequest' +import MedicationRepository from '../../../shared/db/MedicationRepository' +import SortRequest from '../../../shared/db/SortRequest' +import Medication from '../../../shared/model/Medication' +import waitUntilQueryIsSuccessful from '../../test-utils/wait-for-query.util' + +const defaultSortRequest: SortRequest = { + sorts: [ + { + field: 'requestedOn', + direction: 'desc', + }, + ], +} + +describe('useMedicationSearch', () => { + it('it should search medication requests', async () => { + const expectedSearchRequest: MedicationSearchRequest = { + status: 'all', + text: 'some search request', + } + const expectedMedicationRequests = [{ id: 'some id' }] as Medication[] + jest.spyOn(MedicationRepository, 'search').mockResolvedValue(expectedMedicationRequests) + + let actualData: any + await act(async () => { + const renderHookResult = renderHook(() => useMedicationSearch(expectedSearchRequest)) + const { result } = renderHookResult + await waitUntilQueryIsSuccessful(renderHookResult) + actualData = result.current.data + }) + + expect(MedicationRepository.search).toHaveBeenCalledTimes(1) + expect(MedicationRepository.search).toBeCalledWith({ + ...expectedSearchRequest, + defaultSortRequest, + }) + expect(actualData).toEqual(expectedMedicationRequests) + }) +}) diff --git a/src/__tests__/medications/medications-slice.test.ts b/src/__tests__/medications/medications-slice.test.ts deleted file mode 100644 index 9e7359c673..0000000000 --- a/src/__tests__/medications/medications-slice.test.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { AnyAction } from 'redux' -import { mocked } from 'ts-jest/utils' - -import medications, { - fetchMedicationsStart, - fetchMedicationsSuccess, - searchMedications, -} from '../../medications/medications-slice' -import MedicationRepository from '../../shared/db/MedicationRepository' -import SortRequest from '../../shared/db/SortRequest' -import Medication from '../../shared/model/Medication' - -interface SearchContainer { - text: string - status: 'draft' | 'active' | 'completed' | 'canceled' | 'all' - defaultSortRequest: SortRequest -} - -const defaultSortRequest: SortRequest = { - sorts: [ - { - field: 'requestedOn', - direction: 'desc', - }, - ], -} - -const expectedSearchObject: SearchContainer = { - text: 'search string', - status: 'all', - defaultSortRequest, -} - -describe('medications slice', () => { - const setup = (medicationSpyOn: string) => { - const dispatch = jest.fn() - const getState = jest.fn() - jest.spyOn(MedicationRepository, medicationSpyOn as any) - return [dispatch, getState] - } - - beforeEach(() => { - jest.resetAllMocks() - }) - - describe('medications reducer', () => { - it('should create the proper initial state with empty medications array', () => { - const medicationsStore = medications(undefined, {} as AnyAction) - expect(medicationsStore.isLoading).toBeFalsy() - expect(medicationsStore.medications).toHaveLength(0) - expect(medicationsStore.statusFilter).toEqual('all') - }) - - it('it should handle the FETCH_MEDICATIONS_SUCCESS action', () => { - const expectedMedications = [{ id: '1234' }] - const medicationsStore = medications(undefined, { - type: fetchMedicationsSuccess.type, - payload: expectedMedications, - }) - - expect(medicationsStore.isLoading).toBeFalsy() - expect(medicationsStore.medications).toEqual(expectedMedications) - }) - }) - - describe('searchMedications', () => { - it('should dispatch the FETCH_MEDICATIONS_START action', async () => { - const [dispatch, getState] = setup('search') - - await searchMedications('search string', 'all')(dispatch, getState, null) - - expect(dispatch).toHaveBeenCalledWith({ type: fetchMedicationsStart.type }) - }) - - it('should call the MedicationRepository search method with the correct search criteria', async () => { - const [dispatch, getState] = setup('search') - jest.spyOn(MedicationRepository, 'search') - - await searchMedications(expectedSearchObject.text, expectedSearchObject.status)( - dispatch, - getState, - null, - ) - - expect(MedicationRepository.search).toHaveBeenCalledWith(expectedSearchObject) - }) - - it('should call the MedicationRepository findAll method if there is no string text and status is set to all', async () => { - const [dispatch, getState] = setup('findAll') - - await searchMedications('', expectedSearchObject.status)(dispatch, getState, null) - - expect(MedicationRepository.findAll).toHaveBeenCalledTimes(1) - }) - - it('should dispatch the FETCH_MEDICATIONS_SUCCESS action', async () => { - const [dispatch, getState] = setup('findAll') - - const expectedMedications = [ - { - medication: 'text', - }, - ] as Medication[] - - const mockedMedicationRepository = mocked(MedicationRepository, true) - mockedMedicationRepository.search.mockResolvedValue(expectedMedications) - - await searchMedications(expectedSearchObject.text, expectedSearchObject.status)( - dispatch, - getState, - null, - ) - - expect(dispatch).toHaveBeenLastCalledWith({ - type: fetchMedicationsSuccess.type, - payload: expectedMedications, - }) - }) - }) - - describe('sort Request', () => { - it('should have called findAll with sort request in searchMedications method', async () => { - const [dispatch, getState] = setup('findAll') - - await searchMedications('', expectedSearchObject.status)(dispatch, getState, null) - - expect(MedicationRepository.findAll).toHaveBeenCalledWith( - expectedSearchObject.defaultSortRequest, - ) - }) - - it('should include sorts in the search criteria', async () => { - const [dispatch, getState] = setup('search') - - await searchMedications(expectedSearchObject.text, expectedSearchObject.status)( - dispatch, - getState, - null, - ) - - expect(MedicationRepository.search).toHaveBeenCalledWith(expectedSearchObject) - }) - }) -}) diff --git a/src/__tests__/medications/requests/NewMedicationRequest.test.tsx b/src/__tests__/medications/requests/NewMedicationRequest.test.tsx index a4067013ea..48805bd208 100644 --- a/src/__tests__/medications/requests/NewMedicationRequest.test.tsx +++ b/src/__tests__/medications/requests/NewMedicationRequest.test.tsx @@ -9,7 +9,7 @@ import createMockStore from 'redux-mock-store' import thunk from 'redux-thunk' import NewMedicationRequest from '../../../medications/requests/NewMedicationRequest' -import * as titleUtil from '../../../page-header/title/useTitle' +import * as titleUtil from '../../../page-header/title/TitleContext' import TextFieldWithLabelFormGroup from '../../../shared/components/input/TextFieldWithLabelFormGroup' import TextInputWithLabelFormGroup from '../../../shared/components/input/TextInputWithLabelFormGroup' import MedicationRepository from '../../../shared/db/MedicationRepository' @@ -21,47 +21,36 @@ import { RootState } from '../../../shared/store' const mockStore = createMockStore([thunk]) describe('New Medication Request', () => { - describe('title and breadcrumbs', () => { - let titleSpy: any + const setup = async ( + store = mockStore({ medication: { status: 'loading', error: {} } } as any), + ) => { const history = createMemoryHistory() + history.push(`/medications/new`) + jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) - beforeEach(() => { - const store = mockStore({ title: '', medication: { status: 'loading', error: {} } } as any) - titleSpy = jest.spyOn(titleUtil, 'default') - history.push('/medications/new') - - mount( - - + const wrapper: ReactWrapper = await mount( + + + - - , - ) - }) + + + , + ) - it('should have New Medication Request as the title', () => { - expect(titleSpy).toHaveBeenCalledWith('medications.requests.new') - }) - }) + wrapper.find(NewMedicationRequest).props().updateTitle = jest.fn() + wrapper.update() + return { wrapper } + } describe('form layout', () => { - let wrapper: ReactWrapper - const history = createMemoryHistory() - - beforeEach(() => { - const store = mockStore({ title: '', medication: { status: 'loading', error: {} } } as any) - history.push('/medications/new') - - wrapper = mount( - - - - - , - ) + it('should have called the useUpdateTitle hook', async () => { + await setup() + expect(titleUtil.useUpdateTitle).toHaveBeenCalledTimes(1) }) - it('should render a patient typeahead', () => { + it('should render a patient typeahead', async () => { + const { wrapper } = await setup() const typeaheadDiv = wrapper.find('.patient-typeahead') expect(typeaheadDiv).toBeDefined() @@ -76,7 +65,8 @@ describe('New Medication Request', () => { expect(typeahead.prop('searchAccessor')).toEqual('fullName') }) - it('should render a medication input box', () => { + it('should render a medication input box', async () => { + const { wrapper } = await setup() const typeInputBox = wrapper.find(TextInputWithLabelFormGroup).at(0) expect(typeInputBox).toBeDefined() @@ -85,7 +75,8 @@ describe('New Medication Request', () => { expect(typeInputBox.prop('isEditable')).toBeTruthy() }) - it('should render a notes text field', () => { + it('should render a notes text field', async () => { + const { wrapper } = await setup() const notesTextField = wrapper.find(TextFieldWithLabelFormGroup) expect(notesTextField).toBeDefined() @@ -94,34 +85,24 @@ describe('New Medication Request', () => { expect(notesTextField.prop('isEditable')).toBeTruthy() }) - it('should render a save button', () => { + it('should render a save button', async () => { + const { wrapper } = await setup() 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() const cancelButton = wrapper.find(Button).at(1) expect(cancelButton).toBeDefined() expect(cancelButton.text().trim()).toEqual('actions.cancel') }) }) - describe('on cancel', () => { - let wrapper: ReactWrapper + describe('on cancel', async () => { const history = createMemoryHistory() - - beforeEach(() => { - history.push('/medications/new') - const store = mockStore({ title: '', medication: { status: 'loading', error: {} } } as any) - wrapper = mount( - - - - - , - ) - }) + const { wrapper } = await setup() it('should navigate back to /medications', () => { const cancelButton = wrapper.find(Button).at(1) @@ -135,8 +116,7 @@ describe('New Medication Request', () => { }) }) - describe('on save', () => { - let wrapper: ReactWrapper + describe('on save', async () => { const history = createMemoryHistory() let medicationRepositorySaveSpy: any const expectedDate = new Date() @@ -148,7 +128,11 @@ describe('New Medication Request', () => { id: '1234', requestedOn: expectedDate.toISOString(), } as Medication - + const store = mockStore({ + medication: { status: 'loading', error: {} }, + user: { user: { id: 'fake id' } }, + } as any) + const { wrapper } = await setup(store) beforeEach(() => { jest.resetAllMocks() Date.now = jest.fn(() => expectedDate.valueOf()) @@ -161,20 +145,6 @@ describe('New Medication Request', () => { .mockResolvedValue([ { id: expectedMedication.patient, fullName: 'some full name' }, ] as Patient[]) - - history.push('/medications/new') - const store = mockStore({ - title: '', - medication: { status: 'loading', error: {} }, - user: { user: { id: 'fake id' } }, - } as any) - wrapper = mount( - - - - - , - ) }) it('should save the medication request and navigate to "/medications/:id"', async () => { diff --git a/src/__tests__/medications/search/MedicationRequestSearch.test.tsx b/src/__tests__/medications/search/MedicationRequestSearch.test.tsx new file mode 100644 index 0000000000..981d7358ce --- /dev/null +++ b/src/__tests__/medications/search/MedicationRequestSearch.test.tsx @@ -0,0 +1,87 @@ +import { mount, ReactWrapper } from 'enzyme' +import React from 'react' +import { act } from 'react-dom/test-utils' + +import MedicationSearchRequest from '../../../medications/models/MedicationSearchRequest' +import MedicationRequestSearch from '../../../medications/search/MedicationRequestSearch' +import SelectWithLabelFormGroup from '../../../shared/components/input/SelectWithLabelFormGroup' +import TextInputWithLabelFormGroup from '../../../shared/components/input/TextInputWithLabelFormGroup' + +describe('Medication Request Search', () => { + const setup = (givenSearchRequest: MedicationSearchRequest = { text: '', status: 'draft' }) => { + const onChangeSpy = jest.fn() + + const wrapper = mount( + , + ) + wrapper.update() + + return { wrapper: wrapper as ReactWrapper, onChangeSpy } + } + + it('should render a select component with the default value', () => { + const expectedSearchRequest: MedicationSearchRequest = { text: '', status: 'draft' } + const { wrapper } = setup(expectedSearchRequest) + + const select = wrapper.find(SelectWithLabelFormGroup) + expect(select.prop('label')).toEqual('medications.filterTitle') + expect(select.prop('options')).toEqual([ + { label: 'medications.filter.all', value: 'all' }, + { label: 'medications.status.draft', value: 'draft' }, + { label: 'medications.status.active', value: 'active' }, + { label: 'medications.status.onHold', value: 'on hold' }, + { label: 'medications.status.completed', value: 'completed' }, + { label: 'medications.status.enteredInError', value: 'entered in error' }, + { label: 'medications.status.canceled', value: 'canceled' }, + { label: 'medications.status.unknown', value: 'unknown' }, + ]) + expect(select.prop('defaultSelected')).toEqual([ + { + label: 'medications.status.draft', + value: 'draft', + }, + ]) + expect(select.prop('isEditable')).toBeTruthy() + }) + + it('should update the search request when the filter updates', () => { + const expectedSearchRequest: MedicationSearchRequest = { text: '', status: 'draft' } + const expectedNewValue = 'canceled' + const { wrapper, onChangeSpy } = setup(expectedSearchRequest) + + act(() => { + const select = wrapper.find(SelectWithLabelFormGroup) + const onChange = select.prop('onChange') as any + onChange([expectedNewValue]) + }) + + expect(onChangeSpy).toHaveBeenCalledTimes(1) + expect(onChangeSpy).toHaveBeenCalledWith({ ...expectedSearchRequest, status: expectedNewValue }) + }) + + it('should render a text input with the default value', () => { + const expectedSearchRequest: MedicationSearchRequest = { text: '', status: 'draft' } + const { wrapper } = setup(expectedSearchRequest) + + const textInput = wrapper.find(TextInputWithLabelFormGroup) + expect(textInput.prop('label')).toEqual('medications.search') + expect(textInput.prop('placeholder')).toEqual('medications.search') + expect(textInput.prop('value')).toEqual(expectedSearchRequest.text) + expect(textInput.prop('isEditable')).toBeTruthy() + }) + + it('should update the search request when the text input is updated', () => { + const expectedSearchRequest: MedicationSearchRequest = { text: '', status: 'draft' } + const expectedNewValue = 'someNewValue' + const { wrapper, onChangeSpy } = setup(expectedSearchRequest) + + act(() => { + const textInput = wrapper.find(TextInputWithLabelFormGroup) + const onChange = textInput.prop('onChange') as any + onChange({ target: { value: expectedNewValue } }) + }) + + expect(onChangeSpy).toHaveBeenCalledTimes(1) + expect(onChangeSpy).toHaveBeenCalledWith({ ...expectedSearchRequest, text: expectedNewValue }) + }) +}) diff --git a/src/__tests__/medications/search/MedicationRequestTable.test.tsx b/src/__tests__/medications/search/MedicationRequestTable.test.tsx new file mode 100644 index 0000000000..cdadaad467 --- /dev/null +++ b/src/__tests__/medications/search/MedicationRequestTable.test.tsx @@ -0,0 +1,91 @@ +import { Table } from '@hospitalrun/components' +import { mount, ReactWrapper } from 'enzyme' +import { createMemoryHistory } from 'history' +import React from 'react' +import { act } from 'react-dom/test-utils' +import { Router } from 'react-router-dom' + +import MedicationSearchRequest from '../../../medications/models/MedicationSearchRequest' +import MedicationRequestTable from '../../../medications/search/MedicationRequestTable' +import MedicationRepository from '../../../shared/db/MedicationRepository' +import Medication from '../../../shared/model/Medication' + +describe('Medication Request Table', () => { + const setup = async ( + givenSearchRequest: MedicationSearchRequest = { text: '', status: 'all' }, + givenMedications: Medication[] = [], + ) => { + jest.resetAllMocks() + jest.spyOn(MedicationRepository, 'search').mockResolvedValue(givenMedications) + const history = createMemoryHistory() + + let wrapper: any + await act(async () => { + wrapper = await mount( + + + , + ) + }) + wrapper.update() + + return { wrapper: wrapper as ReactWrapper, history } + } + + it('should render a table with the correct columns', async () => { + const { wrapper } = await setup() + + const table = wrapper.find(Table) + const columns = table.prop('columns') + const actions = table.prop('actions') as any + expect(columns[0]).toEqual( + expect.objectContaining({ label: 'medications.medication.medication', key: 'medication' }), + ) + expect(columns[1]).toEqual( + expect.objectContaining({ label: 'medications.medication.priority', key: 'priority' }), + ) + expect(columns[2]).toEqual( + expect.objectContaining({ label: 'medications.medication.intent', key: 'intent' }), + ) + expect(columns[3]).toEqual( + expect.objectContaining({ + label: 'medications.medication.requestedOn', + key: 'requestedOn', + }), + ) + expect(columns[4]).toEqual( + expect.objectContaining({ label: 'medications.medication.status', key: 'status' }), + ) + + expect(actions[0]).toEqual(expect.objectContaining({ label: 'actions.view' })) + expect(table.prop('actionsHeaderText')).toEqual('actions.label') + }) + + it('should fetch medications and display it', async () => { + const expectedSearchRequest: MedicationSearchRequest = { text: 'someText', status: 'draft' } + const expectedMedicationRequests: Medication[] = [{ id: 'someId' } as Medication] + + const { wrapper } = await setup(expectedSearchRequest, expectedMedicationRequests) + + const table = wrapper.find(Table) + expect(MedicationRepository.search).toHaveBeenCalledWith( + expect.objectContaining(expectedSearchRequest), + ) + expect(table.prop('data')).toEqual(expectedMedicationRequests) + }) + + it('should navigate to the medication when the view button is clicked', async () => { + const expectedSearchRequest: MedicationSearchRequest = { text: 'someText', status: 'draft' } + const expectedMedicationRequests: Medication[] = [{ id: 'someId' } as Medication] + + const { wrapper, history } = await setup(expectedSearchRequest, expectedMedicationRequests) + + const tr = wrapper.find('tr').at(1) + act(() => { + const onClick = tr.find('button').prop('onClick') as any + onClick({ stopPropagation: jest.fn() }) + }) + + expect(history.location.pathname).toEqual(`/medications/${expectedMedicationRequests[0].id}`) + }) +}) diff --git a/src/__tests__/medications/search/ViewMedications.test.tsx b/src/__tests__/medications/search/ViewMedications.test.tsx new file mode 100644 index 0000000000..f1e083b27c --- /dev/null +++ b/src/__tests__/medications/search/ViewMedications.test.tsx @@ -0,0 +1,130 @@ +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 MedicationSearchRequest from '../../../medications/models/MedicationSearchRequest' +import MedicationRequestSearch from '../../../medications/search/MedicationRequestSearch' +import MedicationRequestTable from '../../../medications/search/MedicationRequestTable' +import ViewMedications from '../../../medications/search/ViewMedications' +import * as ButtonBarProvider from '../../../page-header/button-toolbar/ButtonBarProvider' +import * as titleUtil from '../../../page-header/title/TitleContext' +import MedicationRepository from '../../../shared/db/MedicationRepository' +import Medication from '../../../shared/model/Medication' +import Permissions from '../../../shared/model/Permissions' +import { RootState } from '../../../shared/store' + +const { TitleProvider } = titleUtil +const mockStore = createMockStore([thunk]) + +describe('View Medications', () => { + const setup = async (medication: Medication, permissions: Permissions[] = []) => { + let wrapper: any + const expectedMedication = ({ + id: '1234', + medication: 'medication', + patient: 'patientId', + status: 'draft', + intent: 'order', + priority: 'routine', + quantity: { value: 1, unit: 'unit' }, + requestedOn: '2020-03-30T04:43:20.102Z', + } as unknown) as Medication + const history = createMemoryHistory() + const store = mockStore({ + user: { permissions }, + medications: { medications: [{ ...expectedMedication, ...medication }] }, + } as any) + const titleSpy = jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) + const setButtonToolBarSpy = jest.fn() + jest.spyOn(MedicationRepository, 'search').mockResolvedValue([]) + jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter').mockReturnValue(setButtonToolBarSpy) + jest.spyOn(MedicationRepository, 'findAll').mockResolvedValue([]) + + await act(async () => { + wrapper = await mount( + + + + + + + , + ) + }) + wrapper.update() + return { + wrapper: wrapper as ReactWrapper, + history, + titleSpy, + setButtonToolBarSpy, + } + } + + describe('title', () => { + it('should have called the useUpdateTitle hook', async () => { + const { titleSpy } = await setup({} as Medication) + + expect(titleSpy).toHaveBeenCalled() + }) + }) + + describe('button bar', () => { + it('should display button to add new medication request', async () => { + const permissions = [Permissions.ViewMedications, Permissions.RequestMedication] + const { setButtonToolBarSpy } = await setup({} as Medication, permissions) + + const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0] + expect((actualButtons[0] as any).props.children).toEqual('medications.requests.new') + }) + + it('should not display button to add new medication request if the user does not have permissions', async () => { + const { setButtonToolBarSpy } = await setup({} as Medication) + + const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0] + expect(actualButtons).toEqual([]) + }) + }) + + describe('table', () => { + it('should render a table with data with the default search', async () => { + const { wrapper } = await setup({} as Medication, [Permissions.ViewMedications]) + + const table = wrapper.find(MedicationRequestTable) + expect(table).toHaveLength(1) + expect(table.prop('searchRequest')).toEqual({ text: '', status: 'all' }) + }) + }) + + describe('search', () => { + it('should render a medication request search component', async () => { + const { wrapper } = await setup({} as Medication) + + const search = wrapper.find(MedicationRequestSearch) + expect(search).toHaveLength(1) + expect(search.prop('searchRequest')).toEqual({ text: '', status: 'all' }) + }) + + it('should update the table when the search changes', async () => { + const expectedSearchRequest: MedicationSearchRequest = { + text: 'someNewText', + status: 'draft', + } + const { wrapper } = await setup({} as Medication) + + await act(async () => { + const search = wrapper.find(MedicationRequestSearch) + const onChange = search.prop('onChange') + await onChange(expectedSearchRequest) + }) + wrapper.update() + + const table = wrapper.find(MedicationRequestTable) + expect(table.prop('searchRequest')).toEqual(expectedSearchRequest) + }) + }) +}) diff --git a/src/__tests__/page-header/title/TitleProvider.test.tsx b/src/__tests__/page-header/title/TitleProvider.test.tsx new file mode 100644 index 0000000000..85275956cc --- /dev/null +++ b/src/__tests__/page-header/title/TitleProvider.test.tsx @@ -0,0 +1,38 @@ +import { renderHook } from '@testing-library/react-hooks' +import React from 'react' +import { Provider } from 'react-redux' +import createMockStore from 'redux-mock-store' +import thunk from 'redux-thunk' + +import { TitleProvider } from '../../../page-header/title/TitleContext' +import { RootState } from '../../../shared/store' + +const mockStore = createMockStore([thunk]) + +describe('useTitle', () => { + const store = mockStore({ + user: { user: { id: '123' }, permissions: [] }, + appointments: { appointments: [] }, + medications: { medications: [] }, + labs: { labs: [] }, + imagings: { imagings: [] }, + breadcrumbs: { breadcrumbs: [] }, + components: { sidebarCollapsed: false }, + } as any) + + it('should call the updateTitle with the correct data.', () => { + const wrapper = ({ children }: any) => ( + + {children} + + ) + + const useTitle = jest.fn() + const expectedTitle = 'title' + + renderHook(() => useTitle(expectedTitle), { wrapper } as any) + + expect(useTitle).toHaveBeenCalledTimes(1) + expect(useTitle).toHaveBeenCalledWith(expectedTitle) + }) +}) diff --git a/src/__tests__/page-header/title/title-slice.test.ts b/src/__tests__/page-header/title/title-slice.test.ts deleted file mode 100644 index 8684ed6513..0000000000 --- a/src/__tests__/page-header/title/title-slice.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { AnyAction } from 'redux' - -import title, { updateTitle, changeTitle } from '../../../page-header/title/title-slice' - -describe('title slice', () => { - describe('title reducer', () => { - it('should create the proper initial title reducer', () => { - const patientsStore = title(undefined, {} as AnyAction) - expect(patientsStore.title).toEqual('') - }) - }) - - it('should handle the CHANGE_TITLE action', () => { - const expectedTitle = 'expected title' - const patientsStore = title(undefined, { - type: changeTitle.type, - payload: expectedTitle, - }) - - expect(patientsStore.title).toEqual(expectedTitle) - }) - - describe('updateTitle', () => { - it('should dispatch the CHANGE_TITLE event', async () => { - const dispatch = jest.fn() - const getState = jest.fn() - const expectedTitle = 'expected title' - - await updateTitle(expectedTitle)(dispatch, getState, null) - - expect(dispatch).toHaveBeenCalledWith({ type: changeTitle.type, payload: expectedTitle }) - }) - }) -}) diff --git a/src/__tests__/page-header/title/useTitle.test.tsx b/src/__tests__/page-header/title/useTitle.test.tsx deleted file mode 100644 index 8155df39c3..0000000000 --- a/src/__tests__/page-header/title/useTitle.test.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { renderHook } from '@testing-library/react-hooks' -import React from 'react' -import { Provider } from 'react-redux' -import createMockStore from 'redux-mock-store' -import thunk from 'redux-thunk' - -import * as titleSlice from '../../../page-header/title/title-slice' -import useTitle from '../../../page-header/title/useTitle' -import { RootState } from '../../../shared/store' - -const mockStore = createMockStore([thunk]) - -describe('useTitle', () => { - it('should call the updateTitle with the correct data', () => { - const wrapper = ({ children }: any) => {children} - - jest.spyOn(titleSlice, 'updateTitle') - const expectedTitle = 'title' - - renderHook(() => useTitle(expectedTitle), { wrapper } as any) - expect(titleSlice.updateTitle).toHaveBeenCalledTimes(1) - expect(titleSlice.updateTitle).toHaveBeenCalledWith(expectedTitle) - }) -}) diff --git a/src/__tests__/patients/Patients.test.tsx b/src/__tests__/patients/Patients.test.tsx index 8214eb15bf..d153419d28 100644 --- a/src/__tests__/patients/Patients.test.tsx +++ b/src/__tests__/patients/Patients.test.tsx @@ -9,6 +9,7 @@ import thunk from 'redux-thunk' import Dashboard from '../../dashboard/Dashboard' import HospitalRun from '../../HospitalRun' import { addBreadcrumbs } from '../../page-header/breadcrumbs/breadcrumbs-slice' +import * as titleUtil from '../../page-header/title/TitleContext' import EditPatient from '../../patients/edit/EditPatient' import NewPatient from '../../patients/new/NewPatient' import ViewPatient from '../../patients/view/ViewPatient' @@ -17,9 +18,11 @@ import Patient from '../../shared/model/Patient' import Permissions from '../../shared/model/Permissions' import { RootState } from '../../shared/store' +const { TitleProvider } = titleUtil const mockStore = createMockStore([thunk]) describe('/patients/new', () => { + jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) it('should render the new patient screen when /patients/new is accessed', async () => { const store = mockStore({ title: 'test', @@ -35,7 +38,9 @@ describe('/patients/new', () => { wrapper = await mount( - + + + , ) @@ -63,7 +68,9 @@ describe('/patients/new', () => { } as any)} > - + + + , ) @@ -99,7 +106,9 @@ describe('/patients/edit/:id', () => { const wrapper = mount( - + + + , ) @@ -127,7 +136,9 @@ describe('/patients/edit/:id', () => { } as any)} > - + + + , ) @@ -146,7 +157,9 @@ describe('/patients/edit/:id', () => { } as any)} > - + + + , ) @@ -179,7 +192,9 @@ describe('/patients/:id', () => { const wrapper = mount( - + + + , ) @@ -206,7 +221,9 @@ describe('/patients/:id', () => { } as any)} > - + + + , ) diff --git a/src/__tests__/patients/appointments/AppointmentsList.test.tsx b/src/__tests__/patients/appointments/AppointmentsList.test.tsx index e9eacf56ea..38889f4cab 100644 --- a/src/__tests__/patients/appointments/AppointmentsList.test.tsx +++ b/src/__tests__/patients/appointments/AppointmentsList.test.tsx @@ -1,6 +1,6 @@ import * as components from '@hospitalrun/components' import { Table } from '@hospitalrun/components' -import { mount } from 'enzyme' +import { mount, ReactWrapper } from 'enzyme' import { createMemoryHistory } from 'history' import React from 'react' import { act } from 'react-dom/test-utils' @@ -45,26 +45,35 @@ const history = createMemoryHistory() let store: any -const setup = (patient = expectedPatient, appointments = expectedAppointments) => { +const setup = async (patient = expectedPatient, appointments = expectedAppointments) => { jest.resetAllMocks() jest.spyOn(PatientRepository, 'getAppointments').mockResolvedValue(appointments) store = mockStore({ patient, appointments: { appointments } } as any) - const wrapper = mount( - - - - - , - ) - return wrapper + + let wrapper: any + + await act(async () => { + wrapper = await mount( + + + + + , + ) + }) + + wrapper.update() + + return { wrapper: wrapper as ReactWrapper } } describe('AppointmentsList', () => { describe('Table', () => { - it('should render a list of appointments', () => { - const wrapper = setup() + it('should render a list of appointments', async () => { + const { wrapper } = await setup() const table = wrapper.find(Table) + const columns = table.prop('columns') const actions = table.prop('actions') as any @@ -91,7 +100,7 @@ describe('AppointmentsList', () => { }) it('should navigate to appointment profile on appointment click', async () => { - const wrapper = setup() + const { wrapper } = await setup() const tr = wrapper.find('tr').at(1) act(() => { @@ -104,8 +113,8 @@ describe('AppointmentsList', () => { }) describe('Empty list', () => { - it('should render a warning message if there are no appointments', () => { - const wrapper = setup(expectedPatient, []) + it('should render a warning message if there are no appointments', async () => { + const { wrapper } = await setup(expectedPatient, []) const alert = wrapper.find(components.Alert) expect(alert).toHaveLength(1) @@ -115,8 +124,16 @@ describe('AppointmentsList', () => { }) describe('New appointment button', () => { - it('should render a new appointment button', () => { - const wrapper = setup() + it('should render a new appointment button if there is an appointment', async () => { + const { wrapper } = await setup() + + const addNewAppointmentButton = wrapper.find(components.Button).at(0) + expect(addNewAppointmentButton).toHaveLength(1) + expect(addNewAppointmentButton.text().trim()).toEqual('scheduling.appointments.new') + }) + + it('should render a new appointment button if there are no appointments', async () => { + const { wrapper } = await setup(expectedPatient, []) const addNewAppointmentButton = wrapper.find(components.Button).at(0) expect(addNewAppointmentButton).toHaveLength(1) @@ -124,7 +141,7 @@ describe('AppointmentsList', () => { }) it('should navigate to new appointment page', async () => { - const wrapper = setup() + const { wrapper } = await setup() await act(async () => { await wrapper.find(components.Button).at(0).simulate('click') diff --git a/src/__tests__/patients/care-goals/AddCareGoalModal.test.tsx b/src/__tests__/patients/care-goals/AddCareGoalModal.test.tsx new file mode 100644 index 0000000000..b8b23551b0 --- /dev/null +++ b/src/__tests__/patients/care-goals/AddCareGoalModal.test.tsx @@ -0,0 +1,101 @@ +import { Modal } from '@hospitalrun/components' +import { mount } from 'enzyme' +import { createMemoryHistory } from 'history' +import React from 'react' +import { act } from 'react-dom/test-utils' +import { Router } from 'react-router-dom' + +import AddCareGoalModal from '../../../patients/care-goals/AddCareGoalModal' +import CareGoalForm from '../../../patients/care-goals/CareGoalForm' +import PatientRepository from '../../../shared/db/PatientRepository' +import CareGoal, { CareGoalStatus, CareGoalAchievementStatus } from '../../../shared/model/CareGoal' +import Patient from '../../../shared/model/Patient' + +describe('Add Care Goal Modal', () => { + const patient = { + givenName: 'given Name', + fullName: 'full name', + careGoals: [] as CareGoal[], + } as Patient + + const onCloseSpy = jest.fn() + const setup = () => { + jest.spyOn(PatientRepository, 'find').mockResolvedValue(patient) + jest.spyOn(PatientRepository, 'saveOrUpdate') + const history = createMemoryHistory() + const wrapper = mount( + + + , + ) + + wrapper.update() + return { wrapper } + } + + beforeEach(() => { + jest.resetAllMocks() + }) + + it('should render a modal', () => { + const { wrapper } = setup() + + const modal = wrapper.find(Modal) + const sucessButton = modal.prop('successButton') + const closeButton = modal.prop('closeButton') + + expect(modal).toHaveLength(1) + expect(modal.prop('title')).toEqual('patient.careGoal.new') + expect(sucessButton?.children).toEqual('patient.careGoal.new') + expect(sucessButton?.icon).toEqual('add') + expect(closeButton?.children).toEqual('actions.cancel') + }) + + it('should render a care goal form', () => { + const { wrapper } = setup() + + const careGoalForm = wrapper.find(CareGoalForm) + expect(careGoalForm).toHaveLength(1) + }) + + it('should save care goal when save button is clicked and close', async () => { + const expectedCreatedDate = new Date() + Date.now = jest.fn().mockReturnValue(expectedCreatedDate) + + const expectedCareGoal = { + id: '123', + description: 'some description', + startDate: new Date().toISOString(), + dueDate: new Date().toISOString(), + note: '', + priority: 'medium', + status: CareGoalStatus.Accepted, + achievementStatus: CareGoalAchievementStatus.InProgress, + createdOn: expectedCreatedDate, + } + + const { wrapper } = setup() + await act(async () => { + const careGoalForm = wrapper.find(CareGoalForm) + const onChange = careGoalForm.prop('onChange') as any + await onChange(expectedCareGoal) + }) + + wrapper.update() + + await act(async () => { + const modal = wrapper.find(Modal) + const sucessButton = modal.prop('successButton') + const onClick = sucessButton?.onClick as any + await onClick() + }) + + expect(PatientRepository.saveOrUpdate).toHaveBeenCalledTimes(1) + expect(PatientRepository.saveOrUpdate).toHaveBeenCalledWith({ + ...patient, + careGoals: [expectedCareGoal], + }) + + expect(onCloseSpy).toHaveBeenCalledTimes(1) + }) +}) diff --git a/src/__tests__/patients/care-goals/CareGoalForm.test.tsx b/src/__tests__/patients/care-goals/CareGoalForm.test.tsx new file mode 100644 index 0000000000..fc94456ab9 --- /dev/null +++ b/src/__tests__/patients/care-goals/CareGoalForm.test.tsx @@ -0,0 +1,292 @@ +import { Alert } from '@hospitalrun/components' +import { addMonths, addDays } from 'date-fns' +import { mount } from 'enzyme' +import React from 'react' +import { act } from 'react-dom/test-utils' + +import CareGoalForm from '../../../patients/care-goals/CareGoalForm' +import CareGoal, { CareGoalStatus, CareGoalAchievementStatus } from '../../../shared/model/CareGoal' + +describe('Care Goal Form', () => { + const onCareGoalChangeSpy = jest.fn() + const careGoal = { + description: 'some description', + startDate: new Date().toISOString(), + dueDate: addMonths(new Date(), 1).toISOString(), + note: '', + priority: 'medium', + status: CareGoalStatus.Accepted, + achievementStatus: CareGoalAchievementStatus.InProgress, + } as CareGoal + + const setup = (disabled = false, initializeCareGoal = true, error?: any) => { + const wrapper = mount( + , + ) + + return wrapper + } + + it('should render a description input', () => { + const wrapper = setup() + + const descriptionInput = wrapper.findWhere((w) => w.prop('name') === 'description') + + expect(descriptionInput).toHaveLength(1) + expect(descriptionInput.prop('label')).toEqual('patient.careGoal.description') + expect(descriptionInput.prop('isRequired')).toBeTruthy() + expect(descriptionInput.prop('value')).toBe(careGoal.description) + }) + + it('should call onChange handler when description changes', () => { + const expectedDescription = 'some new description' + const wrapper = setup(false, false) + + act(() => { + const descriptionInput = wrapper.findWhere((w) => w.prop('name') === 'description') + const onChange = descriptionInput.prop('onChange') as any + onChange({ currentTarget: { value: expectedDescription } }) + }) + + expect(onCareGoalChangeSpy).toHaveBeenCalledWith({ description: expectedDescription }) + }) + + it('should render a priority selector', () => { + const wrapper = setup() + + const priority = wrapper.findWhere((w) => w.prop('name') === 'priority') + + expect(priority).toHaveLength(1) + expect(priority.prop('label')).toEqual('patient.careGoal.priority.label') + expect(priority.prop('isRequired')).toBeTruthy() + expect(priority.prop('defaultSelected')[0].value).toBe(careGoal.priority) + expect(priority.prop('options')).toEqual([ + { + label: 'patient.careGoal.priority.low', + value: 'low', + }, + { + label: 'patient.careGoal.priority.medium', + value: 'medium', + }, + { + label: 'patient.careGoal.priority.high', + value: 'high', + }, + ]) + }) + + it('should call onChange handler when priority changes', () => { + const expectedPriority = 'high' + const wrapper = setup(false, false) + + act(() => { + const prioritySelector = wrapper.findWhere((w) => w.prop('name') === 'priority') + const onChange = prioritySelector.prop('onChange') as any + onChange([expectedPriority]) + }) + + expect(onCareGoalChangeSpy).toHaveBeenCalledWith({ priority: expectedPriority }) + }) + + it('should render a status selector', () => { + const wrapper = setup() + + const status = wrapper.findWhere((w) => w.prop('name') === 'status') + + expect(status).toHaveLength(1) + expect(status.prop('label')).toEqual('patient.careGoal.status') + expect(status.prop('isRequired')).toBeTruthy() + expect(status.prop('defaultSelected')[0].value).toBe(careGoal.status) + expect(status.prop('options')).toEqual( + Object.values(CareGoalStatus).map((v) => ({ label: v, value: v })), + ) + }) + + it('should call onChange handler when status changes', () => { + const expectedStatus = CareGoalStatus.OnHold + const wrapper = setup(false, false) + + act(() => { + const statusSelector = wrapper.findWhere((w) => w.prop('name') === 'status') + const onChange = statusSelector.prop('onChange') as any + onChange([expectedStatus]) + }) + + expect(onCareGoalChangeSpy).toHaveBeenCalledWith({ status: expectedStatus }) + }) + + it('should render the achievement status selector', () => { + const wrapper = setup() + + const achievementStatus = wrapper.findWhere((w) => w.prop('name') === 'achievementStatus') + expect(achievementStatus).toHaveLength(1) + expect(achievementStatus.prop('label')).toEqual('patient.careGoal.achievementStatus') + expect(achievementStatus.prop('isRequired')).toBeTruthy() + expect(achievementStatus.prop('defaultSelected')[0].value).toBe(careGoal.achievementStatus) + expect(achievementStatus.prop('options')).toEqual( + Object.values(CareGoalAchievementStatus).map((v) => ({ label: v, value: v })), + ) + }) + + it('should call onChange handler when achievement status change', () => { + const expectedAchievementStatus = CareGoalAchievementStatus.Improving + const wrapper = setup(false, false) + + act(() => { + const achievementStatusSelector = wrapper.findWhere( + (w) => w.prop('name') === 'achievementStatus', + ) + const onChange = achievementStatusSelector.prop('onChange') as any + onChange([expectedAchievementStatus]) + }) + + expect(onCareGoalChangeSpy).toHaveBeenCalledWith({ + achievementStatus: expectedAchievementStatus, + }) + }) + + it('should render a start date picker', () => { + const wrapper = setup() + + const startDatePicker = wrapper.findWhere((w) => w.prop('name') === 'startDate') + expect(startDatePicker).toHaveLength(1) + expect(startDatePicker.prop('label')).toEqual('patient.careGoal.startDate') + expect(startDatePicker.prop('isRequired')).toBeTruthy() + expect(startDatePicker.prop('value')).toEqual(new Date(careGoal.startDate)) + }) + + it('should call onChange handler when start date change', () => { + const expectedStartDate = addDays(1, new Date().getDate()) + const wrapper = setup(false, false) + + act(() => { + const startDatePicker = wrapper.findWhere((w) => w.prop('name') === 'startDate') + const onChange = startDatePicker.prop('onChange') as any + onChange(expectedStartDate) + }) + + expect(onCareGoalChangeSpy).toHaveBeenCalledWith({ startDate: expectedStartDate.toISOString() }) + }) + + it('should render a due date picker', () => { + const wrapper = setup() + + const dueDatePicker = wrapper.findWhere((w) => w.prop('name') === 'dueDate') + expect(dueDatePicker).toHaveLength(1) + expect(dueDatePicker.prop('label')).toEqual('patient.careGoal.dueDate') + expect(dueDatePicker.prop('isRequired')).toBeTruthy() + expect(dueDatePicker.prop('value')).toEqual(new Date(careGoal.dueDate)) + }) + + it('should call onChange handler when due date change', () => { + const expectedDueDate = addDays(31, new Date().getDate()) + const wrapper = setup(false, false) + + act(() => { + const dueDatePicker = wrapper.findWhere((w) => w.prop('name') === 'dueDate') + const onChange = dueDatePicker.prop('onChange') as any + onChange(expectedDueDate) + }) + + expect(onCareGoalChangeSpy).toHaveBeenCalledWith({ dueDate: expectedDueDate.toISOString() }) + }) + + it('should render a note input', () => { + const wrapper = setup() + + const noteInput = wrapper.findWhere((w) => w.prop('name') === 'note') + expect(noteInput).toHaveLength(1) + expect(noteInput.prop('label')).toEqual('patient.careGoal.note') + expect(noteInput.prop('isRequired')).toBeFalsy() + expect(noteInput.prop('value')).toEqual(careGoal.note) + }) + + it('should call onChange handler when note change', () => { + const expectedNote = 'some new note' + const wrapper = setup(false, false) + + act(() => { + const noteInput = wrapper.findWhere((w) => w.prop('name') === 'note') + const onChange = noteInput.prop('onChange') as any + onChange({ currentTarget: { value: expectedNote } }) + }) + + expect(onCareGoalChangeSpy).toHaveBeenCalledWith({ note: expectedNote }) + }) + + it('should render all the forms fields disabled if the form is disabled', () => { + const wrapper = setup(true) + + const description = wrapper.findWhere((w) => w.prop('name') === 'description') + const priority = wrapper.findWhere((w) => w.prop('name') === 'priority') + const status = wrapper.findWhere((w) => w.prop('name') === 'status') + const achievementStatus = wrapper.findWhere((w) => w.prop('name') === 'achievementStatus') + const startDate = wrapper.findWhere((w) => w.prop('name') === 'startDate') + const dueDate = wrapper.findWhere((w) => w.prop('name') === 'dueDate') + const note = wrapper.findWhere((w) => w.prop('name') === 'note') + + expect(description.prop('isEditable')).toBeFalsy() + expect(priority.prop('isEditable')).toBeFalsy() + expect(status.prop('isEditable')).toBeFalsy() + expect(achievementStatus.prop('isEditable')).toBeFalsy() + expect(startDate.prop('isEditable')).toBeFalsy() + expect(dueDate.prop('isEditable')).toBeFalsy() + expect(note.prop('isEditable')).toBeFalsy() + }) + + it('should render the forms field in an error state', () => { + const expectedError = { + message: 'some error message', + description: 'some description error', + status: 'some status error', + achievementStatus: 'some achievement status error', + priority: 'some priority error', + startDate: 'some start date error', + dueDate: 'some due date error', + note: 'some note error', + } + + const wrapper = setup(false, false, expectedError) + + const alert = wrapper.find(Alert) + const descriptionInput = wrapper.findWhere((w) => w.prop('name') === 'description') + const prioritySelector = wrapper.findWhere((w) => w.prop('name') === 'priority') + const statusSelector = wrapper.findWhere((w) => w.prop('name') === 'status') + const achievementStatusSelector = wrapper.findWhere( + (w) => w.prop('name') === 'achievementStatus', + ) + const startDatePicker = wrapper.findWhere((w) => w.prop('name') === 'startDate') + const dueDatePicker = wrapper.findWhere((w) => w.prop('name') === 'dueDate') + const noteInput = wrapper.findWhere((w) => w.prop('name') === 'note') + + expect(alert).toHaveLength(1) + expect(alert.prop('message')).toEqual(expectedError.message) + + expect(descriptionInput.prop('isInvalid')).toBeTruthy() + expect(descriptionInput.prop('feedback')).toEqual(expectedError.description) + + expect(prioritySelector.prop('isInvalid')).toBeTruthy() + // expect(prioritySelector.prop('feedback')).toEqual(expectedError.priority) + + expect(statusSelector.prop('isInvalid')).toBeTruthy() + // expect(statusSelector.prop('feedback')).toEqual(expectedError.status) + + expect(achievementStatusSelector.prop('isInvalid')).toBeTruthy() + // expect(achievementStatusSelector.prop('feedback')).toEqual(expectedError.achievementStatus) + + expect(startDatePicker.prop('isInvalid')).toBeTruthy() + expect(startDatePicker.prop('feedback')).toEqual(expectedError.startDate) + + expect(dueDatePicker.prop('isInvalid')).toBeTruthy() + expect(dueDatePicker.prop('feedback')).toEqual(expectedError.dueDate) + + expect(noteInput.prop('isInvalid')).toBeTruthy() + expect(noteInput.prop('feedback')).toEqual(expectedError.note) + }) +}) diff --git a/src/__tests__/patients/care-goals/CareGoalTab.test.tsx b/src/__tests__/patients/care-goals/CareGoalTab.test.tsx new file mode 100644 index 0000000000..57b0d42877 --- /dev/null +++ b/src/__tests__/patients/care-goals/CareGoalTab.test.tsx @@ -0,0 +1,111 @@ +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 AddCareGoalModal from '../../../patients/care-goals/AddCareGoalModal' +import CareGoalTab from '../../../patients/care-goals/CareGoalTab' +import CareGoalTable from '../../../patients/care-goals/CareGoalTable' +import ViewCareGoal from '../../../patients/care-goals/ViewCareGoal' +import PatientRepository from '../../../shared/db/PatientRepository' +import Patient from '../../../shared/model/Patient' +import Permissions from '../../../shared/model/Permissions' +import { RootState } from '../../../shared/store' + +const mockStore = createMockStore([thunk]) + +describe('Care Goals Tab', () => { + const patient = { id: 'patientId' } as Patient + + const setup = async (route: string, permissions: Permissions[]) => { + jest.spyOn(PatientRepository, 'find').mockResolvedValue(patient) + const store = mockStore({ user: { permissions } } as any) + const history = createMemoryHistory() + history.push(route) + + let wrapper: any + await act(async () => { + wrapper = await mount( + + + + + , + ) + }) + wrapper.update() + + return wrapper as ReactWrapper + } + + it('should render add care goal button if user has correct permissions', async () => { + const wrapper = await setup('patients/123/care-goals', [Permissions.AddCareGoal]) + + const addNewButton = wrapper.find('Button').at(0) + expect(addNewButton).toHaveLength(1) + expect(addNewButton.text().trim()).toEqual('patient.careGoal.new') + }) + + it('should not render add care goal button if user does not have permissions', async () => { + const wrapper = await setup('patients/123/care-goals', []) + + const addNewButton = wrapper.find('Button') + expect(addNewButton).toHaveLength(0) + }) + + it('should open the add care goal modal on click', async () => { + const wrapper = await setup('patients/123/care-goals', [Permissions.AddCareGoal]) + + await act(async () => { + const addNewButton = wrapper.find('Button').at(0) + const onClick = addNewButton.prop('onClick') as any + onClick() + }) + + wrapper.update() + + const modal = wrapper.find(AddCareGoalModal) + expect(modal.prop('show')).toBeTruthy() + }) + + it('should close the modal when the close button is clicked', async () => { + const wrapper = await setup('patients/123/care-goals', [Permissions.AddCareGoal]) + + await act(async () => { + const addNewButton = wrapper.find('Button').at(0) + const onClick = addNewButton.prop('onClick') as any + onClick() + }) + + wrapper.update() + + await act(async () => { + const modal = wrapper.find(AddCareGoalModal) + const onClose = modal.prop('onCloseButtonClick') as any + onClose() + }) + + wrapper.update() + + const modal = wrapper.find(AddCareGoalModal) + expect(modal.prop('show')).toBeFalsy() + }) + + it('should render care goal table when on patients/123/care-goals', async () => { + const wrapper = await setup('patients/123/care-goals', [Permissions.ReadCareGoal]) + + const careGoalTable = wrapper.find(CareGoalTable) + expect(careGoalTable).toHaveLength(1) + }) + + it('should render care goal view when on patients/123/care-goals/456', async () => { + const wrapper = await setup('patients/123/care-goals/456', [Permissions.ReadCareGoal]) + + const viewCareGoal = wrapper.find(ViewCareGoal) + expect(viewCareGoal).toHaveLength(1) + }) +}) diff --git a/src/__tests__/patients/care-goals/CareGoalTable.test.tsx b/src/__tests__/patients/care-goals/CareGoalTable.test.tsx new file mode 100644 index 0000000000..0917055e96 --- /dev/null +++ b/src/__tests__/patients/care-goals/CareGoalTable.test.tsx @@ -0,0 +1,96 @@ +import { Table, 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 { Router } from 'react-router-dom' + +import CareGoalTable from '../../../patients/care-goals/CareGoalTable' +import PatientRepository from '../../../shared/db/PatientRepository' +import CareGoal, { CareGoalStatus, CareGoalAchievementStatus } from '../../../shared/model/CareGoal' +import Patient from '../../../shared/model/Patient' + +describe('Care Goal Table', () => { + const careGoal: CareGoal = { + id: '123', + description: 'some description', + priority: 'medium', + status: CareGoalStatus.Accepted, + achievementStatus: CareGoalAchievementStatus.Improving, + startDate: new Date().toISOString(), + dueDate: new Date().toISOString(), + createdOn: new Date().toISOString(), + note: 'some note', + } + + const patient = { + givenName: 'given Name', + fullName: 'full Name', + careGoals: [careGoal], + } as Patient + + const setup = async (expectedPatient = patient) => { + jest.spyOn(PatientRepository, 'find').mockResolvedValue(expectedPatient) + const history = createMemoryHistory() + history.push(`/patients/${patient.id}/care-goals/${patient.careGoals[0].id}`) + + let wrapper: any + await act(async () => { + wrapper = await mount( + + + , + ) + }) + wrapper.update() + return { wrapper: wrapper as ReactWrapper, history } + } + + it('should render a table', async () => { + const { wrapper } = await setup() + + const table = wrapper.find(Table) + const columns = table.prop('columns') + + expect(columns[0]).toEqual( + expect.objectContaining({ label: 'patient.careGoal.description', key: 'description' }), + ) + expect(columns[1]).toEqual( + expect.objectContaining({ label: 'patient.careGoal.startDate', key: 'startDate' }), + ) + expect(columns[2]).toEqual( + expect.objectContaining({ label: 'patient.careGoal.dueDate', key: 'dueDate' }), + ) + expect(columns[3]).toEqual( + expect.objectContaining({ label: 'patient.careGoal.status', key: 'status' }), + ) + + const actions = table.prop('actions') as any + expect(actions[0]).toEqual(expect.objectContaining({ label: 'actions.view' })) + expect(table.prop('actionsHeaderText')).toEqual('actions.label') + expect(table.prop('data')).toEqual(patient.careGoals) + }) + + it('should navigate to the care goal view when the view details button is clicked', async () => { + const { wrapper, history } = await setup() + + const tr = wrapper.find('tr').at(1) + + act(() => { + const onClick = tr.find('button').prop('onClick') as any + onClick({ stopPropagation: jest.fn() }) + }) + + expect(history.location.pathname).toEqual(`/patients/${patient.id}/care-goals/${careGoal.id}`) + }) + + it('should display a warning if there are no care goals', async () => { + const { wrapper } = await setup({ ...patient, careGoals: [] }) + + expect(wrapper.exists(Alert)).toBeTruthy() + const alert = wrapper.find(Alert) + expect(alert.prop('color')).toEqual('warning') + expect(alert.prop('title')).toEqual('patient.careGoals.warning.noCareGoals') + expect(alert.prop('message')).toEqual('patient.careGoals.warning.addCareGoalAbove') + }) +}) diff --git a/src/__tests__/patients/care-goals/ViewCareGoal.test.tsx b/src/__tests__/patients/care-goals/ViewCareGoal.test.tsx new file mode 100644 index 0000000000..380c4f8f20 --- /dev/null +++ b/src/__tests__/patients/care-goals/ViewCareGoal.test.tsx @@ -0,0 +1,48 @@ +import { mount, ReactWrapper } from 'enzyme' +import { createMemoryHistory } from 'history' +import React from 'react' +import { act } from 'react-dom/test-utils' +import { Router, Route } from 'react-router-dom' + +import CareGoalForm from '../../../patients/care-goals/CareGoalForm' +import ViewCareGoal from '../../../patients/care-goals/ViewCareGoal' +import PatientRepository from '../../../shared/db/PatientRepository' +import Patient from '../../../shared/model/Patient' + +describe('View Care Goal', () => { + const patient = { + id: '123', + givenName: 'given Name', + fullName: 'full Name', + careGoals: [{ id: '123', description: 'some description' }], + } as Patient + + const setup = async () => { + jest.spyOn(PatientRepository, 'find').mockResolvedValue(patient) + const history = createMemoryHistory() + history.push(`/patients/${patient.id}/care-goals/${patient.careGoals[0].id}`) + + let wrapper: any + await act(async () => { + wrapper = await mount( + + + + + , + ) + }) + + wrapper.update() + + return { wrapper: wrapper as ReactWrapper } + } + + it('should render the care goal form', async () => { + const { wrapper } = await setup() + const careGoalForm = wrapper.find(CareGoalForm) + + expect(careGoalForm).toHaveLength(1) + expect(careGoalForm.prop('careGoal')).toEqual(patient.careGoals[0]) + }) +}) diff --git a/src/__tests__/patients/care-goals/ViewCareGoals.test.tsx b/src/__tests__/patients/care-goals/ViewCareGoals.test.tsx new file mode 100644 index 0000000000..a80f7a7671 --- /dev/null +++ b/src/__tests__/patients/care-goals/ViewCareGoals.test.tsx @@ -0,0 +1,42 @@ +import { mount, ReactWrapper } from 'enzyme' +import { createMemoryHistory } from 'history' +import React from 'react' +import { act } from 'react-dom/test-utils' +import { Route, Router } from 'react-router-dom' + +import CareGoalTable from '../../../patients/care-goals/CareGoalTable' +import ViewCareGoals from '../../../patients/care-goals/ViewCareGoals' +import PatientRepository from '../../../shared/db/PatientRepository' +import CareGoal from '../../../shared/model/CareGoal' +import Patient from '../../../shared/model/Patient' + +describe('View Care Goals', () => { + const patient = { id: '123', careGoals: [] as CareGoal[] } as Patient + const setup = async () => { + jest.spyOn(PatientRepository, 'find').mockResolvedValue(patient) + + const history = createMemoryHistory() + history.push(`/patients/${patient.id}/care-goals`) + let wrapper: any + + await act(async () => { + wrapper = await mount( + + + + + , + ) + }) + + return { wrapper: wrapper as ReactWrapper } + } + + it('should render a care goals table with the patient id', async () => { + const { wrapper } = await setup() + + expect(wrapper.exists(CareGoalTable)).toBeTruthy() + const careGoalTable = wrapper.find(CareGoalTable) + expect(careGoalTable.prop('patientId')).toEqual(patient.id) + }) +}) diff --git a/src/__tests__/patients/diagnoses/AddDiagnosisModal.test.tsx b/src/__tests__/patients/diagnoses/AddDiagnosisModal.test.tsx index 5349e5b095..0ff107dfb8 100644 --- a/src/__tests__/patients/diagnoses/AddDiagnosisModal.test.tsx +++ b/src/__tests__/patients/diagnoses/AddDiagnosisModal.test.tsx @@ -1,27 +1,30 @@ +/* eslint-disable no-console */ import { Modal } from '@hospitalrun/components' import { mount } from 'enzyme' -import { createMemoryHistory } from 'history' import React from 'react' import { act } from 'react-dom/test-utils' -import { Provider } from 'react-redux' -import { Router } from 'react-router-dom' -import createMockStore from 'redux-mock-store' -import thunk from 'redux-thunk' import AddDiagnosisModal from '../../../patients/diagnoses/AddDiagnosisModal' import DiagnosisForm from '../../../patients/diagnoses/DiagnosisForm' -import * as patientSlice from '../../../patients/patient-slice' import PatientRepository from '../../../shared/db/PatientRepository' import { CarePlanIntent, CarePlanStatus } from '../../../shared/model/CarePlan' +import Diagnosis, { DiagnosisStatus } from '../../../shared/model/Diagnosis' import Patient from '../../../shared/model/Patient' -import { RootState } from '../../../shared/store' - -const mockStore = createMockStore([thunk]) describe('Add Diagnosis Modal', () => { - const patient = { + const mockDiagnosis: Diagnosis = { + id: '123', + name: 'some name', + diagnosisDate: new Date().toISOString(), + onsetDate: new Date().toISOString(), + abatementDate: new Date().toISOString(), + status: DiagnosisStatus.Active, + visit: '1234', + note: 'some note', + } + const mockPatient = { id: 'patientId', - diagnoses: [{ id: '123', name: 'some name', diagnosisDate: new Date().toISOString() }], + diagnoses: [mockDiagnosis], carePlans: [ { id: '123', @@ -36,28 +39,20 @@ describe('Add Diagnosis Modal', () => { ], } as Patient - const diagnosisError = { - title: 'some diagnosisError error', - } - - const onCloseSpy = jest.fn() - const setup = () => { + const setup = (patient = mockPatient, onCloseSpy = jest.fn()) => { jest.spyOn(PatientRepository, 'find').mockResolvedValue(patient) - jest.spyOn(PatientRepository, 'saveOrUpdate') - const store = mockStore({ patient: { patient, diagnosisError } } as any) - const history = createMemoryHistory() + jest.spyOn(PatientRepository, 'saveOrUpdate').mockResolvedValue(patient) + const wrapper = mount( - - - - - , + , ) wrapper.update() return { wrapper } } - + beforeEach(() => { + console.error = jest.fn() + }) it('should render a modal', () => { const { wrapper } = setup() @@ -78,48 +73,45 @@ describe('Add Diagnosis Modal', () => { const diagnosisForm = wrapper.find(DiagnosisForm) expect(diagnosisForm).toHaveLength(1) - expect(diagnosisForm.prop('diagnosisError')).toEqual(diagnosisError) }) it('should dispatch add diagnosis when the save button is clicked', async () => { - const { wrapper } = setup() - jest.spyOn(patientSlice, 'addDiagnosis') + const patient = mockPatient + patient.diagnoses = [] + const { wrapper } = setup(patient) + + const newDiagnosis = mockDiagnosis + newDiagnosis.name = 'New Diagnosis Name' act(() => { const diagnosisForm = wrapper.find(DiagnosisForm) const onChange = diagnosisForm.prop('onChange') as any - if (patient.diagnoses != null) { - onChange(patient.diagnoses[0]) - } + onChange(newDiagnosis) }) wrapper.update() await act(async () => { const modal = wrapper.find(Modal) - const successButton = modal.prop('successButton') - const onClick = successButton?.onClick as any - await onClick() + const onSave = (modal.prop('successButton') as any).onClick + await onSave({} as React.MouseEvent) }) - - expect(patientSlice.addDiagnosis).toHaveBeenCalledTimes(1) - if (patient.diagnoses != null) { - expect(patientSlice.addDiagnosis).toHaveBeenCalledWith(patient.id, patient.diagnoses[0]) - } + expect(PatientRepository.saveOrUpdate).toHaveBeenCalledTimes(1) + expect(PatientRepository.saveOrUpdate).toHaveBeenCalledWith( + expect.objectContaining({ + diagnoses: [expect.objectContaining({ name: 'New Diagnosis Name' })], + }), + ) }) - it('should call the on close function when the cancel button is clicked', () => { - const { wrapper } = setup() - + it('should call the on close function when the cancel button is clicked', async () => { + const onCloseButtonClickSpy = jest.fn() + const { wrapper } = setup(mockPatient, onCloseButtonClickSpy) const modal = wrapper.find(Modal) - - expect(modal).toHaveLength(1) - act(() => { - const cancelButton = modal.prop('closeButton') - const onClick = cancelButton?.onClick as any + const { onClick } = modal.prop('closeButton') as any onClick() }) - - expect(onCloseSpy).toHaveBeenCalledTimes(1) + expect(modal).toHaveLength(1) + expect(onCloseButtonClickSpy).toHaveBeenCalledTimes(1) }) }) diff --git a/src/__tests__/patients/diagnoses/Diagnoses.test.tsx b/src/__tests__/patients/diagnoses/Diagnoses.test.tsx index 453bb50cb2..f60b1a58ff 100644 --- a/src/__tests__/patients/diagnoses/Diagnoses.test.tsx +++ b/src/__tests__/patients/diagnoses/Diagnoses.test.tsx @@ -1,3 +1,5 @@ +/* eslint-disable no-console */ + import * as components from '@hospitalrun/components' import { mount } from 'enzyme' import { createMemoryHistory } from 'history' @@ -46,8 +48,8 @@ describe('Diagnoses', () => { beforeEach(() => { jest.resetAllMocks() jest.spyOn(PatientRepository, 'saveOrUpdate') + console.error = jest.fn() }) - it('should render a add diagnoses button', () => { const wrapper = setup() @@ -75,27 +77,4 @@ describe('Diagnoses', () => { expect(wrapper.find(components.Modal).prop('show')).toBeTruthy() }) }) - - describe('diagnoses list', () => { - it('should list the patients diagnoses', () => { - const diagnoses = expectedPatient.diagnoses as Diagnosis[] - const wrapper = setup() - - const list = wrapper.find(components.List) - const listItems = wrapper.find(components.ListItem) - - expect(list).toHaveLength(1) - expect(listItems).toHaveLength(diagnoses.length) - }) - - it('should render a warning message if the patient does not have any diagnoses', () => { - const wrapper = setup({ ...expectedPatient, diagnoses: [] }) - - const alert = wrapper.find(components.Alert) - - expect(alert).toHaveLength(1) - expect(alert.prop('title')).toEqual('patient.diagnoses.warning.noDiagnoses') - expect(alert.prop('message')).toEqual('patient.diagnoses.addDiagnosisAbove') - }) - }) }) diff --git a/src/__tests__/patients/diagnoses/DiagnosesList.test.tsx b/src/__tests__/patients/diagnoses/DiagnosesList.test.tsx new file mode 100644 index 0000000000..e47be82b3d --- /dev/null +++ b/src/__tests__/patients/diagnoses/DiagnosesList.test.tsx @@ -0,0 +1,51 @@ +import { Alert, List, ListItem } from '@hospitalrun/components' +import { mount, ReactWrapper } from 'enzyme' +import React from 'react' +import { act } from 'react-dom/test-utils' + +import DiagnosesList from '../../../patients/diagnoses/DiagnosesList' +import PatientRepository from '../../../shared/db/PatientRepository' +import Diagnosis from '../../../shared/model/Diagnosis' +import Patient from '../../../shared/model/Patient' + +const expectedDiagnoses = [ + { id: '123', name: 'diagnosis1', diagnosisDate: new Date().toISOString() } as Diagnosis, +] + +describe('Diagnoses list', () => { + const setup = async (diagnoses: Diagnosis[]) => { + const mockPatient = { id: '123', diagnoses } as Patient + jest.spyOn(PatientRepository, 'find').mockResolvedValueOnce(mockPatient) + let wrapper: any + await act(async () => { + wrapper = await mount() + }) + + wrapper.update() + return { wrapper: wrapper as ReactWrapper } + } + + it('should list the patients diagnoses', async () => { + const diagnoses = expectedDiagnoses as Diagnosis[] + const { wrapper } = await setup(diagnoses) + + const listItems = wrapper.find(ListItem) + + expect(wrapper.exists(List)).toBeTruthy() + expect(listItems).toHaveLength(expectedDiagnoses.length) + expect(listItems.at(0).text()).toEqual(expectedDiagnoses[0].name) + }) + + it('should render a warning message if the patient does not have any diagnoses', async () => { + const { wrapper } = await setup([]) + + const alert = wrapper.find(Alert) + + expect(wrapper.exists(Alert)).toBeTruthy() + expect(wrapper.exists(List)).toBeFalsy() + + expect(alert.prop('color')).toEqual('warning') + expect(alert.prop('title')).toEqual('patient.diagnoses.warning.noDiagnoses') + expect(alert.prop('message')).toEqual('patient.diagnoses.addDiagnosisAbove') + }) +}) diff --git a/src/__tests__/patients/edit/EditPatient.test.tsx b/src/__tests__/patients/edit/EditPatient.test.tsx index da394a5c40..a49d95261c 100644 --- a/src/__tests__/patients/edit/EditPatient.test.tsx +++ b/src/__tests__/patients/edit/EditPatient.test.tsx @@ -8,7 +8,7 @@ import { Router, Route } from 'react-router-dom' import createMockStore, { MockStore } from 'redux-mock-store' import thunk from 'redux-thunk' -import * as titleUtil from '../../../page-header/title/useTitle' +import * as titleUtil from '../../../page-header/title/TitleContext' import EditPatient from '../../../patients/edit/EditPatient' import GeneralInformation from '../../../patients/GeneralInformation' import * as patientSlice from '../../../patients/patient-slice' @@ -42,6 +42,7 @@ describe('Edit Patient', () => { let store: MockStore const setup = () => { + jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) jest.spyOn(PatientRepository, 'saveOrUpdate').mockResolvedValue(patient) jest.spyOn(PatientRepository, 'find').mockResolvedValue(patient) @@ -53,12 +54,15 @@ describe('Edit Patient', () => { - + + + , ) + wrapper.find(EditPatient).props().updateTitle = jest.fn() wrapper.update() return wrapper } @@ -67,6 +71,13 @@ describe('Edit Patient', () => { jest.restoreAllMocks() }) + it('should have called the useUpdateTitle hook', async () => { + await act(async () => { + await setup() + }) + expect(titleUtil.useUpdateTitle).toHaveBeenCalled() + }) + it('should render an edit patient form', async () => { let wrapper: any await act(async () => { @@ -86,16 +97,6 @@ describe('Edit Patient', () => { expect(store.getActions()).toContainEqual(patientSlice.fetchPatientSuccess(patient)) }) - it('should use "Edit Patient: " plus patient full name as the title', async () => { - jest.spyOn(titleUtil, 'default') - await act(async () => { - await setup() - }) - expect(titleUtil.default).toHaveBeenCalledWith( - 'patients.editPatient: givenName familyName suffix (P00001)', - ) - }) - it('should dispatch updatePatient when save button is clicked', async () => { let wrapper: any await act(async () => { diff --git a/src/__tests__/patients/hooks/useAddCareGoal.test.tsx b/src/__tests__/patients/hooks/useAddCareGoal.test.tsx new file mode 100644 index 0000000000..be5885447e --- /dev/null +++ b/src/__tests__/patients/hooks/useAddCareGoal.test.tsx @@ -0,0 +1,74 @@ +import useAddCareGoal from '../../../patients/hooks/useAddCareGoal' +import { CareGoalError } from '../../../patients/util/validate-caregoal' +import * as validateCareGoal from '../../../patients/util/validate-caregoal' +import PatientRepository from '../../../shared/db/PatientRepository' +import CareGoal, { CareGoalStatus, CareGoalAchievementStatus } from '../../../shared/model/CareGoal' +import Patient from '../../../shared/model/Patient' +import * as uuid from '../../../shared/util/uuid' +import executeMutation from '../../test-utils/use-mutation.util' + +describe('use add care goal', () => { + beforeEach(() => { + jest.resetAllMocks() + }) + + it('should add a care goal to the patient', async () => { + const expectedDate = new Date() + Date.now = jest.fn().mockReturnValue(expectedDate) + + const expectedCareGoal: CareGoal = { + id: '123', + description: 'some description', + priority: 'medium', + status: CareGoalStatus.Accepted, + achievementStatus: CareGoalAchievementStatus.Improving, + startDate: new Date().toISOString(), + dueDate: new Date().toISOString(), + note: 'some note', + createdOn: expectedDate.toISOString(), + } + + const givenPatient = { + id: '123', + givenName: 'given name', + fullName: 'full name', + careGoals: [] as CareGoal[], + } as Patient + + const expectedPatient = { + ...givenPatient, + careGoals: [expectedCareGoal], + } as Patient + + jest.spyOn(PatientRepository, 'find').mockResolvedValue(givenPatient) + jest.spyOn(uuid, 'uuid').mockReturnValue(expectedCareGoal.id) + jest.spyOn(PatientRepository, 'saveOrUpdate').mockResolvedValue(expectedPatient) + + const result = await executeMutation(() => useAddCareGoal(), { + patientId: givenPatient.id, + careGoal: expectedCareGoal, + }) + + expect(PatientRepository.find).toHaveBeenCalledTimes(1) + expect(PatientRepository.saveOrUpdate).toHaveBeenCalledTimes(1) + expect(PatientRepository.saveOrUpdate).toHaveBeenCalledWith(expectedPatient) + expect(result).toEqual([expectedCareGoal]) + }) + + it('should throw an error if validation fails', async () => { + const expectedError = { + message: 'patient.careGoal.error.unableToAdd', + description: 'some error', + } + jest.spyOn(validateCareGoal, 'default').mockReturnValue(expectedError as CareGoalError) + jest.spyOn(PatientRepository, 'saveOrUpdate') + + try { + await executeMutation(() => useAddCareGoal(), { patientId: '123', careGoal: {} }) + } catch (e) { + expect(e).toEqual(expectedError) + } + + expect(PatientRepository.saveOrUpdate).not.toHaveBeenCalled() + }) +}) diff --git a/src/__tests__/patients/hooks/useAddPatientDiagnosis.test.tsx b/src/__tests__/patients/hooks/useAddPatientDiagnosis.test.tsx new file mode 100644 index 0000000000..c234ae3144 --- /dev/null +++ b/src/__tests__/patients/hooks/useAddPatientDiagnosis.test.tsx @@ -0,0 +1,62 @@ +/* eslint-disable no-console */ + +import useAddPatientDiagnosis from '../../../patients/hooks/useAddPatientDiagnosis' +import * as validateDiagnosis from '../../../patients/util/validate-diagnosis' +import PatientRepository from '../../../shared/db/PatientRepository' +import Diagnosis, { DiagnosisStatus } from '../../../shared/model/Diagnosis' +import Patient from '../../../shared/model/Patient' +import * as uuid from '../../../shared/util/uuid' +import executeMutation from '../../test-utils/use-mutation.util' + +describe('use add diagnosis', () => { + beforeEach(() => { + jest.resetAllMocks() + console.error = jest.fn() + }) + + it('should throw an error if diagnosis validation fails', async () => { + const expectedError = { name: 'some error' } + jest.spyOn(validateDiagnosis, 'default').mockReturnValue(expectedError) + jest.spyOn(PatientRepository, 'saveOrUpdate') + + try { + await executeMutation(() => useAddPatientDiagnosis(), { patientId: '123', note: {} }) + } catch (e) { + expect(e).toEqual(expectedError) + } + + expect(PatientRepository.saveOrUpdate).not.toHaveBeenCalled() + }) + + it('should add the diaganosis to the patient', async () => { + const expectedDiagnosis: Diagnosis = { + id: '123', + name: 'New Diagnosis Name', + diagnosisDate: new Date().toISOString(), + onsetDate: new Date().toISOString(), + abatementDate: new Date().toISOString(), + status: DiagnosisStatus.Active, + visit: '1234', + note: 'some note', + } + const givenPatient = { id: 'patientId', diagnoses: [] as Diagnosis[] } as Patient + jest.spyOn(uuid, 'uuid').mockReturnValue(expectedDiagnosis.id) + const expectedPatient = { ...givenPatient, diagnoses: [expectedDiagnosis] } + jest.spyOn(PatientRepository, 'saveOrUpdate').mockResolvedValue(expectedPatient) + jest.spyOn(PatientRepository, 'find').mockResolvedValue(givenPatient) + + const result = await executeMutation(() => useAddPatientDiagnosis(), { + patientId: givenPatient.id, + diagnosis: expectedDiagnosis, + }) + + expect(PatientRepository.find).toHaveBeenCalledTimes(1) + expect(PatientRepository.saveOrUpdate).toHaveBeenCalledTimes(1) + expect(PatientRepository.saveOrUpdate).toHaveBeenCalledWith( + expect.objectContaining({ + diagnoses: [expect.objectContaining({ name: 'New Diagnosis Name' })], + }), + ) + expect(result).toEqual([expectedDiagnosis]) + }) +}) diff --git a/src/__tests__/patients/hooks/useAddPatientNote.test.ts b/src/__tests__/patients/hooks/useAddPatientNote.test.ts new file mode 100644 index 0000000000..5444799dcd --- /dev/null +++ b/src/__tests__/patients/hooks/useAddPatientNote.test.ts @@ -0,0 +1,49 @@ +/* eslint-disable no-console */ + +import useAddPatientNote from '../../../patients/hooks/useAddPatientNote' +import * as validateNote from '../../../patients/util/validate-note' +import PatientRepository from '../../../shared/db/PatientRepository' +import Note from '../../../shared/model/Note' +import Patient from '../../../shared/model/Patient' +import * as uuid from '../../../shared/util/uuid' +import executeMutation from '../../test-utils/use-mutation.util' + +describe('use add note', () => { + beforeEach(() => { + jest.resetAllMocks() + console.error = jest.fn() + }) + + it('should throw an error if note validation fails', async () => { + const expectedError = { nameError: 'some error' } + jest.spyOn(validateNote, 'default').mockReturnValue(expectedError) + jest.spyOn(PatientRepository, 'saveOrUpdate') + + try { + await executeMutation(() => useAddPatientNote(), { patientId: '123', note: {} }) + } catch (e) { + expect(e).toEqual(expectedError) + } + + expect(PatientRepository.saveOrUpdate).not.toHaveBeenCalled() + }) + + it('should add the note to the patient', async () => { + const expectedNote = { id: '456', text: 'eome name', date: '1947-09-09T14:48:00.000Z' } + const givenPatient = { id: 'patientId', notes: [] as Note[] } as Patient + jest.spyOn(uuid, 'uuid').mockReturnValue(expectedNote.id) + const expectedPatient = { ...givenPatient, notes: [expectedNote] } + jest.spyOn(PatientRepository, 'saveOrUpdate').mockResolvedValue(expectedPatient) + jest.spyOn(PatientRepository, 'find').mockResolvedValue(givenPatient) + + const result = await executeMutation(() => useAddPatientNote(), { + patientId: givenPatient.id, + note: expectedNote, + }) + + expect(PatientRepository.find).toHaveBeenCalledTimes(1) + expect(PatientRepository.saveOrUpdate).toHaveBeenCalledTimes(1) + expect(PatientRepository.saveOrUpdate).toHaveBeenCalledWith(expectedPatient) + expect(result).toEqual([expectedNote]) + }) +}) diff --git a/src/__tests__/patients/hooks/useAddPatientRelatedPerson.test.tsx b/src/__tests__/patients/hooks/useAddPatientRelatedPerson.test.tsx new file mode 100644 index 0000000000..6857a00815 --- /dev/null +++ b/src/__tests__/patients/hooks/useAddPatientRelatedPerson.test.tsx @@ -0,0 +1,66 @@ +import useAddPatientRelatedPerson from '../../../patients/hooks/useAddPatientRelatedPerson' +import * as validateRelatedPerson from '../../../patients/util/validate-related-person' +import PatientRepository from '../../../shared/db/PatientRepository' +import Patient from '../../../shared/model/Patient' +import RelatedPerson from '../../../shared/model/RelatedPerson' +import * as uuid from '../../../shared/util/uuid' +import executeMutation from '../../test-utils/use-mutation.util' + +describe('use add patient related person', () => { + beforeEach(() => { + jest.resetAllMocks() + }) + + it('should throw an error if related person not specified', async () => { + const expectedError = { relatedPersonError: 'some error' } + jest.spyOn(validateRelatedPerson, 'default').mockReturnValue(expectedError) + jest.spyOn(PatientRepository, 'saveOrUpdate') + + try { + await executeMutation(() => useAddPatientRelatedPerson(), { + patientId: '123', + relatedPerson: {}, + }) + } catch (e) { + expect(e).toEqual(expectedError) + } + + expect(PatientRepository.saveOrUpdate).not.toHaveBeenCalled() + }) + + it('should throw an error if the relation type is not specified', async () => { + const expectedError = { relationshipTypeError: 'some error' } + jest.spyOn(validateRelatedPerson, 'default').mockReturnValue(expectedError) + jest.spyOn(PatientRepository, 'saveOrUpdate') + + try { + await executeMutation(() => useAddPatientRelatedPerson(), { + patientId: '123', + relatedPerson: { patientId: '456' }, + }) + } catch (e) { + expect(e).toEqual(expectedError) + } + + expect(PatientRepository.saveOrUpdate).not.toHaveBeenCalled() + }) + + it('should add the related person to the patient', async () => { + const expectedRelated = { id: '123', patientId: '456', type: 'some type' } as RelatedPerson + const givenPatient = { id: 'patientId', relatedPersons: [] as RelatedPerson[] } as Patient + const expectedPatient = { ...givenPatient, relatedPersons: [expectedRelated] } as Patient + jest.spyOn(uuid, 'uuid').mockReturnValue(expectedRelated.id) + jest.spyOn(PatientRepository, 'saveOrUpdate').mockResolvedValue(expectedPatient) + jest.spyOn(PatientRepository, 'find').mockResolvedValue(givenPatient) + + const result = await executeMutation(() => useAddPatientRelatedPerson(), { + patientId: givenPatient.id, + relatedPerson: expectedRelated, + }) + + expect(PatientRepository.find).toHaveBeenCalledTimes(2) // Once looking up the patient, once looking up the related person to cache + expect(PatientRepository.saveOrUpdate).toHaveBeenCalledTimes(1) + expect(PatientRepository.saveOrUpdate).toHaveBeenCalledWith(expectedPatient) + expect(result).toEqual([expectedRelated]) + }) +}) diff --git a/src/__tests__/patients/hooks/useAddPatientVisit.test.tsx b/src/__tests__/patients/hooks/useAddPatientVisit.test.tsx new file mode 100644 index 0000000000..091df6fa18 --- /dev/null +++ b/src/__tests__/patients/hooks/useAddPatientVisit.test.tsx @@ -0,0 +1,61 @@ +import useAddVisit from '../../../patients/hooks/useAddVisit' +import * as validateVisit from '../../../patients/util/validate-visit' +import PatientRepository from '../../../shared/db/PatientRepository' +import Patient from '../../../shared/model/Patient' +import Visit from '../../../shared/model/Visit' +import executeMutation from '../../test-utils/use-mutation.util' + +describe('use add visit', () => { + beforeEach(() => { + jest.restoreAllMocks() + }) + + it('should add a visit to the patient', async () => { + const expectedDate = new Date() + Date.now = jest.fn().mockReturnValue(expectedDate) + + const expectedVisit: Visit[] = [ + { + id: '123', + reason: 'reason for visit', + createdAt: expectedDate.toISOString(), + updatedAt: expectedDate.toISOString(), + startDateTime: new Date().toISOString(), + endDateTime: new Date().toISOString(), + type: 'type', + status: 'planned', + reason: 'given reason', + location: 'give location', + }, + ] + const givenPatient = { id: 'patientId' } as Patient + + const expectedPatient = { ...givenPatient, visits: expectedVisit } + jest.spyOn(PatientRepository, 'saveOrUpdate').mockResolvedValue(expectedPatient) + jest.spyOn(PatientRepository, 'find').mockResolvedValue(givenPatient) + + const result = await executeMutation(() => useAddVisit(), { + patientId: givenPatient.id, + visit: expectedVisit[0], + }) + + expect(PatientRepository.find).toHaveBeenCalledTimes(1) + expect(PatientRepository.saveOrUpdate).toHaveBeenCalledTimes(1) + expect(PatientRepository.saveOrUpdate).toHaveBeenCalledWith(expectedPatient) + expect(result).toEqual(expectedVisit) + }) + + it('should throw an error if validation fails', async () => { + const expectedError = { message: 'patient.visit.error.unableToAdd', title: 'some error' } + jest.spyOn(validateVisit, 'default').mockReturnValue(expectedError) + jest.spyOn(PatientRepository, 'saveOrUpdate') + + try { + await executeMutation(() => useAddVisit(), { patientId: '123', visit: {} }) + } catch (e) { + expect(e).toEqual(expectedError) + } + + expect(PatientRepository.saveOrUpdate).not.toHaveBeenCalled() + }) +}) diff --git a/src/__tests__/patients/hooks/usePatientAppointments.test.tsx b/src/__tests__/patients/hooks/usePatientAppointments.test.tsx new file mode 100644 index 0000000000..f2dee0a559 --- /dev/null +++ b/src/__tests__/patients/hooks/usePatientAppointments.test.tsx @@ -0,0 +1,39 @@ +import usePatientAppointments from '../../../patients/hooks/usePatientAppointments' +import PatientRepository from '../../../shared/db/PatientRepository' +import Appointment from '../../shared/model/Appointment' +import executeQuery from '../../test-utils/use-query.util' + +describe('use patient appointments', () => { + it(`should return the should return the patient's appointments`, async () => { + const expectedPatientId = '123' + + const expectedAppointments = [ + { + id: '456', + rev: '1', + patient: expectedPatientId, + startDateTime: new Date(2020, 1, 1, 9, 0, 0, 0).toISOString(), + endDateTime: new Date(2020, 1, 1, 9, 30, 0, 0).toISOString(), + location: 'location', + reason: 'Follow Up', + }, + { + id: '123', + rev: '1', + patient: expectedPatientId, + startDateTime: new Date(2020, 1, 1, 8, 0, 0, 0).toISOString(), + endDateTime: new Date(2020, 1, 1, 8, 30, 0, 0).toISOString(), + location: 'location', + reason: 'Checkup', + }, + ] as Appointment[] + + jest.spyOn(PatientRepository, 'getAppointments').mockResolvedValueOnce(expectedAppointments) + + const actualAppointments = await executeQuery(() => usePatientAppointments(expectedPatientId)) + + expect(PatientRepository.getAppointments).toHaveBeenCalledTimes(1) + expect(PatientRepository.getAppointments).toHaveBeenCalledWith(expectedPatientId) + expect(actualAppointments).toEqual(expectedAppointments) + }) +}) diff --git a/src/__tests__/patients/hooks/usePatientLabs.test.tsx b/src/__tests__/patients/hooks/usePatientLabs.test.tsx new file mode 100644 index 0000000000..b60a3d410a --- /dev/null +++ b/src/__tests__/patients/hooks/usePatientLabs.test.tsx @@ -0,0 +1,18 @@ +import usePatientLabs from '../../../patients/hooks/usePatientLabs' +import PatientRepository from '../../../shared/db/PatientRepository' +import Lab from '../../../shared/model/Lab' +import executeQuery from '../../test-utils/use-query.util' + +describe('use patient labs', () => { + it('should get patient labs', async () => { + const expectedPatientId = '123' + const expectedLabs = ([{ id: expectedPatientId, type: 'lab type' }] as unknown) as Lab[] + jest.spyOn(PatientRepository, 'getLabs').mockResolvedValueOnce(expectedLabs) + + const actualLabs = await executeQuery(() => usePatientLabs(expectedPatientId)) + + expect(PatientRepository.getLabs).toHaveBeenCalledTimes(1) + expect(PatientRepository.getLabs).toHaveBeenCalledWith(expectedPatientId) + expect(actualLabs).toEqual(expectedLabs) + }) +}) diff --git a/src/__tests__/patients/hooks/usePatientNote.test.ts b/src/__tests__/patients/hooks/usePatientNote.test.ts new file mode 100644 index 0000000000..acfbe68f6d --- /dev/null +++ b/src/__tests__/patients/hooks/usePatientNote.test.ts @@ -0,0 +1,42 @@ +/* eslint-disable no-console */ + +import usePatientNote from '../../../patients/hooks/usePatientNote' +import PatientRepository from '../../../shared/db/PatientRepository' +import Patient from '../../../shared/model/Patient' +import executeQuery from '../../test-utils/use-query.util' + +describe('use note', () => { + beforeEach(() => { + jest.resetAllMocks() + console.error = jest.fn() + }) + + it('should return a note given a patient id and note id', async () => { + const expectedPatientId = '123' + const expectedNote = { id: '456', text: 'eome name', date: '1947-09-09T14:48:00.000Z' } + const expectedPatient = { id: expectedPatientId, notes: [expectedNote] } as Patient + jest.spyOn(PatientRepository, 'find').mockResolvedValueOnce(expectedPatient) + + const actualNote = await executeQuery(() => usePatientNote(expectedPatientId, expectedNote.id)) + + expect(PatientRepository.find).toHaveBeenCalledTimes(1) + expect(PatientRepository.find).toHaveBeenCalledWith(expectedPatientId) + expect(actualNote).toEqual(expectedNote) + }) + + it('should throw an error if patient does not have note with id', async () => { + const expectedPatientId = '123' + const expectedNote = { id: '456', text: 'eome name', date: '1947-09-09T14:48:00.000Z' } + const expectedPatient = { + id: expectedPatientId, + notes: [{ id: '426', text: 'eome name', date: '1947-09-09T14:48:00.000Z' }], + } as Patient + jest.spyOn(PatientRepository, 'find').mockResolvedValueOnce(expectedPatient) + + try { + await executeQuery(() => usePatientNote(expectedPatientId, expectedNote.id)) + } catch (e) { + expect(e).toEqual(new Error('Timed out in waitFor after 1000ms.')) + } + }) +}) diff --git a/src/__tests__/patients/hooks/usePatientNotes.test.ts b/src/__tests__/patients/hooks/usePatientNotes.test.ts new file mode 100644 index 0000000000..e916de88e7 --- /dev/null +++ b/src/__tests__/patients/hooks/usePatientNotes.test.ts @@ -0,0 +1,21 @@ +import usePatientNotes from '../../../patients/hooks/usePatientNotes' +import PatientRepository from '../../../shared/db/PatientRepository' +import Patient from '../../../shared/model/Patient' +import executeQuery from '../../test-utils/use-query.util' + +describe('use patient notes', () => { + it('should get patient notes', async () => { + const expectedPatientId = '123' + + const expectedNotes = [{ id: '456', text: 'eome name', date: '1947-09-09T14:48:00.000Z' }] + jest.spyOn(PatientRepository, 'find').mockResolvedValueOnce({ + id: expectedPatientId, + notes: expectedNotes, + } as Patient) + + const actualNotes = await executeQuery(() => usePatientNotes(expectedPatientId)) + + expect(PatientRepository.find).toHaveBeenCalledWith(expectedPatientId) + expect(actualNotes).toEqual(expectedNotes) + }) +}) diff --git a/src/__tests__/patients/hooks/usePatientVisits.test.tsx b/src/__tests__/patients/hooks/usePatientVisits.test.tsx new file mode 100644 index 0000000000..9bbdaa159e --- /dev/null +++ b/src/__tests__/patients/hooks/usePatientVisits.test.tsx @@ -0,0 +1,26 @@ +import usePatientVisits from '../../../patients/hooks/usePatientVisits' +import PatientRepository from '../../../shared/db/PatientRepository' +import Patient from '../../../shared/model/Patient' +import executeQuery from '../../test-utils/use-query.util' + +describe('use patient visits', () => { + it(`should return the should return the patient's visits`, async () => { + const expectedPatientId = '123' + + const patient = { + id: expectedPatientId, + visits: [ + { id: '123', reason: 'reason for visit' }, + { id: '124', reason: 'visit for reason' }, + ], + } as Patient + + jest.spyOn(PatientRepository, 'find').mockResolvedValueOnce(patient) + + const actualVisits = await executeQuery(() => usePatientVisits(expectedPatientId)) + + expect(PatientRepository.find).toHaveBeenCalledTimes(1) + expect(PatientRepository.find).toHaveBeenCalledWith(expectedPatientId) + expect(actualVisits).toEqual(patient.visits) + }) +}) diff --git a/src/__tests__/patients/hooks/useRemovePatientRelatedPerson.test.tsx b/src/__tests__/patients/hooks/useRemovePatientRelatedPerson.test.tsx new file mode 100644 index 0000000000..5bf8bcfe28 --- /dev/null +++ b/src/__tests__/patients/hooks/useRemovePatientRelatedPerson.test.tsx @@ -0,0 +1,48 @@ +import useRemovePatientRelatedPerson from '../../../patients/hooks/useRemovePatientRelatedPerson' +import PatientRepository from '../../../shared/db/PatientRepository' +import Patient from '../../../shared/model/Patient' +import RelatedPerson from '../../../shared/model/RelatedPerson' +import * as uuid from '../../../shared/util/uuid' +import executeMutation from '../../test-utils/use-mutation.util' + +describe('use remove patient related person', () => { + beforeEach(() => { + jest.resetAllMocks() + }) + + it('should remove a related person with the given id', async () => { + const expectedRelatedPersonPatientId = 'expected id' + const expectedPatientId = '123' + + const expectedRelatedPerson = { + id: 'some id', + patientId: expectedRelatedPersonPatientId, + type: 'some type', + } as RelatedPerson + + const expectedPatient = { + id: expectedPatientId, + givenName: 'some name', + relatedPersons: [expectedRelatedPerson], + } as Patient + + const expectedUpdatedPatient = { + ...expectedPatient, + relatedPersons: [], + } as Patient + + jest.spyOn(PatientRepository, 'find').mockResolvedValue(expectedPatient) + jest.spyOn(uuid, 'uuid').mockReturnValue(expectedRelatedPersonPatientId) + jest.spyOn(PatientRepository, 'saveOrUpdate').mockResolvedValue(expectedUpdatedPatient) + + const result = await executeMutation(() => useRemovePatientRelatedPerson(), { + patientId: expectedPatientId, + relatedPersonId: expectedRelatedPersonPatientId, + }) + + expect(PatientRepository.find).toHaveBeenCalledTimes(1) + expect(PatientRepository.saveOrUpdate).toHaveBeenCalledTimes(1) + expect(PatientRepository.saveOrUpdate).toHaveBeenCalledWith(expectedUpdatedPatient) + expect(result).toEqual([]) + }) +}) diff --git a/src/__tests__/patients/hooks/useVisit.test.tsx b/src/__tests__/patients/hooks/useVisit.test.tsx new file mode 100644 index 0000000000..0eff9de4af --- /dev/null +++ b/src/__tests__/patients/hooks/useVisit.test.tsx @@ -0,0 +1,26 @@ +import useVisit from '../../../patients/hooks/useVisit' +import PatientRepository from '../../../shared/db/PatientRepository' +import Patient from '../../../shared/model/Patient' +import executeQuery from '../../test-utils/use-query.util' + +describe('use visit', () => { + it(`should return the should return the patient's visit`, async () => { + const expectedPatientId = '123' + + const patient = { + id: expectedPatientId, + visits: [ + { id: '123', reason: 'reason for visit' }, + { id: '124', reason: 'visit for reason' }, + ], + } as Patient + + jest.spyOn(PatientRepository, 'find').mockResolvedValueOnce(patient) + + const actualVisit = await executeQuery(() => useVisit(expectedPatientId, '123')) + + expect(PatientRepository.find).toHaveBeenCalledTimes(1) + expect(PatientRepository.find).toHaveBeenCalledWith(expectedPatientId) + expect(actualVisit).toEqual(patient.visits[0]) + }) +}) diff --git a/src/__tests__/patients/labs/Labs.test.tsx b/src/__tests__/patients/labs/Labs.test.tsx new file mode 100644 index 0000000000..8959d4deb4 --- /dev/null +++ b/src/__tests__/patients/labs/Labs.test.tsx @@ -0,0 +1,61 @@ +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 Labs from '../../../patients/labs/Labs' +import LabsList from '../../../patients/labs/LabsList' +import PatientRepository from '../../../shared/db/PatientRepository' +import Patient from '../../../shared/model/Patient' +import Permissions from '../../../shared/model/Permissions' +import { RootState } from '../../../shared/store' + +const mockStore = createMockStore([thunk]) +const history = createMemoryHistory() +const expectedPatient = ({ + id: '123', + rev: '123', + labs: [ + { id: '1', type: 'lab type 1' }, + { id: '2', type: 'lab type 2' }, + ], +} as unknown) as Patient + +let store: any + +const setup = async ( + patient = expectedPatient, + permissions = [Permissions.ViewLabs], + route = '/patients/123/labs', +) => { + store = mockStore({ patient: { patient }, user: { permissions } } as any) + history.push(route) + + const wrapper = await mount( + + + + + , + ) + return { wrapper: wrapper as ReactWrapper } +} + +describe('Labs', () => { + beforeEach(() => { + jest.resetAllMocks() + jest.spyOn(PatientRepository, 'find').mockResolvedValue(expectedPatient) + jest.spyOn(PatientRepository, 'saveOrUpdate') + }) + + describe('patient labs list', () => { + it('should render patient labs', async () => { + const { wrapper } = await setup() + + expect(wrapper.exists(LabsList)).toBeTruthy() + }) + }) +}) diff --git a/src/__tests__/patients/labs/LabsList.test.tsx b/src/__tests__/patients/labs/LabsList.test.tsx new file mode 100644 index 0000000000..959ddf8edb --- /dev/null +++ b/src/__tests__/patients/labs/LabsList.test.tsx @@ -0,0 +1,118 @@ +import * as components from '@hospitalrun/components' +import { Table } 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 LabsList from '../../../patients/labs/LabsList' +import PatientRepository from '../../../shared/db/PatientRepository' +import Lab from '../../../shared/model/Lab' +import Patient from '../../../shared/model/Patient' +import { RootState } from '../../../shared/store' + +const expectedPatient = { + id: '1234', +} as Patient + +const expectedLabs = [ + { + id: '456', + rev: '1', + patient: '1234', + requestedOn: new Date(2020, 1, 1, 9, 0, 0, 0).toISOString(), + requestedBy: 'someone', + type: 'lab type', + }, + { + id: '123', + rev: '1', + patient: '1234', + requestedOn: new Date(2020, 1, 1, 9, 0, 0, 0).toISOString(), + requestedBy: 'someone', + type: 'lab type', + }, +] as Lab[] + +const mockStore = createMockStore([thunk]) +const history = createMemoryHistory() + +let store: any + +const setup = async (patient = expectedPatient, labs = expectedLabs) => { + jest.resetAllMocks() + jest.spyOn(PatientRepository, 'getLabs').mockResolvedValue(labs) + store = mockStore({ patient, labs: { labs } } as any) + + let wrapper: any + + await act(async () => { + wrapper = await mount( + + + + + , + ) + }) + + wrapper.update() + + return { wrapper: wrapper as ReactWrapper } +} + +describe('LabsList', () => { + describe('Table', () => { + it('should render a list of labs', async () => { + const { wrapper } = await setup() + + const table = wrapper.find(Table) + + const columns = table.prop('columns') + const actions = table.prop('actions') as any + + expect(table).toHaveLength(1) + + expect(columns[0]).toEqual(expect.objectContaining({ label: 'labs.lab.type', key: 'type' })) + expect(columns[1]).toEqual( + expect.objectContaining({ label: 'labs.lab.requestedOn', key: 'requestedOn' }), + ) + expect(columns[2]).toEqual( + expect.objectContaining({ + label: 'labs.lab.status', + key: 'status', + }), + ) + expect(actions[0]).toEqual(expect.objectContaining({ label: 'actions.view' })) + expect(table.prop('actionsHeaderText')).toEqual('actions.label') + expect(table.prop('data')).toEqual(expectedLabs) + }) + + it('should navigate to lab view on lab click', async () => { + const { wrapper } = await setup() + const tr = wrapper.find('tr').at(1) + + act(() => { + const onClick = tr.find('button').at(0).prop('onClick') as any + onClick({ stopPropagation: jest.fn() }) + }) + + expect(history.location.pathname).toEqual('/labs/456') + }) + }) + + describe('no patient labs', () => { + it('should render a warning message if there are no labs', async () => { + const { wrapper } = await setup(expectedPatient, []) + const alert = wrapper.find(components.Alert) + + expect(alert).toHaveLength(1) + expect(alert.prop('title')).toEqual('patient.labs.warning.noLabs') + expect(alert.prop('message')).toEqual('patient.labs.noLabsMessage') + }) + }) +}) diff --git a/src/__tests__/patients/labs/LabsTab.test.tsx b/src/__tests__/patients/labs/LabsTab.test.tsx deleted file mode 100644 index bd7d7fa884..0000000000 --- a/src/__tests__/patients/labs/LabsTab.test.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import * as components from '@hospitalrun/components' -import { Table } 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 LabsTab from '../../../patients/labs/LabsTab' -import PatientRepository from '../../../shared/db/PatientRepository' -import Lab from '../../../shared/model/Lab' -import Patient from '../../../shared/model/Patient' -import Permissions from '../../../shared/model/Permissions' -import { RootState } from '../../../shared/store' - -const expectedPatient = { - id: '123', -} as Patient - -const expectedLabs = [ - { - id: 'labId', - patient: '123', - type: 'type', - status: 'requested', - requestedOn: new Date().toISOString(), - } as Lab, -] - -const mockStore = createMockStore([thunk]) -const history = createMemoryHistory() - -let user: any -let store: any - -const setup = async (labs = expectedLabs) => { - jest.resetAllMocks() - user = { permissions: [Permissions.ReadPatients] } - store = mockStore({ patient: expectedPatient, user } as any) - jest.spyOn(PatientRepository, 'getLabs').mockResolvedValue(labs) - - let wrapper: any - await act(async () => { - wrapper = await mount( - - - - - , - ) - }) - - wrapper.update() - return { wrapper: wrapper as ReactWrapper } -} - -describe('Labs Tab', () => { - it('should list the patients labs', async () => { - const { wrapper } = await setup() - - const table = wrapper.find(Table) - const columns = table.prop('columns') - const actions = table.prop('actions') as any - expect(columns[0]).toEqual(expect.objectContaining({ label: 'labs.lab.type', key: 'type' })) - expect(columns[1]).toEqual( - expect.objectContaining({ label: 'labs.lab.requestedOn', key: 'requestedOn' }), - ) - expect(columns[2]).toEqual( - expect.objectContaining({ - label: 'labs.lab.status', - key: 'status', - }), - ) - - expect(actions[0]).toEqual(expect.objectContaining({ label: 'actions.view' })) - expect(table.prop('actionsHeaderText')).toEqual('actions.label') - expect(table.prop('data')).toEqual(expectedLabs) - }) - - it('should render a warning message if the patient does not have any labs', async () => { - const { wrapper } = await setup([]) - - const alert = wrapper.find(components.Alert) - - expect(alert).toHaveLength(1) - expect(alert.prop('title')).toEqual('patient.labs.warning.noLabs') - expect(alert.prop('message')).toEqual('patient.labs.noLabsMessage') - }) -}) diff --git a/src/__tests__/patients/new/NewPatient.test.tsx b/src/__tests__/patients/new/NewPatient.test.tsx index 57c4979560..7fa6adc287 100644 --- a/src/__tests__/patients/new/NewPatient.test.tsx +++ b/src/__tests__/patients/new/NewPatient.test.tsx @@ -9,7 +9,7 @@ import createMockStore, { MockStore } from 'redux-mock-store' import thunk from 'redux-thunk' import { mocked } from 'ts-jest/utils' -import * as titleUtil from '../../../page-header/title/useTitle' +import * as titleUtil from '../../../page-header/title/TitleContext' import GeneralInformation from '../../../patients/GeneralInformation' import NewPatient from '../../../patients/new/NewPatient' import * as patientSlice from '../../../patients/patient-slice' @@ -17,6 +17,7 @@ import PatientRepository from '../../../shared/db/PatientRepository' import Patient from '../../../shared/model/Patient' import { RootState } from '../../../shared/store' +const { TitleProvider } = titleUtil const mockStore = createMockStore([thunk]) describe('New Patient', () => { @@ -29,6 +30,7 @@ describe('New Patient', () => { let store: MockStore const setup = (error?: any) => { + jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) jest.spyOn(PatientRepository, 'save') const mockedPatientRepository = mocked(PatientRepository, true) mockedPatientRepository.save.mockResolvedValue(patient) @@ -41,7 +43,9 @@ describe('New Patient', () => { - + + + , @@ -64,15 +68,6 @@ describe('New Patient', () => { expect(wrapper.find(GeneralInformation)).toHaveLength(1) }) - it('should use "New Patient" as the header', async () => { - jest.spyOn(titleUtil, 'default') - await act(async () => { - await setup() - }) - - expect(titleUtil.default).toHaveBeenCalledWith('patients.newPatient') - }) - it('should pass the error object to general information', async () => { const expectedError = { message: 'some message' } let wrapper: any diff --git a/src/__tests__/patients/notes/NewNoteModal.test.tsx b/src/__tests__/patients/notes/NewNoteModal.test.tsx index e71cf5f5ca..59cf0c35a9 100644 --- a/src/__tests__/patients/notes/NewNoteModal.test.tsx +++ b/src/__tests__/patients/notes/NewNoteModal.test.tsx @@ -1,41 +1,42 @@ +/* eslint-disable no-console */ + import { Alert, Modal } from '@hospitalrun/components' import { act } from '@testing-library/react' import { mount } from 'enzyme' import React from 'react' -import { Provider } from 'react-redux' -import createMockStore from 'redux-mock-store' -import thunk from 'redux-thunk' import NewNoteModal from '../../../patients/notes/NewNoteModal' -import * as patientSlice from '../../../patients/patient-slice' import TextFieldWithLabelFormGroup from '../../../shared/components/input/TextFieldWithLabelFormGroup' import PatientRepository from '../../../shared/db/PatientRepository' import Patient from '../../../shared/model/Patient' -import { RootState } from '../../../shared/store' - -const mockStore = createMockStore([thunk]) describe('New Note Modal', () => { + const mockPatient = { + id: '123', + givenName: 'someName', + } as Patient + + const setup = (onCloseSpy = jest.fn()) => { + jest.spyOn(PatientRepository, 'saveOrUpdate').mockResolvedValue(mockPatient) + jest.spyOn(PatientRepository, 'find').mockResolvedValue(mockPatient) + const wrapper = mount( + , + ) + return { wrapper } + } + beforeEach(() => { - jest.spyOn(PatientRepository, 'find') - jest.spyOn(PatientRepository, 'saveOrUpdate') + console.error = jest.fn() }) it('should render a modal with the correct labels', () => { - const expectedPatient = { - id: '1234', - givenName: 'some name', - } - const store = mockStore({ - patient: { - patient: expectedPatient, - }, - } as any) - const wrapper = mount( - - - , - ) + const { wrapper } = setup() + const modal = wrapper.find(Modal) expect(modal).toHaveLength(1) expect(modal.prop('title')).toEqual('patient.notes.new') @@ -47,20 +48,7 @@ describe('New Note Modal', () => { }) it('should render a notes rich text editor', () => { - const expectedPatient = { - id: '1234', - givenName: 'some name', - } - const store = mockStore({ - patient: { - patient: expectedPatient, - }, - } as any) - const wrapper = mount( - - - , - ) + const { wrapper } = setup() const noteTextField = wrapper.find(TextFieldWithLabelFormGroup) expect(noteTextField.prop('label')).toEqual('patient.note') @@ -68,29 +56,22 @@ describe('New Note Modal', () => { expect(noteTextField).toHaveLength(1) }) - it('should render note error', () => { - const expectedPatient = { - id: '1234', - givenName: 'some name', - } + it('should render note error', async () => { const expectedError = { - message: 'some message', - note: 'some note error', + message: 'patient.notes.error.unableToAdd', + note: 'patient.notes.error.noteRequired', } - const store = mockStore({ - patient: { - patient: expectedPatient, - noteError: expectedError, - }, - } as any) - const wrapper = mount( - - - , - ) + const { wrapper } = setup() + await act(async () => { + const modal = wrapper.find(Modal) + const onSave = (modal.prop('successButton') as any).onClick + await onSave({} as React.MouseEvent) + }) + wrapper.update() const alert = wrapper.find(Alert) const noteTextField = wrapper.find(TextFieldWithLabelFormGroup) + expect(alert.prop('title')).toEqual('states.error') expect(alert.prop('message')).toEqual(expectedError.message) expect(noteTextField.prop('isInvalid')).toBeTruthy() @@ -100,20 +81,7 @@ describe('New Note Modal', () => { describe('on cancel', () => { it('should call the onCloseButtonCLick function when the cancel button is clicked', () => { const onCloseButtonClickSpy = jest.fn() - const expectedPatient = { - id: '1234', - givenName: 'some name', - } - const store = mockStore({ - patient: { - patient: expectedPatient, - }, - } as any) - const wrapper = mount( - - - , - ) + const { wrapper } = setup(onCloseButtonClickSpy) act(() => { const modal = wrapper.find(Modal) @@ -126,42 +94,34 @@ describe('New Note Modal', () => { }) describe('on save', () => { - it('should dispatch add note', () => { + it('should dispatch add note', async () => { const expectedNote = 'some note' - jest.spyOn(patientSlice, 'addNote') - const expectedPatient = { - id: '1234', - givenName: 'some name', - } - const store = mockStore({ - patient: { - patient: expectedPatient, - }, - } as any) - - jest.spyOn(PatientRepository, 'find').mockResolvedValue(expectedPatient as Patient) - jest.spyOn(PatientRepository, 'saveOrUpdate').mockResolvedValue(expectedPatient as Patient) - - const wrapper = mount( - - - , - ) + const { wrapper } = setup() + const noteTextField = wrapper.find(TextFieldWithLabelFormGroup) - act(() => { - const noteTextField = wrapper.find(TextFieldWithLabelFormGroup) + await act(async () => { const onChange = noteTextField.prop('onChange') as any - onChange({ currentTarget: { value: expectedNote } }) + await onChange({ currentTarget: { value: expectedNote } }) }) wrapper.update() - act(() => { + + await act(async () => { const modal = wrapper.find(Modal) - const { onClick } = modal.prop('successButton') as any - onClick() + const onSave = (modal.prop('successButton') as any).onClick + await onSave({} as React.MouseEvent) + wrapper.update() }) - expect(patientSlice.addNote).toHaveBeenCalledWith(expectedPatient.id, { text: expectedNote }) + expect(PatientRepository.saveOrUpdate).toHaveBeenCalledTimes(1) + expect(PatientRepository.saveOrUpdate).toHaveBeenCalledWith( + expect.objectContaining({ + notes: [expect.objectContaining({ text: expectedNote })], + }), + ) + + // Does the form reset value back to blank? + expect(noteTextField.prop('value')).toEqual('') }) }) }) diff --git a/src/__tests__/patients/notes/NotesList.test.tsx b/src/__tests__/patients/notes/NotesList.test.tsx new file mode 100644 index 0000000000..860572c139 --- /dev/null +++ b/src/__tests__/patients/notes/NotesList.test.tsx @@ -0,0 +1,74 @@ +import { Alert, List, ListItem } from '@hospitalrun/components' +import { mount, ReactWrapper } from 'enzyme' +import { createMemoryHistory } from 'history' +import React from 'react' +import { act } from 'react-dom/test-utils' +import { Router } from 'react-router-dom' + +import NotesList from '../../../patients/notes/NotesList' +import PatientRepository from '../../../shared/db/PatientRepository' +import Note from '../../../shared/model/Note' +import Patient from '../../../shared/model/Patient' + +describe('Notes list', () => { + const setup = async (notes: Note[]) => { + const mockPatient = { id: '123', notes } as Patient + jest.spyOn(PatientRepository, 'find').mockResolvedValueOnce(mockPatient) + const history = createMemoryHistory() + history.push(`/patients/${mockPatient.id}/notes`) + let wrapper: any + await act(async () => { + wrapper = await mount( + + + , + ) + }) + + wrapper.update() + return { wrapper: wrapper as ReactWrapper, history } + } + + it('should render a list of notes', async () => { + const expectedNotes = [ + { + id: '456', + text: 'some name', + date: '1947-09-09T14:48:00.000Z', + }, + ] + const { wrapper } = await setup(expectedNotes) + const listItems = wrapper.find(ListItem) + + expect(wrapper.exists(List)).toBeTruthy() + expect(listItems).toHaveLength(expectedNotes.length) + expect(listItems.at(0).find('.ref__note-item-date').text().length) + expect(listItems.at(0).find('.ref__note-item-text').text()).toEqual(expectedNotes[0].text) + }) + + it('should display a warning when no notes are present', async () => { + const expectedNotes: Note[] = [] + const { wrapper } = await setup(expectedNotes) + + const alert = wrapper.find(Alert) + + expect(wrapper.exists(Alert)).toBeTruthy() + expect(wrapper.exists(List)).toBeFalsy() + + expect(alert.prop('color')).toEqual('warning') + expect(alert.prop('title')).toEqual('patient.notes.warning.noNotes') + expect(alert.prop('message')).toEqual('patient.notes.addNoteAbove') + }) + + it('should navigate to the note view when the note is clicked', async () => { + const expectedNotes = [{ id: '456', text: 'some name', date: '1947-09-09T14:48:00.000Z' }] + const { wrapper, history } = await setup(expectedNotes) + const item = wrapper.find(ListItem) + act(() => { + const onClick = item.prop('onClick') as any + onClick({ stopPropagation: jest.fn() }) + }) + + expect(history.location.pathname).toEqual(`/patients/123/notes/${expectedNotes[0].id}`) + }) +}) diff --git a/src/__tests__/patients/notes/NotesTab.test.tsx b/src/__tests__/patients/notes/NotesTab.test.tsx index 1f4cda1b77..e91eae2d17 100644 --- a/src/__tests__/patients/notes/NotesTab.test.tsx +++ b/src/__tests__/patients/notes/NotesTab.test.tsx @@ -1,6 +1,9 @@ +/* eslint-disable no-console */ + import * as components from '@hospitalrun/components' import { mount } from 'enzyme' import { createMemoryHistory } from 'history' +import assign from 'lodash/assign' import React from 'react' import { act } from 'react-dom/test-utils' import { Provider } from 'react-redux' @@ -26,16 +29,30 @@ const history = createMemoryHistory() let user: any let store: any -const setup = (patient = expectedPatient, permissions = [Permissions.WritePatients]) => { +const setup = (props: any = {}) => { + const { permissions, patient, route } = assign( + {}, + { + patient: expectedPatient, + permissions: [Permissions.WritePatients], + route: '/patients/123/notes', + }, + props, + ) + user = { permissions } store = mockStore({ patient, user } as any) - const wrapper = mount( - - - - - , - ) + history.push(route) + let wrapper: any + act(() => { + wrapper = mount( + + + + + , + ) + }) return wrapper } @@ -45,6 +62,7 @@ describe('Notes Tab', () => { beforeEach(() => { jest.resetAllMocks() jest.spyOn(PatientRepository, 'saveOrUpdate') + console.error = jest.fn() }) it('should render a add notes button', () => { @@ -56,7 +74,7 @@ describe('Notes Tab', () => { }) it('should not render a add notes button if the user does not have permissions', () => { - const wrapper = setup(expectedPatient, []) + const wrapper = setup({ permissions: [] }) const addNotesButton = wrapper.find(components.Button) expect(addNotesButton).toHaveLength(0) @@ -64,7 +82,6 @@ describe('Notes Tab', () => { it('should open the Add Notes Modal', () => { const wrapper = setup() - act(() => { const onClick = wrapper.find(components.Button).prop('onClick') as any onClick() @@ -74,27 +91,14 @@ describe('Notes Tab', () => { expect(wrapper.find(components.Modal).prop('show')).toBeTruthy() }) }) - - describe('notes list', () => { - it('should list the patients diagnoses', () => { - const notes = expectedPatient.notes as Note[] - const wrapper = setup() - - const list = wrapper.find(components.List) - const listItems = wrapper.find(components.ListItem) - - expect(list).toHaveLength(1) - expect(listItems).toHaveLength(notes.length) - }) - - it('should render a warning message if the patient does not have any diagnoses', () => { - const wrapper = setup({ ...expectedPatient, notes: [] }) - - const alert = wrapper.find(components.Alert) - - expect(alert).toHaveLength(1) - expect(alert.prop('title')).toEqual('patient.notes.warning.noNotes') - expect(alert.prop('message')).toEqual('patient.notes.addNoteAbove') + describe('/patients/:id/notes', () => { + it('should render the view notes screen when /patients/:id/notes is accessed', () => { + const route = '/patients/123/notes' + const permissions = [Permissions.WritePatients] + const wrapper = setup({ route, permissions }) + act(() => { + expect(wrapper.exists(NoteTab)).toBeTruthy() + }) }) }) }) diff --git a/src/__tests__/patients/notes/ViewNote.test.tsx b/src/__tests__/patients/notes/ViewNote.test.tsx new file mode 100644 index 0000000000..74172470c7 --- /dev/null +++ b/src/__tests__/patients/notes/ViewNote.test.tsx @@ -0,0 +1,46 @@ +import { mount, ReactWrapper } from 'enzyme' +import { createMemoryHistory } from 'history' +import React from 'react' +import { act } from 'react-dom/test-utils' +import { Route, Router } from 'react-router-dom' + +import ViewNote from '../../../patients/notes/ViewNote' +import TextInputWithLabelFormGroup from '../../../shared/components/input/TextInputWithLabelFormGroup' +import PatientRepository from '../../../shared/db/PatientRepository' +import Patient from '../../../shared/model/Patient' + +describe('View Note', () => { + const patient = { + id: 'patientId', + notes: [{ id: '123', text: 'some name', date: '1947-09-09T14:48:00.000Z' }], + } as Patient + + const setup = async () => { + jest.spyOn(PatientRepository, 'find').mockResolvedValue(patient) + const history = createMemoryHistory() + history.push(`/patients/${patient.id}/notes/${patient.notes![0].id}`) + let wrapper: any + + await act(async () => { + wrapper = await mount( + + + + + , + ) + }) + + wrapper.update() + + return { wrapper: wrapper as ReactWrapper } + } + + it('should render a note input with the correct data', async () => { + const { wrapper } = await setup() + + const noteText = wrapper.find(TextInputWithLabelFormGroup) + expect(noteText).toHaveLength(1) + expect(noteText.prop('value')).toEqual(patient.notes![0].text) + }) +}) diff --git a/src/__tests__/patients/patient-slice.test.ts b/src/__tests__/patients/patient-slice.test.ts index 66a8a38bfc..fa68233f53 100644 --- a/src/__tests__/patients/patient-slice.test.ts +++ b/src/__tests__/patients/patient-slice.test.ts @@ -6,8 +6,6 @@ import thunk from 'redux-thunk' import patient, { addDiagnosis, addDiagnosisError, - addRelatedPerson, - addRelatedPersonError, createPatient, createPatientError, createPatientStart, @@ -15,7 +13,6 @@ import patient, { fetchPatient, fetchPatientStart, fetchPatientSuccess, - removeRelatedPerson, updatePatient, updatePatientError, updatePatientStart, @@ -24,7 +21,6 @@ import patient, { import PatientRepository from '../../shared/db/PatientRepository' import Diagnosis, { DiagnosisStatus } from '../../shared/model/Diagnosis' import Patient from '../../shared/model/Patient' -import RelatedPerson from '../../shared/model/RelatedPerson' import { RootState } from '../../shared/store' import * as uuid from '../../shared/util/uuid' @@ -447,95 +443,6 @@ describe('patients slice', () => { }) }) - describe('add related person', () => { - it('should add the related person to the patient with the given id', async () => { - const expectedRelatedPersonId = 'expected id' - const store = mockStore() - const expectedPatientId = '123' - - const expectedPatient = { - id: expectedPatientId, - givenName: 'some name', - } as Patient - - const expectedRelatedPerson = { - patientId: '456', - type: '1234', - } as RelatedPerson - - const expectedUpdatedPatient = { - ...expectedPatient, - relatedPersons: [{ ...expectedRelatedPerson, id: expectedRelatedPersonId }], - } as Patient - - const findPatientSpy = jest - .spyOn(PatientRepository, 'find') - .mockResolvedValue(expectedPatient) - jest.spyOn(uuid, 'uuid').mockReturnValue(expectedRelatedPersonId) - jest.spyOn(PatientRepository, 'saveOrUpdate').mockResolvedValue(expectedUpdatedPatient) - const onSuccessSpy = jest.fn() - - await store.dispatch(addRelatedPerson(expectedPatientId, expectedRelatedPerson, onSuccessSpy)) - - expect(findPatientSpy).toHaveBeenCalledWith(expectedPatientId) - expect(store.getActions()[1]).toEqual(updatePatientSuccess(expectedUpdatedPatient)) - expect(onSuccessSpy).toHaveBeenCalledWith(expectedUpdatedPatient) - }) - - it('should validate the related person', async () => { - const expectedError = { - message: 'patient.relatedPersons.error.unableToAddRelatedPerson', - relationshipType: 'patient.relatedPersons.error.relationshipTypeRequired', - relatedPerson: 'patient.relatedPersons.error.relatedPersonRequired', - } - const store = mockStore() - const expectedRelatedPerson = {} as RelatedPerson - const onSuccessSpy = jest.fn() - - await store.dispatch(addRelatedPerson('some id', expectedRelatedPerson, onSuccessSpy)) - - expect(store.getActions()[0]).toEqual(addRelatedPersonError(expectedError)) - expect(onSuccessSpy).not.toHaveBeenCalled() - }) - }) - - describe('remove related person', () => { - it('should remove the related related person rom patient with the given id', async () => { - const store = mockStore() - - const expectedRelatedPersonPatientId = 'expected id' - const expectedPatientId = '123' - - const expectedRelatedPerson = { - id: 'some id', - patientId: expectedRelatedPersonPatientId, - type: 'some type', - } as RelatedPerson - - const expectedPatient = { - id: expectedPatientId, - givenName: 'some name', - relatedPersons: [expectedRelatedPerson], - } as Patient - - const expectedUpdatedPatient = { - ...expectedPatient, - relatedPersons: [], - } as Patient - - const findPatientSpy = jest - .spyOn(PatientRepository, 'find') - .mockResolvedValue(expectedPatient) - jest.spyOn(uuid, 'uuid').mockReturnValue(expectedRelatedPersonPatientId) - jest.spyOn(PatientRepository, 'saveOrUpdate').mockResolvedValue(expectedUpdatedPatient) - - await store.dispatch(removeRelatedPerson(expectedPatientId, expectedRelatedPersonPatientId)) - - expect(findPatientSpy).toHaveBeenCalledWith(expectedPatientId) - expect(store.getActions()[1]).toEqual(updatePatientSuccess(expectedUpdatedPatient)) - }) - }) - describe('add diagnosis', () => { it('should add the diagnosis to the patient with the given id', async () => { const expectedDiagnosisId = 'expected id' diff --git a/src/__tests__/patients/related-persons/AddRelatedPersonModal.test.tsx b/src/__tests__/patients/related-persons/AddRelatedPersonModal.test.tsx index bbf919c155..5069f8ed24 100644 --- a/src/__tests__/patients/related-persons/AddRelatedPersonModal.test.tsx +++ b/src/__tests__/patients/related-persons/AddRelatedPersonModal.test.tsx @@ -1,19 +1,12 @@ import { Modal, Alert, Typeahead } from '@hospitalrun/components' import { act } from '@testing-library/react' -import { ReactWrapper, mount } from 'enzyme' +import { mount } from 'enzyme' import React from 'react' -import { Provider } from 'react-redux' -import createMockStore, { MockStore } from 'redux-mock-store' -import thunk from 'redux-thunk' -import * as patientSlice from '../../../patients/patient-slice' import AddRelatedPersonModal from '../../../patients/related-persons/AddRelatedPersonModal' import TextInputWithLabelFormGroup from '../../../shared/components/input/TextInputWithLabelFormGroup' import PatientRepository from '../../../shared/db/PatientRepository' import Patient from '../../../shared/model/Patient' -import { RootState } from '../../../shared/store' - -const mockStore = createMockStore([thunk]) describe('Add Related Person Modal', () => { const patient = { @@ -33,32 +26,30 @@ describe('Add Related Person Modal', () => { dateOfBirth: new Date().toISOString(), } as Patient - let store: MockStore + const setup = () => { + jest.spyOn(PatientRepository, 'find').mockResolvedValue(patient) + jest.spyOn(PatientRepository, 'saveOrUpdate').mockResolvedValue(patient) - describe('layout', () => { - let wrapper: ReactWrapper - - store = mockStore({ - patient: { patient }, - } as any) - beforeEach(() => { - jest.spyOn(PatientRepository, 'find').mockResolvedValue(patient) - jest.spyOn(PatientRepository, 'saveOrUpdate').mockResolvedValue(patient) - - wrapper = mount( - - - , - ) - }) + return mount( + , + ) + } + describe('layout', () => { it('should render a modal', () => { + const wrapper = setup() const modal = wrapper.find(Modal) expect(modal).toHaveLength(1) expect(modal.prop('show')).toBeTruthy() }) it('should render a patient search typeahead', () => { + const wrapper = setup() const patientSearchTypeahead = wrapper.find(Typeahead) expect(patientSearchTypeahead).toHaveLength(1) @@ -66,6 +57,7 @@ describe('Add Related Person Modal', () => { }) it('should render a relationship type text input', () => { + const wrapper = setup() const relationshipTypeTextInput = wrapper.findWhere((w: any) => w.prop('name') === 'type') expect(relationshipTypeTextInput).toHaveLength(1) @@ -78,6 +70,7 @@ describe('Add Related Person Modal', () => { }) it('should render a cancel button', () => { + const wrapper = setup() const cancelButton = wrapper.findWhere( (w: { text: () => string }) => w.text() === 'actions.cancel', ) @@ -86,27 +79,25 @@ describe('Add Related Person Modal', () => { }) it('should render an add new related person button button', () => { + const wrapper = setup() const modal = wrapper.find(Modal) as any expect(modal.prop('successButton').children).toEqual('patient.relatedPersons.add') }) - it('should render the error', () => { + it('should render the error when there is an error saving', async () => { + const wrapper = setup() const expectedError = { - message: 'some message', - relatedPerson: 'some related person error', - relationshipType: 'some relationship type error', + message: 'patient.relatedPersons.error.unableToAddRelatedPerson', + relatedPersonError: 'patient.relatedPersons.error.relatedPersonRequired', + relationshipTypeError: 'patient.relatedPersons.error.relationshipTypeRequired', } - store = mockStore({ - patient: { - patient, - relatedPersonError: expectedError, - }, - } as any) - wrapper = mount( - - - , - ) + + await act(async () => { + const modal = wrapper.find(Modal) + const onSave = (modal.prop('successButton') as any).onClick + await onSave({} as React.MouseEvent) + }) + wrapper.update() const alert = wrapper.find(Alert) const typeahead = wrapper.find(Typeahead) @@ -116,27 +107,13 @@ describe('Add Related Person Modal', () => { expect(alert.prop('title')).toEqual('states.error') expect(typeahead.prop('isInvalid')).toBeTruthy() expect(relationshipTypeInput.prop('isInvalid')).toBeTruthy() - expect(relationshipTypeInput.prop('feedback')).toEqual(expectedError.relationshipType) + expect(relationshipTypeInput.prop('feedback')).toEqual(expectedError.relationshipTypeError) }) }) describe('save', () => { - jest.spyOn(patientSlice, 'addRelatedPerson') - let wrapper: ReactWrapper - store = mockStore({ - patient: { - patient, - }, - } as any) - beforeEach(() => { - wrapper = mount( - - - , - ) - }) - - it('should call the save function with the correct data', () => { + it('should call the save function with the correct data', async () => { + const wrapper = setup() act(() => { const patientTypeahead = wrapper.find(Typeahead) patientTypeahead.prop('onChange')([{ id: '123' }]) @@ -149,15 +126,22 @@ describe('Add Related Person Modal', () => { }) wrapper.update() - act(() => { + await act(async () => { const { onClick } = wrapper.find(Modal).prop('successButton') as any - onClick({} as React.MouseEvent) + await onClick({} as React.MouseEvent) }) - expect(patientSlice.addRelatedPerson).toHaveBeenCalledWith(patient.id, { - patientId: '123', - type: 'relationship', - }) + expect(PatientRepository.saveOrUpdate).toHaveBeenCalledTimes(1) + expect(PatientRepository.saveOrUpdate).toHaveBeenCalledWith( + expect.objectContaining({ + relatedPersons: [ + expect.objectContaining({ + patientId: '123', + type: 'relationship', + }), + ], + }), + ) }) }) }) diff --git a/src/__tests__/patients/related-persons/RelatedPersonsTab.test.tsx b/src/__tests__/patients/related-persons/RelatedPersonsTab.test.tsx index 2fce3faaa8..958d2cc6d0 100644 --- a/src/__tests__/patients/related-persons/RelatedPersonsTab.test.tsx +++ b/src/__tests__/patients/related-persons/RelatedPersonsTab.test.tsx @@ -9,7 +9,6 @@ import { Router } from 'react-router-dom' import createMockStore from 'redux-mock-store' import thunk from 'redux-thunk' -import * as patientSlice from '../../../patients/patient-slice' import AddRelatedPersonModal from '../../../patients/related-persons/AddRelatedPersonModal' import RelatedPersonTab from '../../../patients/related-persons/RelatedPersonTab' import PatientRepository from '../../../shared/db/PatientRepository' @@ -119,7 +118,10 @@ describe('Related Persons Tab', () => { beforeEach(async () => { jest.spyOn(PatientRepository, 'saveOrUpdate') - jest.spyOn(PatientRepository, 'find').mockResolvedValue(expectedRelatedPerson) + jest + .spyOn(PatientRepository, 'find') + .mockResolvedValueOnce(patient) + .mockResolvedValueOnce(expectedRelatedPerson) await act(async () => { wrapper = await mount( @@ -157,14 +159,14 @@ describe('Related Persons Tab', () => { }) it('should remove the related person when the delete button is clicked', async () => { - const removeRelatedPersonSpy = jest.spyOn(patientSlice, 'removeRelatedPerson') + const removeRelatedPersonSpy = jest.spyOn(PatientRepository, 'saveOrUpdate') const tr = wrapper.find('tr').at(1) - act(() => { + await act(async () => { const onClick = tr.find('button').at(1).prop('onClick') as any - onClick({ stopPropagation: jest.fn() }) + await onClick({ stopPropagation: jest.fn() }) }) - expect(removeRelatedPersonSpy).toHaveBeenCalledWith(patient.id, expectedRelatedPerson.id) + expect(removeRelatedPersonSpy).toHaveBeenCalledWith({ ...patient, relatedPersons: [] }) }) it('should navigate to related person patient profile on related person click', async () => { diff --git a/src/__tests__/patients/search/ViewPatients.test.tsx b/src/__tests__/patients/search/ViewPatients.test.tsx index b2bf936d9a..7d9f333fd1 100644 --- a/src/__tests__/patients/search/ViewPatients.test.tsx +++ b/src/__tests__/patients/search/ViewPatients.test.tsx @@ -5,6 +5,7 @@ import { MemoryRouter } from 'react-router-dom' import configureStore from 'redux-mock-store' import thunk from 'redux-thunk' +import { TitleProvider } from '../../../page-header/title/TitleContext' import SearchPatients from '../../../patients/search/SearchPatients' import ViewPatients from '../../../patients/search/ViewPatients' import PatientRepository from '../../../shared/db/PatientRepository' @@ -18,7 +19,9 @@ describe('Patients', () => { return mount( - + + + , ) diff --git a/src/__tests__/patients/util/validate-caregoal.test.ts b/src/__tests__/patients/util/validate-caregoal.test.ts new file mode 100644 index 0000000000..f0b52ee989 --- /dev/null +++ b/src/__tests__/patients/util/validate-caregoal.test.ts @@ -0,0 +1,34 @@ +import { subDays } from 'date-fns' + +import validateCareGoal from '../../../patients/util/validate-caregoal' +import CareGoal from '../../../shared/model/CareGoal' + +describe('validate care goal', () => { + it('should validate required fields', () => { + const expectedError = { + description: 'patient.careGoal.error.descriptionRequired', + priority: 'patient.careGoal.error.priorityRequired', + status: 'patient.careGoal.error.statusRequired', + achievementStatus: 'patient.careGoal.error.achievementStatusRequired', + startDate: 'patient.careGoal.error.startDate', + dueDate: 'patient.careGoal.error.dueDate', + } + + const actualError = validateCareGoal({} as CareGoal) + + expect(actualError).toEqual(expectedError) + }) + + it('should validate the start date time is before end date time', () => { + const givenCareGoal = { + startDate: new Date().toISOString(), + dueDate: subDays(new Date(), 1).toISOString(), + } as CareGoal + + const actualError = validateCareGoal(givenCareGoal) + + expect(actualError).toEqual( + expect.objectContaining({ dueDate: 'patient.careGoal.error.dueDateMustBeAfterStartDate' }), + ) + }) +}) diff --git a/src/__tests__/patients/util/validate-diagnosis.test.ts b/src/__tests__/patients/util/validate-diagnosis.test.ts new file mode 100644 index 0000000000..949bd4f23d --- /dev/null +++ b/src/__tests__/patients/util/validate-diagnosis.test.ts @@ -0,0 +1,18 @@ +import validateDiagnosis from '../../../patients/util/validate-diagnosis' + +describe('validate diagnosis', () => { + it('should check for required fields', () => { + const diagnosis = {} as any + const expectedError = { + name: 'patient.diagnoses.error.nameRequired', + diagnosisDate: 'patient.diagnoses.error.dateRequired', + onsetDate: 'patient.diagnoses.error.dateRequired', + abatementDate: 'patient.diagnoses.error.dateRequired', + status: 'patient.diagnoses.error.statusRequired', + } + + const actualError = validateDiagnosis(diagnosis) + + expect(actualError).toEqual(expectedError) + }) +}) diff --git a/src/__tests__/patients/util/validate-visit.test.ts b/src/__tests__/patients/util/validate-visit.test.ts new file mode 100644 index 0000000000..7726eb48c4 --- /dev/null +++ b/src/__tests__/patients/util/validate-visit.test.ts @@ -0,0 +1,58 @@ +import { subDays } from 'date-fns' + +import validateVisit from '../../../patients/util/validate-visit' +import Visit from '../../../shared/model/Visit' + +describe('validate visit', () => { + it('should validate required fields', () => { + const expectedError = { + endDateTime: 'patient.visits.error.endDateRequired', + startDateTime: 'patient.visits.error.startDateRequired', + status: 'patient.visits.error.locationRequired', + } + + const actualError = validateVisit({} as Visit) + + expect(actualError).toEqual(expectedError) + }) + + it('should validate the start date time is before end date time', () => { + const givenVisit: Visit = { + startDateTime: new Date().toISOString(), + endDateTime: subDays(new Date(), 1).toISOString(), + id: '123', + reason: 'reason for visit', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + type: 'type', + status: 'planned', + reason: 'given reason', + location: 'give location', + } + + const actualError = validateVisit(givenVisit) + + expect(actualError).toEqual( + expect.objectContaining({ endDateTime: 'patient.visits.error.endDateMustBeAfterStartDate' }), + ) + }) + + it('should not validate given a valid visit', () => { + const givenVisit: Visit = { + startDateTime: new Date().toISOString(), + endDateTime: subDays(new Date(), -1).toISOString(), + id: '123', + reason: 'reason for visit', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + type: 'type', + status: 'planned', + reason: 'given reason', + location: 'give location', + } + + const actualError = validateVisit(givenVisit) + + expect(actualError).toEqual({}) + }) +}) diff --git a/src/__tests__/patients/view/ImportantPatientInfo.test.tsx b/src/__tests__/patients/view/ImportantPatientInfo.test.tsx index f176e0ff9f..2c63aa409d 100644 --- a/src/__tests__/patients/view/ImportantPatientInfo.test.tsx +++ b/src/__tests__/patients/view/ImportantPatientInfo.test.tsx @@ -1,17 +1,14 @@ import * as components from '@hospitalrun/components' -// import { act } from '@testing-library/react' import format from 'date-fns/format' import { mount } from 'enzyme' import { createMemoryHistory } from 'history' import React from 'react' -// import { startOfDay, subYears } from 'date-fns' 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 Diagnoses from '../../../patients/diagnoses/Diagnoses' import NewAllergyModal from '../../../patients/allergies/NewAllergyModal' import AddDiagnosisModal from '../../../patients/diagnoses/AddDiagnosisModal' import ImportantPatientInfo from '../../../patients/view/ImportantPatientInfo' @@ -19,14 +16,9 @@ import AddVisitModal from '../../../patients/visits/AddVisitModal' import PatientRepository from '../../../shared/db/PatientRepository' import CarePlan from '../../../shared/model/CarePlan' import Diagnosis from '../../../shared/model/Diagnosis' -// import Allergies from '../../../patients/allergies/Allergies' -// import AllergiesList from '../../../patients/allergies/AllergiesList' -// import PatientRepository from '../../../shared/db/PatientRepository' import Patient from '../../../shared/model/Patient' import Permissions from '../../../shared/model/Permissions' import { RootState } from '../../../shared/store' -// import * as getPatientName from '../../../patients/util/patient-name-util' -// import AddCarePlanModal from '../../../patients/care-plans/AddCarePlanModal' const mockStore = createMockStore([thunk]) @@ -261,54 +253,4 @@ describe('Important Patient Info Panel', () => { expect(wrapper.find(components.Button)).toHaveLength(0) }) }) - - // describe('add new care plan button', () => { - // it('should render an add diagnosis button if user has correct permissions', async () => { - // const wrapper = await setup(expectedPatient, [Permissions.AddCarePlan]) - - // const addNewButton = wrapper.find(components.Button) - // expect(addNewButton).toHaveLength(1) - // expect(addNewButton.text().trim()).toEqual('patient.carePlan.new') - // }) - - // it('should open the add care plan modal on click', async () => { - // const wrapper = await setup(expectedPatient, [Permissions.AddCarePlan]) - - // act(() => { - // const addNewButton = wrapper.find(components.Button) - // const onClick = addNewButton.prop('onClick') as any - // onClick() - // }) - // wrapper.update() - - // const modal = wrapper.find(AddCarePlanModal) - // expect(modal.prop('show')).toBeTruthy() - // }) - - // it('should close the modal when the close button is clicked', async () => { - // const wrapper = await setup(expectedPatient, [Permissions.AddCarePlan]) - - // act(() => { - // const addNewButton = wrapper.find(components.Button) - // const onClick = addNewButton.prop('onClick') as any - // onClick() - // }) - // wrapper.update() - - // act(() => { - // const modal = wrapper.find(AddCarePlanModal) - // const onClose = modal.prop('onCloseButtonClick') as any - // onClose() - // }) - // wrapper.update() - - // expect(wrapper.find(AddCarePlanModal).prop('show')).toBeFalsy() - // }) - - // it('should not render new care plan button if user does not have permissions', async () => { - // const wrapper = await setup(expectedPatient, []) - - // expect(wrapper.find(components.Button)).toHaveLength(0) - // }) - // }) }) diff --git a/src/__tests__/patients/view/ViewPatient.test.tsx b/src/__tests__/patients/view/ViewPatient.test.tsx index 39aa5d5e87..80713efce7 100644 --- a/src/__tests__/patients/view/ViewPatient.test.tsx +++ b/src/__tests__/patients/view/ViewPatient.test.tsx @@ -10,13 +10,13 @@ import thunk from 'redux-thunk' import { mocked } from 'ts-jest/utils' import * as ButtonBarProvider from '../../../page-header/button-toolbar/ButtonBarProvider' -import * as titleUtil from '../../../page-header/title/useTitle' +import * as titleUtil from '../../../page-header/title/TitleContext' import Allergies from '../../../patients/allergies/Allergies' import AppointmentsList from '../../../patients/appointments/AppointmentsList' import CarePlanTab from '../../../patients/care-plans/CarePlanTab' import Diagnoses from '../../../patients/diagnoses/Diagnoses' import GeneralInformation from '../../../patients/GeneralInformation' -import LabsTab from '../../../patients/labs/LabsTab' +import Labs from '../../../patients/labs/Labs' import NotesTab from '../../../patients/notes/NoteTab' import * as patientSlice from '../../../patients/patient-slice' import RelatedPersonTab from '../../../patients/related-persons/RelatedPersonTab' @@ -26,10 +26,11 @@ import Patient from '../../../shared/model/Patient' import Permissions from '../../../shared/model/Permissions' import { RootState } from '../../../shared/store' +const { TitleProvider } = titleUtil const mockStore = createMockStore([thunk]) describe('ViewPatient', () => { - const patient = { + const patient = ({ id: '123', prefix: 'prefix', givenName: 'givenName', @@ -44,12 +45,13 @@ describe('ViewPatient', () => { address: 'address', code: 'P00001', dateOfBirth: new Date().toISOString(), - } as Patient + } as unknown) as Patient let history: any let store: MockStore const setup = async (permissions = [Permissions.ReadPatients]) => { + jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) jest.spyOn(PatientRepository, 'find') jest.spyOn(PatientRepository, 'getLabs').mockResolvedValue([]) const mockedPatientRepository = mocked(PatientRepository, true) @@ -59,6 +61,7 @@ describe('ViewPatient', () => { patient: { patient }, user: { permissions }, appointments: { appointments: [] }, + labs: { labs: [] }, } as any) history.push('/patients/123') @@ -68,12 +71,15 @@ describe('ViewPatient', () => { - + + + , ) }) + wrapper.find(ViewPatient).props().updateTitle = jest.fn() wrapper.update() return { wrapper: wrapper as ReactWrapper } @@ -91,12 +97,10 @@ describe('ViewPatient', () => { expect(store.getActions()).toContainEqual(patientSlice.fetchPatientSuccess(patient)) }) - it('should render a header with the patients given, family, and suffix', async () => { - jest.spyOn(titleUtil, 'default') - + it('should have called useUpdateTitle hook', async () => { await setup() - expect(titleUtil.default).toHaveBeenCalledWith(`patient.label`) + expect(titleUtil.useUpdateTitle).toHaveBeenCalled() }) it('should add a "Edit Patient" button to the button tool bar if has WritePatients permissions', async () => { @@ -128,7 +132,7 @@ describe('ViewPatient', () => { const tabs = tabsHeader.find(Tab) expect(tabsHeader).toHaveLength(1) - expect(tabs).toHaveLength(9) + expect(tabs).toHaveLength(10) expect(tabs.at(0).prop('label')).toEqual('patient.generalInformation') expect(tabs.at(1).prop('label')).toEqual('patient.relatedPersons.label') expect(tabs.at(2).prop('label')).toEqual('scheduling.appointments.label') @@ -137,7 +141,8 @@ describe('ViewPatient', () => { expect(tabs.at(5).prop('label')).toEqual('patient.notes.label') expect(tabs.at(6).prop('label')).toEqual('patient.labs.label') expect(tabs.at(7).prop('label')).toEqual('patient.carePlan.label') - expect(tabs.at(8).prop('label')).toEqual('patient.visits.label') + expect(tabs.at(8).prop('label')).toEqual('patient.careGoal.label') + expect(tabs.at(9).prop('label')).toEqual('patient.visits.label') }) it('should mark the general information tab as active and render the general information component when route is /patients/:id', async () => { @@ -289,12 +294,12 @@ describe('ViewPatient', () => { const tabsHeader = wrapper.find(TabsHeader) const tabs = tabsHeader.find(Tab) - const labsTab = wrapper.find(LabsTab) + const labsTab = wrapper.find(Labs) expect(history.location.pathname).toEqual(`/patients/${patient.id}/labs`) expect(tabs.at(6).prop('active')).toBeTruthy() expect(labsTab).toHaveLength(1) - expect(labsTab.prop('patientId')).toEqual(patient.id) + expect(labsTab.prop('patient')).toEqual(patient) }) it('should mark the care plans tab as active when it is clicked and render the care plan tab component when route is /patients/:id/care-plans', async () => { @@ -317,4 +322,25 @@ describe('ViewPatient', () => { expect(tabs.at(7).prop('active')).toBeTruthy() expect(carePlansTab).toHaveLength(1) }) + + it('should mark the care goals tab as active when it is clicked and render the care goal tab component when route is /patients/:id/care-goals', async () => { + const { wrapper } = await setup() + + await act(async () => { + const tabHeader = wrapper.find(TabsHeader) + const tabs = tabHeader.find(Tab) + const onClick = tabs.at(8).prop('onClick') as any + onClick() + }) + + wrapper.update() + + const tabsHeader = wrapper.find(TabsHeader) + const tabs = tabsHeader.find(Tab) + const careGoalsTab = tabs.at(8) + + expect(history.location.pathname).toEqual(`/patients/${patient.id}/care-goals`) + expect(careGoalsTab.prop('active')).toBeTruthy() + expect(careGoalsTab).toHaveLength(1) + }) }) diff --git a/src/__tests__/patients/visits/AddVisitModal.test.tsx b/src/__tests__/patients/visits/AddVisitModal.test.tsx index 01c4ae3163..3890cccdda 100644 --- a/src/__tests__/patients/visits/AddVisitModal.test.tsx +++ b/src/__tests__/patients/visits/AddVisitModal.test.tsx @@ -1,22 +1,15 @@ import { Modal } from '@hospitalrun/components' -import { mount } from 'enzyme' +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 * as patientSlice from '../../../patients/patient-slice' import AddVisitModal from '../../../patients/visits/AddVisitModal' import VisitForm from '../../../patients/visits/VisitForm' import PatientRepository from '../../../shared/db/PatientRepository' import Patient from '../../../shared/model/Patient' import { VisitStatus } from '../../../shared/model/Visit' -import { RootState } from '../../../shared/store' - -const mockStore = createMockStore([thunk]) describe('Add Visit Modal', () => { const patient = { @@ -34,26 +27,20 @@ describe('Add Visit Modal', () => { ], } as Patient - const visitError = { - title: 'visit error', - } - const onCloseSpy = jest.fn() const setup = () => { jest.spyOn(PatientRepository, 'find').mockResolvedValue(patient) jest.spyOn(PatientRepository, 'saveOrUpdate') - const store = mockStore({ patient: { patient, visitError } } as any) const history = createMemoryHistory() const wrapper = mount( - - - - - , + + + , ) wrapper.update() - return { wrapper } + + return { wrapper: wrapper as ReactWrapper } } it('should render a modal', () => { @@ -74,14 +61,28 @@ describe('Add Visit Modal', () => { it('should render the visit form', () => { const { wrapper } = setup() - const visitForm = wrapper.find(VisitForm) - expect(visitForm).toHaveLength(1) - expect(visitForm.prop('visitError')).toEqual(visitError) + const addVisitModal = wrapper.find(AddVisitModal) + expect(addVisitModal).toHaveLength(1) + }) + + it('should call the on close function when the cancel button is clicked', () => { + const { wrapper } = setup() + + const modal = wrapper.find(Modal) + + expect(modal).toHaveLength(1) + + act(() => { + const cancelButton = modal.prop('closeButton') + const onClick = cancelButton?.onClick as any + onClick() + }) + + expect(onCloseSpy).toHaveBeenCalledTimes(1) }) - it('should dispatch add visit when the save button is clicked', async () => { + it('should save the visit when the save button is clicked', async () => { const { wrapper } = setup() - jest.spyOn(patientSlice, 'addVisit') act(() => { const visitForm = wrapper.find(VisitForm) @@ -97,23 +98,7 @@ describe('Add Visit Modal', () => { await onClick() }) - expect(patientSlice.addVisit).toHaveBeenCalledTimes(1) - expect(patientSlice.addVisit).toHaveBeenCalledWith(patient.id, patient.visits[0]) - }) - - it('should call the on close function when the cancel button is clicked', () => { - const { wrapper } = setup() - - const modal = wrapper.find(Modal) - - expect(modal).toHaveLength(1) - - act(() => { - const cancelButton = modal.prop('closeButton') - const onClick = cancelButton?.onClick as any - onClick() - }) - - expect(onCloseSpy).toHaveBeenCalledTimes(1) + expect(PatientRepository.saveOrUpdate).toHaveBeenCalledTimes(1) + expect(PatientRepository.saveOrUpdate).toHaveBeenCalledWith(patient) }) }) diff --git a/src/__tests__/patients/visits/ViewVisit.test.tsx b/src/__tests__/patients/visits/ViewVisit.test.tsx index f4fad6f169..3f35166b2a 100644 --- a/src/__tests__/patients/visits/ViewVisit.test.tsx +++ b/src/__tests__/patients/visits/ViewVisit.test.tsx @@ -1,17 +1,13 @@ -import { mount } from 'enzyme' +import { mount, ReactWrapper } from 'enzyme' import { createMemoryHistory } from 'history' import React from 'react' -import { Provider } from 'react-redux' +import { act } from 'react-dom/test-utils' import { Route, Router } from 'react-router-dom' -import createMockStore from 'redux-mock-store' -import thunk from 'redux-thunk' import ViewVisit from '../../../patients/visits/ViewVisit' import VisitForm from '../../../patients/visits/VisitForm' +import PatientRepository from '../../../shared/db/PatientRepository' import Patient from '../../../shared/model/Patient' -import { RootState } from '../../../shared/store' - -const mockStore = createMockStore([thunk]) describe('View Visit', () => { const patient = { @@ -19,31 +15,35 @@ describe('View Visit', () => { visits: [{ id: '123', reason: 'reason for visit' }], } as Patient - const setup = () => { - const store = mockStore({ patient: { patient }, user: { user: { id: '123' } } } as any) + const setup = async () => { + jest.spyOn(PatientRepository, 'find').mockResolvedValue(patient) const history = createMemoryHistory() history.push(`/patients/${patient.id}/visits/${patient.visits[0].id}`) - const wrapper = mount( - + let wrapper: any + + await act(async () => { + wrapper = await mount( - + - - , - ) + , + ) + }) + + wrapper.update() - return { wrapper } + return { wrapper: wrapper as ReactWrapper } } - it('should render the visit reason', () => { - const { wrapper } = setup() + it('should render the visit reason', async () => { + const { wrapper } = await setup() expect(wrapper.find('h2').text()).toEqual(patient.visits[0].reason) }) - it('should render a visit form with the correct data', () => { - const { wrapper } = setup() + it('should render a visit form with the correct data', async () => { + const { wrapper } = await setup() const visitForm = wrapper.find(VisitForm) expect(visitForm).toHaveLength(1) diff --git a/src/__tests__/patients/visits/VisitTab.test.tsx b/src/__tests__/patients/visits/VisitTab.test.tsx index f075b10ef3..bdf46f65fd 100644 --- a/src/__tests__/patients/visits/VisitTab.test.tsx +++ b/src/__tests__/patients/visits/VisitTab.test.tsx @@ -22,32 +22,37 @@ describe('Visit Tab', () => { id: 'patientId', } - const setup = (route: string, permissions: Permissions[]) => { - const store = mockStore({ patient: { patient }, user: { permissions } } as any) + const setup = async (route: string, permissions: Permissions[]) => { + const store = mockStore({ user: { permissions } } as any) const history = createMemoryHistory() history.push(route) - const wrapper = mount( - - - - - , - ) + let wrapper: any + + await act(async () => { + wrapper = await mount( + + + + + , + ) + }) wrapper.update() + return { wrapper, history } } - it('should render an add visit button if user has correct permissions', () => { - const { wrapper } = setup('/patients/123/visits', [Permissions.AddVisit]) + it('should render an add visit button if user has correct permissions', async () => { + const { wrapper } = await setup('/patients/123/visits', [Permissions.AddVisit]) const addNewButton = wrapper.find(Button).at(0) expect(addNewButton).toHaveLength(1) expect(addNewButton.text().trim()).toEqual('patient.visits.new') }) - it('should open the add visit modal on click', () => { - const { wrapper } = setup('/patients/123/visits', [Permissions.AddVisit]) + it('should open the add visit modal on click', async () => { + const { wrapper } = await setup('/patients/123/visits', [Permissions.AddVisit]) act(() => { const addNewButton = wrapper.find(Button).at(0) @@ -60,8 +65,8 @@ describe('Visit Tab', () => { expect(modal.prop('show')).toBeTruthy() }) - it('should close the modal when the close button is clicked', () => { - const { wrapper } = setup('/patients/123/visits', [Permissions.AddVisit]) + it('should close the modal when the close button is clicked', async () => { + const { wrapper } = await setup('/patients/123/visits', [Permissions.AddVisit]) act(() => { const addNewButton = wrapper.find(Button).at(0) @@ -80,20 +85,20 @@ describe('Visit Tab', () => { expect(wrapper.find(AddVisitModal).prop('show')).toBeFalsy() }) - it('should not render visit button if user does not have permissions', () => { - const { wrapper } = setup('/patients/123/visits', []) + it('should not render visit button if user does not have permissions', async () => { + const { wrapper } = await setup('/patients/123/visits', []) expect(wrapper.find(Button)).toHaveLength(0) }) - it('should render the visits table when on /patient/:id/visits', () => { - const { wrapper } = setup('/patients/123/visits', [Permissions.ReadVisits]) + it('should render the visits table when on /patient/:id/visits', async () => { + const { wrapper } = await setup('/patients/123/visits', [Permissions.ReadVisits]) expect(wrapper.find(VisitTable)).toHaveLength(1) }) - it('should render the visit view when on /patient/:id/visits/:visitId', () => { - const { wrapper } = setup('/patients/123/visits/456', [Permissions.ReadVisits]) + it('should render the visit view when on /patient/:id/visits/:visitId', async () => { + const { wrapper } = await setup('/patients/123/visits/456', [Permissions.ReadVisits]) expect(wrapper.find(ViewVisit)).toHaveLength(1) }) diff --git a/src/__tests__/patients/visits/VisitTable.test.tsx b/src/__tests__/patients/visits/VisitTable.test.tsx index 9ba76016b4..0df60931dd 100644 --- a/src/__tests__/patients/visits/VisitTable.test.tsx +++ b/src/__tests__/patients/visits/VisitTable.test.tsx @@ -3,17 +3,12 @@ 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 VisitTable from '../../../patients/visits/VisitTable' +import PatientRepository from '../../../shared/db/PatientRepository' import Patient from '../../../shared/model/Patient' import Visit, { VisitStatus } from '../../../shared/model/Visit' -import { RootState } from '../../../shared/store' - -const mockStore = createMockStore([thunk]) describe('Visit Table', () => { const visit: Visit = { @@ -31,23 +26,27 @@ describe('Visit Table', () => { visits: [visit], } as Patient - const setup = () => { - const store = mockStore({ patient: { patient } } as any) + const setup = async () => { + jest.spyOn(PatientRepository, 'find').mockResolvedValue(patient) const history = createMemoryHistory() history.push(`/patients/${patient.id}/visits/${patient.visits[0].id}`) - const wrapper = mount( - + + let wrapper: any + await act(async () => { + wrapper = await mount( - - - , - ) + + , + ) + }) + + wrapper.update() return { wrapper: wrapper as ReactWrapper, history } } - it('should render a table', () => { - const { wrapper } = setup() + it('should render a table', async () => { + const { wrapper } = await setup() const table = wrapper.find(Table) const columns = table.prop('columns') @@ -76,8 +75,8 @@ describe('Visit Table', () => { expect(table.prop('data')).toEqual(patient.visits) }) - it('should navigate to the visit view when the view details button is clicked', () => { - const { wrapper, history } = setup() + it('should navigate to the visit view when the view details button is clicked', async () => { + const { wrapper, history } = await setup() const tr = wrapper.find('tr').at(1) diff --git a/src/__tests__/scheduling/appointments/Appointments.test.tsx b/src/__tests__/scheduling/appointments/Appointments.test.tsx index 2de4c800d8..869fc348a6 100644 --- a/src/__tests__/scheduling/appointments/Appointments.test.tsx +++ b/src/__tests__/scheduling/appointments/Appointments.test.tsx @@ -1,4 +1,4 @@ -import { mount } from 'enzyme' +import { mount, ReactWrapper } from 'enzyme' import React from 'react' import { act } from 'react-dom/test-utils' import { Provider } from 'react-redux' @@ -9,6 +9,7 @@ import thunk from 'redux-thunk' import Dashboard from '../../../dashboard/Dashboard' import HospitalRun from '../../../HospitalRun' import { addBreadcrumbs } from '../../../page-header/breadcrumbs/breadcrumbs-slice' +import * as titleUtil from '../../../page-header/title/TitleContext' import Appointments from '../../../scheduling/appointments/Appointments' import EditAppointment from '../../../scheduling/appointments/edit/EditAppointment' import NewAppointment from '../../../scheduling/appointments/new/NewAppointment' @@ -20,29 +21,54 @@ import Patient from '../../../shared/model/Patient' import Permissions from '../../../shared/model/Permissions' import { RootState } from '../../../shared/store' +const { TitleProvider } = titleUtil const mockStore = createMockStore([thunk]) +let route: any describe('/appointments', () => { - it('should render the appointments screen when /appointments is accessed', async () => { + // eslint-disable-next-line no-shadow + const setup = (route: string, permissions: Permissions[], renderHr: boolean = false) => { + const appointment = { + id: '123', + patient: '456', + } as Appointment + + const patient = { + id: '456', + } as Patient + + jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) + jest.spyOn(AppointmentRepository, 'find').mockResolvedValue(appointment) + jest.spyOn(PatientRepository, 'find').mockResolvedValue(patient) + const store = mockStore({ title: 'test', - user: { permissions: [Permissions.ReadAppointments] }, - appointments: { appointments: [] }, + user: { user: { id: '123' }, permissions }, + appointment: { appointment, patient: { id: '456' } as Patient }, + appointments: [{ appointment, patient: { id: '456' } as Patient }], breadcrumbs: { breadcrumbs: [] }, components: { sidebarCollapsed: false }, } as any) const wrapper = mount( - - + + {renderHr ? : } , ) + if (!renderHr) { + wrapper.find(Appointments).props().updateTitle = jest.fn() + } + wrapper.update() - await act(async () => { - wrapper.update() - }) + return { wrapper: wrapper as ReactWrapper, store } + } + + it('should render the appointments screen when /appointments is accessed', async () => { + route = '/appointments' + const permissions: Permissions[] = [Permissions.ReadAppointments] + const { wrapper, store } = setup(route, permissions) expect(wrapper.find(ViewAppointments)).toHaveLength(1) @@ -54,46 +80,66 @@ describe('/appointments', () => { ) }) - it('should render the Dashboard when the user does not have read appointment privileges', () => { - const wrapper = mount( - - - - - , - ) + it('should render the Dashboard when the user does not have read appointment privileges', async () => { + route = '/appointments' + const permissions: Permissions[] = [] + const { wrapper } = setup(route, permissions, true) + await act(async () => { + wrapper.update() + }) expect(wrapper.find(Dashboard)).toHaveLength(1) }) }) describe('/appointments/new', () => { - it('should render the new appointment screen when /appointments/new is accessed', async () => { + // eslint-disable-next-line no-shadow + const setup = (route: string, permissions: Permissions[], renderHr: boolean = false) => { + const appointment = { + id: '123', + patient: '456', + } as Appointment + + const patient = { + id: '456', + } as Patient + + jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) + jest.spyOn(AppointmentRepository, 'find').mockResolvedValue(appointment) + jest.spyOn(PatientRepository, 'find').mockResolvedValue(patient) + const store = mockStore({ title: 'test', - user: { permissions: [Permissions.WriteAppointments] }, - appointment: {}, + user: { user: { id: '123' }, permissions }, + appointment: { appointment, patient: { id: '456' } as Patient }, + appointments: [{ appointment, patient: { id: '456' } as Patient }], breadcrumbs: { breadcrumbs: [] }, components: { sidebarCollapsed: false }, } as any) const wrapper = mount( - - + + {renderHr ? : } , ) - + if (!renderHr) { + wrapper.find(NewAppointment).props().updateTitle = jest.fn() + } wrapper.update() + return { wrapper: wrapper as ReactWrapper, store } + } + it('should render the new appointment screen when /appointments/new is accessed', async () => { + route = '/appointments/new' + const permissions: Permissions[] = [Permissions.WriteAppointments] + const { wrapper, store } = setup(route, permissions, false) + + await act(async () => { + await wrapper.update() + }) + expect(wrapper.find(NewAppointment)).toHaveLength(1) expect(store.getActions()).toContainEqual( addBreadcrumbs([ @@ -105,27 +151,17 @@ describe('/appointments/new', () => { }) it('should render the Dashboard when the user does not have read appointment privileges', () => { - const wrapper = mount( - - - - - , - ) + route = '/appointments/new' + const permissions: Permissions[] = [] + const { wrapper } = setup(route, permissions, true) expect(wrapper.find(Dashboard)).toHaveLength(1) }) }) describe('/appointments/edit/:id', () => { - it('should render the edit appointment screen when /appointments/edit/:id is accessed', () => { + // eslint-disable-next-line no-shadow + const setup = (route: string, permissions: Permissions[], renderHr: boolean = false) => { const appointment = { id: '123', patient: '456', @@ -135,24 +171,38 @@ describe('/appointments/edit/:id', () => { id: '456', } as Patient + jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) jest.spyOn(AppointmentRepository, 'find').mockResolvedValue(appointment) jest.spyOn(PatientRepository, 'find').mockResolvedValue(patient) const store = mockStore({ title: 'test', - user: { permissions: [Permissions.WriteAppointments, Permissions.ReadAppointments] }, - appointment: { appointment, patient: {} as Patient }, + user: { user: { id: '123' }, permissions }, + appointment: { appointment, patient: { id: '456' } as Patient }, + appointments: [{ appointment, patient: { id: '456' } as Patient }], breadcrumbs: { breadcrumbs: [] }, components: { sidebarCollapsed: false }, } as any) const wrapper = mount( - - + + {renderHr ? : } , ) + if (!renderHr) { + wrapper.find(EditAppointment).props().updateTitle = jest.fn() + } + wrapper.update() + + return { wrapper: wrapper as ReactWrapper, store } + } + + it('should render the edit appointment screen when /appointments/edit/:id is accessed', () => { + route = '/appointments/edit/123' + const permissions: Permissions[] = [Permissions.WriteAppointments, Permissions.ReadAppointments] + const { wrapper, store } = setup(route, permissions, false) expect(wrapper.find(EditAppointment)).toHaveLength(1) @@ -170,39 +220,17 @@ describe('/appointments/edit/:id', () => { }) it('should render the Dashboard when the user does not have read appointment privileges', () => { - const wrapper = mount( - - - - - , - ) + route = '/appointments/edit/123' + const permissions: Permissions[] = [] + const { wrapper } = setup(route, permissions, true) expect(wrapper.find(Dashboard)).toHaveLength(1) }) it('should render the Dashboard when the user does not have write appointment privileges', () => { - const wrapper = mount( - - - - - , - ) + route = '/appointments/edit/123' + const permissions: Permissions[] = [] + const { wrapper } = setup(route, permissions, true) expect(wrapper.find(Dashboard)).toHaveLength(1) }) diff --git a/src/__tests__/scheduling/appointments/ViewAppointments.test.tsx b/src/__tests__/scheduling/appointments/ViewAppointments.test.tsx index d1f7590fd7..c58addfc0a 100644 --- a/src/__tests__/scheduling/appointments/ViewAppointments.test.tsx +++ b/src/__tests__/scheduling/appointments/ViewAppointments.test.tsx @@ -9,7 +9,7 @@ import thunk from 'redux-thunk' import { mocked } from 'ts-jest/utils' import * as ButtonBarProvider from '../../../page-header/button-toolbar/ButtonBarProvider' -import * as titleUtil from '../../../page-header/title/useTitle' +import * as titleUtil from '../../../page-header/title/TitleContext' import ViewAppointments from '../../../scheduling/appointments/ViewAppointments' import AppointmentRepository from '../../../shared/db/AppointmentRepository' import PatientRepository from '../../../shared/db/PatientRepository' @@ -17,6 +17,8 @@ import Appointment from '../../../shared/model/Appointment' import Patient from '../../../shared/model/Patient' import { RootState } from '../../../shared/store' +const { TitleProvider } = titleUtil + describe('ViewAppointments', () => { const expectedAppointments = [ { @@ -31,6 +33,7 @@ describe('ViewAppointments', () => { ] as Appointment[] const setup = async () => { + jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) jest.spyOn(AppointmentRepository, 'findAll').mockResolvedValue(expectedAppointments) jest.spyOn(PatientRepository, 'find') const mockedPatientRepository = mocked(PatientRepository, true) @@ -42,18 +45,19 @@ describe('ViewAppointments', () => { return mount( - + + + , ) } - it('should use "Appointments" as the header', async () => { - jest.spyOn(titleUtil, 'default') + it('should have called the useUpdateTitle hook', async () => { await act(async () => { await setup() }) - expect(titleUtil.default).toHaveBeenCalledWith('scheduling.appointments.label') + expect(titleUtil.useUpdateTitle).toHaveBeenCalled() }) it('should add a "New Appointment" button to the button tool bar', async () => { diff --git a/src/__tests__/scheduling/appointments/edit/EditAppointment.test.tsx b/src/__tests__/scheduling/appointments/edit/EditAppointment.test.tsx index 5745e2dc14..02783de5fa 100644 --- a/src/__tests__/scheduling/appointments/edit/EditAppointment.test.tsx +++ b/src/__tests__/scheduling/appointments/edit/EditAppointment.test.tsx @@ -1,6 +1,6 @@ import { Button } from '@hospitalrun/components' import { roundToNearestMinutes, addMinutes } from 'date-fns' -import { mount } from 'enzyme' +import { mount, ReactWrapper } from 'enzyme' import { createMemoryHistory } from 'history' import React from 'react' import { act } from 'react-dom/test-utils' @@ -10,7 +10,7 @@ import createMockStore, { MockStore } from 'redux-mock-store' import thunk from 'redux-thunk' import { mocked } from 'ts-jest/utils' -import * as titleUtil from '../../../../page-header/title/useTitle' +import * as titleUtil from '../../../../page-header/title/TitleContext' import * as appointmentSlice from '../../../../scheduling/appointments/appointment-slice' import AppointmentDetailForm from '../../../../scheduling/appointments/AppointmentDetailForm' import EditAppointment from '../../../../scheduling/appointments/edit/EditAppointment' @@ -20,6 +20,7 @@ import Appointment from '../../../../shared/model/Appointment' import Patient from '../../../../shared/model/Patient' import { RootState } from '../../../../shared/store' +const { TitleProvider } = titleUtil const mockStore = createMockStore([thunk]) describe('Edit Appointment', () => { @@ -33,7 +34,7 @@ describe('Edit Appointment', () => { type: 'type', } as Appointment - const patient = { + const patient = ({ id: '456', prefix: 'prefix', givenName: 'givenName', @@ -49,12 +50,13 @@ describe('Edit Appointment', () => { address: 'address', code: 'P00001', dateOfBirth: new Date().toISOString(), - } as Patient + } as unknown) as Patient let history: any let store: MockStore - const setup = () => { + const setup = async () => { + jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) jest.spyOn(AppointmentRepository, 'saveOrUpdate') jest.spyOn(AppointmentRepository, 'find') jest.spyOn(PatientRepository, 'find') @@ -70,18 +72,21 @@ describe('Edit Appointment', () => { store = mockStore({ appointment: { appointment, patient } } as any) history.push('/appointments/edit/123') - const wrapper = mount( + const wrapper = await mount( - + + + , ) + wrapper.find(EditAppointment).props().updateTitle = jest.fn() wrapper.update() - return wrapper + return { wrapper: wrapper as ReactWrapper } } beforeEach(() => { @@ -89,19 +94,15 @@ describe('Edit Appointment', () => { }) it('should render an edit appointment form', async () => { - let wrapper: any - await act(async () => { - wrapper = await setup() - }) - - wrapper.update() + const { wrapper } = await setup() expect(wrapper.find(AppointmentDetailForm)).toHaveLength(1) }) it('should dispatch fetchAppointment when component loads', async () => { + const { wrapper } = await setup() await act(async () => { - await setup() + await wrapper.update() }) expect(AppointmentRepository.find).toHaveBeenCalledWith(appointment.id) @@ -112,22 +113,17 @@ describe('Edit Appointment', () => { ) }) - it('should use the correct title', async () => { - jest.spyOn(titleUtil, 'default') - await act(async () => { - await setup() - }) - expect(titleUtil.default).toHaveBeenCalledWith('scheduling.appointments.editAppointment') + it('should have called useUpdateTitle hook', async () => { + await setup() + expect(titleUtil.useUpdateTitle).toHaveBeenCalled() }) it('should dispatch updateAppointment when save button is clicked', async () => { - let wrapper: any + const { wrapper } = await setup() await act(async () => { - wrapper = await setup() + await wrapper.update() }) - wrapper.update() - const saveButton = wrapper.find(Button).at(0) const onClick = saveButton.prop('onClick') as any expect(saveButton.text().trim()).toEqual('actions.save') @@ -144,12 +140,7 @@ describe('Edit Appointment', () => { }) it('should navigate to /appointments/:id when save is successful', async () => { - let wrapper: any - await act(async () => { - wrapper = await setup() - }) - - wrapper.update() + const { wrapper } = await setup() const saveButton = wrapper.find(Button).at(0) const onClick = saveButton.prop('onClick') as any @@ -162,12 +153,7 @@ describe('Edit Appointment', () => { }) it('should navigate to /appointments/:id when cancel is clicked', async () => { - let wrapper: any - await act(async () => { - wrapper = await setup() - }) - - wrapper.update() + const { wrapper } = await setup() const cancelButton = wrapper.find(Button).at(1) const onClick = cancelButton.prop('onClick') as any diff --git a/src/__tests__/scheduling/appointments/new/NewAppointment.test.tsx b/src/__tests__/scheduling/appointments/new/NewAppointment.test.tsx index 2f81a3b7dd..2d876a0f34 100644 --- a/src/__tests__/scheduling/appointments/new/NewAppointment.test.tsx +++ b/src/__tests__/scheduling/appointments/new/NewAppointment.test.tsx @@ -10,7 +10,7 @@ import createMockStore, { MockStore } from 'redux-mock-store' import thunk from 'redux-thunk' import { mocked } from 'ts-jest/utils' -import * as titleUtil from '../../../../page-header/title/useTitle' +import * as titleUtil from '../../../../page-header/title/TitleContext' import * as appointmentSlice from '../../../../scheduling/appointments/appointment-slice' import AppointmentDetailForm from '../../../../scheduling/appointments/AppointmentDetailForm' import NewAppointment from '../../../../scheduling/appointments/new/NewAppointment' @@ -21,6 +21,7 @@ import Lab from '../../../../shared/model/Lab' import Patient from '../../../../shared/model/Patient' import { RootState } from '../../../../shared/store' +const { TitleProvider } = titleUtil const mockStore = createMockStore([thunk]) const mockedComponents = mocked(components, true) @@ -30,6 +31,7 @@ describe('New Appointment', () => { const expectedNewAppointment = { id: '123' } const setup = () => { + jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) jest.spyOn(AppointmentRepository, 'save') mocked(AppointmentRepository, true).save.mockResolvedValue( expectedNewAppointment as Appointment, @@ -49,7 +51,9 @@ describe('New Appointment', () => { - + + + , @@ -60,13 +64,12 @@ describe('New Appointment', () => { } describe('header', () => { - it('should use "New Appointment" as the header', async () => { - jest.spyOn(titleUtil, 'default') + it('should have called useUpdateTitle hook', async () => { await act(async () => { await setup() }) - expect(titleUtil.default).toHaveBeenCalledWith('scheduling.appointments.new') + expect(titleUtil.useUpdateTitle).toHaveBeenCalled() }) }) diff --git a/src/__tests__/scheduling/appointments/view/ViewAppointment.test.tsx b/src/__tests__/scheduling/appointments/view/ViewAppointment.test.tsx index 3490f28e18..c8687753a1 100644 --- a/src/__tests__/scheduling/appointments/view/ViewAppointment.test.tsx +++ b/src/__tests__/scheduling/appointments/view/ViewAppointment.test.tsx @@ -10,7 +10,7 @@ import thunk from 'redux-thunk' import { mocked } from 'ts-jest/utils' import * as ButtonBarProvider from '../../../../page-header/button-toolbar/ButtonBarProvider' -import * as titleUtil from '../../../../page-header/title/useTitle' +import * as titleUtil from '../../../../page-header/title/TitleContext' import * as appointmentSlice from '../../../../scheduling/appointments/appointment-slice' import AppointmentDetailForm from '../../../../scheduling/appointments/AppointmentDetailForm' import ViewAppointment from '../../../../scheduling/appointments/view/ViewAppointment' @@ -21,6 +21,7 @@ import Patient from '../../../../shared/model/Patient' import Permissions from '../../../../shared/model/Permissions' import { RootState } from '../../../../shared/store' +const { TitleProvider } = titleUtil const mockStore = createMockStore([thunk]) const appointment = { @@ -41,6 +42,7 @@ describe('View Appointment', () => { let store: MockStore const setup = async (status = 'completed', permissions = [Permissions.ReadAppointments]) => { + jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) jest.spyOn(AppointmentRepository, 'find').mockResolvedValue(appointment) jest.spyOn(AppointmentRepository, 'delete').mockResolvedValue(appointment) jest.spyOn(PatientRepository, 'find').mockResolvedValue(patient) @@ -65,7 +67,9 @@ describe('View Appointment', () => { - + + + , @@ -80,11 +84,10 @@ describe('View Appointment', () => { jest.restoreAllMocks() }) - it('should use the correct title', async () => { - jest.spyOn(titleUtil, 'default') + it('should have called the useUpdateTitle hook', async () => { await setup() - expect(titleUtil.default).toHaveBeenCalledWith('scheduling.appointments.viewAppointment') + expect(titleUtil.useUpdateTitle).toHaveBeenCalled() }) it('should add a "Edit Appointment" button to the button tool bar if has WriteAppointment permissions', async () => { diff --git a/src/__tests__/settings/Settings.test.tsx b/src/__tests__/settings/Settings.test.tsx index 18ad6a78f4..1d3b89de64 100644 --- a/src/__tests__/settings/Settings.test.tsx +++ b/src/__tests__/settings/Settings.test.tsx @@ -6,15 +6,16 @@ import { Router } from 'react-router-dom' import createMockStore from 'redux-mock-store' import thunk from 'redux-thunk' -import * as titleUtil from '../../page-header/title/useTitle' +import * as titleUtil from '../../page-header/title/TitleContext' import Settings from '../../settings/Settings' import { RootState } from '../../shared/store' +const { TitleProvider } = titleUtil const mockStore = createMockStore([thunk]) describe('Settings', () => { const setup = () => { - jest.spyOn(titleUtil, 'default') + jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) const store = mockStore({ title: 'test' } as any) @@ -24,7 +25,9 @@ describe('Settings', () => { const wrapper = mount( - + + + , ) @@ -33,9 +36,9 @@ describe('Settings', () => { } describe('layout', () => { - it('should set the title', () => { + it('should call the useUpdateTitle hook', () => { setup() - expect(titleUtil.default).toHaveBeenCalledWith('settings.label') + expect(titleUtil.useUpdateTitle).toHaveBeenCalled() }) }) }) diff --git a/src/__tests__/shared/components/Sidebar.test.tsx b/src/__tests__/shared/components/Sidebar.test.tsx index 944eb108f9..7602971847 100644 --- a/src/__tests__/shared/components/Sidebar.test.tsx +++ b/src/__tests__/shared/components/Sidebar.test.tsx @@ -36,6 +36,7 @@ describe('Sidebar', () => { Permissions.ViewMedication, Permissions.ViewIncidents, Permissions.ViewIncident, + Permissions.ViewIncidentWidgets, Permissions.ReportIncident, Permissions.ReadVisits, Permissions.AddVisit, @@ -464,18 +465,18 @@ describe('Sidebar', () => { const wrapper = setup('/incidents') const listItems = wrapper.find(ListItem) - const lastOne = listItems.length - 1 + const reportsLabel = listItems.length - 2 - expect(listItems.at(lastOne).text().trim()).toBe('incidents.reports.label') + expect(listItems.at(reportsLabel).text().trim()).toBe('incidents.reports.label') expect( listItems - .at(lastOne - 1) + .at(reportsLabel - 1) .text() .trim(), ).toBe('incidents.reports.new') expect( listItems - .at(lastOne - 2) + .at(reportsLabel - 2) .text() .trim(), ).toBe('incidents.label') @@ -517,6 +518,23 @@ describe('Sidebar', () => { expect(incidentsIndex).toBe(-1) }) + it('should render the incidents visualize link', () => { + const wrapper = setup('/incidents') + + const listItems = wrapper.find(ListItem) + expect(listItems.at(10).text().trim()).toEqual('incidents.visualize.label') + }) + + it('should not render the incidents visualize link when user does not have the view incident widgets privileges', () => { + const wrapper = setupNoPermissions('/incidents') + + const listItems = wrapper.find(ListItem) + + listItems.forEach((_, i) => { + expect(listItems.at(i).text().trim()).not.toEqual('incidents.visualize.label') + }) + }) + it('main incidents link should be active when the current path is /incidents', () => { const wrapper = setup('/incidents') @@ -563,6 +581,19 @@ describe('Sidebar', () => { expect(history.location.pathname).toEqual('/incidents/new') }) + it('should navigate to /incidents/visualize when the incidents visualize link is clicked', () => { + const wrapper = setup('/incidents') + + const listItems = wrapper.find(ListItem) + + act(() => { + const onClick = listItems.at(10).prop('onClick') as any + onClick() + }) + + expect(history.location.pathname).toEqual('/incidents/visualize') + }) + it('incidents list link should be active when the current path is /incidents', () => { const wrapper = setup('/incidents') diff --git a/src/__tests__/shared/components/input/SelectWithLabelFormGroup.test.tsx b/src/__tests__/shared/components/input/SelectWithLabelFormGroup.test.tsx index d4cf9cc75a..0d0e9dc903 100644 --- a/src/__tests__/shared/components/input/SelectWithLabelFormGroup.test.tsx +++ b/src/__tests__/shared/components/input/SelectWithLabelFormGroup.test.tsx @@ -2,7 +2,7 @@ import { Label, Select } from '@hospitalrun/components' import { shallow } from 'enzyme' import React from 'react' -import SelectWithLabelFormGroup from '../../../../shared/components/input/SelectWithLableFormGroup' +import SelectWithLabelFormGroup from '../../../../shared/components/input/SelectWithLabelFormGroup' describe('select with label form group', () => { describe('layout', () => { diff --git a/src/__tests__/shared/utils/DataHelpers.test.ts b/src/__tests__/shared/utils/DataHelpers.test.ts new file mode 100644 index 0000000000..16342f7414 --- /dev/null +++ b/src/__tests__/shared/utils/DataHelpers.test.ts @@ -0,0 +1,33 @@ +import { getCSV, DownloadLink } from '../../../shared/util/DataHelpers' + +describe('Use Data Helpers util', () => { + it('should construct csv', () => { + const input = [ + { + code: 'I-eClU6OdkR', + date: '2020-09-06 12:02 PM', + reportedBy: 'some user', + reportedOn: '2020-09-06 12:02 PM', + status: 'reported', + }, + ] + const output = getCSV(input).replace(/(\r\n|\n|\r)/gm, '') + const expectedOutput = + '"code","date","reportedBy","reportedOn","status""I-eClU6OdkR","2020-09-06 12:02 PM","some user","2020-09-06 12:02 PM","reported"' + expect(output).toMatch(expectedOutput) + }) + + it('should download data as expected', () => { + const response = DownloadLink('data to be downloaded', 'filename.txt') + + const element = document.createElement('a') + element.setAttribute( + 'href', + `data:text/plain;charset=utf-8,${encodeURIComponent('data to be downloaded')}`, + ) + element.setAttribute('download', 'filename.txt') + + element.style.display = 'none' + expect(response).toEqual(element) + }) +}) diff --git a/src/dashboard/Dashboard.tsx b/src/dashboard/Dashboard.tsx index 216c5fe3ef..cf2a1910e0 100644 --- a/src/dashboard/Dashboard.tsx +++ b/src/dashboard/Dashboard.tsx @@ -1,11 +1,12 @@ import React from 'react' -import useTitle from '../page-header/title/useTitle' +import { useUpdateTitle } from '../page-header/title/TitleContext' import useTranslator from '../shared/hooks/useTranslator' const Dashboard: React.FC = () => { const { t } = useTranslator() - useTitle(t('dashboard.label')) + const updateTitle = useUpdateTitle() + updateTitle(t('dashboard.label')) return

Example

} diff --git a/src/imagings/Imagings.tsx b/src/imagings/Imagings.tsx index ce9bc893e3..4b13fa1563 100644 --- a/src/imagings/Imagings.tsx +++ b/src/imagings/Imagings.tsx @@ -7,7 +7,7 @@ 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' +import ImagingRequests from './search/ViewImagings' const Imagings = () => { const { permissions } = useSelector((state: RootState) => state.user) diff --git a/src/imagings/ViewImagings.tsx b/src/imagings/ViewImagings.tsx deleted file mode 100644 index 96b2bd4b9f..0000000000 --- a/src/imagings/ViewImagings.tsx +++ /dev/null @@ -1,92 +0,0 @@ -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 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 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(' ', searchFilter)) - }, [dispatch, searchFilter]) - - useEffect(() => { - setButtons(getButtons()) - return () => { - setButtons([]) - } - }, [dispatch, getButtons, setButtons]) - - return ( - <> -
- 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: 'fullName' }, - { - label: t('imagings.imaging.requestedBy'), - key: 'requestedBy', - formatter: (row) => extractUsername(row.requestedBy), - }, - { label: t('imagings.imaging.status'), key: 'status' }, - ]} - data={imagings} - /> - - - ) -} - -export default ViewImagings diff --git a/src/imagings/hooks/useImagingRequest.tsx b/src/imagings/hooks/useImagingRequest.tsx new file mode 100644 index 0000000000..968107ed8c --- /dev/null +++ b/src/imagings/hooks/useImagingRequest.tsx @@ -0,0 +1,12 @@ +import { useQuery } from 'react-query' + +import ImagingRepository from '../../shared/db/ImagingRepository' +import Imaging from '../../shared/model/Imaging' + +function getImagingRequestById(_: string, imagingRequestId: string): Promise { + return ImagingRepository.find(imagingRequestId) +} + +export default function useImagingRequest(imagingRequestId: string) { + return useQuery(['imaging', imagingRequestId], getImagingRequestById) +} diff --git a/src/imagings/hooks/useImagingSearch.tsx b/src/imagings/hooks/useImagingSearch.tsx new file mode 100644 index 0000000000..db661af6af --- /dev/null +++ b/src/imagings/hooks/useImagingSearch.tsx @@ -0,0 +1,23 @@ +import { useQuery } from 'react-query' + +import ImagingRepository from '../../shared/db/ImagingRepository' +import SortRequest from '../../shared/db/SortRequest' +import Imaging from '../../shared/model/Imaging' +import ImagingSearchRequest from '../model/ImagingSearchRequest' + +const defaultSortRequest: SortRequest = { + sorts: [ + { + field: 'requestedOn', + direction: 'desc', + }, + ], +} + +function searchImagingRequests(_: string, searchRequest: ImagingSearchRequest): Promise { + return ImagingRepository.search({ ...searchRequest, defaultSortRequest }) +} + +export default function useImagingSearch(searchRequest: ImagingSearchRequest) { + return useQuery(['imagings', searchRequest], searchImagingRequests) +} diff --git a/src/imagings/hooks/useRequestImaging.tsx b/src/imagings/hooks/useRequestImaging.tsx new file mode 100644 index 0000000000..eba9c52bbc --- /dev/null +++ b/src/imagings/hooks/useRequestImaging.tsx @@ -0,0 +1,38 @@ +import { isEmpty } from 'lodash' +import { queryCache, useMutation } from 'react-query' + +import ImagingRepository from '../../shared/db/ImagingRepository' +import Imaging from '../../shared/model/Imaging' +import validateImagingRequest from '../util/validate-imaging-request' + +export interface ImagingRequest { + status: 'completed' | 'requested' | 'canceled' + patient: string + visitId: string + fullName: string + notes?: string + type: string +} + +async function requestImaging(request: ImagingRequest): Promise { + const error = validateImagingRequest(request) + + if (!isEmpty(error)) { + throw error + } + + await ImagingRepository.save({ + ...request, + requestedBy: 'test', + requestedOn: new Date(Date.now()).toISOString(), + } as Imaging) +} + +export default function useRequestImaging() { + return useMutation(requestImaging, { + onSuccess: async () => { + await queryCache.invalidateQueries('imagings') + }, + throwOnError: true, + }) +} diff --git a/src/imagings/imaging-slice.ts b/src/imagings/imaging-slice.ts deleted file mode 100644 index 70bd442e96..0000000000 --- a/src/imagings/imaging-slice.ts +++ /dev/null @@ -1,104 +0,0 @@ -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 - visitId?: 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 deleted file mode 100644 index e58fd2fc0d..0000000000 --- a/src/imagings/imagings-slice.ts +++ /dev/null @@ -1,66 +0,0 @@ -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/model/ImagingSearchRequest.ts b/src/imagings/model/ImagingSearchRequest.ts new file mode 100644 index 0000000000..8f90ec2437 --- /dev/null +++ b/src/imagings/model/ImagingSearchRequest.ts @@ -0,0 +1,4 @@ +export default interface ImagingSearchRequest { + status: 'completed' | 'requested' | 'canceled' | 'all' + text: string +} diff --git a/src/imagings/requests/NewImagingRequest.tsx b/src/imagings/requests/NewImagingRequest.tsx index f6432f7e47..4ad9848f97 100644 --- a/src/imagings/requests/NewImagingRequest.tsx +++ b/src/imagings/requests/NewImagingRequest.tsx @@ -1,29 +1,28 @@ import { Typeahead, Label, Button, Alert, Column, Row } from '@hospitalrun/components' import format from 'date-fns/format' 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 { useUpdateTitle } from '../../page-header/title/TitleContext' import SelectWithLabelFormGroup, { Option, -} from '../../shared/components/input/SelectWithLableFormGroup' +} from '../../shared/components/input/SelectWithLabelFormGroup' 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' +import useRequestImaging, { ImagingRequest } from '../hooks/useRequestImaging' +import { ImagingRequestError } from '../util/validate-imaging-request' 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 updateTitle = useUpdateTitle() + updateTitle(t('imagings.requests.new')) + const [mutate] = useRequestImaging() + const [error, setError] = useState() const [visitOption, setVisitOption] = useState([] as Option[]) const statusOptions: Option[] = [ @@ -32,12 +31,12 @@ const NewImagingRequest = () => { { label: t('imagings.status.canceled'), value: 'canceled' }, ] - const [newImagingRequest, setNewImagingRequest] = useState({ + const [newImagingRequest, setNewImagingRequest] = useState({ patient: '', fullName: '', - type: '', + status: 'requested', notes: '', - status: '', + type: '', visitId: '', }) @@ -85,7 +84,7 @@ const NewImagingRequest = () => { const onStatusChange = (value: string) => { setNewImagingRequest((previousNewImagingRequest) => ({ ...previousNewImagingRequest, - status: value, + status: value as 'completed' | 'canceled' | 'requested', })) } @@ -105,12 +104,12 @@ const NewImagingRequest = () => { } const onSave = async () => { - const newImaging = newImagingRequest as Imaging - const onSuccess = () => { + try { + await mutate(newImagingRequest) history.push(`/imaging`) + } catch (e) { + setError(e) } - - dispatch(requestImaging(newImaging, onSuccess)) } const onCancel = () => { @@ -126,8 +125,8 @@ const NewImagingRequest = () => { return ( <> - {status === 'error' && ( - + {error !== undefined && ( + )}
@@ -143,8 +142,8 @@ const NewImagingRequest = () => { onSearch={async (query: string) => PatientRepository.search(query)} searchAccessor="fullName" renderMenuItemChildren={(p: Patient) =>
{`${p.fullName} (${p.code})`}
} - isInvalid={!!error.patient} - feedback={t(error.patient as string)} + isInvalid={!!error?.patient} + feedback={t(error?.patient)} /> @@ -170,8 +169,8 @@ const NewImagingRequest = () => { label={t('imagings.imaging.type')} isRequired isEditable - isInvalid={!!error.type} - feedback={t(error.type as string)} + isInvalid={!!error?.type} + feedback={t(error?.type)} value={newImagingRequest.type} onChange={onImagingTypeChange} /> diff --git a/src/imagings/search/ImagingRequestTable.tsx b/src/imagings/search/ImagingRequestTable.tsx new file mode 100644 index 0000000000..efeef47676 --- /dev/null +++ b/src/imagings/search/ImagingRequestTable.tsx @@ -0,0 +1,49 @@ +import { Table } from '@hospitalrun/components' +import format from 'date-fns/format' +import React from 'react' + +import Loading from '../../shared/components/Loading' +import useTranslator from '../../shared/hooks/useTranslator' +import { extractUsername } from '../../shared/util/extractUsername' +import useImagingSearch from '../hooks/useImagingSearch' +import ImagingSearchRequest from '../model/ImagingSearchRequest' + +interface Props { + searchRequest: ImagingSearchRequest +} + +const ImagingRequestTable = (props: Props) => { + const { searchRequest } = props + const { t } = useTranslator() + const { data, status } = useImagingSearch(searchRequest) + + if (data === undefined || status === 'loading') { + return + } + + return ( +
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: 'fullName' }, + { + label: t('imagings.imaging.requestedBy'), + key: 'requestedBy', + formatter: (row) => extractUsername(row.requestedBy), + }, + { label: t('imagings.imaging.status'), key: 'status' }, + ]} + data={data} + /> + ) +} + +export default ImagingRequestTable diff --git a/src/imagings/search/ViewImagings.tsx b/src/imagings/search/ViewImagings.tsx new file mode 100644 index 0000000000..5c31dfa4b1 --- /dev/null +++ b/src/imagings/search/ViewImagings.tsx @@ -0,0 +1,65 @@ +import { Button } from '@hospitalrun/components' +import React, { useState, useEffect, useCallback } from 'react' +import { useSelector } from 'react-redux' +import { useHistory } from 'react-router-dom' + +import { useButtonToolbarSetter } from '../../page-header/button-toolbar/ButtonBarProvider' +import { useUpdateTitle } from '../../page-header/title/TitleContext' +import useTranslator from '../../shared/hooks/useTranslator' +import Permissions from '../../shared/model/Permissions' +import { RootState } from '../../shared/store' +import ImagingSearchRequest from '../model/ImagingSearchRequest' +import ImagingRequestTable from './ImagingRequestTable' + +const ViewImagings = () => { + const { t } = useTranslator() + const { permissions } = useSelector((state: RootState) => state.user) + const history = useHistory() + const setButtons = useButtonToolbarSetter() + const updateTitle = useUpdateTitle() + updateTitle(t('imagings.label')) + + const [searchRequest, setSearchRequest] = useState({ + status: 'all', + text: '', + }) + + const getButtons = useCallback(() => { + const buttons: React.ReactNode[] = [] + + if (permissions.includes(Permissions.RequestImaging)) { + buttons.push( + , + ) + } + + return buttons + }, [permissions, history, t]) + + useEffect(() => { + setSearchRequest((previousRequest) => ({ ...previousRequest, status: 'all' })) + }, []) + + useEffect(() => { + setButtons(getButtons()) + return () => { + setButtons([]) + } + }, [getButtons, setButtons]) + + return ( +
+ +
+ ) +} + +export default ViewImagings diff --git a/src/imagings/util/validate-imaging-request.ts b/src/imagings/util/validate-imaging-request.ts new file mode 100644 index 0000000000..d077ecc0cc --- /dev/null +++ b/src/imagings/util/validate-imaging-request.ts @@ -0,0 +1,38 @@ +import Imaging from '../../shared/model/Imaging' + +const statusType: string[] = ['requested', 'completed', 'canceled'] + +export class ImagingRequestError extends Error { + patient?: string + + type?: string + + status?: string + + constructor(message: string, patient?: string, type?: string, status?: string) { + super(message) + this.patient = patient + this.type = type + this.status = status + Object.setPrototypeOf(this, ImagingRequestError.prototype) + } +} + +export default function validateImagingRequest(request: Partial) { + const imagingRequestError = {} as any + if (!request.patient) { + imagingRequestError.patient = 'imagings.requests.error.patientRequired' + } + + if (!request.type) { + imagingRequestError.type = 'imagings.requests.error.typeRequired' + } + + if (!request.status) { + imagingRequestError.status = 'imagings.requests.error.statusRequired' + } else if (!statusType.includes(request.status)) { + imagingRequestError.status = 'imagings.requests.error.incorrectStatus' + } + + return imagingRequestError +} diff --git a/src/incidents/Incidents.tsx b/src/incidents/Incidents.tsx index 7b19355a56..2dcd39be90 100644 --- a/src/incidents/Incidents.tsx +++ b/src/incidents/Incidents.tsx @@ -9,6 +9,7 @@ import { RootState } from '../shared/store' import ViewIncidents from './list/ViewIncidents' import ReportIncident from './report/ReportIncident' import ViewIncident from './view/ViewIncident' +import VisualizeIncidents from './visualize/VisualizeIncidents' const Incidents = () => { const { permissions } = useSelector((state: RootState) => state.user) @@ -34,6 +35,12 @@ const Incidents = () => { path="/incidents/new" component={ReportIncident} /> + , - searchRequest: IncidentSearchRequest, -): Promise { +function fetchIncidents(_: string, searchRequest: IncidentSearchRequest): Promise { return IncidentRepository.search(searchRequest) } diff --git a/src/incidents/list/ViewIncidents.tsx b/src/incidents/list/ViewIncidents.tsx index 8882214a1f..c2092649ab 100644 --- a/src/incidents/list/ViewIncidents.tsx +++ b/src/incidents/list/ViewIncidents.tsx @@ -3,10 +3,10 @@ import React, { useEffect, useState } from 'react' import { useHistory } from 'react-router-dom' import { useButtonToolbarSetter } from '../../page-header/button-toolbar/ButtonBarProvider' -import useTitle from '../../page-header/title/useTitle' +import { useUpdateTitle } from '../../page-header/title/TitleContext' import SelectWithLabelFormGroup, { Option, -} from '../../shared/components/input/SelectWithLableFormGroup' +} from '../../shared/components/input/SelectWithLabelFormGroup' import useTranslator from '../../shared/hooks/useTranslator' import IncidentFilter from '../IncidentFilter' import ViewIncidentsTable from './ViewIncidentsTable' @@ -15,7 +15,8 @@ const ViewIncidents = () => { const { t } = useTranslator() const history = useHistory() const setButtonToolBar = useButtonToolbarSetter() - useTitle(t('incidents.reports.label')) + const updateTitle = useUpdateTitle() + updateTitle(t('incidents.reports.label')) const [searchFilter, setSearchFilter] = useState(IncidentFilter.reported) useEffect(() => { diff --git a/src/incidents/list/ViewIncidentsTable.tsx b/src/incidents/list/ViewIncidentsTable.tsx index d57532e3f4..e38f723de2 100644 --- a/src/incidents/list/ViewIncidentsTable.tsx +++ b/src/incidents/list/ViewIncidentsTable.tsx @@ -1,9 +1,10 @@ -import { Spinner, Table } from '@hospitalrun/components' +import { Spinner, Table, Dropdown } from '@hospitalrun/components' import format from 'date-fns/format' import React from 'react' import { useHistory } from 'react-router' import useTranslator from '../../shared/hooks/useTranslator' +import { DownloadLink, getCSV } from '../../shared/util/DataHelpers' import { extractUsername } from '../../shared/util/extractUsername' import useIncidents from '../hooks/useIncidents' import IncidentSearchRequest from '../model/IncidentSearchRequest' @@ -12,6 +13,27 @@ interface Props { searchRequest: IncidentSearchRequest } +export function populateExportData(dataToPopulate: any, theData: any) { + let first = true + if (theData != null) { + theData.forEach((elm: any) => { + const entry = { + code: elm.code, + date: format(new Date(elm.date), 'yyyy-MM-dd hh:mm a'), + reportedBy: elm.reportedBy, + reportedOn: format(new Date(elm.reportedOn), 'yyyy-MM-dd hh:mm a'), + status: elm.status, + } + if (first) { + dataToPopulate[0] = entry + first = false + } else { + dataToPopulate.push(entry) + } + }) + } +} + function ViewIncidentsTable(props: Props) { const { searchRequest } = props const { t } = useTranslator() @@ -22,33 +44,85 @@ function ViewIncidentsTable(props: Props) { return } + // filter data + const exportData = [{}] + + function downloadCSV() { + populateExportData(exportData, data) + + const csv = getCSV(exportData) + + const incidentsText = t('incidents.label') + + const filename = incidentsText + .concat('-') + .concat(format(new Date(Date.now()), 'yyyy-MM-dd--hh-mma')) + .concat('.csv') + + DownloadLink(csv, filename) + } + + const dropdownItems = [ + { + onClick: function runfun() { + downloadCSV() + }, + text: 'CSV', + }, + ] + + const dropStyle = { + marginLeft: 'auto', // note the capital 'W' here + marginBottom: '4px', // 'ms' is the only lowercase vendor prefix + } + return ( -
row.id} - data={data} - columns={[ - { label: t('incidents.reports.code'), key: 'code' }, - { - label: t('incidents.reports.dateOfIncident'), - key: 'date', - formatter: (row) => (row.date ? format(new Date(row.date), 'yyyy-MM-dd hh:mm a') : ''), - }, - { - label: t('incidents.reports.reportedBy'), - key: 'reportedBy', - formatter: (row) => extractUsername(row.reportedBy), - }, - { - label: t('incidents.reports.reportedOn'), - key: 'reportedOn', - formatter: (row) => - row.reportedOn ? format(new Date(row.reportedOn), 'yyyy-MM-dd hh:mm a') : '', - }, - { label: t('incidents.reports.status'), key: 'status' }, - ]} - actionsHeaderText={t('actions.label')} - actions={[{ label: t('actions.view'), action: (row) => history.push(`incidents/${row.id}`) }]} - /> + <> + +
row.id} + data={data} + columns={[ + { + label: t('incidents.reports.code'), + key: 'code', + }, + { + label: t('incidents.reports.dateOfIncident'), + key: 'date', + formatter: (row) => (row.date ? format(new Date(row.date), 'yyyy-MM-dd hh:mm a') : ''), + }, + { + label: t('incidents.reports.reportedBy'), + key: 'reportedBy', + formatter: (row) => extractUsername(row.reportedBy), + }, + { + label: t('incidents.reports.reportedOn'), + key: 'reportedOn', + formatter: (row) => + row.reportedOn ? format(new Date(row.reportedOn), 'yyyy-MM-dd hh:mm a') : '', + }, + { + label: t('incidents.reports.status'), + key: 'status', + }, + ]} + actionsHeaderText={t('actions.label')} + actions={[ + { + label: t('actions.view'), + action: (row) => history.push(`incidents/${row.id}`), + }, + ]} + /> + ) } diff --git a/src/incidents/report/ReportIncident.tsx b/src/incidents/report/ReportIncident.tsx index d3ad799e2c..be4fbfeb9f 100644 --- a/src/incidents/report/ReportIncident.tsx +++ b/src/incidents/report/ReportIncident.tsx @@ -3,7 +3,7 @@ import React, { useState } from 'react' import { useHistory } from 'react-router-dom' import useAddBreadcrumbs from '../../page-header/breadcrumbs/useAddBreadcrumbs' -import useTitle from '../../page-header/title/useTitle' +import { useUpdateTitle } from '../../page-header/title/TitleContext' import DateTimePickerWithLabelFormGroup from '../../shared/components/input/DateTimePickerWithLabelFormGroup' import TextFieldWithLabelFormGroup from '../../shared/components/input/TextFieldWithLabelFormGroup' import TextInputWithLabelFormGroup from '../../shared/components/input/TextInputWithLabelFormGroup' @@ -16,7 +16,8 @@ const ReportIncident = () => { const [mutate] = useReportIncident() const history = useHistory() const { t } = useTranslator() - useTitle(t('incidents.reports.new')) + const updateTitle = useUpdateTitle() + updateTitle(t('incidents.reports.new')) const breadcrumbs = [ { i18nKey: 'incidents.reports.new', @@ -50,7 +51,7 @@ const ReportIncident = () => { const onSave = async () => { try { const data = await mutate(incident as Incident) - history.push(`/incidents/${data.id}`) + history.push(`/incidents/${data?.id}`) } catch (e) { setError(e) } diff --git a/src/incidents/view/ViewIncident.tsx b/src/incidents/view/ViewIncident.tsx index 9122033fc0..faa3a99146 100644 --- a/src/incidents/view/ViewIncident.tsx +++ b/src/incidents/view/ViewIncident.tsx @@ -3,17 +3,20 @@ import { useSelector } from 'react-redux' import { useParams } from 'react-router-dom' import useAddBreadcrumbs from '../../page-header/breadcrumbs/useAddBreadcrumbs' -import useTitle from '../../page-header/title/useTitle' +import { useUpdateTitle } from '../../page-header/title/TitleContext' +import useTranslator from '../../shared/hooks/useTranslator' import { RootState } from '../../shared/store' import ViewIncidentDetails from './ViewIncidentDetails' const ViewIncident = () => { const { id } = useParams() const { permissions } = useSelector((root: RootState) => root.user) - useTitle('View Incident') + const { t } = useTranslator() + const updateTitle = useUpdateTitle() + updateTitle(t('incidents.reports.view')) useAddBreadcrumbs([ { - i18nKey: 'View Incident', + i18nKey: 'incidents.reports.view', location: `/incidents/${id}`, }, ]) diff --git a/src/incidents/visualize/VisualizeIncidents.tsx b/src/incidents/visualize/VisualizeIncidents.tsx new file mode 100644 index 0000000000..1b441753cf --- /dev/null +++ b/src/incidents/visualize/VisualizeIncidents.tsx @@ -0,0 +1,124 @@ +import { LineGraph, Spinner } from '@hospitalrun/components' +import React, { useEffect, useState } from 'react' + +import { useUpdateTitle } from '../../page-header/title/TitleContext' +import useTranslator from '../../shared/hooks/useTranslator' +import useIncidents from '../hooks/useIncidents' +import IncidentFilter from '../IncidentFilter' +import IncidentSearchRequest from '../model/IncidentSearchRequest' + +const VisualizeIncidents = () => { + const { t } = useTranslator() + const updateTitle = useUpdateTitle() + updateTitle(t('incidents.visualize.view')) + + const searchFilter = IncidentFilter.reported + const searchRequest: IncidentSearchRequest = { status: searchFilter } + const { data, isLoading } = useIncidents(searchRequest) + const [incident, setIncident] = useState(0) + const [showGraph, setShowGraph] = useState(false) + const [monthlyIncidents, setMonthlyIncidents] = useState(Array(12).fill(0)) + + const getIncidentMonth = (reportedOn: string) => + // reportedOn: "2020-08-12T19:53:30.153Z" + Number(reportedOn.slice(5, 7)) - 1 + + useEffect(() => { + if (data === undefined || isLoading) { + // incidents data not loaded yet, do nothing + } else { + const totalIncidents: number = data.length + if (totalIncidents > incident) { + const incidentMonth = getIncidentMonth(data[incident].reportedOn) + setMonthlyIncidents((prevIncidents) => + prevIncidents.map((value, index) => (index === incidentMonth ? value + 1 : value)), + ) + setIncident(incident + 1) + } else if (totalIncidents === incident) { + // incidents data finished processing + setShowGraph(true) + } + } + }, [data, monthlyIncidents, isLoading, incident]) + + return !showGraph ? ( + + ) : ( + <> + + + ) +} + +export default VisualizeIncidents diff --git a/src/index.css b/src/index.css index bd5338ff8f..f0874932d2 100644 --- a/src/index.css +++ b/src/index.css @@ -1,3 +1,7 @@ +html { + scroll-behavior: smooth; +} + body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', diff --git a/src/index.tsx b/src/index.tsx index a2bbfc7fb6..20a7a15cca 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,7 +1,6 @@ import React from 'react' import ReactDOM from 'react-dom' import { Provider } from 'react-redux' -import '@hospitalrun/components/scss/main.scss' import './index.css' import App from './App' diff --git a/src/labs/ViewLab.tsx b/src/labs/ViewLab.tsx index bec53768c6..d253c4cb21 100644 --- a/src/labs/ViewLab.tsx +++ b/src/labs/ViewLab.tsx @@ -1,17 +1,18 @@ -import { Row, Column, Badge, Button, Alert, Toast } from '@hospitalrun/components' +import { Row, Column, Badge, Button, Alert, Toast, Callout, Label } from '@hospitalrun/components' import format from 'date-fns/format' import React, { useEffect, useState } from 'react' import { useSelector, useDispatch } from 'react-redux' import { useParams, useHistory } from 'react-router-dom' import useAddBreadcrumbs from '../page-header/breadcrumbs/useAddBreadcrumbs' -import useTitle from '../page-header/title/useTitle' +import { useUpdateTitle } from '../page-header/title/TitleContext' import TextFieldWithLabelFormGroup from '../shared/components/input/TextFieldWithLabelFormGroup' import useTranslator from '../shared/hooks/useTranslator' import Lab from '../shared/model/Lab' import Patient from '../shared/model/Patient' import Permissions from '../shared/model/Permissions' import { RootState } from '../shared/store' +import { uuid } from '../shared/util/uuid' import { cancelLab, completeLab, updateLab, fetchLab } from './lab-slice' const getTitle = (patient: Patient | undefined, lab: Lab | undefined) => @@ -26,9 +27,11 @@ const ViewLab = () => { const { lab, patient, status, error } = useSelector((state: RootState) => state.lab) const [labToView, setLabToView] = useState() + const [newNotes, setNewNotes] = useState() const [isEditable, setIsEditable] = useState(true) - useTitle(getTitle(patient, labToView)) + const updateTitle = useUpdateTitle() + updateTitle(getTitle(patient, labToView)) const breadcrumbs = [ { @@ -59,8 +62,7 @@ const ViewLab = () => { const onNotesChange = (event: React.ChangeEvent) => { const notes = event.currentTarget.value - const newLab = labToView as Lab - setLabToView({ ...newLab, notes }) + setNewNotes(notes) } const onUpdate = async () => { @@ -73,7 +75,12 @@ const ViewLab = () => { ) } if (labToView) { - dispatch(updateLab(labToView, onSuccess)) + const newLab = labToView as Lab + if (newNotes) { + newLab.notes = newLab.notes ? [...newLab.notes, newNotes] : [newNotes] + setNewNotes('') + } + dispatch(updateLab(newLab, onSuccess)) } } const onComplete = async () => { @@ -82,7 +89,7 @@ const ViewLab = () => { Toast( 'success', t('states.success'), - `${t('labs.successfullyCompleted')} ${complete.type} ${patient?.fullName} `, + `${t('labs.successfullyCompleted')} ${complete.type} for ${patient?.fullName} `, ) } @@ -167,6 +174,18 @@ const ViewLab = () => { return <> } + const getPastNotes = () => { + if (labToView.notes && labToView.notes[0] !== '') { + return labToView.notes.map((note: string) => ( + +

{note}

+
+ )) + } + + return <> + } + return ( <> {status === 'error' && ( @@ -212,13 +231,16 @@ const ViewLab = () => { feedback={t(error.result as string)} onChange={onResultChange} /> - +
row.id} - columns={[ - { label: t('medications.medication.medication'), key: 'medication' }, - { label: t('medications.medication.priority'), key: 'priority' }, - { label: t('medications.medication.intent'), key: 'intent' }, - { - label: t('medications.medication.requestedOn'), - key: 'requestedOn', - formatter: (row) => - row.requestedOn ? format(new Date(row.requestedOn), 'yyyy-MM-dd hh:mm a') : '', - }, - { label: t('medications.medication.status'), key: 'status' }, - ]} - data={medications} - actionsHeaderText={t('actions.label')} - actions={[{ label: t('actions.view'), action: (row) => onViewClick(row as Medication) }]} - /> - - - ) -} - -export default ViewMedications diff --git a/src/medications/hooks/useMedicationSearch.tsx b/src/medications/hooks/useMedicationSearch.tsx new file mode 100644 index 0000000000..3c022590bb --- /dev/null +++ b/src/medications/hooks/useMedicationSearch.tsx @@ -0,0 +1,29 @@ +import { useQuery } from 'react-query' + +import MedicationRepository from '../../shared/db/MedicationRepository' +import SortRequest from '../../shared/db/SortRequest' +import Medication from '../../shared/model/Medication' +import MedicationSearchRequest from '../models/MedicationSearchRequest' + +const defaultSortRequest: SortRequest = { + sorts: [ + { + field: 'requestedOn', + direction: 'desc', + }, + ], +} + +function searchMedicationRequests( + _: string, + searchRequest: MedicationSearchRequest, +): Promise { + return MedicationRepository.search({ + ...searchRequest, + defaultSortRequest, + }) +} + +export default function useMedicationSearch(searchRequest: MedicationSearchRequest) { + return useQuery(['medication-requests', searchRequest], searchMedicationRequests) +} diff --git a/src/medications/medications-slice.ts b/src/medications/medications-slice.ts deleted file mode 100644 index 40a2fba226..0000000000 --- a/src/medications/medications-slice.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { createSlice, PayloadAction } from '@reduxjs/toolkit' - -import MedicationRepository from '../shared/db/MedicationRepository' -import SortRequest from '../shared/db/SortRequest' -import Medication from '../shared/model/Medication' -import { AppThunk } from '../shared/store' - -interface MedicationsState { - isLoading: boolean - medications: Medication[] - statusFilter: status -} - -type status = - | 'draft' - | 'active' - | 'on hold' - | 'canceled' - | 'completed' - | 'entered in error' - | 'stopped' - | 'unknown' - | 'all' - -const defaultSortRequest: SortRequest = { - sorts: [ - { - field: 'requestedOn', - direction: 'desc', - }, - ], -} - -const initialState: MedicationsState = { - isLoading: false, - medications: [], - statusFilter: 'all', -} - -const startLoading = (state: MedicationsState) => { - state.isLoading = true -} - -const medicationsSlice = createSlice({ - name: 'medications', - initialState, - reducers: { - fetchMedicationsStart: startLoading, - fetchMedicationsSuccess(state, { payload }: PayloadAction) { - state.isLoading = false - state.medications = payload - }, - }, -}) -export const { fetchMedicationsStart, fetchMedicationsSuccess } = medicationsSlice.actions - -export const searchMedications = (text: string, status: status): AppThunk => async (dispatch) => { - dispatch(fetchMedicationsStart()) - - let medications - - if (text.trim() === '' && status === initialState.statusFilter) { - medications = await MedicationRepository.findAll(defaultSortRequest) - } else { - medications = await MedicationRepository.search({ - text, - status, - defaultSortRequest, - }) - } - - dispatch(fetchMedicationsSuccess(medications)) -} - -export default medicationsSlice.reducer diff --git a/src/medications/models/MedicationSearchRequest.ts b/src/medications/models/MedicationSearchRequest.ts new file mode 100644 index 0000000000..9965736b7c --- /dev/null +++ b/src/medications/models/MedicationSearchRequest.ts @@ -0,0 +1,6 @@ +import { MedicationStatus } from './MedicationStatus' + +export default interface MedicationSearchRequest { + text: string + status: MedicationStatus +} diff --git a/src/medications/models/MedicationStatus.ts b/src/medications/models/MedicationStatus.ts new file mode 100644 index 0000000000..1c3afec3a0 --- /dev/null +++ b/src/medications/models/MedicationStatus.ts @@ -0,0 +1,10 @@ +export type MedicationStatus = + | 'draft' + | 'active' + | 'on hold' + | 'canceled' + | 'completed' + | 'entered in error' + | 'stopped' + | 'unknown' + | 'all' diff --git a/src/medications/requests/NewMedicationRequest.tsx b/src/medications/requests/NewMedicationRequest.tsx index e2726bb3f8..3a92887b27 100644 --- a/src/medications/requests/NewMedicationRequest.tsx +++ b/src/medications/requests/NewMedicationRequest.tsx @@ -4,10 +4,10 @@ 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 { useUpdateTitle } from '../../page-header/title/TitleContext' import SelectWithLabelFormGroup, { Option, -} from '../../shared/components/input/SelectWithLableFormGroup' +} from '../../shared/components/input/SelectWithLabelFormGroup' import TextFieldWithLabelFormGroup from '../../shared/components/input/TextFieldWithLabelFormGroup' import TextInputWithLabelFormGroup from '../../shared/components/input/TextInputWithLabelFormGroup' import PatientRepository from '../../shared/db/PatientRepository' @@ -21,7 +21,8 @@ const NewMedicationRequest = () => { const { t } = useTranslator() const dispatch = useDispatch() const history = useHistory() - useTitle(t('medications.requests.new')) + const updateTitle = useUpdateTitle() + updateTitle(t('medications.requests.new')) const { status, error } = useSelector((state: RootState) => state.medication) const [newMedicationRequest, setNewMedicationRequest] = useState(({ diff --git a/src/medications/search/MedicationRequestSearch.tsx b/src/medications/search/MedicationRequestSearch.tsx new file mode 100644 index 0000000000..21781a0ef8 --- /dev/null +++ b/src/medications/search/MedicationRequestSearch.tsx @@ -0,0 +1,71 @@ +import React, { ChangeEvent } from 'react' + +import SelectWithLabelFormGroup, { + Option, +} from '../../shared/components/input/SelectWithLabelFormGroup' +import TextInputWithLabelFormGroup from '../../shared/components/input/TextInputWithLabelFormGroup' +import useTranslator from '../../shared/hooks/useTranslator' +import MedicationSearchRequest from '../models/MedicationSearchRequest' +import { MedicationStatus } from '../models/MedicationStatus' + +interface Props { + searchRequest: MedicationSearchRequest + onChange: (newSearchRequest: MedicationSearchRequest) => void +} + +const MedicationRequestSearch = (props: Props) => { + const { searchRequest, onChange } = props + const { t } = useTranslator() + const filterOptions: Option[] = [ + { label: t('medications.filter.all'), value: 'all' }, + { label: t('medications.status.draft'), value: 'draft' }, + { label: t('medications.status.active'), value: 'active' }, + { label: t('medications.status.onHold'), value: 'on hold' }, + { label: t('medications.status.completed'), value: 'completed' }, + { label: t('medications.status.enteredInError'), value: 'entered in error' }, + { label: t('medications.status.canceled'), value: 'canceled' }, + { label: t('medications.status.unknown'), value: 'unknown' }, + ] + + const onSearchQueryChange = (event: ChangeEvent) => { + const query = event.target.value + onChange({ + ...searchRequest, + text: query, + }) + } + + const onFilterChange = (filter: MedicationStatus) => { + onChange({ + ...searchRequest, + status: filter, + }) + } + + return ( +
+
+ value === searchRequest.status)} + onChange={(values) => onFilterChange(values[0] as MedicationStatus)} + isEditable + /> +
+
+ +
+
+ ) +} + +export default MedicationRequestSearch diff --git a/src/medications/search/MedicationRequestTable.tsx b/src/medications/search/MedicationRequestTable.tsx new file mode 100644 index 0000000000..f684b78a61 --- /dev/null +++ b/src/medications/search/MedicationRequestTable.tsx @@ -0,0 +1,52 @@ +import { Table } from '@hospitalrun/components' +import format from 'date-fns/format' +import React from 'react' +import { useHistory } from 'react-router' + +import Loading from '../../shared/components/Loading' +import useTranslator from '../../shared/hooks/useTranslator' +import Medication from '../../shared/model/Medication' +import useMedicationRequestSearch from '../hooks/useMedicationSearch' +import MedicationSearchRequest from '../models/MedicationSearchRequest' + +interface Props { + searchRequest: MedicationSearchRequest +} + +const MedicationRequestTable = (props: Props) => { + const { searchRequest } = props + const { t } = useTranslator() + const history = useHistory() + const { data, status } = useMedicationRequestSearch(searchRequest) + + if (data === undefined || status === 'loading') { + return + } + + const onViewClick = (medication: Medication) => { + history.push(`/medications/${medication.id}`) + } + + return ( +
row.id} + columns={[ + { label: t('medications.medication.medication'), key: 'medication' }, + { label: t('medications.medication.priority'), key: 'priority' }, + { label: t('medications.medication.intent'), key: 'intent' }, + { + label: t('medications.medication.requestedOn'), + key: 'requestedOn', + formatter: (row) => + row.requestedOn ? format(new Date(row.requestedOn), 'yyyy-MM-dd hh:mm a') : '', + }, + { label: t('medications.medication.status'), key: 'status' }, + ]} + data={data} + actionsHeaderText={t('actions.label')} + actions={[{ label: t('actions.view'), action: (row) => onViewClick(row as Medication) }]} + /> + ) +} + +export default MedicationRequestTable diff --git a/src/medications/search/ViewMedications.tsx b/src/medications/search/ViewMedications.tsx new file mode 100644 index 0000000000..3865d20b67 --- /dev/null +++ b/src/medications/search/ViewMedications.tsx @@ -0,0 +1,72 @@ +import { Button, Column, Container, Row } from '@hospitalrun/components' +import React, { useEffect, useCallback, useState } from 'react' +import { useSelector } from 'react-redux' +import { useHistory } from 'react-router-dom' + +import { useButtonToolbarSetter } from '../../page-header/button-toolbar/ButtonBarProvider' +import { useUpdateTitle } from '../../page-header/title/TitleContext' +import useTranslator from '../../shared/hooks/useTranslator' +import Permissions from '../../shared/model/Permissions' +import { RootState } from '../../shared/store' +import MedicationSearchRequest from '../models/MedicationSearchRequest' +import MedicationRequestSearch from './MedicationRequestSearch' +import MedicationRequestTable from './MedicationRequestTable' + +const ViewMedications = () => { + const { t } = useTranslator() + const history = useHistory() + const setButtons = useButtonToolbarSetter() + const updateTitle = useUpdateTitle() + updateTitle(t('medications.label')) + + const { permissions } = useSelector((state: RootState) => state.user) + + const getButtons = useCallback(() => { + const buttons: React.ReactNode[] = [] + + if (permissions.includes(Permissions.RequestMedication)) { + buttons.push( + , + ) + } + + return buttons + }, [permissions, history, t]) + + useEffect(() => { + setButtons(getButtons()) + return () => { + setButtons([]) + } + }, [getButtons, setButtons]) + + const [searchRequest, setSearchRequest] = useState({ + text: '', + status: 'all', + }) + + const onSearchRequestChange = (newSearchRequest: MedicationSearchRequest) => { + setSearchRequest(newSearchRequest) + } + + return ( + + + + + + + + + ) +} + +export default ViewMedications diff --git a/src/page-header/title/TitleContext.tsx b/src/page-header/title/TitleContext.tsx new file mode 100644 index 0000000000..9ff6b3f1e9 --- /dev/null +++ b/src/page-header/title/TitleContext.tsx @@ -0,0 +1,31 @@ +import * as React from 'react' + +type SetTitle = (title: string) => void +type State = { title: string } +type TitleProviderProps = { children: React.ReactNode } +const TitleStateContext = React.createContext(undefined) +const TitleDispatchContext = React.createContext(undefined) + +function TitleProvider({ children }: TitleProviderProps) { + const [title, setTitle] = React.useState('') + return ( + + {children} + + ) +} +function useTitle() { + const context = React.useContext(TitleStateContext) + if (context === undefined) { + throw new Error('useTitle must be used within a TitleProvider') + } + return context +} +function useUpdateTitle() { + const context = React.useContext(TitleDispatchContext) + if (context === undefined) { + throw new Error('useUpdateTitle must be used within a TitleProvider') + } + return context +} +export { TitleProvider, useTitle, useUpdateTitle } diff --git a/src/page-header/title/title-slice.ts b/src/page-header/title/title-slice.ts deleted file mode 100644 index 3b3bc3339b..0000000000 --- a/src/page-header/title/title-slice.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { createSlice, PayloadAction } from '@reduxjs/toolkit' - -import { AppThunk } from '../../shared/store' - -interface TitleState { - title: string -} - -const initialState: TitleState = { - title: '', -} - -const titleSlice = createSlice({ - name: 'title', - initialState, - reducers: { - changeTitle(state, { payload }: PayloadAction) { - state.title = payload - }, - }, -}) - -export const { changeTitle } = titleSlice.actions - -export const updateTitle = (title: string): AppThunk => async (dispatch) => { - dispatch(changeTitle(title)) -} - -export default titleSlice.reducer diff --git a/src/page-header/title/useTitle.tsx b/src/page-header/title/useTitle.tsx deleted file mode 100644 index ff2382457f..0000000000 --- a/src/page-header/title/useTitle.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { useEffect } from 'react' -import { useDispatch } from 'react-redux' - -import { updateTitle } from './title-slice' - -export default function useTitle(title: string): void { - const dispatch = useDispatch() - - useEffect(() => { - dispatch(updateTitle(title)) - }) -} diff --git a/src/patients/ContactInfo.tsx b/src/patients/ContactInfo.tsx index 70ff3eb473..229b5f685b 100644 --- a/src/patients/ContactInfo.tsx +++ b/src/patients/ContactInfo.tsx @@ -3,7 +3,7 @@ import React, { useEffect, ReactElement } from 'react' import SelectWithLabelFormGroup, { Option, -} from '../shared/components/input/SelectWithLableFormGroup' +} from '../shared/components/input/SelectWithLabelFormGroup' import TextFieldWithLabelFormGroup from '../shared/components/input/TextFieldWithLabelFormGroup' import TextInputWithLabelFormGroup from '../shared/components/input/TextInputWithLabelFormGroup' import useTranslator from '../shared/hooks/useTranslator' diff --git a/src/patients/GeneralInformation.tsx b/src/patients/GeneralInformation.tsx index ce3a63a680..1171bfae6f 100644 --- a/src/patients/GeneralInformation.tsx +++ b/src/patients/GeneralInformation.tsx @@ -5,7 +5,7 @@ import React, { ReactElement } from 'react' import DatePickerWithLabelFormGroup from '../shared/components/input/DatePickerWithLabelFormGroup' import SelectWithLabelFormGroup, { Option, -} from '../shared/components/input/SelectWithLableFormGroup' +} from '../shared/components/input/SelectWithLabelFormGroup' import TextInputWithLabelFormGroup from '../shared/components/input/TextInputWithLabelFormGroup' import useTranslator from '../shared/hooks/useTranslator' import { ContactInfoPiece } from '../shared/model/ContactInformation' diff --git a/src/patients/appointments/AppointmentsList.tsx b/src/patients/appointments/AppointmentsList.tsx index 647925901e..5536c3449c 100644 --- a/src/patients/appointments/AppointmentsList.tsx +++ b/src/patients/appointments/AppointmentsList.tsx @@ -1,25 +1,24 @@ -import { Button, Table, Spinner, Alert } from '@hospitalrun/components' +import { Button, Table, Alert } from '@hospitalrun/components' import format from 'date-fns/format' -import React, { useEffect } from 'react' -import { useSelector, useDispatch } from 'react-redux' +import React from 'react' import { useHistory } from 'react-router-dom' import useAddBreadcrumbs from '../../page-header/breadcrumbs/useAddBreadcrumbs' -import { fetchPatientAppointments } from '../../scheduling/appointments/appointments-slice' +import Loading from '../../shared/components/Loading' import useTranslator from '../../shared/hooks/useTranslator' -import { RootState } from '../../shared/store' +import usePatientsAppointments from '../hooks/usePatientAppointments' interface Props { patientId: string } const AppointmentsList = (props: Props) => { - const dispatch = useDispatch() const history = useHistory() const { t } = useTranslator() const { patientId } = props - const { appointments } = useSelector((state: RootState) => state.appointments) + + const { data, status } = usePatientsAppointments(patientId) const breadcrumbs = [ { @@ -27,11 +26,12 @@ const AppointmentsList = (props: Props) => { location: `/patients/${patientId}/appointments`, }, ] + useAddBreadcrumbs(breadcrumbs) - useEffect(() => { - dispatch(fetchPatientAppointments(patientId)) - }, [dispatch, patientId]) + if (data === undefined || status === 'loading') { + return + } return ( <> @@ -51,49 +51,43 @@ const AppointmentsList = (props: Props) => {
- {appointments ? ( - appointments.length > 0 ? ( -
row.id} - onRowClick={(row) => history.push(`/appointments/${row.id}`)} - columns={[ - { - label: t('scheduling.appointment.startDate'), - key: 'startDateTime', - formatter: (row) => - row.startDateTime - ? format(new Date(row.startDateTime), 'yyyy-MM-dd, hh:mm a') - : '', - }, - { - label: t('scheduling.appointment.endDate'), - key: 'endDateTime', - formatter: (row) => - row.endDateTime - ? format(new Date(row.endDateTime), 'yyyy-MM-dd, hh:mm a') - : '', - }, - { label: t('scheduling.appointment.location'), key: 'location' }, - { label: t('scheduling.appointment.type'), key: 'type' }, - ]} - actionsHeaderText={t('actions.label')} - actions={[ - { - label: t('actions.view'), - action: (row) => history.push(`/appointments/${row.id}`), - }, - ]} - /> - ) : ( - - ) + {data.length > 0 ? ( +
row.id} + onRowClick={(row) => history.push(`/appointments/${row.id}`)} + columns={[ + { + label: t('scheduling.appointment.startDate'), + key: 'startDateTime', + formatter: (row) => + row.startDateTime + ? format(new Date(row.startDateTime), 'yyyy-MM-dd, hh:mm a') + : '', + }, + { + label: t('scheduling.appointment.endDate'), + key: 'endDateTime', + formatter: (row) => + row.endDateTime ? format(new Date(row.endDateTime), 'yyyy-MM-dd, hh:mm a') : '', + }, + { label: t('scheduling.appointment.location'), key: 'location' }, + { label: t('scheduling.appointment.type'), key: 'type' }, + ]} + actionsHeaderText={t('actions.label')} + actions={[ + { + label: t('actions.view'), + action: (row) => history.push(`/appointments/${row.id}`), + }, + ]} + /> ) : ( - + )} diff --git a/src/patients/care-goals/AddCareGoalModal.tsx b/src/patients/care-goals/AddCareGoalModal.tsx new file mode 100644 index 0000000000..ce47b8b767 --- /dev/null +++ b/src/patients/care-goals/AddCareGoalModal.tsx @@ -0,0 +1,82 @@ +import { Modal } from '@hospitalrun/components' +import { addMonths } from 'date-fns' +import React, { useState, useEffect } from 'react' + +import useTranslator from '../../shared/hooks/useTranslator' +import CareGoal, { CareGoalStatus, CareGoalAchievementStatus } from '../../shared/model/CareGoal' +import Patient from '../../shared/model/Patient' +import useAddCareGoal from '../hooks/useAddCareGoal' +import { CareGoalError } from '../util/validate-caregoal' +import CareGoalForm from './CareGoalForm' + +interface Props { + patient: Patient + show: boolean + onCloseButtonClick: () => void +} + +const initialCareGoalState = { + description: '', + startDate: new Date().toISOString(), + dueDate: addMonths(new Date(), 1).toISOString(), + note: '', + priority: 'medium', + status: CareGoalStatus.Accepted, + achievementStatus: CareGoalAchievementStatus.InProgress, +} as CareGoal + +const AddCareGoalModal = (props: Props) => { + const { t } = useTranslator() + const { patient, show, onCloseButtonClick } = props + const [mutate] = useAddCareGoal() + const [careGoal, setCareGoal] = useState(initialCareGoalState) + const [careGoalError, setCareGoalError] = useState(undefined) + + useEffect(() => { + setCareGoal(initialCareGoalState) + }, [show]) + + const onClose = () => { + onCloseButtonClick() + } + + const onCareGoalChange = (newCareGoal: Partial) => { + setCareGoal(newCareGoal as CareGoal) + } + + const onSaveButtonClick = async () => { + try { + await mutate({ patientId: patient.id, careGoal }) + onClose() + } catch (e) { + setCareGoalError(e) + } + } + + const body = ( + + ) + + return ( + + ) +} + +export default AddCareGoalModal diff --git a/src/patients/care-goals/CareGoalForm.tsx b/src/patients/care-goals/CareGoalForm.tsx new file mode 100644 index 0000000000..972c99d2ef --- /dev/null +++ b/src/patients/care-goals/CareGoalForm.tsx @@ -0,0 +1,183 @@ +import { Alert, Row, Column } from '@hospitalrun/components' +import React, { useState } from 'react' + +import DatePickerWithLabelFormGroup from '../../shared/components/input/DatePickerWithLabelFormGroup' +import SelectWithLabelFormGroup, { + Option, +} from '../../shared/components/input/SelectWithLabelFormGroup' +import TextFieldWithLabelFormGroup from '../../shared/components/input/TextFieldWithLabelFormGroup' +import useTranslator from '../../shared/hooks/useTranslator' +import CareGoal, { CareGoalStatus, CareGoalAchievementStatus } from '../../shared/model/CareGoal' + +interface Error { + message?: string + description?: string + status?: string + achievementStatus?: string + priority?: string + startDate?: string + dueDate?: string + note?: string +} + +interface Props { + careGoal: Partial + careGoalError?: Error + onChange?: (newCareGoal: Partial) => void + disabled: boolean +} + +const CareGoalForm = (props: Props) => { + const { careGoal, careGoalError, disabled, onChange } = props + const { t } = useTranslator() + + const [priority, setPriority] = useState(careGoal.priority) + const [status, setStatus] = useState(careGoal.status) + const [achievementStatus, setAchievementStatus] = useState(careGoal.achievementStatus) + + const priorityOptions: Option[] = [ + { label: t('patient.careGoal.priority.low'), value: 'low' }, + { label: t('patient.careGoal.priority.medium'), value: 'medium' }, + { label: t('patient.careGoal.priority.high'), value: 'high' }, + ] + + const statusOptions: Option[] = Object.values(CareGoalStatus).map((v) => ({ label: v, value: v })) + + const achievementsStatusOptions: Option[] = Object.values(CareGoalAchievementStatus).map((v) => ({ + label: v, + value: v, + })) + + const onFieldChange = ( + name: string, + value: string | CareGoalStatus | CareGoalAchievementStatus, + ) => { + if (onChange) { + const newCareGoal = { + ...careGoal, + [name]: value, + } + onChange(newCareGoal) + } + } + + const onPriorityChange = (values: string[]) => { + const value = values[0] as 'low' | 'medium' | 'high' + + onFieldChange('priority', value) + setPriority(value) + } + + return ( + + {careGoalError?.message && } + + + onFieldChange('description', event.currentTarget.value)} + /> + + + + + value === priority)} + isEditable={!disabled} + isInvalid={!!careGoalError?.priority} + onChange={onPriorityChange} + /> + + + + + value === status)} + isEditable={!disabled} + isInvalid={!!careGoalError?.status} + onChange={(values) => { + onFieldChange('status', values[0]) + setStatus(values[0] as CareGoalStatus) + }} + /> + + + value === achievementStatus, + )} + isEditable={!disabled} + isInvalid={!!careGoalError?.achievementStatus} + onChange={(values) => { + onFieldChange('achievementStatus', values[0]) + setAchievementStatus(values[0] as CareGoalAchievementStatus) + }} + /> + + + + + (date ? onFieldChange('startDate', date.toISOString()) : null)} + /> + + + (date ? onFieldChange('dueDate', date.toISOString()) : null)} + /> + + + + + onFieldChange('note', event.currentTarget.value)} + /> + + + + ) +} + +CareGoalForm.defaultProps = { + disabled: false, +} + +export default CareGoalForm diff --git a/src/patients/care-goals/CareGoalTab.tsx b/src/patients/care-goals/CareGoalTab.tsx new file mode 100644 index 0000000000..7d6816c1d2 --- /dev/null +++ b/src/patients/care-goals/CareGoalTab.tsx @@ -0,0 +1,61 @@ +import { Button } from '@hospitalrun/components' +import React, { useState } from 'react' +import { useSelector } from 'react-redux' +import { useParams, Route, Switch } from 'react-router-dom' + +import Loading from '../../shared/components/Loading' +import useTranslator from '../../shared/hooks/useTranslator' +import Permissions from '../../shared/model/Permissions' +import { RootState } from '../../shared/store' +import usePatient from '../hooks/usePatient' +import AddCareGoalModal from './AddCareGoalModal' +import ViewCareGoal from './ViewCareGoal' +import ViewCareGoals from './ViewCareGoals' + +const CareGoalTab = () => { + const { id: patientId } = useParams() + const { t } = useTranslator() + const { permissions } = useSelector((state: RootState) => state.user) + const { data, status } = usePatient(patientId) + const [showAddCareGoalModal, setShowAddCareGoalModal] = useState(false) + + if (data === undefined || status === 'loading') { + return + } + + return ( + <> +
+
+ {permissions.includes(Permissions.AddCareGoal) && ( + + )} +
+
+
+ + + + + + + + + setShowAddCareGoalModal(false)} + /> + + ) +} + +export default CareGoalTab diff --git a/src/patients/care-goals/CareGoalTable.tsx b/src/patients/care-goals/CareGoalTable.tsx new file mode 100644 index 0000000000..6cf95cf39a --- /dev/null +++ b/src/patients/care-goals/CareGoalTable.tsx @@ -0,0 +1,66 @@ +import { Alert, Table } from '@hospitalrun/components' +import { format } from 'date-fns' +import React from 'react' +import { useHistory } from 'react-router' + +import Loading from '../../shared/components/Loading' +import useTranslator from '../../shared/hooks/useTranslator' +import usePatientCareGoals from '../hooks/usePatientCareGoals' + +interface Props { + patientId: string +} + +const CareGoalTable = (props: Props) => { + const { patientId } = props + const history = useHistory() + const { t } = useTranslator() + const { data, status } = usePatientCareGoals(patientId) + + if (data === undefined || status === 'loading') { + return + } + + if (data.length === 0) { + return ( + + ) + } + + return ( +
row.id} + data={data} + columns={[ + { label: t('patient.careGoal.description'), key: 'description' }, + { + label: t('patient.careGoal.startDate'), + key: 'startDate', + formatter: (row) => format(new Date(row.startDate), 'yyyy-MM-dd'), + }, + { + label: t('patient.careGoal.dueDate'), + key: 'dueDate', + formatter: (row) => format(new Date(row.dueDate), 'yyyy-MM-dd'), + }, + { + label: t('patient.careGoal.status'), + key: 'status', + }, + ]} + actionsHeaderText={t('actions.label')} + actions={[ + { + label: 'actions.view', + action: (row) => history.push(`/patients/${patientId}/care-goals/${row.id}`), + }, + ]} + /> + ) +} + +export default CareGoalTable diff --git a/src/patients/care-goals/ViewCareGoal.tsx b/src/patients/care-goals/ViewCareGoal.tsx new file mode 100644 index 0000000000..83573322b8 --- /dev/null +++ b/src/patients/care-goals/ViewCareGoal.tsx @@ -0,0 +1,19 @@ +import React from 'react' +import { useParams } from 'react-router' + +import Loading from '../../shared/components/Loading' +import useCareGoal from '../hooks/useCareGoal' +import CareGoalForm from './CareGoalForm' + +const ViewCareGoal = () => { + const { careGoalId, id: patientId } = useParams() + const { data: careGoal, status } = useCareGoal(patientId, careGoalId) + + if (careGoal === undefined || status === 'loading') { + return + } + + return +} + +export default ViewCareGoal diff --git a/src/patients/care-goals/ViewCareGoals.tsx b/src/patients/care-goals/ViewCareGoals.tsx new file mode 100644 index 0000000000..d0b41ae25c --- /dev/null +++ b/src/patients/care-goals/ViewCareGoals.tsx @@ -0,0 +1,12 @@ +import React from 'react' +import { useParams } from 'react-router-dom' + +import CareGoalTable from './CareGoalTable' + +const ViewCareGoals = () => { + const { id } = useParams() + + return +} + +export default ViewCareGoals diff --git a/src/patients/care-plans/CarePlanForm.tsx b/src/patients/care-plans/CarePlanForm.tsx index 9ff64150c0..1e27aad0f8 100644 --- a/src/patients/care-plans/CarePlanForm.tsx +++ b/src/patients/care-plans/CarePlanForm.tsx @@ -4,7 +4,7 @@ import React, { useState } from 'react' import DatePickerWithLabelFormGroup from '../../shared/components/input/DatePickerWithLabelFormGroup' import SelectWithLabelFormGroup, { Option, -} from '../../shared/components/input/SelectWithLableFormGroup' +} from '../../shared/components/input/SelectWithLabelFormGroup' import TextFieldWithLabelFormGroup from '../../shared/components/input/TextFieldWithLabelFormGroup' import TextInputWithLabelFormGroup from '../../shared/components/input/TextInputWithLabelFormGroup' import useTranslator from '../../shared/hooks/useTranslator' diff --git a/src/patients/diagnoses/AddDiagnosisModal.tsx b/src/patients/diagnoses/AddDiagnosisModal.tsx index f96c1eb66f..3d99e9246a 100644 --- a/src/patients/diagnoses/AddDiagnosisModal.tsx +++ b/src/patients/diagnoses/AddDiagnosisModal.tsx @@ -1,14 +1,15 @@ import { Modal } from '@hospitalrun/components' import React, { useState, useEffect } from 'react' -import { useDispatch, useSelector } from 'react-redux' import useTranslator from '../../shared/hooks/useTranslator' -import Diagnosis from '../../shared/model/Diagnosis' -import { RootState } from '../../shared/store' -import { addDiagnosis } from '../patient-slice' +import Diagnosis, { DiagnosisStatus } from '../../shared/model/Diagnosis' +import Patient from '../../shared/model/Patient' +import useAddPatientDiagnosis from '../hooks/useAddPatientDiagnosis' +import { DiagnosisError } from '../util/validate-diagnosis' import DiagnosisForm from './DiagnosisForm' -interface Props { +interface NewDiagnosisModalProps { + patient: Patient show: boolean onCloseButtonClick: () => void } @@ -20,16 +21,16 @@ const initialDiagnosisState = { abatementDate: new Date().toISOString(), note: '', visit: '', + status: DiagnosisStatus.Active, } -const AddDiagnosisModal = (props: Props) => { - const { show, onCloseButtonClick } = props - const dispatch = useDispatch() - const { diagnosisError, patient } = useSelector((state: RootState) => state.patient) +const AddDiagnosisModal = (props: NewDiagnosisModalProps) => { + const { show, onCloseButtonClick, patient } = props const { t } = useTranslator() + const [mutate] = useAddPatientDiagnosis() const [diagnosis, setDiagnosis] = useState(initialDiagnosisState) - + const [diagnosisError, setDiagnosisError] = useState(undefined) useEffect(() => { setDiagnosis(initialDiagnosisState) }, [show]) @@ -37,8 +38,13 @@ const AddDiagnosisModal = (props: Props) => { const onDiagnosisChange = (newDiagnosis: Partial) => { setDiagnosis(newDiagnosis as Diagnosis) } - const onSaveButtonClick = () => { - dispatch(addDiagnosis(patient.id, diagnosis as Diagnosis)) + const onSaveButtonClick = async () => { + try { + await mutate({ diagnosis, patientId: patient.id }) + onCloseButtonClick() + } catch (e) { + setDiagnosisError(e) + } } const body = ( diff --git a/src/patients/diagnoses/Diagnoses.tsx b/src/patients/diagnoses/Diagnoses.tsx index d529fb306c..12ee041896 100644 --- a/src/patients/diagnoses/Diagnoses.tsx +++ b/src/patients/diagnoses/Diagnoses.tsx @@ -1,14 +1,14 @@ -import { Button, List, ListItem, Alert } from '@hospitalrun/components' +import { Button } from '@hospitalrun/components' import React, { useState } from 'react' import { useSelector } from 'react-redux' import useAddBreadcrumbs from '../../page-header/breadcrumbs/useAddBreadcrumbs' import useTranslator from '../../shared/hooks/useTranslator' -import Diagnosis from '../../shared/model/Diagnosis' import Patient from '../../shared/model/Patient' import Permissions from '../../shared/model/Permissions' import { RootState } from '../../shared/store' import AddDiagnosisModal from './AddDiagnosisModal' +import DiagnosesList from './DiagnosesList' interface Props { patient: Patient @@ -50,19 +50,12 @@ const Diagnoses = (props: Props) => {
- {(!patient.diagnoses || patient.diagnoses.length === 0) && ( - - )} - - {patient.diagnoses?.map((a: Diagnosis) => ( - {a.name} - ))} - - + + ) } diff --git a/src/patients/diagnoses/DiagnosesList.tsx b/src/patients/diagnoses/DiagnosesList.tsx new file mode 100644 index 0000000000..42736e669e --- /dev/null +++ b/src/patients/diagnoses/DiagnosesList.tsx @@ -0,0 +1,41 @@ +import { Alert, List, ListItem } from '@hospitalrun/components' +import React from 'react' + +import Loading from '../../shared/components/Loading' +import useTranslator from '../../shared/hooks/useTranslator' +import Diagnosis from '../../shared/model/Diagnosis' +import usePatientDiagnoses from '../hooks/usePatientDiagnoses' + +interface Props { + patientId: string +} + +const DiagnosesList = (props: Props) => { + const { patientId } = props + const { t } = useTranslator() + const { data, status } = usePatientDiagnoses(patientId) + + if (data === undefined || status === 'loading') { + return + } + + if (data.length === 0) { + return ( + + ) + } + + return ( + + {data.map((diagnosis: Diagnosis) => ( + {diagnosis.name} + ))} + + ) +} + +export default DiagnosesList diff --git a/src/patients/diagnoses/DiagnosisForm.tsx b/src/patients/diagnoses/DiagnosisForm.tsx index e79178806c..7d1a7ebcfc 100644 --- a/src/patients/diagnoses/DiagnosisForm.tsx +++ b/src/patients/diagnoses/DiagnosisForm.tsx @@ -3,12 +3,7 @@ import format from 'date-fns/format' import React, { useState } from 'react' import { useTranslation } from 'react-i18next' -import DatePickerWithLabelFormGroup from '../../shared/components/input/DatePickerWithLabelFormGroup' -import SelectWithLabelFormGroup, { - Option, -} from '../../shared/components/input/SelectWithLableFormGroup' -import TextFieldWithLabelFormGroup from '../../shared/components/input/TextFieldWithLabelFormGroup' -import TextInputWithLabelFormGroup from '../../shared/components/input/TextInputWithLabelFormGroup' +import Input, { Option } from '../../shared/components/input' import Diagnosis, { DiagnosisStatus } from '../../shared/model/Diagnosis' import Patient from '../../shared/model/Patient' @@ -74,7 +69,7 @@ const DiagnosisForm = (props: Props) => { - { - { - { - { - { - { - { (state: RootState) => state.patient, ) - useTitle( + const updateTitle = useUpdateTitle() + updateTitle( `${t('patients.editPatient')}: ${getPatientFullName(reduxPatient)} (${getPatientCode( reduxPatient, )})`, diff --git a/src/patients/hooks/useAddCareGoal.tsx b/src/patients/hooks/useAddCareGoal.tsx new file mode 100644 index 0000000000..f18bd2a7e9 --- /dev/null +++ b/src/patients/hooks/useAddCareGoal.tsx @@ -0,0 +1,47 @@ +import { isEmpty } from 'lodash' +import { queryCache, useMutation } from 'react-query' + +import PatientRepository from '../../shared/db/PatientRepository' +import CareGoal from '../../shared/model/CareGoal' +import { uuid } from '../../shared/util/uuid' +import validateCareGoal from '../util/validate-caregoal' + +interface AddCareGoalRequest { + patientId: string + careGoal: Omit +} + +async function addCareGoal(request: AddCareGoalRequest): Promise { + const error = validateCareGoal(request.careGoal) + + if (isEmpty(error)) { + const patient = await PatientRepository.find(request.patientId) + const careGoals = patient.careGoals ? [...patient.careGoals] : [] + + const newCareGoal: CareGoal = { + id: uuid(), + createdOn: new Date(Date.now()).toISOString(), + ...request.careGoal, + } + careGoals.push(newCareGoal) + + await PatientRepository.saveOrUpdate({ + ...patient, + careGoals, + }) + + return careGoals + } + + error.message = 'patient.careGoal.error.unableToAdd' + throw error +} + +export default function useAddCareGoal() { + return useMutation(addCareGoal, { + onSuccess: async (data, variables) => { + await queryCache.setQueryData(['care-goals', variables.patientId], data) + }, + throwOnError: true, + }) +} diff --git a/src/patients/hooks/useAddPatientDiagnosis.tsx b/src/patients/hooks/useAddPatientDiagnosis.tsx new file mode 100644 index 0000000000..231b05cc96 --- /dev/null +++ b/src/patients/hooks/useAddPatientDiagnosis.tsx @@ -0,0 +1,37 @@ +import { isEmpty } from 'lodash' +import { queryCache, useMutation } from 'react-query' + +import PatientRepository from '../../shared/db/PatientRepository' +import Diagnosis from '../../shared/model/Diagnosis' +import { uuid } from '../../shared/util/uuid' +import validateDiagnosis from '../util/validate-diagnosis' + +interface AddDiagnosisRequest { + patientId: string + diagnosis: Omit +} + +async function addDiagnosis(request: AddDiagnosisRequest): Promise { + const error = validateDiagnosis(request.diagnosis) + if (isEmpty(error)) { + const patient = await PatientRepository.find(request.patientId) + const diagnoses = patient.diagnoses ? [...patient.diagnoses] : [] + const newDiagnosis: Diagnosis = { + id: uuid(), + ...request.diagnosis, + } + diagnoses.push(newDiagnosis) + await PatientRepository.saveOrUpdate({ ...patient, diagnoses }) + return diagnoses + } + throw error +} + +export default function useAddPatientDiagnosis() { + return useMutation(addDiagnosis, { + onSuccess: async (data, variables) => { + await queryCache.setQueryData(['diagnoses', variables.patientId], data) + }, + throwOnError: true, + }) +} diff --git a/src/patients/hooks/useAddPatientNote.tsx b/src/patients/hooks/useAddPatientNote.tsx new file mode 100644 index 0000000000..01f301cc8b --- /dev/null +++ b/src/patients/hooks/useAddPatientNote.tsx @@ -0,0 +1,44 @@ +import { isEmpty } from 'lodash' +import { queryCache, useMutation } from 'react-query' + +import PatientRepository from '../../shared/db/PatientRepository' +import Note from '../../shared/model/Note' +import { uuid } from '../../shared/util/uuid' +import validateNote from '../util/validate-note' + +interface AddNoteRequest { + patientId: string + note: Omit +} + +async function addNote(request: AddNoteRequest): Promise { + const error = validateNote(request.note) + + if (isEmpty(error)) { + const patient = await PatientRepository.find(request.patientId) + const notes = patient.notes ? [...patient.notes] : [] + const newNote: Note = { + id: uuid(), + ...request.note, + } + notes.push(newNote) + + await PatientRepository.saveOrUpdate({ + ...patient, + notes, + }) + + return notes + } + + throw error +} + +export default function useAddPatientNote() { + return useMutation(addNote, { + onSuccess: async (data, variables) => { + await queryCache.setQueryData(['notes', variables.patientId], data) + }, + throwOnError: true, + }) +} diff --git a/src/patients/hooks/useAddPatientRelatedPerson.tsx b/src/patients/hooks/useAddPatientRelatedPerson.tsx new file mode 100644 index 0000000000..577e0a196c --- /dev/null +++ b/src/patients/hooks/useAddPatientRelatedPerson.tsx @@ -0,0 +1,50 @@ +import { isEmpty } from 'lodash' +import { useMutation, queryCache } from 'react-query' + +import PatientRepository from '../../shared/db/PatientRepository' +import RelatedPerson from '../../shared/model/RelatedPerson' +import { uuid } from '../../shared/util/uuid' +import validateRelatedPerson from '../util/validate-related-person' + +interface AddRelatedPersonRequest { + patientId: string + relatedPerson: Omit +} + +async function addRelatedPerson(request: AddRelatedPersonRequest): Promise { + const error = validateRelatedPerson(request.relatedPerson) + + if (isEmpty(error)) { + const patient = await PatientRepository.find(request.patientId) + const relatedPersons = patient.relatedPersons ? [...patient.relatedPersons] : [] + const newRelated: RelatedPerson = { + id: uuid(), + ...request.relatedPerson, + } + relatedPersons.push(newRelated) + + await PatientRepository.saveOrUpdate({ + ...patient, + relatedPersons, + }) + + return relatedPersons + } + + throw error +} + +export default function useAddPatientRelatedPerson() { + return useMutation(addRelatedPerson, { + onSuccess: async (data, variables) => { + const relatedPersons = await Promise.all( + data.map(async (rp) => { + const patient = await PatientRepository.find(rp.patientId) + return { ...patient, type: rp.type } + }), + ) + await queryCache.setQueryData(['related-persons', variables.patientId], relatedPersons) + }, + throwOnError: true, + }) +} diff --git a/src/patients/hooks/useAddVisit.tsx b/src/patients/hooks/useAddVisit.tsx new file mode 100644 index 0000000000..4fef90e21f --- /dev/null +++ b/src/patients/hooks/useAddVisit.tsx @@ -0,0 +1,42 @@ +import { isEmpty } from 'lodash' +import { queryCache, useMutation } from 'react-query' + +import PatientRepository from '../../shared/db/PatientRepository' +import Visit from '../../shared/model/Visit' +import { uuid } from '../../shared/util/uuid' +import validateVisit from '../util/validate-visit' + +export type RequestVisit = Omit +interface AddVisitRequest { + patientId: string + visit: RequestVisit +} + +async function addVisit(request: AddVisitRequest): Promise { + const error = validateVisit(request.visit) + if (isEmpty(error)) { + const patient = await PatientRepository.find(request.patientId) + const visits = patient.visits || ([] as Visit[]) + visits.push({ + id: uuid(), + createdAt: new Date(Date.now().valueOf()).toISOString(), + ...request.visit, + }) + await PatientRepository.saveOrUpdate({ + ...patient, + visits, + }) + return visits + } + error.message = 'patient.visits.error.unableToAdd' + throw error +} + +export default function useAddVisit() { + return useMutation(addVisit, { + onSuccess: async (data, variables) => { + await queryCache.setQueryData(['visits', variables.patientId], data) + }, + throwOnError: true, + }) +} diff --git a/src/patients/hooks/useAllergy.tsx b/src/patients/hooks/useAllergy.tsx index e1a19a044b..3615cf6ef7 100644 --- a/src/patients/hooks/useAllergy.tsx +++ b/src/patients/hooks/useAllergy.tsx @@ -1,13 +1,9 @@ -import { QueryKey, useQuery } from 'react-query' +import { useQuery } from 'react-query' import PatientRepository from '../../shared/db/PatientRepository' import Allergy from '../../shared/model/Allergy' -async function getAllergy( - _: QueryKey, - patientId: string, - allergyId: string, -): Promise { +async function getAllergy(_: string, patientId: string, allergyId: string): Promise { const patient = await PatientRepository.find(patientId) const maybeAllergy = patient.allergies?.find((a) => a.id === allergyId) if (!maybeAllergy) { diff --git a/src/patients/hooks/useCareGoal.tsx b/src/patients/hooks/useCareGoal.tsx new file mode 100644 index 0000000000..4166206bc7 --- /dev/null +++ b/src/patients/hooks/useCareGoal.tsx @@ -0,0 +1,19 @@ +import { useQuery } from 'react-query' + +import PatientRepository from '../../shared/db/PatientRepository' +import CareGoal from '../../shared/model/CareGoal' + +async function getCareGoal(_: string, patientId: string, careGoalId: string): Promise { + const patient = await PatientRepository.find(patientId) + const maybeCareGoal = patient.careGoals?.find((c) => c.id === careGoalId) + + if (!maybeCareGoal) { + throw new Error('Care Goal not found') + } + + return maybeCareGoal +} + +export default function useCareGoal(patientId: string, careGoalId: string) { + return useQuery(['care-goals', patientId, careGoalId], getCareGoal) +} diff --git a/src/patients/hooks/useCarePlan.tsx b/src/patients/hooks/useCarePlan.tsx index 7f3fe9477f..f7f97a21b3 100644 --- a/src/patients/hooks/useCarePlan.tsx +++ b/src/patients/hooks/useCarePlan.tsx @@ -1,13 +1,9 @@ -import { QueryKey, useQuery } from 'react-query' +import { useQuery } from 'react-query' import PatientRepository from '../../shared/db/PatientRepository' import CarePlan from '../../shared/model/CarePlan' -async function getCarePlan( - _: QueryKey, - patientId: string, - allergyId: string, -): Promise { +async function getCarePlan(_: string, patientId: string, allergyId: string): Promise { const patient = await PatientRepository.find(patientId) const maybeCarePlan = patient.carePlans?.find((a) => a.id === allergyId) if (!maybeCarePlan) { diff --git a/src/patients/hooks/usePatientAllergies.tsx b/src/patients/hooks/usePatientAllergies.tsx index af2c0d428b..66d3b0cf4c 100644 --- a/src/patients/hooks/usePatientAllergies.tsx +++ b/src/patients/hooks/usePatientAllergies.tsx @@ -1,9 +1,9 @@ -import { QueryKey, useQuery } from 'react-query' +import { useQuery } from 'react-query' import PatientRepository from '../../shared/db/PatientRepository' import Allergy from '../../shared/model/Allergy' -async function fetchPatientAllergies(_: QueryKey, patientId: string): Promise { +async function fetchPatientAllergies(_: string, patientId: string): Promise { const patient = await PatientRepository.find(patientId) return patient.allergies || [] } diff --git a/src/patients/hooks/usePatientAppointments.tsx b/src/patients/hooks/usePatientAppointments.tsx new file mode 100644 index 0000000000..6a01fb3220 --- /dev/null +++ b/src/patients/hooks/usePatientAppointments.tsx @@ -0,0 +1,12 @@ +import { useQuery } from 'react-query' + +import PatientRepository from '../../shared/db/PatientRepository' +import Appointments from '../../shared/model/Appointment' + +async function fetchPatientAppointments(_: string, patientId: string): Promise { + return PatientRepository.getAppointments(patientId) +} + +export default function usePatientsAppointments(patientId: string) { + return useQuery(['appointments', patientId], fetchPatientAppointments) +} diff --git a/src/patients/hooks/usePatientCareGoals.tsx b/src/patients/hooks/usePatientCareGoals.tsx new file mode 100644 index 0000000000..b187c2a72f --- /dev/null +++ b/src/patients/hooks/usePatientCareGoals.tsx @@ -0,0 +1,13 @@ +import { useQuery } from 'react-query' + +import PatientRepository from '../../shared/db/PatientRepository' +import CareGoal from '../../shared/model/CareGoal' + +async function fetchPatientCareGoals(_: string, patientId: string): Promise { + const patient = await PatientRepository.find(patientId) + return patient.careGoals || [] +} + +export default function usePatientCareGoals(patientId: string) { + return useQuery(['care-goals', patientId], fetchPatientCareGoals) +} diff --git a/src/patients/hooks/usePatientCarePlans.tsx b/src/patients/hooks/usePatientCarePlans.tsx index 68ba6b1453..4e05ba4699 100644 --- a/src/patients/hooks/usePatientCarePlans.tsx +++ b/src/patients/hooks/usePatientCarePlans.tsx @@ -1,9 +1,9 @@ -import { QueryKey, useQuery } from 'react-query' +import { useQuery } from 'react-query' import PatientRepository from '../../shared/db/PatientRepository' import CarePlan from '../../shared/model/CarePlan' -async function fetchPatientCarePlans(_: QueryKey, patientId: string): Promise { +async function fetchPatientCarePlans(_: string, patientId: string): Promise { const patient = await PatientRepository.find(patientId) return patient.carePlans || [] } diff --git a/src/patients/hooks/usePatientDiagnoses.tsx b/src/patients/hooks/usePatientDiagnoses.tsx new file mode 100644 index 0000000000..1cd513baf0 --- /dev/null +++ b/src/patients/hooks/usePatientDiagnoses.tsx @@ -0,0 +1,13 @@ +import { useQuery } from 'react-query' + +import PatientRepository from '../../shared/db/PatientRepository' +import Diagnosis from '../../shared/model/Diagnosis' + +async function fetchPatientDiagnoses(_: string, patientId: string): Promise { + const patient = await PatientRepository.find(patientId) + return patient.diagnoses || [] +} + +export default function usePatientDiagnoses(patientId: string) { + return useQuery(['diagnoses', patientId], fetchPatientDiagnoses) +} diff --git a/src/patients/hooks/usePatientLabs.tsx b/src/patients/hooks/usePatientLabs.tsx new file mode 100644 index 0000000000..0ccd362b7b --- /dev/null +++ b/src/patients/hooks/usePatientLabs.tsx @@ -0,0 +1,13 @@ +import { useQuery } from 'react-query' + +import PatientRepository from '../../shared/db/PatientRepository' +import Lab from '../../shared/model/Lab' + +async function fetchPatientLabs(_: string, patientId: string): Promise { + const fetchedLabs = await PatientRepository.getLabs(patientId) + return fetchedLabs || [] +} + +export default function usePatientLabs(patientId: string) { + return useQuery(['labs', patientId], fetchPatientLabs) +} diff --git a/src/patients/hooks/usePatientNote.tsx b/src/patients/hooks/usePatientNote.tsx new file mode 100644 index 0000000000..83b4b5d546 --- /dev/null +++ b/src/patients/hooks/usePatientNote.tsx @@ -0,0 +1,19 @@ +import { useQuery } from 'react-query' + +import PatientRepository from '../../shared/db/PatientRepository' +import Note from '../../shared/model/Note' + +async function getNote(_: string, patientId: string, noteId: string): Promise { + const patient = await PatientRepository.find(patientId) + const maybeNote = patient.notes?.find((n) => n.id === noteId) + + if (!maybeNote) { + throw new Error('Note not found') + } + + return maybeNote +} + +export default function usePatientNote(patientId: string, noteId: string) { + return useQuery(['notes', patientId, noteId], getNote) +} diff --git a/src/patients/hooks/usePatientNotes.tsx b/src/patients/hooks/usePatientNotes.tsx new file mode 100644 index 0000000000..4e9e21d339 --- /dev/null +++ b/src/patients/hooks/usePatientNotes.tsx @@ -0,0 +1,13 @@ +import { useQuery } from 'react-query' + +import PatientRepository from '../../shared/db/PatientRepository' +import Note from '../../shared/model/Note' + +async function fetchPatientNotes(_: string, patientId: string): Promise { + const patient = await PatientRepository.find(patientId) + return patient.notes || [] +} + +export default function usePatientNotes(patientId: string) { + return useQuery(['notes', patientId], fetchPatientNotes) +} diff --git a/src/patients/hooks/usePatientVisits.tsx b/src/patients/hooks/usePatientVisits.tsx new file mode 100644 index 0000000000..57a17d29e8 --- /dev/null +++ b/src/patients/hooks/usePatientVisits.tsx @@ -0,0 +1,12 @@ +import { useQuery } from 'react-query' + +import PatientRepository from '../../shared/db/PatientRepository' +import Visit from '../../shared/model/Visit' + +async function fetchPatientVisits(_: string, id: string): Promise { + return (await PatientRepository.find(id)).visits +} + +export default function usePatientVisits(id: string) { + return useQuery(['visits', id], fetchPatientVisits) +} diff --git a/src/patients/hooks/useRemovePatientRelatedPerson.tsx b/src/patients/hooks/useRemovePatientRelatedPerson.tsx new file mode 100644 index 0000000000..f1a9c13543 --- /dev/null +++ b/src/patients/hooks/useRemovePatientRelatedPerson.tsx @@ -0,0 +1,37 @@ +import { useMutation, queryCache } from 'react-query' + +import PatientRepository from '../../shared/db/PatientRepository' +import RelatedPerson from '../../shared/model/RelatedPerson' + +interface removeRelatedPersonRequest { + patientId: string + relatedPersonId: string +} + +async function removeRelatedPerson(request: removeRelatedPersonRequest): Promise { + const patient = await PatientRepository.find(request.patientId) + const relatedPersons = patient.relatedPersons + ? patient.relatedPersons.filter((rp) => rp.patientId !== request.relatedPersonId) + : [] + await PatientRepository.saveOrUpdate({ + ...patient, + relatedPersons, + }) + + return relatedPersons +} + +export default function useRemovePatientRelatedPerson() { + return useMutation(removeRelatedPerson, { + onSuccess: async (data, variables) => { + const relatedPersons = await Promise.all( + data.map(async (rp) => { + const patient = await PatientRepository.find(rp.patientId) + return { ...patient, type: rp.type } + }), + ) + await queryCache.setQueryData(['related-persons', variables.patientId], relatedPersons) + }, + throwOnError: true, + }) +} diff --git a/src/patients/hooks/useVisit.tsx b/src/patients/hooks/useVisit.tsx new file mode 100644 index 0000000000..2a953796ca --- /dev/null +++ b/src/patients/hooks/useVisit.tsx @@ -0,0 +1,17 @@ +import { useQuery } from 'react-query' + +import PatientRepository from '../../shared/db/PatientRepository' +import Visit from '../../shared/model/Visit' + +async function fetchVisit( + _: string, + patientId: string, + visitsId: string, +): Promise { + const { visits } = await PatientRepository.find(patientId) + return visits.find(({ id }) => id === visitsId) || undefined +} + +export default function useVisit(patientId: string, visitsId: string) { + return useQuery(['visits', patientId, visitsId], fetchVisit) +} diff --git a/src/patients/labs/Labs.tsx b/src/patients/labs/Labs.tsx new file mode 100644 index 0000000000..88b2545ccf --- /dev/null +++ b/src/patients/labs/Labs.tsx @@ -0,0 +1,30 @@ +import React from 'react' +import { Route } from 'react-router-dom' + +import useAddBreadcrumbs from '../../page-header/breadcrumbs/useAddBreadcrumbs' +import Patient from '../../shared/model/Patient' +import LabsList from './LabsList' + +interface LabsProps { + patient: Patient +} + +const Labs = (props: LabsProps) => { + const { patient } = props + + const breadcrumbs = [ + { + i18nKey: 'patient.labs.label', + location: `/patients/${patient.id}/labs`, + }, + ] + useAddBreadcrumbs(breadcrumbs) + + return ( + + + + ) +} + +export default Labs diff --git a/src/patients/labs/LabsList.tsx b/src/patients/labs/LabsList.tsx new file mode 100644 index 0000000000..0f49b225fd --- /dev/null +++ b/src/patients/labs/LabsList.tsx @@ -0,0 +1,54 @@ +import { Alert, Table } from '@hospitalrun/components' +import format from 'date-fns/format' +import React from 'react' +import { useHistory } from 'react-router-dom' + +import Loading from '../../shared/components/Loading' +import useTranslator from '../../shared/hooks/useTranslator' +import Patient from '../../shared/model/Patient' +import usePatientLabs from '../hooks/usePatientLabs' + +interface Props { + patient: Patient +} + +const LabsList = (props: Props) => { + const { patient } = props + const history = useHistory() + const { t } = useTranslator() + const { data, status } = usePatientLabs(patient.id) + + if (data === undefined || status === 'loading') { + return + } + + if (data.length === 0) { + return ( + + ) + } + + return ( +
row.id} + data={data} + columns={[ + { label: t('labs.lab.type'), key: 'type' }, + { + label: t('labs.lab.requestedOn'), + key: 'requestedOn', + formatter: (row) => format(new Date(row.requestedOn), 'yyyy-MM-dd hh:mm a'), + }, + { label: t('labs.lab.status'), key: 'status' }, + ]} + actions={[{ label: t('actions.view'), action: (row) => history.push(`/labs/${row.id}`) }]} + /> + ) +} + +export default LabsList diff --git a/src/patients/labs/LabsTab.tsx b/src/patients/labs/LabsTab.tsx deleted file mode 100644 index 66296cee95..0000000000 --- a/src/patients/labs/LabsTab.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { Alert, Table } from '@hospitalrun/components' -import format from 'date-fns/format' -import React, { useEffect, useState } from 'react' -import { useHistory } from 'react-router-dom' - -import PatientRepository from '../../shared/db/PatientRepository' -import useTranslator from '../../shared/hooks/useTranslator' -import Lab from '../../shared/model/Lab' - -interface Props { - patientId: string -} - -const LabsTab = (props: Props) => { - const history = useHistory() - const { patientId } = props - const { t } = useTranslator() - - const [labs, setLabs] = useState([]) - - useEffect(() => { - const fetch = async () => { - const fetchedLabs = await PatientRepository.getLabs(patientId) - setLabs(fetchedLabs) - } - - fetch() - }, [patientId]) - - return ( -
- {(!labs || labs.length === 0) && ( - - )} - {labs && labs.length > 0 && ( -
row.id} - data={labs} - columns={[ - { label: t('labs.lab.type'), key: 'type' }, - { - label: t('labs.lab.requestedOn'), - key: 'requestedOn', - formatter: (row) => format(new Date(row.requestedOn), 'yyyy-MM-dd hh:mm a'), - }, - { label: t('labs.lab.status'), key: 'status' }, - ]} - actions={[{ label: t('actions.view'), action: (row) => history.push(`/labs/${row.id}`) }]} - /> - )} - - ) -} - -export default LabsTab diff --git a/src/patients/new/NewPatient.tsx b/src/patients/new/NewPatient.tsx index 30fd1d1393..666909e3dc 100644 --- a/src/patients/new/NewPatient.tsx +++ b/src/patients/new/NewPatient.tsx @@ -4,7 +4,7 @@ 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 { useUpdateTitle } from '../../page-header/title/TitleContext' import useTranslator from '../../shared/hooks/useTranslator' import Patient from '../../shared/model/Patient' import { RootState } from '../../shared/store' @@ -36,7 +36,8 @@ const NewPatient = () => { dateOfBirth: '1963-01-09T05:00:00.000Z', } as Patient - useTitle(t('patients.newPatient')) + const updateTitle = useUpdateTitle() + updateTitle(t('patients.newPatient')) useAddBreadcrumbs(breadcrumbs, true) const onCancel = () => { diff --git a/src/patients/notes/NewNoteModal.tsx b/src/patients/notes/NewNoteModal.tsx index 818522d2fb..93885a6475 100644 --- a/src/patients/notes/NewNoteModal.tsx +++ b/src/patients/notes/NewNoteModal.tsx @@ -1,48 +1,53 @@ import { Modal, Alert } from '@hospitalrun/components' import React, { useState } from 'react' -import { useDispatch, useSelector } from 'react-redux' import TextFieldWithLabelFormGroup from '../../shared/components/input/TextFieldWithLabelFormGroup' import useTranslator from '../../shared/hooks/useTranslator' -import Note from '../../shared/model/Note' -import { RootState } from '../../shared/store' -import { addNote } from '../patient-slice' +import useAddPatientNote from '../hooks/useAddPatientNote' +import { NoteError } from '../util/validate-note' interface Props { show: boolean toggle: () => void onCloseButtonClick: () => void + patientId: string } +const initialNoteState = { text: '', date: new Date().toISOString() } const NewNoteModal = (props: Props) => { - const { show, toggle, onCloseButtonClick } = props - const dispatch = useDispatch() - const { patient, noteError } = useSelector((state: RootState) => state.patient) + const { show, toggle, onCloseButtonClick, patientId } = props const { t } = useTranslator() - const [note, setNote] = useState({ - text: '', - }) + const [mutate] = useAddPatientNote() - const onFieldChange = (key: string, value: string | any) => { - setNote({ - ...note, - [key]: value, - }) - } + const [noteError, setNoteError] = useState(undefined) + const [note, setNote] = useState(initialNoteState) const onNoteTextChange = (event: React.ChangeEvent) => { const text = event.currentTarget.value - onFieldChange('text', text) + setNote({ + ...note, + text, + }) } - const onSaveButtonClick = () => { - dispatch(addNote(patient.id, note as Note)) + const onSaveButtonClick = async () => { + try { + await mutate({ patientId, note }) + setNote(initialNoteState) + onCloseButtonClick() + } catch (e) { + setNoteError(e) + } } const body = (
- {noteError?.message && ( - + {noteError && ( + )}
@@ -53,8 +58,8 @@ const NewNoteModal = (props: Props) => { name="noteTextField" label={t('patient.note')} value={note.text} - isInvalid={!!noteError?.note} - feedback={t(noteError?.note || '')} + isInvalid={!!noteError?.noteError} + feedback={t(noteError?.noteError || '')} onChange={onNoteTextChange} />
diff --git a/src/patients/notes/NoteTab.tsx b/src/patients/notes/NoteTab.tsx index b32ce3e732..31235a2eb5 100644 --- a/src/patients/notes/NoteTab.tsx +++ b/src/patients/notes/NoteTab.tsx @@ -1,13 +1,16 @@ -import { Button, List, ListItem, Alert } from '@hospitalrun/components' +import { Button } from '@hospitalrun/components' import React, { useState } from 'react' import { useSelector } from 'react-redux' +import { Route, Switch } from 'react-router-dom' +import useAddBreadcrumbs from '../../page-header/breadcrumbs/useAddBreadcrumbs' import useTranslator from '../../shared/hooks/useTranslator' -import Note from '../../shared/model/Note' import Patient from '../../shared/model/Patient' import Permissions from '../../shared/model/Permissions' import { RootState } from '../../shared/store' import NewNoteModal from './NewNoteModal' +import NotesList from './NotesList' +import ViewNote from './ViewNote' interface Props { patient: Patient @@ -19,6 +22,14 @@ const NoteTab = (props: Props) => { const { permissions } = useSelector((state: RootState) => state.user) const [showNewNoteModal, setShowNoteModal] = useState(false) + const breadcrumbs = [ + { + i18nKey: 'patient.notes.label', + location: `/patients/${patient.id}/notes`, + }, + ] + useAddBreadcrumbs(breadcrumbs) + const onNewNoteClick = () => { setShowNoteModal(true) } @@ -45,25 +56,20 @@ const NoteTab = (props: Props) => {

- {(!patient.notes || patient.notes.length === 0) && ( - - )} - - {patient.notes?.map((note: Note) => ( - - {new Date(note.date).toLocaleString()} -

{note.text}

-
- ))} -
+ + + + + + + + + ) diff --git a/src/patients/notes/NotesList.tsx b/src/patients/notes/NotesList.tsx new file mode 100644 index 0000000000..cd62c0df3f --- /dev/null +++ b/src/patients/notes/NotesList.tsx @@ -0,0 +1,50 @@ +import { Alert, List, ListItem } from '@hospitalrun/components' +import React from 'react' +import { useHistory } from 'react-router-dom' + +import Loading from '../../shared/components/Loading' +import useTranslator from '../../shared/hooks/useTranslator' +import Note from '../../shared/model/Note' +import usePatientNotes from '../hooks/usePatientNotes' + +interface Props { + patientId: string +} + +const NotesList = (props: Props) => { + const { patientId } = props + const history = useHistory() + const { t } = useTranslator() + const { data, status } = usePatientNotes(patientId) + + if (data === undefined || status === 'loading') { + return + } + + if (data.length === 0) { + return ( + + ) + } + + return ( + + {data.map((note: Note) => ( + history.push(`/patients/${patientId}/notes/${note.id}`)} + > +

{new Date(note.date).toLocaleString()}

+

{note.text}

+
+ ))} +
+ ) +} + +export default NotesList diff --git a/src/patients/notes/ViewNote.tsx b/src/patients/notes/ViewNote.tsx new file mode 100644 index 0000000000..5aef2b0117 --- /dev/null +++ b/src/patients/notes/ViewNote.tsx @@ -0,0 +1,32 @@ +import React from 'react' +import { useParams } from 'react-router-dom' + +import TextInputWithLabelFormGroup from '../../shared/components/input/TextInputWithLabelFormGroup' +import Loading from '../../shared/components/Loading' +import useTranslator from '../../shared/hooks/useTranslator' +import usePatientNote from '../hooks/usePatientNote' + +const ViewNote = () => { + const { t } = useTranslator() + const { noteId, id: patientId } = useParams() + const { data, status } = usePatientNote(patientId, noteId) + + if (data === undefined || status === 'loading') { + return + } + + return ( +
+

Date: {new Date(data.date).toLocaleString()}

+ +
+ ) +} + +export default ViewNote diff --git a/src/patients/patient-slice.ts b/src/patients/patient-slice.ts index 7dcf0a82ca..3119407c02 100644 --- a/src/patients/patient-slice.ts +++ b/src/patients/patient-slice.ts @@ -1,14 +1,11 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' -import { isAfter, isBefore, parseISO } from 'date-fns' +import { isAfter, parseISO } from 'date-fns' import { isEmpty } from 'lodash' import validator from 'validator' import PatientRepository from '../shared/db/PatientRepository' import Diagnosis from '../shared/model/Diagnosis' -import Note from '../shared/model/Note' import Patient from '../shared/model/Patient' -import RelatedPerson from '../shared/model/RelatedPerson' -import Visit from '../shared/model/Visit' import { AppThunk } from '../shared/store' import { uuid } from '../shared/util/uuid' import { cleanupPatient } from './util/set-patient-helper' @@ -22,10 +19,7 @@ interface PatientState { updateError?: Error allergyError?: AddAllergyError diagnosisError?: AddDiagnosisError - noteError?: AddNoteError relatedPersonError?: AddRelatedPersonError - carePlanError?: AddCarePlanError - visitError?: AddVisitError } interface Error { @@ -58,31 +52,6 @@ interface AddDiagnosisError { status?: string } -interface AddNoteError { - message?: string - note?: string -} - -interface AddCarePlanError { - message?: string - title?: string - description?: string - status?: string - intent?: string - startDate?: string - endDate?: string - note?: string - condition?: string -} - -interface AddVisitError { - message?: string - status?: string - intent?: string - startDateTime?: string - endDateTime?: string -} - const initialState: PatientState = { status: 'loading', isUpdatedSuccessfully: false, @@ -90,12 +59,8 @@ const initialState: PatientState = { relatedPersons: [], createError: undefined, updateError: undefined, - allergyError: undefined, diagnosisError: undefined, - noteError: undefined, relatedPersonError: undefined, - carePlanError: undefined, - visitError: undefined, } function start(state: PatientState) { @@ -129,30 +94,10 @@ const patientSlice = createSlice({ state.status = 'error' state.updateError = payload }, - addAllergyError(state, { payload }: PayloadAction) { - state.status = 'error' - state.allergyError = payload - }, addDiagnosisError(state, { payload }: PayloadAction) { state.status = 'error' state.diagnosisError = payload }, - addRelatedPersonError(state, { payload }: PayloadAction) { - state.status = 'error' - state.relatedPersonError = payload - }, - addNoteError(state, { payload }: PayloadAction) { - state.status = 'error' - state.noteError = payload - }, - addCarePlanError(state, { payload }: PayloadAction) { - state.status = 'error' - state.carePlanError = payload - }, - addVisitError(state, { payload }: PayloadAction) { - state.status = 'error' - state.visitError = payload - }, }, }) @@ -165,12 +110,7 @@ export const { updatePatientStart, updatePatientSuccess, updatePatientError, - addAllergyError, addDiagnosisError, - addRelatedPersonError, - addNoteError, - addCarePlanError, - addVisitError, } = patientSlice.actions export const fetchPatient = (id: string): AppThunk => async (dispatch) => { @@ -297,51 +237,6 @@ export const updatePatient = ( } } -function validateRelatedPerson(relatedPerson: RelatedPerson) { - const error: AddRelatedPersonError = {} - - if (!relatedPerson.patientId) { - error.relatedPerson = 'patient.relatedPersons.error.relatedPersonRequired' - } - - if (!relatedPerson.type) { - error.relationshipType = 'patient.relatedPersons.error.relationshipTypeRequired' - } - - return error -} - -export const addRelatedPerson = ( - patientId: string, - relatedPerson: RelatedPerson, - onSuccess?: (patient: Patient) => void, -): AppThunk => async (dispatch) => { - const newRelatedPersonError = validateRelatedPerson(relatedPerson) - - if (isEmpty(newRelatedPersonError)) { - const patient = await PatientRepository.find(patientId) - const relatedPersons = patient.relatedPersons || [] - relatedPersons.push({ id: uuid(), ...relatedPerson }) - patient.relatedPersons = relatedPersons - - await dispatch(updatePatient(patient, onSuccess)) - } else { - newRelatedPersonError.message = 'patient.relatedPersons.error.unableToAddRelatedPerson' - dispatch(addRelatedPersonError(newRelatedPersonError)) - } -} - -export const removeRelatedPerson = ( - patientId: string, - relatedPersonId: string, - onSuccess?: (patient: Patient) => void, -): AppThunk => async (dispatch) => { - const patient = await PatientRepository.find(patientId) - patient.relatedPersons = patient.relatedPersons?.filter((r) => r.patientId !== relatedPersonId) - - await dispatch(updatePatient(patient, onSuccess)) -} - function validateDiagnosis(diagnosis: Diagnosis) { const error: AddDiagnosisError = {} @@ -383,86 +278,4 @@ export const addDiagnosis = ( } } -function validateNote(note: Note) { - const error: AddNoteError = {} - if (!note.text) { - error.message = 'patient.notes.error.noteRequired' - } - - return error -} - -export const addNote = ( - patientId: string, - note: Note, - onSuccess?: (patient: Patient) => void, -): AppThunk => async (dispatch) => { - const newNoteError = validateNote(note) - - if (isEmpty(newNoteError)) { - const patient = await PatientRepository.find(patientId) - const notes = patient.notes || [] - notes.push({ id: uuid(), date: new Date().toISOString(), ...note }) - patient.notes = notes - - await dispatch(updatePatient(patient, onSuccess)) - } else { - newNoteError.message = 'patient.notes.error.unableToAdd' - dispatch(addNoteError(newNoteError)) - } -} - -function validateVisit(visit: Visit): AddVisitError { - const error: AddVisitError = {} - - if (!visit.startDateTime) { - error.startDateTime = 'patient.visits.error.startDateRequired' - } - - if (!visit.endDateTime) { - error.endDateTime = 'patient.visits.error.endDateRequired' - } - - if (!visit.type) { - error.status = 'patient.visits.error.typeRequired' - } - - if (visit.startDateTime && visit.endDateTime) { - if (isBefore(new Date(visit.endDateTime), new Date(visit.startDateTime))) { - error.endDateTime = 'patient.visits.error.endDateMustBeAfterStartDate' - } - } - - if (!visit.status) { - error.status = 'patient.visits.error.statusRequired' - } - if (!visit.reason) { - error.status = 'patient.visits.error.reasonRequired' - } - if (!visit.location) { - error.status = 'patient.visits.error.locationRequired' - } - - return error -} - -export const addVisit = ( - patientId: string, - visit: Visit, - onSuccess?: (patient: Patient) => void, -): AppThunk => async (dispatch) => { - const visitError = validateVisit(visit) - if (isEmpty(visitError)) { - const patient = await PatientRepository.find(patientId) - const visits = patient.visits || ([] as Visit[]) - visits.push({ - id: uuid(), - createdAt: new Date(Date.now().valueOf()).toISOString(), - ...visit, - }) - patient.visits = visits - - await dispatch(updatePatient(patient, onSuccess)) - } -} export default patientSlice.reducer diff --git a/src/patients/related-persons/AddRelatedPersonModal.tsx b/src/patients/related-persons/AddRelatedPersonModal.tsx index c7a30bd8e0..2de08a6e9c 100644 --- a/src/patients/related-persons/AddRelatedPersonModal.tsx +++ b/src/patients/related-persons/AddRelatedPersonModal.tsx @@ -1,33 +1,43 @@ import { Modal, Alert, Typeahead, Label } from '@hospitalrun/components' import format from 'date-fns/format' import React, { useState } from 'react' -import { useDispatch, useSelector } from 'react-redux' import TextInputWithLabelFormGroup from '../../shared/components/input/TextInputWithLabelFormGroup' -import PatientRepository from '../../shared/db/PatientRepository' import useTranslator from '../../shared/hooks/useTranslator' import Patient from '../../shared/model/Patient' -import RelatedPerson from '../../shared/model/RelatedPerson' -import { RootState } from '../../shared/store' -import { addRelatedPerson } from '../patient-slice' +import useAddPatientRelatedPerson from '../hooks/useAddPatientRelatedPerson' +import usePatients from '../hooks/usePatients' +import { RelatedPersonError } from '../util/validate-related-person' interface Props { + patientId: string show: boolean toggle: () => void onCloseButtonClick: () => void } const AddRelatedPersonModal = (props: Props) => { - const dispatch = useDispatch() const { t } = useTranslator() - const { patient, relatedPersonError } = useSelector((state: RootState) => state.patient) - const { show, toggle, onCloseButtonClick } = props + const { patientId, show, toggle, onCloseButtonClick } = props const [relatedPerson, setRelatedPerson] = useState({ patientId: '', type: '', }) + const [patientQuery, setPatientQuery] = useState('') + + const { data, status } = usePatients({ queryString: patientQuery }) + let patients = [] as Patient[] + if (data !== undefined && status !== 'loading') { + patients = data.patients.filter((p: Patient) => p.id !== patientId) + } + + const [mutate] = useAddPatientRelatedPerson() + const [relatedPersonError, setRelatedPersonError] = useState( + undefined, + ) + const onFieldChange = (key: string, value: string) => { setRelatedPerson({ ...relatedPerson, @@ -40,20 +50,35 @@ const AddRelatedPersonModal = (props: Props) => { } const onPatientSelect = (p: Patient[]) => { - setRelatedPerson({ ...relatedPerson, patientId: p[0].id }) + if (p.length > 0) { + setRelatedPerson({ ...relatedPerson, patientId: p[0].id }) + } } const onSearch = async (query: string) => { - const patients: Patient[] = await PatientRepository.search(query) - return patients.filter((p: Patient) => p.id !== patient.id) + setPatientQuery(query) + return [...patients] + } + + const onSaveButtonClick = async () => { + try { + await mutate({ patientId, relatedPerson }) + onCloseButtonClick() + } catch (e) { + setRelatedPersonError(e) + } } const formattedDate = (date: string) => (date ? format(new Date(date), 'yyyy-MM-dd') : '') const body = ( - {relatedPersonError?.message && ( - + {relatedPersonError && ( + )}
@@ -64,15 +89,15 @@ const AddRelatedPersonModal = (props: Props) => { searchAccessor="fullName" placeholder={t('patient.relatedPerson')} onChange={onPatientSelect} - isInvalid={!!relatedPersonError?.relatedPerson} + isInvalid={!!relatedPersonError?.relatedPersonError} onSearch={onSearch} renderMenuItemChildren={(p: Patient) => (
{`${p.fullName} - ${formattedDate(p.dateOfBirth)} (${p.code})`}
)} /> - {relatedPersonError?.relatedPerson && ( + {relatedPersonError?.relatedPersonError && (
- {t(relatedPersonError?.relatedPerson)} + {t(relatedPersonError?.relatedPersonError)}
)}
@@ -85,8 +110,8 @@ const AddRelatedPersonModal = (props: Props) => { label={t('patient.relatedPersons.relationshipType')} value={relatedPerson.type} isEditable - isInvalid={!!relatedPersonError?.relationshipType} - feedback={t(relatedPersonError?.relationshipType || '')} + isInvalid={!!relatedPersonError?.relationshipTypeError} + feedback={t(relatedPersonError?.relationshipTypeError)} isRequired onChange={(event: React.ChangeEvent) => { onInputElementChange(event, 'type') @@ -113,9 +138,7 @@ const AddRelatedPersonModal = (props: Props) => { color: 'success', icon: 'add', iconLocation: 'left', - onClick: () => { - dispatch(addRelatedPerson(patient.id, relatedPerson as RelatedPerson)) - }, + onClick: onSaveButtonClick, }} /> ) diff --git a/src/patients/related-persons/RelatedPersonTab.tsx b/src/patients/related-persons/RelatedPersonTab.tsx index 4524d56f17..b65a220a72 100644 --- a/src/patients/related-persons/RelatedPersonTab.tsx +++ b/src/patients/related-persons/RelatedPersonTab.tsx @@ -1,15 +1,15 @@ import { Button, Alert, Spinner, Table } from '@hospitalrun/components' -import React, { useState, useEffect } from 'react' -import { useDispatch, useSelector } from 'react-redux' +import React, { useState } from 'react' +import { useSelector } from 'react-redux' import { useHistory } from 'react-router-dom' import useAddBreadcrumbs from '../../page-header/breadcrumbs/useAddBreadcrumbs' -import PatientRepository from '../../shared/db/PatientRepository' import useTranslator from '../../shared/hooks/useTranslator' import Patient from '../../shared/model/Patient' import Permissions from '../../shared/model/Permissions' import { RootState } from '../../shared/store' -import { removeRelatedPerson } from '../patient-slice' +import usePatientRelatedPersons from '../hooks/usePatientRelatedPersons' +import useRemovePatientRelatedPerson from '../hooks/useRemovePatientRelatedPerson' import AddRelatedPersonModal from './AddRelatedPersonModal' interface Props { @@ -17,7 +17,6 @@ interface Props { } const RelatedPersonTab = (props: Props) => { - const dispatch = useDispatch() const history = useHistory() const navigateTo = (location: string) => { @@ -27,7 +26,7 @@ const RelatedPersonTab = (props: Props) => { const { t } = useTranslator() const { permissions } = useSelector((state: RootState) => state.user) const [showNewRelatedPersonModal, setShowRelatedPersonModal] = useState(false) - const [relatedPersons, setRelatedPersons] = useState(undefined) + const [mutate] = useRemovePatientRelatedPerson() const breadcrumbs = [ { @@ -37,23 +36,7 @@ const RelatedPersonTab = (props: Props) => { ] useAddBreadcrumbs(breadcrumbs) - useEffect(() => { - const fetchRelatedPersons = async () => { - const fetchedRelatedPersons: Patient[] = [] - if (patient.relatedPersons) { - await Promise.all( - patient.relatedPersons.map(async (person) => { - const fetchedRelatedPerson = await PatientRepository.find(person.patientId) - fetchedRelatedPersons.push({ ...fetchedRelatedPerson, type: person.type }) - }), - ) - } - - setRelatedPersons(fetchedRelatedPersons) - } - - fetchRelatedPersons() - }, [patient.relatedPersons]) + const { data: relatedPersons } = usePatientRelatedPersons(patient.id) const onNewRelatedPersonClick = () => { setShowRelatedPersonModal(true) @@ -64,7 +47,7 @@ const RelatedPersonTab = (props: Props) => { } const onRelatedPersonDelete = (relatedPerson: Patient) => { - dispatch(removeRelatedPerson(patient.id, relatedPerson.id)) + mutate({ patientId: patient.id, relatedPersonId: relatedPerson.id }) } return ( @@ -121,6 +104,7 @@ const RelatedPersonTab = (props: Props) => {
{ const { t } = useTranslator() const history = useHistory() - useTitle(t('patients.label')) + const updateTitle = useUpdateTitle() + updateTitle(t('patients.label')) const dispatch = useDispatch() const setButtonToolBar = useButtonToolbarSetter() diff --git a/src/patients/util/validate-caregoal.ts b/src/patients/util/validate-caregoal.ts new file mode 100644 index 0000000000..75cf848dbc --- /dev/null +++ b/src/patients/util/validate-caregoal.ts @@ -0,0 +1,74 @@ +import { isBefore } from 'date-fns' + +import CareGoal from '../../shared/model/CareGoal' + +export class CareGoalError extends Error { + message: string + + description?: string + + status?: string + + achievementStatus?: string + + priority?: string + + startDate?: string + + dueDate?: string + + constructor( + message: string, + description: string, + status: string, + achievementStatus: string, + priority: string, + startDate: string, + dueDate: string, + ) { + super(message) + this.message = message + this.description = description + this.status = status + this.achievementStatus = achievementStatus + this.priority = priority + this.startDate = startDate + this.dueDate = dueDate + } +} + +export default function validateCareGoal(careGoal: Partial): CareGoalError { + const error = {} as CareGoalError + + if (!careGoal.description) { + error.description = 'patient.careGoal.error.descriptionRequired' + } + + if (!careGoal.status) { + error.status = 'patient.careGoal.error.statusRequired' + } + + if (!careGoal.achievementStatus) { + error.achievementStatus = 'patient.careGoal.error.achievementStatusRequired' + } + + if (!careGoal.priority) { + error.priority = 'patient.careGoal.error.priorityRequired' + } + + if (!careGoal.startDate) { + error.startDate = 'patient.careGoal.error.startDate' + } + + if (!careGoal.dueDate) { + error.dueDate = 'patient.careGoal.error.dueDate' + } + + if (careGoal.startDate && careGoal.dueDate) { + if (isBefore(new Date(careGoal.dueDate), new Date(careGoal.startDate))) { + error.dueDate = 'patient.careGoal.error.dueDateMustBeAfterStartDate' + } + } + + return error +} diff --git a/src/patients/util/validate-diagnosis.ts b/src/patients/util/validate-diagnosis.ts new file mode 100644 index 0000000000..7a5b1ebbeb --- /dev/null +++ b/src/patients/util/validate-diagnosis.ts @@ -0,0 +1,46 @@ +import Diagnosis from '../../shared/model/Diagnosis' + +interface AddDiagnosisError { + message?: string + name?: string + diagnosisDate?: string + onsetDate?: string + abatementDate?: string + status?: string + note?: string +} + +export class DiagnosisError extends Error { + nameError?: string + + constructor(message: string, name: string) { + super(message) + this.nameError = name + Object.setPrototypeOf(this, DiagnosisError.prototype) + } +} + +export default function validateDiagnosis(diagnosis: Partial) { + const error: AddDiagnosisError = {} + + if (!diagnosis.name) { + error.name = 'patient.diagnoses.error.nameRequired' + } + + if (!diagnosis.diagnosisDate) { + error.diagnosisDate = 'patient.diagnoses.error.dateRequired' + } + + if (!diagnosis.onsetDate) { + error.onsetDate = 'patient.diagnoses.error.dateRequired' + } + + if (!diagnosis.abatementDate) { + error.abatementDate = 'patient.diagnoses.error.dateRequired' + } + + if (!diagnosis.status) { + error.status = 'patient.diagnoses.error.statusRequired' + } + return error +} diff --git a/src/patients/util/validate-note.ts b/src/patients/util/validate-note.ts new file mode 100644 index 0000000000..f6b42c8834 --- /dev/null +++ b/src/patients/util/validate-note.ts @@ -0,0 +1,20 @@ +import Note from '../../shared/model/Note' + +export class NoteError extends Error { + noteError?: string + + constructor(message: string, note: string) { + super(message) + this.noteError = note + Object.setPrototypeOf(this, NoteError.prototype) + } +} + +export default function validateNote(note: Partial) { + const error: any = {} + if (!note.text) { + error.noteError = 'patient.notes.error.noteRequired' + } + + return error +} diff --git a/src/patients/util/validate-related-person.ts b/src/patients/util/validate-related-person.ts new file mode 100644 index 0000000000..4e13c14f62 --- /dev/null +++ b/src/patients/util/validate-related-person.ts @@ -0,0 +1,28 @@ +import RelatedPerson from '../../shared/model/RelatedPerson' + +export class RelatedPersonError extends Error { + relatedPersonError?: string + + relationshipTypeError?: string + + constructor(message: string, related: string, relationship: string) { + super(message) + this.relatedPersonError = related + this.relationshipTypeError = relationship + Object.setPrototypeOf(this, RelatedPersonError.prototype) + } +} + +export default function validateRelatedPerson(relatedPerson: Partial) { + const error: any = {} + + if (!relatedPerson.patientId) { + error.relatedPersonError = 'patient.relatedPersons.error.relatedPersonRequired' + } + + if (!relatedPerson.type) { + error.relationshipTypeError = 'patient.relatedPersons.error.relationshipTypeRequired' + } + + return error +} diff --git a/src/patients/util/validate-visit.ts b/src/patients/util/validate-visit.ts new file mode 100644 index 0000000000..50c53b7372 --- /dev/null +++ b/src/patients/util/validate-visit.ts @@ -0,0 +1,57 @@ +import { isBefore } from 'date-fns' + +import Visit from '../../shared/model/Visit' + +interface AddVisitError { + message?: string + status?: string + intent?: string + startDateTime?: string + endDateTime?: string +} + +export class VisitError extends Error { + nameError?: string + + constructor(message: string, name: string) { + super(message) + this.nameError = name + Object.setPrototypeOf(this, VisitError.prototype) + } +} + +export default function validateVisit(visit: Partial) { + const error: AddVisitError = {} + + if (!visit.startDateTime) { + error.startDateTime = 'patient.visits.error.startDateRequired' + } + + if (!visit.endDateTime) { + error.endDateTime = 'patient.visits.error.endDateRequired' + } + + if (!visit.type) { + error.status = 'patient.visits.error.typeRequired' + } + + if (visit.startDateTime && visit.endDateTime) { + if (isBefore(new Date(visit.endDateTime), new Date(visit.startDateTime))) { + error.endDateTime = 'patient.visits.error.endDateMustBeAfterStartDate' + } + } + + if (!visit.status) { + error.status = 'patient.visits.error.statusRequired' + } + + if (!visit.reason) { + error.status = 'patient.visits.error.reasonRequired' + } + + if (!visit.location) { + error.status = 'patient.visits.error.locationRequired' + } + + return error +} diff --git a/src/patients/view/ImportantPatientInfo.tsx b/src/patients/view/ImportantPatientInfo.tsx index dbee5eb150..95a4625b10 100644 --- a/src/patients/view/ImportantPatientInfo.tsx +++ b/src/patients/view/ImportantPatientInfo.tsx @@ -1,4 +1,4 @@ -import { Panel, Container, Row, Table, Button, Typography } from '@hospitalrun/components' +import { Row, Table, Button, Typography } from '@hospitalrun/components' import format from 'date-fns/format' import React, { CSSProperties, useState } from 'react' import { useSelector } from 'react-redux' @@ -14,7 +14,6 @@ import NewAllergyModal from '../allergies/NewAllergyModal' import AddCarePlanModal from '../care-plans/AddCarePlanModal' import AddDiagnosisModal from '../diagnoses/AddDiagnosisModal' import AddVisitModal from '../visits/AddVisitModal' -// import {getPatientFullName} from '../util/patient-name-util' interface Props { patient: Patient @@ -69,154 +68,137 @@ const ImportantPatientInfo = (props: Props) => { return (
- - - -
-

{patient.fullName}

-
-
-
- {t('patient.code')} -
{getPatientCode(patient)}
-
-
-
- {permissions.includes(Permissions.AddVisit) && ( - - )} -
-
- -
-
- {t('patient.sex')} -
{patient.sex}
-
-
- {t('patient.dateOfBirth')} -
- {patient.dateOfBirth - ? format(new Date(patient.dateOfBirth), 'MM/dd/yyyy') - : t('patient.unknownDateOfBirth')} -
-
- {/* - Sex - - {patient.sex} - - DateOfBirth - - - {patient.dateOfBirth - ? format(new Date(patient.dateOfBirth), 'MM/dd/yyyy') - : 'Unknown'} - */} -
- -
- {t('patient.allergies.label')} - {patient.allergies ? ( - patient.allergies?.map((a: Allergy) => ( -
  • - {a.name} -
  • - )) - ) : ( - <> - )} - {permissions.includes(Permissions.AddAllergy) && ( - - )} -
    -
    - -
    - {t('patient.diagnoses.label')} -
    -
    row.id} - columns={[ - { label: t('patient.diagnoses.diagnosisName'), key: 'name' }, - { - label: t('patient.diagnoses.diagnosisDate'), - key: 'diagnosisDate', - formatter: (row) => - row.diagnosisDate - ? format(new Date(row.diagnosisDate), 'yyyy-MM-dd hh:mm a') - : '', - }, - ]} - data={patient.diagnoses ? (patient.diagnoses as Diagnosis[]) : []} - /> - - {permissions.includes(Permissions.AddDiagnosis) && ( - - )} - -
    - {t('patient.carePlan.label')} -
    - {/* */} -
    history.push(`/patients/${patient.id}/care-plans/${row.id}`)} - getID={(row) => row.id} - data={patient.carePlans || []} - columns={[ - { label: t('patient.carePlan.title'), key: 'title' }, - { - label: t('patient.carePlan.startDate'), - key: 'startDate', - formatter: (row) => format(new Date(row.startDate), 'yyyy-MM-dd'), - }, - { - label: t('patient.carePlan.endDate'), - key: 'endDate', - formatter: (row) => format(new Date(row.endDate), 'yyyy-MM-dd'), - }, - { label: t('patient.carePlan.status'), key: 'status' }, - ]} - /> - - {permissions.includes(Permissions.AddCarePlan) && ( - - )} - - - - + +
    +

    {patient.fullName}

    +
    +
    +
    + {t('patient.code')} +
    {getPatientCode(patient)}
    +
    +
    +
    + {permissions.includes(Permissions.AddVisit) && ( + + )} +
    +
    + +
    +
    + {t('patient.sex')} +
    {patient.sex}
    +
    +
    + {t('patient.dateOfBirth')} +
    + {patient.dateOfBirth + ? format(new Date(patient.dateOfBirth), 'MM/dd/yyyy') + : t('patient.unknownDateOfBirth')} +
    +
    +
    + +
    + {t('patient.allergies.label')} + {patient.allergies ? ( + patient.allergies?.map((a: Allergy) => ( +
  • + {a.name} +
  • + )) + ) : ( + <> + )} + {permissions.includes(Permissions.AddAllergy) && ( + + )} +
    +
    + +
    + {t('patient.diagnoses.label')} +
    +
    row.id} + columns={[ + { label: t('patient.diagnoses.diagnosisName'), key: 'name' }, + { + label: t('patient.diagnoses.diagnosisDate'), + key: 'diagnosisDate', + formatter: (row) => + row.diagnosisDate + ? format(new Date(row.diagnosisDate), 'yyyy-MM-dd hh:mm a') + : '', + }, + ]} + data={patient.diagnoses ? (patient.diagnoses as Diagnosis[]) : []} + /> + + {permissions.includes(Permissions.AddDiagnosis) && ( + + )} + +
    + {t('patient.carePlan.label')} +
    +
    history.push(`/patients/${patient.id}/care-plans/${row.id}`)} + getID={(row) => row.id} + data={patient.carePlans || []} + columns={[ + { label: t('patient.carePlan.title'), key: 'title' }, + { + label: t('patient.carePlan.startDate'), + key: 'startDate', + formatter: (row) => format(new Date(row.startDate), 'yyyy-MM-dd'), + }, + { + label: t('patient.carePlan.endDate'), + key: 'endDate', + formatter: (row) => format(new Date(row.endDate), 'yyyy-MM-dd'), + }, + { label: t('patient.carePlan.status'), key: 'status' }, + ]} + /> + + {permissions.includes(Permissions.AddCarePlan) && ( + + )} + + { setShowDiagnosisModal(false)} + patient={patient} /> { setShowAddVisitModal(false)} + patientId={patient.id} />
    diff --git a/src/patients/view/ViewPatient.tsx b/src/patients/view/ViewPatient.tsx index 8fef173cce..f0f9fe7479 100644 --- a/src/patients/view/ViewPatient.tsx +++ b/src/patients/view/ViewPatient.tsx @@ -12,16 +12,17 @@ import { import useAddBreadcrumbs from '../../page-header/breadcrumbs/useAddBreadcrumbs' import { useButtonToolbarSetter } from '../../page-header/button-toolbar/ButtonBarProvider' -import useTitle from '../../page-header/title/useTitle' +import { useUpdateTitle } from '../../page-header/title/TitleContext' import useTranslator from '../../shared/hooks/useTranslator' import Permissions from '../../shared/model/Permissions' import { RootState } from '../../shared/store' import Allergies from '../allergies/Allergies' import AppointmentsList from '../appointments/AppointmentsList' +import CareGoalTab from '../care-goals/CareGoalTab' import CarePlanTab from '../care-plans/CarePlanTab' import Diagnoses from '../diagnoses/Diagnoses' import GeneralInformation from '../GeneralInformation' -import Labs from '../labs/LabsTab' +import Labs from '../labs/Labs' import Note from '../notes/NoteTab' import { fetchPatient } from '../patient-slice' import RelatedPerson from '../related-persons/RelatedPersonTab' @@ -39,7 +40,8 @@ const ViewPatient = () => { const { patient, status } = useSelector((state: RootState) => state.patient) const { permissions } = useSelector((state: RootState) => state.user) - useTitle(t('patient.label')) + const updateTitle = useUpdateTitle() + updateTitle(t('patient.label')) const setButtonToolBar = useButtonToolbarSetter() @@ -129,6 +131,11 @@ const ViewPatient = () => { label={t('patient.carePlan.label')} onClick={() => history.push(`/patients/${patient.id}/care-plans`)} /> + history.push(`/patients/${patient.id}/care-goals`)} + /> { - + + + + - + diff --git a/src/patients/visits/AddVisitModal.tsx b/src/patients/visits/AddVisitModal.tsx index 8c6fbfd4dd..0cd0c823ce 100644 --- a/src/patients/visits/AddVisitModal.tsx +++ b/src/patients/visits/AddVisitModal.tsx @@ -1,30 +1,35 @@ import { Modal } from '@hospitalrun/components' import { addMonths } from 'date-fns' import React, { useState, useEffect } from 'react' -import { useDispatch, useSelector } from 'react-redux' import useTranslator from '../../shared/hooks/useTranslator' -import Visit from '../../shared/model/Visit' -import { RootState } from '../../shared/store' -import { addVisit } from '../patient-slice' +import Visit, { VisitStatus } from '../../shared/model/Visit' +import useAddVisit, { RequestVisit } from '../hooks/useAddVisit' import VisitForm from './VisitForm' interface Props { show: boolean onCloseButtonClick: () => void + patientId: string } const initialVisitState = { startDateTime: new Date().toISOString(), endDateTime: addMonths(new Date(), 1).toISOString(), + updatedAt: '', + type: '', + status: '' as VisitStatus, + reason: '', + location: '', + rev: '', } -const AddVisitModal = (props: Props) => { - const { show, onCloseButtonClick } = props - const dispatch = useDispatch() +const AddVisitModal = ({ show, onCloseButtonClick, patientId }: Props) => { const { t } = useTranslator() - const { visitError, patient } = useSelector((state: RootState) => state.patient) - const [visit, setVisit] = useState(initialVisitState) + + const [mutate] = useAddVisit() + const [visit, setVisit] = useState(initialVisitState) + const [error, setError] = useState(undefined) useEffect(() => { setVisit(initialVisitState) @@ -33,16 +38,20 @@ const AddVisitModal = (props: Props) => { const onVisitChange = (newVisit: Partial) => { setVisit(newVisit as Visit) } - - const onSaveButtonClick = () => { - dispatch(addVisit(patient.id, visit as Visit)) - } - const onClose = () => { onCloseButtonClick() } - const body = + const onSaveButtonClick = async () => { + try { + await mutate({ patientId, visit }) + onClose() + } catch (e) { + setError(e) + } + } + + const body = return ( { - const { patient } = useSelector((root: RootState) => root.patient) +interface Props { + patientId: string +} +const ViewVisit = ({ patientId }: Props) => { const { visitId } = useParams() - const [visit, setVisit] = useState() - - useEffect(() => { - if (patient && visitId) { - const currentVisit = findLast(patient.visits, (c: Visit) => c.id === visitId) - setVisit(currentVisit) - } - }, [setVisit, visitId, patient]) + const { data: visit, status } = useVisit(patientId, visitId) - if (visit) { - return ( - <> -

    {visit?.reason}

    - - - ) + if (visit === undefined || status === 'loading') { + return } - return <> + + return ( + <> +

    {visit.reason}

    + + + ) } export default ViewVisit diff --git a/src/patients/visits/VisitForm.tsx b/src/patients/visits/VisitForm.tsx index d6b992d848..ebc38b2424 100644 --- a/src/patients/visits/VisitForm.tsx +++ b/src/patients/visits/VisitForm.tsx @@ -4,11 +4,12 @@ import React, { useState } from 'react' import DateTimePickerWithLabelFormGroup from '../../shared/components/input/DateTimePickerWithLabelFormGroup' import SelectWithLabelFormGroup, { Option, -} from '../../shared/components/input/SelectWithLableFormGroup' +} from '../../shared/components/input/SelectWithLabelFormGroup' import TextFieldWithLabelFormGroup from '../../shared/components/input/TextFieldWithLabelFormGroup' import TextInputWithLabelFormGroup from '../../shared/components/input/TextInputWithLabelFormGroup' import useTranslator from '../../shared/hooks/useTranslator' -import Visit, { VisitStatus } from '../../shared/model/Visit' +import { VisitStatus } from '../../shared/model/Visit' +import { RequestVisit } from '../hooks/useAddVisit' interface Error { message?: string @@ -20,9 +21,9 @@ interface Error { location?: string } interface Props { - visit: Partial + visit: RequestVisit visitError?: Error - onChange?: (newVisit: Partial) => void + onChange?: (newVisit: Partial) => void disabled?: boolean } @@ -139,6 +140,8 @@ const VisitForm = (props: Props) => { VisitForm.defaultProps = { disabled: false, + onChange: (newVisit: Partial) => newVisit, + visitError: {}, } export default VisitForm diff --git a/src/patients/visits/VisitTab.tsx b/src/patients/visits/VisitTab.tsx index 78a80c7ad1..824c6164d4 100644 --- a/src/patients/visits/VisitTab.tsx +++ b/src/patients/visits/VisitTab.tsx @@ -10,7 +10,11 @@ import AddVisitModal from './AddVisitModal' import ViewVisit from './ViewVisit' import VisitTable from './VisitTable' -const VisitTab = () => { +interface Props { + patientId: string +} + +const VisitTab = ({ patientId }: Props) => { const { t } = useTranslator() const { permissions } = useSelector((state: RootState) => state.user) const [showAddVisitModal, setShowAddVisitModal] = useState(false) @@ -34,14 +38,15 @@ const VisitTab = () => {
    - + - + setShowAddVisitModal(false)} /> diff --git a/src/patients/visits/VisitTable.tsx b/src/patients/visits/VisitTable.tsx index 6cecfee952..6a0c326e1b 100644 --- a/src/patients/visits/VisitTable.tsx +++ b/src/patients/visits/VisitTable.tsx @@ -1,21 +1,29 @@ import { Table } from '@hospitalrun/components' import format from 'date-fns/format' import React from 'react' -import { useSelector } from 'react-redux' import { useHistory } from 'react-router-dom' +import Loading from '../../shared/components/Loading' import useTranslator from '../../shared/hooks/useTranslator' -import { RootState } from '../../shared/store' +import usePatientVisits from '../hooks/usePatientVisits' -const VisitTable = () => { +interface Props { + patientId: string +} +const VisitTable = ({ patientId }: Props) => { const history = useHistory() const { t } = useTranslator() - const { patient } = useSelector((state: RootState) => state.patient) + + const { data: patientVisits, status } = usePatientVisits(patientId) + + if (patientVisits === undefined && status === 'loading') { + return + } return (
    row.id} - data={patient.visits || []} + data={patientVisits || []} columns={[ { label: t('patient.visits.startDateTime'), @@ -36,7 +44,7 @@ const VisitTable = () => { actions={[ { label: t('actions.view'), - action: (row) => history.push(`/patients/${patient.id}/visits/${row.id}`), + action: (row) => history.push(`/patients/${patientId}/visits/${row.id}`), }, ]} /> diff --git a/src/scheduling/appointments/AppointmentDetailForm.tsx b/src/scheduling/appointments/AppointmentDetailForm.tsx index 03c4c87570..8a323f5271 100644 --- a/src/scheduling/appointments/AppointmentDetailForm.tsx +++ b/src/scheduling/appointments/AppointmentDetailForm.tsx @@ -4,7 +4,7 @@ import React from 'react' import DateTimePickerWithLabelFormGroup from '../../shared/components/input/DateTimePickerWithLabelFormGroup' import SelectWithLabelFormGroup, { Option, -} from '../../shared/components/input/SelectWithLableFormGroup' +} from '../../shared/components/input/SelectWithLabelFormGroup' import TextFieldWithLabelFormGroup from '../../shared/components/input/TextFieldWithLabelFormGroup' import TextInputWithLabelFormGroup from '../../shared/components/input/TextInputWithLabelFormGroup' import PatientRepository from '../../shared/db/PatientRepository' diff --git a/src/scheduling/appointments/ViewAppointments.tsx b/src/scheduling/appointments/ViewAppointments.tsx index 8ae3114efd..0746b9e27a 100644 --- a/src/scheduling/appointments/ViewAppointments.tsx +++ b/src/scheduling/appointments/ViewAppointments.tsx @@ -5,7 +5,7 @@ import { useHistory } from 'react-router-dom' import useAddBreadcrumbs from '../../page-header/breadcrumbs/useAddBreadcrumbs' import { useButtonToolbarSetter } from '../../page-header/button-toolbar/ButtonBarProvider' -import useTitle from '../../page-header/title/useTitle' +import { useUpdateTitle } from '../../page-header/title/TitleContext' import PatientRepository from '../../shared/db/PatientRepository' import useTranslator from '../../shared/hooks/useTranslator' import { RootState } from '../../shared/store' @@ -24,7 +24,8 @@ const breadcrumbs = [{ i18nKey: 'scheduling.appointments.label', location: '/app const ViewAppointments = () => { const { t } = useTranslator() const history = useHistory() - useTitle(t('scheduling.appointments.label')) + const updateTitle = useUpdateTitle() + updateTitle(t('scheduling.appointments.label')) const dispatch = useDispatch() const { appointments } = useSelector((state: RootState) => state.appointments) const [events, setEvents] = useState([]) diff --git a/src/scheduling/appointments/appointments-slice.ts b/src/scheduling/appointments/appointments-slice.ts index edf21ece5c..df8564db52 100644 --- a/src/scheduling/appointments/appointments-slice.ts +++ b/src/scheduling/appointments/appointments-slice.ts @@ -1,7 +1,6 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' import AppointmentRepository from '../../shared/db/AppointmentRepository' -import PatientRepository from '../../shared/db/PatientRepository' import Appointment from '../../shared/model/Appointment' import { AppThunk } from '../../shared/store' @@ -39,12 +38,4 @@ export const fetchAppointments = (): AppThunk => async (dispatch) => { dispatch(fetchAppointmentsSuccess(appointments)) } -export const fetchPatientAppointments = (patientId: string): AppThunk => async (dispatch) => { - dispatch(fetchAppointmentsStart()) - - const appointments = await PatientRepository.getAppointments(patientId) - - dispatch(fetchAppointmentsSuccess(appointments)) -} - export default appointmentsSlice.reducer diff --git a/src/scheduling/appointments/edit/EditAppointment.tsx b/src/scheduling/appointments/edit/EditAppointment.tsx index 73e30bed85..7c4987ee9c 100644 --- a/src/scheduling/appointments/edit/EditAppointment.tsx +++ b/src/scheduling/appointments/edit/EditAppointment.tsx @@ -4,7 +4,7 @@ import { useDispatch, useSelector } from 'react-redux' import { useHistory, useParams } from 'react-router-dom' import useAddBreadcrumbs from '../../../page-header/breadcrumbs/useAddBreadcrumbs' -import useTitle from '../../../page-header/title/useTitle' +import { useUpdateTitle } from '../../../page-header/title/TitleContext' import useTranslator from '../../../shared/hooks/useTranslator' import Appointment from '../../../shared/model/Appointment' import { RootState } from '../../../shared/store' @@ -14,7 +14,8 @@ import { getAppointmentLabel } from '../util/scheduling-appointment.util' const EditAppointment = () => { const { t } = useTranslator() - useTitle(t('scheduling.appointments.editAppointment')) + const updateTitle = useUpdateTitle() + updateTitle(t('scheduling.appointments.editAppointment')) const history = useHistory() const dispatch = useDispatch() diff --git a/src/scheduling/appointments/new/NewAppointment.tsx b/src/scheduling/appointments/new/NewAppointment.tsx index b51fe91315..5af533c154 100644 --- a/src/scheduling/appointments/new/NewAppointment.tsx +++ b/src/scheduling/appointments/new/NewAppointment.tsx @@ -6,7 +6,7 @@ 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 { useUpdateTitle } from '../../../page-header/title/TitleContext' import useTranslator from '../../../shared/hooks/useTranslator' import Appointment from '../../../shared/model/Appointment' import { RootState } from '../../../shared/store' @@ -22,7 +22,8 @@ const NewAppointment = () => { const { t } = useTranslator() const history = useHistory() const dispatch = useDispatch() - useTitle(t('scheduling.appointments.new')) + const updateTitle = useUpdateTitle() + updateTitle(t('scheduling.appointments.new')) useAddBreadcrumbs(breadcrumbs, true) const startDateTime = roundToNearestMinutes(new Date(), { nearestTo: 15 }) const endDateTime = addMinutes(startDateTime, 60) diff --git a/src/scheduling/appointments/view/ViewAppointment.tsx b/src/scheduling/appointments/view/ViewAppointment.tsx index b40c336035..b8002eb819 100644 --- a/src/scheduling/appointments/view/ViewAppointment.tsx +++ b/src/scheduling/appointments/view/ViewAppointment.tsx @@ -5,7 +5,7 @@ import { useParams, useHistory } from 'react-router-dom' import useAddBreadcrumbs from '../../../page-header/breadcrumbs/useAddBreadcrumbs' import { useButtonToolbarSetter } from '../../../page-header/button-toolbar/ButtonBarProvider' -import useTitle from '../../../page-header/title/useTitle' +import { useUpdateTitle } from '../../../page-header/title/TitleContext' import useTranslator from '../../../shared/hooks/useTranslator' import Permissions from '../../../shared/model/Permissions' import { RootState } from '../../../shared/store' @@ -15,7 +15,8 @@ import { getAppointmentLabel } from '../util/scheduling-appointment.util' const ViewAppointment = () => { const { t } = useTranslator() - useTitle(t('scheduling.appointments.viewAppointment')) + const updateTitle = useUpdateTitle() + updateTitle(t('scheduling.appointments.viewAppointment')) const dispatch = useDispatch() const { id } = useParams() const history = useHistory() diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 0d31f89c69..a1cedee093 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -1,13 +1,14 @@ import { Row, Column } from '@hospitalrun/components' import React from 'react' -import useTitle from '../page-header/title/useTitle' +import { useUpdateTitle } from '../page-header/title/TitleContext' import LanguageSelector from '../shared/components/input/LanguageSelector' import useTranslator from '../shared/hooks/useTranslator' const Settings = () => { const { t } = useTranslator() - useTitle(t('settings.label')) + const updateTitle = useUpdateTitle() + updateTitle(t('settings.label')) return ( <> diff --git a/src/shared/components/Sidebar.tsx b/src/shared/components/Sidebar.tsx index 52bf477210..0ed2ca4aba 100644 --- a/src/shared/components/Sidebar.tsx +++ b/src/shared/components/Sidebar.tsx @@ -346,6 +346,17 @@ const Sidebar = () => { {!sidebarCollapsed && t('incidents.reports.label')} )} + {permissions.includes(Permissions.ViewIncidentWidgets) && ( + navigateTo('/incidents/visualize')} + active={splittedPath[1].includes('incidents') && splittedPath.length < 3} + > + + {!sidebarCollapsed && t('incidents.visualize.label')} + + )} )} @@ -403,7 +414,7 @@ const Sidebar = () => { return (