diff options
Diffstat (limited to 'browser/modules/test')
43 files changed, 6715 insertions, 0 deletions
diff --git a/browser/modules/test/.eslintrc.js b/browser/modules/test/.eslintrc.js new file mode 100644 index 000000000..e2d7896f8 --- /dev/null +++ b/browser/modules/test/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + "extends": [ + "../../../testing/mochitest/browser.eslintrc.js" + ] +}; diff --git a/browser/modules/test/browser.ini b/browser/modules/test/browser.ini new file mode 100644 index 000000000..af624439c --- /dev/null +++ b/browser/modules/test/browser.ini @@ -0,0 +1,42 @@ +[DEFAULT] +support-files = + head.js + +[browser_BrowserUITelemetry_buckets.js] +[browser_BrowserUITelemetry_defaults.js] +[browser_BrowserUITelemetry_sidebar.js] +[browser_BrowserUITelemetry_syncedtabs.js] +[browser_ContentSearch.js] +skip-if = true # Bug 1308343 +support-files = + contentSearch.js + contentSearchBadImage.xml + contentSearchSuggestions.sjs + contentSearchSuggestions.xml + !/browser/components/search/test/head.js + !/browser/components/search/test/testEngine.xml +[browser_NetworkPrioritizer.js] +[browser_PermissionUI.js] +[browser_ProcessHangNotifications.js] +skip-if = !e10s +[browser_SelfSupportBackend.js] +support-files = + ../../components/uitour/test/uitour.html + ../../components/uitour/UITour-lib.js +[browser_taskbar_preview.js] +skip-if = os != "win" +[browser_UnsubmittedCrashHandler.js] +run-if = crashreporter +[browser_UsageTelemetry.js] +[browser_UsageTelemetry_private_and_restore.js] +[browser_UsageTelemetry_urlbar.js] +support-files = + usageTelemetrySearchSuggestions.sjs + usageTelemetrySearchSuggestions.xml +[browser_UsageTelemetry_searchbar.js] +support-files = + usageTelemetrySearchSuggestions.sjs + usageTelemetrySearchSuggestions.xml +[browser_UsageTelemetry_content.js] +[browser_UsageTelemetry_content_aboutHome.js] +[browser_urlBar_zoom.js] diff --git a/browser/modules/test/browser_BrowserUITelemetry_buckets.js b/browser/modules/test/browser_BrowserUITelemetry_buckets.js new file mode 100644 index 000000000..f55761705 --- /dev/null +++ b/browser/modules/test/browser_BrowserUITelemetry_buckets.js @@ -0,0 +1,97 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + WHERE'S MAH BUCKET?! + \ + ___ + .-9 9 `\ + =(:(::)= ; + |||| \ + |||| `-. + ,\|\| `, + / \ + ; `'---., + | `\ + ; / | + \ | / + ) \ __,.--\ / + .-' \,..._\ \` .-' .-' + `-=`` `: | /-/-/` + `.__/ +*/ + +"use strict"; + + +add_task(function* testBUIT() { + let s = {}; + Components.utils.import("resource:///modules/BrowserUITelemetry.jsm", s); + let BUIT = s.BrowserUITelemetry; + + registerCleanupFunction(function() { + BUIT.setBucket(null); + }); + + + // setBucket + is(BUIT.currentBucket, BUIT.BUCKET_DEFAULT, "Bucket should be default bucket"); + BUIT.setBucket("mah-bucket"); + is(BUIT.currentBucket, BUIT.BUCKET_PREFIX + "mah-bucket", "Bucket should have correct name"); + BUIT.setBucket(null); + is(BUIT.currentBucket, BUIT.BUCKET_DEFAULT, "Bucket should be reset to default"); + + + // _toTimeStr + is(BUIT._toTimeStr(10), "10ms", "Checking time string reprentation, 10ms"); + is(BUIT._toTimeStr(1000 + 10), "1s10ms", "Checking time string reprentation, 1s10ms"); + is(BUIT._toTimeStr((20 * 1000) + 10), "20s10ms", "Checking time string reprentation, 20s10ms"); + is(BUIT._toTimeStr(60 * 1000), "1m", "Checking time string reprentation, 1m"); + is(BUIT._toTimeStr(3 * 60 * 1000), "3m", "Checking time string reprentation, 3m"); + is(BUIT._toTimeStr((3 * 60 * 1000) + 1), "3m1ms", "Checking time string reprentation, 3m1ms"); + is(BUIT._toTimeStr((60 * 60 * 1000) + (10 * 60 * 1000)), "1h10m", "Checking time string reprentation, 1h10m"); + is(BUIT._toTimeStr(100 * 60 * 60 * 1000), "100h", "Checking time string reprentation, 100h"); + + + // setExpiringBucket + BUIT.setExpiringBucket("walrus", [1001, 2001, 3001, 10001]); + is(BUIT.currentBucket, BUIT.BUCKET_PREFIX + "walrus" + BUIT.BUCKET_SEPARATOR + "1s1ms", + "Bucket should be expiring and have time step of 1s1ms"); + + yield waitForConditionPromise(function() { + return BUIT.currentBucket == (BUIT.BUCKET_PREFIX + "walrus" + BUIT.BUCKET_SEPARATOR + "2s1ms"); + }, "Bucket should be expiring and have time step of 2s1ms"); + + yield waitForConditionPromise(function() { + return BUIT.currentBucket == (BUIT.BUCKET_PREFIX + "walrus" + BUIT.BUCKET_SEPARATOR + "3s1ms"); + }, "Bucket should be expiring and have time step of 3s1ms"); + + + // Interupt previous expiring bucket + BUIT.setExpiringBucket("walrus2", [1002, 2002]); + is(BUIT.currentBucket, BUIT.BUCKET_PREFIX + "walrus2" + BUIT.BUCKET_SEPARATOR + "1s2ms", + "Should be new expiring bucket, with time step of 1s2ms"); + + yield waitForConditionPromise(function() { + return BUIT.currentBucket == (BUIT.BUCKET_PREFIX + "walrus2" + BUIT.BUCKET_SEPARATOR + "2s2ms"); + }, "Should be new expiring bucket, with time step of 2s2ms"); + + + // Let expiring bucket expire + yield waitForConditionPromise(function() { + return BUIT.currentBucket == BUIT.BUCKET_DEFAULT; + }, "Bucket should have expired, default bucket should now be active"); + + + // Interupt expiring bucket with normal bucket + BUIT.setExpiringBucket("walrus3", [1003, 2003]); + is(BUIT.currentBucket, BUIT.BUCKET_PREFIX + "walrus3" + BUIT.BUCKET_SEPARATOR + "1s3ms", + "Should be new expiring bucket, with time step of 1s3ms"); + + BUIT.setBucket("mah-bucket"); + is(BUIT.currentBucket, BUIT.BUCKET_PREFIX + "mah-bucket", "Bucket should have correct name"); + + yield waitForConditionPromise(function() { + return BUIT.currentBucket == (BUIT.BUCKET_PREFIX + "mah-bucket"); + }, "Next step of old expiring bucket shouldn't have progressed"); +}); diff --git a/browser/modules/test/browser_BrowserUITelemetry_defaults.js b/browser/modules/test/browser_BrowserUITelemetry_defaults.js new file mode 100644 index 000000000..ced1bbce0 --- /dev/null +++ b/browser/modules/test/browser_BrowserUITelemetry_defaults.js @@ -0,0 +1,37 @@ +// The purpose of this test is to ensure that by default, BrowserUITelemetry +// isn't reporting any UI customizations. This is primarily so changes to +// customizableUI (eg, new buttons, button location changes) also have a +// corresponding BrowserUITelemetry change. + +function test() { + let s = {}; + Cu.import("resource:///modules/CustomizableUI.jsm", s); + Cu.import("resource:///modules/BrowserUITelemetry.jsm", s); + + let { CustomizableUI, BrowserUITelemetry } = s; + + // Bug 1278176 - DevEdition never has the UI in a default state by default. + if (!AppConstants.MOZ_DEV_EDITION) { + Assert.ok(CustomizableUI.inDefaultState, + "No other test should have left CUI in a dirty state."); + } + + let result = BrowserUITelemetry._getWindowMeasurements(window, 0); + + // Bug 1278176 - DevEdition always reports the developer-button is moved. + if (!AppConstants.MOZ_DEV_EDITION) { + Assert.deepEqual(result.defaultMoved, []); + } + Assert.deepEqual(result.nondefaultAdded, []); + // This one is a bit weird - the "social-share-button" is dynamically added + // to the toolbar as the feature is first used - but it's listed as being in + // the toolbar by default so it doesn't end up in nondefaultAdded once it + // is created. The end result is that it ends up in defaultRemoved before + // the feature has been activated. + // Bug 1273358 exists to fix this. + Assert.deepEqual(result.defaultRemoved, ["social-share-button"]); + + // And mochi insists there's only a single window with a single tab when + // starting a test, so check that for good measure. + Assert.deepEqual(result.visibleTabs, [1]); +} diff --git a/browser/modules/test/browser_BrowserUITelemetry_sidebar.js b/browser/modules/test/browser_BrowserUITelemetry_sidebar.js new file mode 100644 index 000000000..5f19eabd5 --- /dev/null +++ b/browser/modules/test/browser_BrowserUITelemetry_sidebar.js @@ -0,0 +1,56 @@ +// Test the sidebar counters in BrowserUITelemetry. +"use strict"; + +const { BrowserUITelemetry: BUIT } = Cu.import("resource:///modules/BrowserUITelemetry.jsm", {}); + +add_task(function* testSidebarOpenClose() { + // Reset BrowserUITelemetry's world. + BUIT._countableEvents = {}; + + yield SidebarUI.show("viewTabsSidebar"); + + let counts = BUIT._countableEvents[BUIT.currentBucket]; + + Assert.deepEqual(counts, { sidebar: { viewTabsSidebar: { show: 1 } } }); + yield SidebarUI.hide(); + Assert.deepEqual(counts, { sidebar: { viewTabsSidebar: { show: 1, hide: 1 } } }); + + yield SidebarUI.show("viewBookmarksSidebar"); + Assert.deepEqual(counts, { + sidebar: { + viewTabsSidebar: { show: 1, hide: 1 }, + viewBookmarksSidebar: { show: 1 }, + } + }); + // Re-open the tabs sidebar while bookmarks is open - bookmarks should + // record a close. + yield SidebarUI.show("viewTabsSidebar"); + Assert.deepEqual(counts, { + sidebar: { + viewTabsSidebar: { show: 2, hide: 1 }, + viewBookmarksSidebar: { show: 1, hide: 1 }, + } + }); + yield SidebarUI.hide(); + Assert.deepEqual(counts, { + sidebar: { + viewTabsSidebar: { show: 2, hide: 2 }, + viewBookmarksSidebar: { show: 1, hide: 1 }, + } + }); + // Toggle - this will re-open viewTabsSidebar + yield SidebarUI.toggle("viewTabsSidebar"); + Assert.deepEqual(counts, { + sidebar: { + viewTabsSidebar: { show: 3, hide: 2 }, + viewBookmarksSidebar: { show: 1, hide: 1 }, + } + }); + yield SidebarUI.toggle("viewTabsSidebar"); + Assert.deepEqual(counts, { + sidebar: { + viewTabsSidebar: { show: 3, hide: 3 }, + viewBookmarksSidebar: { show: 1, hide: 1 }, + } + }); +}); diff --git a/browser/modules/test/browser_BrowserUITelemetry_syncedtabs.js b/browser/modules/test/browser_BrowserUITelemetry_syncedtabs.js new file mode 100644 index 000000000..d3e1eac57 --- /dev/null +++ b/browser/modules/test/browser_BrowserUITelemetry_syncedtabs.js @@ -0,0 +1,114 @@ +// Test the SyncedTabs counters in BrowserUITelemetry. +"use strict"; + +const { BrowserUITelemetry: BUIT } = Cu.import("resource:///modules/BrowserUITelemetry.jsm", {}); +const {SyncedTabs} = Cu.import("resource://services-sync/SyncedTabs.jsm", {}); + +function mockSyncedTabs() { + // Mock SyncedTabs.jsm + let mockedInternal = { + get isConfiguredToSyncTabs() { return true; }, + getTabClients() { + return Promise.resolve([ + { + id: "guid_desktop", + type: "client", + name: "My Desktop", + tabs: [ + { + title: "http://example.com/10", + lastUsed: 10, // the most recent + }, + ], + } + ]); + }, + syncTabs() { + return Promise.resolve(); + }, + hasSyncedThisSession: true, + }; + + let oldInternal = SyncedTabs._internal; + SyncedTabs._internal = mockedInternal; + + // configure our broadcasters so we are in the right state. + document.getElementById("sync-reauth-state").hidden = true; + document.getElementById("sync-setup-state").hidden = true; + document.getElementById("sync-syncnow-state").hidden = false; + + registerCleanupFunction(() => { + SyncedTabs._internal = oldInternal; + + document.getElementById("sync-reauth-state").hidden = true; + document.getElementById("sync-setup-state").hidden = false; + document.getElementById("sync-syncnow-state").hidden = true; + }); +} + +mockSyncedTabs(); + +function promiseTabsUpdated() { + return new Promise(resolve => { + Services.obs.addObserver(function onNotification(aSubject, aTopic, aData) { + Services.obs.removeObserver(onNotification, aTopic); + resolve(); + }, "synced-tabs-menu:test:tabs-updated", false); + }); +} + +add_task(function* test_menu() { + // Reset BrowserUITelemetry's world. + BUIT._countableEvents = {}; + + let tabsUpdated = promiseTabsUpdated(); + + // check the button's functionality + yield PanelUI.show(); + + let syncButton = document.getElementById("sync-button"); + syncButton.click(); + + yield tabsUpdated; + // Get our 1 tab and click on it. + let tabList = document.getElementById("PanelUI-remotetabs-tabslist"); + let tabEntry = tabList.firstChild.nextSibling; + tabEntry.click(); + + let counts = BUIT._countableEvents[BUIT.currentBucket]; + Assert.deepEqual(counts, { + "click-builtin-item": { "sync-button": { left: 1 } }, + "synced-tabs": { open: { "toolbarbutton-subview": 1 } }, + }); +}); + +add_task(function* test_sidebar() { + // Reset BrowserUITelemetry's world. + BUIT._countableEvents = {}; + + yield SidebarUI.show('viewTabsSidebar'); + + let syncedTabsDeckComponent = SidebarUI.browser.contentWindow.syncedTabsDeckComponent; + + syncedTabsDeckComponent._accountStatus = () => Promise.resolve(true); + + // Once the tabs container has been selected (which here means "'selected' + // added to the class list") we are ready to test. + let container = SidebarUI.browser.contentDocument.querySelector(".tabs-container"); + let promiseUpdated = BrowserTestUtils.waitForAttribute("class", container); + + yield syncedTabsDeckComponent.updatePanel(); + yield promiseUpdated; + + let selectedPanel = syncedTabsDeckComponent.container.querySelector(".sync-state.selected"); + let tab = selectedPanel.querySelector(".tab"); + tab.click(); + let counts = BUIT._countableEvents[BUIT.currentBucket]; + Assert.deepEqual(counts, { + sidebar: { + viewTabsSidebar: { show: 1 }, + }, + "synced-tabs": { open: { sidebar: 1 } } + }); + yield SidebarUI.hide(); +}); diff --git a/browser/modules/test/browser_ContentSearch.js b/browser/modules/test/browser_ContentSearch.js new file mode 100644 index 000000000..97bd6ac51 --- /dev/null +++ b/browser/modules/test/browser_ContentSearch.js @@ -0,0 +1,425 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const TEST_MSG = "ContentSearchTest"; +const CONTENT_SEARCH_MSG = "ContentSearch"; +const TEST_CONTENT_SCRIPT_BASENAME = "contentSearch.js"; + +// This timeout is absurdly high to avoid random failures like bug 1087120. +// That bug was reported when the timeout was 5 seconds, so let's try 10. +const SUGGESTIONS_TIMEOUT = 10000; + +var gMsgMan; +/* eslint no-undef:"error" */ +/* import-globals-from ../../components/search/test/head.js */ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/components/search/test/head.js", + this); + +let originalEngine = Services.search.currentEngine; + +add_task(function* setup() { + yield promiseNewEngine("testEngine.xml", { + setAsCurrent: true, + testPath: "chrome://mochitests/content/browser/browser/components/search/test/", + }); + + registerCleanupFunction(() => { + Services.search.currentEngine = originalEngine; + }); +}); + +add_task(function* GetState() { + yield addTab(); + gMsgMan.sendAsyncMessage(TEST_MSG, { + type: "GetState", + }); + let msg = yield waitForTestMsg("State"); + checkMsg(msg, { + type: "State", + data: yield currentStateObj(), + }); +}); + +add_task(function* SetCurrentEngine() { + yield addTab(); + let newCurrentEngine = null; + let oldCurrentEngine = Services.search.currentEngine; + let engines = Services.search.getVisibleEngines(); + for (let engine of engines) { + if (engine != oldCurrentEngine) { + newCurrentEngine = engine; + break; + } + } + if (!newCurrentEngine) { + info("Couldn't find a non-selected search engine, " + + "skipping this part of the test"); + return; + } + gMsgMan.sendAsyncMessage(TEST_MSG, { + type: "SetCurrentEngine", + data: newCurrentEngine.name, + }); + let deferred = Promise.defer(); + Services.obs.addObserver(function obs(subj, topic, data) { + info("Test observed " + data); + if (data == "engine-current") { + ok(true, "Test observed engine-current"); + Services.obs.removeObserver(obs, "browser-search-engine-modified"); + deferred.resolve(); + } + }, "browser-search-engine-modified", false); + let searchPromise = waitForTestMsg("CurrentEngine"); + info("Waiting for test to observe engine-current..."); + yield deferred.promise; + let msg = yield searchPromise; + checkMsg(msg, { + type: "CurrentEngine", + data: yield currentEngineObj(newCurrentEngine), + }); + + Services.search.currentEngine = oldCurrentEngine; + msg = yield waitForTestMsg("CurrentEngine"); + checkMsg(msg, { + type: "CurrentEngine", + data: yield currentEngineObj(oldCurrentEngine), + }); +}); + +add_task(function* modifyEngine() { + yield addTab(); + let engine = Services.search.currentEngine; + let oldAlias = engine.alias; + engine.alias = "ContentSearchTest"; + let msg = yield waitForTestMsg("CurrentState"); + checkMsg(msg, { + type: "CurrentState", + data: yield currentStateObj(), + }); + engine.alias = oldAlias; + msg = yield waitForTestMsg("CurrentState"); + checkMsg(msg, { + type: "CurrentState", + data: yield currentStateObj(), + }); +}); + +add_task(function* search() { + yield addTab(); + let engine = Services.search.currentEngine; + let data = { + engineName: engine.name, + searchString: "ContentSearchTest", + healthReportKey: "ContentSearchTest", + searchPurpose: "ContentSearchTest", + }; + let submissionURL = + engine.getSubmission(data.searchString, "", data.whence).uri.spec; + gMsgMan.sendAsyncMessage(TEST_MSG, { + type: "Search", + data: data, + expectedURL: submissionURL, + }); + let msg = yield waitForTestMsg("loadStopped"); + Assert.equal(msg.data.url, submissionURL, "Correct search page loaded"); +}); + +add_task(function* searchInBackgroundTab() { + // This test is like search(), but it opens a new tab after starting a search + // in another. In other words, it performs a search in a background tab. The + // search page should be loaded in the same tab that performed the search, in + // the background tab. + yield addTab(); + let engine = Services.search.currentEngine; + let data = { + engineName: engine.name, + searchString: "ContentSearchTest", + healthReportKey: "ContentSearchTest", + searchPurpose: "ContentSearchTest", + }; + let submissionURL = + engine.getSubmission(data.searchString, "", data.whence).uri.spec; + gMsgMan.sendAsyncMessage(TEST_MSG, { + type: "Search", + data: data, + expectedURL: submissionURL, + }); + + let newTab = gBrowser.addTab(); + gBrowser.selectedTab = newTab; + registerCleanupFunction(() => gBrowser.removeTab(newTab)); + + let msg = yield waitForTestMsg("loadStopped"); + Assert.equal(msg.data.url, submissionURL, "Correct search page loaded"); +}); + +add_task(function* badImage() { + yield addTab(); + // If the bad image URI caused an exception to be thrown within ContentSearch, + // then we'll hang waiting for the CurrentState responses triggered by the new + // engine. That's what we're testing, and obviously it shouldn't happen. + let vals = yield waitForNewEngine("contentSearchBadImage.xml", 1); + let engine = vals[0]; + let finalCurrentStateMsg = vals[vals.length - 1]; + let expectedCurrentState = yield currentStateObj(); + let expectedEngine = + expectedCurrentState.engines.find(e => e.name == engine.name); + ok(!!expectedEngine, "Sanity check: engine should be in expected state"); + ok(expectedEngine.iconBuffer === null, + "Sanity check: icon array buffer of engine in expected state " + + "should be null: " + expectedEngine.iconBuffer); + checkMsg(finalCurrentStateMsg, { + type: "CurrentState", + data: expectedCurrentState, + }); + // Removing the engine triggers a final CurrentState message. Wait for it so + // it doesn't trip up subsequent tests. + Services.search.removeEngine(engine); + yield waitForTestMsg("CurrentState"); +}); + +add_task(function* GetSuggestions_AddFormHistoryEntry_RemoveFormHistoryEntry() { + yield addTab(); + + // Add the test engine that provides suggestions. + let vals = yield waitForNewEngine("contentSearchSuggestions.xml", 0); + let engine = vals[0]; + + let searchStr = "browser_ContentSearch.js-suggestions-"; + + // Add a form history suggestion and wait for Satchel to notify about it. + gMsgMan.sendAsyncMessage(TEST_MSG, { + type: "AddFormHistoryEntry", + data: searchStr + "form", + }); + let deferred = Promise.defer(); + Services.obs.addObserver(function onAdd(subj, topic, data) { + if (data == "formhistory-add") { + Services.obs.removeObserver(onAdd, "satchel-storage-changed"); + executeSoon(() => deferred.resolve()); + } + }, "satchel-storage-changed", false); + yield deferred.promise; + + // Send GetSuggestions using the test engine. Its suggestions should appear + // in the remote suggestions in the Suggestions response below. + gMsgMan.sendAsyncMessage(TEST_MSG, { + type: "GetSuggestions", + data: { + engineName: engine.name, + searchString: searchStr, + remoteTimeout: SUGGESTIONS_TIMEOUT, + }, + }); + + // Check the Suggestions response. + let msg = yield waitForTestMsg("Suggestions"); + checkMsg(msg, { + type: "Suggestions", + data: { + engineName: engine.name, + searchString: searchStr, + formHistory: [searchStr + "form"], + remote: [searchStr + "foo", searchStr + "bar"], + }, + }); + + // Delete the form history suggestion and wait for Satchel to notify about it. + gMsgMan.sendAsyncMessage(TEST_MSG, { + type: "RemoveFormHistoryEntry", + data: searchStr + "form", + }); + deferred = Promise.defer(); + Services.obs.addObserver(function onRemove(subj, topic, data) { + if (data == "formhistory-remove") { + Services.obs.removeObserver(onRemove, "satchel-storage-changed"); + executeSoon(() => deferred.resolve()); + } + }, "satchel-storage-changed", false); + yield deferred.promise; + + // Send GetSuggestions again. + gMsgMan.sendAsyncMessage(TEST_MSG, { + type: "GetSuggestions", + data: { + engineName: engine.name, + searchString: searchStr, + remoteTimeout: SUGGESTIONS_TIMEOUT, + }, + }); + + // The formHistory suggestions in the Suggestions response should be empty. + msg = yield waitForTestMsg("Suggestions"); + checkMsg(msg, { + type: "Suggestions", + data: { + engineName: engine.name, + searchString: searchStr, + formHistory: [], + remote: [searchStr + "foo", searchStr + "bar"], + }, + }); + + // Finally, clean up by removing the test engine. + Services.search.removeEngine(engine); + yield waitForTestMsg("CurrentState"); +}); + +function buffersEqual(actualArrayBuffer, expectedArrayBuffer) { + let expectedView = new Int8Array(expectedArrayBuffer); + let actualView = new Int8Array(actualArrayBuffer); + for (let i = 0; i < expectedView.length; i++) { + if (actualView[i] != expectedView[i]) { + return false; + } + } + return true; +} + +function arrayBufferEqual(actualArrayBuffer, expectedArrayBuffer) { + ok(actualArrayBuffer instanceof ArrayBuffer, "Actual value is ArrayBuffer."); + ok(expectedArrayBuffer instanceof ArrayBuffer, "Expected value is ArrayBuffer."); + Assert.equal(actualArrayBuffer.byteLength, expectedArrayBuffer.byteLength, + "Array buffers have the same length."); + ok(buffersEqual(actualArrayBuffer, expectedArrayBuffer), "Buffers are equal."); +} + +function checkArrayBuffers(actual, expected) { + if (actual instanceof ArrayBuffer) { + arrayBufferEqual(actual, expected); + } + if (typeof actual == "object") { + for (let i in actual) { + checkArrayBuffers(actual[i], expected[i]); + } + } +} + +function checkMsg(actualMsg, expectedMsgData) { + let actualMsgData = actualMsg.data; + SimpleTest.isDeeply(actualMsg.data, expectedMsgData, "Checking message"); + + // Engines contain ArrayBuffers which we have to compare byte by byte and + // not as Objects (like SimpleTest.isDeeply does). + checkArrayBuffers(actualMsgData, expectedMsgData); +} + +function waitForMsg(name, type) { + let deferred = Promise.defer(); + info("Waiting for " + name + " message " + type + "..."); + gMsgMan.addMessageListener(name, function onMsg(msg) { + info("Received " + name + " message " + msg.data.type + "\n"); + if (msg.data.type == type) { + gMsgMan.removeMessageListener(name, onMsg); + deferred.resolve(msg); + } + }); + return deferred.promise; +} + +function waitForTestMsg(type) { + return waitForMsg(TEST_MSG, type); +} + +function waitForNewEngine(basename, numImages) { + info("Waiting for engine to be added: " + basename); + + // Wait for the search events triggered by adding the new engine. + // engine-added engine-loaded + let expectedSearchEvents = ["CurrentState", "CurrentState"]; + // engine-changed for each of the images + for (let i = 0; i < numImages; i++) { + expectedSearchEvents.push("CurrentState"); + } + let eventPromises = expectedSearchEvents.map(e => waitForTestMsg(e)); + + // Wait for addEngine(). + let addDeferred = Promise.defer(); + let url = getRootDirectory(gTestPath) + basename; + Services.search.addEngine(url, null, "", false, { + onSuccess: function (engine) { + info("Search engine added: " + basename); + addDeferred.resolve(engine); + }, + onError: function (errCode) { + ok(false, "addEngine failed with error code " + errCode); + addDeferred.reject(); + }, + }); + + return Promise.all([addDeferred.promise].concat(eventPromises)); +} + +function addTab() { + let deferred = Promise.defer(); + let tab = gBrowser.addTab(); + gBrowser.selectedTab = tab; + tab.linkedBrowser.addEventListener("load", function load() { + tab.linkedBrowser.removeEventListener("load", load, true); + let url = getRootDirectory(gTestPath) + TEST_CONTENT_SCRIPT_BASENAME; + gMsgMan = tab.linkedBrowser.messageManager; + gMsgMan.sendAsyncMessage(CONTENT_SEARCH_MSG, { + type: "AddToWhitelist", + data: ["about:blank"], + }); + waitForMsg(CONTENT_SEARCH_MSG, "AddToWhitelistAck").then(() => { + gMsgMan.loadFrameScript(url, false); + deferred.resolve(); + }); + }, true); + registerCleanupFunction(() => gBrowser.removeTab(tab)); + return deferred.promise; +} + +var currentStateObj = Task.async(function* () { + let state = { + engines: [], + currentEngine: yield currentEngineObj(), + }; + for (let engine of Services.search.getVisibleEngines()) { + let uri = engine.getIconURLBySize(16, 16); + state.engines.push({ + name: engine.name, + iconBuffer: yield arrayBufferFromDataURI(uri), + hidden: false, + }); + } + return state; +}); + +var currentEngineObj = Task.async(function* () { + let engine = Services.search.currentEngine; + let uriFavicon = engine.getIconURLBySize(16, 16); + let bundle = Services.strings.createBundle("chrome://global/locale/autocomplete.properties"); + return { + name: engine.name, + placeholder: bundle.formatStringFromName("searchWithEngine", [engine.name], 1), + iconBuffer: yield arrayBufferFromDataURI(uriFavicon), + }; +}); + +function arrayBufferFromDataURI(uri) { + if (!uri) { + return Promise.resolve(null); + } + let deferred = Promise.defer(); + let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]. + createInstance(Ci.nsIXMLHttpRequest); + xhr.open("GET", uri, true); + xhr.responseType = "arraybuffer"; + xhr.onerror = () => { + deferred.resolve(null); + }; + xhr.onload = () => { + deferred.resolve(xhr.response); + }; + try { + xhr.send(); + } + catch (err) { + return Promise.resolve(null); + } + return deferred.promise; +} diff --git a/browser/modules/test/browser_NetworkPrioritizer.js b/browser/modules/test/browser_NetworkPrioritizer.js new file mode 100644 index 000000000..91557b0fd --- /dev/null +++ b/browser/modules/test/browser_NetworkPrioritizer.js @@ -0,0 +1,165 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** Tests for NetworkPrioritizer.jsm (Bug 514490) **/ + +const LOWEST = Ci.nsISupportsPriority.PRIORITY_LOWEST; +const LOW = Ci.nsISupportsPriority.PRIORITY_LOW; +const NORMAL = Ci.nsISupportsPriority.PRIORITY_NORMAL; +const HIGH = Ci.nsISupportsPriority.PRIORITY_HIGH; +const HIGHEST = Ci.nsISupportsPriority.PRIORITY_HIGHEST; + +const DELTA = NORMAL - LOW; // lower value means higher priority + +// Test helper functions. +// getPriority and setPriority can take a tab or a Browser +function* getPriority(aBrowser) { + if (aBrowser.localName == "tab") + aBrowser = aBrowser.linkedBrowser; + + return yield ContentTask.spawn(aBrowser, null, function* () { + return docShell.QueryInterface(Components.interfaces.nsIWebNavigation) + .QueryInterface(Components.interfaces.nsIDocumentLoader) + .loadGroup + .QueryInterface(Components.interfaces.nsISupportsPriority) + .priority; + }); +} + +function* setPriority(aBrowser, aPriority) { + if (aBrowser.localName == "tab") + aBrowser = aBrowser.linkedBrowser; + + yield ContentTask.spawn(aBrowser, aPriority, function* (aPriority) { + docShell.QueryInterface(Components.interfaces.nsIWebNavigation) + .QueryInterface(Components.interfaces.nsIDocumentLoader) + .loadGroup + .QueryInterface(Ci.nsISupportsPriority) + .priority = aPriority; + }); +} + +function* isWindowState(aWindow, aTabPriorities) { + let browsers = aWindow.gBrowser.browsers; + // Make sure we have the right number of tabs & priorities + is(browsers.length, aTabPriorities.length, + "Window has expected number of tabs"); + // aState should be in format [ priority, priority, priority ] + for (let i = 0; i < browsers.length; i++) { + is(yield getPriority(browsers[i]), aTabPriorities[i], + "Tab " + i + " had expected priority"); + } +} + +function promiseWaitForFocus(aWindow) { + return new Promise((resolve) => { + waitForFocus(resolve, aWindow); + }); +} + +add_task(function*() { + // This is the real test. It creates multiple tabs & windows, changes focus, + // closes windows/tabs to make sure we behave correctly. + // This test assumes that no priorities have been adjusted and the loadgroup + // priority starts at 0. + + // Call window "window_A" to make the test easier to follow + let window_A = window; + + // Test 1 window, 1 tab case. + yield isWindowState(window_A, [HIGH]); + + // Exising tab is tab_A1 + let tab_A2 = window_A.gBrowser.addTab("http://example.com"); + let tab_A3 = window_A.gBrowser.addTab("about:config"); + yield BrowserTestUtils.browserLoaded(tab_A3.linkedBrowser); + + // tab_A2 isn't focused yet + yield isWindowState(window_A, [HIGH, NORMAL, NORMAL]); + + // focus tab_A2 & make sure priority got updated + window_A.gBrowser.selectedTab = tab_A2; + yield isWindowState(window_A, [NORMAL, HIGH, NORMAL]); + + window_A.gBrowser.removeTab(tab_A2); + // Next tab is auto selected synchronously as part of removeTab, and we + // expect the priority to be updated immediately. + yield isWindowState(window_A, [NORMAL, HIGH]); + + // Open another window then play with focus + let window_B = yield BrowserTestUtils.openNewBrowserWindow(); + + yield promiseWaitForFocus(window_B); + yield isWindowState(window_A, [LOW, NORMAL]); + yield isWindowState(window_B, [HIGH]); + + yield promiseWaitForFocus(window_A); + yield isWindowState(window_A, [NORMAL, HIGH]); + yield isWindowState(window_B, [NORMAL]); + + yield promiseWaitForFocus(window_B); + yield isWindowState(window_A, [LOW, NORMAL]); + yield isWindowState(window_B, [HIGH]); + + // Cleanup + window_A.gBrowser.removeTab(tab_A3); + yield BrowserTestUtils.closeWindow(window_B); +}); + +add_task(function*() { + // This is more a test of nsLoadGroup and how it handles priorities. But since + // we depend on its behavior, it's good to test it. This is testing that there + // are no errors if we adjust beyond nsISupportsPriority's bounds. + + yield promiseWaitForFocus(); + + let tab1 = gBrowser.tabs[0]; + let oldPriority = yield getPriority(tab1); + + // Set the priority of tab1 to the lowest possible. Selecting the other tab + // will try to lower it + yield setPriority(tab1, LOWEST); + + let tab2 = gBrowser.addTab("http://example.com"); + yield BrowserTestUtils.browserLoaded(tab2.linkedBrowser); + gBrowser.selectedTab = tab2; + is(yield getPriority(tab1), LOWEST - DELTA, "Can adjust priority beyond 'lowest'"); + + // Now set priority to "highest" and make sure that no errors occur. + yield setPriority(tab1, HIGHEST); + gBrowser.selectedTab = tab1; + + is(yield getPriority(tab1), HIGHEST + DELTA, "Can adjust priority beyond 'highest'"); + + // Cleanup + gBrowser.removeTab(tab2); + yield setPriority(tab1, oldPriority); +}); + +add_task(function*() { + // This tests that the priority doesn't get lost when switching the browser's remoteness + + if (!gMultiProcessBrowser) { + return; + } + + let browser = gBrowser.selectedBrowser; + + browser.loadURI("http://example.com"); + yield BrowserTestUtils.browserLoaded(browser); + ok(browser.isRemoteBrowser, "web page should be loaded in remote browser"); + is(yield getPriority(browser), HIGH, "priority of selected tab should be 'high'"); + + browser.loadURI("about:rights"); + yield BrowserTestUtils.browserLoaded(browser); + ok(!browser.isRemoteBrowser, "about:rights should switch browser to non-remote"); + is(yield getPriority(browser), HIGH, + "priority of selected tab should be 'high' when going from remote to non-remote"); + + browser.loadURI("http://example.com"); + yield BrowserTestUtils.browserLoaded(browser); + ok(browser.isRemoteBrowser, "going from about:rights to web page should switch browser to remote"); + is(yield getPriority(browser), HIGH, + "priority of selected tab should be 'high' when going from non-remote to remote"); +}); diff --git a/browser/modules/test/browser_PermissionUI.js b/browser/modules/test/browser_PermissionUI.js new file mode 100644 index 000000000..006bc5e66 --- /dev/null +++ b/browser/modules/test/browser_PermissionUI.js @@ -0,0 +1,445 @@ +/** + * These tests test the ability for the PermissionUI module to open + * permission prompts to the user. It also tests to ensure that + * add-ons can introduce their own permission prompts. + */ + +"use strict"; + +Cu.import("resource://gre/modules/Integration.jsm", this); +Cu.import("resource:///modules/PermissionUI.jsm", this); + +/** + * Given a <xul:browser> at some non-internal web page, + * return something that resembles an nsIContentPermissionRequest, + * using the browsers currently loaded document to get a principal. + * + * @param browser (<xul:browser>) + * The browser that we'll create a nsIContentPermissionRequest + * for. + * @returns A nsIContentPermissionRequest-ish object. + */ +function makeMockPermissionRequest(browser) { + let result = { + types: null, + principal: browser.contentPrincipal, + requester: null, + _cancelled: false, + cancel() { + this._cancelled = true; + }, + _allowed: false, + allow() { + this._allowed = true; + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPermissionRequest]), + }; + + // In the e10s-case, nsIContentPermissionRequest will have + // element defined. window is defined otherwise. + if (browser.isRemoteBrowser) { + result.element = browser; + } else { + result.window = browser.contentWindow; + } + + return result; +} + +/** + * For an opened PopupNotification, clicks on the main action, + * and waits for the panel to fully close. + * + * @return {Promise} + * Resolves once the panel has fired the "popuphidden" + * event. + */ +function clickMainAction() { + let removePromise = + BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popuphidden"); + let popupNotification = getPopupNotificationNode(); + popupNotification.button.click(); + return removePromise; +} + +/** + * For an opened PopupNotification, clicks on a secondary action, + * and waits for the panel to fully close. + * + * @param {int} index + * The 0-indexed index of the secondary menuitem to choose. + * @return {Promise} + * Resolves once the panel has fired the "popuphidden" + * event. + */ +function clickSecondaryAction(index) { + let removePromise = + BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popuphidden"); + let popupNotification = getPopupNotificationNode(); + let menuitems = popupNotification.children; + menuitems[index].click(); + return removePromise; +} + +/** + * Makes sure that 1 (and only 1) <xul:popupnotification> is being displayed + * by PopupNotification, and then returns that <xul:popupnotification>. + * + * @return {<xul:popupnotification>} + */ +function getPopupNotificationNode() { + // PopupNotification is a bit overloaded here, so to be + // clear, popupNotifications is a list of <xul:popupnotification> + // nodes. + let popupNotifications = PopupNotifications.panel.childNodes; + Assert.equal(popupNotifications.length, 1, + "Should be showing a <xul:popupnotification>"); + return popupNotifications[0]; +} + +/** + * Tests the PermissionPromptForRequest prototype to ensure that a prompt + * can be displayed. Does not test permission handling. + */ +add_task(function* test_permission_prompt_for_request() { + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: "http://example.com/", + }, function*(browser) { + const kTestNotificationID = "test-notification"; + const kTestMessage = "Test message"; + let mainAction = { + label: "Main", + accessKey: "M", + }; + let secondaryAction = { + label: "Secondary", + accessKey: "S", + }; + + let mockRequest = makeMockPermissionRequest(browser); + let TestPrompt = { + __proto__: PermissionUI.PermissionPromptForRequestPrototype, + request: mockRequest, + notificationID: kTestNotificationID, + message: kTestMessage, + promptActions: [mainAction, secondaryAction], + }; + + let shownPromise = + BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown"); + TestPrompt.prompt(); + yield shownPromise; + let notification = + PopupNotifications.getNotification(kTestNotificationID, browser); + Assert.ok(notification, "Should have gotten the notification"); + + Assert.equal(notification.message, kTestMessage, + "Should be showing the right message"); + Assert.equal(notification.mainAction.label, mainAction.label, + "The main action should have the right label"); + Assert.equal(notification.mainAction.accessKey, mainAction.accessKey, + "The main action should have the right access key"); + Assert.equal(notification.secondaryActions.length, 1, + "There should only be 1 secondary action"); + Assert.equal(notification.secondaryActions[0].label, secondaryAction.label, + "The secondary action should have the right label"); + Assert.equal(notification.secondaryActions[0].accessKey, + secondaryAction.accessKey, + "The secondary action should have the right access key"); + Assert.ok(notification.options.displayURI.equals(mockRequest.principal.URI), + "Should be showing the URI of the requesting page"); + + let removePromise = + BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popuphidden"); + notification.remove(); + yield removePromise; + }); +}); + +/** + * Tests that if the PermissionPrompt sets displayURI to false in popupOptions, + * then there is no URI shown on the popupnotification. + */ +add_task(function* test_permission_prompt_for_popupOptions() { + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: "http://example.com/", + }, function*(browser) { + const kTestNotificationID = "test-notification"; + const kTestMessage = "Test message"; + let mainAction = { + label: "Main", + accessKey: "M", + }; + let secondaryAction = { + label: "Secondary", + accessKey: "S", + }; + + let mockRequest = makeMockPermissionRequest(browser); + let TestPrompt = { + __proto__: PermissionUI.PermissionPromptForRequestPrototype, + request: mockRequest, + notificationID: kTestNotificationID, + message: kTestMessage, + promptActions: [mainAction, secondaryAction], + popupOptions: { + displayURI: false, + }, + }; + + let shownPromise = + BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown"); + TestPrompt.prompt(); + yield shownPromise; + let notification = + PopupNotifications.getNotification(kTestNotificationID, browser); + + Assert.ok(!notification.options.displayURI, + "Should not show the URI of the requesting page"); + + let removePromise = + BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popuphidden"); + notification.remove(); + yield removePromise; + }); +}); + +/** + * Tests that if the PermissionPrompt has the permissionKey + * set that permissions can be set properly by the user. Also + * ensures that callbacks for promptActions are properly fired. + */ +add_task(function* test_with_permission_key() { + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: "http://example.com", + }, function*(browser) { + const kTestNotificationID = "test-notification"; + const kTestMessage = "Test message"; + const kTestPermissionKey = "test-permission-key"; + + let allowed = false; + let mainAction = { + label: "Allow", + accessKey: "M", + action: Ci.nsIPermissionManager.ALLOW_ACTION, + expiryType: Ci.nsIPermissionManager.EXPIRE_SESSION, + callback: function() { + allowed = true; + } + }; + + let denied = false; + let secondaryAction = { + label: "Deny", + accessKey: "D", + action: Ci.nsIPermissionManager.DENY_ACTION, + expiryType: Ci.nsIPermissionManager.EXPIRE_SESSION, + callback: function() { + denied = true; + } + }; + + let mockRequest = makeMockPermissionRequest(browser); + let principal = mockRequest.principal; + registerCleanupFunction(function() { + Services.perms.removeFromPrincipal(principal, kTestPermissionKey); + }); + + let TestPrompt = { + __proto__: PermissionUI.PermissionPromptForRequestPrototype, + request: mockRequest, + notificationID: kTestNotificationID, + permissionKey: kTestPermissionKey, + message: kTestMessage, + promptActions: [mainAction, secondaryAction], + }; + + let shownPromise = + BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown"); + TestPrompt.prompt(); + yield shownPromise; + let notification = + PopupNotifications.getNotification(kTestNotificationID, browser); + Assert.ok(notification, "Should have gotten the notification"); + + let curPerm = + Services.perms.testPermissionFromPrincipal(principal, + kTestPermissionKey); + Assert.equal(curPerm, Ci.nsIPermissionManager.UNKNOWN_ACTION, + "Should be no permission set to begin with."); + + // First test denying the permission request. + Assert.equal(notification.secondaryActions.length, 1, + "There should only be 1 secondary action"); + yield clickSecondaryAction(0); + curPerm = Services.perms.testPermissionFromPrincipal(principal, + kTestPermissionKey); + Assert.equal(curPerm, Ci.nsIPermissionManager.DENY_ACTION, + "Should have denied the action"); + Assert.ok(denied, "The secondaryAction callback should have fired"); + Assert.ok(!allowed, "The mainAction callback should not have fired"); + Assert.ok(mockRequest._cancelled, + "The request should have been cancelled"); + Assert.ok(!mockRequest._allowed, + "The request should not have been allowed"); + + // Clear the permission and pretend we never denied + Services.perms.removeFromPrincipal(principal, kTestPermissionKey); + denied = false; + mockRequest._cancelled = false; + + // Bring the PopupNotification back up now... + shownPromise = + BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown"); + TestPrompt.prompt(); + yield shownPromise; + + // Next test allowing the permission request. + yield clickMainAction(); + curPerm = Services.perms.testPermissionFromPrincipal(principal, + kTestPermissionKey); + Assert.equal(curPerm, Ci.nsIPermissionManager.ALLOW_ACTION, + "Should have allowed the action"); + Assert.ok(!denied, "The secondaryAction callback should not have fired"); + Assert.ok(allowed, "The mainAction callback should have fired"); + Assert.ok(!mockRequest._cancelled, + "The request should not have been cancelled"); + Assert.ok(mockRequest._allowed, + "The request should have been allowed"); + }); +}); + +/** + * Tests that the onBeforeShow method will be called before + * the popup appears. + */ +add_task(function* test_on_before_show() { + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: "http://example.com", + }, function*(browser) { + const kTestNotificationID = "test-notification"; + const kTestMessage = "Test message"; + + let mainAction = { + label: "Test action", + accessKey: "T", + }; + + let mockRequest = makeMockPermissionRequest(browser); + let beforeShown = false; + + let TestPrompt = { + __proto__: PermissionUI.PermissionPromptForRequestPrototype, + request: mockRequest, + notificationID: kTestNotificationID, + message: kTestMessage, + promptActions: [mainAction], + onBeforeShow() { + beforeShown = true; + } + }; + + let shownPromise = + BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown"); + TestPrompt.prompt(); + Assert.ok(beforeShown, "Should have called onBeforeShown"); + yield shownPromise; + let notification = + PopupNotifications.getNotification(kTestNotificationID, browser); + + let removePromise = + BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popuphidden"); + notification.remove(); + yield removePromise; + }); +}); + +/** + * Tests that we can open a PermissionPrompt without wrapping a + * nsIContentPermissionRequest. + */ +add_task(function* test_no_request() { + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: "http://example.com", + }, function*(browser) { + const kTestNotificationID = "test-notification"; + let allowed = false; + let mainAction = { + label: "Allow", + accessKey: "M", + callback: function() { + allowed = true; + } + }; + + let denied = false; + let secondaryAction = { + label: "Deny", + accessKey: "D", + callback: function() { + denied = true; + } + }; + + const kTestMessage = "Test message with no request"; + let principal = browser.contentPrincipal; + let beforeShown = false; + + let TestPrompt = { + __proto__: PermissionUI.PermissionPromptPrototype, + notificationID: kTestNotificationID, + principal, + browser, + message: kTestMessage, + promptActions: [mainAction, secondaryAction], + onBeforeShow() { + beforeShown = true; + } + }; + + let shownPromise = + BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown"); + TestPrompt.prompt(); + Assert.ok(beforeShown, "Should have called onBeforeShown"); + yield shownPromise; + let notification = + PopupNotifications.getNotification(kTestNotificationID, browser); + + Assert.equal(notification.message, kTestMessage, + "Should be showing the right message"); + Assert.equal(notification.mainAction.label, mainAction.label, + "The main action should have the right label"); + Assert.equal(notification.mainAction.accessKey, mainAction.accessKey, + "The main action should have the right access key"); + Assert.equal(notification.secondaryActions.length, 1, + "There should only be 1 secondary action"); + Assert.equal(notification.secondaryActions[0].label, secondaryAction.label, + "The secondary action should have the right label"); + Assert.equal(notification.secondaryActions[0].accessKey, + secondaryAction.accessKey, + "The secondary action should have the right access key"); + Assert.ok(notification.options.displayURI.equals(principal.URI), + "Should be showing the URI of the requesting page"); + + // First test denying the permission request. + Assert.equal(notification.secondaryActions.length, 1, + "There should only be 1 secondary action"); + yield clickSecondaryAction(0); + Assert.ok(denied, "The secondaryAction callback should have fired"); + Assert.ok(!allowed, "The mainAction callback should not have fired"); + + shownPromise = + BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown"); + TestPrompt.prompt(); + yield shownPromise; + + // Next test allowing the permission request. + yield clickMainAction(); + Assert.ok(allowed, "The mainAction callback should have fired"); + }); +}); diff --git a/browser/modules/test/browser_ProcessHangNotifications.js b/browser/modules/test/browser_ProcessHangNotifications.js new file mode 100644 index 000000000..597be611a --- /dev/null +++ b/browser/modules/test/browser_ProcessHangNotifications.js @@ -0,0 +1,189 @@ + +Cu.import("resource://gre/modules/UpdateUtils.jsm"); + +function getNotificationBox(aWindow) { + return aWindow.document.getElementById("high-priority-global-notificationbox"); +} + +function promiseNotificationShown(aWindow, aName) { + return new Promise((resolve) => { + let notification = getNotificationBox(aWindow); + notification.addEventListener("AlertActive", function active() { + notification.removeEventListener("AlertActive", active, true); + is(notification.allNotifications.length, 1, "Notification Displayed."); + resolve(notification); + }); + }); +} + +function promiseReportCallMade(aValue) { + return new Promise((resolve) => { + let old = gTestHangReport.testCallback; + gTestHangReport.testCallback = function (val) { + gTestHangReport.testCallback = old; + is(aValue, val, "was the correct method call made on the hang report object?"); + resolve(); + }; + }); +} + +function pushPrefs(...aPrefs) { + return new Promise((resolve) => { + SpecialPowers.pushPrefEnv({"set": aPrefs}, resolve); + resolve(); + }); +} + +function popPrefs() { + return new Promise((resolve) => { + SpecialPowers.popPrefEnv(resolve); + resolve(); + }); +} + +let gTestHangReport = { + SLOW_SCRIPT: 1, + PLUGIN_HANG: 2, + + TEST_CALLBACK_CANCELED: 1, + TEST_CALLBACK_TERMSCRIPT: 2, + TEST_CALLBACK_TERMPLUGIN: 3, + + _hangType: 1, + _tcb: function (aCallbackType) {}, + + get hangType() { + return this._hangType; + }, + + set hangType(aValue) { + this._hangType = aValue; + }, + + set testCallback(aValue) { + this._tcb = aValue; + }, + + QueryInterface: function (aIID) { + if (aIID.equals(Components.interfaces.nsIHangReport) || + aIID.equals(Components.interfaces.nsISupports)) + return this; + throw Components.results.NS_NOINTERFACE; + }, + + userCanceled: function () { + this._tcb(this.TEST_CALLBACK_CANCELED); + }, + + terminateScript: function () { + this._tcb(this.TEST_CALLBACK_TERMSCRIPT); + }, + + terminatePlugin: function () { + this._tcb(this.TEST_CALLBACK_TERMPLUGIN); + }, + + isReportForBrowser: function(aFrameLoader) { + return true; + } +}; + +// on dev edition we add a button for js debugging of hung scripts. +let buttonCount = (UpdateUtils.UpdateChannel == "aurora" ? 3 : 2); + +/** + * Test if hang reports receive a terminate script callback when the user selects + * stop in response to a script hang. + */ + +add_task(function* terminateScriptTest() { + let promise = promiseNotificationShown(window, "process-hang"); + Services.obs.notifyObservers(gTestHangReport, "process-hang-report", null); + let notification = yield promise; + + let buttons = notification.currentNotification.getElementsByTagName("button"); + // Fails on aurora on-push builds, bug 1232204 + // is(buttons.length, buttonCount, "proper number of buttons"); + + // Click the "Stop It" button, we should get a terminate script callback + gTestHangReport.hangType = gTestHangReport.SLOW_SCRIPT; + promise = promiseReportCallMade(gTestHangReport.TEST_CALLBACK_TERMSCRIPT); + buttons[0].click(); + yield promise; +}); + +/** + * Test if hang reports receive user canceled callbacks after a user selects wait + * and the browser frees up from a script hang on its own. + */ + +add_task(function* waitForScriptTest() { + let promise = promiseNotificationShown(window, "process-hang"); + Services.obs.notifyObservers(gTestHangReport, "process-hang-report", null); + let notification = yield promise; + + let buttons = notification.currentNotification.getElementsByTagName("button"); + // Fails on aurora on-push builds, bug 1232204 + // is(buttons.length, buttonCount, "proper number of buttons"); + + yield pushPrefs(["browser.hangNotification.waitPeriod", 1000]); + + function nocbcheck() { + ok(false, "received a callback?"); + } + let oldcb = gTestHangReport.testCallback; + gTestHangReport.testCallback = nocbcheck; + // Click the "Wait" button this time, we shouldn't get a callback at all. + buttons[1].click(); + gTestHangReport.testCallback = oldcb; + + // send another hang pulse, we should not get a notification here + Services.obs.notifyObservers(gTestHangReport, "process-hang-report", null); + is(notification.currentNotification, null, "no notification should be visible"); + + gTestHangReport.testCallback = function() {}; + Services.obs.notifyObservers(gTestHangReport, "clear-hang-report", null); + gTestHangReport.testCallback = oldcb; + + yield popPrefs(); +}); + +/** + * Test if hang reports receive user canceled callbacks after the content + * process stops sending hang notifications. + */ + +add_task(function* hangGoesAwayTest() { + yield pushPrefs(["browser.hangNotification.expiration", 1000]); + + let promise = promiseNotificationShown(window, "process-hang"); + Services.obs.notifyObservers(gTestHangReport, "process-hang-report", null); + yield promise; + + promise = promiseReportCallMade(gTestHangReport.TEST_CALLBACK_CANCELED); + Services.obs.notifyObservers(gTestHangReport, "clear-hang-report", null); + yield promise; + + yield popPrefs(); +}); + +/** + * Tests if hang reports receive a terminate plugin callback when the user selects + * stop in response to a plugin hang. + */ + +add_task(function* terminatePluginTest() { + let promise = promiseNotificationShown(window, "process-hang"); + Services.obs.notifyObservers(gTestHangReport, "process-hang-report", null); + let notification = yield promise; + + let buttons = notification.currentNotification.getElementsByTagName("button"); + // Fails on aurora on-push builds, bug 1232204 + // is(buttons.length, buttonCount, "proper number of buttons"); + + // Click the "Stop It" button, we should get a terminate script callback + gTestHangReport.hangType = gTestHangReport.PLUGIN_HANG; + promise = promiseReportCallMade(gTestHangReport.TEST_CALLBACK_TERMPLUGIN); + buttons[0].click(); + yield promise; +}); diff --git a/browser/modules/test/browser_SelfSupportBackend.js b/browser/modules/test/browser_SelfSupportBackend.js new file mode 100644 index 000000000..9e2c1d181 --- /dev/null +++ b/browser/modules/test/browser_SelfSupportBackend.js @@ -0,0 +1,214 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Pass an empty scope object to the import to prevent "leaked window property" +// errors in tests. +var Preferences = Cu.import("resource://gre/modules/Preferences.jsm", {}).Preferences; +var PromiseUtils = Cu.import("resource://gre/modules/PromiseUtils.jsm", {}).PromiseUtils; +var SelfSupportBackend = + Cu.import("resource:///modules/SelfSupportBackend.jsm", {}).SelfSupportBackend; + +const PREF_SELFSUPPORT_ENABLED = "browser.selfsupport.enabled"; +const PREF_SELFSUPPORT_URL = "browser.selfsupport.url"; +const PREF_UITOUR_ENABLED = "browser.uitour.enabled"; + +const TEST_WAIT_RETRIES = 60; + +const TEST_PAGE_URL = getRootDirectory(gTestPath) + "uitour.html"; +const TEST_PAGE_URL_HTTPS = TEST_PAGE_URL.replace("chrome://mochitests/content/", "https://example.com/"); + +function sendSessionRestoredNotification() { + let selfSupportBackendImpl = + Cu.import("resource:///modules/SelfSupportBackend.jsm", {}).SelfSupportBackendInternal; + selfSupportBackendImpl.observe(null, "sessionstore-windows-restored", null); +} + +/** + * Find a browser, with an IFRAME as parent, who has aURL as the source attribute. + * + * @param aURL The URL to look for to identify the browser. + * + * @returns {Object} The browser element or null on failure. + */ +function findSelfSupportBrowser(aURL) { + let frames = Services.appShell.hiddenDOMWindow.document.querySelectorAll('iframe'); + for (let frame of frames) { + try { + let browser = frame.contentDocument.getElementById("win").querySelectorAll('browser')[0]; + let url = browser.getAttribute("src"); + if (url == aURL) { + return browser; + } + } catch (e) { + continue; + } + } + return null; +} + +/** + * Wait for self support page to load. + * + * @param aURL The URL to look for to identify the browser. + * + * @returns {Promise} Return a promise which is resolved when SelfSupport page is fully + * loaded. + */ +function promiseSelfSupportLoad(aURL) { + return new Promise((resolve, reject) => { + // Find the SelfSupport browser. + let browserPromise = waitForConditionPromise(() => !!findSelfSupportBrowser(aURL), + "SelfSupport browser not found.", + TEST_WAIT_RETRIES); + + // Once found, append a "load" listener to catch page loads. + browserPromise.then(() => { + let browser = findSelfSupportBrowser(aURL); + if (browser.contentDocument.readyState === "complete") { + resolve(browser); + } else { + let handler = () => { + browser.removeEventListener("load", handler, true); + resolve(browser); + }; + browser.addEventListener("load", handler, true); + } + }, reject); + }); +} + +/** + * Wait for self support to close. + * + * @param aURL The URL to look for to identify the browser. + * + * @returns {Promise} Return a promise which is resolved when SelfSupport browser cannot + * be found anymore. + */ +function promiseSelfSupportClose(aURL) { + return waitForConditionPromise(() => !findSelfSupportBrowser(aURL), + "SelfSupport browser is still open.", TEST_WAIT_RETRIES); +} + +/** + * Prepare the test environment. + */ +add_task(function* setupEnvironment() { + // We always run the SelfSupportBackend in tests to check for weird behaviours. + // Disable it to test its start-up. + SelfSupportBackend.uninit(); + + // Testing prefs are set via |user_pref|, so we need to get their value in order + // to restore them. + let selfSupportEnabled = Preferences.get(PREF_SELFSUPPORT_ENABLED, true); + let uitourEnabled = Preferences.get(PREF_UITOUR_ENABLED, false); + let selfSupportURL = Preferences.get(PREF_SELFSUPPORT_URL, ""); + + // Enable the SelfSupport backend and set the page URL. We also make sure UITour + // is enabled. + Preferences.set(PREF_SELFSUPPORT_ENABLED, true); + Preferences.set(PREF_UITOUR_ENABLED, true); + Preferences.set(PREF_SELFSUPPORT_URL, TEST_PAGE_URL_HTTPS); + + // Whitelist the HTTPS page to use UITour. + let pageURI = Services.io.newURI(TEST_PAGE_URL_HTTPS, null, null); + Services.perms.add(pageURI, "uitour", Services.perms.ALLOW_ACTION); + + registerCleanupFunction(() => { + Services.perms.remove(pageURI, "uitour"); + Preferences.set(PREF_SELFSUPPORT_ENABLED, selfSupportEnabled); + Preferences.set(PREF_UITOUR_ENABLED, uitourEnabled); + Preferences.set(PREF_SELFSUPPORT_URL, selfSupportURL); + }); +}); + +/** + * Test that the self support page can use the UITour API and close itself. + */ +add_task(function* test_selfSupport() { + // Initialise the SelfSupport backend and trigger the load. + SelfSupportBackend.init(); + + // SelfSupportBackend waits for "sessionstore-windows-restored" to start loading. Send it. + info("Sending sessionstore-windows-restored"); + sendSessionRestoredNotification(); + + // Wait for the SelfSupport page to load. + info("Waiting for the SelfSupport local page to load."); + let selfSupportBrowser = yield promiseSelfSupportLoad(TEST_PAGE_URL_HTTPS); + Assert.ok(!!selfSupportBrowser, "SelfSupport browser must exist."); + + // Get a reference to the UITour API. + info("Testing access to the UITour API."); + let contentWindow = + Cu.waiveXrays(selfSupportBrowser.contentDocument.defaultView); + let uitourAPI = contentWindow.Mozilla.UITour; + + // Test the UITour API with a ping. + let pingPromise = new Promise((resolve) => { + uitourAPI.ping(resolve); + }); + yield pingPromise; + info("Ping succeeded"); + + let observePromise = ContentTask.spawn(selfSupportBrowser, null, function* checkObserve() { + yield new Promise(resolve => { + let win = Cu.waiveXrays(content); + win.Mozilla.UITour.observe((event, data) => { + if (event != "Heartbeat:Engaged") { + return; + } + Assert.equal(data.flowId, "myFlowID", "Check flowId"); + Assert.ok(!!data.timestamp, "Check timestamp"); + resolve(data); + }, () => {}); + }); + }); + + info("Notifying Heartbeat:Engaged"); + UITour.notify("Heartbeat:Engaged", { + flowId: "myFlowID", + timestamp: Date.now(), + }); + yield observePromise; + info("Observed in the hidden frame"); + + // Close SelfSupport from content. + contentWindow.close(); + + // Wait until SelfSupport closes. + info("Waiting for the SelfSupport to close."); + yield promiseSelfSupportClose(TEST_PAGE_URL_HTTPS); + + // Find the SelfSupport browser, again. We don't expect to find it. + selfSupportBrowser = findSelfSupportBrowser(TEST_PAGE_URL_HTTPS); + Assert.ok(!selfSupportBrowser, "SelfSupport browser must not exist."); + + // We shouldn't need this, but let's keep it to make sure closing SelfSupport twice + // doesn't create any problem. + SelfSupportBackend.uninit(); +}); + +/** + * Test that SelfSupportBackend only allows HTTPS. + */ +add_task(function* test_selfSupport_noHTTPS() { + Preferences.set(PREF_SELFSUPPORT_URL, TEST_PAGE_URL); + + SelfSupportBackend.init(); + + // SelfSupportBackend waits for "sessionstore-windows-restored" to start loading. Send it. + info("Sending sessionstore-windows-restored"); + sendSessionRestoredNotification(); + + // Find the SelfSupport browser. We don't expect to find it since we are not using https. + let selfSupportBrowser = findSelfSupportBrowser(TEST_PAGE_URL); + Assert.ok(!selfSupportBrowser, "SelfSupport browser must not exist."); + + // We shouldn't need this, but let's keep it to make sure closing SelfSupport twice + // doesn't create any problem. + SelfSupportBackend.uninit(); +}) diff --git a/browser/modules/test/browser_UnsubmittedCrashHandler.js b/browser/modules/test/browser_UnsubmittedCrashHandler.js new file mode 100644 index 000000000..2d78c746b --- /dev/null +++ b/browser/modules/test/browser_UnsubmittedCrashHandler.js @@ -0,0 +1,680 @@ +"use strict"; + +/** + * This suite tests the "unsubmitted crash report" notification + * that is seen when we detect pending crash reports on startup. + */ + +const { UnsubmittedCrashHandler } = + Cu.import("resource:///modules/ContentCrashHandlers.jsm", this); +const { FileUtils } = + Cu.import("resource://gre/modules/FileUtils.jsm", this); +const { makeFakeAppDir } = + Cu.import("resource://testing-common/AppData.jsm", this); +const { OS } = + Cu.import("resource://gre/modules/osfile.jsm", this); + +const DAY = 24 * 60 * 60 * 1000; // milliseconds +const SERVER_URL = "http://example.com/browser/toolkit/crashreporter/test/browser/crashreport.sjs"; + +/** + * Returns the directly where the browsing is storing the + * pending crash reports. + * + * @returns nsIFile + */ +function getPendingCrashReportDir() { + // The fake UAppData directory that makeFakeAppDir provides + // is just UAppData under the profile directory. + return FileUtils.getDir("ProfD", [ + "UAppData", + "Crash Reports", + "pending", + ], false); +} + +/** + * Synchronously deletes all entries inside the pending + * crash report directory. + */ +function clearPendingCrashReports() { + let dir = getPendingCrashReportDir(); + let entries = dir.directoryEntries; + + while (entries.hasMoreElements()) { + let entry = entries.getNext().QueryInterface(Ci.nsIFile); + if (entry.isFile()) { + entry.remove(false); + } + } +} + +/** + * Randomly generates howMany crash report .dmp and .extra files + * to put into the pending crash report directory. We're not + * actually creating real crash reports here, just stubbing + * out enough of the files to satisfy our notification and + * submission code. + * + * @param howMany (int) + * How many pending crash reports to put in the pending + * crash report directory. + * @param accessDate (Date, optional) + * What date to set as the last accessed time on the created + * crash reports. This defaults to the current date and time. + * @returns Promise + */ +function* createPendingCrashReports(howMany, accessDate) { + let dir = getPendingCrashReportDir(); + if (!accessDate) { + accessDate = new Date(); + } + + /** + * Helper function for creating a file in the pending crash report + * directory. + * + * @param fileName (string) + * The filename for the crash report, not including the + * extension. This is usually a UUID. + * @param extension (string) + * The file extension for the created file. + * @param accessDate (Date) + * The date to set lastAccessed to. + * @param contents (string, optional) + * Set this to whatever the file needs to contain, if anything. + * @returns Promise + */ + let createFile = (fileName, extension, accessDate, contents) => { + let file = dir.clone(); + file.append(fileName + "." + extension); + file.create(Ci.nsILocalFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE); + let promises = [OS.File.setDates(file.path, accessDate)]; + + if (contents) { + let encoder = new TextEncoder(); + let array = encoder.encode(contents); + promises.push(OS.File.writeAtomic(file.path, array, { + tmpPath: file.path + ".tmp", + })); + } + return Promise.all(promises); + } + + let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"] + .getService(Ci.nsIUUIDGenerator); + // CrashSubmit expects there to be a ServerURL key-value + // pair in the .extra file, so we'll satisfy it. + let extraFileContents = "ServerURL=" + SERVER_URL; + + return Task.spawn(function*() { + let uuids = []; + for (let i = 0; i < howMany; ++i) { + let uuid = uuidGenerator.generateUUID().toString(); + // Strip the {}... + uuid = uuid.substring(1, uuid.length - 1); + yield createFile(uuid, "dmp", accessDate); + yield createFile(uuid, "extra", accessDate, extraFileContents); + uuids.push(uuid); + } + return uuids; + }); +} + +/** + * Returns a Promise that resolves once CrashSubmit starts sending + * success notifications for crash submission matching the reportIDs + * being passed in. + * + * @param reportIDs (Array<string>) + * The IDs for the reports that we expect CrashSubmit to have sent. + * @returns Promise + */ +function waitForSubmittedReports(reportIDs) { + let promises = []; + for (let reportID of reportIDs) { + let promise = TestUtils.topicObserved("crash-report-status", (subject, data) => { + if (data == "success") { + let propBag = subject.QueryInterface(Ci.nsIPropertyBag2); + let dumpID = propBag.getPropertyAsAString("minidumpID"); + if (dumpID == reportID) { + return true; + } + } + return false; + }); + promises.push(promise); + } + return Promise.all(promises); +} + +/** + * Returns a Promise that resolves once a .dmp.ignore file is created for + * the crashes in the pending directory matching the reportIDs being + * passed in. + * + * @param reportIDs (Array<string>) + * The IDs for the reports that we expect CrashSubmit to have been + * marked for ignoring. + * @returns Promise + */ +function waitForIgnoredReports(reportIDs) { + let dir = getPendingCrashReportDir(); + let promises = []; + for (let reportID of reportIDs) { + let file = dir.clone(); + file.append(reportID + ".dmp.ignore"); + promises.push(OS.File.exists(file.path)); + } + return Promise.all(promises); +} + +let gNotificationBox; + +add_task(function* setup() { + // Pending crash reports are stored in the UAppData folder, + // which exists outside of the profile folder. In order to + // not overwrite / clear pending crash reports for the poor + // soul who runs this test, we use AppData.jsm to point to + // a special made-up directory inside the profile + // directory. + yield makeFakeAppDir(); + // We'll assume that the notifications will be shown in the current + // browser window's global notification box. + gNotificationBox = document.getElementById("global-notificationbox"); + + // If we happen to already be seeing the unsent crash report + // notification, it's because the developer running this test + // happened to have some unsent reports in their UAppDir. + // We'll remove the notification without touching those reports. + let notification = + gNotificationBox.getNotificationWithValue("pending-crash-reports"); + if (notification) { + notification.close(); + } + + let env = Cc["@mozilla.org/process/environment;1"] + .getService(Components.interfaces.nsIEnvironment); + let oldServerURL = env.get("MOZ_CRASHREPORTER_URL"); + env.set("MOZ_CRASHREPORTER_URL", SERVER_URL); + + // nsBrowserGlue starts up UnsubmittedCrashHandler automatically + // so at this point, it is initialized. It's possible that it + // was initialized, but is preffed off, so it's inert, so we + // shut it down, make sure it's preffed on, and then restart it. + // Note that making the component initialize even when it's + // disabled is an intentional choice, as this allows for easier + // simulation of startup and shutdown. + UnsubmittedCrashHandler.uninit(); + yield SpecialPowers.pushPrefEnv({ + set: [ + ["browser.crashReports.unsubmittedCheck.enabled", true], + ], + }); + UnsubmittedCrashHandler.init(); + + registerCleanupFunction(function() { + gNotificationBox = null; + clearPendingCrashReports(); + env.set("MOZ_CRASHREPORTER_URL", oldServerURL); + }); +}); + +/** + * Tests that if there are no pending crash reports, then the + * notification will not show up. + */ +add_task(function* test_no_pending_no_notification() { + // Make absolutely sure there are no pending crash reports first... + clearPendingCrashReports(); + let notification = + yield UnsubmittedCrashHandler.checkForUnsubmittedCrashReports(); + Assert.equal(notification, null, + "There should not be a notification if there are no " + + "pending crash reports"); +}); + +/** + * Tests that there is a notification if there is one pending + * crash report. + */ +add_task(function* test_one_pending() { + yield createPendingCrashReports(1); + let notification = + yield UnsubmittedCrashHandler.checkForUnsubmittedCrashReports(); + Assert.ok(notification, "There should be a notification"); + + gNotificationBox.removeNotification(notification, true); + clearPendingCrashReports(); +}); + +/** + * Tests that there is a notification if there is more than one + * pending crash report. + */ +add_task(function* test_several_pending() { + yield createPendingCrashReports(3); + let notification = + yield UnsubmittedCrashHandler.checkForUnsubmittedCrashReports(); + Assert.ok(notification, "There should be a notification"); + + gNotificationBox.removeNotification(notification, true); + clearPendingCrashReports(); +}); + +/** + * Tests that there is no notification if the only pending crash + * reports are over 28 days old. Also checks that if we put a newer + * crash with that older set, that we can still get a notification. + */ +add_task(function* test_several_pending() { + // Let's create some crash reports from 30 days ago. + let oldDate = new Date(Date.now() - (30 * DAY)); + yield createPendingCrashReports(3, oldDate); + let notification = + yield UnsubmittedCrashHandler.checkForUnsubmittedCrashReports(); + Assert.equal(notification, null, + "There should not be a notification if there are only " + + "old pending crash reports"); + // Now let's create a new one and check again + yield createPendingCrashReports(1); + notification = + yield UnsubmittedCrashHandler.checkForUnsubmittedCrashReports(); + Assert.ok(notification, "There should be a notification"); + + gNotificationBox.removeNotification(notification, true); + clearPendingCrashReports(); +}); + +/** + * Tests that the notification can submit a report. + */ +add_task(function* test_can_submit() { + let reportIDs = yield createPendingCrashReports(1); + let notification = + yield UnsubmittedCrashHandler.checkForUnsubmittedCrashReports(); + Assert.ok(notification, "There should be a notification"); + + // Attempt to submit the notification by clicking on the submit + // button + let buttons = notification.querySelectorAll(".notification-button"); + // ...which should be the first button. + let submit = buttons[0]; + + let promiseReports = waitForSubmittedReports(reportIDs); + info("Sending crash report"); + submit.click(); + info("Sent!"); + // We'll not wait for the notification to finish its transition - + // we'll just remove it right away. + gNotificationBox.removeNotification(notification, true); + + info("Waiting on reports to be received."); + yield promiseReports; + info("Received!"); + clearPendingCrashReports(); +}); + +/** + * Tests that the notification can submit multiple reports. + */ +add_task(function* test_can_submit_several() { + let reportIDs = yield createPendingCrashReports(3); + let notification = + yield UnsubmittedCrashHandler.checkForUnsubmittedCrashReports(); + Assert.ok(notification, "There should be a notification"); + + // Attempt to submit the notification by clicking on the submit + // button + let buttons = notification.querySelectorAll(".notification-button"); + // ...which should be the first button. + let submit = buttons[0]; + + let promiseReports = waitForSubmittedReports(reportIDs); + info("Sending crash reports"); + submit.click(); + info("Sent!"); + // We'll not wait for the notification to finish its transition - + // we'll just remove it right away. + gNotificationBox.removeNotification(notification, true); + + info("Waiting on reports to be received."); + yield promiseReports; + info("Received!"); + clearPendingCrashReports(); +}); + +/** + * Tests that choosing "Send Always" flips the autoSubmit pref + * and sends the pending crash reports. + */ +add_task(function* test_can_submit_always() { + let pref = "browser.crashReports.unsubmittedCheck.autoSubmit2"; + Assert.equal(Services.prefs.getBoolPref(pref), false, + "We should not be auto-submitting by default"); + + let reportIDs = yield createPendingCrashReports(1); + let notification = + yield UnsubmittedCrashHandler.checkForUnsubmittedCrashReports(); + Assert.ok(notification, "There should be a notification"); + + // Attempt to submit the notification by clicking on the send all + // button + let buttons = notification.querySelectorAll(".notification-button"); + // ...which should be the second button. + let sendAll = buttons[1]; + + let promiseReports = waitForSubmittedReports(reportIDs); + info("Sending crash reports"); + sendAll.click(); + info("Sent!"); + // We'll not wait for the notification to finish its transition - + // we'll just remove it right away. + gNotificationBox.removeNotification(notification, true); + + info("Waiting on reports to be received."); + yield promiseReports; + info("Received!"); + + // Make sure the pref was set + Assert.equal(Services.prefs.getBoolPref(pref), true, + "The autoSubmit pref should have been set"); + + // And revert back to default now. + Services.prefs.clearUserPref(pref); + + clearPendingCrashReports(); +}); + +/** + * Tests that if the user has chosen to automatically send + * crash reports that no notification is displayed to the + * user. + */ +add_task(function* test_can_auto_submit() { + yield SpecialPowers.pushPrefEnv({ set: [ + ["browser.crashReports.unsubmittedCheck.autoSubmit2", true], + ]}); + + let reportIDs = yield createPendingCrashReports(3); + let promiseReports = waitForSubmittedReports(reportIDs); + let notification = + yield UnsubmittedCrashHandler.checkForUnsubmittedCrashReports(); + Assert.equal(notification, null, "There should be no notification"); + info("Waiting on reports to be received."); + yield promiseReports; + info("Received!"); + + clearPendingCrashReports(); + yield SpecialPowers.popPrefEnv(); +}); + +/** + * Tests that if the user chooses to dismiss the notification, + * then the current pending requests won't cause the notification + * to appear again in the future. + */ +add_task(function* test_can_ignore() { + let reportIDs = yield createPendingCrashReports(3); + let notification = + yield UnsubmittedCrashHandler.checkForUnsubmittedCrashReports(); + Assert.ok(notification, "There should be a notification"); + + // Dismiss the notification by clicking on the "X" button. + let anonyNodes = document.getAnonymousNodes(notification)[0]; + let closeButton = anonyNodes.querySelector(".close-icon"); + closeButton.click(); + // We'll not wait for the notification to finish its transition - + // we'll just remove it right away. + gNotificationBox.removeNotification(notification, true); + yield waitForIgnoredReports(reportIDs); + + notification = + yield UnsubmittedCrashHandler.checkForUnsubmittedCrashReports(); + Assert.equal(notification, null, "There should be no notification"); + + clearPendingCrashReports(); +}); + +/** + * Tests that if the notification is shown, then the + * lastShownDate is set for today. + */ +add_task(function* test_last_shown_date() { + yield createPendingCrashReports(1); + let notification = + yield UnsubmittedCrashHandler.checkForUnsubmittedCrashReports(); + Assert.ok(notification, "There should be a notification"); + + let today = UnsubmittedCrashHandler.dateString(new Date()); + let lastShownDate = + UnsubmittedCrashHandler.prefs.getCharPref("lastShownDate"); + Assert.equal(today, lastShownDate, + "Last shown date should be today."); + + UnsubmittedCrashHandler.prefs.clearUserPref("lastShownDate"); + gNotificationBox.removeNotification(notification, true); + clearPendingCrashReports(); +}); + +/** + * Tests that if UnsubmittedCrashHandler is uninit with a + * notification still being shown, that + * browser.crashReports.unsubmittedCheck.shutdownWhileShowing is + * set to true. + */ +add_task(function* test_shutdown_while_showing() { + yield createPendingCrashReports(1); + let notification = + yield UnsubmittedCrashHandler.checkForUnsubmittedCrashReports(); + Assert.ok(notification, "There should be a notification"); + + UnsubmittedCrashHandler.uninit(); + let shutdownWhileShowing = + UnsubmittedCrashHandler.prefs.getBoolPref("shutdownWhileShowing"); + Assert.ok(shutdownWhileShowing, + "We should have noticed that we uninitted while showing " + + "the notification."); + UnsubmittedCrashHandler.prefs.clearUserPref("shutdownWhileShowing"); + UnsubmittedCrashHandler.init(); + + gNotificationBox.removeNotification(notification, true); + clearPendingCrashReports(); +}); + +/** + * Tests that if UnsubmittedCrashHandler is uninit after + * the notification has been closed, that + * browser.crashReports.unsubmittedCheck.shutdownWhileShowing is + * not set in prefs. + */ +add_task(function* test_shutdown_while_not_showing() { + let reportIDs = yield createPendingCrashReports(1); + let notification = + yield UnsubmittedCrashHandler.checkForUnsubmittedCrashReports(); + Assert.ok(notification, "There should be a notification"); + + // Dismiss the notification by clicking on the "X" button. + let anonyNodes = document.getAnonymousNodes(notification)[0]; + let closeButton = anonyNodes.querySelector(".close-icon"); + closeButton.click(); + // We'll not wait for the notification to finish its transition - + // we'll just remove it right away. + gNotificationBox.removeNotification(notification, true); + + yield waitForIgnoredReports(reportIDs); + + UnsubmittedCrashHandler.uninit(); + Assert.throws(() => { + UnsubmittedCrashHandler.prefs.getBoolPref("shutdownWhileShowing"); + }, "We should have noticed that the notification had closed before " + + "uninitting."); + UnsubmittedCrashHandler.init(); + + clearPendingCrashReports(); +}); + +/** + * Tests that if + * browser.crashReports.unsubmittedCheck.shutdownWhileShowing is + * set and the lastShownDate is today, then we don't decrement + * browser.crashReports.unsubmittedCheck.chancesUntilSuppress. + */ +add_task(function* test_dont_decrement_chances_on_same_day() { + let initChances = + UnsubmittedCrashHandler.prefs.getIntPref("chancesUntilSuppress"); + Assert.ok(initChances > 1, "We should start with at least 1 chance."); + + yield createPendingCrashReports(1); + let notification = + yield UnsubmittedCrashHandler.checkForUnsubmittedCrashReports(); + Assert.ok(notification, "There should be a notification"); + + UnsubmittedCrashHandler.uninit(); + + gNotificationBox.removeNotification(notification, true); + + let shutdownWhileShowing = + UnsubmittedCrashHandler.prefs.getBoolPref("shutdownWhileShowing"); + Assert.ok(shutdownWhileShowing, + "We should have noticed that we uninitted while showing " + + "the notification."); + + let today = UnsubmittedCrashHandler.dateString(new Date()); + let lastShownDate = + UnsubmittedCrashHandler.prefs.getCharPref("lastShownDate"); + Assert.equal(today, lastShownDate, + "Last shown date should be today."); + + UnsubmittedCrashHandler.init(); + + notification = + yield UnsubmittedCrashHandler.checkForUnsubmittedCrashReports(); + Assert.ok(notification, "There should still be a notification"); + + let chances = + UnsubmittedCrashHandler.prefs.getIntPref("chancesUntilSuppress"); + + Assert.equal(initChances, chances, + "We should not have decremented chances."); + + gNotificationBox.removeNotification(notification, true); + clearPendingCrashReports(); +}); + +/** + * Tests that if + * browser.crashReports.unsubmittedCheck.shutdownWhileShowing is + * set and the lastShownDate is before today, then we decrement + * browser.crashReports.unsubmittedCheck.chancesUntilSuppress. + */ +add_task(function* test_decrement_chances_on_other_day() { + let initChances = + UnsubmittedCrashHandler.prefs.getIntPref("chancesUntilSuppress"); + Assert.ok(initChances > 1, "We should start with at least 1 chance."); + + yield createPendingCrashReports(1); + let notification = + yield UnsubmittedCrashHandler.checkForUnsubmittedCrashReports(); + Assert.ok(notification, "There should be a notification"); + + UnsubmittedCrashHandler.uninit(); + + gNotificationBox.removeNotification(notification, true); + + let shutdownWhileShowing = + UnsubmittedCrashHandler.prefs.getBoolPref("shutdownWhileShowing"); + Assert.ok(shutdownWhileShowing, + "We should have noticed that we uninitted while showing " + + "the notification."); + + // Now pretend that the notification was shown yesterday. + let yesterday = UnsubmittedCrashHandler.dateString(new Date(Date.now() - DAY)); + UnsubmittedCrashHandler.prefs.setCharPref("lastShownDate", yesterday); + + UnsubmittedCrashHandler.init(); + + notification = + yield UnsubmittedCrashHandler.checkForUnsubmittedCrashReports(); + Assert.ok(notification, "There should still be a notification"); + + let chances = + UnsubmittedCrashHandler.prefs.getIntPref("chancesUntilSuppress"); + + Assert.equal(initChances - 1, chances, + "We should have decremented our chances."); + UnsubmittedCrashHandler.prefs.clearUserPref("chancesUntilSuppress"); + + gNotificationBox.removeNotification(notification, true); + clearPendingCrashReports(); +}); + +/** + * Tests that if we've shutdown too many times showing the + * notification, and we've run out of chances, then + * browser.crashReports.unsubmittedCheck.suppressUntilDate is + * set for some days into the future. + */ +add_task(function* test_can_suppress_after_chances() { + // Pretend that a notification was shown yesterday. + let yesterday = UnsubmittedCrashHandler.dateString(new Date(Date.now() - DAY)); + UnsubmittedCrashHandler.prefs.setCharPref("lastShownDate", yesterday); + UnsubmittedCrashHandler.prefs.setBoolPref("shutdownWhileShowing", true); + UnsubmittedCrashHandler.prefs.setIntPref("chancesUntilSuppress", 0); + + yield createPendingCrashReports(1); + let notification = + yield UnsubmittedCrashHandler.checkForUnsubmittedCrashReports(); + Assert.equal(notification, null, + "There should be no notification if we've run out of chances"); + + // We should have set suppressUntilDate into the future + let suppressUntilDate = + UnsubmittedCrashHandler.prefs.getCharPref("suppressUntilDate"); + + let today = UnsubmittedCrashHandler.dateString(new Date()); + Assert.ok(suppressUntilDate > today, + "We should be suppressing until some days into the future."); + + UnsubmittedCrashHandler.prefs.clearUserPref("chancesUntilSuppress"); + UnsubmittedCrashHandler.prefs.clearUserPref("suppressUntilDate"); + UnsubmittedCrashHandler.prefs.clearUserPref("lastShownDate"); + clearPendingCrashReports(); +}); + +/** + * Tests that if there's a suppression date set, then no notification + * will be shown even if there are pending crash reports. + */ +add_task(function* test_suppression() { + let future = UnsubmittedCrashHandler.dateString(new Date(Date.now() + (DAY * 5))); + UnsubmittedCrashHandler.prefs.setCharPref("suppressUntilDate", future); + UnsubmittedCrashHandler.uninit(); + UnsubmittedCrashHandler.init(); + + Assert.ok(UnsubmittedCrashHandler.suppressed, + "The UnsubmittedCrashHandler should be suppressed."); + UnsubmittedCrashHandler.prefs.clearUserPref("suppressUntilDate"); + + UnsubmittedCrashHandler.uninit(); + UnsubmittedCrashHandler.init(); +}); + +/** + * Tests that if there's a suppression date set, but we've exceeded + * it, then we can show the notification again. + */ +add_task(function* test_end_suppression() { + let yesterday = UnsubmittedCrashHandler.dateString(new Date(Date.now() - DAY)); + UnsubmittedCrashHandler.prefs.setCharPref("suppressUntilDate", yesterday); + UnsubmittedCrashHandler.uninit(); + UnsubmittedCrashHandler.init(); + + Assert.ok(!UnsubmittedCrashHandler.suppressed, + "The UnsubmittedCrashHandler should not be suppressed."); + Assert.ok(!UnsubmittedCrashHandler.prefs.prefHasUserValue("suppressUntilDate"), + "The suppression date should been cleared from preferences."); + + UnsubmittedCrashHandler.uninit(); + UnsubmittedCrashHandler.init(); +}); diff --git a/browser/modules/test/browser_UsageTelemetry.js b/browser/modules/test/browser_UsageTelemetry.js new file mode 100644 index 000000000..a84f33a97 --- /dev/null +++ b/browser/modules/test/browser_UsageTelemetry.js @@ -0,0 +1,268 @@ +"use strict"; + +const MAX_CONCURRENT_TABS = "browser.engagement.max_concurrent_tab_count"; +const TAB_EVENT_COUNT = "browser.engagement.tab_open_event_count"; +const MAX_CONCURRENT_WINDOWS = "browser.engagement.max_concurrent_window_count"; +const WINDOW_OPEN_COUNT = "browser.engagement.window_open_event_count"; +const TOTAL_URI_COUNT = "browser.engagement.total_uri_count"; +const UNIQUE_DOMAINS_COUNT = "browser.engagement.unique_domains_count"; +const UNFILTERED_URI_COUNT = "browser.engagement.unfiltered_uri_count"; + +const TELEMETRY_SUBSESSION_TOPIC = "internal-telemetry-after-subsession-split"; + +/** + * Waits for the web progress listener associated with this tab to fire an + * onLocationChange for a non-error page. + * + * @param {xul:browser} browser + * A xul:browser. + * + * @return {Promise} + * @resolves When navigating to a non-error page. + */ +function browserLocationChanged(browser) { + return new Promise(resolve => { + let wpl = { + onStateChange() {}, + onSecurityChange() {}, + onStatusChange() {}, + onLocationChange(aWebProgress, aRequest, aURI, aFlags) { + if (!(aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE)) { + browser.webProgress.removeProgressListener(filter); + filter.removeProgressListener(wpl); + resolve(); + } + }, + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsIWebProgressListener, + Ci.nsIWebProgressListener2, + ]), + }; + const filter = Cc["@mozilla.org/appshell/component/browser-status-filter;1"] + .createInstance(Ci.nsIWebProgress); + filter.addProgressListener(wpl, Ci.nsIWebProgress.NOTIFY_ALL); + browser.webProgress.addProgressListener(filter, Ci.nsIWebProgress.NOTIFY_ALL); + }); +} + +/** + * An helper that checks the value of a scalar if it's expected to be > 0, + * otherwise makes sure that the scalar it's not reported. + */ +let checkScalar = (scalars, scalarName, value, msg) => { + if (value > 0) { + is(scalars[scalarName], value, msg); + return; + } + ok(!(scalarName in scalars), scalarName + " must not be reported."); +}; + +/** + * Get a snapshot of the scalars and check them against the provided values. + */ +let checkScalars = (countsObject) => { + const scalars = + Services.telemetry.snapshotScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN); + + // Check the expected values. Scalars that are never set must not be reported. + checkScalar(scalars, MAX_CONCURRENT_TABS, countsObject.maxTabs, + "The maximum tab count must match the expected value."); + checkScalar(scalars, TAB_EVENT_COUNT, countsObject.tabOpenCount, + "The number of open tab event count must match the expected value."); + checkScalar(scalars, MAX_CONCURRENT_WINDOWS, countsObject.maxWindows, + "The maximum window count must match the expected value."); + checkScalar(scalars, WINDOW_OPEN_COUNT, countsObject.windowsOpenCount, + "The number of window open event count must match the expected value."); + checkScalar(scalars, TOTAL_URI_COUNT, countsObject.totalURIs, + "The total URI count must match the expected value."); + checkScalar(scalars, UNIQUE_DOMAINS_COUNT, countsObject.domainCount, + "The unique domains count must match the expected value."); + checkScalar(scalars, UNFILTERED_URI_COUNT, countsObject.totalUnfilteredURIs, + "The unfiltered URI count must match the expected value."); +}; + +add_task(function* test_tabsAndWindows() { + // Let's reset the counts. + Services.telemetry.clearScalars(); + + let openedTabs = []; + let expectedTabOpenCount = 0; + let expectedWinOpenCount = 0; + let expectedMaxTabs = 0; + let expectedMaxWins = 0; + + // Add a new tab and check that the count is right. + openedTabs.push(yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank")); + expectedTabOpenCount = 1; + expectedMaxTabs = 2; + // This, and all the checks below, also check that initial pages (about:newtab, about:blank, ..) + // are not counted by the total_uri_count and the unfiltered_uri_count probes. + checkScalars({maxTabs: expectedMaxTabs, tabOpenCount: expectedTabOpenCount, maxWindows: expectedMaxWins, + windowsOpenCount: expectedWinOpenCount, totalURIs: 0, domainCount: 0, + totalUnfilteredURIs: 0}); + + // Add two new tabs in the same window. + openedTabs.push(yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank")); + openedTabs.push(yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank")); + expectedTabOpenCount += 2; + expectedMaxTabs += 2; + checkScalars({maxTabs: expectedMaxTabs, tabOpenCount: expectedTabOpenCount, maxWindows: expectedMaxWins, + windowsOpenCount: expectedWinOpenCount, totalURIs: 0, domainCount: 0, + totalUnfilteredURIs: 0}); + + // Add a new window and then some tabs in it. An empty new windows counts as a tab. + let win = yield BrowserTestUtils.openNewBrowserWindow(); + openedTabs.push(yield BrowserTestUtils.openNewForegroundTab(win.gBrowser, "about:blank")); + openedTabs.push(yield BrowserTestUtils.openNewForegroundTab(win.gBrowser, "about:blank")); + openedTabs.push(yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank")); + // The new window started with a new tab, so account for it. + expectedTabOpenCount += 4; + expectedWinOpenCount += 1; + expectedMaxWins = 2; + expectedMaxTabs += 4; + + // Remove a tab from the first window, the max shouldn't change. + yield BrowserTestUtils.removeTab(openedTabs.pop()); + checkScalars({maxTabs: expectedMaxTabs, tabOpenCount: expectedTabOpenCount, maxWindows: expectedMaxWins, + windowsOpenCount: expectedWinOpenCount, totalURIs: 0, domainCount: 0, + totalUnfilteredURIs: 0}); + + // Remove all the extra windows and tabs. + for (let tab of openedTabs) { + yield BrowserTestUtils.removeTab(tab); + } + yield BrowserTestUtils.closeWindow(win); + + // Make sure all the scalars still have the expected values. + checkScalars({maxTabs: expectedMaxTabs, tabOpenCount: expectedTabOpenCount, maxWindows: expectedMaxWins, + windowsOpenCount: expectedWinOpenCount, totalURIs: 0, domainCount: 0, + totalUnfilteredURIs: 0}); +}); + +add_task(function* test_subsessionSplit() { + // Let's reset the counts. + Services.telemetry.clearScalars(); + + // Add a new window (that will have 4 tabs). + let win = yield BrowserTestUtils.openNewBrowserWindow(); + let openedTabs = []; + openedTabs.push(yield BrowserTestUtils.openNewForegroundTab(win.gBrowser, "about:blank")); + openedTabs.push(yield BrowserTestUtils.openNewForegroundTab(win.gBrowser, "about:mozilla")); + openedTabs.push(yield BrowserTestUtils.openNewForegroundTab(win.gBrowser, "http://www.example.com")); + + // Check that the scalars have the right values. We expect 2 unfiltered URI loads + // (about:mozilla and www.example.com, but no about:blank) and 1 URI totalURIs + // (only www.example.com). + checkScalars({maxTabs: 5, tabOpenCount: 4, maxWindows: 2, windowsOpenCount: 1, + totalURIs: 1, domainCount: 1, totalUnfilteredURIs: 2}); + + // Remove a tab. + yield BrowserTestUtils.removeTab(openedTabs.pop()); + + // Simulate a subsession split by clearing the scalars (via |snapshotScalars|) and + // notifying the subsession split topic. + Services.telemetry.snapshotScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, + true /* clearScalars */); + Services.obs.notifyObservers(null, TELEMETRY_SUBSESSION_TOPIC, ""); + + // After a subsession split, only the MAX_CONCURRENT_* scalars must be available + // and have the correct value. No tabs, windows or URIs were opened so other scalars + // must not be reported. + checkScalars({maxTabs: 4, tabOpenCount: 0, maxWindows: 2, windowsOpenCount: 0, + totalURIs: 0, domainCount: 0, totalUnfilteredURIs: 0}); + + // Remove all the extra windows and tabs. + for (let tab of openedTabs) { + yield BrowserTestUtils.removeTab(tab); + } + yield BrowserTestUtils.closeWindow(win); +}); + +add_task(function* test_URIAndDomainCounts() { + // Let's reset the counts. + Services.telemetry.clearScalars(); + + let checkCounts = (countsObject) => { + // Get a snapshot of the scalars and then clear them. + const scalars = + Services.telemetry.snapshotScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN); + checkScalar(scalars, TOTAL_URI_COUNT, countsObject.totalURIs, + "The URI scalar must contain the expected value."); + checkScalar(scalars, UNIQUE_DOMAINS_COUNT, countsObject.domainCount, + "The unique domains scalar must contain the expected value."); + checkScalar(scalars, UNFILTERED_URI_COUNT, countsObject.totalUnfilteredURIs, + "The unfiltered URI scalar must contain the expected value."); + }; + + // Check that about:blank doesn't get counted in the URI total. + let firstTab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank"); + checkCounts({totalURIs: 0, domainCount: 0, totalUnfilteredURIs: 0}); + + // Open a different page and check the counts. + yield BrowserTestUtils.loadURI(firstTab.linkedBrowser, "http://example.com/"); + yield BrowserTestUtils.browserLoaded(firstTab.linkedBrowser); + checkCounts({totalURIs: 1, domainCount: 1, totalUnfilteredURIs: 1}); + + // Activating a different tab must not increase the URI count. + let secondTab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank"); + yield BrowserTestUtils.switchTab(gBrowser, firstTab); + checkCounts({totalURIs: 1, domainCount: 1, totalUnfilteredURIs: 1}); + yield BrowserTestUtils.removeTab(secondTab); + + // Open a new window and set the tab to a new address. + let newWin = yield BrowserTestUtils.openNewBrowserWindow(); + yield BrowserTestUtils.loadURI(newWin.gBrowser.selectedBrowser, "http://example.com/"); + yield BrowserTestUtils.browserLoaded(newWin.gBrowser.selectedBrowser); + checkCounts({totalURIs: 2, domainCount: 1, totalUnfilteredURIs: 2}); + + // We should not count AJAX requests. + const XHR_URL = "http://example.com/r"; + yield ContentTask.spawn(newWin.gBrowser.selectedBrowser, XHR_URL, function(url) { + return new Promise(resolve => { + var xhr = new content.window.XMLHttpRequest(); + xhr.open("GET", url); + xhr.onload = () => resolve(); + xhr.send(); + }); + }); + checkCounts({totalURIs: 2, domainCount: 1, totalUnfilteredURIs: 2}); + + // Check that we're counting page fragments. + let loadingStopped = browserLocationChanged(newWin.gBrowser.selectedBrowser); + yield BrowserTestUtils.loadURI(newWin.gBrowser.selectedBrowser, "http://example.com/#2"); + yield loadingStopped; + checkCounts({totalURIs: 3, domainCount: 1, totalUnfilteredURIs: 3}); + + // Check that a different URI from the example.com domain doesn't increment the unique count. + yield BrowserTestUtils.loadURI(newWin.gBrowser.selectedBrowser, "http://test1.example.com/"); + yield BrowserTestUtils.browserLoaded(newWin.gBrowser.selectedBrowser); + checkCounts({totalURIs: 4, domainCount: 1, totalUnfilteredURIs: 4}); + + // Make sure that the unique domains counter is incrementing for a different domain. + yield BrowserTestUtils.loadURI(newWin.gBrowser.selectedBrowser, "https://example.org/"); + yield BrowserTestUtils.browserLoaded(newWin.gBrowser.selectedBrowser); + checkCounts({totalURIs: 5, domainCount: 2, totalUnfilteredURIs: 5}); + + // Check that we only account for top level loads (e.g. we don't count URIs from + // embedded iframes). + yield ContentTask.spawn(newWin.gBrowser.selectedBrowser, null, function* () { + let doc = content.document; + let iframe = doc.createElement("iframe"); + let promiseIframeLoaded = ContentTaskUtils.waitForEvent(iframe, "load", false); + iframe.src = "https://example.org/test"; + doc.body.insertBefore(iframe, doc.body.firstChild); + yield promiseIframeLoaded; + }); + checkCounts({totalURIs: 5, domainCount: 2, totalUnfilteredURIs: 5}); + + // Check that uncommon protocols get counted in the unfiltered URI probe. + const TEST_PAGE = + "data:text/html,<a id='target' href='%23par1'>Click me</a><a name='par1'>The paragraph.</a>"; + yield BrowserTestUtils.loadURI(newWin.gBrowser.selectedBrowser, TEST_PAGE); + yield BrowserTestUtils.browserLoaded(newWin.gBrowser.selectedBrowser); + checkCounts({totalURIs: 5, domainCount: 2, totalUnfilteredURIs: 6}); + + // Clean up. + yield BrowserTestUtils.removeTab(firstTab); + yield BrowserTestUtils.closeWindow(newWin); +}); diff --git a/browser/modules/test/browser_UsageTelemetry_content.js b/browser/modules/test/browser_UsageTelemetry_content.js new file mode 100644 index 000000000..35c6b5a6d --- /dev/null +++ b/browser/modules/test/browser_UsageTelemetry_content.js @@ -0,0 +1,121 @@ +"use strict"; + +const BASE_PROBE_NAME = "browser.engagement.navigation."; +const SCALAR_CONTEXT_MENU = BASE_PROBE_NAME + "contextmenu"; +const SCALAR_ABOUT_NEWTAB = BASE_PROBE_NAME + "about_newtab"; + +add_task(function* setup() { + // Create two new search engines. Mark one as the default engine, so + // the test don't crash. We need to engines for this test as the searchbar + // in content doesn't display the default search engine among the one-off engines. + Services.search.addEngineWithDetails("MozSearch", "", "mozalias", "", "GET", + "http://example.com/?q={searchTerms}"); + + Services.search.addEngineWithDetails("MozSearch2", "", "mozalias2", "", "GET", + "http://example.com/?q={searchTerms}"); + + // Make the first engine the default search engine. + let engineDefault = Services.search.getEngineByName("MozSearch"); + let originalEngine = Services.search.currentEngine; + Services.search.currentEngine = engineDefault; + + // Move the second engine at the beginning of the one-off list. + let engineOneOff = Services.search.getEngineByName("MozSearch2"); + Services.search.moveEngine(engineOneOff, 0); + + yield SpecialPowers.pushPrefEnv({"set": [ + ["dom.select_events.enabled", true], // We want select events to be fired. + ["toolkit.telemetry.enabled", true] // And Extended Telemetry to be enabled. + ]}); + + // Make sure to restore the engine once we're done. + registerCleanupFunction(function* () { + Services.search.currentEngine = originalEngine; + Services.search.removeEngine(engineDefault); + Services.search.removeEngine(engineOneOff); + }); +}); + +add_task(function* test_context_menu() { + // Let's reset the Telemetry data. + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + let search_hist = getSearchCountsHistogram(); + + // Open a new tab with a page containing some text. + let tab = + yield BrowserTestUtils.openNewForegroundTab(gBrowser, "data:text/plain;charset=utf8,test%20search"); + + info("Select all the text in the page."); + yield ContentTask.spawn(tab.linkedBrowser, "", function*() { + return new Promise(resolve => { + content.document.addEventListener("selectionchange", () => resolve(), { once: true }); + content.document.getSelection().selectAllChildren(content.document.body); + }); + }); + + info("Open the context menu."); + let contextMenu = document.getElementById("contentAreaContextMenu"); + let popupPromise = BrowserTestUtils.waitForEvent(contextMenu, "popupshown"); + BrowserTestUtils.synthesizeMouseAtCenter("body", { type: "contextmenu", button: 2 }, + gBrowser.selectedBrowser); + yield popupPromise; + + info("Click on search."); + let searchItem = contextMenu.getElementsByAttribute("id", "context-searchselect")[0]; + searchItem.click(); + + info("Validate the search metrics."); + const scalars = + Services.telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false); + checkKeyedScalar(scalars, SCALAR_CONTEXT_MENU, "search", 1); + Assert.equal(Object.keys(scalars[SCALAR_CONTEXT_MENU]).length, 1, + "This search must only increment one entry in the scalar."); + + // Make sure SEARCH_COUNTS contains identical values. + checkKeyedHistogram(search_hist, 'other-MozSearch.contextmenu', 1); + + // Also check events. + let events = Services.telemetry.snapshotBuiltinEvents(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false); + events = events.filter(e => e[1] == "navigation" && e[2] == "search"); + checkEvents(events, [["navigation", "search", "contextmenu", null, {engine: "other-MozSearch"}]]); + + contextMenu.hidePopup(); + yield BrowserTestUtils.removeTab(gBrowser.selectedTab); + yield BrowserTestUtils.removeTab(tab); +}); + +add_task(function* test_about_newtab() { + // Let's reset the counts. + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + let search_hist = getSearchCountsHistogram(); + + let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:newtab", false); + yield ContentTask.spawn(tab.linkedBrowser, null, function* () { + yield ContentTaskUtils.waitForCondition(() => !content.document.hidden); + }); + + info("Trigger a simple serch, just text + enter."); + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + yield typeInSearchField(tab.linkedBrowser, "test query", "newtab-search-text"); + yield BrowserTestUtils.synthesizeKey("VK_RETURN", {}, tab.linkedBrowser); + yield p; + + // Check if the scalars contain the expected values. + const scalars = + Services.telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false); + checkKeyedScalar(scalars, SCALAR_ABOUT_NEWTAB, "search_enter", 1); + Assert.equal(Object.keys(scalars[SCALAR_ABOUT_NEWTAB]).length, 1, + "This search must only increment one entry in the scalar."); + + // Make sure SEARCH_COUNTS contains identical values. + checkKeyedHistogram(search_hist, 'other-MozSearch.newtab', 1); + + // Also check events. + let events = Services.telemetry.snapshotBuiltinEvents(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false); + events = events.filter(e => e[1] == "navigation" && e[2] == "search"); + checkEvents(events, [["navigation", "search", "about_newtab", "enter", {engine: "other-MozSearch"}]]); + + yield BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/modules/test/browser_UsageTelemetry_content_aboutHome.js b/browser/modules/test/browser_UsageTelemetry_content_aboutHome.js new file mode 100644 index 000000000..1818ae5fd --- /dev/null +++ b/browser/modules/test/browser_UsageTelemetry_content_aboutHome.js @@ -0,0 +1,84 @@ +"use strict"; + +const SCALAR_ABOUT_HOME = "browser.engagement.navigation.about_home"; + +add_task(function* setup() { + // about:home uses IndexedDB. However, the test finishes too quickly and doesn't + // allow it enougth time to save. So it throws. This disables all the uncaught + // exception in this file and that's the reason why we split about:home tests + // out of the other UsageTelemetry files. + ignoreAllUncaughtExceptions(); + + // Create two new search engines. Mark one as the default engine, so + // the test don't crash. We need to engines for this test as the searchbar + // in content doesn't display the default search engine among the one-off engines. + Services.search.addEngineWithDetails("MozSearch", "", "mozalias", "", "GET", + "http://example.com/?q={searchTerms}"); + + Services.search.addEngineWithDetails("MozSearch2", "", "mozalias2", "", "GET", + "http://example.com/?q={searchTerms}"); + + // Make the first engine the default search engine. + let engineDefault = Services.search.getEngineByName("MozSearch"); + let originalEngine = Services.search.currentEngine; + Services.search.currentEngine = engineDefault; + + // Move the second engine at the beginning of the one-off list. + let engineOneOff = Services.search.getEngineByName("MozSearch2"); + Services.search.moveEngine(engineOneOff, 0); + + // Enable Extended Telemetry. + yield SpecialPowers.pushPrefEnv({"set": [["toolkit.telemetry.enabled", true]]}); + + // Make sure to restore the engine once we're done. + registerCleanupFunction(function* () { + Services.search.currentEngine = originalEngine; + Services.search.removeEngine(engineDefault); + Services.search.removeEngine(engineOneOff); + }); +}); + +add_task(function* test_abouthome_simpleQuery() { + // Let's reset the counts. + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + let search_hist = getSearchCountsHistogram(); + + let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser); + + info("Setup waiting for AboutHomeLoadSnippetsCompleted."); + let promiseAboutHomeLoaded = new Promise(resolve => { + tab.linkedBrowser.addEventListener("AboutHomeLoadSnippetsCompleted", function loadListener(event) { + tab.linkedBrowser.removeEventListener("AboutHomeLoadSnippetsCompleted", loadListener, true); + resolve(); + }, true, true); + }); + + info("Load about:home."); + tab.linkedBrowser.loadURI("about:home"); + info("Wait for AboutHomeLoadSnippetsCompleted."); + yield promiseAboutHomeLoaded; + + info("Trigger a simple serch, just test + enter."); + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + yield typeInSearchField(tab.linkedBrowser, "test query", "searchText"); + yield BrowserTestUtils.synthesizeKey("VK_RETURN", {}, tab.linkedBrowser); + yield p; + + // Check if the scalars contain the expected values. + const scalars = + Services.telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false); + checkKeyedScalar(scalars, SCALAR_ABOUT_HOME, "search_enter", 1); + Assert.equal(Object.keys(scalars[SCALAR_ABOUT_HOME]).length, 1, + "This search must only increment one entry in the scalar."); + + // Make sure SEARCH_COUNTS contains identical values. + checkKeyedHistogram(search_hist, 'other-MozSearch.abouthome', 1); + + // Also check events. + let events = Services.telemetry.snapshotBuiltinEvents(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false); + events = events.filter(e => e[1] == "navigation" && e[2] == "search"); + checkEvents(events, [["navigation", "search", "about_home", "enter", {engine: "other-MozSearch"}]]); + + yield BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/modules/test/browser_UsageTelemetry_private_and_restore.js b/browser/modules/test/browser_UsageTelemetry_private_and_restore.js new file mode 100644 index 000000000..144a4a03f --- /dev/null +++ b/browser/modules/test/browser_UsageTelemetry_private_and_restore.js @@ -0,0 +1,90 @@ +"use strict"; + +const MAX_CONCURRENT_TABS = "browser.engagement.max_concurrent_tab_count"; +const TAB_EVENT_COUNT = "browser.engagement.tab_open_event_count"; +const MAX_CONCURRENT_WINDOWS = "browser.engagement.max_concurrent_window_count"; +const WINDOW_OPEN_COUNT = "browser.engagement.window_open_event_count"; +const TOTAL_URI_COUNT = "browser.engagement.total_uri_count"; +const UNFILTERED_URI_COUNT = "browser.engagement.unfiltered_uri_count"; +const UNIQUE_DOMAINS_COUNT = "browser.engagement.unique_domains_count"; + +function promiseBrowserStateRestored() { + return new Promise(resolve => { + Services.obs.addObserver(function observer(aSubject, aTopic) { + Services.obs.removeObserver(observer, "sessionstore-browser-state-restored"); + resolve(); + }, "sessionstore-browser-state-restored", false); + }); +} + +add_task(function* test_privateMode() { + // Let's reset the counts. + Services.telemetry.clearScalars(); + + // Open a private window and load a website in it. + let privateWin = yield BrowserTestUtils.openNewBrowserWindow({private: true}); + yield BrowserTestUtils.loadURI(privateWin.gBrowser.selectedBrowser, "http://example.com/"); + yield BrowserTestUtils.browserLoaded(privateWin.gBrowser.selectedBrowser); + + // Check that tab and window count is recorded. + const scalars = + Services.telemetry.snapshotScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN); + + ok(!(TOTAL_URI_COUNT in scalars), "We should not track URIs in private mode."); + ok(!(UNFILTERED_URI_COUNT in scalars), "We should not track URIs in private mode."); + ok(!(UNIQUE_DOMAINS_COUNT in scalars), "We should not track unique domains in private mode."); + is(scalars[TAB_EVENT_COUNT], 1, "The number of open tab event count must match the expected value."); + is(scalars[MAX_CONCURRENT_TABS], 2, "The maximum tab count must match the expected value."); + is(scalars[WINDOW_OPEN_COUNT], 1, "The number of window open event count must match the expected value."); + is(scalars[MAX_CONCURRENT_WINDOWS], 2, "The maximum window count must match the expected value."); + + // Clean up. + yield BrowserTestUtils.closeWindow(privateWin); +}); + +add_task(function* test_sessionRestore() { + const PREF_RESTORE_ON_DEMAND = "browser.sessionstore.restore_on_demand"; + Services.prefs.setBoolPref(PREF_RESTORE_ON_DEMAND, false); + registerCleanupFunction(() => { + Services.prefs.clearUserPref(PREF_RESTORE_ON_DEMAND); + }); + + // Let's reset the counts. + Services.telemetry.clearScalars(); + + // The first window will be put into the already open window and the second + // window will be opened with _openWindowWithState, which is the source of the problem. + const state = { + windows: [ + { + tabs: [ + { entries: [{ url: "http://example.org" }], extData: { "uniq": 3785 } } + ], + selected: 1 + } + ] + }; + + // Save the current session. + let SessionStore = + Cu.import("resource:///modules/sessionstore/SessionStore.jsm", {}).SessionStore; + + // Load the custom state and wait for SSTabRestored, as we want to make sure + // that the URI counting code was hit. + let tabRestored = BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "SSTabRestored"); + SessionStore.setBrowserState(JSON.stringify(state)); + yield tabRestored; + + // Check that the URI is not recorded. + const scalars = + Services.telemetry.snapshotScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN); + + ok(!(TOTAL_URI_COUNT in scalars), "We should not track URIs from restored sessions."); + ok(!(UNFILTERED_URI_COUNT in scalars), "We should not track URIs from restored sessions."); + ok(!(UNIQUE_DOMAINS_COUNT in scalars), "We should not track unique domains from restored sessions."); + + // Restore the original session and cleanup. + let sessionRestored = promiseBrowserStateRestored(); + SessionStore.setBrowserState(JSON.stringify(state)); + yield sessionRestored; +}); diff --git a/browser/modules/test/browser_UsageTelemetry_searchbar.js b/browser/modules/test/browser_UsageTelemetry_searchbar.js new file mode 100644 index 000000000..8aa3ceaee --- /dev/null +++ b/browser/modules/test/browser_UsageTelemetry_searchbar.js @@ -0,0 +1,195 @@ +"use strict"; + +const SCALAR_SEARCHBAR = "browser.engagement.navigation.searchbar"; + +let searchInSearchbar = Task.async(function* (inputText) { + let win = window; + yield new Promise(r => waitForFocus(r, win)); + let sb = BrowserSearch.searchBar; + // Write the search query in the searchbar. + sb.focus(); + sb.value = inputText; + sb.textbox.controller.startSearch(inputText); + // Wait for the popup to show. + yield BrowserTestUtils.waitForEvent(sb.textbox.popup, "popupshown"); + // And then for the search to complete. + yield BrowserTestUtils.waitForCondition(() => sb.textbox.controller.searchStatus >= + Ci.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH, + "The search in the searchbar must complete."); +}); + +/** + * Click one of the entries in the search suggestion popup. + * + * @param {String} entryName + * The name of the elemet to click on. + */ +function clickSearchbarSuggestion(entryName) { + let popup = BrowserSearch.searchBar.textbox.popup; + let column = popup.tree.columns[0]; + + for (let rowID = 0; rowID < popup.tree.view.rowCount; rowID++) { + const suggestion = popup.tree.view.getValueAt(rowID, column); + if (suggestion !== entryName) { + continue; + } + + // Make sure the suggestion is visible, just in case. + let tbo = popup.tree.treeBoxObject; + tbo.ensureRowIsVisible(rowID); + // Calculate the click coordinates. + let rect = tbo.getCoordsForCellItem(rowID, column, "text"); + let x = rect.x + rect.width / 2; + let y = rect.y + rect.height / 2; + // Simulate the click. + EventUtils.synthesizeMouse(popup.tree.body, x, y, {}, + popup.tree.ownerGlobal); + break; + } +} + +add_task(function* setup() { + // Create two new search engines. Mark one as the default engine, so + // the test don't crash. We need to engines for this test as the searchbar + // doesn't display the default search engine among the one-off engines. + Services.search.addEngineWithDetails("MozSearch", "", "mozalias", "", "GET", + "http://example.com/?q={searchTerms}"); + + Services.search.addEngineWithDetails("MozSearch2", "", "mozalias2", "", "GET", + "http://example.com/?q={searchTerms}"); + + // Make the first engine the default search engine. + let engineDefault = Services.search.getEngineByName("MozSearch"); + let originalEngine = Services.search.currentEngine; + Services.search.currentEngine = engineDefault; + + // Move the second engine at the beginning of the one-off list. + let engineOneOff = Services.search.getEngineByName("MozSearch2"); + Services.search.moveEngine(engineOneOff, 0); + + // Enable Extended Telemetry. + yield SpecialPowers.pushPrefEnv({"set": [["toolkit.telemetry.enabled", true]]}); + + // Make sure to restore the engine once we're done. + registerCleanupFunction(function* () { + Services.search.currentEngine = originalEngine; + Services.search.removeEngine(engineDefault); + Services.search.removeEngine(engineOneOff); + }); +}); + +add_task(function* test_plainQuery() { + // Let's reset the counts. + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + let search_hist = getSearchCountsHistogram(); + + let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank"); + + info("Simulate entering a simple search."); + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + yield searchInSearchbar("simple query"); + EventUtils.sendKey("return"); + yield p; + + // Check if the scalars contain the expected values. + const scalars = + Services.telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false); + checkKeyedScalar(scalars, SCALAR_SEARCHBAR, "search_enter", 1); + Assert.equal(Object.keys(scalars[SCALAR_SEARCHBAR]).length, 1, + "This search must only increment one entry in the scalar."); + + // Make sure SEARCH_COUNTS contains identical values. + checkKeyedHistogram(search_hist, 'other-MozSearch.searchbar', 1); + + // Also check events. + let events = Services.telemetry.snapshotBuiltinEvents(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false); + events = events.filter(e => e[1] == "navigation" && e[2] == "search"); + checkEvents(events, [["navigation", "search", "searchbar", "enter", {engine: "other-MozSearch"}]]); + + yield BrowserTestUtils.removeTab(tab); +}); + +add_task(function* test_oneOff() { + // Let's reset the counts. + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + let search_hist = getSearchCountsHistogram(); + + let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank"); + + info("Perform a one-off search using the first engine."); + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + yield searchInSearchbar("query"); + + info("Pressing Alt+Down to highlight the first one off engine."); + EventUtils.synthesizeKey("VK_DOWN", { altKey: true }); + EventUtils.sendKey("return"); + yield p; + + // Check if the scalars contain the expected values. + const scalars = + Services.telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false); + checkKeyedScalar(scalars, SCALAR_SEARCHBAR, "search_oneoff", 1); + Assert.equal(Object.keys(scalars[SCALAR_SEARCHBAR]).length, 1, + "This search must only increment one entry in the scalar."); + + // Make sure SEARCH_COUNTS contains identical values. + checkKeyedHistogram(search_hist, 'other-MozSearch2.searchbar', 1); + + // Also check events. + let events = Services.telemetry.snapshotBuiltinEvents(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false); + events = events.filter(e => e[1] == "navigation" && e[2] == "search"); + checkEvents(events, [["navigation", "search", "searchbar", "oneoff", {engine: "other-MozSearch2"}]]); + + yield BrowserTestUtils.removeTab(tab); +}); + +add_task(function* test_suggestion() { + // Let's reset the counts. + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + let search_hist = getSearchCountsHistogram(); + + // Create an engine to generate search suggestions and add it as default + // for this test. + const url = getRootDirectory(gTestPath) + "usageTelemetrySearchSuggestions.xml"; + let suggestionEngine = yield new Promise((resolve, reject) => { + Services.search.addEngine(url, null, "", false, { + onSuccess(engine) { resolve(engine) }, + onError() { reject() } + }); + }); + + let previousEngine = Services.search.currentEngine; + Services.search.currentEngine = suggestionEngine; + + let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank"); + + info("Perform a one-off search using the first engine."); + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + yield searchInSearchbar("query"); + info("Clicking the searchbar suggestion."); + clickSearchbarSuggestion("queryfoo"); + yield p; + + // Check if the scalars contain the expected values. + const scalars = + Services.telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false); + checkKeyedScalar(scalars, SCALAR_SEARCHBAR, "search_suggestion", 1); + Assert.equal(Object.keys(scalars[SCALAR_SEARCHBAR]).length, 1, + "This search must only increment one entry in the scalar."); + + // Make sure SEARCH_COUNTS contains identical values. + let searchEngineId = 'other-' + suggestionEngine.name; + checkKeyedHistogram(search_hist, searchEngineId + '.searchbar', 1); + + // Also check events. + let events = Services.telemetry.snapshotBuiltinEvents(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false); + events = events.filter(e => e[1] == "navigation" && e[2] == "search"); + checkEvents(events, [["navigation", "search", "searchbar", "suggestion", {engine: searchEngineId}]]); + + Services.search.currentEngine = previousEngine; + Services.search.removeEngine(suggestionEngine); + yield BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/modules/test/browser_UsageTelemetry_urlbar.js b/browser/modules/test/browser_UsageTelemetry_urlbar.js new file mode 100644 index 000000000..81d3e28ba --- /dev/null +++ b/browser/modules/test/browser_UsageTelemetry_urlbar.js @@ -0,0 +1,220 @@ +"use strict"; + +const SCALAR_URLBAR = "browser.engagement.navigation.urlbar"; + +// The preference to enable suggestions in the urlbar. +const SUGGEST_URLBAR_PREF = "browser.urlbar.suggest.searches"; +// The name of the search engine used to generate suggestions. +const SUGGESTION_ENGINE_NAME = "browser_UsageTelemetry usageTelemetrySearchSuggestions.xml"; +const ONEOFF_URLBAR_PREF = "browser.urlbar.oneOffSearches"; + +let searchInAwesomebar = Task.async(function* (inputText, win=window) { + yield new Promise(r => waitForFocus(r, win)); + // Write the search query in the urlbar. + win.gURLBar.focus(); + win.gURLBar.value = inputText; + win.gURLBar.controller.startSearch(inputText); + // Wait for the popup to show. + yield BrowserTestUtils.waitForEvent(win.gURLBar.popup, "popupshown"); + // And then for the search to complete. + yield BrowserTestUtils.waitForCondition(() => win.gURLBar.controller.searchStatus >= + Ci.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH); +}); + +/** + * Click one of the entries in the urlbar suggestion popup. + * + * @param {String} entryName + * The name of the elemet to click on. + */ +function clickURLBarSuggestion(entryName) { + // The entry in the suggestion list should follow the format: + // "<search term> <engine name> Search" + const expectedSuggestionName = entryName + " " + SUGGESTION_ENGINE_NAME + " Search"; + for (let child of gURLBar.popup.richlistbox.children) { + if (child.label === expectedSuggestionName) { + // This entry is the search suggestion we're looking for. + child.click(); + return; + } + } +} + +add_task(function* setup() { + // Create a new search engine. + Services.search.addEngineWithDetails("MozSearch", "", "mozalias", "", "GET", + "http://example.com/?q={searchTerms}"); + + // Make it the default search engine. + let engine = Services.search.getEngineByName("MozSearch"); + let originalEngine = Services.search.currentEngine; + Services.search.currentEngine = engine; + + // And the first one-off engine. + Services.search.moveEngine(engine, 0); + + // Enable search suggestions in the urlbar. + Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, true); + + // Enable the urlbar one-off buttons. + Services.prefs.setBoolPref(ONEOFF_URLBAR_PREF, true); + + // Enable Extended Telemetry. + yield SpecialPowers.pushPrefEnv({"set": [["toolkit.telemetry.enabled", true]]}); + + // Make sure to restore the engine once we're done. + registerCleanupFunction(function* () { + Services.search.currentEngine = originalEngine; + Services.search.removeEngine(engine); + Services.prefs.clearUserPref(SUGGEST_URLBAR_PREF, true); + Services.prefs.clearUserPref(ONEOFF_URLBAR_PREF); + }); +}); + +add_task(function* test_simpleQuery() { + // Let's reset the counts. + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + let search_hist = getSearchCountsHistogram(); + + let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank"); + + info("Simulate entering a simple search."); + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + yield searchInAwesomebar("simple query"); + EventUtils.sendKey("return"); + yield p; + + // Check if the scalars contain the expected values. + const scalars = + Services.telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false); + checkKeyedScalar(scalars, SCALAR_URLBAR, "search_enter", 1); + Assert.equal(Object.keys(scalars[SCALAR_URLBAR]).length, 1, + "This search must only increment one entry in the scalar."); + + // Make sure SEARCH_COUNTS contains identical values. + checkKeyedHistogram(search_hist, 'other-MozSearch.urlbar', 1); + + // Also check events. + let events = Services.telemetry.snapshotBuiltinEvents(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false); + events = events.filter(e => e[1] == "navigation" && e[2] == "search"); + checkEvents(events, [["navigation", "search", "urlbar", "enter", {engine: "other-MozSearch"}]]); + + yield BrowserTestUtils.removeTab(tab); +}); + +add_task(function* test_searchAlias() { + // Let's reset the counts. + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + let search_hist = getSearchCountsHistogram(); + + let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank"); + + info("Search using a search alias."); + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + yield searchInAwesomebar("mozalias query"); + EventUtils.sendKey("return"); + yield p; + + // Check if the scalars contain the expected values. + const scalars = + Services.telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false); + checkKeyedScalar(scalars, SCALAR_URLBAR, "search_alias", 1); + Assert.equal(Object.keys(scalars[SCALAR_URLBAR]).length, 1, + "This search must only increment one entry in the scalar."); + + // Make sure SEARCH_COUNTS contains identical values. + checkKeyedHistogram(search_hist, 'other-MozSearch.urlbar', 1); + + // Also check events. + let events = Services.telemetry.snapshotBuiltinEvents(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false); + events = events.filter(e => e[1] == "navigation" && e[2] == "search"); + checkEvents(events, [["navigation", "search", "urlbar", "alias", {engine: "other-MozSearch"}]]); + + yield BrowserTestUtils.removeTab(tab); +}); + +add_task(function* test_oneOff() { + // Let's reset the counts. + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + let search_hist = getSearchCountsHistogram(); + + let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank"); + + info("Perform a one-off search using the first engine."); + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + yield searchInAwesomebar("query"); + + info("Pressing Alt+Down to take us to the first one-off engine."); + EventUtils.synthesizeKey("VK_DOWN", { altKey: true }); + EventUtils.sendKey("return"); + yield p; + + // Check if the scalars contain the expected values. + const scalars = + Services.telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false); + checkKeyedScalar(scalars, SCALAR_URLBAR, "search_oneoff", 1); + Assert.equal(Object.keys(scalars[SCALAR_URLBAR]).length, 1, + "This search must only increment one entry in the scalar."); + + // Make sure SEARCH_COUNTS contains identical values. + checkKeyedHistogram(search_hist, 'other-MozSearch.urlbar', 1); + + // Also check events. + let events = Services.telemetry.snapshotBuiltinEvents(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false); + events = events.filter(e => e[1] == "navigation" && e[2] == "search"); + checkEvents(events, [["navigation", "search", "urlbar", "oneoff", {engine: "other-MozSearch"}]]); + + yield BrowserTestUtils.removeTab(tab); +}); + +add_task(function* test_suggestion() { + // Let's reset the counts. + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + let search_hist = getSearchCountsHistogram(); + + // Create an engine to generate search suggestions and add it as default + // for this test. + const url = getRootDirectory(gTestPath) + "usageTelemetrySearchSuggestions.xml"; + let suggestionEngine = yield new Promise((resolve, reject) => { + Services.search.addEngine(url, null, "", false, { + onSuccess(engine) { resolve(engine) }, + onError() { reject() } + }); + }); + + let previousEngine = Services.search.currentEngine; + Services.search.currentEngine = suggestionEngine; + + let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank"); + + info("Perform a one-off search using the first engine."); + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + yield searchInAwesomebar("query"); + info("Clicking the urlbar suggestion."); + clickURLBarSuggestion("queryfoo"); + yield p; + + // Check if the scalars contain the expected values. + const scalars = + Services.telemetry.snapshotKeyedScalars(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false); + checkKeyedScalar(scalars, SCALAR_URLBAR, "search_suggestion", 1); + Assert.equal(Object.keys(scalars[SCALAR_URLBAR]).length, 1, + "This search must only increment one entry in the scalar."); + + // Make sure SEARCH_COUNTS contains identical values. + let searchEngineId = 'other-' + suggestionEngine.name; + checkKeyedHistogram(search_hist, searchEngineId + '.urlbar', 1); + + // Also check events. + let events = Services.telemetry.snapshotBuiltinEvents(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false); + events = events.filter(e => e[1] == "navigation" && e[2] == "search"); + checkEvents(events, [["navigation", "search", "urlbar", "suggestion", {engine: searchEngineId}]]); + + Services.search.currentEngine = previousEngine; + Services.search.removeEngine(suggestionEngine); + yield BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/modules/test/browser_taskbar_preview.js b/browser/modules/test/browser_taskbar_preview.js new file mode 100644 index 000000000..89295b9e0 --- /dev/null +++ b/browser/modules/test/browser_taskbar_preview.js @@ -0,0 +1,100 @@ +function test() { + var isWin7OrHigher = false; + try { + let version = Cc["@mozilla.org/system-info;1"] + .getService(Ci.nsIPropertyBag2) + .getProperty("version"); + isWin7OrHigher = (parseFloat(version) >= 6.1); + } catch (ex) { } + + is(!!Win7Features, isWin7OrHigher, "Win7Features available when it should be"); + if (!isWin7OrHigher) + return; + + const ENABLE_PREF_NAME = "browser.taskbar.previews.enable"; + + let temp = {}; + Cu.import("resource:///modules/WindowsPreviewPerTab.jsm", temp); + let AeroPeek = temp.AeroPeek; + + waitForExplicitFinish(); + + gPrefService.setBoolPref(ENABLE_PREF_NAME, true); + + is(1, AeroPeek.windows.length, "Got the expected number of windows"); + + checkPreviews(1, "Browser starts with one preview"); + + gBrowser.addTab(); + gBrowser.addTab(); + gBrowser.addTab(); + + checkPreviews(4, "Correct number of previews after adding"); + + for (let preview of AeroPeek.previews) + ok(preview.visible, "Preview is shown as expected"); + + gPrefService.setBoolPref(ENABLE_PREF_NAME, false); + is(0, AeroPeek.previews.length, "Should have 0 previews when disabled"); + + gPrefService.setBoolPref(ENABLE_PREF_NAME, true); + checkPreviews(4, "Previews are back when re-enabling"); + for (let preview of AeroPeek.previews) + ok(preview.visible, "Preview is shown as expected after re-enabling"); + + [1, 2, 3, 4].forEach(function (idx) { + gBrowser.selectedTab = gBrowser.tabs[idx]; + ok(checkSelectedTab(), "Current tab is correctly selected"); + }); + + // Close #4 + getPreviewForTab(gBrowser.selectedTab).controller.onClose(); + checkPreviews(3, "Expected number of previews after closing selected tab via controller"); + ok(gBrowser.tabs.length == 3, "Successfully closed a tab"); + + // Select #1 + ok(getPreviewForTab(gBrowser.tabs[0]).controller.onActivate(), "Activation was accepted"); + ok(gBrowser.tabs[0].selected, "Correct tab was selected"); + checkSelectedTab(); + + // Remove #3 (non active) + gBrowser.removeTab(gBrowser.tabContainer.lastChild); + checkPreviews(2, "Expected number of previews after closing unselected via browser"); + + // Remove #1 (active) + gBrowser.removeTab(gBrowser.tabContainer.firstChild); + checkPreviews(1, "Expected number of previews after closing selected tab via browser"); + + // Add a new tab + gBrowser.addTab(); + checkPreviews(2); + // Check default selection + checkSelectedTab(); + + // Change selection + gBrowser.selectedTab = gBrowser.tabs[0]; + checkSelectedTab(); + // Close nonselected tab via controller + getPreviewForTab(gBrowser.tabs[1]).controller.onClose(); + checkPreviews(1); + + if (gPrefService.prefHasUserValue(ENABLE_PREF_NAME)) + gPrefService.setBoolPref(ENABLE_PREF_NAME, !gPrefService.getBoolPref(ENABLE_PREF_NAME)); + + finish(); + + function checkPreviews(aPreviews, msg) { + let nPreviews = AeroPeek.previews.length; + is(aPreviews, gBrowser.tabs.length, "Browser has expected number of tabs - " + msg); + is(nPreviews, gBrowser.tabs.length, "Browser has one preview per tab - " + msg); + is(nPreviews, aPreviews, msg || "Got expected number of previews"); + } + + function getPreviewForTab(tab) { + return window.gTaskbarTabGroup.previewFromTab(tab); + } + + function checkSelectedTab() { + return getPreviewForTab(gBrowser.selectedTab).active; + } +} diff --git a/browser/modules/test/browser_urlBar_zoom.js b/browser/modules/test/browser_urlBar_zoom.js new file mode 100644 index 000000000..9cb5c96c6 --- /dev/null +++ b/browser/modules/test/browser_urlBar_zoom.js @@ -0,0 +1,73 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var initialPageZoom = ZoomManager.zoom; +const kTimeoutInMS = 20000; + +add_task(function* () { + info("Confirm whether the browser zoom is set to the default level"); + is(initialPageZoom, 1, "Page zoom is set to default (100%)"); + let zoomResetButton = document.getElementById("urlbar-zoom-button"); + is(zoomResetButton.hidden, true, "Zoom reset button is currently hidden"); + + info("Change zoom and confirm zoom button appears"); + let labelUpdatePromise = BrowserTestUtils.waitForAttribute("label", zoomResetButton); + FullZoom.enlarge(); + yield labelUpdatePromise; + info("Zoom increased to " + Math.floor(ZoomManager.zoom * 100) + "%"); + is(zoomResetButton.hidden, false, "Zoom reset button is now visible"); + let pageZoomLevel = Math.floor(ZoomManager.zoom * 100); + let expectedZoomLevel = 110; + let buttonZoomLevel = parseInt(zoomResetButton.getAttribute("label"), 10); + is(buttonZoomLevel, expectedZoomLevel, ("Button label updated successfully to " + Math.floor(ZoomManager.zoom * 100) + "%")); + + let zoomResetPromise = promiseObserverNotification("browser-fullZoom:zoomReset"); + zoomResetButton.click(); + yield zoomResetPromise; + pageZoomLevel = Math.floor(ZoomManager.zoom * 100); + expectedZoomLevel = 100; + is(pageZoomLevel, expectedZoomLevel, "Clicking zoom button successfully resets browser zoom to 100%"); + is(zoomResetButton.hidden, true, "Zoom reset button returns to being hidden"); + +}); + +add_task(function* () { + info("Confirm that URL bar zoom button doesn't appear when customizable zoom widget is added to toolbar"); + CustomizableUI.addWidgetToArea("zoom-controls", CustomizableUI.AREA_NAVBAR); + let zoomCustomizableWidget = document.getElementById("zoom-reset-button"); + let zoomResetButton = document.getElementById("urlbar-zoom-button"); + let zoomChangePromise = promiseObserverNotification("browser-fullZoom:zoomChange"); + FullZoom.enlarge(); + yield zoomChangePromise; + is(zoomResetButton.hidden, true, "URL zoom button remains hidden despite zoom increase"); + is(parseInt(zoomCustomizableWidget.label, 10), 110, "Customizable zoom widget's label has updated to " + zoomCustomizableWidget.label); +}); + +add_task(function* asyncCleanup() { + // reset zoom level and customizable widget + ZoomManager.zoom = initialPageZoom; + is(ZoomManager.zoom, 1, "Zoom level was restored"); + if (document.getElementById("zoom-controls")) { + CustomizableUI.removeWidgetFromArea("zoom-controls", CustomizableUI.AREA_NAVBAR); + ok(!document.getElementById("zoom-controls"), "Customizable zoom widget removed from toolbar"); + } + +}); + +function promiseObserverNotification(aObserver) { + let deferred = Promise.defer(); + function notificationCallback(e) { + Services.obs.removeObserver(notificationCallback, aObserver, false); + clearTimeout(timeoutId); + deferred.resolve(); + } + let timeoutId = setTimeout(() => { + Services.obs.removeObserver(notificationCallback, aObserver, false); + deferred.reject("Notification '" + aObserver + "' did not happen within 20 seconds."); + }, kTimeoutInMS); + Services.obs.addObserver(notificationCallback, aObserver, false); + return deferred.promise; +} diff --git a/browser/modules/test/contentSearch.js b/browser/modules/test/contentSearch.js new file mode 100644 index 000000000..b5dddfe45 --- /dev/null +++ b/browser/modules/test/contentSearch.js @@ -0,0 +1,64 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const TEST_MSG = "ContentSearchTest"; +const SERVICE_EVENT_TYPE = "ContentSearchService"; +const CLIENT_EVENT_TYPE = "ContentSearchClient"; + +// Forward events from the in-content service to the test. +content.addEventListener(SERVICE_EVENT_TYPE, event => { + // The event dispatch code in content.js clones the event detail into the + // content scope. That's generally the right thing, but causes us to end + // up with an XrayWrapper to it here, which will screw us up when trying to + // serialize the object in sendAsyncMessage. Waive Xrays for the benefit of + // the test machinery. + sendAsyncMessage(TEST_MSG, Components.utils.waiveXrays(event.detail)); +}); + +// Forward messages from the test to the in-content service. +addMessageListener(TEST_MSG, msg => { + // If the message is a search, stop the page from loading and then tell the + // test that it loaded. + if (msg.data.type == "Search") { + waitForLoadAndStopIt(msg.data.expectedURL, url => { + sendAsyncMessage(TEST_MSG, { + type: "loadStopped", + url: url, + }); + }); + } + + content.dispatchEvent( + new content.CustomEvent(CLIENT_EVENT_TYPE, { + detail: msg.data, + }) + ); +}); + +function waitForLoadAndStopIt(expectedURL, callback) { + let Ci = Components.interfaces; + let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebProgress); + let listener = { + onStateChange: function (webProg, req, flags, status) { + if (req instanceof Ci.nsIChannel) { + let url = req.originalURI.spec; + dump("waitForLoadAndStopIt: onStateChange " + url + "\n"); + let docStart = Ci.nsIWebProgressListener.STATE_IS_DOCUMENT | + Ci.nsIWebProgressListener.STATE_START; + if ((flags & docStart) && webProg.isTopLevel && url == expectedURL) { + webProgress.removeProgressListener(listener); + req.cancel(Components.results.NS_ERROR_FAILURE); + callback(url); + } + } + }, + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsIWebProgressListener, + Ci.nsISupportsWeakReference, + ]), + }; + webProgress.addProgressListener(listener, Ci.nsIWebProgress.NOTIFY_ALL); + dump("waitForLoadAndStopIt: Waiting for URL to load: " + expectedURL + "\n"); +} diff --git a/browser/modules/test/contentSearchBadImage.xml b/browser/modules/test/contentSearchBadImage.xml new file mode 100644 index 000000000..6e4cb60a5 --- /dev/null +++ b/browser/modules/test/contentSearchBadImage.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/"> +<ShortName>browser_ContentSearch contentSearchBadImage.xml</ShortName> +<Url type="text/html" method="GET" template="http://browser-ContentSearch.com/contentSearchBadImage" rel="searchform"/> +<Image width="16" height="16">data:image/x-icon;base64,notbase64</Image> +</SearchPlugin> diff --git a/browser/modules/test/contentSearchSuggestions.sjs b/browser/modules/test/contentSearchSuggestions.sjs new file mode 100644 index 000000000..1978b4f66 --- /dev/null +++ b/browser/modules/test/contentSearchSuggestions.sjs @@ -0,0 +1,9 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +function handleRequest(req, resp) { + let suffixes = ["foo", "bar"]; + let data = [req.queryString, suffixes.map(s => req.queryString + s)]; + resp.setHeader("Content-Type", "application/json", false); + resp.write(JSON.stringify(data)); +} diff --git a/browser/modules/test/contentSearchSuggestions.xml b/browser/modules/test/contentSearchSuggestions.xml new file mode 100644 index 000000000..81c23379c --- /dev/null +++ b/browser/modules/test/contentSearchSuggestions.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/"> +<ShortName>browser_ContentSearch contentSearchSuggestions.xml</ShortName> +<Url type="application/x-suggestions+json" method="GET" template="http://mochi.test:8888/browser/browser/modules/test/contentSearchSuggestions.sjs?{searchTerms}"/> +<Url type="text/html" method="GET" template="http://browser-ContentSearch.com/contentSearchSuggestions" rel="searchform"/> +</SearchPlugin> diff --git a/browser/modules/test/head.js b/browser/modules/test/head.js new file mode 100644 index 000000000..be0215156 --- /dev/null +++ b/browser/modules/test/head.js @@ -0,0 +1,113 @@ +Cu.import("resource://gre/modules/Promise.jsm"); + +const SINGLE_TRY_TIMEOUT = 100; +const NUMBER_OF_TRIES = 30; + +function waitForConditionPromise(condition, timeoutMsg, tryCount=NUMBER_OF_TRIES) { + let defer = Promise.defer(); + let tries = 0; + function checkCondition() { + if (tries >= tryCount) { + defer.reject(timeoutMsg); + } + var conditionPassed; + try { + conditionPassed = condition(); + } catch (e) { + return defer.reject(e); + } + if (conditionPassed) { + return defer.resolve(); + } + tries++; + setTimeout(checkCondition, SINGLE_TRY_TIMEOUT); + return undefined; + } + setTimeout(checkCondition, SINGLE_TRY_TIMEOUT); + return defer.promise; +} + +function waitForCondition(condition, nextTest, errorMsg) { + waitForConditionPromise(condition, errorMsg).then(nextTest, (reason) => { + ok(false, reason + (reason.stack ? "\n" + reason.stack : "")); + }); +} + +/** + * Checks if the snapshotted keyed scalars contain the expected + * data. + * + * @param {Object} scalars + * The snapshot of the keyed scalars. + * @param {String} scalarName + * The name of the keyed scalar to check. + * @param {String} key + * The key that must be within the keyed scalar. + * @param {String|Boolean|Number} expectedValue + * The expected value for the provided key in the scalar. + */ +function checkKeyedScalar(scalars, scalarName, key, expectedValue) { + Assert.ok(scalarName in scalars, + scalarName + " must be recorded."); + Assert.ok(key in scalars[scalarName], + scalarName + " must contain the '" + key + "' key."); + Assert.ok(scalars[scalarName][key], expectedValue, + scalarName + "['" + key + "'] must contain the expected value"); +} + +/** + * An utility function to write some text in the search input box + * in a content page. + * @param {Object} browser + * The browser that contains the content. + * @param {String} text + * The string to write in the search field. + * @param {String} fieldName + * The name of the field to write to. + */ +let typeInSearchField = Task.async(function* (browser, text, fieldName) { + yield ContentTask.spawn(browser, { fieldName, text }, function* ({fieldName, text}) { + // Avoid intermittent failures. + if (fieldName === "searchText") { + content.wrappedJSObject.gContentSearchController.remoteTimeout = 5000; + } + // Put the focus on the search box. + let searchInput = content.document.getElementById(fieldName); + searchInput.focus(); + searchInput.value = text; + }); +}); + +/** + * Clear and get the SEARCH_COUNTS histogram. + */ +function getSearchCountsHistogram() { + let search_hist = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS"); + search_hist.clear(); + return search_hist; +} + +/** + * Check that the keyed histogram contains the right value. + */ +function checkKeyedHistogram(h, key, expectedValue) { + const snapshot = h.snapshot(); + Assert.ok(key in snapshot, `The histogram must contain ${key}.`); + Assert.equal(snapshot[key].sum, expectedValue, `The key ${key} must contain ${expectedValue}.`); +} + +function checkEvents(events, expectedEvents) { + if (!Services.telemetry.canRecordExtended) { + // Currently we only collect the tested events when extended Telemetry is enabled. + return; + } + + Assert.equal(events.length, expectedEvents.length, "Should have matching amount of events."); + + // Strip timestamps from the events for easier comparison. + events = events.map(e => e.slice(1)); + + for (let i = 0; i < events.length; ++i) { + Assert.deepEqual(events[i], expectedEvents[i], "Events should match."); + } +} diff --git a/browser/modules/test/unit/social/.eslintrc.js b/browser/modules/test/unit/social/.eslintrc.js new file mode 100644 index 000000000..d35787cd2 --- /dev/null +++ b/browser/modules/test/unit/social/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + "extends": [ + "../../../../../testing/xpcshell/xpcshell.eslintrc.js" + ] +}; diff --git a/browser/modules/test/unit/social/blocklist.xml b/browser/modules/test/unit/social/blocklist.xml new file mode 100644 index 000000000..c8d72d624 --- /dev/null +++ b/browser/modules/test/unit/social/blocklist.xml @@ -0,0 +1,6 @@ +<?xml version="1.0"?> +<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist"> + <emItems> + <emItem blockID="s1" id="bad.com@services.mozilla.org"></emItem> + </emItems> +</blocklist> diff --git a/browser/modules/test/unit/social/head.js b/browser/modules/test/unit/social/head.js new file mode 100644 index 000000000..0beabb685 --- /dev/null +++ b/browser/modules/test/unit/social/head.js @@ -0,0 +1,210 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +var Social, SocialService; + +var manifests = [ + { + name: "provider 1", + origin: "https://example1.com", + sidebarURL: "https://example1.com/sidebar/", + }, + { + name: "provider 2", + origin: "https://example2.com", + sidebarURL: "https://example1.com/sidebar/", + } +]; + +const MANIFEST_PREFS = Services.prefs.getBranch("social.manifest."); + +// SocialProvider class relies on blocklisting being enabled. To enable +// blocklisting, we have to setup an app and initialize the blocklist (see +// initApp below). +const gProfD = do_get_profile(); + +function createAppInfo(ID, name, version, platformVersion="1.0") { + let tmp = {}; + Cu.import("resource://testing-common/AppInfo.jsm", tmp); + tmp.updateAppInfo({ + ID, name, version, platformVersion, + crashReporter: true, + }); + gAppInfo = tmp.getAppInfo(); +} + +function initApp() { + createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9"); + // prepare a blocklist file for the blocklist service + var blocklistFile = gProfD.clone(); + blocklistFile.append("blocklist.xml"); + if (blocklistFile.exists()) + blocklistFile.remove(false); + var source = do_get_file("blocklist.xml"); + source.copyTo(gProfD, "blocklist.xml"); + blocklistFile.lastModifiedTime = Date.now(); + + + let internalManager = Cc["@mozilla.org/addons/integration;1"]. + getService(Ci.nsIObserver). + QueryInterface(Ci.nsITimerCallback); + + internalManager.observe(null, "addons-startup", null); +} + +function setManifestPref(manifest) { + let string = Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString); + string.data = JSON.stringify(manifest); + Services.prefs.setComplexValue("social.manifest." + manifest.origin, Ci.nsISupportsString, string); +} + +function do_wait_observer(obsTopic, cb) { + function observer(subject, topic, data) { + Services.obs.removeObserver(observer, topic); + cb(); + } + Services.obs.addObserver(observer, obsTopic, false); +} + +function do_add_providers(cb) { + // run only after social is already initialized + SocialService.addProvider(manifests[0], function() { + do_wait_observer("social:providers-changed", function() { + do_check_eq(Social.providers.length, 2, "2 providers installed"); + do_execute_soon(cb); + }); + SocialService.addProvider(manifests[1]); + }); +} + +function do_initialize_social(enabledOnStartup, cb) { + initApp(); + + if (enabledOnStartup) { + // set prefs before initializing social + manifests.forEach(function (manifest) { + setManifestPref(manifest); + }); + // Set both providers active and flag the first one as "current" + let activeVal = Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString); + let active = {}; + for (let m of manifests) + active[m.origin] = 1; + activeVal.data = JSON.stringify(active); + Services.prefs.setComplexValue("social.activeProviders", + Ci.nsISupportsString, activeVal); + + do_register_cleanup(function() { + manifests.forEach(function (manifest) { + Services.prefs.clearUserPref("social.manifest." + manifest.origin); + }); + Services.prefs.clearUserPref("social.activeProviders"); + }); + + // expecting 2 providers installed + do_wait_observer("social:providers-changed", function() { + do_check_eq(Social.providers.length, 2, "2 providers installed"); + do_execute_soon(cb); + }); + } + + // import and initialize everything + SocialService = Cu.import("resource:///modules/SocialService.jsm", {}).SocialService; + do_check_eq(enabledOnStartup, SocialService.hasEnabledProviders, "Service has enabled providers"); + Social = Cu.import("resource:///modules/Social.jsm", {}).Social; + do_check_false(Social.initialized, "Social is not initialized"); + Social.init(); + do_check_true(Social.initialized, "Social is initialized"); + if (!enabledOnStartup) + do_execute_soon(cb); +} + +function AsyncRunner() { + do_test_pending(); + do_register_cleanup(() => this.destroy()); + + this._callbacks = { + done: do_test_finished, + error: function (err) { + // xpcshell test functions like do_check_eq throw NS_ERROR_ABORT on + // failure. Ignore those so they aren't rethrown here. + if (err !== Cr.NS_ERROR_ABORT) { + if (err.stack) { + err = err + " - See following stack:\n" + err.stack + + "\nUseless do_throw stack"; + } + do_throw(err); + } + }, + consoleError: function (scriptErr) { + // Try to ensure the error is related to the test. + let filename = scriptErr.sourceName || scriptErr.toString() || ""; + if (filename.indexOf("/toolkit/components/social/") >= 0) + do_throw(scriptErr); + }, + }; + this._iteratorQueue = []; + + // This catches errors reported to the console, e.g., via Cu.reportError, but + // not on the runner's stack. + Cc["@mozilla.org/consoleservice;1"]. + getService(Ci.nsIConsoleService). + registerListener(this); +} + +AsyncRunner.prototype = { + + appendIterator: function appendIterator(iter) { + this._iteratorQueue.push(iter); + }, + + next: function next(arg) { + if (!this._iteratorQueue.length) { + this.destroy(); + this._callbacks.done(); + return; + } + + try { + var { done, value: val } = this._iteratorQueue[0].next(arg); + if (done) { + this._iteratorQueue.shift(); + this.next(); + return; + } + } + catch (err) { + this._callbacks.error(err); + } + + // val is an iterator => prepend it to the queue and start on it + // val is otherwise truthy => call next + if (val) { + if (typeof(val) != "boolean") + this._iteratorQueue.unshift(val); + this.next(); + } + }, + + destroy: function destroy() { + Cc["@mozilla.org/consoleservice;1"]. + getService(Ci.nsIConsoleService). + unregisterListener(this); + this.destroy = function alreadyDestroyed() {}; + }, + + observe: function observe(msg) { + if (msg instanceof Ci.nsIScriptError && + !(msg.flags & Ci.nsIScriptError.warningFlag)) + { + this._callbacks.consoleError(msg); + } + }, +}; diff --git a/browser/modules/test/unit/social/test_SocialService.js b/browser/modules/test/unit/social/test_SocialService.js new file mode 100644 index 000000000..e6f354fed --- /dev/null +++ b/browser/modules/test/unit/social/test_SocialService.js @@ -0,0 +1,166 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +Cu.import("resource://gre/modules/Services.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "PlacesTestUtils", + "resource://testing-common/PlacesTestUtils.jsm"); + +function run_test() { + initApp(); + + // NOTE: none of the manifests here can have a workerURL set, or we attempt + // to create a FrameWorker and that fails under xpcshell... + let manifests = [ + { // normal provider + name: "provider 1", + origin: "https://example1.com", + shareURL: "https://example1.com/share/", + }, + { // provider without workerURL + name: "provider 2", + origin: "https://example2.com", + shareURL: "https://example2.com/share/", + } + ]; + + Cu.import("resource:///modules/SocialService.jsm"); + + let runner = new AsyncRunner(); + let next = runner.next.bind(runner); + runner.appendIterator(testAddProviders(manifests, next)); + runner.appendIterator(testGetProvider(manifests, next)); + runner.appendIterator(testGetProviderList(manifests, next)); + runner.appendIterator(testAddRemoveProvider(manifests, next)); + runner.appendIterator(testIsSameOrigin(manifests, next)); + runner.appendIterator(testResolveUri (manifests, next)); + runner.appendIterator(testOrderedProviders(manifests, next)); + runner.appendIterator(testRemoveProviders(manifests, next)); + runner.next(); +} + +function* testAddProviders(manifests, next) { + do_check_false(SocialService.enabled); + let provider = yield SocialService.addProvider(manifests[0], next); + do_check_true(SocialService.enabled); + do_check_false(provider.enabled); + provider = yield SocialService.addProvider(manifests[1], next); + do_check_false(provider.enabled); +} + +function* testRemoveProviders(manifests, next) { + do_check_true(SocialService.enabled); + yield SocialService.disableProvider(manifests[0].origin, next); + yield SocialService.disableProvider(manifests[1].origin, next); + do_check_false(SocialService.enabled); +} + +function* testGetProvider(manifests, next) { + for (let i = 0; i < manifests.length; i++) { + let manifest = manifests[i]; + let provider = yield SocialService.getProvider(manifest.origin, next); + do_check_neq(provider, null); + do_check_eq(provider.name, manifest.name); + do_check_eq(provider.workerURL, manifest.workerURL); + do_check_eq(provider.origin, manifest.origin); + } + do_check_eq((yield SocialService.getProvider("bogus", next)), null); +} + +function* testGetProviderList(manifests, next) { + let providers = yield SocialService.getProviderList(next); + do_check_true(providers.length >= manifests.length); + for (let i = 0; i < manifests.length; i++) { + let providerIdx = providers.map(p => p.origin).indexOf(manifests[i].origin); + let provider = providers[providerIdx]; + do_check_true(!!provider); + do_check_false(provider.enabled); + do_check_eq(provider.workerURL, manifests[i].workerURL); + do_check_eq(provider.name, manifests[i].name); + } +} + +function* testAddRemoveProvider(manifests, next) { + var threw; + try { + // Adding a provider whose origin already exists should fail + SocialService.addProvider(manifests[0]); + } catch (ex) { + threw = ex; + } + do_check_neq(threw.toString().indexOf("SocialService.addProvider: provider with this origin already exists"), -1); + + let originalProviders = yield SocialService.getProviderList(next); + + // Check that provider installation succeeds + let newProvider = yield SocialService.addProvider({ + name: "foo", + origin: "http://example3.com" + }, next); + let retrievedNewProvider = yield SocialService.getProvider(newProvider.origin, next); + do_check_eq(newProvider, retrievedNewProvider); + + let providersAfter = yield SocialService.getProviderList(next); + do_check_eq(providersAfter.length, originalProviders.length + 1); + do_check_neq(providersAfter.indexOf(newProvider), -1); + + // Now remove the provider + yield SocialService.disableProvider(newProvider.origin, next); + providersAfter = yield SocialService.getProviderList(next); + do_check_eq(providersAfter.length, originalProviders.length); + do_check_eq(providersAfter.indexOf(newProvider), -1); + newProvider = yield SocialService.getProvider(newProvider.origin, next); + do_check_true(!newProvider); +} + +function* testIsSameOrigin(manifests, next) { + let providers = yield SocialService.getProviderList(next); + let provider = providers[0]; + // provider.origin is a string. + do_check_true(provider.isSameOrigin(provider.origin)); + do_check_true(provider.isSameOrigin(Services.io.newURI(provider.origin, null, null))); + do_check_true(provider.isSameOrigin(provider.origin + "/some-sub-page")); + do_check_true(provider.isSameOrigin(Services.io.newURI(provider.origin + "/some-sub-page", null, null))); + do_check_false(provider.isSameOrigin("http://something.com")); + do_check_false(provider.isSameOrigin(Services.io.newURI("http://something.com", null, null))); + do_check_false(provider.isSameOrigin("data:text/html,<p>hi")); + do_check_true(provider.isSameOrigin("data:text/html,<p>hi", true)); + do_check_false(provider.isSameOrigin(Services.io.newURI("data:text/html,<p>hi", null, null))); + do_check_true(provider.isSameOrigin(Services.io.newURI("data:text/html,<p>hi", null, null), true)); + // we explicitly handle null and return false + do_check_false(provider.isSameOrigin(null)); +} + +function* testResolveUri(manifests, next) { + let providers = yield SocialService.getProviderList(next); + let provider = providers[0]; + do_check_eq(provider.resolveUri(provider.origin).spec, provider.origin + "/"); + do_check_eq(provider.resolveUri("foo.html").spec, provider.origin + "/foo.html"); + do_check_eq(provider.resolveUri("/foo.html").spec, provider.origin + "/foo.html"); + do_check_eq(provider.resolveUri("http://somewhereelse.com/foo.html").spec, "http://somewhereelse.com/foo.html"); + do_check_eq(provider.resolveUri("data:text/html,<p>hi").spec, "data:text/html,<p>hi"); +} + +function* testOrderedProviders(manifests, next) { + let providers = yield SocialService.getProviderList(next); + + // add visits for only one of the providers + let visits = []; + let startDate = Date.now() * 1000; + for (let i = 0; i < 10; i++) { + visits.push({ + uri: Services.io.newURI(providers[1].shareURL + i, null, null), + visitDate: startDate + i + }); + } + + PlacesTestUtils.addVisits(visits).then(next); + yield; + let orderedProviders = yield SocialService.getOrderedProviderList(next); + do_check_eq(orderedProviders[0], providers[1]); + do_check_eq(orderedProviders[1], providers[0]); + do_check_true(orderedProviders[0].frecency > orderedProviders[1].frecency); + PlacesTestUtils.clearHistory().then(next); + yield; +} diff --git a/browser/modules/test/unit/social/test_SocialServiceMigration21.js b/browser/modules/test/unit/social/test_SocialServiceMigration21.js new file mode 100644 index 000000000..dfe6183bf --- /dev/null +++ b/browser/modules/test/unit/social/test_SocialServiceMigration21.js @@ -0,0 +1,54 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +Cu.import("resource://gre/modules/Services.jsm"); + +const DEFAULT_PREFS = Services.prefs.getDefaultBranch("social.manifest."); + +function run_test() { + // Test must run at startup for migration to occur, so we can only test + // one migration per test file + initApp(); + + // NOTE: none of the manifests here can have a workerURL set, or we attempt + // to create a FrameWorker and that fails under xpcshell... + let manifest = { // normal provider + name: "provider 1", + origin: "https://example1.com", + builtin: true // as of fx22 this should be true for default prefs + }; + + DEFAULT_PREFS.setCharPref(manifest.origin, JSON.stringify(manifest)); + Services.prefs.setBoolPref("social.active", true); + + Cu.import("resource:///modules/SocialService.jsm"); + + let runner = new AsyncRunner(); + let next = runner.next.bind(runner); + runner.appendIterator(testMigration(manifest, next)); + runner.next(); +} + +function* testMigration(manifest, next) { + // look at social.activeProviders, we should have migrated into that, and + // we should be set as a user level pref after migration + do_check_false(MANIFEST_PREFS.prefHasUserValue(manifest.origin)); + // we need to access the providers for everything to initialize + yield SocialService.getProviderList(next); + do_check_true(SocialService.enabled); + do_check_true(Services.prefs.prefHasUserValue("social.activeProviders")); + + let activeProviders; + let pref = Services.prefs.getComplexValue("social.activeProviders", + Ci.nsISupportsString); + activeProviders = JSON.parse(pref); + do_check_true(activeProviders[manifest.origin]); + do_check_true(MANIFEST_PREFS.prefHasUserValue(manifest.origin)); + do_check_true(JSON.parse(DEFAULT_PREFS.getCharPref(manifest.origin)).builtin); + + let userPref = JSON.parse(MANIFEST_PREFS.getCharPref(manifest.origin)); + do_check_true(parseInt(userPref.updateDate) > 0); + // migrated providers wont have an installDate + do_check_true(userPref.installDate === 0); +} diff --git a/browser/modules/test/unit/social/test_SocialServiceMigration22.js b/browser/modules/test/unit/social/test_SocialServiceMigration22.js new file mode 100644 index 000000000..1a3953175 --- /dev/null +++ b/browser/modules/test/unit/social/test_SocialServiceMigration22.js @@ -0,0 +1,67 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +Cu.import("resource://gre/modules/Services.jsm"); + +const DEFAULT_PREFS = Services.prefs.getDefaultBranch("social.manifest."); + +function run_test() { + // Test must run at startup for migration to occur, so we can only test + // one migration per test file + initApp(); + + // NOTE: none of the manifests here can have a workerURL set, or we attempt + // to create a FrameWorker and that fails under xpcshell... + let manifest = { // normal provider + name: "provider 1", + origin: "https://example1.com", + builtin: true // as of fx22 this should be true for default prefs + }; + + DEFAULT_PREFS.setCharPref(manifest.origin, JSON.stringify(manifest)); + + // Set both providers active and flag the first one as "current" + let activeVal = Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString); + let active = {}; + active[manifest.origin] = 1; + // bad.origin tests that a missing manifest does not break migration, bug 859715 + active["bad.origin"] = 1; + activeVal.data = JSON.stringify(active); + Services.prefs.setComplexValue("social.activeProviders", + Ci.nsISupportsString, activeVal); + + Cu.import("resource:///modules/SocialService.jsm"); + + let runner = new AsyncRunner(); + let next = runner.next.bind(runner); + runner.appendIterator(testMigration(manifest, next)); + runner.next(); +} + +function* testMigration(manifest, next) { + // look at social.activeProviders, we should have migrated into that, and + // we should be set as a user level pref after migration + do_check_false(MANIFEST_PREFS.prefHasUserValue(manifest.origin)); + // we need to access the providers for everything to initialize + yield SocialService.getProviderList(next); + do_check_true(SocialService.enabled); + do_check_true(Services.prefs.prefHasUserValue("social.activeProviders")); + + let activeProviders; + let pref = Services.prefs.getComplexValue("social.activeProviders", + Ci.nsISupportsString); + activeProviders = JSON.parse(pref); + do_check_true(activeProviders[manifest.origin]); + do_check_true(MANIFEST_PREFS.prefHasUserValue(manifest.origin)); + do_check_true(JSON.parse(DEFAULT_PREFS.getCharPref(manifest.origin)).builtin); + + let userPref = JSON.parse(MANIFEST_PREFS.getCharPref(manifest.origin)); + do_check_true(parseInt(userPref.updateDate) > 0); + // migrated providers wont have an installDate + do_check_true(userPref.installDate === 0); + + // bug 859715, this should have been removed during migration + do_check_false(!!activeProviders["bad.origin"]); +} diff --git a/browser/modules/test/unit/social/test_SocialServiceMigration29.js b/browser/modules/test/unit/social/test_SocialServiceMigration29.js new file mode 100644 index 000000000..824673ddf --- /dev/null +++ b/browser/modules/test/unit/social/test_SocialServiceMigration29.js @@ -0,0 +1,61 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +Cu.import("resource://gre/modules/Services.jsm"); + + +function run_test() { + // Test must run at startup for migration to occur, so we can only test + // one migration per test file + initApp(); + + // NOTE: none of the manifests here can have a workerURL set, or we attempt + // to create a FrameWorker and that fails under xpcshell... + let manifest = { // normal provider + name: "provider 1", + origin: "https://example1.com", + }; + + MANIFEST_PREFS.setCharPref(manifest.origin, JSON.stringify(manifest)); + + // Set both providers active and flag the first one as "current" + let activeVal = Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString); + let active = {}; + active[manifest.origin] = 1; + activeVal.data = JSON.stringify(active); + Services.prefs.setComplexValue("social.activeProviders", + Ci.nsISupportsString, activeVal); + + // social.enabled pref is the key focus of this test. We set the user pref, + // and then migration should a) remove the provider from activeProviders and + // b) unset social.enabled + Services.prefs.setBoolPref("social.enabled", false); + + Cu.import("resource:///modules/SocialService.jsm"); + + let runner = new AsyncRunner(); + let next = runner.next.bind(runner); + runner.appendIterator(testMigration(manifest, next)); + runner.next(); +} + +function* testMigration(manifest, next) { + // look at social.activeProviders, we should have migrated into that, and + // we should be set as a user level pref after migration + do_check_true(Services.prefs.prefHasUserValue("social.enabled")); + do_check_true(MANIFEST_PREFS.prefHasUserValue(manifest.origin)); + // we need to access the providers for everything to initialize + yield SocialService.getProviderList(next); + do_check_false(SocialService.enabled); + do_check_false(Services.prefs.prefHasUserValue("social.enabled")); + do_check_true(Services.prefs.prefHasUserValue("social.activeProviders")); + + let activeProviders; + let pref = Services.prefs.getComplexValue("social.activeProviders", + Ci.nsISupportsString).data; + activeProviders = JSON.parse(pref); + do_check_true(activeProviders[manifest.origin] == undefined); + do_check_true(MANIFEST_PREFS.prefHasUserValue(manifest.origin)); +} diff --git a/browser/modules/test/unit/social/test_social.js b/browser/modules/test/unit/social/test_social.js new file mode 100644 index 000000000..3117306c1 --- /dev/null +++ b/browser/modules/test/unit/social/test_social.js @@ -0,0 +1,32 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function run_test() { + // we are testing worker startup specifically + do_test_pending(); + add_test(testStartupEnabled); + add_test(testDisableAfterStartup); + do_initialize_social(true, run_next_test); +} + +function testStartupEnabled() { + // wait on startup before continuing + do_check_eq(Social.providers.length, 2, "two social providers enabled"); + do_check_true(Social.providers[0].enabled, "provider 0 is enabled"); + do_check_true(Social.providers[1].enabled, "provider 1 is enabled"); + run_next_test(); +} + +function testDisableAfterStartup() { + let SocialService = Cu.import("resource:///modules/SocialService.jsm", {}).SocialService; + SocialService.disableProvider(Social.providers[0].origin, function() { + do_wait_observer("social:providers-changed", function() { + do_check_eq(Social.enabled, false, "Social is disabled"); + do_check_eq(Social.providers.length, 0, "no social providers available"); + do_test_finished(); + run_next_test(); + }); + SocialService.disableProvider(Social.providers[0].origin) + }); +} diff --git a/browser/modules/test/unit/social/test_socialDisabledStartup.js b/browser/modules/test/unit/social/test_socialDisabledStartup.js new file mode 100644 index 000000000..a2f7a1d5a --- /dev/null +++ b/browser/modules/test/unit/social/test_socialDisabledStartup.js @@ -0,0 +1,29 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function run_test() { + // we are testing worker startup specifically + do_test_pending(); + add_test(testStartupDisabled); + add_test(testEnableAfterStartup); + do_initialize_social(false, run_next_test); +} + +function testStartupDisabled() { + // wait on startup before continuing + do_check_false(Social.enabled, "Social is disabled"); + do_check_eq(Social.providers.length, 0, "zero social providers available"); + run_next_test(); +} + +function testEnableAfterStartup() { + do_add_providers(function () { + do_check_true(Social.enabled, "Social is enabled"); + do_check_eq(Social.providers.length, 2, "two social providers available"); + do_check_true(Social.providers[0].enabled, "provider 0 is enabled"); + do_check_true(Social.providers[1].enabled, "provider 1 is enabled"); + do_test_finished(); + run_next_test(); + }); +} diff --git a/browser/modules/test/unit/social/xpcshell.ini b/browser/modules/test/unit/social/xpcshell.ini new file mode 100644 index 000000000..277dd4f49 --- /dev/null +++ b/browser/modules/test/unit/social/xpcshell.ini @@ -0,0 +1,13 @@ +[DEFAULT] +head = head.js +tail = +firefox-appdir = browser +skip-if = toolkit == 'android' +support-files = blocklist.xml + +[test_social.js] +[test_socialDisabledStartup.js] +[test_SocialService.js] +[test_SocialServiceMigration21.js] +[test_SocialServiceMigration22.js] +[test_SocialServiceMigration29.js] diff --git a/browser/modules/test/usageTelemetrySearchSuggestions.sjs b/browser/modules/test/usageTelemetrySearchSuggestions.sjs new file mode 100644 index 000000000..1978b4f66 --- /dev/null +++ b/browser/modules/test/usageTelemetrySearchSuggestions.sjs @@ -0,0 +1,9 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +function handleRequest(req, resp) { + let suffixes = ["foo", "bar"]; + let data = [req.queryString, suffixes.map(s => req.queryString + s)]; + resp.setHeader("Content-Type", "application/json", false); + resp.write(JSON.stringify(data)); +} diff --git a/browser/modules/test/usageTelemetrySearchSuggestions.xml b/browser/modules/test/usageTelemetrySearchSuggestions.xml new file mode 100644 index 000000000..76276045d --- /dev/null +++ b/browser/modules/test/usageTelemetrySearchSuggestions.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/"> +<ShortName>browser_UsageTelemetry usageTelemetrySearchSuggestions.xml</ShortName> +<Url type="application/x-suggestions+json" method="GET" template="http://mochi.test:8888/browser/browser/modules/test/usageTelemetrySearchSuggestions.sjs?{searchTerms}"/> +<Url type="text/html" method="GET" template="http://example.com" rel="searchform"/> +</SearchPlugin> diff --git a/browser/modules/test/xpcshell/.eslintrc.js b/browser/modules/test/xpcshell/.eslintrc.js new file mode 100644 index 000000000..fee088c17 --- /dev/null +++ b/browser/modules/test/xpcshell/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + "extends": [ + "../../../../testing/xpcshell/xpcshell.eslintrc.js" + ] +}; diff --git a/browser/modules/test/xpcshell/test_AttributionCode.js b/browser/modules/test/xpcshell/test_AttributionCode.js new file mode 100644 index 000000000..d979ae845 --- /dev/null +++ b/browser/modules/test/xpcshell/test_AttributionCode.js @@ -0,0 +1,110 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +"use strict"; + +const {interfaces: Ci, utils: Cu} = Components; + +Cu.import("resource://gre/modules/AppConstants.jsm"); +Cu.import("resource:///modules/AttributionCode.jsm"); +Cu.import('resource://gre/modules/osfile.jsm'); +Cu.import("resource://gre/modules/Services.jsm"); + +let validAttrCodes = [ + {code: "source%3Dgoogle.com%26medium%3Dorganic%26campaign%3D(not%20set)%26content%3D(not%20set)", + parsed: {"source": "google.com", "medium": "organic", + "campaign": "(not%20set)", "content": "(not%20set)"}}, + {code: "source%3Dgoogle.com%26medium%3Dorganic%26campaign%3D%26content%3D", + parsed: {"source": "google.com", "medium": "organic"}}, + {code: "source%3Dgoogle.com%26medium%3Dorganic%26campaign%3D(not%20set)", + parsed: {"source": "google.com", "medium": "organic", "campaign": "(not%20set)"}}, + {code: "source%3Dgoogle.com%26medium%3Dorganic", + parsed: {"source": "google.com", "medium": "organic"}}, + {code: "source%3Dgoogle.com", + parsed: {"source": "google.com"}}, + {code: "medium%3Dgoogle.com", + parsed: {"medium": "google.com"}}, + {code: "campaign%3Dgoogle.com", + parsed: {"campaign": "google.com"}}, + {code: "content%3Dgoogle.com", + parsed: {"content": "google.com"}} +]; + +let invalidAttrCodes = [ + // Empty string + "", + // Not escaped + "source=google.com&medium=organic&campaign=(not set)&content=(not set)", + // Too long + "source%3Dreallyreallyreallyreallyreallyreallyreallyreallyreallylongdomain.com%26medium%3Dorganic%26campaign%3D(not%20set)%26content%3Dalmostexactlyenoughcontenttomakethisstringlongerthanthe200characterlimit", + // Unknown key name + "source%3Dgoogle.com%26medium%3Dorganic%26large%3Dgeneticallymodified", + // Empty key name + "source%3Dgoogle.com%26medium%3Dorganic%26%3Dgeneticallymodified" +]; + +function* writeAttributionFile(data) { + let appDir = Services.dirsvc.get("LocalAppData", Ci.nsIFile); + let file = appDir.clone(); + file.append(Services.appinfo.vendor || "mozilla"); + file.append(AppConstants.MOZ_APP_NAME); + + yield OS.File.makeDir(file.path, + {from: appDir.path, ignoreExisting: true}); + + file.append("postSigningData"); + yield OS.File.writeAtomic(file.path, data); +} + +/** + * Test validation of attribution codes, + * to make sure we reject bad ones and accept good ones. + */ +add_task(function* testValidAttrCodes() { + for (let entry of validAttrCodes) { + AttributionCode._clearCache(); + yield writeAttributionFile(entry.code); + let result = yield AttributionCode.getAttrDataAsync(); + Assert.deepEqual(result, entry.parsed, + "Parsed code should match expected value, code was: " + entry.code); + } + AttributionCode._clearCache(); +}); + +/** + * Make sure codes with various formatting errors are not seen as valid. + */ +add_task(function* testInvalidAttrCodes() { + for (let code of invalidAttrCodes) { + AttributionCode._clearCache(); + yield writeAttributionFile(code); + let result = yield AttributionCode.getAttrDataAsync(); + Assert.deepEqual(result, {}, + "Code should have failed to parse: " + code); + } + AttributionCode._clearCache(); +}); + +/** + * Test the cache by deleting the attribution data file + * and making sure we still get the expected code. + */ +add_task(function* testDeletedFile() { + // Set up the test by clearing the cache and writing a valid file. + yield writeAttributionFile(validAttrCodes[0].code); + let result = yield AttributionCode.getAttrDataAsync(); + Assert.deepEqual(result, validAttrCodes[0].parsed, + "The code should be readable directly from the file"); + + // Delete the file and make sure we can still read the value back from cache. + yield AttributionCode.deleteFileAsync(); + result = yield AttributionCode.getAttrDataAsync(); + Assert.deepEqual(result, validAttrCodes[0].parsed, + "The code should be readable from the cache"); + + // Clear the cache and check we can't read anything. + AttributionCode._clearCache(); + result = yield AttributionCode.getAttrDataAsync(); + Assert.deepEqual(result, {}, + "Shouldn't be able to get a code after file is deleted and cache is cleared"); +}); diff --git a/browser/modules/test/xpcshell/test_DirectoryLinksProvider.js b/browser/modules/test/xpcshell/test_DirectoryLinksProvider.js new file mode 100644 index 000000000..712f52fa6 --- /dev/null +++ b/browser/modules/test/xpcshell/test_DirectoryLinksProvider.js @@ -0,0 +1,1854 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +"use strict"; + +/** + * This file tests the DirectoryLinksProvider singleton in the DirectoryLinksProvider.jsm module. + */ + +var { classes: Cc, interfaces: Ci, results: Cr, utils: Cu, Constructor: CC } = Components; +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource:///modules/DirectoryLinksProvider.jsm"); +Cu.import("resource://gre/modules/Promise.jsm"); +Cu.import("resource://gre/modules/Http.jsm"); +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/osfile.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/PlacesUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", + "resource://gre/modules/NetUtil.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils", + "resource://gre/modules/NewTabUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PlacesTestUtils", + "resource://testing-common/PlacesTestUtils.jsm"); + +do_get_profile(); + +const DIRECTORY_LINKS_FILE = "directoryLinks.json"; +const DIRECTORY_FRECENCY = 1000; +const SUGGESTED_FRECENCY = Infinity; +const kURLData = {"directory": [{"url":"http://example.com", "title":"LocalSource"}]}; +const kTestURL = 'data:application/json,' + JSON.stringify(kURLData); + +// DirectoryLinksProvider preferences +const kLocalePref = DirectoryLinksProvider._observedPrefs.prefSelectedLocale; +const kSourceUrlPref = DirectoryLinksProvider._observedPrefs.linksURL; +const kPingUrlPref = "browser.newtabpage.directory.ping"; +const kNewtabEnhancedPref = "browser.newtabpage.enhanced"; + +// httpd settings +var server; +const kDefaultServerPort = 9000; +const kBaseUrl = "http://localhost:" + kDefaultServerPort; +const kExamplePath = "/exampleTest/"; +const kFailPath = "/fail/"; +const kPingPath = "/ping/"; +const kExampleURL = kBaseUrl + kExamplePath; +const kFailURL = kBaseUrl + kFailPath; +const kPingUrl = kBaseUrl + kPingPath; + +// app/profile/firefox.js are not avaialble in xpcshell: hence, preset them +Services.prefs.setCharPref(kLocalePref, "en-US"); +Services.prefs.setCharPref(kSourceUrlPref, kTestURL); +Services.prefs.setCharPref(kPingUrlPref, kPingUrl); +Services.prefs.setBoolPref(kNewtabEnhancedPref, true); + +const kHttpHandlerData = {}; +kHttpHandlerData[kExamplePath] = {"directory": [{"url":"http://example.com", "title":"RemoteSource"}]}; + +const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream"); + +var gLastRequestPath; + +var suggestedTile1 = { + url: "http://turbotax.com", + type: "affiliate", + lastVisitDate: 4, + adgroup_name: "Adgroup1", + frecent_sites: [ + "taxact.com", + "hrblock.com", + "1040.com", + "taxslayer.com" + ] +}; +var suggestedTile2 = { + url: "http://irs.gov", + type: "affiliate", + lastVisitDate: 3, + adgroup_name: "Adgroup2", + frecent_sites: [ + "taxact.com", + "hrblock.com", + "freetaxusa.com", + "taxslayer.com" + ] +}; +var suggestedTile3 = { + url: "http://hrblock.com", + type: "affiliate", + lastVisitDate: 2, + adgroup_name: "Adgroup3", + frecent_sites: [ + "taxact.com", + "freetaxusa.com", + "1040.com", + "taxslayer.com" + ] +}; +var suggestedTile4 = { + url: "http://sponsoredtile.com", + type: "sponsored", + lastVisitDate: 1, + adgroup_name: "Adgroup4", + frecent_sites: [ + "sponsoredtarget.com" + ] +} +var suggestedTile5 = { + url: "http://eviltile.com", + type: "affiliate", + lastVisitDate: 5, + explanation: "This is an evil tile <form><button formaction='javascript:alert(1)''>X</button></form> muhahaha", + adgroup_name: "WE ARE EVIL <link rel='import' href='test.svg'/>", + frecent_sites: [ + "eviltarget.com" + ] +} +var someOtherSite = {url: "http://someothersite.com", title: "Not_A_Suggested_Site"}; + +function getHttpHandler(path) { + let code = 200; + let body = JSON.stringify(kHttpHandlerData[path]); + if (path == kFailPath) { + code = 204; + } + return function(aRequest, aResponse) { + gLastRequestPath = aRequest.path; + aResponse.setStatusLine(null, code); + aResponse.setHeader("Content-Type", "application/json"); + aResponse.write(body); + }; +} + +function isIdentical(actual, expected) { + if (expected == null) { + do_check_eq(actual, expected); + } + else if (typeof expected == "object") { + // Make sure all the keys match up + do_check_eq(Object.keys(actual).sort() + "", Object.keys(expected).sort()); + + // Recursively check each value individually + Object.keys(expected).forEach(key => { + isIdentical(actual[key], expected[key]); + }); + } + else { + do_check_eq(actual, expected); + } +} + +function fetchData() { + let deferred = Promise.defer(); + + DirectoryLinksProvider.getLinks(linkData => { + deferred.resolve(linkData); + }); + return deferred.promise; +} + +function readJsonFile(jsonFile = DIRECTORY_LINKS_FILE) { + let decoder = new TextDecoder(); + let directoryLinksFilePath = OS.Path.join(OS.Constants.Path.localProfileDir, jsonFile); + return OS.File.read(directoryLinksFilePath).then(array => { + let json = decoder.decode(array); + return JSON.parse(json); + }, () => { return "" }); +} + +function cleanJsonFile(jsonFile = DIRECTORY_LINKS_FILE) { + let directoryLinksFilePath = OS.Path.join(OS.Constants.Path.localProfileDir, jsonFile); + return OS.File.remove(directoryLinksFilePath); +} + +function LinksChangeObserver() { + this.deferred = Promise.defer(); + this.onManyLinksChanged = () => this.deferred.resolve(); + this.onDownloadFail = this.onManyLinksChanged; +} + +function promiseDirectoryDownloadOnPrefChange(pref, newValue) { + let oldValue = Services.prefs.getCharPref(pref); + if (oldValue != newValue) { + // if the preference value is already equal to newValue + // the pref service will not call our observer and we + // deadlock. Hence only setup observer if values differ + let observer = new LinksChangeObserver(); + DirectoryLinksProvider.addObserver(observer); + Services.prefs.setCharPref(pref, newValue); + return observer.deferred.promise.then(() => { + DirectoryLinksProvider.removeObserver(observer); + }); + } + return Promise.resolve(); +} + +function promiseSetupDirectoryLinksProvider(options = {}) { + return Task.spawn(function*() { + let linksURL = options.linksURL || kTestURL; + yield DirectoryLinksProvider.init(); + yield promiseDirectoryDownloadOnPrefChange(kLocalePref, options.locale || "en-US"); + yield promiseDirectoryDownloadOnPrefChange(kSourceUrlPref, linksURL); + do_check_eq(DirectoryLinksProvider._linksURL, linksURL); + DirectoryLinksProvider._lastDownloadMS = options.lastDownloadMS || 0; + }); +} + +function promiseCleanDirectoryLinksProvider() { + return Task.spawn(function*() { + yield promiseDirectoryDownloadOnPrefChange(kLocalePref, "en-US"); + yield promiseDirectoryDownloadOnPrefChange(kSourceUrlPref, kTestURL); + yield DirectoryLinksProvider._clearFrequencyCap(); + yield DirectoryLinksProvider._loadInadjacentSites(); + DirectoryLinksProvider._lastDownloadMS = 0; + DirectoryLinksProvider.reset(); + }); +} + +function run_test() { + // Set up a mock HTTP server to serve a directory page + server = new HttpServer(); + server.registerPrefixHandler(kExamplePath, getHttpHandler(kExamplePath)); + server.registerPrefixHandler(kFailPath, getHttpHandler(kFailPath)); + server.start(kDefaultServerPort); + NewTabUtils.init(); + + run_next_test(); + + // Teardown. + do_register_cleanup(function() { + server.stop(function() { }); + DirectoryLinksProvider.reset(); + Services.prefs.clearUserPref(kLocalePref); + Services.prefs.clearUserPref(kSourceUrlPref); + Services.prefs.clearUserPref(kPingUrlPref); + Services.prefs.clearUserPref(kNewtabEnhancedPref); + }); +} + + +function setTimeout(fun, timeout) { + let timer = Components.classes["@mozilla.org/timer;1"] + .createInstance(Components.interfaces.nsITimer); + var event = { + notify: function () { + fun(); + } + }; + timer.initWithCallback(event, timeout, + Components.interfaces.nsITimer.TYPE_ONE_SHOT); + return timer; +} + +add_task(function test_shouldUpdateSuggestedTile() { + let suggestedLink = { + targetedSite: "somesite.com" + }; + + // DirectoryLinksProvider has no suggested tile and no top sites => no need to update + do_check_eq(DirectoryLinksProvider._getCurrentTopSiteCount(), 0); + isIdentical(NewTabUtils.getProviderLinks(), []); + do_check_eq(DirectoryLinksProvider._shouldUpdateSuggestedTile(), false); + + // DirectoryLinksProvider has a suggested tile and no top sites => need to update + let origGetProviderLinks = NewTabUtils.getProviderLinks; + NewTabUtils.getProviderLinks = (provider) => [suggestedLink]; + + do_check_eq(DirectoryLinksProvider._getCurrentTopSiteCount(), 0); + isIdentical(NewTabUtils.getProviderLinks(), [suggestedLink]); + do_check_eq(DirectoryLinksProvider._shouldUpdateSuggestedTile(), true); + + // DirectoryLinksProvider has a suggested tile and 8 top sites => no need to update + let origCurrentTopSiteCount = DirectoryLinksProvider._getCurrentTopSiteCount; + DirectoryLinksProvider._getCurrentTopSiteCount = () => 8; + + do_check_eq(DirectoryLinksProvider._getCurrentTopSiteCount(), 8); + isIdentical(NewTabUtils.getProviderLinks(), [suggestedLink]); + do_check_eq(DirectoryLinksProvider._shouldUpdateSuggestedTile(), false); + + // DirectoryLinksProvider has no suggested tile and 8 top sites => need to update + NewTabUtils.getProviderLinks = origGetProviderLinks; + do_check_eq(DirectoryLinksProvider._getCurrentTopSiteCount(), 8); + isIdentical(NewTabUtils.getProviderLinks(), []); + do_check_eq(DirectoryLinksProvider._shouldUpdateSuggestedTile(), true); + + // Cleanup + DirectoryLinksProvider._getCurrentTopSiteCount = origCurrentTopSiteCount; +}); + +add_task(function* test_updateSuggestedTile() { + let topSites = ["site0.com", "1040.com", "site2.com", "hrblock.com", "site4.com", "freetaxusa.com", "site6.com"]; + + // Initial setup + let data = {"suggested": [suggestedTile1, suggestedTile2, suggestedTile3], "directory": [someOtherSite]}; + let dataURI = 'data:application/json,' + JSON.stringify(data); + + let testObserver = new TestFirstRun(); + DirectoryLinksProvider.addObserver(testObserver); + + yield promiseSetupDirectoryLinksProvider({linksURL: dataURI}); + let links = yield fetchData(); + + let origIsTopPlacesSite = NewTabUtils.isTopPlacesSite; + NewTabUtils.isTopPlacesSite = function(site) { + return topSites.indexOf(site) >= 0; + } + + let origGetProviderLinks = NewTabUtils.getProviderLinks; + NewTabUtils.getProviderLinks = function(provider) { + return links; + } + + let origCurrentTopSiteCount = DirectoryLinksProvider._getCurrentTopSiteCount; + DirectoryLinksProvider._getCurrentTopSiteCount = () => 8; + + do_check_eq(DirectoryLinksProvider._updateSuggestedTile(), undefined); + + function TestFirstRun() { + this.promise = new Promise(resolve => { + this.onLinkChanged = (directoryLinksProvider, link) => { + links.unshift(link); + let possibleLinks = [suggestedTile1.url, suggestedTile2.url, suggestedTile3.url]; + + isIdentical([...DirectoryLinksProvider._topSitesWithSuggestedLinks], ["hrblock.com", "1040.com", "freetaxusa.com"]); + do_check_true(possibleLinks.indexOf(link.url) > -1); + do_check_eq(link.frecency, SUGGESTED_FRECENCY); + do_check_eq(link.type, "affiliate"); + resolve(); + }; + }); + } + + function TestChangingSuggestedTile() { + this.count = 0; + this.promise = new Promise(resolve => { + this.onLinkChanged = (directoryLinksProvider, link) => { + this.count++; + let possibleLinks = [suggestedTile1.url, suggestedTile2.url, suggestedTile3.url]; + + do_check_true(possibleLinks.indexOf(link.url) > -1); + do_check_eq(link.type, "affiliate"); + do_check_true(this.count <= 2); + + if (this.count == 1) { + // The removed suggested link is the one we added initially. + do_check_eq(link.url, links.shift().url); + do_check_eq(link.frecency, SUGGESTED_FRECENCY); + } else { + links.unshift(link); + do_check_eq(link.frecency, SUGGESTED_FRECENCY); + } + isIdentical([...DirectoryLinksProvider._topSitesWithSuggestedLinks], ["hrblock.com", "freetaxusa.com"]); + resolve(); + } + }); + } + + function TestRemovingSuggestedTile() { + this.count = 0; + this.promise = new Promise(resolve => { + this.onLinkChanged = (directoryLinksProvider, link) => { + this.count++; + + do_check_eq(link.type, "affiliate"); + do_check_eq(this.count, 1); + do_check_eq(link.frecency, SUGGESTED_FRECENCY); + do_check_eq(link.url, links.shift().url); + isIdentical([...DirectoryLinksProvider._topSitesWithSuggestedLinks], []); + resolve(); + } + }); + } + + // Test first call to '_updateSuggestedTile()', called when fetching directory links. + yield testObserver.promise; + DirectoryLinksProvider.removeObserver(testObserver); + + // Removing a top site that doesn't have a suggested link should + // not change the current suggested tile. + let removedTopsite = topSites.shift(); + do_check_eq(removedTopsite, "site0.com"); + do_check_false(NewTabUtils.isTopPlacesSite(removedTopsite)); + let updateSuggestedTile = DirectoryLinksProvider._handleLinkChanged({ + url: "http://" + removedTopsite, + type: "history", + }); + do_check_false(updateSuggestedTile); + + // Removing a top site that has a suggested link should + // remove any current suggested tile and add a new one. + testObserver = new TestChangingSuggestedTile(); + DirectoryLinksProvider.addObserver(testObserver); + removedTopsite = topSites.shift(); + do_check_eq(removedTopsite, "1040.com"); + do_check_false(NewTabUtils.isTopPlacesSite(removedTopsite)); + DirectoryLinksProvider.onLinkChanged(DirectoryLinksProvider, { + url: "http://" + removedTopsite, + type: "history", + }); + yield testObserver.promise; + do_check_eq(testObserver.count, 2); + DirectoryLinksProvider.removeObserver(testObserver); + + // Removing all top sites with suggested links should remove + // the current suggested link and not replace it. + topSites = []; + testObserver = new TestRemovingSuggestedTile(); + DirectoryLinksProvider.addObserver(testObserver); + DirectoryLinksProvider.onManyLinksChanged(); + yield testObserver.promise; + + // Cleanup + yield promiseCleanDirectoryLinksProvider(); + NewTabUtils.isTopPlacesSite = origIsTopPlacesSite; + NewTabUtils.getProviderLinks = origGetProviderLinks; + DirectoryLinksProvider._getCurrentTopSiteCount = origCurrentTopSiteCount; +}); + +add_task(function* test_suggestedLinksMap() { + let data = {"suggested": [suggestedTile1, suggestedTile2, suggestedTile3, suggestedTile4], "directory": [someOtherSite]}; + let dataURI = 'data:application/json,' + JSON.stringify(data); + + yield promiseSetupDirectoryLinksProvider({linksURL: dataURI}); + let links = yield fetchData(); + + // Ensure the suggested tiles were not considered directory tiles. + do_check_eq(links.length, 1); + let expected_data = [{url: "http://someothersite.com", title: "Not_A_Suggested_Site", frecency: DIRECTORY_FRECENCY, lastVisitDate: 1}]; + isIdentical(links, expected_data); + + // Check for correctly saved suggested tiles data. + expected_data = { + "taxact.com": [suggestedTile1, suggestedTile2, suggestedTile3], + "hrblock.com": [suggestedTile1, suggestedTile2], + "1040.com": [suggestedTile1, suggestedTile3], + "taxslayer.com": [suggestedTile1, suggestedTile2, suggestedTile3], + "freetaxusa.com": [suggestedTile2, suggestedTile3], + "sponsoredtarget.com": [suggestedTile4], + }; + + let suggestedSites = [...DirectoryLinksProvider._suggestedLinks.keys()]; + do_check_eq(suggestedSites.indexOf("sponsoredtarget.com"), 5); + do_check_eq(suggestedSites.length, Object.keys(expected_data).length); + + DirectoryLinksProvider._suggestedLinks.forEach((suggestedLinks, site) => { + let suggestedLinksItr = suggestedLinks.values(); + for (let link of expected_data[site]) { + let linkCopy = JSON.parse(JSON.stringify(link)); + linkCopy.targetedName = link.adgroup_name; + linkCopy.explanation = ""; + isIdentical(suggestedLinksItr.next().value, linkCopy); + } + }) + + yield promiseCleanDirectoryLinksProvider(); +}); + +add_task(function* test_topSitesWithSuggestedLinks() { + let topSites = ["site0.com", "1040.com", "site2.com", "hrblock.com", "site4.com", "freetaxusa.com", "site6.com"]; + let origIsTopPlacesSite = NewTabUtils.isTopPlacesSite; + NewTabUtils.isTopPlacesSite = function(site) { + return topSites.indexOf(site) >= 0; + } + + // Mock out getProviderLinks() so we don't have to populate cache in NewTabUtils + let origGetProviderLinks = NewTabUtils.getProviderLinks; + NewTabUtils.getProviderLinks = function(provider) { + return []; + } + + // We start off with no top sites with suggested links. + do_check_eq(DirectoryLinksProvider._topSitesWithSuggestedLinks.size, 0); + + let data = {"suggested": [suggestedTile1, suggestedTile2, suggestedTile3], "directory": [someOtherSite]}; + let dataURI = 'data:application/json,' + JSON.stringify(data); + + yield promiseSetupDirectoryLinksProvider({linksURL: dataURI}); + yield fetchData(); + + // Check we've populated suggested links as expected. + do_check_eq(DirectoryLinksProvider._suggestedLinks.size, 5); + + // When many sites change, we update _topSitesWithSuggestedLinks as expected. + let expectedTopSitesWithSuggestedLinks = ["hrblock.com", "1040.com", "freetaxusa.com"]; + DirectoryLinksProvider._handleManyLinksChanged(); + isIdentical([...DirectoryLinksProvider._topSitesWithSuggestedLinks], expectedTopSitesWithSuggestedLinks); + + // Removing site6.com as a topsite has no impact on _topSitesWithSuggestedLinks. + let popped = topSites.pop(); + DirectoryLinksProvider._handleLinkChanged({url: "http://" + popped}); + isIdentical([...DirectoryLinksProvider._topSitesWithSuggestedLinks], expectedTopSitesWithSuggestedLinks); + + // Removing freetaxusa.com as a topsite will remove it from _topSitesWithSuggestedLinks. + popped = topSites.pop(); + expectedTopSitesWithSuggestedLinks.pop(); + DirectoryLinksProvider._handleLinkChanged({url: "http://" + popped}); + isIdentical([...DirectoryLinksProvider._topSitesWithSuggestedLinks], expectedTopSitesWithSuggestedLinks); + + // Re-adding freetaxusa.com as a topsite will add it to _topSitesWithSuggestedLinks. + topSites.push(popped); + expectedTopSitesWithSuggestedLinks.push(popped); + DirectoryLinksProvider._handleLinkChanged({url: "http://" + popped}); + isIdentical([...DirectoryLinksProvider._topSitesWithSuggestedLinks], expectedTopSitesWithSuggestedLinks); + + // Cleanup. + NewTabUtils.isTopPlacesSite = origIsTopPlacesSite; + NewTabUtils.getProviderLinks = origGetProviderLinks; +}); + +add_task(function* test_suggestedAttributes() { + let origIsTopPlacesSite = NewTabUtils.isTopPlacesSite; + NewTabUtils.isTopPlacesSite = () => true; + + let origCurrentTopSiteCount = DirectoryLinksProvider._getCurrentTopSiteCount; + DirectoryLinksProvider._getCurrentTopSiteCount = () => 8; + + let frecent_sites = "addons.mozilla.org,air.mozilla.org,blog.mozilla.org,bugzilla.mozilla.org,developer.mozilla.org,etherpad.mozilla.org,forums.mozillazine.org,hacks.mozilla.org,hg.mozilla.org,mozilla.org,planet.mozilla.org,quality.mozilla.org,support.mozilla.org,treeherder.mozilla.org,wiki.mozilla.org".split(","); + let imageURI = "https://image/"; + let title = "the title"; + let type = "affiliate"; + let url = "http://test.url/"; + let adgroup_name = "Mozilla"; + let data = { + suggested: [{ + frecent_sites, + imageURI, + title, + type, + url, + adgroup_name + }] + }; + let dataURI = "data:application/json," + escape(JSON.stringify(data)); + + yield promiseSetupDirectoryLinksProvider({linksURL: dataURI}); + + // Wait for links to get loaded + let gLinks = NewTabUtils.links; + gLinks.addProvider(DirectoryLinksProvider); + gLinks.populateCache(); + yield new Promise(resolve => { + NewTabUtils.allPages.register({ + observe: _ => _, + update() { + NewTabUtils.allPages.unregister(this); + resolve(); + } + }); + }); + + // Make sure we get the expected attributes on the suggested tile + let link = gLinks.getLinks()[0]; + do_check_eq(link.imageURI, imageURI); + do_check_eq(link.targetedName, "Mozilla"); + do_check_eq(link.targetedSite, frecent_sites[0]); + do_check_eq(link.title, title); + do_check_eq(link.type, type); + do_check_eq(link.url, url); + + // Cleanup. + NewTabUtils.isTopPlacesSite = origIsTopPlacesSite; + DirectoryLinksProvider._getCurrentTopSiteCount = origCurrentTopSiteCount; + gLinks.removeProvider(DirectoryLinksProvider); + DirectoryLinksProvider.removeObserver(gLinks); +}); + +add_task(function* test_frequencyCappedSites_views() { + Services.prefs.setCharPref(kPingUrlPref, ""); + let origIsTopPlacesSite = NewTabUtils.isTopPlacesSite; + NewTabUtils.isTopPlacesSite = () => true; + + let origCurrentTopSiteCount = DirectoryLinksProvider._getCurrentTopSiteCount; + DirectoryLinksProvider._getCurrentTopSiteCount = () => 8; + + let testUrl = "http://frequency.capped/link"; + let targets = ["top.site.com"]; + let data = { + suggested: [{ + type: "affiliate", + frecent_sites: targets, + url: testUrl, + frequency_caps: {daily: 5}, + adgroup_name: "Test" + }], + directory: [{ + type: "organic", + url: "http://directory.site/" + }] + }; + let dataURI = "data:application/json," + JSON.stringify(data); + + yield promiseSetupDirectoryLinksProvider({linksURL: dataURI}); + + // Wait for links to get loaded + let gLinks = NewTabUtils.links; + gLinks.addProvider(DirectoryLinksProvider); + gLinks.populateCache(); + yield new Promise(resolve => { + NewTabUtils.allPages.register({ + observe: _ => _, + update() { + NewTabUtils.allPages.unregister(this); + resolve(); + } + }); + }); + + function synthesizeAction(action) { + DirectoryLinksProvider.reportSitesAction([{ + link: { + targetedSite: targets[0], + url: testUrl + } + }], action, 0); + } + + function checkFirstTypeAndLength(type, length) { + let links = gLinks.getLinks(); + do_check_eq(links[0].type, type); + do_check_eq(links.length, length); + } + + // Make sure we get 5 views of the link before it is removed + checkFirstTypeAndLength("affiliate", 2); + synthesizeAction("view"); + checkFirstTypeAndLength("affiliate", 2); + synthesizeAction("view"); + checkFirstTypeAndLength("affiliate", 2); + synthesizeAction("view"); + checkFirstTypeAndLength("affiliate", 2); + synthesizeAction("view"); + checkFirstTypeAndLength("affiliate", 2); + synthesizeAction("view"); + checkFirstTypeAndLength("organic", 1); + + // Cleanup. + NewTabUtils.isTopPlacesSite = origIsTopPlacesSite; + DirectoryLinksProvider._getCurrentTopSiteCount = origCurrentTopSiteCount; + gLinks.removeProvider(DirectoryLinksProvider); + DirectoryLinksProvider.removeObserver(gLinks); + Services.prefs.setCharPref(kPingUrlPref, kPingUrl); +}); + +add_task(function* test_frequencyCappedSites_click() { + Services.prefs.setCharPref(kPingUrlPref, ""); + let origIsTopPlacesSite = NewTabUtils.isTopPlacesSite; + NewTabUtils.isTopPlacesSite = () => true; + + let origCurrentTopSiteCount = DirectoryLinksProvider._getCurrentTopSiteCount; + DirectoryLinksProvider._getCurrentTopSiteCount = () => 8; + + let testUrl = "http://frequency.capped/link"; + let targets = ["top.site.com"]; + let data = { + suggested: [{ + type: "affiliate", + frecent_sites: targets, + url: testUrl, + adgroup_name: "Test" + }], + directory: [{ + type: "organic", + url: "http://directory.site/" + }] + }; + let dataURI = "data:application/json," + JSON.stringify(data); + + yield promiseSetupDirectoryLinksProvider({linksURL: dataURI}); + + // Wait for links to get loaded + let gLinks = NewTabUtils.links; + gLinks.addProvider(DirectoryLinksProvider); + gLinks.populateCache(); + yield new Promise(resolve => { + NewTabUtils.allPages.register({ + observe: _ => _, + update() { + NewTabUtils.allPages.unregister(this); + resolve(); + } + }); + }); + + function synthesizeAction(action) { + DirectoryLinksProvider.reportSitesAction([{ + link: { + targetedSite: targets[0], + url: testUrl + } + }], action, 0); + } + + function checkFirstTypeAndLength(type, length) { + let links = gLinks.getLinks(); + do_check_eq(links[0].type, type); + do_check_eq(links.length, length); + } + + // Make sure the link disappears after the first click + checkFirstTypeAndLength("affiliate", 2); + synthesizeAction("view"); + checkFirstTypeAndLength("affiliate", 2); + synthesizeAction("click"); + checkFirstTypeAndLength("organic", 1); + + // Cleanup. + NewTabUtils.isTopPlacesSite = origIsTopPlacesSite; + DirectoryLinksProvider._getCurrentTopSiteCount = origCurrentTopSiteCount; + gLinks.removeProvider(DirectoryLinksProvider); + DirectoryLinksProvider.removeObserver(gLinks); + Services.prefs.setCharPref(kPingUrlPref, kPingUrl); +}); + +add_task(function* test_fetchAndCacheLinks_local() { + yield DirectoryLinksProvider.init(); + yield cleanJsonFile(); + // Trigger cache of data or chrome uri files in profD + yield DirectoryLinksProvider._fetchAndCacheLinks(kTestURL); + let data = yield readJsonFile(); + isIdentical(data, kURLData); +}); + +add_task(function* test_fetchAndCacheLinks_remote() { + yield DirectoryLinksProvider.init(); + yield cleanJsonFile(); + // this must trigger directory links json download and save it to cache file + yield promiseDirectoryDownloadOnPrefChange(kSourceUrlPref, kExampleURL + "%LOCALE%"); + do_check_eq(gLastRequestPath, kExamplePath + "en-US"); + let data = yield readJsonFile(); + isIdentical(data, kHttpHandlerData[kExamplePath]); +}); + +add_task(function* test_fetchAndCacheLinks_malformedURI() { + yield DirectoryLinksProvider.init(); + yield cleanJsonFile(); + let someJunk = "some junk"; + try { + yield DirectoryLinksProvider._fetchAndCacheLinks(someJunk); + do_throw("Malformed URIs should fail") + } catch (e) { + do_check_eq(e, "Error fetching " + someJunk) + } + + // File should be empty. + let data = yield readJsonFile(); + isIdentical(data, ""); +}); + +add_task(function* test_fetchAndCacheLinks_unknownHost() { + yield DirectoryLinksProvider.init(); + yield cleanJsonFile(); + let nonExistentServer = "http://localhost:56789/"; + try { + yield DirectoryLinksProvider._fetchAndCacheLinks(nonExistentServer); + do_throw("BAD URIs should fail"); + } catch (e) { + do_check_true(e.startsWith("Fetching " + nonExistentServer + " results in error code: ")) + } + + // File should be empty. + let data = yield readJsonFile(); + isIdentical(data, ""); +}); + +add_task(function* test_fetchAndCacheLinks_non200Status() { + yield DirectoryLinksProvider.init(); + yield cleanJsonFile(); + yield promiseDirectoryDownloadOnPrefChange(kSourceUrlPref, kFailURL); + do_check_eq(gLastRequestPath, kFailPath); + let data = yield readJsonFile(); + isIdentical(data, {}); +}); + +// To test onManyLinksChanged observer, trigger a fetch +add_task(function* test_DirectoryLinksProvider__linkObservers() { + yield DirectoryLinksProvider.init(); + + let testObserver = new LinksChangeObserver(); + DirectoryLinksProvider.addObserver(testObserver); + do_check_eq(DirectoryLinksProvider._observers.size, 1); + DirectoryLinksProvider._fetchAndCacheLinksIfNecessary(true); + + yield testObserver.deferred.promise; + DirectoryLinksProvider._removeObservers(); + do_check_eq(DirectoryLinksProvider._observers.size, 0); + + yield promiseCleanDirectoryLinksProvider(); +}); + +add_task(function* test_DirectoryLinksProvider__prefObserver_url() { + yield promiseSetupDirectoryLinksProvider({linksURL: kTestURL}); + + let links = yield fetchData(); + do_check_eq(links.length, 1); + let expectedData = [{url: "http://example.com", title: "LocalSource", frecency: DIRECTORY_FRECENCY, lastVisitDate: 1}]; + isIdentical(links, expectedData); + + // tests these 2 things: + // 1. _linksURL is properly set after the pref change + // 2. invalid source url is correctly handled + let exampleUrl = 'http://localhost:56789/bad'; + yield promiseDirectoryDownloadOnPrefChange(kSourceUrlPref, exampleUrl); + do_check_eq(DirectoryLinksProvider._linksURL, exampleUrl); + + // since the download fail, the directory file must remain the same + let newLinks = yield fetchData(); + isIdentical(newLinks, expectedData); + + // now remove the file, and re-download + yield cleanJsonFile(); + yield promiseDirectoryDownloadOnPrefChange(kSourceUrlPref, exampleUrl + " "); + // we now should see empty links + newLinks = yield fetchData(); + isIdentical(newLinks, []); + + yield promiseCleanDirectoryLinksProvider(); +}); + +add_task(function* test_DirectoryLinksProvider_getLinks_noDirectoryData() { + let data = { + "directory": [], + }; + let dataURI = 'data:application/json,' + JSON.stringify(data); + yield promiseSetupDirectoryLinksProvider({linksURL: dataURI}); + + let links = yield fetchData(); + do_check_eq(links.length, 0); + yield promiseCleanDirectoryLinksProvider(); +}); + +add_task(function* test_DirectoryLinksProvider_getLinks_badData() { + let data = { + "en-US": { + "en-US": [{url: "http://example.com", title: "US"}], + }, + }; + let dataURI = 'data:application/json,' + JSON.stringify(data); + yield promiseSetupDirectoryLinksProvider({linksURL: dataURI}); + + // Make sure we get nothing for incorrectly formatted data + let links = yield fetchData(); + do_check_eq(links.length, 0); + yield promiseCleanDirectoryLinksProvider(); +}); + +add_task(function test_DirectoryLinksProvider_needsDownload() { + // test timestamping + DirectoryLinksProvider._lastDownloadMS = 0; + do_check_true(DirectoryLinksProvider._needsDownload); + DirectoryLinksProvider._lastDownloadMS = Date.now(); + do_check_false(DirectoryLinksProvider._needsDownload); + DirectoryLinksProvider._lastDownloadMS = Date.now() - (60*60*24 + 1)*1000; + do_check_true(DirectoryLinksProvider._needsDownload); + DirectoryLinksProvider._lastDownloadMS = 0; +}); + +add_task(function* test_DirectoryLinksProvider_fetchAndCacheLinksIfNecessary() { + yield DirectoryLinksProvider.init(); + yield cleanJsonFile(); + // explicitly change source url to cause the download during setup + yield promiseSetupDirectoryLinksProvider({linksURL: kTestURL+" "}); + yield DirectoryLinksProvider._fetchAndCacheLinksIfNecessary(); + + // inspect lastDownloadMS timestamp which should be 5 seconds less then now() + let lastDownloadMS = DirectoryLinksProvider._lastDownloadMS; + do_check_true((Date.now() - lastDownloadMS) < 5000); + + // we should have fetched a new file during setup + let data = yield readJsonFile(); + isIdentical(data, kURLData); + + // attempt to download again - the timestamp should not change + yield DirectoryLinksProvider._fetchAndCacheLinksIfNecessary(); + do_check_eq(DirectoryLinksProvider._lastDownloadMS, lastDownloadMS); + + // clean the file and force the download + yield cleanJsonFile(); + yield DirectoryLinksProvider._fetchAndCacheLinksIfNecessary(true); + data = yield readJsonFile(); + isIdentical(data, kURLData); + + // make sure that failed download does not corrupt the file, nor changes lastDownloadMS + lastDownloadMS = DirectoryLinksProvider._lastDownloadMS; + yield promiseDirectoryDownloadOnPrefChange(kSourceUrlPref, "http://"); + yield DirectoryLinksProvider._fetchAndCacheLinksIfNecessary(true); + data = yield readJsonFile(); + isIdentical(data, kURLData); + do_check_eq(DirectoryLinksProvider._lastDownloadMS, lastDownloadMS); + + // _fetchAndCacheLinksIfNecessary must return same promise if download is in progress + let downloadPromise = DirectoryLinksProvider._fetchAndCacheLinksIfNecessary(true); + let anotherPromise = DirectoryLinksProvider._fetchAndCacheLinksIfNecessary(true); + do_check_true(downloadPromise === anotherPromise); + yield downloadPromise; + + yield promiseCleanDirectoryLinksProvider(); +}); + +add_task(function* test_DirectoryLinksProvider_fetchDirectoryOnPrefChange() { + yield DirectoryLinksProvider.init(); + + let testObserver = new LinksChangeObserver(); + DirectoryLinksProvider.addObserver(testObserver); + + yield cleanJsonFile(); + // ensure that provider does not think it needs to download + do_check_false(DirectoryLinksProvider._needsDownload); + + // change the source URL, which should force directory download + yield promiseDirectoryDownloadOnPrefChange(kSourceUrlPref, kExampleURL); + // then wait for testObserver to fire and test that json is downloaded + yield testObserver.deferred.promise; + do_check_eq(gLastRequestPath, kExamplePath); + let data = yield readJsonFile(); + isIdentical(data, kHttpHandlerData[kExamplePath]); + + yield promiseCleanDirectoryLinksProvider(); +}); + +add_task(function* test_DirectoryLinksProvider_fetchDirectoryOnShow() { + yield promiseSetupDirectoryLinksProvider(); + + // set lastdownload to 0 to make DirectoryLinksProvider want to download + DirectoryLinksProvider._lastDownloadMS = 0; + do_check_true(DirectoryLinksProvider._needsDownload); + + // download should happen on view + yield DirectoryLinksProvider.reportSitesAction([], "view"); + do_check_true(DirectoryLinksProvider._lastDownloadMS != 0); + + yield promiseCleanDirectoryLinksProvider(); +}); + +add_task(function* test_DirectoryLinksProvider_fetchDirectoryOnInit() { + // ensure preferences are set to defaults + yield promiseSetupDirectoryLinksProvider(); + // now clean to provider, so we can init it again + yield promiseCleanDirectoryLinksProvider(); + + yield cleanJsonFile(); + yield DirectoryLinksProvider.init(); + let data = yield readJsonFile(); + isIdentical(data, kURLData); + + yield promiseCleanDirectoryLinksProvider(); +}); + +add_task(function* test_DirectoryLinksProvider_getLinksFromCorruptedFile() { + yield promiseSetupDirectoryLinksProvider(); + + // write bogus json to a file and attempt to fetch from it + let directoryLinksFilePath = OS.Path.join(OS.Constants.Path.profileDir, DIRECTORY_LINKS_FILE); + yield OS.File.writeAtomic(directoryLinksFilePath, '{"en-US":'); + let data = yield fetchData(); + isIdentical(data, []); + + yield promiseCleanDirectoryLinksProvider(); +}); + +add_task(function* test_DirectoryLinksProvider_getAllowedLinks() { + let data = {"directory": [ + {url: "ftp://example.com"}, + {url: "http://example.net"}, + {url: "javascript:5"}, + {url: "https://example.com"}, + {url: "httpJUNKjavascript:42"}, + {url: "data:text/plain,hi"}, + {url: "http/bork:eh"}, + ]}; + let dataURI = 'data:application/json,' + JSON.stringify(data); + yield promiseSetupDirectoryLinksProvider({linksURL: dataURI}); + + let links = yield fetchData(); + do_check_eq(links.length, 2); + + // The only remaining url should be http and https + do_check_eq(links[0].url, data["directory"][1].url); + do_check_eq(links[1].url, data["directory"][3].url); +}); + +add_task(function* test_DirectoryLinksProvider_getAllowedImages() { + let data = {"directory": [ + {url: "http://example.com", imageURI: "ftp://example.com"}, + {url: "http://example.com", imageURI: "http://example.net"}, + {url: "http://example.com", imageURI: "javascript:5"}, + {url: "http://example.com", imageURI: "https://example.com"}, + {url: "http://example.com", imageURI: "httpJUNKjavascript:42"}, + {url: "http://example.com", imageURI: "data:text/plain,hi"}, + {url: "http://example.com", imageURI: "http/bork:eh"}, + ]}; + let dataURI = 'data:application/json,' + JSON.stringify(data); + yield promiseSetupDirectoryLinksProvider({linksURL: dataURI}); + + let links = yield fetchData(); + do_check_eq(links.length, 2); + + // The only remaining images should be https and data + do_check_eq(links[0].imageURI, data["directory"][3].imageURI); + do_check_eq(links[1].imageURI, data["directory"][5].imageURI); +}); + +add_task(function* test_DirectoryLinksProvider_getAllowedImages_base() { + let data = {"directory": [ + {url: "http://example1.com", imageURI: "https://example.com"}, + {url: "http://example2.com", imageURI: "https://tiles.cdn.mozilla.net"}, + {url: "http://example3.com", imageURI: "https://tiles2.cdn.mozilla.net"}, + {url: "http://example4.com", enhancedImageURI: "https://mozilla.net"}, + {url: "http://example5.com", imageURI: "data:text/plain,hi"}, + ]}; + let dataURI = 'data:application/json,' + JSON.stringify(data); + yield promiseSetupDirectoryLinksProvider({linksURL: dataURI}); + + // Pretend we're using the default pref to trigger base matching + DirectoryLinksProvider.__linksURLModified = false; + + let links = yield fetchData(); + do_check_eq(links.length, 4); + + // The only remaining images should be https with mozilla.net or data URI + do_check_eq(links[0].url, data["directory"][1].url); + do_check_eq(links[1].url, data["directory"][2].url); + do_check_eq(links[2].url, data["directory"][3].url); + do_check_eq(links[3].url, data["directory"][4].url); +}); + +add_task(function* test_DirectoryLinksProvider_getAllowedEnhancedImages() { + let data = {"directory": [ + {url: "http://example.com", enhancedImageURI: "ftp://example.com"}, + {url: "http://example.com", enhancedImageURI: "http://example.net"}, + {url: "http://example.com", enhancedImageURI: "javascript:5"}, + {url: "http://example.com", enhancedImageURI: "https://example.com"}, + {url: "http://example.com", enhancedImageURI: "httpJUNKjavascript:42"}, + {url: "http://example.com", enhancedImageURI: "data:text/plain,hi"}, + {url: "http://example.com", enhancedImageURI: "http/bork:eh"}, + ]}; + let dataURI = 'data:application/json,' + JSON.stringify(data); + yield promiseSetupDirectoryLinksProvider({linksURL: dataURI}); + + let links = yield fetchData(); + do_check_eq(links.length, 2); + + // The only remaining enhancedImages should be http and https and data + do_check_eq(links[0].enhancedImageURI, data["directory"][3].enhancedImageURI); + do_check_eq(links[1].enhancedImageURI, data["directory"][5].enhancedImageURI); +}); + +add_task(function* test_DirectoryLinksProvider_getEnhancedLink() { + let data = {"enhanced": [ + {url: "http://example.net", enhancedImageURI: "data:,net1"}, + {url: "http://example.com", enhancedImageURI: "data:,com1"}, + {url: "http://example.com", enhancedImageURI: "data:,com2"}, + ]}; + let dataURI = 'data:application/json,' + JSON.stringify(data); + yield promiseSetupDirectoryLinksProvider({linksURL: dataURI}); + + let links = yield fetchData(); + do_check_eq(links.length, 0); // There are no directory links. + + function checkEnhanced(url, image) { + let enhanced = DirectoryLinksProvider.getEnhancedLink({url: url}); + do_check_eq(enhanced && enhanced.enhancedImageURI, image); + } + + // Get the expected image for the same site + checkEnhanced("http://example.net/", "data:,net1"); + checkEnhanced("http://example.net/path", "data:,net1"); + checkEnhanced("https://www.example.net/", "data:,net1"); + checkEnhanced("https://www3.example.net/", "data:,net1"); + + // Get the image of the last entry + checkEnhanced("http://example.com", "data:,com2"); + + // Get the inline enhanced image + let inline = DirectoryLinksProvider.getEnhancedLink({ + url: "http://example.com/echo", + enhancedImageURI: "data:,echo", + }); + do_check_eq(inline.enhancedImageURI, "data:,echo"); + do_check_eq(inline.url, "http://example.com/echo"); + + // Undefined for not enhanced + checkEnhanced("http://sub.example.net/", undefined); + checkEnhanced("http://example.org", undefined); + checkEnhanced("http://localhost", undefined); + checkEnhanced("http://127.0.0.1", undefined); + + // Make sure old data is not cached + data = {"enhanced": [ + {url: "http://example.com", enhancedImageURI: "data:,fresh"}, + ]}; + dataURI = 'data:application/json,' + JSON.stringify(data); + yield promiseSetupDirectoryLinksProvider({linksURL: dataURI}); + links = yield fetchData(); + do_check_eq(links.length, 0); // There are no directory links. + checkEnhanced("http://example.net", undefined); + checkEnhanced("http://example.com", "data:,fresh"); +}); + +add_task(function* test_DirectoryLinksProvider_enhancedURIs() { + let origIsTopPlacesSite = NewTabUtils.isTopPlacesSite; + NewTabUtils.isTopPlacesSite = () => true; + let origCurrentTopSiteCount = DirectoryLinksProvider._getCurrentTopSiteCount; + DirectoryLinksProvider._getCurrentTopSiteCount = () => 8; + + let data = { + "suggested": [ + {url: "http://example.net", enhancedImageURI: "data:,net1", title:"SuggestedTitle", adgroup_name: "Test", frecent_sites: ["test.com"]} + ], + "directory": [ + {url: "http://example.net", enhancedImageURI: "data:,net2", title:"DirectoryTitle"} + ] + }; + let dataURI = 'data:application/json,' + JSON.stringify(data); + yield promiseSetupDirectoryLinksProvider({linksURL: dataURI}); + + // Wait for links to get loaded + let gLinks = NewTabUtils.links; + gLinks.addProvider(DirectoryLinksProvider); + gLinks.populateCache(); + yield new Promise(resolve => { + NewTabUtils.allPages.register({ + observe: _ => _, + update() { + NewTabUtils.allPages.unregister(this); + resolve(); + } + }); + }); + + // Check that we've saved the directory tile. + let links = yield fetchData(); + do_check_eq(links.length, 1); + do_check_eq(links[0].title, "DirectoryTitle"); + do_check_eq(links[0].enhancedImageURI, "data:,net2"); + + // Check that the suggested tile with the same URL replaces the directory tile. + links = gLinks.getLinks(); + do_check_eq(links.length, 1); + do_check_eq(links[0].title, "SuggestedTitle"); + do_check_eq(links[0].enhancedImageURI, "data:,net1"); + + // Cleanup. + NewTabUtils.isTopPlacesSite = origIsTopPlacesSite; + DirectoryLinksProvider._getCurrentTopSiteCount = origCurrentTopSiteCount; + gLinks.removeProvider(DirectoryLinksProvider); +}); + +add_task(function test_DirectoryLinksProvider_setDefaultEnhanced() { + function checkDefault(expected) { + Services.prefs.clearUserPref(kNewtabEnhancedPref); + do_check_eq(Services.prefs.getBoolPref(kNewtabEnhancedPref), expected); + } + + // Use the default donottrack prefs (enabled = false) + Services.prefs.clearUserPref("privacy.donottrackheader.enabled"); + checkDefault(true); + + // Turn on DNT - no track + Services.prefs.setBoolPref("privacy.donottrackheader.enabled", true); + checkDefault(false); + + // Turn off DNT header + Services.prefs.clearUserPref("privacy.donottrackheader.enabled"); + checkDefault(true); + + // Clean up + Services.prefs.clearUserPref("privacy.donottrackheader.value"); +}); + +add_task(function* test_timeSensetiveSuggestedTiles() { + // make tile json with start and end dates + let testStartTime = Date.now(); + // start date is now + 1 seconds + let startDate = new Date(testStartTime + 1000); + // end date is now + 3 seconds + let endDate = new Date(testStartTime + 3000); + let suggestedTile = Object.assign({ + time_limits: { + start: startDate.toISOString(), + end: endDate.toISOString(), + } + }, suggestedTile1); + + // Initial setup + let topSites = ["site0.com", "1040.com", "site2.com", "hrblock.com", "site4.com", "freetaxusa.com", "site6.com"]; + let data = {"suggested": [suggestedTile], "directory": [someOtherSite]}; + let dataURI = 'data:application/json,' + JSON.stringify(data); + + let testObserver = new TestTimingRun(); + DirectoryLinksProvider.addObserver(testObserver); + + yield promiseSetupDirectoryLinksProvider({linksURL: dataURI}); + let links = yield fetchData(); + + let origIsTopPlacesSite = NewTabUtils.isTopPlacesSite; + NewTabUtils.isTopPlacesSite = function(site) { + return topSites.indexOf(site) >= 0; + } + + let origGetProviderLinks = NewTabUtils.getProviderLinks; + NewTabUtils.getProviderLinks = function(provider) { + return links; + } + + let origCurrentTopSiteCount = DirectoryLinksProvider._getCurrentTopSiteCount; + DirectoryLinksProvider._getCurrentTopSiteCount = () => 8; + + do_check_eq(DirectoryLinksProvider._updateSuggestedTile(), undefined); + + // this tester will fire twice: when start limit is reached and when tile link + // is removed upon end of the campaign, in which case deleteFlag will be set + function TestTimingRun() { + this.promise = new Promise(resolve => { + this.onLinkChanged = (directoryLinksProvider, link, ignoreFlag, deleteFlag) => { + // if we are not deleting, add link to links, so we can catch it's removal + if (!deleteFlag) { + links.unshift(link); + } + + isIdentical([...DirectoryLinksProvider._topSitesWithSuggestedLinks], ["hrblock.com", "1040.com"]); + do_check_eq(link.frecency, SUGGESTED_FRECENCY); + do_check_eq(link.type, "affiliate"); + do_check_eq(link.url, suggestedTile.url); + let timeDelta = Date.now() - testStartTime; + if (!deleteFlag) { + // this is start timeout corresponding to campaign start + // a seconds must pass and targetedSite must be set + do_print("TESTING START timeDelta: " + timeDelta); + do_check_true(timeDelta >= 1000 / 2); // check for at least half time + do_check_eq(link.targetedSite, "hrblock.com"); + do_check_true(DirectoryLinksProvider._campaignTimeoutID); + } + else { + // this is the campaign end timeout, so 3 seconds must pass + // and timeout should be cleared + do_print("TESTING END timeDelta: " + timeDelta); + do_check_true(timeDelta >= 3000 / 2); // check for at least half time + do_check_false(link.targetedSite); + do_check_false(DirectoryLinksProvider._campaignTimeoutID); + resolve(); + } + }; + }); + } + + // _updateSuggestedTile() is called when fetching directory links. + yield testObserver.promise; + DirectoryLinksProvider.removeObserver(testObserver); + + // shoudl suggest nothing + do_check_eq(DirectoryLinksProvider._updateSuggestedTile(), undefined); + + // set links back to contain directory tile only + links.shift(); + + // drop the end time - we should pick up the tile + suggestedTile.time_limits.end = null; + data = {"suggested": [suggestedTile], "directory": [someOtherSite]}; + dataURI = 'data:application/json,' + JSON.stringify(data); + + // redownload json and getLinks to force time recomputation + yield promiseDirectoryDownloadOnPrefChange(kSourceUrlPref, dataURI); + + // ensure that there's a link returned by _updateSuggestedTile and no timeout + let deferred = Promise.defer(); + DirectoryLinksProvider.getLinks(() => { + let link = DirectoryLinksProvider._updateSuggestedTile(); + // we should have a suggested tile and no timeout + do_check_eq(link.type, "affiliate"); + do_check_eq(link.url, suggestedTile.url); + do_check_false(DirectoryLinksProvider._campaignTimeoutID); + deferred.resolve(); + }); + yield deferred.promise; + + // repeat the test for end time only + suggestedTile.time_limits.start = null; + suggestedTile.time_limits.end = (new Date(Date.now() + 3000)).toISOString(); + + data = {"suggested": [suggestedTile], "directory": [someOtherSite]}; + dataURI = 'data:application/json,' + JSON.stringify(data); + + // redownload json and call getLinks() to force time recomputation + yield promiseDirectoryDownloadOnPrefChange(kSourceUrlPref, dataURI); + + // ensure that there's a link returned by _updateSuggestedTile and timeout set + deferred = Promise.defer(); + DirectoryLinksProvider.getLinks(() => { + let link = DirectoryLinksProvider._updateSuggestedTile(); + // we should have a suggested tile and timeout set + do_check_eq(link.type, "affiliate"); + do_check_eq(link.url, suggestedTile.url); + do_check_true(DirectoryLinksProvider._campaignTimeoutID); + DirectoryLinksProvider._clearCampaignTimeout(); + deferred.resolve(); + }); + yield deferred.promise; + + // Cleanup + yield promiseCleanDirectoryLinksProvider(); + NewTabUtils.isTopPlacesSite = origIsTopPlacesSite; + NewTabUtils.getProviderLinks = origGetProviderLinks; + DirectoryLinksProvider._getCurrentTopSiteCount = origCurrentTopSiteCount; +}); + +add_task(function test_setupStartEndTime() { + let currentTime = Date.now(); + let dt = new Date(currentTime); + let link = { + time_limits: { + start: dt.toISOString() + } + }; + + // test ISO translation + DirectoryLinksProvider._setupStartEndTime(link); + do_check_eq(link.startTime, currentTime); + + // test localtime translation + let shiftedDate = new Date(currentTime - dt.getTimezoneOffset()*60*1000); + link.time_limits.start = shiftedDate.toISOString().replace(/Z$/, ""); + + DirectoryLinksProvider._setupStartEndTime(link); + do_check_eq(link.startTime, currentTime); + + // throw some garbage into date string + delete link.startTime; + link.time_limits.start = "no date" + DirectoryLinksProvider._setupStartEndTime(link); + do_check_false(link.startTime); + + link.time_limits.start = "2015-99999-01T00:00:00" + DirectoryLinksProvider._setupStartEndTime(link); + do_check_false(link.startTime); + + link.time_limits.start = "20150501T00:00:00" + DirectoryLinksProvider._setupStartEndTime(link); + do_check_false(link.startTime); +}); + +add_task(function* test_DirectoryLinksProvider_frequencyCapSetup() { + yield promiseSetupDirectoryLinksProvider(); + yield DirectoryLinksProvider.init(); + + yield promiseCleanDirectoryLinksProvider(); + yield DirectoryLinksProvider._readFrequencyCapFile(); + isIdentical(DirectoryLinksProvider._frequencyCaps, {}); + + // setup few links + DirectoryLinksProvider._updateFrequencyCapSettings({ + url: "1", + }); + DirectoryLinksProvider._updateFrequencyCapSettings({ + url: "2", + frequency_caps: {daily: 1, total: 2} + }); + DirectoryLinksProvider._updateFrequencyCapSettings({ + url: "3", + frequency_caps: {total: 2} + }); + DirectoryLinksProvider._updateFrequencyCapSettings({ + url: "4", + frequency_caps: {daily: 1} + }); + let freqCapsObject = DirectoryLinksProvider._frequencyCaps; + let capObject = freqCapsObject["1"]; + let defaultDaily = capObject.dailyCap; + let defaultTotal = capObject.totalCap; + // check if we have defaults set + do_check_true(capObject.dailyCap > 0); + do_check_true(capObject.totalCap > 0); + // check if defaults are properly handled + do_check_eq(freqCapsObject["2"].dailyCap, 1); + do_check_eq(freqCapsObject["2"].totalCap, 2); + do_check_eq(freqCapsObject["3"].dailyCap, defaultDaily); + do_check_eq(freqCapsObject["3"].totalCap, 2); + do_check_eq(freqCapsObject["4"].dailyCap, 1); + do_check_eq(freqCapsObject["4"].totalCap, defaultTotal); + + // write object to file + yield DirectoryLinksProvider._writeFrequencyCapFile(); + // empty out freqCapsObject and read file back + DirectoryLinksProvider._frequencyCaps = {}; + yield DirectoryLinksProvider._readFrequencyCapFile(); + // re-ran tests - they should all pass + do_check_eq(freqCapsObject["2"].dailyCap, 1); + do_check_eq(freqCapsObject["2"].totalCap, 2); + do_check_eq(freqCapsObject["3"].dailyCap, defaultDaily); + do_check_eq(freqCapsObject["3"].totalCap, 2); + do_check_eq(freqCapsObject["4"].dailyCap, 1); + do_check_eq(freqCapsObject["4"].totalCap, defaultTotal); + + // wait a second and prune frequency caps + yield new Promise(resolve => { + setTimeout(resolve, 1100); + }); + + // update one link and create another + DirectoryLinksProvider._updateFrequencyCapSettings({ + url: "3", + frequency_caps: {daily: 1, total: 2} + }); + DirectoryLinksProvider._updateFrequencyCapSettings({ + url: "7", + frequency_caps: {daily: 1, total: 2} + }); + // now prune the ones that have been in the object longer than 1 second + DirectoryLinksProvider._pruneFrequencyCapUrls(1000); + // make sure all keys but "3" and "7" are deleted + Object.keys(DirectoryLinksProvider._frequencyCaps).forEach(key => { + do_check_true(key == "3" || key == "7"); + }); + + yield promiseCleanDirectoryLinksProvider(); +}); + +add_task(function* test_DirectoryLinksProvider_getFrequencyCapLogic() { + yield promiseSetupDirectoryLinksProvider(); + yield DirectoryLinksProvider.init(); + + // setup suggested links + DirectoryLinksProvider._updateFrequencyCapSettings({ + url: "1", + frequency_caps: {daily: 2, total: 4} + }); + + do_check_true(DirectoryLinksProvider._testFrequencyCapLimits("1")); + // exhaust daily views + DirectoryLinksProvider._addFrequencyCapView("1") + do_check_true(DirectoryLinksProvider._testFrequencyCapLimits("1")); + DirectoryLinksProvider._addFrequencyCapView("1") + do_check_false(DirectoryLinksProvider._testFrequencyCapLimits("1")); + + // now step into the furture + let _wasTodayOrig = DirectoryLinksProvider._wasToday; + DirectoryLinksProvider._wasToday = function () { return false; } + // exhaust total views + DirectoryLinksProvider._addFrequencyCapView("1") + do_check_true(DirectoryLinksProvider._testFrequencyCapLimits("1")); + DirectoryLinksProvider._addFrequencyCapView("1") + // reached totalViews 4, should return false + do_check_false(DirectoryLinksProvider._testFrequencyCapLimits("1")); + + // add more views by updating configuration + DirectoryLinksProvider._updateFrequencyCapSettings({ + url: "1", + frequency_caps: {daily: 5, total: 10} + }); + // should be true, since we have more total views + do_check_true(DirectoryLinksProvider._testFrequencyCapLimits("1")); + + // set click flag + DirectoryLinksProvider._setFrequencyCapClick("1"); + // always false after click + do_check_false(DirectoryLinksProvider._testFrequencyCapLimits("1")); + + // use unknown urls and ensure nothing breaks + DirectoryLinksProvider._addFrequencyCapView("nosuch.url"); + DirectoryLinksProvider._setFrequencyCapClick("nosuch.url"); + // testing unknown url should always return false + do_check_false(DirectoryLinksProvider._testFrequencyCapLimits("nosuch.url")); + + // reset _wasToday back to original function + DirectoryLinksProvider._wasToday = _wasTodayOrig; + yield promiseCleanDirectoryLinksProvider(); +}); + +add_task(function* test_DirectoryLinksProvider_getFrequencyCapReportSiteAction() { + yield promiseSetupDirectoryLinksProvider(); + yield DirectoryLinksProvider.init(); + + // setup suggested links + DirectoryLinksProvider._updateFrequencyCapSettings({ + url: "bar.com", + frequency_caps: {daily: 2, total: 4} + }); + + do_check_true(DirectoryLinksProvider._testFrequencyCapLimits("bar.com")); + // report site action + yield DirectoryLinksProvider.reportSitesAction([{ + link: { + targetedSite: "foo.com", + url: "bar.com" + }, + isPinned: function() { return false; }, + }], "view", 0); + + // read file content and ensure that view counters are updated + let data = yield readJsonFile(DirectoryLinksProvider._frequencyCapFilePath); + do_check_eq(data["bar.com"].dailyViews, 1); + do_check_eq(data["bar.com"].totalViews, 1); + + yield promiseCleanDirectoryLinksProvider(); +}); + +add_task(function* test_DirectoryLinksProvider_ClickRemoval() { + yield promiseSetupDirectoryLinksProvider(); + yield DirectoryLinksProvider.init(); + let landingUrl = "http://foo.com"; + + // setup suggested links + DirectoryLinksProvider._updateFrequencyCapSettings({ + url: landingUrl, + frequency_caps: {daily: 2, total: 4} + }); + + // add views + DirectoryLinksProvider._addFrequencyCapView(landingUrl) + DirectoryLinksProvider._addFrequencyCapView(landingUrl) + // make a click + DirectoryLinksProvider._setFrequencyCapClick(landingUrl); + + // views must be 2 and click must be set + do_check_eq(DirectoryLinksProvider._frequencyCaps[landingUrl].totalViews, 2); + do_check_true(DirectoryLinksProvider._frequencyCaps[landingUrl].clicked); + + // now insert a visit into places + yield new Promise(resolve => { + PlacesUtils.asyncHistory.updatePlaces( + { + uri: NetUtil.newURI(landingUrl), + title: "HELLO", + visits: [{ + visitDate: Date.now()*1000, + transitionType: Ci.nsINavHistoryService.TRANSITION_LINK + }] + }, + { + handleError: function () { do_check_true(false); }, + handleResult: function () {}, + handleCompletion: function () { resolve(); } + } + ); + }); + + function UrlDeletionTester() { + this.promise = new Promise(resolve => { + this.onDeleteURI = (directoryLinksProvider, link) => { + resolve(); + }; + this.onClearHistory = (directoryLinksProvider) => { + resolve(); + }; + }); + } + + let testObserver = new UrlDeletionTester(); + DirectoryLinksProvider.addObserver(testObserver); + + PlacesUtils.bhistory.removePage(NetUtil.newURI(landingUrl)); + yield testObserver.promise; + DirectoryLinksProvider.removeObserver(testObserver); + // views must be 2 and click should not exist + do_check_eq(DirectoryLinksProvider._frequencyCaps[landingUrl].totalViews, 2); + do_check_false(DirectoryLinksProvider._frequencyCaps[landingUrl].hasOwnProperty("clicked")); + + // verify that disk written data is kosher + let data = yield readJsonFile(DirectoryLinksProvider._frequencyCapFilePath); + do_check_eq(data[landingUrl].totalViews, 2); + do_check_false(data[landingUrl].hasOwnProperty("clicked")); + + // now test clear history + DirectoryLinksProvider._updateFrequencyCapSettings({ + url: landingUrl, + frequency_caps: {daily: 2, total: 4} + }); + DirectoryLinksProvider._updateFrequencyCapSettings({ + url: "http://bar.com", + frequency_caps: {daily: 2, total: 4} + }); + + DirectoryLinksProvider._setFrequencyCapClick(landingUrl); + DirectoryLinksProvider._setFrequencyCapClick("http://bar.com"); + // both tiles must have clicked + do_check_true(DirectoryLinksProvider._frequencyCaps[landingUrl].clicked); + do_check_true(DirectoryLinksProvider._frequencyCaps["http://bar.com"].clicked); + + testObserver = new UrlDeletionTester(); + DirectoryLinksProvider.addObserver(testObserver); + yield PlacesTestUtils.clearHistory(); + + yield testObserver.promise; + DirectoryLinksProvider.removeObserver(testObserver); + // no clicks should remain in the cap object + do_check_false(DirectoryLinksProvider._frequencyCaps[landingUrl].hasOwnProperty("clicked")); + do_check_false(DirectoryLinksProvider._frequencyCaps["http://bar.com"].hasOwnProperty("clicked")); + + // verify that disk written data is kosher + data = yield readJsonFile(DirectoryLinksProvider._frequencyCapFilePath); + do_check_false(data[landingUrl].hasOwnProperty("clicked")); + do_check_false(data["http://bar.com"].hasOwnProperty("clicked")); + + yield promiseCleanDirectoryLinksProvider(); +}); + +add_task(function test_DirectoryLinksProvider_anonymous() { + do_check_true(DirectoryLinksProvider._newXHR().mozAnon); +}); + +add_task(function* test_sanitizeExplanation() { + // Note: this is a basic test to ensure we applied sanitization to the link explanation. + // Full testing for appropriate sanitization is done in parser/xml/test/unit/test_sanitizer.js. + let data = {"suggested": [suggestedTile5]}; + let dataURI = 'data:application/json,' + encodeURIComponent(JSON.stringify(data)); + + yield promiseSetupDirectoryLinksProvider({linksURL: dataURI}); + yield fetchData(); + + let suggestedSites = [...DirectoryLinksProvider._suggestedLinks.keys()]; + do_check_eq(suggestedSites.indexOf("eviltarget.com"), 0); + do_check_eq(suggestedSites.length, 1); + + let suggestedLink = [...DirectoryLinksProvider._suggestedLinks.get(suggestedSites[0]).values()][0]; + do_check_eq(suggestedLink.explanation, "This is an evil tile X muhahaha"); + do_check_eq(suggestedLink.targetedName, "WE ARE EVIL "); +}); + +add_task(function* test_inadjecentSites() { + let suggestedTile = Object.assign({ + check_inadjacency: true + }, suggestedTile1); + + // Initial setup + let topSites = ["1040.com", "site2.com", "hrblock.com", "site4.com", "freetaxusa.com", "site6.com"]; + let data = {"suggested": [suggestedTile], "directory": [someOtherSite]}; + let dataURI = 'data:application/json,' + JSON.stringify(data); + + let testObserver = new TestFirstRun(); + DirectoryLinksProvider.addObserver(testObserver); + + yield promiseSetupDirectoryLinksProvider({linksURL: dataURI}); + let links = yield fetchData(); + + let origIsTopPlacesSite = NewTabUtils.isTopPlacesSite; + NewTabUtils.isTopPlacesSite = function(site) { + return topSites.indexOf(site) >= 0; + } + + let origGetProviderLinks = NewTabUtils.getProviderLinks; + NewTabUtils.getProviderLinks = function(provider) { + return links; + } + + let origCurrentTopSiteCount = DirectoryLinksProvider._getCurrentTopSiteCount; + DirectoryLinksProvider._getCurrentTopSiteCount = () => { + origCurrentTopSiteCount.apply(DirectoryLinksProvider); + return 8; + }; + + // store oroginal inadjacent sites url + let origInadjacentSitesUrl = DirectoryLinksProvider._inadjacentSitesUrl; + + // loading inadjacent sites list function + function setInadjacentSites(sites) { + let badSiteB64 = []; + sites.forEach(site => { + badSiteB64.push(DirectoryLinksProvider._generateHash(site)); + }); + let theList = {"domains": badSiteB64}; + let uri = 'data:application/json,' + JSON.stringify(theList); + DirectoryLinksProvider._inadjacentSitesUrl = uri; + return DirectoryLinksProvider._loadInadjacentSites(); + } + + // setup gLinks loader + let gLinks = NewTabUtils.links; + gLinks.addProvider(DirectoryLinksProvider); + + function updateNewTabCache() { + gLinks.populateCache(); + return new Promise(resolve => { + NewTabUtils.allPages.register({ + observe: _ => _, + update() { + NewTabUtils.allPages.unregister(this); + resolve(); + } + }); + }); + } + + // no suggested file + do_check_eq(DirectoryLinksProvider._updateSuggestedTile(), undefined); + // _avoidInadjacentSites should be set, since link.check_inadjacency is on + do_check_true(DirectoryLinksProvider._avoidInadjacentSites); + // make sure example.com is included in inadjacent sites list + do_check_true(DirectoryLinksProvider._isInadjacentLink({baseDomain: "example.com"})); + + function TestFirstRun() { + this.promise = new Promise(resolve => { + this.onLinkChanged = (directoryLinksProvider, link) => { + do_check_eq(link.url, suggestedTile.url); + do_check_eq(link.type, "affiliate"); + resolve(); + }; + }); + } + + // Test first call to '_updateSuggestedTile()', called when fetching directory links. + yield testObserver.promise; + DirectoryLinksProvider.removeObserver(testObserver); + + // update newtab cache + yield updateNewTabCache(); + // this should have set + do_check_true(DirectoryLinksProvider._avoidInadjacentSites); + + // there should be siggested link + let link = DirectoryLinksProvider._updateSuggestedTile(); + do_check_eq(link.url, "http://turbotax.com"); + // and it should have avoidInadjacentSites flag + do_check_true(link.check_inadjacency); + + // make someothersite.com inadjacent + yield setInadjacentSites(["someothersite.com"]); + + // there should be no suggested link + link = DirectoryLinksProvider._updateSuggestedTile(); + do_check_false(link); + do_check_true(DirectoryLinksProvider._newTabHasInadjacentSite); + + // _handleLinkChanged must return true on inadjacent site + do_check_true(DirectoryLinksProvider._handleLinkChanged({ + url: "http://someothersite.com", + type: "history", + })); + // _handleLinkChanged must return false on ok site + do_check_false(DirectoryLinksProvider._handleLinkChanged({ + url: "http://foobar.com", + type: "history", + })); + + // change inadjacent list to sites not on newtab page + yield setInadjacentSites(["foo.com", "bar.com"]); + + link = DirectoryLinksProvider._updateSuggestedTile(); + // we should now have a link + do_check_true(link); + do_check_eq(link.url, "http://turbotax.com"); + + // make newtab offending again + yield setInadjacentSites(["someothersite.com", "foo.com"]); + // there should be no suggested link + link = DirectoryLinksProvider._updateSuggestedTile(); + do_check_false(link); + do_check_true(DirectoryLinksProvider._newTabHasInadjacentSite); + + // remove avoidInadjacentSites flag from suggested tile and reload json + delete suggestedTile.check_inadjacency; + data = {"suggested": [suggestedTile], "directory": [someOtherSite]}; + dataURI = 'data:application/json,' + JSON.stringify(data); + yield promiseDirectoryDownloadOnPrefChange(kSourceUrlPref, dataURI); + yield fetchData(); + + // inadjacent checking should be disabled + do_check_false(DirectoryLinksProvider._avoidInadjacentSites); + link = DirectoryLinksProvider._updateSuggestedTile(); + do_check_true(link); + do_check_eq(link.url, "http://turbotax.com"); + do_check_false(DirectoryLinksProvider._newTabHasInadjacentSite); + + // _handleLinkChanged should return false now, even if newtab has bad site + do_check_false(DirectoryLinksProvider._handleLinkChanged({ + url: "http://someothersite.com", + type: "history", + })); + + // test _isInadjacentLink + do_check_true(DirectoryLinksProvider._isInadjacentLink({baseDomain: "someothersite.com"})); + do_check_false(DirectoryLinksProvider._isInadjacentLink({baseDomain: "bar.com"})); + do_check_true(DirectoryLinksProvider._isInadjacentLink({url: "http://www.someothersite.com"})); + do_check_false(DirectoryLinksProvider._isInadjacentLink({url: "http://www.bar.com"})); + // try to crash _isInadjacentLink + do_check_false(DirectoryLinksProvider._isInadjacentLink({baseDomain: ""})); + do_check_false(DirectoryLinksProvider._isInadjacentLink({url: ""})); + do_check_false(DirectoryLinksProvider._isInadjacentLink({url: "http://localhost:8081/"})); + do_check_false(DirectoryLinksProvider._isInadjacentLink({url: "abracodabra"})); + do_check_false(DirectoryLinksProvider._isInadjacentLink({})); + + // test _checkForInadjacentSites + do_check_true(DirectoryLinksProvider._checkForInadjacentSites()); + + // Cleanup + gLinks.removeProvider(DirectoryLinksProvider); + DirectoryLinksProvider._inadjacentSitesUrl = origInadjacentSitesUrl; + NewTabUtils.isTopPlacesSite = origIsTopPlacesSite; + NewTabUtils.getProviderLinks = origGetProviderLinks; + DirectoryLinksProvider._getCurrentTopSiteCount = origCurrentTopSiteCount; + yield promiseCleanDirectoryLinksProvider(); +}); + +add_task(function* test_blockSuggestedTiles() { + // Initial setup + let topSites = ["site0.com", "1040.com", "site2.com", "hrblock.com", "site4.com", "freetaxusa.com", "site6.com"]; + let data = {"suggested": [suggestedTile1, suggestedTile2, suggestedTile3], "directory": [someOtherSite]}; + let dataURI = 'data:application/json,' + JSON.stringify(data); + + yield promiseSetupDirectoryLinksProvider({linksURL: dataURI}); + let links = yield fetchData(); + + let origIsTopPlacesSite = NewTabUtils.isTopPlacesSite; + NewTabUtils.isTopPlacesSite = function(site) { + return topSites.indexOf(site) >= 0; + } + + let origGetProviderLinks = NewTabUtils.getProviderLinks; + NewTabUtils.getProviderLinks = function(provider) { + return links; + } + + let origCurrentTopSiteCount = DirectoryLinksProvider._getCurrentTopSiteCount; + DirectoryLinksProvider._getCurrentTopSiteCount = () => 8; + + // load the links + yield new Promise(resolve => { + DirectoryLinksProvider.getLinks(resolve); + }); + + // ensure that tile is suggested + let suggestedLink = DirectoryLinksProvider._updateSuggestedTile(); + do_check_true(suggestedLink.frecent_sites); + + // block suggested tile in a regular way + DirectoryLinksProvider.reportSitesAction([{ + isPinned: function() { return false; }, + link: Object.assign({frecency: 1000}, suggestedLink) + }], "block", 0); + + // suggested tile still must be recommended + suggestedLink = DirectoryLinksProvider._updateSuggestedTile(); + do_check_true(suggestedLink.frecent_sites); + + // timestamp suggested_block in the frequency cap object + DirectoryLinksProvider.handleSuggestedTileBlock(); + // no more recommendations should be seen + do_check_eq(DirectoryLinksProvider._updateSuggestedTile(), undefined); + + // move lastUpdated for suggested tile into the past + DirectoryLinksProvider._frequencyCaps["ignore://suggested_block"].lastUpdated = Date.now() - 25*60*60*1000; + // ensure that suggested tile updates again + suggestedLink = DirectoryLinksProvider._updateSuggestedTile(); + do_check_true(suggestedLink.frecent_sites); + + // Cleanup + yield promiseCleanDirectoryLinksProvider(); + NewTabUtils.isTopPlacesSite = origIsTopPlacesSite; + NewTabUtils.getProviderLinks = origGetProviderLinks; + DirectoryLinksProvider._getCurrentTopSiteCount = origCurrentTopSiteCount; +}); diff --git a/browser/modules/test/xpcshell/test_LaterRun.js b/browser/modules/test/xpcshell/test_LaterRun.js new file mode 100644 index 000000000..7b45c7cd5 --- /dev/null +++ b/browser/modules/test/xpcshell/test_LaterRun.js @@ -0,0 +1,138 @@ +"use strict"; + +const kEnabledPref = "browser.laterrun.enabled"; +const kPagePrefRoot = "browser.laterrun.pages."; +const kSessionCountPref = "browser.laterrun.bookkeeping.sessionCount"; +const kProfileCreationTime = "browser.laterrun.bookkeeping.profileCreationTime"; + +Components.utils.import("resource://gre/modules/Services.jsm"); +Components.utils.import("resource:///modules/LaterRun.jsm"); + +Services.prefs.setBoolPref(kEnabledPref, true); +Components.utils.import("resource://testing-common/AppInfo.jsm"); +updateAppInfo(); + +add_task(function* test_page_applies() { + Services.prefs.setCharPref(kPagePrefRoot + "test_LaterRun_unittest.url", "https://www.mozilla.org/%VENDOR%/%NAME%/%ID%/%VERSION%/"); + Services.prefs.setIntPref(kPagePrefRoot + "test_LaterRun_unittest.minimumHoursSinceInstall", 10); + Services.prefs.setIntPref(kPagePrefRoot + "test_LaterRun_unittest.minimumSessionCount", 3); + + let pages = LaterRun.readPages(); + // We have to filter the pages because it's possible Firefox ships with other URLs + // that get included in this test. + pages = pages.filter(page => page.pref == kPagePrefRoot + "test_LaterRun_unittest."); + Assert.equal(pages.length, 1, "Got 1 page"); + let page = pages[0]; + Assert.equal(page.pref, kPagePrefRoot + "test_LaterRun_unittest.", "Should know its own pref"); + Assert.equal(page.minimumHoursSinceInstall, 10, "Needs to have 10 hours since install"); + Assert.equal(page.minimumSessionCount, 3, "Needs to have 3 sessions"); + Assert.equal(page.requireBoth, false, "Either requirement is enough"); + let expectedURL = "https://www.mozilla.org/" + + Services.appinfo.vendor + "/" + + Services.appinfo.name + "/" + + Services.appinfo.ID + "/" + + Services.appinfo.version + "/"; + Assert.equal(page.url, expectedURL, "URL is stored correctly"); + + Assert.ok(page.applies({hoursSinceInstall: 1, sessionCount: 3}), + "Applies when session count has been met."); + Assert.ok(page.applies({hoursSinceInstall: 1, sessionCount: 4}), + "Applies when session count has been exceeded."); + Assert.ok(page.applies({hoursSinceInstall: 10, sessionCount: 2}), + "Applies when total session time has been met."); + Assert.ok(page.applies({hoursSinceInstall: 20, sessionCount: 2}), + "Applies when total session time has been exceeded."); + Assert.ok(page.applies({hoursSinceInstall: 10, sessionCount: 3}), + "Applies when both time and session count have been met."); + Assert.ok(!page.applies({hoursSinceInstall: 1, sessionCount: 1}), + "Does not apply when neither time and session count have been met."); + + page.requireBoth = true; + + Assert.ok(!page.applies({hoursSinceInstall: 1, sessionCount: 3}), + "Does not apply when only session count has been met."); + Assert.ok(!page.applies({hoursSinceInstall: 1, sessionCount: 4}), + "Does not apply when only session count has been exceeded."); + Assert.ok(!page.applies({hoursSinceInstall: 10, sessionCount: 2}), + "Does not apply when only total session time has been met."); + Assert.ok(!page.applies({hoursSinceInstall: 20, sessionCount: 2}), + "Does not apply when only total session time has been exceeded."); + Assert.ok(page.applies({hoursSinceInstall: 10, sessionCount: 3}), + "Applies when both time and session count have been met."); + Assert.ok(!page.applies({hoursSinceInstall: 1, sessionCount: 1}), + "Does not apply when neither time and session count have been met."); + + // Check that pages that have run never apply: + Services.prefs.setBoolPref(kPagePrefRoot + "test_LaterRun_unittest.hasRun", true); + page.requireBoth = false; + + Assert.ok(!page.applies({hoursSinceInstall: 1, sessionCount: 3}), + "Does not apply when page has already run (sessionCount equal)."); + Assert.ok(!page.applies({hoursSinceInstall: 1, sessionCount: 4}), + "Does not apply when page has already run (sessionCount exceeding)."); + Assert.ok(!page.applies({hoursSinceInstall: 10, sessionCount: 2}), + "Does not apply when page has already run (hoursSinceInstall equal)."); + Assert.ok(!page.applies({hoursSinceInstall: 20, sessionCount: 2}), + "Does not apply when page has already run (hoursSinceInstall exceeding)."); + Assert.ok(!page.applies({hoursSinceInstall: 10, sessionCount: 3}), + "Does not apply when page has already run (both criteria equal)."); + Assert.ok(!page.applies({hoursSinceInstall: 1, sessionCount: 1}), + "Does not apply when page has already run (both criteria insufficient anyway)."); + + clearAllPagePrefs(); +}); + +add_task(function* test_get_URL() { + Services.prefs.setIntPref(kProfileCreationTime, Math.floor((Date.now() - 11 * 60 * 60 * 1000) / 1000)); + Services.prefs.setCharPref(kPagePrefRoot + "test_LaterRun_unittest.url", "https://www.mozilla.org/"); + Services.prefs.setIntPref(kPagePrefRoot + "test_LaterRun_unittest.minimumHoursSinceInstall", 10); + Services.prefs.setIntPref(kPagePrefRoot + "test_LaterRun_unittest.minimumSessionCount", 3); + let pages = LaterRun.readPages(); + // We have to filter the pages because it's possible Firefox ships with other URLs + // that get included in this test. + pages = pages.filter(page => page.pref == kPagePrefRoot + "test_LaterRun_unittest."); + Assert.equal(pages.length, 1, "Should only be 1 matching page"); + let page = pages[0]; + let url; + do { + url = LaterRun.getURL(); + // We have to loop because it's possible Firefox ships with other URLs that get triggered by + // this test. + } while (url && url != "https://www.mozilla.org/"); + Assert.equal(url, "https://www.mozilla.org/", "URL should be as expected when prefs are set."); + Assert.ok(Services.prefs.prefHasUserValue(kPagePrefRoot + "test_LaterRun_unittest.hasRun"), "Should have set pref"); + Assert.ok(Services.prefs.getBoolPref(kPagePrefRoot + "test_LaterRun_unittest.hasRun"), "Should have set pref to true"); + Assert.ok(page.hasRun, "Other page objects should know it has run, too."); + + clearAllPagePrefs(); +}); + +add_task(function* test_insecure_urls() { + Services.prefs.setCharPref(kPagePrefRoot + "test_LaterRun_unittest.url", "http://www.mozilla.org/"); + Services.prefs.setIntPref(kPagePrefRoot + "test_LaterRun_unittest.minimumHoursSinceInstall", 10); + Services.prefs.setIntPref(kPagePrefRoot + "test_LaterRun_unittest.minimumSessionCount", 3); + let pages = LaterRun.readPages(); + // We have to filter the pages because it's possible Firefox ships with other URLs + // that get triggered in this test. + pages = pages.filter(page => page.pref == kPagePrefRoot + "test_LaterRun_unittest."); + Assert.equal(pages.length, 0, "URL with non-https scheme should get ignored"); + clearAllPagePrefs(); +}); + +add_task(function* test_dynamic_pref_getter_setter() { + delete LaterRun._sessionCount; + Services.prefs.setIntPref(kSessionCountPref, 0); + Assert.equal(LaterRun.sessionCount, 0, "Should start at 0"); + + LaterRun.sessionCount++; + Assert.equal(LaterRun.sessionCount, 1, "Should increment."); + Assert.equal(Services.prefs.getIntPref(kSessionCountPref), 1, "Should update pref"); +}); + +function clearAllPagePrefs() { + let allChangedPrefs = Services.prefs.getChildList(kPagePrefRoot); + for (let pref of allChangedPrefs) { + Services.prefs.clearUserPref(pref); + } +} + diff --git a/browser/modules/test/xpcshell/test_SitePermissions.js b/browser/modules/test/xpcshell/test_SitePermissions.js new file mode 100644 index 000000000..808d96599 --- /dev/null +++ b/browser/modules/test/xpcshell/test_SitePermissions.js @@ -0,0 +1,115 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +"use strict"; + +Components.utils.import("resource:///modules/SitePermissions.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); + +add_task(function* testPermissionsListing() { + Assert.deepEqual(SitePermissions.listPermissions().sort(), + ["camera", "cookie", "desktop-notification", "geo", "image", + "indexedDB", "install", "microphone", "popup", "screen"], + "Correct list of all permissions"); +}); + +add_task(function* testGetAllByURI() { + // check that it returns an empty array on an invalid URI + // like a file URI, which doesn't support site permissions + let wrongURI = Services.io.newURI("file:///example.js", null, null) + Assert.deepEqual(SitePermissions.getAllByURI(wrongURI), []); + + let uri = Services.io.newURI("https://example.com", null, null) + Assert.deepEqual(SitePermissions.getAllByURI(uri), []); + + SitePermissions.set(uri, "camera", SitePermissions.ALLOW); + Assert.deepEqual(SitePermissions.getAllByURI(uri), [ + { id: "camera", state: SitePermissions.ALLOW } + ]); + + SitePermissions.set(uri, "microphone", SitePermissions.SESSION); + SitePermissions.set(uri, "desktop-notification", SitePermissions.BLOCK); + + Assert.deepEqual(SitePermissions.getAllByURI(uri), [ + { id: "camera", state: SitePermissions.ALLOW }, + { id: "microphone", state: SitePermissions.SESSION }, + { id: "desktop-notification", state: SitePermissions.BLOCK } + ]); + + SitePermissions.remove(uri, "microphone"); + Assert.deepEqual(SitePermissions.getAllByURI(uri), [ + { id: "camera", state: SitePermissions.ALLOW }, + { id: "desktop-notification", state: SitePermissions.BLOCK } + ]); + + SitePermissions.remove(uri, "camera"); + SitePermissions.remove(uri, "desktop-notification"); + Assert.deepEqual(SitePermissions.getAllByURI(uri), []); + + // XXX Bug 1303108 - Control Center should only show non-default permissions + SitePermissions.set(uri, "addon", SitePermissions.BLOCK); + Assert.deepEqual(SitePermissions.getAllByURI(uri), []); + SitePermissions.remove(uri, "addon"); +}); + +add_task(function* testGetPermissionDetailsByURI() { + // check that it returns an empty array on an invalid URI + // like a file URI, which doesn't support site permissions + let wrongURI = Services.io.newURI("file:///example.js", null, null) + Assert.deepEqual(SitePermissions.getPermissionDetailsByURI(wrongURI), []); + + let uri = Services.io.newURI("https://example.com", null, null) + + SitePermissions.set(uri, "camera", SitePermissions.ALLOW); + SitePermissions.set(uri, "cookie", SitePermissions.SESSION); + SitePermissions.set(uri, "popup", SitePermissions.BLOCK); + + let permissions = SitePermissions.getPermissionDetailsByURI(uri); + + let camera = permissions.find(({id}) => id === "camera"); + Assert.deepEqual(camera, { + id: "camera", + label: "Use the Camera", + state: SitePermissions.ALLOW, + availableStates: [ + { id: SitePermissions.UNKNOWN, label: "Always Ask" }, + { id: SitePermissions.ALLOW, label: "Allow" }, + { id: SitePermissions.BLOCK, label: "Block" }, + ] + }); + + // check that removed permissions (State.UNKNOWN) are skipped + SitePermissions.remove(uri, "camera"); + permissions = SitePermissions.getPermissionDetailsByURI(uri); + + camera = permissions.find(({id}) => id === "camera"); + Assert.equal(camera, undefined); + + // check that different available state values are represented + + let cookie = permissions.find(({id}) => id === "cookie"); + Assert.deepEqual(cookie, { + id: "cookie", + label: "Set Cookies", + state: SitePermissions.SESSION, + availableStates: [ + { id: SitePermissions.ALLOW, label: "Allow" }, + { id: SitePermissions.SESSION, label: "Allow for Session" }, + { id: SitePermissions.BLOCK, label: "Block" }, + ] + }); + + let popup = permissions.find(({id}) => id === "popup"); + Assert.deepEqual(popup, { + id: "popup", + label: "Open Pop-up Windows", + state: SitePermissions.BLOCK, + availableStates: [ + { id: SitePermissions.ALLOW, label: "Allow" }, + { id: SitePermissions.BLOCK, label: "Block" }, + ] + }); + + SitePermissions.remove(uri, "cookie"); + SitePermissions.remove(uri, "popup"); +}); diff --git a/browser/modules/test/xpcshell/xpcshell.ini b/browser/modules/test/xpcshell/xpcshell.ini new file mode 100644 index 000000000..28df9c4ed --- /dev/null +++ b/browser/modules/test/xpcshell/xpcshell.ini @@ -0,0 +1,11 @@ +[DEFAULT] +head = +tail = +firefox-appdir = browser +skip-if = toolkit == 'android' + +[test_AttributionCode.js] +skip-if = os != 'win' +[test_DirectoryLinksProvider.js] +[test_SitePermissions.js] +[test_LaterRun.js] |