tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 5b5a45c22519656ab71e7d75f45e131133476051
parent 3d3b2ddaf5660788bea7a5966ccdd099afa214a3
Author: Reem H <42309026+reemhamz@users.noreply.github.com>
Date:   Wed, 10 Dec 2025 20:47:50 +0000

Bug 1985978 - Stop countdown timer from stopping and resetting with one second remaining. r=home-newtab-reviewers,maxx

Differential Revision: https://phabricator.services.mozilla.com/D275731

Diffstat:
Mbrowser/extensions/newtab/content-src/components/Widgets/FocusTimer/FocusTimer.jsx | 19++++++++++++-------
Mbrowser/extensions/newtab/data/content/activity-stream.bundle.js | 19++++++++++++-------
Mbrowser/extensions/newtab/test/unit/content-src/components/Widgets/FocusTimer.test.jsx | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 83 insertions(+), 14 deletions(-)

diff --git a/browser/extensions/newtab/content-src/components/Widgets/FocusTimer/FocusTimer.jsx b/browser/extensions/newtab/content-src/components/Widgets/FocusTimer/FocusTimer.jsx @@ -154,11 +154,18 @@ export const FocusTimer = ({ useEffect(() => { // resets default values after timer ends let interval; + let hasReachedZero = false; if (isRunning && duration > 0) { interval = setInterval(() => { + const currentTime = Math.floor(Date.now() / 1000); + const elapsed = currentTime - startTime; const remaining = calculateTimeRemaining(duration, startTime); - if (remaining <= 0) { + // using setTimeLeft to trigger a re-render of the component to show live countdown each second + setTimeLeft(remaining); + setProgress((initialDuration - remaining) / initialDuration); + + if (elapsed >= duration && hasReachedZero) { clearInterval(interval); batch(() => { @@ -215,13 +222,11 @@ export const FocusTimer = ({ }) ); }); - }, 1500); - }, 1500); + }, 500); + }, 1000); + } else if (elapsed >= duration) { + hasReachedZero = true; } - - // using setTimeLeft to trigger a re-render of the component to show live countdown each second - setTimeLeft(remaining); - setProgress((initialDuration - remaining) / initialDuration); }, 1000); } diff --git a/browser/extensions/newtab/data/content/activity-stream.bundle.js b/browser/extensions/newtab/data/content/activity-stream.bundle.js @@ -12744,10 +12744,17 @@ const FocusTimer = ({ (0,external_React_namespaceObject.useEffect)(() => { // resets default values after timer ends let interval; + let hasReachedZero = false; if (isRunning && duration > 0) { interval = setInterval(() => { + const currentTime = Math.floor(Date.now() / 1000); + const elapsed = currentTime - startTime; const remaining = calculateTimeRemaining(duration, startTime); - if (remaining <= 0) { + + // using setTimeLeft to trigger a re-render of the component to show live countdown each second + setTimeLeft(remaining); + setProgress((initialDuration - remaining) / initialDuration); + if (elapsed >= duration && hasReachedZero) { clearInterval(interval); (0,external_ReactRedux_namespaceObject.batch)(() => { dispatch(actionCreators.AlsoToMain({ @@ -12792,13 +12799,11 @@ const FocusTimer = ({ } })); }); - }, 1500); - }, 1500); + }, 500); + }, 1000); + } else if (elapsed >= duration) { + hasReachedZero = true; } - - // using setTimeLeft to trigger a re-render of the component to show live countdown each second - setTimeLeft(remaining); - setProgress((initialDuration - remaining) / initialDuration); }, 1000); } diff --git a/browser/extensions/newtab/test/unit/content-src/components/Widgets/FocusTimer.test.jsx b/browser/extensions/newtab/test/unit/content-src/components/Widgets/FocusTimer.test.jsx @@ -461,6 +461,65 @@ describe("<FocusTimer>", () => { ); }); + it("should wait one second at zero before completing timer", () => { + const now = Math.floor(Date.now() / 1000); + + const endState = { + ...mockState, + TimerWidget: { + ...mockState.TimerWidget, + timerType: "focus", + focus: { + duration: 300, + initialDuration: 300, + startTime: now - 300, + isRunning: true, + }, + }, + }; + + wrapper = mount( + <WrapWithProvider state={endState}> + <FocusTimer + dispatch={dispatch} + handleUserInteraction={handleUserInteraction} + /> + </WrapWithProvider> + ); + + // First interval tick - should reach zero but not complete + clock.tick(1000); + + // Verify timer has not ended yet (no WIDGETS_TIMER_END dispatched) + const callsAfterFirstTick = dispatch + .getCalls() + .map(call => call.args[0]) + .filter(action => action && action.type === at.WIDGETS_TIMER_END); + + assert.equal( + callsAfterFirstTick.length, + 0, + "WIDGETS_TIMER_END should not be dispatched on first tick at zero" + ); + + // Second interval tick - should now complete + clock.tick(1000); + + // Allowing time for the chained timeouts for animation + clock.tick(2000); + wrapper.update(); + + const endCall = dispatch + .getCalls() + .map(call => call.args[0]) + .find(action => action && action.type === at.WIDGETS_TIMER_END); + + assert.ok( + endCall, + "WIDGETS_TIMER_END should be dispatched after one second at zero" + ); + }); + describe("context menu", () => { it("should render default context menu", () => { assert.ok(wrapper.find(".focus-timer-context-menu-button").exists());