Skip to content

Commit

Permalink
Add: Add a useReload hook to make the Reload component obsolete
Browse files Browse the repository at this point in the history
The new useReload hook is based in useTiming. It calls a timing
function before every reload to calculate the timeout before the reload.
This timing function gets a isVisible argument passed. Using the
argument the timing function can decide to extend the timeout when the
current browser window is not visible.
  • Loading branch information
bjoernricks committed Jun 13, 2024
1 parent 4ef0714 commit 2bf49dc
Show file tree
Hide file tree
Showing 2 changed files with 235 additions and 0 deletions.
188 changes: 188 additions & 0 deletions src/web/hooks/__tests__/useReload.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<button onClick={startTimer} data-testid="startTimer"></button>
<button onClick={clearTimer} data-testid="clearTimer"></button>
<span data-testid="isRunning">{'' + isRunning}</span>
</>
);
};

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(<TestComponent reload={reload} timeout={timeout} />);

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(<TestComponent reload={reload} timeout={timeout} />);

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(<TestComponent reload={reload} timeout={timeout} />);

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(<TestComponent reload={reload} timeout={timeout} />);

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(<TestComponent reload={reload} timeout={timeout} />);

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});
});
});
47 changes: 47 additions & 0 deletions src/web/hooks/useReload.js
Original file line number Diff line number Diff line change
@@ -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;

0 comments on commit 2bf49dc

Please sign in to comment.