Skip to content

Commit 6785236

Browse files
committed
gh-8309: fix moving essential tabs to new window
1 parent 6bc560c commit 6785236

3 files changed

Lines changed: 235 additions & 23 deletions

File tree

src/browser/components/tabbrowser/content/tabbrowser-js.patch

Lines changed: 83 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -823,38 +823,98 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
823823
aTab.selected ||
824824
aTab.closing ||
825825
// Tabs that are sharing the screen, microphone or camera cannot be hidden.
826-
@@ -6780,7 +6974,8 @@
827-
* @param {object} [aOptions={}]
828-
* Key-value pairs that will be serialized into the features string.
829-
*/
826+
@@ -6780,7 +6972,30 @@
827+
* @param {object} [aOptions={}]
828+
* Key-value pairs that will be serialized into the features string.
829+
*/
830830
- replaceTabWithWindow(aTab, aOptions = {}) {
831831
+ replaceTabWithWindow(aTab, aOptions = {}, zenForceSync = false) {
832-
+ if (!this.isTab(aTab)) return; // TODO: Handle tab groups
833-
if (this.tabs.length == 1) {
834-
return null;
835-
}
836-
@@ -6797,7 +6992,7 @@
837-
// tell a new window to take the "dropped" tab
838-
let args = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
839-
args.appendElement(aTab.splitview ?? aTab);
832+
+ const normalizeDuplicatedEssentialTab = tab => {
833+
+ tab.removeAttribute("zen-essential");
834+
+ tab.removeAttribute("zenDefaultUserContextId");
835+
+ if (tab.pinned) {
836+
+ tab.ownerGlobal.gBrowser.unpinTab(tab);
837+
+ }
838+
+ };
839+
+ if (
840+
+ !zenForceSync &&
841+
+ this.isTab(aTab) &&
842+
+ aTab.hasAttribute("zen-essential")
843+
+ ) {
844+
+ const duplicatedTab = this.duplicateTab(aTab, true, {
845+
+ tabIndex: aTab._tPos + 1,
846+
+ });
847+
+ normalizeDuplicatedEssentialTab(duplicatedTab);
848+
+ duplicatedTab.addEventListener(
849+
+ "SSTabRestored",
850+
+ () => normalizeDuplicatedEssentialTab(duplicatedTab),
851+
+ { once: true }
852+
+ );
853+
+ aTab = duplicatedTab;
854+
+ }
855+
if (this.tabs.length == 1) {
856+
return null;
857+
}
858+
@@ -6797,7 +7012,7 @@
859+
// tell a new window to take the "dropped" tab
860+
let args = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
861+
args.appendElement(aTab.splitview ?? aTab);
840862
- return BrowserWindowTracker.openWindow({
841863
+ let win = BrowserWindowTracker.openWindow({
842864
private: PrivateBrowsingUtils.isWindowPrivate(window),
843865
features: Object.entries(aOptions)
844866
.map(([key, value]) => `${key}=${value}`)
845867
@@ -6805,6 +7000,8 @@
846-
openerWindow: window,
847-
args,
848-
});
868+
openerWindow: window,
869+
args,
870+
});
849871
+ win._zenStartupSyncFlag = (zenForceSync || !Services.prefs.getBoolPref("zen.tabs.dnd-open-blank-window", true)) ? 'synced' : 'unsynced';
850872
+ return win;
851-
}
852-
853-
/**
854-
@@ -6917,7 +7114,7 @@
855-
* `true` if element is a `<tab-group>`
856-
*/
857-
isTabGroup(element) {
873+
}
874+
875+
/**
876+
@@ -6825,10 +7042,36 @@
877+
elements = [contextTab.splitview ?? contextTab];
878+
}
879+
880+
- if (this.tabs.length == elements.length) {
881+
+ const normalizeDuplicatedEssentialTab = tab => {
882+
+ tab.removeAttribute("zen-essential");
883+
+ tab.removeAttribute("zenDefaultUserContextId");
884+
+ if (tab.pinned) {
885+
+ tab.ownerGlobal.gBrowser.unpinTab(tab);
886+
+ }
887+
+ };
888+
+ const containsEssentialTab = elements.some(
889+
+ element => this.isTab(element) && element.hasAttribute("zen-essential")
890+
+ );
891+
+ if (this.tabs.length == elements.length && !containsEssentialTab) {
892+
return null;
893+
}
894+
895+
+ elements = elements.map(element => {
896+
+ if (!this.isTab(element) || !element.hasAttribute("zen-essential")) {
897+
+ return element;
898+
+ }
899+
+ const duplicatedTab = this.duplicateTab(element, true, {
900+
+ tabIndex: element._tPos + 1,
901+
+ });
902+
+ normalizeDuplicatedEssentialTab(duplicatedTab);
903+
+ duplicatedTab.addEventListener(
904+
+ "SSTabRestored",
905+
+ () => normalizeDuplicatedEssentialTab(duplicatedTab),
906+
+ { once: true }
907+
+ );
908+
+ return duplicatedTab;
909+
+ });
910+
+
911+
if (elements.length == 1) {
912+
return this.replaceTabWithWindow(elements[0], aOptions);
913+
}
914+
@@ -6917,7 +7160,7 @@
915+
* `true` if element is a `<tab-group>`
916+
*/
917+
isTabGroup(element) {
858918
- return !!(element?.tagName == "tab-group");
859919
+ return !!(element?.tagName == "tab-group" || element?.tagName == "zen-folder");
860920
}
@@ -947,7 +1007,7 @@ index 43fb79a3060e20f671ae6ffc26350c7abf497702..68a037d5a0e3416f31ffcb163592f008
9471007
+ if (!gZenFolders.canDropElement(element, targetElement)) {
9481008
+ element = element.group;
9491009
+ }
950-
+ if (!element.hasAttribute('zen-essential') && targetElement?.hasAttribute('zen-essential')) {
1010+
+ if (!element.hasAttribute('zen-essential') && targetElement?.hasAttribute('zen-essential')) {
9511011
+ targetElement = null;
9521012
+ moveBefore = false;
9531013
+ }

src/zen/tests/pinned/browser.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ prefs = ["zen.workspaces.separate-essentials=false"]
77

88
["browser_issue_8726.js"]
99

10+
["browser_essential_move_to_window.js"]
11+
1012
["browser_pinned_changed.js"]
1113

1214
["browser_pinned_close.js"]
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/* Any copyright is dedicated to the Public Domain.
2+
https://creativecommons.org/publicdomain/zero/1.0/ */
3+
4+
"use strict";
5+
6+
const TEST_URL = "https://example.com/";
7+
8+
function isInEssentialsContainer(tab) {
9+
return !!tab.parentElement.closest(".zen-essentials-container");
10+
}
11+
12+
add_task(async function test_Move_Essential_Tab_To_Window_Duplicates() {
13+
const tab = await BrowserTestUtils.openNewForegroundTab(
14+
gBrowser,
15+
TEST_URL,
16+
true
17+
);
18+
19+
gZenPinnedTabManager.addToEssentials(tab);
20+
ok(tab.hasAttribute("zen-essential"), "The original tab is essential");
21+
ok(isInEssentialsContainer(tab), "The original tab is in Essentials");
22+
23+
const essentialCount = gBrowser._numZenEssentials;
24+
const newWindowPromise = BrowserTestUtils.waitForNewWindow();
25+
const openedWindow = gBrowser.replaceTabWithWindow(tab);
26+
ok(openedWindow, "Moving an Essential tab opens a new window");
27+
28+
const newWindow = await newWindowPromise;
29+
await BrowserTestUtils.waitForCondition(
30+
() => newWindow.gBrowser.selectedBrowser.currentURI.spec == TEST_URL,
31+
"The duplicated tab loaded in the new window"
32+
);
33+
34+
const duplicatedTab = newWindow.gBrowser.selectedTab;
35+
ok(
36+
tab.hasAttribute("zen-essential"),
37+
"The original tab remains marked as essential"
38+
);
39+
ok(
40+
isInEssentialsContainer(tab),
41+
"The original tab remains in the Essentials container"
42+
);
43+
is(
44+
gBrowser._numZenEssentials,
45+
essentialCount,
46+
"The source window keeps the same number of Essentials"
47+
);
48+
ok(
49+
!duplicatedTab.hasAttribute("zen-essential"),
50+
"The new window receives a regular tab copy"
51+
);
52+
ok(!duplicatedTab.pinned, "The new window tab is not pinned");
53+
is(
54+
duplicatedTab.linkedBrowser.currentURI.spec,
55+
TEST_URL,
56+
"The new window tab keeps the original URL"
57+
);
58+
59+
await BrowserTestUtils.closeWindow(newWindow);
60+
await BrowserTestUtils.removeTab(tab);
61+
});
62+
63+
add_task(async function test_Forced_Sync_Moves_Essential_Tab() {
64+
let newWindow;
65+
let duplicated = false;
66+
const tab = await BrowserTestUtils.openNewForegroundTab(
67+
gBrowser,
68+
TEST_URL,
69+
true
70+
);
71+
72+
gZenPinnedTabManager.addToEssentials(tab);
73+
ok(tab.hasAttribute("zen-essential"), "The original tab is essential");
74+
75+
const originalDuplicateTab = gBrowser.duplicateTab;
76+
gBrowser.duplicateTab = (...args) => {
77+
duplicated = true;
78+
return originalDuplicateTab.apply(gBrowser, args);
79+
};
80+
81+
try {
82+
const newWindowPromise = BrowserTestUtils.waitForNewWindow();
83+
const openedWindow = gBrowser.replaceTabWithWindow(
84+
tab,
85+
{},
86+
/* zenForceSync = */ true
87+
);
88+
ok(openedWindow, "Forced sync opens a new window for the Essential tab");
89+
90+
newWindow = await newWindowPromise;
91+
ok(!duplicated, "Forced sync does not duplicate an Essential tab");
92+
} finally {
93+
gBrowser.duplicateTab = originalDuplicateTab;
94+
95+
if (newWindow && !newWindow.closed) {
96+
await BrowserTestUtils.closeWindow(newWindow);
97+
}
98+
for (const candidate of [...gBrowser.tabs]) {
99+
if (
100+
candidate.linkedBrowser.currentURI.spec == TEST_URL &&
101+
!candidate.closing
102+
) {
103+
await BrowserTestUtils.removeTab(candidate);
104+
}
105+
}
106+
}
107+
});
108+
109+
add_task(async function test_Move_Tab_Group_Label_To_Window_Still_Works() {
110+
let newWindow;
111+
const tab1 = await BrowserTestUtils.openNewForegroundTab(
112+
gBrowser,
113+
TEST_URL,
114+
true
115+
);
116+
const tab2 = await BrowserTestUtils.openNewForegroundTab(
117+
gBrowser,
118+
"https://example.org/",
119+
true
120+
);
121+
122+
try {
123+
const group = gBrowser.addTabGroup([tab1, tab2], { insertBefore: tab1 });
124+
125+
const newWindowPromise = BrowserTestUtils.waitForNewWindow();
126+
const openedWindow = gBrowser.replaceTabWithWindow(group.labelElement);
127+
ok(openedWindow, "Moving a tab group label opens a new window");
128+
129+
newWindow = await newWindowPromise;
130+
await BrowserTestUtils.waitForCondition(
131+
() => newWindow.gBrowser.tabs.length >= 2,
132+
"The grouped tabs moved to the new window"
133+
);
134+
135+
is(
136+
newWindow.gBrowser.tabs.length,
137+
2,
138+
"The new window receives both grouped tabs"
139+
);
140+
} finally {
141+
if (newWindow && !newWindow.closed) {
142+
await BrowserTestUtils.closeWindow(newWindow);
143+
}
144+
for (const tab of [tab1, tab2]) {
145+
if (tab.isConnected && !tab.closing) {
146+
await BrowserTestUtils.removeTab(tab);
147+
}
148+
}
149+
}
150+
});

0 commit comments

Comments
 (0)