Skip to content

Commit 6b14e0a

Browse files
committed
test useNoticeTimer behavior
1 parent fb51848 commit 6b14e0a

2 files changed

Lines changed: 221 additions & 29 deletions

File tree

src/hooks/useNoticeTimer.ts

Lines changed: 47 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -16,48 +16,66 @@ export default function useNoticeTimer(
1616
const startTimestampRef = React.useRef<number | null>(null);
1717
const passTimeRef = React.useRef(0);
1818

19+
function syncPassTime() {
20+
const now = Date.now();
21+
const passedTime = now - (startTimestampRef.current || now);
22+
startTimestampRef.current = now;
23+
passTimeRef.current += passedTime;
24+
}
25+
1926
function onPause() {
27+
syncPassTime();
2028
setWalking(false);
2129
}
2230

2331
function onResume() {
24-
setWalking(true);
32+
if (durationMs > 0) {
33+
setWalking(true);
34+
} else {
35+
onEventUpdate(0);
36+
}
2537
}
2638

27-
function updateProgress() {
28-
if (durationMs) {
29-
const now = Date.now();
30-
const passedTime = now - (startTimestampRef.current || now);
31-
startTimestampRef.current = now;
32-
passTimeRef.current += passedTime;
33-
onEventUpdate(Math.min(passTimeRef.current / durationMs, 1));
39+
React.useEffect(() => {
40+
if (durationMs <= 0) {
41+
startTimestampRef.current = null;
42+
onEventUpdate(0);
43+
return;
44+
}
3445

35-
// Return true if timesup
36-
return passTimeRef.current >= durationMs;
46+
syncPassTime();
47+
onEventUpdate(Math.min(passTimeRef.current / durationMs, 1));
48+
49+
if (!walking) {
50+
startTimestampRef.current = null;
51+
return;
3752
}
38-
return false;
39-
}
4053

41-
React.useEffect(() => {
42-
if (walking && durationMs > 0) {
43-
let rafId: number | null = null;
44-
45-
function step() {
46-
if (updateProgress()) {
47-
onEventClose();
48-
} else {
49-
rafId = raf(step);
50-
}
51-
}
54+
if (passTimeRef.current >= durationMs) {
55+
onEventClose();
56+
return;
57+
}
58+
59+
let rafId: number | null = null;
5260

53-
startTimestampRef.current = Date.now();
54-
rafId = raf(step);
61+
function step() {
62+
syncPassTime();
63+
onEventUpdate(Math.min(passTimeRef.current / durationMs, 1));
5564

56-
return () => {
57-
raf.cancel(rafId);
58-
};
65+
if (passTimeRef.current >= durationMs) {
66+
onEventClose();
67+
} else {
68+
rafId = raf(step);
69+
}
5970
}
60-
}, [walking]);
71+
72+
startTimestampRef.current = Date.now();
73+
rafId = raf(step);
74+
75+
return () => {
76+
raf.cancel(rafId);
77+
};
78+
}, [durationMs, walking]);
6179

6280
return [onResume, onPause] as const;
6381
}

tests/useNoticeTimer.test.tsx

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import React, { act } from 'react';
2+
import { fireEvent, render } from '@testing-library/react';
3+
import useNoticeTimer from '../src/hooks/useNoticeTimer';
4+
5+
function TimerDemo({
6+
duration,
7+
onClose,
8+
onUpdate,
9+
}: {
10+
duration: number | false;
11+
onClose: VoidFunction;
12+
onUpdate: (ptg: number) => void;
13+
}) {
14+
const [onResume, onPause] = useNoticeTimer(duration, onClose, onUpdate);
15+
16+
return (
17+
<>
18+
<button type="button" data-testid="pause" onClick={onPause} />
19+
<button type="button" data-testid="resume" onClick={onResume} />
20+
</>
21+
);
22+
}
23+
24+
describe('useNoticeTimer', () => {
25+
beforeEach(() => {
26+
vi.useFakeTimers();
27+
vi.setSystemTime(0);
28+
});
29+
30+
afterEach(() => {
31+
vi.useRealTimers();
32+
});
33+
34+
it('closes after duration and reports progress from 0 to 1', () => {
35+
const onClose = vi.fn();
36+
const updates: number[] = [];
37+
38+
render(<TimerDemo duration={1} onClose={onClose} onUpdate={(ptg) => updates.push(ptg)} />);
39+
40+
expect(updates.at(-1) ?? 0).toBe(0);
41+
42+
act(() => {
43+
vi.advanceTimersByTime(500);
44+
});
45+
46+
expect(onClose).not.toHaveBeenCalled();
47+
expect(updates.at(-1) ?? 0).toBeGreaterThan(0.4);
48+
expect(updates.at(-1) ?? 0).toBeLessThan(0.7);
49+
50+
act(() => {
51+
vi.advanceTimersByTime(600);
52+
});
53+
54+
expect(onClose).toHaveBeenCalledTimes(1);
55+
expect(updates.at(-1)).toBe(1);
56+
});
57+
58+
it('keeps the remaining time across pause and resume', () => {
59+
const onClose = vi.fn();
60+
const updates: number[] = [];
61+
62+
const { getByTestId } = render(
63+
<TimerDemo duration={1} onClose={onClose} onUpdate={(ptg) => updates.push(ptg)} />,
64+
);
65+
66+
act(() => {
67+
vi.advanceTimersByTime(400);
68+
});
69+
70+
const pausedProgress = updates.at(-1) ?? 0;
71+
72+
act(() => {
73+
fireEvent.click(getByTestId('pause'));
74+
});
75+
76+
act(() => {
77+
vi.advanceTimersByTime(1000);
78+
});
79+
80+
expect(onClose).not.toHaveBeenCalled();
81+
expect(updates.at(-1)).toBe(pausedProgress);
82+
83+
act(() => {
84+
fireEvent.click(getByTestId('resume'));
85+
});
86+
87+
act(() => {
88+
vi.advanceTimersByTime(550);
89+
});
90+
91+
expect(onClose).not.toHaveBeenCalled();
92+
93+
act(() => {
94+
vi.advanceTimersByTime(100);
95+
});
96+
97+
expect(onClose).toHaveBeenCalledTimes(1);
98+
expect(updates.at(-1)).toBe(1);
99+
});
100+
101+
it('recomputes progress and remaining time when duration changes', () => {
102+
const onClose = vi.fn();
103+
const updates: number[] = [];
104+
105+
const { rerender } = render(
106+
<TimerDemo duration={1} onClose={onClose} onUpdate={(ptg) => updates.push(ptg)} />,
107+
);
108+
109+
act(() => {
110+
vi.advanceTimersByTime(400);
111+
});
112+
113+
act(() => {
114+
rerender(
115+
<TimerDemo duration={0.5} onClose={onClose} onUpdate={(ptg) => updates.push(ptg)} />,
116+
);
117+
});
118+
119+
expect(updates.at(-1) ?? 0).toBeGreaterThan(0.75);
120+
expect(updates.at(-1) ?? 0).toBeLessThanOrEqual(1);
121+
122+
act(() => {
123+
vi.advanceTimersByTime(64);
124+
});
125+
126+
expect(onClose).not.toHaveBeenCalled();
127+
128+
act(() => {
129+
vi.advanceTimersByTime(96);
130+
});
131+
132+
expect(onClose).toHaveBeenCalledTimes(1);
133+
expect(updates.at(-1)).toBe(1);
134+
});
135+
136+
it('resets progress to 0 and stops closing when duration becomes false', () => {
137+
const onClose = vi.fn();
138+
const updates: number[] = [];
139+
140+
const { getByTestId, rerender } = render(
141+
<TimerDemo duration={1} onClose={onClose} onUpdate={(ptg) => updates.push(ptg)} />,
142+
);
143+
144+
act(() => {
145+
vi.advanceTimersByTime(400);
146+
});
147+
148+
act(() => {
149+
rerender(
150+
<TimerDemo duration={false} onClose={onClose} onUpdate={(ptg) => updates.push(ptg)} />,
151+
);
152+
});
153+
154+
expect(updates.at(-1)).toBe(0);
155+
156+
act(() => {
157+
vi.advanceTimersByTime(2000);
158+
});
159+
160+
expect(onClose).not.toHaveBeenCalled();
161+
expect(updates.at(-1)).toBe(0);
162+
163+
act(() => {
164+
fireEvent.click(getByTestId('resume'));
165+
});
166+
167+
act(() => {
168+
vi.advanceTimersByTime(500);
169+
});
170+
171+
expect(onClose).not.toHaveBeenCalled();
172+
expect(updates.at(-1)).toBe(0);
173+
});
174+
});

0 commit comments

Comments
 (0)