diff options
Diffstat (limited to 'browser/base/content/test/general/head.js')
-rw-r--r-- | browser/base/content/test/general/head.js | 1069 |
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 000000000..6c28615fe --- /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; +} |