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

MozReview-Commit-ID: Ghqe5xLw67Y

--HG--
extra : rebase_source : ef23c5eb90a7e882c09736724ed5b63773be061c
This commit is contained in:
Matthew Wein 2016-11-11 02:06:14 +00:00
Родитель db0967a435
Коммит 1dc56d3c39
13 изменённых файлов: 845 добавлений и 12 удалений

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

@ -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,11 @@ 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">
(Components.utils.import("resource://gre/modules/ExtensionSearchHandler.jsm", {})).ExtensionSearchHandler;
</field>
<constructor><![CDATA[
this._prefs = Components.classes["@mozilla.org/preferences-service;1"]
.getService(Components.interfaces.nsIPrefService)
@ -174,6 +179,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);
@ -478,6 +487,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 {
// This is a fallback for add-ons and old testing code that directly
@ -595,7 +611,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";
@ -1173,6 +1189,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,312 @@
/* 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 all of the registered keywords, where each keyword is
// mapped to a KeywordInfo instance.
let gKeywordMap = new Map();
// Used to keep track of the active input session.
let gActiveInputSession = null;
// Used to keep track of who has control over the active suggestion callback
// so older callbacks can be ignored. The callback ID should increment whenever
// the input changes or the input session ends.
let gCurrentCallbackID = 0;
// Handles keeping track of information associated to the registered keyword.
class KeywordInfo {
constructor(extension, description) {
this._extension = extension;
this._description = description;
}
get description() {
return this._description;
}
set description(desc) {
this._description = desc;
}
get extension() {
return this._extension;
}
}
// Responsible for handling communication between the extension and the urlbar.
class InputSession {
constructor(keyword, extension) {
this._keyword = keyword;
this._extension = extension;
this._suggestionsCallback = null;
this._searchCompleteCallback = null;
}
get keyword() {
return this._keyword;
}
registerSuggestionsCallback(callback) {
this._suggestionsCallback = callback;
}
registerSearchCompleteCallback(callback) {
this._onSearchComplete = callback;
}
addSuggestions(suggestions) {
if (this._suggestionsCallback) {
this._suggestionsCallback(suggestions);
}
}
start(eventName) {
this._extension.emit(eventName);
}
update(eventName, text) {
this._extension.emit(eventName, text, ++gCurrentCallbackID);
if (this._searchCompleteCallback) {
this._searchCompleteCallback();
}
}
cancel(eventName) {
this._extension.emit(eventName);
if (this._searchCompleteCallback) {
this._searchCompleteCallback();
}
}
end(eventName, text, disposition) {
this._extension.emit(eventName, text, disposition);
if (this._searchCompleteCallback) {
this._searchCompleteCallback();
}
}
}
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, extension.name));
},
/**
* 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}"`);
}
gActiveInputSession = 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);
},
/**
* @return {boolean} true if there is an active input session.
*/
hasActiveInputSession() {
return gActiveInputSession != null;
},
/**
* @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;
},
/**
* 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.
*/
addSuggestions(keyword, id, suggestions) {
if (!gKeywordMap.has(keyword)) {
throw new Error(`The keyword provided is not registered: "${keyword}"`);
}
if (!gActiveInputSession || gActiveInputSession.keyword != keyword) {
throw new Error(`The keyword provided is not apart of an active input session: "${keyword}"`);
}
if (id != gCurrentCallbackID) {
throw new Error(`The callback is no longer active for the keyword provided: "${keyword}"`);
}
gActiveInputSession.addSuggestions(suggestions);
},
/**
* 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.
* @return {Promise} promise that resolves when the current search is complete.
*/
handleSearch(keyword, text, callback) {
if (!gKeywordMap.has(keyword)) {
throw new Error(`The keyword provided is not registered: "${keyword}"`);
}
if (gActiveInputSession && gActiveInputSession.keyword != keyword) {
throw new Error("A different input session is already ongoing");
}
if (!text || !text.startsWith(`${keyword} `)) {
throw new Error(`The text provided must start with: "${keyword} "`);
}
if (!callback) {
throw new Error("A callback must be provided");
}
let {extension} = gKeywordMap.get(keyword);
// 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);
// We fire MSG_INPUT_STARTED once we have <keyword><space>, and only fire
// MSG_INPUT_CHANGED when we have text to process. This is different from
// Chrome's behavior, which fires MSG_INPUT_STARTED right before MSG_INPUT_CHANGED
// first fires, but this is a bug in Chrome according to https://crbug.com/258911.
if (!gActiveInputSession) {
gActiveInputSession = new InputSession(keyword, extension);
gActiveInputSession.start(this.MSG_INPUT_STARTED);
// Update the input session if there is text to process.
if (text.length) {
gActiveInputSession.registerSuggestionsCallback(callback);
gActiveInputSession.update(this.MSG_INPUT_CHANGED, text);
}
return Promise.resolve();
}
gActiveInputSession.registerSuggestionsCallback(callback);
gActiveInputSession.update(this.MSG_INPUT_CHANGED, text);
return new Promise(resolve => {
gActiveInputSession.registerSearchCompleteCallback(resolve);
});
},
/**
* 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. This ends the active input session.
*
* @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. Accepted values are:
* "current": open the page in the same tab.
* "tab": open the page in a new foreground tab.
* "tabshifted": open the page in a new background tab.
*/
handleInputEntered(keyword, text, where) {
if (!gKeywordMap.has(keyword)) {
throw new Error(`The keyword provided is not registered: "${keyword}"`);
}
if (gActiveInputSession && gActiveInputSession.keyword != keyword) {
throw new Error("A different input session is already ongoing");
}
if (!text || !text.startsWith(`${keyword} `)) {
throw new Error(`The text provided must start with: "${keyword} "`);
}
let dispositionMap = {
current: "currentTab",
tab: "newForegroundTab",
tabshifted: "newBackgroundTab",
}
let disposition = dispositionMap[where];
if (!disposition) {
throw new Error(`Invalid "where" argument: ${where}`);
}
let {extension} = gKeywordMap.get(keyword);
// The search text in the urlbar currently starts with <keyword><space>, and
// we only want to send the text that follows.
text = text.substring(keyword.length + 1);
gActiveInputSession.end(this.MSG_INPUT_ENTERED, text, disposition)
gActiveInputSession = null;
},
/**
* If the user has ended the keyword input session without accepting the input,
* MSG_INPUT_CANCELLED is emitted and the input session is ended.
*/
handleInputCancelled() {
if (!gActiveInputSession) {
throw new Error("There is no active input session");
}
gActiveInputSession.cancel(this.MSG_INPUT_CANCELLED);
gActiveInputSession = null;
}
});

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

