From 9e5f7308e29023ce26c83b4f69a956404bfdf9fe Mon Sep 17 00:00:00 2001 From: Nick Phura Date: Mon, 1 Nov 2021 09:21:40 -0700 Subject: [PATCH] Enhance useInterval hook to support a timeout. Add unit tests. --- .../surveys/view/SurveyObservations.tsx | 4 +- app/src/hooks/useInterval.test.tsx | 101 ++++++++++++++++++ app/src/hooks/useInterval.ts | 37 +++++-- 3 files changed, 131 insertions(+), 11 deletions(-) create mode 100644 app/src/hooks/useInterval.test.tsx diff --git a/app/src/features/surveys/view/SurveyObservations.tsx b/app/src/features/surveys/view/SurveyObservations.tsx index 9e0b851a30..005cb66c7e 100644 --- a/app/src/features/surveys/view/SurveyObservations.tsx +++ b/app/src/features/surveys/view/SurveyObservations.tsx @@ -133,7 +133,7 @@ const SurveyObservations: React.FC = (props) => { }); }, [biohubApi.observation, projectId, surveyId]); - useInterval(fetchObservationSubmission, pollingTime); + useInterval(fetchObservationSubmission, pollingTime, 60000); useEffect(() => { if (isLoading) { @@ -141,7 +141,7 @@ const SurveyObservations: React.FC = (props) => { } if (isPolling && !pollingTime) { - setPollingTime(2000); + setPollingTime(5000); } }, [ biohubApi, diff --git a/app/src/hooks/useInterval.test.tsx b/app/src/hooks/useInterval.test.tsx new file mode 100644 index 0000000000..aafb5ed090 --- /dev/null +++ b/app/src/hooks/useInterval.test.tsx @@ -0,0 +1,101 @@ +import { render } from '@testing-library/react'; +import React from 'react'; +import { useInterval } from './useInterval'; + +interface ITestComponentProps { + callback: Function | null | undefined; + period: number | null | undefined; + timeout: number; +} + +const TestComponent: React.FC = (props) => { + useInterval(props.callback, props.period, props.timeout); + + return <>; +}; + +describe('useInterval', () => { + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + it('calls the callback 5 times: once every 50 milliseconds for 250 milliseconds', async () => { + const callbackMock = jest.fn(); + + render(); + + expect(callbackMock.mock.calls.length).toEqual(0); // 0 milliseconds + + jest.advanceTimersByTime(49); + expect(callbackMock.mock.calls.length).toEqual(0); // 49 milliseconds + + jest.advanceTimersByTime(1); + expect(callbackMock.mock.calls.length).toEqual(1); // 50 milliseconds + + jest.advanceTimersByTime(49); + expect(callbackMock.mock.calls.length).toEqual(1); // 99 milliseconds + + jest.advanceTimersByTime(1); + expect(callbackMock.mock.calls.length).toEqual(2); // 100 milliseconds + + jest.advanceTimersByTime(50); + expect(callbackMock.mock.calls.length).toEqual(3); // 150 milliseconds + + jest.advanceTimersByTime(850); + expect(callbackMock.mock.calls.length).toEqual(5); // 1000 milliseconds + }); + + it('stops calling the callback if the callback is updated to be falsy', async () => { + const callbackMock = jest.fn(); + + const { rerender } = render(); + + expect(callbackMock.mock.calls.length).toEqual(0); // 0 milliseconds + + jest.advanceTimersByTime(49); + expect(callbackMock.mock.calls.length).toEqual(0); // 49 milliseconds + + jest.advanceTimersByTime(1); + expect(callbackMock.mock.calls.length).toEqual(1); // 50 milliseconds + + jest.advanceTimersByTime(49); + expect(callbackMock.mock.calls.length).toEqual(1); // 99 milliseconds + + jest.advanceTimersByTime(1); + expect(callbackMock.mock.calls.length).toEqual(2); // 100 milliseconds + + rerender(); + + jest.advanceTimersByTime(900); + expect(callbackMock.mock.calls.length).toEqual(2); // 1000 milliseconds + }); + + it('stops calling the callback if the period is updated to be falsy', async () => { + const callbackMock = jest.fn(); + + const { rerender } = render(); + + expect(callbackMock.mock.calls.length).toEqual(0); // 0 milliseconds + + jest.advanceTimersByTime(49); + expect(callbackMock.mock.calls.length).toEqual(0); // 49 milliseconds + + jest.advanceTimersByTime(1); + expect(callbackMock.mock.calls.length).toEqual(1); // 50 milliseconds + + jest.advanceTimersByTime(49); + expect(callbackMock.mock.calls.length).toEqual(1); // 99 milliseconds + + jest.advanceTimersByTime(1); + expect(callbackMock.mock.calls.length).toEqual(2); // 100 milliseconds + + rerender(); + + jest.advanceTimersByTime(900); + expect(callbackMock.mock.calls.length).toEqual(2); // 1000 milliseconds + }); +}); diff --git a/app/src/hooks/useInterval.ts b/app/src/hooks/useInterval.ts index d0deb784ac..d492beb77f 100644 --- a/app/src/hooks/useInterval.ts +++ b/app/src/hooks/useInterval.ts @@ -1,18 +1,25 @@ import { useRef, useEffect } from 'react'; /** - * Runs a `callback` function on a timer, once every `delay` milliseconds. + * Runs a `callback` function on a timer, once every `period` milliseconds. * - * Note: Does nothing if either `callback` or `delay` are null/undefined/falsy. + * Note: Does nothing if either `callback` or `period` are null/undefined/falsy. * - * Note: If both `callback` and `delay` are valid, the `callback` function will run for the first time after `delay` + * Note: If both `callback` and `period` are valid, the `callback` function will run for the first time after `period` * milliseconds (it will not run at time=0). * * @param {(Function | null | undefined)} callback the function to run at each interval. Set to a falsy value to stop * the interval. - * @param {(number | null | undefined)} delay timer delay in milliseconds. Set to a falsy value to stop the interval. + * @param {(number | null | undefined)} period interval period in milliseconds. How often the `callback` should run. + * Set to a falsy value to stop the interval. + * @param {(number)} [timeout] timeout in milliseconds. The total polling time before the interval times out and + * automatically stops. */ -export const useInterval = (callback: Function | null | undefined, delay: number | null | undefined): void => { +export const useInterval = ( + callback: Function | null | undefined, + period: number | null | undefined, + timeout?: number +): void => { const savedCallback = useRef(callback); useEffect(() => { @@ -20,12 +27,24 @@ export const useInterval = (callback: Function | null | undefined, delay: number }, [callback]); useEffect(() => { - if (!delay || !savedCallback?.current) { + if (!period || !savedCallback?.current) { return; } - const timeout = setInterval(() => savedCallback?.current?.(), delay); + const interval = setInterval(() => savedCallback?.current?.(), period); - return () => clearInterval(timeout); - }, [delay]); + let intervalTimeout: NodeJS.Timeout | undefined; + + if (timeout) { + intervalTimeout = setTimeout(() => clearInterval(interval), timeout); + } + + return () => { + clearInterval(interval); + + if (intervalTimeout) { + clearTimeout(intervalTimeout); + } + }; + }, [period, timeout]); };