diff options
Diffstat (limited to 'toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js')
-rw-r--r-- | toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js | 578 |
1 files changed, 578 insertions, 0 deletions
diff --git a/toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js b/toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js new file mode 100644 index 0000000000..0fb763095a --- /dev/null +++ b/toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js @@ -0,0 +1,578 @@ +/* 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/. */ + +const FRECENCY_DEFAULT = 10000; + +var { ObjectUtils } = ChromeUtils.import( + "resource://gre/modules/ObjectUtils.jsm" +); +var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +var { + HTTP_400, + HTTP_401, + HTTP_402, + HTTP_403, + HTTP_404, + HTTP_405, + HTTP_406, + HTTP_407, + HTTP_408, + HTTP_409, + HTTP_410, + HTTP_411, + HTTP_412, + HTTP_413, + HTTP_414, + HTTP_415, + HTTP_417, + HTTP_500, + HTTP_501, + HTTP_502, + HTTP_503, + HTTP_504, + HTTP_505, + HttpError, + HttpServer, +} = ChromeUtils.import("resource://testing-common/httpd.js"); + +// Import common head. +{ + /* import-globals-from ../head_common.js */ + 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. + +XPCOMUtils.defineLazyModuleGetters(this, { + UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm", + UrlbarProviderOpenTabs: "resource:///modules/UrlbarProviderOpenTabs.jsm", + UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.jsm", + UrlbarUtils: "resource:///modules/UrlbarUtils.jsm", +}); + +const { AddonTestUtils } = ChromeUtils.import( + "resource://testing-common/AddonTestUtils.jsm" +); +AddonTestUtils.init(this, false); +AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "42", + "42" +); + +add_task(async function setup() { + await AddonTestUtils.promiseStartupManager(); +}); + +async function cleanup() { + Services.prefs.clearUserPref("browser.urlbar.autoFill"); + Services.prefs.clearUserPref("browser.urlbar.autoFill.searchEngines"); + let suggestPrefs = ["history", "bookmark", "openpage", "searches"]; + for (let type of suggestPrefs) { + Services.prefs.clearUserPref("browser.urlbar.suggest." + type); + } + Services.prefs.clearUserPref("browser.search.suggest.enabled"); + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); +} +registerCleanupFunction(cleanup); + +/** + * @param {Array} aSearches Array of AutoCompleteSearch names. + */ +function AutoCompleteInput(aSearches) { + this.searches = aSearches; +} +AutoCompleteInput.prototype = { + popup: { + selectedIndex: -1, + invalidate() {}, + QueryInterface: ChromeUtils.generateQI(["nsIAutoCompletePopup"]), + }, + popupOpen: false, + + disableAutoComplete: false, + completeDefaultIndex: true, + completeSelectedIndex: true, + forceComplete: false, + + minResultsForPopup: 0, + maxRows: 0, + + timeout: 10, + searchParam: "", + + get searchCount() { + return this.searches.length; + }, + getSearchAt(aIndex) { + return this.searches[aIndex]; + }, + + textValue: "", + // Text selection range + _selStart: 0, + _selEnd: 0, + get selectionStart() { + return this._selStart; + }, + get selectionEnd() { + return this._selEnd; + }, + selectTextRange(aStart, aEnd) { + this._selStart = aStart; + this._selEnd = aEnd; + }, + + onSearchBegin() {}, + onSearchComplete() {}, + + onTextEntered: () => false, + onTextReverted: () => false, + + QueryInterface: ChromeUtils.generateQI(["nsIAutoCompleteInput"]), +}; + +/** + * A helper for check_autocomplete to check a specific match against data from + * the controller. + * + * @param {Object} match The expected match for the result, in the following form: + * { + * uri: {String|nsIURI} The expected uri. Note: nsIURI should be considered + * deprecated. + * title: {String} The title of the entry. + * tags: {String} The tags for the entry. + * style: {Array} The style of the entry. + * } + * @param {Object} result The result to compare the result against with the same + * properties as the match param. + * @returns {boolean} Returns true if the result matches. + */ +async function _check_autocomplete_matches(match, result) { + let { uri, tags, style } = match; + if (uri instanceof Ci.nsIURI) { + uri = uri.spec; + } + let title = match.comment || match.title; + + if (tags) { + title += UrlbarUtils.TITLE_TAGS_SEPARATOR + tags.sort().join(", "); + } + if (style) { + style = style.sort(); + } else { + style = ["favicon"]; + } + + let actual = { value: result.value, comment: result.comment }; + let expected = { value: match.value || uri, comment: title }; + info( + `Checking match: ` + + `actual=${JSON.stringify(actual)} ... ` + + `expected=${JSON.stringify(expected)}` + ); + + let actualAction = PlacesUtils.parseActionUrl(actual.value); + let expectedAction = PlacesUtils.parseActionUrl(expected.value); + if (actualAction && expectedAction) { + if (!ObjectUtils.deepEqual(actualAction, expectedAction)) { + return false; + } + } else if (actual.value != expected.value) { + return false; + } + + if (actual.comment != expected.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 && uri.startsWith("moz-action:")) { + Assert.ok( + actualStyle.includes("action"), + "moz-action results should always have 'action' in their style" + ); + } + + if (match.icon) { + await compareFavicons( + result.image, + match.icon, + "Match should have the expected icon" + ); + } + + return true; +} + +/** + * Helper function to test an autocomplete entry and check the resultant matches. + * + * @param {Object} test An object representing the test to run, in the following form: + * { + * search: {String} The string to enter for autocompleting. + * searchParam: {String} The search parameters to apply to the + * autocomplete search. + * matches: {Object[]} The expected results in match format. see + * _check_autocomplete_matches. + * } + */ +async 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. + await PlacesTestUtils.promiseAsyncUpdates(); + + // Make an AutoCompleteInput that uses our searches and confirms results. + let input = test.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 = () => { + info("onSearchBegin received"); + numSearchesStarted++; + }; + let searchCompletePromise = new Promise(resolve => { + input.onSearchComplete = () => { + info("onSearchComplete received"); + resolve(); + }; + }); + let expectedSearches = 1; + + info("Searching for: '" + test.search + "'"); + controller.startSearch(test.search); + await 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")) { + let result = { + value: controller.getValueAt(0), + comment: controller.getCommentAt(0), + style: controller.getStyleAt(0), + image: controller.getImageAt(0), + }; + // We only care about the positioning of the first result if it is + // heuristic. + if (result.style.includes("heuristic")) { + info("Checking first match is first autocomplete entry"); + info(`First match is "${result.value}", "${result.comment}"`); + Assert.ok( + await _check_autocomplete_matches(matches[0], result), + "first item is correct" + ); + info("Checking rest of the matches"); + firstIndexToCheck = 1; + } + } + + 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), + }; + info(`Actual result at index ${i}: ${JSON.stringify(result)}`); + 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 (await _check_autocomplete_matches(matches[j], result)) { + info("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. + Assert.equal( + 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"); + } + return input; +} + +async function addOpenPages(aUri, aCount = 1, aUserContextId = 0) { + for (let i = 0; i < aCount; i++) { + await UrlbarProviderOpenTabs.registerOpenTab(aUri.spec, aUserContextId); + } +} + +async function removeOpenPages(aUri, aCount = 1, aUserContextId = 0) { + for (let i = 0; i < aCount; i++) { + await UrlbarProviderOpenTabs.unregisterOpenTab(aUri.spec, aUserContextId); + } +} + +/** + * Strip prefixes from the URI that we don't care about for searching. + * + * @param {String} spec + * The text to modify. + * @return {String} 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 Services.io.newURI(url); +} + +// Creates a full "match" entry for a search result, suitable for passing as +// an entry to check_autocomplete. +function makeSearchMatch(input, extra = {}) { + let params = { + engineName: extra.engineName || "MozSearch", + input, + searchQuery: "searchQuery" in extra ? extra.searchQuery : input, + }; + let style = ["action", "searchengine"]; + if ("style" in extra && Array.isArray(extra.style)) { + style.push(...extra.style); + } + if (extra.heuristic) { + style.push("heuristic"); + } + if ("alias" in extra) { + params.alias = extra.alias; + style.push("alias"); + } + if ("searchSuggestion" in extra) { + params.searchSuggestion = extra.searchSuggestion; + style.push("suggestion"); + } + if ("isSearchHistory" in extra) { + params.isSearchHistory = extra.isSearchHistory; + } + 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"); + } + let displaySpec = Services.textToSubURI.unEscapeURIForUI(url); + return { + uri: makeActionURI("visiturl", params), + title: extra.title || displaySpec, + style, + }; +} + +function makeSwitchToTabMatch(url, extra = {}) { + let displaySpec = Services.textToSubURI.unEscapeURIForUI(url); + return { + uri: makeActionURI("switchtab", { url }), + title: extra.title || displaySpec, + 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 makeTestServer(port = -1) { + let httpServer = new HttpServer(); + httpServer.start(port); + registerCleanupFunction(() => 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/"; + + info("Adding engine: " + basename); + return new Promise(resolve => { + Services.obs.addObserver(function obs(subject, topic, data) { + let engine = subject.QueryInterface(Ci.nsISearchEngine); + info("Observed " + data + " for " + engine.name); + if (data != "engine-added" || engine.name != basename) { + return; + } + + Services.obs.removeObserver(obs, "browser-search-engine-modified"); + registerCleanupFunction(() => Services.search.removeEngine(engine)); + resolve(engine); + }, "browser-search-engine-modified"); + + info("Adding engine from URL: " + dataUrl + basename); + Services.search.addOpenSearchEngine(dataUrl + basename, null); + }); +} + +/** + * Sets up a search engine that provides some suggestions by appending strings + * onto the search query. + * + * @param {function} suggestionsFn + * A function that returns an array of suggestion strings given a + * search string. If not given, a default function is used. + * @returns {nsISearchEngine} The new engine. + */ +async function addTestSuggestionsEngine(suggestionsFn = null) { + // This port number should match the number in engine-suggestions.xml. + let server = makeTestServer(9000); + server.registerPathHandler("/suggest", (req, resp) => { + // URL query params are x-www-form-urlencoded, which converts spaces into + // plus signs, so un-convert any plus signs back to spaces. + let searchStr = decodeURIComponent(req.queryString.replace(/\+/g, " ")); + let suggestions = suggestionsFn + ? suggestionsFn(searchStr) + : [searchStr].concat(["foo", "bar"].map(s => searchStr + " " + s)); + let data = [searchStr, suggestions]; + resp.setHeader("Content-Type", "application/json", false); + resp.write(JSON.stringify(data)); + }); + let engine = await addTestEngine("engine-suggestions.xml", server); + return engine; +} + +// Ensure we have a default search engine and the keyword.enabled preference +// set. +add_task(async function ensure_search_engine() { + // keyword.enabled is necessary for the tests to see keyword searches. + Services.prefs.setBoolPref("keyword.enabled", true); + + // Before initializing the search service, set the geo IP url pref to a dummy + // string. When the search service is initialized, it contacts the URI named + // in this pref, causing unnecessary error logs. + let geoPref = "browser.search.geoip.url"; + Services.prefs.setCharPref(geoPref, ""); + registerCleanupFunction(() => Services.prefs.clearUserPref(geoPref)); + // Remove any existing engines before adding ours. + for (let engine of await Services.search.getEngines()) { + await Services.search.removeEngine(engine); + } + await Services.search.addEngineWithDetails("MozSearch", { + method: "GET", + template: "http://s.example.com/search", + }); + let engine = Services.search.getEngineByName("MozSearch"); + await Services.search.setDefault(engine); +}); |