506 lines
16 KiB
JavaScript
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;
|
|
});
|