Mypal/toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js

506 lines
16 KiB
JavaScript

/* 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/. */
var Ci = Components.interfaces;
var Cc = Components.classes;
var Cr = Components.results;
var Cu = Components.utils;
const FRECENCY_DEFAULT = 10000;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://testing-common/httpd.js");
// Import common head.
{
let commonFile = do_get_file("../head_common.js", false);
let uri = Services.io.newFileURI(commonFile);
Services.scriptloader.loadSubScript(uri.spec, this);
}
// Put any other stuff relative to this test folder below.
const TITLE_SEARCH_ENGINE_SEPARATOR = " \u00B7\u2013\u00B7 ";
function run_test() {
run_next_test();
}
function* cleanup() {
Services.prefs.clearUserPref("browser.urlbar.autocomplete.enabled");
Services.prefs.clearUserPref("browser.urlbar.autoFill");
Services.prefs.clearUserPref("browser.urlbar.autoFill.typed");
Services.prefs.clearUserPref("browser.urlbar.autoFill.searchEngines");
let suggestPrefs = [
"history",
"bookmark",
"history.onlyTyped",
"openpage",
"searches",
];
for (let type of suggestPrefs) {
Services.prefs.clearUserPref("browser.urlbar.suggest." + type);
}
Services.prefs.clearUserPref("browser.search.suggest.enabled");
yield PlacesUtils.bookmarks.eraseEverything();
yield PlacesTestUtils.clearHistory();
}
do_register_cleanup(cleanup);
/**
* @param aSearches
* Array of AutoCompleteSearch names.
*/
function AutoCompleteInput(aSearches) {
this.searches = aSearches;
}
AutoCompleteInput.prototype = {
popup: {
selectedIndex: -1,
invalidate: function () {},
QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompletePopup])
},
popupOpen: false,
disableAutoComplete: false,
completeDefaultIndex: true,
completeSelectedIndex: true,
forceComplete: false,
minResultsForPopup: 0,
maxRows: 0,
showCommentColumn: false,
showImageColumn: false,
timeout: 10,
searchParam: "",
get searchCount() {
return this.searches.length;
},
getSearchAt: function(aIndex) {
return this.searches[aIndex];
},
textValue: "",
// Text selection range
_selStart: 0,
_selEnd: 0,
get selectionStart() {
return this._selStart;
},
get selectionEnd() {
return this._selEnd;
},
selectTextRange: function(aStart, aEnd) {
this._selStart = aStart;
this._selEnd = aEnd;
},
onSearchBegin: function () {},
onSearchComplete: function () {},
onTextEntered: () => false,
onTextReverted: () => false,
QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompleteInput])
}
// A helper for check_autocomplete to check a specific match against data from
// the controller.
function _check_autocomplete_matches(match, result) {
let { uri, title, tags, style } = match;
if (tags)
title += " \u2013 " + tags.sort().join(", ");
if (style)
style = style.sort();
else
style = ["favicon"];
do_print(`Checking against expected "${uri.spec}", "${title}"`);
// Got a match on both uri and title?
if (stripPrefix(uri.spec) != stripPrefix(result.value) || title != result.comment) {
return false;
}
let actualStyle = result.style.split(/\s+/).sort();
if (style)
Assert.equal(actualStyle.toString(), style.toString(), "Match should have expected style");
if (uri.spec.startsWith("moz-action:")) {
Assert.ok(actualStyle.includes("action"), "moz-action results should always have 'action' in their style");
}
if (match.icon)
Assert.equal(result.image, match.icon, "Match should have expected image");
return true;
}
function* check_autocomplete(test) {
// At this point frecency could still be updating due to latest pages
// updates.
// This is not a problem in real life, but autocomplete tests should
// return reliable resultsets, thus we have to wait.
yield PlacesTestUtils.promiseAsyncUpdates();
// Make an AutoCompleteInput that uses our searches and confirms results.
let input = new AutoCompleteInput(["unifiedcomplete"]);
input.textValue = test.search;
if (test.searchParam)
input.searchParam = test.searchParam;
// Caret must be at the end for autoFill to happen.
let strLen = test.search.length;
input.selectTextRange(strLen, strLen);
Assert.equal(input.selectionStart, strLen, "Selection starts at end");
Assert.equal(input.selectionEnd, strLen, "Selection ends at the end");
let controller = Cc["@mozilla.org/autocomplete/controller;1"]
.getService(Ci.nsIAutoCompleteController);
controller.input = input;
let numSearchesStarted = 0;
input.onSearchBegin = () => {
do_print("onSearchBegin received");
numSearchesStarted++;
};
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 searchCompletePromise;
Assert.equal(numSearchesStarted, expectedSearches, "All searches started");
// Check to see the expected uris and titles match up. If 'enable-actions'
// is specified, we check that the first specified match is the first
// controller value (as this is the "special" always selected item), but the
// rest can match in any order.
// If 'enable-actions' is not specified, they can match in any order.
if (test.matches) {
// Do not modify the test original matches.
let matches = test.matches.slice();
if (matches.length) {
let firstIndexToCheck = 0;
if (test.searchParam && test.searchParam.includes("enable-actions")) {
firstIndexToCheck = 1;
do_print("Checking first match is first autocomplete entry")
let result = {
value: controller.getValueAt(0),
comment: controller.getCommentAt(0),
style: controller.getStyleAt(0),
image: controller.getImageAt(0),
}
do_print(`First match is "${result.value}", "${result.comment}"`);
Assert.ok(_check_autocomplete_matches(matches[0], result), "first item is correct");
do_print("Checking rest of the matches");
}
for (let i = firstIndexToCheck; i < controller.matchCount; i++) {
let result = {
value: controller.getValueAt(i),
comment: controller.getCommentAt(i),
style: controller.getStyleAt(i),
image: controller.getImageAt(i),
}
do_print(`Looking for "${result.value}", "${result.comment}" in expected results...`);
let lowerBound = test.checkSorting ? i : firstIndexToCheck;
let upperBound = test.checkSorting ? i + 1 : matches.length;
let found = false;
for (let j = lowerBound; j < upperBound; ++j) {
// Skip processed expected results
if (matches[j] == undefined)
continue;
if (_check_autocomplete_matches(matches[j], result)) {
do_print("Got a match at index " + j + "!");
// Make it undefined so we don't process it again
matches[j] = undefined;
found = true;
break;
}
}
if (!found)
do_throw(`Didn't find the current result ("${result.value}", "${result.comment}") in matches`); // ' (Emacs syntax highlighting fix)
}
}
Assert.equal(controller.matchCount, matches.length,
"Got as many results as expected");
// If we expect results, make sure we got matches.
do_check_eq(controller.searchStatus, matches.length ?
Ci.nsIAutoCompleteController.STATUS_COMPLETE_MATCH :
Ci.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH);
}
if (test.autofilled) {
// Check the autoFilled result.
Assert.equal(input.textValue, test.autofilled,
"Autofilled value is correct");
// Now force completion and check correct casing of the result.
// This ensures the controller is able to do its magic case-preserving
// stuff and correct replacement of the user's casing with result's one.
controller.handleEnter(false);
Assert.equal(input.textValue, test.completed,
"Completed value is correct");
}
}
var addBookmark = Task.async(function* (aBookmarkObj) {
Assert.ok(!!aBookmarkObj.uri, "Bookmark object contains an uri");
let parentId = aBookmarkObj.parentId ? aBookmarkObj.parentId
: PlacesUtils.unfiledBookmarksFolderId;
let bm = yield PlacesUtils.bookmarks.insert({
parentGuid: (yield PlacesUtils.promiseItemGuid(parentId)),
title: aBookmarkObj.title || "A bookmark",
url: aBookmarkObj.uri
});
yield PlacesUtils.promiseItemId(bm.guid);
if (aBookmarkObj.keyword) {
yield PlacesUtils.keywords.insert({ keyword: aBookmarkObj.keyword,
url: aBookmarkObj.uri.spec,
postData: aBookmarkObj.postData
});
}
if (aBookmarkObj.tags) {
PlacesUtils.tagging.tagURI(aBookmarkObj.uri, aBookmarkObj.tags);
}
});
function addOpenPages(aUri, aCount=1) {
let ac = Cc["@mozilla.org/autocomplete/search;1?name=unifiedcomplete"]
.getService(Ci.mozIPlacesAutoComplete);
for (let i = 0; i < aCount; i++) {
ac.registerOpenPage(aUri);
}
}
function removeOpenPages(aUri, aCount=1) {
let ac = Cc["@mozilla.org/autocomplete/search;1?name=unifiedcomplete"]
.getService(Ci.mozIPlacesAutoComplete);
for (let i = 0; i < aCount; i++) {
ac.unregisterOpenPage(aUri);
}
}
function changeRestrict(aType, aChar) {
let branch = "browser.urlbar.";
// "title" and "url" are different from everything else, so special case them.
if (aType == "title" || aType == "url")
branch += "match.";
else
branch += "restrict.";
do_print("changing restrict for " + aType + " to '" + aChar + "'");
Services.prefs.setCharPref(branch + aType, aChar);
}
function resetRestrict(aType) {
let branch = "browser.urlbar.";
// "title" and "url" are different from everything else, so special case them.
if (aType == "title" || aType == "url")
branch += "match.";
else
branch += "restrict.";
Services.prefs.clearUserPref(branch + aType);
}
/**
* Strip prefixes from the URI that we don't care about for searching.
*
* @param spec
* The text to modify.
* @return the modified spec.
*/
function stripPrefix(spec)
{
["http://", "https://", "ftp://"].some(scheme => {
if (spec.startsWith(scheme)) {
spec = spec.slice(scheme.length);
return true;
}
return false;
});
if (spec.startsWith("www.")) {
spec = spec.slice(4);
}
return spec;
}
function makeActionURI(action, params) {
let encodedParams = {};
for (let key in params) {
encodedParams[key] = encodeURIComponent(params[key]);
}
let url = "moz-action:" + action + "," + JSON.stringify(encodedParams);
return NetUtil.newURI(url);
}
// Creates a full "match" entry for a search result, suitable for passing as
// an entry to check_autocomplete.
function makeSearchMatch(input, extra = {}) {
// Note that counter-intuitively, the order the object properties are defined
// in the object passed to makeActionURI is important for check_autocomplete
// to match them :(
let params = {
engineName: extra.engineName || "MozSearch",
input,
searchQuery: "searchQuery" in extra ? extra.searchQuery : input,
};
if ("alias" in extra) {
// May be undefined, which is expected, but in that case make sure it's not
// included in the params of the moz-action URL.
params.alias = extra.alias;
}
let style = [ "action", "searchengine" ];
if (Array.isArray(extra.style)) {
style.push(...extra.style);
}
if (extra.heuristic) {
style.push("heuristic");
}
return {
uri: makeActionURI("searchengine", params),
title: params.engineName,
style,
}
}
// Creates a full "match" entry for a search result, suitable for passing as
// an entry to check_autocomplete.
function makeVisitMatch(input, url, extra = {}) {
// Note that counter-intuitively, the order the object properties are defined
// in the object passed to makeActionURI is important for check_autocomplete
// to match them :(
let params = {
url,
input,
}
let style = [ "action", "visiturl" ];
if (extra.heuristic) {
style.push("heuristic");
}
return {
uri: makeActionURI("visiturl", params),
title: extra.title || url,
style,
}
}
function makeSwitchToTabMatch(url, extra = {}) {
return {
uri: makeActionURI("switchtab", {url}),
title: extra.title || url,
style: [ "action", "switchtab" ],
}
}
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(
NetUtil.newURI(href),
NetUtil.newURI(iconHref),
true,
PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
resolve,
Services.scriptSecurityManager.getSystemPrincipal()
);
});
}
function makeTestServer(port=-1) {
let httpServer = new HttpServer();
httpServer.start(port);
do_register_cleanup(() => httpServer.stop(() => {}));
return httpServer;
}
function* addTestEngine(basename, httpServer=undefined) {
httpServer = httpServer || makeTestServer();
httpServer.registerDirectory("/", do_get_cwd());
let dataUrl =
"http://localhost:" + httpServer.identity.primaryPort + "/data/";
do_print("Adding engine: " + basename);
return yield new Promise(resolve => {
Services.obs.addObserver(function obs(subject, topic, data) {
let engine = subject.QueryInterface(Ci.nsISearchEngine);
do_print("Observed " + data + " for " + engine.name);
if (data != "engine-added" || engine.name != basename) {
return;
}
Services.obs.removeObserver(obs, "browser-search-engine-modified");
do_register_cleanup(() => Services.search.removeEngine(engine));
resolve(engine);
}, "browser-search-engine-modified", false);
do_print("Adding engine from URL: " + dataUrl + basename);
Services.search.addEngine(dataUrl + basename, null, null, false);
});
}
// Ensure we have a default search engine and the keyword.enabled preference
// set.
add_task(function* ensure_search_engine() {
// keyword.enabled is necessary for the tests to see keyword searches.
Services.prefs.setBoolPref("keyword.enabled", true);
// Initialize the search service, but first set this geo IP pref to a dummy
// string. When the search service is initialized, it contacts the URI named
// in this pref, which breaks the test since outside connections aren't
// allowed.
let geoPref = "browser.search.geoip.url";
Services.prefs.setCharPref(geoPref, "");
do_register_cleanup(() => Services.prefs.clearUserPref(geoPref));
yield new Promise(resolve => {
Services.search.init(resolve);
});
// Remove any existing engines before adding ours.
for (let engine of Services.search.getEngines()) {
Services.search.removeEngine(engine);
}
Services.search.addEngineWithDetails("MozSearch", "", "", "", "GET",
"http://s.example.com/search");
let engine = Services.search.getEngineByName("MozSearch");
Services.search.currentEngine = engine;
});