-
Notifications
You must be signed in to change notification settings - Fork 95
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
The hook can be used to run a function after a specific amount of time for example for doing a reload of data.
- Loading branch information
1 parent
43053c5
commit a5a0c42
Showing
2 changed files
with
248 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
/* SPDX-FileCopyrightText: 2024 Greenbone AG | ||
* | ||
* SPDX-License-Identifier: AGPL-3.0-or-later | ||
*/ | ||
|
||
/* eslint-disable react/prop-types */ | ||
|
||
import {useState} from 'react'; | ||
|
||
import {describe, test, expect, testing} from '@gsa/testing'; | ||
|
||
import {act, fireEvent, render, screen} from 'web/utils/testing'; | ||
|
||
import useTiming from '../useTiming'; | ||
|
||
const TestComponent = ({doFunc}) => { | ||
const [value, setValue] = useState(0); | ||
const timingFunc = () => { | ||
setValue(value => value + 1); | ||
return doFunc(); | ||
}; | ||
const [startTimer, clearTimer, isRunning] = useTiming(timingFunc, 900); | ||
return ( | ||
<> | ||
<button onClick={startTimer} data-testid="startTimer"></button> | ||
<button onClick={clearTimer} data-testid="clearTimer"></button> | ||
<span data-testid="value">{value}</span> | ||
<span data-testid="isRunning">{'' + isRunning}</span> | ||
</> | ||
); | ||
}; | ||
|
||
const runTimers = async () => { | ||
await act(async () => { | ||
await testing.advanceTimersToNextTimerAsync(); | ||
}); | ||
}; | ||
|
||
describe('useTiming', () => { | ||
test('should start a timer', async () => { | ||
testing.useFakeTimers(); | ||
const doFunc = testing.fn().mockImplementation(() => {}); | ||
|
||
render(<TestComponent doFunc={doFunc} />); | ||
|
||
const value = screen.getByTestId('value'); | ||
const isRunning = screen.getByTestId('isRunning'); | ||
|
||
expect(value).toHaveTextContent('0'); | ||
expect(isRunning).toHaveTextContent('false'); | ||
|
||
fireEvent.click(screen.getByTestId('startTimer')); | ||
|
||
expect(value).toHaveTextContent('0'); | ||
expect(isRunning).toHaveTextContent('true'); | ||
|
||
await runTimers(); | ||
|
||
expect(screen.getByTestId('value')).toHaveTextContent('1'); | ||
expect(screen.getByTestId('isRunning')).toHaveTextContent('false'); | ||
}); | ||
|
||
test('should keep running a timer when a promise is used', async () => { | ||
testing.useFakeTimers(); | ||
const doFunc = testing.fn().mockResolvedValueOnce(); | ||
|
||
render(<TestComponent doFunc={doFunc} />); | ||
|
||
const value = screen.getByTestId('value'); | ||
const isRunning = screen.getByTestId('isRunning'); | ||
|
||
expect(value).toHaveTextContent('0'); | ||
expect(isRunning).toHaveTextContent('false'); | ||
|
||
fireEvent.click(screen.getByTestId('startTimer')); | ||
|
||
expect(value).toHaveTextContent('0'); | ||
expect(isRunning).toHaveTextContent('true'); | ||
|
||
await runTimers(); | ||
|
||
expect(screen.getByTestId('value')).toHaveTextContent('1'); | ||
expect(screen.getByTestId('isRunning')).toHaveTextContent('true'); | ||
|
||
await runTimers(); | ||
|
||
expect(screen.getByTestId('value')).toHaveTextContent('2'); | ||
expect(screen.getByTestId('isRunning')).toHaveTextContent('false'); | ||
}); | ||
|
||
test('should not rerun timer when a promise fails', async () => { | ||
testing.useFakeTimers(); | ||
const doFunc = testing.fn().mockRejectedValue(); | ||
|
||
render(<TestComponent doFunc={doFunc} />); | ||
|
||
const value = screen.getByTestId('value'); | ||
const isRunning = screen.getByTestId('isRunning'); | ||
|
||
expect(value).toHaveTextContent('0'); | ||
expect(isRunning).toHaveTextContent('false'); | ||
|
||
fireEvent.click(screen.getByTestId('startTimer')); | ||
|
||
expect(value).toHaveTextContent('0'); | ||
expect(isRunning).toHaveTextContent('true'); | ||
|
||
await runTimers(); | ||
|
||
expect(screen.getByTestId('value')).toHaveTextContent('1'); | ||
expect(screen.getByTestId('isRunning')).toHaveTextContent('false'); | ||
}); | ||
|
||
test('should allow to clear the timer', async () => { | ||
testing.useFakeTimers(); | ||
const doFunc = testing.fn().mockResolvedValue(); | ||
|
||
render(<TestComponent doFunc={doFunc} />); | ||
|
||
const value = screen.getByTestId('value'); | ||
const isRunning = screen.getByTestId('isRunning'); | ||
|
||
expect(value).toHaveTextContent('0'); | ||
expect(isRunning).toHaveTextContent('false'); | ||
|
||
fireEvent.click(screen.getByTestId('startTimer')); | ||
|
||
expect(value).toHaveTextContent('0'); | ||
expect(isRunning).toHaveTextContent('true'); | ||
|
||
await runTimers(); | ||
|
||
expect(screen.getByTestId('value')).toHaveTextContent('1'); | ||
expect(screen.getByTestId('isRunning')).toHaveTextContent('true'); | ||
|
||
fireEvent.click(screen.getByTestId('clearTimer')); | ||
|
||
expect(screen.getByTestId('value')).toHaveTextContent('1'); | ||
expect(screen.getByTestId('isRunning')).toHaveTextContent('false'); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
/* SPDX-FileCopyrightText: 2024 Greenbone AG | ||
* | ||
* SPDX-License-Identifier: AGPL-3.0-or-later | ||
*/ | ||
|
||
import {useEffect, useCallback, useState} from 'react'; | ||
|
||
import logger from 'gmp/log'; | ||
|
||
import {hasValue, isFunction} from 'gmp/utils/identity'; | ||
|
||
import useInstanceVariable from './useInstanceVariable'; | ||
|
||
const log = logger.getLogger('web.hooks.useTiming'); | ||
|
||
/** | ||
* Hook to start a timer that calls a function after a given timeout | ||
* | ||
* @param {Function} doFunc The function to call | ||
* @param {Number|Function} timeout The timeout in milliseconds or a function that returns the timeout | ||
* @returns Array of startTimer function, clearTimer function and boolean isRunning | ||
*/ | ||
const useTiming = (doFunc, timeout) => { | ||
const timer = useInstanceVariable({}); | ||
const [timerId, setTimerId] = useState(); // store timerId in state too to trigger re-render if it changes | ||
const isRunning = Boolean(timerId); | ||
timer.doFunc = doFunc; // always use the latest version of the function | ||
|
||
const updateTimerId = useCallback( | ||
newTimerId => { | ||
timer.timerId = newTimerId; | ||
setTimerId(newTimerId); | ||
}, | ||
[timer], | ||
); | ||
|
||
const startTimer = useCallback(() => { | ||
if (hasValue(timer.timerId)) { | ||
log.debug('Not starting timer. A timer is already running.', { | ||
timer: timer.timerId, | ||
}); | ||
return; | ||
} | ||
|
||
const timeoutValue = isFunction(timeout) ? timeout() : timeout; | ||
|
||
if (!hasValue(timeoutValue) || timeoutValue < 0) { | ||
log.debug('Not starting timer because timeout value was', timeoutValue); | ||
return; | ||
} | ||
|
||
updateTimerId( | ||
setTimeout(async () => { | ||
log.debug('Timer with id', timer.timerId, 'fired.'); | ||
|
||
const promise = timer.doFunc(); | ||
try { | ||
if (promise?.then) { | ||
await promise; | ||
updateTimerId(); | ||
timer.startTimer(); | ||
} else { | ||
updateTimerId(); | ||
} | ||
} catch (error) { | ||
updateTimerId(); | ||
} | ||
}, timeoutValue), | ||
); | ||
|
||
log.debug( | ||
'Started timer with id', | ||
timer.timerId, | ||
'and timeout of', | ||
timeoutValue, | ||
'milliseconds', | ||
); | ||
}, [timeout, timer, updateTimerId]); | ||
|
||
const clearTimer = useCallback(() => { | ||
if (hasValue(timer.timerId)) { | ||
log.debug('Clearing timer with id', timer.timerId); | ||
|
||
clearTimeout(timer.timerId); | ||
updateTimerId(); | ||
} | ||
}, [timer, updateTimerId]); | ||
|
||
useEffect(() => { | ||
// put starTimer func into timer ref to allow referencing the NEWEST version | ||
// when a promise has ended | ||
timer.startTimer = startTimer; | ||
}); | ||
|
||
// clear timer on unmount | ||
useEffect( | ||
() => () => { | ||
log.debug('Removing timer on unmount'); | ||
clearTimer(); | ||
}, | ||
[clearTimer], | ||
); | ||
|
||
return [startTimer, clearTimer, isRunning]; | ||
}; | ||
|
||
export default useTiming; |