summaryrefslogtreecommitdiffstats
path: root/browser/modules/test
diff options
context:
space:
mode:
Diffstat (limited to 'browser/modules/test')
-rw-r--r--browser/modules/test/.eslintrc.js7
-rw-r--r--browser/modules/test/browser.ini42
-rw-r--r--browser/modules/test/browser_BrowserUITelemetry_buckets.js97
-rw-r--r--browser/modules/test/browser_BrowserUITelemetry_defaults.js37
-rw-r--r--browser/modules/test/browser_BrowserUITelemetry_sidebar.js56
-rw-r--r--browser/modules/test/browser_BrowserUITelemetry_syncedtabs.js114
-rw-r--r--browser/modules/test/browser_ContentSearch.js425
-rw-r--r--browser/modules/test/browser_NetworkPrioritizer.js165
-rw-r--r--browser/modules/test/browser_PermissionUI.js445
-rw-r--r--browser/modules/test/browser_ProcessHangNotifications.js189
-rw-r--r--browser/modules/test/browser_SelfSupportBackend.js214
-rw-r--r--browser/modules/test/browser_UnsubmittedCrashHandler.js680
-rw-r--r--browser/modules/test/browser_UsageTelemetry.js268
-rw-r--r--browser/modules/test/browser_UsageTelemetry_content.js121
-rw-r--r--browser/modules/test/browser_UsageTelemetry_content_aboutHome.js84
-rw-r--r--browser/modules/test/browser_UsageTelemetry_private_and_restore.js90
-rw-r--r--browser/modules/test/browser_UsageTelemetry_searchbar.js195
-rw-r--r--browser/modules/test/browser_UsageTelemetry_urlbar.js220
-rw-r--r--browser/modules/test/browser_taskbar_preview.js100
-rw-r--r--browser/modules/test/browser_urlBar_zoom.js73
-rw-r--r--browser/modules/test/contentSearch.js64
-rw-r--r--browser/modules/test/contentSearchBadImage.xml6
-rw-r--r--browser/modules/test/contentSearchSuggestions.sjs9
-rw-r--r--browser/modules/test/contentSearchSuggestions.xml6
-rw-r--r--browser/modules/test/head.js113
-rw-r--r--browser/modules/test/unit/social/.eslintrc.js7
-rw-r--r--browser/modules/test/unit/social/blocklist.xml6
-rw-r--r--browser/modules/test/unit/social/head.js210
-rw-r--r--browser/modules/test/unit/social/test_SocialService.js166
-rw-r--r--browser/modules/test/unit/social/test_SocialServiceMigration21.js54
-rw-r--r--browser/modules/test/unit/social/test_SocialServiceMigration22.js67
-rw-r--r--browser/modules/test/unit/social/test_SocialServiceMigration29.js61
-rw-r--r--browser/modules/test/unit/social/test_social.js32
-rw-r--r--browser/modules/test/unit/social/test_socialDisabledStartup.js29
-rw-r--r--browser/modules/test/unit/social/xpcshell.ini13
-rw-r--r--browser/modules/test/usageTelemetrySearchSuggestions.sjs9
-rw-r--r--browser/modules/test/usageTelemetrySearchSuggestions.xml6
-rw-r--r--browser/modules/test/xpcshell/.eslintrc.js7
-rw-r--r--browser/modules/test/xpcshell/test_AttributionCode.js110
-rw-r--r--browser/modules/test/xpcshell/test_DirectoryLinksProvider.js1854
-rw-r--r--browser/modules/test/xpcshell/test_LaterRun.js138
-rw-r--r--browser/modules/test/xpcshell/test_SitePermissions.js115
-rw-r--r--browser/modules/test/xpcshell/xpcshell.ini11
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]