summaryrefslogtreecommitdiffstats
path: root/browser/components/urlbar/tests/ext
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/urlbar/tests/ext')
-rw-r--r--browser/components/urlbar/tests/ext/api.js260
-rw-r--r--browser/components/urlbar/tests/ext/browser/.eslintrc.js7
-rw-r--r--browser/components/urlbar/tests/ext/browser/browser.ini18
-rw-r--r--browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_attributionURL.js16
-rw-r--r--browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_clearInput.js31
-rw-r--r--browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_dynamicResult.js137
-rw-r--r--browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_engagementTelemetry.js18
-rw-r--r--browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_extensionTimeout.js16
-rw-r--r--browser/components/urlbar/tests/ext/browser/dynamicResult.css36
-rw-r--r--browser/components/urlbar/tests/ext/browser/head.js253
-rw-r--r--browser/components/urlbar/tests/ext/schema.json113
11 files changed, 905 insertions, 0 deletions
diff --git a/browser/components/urlbar/tests/ext/api.js b/browser/components/urlbar/tests/ext/api.js
new file mode 100644
index 0000000000..51a2757b5a
--- /dev/null
+++ b/browser/components/urlbar/tests/ext/api.js
@@ -0,0 +1,260 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* global ExtensionAPI, ExtensionCommon */
+
+"use strict";
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ Preferences: "resource://gre/modules/Preferences.sys.mjs",
+ UrlbarProviderExtension:
+ "resource:///modules/UrlbarProviderExtension.sys.mjs",
+ UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs",
+ UrlbarView: "resource:///modules/UrlbarView.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(
+ this,
+ "defaultPreferences",
+ () => new Preferences({ defaultBranch: true })
+);
+
+let { EventManager } = ExtensionCommon;
+
+this.experiments_urlbar = class extends ExtensionAPI {
+ getAPI(context) {
+ return {
+ experiments: {
+ urlbar: {
+ addDynamicResultType: (name, type) => {
+ this._addDynamicResultType(name, type);
+ },
+
+ addDynamicViewTemplate: (name, viewTemplate) => {
+ this._addDynamicViewTemplate(name, viewTemplate);
+ },
+
+ attributionURL: this._getDefaultSettingsAPI(
+ "browser.partnerlink.attributionURL"
+ ),
+
+ clearInput() {
+ let window = BrowserWindowTracker.getTopWindow();
+ window.gURLBar.value = "";
+ window.gURLBar.setPageProxyState("invalid");
+ },
+
+ engagementTelemetry: this._getDefaultSettingsAPI(
+ "browser.urlbar.eventTelemetry.enabled"
+ ),
+
+ extensionTimeout: this._getDefaultSettingsAPI(
+ "browser.urlbar.extension.timeout"
+ ),
+
+ onViewUpdateRequested: new EventManager({
+ context,
+ name: "experiments.urlbar.onViewUpdateRequested",
+ register: (fire, providerName) => {
+ let provider = UrlbarProviderExtension.getOrCreate(providerName);
+ provider.setEventListener(
+ "getViewUpdate",
+ (result, idsByName) => {
+ return fire.async(result.payload, idsByName).catch(error => {
+ throw context.normalizeError(error);
+ });
+ }
+ );
+ return () => provider.setEventListener("getViewUpdate", null);
+ },
+ }).api(),
+ },
+ },
+ };
+ }
+
+ onShutdown() {
+ // Reset the default prefs. This is necessary because
+ // ExtensionPreferencesManager doesn't properly reset prefs set on the
+ // default branch. See bug 1586543, bug 1578513, bug 1578508.
+ if (this._initialDefaultPrefs) {
+ for (let [pref, value] of this._initialDefaultPrefs.entries()) {
+ defaultPreferences.set(pref, value);
+ }
+ }
+
+ this._removeDynamicViewTemplates();
+ this._removeDynamicResultTypes();
+ }
+
+ _getDefaultSettingsAPI(pref) {
+ return {
+ get: details => {
+ return {
+ value: Preferences.get(pref),
+
+ // Nothing actually uses this, but on debug builds there are extra
+ // checks enabled in Schema.jsm that fail if it's not present. The
+ // value doesn't matter.
+ levelOfControl: "controllable_by_this_extension",
+ };
+ },
+ set: details => {
+ if (!this._initialDefaultPrefs) {
+ this._initialDefaultPrefs = new Map();
+ }
+ if (!this._initialDefaultPrefs.has(pref)) {
+ this._initialDefaultPrefs.set(pref, defaultPreferences.get(pref));
+ }
+ defaultPreferences.set(pref, details.value);
+ return true;
+ },
+ clear: details => {
+ if (this._initialDefaultPrefs && this._initialDefaultPrefs.has(pref)) {
+ defaultPreferences.set(pref, this._initialDefaultPrefs.get(pref));
+ return true;
+ }
+ return false;
+ },
+ };
+ }
+
+ // We use the following four properties as bookkeeping to keep track of
+ // dynamic result types and view templates registered by extensions so that
+ // they can be properly removed on extension shutdown.
+
+ // Names of dynamic result types added by this extension.
+ _dynamicResultTypeNames = new Set();
+
+ // Names of dynamic result type view templates added by this extension.
+ _dynamicViewTemplateNames = new Set();
+
+ // Maps dynamic result type names to Sets of IDs of extensions that have
+ // registered those types.
+ static extIDsByDynamicResultTypeName = new Map();
+
+ // Maps dynamic result type view template names to Sets of IDs of extensions
+ // that have registered those view templates.
+ static extIDsByDynamicViewTemplateName = new Map();
+
+ /**
+ * Adds a dynamic result type and includes it in our bookkeeping. See
+ * UrlbarResult.addDynamicResultType().
+ *
+ * @param {string} name
+ * The name of the dynamic result type.
+ * @param {object} type
+ * The type.
+ */
+ _addDynamicResultType(name, type) {
+ this._dynamicResultTypeNames.add(name);
+ this._addExtIDToDynamicResultTypeMap(
+ experiments_urlbar.extIDsByDynamicResultTypeName,
+ name
+ );
+ UrlbarResult.addDynamicResultType(name, type);
+ }
+
+ /**
+ * Removes all dynamic result types added by the extension.
+ */
+ _removeDynamicResultTypes() {
+ for (let name of this._dynamicResultTypeNames) {
+ let allRemoved = this._removeExtIDFromDynamicResultTypeMap(
+ experiments_urlbar.extIDsByDynamicResultTypeName,
+ name
+ );
+ if (allRemoved) {
+ UrlbarResult.removeDynamicResultType(name);
+ }
+ }
+ }
+
+ /**
+ * Adds a dynamic result type view template and includes it in our
+ * bookkeeping. See UrlbarView.addDynamicViewTemplate().
+ *
+ * @param {string} name
+ * The view template will be registered for the dynamic result type with
+ * this name.
+ * @param {object} viewTemplate
+ * The view template.
+ */
+ _addDynamicViewTemplate(name, viewTemplate) {
+ this._dynamicViewTemplateNames.add(name);
+ this._addExtIDToDynamicResultTypeMap(
+ experiments_urlbar.extIDsByDynamicViewTemplateName,
+ name
+ );
+ if (viewTemplate.stylesheet) {
+ viewTemplate.stylesheet = this.extension.baseURI.resolve(
+ viewTemplate.stylesheet
+ );
+ }
+ UrlbarView.addDynamicViewTemplate(name, viewTemplate);
+ }
+
+ /**
+ * Removes all dynamic result type view templates added by the extension.
+ */
+ _removeDynamicViewTemplates() {
+ for (let name of this._dynamicViewTemplateNames) {
+ let allRemoved = this._removeExtIDFromDynamicResultTypeMap(
+ experiments_urlbar.extIDsByDynamicViewTemplateName,
+ name
+ );
+ if (allRemoved) {
+ UrlbarView.removeDynamicViewTemplate(name);
+ }
+ }
+ }
+
+ /**
+ * Adds a dynamic result type name and this extension's ID to a bookkeeping
+ * map.
+ *
+ * @param {Map} map
+ * Either extIDsByDynamicResultTypeName or extIDsByDynamicViewTemplateName.
+ * @param {string} dynamicTypeName
+ * The dynamic result type name.
+ */
+ _addExtIDToDynamicResultTypeMap(map, dynamicTypeName) {
+ let extIDs = map.get(dynamicTypeName);
+ if (!extIDs) {
+ extIDs = new Set();
+ map.set(dynamicTypeName, extIDs);
+ }
+ extIDs.add(this.extension.id);
+ }
+
+ /**
+ * Removes a dynamic result type name and this extension's ID from a
+ * bookkeeping map.
+ *
+ * @param {Map} map
+ * Either extIDsByDynamicResultTypeName or extIDsByDynamicViewTemplateName.
+ * @param {string} dynamicTypeName
+ * The dynamic result type name.
+ * @returns {boolean}
+ * True if no other extension IDs are in the map under the same
+ * dynamicTypeName, and false otherwise.
+ */
+ _removeExtIDFromDynamicResultTypeMap(map, dynamicTypeName) {
+ let extIDs = map.get(dynamicTypeName);
+ extIDs.delete(this.extension.id);
+ if (!extIDs.size) {
+ map.delete(dynamicTypeName);
+ return true;
+ }
+ return false;
+ }
+};
diff --git a/browser/components/urlbar/tests/ext/browser/.eslintrc.js b/browser/components/urlbar/tests/ext/browser/.eslintrc.js
new file mode 100644
index 0000000000..e57058ecb1
--- /dev/null
+++ b/browser/components/urlbar/tests/ext/browser/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ env: {
+ webextensions: true,
+ },
+};
diff --git a/browser/components/urlbar/tests/ext/browser/browser.ini b/browser/components/urlbar/tests/ext/browser/browser.ini
new file mode 100644
index 0000000000..416fc52eb3
--- /dev/null
+++ b/browser/components/urlbar/tests/ext/browser/browser.ini
@@ -0,0 +1,18 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+[DEFAULT]
+support-files =
+ ../../browser/head-common.js
+ ../api.js
+ ../schema.json
+ head.js
+
+[browser_ext_urlbar_attributionURL.js]
+[browser_ext_urlbar_clearInput.js]
+[browser_ext_urlbar_dynamicResult.js]
+support-files =
+ dynamicResult.css
+[browser_ext_urlbar_engagementTelemetry.js]
+[browser_ext_urlbar_extensionTimeout.js]
diff --git a/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_attributionURL.js b/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_attributionURL.js
new file mode 100644
index 0000000000..a5bccc8eba
--- /dev/null
+++ b/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_attributionURL.js
@@ -0,0 +1,16 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* global browser */
+
+// This tests the browser.experiments.urlbar.engagementTelemetry WebExtension
+// Experiment API.
+
+"use strict";
+
+add_settings_tasks("browser.partnerlink.attributionURL", "string", () => {
+ browser.test.onMessage.addListener(async (method, arg) => {
+ let result = await browser.experiments.urlbar.attributionURL[method](arg);
+ browser.test.sendMessage("done", result);
+ });
+});
diff --git a/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_clearInput.js b/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_clearInput.js
new file mode 100644
index 0000000000..afeff3b8a1
--- /dev/null
+++ b/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_clearInput.js
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* global browser */
+
+// This tests the browser.experiments.urlbar.clearInput WebExtension Experiment
+// API.
+
+"use strict";
+
+add_task(async function test() {
+ // Load a page so that pageproxystate is valid. When the extension calls
+ // clearInput, the pageproxystate should become invalid.
+ await BrowserTestUtils.withNewTab("http://example.com/", async () => {
+ Assert.notEqual(gURLBar.value, "", "Input is not empty");
+ Assert.equal(gURLBar.getAttribute("pageproxystate"), "valid");
+
+ let ext = await loadExtension({
+ background: async () => {
+ await browser.experiments.urlbar.clearInput();
+ browser.test.sendMessage("done");
+ },
+ });
+ await ext.awaitMessage("done");
+
+ Assert.equal(gURLBar.value, "", "Input is empty");
+ Assert.equal(gURLBar.getAttribute("pageproxystate"), "invalid");
+
+ await ext.unload();
+ });
+});
diff --git a/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_dynamicResult.js b/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_dynamicResult.js
new file mode 100644
index 0000000000..a710d8949d
--- /dev/null
+++ b/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_dynamicResult.js
@@ -0,0 +1,137 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* global browser */
+
+// This tests dynamic results using the WebExtension Experiment API.
+
+"use strict";
+
+add_task(async function test() {
+ let ext = await loadExtension({
+ extraFiles: {
+ "dynamicResult.css": await (
+ await fetch("file://" + getTestFilePath("dynamicResult.css"))
+ ).text(),
+ },
+ background: async () => {
+ browser.experiments.urlbar.addDynamicResultType("testDynamicType");
+ browser.experiments.urlbar.addDynamicViewTemplate("testDynamicType", {
+ stylesheet: "dynamicResult.css",
+ children: [
+ {
+ name: "text",
+ tag: "span",
+ },
+ {
+ name: "button",
+ tag: "span",
+ attributes: {
+ role: "button",
+ },
+ },
+ ],
+ });
+ browser.urlbar.onBehaviorRequested.addListener(query => {
+ return "restricting";
+ }, "test");
+ browser.urlbar.onResultsRequested.addListener(query => {
+ return [
+ {
+ type: "dynamic",
+ source: "local",
+ heuristic: true,
+ payload: {
+ dynamicType: "testDynamicType",
+ },
+ },
+ ];
+ }, "test");
+ browser.experiments.urlbar.onViewUpdateRequested.addListener(payload => {
+ return {
+ text: {
+ textContent: "This is a dynamic result.",
+ },
+ button: {
+ textContent: "Click Me",
+ },
+ };
+ }, "test");
+ browser.urlbar.onResultPicked.addListener((payload, elementName) => {
+ browser.test.sendMessage("onResultPicked", [payload, elementName]);
+ }, "test");
+ },
+ });
+
+ // Wait for the provider and dynamic type to be registered before continuing.
+ await TestUtils.waitForCondition(
+ () =>
+ UrlbarProvidersManager.getProvider("test") &&
+ UrlbarResult.getDynamicResultType("testDynamicType"),
+ "Waiting for provider and dynamic type to be registered"
+ );
+ Assert.ok(
+ UrlbarProvidersManager.getProvider("test"),
+ "Provider should be registered"
+ );
+ Assert.ok(
+ UrlbarResult.getDynamicResultType("testDynamicType"),
+ "Dynamic type should be registered"
+ );
+
+ // Do a search.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ waitForFocus: SimpleTest.waitForFocus,
+ });
+
+ // Get the row.
+ let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0);
+ Assert.equal(
+ row.result.type,
+ UrlbarUtils.RESULT_TYPE.DYNAMIC,
+ "row.result.type"
+ );
+ Assert.equal(
+ row.getAttribute("dynamicType"),
+ "testDynamicType",
+ "row[dynamicType]"
+ );
+
+ let text = row.querySelector(".urlbarView-dynamic-testDynamicType-text");
+
+ // The view's call to provider.getViewUpdate is async, so we need to make sure
+ // the update has been applied before continuing to avoid intermittent
+ // failures.
+ await TestUtils.waitForCondition(
+ () => text.textContent == "This is a dynamic result."
+ );
+
+ // Check the elements.
+ Assert.equal(
+ text.textContent,
+ "This is a dynamic result.",
+ "text.textContent"
+ );
+ let button = row.querySelector(".urlbarView-dynamic-testDynamicType-button");
+ Assert.equal(button.textContent, "Click Me", "button.textContent");
+
+ // The result's button should be selected since the result is the heuristic.
+ Assert.equal(
+ UrlbarTestUtils.getSelectedElement(window),
+ button,
+ "Button should be selected"
+ );
+
+ // Pick the button.
+ let pickPromise = ext.awaitMessage("onResultPicked");
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeKey("KEY_Enter")
+ );
+ let [payload, elementName] = await pickPromise;
+ Assert.equal(payload.dynamicType, "testDynamicType", "Picked payload");
+ Assert.equal(elementName, "button", "Picked element name");
+
+ await ext.unload();
+});
diff --git a/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_engagementTelemetry.js b/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_engagementTelemetry.js
new file mode 100644
index 0000000000..50ded14d4e
--- /dev/null
+++ b/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_engagementTelemetry.js
@@ -0,0 +1,18 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* global browser */
+
+// This tests the browser.experiments.urlbar.engagementTelemetry WebExtension
+// Experiment API.
+
+"use strict";
+
+add_settings_tasks("browser.urlbar.eventTelemetry.enabled", "boolean", () => {
+ browser.test.onMessage.addListener(async (method, arg) => {
+ let result = await browser.experiments.urlbar.engagementTelemetry[method](
+ arg
+ );
+ browser.test.sendMessage("done", result);
+ });
+});
diff --git a/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_extensionTimeout.js b/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_extensionTimeout.js
new file mode 100644
index 0000000000..de09ef263c
--- /dev/null
+++ b/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_extensionTimeout.js
@@ -0,0 +1,16 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* global browser */
+
+// This tests the browser.experiments.urlbar.engagementTelemetry WebExtension
+// Experiment API.
+
+"use strict";
+
+add_settings_tasks("browser.urlbar.extension.timeout", "number", () => {
+ browser.test.onMessage.addListener(async (method, arg) => {
+ let result = await browser.experiments.urlbar.extensionTimeout[method](arg);
+ browser.test.sendMessage("done", result);
+ });
+});
diff --git a/browser/components/urlbar/tests/ext/browser/dynamicResult.css b/browser/components/urlbar/tests/ext/browser/dynamicResult.css
new file mode 100644
index 0000000000..efd0c8c950
--- /dev/null
+++ b/browser/components/urlbar/tests/ext/browser/dynamicResult.css
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+.urlbarView-row[dynamicType=testDynamicType] > .urlbarView-row-inner {
+ display: flex;
+ align-items: center;
+ min-height: 32px;
+ width: 100%;
+}
+
+.urlbarView-dynamic-testDynamicType-text {
+ flex-grow: 1;
+ flex-shrink: 1;
+ padding: 10px;
+}
+
+.urlbarView-dynamic-testDynamicType-button {
+ min-height: 16px;
+ padding: 8px;
+ border: none;
+ border-radius: 2px;
+ font-size: 0.93em;
+ color: inherit;
+ background-color: var(--urlbarView-button-background);
+ min-width: 8.75em;
+ text-align: center;
+ flex-basis: initial;
+ flex-shrink: 0;
+ margin-inline-end: 10px;
+}
+
+.urlbarView-dynamic-testDynamicType-button[selected] {
+ color: white;
+ background-color: var(--urlbarView-primary-button-background);
+ box-shadow: 0 0 0 1px #0a84ff inset, 0 0 0 1px #0a84ff, 0 0 0 4px rgba(10, 132, 255, 0.3);
+}
diff --git a/browser/components/urlbar/tests/ext/browser/head.js b/browser/components/urlbar/tests/ext/browser/head.js
new file mode 100644
index 0000000000..b9cd89a470
--- /dev/null
+++ b/browser/components/urlbar/tests/ext/browser/head.js
@@ -0,0 +1,253 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * The files in this directory test the browser.urlbarExperiments WebExtension
+ * Experiment APIs, which are the WebExtension APIs we ship in our urlbar
+ * experiment extensions. Unlike the WebExtension APIs we ship in mozilla-
+ * central, which have continuous test coverage [1], our WebExtension Experiment
+ * APIs would not have continuous test coverage were it not for the fact that we
+ * copy and test them here. This is especially useful for APIs that are used in
+ * experiments that target multiple versions of Firefox, and for APIs that are
+ * reused in multiple experiments. See [2] and [3] for more info on
+ * experiments.
+ *
+ * [1] See browser/components/extensions/test
+ * [2] browser/components/urlbar/docs/experiments.rst
+ * [3] https://firefox-source-docs.mozilla.org/toolkit/components/extensions/webextensions/basics.html#webextensions-experiments
+ */
+
+"use strict";
+
+/* import-globals-from ../../browser/head-common.js */
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/components/urlbar/tests/ext/browser/head-common.js",
+ this
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ Preferences: "resource://gre/modules/Preferences.sys.mjs",
+});
+
+const SCHEMA_BASENAME = "schema.json";
+const SCRIPT_BASENAME = "api.js";
+
+const SCHEMA_PATH = getTestFilePath(SCHEMA_BASENAME);
+const SCRIPT_PATH = getTestFilePath(SCRIPT_BASENAME);
+
+let schemaSource;
+let scriptSource;
+
+add_setup(async function loadSource() {
+ schemaSource = await (await fetch("file://" + SCHEMA_PATH)).text();
+ scriptSource = await (await fetch("file://" + SCRIPT_PATH)).text();
+});
+
+/**
+ * Loads a mock extension with our browser.experiments.urlbar API and a
+ * background script. Be sure to call `await ext.unload()` when you're done
+ * with it.
+ *
+ * @param {object} options
+ * Options object
+ * @param {Function} options.background
+ * This function is serialized and becomes the background script.
+ * @param {object} [options.extraFiles]
+ * Extra files to load in the extension.
+ * @returns {object}
+ * The extension.
+ */
+async function loadExtension({ background, extraFiles = {} }) {
+ let ext = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["urlbar"],
+ experiment_apis: {
+ experiments_urlbar: {
+ schema: SCHEMA_BASENAME,
+ parent: {
+ scopes: ["addon_parent"],
+ paths: [["experiments", "urlbar"]],
+ script: SCRIPT_BASENAME,
+ },
+ },
+ },
+ },
+ files: {
+ [SCHEMA_BASENAME]: schemaSource,
+ [SCRIPT_BASENAME]: scriptSource,
+ ...extraFiles,
+ },
+ isPrivileged: true,
+ background,
+ });
+ await ext.startup();
+ return ext;
+}
+
+/**
+ * Tests toggling a preference value via an experiments.urlbar API.
+ *
+ * @param {string} prefName
+ * The name of the pref to be tested.
+ * @param {string} type
+ * The type of the pref being set. One of "string", "boolean", or "number".
+ * @param {Function} background
+ * Boilerplate function that returns the value from calling the
+ * browser.experiments.urlbar.prefName[method] APIs.
+ */
+function add_settings_tasks(prefName, type, background) {
+ let defaultPreferences = new Preferences({ defaultBranch: true });
+
+ let originalValue = defaultPreferences.get(prefName);
+ registerCleanupFunction(() => {
+ defaultPreferences.set(prefName, originalValue);
+ });
+
+ let firstValue, secondValue;
+ switch (type) {
+ case "string":
+ firstValue = "test value 1";
+ secondValue = "test value 2";
+ break;
+ case "number":
+ firstValue = 10;
+ secondValue = 100;
+ break;
+ case "boolean":
+ firstValue = false;
+ secondValue = true;
+ break;
+ default:
+ Assert.fail(
+ `"type" parameter must be one of "string", "number", or "boolean"`
+ );
+ }
+
+ add_task(async function get() {
+ let ext = await loadExtension({ background });
+
+ defaultPreferences.set(prefName, firstValue);
+ ext.sendMessage("get", {});
+ let result = await ext.awaitMessage("done");
+ Assert.strictEqual(result.value, firstValue);
+
+ defaultPreferences.set(prefName, secondValue);
+ ext.sendMessage("get", {});
+ result = await ext.awaitMessage("done");
+ Assert.strictEqual(result.value, secondValue);
+
+ await ext.unload();
+ });
+
+ add_task(async function set() {
+ let ext = await loadExtension({ background });
+
+ defaultPreferences.set(prefName, firstValue);
+ ext.sendMessage("set", { value: secondValue });
+ let result = await ext.awaitMessage("done");
+ Assert.strictEqual(result, true);
+ Assert.strictEqual(defaultPreferences.get(prefName), secondValue);
+
+ ext.sendMessage("set", { value: firstValue });
+ result = await ext.awaitMessage("done");
+ Assert.strictEqual(result, true);
+ Assert.strictEqual(defaultPreferences.get(prefName), firstValue);
+
+ await ext.unload();
+ });
+
+ add_task(async function clear() {
+ // no set()
+ defaultPreferences.set(prefName, firstValue);
+ let ext = await loadExtension({ background });
+ ext.sendMessage("clear", {});
+ let result = await ext.awaitMessage("done");
+ Assert.strictEqual(result, false);
+ Assert.strictEqual(defaultPreferences.get(prefName), firstValue);
+ await ext.unload();
+
+ // firstValue -> secondValue
+ defaultPreferences.set(prefName, firstValue);
+ ext = await loadExtension({ background });
+ ext.sendMessage("set", { value: secondValue });
+ await ext.awaitMessage("done");
+ ext.sendMessage("clear", {});
+ result = await ext.awaitMessage("done");
+ Assert.strictEqual(result, true);
+ Assert.strictEqual(defaultPreferences.get(prefName), firstValue);
+ await ext.unload();
+
+ // secondValue -> firstValue
+ defaultPreferences.set(prefName, secondValue);
+ ext = await loadExtension({ background });
+ ext.sendMessage("set", { value: firstValue });
+ await ext.awaitMessage("done");
+ ext.sendMessage("clear", {});
+ result = await ext.awaitMessage("done");
+ Assert.strictEqual(result, true);
+ Assert.strictEqual(defaultPreferences.get(prefName), secondValue);
+ await ext.unload();
+
+ // firstValue -> firstValue
+ defaultPreferences.set(prefName, firstValue);
+ ext = await loadExtension({ background });
+ ext.sendMessage("set", { value: firstValue });
+ await ext.awaitMessage("done");
+ ext.sendMessage("clear", {});
+ result = await ext.awaitMessage("done");
+ Assert.strictEqual(result, true);
+ Assert.strictEqual(defaultPreferences.get(prefName), firstValue);
+ await ext.unload();
+
+ // secondValue -> secondValue
+ defaultPreferences.set(prefName, secondValue);
+ ext = await loadExtension({ background });
+ ext.sendMessage("set", { value: secondValue });
+ await ext.awaitMessage("done");
+ ext.sendMessage("clear", {});
+ result = await ext.awaitMessage("done");
+ Assert.strictEqual(result, true);
+ Assert.strictEqual(defaultPreferences.get(prefName), secondValue);
+ await ext.unload();
+ });
+
+ add_task(async function shutdown() {
+ // no set()
+ defaultPreferences.set(prefName, firstValue);
+ let ext = await loadExtension({ background });
+ await ext.unload();
+ Assert.strictEqual(defaultPreferences.get(prefName), firstValue);
+
+ // firstValue -> secondValue
+ defaultPreferences.set(prefName, firstValue);
+ ext = await loadExtension({ background });
+ ext.sendMessage("set", { value: secondValue });
+ await ext.awaitMessage("done");
+ await ext.unload();
+ Assert.strictEqual(defaultPreferences.get(prefName), firstValue);
+
+ // secondValue -> firstValue
+ defaultPreferences.set(prefName, secondValue);
+ ext = await loadExtension({ background });
+ ext.sendMessage("set", { value: firstValue });
+ await ext.awaitMessage("done");
+ await ext.unload();
+ Assert.strictEqual(defaultPreferences.get(prefName), secondValue);
+
+ // firstValue -> firstValue
+ defaultPreferences.set(prefName, firstValue);
+ ext = await loadExtension({ background });
+ ext.sendMessage("set", { value: firstValue });
+ await ext.awaitMessage("done");
+ await ext.unload();
+ Assert.strictEqual(defaultPreferences.get(prefName), firstValue);
+
+ // secondValue -> secondValue
+ defaultPreferences.set(prefName, secondValue);
+ ext = await loadExtension({ background });
+ ext.sendMessage("set", { value: secondValue });
+ await ext.awaitMessage("done");
+ await ext.unload();
+ Assert.strictEqual(defaultPreferences.get(prefName), secondValue);
+ });
+}
diff --git a/browser/components/urlbar/tests/ext/schema.json b/browser/components/urlbar/tests/ext/schema.json
new file mode 100644
index 0000000000..26b0df5a34
--- /dev/null
+++ b/browser/components/urlbar/tests/ext/schema.json
@@ -0,0 +1,113 @@
+[
+ {
+ "namespace": "experiments.urlbar",
+ "description": "APIs supporting urlbar experiments",
+ "types": [
+ {
+ "id": "DynamicResultType",
+ "type": "object",
+ "description": "Describes a dynamic result type.",
+ "properties": {
+ "viewTemplate": {
+ "type": "object",
+ "description": "An object describing the type's view.",
+ "additionalProperties": true
+ }
+ }
+ }
+ ],
+ "properties": {
+ "attributionURL": {
+ "$ref": "types.Setting",
+ "description": "Gets or sets the attribution URL for the current browser session."
+ },
+ "engagementTelemetry": {
+ "$ref": "types.Setting",
+ "description": "Enables or disables the engagement telemetry for the current browser session."
+ },
+ "extensionTimeout": {
+ "$ref": "types.Setting",
+ "description": "Sets the amount of time in ms that extensions have to return results to the browser.urlbar API."
+ }
+ },
+ "events": [
+ {
+ "name": "onViewUpdateRequested",
+ "type": "function",
+ "description": "Fired when the urlbar view updates the view of one of the results of the provider.",
+ "parameters": [
+ {
+ "name": "payload",
+ "type": "object",
+ "description": "The result's payload."
+ },
+ {
+ "name": "idsByName",
+ "type": "object",
+ "description": "A Map from an element's name, as defined by the provider; to its ID in the DOM, as defined by the browser."
+ }
+ ],
+ "extraParameters": [
+ {
+ "name": "providerName",
+ "type": "string",
+ "pattern": "^[a-zA-Z0-9_-]+$",
+ "description": "The name of the provider you want to provide updates for."
+ }
+ ],
+ "returns": {
+ "type": "object",
+ "description": "An object describing the view update."
+ }
+ }
+ ],
+ "functions": [
+ {
+ "name": "addDynamicResultType",
+ "type": "function",
+ "async": true,
+ "description": "Adds a dynamic result type. See UrlbarResult.addDynamicResultType().",
+ "parameters": [
+ {
+ "name": "name",
+ "type": "string",
+ "description": "The name of the result type."
+ },
+ {
+ "name": "type",
+ "type": "object",
+ "default": {},
+ "optional": true,
+ "description": "The result type. Currently this should be an empty object (which is the default value)."
+ }
+ ]
+ },
+ {
+ "name": "addDynamicViewTemplate",
+ "type": "function",
+ "async": true,
+ "description": "Adds a view template for a dynamic result type. See UrlbarView.addDynamicViewTemplate().",
+ "parameters": [
+ {
+ "name": "name",
+ "type": "string",
+ "description": "The view template will be registered for the dynamic result type with this name."
+ },
+ {
+ "name": "viewTemplate",
+ "type": "object",
+ "additionalProperties": true,
+ "description": "The view template."
+ }
+ ]
+ },
+ {
+ "name": "clearInput",
+ "type": "function",
+ "async": true,
+ "description": "Sets urlbar.value to the empty string and the pageproxystate to invalid.",
+ "parameters": []
+ }
+ ]
+ }
+]