summaryrefslogtreecommitdiffstats
path: root/browser/components/extensions/test/xpcshell
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/extensions/test/xpcshell')
-rw-r--r--browser/components/extensions/test/xpcshell/.eslintrc.js9
-rw-r--r--browser/components/extensions/test/xpcshell/data/test/manifest.json80
-rw-r--r--browser/components/extensions/test/xpcshell/data/test2/manifest.json23
-rw-r--r--browser/components/extensions/test/xpcshell/head.js78
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_bookmarks.js1725
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_browsingData_downloads.js126
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_browsingData_passwords.js96
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_browsingData_settings.js147
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_chrome_settings_overrides_home.js231
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_chrome_settings_overrides_update.js794
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_distribution_popup.js56
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_history.js864
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_homepage_overrides_private.js134
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_manifest.js105
-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.js85
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_menu_caller.js53
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_menu_startup.js432
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_normandyAddonStudy.js243
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_pageAction_shutdown.js81
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_pkcs11_management.js300
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_settings_overrides_defaults.js263
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_settings_overrides_search.js597
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_settings_overrides_search_mozParam.js239
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_settings_overrides_shutdown.js109
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_settings_validate.js193
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_topSites.js293
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_url_overrides_newtab.js340
-rw-r--r--browser/components/extensions/test/xpcshell/test_ext_url_overrides_newtab_update.js127
-rw-r--r--browser/components/extensions/test/xpcshell/xpcshell.toml69
31 files changed, 8006 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..b14c90e9c4
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/data/test/manifest.json
@@ -0,0 +1,80 @@
+{
+ "name": "MozParamsTest",
+ "manifest_version": 2,
+ "version": "1.0",
+ "browser_specific_settings": {
+ "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"
+ },
+ {
+ "name": "experimenter-1",
+ "condition": "pref",
+ "pref": "nimbus-key-1"
+ },
+ {
+ "name": "experimenter-2",
+ "condition": "pref",
+ "pref": "nimbus-key-2"
+ }
+ ]
+ }
+ }
+}
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..197a993189
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/data/test2/manifest.json
@@ -0,0 +1,23 @@
+{
+ "name": "MozParamsTest2",
+ "manifest_version": 2,
+ "version": "1.0",
+ "browser_specific_settings": {
+ "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..9ac33637ed
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/head.js
@@ -0,0 +1,78 @@
+"use strict";
+
+/* exported createHttpServer, promiseConsoleOutput, assertPersistentListeners */
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+// eslint-disable-next-line no-unused-vars
+ChromeUtils.defineESModuleGetters(this, {
+ Extension: "resource://gre/modules/Extension.sys.mjs",
+ ExtensionData: "resource://gre/modules/Extension.sys.mjs",
+ ExtensionTestUtils:
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs",
+ FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
+ HttpServer: "resource://testing-common/httpd.sys.mjs",
+ NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
+ Schemas: "resource://gre/modules/Schemas.sys.mjs",
+ TestUtils: "resource://testing-common/TestUtils.sys.mjs",
+});
+
+ExtensionTestUtils.init(this);
+
+// Persistent Listener test functionality
+const { assertPersistentListeners } = ExtensionTestUtils.testAssertions;
+
+/**
+ * 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..15d09d1163
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_bookmarks.js
@@ -0,0 +1,1725 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
+});
+
+AddonTestUtils.init(this);
+AddonTestUtils.overrideCertDB();
+AddonTestUtils.createAppInfo(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "1",
+ "43"
+);
+
+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, title, 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(
+ title,
+ node.title,
+ "onRemoved event received the expected node title"
+ );
+ 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,
+ "new test title",
+ "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: "Μοζιλλας",
+ 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,
+ "Μοζιλλας",
+ "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("ΜοΖΙΛΛΑς");
+ })
+ .then(results => {
+ browser.test.assertEq(
+ 1,
+ results.length,
+ "Expected number of results returned for non-ascii search"
+ );
+ browser.test.assertEq(
+ "Μοζιλλας",
+ 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,
+ "Mozilla Folder"
+ );
+
+ 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,
+ "Empty 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.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,
+ "Empty Folder",
+ 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();
+});
+
+add_task(
+ {
+ pref_set: [["extensions.eventPages.enabled", true]],
+ },
+ async function test_bookmarks_event_page() {
+ await AddonTestUtils.promiseStartupManager();
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ browser_specific_settings: { gecko: { id: "eventpage@bookmarks" } },
+ permissions: ["bookmarks"],
+ background: { persistent: false },
+ },
+ background() {
+ browser.bookmarks.onCreated.addListener(() => {
+ browser.test.sendMessage("onCreated");
+ });
+ browser.bookmarks.onRemoved.addListener(() => {
+ browser.test.sendMessage("onRemoved");
+ });
+ browser.bookmarks.onChanged.addListener(() => {});
+ browser.bookmarks.onMoved.addListener(() => {});
+ browser.test.sendMessage("ready");
+ },
+ });
+
+ const EVENTS = ["onCreated", "onRemoved", "onChanged", "onMoved"];
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+ for (let event of EVENTS) {
+ assertPersistentListeners(extension, "bookmarks", event, {
+ primed: false,
+ });
+ }
+
+ // test events waken background
+ await extension.terminateBackground();
+ for (let event of EVENTS) {
+ assertPersistentListeners(extension, "bookmarks", event, {
+ primed: true,
+ });
+ }
+
+ let bookmark = {
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: `http://example.com/12345`,
+ title: `My bookmark 12345`,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ };
+ await PlacesUtils.bookmarks.insert(bookmark);
+
+ await extension.awaitMessage("ready");
+ await extension.awaitMessage("onCreated");
+ for (let event of EVENTS) {
+ assertPersistentListeners(extension, "bookmarks", event, {
+ primed: false,
+ });
+ }
+
+ await AddonTestUtils.promiseRestartManager();
+ await extension.awaitStartup();
+
+ for (let event of EVENTS) {
+ assertPersistentListeners(extension, "bookmarks", event, {
+ primed: true,
+ });
+ }
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await extension.awaitMessage("ready");
+ await extension.awaitMessage("onRemoved");
+
+ await extension.unload();
+ await AddonTestUtils.promiseShutdownManager();
+ }
+);
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..1257f23600
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_browsingData_downloads.js
@@ -0,0 +1,126 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ Downloads: "resource://gre/modules/Downloads.sys.mjs",
+});
+
+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..68a2c2cdc5
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_browsingData_passwords.js
@@ -0,0 +1,96 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+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";
+
+async function checkLoginExists(origin, shouldExist) {
+ const logins = await Services.logins.searchLoginsAsync({ origin });
+ equal(
+ logins.length,
+ shouldExist ? 1 : 0,
+ `Login for origin ${origin} should ${shouldExist ? "" : "not"} be found.`
+ );
+}
+
+async function addLogin(host, timestamp) {
+ await 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;
+ await Services.logins.addLoginAsync(login);
+ await checkLoginExists(host, true);
+}
+
+async function setupPasswords() {
+ // Remove all logins if any (included FxAccounts one in case one got captured in
+ // a conditioned profile, see Bug 1853617).
+ Services.logins.removeAllLogins();
+ await addLogin(FXA_HOST, REFERENCE_DATE);
+ await addLogin(NEW_HOST, REFERENCE_DATE);
+ await 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");
+ });
+ }
+
+ const 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");
+
+ await checkLoginExists(OLD_HOST, false);
+ await checkLoginExists(NEW_HOST, false);
+ await checkLoginExists(FXA_HOST, true);
+
+ // Clear passwords with recent since value.
+ await setupPasswords();
+ extension.sendMessage(method, { since: REFERENCE_DATE - 1000 });
+ await extension.awaitMessage("passwordsRemoved");
+
+ await checkLoginExists(OLD_HOST, true);
+ await checkLoginExists(NEW_HOST, false);
+ await checkLoginExists(FXA_HOST, true);
+
+ // Clear passwords with old since value.
+ await setupPasswords();
+ extension.sendMessage(method, { since: REFERENCE_DATE - 20000 });
+ await extension.awaitMessage("passwordsRemoved");
+
+ await checkLoginExists(OLD_HOST, false);
+ await checkLoginExists(NEW_HOST, false);
+ await 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..9d2241895c
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_browsingData_settings.js
@@ -0,0 +1,147 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ Preferences: "resource://gre/modules/Preferences.sys.mjs",
+ Sanitizer: "resource:///modules/Sanitizer.sys.mjs",
+});
+
+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.
+ Assert.less(
+ 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..d61e5b6b5e
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_chrome_settings_overrides_home.js
@@ -0,0 +1,231 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ HomePage: "resource:///modules/HomePage.sys.mjs",
+ RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
+ TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs",
+ sinon: "resource://testing-common/Sinon.sys.mjs",
+});
+
+function promisePrefChanged(expectedValue) {
+ return TestUtils.waitForPrefChange("browser.startup.homepage", value =>
+ value.endsWith(expectedValue)
+ );
+}
+
+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;
+});
+
+add_task(async function test_overriding_homepage_locale() {
+ Services.locale.availableLocales = ["en-US", "es-ES"];
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: {
+ gecko: {
+ id: "homepage@example.com",
+ },
+ },
+ chrome_settings_overrides: {
+ homepage: "/__MSG_homepage__",
+ },
+ name: "extension",
+ default_locale: "en",
+ },
+ useAddonManager: "permanent",
+
+ files: {
+ "_locales/en/messages.json": {
+ homepage: {
+ message: "homepage.html",
+ description: "homepage",
+ },
+ },
+
+ "_locales/es_ES/messages.json": {
+ homepage: {
+ message: "default.html",
+ description: "homepage",
+ },
+ },
+ },
+ });
+
+ let prefPromise = promisePrefChanged("homepage.html");
+ await extension.startup();
+ await prefPromise;
+
+ Assert.equal(
+ HomePage.get(),
+ `moz-extension://${extension.uuid}/homepage.html`,
+ "Should have overridden the new homepage"
+ );
+
+ // Set the new locale now, and disable the L10nRegistry reset
+ // when shutting down the addon mananger. This allows us to
+ // restart under a new locale without a lot of fuss.
+ let reqLoc = Services.locale.requestedLocales;
+ Services.locale.requestedLocales = ["es-ES"];
+
+ prefPromise = promisePrefChanged("default.html");
+ await AddonTestUtils.promiseShutdownManager({ clearL10nRegistry: false });
+ await AddonTestUtils.promiseStartupManager();
+ await extension.awaitStartup();
+ await prefPromise;
+
+ Assert.equal(
+ HomePage.get(),
+ `moz-extension://${extension.uuid}/default.html`,
+ "Should have overridden the new homepage"
+ );
+
+ await extension.unload();
+
+ Services.locale.requestedLocales = reqLoc;
+});
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..aac00a8023
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_chrome_settings_overrides_update.js
@@ -0,0 +1,794 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
+ HomePage: "resource:///modules/HomePage.sys.mjs",
+ RemoteSettings: "resource://services-settings/remote-settings.sys.mjs",
+ sinon: "resource://testing-common/Sinon.sys.mjs",
+});
+
+AddonTestUtils.init(this);
+AddonTestUtils.overrideCertDB();
+
+AddonTestUtils.createAppInfo(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "1",
+ "42"
+);
+
+// Similar to TestUtils.topicObserved, but returns a deferred promise that
+// can be resolved
+function topicObservable(topic, checkFn) {
+ let deferred = Promise.withResolvers();
+ function observer(subject, topic, data) {
+ try {
+ if (checkFn && !checkFn(subject, data)) {
+ return;
+ }
+ deferred.resolve([subject, data]);
+ } catch (ex) {
+ deferred.reject(ex);
+ }
+ }
+ deferred.promise.finally(() => {
+ Services.obs.removeObserver(observer, topic);
+ checkFn = null;
+ });
+ Services.obs.addObserver(observer, topic);
+
+ return deferred;
+}
+
+async function setupRemoteSettings() {
+ const settings = await RemoteSettings("hijack-blocklists");
+ sinon.stub(settings, "get").returns([
+ {
+ id: "homepage-urls",
+ matches: ["ignore=me"],
+ _status: "synced",
+ },
+ ]);
+}
+
+function promisePrefChanged(expectedValue) {
+ return TestUtils.waitForPrefChange("browser.startup.homepage", value =>
+ value.endsWith(expectedValue)
+ );
+}
+
+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 search without user
+ * interaction. */
+
+ const EXTENSION_ID = "test_overrides_update@tests.mozilla.org";
+ const HOMEPAGE_URI = "webext-homepage-1.html";
+
+ let extensionInfo = {
+ useAddonManager: "permanent",
+ manifest: {
+ version: "1.0",
+ browser_specific_settings: {
+ 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;
+ Assert.notStrictEqual(
+ defaultEngineName,
+ "DuckDuckGo",
+ "Default engine is not DuckDuckGo."
+ );
+
+ let prefPromise = promisePrefChanged(HOMEPAGE_URI);
+
+ // When an addon is installed that overrides an app-provided engine (builtin)
+ // that is the default, we do not prompt for default.
+ let deferredPrompt = topicObservable(
+ "webextension-defaultsearch-prompt",
+ (subject, message) => {
+ if (subject.wrappedJSObject.id == extension.id) {
+ ok(false, "default override should not prompt");
+ }
+ }
+ );
+
+ await Promise.race([extension.startup(), deferredPrompt.promise]);
+ deferredPrompt.resolve();
+ 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",
+ browser_specific_settings: {
+ 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",
+ browser_specific_settings: {
+ gecko: {
+ id: EXTENSION_ID,
+ },
+ },
+ },
+ };
+ let extension = ExtensionTestUtils.loadExtension(extensionInfo);
+
+ let defaultHomepageURL = HomePage.get();
+ let defaultEngineName = (await Services.search.getDefault()).name;
+ Assert.notStrictEqual(
+ 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",
+ browser_specific_settings: {
+ 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 prefPromise = promisePrefChanged(HOMEPAGE_URI);
+
+ let deferredUpgradePrompt = topicObservable(
+ "webextension-defaultsearch-prompt",
+ (subject, message) => {
+ if (subject.wrappedJSObject.id == extension.id) {
+ ok(false, "should not prompt on update");
+ }
+ }
+ );
+
+ await Promise.race([
+ extension.upgrade(extensionInfo),
+ deferredUpgradePrompt.promise,
+ ]);
+ deferredUpgradePrompt.resolve();
+ await AddonTestUtils.waitForSearchProviderStartup(extension);
+ await prefPromise;
+
+ equal(
+ extension.version,
+ "2.0",
+ "The updated addon has the expected version."
+ );
+ ok(
+ HomePage.get().endsWith(HOMEPAGE_URI),
+ "Home page url is 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();
+});
+
+add_task(async function test_overrides_update_homepage_change() {
+ /* This tests the scenario where an addon changes
+ * a homepage url when upgrading. */
+
+ const EXTENSION_ID = "test_overrides_update@tests.mozilla.org";
+ const HOMEPAGE_URI = "webext-homepage-1.html";
+ const HOMEPAGE_URI_2 = "webext-homepage-2.html";
+
+ let extensionInfo = {
+ useAddonManager: "permanent",
+ manifest: {
+ version: "1.0",
+ browser_specific_settings: {
+ gecko: {
+ id: EXTENSION_ID,
+ },
+ },
+ chrome_settings_overrides: {
+ homepage: HOMEPAGE_URI,
+ },
+ },
+ };
+ let extension = ExtensionTestUtils.loadExtension(extensionInfo);
+
+ let prefPromise = promisePrefChanged(HOMEPAGE_URI);
+ await extension.startup();
+ await prefPromise;
+
+ equal(
+ extension.version,
+ "1.0",
+ "The installed addon has the expected version."
+ );
+ ok(
+ HomePage.get().endsWith(HOMEPAGE_URI),
+ "Home page url is the extension url after startup."
+ );
+
+ extensionInfo.manifest = {
+ version: "2.0",
+ browser_specific_settings: {
+ gecko: {
+ id: EXTENSION_ID,
+ },
+ },
+ chrome_settings_overrides: {
+ homepage: HOMEPAGE_URI_2,
+ },
+ };
+
+ prefPromise = promisePrefChanged(HOMEPAGE_URI_2);
+ await extension.upgrade(extensionInfo);
+ await prefPromise;
+
+ equal(
+ extension.version,
+ "2.0",
+ "The updated addon has the expected version."
+ );
+ ok(
+ HomePage.get().endsWith(HOMEPAGE_URI_2),
+ "Home page url is by the extension after upgrade."
+ );
+
+ await extension.unload();
+});
+
+async function withHandlingDefaultSearchPrompt({ extensionId, respond }, cb) {
+ const promptResponseHandled = TestUtils.topicObserved(
+ "webextension-defaultsearch-prompt-response"
+ );
+ const prompted = TestUtils.topicObserved(
+ "webextension-defaultsearch-prompt",
+ (subject, message) => {
+ if (subject.wrappedJSObject.id == extensionId) {
+ return subject.wrappedJSObject.respond(respond);
+ }
+ }
+ );
+
+ await Promise.all([cb(), prompted, promptResponseHandled]);
+}
+
+async function assertUpdateDoNotPrompt(extension, updateExtensionInfo) {
+ let deferredUpgradePrompt = topicObservable(
+ "webextension-defaultsearch-prompt",
+ (subject, message) => {
+ if (subject.wrappedJSObject.id == extension.id) {
+ ok(false, "should not prompt on update");
+ }
+ }
+ );
+
+ await Promise.race([
+ extension.upgrade(updateExtensionInfo),
+ deferredUpgradePrompt.promise,
+ ]);
+ deferredUpgradePrompt.resolve();
+
+ await AddonTestUtils.waitForSearchProviderStartup(extension);
+
+ equal(
+ extension.version,
+ updateExtensionInfo.manifest.version,
+ "The updated addon has the expected version."
+ );
+}
+
+add_task(async function test_default_search_prompts() {
+ /* This tests the scenario where an addon did not gain
+ * default search during install, and later upgrades.
+ * The addon should not gain default in updates.
+ * If the addon is disabled, it should prompt again when
+ * enabled.
+ */
+
+ const EXTENSION_ID = "test_default_update@tests.mozilla.org";
+
+ let extensionInfo = {
+ useAddonManager: "permanent",
+ manifest: {
+ version: "1.0",
+ browser_specific_settings: {
+ gecko: {
+ id: EXTENSION_ID,
+ },
+ },
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "Example",
+ search_url: "https://example.com/?q={searchTerms}",
+ is_default: true,
+ },
+ },
+ },
+ };
+
+ let extension = ExtensionTestUtils.loadExtension(extensionInfo);
+
+ let defaultEngineName = (await Services.search.getDefault()).name;
+ Assert.notStrictEqual(defaultEngineName, "Example", "Search is not Example.");
+
+ // Mock a response from the default search prompt where we
+ // say no to setting this as the default when installing.
+ await withHandlingDefaultSearchPrompt(
+ { extensionId: EXTENSION_ID, respond: false },
+ () => extension.startup()
+ );
+
+ equal(
+ extension.version,
+ "1.0",
+ "The installed addon has the expected version."
+ );
+ equal(
+ (await Services.search.getDefault()).name,
+ defaultEngineName,
+ "Default engine is the default after startup."
+ );
+
+ info(
+ "Verify that updating the extension does not prompt and does not take over the default engine"
+ );
+
+ extensionInfo.manifest.version = "2.0";
+ await assertUpdateDoNotPrompt(extension, extensionInfo);
+ equal(
+ (await Services.search.getDefault()).name,
+ defaultEngineName,
+ "Default engine is still the default after update."
+ );
+
+ info("Verify that disable/enable the extension does prompt the user");
+
+ let addon = await AddonManager.getAddonByID(EXTENSION_ID);
+
+ await withHandlingDefaultSearchPrompt(
+ { extensionId: EXTENSION_ID, respond: false },
+ async () => {
+ await addon.disable();
+ await addon.enable();
+ }
+ );
+
+ // we still said no.
+ equal(
+ (await Services.search.getDefault()).name,
+ defaultEngineName,
+ "Default engine is the default after being disabling/enabling."
+ );
+
+ await extension.unload();
+});
+
+async function test_default_search_on_updating_addons_installed_before_bug1757760({
+ builtinAsInitialDefault,
+}) {
+ /* This tests covers a scenario similar to the previous test but with an extension-settings.json file
+ content like the one that would be available in the profile if the add-on was installed on firefox
+ versions that didn't include the changes from Bug 1757760 (See Bug 1767550).
+ */
+
+ const EXTENSION_ID = `test_old_addon@tests.mozilla.org`;
+ const EXTENSION_ID2 = `test_old_addon2@tests.mozilla.org`;
+
+ const extensionInfo = {
+ useAddonManager: "permanent",
+ manifest: {
+ version: "1.1",
+ browser_specific_settings: {
+ gecko: {
+ id: EXTENSION_ID,
+ },
+ },
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "Test SearchEngine",
+ search_url: "https://example.com/?q={searchTerms}",
+ is_default: true,
+ },
+ },
+ },
+ };
+
+ const extensionInfo2 = {
+ useAddonManager: "permanent",
+ manifest: {
+ version: "1.2",
+ browser_specific_settings: {
+ gecko: {
+ id: EXTENSION_ID2,
+ },
+ },
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "Test SearchEngine2",
+ search_url: "https://example.com/?q={searchTerms}",
+ is_default: true,
+ },
+ },
+ },
+ };
+
+ const { ExtensionSettingsStore } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionSettingsStore.sys.mjs"
+ );
+
+ async function assertExtensionSettingsStore(
+ extensionInfo,
+ expectedLevelOfControl
+ ) {
+ const { id } = extensionInfo.manifest.browser_specific_settings.gecko;
+ info(`Asserting ExtensionSettingsStore for ${id}`);
+ const item = ExtensionSettingsStore.getSetting(
+ "default_search",
+ "defaultSearch",
+ id
+ );
+ equal(
+ item.value,
+ extensionInfo.manifest.chrome_settings_overrides.search_provider.name,
+ "Got the expected item returned by ExtensionSettingsStore.getSetting"
+ );
+ const control = await ExtensionSettingsStore.getLevelOfControl(
+ id,
+ "default_search",
+ "defaultSearch"
+ );
+ equal(
+ control,
+ expectedLevelOfControl,
+ `Got expected levelOfControl for ${id}`
+ );
+ }
+
+ info("Install test extensions without opt-in to the related search engines");
+
+ let extension = ExtensionTestUtils.loadExtension(extensionInfo);
+ let extension2 = ExtensionTestUtils.loadExtension(extensionInfo2);
+
+ // Mock a response from the default search prompt where we
+ // say no to setting this as the default when installing.
+ await withHandlingDefaultSearchPrompt(
+ { extensionId: EXTENSION_ID, respond: false },
+ () => extension.startup()
+ );
+
+ equal(
+ extension.version,
+ "1.1",
+ "first installed addon has the expected version."
+ );
+
+ // Mock a response from the default search prompt where we
+ // say no to setting this as the default when installing.
+ await withHandlingDefaultSearchPrompt(
+ { extensionId: EXTENSION_ID2, respond: false },
+ () => extension2.startup()
+ );
+
+ equal(
+ extension2.version,
+ "1.2",
+ "second installed addon has the expected version."
+ );
+
+ info("Setup preconditions (set the initial default search engine)");
+
+ // Sanity check to be sure the initial engine expected as precondition
+ // for the scenario covered by the current test case.
+ let initialEngine;
+ if (builtinAsInitialDefault) {
+ initialEngine = Services.search.appDefaultEngine;
+ } else {
+ initialEngine = Services.search.getEngineByName(
+ extensionInfo.manifest.chrome_settings_overrides.search_provider.name
+ );
+ }
+ await Services.search.setDefault(
+ initialEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+
+ let defaultEngineName = (await Services.search.getDefault()).name;
+ Assert.equal(
+ defaultEngineName,
+ initialEngine.name,
+ `initial default search engine expected to be ${
+ builtinAsInitialDefault ? "app-provided" : EXTENSION_ID
+ }`
+ );
+ Assert.notEqual(
+ defaultEngineName,
+ extensionInfo2.manifest.chrome_settings_overrides.search_provider.name,
+ "initial default search engine name should not be the same as the second extension search_provider"
+ );
+
+ equal(
+ (await Services.search.getDefault()).name,
+ initialEngine.name,
+ `Default engine should still be set to the ${
+ builtinAsInitialDefault ? "app-provided" : EXTENSION_ID
+ }.`
+ );
+
+ // Mock an update from settings stored as in an older Firefox version where Bug 1757760 was not landed yet.
+ info(
+ "Setup preconditions (inject mock extension-settings.json data and assert on the expected setting and levelOfControl)"
+ );
+
+ let addon = await AddonManager.getAddonByID(EXTENSION_ID);
+ let addon2 = await AddonManager.getAddonByID(EXTENSION_ID2);
+
+ const extensionSettingsData = {
+ version: 2,
+ url_overrides: {},
+ prefs: {},
+ homepageNotification: {},
+ tabHideNotification: {},
+ default_search: {
+ defaultSearch: {
+ initialValue: Services.search.appDefaultEngine.name,
+ precedenceList: [
+ {
+ id: EXTENSION_ID2,
+ // The install dates are used in ExtensionSettingsStore.getLevelOfControl
+ // and to recreate the expected preconditions the last extension installed
+ // should have a installDate timestamp > then the first one.
+ installDate: addon2.installDate.getTime() + 1000,
+ value:
+ extensionInfo2.manifest.chrome_settings_overrides.search_provider
+ .name,
+ // When an addon with a default search engine override is installed in Firefox versions
+ // without the changes landed from Bug 1757760, `enabled` will be set to true in all cases
+ // (Prompt never answered, or when No or Yes is selected by the user).
+ enabled: true,
+ },
+ {
+ id: EXTENSION_ID,
+ installDate: addon.installDate.getTime(),
+ value:
+ extensionInfo.manifest.chrome_settings_overrides.search_provider
+ .name,
+ enabled: true,
+ },
+ ],
+ },
+ },
+ newTabNotification: {},
+ commands: {},
+ };
+
+ const file = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ file.append("extension-settings.json");
+
+ info(`writing mock settings data into ${file.path}`);
+ await IOUtils.writeJSON(file.path, extensionSettingsData);
+ await ExtensionSettingsStore._reloadFile(false);
+
+ equal(
+ (await Services.search.getDefault()).name,
+ initialEngine.name,
+ "Default engine is still set to the initial one."
+ );
+
+ // The following assertions verify that the migration applied from ExtensionSettingsStore
+ // fixed the inconsistent state and kept the search engine unchanged.
+ //
+ // - With the fixed settings we expect both to be resolved to "controllable_by_this_extension".
+ // - Without the fix applied during the migration the levelOfControl resolved would be:
+ // - for the last installed: "controlled_by_this_extension"
+ // - for the first installed: "controlled_by_other_extensions"
+ await assertExtensionSettingsStore(
+ extensionInfo2,
+ "controlled_by_this_extension"
+ );
+ await assertExtensionSettingsStore(
+ extensionInfo,
+ "controlled_by_other_extensions"
+ );
+
+ info(
+ "Verify that updating the extension does not prompt and does not take over the default engine"
+ );
+
+ extensionInfo2.manifest.version = "2.2";
+ await assertUpdateDoNotPrompt(extension2, extensionInfo2);
+
+ extensionInfo.manifest.version = "2.1";
+ await assertUpdateDoNotPrompt(extension, extensionInfo);
+
+ equal(
+ (await Services.search.getDefault()).name,
+ initialEngine.name,
+ "Default engine is still the same after updating both the test extensions."
+ );
+
+ // After both the extensions have been updated and their inconsistent state
+ // updated internally, both extensions should have levelOfControl "controllable_*".
+ await assertExtensionSettingsStore(
+ extensionInfo2,
+ "controllable_by_this_extension"
+ );
+ await assertExtensionSettingsStore(
+ extensionInfo,
+ // We expect levelOfControl to be controlled_by_this_extension if the test case
+ // is expecting the third party extension to stay set as default.
+ builtinAsInitialDefault
+ ? "controllable_by_this_extension"
+ : "controlled_by_this_extension"
+ );
+
+ info("Verify that disable/enable the extension does prompt the user");
+
+ await withHandlingDefaultSearchPrompt(
+ { extensionId: EXTENSION_ID2, respond: false },
+ async () => {
+ await addon2.disable();
+ await addon2.enable();
+ }
+ );
+
+ // we said no.
+ equal(
+ (await Services.search.getDefault()).name,
+ initialEngine.name,
+ `Default engine should still be the same after disabling/enabling ${EXTENSION_ID2}.`
+ );
+
+ await withHandlingDefaultSearchPrompt(
+ { extensionId: EXTENSION_ID, respond: false },
+ async () => {
+ await addon.disable();
+ await addon.enable();
+ }
+ );
+
+ // we said no.
+ equal(
+ (await Services.search.getDefault()).name,
+ Services.search.appDefaultEngine.name,
+ `Default engine should be set to the app default after disabling/enabling ${EXTENSION_ID}.`
+ );
+
+ await withHandlingDefaultSearchPrompt(
+ { extensionId: EXTENSION_ID, respond: true },
+ async () => {
+ await addon.disable();
+ await addon.enable();
+ }
+ );
+
+ // we responded yes.
+ equal(
+ (await Services.search.getDefault()).name,
+ extensionInfo.manifest.chrome_settings_overrides.search_provider.name,
+ "Default engine should be set to the one opted-in from the last prompt."
+ );
+
+ await extension.unload();
+ await extension2.unload();
+}
+
+add_task(function test_builtin_default_search_after_updating_old_addons() {
+ return test_default_search_on_updating_addons_installed_before_bug1757760({
+ builtinAsInitialDefault: true,
+ });
+});
+
+add_task(function test_third_party_default_search_after_updating_old_addons() {
+ return test_default_search_on_updating_addons_installed_before_bug1757760({
+ builtinAsInitialDefault: false,
+ });
+});
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..030e0b27be
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_distribution_popup.js
@@ -0,0 +1,56 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ ExtensionControlledPopup:
+ "resource:///modules/ExtensionControlledPopup.sys.mjs",
+ ExtensionSettingsStore:
+ "resource://gre/modules/ExtensionSettingsStore.sys.mjs",
+});
+
+/*
+ * 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: {
+ browser_specific_settings: { gecko: { id: distExtId } },
+ name: "Ext Distribution",
+ },
+ });
+
+ let userExtId = "ext-user@mochi.test";
+ let userExtension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ browser_specific_settings: { 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..c0f6c39be7
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_history.js
@@ -0,0 +1,864 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ ExtensionCommon: "resource://gre/modules/ExtensionCommon.sys.mjs",
+ PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
+});
+
+AddonTestUtils.init(this);
+AddonTestUtils.overrideCertDB();
+AddonTestUtils.createAppInfo(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "1",
+ "43"
+);
+
+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 {
+ removedUrls.push(...data.urls);
+ }
+ });
+
+ 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();
+});
+
+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) }],
+ },
+];
+
+add_task(async function test_search() {
+ 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");
+ 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"
+ );
+ Assert.deepEqual(
+ {
+ id: onVisitedData[3].id,
+ url: SINGLE_VISIT_URL,
+ title: "Title Changed",
+ },
+ onTitleChangedData,
+ "expected event data for onTitleChanged"
+ );
+
+ await extension.unload();
+});
+
+add_task(
+ {
+ pref_set: [["extensions.eventPages.enabled", true]],
+ },
+ async function test_history_event_page() {
+ await AddonTestUtils.promiseStartupManager();
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ browser_specific_settings: { gecko: { id: "eventpage@history" } },
+ permissions: ["history"],
+ background: { persistent: false },
+ },
+ background() {
+ browser.history.onVisited.addListener(() => {
+ browser.test.sendMessage("onVisited");
+ });
+ browser.history.onVisitRemoved.addListener(() => {
+ browser.test.sendMessage("onVisitRemoved");
+ });
+ browser.history.onTitleChanged.addListener(() => {});
+ browser.test.sendMessage("ready");
+ },
+ });
+
+ const EVENTS = ["onVisited", "onVisitRemoved", "onTitleChanged"];
+ await PlacesUtils.history.clear();
+
+ await extension.startup();
+ await extension.awaitMessage("ready");
+ for (let event of EVENTS) {
+ assertPersistentListeners(extension, "history", event, {
+ primed: false,
+ });
+ }
+
+ // test events waken background
+ await extension.terminateBackground();
+ for (let event of EVENTS) {
+ assertPersistentListeners(extension, "history", event, {
+ primed: true,
+ });
+ }
+
+ await PlacesUtils.history.insertMany(PAGE_INFOS);
+
+ await extension.awaitMessage("ready");
+ await extension.awaitMessage("onVisited");
+ ok(true, "persistent event woke background");
+ for (let event of EVENTS) {
+ assertPersistentListeners(extension, "history", event, {
+ primed: false,
+ });
+ }
+
+ await AddonTestUtils.promiseRestartManager();
+ await extension.awaitStartup();
+
+ for (let event of EVENTS) {
+ assertPersistentListeners(extension, "history", event, {
+ primed: true,
+ });
+ }
+
+ await PlacesUtils.history.clear();
+ await extension.awaitMessage("ready");
+ await extension.awaitMessage("onVisitRemoved");
+
+ await extension.unload();
+ await AddonTestUtils.promiseShutdownManager();
+ }
+);
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..2d2bccc1e2
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_homepage_overrides_private.js
@@ -0,0 +1,134 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+const { HomePage } = ChromeUtils.importESModule(
+ "resource:///modules/HomePage.sys.mjs"
+);
+const { ExtensionPermissions } = ChromeUtils.importESModule(
+ "resource://gre/modules/ExtensionPermissions.sys.mjs"
+);
+
+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);
+
+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",
+ browser_specific_settings: {
+ 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",
+ browser_specific_settings: {
+ 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..b978172ca2
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_manifest.js
@@ -0,0 +1,105 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+Services.prefs.setBoolPref("extensions.manifestV3.enabled", true);
+
+async function testManifest(manifest, expectedError) {
+ ExtensionTestUtils.failOnSchemaWarnings(false);
+ let normalized = await ExtensionTestUtils.normalizeManifest(manifest);
+ ExtensionTestUtils.failOnSchemaWarnings(true);
+
+ if (expectedError) {
+ ok(
+ expectedError.test(normalized.error),
+ `Should have an error for ${JSON.stringify(manifest)}, got ${
+ normalized.error
+ }`
+ );
+ } else {
+ ok(
+ !normalized.error,
+ `Should not have an error ${JSON.stringify(manifest)}, ${
+ normalized.error
+ }`
+ );
+ }
+ return normalized.errors;
+}
+
+const all_actions = [
+ "action",
+ "browser_action",
+ "page_action",
+ "sidebar_action",
+];
+
+add_task(async function test_manifest() {
+ let badpaths = ["", " ", "\t", "http://foo.com/icon.png"];
+ for (let path of badpaths) {
+ for (let action of all_actions) {
+ let manifest_version = action == "action" ? 3 : 2;
+ let manifest = { manifest_version };
+ manifest[action] = { default_icon: path };
+ let error = new RegExp(`Error processing ${action}.default_icon`);
+ await testManifest(manifest, error);
+
+ manifest[action] = { default_icon: { 16: path } };
+ await testManifest(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 all_actions) {
+ let manifest_version = action == "action" ? 3 : 2;
+ let manifest = { manifest_version };
+ manifest[action] = { default_icon: path };
+ if (action == "sidebar_action") {
+ // Sidebar requires panel.
+ manifest[action].default_panel = "foo.html";
+ }
+ await testManifest(manifest);
+
+ manifest[action] = { default_icon: { 16: path } };
+ if (action == "sidebar_action") {
+ manifest[action].default_panel = "foo.html";
+ }
+ await testManifest(manifest);
+ }
+ }
+});
+
+add_task(async function test_action_version() {
+ let warnings = await testManifest({
+ manifest_version: 3,
+ browser_action: {
+ default_panel: "foo.html",
+ },
+ });
+ Assert.deepEqual(
+ warnings,
+ [`Property "browser_action" is unsupported in Manifest Version 3`],
+ `Manifest v3 with "browser_action" key logs an error.`
+ );
+
+ warnings = await testManifest({
+ manifest_version: 2,
+ action: {
+ default_icon: "",
+ default_panel: "foo.html",
+ },
+ });
+
+ Assert.deepEqual(
+ warnings,
+ [`Property "action" is unsupported in Manifest Version 2`],
+ `Manifest v2 with "action" key first warning is clear.`
+ );
+});
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..fed7af5d5b
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_manifest_permissions.js
@@ -0,0 +1,85 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+/* globals chrome */
+
+Services.prefs.setBoolPref("extensions.manifestV3.enabled", true);
+
+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_action() {
+ await testPermission({
+ namespace: "action",
+ manifest: {
+ manifest_version: 3,
+ action: {},
+ },
+ });
+});
+
+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_menu_caller.js b/browser/components/extensions/test/xpcshell/test_ext_menu_caller.js
new file mode 100644
index 0000000000..5aa04bbc78
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_menu_caller.js
@@ -0,0 +1,53 @@
+/* -*- 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_create_menu_ext_error() {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["menus"],
+ },
+ async background() {
+ let { fileName } = new Error();
+ browser.menus.create({
+ id: "muted-tab",
+ title: "open link with Menu 1",
+ contexts: ["link"],
+ });
+ await new Promise(resolve => {
+ browser.menus.create(
+ {
+ id: "muted-tab",
+ title: "open link with Menu 2",
+ contexts: ["link"],
+ },
+ resolve
+ );
+ });
+ browser.test.sendMessage("fileName", fileName);
+ },
+ });
+
+ let fileName;
+ const { messages } = await promiseConsoleOutput(async () => {
+ await extension.startup();
+ fileName = await extension.awaitMessage("fileName");
+ await extension.unload();
+ });
+ let [msg] = messages
+ .filter(m => m.message.includes("Unchecked lastError"))
+ .map(m => m.QueryInterface(Ci.nsIScriptError));
+ equal(msg.sourceName, fileName, "Message source");
+
+ equal(
+ msg.errorMessage,
+ "Unchecked lastError value: Error: ID already exists: muted-tab",
+ "Message content"
+ );
+ equal(msg.lineNumber, 9, "Message line");
+
+ let frame = msg.stack;
+ equal(frame.source, fileName, "Frame source");
+ equal(frame.line, 9, "Frame line");
+ equal(frame.column, 23, "Frame column");
+});
diff --git a/browser/components/extensions/test/xpcshell/test_ext_menu_startup.js b/browser/components/extensions/test/xpcshell/test_ext_menu_startup.js
new file mode 100644
index 0000000000..aa019c6584
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_menu_startup.js
@@ -0,0 +1,432 @@
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ ExtensionParent: "resource://gre/modules/ExtensionParent.sys.mjs",
+ Management: "resource://gre/modules/Extension.sys.mjs",
+});
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+AddonTestUtils.init(this);
+AddonTestUtils.overrideCertDB();
+AddonTestUtils.createAppInfo(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "42",
+ "42"
+);
+
+Services.prefs.setBoolPref("extensions.eventPages.enabled", true);
+
+function getExtension(id, background, useAddonManager) {
+ return ExtensionTestUtils.loadExtension({
+ useAddonManager,
+ manifest: {
+ browser_specific_settings: { gecko: { id } },
+ permissions: ["menus"],
+ background: { persistent: false },
+ },
+ background,
+ });
+}
+
+async function expectCached(extension, expect) {
+ let { StartupCache } = ExtensionParent;
+ let cached = await StartupCache.menus.get(extension.id);
+ let createProperties = Array.from(cached.values());
+ equal(cached.size, expect.length, "menus saved in cache");
+ // The menus startupCache is a map and the order is significant
+ // for recreating menus on startup. Ensure that they are in
+ // the expected order. We only verify specific keys here rather
+ // than all menu properties.
+ for (let i in createProperties) {
+ Assert.deepEqual(
+ createProperties[i],
+ expect[i],
+ "expected cached properties exist"
+ );
+ }
+}
+
+function promiseExtensionEvent(wrapper, event) {
+ return new Promise(resolve => {
+ wrapper.extension.once(event, (kind, data) => {
+ resolve(data);
+ });
+ });
+}
+
+add_setup(async () => {
+ await AddonTestUtils.promiseStartupManager();
+});
+
+add_task(async function test_menu_onInstalled() {
+ async function background() {
+ browser.runtime.onInstalled.addListener(async () => {
+ const parentId = browser.menus.create({
+ contexts: ["all"],
+ title: "parent",
+ id: "test-parent",
+ });
+ browser.menus.create({
+ parentId,
+ title: "click A",
+ id: "test-click-a",
+ });
+ browser.menus.create(
+ {
+ parentId,
+ title: "click B",
+ id: "test-click-b",
+ },
+ () => {
+ browser.test.sendMessage("onInstalled");
+ }
+ );
+ });
+ browser.menus.create(
+ {
+ contexts: ["tab"],
+ title: "top-level",
+ id: "test-top-level",
+ },
+ () => {
+ browser.test.sendMessage("create", browser.runtime.lastError?.message);
+ }
+ );
+
+ browser.test.onMessage.addListener(async msg => {
+ browser.test.log(`onMessage ${msg}`);
+ if (msg == "updatemenu") {
+ await browser.menus.update("test-click-a", { title: "click updated" });
+ } else if (msg == "removemenu") {
+ await browser.menus.remove("test-click-b");
+ } else if (msg == "removeall") {
+ await browser.menus.removeAll();
+ }
+ browser.test.sendMessage("updated");
+ });
+ }
+
+ const extension = getExtension(
+ "test-persist@mochitest",
+ background,
+ "permanent"
+ );
+
+ await extension.startup();
+ let lastError = await extension.awaitMessage("create");
+ Assert.equal(lastError, undefined, "no error creating menu");
+ await extension.awaitMessage("onInstalled");
+ await extension.terminateBackground();
+
+ await expectCached(extension, [
+ {
+ contexts: ["tab"],
+ id: "test-top-level",
+ title: "top-level",
+ },
+ { contexts: ["all"], id: "test-parent", title: "parent" },
+ {
+ id: "test-click-a",
+ parentId: "test-parent",
+ title: "click A",
+ },
+ {
+ id: "test-click-b",
+ parentId: "test-parent",
+ title: "click B",
+ },
+ ]);
+
+ await extension.wakeupBackground();
+ lastError = await extension.awaitMessage("create");
+ Assert.equal(
+ lastError,
+ "The menu id test-top-level already exists in menus.create.",
+ "correct error creating menu"
+ );
+
+ await AddonTestUtils.promiseRestartManager();
+ await extension.awaitStartup();
+
+ // verify the startupcache
+ await expectCached(extension, [
+ {
+ contexts: ["tab"],
+ id: "test-top-level",
+ title: "top-level",
+ },
+ { contexts: ["all"], id: "test-parent", title: "parent" },
+ {
+ id: "test-click-a",
+ parentId: "test-parent",
+ title: "click A",
+ },
+ {
+ id: "test-click-b",
+ parentId: "test-parent",
+ title: "click B",
+ },
+ ]);
+
+ equal(
+ extension.extension.backgroundState,
+ "stopped",
+ "background is not running"
+ );
+ await extension.wakeupBackground();
+ lastError = await extension.awaitMessage("create");
+ Assert.equal(
+ lastError,
+ "The menu id test-top-level already exists in menus.create.",
+ "correct error creating menu"
+ );
+
+ extension.sendMessage("updatemenu");
+ await extension.awaitMessage("updated");
+ await extension.terminateBackground();
+
+ // Title change is cached
+ await expectCached(extension, [
+ {
+ contexts: ["tab"],
+ id: "test-top-level",
+ title: "top-level",
+ },
+ { contexts: ["all"], id: "test-parent", title: "parent" },
+ {
+ id: "test-click-a",
+ parentId: "test-parent",
+ title: "click updated",
+ },
+ {
+ id: "test-click-b",
+ parentId: "test-parent",
+ title: "click B",
+ },
+ ]);
+
+ await extension.wakeupBackground();
+ lastError = await extension.awaitMessage("create");
+ Assert.equal(
+ lastError,
+ "The menu id test-top-level already exists in menus.create.",
+ "correct error creating menu"
+ );
+
+ extension.sendMessage("removemenu");
+ await extension.awaitMessage("updated");
+ await extension.terminateBackground();
+
+ // menu removed
+ await expectCached(extension, [
+ {
+ contexts: ["tab"],
+ id: "test-top-level",
+ title: "top-level",
+ },
+ { contexts: ["all"], id: "test-parent", title: "parent" },
+ {
+ id: "test-click-a",
+ parentId: "test-parent",
+ title: "click updated",
+ },
+ ]);
+
+ await extension.wakeupBackground();
+ lastError = await extension.awaitMessage("create");
+ Assert.equal(
+ lastError,
+ "The menu id test-top-level already exists in menus.create.",
+ "correct error creating menu"
+ );
+
+ extension.sendMessage("removeall");
+ await extension.awaitMessage("updated");
+ await extension.terminateBackground();
+
+ // menus removed
+ await expectCached(extension, []);
+
+ await extension.unload();
+});
+
+add_task(async function test_menu_nested() {
+ async function background() {
+ browser.test.onMessage.addListener(async (action, properties) => {
+ browser.test.log(`onMessage ${action}`);
+ switch (action) {
+ case "create":
+ await new Promise(resolve => {
+ browser.menus.create(properties, resolve);
+ });
+ break;
+ case "update":
+ {
+ let { id, ...update } = properties;
+ await browser.menus.update(id, update);
+ }
+ break;
+ case "remove":
+ {
+ let { id } = properties;
+ await browser.menus.remove(id);
+ }
+ break;
+ case "removeAll":
+ await browser.menus.removeAll();
+ break;
+ }
+ browser.test.sendMessage("updated");
+ });
+ }
+
+ const extension = getExtension(
+ "test-nesting@mochitest",
+ background,
+ "permanent"
+ );
+ await extension.startup();
+
+ extension.sendMessage("create", {
+ id: "first",
+ contexts: ["all"],
+ title: "first",
+ });
+ await extension.awaitMessage("updated");
+ await expectCached(extension, [
+ { contexts: ["all"], id: "first", title: "first" },
+ ]);
+
+ extension.sendMessage("create", {
+ id: "second",
+ contexts: ["all"],
+ title: "second",
+ });
+ await extension.awaitMessage("updated");
+ await expectCached(extension, [
+ { contexts: ["all"], id: "first", title: "first" },
+ { contexts: ["all"], id: "second", title: "second" },
+ ]);
+
+ extension.sendMessage("create", {
+ id: "third",
+ contexts: ["all"],
+ title: "third",
+ parentId: "first",
+ });
+ await extension.awaitMessage("updated");
+ await expectCached(extension, [
+ { contexts: ["all"], id: "first", title: "first" },
+ { contexts: ["all"], id: "second", title: "second" },
+ {
+ contexts: ["all"],
+ id: "third",
+ parentId: "first",
+ title: "third",
+ },
+ ]);
+
+ extension.sendMessage("create", {
+ id: "fourth",
+ contexts: ["all"],
+ title: "fourth",
+ });
+ await extension.awaitMessage("updated");
+ await expectCached(extension, [
+ { contexts: ["all"], id: "first", title: "first" },
+ { contexts: ["all"], id: "second", title: "second" },
+ {
+ contexts: ["all"],
+ id: "third",
+ parentId: "first",
+ title: "third",
+ },
+ { contexts: ["all"], id: "fourth", title: "fourth" },
+ ]);
+
+ extension.sendMessage("update", {
+ id: "first",
+ parentId: "second",
+ });
+ await extension.awaitMessage("updated");
+ await expectCached(extension, [
+ { contexts: ["all"], id: "second", title: "second" },
+ { contexts: ["all"], id: "fourth", title: "fourth" },
+ {
+ contexts: ["all"],
+ id: "first",
+ title: "first",
+ parentId: "second",
+ },
+ {
+ contexts: ["all"],
+ id: "third",
+ parentId: "first",
+ title: "third",
+ },
+ ]);
+
+ await AddonTestUtils.promiseShutdownManager();
+ // We need to attach an event listener before the
+ // startup event is emitted. Fortunately, we
+ // emit via Management before emitting on extension.
+ let promiseMenus;
+ Management.once("startup", (kind, ext) => {
+ info(`management ${kind} ${ext.id}`);
+ promiseMenus = promiseExtensionEvent(
+ { extension: ext },
+ "webext-menus-created"
+ );
+ });
+ await AddonTestUtils.promiseStartupManager();
+ await extension.awaitStartup();
+ await extension.wakeupBackground();
+
+ await expectCached(extension, [
+ { contexts: ["all"], id: "second", title: "second" },
+ { contexts: ["all"], id: "fourth", title: "fourth" },
+ {
+ contexts: ["all"],
+ id: "first",
+ title: "first",
+ parentId: "second",
+ },
+ {
+ contexts: ["all"],
+ id: "third",
+ parentId: "first",
+ title: "third",
+ },
+ ]);
+ // validate nesting
+ let menus = await promiseMenus;
+ equal(menus.get("first").parentId, "second", "menuitem parent is correct");
+ equal(
+ menus.get("second").children.length,
+ 1,
+ "menuitem parent has correct number of children"
+ );
+ equal(
+ menus.get("second").root.children.length,
+ 2, // second and forth
+ "menuitem root has correct number of children"
+ );
+
+ extension.sendMessage("remove", {
+ id: "second",
+ });
+ await extension.awaitMessage("updated");
+ await expectCached(extension, [
+ { contexts: ["all"], id: "fourth", title: "fourth" },
+ ]);
+
+ extension.sendMessage("removeAll");
+ await extension.awaitMessage("updated");
+ await expectCached(extension, []);
+
+ await extension.unload();
+});
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..0a2a9dcd49
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_normandyAddonStudy.js
@@ -0,0 +1,243 @@
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
+});
+
+const { AddonStudies } = ChromeUtils.importESModule(
+ "resource://normandy/lib/AddonStudies.sys.mjs"
+);
+const { NormandyTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/NormandyTestUtils.sys.mjs"
+);
+const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+var { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+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: {
+ browser_specific_settings: {
+ 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_normandyAddonStudy_temporary_without_privilege() {
+ let extension = ExtensionTestUtils.loadExtension({
+ temporarilyInstalled: true,
+ isPrivileged: false,
+ manifest: {
+ permissions: ["normandyAddonStudy"],
+ },
+ });
+ ExtensionTestUtils.failOnSchemaWarnings(false);
+ let { messages } = await promiseConsoleOutput(async () => {
+ await Assert.rejects(
+ extension.startup(),
+ /Using the privileged permission/,
+ "Startup failed with privileged permission"
+ );
+ });
+ ExtensionTestUtils.failOnSchemaWarnings(true);
+ AddonTestUtils.checkMessages(
+ messages,
+ {
+ expected: [
+ {
+ message:
+ /Using the privileged permission 'normandyAddonStudy' requires a privileged add-on/,
+ },
+ ],
+ },
+ true
+ );
+});
+
+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");
+
+ Assert.strictEqual(
+ clientMetadata.updateChannel,
+ Services.appinfo.defaultUpdateChannel,
+ "clientMetadata contains correct updateChannel"
+ );
+
+ Assert.strictEqual(
+ 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..bd462ec9b6
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_pageAction_shutdown.js
@@ -0,0 +1,81 @@
+/* -*- 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.defineESModuleGetters(this, {
+ PageActions: "resource:///modules/PageActions.sys.mjs",
+});
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+const { createAppInfo, promiseShutdownManager, promiseStartupManager } =
+ AddonTestUtils;
+
+AddonTestUtils.init(this);
+AddonTestUtils.overrideCertDB();
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "58");
+
+// This is copied and pasted from ext-browser.js and used in ext-pageAction.js.
+// It's used as the PageActions action ID.
+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();
+
+ action = PageActions.actionForID(actionID);
+ Assert.equal(action.pinnedToUrlbar, true);
+
+ // 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..8c713191cc
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_pkcs11_management.js
@@ -0,0 +1,300 @@
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ MockRegistry: "resource://testing-common/MockRegistry.sys.mjs",
+ ctypes: "resource://gre/modules/ctypes.sys.mjs",
+});
+
+do_get_profile();
+
+let tmpDir;
+let baseDir;
+let slug =
+ AppConstants.platform === "linux" ? "pkcs11-modules" : "PKCS11Modules";
+
+add_task(async function setupTest() {
+ tmpDir = await IOUtils.createUniqueDirectory(
+ Services.dirsvc.get("TmpD", Ci.nsIFile).path,
+ "PKCS11"
+ );
+
+ baseDir = PathUtils.join(tmpDir, slug);
+ await IOUtils.makeDirectory(baseDir);
+});
+
+registerCleanupFunction(async () => {
+ await IOUtils.remove(tmpDir, { recursive: true });
+});
+
+const testmodule = PathUtils.join(
+ PathUtils.parent(Services.dirsvc.get("CurWorkD", Ci.nsIFile).path, 5),
+ "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 = PathUtils.join(baseDir, `${module.name}.json`);
+ await IOUtils.writeJSON(manifestPath, manifest);
+
+ return manifestPath;
+ }
+
+ switch (AppConstants.platform) {
+ case "macosx":
+ case "linux":
+ let dirProvider = {
+ getFile(property) {
+ if (
+ property == "XREUserNativeManifests" ||
+ property == "XRESysNativeManifests"
+ ) {
+ return new FileUtils.File(tmpDir);
+ }
+ 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) {
+ 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 {
+ const { os } = await browser.runtime.getPlatformInfo();
+ if (os !== "win") {
+ // Expect this call to not throw (explicitly cover regression fixed in Bug 1759162).
+ let isInstalledNonAbsolute = await browser.pkcs11.isModuleInstalled(
+ "testmoduleNonAbsolutePath"
+ );
+ browser.test.assertFalse(
+ isInstalledNonAbsolute,
+ "PKCS#11 module with non absolute path expected to not be installed"
+ );
+ }
+ 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"
+ );
+ await browser.test.assertRejects(
+ browser.pkcs11.installModule("ipcclientcerts", 0),
+ /No such PKCS#11 module ipcclientcerts/,
+ "installModule should not work on the built-in ipcclientcerts module"
+ );
+ await browser.test.assertRejects(
+ browser.pkcs11.uninstallModule("ipcclientcerts"),
+ /No such PKCS#11 module ipcclientcerts/,
+ "uninstallModule should not work on the built-in ipcclientcerts module"
+ );
+ await browser.test.assertRejects(
+ browser.pkcs11.isModuleInstalled("ipcclientcerts"),
+ /No such PKCS#11 module ipcclientcerts/,
+ "isModuleLoaded should not work on the built-in ipcclientcerts module"
+ );
+ await browser.test.assertRejects(
+ browser.pkcs11.getModuleSlots("ipcclientcerts"),
+ /No such PKCS#11 module ipcclientcerts/,
+ "getModuleSlots should not work on the built-in ipcclientcerts 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: "testmoduleNonAbsolutePath",
+ description: "PKCS#11 Test Module",
+ path: ctypes.libraryName("pkcs11testmodule"),
+ 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: PathUtils.join(
+ Services.dirsvc.get("CurWorkD", Ci.nsIFile).path,
+ ctypes.libraryName("nssckbi")
+ ),
+ id: "pkcs11@tests.mozilla.org",
+ },
+ {
+ name: "osclientcerts",
+ description: "OS Client Cert Module",
+ path: PathUtils.join(libDir.path, ctypes.libraryName("osclientcerts")),
+ id: "pkcs11@tests.mozilla.org",
+ },
+ ]);
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["pkcs11"],
+ browser_specific_settings: { 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..dd24be3aff
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_settings_overrides_defaults.js
@@ -0,0 +1,263 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+const { SearchTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/SearchTestUtils.sys.mjs"
+);
+
+const { SearchUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/SearchUtils.sys.mjs"
+);
+
+const { RemoteSettings } = ChromeUtils.importESModule(
+ "resource://services-settings/remote-settings.sys.mjs"
+);
+
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+
+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();
+ });
+});
+
+function assertEngineParameters({
+ name,
+ searchURL,
+ suggestionURL,
+ messageSnippet,
+}) {
+ let engine = Services.search.getEngineByName(name);
+ Assert.ok(engine, `Should have found ${name}`);
+
+ Assert.equal(
+ engine.getSubmission("{searchTerms}").uri.spec,
+ encodeURI(searchURL),
+ `Should have ${messageSnippet} the suggestion url.`
+ );
+ Assert.equal(
+ engine.getSubmission("{searchTerms}", URLTYPE_SUGGEST_JSON)?.uri.spec,
+ suggestionURL ? encodeURI(suggestionURL) : suggestionURL,
+ `Should ${messageSnippet} the submission URL.`
+ );
+}
+
+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."
+ );
+
+ assertEngineParameters({
+ name: "MozParamsTest2",
+ searchURL: "https://example.com/2/?q={searchTerms}&simple2=5",
+ messageSnippet: "left unchanged",
+ });
+
+ 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: {
+ browser_specific_settings: {
+ 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",
+ });
+
+ info("startup");
+
+ await ext1.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(ext1);
+
+ Assert.equal(
+ Services.search.defaultEngine.name,
+ "MozParamsTest2",
+ "Should have switched the default engine."
+ );
+ assertEngineParameters({
+ name: "MozParamsTest2",
+ searchURL: kSearchEngineURL,
+ suggestionURL: `${kSuggestURL}?${kSuggestURLParams}`,
+ messageSnippet: "changed",
+ });
+
+ info("disable");
+
+ let promiseDefaultBrowserChange = SearchTestUtils.promiseSearchNotification(
+ "engine-default",
+ "browser-search-engine-modified"
+ );
+ await ext1.addon.disable();
+ await promiseDefaultBrowserChange;
+
+ Assert.equal(
+ Services.search.defaultEngine.name,
+ "MozParamsTest",
+ "Should have reverted to the original default engine."
+ );
+ assertEngineParameters({
+ name: "MozParamsTest2",
+ searchURL: "https://example.com/2/?q={searchTerms}&simple2=5",
+ messageSnippet: "reverted",
+ });
+
+ info("enable");
+
+ promiseDefaultBrowserChange = SearchTestUtils.promiseSearchNotification(
+ "engine-default",
+ "browser-search-engine-modified"
+ );
+ await ext1.addon.enable();
+ await promiseDefaultBrowserChange;
+
+ Assert.equal(
+ Services.search.defaultEngine.name,
+ "MozParamsTest2",
+ "Should have switched the default engine."
+ );
+
+ assertEngineParameters({
+ name: "MozParamsTest2",
+ searchURL: kSearchEngineURL,
+ suggestionURL: `${kSuggestURL}?${kSuggestURLParams}`,
+ messageSnippet: "changed",
+ });
+
+ info("unload");
+
+ 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."
+ );
+
+ assertEngineParameters({
+ name: "MozParamsTest2",
+ searchURL: "https://example.com/2/?q={searchTerms}&simple2=5",
+ messageSnippet: "reverted",
+ });
+ sinon.restore();
+});
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..10fed4d36b
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_settings_overrides_search.js
@@ -0,0 +1,597 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+const { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+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"
+);
+
+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.getIconURL(),
+ baseURI.resolve("foo.ico"),
+ "16x16 icon path matches"
+ );
+ equal(
+ engine.getIconURL(16),
+ baseURI.resolve("foo.ico"),
+ "16x16 icon path matches"
+ );
+ // TODO: Bug 1871036 - Differently sized icons are currently incorrectly
+ // handled for add-ons.
+ // equal(
+ // engine.getIconURL(32),
+ // baseURI.resolve("foo32.ico"),
+ // "32x32 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}",
+ },
+ },
+ browser_specific_settings: {
+ 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,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ 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}",
+ },
+ },
+ browser_specific_settings: {
+ 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();
+});
+
+add_task(async function test_search_favicon_mv3() {
+ Services.prefs.setBoolPref("extensions.manifestV3.enabled", true);
+ let normalized = await ExtensionTestUtils.normalizeManifest({
+ manifest_version: 3,
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "HTTP Icon in MV3",
+ search_url: "https://example.org/",
+ favicon_url: "https://example.org/icon.png",
+ },
+ },
+ });
+ Assert.ok(
+ normalized.error.endsWith("must be a relative URL"),
+ "Should have an error"
+ );
+ normalized = await ExtensionTestUtils.normalizeManifest({
+ manifest_version: 3,
+ chrome_settings_overrides: {
+ search_provider: {
+ name: "HTTP Icon in MV3",
+ search_url: "https://example.org/",
+ favicon_url: "/icon.png",
+ },
+ },
+ });
+ Assert.ok(!normalized.error, "Should not have an error");
+});
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..3248c5cefa
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_settings_overrides_search_mozParam.js
@@ -0,0 +1,239 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+const { SearchTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/SearchTestUtils.sys.mjs"
+);
+const { NimbusFeatures } = ChromeUtils.importESModule(
+ "resource://nimbus/ExperimentAPI.sys.mjs"
+);
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+
+AddonTestUtils.init(this);
+AddonTestUtils.overrideCertDB();
+AddonTestUtils.createAppInfo(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "42",
+ "42"
+);
+
+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() {
+ let readyStub = sinon.stub(NimbusFeatures.search, "ready").resolves();
+ let updateStub = sinon.stub(NimbusFeatures.search, "onUpdate");
+ 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();
+ readyStub.restore();
+ updateStub.restore();
+ });
+});
+
+/* 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"
+ );
+ }
+
+ defaultBranch.setCharPref("param.code", "");
+});
+
+add_task(async function test_nimbus_params() {
+ let sandbox = sinon.createSandbox();
+ let stub = sandbox.stub(NimbusFeatures.search, "getVariable");
+ // These values should match the nimbusParams below and the data/test/manifest.json
+ // search engine configuration
+ stub.withArgs("extraParams").returns([
+ {
+ key: "nimbus-key-1",
+ value: "nimbus-value-1",
+ },
+ {
+ key: "nimbus-key-2",
+ value: "nimbus-value-2",
+ },
+ ]);
+
+ Assert.ok(
+ NimbusFeatures.search.onUpdate.called,
+ "Called to initialize the cache"
+ );
+
+ // Populate the cache with the `getVariable` mock values
+ NimbusFeatures.search.onUpdate.firstCall.args[0]();
+
+ let engine = Services.search.getEngineByName("MozParamsTest");
+
+ // 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 nimbusParams = [
+ { name: "experimenter-1", condition: "pref", pref: "nimbus-key-1" },
+ { name: "experimenter-2", condition: "pref", pref: "nimbus-key-2" },
+ ];
+ const experimentCache = {
+ "nimbus-key-1": "nimbus-value-1",
+ "nimbus-key-2": "nimbus-value-2",
+ };
+
+ let extraParams = [];
+ for (let p of params) {
+ 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 if (p.condition !== "pref") {
+ // Ignoring pref parameters
+ extraParams.push(`${p.name}=${p.value}`);
+ }
+ }
+ for (let p of nimbusParams) {
+ if (p.condition == "pref") {
+ extraParams.push(`${p.name}=${experimentCache[p.pref]}`);
+ }
+ }
+ 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"
+ );
+ }
+
+ sandbox.restore();
+});
+
+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: {
+ browser_specific_settings: {
+ 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..851efd6b2a
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_settings_overrides_shutdown.js
@@ -0,0 +1,109 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+
+"use strict";
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+// Lazily import ExtensionParent to allow AddonTestUtils.createAppInfo to
+// override Services.appinfo.
+ChromeUtils.defineESModuleGetters(this, {
+ ExtensionParent: "resource://gre/modules/ExtensionParent.sys.mjs",
+});
+
+AddonTestUtils.init(this);
+AddonTestUtils.overrideCertDB();
+AddonTestUtils.createAppInfo(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "42",
+ "42"
+);
+
+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;
+ Services.search.promiseInitialized.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..2f0d36f6e8
--- /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.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+const { AddonManager } = ChromeUtils.importESModule(
+ "resource://gre/modules/AddonManager.sys.mjs"
+);
+
+const { AboutNewTab } = ChromeUtils.importESModule(
+ "resource:///modules/AboutNewTab.sys.mjs"
+);
+
+// Lazy load to avoid having Services.appinfo cached first.
+ChromeUtils.defineESModuleGetters(this, {
+ ExtensionParent: "resource://gre/modules/ExtensionParent.sys.mjs",
+});
+
+const { HomePage } = ChromeUtils.importESModule(
+ "resource:///modules/HomePage.sys.mjs"
+);
+
+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",
+ browser_specific_settings: { 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",
+ browser_specific_settings: { 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..8064ade1e8
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_topSites.js
@@ -0,0 +1,293 @@
+"use strict";
+
+const { PlacesUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/PlacesUtils.sys.mjs"
+);
+const { NewTabUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/NewTabUtils.sys.mjs"
+);
+const { PlacesTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PlacesTestUtils.sys.mjs"
+);
+
+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();
+
+ async function setVisit(visit) {
+ for (let j = 0; j < numVisits; ++j) {
+ visitDate -= 1000;
+ visit.visits.push({ date: new Date(visitDate) });
+ }
+ visits.push(visit);
+ await PlacesUtils.history.insert(visit);
+ }
+ // Stick a couple sites into history.
+ for (let i = 0; i < 2; ++i) {
+ await setVisit({
+ url: `http://example${i}.com/`,
+ title: `visit${i}`,
+ visits: [],
+ });
+ await setVisit({
+ url: `http://www.example${i}.com/foobar`,
+ title: `visit${i}-www`,
+ visits: [],
+ });
+ }
+ NewTabUtils.init();
+
+ // 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 = await browser.topSites.get(options);
+ 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 = await browser.topSites.get(options);
+ 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..9ea6c4eea6
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_url_overrides_newtab.js
@@ -0,0 +1,340 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
+ Management: "resource://gre/modules/Extension.sys.mjs",
+});
+
+const { AboutNewTab } = ChromeUtils.importESModule(
+ "resource:///modules/AboutNewTab.sys.mjs"
+);
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+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.browser_specific_settings = { gecko: { id: EXT_2_ID } };
+ let ext2 = ExtensionTestUtils.loadExtension(extObj);
+
+ extObj.manifest.chrome_url_overrides = { newtab: NEWTAB_URI_3 };
+ extObj.manifest.browser_specific_settings.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: {
+ browser_specific_settings: {
+ 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: {
+ browser_specific_settings: {
+ 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..17ee81e5ef
--- /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.importESModule(
+ "resource:///modules/AboutNewTab.sys.mjs"
+);
+
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+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",
+ browser_specific_settings: {
+ 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",
+ browser_specific_settings: {
+ 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/xpcshell.toml b/browser/components/extensions/test/xpcshell/xpcshell.toml
new file mode 100644
index 0000000000..e98f696264
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/xpcshell.toml
@@ -0,0 +1,69 @@
+[DEFAULT]
+skip-if = ["os == 'android'"] # bug 1730213
+head = "head.js"
+firefox-appdir = "browser"
+tags = "webextensions condprof"
+dupe-manifest = ""
+
+["test_ext_bookmarks.js"]
+skip-if = ["condprof"] # Bug 1769184 - by design for now
+
+["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"]
+run-sequentially = "very high failure rate in parallel"
+
+["test_ext_manifest_omnibox.js"]
+
+["test_ext_manifest_permissions.js"]
+
+["test_ext_menu_caller.js"]
+
+["test_ext_menu_startup.js"]
+
+["test_ext_normandyAddonStudy.js"]
+
+["test_ext_pageAction_shutdown.js"]
+
+["test_ext_pkcs11_management.js"]
+
+["test_ext_settings_overrides_defaults.js"]
+skip-if = ["condprof"] # Bug 1776135 - by design, modifies search settings at start of test
+support-files = [
+ "data/test/manifest.json",
+ "data/test2/manifest.json",
+]
+
+["test_ext_settings_overrides_search.js"]
+
+["test_ext_settings_overrides_search_mozParam.js"]
+skip-if = ["condprof"] # Bug 1776652
+support-files = ["data/test/manifest.json"]
+
+["test_ext_settings_overrides_shutdown.js"]
+
+["test_ext_settings_validate.js"]
+
+["test_ext_topSites.js"]
+skip-if = ["condprof"] # Bug 1769184 - by design for now
+
+["test_ext_url_overrides_newtab.js"]
+
+["test_ext_url_overrides_newtab_update.js"]