commit dee465c6494f48fe1780f43a6cdd4ed084224ce1
parent 1bce2ce8e3c40f20b3c99650470004a1eeb77901
Author: Reem H <42309026+reemhamz@users.noreply.github.com>
Date: Mon, 3 Nov 2025 06:16:44 +0000
Bug 1981970 - Only allow numerical values to be input into the Timer widget. r=home-newtab-reviewers,npypchenko
Differential Revision: https://phabricator.services.mozilla.com/D270095
Diffstat:
3 files changed, 153 insertions(+), 13 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
@@ -46,6 +46,27 @@ export const formatTime = seconds => {
};
/**
+ * Validates that the inputs in the timer only allow numerical digits (0-9)
+ *
+ * @param input - The character being input
+ * @returns boolean - true if valid numeric input, false otherwise
+ */
+export const isNumericValue = input => {
+ // Check for null/undefined input or non-numeric characters
+ return input && /^\d+$/.test(input);
+};
+
+/**
+ * Validates if adding a new digit would exceed the 2-character limit
+ *
+ * @param currentValue - The current value in the field
+ * @returns boolean - true if at 2-character limit, false otherwise
+ */
+export const isAtMaxLength = currentValue => {
+ return currentValue.length >= 2;
+};
+
+/**
* Converts a polar coordinate (angle on circle) into a percentage-based [x,y] position for clip-path
*
* @param cx
@@ -416,13 +437,9 @@ export const FocusTimer = ({ dispatch, handleUserInteraction }) => {
const values = e.target.innerText.trim();
// only allow numerical digits 0–9 for time input
- if (!/^\d+$/.test(input)) {
- e.preventDefault();
- }
-
- // only allow 2 values each for minutes and seconds
- if (values.length >= 2) {
+ if (!isNumericValue(input)) {
e.preventDefault();
+ return;
}
const selection = window.getSelection();
@@ -441,6 +458,12 @@ export const FocusTimer = ({ dispatch, handleUserInteraction }) => {
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
+ return;
+ }
+
+ // only allow 2 values each for minutes and seconds
+ if (isAtMaxLength(values)) {
+ e.preventDefault();
}
};
diff --git a/browser/extensions/newtab/data/content/activity-stream.bundle.js b/browser/extensions/newtab/data/content/activity-stream.bundle.js
@@ -12958,6 +12958,27 @@ const formatTime = seconds => {
};
/**
+ * Validates that input is a numeric digit (0-9)
+ *
+ * @param input - The character being input
+ * @returns boolean - true if valid numeric input, false otherwise
+ */
+const isNumericValue = input => {
+ // Check for null/undefined input or non-numeric characters
+ return input && /^\d+$/.test(input);
+};
+
+/**
+ * Validates if adding a new digit would exceed the 2-character limit
+ *
+ * @param currentValue - The current value in the field
+ * @returns boolean - true if at 2-character limit, false otherwise
+ */
+const isAtMaxLength = currentValue => {
+ return currentValue.length >= 2;
+};
+
+/**
* Converts a polar coordinate (angle on circle) into a percentage-based [x,y] position for clip-path
*
* @param cx
@@ -13264,13 +13285,9 @@ const FocusTimer = ({
const values = e.target.innerText.trim();
// only allow numerical digits 0–9 for time input
- if (!/^\d+$/.test(input)) {
- e.preventDefault();
- }
-
- // only allow 2 values each for minutes and seconds
- if (values.length >= 2) {
+ if (!isNumericValue(input)) {
e.preventDefault();
+ return;
}
const selection = window.getSelection();
const selectedText = selection.toString();
@@ -13288,6 +13305,12 @@ const FocusTimer = ({
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
+ return;
+ }
+
+ // only allow 2 values each for minutes and seconds
+ if (isAtMaxLength(values)) {
+ e.preventDefault();
}
};
const handleFocus = e => {
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
@@ -4,7 +4,11 @@ import { Provider } from "react-redux";
import { mount } from "enzyme";
import { INITIAL_STATE, reducers } from "common/Reducers.sys.mjs";
import { actionTypes as at } from "common/Actions.mjs";
-import { FocusTimer } from "content-src/components/Widgets/FocusTimer/FocusTimer";
+import {
+ FocusTimer,
+ isNumericValue,
+ isAtMaxLength,
+} from "content-src/components/Widgets/FocusTimer/FocusTimer";
const PREF_WIDGETS_SYSTEM_NOTIFICATIONS_ENABLED =
"widgets.focusTimer.showSystemNotifications";
@@ -564,4 +568,94 @@ describe("<FocusTimer>", () => {
assert.equal(action.type, at.OPEN_LINK);
});
});
+
+ // Tests for the focus timer input. It should only allow numbers
+ describe("isNumericValue", () => {
+ it("should return true for single digit numbers", () => {
+ assert.isTrue(isNumericValue("0"));
+ assert.isTrue(isNumericValue("1"));
+ assert.isTrue(isNumericValue("5"));
+ assert.isTrue(isNumericValue("9"));
+ });
+
+ it("should return true for multi-digit numbers", () => {
+ assert.isTrue(isNumericValue("10"));
+ assert.isTrue(isNumericValue("25"));
+ assert.isTrue(isNumericValue("99"));
+ });
+
+ it("should return false for non-numeric characters", () => {
+ assert.isFalse(isNumericValue("a"));
+ assert.isFalse(isNumericValue("Z"));
+ assert.isFalse(isNumericValue("!"));
+ assert.isFalse(isNumericValue("@"));
+ assert.isFalse(isNumericValue(" "));
+ });
+
+ it("should return false for special characters", () => {
+ assert.isFalse(isNumericValue("-"));
+ assert.isFalse(isNumericValue("+"));
+ assert.isFalse(isNumericValue("."));
+ assert.isFalse(isNumericValue(","));
+ });
+
+ it("should return false for mixed alphanumeric strings", () => {
+ assert.isFalse(isNumericValue("1a"));
+ assert.isFalse(isNumericValue("a1"));
+ assert.isFalse(isNumericValue("5x"));
+ });
+
+ it("should return false for empty string", () => {
+ assert.isFalse(isNumericValue(" "));
+ });
+ });
+
+ // Tests for the 2-character limit (enforces max 99 minutes, 59 seconds)
+ describe("isAtMaxLength", () => {
+ it("should return false for empty string", () => {
+ assert.isFalse(isAtMaxLength(""));
+ });
+
+ it("should return false for single character", () => {
+ assert.isFalse(isAtMaxLength("5"));
+ assert.isFalse(isAtMaxLength("9"));
+ });
+
+ it("should return true for 2 characters", () => {
+ assert.isTrue(isAtMaxLength("25"));
+ assert.isTrue(isAtMaxLength("99"));
+ assert.isTrue(isAtMaxLength("00"));
+ });
+
+ it("should return true for more than 2 characters", () => {
+ assert.isTrue(isAtMaxLength("123"));
+ assert.isTrue(isAtMaxLength("999"));
+ });
+ });
+
+ it("should clamp minutes to 99 and seconds to 59 when setting duration", () => {
+ // Find the editable fields
+ const minutes = wrapper.find(".timer-set-minutes").at(0);
+ const seconds = wrapper.find(".timer-set-seconds").at(0);
+
+ // Simulate user typing values beyond limits
+ minutes.getDOMNode().innerText = "100";
+ seconds.getDOMNode().innerText = "85";
+
+ // Trigger blur, which calls setTimerDuration()
+ seconds.simulate("blur");
+
+ // Clamp check
+ const clampedMinutes = Math.min(
+ parseInt(minutes.getDOMNode().innerText, 10),
+ 99
+ );
+ const clampedSeconds = Math.min(
+ parseInt(seconds.getDOMNode().innerText, 10),
+ 59
+ );
+
+ assert.equal(clampedMinutes, 99, "minutes should be clamped to 99");
+ assert.equal(clampedSeconds, 59, "seconds should be clamped to 59");
+ });
});