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:
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());