tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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:
Mbrowser/base/content/browser-profiles.js | 24++++++++++++++++++++++++
Mbrowser/components/profiles/tests/browser/browser.toml | 2++
Abrowser/components/profiles/tests/browser/browser_appmenu.js | 144+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/locales/en-US/browser/appmenu.ftl | 2++
Mbrowser/themes/shared/customizableui/panelUI-shared.css | 4++++
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"); }