new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/ext-c-omnibox.js
@@ -0,0 +1,32 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+
+var {
+ runSafeSyncWithoutClone,
+ SingletonEventManager,
+} = ExtensionUtils;
+
+extensions.registerSchemaAPI("omnibox", "addon_child", context => {
+ return {
+ omnibox: {
+ onInputChanged: new SingletonEventManager(context, "omnibox.onInputChanged", fire => {
+ let listener = (text, id) => {
+ runSafeSyncWithoutClone(fire, text, suggestions => {
+ // TODO: Switch to using callParentFunctionNoReturn once bug 1314903 is fixed.
+ context.childManager.callParentAsyncFunction("omnibox_internal.addSuggestions", [
+ id,
+ suggestions,
+ ]);
+ });
+ };
+ context.childManager.getParentEvent("omnibox_internal.onInputChanged").addListener(listener);
+ return () => {
+ context.childManager.getParentEvent("omnibox_internal.onInputChanged").removeListener(listener);
+ };
+ }).api(),
+ },
+ };
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/ext-omnibox.js
@@ -0,0 +1,104 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionSearchHandler",
+ "resource://gre/modules/ExtensionSearchHandler.jsm");
+var {
+ SingletonEventManager,
+} = ExtensionUtils;
+
+// WeakMap[extension -> keyword]
+let gKeywordMap = new WeakMap();
+
+/* eslint-disable mozilla/balanced-listeners */
+extensions.on("manifest_omnibox", (type, directive, extension, manifest) => {
+ let keyword = manifest.omnibox.keyword;
+ try {
+ // This will throw if the keyword is already registered.
+ ExtensionSearchHandler.registerKeyword(keyword, extension);
+ gKeywordMap.set(extension, keyword);
+ } catch (e) {
+ extension.manifestError(e.message);
+ }
+});
+
+extensions.on("shutdown", (type, extension) => {
+ let keyword = gKeywordMap.get(extension);
+ if (keyword) {
+ ExtensionSearchHandler.unregisterKeyword(keyword);
+ gKeywordMap.delete(extension);
+ }
+});
+/* eslint-enable mozilla/balanced-listeners */
+
+extensions.registerSchemaAPI("omnibox", "addon_parent", context => {
+ let {extension} = context;
+ return {
+ omnibox: {
+ setDefaultSuggestion(suggestion) {
+ let keyword = gKeywordMap.get(extension);
+ try {
+ // This will throw if the keyword failed to register.
+ ExtensionSearchHandler.setDefaultSuggestion(keyword, suggestion);
+ } catch (e) {
+ return Promise.reject(e.message);
+ }
+ },
+
+ onInputStarted: new SingletonEventManager(context, "omnibox.onInputStarted", fire => {
+ let listener = (eventName) => {
+ fire();
+ };
+ extension.on(ExtensionSearchHandler.MSG_INPUT_STARTED, listener);
+ return () => {
+ extension.off(ExtensionSearchHandler.MSG_INPUT_STARTED, listener);
+ };
+ }).api(),
+
+ onInputCancelled: new SingletonEventManager(context, "omnibox.onInputCancelled", fire => {
+ let listener = (eventName) => {
+ fire();
+ };
+ extension.on(ExtensionSearchHandler.MSG_INPUT_CANCELLED, listener);
+ return () => {
+ extension.off(ExtensionSearchHandler.MSG_INPUT_CANCELLED, listener);
+ };
+ }).api(),
+
+ onInputEntered: new SingletonEventManager(context, "omnibox.onInputEntered", fire => {
+ let listener = (eventName, text, disposition) => {
+ fire(text, disposition);
+ };
+ extension.on(ExtensionSearchHandler.MSG_INPUT_ENTERED, listener);
+ return () => {
+ extension.off(ExtensionSearchHandler.MSG_INPUT_ENTERED, listener);
+ };
+ }).api(),
+ },
+
+ omnibox_internal: {
+ addSuggestions(id, suggestions) {
+ let keyword = gKeywordMap.get(extension);
+ try {
+ ExtensionSearchHandler.addSuggestions(keyword, id, suggestions);
+ } catch (e) {
+ // Silently fail because the extension developer can not know for sure if the user
+ // has already invalidated the callback when asynchronously providing suggestions.
+ }
+ },
+
+ onInputChanged: new SingletonEventManager(context, "omnibox_internal.onInputChanged", fire => {
+ let listener = (eventName, text, id) => {
+ fire(text, id);
+ };
+ extension.on(ExtensionSearchHandler.MSG_INPUT_CHANGED, listener);
+ return () => {
+ extension.off(ExtensionSearchHandler.MSG_INPUT_CHANGED, listener);
+ };
+ }).api(),
+ },
+ };
+});
--- a/browser/components/extensions/extensions-browser.manifest
+++ b/browser/components/extensions/extensions-browser.manifest
@@ -1,28 +1,31 @@
# scripts
category webextension-scripts bookmarks chrome://browser/content/ext-bookmarks.js
category webextension-scripts browserAction chrome://browser/content/ext-browserAction.js
category webextension-scripts commands chrome://browser/content/ext-commands.js
category webextension-scripts contextMenus chrome://browser/content/ext-contextMenus.js
category webextension-scripts desktop-runtime chrome://browser/content/ext-desktop-runtime.js
category webextension-scripts history chrome://browser/content/ext-history.js
+category webextension-scripts omnibox chrome://browser/content/ext-omnibox.js
category webextension-scripts pageAction chrome://browser/content/ext-pageAction.js
category webextension-scripts tabs chrome://browser/content/ext-tabs.js
category webextension-scripts utils chrome://browser/content/ext-utils.js
category webextension-scripts windows chrome://browser/content/ext-windows.js
# scripts that must run in the same process as addon code.
category webextension-scripts-addon browserAction chrome://browser/content/ext-c-browserAction.js
category webextension-scripts-addon contextMenus chrome://browser/content/ext-c-contextMenus.js
+category webextension-scripts-addon omnibox chrome://browser/content/ext-c-omnibox.js
category webextension-scripts-addon pageAction chrome://browser/content/ext-c-pageAction.js
category webextension-scripts-addon tabs chrome://browser/content/ext-c-tabs.js
# schemas
category webextension-schemas bookmarks chrome://browser/content/schemas/bookmarks.json
category webextension-schemas browser_action chrome://browser/content/schemas/browser_action.json
category webextension-schemas commands chrome://browser/content/schemas/commands.json
category webextension-schemas context_menus chrome://browser/content/schemas/context_menus.json
category webextension-schemas context_menus_internal chrome://browser/content/schemas/context_menus_internal.json
category webextension-schemas history chrome://browser/content/schemas/history.json
+category webextension-schemas omnibox chrome://browser/content/schemas/omnibox.json
category webextension-schemas page_action chrome://browser/content/schemas/page_action.json
category webextension-schemas tabs chrome://browser/content/schemas/tabs.json
category webextension-schemas windows chrome://browser/content/schemas/windows.json
--- a/browser/components/extensions/jar.mn
+++ b/browser/components/extensions/jar.mn
@@ -13,16 +13,18 @@ browser.jar:
#endif
content/browser/extension.svg
content/browser/ext-bookmarks.js
content/browser/ext-browserAction.js
content/browser/ext-commands.js
content/browser/ext-contextMenus.js
content/browser/ext-desktop-runtime.js
content/browser/ext-history.js
+ content/browser/ext-omnibox.js
content/browser/ext-pageAction.js
content/browser/ext-tabs.js
content/browser/ext-utils.js
content/browser/ext-windows.js
content/browser/ext-c-browserAction.js
content/browser/ext-c-contextMenus.js
+ content/browser/ext-c-omnibox.js
content/browser/ext-c-pageAction.js
content/browser/ext-c-tabs.js
--- a/browser/components/extensions/schemas/jar.mn
+++ b/browser/components/extensions/schemas/jar.mn
@@ -4,11 +4,12 @@
browser.jar:
content/browser/schemas/bookmarks.json
content/browser/schemas/browser_action.json
content/browser/schemas/commands.json
content/browser/schemas/context_menus.json
content/browser/schemas/context_menus_internal.json
content/browser/schemas/history.json
+ content/browser/schemas/omnibox.json
content/browser/schemas/page_action.json
content/browser/schemas/tabs.json
content/browser/schemas/windows.json
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/schemas/omnibox.json
@@ -0,0 +1,248 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+[
+ {
+ "namespace": "manifest",
+ "types": [
+ {
+ "$extend": "WebExtensionManifest",
+ "properties": {
+ "omnibox": {
+ "type": "object",
+ "additionalProperties": { "$ref": "UnrecognizedProperty" },
+ "properties": {
+ "keyword": {
+ "type": "string",
+ "pattern": "^[^?\\s:]([^\\s:]*[^/\\s:])?$"
+ }
+ },
+ "optional": true
+ }
+ }
+ }
+ ]
+ },
+ {
+ "namespace": "omnibox",
+ "description": "The omnibox API allows you to register a keyword with Firefox's address bar.",
+ "permissions": ["manifest:omnibox"],
+ "types": [
+ {
+ "id": "DescriptionStyleType",
+ "type": "string",
+ "description": "The style type.",
+ "enum": ["url", "match", "dim"]
+ },
+ {
+ "id": "OnInputEnteredDisposition",
+ "type": "string",
+ "enum": ["currentTab", "newForegroundTab", "newBackgroundTab"],
+ "description": "The window disposition for the omnibox query. This is the recommended context to display results. For example, if the omnibox command is to navigate to a certain URL, a disposition of 'newForegroundTab' means the navigation should take place in a new selected tab."
+ },
+ {
+ "id": "SuggestResult",
+ "type": "object",
+ "description": "A suggest result.",
+ "properties": {
+ "content": {
+ "type": "string",
+ "minLength": 1,
+ "description": "The text that is put into the URL bar, and that is sent to the extension when the user chooses this entry."
+ },
+ "description": {
+ "type": "string",
+ "minLength": 1,
+ "description": "The text that is displayed in the URL dropdown. Can contain XML-style markup for styling. The supported tags are 'url' (for a literal URL), 'match' (for highlighting text that matched what the user's query), and 'dim' (for dim helper text). The styles can be nested, eg. <dim><match>dimmed match</match></dim>. You must escape the five predefined entities to display them as text: stackoverflow.com/a/1091953/89484 "
+ },
+ "descriptionStyles": {
+ "optional": true,
+ "unsupported": true,
+ "type": "array",
+ "description": "An array of style ranges for the description, as provided by the extension.",
+ "items": {
+ "type": "object",
+ "description": "The style ranges for the description, as provided by the extension.",
+ "properties": {
+ "offset": { "type": "integer" },
+ "type": { "description": "The style type", "$ref": "DescriptionStyleType"},
+ "length": { "type": "integer", "optional": true }
+ }
+ }
+ },
+ "descriptionStylesRaw": {
+ "optional": true,
+ "unsupported": true,
+ "type": "array",
+ "description": "An array of style ranges for the description, as provided by ToValue().",
+ "items": {
+ "type": "object",
+ "description": "The style ranges for the description, as provided by ToValue().",
+ "properties": {
+ "offset": { "type": "integer" },
+ "type": { "type": "integer" }
+ }
+ }
+ }
+ }
+ },
+ {
+ "id": "DefaultSuggestResult",
+ "type": "object",
+ "description": "A suggest result.",
+ "properties": {
+ "description": {
+ "type": "string",
+ "minLength": 1,
+ "description": "The text that is displayed in the URL dropdown."
+ },
+ "descriptionStyles": {
+ "optional": true,
+ "unsupported": true,
+ "type": "array",
+ "description": "An array of style ranges for the description, as provided by the extension.",
+ "items": {
+ "type": "object",
+ "description": "The style ranges for the description, as provided by the extension.",
+ "properties": {
+ "offset": { "type": "integer" },
+ "type": { "description": "The style type", "$ref": "DescriptionStyleType"},
+ "length": { "type": "integer", "optional": true }
+ }
+ }
+ },
+ "descriptionStylesRaw": {
+ "optional": true,
+ "unsupported": true,
+ "type": "array",
+ "description": "An array of style ranges for the description, as provided by ToValue().",
+ "items": {
+ "type": "object",
+ "description": "The style ranges for the description, as provided by ToValue().",
+ "properties": {
+ "offset": { "type": "integer" },
+ "type": { "type": "integer" }
+ }
+ }
+ }
+ }
+ }
+ ],
+ "functions": [
+ {
+ "name": "setDefaultSuggestion",
+ "type": "function",
+ "description": "Sets the description and styling for the default suggestion. The default suggestion is the text that is displayed in the first suggestion row underneath the URL bar.",
+ "parameters": [
+ {
+ "name": "suggestion",
+ "$ref": "DefaultSuggestResult",
+ "description": "A partial SuggestResult object, without the 'content' parameter."
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "onInputStarted",
+ "type": "function",
+ "description": "User has started a keyword input session by typing the extension's keyword. This is guaranteed to be sent exactly once per input session, and before any onInputChanged events.",
+ "parameters": []
+ },
+ {
+ "name": "onInputChanged",
+ "type": "function",
+ "description": "User has changed what is typed into the omnibox.",
+ "parameters": [
+ {
+ "type": "string",
+ "name": "text"
+ },
+ {
+ "name": "suggest",
+ "type": "function",
+ "description": "A callback passed to the onInputChanged event used for sending suggestions back to the browser.",
+ "parameters": [
+ {
+ "name": "suggestResults",
+ "type": "array",
+ "description": "Array of suggest results",
+ "items": {
+ "$ref": "SuggestResult"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "onInputEntered",
+ "type": "function",
+ "description": "User has accepted what is typed into the omnibox.",
+ "parameters": [
+ {
+ "type": "string",
+ "name": "text"
+ },
+ {
+ "name": "disposition",
+ "$ref": "OnInputEnteredDisposition"
+ }
+ ]
+ },
+ {
+ "name": "onInputCancelled",
+ "type": "function",
+ "description": "User has ended the keyword input session without accepting the input.",
+ "parameters": []
+ }
+ ]
+ },
+ {
+ "namespace": "omnibox_internal",
+ "description": "The internal namespace used by the omnibox API.",
+ "defaultContexts": ["addon_parent_only"],
+ "functions": [
+ {
+ "name": "addSuggestions",
+ "type": "function",
+ "async": "callback",
+ "description": "Internal function used by omnibox.onInputChanged for adding search suggestions",
+ "parameters": [
+ {
+ "name": "id",
+ "type": "integer",
+ "description": "The ID of the callback received by onInputChangedInternal"
+ },
+ {
+ "name": "suggestResults",
+ "type": "array",
+ "description": "Array of suggest results",
+ "items": {
+ "$ref": "omnibox.SuggestResult"
+ }
+ },
+ {
+ "type": "function",
+ "name": "callback",
+ "optional": true,
+ "parameters": []
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "onInputChanged",
+ "type": "function",
+ "description": "Identical to omnibox.onInputChanged except no 'suggest' callback is provided.",
+ "parameters": [
+ {
+ "type": "string",
+ "name": "text"
+ }
+ ]
+ }
+ ]
+ }
+]
\ No newline at end of file
--- a/browser/components/extensions/test/browser/browser.ini
+++ b/browser/components/extensions/test/browser/browser.ini
@@ -39,16 +39,17 @@ tags = webextensions
[browser_ext_contextMenus_radioGroups.js]
[browser_ext_contextMenus_uninstall.js]
[browser_ext_contextMenus_urlPatterns.js]
[browser_ext_currentWindow.js]
[browser_ext_getViews.js]
[browser_ext_incognito_popup.js]
[browser_ext_lastError.js]
[browser_ext_legacy_extension_context_contentscript.js]
+[browser_ext_omnibox.js]
[browser_ext_optionsPage_privileges.js]
[browser_ext_pageAction_context.js]
[browser_ext_pageAction_popup.js]
[browser_ext_pageAction_popup_resize.js]
[browser_ext_pageAction_simple.js]
[browser_ext_pageAction_title.js]
[browser_ext_popup_api_injection.js]
[browser_ext_popup_background.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_omnibox.js
@@ -0,0 +1,280 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(function* () {
+ let keyword = "test";
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ "omnibox": {
+ "keyword": keyword,
+ },
+ },
+
+ background: function() {
+ browser.omnibox.onInputStarted.addListener(() => {
+ browser.test.sendMessage("on-input-started-fired");
+ });
+
+ let synchronous = true;
+ let suggestions = null;
+ let suggestCallback = null;
+
+ browser.omnibox.onInputChanged.addListener((text, suggest) => {
+ if (synchronous && suggestions) {
+ suggest(suggestions);
+ } else {
+ suggestCallback = suggest;
+ }
+ browser.test.sendMessage("on-input-changed-fired", {text});
+ });
+
+ browser.omnibox.onInputCancelled.addListener(() => {
+ browser.test.sendMessage("on-input-cancelled-fired");
+ });
+
+ browser.omnibox.onInputEntered.addListener((text, disposition) => {
+ browser.test.sendMessage("on-input-entered-fired", {text, disposition});
+ });
+
+ browser.test.onMessage.addListener((msg, data) => {
+ switch (msg) {
+ case "set-suggestions":
+ suggestions = data.suggestions;
+ browser.test.sendMessage("suggestions-set");
+ break;
+ case "set-default-suggestion":
+ browser.omnibox.setDefaultSuggestion(data.suggestion);
+ browser.test.sendMessage("default-suggestion-set");
+ break;
+ case "set-synchronous":
+ synchronous = data.synchronous;
+ break;
+ case "test-multiple-suggest-calls":
+ suggestions.forEach(suggestion => suggestCallback([suggestion]));
+ browser.test.sendMessage("test-ready");
+ break;
+ case "test-suggestions-after-delay":
+ Promise.resolve().then(() => {
+ suggestCallback(suggestions);
+ browser.test.sendMessage("test-ready");
+ });
+ break;
+ }
+ });
+ },
+ });
+
+ function* expectEvent(event, expected = {}) {
+ let actual = yield extension.awaitMessage(event);
+ if (expected.text) {
+ is(actual.text, expected.text,
+ `Expected "${event}" to have fired with text: "${expected.text}".`);
+ }
+ if (expected.disposition) {
+ is(actual.disposition, expected.disposition,
+ `Expected "${event}" to have fired with disposition: "${expected.disposition}".`);
+ }
+ }
+
+ function* startInputSession() {
+ gURLBar.focus();
+ gURLBar.value = keyword;
+ EventUtils.synthesizeKey(" ", {});
+ yield expectEvent("on-input-started-fired");
+ EventUtils.synthesizeKey("t", {});
+ yield expectEvent("on-input-changed-fired", {text: "t"});
+ return "t";
+ }
+
+ function* testInputEvents() {
+ yield extension.startup();
+ gURLBar.focus();
+
+ // Start an input session by typing in <keyword><space>.
+ for (let letter of keyword) {
+ EventUtils.synthesizeKey(letter, {});
+ }
+ EventUtils.synthesizeKey(" ", {});
+ yield expectEvent("on-input-started-fired");
+
+ // We should expect input changed events now that the keyword is active.
+ EventUtils.synthesizeKey("b", {});
+ yield expectEvent("on-input-changed-fired", {text: "b"});
+
+ EventUtils.synthesizeKey("c", {});
+ yield expectEvent("on-input-changed-fired", {text: "bc"});
+
+ EventUtils.synthesizeKey("VK_BACK_SPACE", {});
+ yield expectEvent("on-input-changed-fired", {text: "b"});
+
+ // Even though the input is <keyword><space> We should not expect an
+ // input started event to fire since the keyword is active.
+ EventUtils.synthesizeKey("VK_BACK_SPACE", {});
+ yield expectEvent("on-input-changed-fired", {text: ""});
+
+ // Make the keyword inactive by hitting backspace.
+ EventUtils.synthesizeKey("VK_BACK_SPACE", {});
+ yield expectEvent("on-input-cancelled-fired");
+
+ // Activate the keyword by typing a space.
+ // Expect onInputStarted to fire.
+ EventUtils.synthesizeKey(" ", {});
+ yield expectEvent("on-input-started-fired");
+
+ // onInputChanged should fire even if a space is entered.
+ EventUtils.synthesizeKey(" ", {});
+ yield expectEvent("on-input-changed-fired", {text: " "});
+
+ // The active session should cancel if the input blurs.
+ gURLBar.blur();
+ yield expectEvent("on-input-cancelled-fired");
+ }
+
+ function* testHeuristicResult(expectedText, setDefaultSuggestion) {
+ if (setDefaultSuggestion) {
+ extension.sendMessage("set-default-suggestion", {
+ suggestion: {
+ description: expectedText,
+ },
+ });
+ yield extension.awaitMessage("default-suggestion-set");
+ }
+
+ let text = yield startInputSession();
+
+ // Select the heuristic result. Due to bug 1309047, we currently
+ // have to hit down and then up in order to properly select the heuristic result.
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ EventUtils.synthesizeKey("VK_UP", {});
+
+ // Click on the heuristic result.
+ let item = gURLBar.popup.richlistbox.children[0];
+
+ is(item.getAttribute("title"), expectedText,
+ `Expected heuristic result to have title: "${expectedText}".`);
+
+ is(item.getAttribute("displayurl"), `${keyword} ${text}`,
+ `Expected heuristic result to have displayurl: "${keyword} ${text}".`);
+
+ EventUtils.synthesizeMouseAtCenter(item, {});
+
+ yield expectEvent("on-input-entered-fired", {
+ text,
+ disposition: "currentTab",
+ });
+ }
+
+ function* testDisposition(suggestionIndex, expectedDisposition, expectedText) {
+ yield startInputSession();
+
+ // Select the suggestion.
+ for (let i = 0; i < suggestionIndex; i++) {
+ EventUtils.synthesizeKey("VK_DOWN", {});
+ }
+
+ let item = gURLBar.popup.richlistbox.children[suggestionIndex];
+ if (expectedDisposition == "currentTab") {
+ EventUtils.synthesizeMouseAtCenter(item, {});
+ } else if (expectedDisposition == "newForegroundTab") {
+ EventUtils.synthesizeMouseAtCenter(item, {accelKey: true});
+ } else if (expectedDisposition == "newBackgroundTab") {
+ EventUtils.synthesizeMouseAtCenter(item, {shiftKey: true, accelKey: true});
+ }
+
+ yield expectEvent("on-input-entered-fired", {
+ text: expectedText,
+ disposition: expectedDisposition,
+ });
+ }
+
+ function* testSuggestions(info) {
+ extension.sendMessage("set-synchronous", {synchronous: false});
+
+ function expectSuggestion({content, description}, index) {
+ let item = gURLBar.popup.richlistbox.children[index + 1]; // Skip the heuristic result.
+
+ is(item.getAttribute("title"), description,
+ `Expected suggestion to have title: "${description}".`);
+
+ is(item.getAttribute("displayurl"), `${keyword} ${content}`,
+ `Expected suggestion to have displayurl: "${keyword} ${content}".`);
+ }
+
+ let text = yield startInputSession();
+
+ extension.sendMessage(info.test);
+ yield extension.awaitMessage("test-ready");
+
+ info.suggestions.forEach(expectSuggestion);
+
+ EventUtils.synthesizeMouseAtCenter(gURLBar.popup.richlistbox.children[0], {});
+ yield expectEvent("on-input-entered-fired", {
+ text,
+ disposition: "currentTab",
+ });
+ }
+
+ yield testInputEvents();
+
+ // Test the heuristic result with default suggestions.
+ yield testHeuristicResult("Generated extension", false /* setDefaultSuggestion */);
+ yield testHeuristicResult("hello world", true /* setDefaultSuggestion */);
+ yield testHeuristicResult("foo bar", true /* setDefaultSuggestion */);
+
+ let suggestions = [
+ {content: "a", description: "select a"},
+ {content: "b", description: "select b"},
+ {content: "c", description: "select c"},
+ ];
+
+ extension.sendMessage("set-suggestions", {suggestions});
+ yield extension.awaitMessage("suggestions-set");
+
+ // Test each suggestion and search disposition.
+ yield testDisposition(1, "currentTab", suggestions[0].content);
+ yield testDisposition(2, "newForegroundTab", suggestions[1].content);
+ yield testDisposition(3, "newBackgroundTab", suggestions[2].content);
+
+ extension.sendMessage("set-suggestions", {suggestions});
+ yield extension.awaitMessage("suggestions-set");
+
+ // Test adding suggesions asynchronously.
+ yield testSuggestions({
+ test: "test-multiple-suggest-calls",
+ skipHeuristic: true,
+ suggestions,
+ });
+ yield testSuggestions({
+ test: "test-suggestions-after-delay",
+ skipHeuristic: true,
+ suggestions,
+ });
+
+ // Start monitoring the console.
+ SimpleTest.waitForExplicitFinish();
+ let waitForConsole = new Promise(resolve => {
+ SimpleTest.monitorConsole(resolve, [{
+ message: new RegExp(`The keyword provided is already registered: ${keyword}`),
+ }]);
+ });
+
+ // Try registering another extension with the same keyword
+ let extension2 = ExtensionTestUtils.loadExtension({
+ manifest: {
+ "omnibox": {
+ "keyword": keyword,
+ },
+ },
+ });
+
+ yield extension2.startup();
+
+ // Stop monitoring the console and confirm the correct errors are logged.
+ SimpleTest.endMonitorConsole();
+ yield waitForConsole;
+
+ yield extension2.unload();
+ yield extension.unload();
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/xpcshell/test_ext_manifest_omnibox.js
@@ -0,0 +1,61 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+function* testKeyword(params) {
+ let normalized = yield ExtensionTestUtils.normalizeManifest({
+ "omnibox": {
+ "keyword": params.keyword,
+ },
+ });
+
+ if (params.expectError) {
+ let expectedError = (
+ String.raw`omnibox.keyword: String "${params.keyword}" ` +
+ String.raw`must match /^[^?\s:]([^\s:]*[^/\s:])?$/`
+ );
+ ok(normalized.error.includes(expectedError),
+ `The manifest error ${JSON.stringify(normalized.error)} ` +
+ `must contain ${JSON.stringify(expectedError)}`);
+ } else {
+ equal(normalized.error, undefined, "Should not have an error");
+ equal(normalized.errors.length, 0, "Should not have warnings");
+ }
+}
+
+add_task(function* test_manifest_commands() {
+ // accepted single character keywords
+ yield testKeyword({keyword: "a", expectError: false});
+ yield testKeyword({keyword: "-", expectError: false});
+ yield testKeyword({keyword: "嗨", expectError: false});
+ yield testKeyword({keyword: "*", expectError: false});
+ yield testKeyword({keyword: "/", expectError: false});
+
+ // rejected single character keywords
+ yield testKeyword({keyword: "?", expectError: true});
+ yield testKeyword({keyword: " ", expectError: true});
+ yield testKeyword({keyword: ":", expectError: true});
+
+ // accepted multi-character keywords
+ yield testKeyword({keyword: "aa", expectError: false});
+ yield testKeyword({keyword: "http", expectError: false});
+ yield testKeyword({keyword: "f?a", expectError: false});
+ yield testKeyword({keyword: "fa?", expectError: false});
+ yield testKeyword({keyword: "f/x", expectError: false});
+ yield testKeyword({keyword: "/fx", expectError: false});
+
+ // rejected multi-character keywords
+ yield testKeyword({keyword: " a", expectError: true});
+ yield testKeyword({keyword: "a ", expectError: true});
+ yield testKeyword({keyword: " ", expectError: true});
+ yield testKeyword({keyword: " a ", expectError: true});
+ yield testKeyword({keyword: "?fx", expectError: true});
+ yield testKeyword({keyword: "fx/", expectError: true});
+ yield testKeyword({keyword: "f:x", expectError: true});
+ yield testKeyword({keyword: "fx:", expectError: true});
+ yield testKeyword({keyword: "f x", expectError: true});
+
+ // miscellaneous tests
+ yield testKeyword({keyword: "こんにちは", expectError: false});
+ yield testKeyword({keyword: "http://", expectError: true});
+});
--- a/browser/components/extensions/test/xpcshell/xpcshell.ini
+++ b/browser/components/extensions/test/xpcshell/xpcshell.ini
@@ -2,9 +2,10 @@
head = head.js
tail =
firefox-appdir = browser
tags = webextensions
[test_ext_bookmarks.js]
[test_ext_history.js]
[test_ext_manifest_commands.js]
+[test_ext_manifest_omnibox.js]
[test_ext_manifest_permissions.js]
--- a/toolkit/components/places/ExtensionSearchHandler.jsm
+++ b/toolkit/components/places/ExtensionSearchHandler.jsm
@@ -119,21 +119,20 @@ var ExtensionSearchHandler = Object.free
throw new Error(`The keyword provided is not registered: ${keyword}`);
}
if (keyword != gActiveKeyword) {
throw new Error("A different input session is already ongoing");
}
if (id != gCurrentCallbackID) {
- return false;
+ throw new Error("The callback is no longer active");
}
gSuggestionsCallback(suggestions);
- return true;
},
/**
* Called when the input in the urlbar begins with `<keyword><space>`.
*
* If the keyword is inactive, MSG_INPUT_STARTED is emitted and the
* keyword is marked as active. If the keyword is followed by any text,
* MSG_INPUT_CHANGED is fired with the current callback ID that can be
--- a/toolkit/components/places/tests/unifiedcomplete/test_extension_matches.js
+++ b/toolkit/components/places/tests/unifiedcomplete/test_extension_matches.js
@@ -36,31 +36,32 @@ add_task(function* test_correct_errors_a
Assert.throws(() => ExtensionSearchHandler.setDefaultSuggestion(unregisteredKeyword, "suggestion"));
// Try calling handleInputCancelled when there is no active session.
Assert.throws(() => ExtensionSearchHandler.handleInputCancelled());
// Start a session by calling handleSearch with the registered keyword.
ExtensionSearchHandler.handleSearch(keyword, `${keyword} test`, () => {});
- // Try providing suggestions for an unregistered keyword.
- Assert.throws(() => ExtensionSearchHandler.addSuggestions(unregisteredKeyword, 0, []));
-
- // Try providing suggestions for an inactive keyword.
- Assert.throws(() => ExtensionSearchHandler.addSuggestions(anotherKeyword, 0, []));
-
// Try calling handleSearch for an inactive keyword
Assert.throws(() => ExtensionSearchHandler.handleSearch(anotherKeyword, `${anotherKeyword} `, () => {}));
- // Try providing suggestions with inactive callback IDs.
- Assert.ok(!ExtensionSearchHandler.addSuggestions(keyword, 0, []));
- Assert.ok(!ExtensionSearchHandler.addSuggestions(keyword, 2, []));
+ // Try providing suggestions for an unregistered keyword.
+ Assert.throws(() => ExtensionSearchHandler.addSuggestions(unregisteredKeyword, 1, []));
+
+ // Try providing suggestions for an inactive keyword.
+ Assert.throws(() => ExtensionSearchHandler.addSuggestions(anotherKeyword, 1, []));
- // Add suggestions for a valid callback ID.
- Assert.ok(ExtensionSearchHandler.addSuggestions(keyword, 1, []));
+ // Try providing suggestions with an inactive callback ID.
+ Assert.throws(() => ExtensionSearchHandler.addSuggestions(keyword, 0, []));
+
+ // Try providing suggestions with another inactive callback ID.
+ Assert.throws(() => ExtensionSearchHandler.addSuggestions(keyword, 2, []));
+
+ ExtensionSearchHandler.addSuggestions(keyword, 1, []);
// End the input session by calling handleInputCancelled.
ExtensionSearchHandler.handleInputCancelled();
// Try handling input after the session has ended using handleInputCancelled.
Assert.throws(() => ExtensionSearchHandler.handleInputCancelled());
// Start a new session by calling handleSearch with a different keyword