commit cfcd5aa323b18965f2bf944037ac66e11c1c1041
parent 23642e725c0dd53ef7b82712bc7aed3ee727a054
Author: Jared Hirsch <ohai@6a68.net>
Date: Fri, 24 Oct 2025 17:40:45 +0000
Bug 1992199 - Add copy profile button to profiles subview r=profiles-reviewers,fluent-reviewers,desktop-theme-reviewers,bolsson,dao,niklas
Also add some tests for the different layouts of the profiles
subview with and without multiple profiles.
Differential Revision: https://phabricator.services.mozilla.com/D268776
Diffstat:
5 files changed, 176 insertions(+), 0 deletions(-)
diff --git a/browser/base/content/browser-profiles.js b/browser/base/content/browser-profiles.js
@@ -4,6 +4,7 @@
var gProfiles = {
async init() {
+ this.copyProfile = this.copyProfile.bind(this);
this.createNewProfile = this.createNewProfile.bind(this);
this.handleCommand = this.handleCommand.bind(this);
this.launchProfile = this.launchProfile.bind(this);
@@ -165,6 +166,10 @@ var gProfiles = {
});
},
+ copyProfile() {
+ SelectableProfileService.currentProfile.copyProfile();
+ },
+
createNewProfile() {
SelectableProfileService.createNewProfile();
},
@@ -218,6 +223,10 @@ var gProfiles = {
this.manageProfiles();
break;
}
+ case "profiles-copy-profile-button": {
+ this.copyProfile();
+ break;
+ }
case "profiles-create-profile-button": {
this.createNewProfile();
break;
@@ -313,6 +322,18 @@ var gProfiles = {
);
}
+ let copyProfileButton = PanelMultiView.getViewNode(
+ document,
+ "profiles-copy-profile-button"
+ );
+
+ if (!copyProfileButton) {
+ copyProfileButton = document.createXULElement("toolbarbutton");
+ copyProfileButton.id = "profiles-copy-profile-button";
+ copyProfileButton.classList.add("subviewbutton", "subviewbutton-iconic");
+ copyProfileButton.setAttribute("data-l10n-id", "appmenu-copy-profile");
+ }
+
let manageProfilesButton = PanelMultiView.getViewNode(
document,
"profiles-manage-profiles-button"
@@ -339,6 +360,7 @@ var gProfiles = {
footerSeparator.hidden = true;
const subviewBody = subview.querySelector(".panel-subview-body");
subview.insertBefore(createProfileButton, subviewBody);
+ subview.insertBefore(copyProfileButton, subviewBody);
subview.insertBefore(manageProfilesButton, subviewBody);
} else {
profilesHeader.style.backgroundColor = "var(--appmenu-profiles-theme-bg)";
@@ -352,8 +374,10 @@ var gProfiles = {
subview.style.setProperty("--appmenu-profiles-theme-fg", themeFg);
headerSeparator.hidden = true;
+ footerSeparator.hidden = false;
subview.appendChild(footerSeparator);
subview.appendChild(createProfileButton);
+ subview.appendChild(copyProfileButton);
subview.appendChild(manageProfilesButton);
let headerText = PanelMultiView.getViewNode(
diff --git a/browser/components/profiles/tests/browser/browser.toml b/browser/components/profiles/tests/browser/browser.toml
@@ -8,6 +8,8 @@ run-if = [
"os != 'linux'",
] # This seems to mess up focus on Linux for some reason.
+["browser_appmenu.js"]
+
["browser_appmenu_menuitem_updates.js"]
head = "../unit/head.js head.js"
diff --git a/browser/components/profiles/tests/browser/browser_appmenu.js b/browser/components/profiles/tests/browser/browser_appmenu.js
@@ -0,0 +1,144 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+
+add_setup(async () => {
+ await initGroupDatabase();
+ let profile = SelectableProfileService.currentProfile;
+ Assert.ok(profile, "Should have a profile now");
+
+ // Mock the executable process so we don't launch a new process
+ sinon.stub(SelectableProfileService, "execProcess");
+
+ registerCleanupFunction(() => {
+ sinon.restore();
+ });
+});
+
+// Opens the subview, clicking either of the two app menu profiles buttons
+// to open the subview.
+async function promiseSubViewOpened() {
+ let promiseViewShown = BrowserTestUtils.waitForEvent(
+ PanelUI.panel,
+ "ViewShown"
+ );
+ PanelUI.show();
+ await promiseViewShown;
+
+ let panel = PanelMultiView.getViewNode(document, "PanelUI-profiles");
+ let emptyButton = document.getElementById("appMenu-empty-profiles-button");
+ let button = document.getElementById("appMenu-profiles-button");
+ let visibleButton = button.hidden ? emptyButton : button;
+ EventUtils.synthesizeMouseAtCenter(visibleButton, {});
+ return BrowserTestUtils.waitForCondition(() =>
+ BrowserTestUtils.isVisible(panel)
+ );
+}
+
+function getElements() {
+ const editButton = PanelMultiView.getViewNode(
+ document,
+ "profiles-edit-this-profile-button"
+ );
+ const newProfileButton = PanelMultiView.getViewNode(
+ document,
+ "profiles-create-profile-button"
+ );
+ const copyProfileButton = PanelMultiView.getViewNode(
+ document,
+ "profiles-copy-profile-button"
+ );
+ const manageProfilesButton = PanelMultiView.getViewNode(
+ document,
+ "profiles-manage-profiles-button"
+ );
+ const subview = PanelMultiView.getViewNode(document, "PanelUI-profiles");
+ const subviewBody = subview.querySelector(".panel-subview-body");
+
+ return {
+ editButton,
+ newProfileButton,
+ copyProfileButton,
+ manageProfilesButton,
+ subviewBody,
+ };
+}
+
+add_task(async function test_appmenu_layout_no_profiles() {
+ await SelectableProfileService.init();
+ await promiseSubViewOpened();
+ let {
+ editButton,
+ newProfileButton,
+ copyProfileButton,
+ manageProfilesButton,
+ subviewBody,
+ } = getElements();
+
+ Assert.ok(BrowserTestUtils.isHidden(editButton));
+ Assert.ok(BrowserTestUtils.isVisible(newProfileButton));
+ Assert.ok(BrowserTestUtils.isVisible(copyProfileButton));
+ Assert.ok(BrowserTestUtils.isVisible(manageProfilesButton));
+ Assert.ok(!subviewBody.contains(newProfileButton));
+ Assert.ok(!subviewBody.contains(copyProfileButton));
+ Assert.ok(!subviewBody.contains(manageProfilesButton));
+ Assert.strictEqual(
+ subviewBody.compareDocumentPosition(newProfileButton),
+ 2,
+ "The new profile button should precede the subview body"
+ );
+ Assert.strictEqual(
+ subviewBody.compareDocumentPosition(copyProfileButton),
+ 2,
+ "The copy profile button should precede the subview body"
+ );
+ Assert.strictEqual(
+ subviewBody.compareDocumentPosition(manageProfilesButton),
+ 2,
+ "The manage profiles button should precede the subview body"
+ );
+
+ await PanelUI.hide();
+});
+
+add_task(async function test_appmenu_layout_two_profiles() {
+ await SelectableProfileService.init();
+
+ await SelectableProfileService.createNewProfile();
+
+ await promiseSubViewOpened();
+ let {
+ editButton,
+ newProfileButton,
+ copyProfileButton,
+ manageProfilesButton,
+ subviewBody,
+ } = getElements();
+
+ Assert.ok(BrowserTestUtils.isVisible(editButton));
+ Assert.ok(BrowserTestUtils.isVisible(newProfileButton));
+ Assert.ok(BrowserTestUtils.isVisible(copyProfileButton));
+ Assert.ok(BrowserTestUtils.isVisible(manageProfilesButton));
+ Assert.strictEqual(
+ subviewBody.compareDocumentPosition(newProfileButton),
+ 4,
+ "The new profile button should follow the subview body"
+ );
+ Assert.strictEqual(
+ subviewBody.compareDocumentPosition(copyProfileButton),
+ 4,
+ "The copy profile button should follow the subview body"
+ );
+ Assert.strictEqual(
+ subviewBody.compareDocumentPosition(manageProfilesButton),
+ 4,
+ "The manage profiles button should follow the subview body"
+ );
+
+ await PanelUI.hide();
+});
diff --git a/browser/locales/en-US/browser/appmenu.ftl b/browser/locales/en-US/browser/appmenu.ftl
@@ -328,6 +328,8 @@ appmenu-profiles-2 =
appmenu-other-profiles = Other profiles
appmenu-manage-profiles =
.label = Manage profiles
+appmenu-copy-profile =
+ .label = Copy this profile
appmenu-create-profile =
.label = New profile
appmenu-edit-profile =
diff --git a/browser/themes/shared/customizableui/panelUI-shared.css b/browser/themes/shared/customizableui/panelUI-shared.css
@@ -2280,6 +2280,10 @@ radiogroup:focus-visible > .subviewradio[focused="true"] {
display: none;
}
+#profiles-copy-profile-button {
+ list-style-image: url("chrome://global/skin/icons/edit-copy.svg");
+}
+
#profiles-create-profile-button {
list-style-image: url("chrome://global/skin/icons/plus.svg");
}