commit 2aba8a94991c5062a3c51605a236d47101f5010d
parent 9590957bfee52c1a836a135834eee255aa336905
Author: Jeremy Swinarton <jswinarton@mozilla.com>
Date: Fri, 14 Nov 2025 16:09:19 +0000
Bug 1994523: Proof-of-concept API and data store for Tab Notes r=sthompson,tabbrowser-reviewers
Differential Revision: https://phabricator.services.mozilla.com/D271783
Diffstat:
5 files changed, 151 insertions(+), 0 deletions(-)
diff --git a/browser/components/tabbrowser/TabNotes.sys.mjs b/browser/components/tabbrowser/TabNotes.sys.mjs
@@ -0,0 +1,79 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+export class TabNotesStorage {
+ /** @type {Map<URL, string>} */
+ #store;
+
+ constructor() {
+ this.reset();
+ }
+
+ /**
+ * Retrieve a note for a URL, if it exists.
+ *
+ * @param {URL} url
+ * The URL that the note is associated with
+ * @returns {string | undefined }
+ */
+ get(url) {
+ return this.#store.get(url);
+ }
+
+ /**
+ * Set a note for a URL.
+ *
+ * @param {URL} url
+ * The URL that the note should be associated with
+ * @param {string} note
+ * The note itself
+ * @returns {string}
+ * The actual note that was set after sanitization
+ */
+ set(url, note) {
+ let sanitized = this.#sanitizeInput(note);
+ this.#store.set(url, sanitized);
+ return sanitized;
+ }
+
+ /**
+ * Delete a note for a URL.
+ *
+ * @param {URL} url
+ * The URL of the note
+ * @returns {boolean}
+ * True if there was a note and it was deleted; false otherwise
+ */
+ delete(url) {
+ return this.#store.delete(url);
+ }
+
+ /**
+ * Check if a URL has a note.
+ *
+ * @param {URL} url
+ * The URL of the note
+ * @returns {boolean}
+ * True if a note is associated with this URL; false otherwise
+ */
+ has(url) {
+ return this.#store.has(url);
+ }
+
+ /**
+ * Clear all notes for all URLs.
+ *
+ * @returns {void}
+ */
+ reset() {
+ this.#store = new Map();
+ }
+
+ #sanitizeInput(value) {
+ return value.slice(0, 1000);
+ }
+}
+
+// Singleton object accessible from all windows
+export const TabNotes = new TabNotesStorage();
diff --git a/browser/components/tabbrowser/content/tabbrowser.js b/browser/components/tabbrowser/content/tabbrowser.js
@@ -110,6 +110,7 @@
"moz-src:///browser/components/newtab/SponsorProtection.sys.mjs",
TabMetrics:
"moz-src:///browser/components/tabbrowser/TabMetrics.sys.mjs",
+ TabNotes: "moz-src:///browser/components/tabbrowser/TabNotes.sys.mjs",
TabStateFlusher:
"resource:///modules/sessionstore/TabStateFlusher.sys.mjs",
TaskbarTabsUtils:
diff --git a/browser/components/tabbrowser/moz.build b/browser/components/tabbrowser/moz.build
@@ -14,6 +14,7 @@ MOZ_SRC_FILES += [
"OpenInTabsUtils.sys.mjs",
"SmartTabGrouping.sys.mjs",
"TabMetrics.sys.mjs",
+ "TabNotes.sys.mjs",
"TabsList.sys.mjs",
"TabUnloader.sys.mjs",
]
diff --git a/browser/components/tabbrowser/test/browser/tabs/browser.toml b/browser/components/tabbrowser/test/browser/tabs/browser.toml
@@ -579,6 +579,8 @@ tags = "vertical-tabs"
["browser_tab_move_to_new_window_reload.js"]
tags = "vertical-tabs"
+["browser_tab_notes.js"]
+
["browser_tab_play.js"]
tags = "vertical-tabs"
diff --git a/browser/components/tabbrowser/test/browser/tabs/browser_tab_notes.js b/browser/components/tabbrowser/test/browser/tabs/browser_tab_notes.js
@@ -0,0 +1,68 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.tabs.notes.enabled", true]],
+ });
+
+ registerCleanupFunction(async function () {
+ await SpecialPowers.popPrefEnv();
+ });
+});
+
+add_task(async function tabNotesBasicStorageTests() {
+ let url = "https://example.com/abc";
+ let value = "some note";
+
+ let result = gBrowser.TabNotes.set(url, value);
+ Assert.equal(result, value, "TabNotes.set returns note value");
+
+ Assert.equal(
+ gBrowser.TabNotes.has(url),
+ true,
+ "TabNotes.has indicates that URL has a note"
+ );
+
+ Assert.equal(
+ gBrowser.TabNotes.get(url),
+ value,
+ "TabNotes.get returns previously set note value"
+ );
+
+ let fgWindow = await BrowserTestUtils.openNewBrowserWindow();
+ Assert.equal(
+ fgWindow.gBrowser.TabNotes.get(url),
+ value,
+ "TabNotes.get returns previously set note value from any window"
+ );
+ await BrowserTestUtils.closeWindow(fgWindow);
+
+ gBrowser.TabNotes.delete(url);
+
+ Assert.equal(
+ gBrowser.TabNotes.has(url),
+ false,
+ "TabNotes.has indicates that the deleted URL no longer has a note"
+ );
+
+ Assert.equal(
+ gBrowser.TabNotes.get(url),
+ undefined,
+ "TabNotes.get returns undefined for URL that does not have a note"
+ );
+
+ gBrowser.TabNotes.reset();
+});
+
+add_task(async function tabNotesSanitizationTests() {
+ let url = "https://example.com/";
+ let tooLongValue = "x".repeat(1500);
+ let correctValue = "x".repeat(1000);
+
+ gBrowser.TabNotes.set(url, tooLongValue);
+ let result = gBrowser.TabNotes.get(url);
+
+ Assert.equal(result, correctValue, "TabNotes.set truncates note length");
+});