summaryrefslogtreecommitdiffstats
path: root/browser/components/extensions/test/xpcshell
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--browser/components/extensions/test/xpcshell/.eslintrc.js9
-rw-r--r--browser/components/extensions/test/xpcshell/data/test/manifest.json70
-rw-r--r--browser/components/extensions/test/xpcshell/data/test2/manifest.json23
-rw-r--r--browser/components/extensions/test/xpcshell/head.js76
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_bookmarks.js1617
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_browsingData_downloads.js128
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_browsingData_passwords.js101
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_browsingData_settings.js152
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_chrome_settings_overrides_home.js156
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_chrome_settings_overrides_update.js228
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_distribution_popup.js60
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_history.js791
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_homepage_overrides_private.js136
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_manifest.js61
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_manifest_commands.js52
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_manifest_omnibox.js62
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_manifest_permissions.js73
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_normandyAddonStudy.js213
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_pageAction_shutdown.js87
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_pkcs11_management.js252
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_settings_overrides_defaults.js217
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_settings_overrides_search.js554
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_settings_overrides_search_mozParam.js158
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_settings_overrides_shutdown.js114
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_settings_validate.js193
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_topSites.js303
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_url_overrides_newtab.js351
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_url_overrides_newtab_update.js127
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_urlbar.js1455
-rw-r--r--browser/components/extensions/test/xpcshell/xpcshell.ini38
30 files changed, 7857 insertions, 0 deletions
diff --git a/browser/components/extensions/test/xpcshell/.eslintrc.js b/browser/components/extensions/test/xpcshell/.eslintrc.js
new file mode 100644
index 0000000000..3622fff4f6
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/.eslintrc.js
@@ -0,0 +1,9 @@
+"use strict";
+
+module.exports = {
+ env: {
+ // The tests in this folder are testing based on WebExtensions, so lets
+ // just define the webextensions environment here.
+ webextensions: true,
+ },
+};
diff --git a/browser/components/extensions/test/xpcshell/data/test/manifest.json b/browser/components/extensions/test/xpcshell/data/test/manifest.json
new file mode 100644
index 0000000000..5b75ba271d
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/data/test/manifest.json
@@ -0,0 +1,70 @@
+{
+ "name": "MozParamsTest",
+ "manifest_version": 2,
+ "version": "1.0",
+ "applications": {
+ "gecko": {
+ "id": "test@search.mozilla.org"
+ }
+ },
+ "description": "A test search engine (based on Google search)",
+ "chrome_settings_overrides": {
+ "search_provider": {
+ "name": "MozParamsTest",
+ "search_url": "https://example.com/?q={searchTerms}",
+ "params": [
+ {
+ "name": "test-0",
+ "condition": "purpose",
+ "purpose": "contextmenu",
+ "value": "0"
+ },
+ {
+ "name": "test-1",
+ "condition": "purpose",
+ "purpose": "searchbar",
+ "value": "1"
+ },
+ {
+ "name": "test-2",
+ "condition": "purpose",
+ "purpose": "homepage",
+ "value": "2"
+ },
+ {
+ "name": "test-3",
+ "condition": "purpose",
+ "purpose": "keyword",
+ "value": "3"
+ },
+ {
+ "name": "test-4",
+ "condition": "purpose",
+ "purpose": "newtab",
+ "value": "4"
+ },
+ {
+ "name": "simple",
+ "value": "5"
+ },
+ {
+ "name": "term",
+ "value": "{searchTerms}"
+ },
+ {
+ "name": "lang",
+ "value": "{language}"
+ },
+ {
+ "name": "locale",
+ "value": "{moz:locale}"
+ },
+ {
+ "name": "prefval",
+ "condition": "pref",
+ "pref": "code"
+ }
+ ]
+ }
+ }
+}
diff --git a/browser/components/extensions/test/xpcshell/data/test2/manifest.json b/browser/components/extensions/test/xpcshell/data/test2/manifest.json
new file mode 100644
index 0000000000..3fb62136f2
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/data/test2/manifest.json
@@ -0,0 +1,23 @@
+{
+ "name": "MozParamsTest2",
+ "manifest_version": 2,
+ "version": "1.0",
+ "applications": {
+ "gecko": {
+ "id": "test2@search.mozilla.org"
+ }
+ },
+ "description": "A second test search engine",
+ "chrome_settings_overrides": {
+ "search_provider": {
+ "name": "MozParamsTest2",
+ "search_url": "https://example.com/2/?q={searchTerms}",
+ "params": [
+ {
+ "name": "simple2",
+ "value": "5"
+ }
+ ]
+ }
+ }
+}
diff --git a/browser/components/extensions/test/xpcshell/head.js b/browser/components/extensions/test/xpcshell/head.js
new file mode 100644
index 0000000000..3c76792b55
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/head.js
@@ -0,0 +1,76 @@
+"use strict";
+
+/* exported createHttpServer, promiseConsoleOutput */
+
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+// eslint-disable-next-line no-unused-vars
+XPCOMUtils.defineLazyModuleGetters(this, {
+ AppConstants: "resource://gre/modules/AppConstants.jsm",
+ Extension: "resource://gre/modules/Extension.jsm",
+ ExtensionData: "resource://gre/modules/Extension.jsm",
+ ExtensionTestUtils: "resource://testing-common/ExtensionXPCShellUtils.jsm",
+ FileUtils: "resource://gre/modules/FileUtils.jsm",
+ HttpServer: "resource://testing-common/httpd.js",
+ NetUtil: "resource://gre/modules/NetUtil.jsm",
+ Schemas: "resource://gre/modules/Schemas.jsm",
+ TestUtils: "resource://testing-common/TestUtils.jsm",
+});
+
+ExtensionTestUtils.init(this);
+
+/**
+ * Creates a new HttpServer for testing, and begins listening on the
+ * specified port. Automatically shuts down the server when the test
+ * unit ends.
+ *
+ * @param {integer} [port]
+ * The port to listen on. If omitted, listen on a random
+ * port. The latter is the preferred behavior.
+ *
+ * @returns {HttpServer}
+ */
+function createHttpServer(port = -1) {
+ let server = new HttpServer();
+ server.start(port);
+
+ registerCleanupFunction(() => {
+ return new Promise(resolve => {
+ server.stop(resolve);
+ });
+ });
+
+ return server;
+}
+
+var promiseConsoleOutput = async function(task) {
+ const DONE = `=== console listener ${Math.random()} done ===`;
+
+ let listener;
+ let messages = [];
+ let awaitListener = new Promise(resolve => {
+ listener = msg => {
+ if (msg == DONE) {
+ resolve();
+ } else {
+ void (msg instanceof Ci.nsIConsoleMessage);
+ messages.push(msg);
+ }
+ };
+ });
+
+ Services.console.registerListener(listener);
+ try {
+ let result = await task();
+
+ Services.console.logStringMessage(DONE);
+ await awaitListener;
+
+ return { messages, result };
+ } finally {
+ Services.console.unregisterListener(listener);
+ }
+};
diff --git a/browser/components/extensions/test/xpcshell/test_ext_bookmarks.js b/browser/components/extensions/test/xpcshell/test_ext_bookmarks.js
new file mode 100644
index 0000000000..3c5ba45d7a
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_bookmarks.js
@@ -0,0 +1,1617 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm"
+);
+
+add_task(async function test_bookmarks() {
+ async function background() {
+ let unsortedId, ourId;
+ let initialBookmarkCount = 0;
+ let createdBookmarks = new Set();
+ let createdFolderId;
+ let createdSeparatorId;
+ let collectedEvents = [];
+ const nonExistentId = "000000000000";
+ const bookmarkGuids = {
+ menuGuid: "menu________",
+ toolbarGuid: "toolbar_____",
+ unfiledGuid: "unfiled_____",
+ rootGuid: "root________",
+ };
+
+ function checkOurBookmark(bookmark) {
+ browser.test.assertEq(ourId, bookmark.id, "Bookmark has the expected Id");
+ browser.test.assertTrue(
+ "parentId" in bookmark,
+ "Bookmark has a parentId"
+ );
+ browser.test.assertEq(
+ 0,
+ bookmark.index,
+ "Bookmark has the expected index"
+ ); // We assume there are no other bookmarks.
+ browser.test.assertEq(
+ "http://example.org/",
+ bookmark.url,
+ "Bookmark has the expected url"
+ );
+ browser.test.assertEq(
+ "test bookmark",
+ bookmark.title,
+ "Bookmark has the expected title"
+ );
+ browser.test.assertTrue(
+ "dateAdded" in bookmark,
+ "Bookmark has a dateAdded"
+ );
+ browser.test.assertFalse(
+ "dateGroupModified" in bookmark,
+ "Bookmark does not have a dateGroupModified"
+ );
+ browser.test.assertFalse(
+ "unmodifiable" in bookmark,
+ "Bookmark is not unmodifiable"
+ );
+ browser.test.assertEq(
+ "bookmark",
+ bookmark.type,
+ "Bookmark is of type bookmark"
+ );
+ }
+
+ function checkBookmark(expected, bookmark) {
+ browser.test.assertEq(
+ expected.url,
+ bookmark.url,
+ "Bookmark has the expected url"
+ );
+ browser.test.assertEq(
+ expected.title,
+ bookmark.title,
+ "Bookmark has the expected title"
+ );
+ browser.test.assertEq(
+ expected.index,
+ bookmark.index,
+ "Bookmark has expected index"
+ );
+ browser.test.assertEq(
+ "bookmark",
+ bookmark.type,
+ "Bookmark is of type bookmark"
+ );
+ if ("parentId" in expected) {
+ browser.test.assertEq(
+ expected.parentId,
+ bookmark.parentId,
+ "Bookmark has the expected parentId"
+ );
+ }
+ }
+
+ function checkOnCreated(
+ id,
+ parentId,
+ index,
+ title,
+ url,
+ dateAdded,
+ type = "bookmark"
+ ) {
+ let createdData = collectedEvents.pop();
+ browser.test.assertEq(
+ "onCreated",
+ createdData.event,
+ "onCreated was the last event received"
+ );
+ browser.test.assertEq(
+ id,
+ createdData.id,
+ "onCreated event received the expected id"
+ );
+ let bookmark = createdData.bookmark;
+ browser.test.assertEq(
+ id,
+ bookmark.id,
+ "onCreated event received the expected bookmark id"
+ );
+ browser.test.assertEq(
+ parentId,
+ bookmark.parentId,
+ "onCreated event received the expected bookmark parentId"
+ );
+ browser.test.assertEq(
+ index,
+ bookmark.index,
+ "onCreated event received the expected bookmark index"
+ );
+ browser.test.assertEq(
+ title,
+ bookmark.title,
+ "onCreated event received the expected bookmark title"
+ );
+ browser.test.assertEq(
+ url,
+ bookmark.url,
+ "onCreated event received the expected bookmark url"
+ );
+ browser.test.assertEq(
+ dateAdded,
+ bookmark.dateAdded,
+ "onCreated event received the expected bookmark dateAdded"
+ );
+ browser.test.assertEq(
+ type,
+ bookmark.type,
+ "onCreated event received the expected bookmark type"
+ );
+ }
+
+ function checkOnChanged(id, url, title) {
+ // If both url and title are changed, then url is fired last.
+ let changedData = collectedEvents.pop();
+ browser.test.assertEq(
+ "onChanged",
+ changedData.event,
+ "onChanged was the last event received"
+ );
+ browser.test.assertEq(
+ id,
+ changedData.id,
+ "onChanged event received the expected id"
+ );
+ browser.test.assertEq(
+ url,
+ changedData.info.url,
+ "onChanged event received the expected url"
+ );
+ // title is fired first.
+ changedData = collectedEvents.pop();
+ browser.test.assertEq(
+ "onChanged",
+ changedData.event,
+ "onChanged was the last event received"
+ );
+ browser.test.assertEq(
+ id,
+ changedData.id,
+ "onChanged event received the expected id"
+ );
+ browser.test.assertEq(
+ title,
+ changedData.info.title,
+ "onChanged event received the expected title"
+ );
+ }
+
+ function checkOnMoved(id, parentId, oldParentId, index, oldIndex) {
+ let movedData = collectedEvents.pop();
+ browser.test.assertEq(
+ "onMoved",
+ movedData.event,
+ "onMoved was the last event received"
+ );
+ browser.test.assertEq(
+ id,
+ movedData.id,
+ "onMoved event received the expected id"
+ );
+ let info = movedData.info;
+ browser.test.assertEq(
+ parentId,
+ info.parentId,
+ "onMoved event received the expected parentId"
+ );
+ browser.test.assertEq(
+ oldParentId,
+ info.oldParentId,
+ "onMoved event received the expected oldParentId"
+ );
+ browser.test.assertEq(
+ index,
+ info.index,
+ "onMoved event received the expected index"
+ );
+ browser.test.assertEq(
+ oldIndex,
+ info.oldIndex,
+ "onMoved event received the expected oldIndex"
+ );
+ }
+
+ function checkOnRemoved(id, parentId, index, url, type = "folder") {
+ let removedData = collectedEvents.pop();
+ browser.test.assertEq(
+ "onRemoved",
+ removedData.event,
+ "onRemoved was the last event received"
+ );
+ browser.test.assertEq(
+ id,
+ removedData.id,
+ "onRemoved event received the expected id"
+ );
+ let info = removedData.info;
+ browser.test.assertEq(
+ parentId,
+ removedData.info.parentId,
+ "onRemoved event received the expected parentId"
+ );
+ browser.test.assertEq(
+ index,
+ removedData.info.index,
+ "onRemoved event received the expected index"
+ );
+ let node = info.node;
+ browser.test.assertEq(
+ id,
+ node.id,
+ "onRemoved event received the expected node id"
+ );
+ browser.test.assertEq(
+ parentId,
+ node.parentId,
+ "onRemoved event received the expected node parentId"
+ );
+ browser.test.assertEq(
+ index,
+ node.index,
+ "onRemoved event received the expected node index"
+ );
+ browser.test.assertEq(
+ url,
+ node.url,
+ "onRemoved event received the expected node url"
+ );
+ browser.test.assertEq(
+ type,
+ node.type,
+ "onRemoved event received the expected node type"
+ );
+ }
+
+ browser.bookmarks.onChanged.addListener((id, info) => {
+ collectedEvents.push({ event: "onChanged", id, info });
+ });
+
+ browser.bookmarks.onCreated.addListener((id, bookmark) => {
+ collectedEvents.push({ event: "onCreated", id, bookmark });
+ });
+
+ browser.bookmarks.onMoved.addListener((id, info) => {
+ collectedEvents.push({ event: "onMoved", id, info });
+ });
+
+ browser.bookmarks.onRemoved.addListener((id, info) => {
+ collectedEvents.push({ event: "onRemoved", id, info });
+ });
+
+ await browser.test.assertRejects(
+ browser.bookmarks.get(["not-a-bookmark-guid"]),
+ /Invalid value for property 'guid': "not-a-bookmark-guid"/,
+ "Expected error thrown when trying to get a bookmark using an invalid guid"
+ );
+
+ await browser.test
+ .assertRejects(
+ browser.bookmarks.get([nonExistentId]),
+ /Bookmark not found/,
+ "Expected error thrown when trying to get a bookmark using a non-existent Id"
+ )
+ .then(() => {
+ return browser.bookmarks.search({});
+ })
+ .then(results => {
+ initialBookmarkCount = results.length;
+ return browser.bookmarks.create({
+ title: "test bookmark",
+ url: "http://example.org",
+ type: "bookmark",
+ });
+ })
+ .then(result => {
+ ourId = result.id;
+ checkOurBookmark(result);
+ browser.test.assertEq(
+ 1,
+ collectedEvents.length,
+ "1 expected event received"
+ );
+ checkOnCreated(
+ ourId,
+ bookmarkGuids.unfiledGuid,
+ 0,
+ "test bookmark",
+ "http://example.org/",
+ result.dateAdded
+ );
+
+ return browser.bookmarks.get(ourId);
+ })
+ .then(results => {
+ browser.test.assertEq(results.length, 1);
+ checkOurBookmark(results[0]);
+
+ unsortedId = results[0].parentId;
+ return browser.bookmarks.get(unsortedId);
+ })
+ .then(results => {
+ let folder = results[0];
+ browser.test.assertEq(1, results.length, "1 bookmark was returned");
+
+ browser.test.assertEq(
+ unsortedId,
+ folder.id,
+ "Folder has the expected id"
+ );
+ browser.test.assertTrue("parentId" in folder, "Folder has a parentId");
+ browser.test.assertTrue("index" in folder, "Folder has an index");
+ browser.test.assertEq(
+ undefined,
+ folder.url,
+ "Folder does not have a url"
+ );
+ browser.test.assertEq(
+ "Other Bookmarks",
+ folder.title,
+ "Folder has the expected title"
+ );
+ browser.test.assertTrue(
+ "dateAdded" in folder,
+ "Folder has a dateAdded"
+ );
+ browser.test.assertTrue(
+ "dateGroupModified" in folder,
+ "Folder has a dateGroupModified"
+ );
+ browser.test.assertFalse(
+ "unmodifiable" in folder,
+ "Folder is not unmodifiable"
+ ); // TODO: Do we want to enable this?
+ browser.test.assertEq(
+ "folder",
+ folder.type,
+ "Folder has a type of folder"
+ );
+
+ return browser.bookmarks.getChildren(unsortedId);
+ })
+ .then(async results => {
+ browser.test.assertEq(1, results.length, "The folder has one child");
+ checkOurBookmark(results[0]);
+
+ await browser.test.assertRejects(
+ browser.bookmarks.update(nonExistentId, { title: "new test title" }),
+ /No bookmarks found for the provided GUID/,
+ "Expected error thrown when trying to update a non-existent bookmark"
+ );
+ return browser.bookmarks.update(ourId, {
+ title: "new test title",
+ url: "http://example.com/",
+ });
+ })
+ .then(async result => {
+ browser.test.assertEq(
+ "new test title",
+ result.title,
+ "Updated bookmark has the expected title"
+ );
+ browser.test.assertEq(
+ "http://example.com/",
+ result.url,
+ "Updated bookmark has the expected URL"
+ );
+ browser.test.assertEq(
+ ourId,
+ result.id,
+ "Updated bookmark has the expected id"
+ );
+ browser.test.assertEq(
+ "bookmark",
+ result.type,
+ "Updated bookmark has a type of bookmark"
+ );
+
+ browser.test.assertEq(
+ 2,
+ collectedEvents.length,
+ "2 expected events received"
+ );
+ checkOnChanged(ourId, "http://example.com/", "new test title");
+
+ await browser.test.assertRejects(
+ browser.bookmarks.update(ourId, { url: "this is not a valid url" }),
+ /Invalid bookmark:/,
+ "Expected error thrown when trying update with an invalid url"
+ );
+ return browser.bookmarks.getTree();
+ })
+ .then(results => {
+ browser.test.assertEq(1, results.length, "getTree returns one result");
+ let bookmark = results[0].children.find(
+ bookmarkItem => bookmarkItem.id == unsortedId
+ );
+ browser.test.assertEq(
+ "Other Bookmarks",
+ bookmark.title,
+ "Folder returned from getTree has the expected title"
+ );
+ browser.test.assertEq(
+ "folder",
+ bookmark.type,
+ "Folder returned from getTree has the expected type"
+ );
+
+ return browser.test.assertRejects(
+ browser.bookmarks.create({ parentId: "invalid" }),
+ error =>
+ error.message.includes("Invalid bookmark") &&
+ error.message.includes(`"parentGuid":"invalid"`),
+ "Expected error thrown when trying to create a bookmark with an invalid parentId"
+ );
+ })
+ .then(() => {
+ return browser.bookmarks.remove(ourId);
+ })
+ .then(result => {
+ browser.test.assertEq(
+ undefined,
+ result,
+ "Removing a bookmark returns undefined"
+ );
+
+ browser.test.assertEq(
+ 1,
+ collectedEvents.length,
+ "1 expected events received"
+ );
+ checkOnRemoved(
+ ourId,
+ bookmarkGuids.unfiledGuid,
+ 0,
+ "http://example.com/",
+ "bookmark"
+ );
+
+ return browser.test.assertRejects(
+ browser.bookmarks.get(ourId),
+ /Bookmark not found/,
+ "Expected error thrown when trying to get a removed bookmark"
+ );
+ })
+ .then(() => {
+ return browser.test.assertRejects(
+ browser.bookmarks.remove(nonExistentId),
+ /No bookmarks found for the provided GUID/,
+ "Expected error thrown when trying removed a non-existent bookmark"
+ );
+ })
+ .then(() => {
+ // test bookmarks.search
+ return Promise.all([
+ browser.bookmarks.create({
+ title: "MØzillä",
+ url: "http://møzîllä.örg/",
+ }),
+ browser.bookmarks.create({
+ title: "Example",
+ url: "http://example.org/",
+ }),
+ browser.bookmarks.create({ title: "Mozilla Folder", type: "folder" }),
+ browser.bookmarks.create({ title: "EFF", url: "http://eff.org/" }),
+ browser.bookmarks.create({
+ title: "Menu Item",
+ url: "http://menu.org/",
+ parentId: bookmarkGuids.menuGuid,
+ }),
+ browser.bookmarks.create({
+ title: "Toolbar Item",
+ url: "http://toolbar.org/",
+ parentId: bookmarkGuids.toolbarGuid,
+ }),
+ ]);
+ })
+ .then(results => {
+ browser.test.assertEq(
+ 6,
+ collectedEvents.length,
+ "6 expected events received"
+ );
+ checkOnCreated(
+ results[5].id,
+ bookmarkGuids.toolbarGuid,
+ 0,
+ "Toolbar Item",
+ "http://toolbar.org/",
+ results[5].dateAdded
+ );
+ checkOnCreated(
+ results[4].id,
+ bookmarkGuids.menuGuid,
+ 0,
+ "Menu Item",
+ "http://menu.org/",
+ results[4].dateAdded
+ );
+ checkOnCreated(
+ results[3].id,
+ bookmarkGuids.unfiledGuid,
+ 0,
+ "EFF",
+ "http://eff.org/",
+ results[3].dateAdded
+ );
+ checkOnCreated(
+ results[2].id,
+ bookmarkGuids.unfiledGuid,
+ 0,
+ "Mozilla Folder",
+ undefined,
+ results[2].dateAdded,
+ "folder"
+ );
+ checkOnCreated(
+ results[1].id,
+ bookmarkGuids.unfiledGuid,
+ 0,
+ "Example",
+ "http://example.org/",
+ results[1].dateAdded
+ );
+ checkOnCreated(
+ results[0].id,
+ bookmarkGuids.unfiledGuid,
+ 0,
+ "MØzillä",
+ "http://xn--mzll-ooa1dud.xn--rg-eka/",
+ results[0].dateAdded
+ );
+
+ for (let result of results) {
+ if (result.title !== "Mozilla Folder") {
+ createdBookmarks.add(result.id);
+ }
+ }
+ let folderResult = results[2];
+ createdFolderId = folderResult.id;
+ return Promise.all([
+ browser.bookmarks.create({
+ title: "Mozilla",
+ url: "http://allizom.org/",
+ parentId: createdFolderId,
+ }),
+ browser.bookmarks.create({
+ parentId: createdFolderId,
+ type: "separator",
+ }),
+ browser.bookmarks.create({
+ title: "Mozilla Corporation",
+ url: "http://allizom.com/",
+ parentId: createdFolderId,
+ }),
+ browser.bookmarks.create({
+ title: "Firefox",
+ url: "http://allizom.org/firefox/",
+ parentId: createdFolderId,
+ }),
+ ])
+ .then(newBookmarks => {
+ browser.test.assertEq(
+ 4,
+ collectedEvents.length,
+ "4 expected events received"
+ );
+ checkOnCreated(
+ newBookmarks[3].id,
+ createdFolderId,
+ 0,
+ "Firefox",
+ "http://allizom.org/firefox/",
+ newBookmarks[3].dateAdded
+ );
+ checkOnCreated(
+ newBookmarks[2].id,
+ createdFolderId,
+ 0,
+ "Mozilla Corporation",
+ "http://allizom.com/",
+ newBookmarks[2].dateAdded
+ );
+ checkOnCreated(
+ newBookmarks[1].id,
+ createdFolderId,
+ 0,
+ "",
+ "data:",
+ newBookmarks[1].dateAdded,
+ "separator"
+ );
+ checkOnCreated(
+ newBookmarks[0].id,
+ createdFolderId,
+ 0,
+ "Mozilla",
+ "http://allizom.org/",
+ newBookmarks[0].dateAdded
+ );
+
+ return browser.bookmarks.create({
+ title: "About Mozilla",
+ url: "http://allizom.org/about/",
+ parentId: createdFolderId,
+ index: 1,
+ });
+ })
+ .then(result => {
+ browser.test.assertEq(
+ 1,
+ collectedEvents.length,
+ "1 expected events received"
+ );
+ checkOnCreated(
+ result.id,
+ createdFolderId,
+ 1,
+ "About Mozilla",
+ "http://allizom.org/about/",
+ result.dateAdded
+ );
+
+ // returns all items on empty object
+ return browser.bookmarks.search({});
+ })
+ .then(async bookmarksSearchResults => {
+ browser.test.assertTrue(
+ bookmarksSearchResults.length >= 10,
+ "At least as many bookmarks as added were returned by search({})"
+ );
+
+ await browser.test.assertRejects(
+ browser.bookmarks.remove(createdFolderId),
+ /Cannot remove a non-empty folder/,
+ "Expected error thrown when trying to remove a non-empty folder"
+ );
+ return browser.bookmarks.getSubTree(createdFolderId);
+ });
+ })
+ .then(results => {
+ browser.test.assertEq(
+ 1,
+ results.length,
+ "Expected number of nodes returned by getSubTree"
+ );
+ browser.test.assertEq(
+ "Mozilla Folder",
+ results[0].title,
+ "Folder has the expected title"
+ );
+ browser.test.assertEq(
+ bookmarkGuids.unfiledGuid,
+ results[0].parentId,
+ "Folder has the expected parentId"
+ );
+ browser.test.assertEq(
+ "folder",
+ results[0].type,
+ "Folder has the expected type"
+ );
+ let children = results[0].children;
+ browser.test.assertEq(
+ 5,
+ children.length,
+ "Expected number of bookmarks returned by getSubTree"
+ );
+ browser.test.assertEq(
+ "Firefox",
+ children[0].title,
+ "Bookmark has the expected title"
+ );
+ browser.test.assertEq(
+ "bookmark",
+ children[0].type,
+ "Bookmark has the expected type"
+ );
+ browser.test.assertEq(
+ "About Mozilla",
+ children[1].title,
+ "Bookmark has the expected title"
+ );
+ browser.test.assertEq(
+ "bookmark",
+ children[1].type,
+ "Bookmark has the expected type"
+ );
+ browser.test.assertEq(
+ 1,
+ children[1].index,
+ "Bookmark has the expected index"
+ );
+ browser.test.assertEq(
+ "Mozilla Corporation",
+ children[2].title,
+ "Bookmark has the expected title"
+ );
+ browser.test.assertEq(
+ "",
+ children[3].title,
+ "Separator has the expected title"
+ );
+ browser.test.assertEq(
+ "data:",
+ children[3].url,
+ "Separator has the expected url"
+ );
+ browser.test.assertEq(
+ "separator",
+ children[3].type,
+ "Separator has the expected type"
+ );
+ browser.test.assertEq(
+ "Mozilla",
+ children[4].title,
+ "Bookmark has the expected title"
+ );
+
+ // throws an error for invalid query objects
+ browser.test.assertThrows(
+ () => browser.bookmarks.search(),
+ /Incorrect argument types for bookmarks.search/,
+ "Expected error thrown when trying to search with no arguments"
+ );
+
+ browser.test.assertThrows(
+ () => browser.bookmarks.search(null),
+ /Incorrect argument types for bookmarks.search/,
+ "Expected error thrown when trying to search with null as an argument"
+ );
+
+ browser.test.assertThrows(
+ () => browser.bookmarks.search(() => {}),
+ /Incorrect argument types for bookmarks.search/,
+ "Expected error thrown when trying to search with a function as an argument"
+ );
+
+ browser.test.assertThrows(
+ () => browser.bookmarks.search({ banana: "banana" }),
+ /an unexpected "banana" property/,
+ "Expected error thrown when trying to search with a banana as an argument"
+ );
+
+ browser.test.assertThrows(
+ () => browser.bookmarks.search({ url: "spider-man vs. batman" }),
+ /must match the format "url"/,
+ "Expected error thrown when trying to search with a illegally formatted URL"
+ );
+ // queries the full url
+ return browser.bookmarks.search("http://example.org/");
+ })
+ .then(results => {
+ browser.test.assertEq(
+ 1,
+ results.length,
+ "Expected number of results returned for url search"
+ );
+ checkBookmark(
+ { title: "Example", url: "http://example.org/", index: 2 },
+ results[0]
+ );
+
+ // queries a partial url
+ return browser.bookmarks.search("example.org");
+ })
+ .then(results => {
+ browser.test.assertEq(
+ 1,
+ results.length,
+ "Expected number of results returned for url search"
+ );
+ checkBookmark(
+ { title: "Example", url: "http://example.org/", index: 2 },
+ results[0]
+ );
+
+ // queries the title
+ return browser.bookmarks.search("EFF");
+ })
+ .then(results => {
+ browser.test.assertEq(
+ 1,
+ results.length,
+ "Expected number of results returned for title search"
+ );
+ checkBookmark(
+ {
+ title: "EFF",
+ url: "http://eff.org/",
+ index: 0,
+ parentId: bookmarkGuids.unfiledGuid,
+ },
+ results[0]
+ );
+
+ // finds menu items
+ return browser.bookmarks.search("Menu Item");
+ })
+ .then(results => {
+ browser.test.assertEq(
+ 1,
+ results.length,
+ "Expected number of results returned for menu item search"
+ );
+ checkBookmark(
+ {
+ title: "Menu Item",
+ url: "http://menu.org/",
+ index: 0,
+ parentId: bookmarkGuids.menuGuid,
+ },
+ results[0]
+ );
+
+ // finds toolbar items
+ return browser.bookmarks.search("Toolbar Item");
+ })
+ .then(results => {
+ browser.test.assertEq(
+ 1,
+ results.length,
+ "Expected number of results returned for toolbar item search"
+ );
+ checkBookmark(
+ {
+ title: "Toolbar Item",
+ url: "http://toolbar.org/",
+ index: 0,
+ parentId: bookmarkGuids.toolbarGuid,
+ },
+ results[0]
+ );
+
+ // finds folders
+ return browser.bookmarks.search("Mozilla Folder");
+ })
+ .then(results => {
+ browser.test.assertEq(
+ 1,
+ results.length,
+ "Expected number of folders returned"
+ );
+ browser.test.assertEq(
+ "Mozilla Folder",
+ results[0].title,
+ "Folder has the expected title"
+ );
+ browser.test.assertEq(
+ "folder",
+ results[0].type,
+ "Folder has the expected type"
+ );
+
+ // is case-insensitive
+ return browser.bookmarks.search("corporation");
+ })
+ .then(results => {
+ browser.test.assertEq(
+ 1,
+ results.length,
+ "Expected number of results returnedfor case-insensitive search"
+ );
+ browser.test.assertEq(
+ "Mozilla Corporation",
+ results[0].title,
+ "Bookmark has the expected title"
+ );
+
+ // is case-insensitive for non-ascii
+ return browser.bookmarks.search("MøZILLÄ");
+ })
+ .then(results => {
+ browser.test.assertEq(
+ 1,
+ results.length,
+ "Expected number of results returned for non-ascii search"
+ );
+ browser.test.assertEq(
+ "MØzillä",
+ results[0].title,
+ "Bookmark has the expected title"
+ );
+
+ // returns multiple results
+ return browser.bookmarks.search("allizom");
+ })
+ .then(results => {
+ browser.test.assertEq(
+ 4,
+ results.length,
+ "Expected number of multiple results returned"
+ );
+ browser.test.assertEq(
+ "Mozilla",
+ results[0].title,
+ "Bookmark has the expected title"
+ );
+ browser.test.assertEq(
+ "Mozilla Corporation",
+ results[1].title,
+ "Bookmark has the expected title"
+ );
+ browser.test.assertEq(
+ "Firefox",
+ results[2].title,
+ "Bookmark has the expected title"
+ );
+ browser.test.assertEq(
+ "About Mozilla",
+ results[3].title,
+ "Bookmark has the expected title"
+ );
+
+ // accepts a url field
+ return browser.bookmarks.search({ url: "http://allizom.com/" });
+ })
+ .then(results => {
+ browser.test.assertEq(
+ 1,
+ results.length,
+ "Expected number of results returned for url field"
+ );
+ checkBookmark(
+ {
+ title: "Mozilla Corporation",
+ url: "http://allizom.com/",
+ index: 2,
+ },
+ results[0]
+ );
+
+ // normalizes urls
+ return browser.bookmarks.search({ url: "http://allizom.com" });
+ })
+ .then(results => {
+ browser.test.assertEq(
+ results.length,
+ 1,
+ "Expected number of results returned for normalized url field"
+ );
+ checkBookmark(
+ {
+ title: "Mozilla Corporation",
+ url: "http://allizom.com/",
+ index: 2,
+ },
+ results[0]
+ );
+
+ // normalizes urls even more
+ return browser.bookmarks.search({ url: "http:allizom.com" });
+ })
+ .then(results => {
+ browser.test.assertEq(
+ results.length,
+ 1,
+ "Expected number of results returned for normalized url field"
+ );
+ checkBookmark(
+ {
+ title: "Mozilla Corporation",
+ url: "http://allizom.com/",
+ index: 2,
+ },
+ results[0]
+ );
+
+ // accepts a title field
+ return browser.bookmarks.search({ title: "Mozilla" });
+ })
+ .then(results => {
+ browser.test.assertEq(
+ results.length,
+ 1,
+ "Expected number of results returned for title field"
+ );
+ checkBookmark(
+ { title: "Mozilla", url: "http://allizom.org/", index: 4 },
+ results[0]
+ );
+
+ // can combine title and query
+ return browser.bookmarks.search({ title: "Mozilla", query: "allizom" });
+ })
+ .then(results => {
+ browser.test.assertEq(
+ 1,
+ results.length,
+ "Expected number of results returned for title and query fields"
+ );
+ checkBookmark(
+ { title: "Mozilla", url: "http://allizom.org/", index: 4 },
+ results[0]
+ );
+
+ // uses AND conditions
+ return browser.bookmarks.search({ title: "EFF", query: "allizom" });
+ })
+ .then(results => {
+ browser.test.assertEq(
+ 0,
+ results.length,
+ "Expected number of results returned for non-matching title and query fields"
+ );
+
+ // returns an empty array on item not found
+ return browser.bookmarks.search("microsoft");
+ })
+ .then(results => {
+ browser.test.assertEq(
+ 0,
+ results.length,
+ "Expected number of results returned for non-matching search"
+ );
+
+ browser.test.assertThrows(
+ () => browser.bookmarks.getRecent(""),
+ /Incorrect argument types for bookmarks.getRecent/,
+ "Expected error thrown when calling getRecent with an empty string"
+ );
+ })
+ .then(() => {
+ browser.test.assertThrows(
+ () => browser.bookmarks.getRecent(1.234),
+ /Incorrect argument types for bookmarks.getRecent/,
+ "Expected error thrown when calling getRecent with a decimal number"
+ );
+ })
+ .then(() => {
+ return Promise.all([
+ browser.bookmarks.search("corporation"),
+ browser.bookmarks.getChildren(bookmarkGuids.menuGuid),
+ ]);
+ })
+ .then(results => {
+ let corporationBookmark = results[0][0];
+ let childCount = results[1].length;
+
+ browser.test.assertEq(
+ 2,
+ corporationBookmark.index,
+ "Bookmark has the expected index"
+ );
+
+ return browser.bookmarks
+ .move(corporationBookmark.id, { index: 0 })
+ .then(result => {
+ browser.test.assertEq(
+ 0,
+ result.index,
+ "Bookmark has the expected index"
+ );
+
+ browser.test.assertEq(
+ 1,
+ collectedEvents.length,
+ "1 expected events received"
+ );
+ checkOnMoved(
+ corporationBookmark.id,
+ createdFolderId,
+ createdFolderId,
+ 0,
+ 2
+ );
+
+ return browser.bookmarks.move(corporationBookmark.id, {
+ parentId: bookmarkGuids.menuGuid,
+ });
+ })
+ .then(result => {
+ browser.test.assertEq(
+ bookmarkGuids.menuGuid,
+ result.parentId,
+ "Bookmark has the expected parent"
+ );
+ browser.test.assertEq(
+ childCount,
+ result.index,
+ "Bookmark has the expected index"
+ );
+
+ browser.test.assertEq(
+ 1,
+ collectedEvents.length,
+ "1 expected events received"
+ );
+ checkOnMoved(
+ corporationBookmark.id,
+ bookmarkGuids.menuGuid,
+ createdFolderId,
+ 1,
+ 0
+ );
+
+ return browser.bookmarks.move(corporationBookmark.id, { index: 0 });
+ })
+ .then(result => {
+ browser.test.assertEq(
+ bookmarkGuids.menuGuid,
+ result.parentId,
+ "Bookmark has the expected parent"
+ );
+ browser.test.assertEq(
+ 0,
+ result.index,
+ "Bookmark has the expected index"
+ );
+
+ browser.test.assertEq(
+ 1,
+ collectedEvents.length,
+ "1 expected events received"
+ );
+ checkOnMoved(
+ corporationBookmark.id,
+ bookmarkGuids.menuGuid,
+ bookmarkGuids.menuGuid,
+ 0,
+ 1
+ );
+
+ return browser.bookmarks.move(corporationBookmark.id, {
+ parentId: bookmarkGuids.toolbarGuid,
+ index: 1,
+ });
+ })
+ .then(result => {
+ browser.test.assertEq(
+ bookmarkGuids.toolbarGuid,
+ result.parentId,
+ "Bookmark has the expected parent"
+ );
+ browser.test.assertEq(
+ 1,
+ result.index,
+ "Bookmark has the expected index"
+ );
+
+ browser.test.assertEq(
+ 1,
+ collectedEvents.length,
+ "1 expected events received"
+ );
+ checkOnMoved(
+ corporationBookmark.id,
+ bookmarkGuids.toolbarGuid,
+ bookmarkGuids.menuGuid,
+ 1,
+ 0
+ );
+
+ createdBookmarks.add(corporationBookmark.id);
+ });
+ })
+ .then(() => {
+ return browser.bookmarks.getRecent(4);
+ })
+ .then(results => {
+ browser.test.assertEq(
+ 4,
+ results.length,
+ "Expected number of results returned by getRecent"
+ );
+ let prevDate = results[0].dateAdded;
+ for (let bookmark of results) {
+ browser.test.assertTrue(
+ bookmark.dateAdded <= prevDate,
+ "The recent bookmarks are sorted by dateAdded"
+ );
+ prevDate = bookmark.dateAdded;
+ }
+ let bookmarksByTitle = results.sort((a, b) => {
+ return a.title.localeCompare(b.title);
+ });
+ browser.test.assertEq(
+ "About Mozilla",
+ bookmarksByTitle[0].title,
+ "Bookmark has the expected title"
+ );
+ browser.test.assertEq(
+ "Firefox",
+ bookmarksByTitle[1].title,
+ "Bookmark has the expected title"
+ );
+ browser.test.assertEq(
+ "Mozilla",
+ bookmarksByTitle[2].title,
+ "Bookmark has the expected title"
+ );
+ browser.test.assertEq(
+ "Mozilla Corporation",
+ bookmarksByTitle[3].title,
+ "Bookmark has the expected title"
+ );
+
+ return browser.bookmarks.search({});
+ })
+ .then(results => {
+ let startBookmarkCount = results.length;
+
+ return browser.bookmarks
+ .search({ title: "Mozilla Folder" })
+ .then(result => {
+ return browser.bookmarks.removeTree(result[0].id);
+ })
+ .then(() => {
+ browser.test.assertEq(
+ 1,
+ collectedEvents.length,
+ "1 expected events received"
+ );
+ checkOnRemoved(createdFolderId, bookmarkGuids.unfiledGuid, 1);
+
+ return browser.bookmarks.search({}).then(searchResults => {
+ browser.test.assertEq(
+ startBookmarkCount - 5,
+ searchResults.length,
+ "Expected number of results returned after removeTree"
+ );
+ });
+ });
+ })
+ .then(() => {
+ return browser.bookmarks.create({ title: "Empty Folder" });
+ })
+ .then(result => {
+ createdFolderId = result.id;
+
+ browser.test.assertEq(
+ 1,
+ collectedEvents.length,
+ "1 expected events received"
+ );
+ checkOnCreated(
+ createdFolderId,
+ bookmarkGuids.unfiledGuid,
+ 3,
+ "Empty Folder",
+ undefined,
+ result.dateAdded,
+ "folder"
+ );
+
+ browser.test.assertEq(
+ "Empty Folder",
+ result.title,
+ "Folder has the expected title"
+ );
+ browser.test.assertEq(
+ "folder",
+ result.type,
+ "Folder has the expected type"
+ );
+
+ return browser.bookmarks.create({
+ parentId: createdFolderId,
+ type: "separator",
+ });
+ })
+ .then(result => {
+ createdSeparatorId = result.id;
+ browser.test.assertEq(
+ 1,
+ collectedEvents.length,
+ "1 expected events received"
+ );
+ checkOnCreated(
+ createdSeparatorId,
+ createdFolderId,
+ 0,
+ "",
+ "data:",
+ result.dateAdded,
+ "separator"
+ );
+ return browser.bookmarks.remove(createdSeparatorId);
+ })
+ .then(() => {
+ browser.test.assertEq(
+ 1,
+ collectedEvents.length,
+ "1 expected events received"
+ );
+ checkOnRemoved(
+ createdSeparatorId,
+ createdFolderId,
+ 0,
+ "data:",
+ "separator"
+ );
+
+ return browser.bookmarks.remove(createdFolderId);
+ })
+ .then(() => {
+ browser.test.assertEq(
+ 1,
+ collectedEvents.length,
+ "1 expected events received"
+ );
+ checkOnRemoved(createdFolderId, bookmarkGuids.unfiledGuid, 3);
+
+ return browser.test.assertRejects(
+ browser.bookmarks.get(createdFolderId),
+ /Bookmark not found/,
+ "Expected error thrown when trying to get a removed folder"
+ );
+ })
+ .then(() => {
+ return browser.test.assertRejects(
+ browser.bookmarks.getChildren(nonExistentId),
+ /root is null/,
+ "Expected error thrown when trying to getChildren for a non-existent folder"
+ );
+ })
+ .then(() => {
+ return browser.test.assertRejects(
+ browser.bookmarks.move(nonExistentId, {}),
+ /No bookmarks found for the provided GUID/,
+ "Expected error thrown when calling move with a non-existent bookmark"
+ );
+ })
+ .then(() => {
+ return browser.test.assertRejects(
+ browser.bookmarks.create({
+ title: "test root folder",
+ parentId: bookmarkGuids.rootGuid,
+ }),
+ "The bookmark root cannot be modified",
+ "Expected error thrown when creating bookmark folder at the root"
+ );
+ })
+ .then(() => {
+ return browser.test.assertRejects(
+ browser.bookmarks.update(bookmarkGuids.rootGuid, {
+ title: "test update title",
+ }),
+ "The bookmark root cannot be modified",
+ "Expected error thrown when updating root"
+ );
+ })
+ .then(() => {
+ return browser.test.assertRejects(
+ browser.bookmarks.remove(bookmarkGuids.rootGuid),
+ "The bookmark root cannot be modified",
+ "Expected error thrown when removing root"
+ );
+ })
+ .then(() => {
+ return browser.test.assertRejects(
+ browser.bookmarks.removeTree(bookmarkGuids.rootGuid),
+ "The bookmark root cannot be modified",
+ "Expected error thrown when removing root tree"
+ );
+ })
+ .then(() => {
+ return browser.bookmarks.create({ title: "Empty Folder" });
+ })
+ .then(async result => {
+ createdFolderId = result.id;
+
+ browser.test.assertEq(
+ 1,
+ collectedEvents.length,
+ "1 expected events received"
+ );
+ checkOnCreated(
+ createdFolderId,
+ bookmarkGuids.unfiledGuid,
+ 3,
+ "Empty Folder",
+ undefined,
+ result.dateAdded,
+ "folder"
+ );
+
+ await browser.test.assertRejects(
+ browser.bookmarks.move(createdFolderId, {
+ parentId: bookmarkGuids.rootGuid,
+ }),
+ "The bookmark root cannot be modified",
+ "Expected error thrown when moving bookmark folder to the root"
+ );
+
+ return browser.bookmarks.remove(createdFolderId);
+ })
+ .then(() => {
+ browser.test.assertEq(
+ 1,
+ collectedEvents.length,
+ "1 expected events received"
+ );
+ checkOnRemoved(
+ createdFolderId,
+ bookmarkGuids.unfiledGuid,
+ 3,
+ undefined,
+ "folder"
+ );
+
+ return browser.test.assertRejects(
+ browser.bookmarks.get(createdFolderId),
+ "Bookmark not found",
+ "Expected error thrown when trying to get a removed folder"
+ );
+ })
+ .then(() => {
+ return browser.test.assertRejects(
+ browser.bookmarks.move(bookmarkGuids.rootGuid, {
+ parentId: bookmarkGuids.unfiledGuid,
+ }),
+ "The bookmark root cannot be modified",
+ "Expected error thrown when moving root"
+ );
+ })
+ .then(() => {
+ // remove all created bookmarks
+ let promises = Array.from(createdBookmarks, guid =>
+ browser.bookmarks.remove(guid)
+ );
+ return Promise.all(promises);
+ })
+ .then(() => {
+ browser.test.assertEq(
+ createdBookmarks.size,
+ collectedEvents.length,
+ "expected number of events received"
+ );
+
+ return browser.bookmarks.search({});
+ })
+ .then(results => {
+ browser.test.assertEq(
+ initialBookmarkCount,
+ results.length,
+ "All created bookmarks have been removed"
+ );
+
+ return browser.test.notifyPass("bookmarks");
+ })
+ .catch(error => {
+ browser.test.fail(`Error: ${String(error)} :: ${error.stack}`);
+ browser.test.notifyFail("bookmarks");
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["bookmarks"],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitFinish("bookmarks");
+ await extension.unload();
+});
+
+add_task(async function test_get_recent_with_tag_and_query() {
+ function background() {
+ browser.bookmarks.getRecent(100).then(bookmarks => {
+ browser.test.sendMessage("bookmarks", bookmarks);
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["bookmarks"],
+ },
+ });
+
+ // Start with an empty bookmarks database.
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ let createdBookmarks = [];
+ for (let i = 0; i < 3; i++) {
+ let bookmark = {
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: `http://example.com/${i}`,
+ title: `My bookmark ${i}`,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ };
+ createdBookmarks.unshift(bookmark);
+ await PlacesUtils.bookmarks.insert(bookmark);
+ }
+
+ // Add a tag to the most recent url to prove it doesn't get returned.
+ PlacesUtils.tagging.tagURI(NetUtil.newURI("http://example.com/${i}"), [
+ "Test Tag",
+ ]);
+
+ // Add a query bookmark.
+ let queryURL = `place:parent=${PlacesUtils.bookmarks.menuGuid}&queryType=1`;
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: queryURL,
+ title: "a test query",
+ });
+
+ await extension.startup();
+ let receivedBookmarks = await extension.awaitMessage("bookmarks");
+
+ equal(
+ receivedBookmarks.length,
+ 3,
+ "The expected number of bookmarks was returned."
+ );
+ for (let i = 0; i < 3; i++) {
+ let actual = receivedBookmarks[i];
+ let expected = createdBookmarks[i];
+ equal(actual.url, expected.url, "Bookmark has the expected url.");
+ equal(actual.title, expected.title, "Bookmark has the expected title.");
+ equal(
+ actual.parentId,
+ expected.parentGuid,
+ "Bookmark has the expected parentId."
+ );
+ }
+
+ await extension.unload();
+});
+
+add_task(async function test_tree_with_empty_folder() {
+ async function background() {
+ await browser.bookmarks.create({ title: "Empty Folder" });
+ let nonEmptyFolder = await browser.bookmarks.create({
+ title: "Non-Empty Folder",
+ });
+ await browser.bookmarks.create({
+ title: "A bookmark",
+ url: "http://example.com",
+ parentId: nonEmptyFolder.id,
+ });
+
+ let tree = await browser.bookmarks.getSubTree(nonEmptyFolder.parentId);
+ browser.test.assertEq(
+ 0,
+ tree[0].children[0].children.length,
+ "The empty folder returns an empty array for children."
+ );
+ browser.test.assertEq(
+ 1,
+ tree[0].children[1].children.length,
+ "The non-empty folder returns a single item array for children."
+ );
+
+ let children = await browser.bookmarks.getChildren(nonEmptyFolder.parentId);
+ // getChildren should only return immediate children. This is not tested in the
+ // monster test above.
+ for (let child of children) {
+ browser.test.assertEq(
+ undefined,
+ child.children,
+ "Child from getChildren does not contain any children."
+ );
+ }
+
+ browser.test.sendMessage("done");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["bookmarks"],
+ },
+ });
+
+ // Start with an empty bookmarks database.
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ await extension.startup();
+ await extension.awaitMessage("done");
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/xpcshell/test_ext_browsingData_downloads.js b/browser/components/extensions/test/xpcshell/test_ext_browsingData_downloads.js
new file mode 100644
index 0000000000..fa559e53c9
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_browsingData_downloads.js
@@ -0,0 +1,128 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "Downloads",
+ "resource://gre/modules/Downloads.jsm"
+);
+
+const OLD_NAMES = {
+ [Downloads.PUBLIC]: "old-public",
+ [Downloads.PRIVATE]: "old-private",
+};
+const RECENT_NAMES = {
+ [Downloads.PUBLIC]: "recent-public",
+ [Downloads.PRIVATE]: "recent-private",
+};
+const REFERENCE_DATE = new Date();
+const OLD_DATE = new Date(Number(REFERENCE_DATE) - 10000);
+
+async function downloadExists(list, path) {
+ let listArray = await list.getAll();
+ return listArray.some(i => i.target.path == path);
+}
+
+async function checkDownloads(
+ expectOldExists = true,
+ expectRecentExists = true
+) {
+ for (let listType of [Downloads.PUBLIC, Downloads.PRIVATE]) {
+ let downloadsList = await Downloads.getList(listType);
+ equal(
+ await downloadExists(downloadsList, OLD_NAMES[listType]),
+ expectOldExists,
+ `Fake old download ${expectOldExists ? "was found" : "was removed"}.`
+ );
+ equal(
+ await downloadExists(downloadsList, RECENT_NAMES[listType]),
+ expectRecentExists,
+ `Fake recent download ${
+ expectRecentExists ? "was found" : "was removed"
+ }.`
+ );
+ }
+}
+
+async function setupDownloads() {
+ let downloadsList = await Downloads.getList(Downloads.ALL);
+ await downloadsList.removeFinished();
+
+ for (let listType of [Downloads.PUBLIC, Downloads.PRIVATE]) {
+ downloadsList = await Downloads.getList(listType);
+ let download = await Downloads.createDownload({
+ source: {
+ url: "https://bugzilla.mozilla.org/show_bug.cgi?id=1321303",
+ isPrivate: listType == Downloads.PRIVATE,
+ },
+ target: OLD_NAMES[listType],
+ });
+ download.startTime = OLD_DATE;
+ download.canceled = true;
+ await downloadsList.add(download);
+
+ download = await Downloads.createDownload({
+ source: {
+ url: "https://bugzilla.mozilla.org/show_bug.cgi?id=1321303",
+ isPrivate: listType == Downloads.PRIVATE,
+ },
+ target: RECENT_NAMES[listType],
+ });
+ download.startTime = REFERENCE_DATE;
+ download.canceled = true;
+ await downloadsList.add(download);
+ }
+
+ // Confirm everything worked.
+ downloadsList = await Downloads.getList(Downloads.ALL);
+ equal((await downloadsList.getAll()).length, 4, "4 fake downloads added.");
+ checkDownloads();
+}
+
+add_task(async function testDownloads() {
+ function background() {
+ browser.test.onMessage.addListener(async (msg, options) => {
+ if (msg == "removeDownloads") {
+ await browser.browsingData.removeDownloads(options);
+ } else {
+ await browser.browsingData.remove(options, { downloads: true });
+ }
+ browser.test.sendMessage("downloadsRemoved");
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["browsingData"],
+ },
+ });
+
+ async function testRemovalMethod(method) {
+ // Clear downloads with no since value.
+ await setupDownloads();
+ extension.sendMessage(method, {});
+ await extension.awaitMessage("downloadsRemoved");
+ await checkDownloads(false, false);
+
+ // Clear downloads with recent since value.
+ await setupDownloads();
+ extension.sendMessage(method, { since: REFERENCE_DATE });
+ await extension.awaitMessage("downloadsRemoved");
+ await checkDownloads(true, false);
+
+ // Clear downloads with old since value.
+ await setupDownloads();
+ extension.sendMessage(method, { since: REFERENCE_DATE - 100000 });
+ await extension.awaitMessage("downloadsRemoved");
+ await checkDownloads(false, false);
+ }
+
+ await extension.startup();
+
+ await testRemovalMethod("removeDownloads");
+ await testRemovalMethod("remove");
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/xpcshell/test_ext_browsingData_passwords.js b/browser/components/extensions/test/xpcshell/test_ext_browsingData_passwords.js
new file mode 100644
index 0000000000..5fab4f73b7
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_browsingData_passwords.js
@@ -0,0 +1,101 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "loginManager",
+ "@mozilla.org/login-manager;1",
+ "nsILoginManager"
+);
+
+const REFERENCE_DATE = Date.now();
+const LOGIN_USERNAME = "username";
+const LOGIN_PASSWORD = "password";
+const OLD_HOST = "http://mozilla.org";
+const NEW_HOST = "http://mozilla.com";
+const FXA_HOST = "chrome://FirefoxAccounts";
+
+function checkLoginExists(host, shouldExist) {
+ let logins = loginManager.findLogins(host, "", null);
+ equal(
+ logins.length,
+ shouldExist ? 1 : 0,
+ `Login was ${shouldExist ? "" : "not "} found.`
+ );
+}
+
+function addLogin(host, timestamp) {
+ checkLoginExists(host, false);
+ let login = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(
+ Ci.nsILoginInfo
+ );
+ login.init(host, "", null, LOGIN_USERNAME, LOGIN_PASSWORD);
+ login.QueryInterface(Ci.nsILoginMetaInfo);
+ login.timePasswordChanged = timestamp;
+ loginManager.addLogin(login);
+ checkLoginExists(host, true);
+}
+
+async function setupPasswords() {
+ loginManager.removeAllUserFacingLogins();
+ addLogin(FXA_HOST, REFERENCE_DATE);
+ addLogin(NEW_HOST, REFERENCE_DATE);
+ addLogin(OLD_HOST, REFERENCE_DATE - 10000);
+}
+
+add_task(async function testPasswords() {
+ function background() {
+ browser.test.onMessage.addListener(async (msg, options) => {
+ if (msg == "removeHistory") {
+ await browser.browsingData.removePasswords(options);
+ } else {
+ await browser.browsingData.remove(options, { passwords: true });
+ }
+ browser.test.sendMessage("passwordsRemoved");
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["browsingData"],
+ },
+ });
+
+ async function testRemovalMethod(method) {
+ // Clear passwords with no since value.
+ await setupPasswords();
+ extension.sendMessage(method, {});
+ await extension.awaitMessage("passwordsRemoved");
+
+ checkLoginExists(OLD_HOST, false);
+ checkLoginExists(NEW_HOST, false);
+ checkLoginExists(FXA_HOST, true);
+
+ // Clear passwords with recent since value.
+ await setupPasswords();
+ extension.sendMessage(method, { since: REFERENCE_DATE - 1000 });
+ await extension.awaitMessage("passwordsRemoved");
+
+ checkLoginExists(OLD_HOST, true);
+ checkLoginExists(NEW_HOST, false);
+ checkLoginExists(FXA_HOST, true);
+
+ // Clear passwords with old since value.
+ await setupPasswords();
+ extension.sendMessage(method, { since: REFERENCE_DATE - 20000 });
+ await extension.awaitMessage("passwordsRemoved");
+
+ checkLoginExists(OLD_HOST, false);
+ checkLoginExists(NEW_HOST, false);
+ checkLoginExists(FXA_HOST, true);
+ }
+
+ await extension.startup();
+
+ await testRemovalMethod("removePasswords");
+ await testRemovalMethod("remove");
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/xpcshell/test_ext_browsingData_settings.js b/browser/components/extensions/test/xpcshell/test_ext_browsingData_settings.js
new file mode 100644
index 0000000000..649f8644fe
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_browsingData_settings.js
@@ -0,0 +1,152 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "Preferences",
+ "resource://gre/modules/Preferences.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "Sanitizer",
+ "resource:///modules/Sanitizer.jsm"
+);
+
+const PREF_DOMAIN = "privacy.cpd.";
+const SETTINGS_LIST = [
+ "cache",
+ "cookies",
+ "history",
+ "formData",
+ "downloads",
+].sort();
+
+add_task(async function testSettingsProperties() {
+ function background() {
+ browser.test.onMessage.addListener(msg => {
+ browser.browsingData.settings().then(settings => {
+ browser.test.sendMessage("settings", settings);
+ });
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["browsingData"],
+ },
+ });
+
+ await extension.startup();
+
+ extension.sendMessage("settings");
+ let settings = await extension.awaitMessage("settings");
+
+ // Verify that we get the keys back we expect.
+ deepEqual(
+ Object.keys(settings.dataToRemove).sort(),
+ SETTINGS_LIST,
+ "dataToRemove contains expected properties."
+ );
+ deepEqual(
+ Object.keys(settings.dataRemovalPermitted).sort(),
+ SETTINGS_LIST,
+ "dataToRemove contains expected properties."
+ );
+
+ let dataTypeSet = settings.dataToRemove;
+ for (let key of Object.keys(dataTypeSet)) {
+ equal(
+ Preferences.get(`${PREF_DOMAIN}${key.toLowerCase()}`),
+ dataTypeSet[key],
+ `${key} property of dataToRemove matches the expected pref.`
+ );
+ }
+
+ dataTypeSet = settings.dataRemovalPermitted;
+ for (let key of Object.keys(dataTypeSet)) {
+ equal(
+ true,
+ dataTypeSet[key],
+ `${key} property of dataRemovalPermitted is true.`
+ );
+ }
+
+ // Explicitly set a pref to both true and false and then check.
+ const SINGLE_OPTION = "cache";
+ const SINGLE_PREF = "privacy.cpd.cache";
+
+ registerCleanupFunction(() => {
+ Preferences.reset(SINGLE_PREF);
+ });
+
+ Preferences.set(SINGLE_PREF, true);
+
+ extension.sendMessage("settings");
+ settings = await extension.awaitMessage("settings");
+ equal(
+ settings.dataToRemove[SINGLE_OPTION],
+ true,
+ "Preference that was set to true returns true."
+ );
+
+ Preferences.set(SINGLE_PREF, false);
+
+ extension.sendMessage("settings");
+ settings = await extension.awaitMessage("settings");
+ equal(
+ settings.dataToRemove[SINGLE_OPTION],
+ false,
+ "Preference that was set to false returns false."
+ );
+
+ await extension.unload();
+});
+
+add_task(async function testSettingsSince() {
+ const TIMESPAN_PREF = "privacy.sanitize.timeSpan";
+ const TEST_DATA = {
+ TIMESPAN_5MIN: Date.now() - 5 * 60 * 1000,
+ TIMESPAN_HOUR: Date.now() - 60 * 60 * 1000,
+ TIMESPAN_2HOURS: Date.now() - 2 * 60 * 60 * 1000,
+ TIMESPAN_EVERYTHING: 0,
+ };
+
+ function background() {
+ browser.test.onMessage.addListener(msg => {
+ browser.browsingData.settings().then(settings => {
+ browser.test.sendMessage("settings", settings);
+ });
+ });
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["browsingData"],
+ },
+ });
+
+ await extension.startup();
+
+ registerCleanupFunction(() => {
+ Preferences.reset(TIMESPAN_PREF);
+ });
+
+ for (let timespan in TEST_DATA) {
+ Preferences.set(TIMESPAN_PREF, Sanitizer[timespan]);
+
+ extension.sendMessage("settings");
+ let settings = await extension.awaitMessage("settings");
+
+ // Because it is based on the current timestamp, we cannot know the exact
+ // value to expect for since, so allow a 10s variance.
+ ok(
+ Math.abs(settings.options.since - TEST_DATA[timespan]) < 10000,
+ "settings.options contains the expected since value."
+ );
+ }
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/xpcshell/test_ext_chrome_settings_overrides_home.js b/browser/components/extensions/test/xpcshell/test_ext_chrome_settings_overrides_home.js
new file mode 100644
index 0000000000..4734af5ad1
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_chrome_settings_overrides_home.js
@@ -0,0 +1,156 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.import(
+ "resource://testing-common/AddonTestUtils.jsm"
+);
+XPCOMUtils.defineLazyModuleGetters(this, {
+ HomePage: "resource:///modules/HomePage.jsm",
+ RemoteSettings: "resource://services-settings/remote-settings.js",
+ sinon: "resource://testing-common/Sinon.jsm",
+ TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.jsm",
+});
+
+const HOMEPAGE_EXTENSION_CONTROLLED =
+ "browser.startup.homepage_override.extensionControlled";
+
+AddonTestUtils.init(this);
+AddonTestUtils.overrideCertDB();
+
+AddonTestUtils.createAppInfo(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "1",
+ "42"
+);
+
+async function setupRemoteSettings() {
+ const settings = await RemoteSettings("hijack-blocklists");
+ sinon.stub(settings, "get").returns([
+ {
+ id: "homepage-urls",
+ matches: ["ignore=me"],
+ _status: "synced",
+ },
+ ]);
+}
+
+add_task(async function setup() {
+ await AddonTestUtils.promiseStartupManager();
+ await setupRemoteSettings();
+});
+
+add_task(async function test_overriding_with_ignored_url() {
+ // Manually poke into the ignore list a value to be ignored.
+ HomePage._ignoreList.push("ignore=me");
+ Services.prefs.setBoolPref(HOMEPAGE_EXTENSION_CONTROLLED, false);
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: "ignore_homepage@example.com",
+ },
+ },
+ chrome_settings_overrides: { homepage: "https://example.com/?ignore=me" },
+ name: "extension",
+ },
+ useAddonManager: "temporary",
+ });
+
+ await extension.startup();
+
+ Assert.ok(HomePage.isDefault, "Should still have the default homepage");
+ Assert.equal(
+ Services.prefs.getBoolPref(
+ "browser.startup.homepage_override.extensionControlled"
+ ),
+ false,
+ "Should not be extension controlled."
+ );
+ TelemetryTestUtils.assertEvents(
+ [
+ {
+ object: "ignore",
+ value: "set_blocked_extension",
+ extra: { webExtensionId: "ignore_homepage@example.com" },
+ },
+ ],
+ {
+ category: "homepage",
+ method: "preference",
+ }
+ );
+
+ await extension.unload();
+ HomePage._ignoreList.pop();
+});
+
+add_task(async function test_overriding_cancelled_after_ignore_update() {
+ const oldHomePageIgnoreList = HomePage._ignoreList;
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: "ignore_homepage1@example.com",
+ },
+ },
+ chrome_settings_overrides: {
+ homepage: "https://example.com/?ignore1=me",
+ },
+ name: "extension",
+ },
+ useAddonManager: "temporary",
+ });
+
+ await extension.startup();
+
+ Assert.ok(!HomePage.isDefault, "Should have overriden the new homepage");
+ Assert.equal(
+ Services.prefs.getBoolPref(
+ "browser.startup.homepage_override.extensionControlled"
+ ),
+ true,
+ "Should be extension controlled."
+ );
+
+ let prefChanged = TestUtils.waitForPrefChange(
+ "browser.startup.homepage_override.extensionControlled"
+ );
+
+ await HomePage._handleIgnoreListUpdated({
+ data: {
+ current: [{ id: "homepage-urls", matches: ["ignore1=me"] }],
+ },
+ });
+
+ await prefChanged;
+
+ await TestUtils.waitForCondition(
+ () =>
+ !Services.prefs.getBoolPref(
+ "browser.startup.homepage_override.extensionControlled",
+ false
+ ),
+ "Should not longer be extension controlled"
+ );
+
+ Assert.ok(HomePage.isDefault, "Should have reset the homepage");
+
+ TelemetryTestUtils.assertEvents(
+ [
+ {
+ object: "ignore",
+ value: "saved_reset",
+ },
+ ],
+ {
+ category: "homepage",
+ method: "preference",
+ }
+ );
+
+ await extension.unload();
+ HomePage._ignoreList = oldHomePageIgnoreList;
+});
diff --git a/browser/components/extensions/test/xpcshell/test_ext_chrome_settings_overrides_update.js b/browser/components/extensions/test/xpcshell/test_ext_chrome_settings_overrides_update.js
new file mode 100644
index 0000000000..f1dfe94dd7
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_chrome_settings_overrides_update.js
@@ -0,0 +1,228 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.import(
+ "resource://testing-common/AddonTestUtils.jsm"
+);
+XPCOMUtils.defineLazyModuleGetters(this, {
+ HomePage: "resource:///modules/HomePage.jsm",
+ RemoteSettings: "resource://services-settings/remote-settings.js",
+ sinon: "resource://testing-common/Sinon.jsm",
+});
+
+AddonTestUtils.init(this);
+AddonTestUtils.overrideCertDB();
+
+AddonTestUtils.createAppInfo(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "1",
+ "42"
+);
+// Override ExtensionXPCShellUtils.jsm's overriding of the pref as the
+// search service needs it.
+Services.prefs.clearUserPref("services.settings.default_bucket");
+
+async function setupRemoteSettings() {
+ const settings = await RemoteSettings("hijack-blocklists");
+ sinon.stub(settings, "get").returns([
+ {
+ id: "homepage-urls",
+ matches: ["ignore=me"],
+ _status: "synced",
+ },
+ ]);
+}
+
+add_task(async function setup() {
+ await AddonTestUtils.promiseStartupManager();
+ await setupRemoteSettings();
+});
+
+add_task(async function test_overrides_update_removal() {
+ /* This tests the scenario where the manifest key for homepage and/or
+ * search_provider are removed between updates and therefore the
+ * settings are expected to revert. It also tests that an extension
+ * can make a builtin extension the default extension without user
+ * interaction. */
+
+ const EXTENSION_ID = "test_overrides_update@tests.mozilla.org";
+ const HOMEPAGE_URI = "webext-homepage-1.html";
+
+ const HOMEPAGE_URL_PREF = "browser.startup.homepage";
+
+ function promisePrefChanged(value) {
+ return new Promise((resolve, reject) => {
+ Services.prefs.addObserver(HOMEPAGE_URL_PREF, function observer() {
+ if (HomePage.get().endsWith(value)) {
+ Services.prefs.removeObserver(HOMEPAGE_URL_PREF, observer);
+ resolve();
+ }
+ });
+ });
+ }
+
+ let extensionInfo = {
+ useAddonManager: "permanent",
+ manifest: {
+ version: "1.0",
+ applications: {
+ gecko: {
+ id: EXTENSION_ID,
+ },
+ },
+ chrome_settings_overrides: {
+ homepage: HOMEPAGE_URI,
+ search_provider: {
+ name: "DuckDuckGo",
+ search_url: "https://example.com/?q={searchTerms}",
+ is_default: true,
+ },
+ },
+ },
+ };
+ let extension = ExtensionTestUtils.loadExtension(extensionInfo);
+
+ let defaultHomepageURL = HomePage.get();
+ let defaultEngineName = (await Services.search.getDefault()).name;
+ ok(defaultEngineName !== "DuckDuckGo", "Default engine is not DuckDuckGo.");
+
+ let prefPromise = promisePrefChanged(HOMEPAGE_URI);
+ await extension.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(extension);
+ await prefPromise;
+
+ equal(
+ extension.version,
+ "1.0",
+ "The installed addon has the expected version."
+ );
+ ok(
+ HomePage.get().endsWith(HOMEPAGE_URI),
+ "Home page url is overridden by the extension."
+ );
+ equal(
+ (await Services.search.getDefault()).name,
+ "DuckDuckGo",
+ "Builtin default engine was set default by extension"
+ );
+
+ extensionInfo.manifest = {
+ version: "2.0",
+ applications: {
+ gecko: {
+ id: EXTENSION_ID,
+ },
+ },
+ };
+
+ prefPromise = promisePrefChanged(defaultHomepageURL);
+ await extension.upgrade(extensionInfo);
+ await prefPromise;
+
+ equal(
+ extension.version,
+ "2.0",
+ "The updated addon has the expected version."
+ );
+ equal(
+ HomePage.get(),
+ defaultHomepageURL,
+ "Home page url reverted to the default after update."
+ );
+ equal(
+ (await Services.search.getDefault()).name,
+ defaultEngineName,
+ "Default engine reverted to the default after update."
+ );
+
+ await extension.unload();
+});
+
+add_task(async function test_overrides_update_adding() {
+ /* This tests the scenario where an addon adds support for
+ * a homepage or search service when upgrading. Neither
+ * should override existing entries for those when added
+ * in an upgrade. Also, a search_provider being added
+ * with is_default should not prompt the user or override
+ * the current default engine. */
+
+ const EXTENSION_ID = "test_overrides_update@tests.mozilla.org";
+ const HOMEPAGE_URI = "webext-homepage-1.html";
+
+ let extensionInfo = {
+ useAddonManager: "permanent",
+ manifest: {
+ version: "1.0",
+ applications: {
+ gecko: {
+ id: EXTENSION_ID,
+ },
+ },
+ },
+ };
+ let extension = ExtensionTestUtils.loadExtension(extensionInfo);
+
+ let defaultHomepageURL = HomePage.get();
+ let defaultEngineName = (await Services.search.getDefault()).name;
+ ok(defaultEngineName !== "DuckDuckGo", "Home page url is not DuckDuckGo.");
+
+ await extension.startup();
+
+ equal(
+ extension.version,
+ "1.0",
+ "The installed addon has the expected version."
+ );
+ equal(
+ HomePage.get(),
+ defaultHomepageURL,
+ "Home page url is the default after startup."
+ );
+ equal(
+ (await Services.search.getDefault()).name,
+ defaultEngineName,
+ "Default engine is the default after startup."
+ );
+
+ extensionInfo.manifest = {
+ version: "2.0",
+ applications: {
+ gecko: {
+ id: EXTENSION_ID,
+ },
+ },
+ chrome_settings_overrides: {
+ homepage: HOMEPAGE_URI,
+ search_provider: {
+ name: "DuckDuckGo",
+ search_url: "https://example.com/?q={searchTerms}",
+ is_default: true,
+ },
+ },
+ };
+
+ await extension.upgrade(extensionInfo);
+ await AddonTestUtils.waitForSearchProviderStartup(extension);
+
+ equal(
+ extension.version,
+ "2.0",
+ "The updated addon has the expected version."
+ );
+ equal(
+ HomePage.get(),
+ defaultHomepageURL,
+ "Home page url is not overridden by the extension during upgrade."
+ );
+ // An upgraded extension adding a search engine cannot override
+ // the default engine.
+ equal(
+ (await Services.search.getDefault()).name,
+ defaultEngineName,
+ "Default engine is still the default after startup."
+ );
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/xpcshell/test_ext_distribution_popup.js b/browser/components/extensions/test/xpcshell/test_ext_distribution_popup.js
new file mode 100644
index 0000000000..80bfa75380
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_distribution_popup.js
@@ -0,0 +1,60 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+
+"use strict";
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "ExtensionSettingsStore",
+ "resource://gre/modules/ExtensionSettingsStore.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "ExtensionControlledPopup",
+ "resource:///modules/ExtensionControlledPopup.jsm"
+);
+
+/*
+ * This function is a unit test for distributions disabling the ExtensionControlledPopup.
+ */
+add_task(async function testDistributionPopup() {
+ let distExtId = "ext-distribution@mochi.test";
+ Services.prefs.setCharPref(
+ `extensions.installedDistroAddon.${distExtId}`,
+ true
+ );
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ applications: { gecko: { id: distExtId } },
+ name: "Ext Distribution",
+ },
+ });
+
+ let userExtId = "ext-user@mochi.test";
+ let userExtension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ applications: { gecko: { id: userExtId } },
+ name: "Ext User Installed",
+ },
+ });
+
+ await extension.startup();
+ await userExtension.startup();
+ await ExtensionSettingsStore.initialize();
+
+ let confirmedType = "extension-controlled-confirmed";
+ equal(
+ new ExtensionControlledPopup({ confirmedType }).userHasConfirmed(distExtId),
+ true,
+ "The popup has been disabled."
+ );
+
+ equal(
+ new ExtensionControlledPopup({ confirmedType }).userHasConfirmed(userExtId),
+ false,
+ "The popup has not been disabled."
+ );
+
+ await extension.unload();
+ await userExtension.unload();
+});
diff --git a/browser/components/extensions/test/xpcshell/test_ext_history.js b/browser/components/extensions/test/xpcshell/test_ext_history.js
new file mode 100644
index 0000000000..b3f4532eb1
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_history.js
@@ -0,0 +1,791 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "PlacesTestUtils",
+ "resource://testing-common/PlacesTestUtils.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "ExtensionCommon",
+ "resource://gre/modules/ExtensionCommon.jsm"
+);
+
+add_task(async function test_delete() {
+ function background() {
+ let historyClearedCount = 0;
+ let removedUrls = [];
+
+ browser.history.onVisitRemoved.addListener(data => {
+ if (data.allHistory) {
+ historyClearedCount++;
+ browser.test.assertEq(
+ 0,
+ data.urls.length,
+ "onVisitRemoved received an empty urls array"
+ );
+ } else {
+ browser.test.assertEq(
+ 1,
+ data.urls.length,
+ "onVisitRemoved received one URL"
+ );
+ removedUrls.push(data.urls[0]);
+ }
+ });
+
+ browser.test.onMessage.addListener((msg, arg) => {
+ if (msg === "delete-url") {
+ browser.history.deleteUrl({ url: arg }).then(result => {
+ browser.test.assertEq(
+ undefined,
+ result,
+ "browser.history.deleteUrl returns nothing"
+ );
+ browser.test.sendMessage("url-deleted");
+ });
+ } else if (msg === "delete-range") {
+ browser.history.deleteRange(arg).then(result => {
+ browser.test.assertEq(
+ undefined,
+ result,
+ "browser.history.deleteRange returns nothing"
+ );
+ browser.test.sendMessage("range-deleted", removedUrls);
+ });
+ } else if (msg === "delete-all") {
+ browser.history.deleteAll().then(result => {
+ browser.test.assertEq(
+ undefined,
+ result,
+ "browser.history.deleteAll returns nothing"
+ );
+ browser.test.sendMessage("history-cleared", [
+ historyClearedCount,
+ removedUrls,
+ ]);
+ });
+ }
+ });
+
+ browser.test.sendMessage("ready");
+ }
+
+ const BASE_URL = "http://mozilla.com/test_history/";
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["history"],
+ },
+ background: `(${background})()`,
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+ await PlacesUtils.history.clear();
+
+ let historyClearedCount;
+ let visits = [];
+ let visitDate = new Date(1999, 9, 9, 9, 9).getTime();
+
+ function pushVisit(subvisits) {
+ visitDate += 1000;
+ subvisits.push({ date: new Date(visitDate) });
+ }
+
+ // Add 5 visits for one uri and 3 visits for 3 others
+ for (let i = 0; i < 4; ++i) {
+ let visit = {
+ url: `${BASE_URL}${i}`,
+ title: "visit " + i,
+ visits: [],
+ };
+ if (i === 0) {
+ for (let j = 0; j < 5; ++j) {
+ pushVisit(visit.visits);
+ }
+ } else {
+ pushVisit(visit.visits);
+ }
+ visits.push(visit);
+ }
+
+ await PlacesUtils.history.insertMany(visits);
+ equal(
+ await PlacesTestUtils.visitsInDB(visits[0].url),
+ 5,
+ "5 visits for uri found in history database"
+ );
+
+ let testUrl = visits[2].url;
+ ok(
+ await PlacesTestUtils.isPageInDB(testUrl),
+ "expected url found in history database"
+ );
+
+ extension.sendMessage("delete-url", testUrl);
+ await extension.awaitMessage("url-deleted");
+ equal(
+ await PlacesTestUtils.isPageInDB(testUrl),
+ false,
+ "expected url not found in history database"
+ );
+
+ // delete 3 of the 5 visits for url 1
+ let filter = {
+ startTime: visits[0].visits[0].date,
+ endTime: visits[0].visits[2].date,
+ };
+
+ extension.sendMessage("delete-range", filter);
+ let removedUrls = await extension.awaitMessage("range-deleted");
+ ok(
+ !removedUrls.includes(visits[0].url),
+ `${visits[0].url} not received by onVisitRemoved`
+ );
+ ok(
+ await PlacesTestUtils.isPageInDB(visits[0].url),
+ "expected uri found in history database"
+ );
+ equal(
+ await PlacesTestUtils.visitsInDB(visits[0].url),
+ 2,
+ "2 visits for uri found in history database"
+ );
+ ok(
+ await PlacesTestUtils.isPageInDB(visits[1].url),
+ "expected uri found in history database"
+ );
+ equal(
+ await PlacesTestUtils.visitsInDB(visits[1].url),
+ 1,
+ "1 visit for uri found in history database"
+ );
+
+ // delete the rest of the visits for url 1, and the visit for url 2
+ filter.startTime = visits[0].visits[0].date;
+ filter.endTime = visits[1].visits[0].date;
+
+ extension.sendMessage("delete-range", filter);
+ await extension.awaitMessage("range-deleted");
+
+ equal(
+ await PlacesTestUtils.isPageInDB(visits[0].url),
+ false,
+ "expected uri not found in history database"
+ );
+ equal(
+ await PlacesTestUtils.visitsInDB(visits[0].url),
+ 0,
+ "0 visits for uri found in history database"
+ );
+ equal(
+ await PlacesTestUtils.isPageInDB(visits[1].url),
+ false,
+ "expected uri not found in history database"
+ );
+ equal(
+ await PlacesTestUtils.visitsInDB(visits[1].url),
+ 0,
+ "0 visits for uri found in history database"
+ );
+
+ ok(
+ await PlacesTestUtils.isPageInDB(visits[3].url),
+ "expected uri found in history database"
+ );
+
+ extension.sendMessage("delete-all");
+ [historyClearedCount, removedUrls] = await extension.awaitMessage(
+ "history-cleared"
+ );
+ equal(
+ historyClearedCount,
+ 2,
+ "onVisitRemoved called for each clearing of history"
+ );
+ equal(
+ removedUrls.length,
+ 3,
+ "onVisitRemoved called the expected number of times"
+ );
+ for (let i = 1; i < 3; ++i) {
+ let url = visits[i].url;
+ ok(removedUrls.includes(url), `${url} received by onVisitRemoved`);
+ }
+ await extension.unload();
+});
+
+add_task(async function test_search() {
+ const SINGLE_VISIT_URL = "http://example.com/";
+ const DOUBLE_VISIT_URL = "http://example.com/2/";
+ const MOZILLA_VISIT_URL = "http://mozilla.com/";
+ const REFERENCE_DATE = new Date();
+ // pages/visits to add via History.insert
+ const PAGE_INFOS = [
+ {
+ url: SINGLE_VISIT_URL,
+ title: `test visit for ${SINGLE_VISIT_URL}`,
+ visits: [{ date: new Date(Number(REFERENCE_DATE) - 1000) }],
+ },
+ {
+ url: DOUBLE_VISIT_URL,
+ title: `test visit for ${DOUBLE_VISIT_URL}`,
+ visits: [
+ { date: REFERENCE_DATE },
+ { date: new Date(Number(REFERENCE_DATE) - 2000) },
+ ],
+ },
+ {
+ url: MOZILLA_VISIT_URL,
+ title: `test visit for ${MOZILLA_VISIT_URL}`,
+ visits: [{ date: new Date(Number(REFERENCE_DATE) - 3000) }],
+ },
+ ];
+
+ function background(BGSCRIPT_REFERENCE_DATE) {
+ const futureTime = Date.now() + 24 * 60 * 60 * 1000;
+
+ browser.test.onMessage.addListener(msg => {
+ browser.history
+ .search({ text: "" })
+ .then(results => {
+ browser.test.sendMessage("empty-search", results);
+ return browser.history.search({ text: "mozilla.com" });
+ })
+ .then(results => {
+ browser.test.sendMessage("text-search", results);
+ return browser.history.search({ text: "example.com", maxResults: 1 });
+ })
+ .then(results => {
+ browser.test.sendMessage("max-results-search", results);
+ return browser.history.search({
+ text: "",
+ startTime: BGSCRIPT_REFERENCE_DATE - 2000,
+ endTime: BGSCRIPT_REFERENCE_DATE - 1000,
+ });
+ })
+ .then(results => {
+ browser.test.sendMessage("date-range-search", results);
+ return browser.history.search({ text: "", startTime: futureTime });
+ })
+ .then(results => {
+ browser.test.assertEq(
+ 0,
+ results.length,
+ "no results returned for late start time"
+ );
+ return browser.history.search({ text: "", endTime: 0 });
+ })
+ .then(results => {
+ browser.test.assertEq(
+ 0,
+ results.length,
+ "no results returned for early end time"
+ );
+ return browser.history.search({
+ text: "",
+ startTime: Date.now(),
+ endTime: 0,
+ });
+ })
+ .then(
+ results => {
+ browser.test.fail(
+ "history.search rejects with startTime that is after the endTime"
+ );
+ },
+ error => {
+ browser.test.assertEq(
+ "The startTime cannot be after the endTime",
+ error.message,
+ "history.search rejects with startTime that is after the endTime"
+ );
+ }
+ )
+ .then(() => {
+ browser.test.notifyPass("search");
+ });
+ });
+
+ browser.test.sendMessage("ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["history"],
+ },
+ background: `(${background})(${Number(REFERENCE_DATE)})`,
+ });
+
+ function findResult(url, results) {
+ return results.find(r => r.url === url);
+ }
+
+ function checkResult(results, url, expectedCount) {
+ let result = findResult(url, results);
+ notEqual(result, null, `history.search result was found for ${url}`);
+ equal(
+ result.visitCount,
+ expectedCount,
+ `history.search reports ${expectedCount} visit(s)`
+ );
+ equal(
+ result.title,
+ `test visit for ${url}`,
+ "title for search result is correct"
+ );
+ }
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+ await PlacesUtils.history.clear();
+
+ await PlacesUtils.history.insertMany(PAGE_INFOS);
+
+ extension.sendMessage("check-history");
+
+ let results = await extension.awaitMessage("empty-search");
+ equal(results.length, 3, "history.search with empty text returned 3 results");
+ checkResult(results, SINGLE_VISIT_URL, 1);
+ checkResult(results, DOUBLE_VISIT_URL, 2);
+ checkResult(results, MOZILLA_VISIT_URL, 1);
+
+ results = await extension.awaitMessage("text-search");
+ equal(
+ results.length,
+ 1,
+ "history.search with specific text returned 1 result"
+ );
+ checkResult(results, MOZILLA_VISIT_URL, 1);
+
+ results = await extension.awaitMessage("max-results-search");
+ equal(results.length, 1, "history.search with maxResults returned 1 result");
+ checkResult(results, DOUBLE_VISIT_URL, 2);
+
+ results = await extension.awaitMessage("date-range-search");
+ equal(
+ results.length,
+ 2,
+ "history.search with a date range returned 2 result"
+ );
+ checkResult(results, DOUBLE_VISIT_URL, 2);
+ checkResult(results, SINGLE_VISIT_URL, 1);
+
+ await extension.awaitFinish("search");
+ await extension.unload();
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function test_add_url() {
+ function background() {
+ const TEST_DOMAIN = "http://example.com/";
+
+ browser.test.onMessage.addListener((msg, testData) => {
+ let [details, type] = testData;
+ details.url = details.url || `${TEST_DOMAIN}${type}`;
+ if (msg === "add-url") {
+ details.title = `Title for ${type}`;
+ browser.history
+ .addUrl(details)
+ .then(() => {
+ return browser.history.search({ text: details.url });
+ })
+ .then(results => {
+ browser.test.assertEq(
+ 1,
+ results.length,
+ "1 result found when searching for added URL"
+ );
+ browser.test.sendMessage("url-added", {
+ details,
+ result: results[0],
+ });
+ });
+ } else if (msg === "expect-failure") {
+ let expectedMsg = testData[2];
+ browser.history.addUrl(details).then(
+ () => {
+ browser.test.fail(`Expected error thrown for ${type}`);
+ },
+ error => {
+ browser.test.assertTrue(
+ error.message.includes(expectedMsg),
+ `"Expected error thrown when trying to add a URL with ${type}`
+ );
+ browser.test.sendMessage("add-failed");
+ }
+ );
+ }
+ });
+
+ browser.test.sendMessage("ready");
+ }
+
+ let addTestData = [
+ [{}, "default"],
+ [{ visitTime: new Date() }, "with_date"],
+ [{ visitTime: Date.now() }, "with_ms_number"],
+ [{ visitTime: new Date().toISOString() }, "with_iso_string"],
+ [{ transition: "typed" }, "valid_transition"],
+ ];
+
+ let failTestData = [
+ [
+ { transition: "generated" },
+ "an invalid transition",
+ "|generated| is not a supported transition for history",
+ ],
+ [{ visitTime: Date.now() + 1000000 }, "a future date", "Invalid value"],
+ [{ url: "about.config" }, "an invalid url", "Invalid value"],
+ ];
+
+ async function checkUrl(results) {
+ ok(
+ await PlacesTestUtils.isPageInDB(results.details.url),
+ `${results.details.url} found in history database`
+ );
+ ok(
+ PlacesUtils.isValidGuid(results.result.id),
+ "URL was added with a valid id"
+ );
+ equal(
+ results.result.title,
+ results.details.title,
+ "URL was added with the correct title"
+ );
+ if (results.details.visitTime) {
+ equal(
+ results.result.lastVisitTime,
+ Number(ExtensionCommon.normalizeTime(results.details.visitTime)),
+ "URL was added with the correct date"
+ );
+ }
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["history"],
+ },
+ background: `(${background})()`,
+ });
+
+ await PlacesUtils.history.clear();
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ for (let data of addTestData) {
+ extension.sendMessage("add-url", data);
+ let results = await extension.awaitMessage("url-added");
+ await checkUrl(results);
+ }
+
+ for (let data of failTestData) {
+ extension.sendMessage("expect-failure", data);
+ await extension.awaitMessage("add-failed");
+ }
+
+ await extension.unload();
+});
+
+add_task(async function test_get_visits() {
+ async function background() {
+ const TEST_DOMAIN = "http://example.com/";
+ const FIRST_DATE = Date.now();
+ const INITIAL_DETAILS = {
+ url: TEST_DOMAIN,
+ visitTime: FIRST_DATE,
+ transition: "link",
+ };
+
+ let visitIds = new Set();
+
+ async function checkVisit(visit, expected) {
+ visitIds.add(visit.visitId);
+ browser.test.assertEq(
+ expected.visitTime,
+ visit.visitTime,
+ "visit has the correct visitTime"
+ );
+ browser.test.assertEq(
+ expected.transition,
+ visit.transition,
+ "visit has the correct transition"
+ );
+ let results = await browser.history.search({ text: expected.url });
+ // all results will have the same id, so we only need to use the first one
+ browser.test.assertEq(
+ results[0].id,
+ visit.id,
+ "visit has the correct id"
+ );
+ }
+
+ let details = Object.assign({}, INITIAL_DETAILS);
+
+ await browser.history.addUrl(details);
+ let results = await browser.history.getVisits({ url: details.url });
+
+ browser.test.assertEq(
+ 1,
+ results.length,
+ "the expected number of visits were returned"
+ );
+ await checkVisit(results[0], details);
+
+ details.url = `${TEST_DOMAIN}/1/`;
+ await browser.history.addUrl(details);
+
+ results = await browser.history.getVisits({ url: details.url });
+ browser.test.assertEq(
+ 1,
+ results.length,
+ "the expected number of visits were returned"
+ );
+ await checkVisit(results[0], details);
+
+ details.visitTime = FIRST_DATE - 1000;
+ details.transition = "typed";
+ await browser.history.addUrl(details);
+ results = await browser.history.getVisits({ url: details.url });
+
+ browser.test.assertEq(
+ 2,
+ results.length,
+ "the expected number of visits were returned"
+ );
+ await checkVisit(results[0], INITIAL_DETAILS);
+ await checkVisit(results[1], details);
+ browser.test.assertEq(3, visitIds.size, "each visit has a unique visitId");
+ await browser.test.notifyPass("get-visits");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["history"],
+ },
+ background: `(${background})()`,
+ });
+
+ await PlacesUtils.history.clear();
+ await extension.startup();
+
+ await extension.awaitFinish("get-visits");
+ await extension.unload();
+});
+
+add_task(async function test_transition_types() {
+ const VISIT_URL_PREFIX = "http://example.com/";
+ const TRANSITIONS = [
+ ["link", Ci.nsINavHistoryService.TRANSITION_LINK],
+ ["typed", Ci.nsINavHistoryService.TRANSITION_TYPED],
+ ["auto_bookmark", Ci.nsINavHistoryService.TRANSITION_BOOKMARK],
+ // Only session history contains TRANSITION_EMBED visits,
+ // So global history query cannot find them.
+ // ["auto_subframe", Ci.nsINavHistoryService.TRANSITION_EMBED],
+ // Redirects are not correctly tested here because History
+ // will not make redirect entries hidden.
+ ["link", Ci.nsINavHistoryService.TRANSITION_REDIRECT_PERMANENT],
+ ["link", Ci.nsINavHistoryService.TRANSITION_REDIRECT_TEMPORARY],
+ ["link", Ci.nsINavHistoryService.TRANSITION_DOWNLOAD],
+ ["manual_subframe", Ci.nsINavHistoryService.TRANSITION_FRAMED_LINK],
+ ["reload", Ci.nsINavHistoryService.TRANSITION_RELOAD],
+ ];
+
+ // pages/visits to add via History.insertMany
+ let pageInfos = [];
+ let visitDate = new Date(1999, 9, 9, 9, 9).getTime();
+ for (let [, transitionType] of TRANSITIONS) {
+ pageInfos.push({
+ url: VISIT_URL_PREFIX + transitionType + "/",
+ visits: [
+ { transition: transitionType, date: new Date((visitDate -= 1000)) },
+ ],
+ });
+ }
+
+ function background() {
+ browser.test.onMessage.addListener(async (msg, url) => {
+ switch (msg) {
+ case "search": {
+ let results = await browser.history.search({
+ text: "",
+ startTime: new Date(0),
+ });
+ browser.test.sendMessage("search-result", results);
+ break;
+ }
+ case "get-visits": {
+ let results = await browser.history.getVisits({ url });
+ browser.test.sendMessage("get-visits-result", results);
+ break;
+ }
+ }
+ });
+
+ browser.test.sendMessage("ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["history"],
+ },
+ background,
+ });
+
+ await PlacesUtils.history.clear();
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ await PlacesUtils.history.insertMany(pageInfos);
+
+ extension.sendMessage("search");
+ let results = await extension.awaitMessage("search-result");
+ equal(
+ results.length,
+ pageInfos.length,
+ "search returned expected length of results"
+ );
+ for (let i = 0; i < pageInfos.length; ++i) {
+ equal(results[i].url, pageInfos[i].url, "search returned the expected url");
+
+ extension.sendMessage("get-visits", pageInfos[i].url);
+ let visits = await extension.awaitMessage("get-visits-result");
+ equal(visits.length, 1, "getVisits returned expected length of visits");
+ equal(
+ visits[0].transition,
+ TRANSITIONS[i][0],
+ "getVisits returned the expected transition"
+ );
+ }
+
+ await extension.unload();
+});
+
+add_task(async function test_on_visited() {
+ const SINGLE_VISIT_URL = "http://example.com/1/";
+ const DOUBLE_VISIT_URL = "http://example.com/2/";
+ let visitDate = new Date(1999, 9, 9, 9, 9).getTime();
+
+ // pages/visits to add via History.insertMany
+ const PAGE_INFOS = [
+ {
+ url: SINGLE_VISIT_URL,
+ title: `visit to ${SINGLE_VISIT_URL}`,
+ visits: [{ date: new Date(visitDate) }],
+ },
+ {
+ url: DOUBLE_VISIT_URL,
+ title: `visit to ${DOUBLE_VISIT_URL}`,
+ visits: [
+ { date: new Date((visitDate += 1000)) },
+ { date: new Date((visitDate += 1000)) },
+ ],
+ },
+ {
+ url: SINGLE_VISIT_URL,
+ title: "Title Changed",
+ visits: [{ date: new Date(visitDate) }],
+ },
+ ];
+
+ function background() {
+ let onVisitedData = [];
+
+ browser.history.onVisited.addListener(data => {
+ if (data.url.includes("moz-extension")) {
+ return;
+ }
+ onVisitedData.push(data);
+ if (onVisitedData.length == 4) {
+ browser.test.sendMessage("on-visited-data", onVisitedData);
+ }
+ });
+
+ // Verifying onTitleChange Event along with onVisited event
+ browser.history.onTitleChanged.addListener(data => {
+ browser.test.sendMessage("on-title-changed-data", data);
+ });
+
+ browser.test.sendMessage("ready");
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["history"],
+ },
+ background: `(${background})()`,
+ });
+
+ await PlacesUtils.history.clear();
+ await extension.startup();
+ await extension.awaitMessage("ready");
+
+ await PlacesUtils.history.insertMany(PAGE_INFOS);
+
+ let onVisitedData = await extension.awaitMessage("on-visited-data");
+
+ function checkOnVisitedData(index, expected) {
+ let onVisited = onVisitedData[index];
+ ok(PlacesUtils.isValidGuid(onVisited.id), "onVisited received a valid id");
+ equal(onVisited.url, expected.url, "onVisited received the expected url");
+ // Title will be blank until bug 1287928 lands
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1287928
+ equal(
+ onVisited.title,
+ expected.title,
+ "onVisited received the expected title"
+ );
+ equal(
+ onVisited.lastVisitTime,
+ expected.time,
+ "onVisited received the expected time"
+ );
+ equal(
+ onVisited.visitCount,
+ expected.visitCount,
+ "onVisited received the expected visitCount"
+ );
+ }
+
+ let expected = {
+ url: PAGE_INFOS[0].url,
+ title: PAGE_INFOS[0].title,
+ time: PAGE_INFOS[0].visits[0].date.getTime(),
+ visitCount: 1,
+ };
+ checkOnVisitedData(0, expected);
+
+ expected.url = PAGE_INFOS[1].url;
+ expected.title = PAGE_INFOS[1].title;
+ expected.time = PAGE_INFOS[1].visits[0].date.getTime();
+ checkOnVisitedData(1, expected);
+
+ expected.time = PAGE_INFOS[1].visits[1].date.getTime();
+ expected.visitCount = 2;
+ checkOnVisitedData(2, expected);
+
+ expected.url = PAGE_INFOS[2].url;
+ expected.title = PAGE_INFOS[2].title;
+ expected.time = PAGE_INFOS[2].visits[0].date.getTime();
+ expected.visitCount = 2;
+ checkOnVisitedData(3, expected);
+
+ let onTitleChangedData = await extension.awaitMessage(
+ "on-title-changed-data"
+ );
+ equal(
+ onTitleChangedData.title,
+ "Title Changed",
+ "ontitleChanged received the expected title."
+ );
+
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/xpcshell/test_ext_homepage_overrides_private.js b/browser/components/extensions/test/xpcshell/test_ext_homepage_overrides_private.js
new file mode 100644
index 0000000000..c780572846
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_homepage_overrides_private.js
@@ -0,0 +1,136 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.import(
+ "resource://testing-common/AddonTestUtils.jsm"
+);
+const { HomePage } = ChromeUtils.import("resource:///modules/HomePage.jsm");
+const { ExtensionPermissions } = ChromeUtils.import(
+ "resource://gre/modules/ExtensionPermissions.jsm"
+);
+
+const {
+ createAppInfo,
+ promiseShutdownManager,
+ promiseStartupManager,
+} = AddonTestUtils;
+
+const EXTENSION_ID = "test_overrides@tests.mozilla.org";
+const HOMEPAGE_EXTENSION_CONTROLLED =
+ "browser.startup.homepage_override.extensionControlled";
+const HOMEPAGE_PRIVATE_ALLOWED =
+ "browser.startup.homepage_override.privateAllowed";
+const HOMEPAGE_URL_PREF = "browser.startup.homepage";
+const HOMEPAGE_URI = "webext-homepage-1.html";
+
+Services.prefs.setBoolPref("browser.privatebrowsing.autostart", true);
+Services.prefs.setBoolPref("extensions.allowPrivateBrowsingByDefault", false);
+
+AddonTestUtils.init(this);
+AddonTestUtils.usePrivilegedSignatures = false;
+AddonTestUtils.overrideCertDB();
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");
+
+function promisePrefChange(pref) {
+ return new Promise((resolve, reject) => {
+ Services.prefs.addObserver(pref, function observer() {
+ Services.prefs.removeObserver(pref, observer);
+ resolve(arguments);
+ });
+ });
+}
+
+let defaultHomepageURL;
+
+function verifyPrefSettings(controlled, allowed) {
+ equal(
+ Services.prefs.getBoolPref(HOMEPAGE_EXTENSION_CONTROLLED, false),
+ controlled,
+ "homepage extension controlled"
+ );
+ equal(
+ Services.prefs.getBoolPref(HOMEPAGE_PRIVATE_ALLOWED, false),
+ allowed,
+ "homepage private permission after permission change"
+ );
+
+ if (controlled && allowed) {
+ ok(
+ HomePage.get().endsWith(HOMEPAGE_URI),
+ "Home page url is overridden by the extension"
+ );
+ } else {
+ equal(HomePage.get(), defaultHomepageURL, "Home page url is default.");
+ }
+}
+
+async function promiseUpdatePrivatePermission(allowed, extension) {
+ info(`update private allowed permission`);
+ await Promise.all([
+ promisePrefChange(HOMEPAGE_PRIVATE_ALLOWED),
+ ExtensionPermissions[allowed ? "add" : "remove"](
+ extension.id,
+ { permissions: ["internal:privateBrowsingAllowed"], origins: [] },
+ extension
+ ),
+ ]);
+
+ verifyPrefSettings(true, allowed);
+}
+
+add_task(async function test_overrides_private() {
+ await promiseStartupManager();
+
+ let extensionInfo = {
+ useAddonManager: "permanent",
+ manifest: {
+ version: "1.0",
+ applications: {
+ gecko: {
+ id: EXTENSION_ID,
+ },
+ },
+ chrome_settings_overrides: {
+ homepage: HOMEPAGE_URI,
+ },
+ },
+ };
+ let extension = ExtensionTestUtils.loadExtension(extensionInfo);
+
+ defaultHomepageURL = HomePage.get();
+
+ await extension.startup();
+
+ verifyPrefSettings(true, false);
+
+ equal(HomePage.get(), defaultHomepageURL, "Home page url is default.");
+
+ info("add permission to extension");
+ await promiseUpdatePrivatePermission(true, extension.extension);
+ info("remove permission from extension");
+ await promiseUpdatePrivatePermission(false, extension.extension);
+ // set back to true to test upgrade removing extension control
+ info("add permission back to prepare for upgrade test");
+ await promiseUpdatePrivatePermission(true, extension.extension);
+
+ extensionInfo.manifest = {
+ version: "2.0",
+ applications: {
+ gecko: {
+ id: EXTENSION_ID,
+ },
+ },
+ };
+
+ await Promise.all([
+ promisePrefChange(HOMEPAGE_URL_PREF),
+ extension.upgrade(extensionInfo),
+ ]);
+
+ verifyPrefSettings(false, false);
+
+ await extension.unload();
+ await promiseShutdownManager();
+});
diff --git a/browser/components/extensions/test/xpcshell/test_ext_manifest.js b/browser/components/extensions/test/xpcshell/test_ext_manifest.js
new file mode 100644
index 0000000000..376d3901c0
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_manifest.js
@@ -0,0 +1,61 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+async function testIconPaths(icon, manifest, expectedError) {
+ let normalized = await ExtensionTestUtils.normalizeManifest(manifest);
+
+ if (expectedError) {
+ ok(
+ expectedError.test(normalized.error),
+ `Should have an error for ${JSON.stringify(manifest)}`
+ );
+ } else {
+ ok(
+ !normalized.error,
+ `Should not have an error ${JSON.stringify(manifest)}, ${
+ normalized.error
+ }`
+ );
+ }
+}
+
+add_task(async function test_manifest() {
+ let badpaths = ["", " ", "\t", "http://foo.com/icon.png"];
+ for (let path of badpaths) {
+ for (let action of ["browser_action", "page_action", "sidebar_action"]) {
+ let manifest = {};
+ manifest[action] = { default_icon: path };
+ let error = new RegExp(`Error processing ${action}.default_icon`);
+ await testIconPaths(path, manifest, error);
+
+ manifest[action] = { default_icon: { "16": path } };
+ await testIconPaths(path, manifest, error);
+ }
+ }
+
+ let paths = [
+ "icon.png",
+ "/icon.png",
+ "./icon.png",
+ "path to an icon.png",
+ " icon.png",
+ ];
+ for (let path of paths) {
+ for (let action of ["browser_action", "page_action", "sidebar_action"]) {
+ let manifest = {};
+ manifest[action] = { default_icon: path };
+ if (action == "sidebar_action") {
+ // Sidebar requires panel.
+ manifest[action].default_panel = "foo.html";
+ }
+ await testIconPaths(path, manifest);
+
+ manifest[action] = { default_icon: { "16": path } };
+ if (action == "sidebar_action") {
+ manifest[action].default_panel = "foo.html";
+ }
+ await testIconPaths(path, manifest);
+ }
+ }
+});
diff --git a/browser/components/extensions/test/xpcshell/test_ext_manifest_commands.js b/browser/components/extensions/test/xpcshell/test_ext_manifest_commands.js
new file mode 100644
index 0000000000..8196ab0e24
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_manifest_commands.js
@@ -0,0 +1,52 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(async function test_manifest_commands() {
+ const validShortcuts = [
+ "Ctrl+Y",
+ "MacCtrl+Y",
+ "Command+Y",
+ "Alt+Shift+Y",
+ "Ctrl+Alt+Y",
+ "F1",
+ "MediaNextTrack",
+ ];
+ const invalidShortcuts = ["Shift+Y", "Y", "Ctrl+Ctrl+Y", "Ctrl+Command+Y"];
+
+ async function validateShortcut(shortcut, isValid) {
+ let normalized = await ExtensionTestUtils.normalizeManifest({
+ commands: {
+ "toggle-feature": {
+ suggested_key: { default: shortcut },
+ description: "Send a 'toggle-feature' event to the extension",
+ },
+ },
+ });
+ if (isValid) {
+ ok(!normalized.error, "There should be no manifest errors.");
+ } else {
+ let expectedError =
+ String.raw`Error processing commands.toggle-feature.suggested_key.default: Error: ` +
+ String.raw`Value "${shortcut}" must consist of ` +
+ String.raw`either a combination of one or two modifiers, including ` +
+ String.raw`a mandatory primary modifier and a key, separated by '+', ` +
+ String.raw`or a media key. For details see: ` +
+ String.raw`https://developer.mozilla.org/en-US/Add-ons/WebExtensions/manifest.json/commands#Key_combinations`;
+
+ ok(
+ normalized.error.includes(expectedError),
+ `The manifest error ${JSON.stringify(
+ normalized.error
+ )} must contain ${JSON.stringify(expectedError)}`
+ );
+ }
+ }
+
+ for (let shortcut of validShortcuts) {
+ validateShortcut(shortcut, true);
+ }
+ for (let shortcut of invalidShortcuts) {
+ validateShortcut(shortcut, false);
+ }
+});
diff --git a/browser/components/extensions/test/xpcshell/test_ext_manifest_omnibox.js b/browser/components/extensions/test/xpcshell/test_ext_manifest_omnibox.js
new file mode 100644
index 0000000000..f81e7d3cb5
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_manifest_omnibox.js
@@ -0,0 +1,62 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+async function testKeyword(params) {
+ let normalized = await ExtensionTestUtils.normalizeManifest({
+ omnibox: {
+ keyword: params.keyword,
+ },
+ });
+
+ if (params.expectError) {
+ let expectedError =
+ String.raw`omnibox.keyword: String "${params.keyword}" ` +
+ String.raw`must match /^[^?\s:][^\s:]*$/`;
+ ok(
+ normalized.error.includes(expectedError),
+ `The manifest error ${JSON.stringify(normalized.error)} ` +
+ `must contain ${JSON.stringify(expectedError)}`
+ );
+ } else {
+ equal(normalized.error, undefined, "Should not have an error");
+ equal(normalized.errors.length, 0, "Should not have warnings");
+ }
+}
+
+add_task(async function test_manifest_commands() {
+ // accepted single character keywords
+ await testKeyword({ keyword: "a", expectError: false });
+ await testKeyword({ keyword: "-", expectError: false });
+ await testKeyword({ keyword: "嗨", expectError: false });
+ await testKeyword({ keyword: "*", expectError: false });
+ await testKeyword({ keyword: "/", expectError: false });
+
+ // rejected single character keywords
+ await testKeyword({ keyword: "?", expectError: true });
+ await testKeyword({ keyword: " ", expectError: true });
+ await testKeyword({ keyword: ":", expectError: true });
+
+ // accepted multi-character keywords
+ await testKeyword({ keyword: "aa", expectError: false });
+ await testKeyword({ keyword: "http", expectError: false });
+ await testKeyword({ keyword: "f?a", expectError: false });
+ await testKeyword({ keyword: "fa?", expectError: false });
+ await testKeyword({ keyword: "f/x", expectError: false });
+ await testKeyword({ keyword: "/fx", expectError: false });
+ await testKeyword({ keyword: "fx/", expectError: false });
+
+ // rejected multi-character keywords
+ await testKeyword({ keyword: " a", expectError: true });
+ await testKeyword({ keyword: "a ", expectError: true });
+ await testKeyword({ keyword: " ", expectError: true });
+ await testKeyword({ keyword: " a ", expectError: true });
+ await testKeyword({ keyword: "?fx", expectError: true });
+ await testKeyword({ keyword: "f:x", expectError: true });
+ await testKeyword({ keyword: "fx:", expectError: true });
+ await testKeyword({ keyword: "f x", expectError: true });
+
+ // miscellaneous tests
+ await testKeyword({ keyword: "こんにちは", expectError: false });
+ await testKeyword({ keyword: "http://", expectError: true });
+});
diff --git a/browser/components/extensions/test/xpcshell/test_ext_manifest_permissions.js b/browser/components/extensions/test/xpcshell/test_ext_manifest_permissions.js
new file mode 100644
index 0000000000..0712b9bac8
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_manifest_permissions.js
@@ -0,0 +1,73 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+/* globals chrome */
+
+async function testPermission(options) {
+ function background(bgOptions) {
+ browser.test.sendMessage("typeof-namespace", {
+ browser: typeof browser[bgOptions.namespace],
+ chrome: typeof chrome[bgOptions.namespace],
+ });
+ }
+
+ let extensionDetails = {
+ background: `(${background})(${JSON.stringify(options)})`,
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionDetails);
+
+ await extension.startup();
+
+ let types = await extension.awaitMessage("typeof-namespace");
+ equal(
+ types.browser,
+ "undefined",
+ `Type of browser.${options.namespace} without manifest entry`
+ );
+ equal(
+ types.chrome,
+ "undefined",
+ `Type of chrome.${options.namespace} without manifest entry`
+ );
+
+ await extension.unload();
+
+ extensionDetails.manifest = options.manifest;
+ extension = ExtensionTestUtils.loadExtension(extensionDetails);
+
+ await extension.startup();
+
+ types = await extension.awaitMessage("typeof-namespace");
+ equal(
+ types.browser,
+ "object",
+ `Type of browser.${options.namespace} with manifest entry`
+ );
+ equal(
+ types.chrome,
+ "object",
+ `Type of chrome.${options.namespace} with manifest entry`
+ );
+
+ await extension.unload();
+}
+
+add_task(async function test_browserAction() {
+ await testPermission({
+ namespace: "browserAction",
+ manifest: {
+ browser_action: {},
+ },
+ });
+});
+
+add_task(async function test_pageAction() {
+ await testPermission({
+ namespace: "pageAction",
+ manifest: {
+ page_action: {},
+ },
+ });
+});
diff --git a/browser/components/extensions/test/xpcshell/test_ext_normandyAddonStudy.js b/browser/components/extensions/test/xpcshell/test_ext_normandyAddonStudy.js
new file mode 100644
index 0000000000..12f7318864
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_normandyAddonStudy.js
@@ -0,0 +1,213 @@
+"use strict";
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "AddonManager",
+ "resource://gre/modules/AddonManager.jsm"
+);
+
+const { AddonStudies } = ChromeUtils.import(
+ "resource://normandy/lib/AddonStudies.jsm"
+);
+const { NormandyTestUtils } = ChromeUtils.import(
+ "resource://testing-common/NormandyTestUtils.jsm"
+);
+const { TestUtils } = ChromeUtils.import(
+ "resource://testing-common/TestUtils.jsm"
+);
+var { AddonTestUtils } = ChromeUtils.import(
+ "resource://testing-common/AddonTestUtils.jsm"
+);
+
+const { addonStudyFactory } = NormandyTestUtils.factories;
+
+AddonTestUtils.init(this);
+
+// All tests run privileged unless otherwise specified not to.
+function createExtension(backgroundScript, permissions, isPrivileged = true) {
+ let extensionData = {
+ background: backgroundScript,
+ manifest: {
+ applications: {
+ gecko: {
+ id: "test@shield.mozilla.com",
+ },
+ },
+ permissions,
+ },
+ isPrivileged,
+ };
+ return ExtensionTestUtils.loadExtension(extensionData);
+}
+
+async function run(test) {
+ let extension = createExtension(
+ test.backgroundScript,
+ test.permissions || ["normandyAddonStudy"],
+ test.isPrivileged
+ );
+ const promiseValidation = test.validationScript
+ ? test.validationScript(extension)
+ : Promise.resolve();
+
+ await extension.startup();
+
+ await promiseValidation;
+
+ if (test.doneSignal) {
+ await extension.awaitFinish(test.doneSignal);
+ }
+
+ await extension.unload();
+}
+
+add_task(async function setup() {
+ await ExtensionTestUtils.startAddonManager();
+});
+
+add_task(
+ async function test_normandyAddonStudy_without_normandyAddonStudy_permission_privileged() {
+ await run({
+ backgroundScript: () => {
+ browser.test.assertTrue(
+ !browser.normandyAddonStudy,
+ "'normandyAddonStudy' permission is required"
+ );
+ browser.test.notifyPass("normandyAddonStudy_permission");
+ },
+ permissions: [],
+ doneSignal: "normandyAddonStudy_permission",
+ });
+ }
+);
+
+add_task(async function test_normandyAddonStudy_without_privilege() {
+ await run({
+ backgroundScript: () => {
+ browser.test.assertTrue(
+ !browser.normandyAddonStudy,
+ "Extension must be privileged"
+ );
+ browser.test.notifyPass("normandyAddonStudy_permission");
+ },
+ isPrivileged: false,
+ doneSignal: "normandyAddonStudy_permission",
+ });
+});
+
+add_task(async function test_getStudy_works() {
+ const study = addonStudyFactory({
+ addonId: "test@shield.mozilla.com",
+ });
+
+ const testWrapper = AddonStudies.withStudies([study]);
+ const test = testWrapper(async () => {
+ await run({
+ backgroundScript: async () => {
+ const result = await browser.normandyAddonStudy.getStudy();
+ browser.test.sendMessage("study", result);
+ },
+ validationScript: async extension => {
+ let studyResult = await extension.awaitMessage("study");
+ deepEqual(
+ studyResult,
+ study,
+ "normandyAddonStudy.getStudy returns the correct study"
+ );
+ },
+ });
+ });
+
+ await test();
+});
+
+add_task(async function test_endStudy_works() {
+ const study = addonStudyFactory({
+ addonId: "test@shield.mozilla.com",
+ });
+
+ const testWrapper = AddonStudies.withStudies([study]);
+ const test = testWrapper(async () => {
+ await run({
+ backgroundScript: async () => {
+ await browser.normandyAddonStudy.endStudy("test");
+ },
+ validationScript: async () => {
+ // Check that `AddonStudies.markAsEnded` was called
+ await TestUtils.topicObserved(
+ "shield-study-ended",
+ (subject, message) => {
+ return message === `${study.recipeId}`;
+ }
+ );
+
+ const addon = await AddonManager.getAddonByID(study.addonId);
+ equal(addon, undefined, "Addon should be uninstalled.");
+ },
+ });
+ });
+
+ await test();
+});
+
+add_task(async function test_getClientMetadata_works() {
+ const study = addonStudyFactory({
+ addonId: "test@shield.mozilla.com",
+ slug: "test-slug",
+ branch: "test-branch",
+ });
+
+ const testWrapper = AddonStudies.withStudies([study]);
+ const test = testWrapper(async () => {
+ await run({
+ backgroundScript: async () => {
+ const metadata = await browser.normandyAddonStudy.getClientMetadata();
+ browser.test.sendMessage("clientMetadata", metadata);
+ },
+ validationScript: async extension => {
+ let clientMetadata = await extension.awaitMessage("clientMetadata");
+
+ ok(
+ clientMetadata.updateChannel ===
+ Services.appinfo.defaultUpdateChannel,
+ "clientMetadata contains correct updateChannel"
+ );
+
+ ok(
+ clientMetadata.fxVersion === Services.appinfo.version,
+ "clientMetadata contains correct fxVersion"
+ );
+
+ ok("clientID" in clientMetadata, "clientMetadata contains a clientID");
+ },
+ });
+ });
+
+ await test();
+});
+
+add_task(async function test_onUnenroll_works() {
+ const study = addonStudyFactory({
+ addonId: "test@shield.mozilla.com",
+ });
+
+ const testWrapper = AddonStudies.withStudies([study]);
+ const test = testWrapper(async () => {
+ await run({
+ backgroundScript: () => {
+ browser.normandyAddonStudy.onUnenroll.addListener(reason => {
+ browser.test.sendMessage("unenrollReason", reason);
+ });
+ browser.test.sendMessage("bgpageReady");
+ },
+ validationScript: async extension => {
+ await extension.awaitMessage("bgpageReady");
+ await AddonStudies.markAsEnded(study, "test");
+ const unenrollReason = await extension.awaitMessage("unenrollReason");
+ equal(unenrollReason, "test", "Unenroll listener should be called.");
+ },
+ });
+ });
+
+ await test();
+});
diff --git a/browser/components/extensions/test/xpcshell/test_ext_pageAction_shutdown.js b/browser/components/extensions/test/xpcshell/test_ext_pageAction_shutdown.js
new file mode 100644
index 0000000000..501e17ceb0
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_pageAction_shutdown.js
@@ -0,0 +1,87 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+// Load lazy so we create the app info first.
+ChromeUtils.defineModuleGetter(
+ this,
+ "PageActions",
+ "resource:///modules/PageActions.jsm"
+);
+
+const { AddonTestUtils } = ChromeUtils.import(
+ "resource://testing-common/AddonTestUtils.jsm"
+);
+
+const {
+ createAppInfo,
+ promiseShutdownManager,
+ promiseStartupManager,
+} = AddonTestUtils;
+
+AddonTestUtils.init(this);
+AddonTestUtils.overrideCertDB();
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "58");
+
+// This is copied and pasted from ExtensionPopups.jsm. It's used as the
+// PageActions action ID. See ext-pageAction.js.
+function makeWidgetId(id) {
+ id = id.toLowerCase();
+ // FIXME: This allows for collisions.
+ return id.replace(/[^a-z0-9_-]/g, "_");
+}
+
+// Tests that the pinnedToUrlbar property of the PageActions.Action object
+// backing the extension's page action persists across app restarts.
+add_task(async function testAppShutdown() {
+ let extensionData = {
+ useAddonManager: "permanent",
+ manifest: {
+ page_action: {
+ default_title: "test_ext_pageAction_shutdown.js",
+ browser_style: false,
+ },
+ },
+ };
+
+ // Simulate starting up the app.
+ PageActions.init();
+ await promiseStartupManager();
+
+ let extension = ExtensionTestUtils.loadExtension(extensionData);
+ await extension.startup();
+
+ // Get the PageAction.Action object. Its pinnedToUrlbar should have been
+ // initialized to true in ext-pageAction.js, when it's created.
+ let actionID = makeWidgetId(extension.id);
+ let action = PageActions.actionForID(actionID);
+ Assert.equal(action.pinnedToUrlbar, true);
+
+ // Simulate restarting the app without first unloading the extension.
+ await promiseShutdownManager();
+ PageActions._reset();
+ await promiseStartupManager();
+ await extension.awaitStartup();
+
+ // Get the action. Its pinnedToUrlbar should remain true.
+ action = PageActions.actionForID(actionID);
+ Assert.equal(action.pinnedToUrlbar, true);
+
+ // Now set its pinnedToUrlbar to false.
+ action.pinnedToUrlbar = false;
+
+ // Simulate restarting the app again without first unloading the extension.
+ await promiseShutdownManager();
+ PageActions._reset();
+ await promiseStartupManager();
+ await extension.awaitStartup();
+
+ // Get the action. Its pinnedToUrlbar should remain false.
+ action = PageActions.actionForID(actionID);
+ Assert.equal(action.pinnedToUrlbar, false);
+
+ // Now unload the extension and quit the app.
+ await extension.unload();
+ await promiseShutdownManager();
+});
diff --git a/browser/components/extensions/test/xpcshell/test_ext_pkcs11_management.js b/browser/components/extensions/test/xpcshell/test_ext_pkcs11_management.js
new file mode 100644
index 0000000000..ffd510fe65
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_pkcs11_management.js
@@ -0,0 +1,252 @@
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ ctypes: "resource://gre/modules/ctypes.jsm",
+ MockRegistry: "resource://testing-common/MockRegistry.jsm",
+ OS: "resource://gre/modules/osfile.jsm",
+});
+
+do_get_profile();
+let tmpDir = FileUtils.getDir("TmpD", ["PKCS11"]);
+let slug =
+ AppConstants.platform === "linux" ? "pkcs11-modules" : "PKCS11Modules";
+tmpDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+let baseDir = OS.Path.join(tmpDir.path, slug);
+OS.File.makeDir(baseDir);
+
+registerCleanupFunction(() => {
+ tmpDir.remove(true);
+});
+
+function getPath(filename) {
+ return OS.Path.join(baseDir, filename);
+}
+
+const testmodule =
+ "../../../../../security/manager/ssl/tests/unit/pkcs11testmodule/" +
+ ctypes.libraryName("pkcs11testmodule");
+
+// This function was inspired by the native messaging test under
+// toolkit/components/extensions
+
+async function setupManifests(modules) {
+ async function writeManifest(module) {
+ let manifest = {
+ name: module.name,
+ description: module.description,
+ path: module.path,
+ type: "pkcs11",
+ allowed_extensions: [module.id],
+ };
+
+ let manifestPath = getPath(`${module.name}.json`);
+ await OS.File.writeAtomic(manifestPath, JSON.stringify(manifest));
+
+ return manifestPath;
+ }
+
+ switch (AppConstants.platform) {
+ case "macosx":
+ case "linux":
+ let dirProvider = {
+ getFile(property) {
+ if (property == "XREUserNativeManifests") {
+ return tmpDir.clone();
+ } else if (property == "XRESysNativeManifests") {
+ return tmpDir.clone();
+ }
+ return null;
+ },
+ };
+
+ Services.dirsvc.registerProvider(dirProvider);
+ registerCleanupFunction(() => {
+ Services.dirsvc.unregisterProvider(dirProvider);
+ });
+
+ for (let module of modules) {
+ await writeManifest(module);
+ }
+ break;
+
+ case "win":
+ const REGKEY = String.raw`Software\Mozilla\PKCS11Modules`;
+
+ let registry = new MockRegistry();
+ registerCleanupFunction(() => {
+ registry.shutdown();
+ });
+
+ for (let module of modules) {
+ if (!OS.Path.winIsAbsolute(module.path)) {
+ let cwd = await OS.File.getCurrentDirectory();
+ module.path = OS.Path.join(cwd, module.path);
+ }
+ let manifestPath = await writeManifest(module);
+ registry.setValue(
+ Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ `${REGKEY}\\${module.name}`,
+ "",
+ manifestPath
+ );
+ }
+ break;
+
+ default:
+ ok(
+ false,
+ `Loading of PKCS#11 modules is not supported on ${AppConstants.platform}`
+ );
+ }
+}
+
+add_task(async function test_pkcs11() {
+ async function background() {
+ try {
+ let isInstalled = await browser.pkcs11.isModuleInstalled("testmodule");
+ browser.test.assertFalse(
+ isInstalled,
+ "PKCS#11 module is not installed before we install it"
+ );
+ await browser.pkcs11.installModule("testmodule", 0);
+ isInstalled = await browser.pkcs11.isModuleInstalled("testmodule");
+ browser.test.assertTrue(
+ isInstalled,
+ "PKCS#11 module is installed after we install it"
+ );
+ let slots = await browser.pkcs11.getModuleSlots("testmodule");
+ browser.test.assertEq(
+ "Test PKCS11 Slot",
+ slots[0].name,
+ "The first slot name matches the expected name"
+ );
+ browser.test.assertEq(
+ "Test PKCS11 Slot 二",
+ slots[1].name,
+ "The second slot name matches the expected name"
+ );
+ browser.test.assertTrue(slots[1].token, "The second slot has a token");
+ browser.test.assertFalse(slots[2].token, "The third slot has no token");
+ browser.test.assertEq(
+ "Test PKCS11 Tokeñ 2 Label",
+ slots[1].token.name,
+ "The token name matches the expected name"
+ );
+ browser.test.assertEq(
+ "Test PKCS11 Manufacturer ID",
+ slots[1].token.manufacturer,
+ "The token manufacturer matches the expected manufacturer"
+ );
+ browser.test.assertEq(
+ "0.0",
+ slots[1].token.HWVersion,
+ "The token hardware version matches the expected version"
+ );
+ browser.test.assertEq(
+ "0.0",
+ slots[1].token.FWVersion,
+ "The token firmware version matches the expected version"
+ );
+ browser.test.assertEq(
+ "",
+ slots[1].token.serial,
+ "The token has no serial number"
+ );
+ browser.test.assertFalse(
+ slots[1].token.isLoggedIn,
+ "The token is not logged in"
+ );
+ await browser.pkcs11.uninstallModule("testmodule");
+ isInstalled = await browser.pkcs11.isModuleInstalled("testmodule");
+ browser.test.assertFalse(
+ isInstalled,
+ "PKCS#11 module is no longer installed after we uninstall it"
+ );
+ await browser.pkcs11.installModule("testmodule");
+ isInstalled = await browser.pkcs11.isModuleInstalled("testmodule");
+ browser.test.assertTrue(
+ isInstalled,
+ "Installing the PKCS#11 module without flags parameter succeeds"
+ );
+ await browser.pkcs11.uninstallModule("testmodule");
+ await browser.test.assertRejects(
+ browser.pkcs11.isModuleInstalled("nonexistingmodule"),
+ /No such PKCS#11 module nonexistingmodule/,
+ "We cannot access modules if no JSON file exists"
+ );
+ await browser.test.assertRejects(
+ browser.pkcs11.isModuleInstalled("othermodule"),
+ /No such PKCS#11 module othermodule/,
+ "We cannot access modules if we're not listed in the module's manifest file's allowed_extensions key"
+ );
+ await browser.test.assertRejects(
+ browser.pkcs11.uninstallModule("internalmodule"),
+ /No such PKCS#11 module internalmodule/,
+ "We cannot uninstall the NSS Builtin Roots Module"
+ );
+ await browser.test.assertRejects(
+ browser.pkcs11.installModule("osclientcerts", 0),
+ /No such PKCS#11 module osclientcerts/,
+ "installModule should not work on the built-in osclientcerts module"
+ );
+ await browser.test.assertRejects(
+ browser.pkcs11.uninstallModule("osclientcerts"),
+ /No such PKCS#11 module osclientcerts/,
+ "uninstallModule should not work on the built-in osclientcerts module"
+ );
+ await browser.test.assertRejects(
+ browser.pkcs11.isModuleInstalled("osclientcerts"),
+ /No such PKCS#11 module osclientcerts/,
+ "isModuleLoaded should not work on the built-in osclientcerts module"
+ );
+ await browser.test.assertRejects(
+ browser.pkcs11.getModuleSlots("osclientcerts"),
+ /No such PKCS#11 module osclientcerts/,
+ "getModuleSlots should not work on the built-in osclientcerts module"
+ );
+ browser.test.notifyPass("pkcs11");
+ } catch (e) {
+ browser.test.fail(`Error: ${String(e)} :: ${e.stack}`);
+ browser.test.notifyFail("pkcs11 failed");
+ }
+ }
+
+ let libDir = FileUtils.getDir("GreBinD", []);
+ await setupManifests([
+ {
+ name: "testmodule",
+ description: "PKCS#11 Test Module",
+ path: testmodule,
+ id: "pkcs11@tests.mozilla.org",
+ },
+ {
+ name: "othermodule",
+ description: "PKCS#11 Test Module",
+ path: testmodule,
+ id: "other@tests.mozilla.org",
+ },
+ {
+ name: "internalmodule",
+ description: "Builtin Roots Module",
+ path: ctypes.libraryName("nssckbi"),
+ id: "pkcs11@tests.mozilla.org",
+ },
+ {
+ name: "osclientcerts",
+ description: "OS Client Cert Module",
+ path: OS.Path.join(libDir.path, ctypes.libraryName("osclientcerts")),
+ id: "pkcs11@tests.mozilla.org",
+ },
+ ]);
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["pkcs11"],
+ applications: { gecko: { id: "pkcs11@tests.mozilla.org" } },
+ },
+ background: background,
+ });
+ await extension.startup();
+ await extension.awaitFinish("pkcs11");
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/xpcshell/test_ext_settings_overrides_defaults.js b/browser/components/extensions/test/xpcshell/test_ext_settings_overrides_defaults.js
new file mode 100644
index 0000000000..8979ebfb79
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_settings_overrides_defaults.js
@@ -0,0 +1,217 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.import(
+ "resource://testing-common/AddonTestUtils.jsm"
+);
+
+const { SearchTestUtils } = ChromeUtils.import(
+ "resource://testing-common/SearchTestUtils.jsm"
+);
+
+const { SearchUtils } = ChromeUtils.import(
+ "resource://gre/modules/SearchUtils.jsm"
+);
+
+const { RemoteSettings } = ChromeUtils.import(
+ "resource://services-settings/remote-settings.js"
+);
+
+const { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm");
+
+const URLTYPE_SUGGEST_JSON = "application/x-suggestions+json";
+
+AddonTestUtils.init(this);
+AddonTestUtils.createAppInfo(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "42",
+ "42"
+);
+
+const kSearchEngineURL = "https://example.com/?q={searchTerms}&foo=myparams";
+const kSuggestURL = "https://example.com/fake/suggest/";
+const kSuggestURLParams = "q={searchTerms}&type=list2";
+
+Services.prefs.setBoolPref("browser.search.log", true);
+
+add_task(async function setup() {
+ AddonTestUtils.usePrivilegedSignatures = false;
+ AddonTestUtils.overrideCertDB();
+ await AddonTestUtils.promiseStartupManager();
+ await SearchTestUtils.useTestEngines("data", null, [
+ {
+ webExtension: {
+ id: "test@search.mozilla.org",
+ },
+ appliesTo: [
+ {
+ included: { everywhere: true },
+ default: "yes",
+ },
+ ],
+ },
+ {
+ webExtension: {
+ id: "test2@search.mozilla.org",
+ },
+ appliesTo: [
+ {
+ included: { everywhere: true },
+ },
+ ],
+ },
+ ]);
+ await Services.search.init();
+ registerCleanupFunction(async () => {
+ await AddonTestUtils.promiseShutdownManager();
+ });
+});
+
+add_task(async function test_extension_changing_to_app_provided_default() {
+ let ext1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ icons: {
+ "16": "foo.ico",
+ },
+ chrome_settings_overrides: {
+ search_provider: {
+ is_default: true,
+ name: "MozParamsTest2",
+ keyword: "MozSearch",
+ search_url: kSearchEngineURL,
+ suggest_url: kSuggestURL,
+ suggest_url_get_params: kSuggestURLParams,
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ await ext1.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext1);
+
+ Assert.equal(
+ Services.search.defaultEngine.name,
+ "MozParamsTest2",
+ "Should have switched the default engine."
+ );
+
+ let engine = Services.search.getEngineByName("MozParamsTest2");
+ Assert.ok(engine, "Should have found the engine");
+
+ Assert.equal(
+ engine.getSubmission("{searchTerms}").uri.spec,
+ encodeURI("https://example.com/2/?q={searchTerms}&simple2=5"),
+ "Should have not changed the submission url."
+ );
+ Assert.equal(
+ engine.getSubmission("{searchTerms}", URLTYPE_SUGGEST_JSON),
+ null,
+ "Should not have set a suggestion URL."
+ );
+
+ let promiseDefaultBrowserChange = SearchTestUtils.promiseSearchNotification(
+ "engine-default",
+ "browser-search-engine-modified"
+ );
+ await ext1.unload();
+ await promiseDefaultBrowserChange;
+
+ Assert.equal(
+ Services.search.defaultEngine.name,
+ "MozParamsTest",
+ "Should have reverted to the original default engine."
+ );
+});
+
+add_task(async function test_extension_overriding_app_provided_default() {
+ const settings = await RemoteSettings(SearchUtils.SETTINGS_ALLOWLIST_KEY);
+ sinon.stub(settings, "get").returns([
+ {
+ thirdPartyId: "test@thirdparty.example.com",
+ overridesId: "test2@search.mozilla.org",
+ urls: [
+ {
+ search_url: "https://example.com/?q={searchTerms}&foo=myparams",
+ },
+ ],
+ },
+ ]);
+
+ let ext1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ applications: {
+ gecko: {
+ id: "test@thirdparty.example.com",
+ },
+ },
+ icons: {
+ "16": "foo.ico",
+ },
+ chrome_settings_overrides: {
+ search_provider: {
+ is_default: true,
+ name: "MozParamsTest2",
+ keyword: "MozSearch",
+ search_url: kSearchEngineURL,
+ suggest_url: kSuggestURL,
+ suggest_url_get_params: kSuggestURLParams,
+ },
+ },
+ },
+ useAddonManager: "permanent",
+ });
+
+ await ext1.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext1);
+
+ Assert.equal(
+ Services.search.defaultEngine.name,
+ "MozParamsTest2",
+ "Should have switched the default engine."
+ );
+
+ let engine = Services.search.getEngineByName("MozParamsTest2");
+ Assert.ok(engine, "Should have found the engine");
+
+ Assert.equal(
+ engine.getSubmission("{searchTerms}").uri.spec,
+ encodeURI(kSearchEngineURL),
+ "Should have changed the suggestion url."
+ );
+ Assert.equal(
+ engine.getSubmission("{searchTerms}", URLTYPE_SUGGEST_JSON).uri.spec,
+ encodeURI(`${kSuggestURL}?${kSuggestURLParams}`),
+ "Should set a submission URL."
+ );
+
+ let promiseDefaultBrowserChange = SearchTestUtils.promiseSearchNotification(
+ "engine-default",
+ "browser-search-engine-modified"
+ );
+ await ext1.unload();
+ await promiseDefaultBrowserChange;
+
+ Assert.equal(
+ Services.search.defaultEngine.name,
+ "MozParamsTest",
+ "Should have reverted to the original default engine."
+ );
+
+ engine = Services.search.getEngineByName("MozParamsTest2");
+ Assert.ok(engine, "Should have found the engine");
+
+ Assert.equal(
+ engine.getSubmission("{searchTerms}").uri.spec,
+ encodeURI("https://example.com/2/?q={searchTerms}&simple2=5"),
+ "Should have reverted the submission url."
+ );
+ Assert.equal(
+ engine.getSubmission("{searchTerms}", URLTYPE_SUGGEST_JSON),
+ null,
+ "Should have reverted the suggestion URL."
+ );
+});
diff --git a/browser/components/extensions/test/xpcshell/test_ext_settings_overrides_search.js b/browser/components/extensions/test/xpcshell/test_ext_settings_overrides_search.js
new file mode 100644
index 0000000000..ea4cdd6534
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_settings_overrides_search.js
@@ -0,0 +1,554 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.import(
+ "resource://testing-common/AddonTestUtils.jsm"
+);
+const { setTimeout } = ChromeUtils.import("resource://gre/modules/Timer.jsm");
+
+let delay = () => new Promise(resolve => setTimeout(resolve, 0));
+
+const kSearchFormURL = "https://example.com/searchform";
+const kSearchEngineURL = "https://example.com/?search={searchTerms}";
+const kSearchSuggestURL = "https://example.com/?suggest={searchTerms}";
+const kSearchTerm = "foo";
+const kSearchTermIntl = "日";
+const URLTYPE_SUGGEST_JSON = "application/x-suggestions+json";
+
+AddonTestUtils.init(this);
+AddonTestUtils.createAppInfo(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "42",
+ "42"
+);
+// Override ExtensionXPCShellUtils.jsm's overriding of the pref as the
+// search service needs it.
+Services.prefs.clearUserPref("services.settings.default_bucket");
+
+add_task(async function setup() {
+ await AddonTestUtils.promiseStartupManager();
+ await Services.search.init();
+});
+
+add_task(async function test_extension_adding_engine() {
+ let ext1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ icons: {
+ "16": "foo.ico",
+ "32": "foo32.ico",
+ },
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "MozSearch",
+ keyword: "MozSearch",
+ search_form: kSearchFormURL,
+ search_url: kSearchEngineURL,
+ suggest_url: kSearchSuggestURL,
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ await ext1.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext1);
+
+ let engine = Services.search.getEngineByName("MozSearch");
+ ok(engine, "Engine should exist.");
+
+ let { baseURI } = ext1.extension;
+ equal(engine.iconURI.spec, baseURI.resolve("foo.ico"), "icon path matches");
+ let icons = engine.getIcons();
+ equal(icons.length, 2, "both icons avialable");
+ equal(icons[0].url, baseURI.resolve("foo.ico"), "icon path matches");
+ equal(icons[1].url, baseURI.resolve("foo32.ico"), "icon path matches");
+
+ let expectedSuggestURL = kSearchSuggestURL.replace(
+ "{searchTerms}",
+ kSearchTerm
+ );
+ let submissionSuggest = engine.getSubmission(
+ kSearchTerm,
+ URLTYPE_SUGGEST_JSON
+ );
+ let encodedSubmissionURL = engine.getSubmission(kSearchTermIntl).uri.spec;
+ let testSubmissionURL = kSearchEngineURL.replace(
+ "{searchTerms}",
+ encodeURIComponent(kSearchTermIntl)
+ );
+ equal(
+ encodedSubmissionURL,
+ testSubmissionURL,
+ "Encoded UTF-8 URLs should match"
+ );
+
+ equal(
+ submissionSuggest.uri.spec,
+ expectedSuggestURL,
+ "Suggest URLs should match"
+ );
+
+ equal(engine.searchForm, kSearchFormURL, "Search form URLs should match");
+ await ext1.unload();
+ await delay();
+
+ engine = Services.search.getEngineByName("MozSearch");
+ ok(!engine, "Engine should not exist");
+});
+
+add_task(async function test_extension_adding_engine_with_spaces() {
+ let ext1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "MozSearch ",
+ keyword: "MozSearch",
+ search_url: "https://example.com/?q={searchTerms}",
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ await ext1.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext1);
+
+ let engine = Services.search.getEngineByName("MozSearch");
+ ok(engine, "Engine should exist.");
+
+ await ext1.unload();
+ await delay();
+
+ engine = Services.search.getEngineByName("MozSearch");
+ ok(!engine, "Engine should not exist");
+});
+
+add_task(async function test_upgrade_default_position_engine() {
+ let ext1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "MozSearch",
+ keyword: "MozSearch",
+ search_url: "https://example.com/?q={searchTerms}",
+ },
+ },
+ applications: {
+ gecko: {
+ id: "testengine@mozilla.com",
+ },
+ },
+ version: "0.1",
+ },
+ useAddonManager: "temporary",
+ });
+
+ await ext1.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext1);
+
+ let engine = Services.search.getEngineByName("MozSearch");
+ await Services.search.setDefault(engine);
+ await Services.search.moveEngine(engine, 1);
+
+ await ext1.upgrade({
+ manifest: {
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "MozSearch",
+ keyword: "MozSearch",
+ search_url: "https://example.com/?q={searchTerms}",
+ },
+ },
+ applications: {
+ gecko: {
+ id: "testengine@mozilla.com",
+ },
+ },
+ version: "0.2",
+ },
+ useAddonManager: "temporary",
+ });
+ await AddonTestUtils.waitForSearchProviderStartup(ext1);
+
+ engine = Services.search.getEngineByName("MozSearch");
+ equal(
+ Services.search.defaultEngine,
+ engine,
+ "Default engine should still be MozSearch"
+ );
+ equal(
+ (await Services.search.getEngines()).map(e => e.name).indexOf(engine.name),
+ 1,
+ "Engine is in position 1"
+ );
+
+ await ext1.unload();
+ await delay();
+
+ engine = Services.search.getEngineByName("MozSearch");
+ ok(!engine, "Engine should not exist");
+});
+
+add_task(async function test_extension_get_params() {
+ let ext1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "MozSearch",
+ keyword: "MozSearch",
+ search_url: kSearchEngineURL,
+ search_url_get_params: "foo=bar&bar=foo",
+ suggest_url: kSearchSuggestURL,
+ suggest_url_get_params: "foo=bar&bar=foo",
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ await ext1.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext1);
+
+ let engine = Services.search.getEngineByName("MozSearch");
+ ok(engine, "Engine should exist.");
+
+ let url = engine.wrappedJSObject._getURLOfType("text/html");
+ equal(url.method, "GET", "Search URLs method is GET");
+
+ let expectedURL = kSearchEngineURL.replace("{searchTerms}", kSearchTerm);
+ let submission = engine.getSubmission(kSearchTerm);
+ equal(
+ submission.uri.spec,
+ `${expectedURL}&foo=bar&bar=foo`,
+ "Search URLs should match"
+ );
+
+ let expectedSuggestURL = kSearchSuggestURL.replace(
+ "{searchTerms}",
+ kSearchTerm
+ );
+ let submissionSuggest = engine.getSubmission(
+ kSearchTerm,
+ URLTYPE_SUGGEST_JSON
+ );
+ equal(
+ submissionSuggest.uri.spec,
+ `${expectedSuggestURL}&foo=bar&bar=foo`,
+ "Suggest URLs should match"
+ );
+
+ await ext1.unload();
+});
+
+add_task(async function test_extension_post_params() {
+ let ext1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "MozSearch",
+ keyword: "MozSearch",
+ search_url: kSearchEngineURL,
+ search_url_post_params: "foo=bar&bar=foo",
+ suggest_url: kSearchSuggestURL,
+ suggest_url_post_params: "foo=bar&bar=foo",
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ await ext1.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext1);
+
+ let engine = Services.search.getEngineByName("MozSearch");
+ ok(engine, "Engine should exist.");
+
+ let url = engine.wrappedJSObject._getURLOfType("text/html");
+ equal(url.method, "POST", "Search URLs method is POST");
+
+ let expectedURL = kSearchEngineURL.replace("{searchTerms}", kSearchTerm);
+ let submission = engine.getSubmission(kSearchTerm);
+ equal(submission.uri.spec, expectedURL, "Search URLs should match");
+ // postData is a nsIMIMEInputStream which contains a nsIStringInputStream.
+ equal(
+ submission.postData.data.data,
+ "foo=bar&bar=foo",
+ "Search postData should match"
+ );
+
+ let expectedSuggestURL = kSearchSuggestURL.replace(
+ "{searchTerms}",
+ kSearchTerm
+ );
+ let submissionSuggest = engine.getSubmission(
+ kSearchTerm,
+ URLTYPE_SUGGEST_JSON
+ );
+ equal(
+ submissionSuggest.uri.spec,
+ expectedSuggestURL,
+ "Suggest URLs should match"
+ );
+ equal(
+ submissionSuggest.postData.data.data,
+ "foo=bar&bar=foo",
+ "Suggest postData should match"
+ );
+
+ await ext1.unload();
+});
+
+add_task(async function test_extension_no_query_params() {
+ const ext1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "MozSearch",
+ keyword: "MozSearch",
+ search_url: "https://example.com/{searchTerms}",
+ suggest_url: "https://example.com/suggest/{searchTerms}",
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ await ext1.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext1);
+
+ let engine = Services.search.getEngineByName("MozSearch");
+ ok(engine, "Engine should exist.");
+
+ const encodedSubmissionURL = engine.getSubmission(kSearchTermIntl).uri.spec;
+ const testSubmissionURL =
+ "https://example.com/" + encodeURIComponent(kSearchTermIntl);
+ equal(
+ encodedSubmissionURL,
+ testSubmissionURL,
+ "Encoded UTF-8 URLs should match"
+ );
+
+ const expectedSuggestURL = "https://example.com/suggest/" + kSearchTerm;
+ let submissionSuggest = engine.getSubmission(
+ kSearchTerm,
+ URLTYPE_SUGGEST_JSON
+ );
+ equal(
+ submissionSuggest.uri.spec,
+ expectedSuggestURL,
+ "Suggest URLs should match"
+ );
+
+ await ext1.unload();
+ await delay();
+
+ engine = Services.search.getEngineByName("MozSearch");
+ ok(!engine, "Engine should not exist");
+});
+
+add_task(async function test_extension_empty_suggestUrl() {
+ let ext1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ default_locale: "en",
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "MozSearch",
+ keyword: "MozSearch",
+ search_url: kSearchEngineURL,
+ search_url_post_params: "foo=bar&bar=foo",
+ suggest_url: "__MSG_suggestUrl__",
+ suggest_url_get_params: "__MSG_suggestUrlGetParams__",
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ files: {
+ "_locales/en/messages.json": {
+ suggestUrl: {
+ message: "",
+ },
+ suggestUrlGetParams: {
+ message: "",
+ },
+ },
+ },
+ });
+
+ await ext1.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext1);
+
+ let engine = Services.search.getEngineByName("MozSearch");
+ ok(engine, "Engine should exist.");
+
+ let url = engine.wrappedJSObject._getURLOfType("text/html");
+ equal(url.method, "POST", "Search URLs method is POST");
+
+ let expectedURL = kSearchEngineURL.replace("{searchTerms}", kSearchTerm);
+ let submission = engine.getSubmission(kSearchTerm);
+ equal(submission.uri.spec, expectedURL, "Search URLs should match");
+ // postData is a nsIMIMEInputStream which contains a nsIStringInputStream.
+ equal(
+ submission.postData.data.data,
+ "foo=bar&bar=foo",
+ "Search postData should match"
+ );
+
+ let submissionSuggest = engine.getSubmission(
+ kSearchTerm,
+ URLTYPE_SUGGEST_JSON
+ );
+ ok(!submissionSuggest, "There should be no suggest URL.");
+
+ await ext1.unload();
+});
+
+add_task(async function test_extension_empty_suggestUrl_with_params() {
+ let ext1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ default_locale: "en",
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "MozSearch",
+ keyword: "MozSearch",
+ search_url: kSearchEngineURL,
+ search_url_post_params: "foo=bar&bar=foo",
+ suggest_url: "__MSG_suggestUrl__",
+ suggest_url_get_params: "__MSG_suggestUrlGetParams__",
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ files: {
+ "_locales/en/messages.json": {
+ suggestUrl: {
+ message: "",
+ },
+ suggestUrlGetParams: {
+ message: "abc",
+ },
+ },
+ },
+ });
+
+ await ext1.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext1);
+
+ let engine = Services.search.getEngineByName("MozSearch");
+ ok(engine, "Engine should exist.");
+
+ let url = engine.wrappedJSObject._getURLOfType("text/html");
+ equal(url.method, "POST", "Search URLs method is POST");
+
+ let expectedURL = kSearchEngineURL.replace("{searchTerms}", kSearchTerm);
+ let submission = engine.getSubmission(kSearchTerm);
+ equal(submission.uri.spec, expectedURL, "Search URLs should match");
+ // postData is a nsIMIMEInputStream which contains a nsIStringInputStream.
+ equal(
+ submission.postData.data.data,
+ "foo=bar&bar=foo",
+ "Search postData should match"
+ );
+
+ let submissionSuggest = engine.getSubmission(
+ kSearchTerm,
+ URLTYPE_SUGGEST_JSON
+ );
+ ok(!submissionSuggest, "There should be no suggest URL.");
+
+ await ext1.unload();
+});
+
+async function checkBadUrl(searchProviderKey, urlValue) {
+ let normalized = await ExtensionTestUtils.normalizeManifest({
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "MozSearch",
+ keyword: "MozSearch",
+ search_url: "https://example.com/",
+ [searchProviderKey]: urlValue,
+ },
+ },
+ });
+
+ ok(
+ /Error processing chrome_settings_overrides\.search_provider[^:]*: .* must match/.test(
+ normalized.error
+ ),
+ `Expected error for ${searchProviderKey}:${urlValue} "${normalized.error}"`
+ );
+}
+
+async function checkValidUrl(urlValue) {
+ let normalized = await ExtensionTestUtils.normalizeManifest({
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "MozSearch",
+ keyword: "MozSearch",
+ search_form: urlValue,
+ search_url: urlValue,
+ suggest_url: urlValue,
+ },
+ },
+ });
+ equal(normalized.error, undefined, `Valid search_provider url: ${urlValue}`);
+}
+
+add_task(async function test_extension_not_allow_http() {
+ await checkBadUrl("search_form", "http://example.com/{searchTerms}");
+ await checkBadUrl("search_url", "http://example.com/{searchTerms}");
+ await checkBadUrl("suggest_url", "http://example.com/{searchTerms}");
+});
+
+add_task(async function test_manifest_disallows_http_localhost_prefix() {
+ await checkBadUrl("search_url", "http://localhost.example.com");
+ await checkBadUrl("search_url", "http://localhost.example.com/");
+ await checkBadUrl("search_url", "http://127.0.0.1.example.com/");
+ await checkBadUrl("search_url", "http://localhost:1234@example.com/");
+});
+
+add_task(async function test_manifest_allow_http_for_localhost() {
+ await checkValidUrl("http://localhost");
+ await checkValidUrl("http://localhost/");
+ await checkValidUrl("http://localhost:/");
+ await checkValidUrl("http://localhost:1/");
+ await checkValidUrl("http://localhost:65535/");
+
+ await checkValidUrl("http://127.0.0.1");
+ await checkValidUrl("http://127.0.0.1:");
+ await checkValidUrl("http://127.0.0.1:/");
+ await checkValidUrl("http://127.0.0.1/");
+ await checkValidUrl("http://127.0.0.1:80/");
+
+ await checkValidUrl("http://[::1]");
+ await checkValidUrl("http://[::1]:");
+ await checkValidUrl("http://[::1]:/");
+ await checkValidUrl("http://[::1]/");
+ await checkValidUrl("http://[::1]:80/");
+});
+
+add_task(async function test_extension_allow_http_for_localhost() {
+ let ext1 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "MozSearch",
+ keyword: "MozSearch",
+ search_url: "http://localhost/{searchTerms}",
+ suggest_url: "http://localhost/suggest/{searchTerms}",
+ },
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ await ext1.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext1);
+
+ let engine = Services.search.getEngineByName("MozSearch");
+ ok(engine, "Engine should exist.");
+
+ await ext1.unload();
+});
diff --git a/browser/components/extensions/test/xpcshell/test_ext_settings_overrides_search_mozParam.js b/browser/components/extensions/test/xpcshell/test_ext_settings_overrides_search_mozParam.js
new file mode 100644
index 0000000000..94f2742fdc
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_settings_overrides_search_mozParam.js
@@ -0,0 +1,158 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.import(
+ "resource://testing-common/AddonTestUtils.jsm"
+);
+
+const { SearchTestUtils } = ChromeUtils.import(
+ "resource://testing-common/SearchTestUtils.jsm"
+);
+
+AddonTestUtils.init(this);
+AddonTestUtils.overrideCertDB();
+AddonTestUtils.createAppInfo(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "42",
+ "42"
+);
+// Override ExtensionXPCShellUtils.jsm's overriding of the pref as the
+// search service needs it.
+Services.prefs.clearUserPref("services.settings.default_bucket");
+
+let { promiseShutdownManager, promiseStartupManager } = AddonTestUtils;
+
+// Note: these lists should be kept in sync with the lists in
+// browser/components/extensions/test/xpcshell/data/test/manifest.json
+// These params are conditional based on how search is initiated.
+const mozParams = [
+ {
+ name: "test-0",
+ condition: "purpose",
+ purpose: "contextmenu",
+ value: "0",
+ },
+ { name: "test-1", condition: "purpose", purpose: "searchbar", value: "1" },
+ { name: "test-2", condition: "purpose", purpose: "homepage", value: "2" },
+ { name: "test-3", condition: "purpose", purpose: "keyword", value: "3" },
+ { name: "test-4", condition: "purpose", purpose: "newtab", value: "4" },
+];
+// These params are always included.
+const params = [
+ { name: "simple", value: "5" },
+ { name: "term", value: "{searchTerms}" },
+ { name: "lang", value: "{language}" },
+ { name: "locale", value: "{moz:locale}" },
+ { name: "prefval", condition: "pref", pref: "code" },
+];
+
+add_task(async function setup() {
+ await promiseStartupManager();
+ await SearchTestUtils.useTestEngines("data", null, [
+ {
+ webExtension: {
+ id: "test@search.mozilla.org",
+ },
+ appliesTo: [
+ {
+ included: { everywhere: true },
+ default: "yes",
+ },
+ ],
+ },
+ ]);
+ await Services.search.init();
+ registerCleanupFunction(async () => {
+ await promiseShutdownManager();
+ });
+});
+
+/* This tests setting moz params. */
+add_task(async function test_extension_setting_moz_params() {
+ let defaultBranch = Services.prefs.getDefaultBranch("browser.search.");
+ defaultBranch.setCharPref("param.code", "good");
+
+ let engine = Services.search.getEngineByName("MozParamsTest");
+
+ let extraParams = [];
+ for (let p of params) {
+ if (p.condition == "pref") {
+ extraParams.push(`${p.name}=good`);
+ } else if (p.value == "{searchTerms}") {
+ extraParams.push(`${p.name}=test`);
+ } else if (p.value == "{language}") {
+ extraParams.push(`${p.name}=${Services.locale.requestedLocale || "*"}`);
+ } else if (p.value == "{moz:locale}") {
+ extraParams.push(`${p.name}=${Services.locale.requestedLocale}`);
+ } else {
+ extraParams.push(`${p.name}=${p.value}`);
+ }
+ }
+ let paramStr = extraParams.join("&");
+
+ for (let p of mozParams) {
+ let expectedURL = engine.getSubmission(
+ "test",
+ null,
+ p.condition == "purpose" ? p.purpose : null
+ ).uri.spec;
+ equal(
+ expectedURL,
+ `https://example.com/?q=test&${p.name}=${p.value}&${paramStr}`,
+ "search url is expected"
+ );
+ }
+});
+
+add_task(async function test_extension_setting_moz_params_fail() {
+ // Ensure that the test infra does not automatically make
+ // this privileged.
+ AddonTestUtils.usePrivilegedSignatures = false;
+ Services.prefs.setCharPref(
+ "extensions.installedDistroAddon.test@mochitest",
+ ""
+ );
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ applications: {
+ gecko: { id: "test1@mochitest" },
+ },
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "MozParamsTest1",
+ search_url: "https://example.com/",
+ params: [
+ {
+ name: "testParam",
+ condition: "purpose",
+ purpose: "contextmenu",
+ value: "0",
+ },
+ { name: "prefval", condition: "pref", pref: "code" },
+ { name: "q", value: "{searchTerms}" },
+ ],
+ },
+ },
+ },
+ useAddonManager: "permanent",
+ });
+ await extension.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(extension);
+ equal(
+ extension.extension.isPrivileged,
+ false,
+ "extension is not priviledged"
+ );
+ let engine = Services.search.getEngineByName("MozParamsTest1");
+ let expectedURL = engine.getSubmission("test", null, "contextmenu").uri.spec;
+ equal(
+ expectedURL,
+ "https://example.com/?q=test",
+ "engine cannot have conditional or pref params"
+ );
+ await extension.unload();
+});
diff --git a/browser/components/extensions/test/xpcshell/test_ext_settings_overrides_shutdown.js b/browser/components/extensions/test/xpcshell/test_ext_settings_overrides_shutdown.js
new file mode 100644
index 0000000000..d4c9f89f35
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_settings_overrides_shutdown.js
@@ -0,0 +1,114 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.import(
+ "resource://testing-common/AddonTestUtils.jsm"
+);
+// Lazily import ExtensionParent to allow AddonTestUtils.createAppInfo to
+// override Services.appinfo.
+ChromeUtils.defineModuleGetter(
+ this,
+ "ExtensionParent",
+ "resource://gre/modules/ExtensionParent.jsm"
+);
+
+AddonTestUtils.init(this);
+AddonTestUtils.overrideCertDB();
+AddonTestUtils.createAppInfo(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "42",
+ "42"
+);
+// Override ExtensionXPCShellUtils.jsm's overriding of the pref as the
+// search service needs it.
+Services.prefs.clearUserPref("services.settings.default_bucket");
+
+add_task(async function shutdown_during_search_provider_startup() {
+ await AddonTestUtils.promiseStartupManager();
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ chrome_settings_overrides: {
+ search_provider: {
+ is_default: true,
+ name: "dummy name",
+ search_url: "https://example.com/",
+ },
+ },
+ },
+ });
+
+ info("Starting up search extension");
+ await extension.startup();
+ let extStartPromise = AddonTestUtils.waitForSearchProviderStartup(extension, {
+ // Search provider registration is expected to be pending because the search
+ // service has not been initialized yet.
+ expectPending: true,
+ });
+
+ let initialized = false;
+ ExtensionParent.apiManager.global.searchInitialized.then(() => {
+ initialized = true;
+ });
+
+ await extension.addon.disable();
+
+ info("Extension managed to shut down despite the uninitialized search");
+ // Initialize search after extension shutdown to check that it does not cause
+ // any problems, and that the test can continue to test uninstall behavior.
+ Assert.ok(!initialized, "Search service should not have been initialized");
+
+ extension.addon.enable();
+ await extension.awaitStartup();
+
+ // Check that uninstall is blocked until the search registration at startup
+ // has finished. This registration only finished once the search service is
+ // initialized.
+ let uninstallingPromise = new Promise(resolve => {
+ let Management = ExtensionParent.apiManager;
+ Management.on("uninstall", function listener(eventName, { id }) {
+ Management.off("uninstall", listener);
+ Assert.equal(id, extension.id, "Expected extension");
+ resolve();
+ });
+ });
+
+ let extRestartPromise = AddonTestUtils.waitForSearchProviderStartup(
+ extension,
+ {
+ // Search provider registration is expected to be pending again,
+ // because the search service has still not been initialized yet.
+ expectPending: true,
+ }
+ );
+
+ let uninstalledPromise = extension.addon.uninstall();
+ let uninstalled = false;
+ uninstalledPromise.then(() => {
+ uninstalled = true;
+ });
+
+ await uninstallingPromise;
+ Assert.ok(!uninstalled, "Uninstall should not be finished yet");
+ Assert.ok(!initialized, "Search service should still be uninitialized");
+ await Services.search.init();
+ Assert.ok(initialized, "Search service should be initialized");
+
+ // After initializing the search service, the search provider registration
+ // promises should settle eventually.
+
+ // Despite the interrupted startup, the promise should still resolve without
+ // an error.
+ await extStartPromise;
+ // The extension that is still active. The promise should just resolve.
+ await extRestartPromise;
+
+ // After initializing the search service, uninstall should eventually finish.
+ await uninstalledPromise;
+
+ await AddonTestUtils.promiseShutdownManager();
+});
diff --git a/browser/components/extensions/test/xpcshell/test_ext_settings_validate.js b/browser/components/extensions/test/xpcshell/test_ext_settings_validate.js
new file mode 100644
index 0000000000..874a009018
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_settings_validate.js
@@ -0,0 +1,193 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.import(
+ "resource://testing-common/AddonTestUtils.jsm"
+);
+
+const { AddonManager } = ChromeUtils.import(
+ "resource://gre/modules/AddonManager.jsm"
+);
+
+const { AboutNewTab } = ChromeUtils.import(
+ "resource:///modules/AboutNewTab.jsm"
+);
+
+// Lazy load to avoid having Services.appinfo cached first.
+ChromeUtils.defineModuleGetter(
+ this,
+ "ExtensionParent",
+ "resource://gre/modules/ExtensionParent.jsm"
+);
+
+const { HomePage } = ChromeUtils.import("resource:///modules/HomePage.jsm");
+
+AddonTestUtils.init(this);
+
+// Allow for unsigned addons.
+AddonTestUtils.overrideCertDB();
+
+AddonTestUtils.createAppInfo(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "42",
+ "42"
+);
+
+add_task(async function test_settings_modules_not_loaded() {
+ await ExtensionParent.apiManager.lazyInit();
+ // Test that no settings modules are loaded.
+ let modules = Array.from(ExtensionParent.apiManager.settingsModules);
+ ok(modules.length, "we have settings modules");
+ for (let name of modules) {
+ ok(
+ !ExtensionParent.apiManager.getModule(name).loaded,
+ `${name} is not loaded`
+ );
+ }
+});
+
+add_task(async function test_settings_validated() {
+ let defaultNewTab = AboutNewTab.newTabURL;
+ equal(defaultNewTab, "about:newtab", "Newtab url is default.");
+ let defaultHomepageURL = HomePage.get();
+ equal(defaultHomepageURL, "about:home", "Home page url is default.");
+
+ let xpi = await AddonTestUtils.createTempWebExtensionFile({
+ manifest: {
+ version: "1.0",
+ applications: { gecko: { id: "test@mochi" } },
+ chrome_url_overrides: {
+ newtab: "/newtab",
+ },
+ chrome_settings_overrides: {
+ homepage: "https://example.com/",
+ },
+ },
+ });
+ let extension = ExtensionTestUtils.expectExtension("test@mochi");
+ let file = await AddonTestUtils.manuallyInstall(xpi);
+ await AddonTestUtils.promiseStartupManager();
+ await extension.awaitStartup();
+
+ equal(
+ HomePage.get(),
+ "https://example.com/",
+ "Home page url is extension controlled."
+ );
+ ok(
+ AboutNewTab.newTabURL.endsWith("/newtab"),
+ "newTabURL is extension controlled."
+ );
+
+ await AddonTestUtils.promiseShutdownManager();
+ // After shutdown, delete the xpi file.
+ Services.obs.notifyObservers(xpi, "flush-cache-entry");
+ try {
+ file.remove(true);
+ } catch (e) {
+ ok(false, e);
+ }
+ await AddonTestUtils.cleanupTempXPIs();
+
+ // Restart everything, the ExtensionAddonObserver should handle updating state.
+ let prefChanged = TestUtils.waitForPrefChange("browser.startup.homepage");
+ await AddonTestUtils.promiseStartupManager();
+ await prefChanged;
+
+ equal(HomePage.get(), defaultHomepageURL, "Home page url is default.");
+ equal(AboutNewTab.newTabURL, defaultNewTab, "newTabURL is reset to default.");
+ await AddonTestUtils.promiseShutdownManager();
+});
+
+add_task(async function test_settings_validated_safemode() {
+ let defaultNewTab = AboutNewTab.newTabURL;
+ equal(defaultNewTab, "about:newtab", "Newtab url is default.");
+ let defaultHomepageURL = HomePage.get();
+ equal(defaultHomepageURL, "about:home", "Home page url is default.");
+
+ function isDefaultSettings(postfix) {
+ equal(
+ HomePage.get(),
+ defaultHomepageURL,
+ `Home page url is default ${postfix}.`
+ );
+ equal(
+ AboutNewTab.newTabURL,
+ defaultNewTab,
+ `newTabURL is default ${postfix}.`
+ );
+ }
+
+ function isExtensionSettings(postfix) {
+ equal(
+ HomePage.get(),
+ "https://example.com/",
+ `Home page url is extension controlled ${postfix}.`
+ );
+ ok(
+ AboutNewTab.newTabURL.endsWith("/newtab"),
+ `newTabURL is extension controlled ${postfix}.`
+ );
+ }
+
+ async function switchSafeMode(inSafeMode) {
+ await AddonTestUtils.promiseShutdownManager();
+ AddonTestUtils.appInfo.inSafeMode = inSafeMode;
+ await AddonTestUtils.promiseStartupManager();
+ return AddonManager.getAddonByID("test@mochi");
+ }
+
+ let xpi = await AddonTestUtils.createTempWebExtensionFile({
+ manifest: {
+ version: "1.0",
+ applications: { gecko: { id: "test@mochi" } },
+ chrome_url_overrides: {
+ newtab: "/newtab",
+ },
+ chrome_settings_overrides: {
+ homepage: "https://example.com/",
+ },
+ },
+ });
+ let extension = ExtensionTestUtils.expectExtension("test@mochi");
+ await AddonTestUtils.manuallyInstall(xpi);
+ await AddonTestUtils.promiseStartupManager();
+ await extension.awaitStartup();
+
+ isExtensionSettings("on extension startup");
+
+ // Disable in safemode and verify settings are removed in normal mode.
+ let addon = await switchSafeMode(true);
+ await addon.disable();
+ addon = await switchSafeMode(false);
+ isDefaultSettings("after disabling addon during safemode");
+
+ // Enable in safemode and verify settings are back in normal mode.
+ addon = await switchSafeMode(true);
+ await addon.enable();
+ addon = await switchSafeMode(false);
+ isExtensionSettings("after enabling addon during safemode");
+
+ // Uninstall in safemode and verify settings are removed in normal mode.
+ addon = await switchSafeMode(true);
+ await addon.uninstall();
+ addon = await switchSafeMode(false);
+ isDefaultSettings("after uninstalling addon during safemode");
+
+ await AddonTestUtils.promiseShutdownManager();
+ await AddonTestUtils.cleanupTempXPIs();
+});
+
+// There are more settings modules than used in this test file, they should have been
+// loaded during the test extensions uninstall. Ensure that all settings modules have
+// been loaded.
+add_task(async function test_settings_modules_loaded() {
+ // Test that all settings modules are loaded.
+ let modules = Array.from(ExtensionParent.apiManager.settingsModules);
+ ok(modules.length, "we have settings modules");
+ for (let name of modules) {
+ ok(ExtensionParent.apiManager.getModule(name).loaded, `${name} was loaded`);
+ }
+});
diff --git a/browser/components/extensions/test/xpcshell/test_ext_topSites.js b/browser/components/extensions/test/xpcshell/test_ext_topSites.js
new file mode 100644
index 0000000000..7fb604338e
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_topSites.js
@@ -0,0 +1,303 @@
+"use strict";
+
+const { PlacesUtils } = ChromeUtils.import(
+ "resource://gre/modules/PlacesUtils.jsm"
+);
+const { NewTabUtils } = ChromeUtils.import(
+ "resource://gre/modules/NewTabUtils.jsm"
+);
+const { PlacesTestUtils } = ChromeUtils.import(
+ "resource://testing-common/PlacesTestUtils.jsm"
+);
+
+const SEARCH_SHORTCUTS_EXPERIMENT_PREF =
+ "browser.newtabpage.activity-stream.improvesearch.topSiteSearchShortcuts";
+
+// A small 1x1 test png
+const IMAGE_1x1 =
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg==";
+
+add_task(async function test_topSites() {
+ Services.prefs.setBoolPref(SEARCH_SHORTCUTS_EXPERIMENT_PREF, false);
+ let visits = [];
+ const numVisits = 15; // To make sure we get frecency.
+ let visitDate = new Date(1999, 9, 9, 9, 9).getTime();
+
+ function setVisit(visit) {
+ for (let j = 0; j < numVisits; ++j) {
+ visitDate -= 1000;
+ visit.visits.push({ date: new Date(visitDate) });
+ }
+ visits.push(visit);
+ }
+ // Stick a couple sites into history.
+ for (let i = 0; i < 2; ++i) {
+ setVisit({
+ url: `http://example${i}.com/`,
+ title: `visit${i}`,
+ visits: [],
+ });
+ setVisit({
+ url: `http://www.example${i}.com/foobar`,
+ title: `visit${i}-www`,
+ visits: [],
+ });
+ }
+ NewTabUtils.init();
+ await PlacesUtils.history.insertMany(visits);
+
+ // Insert a favicon to show that favicons are not returned by default.
+ let faviconData = new Map();
+ faviconData.set("http://example0.com", IMAGE_1x1);
+ await PlacesTestUtils.addFavicons(faviconData);
+
+ // Ensure our links show up in activityStream.
+ let links = await NewTabUtils.activityStreamLinks.getTopSites({
+ onePerDomain: false,
+ topsiteFrecency: 1,
+ });
+
+ equal(
+ links.length,
+ visits.length,
+ "Top sites has been successfully initialized"
+ );
+
+ // Drop the visits.visits for later testing.
+ visits = visits.map(v => {
+ return { url: v.url, title: v.title, favicon: undefined, type: "url" };
+ });
+
+ // Test that results from all providers are returned by default.
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["topSites"],
+ },
+ background() {
+ browser.test.onMessage.addListener(async options => {
+ let sites;
+ if (typeof options !== undefined) {
+ sites = await browser.topSites.get(options);
+ } else {
+ sites = await browser.topSites.get();
+ }
+ browser.test.sendMessage("sites", sites);
+ });
+ },
+ });
+
+ await extension.startup();
+
+ function getSites(options) {
+ extension.sendMessage(options);
+ return extension.awaitMessage("sites");
+ }
+
+ Assert.deepEqual(
+ [visits[0], visits[2]],
+ await getSites(),
+ "got topSites default"
+ );
+ Assert.deepEqual(
+ visits,
+ await getSites({ onePerDomain: false }),
+ "got topSites all links"
+ );
+
+ NewTabUtils.activityStreamLinks.blockURL(visits[0]);
+ ok(
+ NewTabUtils.blockedLinks.isBlocked(visits[0]),
+ `link ${visits[0].url} is blocked`
+ );
+
+ Assert.deepEqual(
+ [visits[2], visits[1]],
+ await getSites(),
+ "got topSites with blocked links filtered out"
+ );
+ Assert.deepEqual(
+ [visits[0], visits[2]],
+ await getSites({ includeBlocked: true }),
+ "got topSites with blocked links included"
+ );
+
+ // Test favicon result
+ let topSites = await getSites({ includeBlocked: true, includeFavicon: true });
+ equal(topSites[0].favicon, IMAGE_1x1, "received favicon");
+
+ equal(
+ 1,
+ (await getSites({ limit: 1, includeBlocked: true })).length,
+ "limit 1 topSite"
+ );
+
+ NewTabUtils.uninit();
+ await extension.unload();
+ await PlacesUtils.history.clear();
+ Services.prefs.clearUserPref(SEARCH_SHORTCUTS_EXPERIMENT_PREF);
+});
+
+// Test pinned likns and search shortcuts.
+add_task(async function test_topSites_complete() {
+ Services.prefs.setBoolPref(SEARCH_SHORTCUTS_EXPERIMENT_PREF, true);
+ NewTabUtils.init();
+ let time = new Date();
+ let pinnedIndex = 0;
+ let entries = [
+ {
+ url: `http://pinned1.com/`,
+ title: "pinned1",
+ type: "url",
+ pinned: pinnedIndex++,
+ visitDate: time,
+ },
+ {
+ url: `http://search1.com/`,
+ title: "@search1",
+ type: "search",
+ pinned: pinnedIndex++,
+ visitDate: new Date(--time),
+ },
+ {
+ url: `https://amazon.com`,
+ title: "@amazon",
+ type: "search",
+ visitDate: new Date(--time),
+ },
+ {
+ url: `http://history1.com/`,
+ title: "history1",
+ type: "url",
+ visitDate: new Date(--time),
+ },
+ {
+ url: `http://history2.com/`,
+ title: "history2",
+ type: "url",
+ visitDate: new Date(--time),
+ },
+ {
+ url: `https://blocked1.com/`,
+ title: "blocked1",
+ type: "blocked",
+ visitDate: new Date(--time),
+ },
+ ];
+
+ for (let entry of entries) {
+ // Build up frecency.
+ await PlacesUtils.history.insert({
+ url: entry.url,
+ title: entry.title,
+ visits: new Array(15).fill({
+ date: entry.visitDate,
+ transition: PlacesUtils.history.TRANSITIONS.LINK,
+ }),
+ });
+ // Insert a favicon to show that favicons are not returned by default.
+ await PlacesTestUtils.addFavicons(new Map([[entry.url, IMAGE_1x1]]));
+ if (entry.pinned !== undefined) {
+ let info =
+ entry.type == "search"
+ ? { url: entry.url, label: entry.title, searchTopSite: true }
+ : { url: entry.url, title: entry.title };
+ NewTabUtils.pinnedLinks.pin(info, entry.pinned);
+ }
+ if (entry.type == "blocked") {
+ NewTabUtils.activityStreamLinks.blockURL({ url: entry.url });
+ }
+ }
+
+ // Some transformation is necessary to match output data.
+ let expectedResults = entries
+ .filter(e => e.type != "blocked")
+ .map(e => {
+ e.favicon = undefined;
+ delete e.visitDate;
+ delete e.pinned;
+ return e;
+ });
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["topSites"],
+ },
+ background() {
+ browser.test.onMessage.addListener(async options => {
+ let sites;
+ if (typeof options !== undefined) {
+ sites = await browser.topSites.get(options);
+ } else {
+ sites = await browser.topSites.get();
+ }
+ browser.test.sendMessage("sites", sites);
+ });
+ },
+ });
+
+ await extension.startup();
+
+ // Test that results are returned by the API.
+ function getSites(options) {
+ extension.sendMessage(options);
+ return extension.awaitMessage("sites");
+ }
+
+ Assert.deepEqual(
+ expectedResults,
+ await getSites({ includePinned: true, includeSearchShortcuts: true }),
+ "got topSites all links"
+ );
+
+ // Test no shortcuts.
+ dump(JSON.stringify(await getSites({ includePinned: true })) + "\n");
+ Assert.ok(
+ !(await getSites({ includePinned: true })).some(
+ link => link.type == "search"
+ ),
+ "should get no shortcuts"
+ );
+
+ // Test favicons.
+ let topSites = await getSites({
+ includePinned: true,
+ includeSearchShortcuts: true,
+ includeFavicon: true,
+ });
+ Assert.ok(
+ topSites.every(f => f.favicon == IMAGE_1x1),
+ "favicon is correct"
+ );
+
+ // Test options.limit.
+ Assert.equal(
+ 1,
+ (
+ await getSites({
+ includePinned: true,
+ includeSearchShortcuts: true,
+ limit: 1,
+ })
+ ).length,
+ "limit to 1 topSite"
+ );
+
+ // Clear history for a pinned entry, then check results.
+ await PlacesUtils.history.remove("http://pinned1.com/");
+ let links = await getSites({ includePinned: true });
+ Assert.ok(
+ links.find(link => link.url == "http://pinned1.com/"),
+ "Check unvisited pinned links are returned."
+ );
+ links = await getSites();
+ Assert.ok(
+ !links.find(link => link.url == "http://pinned1.com/"),
+ "Check unvisited pinned links are not returned."
+ );
+
+ await extension.unload();
+ NewTabUtils.uninit();
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+ Services.prefs.clearUserPref(SEARCH_SHORTCUTS_EXPERIMENT_PREF);
+});
diff --git a/browser/components/extensions/test/xpcshell/test_ext_url_overrides_newtab.js b/browser/components/extensions/test/xpcshell/test_ext_url_overrides_newtab.js
new file mode 100644
index 0000000000..34edeea93d
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_url_overrides_newtab.js
@@ -0,0 +1,351 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+XPCOMUtils.defineLazyGetter(this, "Management", () => {
+ // eslint-disable-next-line no-shadow
+ const { Management } = ChromeUtils.import(
+ "resource://gre/modules/Extension.jsm",
+ null
+ );
+ return Management;
+});
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "AddonManager",
+ "resource://gre/modules/AddonManager.jsm"
+);
+
+const { AboutNewTab } = ChromeUtils.import(
+ "resource:///modules/AboutNewTab.jsm"
+);
+
+const { AddonTestUtils } = ChromeUtils.import(
+ "resource://testing-common/AddonTestUtils.jsm"
+);
+
+const {
+ createAppInfo,
+ promiseRestartManager,
+ promiseShutdownManager,
+ promiseStartupManager,
+} = AddonTestUtils;
+
+AddonTestUtils.init(this);
+AddonTestUtils.overrideCertDB();
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "42", "42");
+
+function awaitEvent(eventName) {
+ return new Promise(resolve => {
+ Management.once(eventName, (e, ...args) => resolve(...args));
+ });
+}
+
+const DEFAULT_NEW_TAB_URL = AboutNewTab.newTabURL;
+
+add_task(async function test_multiple_extensions_overriding_newtab_page() {
+ const NEWTAB_URI_2 = "webext-newtab-1.html";
+ const NEWTAB_URI_3 = "webext-newtab-2.html";
+ const EXT_2_ID = "ext2@tests.mozilla.org";
+ const EXT_3_ID = "ext3@tests.mozilla.org";
+
+ const CONTROLLED_BY_THIS = "controlled_by_this_extension";
+ const CONTROLLED_BY_OTHER = "controlled_by_other_extensions";
+ const NOT_CONTROLLABLE = "not_controllable";
+
+ const NEW_TAB_PRIVATE_ALLOWED = "browser.newtab.privateAllowed";
+ const NEW_TAB_EXTENSION_CONTROLLED = "browser.newtab.extensionControlled";
+
+ function background() {
+ browser.test.onMessage.addListener(async msg => {
+ switch (msg) {
+ case "checkNewTabPage":
+ let newTabPage = await browser.browserSettings.newTabPageOverride.get(
+ {}
+ );
+ browser.test.sendMessage("newTabPage", newTabPage);
+ break;
+ case "trySet":
+ let setResult = await browser.browserSettings.newTabPageOverride.set({
+ value: "foo",
+ });
+ browser.test.assertFalse(
+ setResult,
+ "Calling newTabPageOverride.set returns false."
+ );
+ browser.test.sendMessage("newTabPageSet");
+ break;
+ case "tryClear":
+ let clearResult = await browser.browserSettings.newTabPageOverride.clear(
+ {}
+ );
+ browser.test.assertFalse(
+ clearResult,
+ "Calling newTabPageOverride.clear returns false."
+ );
+ browser.test.sendMessage("newTabPageCleared");
+ break;
+ }
+ });
+ }
+
+ async function checkNewTabPageOverride(
+ ext,
+ expectedValue,
+ expectedLevelOfControl
+ ) {
+ ext.sendMessage("checkNewTabPage");
+ let newTabPage = await ext.awaitMessage("newTabPage");
+
+ ok(
+ newTabPage.value.endsWith(expectedValue),
+ `newTabPageOverride setting returns the expected value ending with: ${expectedValue}.`
+ );
+ equal(
+ newTabPage.levelOfControl,
+ expectedLevelOfControl,
+ `newTabPageOverride setting returns the expected levelOfControl: ${expectedLevelOfControl}.`
+ );
+ }
+
+ function verifyNewTabSettings(ext, expectedLevelOfControl) {
+ if (expectedLevelOfControl !== NOT_CONTROLLABLE) {
+ // Verify the preferences are set as expected.
+ let policy = WebExtensionPolicy.getByID(ext.id);
+ equal(
+ policy && policy.privateBrowsingAllowed,
+ Services.prefs.getBoolPref(NEW_TAB_PRIVATE_ALLOWED),
+ "private browsing flag set correctly"
+ );
+ ok(
+ Services.prefs.getBoolPref(NEW_TAB_EXTENSION_CONTROLLED),
+ `extension controlled flag set correctly`
+ );
+ } else {
+ ok(
+ !Services.prefs.prefHasUserValue(NEW_TAB_PRIVATE_ALLOWED),
+ "controlled flag reset"
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(NEW_TAB_EXTENSION_CONTROLLED),
+ "controlled flag reset"
+ );
+ }
+ }
+
+ let extObj = {
+ manifest: {
+ chrome_url_overrides: {},
+ permissions: ["browserSettings"],
+ },
+ useAddonManager: "temporary",
+ background,
+ };
+
+ let ext1 = ExtensionTestUtils.loadExtension(extObj);
+
+ extObj.manifest.chrome_url_overrides = { newtab: NEWTAB_URI_2 };
+ extObj.manifest.applications = { gecko: { id: EXT_2_ID } };
+ let ext2 = ExtensionTestUtils.loadExtension(extObj);
+
+ extObj.manifest.chrome_url_overrides = { newtab: NEWTAB_URI_3 };
+ extObj.manifest.applications.gecko.id = EXT_3_ID;
+ extObj.incognitoOverride = "spanning";
+ let ext3 = ExtensionTestUtils.loadExtension(extObj);
+
+ equal(
+ AboutNewTab.newTabURL,
+ DEFAULT_NEW_TAB_URL,
+ "newTabURL is set to the default."
+ );
+
+ await promiseStartupManager();
+
+ await ext1.startup();
+ equal(
+ AboutNewTab.newTabURL,
+ DEFAULT_NEW_TAB_URL,
+ "newTabURL is still set to the default."
+ );
+
+ await checkNewTabPageOverride(ext1, AboutNewTab.newTabURL, NOT_CONTROLLABLE);
+ verifyNewTabSettings(ext1, NOT_CONTROLLABLE);
+
+ await ext2.startup();
+ ok(
+ AboutNewTab.newTabURL.endsWith(NEWTAB_URI_2),
+ "newTabURL is overridden by the second extension."
+ );
+ await checkNewTabPageOverride(ext1, NEWTAB_URI_2, CONTROLLED_BY_OTHER);
+ verifyNewTabSettings(ext2, CONTROLLED_BY_THIS);
+
+ // Verify that calling set and clear do nothing.
+ ext2.sendMessage("trySet");
+ await ext2.awaitMessage("newTabPageSet");
+ await checkNewTabPageOverride(ext1, NEWTAB_URI_2, CONTROLLED_BY_OTHER);
+ verifyNewTabSettings(ext2, CONTROLLED_BY_THIS);
+
+ ext2.sendMessage("tryClear");
+ await ext2.awaitMessage("newTabPageCleared");
+ await checkNewTabPageOverride(ext1, NEWTAB_URI_2, CONTROLLED_BY_OTHER);
+ verifyNewTabSettings(ext2, CONTROLLED_BY_THIS);
+
+ // Disable the second extension.
+ let addon = await AddonManager.getAddonByID(EXT_2_ID);
+ let disabledPromise = awaitEvent("shutdown");
+ await addon.disable();
+ await disabledPromise;
+ equal(
+ AboutNewTab.newTabURL,
+ DEFAULT_NEW_TAB_URL,
+ "newTabURL url is reset to the default after second extension is disabled."
+ );
+ await checkNewTabPageOverride(ext1, AboutNewTab.newTabURL, NOT_CONTROLLABLE);
+ verifyNewTabSettings(ext1, NOT_CONTROLLABLE);
+
+ // Re-enable the second extension.
+ let enabledPromise = awaitEvent("ready");
+ await addon.enable();
+ await enabledPromise;
+ ok(
+ AboutNewTab.newTabURL.endsWith(NEWTAB_URI_2),
+ "newTabURL is overridden by the second extension."
+ );
+ await checkNewTabPageOverride(ext2, NEWTAB_URI_2, CONTROLLED_BY_THIS);
+ verifyNewTabSettings(ext2, CONTROLLED_BY_THIS);
+
+ await ext1.unload();
+ ok(
+ AboutNewTab.newTabURL.endsWith(NEWTAB_URI_2),
+ "newTabURL is still overridden by the second extension."
+ );
+ await checkNewTabPageOverride(ext2, NEWTAB_URI_2, CONTROLLED_BY_THIS);
+ verifyNewTabSettings(ext2, CONTROLLED_BY_THIS);
+
+ await ext3.startup();
+ ok(
+ AboutNewTab.newTabURL.endsWith(NEWTAB_URI_3),
+ "newTabURL is overridden by the third extension."
+ );
+ await checkNewTabPageOverride(ext2, NEWTAB_URI_3, CONTROLLED_BY_OTHER);
+ verifyNewTabSettings(ext3, CONTROLLED_BY_THIS);
+
+ // Disable the second extension.
+ disabledPromise = awaitEvent("shutdown");
+ await addon.disable();
+ await disabledPromise;
+ ok(
+ AboutNewTab.newTabURL.endsWith(NEWTAB_URI_3),
+ "newTabURL is still overridden by the third extension."
+ );
+ await checkNewTabPageOverride(ext3, NEWTAB_URI_3, CONTROLLED_BY_THIS);
+ verifyNewTabSettings(ext3, CONTROLLED_BY_THIS);
+
+ // Re-enable the second extension.
+ enabledPromise = awaitEvent("ready");
+ await addon.enable();
+ await enabledPromise;
+ ok(
+ AboutNewTab.newTabURL.endsWith(NEWTAB_URI_3),
+ "newTabURL is still overridden by the third extension."
+ );
+ await checkNewTabPageOverride(ext3, NEWTAB_URI_3, CONTROLLED_BY_THIS);
+ verifyNewTabSettings(ext3, CONTROLLED_BY_THIS);
+
+ await ext3.unload();
+ ok(
+ AboutNewTab.newTabURL.endsWith(NEWTAB_URI_2),
+ "newTabURL reverts to being overridden by the second extension."
+ );
+ await checkNewTabPageOverride(ext2, NEWTAB_URI_2, CONTROLLED_BY_THIS);
+ verifyNewTabSettings(ext2, CONTROLLED_BY_THIS);
+
+ await ext2.unload();
+ equal(
+ AboutNewTab.newTabURL,
+ DEFAULT_NEW_TAB_URL,
+ "newTabURL url is reset to the default."
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(NEW_TAB_PRIVATE_ALLOWED),
+ "controlled flag reset"
+ );
+ ok(
+ !Services.prefs.prefHasUserValue(NEW_TAB_EXTENSION_CONTROLLED),
+ "controlled flag reset"
+ );
+
+ await promiseShutdownManager();
+});
+
+// Tests that we handle the upgrade/downgrade process correctly
+// when an extension is installed temporarily on top of a permanently
+// installed one.
+add_task(async function test_temporary_installation() {
+ const ID = "newtab@tests.mozilla.org";
+ const PAGE1 = "page1.html";
+ const PAGE2 = "page2.html";
+
+ equal(
+ AboutNewTab.newTabURL,
+ DEFAULT_NEW_TAB_URL,
+ "newTabURL is set to the default."
+ );
+
+ await promiseStartupManager();
+
+ let permanent = ExtensionTestUtils.loadExtension({
+ manifest: {
+ applications: {
+ gecko: { id: ID },
+ },
+ chrome_url_overrides: {
+ newtab: PAGE1,
+ },
+ },
+ useAddonManager: "permanent",
+ });
+
+ await permanent.startup();
+ ok(
+ AboutNewTab.newTabURL.endsWith(PAGE1),
+ "newTabURL is overridden by permanent extension."
+ );
+
+ let temporary = ExtensionTestUtils.loadExtension({
+ manifest: {
+ applications: {
+ gecko: { id: ID },
+ },
+ chrome_url_overrides: {
+ newtab: PAGE2,
+ },
+ },
+ useAddonManager: "temporary",
+ });
+
+ await temporary.startup();
+ ok(
+ AboutNewTab.newTabURL.endsWith(PAGE2),
+ "newTabURL is overridden by temporary extension."
+ );
+
+ await promiseRestartManager();
+ await permanent.awaitStartup();
+
+ ok(
+ AboutNewTab.newTabURL.endsWith(PAGE1),
+ "newTabURL is back to the value set by permanent extension."
+ );
+
+ await permanent.unload();
+
+ equal(
+ AboutNewTab.newTabURL,
+ DEFAULT_NEW_TAB_URL,
+ "newTabURL is set back to the default."
+ );
+ await promiseShutdownManager();
+});
diff --git a/browser/components/extensions/test/xpcshell/test_ext_url_overrides_newtab_update.js b/browser/components/extensions/test/xpcshell/test_ext_url_overrides_newtab_update.js
new file mode 100644
index 0000000000..fd4fa99c62
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_url_overrides_newtab_update.js
@@ -0,0 +1,127 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const { AboutNewTab } = ChromeUtils.import(
+ "resource:///modules/AboutNewTab.jsm"
+);
+
+const { AddonTestUtils } = ChromeUtils.import(
+ "resource://testing-common/AddonTestUtils.jsm"
+);
+
+const {
+ createAppInfo,
+ createTempWebExtensionFile,
+ promiseCompleteAllInstalls,
+ promiseFindAddonUpdates,
+ promiseShutdownManager,
+ promiseStartupManager,
+} = AddonTestUtils;
+
+AddonTestUtils.init(this);
+
+// Allow for unsigned addons.
+AddonTestUtils.overrideCertDB();
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "42", "42");
+
+add_task(async function test_url_overrides_newtab_update() {
+ const EXTENSION_ID = "test_url_overrides_update@tests.mozilla.org";
+ const NEWTAB_URI = "webext-newtab-1.html";
+ const PREF_EM_CHECK_UPDATE_SECURITY = "extensions.checkUpdateSecurity";
+
+ const testServer = createHttpServer();
+ const port = testServer.identity.primaryPort;
+
+ // The test extension uses an insecure update url.
+ Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false);
+
+ testServer.registerPathHandler("/test_update.json", (request, response) => {
+ response.write(`{
+ "addons": {
+ "${EXTENSION_ID}": {
+ "updates": [
+ {
+ "version": "2.0",
+ "update_link": "http://localhost:${port}/addons/test_url_overrides-2.0.xpi"
+ }
+ ]
+ }
+ }
+ }`);
+ });
+
+ let webExtensionFile = createTempWebExtensionFile({
+ manifest: {
+ version: "2.0",
+ applications: {
+ gecko: {
+ id: EXTENSION_ID,
+ },
+ },
+ },
+ });
+
+ testServer.registerFile(
+ "/addons/test_url_overrides-2.0.xpi",
+ webExtensionFile
+ );
+
+ await promiseStartupManager();
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ version: "1.0",
+ applications: {
+ gecko: {
+ id: EXTENSION_ID,
+ update_url: `http://localhost:${port}/test_update.json`,
+ },
+ },
+ chrome_url_overrides: { newtab: NEWTAB_URI },
+ },
+ });
+
+ let defaultNewTabURL = AboutNewTab.newTabURL;
+ equal(
+ AboutNewTab.newTabURL,
+ defaultNewTabURL,
+ `Default newtab url is ${defaultNewTabURL}.`
+ );
+
+ await extension.startup();
+
+ equal(
+ extension.version,
+ "1.0",
+ "The installed addon has the expected version."
+ );
+ ok(
+ AboutNewTab.newTabURL.endsWith(NEWTAB_URI),
+ "Newtab url is overridden by the extension."
+ );
+
+ let update = await promiseFindAddonUpdates(extension.addon);
+ let install = update.updateAvailable;
+
+ await promiseCompleteAllInstalls([install]);
+
+ await extension.awaitStartup();
+
+ equal(
+ extension.version,
+ "2.0",
+ "The updated addon has the expected version."
+ );
+ equal(
+ AboutNewTab.newTabURL,
+ defaultNewTabURL,
+ "Newtab url reverted to the default after update."
+ );
+
+ await extension.unload();
+
+ await promiseShutdownManager();
+});
diff --git a/browser/components/extensions/test/xpcshell/test_ext_urlbar.js b/browser/components/extensions/test/xpcshell/test_ext_urlbar.js
new file mode 100644
index 0000000000..e656d9a604
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_urlbar.js
@@ -0,0 +1,1455 @@
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.import(
+ "resource://testing-common/AddonTestUtils.jsm"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ ExtensionParent: "resource://gre/modules/ExtensionParent.jsm",
+ UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm",
+ UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.jsm",
+ UrlbarQueryContext: "resource:///modules/UrlbarUtils.jsm",
+ UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.jsm",
+ UrlbarUtils: "resource:///modules/UrlbarUtils.jsm",
+});
+
+AddonTestUtils.init(this);
+AddonTestUtils.overrideCertDB();
+AddonTestUtils.createAppInfo(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "1",
+ "42"
+);
+// Override ExtensionXPCShellUtils.jsm's overriding of the pref as the
+// search service needs it.
+Services.prefs.clearUserPref("services.settings.default_bucket");
+
+function promiseUninstallCompleted(extensionId) {
+ return new Promise(resolve => {
+ // eslint-disable-next-line mozilla/balanced-listeners
+ ExtensionParent.apiManager.on("uninstall-complete", (type, { id }) => {
+ if (id === extensionId) {
+ executeSoon(resolve);
+ }
+ });
+ });
+}
+
+function getPayload(result) {
+ let payload = {};
+ for (let [key, value] of Object.entries(result.payload)) {
+ if (value !== undefined) {
+ payload[key] = value;
+ }
+ }
+ return payload;
+}
+
+add_task(async function startup() {
+ Services.prefs.setCharPref("browser.search.region", "US");
+ Services.prefs.setIntPref("browser.search.addonLoadTimeout", 0);
+ Services.prefs.setBoolPref(
+ "browser.search.separatePrivateDefault.ui.enabled",
+ false
+ );
+ // Set the notification timeout to a really high value to avoid intermittent
+ // failures due to the mock extensions not responding in time.
+ Services.prefs.setIntPref("browser.urlbar.extension.timeout", 5000);
+
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.urlbar.extension.timeout");
+ });
+
+ await AddonTestUtils.promiseStartupManager();
+ await UrlbarTestUtils.initXPCShellDependencies();
+
+ // Add a test engine and make it default so that when we do searches below,
+ // Firefox doesn't try to include search suggestions from the actual default
+ // engine from over the network.
+ let engine = await Services.search.addEngineWithDetails("Test engine", {
+ template: "http://example.com/?s=%S",
+ alias: "@testengine",
+ });
+ Services.search.defaultEngine = engine;
+});
+
+// Extensions must specify the "urlbar" permission to use browser.urlbar.
+add_task(async function test_urlbar_without_urlbar_permission() {
+ let ext = ExtensionTestUtils.loadExtension({
+ isPrivileged: true,
+ background() {
+ browser.test.assertEq(
+ browser.urlbar,
+ undefined,
+ "'urlbar' permission is required"
+ );
+ },
+ });
+ await ext.startup();
+ await ext.unload();
+});
+
+// Extensions must be privileged to use browser.urlbar.
+add_task(async function test_urlbar_no_privilege() {
+ let ext = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["urlbar"],
+ },
+ background() {
+ browser.test.assertEq(
+ browser.urlbar,
+ undefined,
+ "'urlbar' permission is privileged"
+ );
+ },
+ });
+ await ext.startup();
+ await ext.unload();
+});
+
+// Checks that providers are added and removed properly.
+add_task(async function test_registerProvider() {
+ // A copy of the default providers.
+ let providers = UrlbarProvidersManager.providers.slice();
+
+ let ext = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["urlbar"],
+ },
+ isPrivileged: true,
+ incognitoOverride: "spanning",
+ background() {
+ for (let state of ["active", "inactive", "restricting"]) {
+ let name = `Test-${state}`;
+ browser.urlbar.onBehaviorRequested.addListener(query => {
+ browser.test.assertFalse(query.isPrivate, "Context is non private");
+ browser.test.assertEq(query.maxResults, 10, "Check maxResults");
+ browser.test.assertTrue(
+ query.searchString,
+ "SearchString is non empty"
+ );
+ browser.test.assertTrue(
+ Array.isArray(query.sources),
+ "sources is an array"
+ );
+ return state;
+ }, name);
+ browser.urlbar.onResultsRequested.addListener(query => [], name);
+ }
+ },
+ });
+ await ext.startup();
+
+ Assert.greater(
+ UrlbarProvidersManager.providers.length,
+ providers.length,
+ "Providers have been added"
+ );
+
+ // Run a query, this should execute the above listeners and checks, plus it
+ // will set the provider's isActive and priority.
+ let queryContext = new UrlbarQueryContext({
+ allowAutofill: false,
+ isPrivate: false,
+ maxResults: 10,
+ searchString: "*",
+ });
+ let controller = UrlbarTestUtils.newMockController();
+ await controller.startQuery(queryContext);
+
+ // Check the providers behavior has been setup properly.
+ for (let provider of UrlbarProvidersManager.providers) {
+ if (!provider.name.startsWith("Test")) {
+ continue;
+ }
+ let [, state] = provider.name.split("-");
+ let isActive = state != "inactive";
+ let restricting = state == "restricting";
+ Assert.equal(
+ isActive,
+ provider.isActive(queryContext),
+ "Check active callback"
+ );
+ if (restricting) {
+ Assert.notEqual(
+ provider.getPriority(queryContext),
+ 0,
+ "Check provider priority"
+ );
+ } else {
+ Assert.equal(
+ provider.getPriority(queryContext),
+ 0,
+ "Check provider priority"
+ );
+ }
+ }
+
+ await ext.unload();
+
+ // Sanity check the providers.
+ Assert.deepEqual(
+ UrlbarProvidersManager.providers,
+ providers,
+ "Should return to the default providers"
+ );
+});
+
+// Adds a single active provider that returns many kinds of results. This also
+// checks that the heuristic result from the built-in HeuristicFallback provider
+// is included.
+add_task(async function test_onProviderResultsRequested() {
+ let ext = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["urlbar"],
+ },
+ isPrivileged: true,
+ incognitoOverride: "spanning",
+ background() {
+ browser.urlbar.onBehaviorRequested.addListener(query => {
+ return "active";
+ }, "test");
+ browser.urlbar.onResultsRequested.addListener(query => {
+ browser.test.assertFalse(query.isPrivate);
+ browser.test.assertEq(query.maxResults, 10);
+ browser.test.assertEq(query.searchString, "test");
+ browser.test.assertTrue(Array.isArray(query.sources));
+ return [
+ {
+ type: "remote_tab",
+ source: "tabs",
+ payload: {
+ title: "Test remote_tab-tabs result",
+ url: "http://example.com/remote_tab-tabs",
+ device: "device",
+ },
+ },
+ {
+ type: "search",
+ source: "search",
+ payload: {
+ suggestion: "Test search-search result",
+ engine: "Test engine",
+ },
+ },
+ {
+ type: "tab",
+ source: "tabs",
+ payload: {
+ title: "Test tab-tabs result",
+ url: "http://example.com/tab-tabs",
+ },
+ },
+ {
+ type: "tip",
+ source: "local",
+ payload: {
+ text: "Test tip-local result text",
+ buttonText: "Test tip-local result button text",
+ buttonUrl: "http://example.com/tip-button",
+ helpUrl: "http://example.com/tip-help",
+ },
+ },
+ {
+ type: "url",
+ source: "history",
+ payload: {
+ title: "Test url-history result",
+ url: "http://example.com/url-history",
+ },
+ },
+ ];
+ }, "test");
+ },
+ });
+ await ext.startup();
+
+ // Check the provider.
+ let provider = UrlbarProvidersManager.getProvider("test");
+ Assert.ok(provider);
+
+ // Run a query.
+ let context = new UrlbarQueryContext({
+ allowAutofill: false,
+ isPrivate: false,
+ maxResults: 10,
+ searchString: "test",
+ });
+ let controller = UrlbarTestUtils.newMockController();
+ await controller.startQuery(context);
+
+ // Check isActive and priority.
+ Assert.ok(provider.isActive(context));
+ Assert.equal(provider.getPriority(context), 0);
+
+ // Check the results.
+ let expectedResults = [
+ // The first result should be a search result returned by HeuristicFallback.
+ {
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ title: "test",
+ heuristic: true,
+ payload: {
+ query: "test",
+ engine: "Test engine",
+ },
+ },
+ // The second result should be our search suggestion result since the
+ // default muxer sorts search suggestion results before other types.
+ {
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ title: "Test search-search result",
+ heuristic: false,
+ payload: {
+ engine: "Test engine",
+ suggestion: "Test search-search result",
+ },
+ },
+ // The rest of the results should appear in the order we returned them
+ // above.
+ {
+ type: UrlbarUtils.RESULT_TYPE.REMOTE_TAB,
+ source: UrlbarUtils.RESULT_SOURCE.TABS,
+ title: "Test remote_tab-tabs result",
+ heuristic: false,
+ payload: {
+ title: "Test remote_tab-tabs result",
+ url: "http://example.com/remote_tab-tabs",
+ displayUrl: "http://example.com/remote_tab-tabs",
+ device: "device",
+ },
+ },
+ {
+ type: UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
+ source: UrlbarUtils.RESULT_SOURCE.TABS,
+ title: "Test tab-tabs result",
+ heuristic: false,
+ payload: {
+ title: "Test tab-tabs result",
+ url: "http://example.com/tab-tabs",
+ displayUrl: "http://example.com/tab-tabs",
+ },
+ },
+ {
+ type: UrlbarUtils.RESULT_TYPE.TIP,
+ source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ title: "",
+ heuristic: false,
+ payload: {
+ text: "Test tip-local result text",
+ buttonText: "Test tip-local result button text",
+ buttonUrl: "http://example.com/tip-button",
+ helpUrl: "http://example.com/tip-help",
+ type: "extension",
+ },
+ },
+ {
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ source: UrlbarUtils.RESULT_SOURCE.HISTORY,
+ title: "Test url-history result",
+ heuristic: false,
+ payload: {
+ title: "Test url-history result",
+ url: "http://example.com/url-history",
+ displayUrl: "http://example.com/url-history",
+ },
+ },
+ ];
+
+ Assert.ok(context.results.every(r => r.suggestedIndex == -1));
+ let actualResults = context.results.map(r => ({
+ type: r.type,
+ source: r.source,
+ title: r.title,
+ heuristic: r.heuristic,
+ payload: getPayload(r),
+ }));
+
+ Assert.deepEqual(actualResults, expectedResults);
+
+ await ext.unload();
+});
+
+// Extensions can specify search engines using engine names, aliases, and URLs.
+add_task(async function test_onProviderResultsRequested_searchEngines() {
+ let ext = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["urlbar"],
+ },
+ isPrivileged: true,
+ incognitoOverride: "spanning",
+ background() {
+ browser.urlbar.onBehaviorRequested.addListener(query => {
+ return "restricting";
+ }, "test");
+ browser.urlbar.onResultsRequested.addListener(query => {
+ return [
+ {
+ type: "search",
+ source: "search",
+ payload: {
+ engine: "Test engine",
+ suggestion: "engine specified",
+ },
+ },
+ {
+ type: "search",
+ source: "search",
+ payload: {
+ keyword: "@testengine",
+ suggestion: "keyword specified",
+ },
+ },
+ {
+ type: "search",
+ source: "search",
+ payload: {
+ url: "http://example.com/?s",
+ suggestion: "url specified",
+ },
+ },
+ {
+ type: "search",
+ source: "search",
+ payload: {
+ engine: "Test engine",
+ keyword: "@testengine",
+ url: "http://example.com/?s",
+ suggestion: "engine, keyword, and url specified",
+ },
+ },
+ {
+ type: "search",
+ source: "search",
+ payload: {
+ keyword: "@testengine",
+ url: "http://example.com/?s",
+ suggestion: "keyword and url specified",
+ },
+ },
+ {
+ type: "search",
+ source: "search",
+ payload: {
+ suggestion: "no engine",
+ },
+ },
+ {
+ type: "search",
+ source: "search",
+ payload: {
+ engine: "bogus",
+ suggestion: "no matching engine",
+ },
+ },
+ {
+ type: "search",
+ source: "search",
+ payload: {
+ keyword: "@bogus",
+ suggestion: "no matching keyword",
+ },
+ },
+ {
+ type: "search",
+ source: "search",
+ payload: {
+ url: "http://bogus-no-search-engine.com/",
+ suggestion: "no matching url",
+ },
+ },
+ {
+ type: "search",
+ source: "search",
+ payload: {
+ url: "bogus",
+ suggestion: "invalid url",
+ },
+ },
+ {
+ type: "search",
+ source: "search",
+ payload: {
+ url: "foo:bar",
+ suggestion: "url with no hostname",
+ },
+ },
+ ];
+ }, "test");
+ },
+ });
+ await ext.startup();
+
+ // Run a query.
+ let context = new UrlbarQueryContext({
+ allowAutofill: false,
+ isPrivate: false,
+ maxResults: 10,
+ searchString: "test",
+ });
+ let controller = UrlbarTestUtils.newMockController();
+ await controller.startQuery(context);
+
+ // Check the results. The first several are valid and should include "Test
+ // engine" as the engine. The others don't specify an engine and are
+ // therefore invalid, so they should be ignored.
+ let expectedResults = [
+ {
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ engine: "Test engine",
+ title: "engine specified",
+ heuristic: false,
+ },
+ {
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ engine: "Test engine",
+ title: "keyword specified",
+ heuristic: false,
+ },
+ {
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ engine: "Test engine",
+ title: "url specified",
+ heuristic: false,
+ },
+ {
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ engine: "Test engine",
+ title: "engine, keyword, and url specified",
+ heuristic: false,
+ },
+ {
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ source: UrlbarUtils.RESULT_SOURCE.SEARCH,
+ engine: "Test engine",
+ title: "keyword and url specified",
+ heuristic: false,
+ },
+ ];
+
+ let actualResults = context.results.map(r => ({
+ type: r.type,
+ source: r.source,
+ engine: r.payload.engine || null,
+ title: r.title,
+ heuristic: r.heuristic,
+ }));
+
+ Assert.deepEqual(actualResults, expectedResults);
+
+ await ext.unload();
+});
+
+// Adds two providers, one active and one inactive. Only the active provider
+// should be asked to return results.
+add_task(async function test_activeAndInactiveProviders() {
+ let ext = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["urlbar"],
+ },
+ isPrivileged: true,
+ incognitoOverride: "spanning",
+ background() {
+ for (let behavior of ["active", "inactive"]) {
+ browser.urlbar.onBehaviorRequested.addListener(query => {
+ return behavior;
+ }, behavior);
+ browser.urlbar.onResultsRequested.addListener(query => {
+ browser.test.assertEq(
+ behavior,
+ "active",
+ "onResultsRequested should be fired only for the active provider"
+ );
+ return [
+ {
+ type: "url",
+ source: "history",
+ payload: {
+ title: `Test result ${behavior}`,
+ url: `http://example.com/${behavior}`,
+ },
+ },
+ ];
+ }, behavior);
+ }
+ },
+ });
+ await ext.startup();
+
+ // Check the providers.
+ let active = UrlbarProvidersManager.getProvider("active");
+ let inactive = UrlbarProvidersManager.getProvider("inactive");
+ Assert.ok(active);
+ Assert.ok(inactive);
+
+ // Run a query.
+ let context = new UrlbarQueryContext({
+ allowAutofill: false,
+ isPrivate: false,
+ maxResults: 10,
+ searchString: "test",
+ });
+ let controller = UrlbarTestUtils.newMockController();
+ await controller.startQuery(context);
+
+ // Check isActive and priority.
+ Assert.ok(active.isActive(context));
+ Assert.ok(!inactive.isActive(context));
+ Assert.equal(active.getPriority(context), 0);
+ Assert.equal(inactive.getPriority(context), 0);
+
+ // Check the results.
+ Assert.equal(context.results.length, 2);
+ Assert.ok(context.results[0].heuristic);
+ Assert.equal(context.results[1].title, "Test result active");
+
+ await ext.unload();
+});
+
+// Adds three active providers. They all should be asked for results.
+add_task(async function test_threeActiveProviders() {
+ let ext = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["urlbar"],
+ },
+ isPrivileged: true,
+ incognitoOverride: "spanning",
+ background() {
+ for (let i = 0; i < 3; i++) {
+ let name = `test-${i}`;
+ browser.urlbar.onBehaviorRequested.addListener(query => {
+ return "active";
+ }, name);
+ browser.urlbar.onResultsRequested.addListener(query => {
+ return [
+ {
+ type: "url",
+ source: "history",
+ payload: {
+ title: `Test result ${i}`,
+ url: `http://example.com/${i}`,
+ },
+ },
+ ];
+ }, name);
+ }
+ },
+ });
+ await ext.startup();
+
+ // Check the providers.
+ let providers = [];
+ for (let i = 0; i < 3; i++) {
+ let name = `test-${i}`;
+ let provider = UrlbarProvidersManager.getProvider(name);
+ Assert.ok(provider);
+ providers.push(provider);
+ }
+
+ // Run a query.
+ let context = new UrlbarQueryContext({
+ allowAutofill: false,
+ isPrivate: false,
+ maxResults: 10,
+ searchString: "test",
+ });
+ let controller = UrlbarTestUtils.newMockController();
+ await controller.startQuery(context);
+
+ // Check isActive and priority.
+ for (let provider of providers) {
+ Assert.ok(provider.isActive(context));
+ Assert.equal(provider.getPriority(context), 0);
+ }
+
+ // Check the results.
+ Assert.equal(context.results.length, 4);
+ Assert.ok(context.results[0].heuristic);
+ for (let i = 0; i < providers.length; i++) {
+ Assert.equal(context.results[i + 1].title, `Test result ${i}`);
+ }
+
+ await ext.unload();
+});
+
+// Adds three inactive providers. None of them should be asked for results.
+// This also checks that provider behavior is "inactive" by default.
+add_task(async function test_threeInactiveProviders() {
+ let ext = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["urlbar"],
+ },
+ isPrivileged: true,
+ incognitoOverride: "spanning",
+ background() {
+ for (let i = 0; i < 3; i++) {
+ // Don't add an onBehaviorRequested listener. That way we can test that
+ // the default behavior is inactive.
+ browser.urlbar.onResultsRequested.addListener(query => {
+ browser.test.notifyFail(
+ "onResultsRequested fired for inactive provider"
+ );
+ }, `test-${i}`);
+ }
+ },
+ });
+ await ext.startup();
+
+ // Check the providers.
+ let providers = [];
+ for (let i = 0; i < 3; i++) {
+ let name = `test-${i}`;
+ let provider = UrlbarProvidersManager.getProvider(name);
+ Assert.ok(provider);
+ providers.push(provider);
+ }
+
+ // Run a query.
+ let context = new UrlbarQueryContext({
+ allowAutofill: false,
+ isPrivate: false,
+ maxResults: 10,
+ searchString: "test",
+ });
+ let controller = UrlbarTestUtils.newMockController();
+ await controller.startQuery(context);
+
+ // Check isActive and priority.
+ for (let provider of providers) {
+ Assert.ok(!provider.isActive(context));
+ Assert.equal(provider.getPriority(context), 0);
+ }
+
+ // Check the results.
+ Assert.equal(context.results.length, 1);
+ Assert.ok(context.results[0].heuristic);
+
+ await ext.unload();
+});
+
+// Adds active, inactive, and restricting providers. Only the restricting
+// provider should be asked to return results.
+add_task(async function test_activeInactiveAndRestrictingProviders() {
+ let ext = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["urlbar"],
+ },
+ isPrivileged: true,
+ incognitoOverride: "spanning",
+ background() {
+ for (let behavior of ["active", "inactive", "restricting"]) {
+ browser.urlbar.onBehaviorRequested.addListener(query => {
+ return behavior;
+ }, behavior);
+ browser.urlbar.onResultsRequested.addListener(query => {
+ browser.test.assertEq(
+ behavior,
+ "restricting",
+ "onResultsRequested should be fired for the restricting provider"
+ );
+ return [
+ {
+ type: "url",
+ source: "history",
+ payload: {
+ title: `Test result ${behavior}`,
+ url: `http://example.com/${behavior}`,
+ },
+ },
+ ];
+ }, behavior);
+ }
+ },
+ });
+ await ext.startup();
+
+ // Check the providers.
+ let providers = {};
+ for (let behavior of ["active", "inactive", "restricting"]) {
+ let provider = UrlbarProvidersManager.getProvider(behavior);
+ Assert.ok(provider);
+ providers[behavior] = provider;
+ }
+
+ // Run a query.
+ let context = new UrlbarQueryContext({
+ allowAutofill: false,
+ isPrivate: false,
+ maxResults: 10,
+ searchString: "test",
+ });
+ let controller = UrlbarTestUtils.newMockController();
+ await controller.startQuery(context);
+
+ // Check isActive and isRestricting.
+ Assert.ok(providers.active.isActive(context));
+ Assert.equal(providers.active.getPriority(context), 0);
+ Assert.ok(!providers.inactive.isActive(context));
+ Assert.equal(providers.inactive.getPriority(context), 0);
+ Assert.ok(providers.restricting.isActive(context));
+ Assert.notEqual(providers.restricting.getPriority(context), 0);
+
+ // Check the results.
+ Assert.equal(context.results.length, 1);
+ Assert.equal(context.results[0].title, "Test result restricting");
+
+ await ext.unload();
+});
+
+// Adds a restricting provider that returns a heuristic result. The actual
+// result created from the extension's result should be a heuristic.
+add_task(async function test_heuristicRestricting() {
+ let ext = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["urlbar"],
+ },
+ isPrivileged: true,
+ incognitoOverride: "spanning",
+ background() {
+ browser.urlbar.onBehaviorRequested.addListener(query => {
+ return "restricting";
+ }, "test");
+ browser.urlbar.onResultsRequested.addListener(query => {
+ return [
+ {
+ type: "url",
+ source: "history",
+ heuristic: true,
+ payload: {
+ title: "Test result",
+ url: "http://example.com/",
+ },
+ },
+ ];
+ }, "test");
+ },
+ });
+ await ext.startup();
+
+ // Check the provider.
+ let provider = UrlbarProvidersManager.getProvider("test");
+ Assert.ok(provider);
+
+ // Run a query.
+ let context = new UrlbarQueryContext({
+ allowAutofill: false,
+ isPrivate: false,
+ maxResults: 10,
+ searchString: "test",
+ });
+ let controller = UrlbarTestUtils.newMockController();
+ await controller.startQuery(context);
+
+ // Check the results.
+ Assert.equal(context.results.length, 1);
+ Assert.ok(context.results[0].heuristic);
+
+ await ext.unload();
+});
+
+// Adds a non-restricting provider that returns a heuristic result. The actual
+// result created from the extension's result should *not* be a heuristic, and
+// the usual UnifiedComplete heuristic should be present.
+add_task(async function test_heuristicNonRestricting() {
+ let ext = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["urlbar"],
+ },
+ isPrivileged: true,
+ incognitoOverride: "spanning",
+ background() {
+ browser.urlbar.onBehaviorRequested.addListener(query => {
+ return "active";
+ }, "test");
+ browser.urlbar.onResultsRequested.addListener(query => {
+ return [
+ {
+ type: "url",
+ source: "history",
+ heuristic: true,
+ payload: {
+ title: "Test result",
+ url: "http://example.com/",
+ },
+ },
+ ];
+ }, "test");
+ },
+ });
+ await ext.startup();
+
+ // Check the provider.
+ let provider = UrlbarProvidersManager.getProvider("test");
+ Assert.ok(provider);
+
+ // Run a query.
+ let context = new UrlbarQueryContext({
+ allowAutofill: false,
+ isPrivate: false,
+ maxResults: 10,
+ searchString: "test",
+ });
+ let controller = UrlbarTestUtils.newMockController();
+ await controller.startQuery(context);
+
+ // Check the results. The first result should be UnifiedComplete's heuristic.
+ let firstResult = context.results[0];
+ Assert.ok(firstResult.heuristic);
+ Assert.equal(firstResult.type, UrlbarUtils.RESULT_TYPE.SEARCH);
+ Assert.equal(firstResult.source, UrlbarUtils.RESULT_SOURCE.SEARCH);
+ Assert.equal(firstResult.payload.engine, "Test engine");
+
+ // The extension result should be present but not the heuristic.
+ let result = context.results.find(r => r.title == "Test result");
+ Assert.ok(result);
+ Assert.ok(!result.heuristic);
+
+ await ext.unload();
+});
+
+// Adds an active provider that doesn't have a listener for onResultsRequested.
+// No results should be added.
+add_task(async function test_onResultsRequestedNotImplemented() {
+ let ext = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["urlbar"],
+ },
+ isPrivileged: true,
+ incognitoOverride: "spanning",
+ background() {
+ browser.urlbar.onBehaviorRequested.addListener(query => {
+ return "active";
+ }, "test");
+ },
+ });
+ await ext.startup();
+
+ // Check the provider.
+ let provider = UrlbarProvidersManager.getProvider("test");
+ Assert.ok(provider);
+
+ // Run a query.
+ let context = new UrlbarQueryContext({
+ allowAutofill: false,
+ isPrivate: false,
+ maxResults: 10,
+ searchString: "test",
+ });
+ let controller = UrlbarTestUtils.newMockController();
+ await controller.startQuery(context);
+
+ // Check isActive and isRestricting.
+ Assert.ok(provider.isActive(context));
+ Assert.equal(provider.getPriority(context), 0);
+
+ // Check the results.
+ Assert.equal(context.results.length, 1);
+ Assert.ok(context.results[0].heuristic);
+
+ await ext.unload();
+});
+
+// Adds an active provider that returns a result with a malformed payload. The
+// bad result shouldn't be added.
+add_task(async function test_badPayload() {
+ let ext = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["urlbar"],
+ },
+ isPrivileged: true,
+ incognitoOverride: "spanning",
+ background() {
+ browser.urlbar.onBehaviorRequested.addListener(query => {
+ return "active";
+ }, "test");
+ browser.urlbar.onResultsRequested.addListener(async query => {
+ return [
+ {
+ type: "url",
+ source: "history",
+ payload: "this is a bad payload",
+ },
+ {
+ type: "url",
+ source: "history",
+ payload: {
+ title: "Test result",
+ url: "http://example.com/",
+ },
+ },
+ ];
+ }, "test");
+ },
+ });
+ await ext.startup();
+
+ // Check the provider.
+ let provider = UrlbarProvidersManager.getProvider("test");
+ Assert.ok(provider);
+
+ // Run a query.
+ let context = new UrlbarQueryContext({
+ allowAutofill: false,
+ isPrivate: false,
+ maxResults: 10,
+ searchString: "test",
+ });
+ let controller = UrlbarTestUtils.newMockController();
+ await controller.startQuery(context);
+
+ // Check the results.
+ Assert.equal(context.results.length, 2);
+ Assert.ok(context.results[0].heuristic);
+ Assert.equal(context.results[1].title, "Test result");
+
+ await ext.unload();
+});
+
+// Tests the onQueryCanceled event.
+add_task(async function test_onQueryCanceled() {
+ let ext = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["urlbar"],
+ },
+ isPrivileged: true,
+ incognitoOverride: "spanning",
+ background() {
+ browser.urlbar.onBehaviorRequested.addListener(query => {
+ return "active";
+ }, "test");
+ browser.urlbar.onQueryCanceled.addListener(query => {
+ browser.test.notifyPass("canceled");
+ }, "test");
+ },
+ });
+ await ext.startup();
+
+ // Check the provider.
+ let provider = UrlbarProvidersManager.getProvider("test");
+ Assert.ok(provider);
+
+ // Run a query but immediately cancel it.
+ let context = new UrlbarQueryContext({
+ allowAutofill: false,
+ isPrivate: false,
+ maxResults: 10,
+ searchString: "test",
+ });
+ let controller = UrlbarTestUtils.newMockController();
+
+ let startPromise = controller.startQuery(context);
+ controller.cancelQuery();
+ await startPromise;
+
+ await ext.awaitFinish("canceled");
+
+ await ext.unload();
+});
+
+// Adds an onBehaviorRequested listener that takes too long to respond. The
+// provider should default to inactive.
+add_task(async function test_onBehaviorRequestedTimeout() {
+ let ext = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["urlbar"],
+ },
+ isPrivileged: true,
+ incognitoOverride: "spanning",
+ background() {
+ browser.urlbar.onBehaviorRequested.addListener(async query => {
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, 500));
+ return "active";
+ }, "test");
+ browser.urlbar.onResultsRequested.addListener(query => {
+ browser.test.notifyFail(
+ "onResultsRequested fired for inactive provider"
+ );
+ }, "test");
+ },
+ });
+ await ext.startup();
+
+ // Check the provider.
+ let provider = UrlbarProvidersManager.getProvider("test");
+ Assert.ok(provider);
+
+ // Run a query.
+ let context = new UrlbarQueryContext({
+ allowAutofill: false,
+ isPrivate: false,
+ maxResults: 10,
+ searchString: "test",
+ });
+ let controller = UrlbarTestUtils.newMockController();
+
+ Services.prefs.setIntPref("browser.urlbar.extension.timeout", 0);
+ await controller.startQuery(context);
+ Services.prefs.clearUserPref("browser.urlbar.extension.timeout");
+
+ // Check isActive and priority.
+ Assert.ok(!provider.isActive(context));
+ Assert.equal(provider.getPriority(context), 0);
+
+ // Check the results.
+ Assert.equal(context.results.length, 1);
+ Assert.ok(context.results[0].heuristic);
+
+ await ext.unload();
+});
+
+// Adds an onResultsRequested listener that takes too long to respond. The
+// provider's results should default to no results.
+add_task(async function test_onResultsRequestedTimeout() {
+ let ext = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["urlbar"],
+ },
+ isPrivileged: true,
+ incognitoOverride: "spanning",
+ background() {
+ browser.urlbar.onBehaviorRequested.addListener(query => {
+ return "active";
+ }, "test");
+ browser.urlbar.onResultsRequested.addListener(async query => {
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, 600));
+ return [
+ {
+ type: "url",
+ source: "history",
+ payload: {
+ title: "Test result",
+ url: "http://example.com/",
+ },
+ },
+ ];
+ }, "test");
+ },
+ });
+ await ext.startup();
+
+ // Check the provider.
+ let provider = UrlbarProvidersManager.getProvider("test");
+ Assert.ok(provider);
+
+ // Run a query.
+ let context = new UrlbarQueryContext({
+ allowAutofill: false,
+ isPrivate: false,
+ maxResults: 10,
+ searchString: "test",
+ });
+ let controller = UrlbarTestUtils.newMockController();
+
+ await controller.startQuery(context);
+
+ // Check isActive and priority.
+ Assert.ok(provider.isActive(context));
+ Assert.equal(provider.getPriority(context), 0);
+
+ // Check the results.
+ Assert.equal(context.results.length, 1);
+ Assert.ok(context.results[0].heuristic);
+
+ await ext.unload();
+});
+
+// Performs a search in a private context for an extension that does not allow
+// private browsing. The extension's listeners should not be called.
+add_task(async function test_privateBrowsing_not_allowed() {
+ let ext = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["urlbar"],
+ incognito: "not_allowed",
+ },
+ isPrivileged: true,
+ background() {
+ browser.urlbar.onBehaviorRequested.addListener(query => {
+ browser.test.notifyFail(
+ "onBehaviorRequested fired in private browsing"
+ );
+ }, "Test-private");
+ browser.urlbar.onResultsRequested.addListener(query => {
+ browser.test.notifyFail("onResultsRequested fired in private browsing");
+ }, "Test-private");
+ // We can't easily test onQueryCanceled here because immediately canceling
+ // the query will cause onResultsRequested not to be fired.
+ // onResultsRequested should in fact not be fired, but that should be
+ // because this test runs in private-browsing mode, not because the query
+ // was canceled. See the next test task for onQueryCanceled.
+ },
+ });
+ await ext.startup();
+
+ // Run a query, this should execute the above listeners and checks, plus it
+ // will set the provider's isActive and priority.
+ let queryContext = new UrlbarQueryContext({
+ allowAutofill: false,
+ isPrivate: true,
+ maxResults: 10,
+ searchString: "*",
+ });
+ let controller = UrlbarTestUtils.newMockController();
+ await controller.startQuery(queryContext);
+ // Check the providers behavior has been setup properly.
+ let provider = UrlbarProvidersManager.getProvider("Test-private");
+ Assert.ok(!provider.isActive({}), "Check provider is inactive");
+
+ await ext.unload();
+});
+
+// Same as the previous task but tests the onQueryCanceled event: Performs a
+// search in a private context for an extension that does not allow private
+// browsing. The extension's onQueryCanceled listener should not be called.
+add_task(async function test_privateBrowsing_not_allowed_onQueryCanceled() {
+ let ext = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["urlbar"],
+ incognito: "not_allowed",
+ },
+ isPrivileged: true,
+ background() {
+ browser.urlbar.onBehaviorRequested.addListener(query => {
+ browser.test.notifyFail(
+ "onBehaviorRequested fired in private browsing"
+ );
+ }, "test");
+ browser.urlbar.onQueryCanceled.addListener(query => {
+ browser.test.notifyFail("onQueryCanceled fired in private browsing");
+ }, "test");
+ },
+ });
+ await ext.startup();
+
+ // Check the provider.
+ let provider = UrlbarProvidersManager.getProvider("test");
+ Assert.ok(provider);
+
+ // Run a query but immediately cancel it.
+ let context = new UrlbarQueryContext({
+ allowAutofill: false,
+ isPrivate: true,
+ maxResults: 10,
+ searchString: "*",
+ });
+ let controller = UrlbarTestUtils.newMockController();
+
+ let startPromise = controller.startQuery(context);
+ controller.cancelQuery();
+ await startPromise;
+
+ // Check isActive and priority.
+ Assert.ok(!provider.isActive(context));
+ Assert.equal(provider.getPriority(context), 0);
+
+ await ext.unload();
+});
+
+// Performs a search in a private context for an extension that allows private
+// browsing. The extension's listeners should be called.
+add_task(async function test_privateBrowsing_allowed() {
+ let ext = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["urlbar"],
+ },
+ isPrivileged: true,
+ incognitoOverride: "spanning",
+ background() {
+ let name = "Test-private";
+ browser.urlbar.onBehaviorRequested.addListener(query => {
+ browser.test.sendMessage("onBehaviorRequested");
+ return "active";
+ }, name);
+ browser.urlbar.onResultsRequested.addListener(query => {
+ browser.test.sendMessage("onResultsRequested");
+ return [];
+ }, name);
+ // We can't easily test onQueryCanceled here because immediately canceling
+ // the query will cause onResultsRequested not to be fired. See the next
+ // test task for onQueryCanceled.
+ },
+ });
+ await ext.startup();
+
+ // Check the provider.
+ let provider = UrlbarProvidersManager.getProvider("Test-private");
+ Assert.ok(provider);
+
+ // Run a query.
+ let context = new UrlbarQueryContext({
+ allowAutofill: false,
+ isPrivate: true,
+ maxResults: 10,
+ searchString: "*",
+ });
+ let controller = UrlbarTestUtils.newMockController();
+ await controller.startQuery(context);
+
+ // Check isActive and priority.
+ Assert.ok(provider.isActive(context));
+ Assert.equal(provider.getPriority(context), 0);
+
+ // The events should have been fired.
+ await Promise.all(
+ ["onBehaviorRequested", "onResultsRequested"].map(msg =>
+ ext.awaitMessage(msg)
+ )
+ );
+
+ await ext.unload();
+});
+
+// Same as the previous task but tests the onQueryCanceled event: Performs a
+// search in a private context for an extension that allows private browsing,
+// but cancels the search. The extension's onQueryCanceled listener should be
+// called.
+add_task(async function test_privateBrowsing_allowed_onQueryCanceled() {
+ let ext = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["urlbar"],
+ },
+ isPrivileged: true,
+ incognitoOverride: "spanning",
+ background() {
+ let name = "Test-private";
+ browser.urlbar.onBehaviorRequested.addListener(query => {
+ browser.test.sendMessage("onBehaviorRequested");
+ return "active";
+ }, name);
+ browser.urlbar.onQueryCanceled.addListener(query => {
+ browser.test.sendMessage("onQueryCanceled");
+ }, name);
+ },
+ });
+ await ext.startup();
+
+ // Check the provider.
+ let provider = UrlbarProvidersManager.getProvider("Test-private");
+ Assert.ok(provider);
+
+ // Run a query but immediately cancel it.
+ let context = new UrlbarQueryContext({
+ allowAutofill: false,
+ isPrivate: true,
+ maxResults: 10,
+ searchString: "*",
+ });
+ let controller = UrlbarTestUtils.newMockController();
+
+ let startPromise = controller.startQuery(context);
+ controller.cancelQuery();
+ await startPromise;
+
+ // onQueryCanceled should have been fired.
+ await ext.awaitMessage("onQueryCanceled");
+
+ await ext.unload();
+});
+
+// Performs a search in a non-private context for an extension that does not
+// allow private browsing. The extension's listeners should be called.
+add_task(async function test_nonPrivateBrowsing() {
+ let ext = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["urlbar"],
+ incognito: "not_allowed",
+ },
+ isPrivileged: true,
+ incognitoOverride: "spanning",
+ background() {
+ browser.urlbar.onBehaviorRequested.addListener(query => {
+ return "active";
+ }, "test");
+ browser.urlbar.onResultsRequested.addListener(query => {
+ return [
+ {
+ type: "url",
+ source: "history",
+ payload: {
+ title: "Test result",
+ url: "http://example.com/",
+ },
+ suggestedIndex: 1,
+ },
+ ];
+ }, "test");
+ },
+ });
+ await ext.startup();
+
+ // Check the provider.
+ let provider = UrlbarProvidersManager.getProvider("test");
+ Assert.ok(provider);
+
+ // Run a query.
+ let context = new UrlbarQueryContext({
+ allowAutofill: false,
+ isPrivate: false,
+ maxResults: 10,
+ searchString: "test",
+ });
+ let controller = UrlbarTestUtils.newMockController();
+ await controller.startQuery(context);
+
+ // Check isActive and priority.
+ Assert.ok(provider.isActive(context));
+ Assert.equal(provider.getPriority(context), 0);
+
+ // Check the results.
+ Assert.equal(context.results.length, 2);
+ Assert.ok(context.results[0].heuristic);
+ Assert.equal(context.results[1].title, "Test result");
+ Assert.equal(context.results[1].suggestedIndex, 1);
+
+ await ext.unload();
+});
+
+// Tests the engagementTelemetry property.
+add_task(async function test_engagementTelemetry() {
+ let getPrefValue = () => UrlbarPrefs.get("eventTelemetry.enabled");
+
+ Assert.equal(
+ getPrefValue(),
+ false,
+ "Engagement telemetry should be disabled by default"
+ );
+
+ let ext = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["urlbar"],
+ },
+ isPrivileged: true,
+ incognitoOverride: "spanning",
+ useAddonManager: "temporary",
+ async background() {
+ await browser.urlbar.engagementTelemetry.set({ value: true });
+ browser.test.sendMessage("ready");
+ },
+ });
+ await ext.startup();
+ await ext.awaitMessage("ready");
+
+ Assert.equal(
+ getPrefValue(),
+ true,
+ "Successfully enabled the engagement telemetry"
+ );
+
+ let completed = promiseUninstallCompleted(ext.id);
+ await ext.unload();
+ await completed;
+
+ Assert.equal(
+ getPrefValue(),
+ false,
+ "Engagement telemetry should be reset after unloading the add-on"
+ );
+});
diff --git a/browser/components/extensions/test/xpcshell/xpcshell.ini b/browser/components/extensions/test/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..99e91b32d1
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/xpcshell.ini
@@ -0,0 +1,38 @@
+[DEFAULT]
+head = head.js
+firefox-appdir = browser
+tags = webextensions
+dupe-manifest =
+
+[test_ext_bookmarks.js]
+[test_ext_browsingData_downloads.js]
+[test_ext_browsingData_passwords.js]
+skip-if = tsan # Times out, bug 1612707
+[test_ext_browsingData_settings.js]
+[test_ext_chrome_settings_overrides_home.js]
+[test_ext_chrome_settings_overrides_update.js]
+[test_ext_distribution_popup.js]
+[test_ext_history.js]
+[test_ext_homepage_overrides_private.js]
+[test_ext_manifest.js]
+[test_ext_manifest_commands.js]
+[test_ext_manifest_omnibox.js]
+[test_ext_manifest_permissions.js]
+[test_ext_normandyAddonStudy.js]
+[test_ext_pageAction_shutdown.js]
+[test_ext_pkcs11_management.js]
+[test_ext_settings_overrides_defaults.js]
+support-files =
+ data/test/manifest.json
+ data/test2/manifest.json
+[test_ext_settings_overrides_search.js]
+[test_ext_settings_overrides_search_mozParam.js]
+support-files =
+ data/test/manifest.json
+[test_ext_settings_overrides_shutdown.js]
+[test_ext_settings_validate.js]
+[test_ext_topSites.js]
+[test_ext_url_overrides_newtab.js]
+[test_ext_url_overrides_newtab_update.js]
+[test_ext_urlbar.js]
+skip-if = tsan # Unreasonably slow, bug 1612707