test: Started adding tests for window sync, b=no-bug, c=tests, workspaces

This commit is contained in:
mr. m
2026-01-06 01:23:20 +01:00
parent 75f4f0c3e6
commit 9820bd5772
9 changed files with 175 additions and 20 deletions

View File

@@ -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 || [],
});

View File

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

View File

@@ -13,6 +13,7 @@ BROWSER_CHROME_MANIFESTS += [
"ub-actions/browser.toml",
"urlbar/browser.toml",
"welcome/browser.toml",
"window_sync/browser.toml",
"workspaces/browser.toml",
]

View File

@@ -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"]

View File

@@ -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'
);
});
});

View File

@@ -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');
});
});

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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<String, bool> {