diff --git a/src/components/MessageInput/CooldownTimer.tsx b/src/components/MessageInput/CooldownTimer.tsx index 3f36f6d0a..2fe1127ae 100644 --- a/src/components/MessageInput/CooldownTimer.tsx +++ b/src/components/MessageInput/CooldownTimer.tsx @@ -4,20 +4,24 @@ export type CooldownTimerProps = { cooldownInterval: number; setCooldownRemaining: React.Dispatch>; }; -export const CooldownTimer = ({ cooldownInterval, setCooldownRemaining }: CooldownTimerProps) => { - const [seconds, setSeconds] = useState(cooldownInterval); +export const CooldownTimer = ({ cooldownInterval }: CooldownTimerProps) => { + const [seconds, setSeconds] = useState(); useEffect(() => { - const countdownInterval = setInterval(() => { - if (seconds > 0) { + let countdownTimeout: ReturnType; + if (typeof seconds === 'number' && seconds > 0) { + countdownTimeout = setTimeout(() => { setSeconds(seconds - 1); - } else { - setCooldownRemaining(0); - } - }, 1000); + }, 1000); + } + return () => { + clearTimeout(countdownTimeout); + }; + }, [seconds]); - return () => clearInterval(countdownInterval); - }); + useEffect(() => { + setSeconds(cooldownInterval ?? 0); + }, [cooldownInterval]); return (
diff --git a/src/components/MessageInput/__tests__/CooldownTimer.test.js b/src/components/MessageInput/__tests__/CooldownTimer.test.js new file mode 100644 index 000000000..320f87449 --- /dev/null +++ b/src/components/MessageInput/__tests__/CooldownTimer.test.js @@ -0,0 +1,65 @@ +import React from 'react'; +import { act, render, screen } from '@testing-library/react'; +import { CooldownTimer } from '../CooldownTimer'; +import '@testing-library/jest-dom'; + +jest.useFakeTimers(); + +const TIMER_TEST_ID = 'cooldown-timer'; +const remainingProp = 'cooldownInterval'; +describe('CooldownTimer', () => { + it('renders CooldownTimer component', () => { + render(); + expect(screen.getByTestId(TIMER_TEST_ID)).toHaveTextContent('0'); + }); + + it('initializes with correct state based on cooldownRemaining prop', () => { + const props = { [remainingProp]: 10 }; + render(); + expect(screen.getByTestId(TIMER_TEST_ID)).toHaveTextContent('10'); + }); + + it('updates countdown logic correctly', () => { + const cooldownRemaining = 5; + const props = { [remainingProp]: cooldownRemaining }; + render(); + + for (let countDown = cooldownRemaining; countDown >= 0; countDown--) { + expect(screen.getByTestId(TIMER_TEST_ID)).toHaveTextContent(countDown.toString()); + act(() => { + jest.runAllTimers(); + }); + } + expect(screen.getByTestId(TIMER_TEST_ID)).toHaveTextContent('0'); + }); + + it('resets countdown when cooldownRemaining prop changes', () => { + const cooldownRemaining1 = 5; + const cooldownRemaining2 = 10; + const props1 = { [remainingProp]: cooldownRemaining1 }; + const props2 = { [remainingProp]: cooldownRemaining2 }; + const timeElapsedBeforeUpdate = 2; + + const { rerender } = render(); + + for (let round = timeElapsedBeforeUpdate; round > 0; round--) { + act(() => { + jest.runAllTimers(); + }); + } + + expect(screen.getByTestId(TIMER_TEST_ID)).toHaveTextContent( + (cooldownRemaining1 - timeElapsedBeforeUpdate).toString(), + ); + + rerender(); + + expect(screen.queryByTestId(TIMER_TEST_ID)).toHaveTextContent(cooldownRemaining2.toString()); + act(() => { + jest.runAllTimers(); + }); + expect(screen.queryByTestId(TIMER_TEST_ID)).toHaveTextContent( + (cooldownRemaining2 - 1).toString(), + ); + }); +}); diff --git a/src/components/MessageInput/__tests__/MessageInput.test.js b/src/components/MessageInput/__tests__/MessageInput.test.js index 4489b7765..d6544d6ee 100644 --- a/src/components/MessageInput/__tests__/MessageInput.test.js +++ b/src/components/MessageInput/__tests__/MessageInput.test.js @@ -58,6 +58,7 @@ const mockedChannelData = generateChannel({ thread: [threadMessage], }); +const cooldown = 30; const filename = 'some.txt'; const fileUploadUrl = 'http://www.getstream.io'; // real url, because ImagePreview will try to load the image @@ -1147,7 +1148,7 @@ function axeNoViolations(container) { const renderWithActiveCooldown = async ({ messageInputProps = {} } = {}) => { channel = chatClient.channel('messaging', mockedChannelData.channel.id); - channel.data.cooldown = 30; + channel.data.cooldown = cooldown; channel.initialized = true; const lastSentSecondsAhead = 5; await render({ @@ -1263,6 +1264,17 @@ function axeNoViolations(container) { expect(screen.queryByTestId(COOLDOWN_TIMER_TEST_ID)).not.toBeInTheDocument(); } }); + + it('should be removed after cool-down period elapsed', async () => { + jest.useFakeTimers(); + await renderWithActiveCooldown(); + expect(screen.getByTestId(COOLDOWN_TIMER_TEST_ID)).toHaveTextContent(cooldown.toString()); + act(() => { + jest.advanceTimersByTime(cooldown * 1000); + }); + expect(screen.queryByTestId(COOLDOWN_TIMER_TEST_ID)).not.toBeInTheDocument(); + jest.useRealTimers(); + }); }); }); }); diff --git a/src/components/MessageInput/hooks/__tests__/useCooldownTimer.test.js b/src/components/MessageInput/hooks/__tests__/useCooldownTimer.test.js index 85508be10..2691c3223 100644 --- a/src/components/MessageInput/hooks/__tests__/useCooldownTimer.test.js +++ b/src/components/MessageInput/hooks/__tests__/useCooldownTimer.test.js @@ -5,6 +5,9 @@ import { useCooldownTimer } from '../useCooldownTimer'; import { ChannelStateProvider, ChatProvider } from '../../../../context'; import { getTestClient } from '../../../../mock-builders'; +import { act } from '@testing-library/react'; + +jest.useFakeTimers(); async function renderUseCooldownTimerHook({ channel, chatContext }) { const client = await getTestClient(); @@ -126,4 +129,23 @@ describe('useCooldownTimer', () => { const { result } = await renderUseCooldownTimerHook({ channel, chatContext }); expect(result.current.cooldownRemaining).toBe(cooldown); }); + + it('remove the cooldown after the cooldown period elapses', async () => { + const channel = { cid, data: { cooldown } }; + const chatContext = { + latestMessageDatesByChannels: { + [cid]: new Date(), + }, + }; + + const { result } = await renderUseCooldownTimerHook({ channel, chatContext }); + + expect(result.current.cooldownRemaining).toBe(cooldown); + + await act(() => { + jest.advanceTimersByTime(cooldown * 1000); + }); + + expect(result.current.cooldownRemaining).toBe(0); + }); }); diff --git a/src/components/MessageInput/hooks/useCooldownTimer.tsx b/src/components/MessageInput/hooks/useCooldownTimer.tsx index fa81598f6..a86207ee3 100644 --- a/src/components/MessageInput/hooks/useCooldownTimer.tsx +++ b/src/components/MessageInput/hooks/useCooldownTimer.tsx @@ -40,13 +40,24 @@ export const useCooldownTimer = < Math.max(0, (new Date().getTime() - ownLatestMessageDate.getTime()) / 1000) : undefined; - setCooldownRemaining( + const remaining = !skipCooldown && - typeof timeSinceOwnLastMessage !== 'undefined' && - cooldownInterval > timeSinceOwnLastMessage + typeof timeSinceOwnLastMessage !== 'undefined' && + cooldownInterval > timeSinceOwnLastMessage ? Math.round(cooldownInterval - timeSinceOwnLastMessage) - : 0, - ); + : 0; + + setCooldownRemaining(remaining); + + if (!remaining) return; + + const timeout = setTimeout(() => { + setCooldownRemaining(0); + }, remaining * 1000); + + return () => { + clearTimeout(timeout); + }; }, [cooldownInterval, ownLatestMessageDate, skipCooldown]); return {