From aae564b8d0b5e86c30ce3a45e52bb62854139fb9 Mon Sep 17 00:00:00 2001 From: Eksiart Date: Fri, 13 Mar 2026 16:28:06 +0800 Subject: [PATCH 1/3] =?UTF-8?q?stopwatch-bugfix=20=F0=9F=A7=8A=20fix=20tim?= =?UTF-8?q?e=20update=20order.=20fix=20autostart=20when=20has=20initialTim?= =?UTF-8?q?e.=20fix=20jsDoc=20enabled=20->=20immediately.=20remove=20unuse?= =?UTF-8?q?d=20over=20from=20UseStopwatchReturn.=20add=20tests=20for=20edg?= =?UTF-8?q?e=20cases.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bundle/hooks/useStopwatch/useStopwatch.js | 24 ++++----- .../hooks/useStopwatch/useStopwatch.test.ts | 54 +++++++++++++++++++ .../src/hooks/useStopwatch/useStopwatch.ts | 26 +++++---- 3 files changed, 78 insertions(+), 26 deletions(-) diff --git a/packages/core/src/bundle/hooks/useStopwatch/useStopwatch.js b/packages/core/src/bundle/hooks/useStopwatch/useStopwatch.js index eb05949e..b6251078 100644 --- a/packages/core/src/bundle/hooks/useStopwatch/useStopwatch.js +++ b/packages/core/src/bundle/hooks/useStopwatch/useStopwatch.js @@ -21,35 +21,37 @@ const getStopwatchTime = (time) => { * * @overload * @param {number} [initialTime=0] The initial time of the timer - * @param {boolean} [options.enabled=true] The enabled state of the timer + * @param {boolean} [options.immediately=false] Start the stopwatch immediately * @returns {UseStopwatchReturn} An object containing the current time and functions to interact with the timer * * @example - * const { seconds, minutes, start, pause, reset } = useStopwatch(1000, { enabled: false }); + * const { seconds, minutes, start, pause, reset } = useStopwatch(1000, { immediately: false }); * * @overload - * @param {number} [options.initialTime=0] -The initial time of the timer - * @param {boolean} [options.enabled=true] The enabled state of the timer + * @param {number} [options.initialTime=0] The initial time of the timer + * @param {boolean} [options.immediately=false] Start the stopwatch immediately * @returns {UseStopwatchReturn} An object containing the current time and functions to interact with the timer * * @example - * const { seconds, minutes, start, pause, reset } = useStopwatch({ initialTime: 1000, enabled: false }); + * const { seconds, minutes, start, pause, reset } = useStopwatch({ initialTime: 1000, immediately: false }); */ export const useStopwatch = (...params) => { const initialTime = (typeof params[0] === 'number' ? params[0] : params[0]?.initialTime) ?? 0; const options = typeof params[0] === 'number' ? params[1] : params[0]; const immediately = options?.immediately ?? false; const [time, setTime] = useState(getStopwatchTime(initialTime)); - const [paused, setPaused] = useState(!immediately && !initialTime); + const [paused, setPaused] = useState(!immediately); useEffect(() => { if (paused) return; const onInterval = () => { setTime((prevTime) => { const updatedCount = prevTime.count + 1; - if (updatedCount % 60 === 0) { + if (updatedCount % (60 * 60 * 24) === 0) { return { ...prevTime, - minutes: prevTime.minutes + 1, + days: prevTime.days + 1, + hours: 0, + minutes: 0, seconds: 0, count: updatedCount }; @@ -63,12 +65,10 @@ export const useStopwatch = (...params) => { count: updatedCount }; } - if (updatedCount % (60 * 60 * 24) === 0) { + if (updatedCount % 60 === 0) { return { ...prevTime, - days: prevTime.days + 1, - hours: 0, - minutes: 0, + minutes: prevTime.minutes + 1, seconds: 0, count: updatedCount }; diff --git a/packages/core/src/hooks/useStopwatch/useStopwatch.test.ts b/packages/core/src/hooks/useStopwatch/useStopwatch.test.ts index 4ab7ccf9..115b3916 100644 --- a/packages/core/src/hooks/useStopwatch/useStopwatch.test.ts +++ b/packages/core/src/hooks/useStopwatch/useStopwatch.test.ts @@ -150,3 +150,57 @@ it('Should cleanup interval on unmount', () => { expect(clearIntervalSpy).toHaveBeenCalled(); clearIntervalSpy.mockRestore(); }); + +it('Should correctly transition from 59 seconds to 1 minute', () => { + const { result } = renderHook(useStopwatch); + + act(() => result.current.start()); + + act(() => vi.advanceTimersByTime(59_000)); + + expect(result.current.seconds).toBe(59); + expect(result.current.minutes).toBe(0); + + act(() => vi.advanceTimersByTime(1_000)); + + expect(result.current.seconds).toBe(0); + expect(result.current.minutes).toBe(1); +}); + +it('Should correctly transition from 59 minutes 59 seconds to 1 hour', () => { + const { result } = renderHook(useStopwatch); + + act(() => result.current.start()); + + act(() => vi.advanceTimersByTime(3_599_000)); + + expect(result.current.hours).toBe(0); + expect(result.current.minutes).toBe(59); + expect(result.current.seconds).toBe(59); + + act(() => vi.advanceTimersByTime(1_000)); + + expect(result.current.hours).toBe(1); + expect(result.current.minutes).toBe(0); + expect(result.current.seconds).toBe(0); +}); + +it('Should correctly transition from 23:59:59 to 1 day', () => { + const { result } = renderHook(useStopwatch); + + act(() => result.current.start()); + + act(() => vi.advanceTimersByTime(86_399_000)); + + expect(result.current.days).toBe(0); + expect(result.current.hours).toBe(23); + expect(result.current.minutes).toBe(59); + expect(result.current.seconds).toBe(59); + + act(() => vi.advanceTimersByTime(1_000)); + + expect(result.current.days).toBe(1); + expect(result.current.hours).toBe(0); + expect(result.current.minutes).toBe(0); + expect(result.current.seconds).toBe(0); +}); diff --git a/packages/core/src/hooks/useStopwatch/useStopwatch.ts b/packages/core/src/hooks/useStopwatch/useStopwatch.ts index 4100fc9f..b2c7cfab 100644 --- a/packages/core/src/hooks/useStopwatch/useStopwatch.ts +++ b/packages/core/src/hooks/useStopwatch/useStopwatch.ts @@ -28,8 +28,6 @@ export interface UseStopwatchReturn { hours: number; /** The minute count of the stopwatch */ minutes: number; - /** The over state of the stopwatch */ - over: boolean; /** The paused state of the stopwatch */ paused: boolean; /** The second count of the stopwatch */ @@ -61,19 +59,19 @@ interface UseStopwatch { * * @overload * @param {number} [initialTime=0] The initial time of the timer - * @param {boolean} [options.enabled=true] The enabled state of the timer + * @param {boolean} [options.immediately=false] Start the stopwatch immediately * @returns {UseStopwatchReturn} An object containing the current time and functions to interact with the timer * * @example - * const { seconds, minutes, start, pause, reset } = useStopwatch(1000, { enabled: false }); + * const { seconds, minutes, start, pause, reset } = useStopwatch(1000, { immediately: false }); * * @overload - * @param {number} [options.initialTime=0] -The initial time of the timer - * @param {boolean} [options.enabled=true] The enabled state of the timer + * @param {number} [options.initialTime=0] The initial time of the timer + * @param {boolean} [options.immediately=false] Start the stopwatch immediately * @returns {UseStopwatchReturn} An object containing the current time and functions to interact with the timer * * @example - * const { seconds, minutes, start, pause, reset } = useStopwatch({ initialTime: 1000, enabled: false }); + * const { seconds, minutes, start, pause, reset } = useStopwatch({ initialTime: 1000, immediately: false }); */ export const useStopwatch = ((...params: any[]) => { const initialTime = @@ -89,7 +87,7 @@ export const useStopwatch = ((...params: any[]) => { const immediately = options?.immediately ?? false; const [time, setTime] = useState(getStopwatchTime(initialTime)); - const [paused, setPaused] = useState(!immediately && !initialTime); + const [paused, setPaused] = useState(!immediately); useEffect(() => { if (paused) return; @@ -97,10 +95,12 @@ export const useStopwatch = ((...params: any[]) => { setTime((prevTime) => { const updatedCount = prevTime.count + 1; - if (updatedCount % 60 === 0) { + if (updatedCount % (60 * 60 * 24) === 0) { return { ...prevTime, - minutes: prevTime.minutes + 1, + days: prevTime.days + 1, + hours: 0, + minutes: 0, seconds: 0, count: updatedCount }; @@ -116,12 +116,10 @@ export const useStopwatch = ((...params: any[]) => { }; } - if (updatedCount % (60 * 60 * 24) === 0) { + if (updatedCount % 60 === 0) { return { ...prevTime, - days: prevTime.days + 1, - hours: 0, - minutes: 0, + minutes: prevTime.minutes + 1, seconds: 0, count: updatedCount }; From b1a9382d24b1ce19ccf333ca753eabd60e343818 Mon Sep 17 00:00:00 2001 From: Eksiart Date: Fri, 13 Mar 2026 20:32:50 +0800 Subject: [PATCH 2/3] =?UTF-8?q?stopwatch-bugfix=20=F0=9F=A7=8A=20simplify?= =?UTF-8?q?=20time=20state=20management?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bundle/hooks/useInterval/useInterval.js | 11 ++-- .../bundle/hooks/useStopwatch/useStopwatch.js | 46 +++------------- .../src/hooks/useStopwatch/useStopwatch.ts | 52 ++++--------------- 3 files changed, 22 insertions(+), 87 deletions(-) diff --git a/packages/core/src/bundle/hooks/useInterval/useInterval.js b/packages/core/src/bundle/hooks/useInterval/useInterval.js index 29eefac6..39db0dbd 100644 --- a/packages/core/src/bundle/hooks/useInterval/useInterval.js +++ b/packages/core/src/bundle/hooks/useInterval/useInterval.js @@ -23,10 +23,10 @@ import { useEffect, useRef, useState } from 'react'; */ export const useInterval = (...params) => { const callback = params[0]; - const interval = (typeof params[1] === 'number' ? params[1] : params[1].interval) ?? 1000; + const interval = (typeof params[1] === 'number' ? params[1] : params[1]?.interval) ?? 1000; const options = typeof params[1] === 'object' ? params[1] : params[2]; const immediately = options?.immediately ?? true; - const [active, setActive] = useState(immediately ?? true); + const [active, setActive] = useState(immediately); const intervalIdRef = useRef(undefined); const internalCallbackRef = useRef(callback); internalCallbackRef.current = callback; @@ -38,11 +38,8 @@ export const useInterval = (...params) => { }; }, [active, interval]); const pause = () => setActive(false); - const resume = () => { - if (interval <= 0) return; - setActive(true); - }; - const toggle = () => setActive(!active); + const resume = () => setActive(true); + const toggle = () => setActive((prev) => !prev); return { active, pause, diff --git a/packages/core/src/bundle/hooks/useStopwatch/useStopwatch.js b/packages/core/src/bundle/hooks/useStopwatch/useStopwatch.js index b6251078..480738b2 100644 --- a/packages/core/src/bundle/hooks/useStopwatch/useStopwatch.js +++ b/packages/core/src/bundle/hooks/useStopwatch/useStopwatch.js @@ -39,56 +39,26 @@ export const useStopwatch = (...params) => { const initialTime = (typeof params[0] === 'number' ? params[0] : params[0]?.initialTime) ?? 0; const options = typeof params[0] === 'number' ? params[1] : params[0]; const immediately = options?.immediately ?? false; - const [time, setTime] = useState(getStopwatchTime(initialTime)); + const [count, setCount] = useState(initialTime); const [paused, setPaused] = useState(!immediately); + useEffect(() => { + setCount(initialTime); + }, [initialTime]); useEffect(() => { if (paused) return; const onInterval = () => { - setTime((prevTime) => { - const updatedCount = prevTime.count + 1; - if (updatedCount % (60 * 60 * 24) === 0) { - return { - ...prevTime, - days: prevTime.days + 1, - hours: 0, - minutes: 0, - seconds: 0, - count: updatedCount - }; - } - if (updatedCount % (60 * 60) === 0) { - return { - ...prevTime, - hours: prevTime.hours + 1, - minutes: 0, - seconds: 0, - count: updatedCount - }; - } - if (updatedCount % 60 === 0) { - return { - ...prevTime, - minutes: prevTime.minutes + 1, - seconds: 0, - count: updatedCount - }; - } - return { - ...prevTime, - seconds: prevTime.seconds + 1, - count: updatedCount - }; - }); + setCount((prevCount) => prevCount + 1); }; - const interval = setInterval(() => onInterval(), 1000); + const interval = setInterval(onInterval, 1000); return () => clearInterval(interval); }, [paused]); + const time = getStopwatchTime(count); return { ...time, paused, pause: () => setPaused(true), start: () => setPaused(false), - reset: () => setTime(getStopwatchTime(initialTime)), + reset: () => setCount(initialTime), toggle: () => setPaused((prevPause) => !prevPause) }; }; diff --git a/packages/core/src/hooks/useStopwatch/useStopwatch.ts b/packages/core/src/hooks/useStopwatch/useStopwatch.ts index b2c7cfab..9b2f5459 100644 --- a/packages/core/src/hooks/useStopwatch/useStopwatch.ts +++ b/packages/core/src/hooks/useStopwatch/useStopwatch.ts @@ -86,63 +86,31 @@ export const useStopwatch = ((...params: any[]) => { const immediately = options?.immediately ?? false; - const [time, setTime] = useState(getStopwatchTime(initialTime)); + const [count, setCount] = useState(initialTime); const [paused, setPaused] = useState(!immediately); + useEffect(() => { + setCount(initialTime); + }, [initialTime]); + useEffect(() => { if (paused) return; const onInterval = () => { - setTime((prevTime) => { - const updatedCount = prevTime.count + 1; - - if (updatedCount % (60 * 60 * 24) === 0) { - return { - ...prevTime, - days: prevTime.days + 1, - hours: 0, - minutes: 0, - seconds: 0, - count: updatedCount - }; - } - - if (updatedCount % (60 * 60) === 0) { - return { - ...prevTime, - hours: prevTime.hours + 1, - minutes: 0, - seconds: 0, - count: updatedCount - }; - } - - if (updatedCount % 60 === 0) { - return { - ...prevTime, - minutes: prevTime.minutes + 1, - seconds: 0, - count: updatedCount - }; - } - - return { - ...prevTime, - seconds: prevTime.seconds + 1, - count: updatedCount - }; - }); + setCount((prevCount) => prevCount + 1); }; - const interval = setInterval(() => onInterval(), 1000); + const interval = setInterval(onInterval, 1000); return () => clearInterval(interval); }, [paused]); + const time = getStopwatchTime(count); + return { ...time, paused, pause: () => setPaused(true), start: () => setPaused(false), - reset: () => setTime(getStopwatchTime(initialTime)), + reset: () => setCount(initialTime), toggle: () => setPaused((prevPause) => !prevPause) }; }) as UseStopwatch; From a5c029925e2c3ee58c8986d5f80af2d4afc83f6a Mon Sep 17 00:00:00 2001 From: Eksiart Date: Fri, 13 Mar 2026 20:36:54 +0800 Subject: [PATCH 3/3] =?UTF-8?q?stopwatch-bugfix=20=F0=9F=A7=8A=20fix=20int?= =?UTF-8?q?erval=20definition=20when=20params[1]=20undefined.=20simplify?= =?UTF-8?q?=20active=20definition.=20toggle=20improvement.=20resume=20remo?= =?UTF-8?q?ved=20interval=20check.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/src/hooks/useInterval/useInterval.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/core/src/hooks/useInterval/useInterval.ts b/packages/core/src/hooks/useInterval/useInterval.ts index 612b4755..203efac6 100644 --- a/packages/core/src/hooks/useInterval/useInterval.ts +++ b/packages/core/src/hooks/useInterval/useInterval.ts @@ -51,14 +51,15 @@ export const useInterval = ((...params: any[]): UseIntervalReturn => { const interval = ((typeof params[1] === 'number' ? params[1] - : (params[1] as UseIntervalOptions & { interval?: number }).interval) as number) ?? 1000; + : (params[1] as (UseIntervalOptions & { interval?: number }) | undefined) + ?.interval) as number) ?? 1000; const options = typeof params[1] === 'object' ? (params[1] as (UseIntervalOptions & { interval?: number }) | undefined) : (params[2] as UseIntervalOptions | undefined); const immediately = options?.immediately ?? true; - const [active, setActive] = useState(immediately ?? true); + const [active, setActive] = useState(immediately); const intervalIdRef = useRef>(undefined); const internalCallbackRef = useRef(callback); @@ -75,12 +76,9 @@ export const useInterval = ((...params: any[]): UseIntervalReturn => { const pause = () => setActive(false); - const resume = () => { - if (interval <= 0) return; - setActive(true); - }; + const resume = () => setActive(true); - const toggle = () => setActive(!active); + const toggle = () => setActive((prev) => !prev); return { active,