From b89535b8bf55ca0daf63a4b658521ef1e6fe33fc Mon Sep 17 00:00:00 2001 From: Vedaant Rajoo Date: Mon, 16 Mar 2026 20:27:46 -0700 Subject: [PATCH 1/6] 12740: remove deleted workspace tabs from switch-to-tab --- src/zen/spaces/ZenSpaceManager.mjs | 25 ++- src/zen/tests/workspaces/browser.toml | 2 + .../workspaces/browser_workspace_unload.js | 162 +++++++++++++----- 3 files changed, 146 insertions(+), 43 deletions(-) diff --git a/src/zen/spaces/ZenSpaceManager.mjs b/src/zen/spaces/ZenSpaceManager.mjs index 54e5013d12..3b2a86e070 100644 --- a/src/zen/spaces/ZenSpaceManager.mjs +++ b/src/zen/spaces/ZenSpaceManager.mjs @@ -1261,12 +1261,14 @@ class nsZenWorkspaces { } removeWorkspace(windowID) { + this.#deleteWorkspaceOwnedTabs(windowID); let workspacesData = this.getWorkspaces(); // Remove the workspace from the cache workspacesData = workspacesData.filter( workspace => workspace.uuid !== windowID ); this.#propagateWorkspaceData(workspacesData); + this._allStoredTabs = null; } isWorkspaceActive(workspace) { @@ -1438,6 +1440,15 @@ class nsZenWorkspaces { ); } + #workspaceOwnedTabs(workspaceID) { + return gBrowser.tabs.filter( + tab => + tab.getAttribute("zen-workspace-id") === workspaceID && + !tab.hasAttribute("zen-essential") && + !tab.hasAttribute("zen-empty-tab") + ); + } + #getClosableTabs(tabs) { const remainingTabs = tabs.filter(tab => { const attributes = [ @@ -1472,6 +1483,18 @@ class nsZenWorkspaces { }); } + #deleteWorkspaceOwnedTabs(workspaceID) { + const tabs = this.#workspaceOwnedTabs(workspaceID); + if (!tabs.length) { + return; + } + + gBrowser.removeTabs(tabs, { + closeWindowWithLastTab: false, + }); + this._allStoredTabs = null; + } + async unloadWorkspace() { const workspaceId = this.#contextMenuData?.workspaceId || this.activeWorkspace; @@ -1659,7 +1682,7 @@ class nsZenWorkspaces { onInit, previousWorkspace.uuid ); - if (tabToSelect.linkedBrowser) { + if (tabToSelect?.linkedBrowser) { gBrowser.warmupTab(tabToSelect); } diff --git a/src/zen/tests/workspaces/browser.toml b/src/zen/tests/workspaces/browser.toml index ac77baa441..9e2f7ee4f5 100644 --- a/src/zen/tests/workspaces/browser.toml +++ b/src/zen/tests/workspaces/browser.toml @@ -25,3 +25,5 @@ support-files = [ ["browser_private_mode_startup.js"] ["browser_workspace_bookmarks.js"] + +["browser_workspace_unload.js"] diff --git a/src/zen/tests/workspaces/browser_workspace_unload.js b/src/zen/tests/workspaces/browser_workspace_unload.js index ec3954962d..28feff28e0 100644 --- a/src/zen/tests/workspaces/browser_workspace_unload.js +++ b/src/zen/tests/workspaces/browser_workspace_unload.js @@ -3,19 +3,50 @@ "use strict"; +async function waitForActiveWorkspace(workspaceId) { + await BrowserTestUtils.waitForCondition( + () => gZenWorkspaces.activeWorkspace === workspaceId, + `Workspace ${workspaceId} should become active` + ); +} + +async function openWorkspaceTab(workspaceId, title) { + await gZenWorkspaces.changeWorkspaceWithID(workspaceId); + await waitForActiveWorkspace(workspaceId); + const tab = await BrowserTestUtils.openNewForegroundTab( + window.gBrowser, + `data:text/html,${title}`, + true, + { skipAnimation: true } + ); + gZenWorkspaces.moveTabToWorkspace(tab, workspaceId); + return tab; +} + +function removeNonWorkspaceTabs(excludedTabs = []) { + const excluded = new Set(excludedTabs); + for (const tab of [...gBrowser.tabs]) { + if (excluded.has(tab)) { + continue; + } + if ( + !tab.hasAttribute("zen-workspace-id") && + !tab.hasAttribute("zen-empty-tab") + ) { + BrowserTestUtils.removeTab(tab); + } + } +} + // verify that workspace unloading works add_task(async function test_UnloadWorkspace_WithMultipleTabs() { - const workspaceId = + const workspace = await gZenWorkspaces.createAndSaveWorkspace("Test Workspace 1"); + const workspaceId = workspace.uuid; + await waitForActiveWorkspace(workspaceId); const tabs = []; for (let i = 0; i < 3; i++) { - const tab = await BrowserTestUtils.openNewForegroundTab( - window.gBrowser, - `data:text/html,Workspace Tab ${i}`, - true, - { skipAnimation: true } - ); - tab.setAttribute("zen-workspace-id", workspaceId); + const tab = await openWorkspaceTab(workspaceId, `Workspace Tab ${i}`); tabs.push(tab); } @@ -36,25 +67,18 @@ add_task(async function test_UnloadWorkspace_WithMultipleTabs() { // verify that essential tabs are not unloaded add_task(async function test_UnloadWorkspace_WithEssentialTabs() { - const workspaceId = - await gZenWorkspaces.createAndSaveWorkspace("Test Workspace 2"); - - const regularTab = await BrowserTestUtils.openNewForegroundTab( - window.gBrowser, - "data:text/html,Hi! I am a Regular Tab", - true, - { skipAnimation: true } + const workspace = + await gZenWorkspaces.createAndSaveWorkspace("Test Workspace 2", undefined, true); + const workspaceId = workspace.uuid; + await gZenWorkspaces.changeWorkspaceWithID(workspaceId); + await waitForActiveWorkspace(workspaceId); + + const regularTab = await openWorkspaceTab(workspaceId, "Hi! I am a Regular Tab"); + const essentialTab = await openWorkspaceTab( + workspaceId, + "Hi! I am an Essential Tab" ); - regularTab.setAttribute("zen-workspace-id", workspaceId); - - const essentialTab = await BrowserTestUtils.openNewForegroundTab( - window.gBrowser, - "data:text/html,Hi! I am an Essential Tab", - true, - { skipAnimation: true } - ); - essentialTab.setAttribute("zen-workspace-id", workspaceId); - essentialTab.setAttribute("zen-essential", "true"); + gZenPinnedTabManager.addToEssentials(essentialTab); await gZenWorkspaces.unloadWorkspace(); @@ -66,40 +90,47 @@ add_task(async function test_UnloadWorkspace_WithEssentialTabs() { "Essential tab should not be unloaded" ); ok(essentialTab.linkedPanel, "Essential tab should still have linked panel"); + ok( + !essentialTab.hasAttribute("zen-workspace-id"), + "Essential tab should not stay attached to the workspace" + ); await gZenWorkspaces.removeWorkspace(workspaceId); + removeNonWorkspaceTabs(); }); // only tabs from the targeted workspace should be unloaded add_task(async function test_UnloadWorkspace_TargetedWorkspaceIsolation() { - const inActiveWorkspaceId = await gZenWorkspaces.createAndSaveWorkspace( - "Test In-Active Workspace" + const inActiveWorkspace = await gZenWorkspaces.createAndSaveWorkspace( + "Test In-Active Workspace", + undefined, + true ); - const activeWorkspaceId = await gZenWorkspaces.createAndSaveWorkspace( - "Test Active Workspace" + const activeWorkspace = await gZenWorkspaces.createAndSaveWorkspace( + "Test Active Workspace", + undefined, + true ); + const inActiveWorkspaceId = inActiveWorkspace.uuid; + const activeWorkspaceId = activeWorkspace.uuid; + await gZenWorkspaces.changeWorkspaceWithID(activeWorkspaceId); + await waitForActiveWorkspace(activeWorkspaceId); const inActiveWorkspaceTabs = []; for (let i = 0; i < 2; i++) { - const tab = await BrowserTestUtils.openNewForegroundTab( - window.gBrowser, - `data:text/html,In-Active Workspace Tab ${i}`, - true, - { skipAnimation: true } + const tab = await openWorkspaceTab( + inActiveWorkspaceId, + `In-Active Workspace Tab ${i}` ); - tab.setAttribute("zen-workspace-id", inActiveWorkspaceId); inActiveWorkspaceTabs.push(tab); } const activeWorkspaceTabs = []; for (let i = 0; i < 2; i++) { - const tab = await BrowserTestUtils.openNewForegroundTab( - window.gBrowser, - `data:text/html,Active Workspace Tab ${i}`, - true, - { skipAnimation: true } + const tab = await openWorkspaceTab( + activeWorkspaceId, + `Active Workspace Tab ${i}` ); - tab.setAttribute("zen-workspace-id", activeWorkspaceId); activeWorkspaceTabs.push(tab); } @@ -129,4 +160,51 @@ add_task(async function test_UnloadWorkspace_TargetedWorkspaceIsolation() { await gZenWorkspaces.removeWorkspace(inActiveWorkspaceId); await gZenWorkspaces.removeWorkspace(activeWorkspaceId); + removeNonWorkspaceTabs(); +}); + +add_task(async function test_DeleteWorkspace_RemovesWorkspaceOwnedTabs() { + const originalWorkspaceId = gZenWorkspaces.activeWorkspace; + const originalWorkspaceTab = await openWorkspaceTab( + originalWorkspaceId, + "Original Workspace Tab" + ); + + const deletedWorkspace = + await gZenWorkspaces.createAndSaveWorkspace( + "Workspace To Delete", + undefined, + true + ); + const deletedWorkspaceId = deletedWorkspace.uuid; + await gZenWorkspaces.changeWorkspaceWithID(deletedWorkspaceId); + await waitForActiveWorkspace(deletedWorkspaceId); + + const regularTab = await openWorkspaceTab(deletedWorkspaceId, "Delete Me Regular"); + const pinnedTab = await openWorkspaceTab(deletedWorkspaceId, "Delete Me Pinned"); + gBrowser.pinTab(pinnedTab); + gZenWorkspaces.moveTabToWorkspace(pinnedTab, deletedWorkspaceId); + + await gZenWorkspaces.removeWorkspace(deletedWorkspaceId); + + ok( + !gBrowser.tabs.includes(regularTab), + "Regular workspace-owned tab should be removed when deleting the workspace" + ); + ok( + !gBrowser.tabs.includes(pinnedTab), + "Pinned workspace-owned tab should be removed when deleting the workspace" + ); + ok( + gBrowser.tabs.includes(originalWorkspaceTab), + "Tab from the original workspace should remain after deleting another workspace" + ); + ok( + !gBrowser.tabs.some( + tab => tab.getAttribute("zen-workspace-id") === deletedWorkspaceId + ), + "No remaining tab should keep the deleted workspace id" + ); + + BrowserTestUtils.removeTab(originalWorkspaceTab); }); From 5dca4f5c9f67996d9daf20978ae034feaec189ff Mon Sep 17 00:00:00 2001 From: Vedaant Rajoo Date: Tue, 17 Mar 2026 21:14:45 -0700 Subject: [PATCH 2/6] fix: Remove unnecessary null assignment for stored tabs in ZenSpaceManager --- src/zen/spaces/ZenSpaceManager.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/zen/spaces/ZenSpaceManager.mjs b/src/zen/spaces/ZenSpaceManager.mjs index 3b2a86e070..11f232cc21 100644 --- a/src/zen/spaces/ZenSpaceManager.mjs +++ b/src/zen/spaces/ZenSpaceManager.mjs @@ -1492,7 +1492,6 @@ class nsZenWorkspaces { gBrowser.removeTabs(tabs, { closeWindowWithLastTab: false, }); - this._allStoredTabs = null; } async unloadWorkspace() { From 3f908dafbc4e8e1adeaa06504c7ed82e32fd0220 Mon Sep 17 00:00:00 2001 From: Vedaant Rajoo Date: Thu, 19 Mar 2026 18:01:58 -0700 Subject: [PATCH 3/6] gh-12740: implement folder deletion and fix tests --- src/zen/spaces/ZenSpaceManager.mjs | 19 +++++++++ .../workspaces/browser_workspace_unload.js | 40 ++++++++++++++++++- 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/zen/spaces/ZenSpaceManager.mjs b/src/zen/spaces/ZenSpaceManager.mjs index 11f232cc21..d8e3284c24 100644 --- a/src/zen/spaces/ZenSpaceManager.mjs +++ b/src/zen/spaces/ZenSpaceManager.mjs @@ -1261,6 +1261,7 @@ class nsZenWorkspaces { } removeWorkspace(windowID) { + this.#deleteWorkspaceFolders(windowID); this.#deleteWorkspaceOwnedTabs(windowID); let workspacesData = this.getWorkspaces(); // Remove the workspace from the cache @@ -1449,6 +1450,18 @@ class nsZenWorkspaces { ); } + #workspaceFolders(workspaceID) { + const workspaceElement = this.workspaceElement(workspaceID); + if (!workspaceElement) { + return []; + } + + return [ + ...workspaceElement.pinnedTabsContainer.children, + ...workspaceElement.tabsContainer.children, + ].filter(item => item?.isZenFolder); + } + #getClosableTabs(tabs) { const remainingTabs = tabs.filter(tab => { const attributes = [ @@ -1494,6 +1507,12 @@ class nsZenWorkspaces { }); } + #deleteWorkspaceFolders(workspaceID) { + for (const folder of this.#workspaceFolders(workspaceID)) { + folder.delete(); + } + } + async unloadWorkspace() { const workspaceId = this.#contextMenuData?.workspaceId || this.activeWorkspace; diff --git a/src/zen/tests/workspaces/browser_workspace_unload.js b/src/zen/tests/workspaces/browser_workspace_unload.js index 28feff28e0..73381ad5a9 100644 --- a/src/zen/tests/workspaces/browser_workspace_unload.js +++ b/src/zen/tests/workspaces/browser_workspace_unload.js @@ -23,6 +23,15 @@ async function openWorkspaceTab(workspaceId, title) { return tab; } +function waitForTabClose(tab) { + return BrowserTestUtils.waitForEvent( + window, + "TabClose", + false, + event => event.target === tab + ); +} + function removeNonWorkspaceTabs(excludedTabs = []) { const excluded = new Set(excludedTabs); for (const tab of [...gBrowser.tabs]) { @@ -184,8 +193,29 @@ add_task(async function test_DeleteWorkspace_RemovesWorkspaceOwnedTabs() { const pinnedTab = await openWorkspaceTab(deletedWorkspaceId, "Delete Me Pinned"); gBrowser.pinTab(pinnedTab); gZenWorkspaces.moveTabToWorkspace(pinnedTab, deletedWorkspaceId); + const folderTab = await openWorkspaceTab(deletedWorkspaceId, "Delete Me Folder"); + const folder = await gZenFolders.createFolder([folderTab], { + label: "Folder To Delete", + renameFolder: false, + workspaceId: deletedWorkspaceId, + }); + const folderEmptyTab = folder.tabs[0]; + + ok( + folderEmptyTab.hasAttribute("zen-empty-tab"), + "Workspace folder should create an empty placeholder tab" + ); - await gZenWorkspaces.removeWorkspace(deletedWorkspaceId); + const removalEvents = [ + waitForTabClose(regularTab), + waitForTabClose(pinnedTab), + waitForTabClose(folderTab), + waitForTabClose(folderEmptyTab), + BrowserTestUtils.waitForEvent(folder, "TabGroupRemoved"), + ]; + + gZenWorkspaces.removeWorkspace(deletedWorkspaceId); + await Promise.all(removalEvents); ok( !gBrowser.tabs.includes(regularTab), @@ -195,6 +225,14 @@ add_task(async function test_DeleteWorkspace_RemovesWorkspaceOwnedTabs() { !gBrowser.tabs.includes(pinnedTab), "Pinned workspace-owned tab should be removed when deleting the workspace" ); + ok( + !gBrowser.tabs.includes(folderTab), + "Folder tab should be removed when deleting the workspace" + ); + ok( + !gBrowser.tabs.includes(folderEmptyTab), + "Folder empty tab should be removed when deleting the workspace" + ); ok( gBrowser.tabs.includes(originalWorkspaceTab), "Tab from the original workspace should remain after deleting another workspace" From 832cd5031c969d43aed6e1fa760c20eed0ccd82a Mon Sep 17 00:00:00 2001 From: Vedaant Rajoo Date: Fri, 20 Mar 2026 11:19:03 -0700 Subject: [PATCH 4/6] gh-12740: Update ZenSpaceManager to use tab groups for workspace folders and improve workspace switching tests --- src/zen/spaces/ZenSpaceManager.mjs | 21 +++++++++---------- .../workspaces/browser_workspace_unload.js | 3 +++ 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/zen/spaces/ZenSpaceManager.mjs b/src/zen/spaces/ZenSpaceManager.mjs index d8e3284c24..b895bcedf2 100644 --- a/src/zen/spaces/ZenSpaceManager.mjs +++ b/src/zen/spaces/ZenSpaceManager.mjs @@ -1269,7 +1269,7 @@ class nsZenWorkspaces { workspace => workspace.uuid !== windowID ); this.#propagateWorkspaceData(workspacesData); - this._allStoredTabs = null; + gBrowser.tabContainer._invalidateCachedVisibleTabs(); } isWorkspaceActive(workspace) { @@ -1442,7 +1442,7 @@ class nsZenWorkspaces { } #workspaceOwnedTabs(workspaceID) { - return gBrowser.tabs.filter( + return this.allStoredTabs.filter( tab => tab.getAttribute("zen-workspace-id") === workspaceID && !tab.hasAttribute("zen-essential") && @@ -1451,15 +1451,14 @@ class nsZenWorkspaces { } #workspaceFolders(workspaceID) { - const workspaceElement = this.workspaceElement(workspaceID); - if (!workspaceElement) { - return []; - } - - return [ - ...workspaceElement.pinnedTabsContainer.children, - ...workspaceElement.tabsContainer.children, - ].filter(item => item?.isZenFolder); + return gBrowser.tabGroups.filter( + group => + group?.isZenFolder && + !group.group && + group.allItemsRecursive.some( + item => item.getAttribute("zen-workspace-id") === workspaceID + ) + ); } #getClosableTabs(tabs) { diff --git a/src/zen/tests/workspaces/browser_workspace_unload.js b/src/zen/tests/workspaces/browser_workspace_unload.js index 73381ad5a9..b8caf3d7e8 100644 --- a/src/zen/tests/workspaces/browser_workspace_unload.js +++ b/src/zen/tests/workspaces/browser_workspace_unload.js @@ -214,6 +214,9 @@ add_task(async function test_DeleteWorkspace_RemovesWorkspaceOwnedTabs() { BrowserTestUtils.waitForEvent(folder, "TabGroupRemoved"), ]; + await gZenWorkspaces.changeWorkspaceWithID(originalWorkspaceId); + await waitForActiveWorkspace(originalWorkspaceId); + gZenWorkspaces.removeWorkspace(deletedWorkspaceId); await Promise.all(removalEvents); From 6c3ad535c6437f703c21fc169af80ff9a411bf99 Mon Sep 17 00:00:00 2001 From: Vedaant Rajoo Date: Sat, 21 Mar 2026 15:36:36 -0700 Subject: [PATCH 5/6] Update src/zen/tests/workspaces/browser_workspace_unload.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Vedaant Rajoo --- src/zen/tests/workspaces/browser_workspace_unload.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zen/tests/workspaces/browser_workspace_unload.js b/src/zen/tests/workspaces/browser_workspace_unload.js index b8caf3d7e8..04b11ca82b 100644 --- a/src/zen/tests/workspaces/browser_workspace_unload.js +++ b/src/zen/tests/workspaces/browser_workspace_unload.js @@ -179,7 +179,7 @@ add_task(async function test_DeleteWorkspace_RemovesWorkspaceOwnedTabs() { "Original Workspace Tab" ); - const deletedWorkspace = + const deletedWorkspace = await gZenWorkspaces.createAndSaveWorkspace( "Workspace To Delete", undefined, From 964dc080f6d70f82a11f0c905e87961207667bb1 Mon Sep 17 00:00:00 2001 From: "mr. m" Date: Sun, 22 Mar 2026 16:50:21 +0100 Subject: [PATCH 6/6] no-bug: Clean up --- src/zen/spaces/ZenSpaceBookmarksStorage.js | 33 ++- src/zen/spaces/ZenSpaceManager.mjs | 47 ++-- src/zen/tests/moz.build | 2 +- .../tests/{workspaces => spaces}/browser.toml | 2 - .../browser_basic_workspaces.js | 2 +- .../browser_change_to_empty.js | 0 .../browser_double_click_newtab.js | 0 .../browser_issue_10455.js | 2 + .../browser_issue_8699.js | 0 .../browser_issue_9900.js | 0 .../browser_overflow_scrollbox.js | 0 .../browser_private_mode.js | 0 .../browser_private_mode_startup.js | 0 .../browser_workspace_bookmarks.js | 0 src/zen/tests/{workspaces => spaces}/head.js | 0 .../workspaces/browser_workspace_unload.js | 251 ------------------ 16 files changed, 43 insertions(+), 296 deletions(-) rename src/zen/tests/{workspaces => spaces}/browser.toml (94%) rename src/zen/tests/{workspaces => spaces}/browser_basic_workspaces.js (96%) rename src/zen/tests/{workspaces => spaces}/browser_change_to_empty.js (100%) rename src/zen/tests/{workspaces => spaces}/browser_double_click_newtab.js (100%) rename src/zen/tests/{workspaces => spaces}/browser_issue_10455.js (98%) rename src/zen/tests/{workspaces => spaces}/browser_issue_8699.js (100%) rename src/zen/tests/{workspaces => spaces}/browser_issue_9900.js (100%) rename src/zen/tests/{workspaces => spaces}/browser_overflow_scrollbox.js (100%) rename src/zen/tests/{workspaces => spaces}/browser_private_mode.js (100%) rename src/zen/tests/{workspaces => spaces}/browser_private_mode_startup.js (100%) rename src/zen/tests/{workspaces => spaces}/browser_workspace_bookmarks.js (100%) rename src/zen/tests/{workspaces => spaces}/head.js (100%) delete mode 100644 src/zen/tests/workspaces/browser_workspace_unload.js diff --git a/src/zen/spaces/ZenSpaceBookmarksStorage.js b/src/zen/spaces/ZenSpaceBookmarksStorage.js index e9d94e06c6..f4e577370f 100644 --- a/src/zen/spaces/ZenSpaceBookmarksStorage.js +++ b/src/zen/spaces/ZenSpaceBookmarksStorage.js @@ -13,6 +13,9 @@ window.ZenWorkspaceBookmarksStorage = { if (!window.gZenWorkspaces) { return; } + this.promiseInitialized = new Promise(resolve => { + this._resolveInitialized = resolve; + }); await this._ensureTable(); }, @@ -54,9 +57,12 @@ window.ZenWorkspaceBookmarksStorage = { // Create index for changes tracking await db.execute(` - CREATE INDEX IF NOT EXISTS idx_bookmarks_workspaces_changes - ON zen_bookmarks_workspaces_changes(bookmark_guid, workspace_uuid) - `); + CREATE INDEX IF NOT EXISTS idx_bookmarks_workspaces_changes + ON zen_bookmarks_workspaces_changes(bookmark_guid, workspace_uuid) + `); + + this._resolveInitialized(); + delete this._resolveInitialized; } ); }, @@ -68,6 +74,7 @@ window.ZenWorkspaceBookmarksStorage = { */ async updateLastChangeTimestamp(db) { const now = Date.now(); + await this.promiseInitialized; await db.execute( ` INSERT OR REPLACE INTO moz_meta (key, value) @@ -84,6 +91,7 @@ window.ZenWorkspaceBookmarksStorage = { */ async getLastChangeTimestamp() { const db = await this.lazy.PlacesUtils.promiseDBConnection(); + await this.promiseInitialized; const result = await db.executeCached(` SELECT value FROM moz_meta WHERE key = 'zen_bookmarks_workspaces_last_change' `); @@ -91,16 +99,21 @@ window.ZenWorkspaceBookmarksStorage = { }, async getBookmarkWorkspaces(bookmarkGuid) { + await this.promiseInitialized; const db = await this.lazy.PlacesUtils.promiseDBConnection(); - - const rows = await db.execute( - ` + let rows = []; + try { + rows = await db.execute( + ` SELECT workspace_uuid FROM zen_bookmarks_workspaces WHERE bookmark_guid = :bookmark_guid `, - { bookmark_guid: bookmarkGuid } - ); + { bookmark_guid: bookmarkGuid } + ); + } catch (e) { + console.error("Error fetching bookmark workspaces:", e); + } return rows.map(row => row.getResultByName("workspace_uuid")); }, @@ -117,8 +130,8 @@ window.ZenWorkspaceBookmarksStorage = { * } */ async getBookmarkGuidsByWorkspace() { + await this.promiseInitialized; const db = await this.lazy.PlacesUtils.promiseDBConnection(); - const rows = await db.execute(` SELECT workspace_uuid, GROUP_CONCAT(bookmark_guid) as bookmark_guids FROM zen_bookmarks_workspaces @@ -141,6 +154,7 @@ window.ZenWorkspaceBookmarksStorage = { * @returns {Promise} An object mapping bookmark+workspace pairs to their change data. */ async getChangedIDs() { + await this.promiseInitialized; const db = await this.lazy.PlacesUtils.promiseDBConnection(); const rows = await db.execute(` SELECT bookmark_guid, workspace_uuid, change_type, timestamp @@ -162,6 +176,7 @@ window.ZenWorkspaceBookmarksStorage = { * Clear all recorded changes. */ async clearChangedIDs() { + await this.promiseInitialized; await this.lazy.PlacesUtils.withConnectionWrapper( "ZenWorkspaceBookmarksStorage.clearChangedIDs", async db => { diff --git a/src/zen/spaces/ZenSpaceManager.mjs b/src/zen/spaces/ZenSpaceManager.mjs index d985b11145..e1d619ab88 100644 --- a/src/zen/spaces/ZenSpaceManager.mjs +++ b/src/zen/spaces/ZenSpaceManager.mjs @@ -1261,15 +1261,23 @@ class nsZenWorkspaces { } removeWorkspace(windowID) { - this.#deleteWorkspaceFolders(windowID); + let { promise, resolve } = Promise.withResolvers(); this.#deleteWorkspaceOwnedTabs(windowID); let workspacesData = this.getWorkspaces(); // Remove the workspace from the cache workspacesData = workspacesData.filter( workspace => workspace.uuid !== windowID ); + window.addEventListener( + "ZenWorkspacesUIUpdate", + () => { + resolve(); + }, + { once: true } + ); this.#propagateWorkspaceData(workspacesData); gBrowser.tabContainer._invalidateCachedVisibleTabs(); + return promise; } isWorkspaceActive(workspace) { @@ -1441,26 +1449,6 @@ class nsZenWorkspaces { ); } - #workspaceOwnedTabs(workspaceID) { - return this.allStoredTabs.filter( - tab => - tab.getAttribute("zen-workspace-id") === workspaceID && - !tab.hasAttribute("zen-essential") && - !tab.hasAttribute("zen-empty-tab") - ); - } - - #workspaceFolders(workspaceID) { - return gBrowser.tabGroups.filter( - group => - group?.isZenFolder && - !group.group && - group.allItemsRecursive.some( - item => item.getAttribute("zen-workspace-id") === workspaceID - ) - ); - } - #getClosableTabs(tabs) { const remainingTabs = tabs.filter(tab => { const attributes = [ @@ -1496,22 +1484,17 @@ class nsZenWorkspaces { } #deleteWorkspaceOwnedTabs(workspaceID) { - const tabs = this.#workspaceOwnedTabs(workspaceID); - if (!tabs.length) { - return; - } - + const tabs = this.allStoredTabs.filter( + tab => + tab.getAttribute("zen-workspace-id") === workspaceID && + !tab.hasAttribute("zen-essential") && + !(tab.hasAttribute("zen-empty-tab") && !tab.group) + ); gBrowser.removeTabs(tabs, { closeWindowWithLastTab: false, }); } - #deleteWorkspaceFolders(workspaceID) { - for (const folder of this.#workspaceFolders(workspaceID)) { - folder.delete(); - } - } - async unloadWorkspace() { const workspaceId = this.#contextMenuData?.workspaceId || this.activeWorkspace; diff --git a/src/zen/tests/moz.build b/src/zen/tests/moz.build index d8529fe677..17ebbab072 100644 --- a/src/zen/tests/moz.build +++ b/src/zen/tests/moz.build @@ -9,13 +9,13 @@ BROWSER_CHROME_MANIFESTS += [ "glance/browser.toml", "live-folders/browser.toml", "pinned/browser.toml", + "spaces/browser.toml", "split_view/browser.toml", "tabs/browser.toml", "ub-actions/browser.toml", "urlbar/browser.toml", "welcome/browser.toml", "window_sync/browser.toml", - "workspaces/browser.toml", ] DIRS += [ diff --git a/src/zen/tests/workspaces/browser.toml b/src/zen/tests/spaces/browser.toml similarity index 94% rename from src/zen/tests/workspaces/browser.toml rename to src/zen/tests/spaces/browser.toml index 9e2f7ee4f5..ac77baa441 100644 --- a/src/zen/tests/workspaces/browser.toml +++ b/src/zen/tests/spaces/browser.toml @@ -25,5 +25,3 @@ support-files = [ ["browser_private_mode_startup.js"] ["browser_workspace_bookmarks.js"] - -["browser_workspace_unload.js"] diff --git a/src/zen/tests/workspaces/browser_basic_workspaces.js b/src/zen/tests/spaces/browser_basic_workspaces.js similarity index 96% rename from src/zen/tests/workspaces/browser_basic_workspaces.js rename to src/zen/tests/spaces/browser_basic_workspaces.js index e3ec5f6e3a..c12a1bc3e8 100644 --- a/src/zen/tests/workspaces/browser_basic_workspaces.js +++ b/src/zen/tests/spaces/browser_basic_workspaces.js @@ -26,7 +26,7 @@ add_task(async function test_Check_Creation() { await gZenWorkspaces.removeWorkspace(gZenWorkspaces.activeWorkspace); const workspacesAfterRemove = gZenWorkspaces.getWorkspaces(); Assert.strictEqual( - workspacesAfterRemove.workspaces.length, + workspacesAfterRemove.length, 1, "One workspace should exist." ); diff --git a/src/zen/tests/workspaces/browser_change_to_empty.js b/src/zen/tests/spaces/browser_change_to_empty.js similarity index 100% rename from src/zen/tests/workspaces/browser_change_to_empty.js rename to src/zen/tests/spaces/browser_change_to_empty.js diff --git a/src/zen/tests/workspaces/browser_double_click_newtab.js b/src/zen/tests/spaces/browser_double_click_newtab.js similarity index 100% rename from src/zen/tests/workspaces/browser_double_click_newtab.js rename to src/zen/tests/spaces/browser_double_click_newtab.js diff --git a/src/zen/tests/workspaces/browser_issue_10455.js b/src/zen/tests/spaces/browser_issue_10455.js similarity index 98% rename from src/zen/tests/workspaces/browser_issue_10455.js rename to src/zen/tests/spaces/browser_issue_10455.js index a4cf4115df..bf6b6de6cf 100644 --- a/src/zen/tests/workspaces/browser_issue_10455.js +++ b/src/zen/tests/spaces/browser_issue_10455.js @@ -4,6 +4,7 @@ "use strict"; add_task(async function test_Issue_10455() { + debugger; await SpecialPowers.pushPrefEnv({ set: [["browser.tabs.closeWindowWithLastTab", true]], }); @@ -24,6 +25,7 @@ add_task(async function test_Issue_10455() { }); add_task(async function test_Issue_10455_Dont_Close() { + debugger; await SpecialPowers.pushPrefEnv({ set: [["browser.tabs.closeWindowWithLastTab", false]], }); diff --git a/src/zen/tests/workspaces/browser_issue_8699.js b/src/zen/tests/spaces/browser_issue_8699.js similarity index 100% rename from src/zen/tests/workspaces/browser_issue_8699.js rename to src/zen/tests/spaces/browser_issue_8699.js diff --git a/src/zen/tests/workspaces/browser_issue_9900.js b/src/zen/tests/spaces/browser_issue_9900.js similarity index 100% rename from src/zen/tests/workspaces/browser_issue_9900.js rename to src/zen/tests/spaces/browser_issue_9900.js diff --git a/src/zen/tests/workspaces/browser_overflow_scrollbox.js b/src/zen/tests/spaces/browser_overflow_scrollbox.js similarity index 100% rename from src/zen/tests/workspaces/browser_overflow_scrollbox.js rename to src/zen/tests/spaces/browser_overflow_scrollbox.js diff --git a/src/zen/tests/workspaces/browser_private_mode.js b/src/zen/tests/spaces/browser_private_mode.js similarity index 100% rename from src/zen/tests/workspaces/browser_private_mode.js rename to src/zen/tests/spaces/browser_private_mode.js diff --git a/src/zen/tests/workspaces/browser_private_mode_startup.js b/src/zen/tests/spaces/browser_private_mode_startup.js similarity index 100% rename from src/zen/tests/workspaces/browser_private_mode_startup.js rename to src/zen/tests/spaces/browser_private_mode_startup.js diff --git a/src/zen/tests/workspaces/browser_workspace_bookmarks.js b/src/zen/tests/spaces/browser_workspace_bookmarks.js similarity index 100% rename from src/zen/tests/workspaces/browser_workspace_bookmarks.js rename to src/zen/tests/spaces/browser_workspace_bookmarks.js diff --git a/src/zen/tests/workspaces/head.js b/src/zen/tests/spaces/head.js similarity index 100% rename from src/zen/tests/workspaces/head.js rename to src/zen/tests/spaces/head.js diff --git a/src/zen/tests/workspaces/browser_workspace_unload.js b/src/zen/tests/workspaces/browser_workspace_unload.js deleted file mode 100644 index 04b11ca82b..0000000000 --- a/src/zen/tests/workspaces/browser_workspace_unload.js +++ /dev/null @@ -1,251 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - https://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -async function waitForActiveWorkspace(workspaceId) { - await BrowserTestUtils.waitForCondition( - () => gZenWorkspaces.activeWorkspace === workspaceId, - `Workspace ${workspaceId} should become active` - ); -} - -async function openWorkspaceTab(workspaceId, title) { - await gZenWorkspaces.changeWorkspaceWithID(workspaceId); - await waitForActiveWorkspace(workspaceId); - const tab = await BrowserTestUtils.openNewForegroundTab( - window.gBrowser, - `data:text/html,${title}`, - true, - { skipAnimation: true } - ); - gZenWorkspaces.moveTabToWorkspace(tab, workspaceId); - return tab; -} - -function waitForTabClose(tab) { - return BrowserTestUtils.waitForEvent( - window, - "TabClose", - false, - event => event.target === tab - ); -} - -function removeNonWorkspaceTabs(excludedTabs = []) { - const excluded = new Set(excludedTabs); - for (const tab of [...gBrowser.tabs]) { - if (excluded.has(tab)) { - continue; - } - if ( - !tab.hasAttribute("zen-workspace-id") && - !tab.hasAttribute("zen-empty-tab") - ) { - BrowserTestUtils.removeTab(tab); - } - } -} - -// verify that workspace unloading works -add_task(async function test_UnloadWorkspace_WithMultipleTabs() { - const workspace = - await gZenWorkspaces.createAndSaveWorkspace("Test Workspace 1"); - const workspaceId = workspace.uuid; - await waitForActiveWorkspace(workspaceId); - const tabs = []; - for (let i = 0; i < 3; i++) { - const tab = await openWorkspaceTab(workspaceId, `Workspace Tab ${i}`); - tabs.push(tab); - } - - for (const tab of tabs) { - ok(!tab.hasAttribute("pending"), "Tab should not be pending before unload"); - ok(tab.linkedPanel, "Tab should have linked panel before unload"); - } - - await gZenWorkspaces.unloadWorkspace(); - - for (const tab of tabs) { - ok(tab.hasAttribute("pending"), "Tab should be pending after unload"); - ok(!tab.linkedPanel, "Tab should not have linked panel after unload"); - } - - await gZenWorkspaces.removeWorkspace(workspaceId); -}); - -// verify that essential tabs are not unloaded -add_task(async function test_UnloadWorkspace_WithEssentialTabs() { - const workspace = - await gZenWorkspaces.createAndSaveWorkspace("Test Workspace 2", undefined, true); - const workspaceId = workspace.uuid; - await gZenWorkspaces.changeWorkspaceWithID(workspaceId); - await waitForActiveWorkspace(workspaceId); - - const regularTab = await openWorkspaceTab(workspaceId, "Hi! I am a Regular Tab"); - const essentialTab = await openWorkspaceTab( - workspaceId, - "Hi! I am an Essential Tab" - ); - gZenPinnedTabManager.addToEssentials(essentialTab); - - await gZenWorkspaces.unloadWorkspace(); - - ok(regularTab.hasAttribute("pending"), "Regular tab should be unloaded"); - ok(!regularTab.linkedPanel, "Regular tab should not have linked panel"); - - ok( - !essentialTab.hasAttribute("pending"), - "Essential tab should not be unloaded" - ); - ok(essentialTab.linkedPanel, "Essential tab should still have linked panel"); - ok( - !essentialTab.hasAttribute("zen-workspace-id"), - "Essential tab should not stay attached to the workspace" - ); - - await gZenWorkspaces.removeWorkspace(workspaceId); - removeNonWorkspaceTabs(); -}); - -// only tabs from the targeted workspace should be unloaded -add_task(async function test_UnloadWorkspace_TargetedWorkspaceIsolation() { - const inActiveWorkspace = await gZenWorkspaces.createAndSaveWorkspace( - "Test In-Active Workspace", - undefined, - true - ); - const activeWorkspace = await gZenWorkspaces.createAndSaveWorkspace( - "Test Active Workspace", - undefined, - true - ); - const inActiveWorkspaceId = inActiveWorkspace.uuid; - const activeWorkspaceId = activeWorkspace.uuid; - await gZenWorkspaces.changeWorkspaceWithID(activeWorkspaceId); - await waitForActiveWorkspace(activeWorkspaceId); - - const inActiveWorkspaceTabs = []; - for (let i = 0; i < 2; i++) { - const tab = await openWorkspaceTab( - inActiveWorkspaceId, - `In-Active Workspace Tab ${i}` - ); - inActiveWorkspaceTabs.push(tab); - } - - const activeWorkspaceTabs = []; - for (let i = 0; i < 2; i++) { - const tab = await openWorkspaceTab( - activeWorkspaceId, - `Active Workspace Tab ${i}` - ); - activeWorkspaceTabs.push(tab); - } - - await gZenWorkspaces.unloadWorkspace(); // this unloads the latest created workspace -> activeWorkspaceId - - for (const tab of activeWorkspaceTabs) { - ok( - tab.hasAttribute("pending"), - "Active workspace tab should be pending after unload" - ); - ok( - !tab.linkedPanel, - "Active workspace tab should not have linked panel after unload" - ); - } - - for (const tab of inActiveWorkspaceTabs) { - ok( - !tab.hasAttribute("pending"), - "In-Active workspace tab should NOT be pending after unload" - ); - ok( - tab.linkedPanel, - "In-Active workspace tab should still have linked panel after unload" - ); - } - - await gZenWorkspaces.removeWorkspace(inActiveWorkspaceId); - await gZenWorkspaces.removeWorkspace(activeWorkspaceId); - removeNonWorkspaceTabs(); -}); - -add_task(async function test_DeleteWorkspace_RemovesWorkspaceOwnedTabs() { - const originalWorkspaceId = gZenWorkspaces.activeWorkspace; - const originalWorkspaceTab = await openWorkspaceTab( - originalWorkspaceId, - "Original Workspace Tab" - ); - - const deletedWorkspace = - await gZenWorkspaces.createAndSaveWorkspace( - "Workspace To Delete", - undefined, - true - ); - const deletedWorkspaceId = deletedWorkspace.uuid; - await gZenWorkspaces.changeWorkspaceWithID(deletedWorkspaceId); - await waitForActiveWorkspace(deletedWorkspaceId); - - const regularTab = await openWorkspaceTab(deletedWorkspaceId, "Delete Me Regular"); - const pinnedTab = await openWorkspaceTab(deletedWorkspaceId, "Delete Me Pinned"); - gBrowser.pinTab(pinnedTab); - gZenWorkspaces.moveTabToWorkspace(pinnedTab, deletedWorkspaceId); - const folderTab = await openWorkspaceTab(deletedWorkspaceId, "Delete Me Folder"); - const folder = await gZenFolders.createFolder([folderTab], { - label: "Folder To Delete", - renameFolder: false, - workspaceId: deletedWorkspaceId, - }); - const folderEmptyTab = folder.tabs[0]; - - ok( - folderEmptyTab.hasAttribute("zen-empty-tab"), - "Workspace folder should create an empty placeholder tab" - ); - - const removalEvents = [ - waitForTabClose(regularTab), - waitForTabClose(pinnedTab), - waitForTabClose(folderTab), - waitForTabClose(folderEmptyTab), - BrowserTestUtils.waitForEvent(folder, "TabGroupRemoved"), - ]; - - await gZenWorkspaces.changeWorkspaceWithID(originalWorkspaceId); - await waitForActiveWorkspace(originalWorkspaceId); - - gZenWorkspaces.removeWorkspace(deletedWorkspaceId); - await Promise.all(removalEvents); - - ok( - !gBrowser.tabs.includes(regularTab), - "Regular workspace-owned tab should be removed when deleting the workspace" - ); - ok( - !gBrowser.tabs.includes(pinnedTab), - "Pinned workspace-owned tab should be removed when deleting the workspace" - ); - ok( - !gBrowser.tabs.includes(folderTab), - "Folder tab should be removed when deleting the workspace" - ); - ok( - !gBrowser.tabs.includes(folderEmptyTab), - "Folder empty tab should be removed when deleting the workspace" - ); - ok( - gBrowser.tabs.includes(originalWorkspaceTab), - "Tab from the original workspace should remain after deleting another workspace" - ); - ok( - !gBrowser.tabs.some( - tab => tab.getAttribute("zen-workspace-id") === deletedWorkspaceId - ), - "No remaining tab should keep the deleted workspace id" - ); - - BrowserTestUtils.removeTab(originalWorkspaceTab); -});