From 0a548c7572dd94afdc0240daaaabc4af5da5a99c Mon Sep 17 00:00:00 2001 From: "mr. m" <91018726+mr-cheffy@users.noreply.github.com> Date: Thu, 10 Sep 2026 22:20:12 +0200 Subject: [PATCH] gh-15329: Prevent split view preview from opening for multiple tabs (gh-15340) --- src/zen/split-view/ZenViewSplitter.mjs | 4 +- src/zen/sync/ZenSpacesSyncApplier.sys.mjs | 3 - .../{spaces_sync => device_sync}/browser.toml | 2 + .../device_sync/browser_folder_delete.js | 100 ++++++++++++++++++ .../browser_normal_tabs_apply.js | 0 .../browser_normal_tabs_projection.js | 0 .../{spaces_sync => device_sync}/head.js | 38 ++++++- src/zen/tests/moz.build | 2 +- 8 files changed, 142 insertions(+), 7 deletions(-) rename src/zen/tests/{spaces_sync => device_sync}/browser.toml (93%) create mode 100644 src/zen/tests/device_sync/browser_folder_delete.js rename src/zen/tests/{spaces_sync => device_sync}/browser_normal_tabs_apply.js (100%) rename src/zen/tests/{spaces_sync => device_sync}/browser_normal_tabs_projection.js (100%) rename src/zen/tests/{spaces_sync => device_sync}/head.js (71%) diff --git a/src/zen/split-view/ZenViewSplitter.mjs b/src/zen/split-view/ZenViewSplitter.mjs index 3eb1b1a35..9f6274151 100644 --- a/src/zen/split-view/ZenViewSplitter.mjs +++ b/src/zen/split-view/ZenViewSplitter.mjs @@ -336,7 +336,9 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature { if ( !gBrowser.isTab(draggedTab) || gBrowser.selectedTab.hasAttribute("zen-empty-tab") || - draggedTab.documentGlobal !== window + draggedTab.documentGlobal !== window || + // See gh-15329. + draggedTab.multiselected ) { return; } diff --git a/src/zen/sync/ZenSpacesSyncApplier.sys.mjs b/src/zen/sync/ZenSpacesSyncApplier.sys.mjs index aeba7b2e6..107f610e0 100644 --- a/src/zen/sync/ZenSpacesSyncApplier.sys.mjs +++ b/src/zen/sync/ZenSpacesSyncApplier.sys.mjs @@ -526,9 +526,6 @@ class nsZenSpacesSyncApplier { if (!folder?.isZenFolder) { continue; } - // Members without their own tombstone survive: unpack, then delete - // the (now empty) folder. - await folder.unpackTabs(); await folder.delete(); } catch (e) { fail(record, e); diff --git a/src/zen/tests/spaces_sync/browser.toml b/src/zen/tests/device_sync/browser.toml similarity index 93% rename from src/zen/tests/spaces_sync/browser.toml rename to src/zen/tests/device_sync/browser.toml index 5209e17ad..65b727ebe 100644 --- a/src/zen/tests/spaces_sync/browser.toml +++ b/src/zen/tests/device_sync/browser.toml @@ -12,6 +12,8 @@ support-files = [ "head.js", ] +["browser_folder_delete.js"] + ["browser_normal_tabs_apply.js"] ["browser_normal_tabs_projection.js"] diff --git a/src/zen/tests/device_sync/browser_folder_delete.js b/src/zen/tests/device_sync/browser_folder_delete.js new file mode 100644 index 000000000..9dcdff060 --- /dev/null +++ b/src/zen/tests/device_sync/browser_folder_delete.js @@ -0,0 +1,100 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +async function applyFolderWithMembers(fid, memberIds) { + const failed = await ZenSpacesSyncApplier.applyBatch([ + folderRecord(fid), + ...memberIds.map(id => tabRecord(id, { pinned: true, folderId: fid })), + ]); + Assert.deepEqual(failed, [], "The folder batch should apply cleanly"); + const folder = document.getElementById(fid); + Assert.ok(folder?.isZenFolder, "The folder should materialize"); + for (const id of memberIds) { + Assert.equal( + document.getElementById(id)?.group, + folder, + `member ${id} should live in the folder` + ); + } +} + +add_task(async function test_FolderTombstoneWithMemberTombstones() { + await gZenWorkspaces.promiseInitialized; + const fid = "test-sync-del-a-folder"; + const members = ["test-sync-del-a-1", "test-sync-del-a-2"]; + await applyFolderWithMembers(fid, members); + + const failed = await ZenSpacesSyncApplier.applyBatch([ + tombstone(fid), + ...members.map(tombstone), + ]); + Assert.deepEqual(failed, [], "The tombstone batch should apply cleanly"); + await TestUtils.waitForCondition( + () => !document.getElementById(fid), + "waiting for the folder to be removed" + ); + for (const id of members) { + Assert.ok(!document.getElementById(id), `member ${id} should be gone`); + } + + for (const id of [fid, ...members]) { + ZenSpacesSyncModel.noteApplied(id, null); + } +}); + +add_task(async function test_FolderTombstoneAloneDeletesMembers() { + await gZenWorkspaces.promiseInitialized; + const fid = "test-sync-del-b-folder"; + const members = ["test-sync-del-b-1", "test-sync-del-b-2"]; + await applyFolderWithMembers(fid, members); + + const failed = await ZenSpacesSyncApplier.applyBatch([tombstone(fid)]); + Assert.deepEqual(failed, [], "The tombstone batch should apply cleanly"); + await TestUtils.waitForCondition( + () => !document.getElementById(fid), + "waiting for the folder to be removed" + ); + for (const id of members) { + const tab = document.getElementById(id); + Assert.ok(!tab, `member ${id} should be deleted, not orphaned as pinned`); + } + + for (const id of [fid, ...members]) { + ZenSpacesSyncModel.noteApplied(id, null); + } +}); + +add_task(async function test_MemberMovedOutSurvivesFolderTombstone() { + await gZenWorkspaces.promiseInitialized; + const fid = "test-sync-del-c-folder"; + const stays = "test-sync-del-c-stays"; + const moved = "test-sync-del-c-moved"; + await applyFolderWithMembers(fid, [stays, moved]); + + const failed = await ZenSpacesSyncApplier.applyBatch([ + tabRecord(moved, { pinned: true, folderId: null }), + tombstone(fid), + ]); + Assert.deepEqual(failed, [], "The batch should apply cleanly"); + await TestUtils.waitForCondition( + () => !document.getElementById(fid), + "waiting for the folder to be removed" + ); + const movedTab = document.getElementById(moved); + Assert.ok(movedTab, "the moved-out member should survive"); + Assert.ok(!movedTab.group?.isZenFolder, "and no longer be in a folder"); + Assert.ok( + !document.getElementById(stays), + "the member with no surviving record should be deleted" + ); + + ZenSpacesSyncModel.noteApplied(moved, null); + for (const id of [fid, stays]) { + ZenSpacesSyncModel.noteApplied(id, null); + } + if (movedTab) { + BrowserTestUtils.removeTab(movedTab); + } +}); diff --git a/src/zen/tests/spaces_sync/browser_normal_tabs_apply.js b/src/zen/tests/device_sync/browser_normal_tabs_apply.js similarity index 100% rename from src/zen/tests/spaces_sync/browser_normal_tabs_apply.js rename to src/zen/tests/device_sync/browser_normal_tabs_apply.js diff --git a/src/zen/tests/spaces_sync/browser_normal_tabs_projection.js b/src/zen/tests/device_sync/browser_normal_tabs_projection.js similarity index 100% rename from src/zen/tests/spaces_sync/browser_normal_tabs_projection.js rename to src/zen/tests/device_sync/browser_normal_tabs_projection.js diff --git a/src/zen/tests/spaces_sync/head.js b/src/zen/tests/device_sync/head.js similarity index 71% rename from src/zen/tests/spaces_sync/head.js rename to src/zen/tests/device_sync/head.js index e04d44b63..46a445c52 100644 --- a/src/zen/tests/spaces_sync/head.js +++ b/src/zen/tests/device_sync/head.js @@ -22,6 +22,11 @@ const { TestUtils } = ChromeUtils.importESModule( const { E10SUtils } = ChromeUtils.importESModule( "resource://gre/modules/E10SUtils.sys.mjs" ); +const { PromiseTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/PromiseTestUtils.sys.mjs" +); + +PromiseTestUtils.allowMatchingRejectionsGlobally(/menu-bookmark-tab/); const NORMAL_TABS_PREF = "zen.spaces-sync.normal-tabs"; @@ -47,7 +52,13 @@ async function openSyncableTab(url, { pinned = false } = {}) { function tabRecord( id, - { pinned, url = "https://example.com/", current = null, workspaceUuid } = {} + { + pinned, + url = "https://example.com/", + current = null, + workspaceUuid, + folderId = null, + } = {} ) { return { id, @@ -64,7 +75,7 @@ function tabRecord( essential: false, pinned, workspaceUuid: workspaceUuid ?? gZenWorkspaces.activeWorkspace, - folderId: null, + folderId, staticLabel: null, hasStaticIcon: false, defaultContainer: false, @@ -72,3 +83,26 @@ function tabRecord( }, }; } + +function folderRecord(id, { name = "Synced folder", workspaceUuid } = {}) { + return { + id, + deleted: false, + cleartext: { + kind: "folder", + data: { + folderId: id, + name, + icon: null, + workspaceUuid: workspaceUuid ?? gZenWorkspaces.activeWorkspace, + parentFolderId: null, + live: null, + children: [], + }, + }, + }; +} + +function tombstone(id) { + return { id, deleted: true }; +} diff --git a/src/zen/tests/moz.build b/src/zen/tests/moz.build index b8c025b51..6a9a0f081 100644 --- a/src/zen/tests/moz.build +++ b/src/zen/tests/moz.build @@ -6,6 +6,7 @@ BROWSER_CHROME_MANIFESTS += [ "boosts/browser.toml", "compact_mode/browser.toml", "container_essentials/browser.toml", + "device_sync/browser.toml", "folders/browser.toml", "glance/browser.toml", "live-folders/browser.toml", @@ -16,7 +17,6 @@ BROWSER_CHROME_MANIFESTS += [ "site_control/browser.toml", "space_routing/browser.toml", "spaces/browser.toml", - "spaces_sync/browser.toml", "split_view/browser.toml", "tabs/browser.toml", "ub-actions/browser.toml",