From 9820bd577260e6d28254d6ffeb72c52da43ae400 Mon Sep 17 00:00:00 2001 From: "mr. m" Date: Tue, 6 Jan 2026 01:23:20 +0100 Subject: [PATCH] test: Started adding tests for window sync, b=no-bug, c=tests, workspaces --- .../sessionstore/ZenSessionManager.sys.mjs | 8 ++- src/zen/sessionstore/ZenWindowSync.sys.mjs | 64 +++++++++++++++---- src/zen/tests/moz.build | 1 + src/zen/tests/window_sync/browser.toml | 12 ++++ .../window_sync/browser_sync_tab_label.js | 39 +++++++++++ .../window_sync/browser_sync_tab_open.js | 14 ++++ src/zen/tests/window_sync/head.js | 47 ++++++++++++++ src/zen/workspaces/ZenWorkspaces.mjs | 8 ++- tools/ffprefs/src/main.rs | 2 +- 9 files changed, 175 insertions(+), 20 deletions(-) create mode 100644 src/zen/tests/window_sync/browser.toml create mode 100644 src/zen/tests/window_sync/browser_sync_tab_label.js create mode 100644 src/zen/tests/window_sync/browser_sync_tab_open.js create mode 100644 src/zen/tests/window_sync/head.js diff --git a/src/zen/sessionstore/ZenSessionManager.sys.mjs b/src/zen/sessionstore/ZenSessionManager.sys.mjs index 543b8e5af..745752c72 100644 --- a/src/zen/sessionstore/ZenSessionManager.sys.mjs +++ b/src/zen/sessionstore/ZenSessionManager.sys.mjs @@ -14,6 +14,7 @@ ChromeUtils.defineESModuleGetters(lazy, { SessionStore: 'resource:///modules/sessionstore/SessionStore.sys.mjs', SessionSaver: 'resource:///modules/sessionstore/SessionSaver.sys.mjs', setTimeout: 'resource://gre/modules/Timer.sys.mjs', + gWindowSyncEnabled: 'resource:///modules/zen/ZenWindowSync.sys.mjs', }); XPCOMUtils.defineLazyPreferenceGetter(lazy, 'gShouldLog', 'zen.session-store.log', true); @@ -125,6 +126,7 @@ export class nsZenSessionManager { */ async readFile() { try { + this.log('Reading Zen session file from disk'); let promises = []; promises.push(this.#file.load()); if (!Services.prefs.getBoolPref(MIGRATION_PREF, false)) { @@ -145,6 +147,7 @@ export class nsZenSessionManager { * The initial session state read from the session file. */ onFileRead(initialState) { + if (!lazy.gWindowSyncEnabled) return; // For the first time after migration, we restore the tabs // That where going to be restored by SessionStore. The sidebar // object will always be empty after migration because we haven't @@ -233,7 +236,7 @@ export class nsZenSessionManager { * @param state The current session state. */ saveState(state) { - if (!state?.windows?.length) { + if (!state?.windows?.length || !lazy.gWindowSyncEnabled) { // Don't save (or even collect) anything in permanent private // browsing mode. We also don't want to save if there are no windows. return; @@ -350,7 +353,7 @@ export class nsZenSessionManager { * Whether this new window is being restored from a closed window. */ restoreNewWindow(aWindow, SessionStoreInternal, fromClosedWindow = false) { - if (aWindow.gZenWorkspaces?.privateWindowOrDisabled) { + if (aWindow.gZenWorkspaces?.privateWindowOrDisabled || !lazy.gWindowSyncEnabled) { return; } this.log('Restoring new window with Zen session data'); @@ -401,6 +404,7 @@ export class nsZenSessionManager { * @returns */ onNewEmptySession(aWindow) { + this.log('Restoring empty session with Zen session data'); aWindow.gZenWorkspaces.restoreWorkspacesFromSessionStore({ spaces: this.#sidebar.spaces || [], }); diff --git a/src/zen/sessionstore/ZenWindowSync.sys.mjs b/src/zen/sessionstore/ZenWindowSync.sys.mjs index 61808b8d4..8758f9753 100644 --- a/src/zen/sessionstore/ZenWindowSync.sys.mjs +++ b/src/zen/sessionstore/ZenWindowSync.sys.mjs @@ -68,6 +68,12 @@ class nsZenWindowSync { lastHandlerPromise: Promise.resolve(), }; + /** + * Map of sync handlers for different event types. + * Each handler is a function that takes the event as an argument. + */ + #syncHandlers = new Set(); + /** * Last focused window. * Used to determine which window to sync tab contents visibility from. @@ -280,6 +286,25 @@ class nsZenWindowSync { }); } + /** + * Adds a sync handler for a specific event type. + * @param {Function} aHandler - The sync handler function to add. + */ + addSyncHandler(aHandler) { + if (!aHandler || this.#syncHandlers.has(aHandler)) { + return; + } + this.#syncHandlers.add(aHandler); + } + + /** + * Removes a sync handler for a specific event type. + * @param {Function} aHandler - The sync handler function to remove. + */ + removeSyncHandler(aHandler) { + this.#syncHandlers.delete(aHandler); + } + /** * Handles the next event by calling the appropriate handler method. * @@ -289,7 +314,17 @@ class nsZenWindowSync { const handler = `on_${aEvent.type}`; try { if (typeof this[handler] === 'function') { - return this[handler](aEvent) || Promise.resolve(); + let promise = this[handler](aEvent) || Promise.resolve(); + promise.then(() => { + for (let syncHandler of this.#syncHandlers) { + try { + syncHandler(aEvent); + } catch (e) { + console.error(e); + } + } + }); + return promise; } else { throw new Error(`No handler for event type: ${aEvent.type}`); } @@ -308,7 +343,7 @@ class nsZenWindowSync { } let permanentKey = aTab.linkedBrowser.permanentKey; this.#runOnAllWindows(null, (win) => { - const tab = this.#getItemFromWindow(win, aTab.id); + const tab = this.getItemFromWindow(win, aTab.id); if (tab) { tab.linkedBrowser.permanentKey = permanentKey; tab.permanentKey = permanentKey; @@ -323,7 +358,7 @@ class nsZenWindowSync { * @param {string} aItemId - The ID of the item to retrieve. * @returns {MozTabbrowserTab|MozTabbrowserTabGroup|null} The item element if found, otherwise null. */ - #getItemFromWindow(aWindow, aItemId) { + getItemFromWindow(aWindow, aItemId) { if (!aItemId) { return null; } @@ -453,7 +488,7 @@ class nsZenWindowSync { let container; const parentGroup = aOriginalItem.group; if (parentGroup?.hasAttribute('id')) { - container = this.#getItemFromWindow(aWindow, parentGroup.getAttribute('id')); + container = this.getItemFromWindow(aWindow, parentGroup.getAttribute('id')); if (container) { if (container?.tabs?.length) { // First tab in folders is the empty tab placeholder. @@ -480,7 +515,7 @@ class nsZenWindowSync { } return; } - const relativeTab = this.#getItemFromWindow(aWindow, originalSibling.id); + const relativeTab = this.getItemFromWindow(aWindow, originalSibling.id); if (relativeTab) { gBrowser.tabContainer.tabDragAndDrop.handle_drop_transition( relativeTab, @@ -502,7 +537,7 @@ class nsZenWindowSync { #syncItemForAllWindows(aItem, flags = 0) { const window = aItem.ownerGlobal; this.#runOnAllWindows(window, (win) => { - this.#syncItemWithOriginal(aItem, this.#getItemFromWindow(win, aItem.id), win, flags); + this.#syncItemWithOriginal(aItem, this.getItemFromWindow(win, aItem.id), win, flags); }); } @@ -713,7 +748,7 @@ class nsZenWindowSync { */ #getActiveTabFromOtherWindows(aWindow, aTabId, filter = (tab) => tab?._zenContentsVisible) { return this.#runOnAllWindows(aWindow, (win) => { - const tab = this.#getItemFromWindow(win, aTabId); + const tab = this.getItemFromWindow(win, aTabId); if (filter(tab)) { return tab; } @@ -735,7 +770,7 @@ class nsZenWindowSync { (tab) => tab._zenContentsVisible ); for (let tab of activeTabsOnClosedWindow) { - const targetTab = this.#getItemFromWindow(mostRecentWindow, tab.id); + const targetTab = this.getItemFromWindow(mostRecentWindow, tab.id); if (targetTab) { targetTab._zenContentsVisible = true; this.log(`Moving active tab ${tab.id} to most recent window on close`); @@ -830,7 +865,7 @@ class nsZenWindowSync { image: state.image, }; this.#runOnAllWindows(null, (win) => { - const targetTab = this.#getItemFromWindow(win, aTab.id); + const targetTab = this.getItemFromWindow(win, aTab.id); if (targetTab) { targetTab._zenPinnedInitialState = initialState; } @@ -958,7 +993,7 @@ class nsZenWindowSync { on_TabUnpinned(aEvent) { const tab = aEvent.target; this.#runOnAllWindows(null, (win) => { - const targetTab = this.#getItemFromWindow(win, tab.id); + const targetTab = this.getItemFromWindow(win, tab.id); if (targetTab) { delete targetTab._zenPinnedInitialState; } @@ -978,7 +1013,7 @@ class nsZenWindowSync { const tab = aEvent.target; const window = tab.ownerGlobal; this.#runOnAllWindows(window, (win) => { - const targetTab = this.#getItemFromWindow(win, tab.id); + const targetTab = this.getItemFromWindow(win, tab.id); if (targetTab) { win.gBrowser.removeTab(targetTab, { animate: true }); } @@ -1052,7 +1087,7 @@ class nsZenWindowSync { const tabGroup = aEvent.target; const window = tabGroup.ownerGlobal; this.#runOnAllWindows(window, (win) => { - const targetGroup = this.#getItemFromWindow(win, tabGroup.id); + const targetGroup = this.getItemFromWindow(win, tabGroup.id); if (targetGroup) { if (targetGroup.isZenFolder) { targetGroup.delete(); @@ -1075,7 +1110,7 @@ class nsZenWindowSync { const tab = aEvent.target; const window = tab.ownerGlobal; this.#runOnAllWindows(window, (win) => { - const targetTab = this.#getItemFromWindow(win, tab.id); + const targetTab = this.getItemFromWindow(win, tab.id); if (targetTab && win.gZenViewSplitter) { win.gZenViewSplitter.removeTabFromGroup(targetTab); } @@ -1088,7 +1123,7 @@ class nsZenWindowSync { const tabs = tabGroup.tabs; this.#runOnAllWindows(window, (win) => { const otherWindowTabs = tabs - .map((tab) => this.#getItemFromWindow(win, tab.id)) + .map((tab) => this.getItemFromWindow(win, tab.id)) .filter(Boolean); if (otherWindowTabs.length > 0 && win.gZenViewSplitter) { const group = win.gZenViewSplitter.splitTabs(otherWindowTabs, 'grid', -1); @@ -1104,4 +1139,5 @@ class nsZenWindowSync { } } +export const gWindowSyncEnabled = lazy.gWindowSyncEnabled; export const ZenWindowSync = new nsZenWindowSync(); diff --git a/src/zen/tests/moz.build b/src/zen/tests/moz.build index d857475e7..c8c60de2c 100644 --- a/src/zen/tests/moz.build +++ b/src/zen/tests/moz.build @@ -13,6 +13,7 @@ BROWSER_CHROME_MANIFESTS += [ "ub-actions/browser.toml", "urlbar/browser.toml", "welcome/browser.toml", + "window_sync/browser.toml", "workspaces/browser.toml", ] diff --git a/src/zen/tests/window_sync/browser.toml b/src/zen/tests/window_sync/browser.toml new file mode 100644 index 000000000..a25425af3 --- /dev/null +++ b/src/zen/tests/window_sync/browser.toml @@ -0,0 +1,12 @@ +# 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/. + +[DEFAULT] +prefs = ["zen.window-sync.enabled=true", "zen.urlbar.replace-newtab=false"] +support-files = [ + "head.js", +] + +["browser_sync_tab_open.js"] +["browser_sync_tab_label.js"] diff --git a/src/zen/tests/window_sync/browser_sync_tab_label.js b/src/zen/tests/window_sync/browser_sync_tab_label.js new file mode 100644 index 000000000..2aaa54dee --- /dev/null +++ b/src/zen/tests/window_sync/browser_sync_tab_label.js @@ -0,0 +1,39 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +'use strict'; + +add_task(async function test_SimpleLabelChange() { + let newLabel = 'Test Label'; + await withNewTabAndWindow(async (newTab, win) => { + let otherTab = gZenWindowSync.getItemFromWindow(win, newTab.id); + await runSyncAction( + () => { + gBrowser._setTabLabel(newTab, newLabel); + Assert.equal(newTab.label, newLabel, 'The original tab label should be changed'); + }, + async () => { + Assert.equal( + otherTab.label, + newLabel, + 'The synced tab label should match the changed label' + ); + }, + 'ZenTabLabelChanged' + ); + }); +}); + +add_task(async function test_DontChangeBluredTabLabel() { + let newLabel = 'Test Label'; + await withNewTabAndWindow(async (newTab, win) => { + let otherTab = gZenWindowSync.getItemFromWindow(win, newTab.id); + Assert.ok(!otherTab._zenContentsVisible, 'The synced tab should be blured'); + gBrowser._setTabLabel(newTab, newLabel); + Assert.notEqual( + otherTab.label, + newLabel, + 'The synced tab label should NOT match the changed label' + ); + }); +}); diff --git a/src/zen/tests/window_sync/browser_sync_tab_open.js b/src/zen/tests/window_sync/browser_sync_tab_open.js new file mode 100644 index 000000000..8d5fc1ff9 --- /dev/null +++ b/src/zen/tests/window_sync/browser_sync_tab_open.js @@ -0,0 +1,14 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +'use strict'; + +add_task(async function test_SimpleTabOpen() { + await withNewTabAndWindow(async (newTab, win) => { + let tabId = newTab.id; + let otherTab = gZenWindowSync.getItemFromWindow(win, tabId); + Assert.ok(otherTab, 'The opened tab should be found in the synced window'); + Assert.ok(newTab._zenContentsVisible, 'The opened tab should be visible'); + Assert.equal(otherTab.id, tabId, 'The opened tab ID should match the synced tab ID'); + }); +}); diff --git a/src/zen/tests/window_sync/head.js b/src/zen/tests/window_sync/head.js new file mode 100644 index 000000000..4ad9d8219 --- /dev/null +++ b/src/zen/tests/window_sync/head.js @@ -0,0 +1,47 @@ +/* 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/. */ + +async function withNewSyncedWindow(action) { + await gZenWorkspaces.promiseInitialized; + const win = await BrowserTestUtils.openNewBrowserWindow(); + await win.gZenWorkspaces.promiseInitialized; + await action(win); + await BrowserTestUtils.closeWindow(win); +} + +async function runSyncAction(action, callback, type) { + await new Promise((resolve) => { + window.gZenWindowSync.addSyncHandler(async function handler(aEvent) { + if (aEvent.type === type) { + window.gZenWindowSync.removeSyncHandler(handler); + await callback(aEvent); + resolve(); + } + }); + action(); + }); +} + +function getTabState(tab) { + return JSON.parse(SessionStore.getTabState(tab)); +} + +async function withNewTabAndWindow(action) { + let newTab = null; + await withNewSyncedWindow(async (win) => { + await runSyncAction( + () => { + newTab = gBrowser.addTrustedTab('https://example.com/', { inBackground: true }); + }, + async (aEvent) => { + Assert.equal(aEvent.type, 'TabOpen', 'Event type should be TabOpen'); + await action(newTab, win); + }, + 'TabOpen' + ); + }); + let portalTabClosing = BrowserTestUtils.waitForTabClosing(newTab); + BrowserTestUtils.removeTab(newTab); + await portalTabClosing; +} diff --git a/src/zen/workspaces/ZenWorkspaces.mjs b/src/zen/workspaces/ZenWorkspaces.mjs index 26d79cc8b..32bdc8fe9 100644 --- a/src/zen/workspaces/ZenWorkspaces.mjs +++ b/src/zen/workspaces/ZenWorkspaces.mjs @@ -848,7 +848,7 @@ class nsZenWorkspaces { const spacesFromStore = aWinData.spaces || []; this._workspaceCache = spacesFromStore.length ? [...spacesFromStore] - : [this.#createWorkspaceData('Space', undefined, true)]; + : [this.#createWorkspaceData('Space', undefined)]; this.activeWorkspace = aWinData.activeZenSpace || this._workspaceCache[0].uuid; let promise = this.#initializeWorkspaces(); for (const workspace of spacesFromStore) { @@ -912,7 +912,7 @@ class nsZenWorkspaces { } async selectStartPage() { - if (!this.workspaceEnabled) { + if (!this.workspaceEnabled || gZenUIManager.testingEnabled) { return; } await this.promiseInitialized; @@ -1615,7 +1615,9 @@ class nsZenWorkspaces { // Second pass: Handle tab selection this.tabContainer._invalidateCachedTabs(); const tabToSelect = await this._handleTabSelection(workspace, onInit, previousWorkspace.uuid); - gBrowser.warmupTab(tabToSelect); + if (tabToSelect.linkedBrowser) { + gBrowser.warmupTab(tabToSelect); + } // Update UI and state const previousWorkspaceIndex = workspaces.findIndex((w) => w.uuid === previousWorkspace.uuid); diff --git a/tools/ffprefs/src/main.rs b/tools/ffprefs/src/main.rs index e2c7259e6..ac11c8e5b 100644 --- a/tools/ffprefs/src/main.rs +++ b/tools/ffprefs/src/main.rs @@ -326,7 +326,7 @@ fn is_twilight_build() -> bool { if let Ok(content) = fs::read_to_string(&dynamic_config_path) { return !content.contains("\"release\""); } - false + true } fn get_env_values() -> HashMap {