Source code
Revision control
Copy as Markdown
Other Tools
/* eslint no-unused-vars: ["error", {vars: "local", args: "none"}] */
const { PermissionTestUtils } = ChromeUtils.importESModule(
);
const { PromptTestUtils } = ChromeUtils.importESModule(
);
const RELATIVE_DIR = "toolkit/mozapps/extensions/test/xpinstall/";
const PROMPT_URL = "chrome://global/content/commonDialog.xhtml";
const ADDONS_URL = "chrome://mozapps/content/extensions/aboutaddons.html";
const PREF_LOGGING_ENABLED = "extensions.logging.enabled";
const PREF_INSTALL_REQUIREBUILTINCERTS =
"extensions.install.requireBuiltInCerts";
const PREF_INSTALL_REQUIRESECUREORIGIN =
"extensions.install.requireSecureOrigin";
const CHROME_NAME = "mochikit";
function getChromeRoot(path) {
if (path === undefined) {
return "chrome://" + CHROME_NAME + "/content/browser/" + RELATIVE_DIR;
}
return getRootDirectory(path);
}
function extractChromeRoot(path) {
var chromeRootPath = getChromeRoot(path);
var jar = getJar(chromeRootPath);
if (jar) {
var tmpdir = extractJarToTmp(jar);
return "file://" + tmpdir.path + "/";
}
return chromeRootPath;
}
function setInstallTriggerPrefs() {
Services.prefs.setBoolPref("extensions.InstallTrigger.enabled", true);
Services.prefs.setBoolPref("extensions.InstallTriggerImpl.enabled", true);
// Relax the user input requirements while running tests that call this test helper.
Services.prefs.setBoolPref("xpinstall.userActivation.required", false);
registerCleanupFunction(clearInstallTriggerPrefs);
}
function clearInstallTriggerPrefs() {
Services.prefs.clearUserPref("extensions.InstallTrigger.enabled");
Services.prefs.clearUserPref("extensions.InstallTriggerImpl.enabled");
Services.prefs.clearUserPref("xpinstall.userActivation.required");
}
/**
* This is a test harness designed to handle responding to UI during the process
* of installing an XPI. A test can set callbacks to hear about specific parts
* of the sequence.
* Before use setup must be called and finish must be called afterwards.
*/
var Harness = {
// If set then the callback is called when an install is attempted and
// software installation is disabled.
installDisabledCallback: null,
// If set then the callback is called when an install is attempted and
// then canceled.
installCancelledCallback: null,
// If set then the callback will be called when an install's origin is blocked.
installOriginBlockedCallback: null,
// If set then the callback will be called when an install is blocked by the
// whitelist. The callback should return true to continue with the install
// anyway.
installBlockedCallback: null,
// If set will be called in the event of authentication being needed to get
// the xpi. Should return a 2 element array of username and password, or
// null to not authenticate.
authenticationCallback: null,
// If set this will be called to allow checking the contents of the xpinstall
// confirmation dialog. The callback should return true to continue the install.
installConfirmCallback: null,
// If set will be called when downloading of an item has begun.
downloadStartedCallback: null,
// If set will be called during the download of an item.
downloadProgressCallback: null,
// If set will be called when an xpi fails to download.
downloadFailedCallback: null,
// If set will be called when an xpi download is cancelled.
downloadCancelledCallback: null,
// If set will be called when downloading of an item has ended.
downloadEndedCallback: null,
// If set will be called when installation by the extension manager of an xpi
// item starts
installStartedCallback: null,
// If set will be called when an xpi fails to install.
installFailedCallback: null,
// If set will be called when each xpi item to be installed completes
// installation.
installEndedCallback: null,
// If set will be called when all triggered items are installed or the install
// is canceled.
installsCompletedCallback: null,
// If set the harness will wait for this DOM event before calling
// installsCompletedCallback
finalContentEvent: null,
waitingForEvent: false,
pendingCount: null,
installCount: null,
runningInstalls: null,
waitingForFinish: false,
// A unique value to return from the installConfirmCallback to indicate that
// the install UI shouldn't be closed automatically
leaveOpen: {},
// Setup and tear down functions
setup(win = window) {
if (!this.waitingForFinish) {
waitForExplicitFinish();
this.waitingForFinish = true;
Services.prefs.setBoolPref(PREF_INSTALL_REQUIRESECUREORIGIN, false);
Services.prefs.setBoolPref(PREF_LOGGING_ENABLED, true);
Services.prefs.setBoolPref(
"network.cookieJarSettings.unblocked_for_testing",
true
);
Services.obs.addObserver(this, "addon-install-started");
Services.obs.addObserver(this, "addon-install-disabled");
Services.obs.addObserver(this, "addon-install-origin-blocked");
Services.obs.addObserver(this, "addon-install-blocked");
Services.obs.addObserver(this, "addon-install-failed");
// For browser_auth tests which trigger auth dialogs.
Services.obs.addObserver(this, "common-dialog-loaded");
this._boundWin = Cu.getWeakReference(win); // need this so our addon manager listener knows which window to use.
AddonManager.addInstallListener(this);
AddonManager.addAddonListener(this);
win.addEventListener("popupshown", this);
win.PanelUI.notificationPanel.addEventListener("popupshown", this);
var self = this;
registerCleanupFunction(async function () {
Services.prefs.clearUserPref(PREF_LOGGING_ENABLED);
Services.prefs.clearUserPref(PREF_INSTALL_REQUIRESECUREORIGIN);
Services.prefs.clearUserPref(
"network.cookieJarSettings.unblocked_for_testing"
);
Services.obs.removeObserver(self, "addon-install-started");
Services.obs.removeObserver(self, "addon-install-disabled");
Services.obs.removeObserver(self, "addon-install-origin-blocked");
Services.obs.removeObserver(self, "addon-install-blocked");
Services.obs.removeObserver(self, "addon-install-failed");
Services.obs.removeObserver(self, "common-dialog-loaded");
AddonManager.removeInstallListener(self);
AddonManager.removeAddonListener(self);
win.removeEventListener("popupshown", self);
win.PanelUI.notificationPanel.removeEventListener("popupshown", self);
win = null;
let aInstalls = await AddonManager.getAllInstalls();
is(
aInstalls.length,
0,
"Should be no active installs at the end of the test"
);
await Promise.all(
aInstalls.map(async function (aInstall) {
info(
"Install for " +
aInstall.sourceURI +
" is in state " +
aInstall.state
);
if (aInstall.state == AddonManager.STATE_INSTALLED) {
await aInstall.addon.uninstall();
} else {
aInstall.cancel();
}
})
);
});
}
this.installCount = 0;
this.pendingCount = 0;
this.runningInstalls = [];
},
finish(win = window) {
// Some tests using this harness somehow finish leaving
// the addon-installed panel open. hiding here addresses
// that which fixes the rest of the tests. Since no test
// here cares about this panel, we just need it to close.
win.PanelUI.notificationPanel.hidePopup();
win.AppMenuNotifications.removeNotification("addon-installed");
delete this._boundWin;
finish();
},
endTest() {
let callback = this.installsCompletedCallback;
let count = this.installCount;
is(this.runningInstalls.length, 0, "Should be no running installs left");
this.runningInstalls.forEach(function (aInstall) {
info(
"Install for " + aInstall.sourceURI + " is in state " + aInstall.state
);
});
this.installOriginBlockedCallback = null;
this.installBlockedCallback = null;
this.authenticationCallback = null;
this.installConfirmCallback = null;
this.downloadStartedCallback = null;
this.downloadProgressCallback = null;
this.downloadCancelledCallback = null;
this.downloadFailedCallback = null;
this.downloadEndedCallback = null;
this.installStartedCallback = null;
this.installFailedCallback = null;
this.installEndedCallback = null;
this.installsCompletedCallback = null;
this.runningInstalls = null;
if (callback) {
executeSoon(() => callback(count));
}
},
promptReady(dialog) {
let promptType = dialog.args.promptType;
switch (promptType) {
case "alert":
case "alertCheck":
case "confirmCheck":
case "confirm":
case "confirmEx":
PromptTestUtils.handlePrompt(dialog, { buttonNumClick: 0 });
break;
case "promptUserAndPass":
// This is a login dialog, hopefully an authentication prompt
// for the xpi.
if (this.authenticationCallback) {
var auth = this.authenticationCallback();
if (auth && auth.length == 2) {
PromptTestUtils.handlePrompt(dialog, {
loginInput: auth[0],
passwordInput: auth[1],
buttonNumClick: 0,
});
} else {
PromptTestUtils.handlePrompt(dialog, { buttonNumClick: 1 });
}
} else {
PromptTestUtils.handlePrompt(dialog, { buttonNumClick: 1 });
}
break;
default:
ok(false, "prompt type " + promptType + " not handled in test.");
break;
}
},
popupReady(panel) {
if (this.installBlockedCallback) {
ok(false, "Should have been blocked by the whitelist");
}
this.pendingCount++;
// If there is a confirm callback then its return status determines whether
// to install the items or not. If not the test is over.
let result = true;
if (this.installConfirmCallback) {
result = this.installConfirmCallback(panel);
if (result === this.leaveOpen) {
return;
}
}
const panelEl = panel.closest("panel");
const panelState = panelEl.state;
const clickButton = () => {
info(`Clicking ${result ? "primary" : "secondary"} panel button`);
Assert.equal(
panelEl.state,
"open",
"Expect panel state to be open when clicking panel buttons"
);
if (!result) {
panel.secondaryButton.click();
} else {
panel.button.click();
}
};
if (panelState === "showing") {
info(
"panel is still showing, wait for 'popup-shown' topic to be notified"
);
BrowserUtils.promiseObserved(
"popup-shown",
shownPanel => shownPanel === panelEl
).then(clickButton);
} else {
clickButton();
}
},
handleEvent(event) {
if (event.type === "popupshown") {
if (event.target == event.view.PanelUI.notificationPanel) {
event.view.PanelUI.notificationPanel.hidePopup();
} else if (event.target.firstElementChild) {
let popupId = event.target.firstElementChild.getAttribute("popupid");
if (popupId === "addon-webext-permissions") {
this.popupReady(event.target.firstElementChild);
} else if (popupId === "addon-install-failed") {
event.target.firstElementChild.button.click();
}
}
}
},
// Install blocked handling
installDisabled(installInfo) {
ok(
!!this.installDisabledCallback,
"Installation shouldn't have been disabled"
);
if (this.installDisabledCallback) {
this.installDisabledCallback(installInfo);
}
this.expectingCancelled = true;
this.expectingCancelled = false;
this.endTest();
},
installCancelled(installInfo) {
if (this.expectingCancelled) {
return;
}
ok(
!!this.installCancelledCallback,
"Installation shouldn't have been cancelled"
);
if (this.installCancelledCallback) {
this.installCancelledCallback(installInfo);
}
this.endTest();
},
installOriginBlocked(installInfo) {
ok(!!this.installOriginBlockedCallback, "Shouldn't have been blocked");
if (this.installOriginBlockedCallback) {
this.installOriginBlockedCallback(installInfo);
}
this.endTest();
},
installBlocked(installInfo) {
ok(
!!this.installBlockedCallback,
"Shouldn't have been blocked by the whitelist"
);
if (
this.installBlockedCallback &&
this.installBlockedCallback(installInfo)
) {
this.installBlockedCallback = null;
installInfo.install();
} else {
this.expectingCancelled = true;
installInfo.installs.forEach(function (install) {
install.cancel();
});
this.expectingCancelled = false;
this.endTest();
}
},
// Addon Install Listener
onNewInstall(install) {
this.runningInstalls.push(install);
if (this.finalContentEvent && !this.waitingForEvent) {
this.waitingForEvent = true;
info("Waiting for " + this.finalContentEvent);
BrowserTestUtils.waitForContentEvent(
this._boundWin.get().gBrowser.selectedBrowser,
this.finalContentEvent,
true,
null,
true
).then(() => {
info("Saw " + this.finalContentEvent + "," + this.waitingForEvent);
this.waitingForEvent = false;
if (this.pendingCount == 0) {
this.endTest();
}
});
}
},
onDownloadStarted(install) {
this.pendingCount++;
if (this.downloadStartedCallback) {
this.downloadStartedCallback(install);
}
},
onDownloadProgress(install) {
if (this.downloadProgressCallback) {
this.downloadProgressCallback(install);
}
},
onDownloadEnded(install) {
if (this.downloadEndedCallback) {
this.downloadEndedCallback(install);
}
},
onDownloadCancelled(install) {
isnot(
this.runningInstalls.indexOf(install),
-1,
"Should only see cancelations for started installs"
);
this.runningInstalls.splice(this.runningInstalls.indexOf(install), 1);
if (
this.downloadCancelledCallback &&
this.downloadCancelledCallback(install) === false
) {
return;
}
this.checkTestEnded();
},
onDownloadFailed(install) {
if (this.downloadFailedCallback) {
this.downloadFailedCallback(install);
}
this.checkTestEnded();
},
onInstallStarted(install) {
if (this.installStartedCallback) {
this.installStartedCallback(install);
}
},
async onInstallEnded(install, addon) {
this.installCount++;
if (this.installEndedCallback) {
await this.installEndedCallback(install, addon);
}
this.checkTestEnded();
},
onInstallFailed(install) {
if (this.installFailedCallback) {
this.installFailedCallback(install);
}
this.checkTestEnded();
},
onUninstalled(addon) {
let idx = this.runningInstalls.findIndex(install => install.addon == addon);
if (idx != -1) {
this.runningInstalls.splice(idx, 1);
this.checkTestEnded();
}
},
onInstallCancelled(install) {
// This is ugly. We have a bunch of tests that cancel installs
// but don't expect this event to be raised.
// For at least one test (browser_whitelist3.js), we used to generate
// onDownloadCancelled when the user cancelled the installation at the
// confirmation prompt. We're now generating onInstallCancelled instead
// of onDownloadCancelled but making this code unconditional breaks a
// bunch of other tests. Ugh.
let idx = this.runningInstalls.indexOf(install);
if (idx != -1) {
this.runningInstalls.splice(this.runningInstalls.indexOf(install), 1);
this.checkTestEnded();
}
},
checkTestEnded() {
if (--this.pendingCount == 0 && !this.waitingForEvent) {
this.endTest();
}
},
// nsIObserver
observe(subject, topic) {
var installInfo = subject.wrappedJSObject;
switch (topic) {
case "addon-install-started":
is(
this.runningInstalls.length,
installInfo.installs.length,
"Should have seen the expected number of installs started"
);
break;
case "addon-install-disabled":
this.installDisabled(installInfo);
break;
case "addon-install-cancelled":
this.installCancelled(installInfo);
break;
case "addon-install-origin-blocked":
this.installOriginBlocked(installInfo);
break;
case "addon-install-blocked":
this.installBlocked(installInfo);
break;
case "addon-install-failed":
installInfo.installs.forEach(function (aInstall) {
isnot(
this.runningInstalls.indexOf(aInstall),
-1,
"Should only see failures for started installs"
);
ok(
aInstall.error != 0 || aInstall.addon.appDisabled,
"Failed installs should have an error or be appDisabled"
);
this.runningInstalls.splice(
this.runningInstalls.indexOf(aInstall),
1
);
}, this);
break;
case "common-dialog-loaded":
this.promptReady(subject.Dialog);
break;
}
},
QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
};