Bug 1267810 - Add a module for registering keywords and handling keyword input sessions. r=adw

MozReview-Commit-ID: Ghqe5xLw67Y

--HG--
extra : rebase_source : cae9a75edb18529f608bc68f464a432300b8b88e
This commit is contained in:
Matthew Wein 2016-11-03 16:27:50 +00:00
Родитель 17153e5c27
Коммит dde16e219d
13 изменённых файлов: 716 добавлений и 4 удалений

Просмотреть файл

@ -472,7 +472,15 @@ toolbar:not(#TabsToolbar) > #personal-bookmarks {
list-style-image: none;
}
#urlbar:not([actiontype="switchtab"]) > #urlbar-display-box {
#urlbar:not([actiontype="switchtab"]):not([actiontype="extension"]) > #urlbar-display-box {
display: none;
}
#urlbar:not([actiontype="switchtab"]) > #urlbar-display-box > #switchtab {
display: none;
}
#urlbar:not([actiontype="extension"]) > #urlbar-display-box > #extension {
display: none;
}

Просмотреть файл

@ -760,7 +760,8 @@
</hbox>
</box>
<box id="urlbar-display-box" align="center">
<label class="urlbar-display urlbar-display-switchtab" value="&urlbar.switchToTab.label;"/>
<label id="switchtab" class="urlbar-display urlbar-display-switchtab" value="&urlbar.switchToTab.label;"/>
<label id="extension" class="urlbar-display urlbar-display-extension" value="&urlbar.extension.label;"/>
</box>
<hbox id="urlbar-icons">
<image id="page-report-button"

Просмотреть файл

