commit b1dd24598576a1c9c14d14ae3ee3b59208777d94
parent e17c721cc09b4de01c015c1ad03752e200d274da
Author: Tim Giles <tgiles@mozilla.com>
Date: Thu, 30 Oct 2025 20:58:05 +0000
Bug 1985656 - Add re-enable extension message bar to new config settings. r=desktop-theme-reviewers,hjones
When all controlling extensions are disabled for a particular setting,
then a message bar explaining how to re-enable the extension will appear
in the extension controlled message bar location. This can be tested
using Storybook and navigating to the "Settings" folder under
"Domain-Specific UI Widgets", then "Settings Control", and finally
"Extension Controlled". Disabling the extension will show the re-enable
message bar element.
We use a flag showEnableMessage to determine when the re-enable
message bar should be rendered. This is controlled by the Setting class
and correctly handles the case where the setting is not initially
controlled by an extension, but becomes controlled at a later time.
In the SettingControl class, we check this.shouldShowEnableMessage()
to determine if we should render a moz-message-bar for the re-enable
message. Currently we set the .messageL10nId property directly on this
message bar so that we can use the existing "extension-controlled-enable"
string. Note: since we don't support rendering images in the
moz-message-bar, we will get consistent Fluent warnings about not being
able to find the <img> in the source string.
Differential Revision: https://phabricator.services.mozilla.com/D269684
Diffstat:
5 files changed, 341 insertions(+), 36 deletions(-)
diff --git a/browser/components/preferences/tests/chrome/test_setting_control_extension_controlled.html b/browser/components/preferences/tests/chrome/test_setting_control_extension_controlled.html
@@ -46,6 +46,8 @@
ExtensionSettingsStore:
"resource://gre/modules/ExtensionSettingsStore.sys.mjs",
AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
+ BrowserWindowTracker:
+ "resource:///modules/BrowserWindowTracker.sys.mjs",
});
/* import-globals-from /toolkit/content/preferencesBindings.js */
@@ -75,6 +77,11 @@
await synthesizeMouseAtCenter(elem, {});
}
+ const getExtensionControlledMessageBar = control =>
+ control.querySelector(".extension-controlled-message-bar");
+ const getReenableExtensionMessageBar = control =>
+ control.querySelector(".reenable-extensions-message-bar");
+
add_setup(async function setup() {
testHelpers = new InputTestHelpers();
({ html } = await testHelpers.setupLit());
@@ -270,27 +277,30 @@
let setting = Preferences.getSetting(SETTING_ID);
let control = await renderTemplate(itemConfig, setting);
// Assert that moz-message-bar appears with the correct Fluent attributes/args
- let messageBar = control.querySelector("moz-message-bar");
+ let extensionControlledMessageBar =
+ getExtensionControlledMessageBar(control);
+
ok(
- messageBar,
+ extensionControlledMessageBar,
"There should be an extension controlled message bar element"
);
is(
- messageBar.messageL10nId,
+ extensionControlledMessageBar.messageL10nId,
TEST_FLUENT_ID,
"The l10nId should be the same as the one in the config"
);
is(
- messageBar.messageL10nArgs.name,
+ extensionControlledMessageBar.messageL10nArgs.name,
ADDON_NAME,
"The name used within the message-bar should be the extension name"
);
// Assert that moz-message-bar appears with an actions button with correct Fluent attributes
- let disableExtensionButton = messageBar.querySelector(
- "moz-button[slot='actions']"
- );
+ let disableExtensionButton =
+ extensionControlledMessageBar.querySelector(
+ "moz-button[slot='actions']"
+ );
ok(
disableExtensionButton,
"There should be a button to disable the extension"
@@ -304,14 +314,33 @@
await disableExtensionByMouse(disableExtensionButton);
await TestUtils.waitForCondition(
- () => !control.querySelector("moz-message-bar"),
- "Wait for the message bar to be removed after disabling the only controlling extension"
+ () => !getExtensionControlledMessageBar(control),
+ "Wait for the extension controlled message bar to be removed after disabling the only controlling extension"
);
- messageBar = control.querySelector("moz-message-bar");
+ await TestUtils.waitForCondition(
+ () => getReenableExtensionMessageBar(control),
+ "Wait for the re-enable extension message bar to be rendered"
+ );
+ let enableExtensionMessageBar = getReenableExtensionMessageBar(control);
ok(
- !messageBar,
- "The message bar should be removed after activating the disable extension button."
+ enableExtensionMessageBar,
+ "there should be a message bar for re-enabling extensions"
+ );
+
+ let addonsLink = enableExtensionMessageBar.querySelector("[slot] a");
+ is(
+ enableExtensionMessageBar
+ .querySelector("[slot]")
+ .getAttribute("data-l10n-id"),
+ "extension-controlled-enable-2",
+ "The re-enable extension message bar should have a slot with the correct data-l10n-id"
+ );
+
+ is(
+ addonsLink.getAttribute("data-l10n-name"),
+ "addons-link",
+ "The slot within the re-enable extension bar should have the correct data-l10n-name"
);
await addon.enable();
@@ -320,10 +349,15 @@
"Wait for the control to become disabled as a side-effect of enabling the controlling extension"
);
- messageBar = control.querySelector("moz-message-bar");
+ await TestUtils.waitForCondition(
+ () => !getReenableExtensionMessageBar(control),
+ "The re-enable extension message bar should be removed when the extension is enabled again."
+ );
+ extensionControlledMessageBar =
+ getExtensionControlledMessageBar(control);
ok(
- messageBar,
- "The message bar should be rendered when the controlling addon is enabled elsewhere."
+ extensionControlledMessageBar,
+ "The extension controlled message bar should be rendered when the controlling addon is enabled elsewhere."
);
await addon.disable();
@@ -331,10 +365,16 @@
() => !control.controlEl.disabled,
"Wait for the control to become mutable as a side-effect of disabling the controlling extension"
);
-
- messageBar = control.querySelector("moz-message-bar");
+ enableExtensionMessageBar = getReenableExtensionMessageBar(control);
+ is(
+ enableExtensionMessageBar
+ .querySelector("[slot]")
+ .getAttribute("data-l10n-id"),
+ "extension-controlled-enable-2",
+ "The re-enable extension message bar should have a slot with the correct data-l10n-id"
+ );
ok(
- !messageBar,
+ !getExtensionControlledMessageBar(control),
"The message bar should not be rendered when the controlling addon is disabled elsewhere."
);
@@ -354,10 +394,17 @@
() => !control.controlEl.disabled,
"Wait for the control to become mutable as a side-effect of disabling the controlling extension"
);
- messageBar = control.querySelector("moz-message-bar");
+ extensionControlledMessageBar =
+ getExtensionControlledMessageBar(control);
ok(
- !messageBar,
- "The message bar should not be rendered after hitting Space on the disable extension button."
+ !extensionControlledMessageBar,
+ "The extension controlled message bar should not be rendered after hitting Space on the disable extension button."
+ );
+
+ enableExtensionMessageBar = getReenableExtensionMessageBar(control);
+ ok(
+ enableExtensionMessageBar,
+ "The message bar to re-enable the extension should be rendered"
);
// Unload the test extension and remove the setting control
@@ -448,26 +495,28 @@
);
// Assert that moz-message-bar appears with the correct Fluent attributes/args
- let messageBar = control.querySelector("moz-message-bar");
+ let extensionControlledMessageBar =
+ getExtensionControlledMessageBar(control);
ok(
- messageBar,
+ extensionControlledMessageBar,
"There should be an extension controlled message bar element"
);
is(
- messageBar.messageL10nId,
+ extensionControlledMessageBar.messageL10nId,
TEST_FLUENT_ID,
"The l10nId should be the same as the one in the config"
);
is(
- messageBar.messageL10nArgs.name,
+ extensionControlledMessageBar.messageL10nArgs.name,
ADDON_NAME_2,
"The name used within the message-bar should be the most recently enabled extension name"
);
// Assert that moz-message-bar appears with an actions button with correct Fluent attributes
- let disableExtensionButton = messageBar.querySelector(
- "moz-button[slot='actions']"
- );
+ let disableExtensionButton =
+ extensionControlledMessageBar.querySelector(
+ "moz-button[slot='actions']"
+ );
ok(
disableExtensionButton,
"There should be a button to disable the extension"
@@ -477,26 +526,28 @@
"disable-extension",
"The disable extension button should have the correct data-l10n-id"
);
+
await disableExtensionByMouse(disableExtensionButton);
await TestUtils.waitForCondition(
() =>
- control.querySelector("moz-message-bar")?.messageL10nArgs.name ===
+ getExtensionControlledMessageBar(control)?.messageL10nArgs.name ===
ADDON_NAME,
"Wait for the message bar to be refreshed after disabling the controlling extension"
);
- messageBar = control.querySelector("moz-message-bar");
- disableExtensionButton = messageBar.querySelector(
+ extensionControlledMessageBar =
+ getExtensionControlledMessageBar(control);
+ disableExtensionButton = extensionControlledMessageBar.querySelector(
"moz-button[slot='actions']"
);
ok(
- messageBar,
+ extensionControlledMessageBar,
"The message bar should still be present due to multiple extensions controlling the setting"
);
is(
- messageBar.messageL10nArgs.name,
+ extensionControlledMessageBar.messageL10nArgs.name,
ADDON_NAME,
"The name used within the message-bar should be the oldest enabled extension name"
);
@@ -510,7 +561,7 @@
await disableExtensionByMouse(disableExtensionButton);
await TestUtils.waitForCondition(
- () => !control.querySelector("moz-message-bar"),
+ () => !getExtensionControlledMessageBar(control),
"Wait for the message bar to be removed after disabling the last controlling extension"
);
@@ -520,11 +571,219 @@
"The control element should not be disabled since there are no more controlling extensions"
);
+ await TestUtils.waitForCondition(
+ () => getReenableExtensionMessageBar(control),
+ "Wait for the re-enable extension message bar to render"
+ );
+ let enableExtensionMessageBar = getReenableExtensionMessageBar(control);
+ ok(
+ enableExtensionMessageBar,
+ "The message bar for enabling extensions should be rendered"
+ );
+ is(
+ enableExtensionMessageBar
+ .querySelector("[slot]")
+ .getAttribute("data-l10n-id"),
+ "extension-controlled-enable-2",
+ "The re-enable extension message bar should have a slot with the correct data-l10n-id"
+ );
+
// Clean up extensions and rendered setting control element
await extension.unload();
await secondExtension.unload();
control.remove();
});
+ add_task(async function test_no_initial_controlling_extension() {
+ // Setup pre-test items: extension, Preferences, ExtensionSettingStore
+ const SETTING_ID = "extension-controlled-setting";
+ const STORE_ID = "privacy.containers";
+ const TEST_FLUENT_ID = "test-fluent-id";
+
+ // Assert there is no markup that is generated by pre-test setup since we
+ // don't have a setting that is being controlled by the STORE_ID
+
+ let settingControl = document.getElementById(SETTING_ID);
+ is(
+ settingControl,
+ null,
+ "The setting control under test should not exist yet."
+ );
+
+ // Add setting that is not initially controlled by an extension,
+ // but is configured so that it can be controlled.
+ Preferences.addSetting({
+ id: SETTING_ID,
+ pref: PREF_ID,
+ controllingExtensionInfo: {
+ storeId: STORE_ID,
+ l10nId: TEST_FLUENT_ID,
+ },
+ });
+
+ // Create itemConfig for expected setting-control element
+ let itemConfig = {
+ l10nId: "test-fluent-id",
+ id: SETTING_ID,
+ };
+
+ // Wait for setting-control element to be rendered
+ let setting = Preferences.getSetting(SETTING_ID);
+ let control = await renderTemplate(itemConfig, setting);
+
+ // Assert checkbox control is created and NOT disabled due to
+ // no extension controlling the pref.
+
+ is(
+ control.controlEl.localName,
+ "moz-checkbox",
+ "The control rendered the default checkbox"
+ );
+ is(
+ control.controlEl.disabled,
+ false,
+ "The control should not disabled since it is not controlled by an extension"
+ );
+
+ // Assert that there are no moz-message-bar elements rendered since
+ // the setting is not controlled by an extension
+ let messageBars = control.querySelectorAll("moz-message-bar");
+
+ ok(
+ !messageBars.length,
+ "There should be no rendered message bar elements"
+ );
+
+ control.remove();
+ });
+ add_task(async function test_addons_link_in_message_bar() {
+ // Setup pre-test items like extension, AddonManager, ExtensionSettingStore
+ const SETTING_ID = "extension-controlled-setting";
+ const ADDON_ID = "ext-controlled@mochi.test";
+ const ADDON_NAME = "Ext Controlled";
+ const STORE_ID = "privacy.containers";
+ const TEST_FLUENT_ID = "test-fluent-id";
+ await SpecialPowers.pushPrefEnv({
+ set: [[PREF_ID, false]],
+ });
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: { gecko: { id: ADDON_ID } },
+ name: ADDON_NAME,
+ permissions: ["contextualIdentities", "cookies"],
+ },
+ // We need to be able to find the extension using AddonManager.
+ useAddonManager: "temporary",
+ });
+ await extension.startup();
+
+ await ExtensionSettingsStore.initialize();
+
+ // Assert there is no markup that is generated by pre-test setup since we
+ // don't have a setting that is being controlled by the STORE_ID
+
+ let settingControl = document.getElementById(SETTING_ID);
+ is(
+ settingControl,
+ null,
+ "The setting control under test should not exist yet."
+ );
+
+ // Add setting that is being controlled by our test extension
+ Preferences.addSetting({
+ id: SETTING_ID,
+ pref: PREF_ID,
+ controllingExtensionInfo: {
+ storeId: STORE_ID,
+ l10nId: TEST_FLUENT_ID,
+ },
+ });
+
+ // Create itemConfig for expected setting-control element
+ let itemConfig = {
+ l10nId: "test-fluent-id",
+ id: SETTING_ID,
+ };
+
+ // Wait for setting-control element to be rendered
+ let setting = Preferences.getSetting(SETTING_ID);
+ let control = await renderTemplate(itemConfig, setting);
+ let messageBar = control.querySelector("moz-message-bar");
+ let disableExtensionButton = messageBar.querySelector(
+ "moz-button[slot='actions']"
+ );
+
+ await disableExtensionByMouse(disableExtensionButton);
+
+ await TestUtils.waitForCondition(
+ () => !getExtensionControlledMessageBar(control),
+ "Wait for the extension controlled message bar to be removed after disabling the only controlling extension"
+ );
+
+ await TestUtils.waitForCondition(
+ () => getReenableExtensionMessageBar(control),
+ "Wait for the re-enable extension message bar to be rendered"
+ );
+ let enableExtensionMessageBar = getReenableExtensionMessageBar(control);
+ ok(
+ enableExtensionMessageBar,
+ "there should be a message bar for re-enabling extensions"
+ );
+ let addonsLink = enableExtensionMessageBar.querySelector("[slot] a");
+
+ is(
+ enableExtensionMessageBar
+ .querySelector("[slot]")
+ .getAttribute("data-l10n-id"),
+ "extension-controlled-enable-2",
+ "The re-enable extension message bar should have a slot with the correct data-l10n-id"
+ );
+
+ is(
+ addonsLink.getAttribute("data-l10n-name"),
+ "addons-link",
+ "The slot within the re-enable extension bar should have the correct data-l10n-name"
+ );
+
+ let gBrowser = BrowserWindowTracker.getTopWindow().top.gBrowser;
+ let addonsTabPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ "about:addons"
+ );
+ await synthesizeMouseAtCenter(addonsLink, {});
+
+ let tab = await addonsTabPromise;
+ is(
+ tab.linkedBrowser.currentURI.spec,
+ "about:addons",
+ "addons link correctly navigates to about:addons"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ enableExtensionMessageBar = getReenableExtensionMessageBar(control);
+ addonsLink = enableExtensionMessageBar.querySelector("[slot] a");
+
+ ok(
+ enableExtensionMessageBar,
+ "there should still be a message bar for re-enabling extensions after navigating back to about:preferences"
+ );
+ is(
+ enableExtensionMessageBar
+ .querySelector("[slot]")
+ .getAttribute("data-l10n-id"),
+ "extension-controlled-enable-2",
+ "The re-enable extension message bar should have a slot with the correct data-l10n-id after navigating back to about:preferences"
+ );
+
+ is(
+ addonsLink.getAttribute("data-l10n-name"),
+ "addons-link",
+ "The slot within the re-enable extension bar should have the correct data-l10n-name after navigating back to about:preferences"
+ );
+
+ // Unload the test extension and remove the setting control
+ await extension.unload();
+ control.remove();
+ });
</script>
</head>
diff --git a/browser/components/preferences/widgets/setting-control/setting-control.css b/browser/components/preferences/widgets/setting-control/setting-control.css
@@ -2,6 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
-.extension-controlled-message-bar {
+.extension-controlled-message-bar,
+.reenable-extensions-message-bar {
margin-block: var(--space-large);
}
diff --git a/browser/components/preferences/widgets/setting-control/setting-control.mjs b/browser/components/preferences/widgets/setting-control/setting-control.mjs
@@ -59,6 +59,7 @@ export class SettingControl extends SettingElement {
config: { type: Object },
value: {},
parentDisabled: { type: Boolean },
+ showEnableExtensionMessage: { type: Boolean },
};
constructor() {
@@ -84,6 +85,11 @@ export class SettingControl extends SettingElement {
* @type {boolean | undefined}
*/
this.parentDisabled = undefined;
+
+ /**
+ * @type {boolean}
+ */
+ this.showEnableExtensionMessage = false;
}
createRenderRoot() {
@@ -221,6 +227,7 @@ export class SettingControl extends SettingElement {
async disableExtension() {
await this.setting.disableControllingExtension();
+ this.showEnableExtensionMessage = true;
}
isControlledByExtension() {
@@ -230,6 +237,18 @@ export class SettingControl extends SettingElement {
);
}
+ handleEnableExtensionDismiss() {
+ this.showEnableExtensionMessage = false;
+ }
+
+ navigateToAddons(event) {
+ if (event.target.matches("a[data-l10n-name='addons-link']")) {
+ event.preventDefault();
+ let mainWindow = window.browsingContext.topChromeWindow;
+ mainWindow.BrowserAddonUI.openAddonsMgr("addons://list/theme");
+ }
+ }
+
get extensionName() {
return this.setting.controllingExtensionInfo.name;
}
@@ -279,6 +298,9 @@ export class SettingControl extends SettingElement {
let tag = unsafeStatic(control);
let messageBar;
+
+ // NOTE: the showEnableMessage message bar should ONLY appear when
+ // there are no extensions controlling the setting.
if (this.isControlledByExtension()) {
let args = { name: this.extensionName };
messageBar = html`<moz-message-bar
@@ -292,6 +314,20 @@ export class SettingControl extends SettingElement {
data-l10n-id="disable-extension"
></moz-button>
</moz-message-bar>`;
+ } else if (this.showEnableExtensionMessage) {
+ messageBar = html`<moz-message-bar
+ class="reenable-extensions-message-bar"
+ dismissable=""
+ @message-bar:user-dismissed=${this.handleEnableExtensionDismiss}
+ >
+ <span
+ @click=${this.navigateToAddons}
+ slot="message"
+ data-l10n-id="extension-controlled-enable-2"
+ >
+ <a data-l10n-name="addons-link" href="#"></a>
+ </span>
+ </moz-message-bar>`;
}
return staticHtml`
${messageBar}
diff --git a/browser/components/preferences/widgets/setting-control/setting-control.stories.mjs b/browser/components/preferences/widgets/setting-control/setting-control.stories.mjs
@@ -37,7 +37,7 @@ radio-option-2 =
extension-controlled-input =
.label = Setting controlled by extension
extension-controlled-message = <strong>My Extension</strong> requires Controlled Setting.
-`,
+extension-controlled-enable-2 = Storybook Only: Refresh the page to enable the extension. To re-enable this extension visit <a data-l10n-name="addons-link">Extensions and themes</a>.`,
},
};
@@ -134,6 +134,14 @@ ExtensionControlled.args = {
},
setting: {
...DEFAULT_SETTING,
+ disableControllingExtension() {
+ delete this.controllingExtensionInfo.id;
+ delete this.controllingExtensionInfo.name;
+ document
+ .querySelector("with-common-styles")
+ .shadowRoot.querySelector("setting-control")
+ .requestUpdate();
+ },
controllingExtensionInfo: {
id: "extension-controlled-example",
l10nId: "extension-controlled-message",
diff --git a/browser/locales/en-US/browser/preferences/preferences.ftl b/browser/locales/en-US/browser/preferences/preferences.ftl
@@ -124,6 +124,7 @@ extension-controlling-proxy-config = <img data-l10n-name ="icon"/> <strong>{ $na
# <img data-l10n-name="menu-icon"/> will be replaced with Menu icon
extension-controlled-enable = To enable the extension go to <img data-l10n-name="addons-icon"/> Add-ons in the <img data-l10n-name="menu-icon"/> menu.
+extension-controlled-enable-2 = To re-enable this extension visit <a data-l10n-name="addons-link">Extensions and themes</a>.
# This string is shown to notify the user that their home page or new tab preferences
# are being controlled by an extension.
extension-controlling-homepage = { $name } controls some of your homepage settings.