@ -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,16 @@ 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,7 +954,19 @@ Search.prototype = {
}
}
// Ensure to fill any remaining space.
// 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) {
yield this._matchExtensionSuggestions();
if (!this.pending)
return;
} else if (ExtensionSearchHandler.hasActiveInputSession()) {
ExtensionSearchHandler.handleInputCancelled();
}
// Ensure to fill any remaining space. Suggestions which come from extensions are
// inserted at the beginning, so any suggestions
yield Promise.all(this._remoteMatchesPromises);
}),
@ -950,7 +974,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 +1156,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 +1278,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 +1319,18 @@ Search.prototype = {
});
},
*_matchExtensionSuggestions() {
let promise = ExtensionSearchHandler.handleSearch(this._searchTokens[0], this._originalSearchString,
suggestions => {
suggestions.forEach(suggestion => {
let content = `${this._searchTokens[0]} ${suggestion.content}`;
this._addExtensionMatch(content, suggestion.description);
});
}
);
this._remoteMatchesPromises.push(promise);
},
*_matchRemoteTabs() {
let matches = yield PlacesRemoteTabsAutocompleteProvider.getMatches(this._originalSearchString);
for (let {url, title, icon, deviceName} of matches) {
@ -1464,6 +1536,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',

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

@ -167,20 +167,21 @@ function* check_autocomplete(test) {
do_print("onSearchBegin received");
numSearchesStarted++;
};
let deferred = Promise.defer();
input.onSearchComplete = () => {
do_print("onSearchComplete received");
deferred.resolve();
}
let searchCompletePromise = new Promise(resolve => {
input.onSearchComplete = () => {
do_print("onSearchComplete received");
resolve();
}
});
let expectedSearches = 1;
if (test.incompleteSearch) {
controller.startSearch(test.incompleteSearch);
expectedSearches++;
}
do_print("Searching for: '" + test.search + "'");
controller.startSearch(test.search);
yield deferred.promise;
yield searchCompletePromise;
Assert.equal(numSearchesStarted, expectedSearches, "All searches started");
@ -415,6 +416,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,384 @@
/* -*- 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");
let controller = Cc["@mozilla.org/autocomplete/controller;1"].getService(Ci.nsIAutoCompleteController);
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 input session.
Assert.throws(() => ExtensionSearchHandler.handleInputCancelled());
// Try calling handleInputEntered when there is no active input session.
Assert.throws(() => ExtensionSearchHandler.handleInputEntered(anotherKeyword, `${anotherKeyword} test`, "tab"));
// 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 calling addSuggestions with an old callback ID.
Assert.throws(() => ExtensionSearchHandler.addSuggestions(keyword, 0, []));
// Add suggestions with a valid callback ID.
ExtensionSearchHandler.addSuggestions(keyword, 1, []);
// Add suggestions again with a valid callback ID.
ExtensionSearchHandler.addSuggestions(keyword, 1, []);
// Try calling addSuggestions with a future callback ID.
Assert.throws(() => ExtensionSearchHandler.addSuggestions(keyword, 2, []));
// End the input session by calling handleInputCancelled.
ExtensionSearchHandler.handleInputCancelled();
// Try calling handleInputCancelled after the session has ended.
Assert.throws(() => ExtensionSearchHandler.handleInputCancelled());
// Try calling handleSearch that doesn't have a space after the keyword.
Assert.throws(() => ExtensionSearchHandler.handleSearch(anotherKeyword, `${anotherKeyword}`, () => {}));
// Try calling handleSearch with text starting with the wrong keyword.
Assert.throws(() => ExtensionSearchHandler.handleSearch(anotherKeyword, `${keyword} test`, () => {}));
// Start a new session by calling handleSearch with a different keyword
ExtensionSearchHandler.handleSearch(anotherKeyword, `${anotherKeyword} test`, () => {});
// Try adding suggestions again with the same callback ID now that the input session has ended.
Assert.throws(() => ExtensionSearchHandler.addSuggestions(keyword, 1, []));
// Add suggestions with a valid callback ID.
ExtensionSearchHandler.addSuggestions(anotherKeyword, 2, []);
// Try adding suggestions with a valid callback ID but a different keyword.
Assert.throws(() => ExtensionSearchHandler.addSuggestions(keyword, 2, []));
// Try adding suggestions with a valid callback ID but an unregistered keyword.
Assert.throws(() => ExtensionSearchHandler.addSuggestions(unregisteredKeyword, 2, []));
// Set the default suggestion.
ExtensionSearchHandler.setDefaultSuggestion(anotherKeyword, {description: "test result"});
// Try ending the session using handleInputEntered with a different keyword.
Assert.throws(() => ExtensionSearchHandler.handleInputEntered(keyword, `${keyword} test`, "tab"));
// Try calling handleInputEntered with invalid text.
Assert.throws(() => ExtensionSearchHandler.handleInputEntered(anotherKeyword, ` test`, "tab"));
// Try calling handleInputEntered with an invalid disposition.
Assert.throws(() => ExtensionSearchHandler.handleInputEntered(anotherKeyword, `${anotherKeyword} test`, "invalid"));
// End the session by calling handleInputEntered.
ExtensionSearchHandler.handleInputEntered(anotherKeyword, `${anotherKeyword} test`, "tab");
// Try calling handleInputEntered after the session has ended.
Assert.throws(() => ExtensionSearchHandler.handleInputEntered(anotherKeyword, `${anotherKeyword} test`, "tab"));
// 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(anotherKeyword, `${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"},
]);
controller.stopSearch();
}
}
};
ExtensionSearchHandler.registerKeyword(keyword, mockExtension);
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"},
]);
}
controller.stopSearch();
}
};
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(message, text, id) {
if (message === ExtensionSearchHandler.MSG_INPUT_CHANGED) {
ExtensionSearchHandler.addSuggestions(keyword, id, []);
}
controller.stopSearch();
}
};
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"},
]);
controller.stopSearch();
}
}
};
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]

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

@ -2127,6 +2127,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);
}
}