@ -56,6 +56,15 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
<field name="AppConstants" readonly="true">
(Components.utils.import("resource://gre/modules/AppConstants.jsm", {})).AppConstants;
</field>
<field name="ExtensionSearchHandler" readonly="true">
{
let searchHandler = {};
Components.utils.import("resource://gre/modules/ExtensionSearchHandler.jsm", searchHandler);
searchHandler.ExtensionSearchHandler;
}
</field>
<constructor><![CDATA[
this._prefs = Components.classes["@mozilla.org/preferences-service;1"]
.getService(Components.interfaces.nsIPrefService)
@ -174,6 +183,10 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
returnValue = action.params.input;
break;
}
case "extension": {
returnValue = action.params.content;
break;
}
}
} else {
let originalUrl = ReaderMode.getOriginalUrl(aValue);
@ -470,6 +483,13 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
actionDetails
);
break;
case "extension":
this.handleRevert();
// Give the extension control of handling the command.
let searchString = action.params.content;
let keyword = action.params.keyword;
this.ExtensionSearchHandler.handleInputEntered(keyword, searchString, where);
return;
}
} else if (selectedOneOff && selectedOneOff.engine) {
// If there's a selected one-off button and the input value is a
@ -606,7 +626,7 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
engineOrEngineName;
let isOneOff = this.popup.oneOffSearchButtons
.maybeRecordTelemetry(event, openUILinkWhere, openUILinkParams);
// Infer the type of the even which triggered the search.
// Infer the type of the event which triggered the search.
let eventType = "unknown";
if (event instanceof KeyboardEvent) {
eventType = "key";
@ -1184,6 +1204,9 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
this._clearNoActions();
this.formatValue();
}
if (ExtensionSearchHandler.hasActiveInputSession()) {
ExtensionSearchHandler.handleInputCancelled();
}
]]></handler>
<handler event="dragstart" phase="capturing"><![CDATA[

Просмотреть файл

@ -421,6 +421,7 @@ BrowserGlue.prototype = {
tag: 7,
visiturl: 8,
remotetab: 9,
extension: 10,
};
if (actionType in buckets) {
Services.telemetry

Просмотреть файл

@ -392,6 +392,7 @@ These should match what Safari and other Apple applications use on OS X Lion. --
<!ENTITY openCmd.commandkey "l">
<!ENTITY urlbar.placeholder2 "Search or enter address">
<!ENTITY urlbar.accesskey "d">
<!ENTITY urlbar.extension.label "Extension:">
<!ENTITY urlbar.switchToTab.label "Switch to tab:">
<!ENTITY urlbar.searchSuggestionsNotification.question "Would you like to improve your search experience with suggestions?">

Просмотреть файл

@ -79,6 +79,13 @@
height: 16px;
}
#urlbar[actiontype="extension"] > #identity-box > #identity-icon {
-moz-image-region: inherit;
list-style-image: url(chrome://browser/skin/addons/addon-install-anchor.svg);
width: 16px;
height: 16px;
}
/* SHARING ICON */
#sharing-icon {

Просмотреть файл

@ -0,0 +1,234 @@
/* 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/. */
"use strict";
this.EXPORTED_SYMBOLS = [ "ExtensionSearchHandler" ];
// Used to keep track of the active input session.
let gActiveKeyword = null;
// Used to keep track of who has control over the active suggestion callback
// so others can be ignored. The callback ID should increment whenever the input
// changes or the input session ends.
let gCurrentCallbackID = 0;
// The callback used to provided suggestions to the urlbar from the extension.
let gSuggestionsCallback = null;
// Maps keywords to KeywordInfo instances.
let gKeywordMap = new Map();
// Stores information associated to a registered keyword.
class KeywordInfo {
constructor(extension) {
this.extension = extension;
// The extension name is used as the default description
// for the heuristic result.
this.description = extension.name;
}
}
var ExtensionSearchHandler = Object.freeze({
MSG_INPUT_STARTED: "webext-omnibox-input-started",
MSG_INPUT_CHANGED: "webext-omnibox-input-changed",
MSG_INPUT_ENTERED: "webext-omnibox-input-entered",
MSG_INPUT_CANCELLED: "webext-omnibox-input-cancelled",
/**
* Registers a keyword.
*
* @param {string} keyword The keyword to register.
* @param {Extension} extension The extension registering the keyword.
*/
registerKeyword(keyword, extension) {
if (gKeywordMap.has(keyword)) {
throw new Error(`The keyword provided is already registered: ${keyword}`);
}
gKeywordMap.set(keyword, new KeywordInfo(extension));
},
/**
* Unregisters a keyword.
*
* @param {string} keyword The keyword to unregister.
*/
unregisterKeyword(keyword) {
if (!gKeywordMap.has(keyword)) {
throw new Error(`The keyword provided is not registered: ${keyword}`);
}
gActiveKeyword = null;
gKeywordMap.delete(keyword);
},
/**
* Checks if a keyword is registered.
* @param {string} keyword The word to check.
* @return {boolean} true if the word is a registered keyword.
*/
isKeywordRegistered(keyword) {
return gKeywordMap.has(keyword);
},
/**
* @param {string} keyword The keyword to look up.
* @return {string} the description to use for the heuristic result.
*/
getDescription(keyword) {
if (!gKeywordMap.has(keyword)) {
throw new Error(`The keyword provided is not registered: ${keyword}`);
}
return gKeywordMap.get(keyword).description;
},
/**
* @return {boolean} true if there is an input session is currently active.
*/
hasActiveInputSession() {
return gActiveKeyword != null;
},
/**
* Sets the default suggestion for the registered keyword. The suggestion's
* description will be used for the comment in the heuristic result.
*
* @param {string} keyword The keyword.
* @param {string} description The description to use for the heuristic result.
*/
setDefaultSuggestion(keyword, {description}) {
if (!gKeywordMap.has(keyword)) {
throw new Error(`The keyword provided is not registered: ${keyword}`);
}
gKeywordMap.get(keyword).description = description;
},
/**
* Adds suggestions for the registered keyword. This function will throw if
* the keyword provided is not registered or active, or if the callback ID
* provided is no longer equal to the active callback ID.
*
* @param {string} keyword The keyword.
* @param {integer} id The ID of the suggestion callback.
* @param {Array<Object>} suggestions An array of suggestions to provide to the urlbar.
* @return {boolean} true if a valid callback ID was provided; false otherwise.
*/
addSuggestions(keyword, id, suggestions) {
if (!gKeywordMap.has(keyword)) {
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;
}
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
* used to provide suggestions to the urlbar while the callback ID is active.
* The callback is invalidated when either the input changes or the urlbar blurs.
*
* @param {string} keyword The keyword to handle.
* @param {string} text The search text in the urlbar.
* @param {Function} callback The callback used to provide search suggestions.
*/
handleSearch(keyword, text, callback) {
if (!gKeywordMap.has(keyword)) {
throw new Error(`The keyword provided is not registered: ${keyword}`);
}
if (gActiveKeyword && keyword != gActiveKeyword) {
throw new Error("A different input session is already ongoing");
}
if (!callback) {
throw new Error("A callback must be provided");
}
let {extension} = gKeywordMap.get(keyword);
// Only emit MSG_INPUT_STARTED when the session first becomes active.
// This is different from Chrome's behavior, which fires MSG_INPUT_STARTED
// when MSG_INPUT_CHANGED first fires, but this is a bug in Chrome according
// to https://crbug.com/258911.
if (!gActiveKeyword) {
extension.emit(this.MSG_INPUT_STARTED);
}
// The search text in the urlbar currently starts with <keyword><space>, and
// we only want the text that follows.
text = text.substring(keyword.length + 1);
// Only emit MSG_INPUT_CHANGED if the session is already an active
// session or there is text to process.
if (gActiveKeyword || text.length) {
// Increment the callback ID before emitting MSG_INPUT_CHANGED so
// the active callback ID is passed to the extension.
gCurrentCallbackID++;
gSuggestionsCallback = callback;
extension.emit(this.MSG_INPUT_CHANGED, text, gCurrentCallbackID);
}
// Set the active keyword so we only emit MSG_INPUT_STARTED when the
// input session starts.
if (!gActiveKeyword) {
gActiveKeyword = keyword;
}
},
/**
* Called when the user submits a suggestion that was added by
* an extension. MSG_INPUT_ENTERED is emitted to the extension with
* the keyword, the current search string, and info about how the
* the search should be handled. In addition, the active keyword and
* active callback are reset to null.
*
* @param {string} keyword The keyword associated to the suggestion.
* @param {string} text The search text in the urlbar.
* @param {string} where Where the page should be opened.
*/
handleInputEntered(keyword, text, where) {
let dispositionMap = {
current: "currentTab",
tab: "newForegroundTab",
tabshifted: "newBackgroundTab",
}
let {extension} = gKeywordMap.get(keyword);
let disposition = dispositionMap[where] || dispositionMap.current;
// The search text in the urlbar currently starts with <keyword><space>, and
// we only want the text that follows.
text = text.substring(keyword.length + 1);
gCurrentCallbackID++;
gActiveKeyword = null;
extension.emit(this.MSG_INPUT_ENTERED, text, disposition);
},
/**
* If the user has ended the keyword input session without accepting the input,
* MSG_INPUT_CANCELLED is emitted, and the active keyword and active callback
* are reset to null.
*/
handleInputCancelled() {
if (!gActiveKeyword) {
throw new Error("There is no active input session to handle");
}
let {extension} = gKeywordMap.get(gActiveKeyword);
gCurrentCallbackID++;
gActiveKeyword = null;
extension.emit(this.MSG_INPUT_CANCELLED);
}
});

Просмотреть файл

@ -69,6 +69,11 @@ const FRECENCY_DEFAULT = 1000;
// always try to have at least MINIMUM_LOCAL_MATCHES local matches.
const MINIMUM_LOCAL_MATCHES = 6;
// Extensions are allowed to add suggestions if they have registered a keyword
// with the omnibox API. This is the maximum number of suggestions an extension
// is allowed to add for a given search string.
const MAXIMUM_ALLOWED_EXTENSION_MATCHES = 6;
// A regex that matches "single word" hostnames for whitelisting purposes.
// The hostname will already have been checked for general validity, so we
// don't need to be exhaustive here, so allow dashes anywhere.
@ -264,6 +269,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
"resource://gre/modules/PromiseUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ExtensionSearchHandler",
"resource://gre/modules/ExtensionSearchHandler.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesSearchAutocompleteProvider",
@ -715,11 +722,15 @@ function Search(searchString, searchParam, autocompleteListener,
// The index to insert remote matches at.
this._remoteMatchesStartIndex = 0;
// The index to insert local matches at.
this._localMatchesStartIndex = 0;
// Counts the number of inserted local matches.
this._localMatchesCount = 0;
// Counts the number of inserted remote matches.
this._remoteMatchesCount = 0;
// Counts the number of inserted extension matches.
this._extensionMatchesCount = 0;
}
Search.prototype = {
@ -942,6 +953,17 @@ Search.prototype = {
}
}
// Only add extension suggestions if the first token is a registered keyword
// and the search string has characters after the first token.
if (ExtensionSearchHandler.isKeywordRegistered(this._searchTokens[0]) &&
this._originalSearchString.length > this._searchTokens[0].length) {
// We don't yield here because, unlike the other calls, the extension can add
// suggestions until either the input changes or the urlbar blurs.
this._matchExtensionSuggestions();
} else if (ExtensionSearchHandler.hasActiveInputSession()) {
ExtensionSearchHandler.handleInputCancelled();
}
// Ensure to fill any remaining space.
yield Promise.all(this._remoteMatchesPromises);
}),
@ -950,7 +972,15 @@ Search.prototype = {
// We always try to make the first result a special "heuristic" result. The
// heuristics below determine what type of result it will be, if any.
let hasSearchTerms = this._searchTokens.length > 0 ;
let hasSearchTerms = this._searchTokens.length > 0;
if (hasSearchTerms) {
// It may be a keyword registered by an extension.
let matched = yield this._matchExtensionHeuristicResult();
if (matched) {
return true;
}
}
if (this._enableActions && hasSearchTerms) {
// It may be a search engine with an alias - which works like a keyword.
@ -1124,6 +1154,16 @@ Search.prototype = {
return gotResult;
},
_matchExtensionHeuristicResult: function* () {
if (ExtensionSearchHandler.isKeywordRegistered(this._searchTokens[0]) &&
this._originalSearchString.length > this._searchTokens[0].length) {
let description = ExtensionSearchHandler.getDescription(this._searchTokens[0]);
this._addExtensionMatch(this._originalSearchString, description);
return true;
}
return false;
},
_matchPlacesKeyword: function* () {
// The first word could be a keyword, so that's what we'll search.
let keyword = this._searchTokens[0];
@ -1236,6 +1276,24 @@ Search.prototype = {
return true;
},
_addExtensionMatch(content, comment) {
if (this._extensionMatchesCount >= MAXIMUM_ALLOWED_EXTENSION_MATCHES) {
return;
}
this._addMatch({
value: PlacesUtils.mozActionURI("extension", {
content,
keyword: this._searchTokens[0]
}),
comment,
icon: "chrome://browser/content/extension.svg",
style: "action extension",
frecency: FRECENCY_DEFAULT,
extension: true,
});
},
_addSearchEngineMatch(match, query, suggestion) {
let actionURLParams = {
engineName: match.engineName,
@ -1259,6 +1317,17 @@ Search.prototype = {
});
},
_matchExtensionSuggestions() {
ExtensionSearchHandler.handleSearch(this._searchTokens[0], this._originalSearchString,
suggestions => {
suggestions.forEach(suggestion => {
let content = `${this._searchTokens[0]} ${suggestion.content}`;
this._addExtensionMatch(content, suggestion.description);
});
}
);
},
*_matchRemoteTabs() {
let matches = yield PlacesRemoteTabsAutocompleteProvider.getMatches(this._originalSearchString);
for (let {url, title, icon, deviceClass, deviceName} of matches) {
@ -1464,6 +1533,11 @@ Search.prototype = {
// Append after local matches.
index = this._remoteMatchesStartIndex + this._remoteMatchesCount;
this._remoteMatchesCount++;
} else if (match.extension) {
index = this._localMatchesStartIndex;
this._localMatchesStartIndex++;
this._remoteMatchesStartIndex++;
this._extensionMatchesCount++;
} else {
// This is a local match.
if (match.frecency > FRECENCY_DEFAULT ||

Просмотреть файл

@ -64,6 +64,7 @@ if CONFIG['MOZ_PLACES']:
'ClusterLib.js',
'ColorAnalyzer_worker.js',
'ColorConversion.js',
'ExtensionSearchHandler.jsm',
'History.jsm',
'PlacesBackups.jsm',
'PlacesDBUtils.jsm',

Просмотреть файл

@ -415,6 +415,22 @@ function makeSwitchToTabMatch(url, extra = {}) {
}
}
function makeExtensionMatch(extra = {}) {
let style = [ "action", "extension" ];
if (extra.heuristic) {
style.push("heuristic");
}
return {
uri: makeActionURI("extension", {
content: extra.content,
keyword: extra.keyword,
}),
title: extra.description,
style,
};
}
function setFaviconForHref(href, iconHref) {
return new Promise(resolve => {
PlacesUtils.favicons.setAndFetchFaviconForPage(

Просмотреть файл

@ -0,0 +1,341 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
* vim:set ts=2 sw=2 sts=2 et:
* 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/. */
Cu.import("resource://gre/modules/ExtensionSearchHandler.jsm");
add_task(function* test_correct_errors_are_thrown() {
let keyword = "foo";
let anotherKeyword = "bar";
let unregisteredKeyword = "baz";
// Register a keyword.
ExtensionSearchHandler.registerKeyword(keyword, { emit: () => {} });
// Try registering the keyword again.
Assert.throws(() => ExtensionSearchHandler.registerKeyword(keyword, { emit: () => {} }));
// Register a different keyword.
ExtensionSearchHandler.registerKeyword(anotherKeyword, { emit: () => {} });
// Try calling handleSearch for an unregistered keyword.
Assert.throws(() => ExtensionSearchHandler.handleSearch(unregisteredKeyword, `${unregisteredKeyword} `, () => {}));
// Try calling handleSearch without a callback.
Assert.throws(() => ExtensionSearchHandler.handleSearch(unregisteredKeyword, `${unregisteredKeyword} `));
// Try getting the description for a keyword which isn't registered.
Assert.throws(() => ExtensionSearchHandler.getDescription(unregisteredKeyword));
// Try getting the extension name for a keyword which isn't registered.
Assert.throws(() => ExtensionSearchHandler.getExtensionName(unregisteredKeyword));
// Try setting the default suggestion for a keyword which isn't registered.
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, []));
// Add suggestions for a valid callback ID.
Assert.ok(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
ExtensionSearchHandler.handleSearch(anotherKeyword, `${anotherKeyword} test`, () => {});
// Set the default suggestion.
ExtensionSearchHandler.setDefaultSuggestion(anotherKeyword, {description: "test result"});
// End the session by calling handleInputEntered.
ExtensionSearchHandler.handleInputEntered(anotherKeyword, `${anotherKeyword} test`, "tab");
// Try handling input after the session has ended using handleInputCancelled.
Assert.throws(() => ExtensionSearchHandler.handleInputCancelled());
// Unregister the keyword.
ExtensionSearchHandler.unregisterKeyword(keyword);
// Try setting the default suggestion for the unregistered keyword.
Assert.throws(() => ExtensionSearchHandler.setDefaultSuggestion(keyword, {description: "test"}));
// Try handling a search with the unregistered keyword.
Assert.throws(() => ExtensionSearchHandler.handleSearch(keyword, `${keyword} test`, () => {}));
// Try unregistering the keyword again.
Assert.throws(() => ExtensionSearchHandler.unregisterKeyword(keyword));
// Unregister the other keyword.
ExtensionSearchHandler.unregisterKeyword(anotherKeyword);
// Try unregistering the word which was never registered.
Assert.throws(() => ExtensionSearchHandler.unregisterKeyword(unregisteredKeyword));
// Try setting the default suggestion for a word that was never registered.
Assert.throws(() => ExtensionSearchHandler.setDefaultSuggestion(unregisteredKeyword, {description: "test"}));
yield cleanup();
});
add_task(function* test_correct_events_are_emitted() {
let events = [];
function checkEvents(expectedEvents) {
Assert.equal(events.length, expectedEvents.length, "The correct number of events fired");
expectedEvents.forEach((e, i) => Assert.equal(e, events[i], `Expected "${e}" event to fire`));
events = [];
}
let mockExtension = { emit: message => events.push(message) };
let keyword = "foo";
let anotherKeyword = "bar";
ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
ExtensionSearchHandler.registerKeyword(anotherKeyword, mockExtension);
ExtensionSearchHandler.handleSearch(keyword, `${keyword} `, () => {});
checkEvents([ExtensionSearchHandler.MSG_INPUT_STARTED]);
ExtensionSearchHandler.handleSearch(keyword, `${keyword} f`, () => {});
checkEvents([ExtensionSearchHandler.MSG_INPUT_CHANGED]);
ExtensionSearchHandler.handleInputEntered(keyword, `${keyword} f`, "tab");
checkEvents([ExtensionSearchHandler.MSG_INPUT_ENTERED]);
ExtensionSearchHandler.handleSearch(keyword, `${keyword} f`, () => {});
checkEvents([ExtensionSearchHandler.MSG_INPUT_STARTED, ExtensionSearchHandler.MSG_INPUT_CHANGED]);
ExtensionSearchHandler.handleInputCancelled();
checkEvents([ExtensionSearchHandler.MSG_INPUT_CANCELLED]);
ExtensionSearchHandler.handleSearch(anotherKeyword, `${anotherKeyword} baz`, () => {});
checkEvents([ExtensionSearchHandler.MSG_INPUT_STARTED, ExtensionSearchHandler.MSG_INPUT_CHANGED]);
ExtensionSearchHandler.handleInputEntered(keyword, `${anotherKeyword} baz`, "tab");
checkEvents([ExtensionSearchHandler.MSG_INPUT_ENTERED]);
ExtensionSearchHandler.unregisterKeyword(keyword);
});
add_task(function* test_removes_suggestion_if_its_content_is_typed_in() {
let keyword = "test";
let extensionName = "Foo Bar";
let mockExtension = {
name: extensionName,
emit(message, text, id) {
if (message === ExtensionSearchHandler.MSG_INPUT_CHANGED) {
ExtensionSearchHandler.addSuggestions(keyword, id, [
{content: "foo", description: "first suggestion"},
{content: "bar", description: "second suggestion"},
{content: "baz", description: "third suggestion"},
]);
}
}
};
ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
yield check_autocomplete({
search: `${keyword} `,
searchParam: "enable-actions",
matches: [
makeExtensionMatch({heuristic: true, keyword, description: extensionName, content: `${keyword} `}),
]
});
yield check_autocomplete({
search: `${keyword} unmatched`,
searchParam: "enable-actions",
matches: [
makeExtensionMatch({heuristic: true, keyword, description: extensionName, content: `${keyword} unmatched`}),
makeExtensionMatch({keyword, content: `${keyword} foo`, description: "first suggestion"}),
makeExtensionMatch({keyword, content: `${keyword} bar`, description: "second suggestion"}),
makeExtensionMatch({keyword, content: `${keyword} baz`, description: "third suggestion"})
]
});
yield check_autocomplete({
search: `${keyword} foo`,
searchParam: "enable-actions",
matches: [
makeExtensionMatch({heuristic: true, keyword, description: extensionName, content: `${keyword} foo`}),
makeExtensionMatch({keyword, content: `${keyword} bar`, description: "second suggestion"}),
makeExtensionMatch({keyword, content: `${keyword} baz`, description: "third suggestion"})
]
});
yield check_autocomplete({
search: `${keyword} bar`,
searchParam: "enable-actions",
matches: [
makeExtensionMatch({heuristic: true, keyword, description: extensionName, content: `${keyword} bar`}),
makeExtensionMatch({keyword, content: `${keyword} foo`, description: "first suggestion"}),
makeExtensionMatch({keyword, content: `${keyword} baz`, description: "third suggestion"})
]
});
yield check_autocomplete({
search: `${keyword} baz`,
searchParam: "enable-actions",
matches: [
makeExtensionMatch({heuristic: true, keyword, description: extensionName, content: `${keyword} baz`}),
makeExtensionMatch({keyword, content: `${keyword} foo`, description: "first suggestion"}),
makeExtensionMatch({keyword, content: `${keyword} bar`, description: "second suggestion"})
]
});
ExtensionSearchHandler.unregisterKeyword(keyword);
yield cleanup();
});
add_task(function* test_extension_results_should_come_first() {
let keyword = "test";
let extensionName = "Omnibox Example";
let uri = NetUtil.newURI(`http://a.com/b`);
yield PlacesTestUtils.addVisits([
{ uri, title: `${keyword} -` },
]);
let mockExtension = {
name: extensionName,
emit(message, text, id) {
if (message === ExtensionSearchHandler.MSG_INPUT_CHANGED) {
ExtensionSearchHandler.addSuggestions(keyword, id, [
{content: "foo", description: "first suggestion"},
{content: "bar", description: "second suggestion"},
{content: "baz", description: "third suggestion"},
]);
}
}
};
ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
// Start an input session before testing MSG_INPUT_CHANGED.
ExtensionSearchHandler.handleSearch(keyword, `${keyword} `, () => {});
yield check_autocomplete({
search: `${keyword} -`,
searchParam: "enable-actions",
matches: [
makeExtensionMatch({heuristic: true, keyword, description: extensionName, content: `${keyword} -`}),
makeExtensionMatch({keyword, content: `${keyword} foo`, description: "first suggestion"}),
makeExtensionMatch({keyword, content: `${keyword} bar`, description: "second suggestion"}),
makeExtensionMatch({keyword, content: `${keyword} baz`, description: "third suggestion"}),
{ uri, title: `${keyword} -` }
]
});
ExtensionSearchHandler.unregisterKeyword(keyword);
yield cleanup();
});
add_task(function* test_setting_the_default_suggestion() {
let keyword = "test";
let extensionName = "Omnibox Example";
let mockExtension = {
name: extensionName,
emit() {}
};
ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
ExtensionSearchHandler.setDefaultSuggestion(keyword, {
description: "hello world"
});
let searchString = `${keyword} search query`;
yield check_autocomplete({
search: searchString,
searchParam: "enable-actions",
matches: [
makeExtensionMatch({heuristic: true, keyword, description: "hello world", content: searchString}),
]
});
ExtensionSearchHandler.setDefaultSuggestion(keyword, {
description: "foo bar"
});
yield check_autocomplete({
search: searchString,
searchParam: "enable-actions",
matches: [
makeExtensionMatch({heuristic: true, keyword, description: "foo bar", content: searchString}),
]
});
ExtensionSearchHandler.unregisterKeyword(keyword);
yield cleanup();
});
add_task(function* test_maximum_number_of_suggestions_is_enforced() {
let keyword = "test";
let extensionName = "Omnibox Example";
let mockExtension = {
name: extensionName,
emit(message, text, id) {
if (message === ExtensionSearchHandler.MSG_INPUT_CHANGED) {
ExtensionSearchHandler.addSuggestions(keyword, id, [
{content: "a", description: "first suggestion"},
{content: "b", description: "second suggestion"},
{content: "c", description: "third suggestion"},
{content: "d", description: "fourth suggestion"},
{content: "e", description: "fifth suggestion"},
{content: "f", description: "sixth suggestion"},
{content: "g", description: "seventh suggestion"},
{content: "h", description: "eigth suggestion"},
{content: "i", description: "ninth suggestion"},
{content: "j", description: "tenth suggestion"},
]);
}
}
};
ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
// Start an input session before testing MSG_INPUT_CHANGED.
ExtensionSearchHandler.handleSearch(keyword, `${keyword} `, () => {});
yield check_autocomplete({
search: `${keyword} #`,
searchParam: "enable-actions",
matches: [
makeExtensionMatch({heuristic: true, keyword, description: extensionName, content: `${keyword} #`}),
makeExtensionMatch({keyword, content: `${keyword} a`, description: "first suggestion"}),
makeExtensionMatch({keyword, content: `${keyword} b`, description: "second suggestion"}),
makeExtensionMatch({keyword, content: `${keyword} c`, description: "third suggestion"}),
makeExtensionMatch({keyword, content: `${keyword} d`, description: "fourth suggestion"}),
makeExtensionMatch({keyword, content: `${keyword} e`, description: "fifth suggestion"}),
]
});
ExtensionSearchHandler.unregisterKeyword(keyword);
yield cleanup();
});

Просмотреть файл

@ -24,6 +24,7 @@ support-files =
[test_empty_search.js]
[test_enabled.js]
[test_escape_self.js]
[test_extension_matches.js]
[test_ignore_protocol.js]
[test_keyword_search.js]
[test_keyword_search_actions.js]

Просмотреть файл

@ -2125,6 +2125,10 @@ extends="chrome://global/content/bindings/popup.xml#popup">
titleLooksLikeUrl = true;
let visitStr = this._stringBundle.GetStringFromName("visit");
this._setUpDescription(this._actionText, visitStr, true);
} else if (action.type == "extension") {
let content = action.params.content;
displayUrl = content;
this._setUpDescription(this._actionText, content, true);
}
}