Skip to content

Commit 6561e5a

Browse files
fix: Multiple API calls in progress update modal (#1364)
* fix: added debounce util function & unit test * fix: trigger update onChange event * fix: added test cases * fix: addressed not comments * fix: failing test * fix: failing test * fix: removed unnecessary dependency from the callback * fix: failing test * fix: bug in non-dev * chore: refactored * chore: refactored more blocks * chore: add todo comments --------- Co-authored-by: Amit Prakash <[email protected]>
1 parent e25bb66 commit 6561e5a

File tree

5 files changed

+198
-18
lines changed

5 files changed

+198
-18
lines changed

__tests__/Unit/Components/Tasks/ProgressContainer.test.tsx

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,4 +319,70 @@ describe('ProgressContainer', () => {
319319
).not.toBeInTheDocument();
320320
});
321321
});
322+
323+
test('should debounce and make only one fetch call in dev mode', async () => {
324+
const fetchSpy = jest.spyOn(window, 'fetch');
325+
server.use(superUserSelfHandler);
326+
renderWithRouter(
327+
<Provider store={store()}>
328+
<ProgressContainer content={CONTENT[0]} />
329+
</Provider>,
330+
{
331+
query: { dev: 'true' },
332+
}
333+
);
334+
335+
const updateButton = await screen.findByTestId(
336+
'progress-update-text-dev'
337+
);
338+
expect(updateButton).toBeInTheDocument();
339+
340+
fireEvent.click(updateButton);
341+
const sliderInput = screen.getByRole('slider');
342+
343+
for (let i = 0; i < 3; i++) {
344+
fireEvent.change(sliderInput, { target: { value: 50 + i * 10 } });
345+
}
346+
347+
await waitFor(() => {
348+
expect(fetchSpy).toBeCalledTimes(1);
349+
});
350+
351+
fetchSpy.mockRestore();
352+
});
353+
354+
//TODO: Remove this test case while removing feature flag (ISSUE: #1366)
355+
test('should not debounce and make multiple fetch calls in non-dev mode', async () => {
356+
const fetchSpy = jest.spyOn(window, 'fetch');
357+
server.use(superUserSelfHandler);
358+
renderWithRouter(
359+
<Provider store={store()}>
360+
<ProgressContainer content={CONTENT[0]} />
361+
<ToastContainer />
362+
</Provider>
363+
);
364+
365+
const updateButton = await screen.findByTestId(
366+
'progress-update-text-dev'
367+
);
368+
expect(updateButton).toBeInTheDocument();
369+
370+
fireEvent.click(updateButton);
371+
const sliderInput = screen.getByRole('slider');
372+
373+
for (let i = 0; i < 3; i++) {
374+
fireEvent.change(sliderInput, { target: { value: 50 + i * 10 } });
375+
fireEvent.mouseUp(sliderInput);
376+
}
377+
378+
await waitFor(
379+
() => {
380+
expect(fetchSpy).toBeCalledTimes(3);
381+
},
382+
{
383+
timeout: 1050,
384+
}
385+
);
386+
fetchSpy.mockRestore();
387+
});
322388
});

__tests__/Utils/debounce.test.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { debounce } from '@/utils/common';
2+
3+
describe('debounce', () => {
4+
beforeEach(() => {
5+
jest.useFakeTimers();
6+
});
7+
8+
afterEach(() => {
9+
jest.useRealTimers();
10+
});
11+
12+
test('should call the callback after the specified delay', () => {
13+
const mockCallback = jest.fn();
14+
const debouncedFn = debounce(mockCallback, 1000);
15+
16+
debouncedFn('test');
17+
18+
expect(mockCallback).not.toHaveBeenCalled();
19+
20+
jest.advanceTimersByTime(1000);
21+
22+
expect(mockCallback).toHaveBeenCalledTimes(1);
23+
expect(mockCallback).toHaveBeenCalledWith('test');
24+
});
25+
26+
test('should reset the timer when called multiple times', () => {
27+
const mockCallback = jest.fn();
28+
const debouncedFn = debounce(mockCallback, 1000);
29+
30+
debouncedFn('first');
31+
jest.advanceTimersByTime(500);
32+
33+
debouncedFn('second');
34+
jest.advanceTimersByTime(500);
35+
36+
expect(mockCallback).not.toHaveBeenCalled();
37+
38+
jest.advanceTimersByTime(500);
39+
40+
expect(mockCallback).toHaveBeenCalledTimes(1);
41+
expect(mockCallback).toHaveBeenCalledWith('second');
42+
});
43+
44+
test('should pass multiple arguments correctly', () => {
45+
const mockCallback = jest.fn();
46+
const debouncedFn = debounce(mockCallback, 1000);
47+
48+
debouncedFn('arg1', 'arg2', 123);
49+
50+
jest.advanceTimersByTime(1000);
51+
52+
expect(mockCallback).toHaveBeenCalledWith('arg1', 'arg2', 123);
53+
});
54+
55+
test('should handle multiple rapid calls and only execute the last one', () => {
56+
const mockCallback = jest.fn();
57+
const debouncedFn = debounce(mockCallback, 1000);
58+
59+
debouncedFn('call1');
60+
debouncedFn('call2');
61+
debouncedFn('call3');
62+
debouncedFn('call4');
63+
64+
jest.advanceTimersByTime(1000);
65+
66+
expect(mockCallback).toHaveBeenCalledTimes(1);
67+
expect(mockCallback).toHaveBeenCalledWith('call4');
68+
});
69+
});

