diff --git a/src/web/hooks/__tests__/useReload.jsx b/src/web/hooks/__tests__/useReload.jsx new file mode 100644 index 0000000000..e3a33e64eb --- /dev/null +++ b/src/web/hooks/__tests__/useReload.jsx @@ -0,0 +1,188 @@ +/* SPDX-FileCopyrightText: 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +/* eslint-disable react/prop-types */ + +import {describe, test, expect, testing} from '@gsa/testing'; + +import {act, fireEvent, render, screen} from 'web/utils/testing'; + +import useReload from '../useReload'; + +const TestComponent = ({reload, timeout}) => { + const [startTimer, clearTimer, isRunning] = useReload(reload, timeout); + return ( + <> + + + {'' + isRunning} + + ); +}; + +const runTimers = async () => { + await act(async () => { + await testing.advanceTimersToNextTimerAsync(); + }); +}; + +describe('useTiming', () => { + test('should start a timer to reload', async () => { + testing.useFakeTimers(); + + const reload = testing.fn(); + const timeout = testing.fn().mockImplementation(() => 900); + + render(); + + const isRunning = screen.getByTestId('isRunning'); + + expect(isRunning).toHaveTextContent('false'); + expect(reload).not.toHaveBeenCalled(); + expect(timeout).not.toHaveBeenCalled(); + + fireEvent.click(screen.getByTestId('startTimer')); + + expect(isRunning).toHaveTextContent('true'); + expect(reload).not.toHaveBeenCalled(); + expect(timeout).toHaveBeenCalledWith({isVisible: true}); + + timeout.mockClear(); + + await runTimers(); + + expect(isRunning).toHaveTextContent('false'); + expect(reload).toHaveBeenCalled(); + expect(timeout).not.toHaveBeenCalled(); + }); + + test('should reload forever', async () => { + testing.useFakeTimers(); + + const reload = testing.fn().mockResolvedValue(); + const timeout = testing.fn().mockImplementation(() => 900); + + render(); + + const isRunning = screen.getByTestId('isRunning'); + + expect(isRunning).toHaveTextContent('false'); + expect(reload).not.toHaveBeenCalled(); + expect(timeout).not.toHaveBeenCalled(); + + fireEvent.click(screen.getByTestId('startTimer')); + + expect(isRunning).toHaveTextContent('true'); + expect(reload).not.toHaveBeenCalled(); + expect(timeout).toHaveBeenCalledWith({isVisible: true}); + + timeout.mockClear(); + + await runTimers(); + + expect(isRunning).toHaveTextContent('true'); + expect(reload).toHaveBeenCalled(); + expect(timeout).toHaveBeenCalledWith({isVisible: true}); + + timeout.mockClear(); + reload.mockClear(); + + await runTimers(); + + expect(isRunning).toHaveTextContent('true'); + expect(reload).toHaveBeenCalled(); + expect(timeout).toHaveBeenCalledWith({isVisible: true}); + }); + + test('should not reload if loading fails', async () => { + testing.useFakeTimers(); + + const reload = testing.fn().mockRejectedValue(); + const timeout = testing.fn().mockImplementation(() => 900); + + render(); + + const isRunning = screen.getByTestId('isRunning'); + + expect(isRunning).toHaveTextContent('false'); + expect(reload).not.toHaveBeenCalled(); + expect(timeout).not.toHaveBeenCalled(); + + fireEvent.click(screen.getByTestId('startTimer')); + + expect(isRunning).toHaveTextContent('true'); + expect(reload).not.toHaveBeenCalled(); + expect(timeout).toHaveBeenCalledWith({isVisible: true}); + + timeout.mockClear(); + + await runTimers(); + + expect(isRunning).toHaveTextContent('false'); + expect(reload).toHaveBeenCalled(); + expect(timeout).not.toHaveBeenCalled(); + }); + + test('should allow to cancel reload', async () => { + testing.useFakeTimers(); + + const reload = testing.fn().mockResolvedValue(); + const timeout = testing.fn().mockImplementation(() => 900); + + render(); + + const isRunning = screen.getByTestId('isRunning'); + + expect(isRunning).toHaveTextContent('false'); + expect(reload).not.toHaveBeenCalled(); + expect(timeout).not.toHaveBeenCalled(); + + fireEvent.click(screen.getByTestId('startTimer')); + + expect(isRunning).toHaveTextContent('true'); + expect(reload).not.toHaveBeenCalled(); + expect(timeout).toHaveBeenCalledWith({isVisible: true}); + + timeout.mockClear(); + + await runTimers(); + + expect(isRunning).toHaveTextContent('true'); + expect(reload).toHaveBeenCalled(); + expect(timeout).toHaveBeenCalledWith({isVisible: true}); + + timeout.mockClear(); + reload.mockClear(); + + fireEvent.click(screen.getByTestId('clearTimer')); + + await runTimers(); + + expect(isRunning).toHaveTextContent('false'); + expect(reload).not.toHaveBeenCalled(); + expect(timeout).not.toHaveBeenCalled(); + }); + + test('should not start reload reload timer', async () => { + testing.useFakeTimers(); + + const reload = testing.fn(); + const timeout = testing.fn(); + + render(); + + const isRunning = screen.getByTestId('isRunning'); + + expect(isRunning).toHaveTextContent('false'); + expect(reload).not.toHaveBeenCalled(); + expect(timeout).not.toHaveBeenCalled(); + + fireEvent.click(screen.getByTestId('startTimer')); + + expect(isRunning).toHaveTextContent('false'); + expect(reload).not.toHaveBeenCalled(); + expect(timeout).toHaveBeenCalledWith({isVisible: true}); + }); +}); diff --git a/src/web/hooks/useReload.js b/src/web/hooks/useReload.js new file mode 100644 index 0000000000..c950401ec6 --- /dev/null +++ b/src/web/hooks/useReload.js @@ -0,0 +1,47 @@ +/* SPDX-FileCopyrightText: 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import {useCallback, useEffect} from 'react'; + +import useTiming from 'web/hooks/useTiming'; + +/** + * A hook to reload data considering the visibility change of the browser tab. + * + * @param {Function} reloadFunc Function to call when the timer fires + * @param {Function} timeoutFunc Function to get the timeout value from + * @returns Array of startTimer function, clearTimer function and boolean isRunning + */ +const useReload = (reloadFunc, timeoutFunc) => { + const timeout = useCallback( + () => timeoutFunc({isVisible: !document.hidden}), + [timeoutFunc], + ); + + const [startTimer, clearTimer, isRunning] = useTiming(reloadFunc, timeout); + + const handleVisibilityChange = useCallback(() => { + const isVisible = !document.hidden; + + if (isVisible) { + // browser tab is visible again + // restart timer to get a possible shorter interval as the remaining time + + clearTimer(); + startTimer(); + } + }, [clearTimer, startTimer]); + + useEffect(() => { + document.addEventListener('visibilitychange', handleVisibilityChange); + return () => { + document.removeEventListener('visibilitychange', handleVisibilityChange); + }; + }, [handleVisibilityChange]); + + return [startTimer, clearTimer, isRunning]; +}; + +export default useReload;