diff --git a/src/__tests__/hooks/debounce.spec.ts b/src/__tests__/hooks/debounce.spec.ts new file mode 100644 index 0000000000..73be39e3eb --- /dev/null +++ b/src/__tests__/hooks/debounce.spec.ts @@ -0,0 +1,46 @@ +import { renderHook, act } from '@testing-library/react-hooks' +import useDebounce from 'hooks/debounce' + +describe('useDebounce', () => { + beforeAll(() => jest.useFakeTimers()) + + afterAll(() => jest.useRealTimers()) + + it('should set the next value after the input value has not changed for the specified amount of time', () => { + const initialValue = 'initialValue' + const expectedValue = 'someValue' + const debounceDelay = 500 + + let currentValue = initialValue + + const { rerender, result } = renderHook(() => useDebounce(currentValue, debounceDelay)) + + currentValue = expectedValue + + act(() => { + rerender() + jest.advanceTimersByTime(debounceDelay) + }) + + expect(result.current).toBe(expectedValue) + }) + + it('should not set a new value before the specified delay has elapsed', () => { + const initialValue = 'initialValue' + const nextValue = 'someValue' + const debounceDelay = 500 + + let currentValue = initialValue + + const { rerender, result } = renderHook(() => useDebounce(currentValue, debounceDelay)) + + currentValue = nextValue + + act(() => { + rerender() + jest.advanceTimersByTime(debounceDelay - 1) + }) + + expect(result.current).toBe(initialValue) + }) +}) diff --git a/src/__tests__/patients/list/Patients.test.tsx b/src/__tests__/patients/list/Patients.test.tsx index bfce7907c7..56cb7a3e21 100644 --- a/src/__tests__/patients/list/Patients.test.tsx +++ b/src/__tests__/patients/list/Patients.test.tsx @@ -1,7 +1,7 @@ import '../../../__mocks__/matchMediaMock' import React from 'react' import { mount } from 'enzyme' -import { TextInput, Button, Spinner } from '@hospitalrun/components' +import { TextInput, Spinner } from '@hospitalrun/components' import { MemoryRouter } from 'react-router-dom' import { Provider } from 'react-redux' import thunk from 'redux-thunk' @@ -58,15 +58,6 @@ describe('Patients', () => { jest.restoreAllMocks() }) - it('should render a search input with button', () => { - const wrapper = setup() - const searchInput = wrapper.find(TextInput) - const searchButton = wrapper.find(Button) - expect(searchInput).toHaveLength(1) - expect(searchInput.prop('placeholder')).toEqual('actions.search') - expect(searchButton.text().trim()).toEqual('actions.search') - }) - it('should render a loading bar if it is loading', () => { const wrapper = setup(true) @@ -111,10 +102,15 @@ describe('Patients', () => { }) describe('search functionality', () => { - it('should call the searchPatients() action with the correct data', () => { + beforeEach(() => jest.useFakeTimers()) + + afterEach(() => jest.useRealTimers()) + + it('should search for patients after the search text has not changed for 500 milliseconds', () => { const searchPatientsSpy = jest.spyOn(patientSlice, 'searchPatients') - const expectedSearchText = 'search text' const wrapper = setup() + searchPatientsSpy.mockClear() + const expectedSearchText = 'search text' act(() => { ;(wrapper.find(TextInput).prop('onChange') as any)({ @@ -127,14 +123,8 @@ describe('Patients', () => { } as React.ChangeEvent) }) - wrapper.update() - act(() => { - ;(wrapper.find(Button).prop('onClick') as any)({ - preventDefault(): void { - // noop - }, - } as React.MouseEvent) + jest.advanceTimersByTime(500) }) wrapper.update() diff --git a/src/hooks/debounce.ts b/src/hooks/debounce.ts new file mode 100644 index 0000000000..2d80517c0b --- /dev/null +++ b/src/hooks/debounce.ts @@ -0,0 +1,13 @@ +import { useState, useEffect } from 'react' + +export default function (value: T, delayInMilliseconds: number): T { + const [debouncedValue, setDebouncedValue] = useState(value) + + useEffect(() => { + const debounceHandler = setTimeout(() => setDebouncedValue(value), delayInMilliseconds) + + return () => clearTimeout(debounceHandler) + }, [value, delayInMilliseconds]) + + return debouncedValue +} diff --git a/src/patients/list/Patients.tsx b/src/patients/list/Patients.tsx index 9cdef6b619..a56f374321 100644 --- a/src/patients/list/Patients.tsx +++ b/src/patients/list/Patients.tsx @@ -9,6 +9,7 @@ import { RootState } from '../../store' import { fetchPatients, searchPatients } from '../patients-slice' import useTitle from '../../page-header/useTitle' import useAddBreadcrumbs from '../../breadcrumbs/useAddBreadcrumbs' +import useDebounce from '../../hooks/debounce' const breadcrumbs = [{ i18nKey: 'patients.label', location: '/patients' }] @@ -35,6 +36,12 @@ const Patients = () => { const [searchText, setSearchText] = useState('') + const debouncedSearchText = useDebounce(searchText, 500) + + useEffect(() => { + dispatch(searchPatients(debouncedSearchText)) + }, [dispatch, debouncedSearchText]) + useEffect(() => { dispatch(fetchPatients()) @@ -43,9 +50,21 @@ const Patients = () => { } }, [dispatch, setButtonToolBar]) - if (isLoading) { - return - } + const loadingIndicator = + + const listBody = ( + + {patients.map((p) => ( + history.push(`/patients/${p.id}`)}> + {p.code} + {p.givenName} + {p.familyName} + {p.sex} + {p.dateOfBirth ? format(new Date(p.dateOfBirth), 'yyyy-MM-dd') : ''} + + ))} + + ) const list = ( @@ -58,17 +77,7 @@ const Patients = () => { - - {patients.map((p) => ( - history.push(`/patients/${p.id}`)}> - - - - - - - ))} - + {isLoading ? loadingIndicator : listBody}
{t('patient.dateOfBirth')}
{p.code}{p.givenName}{p.familyName}{p.sex}{p.dateOfBirth ? format(new Date(p.dateOfBirth), 'yyyy-MM-dd') : ''}
) @@ -76,31 +85,19 @@ const Patients = () => { setSearchText(event.target.value) } - const onSearchFormSubmit = (event: React.FormEvent | React.MouseEvent) => { - event.preventDefault() - dispatch(searchPatients(searchText)) - } - return ( -
- - - - - - - - -
+ + + + + {list}