/* eslint no-unused-vars: ["error", {vars: "local", args: "none"}] */
const { PermissionTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/PermissionTestUtils.sys.mjs"
);
const { PromptTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/PromptTestUtils.sys.mjs"
);
const RELATIVE_DIR =
"toolkit/mozapps/extensions/test/xpinstall/";
const TESTROOT =
"http://example.com/browser/" + RELATIVE_DIR;
const TESTROOT2 =
"http://example.org/browser/" + RELATIVE_DIR;
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"]),
};