tor-browser

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

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:
Mbrowser/extensions/newtab/content-src/components/Widgets/FocusTimer/FocusTimer.jsx | 35+++++++++++++++++++++++++++++------
Mbrowser/extensions/newtab/data/content/activity-stream.bundle.js | 35+++++++++++++++++++++++++++++------
Mbrowser/extensions/newtab/test/unit/content-src/components/Widgets/FocusTimer.test.jsx | 96++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
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"); + }); });