561 lines
17 KiB
JavaScript
561 lines
17 KiB
JavaScript
/* 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"]),
|
|
};
|