534 lines
15 KiB
JavaScript
534 lines
15 KiB
JavaScript
/* eslint-disable mozilla/no-arbitrary-setTimeout */
|
|
const { AddonManagerPrivate } = ChromeUtils.importESModule(
|
|
"resource://gre/modules/AddonManager.sys.mjs"
|
|
);
|
|
|
|
const { AddonTestUtils } = ChromeUtils.importESModule(
|
|
"resource://testing-common/AddonTestUtils.sys.mjs"
|
|
);
|
|
|
|
AddonTestUtils.initMochitest(this);
|
|
|
|
AddonTestUtils.hookAMTelemetryEvents();
|
|
|
|
const kSideloaded = true;
|
|
|
|
async function createWebExtension(details) {
|
|
let options = {
|
|
manifest: {
|
|
manifest_version: details.manifest_version ?? 2,
|
|
|
|
browser_specific_settings: { gecko: { id: details.id } },
|
|
|
|
name: details.name,
|
|
|
|
permissions: details.permissions,
|
|
host_permissions: details.host_permissions,
|
|
incognito: details.incognito,
|
|
},
|
|
};
|
|
|
|
if (details.iconURL) {
|
|
options.manifest.icons = { 64: details.iconURL };
|
|
}
|
|
|
|
let xpi = AddonTestUtils.createTempWebExtensionFile(options);
|
|
|
|
await AddonTestUtils.manuallyInstall(xpi);
|
|
}
|
|
|
|
function promiseEvent(eventEmitter, event) {
|
|
return new Promise(resolve => {
|
|
eventEmitter.once(event, resolve);
|
|
});
|
|
}
|
|
|
|
function getAddonElement(managerWindow, addonId) {
|
|
return TestUtils.waitForCondition(
|
|
() =>
|
|
managerWindow.document.querySelector(`addon-card[addon-id="${addonId}"]`),
|
|
`Found entry for sideload extension addon "${addonId}" in HTML about:addons`
|
|
);
|
|
}
|
|
|
|
function assertSideloadedAddonElementState(addonElement, pressed) {
|
|
const enableBtn = addonElement.querySelector('[action="toggle-disabled"]');
|
|
is(
|
|
enableBtn.pressed,
|
|
pressed,
|
|
`The enable button is ${!pressed ? " not " : ""} pressed`
|
|
);
|
|
is(enableBtn.localName, "moz-toggle", "The enable button is a toggle");
|
|
}
|
|
|
|
function clickEnableExtension(addonElement) {
|
|
addonElement.querySelector('[action="toggle-disabled"]').click();
|
|
}
|
|
|
|
add_task(async function test_sideloading() {
|
|
const DEFAULT_ICON_URL =
|
|
"chrome://mozapps/skin/extensions/extensionGeneric.svg";
|
|
|
|
await SpecialPowers.pushPrefEnv({
|
|
set: [
|
|
["xpinstall.signatures.required", false],
|
|
["extensions.autoDisableScopes", 15],
|
|
["extensions.ui.showAddonIconForUnsigned", true],
|
|
],
|
|
});
|
|
|
|
Services.fog.testResetFOG();
|
|
|
|
const ID1 = "addon1@tests.mozilla.org";
|
|
await createWebExtension({
|
|
id: ID1,
|
|
name: "Test 1",
|
|
userDisabled: true,
|
|
permissions: ["history", "https://*/*"],
|
|
iconURL: "foo-icon.png",
|
|
});
|
|
|
|
const ID2 = "addon2@tests.mozilla.org";
|
|
await createWebExtension({
|
|
manifest_version: 3,
|
|
id: ID2,
|
|
name: "Test 2",
|
|
host_permissions: ["<all_urls>"],
|
|
});
|
|
|
|
const ID3 = "addon3@tests.mozilla.org";
|
|
await createWebExtension({
|
|
id: ID3,
|
|
name: "Test 3",
|
|
permissions: ["<all_urls>"],
|
|
});
|
|
|
|
const ID4 = "addon4@tests.mozilla.org";
|
|
await createWebExtension({
|
|
id: ID4,
|
|
name: "Test 4",
|
|
incognito: "not_allowed",
|
|
permissions: [],
|
|
});
|
|
|
|
const ID5 = "addon5@tests.mozilla.org";
|
|
await createWebExtension({
|
|
id: ID5,
|
|
name: "Test 5",
|
|
incognito: "not_allowed",
|
|
permissions: ["<all_urls>"],
|
|
});
|
|
|
|
const ID6 = "addon6@tests.mozilla.org";
|
|
await createWebExtension({
|
|
id: ID6,
|
|
name: "Test 6",
|
|
incognito: "not_allowed",
|
|
permissions: ["history", "https://*/*"],
|
|
});
|
|
|
|
testCleanup = async function () {
|
|
// clear out ExtensionsUI state about sideloaded extensions so
|
|
// subsequent tests don't get confused.
|
|
ExtensionsUI.sideloaded.clear();
|
|
ExtensionsUI.emit("change");
|
|
};
|
|
|
|
// Navigate away from the starting page to force about:addons to load
|
|
// in a new tab during the tests below.
|
|
BrowserTestUtils.startLoadingURIString(
|
|
gBrowser.selectedBrowser,
|
|
"about:robots"
|
|
);
|
|
await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
|
|
|
|
registerCleanupFunction(async function () {
|
|
// Return to about:blank when we're done
|
|
BrowserTestUtils.startLoadingURIString(
|
|
gBrowser.selectedBrowser,
|
|
"about:blank"
|
|
);
|
|
await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
|
|
});
|
|
|
|
let changePromise = new Promise(resolve => {
|
|
ExtensionsUI.on("change", function listener() {
|
|
ExtensionsUI.off("change", listener);
|
|
resolve();
|
|
});
|
|
});
|
|
ExtensionsUI._checkForSideloaded();
|
|
await changePromise;
|
|
|
|
// Check for the addons badge on the hamburger menu
|
|
let menuButton = document.getElementById("PanelUI-menu-button");
|
|
is(
|
|
menuButton.getAttribute("badge-status"),
|
|
"addon-alert",
|
|
"Should have addon alert badge"
|
|
);
|
|
|
|
// Find the menu entries for sideloaded extensions
|
|
await gCUITestUtils.openMainMenu();
|
|
|
|
let addons = PanelUI.addonNotificationContainer;
|
|
is(
|
|
addons.children.length,
|
|
4,
|
|
"Have 4 menu entries for sideloaded extensions"
|
|
);
|
|
|
|
info(
|
|
"Test disabling sideloaded addon 1 using the permission prompt secondary button"
|
|
);
|
|
|
|
// Click the first sideloaded extension
|
|
let popupPromise = promisePopupNotificationShown("addon-webext-permissions");
|
|
addons.children[0].click();
|
|
|
|
// The click should hide the main menu. This is currently synchronous.
|
|
Assert.notEqual(
|
|
PanelUI.panel.state,
|
|
"open",
|
|
"Main menu is closed or closing."
|
|
);
|
|
|
|
// When we get the permissions prompt, we should be at the extensions
|
|
// list in about:addons
|
|
let panel = await popupPromise;
|
|
is(
|
|
gBrowser.currentURI.spec,
|
|
"about:addons",
|
|
"Foreground tab is at about:addons"
|
|
);
|
|
|
|
const VIEW = "addons://list/extension";
|
|
let win = gBrowser.selectedBrowser.contentWindow;
|
|
|
|
await TestUtils.waitForCondition(
|
|
() => !win.gViewController.isLoading,
|
|
"about:addons view is fully loaded"
|
|
);
|
|
is(
|
|
win.gViewController.currentViewId,
|
|
VIEW,
|
|
"about:addons is at extensions list"
|
|
);
|
|
|
|
// Check the contents of the notification, then choose "Cancel"
|
|
checkNotification(
|
|
panel,
|
|
/\/foo-icon\.png$/,
|
|
[
|
|
["webext-perms-host-description-all-urls"],
|
|
["webext-perms-description-history"],
|
|
],
|
|
kSideloaded
|
|
);
|
|
|
|
panel.secondaryButton.click();
|
|
|
|
let [addon1, addon2, addon3, addon4, addon5, addon6] =
|
|
await AddonManager.getAddonsByIDs([ID1, ID2, ID3, ID4, ID5, ID6]);
|
|
ok(addon1.seen, "Addon should be marked as seen");
|
|
is(addon1.userDisabled, true, "Addon 1 should still be disabled");
|
|
is(addon2.userDisabled, true, "Addon 2 should still be disabled");
|
|
is(addon3.userDisabled, true, "Addon 3 should still be disabled");
|
|
is(addon4.userDisabled, true, "Addon 4 should still be disabled");
|
|
is(addon5.userDisabled, true, "Addon 5 should still be disabled");
|
|
is(addon6.userDisabled, true, "Addon 6 should still be disabled");
|
|
|
|
BrowserTestUtils.removeTab(gBrowser.selectedTab);
|
|
|
|
// Should still have 2 entries in the hamburger menu
|
|
await gCUITestUtils.openMainMenu();
|
|
|
|
addons = PanelUI.addonNotificationContainer;
|
|
is(
|
|
addons.children.length,
|
|
4,
|
|
"Have 4 menu entries for sideloaded extensions"
|
|
);
|
|
|
|
// Close the hamburger menu and go directly to the addons manager
|
|
await gCUITestUtils.hideMainMenu();
|
|
|
|
win = await BrowserAddonUI.openAddonsMgr(VIEW);
|
|
await waitAboutAddonsViewLoaded(win.document);
|
|
|
|
// about:addons addon entry element.
|
|
const addonElement = await getAddonElement(win, ID2);
|
|
|
|
assertSideloadedAddonElementState(addonElement, false);
|
|
|
|
info("Test enabling sideloaded addon 2 from about:addons enable button");
|
|
|
|
// When clicking enable we should see the permissions notification
|
|
popupPromise = promisePopupNotificationShown("addon-webext-permissions");
|
|
clickEnableExtension(addonElement);
|
|
panel = await popupPromise;
|
|
checkNotification(
|
|
panel,
|
|
DEFAULT_ICON_URL,
|
|
[["webext-perms-host-description-all-urls"]],
|
|
kSideloaded
|
|
);
|
|
ok(
|
|
panel.querySelector(".webext-perm-privatebrowsing moz-checkbox"),
|
|
"Expect incognito checkbox in sideload prompt"
|
|
);
|
|
|
|
// Accept the permissions
|
|
panel.button.click();
|
|
await promiseEvent(ExtensionsUI, "change");
|
|
|
|
addon2 = await AddonManager.getAddonByID(ID2);
|
|
is(addon2.userDisabled, false, "Addon 2 should be enabled");
|
|
assertSideloadedAddonElementState(addonElement, true);
|
|
|
|
// Should still have 1 entry in the hamburger menu
|
|
await gCUITestUtils.openMainMenu();
|
|
|
|
addons = PanelUI.addonNotificationContainer;
|
|
is(addons.children.length, 4, "Have 4 menu entry for sideloaded extensions");
|
|
|
|
// Close the hamburger menu and go to the detail page for this addon
|
|
await gCUITestUtils.hideMainMenu();
|
|
|
|
win = await BrowserAddonUI.openAddonsMgr(
|
|
`addons://detail/${encodeURIComponent(ID3)}`
|
|
);
|
|
|
|
// Trigger all remaining addon install from the app menu, to be able to cover the
|
|
// post install notification that should be triggered when the permission
|
|
// dialog is accepted from that flow.
|
|
const enableSideloadedFromAppMenu = async (addon, testPanelCb) => {
|
|
popupPromise = promisePopupNotificationShown("addon-webext-permissions");
|
|
ExtensionsUI.showSideloaded(gBrowser, addon);
|
|
panel = await popupPromise;
|
|
await testPanelCb();
|
|
// Accept the permissions
|
|
panel.button.click();
|
|
await promiseEvent(ExtensionsUI, "change");
|
|
};
|
|
|
|
info("Test enabling sideloaded addon 3 from app menu");
|
|
await enableSideloadedFromAppMenu(addon3, () => {
|
|
checkNotification(
|
|
panel,
|
|
DEFAULT_ICON_URL,
|
|
[["webext-perms-host-description-all-urls"]],
|
|
kSideloaded
|
|
);
|
|
});
|
|
|
|
addon3 = await AddonManager.getAddonByID(ID3);
|
|
is(addon3.userDisabled, false, "Addon 3 should be enabled");
|
|
|
|
addons = PanelUI.addonNotificationContainer;
|
|
is(addons.children.length, 3, "Have 3 menu entry for sideloaded extensions");
|
|
|
|
info("Test enabling sideloaded addon 4 from app menu");
|
|
await enableSideloadedFromAppMenu(addon4, () => {
|
|
checkNotification(panel, DEFAULT_ICON_URL, [], kSideloaded);
|
|
});
|
|
addon4 = await AddonManager.getAddonByID(ID4);
|
|
is(addon4.userDisabled, false, "Addon 4 should be enabled");
|
|
addons = PanelUI.addonNotificationContainer;
|
|
is(addons.children.length, 2, "Have 2 menu entry for sideloaded extensions");
|
|
|
|
info("Test enabling sideloaded addon 5 from app menu");
|
|
await enableSideloadedFromAppMenu(addon5, () => {
|
|
checkNotification(
|
|
panel,
|
|
DEFAULT_ICON_URL,
|
|
[["webext-perms-host-description-all-urls"]],
|
|
kSideloaded
|
|
);
|
|
});
|
|
addon5 = await AddonManager.getAddonByID(ID5);
|
|
is(addon5.userDisabled, false, "Addon 5 should be enabled");
|
|
addons = PanelUI.addonNotificationContainer;
|
|
is(addons.children.length, 1, "Have 1 menu entry for sideloaded extensions");
|
|
|
|
info("Test enabling sideloaded addon 6 from app menu");
|
|
await enableSideloadedFromAppMenu(addon6, () => {
|
|
checkNotification(
|
|
panel,
|
|
DEFAULT_ICON_URL,
|
|
[
|
|
["webext-perms-host-description-all-urls"],
|
|
["webext-perms-description-history"],
|
|
],
|
|
kSideloaded
|
|
);
|
|
});
|
|
addon6 = await AddonManager.getAddonByID(ID6);
|
|
is(addon6.userDisabled, false, "Addon 6 should be enabled");
|
|
|
|
isnot(
|
|
menuButton.getAttribute("badge-status"),
|
|
"addon-alert",
|
|
"Should no longer have addon alert badge"
|
|
);
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
|
|
for (let addon of [addon1, addon2, addon3, addon4, addon5, addon6]) {
|
|
await addon.uninstall();
|
|
}
|
|
|
|
isnot(
|
|
menuButton.getAttribute("badge-status"),
|
|
"addon-alert",
|
|
"Should no longer have addon alert badge"
|
|
);
|
|
|
|
BrowserTestUtils.removeTab(gBrowser.selectedTab);
|
|
|
|
// Assert that the expected AddonManager telemetry are being recorded.
|
|
const expectedExtra = { source: "app-profile", method: "sideload" };
|
|
|
|
const baseEvent = { object: "extension", extra: expectedExtra };
|
|
const createBaseEventAddon = n => ({
|
|
...baseEvent,
|
|
value: `addon${n}@tests.mozilla.org`,
|
|
});
|
|
const getEventsForAddonId = (events, addonId) =>
|
|
events.filter(ev => ev.value === addonId);
|
|
|
|
const amEvents = AddonTestUtils.getAMTelemetryEvents();
|
|
|
|
// Test telemetry events for addon1 (1 permission and 1 origin).
|
|
info("Test telemetry events collected for addon1");
|
|
|
|
const baseEventAddon1 = createBaseEventAddon(1);
|
|
|
|
const blocklist_state = `${Ci.nsIBlocklistService.STATE_NOT_BLOCKED}`;
|
|
|
|
Assert.deepEqual(
|
|
AddonTestUtils.getAMGleanEvents("manage", { addon_id: ID1 }),
|
|
[
|
|
{
|
|
addon_id: ID1,
|
|
method: "sideload_prompt",
|
|
addon_type: "extension",
|
|
source: "app-profile",
|
|
source_method: "sideload",
|
|
num_strings: "2",
|
|
blocklist_state,
|
|
},
|
|
{
|
|
addon_id: ID1,
|
|
method: "uninstall",
|
|
addon_type: "extension",
|
|
source: "app-profile",
|
|
source_method: "sideload",
|
|
blocklist_state,
|
|
},
|
|
],
|
|
"Got the expected Glean events for addon1."
|
|
);
|
|
|
|
const collectedEventsAddon1 = getEventsForAddonId(
|
|
amEvents,
|
|
baseEventAddon1.value
|
|
);
|
|
const expectedEventsAddon1 = [
|
|
{
|
|
...baseEventAddon1,
|
|
method: "sideload_prompt",
|
|
extra: { ...expectedExtra, num_strings: "2", blocklist_state },
|
|
},
|
|
{
|
|
...baseEventAddon1,
|
|
method: "uninstall",
|
|
extra: { ...expectedExtra, blocklist_state },
|
|
},
|
|
];
|
|
|
|
let i = 0;
|
|
for (let event of collectedEventsAddon1) {
|
|
Assert.deepEqual(
|
|
event,
|
|
expectedEventsAddon1[i++],
|
|
"Got the expected telemetry event"
|
|
);
|
|
}
|
|
|
|
is(
|
|
collectedEventsAddon1.length,
|
|
expectedEventsAddon1.length,
|
|
"Got the expected number of telemetry events for addon1"
|
|
);
|
|
|
|
const baseEventAddon2 = createBaseEventAddon(2);
|
|
const collectedEventsAddon2 = getEventsForAddonId(
|
|
amEvents,
|
|
baseEventAddon2.value
|
|
);
|
|
const expectedEventsAddon2 = [
|
|
{
|
|
...baseEventAddon2,
|
|
method: "sideload_prompt",
|
|
extra: { ...expectedExtra, num_strings: "1", blocklist_state },
|
|
},
|
|
{
|
|
...baseEventAddon2,
|
|
method: "enable",
|
|
extra: { ...expectedExtra, blocklist_state },
|
|
},
|
|
{
|
|
...baseEventAddon2,
|
|
method: "uninstall",
|
|
extra: { ...expectedExtra, blocklist_state },
|
|
},
|
|
];
|
|
|
|
i = 0;
|
|
for (let event of collectedEventsAddon2) {
|
|
Assert.deepEqual(
|
|
event,
|
|
expectedEventsAddon2[i++],
|
|
"Got the expected telemetry event"
|
|
);
|
|
}
|
|
|
|
is(
|
|
collectedEventsAddon2.length,
|
|
expectedEventsAddon2.length,
|
|
"Got the expected number of telemetry events for addon2"
|
|
);
|
|
|
|
Assert.deepEqual(
|
|
AddonTestUtils.getAMGleanEvents("manage", { addon_id: ID2 }),
|
|
[
|
|
{
|
|
addon_id: ID2,
|
|
method: "sideload_prompt",
|
|
addon_type: "extension",
|
|
source: "app-profile",
|
|
source_method: "sideload",
|
|
num_strings: "1",
|
|
blocklist_state,
|
|
},
|
|
{
|
|
addon_id: ID2,
|
|
method: "enable",
|
|
addon_type: "extension",
|
|
source: "app-profile",
|
|
source_method: "sideload",
|
|
blocklist_state,
|
|
},
|
|
{
|
|
addon_id: ID2,
|
|
method: "uninstall",
|
|
addon_type: "extension",
|
|
source: "app-profile",
|
|
source_method: "sideload",
|
|
blocklist_state,
|
|
},
|
|
],
|
|
"Got the expected Glean events for addon2."
|
|
);
|
|
});
|