summaryrefslogtreecommitdiff
path: root/browser/base/content/test/general/head.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/base/content/test/general/head.js')
-rw-r--r--browser/base/content/test/general/head.js1069
1 files changed, 1069 insertions, 0 deletions
diff --git a/browser/base/content/test/general/head.js b/browser/base/content/test/general/head.js
new file mode 100644
index 0000000000..6c28615fee
--- /dev/null
+++ b/browser/base/content/test/general/head.js
@@ -0,0 +1,1069 @@
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesTestUtils",
+ "resource://testing-common/PlacesTestUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "TabCrashHandler",
+ "resource:///modules/ContentCrashHandlers.jsm");
+
+/**
+ * Wait for a <notification> to be closed then call the specified callback.
+ */
+function waitForNotificationClose(notification, cb) {
+ let parent = notification.parentNode;
+
+ let observer = new MutationObserver(function onMutatations(mutations) {
+ for (let mutation of mutations) {
+ for (let i = 0; i < mutation.removedNodes.length; i++) {
+ let node = mutation.removedNodes.item(i);
+ if (node != notification) {
+ continue;
+ }
+ observer.disconnect();
+ cb();
+ }
+ }
+ });
+ observer.observe(parent, {childList: true});
+}
+
+function closeAllNotifications () {
+ let notificationBox = document.getElementById("global-notificationbox");
+
+ if (!notificationBox || !notificationBox.currentNotification) {
+ return Promise.resolve();
+ }
+
+ let deferred = Promise.defer();
+ for (let notification of notificationBox.allNotifications) {
+ waitForNotificationClose(notification, function () {
+ if (notificationBox.allNotifications.length === 0) {
+ deferred.resolve();
+ }
+ });
+ notification.close();
+ }
+
+ return deferred.promise;
+}
+
+function whenDelayedStartupFinished(aWindow, aCallback) {
+ Services.obs.addObserver(function observer(aSubject, aTopic) {
+ if (aWindow == aSubject) {
+ Services.obs.removeObserver(observer, aTopic);
+ executeSoon(aCallback);
+ }
+ }, "browser-delayed-startup-finished", false);
+}
+
+function updateTabContextMenu(tab, onOpened) {
+ let menu = document.getElementById("tabContextMenu");
+ if (!tab)
+ tab = gBrowser.selectedTab;
+ var evt = new Event("");
+ tab.dispatchEvent(evt);
+ menu.openPopup(tab, "end_after", 0, 0, true, false, evt);
+ is(TabContextMenu.contextTab, tab, "TabContextMenu context is the expected tab");
+ const onFinished = () => menu.hidePopup();
+ if (onOpened) {
+ return Task.spawn(function*() {
+ yield onOpened();
+ onFinished();
+ });
+ }
+ onFinished();
+ return Promise.resolve();
+}
+
+function openToolbarCustomizationUI(aCallback, aBrowserWin) {
+ if (!aBrowserWin)
+ aBrowserWin = window;
+
+ aBrowserWin.gCustomizeMode.enter();
+
+ aBrowserWin.gNavToolbox.addEventListener("customizationready", function UI_loaded() {
+ aBrowserWin.gNavToolbox.removeEventListener("customizationready", UI_loaded);
+ executeSoon(function() {
+ aCallback(aBrowserWin)
+ });
+ });
+}
+
+function closeToolbarCustomizationUI(aCallback, aBrowserWin) {
+ aBrowserWin.gNavToolbox.addEventListener("aftercustomization", function unloaded() {
+ aBrowserWin.gNavToolbox.removeEventListener("aftercustomization", unloaded);
+ executeSoon(aCallback);
+ });
+
+ aBrowserWin.gCustomizeMode.exit();
+}
+
+function waitForCondition(condition, nextTest, errorMsg, retryTimes) {
+ retryTimes = typeof retryTimes !== 'undefined' ? retryTimes : 30;
+ var tries = 0;
+ var interval = setInterval(function() {
+ if (tries >= retryTimes) {
+ ok(false, errorMsg);
+ moveOn();
+ }
+ var conditionPassed;
+ try {
+ conditionPassed = condition();
+ } catch (e) {
+ ok(false, e + "\n" + e.stack);
+ conditionPassed = false;
+ }
+ if (conditionPassed) {
+ moveOn();
+ }
+ tries++;
+ }, 100);
+ var moveOn = function() { clearInterval(interval); nextTest(); };
+}
+
+function promiseWaitForCondition(aConditionFn) {
+ let deferred = Promise.defer();
+ waitForCondition(aConditionFn, deferred.resolve, "Condition didn't pass.");
+ return deferred.promise;
+}
+
+function promiseWaitForEvent(object, eventName, capturing = false, chrome = false) {
+ return new Promise((resolve) => {
+ function listener(event) {
+ info("Saw " + eventName);
+ object.removeEventListener(eventName, listener, capturing, chrome);
+ resolve(event);
+ }
+
+ info("Waiting for " + eventName);
+ object.addEventListener(eventName, listener, capturing, chrome);
+ });
+}
+
+/**
+ * Allows setting focus on a window, and waiting for that window to achieve
+ * focus.
+ *
+ * @param aWindow
+ * The window to focus and wait for.
+ *
+ * @return {Promise}
+ * @resolves When the window is focused.
+ * @rejects Never.
+ */
+function promiseWaitForFocus(aWindow) {
+ return new Promise((resolve) => {
+ waitForFocus(resolve, aWindow);
+ });
+}
+
+function getTestPlugin(aName) {
+ var pluginName = aName || "Test Plug-in";
+ var ph = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
+ var tags = ph.getPluginTags();
+
+ // Find the test plugin
+ for (var i = 0; i < tags.length; i++) {
+ if (tags[i].name == pluginName)
+ return tags[i];
+ }
+ ok(false, "Unable to find plugin");
+ return null;
+}
+
+// call this to set the test plugin(s) initially expected enabled state.
+// it will automatically be reset to it's previous value after the test
+// ends
+function setTestPluginEnabledState(newEnabledState, pluginName) {
+ var plugin = getTestPlugin(pluginName);
+ var oldEnabledState = plugin.enabledState;
+ plugin.enabledState = newEnabledState;
+ SimpleTest.registerCleanupFunction(function() {
+ getTestPlugin(pluginName).enabledState = oldEnabledState;
+ });
+}
+
+function pushPrefs(...aPrefs) {
+ let deferred = Promise.defer();
+ SpecialPowers.pushPrefEnv({"set": aPrefs}, deferred.resolve);
+ return deferred.promise;
+}
+
+function updateBlocklist(aCallback) {
+ var blocklistNotifier = Cc["@mozilla.org/extensions/blocklist;1"]
+ .getService(Ci.nsITimerCallback);
+ var observer = function() {
+ Services.obs.removeObserver(observer, "blocklist-updated");
+ SimpleTest.executeSoon(aCallback);
+ };
+ Services.obs.addObserver(observer, "blocklist-updated", false);
+ blocklistNotifier.notify(null);
+}
+
+var _originalTestBlocklistURL = null;
+function setAndUpdateBlocklist(aURL, aCallback) {
+ if (!_originalTestBlocklistURL)
+ _originalTestBlocklistURL = Services.prefs.getCharPref("extensions.blocklist.url");
+ Services.prefs.setCharPref("extensions.blocklist.url", aURL);
+ updateBlocklist(aCallback);
+}
+
+function resetBlocklist() {
+ Services.prefs.setCharPref("extensions.blocklist.url", _originalTestBlocklistURL);
+}
+
+function whenNewWindowLoaded(aOptions, aCallback) {
+ let win = OpenBrowserWindow(aOptions);
+ win.addEventListener("load", function onLoad() {
+ win.removeEventListener("load", onLoad, false);
+ aCallback(win);
+ }, false);
+}
+
+function promiseWindowWillBeClosed(win) {
+ return new Promise((resolve, reject) => {
+ Services.obs.addObserver(function observe(subject, topic) {
+ if (subject == win) {
+ Services.obs.removeObserver(observe, topic);
+ resolve();
+ }
+ }, "domwindowclosed", false);
+ });
+}
+
+function promiseWindowClosed(win) {
+ let promise = promiseWindowWillBeClosed(win);
+ win.close();
+ return promise;
+}
+
+function promiseOpenAndLoadWindow(aOptions, aWaitForDelayedStartup=false) {
+ let deferred = Promise.defer();
+ let win = OpenBrowserWindow(aOptions);
+ if (aWaitForDelayedStartup) {
+ Services.obs.addObserver(function onDS(aSubject, aTopic, aData) {
+ if (aSubject != win) {
+ return;
+ }
+ Services.obs.removeObserver(onDS, "browser-delayed-startup-finished");
+ deferred.resolve(win);
+ }, "browser-delayed-startup-finished", false);
+
+ } else {
+ win.addEventListener("load", function onLoad() {
+ win.removeEventListener("load", onLoad);
+ deferred.resolve(win);
+ });
+ }
+ return deferred.promise;
+}
+
+/**
+ * Waits for all pending async statements on the default connection, before
+ * proceeding with aCallback.
+ *
+ * @param aCallback
+ * Function to be called when done.
+ * @param aScope
+ * Scope for the callback.
+ * @param aArguments
+ * Arguments array for the callback.
+ *
+ * @note The result is achieved by asynchronously executing a query requiring
+ * a write lock. Since all statements on the same connection are
+ * serialized, the end of this write operation means that all writes are
+ * complete. Note that WAL makes so that writers don't block readers, but
+ * this is a problem only across different connections.
+ */
+function waitForAsyncUpdates(aCallback, aScope, aArguments) {
+ let scope = aScope || this;
+ let args = aArguments || [];
+ let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
+ .DBConnection;
+ let begin = db.createAsyncStatement("BEGIN EXCLUSIVE");
+ begin.executeAsync();
+ begin.finalize();
+
+ let commit = db.createAsyncStatement("COMMIT");
+ commit.executeAsync({
+ handleResult: function() {},
+ handleError: function() {},
+ handleCompletion: function(aReason) {
+ aCallback.apply(scope, args);
+ }
+ });
+ commit.finalize();
+}
+
+/**
+ * Asynchronously check a url is visited.
+
+ * @param aURI The URI.
+ * @param aExpectedValue The expected value.
+ * @return {Promise}
+ * @resolves When the check has been added successfully.
+ * @rejects JavaScript exception.
+ */
+function promiseIsURIVisited(aURI, aExpectedValue) {
+ let deferred = Promise.defer();
+ PlacesUtils.asyncHistory.isURIVisited(aURI, function(unused, aIsVisited) {
+ deferred.resolve(aIsVisited);
+ });
+
+ return deferred.promise;
+}
+
+function whenNewTabLoaded(aWindow, aCallback) {
+ aWindow.BrowserOpenTab();
+
+ let browser = aWindow.gBrowser.selectedBrowser;
+ if (browser.contentDocument.readyState === "complete") {
+ aCallback();
+ return;
+ }
+
+ whenTabLoaded(aWindow.gBrowser.selectedTab, aCallback);
+}
+
+function whenTabLoaded(aTab, aCallback) {
+ promiseTabLoadEvent(aTab).then(aCallback);
+}
+
+function promiseTabLoaded(aTab) {
+ let deferred = Promise.defer();
+ whenTabLoaded(aTab, deferred.resolve);
+ return deferred.promise;
+}
+
+/**
+ * Ensures that the specified URIs are either cleared or not.
+ *
+ * @param aURIs
+ * Array of page URIs
+ * @param aShouldBeCleared
+ * True if each visit to the URI should be cleared, false otherwise
+ */
+function promiseHistoryClearedState(aURIs, aShouldBeCleared) {
+ let deferred = Promise.defer();
+ let callbackCount = 0;
+ let niceStr = aShouldBeCleared ? "no longer" : "still";
+ function callbackDone() {
+ if (++callbackCount == aURIs.length)
+ deferred.resolve();
+ }
+ aURIs.forEach(function (aURI) {
+ PlacesUtils.asyncHistory.isURIVisited(aURI, function(uri, isVisited) {
+ is(isVisited, !aShouldBeCleared,
+ "history visit " + uri.spec + " should " + niceStr + " exist");
+ callbackDone();
+ });
+ });
+
+ return deferred.promise;
+}
+
+/**
+ * Waits for the next top-level document load in the current browser. The URI
+ * of the document is compared against aExpectedURL. The load is then stopped
+ * before it actually starts.
+ *
+ * @param aExpectedURL
+ * The URL of the document that is expected to load.
+ * @param aStopFromProgressListener
+ * Whether to cancel the load directly from the progress listener. Defaults to true.
+ * If you're using this method to avoid hitting the network, you want the default (true).
+ * However, the browser UI will behave differently for loads stopped directly from
+ * the progress listener (effectively in the middle of a call to loadURI) and so there
+ * are cases where you may want to avoid stopping the load directly from within the
+ * progress listener callback.
+ * @return promise
+ */
+function waitForDocLoadAndStopIt(aExpectedURL, aBrowser=gBrowser.selectedBrowser, aStopFromProgressListener=true) {
+ function content_script(contentStopFromProgressListener) {
+ let { interfaces: Ci, utils: Cu } = Components;
+ Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+ let wp = docShell.QueryInterface(Ci.nsIWebProgress);
+
+ function stopContent(now, uri) {
+ if (now) {
+ /* Hammer time. */
+ content.stop();
+
+ /* Let the parent know we're done. */
+ sendAsyncMessage("Test:WaitForDocLoadAndStopIt", { uri });
+ } else {
+ setTimeout(stopContent.bind(null, true, uri), 0);
+ }
+ }
+
+ let progressListener = {
+ onStateChange: function (webProgress, req, flags, status) {
+ dump("waitForDocLoadAndStopIt: onStateChange " + flags.toString(16) + ": " + req.name + "\n");
+
+ if (webProgress.isTopLevel &&
+ flags & Ci.nsIWebProgressListener.STATE_START) {
+ wp.removeProgressListener(progressListener);
+
+ let chan = req.QueryInterface(Ci.nsIChannel);
+ dump(`waitForDocLoadAndStopIt: Document start: ${chan.URI.spec}\n`);
+
+ stopContent(contentStopFromProgressListener, chan.originalURI.spec);
+ }
+ },
+ QueryInterface: XPCOMUtils.generateQI(["nsISupportsWeakReference"])
+ };
+ wp.addProgressListener(progressListener, wp.NOTIFY_STATE_WINDOW);
+
+ /**
+ * As |this| is undefined and we can't extend |docShell|, adding an unload
+ * event handler is the easiest way to ensure the weakly referenced
+ * progress listener is kept alive as long as necessary.
+ */
+ addEventListener("unload", function () {
+ try {
+ wp.removeProgressListener(progressListener);
+ } catch (e) { /* Will most likely fail. */ }
+ });
+ }
+
+ return new Promise((resolve, reject) => {
+ function complete({ data }) {
+ is(data.uri, aExpectedURL, "waitForDocLoadAndStopIt: The expected URL was loaded");
+ mm.removeMessageListener("Test:WaitForDocLoadAndStopIt", complete);
+ resolve();
+ }
+
+ let mm = aBrowser.messageManager;
+ mm.loadFrameScript("data:,(" + content_script.toString() + ")(" + aStopFromProgressListener + ");", true);
+ mm.addMessageListener("Test:WaitForDocLoadAndStopIt", complete);
+ info("waitForDocLoadAndStopIt: Waiting for URL: " + aExpectedURL);
+ });
+}
+
+/**
+ * Waits for the next load to complete in any browser or the given browser.
+ * If a <tabbrowser> is given it waits for a load in any of its browsers.
+ *
+ * @return promise
+ */
+function waitForDocLoadComplete(aBrowser=gBrowser) {
+ return new Promise(resolve => {
+ let listener = {
+ onStateChange: function (webProgress, req, flags, status) {
+ let docStop = Ci.nsIWebProgressListener.STATE_IS_NETWORK |
+ Ci.nsIWebProgressListener.STATE_STOP;
+ info("Saw state " + flags.toString(16) + " and status " + status.toString(16));
+
+ // When a load needs to be retargetted to a new process it is cancelled
+ // with NS_BINDING_ABORTED so ignore that case
+ if ((flags & docStop) == docStop && status != Cr.NS_BINDING_ABORTED) {
+ aBrowser.removeProgressListener(this);
+ waitForDocLoadComplete.listeners.delete(this);
+
+ let chan = req.QueryInterface(Ci.nsIChannel);
+ info("Browser loaded " + chan.originalURI.spec);
+ resolve();
+ }
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
+ Ci.nsISupportsWeakReference])
+ };
+ aBrowser.addProgressListener(listener);
+ waitForDocLoadComplete.listeners.add(listener);
+ info("Waiting for browser load");
+ });
+}
+
+// Keep a set of progress listeners for waitForDocLoadComplete() to make sure
+// they're not GC'ed before we saw the page load.
+waitForDocLoadComplete.listeners = new Set();
+registerCleanupFunction(() => waitForDocLoadComplete.listeners.clear());
+
+var FullZoomHelper = {
+
+ selectTabAndWaitForLocationChange: function selectTabAndWaitForLocationChange(tab) {
+ if (!tab)
+ throw new Error("tab must be given.");
+ if (gBrowser.selectedTab == tab)
+ return Promise.resolve();
+
+ return Promise.all([BrowserTestUtils.switchTab(gBrowser, tab),
+ this.waitForLocationChange()]);
+ },
+
+ removeTabAndWaitForLocationChange: function removeTabAndWaitForLocationChange(tab) {
+ tab = tab || gBrowser.selectedTab;
+ let selected = gBrowser.selectedTab == tab;
+ gBrowser.removeTab(tab);
+ if (selected)
+ return this.waitForLocationChange();
+ return Promise.resolve();
+ },
+
+ waitForLocationChange: function waitForLocationChange() {
+ return new Promise(resolve => {
+ Services.obs.addObserver(function obs(subj, topic, data) {
+ Services.obs.removeObserver(obs, topic);
+ resolve();
+ }, "browser-fullZoom:location-change", false);
+ });
+ },
+
+ load: function load(tab, url) {
+ return new Promise(resolve => {
+ let didLoad = false;
+ let didZoom = false;
+
+ promiseTabLoadEvent(tab).then(event => {
+ didLoad = true;
+ if (didZoom)
+ resolve();
+ }, true);
+
+ this.waitForLocationChange().then(function () {
+ didZoom = true;
+ if (didLoad)
+ resolve();
+ });
+
+ tab.linkedBrowser.loadURI(url);
+ });
+ },
+
+ zoomTest: function zoomTest(tab, val, msg) {
+ is(ZoomManager.getZoomForBrowser(tab.linkedBrowser), val, msg);
+ },
+
+ enlarge: function enlarge() {
+ return new Promise(resolve => FullZoom.enlarge(resolve));
+ },
+
+ reduce: function reduce() {
+ return new Promise(resolve => FullZoom.reduce(resolve));
+ },
+
+ reset: function reset() {
+ return FullZoom.reset();
+ },
+
+ BACK: 0,
+ FORWARD: 1,
+ navigate: function navigate(direction) {
+ return new Promise(resolve => {
+ let didPs = false;
+ let didZoom = false;
+
+ gBrowser.addEventListener("pageshow", function listener(event) {
+ gBrowser.removeEventListener("pageshow", listener, true);
+ didPs = true;
+ if (didZoom)
+ resolve();
+ }, true);
+
+ if (direction == this.BACK)
+ gBrowser.goBack();
+ else if (direction == this.FORWARD)
+ gBrowser.goForward();
+
+ this.waitForLocationChange().then(function () {
+ didZoom = true;
+ if (didPs)
+ resolve();
+ });
+ });
+ },
+
+ failAndContinue: function failAndContinue(func) {
+ return function (err) {
+ ok(false, err);
+ func();
+ };
+ },
+};
+
+/**
+ * Waits for a load (or custom) event to finish in a given tab. If provided
+ * load an uri into the tab.
+ *
+ * @param tab
+ * The tab to load into.
+ * @param [optional] url
+ * The url to load, or the current url.
+ * @return {Promise} resolved when the event is handled.
+ * @resolves to the received event
+ * @rejects if a valid load event is not received within a meaningful interval
+ */
+function promiseTabLoadEvent(tab, url)
+{
+ info("Wait tab event: load");
+
+ function handle(loadedUrl) {
+ if (loadedUrl === "about:blank" || (url && loadedUrl !== url)) {
+ info(`Skipping spurious load event for ${loadedUrl}`);
+ return false;
+ }
+
+ info("Tab event received: load");
+ return true;
+ }
+
+ let loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, handle);
+
+ if (url)
+ BrowserTestUtils.loadURI(tab.linkedBrowser, url);
+
+ return loaded;
+}
+
+/**
+ * Returns a Promise that resolves once a new tab has been opened in
+ * a xul:tabbrowser.
+ *
+ * @param aTabBrowser
+ * The xul:tabbrowser to monitor for a new tab.
+ * @return {Promise}
+ * Resolved when the new tab has been opened.
+ * @resolves to the TabOpen event that was fired.
+ * @rejects Never.
+ */
+function waitForNewTabEvent(aTabBrowser) {
+ return promiseWaitForEvent(aTabBrowser.tabContainer, "TabOpen");
+}
+
+/**
+ * Test the state of the identity box and control center to make
+ * sure they are correctly showing the expected mixed content states.
+ *
+ * @note The checks are done synchronously, but new code should wait on the
+ * returned Promise object to ensure the identity panel has closed.
+ * Bug 1221114 is filed to fix the existing code.
+ *
+ * @param tabbrowser
+ * @param Object states
+ * MUST include the following properties:
+ * {
+ * activeLoaded: true|false,
+ * activeBlocked: true|false,
+ * passiveLoaded: true|false,
+ * }
+ *
+ * @return {Promise}
+ * @resolves When the operation has finished and the identity panel has closed.
+ */
+function assertMixedContentBlockingState(tabbrowser, states = {}) {
+ if (!tabbrowser || !("activeLoaded" in states) ||
+ !("activeBlocked" in states) || !("passiveLoaded" in states)) {
+ throw new Error("assertMixedContentBlockingState requires a browser and a states object");
+ }
+
+ let {passiveLoaded, activeLoaded, activeBlocked} = states;
+ let {gIdentityHandler} = tabbrowser.ownerGlobal;
+ let doc = tabbrowser.ownerDocument;
+ let identityBox = gIdentityHandler._identityBox;
+ let classList = identityBox.classList;
+ let connectionIcon = doc.getElementById("connection-icon");
+ let connectionIconImage = tabbrowser.ownerGlobal.getComputedStyle(connectionIcon).
+ getPropertyValue("list-style-image");
+
+ let stateSecure = gIdentityHandler._state & Ci.nsIWebProgressListener.STATE_IS_SECURE;
+ let stateBroken = gIdentityHandler._state & Ci.nsIWebProgressListener.STATE_IS_BROKEN;
+ let stateInsecure = gIdentityHandler._state & Ci.nsIWebProgressListener.STATE_IS_INSECURE;
+ let stateActiveBlocked = gIdentityHandler._state & Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT;
+ let stateActiveLoaded = gIdentityHandler._state & Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT;
+ let statePassiveLoaded = gIdentityHandler._state & Ci.nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT;
+
+ is(activeBlocked, !!stateActiveBlocked, "Expected state for activeBlocked matches UI state");
+ is(activeLoaded, !!stateActiveLoaded, "Expected state for activeLoaded matches UI state");
+ is(passiveLoaded, !!statePassiveLoaded, "Expected state for passiveLoaded matches UI state");
+
+ if (stateInsecure) {
+ // HTTP request, there should be no MCB classes for the identity box and the non secure icon
+ // should always be visible regardless of MCB state.
+ ok(classList.contains("unknownIdentity"), "unknownIdentity on HTTP page");
+ is_element_hidden(connectionIcon);
+
+ ok(!classList.contains("mixedActiveContent"), "No MCB icon on HTTP page");
+ ok(!classList.contains("mixedActiveBlocked"), "No MCB icon on HTTP page");
+ ok(!classList.contains("mixedDisplayContent"), "No MCB icon on HTTP page");
+ ok(!classList.contains("mixedDisplayContentLoadedActiveBlocked"), "No MCB icon on HTTP page");
+ } else {
+ // Make sure the identity box UI has the correct mixedcontent states and icons
+ is(classList.contains("mixedActiveContent"), activeLoaded,
+ "identityBox has expected class for activeLoaded");
+ is(classList.contains("mixedActiveBlocked"), activeBlocked && !passiveLoaded,
+ "identityBox has expected class for activeBlocked && !passiveLoaded");
+ is(classList.contains("mixedDisplayContent"), passiveLoaded && !(activeLoaded || activeBlocked),
+ "identityBox has expected class for passiveLoaded && !(activeLoaded || activeBlocked)");
+ is(classList.contains("mixedDisplayContentLoadedActiveBlocked"), passiveLoaded && activeBlocked,
+ "identityBox has expected class for passiveLoaded && activeBlocked");
+
+ is_element_visible(connectionIcon);
+ if (activeLoaded) {
+ is(connectionIconImage, "url(\"chrome://browser/skin/connection-mixed-active-loaded.svg#icon\")",
+ "Using active loaded icon");
+ }
+ if (activeBlocked && !passiveLoaded) {
+ is(connectionIconImage, "url(\"chrome://browser/skin/connection-secure.svg\")",
+ "Using active blocked icon");
+ }
+ if (passiveLoaded && !(activeLoaded || activeBlocked)) {
+ is(connectionIconImage, "url(\"chrome://browser/skin/connection-mixed-passive-loaded.svg#icon\")",
+ "Using passive loaded icon");
+ }
+ if (passiveLoaded && activeBlocked) {
+ is(connectionIconImage, "url(\"chrome://browser/skin/connection-mixed-passive-loaded.svg#icon\")",
+ "Using active blocked and passive loaded icon");
+ }
+ }
+
+ // Make sure the identity popup has the correct mixedcontent states
+ gIdentityHandler._identityBox.click();
+ let popupAttr = doc.getElementById("identity-popup").getAttribute("mixedcontent");
+ let bodyAttr = doc.getElementById("identity-popup-securityView-body").getAttribute("mixedcontent");
+
+ is(popupAttr.includes("active-loaded"), activeLoaded,
+ "identity-popup has expected attr for activeLoaded");
+ is(bodyAttr.includes("active-loaded"), activeLoaded,
+ "securityView-body has expected attr for activeLoaded");
+
+ is(popupAttr.includes("active-blocked"), activeBlocked,
+ "identity-popup has expected attr for activeBlocked");
+ is(bodyAttr.includes("active-blocked"), activeBlocked,
+ "securityView-body has expected attr for activeBlocked");
+
+ is(popupAttr.includes("passive-loaded"), passiveLoaded,
+ "identity-popup has expected attr for passiveLoaded");
+ is(bodyAttr.includes("passive-loaded"), passiveLoaded,
+ "securityView-body has expected attr for passiveLoaded");
+
+ // Make sure the correct icon is visible in the Control Center.
+ // This logic is controlled with CSS, so this helps prevent regressions there.
+ let securityView = doc.getElementById("identity-popup-securityView");
+ let securityViewBG = tabbrowser.ownerGlobal.getComputedStyle(securityView).
+ getPropertyValue("background-image");
+ let securityContentBG = tabbrowser.ownerGlobal.getComputedStyle(securityView).
+ getPropertyValue("background-image");
+
+ if (stateInsecure) {
+ is(securityViewBG, "url(\"chrome://browser/skin/controlcenter/conn-not-secure.svg\")",
+ "CC using 'not secure' icon");
+ is(securityContentBG, "url(\"chrome://browser/skin/controlcenter/conn-not-secure.svg\")",
+ "CC using 'not secure' icon");
+ }
+
+ if (stateSecure) {
+ is(securityViewBG, "url(\"chrome://browser/skin/controlcenter/connection.svg#connection-secure\")",
+ "CC using secure icon");
+ is(securityContentBG, "url(\"chrome://browser/skin/controlcenter/connection.svg#connection-secure\")",
+ "CC using secure icon");
+ }
+
+ if (stateBroken) {
+ if (activeLoaded) {
+ is(securityViewBG, "url(\"chrome://browser/skin/controlcenter/mcb-disabled.svg\")",
+ "CC using active loaded icon");
+ is(securityContentBG, "url(\"chrome://browser/skin/controlcenter/mcb-disabled.svg\")",
+ "CC using active loaded icon");
+ } else if (activeBlocked || passiveLoaded) {
+ is(securityViewBG, "url(\"chrome://browser/skin/controlcenter/connection.svg#connection-degraded\")",
+ "CC using degraded icon");
+ is(securityContentBG, "url(\"chrome://browser/skin/controlcenter/connection.svg#connection-degraded\")",
+ "CC using degraded icon");
+ } else {
+ // There is a case here with weak ciphers, but no bc tests are handling this yet.
+ is(securityViewBG, "url(\"chrome://browser/skin/controlcenter/connection.svg#connection-degraded\")",
+ "CC using degraded icon");
+ is(securityContentBG, "url(\"chrome://browser/skin/controlcenter/connection.svg#connection-degraded\")",
+ "CC using degraded icon");
+ }
+ }
+
+ if (activeLoaded || activeBlocked || passiveLoaded) {
+ doc.getElementById("identity-popup-security-expander").click();
+ is(Array.filter(doc.querySelectorAll("[observes=identity-popup-mcb-learn-more]"),
+ element => !is_hidden(element)).length, 1,
+ "The 'Learn more' link should be visible once.");
+ }
+
+ gIdentityHandler._identityPopup.hidden = true;
+
+ // Wait for the panel to be closed before continuing. The promisePopupHidden
+ // function cannot be used because it's unreliable unless promisePopupShown is
+ // also called before closing the panel. This cannot be done until all callers
+ // are made asynchronous (bug 1221114).
+ return new Promise(resolve => executeSoon(resolve));
+}
+
+function is_hidden(element) {
+ var style = element.ownerGlobal.getComputedStyle(element);
+ if (style.display == "none")
+ return true;
+ if (style.visibility != "visible")
+ return true;
+ if (style.display == "-moz-popup")
+ return ["hiding", "closed"].indexOf(element.state) != -1;
+
+ // Hiding a parent element will hide all its children
+ if (element.parentNode != element.ownerDocument)
+ return is_hidden(element.parentNode);
+
+ return false;
+}
+
+function is_visible(element) {
+ var style = element.ownerGlobal.getComputedStyle(element);
+ if (style.display == "none")
+ return false;
+ if (style.visibility != "visible")
+ return false;
+ if (style.display == "-moz-popup" && element.state != "open")
+ return false;
+
+ // Hiding a parent element will hide all its children
+ if (element.parentNode != element.ownerDocument)
+ return is_visible(element.parentNode);
+
+ return true;
+}
+
+function is_element_visible(element, msg) {
+ isnot(element, null, "Element should not be null, when checking visibility");
+ ok(is_visible(element), msg || "Element should be visible");
+}
+
+function is_element_hidden(element, msg) {
+ isnot(element, null, "Element should not be null, when checking visibility");
+ ok(is_hidden(element), msg || "Element should be hidden");
+}
+
+function promisePopupEvent(popup, eventSuffix) {
+ let endState = {shown: "open", hidden: "closed"}[eventSuffix];
+
+ if (popup.state == endState)
+ return Promise.resolve();
+
+ let eventType = "popup" + eventSuffix;
+ let deferred = Promise.defer();
+ popup.addEventListener(eventType, function onPopupShown(event) {
+ popup.removeEventListener(eventType, onPopupShown);
+ deferred.resolve();
+ });
+
+ return deferred.promise;
+}
+
+function promisePopupShown(popup) {
+ return promisePopupEvent(popup, "shown");
+}
+
+function promisePopupHidden(popup) {
+ return promisePopupEvent(popup, "hidden");
+}
+
+function promiseNotificationShown(notification) {
+ let win = notification.browser.ownerGlobal;
+ if (win.PopupNotifications.panel.state == "open") {
+ return Promise.resolve();
+ }
+ let panelPromise = promisePopupShown(win.PopupNotifications.panel);
+ notification.reshow();
+ return panelPromise;
+}
+
+/**
+ * Allows waiting for an observer notification once.
+ *
+ * @param aTopic
+ * Notification topic to observe.
+ *
+ * @return {Promise}
+ * @resolves An object with subject and data properties from the observed
+ * notification.
+ * @rejects Never.
+ */
+function promiseTopicObserved(aTopic)
+{
+ return new Promise((resolve) => {
+ Services.obs.addObserver(
+ function PTO_observe(aSubject, aTopic2, aData) {
+ Services.obs.removeObserver(PTO_observe, aTopic2);
+ resolve({subject: aSubject, data: aData});
+ }, aTopic, false);
+ });
+}
+
+function promiseNewSearchEngine(basename) {
+ return new Promise((resolve, reject) => {
+ info("Waiting for engine to be added: " + basename);
+ let url = getRootDirectory(gTestPath) + basename;
+ Services.search.addEngine(url, null, "", false, {
+ onSuccess: function (engine) {
+ info("Search engine added: " + basename);
+ registerCleanupFunction(() => Services.search.removeEngine(engine));
+ resolve(engine);
+ },
+ onError: function (errCode) {
+ Assert.ok(false, "addEngine failed with error code " + errCode);
+ reject();
+ },
+ });
+ });
+}
+
+// Compares the security state of the page with what is expected
+function isSecurityState(expectedState) {
+ let ui = gTestBrowser.securityUI;
+ if (!ui) {
+ ok(false, "No security UI to get the security state");
+ return;
+ }
+
+ const wpl = Components.interfaces.nsIWebProgressListener;
+
+ // determine the security state
+ let isSecure = ui.state & wpl.STATE_IS_SECURE;
+ let isBroken = ui.state & wpl.STATE_IS_BROKEN;
+ let isInsecure = ui.state & wpl.STATE_IS_INSECURE;
+
+ let actualState;
+ if (isSecure && !(isBroken || isInsecure)) {
+ actualState = "secure";
+ } else if (isBroken && !(isSecure || isInsecure)) {
+ actualState = "broken";
+ } else if (isInsecure && !(isSecure || isBroken)) {
+ actualState = "insecure";
+ } else {
+ actualState = "unknown";
+ }
+
+ is(expectedState, actualState, "Expected state " + expectedState + " and the actual state is " + actualState + ".");
+}
+
+/**
+ * Resolves when a bookmark with the given uri is added.
+ */
+function promiseOnBookmarkItemAdded(aExpectedURI) {
+ return new Promise((resolve, reject) => {
+ let bookmarksObserver = {
+ onItemAdded: function (aItemId, aFolderId, aIndex, aItemType, aURI) {
+ info("Added a bookmark to " + aURI.spec);
+ PlacesUtils.bookmarks.removeObserver(bookmarksObserver);
+ if (aURI.equals(aExpectedURI)) {
+ resolve();
+ }
+ else {
+ reject(new Error("Added an unexpected bookmark"));
+ }
+ },
+ onBeginUpdateBatch: function () {},
+ onEndUpdateBatch: function () {},
+ onItemRemoved: function () {},
+ onItemChanged: function () {},
+ onItemVisited: function () {},
+ onItemMoved: function () {},
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsINavBookmarkObserver,
+ ])
+ };
+ info("Waiting for a bookmark to be added");
+ PlacesUtils.bookmarks.addObserver(bookmarksObserver, false);
+ });
+}
+
+function promiseErrorPageLoaded(browser) {
+ return new Promise(resolve => {
+ browser.addEventListener("DOMContentLoaded", function onLoad() {
+ browser.removeEventListener("DOMContentLoaded", onLoad, false, true);
+ resolve();
+ }, false, true);
+ });
+}
+
+function* loadBadCertPage(url) {
+ const EXCEPTION_DIALOG_URI = "chrome://pippki/content/exceptionDialog.xul";
+ let exceptionDialogResolved = new Promise(function(resolve) {
+ // When the certificate exception dialog has opened, click the button to add
+ // an exception.
+ let certExceptionDialogObserver = {
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic == "cert-exception-ui-ready") {
+ Services.obs.removeObserver(this, "cert-exception-ui-ready");
+ let certExceptionDialog = getCertExceptionDialog(EXCEPTION_DIALOG_URI);
+ ok(certExceptionDialog, "found exception dialog");
+ executeSoon(function() {
+ certExceptionDialog.documentElement.getButton("extra1").click();
+ resolve();
+ });
+ }
+ }
+ };
+
+ Services.obs.addObserver(certExceptionDialogObserver,
+ "cert-exception-ui-ready", false);
+ });
+
+ let loaded = BrowserTestUtils.waitForErrorPage(gBrowser.selectedBrowser);
+ yield BrowserTestUtils.loadURI(gBrowser.selectedBrowser, url);
+ yield loaded;
+
+ yield ContentTask.spawn(gBrowser.selectedBrowser, null, function*() {
+ content.document.getElementById("exceptionDialogButton").click();
+ });
+ yield exceptionDialogResolved;
+ yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+}
+
+// Utility function to get a handle on the certificate exception dialog.
+// Modified from toolkit/components/passwordmgr/test/prompt_common.js
+function getCertExceptionDialog(aLocation) {
+ let enumerator = Services.wm.getXULWindowEnumerator(null);
+
+ while (enumerator.hasMoreElements()) {
+ let win = enumerator.getNext();
+ let windowDocShell = win.QueryInterface(Ci.nsIXULWindow).docShell;
+
+ let containedDocShells = windowDocShell.getDocShellEnumerator(
+ Ci.nsIDocShellTreeItem.typeChrome,
+ Ci.nsIDocShell.ENUMERATE_FORWARDS);
+ while (containedDocShells.hasMoreElements()) {
+ // Get the corresponding document for this docshell
+ let childDocShell = containedDocShells.getNext();
+ let childDoc = childDocShell.QueryInterface(Ci.nsIDocShell)
+ .contentViewer
+ .DOMDocument;
+
+ if (childDoc.location.href == aLocation) {
+ return childDoc;
+ }
+ }
+ }
+ return undefined;
+}
+
+function setupRemoteClientsFixture(fixture) {
+ let oldRemoteClientsGetter =
+ Object.getOwnPropertyDescriptor(gFxAccounts, "remoteClients").get;
+
+ Object.defineProperty(gFxAccounts, "remoteClients", {
+ get: function() { return fixture; }
+ });
+ return oldRemoteClientsGetter;
+}
+
+function restoreRemoteClients(getter) {
+ Object.defineProperty(gFxAccounts, "remoteClients", {
+ get: getter
+ });
+}
+
+function* openMenuItemSubmenu(id) {
+ let menuPopup = document.getElementById(id).menupopup;
+ let menuPopupPromise = BrowserTestUtils.waitForEvent(menuPopup, "popupshown");
+ menuPopup.showPopup();
+ yield menuPopupPromise;
+}