src/components/tasks/card/progressContainer/ProgressSlider.tsx

Lines changed: 41 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,55 @@ import { FC } from 'react';
22

33
import { ProgressSliderProps } from '@/interfaces/task.type';
44
import styles from '@/components/tasks/card/card.module.scss';
5+
import { useRouter } from 'next/router';
56

67
const ProgressSlider: FC<ProgressSliderProps> = ({
78
value,
89
debounceSlider,
910
handleProgressChange,
1011
isLoading,
1112
}) => {
13+
const router = useRouter();
14+
const isDev = router.query.dev === 'true';
1215
return (
13-
<input
14-
className={styles.slider}
15-
type="range"
16-
value={value}
17-
min="0"
18-
max="100"
19-
step="10"
20-
onChange={(e) => handleProgressChange(e)}
21-
onTouchEnd={() => debounceSlider(1000)}
22-
onMouseUp={() => debounceSlider(1000)}
23-
disabled={isLoading}
24-
style={
25-
{
26-
'--progress': `${value}%`,
27-
} as React.CSSProperties
28-
}
29-
/>
16+
<>
17+
{isDev ? (
18+
<input
19+
className={styles.slider}
20+
type="range"
21+
value={value}
22+
min="0"
23+
max="100"
24+
step="10"
25+
onChange={(e) => handleProgressChange(e)}
26+
disabled={isLoading}
27+
style={
28+
{
29+
'--progress': `${value}%`,
30+
} as React.CSSProperties
31+
}
32+
/>
33+
) : (
34+
//TODO: Remove this block while removing feature flag (ISSUE: #1366)
35+
<input
36+
className={styles.slider}
37+
type="range"
38+
value={value}
39+
min="0"
40+
max="100"
41+
step="10"
42+
onChange={(e) => handleProgressChange(e)}
43+
onTouchEnd={() => debounceSlider(1000)}
44+
onMouseUp={() => debounceSlider(1000)}
45+
disabled={isLoading}
46+
style={
47+
{
48+
'--progress': `${value}%`,
49+
} as React.CSSProperties
50+
}
51+
/>
52+
)}
53+
</>
3054
);
3155
};
3256

src/components/tasks/card/progressContainer/index.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { FC, useState } from 'react';
1+
import { FC, useCallback, useState } from 'react';
22
import { useRouter } from 'next/router';
33
import { toast, ToastTypes } from '@/helperFunctions/toast';
44
import {
@@ -13,11 +13,14 @@ import { CompletionModal } from '@/components/Modal/CompletionModal';
1313
import { ERROR_MESSAGE, PROGRESS_SUCCESSFUL } from '@/constants/constants';
1414
import styles from '@/components/tasks/card/card.module.scss';
1515
import { ProgressContainerProps } from '@/interfaces/task.type';
16+
import { debounce } from '@/utils/common';
1617

1718
const ProgressContainer: FC<ProgressContainerProps> = ({
1819
content,
1920
readOnly = false,
2021
}) => {
22+
const DEBOUNCE_DELAY = 1000;
23+
2124
const router = useRouter();
2225
const { dev } = router.query;
2326
const isDev = dev === 'true';
@@ -77,10 +80,20 @@ const ProgressContainer: FC<ProgressContainerProps> = ({
7780
}, 1000);
7881
};
7982

83+
const debouncedUpdate = useCallback(
84+
debounce((id: string, value: number) => {
85+
handleSliderChangeComplete(id, value);
86+
}, DEBOUNCE_DELAY),
87+
[isUserAuthorized]
88+
);
89+
8090
const handleProgressChange = (
8191
event: React.ChangeEvent<HTMLInputElement>
8292
) => {
8393
setProgressValue(Number(event.target.value));
94+
if (isDev) {
95+
debouncedUpdate(content.id, Number(event.target.value));
96+
}
8497
};
8598

8699
const handleProgressUpdate = () => {

src/utils/common.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,11 @@ export const readMoreFormatter = (
1212

1313
return stringToFormat;
1414
};
15+
16+
export function debounce(cb: (...args: any[]) => any, delayMs: number) {
17+
let timerId: ReturnType<typeof setTimeout>;
18+
return function (...args: any[]) {
19+
clearTimeout(timerId);
20+
timerId = setTimeout(() => cb(...args), delayMs);
21+
};
22+
}

0 commit comments

Comments
 (0)