Skip to content

Commit f5ade40

Browse files
author
Niklas Baumgardner
committed
Bug 1990551 - Send tabs to other profiles from context menu. r=profiles-reviewers,fluent-reviewers,bolsson,jhirsch
Differential Revision: https://phabricator.services.mozilla.com/D269856
1 parent 2cc73d2 commit f5ade40

File tree

12 files changed

+311
-11
lines changed

12 files changed

+311
-11
lines changed

browser/base/content/browser-profiles.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,16 @@ var gProfiles = {
194194
});
195195
},
196196

197+
async openTabsInProfile(aEvent, tabsToOpen) {
198+
let profile = await SelectableProfileService.getProfile(
199+
aEvent.target.getAttribute("profileid")
200+
);
201+
SelectableProfileService.launchInstance(
202+
profile,
203+
tabsToOpen.map(tab => tab.linkedBrowser.currentURI.spec)
204+
);
205+
},
206+
197207
async handleCommand(aEvent) {
198208
switch (aEvent.target.id) {
199209
/* App menu button events */
@@ -247,6 +257,16 @@ var gProfiles = {
247257
this.launchProfile(aEvent.sourceEvent);
248258
break;
249259
}
260+
case "Profiles:MoveTabsToProfile": {
261+
let tabs;
262+
if (TabContextMenu.contextTab.multiselected) {
263+
tabs = gBrowser.selectedTabs;
264+
} else {
265+
tabs = [TabContextMenu.contextTab];
266+
}
267+
this.openTabsInProfile(aEvent.sourceEvent, tabs);
268+
break;
269+
}
250270
}
251271
/* Subpanel profile events that may be triggered in FxA menu or app menu */
252272
if (aEvent.target.classList.contains("profile-item")) {
@@ -427,4 +447,50 @@ var gProfiles = {
427447
profilesList.appendChild(button);
428448
}
429449
},
450+
451+
async populateMoveTabMenu(menuPopup) {
452+
if (!SelectableProfileService.initialized) {
453+
return;
454+
}
455+
456+
const profiles = await SelectableProfileService.getAllProfiles();
457+
const currentProfile = SelectableProfileService.currentProfile;
458+
459+
const separator = document.getElementById("moveTabSeparator");
460+
separator.hidden = profiles.length < 2;
461+
462+
let existingItems = [
463+
...menuPopup.querySelectorAll(":scope > menuitem[profileid]"),
464+
];
465+
466+
for (let profile of profiles) {
467+
if (profile.id === currentProfile.id) {
468+
continue;
469+
}
470+
471+
let menuitem = existingItems.shift();
472+
let isNewItem = !menuitem;
473+
if (isNewItem) {
474+
menuitem = document.createXULElement("menuitem");
475+
menuitem.setAttribute("tbattr", "tabbrowser-multiple-visible");
476+
menuitem.setAttribute("data-l10n-id", "move-to-new-profile");
477+
menuitem.setAttribute("command", "Profiles:MoveTabsToProfile");
478+
}
479+
480+
menuitem.disabled = false;
481+
menuitem.setAttribute("profileid", profile.id);
482+
menuitem.setAttribute(
483+
"data-l10n-args",
484+
JSON.stringify({ profileName: profile.name })
485+
);
486+
487+
if (isNewItem) {
488+
menuPopup.appendChild(menuitem);
489+
}
490+
}
491+
// If there's any old item to remove, do so now.
492+
for (let remaining of existingItems) {
493+
remaining.remove();
494+
}
495+
},
430496
};

browser/base/content/browser-sets.inc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
<command id="Profiles:CreateProfile" />
7979
<command id="Profiles:ManageProfiles" />
8080
<command id="Profiles:LaunchProfile" />
81+
<command id="Profiles:MoveTabsToProfile" />
8182
<command id="Browser:NextTab" />
8283
<command id="Browser:PrevTab" />
8384
<command id="Browser:ShowAllTabs" />

