forked from mirrors/gecko-dev
Bug 1855958 - Remove the experimental urlbar API (first patch). r=mak,extension-reviewers,desktop-theme-reviewers,robwu
ext-urlbar.js is unused but will be removed in a separate patch to avoid test failures in condprof tests. Condprof tests may use an outdated profile directory that still references ext-urlbar.js in startupcache files, which triggers a test-only test failure in mozilla::net::CheckForBrokenChromeURL. Differential Revision: https://phabricator.services.mozilla.com/D199769
This commit is contained in:
parent
5255efd04b
commit
f19bf97d24
34 changed files with 9 additions and 4580 deletions
|
|
@ -726,7 +726,6 @@ module.exports = {
|
|||
"browser/components/extensions/test/browser/browser_ext_tabs_warmup.js",
|
||||
"browser/components/extensions/test/browser/browser_ext_tabs_zoom.js",
|
||||
"browser/components/extensions/test/browser/browser_ext_topSites.js",
|
||||
"browser/components/extensions/test/browser/browser_ext_urlbar.js",
|
||||
"browser/components/extensions/test/browser/browser_ext_webNavigation_containerIsolation.js",
|
||||
"browser/components/extensions/test/browser/browser_ext_webNavigation_getFrames.js",
|
||||
"browser/components/extensions/test/browser/browser_ext_webNavigation_urlbar_transitions.js",
|
||||
|
|
|
|||
|
|
@ -285,6 +285,10 @@ var allowlist = [
|
|||
{ file: "chrome://browser/content/screenshots/copy.svg" },
|
||||
{ file: "chrome://browser/content/screenshots/download.svg" },
|
||||
{ file: "chrome://browser/content/screenshots/download-white.svg" },
|
||||
|
||||
// The file is temporarily left until a new conditioned profile is created,
|
||||
// to be removed with bug 1878264
|
||||
{ file: "chrome://browser/content/parent/ext-urlbar.js" },
|
||||
];
|
||||
|
||||
if (AppConstants.platform != "win") {
|
||||
|
|
|
|||
|
|
@ -163,13 +163,6 @@
|
|||
"scopes": ["addon_parent"],
|
||||
"paths": [["topSites"]]
|
||||
},
|
||||
"urlbar": {
|
||||
"url": "chrome://browser/content/parent/ext-urlbar.js",
|
||||
"schema": "chrome://browser/content/schemas/urlbar.json",
|
||||
"scopes": ["addon_parent"],
|
||||
"settings": true,
|
||||
"paths": [["urlbar"]]
|
||||
},
|
||||
"urlOverrides": {
|
||||
"url": "chrome://browser/content/parent/ext-url-overrides.js",
|
||||
"schema": "chrome://browser/content/schemas/url_overrides.json",
|
||||
|
|
|
|||
|
|
@ -23,5 +23,4 @@ browser.jar:
|
|||
content/browser/schemas/tabs.json
|
||||
content/browser/schemas/top_sites.json
|
||||
content/browser/schemas/url_overrides.json
|
||||
content/browser/schemas/urlbar.json
|
||||
content/browser/schemas/windows.json
|
||||
|
|
|
|||
|
|
@ -1,274 +0,0 @@
|
|||
[
|
||||
{
|
||||
"namespace": "manifest",
|
||||
"types": [
|
||||
{
|
||||
"$extend": "PermissionPrivileged",
|
||||
"choices": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": ["urlbar"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"namespace": "urlbar",
|
||||
"description": "Use the <code>browser.urlbar</code> API to experiment with new features in the URLBar. Restricted to Mozilla privileged WebExtensions.",
|
||||
"permissions": ["urlbar"],
|
||||
"types": [
|
||||
{
|
||||
"id": "EngagementState",
|
||||
"type": "string",
|
||||
"enum": ["start", "engagement", "abandonment", "discard"],
|
||||
"description": "The state of an engagement made with the urlbar by the user. <code>start</code>: The user has started an engagement. <code>engagement</code>: The user has completed an engagement by picking a result. <code>abandonment</code>: The user has abandoned their engagement, for example by blurring the urlbar. <code>discard</code>: The engagement ended in a way that should be ignored by listeners."
|
||||
},
|
||||
{
|
||||
"id": "Query",
|
||||
"type": "object",
|
||||
"description": "A query performed in the urlbar.",
|
||||
"properties": {
|
||||
"isPrivate": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the query's browser context is private."
|
||||
},
|
||||
"maxResults": {
|
||||
"type": "integer",
|
||||
"description": "The maximum number of results shown to the user."
|
||||
},
|
||||
"searchString": {
|
||||
"type": "string",
|
||||
"description": "The query's search string."
|
||||
},
|
||||
"sources": {
|
||||
"type": "array",
|
||||
"description": "List of acceptable source types to return.",
|
||||
"items": {
|
||||
"$ref": "SourceType"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "Result",
|
||||
"type": "object",
|
||||
"description": "A result of a query. Queries can have many results. Each result is created by a provider.",
|
||||
"properties": {
|
||||
"payload": {
|
||||
"type": "object",
|
||||
"description": "An object with arbitrary properties depending on the result's type."
|
||||
},
|
||||
"source": {
|
||||
"$ref": "SourceType",
|
||||
"description": "The result's source."
|
||||
},
|
||||
"type": {
|
||||
"$ref": "ResultType",
|
||||
"description": "The result's type."
|
||||
},
|
||||
"suggestedIndex": {
|
||||
"type": "integer",
|
||||
"description": "Suggest a preferred position for this result within the result set.",
|
||||
"optional": true,
|
||||
"default": -1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ResultType",
|
||||
"type": "string",
|
||||
"enum": ["dynamic", "remote_tab", "search", "tab", "tip", "url"],
|
||||
"description": "Possible types of results. <code>dynamic</code>: A result whose view and payload are specified by the extension. <code>remote_tab</code>: A synced tab from another device. <code>search</code>: A search suggestion from a search engine. <code>tab</code>: An open tab in the browser. <code>tip</code>: An actionable message to help the user with their query. <code>url</code>: A URL that's not one of the other types."
|
||||
},
|
||||
{
|
||||
"id": "SearchOptions",
|
||||
"type": "object",
|
||||
"description": "Options to the <code>search</code> function.",
|
||||
"properties": {
|
||||
"focus": {
|
||||
"type": "boolean",
|
||||
"optional": true,
|
||||
"default": true,
|
||||
"description": "Whether to focus the input field and select its contents."
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "SourceType",
|
||||
"type": "string",
|
||||
"enum": ["bookmarks", "history", "local", "network", "search", "tabs"],
|
||||
"description": "Possible sources of results. <code>bookmarks</code>: The result comes from the user's bookmarks. <code>history</code>: The result comes from the user's history. <code>local</code>: The result comes from some local source not covered by another source type. <code>network</code>: The result comes from some network source not covered by another source type. <code>search</code>: The result comes from a search engine. <code>tabs</code>: The result is an open tab in the browser or a synced tab from another device."
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"engagementTelemetry": {
|
||||
"$ref": "types.Setting",
|
||||
"description": "Enables or disables the engagement telemetry."
|
||||
}
|
||||
},
|
||||
"functions": [
|
||||
{
|
||||
"name": "closeView",
|
||||
"type": "function",
|
||||
"async": true,
|
||||
"description": "Closes the urlbar view in the current window.",
|
||||
"parameters": []
|
||||
},
|
||||
{
|
||||
"name": "focus",
|
||||
"type": "function",
|
||||
"async": true,
|
||||
"description": "Focuses the urlbar in the current window.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "select",
|
||||
"type": "boolean",
|
||||
"optional": true,
|
||||
"default": false,
|
||||
"description": "If true, the text in the urlbar will also be selected."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "search",
|
||||
"type": "function",
|
||||
"async": true,
|
||||
"description": "Starts a search in the urlbar in the current window.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "searchString",
|
||||
"type": "string",
|
||||
"description": "The search string."
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"$ref": "SearchOptions",
|
||||
"optional": true,
|
||||
"default": {},
|
||||
"description": "Options for the search."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"events": [
|
||||
{
|
||||
"name": "onBehaviorRequested",
|
||||
"type": "function",
|
||||
"description": "Before a query starts, this event is fired for the given provider. Its purpose is to request the provider's behavior for the query. The listener should return a behavior in response. By default, providers are inactive, so if your provider should always be inactive, you don't need to listen for this event.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "query",
|
||||
"$ref": "Query",
|
||||
"description": "The query for which the behavior is requested."
|
||||
}
|
||||
],
|
||||
"extraParameters": [
|
||||
{
|
||||
"name": "providerName",
|
||||
"type": "string",
|
||||
"pattern": "^[a-zA-Z0-9_-]+$",
|
||||
"description": "The name of the provider whose behavior the listener returns."
|
||||
}
|
||||
],
|
||||
"returns": {
|
||||
"type": "string",
|
||||
"enum": ["active", "inactive", "restricting"],
|
||||
"description": "The behavior of the provider for the query."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "onEngagement",
|
||||
"type": "function",
|
||||
"description": "This event is fired when the user starts and ends an engagement with the urlbar.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "state",
|
||||
"$ref": "EngagementState",
|
||||
"description": "The state of the engagement."
|
||||
}
|
||||
],
|
||||
"extraParameters": [
|
||||
{
|
||||
"name": "providerName",
|
||||
"type": "string",
|
||||
"pattern": "^[a-zA-Z0-9_-]+$",
|
||||
"description": "The name of the provider that will listen for engagement events."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "onQueryCanceled",
|
||||
"type": "function",
|
||||
"description": "This event is fired for the given provider when a query is canceled. The listener should stop any ongoing fetch or creation of results and clean up its resources.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "query",
|
||||
"$ref": "Query",
|
||||
"description": "The query that was canceled."
|
||||
}
|
||||
],
|
||||
"extraParameters": [
|
||||
{
|
||||
"name": "providerName",
|
||||
"type": "string",
|
||||
"pattern": "^[a-zA-Z0-9_-]+$",
|
||||
"description": "The name of the provider that is creating results for the query."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "onResultsRequested",
|
||||
"type": "function",
|
||||
"description": "When a query starts, this event is fired for the given provider if the provider is active for the query and there are no other providers that are restricting. Its purpose is to request the provider's results for the query. The listener should return a list of results in response.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "query",
|
||||
"$ref": "Query",
|
||||
"description": "The query for which results are requested."
|
||||
}
|
||||
],
|
||||
"extraParameters": [
|
||||
{
|
||||
"name": "providerName",
|
||||
"type": "string",
|
||||
"pattern": "^[a-zA-Z0-9_-]+$",
|
||||
"description": "The name of the provider whose results the listener returns."
|
||||
}
|
||||
],
|
||||
"returns": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "Result"
|
||||
},
|
||||
"description": "The results that the provider fetched for the query."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "onResultPicked",
|
||||
"type": "function",
|
||||
"description": "Typically, a provider includes a <code>url</code> property in its results' payloads. When the user picks a result with a URL, Firefox automatically loads the URL. URLs don't make sense for every result type, however. When the user picks a result without a URL, this event is fired. The provider should take an appropriate action in response. Currently the only applicable <code>ResultTypes</code> are <code>dynamic</code> and <code>tip</code>.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "payload",
|
||||
"type": "object",
|
||||
"description": "The payload of the result that was picked."
|
||||
},
|
||||
{
|
||||
"name": "elementName",
|
||||
"type": "string",
|
||||
"description": "If the result is a dynamic type, this is the name of the element in the result view that was picked. If the result is not a dynamic type, this is an empty string."
|
||||
}
|
||||
],
|
||||
"extraParameters": [
|
||||
{
|
||||
"name": "providerName",
|
||||
"type": "string",
|
||||
"pattern": "^[a-zA-Z0-9_-]+$",
|
||||
"description": "The listener will be called for the results of the provider with this name."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
@ -612,9 +612,6 @@ skip-if = [
|
|||
"win11_2009 && debug", # Bug 1797751
|
||||
]
|
||||
|
||||
["browser_ext_urlbar.js"]
|
||||
https_first_disabled = true
|
||||
|
||||
["browser_ext_user_events.js"]
|
||||
|
||||
["browser_ext_webNavigation_containerIsolation.js"]
|
||||
|
|
|
|||
|
|
@ -1,590 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
ChromeUtils.defineESModuleGetters(this, {
|
||||
PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
|
||||
PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
|
||||
UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.sys.mjs",
|
||||
});
|
||||
|
||||
ChromeUtils.defineLazyGetter(this, "UrlbarTestUtils", () => {
|
||||
const { UrlbarTestUtils: module } = ChromeUtils.importESModule(
|
||||
"resource://testing-common/UrlbarTestUtils.sys.mjs"
|
||||
);
|
||||
module.init(this);
|
||||
return module;
|
||||
});
|
||||
|
||||
async function loadTipExtension(options = {}) {
|
||||
let ext = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
permissions: ["urlbar"],
|
||||
},
|
||||
isPrivileged: true,
|
||||
background() {
|
||||
browser.test.onMessage.addListener(options => {
|
||||
browser.urlbar.onBehaviorRequested.addListener(query => {
|
||||
return "restricting";
|
||||
}, "test");
|
||||
browser.urlbar.onResultsRequested.addListener(query => {
|
||||
return [
|
||||
{
|
||||
type: "tip",
|
||||
source: "local",
|
||||
heuristic: true,
|
||||
payload: {
|
||||
text: "Test",
|
||||
buttonText: "OK",
|
||||
buttonUrl: options.buttonUrl,
|
||||
helpUrl: options.helpUrl,
|
||||
},
|
||||
},
|
||||
];
|
||||
}, "test");
|
||||
browser.urlbar.onResultPicked.addListener((payload, details) => {
|
||||
browser.test.assertEq(payload.text, "Test", "payload.text");
|
||||
browser.test.assertEq(payload.buttonText, "OK", "payload.buttonText");
|
||||
browser.test.sendMessage("onResultPicked received", details);
|
||||
}, "test");
|
||||
});
|
||||
},
|
||||
});
|
||||
await ext.startup();
|
||||
ext.sendMessage(options);
|
||||
|
||||
// Wait for the provider to be registered before continuing. The provider
|
||||
// will be registered once the parent process receives the first addListener
|
||||
// call from the extension. There's no better way to do this, unfortunately.
|
||||
// For example, if the extension sends a message to the test after it adds its
|
||||
// listeners and then we wait here for that message, there's no guarantee that
|
||||
// the addListener calls will have been received in the parent yet.
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => UrlbarProvidersManager.getProvider("test"),
|
||||
"Waiting for provider to be registered"
|
||||
);
|
||||
|
||||
Assert.ok(
|
||||
UrlbarProvidersManager.getProvider("test"),
|
||||
"Provider should have been registered"
|
||||
);
|
||||
return ext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the Top Sites feed.
|
||||
*
|
||||
* @param {Function} condition
|
||||
* A callback that returns true after Top Sites are successfully updated.
|
||||
* @param {boolean} searchShortcuts
|
||||
* True if Top Sites search shortcuts should be enabled.
|
||||
*/
|
||||
async function updateTopSites(condition, searchShortcuts = false) {
|
||||
// Toggle the pref to clear the feed cache and force an update.
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [
|
||||
["browser.newtabpage.activity-stream.feeds.system.topsites", false],
|
||||
["browser.newtabpage.activity-stream.feeds.system.topsites", true],
|
||||
[
|
||||
"browser.newtabpage.activity-stream.improvesearch.topSiteSearchShortcuts",
|
||||
searchShortcuts,
|
||||
],
|
||||
],
|
||||
});
|
||||
|
||||
// Wait for the feed to be updated.
|
||||
await TestUtils.waitForCondition(() => {
|
||||
let sites = AboutNewTab.getTopSites();
|
||||
return condition(sites);
|
||||
}, "Waiting for top sites to be updated");
|
||||
}
|
||||
|
||||
add_setup(async function () {
|
||||
Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", false);
|
||||
registerCleanupFunction(async () => {
|
||||
Services.prefs.clearUserPref("browser.urlbar.suggest.quickactions");
|
||||
});
|
||||
// Set the notification timeout to a really high value to avoid intermittent
|
||||
// failures due to the mock extensions not responding in time.
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [["browser.urlbar.extension.timeout", 5000]],
|
||||
});
|
||||
});
|
||||
|
||||
// Loads a tip extension without a main button URL and presses enter on the main
|
||||
// button.
|
||||
add_task(async function tip_onResultPicked_mainButton_noURL_enter() {
|
||||
let ext = await loadTipExtension();
|
||||
await UrlbarTestUtils.promiseAutocompleteResultPopup({
|
||||
window,
|
||||
waitForFocus,
|
||||
value: "test",
|
||||
});
|
||||
EventUtils.synthesizeKey("KEY_Enter");
|
||||
await ext.awaitMessage("onResultPicked received");
|
||||
await ext.unload();
|
||||
});
|
||||
|
||||
// Loads a tip extension without a main button URL and clicks the main button.
|
||||
add_task(async function tip_onResultPicked_mainButton_noURL_mouse() {
|
||||
let ext = await loadTipExtension();
|
||||
await UrlbarTestUtils.promiseAutocompleteResultPopup({
|
||||
window,
|
||||
waitForFocus,
|
||||
value: "test",
|
||||
});
|
||||
let mainButton = gURLBar.querySelector(".urlbarView-button-tip");
|
||||
Assert.ok(mainButton);
|
||||
EventUtils.synthesizeMouseAtCenter(mainButton, {});
|
||||
await ext.awaitMessage("onResultPicked received");
|
||||
await ext.unload();
|
||||
});
|
||||
|
||||
// Loads a tip extension with a main button URL and presses enter on the main
|
||||
// button.
|
||||
add_task(async function tip_onResultPicked_mainButton_url_enter() {
|
||||
let ext = await loadTipExtension({ buttonUrl: "https://example.com/" });
|
||||
await BrowserTestUtils.withNewTab("about:blank", async () => {
|
||||
await UrlbarTestUtils.promiseAutocompleteResultPopup({
|
||||
window,
|
||||
waitForFocus,
|
||||
value: "test",
|
||||
});
|
||||
let loadedPromise = BrowserTestUtils.browserLoaded(
|
||||
gBrowser.selectedBrowser
|
||||
);
|
||||
ext.onMessage("onResultPicked received", () => {
|
||||
Assert.ok(false, "onResultPicked should not be called");
|
||||
});
|
||||
EventUtils.synthesizeKey("KEY_Enter");
|
||||
await loadedPromise;
|
||||
Assert.equal(gBrowser.currentURI.spec, "https://example.com/");
|
||||
});
|
||||
await ext.unload();
|
||||
});
|
||||
|
||||
// Loads a tip extension with a main button URL and clicks the main button.
|
||||
add_task(async function tip_onResultPicked_mainButton_url_mouse() {
|
||||
let ext = await loadTipExtension({ buttonUrl: "https://example.com/" });
|
||||
await BrowserTestUtils.withNewTab("about:blank", async () => {
|
||||
await UrlbarTestUtils.promiseAutocompleteResultPopup({
|
||||
window,
|
||||
waitForFocus,
|
||||
value: "test",
|
||||
});
|
||||
let mainButton = gURLBar.querySelector(".urlbarView-button-tip");
|
||||
Assert.ok(mainButton);
|
||||
let loadedPromise = BrowserTestUtils.browserLoaded(
|
||||
gBrowser.selectedBrowser
|
||||
);
|
||||
ext.onMessage("onResultPicked received", () => {
|
||||
Assert.ok(false, "onResultPicked should not be called");
|
||||
});
|
||||
EventUtils.synthesizeMouseAtCenter(mainButton, {});
|
||||
await loadedPromise;
|
||||
Assert.equal(gBrowser.currentURI.spec, "https://example.com/");
|
||||
});
|
||||
await ext.unload();
|
||||
});
|
||||
|
||||
// Loads a tip extension with a help button URL and presses enter on the help
|
||||
// button.
|
||||
add_task(async function tip_onResultPicked_helpButton_url_enter() {
|
||||
let ext = await loadTipExtension({ helpUrl: "https://example.com/" });
|
||||
await BrowserTestUtils.withNewTab("about:blank", async () => {
|
||||
await UrlbarTestUtils.promiseAutocompleteResultPopup({
|
||||
window,
|
||||
waitForFocus,
|
||||
value: "test",
|
||||
});
|
||||
ext.onMessage("onResultPicked received", () => {
|
||||
Assert.ok(false, "onResultPicked should not be called");
|
||||
});
|
||||
let loadedPromise = BrowserTestUtils.browserLoaded(
|
||||
gBrowser.selectedBrowser
|
||||
);
|
||||
await UrlbarTestUtils.openResultMenuAndPressAccesskey(window, "h");
|
||||
info("Waiting for help URL to load");
|
||||
await loadedPromise;
|
||||
Assert.equal(gBrowser.currentURI.spec, "https://example.com/");
|
||||
});
|
||||
await ext.unload();
|
||||
});
|
||||
|
||||
// Loads a tip extension with a help button URL and clicks the help button.
|
||||
add_task(async function tip_onResultPicked_helpButton_url_mouse() {
|
||||
let ext = await loadTipExtension({ helpUrl: "https://example.com/" });
|
||||
await BrowserTestUtils.withNewTab("about:blank", async () => {
|
||||
await UrlbarTestUtils.promiseAutocompleteResultPopup({
|
||||
window,
|
||||
waitForFocus,
|
||||
value: "test",
|
||||
});
|
||||
ext.onMessage("onResultPicked received", () => {
|
||||
Assert.ok(false, "onResultPicked should not be called");
|
||||
});
|
||||
let loadedPromise = BrowserTestUtils.browserLoaded(
|
||||
gBrowser.selectedBrowser
|
||||
);
|
||||
await UrlbarTestUtils.openResultMenuAndPressAccesskey(window, "h", {
|
||||
openByMouse: true,
|
||||
});
|
||||
info("Waiting for help URL to load");
|
||||
await loadedPromise;
|
||||
Assert.equal(gBrowser.currentURI.spec, "https://example.com/");
|
||||
});
|
||||
await ext.unload();
|
||||
});
|
||||
|
||||
// Tests the search function with a non-empty string.
|
||||
add_task(async function search() {
|
||||
gURLBar.blur();
|
||||
|
||||
let ext = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
permissions: ["urlbar"],
|
||||
},
|
||||
isPrivileged: true,
|
||||
background: () => {
|
||||
browser.urlbar.search("test");
|
||||
},
|
||||
});
|
||||
await ext.startup();
|
||||
|
||||
let context = await UrlbarTestUtils.promiseSearchComplete(window);
|
||||
Assert.equal(gURLBar.value, "test");
|
||||
Assert.equal(context.searchString, "test");
|
||||
Assert.ok(gURLBar.focused);
|
||||
Assert.equal(gURLBar.getAttribute("focused"), "true");
|
||||
|
||||
await UrlbarTestUtils.promisePopupClose(window);
|
||||
await ext.unload();
|
||||
});
|
||||
|
||||
// Tests the search function with an empty string.
|
||||
add_task(async function searchEmpty() {
|
||||
gURLBar.blur();
|
||||
|
||||
// Searching for an empty string shows the history view, but there may be no
|
||||
// history here since other tests may have cleared it or since this test is
|
||||
// running in isolation. We want to make sure providers are called and their
|
||||
// results are shown, so add a provider that returns a tip.
|
||||
let ext = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
permissions: ["urlbar"],
|
||||
},
|
||||
isPrivileged: true,
|
||||
background() {
|
||||
browser.urlbar.onBehaviorRequested.addListener(query => {
|
||||
return "restricting";
|
||||
}, "test");
|
||||
browser.urlbar.onResultsRequested.addListener(query => {
|
||||
return [
|
||||
{
|
||||
type: "tip",
|
||||
source: "local",
|
||||
heuristic: true,
|
||||
payload: {
|
||||
text: "Test",
|
||||
buttonText: "OK",
|
||||
},
|
||||
},
|
||||
];
|
||||
}, "test");
|
||||
browser.urlbar.search("");
|
||||
},
|
||||
});
|
||||
await ext.startup();
|
||||
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => UrlbarProvidersManager.getProvider("test"),
|
||||
"Waiting for provider to be registered"
|
||||
);
|
||||
|
||||
let context = await UrlbarTestUtils.promiseSearchComplete(window);
|
||||
Assert.equal(gURLBar.value, "");
|
||||
Assert.equal(context.searchString, "");
|
||||
Assert.equal(context.results.length, 1);
|
||||
Assert.equal(context.results[0].type, UrlbarUtils.RESULT_TYPE.TIP);
|
||||
Assert.ok(gURLBar.focused);
|
||||
Assert.equal(gURLBar.getAttribute("focused"), "true");
|
||||
|
||||
await UrlbarTestUtils.promisePopupClose(window);
|
||||
await ext.unload();
|
||||
await SpecialPowers.popPrefEnv();
|
||||
});
|
||||
|
||||
// Tests the search function with `focus: false`.
|
||||
add_task(async function searchFocusFalse() {
|
||||
await PlacesUtils.history.clear();
|
||||
await PlacesUtils.bookmarks.eraseEverything();
|
||||
await PlacesTestUtils.addVisits([
|
||||
"https://example.com/test1",
|
||||
"https://example.com/test2",
|
||||
]);
|
||||
|
||||
gURLBar.blur();
|
||||
|
||||
let ext = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
permissions: ["urlbar"],
|
||||
},
|
||||
isPrivileged: true,
|
||||
background: () => {
|
||||
browser.urlbar.search("test", { focus: false });
|
||||
},
|
||||
});
|
||||
await ext.startup();
|
||||
|
||||
let context = await UrlbarTestUtils.promiseSearchComplete(window);
|
||||
Assert.equal(gURLBar.value, "test");
|
||||
Assert.equal(context.searchString, "test");
|
||||
Assert.ok(!gURLBar.focused);
|
||||
Assert.ok(!gURLBar.hasAttribute("focused"));
|
||||
|
||||
let resultCount = UrlbarTestUtils.getResultCount(window);
|
||||
Assert.equal(resultCount, 3);
|
||||
|
||||
let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
|
||||
Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.SEARCH);
|
||||
Assert.equal(result.title, "test");
|
||||
|
||||
result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
|
||||
Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.URL);
|
||||
Assert.equal(result.url, "https://example.com/test2");
|
||||
|
||||
result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2);
|
||||
Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.URL);
|
||||
Assert.equal(result.url, "https://example.com/test1");
|
||||
|
||||
await UrlbarTestUtils.promisePopupClose(window);
|
||||
await ext.unload();
|
||||
await SpecialPowers.popPrefEnv();
|
||||
});
|
||||
|
||||
// Tests the search function with `focus: false` and an empty string.
|
||||
add_task(async function searchFocusFalseEmpty() {
|
||||
await PlacesUtils.history.clear();
|
||||
await PlacesUtils.bookmarks.eraseEverything();
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await PlacesTestUtils.addVisits(["https://example.com/test1"]);
|
||||
}
|
||||
await updateTopSites(sites => sites.length == 1);
|
||||
gURLBar.blur();
|
||||
|
||||
let ext = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
permissions: ["urlbar"],
|
||||
},
|
||||
isPrivileged: true,
|
||||
background: () => {
|
||||
browser.urlbar.search("", { focus: false });
|
||||
},
|
||||
});
|
||||
await ext.startup();
|
||||
|
||||
let context = await UrlbarTestUtils.promiseSearchComplete(window);
|
||||
Assert.equal(gURLBar.value, "");
|
||||
Assert.equal(context.searchString, "");
|
||||
Assert.ok(!gURLBar.focused);
|
||||
Assert.ok(!gURLBar.hasAttribute("focused"));
|
||||
|
||||
let resultCount = UrlbarTestUtils.getResultCount(window);
|
||||
Assert.equal(resultCount, 1);
|
||||
|
||||
let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0);
|
||||
Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.URL);
|
||||
Assert.equal(result.url, "https://example.com/test1");
|
||||
|
||||
await UrlbarTestUtils.promisePopupClose(window);
|
||||
await ext.unload();
|
||||
await SpecialPowers.popPrefEnv();
|
||||
});
|
||||
|
||||
// Tests the focus function with select = false.
|
||||
add_task(async function focusSelectFalse() {
|
||||
gURLBar.blur();
|
||||
gURLBar.value = "test";
|
||||
Assert.ok(!gURLBar.focused);
|
||||
Assert.ok(!gURLBar.hasAttribute("focused"));
|
||||
|
||||
let ext = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
permissions: ["urlbar"],
|
||||
},
|
||||
isPrivileged: true,
|
||||
background: () => {
|
||||
browser.urlbar.focus();
|
||||
},
|
||||
});
|
||||
await ext.startup();
|
||||
|
||||
await TestUtils.waitForCondition(() => gURLBar.focused);
|
||||
Assert.ok(gURLBar.focused);
|
||||
Assert.ok(gURLBar.hasAttribute("focused"));
|
||||
Assert.equal(gURLBar.selectionStart, gURLBar.selectionEnd);
|
||||
|
||||
await ext.unload();
|
||||
});
|
||||
|
||||
// Tests the focus function with select = true.
|
||||
add_task(async function focusSelectTrue() {
|
||||
gURLBar.blur();
|
||||
gURLBar.value = "test";
|
||||
Assert.ok(!gURLBar.focused);
|
||||
Assert.ok(!gURLBar.hasAttribute("focused"));
|
||||
|
||||
let ext = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
permissions: ["urlbar"],
|
||||
},
|
||||
isPrivileged: true,
|
||||
background: () => {
|
||||
browser.urlbar.focus(true);
|
||||
},
|
||||
});
|
||||
await ext.startup();
|
||||
|
||||
await TestUtils.waitForCondition(() => gURLBar.focused);
|
||||
Assert.ok(gURLBar.focused);
|
||||
Assert.ok(gURLBar.hasAttribute("focused"));
|
||||
Assert.equal(gURLBar.selectionStart, 0);
|
||||
Assert.equal(gURLBar.selectionEnd, "test".length);
|
||||
|
||||
await ext.unload();
|
||||
});
|
||||
|
||||
// Tests the closeView function.
|
||||
add_task(async function closeView() {
|
||||
await UrlbarTestUtils.promiseAutocompleteResultPopup({
|
||||
window,
|
||||
waitForFocus,
|
||||
value: "test",
|
||||
});
|
||||
|
||||
let ext = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
permissions: ["urlbar"],
|
||||
},
|
||||
isPrivileged: true,
|
||||
background: () => {
|
||||
browser.urlbar.closeView();
|
||||
},
|
||||
});
|
||||
await UrlbarTestUtils.promisePopupClose(window, () => ext.startup());
|
||||
await ext.unload();
|
||||
});
|
||||
|
||||
// Tests the onEngagement events.
|
||||
add_task(async function onEngagement() {
|
||||
gURLBar.blur();
|
||||
|
||||
let ext = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
permissions: ["urlbar"],
|
||||
},
|
||||
isPrivileged: true,
|
||||
background() {
|
||||
browser.urlbar.onEngagement.addListener(state => {
|
||||
browser.test.sendMessage("onEngagement", state);
|
||||
}, "test");
|
||||
browser.urlbar.onBehaviorRequested.addListener(query => {
|
||||
return "restricting";
|
||||
}, "test");
|
||||
browser.urlbar.onResultsRequested.addListener(query => {
|
||||
return [
|
||||
{
|
||||
type: "tip",
|
||||
source: "local",
|
||||
heuristic: true,
|
||||
payload: {
|
||||
text: "Test",
|
||||
buttonText: "OK",
|
||||
},
|
||||
},
|
||||
];
|
||||
}, "test");
|
||||
browser.urlbar.search("");
|
||||
},
|
||||
});
|
||||
await ext.startup();
|
||||
|
||||
// Start an engagement.
|
||||
let messagePromise = ext.awaitMessage("onEngagement");
|
||||
await UrlbarTestUtils.promiseAutocompleteResultPopup({
|
||||
window,
|
||||
waitForFocus,
|
||||
value: "test",
|
||||
fireInputEvent: true,
|
||||
});
|
||||
let state = await messagePromise;
|
||||
Assert.equal(state, "start");
|
||||
|
||||
// Abandon the engagement.
|
||||
messagePromise = ext.awaitMessage("onEngagement");
|
||||
gURLBar.blur();
|
||||
state = await messagePromise;
|
||||
Assert.equal(state, "abandonment");
|
||||
|
||||
// Start an engagement.
|
||||
messagePromise = ext.awaitMessage("onEngagement");
|
||||
await UrlbarTestUtils.promiseAutocompleteResultPopup({
|
||||
window,
|
||||
waitForFocus,
|
||||
value: "test",
|
||||
fireInputEvent: true,
|
||||
});
|
||||
state = await messagePromise;
|
||||
Assert.equal(state, "start");
|
||||
|
||||
// End the engagement by pressing enter on the extension's tip result.
|
||||
messagePromise = ext.awaitMessage("onEngagement");
|
||||
EventUtils.synthesizeKey("KEY_Enter");
|
||||
state = await messagePromise;
|
||||
Assert.equal(state, "engagement");
|
||||
|
||||
// We'll open about:preferences next. Since it won't open in a new tab if the
|
||||
// current tab is blank, open a new tab now.
|
||||
await BrowserTestUtils.withNewTab("about:blank", async () => {
|
||||
// Start an engagement.
|
||||
messagePromise = ext.awaitMessage("onEngagement");
|
||||
await UrlbarTestUtils.promiseAutocompleteResultPopup({
|
||||
window,
|
||||
waitForFocus,
|
||||
value: "test",
|
||||
fireInputEvent: true,
|
||||
});
|
||||
state = await messagePromise;
|
||||
Assert.equal(state, "start");
|
||||
|
||||
// Press up and enter to pick the search settings button.
|
||||
messagePromise = ext.awaitMessage("onEngagement");
|
||||
EventUtils.synthesizeKey("KEY_ArrowUp");
|
||||
EventUtils.synthesizeKey("KEY_Enter");
|
||||
await BrowserTestUtils.browserLoaded(
|
||||
gBrowser.selectedBrowser,
|
||||
false,
|
||||
"about:preferences#search"
|
||||
);
|
||||
state = await messagePromise;
|
||||
Assert.equal(state, "discard");
|
||||
});
|
||||
|
||||
// Start a final engagement to make sure the previous discard didn't mess
|
||||
// anything up.
|
||||
messagePromise = ext.awaitMessage("onEngagement");
|
||||
await UrlbarTestUtils.promiseAutocompleteResultPopup({
|
||||
window,
|
||||
waitForFocus,
|
||||
value: "test",
|
||||
fireInputEvent: true,
|
||||
});
|
||||
state = await messagePromise;
|
||||
Assert.equal(state, "start");
|
||||
|
||||
// End the engagement by pressing enter on the extension's tip result.
|
||||
messagePromise = ext.awaitMessage("onEngagement");
|
||||
EventUtils.synthesizeKey("KEY_Enter");
|
||||
state = await messagePromise;
|
||||
Assert.equal(state, "engagement");
|
||||
|
||||
await ext.unload();
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -67,9 +67,3 @@ skip-if = ["condprof"] # Bug 1769184 - by design for now
|
|||
["test_ext_url_overrides_newtab.js"]
|
||||
|
||||
["test_ext_url_overrides_newtab_update.js"]
|
||||
|
||||
["test_ext_urlbar.js"]
|
||||
skip-if = [
|
||||
"tsan", # Unreasonably slow, bug 1612707
|
||||
"condprof", # Bug 1769184 - by design for now
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,413 +0,0 @@
|
|||
/* 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/. */
|
||||
|
||||
/**
|
||||
* This module exports a provider class that is used for providers created by
|
||||
* extensions.
|
||||
*/
|
||||
|
||||
import {
|
||||
SkippableTimer,
|
||||
UrlbarProvider,
|
||||
UrlbarUtils,
|
||||
} from "resource:///modules/UrlbarUtils.sys.mjs";
|
||||
|
||||
const lazy = {};
|
||||
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
|
||||
UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.sys.mjs",
|
||||
UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs",
|
||||
UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.sys.mjs",
|
||||
});
|
||||
|
||||
// The set of `UrlbarQueryContext` properties that aren't serializable.
|
||||
const NONSERIALIZABLE_CONTEXT_PROPERTIES = new Set(["view"]);
|
||||
|
||||
/**
|
||||
* The browser.urlbar extension API allows extensions to create their own urlbar
|
||||
* providers. The results from extension providers are integrated into the
|
||||
* urlbar view just like the results from providers that are built into Firefox.
|
||||
*
|
||||
* This class is the interface between the provider-related parts of the
|
||||
* browser.urlbar extension API implementation and our internal urlbar
|
||||
* implementation. The API implementation should use this class to manage
|
||||
* providers created by extensions. All extension providers must be instances
|
||||
* of this class.
|
||||
*
|
||||
* When an extension requires a provider, the API implementation should call
|
||||
* getOrCreate() to get or create it. When an extension adds an event listener
|
||||
* related to a provider, the API implementation should call setEventListener()
|
||||
* to register its own event listener with the provider.
|
||||
*/
|
||||
export class UrlbarProviderExtension extends UrlbarProvider {
|
||||
/**
|
||||
* Returns the extension provider with the given name, creating it first if
|
||||
* it doesn't exist.
|
||||
*
|
||||
* @param {string} name
|
||||
* The provider name.
|
||||
* @returns {UrlbarProviderExtension}
|
||||
* The provider.
|
||||
*/
|
||||
static getOrCreate(name) {
|
||||
let provider = lazy.UrlbarProvidersManager.getProvider(name);
|
||||
if (!provider) {
|
||||
provider = new UrlbarProviderExtension(name);
|
||||
lazy.UrlbarProvidersManager.registerProvider(provider);
|
||||
}
|
||||
return provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param {string} name
|
||||
* The provider's name.
|
||||
*/
|
||||
constructor(name) {
|
||||
super();
|
||||
this._name = name;
|
||||
this._eventListeners = new Map();
|
||||
this.behavior = "inactive";
|
||||
}
|
||||
|
||||
/**
|
||||
* The provider's name.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
get name() {
|
||||
return this._name;
|
||||
}
|
||||
|
||||
/**
|
||||
* The provider's type. The type of extension providers is always
|
||||
* UrlbarUtils.PROVIDER_TYPE.EXTENSION.
|
||||
*
|
||||
* @returns {UrlbarUtils.PROVIDER_TYPE}
|
||||
*/
|
||||
get type() {
|
||||
return UrlbarUtils.PROVIDER_TYPE.EXTENSION;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the provider should be invoked for the given context. If this
|
||||
* method returns false, the providers manager won't start a query with this
|
||||
* provider, to save on resources.
|
||||
*
|
||||
* @param {UrlbarQueryContext} context
|
||||
* The query context object.
|
||||
* @returns {boolean}
|
||||
* Whether this provider should be invoked for the search.
|
||||
*/
|
||||
isActive(context) {
|
||||
return this.behavior != "inactive";
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the provider's priority.
|
||||
*
|
||||
* @param {UrlbarQueryContext} context
|
||||
* The query context object.
|
||||
* @returns {number}
|
||||
* The provider's priority for the given query.
|
||||
*/
|
||||
getPriority(context) {
|
||||
// We give restricting extension providers a very high priority so that they
|
||||
// normally override all built-in providers, but not Infinity so that we can
|
||||
// still override them if necessary.
|
||||
return this.behavior == "restricting" ? 999 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the listener function for an event. The extension API implementation
|
||||
* should call this from its EventManager.register() implementations. Since
|
||||
* EventManager.register() is called at most only once for each extension
|
||||
* event (the first time the extension adds a listener for the event), each
|
||||
* provider instance needs at most only one listener per event, and that's why
|
||||
* this method is named setEventListener instead of addEventListener.
|
||||
*
|
||||
* The given listener function may return a promise that's resolved once the
|
||||
* extension responds to the event, or if the event requires no response from
|
||||
* the extension, it may return a non-promise value (possibly nothing).
|
||||
*
|
||||
* To remove the previously set listener, call this method again but pass null
|
||||
* as the listener function.
|
||||
*
|
||||
* The event name should be one of the following:
|
||||
*
|
||||
* behaviorRequested
|
||||
* This event is fired when the provider's behavior is needed from the
|
||||
* extension. The listener should return a behavior string.
|
||||
* queryCanceled
|
||||
* This event is fired when an ongoing query is canceled. The listener
|
||||
* shouldn't return anything.
|
||||
* resultsRequested
|
||||
* This event is fired when the provider's results are needed from the
|
||||
* extension. The listener should return an array of results.
|
||||
*
|
||||
* @param {string} eventName
|
||||
* The name of the event to listen to.
|
||||
* @param {Function} listener
|
||||
* The function that will be called when the event is fired.
|
||||
*/
|
||||
setEventListener(eventName, listener) {
|
||||
if (listener) {
|
||||
this._eventListeners.set(eventName, listener);
|
||||
} else {
|
||||
this._eventListeners.delete(eventName);
|
||||
if (!this._eventListeners.size) {
|
||||
lazy.UrlbarProvidersManager.unregisterProvider(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called by the providers manager before a query starts to
|
||||
* update each extension provider's behavior. It fires the behaviorRequested
|
||||
* event.
|
||||
*
|
||||
* @param {UrlbarQueryContext} context
|
||||
* The query context.
|
||||
*/
|
||||
async updateBehavior(context) {
|
||||
let behavior = await this._notifyListener(
|
||||
"behaviorRequested",
|
||||
makeSerializable(context)
|
||||
);
|
||||
if (behavior) {
|
||||
this.behavior = behavior;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is called only for dynamic result types, when the urlbar view updates
|
||||
* the view of one of the results of the provider. It should return an object
|
||||
* describing the view update. See the base UrlbarProvider class for more.
|
||||
*
|
||||
* @param {UrlbarResult} result The result whose view will be updated.
|
||||
* @param {Map} idsByName
|
||||
* A Map from an element's name, as defined by the provider; to its ID in
|
||||
* the DOM, as defined by the browser.
|
||||
* @returns {object} An object describing the view update.
|
||||
*/
|
||||
async getViewUpdate(result, idsByName) {
|
||||
return this._notifyListener("getViewUpdate", result, idsByName);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called by the providers manager when a query starts to fetch
|
||||
* each extension provider's results. It fires the resultsRequested event.
|
||||
*
|
||||
* @param {UrlbarQueryContext} context
|
||||
* The query context.
|
||||
* @param {Function} addCallback
|
||||
* The callback invoked by this method to add each result.
|
||||
*/
|
||||
async startQuery(context, addCallback) {
|
||||
let extResults = await this._notifyListener(
|
||||
"resultsRequested",
|
||||
makeSerializable(context)
|
||||
);
|
||||
if (extResults) {
|
||||
for (let extResult of extResults) {
|
||||
let result = await this._makeUrlbarResult(context, extResult).catch(
|
||||
ex => this.logger.error(ex)
|
||||
);
|
||||
if (result) {
|
||||
addCallback(this, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called by the providers manager when an ongoing query is
|
||||
* canceled. It fires the queryCanceled event.
|
||||
*
|
||||
* @param {UrlbarQueryContext} context
|
||||
* The query context.
|
||||
*/
|
||||
cancelQuery(context) {
|
||||
this._notifyListener("queryCanceled", makeSerializable(context));
|
||||
}
|
||||
|
||||
#pickResult(result, element) {
|
||||
let dynamicElementName = "";
|
||||
if (element && result.type == UrlbarUtils.RESULT_TYPE.DYNAMIC) {
|
||||
dynamicElementName = element.getAttribute("name");
|
||||
}
|
||||
this._notifyListener("resultPicked", result.payload, dynamicElementName);
|
||||
}
|
||||
|
||||
onEngagement(state, queryContext, details, controller) {
|
||||
let { result, element } = details;
|
||||
// By design, the "resultPicked" extension event should not be fired when
|
||||
// the picked element has a URL.
|
||||
if (result?.providerName == this.name && !element?.dataset.url) {
|
||||
this.#pickResult(result, element);
|
||||
}
|
||||
|
||||
this._notifyListener("engagement", queryContext.isPrivate, state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls a listener function set by the extension API implementation, if any.
|
||||
*
|
||||
* @param {string} eventName
|
||||
* The name of the listener to call (i.e., the name of the event to fire).
|
||||
* @param {*} args
|
||||
* Arguments to the listener function.
|
||||
* @returns {*}
|
||||
* The value returned by the listener function, if any.
|
||||
*/
|
||||
async _notifyListener(eventName, ...args) {
|
||||
let listener = this._eventListeners.get(eventName);
|
||||
if (!listener) {
|
||||
return undefined;
|
||||
}
|
||||
let result;
|
||||
try {
|
||||
result = listener(...args);
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
return undefined;
|
||||
}
|
||||
if (result.catch) {
|
||||
// The result is a promise, so wait for it to be resolved. Set up a timer
|
||||
// so that we're not stuck waiting forever.
|
||||
let timer = new SkippableTimer({
|
||||
name: "UrlbarProviderExtension notification timer",
|
||||
time: lazy.UrlbarPrefs.get("extension.timeout"),
|
||||
reportErrorOnTimeout: true,
|
||||
logger: this.logger,
|
||||
});
|
||||
result = await Promise.race([
|
||||
timer.promise,
|
||||
result.catch(ex => this.logger.error(ex)),
|
||||
]);
|
||||
timer.cancel();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a plain-JS-object result created by the extension into a
|
||||
* UrlbarResult object.
|
||||
*
|
||||
* @param {UrlbarQueryContext} context
|
||||
* The query context.
|
||||
* @param {object} extResult
|
||||
* A plain JS object representing a result created by the extension.
|
||||
* @returns {UrlbarResult}
|
||||
* The UrlbarResult object.
|
||||
*/
|
||||
async _makeUrlbarResult(context, extResult) {
|
||||
// If the result is a search result, make sure its payload has a valid
|
||||
// `engine` property, which is the name of an engine, and which we use later
|
||||
// on to look up the nsISearchEngine. We allow the extension to specify the
|
||||
// engine by its name, alias, or domain. Prefer aliases over domains since
|
||||
// one domain can have many engines.
|
||||
if (extResult.type == "search") {
|
||||
let engine;
|
||||
if (extResult.payload.engine) {
|
||||
// Validate the engine name by looking it up.
|
||||
engine = Services.search.getEngineByName(extResult.payload.engine);
|
||||
} else if (extResult.payload.keyword) {
|
||||
// Look up the engine by its alias.
|
||||
engine = await lazy.UrlbarSearchUtils.engineForAlias(
|
||||
extResult.payload.keyword
|
||||
);
|
||||
} else if (extResult.payload.url) {
|
||||
// Look up the engine by its domain.
|
||||
let host;
|
||||
try {
|
||||
host = new URL(extResult.payload.url).hostname;
|
||||
} catch (err) {}
|
||||
if (host) {
|
||||
engine = (
|
||||
await lazy.UrlbarSearchUtils.enginesForDomainPrefix(host)
|
||||
)[0];
|
||||
}
|
||||
}
|
||||
if (!engine) {
|
||||
// No engine found.
|
||||
throw new Error("Invalid or missing engine specified by extension");
|
||||
}
|
||||
extResult.payload.engine = engine.name;
|
||||
}
|
||||
|
||||
let type = UrlbarProviderExtension.RESULT_TYPES[extResult.type];
|
||||
if (type == UrlbarUtils.RESULT_TYPE.TIP) {
|
||||
extResult.payload.type ||= "extension";
|
||||
extResult.payload.helpL10n = {
|
||||
id: "urlbar-result-menu-tip-get-help",
|
||||
};
|
||||
}
|
||||
|
||||
let result = new lazy.UrlbarResult(
|
||||
UrlbarProviderExtension.RESULT_TYPES[extResult.type],
|
||||
UrlbarProviderExtension.SOURCE_TYPES[extResult.source],
|
||||
...lazy.UrlbarResult.payloadAndSimpleHighlights(
|
||||
context.tokens,
|
||||
extResult.payload || {}
|
||||
)
|
||||
);
|
||||
if (extResult.heuristic && this.behavior == "restricting") {
|
||||
// The muxer chooses the final heuristic result by taking the first one
|
||||
// that claims to be the heuristic. We don't want extensions to clobber
|
||||
// the default heuristic, so we allow this only if the provider is
|
||||
// restricting.
|
||||
result.heuristic = extResult.heuristic;
|
||||
}
|
||||
if (extResult.suggestedIndex !== undefined) {
|
||||
result.suggestedIndex = extResult.suggestedIndex;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// Maps extension result type enums to internal result types.
|
||||
UrlbarProviderExtension.RESULT_TYPES = {
|
||||
dynamic: UrlbarUtils.RESULT_TYPE.DYNAMIC,
|
||||
keyword: UrlbarUtils.RESULT_TYPE.KEYWORD,
|
||||
omnibox: UrlbarUtils.RESULT_TYPE.OMNIBOX,
|
||||
remote_tab: UrlbarUtils.RESULT_TYPE.REMOTE_TAB,
|
||||
search: UrlbarUtils.RESULT_TYPE.SEARCH,
|
||||
tab: UrlbarUtils.RESULT_TYPE.TAB_SWITCH,
|
||||
tip: UrlbarUtils.RESULT_TYPE.TIP,
|
||||
url: UrlbarUtils.RESULT_TYPE.URL,
|
||||
};
|
||||
|
||||
// Maps extension source type enums to internal source types.
|
||||
UrlbarProviderExtension.SOURCE_TYPES = {
|
||||
bookmarks: UrlbarUtils.RESULT_SOURCE.BOOKMARKS,
|
||||
history: UrlbarUtils.RESULT_SOURCE.HISTORY,
|
||||
local: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
|
||||
network: UrlbarUtils.RESULT_SOURCE.OTHER_NETWORK,
|
||||
search: UrlbarUtils.RESULT_SOURCE.SEARCH,
|
||||
tabs: UrlbarUtils.RESULT_SOURCE.TABS,
|
||||
actions: UrlbarUtils.RESULT_SOURCE.ACTIONS,
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a copy of a query context stripped of non-serializable properties.
|
||||
* This is necessary because query contexts are passed to extensions where they
|
||||
* become `Query` objects, as defined in the urlbar extensions schema. The
|
||||
* WebExtensions framework automatically excludes serializable properties that
|
||||
* aren't defined in the schema, but it chokes on non-serializable properties.
|
||||
*
|
||||
* @param {UrlbarQueryContext} context
|
||||
* The query context.
|
||||
* @returns {object}
|
||||
* A copy of `context` with only serializable properties.
|
||||
*/
|
||||
function makeSerializable(context) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(context).filter(
|
||||
([key]) => !NONSERIALIZABLE_CONTEXT_PROPERTIES.has(key)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
@ -288,25 +288,6 @@ class ProvidersManager {
|
|||
return;
|
||||
}
|
||||
|
||||
// Update the behavior of extension providers.
|
||||
let updateBehaviorPromises = [];
|
||||
for (let provider of this.providers) {
|
||||
if (
|
||||
provider.type == lazy.UrlbarUtils.PROVIDER_TYPE.EXTENSION &&
|
||||
provider.name != "Omnibox"
|
||||
) {
|
||||
updateBehaviorPromises.push(
|
||||
provider.tryMethod("updateBehavior", queryContext)
|
||||
);
|
||||
}
|
||||
}
|
||||
if (updateBehaviorPromises.length) {
|
||||
await Promise.all(updateBehaviorPromises);
|
||||
if (query.canceled) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await query.start();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ export var UrlbarUtils = {
|
|||
REMOTE_TAB: 6,
|
||||
// An actionable message to help the user with their query.
|
||||
TIP: 7,
|
||||
// A type of result created at runtime, for example by an extension.
|
||||
// A type of result which layout is defined at runtime.
|
||||
DYNAMIC: 8,
|
||||
|
||||
// When you add a new type, also add its schema to
|
||||
|
|
@ -1424,6 +1424,8 @@ export var UrlbarUtils = {
|
|||
return selType === "oneoff" ? "search_shortcut_button" : "input_field";
|
||||
}
|
||||
|
||||
// While product doesn't use experimental addons anymore, tests may still do
|
||||
// for testing purposes.
|
||||
if (
|
||||
result.providerType === UrlbarUtils.PROVIDER_TYPE.EXTENSION &&
|
||||
result.providerName != "Omnibox"
|
||||
|
|
|
|||
|
|
@ -707,100 +707,3 @@ types.
|
|||
__ https://github.com/0c0w3/dynamic-result-type-extension
|
||||
__ https://github.com/mozilla-extensions/firefox-quick-suggest-weather/blob/master/src/background.js
|
||||
__ https://searchfox.org/mozilla-central/source/browser/components/urlbar/UrlbarProviderTabToSearch.sys.mjs
|
||||
|
||||
Appendix B: Using the WebExtensions API Directly
|
||||
------------------------------------------------
|
||||
|
||||
If you're developing an extension, the recommended way of using dynamic result
|
||||
types is to use the shim_, which abstracts away the differences between writing
|
||||
internal address bar code and extensions code. The `implementation steps`_ above
|
||||
apply to extensions as long as you're using the shim.
|
||||
|
||||
For completeness, in this section we'll document the WebExtensions APIs that the
|
||||
shim is built on. If you don't use the shim for some reason, then follow these
|
||||
steps instead. You'll see that each step above using the shim has an analogous
|
||||
step here.
|
||||
|
||||
The WebExtensions API schema is declared in `schema.json`_ and implemented in
|
||||
`api.js`_.
|
||||
|
||||
.. _schema.json: https://github.com/0c0w3/dynamic-result-type-extension/blob/master/src/experiments/urlbar/schema.json
|
||||
.. _api.js: https://github.com/0c0w3/dynamic-result-type-extension/blob/master/src/experiments/urlbar/api.js
|
||||
|
||||
1. Register the dynamic result type
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
First, register the new dynamic result type:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
browser.experiments.urlbar.addDynamicResultType(name, type);
|
||||
|
||||
``name`` is a string identifier for the new type. See step 1 in `Implementation
|
||||
Steps`_ for a description, which applies here, too.
|
||||
|
||||
``type`` is an object with metadata for the new type. Currently no metadata is
|
||||
supported, so this should be an empty object, which is the default value.
|
||||
|
||||
2. Register the view template
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Next, add the view template for the new type:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
browser.experiments.urlbar.addDynamicViewTemplate(name, viewTemplate);
|
||||
|
||||
See step 2 above for a description of the parameters.
|
||||
|
||||
3. Add WebExtension event listeners
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Add all the WebExtension event listeners you normally would in an address bar
|
||||
extension, including the two required listeners, ``onBehaviorRequested`` and
|
||||
and ``onResultsRequested``.
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
browser.urlbar.onBehaviorRequested.addListener(query => {
|
||||
return "active";
|
||||
}, providerName);
|
||||
|
||||
browser.urlbar.onResultsRequested.addListener(query => {
|
||||
let results = [
|
||||
// ...
|
||||
];
|
||||
return results;
|
||||
}, providerName);
|
||||
|
||||
See the address bar extensions__ document for help on the urlbar WebExtensions
|
||||
API.
|
||||
|
||||
__ https://firefox-source-docs.mozilla.org/browser/urlbar/experiments.html
|
||||
|
||||
4. Add an onViewUpdateRequested event listener
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
``onViewUpdateRequested`` is a WebExtensions event particular to dynamic result
|
||||
types. It's analogous to the ``getViewUpdate`` provider method described
|
||||
earlier.
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
browser.experiments.urlbar.onViewUpdateRequested.addListener((payload, idsByName) => {
|
||||
let viewUpdate = {
|
||||
// ...
|
||||
};
|
||||
return viewUpdate;
|
||||
});
|
||||
|
||||
Note that unlike ``getViewUpdate``, here the listener's first parameter is a
|
||||
result payload, not the result itself.
|
||||
|
||||
The listener should return a view update object.
|
||||
|
||||
5. Style the results
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This step is the same as step 5 above. Bundle a CSS file in your extension and
|
||||
declare it in the top-level ``stylesheet`` property of your view template.
|
||||
|
|
|
|||
|
|
@ -1,726 +0,0 @@
|
|||
Extensions & Experiments
|
||||
========================
|
||||
|
||||
This document describes address bar extensions and experiments: what they are,
|
||||
how to run them, how to write them, and the processes involved in each.
|
||||
|
||||
The primary purpose right now for writing address bar extensions is to run
|
||||
address bar experiments. But extensions are useful outside of experiments, and
|
||||
not all experiments use extensions.
|
||||
|
||||
Like all Firefox extensions, address bar extensions use the WebExtensions_
|
||||
framework.
|
||||
|
||||
.. _WebExtensions: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions
|
||||
|
||||
.. contents::
|
||||
:depth: 2
|
||||
|
||||
|
||||
WebExtensions
|
||||
-------------
|
||||
|
||||
**WebExtensions** is the name of Firefox's extension architecture. The "web"
|
||||
part of the name hints at the fact that Firefox extensions are built using Web
|
||||
technologies: JavaScript, HTML, CSS, and to a certain extent the DOM.
|
||||
|
||||
Individual extensions themselves often are referred to as *WebExtensions*. For
|
||||
clarity and conciseness, this document will refer to WebExtensions as
|
||||
*extensions*.
|
||||
|
||||
Why are we interested in extensions? Mainly because they're a powerful way to
|
||||
run experiments in Firefox. See Experiments_ for more on that. In addition, we'd
|
||||
also like to build up a robust set of APIs useful to extension authors, although
|
||||
right now the API can only be used by Mozilla extensions.
|
||||
|
||||
WebExtensions are introduced and discussed in detail on `MDN
|
||||
<WebExtensions_>`__. You'll need a lot of that knowledge in order to build
|
||||
address bar extensions.
|
||||
|
||||
Developing Address Bar Extensions
|
||||
---------------------------------
|
||||
|
||||
Overview
|
||||
~~~~~~~~
|
||||
|
||||
The address bar WebExtensions API currently lives in two API namespaces,
|
||||
``browser.urlbar`` and ``browser.experiments.urlbar``. The reason for this is
|
||||
historical and is discussed in the `Developing Address Bar Extension APIs`_
|
||||
section. As a consumer of the API, there are only two important things you need
|
||||
to know:
|
||||
|
||||
* There's no meaningful difference between the APIs of the two namespaces.
|
||||
Their kinds of functions, events, and properties are similar. You should
|
||||
think of the address bar API as one single API that happens to be split into
|
||||
two namespaces.
|
||||
|
||||
* However, there is a big difference between the two when it comes to setting up
|
||||
your extension to use them. This is discussed next.
|
||||
|
||||
The ``browser.urlbar`` API namespace is built into Firefox. It's a
|
||||
**privileged API**, which means that only Mozilla-signed and temporarily
|
||||
installed extensions can use it. The only thing your Mozilla extension needs to
|
||||
do in order to use it is to request the ``urlbar`` permission in its
|
||||
manifest.json, as illustrated `here <urlbarPermissionExample_>`__.
|
||||
|
||||
In contrast, the ``browser.experiments.urlbar`` API namespace is bundled inside
|
||||
your extension. APIs that are bundled inside extensions are called
|
||||
**experimental APIs**, and the extensions in which they're bundled are called
|
||||
**WebExtension experiments**. As with privileged APIs, experimental APIs are
|
||||
available only to Mozilla-signed and temporarily installed extensions.
|
||||
("WebExtension experiments" is a term of art and shouldn't be confused with the
|
||||
general notion of experiments that happen to use extensions.) For more on
|
||||
experimental APIs and WebExtension experiments, see the `WebExtensions API
|
||||
implementation documentation <webextAPIImplBasicsDoc_>`__.
|
||||
|
||||
Since ``browser.experiments.urlbar`` is bundled inside your extension, you'll
|
||||
need to include it in your extension's repo by doing the following:
|
||||
|
||||
1. The implementation consists of two files, api.js_ and schema.json_. In your
|
||||
extension repo, create a *experiments/urlbar* subdirectory and copy the
|
||||
files there. See `this repo`__ for an example.
|
||||
|
||||
2. Add the following ``experiment_apis`` key to your manifest.json (see here__
|
||||
for an example in context)::
|
||||
|
||||
"experiment_apis": {
|
||||
"experiments_urlbar": {
|
||||
"schema": "experiments/urlbar/schema.json",
|
||||
"parent": {
|
||||
"scopes": ["addon_parent"],
|
||||
"paths": [["experiments", "urlbar"]],
|
||||
"script": "experiments/urlbar/api.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
As mentioned, only Mozilla-signed and temporarily installed extensions can use
|
||||
these two API namespaces. For information on running the extensions you develop
|
||||
that use these namespaces, see `Running Address Bar Extensions`_.
|
||||
|
||||
.. _urlbarPermissionExample: https://github.com/0c0w3/urlbar-top-sites-experiment/blob/ac1517118bb7ee165fb9989834514b1082575c10/src/manifest.json#L24
|
||||
.. _webextAPIImplBasicsDoc: https://firefox-source-docs.mozilla.org/toolkit/components/extensions/webextensions/basics.html
|
||||
.. _api.js: https://searchfox.org/mozilla-central/source/browser/components/urlbar/tests/ext/api.js
|
||||
.. _schema.json: https://searchfox.org/mozilla-central/source/browser/components/urlbar/tests/ext/schema.json
|
||||
__ https://github.com/0c0w3/dynamic-result-type-extension/tree/master/src/experiments/urlbar
|
||||
__ https://github.com/0c0w3/dynamic-result-type-extension/blob/0987da4b259b9fcb139b31d771883a2f822712b5/src/manifest.json#L28
|
||||
|
||||
browser.urlbar
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Currently the only documentation for ``browser.urlbar`` is its `schema
|
||||
<urlbar.json_>`__. Fortunately WebExtension schemas are JSON and aren't too hard
|
||||
to read. If you need help understanding it, see the `WebExtensions API
|
||||
implementation documentation <webextAPIImplDoc_>`__.
|
||||
|
||||
For examples on using the API, see the Cookbook_ section.
|
||||
|
||||
.. _urlbar.json: https://searchfox.org/mozilla-central/source/browser/components/extensions/schemas/urlbar.json
|
||||
|
||||
browser.experiments.urlbar
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
As with ``browser.urlbar``, currently the only documentation for
|
||||
``browser.experiments.urlbar`` is its schema__. For examples on using the API,
|
||||
see the Cookbook_ section.
|
||||
|
||||
__ https://searchfox.org/mozilla-central/source/browser/components/urlbar/tests/ext/schema.json
|
||||
|
||||
Workflow
|
||||
~~~~~~~~
|
||||
|
||||
The web-ext_ command-line tool makes the extension-development workflow very
|
||||
simple. Simply start it with the *run* command, passing it the location of the
|
||||
Firefox binary you want to use. web-ext will launch your Firefox and remain
|
||||
running until you stop it, watching for changes you make to your extension's
|
||||
files. When it sees a change, it automatically reloads your extension — in
|
||||
Firefox, in the background — without your having to do anything. It's really
|
||||
nice.
|
||||
|
||||
The `web-ext documentation <web-ext commands_>`__ lists all its options, but
|
||||
here are some worth calling out for the *run* command:
|
||||
|
||||
``--browser-console``
|
||||
Automatically open the browser console when Firefox starts. Very useful for
|
||||
watching your extension's console logging. (Make sure "Show Content Messages"
|
||||
is checked in the console.)
|
||||
|
||||
``-p``
|
||||
This option lets you specify a path to a profile directory.
|
||||
|
||||
``--keep-profile-changes``
|
||||
Normally web-ext doesn't save any changes you make to the profile. Use this
|
||||
option along with ``-p`` to reuse the same profile again and again.
|
||||
|
||||
``--verbose``
|
||||
web-ext suppresses Firefox messages in the terminal unless you pass this
|
||||
option. If you've added some ``dump`` calls in Firefox because you're working
|
||||
on a new ``browser.urlbar`` API, for example, you won't see them without this.
|
||||
|
||||
web-ext also has a *build* command that packages your extension's files into a
|
||||
zip file. The following *build* options are useful:
|
||||
|
||||
``--overwrite-dest``
|
||||
Without this option, web-ext won't overwrite a zip file it previously created.
|
||||
|
||||
web-ext can load its configuration from your extension's package.json. That's
|
||||
the recommended way to configure it. Here's an example__.
|
||||
|
||||
Finally, web-ext can also sign extensions, but if you're developing your
|
||||
extension for an experiment, you'll use a different process for signing. See
|
||||
`The Experiment Development Process`_.
|
||||
|
||||
.. _web-ext: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Getting_started_with_web-ext
|
||||
.. _web-ext commands: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/web-ext_command_reference
|
||||
__ https://github.com/0c0w3/urlbar-top-sites-experiment/blob/6681a7126986bc2565d036b888cb5b8807397ce5/package.json#L7
|
||||
|
||||
Automated Tests
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
It's possible to write `browser chrome mochitests`_ for your extension the same
|
||||
way we write tests for Firefox. One of the example extensions linked throughout
|
||||
this document includes a test_, for instance.
|
||||
|
||||
See the readme in the example-addon-experiment_ repo for a workflow.
|
||||
|
||||
.. _browser chrome mochitests: https://developer.mozilla.org/en-US/docs/Mozilla/Browser_chrome_tests
|
||||
.. _test: https://github.com/0c0w3/urlbar-top-sites-experiment/blob/master/tests/tests/browser/browser_urlbarTopSitesExtension.js
|
||||
|
||||
Cookbook
|
||||
~~~~~~~~
|
||||
|
||||
*To be written.* For now, you can find example uses of ``browser.experiments.urlbar`` and ``browser.urlbar`` in the following repos:
|
||||
|
||||
* https://github.com/mozilla-extensions/firefox-quick-suggest-weather
|
||||
* https://github.com/0c0w3/urlbar-tips-experiment
|
||||
* https://github.com/0c0w3/urlbar-top-sites-experiment
|
||||
* https://github.com/0c0w3/urlbar-search-interventions-experiment
|
||||
|
||||
Further Reading
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
`WebExtensions on MDN <WebExtensions_>`__
|
||||
The place to learn about developing WebExtensions in general.
|
||||
|
||||
`Getting started with web-ext <web-ext_>`__
|
||||
MDN's tutorial on using web-ext.
|
||||
|
||||
`web-ext command reference <web-ext commands_>`__
|
||||
MDN's documentation on web-ext's commands and their options.
|
||||
|
||||
Developing Address Bar Extension APIs
|
||||
-------------------------------------
|
||||
|
||||
Built-In APIs vs. Experimental APIs
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Originally we developed the address bar extension API in the ``browser.urlbar``
|
||||
namespace, which is built into Firefox as discussed above. By "built into
|
||||
Firefox," we mean that the API is developed in `mozilla-central
|
||||
<urlbar.json_>`__ and shipped inside Firefox just like any other Firefox
|
||||
feature. At the time, that seemed like the right thing to do because we wanted
|
||||
to build an API that ultimately could be used by all extension authors, not only
|
||||
Mozilla.
|
||||
|
||||
However, there were a number of disadvantages to this development model. The
|
||||
biggest was that it tightly coupled our experiments to specific versions of
|
||||
Firefox. For example, if we were working on an experiment that targeted Firefox
|
||||
72, then any APIs used by that experiment needed to land and ship in 72. If we
|
||||
weren't able to finish an API by the time 72 shipped, then the experiment would
|
||||
have to be postponed until 73. Our experiment development timeframes were always
|
||||
very short because we always wanted to ship our experiments ASAP. Often we
|
||||
targeted the Firefox version that was then in Nightly; sometimes we even
|
||||
targeted the version in Beta. Either way, it meant that we were always uplifting
|
||||
patch after patch to Beta. This tight coupling between Firefox versions and
|
||||
experiments erased what should have been a big advantage of implementing
|
||||
experiments as extensions in the first place: the ability to ship experiments
|
||||
outside the usual cyclical release process.
|
||||
|
||||
Another notable disadvantage of this model was just the cognitive weight of the
|
||||
idea that we were developing APIs not only for ourselves and our experiments but
|
||||
potentially for all extensions. This meant that not only did we have to design
|
||||
APIs to meet our immediate needs, we also had to imagine use cases that could
|
||||
potentially arise and then design for them as well.
|
||||
|
||||
For these reasons, we stopped developing ``browser.urlbar`` and created the
|
||||
``browser.experiments.urlbar`` experimental API. As discussed earlier,
|
||||
experimental APIs are APIs that are bundled inside extensions. Experimental APIs
|
||||
can do anything that built-in APIs can do with the added flexibility of not
|
||||
being tied to specific versions of Firefox.
|
||||
|
||||
Adding New APIs
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
All new address bar APIs should be added to ``browser.experiments.urlbar``.
|
||||
Although this API does not ship in Firefox, it's currently developed in
|
||||
mozilla-central, in `browser/components/urlbar/tests/ext/ <extDirectory_>`__ --
|
||||
note the "tests" subdirectory. Developing it in mozilla-central lets us take
|
||||
advantage of our usual build and testing infrastructure. This way we have API
|
||||
tests running against each mozilla-central checkin, against all versions of
|
||||
Firefox that are tested on Mozilla's infrastructure, and we're alerted to any
|
||||
breaking changes we accidentally make. When we start a new extension repo, we
|
||||
copy schema.json and api.js to it as described earlier (or clone an example repo
|
||||
with up-to-date copies of these files).
|
||||
|
||||
Generally changes to the API should be reviewed by someone on the address bar
|
||||
team and someone on the WebExtensions team. Shane (mixedpuppy) is a good
|
||||
contact.
|
||||
|
||||
.. _extDirectory: https://searchfox.org/mozilla-central/source/browser/components/urlbar/tests/ext/
|
||||
|
||||
Anatomy of an API
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
Roughly speaking, a WebExtensions API implementation comprises three different
|
||||
pieces:
|
||||
|
||||
Schema
|
||||
The schema declares the functions, properties, events, and types that the API
|
||||
makes available to extensions. Schemas are written in JSON.
|
||||
|
||||
The ``browser.experiments.urlbar`` schema is schema.json_, and the
|
||||
``browser.urlbar`` schema is urlbar.json_.
|
||||
|
||||
For reference, the schemas of built-in APIs are in
|
||||
`browser/components/extensions/schemas`_ and
|
||||
`toolkit/components/extensions/schemas`_.
|
||||
|
||||
.. _browser/components/extensions/schemas: https://searchfox.org/mozilla-central/source/browser/components/extensions/schemas/
|
||||
.. _toolkit/components/extensions/schemas: https://searchfox.org/mozilla-central/source/toolkit/components/extensions/schemas/
|
||||
|
||||
Internals
|
||||
Every API hooks into some internal part of Firefox. For the address bar API,
|
||||
that's the Urlbar implementation in `browser/components/urlbar`_.
|
||||
|
||||
.. _browser/components/urlbar: https://searchfox.org/mozilla-central/source/browser/components/urlbar/
|
||||
|
||||
Glue
|
||||
Finally, there's some glue code that implements everything declared in the
|
||||
schema. Essentially, this code mediates between the previous two pieces. It
|
||||
translates the function calls, property accesses, and event listener
|
||||
registrations made by extensions using the public-facing API into terms that
|
||||
the Firefox internals understand, and vice versa.
|
||||
|
||||
For ``browser.experiments.urlbar``, this is api.js_, and for
|
||||
``browser.urlbar``, it's ext-urlbar.js_.
|
||||
|
||||
For reference, the implementations of built-in APIs are in
|
||||
`browser/components/extensions`_ and `toolkit/components/extensions`_, in the
|
||||
*parent* and *child* subdirecties. As you might guess, code in *parent* runs
|
||||
in the main process, and code in *child* runs in the extensions process.
|
||||
Address bar APIs deal with browser chrome and their implementations therefore
|
||||
run in the parent process.
|
||||
|
||||
.. _ext-urlbar.js: https://searchfox.org/mozilla-central/source/browser/components/extensions/parent/ext-urlbar.js
|
||||
.. _browser/components/extensions: https://searchfox.org/mozilla-central/source/browser/components/extensions/
|
||||
.. _toolkit/components/extensions: https://searchfox.org/mozilla-central/source/toolkit/components/extensions/
|
||||
|
||||
Keep in mind that extensions run in a different process from the main process.
|
||||
That has implications for your APIs. They'll generally need to be async, for
|
||||
example.
|
||||
|
||||
Further Reading
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
`WebExtensions API implementation documentation <webextAPIImplDoc_>`__
|
||||
Detailed info on implementing a WebExtensions API.
|
||||
|
||||
.. _webextAPIImplDoc: https://firefox-source-docs.mozilla.org/toolkit/components/extensions/webextensions/
|
||||
|
||||
Running Address Bar Extensions
|
||||
------------------------------
|
||||
|
||||
As discussed above, ``browser.experiments.urlbar`` and ``browser.urlbar`` are
|
||||
privileged APIs. There are two different points to consider when it comes to
|
||||
running an extension that uses privileged APIs: loading the extension in the
|
||||
first place, and granting it access to privileged APIs. There's a certain bar
|
||||
for loading any extension regardless of its API usage that depends on its signed
|
||||
state and the Firefox build you want to run it in. There's yet a higher bar for
|
||||
granting it access to privileged APIs. This section discusses how to load
|
||||
extensions so that they can access privileged APIs.
|
||||
|
||||
Since we're interested in extensions primarily for running experiments, there
|
||||
are three particular signed states relevant to us:
|
||||
|
||||
Unsigned
|
||||
There are two ways to run unsigned extensions that use privileged APIs.
|
||||
|
||||
They can be loaded temporarily using a Firefox Nightly build or
|
||||
Developer Edition but not Beta or Release [source__], and the
|
||||
``extensions.experiments.enabled`` preference must be set to true [source__].
|
||||
You can load extensions temporarily by visiting
|
||||
about:debugging#/runtime/this-firefox and clicking "Load Temporary Add-on."
|
||||
`web-ext <Workflow_>`__ also loads extensions temporarily.
|
||||
|
||||
__ https://searchfox.org/mozilla-central/rev/053826b10f838f77c27507e5efecc96e34718541/toolkit/components/extensions/Extension.jsm#1884
|
||||
__ https://searchfox.org/mozilla-central/rev/014fe72eaba26dcf6082fb9bbaf208f97a38594e/toolkit/mozapps/extensions/internal/AddonSettings.jsm#93
|
||||
|
||||
They can be also be loaded normally (not temporarily) in a custom build where
|
||||
the build-time setting ``AppConstants.MOZ_REQUIRE_SIGNING`` [source__, source__]
|
||||
and ``xpinstall.signatures.required`` pref are both false. As in the previous
|
||||
paragraph, such builds include Nightly and Developer Edition but not Beta or
|
||||
Release [source__]. In addition, your custom build must modify the
|
||||
``Extension.isPrivileged`` getter__ to return true. This getter determines
|
||||
whether an extension can access privileged APIs.
|
||||
|
||||
__ https://searchfox.org/mozilla-central/rev/053826b10f838f77c27507e5efecc96e34718541/toolkit/mozapps/extensions/internal/XPIProvider.jsm#2382
|
||||
__ https://searchfox.org/mozilla-central/rev/053826b10f838f77c27507e5efecc96e34718541/toolkit/mozapps/extensions/internal/AddonSettings.jsm#36
|
||||
__ https://searchfox.org/mozilla-central/search?q=MOZ_REQUIRE_SIGNING&case=false®exp=false&path=
|
||||
__ https://searchfox.org/mozilla-central/rev/053826b10f838f77c27507e5efecc96e34718541/toolkit/components/extensions/Extension.jsm#1874
|
||||
|
||||
Extensions remain unsigned as you develop them. See the Workflow_ section for
|
||||
more.
|
||||
|
||||
Signed for testing (Signed for QA)
|
||||
Signed-for-testing extensions that use privileged APIs can be run using the
|
||||
same techniques for running unsigned extensions.
|
||||
|
||||
They can also be loaded normally (not temporarily) if you use a Firefox build
|
||||
where the build-time setting ``AppConstants.MOZ_REQUIRE_SIGNING`` is false and
|
||||
you set the ``xpinstall.signatures.dev-root`` pref to true
|
||||
[source__]. ``xpinstall.signatures.dev-root`` does not exist by default and
|
||||
must be created.
|
||||
|
||||
__ https://searchfox.org/mozilla-central/rev/053826b10f838f77c27507e5efecc96e34718541/toolkit/mozapps/extensions/internal/XPIInstall.jsm#262
|
||||
|
||||
You encounter extensions that are signed for testing when you are writing
|
||||
extensions for experiments. See the Experiments_ section for details.
|
||||
|
||||
"Signed for QA" is another way of referring to this signed state.
|
||||
|
||||
Signed for release
|
||||
Signed-for-release extensions that use privileged APIs can be run in any
|
||||
Firefox build with no special requirements.
|
||||
|
||||
You encounter extensions that are signed for release when you are writing
|
||||
extensions for experiments. See the Experiments_ section for details.
|
||||
|
||||
.. important::
|
||||
To see console logs from extensions in the browser console, select the "Show
|
||||
Content Messages" option in the console's settings. This is necessary because
|
||||
extensions run outside the main process.
|
||||
|
||||
Experiments
|
||||
-----------
|
||||
|
||||
**Experiments** let us try out ideas in Firefox outside the usual release cycle
|
||||
and on particular populations of users.
|
||||
|
||||
For example, say we have a hunch that the top sites shown on the new-tab page
|
||||
aren't very discoverable, so we want to make them more visible. We have one idea
|
||||
that might work — show them every time the user begins an interaction with the
|
||||
address bar — but we aren't sure how good an idea it is. So we test it. We write
|
||||
an extension that does just that, make sure it collects telemetry that will help
|
||||
us answer our question, ship it outside the usual release cycle to a small
|
||||
percentage of Beta users, collect and analyze the telemetry, and determine
|
||||
whether the experiment was successful. If it was, then we might want to ship the
|
||||
feature to all Firefox users.
|
||||
|
||||
Experiments sometimes are also called **studies** (not to be confused with *user
|
||||
studies*, which are face-to-face interviews with users conducted by user
|
||||
researchers).
|
||||
|
||||
There are two types of experiments:
|
||||
|
||||
Pref-flip experiments
|
||||
Pref-flip experiments are simple. If we have a fully baked feature in the
|
||||
browser that's preffed off, a pref-flip experiment just flips the pref on,
|
||||
enabling the feature for users running the experiment. No code is required.
|
||||
We tell the experiments team the name of the pref we want to flip, and they
|
||||
handle it.
|
||||
|
||||
One important caveat to pref-flip studies is that they're currently capable of
|
||||
flipping only a single pref. There's an extension called Multipreffer_ that
|
||||
can flip multiple prefs, though.
|
||||
|
||||
.. _Multipreffer: https://github.com/mozilla/multipreffer
|
||||
|
||||
Add-on experiments
|
||||
Add-on experiments are much more complex but much more powerful. (Here
|
||||
*add-on* is a synonym for extension.) They're the type of experiments that
|
||||
this document has been discussing all along.
|
||||
|
||||
An add-on experiment is shipped as an extension that we write and that
|
||||
implements the experimental feature we want to test. To reiterate, the
|
||||
extension is a WebExtension and uses WebExtensions APIs. If the current
|
||||
WebExtensions APIs do not meet the needs of your experiment, then you must
|
||||
create either experimental or built-in APIs so that your extension can use
|
||||
them. If necessary, you can make any new built-in APIs privileged so that they
|
||||
are available only to Mozilla extensions.
|
||||
|
||||
An add-on experiment can collect additional telemetry that's not collected in
|
||||
the product by using the privileged ``browser.telemetry`` WebExtensions API,
|
||||
and of course the product will continue to collect all the telemetry it
|
||||
usually does. The telemetry pings from users running the experiment will be
|
||||
correlated with the experiment with no extra work on our part.
|
||||
|
||||
A single experiment can deliver different UXes to different groups of users
|
||||
running the experiment. Each group or UX within an experiment is called a
|
||||
**branch**. Experiments often have two branches, control and treatment. The
|
||||
**control branch** actually makes no UX changes. It may capture additional
|
||||
telemetry, though. Think of it as the control in a science experiment. It's
|
||||
there so we can compare it to data from the **treatment branch**, which does
|
||||
make UX changes. Some experiments may require multiple treatment branches, in
|
||||
which case the different branches will have different names. Add-on experiments
|
||||
can implement all branches in the same extension or each branch in its own
|
||||
extension.
|
||||
|
||||
Experiments are delivered to users by a system called **Normandy**. Normandy
|
||||
comprises a client side that lives in Firefox and a server side. In Normandy,
|
||||
experiments are defined server-side in files called **recipes**. Recipes include
|
||||
information about the experiment like the Firefox release channel and version
|
||||
that the experiment targets, the number of users to be included in the
|
||||
experiment, the branches in the experiment, the percentage of users on each
|
||||
branch, and so on.
|
||||
|
||||
Experiments are tracked by Mozilla project management using a system called
|
||||
Experimenter_.
|
||||
|
||||
Finally, there was an older version of the experiments program called
|
||||
**Shield**. Experiments under this system were called **Shield studies** and
|
||||
could be be shipped as extensions too.
|
||||
|
||||
.. _Experimenter: https://experimenter.services.mozilla.com/
|
||||
|
||||
Further Reading
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
`Pref-Flip and Add-On Experiments <https://mana.mozilla.org/wiki/pages/viewpage.action?spaceKey=FIREFOX&title=Pref-Flip+and+Add-On+Experiments>`__
|
||||
A comprehensive document on experiments from the Experimenter team. See the
|
||||
child pages in the sidebar, too.
|
||||
|
||||
`Client Implementation Guidelines for Experiments <https://docs.telemetry.mozilla.org/cookbooks/client_guidelines.html>`_
|
||||
Relevant documentation from the telemetry team.
|
||||
|
||||
#ask-experimenter Slack channel
|
||||
A friendly place to get answers to your experiment questions.
|
||||
|
||||
The Experiment Development Process
|
||||
----------------------------------
|
||||
|
||||
This section describes an experiment's life cycle.
|
||||
|
||||
1. Experiments usually originate with product management and UX. They're
|
||||
responsible for identifying a problem, deciding how an experiment should
|
||||
approach it, the questions we want to answer, the data we need to answer
|
||||
those questions, the user population that should be enrolled in the
|
||||
experiment, the definition of success, and so on.
|
||||
|
||||
2. UX makes a spec that describes what the extension looks like and how it
|
||||
behaves.
|
||||
|
||||
3. There's a kickoff meeting among the team to introduce the experiment and UX
|
||||
spec. It's an opportunity for engineering to ask questions of management, UX,
|
||||
and data science. It's really important for engineering to get a precise and
|
||||
accurate understanding of how the extension is supposed to behave — right
|
||||
down to the UI changes — so that no one makes erroneous assumptions during
|
||||
development.
|
||||
|
||||
4. At some point around this time, the team (usually management) creates a few
|
||||
artifacts to track the work and facilitate communication with outside teams
|
||||
involved in shipping experiments. They include:
|
||||
|
||||
* A page on `Experimenter <Experiments_>`__
|
||||
* A QA PI (product integrity) request so that QA resources are allocated
|
||||
* A bug in `Data Science :: Experiment Collaboration`__ so that data science
|
||||
can track the work and discuss telemetry (engineering might file this one)
|
||||
|
||||
__ https://bugzilla.mozilla.org/enter_bug.cgi?assigned_to=nobody%40mozilla.org&bug_ignored=0&bug_severity=normal&bug_status=NEW&bug_type=task&cf_firefox_messaging_system=---&cf_fx_iteration=---&cf_fx_points=---&comment=%23%23%20Brief%20Description%20of%20the%20request%20%28required%29%3A%0D%0A%0D%0A%23%23%20Business%20purpose%20for%20this%20request%20%28required%29%3A%0D%0A%0D%0A%23%23%20Requested%20timelines%20for%20the%20request%20or%20how%20this%20fits%20into%20roadmaps%20or%20critical%20decisions%20%28required%29%3A%0D%0A%0D%0A%23%23%20Links%20to%20any%20assets%20%28e.g%20Start%20of%20a%20PHD%2C%20BRD%3B%20any%20document%20that%20helps%20describe%20the%20project%29%3A%0D%0A%0D%0A%23%23%20Name%20of%20Data%20Scientist%20%28If%20Applicable%29%3A%0D%0A%0D%0A%2APlease%20note%20if%20it%20is%20found%20that%20not%20enough%20information%20has%20been%20given%20this%20will%20delay%20the%20triage%20of%20this%20request.%2A&component=Experiment%20Collaboration&contenttypemethod=list&contenttypeselection=text%2Fplain&filed_via=standard_form&flag_type-4=X&flag_type-607=X&flag_type-800=X&flag_type-803=X&flag_type-936=X&form_name=enter_bug&maketemplate=Remember%20values%20as%20bookmarkable%20template&op_sys=Unspecified&priority=--&product=Data%20Science&rep_platform=Unspecified&target_milestone=---&version=unspecified
|
||||
|
||||
5. Engineering breaks down the work and files bugs. There's another engineering
|
||||
meeting to discuss the breakdown, or it's discussed asynchronously.
|
||||
|
||||
6. Engineering sets up a GitHub repo for the extension. See `Implementing
|
||||
Experiments`_ for an example repo you can clone to get started. Disable
|
||||
GitHub Issues on the repo so that QA will file bugs in Bugzilla instead of
|
||||
GitHub. There's nothing wrong with GitHub Issues, but our team's project
|
||||
management tracks all work through Bugzilla. If it's not there, it's not
|
||||
captured.
|
||||
|
||||
7. Engineering or management fills out the Add-on section of the Experimenter
|
||||
page as much as possible at this point. "Active Experiment Name" isn't
|
||||
necessary, and "Signed Release URL" won't be available until the end of the
|
||||
process.
|
||||
|
||||
8. Engineering implements the extension and any new WebExtensions APIs it
|
||||
requires.
|
||||
|
||||
9. When the extension is done, engineering or management clicks the "Ready for
|
||||
Sign-Off" button on the Experimenter page. That changes the page's status
|
||||
from "Draft" to "Ready for Sign-Off," which allows QA and other teams to sign
|
||||
off on their portions of the experiment.
|
||||
|
||||
10. Engineering requests the extension be signed "for testing" (or "for
|
||||
QA"). Michael (mythmon) from the Experiments team and Rehan (rdalal) from
|
||||
Services Engineering are good contacts. Build the extension zip file using
|
||||
web-ext as discussed in Workflow_. Attach it to a bug (a metabug for
|
||||
implementing the extension, for example), needinfo Michael or Rehan, and ask
|
||||
him to sign it. He'll attach the signed version to the bug. If neither
|
||||
Michael nor Rehan is available, try asking in the #ask-experimenter Slack
|
||||
channel.
|
||||
|
||||
11. Engineering sends QA the link to the signed extension and works with them to
|
||||
resolve bugs they find.
|
||||
|
||||
12. When QA signs off, engineering asks Michael to sign the extension "for
|
||||
release" using the same needinfo process described earlier.
|
||||
|
||||
13. Paste the URL of the signed extension in the "Signed Release URL" textbox of
|
||||
the Add-on section of the Experimenter page.
|
||||
|
||||
14. Other teams sign off as they're ready.
|
||||
|
||||
15. The experiment ships! 🎉
|
||||
|
||||
|
||||
Implementing Experiments
|
||||
------------------------
|
||||
|
||||
This section discusses how to implement add-on experiments. Pref-flip
|
||||
experiments are much simpler and don't need a lot of explanation. You should be
|
||||
familiar with the concepts discussed in the `Developing Address Bar Extensions`_
|
||||
and `Running Address Bar Extensions`_ sections before reading this one.
|
||||
|
||||
The most salient thing about add-on experiments is that they're implemented
|
||||
simply as privileged extensions. Other than being privileged and possibly
|
||||
containing bundled experimental APIs, they're similar to all other extensions.
|
||||
|
||||
The `top-sites experiment extension <topSites_>`__ is an example of a real,
|
||||
shipped experiment.
|
||||
|
||||
.. _topSites: https://github.com/0c0w3/urlbar-top-sites-experiment
|
||||
|
||||
Setup
|
||||
~~~~~
|
||||
|
||||
example-addon-experiment_ is a repo you can clone to get started. It's geared
|
||||
toward urlbar extensions and includes the stub of a browser chrome mochitest.
|
||||
|
||||
.. _example-addon-experiment: https://github.com/0c0w3/example-addon-experiment
|
||||
|
||||
browser.normandyAddonStudy
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
As discussed in Experiments_, an experiment typically has more than one branch
|
||||
so that it can test different UXes. The experiment's extension(s) needs to know
|
||||
the branch the user is enrolled in so that it can behave appropriately for the
|
||||
branch: show the user the proper UX, collect the proper telemetry, and so on.
|
||||
|
||||
This is the purpose of the ``browser.normandyAddonStudy`` WebExtensions API.
|
||||
Like ``browser.urlbar``, it's a privileged API available only to Mozilla
|
||||
extensions.
|
||||
|
||||
Its schema is normandyAddonStudy.json_.
|
||||
|
||||
It's a very simple API. The primary function is ``getStudy``, which returns the
|
||||
study the user is currently enrolled in or null if there isn't one. (Recall that
|
||||
*study* is a synonym for *experiment*.) One of the first things an experiment
|
||||
extension typically does is to call this function.
|
||||
|
||||
The Normandy client in Firefox will keep an experiment extension installed only
|
||||
while the experiment is active. Therefore, ``getStudy`` should always return a
|
||||
non-null study object. Nevertheless, the study object has an ``active`` boolean
|
||||
property that's trivial to sanity check. (The example extension does.)
|
||||
|
||||
The more important property is ``branch``, the name of the branch that the user
|
||||
is enrolled in. Your extension should use it to determine the appropriate UX.
|
||||
|
||||
Finally, there's an ``onUnenroll`` event that's fired when the user is
|
||||
unenrolled in the study. It's not quite clear in what cases an extension would
|
||||
need to listen for this event given that Normandy automatically uninstalls
|
||||
extensions on unenrollment. Maybe if they create some persistent state that's
|
||||
not automatically undone on uninstall by the WebExtensions framework?
|
||||
|
||||
If your extension itself needs to unenroll the user for some reason, call
|
||||
``endStudy``.
|
||||
|
||||
.. _normandyAddonStudy.json: https://searchfox.org/mozilla-central/source/browser/components/extensions/schemas/normandyAddonStudy.json
|
||||
|
||||
Telemetry
|
||||
~~~~~~~~~
|
||||
|
||||
Experiments can capture telemetry in two places: in the product itself and
|
||||
through the privileged ``browser.telemetry`` WebExtensions API. The API schema
|
||||
is telemetry.json_.
|
||||
|
||||
The telemetry pings from users running experiments are automatically correlated
|
||||
with those experiments, no extra work required. That's true regardless of
|
||||
whether the telemetry is captured in the product or though
|
||||
``browser.telemetry``.
|
||||
|
||||
The address bar has some in-product, preffed off telemetry that we want to
|
||||
enable for all our experiments — at least that's the thinking as of August 2019.
|
||||
It's called `engagement event telemetry`_, and it records user *engagements*
|
||||
with and *abandonments* of the address bar [source__]. We added a
|
||||
BrowserSetting_ on ``browser.urlbar`` just to let us flip the pref and enable
|
||||
this telemetry in our experiment extensions. Call it like this::
|
||||
|
||||
await browser.urlbar.engagementTelemetry.set({ value: true });
|
||||
|
||||
.. _telemetry.json: https://searchfox.org/mozilla-central/source/toolkit/components/extensions/schemas/telemetry.json
|
||||
.. _engagement event telemetry: https://bugzilla.mozilla.org/show_bug.cgi?id=1559136
|
||||
__ https://searchfox.org/mozilla-central/rev/7088fc958db5935eba24b413b1f16d6ab7bd13ea/browser/components/urlbar/UrlbarController.jsm#598
|
||||
.. _BrowserSetting: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/types/BrowserSetting
|
||||
|
||||
Engineering Best Practices
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Clear up questions with your UX person early and often. There's often a gap
|
||||
between what they have in their mind and what you have in yours. Nothing wrong
|
||||
with that, it's just the nature of development. But misunderstandings can cause
|
||||
big problems when they're discovered late. This is especially true of UX
|
||||
behaviors, as opposed to visuals or styling. It's no fun to realize at the end
|
||||
of a release cycle that you've designed the wrong WebExtensions API because some
|
||||
UX detail was overlooked.
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Related to the previous point, make builds of your extension for your UX person
|
||||
so they can test it.
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Taking the previous point even further, if your experiment will require a
|
||||
substantial new API(s), you might think about prototyping the experiment
|
||||
entirely in a custom Firefox build before designing the API at all. Give it to
|
||||
your UX person. Let them disect it and tell you all the problems with it. Fill
|
||||
in all the gaps in your understanding, and then design the API. We've never
|
||||
actually done this, though.
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
It's a good idea to work on the extension as you're designing and developing the
|
||||
APIs it'll use. You might even go as far as writing the first draft of the
|
||||
extension before even starting to implement the APIs. That lets you spot
|
||||
problems that may not be obvious were you to design the API in isolation.
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Your extension's ID should end in ``@shield.mozilla.org``. QA will flag it if it
|
||||
doesn't.
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Set ``"hidden": true`` in your extension's manifest.json. That hides it on
|
||||
about:addons. (It can still be seen on about:studies.) QA will spot this if you
|
||||
don't.
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
There are drawbacks of hiding features behind prefs and enabling them in
|
||||
experiment extensions. Consider not doing that if feasible, or at least weigh
|
||||
these drawbacks against your expected benefits.
|
||||
|
||||
* Prefs stay flipped on in private windows, but experiments often have special
|
||||
requirements around private-browsing mode (PBM). Usually, they shouldn't be
|
||||
active in PBM at all, unless of course the point of the experiment is to test
|
||||
PBM. Extensions also must request PBM access ("incognito" in WebExtensions
|
||||
terms), and the user can disable access at any time. The result is that part
|
||||
of your experiment could remain enabled — the part behind the pref — while
|
||||
other parts are disabled.
|
||||
|
||||
* Prefs stay flipped on in safe mode, even though your extension (like all
|
||||
extensions) will be disabled. This might be a bug__ in the WebExtensions
|
||||
framework, though.
|
||||
|
||||
__ https://bugzilla.mozilla.org/show_bug.cgi?id=1576997
|
||||
|
|
@ -40,7 +40,6 @@ Table of Contents
|
|||
firefox-suggest-telemetry
|
||||
debugging
|
||||
ranking
|
||||
experiments
|
||||
dynamic-result-types
|
||||
preferences
|
||||
testing
|
||||
|
|
|
|||
|
|
@ -400,6 +400,6 @@ The following RESULT_TYPEs are supported:
|
|||
// An actionable message to help the user with their query.
|
||||
// Payload: { buttons, helpL10n, helpUrl, icon, titleL10n, type }
|
||||
TIP: 7,
|
||||
// A type of result created at runtime, for example by an extension.
|
||||
// A type of result which layout is defined at runtime.
|
||||
// Payload: { dynamicType }
|
||||
DYNAMIC: 8,
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ EXTRA_JS_MODULES += [
|
|||
"UrlbarProviderCalculator.sys.mjs",
|
||||
"UrlbarProviderClipboard.sys.mjs",
|
||||
"UrlbarProviderContextualSearch.sys.mjs",
|
||||
"UrlbarProviderExtension.sys.mjs",
|
||||
"UrlbarProviderHeuristicFallback.sys.mjs",
|
||||
"UrlbarProviderHistoryUrlHeuristic.sys.mjs",
|
||||
"UrlbarProviderInputHistory.sys.mjs",
|
||||
|
|
@ -84,7 +83,6 @@ BROWSER_CHROME_MANIFESTS += [
|
|||
"tests/browser-updateResults/browser.toml",
|
||||
"tests/browser/browser.toml",
|
||||
"tests/engagementTelemetry/browser/browser.toml",
|
||||
"tests/ext/browser/browser.toml",
|
||||
"tests/quicksuggest/browser/browser.toml",
|
||||
]
|
||||
XPCSHELL_TESTS_MANIFESTS += [
|
||||
|
|
|
|||
|
|
@ -13,9 +13,6 @@ support-files = [
|
|||
"head-sap.js",
|
||||
"head-search_mode.js",
|
||||
"../../browser-tips/head.js",
|
||||
"../../ext/browser/head.js",
|
||||
"../../ext/api.js",
|
||||
"../../ext/schema.json",
|
||||
]
|
||||
prefs = ["browser.bookmarks.testing.skipDefaultBookmarksImport=true"]
|
||||
|
||||
|
|
|
|||
|
|
@ -5,11 +5,6 @@
|
|||
|
||||
// Test edge cases for engagement.
|
||||
|
||||
Services.scriptloader.loadSubScript(
|
||||
"chrome://mochitests/content/browser/browser/components/urlbar/tests/ext/browser/head.js",
|
||||
this
|
||||
);
|
||||
|
||||
add_setup(async function () {
|
||||
await setup();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -18,11 +18,6 @@ ChromeUtils.defineESModuleGetters(this, {
|
|||
// This test has many subtests and can time out in verify mode.
|
||||
requestLongerTimeout(5);
|
||||
|
||||
Services.scriptloader.loadSubScript(
|
||||
"chrome://mochitests/content/browser/browser/components/urlbar/tests/ext/browser/head.js",
|
||||
this
|
||||
);
|
||||
|
||||
add_setup(async function () {
|
||||
await setup();
|
||||
});
|
||||
|
|
@ -540,73 +535,6 @@ add_task(async function selected_result_site_specific_contextual_search() {
|
|||
await SpecialPowers.popPrefEnv();
|
||||
});
|
||||
|
||||
add_task(async function selected_result_experimental_addon() {
|
||||
const extension = await loadExtension({
|
||||
background: async () => {
|
||||
browser.experiments.urlbar.addDynamicResultType("testDynamicType");
|
||||
browser.experiments.urlbar.addDynamicViewTemplate("testDynamicType", {
|
||||
children: [
|
||||
{
|
||||
name: "text",
|
||||
tag: "span",
|
||||
attributes: {
|
||||
role: "button",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
browser.urlbar.onBehaviorRequested.addListener(query => {
|
||||
return "active";
|
||||
}, "testProvider");
|
||||
browser.urlbar.onResultsRequested.addListener(query => {
|
||||
return [
|
||||
{
|
||||
type: "dynamic",
|
||||
source: "local",
|
||||
payload: {
|
||||
dynamicType: "testDynamicType",
|
||||
},
|
||||
},
|
||||
];
|
||||
}, "testProvider");
|
||||
browser.experiments.urlbar.onViewUpdateRequested.addListener(payload => {
|
||||
return {
|
||||
text: {
|
||||
textContent: "This is a dynamic result.",
|
||||
},
|
||||
};
|
||||
}, "testProvider");
|
||||
},
|
||||
});
|
||||
|
||||
await TestUtils.waitForCondition(
|
||||
() =>
|
||||
UrlbarProvidersManager.getProvider("testProvider") &&
|
||||
UrlbarResult.getDynamicResultType("testDynamicType"),
|
||||
"Waiting for provider and dynamic type to be registered"
|
||||
);
|
||||
|
||||
await doTest(async browser => {
|
||||
await openPopup("test");
|
||||
EventUtils.synthesizeKey("KEY_ArrowDown");
|
||||
await UrlbarTestUtils.promisePopupClose(window, () =>
|
||||
EventUtils.synthesizeKey("KEY_Enter")
|
||||
);
|
||||
|
||||
assertEngagementTelemetry([
|
||||
{
|
||||
selected_result: "experimental_addon",
|
||||
selected_result_subtype: "",
|
||||
selected_position: 2,
|
||||
provider: "testProvider",
|
||||
results: "search_engine,experimental_addon",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
await extension.unload();
|
||||
});
|
||||
|
||||
add_task(async function selected_result_rs_adm_sponsored() {
|
||||
const cleanupQuickSuggest = await ensureQuickSuggestInit();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,266 +0,0 @@
|
|||
/* 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"
|
||||
);
|
||||
|
||||
const { ExtensionPreferencesManager } = ChromeUtils.importESModule(
|
||||
"resource://gre/modules/ExtensionPreferencesManager.sys.mjs"
|
||||
);
|
||||
|
||||
var { getSettingsAPI } = ExtensionPreferencesManager;
|
||||
|
||||
ChromeUtils.defineESModuleGetters(this, {
|
||||
BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
|
||||
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",
|
||||
});
|
||||
|
||||
ChromeUtils.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: getSettingsAPI({
|
||||
context,
|
||||
name: "engagementTelemetry",
|
||||
readOnly: true,
|
||||
callback: () => false,
|
||||
}),
|
||||
|
||||
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.sys.mjs 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;
|
||||
}
|
||||
};
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
env: {
|
||||
webextensions: true,
|
||||
},
|
||||
};
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
# 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",
|
||||
]
|
||||
prefs = ["browser.bookmarks.testing.skipDefaultBookmarksImport=true"]
|
||||
|
||||
["browser_ext_urlbar_attributionURL.js"]
|
||||
|
||||
["browser_ext_urlbar_clearInput.js"]
|
||||
|
||||
["browser_ext_urlbar_dynamicResult.js"]
|
||||
support-files = ["dynamicResult.css"]
|
||||
|
||||
["browser_ext_urlbar_extensionTimeout.js"]
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
/* 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);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
/* 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();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,137 +0,0 @@
|
|||
/* 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();
|
||||
});
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
/* 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);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
/* 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);
|
||||
}
|
||||
|
|
@ -1,253 +0,0 @@
|
|||
/* 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";
|
||||
|
||||
Services.scriptloader.loadSubScript(
|
||||
"chrome://mochitests/content/browser/browser/components/urlbar/tests/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.ok(
|
||||
false,
|
||||
`"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);
|
||||
});
|
||||
}
|
||||
|
|
@ -1,113 +0,0 @@
|
|||
[
|
||||
{
|
||||
"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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
@ -189,7 +189,6 @@
|
|||
"browser/components/uitour/test/browser.ini": 85.74,
|
||||
"browser/components/urlbar/tests/browser-tips/browser.ini": 47.21,
|
||||
"browser/components/urlbar/tests/browser/browser.ini": 417.2,
|
||||
"browser/components/urlbar/tests/ext/browser/browser.ini": 3.68,
|
||||
"browser/extensions/doh-rollout/test/browser/browser.ini": 130.64,
|
||||
"browser/extensions/formautofill/test/browser/browser.ini": 55.03,
|
||||
"browser/extensions/formautofill/test/browser/creditCard/browser.ini": 56.34,
|
||||
|
|
|
|||
|
|
@ -188,7 +188,6 @@
|
|||
"browser/components/uitour/test/browser.ini": 56.84,
|
||||
"browser/components/urlbar/tests/browser-tips/browser.ini": 35.74,
|
||||
"browser/components/urlbar/tests/browser/browser.ini": 347.01,
|
||||
"browser/components/urlbar/tests/ext/browser/browser.ini": 1.8,
|
||||
"browser/extensions/doh-rollout/test/browser/browser.ini": 151.75,
|
||||
"browser/extensions/formautofill/test/browser/browser.ini": 42.02,
|
||||
"browser/extensions/formautofill/test/browser/creditCard/browser.ini": 12.18,
|
||||
|
|
|
|||
|
|
@ -220,7 +220,6 @@ const PRIVILEGED_PERMS = new Set([
|
|||
"networkStatus",
|
||||
"normandyAddonStudy",
|
||||
"telemetry",
|
||||
"urlbar",
|
||||
]);
|
||||
|
||||
const PRIVILEGED_PERMS_ANDROID_ONLY = new Set([
|
||||
|
|
@ -229,7 +228,7 @@ const PRIVILEGED_PERMS_ANDROID_ONLY = new Set([
|
|||
"nativeMessaging",
|
||||
]);
|
||||
|
||||
const PRIVILEGED_PERMS_DESKTOP_ONLY = new Set(["normandyAddonStudy", "urlbar"]);
|
||||
const PRIVILEGED_PERMS_DESKTOP_ONLY = new Set(["normandyAddonStudy"]);
|
||||
|
||||
if (AppConstants.platform == "android") {
|
||||
for (const perm of PRIVILEGED_PERMS_ANDROID_ONLY) {
|
||||
|
|
|
|||
|
|
@ -677,7 +677,6 @@ const GRANTED_WITHOUT_USER_PROMPT = [
|
|||
"telemetry",
|
||||
"theme",
|
||||
"unlimitedStorage",
|
||||
"urlbar",
|
||||
"webRequest",
|
||||
"webRequestBlocking",
|
||||
"webRequestFilterResponse",
|
||||
|
|
|
|||
Loading…
Reference in a new issue