browser/base/content/browser-sets.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ document.addEventListener(
203203
case "Profiles:CreateProfile":
204204
case "Profiles:ManageProfiles":
205205
case "Profiles:LaunchProfile":
206+
case "Profiles:MoveTabsToProfile":
206207
gProfiles.handleCommand(event);
207208
break;
208209
case "Tools:Search":

browser/base/content/main-popupset.inc.xhtml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
tbattr="tabbrowser-multiple-visible"/>
7676
<menuitem id="context_openTabInWindow" data-lazy-l10n-id="move-to-new-window"
7777
tbattr="tabbrowser-multiple-visible"/>
78+
<menuseparator id="moveTabSeparator" hidden="true"/>
7879
</menupopup>
7980
</menu>
8081
<menu id="context_sendTabToDevice"

browser/base/content/main-popupset.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,9 @@ document.addEventListener(
532532
case "tabContextMenu":
533533
TabContextMenu.addNewBadge();
534534
break;
535+
case "moveTabOptionsMenu":
536+
gProfiles.populateMoveTabMenu(event.target);
537+
break;
535538
}
536539
});
537540

browser/components/profiles/SelectableProfileService.sys.mjs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -619,13 +619,17 @@ class SelectableProfileServiceClass extends EventEmitter {
619619
* Launch a new Firefox instance using the given selectable profile.
620620
*
621621
* @param {SelectableProfile} aProfile The profile to launch
622-
* @param {string} aUrl A url to open in launched profile
622+
* @param {Array<string>} aUrls An array of urls to open in launched profile
623623
*/
624-
launchInstance(aProfile, aUrl) {
624+
launchInstance(aProfile, aUrls) {
625625
let args = [];
626626

627-
if (aUrl) {
628-
args.push("-url", aUrl);
627+
if (aUrls?.length) {
628+
// See https://wiki.mozilla.org/Firefox/CommandLineOptions#-url_URL
629+
// Use '-new-tab' instead of '-url' because when opening multiple URLs,
630+
// Firefox always opens them as tabs in a new window and we want to
631+
// attempt opening these tabs in an existing window.
632+
args.push(...aUrls.flatMap(url => ["-new-tab", url]));
629633
} else {
630634
args.push(`--${COMMAND_LINE_ACTIVATE}`);
631635
}
@@ -1440,7 +1444,7 @@ class SelectableProfileServiceClass extends EventEmitter {
14401444

14411445
let profile = await this.#createProfile();
14421446
if (launchProfile) {
1443-
this.launchInstance(profile, "about:newprofile");
1447+
this.launchInstance(profile, ["about:newprofile"]);
14441448
}
14451449
return profile;
14461450
}

browser/components/profiles/content/profile-selector.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ export class ProfileSelector extends MozLitElement {
126126
await this.setLaunchArguments(profile, url ? ["-url", url] : []);
127127
await this.selectableProfileService.uninit();
128128
} else {
129-
this.selectableProfileService.launchInstance(profile, url);
129+
this.selectableProfileService.launchInstance(profile, [url]);
130130
}
131131

132132
window.close();

browser/components/profiles/tests/browser/browser.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ skip-if = [
5050
["browser_menubar_profiles.js"]
5151
head = "../unit/head.js head.js"
5252

53+
["browser_moveTabToProfile.js"]
54+
5355
["browser_notify_changes.js"]
5456
run-if = ["os != 'linux'"] # Linux clients cannot remote themselves.
5557
skip-if = ["os == 'mac' && os_version == '15.30' && arch == 'aarch64' && opt && !socketprocess_networking"] # Bug 1929273
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
/* Any copyright is dedicated to the Public Domain.
2+
https://creativecommons.org/publicdomain/zero/1.0/ */
3+
4+
"use strict";
5+
6+
const { sinon } = ChromeUtils.importESModule(
7+
"resource://testing-common/Sinon.sys.mjs"
8+
);
9+
const { ObjectUtils } = ChromeUtils.importESModule(
10+
"resource://gre/modules/ObjectUtils.sys.mjs"
11+
);
12+
13+
const EXAMPLE_URL = "https://example.com/";
14+
15+
async function addTab(url = EXAMPLE_URL) {
16+
const tab = BrowserTestUtils.addTab(gBrowser, url, {
17+
skipAnimation: true,
18+
});
19+
const browser = gBrowser.getBrowserForTab(tab);
20+
await BrowserTestUtils.browserLoaded(browser);
21+
return tab;
22+
}
23+
24+
async function openContextMenu(tab) {
25+
let contextMenu = document.getElementById("tabContextMenu");
26+
let openTabContextMenuPromise = BrowserTestUtils.waitForPopupEvent(
27+
contextMenu,
28+
"shown"
29+
);
30+
31+
await sendAndWaitForMouseEvent(tab, { type: "contextmenu" });
32+
await openTabContextMenuPromise;
33+
return contextMenu;
34+
}
35+
36+
async function openMoveTabOptionsMenuPopup(contextMenu) {
37+
let moveTabMenuItem = contextMenu.querySelector("#context_moveTabOptions");
38+
let subMenu = contextMenu.querySelector("#moveTabOptionsMenu");
39+
let popupShown = BrowserTestUtils.waitForEvent(subMenu, "popupshown");
40+
41+
if (AppConstants.platform === "macosx") {
42+
moveTabMenuItem.openMenu(true);
43+
} else {
44+
await sendAndWaitForMouseEvent(moveTabMenuItem);
45+
}
46+
47+
await popupShown;
48+
49+
const separator = subMenu.querySelector("#moveTabSeparator");
50+
await TestUtils.waitForCondition(
51+
() =>
52+
BrowserTestUtils.isVisible(separator) &&
53+
BrowserTestUtils.isVisible(separator.nextElementSibling)
54+
);
55+
56+
return subMenu;
57+
}
58+
59+
async function clickMoveToProfileMenuItem(subMenu) {
60+
let profileMenuItem = subMenu.querySelector(":scope > menuitem[profileid]");
61+
if (AppConstants.platform === "macosx") {
62+
subMenu.activateItem(profileMenuItem);
63+
} else {
64+
await sendAndWaitForMouseEvent(profileMenuItem);
65+
}
66+
}
67+
68+
async function sendAndWaitForMouseEvent(target, options = {}) {
69+
let promise = BrowserTestUtils.waitForEvent(target, options.type ?? "click");
70+
EventUtils.synthesizeMouseAtCenter(target, options);
71+
return promise;
72+
}
73+
74+
const execProcess = sinon.fake();
75+
const sendCommandLine = sinon.fake.throws(Cr.NS_ERROR_NOT_AVAILABLE);
76+
sinon.replace(
77+
SelectableProfileService,
78+
"sendCommandLine",
79+
(path, args, raise) => sendCommandLine(path, [...args], raise)
80+
);
81+
sinon.replace(SelectableProfileService, "execProcess", execProcess);
82+
83+
registerCleanupFunction(() => {
84+
sinon.restore();
85+
});
86+
87+
let lastCommandLineCallCount = 1;
88+
async function assertCommandLineExists(expected) {
89+
await TestUtils.waitForCondition(
90+
() => sendCommandLine.callCount > lastCommandLineCallCount,
91+
"Waiting for notify task to complete"
92+
);
93+
94+
let allCommandLineCalls = sendCommandLine.getCalls();
95+
96+
lastCommandLineCallCount++;
97+
98+
let expectedCount = allCommandLineCalls.reduce((count, call) => {
99+
if (ObjectUtils.deepEqual(call.args, expected)) {
100+
return count + 1;
101+
}
102+
103+
return count;
104+
}, 0);
105+
106+
Assert.equal(expectedCount, 1, "Found expected args");
107+
Assert.deepEqual(
108+
allCommandLineCalls.find(call => ObjectUtils.deepEqual(call.args, expected))
109+
.args,
110+
expected,
111+
"Expected sendCommandLine arguments to open tab in profile"
112+
);
113+
}
114+
115+
add_task(async function test_moveSelectedTab() {
116+
await initGroupDatabase();
117+
118+
const allProfiles = await SelectableProfileService.getAllProfiles();
119+
let otherProfile;
120+
if (allProfiles.length < 2) {
121+
otherProfile = await SelectableProfileService.createNewProfile(false);
122+
} else {
123+
otherProfile = allProfiles.find(
124+
p => p.id !== SelectableProfileService.currentProfile.id
125+
);
126+
}
127+
128+
let tab2 = await addTab(EXAMPLE_URL + "2");
129+
130+
gBrowser.selectedTab = tab2;
131+
132+
let contextMenu = await openContextMenu(tab2);
133+
let subMenu = await openMoveTabOptionsMenuPopup(contextMenu);
134+
await clickMoveToProfileMenuItem(subMenu);
135+
136+
let expectedArgs = ["-new-tab", EXAMPLE_URL + "2"];
137+
138+
await assertCommandLineExists([otherProfile.path, expectedArgs, true]);
139+
140+
gBrowser.removeTab(tab2);
141+
});
142+
143+
add_task(async function test_moveNonSelectedTab() {
144+
await initGroupDatabase();
145+
146+
const allProfiles = await SelectableProfileService.getAllProfiles();
147+
let otherProfile;
148+
if (allProfiles.length < 2) {
149+
otherProfile = await SelectableProfileService.createNewProfile(false);
150+
} else {
151+
otherProfile = allProfiles.find(
152+
p => p.id !== SelectableProfileService.currentProfile.id
153+
);
154+
}
155+
156+
let tab2 = await addTab(EXAMPLE_URL + "2");
157+
let tab3 = await addTab(EXAMPLE_URL + "3");
158+
159+
gBrowser.selectedTab = tab2;
160+
161+
let contextMenu = await openContextMenu(tab3);
162+
let subMenu = await openMoveTabOptionsMenuPopup(contextMenu);
163+
await clickMoveToProfileMenuItem(subMenu);
164+
165+
let expectedArgs = ["-new-tab", EXAMPLE_URL + "3"];
166+
167+
await assertCommandLineExists([otherProfile.path, expectedArgs, true]);
168+
169+
gBrowser.removeTabs([tab2, tab3]);
170+
});
171+
172+
add_task(async function test_moveMultipleSelectedTabs() {
173+
await initGroupDatabase();
174+
175+
const allProfiles = await SelectableProfileService.getAllProfiles();
176+
let otherProfile;
177+
if (allProfiles.length < 2) {
178+
otherProfile = await SelectableProfileService.createNewProfile(false);
179+
} else {
180+
otherProfile = allProfiles.find(
181+
p => p.id !== SelectableProfileService.currentProfile.id
182+
);
183+
}
184+
185+
let tab2 = await addTab(EXAMPLE_URL + "2");
186+
let tab3 = await addTab(EXAMPLE_URL + "3");
187+
let tab4 = await addTab(EXAMPLE_URL + "4");
188+
189+
gBrowser.selectedTab = tab2;
190+
191+
await sendAndWaitForMouseEvent(tab2);
192+
if (AppConstants.platform === "macosx") {
193+
await sendAndWaitForMouseEvent(tab3, { metaKey: true });
194+
await sendAndWaitForMouseEvent(tab4, { metaKey: true });
195+
} else {
196+
await sendAndWaitForMouseEvent(tab3, { ctrlKey: true });
197+
await sendAndWaitForMouseEvent(tab4, { ctrlKey: true });
198+
}
199+
200+
Assert.ok(tab2.multiselected, "Tab2 is multiselected");
201+
Assert.ok(tab3.multiselected, "Tab3 is multiselected");
202+
Assert.ok(tab4.multiselected, "Tab4 is multiselected");
203+
204+
let contextMenu = await openContextMenu(tab4);
205+
let subMenu = await openMoveTabOptionsMenuPopup(contextMenu);
206+
await clickMoveToProfileMenuItem(subMenu);
207+
208+
let expectedArgs = [
209+
"-new-tab",
210+
EXAMPLE_URL + "2",
211+
"-new-tab",
212+
EXAMPLE_URL + "3",
213+
"-new-tab",
214+
EXAMPLE_URL + "4",
215+
];
216+
217+
await assertCommandLineExists([otherProfile.path, expectedArgs, true]);
218+
219+
gBrowser.removeTabs([tab2, tab3, tab4]);
220+
});

0 commit comments

Comments
 (0)