diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /browser/components/urlbar/tests | |
parent | Initial commit. (diff) | |
download | firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/components/urlbar/tests')
256 files changed, 51909 insertions, 0 deletions
diff --git a/browser/components/urlbar/tests/UrlbarTestUtils.jsm b/browser/components/urlbar/tests/UrlbarTestUtils.jsm new file mode 100644 index 0000000000..2150b5364a --- /dev/null +++ b/browser/components/urlbar/tests/UrlbarTestUtils.jsm @@ -0,0 +1,915 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const EXPORTED_SYMBOLS = ["UrlbarTestUtils"]; + +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); + +XPCOMUtils.defineLazyModuleGetters(this, { + AddonTestUtils: "resource://testing-common/AddonTestUtils.jsm", + AppConstants: "resource://gre/modules/AppConstants.jsm", + BrowserTestUtils: "resource://testing-common/BrowserTestUtils.jsm", + BrowserUtils: "resource://gre/modules/BrowserUtils.jsm", + BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm", + FormHistoryTestUtils: "resource://testing-common/FormHistoryTestUtils.jsm", + PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm", + Services: "resource://gre/modules/Services.jsm", + setTimeout: "resource://gre/modules/Timer.jsm", + TestUtils: "resource://testing-common/TestUtils.jsm", + UrlbarController: "resource:///modules/UrlbarController.jsm", + UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm", + UrlbarProvider: "resource:///modules/UrlbarUtils.jsm", + UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.jsm", + UrlbarUtils: "resource:///modules/UrlbarUtils.jsm", +}); + +var UrlbarTestUtils = { + /** + * This maps the categories used by the FX_URLBAR_SELECTED_RESULT_METHOD and + * FX_SEARCHBAR_SELECTED_RESULT_METHOD histograms to their indexes in the + * `labels` array. This only needs to be used by tests that need to map from + * category names to indexes in histogram snapshots. Actual app code can use + * these category names directly when they add to a histogram. + */ + SELECTED_RESULT_METHODS: { + enter: 0, + enterSelection: 1, + click: 2, + arrowEnterSelection: 3, + tabEnterSelection: 4, + rightClickEnter: 5, + }, + + /** + * Running this init allows helpers to access test scope helpers, like Assert + * and SimpleTest. Note this initialization is not enforced, thus helpers + * should always check _testScope and provide a fallback path. + * @param {object} scope The global scope where tests are being run. + */ + init(scope) { + this._testScope = scope; + if (scope) { + this.Assert = scope.Assert; + this.EventUtils = scope.EventUtils; + } + }, + + /** + * If tests initialize UrlbarTestUtils, they may need to call this function in + * their cleanup callback, or else their scope will affect subsequent tests. + * This is usually only required for tests outside browser/components/urlbar. + */ + uninit() { + this._testScope = null; + }, + + /** + * Waits to a search to be complete. + * @param {object} win The window containing the urlbar + * @returns {Promise} Resolved when done. + */ + async promiseSearchComplete(win) { + let waitForQuery = () => { + return this.promisePopupOpen(win, () => {}).then( + () => win.gURLBar.lastQueryContextPromise + ); + }; + let context = await waitForQuery(); + if (win.gURLBar.searchMode) { + // Search mode may start a second query. + context = await waitForQuery(); + } + return context; + }, + + /** + * Starts a search for a given string and waits for the search to be complete. + * @param {object} options.window The window containing the urlbar + * @param {string} options.value the search string + * @param {function} options.waitForFocus The SimpleTest function + * @param {boolean} [options.fireInputEvent] whether an input event should be + * used when starting the query (simulates the user's typing, sets + * userTypedValued, triggers engagement event telemetry, etc.) + * @param {number} [options.selectionStart] The input's selectionStart + * @param {number} [options.selectionEnd] The input's selectionEnd + */ + async promiseAutocompleteResultPopup({ + window, + value, + waitForFocus, + fireInputEvent = false, + selectionStart = -1, + selectionEnd = -1, + } = {}) { + if (this._testScope) { + await this._testScope.SimpleTest.promiseFocus(window); + } else { + await new Promise(resolve => waitForFocus(resolve, window)); + } + window.gURLBar.inputField.focus(); + // Using the value setter in some cases may trim and fetch unexpected + // results, then pick an alternate path. + if (UrlbarPrefs.get("trimURLs") && value != BrowserUtils.trimURL(value)) { + window.gURLBar.inputField.value = value; + fireInputEvent = true; + } else { + window.gURLBar.value = value; + } + if (selectionStart >= 0 && selectionEnd >= 0) { + window.gURLBar.selectionEnd = selectionEnd; + window.gURLBar.selectionStart = selectionStart; + } + + // An input event will start a new search, so be careful not to start a + // search if we fired an input event since that would start two searches. + if (fireInputEvent) { + // This is necessary to get the urlbar to set gBrowser.userTypedValue. + this.fireInputEvent(window); + } else { + window.gURLBar.setPageProxyState("invalid"); + window.gURLBar.startQuery(); + } + + return this.promiseSearchComplete(window); + }, + + /** + * Waits for a result to be added at a certain index. Since we implement lazy + * results replacement, even if we have a result at an index, it may be + * related to the previous query, this methods ensures the result is current. + * @param {object} win The window containing the urlbar + * @param {number} index The index to look for + * @returns {HtmlElement|XulElement} the result's element. + */ + async waitForAutocompleteResultAt(win, index) { + // TODO Bug 1530338: Quantum Bar doesn't yet implement lazy results replacement. + await this.promiseSearchComplete(win); + if (index >= win.gURLBar.view._rows.children.length) { + throw new Error("Not enough results"); + } + return win.gURLBar.view._rows.children[index]; + }, + + /** + * Returns the oneOffSearchButtons object for the urlbar. + * @param {object} win The window containing the urlbar + * @returns {object} The oneOffSearchButtons + */ + getOneOffSearchButtons(win) { + return win.gURLBar.view.oneOffSearchButtons; + }, + + /** + * Returns true if the oneOffSearchButtons are visible. + * @param {object} win The window containing the urlbar + * @returns {boolean} True if the buttons are visible. + */ + getOneOffSearchButtonsVisible(win) { + let buttons = this.getOneOffSearchButtons(win); + return buttons.style.display != "none" && !buttons.container.hidden; + }, + + /** + * Gets an abstracted representation of the result at an index. + * @param {object} win The window containing the urlbar + * @param {number} index The index to look for + * @returns {object} An object with numerous properties describing the result. + */ + async getDetailsOfResultAt(win, index) { + let element = await this.waitForAutocompleteResultAt(win, index); + let details = {}; + let result = element.result; + let { url, postData } = UrlbarUtils.getUrlFromResult(result); + details.url = url; + details.postData = postData; + details.type = result.type; + details.source = result.source; + details.heuristic = result.heuristic; + details.autofill = !!result.autofill; + details.image = element.getElementsByClassName("urlbarView-favicon")[0].src; + details.title = result.title; + details.tags = "tags" in result.payload ? result.payload.tags : []; + let actions = element.getElementsByClassName("urlbarView-action"); + let urls = element.getElementsByClassName("urlbarView-url"); + let typeIcon = element.querySelector(".urlbarView-type-icon"); + await win.document.l10n.translateFragment(element); + details.displayed = { + title: element.getElementsByClassName("urlbarView-title")[0].textContent, + action: actions.length ? actions[0].textContent : null, + url: urls.length ? urls[0].textContent : null, + typeIcon: typeIcon + ? win.getComputedStyle(typeIcon)["background-image"] + : null, + }; + details.element = { + action: element.getElementsByClassName("urlbarView-action")[0], + row: element, + separator: element.getElementsByClassName( + "urlbarView-title-separator" + )[0], + title: element.getElementsByClassName("urlbarView-title")[0], + url: element.getElementsByClassName("urlbarView-url")[0], + }; + if (details.type == UrlbarUtils.RESULT_TYPE.SEARCH) { + details.searchParams = { + engine: result.payload.engine, + keyword: result.payload.keyword, + query: result.payload.query, + suggestion: result.payload.suggestion, + inPrivateWindow: result.payload.inPrivateWindow, + isPrivateEngine: result.payload.isPrivateEngine, + }; + } else if (details.type == UrlbarUtils.RESULT_TYPE.KEYWORD) { + details.keyword = result.payload.keyword; + } + return details; + }, + + /** + * Gets the currently selected element. + * @param {object} win The window containing the urlbar. + * @returns {HtmlElement|XulElement} The selected element. + */ + getSelectedElement(win) { + return win.gURLBar.view.selectedElement || null; + }, + + /** + * Gets the index of the currently selected element. + * @param {object} win The window containing the urlbar. + * @returns {number} The selected index. + */ + getSelectedElementIndex(win) { + return win.gURLBar.view.selectedElementIndex; + }, + + /** + * Gets the currently selected row. If the selected element is a descendant of + * a row, this will return the ancestor row. + * @param {object} win The window containing the urlbar. + * @returns {HTMLElement|XulElement} The selected row. + */ + getSelectedRow(win) { + return win.gURLBar.view._getSelectedRow() || null; + }, + + /** + * Gets the index of the currently selected element. + * @param {object} win The window containing the urlbar. + * @returns {number} The selected row index. + */ + getSelectedRowIndex(win) { + return win.gURLBar.view.selectedRowIndex; + }, + + /** + * Selects the element at the index specified. + * @param {object} win The window containing the urlbar. + * @param {index} index The index to select. + */ + setSelectedRowIndex(win, index) { + win.gURLBar.view.selectedRowIndex = index; + }, + + /** + * Gets the number of results. + * You must wait for the query to be complete before using this. + * @param {object} win The window containing the urlbar + * @returns {number} the number of results. + */ + getResultCount(win) { + return win.gURLBar.view._rows.children.length; + }, + + /** + * Ensures at least one search suggestion is present. + * @param {object} win The window containing the urlbar + * @returns {boolean} whether at least one search suggestion is present. + */ + promiseSuggestionsPresent(win) { + // TODO Bug 1530338: Quantum Bar doesn't yet implement lazy results replacement. When + // we do that, we'll have to be sure the suggestions we find are relevant + // for the current query. For now let's just wait for the search to be + // complete. + return this.promiseSearchComplete(win).then(context => { + // Look for search suggestions. + let firstSearchSuggestionIndex = context.results.findIndex( + r => r.type == UrlbarUtils.RESULT_TYPE.SEARCH && r.payload.suggestion + ); + if (firstSearchSuggestionIndex == -1) { + throw new Error("Cannot find a search suggestion"); + } + return firstSearchSuggestionIndex; + }); + }, + + /** + * Waits for the given number of connections to an http server. + * @param {object} httpserver an HTTP Server instance + * @param {number} count Number of connections to wait for + * @returns {Promise} resolved when all the expected connections were started. + */ + promiseSpeculativeConnections(httpserver, count) { + if (!httpserver) { + throw new Error("Must provide an http server"); + } + return BrowserTestUtils.waitForCondition( + () => httpserver.connectionNumber == count, + "Waiting for speculative connection setup" + ); + }, + + /** + * Waits for the popup to be shown. + * @param {object} win The window containing the urlbar + * @param {function} openFn Function to be used to open the popup. + * @returns {Promise} resolved once the popup is closed + */ + async promisePopupOpen(win, openFn) { + if (!openFn) { + throw new Error("openFn should be supplied to promisePopupOpen"); + } + await openFn(); + if (win.gURLBar.view.isOpen) { + return; + } + if (this._testScope) { + this._testScope.info("Awaiting for the urlbar panel to open"); + } + await new Promise(resolve => { + win.gURLBar.controller.addQueryListener({ + onViewOpen() { + win.gURLBar.controller.removeQueryListener(this); + resolve(); + }, + }); + }); + }, + + /** + * Waits for the popup to be hidden. + * @param {object} win The window containing the urlbar + * @param {function} [closeFn] Function to be used to close the popup, if not + * supplied it will default to a closing the popup directly. + * @returns {Promise} resolved once the popup is closed + */ + async promisePopupClose(win, closeFn = null) { + if (closeFn) { + await closeFn(); + } else { + win.gURLBar.view.close(); + } + if (!win.gURLBar.view.isOpen) { + return; + } + if (this._testScope) { + this._testScope.info("Awaiting for the urlbar panel to close"); + } + await new Promise(resolve => { + win.gURLBar.controller.addQueryListener({ + onViewClose() { + win.gURLBar.controller.removeQueryListener(this); + resolve(); + }, + }); + }); + }, + + /** + * @param {object} win The browser window + * @returns {boolean} Whether the popup is open + */ + isPopupOpen(win) { + return win.gURLBar.view.isOpen; + }, + + /** + * Asserts that the input is in a given search mode, or no search mode. + * + * @param {Window} window + * The browser window. + * @param {object} expectedSearchMode + * The expected search mode object. + * @note Can only be used if UrlbarTestUtils has been initialized with init(). + */ + async assertSearchMode(window, expectedSearchMode) { + this.Assert.equal( + !!window.gURLBar.searchMode, + window.gURLBar.hasAttribute("searchmode"), + "Urlbar should never be in search mode without the corresponding attribute." + ); + + this.Assert.equal( + !!window.gURLBar.searchMode, + !!expectedSearchMode, + "gURLBar.searchMode should exist as expected" + ); + + if (!expectedSearchMode) { + // Check the input's placeholder. + const prefName = + "browser.urlbar.placeholderName" + + (PrivateBrowsingUtils.isWindowPrivate(window) ? ".private" : ""); + let engineName = Services.prefs.getStringPref(prefName, ""); + this.Assert.deepEqual( + window.document.l10n.getAttributes(window.gURLBar.inputField), + engineName + ? { id: "urlbar-placeholder-with-name", args: { name: engineName } } + : { id: "urlbar-placeholder", args: null }, + "Expected placeholder l10n when search mode is inactive" + ); + return; + } + + // Default to full search mode for less verbose tests. + expectedSearchMode = { ...expectedSearchMode }; + if (!expectedSearchMode.hasOwnProperty("isPreview")) { + expectedSearchMode.isPreview = false; + } + + // expectedSearchMode may come from UrlbarUtils.LOCAL_SEARCH_MODES. The + // objects in that array include useful metadata like icon URIs and pref + // names that are not usually included in actual search mode objects. For + // convenience, ignore those properties if they aren't also present in the + // urlbar's actual search mode object. + let ignoreProperties = ["icon", "pref", "restrict"]; + for (let prop of ignoreProperties) { + if (prop in expectedSearchMode && !(prop in window.gURLBar.searchMode)) { + if (this._testScope) { + this._testScope.info( + `Ignoring unimportant property '${prop}' in expected search mode` + ); + } + delete expectedSearchMode[prop]; + } + } + + this.Assert.deepEqual( + window.gURLBar.searchMode, + expectedSearchMode, + "Expected searchMode" + ); + + // Check the textContent and l10n attributes of the indicator and label. + let expectedTextContent = ""; + let expectedL10n = { id: null, args: null }; + if (expectedSearchMode.engineName) { + expectedTextContent = expectedSearchMode.engineName; + } else if (expectedSearchMode.source) { + let name = UrlbarUtils.getResultSourceName(expectedSearchMode.source); + this.Assert.ok(name, "Expected result source should have a name"); + expectedL10n = { id: `urlbar-search-mode-${name}`, args: null }; + } else { + this.Assert.ok(false, "Unexpected searchMode"); + } + + for (let element of [ + window.gURLBar._searchModeIndicatorTitle, + window.gURLBar._searchModeLabel, + ]) { + if (expectedTextContent) { + this.Assert.equal( + element.textContent, + expectedTextContent, + "Expected textContent" + ); + } + this.Assert.deepEqual( + window.document.l10n.getAttributes(element), + expectedL10n, + "Expected l10n" + ); + } + + // Check the input's placeholder. + let expectedPlaceholderL10n; + if (expectedSearchMode.engineName) { + expectedPlaceholderL10n = { + id: UrlbarUtils.WEB_ENGINE_NAMES.has(expectedSearchMode.engineName) + ? "urlbar-placeholder-search-mode-web-2" + : "urlbar-placeholder-search-mode-other-engine", + args: { name: expectedSearchMode.engineName }, + }; + } else if (expectedSearchMode.source) { + let name = UrlbarUtils.getResultSourceName(expectedSearchMode.source); + expectedPlaceholderL10n = { + id: `urlbar-placeholder-search-mode-other-${name}`, + args: null, + }; + } + this.Assert.deepEqual( + window.document.l10n.getAttributes(window.gURLBar.inputField), + expectedPlaceholderL10n, + "Expected placeholder l10n when search mode is active" + ); + + // If this is an engine search mode, check that all results are either + // search results with the same engine or have the same host as the engine. + // Search mode preview can show other results since it is not supposed to + // start a query. + if ( + expectedSearchMode.engineName && + !expectedSearchMode.isPreview && + this.isPopupOpen(window) + ) { + let resultCount = this.getResultCount(window); + for (let i = 0; i < resultCount; i++) { + let result = await this.getDetailsOfResultAt(window, i); + if (result.source == UrlbarUtils.RESULT_SOURCE.SEARCH) { + this.Assert.equal( + expectedSearchMode.engineName, + result.searchParams.engine, + "Search mode result matches engine name." + ); + } else { + let engine = Services.search.getEngineByName( + expectedSearchMode.engineName + ); + let engineRootDomain = UrlbarSearchUtils.getRootDomainFromEngine( + engine + ); + let resultUrl = new URL(result.url); + this.Assert.ok( + resultUrl.hostname.includes(engineRootDomain), + "Search mode result matches engine host." + ); + } + } + } + }, + + /** + * Enters search mode by clicking a one-off. The view must already be open + * before you call this. + * @param {object} window + * @param {object} searchMode + * If given, the one-off matching this search mode will be clicked; it + * should be a full search mode object as described in + * UrlbarInput.setSearchMode. If not given, the first one-off is clicked. + * @note Can only be used if UrlbarTestUtils has been initialized with init(). + */ + async enterSearchMode(window, searchMode = null) { + // Ensure any pending query is complete. + await this.promiseSearchComplete(window); + + // Ensure the the one-offs are finished rebuilding and visible. + let oneOffs = this.getOneOffSearchButtons(window); + await TestUtils.waitForCondition( + () => !oneOffs._rebuilding, + "Waiting for one-offs to finish rebuilding" + ); + this.Assert.equal( + UrlbarTestUtils.getOneOffSearchButtonsVisible(window), + true, + "One-offs are visible" + ); + + let buttons = oneOffs.getSelectableButtons(true); + if (!searchMode) { + searchMode = { engineName: buttons[0].engine.name }; + if (UrlbarUtils.WEB_ENGINE_NAMES.has(searchMode.engineName)) { + searchMode.source = UrlbarUtils.RESULT_SOURCE.SEARCH; + } + } + + if (!searchMode.entry) { + searchMode.entry = "oneoff"; + } + + let oneOff = buttons.find(o => + searchMode.engineName + ? o.engine.name == searchMode.engineName + : o.source == searchMode.source + ); + this.Assert.ok(oneOff, "Found one-off button for search mode"); + this.EventUtils.synthesizeMouseAtCenter(oneOff, {}, window); + await this.promiseSearchComplete(window); + this.Assert.ok(this.isPopupOpen(window), "Urlbar view is still open."); + await this.assertSearchMode(window, searchMode); + }, + + /** + * Exits search mode. + * @param {object} window + * @param {boolean} options.backspace + * Exits search mode by backspacing at the beginning of the search string. + * @param {boolean} options.clickClose + * Exits search mode by clicking the close button on the search mode + * indicator. + * @param {boolean} [waitForSearch] + * Whether the test should wait for a search after exiting search mode. + * Defaults to true. + * @note If neither `backspace` nor `clickClose` is given, we'll default to + * backspacing. + * @note Can only be used if UrlbarTestUtils has been initialized with init(). + */ + async exitSearchMode( + window, + { backspace, clickClose, waitForSearch = true } = {} + ) { + let urlbar = window.gURLBar; + // If the Urlbar is not extended, ignore the clickClose parameter. The close + // button is not clickable in this state. This state might be encountered on + // Linux, where prefers-reduced-motion is enabled in automation. + if (!urlbar.hasAttribute("breakout-extend") && clickClose) { + if (waitForSearch) { + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + urlbar.searchMode = null; + await searchPromise; + } else { + urlbar.searchMode = null; + } + return; + } + + if (!backspace && !clickClose) { + backspace = true; + } + + if (backspace) { + let urlbarValue = urlbar.value; + urlbar.selectionStart = urlbar.selectionEnd = 0; + if (waitForSearch) { + let searchPromise = this.promiseSearchComplete(window); + this.EventUtils.synthesizeKey("KEY_Backspace", {}, window); + await searchPromise; + } else { + this.EventUtils.synthesizeKey("KEY_Backspace", {}, window); + } + this.Assert.equal( + urlbar.value, + urlbarValue, + "Urlbar value hasn't changed." + ); + this.assertSearchMode(window, null); + } else if (clickClose) { + // We need to hover the indicator to make the close button clickable in the + // test. + let indicator = urlbar.querySelector("#urlbar-search-mode-indicator"); + this.EventUtils.synthesizeMouseAtCenter( + indicator, + { type: "mouseover" }, + window + ); + let closeButton = urlbar.querySelector( + "#urlbar-search-mode-indicator-close" + ); + if (waitForSearch) { + let searchPromise = this.promiseSearchComplete(window); + this.EventUtils.synthesizeMouseAtCenter(closeButton, {}, window); + await searchPromise; + } else { + this.EventUtils.synthesizeMouseAtCenter(closeButton, {}, window); + } + await this.assertSearchMode(window, null); + } + }, + + /** + * Returns the userContextId (container id) for the last search. + * @param {object} win The browser window + * @returns {Promise} resolved when fetching is complete + * @resolves {number} a userContextId + */ + async promiseUserContextId(win) { + const defaultId = Ci.nsIScriptSecurityManager.DEFAULT_USER_CONTEXT_ID; + let context = await win.gURLBar.lastQueryContextPromise; + return context.userContextId || defaultId; + }, + + /** + * Dispatches an input event to the input field. + * @param {object} win The browser window + */ + fireInputEvent(win) { + // Set event.data to the last character in the input, for a couple of + // reasons: It simulates the user typing, and it's necessary for autofill. + let event = new InputEvent("input", { + data: win.gURLBar.value[win.gURLBar.value.length - 1] || null, + }); + win.gURLBar.inputField.dispatchEvent(event); + }, + + /** + * Returns a new mock controller. This is useful for xpcshell tests. + * @param {object} options Additional options to pass to the UrlbarController + * constructor. + * @returns {UrlbarController} A new controller. + */ + newMockController(options = {}) { + return new UrlbarController( + Object.assign( + { + input: { + isPrivate: false, + onFirstResult() { + return false; + }, + window: { + location: { + href: AppConstants.BROWSER_CHROME_URL, + }, + }, + }, + }, + options + ) + ); + }, + + /** + * Initializes some external components used by the urlbar. This is necessary + * in xpcshell tests but not in browser tests. + */ + async initXPCShellDependencies() { + // The FormHistoryStartup component must be initialized since urlbar uses + // form history. + Cc["@mozilla.org/satchel/form-history-startup;1"] + .getService(Ci.nsIObserver) + .observe(null, "profile-after-change", null); + + // This is necessary because UrlbarMuxerUnifiedComplete.sort calls + // Services.search.parseSubmissionURL, so we need engines. + try { + await AddonTestUtils.promiseStartupManager(); + } catch (error) { + if (!error.message.includes("already started")) { + throw error; + } + } + }, +}; + +UrlbarTestUtils.formHistory = { + /** + * Adds values to the urlbar's form history. + * + * @param {array} values + * The form history entries to remove. + * @param {object} window + * The window containing the urlbar. + * @returns {Promise} resolved once the operation is complete. + */ + add(values = [], window = BrowserWindowTracker.getTopWindow()) { + let fieldname = this.getFormHistoryName(window); + return FormHistoryTestUtils.add(fieldname, values); + }, + + /** + * Removes values from the urlbar's form history. If you want to remove all + * history, use clearFormHistory. + * + * @param {array} values + * The form history entries to remove. + * @param {object} window + * The window containing the urlbar. + * @returns {Promise} resolved once the operation is complete. + */ + remove(values = [], window = BrowserWindowTracker.getTopWindow()) { + let fieldname = this.getFormHistoryName(window); + return FormHistoryTestUtils.remove(fieldname, values); + }, + + /** + * Removes all values from the urlbar's form history. If you want to remove + * individual values, use removeFormHistory. + * + * @param {object} window + * The window containing the urlbar. + * @returns {Promise} resolved once the operation is complete. + */ + clear(window = BrowserWindowTracker.getTopWindow()) { + let fieldname = this.getFormHistoryName(window); + return FormHistoryTestUtils.clear(fieldname); + }, + + /** + * Searches the urlbar's form history. + * + * @param {object} criteria + * Criteria to narrow the search. See FormHistory.search. + * @param {object} window + * The window containing the urlbar. + * @returns {Promise} + * A promise resolved with an array of found form history entries. + */ + search(criteria = {}, window = BrowserWindowTracker.getTopWindow()) { + let fieldname = this.getFormHistoryName(window); + return FormHistoryTestUtils.search(fieldname, criteria); + }, + + /** + * Returns a promise that's resolved on the next form history change. + * + * @param {string} change + * Null to listen for any change, or one of: add, remove, update + * @returns {Promise} + * Resolved on the next specified form history change. + */ + promiseChanged(change = null) { + return TestUtils.topicObserved( + "satchel-storage-changed", + (subject, data) => !change || data == "formhistory-" + change + ); + }, + + /** + * Returns the form history name for the urlbar in a window. + * + * @param {object} window + * The window. + * @returns {string} + * The form history name of the urlbar in the window. + */ + getFormHistoryName(window = BrowserWindowTracker.getTopWindow()) { + return window ? window.gURLBar.formHistoryName : "searchbar-history"; + }, +}; + +/** + * A test provider. If you need a test provider whose behavior is different + * from this, then consider modifying the implementation below if you think the + * new behavior would be useful for other tests. Otherwise, you can create a + * new TestProvider instance and then override its methods. + */ +class TestProvider extends UrlbarProvider { + /** + * Constructor. + * + * @param {array} results + * An array of UrlbarResult objects that will be the provider's results. + * @param {string} [name] + * The provider's name. Provider names should be unique. + * @param {UrlbarUtils.PROVIDER_TYPE} [type] + * The provider's type. + * @param {number} [priority] + * The provider's priority. Built-in providers have a priority of zero. + * @param {number} [addTimeout] + * If non-zero, each result will be added on this timeout. If zero, all + * results will be added immediately and synchronously. + * @param {function} [onCancel] + * If given, a function that will be called when the provider's cancelQuery + * method is called. + */ + constructor({ + results, + name = Math.floor(Math.random() * 100000), + type = UrlbarUtils.PROVIDER_TYPE.PROFILE, + priority = 0, + addTimeout = 0, + onCancel = null, + onSelection = null, + } = {}) { + super(); + this._results = results; + this._name = name; + this._type = type; + this._priority = priority; + this._addTimeout = addTimeout; + this._onCancel = onCancel; + this._onSelection = onSelection; + } + get name() { + return "TestProvider" + this._name; + } + get type() { + return this._type; + } + getPriority(context) { + return this._priority; + } + isActive(context) { + return true; + } + async startQuery(context, addCallback) { + for (let result of this._results) { + if (!this._addTimeout) { + addCallback(this, result); + } else { + await new Promise(resolve => { + setTimeout(() => { + addCallback(this, result); + resolve(); + }, this._addTimeout); + }); + } + } + } + cancelQuery(context) { + if (this._onCancel) { + this._onCancel(); + } + } + + onSelection(result, element) { + if (this._onSelection) { + this._onSelection(result, element); + } + } +} + +UrlbarTestUtils.TestProvider = TestProvider; diff --git a/browser/components/urlbar/tests/browser-tips/README.txt b/browser/components/urlbar/tests/browser-tips/README.txt new file mode 100644 index 0000000000..04a7b09707 --- /dev/null +++ b/browser/components/urlbar/tests/browser-tips/README.txt @@ -0,0 +1,7 @@ +If you're running these tests and you get an error like this: + +FAIL head.js import threw an exception - Error opening input stream (invalid filename?): chrome://mochitests/content/browser/toolkit/mozapps/update/tests/browser/head.js + +Then run `mach test toolkit/mozapps/update/tests/browser` first. You can +stop mach as soon as it starts the first test, but this is necessary so that +mach builds the update tests in your objdir. diff --git a/browser/components/urlbar/tests/browser-tips/browser.ini b/browser/components/urlbar/tests/browser-tips/browser.ini new file mode 100644 index 0000000000..0c4bb95cc5 --- /dev/null +++ b/browser/components/urlbar/tests/browser-tips/browser.ini @@ -0,0 +1,17 @@ +# 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/. + +[DEFAULT] +support-files = + head.js + +[browser_interventions.js] +[browser_picks.js] +[browser_searchTips_interaction.js] +[browser_searchTips.js] +[browser_selection.js] +[browser_updateAsk.js] +[browser_updateRefresh.js] +[browser_updateRestart.js] +[browser_updateWeb.js] diff --git a/browser/components/urlbar/tests/browser-tips/browser_interventions.js b/browser/components/urlbar/tests/browser-tips/browser_interventions.js new file mode 100644 index 0000000000..87ee03e75d --- /dev/null +++ b/browser/components/urlbar/tests/browser-tips/browser_interventions.js @@ -0,0 +1,209 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +XPCOMUtils.defineLazyModuleGetters(this, { + UrlbarProviderInterventions: + "resource:///modules/UrlbarProviderInterventions.jsm", +}); + +add_task(async function init() { + makeProfileResettable(); +}); + +// Tests the refresh tip. +add_task(async function refresh() { + // Pick the tip, which should open the refresh dialog. Click its cancel + // button. + await checkIntervention({ + searchString: SEARCH_STRINGS.REFRESH, + tip: UrlbarProviderInterventions.TIP_TYPE.REFRESH, + title: + "Restore default settings and remove old add-ons for optimal performance.", + button: /^Refresh .+…$/, + awaitCallback() { + return promiseAlertDialog("cancel", [ + "chrome://global/content/resetProfile.xhtml", + "chrome://global/content/resetProfile.xul", + ]); + }, + }); +}); + +// Tests the clear tip. +add_task(async function clear() { + // Pick the tip, which should open the refresh dialog. Click its cancel + // button. + await checkIntervention({ + searchString: SEARCH_STRINGS.CLEAR, + tip: UrlbarProviderInterventions.TIP_TYPE.CLEAR, + title: "Clear your cache, cookies, history and more.", + button: "Choose What to Clear…", + awaitCallback() { + return promiseAlertDialog("cancel", [ + "chrome://browser/content/sanitize.xhtml", + "chrome://browser/content/sanitize.xul", + ]); + }, + }); +}); + +// Tests the clear tip in a private window. The clear tip shouldn't appear in +// private windows. +add_task(async function clear_private() { + let win = await BrowserTestUtils.openNewBrowserWindow({ private: true }); + // First, make sure the extension works in PBM by triggering a non-clear + // tip. + let result = (await awaitTip(SEARCH_STRINGS.REFRESH, win))[0]; + Assert.strictEqual( + result.payload.type, + UrlbarProviderInterventions.TIP_TYPE.REFRESH + ); + + // Blur the urlbar so that the engagement is ended. + await UrlbarTestUtils.promisePopupClose(win, () => win.gURLBar.blur()); + + // Now do a search that would trigger the clear tip. + await awaitNoTip(SEARCH_STRINGS.CLEAR, win); + + // Blur the urlbar so that the engagement is ended. + await UrlbarTestUtils.promisePopupClose(win, () => win.gURLBar.blur()); + + await BrowserTestUtils.closeWindow(win); +}); + +// Tests that if multiple interventions of the same type are seen in the same +// engagement, only one instance is recorded in Telemetry. +add_task(async function multipleInterventionsInOneEngagement() { + Services.telemetry.clearScalars(); + let result = (await awaitTip(SEARCH_STRINGS.REFRESH, window))[0]; + Assert.strictEqual( + result.payload.type, + UrlbarProviderInterventions.TIP_TYPE.REFRESH + ); + result = (await awaitTip(SEARCH_STRINGS.CLEAR, window))[0]; + Assert.strictEqual( + result.payload.type, + UrlbarProviderInterventions.TIP_TYPE.CLEAR + ); + result = (await awaitTip(SEARCH_STRINGS.REFRESH, window))[0]; + Assert.strictEqual( + result.payload.type, + UrlbarProviderInterventions.TIP_TYPE.REFRESH + ); + + // Blur the urlbar so that the engagement is ended. + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); + + const scalars = TelemetryTestUtils.getProcessScalars("parent", true, true); + // We should only record one impression for the Refresh tip. Although it was + // seen twice, it was in the same engagement. + TelemetryTestUtils.assertKeyedScalar( + scalars, + "urlbar.tips", + `${UrlbarProviderInterventions.TIP_TYPE.REFRESH}-shown`, + 1 + ); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "urlbar.tips", + `${UrlbarProviderInterventions.TIP_TYPE.CLEAR}-shown`, + 1 + ); +}); + +add_task(async function tipsAreEnglishOnly() { + // Test that Interventions are working in en-US. + let result = (await awaitTip(SEARCH_STRINGS.REFRESH, window))[0]; + Assert.strictEqual( + result.payload.type, + UrlbarProviderInterventions.TIP_TYPE.REFRESH + ); + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); + + // We will need to fetch new engines when we switch locales. + let enginesReloaded = SearchTestUtils.promiseSearchNotification( + "engines-reloaded" + ); + + const originalAvailable = Services.locale.availableLocales; + const originalRequested = Services.locale.requestedLocales; + Services.locale.availableLocales = ["en-US", "de"]; + Services.locale.requestedLocales = ["de"]; + + registerCleanupFunction(async () => { + let enginesReloaded2 = SearchTestUtils.promiseSearchNotification( + "engines-reloaded" + ); + Services.locale.requestedLocales = originalRequested; + Services.locale.availableLocales = originalAvailable; + await enginesReloaded2; + }); + + let appLocales = Services.locale.appLocalesAsBCP47; + Assert.equal(appLocales[0], "de"); + + await enginesReloaded; + + // Interventions should no longer work in the new locale. + await awaitNoTip(SEARCH_STRINGS.CLEAR, window); + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); +}); + +/** + * Picks the help button from an Intervention. We spoof the Intervention in this + * test because our withDNSRedirect helper cannot handle the HTTPS SUMO links. + */ +add_task(async function pickHelpButton() { + const helpUrl = "http://example.com/"; + let results = [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://mozilla.org/a" } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TIP, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + type: UrlbarProviderInterventions.TIP_TYPE.CLEAR, + text: "This is a test tip.", + buttonText: "Done", + helpUrl, + } + ), + ]; + let interventionProvider = new UrlbarTestUtils.TestProvider({ + results, + priority: 2, + }); + UrlbarProvidersManager.registerProvider(interventionProvider); + + registerCleanupFunction(() => { + UrlbarProvidersManager.unregisterProvider(interventionProvider); + }); + + await BrowserTestUtils.withNewTab("about:blank", async () => { + let [result, element] = await awaitTip(SEARCH_STRINGS.CLEAR); + Assert.strictEqual( + result.payload.type, + UrlbarProviderInterventions.TIP_TYPE.CLEAR + ); + + let helpButton = element._elements.get("helpButton"); + Assert.ok(BrowserTestUtils.is_visible(helpButton)); + EventUtils.synthesizeMouseAtCenter(helpButton, {}); + + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, helpUrl); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + + const scalars = TelemetryTestUtils.getProcessScalars("parent", true, true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "urlbar.tips", + `${UrlbarProviderInterventions.TIP_TYPE.CLEAR}-help`, + 1 + ); + }); +}); diff --git a/browser/components/urlbar/tests/browser-tips/browser_picks.js b/browser/components/urlbar/tests/browser-tips/browser_picks.js new file mode 100644 index 0000000000..9aacddfad0 --- /dev/null +++ b/browser/components/urlbar/tests/browser-tips/browser_picks.js @@ -0,0 +1,213 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests clicks and enter key presses on UrlbarUtils.RESULT_TYPE.TIP results. + +"use strict"; + +const TIP_URL = "http://example.com/tip"; +const HELP_URL = "http://example.com/help"; + +add_task(async function init() { + window.windowUtils.disableNonTestMouseEvents(true); + registerCleanupFunction(() => { + window.windowUtils.disableNonTestMouseEvents(false); + }); + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.eventTelemetry.enabled", true]], + }); +}); + +add_task(async function enter_mainButton_url() { + await doTest({ click: false, buttonUrl: TIP_URL }); +}); + +add_task(async function enter_mainButton_noURL() { + await doTest({ click: false }); +}); + +add_task(async function enter_helpButton() { + await doTest({ click: false, helpUrl: HELP_URL }); +}); + +add_task(async function mouse_mainButton_url() { + await doTest({ click: true, buttonUrl: TIP_URL }); +}); + +add_task(async function mouse_mainButton_noURL() { + await doTest({ click: true }); +}); + +add_task(async function mouse_helpButton() { + await doTest({ click: true, helpUrl: HELP_URL }); +}); + +// Clicks inside a tip but not on any button. +add_task(async function mouse_insideTipButNotOnButtons() { + let results = [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TIP, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + type: "test", + text: "This is a test tip.", + buttonText: "Done", + helpUrl: HELP_URL, + buttonUrl: TIP_URL, + } + ), + ]; + let provider = new UrlbarTestUtils.TestProvider({ results, priority: 1 }); + UrlbarProvidersManager.registerProvider(provider); + + // Click inside the tip but outside the buttons. Nothing should happen. Make + // the result the heuristic to check that the selection on the main button + // isn't lost. + results[0].heuristic = true; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + value: "test", + window, + fireInputEvent: true, + }); + let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0); + Assert.equal( + UrlbarTestUtils.getSelectedElementIndex(window), + 0, + "The main button's index should be selected initially" + ); + Assert.equal( + UrlbarTestUtils.getSelectedElement(window), + row._elements.get("tipButton"), + "The main button element should be selected initially" + ); + EventUtils.synthesizeMouseAtCenter(row, {}); + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(r => setTimeout(r, 500)); + Assert.ok(gURLBar.view.isOpen, "The view should remain open"); + Assert.equal( + UrlbarTestUtils.getSelectedElementIndex(window), + 0, + "The main button's index should remain selected" + ); + Assert.equal( + UrlbarTestUtils.getSelectedElement(window), + row._elements.get("tipButton"), + "The main button element should remain selected" + ); + + await UrlbarTestUtils.promisePopupClose(window); + UrlbarProvidersManager.unregisterProvider(provider); +}); + +/** + * Runs this test's main checks. + * + * @param {boolean} click + * Pass true to trigger a click, false to trigger an enter key. + * @param {string} buttonUrl + * Pass a URL if picking the main button should open a URL. Pass nothing if + * picking it should call provider.pickResult instead, or if you want to pick + * the help button instead of the main button. + * @param {string} helpUrl + * Pass a URL if you want to pick the help button. Pass nothing if you want + * to pick the main button instead. + */ +async function doTest({ click, buttonUrl = undefined, helpUrl = undefined }) { + // Open a new tab for the test if we expect to load a URL. + let tab; + if (buttonUrl || helpUrl) { + tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: "about:blank", + }); + } + + // Add our test provider. + let provider = new UrlbarTestUtils.TestProvider({ + results: [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TIP, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + type: "test", + text: "This is a test tip.", + buttonText: "Done", + buttonUrl, + helpUrl, + } + ), + ], + priority: 1, + }); + UrlbarProvidersManager.registerProvider(provider); + + // If we don't expect to load a URL, then override provider.pickResult so we + // can make sure it's called. + let pickedPromise = + !buttonUrl && !helpUrl + ? new Promise(resolve => (provider.pickResult = resolve)) + : null; + + // Do a search to show our tip result. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + value: "test", + window, + fireInputEvent: true, + }); + let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0); + let mainButton = row._elements.get("tipButton"); + let helpButton = row._elements.get("helpButton"); + let target = helpUrl ? helpButton : mainButton; + + // If we're picking the tip with the keyboard, arrow down to select the proper + // target. + if (!click) { + EventUtils.synthesizeKey("KEY_ArrowDown", { repeat: helpUrl ? 2 : 1 }); + Assert.equal( + UrlbarTestUtils.getSelectedElement(window), + target, + `${target.className} should be selected.` + ); + } + + // Now pick the target and wait for provider.pickResult to be called if we + // don't expect to load a URL, or wait for the URL to load otherwise. + await Promise.all([ + pickedPromise || BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser), + UrlbarTestUtils.promisePopupClose(window, () => { + if (click) { + EventUtils.synthesizeMouseAtCenter(target, {}); + } else { + EventUtils.synthesizeKey("KEY_Enter"); + } + }), + ]); + + // Check telemetry. + let scalars = TelemetryTestUtils.getProcessScalars("parent", true, true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "urlbar.tips", + helpUrl ? "test-help" : "test-picked", + 1 + ); + TelemetryTestUtils.assertEvents( + [ + { + category: "urlbar", + method: "engagement", + object: click ? "click" : "enter", + value: "typed", + }, + ], + { category: "urlbar" } + ); + + // Done. + UrlbarProvidersManager.unregisterProvider(provider); + if (tab) { + BrowserTestUtils.removeTab(tab); + } +} diff --git a/browser/components/urlbar/tests/browser-tips/browser_searchTips.js b/browser/components/urlbar/tests/browser-tips/browser_searchTips.js new file mode 100644 index 0000000000..aeb2a6cd83 --- /dev/null +++ b/browser/components/urlbar/tests/browser-tips/browser_searchTips.js @@ -0,0 +1,305 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests the Search Tips feature, which displays a prompt to use the Urlbar on +// the newtab page and on the user's default search engine's homepage. +// Specifically, it tests that the Tips appear when they should be appearing. +// This doesn't test the max-shown-count limit or the restriction on tips when +// we show the default browser prompt because those require restarting the +// browser. + +"use strict"; + +XPCOMUtils.defineLazyModuleGetters(this, { + AppMenuNotifications: "resource://gre/modules/AppMenuNotifications.jsm", + HttpServer: "resource://testing-common/httpd.js", + ProfileAge: "resource://gre/modules/ProfileAge.jsm", + UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm", + UrlbarProviderSearchTips: "resource:///modules/UrlbarProviderSearchTips.jsm", +}); + +// These should match the same consts in UrlbarProviderSearchTips.jsm. +const MAX_SHOWN_COUNT = 4; +const LAST_UPDATE_THRESHOLD_MS = 24 * 60 * 60 * 1000; + +// We test some of the bigger Google domains. +const GOOGLE_DOMAINS = [ + "www.google.com", + "www.google.ca", + "www.google.co.uk", + "www.google.com.au", + "www.google.co.nz", +]; + +add_task(async function init() { + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + + await SpecialPowers.pushPrefEnv({ + set: [ + [ + `browser.urlbar.tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.ONBOARD}`, + 0, + ], + [ + `browser.urlbar.tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.REDIRECT}`, + 0, + ], + ], + }); + + // Write an old profile age so tips are actually shown. + let age = await ProfileAge(); + let originalTimes = age._times; + let date = Date.now() - LAST_UPDATE_THRESHOLD_MS - 30000; + age._times = { created: date, firstUse: date }; + await age.writeTimes(); + + // Remove update history and the current active update so tips are shown. + let updateRootDir = Services.dirsvc.get("UpdRootD", Ci.nsIFile); + let updatesFile = updateRootDir.clone(); + updatesFile.append("updates.xml"); + let activeUpdateFile = updateRootDir.clone(); + activeUpdateFile.append("active-update.xml"); + try { + updatesFile.remove(false); + } catch (e) {} + try { + activeUpdateFile.remove(false); + } catch (e) {} + + let defaultEngine = await Services.search.getDefault(); + let defaultEngineName = defaultEngine.name; + Assert.equal(defaultEngineName, "Google", "Default engine should be Google."); + + registerCleanupFunction(async () => { + let age2 = await ProfileAge(); + age2._times = originalTimes; + await age2.writeTimes(); + await setDefaultEngine(defaultEngineName); + resetSearchTipsProvider(); + }); +}); + +// The onboarding tip should be shown on about:newtab. +add_task(async function newtab() { + await checkTab( + window, + "about:newtab", + UrlbarProviderSearchTips.TIP_TYPE.ONBOARD + ); +}); + +// The onboarding tip should be shown on about:home. +add_task(async function home() { + await checkTab( + window, + "about:home", + UrlbarProviderSearchTips.TIP_TYPE.ONBOARD + ); +}); + +// The redirect tip should be shown for www.google.com when it's the default +// engine. +add_task(async function google() { + await setDefaultEngine("Google"); + for (let domain of GOOGLE_DOMAINS) { + await withDNSRedirect(domain, "/", async url => { + await checkTab(window, url, UrlbarProviderSearchTips.TIP_TYPE.REDIRECT); + }); + } +}); + +// The redirect tip should be shown for www.google.com/webhp when it's the +// default engine. +add_task(async function googleWebhp() { + await setDefaultEngine("Google"); + for (let domain of GOOGLE_DOMAINS) { + await withDNSRedirect(domain, "/webhp", async url => { + await checkTab(window, url, UrlbarProviderSearchTips.TIP_TYPE.REDIRECT); + }); + } +}); + +// The redirect tip should be shown for the Google homepage when query strings +// are appended. +add_task(async function googleQueryString() { + await setDefaultEngine("Google"); + for (let domain of GOOGLE_DOMAINS) { + await withDNSRedirect(domain, "/webhp", async url => { + await checkTab( + window, + `${url}?hl=en`, + UrlbarProviderSearchTips.TIP_TYPE.REDIRECT + ); + }); + } +}); + +// The redirect tip should not be shown on Google results pages. +add_task(async function googleResults() { + await setDefaultEngine("Google"); + for (let domain of GOOGLE_DOMAINS) { + await withDNSRedirect(domain, "/search", async url => { + await checkTab( + window, + `${url}?q=firefox`, + UrlbarProviderSearchTips.TIP_TYPE.NONE + ); + }); + } +}); + +// The redirect tip should not be shown for www.google.com when it's not the +// default engine. +add_task(async function googleNotDefault() { + await setDefaultEngine("Bing"); + for (let domain of GOOGLE_DOMAINS) { + await withDNSRedirect(domain, "/", async url => { + await checkTab(window, url, UrlbarProviderSearchTips.TIP_TYPE.NONE); + }); + } +}); + +// The redirect tip should not be shown for www.google.com/webhp when it's not +// the default engine. +add_task(async function googleWebhpNotDefault() { + await setDefaultEngine("Bing"); + for (let domain of GOOGLE_DOMAINS) { + await withDNSRedirect(domain, "/webhp", async url => { + await checkTab(window, url, UrlbarProviderSearchTips.TIP_TYPE.NONE); + }); + } +}); + +// The redirect tip should be shown for www.bing.com when it's the default +// engine. +add_task(async function bing() { + await setDefaultEngine("Bing"); + await withDNSRedirect("www.bing.com", "/", async url => { + await checkTab(window, url, UrlbarProviderSearchTips.TIP_TYPE.REDIRECT); + }); +}); + +// The redirect tip should be shown on the Bing homepage even when Bing appends +// query strings. +add_task(async function bingQueryString() { + await setDefaultEngine("Bing"); + await withDNSRedirect("www.bing.com", "/", async url => { + await checkTab( + window, + `${url}?toWww=1`, + UrlbarProviderSearchTips.TIP_TYPE.REDIRECT + ); + }); +}); + +// The redirect tip should not be shown on Bing results pages. +add_task(async function bingResults() { + await setDefaultEngine("Bing"); + await withDNSRedirect("www.bing.com", "/search", async url => { + await checkTab( + window, + `${url}?q=firefox`, + UrlbarProviderSearchTips.TIP_TYPE.NONE + ); + }); +}); + +// The redirect tip should not be shown for www.bing.com when it's not the +// default engine. +add_task(async function bingNotDefault() { + await setDefaultEngine("Google"); + await withDNSRedirect("www.bing.com", "/", async url => { + await checkTab(window, url, UrlbarProviderSearchTips.TIP_TYPE.NONE); + }); +}); + +// The redirect tip should be shown for duckduckgo.com when it's the default +// engine. +add_task(async function ddg() { + await setDefaultEngine("DuckDuckGo"); + await withDNSRedirect("duckduckgo.com", "/", async url => { + await checkTab(window, url, UrlbarProviderSearchTips.TIP_TYPE.REDIRECT); + }); +}); + +// The redirect tip should be shown for start.duckduckgo.com when it's the +// default engine. +add_task(async function ddgStart() { + await setDefaultEngine("DuckDuckGo"); + await withDNSRedirect("start.duckduckgo.com", "/", async url => { + await checkTab(window, url, UrlbarProviderSearchTips.TIP_TYPE.REDIRECT); + }); +}); + +// The redirect tip should not be shown for duckduckgo.com when it's not the +// default engine. +add_task(async function ddgNotDefault() { + await setDefaultEngine("Google"); + await withDNSRedirect("duckduckgo.com", "/", async url => { + await checkTab(window, url, UrlbarProviderSearchTips.TIP_TYPE.NONE); + }); +}); + +// The redirect tip should not be shown for start.duckduckgo.com when it's not +// the default engine. +add_task(async function ddgStartNotDefault() { + await setDefaultEngine("Google"); + await withDNSRedirect("start.duckduckgo.com", "/", async url => { + await checkTab(window, url, UrlbarProviderSearchTips.TIP_TYPE.NONE); + }); +}); + +// The redirect tip should not be shown for duckduckgo.com/?q=foo, the search +// results page, which happens to have the same domain and path as the home +// page. +add_task(async function ddgSearchResultsPage() { + await setDefaultEngine("DuckDuckGo"); + await withDNSRedirect("duckduckgo.com", "/", async url => { + await checkTab( + window, + `${url}?q=test`, + UrlbarProviderSearchTips.TIP_TYPE.NONE + ); + }); +}); + +// The redirect tip should not be shown on a non-engine page. +add_task(async function nonEnginePage() { + await checkTab( + window, + "http://example.com/", + UrlbarProviderSearchTips.TIP_TYPE.NONE + ); +}); + +// Tips should be shown at most once per session regardless of their type. +add_task(async function oncePerSession() { + await setDefaultEngine("Google"); + await checkTab( + window, + "about:newtab", + UrlbarProviderSearchTips.TIP_TYPE.ONBOARD, + false + ); + await checkTab( + window, + "about:newtab", + UrlbarProviderSearchTips.TIP_TYPE.NONE, + false + ); + await withDNSRedirect("www.google.com", "/", async url => { + await checkTab(window, url, UrlbarProviderSearchTips.TIP_TYPE.NONE); + }); +}); + +// The one-off search buttons should not be shown when +// a search tip is shown even though the search string is empty. +add_task(async function shortcut_buttons_with_tip() { + await checkTab( + window, + "about:newtab", + UrlbarProviderSearchTips.TIP_TYPE.ONBOARD + ); +}); diff --git a/browser/components/urlbar/tests/browser-tips/browser_searchTips_interaction.js b/browser/components/urlbar/tests/browser-tips/browser_searchTips_interaction.js new file mode 100644 index 0000000000..7484749002 --- /dev/null +++ b/browser/components/urlbar/tests/browser-tips/browser_searchTips_interaction.js @@ -0,0 +1,620 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests the Search Tips feature, which displays a prompt to use the Urlbar on +// the newtab page and on the user's default search engine's homepage. +// Specifically, it tests that the Tips appear when they should be appearing. +// This doesn't test the max-shown-count limit because it requires restarting +// the browser. + +"use strict"; + +XPCOMUtils.defineLazyModuleGetters(this, { + AppMenuNotifications: "resource://gre/modules/AppMenuNotifications.jsm", + HttpServer: "resource://testing-common/httpd.js", + ProfileAge: "resource://gre/modules/ProfileAge.jsm", + UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm", + UrlbarProviderSearchTips: "resource:///modules/UrlbarProviderSearchTips.jsm", +}); + +XPCOMUtils.defineLazyServiceGetter( + this, + "clipboardHelper", + "@mozilla.org/widget/clipboardhelper;1", + "nsIClipboardHelper" +); + +// These should match the same consts in UrlbarProviderSearchTips.jsm. +const MAX_SHOWN_COUNT = 4; +const LAST_UPDATE_THRESHOLD_MS = 24 * 60 * 60 * 1000; + +// We test some of the bigger Google domains. +const GOOGLE_DOMAINS = [ + "www.google.com", + "www.google.ca", + "www.google.co.uk", + "www.google.com.au", + "www.google.co.nz", +]; + +add_task(async function init() { + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + + await SpecialPowers.pushPrefEnv({ + set: [ + [ + `browser.urlbar.tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.ONBOARD}`, + 0, + ], + [ + `browser.urlbar.tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.REDIRECT}`, + 0, + ], + ], + }); + + // Write an old profile age so tips are actually shown. + let age = await ProfileAge(); + let originalTimes = age._times; + let date = Date.now() - LAST_UPDATE_THRESHOLD_MS - 30000; + age._times = { created: date, firstUse: date }; + await age.writeTimes(); + + // Remove update history and the current active update so tips are shown. + let updateRootDir = Services.dirsvc.get("UpdRootD", Ci.nsIFile); + let updatesFile = updateRootDir.clone(); + updatesFile.append("updates.xml"); + let activeUpdateFile = updateRootDir.clone(); + activeUpdateFile.append("active-update.xml"); + try { + updatesFile.remove(false); + } catch (e) {} + try { + activeUpdateFile.remove(false); + } catch (e) {} + + let defaultEngine = await Services.search.getDefault(); + let defaultEngineName = defaultEngine.name; + Assert.equal(defaultEngineName, "Google", "Default engine should be Google."); + + registerCleanupFunction(async () => { + let age2 = await ProfileAge(); + age2._times = originalTimes; + await age2.writeTimes(); + await setDefaultEngine(defaultEngineName); + resetSearchTipsProvider(); + }); +}); + +// Picking the tip's button should cause the Urlbar to blank out and the tip to +// be not to be shown again in any session. Telemetry should be updated. +add_task(async function pickButton_onboard() { + UrlbarProviderSearchTips.disableTipsForCurrentSession = false; + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.eventTelemetry.enabled", true]], + }); + + Services.telemetry.clearEvents(); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: "about:newtab", + waitForLoad: false, + }); + await checkTip(window, UrlbarProviderSearchTips.TIP_TYPE.ONBOARD, false); + + // Click the tip button. + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + let button = result.element.row._elements.get("tipButton"); + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeMouseAtCenter(button, {}); + }); + gURLBar.blur(); + + // Check telemetry. + const scalars = TelemetryTestUtils.getProcessScalars("parent", true, true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "urlbar.tips", + `${UrlbarProviderSearchTips.TIP_TYPE.ONBOARD}-picked`, + 1 + ); + TelemetryTestUtils.assertEvents( + [ + { + category: "urlbar", + method: "engagement", + object: "click", + value: "typed", + }, + ], + { category: "urlbar" } + ); + + Assert.equal( + UrlbarPrefs.get( + `tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.ONBOARD}` + ), + MAX_SHOWN_COUNT, + "Onboarding tips are disabled after tip button is picked." + ); + Assert.equal(gURLBar.value, "", "The Urlbar should be empty."); + resetSearchTipsProvider(); + + BrowserTestUtils.removeTab(tab); + await SpecialPowers.popPrefEnv(); +}); + +// Picking the tip's button should cause the Urlbar to blank out and the tip to +// be not to be shown again in any session. Telemetry should be updated. +add_task(async function pickButton_redirect() { + UrlbarProviderSearchTips.disableTipsForCurrentSession = false; + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.eventTelemetry.enabled", true]], + }); + Services.telemetry.clearEvents(); + + await setDefaultEngine("Google"); + await BrowserTestUtils.withNewTab("about:blank", async () => { + await withDNSRedirect("www.google.com", "/", async url => { + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, url); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + await checkTip(window, UrlbarProviderSearchTips.TIP_TYPE.REDIRECT, false); + + // Click the tip button. + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + let button = result.element.row._elements.get("tipButton"); + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeMouseAtCenter(button, {}); + }); + gURLBar.blur(); + }); + }); + + // Check telemetry. + const scalars = TelemetryTestUtils.getProcessScalars("parent", true, true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "urlbar.tips", + `${UrlbarProviderSearchTips.TIP_TYPE.REDIRECT}-picked`, + 1 + ); + TelemetryTestUtils.assertEvents( + [ + { + category: "urlbar", + method: "engagement", + object: "click", + value: "typed", + }, + ], + { category: "urlbar" } + ); + + Assert.equal( + UrlbarPrefs.get( + `tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.REDIRECT}` + ), + MAX_SHOWN_COUNT, + "Redirect tips are disabled after tip button is picked." + ); + Assert.equal(gURLBar.value, "", "The Urlbar should be empty."); + resetSearchTipsProvider(); + await SpecialPowers.popPrefEnv(); +}); + +// Clicking in the input while the onboard tip is showing should have the same +// effect as picking the tip. +add_task(async function clickInInput_onboard() { + UrlbarProviderSearchTips.disableTipsForCurrentSession = false; + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.eventTelemetry.enabled", true]], + }); + + Services.telemetry.clearEvents(); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: "about:newtab", + waitForLoad: false, + }); + await checkTip(window, UrlbarProviderSearchTips.TIP_TYPE.ONBOARD, false); + + // Click in the input. + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeMouseAtCenter(gURLBar.textbox.parentNode, {}); + }); + gURLBar.blur(); + + // Check telemetry. + const scalars = TelemetryTestUtils.getProcessScalars("parent", true, true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "urlbar.tips", + `${UrlbarProviderSearchTips.TIP_TYPE.ONBOARD}-picked`, + 1 + ); + TelemetryTestUtils.assertEvents( + [ + { + category: "urlbar", + method: "engagement", + object: "click", + value: "typed", + }, + ], + { category: "urlbar" } + ); + + Assert.equal( + UrlbarPrefs.get( + `tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.ONBOARD}` + ), + MAX_SHOWN_COUNT, + "Onboarding tips are disabled after tip button is picked." + ); + Assert.equal(gURLBar.value, "", "The Urlbar should be empty."); + resetSearchTipsProvider(); + + BrowserTestUtils.removeTab(tab); + await SpecialPowers.popPrefEnv(); +}); + +// Pressing Ctrl+L (the open location command) while the onboard tip is showing +// should have the same effect as picking the tip. +add_task(async function openLocation_onboard() { + UrlbarProviderSearchTips.disableTipsForCurrentSession = false; + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.eventTelemetry.enabled", true]], + }); + + Services.telemetry.clearEvents(); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: "about:newtab", + waitForLoad: false, + }); + await checkTip(window, UrlbarProviderSearchTips.TIP_TYPE.ONBOARD, false); + + // Trigger the open location command. + await UrlbarTestUtils.promisePopupClose(window, () => { + document.getElementById("Browser:OpenLocation").doCommand(); + }); + gURLBar.blur(); + + // Check telemetry. + const scalars = TelemetryTestUtils.getProcessScalars("parent", true, true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "urlbar.tips", + `${UrlbarProviderSearchTips.TIP_TYPE.ONBOARD}-picked`, + 1 + ); + TelemetryTestUtils.assertEvents( + [ + { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + }, + ], + { category: "urlbar" } + ); + + Assert.equal( + UrlbarPrefs.get( + `tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.ONBOARD}` + ), + MAX_SHOWN_COUNT, + "Onboarding tips are disabled after tip button is picked." + ); + Assert.equal(gURLBar.value, "", "The Urlbar should be empty."); + resetSearchTipsProvider(); + + BrowserTestUtils.removeTab(tab); + await SpecialPowers.popPrefEnv(); +}); + +// Clicking in the input while the redirect tip is showing should have the same +// effect as picking the tip. +add_task(async function clickInInput_redirect() { + UrlbarProviderSearchTips.disableTipsForCurrentSession = false; + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.eventTelemetry.enabled", true]], + }); + Services.telemetry.clearEvents(); + + await setDefaultEngine("Google"); + await BrowserTestUtils.withNewTab("about:blank", async () => { + await withDNSRedirect("www.google.com", "/", async url => { + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, url); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + await checkTip(window, UrlbarProviderSearchTips.TIP_TYPE.REDIRECT, false); + + // Click in the input. + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeMouseAtCenter(gURLBar.textbox.parentNode, {}); + }); + gURLBar.blur(); + }); + }); + + // Check telemetry. + const scalars = TelemetryTestUtils.getProcessScalars("parent", true, true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "urlbar.tips", + `${UrlbarProviderSearchTips.TIP_TYPE.REDIRECT}-picked`, + 1 + ); + TelemetryTestUtils.assertEvents( + [ + { + category: "urlbar", + method: "engagement", + object: "click", + value: "typed", + }, + ], + { category: "urlbar" } + ); + + Assert.equal( + UrlbarPrefs.get( + `tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.REDIRECT}` + ), + MAX_SHOWN_COUNT, + "Redirect tips are disabled after tip button is picked." + ); + Assert.equal(gURLBar.value, "", "The Urlbar should be empty."); + resetSearchTipsProvider(); + await SpecialPowers.popPrefEnv(); +}); + +// Pressing Ctrl+L (the open location command) while the redirect tip is showing +// should have the same effect as picking the tip. +add_task(async function openLocation_redirect() { + UrlbarProviderSearchTips.disableTipsForCurrentSession = false; + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.eventTelemetry.enabled", true]], + }); + Services.telemetry.clearEvents(); + + await setDefaultEngine("Google"); + await BrowserTestUtils.withNewTab("about:blank", async () => { + await withDNSRedirect("www.google.com", "/", async url => { + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, url); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + await checkTip(window, UrlbarProviderSearchTips.TIP_TYPE.REDIRECT, false); + + // Trigger the open location command. + await UrlbarTestUtils.promisePopupClose(window, () => { + document.getElementById("Browser:OpenLocation").doCommand(); + }); + gURLBar.blur(); + }); + }); + + // Check telemetry. + const scalars = TelemetryTestUtils.getProcessScalars("parent", true, true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "urlbar.tips", + `${UrlbarProviderSearchTips.TIP_TYPE.REDIRECT}-picked`, + 1 + ); + TelemetryTestUtils.assertEvents( + [ + { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + }, + ], + { category: "urlbar" } + ); + + Assert.equal( + UrlbarPrefs.get( + `tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.REDIRECT}` + ), + MAX_SHOWN_COUNT, + "Redirect tips are disabled after tip button is picked." + ); + Assert.equal(gURLBar.value, "", "The Urlbar should be empty."); + resetSearchTipsProvider(); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function pickingTipDoesNotDisableOtherKinds() { + UrlbarProviderSearchTips.disableTipsForCurrentSession = false; + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: "about:newtab", + waitForLoad: false, + }); + await checkTip(window, UrlbarProviderSearchTips.TIP_TYPE.ONBOARD, false); + + // Click the tip button. + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + let button = result.element.row._elements.get("tipButton"); + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeMouseAtCenter(button, {}); + }); + + gURLBar.blur(); + Assert.equal( + UrlbarPrefs.get( + `tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.ONBOARD}` + ), + MAX_SHOWN_COUNT, + "Onboarding tips are disabled after tip button is picked." + ); + + BrowserTestUtils.removeTab(tab); + + // Simulate a new session. + UrlbarProviderSearchTips.disableTipsForCurrentSession = false; + + // Onboarding tips should no longer be shown. + let tab2 = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: "about:newtab", + waitForLoad: false, + }); + await checkTip(window, UrlbarProviderSearchTips.TIP_TYPE.NONE); + + // We should still show redirect tips. + await withDNSRedirect("www.google.com", "/", async url => { + await checkTab(window, url, UrlbarProviderSearchTips.TIP_TYPE.REDIRECT); + }); + + BrowserTestUtils.removeTab(tab2); + resetSearchTipsProvider(); +}); + +// The tip shouldn't be shown when there's another notification present. +add_task(async function notification() { + await BrowserTestUtils.withNewTab("about:blank", async () => { + let box = gBrowser.getNotificationBox(); + let note = box.appendNotification( + "Test", + "urlbar-test", + null, + box.PRIORITY_INFO_HIGH, + null, + null, + null + ); + // Give it a big persistence so it doesn't go away on page load. + note.persistence = 100; + await withDNSRedirect("www.google.com", "/", async url => { + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, url); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + await checkTip(window, UrlbarProviderSearchTips.TIP_TYPE.NONE); + box.removeNotification(note, true); + }); + }); + resetSearchTipsProvider(); +}); + +// The tip should be shown when switching to a tab where it should be shown. +add_task(async function tabSwitch() { + let tab = BrowserTestUtils.addTab(gBrowser, "about:newtab"); + UrlbarProviderSearchTips.disableTipsForCurrentSession = false; + Services.telemetry.clearScalars(); + await BrowserTestUtils.switchTab(gBrowser, tab); + await checkTip(window, UrlbarProviderSearchTips.TIP_TYPE.ONBOARD); + BrowserTestUtils.removeTab(tab); + resetSearchTipsProvider(); +}); + +// The engagement event should be ended if the user ignores a tip. +// See bug 1610024. +add_task(async function ignoreEndsEngagement() { + await setDefaultEngine("Google"); + await BrowserTestUtils.withNewTab("about:blank", async () => { + await withDNSRedirect("www.google.com", "/", async url => { + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, url); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + await checkTip(window, UrlbarProviderSearchTips.TIP_TYPE.REDIRECT, false); + // We're just looking for any target outside the Urlbar. + let spring = gURLBar.inputField + .closest("#nav-bar") + .querySelector("toolbarspring"); + await UrlbarTestUtils.promisePopupClose(window, async () => { + await EventUtils.synthesizeMouseAtCenter(spring, {}); + }); + Assert.ok( + UrlbarProviderSearchTips.showedTipTypeInCurrentEngagement == + UrlbarProviderSearchTips.TIP_TYPE.NONE, + "The engagement should have ended after the tip was ignored." + ); + }); + }); + resetSearchTipsProvider(); +}); + +add_task(async function pasteAndGo_url() { + await doPasteAndGoTest("http://example.com/", "http://example.com/"); +}); + +add_task(async function pasteAndGo_nonURL() { + // Add a mock engine so we don't hit the network loading the SERP. + let engine = await Services.search.addEngineWithDetails("Test", { + template: "http://example.com/?search={searchTerms}", + }); + let oldDefaultEngine = await Services.search.getDefault(); + Services.search.setDefault(engine); + + await doPasteAndGoTest( + "pasteAndGo_nonURL", + "http://example.com/?search=pasteAndGo_nonURL" + ); + + Services.search.setDefault(oldDefaultEngine); + await Services.search.removeEngine(engine); +}); + +async function doPasteAndGoTest(searchString, expectedURL) { + UrlbarProviderSearchTips.disableTipsForCurrentSession = false; + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: "about:newtab", + waitForLoad: false, + }); + await checkTip(window, UrlbarProviderSearchTips.TIP_TYPE.ONBOARD, false); + + await SimpleTest.promiseClipboardChange(searchString, () => { + clipboardHelper.copyString(searchString); + }); + + let textBox = gURLBar.querySelector("moz-input-box"); + let cxmenu = textBox.menupopup; + let cxmenuPromise = BrowserTestUtils.waitForEvent(cxmenu, "popupshown"); + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, { + type: "contextmenu", + button: 2, + }); + await cxmenuPromise; + let menuitem = textBox.getMenuItem("paste-and-go"); + + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + expectedURL + ); + EventUtils.synthesizeMouseAtCenter(menuitem, {}); + await browserLoadedPromise; + BrowserTestUtils.removeTab(tab); + resetSearchTipsProvider(); +} + +// Since we coupled the logic that decides whether to show the tip with our +// gURLBar.search call, we should make sure search isn't called when +// the conditions for a tip are met but the provider is disabled. +add_task(async function noActionWhenDisabled() { + await setDefaultEngine("Bing"); + await withDNSRedirect("www.bing.com", "/", async url => { + await checkTab(window, url, UrlbarProviderSearchTips.TIP_TYPE.REDIRECT); + }); + + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features", + false, + ], + ], + }); + + await withDNSRedirect("www.bing.com", "/", async url => { + Assert.ok( + !UrlbarTestUtils.isPopupOpen(window), + "The UrlbarView should not be open." + ); + }); + + await SpecialPowers.popPrefEnv(); +}); diff --git a/browser/components/urlbar/tests/browser-tips/browser_selection.js b/browser/components/urlbar/tests/browser-tips/browser_selection.js new file mode 100644 index 0000000000..b7834a8518 --- /dev/null +++ b/browser/components/urlbar/tests/browser-tips/browser_selection.js @@ -0,0 +1,284 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests keyboard selection within UrlbarUtils.RESULT_TYPE.TIP results. + +"use strict"; + +const HELP_URL = "about:mozilla"; +const TIP_URL = "about:about"; + +add_task(async function tipIsSecondResult() { + let results = [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://mozilla.org/a" } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TIP, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + icon: "", + text: "This is a test intervention.", + buttonText: "Done", + type: "test", + helpUrl: HELP_URL, + buttonUrl: TIP_URL, + } + ), + ]; + + let provider = new UrlbarTestUtils.TestProvider({ results, priority: 1 }); + UrlbarProvidersManager.registerProvider(provider); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + value: "test", + window, + }); + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 2, + "There should be two results in the view." + ); + let secondResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal( + secondResult.type, + UrlbarUtils.RESULT_TYPE.TIP, + "The second result should be a tip." + ); + + EventUtils.synthesizeKey("KEY_ArrowDown"); + Assert.equal( + UrlbarTestUtils.getSelectedElementIndex(window), + 0, + "The first element should be selected." + ); + + EventUtils.synthesizeKey("KEY_ArrowDown"); + Assert.ok( + UrlbarTestUtils.getSelectedElement(window).classList.contains( + "urlbarView-tip-button" + ), + "The selected element should be the tip button." + ); + Assert.equal( + UrlbarTestUtils.getSelectedElementIndex(window), + 1, + "The first element should be selected." + ); + + EventUtils.synthesizeKey("KEY_ArrowDown"); + Assert.ok( + UrlbarTestUtils.getSelectedElement(window).classList.contains( + "urlbarView-tip-help" + ), + "The selected element should be the tip help button." + ); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 1, + "getSelectedRowIndex should return 1 even though the help button is selected." + ); + Assert.equal( + UrlbarTestUtils.getSelectedElementIndex(window), + 2, + "The third element should be selected." + ); + + // If this test is running alone, the one-offs will rebuild themselves when + // the view is opened above, and they may not be visible yet. Wait for the + // first one to become visible before trying to select it. + await TestUtils.waitForCondition(() => { + return ( + gURLBar.view.oneOffSearchButtons.buttons.firstElementChild && + BrowserTestUtils.is_visible( + gURLBar.view.oneOffSearchButtons.buttons.firstElementChild + ) + ); + }, "Waiting for first one-off to become visible."); + + EventUtils.synthesizeKey("KEY_ArrowDown"); + await TestUtils.waitForCondition(() => { + return gURLBar.view.oneOffSearchButtons.selectedButton; + }, "Waiting for one-off to become selected."); + Assert.equal( + UrlbarTestUtils.getSelectedElementIndex(window), + -1, + "No results should be selected." + ); + + EventUtils.synthesizeKey("KEY_ArrowUp"); + Assert.ok( + UrlbarTestUtils.getSelectedElement(window).classList.contains( + "urlbarView-tip-help" + ), + "The selected element should be the tip help button." + ); + + gURLBar.view.close(); + UrlbarProvidersManager.unregisterProvider(provider); +}); + +add_task(async function tipIsOnlyResult() { + let results = [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TIP, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + icon: "", + text: "This is a test intervention.", + buttonText: "Done", + type: "test", + helpUrl: + "https://support.mozilla.org/en-US/kb/delete-browsing-search-download-history-firefox", + } + ), + ]; + + let provider = new UrlbarTestUtils.TestProvider({ results, priority: 1 }); + UrlbarProvidersManager.registerProvider(provider); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + value: "test", + window, + }); + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 1, + "There should be one result in the view." + ); + let firstResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + firstResult.type, + UrlbarUtils.RESULT_TYPE.TIP, + "The first and only result should be a tip." + ); + + EventUtils.synthesizeKey("KEY_ArrowDown"); + Assert.ok( + UrlbarTestUtils.getSelectedElement(window).classList.contains( + "urlbarView-tip-button" + ), + "The selected element should be the tip button." + ); + Assert.equal( + UrlbarTestUtils.getSelectedElementIndex(window), + 0, + "The first element should be selected." + ); + + EventUtils.synthesizeKey("KEY_ArrowDown"); + Assert.ok( + UrlbarTestUtils.getSelectedElement(window).classList.contains( + "urlbarView-tip-help" + ), + "The selected element should be the tip help button." + ); + Assert.equal( + UrlbarTestUtils.getSelectedElementIndex(window), + 1, + "The second element should be selected." + ); + + EventUtils.synthesizeKey("KEY_ArrowDown"); + Assert.equal( + UrlbarTestUtils.getSelectedElementIndex(window), + -1, + "There should be no selection." + ); + + EventUtils.synthesizeKey("KEY_ArrowUp"); + Assert.ok( + UrlbarTestUtils.getSelectedElement(window).classList.contains( + "urlbarView-tip-help" + ), + "The selected element should be the tip help button." + ); + + gURLBar.view.close(); + UrlbarProvidersManager.unregisterProvider(provider); +}); + +add_task(async function tipHasNoHelpButton() { + let results = [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://mozilla.org/a" } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TIP, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + icon: "", + text: "This is a test intervention.", + buttonText: "Done", + type: "test", + } + ), + ]; + + let provider = new UrlbarTestUtils.TestProvider({ results, priority: 1 }); + UrlbarProvidersManager.registerProvider(provider); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + value: "test", + window, + }); + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 2, + "There should be two results in the view." + ); + let secondResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal( + secondResult.type, + UrlbarUtils.RESULT_TYPE.TIP, + "The second result should be a tip." + ); + + EventUtils.synthesizeKey("KEY_ArrowDown"); + Assert.equal( + UrlbarTestUtils.getSelectedElementIndex(window), + 0, + "The first element should be selected." + ); + + EventUtils.synthesizeKey("KEY_ArrowDown"); + Assert.ok( + UrlbarTestUtils.getSelectedElement(window).classList.contains( + "urlbarView-tip-button" + ), + "The selected element should be the tip button." + ); + Assert.equal( + UrlbarTestUtils.getSelectedElementIndex(window), + 1, + "The first element should be selected." + ); + + EventUtils.synthesizeKey("KEY_ArrowDown"); + await TestUtils.waitForCondition(() => { + return gURLBar.view.oneOffSearchButtons.selectedButton; + }, "Waiting for one-off to become selected."); + Assert.equal( + UrlbarTestUtils.getSelectedElementIndex(window), + -1, + "No results should be selected." + ); + + EventUtils.synthesizeKey("KEY_ArrowUp"); + Assert.ok( + UrlbarTestUtils.getSelectedElement(window).classList.contains( + "urlbarView-tip-button" + ), + "The selected element should be the tip button." + ); + + gURLBar.view.close(); + UrlbarProvidersManager.unregisterProvider(provider); +}); diff --git a/browser/components/urlbar/tests/browser-tips/browser_updateAsk.js b/browser/components/urlbar/tests/browser-tips/browser_updateAsk.js new file mode 100644 index 0000000000..009aaf4609 --- /dev/null +++ b/browser/components/urlbar/tests/browser-tips/browser_updateAsk.js @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Checks the UPDATE_ASK tip. +// +// The update parts of this test are adapted from: +// https://searchfox.org/mozilla-central/source/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_downloadOptIn.js + +"use strict"; + +let params = { queryString: "&invalidCompleteSize=1" }; + +let downloadInfo = []; +if (Services.prefs.getBoolPref(PREF_APP_UPDATE_BITS_ENABLED, false)) { + downloadInfo[0] = { patchType: "partial", bitsResult: "0" }; +} else { + downloadInfo[0] = { patchType: "partial", internalResult: "0" }; +} + +let preSteps = [ + { + panelId: "checkingForUpdates", + checkActiveUpdate: null, + continueFile: CONTINUE_CHECK, + }, + { + panelId: "downloadAndInstall", + checkActiveUpdate: null, + continueFile: null, + }, +]; + +let postSteps = [ + { + panelId: "downloading", + checkActiveUpdate: { state: STATE_DOWNLOADING }, + continueFile: CONTINUE_DOWNLOAD, + downloadInfo, + }, + { + panelId: "apply", + checkActiveUpdate: { state: STATE_PENDING }, + continueFile: null, + }, +]; + +add_task(async function test() { + // Disable the pref that automatically downloads and installs updates. + await UpdateUtils.setAppUpdateAutoEnabled(false); + + // Set up the "download and install" update state. + await initUpdate(params); + UrlbarProviderInterventions.checkForBrowserUpdate(true); + await processUpdateSteps(preSteps); + + // Pick the tip and continue with the mock update, which should attempt to + // restart the browser. + await doUpdateTest({ + searchString: SEARCH_STRINGS.UPDATE, + tip: UrlbarProviderInterventions.TIP_TYPE.UPDATE_ASK, + title: /^A new version of .+ is available\.$/, + button: "Install and Restart to Update", + awaitCallback() { + return Promise.all([ + processUpdateSteps(postSteps), + awaitAppRestartRequest(), + ]); + }, + }); +}); diff --git a/browser/components/urlbar/tests/browser-tips/browser_updateRefresh.js b/browser/components/urlbar/tests/browser-tips/browser_updateRefresh.js new file mode 100644 index 0000000000..c462a595b7 --- /dev/null +++ b/browser/components/urlbar/tests/browser-tips/browser_updateRefresh.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Checks the UPDATE_REFRESH tip. +// +// The update parts of this test are adapted from: +// https://searchfox.org/mozilla-central/source/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_check_noUpdate.js + +"use strict"; + +let params = { queryString: "&noUpdates=1" }; + +let preSteps = [ + { + panelId: "checkingForUpdates", + checkActiveUpdate: null, + continueFile: CONTINUE_CHECK, + }, + { + panelId: "noUpdatesFound", + checkActiveUpdate: null, + continueFile: null, + }, +]; + +add_task(async function test() { + makeProfileResettable(); + + // Set up the "no updates" update state. + await initUpdate(params); + UrlbarProviderInterventions.checkForBrowserUpdate(true); + await processUpdateSteps(preSteps); + + // Picking the tip should open the refresh dialog. Click its cancel + // button. + await doUpdateTest({ + searchString: SEARCH_STRINGS.UPDATE, + tip: UrlbarProviderInterventions.TIP_TYPE.UPDATE_REFRESH, + title: /^.+ is up to date\. Trying to fix a problem\? Restore default settings and remove old add-ons for optimal performance\.$/, + button: /^Refresh .+…$/, + awaitCallback() { + return BrowserTestUtils.promiseAlertDialog( + "cancel", + "chrome://global/content/resetProfile.xhtml" + ); + }, + }); +}); diff --git a/browser/components/urlbar/tests/browser-tips/browser_updateRestart.js b/browser/components/urlbar/tests/browser-tips/browser_updateRestart.js new file mode 100644 index 0000000000..9d6b5e48a5 --- /dev/null +++ b/browser/components/urlbar/tests/browser-tips/browser_updateRestart.js @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Checks the UPDATE_RESTART tip. +// +// The update parts of this test are adapted from: +// https://searchfox.org/mozilla-central/source/toolkit/mozapps/update/tests/browser/browser_aboutDialog_bc_downloaded_staged.js + +"use strict"; + +let params = { + queryString: "&invalidCompleteSize=1", + backgroundUpdate: true, + continueFile: CONTINUE_STAGING, + waitForUpdateState: STATE_APPLIED, +}; + +let preSteps = [ + { + panelId: "apply", + checkActiveUpdate: { state: STATE_APPLIED }, + continueFile: null, + }, +]; + +add_task(async function test() { + // Enable the pref that automatically downloads and installs updates. + await SpecialPowers.pushPrefEnv({ + set: [[PREF_APP_UPDATE_STAGING_ENABLED, true]], + }); + + // Set up the "apply" update state. + await initUpdate(params); + UrlbarProviderInterventions.checkForBrowserUpdate(true); + await processUpdateSteps(preSteps); + + // Picking the tip should attempt to restart the browser. + await doUpdateTest({ + searchString: SEARCH_STRINGS.UPDATE, + tip: UrlbarProviderInterventions.TIP_TYPE.UPDATE_RESTART, + title: /^The latest .+ is downloaded and ready to install\.$/, + button: "Restart to Update", + awaitCallback: awaitAppRestartRequest, + }); +}); diff --git a/browser/components/urlbar/tests/browser-tips/browser_updateWeb.js b/browser/components/urlbar/tests/browser-tips/browser_updateWeb.js new file mode 100644 index 0000000000..4db9fb6019 --- /dev/null +++ b/browser/components/urlbar/tests/browser-tips/browser_updateWeb.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Checks the UPDATE_WEB tip. +// +// The update parts of this test are adapted from: +// https://searchfox.org/mozilla-central/source/toolkit/mozapps/update/tests/browser/browser_aboutDialog_fc_check_unsupported.js + +"use strict"; + +let params = { queryString: "&unsupported=1" }; + +let preSteps = [ + { + panelId: "checkingForUpdates", + checkActiveUpdate: null, + continueFile: CONTINUE_CHECK, + }, + { + panelId: "unsupportedSystem", + checkActiveUpdate: null, + continueFile: null, + }, +]; + +add_task(async function test() { + // Set up the "unsupported update" update state. + await initUpdate(params); + UrlbarProviderInterventions.checkForBrowserUpdate(true); + await processUpdateSteps(preSteps); + + // Picking the tip should open the download page in a new tab. + let downloadTab = await doUpdateTest({ + searchString: SEARCH_STRINGS.UPDATE, + tip: UrlbarProviderInterventions.TIP_TYPE.UPDATE_WEB, + title: /^Get the latest .+ browser\.$/, + button: "Download Now", + awaitCallback() { + return BrowserTestUtils.waitForNewTab( + gBrowser, + "https://www.mozilla.org/firefox/new/" + ); + }, + }); + + Assert.equal(gBrowser.selectedTab, downloadTab); + BrowserTestUtils.removeTab(downloadTab); +}); diff --git a/browser/components/urlbar/tests/browser-tips/head.js b/browser/components/urlbar/tests/browser-tips/head.js new file mode 100644 index 0000000000..f76377b80e --- /dev/null +++ b/browser/components/urlbar/tests/browser-tips/head.js @@ -0,0 +1,754 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This directory contains tests that check tips and interventions, and in +// particular the update-related interventions. +// We mock updates by using the test helpers in +// toolkit/mozapps/update/tests/browser. + +"use strict"; + +/* import-globals-from ../../../../../toolkit/mozapps/update/tests/browser/head.js */ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/toolkit/mozapps/update/tests/browser/head.js", + this +); + +XPCOMUtils.defineLazyModuleGetters(this, { + HttpServer: "resource://testing-common/httpd.js", + ResetProfile: "resource://gre/modules/ResetProfile.jsm", + UrlbarProviderInterventions: + "resource:///modules/UrlbarProviderInterventions.jsm", + UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.jsm", + UrlbarResult: "resource:///modules/UrlbarResult.jsm", + TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.jsm", +}); + +XPCOMUtils.defineLazyGetter(this, "UrlbarTestUtils", () => { + const { UrlbarTestUtils: module } = ChromeUtils.import( + "resource://testing-common/UrlbarTestUtils.jsm" + ); + module.init(this); + return module; +}); + +XPCOMUtils.defineLazyGetter(this, "SearchTestUtils", () => { + const { SearchTestUtils: module } = ChromeUtils.import( + "resource://testing-common/SearchTestUtils.jsm" + ); + module.init(this); + return module; +}); + +// For each intervention type, a search string that trigger the intervention. +const SEARCH_STRINGS = { + CLEAR: "firefox history", + REFRESH: "firefox slow", + UPDATE: "firefox update", +}; + +add_task(async function init() { + registerCleanupFunction(() => { + // We need to reset the provider's appUpdater.status between tests so that + // each test doesn't interfere with the next. + UrlbarProviderInterventions.resetAppUpdater(); + }); +}); + +/** + * Override our binary path so that the update lock doesn't think more than one + * instance of this test is running. + * This is a heavily pared down copy of the function in xpcshellUtilsAUS.js. + */ +function adjustGeneralPaths() { + let dirProvider = { + getFile(aProp, aPersistent) { + // Set the value of persistent to false so when this directory provider is + // unregistered it will revert back to the original provider. + aPersistent.value = false; + // The sync manager only uses XRE_EXECUTABLE_FILE, so that's all we need + // to override, we won't bother handling anything else. + if (aProp == XRE_EXECUTABLE_FILE) { + // The temp directory that the mochitest runner creates is unique per + // test, so its path can serve to provide the unique key that the update + // sync manager requires (it doesn't need for this to be the actual + // path to any real file, it's only used as an opaque string). + let tempPath = gEnv.get("MOZ_PROCESS_LOG"); + let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + file.initWithPath(tempPath); + return file; + } + return null; + }, + QueryInterface: ChromeUtils.generateQI(["nsIDirectoryServiceProvider"]), + }; + + let ds = Services.dirsvc.QueryInterface(Ci.nsIDirectoryService); + try { + ds.QueryInterface(Ci.nsIProperties).undefine(XRE_EXECUTABLE_FILE); + } catch (_ex) { + // We only override one property, so we have nothing to do if that fails. + return; + } + ds.registerProvider(dirProvider); + registerCleanupFunction(() => { + ds.unregisterProvider(dirProvider); + // Reset the update lock once again so that we know the lock we're + // interested in here will be closed properly (normally that happens during + // XPCOM shutdown, but that isn't consistent during tests). + let syncManager = Cc[ + "@mozilla.org/updates/update-sync-manager;1" + ].getService(Ci.nsIUpdateSyncManager); + syncManager.resetLock(); + }); + + // Now that we've overridden the directory provider, the name of the update + // lock needs to be changed to match the overridden path. + let syncManager = Cc["@mozilla.org/updates/update-sync-manager;1"].getService( + Ci.nsIUpdateSyncManager + ); + syncManager.resetLock(); +} + +/** + * Initializes a mock app update. Adapted from runAboutDialogUpdateTest: + * https://searchfox.org/mozilla-central/source/toolkit/mozapps/update/tests/browser/head.js + * + * @param {object} params + * See the files in toolkit/mozapps/update/tests/browser. + */ +async function initUpdate(params) { + gEnv.set("MOZ_TEST_SLOW_SKIP_UPDATE_STAGE", "1"); + await SpecialPowers.pushPrefEnv({ + set: [ + [PREF_APP_UPDATE_DISABLEDFORTESTING, false], + [PREF_APP_UPDATE_URL_MANUAL, gDetailsURL], + ], + }); + + adjustGeneralPaths(); + await setupTestUpdater(); + + let queryString = params.queryString ? params.queryString : ""; + let updateURL = + URL_HTTP_UPDATE_SJS + + "?detailsURL=" + + gDetailsURL + + queryString + + getVersionParams(); + if (params.backgroundUpdate) { + setUpdateURL(updateURL); + gAUS.checkForBackgroundUpdates(); + if (params.continueFile) { + await continueFileHandler(params.continueFile); + } + if (params.waitForUpdateState) { + let whichUpdate = + params.waitForUpdateState == STATE_DOWNLOADING + ? "downloadingUpdate" + : "readyUpdate"; + await TestUtils.waitForCondition( + () => + gUpdateManager[whichUpdate] && + gUpdateManager[whichUpdate].state == params.waitForUpdateState, + "Waiting for update state: " + params.waitForUpdateState, + undefined, + 200 + ).catch(e => { + // Instead of throwing let the check below fail the test so the panel + // ID and the expected panel ID is printed in the log. + logTestInfo(e); + }); + // Display the UI after the update state equals the expected value. + Assert.equal( + gUpdateManager[whichUpdate].state, + params.waitForUpdateState, + "The update state value should equal " + params.waitForUpdateState + ); + } + } else { + updateURL += "&slowUpdateCheck=1&useSlowDownloadMar=1"; + setUpdateURL(updateURL); + } +} + +/** + * Performs steps in a mock update. Adapted from runAboutDialogUpdateTest: + * https://searchfox.org/mozilla-central/source/toolkit/mozapps/update/tests/browser/head.js + * + * @param {array} steps + * See the files in toolkit/mozapps/update/tests/browser. + */ +async function processUpdateSteps(steps) { + for (let step of steps) { + await processUpdateStep(step); + } +} + +/** + * Performs a step in a mock update. Adapted from runAboutDialogUpdateTest: + * https://searchfox.org/mozilla-central/source/toolkit/mozapps/update/tests/browser/head.js + * + * @param {object} step + * See the files in toolkit/mozapps/update/tests/browser. + */ +async function processUpdateStep(step) { + if (typeof step == "function") { + step(); + return; + } + + const { panelId, checkActiveUpdate, continueFile, downloadInfo } = step; + if (checkActiveUpdate) { + let whichUpdate = + checkActiveUpdate.state == STATE_DOWNLOADING + ? "downloadingUpdate" + : "readyUpdate"; + await TestUtils.waitForCondition( + () => gUpdateManager[whichUpdate], + "Waiting for active update" + ); + Assert.ok( + !!gUpdateManager[whichUpdate], + "There should be an active update" + ); + Assert.equal( + gUpdateManager[whichUpdate].state, + checkActiveUpdate.state, + "The active update state should equal " + checkActiveUpdate.state + ); + } else { + Assert.ok( + !gUpdateManager.readyUpdate, + "There should not be a ready update" + ); + Assert.ok( + !gUpdateManager.downloadingUpdate, + "There should not be a downloadingUpdate update" + ); + } + + if (panelId == "downloading") { + for (let i = 0; i < downloadInfo.length; ++i) { + let data = downloadInfo[i]; + // The About Dialog tests always specify a continue file. + await continueFileHandler(continueFile); + let patch = getPatchOfType( + data.patchType, + gUpdateManager.downloadingUpdate + ); + // The update is removed early when the last download fails so check + // that there is a patch before proceeding. + let isLastPatch = i == downloadInfo.length - 1; + if (!isLastPatch || patch) { + let resultName = data.bitsResult ? "bitsResult" : "internalResult"; + patch.QueryInterface(Ci.nsIWritablePropertyBag); + await TestUtils.waitForCondition( + () => patch.getProperty(resultName) == data[resultName], + "Waiting for expected patch property " + + resultName + + " value: " + + data[resultName], + undefined, + 200 + ).catch(e => { + // Instead of throwing let the check below fail the test so the + // property value and the expected property value is printed in + // the log. + logTestInfo(e); + }); + Assert.equal( + patch.getProperty(resultName), + data[resultName], + "The patch property " + + resultName + + " value should equal " + + data[resultName] + ); + } + } + } else if (continueFile) { + await continueFileHandler(continueFile); + } +} + +/** + * Checks an intervention tip. This works by starting a search that should + * trigger a tip, picks the tip, and waits for the tip's action to happen. + * + * @param {string} searchString + * The search string. + * @param {string} tip + * The expected tip type. + * @param {string/regexp} title + * The expected tip title. + * @param {string/regexp} button + * The expected button title. + * @param {function} awaitCallback + * A function that checks the tip's action. Should return a promise (or be + * async). + * @returns {object} + * The value returned from `awaitCallback`. + */ +async function doUpdateTest({ + searchString, + tip, + title, + button, + awaitCallback, +} = {}) { + // Do a search that triggers the tip. + let [result, element] = await awaitTip(searchString); + Assert.strictEqual(result.payload.type, tip, "Tip type"); + await element.ownerDocument.l10n.translateFragment(element); + + let actualTitle = element._elements.get("title").textContent; + if (typeof title == "string") { + Assert.equal(actualTitle, title, "Title string"); + } else { + // regexp + Assert.ok(title.test(actualTitle), "Title regexp"); + } + + let actualButton = element._elements.get("tipButton").textContent; + if (typeof button == "string") { + Assert.equal(actualButton, button, "Button string"); + } else { + // regexp + Assert.ok(button.test(actualButton), "Button regexp"); + } + + Assert.ok( + BrowserTestUtils.is_visible(element._elements.get("helpButton")), + "Help button visible" + ); + + // Pick the tip and wait for the action. + let values = await Promise.all([awaitCallback(), pickTip()]); + + // Check telemetry. + const scalars = TelemetryTestUtils.getProcessScalars("parent", true, true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "urlbar.tips", + `${tip}-shown`, + 1 + ); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "urlbar.tips", + `${tip}-picked`, + 1 + ); + + return values[0] || null; +} + +/** + * Starts a search and asserts that the second result is a tip. + * + * @param {string} searchString + * The search string. + * @param {window} win + * The window. + * @returns {[result, element]} + * The result and its element in the DOM. + */ +async function awaitTip(searchString, win = window) { + let context = await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: searchString, + waitForFocus, + fireInputEvent: true, + }); + Assert.ok(context.results.length >= 2); + let result = context.results[1]; + Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.TIP); + let element = await UrlbarTestUtils.waitForAutocompleteResultAt(win, 1); + return [result, element]; +} + +/** + * Picks the current tip's button. The view should be open and the second + * result should be a tip. + */ +async function pickTip() { + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + let button = result.element.row._elements.get("tipButton"); + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeMouseAtCenter(button, {}); + }); +} + +/** + * Waits for the quit-application-requested notification and cancels it (so that + * the app isn't actually restarted). + */ +async function awaitAppRestartRequest() { + await TestUtils.topicObserved( + "quit-application-requested", + (cancelQuit, data) => { + if (data == "restart") { + cancelQuit.QueryInterface(Ci.nsISupportsPRBool).data = true; + return true; + } + return false; + } + ); +} + +/** + * Sets up the profile so that it can be reset. + */ +function makeProfileResettable() { + // Make reset possible. + let profileService = Cc["@mozilla.org/toolkit/profile-service;1"].getService( + Ci.nsIToolkitProfileService + ); + let currentProfileDir = Services.dirsvc.get("ProfD", Ci.nsIFile); + let profileName = "mochitest-test-profile-temp-" + Date.now(); + let tempProfile = profileService.createProfile( + currentProfileDir, + profileName + ); + Assert.ok( + ResetProfile.resetSupported(), + "Should be able to reset from mochitest's temporary profile once it's in the profile manager." + ); + + registerCleanupFunction(() => { + tempProfile.remove(false); + Assert.ok( + !ResetProfile.resetSupported(), + "Shouldn't be able to reset from mochitest's temporary profile once removed from the profile manager." + ); + }); +} + +/** + * Starts a search that should trigger a tip, picks the tip, and waits for the + * tip's action to happen. + * + * @param {string} searchString + * The search string. + * @param {TIPS} tip + * The expected tip type. + * @param {string} title + * The expected tip title. + * @param {string} button + * The expected button title. + * @param {function} awaitCallback + * A function that checks the tip's action. Should return a promise (or be + * async). + * @returns {*} + * The value returned from `awaitCallback`. + */ +function checkIntervention({ + searchString, + tip, + title, + button, + awaitCallback, +} = {}) { + // Opening modal dialogs confuses focus on Linux just after them, thus run + // these checks in separate tabs to better isolate them. + return BrowserTestUtils.withNewTab("about:blank", async () => { + // Do a search that triggers the tip. + let [result, element] = await awaitTip(searchString); + Assert.strictEqual(result.payload.type, tip); + await element.ownerDocument.l10n.translateFragment(element); + + let actualTitle = element._elements.get("title").textContent; + if (typeof title == "string") { + Assert.equal(actualTitle, title, "Title string"); + } else { + // regexp + Assert.ok(title.test(actualTitle), "Title regexp"); + } + + let actualButton = element._elements.get("tipButton").textContent; + if (typeof button == "string") { + Assert.equal(actualButton, button, "Button string"); + } else { + // regexp + Assert.ok(button.test(actualButton), "Button regexp"); + } + + Assert.ok(BrowserTestUtils.is_visible(element._elements.get("helpButton"))); + + let values = await Promise.all([awaitCallback(), pickTip()]); + Assert.ok(true, "Refresh dialog opened"); + + // Ensure the urlbar is closed so that the engagement is ended. + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); + + const scalars = TelemetryTestUtils.getProcessScalars("parent", true, true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "urlbar.tips", + `${tip}-shown`, + 1 + ); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "urlbar.tips", + `${tip}-picked`, + 1 + ); + + return values[0] || null; + }); +} + +/** + * Starts a search and asserts that there are no tips. + * + * @param {string} searchString + * The search string. + * @param {Window} win + */ +async function awaitNoTip(searchString, win = window) { + let context = await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: searchString, + waitForFocus, + fireInputEvent: true, + }); + for (let result of context.results) { + Assert.notEqual(result.type, UrlbarUtils.RESULT_TYPE.TIP); + } +} + +/** + * Copied from BrowserTestUtils.jsm, but lets you listen for any one of multiple + * dialog URIs instead of only one. + * @param {string} buttonAction + * What button should be pressed on the alert dialog. + * @param {array} uris + * The URIs for the alert dialogs. + * @param {function} [func] + * An optional callback. + */ +async function promiseAlertDialogOpen(buttonAction, uris, func) { + let win = await BrowserTestUtils.domWindowOpened(null, async aWindow => { + // The test listens for the "load" event which guarantees that the alert + // class has already been added (it is added when "DOMContentLoaded" is + // fired). + await BrowserTestUtils.waitForEvent(aWindow, "load"); + + return uris.includes(aWindow.document.documentURI); + }); + + if (func) { + await func(win); + return win; + } + + let dialog = win.document.querySelector("dialog"); + dialog.getButton(buttonAction).click(); + + return win; +} + +/** + * Copied from BrowserTestUtils.jsm, but lets you listen for any one of multiple + * dialog URIs instead of only one. + * @param {string} buttonAction + * What button should be pressed on the alert dialog. + * @param {array} uris + * The URIs for the alert dialogs. + * @param {function} [func] + * An optional callback. + */ +async function promiseAlertDialog(buttonAction, uris, func) { + let win = await promiseAlertDialogOpen(buttonAction, uris, func); + return BrowserTestUtils.windowClosed(win); +} + +/** + * Search tips helper. Asserts that a particular search tip is shown or that no + * search tip is shown. + * + * @param {window} win + * A browser window. + * @param {UrlbarProviderSearchTips.TIP_TYPE} expectedTip + * The expected search tip. Pass a falsey value (like zero) for none. + * @param {boolean} closeView + * If true, this function closes the urlbar view before returning. + */ +async function checkTip(win, expectedTip, closeView = true) { + if (!expectedTip) { + // Wait a bit for the tip to not show up. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 100)); + Assert.ok(!win.gURLBar.view.isOpen); + return; + } + + // Wait for the view to open, and then check the tip result. + await UrlbarTestUtils.promisePopupOpen(win, () => {}); + Assert.ok(true, "View opened"); + Assert.equal(UrlbarTestUtils.getResultCount(win), 1); + let result = await UrlbarTestUtils.getDetailsOfResultAt(win, 0); + Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.TIP); + let heuristic; + let title; + let name = Services.search.defaultEngine.name; + switch (expectedTip) { + case UrlbarProviderSearchTips.TIP_TYPE.ONBOARD: + heuristic = true; + title = + `Type less, find more: Search ${name} right from your ` + + `address bar.`; + break; + case UrlbarProviderSearchTips.TIP_TYPE.REDIRECT: + heuristic = false; + title = + `Start your search in the address bar to see suggestions from ` + + `${name} and your browsing history.`; + break; + } + Assert.equal(result.heuristic, heuristic); + Assert.equal(result.displayed.title, title); + Assert.equal( + result.element.row._elements.get("tipButton").textContent, + `Okay, Got It` + ); + Assert.ok( + BrowserTestUtils.is_hidden(result.element.row._elements.get("helpButton")) + ); + + const scalars = TelemetryTestUtils.getProcessScalars("parent", true, true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "urlbar.tips", + `${expectedTip}-shown`, + 1 + ); + + Assert.ok( + !UrlbarTestUtils.getOneOffSearchButtonsVisible(window), + "One-offs should be hidden when showing a search tip" + ); + + if (closeView) { + await UrlbarTestUtils.promisePopupClose(win); + } +} + +/** + * Search tips helper. Opens a foreground tab and asserts that a particular + * search tip is shown or that no search tip is shown. + * + * @param {window} win + * A browser window. + * @param {string} url + * The URL to load in a new foreground tab. + * @param {UrlbarProviderSearchTips.TIP_TYPE} expectedTip + * The expected search tip. Pass a falsey value (like zero) for none. + * @param {boolean} reset + * If true, the search tips provider will be reset before this function + * returns. See resetSearchTipsProvider. + */ +async function checkTab(win, url, expectedTip, reset = true) { + // BrowserTestUtils.withNewTab always waits for tab load, which hangs on + // about:newtab for some reason, so don't use it. + let shownCount; + if (expectedTip) { + shownCount = UrlbarPrefs.get(`tipShownCount.${expectedTip}`); + } + + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser: win.gBrowser, + url, + waitForLoad: url != "about:newtab", + }); + + await checkTip(win, expectedTip, true); + if (expectedTip) { + Assert.equal( + UrlbarPrefs.get(`tipShownCount.${expectedTip}`), + shownCount + 1, + "The shownCount pref should have been incremented by one." + ); + } + + if (reset) { + resetSearchTipsProvider(); + } + + BrowserTestUtils.removeTab(tab); +} + +/** + * This lets us visit www.google.com (for example) and have it redirect to + * our test HTTP server instead of visiting the actual site. + * @param {string} domain + * The domain to which we are redirecting. + * @param {string} path + * The pathname on the domain. + * @param {function} callback + * Executed when the test suite thinks `domain` is loaded. + */ +async function withDNSRedirect(domain, path, callback) { + // Some domains have special security requirements, like www.bing.com. We + // need to override them to successfully load them. This part is adapted from + // testing/marionette/cert.js. + const certOverrideService = Cc[ + "@mozilla.org/security/certoverride;1" + ].getService(Ci.nsICertOverrideService); + Services.prefs.setBoolPref( + "network.stricttransportsecurity.preloadlist", + false + ); + Services.prefs.setIntPref("security.cert_pinning.enforcement_level", 0); + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + true + ); + + // Now set network.dns.localDomains to redirect the domain to localhost and + // set up an HTTP server. + Services.prefs.setCharPref("network.dns.localDomains", domain); + + let server = new HttpServer(); + server.registerPathHandler(path, (req, resp) => { + resp.write(`Test! http://${domain}${path}`); + }); + server.start(-1); + server.identity.setPrimary("http", domain, server.identity.primaryPort); + let url = `http://${domain}:${server.identity.primaryPort}${path}`; + + await callback(url); + + // Reset network.dns.localDomains and stop the server. + Services.prefs.clearUserPref("network.dns.localDomains"); + await new Promise(resolve => server.stop(resolve)); + + // Reset the security stuff. + certOverrideService.setDisableAllSecurityChecksAndLetAttackersInterceptMyData( + false + ); + Services.prefs.clearUserPref("network.stricttransportsecurity.preloadlist"); + Services.prefs.clearUserPref("security.cert_pinning.enforcement_level"); + const sss = Cc["@mozilla.org/ssservice;1"].getService( + Ci.nsISiteSecurityService + ); + sss.clearAll(); + sss.clearPreloads(); +} + +function resetSearchTipsProvider() { + Services.prefs.clearUserPref( + `browser.urlbar.tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.ONBOARD}` + ); + Services.prefs.clearUserPref( + `browser.urlbar.tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.REDIRECT}` + ); + UrlbarProviderSearchTips.disableTipsForCurrentSession = false; +} + +async function setDefaultEngine(name) { + let engine = (await Services.search.getEngines()).find(e => e.name == name); + Assert.ok(engine); + await Services.search.setDefault(engine); +} diff --git a/browser/components/urlbar/tests/browser/POSTSearchEngine.xml b/browser/components/urlbar/tests/browser/POSTSearchEngine.xml new file mode 100644 index 0000000000..8b387ea9ae --- /dev/null +++ b/browser/components/urlbar/tests/browser/POSTSearchEngine.xml @@ -0,0 +1,6 @@ +<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"> + <ShortName>POST Search</ShortName> + <Url type="text/html" method="POST" template="http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/print_postdata.sjs"> + <Param name="searchterms" value="{searchTerms}"/> + </Url> +</OpenSearchDescription> diff --git a/browser/components/urlbar/tests/browser/authenticate.sjs b/browser/components/urlbar/tests/browser/authenticate.sjs new file mode 100644 index 0000000000..58da655cf9 --- /dev/null +++ b/browser/components/urlbar/tests/browser/authenticate.sjs @@ -0,0 +1,220 @@ +function handleRequest(request, response) +{ + try { + reallyHandleRequest(request, response); + } catch (e) { + response.setStatusLine("1.0", 200, "AlmostOK"); + response.write("Error handling request: " + e); + } +} + + +function reallyHandleRequest(request, response) { + var match; + var requestAuth = true, requestProxyAuth = true; + + // Allow the caller to drive how authentication is processed via the query. + // Eg, http://localhost:8888/authenticate.sjs?user=foo&realm=bar + // The extra ? allows the user/pass/realm checks to succeed if the name is + // at the beginning of the query string. + var query = "?" + request.queryString; + + var expected_user = "", expected_pass = "", realm = "mochitest"; + var proxy_expected_user = "", proxy_expected_pass = "", proxy_realm = "mochi-proxy"; + var huge = false, plugin = false, anonymous = false; + var authHeaderCount = 1; + // user=xxx + match = /[^_]user=([^&]*)/.exec(query); + if (match) + expected_user = match[1]; + + // pass=xxx + match = /[^_]pass=([^&]*)/.exec(query); + if (match) + expected_pass = match[1]; + + // realm=xxx + match = /[^_]realm=([^&]*)/.exec(query); + if (match) + realm = match[1]; + + // proxy_user=xxx + match = /proxy_user=([^&]*)/.exec(query); + if (match) + proxy_expected_user = match[1]; + + // proxy_pass=xxx + match = /proxy_pass=([^&]*)/.exec(query); + if (match) + proxy_expected_pass = match[1]; + + // proxy_realm=xxx + match = /proxy_realm=([^&]*)/.exec(query); + if (match) + proxy_realm = match[1]; + + // huge=1 + match = /huge=1/.exec(query); + if (match) + huge = true; + + // plugin=1 + match = /plugin=1/.exec(query); + if (match) + plugin = true; + + // multiple=1 + match = /multiple=([^&]*)/.exec(query); + if (match) + authHeaderCount = match[1]+0; + + // anonymous=1 + match = /anonymous=1/.exec(query); + if (match) + anonymous = true; + + // Look for an authentication header, if any, in the request. + // + // EG: Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== + // + // This test only supports Basic auth. The value sent by the client is + // "username:password", obscured with base64 encoding. + + var actual_user = "", actual_pass = "", authHeader, authPresent = false; + if (request.hasHeader("Authorization")) { + authPresent = true; + authHeader = request.getHeader("Authorization"); + match = /Basic (.+)/.exec(authHeader); + if (match.length != 2) + throw "Couldn't parse auth header: " + authHeader; + + var userpass = base64ToString(match[1]); // no atob() :-( + match = /(.*):(.*)/.exec(userpass); + if (match.length != 3) + throw "Couldn't decode auth header: " + userpass; + actual_user = match[1]; + actual_pass = match[2]; + } + + var proxy_actual_user = "", proxy_actual_pass = ""; + if (request.hasHeader("Proxy-Authorization")) { + authHeader = request.getHeader("Proxy-Authorization"); + match = /Basic (.+)/.exec(authHeader); + if (match.length != 2) + throw "Couldn't parse auth header: " + authHeader; + + var userpass = base64ToString(match[1]); // no atob() :-( + match = /(.*):(.*)/.exec(userpass); + if (match.length != 3) + throw "Couldn't decode auth header: " + userpass; + proxy_actual_user = match[1]; + proxy_actual_pass = match[2]; + } + + // Don't request authentication if the credentials we got were what we + // expected. + if (expected_user == actual_user && + expected_pass == actual_pass) { + requestAuth = false; + } + if (proxy_expected_user == proxy_actual_user && + proxy_expected_pass == proxy_actual_pass) { + requestProxyAuth = false; + } + + if (anonymous) { + if (authPresent) { + response.setStatusLine("1.0", 400, "Unexpected authorization header found"); + } else { + response.setStatusLine("1.0", 200, "Authorization header not found"); + } + } else { + if (requestProxyAuth) { + response.setStatusLine("1.0", 407, "Proxy authentication required"); + for (i = 0; i < authHeaderCount; ++i) + response.setHeader("Proxy-Authenticate", "basic realm=\"" + proxy_realm + "\"", true); + } else if (requestAuth) { + response.setStatusLine("1.0", 401, "Authentication required"); + for (i = 0; i < authHeaderCount; ++i) + response.setHeader("WWW-Authenticate", "basic realm=\"" + realm + "\"", true); + } else { + response.setStatusLine("1.0", 200, "OK"); + } + } + + response.setHeader("Content-Type", "application/xhtml+xml", false); + response.write("<html xmlns='http://www.w3.org/1999/xhtml'>"); + response.write("<p>Login: <span id='ok'>" + (requestAuth ? "FAIL" : "PASS") + "</span></p>\n"); + response.write("<p>Proxy: <span id='proxy'>" + (requestProxyAuth ? "FAIL" : "PASS") + "</span></p>\n"); + response.write("<p>Auth: <span id='auth'>" + authHeader + "</span></p>\n"); + response.write("<p>User: <span id='user'>" + actual_user + "</span></p>\n"); + response.write("<p>Pass: <span id='pass'>" + actual_pass + "</span></p>\n"); + + if (huge) { + response.write("<div style='display: none'>"); + for (i = 0; i < 100000; i++) { + response.write("123456789\n"); + } + response.write("</div>"); + response.write("<span id='footnote'>This is a footnote after the huge content fill</span>"); + } + + if (plugin) { + response.write("<embed id='embedtest' style='width: 400px; height: 100px;' " + + "type='application/x-test'></embed>\n"); + } + + response.write("</html>"); +} + + +// base64 decoder +// +// Yoinked from extensions/xml-rpc/src/nsXmlRpcClient.js because btoa() +// doesn't seem to exist. :-( +/* Convert Base64 data to a string */ +const toBinaryTable = [ + -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, + -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, + -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,62, -1,-1,-1,63, + 52,53,54,55, 56,57,58,59, 60,61,-1,-1, -1, 0,-1,-1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11,12,13,14, + 15,16,17,18, 19,20,21,22, 23,24,25,-1, -1,-1,-1,-1, + -1,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40, + 41,42,43,44, 45,46,47,48, 49,50,51,-1, -1,-1,-1,-1 +]; +const base64Pad = '='; + +function base64ToString(data) { + + var result = ''; + var leftbits = 0; // number of bits decoded, but yet to be appended + var leftdata = 0; // bits decoded, but yet to be appended + + // Convert one by one. + for (var i = 0; i < data.length; i++) { + var c = toBinaryTable[data.charCodeAt(i) & 0x7f]; + var padding = (data[i] == base64Pad); + // Skip illegal characters and whitespace + if (c == -1) continue; + + // Collect data into leftdata, update bitcount + leftdata = (leftdata << 6) | c; + leftbits += 6; + + // If we have 8 or more bits, append 8 bits to the result + if (leftbits >= 8) { + leftbits -= 8; + // Append if not padding. + if (!padding) + result += String.fromCharCode((leftdata >> leftbits) & 0xff); + leftdata &= (1 << leftbits) - 1; + } + } + + // If there are any bits left, the base64 string was corrupted + if (leftbits) + throw Components.Exception('Corrupted base64 string'); + + return result; +} diff --git a/browser/components/urlbar/tests/browser/browser.ini b/browser/components/urlbar/tests/browser/browser.ini new file mode 100644 index 0000000000..09b29034a8 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser.ini @@ -0,0 +1,297 @@ +# 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/. + +[DEFAULT] +support-files = + dummy_page.html + head.js + head-common.js + +[browser_aboutHomeLoading.js] +skip-if = tsan # Intermittently times out, see 1622698 (frequent on TSan). +[browser_action_searchengine.js] +[browser_action_searchengine_alias.js] +[browser_autocomplete_a11y_label.js] +support-files = + searchSuggestionEngine.xml + searchSuggestionEngine.sjs +[browser_autocomplete_autoselect.js] +[browser_autocomplete_cursor.js] +[browser_autocomplete_edit_completed.js] +[browser_autocomplete_enter_race.js] +[browser_autocomplete_no_title.js] +[browser_autocomplete_readline_navigation.js] +skip-if = os != "mac" # Mac only feature +[browser_autocomplete_tag_star_visibility.js] +[browser_autoFill_backspaced.js] +[browser_autoFill_canonize.js] +[browser_autoFill_caretNotAtEnd.js] +[browser_autoFill_firstResult.js] +[browser_autoFill_paste.js] +[browser_autoFill_placeholder.js] +[browser_autoFill_preserve.js] +[browser_autoFill_trimURLs.js] +[browser_autoFill_typed.js] +[browser_autoFill_undo.js] +[browser_autoOpen.js] +[browser_blanking.js] +support-files = + file_blank_but_not_blank.html +[browser_canonizeURL.js] +support-files = + searchSuggestionEngine.xml + searchSuggestionEngine.sjs +[browser_caret_navigation.js] +[browser_closePanelOnClick.js] +[browser_content_opener.js] +[browser_copy_during_load.js] +support-files = + slow-page.sjs +[browser_copying.js] +support-files = + authenticate.sjs +[browser_customizeMode.js] +[browser_cutting.js] +[browser_decode.js] +[browser_delete.js] +[browser_deleteAllText.js] +[browser_display_selectedAction_Extensions.js] +[browser_dns_first_for_single_words.js] +skip-if = verify && os == 'linux' # Bug 1581635 +[browser_downArrowKeySearch.js] +[browser_dragdropURL.js] +[browser_dynamicResults.js] +support-files = + dynamicResult0.css + dynamicResult1.css +[browser_edit_invalid_url.js] +[browser_enter.js] +[browser_enterAfterMouseOver.js] +[browser_focusedCmdK.js] +[browser_handleCommand_fallback.js] +[browser_hashChangeProxyState.js] +[browser_heuristicNotAddedFirst.js] +[browser_ime_composition.js] +[browser_inputHistory.js] +support-files = + searchSuggestionEngine.xml + searchSuggestionEngine.sjs +[browser_inputHistory_emptystring.js] +[browser_keepStateAcrossTabSwitches.js] +[browser_keywordBookmarklets.js] +[browser_keyword_override.js] +[browser_keywordSearch.js] +support-files = + searchSuggestionEngine.xml + searchSuggestionEngine.sjs +[browser_keywordSearch_postData.js] +support-files = + POSTSearchEngine.xml + print_postdata.sjs +[browser_keyword_select_and_type.js] +[browser_keyword.js] +support-files = + print_postdata.sjs + searchSuggestionEngine.xml + searchSuggestionEngine.sjs +[browser_loadRace.js] +[browser_locationBarCommand.js] +skip-if = (os == 'mac' && os_version == '10.14') # bug 1554807 +[browser_locationBarExternalLoad.js] +[browser_locationchange_urlbar_edit_dos.js] +support-files = + file_urlbar_edit_dos.html +[browser_new_tab_urlbar_reset.js] +[browser_oneOffs_contextMenu.js] +support-files = + searchSuggestionEngine.xml + searchSuggestionEngine.sjs +[browser_oneOffs_heuristicRestyle.js] +[browser_oneOffs_keyModifiers.js] +support-files = + searchSuggestionEngine.xml + searchSuggestionEngine.sjs +[browser_oneOffs_searchSuggestions.js] +support-files = + searchSuggestionEngine.xml + searchSuggestionEngine.sjs + searchSuggestionEngine2.xml +[browser_oneOffs_settings.js] +[browser_oneOffs.js] +support-files = + searchSuggestionEngine.xml + searchSuggestionEngine.sjs +[browser_pasteAndGo.js] +[browser_percent_encoded.js] +[browser_placeholder.js] +support-files = + searchSuggestionEngine.xml + searchSuggestionEngine2.xml + searchSuggestionEngine.sjs +[browser_populateAfterPushState.js] +[browser_primary_selection_safe_on_new_tab.js] +[browser_privateBrowsingWindowChange.js] +[browser_raceWithTabs.js] +[browser_redirect_error.js] +support-files = redirect_error.sjs +[browser_remoteness_switch.js] +run-if = e10s +[browser_remotetab.js] +[browser_remove_match.js] +[browser_removeUnsafeProtocolsFromURLBarPaste.js] +[browser_restoreEmptyInput.js] +[browser_result_onSelection.js] +[browser_resultSpan.js] +[browser_retainedResultsOnFocus.js] +[browser_revert.js] +[browser_searchFunction.js] +[browser_searchHistoryLimit.js] +[browser_searchMode_alias_replacement.js] +support-files = + searchSuggestionEngine.xml + searchSuggestionEngine.sjs +[browser_searchMode_autofill.js] +[browser_searchMode_clickLink.js] +support-files = + dummy_page.html +[browser_searchMode_engineRemoval.js] +[browser_searchMode_excludeResults.js] +[browser_searchMode_heuristic.js] +[browser_searchMode_indicator.js] +support-files = + searchSuggestionEngine.xml + searchSuggestionEngine.sjs +[browser_searchMode_localOneOffs_actionText.js] +support-files = + searchSuggestionEngine.xml + searchSuggestionEngine.sjs +[browser_searchMode_no_results.js] +[browser_searchMode_oneOffButton.js] +[browser_searchMode_pickResult.js] +[browser_searchMode_preview.js] +[browser_searchMode_sessionStore.js] +skip-if = os == 'mac' && debug && verify # bug 1671045 +[browser_searchMode_setURI.js] +[browser_searchMode_suggestions.js] +support-files = + searchSuggestionEngine.xml + searchSuggestionEngine.sjs + searchSuggestionEngineMany.xml +[browser_searchMode_switchTabs.js] +[browser_searchSettings.js] +[browser_searchSingleWordNotification.js] +[browser_searchSuggestions.js] +support-files = + searchSuggestionEngine.xml + searchSuggestionEngine.sjs +[browser_selectionKeyNavigation.js] +[browser_selectStaleResults.js] +support-files = + searchSuggestionEngineSlow.xml + searchSuggestionEngine.sjs +[browser_searchTelemetry.js] +support-files = + searchSuggestionEngine.xml + searchSuggestionEngine.sjs +[browser_separatePrivateDefault.js] +support-files = + POSTSearchEngine.xml + print_postdata.sjs + searchSuggestionEngine.xml + searchSuggestionEngine.sjs + searchSuggestionEngine2.xml +[browser_separatePrivateDefault_differentEngine.js] +support-files = + POSTSearchEngine.xml + print_postdata.sjs + searchSuggestionEngine.xml + searchSuggestionEngine.sjs + searchSuggestionEngine2.xml +[browser_speculative_connect.js] +support-files = + searchSuggestionEngine2.xml + searchSuggestionEngine.sjs +[browser_speculative_connect_not_with_client_cert.js] +[browser_stop.js] +skip-if = os == 'mac' # macosx1014 fails due to 1485288 +[browser_stop_pending.js] +support-files = + slow-page.sjs +[browser_stopSearchOnSelection.js] +support-files = + searchSuggestionEngineSlow.xml + searchSuggestionEngine.sjs +[browser_suggestedIndex.js] +[browser_switchTab_closesUrlbarPopup.js] +[browser_switchTab_decodeuri.js] +[browser_switchTab_override.js] +[browser_switchToTab_closes_newtab.js] +[browser_switchToTab_fullUrl_repeatedKeydown.js] +[browser_switchToTabHavingURI_aOpenParams.js] +[browser_tabKeyBehavior.js] +[browser_tabMatchesInAwesomebar_perwindowpb.js] +[browser_tabMatchesInAwesomebar.js] +skip-if = fission && os == 'linux' && debug # bug 1590880 +support-files = + moz.png +[browser_tabToSearch.js] +[browser_textruns.js] +[browser_tokenAlias.js] +[browser_top_sites.js] +[browser_top_sites_private.js] +[browser_typed_value.js] +[browser_updateForDomainCompletion.js] +[browser_updateRows.js] +[browser_urlbar_event_telemetry.js] +support-files = + searchSuggestionEngine.xml + searchSuggestionEngine.sjs +[browser_urlbar_selection.js] +skip-if = (os == 'mac') # bug 1570474 +[browser_urlbar_telemetry_dynamic.js] +support-files = + urlbarTelemetryUrlbarDynamic.css +[browser_urlbar_telemetry_extension.js] +tags = search-telemetry +[browser_urlbar_telemetry_places.js] +tags = search-telemetry +[browser_urlbar_telemetry_remotetab.js] +tags = search-telemetry +[browser_urlbar_telemetry_searchmode.js] +tags = search-telemetry +support-files = + urlbarTelemetrySearchSuggestions.sjs + urlbarTelemetrySearchSuggestions.xml +[browser_urlbar_telemetry_tabtosearch.js] +tags = search-telemetry +[browser_urlbar_telemetry_tip.js] +tags = search-telemetry +[browser_urlbar_telemetry_topsite.js] +tags = search-telemetry +[browser_urlbar_telemetry.js] +tags = search-telemetry +support-files = + urlbarTelemetrySearchSuggestions.sjs + urlbarTelemetrySearchSuggestions.xml +[browser_UrlbarInput_formatValue.js] +[browser_UrlbarInput_hiddenFocus.js] +[browser_UrlbarInput_overflow.js] +[browser_UrlbarInput_overflow_resize.js] +[browser_UrlbarInput_setURI.js] +[browser_UrlbarInput_tooltip.js] +[browser_UrlbarInput_trimURLs.js] + +[browser_userTypedValue.js] +support-files = file_userTypedValue.html +[browser_valueOnTabSwitch.js] +[browser_view_emptyResultSet.js] +[browser_view_resultDisplay.js] +[browser_view_resultTypes_display.js] +support-files = + print_postdata.sjs + searchSuggestionEngine.xml + searchSuggestionEngine.sjs +[browser_waitForLoadOrTimeout.js] +skip-if = tsan # Bug 1683730 +[browser_whereToOpen.js] diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_formatValue.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_formatValue.js new file mode 100644 index 0000000000..00c659f62b --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_formatValue.js @@ -0,0 +1,174 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +// Checks that the url formatter properly recognizes the host and de-emphasizes +// the rest of the url. + +/** + * Tests a given url. + * The de-emphasized parts must be wrapped in "<" and ">" chars. + * @param {string} aExpected The url to test. + * @param {string} aClobbered [optional] Normally the url is de-emphasized + * in-place, thus it's enough to pass aExpected. Though, in some cases + * the formatter may decide to replace the url with a fixed one, because + * it can't properly guess a host. In that case aClobbered is the + * expected de-emphasized value. + * @param {boolean} synthesizeInput [optional] Whether to synthesize an input + * event to test. + */ +function testVal(aExpected, aClobbered = null, synthesizeInput = false) { + let str = aExpected.replace(/[<>]/g, ""); + if (synthesizeInput) { + gURLBar.focus(); + gURLBar.select(); + EventUtils.sendString(str); + Assert.equal( + gURLBar.editor.rootElement.textContent, + str, + "Url is not highlighted" + ); + gBrowser.selectedBrowser.focus(); + } else { + gURLBar.value = str; + } + + let selectionController = gURLBar.editor.selectionController; + let selection = selectionController.getSelection( + selectionController.SELECTION_URLSECONDARY + ); + let value = gURLBar.editor.rootElement.textContent; + let result = ""; + for (let i = 0; i < selection.rangeCount; i++) { + let range = selection.getRangeAt(i).toString(); + let pos = value.indexOf(range); + result += value.substring(0, pos) + "<" + range + ">"; + value = value.substring(pos + range.length); + } + result += value; + Assert.equal( + result, + aClobbered || aExpected, + "Correct part of the url is de-emphasized" + + (synthesizeInput ? " (with input simulation)" : "") + ); + + // Now re-test synthesizing input. + if (!synthesizeInput) { + testVal(aExpected, aClobbered, true); + } +} + +function test() { + const prefname = "browser.urlbar.formatting.enabled"; + + registerCleanupFunction(function() { + Services.prefs.clearUserPref(prefname); + gURLBar.setURI(); + }); + + gBrowser.selectedBrowser.focus(); + + testVal("<https://>mozilla.org"); + testVal("<https://>mözilla.org"); + testVal("<https://>mozilla.imaginatory"); + + testVal("<https://www.>mozilla.org"); + testVal("<https://sub.>mozilla.org"); + testVal("<https://sub1.sub2.sub3.>mozilla.org"); + testVal("<www.>mozilla.org"); + testVal("<sub.>mozilla.org"); + testVal("<sub1.sub2.sub3.>mozilla.org"); + testVal("<mozilla.com.>mozilla.com"); + testVal("<https://mozilla.com:mozilla.com@>mozilla.com"); + testVal("<mozilla.com:mozilla.com@>mozilla.com"); + + testVal("<ftp.>mozilla.org"); + testVal("<ftp://ftp.>mozilla.org"); + + testVal("<https://sub.>mozilla.org"); + testVal("<https://sub1.sub2.sub3.>mozilla.org"); + testVal("<https://user:pass@sub1.sub2.sub3.>mozilla.org"); + testVal("<https://user:pass@>mozilla.org"); + testVal("<user:pass@sub1.sub2.sub3.>mozilla.org"); + testVal("<user:pass@>mozilla.org"); + + testVal("<https://>mozilla.org< >"); + testVal("mozilla.org< >"); + + testVal("<https://>mozilla.org</file.ext>"); + testVal("<https://>mozilla.org</sub/file.ext>"); + testVal("<https://>mozilla.org</sub/file.ext?foo>"); + testVal("<https://>mozilla.org</sub/file.ext?foo&bar>"); + testVal("<https://>mozilla.org</sub/file.ext?foo&bar#top>"); + testVal("<https://>mozilla.org</sub/file.ext?foo&bar#top>"); + testVal("foo.bar<?q=test>"); + testVal("foo.bar<#mozilla.org>"); + testVal("foo.bar<?somewhere.mozilla.org>"); + testVal("foo.bar<?@mozilla.org>"); + testVal("foo.bar<#x@mozilla.org>"); + testVal("foo.bar<#@x@mozilla.org>"); + testVal("foo.bar<?x@mozilla.org>"); + testVal("foo.bar<?@x@mozilla.org>"); + testVal("<foo.bar@x@>mozilla.org"); + testVal("<foo.bar@:baz@>mozilla.org"); + testVal("<foo.bar:@baz@>mozilla.org"); + testVal("<foo.bar@:ba:z@>mozilla.org"); + testVal("<foo.:bar:@baz@>mozilla.org"); + testVal( + "foopy:\\blah@somewhere.com//whatever/", + "foopy</blah@somewhere.com//whatever/>" + ); + + testVal("<https://sub.>mozilla.org<:666/file.ext>"); + testVal("<sub.>mozilla.org<:666/file.ext>"); + testVal("localhost<:666/file.ext>"); + + let IPs = [ + "192.168.1.1", + "[::]", + "[::1]", + "[1::]", + "[::]", + "[::1]", + "[1::]", + "[1:2:3:4:5:6:7::]", + "[::1:2:3:4:5:6:7]", + "[1:2:a:B:c:D:e:F]", + "[1::8]", + "[1:2::8]", + "[fe80::222:19ff:fe11:8c76]", + "[0000:0123:4567:89AB:CDEF:abcd:ef00:0000]", + "[::192.168.1.1]", + "[1::0.0.0.0]", + "[1:2::255.255.255.255]", + "[1:2:3::255.255.255.255]", + "[1:2:3:4::255.255.255.255]", + "[1:2:3:4:5::255.255.255.255]", + "[1:2:3:4:5:6:255.255.255.255]", + ]; + IPs.forEach(function(IP) { + testVal(IP); + testVal(IP + "</file.ext>"); + testVal(IP + "<:666/file.ext>"); + testVal("<https://>" + IP); + testVal(`<https://>${IP}</file.ext>`); + testVal(`<https://user:pass@>${IP}<:666/file.ext>`); + testVal(`<user:pass@>${IP}<:666/file.ext>`); + testVal(`user:\\pass@${IP}/`, `user</pass@${IP}/>`); + }); + + testVal("mailto:admin@mozilla.org"); + testVal("gopher://mozilla.org/"); + testVal("about:config"); + testVal("jar:http://mozilla.org/example.jar!/"); + testVal("view-source:http://mozilla.org/"); + testVal("foo9://mozilla.org/"); + testVal("foo+://mozilla.org/"); + testVal("foo.://mozilla.org/"); + testVal("foo-://mozilla.org/"); + + // Disable formatting. + Services.prefs.setBoolPref(prefname, false); + + testVal("https://mozilla.org"); +} diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_hiddenFocus.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_hiddenFocus.js new file mode 100644 index 0000000000..70e43e6c42 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_hiddenFocus.js @@ -0,0 +1,21 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(async function() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + registerCleanupFunction(async function() { + BrowserTestUtils.removeTab(tab); + gURLBar.setURI(); + }); + + gURLBar.blur(); + ok(!gURLBar.focused, "url bar is not focused"); + ok(!gURLBar.hasAttribute("focused"), "url bar is not visibly focused"); + gURLBar.setHiddenFocus(); + ok(gURLBar.focused, "url bar is focused"); + ok(!gURLBar.hasAttribute("focused"), "url bar is not visibly focused"); + gURLBar.removeHiddenFocus(); + ok(gURLBar.focused, "url bar is focused"); + ok(gURLBar.hasAttribute("focused"), "url bar is visibly focused"); +}); diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_overflow.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_overflow.js new file mode 100644 index 0000000000..eab30a175d --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_overflow.js @@ -0,0 +1,156 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +async function testVal(aExpected, overflowSide = "") { + info(`Testing ${aExpected}`); + try { + gURLBar.setURI(makeURI(aExpected)); + } catch (ex) { + if (ex.result != Cr.NS_ERROR_MALFORMED_URI) { + throw ex; + } + // For values without a protocol fallback to setting the raw value. + gURLBar.value = aExpected; + } + + Assert.equal( + gURLBar.selectionStart, + gURLBar.selectionEnd, + "Selection sanity check" + ); + + gURLBar.focus(); + Assert.equal( + document.activeElement, + gURLBar.inputField, + "URL Bar should be focused" + ); + Assert.equal( + gURLBar.valueFormatter.scheme.value, + "", + "Check the scheme value" + ); + Assert.equal( + getComputedStyle(gURLBar.valueFormatter.scheme).visibility, + "hidden", + "Check the scheme box visibility" + ); + + gURLBar.blur(); + await window.promiseDocumentFlushed(() => {}); + // The attribute doesn't always change, so we can't use waitForAttribute. + await TestUtils.waitForCondition( + () => gURLBar.getAttribute("textoverflow") === overflowSide + ); + + let scheme = aExpected.match(/^([a-z]+:\/{0,2})/)?.[1] || ""; + // We strip http, so we should not show the scheme for it. + if ( + scheme == "http://" && + Services.prefs.getBoolPref("browser.urlbar.trimURLs", true) + ) { + scheme = ""; + } + + Assert.equal( + gURLBar.valueFormatter.scheme.value, + scheme, + "Check the scheme value" + ); + let isOverflowed = + gURLBar.inputField.scrollWidth > gURLBar.inputField.clientWidth; + Assert.equal(isOverflowed, !!overflowSide, "Check The input field overflow"); + Assert.equal( + gURLBar.getAttribute("textoverflow"), + overflowSide, + "Check the textoverflow attribute" + ); + if (overflowSide) { + let side = gURLBar.getAttribute("domaindir") == "ltr" ? "right" : "left"; + Assert.equal(side, overflowSide, "Check the overflow side"); + Assert.equal( + getComputedStyle(gURLBar.valueFormatter.scheme).visibility, + scheme && isOverflowed && overflowSide == "left" ? "visible" : "hidden", + "Check the scheme box visibility" + ); + + info("Focus, change scroll position and blur, to ensure proper restore"); + gURLBar.focus(); + EventUtils.synthesizeKey("KEY_End"); + gURLBar.blur(); + await window.promiseDocumentFlushed(() => {}); + // The attribute doesn't always change, so we can't use waitForAttribute. + await TestUtils.waitForCondition( + () => gURLBar.getAttribute("textoverflow") === overflowSide + ); + + Assert.equal(side, overflowSide, "Check the overflow side"); + Assert.equal( + getComputedStyle(gURLBar.valueFormatter.scheme).visibility, + scheme && isOverflowed && overflowSide == "left" ? "visible" : "hidden", + "Check the scheme box visibility" + ); + } +} + +add_task(async function() { + // We use a new tab for the test to be sure all the tab switching and loading + // is complete before starting, otherwise onLocationChange for this tab could + // override the value we set with an empty value. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + registerCleanupFunction(function() { + gURLBar.setURI(); + BrowserTestUtils.removeTab(tab); + }); + + let lotsOfSpaces = "%20".repeat(200); + + // اسماء.شبكة + let rtlDomain = + "\u0627\u0633\u0645\u0627\u0621\u002e\u0634\u0628\u0643\u0629"; + let rtlChar = "\u0627"; + + // Mix the direction of the tests to cover more cases, and to ensure the + // textoverflow attribute changes every time, because tewtVal waits for that. + await testVal(`https://mozilla.org/${lotsOfSpaces}/test/`, "right"); + await testVal(`https://mozilla.org/`); + await testVal(`https://${rtlDomain}/${lotsOfSpaces}/test/`, "left"); + await testVal(`https://mozilla.org:8888/${lotsOfSpaces}/test/`, "right"); + await testVal(`https://${rtlDomain}:8888/${lotsOfSpaces}/test/`, "left"); + + await testVal(`ftp://mozilla.org/${lotsOfSpaces}/test/`, "right"); + await testVal(`ftp://${rtlDomain}/${lotsOfSpaces}/test/`, "left"); + await testVal(`ftp://mozilla.org/`); + + await testVal(`http://${rtlDomain}/${lotsOfSpaces}/test/`, "left"); + await testVal(`http://mozilla.org/`); + await testVal(`http://mozilla.org/${lotsOfSpaces}/test/`, "right"); + await testVal(`http://${rtlDomain}:8888/${lotsOfSpaces}/test/`, "left"); + await testVal(`http://[::1]/${rtlChar}/${lotsOfSpaces}/test/`, "right"); + + info("Test with formatting disabled"); + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.formatting.enabled", false], + ["browser.urlbar.trimURLs", false], + ], + }); + + await testVal(`https://mozilla.org/`); + await testVal(`https://${rtlDomain}/${lotsOfSpaces}/test/`, "left"); + await testVal(`https://mozilla.org/${lotsOfSpaces}/test/`, "right"); + + info("Test with trimURLs disabled"); + await testVal(`http://${rtlDomain}/${lotsOfSpaces}/test/`, "left"); + + await SpecialPowers.popPrefEnv(); + + info("Tests without protocol"); + await testVal(`mozilla.org/${lotsOfSpaces}/test/`, "right"); + await testVal(`mozilla.org/`); + await testVal(`${rtlDomain}/${lotsOfSpaces}/test/`, "left"); + await testVal(`mozilla.org:8888/${lotsOfSpaces}/test/`, "right"); + await testVal(`${rtlDomain}:8888/${lotsOfSpaces}/test/`, "left"); + await testVal(`[::1]/${rtlChar}/${lotsOfSpaces}/test/`, "right"); +}); diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_overflow_resize.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_overflow_resize.js new file mode 100644 index 0000000000..050961abe0 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_overflow_resize.js @@ -0,0 +1,58 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +async function testVal(win, url) { + info(`Testing ${url}`); + win.gURLBar.setURI(makeURI(url)); + + let urlbar = win.gURLBar; + urlbar.blur(); + + for (let width of [1000, 800]) { + win.resizeTo(width, 500); + await win.promiseDocumentFlushed(() => {}); + Assert.greater( + urlbar.inputField.scrollWidth, + urlbar.inputField.clientWidth, + "Check The input field overflows" + ); + // Resize is handled on a timer, so we must wait for it. + await TestUtils.waitForCondition( + () => urlbar.inputField.scrollLeft == urlbar.inputField.scrollLeftMax, + "The urlbar input field is completely scrolled to the end" + ); + await TestUtils.waitForCondition( + () => urlbar.getAttribute("textoverflow") == "left", + "Wait for the textoverflow attribute" + ); + } +} + +add_task(async function() { + // We use a new tab for the test to be sure all the tab switching and loading + // is complete before starting, otherwise onLocationChange for this tab could + // override the value we set with an empty value. + let win = await BrowserTestUtils.openNewBrowserWindow(); + registerCleanupFunction(() => BrowserTestUtils.closeWindow(win)); + + let lotsOfSpaces = "%20".repeat(200); + + // اسماء.شبكة + let rtlDomain = + "\u0627\u0633\u0645\u0627\u0621\u002e\u0634\u0628\u0643\u0629"; + + // Mix the direction of the tests to cover more cases, and to ensure the + // textoverflow attribute changes every time, because tewtVal waits for that. + await testVal(win, `https://${rtlDomain}/${lotsOfSpaces}/test/`); + + info("Test with formatting and trimurl disabled"); + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.formatting.enabled", false], + ["browser.urlbar.trimURLs", false], + ], + }); + + await testVal(win, `https://${rtlDomain}/${lotsOfSpaces}/test/`); + await testVal(win, `http://${rtlDomain}/${lotsOfSpaces}/test/`); +}); diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_setURI.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_setURI.js new file mode 100644 index 0000000000..3aabe1ecac --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_setURI.js @@ -0,0 +1,125 @@ +/* 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/. */ + +function test() { + waitForExplicitFinish(); + + // avoid prompting about phishing + Services.prefs.setIntPref(phishyUserPassPref, 32); + registerCleanupFunction(function() { + Services.prefs.clearUserPref(phishyUserPassPref); + }); + + nextTest(); +} + +const phishyUserPassPref = "network.http.phishy-userpass-length"; + +function nextTest() { + let testCase = tests.shift(); + if (testCase) { + testCase(function() { + executeSoon(nextTest); + }); + } else { + executeSoon(finish); + } +} + +var tests = [ + function revert(next) { + loadTabInWindow(window, function(tab) { + gURLBar.handleRevert(); + is( + gURLBar.value, + "example.com", + "URL bar had user/pass stripped after reverting" + ); + gBrowser.removeTab(tab); + next(); + }); + }, + function customize(next) { + // Need to wait for delayedStartup for the customization part of the test, + // since that's where BrowserToolboxCustomizeDone is set. + BrowserTestUtils.openNewBrowserWindow().then(function(win) { + loadTabInWindow(win, function() { + openToolbarCustomizationUI(function() { + closeToolbarCustomizationUI(function() { + is( + win.gURLBar.value, + "example.com", + "URL bar had user/pass stripped after customize" + ); + win.close(); + next(); + }, win); + }, win); + }); + }); + }, + function pageloaderror(next) { + loadTabInWindow(window, function(tab) { + // Load a new URL and then immediately stop it, to simulate a page load + // error. + BrowserTestUtils.loadURI(tab.linkedBrowser, "http://test1.example.com"); + tab.linkedBrowser.stop(); + is( + gURLBar.value, + "example.com", + "URL bar had user/pass stripped after load error" + ); + gBrowser.removeTab(tab); + next(); + }); + }, +]; + +function loadTabInWindow(win, callback) { + info("Loading tab"); + let url = "http://user:pass@example.com/"; + let tab = (win.gBrowser.selectedTab = BrowserTestUtils.addTab( + win.gBrowser, + url + )); + BrowserTestUtils.browserLoaded(tab.linkedBrowser, false, url).then(() => { + info("Tab loaded"); + is( + win.gURLBar.value, + "example.com", + "URL bar had user/pass stripped initially" + ); + callback(tab); + }, true); +} + +function openToolbarCustomizationUI(aCallback, aBrowserWin) { + if (!aBrowserWin) { + aBrowserWin = window; + } + + aBrowserWin.gCustomizeMode.enter(); + + aBrowserWin.gNavToolbox.addEventListener( + "customizationready", + function() { + executeSoon(function() { + aCallback(aBrowserWin); + }); + }, + { once: true } + ); +} + +function closeToolbarCustomizationUI(aCallback, aBrowserWin) { + aBrowserWin.gNavToolbox.addEventListener( + "aftercustomization", + function() { + executeSoon(aCallback); + }, + { once: true } + ); + + aBrowserWin.gCustomizeMode.exit(); +} diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_tooltip.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_tooltip.js new file mode 100644 index 0000000000..5c182d1b92 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_tooltip.js @@ -0,0 +1,83 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +function synthesizeMouseOver(element) { + info("synthesize mouseover"); + let promise = BrowserTestUtils.waitForEvent(element, "mouseover"); + EventUtils.synthesizeMouseAtCenter(document.documentElement, { + type: "mouseout", + }); + EventUtils.synthesizeMouseAtCenter(element, { type: "mouseover" }); + EventUtils.synthesizeMouseAtCenter(element, { type: "mousemove" }); + return promise; +} + +function synthesizeMouseOut(element) { + info("synthesize mouseout"); + let promise = BrowserTestUtils.waitForEvent(element, "mouseout"); + EventUtils.synthesizeMouseAtCenter(element, { type: "mouseover" }); + EventUtils.synthesizeMouseAtCenter(element, { type: "mouseout" }); + EventUtils.synthesizeMouseAtCenter(document.documentElement, { + type: "mousemove", + }); + return promise; +} + +async function expectTooltip(text) { + if (!gURLBar._overflowing && !gURLBar._inOverflow) { + info("waiting for overflow event"); + await BrowserTestUtils.waitForEvent(gURLBar.inputField, "overflow"); + } + + let tooltip = document.getElementById("aHTMLTooltip"); + let element = gURLBar.inputField; + + let popupShownPromise = BrowserTestUtils.waitForEvent(tooltip, "popupshown"); + await synthesizeMouseOver(element); + info("awaiting for tooltip popup"); + await popupShownPromise; + + is(element.getAttribute("title"), text, "title attribute has expected text"); + is(tooltip.textContent, text, "tooltip shows expected text"); + + await synthesizeMouseOut(element); +} + +async function expectNoTooltip() { + if (gURLBar._overflowing || gURLBar._inOverflow) { + info("waiting for underflow event"); + await BrowserTestUtils.waitForEvent(gURLBar.inputField, "underflow"); + } + + let element = gURLBar.inputField; + await synthesizeMouseOver(element); + + is(element.getAttribute("title"), null, "title attribute shouldn't be set"); + + await synthesizeMouseOut(element); +} + +add_task(async function() { + window.windowUtils.disableNonTestMouseEvents(true); + registerCleanupFunction(() => { + window.windowUtils.disableNonTestMouseEvents(false); + }); + + // Ensure the URL bar is neither focused nor hovered before we start. + gBrowser.selectedBrowser.focus(); + await synthesizeMouseOut(gURLBar.inputField); + + gURLBar.value = "short string"; + await expectNoTooltip(); + + let longURL = "http://longurl.com/" + "foobar/".repeat(30); + gURLBar.value = longURL; + is( + gURLBar.inputField.value, + longURL.replace(/^http:\/\//, ""), + "Urlbar value has http:// stripped" + ); + await expectTooltip(longURL); +}); diff --git a/browser/components/urlbar/tests/browser/browser_UrlbarInput_trimURLs.js b/browser/components/urlbar/tests/browser/browser_UrlbarInput_trimURLs.js new file mode 100644 index 0000000000..3c1f3ea8e4 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_UrlbarInput_trimURLs.js @@ -0,0 +1,127 @@ +add_task(async function() { + const PREF_TRIMURLS = "browser.urlbar.trimURLs"; + + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + registerCleanupFunction(async function() { + BrowserTestUtils.removeTab(tab); + Services.prefs.clearUserPref(PREF_TRIMURLS); + gURLBar.setURI(); + }); + + // Avoid search service sync init warnings due to URIFixup, when running the + // test alone. + await Services.search.init(); + + Services.prefs.setBoolPref(PREF_TRIMURLS, true); + + testVal("http://mozilla.org/", "mozilla.org"); + testVal("https://mozilla.org/", "https://mozilla.org"); + testVal("http://mözilla.org/", "mözilla.org"); + // This isn't a valid public suffix, thus we should untrim it or it would + // end up doing a search. + testVal("http://mozilla.imaginatory/"); + testVal("http://www.mozilla.org/", "www.mozilla.org"); + testVal("http://sub.mozilla.org/", "sub.mozilla.org"); + testVal("http://sub1.sub2.sub3.mozilla.org/", "sub1.sub2.sub3.mozilla.org"); + testVal("http://mozilla.org/file.ext", "mozilla.org/file.ext"); + testVal("http://mozilla.org/sub/", "mozilla.org/sub/"); + + testVal("http://ftp.mozilla.org/", "ftp.mozilla.org"); + testVal("http://ftp1.mozilla.org/", "ftp1.mozilla.org"); + testVal("http://ftp42.mozilla.org/", "ftp42.mozilla.org"); + testVal("http://ftpx.mozilla.org/", "ftpx.mozilla.org"); + testVal("ftp://ftp.mozilla.org/", "ftp://ftp.mozilla.org"); + testVal("ftp://ftp1.mozilla.org/", "ftp://ftp1.mozilla.org"); + testVal("ftp://ftp42.mozilla.org/", "ftp://ftp42.mozilla.org"); + testVal("ftp://ftpx.mozilla.org/", "ftp://ftpx.mozilla.org"); + + testVal("https://user:pass@mozilla.org/", "https://user:pass@mozilla.org"); + testVal("https://user@mozilla.org/", "https://user@mozilla.org"); + testVal("http://user:pass@mozilla.org/", "user:pass@mozilla.org"); + testVal("http://user@mozilla.org/", "user@mozilla.org"); + testVal("http://sub.mozilla.org:666/", "sub.mozilla.org:666"); + + testVal("https://[fe80::222:19ff:fe11:8c76]/file.ext"); + testVal("http://[fe80::222:19ff:fe11:8c76]/", "[fe80::222:19ff:fe11:8c76]"); + testVal("https://user:pass@[fe80::222:19ff:fe11:8c76]:666/file.ext"); + testVal( + "http://user:pass@[fe80::222:19ff:fe11:8c76]:666/file.ext", + "user:pass@[fe80::222:19ff:fe11:8c76]:666/file.ext" + ); + + testVal("mailto:admin@mozilla.org"); + testVal("gopher://mozilla.org/"); + testVal("about:config"); + testVal("jar:http://mozilla.org/example.jar!/"); + testVal("view-source:http://mozilla.org/"); + + // Behaviour for hosts with no dots depends on the whitelist: + let fixupWhitelistPref = "browser.fixup.domainwhitelist.localhost"; + Services.prefs.setBoolPref(fixupWhitelistPref, false); + testVal("http://localhost"); + Services.prefs.setBoolPref(fixupWhitelistPref, true); + testVal("http://localhost", "localhost"); + Services.prefs.clearUserPref(fixupWhitelistPref); + + testVal("http:// invalid url"); + + testVal("http://someotherhostwithnodots"); + + // This host is whitelisted, it can be trimmed. + testVal("http://localhost/ foo bar baz", "localhost/ foo bar baz"); + + // This is not trimmed because it's not in the domain whitelist. + testVal( + "http://localhost.localdomain/ foo bar baz", + "http://localhost.localdomain/ foo bar baz" + ); + + Services.prefs.setBoolPref(PREF_TRIMURLS, false); + + testVal("http://mozilla.org/"); + + Services.prefs.setBoolPref(PREF_TRIMURLS, true); + + let promiseLoaded = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + "http://example.com/" + ); + BrowserTestUtils.loadURI(gBrowser, "http://example.com/"); + await promiseLoaded; + + await testCopy("example.com", "http://example.com/"); + + gURLBar.setPageProxyState("invalid"); + gURLBar.valueIsTyped = true; + await testCopy("example.com", "example.com"); +}); + +function testVal(originalValue, targetValue) { + gURLBar.value = originalValue; + gURLBar.valueIsTyped = false; + let trimmedValue = UrlbarPrefs.get("trimURLs") + ? BrowserUtils.trimURL(originalValue) + : originalValue; + Assert.equal(gURLBar.value, trimmedValue, "url bar value set"); + // Now focus the urlbar and check the inputField value is properly set. + gURLBar.focus(); + Assert.equal( + gURLBar.value, + targetValue || originalValue, + "Check urlbar value on focus" + ); + // On blur we should trim again. + gURLBar.blur(); + Assert.equal(gURLBar.value, trimmedValue, "Check urlbar value on blur"); +} + +function testCopy(originalValue, targetValue) { + return SimpleTest.promiseClipboardChange(targetValue, () => { + Assert.equal(gURLBar.value, originalValue, "url bar copy value set"); + gURLBar.focus(); + gURLBar.select(); + goDoCommand("cmd_copy"); + }); +} diff --git a/browser/components/urlbar/tests/browser/browser_aboutHomeLoading.js b/browser/components/urlbar/tests/browser/browser_aboutHomeLoading.js new file mode 100644 index 0000000000..7ba9ed328c --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_aboutHomeLoading.js @@ -0,0 +1,189 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This tests ensures the urlbar is cleared properly when about:home is visited. + */ + +"use strict"; + +const { SessionSaver } = ChromeUtils.import( + "resource:///modules/sessionstore/SessionSaver.jsm" +); +const { TabStateFlusher } = ChromeUtils.import( + "resource:///modules/sessionstore/TabStateFlusher.jsm" +); + +/** + * Test what happens if loading a URL that should clear the + * location bar after a parent process URL. + */ +add_task(async function clearURLBarAfterParentProcessURL() { + let tab = await new Promise(resolve => { + gBrowser.selectedTab = BrowserTestUtils.addTab( + gBrowser, + "about:preferences" + ); + let newTabBrowser = gBrowser.getBrowserForTab(gBrowser.selectedTab); + newTabBrowser.addEventListener( + "Initialized", + async function() { + resolve(gBrowser.selectedTab); + }, + { capture: true, once: true } + ); + }); + document.getElementById("home-button").click(); + await BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + HomePage.get() + ); + is(gURLBar.value, "", "URL bar should be empty"); + is( + tab.linkedBrowser.userTypedValue, + null, + "The browser should have no recorded userTypedValue" + ); + BrowserTestUtils.removeTab(tab); +}); + +/** + * Same as above, but open the tab without passing the URL immediately + * which changes behaviour in tabbrowser.xml. + */ +add_task(async function clearURLBarAfterParentProcessURLInExistingTab() { + let tab = await new Promise(resolve => { + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + let newTabBrowser = gBrowser.getBrowserForTab(gBrowser.selectedTab); + newTabBrowser.addEventListener( + "Initialized", + async function() { + resolve(gBrowser.selectedTab); + }, + { capture: true, once: true } + ); + BrowserTestUtils.loadURI(newTabBrowser, "about:preferences"); + }); + document.getElementById("home-button").click(); + await BrowserTestUtils.browserLoaded( + tab.linkedBrowser, + false, + HomePage.get() + ); + is(gURLBar.value, "", "URL bar should be empty"); + is( + tab.linkedBrowser.userTypedValue, + null, + "The browser should have no recorded userTypedValue" + ); + BrowserTestUtils.removeTab(tab); +}); + +/** + * Load about:home directly from an about:newtab page. Because it is an + * 'initial' page, we need to treat this specially if the user actually + * loads a page like this from the URL bar. + */ +add_task(async function clearURLBarAfterManuallyLoadingAboutHome() { + let promiseTabOpenedAndSwitchedTo = BrowserTestUtils.switchTab( + gBrowser, + () => {} + ); + // This opens about:newtab: + BrowserOpenTab(); + let tab = await promiseTabOpenedAndSwitchedTo; + is(gURLBar.value, "", "URL bar should be empty"); + is(tab.linkedBrowser.userTypedValue, null, "userTypedValue should be null"); + + gURLBar.value = "about:home"; + gURLBar.select(); + let aboutHomeLoaded = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + "about:home" + ); + EventUtils.sendKey("return"); + await aboutHomeLoaded; + + is(gURLBar.value, "", "URL bar should be empty"); + is(tab.linkedBrowser.userTypedValue, null, "userTypedValue should be null"); + BrowserTestUtils.removeTab(tab); +}); + +/** + * Ensure we don't show 'about:home' in the URL bar temporarily in new tabs + * while we're switching remoteness (when the URL we're loading and the + * default content principal are different). + */ +add_task(async function dontTemporarilyShowAboutHome() { + requestLongerTimeout(2); + + await SpecialPowers.pushPrefEnv({ set: [["browser.startup.page", 1]] }); + let windowOpenedPromise = BrowserTestUtils.waitForNewWindow(); + let win = OpenBrowserWindow(); + await windowOpenedPromise; + let promiseTabSwitch = BrowserTestUtils.switchTab(win.gBrowser, () => {}); + win.BrowserOpenTab(); + await promiseTabSwitch; + is(win.gBrowser.visibleTabs.length, 2, "2 tabs opened"); + await TabStateFlusher.flush(win.gBrowser.selectedBrowser); + await BrowserTestUtils.closeWindow(win); + ok(SessionStore.getClosedWindowCount(), "Should have a closed window"); + + await SessionSaver.run(); + + windowOpenedPromise = BrowserTestUtils.waitForNewWindow(); + win = SessionStore.undoCloseWindow(0); + await windowOpenedPromise; + let wpl = { + onLocationChange() { + is(win.gURLBar.value, "", "URL bar value should stay empty."); + }, + }; + win.gBrowser.addProgressListener(wpl); + + if (win.gBrowser.visibleTabs.length < 2) { + await BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "TabOpen"); + } + let otherTab = win.gBrowser.selectedTab.previousElementSibling; + let tabLoaded = BrowserTestUtils.browserLoaded( + otherTab.linkedBrowser, + false, + "about:home" + ); + await BrowserTestUtils.switchTab(win.gBrowser, otherTab); + await tabLoaded; + win.gBrowser.removeProgressListener(wpl); + is(win.gURLBar.value, "", "URL bar value should be empty."); + + await BrowserTestUtils.closeWindow(win); +}); + +/** + * Test that if the Home Button is clicked after a user has typed + * some value into the URL bar, that the URL bar is cleared if + * the homepage is one of the initial pages set. + */ +add_task(async function() { + await BrowserTestUtils.withNewTab( + { + url: "http://example.com", + gBrowser, + }, + async browser => { + const TYPED_VALUE = "This string should get cleared"; + gURLBar.value = TYPED_VALUE; + browser.userTypedValue = TYPED_VALUE; + + document.getElementById("home-button").click(); + await BrowserTestUtils.browserLoaded(browser, false, HomePage.get()); + is(gURLBar.value, "", "URL bar should be empty"); + is( + browser.userTypedValue, + null, + "The browser should have no recorded userTypedValue" + ); + } + ); +}); diff --git a/browser/components/urlbar/tests/browser/browser_action_searchengine.js b/browser/components/urlbar/tests/browser/browser_action_searchengine.js new file mode 100644 index 0000000000..68bef141e5 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_action_searchengine.js @@ -0,0 +1,127 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that a search result has the correct attributes and visits the + * expected URL for the engine. + */ + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.search.separatePrivateDefault.ui.enabled", true], + ["browser.search.separatePrivateDefault", false], + ], + }); + + const engine = await Services.search.addEngineWithDetails("MozSearch", { + method: "GET", + template: "http://example.com/?q={searchTerms}", + }); + const engine2 = await Services.search.addEngineWithDetails( + "MozSearchPrivate", + { + method: "GET", + template: "http://example.com/private?q={searchTerms}", + } + ); + let originalEngine = await Services.search.getDefault(); + await Services.search.setDefault(engine); + + registerCleanupFunction(async function() { + await Services.search.setDefault(originalEngine); + await Services.search.removeEngine(engine); + await Services.search.removeEngine(engine2); + await PlacesUtils.history.clear(); + await UrlbarTestUtils.formHistory.clear(); + }); +}); + +async function testSearch(win, expectedName, expectedBaseUrl) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "open a search", + }); + let result = await UrlbarTestUtils.getDetailsOfResultAt(win, 0); + + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.SEARCH, + "Should have type search" + ); + Assert.deepEqual( + result.searchParams, + { + engine: expectedName, + keyword: undefined, + query: "open a search", + suggestion: undefined, + inPrivateWindow: undefined, + isPrivateEngine: undefined, + }, + "Should have the correct result parameters." + ); + + Assert.equal( + result.image, + UrlbarUtils.ICON.SEARCH_GLASS, + "Should have the search icon image" + ); + + let tabPromise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + let element = await UrlbarTestUtils.waitForAutocompleteResultAt(win, 0); + EventUtils.synthesizeMouseAtCenter(element, {}, win); + await tabPromise; + + Assert.equal( + win.gBrowser.selectedBrowser.currentURI.spec, + expectedBaseUrl + "?q=open+a+search", + "Should have loaded the correct page" + ); +} + +add_task(async function test_search_normal_window() { + const tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:mozilla" + ); + + registerCleanupFunction(async function() { + try { + BrowserTestUtils.removeTab(tab); + } catch (ex) { + /* tab may have already been closed in case of failure */ + } + }); + + await testSearch(window, "MozSearch", "http://example.com/"); +}); + +add_task(async function test_search_private_window_no_separate_default() { + const win = await BrowserTestUtils.openNewBrowserWindow({ private: true }); + + registerCleanupFunction(async function() { + await BrowserTestUtils.closeWindow(win); + }); + + await testSearch(win, "MozSearch", "http://example.com/"); +}); + +add_task(async function test_search_private_window() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.search.separatePrivateDefault", true]], + }); + + let engine = Services.search.getEngineByName("MozSearchPrivate"); + let originalEngine = await Services.search.getDefaultPrivate(); + await Services.search.setDefaultPrivate(engine); + + registerCleanupFunction(async () => { + await BrowserTestUtils.closeWindow(win); + await Services.search.setDefaultPrivate(originalEngine); + }); + + const win = await BrowserTestUtils.openNewBrowserWindow({ private: true }); + + await testSearch(win, "MozSearchPrivate", "http://example.com/private"); +}); diff --git a/browser/components/urlbar/tests/browser/browser_action_searchengine_alias.js b/browser/components/urlbar/tests/browser/browser_action_searchengine_alias.js new file mode 100644 index 0000000000..b399b66450 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_action_searchengine_alias.js @@ -0,0 +1,77 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that search result obtained using a search keyword gives an entry with + * the correct attributes and visits the expected URL for the engine. + */ + +add_task(async function() { + const ICON_URI = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAA" + + "CQkWg2AAABGklEQVQoz2NgGB6AnZ1dUlJSXl4eSDIyMhLW4Ovr%2B%2Fr168uXL69Zs4YoG%2BL" + + "i4i5dusTExMTGxsbNzd3f37937976%2BnpmZmagbHR09J49e5YvX66kpATVEBYW9ubNm2nTphkb" + + "G7e2tp44cQLIuHfvXm5urpaWFlDKysqqu7v73LlzECMYIiIiHj58mJCQoKKicvXq1bS0NKBgW1v" + + "bjh074uPjgeqAXE1NzSdPnvDz84M0AEUvXLgAsW379u1z5swBen3jxo2zZ892cHB4%2BvQp0KlA" + + "fwI1cHJyghQFBwfv2rULokFXV%2FfixYu7d%2B8GGqGgoMDKyrpu3br9%2B%2FcDuXl5eVA%2FA" + + "EWBfoWHAdAYoNuAYQ0XAeoUERFhGDYAAPoUaT2dfWJuAAAAAElFTkSuQmCC"; + await Services.search.addEngineWithDetails("MozSearch", { + iconURL: ICON_URI, + alias: "moz", + method: "GET", + template: "http://example.com/?q={searchTerms}", + }); + let engine = Services.search.getEngineByName("MozSearch"); + let originalEngine = await Services.search.getDefault(); + await Services.search.setDefault(engine); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:mozilla" + ); + + // Disable autofill so mozilla.org isn't autofilled below. + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.autoFill", false]], + }); + + registerCleanupFunction(async function() { + await Services.search.setDefault(originalEngine); + await Services.search.removeEngine(engine); + try { + BrowserTestUtils.removeTab(tab); + } catch (ex) { + /* tab may have already been closed in case of failure */ + } + await PlacesUtils.history.clear(); + await UrlbarTestUtils.formHistory.clear(); + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "moz", + }); + Assert.equal(gURLBar.value, "moz", "Value should be unchanged"); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "moz open a search", + }); + // Wait for the second new search that starts when search mode is entered. + await UrlbarTestUtils.promiseSearchComplete(window); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: engine.name, + entry: "typed", + }); + Assert.equal(gURLBar.value, "open a search", "value should be query"); + + let tabPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + EventUtils.synthesizeKey("KEY_Enter"); + await tabPromise; + + Assert.equal( + gBrowser.selectedBrowser.currentURI.spec, + "http://example.com/?q=open+a+search", + "Should have loaded the correct page" + ); +}); diff --git a/browser/components/urlbar/tests/browser/browser_autoFill_backspaced.js b/browser/components/urlbar/tests/browser/browser_autoFill_backspaced.js new file mode 100644 index 0000000000..92da82dfaf --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autoFill_backspaced.js @@ -0,0 +1,262 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* This test ensures that backspacing autoFilled values still allows to + * confirm the remaining value. + */ + +"use strict"; + +async function test_autocomplete(data) { + let { desc, typed, autofilled, modified, keys, type, onAutoFill } = data; + info(desc); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: typed, + fireInputEvent: true, + }); + Assert.equal(gURLBar.value, autofilled, "autofilled value is as expected"); + if (onAutoFill) { + onAutoFill(); + } + + info("Synthesizing keys"); + for (let key of keys) { + let args = Array.isArray(key) ? key : [key]; + EventUtils.synthesizeKey(...args); + } + await UrlbarTestUtils.promiseSearchComplete(window); + + Assert.equal(gURLBar.value, modified, "backspaced value is as expected"); + + Assert.greater( + UrlbarTestUtils.getResultCount(window), + 0, + "Should get at least 1 result" + ); + + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + + Assert.equal(result.type, type, "Should have the correct result type"); + + await UrlbarTestUtils.promisePopupClose(window); + gURLBar.blur(); +} + +add_task(async function() { + registerCleanupFunction(async function() { + Services.prefs.clearUserPref("browser.urlbar.autoFill"); + gURLBar.handleRevert(); + await PlacesUtils.history.clear(); + }); + Services.prefs.setBoolPref("browser.urlbar.autoFill", true); + + await PlacesTestUtils.addVisits([ + "http://example.com/", + "http://example.com/foo", + ]); + + await test_autocomplete({ + desc: "DELETE the autofilled part should search", + typed: "exam", + autofilled: "example.com/", + modified: "exam", + keys: ["KEY_Delete"], + type: UrlbarUtils.RESULT_TYPE.SEARCH, + }); + await test_autocomplete({ + desc: "DELETE the final slash should visit", + typed: "example.com", + autofilled: "example.com/", + modified: "example.com", + keys: ["KEY_Delete"], + type: UrlbarUtils.RESULT_TYPE.URL, + }); + + await test_autocomplete({ + desc: "BACK_SPACE the autofilled part should search", + typed: "exam", + autofilled: "example.com/", + modified: "exam", + keys: ["KEY_Backspace"], + type: UrlbarUtils.RESULT_TYPE.SEARCH, + }); + await test_autocomplete({ + desc: "BACK_SPACE the final slash should visit", + typed: "example.com", + autofilled: "example.com/", + modified: "example.com", + keys: ["KEY_Backspace"], + type: UrlbarUtils.RESULT_TYPE.URL, + }); + + await test_autocomplete({ + desc: "DELETE the autofilled part, then BACK_SPACE, should search", + typed: "exam", + autofilled: "example.com/", + modified: "exa", + keys: ["KEY_Delete", "KEY_Backspace"], + type: UrlbarUtils.RESULT_TYPE.SEARCH, + }); + await test_autocomplete({ + desc: "DELETE the final slash, then BACK_SPACE, should search", + typed: "example.com", + autofilled: "example.com/", + modified: "example.co", + keys: ["KEY_Delete", "KEY_Backspace"], + type: UrlbarUtils.RESULT_TYPE.URL, + }); + + await test_autocomplete({ + desc: "BACK_SPACE the autofilled part, then BACK_SPACE, should search", + typed: "exam", + autofilled: "example.com/", + modified: "exa", + keys: ["KEY_Backspace", "KEY_Backspace"], + type: UrlbarUtils.RESULT_TYPE.SEARCH, + }); + await test_autocomplete({ + desc: "BACK_SPACE the final slash, then BACK_SPACE, should search", + typed: "example.com", + autofilled: "example.com/", + modified: "example.co", + keys: ["KEY_Backspace", "KEY_Backspace"], + type: UrlbarUtils.RESULT_TYPE.URL, + }); + + await test_autocomplete({ + desc: "BACK_SPACE after blur should search", + typed: "ex", + autofilled: "example.com/", + modified: "e", + keys: ["KEY_Backspace"], + type: UrlbarUtils.RESULT_TYPE.SEARCH, + onAutoFill: () => { + gURLBar.blur(); + gURLBar.focus(); + Assert.equal( + gURLBar.selectionStart, + gURLBar.value.length, + "blur/focus should not change selection" + ); + Assert.equal( + gURLBar.selectionEnd, + gURLBar.value.length, + "blur/focus should not change selection" + ); + }, + }); + await test_autocomplete({ + desc: "DELETE after blur should search", + typed: "ex", + autofilled: "example.com/", + modified: "e", + keys: ["KEY_ArrowLeft", "KEY_Delete"], + type: UrlbarUtils.RESULT_TYPE.SEARCH, + onAutoFill: () => { + gURLBar.blur(); + gURLBar.focus(); + Assert.equal( + gURLBar.selectionStart, + gURLBar.value.length, + "blur/focus should not change selection" + ); + Assert.equal( + gURLBar.selectionEnd, + gURLBar.value.length, + "blur/focus should not change selection" + ); + }, + }); + await test_autocomplete({ + desc: "double BACK_SPACE after blur should search", + typed: "exa", + autofilled: "example.com/", + modified: "e", + keys: ["KEY_Backspace", "KEY_Backspace"], + type: UrlbarUtils.RESULT_TYPE.SEARCH, + onAutoFill: () => { + gURLBar.blur(); + gURLBar.focus(); + Assert.equal( + gURLBar.selectionStart, + gURLBar.value.length, + "blur/focus should not change selection" + ); + Assert.equal( + gURLBar.selectionEnd, + gURLBar.value.length, + "blur/focus should not change selection" + ); + }, + }); + + await test_autocomplete({ + desc: + "Right arrow key and then backspace should delete the backslash and not re-trigger autofill", + typed: "ex", + autofilled: "example.com/", + modified: "example.com", + keys: ["KEY_ArrowRight", "KEY_Backspace"], + type: UrlbarUtils.RESULT_TYPE.URL, + }); + + await test_autocomplete({ + desc: + "Right arrow key, selecting the last few characters using the keyboard, and then backspace should delete the characters and not re-trigger autofill", + typed: "ex", + autofilled: "example.com/", + modified: "example.c", + keys: [ + "KEY_ArrowRight", + ["KEY_ArrowLeft", { shiftKey: true }], + ["KEY_ArrowLeft", { shiftKey: true }], + ["KEY_ArrowLeft", { shiftKey: true }], + "KEY_Backspace", + ], + type: UrlbarUtils.RESULT_TYPE.SEARCH, + }); + + await test_autocomplete({ + desc: + "End and then backspace should delete the backslash and not re-trigger autofill", + typed: "ex", + autofilled: "example.com/", + modified: "example.com", + keys: [ + AppConstants.platform == "macosx" + ? ["KEY_ArrowRight", { metaKey: true }] + : "KEY_End", + "KEY_Backspace", + ], + type: UrlbarUtils.RESULT_TYPE.URL, + }); + + await test_autocomplete({ + desc: + "Clicking in the input after the text and then backspace should delete the backslash and not re-trigger autofill", + typed: "ex", + autofilled: "example.com/", + modified: "example.com", + keys: ["KEY_Backspace"], + type: UrlbarUtils.RESULT_TYPE.URL, + onAutoFill: () => { + // This assumes that the center of the input is to the right of the end + // of the text, so the caret is placed at the end of the text on click. + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }, + }); + + await test_autocomplete({ + desc: + "Selecting the next result and then backspace should delete the last character and not re-trigger autofill", + typed: "ex", + autofilled: "example.com/", + modified: "example.com/fo", + keys: ["KEY_ArrowDown", "KEY_Backspace"], + type: UrlbarUtils.RESULT_TYPE.URL, + }); + + await PlacesUtils.history.clear(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_autoFill_canonize.js b/browser/components/urlbar/tests/browser/browser_autoFill_canonize.js new file mode 100644 index 0000000000..099fc90417 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autoFill_canonize.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* This test ensures that pressing ctrl+enter bypasses the autoFilled + * value, and only considers what the user typed (but not just enter). + */ + +async function test_autocomplete(data) { + let { desc, typed, autofilled, modified, waitForUrl, keys } = data; + info(desc); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: typed, + }); + Assert.equal(gURLBar.value, autofilled, "autofilled value is as expected"); + + let promiseLoad = BrowserTestUtils.waitForDocLoadAndStopIt( + waitForUrl, + gBrowser.selectedBrowser + ); + + keys.forEach(([key, mods]) => EventUtils.synthesizeKey(key, mods)); + + Assert.equal(gURLBar.value, modified, "value is as expected"); + + await promiseLoad; + gURLBar.blur(); +} + +add_task(async function() { + registerCleanupFunction(async function() { + Services.prefs.clearUserPref("browser.urlbar.autoFill"); + gURLBar.handleRevert(); + await PlacesUtils.history.clear(); + }); + Services.prefs.setBoolPref("browser.urlbar.autoFill", true); + + // Add a typed visit, so it will be autofilled. + await PlacesTestUtils.addVisits({ + uri: "http://example.com/", + transition: Ci.nsINavHistoryService.TRANSITION_TYPED, + }); + + await test_autocomplete({ + desc: "CTRL+ENTER on the autofilled part should use autofill", + typed: "exam", + autofilled: "example.com/", + modified: "example.com", + waitForUrl: "http://example.com/", + keys: [["KEY_Enter"]], + }); + + await test_autocomplete({ + desc: "CTRL+ENTER on the autofilled part should bypass autofill", + typed: "exam", + autofilled: "example.com/", + modified: "https://www.exam.com", + waitForUrl: "https://www.exam.com/", + keys: [["KEY_Enter", { ctrlKey: true }]], + }); +}); diff --git a/browser/components/urlbar/tests/browser/browser_autoFill_caretNotAtEnd.js b/browser/components/urlbar/tests/browser/browser_autoFill_caretNotAtEnd.js new file mode 100644 index 0000000000..570a1c2c8c --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autoFill_caretNotAtEnd.js @@ -0,0 +1,34 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function noAutofillWhenCaretNotAtEnd() { + gURLBar.focus(); + + // Add a visit that can be autofilled. + await PlacesUtils.history.clear(); + await PlacesTestUtils.addVisits([ + { + uri: "http://example.com/", + }, + ]); + + // Fill the input with xample. + gURLBar.inputField.value = "xample"; + + // Move the caret to the beginning and type e. + gURLBar.selectionStart = 0; + gURLBar.selectionEnd = 0; + EventUtils.sendString("e"); + + // Check the first result and input. + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(!result.autofill, "The first result should not be autofill"); + + Assert.equal(gURLBar.value, "example"); + Assert.equal(gURLBar.selectionStart, 1); + Assert.equal(gURLBar.selectionEnd, 1); + + await PlacesUtils.history.clear(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_autoFill_firstResult.js b/browser/components/urlbar/tests/browser/browser_autoFill_firstResult.js new file mode 100644 index 0000000000..a425ddd4a7 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autoFill_firstResult.js @@ -0,0 +1,199 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This test makes sure that autofilling the first result of a new search works +// correctly: autofill happens when it should and doesn't when it shouldn't. + +"use strict"; + +add_task(async function init() { + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); + await PlacesTestUtils.addVisits(["http://example.com/"]); + + // Disable placeholder completion. The point of this test is to make sure the + // first result is autofilled (or not) correctly. Autofilling the placeholder + // before the search starts interferes with that. + gURLBar._enableAutofillPlaceholder = false; + registerCleanupFunction(async () => { + gURLBar._enableAutofillPlaceholder = true; + }); +}); + +// The first result should be autofilled when all conditions are met. This also +// does a sanity check to make sure that placeholder autofill is correctly +// disabled, which is helpful for all tasks here and is why this one is first. +add_task(async function successfulAutofill() { + // Do a simple search that should autofill. This will also set up the + // autofill placeholder string, which next we make sure is *not* autofilled. + await doInitialAutofillSearch(); + + // As a sanity check, do another search to make sure the placeholder is *not* + // autofilled. Make sure it's not autofilled by checking the input value and + // selection *before* the search completes. If placeholder autofill was not + // correctly disabled, then these assertions will fail. + + gURLBar.value = "exa"; + UrlbarTestUtils.fireInputEvent(window); + + // before the search completes: no autofill + Assert.equal(gURLBar.value, "exa"); + Assert.equal(gURLBar.selectionStart, "exa".length); + Assert.equal(gURLBar.selectionEnd, "exa".length); + + await UrlbarTestUtils.promiseSearchComplete(window); + + // after the search completes: successful autofill + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill); + Assert.equal(gURLBar.value, "example.com/"); + Assert.equal(gURLBar.selectionStart, "exa".length); + Assert.equal(gURLBar.selectionEnd, "example.com/".length); +}); + +// The first result should not be autofilled when it's not an autofill result. +add_task(async function firstResultNotAutofill() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "foo", + fireInputEvent: true, + }); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(!details.autofill); + Assert.equal(gURLBar.value, "foo"); + Assert.equal(gURLBar.selectionStart, "foo".length); + Assert.equal(gURLBar.selectionEnd, "foo".length); +}); + +// The first result should *not* be autofilled when the placeholder is not +// selected, the selection is empty, and the caret is *not* at the end of the +// search string. +add_task(async function caretNotAtEndOfSearchString() { + // To set up the placeholder, do an initial search that triggers autofill. + await doInitialAutofillSearch(); + + // Now do another search but set the caret to somewhere else besides the end + // of the new search string. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exam", + selectionStart: "exa".length, + selectionEnd: "exa".length, + }); + + // The first result should be an autofill result, but it should not have been + // autofilled. + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill); + Assert.equal(gURLBar.value, "exam"); + Assert.equal(gURLBar.selectionStart, "exa".length); + Assert.equal(gURLBar.selectionEnd, "exa".length); + + await cleanUp(); +}); + +// The first result should *not* be autofilled when the placeholder is not +// selected, the selection is *not* empty, and the caret is at the end of the +// search string. +add_task(async function selectionNotEmpty() { + // To set up the placeholder, do an initial search that triggers autofill. + await doInitialAutofillSearch(); + + // Now do another search. Set the selection end at the end of the search + // string, but make the selection non-empty. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exam", + selectionStart: "exa".length, + selectionEnd: "exam".length, + }); + + // The first result should be an autofill result, but it should not have been + // autofilled. + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill); + Assert.equal(gURLBar.value, "exam"); + Assert.equal(gURLBar.selectionStart, "exa".length); + Assert.equal(gURLBar.selectionEnd, "exam".length); + + await cleanUp(); +}); + +// The first result should be autofilled when the placeholder is not selected, +// the selection is empty, and the caret is at the end of the search string. +add_task(async function successfulAutofillAfterSettingPlaceholder() { + // To set up the placeholder, do an initial search that triggers autofill. + await doInitialAutofillSearch(); + + // Now do another search. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exam", + selectionStart: "exam".length, + selectionEnd: "exam".length, + }); + + // It should be autofilled. + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill); + Assert.equal(gURLBar.value, "example.com/"); + Assert.equal(gURLBar.selectionStart, "exam".length); + Assert.equal(gURLBar.selectionEnd, "example.com/".length); + + await cleanUp(); +}); + +// The first result should be autofilled when the placeholder *is* selected -- +// more precisely, when the portion of the placeholder after the new search +// string is selected. +add_task(async function successfulAutofillPlaceholderSelected() { + // To set up the placeholder, do an initial search that triggers autofill. + await doInitialAutofillSearch(); + + // Now do another search and select the portion of the placeholder after the + // new search string. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exam", + selectionStart: "exam".length, + selectionEnd: "example.com/".length, + }); + + // It should be autofilled. + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill); + Assert.equal(gURLBar.value, "example.com/"); + Assert.equal(gURLBar.selectionStart, "exam".length); + Assert.equal(gURLBar.selectionEnd, "example.com/".length); + + await cleanUp(); +}); + +async function doInitialAutofillSearch() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "ex", + fireInputEvent: true, + }); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill); + Assert.equal(gURLBar.value, "example.com/"); + Assert.equal(gURLBar.selectionStart, "ex".length); + Assert.equal(gURLBar.selectionEnd, "example.com/".length); +} + +async function cleanUp() { + // In some cases above, a test task searches for "exam" at the end, and then + // the next task searches for "ex". Autofill results will not be allowed in + // the next task in that case since the old search string starts with the new + // search string. To prevent one task from interfering with the next, do a + // search that changes the search string. Also close the popup while we're + // here, although that's not really necessary. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "reset last search string", + }); + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_Escape"); + }); +} diff --git a/browser/components/urlbar/tests/browser/browser_autoFill_paste.js b/browser/components/urlbar/tests/browser/browser_autoFill_paste.js new file mode 100644 index 0000000000..47d92cb7d3 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autoFill_paste.js @@ -0,0 +1,38 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This test checks we don't autofill on paste. + +"use strict"; + +add_task(async function test() { + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); + await PlacesTestUtils.addVisits(["http://example.com/"]); + registerCleanupFunction(async () => { + await PlacesUtils.history.clear(); + }); + + // Search for "e". It should autofill to example.com/. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "e", + fireInputEvent: true, + }); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill); + Assert.equal(gURLBar.value, "example.com/"); + Assert.equal(gURLBar.selectionStart, "e".length); + Assert.equal(gURLBar.selectionEnd, "example.com/".length); + + // Now paste. + await selectAndPaste("ex"); + + // Nothing should have been autofilled. + await UrlbarTestUtils.promiseSearchComplete(window); + details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(!details.autofill); + Assert.equal(gURLBar.value, "ex"); + Assert.equal(gURLBar.selectionStart, "ex".length); + Assert.equal(gURLBar.selectionEnd, "ex".length); +}); diff --git a/browser/components/urlbar/tests/browser/browser_autoFill_placeholder.js b/browser/components/urlbar/tests/browser/browser_autoFill_placeholder.js new file mode 100644 index 0000000000..7da2230ad1 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autoFill_placeholder.js @@ -0,0 +1,257 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This test makes sure that the autofill placeholder value is autofilled +// correctly. The placeholder is a string that we immediately autofill when a +// search starts and before its first result arrives in order to prevent text +// flicker in the input. +// +// Because this test specifically checks autofill *before* searches complete, we +// can't use promiseAutocompleteResultPopup() or other helpers that wait for +// searches to complete. Instead the test uses fireInputEvent() to trigger +// placeholder autofill and then immediately checks autofill status. + +"use strict"; + +add_task(async function init() { + await cleanUp(); +}); + +add_task(async function origin() { + await PlacesTestUtils.addVisits("http://example.com/"); + + // Do an initial search that triggers autofill so that the placeholder has an + // initial value. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "ex", + fireInputEvent: true, + }); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill); + Assert.equal(gURLBar.value, "example.com/"); + Assert.equal(gURLBar.selectionStart, "ex".length); + Assert.equal(gURLBar.selectionEnd, "example.com/".length); + + await searchAndCheck("exa", "example.com/"); + await searchAndCheck("EXAM", "EXAMple.com/"); + await searchAndCheck("eXaMp", "eXaMple.com/"); + await searchAndCheck("exampl", "example.com/"); + + await cleanUp(); +}); + +add_task(async function tokenAlias() { + // We have built-in engine aliases that may conflict with the one we choose + // here in terms of autofill, so be careful and choose a weird alias. + await Services.search.addEngineWithDetails("Test", { + alias: "@__example", + template: "http://example.com/?search={searchTerms}", + }); + registerCleanupFunction(async function() { + let engine = Services.search.getEngineByName("Test"); + await Services.search.removeEngine(engine); + }); + + // Do an initial search that triggers autofill so that the placeholder has an + // initial value. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "@__ex", + fireInputEvent: true, + }); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill); + Assert.equal(gURLBar.value, "@__example "); + Assert.equal(gURLBar.selectionStart, "@__ex".length); + Assert.equal(gURLBar.selectionEnd, "@__example ".length); + + await searchAndCheck("@__exa", "@__example "); + await searchAndCheck("@__EXAM", "@__EXAMple "); + await searchAndCheck("@__eXaMp", "@__eXaMple "); + await searchAndCheck("@__exampl", "@__example "); + + await cleanUp(); +}); + +add_task(async function noMatch1() { + await PlacesTestUtils.addVisits("http://example.com/"); + + // Do an initial search that triggers autofill so that the placeholder has an + // initial value. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "ex", + fireInputEvent: true, + }); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill); + Assert.equal(gURLBar.value, "example.com/"); + Assert.equal(gURLBar.selectionStart, "ex".length); + Assert.equal(gURLBar.selectionEnd, "example.com/".length); + + // Search with a string that does not match the placeholder. Placeholder + // autofill shouldn't happen. + gURLBar.value = "moz"; + UrlbarTestUtils.fireInputEvent(window); + Assert.equal(gURLBar.value, "moz"); + Assert.equal(gURLBar.selectionStart, "moz".length); + Assert.equal(gURLBar.selectionEnd, "moz".length); + await UrlbarTestUtils.promiseSearchComplete(window); + + // Search for "ex" again. It should be autofilled. Placeholder autofill + // won't happen. It's not important for this test to check that. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "ex", + fireInputEvent: true, + }); + details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill); + Assert.equal(gURLBar.value, "example.com/"); + Assert.equal(gURLBar.selectionStart, "ex".length); + Assert.equal(gURLBar.selectionEnd, "example.com/".length); + + // Placeholder autofill should work again for example.com searches. + await searchAndCheck("exa", "example.com/"); + await searchAndCheck("EXAM", "EXAMple.com/"); + await searchAndCheck("eXaMp", "eXaMple.com/"); + await searchAndCheck("exampl", "example.com/"); + + await cleanUp(); +}); + +add_task(async function noMatch2() { + await PlacesTestUtils.addVisits([ + "http://mozilla.org/", + "http://example.com/", + ]); + + // Do an initial search that triggers autofill so that the placeholder has an + // initial value. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "moz", + fireInputEvent: true, + }); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill); + Assert.equal(gURLBar.value, "mozilla.org/"); + Assert.equal(gURLBar.selectionStart, "moz".length); + Assert.equal(gURLBar.selectionEnd, "mozilla.org/".length); + + // Search with a string that does not match the placeholder but does trigger + // autofill. Placeholder autofill shouldn't happen. + gURLBar.value = "ex"; + UrlbarTestUtils.fireInputEvent(window); + Assert.equal(gURLBar.value, "ex"); + Assert.equal(gURLBar.selectionStart, "ex".length); + Assert.equal(gURLBar.selectionEnd, "ex".length); + await UrlbarTestUtils.promiseSearchComplete(window); + Assert.equal(gURLBar.value, "example.com/"); + Assert.equal(gURLBar.selectionStart, "ex".length); + Assert.equal(gURLBar.selectionEnd, "example.com/".length); + + // Do some searches that should trigger placeholder autofill. + await searchAndCheck("exa", "example.com/"); + await searchAndCheck("EXAm", "EXAmple.com/"); + + // Search for "moz" again. It should be autofilled. Placeholder autofill + // shouldn't happen. + gURLBar.value = "moz"; + UrlbarTestUtils.fireInputEvent(window); + Assert.equal(gURLBar.value, "moz"); + Assert.equal(gURLBar.selectionStart, "moz".length); + Assert.equal(gURLBar.selectionEnd, "moz".length); + await UrlbarTestUtils.promiseSearchComplete(window); + Assert.equal(gURLBar.value, "mozilla.org/"); + Assert.equal(gURLBar.selectionStart, "moz".length); + Assert.equal(gURLBar.selectionEnd, "mozilla.org/".length); + + // Do some searches that should trigger placeholder autofill. + await searchAndCheck("mozi", "mozilla.org/"); + await searchAndCheck("MOZil", "MOZilla.org/"); + + await cleanUp(); +}); + +add_task(async function clear_placeholder_for_keyword_or_alias() { + info("Clear the autofill placeholder if a keyword is typed"); + await PlacesTestUtils.addVisits("http://example.com/"); + await PlacesUtils.keywords.insert({ + keyword: "ex", + url: "http://somekeyword.com/", + }); + let engine = await Services.search.addEngineWithDetails("AutofillTest", { + alias: "exam", + template: "http://example.com/?search={searchTerms}", + }); + registerCleanupFunction(async function() { + await PlacesUtils.keywords.remove("ex"); + await Services.search.removeEngine(engine); + }); + + // Do an initial search that triggers autofill so that the placeholder has an + // initial value. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "e", + fireInputEvent: true, + }); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill); + Assert.equal(gURLBar.value, "example.com/"); + Assert.equal(gURLBar.selectionStart, "e".length); + Assert.equal(gURLBar.selectionEnd, "example.com/".length); + + // The values are initially autofilled on input, then the placeholder is + // removed when the first non-autofill result arrives. + + // Matches the keyword. + await searchAndCheck("ex", "example.com/", "ex"); + await searchAndCheck("EXA", "EXAmple.com/", "EXAmple.com/"); + // Matches the alias. + + await searchAndCheck("eXaM", "eXaMple.com/", "eXaMple.com/"); + await searchAndCheck("examp", "example.com/", "example.com/"); + + await cleanUp(); +}); + +async function searchAndCheck( + searchString, + expectedAutofillValue, + onCompleteValue = "" +) { + gURLBar.value = searchString; + + // Placeholder autofill is done on input, so fire an input event. As the + // comment at the top of this file says, we can't use + // promiseAutocompleteResultPopup() or other helpers that wait for searches to + // complete because we are specifically checking autofill before the search + // completes. + UrlbarTestUtils.fireInputEvent(window); + + // Check the input value and selection immediately, before waiting on the + // search to complete. + Assert.equal(gURLBar.value, expectedAutofillValue); + Assert.equal(gURLBar.selectionStart, searchString.length); + Assert.equal(gURLBar.selectionEnd, expectedAutofillValue.length); + + await UrlbarTestUtils.promiseSearchComplete(window); + + if (onCompleteValue) { + // Check the final value after the results arrived. + Assert.equal(gURLBar.value, onCompleteValue); + Assert.equal(gURLBar.selectionStart, searchString.length); + Assert.equal(gURLBar.selectionEnd, onCompleteValue.length); + } +} + +async function cleanUp() { + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_Escape"); + }); + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); +} diff --git a/browser/components/urlbar/tests/browser/browser_autoFill_preserve.js b/browser/components/urlbar/tests/browser/browser_autoFill_preserve.js new file mode 100644 index 0000000000..bfd7f6e20e --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autoFill_preserve.js @@ -0,0 +1,260 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This test makes sure that a few of aspects of autofill are correctly +// preserved: +// +// * Autofill should preserve the user's case. If you type ExA, it should be +// autofilled to ExAmple.com/, not example.com/. +// * When you key down and then back up to the autofill result, autofill should +// be restored, with the text selection and the user's case both preserved. +// * When you key down/up so that no result is selected, the value that the +// user typed to trigger autofill should be restored in the input. + +"use strict"; + +add_task(async function init() { + await cleanUp(); +}); + +add_task(async function origin() { + await PlacesTestUtils.addVisits([ + "http://example.com/", + "http://mozilla.org/example", + ]); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "ExA", + fireInputEvent: true, + }); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill); + Assert.equal(gURLBar.value, "ExAmple.com/"); + Assert.equal(gURLBar.selectionStart, "ExA".length); + Assert.equal(gURLBar.selectionEnd, "ExAmple.com/".length); + checkKeys([ + ["KEY_ArrowDown", "http://mozilla.org/example", 1], + ["KEY_ArrowDown", "ExA", -1], + ["KEY_ArrowUp", "http://mozilla.org/example", 1], + ["KEY_ArrowUp", "ExAmple.com/", 0], + ["KEY_ArrowUp", "ExA", -1], + ["KEY_ArrowDown", "ExAmple.com/", 0], + ]); + await cleanUp(); +}); + +add_task(async function originPort() { + await PlacesTestUtils.addVisits([ + "http://example.com:8888/", + "http://mozilla.org/example", + ]); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "ExA", + fireInputEvent: true, + }); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill); + Assert.equal(gURLBar.value, "ExAmple.com:8888/"); + Assert.equal(gURLBar.selectionStart, "ExA".length); + Assert.equal(gURLBar.selectionEnd, "ExAmple.com:8888/".length); + checkKeys([ + ["KEY_ArrowDown", "http://mozilla.org/example", 1], + ["KEY_ArrowDown", "ExA", -1], + ["KEY_ArrowUp", "http://mozilla.org/example", 1], + ["KEY_ArrowUp", "ExAmple.com:8888/", 0], + ["KEY_ArrowUp", "ExA", -1], + ["KEY_ArrowDown", "ExAmple.com:8888/", 0], + ]); + await cleanUp(); +}); + +add_task(async function originScheme() { + await PlacesTestUtils.addVisits([ + "http://example.com/", + "http://mozilla.org/example", + ]); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "http://ExA", + fireInputEvent: true, + }); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill); + Assert.equal(gURLBar.value, "http://ExAmple.com/"); + Assert.equal(gURLBar.selectionStart, "http://ExA".length); + Assert.equal(gURLBar.selectionEnd, "http://ExAmple.com/".length); + checkKeys([ + ["KEY_ArrowDown", "http://mozilla.org/example", 1], + ["KEY_ArrowDown", "http://ExA", -1], + ["KEY_ArrowUp", "http://mozilla.org/example", 1], + ["KEY_ArrowUp", "http://ExAmple.com/", 0], + ["KEY_ArrowUp", "http://ExA", -1], + ["KEY_ArrowDown", "http://ExAmple.com/", 0], + ]); + await cleanUp(); +}); + +add_task(async function originPortScheme() { + await PlacesTestUtils.addVisits([ + "http://example.com:8888/", + "http://mozilla.org/example", + ]); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "http://ExA", + fireInputEvents: true, + }); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill); + Assert.equal(gURLBar.value, "http://ExAmple.com:8888/"); + Assert.equal(gURLBar.selectionStart, "http://ExA".length); + Assert.equal(gURLBar.selectionEnd, "http://ExAmple.com:8888/".length); + checkKeys([ + ["KEY_ArrowDown", "http://mozilla.org/example", 1], + ["KEY_ArrowDown", "http://ExA", -1], + ["KEY_ArrowUp", "http://mozilla.org/example", 1], + ["KEY_ArrowUp", "http://ExAmple.com:8888/", 0], + ["KEY_ArrowUp", "http://ExA", -1], + ["KEY_ArrowDown", "http://ExAmple.com:8888/", 0], + ]); + await cleanUp(); +}); + +add_task(async function url() { + await PlacesTestUtils.addVisits([ + "http://example.com/foo", + "http://example.com/foo", + "http://example.com/fff", + ]); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "ExAmple.com/f", + fireInputEvent: true, + }); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill); + Assert.equal(gURLBar.value, "ExAmple.com/foo"); + Assert.equal(gURLBar.selectionStart, "ExAmple.com/f".length); + Assert.equal(gURLBar.selectionEnd, "ExAmple.com/foo".length); + checkKeys([ + ["KEY_ArrowDown", "http://example.com/fff", 1], + ["KEY_ArrowDown", "ExAmple.com/f", -1], + ["KEY_ArrowUp", "http://example.com/fff", 1], + ["KEY_ArrowUp", "ExAmple.com/foo", 0], + ["KEY_ArrowUp", "ExAmple.com/f", -1], + ["KEY_ArrowDown", "ExAmple.com/foo", 0], + ]); + await cleanUp(); +}); + +add_task(async function urlPort() { + await PlacesTestUtils.addVisits([ + "http://example.com:8888/foo", + "http://example.com:8888/foo", + "http://example.com:8888/fff", + ]); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "ExAmple.com:8888/f", + fireInputEvent: true, + }); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill); + Assert.equal(gURLBar.value, "ExAmple.com:8888/foo"); + Assert.equal(gURLBar.selectionStart, "ExAmple.com:8888/f".length); + Assert.equal(gURLBar.selectionEnd, "ExAmple.com:8888/foo".length); + checkKeys([ + ["KEY_ArrowDown", "http://example.com:8888/fff", 1], + ["KEY_ArrowDown", "ExAmple.com:8888/f", -1], + ["KEY_ArrowUp", "http://example.com:8888/fff", 1], + ["KEY_ArrowUp", "ExAmple.com:8888/foo", 0], + ["KEY_ArrowUp", "ExAmple.com:8888/f", -1], + ["KEY_ArrowDown", "ExAmple.com:8888/foo", 0], + ]); + await cleanUp(); +}); + +add_task(async function tokenAlias() { + await Services.search.addEngineWithDetails("Test", { + alias: "@example", + template: "http://example.com/?search={searchTerms}", + }); + registerCleanupFunction(async function() { + let engine = Services.search.getEngineByName("Test"); + await Services.search.removeEngine(engine); + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "@ExA", + fireInputEvent: true, + }); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill); + Assert.equal(gURLBar.value, "@ExAmple "); + Assert.equal(gURLBar.selectionStart, "@ExA".length); + Assert.equal(gURLBar.selectionEnd, "@ExAmple ".length); + // Token aliases (1) hide the one-off buttons and (2) show only a single + // result, the "Search with" result for the alias's engine, so there's no way + // to key up/down to change the selection, so this task doesn't check key + // presses like the others do. + await cleanUp(); +}); + +// This test is a little different from the others. It backspaces over the +// autofilled substring and checks that autofill is *not* preserved. +add_task(async function backspaceNoAutofill() { + await PlacesTestUtils.addVisits([ + "http://example.com/", + "http://example.com/", + "http://mozilla.org/example", + ]); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "ExA", + fireInputEvent: true, + }); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill); + Assert.equal(gURLBar.value, "ExAmple.com/"); + Assert.equal(gURLBar.selectionStart, "ExA".length); + Assert.equal(gURLBar.selectionEnd, "ExAmple.com/".length); + + EventUtils.synthesizeKey("KEY_Backspace"); + await UrlbarTestUtils.promiseSearchComplete(window); + details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(!details.autofill); + Assert.equal(gURLBar.value, "ExA"); + Assert.equal(gURLBar.selectionStart, "ExA".length); + Assert.equal(gURLBar.selectionEnd, "ExA".length); + + let heuristicValue = "ExA"; + + checkKeys([ + ["KEY_ArrowDown", "http://example.com/", 1], + ["KEY_ArrowDown", "http://mozilla.org/example", 2], + ["KEY_ArrowDown", "ExA", -1], + ["KEY_ArrowUp", "http://mozilla.org/example", 2], + ["KEY_ArrowUp", "http://example.com/", 1], + ["KEY_ArrowUp", heuristicValue, 0], + ["KEY_ArrowUp", "ExA", -1], + ["KEY_ArrowDown", heuristicValue, 0], + ]); + + await cleanUp(); +}); + +function checkKeys(testTuples) { + for (let [key, value, selectedIndex] of testTuples) { + EventUtils.synthesizeKey(key); + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), selectedIndex); + Assert.equal(gURLBar.untrimmedValue, value); + } +} + +async function cleanUp() { + EventUtils.synthesizeKey("KEY_Escape"); + await UrlbarTestUtils.promisePopupClose(window); + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); +} diff --git a/browser/components/urlbar/tests/browser/browser_autoFill_trimURLs.js b/browser/components/urlbar/tests/browser/browser_autoFill_trimURLs.js new file mode 100644 index 0000000000..1d4b805681 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autoFill_trimURLs.js @@ -0,0 +1,183 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This test ensures that autoFilled values are not trimmed, unless the user +// selects from the autocomplete popup. + +"use strict"; + +add_task(async function setup() { + const PREF_TRIMURL = "browser.urlbar.trimURLs"; + const PREF_AUTOFILL = "browser.urlbar.autoFill"; + + registerCleanupFunction(async function() { + Services.prefs.clearUserPref(PREF_TRIMURL); + Services.prefs.clearUserPref(PREF_AUTOFILL); + await PlacesUtils.history.clear(); + gURLBar.handleRevert(); + }); + Services.prefs.setBoolPref(PREF_TRIMURL, true); + Services.prefs.setBoolPref(PREF_AUTOFILL, true); + + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); + + // Adding a tab would hit switch-to-tab, so it's safer to just add a visit. + await PlacesTestUtils.addVisits([ + { + uri: "http://www.autofilltrimurl.com/whatever", + }, + { + uri: "https://www.secureautofillurl.com/whatever", + }, + ]); +}); + +async function promiseSearch(searchtext) { + gURLBar.focus(); + gURLBar.inputField.value = searchtext.substr(0, searchtext.length - 1); + EventUtils.sendString(searchtext.substr(-1, 1)); + await UrlbarTestUtils.promiseSearchComplete(window); +} + +async function promiseTestResult(test) { + info(`Searching for '${test.search}'`); + + await promiseSearch(test.search); + + Assert.equal( + gURLBar.inputField.value, + test.autofilledValue, + `Autofilled value is as expected for search '${test.search}'` + ); + + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + + Assert.equal( + result.displayed.title, + test.resultListDisplayTitle, + `Autocomplete result should have displayed title as expected for search '${test.search}'` + ); + + Assert.equal( + result.displayed.action, + test.resultListActionText, + `Autocomplete action text should be as expected for search '${test.search}'` + ); + + Assert.equal( + result.type, + test.resultListType, + `Autocomplete result should have searchengine for the type for search '${test.search}'` + ); + + Assert.equal( + !!result.searchParams, + !!test.searchParams, + "Should have search params if expected" + ); + if (test.searchParams) { + let definedParams = {}; + for (let [k, v] of Object.entries(result.searchParams)) { + if (v !== undefined) { + definedParams[k] = v; + } + } + Assert.deepEqual( + definedParams, + test.searchParams, + "Shoud have the correct search params" + ); + } else { + Assert.equal( + result.url, + test.finalCompleteValue, + "Should have the correct URL/finalCompleteValue" + ); + } +} + +const tests = [ + { + search: "http://", + autofilledValue: "http://", + resultListDisplayTitle: "http://", + resultListActionText: "Search with Google", + resultListType: UrlbarUtils.RESULT_TYPE.SEARCH, + searchParams: { + engine: "Google", + query: "http://", + }, + }, + { + search: "https://", + autofilledValue: "https://", + resultListDisplayTitle: "https://", + resultListActionText: "Search with Google", + resultListType: UrlbarUtils.RESULT_TYPE.SEARCH, + searchParams: { + engine: "Google", + query: "https://", + }, + }, + { + search: "au", + autofilledValue: "autofilltrimurl.com/", + resultListDisplayTitle: "www.autofilltrimurl.com", + resultListActionText: "Visit", + resultListType: UrlbarUtils.RESULT_TYPE.URL, + finalCompleteValue: "http://www.autofilltrimurl.com/", + }, + { + search: "http://au", + autofilledValue: "http://autofilltrimurl.com/", + resultListDisplayTitle: "www.autofilltrimurl.com", + resultListActionText: "Visit", + resultListType: UrlbarUtils.RESULT_TYPE.URL, + finalCompleteValue: "http://www.autofilltrimurl.com/", + }, + { + search: "sec", + autofilledValue: "secureautofillurl.com/", + resultListDisplayTitle: "https://www.secureautofillurl.com", + resultListActionText: "Visit", + resultListType: UrlbarUtils.RESULT_TYPE.URL, + finalCompleteValue: "https://www.secureautofillurl.com/", + }, + { + search: "https://sec", + autofilledValue: "https://secureautofillurl.com/", + resultListDisplayTitle: "https://www.secureautofillurl.com", + resultListActionText: "Visit", + resultListType: UrlbarUtils.RESULT_TYPE.URL, + finalCompleteValue: "https://www.secureautofillurl.com/", + }, +]; + +add_task(async function autofill_tests() { + for (let test of tests) { + await promiseTestResult(test); + } +}); + +add_task(async function autofill_complete_domain() { + await promiseSearch("http://www.autofilltrimurl.com"); + Assert.equal( + gURLBar.inputField.value, + "http://www.autofilltrimurl.com/", + "Should have the correct autofill value" + ); + + // Now ensure selecting from the popup correctly trims. + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 2, + "Should have the correct matches" + ); + EventUtils.synthesizeKey("KEY_ArrowDown"); + Assert.equal( + gURLBar.inputField.value, + "www.autofilltrimurl.com/whatever", + "Should have applied trim correctly" + ); +}); diff --git a/browser/components/urlbar/tests/browser/browser_autoFill_typed.js b/browser/components/urlbar/tests/browser/browser_autoFill_typed.js new file mode 100644 index 0000000000..03be7d5424 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autoFill_typed.js @@ -0,0 +1,179 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This test makes sure that autofill works as expected when typing, character +// by character. + +"use strict"; + +add_task(async function init() { + await cleanUp(); +}); + +add_task(async function origin() { + await PlacesTestUtils.addVisits(["http://example.com/"]); + // all lowercase + await typeAndCheck([ + ["e", "example.com/"], + ["x", "example.com/"], + ["a", "example.com/"], + ["m", "example.com/"], + ["p", "example.com/"], + ["l", "example.com/"], + ["e", "example.com/"], + [".", "example.com/"], + ["c", "example.com/"], + ["o", "example.com/"], + ["m", "example.com/"], + ["/", "example.com/"], + ]); + gURLBar.value = ""; + // mixed case + await typeAndCheck([ + ["E", "Example.com/"], + ["x", "Example.com/"], + ["A", "ExAmple.com/"], + ["m", "ExAmple.com/"], + ["P", "ExAmPle.com/"], + ["L", "ExAmPLe.com/"], + ["e", "ExAmPLe.com/"], + [".", "ExAmPLe.com/"], + ["C", "ExAmPLe.Com/"], + ["o", "ExAmPLe.Com/"], + ["M", "ExAmPLe.CoM/"], + ["/", "ExAmPLe.CoM/"], + ]); + await cleanUp(); +}); + +add_task(async function url() { + await PlacesTestUtils.addVisits(["http://example.com/foo/bar"]); + // all lowercase + await typeAndCheck([ + ["e", "example.com/"], + ["x", "example.com/"], + ["a", "example.com/"], + ["m", "example.com/"], + ["p", "example.com/"], + ["l", "example.com/"], + ["e", "example.com/"], + [".", "example.com/"], + ["c", "example.com/"], + ["o", "example.com/"], + ["m", "example.com/"], + ["/", "example.com/"], + ["f", "example.com/foo/"], + ["o", "example.com/foo/"], + ["o", "example.com/foo/"], + ["/", "example.com/foo/"], + ["b", "example.com/foo/bar"], + ["a", "example.com/foo/bar"], + ["r", "example.com/foo/bar"], + ]); + gURLBar.value = ""; + // mixed case + await typeAndCheck([ + ["E", "Example.com/"], + ["x", "Example.com/"], + ["A", "ExAmple.com/"], + ["m", "ExAmple.com/"], + ["P", "ExAmPle.com/"], + ["L", "ExAmPLe.com/"], + ["e", "ExAmPLe.com/"], + [".", "ExAmPLe.com/"], + ["C", "ExAmPLe.Com/"], + ["o", "ExAmPLe.Com/"], + ["M", "ExAmPLe.CoM/"], + ["/", "ExAmPLe.CoM/"], + ["f", "ExAmPLe.CoM/foo/"], + ["o", "ExAmPLe.CoM/foo/"], + ["o", "ExAmPLe.CoM/foo/"], + ["/", "ExAmPLe.CoM/foo/"], + ["b", "ExAmPLe.CoM/foo/bar"], + ["a", "ExAmPLe.CoM/foo/bar"], + ["r", "ExAmPLe.CoM/foo/bar"], + ]); + await cleanUp(); +}); + +add_task(async function tokenAlias() { + // We have built-in engine aliases that may conflict with the one we choose + // here in terms of autofill, so be careful and choose a weird alias. + await Services.search.addEngineWithDetails("Test", { + alias: "@__example", + template: "http://example.com/?search={searchTerms}", + }); + registerCleanupFunction(async function() { + let engine = Services.search.getEngineByName("Test"); + await Services.search.removeEngine(engine); + }); + // all lowercase + await typeAndCheck([ + ["@", "@"], + ["_", "@__example "], + ["_", "@__example "], + ["e", "@__example "], + ["x", "@__example "], + ["a", "@__example "], + ["m", "@__example "], + ["p", "@__example "], + ["l", "@__example "], + ["e", "@__example "], + ]); + gURLBar.value = ""; + // mixed case + await typeAndCheck([ + ["@", "@"], + ["_", "@__example "], + ["_", "@__example "], + ["E", "@__Example "], + ["x", "@__Example "], + ["A", "@__ExAmple "], + ["m", "@__ExAmple "], + ["P", "@__ExAmPle "], + ["L", "@__ExAmPLe "], + ["e", "@__ExAmPLe "], + ]); + await cleanUp(); +}); + +async function typeAndCheck(values) { + gURLBar.focus(); + for (let i = 0; i < values.length; i++) { + let [char, expectedInputValue] = values[i]; + info( + `Typing: i=${i} char=${char} ` + + `substring="${expectedInputValue.substring(0, i + 1)}"` + ); + EventUtils.synthesizeKey(char); + if (i == 0 && char == "@") { + // A single "@" doesn't trigger autofill, so skip the checks below. (It + // shows all the @ aliases.) + continue; + } + await UrlbarTestUtils.promiseSearchComplete(window); + let restIsSpaces = !expectedInputValue.substring(i + 1).trim(); + Assert.equal(gURLBar.value, expectedInputValue); + Assert.equal(gURLBar.selectionStart, i + 1); + Assert.equal(gURLBar.selectionEnd, expectedInputValue.length); + if (restIsSpaces) { + // Autofilled @ aliases have a trailing space. We should check that the + // space is autofilled when each preceding character is typed, but once + // the final non-space char is typed, autofill actually stops and the + // trailing space is not autofilled. (Which is maybe not the way it + // should work...) Skip the check below. + continue; + } + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill); + } +} + +async function cleanUp() { + gURLBar.value = ""; + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_Escape"); + }); + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); +} diff --git a/browser/components/urlbar/tests/browser/browser_autoFill_undo.js b/browser/components/urlbar/tests/browser/browser_autoFill_undo.js new file mode 100644 index 0000000000..c233da80f2 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autoFill_undo.js @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This test checks the behavior of text undo (Ctrl-Z, cmd_undo) in regard to +// autofill. + +"use strict"; + +add_task(async function test() { + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); + await PlacesTestUtils.addVisits(["http://example.com/"]); + + // Search for "ex". It should autofill to example.com/. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "ex", + fireInputEvent: true, + }); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill); + Assert.equal(gURLBar.value, "example.com/"); + Assert.equal(gURLBar.selectionStart, "ex".length); + Assert.equal(gURLBar.selectionEnd, "example.com/".length); + + // Type an x. + EventUtils.synthesizeKey("x"); + + // Nothing should have been autofilled. + await UrlbarTestUtils.promiseSearchComplete(window); + details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(!details.autofill); + Assert.equal(gURLBar.value, "exx"); + Assert.equal(gURLBar.selectionStart, "exx".length); + Assert.equal(gURLBar.selectionEnd, "exx".length); + + // Undo the typed x. + goDoCommand("cmd_undo"); + + // The text should be restored to ex[ample.com/] (with the part in brackets + // autofilled and selected). + await UrlbarTestUtils.promiseSearchComplete(window); + details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal(gURLBar.value, "example.com/"); + Assert.ok(!details.autofill, "Autofill should not be set."); + Assert.equal(gURLBar.selectionStart, "ex".length); + Assert.equal(gURLBar.selectionEnd, "example.com/".length); + + await PlacesUtils.history.clear(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_autoOpen.js b/browser/components/urlbar/tests/browser/browser_autoOpen.js new file mode 100644 index 0000000000..bd43e52565 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autoOpen.js @@ -0,0 +1,93 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +async function checkOpensOnFocus(win = window) { + // The view should not open when the input is focused programmatically. + win.gURLBar.blur(); + win.gURLBar.focus(); + Assert.ok(!win.gURLBar.view.isOpen, "check urlbar panel is not open"); + win.gURLBar.blur(); + + // Check the keyboard shortcut. + await UrlbarTestUtils.promisePopupOpen(win, () => { + win.document.getElementById("Browser:OpenLocation").doCommand(); + }); + await UrlbarTestUtils.promisePopupClose(win, () => { + win.gURLBar.blur(); + }); + + // Focus with the mouse. + await UrlbarTestUtils.promisePopupOpen(win, () => { + EventUtils.synthesizeMouseAtCenter(win.gURLBar.inputField, {}, win); + }); + await UrlbarTestUtils.promisePopupClose(win, () => { + win.gURLBar.blur(); + }); +} + +add_task(async function setUp() { + // Add some history for the empty panel. + await PlacesTestUtils.addVisits([ + { + uri: "http://mochi.test:8888/", + transition: PlacesUtils.history.TRANSITIONS.TYPED, + }, + ]); + registerCleanupFunction(() => PlacesUtils.history.clear()); +}); + +add_task(async function test() { + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:blank" }, + async browser => { + await checkOpensOnFocus(); + } + ); +}); + +add_task(async function newtabAndHome() { + for (let url of ["about:newtab", "about:home"]) { + // withNewTab randomly hangs on these pages when waitForLoad = true (the + // default), so pass false. + await BrowserTestUtils.withNewTab( + { gBrowser, url, waitForLoad: false }, + async browser => { + // We don't wait for load, but we must ensure to be on the expected url. + await TestUtils.waitForCondition( + () => gBrowser.currentURI.spec == url, + "Ensure we're on the expected page" + ); + await checkOpensOnFocus(); + await BrowserTestUtils.withNewTab( + { gBrowser, url: "http://example.com/" }, + async otherBrowser => { + await checkOpensOnFocus(); + // Switch back to about:newtab/home. + await BrowserTestUtils.switchTab( + gBrowser, + gBrowser.getTabForBrowser(browser) + ); + await checkOpensOnFocus(); + // Switch back to example.com. + await BrowserTestUtils.switchTab( + gBrowser, + gBrowser.getTabForBrowser(otherBrowser) + ); + await checkOpensOnFocus(); + } + ); + // After example.com closes, about:newtab/home is selected again. + await checkOpensOnFocus(); + // Load example.com in the same tab. + BrowserTestUtils.loadURI( + gBrowser.selectedBrowser, + "http://example.com/" + ); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + await checkOpensOnFocus(); + } + ); + } +}); diff --git a/browser/components/urlbar/tests/browser/browser_autocomplete_a11y_label.js b/browser/components/urlbar/tests/browser/browser_autocomplete_a11y_label.js new file mode 100644 index 0000000000..0f8652248b --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autocomplete_a11y_label.js @@ -0,0 +1,155 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This test ensures that we produce good labels for a11y purposes. + */ + +const SUGGEST_ALL_PREF = "browser.search.suggest.enabled"; +const SUGGEST_URLBAR_PREF = "browser.urlbar.suggest.searches"; +const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml"; + +async function getResultText(element) { + await initAccessibilityService(); + await BrowserTestUtils.waitForCondition(() => + accService.getAccessibleFor(element) + ); + let accessible = accService.getAccessibleFor(element); + return accessible.name; +} + +let accService; +async function initAccessibilityService() { + if (accService) { + return; + } + accService = Cc["@mozilla.org/accessibilityService;1"].getService( + Ci.nsIAccessibilityService + ); + if (Services.appinfo.accessibilityEnabled) { + return; + } + + async function promiseInitOrShutdown(init = true) { + await new Promise(resolve => { + let observe = (subject, topic, data) => { + Services.obs.removeObserver(observe, "a11y-init-or-shutdown"); + // "1" indicates that the accessibility service is initialized. + if (data === (init ? "1" : "0")) { + resolve(); + } + }; + Services.obs.addObserver(observe, "a11y-init-or-shutdown"); + }); + } + await promiseInitOrShutdown(true); + registerCleanupFunction(async () => { + accService = null; + await promiseInitOrShutdown(false); + }); +} + +add_task(async function switchToTab() { + let tab = BrowserTestUtils.addTab(gBrowser, "about:robots"); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "% robots", + }); + + let index = 0; + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, index); + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.TAB_SWITCH, + "Should have a switch tab result" + ); + + let element = await UrlbarTestUtils.waitForAutocompleteResultAt( + window, + index + ); + is( + await getResultText(element), + "about: robots— Switch to Tab", + "Result a11y label should be: <title>— Switch to Tab" + ); + + await UrlbarTestUtils.promisePopupClose(window); + gURLBar.handleRevert(); + gBrowser.removeTab(tab); +}); + +add_task(async function searchSuggestions() { + let engine = await SearchTestUtils.promiseNewSearchEngine( + getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME + ); + let oldDefaultEngine = await Services.search.getDefault(); + await Services.search.setDefault(engine); + Services.prefs.setBoolPref(SUGGEST_ALL_PREF, true); + let suggestionsEnabled = Services.prefs.getBoolPref(SUGGEST_URLBAR_PREF); + Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, true); + registerCleanupFunction(async function() { + await Services.search.setDefault(oldDefaultEngine); + Services.prefs.clearUserPref(SUGGEST_ALL_PREF); + Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, suggestionsEnabled); + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "foo", + }); + let length = await UrlbarTestUtils.getResultCount(window); + // Don't assume that the search doesn't match history or bookmarks left around + // by earlier tests. + Assert.greaterOrEqual( + length, + 3, + "Should get at least heuristic result + two search suggestions" + ); + // The first expected search is the search term itself since the heuristic + // result will come before the search suggestions. + // The extra spaces are here due to bug 1550644. + let searchTerm = "foo "; + let expectedSearches = [searchTerm, "foo foo", "foo bar"]; + for (let i = 0; i < length; i++) { + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + if (result.type === UrlbarUtils.RESULT_TYPE.SEARCH) { + Assert.greaterOrEqual( + expectedSearches.length, + 0, + "Should still have expected searches remaining" + ); + + let element = await UrlbarTestUtils.waitForAutocompleteResultAt( + window, + i + ); + let selected = element.hasAttribute("selected"); + if (!selected) { + // Simulate the result being selected so we see the expanded text. + element.toggleAttribute("selected", true); + } + if (result.searchParams.inPrivateWindow) { + Assert.equal( + await getResultText(element), + searchTerm + "— Search in a Private Window", + "Check result label" + ); + } else { + let suggestion = expectedSearches.shift(); + Assert.equal( + await getResultText(element), + suggestion + + "— Search with browser_searchSuggestionEngine searchSuggestionEngine.xml", + "Check result label" + ); + } + if (!selected) { + element.toggleAttribute("selected", false); + } + } + } + Assert.ok(!expectedSearches.length); +}); diff --git a/browser/components/urlbar/tests/browser/browser_autocomplete_autoselect.js b/browser/components/urlbar/tests/browser/browser_autocomplete_autoselect.js new file mode 100644 index 0000000000..750cbb01a9 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autocomplete_autoselect.js @@ -0,0 +1,128 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that the first item is correctly autoselected and some navigation + * around the results list. + */ + +function repeat(limit, func) { + for (let i = 0; i < limit; i++) { + func(i); + } +} + +function assertSelected(index) { + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + index, + "Should have selected the correct item" + ); + // Also check the "selected" attribute, to ensure it is not a "fake" selection + // due to binding misbehaviors. + let element = UrlbarTestUtils.getSelectedRow(window); + Assert.ok( + element.hasAttribute("selected"), + "Should have the selected attribute on the row element" + ); + + // This is true because although both the listbox and the one-offs can have + // selections, the test doesn't check that. + Assert.equal( + UrlbarTestUtils.getOneOffSearchButtons(window).selectedButton, + null, + "A result is selected, so the one-offs should not have a selection" + ); +} + +function assertSelected_one_off(index) { + Assert.equal( + UrlbarTestUtils.getOneOffSearchButtons(window).selectedButtonIndex, + index, + "Expected one-off button should be selected" + ); + + // This is true because although both the listbox and the one-offs can have + // selections, the test doesn't check that. + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + -1, + "A one-off is selected, so the listbox should not have a selection" + ); +} + +add_task(async function() { + let maxResults = Services.prefs.getIntPref("browser.urlbar.maxRichResults"); + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:mozilla" + ); + registerCleanupFunction(async function() { + await PlacesUtils.history.clear(); + BrowserTestUtils.removeTab(tab); + }); + + let visits = []; + repeat(maxResults, i => { + visits.push({ + uri: makeURI("http://example.com/autocomplete/?" + i), + }); + }); + await PlacesTestUtils.addVisits(visits); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "example.com/autocomplete", + fireInputEvent: true, + }); + + let resultCount = await UrlbarTestUtils.getResultCount(window); + + Assert.equal( + resultCount, + maxResults, + "Should get the expected amount of results" + ); + assertSelected(0); + + info("Key Down to select the next item"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + assertSelected(1); + + info("Key Down maxResults-1 times should select the first one-off"); + repeat(maxResults - 1, () => EventUtils.synthesizeKey("KEY_ArrowDown")); + assertSelected_one_off(0); + + info("Key Down numButtons-1 should select the last one-off"); + let numButtons = UrlbarTestUtils.getOneOffSearchButtons( + window + ).getSelectableButtons(true).length; + repeat(numButtons - 1, () => EventUtils.synthesizeKey("KEY_ArrowDown")); + assertSelected_one_off(numButtons - 1); + + info("Key Down twice more should select the second result"); + repeat(2, () => EventUtils.synthesizeKey("KEY_ArrowDown")); + assertSelected(1); + + info("Key Down maxResults + numButtons times should wrap around"); + repeat(maxResults + numButtons, () => + EventUtils.synthesizeKey("KEY_ArrowDown") + ); + assertSelected(1); + + info("Key Up maxResults + numButtons times should wrap around the other way"); + repeat(maxResults + numButtons, () => + EventUtils.synthesizeKey("KEY_ArrowUp") + ); + assertSelected(1); + + info("Page Up will go up the list, but not wrap"); + EventUtils.synthesizeKey("KEY_PageUp"); + assertSelected(0); + + info("Page Up again will wrap around to the end of the list"); + EventUtils.synthesizeKey("KEY_PageUp"); + assertSelected(maxResults - 1); +}); diff --git a/browser/components/urlbar/tests/browser/browser_autocomplete_cursor.js b/browser/components/urlbar/tests/browser/browser_autocomplete_cursor.js new file mode 100644 index 0000000000..5e0081a92c --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autocomplete_cursor.js @@ -0,0 +1,37 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests the cursor remains in the right place when a new window is opened. + */ + +add_task(async function test_windowSwitch() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:mozilla" + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "www.mozilla.org", + }); + await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0); + + gURLBar.focus(); + gURLBar.inputField.setSelectionRange(4, 4); + + let newWindow = await BrowserTestUtils.openNewBrowserWindow(); + + await BrowserTestUtils.closeWindow(newWindow); + + Assert.equal( + document.activeElement, + gURLBar.inputField, + "URL Bar should be focused" + ); + Assert.equal(gURLBar.selectionStart, 4, "Should not have moved the cursor"); + Assert.equal(gURLBar.selectionEnd, 4, "Should not have selected anything"); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_autocomplete_edit_completed.js b/browser/components/urlbar/tests/browser/browser_autocomplete_edit_completed.js new file mode 100644 index 0000000000..51e1972dcc --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autocomplete_edit_completed.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests selecting a result, and editing the value of that autocompleted result. + */ + +add_task(async function() { + await PlacesUtils.history.clear(); + + await PlacesTestUtils.addVisits([ + { uri: makeURI("http://example.com/foo") }, + { uri: makeURI("http://example.com/foo/bar") }, + ]); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + registerCleanupFunction(async function() { + BrowserTestUtils.removeTab(tab); + await PlacesUtils.history.clear(); + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "http://example.com", + }); + + const initialIndex = UrlbarTestUtils.getSelectedRowIndex(window); + + info("Key Down to select the next item."); + EventUtils.synthesizeKey("KEY_ArrowDown"); + + let nextIndex = initialIndex + 1; + let nextResult = await UrlbarTestUtils.getDetailsOfResultAt( + window, + nextIndex + ); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + nextIndex, + "Should have selected the next item" + ); + Assert.equal( + gURLBar.untrimmedValue, + nextResult.url, + "Should have completed the URL" + ); + + info("Press backspace"); + EventUtils.synthesizeKey("KEY_Backspace"); + await UrlbarTestUtils.promiseSearchComplete(window); + + let editedValue = gURLBar.value; + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + initialIndex, + "Should have selected the initialIndex again" + ); + Assert.notEqual(editedValue, nextResult.url, "The URL has changed."); + + let docLoad = BrowserTestUtils.waitForDocLoadAndStopIt( + "http://" + editedValue, + gBrowser.selectedBrowser + ); + + info("Press return to load edited URL."); + + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_Enter"); + }); + + await docLoad; +}); diff --git a/browser/components/urlbar/tests/browser/browser_autocomplete_enter_race.js b/browser/components/urlbar/tests/browser/browser_autocomplete_enter_race.js new file mode 100644 index 0000000000..7fda51305a --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autocomplete_enter_race.js @@ -0,0 +1,178 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests what happens when the enter key is pressed quickly after entering text. + */ + +// The order of these tests matters! + +add_task(async function setup() { + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.com/?q=%s", + title: "test", + }); + registerCleanupFunction(async function() { + await PlacesUtils.bookmarks.remove(bm); + }); + await PlacesUtils.keywords.insert({ + keyword: "keyword", + url: "http://example.com/?q=%s", + }); + // Needs at least one success. + ok(true, "Setup complete"); +}); + +add_task( + taskWithNewTab(async function test_keyword() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "keyword bear", + }); + gURLBar.focus(); + EventUtils.sendString("d"); + EventUtils.synthesizeKey("KEY_Enter"); + info("wait for the page to load"); + await BrowserTestUtils.browserLoaded( + gBrowser.selectedTab.linkedBrowser, + false, + "http://example.com/?q=beard" + ); + }) +); + +add_task( + taskWithNewTab(async function test_sametext() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "example.com", + fireInputEvent: true, + }); + + // Simulate re-entering the same text searched the last time. This may happen + // through a copy paste, but clipboard handling is not much reliable, so just + // fire an input event. + info("synthesize input event"); + let event = document.createEvent("Events"); + event.initEvent("input", true, true); + gURLBar.inputField.dispatchEvent(event); + EventUtils.synthesizeKey("KEY_Enter"); + + info("wait for the page to load"); + await BrowserTestUtils.browserLoaded( + gBrowser.selectedTab.linkedBrowser, + false, + "http://example.com/" + ); + }) +); + +add_task( + taskWithNewTab(async function test_after_empty_search() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + gURLBar.focus(); + gURLBar.value = "e"; + EventUtils.synthesizeKey("x"); + EventUtils.synthesizeKey("KEY_Enter"); + + info("wait for the page to load"); + await BrowserTestUtils.browserLoaded( + gBrowser.selectedTab.linkedBrowser, + false, + "http://example.com/" + ); + }) +); + +add_task( + taskWithNewTab(async function test_disabled_ac() { + // Disable autocomplete. + let suggestHistory = Preferences.get("browser.urlbar.suggest.history"); + Preferences.set("browser.urlbar.suggest.history", false); + let suggestBookmarks = Preferences.get("browser.urlbar.suggest.bookmark"); + Preferences.set("browser.urlbar.suggest.bookmark", false); + let suggestOpenPages = Preferences.get("browser.urlbar.suggest.openpage"); + Preferences.set("browser.urlbar.suggest.openpage", false); + + await Services.search.addEngineWithDetails("MozSearch", { + method: "GET", + template: "http://example.com/?q={searchTerms}", + }); + let engine = Services.search.getEngineByName("MozSearch"); + let originalEngine = await Services.search.getDefault(); + await Services.search.setDefault(engine); + + async function cleanup() { + Preferences.set("browser.urlbar.suggest.history", suggestHistory); + Preferences.set("browser.urlbar.suggest.bookmark", suggestBookmarks); + Preferences.set("browser.urlbar.suggest.openpage", suggestOpenPages); + + await Services.search.setDefault(originalEngine); + let mozSearchEngine = Services.search.getEngineByName("MozSearch"); + if (mozSearchEngine) { + await Services.search.removeEngine(mozSearchEngine); + } + } + registerCleanupFunction(cleanup); + + gURLBar.focus(); + gURLBar.value = "e"; + EventUtils.sendString("x"); + EventUtils.synthesizeKey("KEY_Enter"); + + info("wait for the page to load"); + await BrowserTestUtils.browserLoaded( + gBrowser.selectedTab.linkedBrowser, + false, + "http://example.com/?q=ex" + ); + await cleanup(); + }) +); + +add_task( + taskWithNewTab(async function test_delay() { + // This is needed to clear the current value, otherwise autocomplete may think + // the user removed text from the end. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + await UrlbarTestUtils.promisePopupClose(window); + + // Set a large delay. + const TIMEOUT = 3000; + let delay = UrlbarPrefs.get("delay"); + UrlbarPrefs.set("delay", TIMEOUT); + registerCleanupFunction(function() { + UrlbarPrefs.set("delay", delay); + }); + + let start = Cu.now(); + gURLBar.focus(); + gURLBar.value = "e"; + EventUtils.sendString("x"); + EventUtils.synthesizeKey("KEY_Enter"); + info("wait for the page to load"); + await BrowserTestUtils.browserLoaded( + gBrowser.selectedTab.linkedBrowser, + false, + "http://example.com/" + ); + Assert.ok(Cu.now() - start < TIMEOUT); + }) +); + +// The main reason for running each test task in a new tab that's closed when +// the task finishes is to avoid switch-to-tab results. +function taskWithNewTab(fn) { + return async function() { + await BrowserTestUtils.withNewTab("about:blank", fn); + }; +} diff --git a/browser/components/urlbar/tests/browser/browser_autocomplete_no_title.js b/browser/components/urlbar/tests/browser/browser_autocomplete_no_title.js new file mode 100644 index 0000000000..13c9fc3fb5 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autocomplete_no_title.js @@ -0,0 +1,34 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test ensures that we display just the domain name when a URL result doesn't + * have a title. + */ + +add_task(async function() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:mozilla" + ); + await PlacesUtils.history.clear(); + const uri = "http://bug1060642.example.com/beards/are/pretty/great"; + await PlacesTestUtils.addVisits([{ uri, title: "" }]); + registerCleanupFunction(async function() { + await PlacesUtils.history.clear(); + BrowserTestUtils.removeTab(tab); + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "bug1060642", + }); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal( + result.displayed.title, + "bug1060642.example.com", + "Result title should be as expected" + ); +}); diff --git a/browser/components/urlbar/tests/browser/browser_autocomplete_readline_navigation.js b/browser/components/urlbar/tests/browser/browser_autocomplete_readline_navigation.js new file mode 100644 index 0000000000..399612738d --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autocomplete_readline_navigation.js @@ -0,0 +1,71 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests navigation between results using ctrl-n/p. + */ + +function repeat(limit, func) { + for (let i = 0; i < limit; i++) { + func(i); + } +} + +function assertSelected(index) { + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + index, + "Should have the correct item selected" + ); + + // This is true because although both the listbox and the one-offs can have + // selections, the test doesn't check that. + Assert.equal( + UrlbarTestUtils.getOneOffSearchButtons(window).selectedButton, + null, + "A result is selected, so the one-offs should not have a selection" + ); +} + +add_task(async function() { + let maxResults = Services.prefs.getIntPref("browser.urlbar.maxRichResults"); + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:mozilla" + ); + registerCleanupFunction(async function() { + await PlacesUtils.history.clear(); + BrowserTestUtils.removeTab(tab); + }); + + let visits = []; + repeat(maxResults, i => { + visits.push({ + uri: makeURI("http://example.com/autocomplete/?" + i), + }); + }); + await PlacesTestUtils.addVisits(visits); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "example.com/autocomplete", + }); + await UrlbarTestUtils.waitForAutocompleteResultAt(window, maxResults - 1); + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + maxResults, + "Should get maxResults=" + maxResults + " results" + ); + assertSelected(0); + + info("Ctrl-n to select the next item"); + EventUtils.synthesizeKey("n", { ctrlKey: true }); + assertSelected(1); + + info("Ctrl-p to select the previous item"); + EventUtils.synthesizeKey("p", { ctrlKey: true }); + assertSelected(0); +}); diff --git a/browser/components/urlbar/tests/browser/browser_autocomplete_tag_star_visibility.js b/browser/components/urlbar/tests/browser/browser_autocomplete_tag_star_visibility.js new file mode 100644 index 0000000000..a2dc30fdcc --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_autocomplete_tag_star_visibility.js @@ -0,0 +1,167 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests for the bookmark star being correct displayed for results matching + * tags. + */ + +add_task(async function() { + registerCleanupFunction(async function() { + await PlacesUtils.bookmarks.eraseEverything(); + }); + + async function addTagItem(tagName) { + let url = `http://example.com/this/is/tagged/${tagName}`; + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url, + title: `test ${tagName}`, + }); + PlacesUtils.tagging.tagURI(Services.io.newURI(url), [tagName]); + await PlacesTestUtils.addVisits({ + uri: url, + title: `Test page with tag ${tagName}`, + }); + } + + // We use different tags for each part of the test, as otherwise the + // autocomplete code tries to be smart by using the previously cached element + // without updating it (since all parameters it knows about are the same). + + let testcases = [ + { + description: "Test with suggest.bookmark=true", + tagName: "tagtest1", + prefs: { + "suggest.bookmark": true, + }, + input: "tagtest1", + expected: { + typeImageVisible: true, + }, + }, + { + description: "Test with suggest.bookmark=false", + tagName: "tagtest2", + prefs: { + "suggest.bookmark": false, + }, + input: "tagtest2", + expected: { + typeImageVisible: false, + }, + }, + { + description: "Test with suggest.bookmark=true (again)", + tagName: "tagtest3", + prefs: { + "suggest.bookmark": true, + }, + input: "tagtest3", + expected: { + typeImageVisible: true, + }, + }, + { + description: "Test with bookmark restriction token", + tagName: "tagtest4", + prefs: { + "suggest.bookmark": true, + }, + input: "* tagtest4", + expected: { + typeImageVisible: true, + }, + }, + { + description: "Test with history restriction token", + tagName: "tagtest5", + prefs: { + "suggest.bookmark": true, + }, + input: "^ tagtest5", + expected: { + typeImageVisible: false, + }, + }, + { + description: "Test partial tag and casing", + tagName: "tagtest6", + prefs: { + "suggest.bookmark": true, + }, + input: "TeSt6", + expected: { + typeImageVisible: true, + }, + }, + ]; + + for (let testcase of testcases) { + info(`Test case: ${testcase.description}`); + + await addTagItem(testcase.tagName); + for (let prefName of Object.keys(testcase.prefs)) { + Services.prefs.setBoolPref( + `browser.urlbar.${prefName}`, + testcase.prefs[prefName] + ); + } + + let context = await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: testcase.input, + }); + + // If testcase.input triggers local search mode, there won't be a heuristic. + let resultIndex = + context.searchMode && !context.searchMode.engineName ? 0 : 1; + + Assert.greaterOrEqual( + UrlbarTestUtils.getResultCount(window), + resultIndex + 1, + `Should be at least ${resultIndex + 1} results` + ); + + let result = await UrlbarTestUtils.getDetailsOfResultAt( + window, + resultIndex + ); + + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.URL, + "Should have a URL result type" + ); + // The Quantum Bar differs from the legacy urlbar in the fact that, if + // bookmarks are filtered out, it won't show tags for history results. + let expected_tags = !testcase.expected.typeImageVisible + ? [] + : [testcase.tagName]; + Assert.deepEqual( + result.tags, + expected_tags, + "Should have the expected tag" + ); + + if (testcase.expected.typeImageVisible) { + Assert.equal( + result.displayed.typeIcon, + 'url("chrome://browser/skin/bookmark-12.svg")', + "Should have the star image displayed or not as expected" + ); + } else { + Assert.equal( + result.displayed.typeIcon, + "none", + "Should have the star image displayed or not as expected" + ); + } + + await UrlbarTestUtils.promisePopupClose(window); + gURLBar.handleRevert(); + } +}); diff --git a/browser/components/urlbar/tests/browser/browser_blanking.js b/browser/components/urlbar/tests/browser/browser_blanking.js new file mode 100644 index 0000000000..3cfb1d3c1e --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_blanking.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URL = `${TEST_BASE_URL}file_blank_but_not_blank.html`; + +add_task(async function() { + for (let page of gInitialPages) { + if (page == "about:newtab") { + // New tab preloading makes this a pain to test, so skip + continue; + } + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, page); + ok( + !gURLBar.value, + "The URL bar should be empty if we load a plain " + page + " page." + ); + BrowserTestUtils.removeTab(tab); + } +}); + +add_task(async function() { + // The test was originally to check that reloading of a javascript: URL could + // throw an error and empty the URL bar. This situation can no longer happen + // as in bug 836567 we set document.URL to active document's URL on navigation + // to a javascript: URL; reloading after that will simply reload the original + // active document rather than the javascript: URL itself. But we can still + // verify that the URL bar's value is correct. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + is(gURLBar.value, TEST_URL, "The URL bar should match the URI"); + let browserLoaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + SpecialPowers.spawn(tab.linkedBrowser, [], function() { + content.document.querySelector("a").click(); + }); + await browserLoaded; + is( + gURLBar.value, + TEST_URL, + "The URL bar should be the previous active document's URI." + ); + await SpecialPowers.spawn(tab.linkedBrowser, [], async function() { + // This is sync, so by the time we return we should have changed the URL bar. + content.location.reload(); + }).catch(e => { + // Ignore expected exception. + }); + is( + gURLBar.value, + TEST_URL, + "The URL bar should still be the previous active document's URI." + ); + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_canonizeURL.js b/browser/components/urlbar/tests/browser/browser_canonizeURL.js new file mode 100644 index 0000000000..e15f6a393e --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_canonizeURL.js @@ -0,0 +1,255 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests turning non-url-looking values typed in the input field into proper URLs. + */ + +const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml"; + +add_task(async function checkCtrlWorks() { + registerCleanupFunction(async function() { + await PlacesUtils.history.clear(); + await UrlbarTestUtils.formHistory.clear(); + }); + + let defaultEngine = await Services.search.getDefault(); + let testcases = [ + ["example", "https://www.example.com/", { ctrlKey: true }], + // Check that a direct load is not overwritten by a previous canonization. + ["http://example.com/test/", "http://example.com/test/", {}], + ["ex-ample", "https://www.ex-ample.com/", { ctrlKey: true }], + [" example ", "https://www.example.com/", { ctrlKey: true }], + [" example/foo ", "https://www.example.com/foo", { ctrlKey: true }], + [ + " example/foo bar ", + "https://www.example.com/foo%20bar", + { ctrlKey: true }, + ], + ["example.net", "http://example.net/", { ctrlKey: true }], + ["http://example", "http://example/", { ctrlKey: true }], + ["example:8080", "http://example:8080/", { ctrlKey: true }], + ["ex-ample.foo", "http://ex-ample.foo/", { ctrlKey: true }], + ["example.foo/bar ", "http://example.foo/bar", { ctrlKey: true }], + ["1.1.1.1", "http://1.1.1.1/", { ctrlKey: true }], + ["ftp.example.bar", "http://ftp.example.bar/", { ctrlKey: true }], + [ + "ex ample", + defaultEngine.getSubmission("ex ample", null, "keyword").uri.spec, + { ctrlKey: true }, + ], + ]; + + if (Services.prefs.getBoolPref("network.ftp.enabled")) { + // Include FTP testcase only if FTP protocol handler is enabled, otherwise + // the test would hang on external application chooser popup. + testcases.push(["ftp://example", "ftp://example/", { ctrlKey: true }]); + } + + // Disable autoFill for this test, since it could mess up the results. + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.autoFill", false], + ["browser.urlbar.ctrlCanonizesURLs", true], + ], + }); + + for (let [inputValue, expectedURL, options] of testcases) { + info(`Testing input string: "${inputValue}" - expected: "${expectedURL}"`); + let promiseLoad = BrowserTestUtils.waitForDocLoadAndStopIt( + expectedURL, + gBrowser.selectedBrowser + ); + let promiseStopped = BrowserTestUtils.browserStopped( + gBrowser.selectedBrowser, + undefined, + true + ); + gURLBar.focus(); + gURLBar.inputField.value = inputValue.slice(0, -1); + EventUtils.sendString(inputValue.slice(-1)); + EventUtils.synthesizeKey("KEY_Enter", options); + await Promise.all([promiseLoad, promiseStopped]); + } +}); + +add_task(async function checkPrefTurnsOffCanonize() { + // Add a dummy search engine to avoid hitting the network. + let engine = await SearchTestUtils.promiseNewSearchEngine( + getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME + ); + let oldDefaultEngine = await Services.search.getDefault(); + await Services.search.setDefault(engine); + registerCleanupFunction(async () => + Services.search.setDefault(oldDefaultEngine) + ); + + // Ensure we don't end up loading something in the current tab becuase it's empty: + let initialTab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: "about:mozilla", + }); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.ctrlCanonizesURLs", false]], + }); + + let newURL = "http://mochi.test:8888/?terms=example"; + // On MacOS CTRL+Enter is not supposed to open in a new tab, because it uses + // CMD+Enter for that. + let promiseLoaded = + AppConstants.platform == "macosx" + ? BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, false, newURL) + : BrowserTestUtils.waitForNewTab(gBrowser); + + gURLBar.focus(); + gURLBar.selectionStart = gURLBar.selectionEnd = + gURLBar.inputField.value.length; + gURLBar.inputField.value = "exampl"; + EventUtils.sendString("e"); + EventUtils.synthesizeKey("KEY_Enter", { ctrlKey: true }); + + await promiseLoaded; + if (AppConstants.platform == "macosx") { + Assert.equal( + initialTab.linkedBrowser.currentURI.spec, + newURL, + "Original tab should have navigated" + ); + } else { + Assert.equal( + initialTab.linkedBrowser.currentURI.spec, + "about:mozilla", + "Original tab shouldn't have navigated" + ); + Assert.equal( + gBrowser.selectedBrowser.currentURI.spec, + newURL, + "New tab should have navigated" + ); + } + while (gBrowser.tabs.length > 1) { + gBrowser.removeTab(gBrowser.selectedTab, { animate: false }); + } +}); + +add_task(async function autofill() { + // Re-enable autofill and canonization. + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.autoFill", true], + ["browser.urlbar.ctrlCanonizesURLs", true], + ], + }); + + // Quantumbar automatically disables autofill when the old search string + // starts with the new search string, so to make sure that doesn't happen and + // that earlier tests don't conflict with this one, start a new search for + // some other string. + gURLBar.select(); + EventUtils.sendString("blah"); + + // Add a visit that will be autofilled. + await PlacesUtils.history.clear(); + await PlacesTestUtils.addVisits([ + { + uri: "http://example.com/", + transition: PlacesUtils.history.TRANSITIONS.TYPED, + }, + ]); + + let testcases = [ + ["ex", "https://www.ex.com/", { ctrlKey: true }], + // Check that a direct load is not overwritten by a previous canonization. + ["ex", "http://example.com/", {}], + // search alias + ["@goo", "https://www.goo.com/", { ctrlKey: true }], + ]; + + function promiseAutofill() { + return BrowserTestUtils.waitForEvent(gURLBar.inputField, "select"); + } + + for (let [inputValue, expectedURL, options] of testcases) { + let promiseLoad = BrowserTestUtils.waitForDocLoadAndStopIt( + expectedURL, + gBrowser.selectedBrowser + ); + gURLBar.select(); + let autofillPromise = promiseAutofill(); + EventUtils.sendString(inputValue); + await autofillPromise; + EventUtils.synthesizeKey("KEY_Enter", options); + await promiseLoad; + + // Here again, make sure autofill isn't disabled for the next search. See + // the comment above. + gURLBar.select(); + EventUtils.sendString("blah"); + } + + await PlacesUtils.history.clear(); +}); + +add_task(async function() { + info( + "Test whether canonization is disabled until the ctrl key is releasing if the key was used to paste text into urlbar" + ); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.ctrlCanonizesURLs", true]], + }); + + info("Paste the word to the urlbar"); + const testWord = "example"; + simulatePastingToUrlbar(testWord); + is(gURLBar.value, testWord, "Paste the test word correctly"); + + info("Send enter key while pressing the ctrl key"); + EventUtils.synthesizeKey("VK_RETURN", { type: "keydown", ctrlKey: true }); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + is( + gBrowser.selectedBrowser.documentURI.spec, + `http://mochi.test:8888/?terms=${testWord}`, + "The loaded url is not canonized" + ); + + EventUtils.synthesizeKey("VK_CONTROL", { type: "keyup" }); +}); + +add_task(async function() { + info("Test whether canonization is enabled again after releasing the ctrl"); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.ctrlCanonizesURLs", true]], + }); + + info("Paste the word to the urlbar"); + const testWord = "example"; + simulatePastingToUrlbar(testWord); + is(gURLBar.value, testWord, "Paste the test word correctly"); + + info("Release the ctrl key befoer typing Enter key"); + EventUtils.synthesizeKey("VK_CONTROL", { type: "keyup" }); + + info("Send enter key with the ctrl"); + const onLoad = BrowserTestUtils.waitForDocLoadAndStopIt( + `https://www.${testWord}.com/`, + gBrowser.selectedBrowser + ); + EventUtils.synthesizeKey("VK_RETURN", { type: "keydown", ctrlKey: true }); + await onLoad; + info("The loaded url is canonized"); +}); + +function simulatePastingToUrlbar(text) { + gURLBar.focus(); + + const keyForPaste = document + .getElementById("key_paste") + .getAttribute("key") + .toLowerCase(); + EventUtils.synthesizeKey(keyForPaste, { type: "keydown", ctrlKey: true }); + + gURLBar.select(); + EventUtils.sendString(text); +} diff --git a/browser/components/urlbar/tests/browser/browser_caret_navigation.js b/browser/components/urlbar/tests/browser/browser_caret_navigation.js new file mode 100644 index 0000000000..890acd30d6 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_caret_navigation.js @@ -0,0 +1,115 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that up and down keys move the caret on certain platforms, and that + * opening the popup doesn't change the caret position. + */ + +add_task(async function() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "This is a generic sentence", + }); + await UrlbarTestUtils.promisePopupClose(window); + + const INITIAL_SELECTION_START = 3; + const INITIAL_SELECTION_END = 10; + gURLBar.selectionStart = INITIAL_SELECTION_START; + gURLBar.selectionEnd = INITIAL_SELECTION_END; + + if (AppConstants.platform == "macosx" || AppConstants.platform == "linux") { + if (AppConstants.platform == "linux") { + await checkCaretMoves( + "KEY_ArrowUp", + INITIAL_SELECTION_START, + "Selection should be collapsed to its start" + ); + + gURLBar.selectionStart = INITIAL_SELECTION_START; + gURLBar.selectionEnd = INITIAL_SELECTION_END; + await checkCaretMoves( + "KEY_ArrowDown", + INITIAL_SELECTION_END, + "Selection should be collapsed to its end" + ); + } + + await checkCaretMoves( + "KEY_ArrowDown", + gURLBar.value.length, + "Caret should have moved to the end" + ); + await checkPopupOpens("KEY_ArrowDown"); + + await checkCaretMoves( + "KEY_ArrowUp", + 0, + "Caret should have moved to the start" + ); + await checkPopupOpens("KEY_ArrowUp"); + } else { + await checkPopupOpens("KEY_ArrowDown"); + await checkPopupOpens("KEY_ArrowUp"); + } +}); + +async function checkCaretMoves(key, pos, msg) { + checkIfKeyStartsQuery(key, false); + Assert.equal( + UrlbarTestUtils.isPopupOpen(window), + false, + `${key}: Popup shouldn't be open` + ); + Assert.equal( + gURLBar.selectionStart, + gURLBar.selectionEnd, + `${key}: Input selection should be empty` + ); + Assert.equal(gURLBar.selectionStart, pos, `${key}: ${msg}`); +} + +async function checkPopupOpens(key) { + // Store current selection and check it doesn't change. + let selectionStart = gURLBar.selectionStart; + let selectionEnd = gURLBar.selectionEnd; + await UrlbarTestUtils.promisePopupOpen(window, () => { + checkIfKeyStartsQuery(key, true); + }); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 0, + `${key}: Heuristic result should be selected` + ); + Assert.equal( + gURLBar.selectionStart, + selectionStart, + `${key}: Input selection start should not change` + ); + Assert.equal( + gURLBar.selectionEnd, + selectionEnd, + `${key}: Input selection end should not change` + ); + await UrlbarTestUtils.promisePopupClose(window); +} + +function checkIfKeyStartsQuery(key, shouldStartQuery) { + let queryStarted = false; + let queryListener = { + onQueryStarted() { + queryStarted = true; + }, + }; + gURLBar.controller.addQueryListener(queryListener); + EventUtils.synthesizeKey(key); + gURLBar.eventBufferer.replayDeferredEvents(false); + gURLBar.controller.removeQueryListener(queryListener); + Assert.equal( + queryStarted, + shouldStartQuery, + `${key}: Should${shouldStartQuery ? "" : "n't"} have started a query` + ); +} diff --git a/browser/components/urlbar/tests/browser/browser_closePanelOnClick.js b/browser/components/urlbar/tests/browser/browser_closePanelOnClick.js new file mode 100644 index 0000000000..f7d88ffbf5 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_closePanelOnClick.js @@ -0,0 +1,30 @@ +/* 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/. */ + +/** + * This tests that the urlbar panel closes when clicking certain ui elements. + */ + +"use strict"; + +add_task(async function() { + await BrowserTestUtils.withNewTab("about:robots", async () => { + for (let elt of [ + gBrowser.selectedBrowser, + gBrowser.tabContainer, + document.querySelector("#nav-bar toolbarspring"), + ]) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + waitForFocus, + value: "dummy", + }); + // Must have at least one test. + Assert.ok(!!elt, "Found a valid element: " + (elt.id || elt.localName)); + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeNativeMouseClickAtCenter(elt) + ); + } + }); +}); diff --git a/browser/components/urlbar/tests/browser/browser_content_opener.js b/browser/components/urlbar/tests/browser/browser_content_opener.js new file mode 100644 index 0000000000..1f7b80b21f --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_content_opener.js @@ -0,0 +1,23 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function() { + await BrowserTestUtils.withNewTab( + TEST_BASE_URL + "dummy_page.html", + async function(browser) { + let windowOpenedPromise = BrowserTestUtils.waitForNewWindow(); + await SpecialPowers.spawn(browser, [], function() { + content.window.open("", "_BLANK", "toolbar=no,height=300,width=500"); + }); + let newWin = await windowOpenedPromise; + is( + newWin.gURLBar.value, + "about:blank", + "Should be displaying about:blank for the opened window." + ); + await BrowserTestUtils.closeWindow(newWin); + } + ); +}); diff --git a/browser/components/urlbar/tests/browser/browser_copy_during_load.js b/browser/components/urlbar/tests/browser/browser_copy_during_load.js new file mode 100644 index 0000000000..b24cbe077d --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_copy_during_load.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that copying from the urlbar page works correctly after a result is +// confirmed but takes a while to load. + +add_task(async function() { + const SLOW_PAGE = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://www.example.com" + ) + "slow-page.sjs"; + + await BrowserTestUtils.withNewTab(gBrowser, async tab => { + gURLBar.focus(); + gURLBar.value = SLOW_PAGE; + let promise = TestUtils.waitForCondition( + () => gURLBar.getAttribute("pageproxystate") == "invalid" + ); + EventUtils.synthesizeKey("KEY_Enter"); + info("wait for the initial conditions"); + await promise; + + info("Copy the whole url"); + await SimpleTest.promiseClipboardChange(SLOW_PAGE, () => { + gURLBar.select(); + goDoCommand("cmd_copy"); + }); + + info("Copy the initial part of the url, as a different valid url"); + await SimpleTest.promiseClipboardChange( + SLOW_PAGE.substring(0, SLOW_PAGE.indexOf("slow-page.sjs")), + () => { + gURLBar.selectionStart = 0; + gURLBar.selectionEnd = gURLBar.value.indexOf("slow-page.sjs"); + goDoCommand("cmd_copy"); + } + ); + + // This is apparently necessary to avoid a timeout on mochitest shutdown(!?) + let browserStoppedPromise = BrowserTestUtils.browserStopped( + gBrowser, + null, + true + ); + BrowserStop(); + await browserStoppedPromise; + }); +}); diff --git a/browser/components/urlbar/tests/browser/browser_copying.js b/browser/components/urlbar/tests/browser/browser_copying.js new file mode 100644 index 0000000000..c1858cffa8 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_copying.js @@ -0,0 +1,386 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function getUrl(hostname, file) { + return ( + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + hostname + ) + file + ); +} + +add_task(async function() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + registerCleanupFunction(function() { + gBrowser.removeTab(tab); + gURLBar.setURI(); + }); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.trimURLs", true], + // avoid prompting about phishing + ["network.http.phishy-userpass-length", 32], + ], + }); + + for (let testCase of tests) { + if (testCase.setup) { + await testCase.setup(); + } + + if (testCase.loadURL) { + info(`Loading : ${testCase.loadURL}`); + let expectedLoad = testCase.expectedLoad || testCase.loadURL; + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, testCase.loadURL); + await BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + expectedLoad + ); + } else if (testCase.setURL) { + gURLBar.value = testCase.setURL; + } + if (testCase.setURL || testCase.loadURL) { + gURLBar.valueIsTyped = !!testCase.setURL; + is( + gURLBar.value, + testCase.expectedURL, + "url bar value set to " + gURLBar.value + ); + } + + gURLBar.focus(); + if (testCase.expectedValueOnFocus) { + Assert.equal( + gURLBar.value, + testCase.expectedValueOnFocus, + "Check value on focus" + ); + } + await testCopy(testCase.copyVal, testCase.copyExpected); + gURLBar.blur(); + + if (testCase.cleanup) { + await testCase.cleanup(); + } + } +}); + +var tests = [ + // pageproxystate="invalid" + { + setURL: "http://example.com/", + expectedURL: "example.com", + copyExpected: "example.com", + }, + { + copyVal: "<e>xample.com", + copyExpected: "e", + }, + { + copyVal: "<e>x<a>mple.com", + copyExpected: "ea", + }, + { + copyVal: "<e><xa>mple.com", + copyExpected: "exa", + }, + { + copyVal: "<e><xa>mple.co<m>", + copyExpected: "exam", + }, + { + copyVal: "<e><xample.co><m>", + copyExpected: "example.com", + }, + + // pageproxystate="valid" from this point on (due to the load) + { + loadURL: "http://example.com/", + expectedURL: "example.com", + copyExpected: "http://example.com/", + }, + { + copyVal: "<example.co>m", + copyExpected: "example.co", + }, + { + copyVal: "e<x>ample.com", + copyExpected: "x", + }, + { + copyVal: "<e>xample.com", + copyExpected: "e", + }, + { + copyVal: "<e>xample.co<m>", + copyExpected: "em", + }, + { + copyVal: "<exam><ple.com>", + copyExpected: "example.com", + }, + + { + loadURL: "http://example.com/foo", + expectedURL: "example.com/foo", + copyExpected: "http://example.com/foo", + }, + { + copyVal: "<example.com>/foo", + copyExpected: "http://example.com", + }, + { + copyVal: "<example>.com/foo", + copyExpected: "example", + }, + + // Test that userPass is stripped out + { + loadURL: getUrl( + "http://user:pass@mochi.test:8888", + "authenticate.sjs?user=user&pass=pass" + ), + expectedURL: getUrl( + "mochi.test:8888", + "authenticate.sjs?user=user&pass=pass" + ), + copyExpected: getUrl( + "http://mochi.test:8888", + "authenticate.sjs?user=user&pass=pass" + ), + }, + + // Test escaping + { + loadURL: "http://example.com/()%28%29%C3%A9", + expectedURL: "example.com/()()\xe9", + copyExpected: "http://example.com/()%28%29%C3%A9", + }, + { + copyVal: "<example.com/(>)()\xe9", + copyExpected: "http://example.com/(", + }, + { + copyVal: "e<xample.com/(>)()\xe9", + copyExpected: "xample.com/(", + }, + + { + loadURL: "http://example.com/%C3%A9%C3%A9", + expectedURL: "example.com/\xe9\xe9", + copyExpected: "http://example.com/%C3%A9%C3%A9", + }, + { + copyVal: "e<xample.com/\xe9>\xe9", + copyExpected: "xample.com/\xe9", + }, + { + copyVal: "<example.com/\xe9>\xe9", + copyExpected: "http://example.com/\xe9", + }, + { + // Note: it seems BrowserTestUtils.loadURI fails for unicode domains + loadURL: "http://sub2.xn--lt-uia.mochi.test:8888/foo", + expectedURL: "sub2.ält.mochi.test:8888/foo", + copyExpected: "http://sub2.ält.mochi.test:8888/foo", + }, + { + copyVal: "s<ub2.ält.mochi.test:8888/f>oo", + copyExpected: "ub2.ält.mochi.test:8888/f", + }, + { + copyVal: "<sub2.ält.mochi.test:8888/f>oo", + copyExpected: "http://sub2.ält.mochi.test:8888/f", + }, + + { + loadURL: "http://example.com/?%C3%B7%C3%B7", + expectedURL: "example.com/?\xf7\xf7", + copyExpected: "http://example.com/?%C3%B7%C3%B7", + }, + { + copyVal: "e<xample.com/?\xf7>\xf7", + copyExpected: "xample.com/?\xf7", + }, + { + copyVal: "<example.com/?\xf7>\xf7", + copyExpected: "http://example.com/?\xf7", + }, + { + loadURL: "http://example.com/a%20test", + expectedURL: "example.com/a test", + copyExpected: "http://example.com/a%20test", + }, + { + loadURL: "http://example.com/a%E3%80%80test", + expectedURL: "example.com/a%E3%80%80test", + copyExpected: "http://example.com/a%E3%80%80test", + }, + { + loadURL: "http://example.com/a%20%C2%A0test", + expectedURL: "example.com/a %C2%A0test", + copyExpected: "http://example.com/a%20%C2%A0test", + }, + { + loadURL: "http://example.com/%20%20%20", + expectedURL: "example.com/%20%20%20", + copyExpected: "http://example.com/%20%20%20", + }, + { + loadURL: "http://example.com/%E3%80%80%E3%80%80", + expectedURL: "example.com/%E3%80%80%E3%80%80", + copyExpected: "http://example.com/%E3%80%80%E3%80%80", + }, + + // Loading of javascript: URI results in previous URI, so if the previous + // entry changes, change this one too! + { + loadURL: "javascript:('%C3%A9%20%25%50')", + expectedLoad: "http://example.com/%E3%80%80%E3%80%80", + expectedURL: "example.com/%E3%80%80%E3%80%80", + copyExpected: "http://example.com/%E3%80%80%E3%80%80", + }, + + // data: URIs shouldn't be encoded + { + loadURL: "data:text/html,(%C3%A9%20%25%50)", + expectedURL: "data:text/html,(%C3%A9 %25P)", + copyExpected: "data:text/html,(%C3%A9 %25P)", + }, + { + copyVal: "<data:text/html,(>%C3%A9 %25P)", + copyExpected: "data:text/html,(", + }, + { + copyVal: "<data:text/html,(%C3%A9 %25P>)", + copyExpected: "data:text/html,(%C3%A9 %25P", + }, + + { + async setup() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.decodeURLsOnCopy", true]], + }); + }, + async cleanup() { + await SpecialPowers.popPrefEnv(); + }, + loadURL: + "http://example.com/%D0%B1%D0%B8%D0%BE%D0%B3%D1%80%D0%B0%D1%84%D0%B8%D1%8F", + expectedURL: "example.com/биография", + copyExpected: "http://example.com/биография", + }, + { + copyVal: "<example.com/би>ография", + copyExpected: "http://example.com/би", + }, + + { + async setup() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.decodeURLsOnCopy", true]], + }); + // Setup a valid intranet url that resolves but is not yet known. + const proxyService = Cc[ + "@mozilla.org/network/protocol-proxy-service;1" + ].getService(Ci.nsIProtocolProxyService); + let proxyInfo = proxyService.newProxyInfo( + "http", + "localhost", + 8888, + "", + "", + 0, + 4096, + null + ); + this._proxyFilter = { + applyFilter(channel, defaultProxyInfo, callback) { + callback.onProxyFilterResult( + channel.URI.host === "mytest" ? proxyInfo : defaultProxyInfo + ); + }, + }; + proxyService.registerChannelFilter(this._proxyFilter, 0); + registerCleanupFunction(() => { + if (this._proxyFilter) { + proxyService.unregisterChannelFilter(this._proxyFilter); + } + }); + }, + async cleanup() { + await SpecialPowers.popPrefEnv(); + const proxyService = Cc[ + "@mozilla.org/network/protocol-proxy-service;1" + ].getService(Ci.nsIProtocolProxyService); + proxyService.unregisterChannelFilter(this._proxyFilter); + this._proxyFilter = null; + }, + loadURL: "http://mytest/", + expectedURL: "mytest", + expectedValueOnFocus: "http://mytest/", + copyExpected: "http://mytest/", + }, + + { + async setup() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.decodeURLsOnCopy", true]], + }); + }, + async cleanup() { + await SpecialPowers.popPrefEnv(); + }, + loadURL: "https://example.com/", + expectedURL: "https://example.com", + copyExpected: "https://example.com", + }, +]; + +function testCopy(copyVal, targetValue) { + info("Expecting copy of: " + targetValue); + + if (copyVal) { + let offsets = []; + while (true) { + let startBracket = copyVal.indexOf("<"); + let endBracket = copyVal.indexOf(">"); + if (startBracket == -1 && endBracket == -1) { + break; + } + if (startBracket > endBracket || startBracket == -1) { + offsets = []; + break; + } + offsets.push([startBracket, endBracket - 1]); + copyVal = copyVal.replace("<", "").replace(">", ""); + } + if (!offsets.length || copyVal != gURLBar.value) { + ok(false, "invalid copyVal: " + copyVal); + } + gURLBar.selectionStart = offsets[0][0]; + gURLBar.selectionEnd = offsets[0][1]; + if (offsets.length > 1) { + let sel = gURLBar.editor.selection; + let r0 = sel.getRangeAt(0); + let node0 = r0.startContainer; + sel.removeAllRanges(); + offsets.map(function(startEnd) { + let range = r0.cloneRange(); + range.setStart(node0, startEnd[0]); + range.setEnd(node0, startEnd[1]); + sel.addRange(range); + }); + } + } else { + gURLBar.select(); + } + + return SimpleTest.promiseClipboardChange(targetValue, () => + goDoCommand("cmd_copy") + ); +} diff --git a/browser/components/urlbar/tests/browser/browser_customizeMode.js b/browser/components/urlbar/tests/browser/browser_customizeMode.js new file mode 100644 index 0000000000..0ed26644cc --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_customizeMode.js @@ -0,0 +1,73 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This test checks that the left/right arrow keys and home/end keys work in +// the urlbar after customize mode starts and ends. + +"use strict"; + +add_task(async function test() { + let win = await BrowserTestUtils.openNewBrowserWindow(); + await startCustomizing(win); + await endCustomizing(win); + + let urlbar = win.gURLBar; + + let value = "example"; + urlbar.value = value; + urlbar.focus(); + urlbar.selectionEnd = value.length; + urlbar.selectionStart = value.length; + + // left + EventUtils.synthesizeKey("KEY_ArrowLeft", {}, win); + Assert.equal(urlbar.selectionStart, value.length - 1); + Assert.equal(urlbar.selectionEnd, value.length - 1); + + // home + if (AppConstants.platform == "macosx") { + EventUtils.synthesizeKey("KEY_ArrowLeft", { metaKey: true }, win); + } else { + EventUtils.synthesizeKey("KEY_Home", {}, win); + } + Assert.equal(urlbar.selectionStart, 0); + Assert.equal(urlbar.selectionEnd, 0); + + // right + EventUtils.synthesizeKey("KEY_ArrowRight", {}, win); + Assert.equal(urlbar.selectionStart, 1); + Assert.equal(urlbar.selectionEnd, 1); + + // end + if (AppConstants.platform == "macosx") { + EventUtils.synthesizeKey("KEY_ArrowRight", { metaKey: true }, win); + } else { + EventUtils.synthesizeKey("KEY_End", {}, win); + } + Assert.equal(urlbar.selectionStart, value.length); + Assert.equal(urlbar.selectionEnd, value.length); + + await BrowserTestUtils.closeWindow(win); +}); + +async function startCustomizing(win = window) { + if (win.document.documentElement.getAttribute("customizing") != "true") { + let eventPromise = BrowserTestUtils.waitForEvent( + win.gNavToolbox, + "customizationready" + ); + win.gCustomizeMode.enter(); + await eventPromise; + } +} + +async function endCustomizing(win = window) { + if (win.document.documentElement.getAttribute("customizing") == "true") { + let eventPromise = BrowserTestUtils.waitForEvent( + win.gNavToolbox, + "aftercustomization" + ); + win.gCustomizeMode.exit(); + await eventPromise; + } +} diff --git a/browser/components/urlbar/tests/browser/browser_cutting.js b/browser/components/urlbar/tests/browser/browser_cutting.js new file mode 100644 index 0000000000..31b51751d9 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_cutting.js @@ -0,0 +1,17 @@ +/* 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/. */ + +function test() { + gURLBar.focus(); + gURLBar.inputField.value = "https://example.com/"; + gURLBar.selectionStart = 4; + gURLBar.selectionEnd = 5; + goDoCommand("cmd_cut"); + is( + gURLBar.inputField.value, + "http://example.com/", + "location bar value after cutting 's' from https" + ); + gURLBar.handleRevert(); +} diff --git a/browser/components/urlbar/tests/browser/browser_decode.js b/browser/components/urlbar/tests/browser/browser_decode.js new file mode 100644 index 0000000000..4dc199c1f7 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_decode.js @@ -0,0 +1,144 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// This test makes sure (1) you can't break the urlbar by typing particular JSON +// or JS fragments into it, (2) urlbar.textValue shows URLs unescaped, and (3) +// the urlbar also shows the URLs embedded in action URIs unescaped. See bug +// 1233672. + +add_task(async function injectJSON() { + let inputStrs = [ + 'http://example.com/ ", "url": "bar', + "http://example.com/\\", + 'http://example.com/"', + 'http://example.com/","url":"evil.com', + "http://mozilla.org/\\u0020", + 'http://www.mozilla.org/","url":1e6,"some-key":"foo', + 'http://www.mozilla.org/","url":null,"some-key":"foo', + 'http://www.mozilla.org/","url":["foo","bar"],"some-key":"foo', + ]; + for (let inputStr of inputStrs) { + await checkInput(inputStr); + } + gURLBar.value = ""; + gURLBar.handleRevert(); + gURLBar.blur(); +}); + +add_task(function losslessDecode() { + let urlNoScheme = "example.com/\u30a2\u30a4\u30a6\u30a8\u30aa"; + let url = "http://" + urlNoScheme; + const result = new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TAB_SWITCH, + UrlbarUtils.RESULT_SOURCE.TABS, + { url } + ); + gURLBar.setValueFromResult(result); + // Since this is directly setting textValue, it is expected to be trimmed. + Assert.equal( + gURLBar.inputField.value, + urlNoScheme, + "The string displayed in the textbox should not be escaped" + ); + gURLBar.value = ""; + gURLBar.handleRevert(); + gURLBar.blur(); +}); + +add_task(async function actionURILosslessDecode() { + let urlNoScheme = "example.com/\u30a2\u30a4\u30a6\u30a8\u30aa"; + let url = "http://" + urlNoScheme; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: url, + }); + + // At this point the heuristic result is selected but the urlbar's value is + // simply `url`. Key down and back around until the heuristic result is + // selected again. + do { + EventUtils.synthesizeKey("KEY_ArrowDown"); + } while (UrlbarTestUtils.getSelectedRowIndex(window) != 0); + + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.URL, + "Should have selected a result of URL type" + ); + + Assert.equal( + gURLBar.inputField.value, + urlNoScheme, + "The string displayed in the textbox should not be escaped" + ); + + gURLBar.value = ""; + gURLBar.handleRevert(); + gURLBar.blur(); +}); + +add_task(async function test_resultsDisplayDecoded() { + await PlacesUtils.history.clear(); + await UrlbarTestUtils.formHistory.clear(); + + await PlacesTestUtils.addVisits("http://example.com/%E9%A1%B5"); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "example", + }); + + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal( + result.displayed.url, + "http://example.com/\u9875", + "Should be displayed the correctly unescaped URL" + ); +}); + +async function checkInput(inputStr) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: inputStr, + }); + + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + + // URL matches have their param.urls fixed up. + let fixupInfo = Services.uriFixup.getFixupURIInfo( + inputStr, + Ci.nsIURIFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS | + Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP + ); + let expectedVisitURL = fixupInfo.fixedURI.spec; + + Assert.equal(result.url, expectedVisitURL, "Should have the correct URL"); + Assert.equal( + result.title, + inputStr.replace("\\", "/"), + "Should have the correct title" + ); + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.URL, + "Should have be a result of type URL" + ); + + Assert.equal( + result.displayed.title, + inputStr.replace("\\", "/"), + "Should be displaying the correct text" + ); + let [action] = await document.l10n.formatValues([ + { id: "urlbar-result-action-visit" }, + ]); + Assert.equal( + result.displayed.action, + action, + "Should be displaying the correct action text" + ); +} diff --git a/browser/components/urlbar/tests/browser/browser_delete.js b/browser/components/urlbar/tests/browser/browser_delete.js new file mode 100644 index 0000000000..48fce657ad --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_delete.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test deleting the start of urls works correctly. + */ + +add_task(async function() { + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://bug1105244.example.com/", + title: "test", + }); + + registerCleanupFunction(async function() { + await PlacesUtils.bookmarks.remove(bm); + }); + + await BrowserTestUtils.withNewTab("about:blank", testDelete); +}); + +function sendHome() { + // unclear why VK_HOME doesn't work on Mac, but it doesn't... + if (AppConstants.platform == "macosx") { + EventUtils.synthesizeKey("KEY_ArrowLeft", { metaKey: true }); + } else { + EventUtils.synthesizeKey("KEY_Home"); + } +} + +function sendDelete() { + EventUtils.synthesizeKey("KEY_Delete"); +} + +async function testDelete() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "bug1105244", + }); + + // move to the start. + sendHome(); + + // delete the first few chars - each delete should operate on the input field. + await UrlbarTestUtils.promisePopupOpen(window, sendDelete); + Assert.equal(gURLBar.inputField.value, "ug1105244.example.com/"); + sendDelete(); + Assert.equal(gURLBar.inputField.value, "g1105244.example.com/"); +} diff --git a/browser/components/urlbar/tests/browser/browser_deleteAllText.js b/browser/components/urlbar/tests/browser/browser_deleteAllText.js new file mode 100644 index 0000000000..5b355fa477 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_deleteAllText.js @@ -0,0 +1,100 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This test makes sure that deleting all text in the input doesn't mess up +// subsequent searches. + +"use strict"; + +add_task(async function test() { + await runTest(); + // Setting suggest.topsites to false disables the view's autoOpen behavior, + // which changes this test's outcomes. + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.topsites", false]], + }); + info("Running the test with autoOpen disabled."); + await runTest(); + await SpecialPowers.popPrefEnv(); +}); + +async function runTest() { + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); + await PlacesTestUtils.addVisits([ + "http://example.com/", + "http://mozilla.org/", + ]); + + // Do an initial search for "x". + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "x", + fireInputEvent: true, + }); + await checkResults(); + + await deleteInput(); + + // Type "x". A new search should start. Don't use + // promiseAutocompleteResultPopup, which has some logic that starts the search + // manually in certain conditions. We want to specifically check that the + // input event causes UrlbarInput to start a new search on its own. If it + // doesn't, then the test will hang here on promiseSearchComplete. + EventUtils.synthesizeKey("x"); + await UrlbarTestUtils.promiseSearchComplete(window); + await checkResults(); + + // Now repeat the backspace + x two more times. Same thing should happen. + for (let i = 0; i < 2; i++) { + await deleteInput(); + EventUtils.synthesizeKey("x"); + await UrlbarTestUtils.promiseSearchComplete(window); + await checkResults(); + } + + await deleteInput(); + // autoOpen opened the panel, so we need to close it. + gURLBar.view.close(); +} + +async function checkResults() { + Assert.equal(await UrlbarTestUtils.getResultCount(window), 2); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal(details.type, UrlbarUtils.RESULT_TYPE.SEARCH); + Assert.equal(details.searchParams.query, "x"); + details = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal(details.type, UrlbarUtils.RESULT_TYPE.URL); + Assert.equal(details.url, "http://example.com/"); +} + +async function deleteInput() { + if (UrlbarPrefs.get("suggest.topsites")) { + // The popup should remain open and show top sites. + while (gURLBar.value.length) { + EventUtils.synthesizeKey("KEY_Backspace"); + } + Assert.ok( + gURLBar.view.isOpen, + "View should remain open when deleting all input text" + ); + let queryContext = await UrlbarTestUtils.promiseSearchComplete(window); + Assert.notEqual( + queryContext.results.length, + 0, + "View should show results when deleting all input text" + ); + Assert.equal( + queryContext.searchString, + "", + "Results should be for the empty search string (i.e. top sites) when deleting all input text" + ); + } else { + // Deleting all text should close the view. + await UrlbarTestUtils.promisePopupClose(window, () => { + while (gURLBar.value.length) { + EventUtils.synthesizeKey("KEY_Backspace"); + } + }); + } +} diff --git a/browser/components/urlbar/tests/browser/browser_display_selectedAction_Extensions.js b/browser/components/urlbar/tests/browser/browser_display_selectedAction_Extensions.js new file mode 100644 index 0000000000..d3a51ede76 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_display_selectedAction_Extensions.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests for the presence of selected action text "Extensions:" in the URL bar. + */ + +add_task(async function testSwitchToTabTextDisplay() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + omnibox: { + keyword: "omniboxtest", + }, + + background() { + /* global browser */ + browser.omnibox.setDefaultSuggestion({ + description: "doit", + }); + // Just do nothing for this test. + }, + }, + }); + + await extension.startup(); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "omniboxtest ", + fireInputEvent: true, + }); + + // The "Extension:" label appears after a key down followed by a key up + // back to the extension result. + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_ArrowUp"); + + // Checks to see if "Extension:" text in URL bar is visible + const extensionText = document.getElementById("urlbar-label-extension"); + Assert.ok(BrowserTestUtils.is_visible(extensionText)); + Assert.equal(extensionText.value, "Extension:"); + + // Check to see if all other labels are hidden + const allLabels = document.getElementById("urlbar-label-box").children; + for (let label of allLabels) { + if (label != extensionText) { + Assert.ok(BrowserTestUtils.is_hidden(label)); + } + } + + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Escape") + ); + await extension.unload(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_dns_first_for_single_words.js b/browser/components/urlbar/tests/browser/browser_dns_first_for_single_words.js new file mode 100644 index 0000000000..5a294afcbc --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_dns_first_for_single_words.js @@ -0,0 +1,49 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +// Checks that if browser.fixup.dns_first_for_single_words pref is set, we pass +// the original search string to the docshell and not a search url. + +add_task(async function test() { + const { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm"); + const sandbox = sinon.createSandbox(); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.fixup.dns_first_for_single_words", true]], + }); + + registerCleanupFunction(sandbox.restore); + + /** + * Tests the given search string. + * @param {string} str The search string + * @param {boolean} passthrough whether the value should be passed unchanged + * to the docshell that will first execute a DNS request. + */ + async function testVal(str, passthrough) { + sandbox.stub(gURLBar, "_loadURL").callsFake(url => { + if (passthrough) { + Assert.equal(url, str, "Should pass the unmodified search string"); + } else { + Assert.ok(url.startsWith("http"), "Should pass an url"); + } + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: str, + }); + EventUtils.synthesizeKey("KEY_Enter"); + sandbox.restore(); + } + + await testVal("test", true); + await testVal("te-st", true); + await testVal("test ", true); + await testVal(" test", true); + await testVal(" test", true); + await testVal("test.test", true); + await testVal("test test", false); + // This is not a single word host, though it contains one. At a certain point + // we may evaluate to increase coverage of the feature to also ask for this. + await testVal("test/test", false); +}); diff --git a/browser/components/urlbar/tests/browser/browser_downArrowKeySearch.js b/browser/components/urlbar/tests/browser/browser_downArrowKeySearch.js new file mode 100644 index 0000000000..15273b4285 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_downArrowKeySearch.js @@ -0,0 +1,83 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Checks that pressing the down arrow key starts the proper searches, depending +// on the input value/state. + +"use strict"; + +add_task(async function init() { + await PlacesUtils.history.clear(); + // Enough vists to get this site into Top Sites. + for (let i = 0; i < 5; i++) { + await PlacesTestUtils.addVisits("http://example.com/"); + } + + await updateTopSites( + sites => sites && sites[0] && sites[0].url == "http://example.com/" + ); + registerCleanupFunction(async function() { + await PlacesUtils.history.clear(); + }); +}); + +add_task(async function url() { + await BrowserTestUtils.withNewTab("http://example.com/", async () => { + gURLBar.focus(); + gURLBar.selectionEnd = gURLBar.untrimmedValue.length; + gURLBar.selectionStart = gURLBar.untrimmedValue.length; + EventUtils.synthesizeKey("KEY_ArrowDown"); + await UrlbarTestUtils.promiseSearchComplete(window); + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), 0); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill); + Assert.equal(details.url, "http://example.com/"); + Assert.equal(gURLBar.value, "example.com/"); + await UrlbarTestUtils.promisePopupClose(window); + }); +}); + +add_task(async function userTyping() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "foo", + fireInputEvent: true, + }); + await UrlbarTestUtils.promisePopupClose(window); + EventUtils.synthesizeKey("KEY_ArrowDown"); + await UrlbarTestUtils.promiseSearchComplete(window); + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), 0); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal(details.type, UrlbarUtils.RESULT_TYPE.SEARCH); + Assert.ok(details.searchParams); + Assert.equal(details.searchParams.query, "foo"); + Assert.equal(gURLBar.value, "foo"); + await UrlbarTestUtils.promisePopupClose(window); +}); + +add_task(async function empty() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + fireInputEvent: true, + }); + await UrlbarTestUtils.promisePopupClose(window); + EventUtils.synthesizeKey("KEY_ArrowDown"); + await UrlbarTestUtils.promiseSearchComplete(window); + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), -1); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal(details.url, "http://example.com/"); + Assert.equal(gURLBar.value, ""); +}); + +add_task(async function new_window() { + let win = await BrowserTestUtils.openNewBrowserWindow(); + win.gURLBar.focus(); + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + await UrlbarTestUtils.promiseSearchComplete(win); + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(win), -1); + let details = await UrlbarTestUtils.getDetailsOfResultAt(win, 0); + Assert.equal(details.url, "http://example.com/"); + Assert.equal(win.gURLBar.value, ""); + await BrowserTestUtils.closeWindow(win); +}); diff --git a/browser/components/urlbar/tests/browser/browser_dragdropURL.js b/browser/components/urlbar/tests/browser/browser_dragdropURL.js new file mode 100644 index 0000000000..c4a4a04749 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_dragdropURL.js @@ -0,0 +1,108 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests for draging and dropping to the Urlbar. + */ + +const TEST_URL = "data:text/html,a test page"; + +add_task(async function test_setup() { + // Stop search-engine loads from hitting the network. + await Services.search.addEngineWithDetails("MozSearch", { + method: "GET", + template: "http://example.com/?q={searchTerms}", + }); + let originalEngine = await Services.search.getDefault(); + let engine = Services.search.getEngineByName("MozSearch"); + await Services.search.setDefault(engine); + + registerCleanupFunction(async function cleanup() { + while (gBrowser.tabs.length > 1) { + BrowserTestUtils.removeTab(gBrowser.tabs[gBrowser.tabs.length - 1]); + } + await Services.search.setDefault(originalEngine); + await Services.search.removeEngine(engine); + }); +}); + +/** + * Simulates a drop on the URL bar input field. + * The drag source must be something different from the URL bar, so we pick the + * home button somewhat arbitrarily. + * @param {object} content a {type, data} object representing the DND content. + */ +function simulateURLBarDrop(content) { + EventUtils.synthesizeDrop( + document.getElementById("home-button"), // Dragstart element. + gURLBar.inputField, // Drop element. + [[content]], // Drag data. + "copy", + window + ); +} + +add_task(async function checkDragURL() { + await BrowserTestUtils.withNewTab(TEST_URL, function(browser) { + info("Check dragging a normal url to the urlbar"); + const DRAG_URL = "http://www.example.com/"; + simulateURLBarDrop({ type: "text/plain", data: DRAG_URL }); + Assert.equal( + gURLBar.value, + TEST_URL, + "URL bar value should not have changed" + ); + Assert.equal( + gBrowser.selectedBrowser.userTypedValue, + null, + "Stored URL bar value should not have changed" + ); + }); +}); + +add_task(async function checkDragForbiddenURL() { + await BrowserTestUtils.withNewTab(TEST_URL, function(browser) { + // See also browser_removeUnsafeProtocolsFromURLBarPaste.js for other + // examples. In general we trust that function, we pick some testcases to + // ensure we disallow dropping trimmed text. + for (let url of [ + "chrome://browser/content/aboutDialog.xhtml", + "file:///", + "javascript:", + "javascript:void(0)", + "java\r\ns\ncript:void(0)", + " javascript:void(0)", + "\u00A0java\nscript:void(0)", + "javascript:document.domain", + "javascript:javascript:alert('hi!')", + ]) { + info(`Check dragging "{$url}" to the URL bar`); + simulateURLBarDrop({ type: "text/plain", data: url }); + Assert.notEqual( + gURLBar.value, + url, + `Shouldn't be allowed to drop ${url} on URL bar` + ); + } + }); +}); + +add_task(async function checkDragText() { + await BrowserTestUtils.withNewTab(TEST_URL, async browser => { + info("Check dragging multi word text to the urlbar"); + const TEXT = "Firefox is awesome"; + const TEXT_URL = "http://example.com/?q=Firefox+is+awesome"; + let promiseLoad = BrowserTestUtils.browserLoaded(browser, false, TEXT_URL); + simulateURLBarDrop({ type: "text/plain", data: TEXT }); + await promiseLoad; + + info("Check dragging single word text to the urlbar"); + const WORD = "Firefox"; + const WORD_URL = "http://example.com/?q=Firefox"; + promiseLoad = BrowserTestUtils.browserLoaded(browser, false, WORD_URL); + simulateURLBarDrop({ type: "text/plain", data: WORD }); + await promiseLoad; + }); +}); diff --git a/browser/components/urlbar/tests/browser/browser_dynamicResults.js b/browser/components/urlbar/tests/browser/browser_dynamicResults.js new file mode 100644 index 0000000000..97126f35c3 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_dynamicResults.js @@ -0,0 +1,740 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This tests dynamic results. + */ + +"use strict"; + +const DYNAMIC_TYPE_NAME = "test"; + +const DYNAMIC_TYPE_VIEW_TEMPLATE = { + stylesheet: getRootDirectory(gTestPath) + "dynamicResult0.css", + children: [ + { + name: "selectable", + tag: "span", + attributes: { + selectable: "true", + }, + }, + { + name: "text", + tag: "span", + }, + { + name: "buttonBox", + tag: "span", + children: [ + { + name: "button1", + tag: "span", + attributes: { + role: "button", + }, + }, + { + name: "button2", + tag: "span", + attributes: { + role: "button", + }, + }, + ], + }, + ], +}; + +const DUMMY_PAGE = + "http://example.com/browser/browser/base/content/test/general/dummy_page.html"; + +// Tests the dynamic type registration functions and stylesheet loading. +add_task(async function registration() { + // Get our test stylesheet URIs. + let stylesheetURIs = []; + for (let i = 0; i < 2; i++) { + stylesheetURIs.push( + Services.io.newURI(getRootDirectory(gTestPath) + `dynamicResult${i}.css`) + ); + } + + // Maps from dynamic type names to their type. + let viewTemplatesByName = { + foo: { + stylesheet: stylesheetURIs[0].spec, + children: [ + { + name: "text", + tag: "span", + }, + ], + }, + bar: { + stylesheet: stylesheetURIs[1].spec, + children: [ + { + name: "icon", + tag: "span", + }, + { + name: "button", + tag: "span", + attributes: { + role: "button", + }, + }, + ], + }, + }; + + // First, open another window so that multiple windows are open when we add + // the types so we can verify below that the stylesheets are added to all open + // windows. + let newWindows = []; + newWindows.push(await BrowserTestUtils.openNewBrowserWindow()); + + // Add the test dynamic types. + for (let [name, viewTemplate] of Object.entries(viewTemplatesByName)) { + UrlbarResult.addDynamicResultType(name); + UrlbarView.addDynamicViewTemplate(name, viewTemplate); + } + + // Get them back to make sure they were added. + for (let name of Object.keys(viewTemplatesByName)) { + let actualType = UrlbarResult.getDynamicResultType(name); + // Types are currently just empty objects. + Assert.deepEqual(actualType, {}, "Types should match"); + } + + // Their stylesheets should have been applied to all open windows. There's no + // good way to check this because: + // + // * nsIStyleSheetService has a function that returns whether a stylesheet has + // been loaded, but it's global and not per window. + // * nsIDOMWindowUtils has functions to load stylesheets but not one to check + // whether a stylesheet has been loaded. + // * document.stylesheets only contains stylesheets in the DOM. + // + // So instead we set a CSS variable on #urlbar in each of our stylesheets and + // check that it's present. + function getCSSVariables(windows) { + let valuesByWindow = new Map(); + for (let win of windows) { + let values = []; + valuesByWindow.set(window, values); + for (let i = 0; i < stylesheetURIs.length; i++) { + let value = win + .getComputedStyle(gURLBar.panel) + .getPropertyValue(`--testDynamicResult${i}`); + values.push((value || "").trim()); + } + } + return valuesByWindow; + } + function checkCSSVariables(windows) { + for (let values of getCSSVariables(windows).values()) { + for (let i = 0; i < stylesheetURIs.length; i++) { + if (values[i].trim() !== `ok${i}`) { + return false; + } + } + } + return true; + } + + // The stylesheets are loaded asyncly, so we need to poll for it. + await TestUtils.waitForCondition(() => + checkCSSVariables(BrowserWindowTracker.orderedWindows) + ); + Assert.ok(true, "Stylesheets loaded in all open windows"); + + // Open another window to make sure the stylesheets are loaded in it after we + // added the new dynamic types. + let newWin = await BrowserTestUtils.openNewBrowserWindow(); + newWindows.push(newWin); + await TestUtils.waitForCondition(() => checkCSSVariables([newWin])); + Assert.ok(true, "Stylesheets loaded in new window"); + + // Remove the dynamic types. + for (let name of Object.keys(viewTemplatesByName)) { + UrlbarView.removeDynamicViewTemplate(name); + UrlbarResult.removeDynamicResultType(name); + let actualType = UrlbarResult.getDynamicResultType(name); + Assert.equal(actualType, null, "Type should be unregistered"); + } + + // The stylesheets should be removed from all windows. + let valuesByWindow = getCSSVariables(BrowserWindowTracker.orderedWindows); + for (let values of valuesByWindow.values()) { + for (let i = 0; i < stylesheetURIs.length; i++) { + Assert.ok(!values[i], "Stylesheet should be removed"); + } + } + + // Close the new windows. + for (let win of newWindows) { + await BrowserTestUtils.closeWindow(win); + } +}); + +// Tests that the view is created correctly from the view template. +add_task(async function viewCreated() { + await withDynamicTypeProvider(async () => { + // Do a search. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + waitForFocus: SimpleTest.waitForFocus, + }); + + // Get the row. + let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1); + Assert.equal( + row.result.type, + UrlbarUtils.RESULT_TYPE.DYNAMIC, + "row.result.type" + ); + Assert.equal( + row.getAttribute("dynamicType"), + DYNAMIC_TYPE_NAME, + "row[dynamicType]" + ); + let inner = row.querySelector(".urlbarView-row-inner"); + Assert.ok(inner, ".urlbarView-row-inner should exist"); + + // Check the DOM. + checkDOM(inner, DYNAMIC_TYPE_VIEW_TEMPLATE.children); + + await UrlbarTestUtils.promisePopupClose(window); + }); +}); + +// Tests that the view is updated correctly. +add_task(async function viewUpdated() { + await withDynamicTypeProvider(async () => { + // Test a few different search strings. The dynamic result view will be + // updated to reflect the current string. + for (let searchString of ["test", "some other string", "and another"]) { + // Do a search. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: searchString, + waitForFocus: SimpleTest.waitForFocus, + }); + + let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1); + let text = row.querySelector( + `.urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-text` + ); + + // The view's call to provider.getViewUpdate is async, so we need to make + // sure the update has been applied before continuing to avoid + // intermittent failures. + await TestUtils.waitForCondition( + () => text.getAttribute("searchString") == searchString + ); + + // The "searchString" attribute of these elements should be updated. + let elementNames = ["selectable", "text", "button1", "button2"]; + for (let name of elementNames) { + let element = row.querySelector( + `.urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-${name}` + ); + Assert.equal( + element.getAttribute("searchString"), + searchString, + 'element.getAttribute("searchString")' + ); + } + + // text.textContent should be updated. + Assert.equal( + text.textContent, + `result.payload.searchString is: ${searchString}`, + "text.textContent" + ); + + await UrlbarTestUtils.promisePopupClose(window); + } + }); +}); + +// Tests that selection correctly moves through buttons and selectables in a +// dynamic result. +add_task(async function selection() { + await withDynamicTypeProvider(async () => { + // Add a visit so we have at least one result after the dynamic result. + await PlacesUtils.history.clear(); + await PlacesTestUtils.addVisits("http://example.com/test"); + + // Do a search. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + waitForFocus: SimpleTest.waitForFocus, + }); + + // Sanity check that the dynamic result is at index 1. + let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1); + Assert.equal( + row.result.type, + UrlbarUtils.RESULT_TYPE.DYNAMIC, + "row.result.type" + ); + + // The heuristic result will be selected. Arrow down from the heuristic + // through all the selectable elements in the dynamic result. + let selectables = ["selectable", "button1", "button2"]; + for (let name of selectables) { + let element = row.querySelector( + `.urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-${name}` + ); + Assert.ok(element, "Sanity check element"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + Assert.equal( + UrlbarTestUtils.getSelectedElement(window), + element, + `Selected element: ${name}` + ); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 1, + "Row at index 1 selected" + ); + Assert.equal(UrlbarTestUtils.getSelectedRow(window), row, "Row selected"); + } + + // Arrow down again to select the result after the dynamic result. + EventUtils.synthesizeKey("KEY_ArrowDown"); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 2, + "Row at index 2 selected" + ); + Assert.notEqual( + UrlbarTestUtils.getSelectedRow(window), + row, + "Row is not selected" + ); + + // Arrow back up through the dynamic result. + for (let name of selectables.reverse()) { + let element = row.querySelector( + `.urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-${name}` + ); + Assert.ok(element, "Sanity check element"); + EventUtils.synthesizeKey("KEY_ArrowUp"); + Assert.equal( + UrlbarTestUtils.getSelectedElement(window), + element, + `Selected element: ${name}` + ); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 1, + "Row at index 1 selected" + ); + Assert.equal(UrlbarTestUtils.getSelectedRow(window), row, "Row selected"); + } + + // Arrow up again to select the heuristic result. + EventUtils.synthesizeKey("KEY_ArrowUp"); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 0, + "Row at index 0 selected" + ); + Assert.notEqual( + UrlbarTestUtils.getSelectedRow(window), + row, + "Row is not selected" + ); + + await UrlbarTestUtils.promisePopupClose(window); + await PlacesUtils.history.clear(); + }); +}); + +// Tests picking elements in a dynamic result. +add_task(async function pick() { + await withDynamicTypeProvider(async provider => { + let selectables = ["selectable", "button1", "button2"]; + for (let i = 0; i < selectables.length; i++) { + let selectable = selectables[i]; + + // Do a search. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + waitForFocus: SimpleTest.waitForFocus, + }); + + // Sanity check that the dynamic result is at index 1. + let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1); + Assert.equal( + row.result.type, + UrlbarUtils.RESULT_TYPE.DYNAMIC, + "row.result.type" + ); + + // The heuristic result will be selected. Arrow down from the heuristic + // to the selectable element. + let element = row.querySelector( + `.urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-${selectable}` + ); + Assert.ok(element, "Sanity check element"); + EventUtils.synthesizeKey("KEY_ArrowDown", { repeat: i + 1 }); + Assert.equal( + UrlbarTestUtils.getSelectedElement(window), + element, + `Selected element: ${name}` + ); + + // Pick the element. + let pickPromise = provider.promisePick(); + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Enter") + ); + let [result, pickedElement] = await pickPromise; + Assert.equal(result, row.result, "Picked result"); + Assert.equal(pickedElement, element, "Picked element"); + } + }); +}); + +// Tests picking elements in a dynamic result. +add_task(async function shouldNavigate() { + /** + * A dummy provider that providers results with a `shouldNavigate` property. + */ + class TestShouldNavigateProvider extends TestProvider { + /** + * @param {object} context - Data regarding the context of the query. + * @param {function} addCallback - Function to add a result to the query. + */ + async startQuery(context, addCallback) { + for (let result of this._results) { + result.payload.searchString = context.searchString; + result.payload.shouldNavigate = true; + result.payload.url = DUMMY_PAGE; + addCallback(this, result); + } + } + } + + await withDynamicTypeProvider(async provider => { + // Do a search. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + waitForFocus: SimpleTest.waitForFocus, + }); + + // Sanity check that the dynamic result is at index 1. + let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1); + Assert.equal( + row.result.type, + UrlbarUtils.RESULT_TYPE.DYNAMIC, + "row.result.type" + ); + + // The heuristic result will be selected. Arrow down from the heuristic + // to the selectable element. + let element = row.querySelector( + `.urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-selectable` + ); + Assert.ok(element, "Sanity check element"); + EventUtils.synthesizeKey("KEY_ArrowDown", { repeat: 1 }); + Assert.equal( + UrlbarTestUtils.getSelectedElement(window), + element, + `Selected element: ${name}` + ); + + // Pick the element. + let pickPromise = provider.promisePick(); + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Enter") + ); + // Verify that pickResult was still called. + let [result, pickedElement] = await pickPromise; + Assert.equal(result, row.result, "Picked result"); + Assert.equal(pickedElement, element, "Picked element"); + + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + is( + gBrowser.currentURI.spec, + DUMMY_PAGE, + "We navigated to payload.url when result selected" + ); + + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, "about:home"); + await BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + "about:home" + ); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + waitForFocus: SimpleTest.waitForFocus, + }); + + row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1); + element = row.querySelector( + `.urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-selectable` + ); + + pickPromise = provider.promisePick(); + EventUtils.synthesizeMouseAtCenter(element, {}); + [result, pickedElement] = await pickPromise; + Assert.equal(result, row.result, "Picked result"); + Assert.equal(pickedElement, element, "Picked element"); + + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + is( + gBrowser.currentURI.spec, + DUMMY_PAGE, + "We navigated to payload.url when result is clicked" + ); + }, new TestShouldNavigateProvider()); +}); + +// Tests applying highlighting to a dynamic result. +add_task(async function highlighting() { + /** + * Provides a dynamic result with highlighted text. + */ + class TestHighlightProvider extends TestProvider { + startQuery(context, addCallback) { + let result = Object.assign( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.DYNAMIC, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + ...UrlbarResult.payloadAndSimpleHighlights(context.tokens, { + dynamicType: DYNAMIC_TYPE_NAME, + text: ["Test title", UrlbarUtils.HIGHLIGHT.SUGGESTED], + }) + ), + { suggestedIndex: 1 } + ); + addCallback(this, result); + } + + getViewUpdate(result, idsByName) { + return {}; + } + } + + // Test that highlighting is applied. + await withDynamicTypeProvider(async () => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + waitForFocus: SimpleTest.waitForFocus, + }); + + let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1); + Assert.equal( + row.result.type, + UrlbarUtils.RESULT_TYPE.DYNAMIC, + "row.result.type" + ); + let parentTextNode = row.querySelector( + `.urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-text` + ); + let highlightedTextNode = row.querySelector( + `.urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-text > strong` + ); + Assert.equal(parentTextNode.firstChild.textContent, "Test"); + Assert.equal( + highlightedTextNode.textContent, + " title", + "The highlighting was applied successfully." + ); + }, new TestHighlightProvider()); + + /** + * Provides a dynamic result with highlighted text that is then overridden. + */ + class TestHighlightProviderOveridden extends TestHighlightProvider { + getViewUpdate(result, idsByName) { + return { + text: { + textContent: "Test title", + }, + }; + } + } + + // Test that highlighting is not applied when overridden from getViewUpdate. + await withDynamicTypeProvider(async () => { + // Do a search. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + waitForFocus: SimpleTest.waitForFocus, + }); + + let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1); + Assert.equal( + row.result.type, + UrlbarUtils.RESULT_TYPE.DYNAMIC, + "row.result.type" + ); + let parentTextNode = row.querySelector( + `.urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-text` + ); + let highlightedTextNode = row.querySelector( + `.urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-text > strong` + ); + Assert.equal( + parentTextNode.firstChild.textContent, + "Test title", + "No highlighting was applied" + ); + Assert.ok(!highlightedTextNode, "The <strong> child node was deleted."); + }, new TestHighlightProviderOveridden()); +}); + +/** + * Provides a dynamic result. + */ +class TestProvider extends UrlbarTestUtils.TestProvider { + constructor() { + super({ + results: [ + Object.assign( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.DYNAMIC, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + dynamicType: DYNAMIC_TYPE_NAME, + } + ), + { suggestedIndex: 1 } + ), + ], + }); + } + + async startQuery(context, addCallback) { + for (let result of this._results) { + result.payload.searchString = context.searchString; + addCallback(this, result); + } + } + + getViewUpdate(result, idsByName) { + for (let child of DYNAMIC_TYPE_VIEW_TEMPLATE.children) { + Assert.ok(idsByName.get(child.name), `idsByName contains ${child.name}`); + } + + return { + selectable: { + textContent: "Selectable", + attributes: { + searchString: result.payload.searchString, + }, + }, + text: { + textContent: `result.payload.searchString is: ${result.payload.searchString}`, + attributes: { + searchString: result.payload.searchString, + }, + }, + button1: { + textContent: "Button 1", + attributes: { + searchString: result.payload.searchString, + }, + }, + button2: { + textContent: "Button 2", + attributes: { + searchString: result.payload.searchString, + }, + }, + }; + } + + pickResult(result, element) { + if (this._pickPromiseResolve) { + this._pickPromiseResolve([result, element]); + delete this._pickPromiseResolve; + delete this._pickPromise; + } + } + + promisePick() { + this._pickPromise = new Promise(resolve => { + this._pickPromiseResolve = resolve; + }); + return this._pickPromise; + } +} + +/** + * Provides a dynamic result. + * @param {object} callback - Function that runs the body of the test. + * @param {object} provider - The dummy provider to use. + */ +async function withDynamicTypeProvider( + callback, + provider = new TestProvider() +) { + // Add a dynamic result type. + UrlbarResult.addDynamicResultType(DYNAMIC_TYPE_NAME); + UrlbarView.addDynamicViewTemplate( + DYNAMIC_TYPE_NAME, + DYNAMIC_TYPE_VIEW_TEMPLATE + ); + + // Add a provider of the dynamic type. + UrlbarProvidersManager.registerProvider(provider); + + await callback(provider); + + // Clean up. + UrlbarProvidersManager.unregisterProvider(provider); + UrlbarView.removeDynamicViewTemplate(DYNAMIC_TYPE_NAME); + UrlbarResult.removeDynamicResultType(DYNAMIC_TYPE_NAME); +} + +function checkDOM(parentNode, expectedChildren) { + info( + `checkDOM: Checking parentNode id=${parentNode.id} className=${parentNode.className}` + ); + for (let i = 0; i < expectedChildren.length; i++) { + let child = expectedChildren[i]; + let actualChild = parentNode.children[i]; + info(`checkDOM: Checking expected child: ${JSON.stringify(child)}`); + Assert.ok(actualChild, "actualChild should exist"); + Assert.equal(actualChild.tagName, child.tag, "child.tag"); + Assert.equal(actualChild.getAttribute("name"), child.name, "child.name"); + Assert.ok( + actualChild.classList.contains( + `urlbarView-dynamic-${DYNAMIC_TYPE_NAME}-${child.name}` + ), + "child.name should be in classList" + ); + // We have to use startsWith/endsWith since the middle of the ID is a random + // number. + Assert.ok(actualChild.id.startsWith("urlbarView-row-")); + Assert.ok( + actualChild.id.endsWith(child.name), + "The child was assigned the correct ID." + ); + for (let [name, value] of Object.entries(child.attributes || {})) { + Assert.equal(actualChild.getAttribute(name), value, `attribute: ${name}`); + } + for (let name of child.classList || []) { + Assert.ok(actualChild.classList.contains(name), `classList: ${name}`); + } + if (child.children) { + checkDOM(actualChild, child.children); + } + } +} diff --git a/browser/components/urlbar/tests/browser/browser_edit_invalid_url.js b/browser/components/urlbar/tests/browser/browser_edit_invalid_url.js new file mode 100644 index 0000000000..bbebc0f940 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_edit_invalid_url.js @@ -0,0 +1,91 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +// Checks that we trim invalid urls when they are selected, so that if the user +// modifies the selected url, or just closes the results pane, we do a visit +// rather than searching for the trimmed string. + +const url = BrowserUtils.trimURLProtocol + "invalid.somehost/mytest"; + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.trimURLs", true]], + }); + await PlacesTestUtils.addVisits(url); + registerCleanupFunction(PlacesUtils.history.clear); +}); + +add_task(async function test_escape() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "invalid", + }); + // Look for our result. + let resultCount = UrlbarTestUtils.getResultCount(window); + Assert.greater(resultCount, 1, "There should be at least two results"); + for (let i = 0; i < resultCount; i++) { + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + info(`Result at ${i} has url ${result.url}`); + if (result.url.startsWith(url)) { + break; + } + EventUtils.synthesizeKey("KEY_ArrowDown"); + } + Assert.equal( + gURLBar.value, + url, + "The string displayed in the textbox should be the untrimmed url" + ); + // Close the results pane by ESC. + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_Escape"); + }); + // Confirm the result and check the loaded page. + let promise = waitforLoadURL(); + EventUtils.synthesizeKey("KEY_Enter"); + let loadedUrl = await promise; + Assert.equal(loadedUrl, url, "Should try to load a url"); +}); + +add_task(async function test_edit_url() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "invalid", + }); + // Look for our result. + let resultCount = UrlbarTestUtils.getResultCount(window); + Assert.greater(resultCount, 1, "There should be at least two results"); + for (let i = 1; i < resultCount; i++) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + info(`Result at ${i} has url ${result.url}`); + if (result.url.startsWith(url)) { + break; + } + } + Assert.equal( + gURLBar.value, + url, + "The string displayed in the textbox should be the untrimmed url" + ); + // Modify the url. + EventUtils.synthesizeKey("2"); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.URL, "Should visit a url"); + Assert.equal(result.url, url + "2", "Should visit the modified url"); + + // Confirm the result and check the loaded page. + let promise = waitforLoadURL(); + EventUtils.synthesizeKey("KEY_Enter"); + let loadedUrl = await promise; + Assert.equal(loadedUrl, url + "2", "Should try to load the modified url"); +}); + +async function waitforLoadURL() { + let sandbox = sinon.createSandbox(); + let loadedUrl = await new Promise(resolve => + sandbox.stub(gURLBar, "_loadURL").callsFake(resolve) + ); + sandbox.restore(); + return loadedUrl; +} diff --git a/browser/components/urlbar/tests/browser/browser_enter.js b/browser/components/urlbar/tests/browser/browser_enter.js new file mode 100644 index 0000000000..567ccfa834 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_enter.js @@ -0,0 +1,268 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_VALUE = "example.com/\xF7?\xF7"; +const START_VALUE = "example.com/%C3%B7?%C3%B7"; + +add_task(async function setup() { + const engine = await SearchTestUtils.promiseNewSearchEngine( + getRootDirectory(gTestPath) + "searchSuggestionEngine.xml" + ); + engine.alias = "@default"; + + const defaultEngine = Services.search.defaultEngine; + Services.search.defaultEngine = engine; + + registerCleanupFunction(async function() { + Services.search.defaultEngine = defaultEngine; + }); +}); + +add_task(async function returnKeypress() { + info("Simple return keypress"); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, START_VALUE); + + gURLBar.focus(); + EventUtils.synthesizeKey("KEY_Enter"); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + + // Check url bar and selected tab. + is( + gURLBar.value, + TEST_VALUE, + "Urlbar should preserve the value on return keypress" + ); + is(gBrowser.selectedTab, tab, "New URL was loaded in the current tab"); + + // Cleanup. + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function altReturnKeypress() { + info("Alt+Return keypress"); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, START_VALUE); + + let tabOpenPromise = BrowserTestUtils.waitForEvent( + gBrowser.tabContainer, + "TabOpen" + ); + gURLBar.focus(); + EventUtils.synthesizeKey("KEY_Enter", { altKey: true }); + + // wait for the new tab to appear. + await tabOpenPromise; + + // Check url bar and selected tab. + is( + gURLBar.value, + TEST_VALUE, + "Urlbar should preserve the value on return keypress" + ); + isnot(gBrowser.selectedTab, tab, "New URL was loaded in a new tab"); + + // Cleanup. + BrowserTestUtils.removeTab(tab); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function altGrReturnKeypress() { + info("AltGr+Return keypress"); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, START_VALUE); + + let tabOpenPromise = BrowserTestUtils.waitForEvent( + gBrowser.tabContainer, + "TabOpen" + ); + gURLBar.focus(); + EventUtils.synthesizeKey("KEY_Enter", { altGraphKey: true }); + + // wait for the new tab to appear. + await tabOpenPromise; + + // Check url bar and selected tab. + is( + gURLBar.value, + TEST_VALUE, + "Urlbar should preserve the value on return keypress" + ); + isnot(gBrowser.selectedTab, tab, "New URL was loaded in a new tab"); + + // Cleanup. + BrowserTestUtils.removeTab(tab); + BrowserTestUtils.removeTab(gBrowser.selectedTab); +}); + +add_task(async function searchOnEnterNoPick() { + info("Search on Enter without picking a urlbar result"); + + // Why is BrowserTestUtils.openNewForegroundTab not causing the bug? + let promiseTabOpened = BrowserTestUtils.waitForEvent( + gBrowser.tabContainer, + "TabOpen" + ); + EventUtils.synthesizeMouseAtCenter(gBrowser.tabContainer.newTabButton, {}); + let openEvent = await promiseTabOpened; + let tab = openEvent.target; + + let loadPromise = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + null, + true + ); + gURLBar.focus(); + gURLBar.value = "test test"; + EventUtils.synthesizeKey("KEY_Enter"); + await loadPromise; + + Assert.ok( + gBrowser.selectedBrowser.currentURI.spec.endsWith("test+test"), + "Should have loaded the correct page" + ); + Assert.equal( + gBrowser.selectedBrowser.currentURI.spec, + gURLBar.untrimmedValue, + "The location should have changed" + ); + + // Cleanup. + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function searchOnEnterSoon() { + info("Search on Enter as soon as typing a char"); + const tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + START_VALUE + ); + + const onLoad = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + const onBeforeUnload = SpecialPowers.spawn( + gBrowser.selectedBrowser, + [], + () => { + return new Promise(resolve => { + content.window.addEventListener("beforeunload", () => { + resolve(); + }); + }); + } + ); + const onResult = SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + return new Promise(resolve => { + content.window.addEventListener("keyup", () => { + resolve("keyup"); + }); + content.window.addEventListener("unload", () => { + resolve("unload"); + }); + }); + }); + + // Focus on the input field in urlbar. + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + const ownerDocument = gBrowser.selectedBrowser.ownerDocument; + is( + ownerDocument.activeElement, + gURLBar.inputField, + "The input field in urlbar has focus" + ); + + info("Keydown a char and Enter"); + EventUtils.synthesizeKey("x", { type: "keydown" }); + EventUtils.synthesizeKey("KEY_Enter", { type: "keydown" }); + + // Wait for beforeUnload event in the content. + await onBeforeUnload; + is( + ownerDocument.activeElement, + gURLBar.inputField, + "The input field in urlbar still has focus" + ); + + // Check the caret position. + Assert.equal( + gURLBar.selectionStart, + gURLBar.value.length, + "The selectionStart indicates at ending of the value" + ); + Assert.equal( + gURLBar.selectionEnd, + gURLBar.value.length, + "The selectionEnd indicates at ending of the value" + ); + + // Keyup both key as soon as beforeUnload event happens. + EventUtils.synthesizeKey("x", { type: "keyup" }); + EventUtils.synthesizeKey("KEY_Enter", { type: "keyup" }); + + // Wait for moving the focus. + await TestUtils.waitForCondition( + () => ownerDocument.activeElement === gBrowser.selectedBrowser + ); + info("The focus is moved to the browser"); + + // Check whether keyup event is not captured before unload event happens. + const result = await onResult; + is(result, "unload", "Keyup event is not captured."); + + // Check the caret position again. + Assert.equal( + gURLBar.selectionStart, + 0, + "The selectionStart indicates at beginning of the value" + ); + Assert.equal( + gURLBar.selectionEnd, + 0, + "The selectionEnd indicates at beginning of the value" + ); + + // Cleanup. + await onLoad; + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function searchByMultipleEnters() { + info("Search on Enter after selecting the search engine by Enter"); + const tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + START_VALUE + ); + + info("Select a search engine by Enter key"); + gURLBar.focus(); + gURLBar.select(); + EventUtils.sendString("@default"); + EventUtils.synthesizeKey("KEY_Enter"); + await TestUtils.waitForCondition( + () => gURLBar.searchMode, + "Wait until entering search mode" + ); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: "browser_searchSuggestionEngine searchSuggestionEngine.xml", + entry: "keywordoffer", + }); + const ownerDocument = gBrowser.selectedBrowser.ownerDocument; + is( + ownerDocument.activeElement, + gURLBar.inputField, + "The input field in urlbar has focus" + ); + + info("Search by Enter key"); + const onLoad = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + EventUtils.sendString("mozilla"); + EventUtils.synthesizeKey("KEY_Enter"); + await onLoad; + is( + ownerDocument.activeElement, + gBrowser.selectedBrowser, + "The focus is moved to the browser" + ); + + // Cleanup. + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_enterAfterMouseOver.js b/browser/components/urlbar/tests/browser/browser_enterAfterMouseOver.js new file mode 100644 index 0000000000..3629774c73 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_enterAfterMouseOver.js @@ -0,0 +1,97 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that enter works correctly after a mouse over. + */ + +function repeat(limit, func) { + for (let i = 0; i < limit; i++) { + func(i); + } +} + +async function promiseAutoComplete(inputText) { + gURLBar.focus(); + gURLBar.value = inputText.slice(0, -1); + EventUtils.sendString(inputText.slice(-1)); + await UrlbarTestUtils.promiseSearchComplete(window); +} + +function assertSelected(index) { + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + index, + "Should have the correct index selected" + ); +} + +let gMaxResults; + +add_task(async function() { + registerCleanupFunction(async function() { + await PlacesUtils.history.clear(); + }); + + await PlacesUtils.history.clear(); + + gMaxResults = Services.prefs.getIntPref("browser.urlbar.maxRichResults"); + + let visits = []; + repeat(gMaxResults, i => { + visits.push({ + uri: makeURI("http://example.com/autocomplete/?" + i), + }); + }); + await PlacesTestUtils.addVisits(visits); + + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank"); + await promiseAutoComplete("http://example.com/autocomplete/"); + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + gMaxResults, + "Should have got the correct amount of results" + ); + + let initiallySelected = UrlbarTestUtils.getSelectedRowIndex(window); + + info("Key Down to select the next item"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + assertSelected(initiallySelected + 1); + + let result = await UrlbarTestUtils.getDetailsOfResultAt( + window, + initiallySelected + 1 + ); + let expectedURL = result.url; + + Assert.equal( + gURLBar.untrimmedValue, + expectedURL, + "Value in the URL bar should be updated by keyboard selection" + ); + + // Verify that what we're about to do changes the selectedIndex: + Assert.notEqual( + initiallySelected + 1, + 3, + "Shouldn't be changing the selectedIndex to the same index we keyboard-selected." + ); + + let element = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 3); + EventUtils.synthesizeMouseAtCenter(element, { type: "mousemove" }); + + await UrlbarTestUtils.promisePopupClose(window, async () => { + let openedExpectedPage = BrowserTestUtils.waitForDocLoadAndStopIt( + expectedURL, + gBrowser.selectedBrowser + ); + EventUtils.synthesizeKey("KEY_Enter"); + await openedExpectedPage; + }); + + gBrowser.removeCurrentTab(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_focusedCmdK.js b/browser/components/urlbar/tests/browser/browser_focusedCmdK.js new file mode 100644 index 0000000000..c03689d392 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_focusedCmdK.js @@ -0,0 +1,15 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(async function() { + // Test that Ctrl/Cmd + K will focus the url bar + let focusPromise = BrowserTestUtils.waitForEvent(gURLBar, "focus"); + document.documentElement.focus(); + EventUtils.synthesizeKey("k", { accelKey: true }); + await focusPromise; + Assert.equal( + document.activeElement, + gURLBar.inputField, + "URL Bar should be focused" + ); +}); diff --git a/browser/components/urlbar/tests/browser/browser_handleCommand_fallback.js b/browser/components/urlbar/tests/browser/browser_handleCommand_fallback.js new file mode 100644 index 0000000000..51cec4e8be --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_handleCommand_fallback.js @@ -0,0 +1,151 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that the fallback paths of handleCommand (no view and no previous + * result) work consistently against the normal case of picking the heuristic + * result. + */ + +const TEST_STRINGS = [ + "test", + "test/", + "test.com", + "test.invalid", + "moz", + "moz test", + "@moz test", + "keyword", + "keyword test", + "test/test/", + "test /test/", +]; + +add_task(async function() { + // Disable autofill so mozilla.org isn't autofilled below. + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.autoFill", false]], + }); + + sandbox = sinon.createSandbox(); + let engine = await Services.search.addEngineWithDetails("MozSearch", { + alias: "moz", + method: "GET", + template: "http://example.com/?q={searchTerms}", + }); + let engine2 = await Services.search.addEngineWithDetails("MozSearch2", { + alias: "@moz", + method: "GET", + template: "http://example.com/?q={searchTerms}", + }); + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.com/?q=%s", + title: "test", + }); + await PlacesUtils.keywords.insert({ + keyword: "keyword", + url: "http://example.com/?q=%s", + }); + registerCleanupFunction(async () => { + sandbox.restore(); + await Services.search.removeEngine(engine); + await Services.search.removeEngine(engine2); + await PlacesUtils.bookmarks.remove(bm); + await UrlbarTestUtils.formHistory.clear(); + }); + + async function promiseLoadURL() { + return new Promise(resolve => { + sandbox.stub(gURLBar, "_loadURL").callsFake(function() { + sandbox.restore(); + // The last arguments are optional and apply only to some cases, so we + // could not use deepEqual with them. + resolve(Array.from(arguments).slice(0, 3)); + }); + }); + } + + // Run the string through a normal search where the user types the string + // and confirms the heuristic result, store the arguments to _loadURL, then + // confirm the same string without a view and without an input event, and + // compare the arguments. + for (let value of TEST_STRINGS) { + info("Input the value normally and Enter."); + let promise = promiseLoadURL(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value, + }); + EventUtils.synthesizeKey("KEY_Enter"); + let args = await promise; + Assert.ok(args.length, "Sanity check"); + info("Close the panel and confirm again."); + promise = promiseLoadURL(); + await UrlbarTestUtils.promisePopupClose(window); + EventUtils.synthesizeKey("KEY_Enter"); + Assert.deepEqual(await promise, args, "Check arguments are coherent"); + + info("Set the value directly and Enter."); + // To properly testing the original value we must be out of search mode. + if (gURLBar.searchMode) { + await UrlbarTestUtils.exitSearchMode(window); + // Exiting search mode may reopen the panel. + await UrlbarTestUtils.promisePopupClose(window); + } + promise = promiseLoadURL(); + gURLBar.value = value; + let spy = sinon.spy(UrlbarUtils, "getHeuristicResultFor"); + EventUtils.synthesizeKey("KEY_Enter"); + spy.restore(); + Assert.ok(spy.called, "invoked getHeuristicResultFor"); + Assert.deepEqual(await promise, args, "Check arguments are coherent"); + gURLBar.handleRevert(); + } +}); + +// This is testing the final fallback case that may happen when we can't +// get a heuristic result, maybe because the Places database is corrupt. +add_task(async function no_heuristic_test() { + sandbox = sinon.createSandbox(); + + let stub = sandbox + .stub(UrlbarUtils, "getHeuristicResultFor") + .callsFake(async function() { + throw new Error("I failed!"); + }); + + registerCleanupFunction(async () => { + sandbox.restore(); + await UrlbarTestUtils.formHistory.clear(); + }); + + async function promiseLoadURL() { + return new Promise(resolve => { + sandbox.stub(gURLBar, "_loadURL").callsFake(function() { + sandbox.restore(); + // The last arguments are optional and apply only to some cases, so we + // could not use deepEqual with them. + resolve(Array.from(arguments).slice(0, 3)); + }); + }); + } + + // Run the string through a normal search where the user types the string + // and confirms the heuristic result, store the arguments to _loadURL, then + // confirm the same string without a view and without an input event, and + // compare the arguments. + for (let value of TEST_STRINGS) { + // To properly testing the original value we must be out of search mode. + if (gURLBar.searchMode) { + await UrlbarTestUtils.exitSearchMode(window); + } + let promise = promiseLoadURL(); + gURLBar.value = value; + EventUtils.synthesizeKey("KEY_Enter"); + Assert.ok(stub.called, "invoked getHeuristicResultFor"); + // The first argument to _loadURL should always be a valid url, so this + // should never throw. + new URL((await promise)[0]); + } +}); diff --git a/browser/components/urlbar/tests/browser/browser_hashChangeProxyState.js b/browser/components/urlbar/tests/browser/browser_hashChangeProxyState.js new file mode 100644 index 0000000000..d6d0cf0e3c --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_hashChangeProxyState.js @@ -0,0 +1,148 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Check that navigating through both the URL bar and using in-page hash- or ref- + * based links and back or forward navigation updates the URL bar and identity block correctly. + */ +add_task(async function() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.navigation.requireUserInteraction", false]], + }); + let baseURL = `${TEST_BASE_URL}dummy_page.html`; + let url = baseURL + "#foo"; + await BrowserTestUtils.withNewTab({ gBrowser, url }, async function(browser) { + let identityBox = document.getElementById("identity-box"); + let expectedURL = url; + + let verifyURLBarState = testType => { + is( + gURLBar.value, + expectedURL, + "URL bar visible value should be correct " + testType + ); + is( + gURLBar.untrimmedValue, + expectedURL, + "URL bar value should be correct " + testType + ); + ok( + identityBox.classList.contains("verifiedDomain"), + "Identity box should know we're doing SSL " + testType + ); + is( + gURLBar.getAttribute("pageproxystate"), + "valid", + "URL bar is in valid page proxy state" + ); + }; + + verifyURLBarState("at the beginning"); + + let locationChangePromise; + let resolveLocationChangePromise; + let expectURL = urlTemp => { + expectedURL = urlTemp; + locationChangePromise = new Promise( + r => (resolveLocationChangePromise = r) + ); + }; + let wpl = { + onLocationChange(unused, unused2, location) { + is(location.spec, expectedURL, "Got the expected URL"); + resolveLocationChangePromise(); + }, + }; + gBrowser.addProgressListener(wpl); + + expectURL(baseURL + "#foo"); + gURLBar.select(); + EventUtils.sendKey("return"); + + await locationChangePromise; + verifyURLBarState("after hitting enter on the same URL a second time"); + + expectURL(baseURL + "#bar"); + gURLBar.value = expectedURL; + gURLBar.select(); + EventUtils.sendKey("return"); + + await locationChangePromise; + verifyURLBarState("after a URL bar hash navigation"); + + expectURL(baseURL + "#foo"); + await SpecialPowers.spawn(browser, [], function() { + let a = content.document.createElement("a"); + a.href = "#foo"; + a.textContent = "Foo Link"; + content.document.body.appendChild(a); + a.click(); + }); + + await locationChangePromise; + verifyURLBarState("after a page link hash navigation"); + + expectURL(baseURL + "#bar"); + gBrowser.goBack(); + + await locationChangePromise; + verifyURLBarState("after going back"); + + expectURL(baseURL + "#foo"); + gBrowser.goForward(); + + await locationChangePromise; + verifyURLBarState("after going forward"); + + expectURL(baseURL + "#foo"); + gURLBar.select(); + EventUtils.sendKey("return"); + + await locationChangePromise; + verifyURLBarState("after hitting enter on the same URL"); + + gBrowser.removeProgressListener(wpl); + }); +}); + +/** + * Check that initial secure loads that swap remoteness + * get the correct page icon when finished. + */ +add_task(async function() { + // Ensure there's no preloaded newtab browser, since that'll not fire a load event. + NewTabPagePreloading.removePreloadedBrowser(window); + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:newtab" + ); + let url = `${TEST_BASE_URL}dummy_page.html#foo`; + gURLBar.value = url; + gURLBar.select(); + EventUtils.sendKey("return"); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + is( + gURLBar.value, + url, + "URL bar visible value should be correct when the page loads from about:newtab" + ); + is( + gURLBar.untrimmedValue, + url, + "URL bar value should be correct when the page loads from about:newtab" + ); + let identityBox = document.getElementById("identity-box"); + ok( + identityBox.classList.contains("verifiedDomain"), + "Identity box should know we're doing SSL when the page loads from about:newtab" + ); + is( + gURLBar.getAttribute("pageproxystate"), + "valid", + "URL bar is in valid page proxy state when SSL page with hash loads from about:newtab" + ); + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_heuristicNotAddedFirst.js b/browser/components/urlbar/tests/browser/browser_heuristicNotAddedFirst.js new file mode 100644 index 0000000000..7a25235d34 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_heuristicNotAddedFirst.js @@ -0,0 +1,165 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// When the heuristic result is not the first result added, it should still be +// selected. + +"use strict"; + +// When the heuristic result is not the first result added, it should still be +// selected. +add_task(async function slowHeuristicSelected() { + // First, add a provider that adds a heuristic result on a delay. Both this + // provider and the one below have a high priority so that only they are used + // during the test. + let engine = await Services.search.getDefault(); + let heuristicResult = new UrlbarResult( + UrlbarUtils.RESULT_TYPE.SEARCH, + UrlbarUtils.RESULT_SOURCE.SEARCH, + { + suggestion: "test", + engine: engine.name, + } + ); + heuristicResult.heuristic = true; + let heuristicProvider = new UrlbarTestUtils.TestProvider({ + results: [heuristicResult], + name: "heuristicProvider", + priority: Infinity, + addTimeout: 500, + }); + UrlbarProvidersManager.registerProvider(heuristicProvider); + + // Second, add another provider that adds a non-heuristic result immediately + // with suggestedIndex = 1. + let nonHeuristicResult = new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TIP, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + text: "This is a test tip.", + buttonText: "Done", + type: "test", + helpUrl: "http://example.com/", + } + ); + nonHeuristicResult.suggestedIndex = 1; + let nonHeuristicProvider = new UrlbarTestUtils.TestProvider({ + results: [nonHeuristicResult], + name: "nonHeuristicProvider", + priority: Infinity, + }); + UrlbarProvidersManager.registerProvider(nonHeuristicProvider); + + // Do a search. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + value: "test", + window, + }); + + // The first result should be the heuristic and it should be selected. + let actualHeuristic = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal(actualHeuristic.type, UrlbarUtils.RESULT_TYPE.SEARCH); + Assert.equal( + UrlbarTestUtils.getSelectedElement(window), + actualHeuristic.element.row + ); + Assert.equal(UrlbarTestUtils.getSelectedElementIndex(window), 0); + + // Check the second result for good measure. + let actualNonHeuristic = await UrlbarTestUtils.getDetailsOfResultAt( + window, + 1 + ); + Assert.equal(actualNonHeuristic.type, UrlbarUtils.RESULT_TYPE.TIP); + + await UrlbarTestUtils.promisePopupClose(window); + UrlbarProvidersManager.unregisterProvider(heuristicProvider); + UrlbarProvidersManager.unregisterProvider(nonHeuristicProvider); +}); + +// When the heuristic result is not the first result added but a one-off search +// button is already selected, the heuristic result should not steal the +// selection from the one-off button. +add_task(async function oneOffRemainsSelected() { + // First, add a provider that adds a heuristic result on a delay. Both this + // provider and the one below have a high priority so that only they are used + // during the test. + let engine = await Services.search.getDefault(); + let heuristicResult = new UrlbarResult( + UrlbarUtils.RESULT_TYPE.SEARCH, + UrlbarUtils.RESULT_SOURCE.SEARCH, + { + suggestion: "test", + engine: engine.name, + } + ); + heuristicResult.heuristic = true; + let heuristicProvider = new UrlbarTestUtils.TestProvider({ + results: [heuristicResult], + name: "heuristicProvider", + priority: Infinity, + addTimeout: 500, + }); + UrlbarProvidersManager.registerProvider(heuristicProvider); + + // Second, add another provider that adds a non-heuristic result immediately + // with suggestedIndex = 1. + let nonHeuristicResult = new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TIP, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + text: "This is a test tip.", + buttonText: "Done", + type: "test", + helpUrl: "http://example.com/", + } + ); + nonHeuristicResult.suggestedIndex = 1; + let nonHeuristicProvider = new UrlbarTestUtils.TestProvider({ + results: [nonHeuristicResult], + name: "nonHeuristicProvider", + priority: Infinity, + }); + UrlbarProvidersManager.registerProvider(nonHeuristicProvider); + + // Do a search but don't wait for it to finish. + let searchPromise = UrlbarTestUtils.promiseAutocompleteResultPopup({ + value: "test", + window, + }); + + // When the view opens, press the up arrow key to select the one-off search + // settings button. There's no point in selecting instead the non-heuristic + // result because once we do that, the search is canceled, and the heuristic + // result will never be added. + await UrlbarTestUtils.promisePopupOpen(window, () => {}); + EventUtils.synthesizeKey("KEY_ArrowUp"); + + // Wait for the search to finish. + await searchPromise; + + // The first result should be the heuristic. + let actualHeuristic = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal(actualHeuristic.type, UrlbarUtils.RESULT_TYPE.SEARCH); + + // Check the second result for good measure. + let actualNonHeuristic = await UrlbarTestUtils.getDetailsOfResultAt( + window, + 1 + ); + Assert.equal(actualNonHeuristic.type, UrlbarUtils.RESULT_TYPE.TIP); + + // No result should be selected. + Assert.equal(UrlbarTestUtils.getSelectedElement(window), null); + Assert.equal(UrlbarTestUtils.getSelectedElementIndex(window), -1); + + // The one-off settings button should be selected. + Assert.equal( + gURLBar.view.oneOffSearchButtons.selectedButton, + gURLBar.view.oneOffSearchButtons.settingsButtonCompact + ); + + await UrlbarTestUtils.promisePopupClose(window); + UrlbarProvidersManager.unregisterProvider(heuristicProvider); + UrlbarProvidersManager.unregisterProvider(nonHeuristicProvider); +}); diff --git a/browser/components/urlbar/tests/browser/browser_ime_composition.js b/browser/components/urlbar/tests/browser/browser_ime_composition.js new file mode 100644 index 0000000000..94df57fc7f --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_ime_composition.js @@ -0,0 +1,287 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests ime composition handling. + +function composeAndCheckPanel(string, isPopupOpen) { + EventUtils.synthesizeCompositionChange({ + composition: { + string, + clauses: [ + { + length: string.length, + attr: Ci.nsITextInputProcessor.ATTR_RAW_CLAUSE, + }, + ], + }, + caret: { start: string.length, length: 0 }, + key: { key: string ? string[string.length - 1] : "KEY_Backspace" }, + }); + Assert.equal( + UrlbarTestUtils.isPopupOpen(window), + isPopupOpen, + "Check panel open state" + ); +} + +add_task(async function() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.tabToSearch.onboard.interactionsLeft", 0]], + }); + + await PlacesUtils.history.clear(); + // Add at least one typed entry for the empty results set. Also clear history + // so that this can be over the autofill threshold. + await PlacesTestUtils.addVisits({ + uri: "http://mozilla.org/", + transition: PlacesUtils.history.TRANSITIONS.TYPED, + }); + // Add a bookmark to ensure we autofill the engine domain for tab-to-search. + let bm = await PlacesUtils.bookmarks.insert({ + url: "http://example.com/", + parentGuid: PlacesUtils.bookmarks.menuGuid, + }); + let engine = await Services.search.addEngineWithDetails("Test", { + alias: "@test", + template: `http://example.com/?search={searchTerms}`, + }); + let originalEngine = await Services.search.getDefault(); + await Services.search.setDefault(engine); + + registerCleanupFunction(async () => { + await Services.search.setDefault(originalEngine); + await Services.search.removeEngine(engine); + await PlacesUtils.bookmarks.remove(bm); + await PlacesUtils.history.clear(); + }); + + // Test both pref values. + for (let val of [true, false]) { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.imeCompositionClosesPanel", val]], + }); + await test_composition(val); + await test_composition_searchMode_preview(val); + await test_composition_tabToSearch(val); + } +}); + +async function test_composition(compositionClosesPanel) { + gURLBar.focus(); + await UrlbarTestUtils.promisePopupClose(window); + + info("Check the panel state during composition"); + composeAndCheckPanel("I", false); + Assert.equal(gURLBar.value, "I", "Check urlbar value"); + composeAndCheckPanel("In", false); + Assert.equal(gURLBar.value, "In", "Check urlbar value"); + + info("Committing composition should open the panel."); + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeComposition({ + type: "compositioncommitasis", + key: { key: "KEY_Enter" }, + }); + }); + Assert.equal(gURLBar.value, "In", "Check urlbar value"); + + info("Check the panel state starting from an open panel."); + Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Popup should be open"); + composeAndCheckPanel("t", !compositionClosesPanel); + Assert.equal(gURLBar.value, "Int", "Check urlbar value"); + composeAndCheckPanel("te", !compositionClosesPanel); + Assert.equal(gURLBar.value, "Inte", "Check urlbar value"); + + // Committing composition should open the popup. + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeComposition({ + type: "compositioncommitasis", + key: { key: "KEY_Enter" }, + }); + }); + Assert.equal(gURLBar.value, "Inte", "Check urlbar value"); + + info("If composition is cancelled, the value shouldn't be changed."); + Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Popup should be open"); + composeAndCheckPanel("r", !compositionClosesPanel); + Assert.equal(gURLBar.value, "Inter", "Check urlbar value"); + composeAndCheckPanel("", !compositionClosesPanel); + Assert.equal(gURLBar.value, "Inte", "Check urlbar value"); + // Canceled compositionend should reopen the popup. + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeComposition({ + type: "compositioncommit", + data: "", + key: { key: "KEY_Escape" }, + }); + }); + Assert.equal(gURLBar.value, "Inte", "Check urlbar value"); + + info( + "If composition replaces some characters and canceled, the search string should be the latest value." + ); + Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Popup should be open"); + EventUtils.synthesizeKey("VK_LEFT", { shiftKey: true }); + EventUtils.synthesizeKey("VK_LEFT", { shiftKey: true }); + composeAndCheckPanel("t", !compositionClosesPanel); + Assert.equal(gURLBar.value, "Int", "Check urlbar value"); + composeAndCheckPanel("te", !compositionClosesPanel); + Assert.equal(gURLBar.value, "Inte", "Check urlbar value"); + composeAndCheckPanel("", !compositionClosesPanel); + Assert.equal(gURLBar.value, "In", "Check urlbar value"); + + // Canceled compositionend should search the result with the latest value. + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeComposition({ + type: "compositioncommitasis", + key: { key: "KEY_Escape" }, + }); + }); + Assert.equal(gURLBar.value, "In", "Check urlbar value"); + + Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Popup should be open"); + info( + "Removing all characters should leave the popup open, Esc should then close it." + ); + EventUtils.synthesizeKey("KEY_Backspace", {}); + EventUtils.synthesizeKey("KEY_Backspace", {}); + Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Popup should be open"); + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_Escape", {}); + }); + Assert.equal(gURLBar.value, "", "Check urlbar value"); + + info("Composition which is canceled shouldn't cause opening the popup."); + Assert.ok(!UrlbarTestUtils.isPopupOpen(window), "Popup should be closed"); + composeAndCheckPanel("I", false); + Assert.equal(gURLBar.value, "I", "Check urlbar value"); + composeAndCheckPanel("In", false); + Assert.equal(gURLBar.value, "In", "Check urlbar value"); + composeAndCheckPanel("", false); + Assert.equal(gURLBar.value, "", "Check urlbar value"); + + info("Canceled compositionend shouldn't open the popup if it was closed."); + await UrlbarTestUtils.promisePopupClose(window); + EventUtils.synthesizeComposition({ + type: "compositioncommitasis", + key: { key: "KEY_Escape" }, + }); + Assert.ok(!UrlbarTestUtils.isPopupOpen(window), "Popup should be closed"); + Assert.equal(gURLBar.value, "", "Check urlbar value"); + + info("Down key should open the popup even if the editor is empty."); + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeKey("KEY_ArrowDown", {}); + }); + Assert.equal(gURLBar.value, "", "Check urlbar value"); + + info( + "If popup is open at starting composition, the popup should be reopened after composition anyway." + ); + Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Popup should be open"); + composeAndCheckPanel("I", !compositionClosesPanel); + Assert.equal(gURLBar.value, "I", "Check urlbar value"); + composeAndCheckPanel("In", !compositionClosesPanel); + Assert.equal(gURLBar.value, "In", "Check urlbar value"); + composeAndCheckPanel("", !compositionClosesPanel); + Assert.equal(gURLBar.value, "", "Check urlbar value"); + // A canceled compositionend should open the popup if it was open. + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeComposition({ + type: "compositioncommitasis", + key: { key: "KEY_Escape" }, + }); + }); + Assert.equal(gURLBar.value, "", "Check urlbar value"); + + info("Type normally, and hit escape, the popup should be closed."); + Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Popup should be open"); + EventUtils.synthesizeKey("I", {}); + EventUtils.synthesizeKey("n", {}); + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_Escape", {}); + }); + Assert.equal(gURLBar.value, "In", "Check urlbar value"); + // Clear typed chars. + EventUtils.synthesizeKey("KEY_Backspace", {}); + EventUtils.synthesizeKey("KEY_Backspace", {}); + Assert.equal(gURLBar.value, "", "Check urlbar value"); + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_Escape", {}); + }); + + info("With autofill, compositionstart shouldn't open the popup"); + Assert.ok(!UrlbarTestUtils.isPopupOpen(window), "Popup should be closed"); + composeAndCheckPanel("M", false); + Assert.equal(gURLBar.value, "M", "Check urlbar value"); + composeAndCheckPanel("Mo", false); + Assert.equal(gURLBar.value, "Mo", "Check urlbar value"); + // Committing composition should open the popup. + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeComposition({ + type: "compositioncommitasis", + key: { key: "KEY_Enter" }, + }); + }); + Assert.equal(gURLBar.value, "Mozilla.org/", "Check urlbar value"); +} + +async function test_composition_searchMode_preview(compositionClosesPanel) { + info("Check Search Mode preview is retained by composition"); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "@", + }); + + while (gURLBar.searchMode?.engineName != "Test") { + EventUtils.synthesizeKey("KEY_ArrowDown", {}, window); + } + let expectedSearchMode = { + engineName: "Test", + isPreview: true, + entry: "keywordoffer", + }; + await UrlbarTestUtils.assertSearchMode(window, expectedSearchMode); + composeAndCheckPanel("I", !compositionClosesPanel); + Assert.equal(gURLBar.value, "I", "Check urlbar value"); + if (!compositionClosesPanel) { + await UrlbarTestUtils.promiseSearchComplete(window); + } + // Test that we are in confirmed search mode. + await UrlbarTestUtils.assertSearchMode(window, { + engineName: "Test", + entry: "keywordoffer", + }); + await UrlbarTestUtils.exitSearchMode(window); +} + +async function test_composition_tabToSearch(compositionClosesPanel) { + info("Check Tab-to-Search is retained by composition"); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exa", + }); + + while (gURLBar.searchMode?.engineName != "Test") { + EventUtils.synthesizeKey("KEY_Tab", {}, window); + } + let expectedSearchMode = { + engineName: "Test", + isPreview: true, + entry: "tabtosearch", + }; + await UrlbarTestUtils.assertSearchMode(window, expectedSearchMode); + composeAndCheckPanel("I", !compositionClosesPanel); + Assert.equal(gURLBar.value, "I", "Check urlbar value"); + if (!compositionClosesPanel) { + await UrlbarTestUtils.promiseSearchComplete(window); + } + // Test that we are in confirmed search mode. + await UrlbarTestUtils.assertSearchMode(window, { + engineName: "Test", + entry: "tabtosearch", + }); + await UrlbarTestUtils.exitSearchMode(window); +} diff --git a/browser/components/urlbar/tests/browser/browser_inputHistory.js b/browser/components/urlbar/tests/browser/browser_inputHistory.js new file mode 100644 index 0000000000..191032a78d --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_inputHistory.js @@ -0,0 +1,361 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This tests the urlbar adaptive behavior powered by input history. + */ + +"use strict"; + +async function bumpScore(uri, searchString, counts, useMouseClick = false) { + if (counts.visits) { + let visits = new Array(counts.visits).fill(uri); + await PlacesTestUtils.addVisits(visits); + } + if (counts.picks) { + for (let i = 0; i < counts.picks; ++i) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: searchString, + }); + let promise = BrowserTestUtils.waitForDocLoadAndStopIt( + uri, + gBrowser.selectedBrowser + ); + // Look for the expected uri. + while (gURLBar.untrimmedValue != uri) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + } + if (useMouseClick) { + let element = UrlbarTestUtils.getSelectedRow(window); + EventUtils.synthesizeMouseAtCenter(element, {}); + } else { + EventUtils.synthesizeKey("KEY_Enter"); + } + await promise; + } + } + await PlacesTestUtils.promiseAsyncUpdates(); +} + +async function decayInputHistory() { + PlacesUtils.history.decayFrecency(); + await PlacesTestUtils.promiseAsyncUpdates(); +} + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [ + // We don't want autofill to influence this test. + ["browser.urlbar.autoFill", false], + ], + }); + registerCleanupFunction(async () => { + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + }); +}); + +add_task(async function test_adaptive_with_search_terms() { + let url1 = "http://site.tld/1"; + let url2 = "http://site.tld/2"; + + info("Same visit count, same picks, one partial match, one exact match"); + await PlacesUtils.history.clear(); + await bumpScore(url1, "si", { visits: 3, picks: 3 }); + await bumpScore(url2, "site", { visits: 3, picks: 3 }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "si", + }); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal(result.url, url1, "Check first result"); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2); + Assert.equal(result.url, url2, "Check second result"); + + info( + "Same visit count, same picks, one partial match, one exact match, invert" + ); + await PlacesUtils.history.clear(); + await bumpScore(url1, "site", { visits: 3, picks: 3 }); + await bumpScore(url2, "si", { visits: 3, picks: 3 }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "si", + }); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal(result.url, url2, "Check first result"); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2); + Assert.equal(result.url, url1, "Check second result"); + + info("Same visit count, different picks, both exact match"); + await PlacesUtils.history.clear(); + await bumpScore(url1, "si", { visits: 3, picks: 3 }); + await bumpScore(url2, "si", { visits: 3, picks: 1 }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "si", + }); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal(result.url, url1, "Check first result"); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2); + Assert.equal(result.url, url2, "Check second result"); + + info("Same visit count, different picks, both exact match, invert"); + await PlacesUtils.history.clear(); + await bumpScore(url1, "si", { visits: 3, picks: 1 }); + await bumpScore(url2, "si", { visits: 3, picks: 3 }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "si", + }); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal(result.url, url2, "Check first result"); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2); + Assert.equal(result.url, url1, "Check second result"); + + info("Same visit count, different picks, both partial match"); + await PlacesUtils.history.clear(); + await bumpScore(url1, "site", { visits: 3, picks: 3 }); + await bumpScore(url2, "site", { visits: 3, picks: 1 }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "si", + }); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal(result.url, url1, "Check first result"); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2); + Assert.equal(result.url, url2, "Check second result"); + + info("Same visit count, different picks, both partial match, invert"); + await PlacesUtils.history.clear(); + await bumpScore(url1, "site", { visits: 3, picks: 1 }); + await bumpScore(url2, "site", { visits: 3, picks: 3 }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "si", + }); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal(result.url, url2, "Check first result"); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2); + Assert.equal(result.url, url1, "Check second result"); +}); + +add_task(async function test_adaptive_with_decay() { + let url1 = "http://site.tld/1"; + let url2 = "http://site.tld/2"; + + info("Same visit count, same picks, both exact match, decay first"); + await PlacesUtils.history.clear(); + await bumpScore(url1, "si", { visits: 3, picks: 3 }); + await decayInputHistory(); + await bumpScore(url2, "si", { visits: 3, picks: 3 }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "si", + }); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal(result.url, url2, "Check first result"); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2); + Assert.equal(result.url, url1, "Check second result"); + + info("Same visit count, same picks, both exact match, decay second"); + await PlacesUtils.history.clear(); + await bumpScore(url2, "si", { visits: 3, picks: 3 }); + await decayInputHistory(); + await bumpScore(url1, "si", { visits: 3, picks: 3 }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "si", + }); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal(result.url, url1, "Check first result"); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2); + Assert.equal(result.url, url2, "Check second result"); +}); + +add_task(async function test_adaptive_limited() { + let url1 = "http://site.tld/1"; + let url2 = "http://site.tld/2"; + + info("Same visit count, same picks, both exact match, decay first"); + await PlacesUtils.history.clear(); + await bumpScore(url1, "si", { visits: 3, picks: 3 }); + await decayInputHistory(); + await bumpScore(url2, "si", { visits: 3, picks: 3 }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "si", + }); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal(result.url, url2, "Check first result"); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2); + Assert.equal(result.url, url1, "Check second result"); + + info("Same visit count, same picks, both exact match, decay second"); + await PlacesUtils.history.clear(); + await bumpScore(url2, "si", { visits: 3, picks: 3 }); + await decayInputHistory(); + await bumpScore(url1, "si", { visits: 3, picks: 3 }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "si", + }); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal(result.url, url1, "Check first result"); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2); + Assert.equal(result.url, url2, "Check second result"); +}); + +add_task(async function test_adaptive_limited() { + info( + "Adaptive results should be added at the top up to maxRichResults / 4, then enqueued" + ); + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + + // Add as many adaptive results as maxRichResults. + let n = UrlbarPrefs.get("maxRichResults"); + let urls = Array(n) + .fill(0) + .map((e, i) => "http://site.tld/" + i); + for (let url of urls) { + await bumpScore(url, "site", { visits: 1, picks: 1 }); + } + + // Add a matching bookmark with an higher frecency. + let url = "http://site.bookmark.tld/"; + await PlacesTestUtils.addVisits(url); + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "test_site_book", + url, + }); + + let expectedBookmarkIndex = Math.floor(n / 4) + 2; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "site", + }); + let result = await UrlbarTestUtils.getDetailsOfResultAt( + window, + expectedBookmarkIndex + ); + Assert.equal(result.url, url, "Check bookmarked result"); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, n - 1); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + n, + "Check all the results are filled" + ); + Assert.ok( + result.url.startsWith("http://site.tld"), + "Check last adaptive result" + ); + await PlacesUtils.bookmarks.remove(bm); +}); + +add_task(async function test_adaptive_behaviors() { + info( + "Check adaptive results are not provided regardless of the requested behavior" + ); + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + + // Add an adaptive entry. + await bumpScore("http://site.tld/1", "site", { visits: 1, picks: 1 }); + + let url = "http://bookmarked.site.tld/1"; + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "test_book", + url, + }); + + await SpecialPowers.pushPrefEnv({ + set: [ + // Search only bookmarks. + ["browser.urlbar.suggest.bookmarks", true], + ["browser.urlbar.suggest.history", false], + ], + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "site", + }); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal(result.url, url, "Check bookmarked result"); + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 2, + "Check there are no unexpected results" + ); + + await PlacesUtils.bookmarks.remove(bm); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_adaptive_mouse() { + info("Check adaptive results are updated on mouse picks"); + await PlacesUtils.history.clear(); + + let url1 = "http://site.tld/1"; + let url2 = "http://site.tld/2"; + + info("Same visit count, different picks"); + await PlacesUtils.history.clear(); + await bumpScore(url1, "site", { visits: 3, picks: 3 }, true); + await bumpScore(url2, "site", { visits: 3, picks: 1 }, true); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "si", + }); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal(result.url, url1, "Check first result"); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2); + Assert.equal(result.url, url2, "Check second result"); + + info("Same visit count, different picks, invert"); + await PlacesUtils.history.clear(); + await bumpScore(url1, "site", { visits: 3, picks: 1 }, true); + await bumpScore(url2, "site", { visits: 3, picks: 3 }, true); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "si", + }); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal(result.url, url2, "Check first result"); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2); + Assert.equal(result.url, url1, "Check second result"); +}); + +add_task(async function test_adaptive_searchmode() { + info("Check adaptive history is not shown in search mode."); + + let suggestionsEngine = await SearchTestUtils.promiseNewSearchEngine( + getRootDirectory(gTestPath) + "searchSuggestionEngine.xml" + ); + + let url1 = "http://site.tld/1"; + let url2 = "http://site.tld/2"; + + info("Sanity check: adaptive history is shown for a normal search."); + await PlacesUtils.history.clear(); + await bumpScore(url1, "site", { visits: 3, picks: 3 }, true); + await bumpScore(url2, "site", { visits: 3, picks: 1 }, true); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "si", + }); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal(result.url, url1, "Check first result"); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2); + Assert.equal(result.url, url2, "Check second result"); + + info("Entering search mode."); + // enterSearchMode checks internally that our site.tld URLs are not shown. + await UrlbarTestUtils.enterSearchMode(window, { + engineName: suggestionsEngine.name, + }); +}); diff --git a/browser/components/urlbar/tests/browser/browser_inputHistory_emptystring.js b/browser/components/urlbar/tests/browser/browser_inputHistory_emptystring.js new file mode 100644 index 0000000000..8fc44329ff --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_inputHistory_emptystring.js @@ -0,0 +1,100 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This tests input history in cases where the search string is empty. + * In the future we may want to not account for these, but for now they are + * stored with an empty input field. + */ + +"use strict"; + +async function checkInputHistory(len = 0) { + await PlacesUtils.withConnectionWrapper( + "test::checkInputHistory", + async db => { + let rows = await db.executeCached(`SELECT input FROM moz_inputhistory`); + Assert.equal(rows.length, len, "There should only be 1 entry"); + if (len) { + Assert.equal(rows[0].getResultByIndex(0), "", "Input should be empty"); + } + } + ); +} + +async function clearInputHistory() { + await PlacesUtils.withConnectionWrapper("test::clearInputHistory", db => { + return db.executeCached(`DELETE FROM moz_inputhistory`); + }); +} + +const TEST_URL = "http://example.com/"; + +async function do_test(openFn, pickMethod) { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: "about:blank", + }, + async function(browser) { + await clearInputHistory(); + await openFn(); + await UrlbarTestUtils.promiseSearchComplete(window); + let promise = BrowserTestUtils.waitForDocLoadAndStopIt(TEST_URL, browser); + if (pickMethod == "keyboard") { + info(`Test pressing Enter`); + EventUtils.sendKey("down"); + EventUtils.sendKey("return"); + } else { + info("Test with click"); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + EventUtils.synthesizeMouseAtCenter(result.element.row, {}); + } + await promise; + await checkInputHistory(1); + } + ); +} + +add_task(async function setup() { + await PlacesUtils.history.clear(); + for (let i = 0; i < 5; i++) { + await PlacesTestUtils.addVisits(TEST_URL); + } + + await updateTopSites(sites => sites && sites[0] && sites[0].url == TEST_URL); + registerCleanupFunction(async () => { + await PlacesUtils.history.clear(); + }); +}); + +add_task(async function test_history_no_search_terms() { + for (let pickMethod of ["keyboard", "mouse"]) { + // If a testFn returns false, it will be skipped. + for (let openFn of [ + () => { + info("Test opening panel with down key"); + gURLBar.focus(); + EventUtils.sendKey("down"); + }, + async () => { + info("Test opening panel on focus"); + gURLBar.blur(); + EventUtils.synthesizeMouseAtCenter(gURLBar.textbox, {}); + }, + async () => { + info("Test opening panel on focus on a page"); + let selectedBrowser = gBrowser.selectedBrowser; + // A page other than TEST_URL must be loaded, or the first Top Site + // result will be a switch-to-tab result and page won't be reloaded when + // the result is selected. + BrowserTestUtils.loadURI(selectedBrowser, "http://example.org/"); + await BrowserTestUtils.browserLoaded(selectedBrowser); + gURLBar.blur(); + EventUtils.synthesizeMouseAtCenter(gURLBar.textbox, {}); + }, + ]) { + await do_test(openFn, pickMethod); + } + } +}); diff --git a/browser/components/urlbar/tests/browser/browser_keepStateAcrossTabSwitches.js b/browser/components/urlbar/tests/browser/browser_keepStateAcrossTabSwitches.js new file mode 100644 index 0000000000..4e21f232b4 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_keepStateAcrossTabSwitches.js @@ -0,0 +1,68 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Verify user typed text remains in the URL bar when tab switching, even when + * loads fail. + */ +add_task(async function validURL() { + let input = "i-definitely-dont-exist.example.com"; + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "http://example.com/" + ); + let browser = tab.linkedBrowser; + // Note: Waiting on content document not being hidden because new tab pages can be preloaded, + // in which case no load events fire. + await SpecialPowers.spawn(browser, [], async () => { + await ContentTaskUtils.waitForCondition(() => { + return content.document && !content.document.hidden; + }); + }); + let errorPageLoaded = BrowserTestUtils.waitForErrorPage(browser); + gURLBar.value = input; + gURLBar.select(); + EventUtils.sendKey("return"); + await errorPageLoaded; + is(gURLBar.value, input, "Text is still in URL bar"); + await BrowserTestUtils.switchTab(gBrowser, tab.previousElementSibling); + await BrowserTestUtils.switchTab(gBrowser, tab); + is(gURLBar.value, input, "Text is still in URL bar after tab switch"); + BrowserTestUtils.removeTab(tab); +}); + +/** + * Invalid URIs fail differently (that is, immediately, in the loadURI call) + * if keyword searches are turned off. Test that this works, too. + */ +add_task(async function invalidURL() { + let input = "To be or not to be-that is the question"; + await SpecialPowers.pushPrefEnv({ set: [["keyword.enabled", false]] }); + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank", + false + ); + let browser = tab.linkedBrowser; + // Note: Waiting on content document not being hidden because new tab pages can be preloaded, + // in which case no load events fire. + await SpecialPowers.spawn(browser, [], async () => { + await ContentTaskUtils.waitForCondition(() => { + return content.document && !content.document.hidden; + }); + }); + let errorPageLoaded = BrowserTestUtils.waitForErrorPage(tab.linkedBrowser); + gURLBar.value = input; + gURLBar.select(); + EventUtils.sendKey("return"); + await errorPageLoaded; + is(gURLBar.value, input, "Text is still in URL bar"); + is(tab.linkedBrowser.userTypedValue, input, "Text still stored on browser"); + await BrowserTestUtils.switchTab(gBrowser, tab.previousElementSibling); + await BrowserTestUtils.switchTab(gBrowser, tab); + is(gURLBar.value, input, "Text is still in URL bar after tab switch"); + is(tab.linkedBrowser.userTypedValue, input, "Text still stored on browser"); + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_keyword.js b/browser/components/urlbar/tests/browser/browser_keyword.js new file mode 100644 index 0000000000..8b6d84ec39 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_keyword.js @@ -0,0 +1,240 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This tests that keywords are displayed and handled correctly. + */ + +async function promise_first_result(inputText) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: inputText, + }); + + return UrlbarTestUtils.getDetailsOfResultAt(window, 0); +} + +function assertURL(result, expectedUrl, keyword, input, postData) { + Assert.equal(result.url, expectedUrl, "Should have the correct URL"); + if (postData) { + Assert.equal( + NetUtil.readInputStreamToString( + result.postData, + result.postData.available() + ), + postData, + "Should have the correct postData" + ); + } +} + +const TEST_URL = `${TEST_BASE_URL}print_postdata.sjs`; + +add_task(async function setup() { + await PlacesUtils.keywords.insert({ + keyword: "get", + url: TEST_URL + "?q=%s", + }); + await PlacesUtils.keywords.insert({ + keyword: "post", + url: TEST_URL, + postData: "q=%s", + }); + await PlacesUtils.keywords.insert({ + keyword: "question?", + url: TEST_URL + "?q2=%s", + }); + await PlacesUtils.keywords.insert({ + keyword: "?question", + url: TEST_URL + "?q3=%s", + }); + // Avoid fetching search suggestions. + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.searches", false]], + }); + let engine = await SearchTestUtils.promiseNewSearchEngine( + getRootDirectory(gTestPath) + "searchSuggestionEngine.xml" + ); + let defaultEngine = Services.search.defaultEngine; + Services.search.defaultEngine = engine; + + registerCleanupFunction(async function() { + Services.search.defaultEngine = defaultEngine; + await PlacesUtils.keywords.remove("get"); + await PlacesUtils.keywords.remove("post"); + await PlacesUtils.keywords.remove("question?"); + await PlacesUtils.keywords.remove("?question"); + while (gBrowser.tabs.length > 1) { + BrowserTestUtils.removeTab(gBrowser.selectedTab); + } + }); +}); + +add_task(async function test_display_keyword_without_query() { + await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:mozilla"); + + // Test a keyword that also has blank spaces to ensure they are ignored as well. + let result = await promise_first_result("get "); + + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.KEYWORD, + "Should have a keyword result" + ); + Assert.equal( + result.displayed.title, + "https://example.com/browser/browser/components/urlbar/tests/browser/print_postdata.sjs?q=", + "Node should contain the url of the bookmark" + ); + let [action] = await document.l10n.formatValues([ + { id: "urlbar-result-action-visit" }, + ]); + Assert.equal(result.displayed.action, action, "Should have visit indicated"); +}); + +add_task(async function test_keyword_using_get() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:mozilla" + ); + + let result = await promise_first_result("get something"); + + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.KEYWORD, + "Should have a keyword result" + ); + Assert.equal( + result.displayed.title, + "example.com: something", + "Node should contain the name of the bookmark and query" + ); + Assert.ok(!result.displayed.action, "Should have an empty action"); + + assertURL(result, TEST_URL + "?q=something", "get", "get something"); + + let element = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0); + + // Click on the result + info("Normal click on result"); + let tabPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + EventUtils.synthesizeMouseAtCenter(element, {}); + await tabPromise; + Assert.equal( + tab.linkedBrowser.currentURI.spec, + TEST_URL + "?q=something", + "Tab should have loaded from clicking on result" + ); + + // Middle-click on the result + info("Middle-click on result"); + result = await promise_first_result("get somethingmore"); + + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.KEYWORD, + "Should have a keyword result" + ); + + assertURL(result, TEST_URL + "?q=somethingmore", "get", "get somethingmore"); + + tabPromise = BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "TabOpen"); + element = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0); + EventUtils.synthesizeMouseAtCenter(element, { button: 1 }); + let tabOpenEvent = await tabPromise; + let newTab = tabOpenEvent.target; + await BrowserTestUtils.browserLoaded(newTab.linkedBrowser); + Assert.equal( + newTab.linkedBrowser.currentURI.spec, + TEST_URL + "?q=somethingmore", + "Tab should have loaded from middle-clicking on result" + ); +}); + +add_task(async function test_keyword_using_post() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:mozilla" + ); + + let result = await promise_first_result("post something"); + + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.KEYWORD, + "Should have a keyword result" + ); + Assert.equal( + result.displayed.title, + "example.com: something", + "Node should contain the name of the bookmark and query" + ); + Assert.ok(!result.displayed.action, "Should have an empty action"); + + assertURL(result, TEST_URL, "post", "post something", "q=something"); + + // Click on the result + info("Normal click on result"); + let tabPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + let element = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0); + EventUtils.synthesizeMouseAtCenter(element, {}); + info("waiting for tab"); + await tabPromise; + Assert.equal( + tab.linkedBrowser.currentURI.spec, + TEST_URL, + "Tab should have loaded from clicking on result" + ); + + let postData = await SpecialPowers.spawn( + tab.linkedBrowser, + [], + async function() { + return content.document.body.textContent; + } + ); + Assert.equal(postData, "q=something", "post data was submitted correctly"); +}); + +add_task(async function test_keyword_with_question_mark() { + // TODO Bug 1517140: keywords containing restriction chars should not be + // allowed, or properly supported. + let result = await promise_first_result("question?"); + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.SEARCH, + "Result should be a search" + ); + Assert.equal(result.searchParams.query, "question?", "Check search query"); + + result = await promise_first_result("question? something"); + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.KEYWORD, + "Result should be a keyword" + ); + Assert.equal(result.keyword, "question?", "Check search query"); + + result = await promise_first_result("?question"); + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.SEARCH, + "Result should be a search" + ); + Assert.equal(result.searchParams.query, "question", "Check search query"); + + result = await promise_first_result("?question something"); + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.SEARCH, + "Result should be a search" + ); + Assert.equal( + result.searchParams.query, + "question something", + "Check search query" + ); +}); diff --git a/browser/components/urlbar/tests/browser/browser_keywordBookmarklets.js b/browser/components/urlbar/tests/browser/browser_keywordBookmarklets.js new file mode 100644 index 0000000000..97220fc268 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_keywordBookmarklets.js @@ -0,0 +1,133 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function setup() { + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "bookmarklet", + url: "javascript:'%sx'%20", + }); + await PlacesUtils.keywords.insert({ keyword: "bm", url: bm.url }); + registerCleanupFunction(async function() { + await PlacesUtils.bookmarks.remove(bm); + }); + + let testFns = [ + function() { + info("Type keyword and immediately press enter"); + gURLBar.value = "bm"; + gURLBar.focus(); + EventUtils.synthesizeKey("KEY_Enter"); + return "x"; + }, + function() { + info("Type keyword with searchstring and immediately press enter"); + gURLBar.value = "bm a"; + gURLBar.focus(); + EventUtils.synthesizeKey("KEY_Enter"); + return "ax"; + }, + async function() { + info("Search keyword, then press enter"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "bm", + }); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal(result.title, "javascript:'x' ", "Check title"); + EventUtils.synthesizeKey("KEY_Enter"); + return "x"; + }, + async function() { + info("Search keyword with searchstring, then press enter"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "bm a", + }); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal(result.title, "javascript:'ax' ", "Check title"); + EventUtils.synthesizeKey("KEY_Enter"); + return "ax"; + }, + async function() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "bm", + }); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal(result.title, "javascript:'x' ", "Check title"); + let element = UrlbarTestUtils.getSelectedRow(window); + EventUtils.synthesizeMouseAtCenter(element, {}); + return "x"; + }, + async function() { + info("Search keyword with searchstring, then click"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "bm a", + }); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal(result.title, "javascript:'ax' ", "Check title"); + let element = UrlbarTestUtils.getSelectedRow(window); + EventUtils.synthesizeMouseAtCenter(element, {}); + return "ax"; + }, + ]; + for (let testFn of testFns) { + await do_test(testFn); + } +}); + +async function do_test(loadFn) { + await BrowserTestUtils.withNewTab( + { + gBrowser, + }, + async browser => { + let originalPrincipal = gBrowser.contentPrincipal; + let originalPrincipalURI = await getPrincipalURI(browser); + + let promise = BrowserTestUtils.waitForContentEvent(browser, "pageshow"); + const expectedTextContent = await loadFn(); + info("Awaiting pageshow event"); + await promise; + // URI should not change when we run a javascript: URL. + Assert.equal(gBrowser.currentURI.spec, "about:blank"); + const textContent = await ContentTask.spawn(browser, [], function() { + return content.document.documentElement.textContent; + }); + Assert.equal(textContent, expectedTextContent); + + let newPrincipalURI = await getPrincipalURI(browser); + Assert.equal( + newPrincipalURI, + originalPrincipalURI, + "content has the same principal" + ); + + // In e10s, null principals don't round-trip so the same null principal sent + // from the child will be a new null principal. Verify that this is the + // case. + if (browser.isRemoteBrowser) { + Assert.ok( + originalPrincipal.isNullPrincipal && + gBrowser.contentPrincipal.isNullPrincipal, + "both principals should be null principals in the parent" + ); + } else { + Assert.ok( + gBrowser.contentPrincipal.equals(originalPrincipal), + "javascript bookmarklet should inherit principal" + ); + } + } + ); +} + +function getPrincipalURI(browser) { + return SpecialPowers.spawn(browser, [], function() { + return content.document.nodePrincipal.spec; + }); +} diff --git a/browser/components/urlbar/tests/browser/browser_keywordSearch.js b/browser/components/urlbar/tests/browser/browser_keywordSearch.js new file mode 100644 index 0000000000..5ba5805589 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_keywordSearch.js @@ -0,0 +1,61 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + **/ + +var gTests = [ + { + name: "normal search (search service)", + text: "test search", + expectText: "test+search", + }, + { + name: "?-prefixed search (search service)", + text: "? foo ", + expectText: "foo", + }, +]; + +add_task(async function setup() { + let engine = await SearchTestUtils.promiseNewSearchEngine( + getRootDirectory(gTestPath) + "searchSuggestionEngine.xml" + ); + let oldDefaultEngine = await Services.search.getDefault(); + await Services.search.setDefault(engine); + registerCleanupFunction(async function() { + await Services.search.setDefault(oldDefaultEngine); + }); +}); + +add_task(async function() { + // Test both directly setting a value and pressing enter, or setting the + // value through input events, like the user would do. + const setValueFns = [ + value => { + gURLBar.value = value; + }, + value => { + return UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value, + }); + }, + ]; + + for (let test of gTests) { + info("Testing: " + test.name); + await BrowserTestUtils.withNewTab({ gBrowser }, async browser => { + for (let setValueFn of setValueFns) { + gURLBar.select(); + await setValueFn(test.text); + EventUtils.synthesizeKey("KEY_Enter"); + + let expectedUrl = "http://mochi.test:8888/?terms=" + test.expectText; + info("Waiting for load: " + expectedUrl); + await BrowserTestUtils.browserLoaded(browser, false, expectedUrl); + // At least one test. + Assert.equal(browser.currentURI.spec, expectedUrl); + } + }); + } +}); diff --git a/browser/components/urlbar/tests/browser/browser_keywordSearch_postData.js b/browser/components/urlbar/tests/browser/browser_keywordSearch_postData.js new file mode 100644 index 0000000000..8484cd778d --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_keywordSearch_postData.js @@ -0,0 +1,78 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + **/ + +var gTests = [ + { + name: "single word search (search service)", + text: "pizza", + expectText: "pizza", + }, + { + name: "multi word search (search service)", + text: "test search", + expectText: "test+search", + }, + { + name: "?-prefixed search (search service)", + text: "? foo ", + expectText: "foo", + }, +]; + +add_task(async function setup() { + let engine = await SearchTestUtils.promiseNewSearchEngine( + getRootDirectory(gTestPath) + "POSTSearchEngine.xml" + ); + let oldDefaultEngine = await Services.search.getDefault(); + await Services.search.setDefault(engine); + registerCleanupFunction(async function() { + await Services.search.setDefault(oldDefaultEngine); + }); +}); + +add_task(async function() { + // Test both directly setting a value and pressing enter, or setting the + // value through input events, like the user would do. + const setValueFns = [ + value => { + gURLBar.value = value; + }, + value => { + return UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value, + }); + }, + ]; + + for (let test of gTests) { + info("Testing: " + test.name); + await BrowserTestUtils.withNewTab({ gBrowser }, async browser => { + for (let setValueFn of setValueFns) { + gURLBar.select(); + await setValueFn(test.text); + EventUtils.synthesizeKey("KEY_Enter"); + + await BrowserTestUtils.browserLoaded( + browser, + false, + "http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/print_postdata.sjs" + ); + + let textContent = await SpecialPowers.spawn(browser, [], async () => { + return content.document.body.textContent; + }); + + Assert.ok(textContent, "search page loaded"); + let needle = "searchterms=" + test.expectText; + Assert.equal( + textContent, + needle, + "The query POST data should be returned in the response" + ); + } + }); + } +}); diff --git a/browser/components/urlbar/tests/browser/browser_keyword_override.js b/browser/components/urlbar/tests/browser/browser_keyword_override.js new file mode 100644 index 0000000000..c53adaef73 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_keyword_override.js @@ -0,0 +1,61 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This tests that the display of keyword results are not changed when the user + * presses the override button. + */ + +add_task(async function() { + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.com/?q=%s", + title: "test", + }); + await PlacesUtils.keywords.insert({ + keyword: "keyword", + url: "http://example.com/?q=%s", + }); + + registerCleanupFunction(async function() { + await PlacesUtils.bookmarks.remove(bm); + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "keyword search", + }); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + + info("Before override"); + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.KEYWORD, + "Should have a keyword result" + ); + Assert.equal( + result.displayed.title, + "example.com: search", + "Node should contain the name of the bookmark and query" + ); + Assert.ok(!result.displayed.action, "Should have an empty action"); + + info("During override"); + EventUtils.synthesizeKey("VK_SHIFT", { type: "keydown" }); + + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.KEYWORD, + "Should have a keyword result" + ); + Assert.equal( + result.displayed.title, + "example.com: search", + "Node should contain the name of the bookmark and query" + ); + Assert.ok(!result.displayed.action, "Should have an empty action"); + + EventUtils.synthesizeKey("VK_SHIFT", { type: "keyup" }); +}); diff --git a/browser/components/urlbar/tests/browser/browser_keyword_select_and_type.js b/browser/components/urlbar/tests/browser/browser_keyword_select_and_type.js new file mode 100644 index 0000000000..dc907b5ee9 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_keyword_select_and_type.js @@ -0,0 +1,97 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This tests that changing away from a keyword result and back again, still + * operates correctly. + */ + +add_task(async function() { + let bookmarks = []; + bookmarks.push( + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.com/?q=%s", + title: "test", + }) + ); + await PlacesUtils.keywords.insert({ + keyword: "keyword", + url: "http://example.com/?q=%s", + }); + + // This item is only needed so we can select the keyword item, select something + // else, then select the keyword item again. + bookmarks.push( + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.com/keyword", + title: "keyword abc", + }) + ); + + registerCleanupFunction(async function() { + for (let bm of bookmarks) { + await PlacesUtils.bookmarks.remove(bm); + } + }); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:mozilla" + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "keyword a", + }); + await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1); + + // First item should already be selected + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 0, + "Should have the first item selected" + ); + + // Select next one (important!) + EventUtils.synthesizeKey("KEY_ArrowDown"); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 1, + "Should have the second item selected" + ); + + // Re-select keyword item + EventUtils.synthesizeKey("KEY_ArrowUp"); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 0, + "Should have the first item selected" + ); + + EventUtils.sendString("b"); + await UrlbarTestUtils.promiseSearchComplete(window); + + Assert.equal( + gURLBar.value, + "keyword ab", + "urlbar should have expected input" + ); + + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.KEYWORD, + "Should have a result of type keyword" + ); + Assert.equal( + result.url, + "http://example.com/?q=ab", + "Should have the correct url" + ); + + gBrowser.removeTab(tab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_loadRace.js b/browser/components/urlbar/tests/browser/browser_loadRace.js new file mode 100644 index 0000000000..4e1e48e5d6 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_loadRace.js @@ -0,0 +1,90 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// This test is for testing races of loading the Urlbar when loading shortcuts. +// For example, ensuring that if a search query is entered, but something causes +// a page load whilst we're getting the search url, then we don't handle the +// original search query. + +add_task(async function setup() { + sandbox = sinon.createSandbox(); + + registerCleanupFunction(async () => { + sandbox.restore(); + }); +}); + +async function checkShortcutLoading(modifierKeys) { + let deferred = PromiseUtils.defer(); + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: "about:robots", + }); + + // We stub getHeuristicResultFor to guarentee it doesn't resolve until after + // we've loaded a new page. + let original = UrlbarUtils.getHeuristicResultFor; + sandbox + .stub(UrlbarUtils, "getHeuristicResultFor") + .callsFake(async searchString => { + await deferred.promise; + return original.call(this, searchString); + }); + + // This load will be blocked until the deferred is resolved. + // Use a string that will be interepreted as a local URL to avoid hitting the + // network. + gURLBar.focus(); + gURLBar.value = "example.com"; + gURLBar.userTypedValue = true; + EventUtils.synthesizeKey("KEY_Enter", modifierKeys); + + Assert.ok( + UrlbarUtils.getHeuristicResultFor.calledOnce, + "should have called getHeuristicResultFor" + ); + + // Now load a different page. + BrowserTestUtils.loadURI(tab.linkedBrowser, "about:license"); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + Assert.equal(gBrowser.visibleTabs.length, 2, "Should have 2 tabs"); + + // Now that the new page has loaded, unblock the previous urlbar load. + deferred.resolve(); + if (modifierKeys) { + let openedTab = await new Promise(resolve => { + window.addEventListener( + "TabOpen", + event => { + resolve(event.target); + }, + { once: true } + ); + }); + await BrowserTestUtils.browserLoaded(openedTab.linkedBrowser); + Assert.ok( + openedTab.linkedBrowser.currentURI.spec.includes("example.com"), + "Should have attempted to open the shortcut page" + ); + BrowserTestUtils.removeTab(openedTab); + } + + Assert.equal( + tab.linkedBrowser.currentURI.spec, + "about:license", + "Tab url should not have changed" + ); + Assert.equal(gBrowser.visibleTabs.length, 2, "Should still have 2 tabs"); + + BrowserTestUtils.removeTab(tab); + sandbox.restore(); +} + +add_task(async function test_location_change_stops_load() { + await checkShortcutLoading(); +}); + +add_task(async function test_opening_different_tab_with_location_change() { + await checkShortcutLoading({ altKey: true }); +}); diff --git a/browser/components/urlbar/tests/browser/browser_locationBarCommand.js b/browser/components/urlbar/tests/browser/browser_locationBarCommand.js new file mode 100644 index 0000000000..aeffac3045 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_locationBarCommand.js @@ -0,0 +1,291 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This test is designed to ensure that the correct command/operation happens + * when pressing enter with various key combinations in the urlbar. + */ + +const TEST_VALUE = "example.com"; +const START_VALUE = "example.org"; + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.altClickSave", true], + ["browser.urlbar.autoFill", false], + ], + }); +}); + +add_task(async function alt_left_click_test() { + info("Running test: Alt left click"); + + // Monkey patch saveURL() to avoid dealing with file save code paths. + let oldSaveURL = saveURL; + let saveURLPromise = new Promise(resolve => { + saveURL = () => { + // Restore old saveURL() value. + saveURL = oldSaveURL; + resolve(); + }; + }); + + await triggerCommand("click", { altKey: true }); + + await saveURLPromise; + ok(true, "SaveURL was called"); + is(gURLBar.value, "", "Urlbar reverted to original value"); +}); + +add_task(async function shift_left_click_test() { + info("Running test: Shift left click"); + + let destinationURL = "http://" + TEST_VALUE + "/"; + let newWindowPromise = BrowserTestUtils.waitForNewWindow({ + url: destinationURL, + }); + await triggerCommand("click", { shiftKey: true }); + let win = await newWindowPromise; + + info("URL should be loaded in a new window"); + is(gURLBar.value, "", "Urlbar reverted to original value"); + await promiseCheckChildNoFocusedElement(gBrowser.selectedBrowser); + is( + document.activeElement, + gBrowser.selectedBrowser, + "Content window should be focused" + ); + is(win.gURLBar.value, TEST_VALUE, "New URL is loaded in new window"); + + // Cleanup. + let ourWindowRefocusedPromise = Promise.all([ + BrowserTestUtils.waitForEvent(window, "activate"), + BrowserTestUtils.waitForEvent(window, "focus", true), + ]); + await BrowserTestUtils.closeWindow(win); + await ourWindowRefocusedPromise; +}); + +add_task(async function right_click_test() { + info("Running test: Right click on go button"); + + // Add a new tab. + await promiseOpenNewTab(); + + await triggerCommand("click", { button: 2 }); + + // Right click should do nothing (context menu will be shown). + is(gURLBar.value, TEST_VALUE, "Urlbar still has the value we entered"); + + // Cleanup. + gBrowser.removeCurrentTab(); +}); + +add_task(async function shift_accel_left_click_test() { + info("Running test: Shift+Ctrl/Cmd left click on go button"); + + // Add a new tab. + let tab = await promiseOpenNewTab(); + + let loadStartedPromise = promiseLoadStarted(); + await triggerCommand("click", { accelKey: true, shiftKey: true }); + await loadStartedPromise; + + // Check the load occurred in a new background tab. + info("URL should be loaded in a new background tab"); + is(gURLBar.value, "", "Urlbar reverted to original value"); + ok(!gURLBar.focused, "Urlbar is no longer focused after urlbar command"); + is(gBrowser.selectedTab, tab, "Focus did not change to the new tab"); + + // Select the new background tab + gBrowser.selectedTab = gBrowser.selectedTab.nextElementSibling; + is(gURLBar.value, TEST_VALUE, "New URL is loaded in new tab"); + + // Cleanup. + gBrowser.removeCurrentTab(); + gBrowser.removeCurrentTab(); +}); + +add_task(async function load_in_current_tab_test() { + let tests = [ + { + desc: "Simple return keypress", + type: "keypress", + }, + { + desc: "Left click on go button", + type: "click", + }, + { + desc: "Ctrl/Cmd+Return keypress", + type: "keypress", + details: { accelKey: true }, + }, + { + desc: "Alt+Return keypress in a blank tab", + type: "keypress", + details: { altKey: true }, + }, + { + desc: "AltGr+Return keypress in a blank tab", + type: "keypress", + details: { altGraphKey: true }, + }, + ]; + + for (let { desc, type, details } of tests) { + info(`Running test: ${desc}`); + + // Add a new tab. + let tab = await promiseOpenNewTab(); + + // Trigger a load and check it occurs in the current tab. + let loadStartedPromise = promiseLoadStarted(); + await triggerCommand(type, details); + await loadStartedPromise; + + info("URL should be loaded in the current tab"); + is(gURLBar.value, TEST_VALUE, "Urlbar still has the value we entered"); + await promiseCheckChildNoFocusedElement(gBrowser.selectedBrowser); + is( + document.activeElement, + gBrowser.selectedBrowser, + "Content window should be focused" + ); + is(gBrowser.selectedTab, tab, "New URL was loaded in the current tab"); + + // Cleanup. + gBrowser.removeCurrentTab(); + } +}); + +add_task(async function load_in_new_tab_test() { + let tests = [ + { + desc: "Ctrl/Cmd left click on go button", + type: "click", + details: { accelKey: true }, + url: "about:blank", + }, + { + desc: "Alt+Return keypress in a dirty tab", + type: "keypress", + details: { altKey: true }, + url: START_VALUE, + }, + { + desc: "AltGr+Return keypress in a dirty tab", + type: "keypress", + details: { altGraphKey: true }, + url: START_VALUE, + }, + ]; + + for (let { desc, type, details, url } of tests) { + info(`Running test: ${desc}`); + + // Add a new tab. + let tab = await promiseOpenNewTab(url); + + // Trigger a load and check it occurs in a new tab. + let tabSwitchedPromise = promiseNewTabSwitched(); + await triggerCommand(type, details); + await tabSwitchedPromise; + + // Check the load occurred in a new tab. + info("URL should be loaded in a new focused tab"); + is(gURLBar.value, TEST_VALUE, "Urlbar still has the value we entered"); + await promiseCheckChildNoFocusedElement(gBrowser.selectedBrowser); + is( + document.activeElement, + gBrowser.selectedBrowser, + "Content window should be focused" + ); + isnot(gBrowser.selectedTab, tab, "New URL was loaded in a new tab"); + + // Cleanup. + gBrowser.removeCurrentTab(); + gBrowser.removeCurrentTab(); + } +}); + +async function triggerCommand(type, details = {}) { + gURLBar.focus(); + gURLBar.value = ""; + EventUtils.sendString(TEST_VALUE); + + Assert.equal( + await UrlbarTestUtils.promiseUserContextId(window), + gBrowser.selectedTab.getAttribute("usercontextid"), + "userContextId must be the same as the originating tab" + ); + + if (type == "click") { + ok( + gURLBar.hasAttribute("usertyping"), + "usertyping attribute must be set for the go button to be visible" + ); + EventUtils.synthesizeMouseAtCenter(gURLBar.goButton, details); + } else if (type == "keypress") { + EventUtils.synthesizeKey("KEY_Enter", details); + } else { + throw new Error("Unsupported event type"); + } +} + +function promiseLoadStarted() { + return new Promise(resolve => { + gBrowser.addTabsProgressListener({ + onStateChange(browser, webProgress, req, flags, status) { + if (flags & Ci.nsIWebProgressListener.STATE_START) { + gBrowser.removeTabsProgressListener(this); + resolve(); + } + }, + }); + }); +} + +let gUserContextIdSerial = 1; +async function promiseOpenNewTab(url = "about:blank") { + let tab = BrowserTestUtils.addTab(gBrowser, url, { + userContextId: gUserContextIdSerial++, + }); + let tabSwitchPromise = promiseNewTabSwitched(tab); + gBrowser.selectedTab = tab; + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + await tabSwitchPromise; + return tab; +} + +function promiseNewTabSwitched() { + return new Promise(resolve => { + gBrowser.addEventListener( + "TabSwitchDone", + function() { + executeSoon(resolve); + }, + { once: true } + ); + }); +} + +function promiseCheckChildNoFocusedElement(browser) { + if (!gMultiProcessBrowser) { + Assert.equal( + Services.focus.focusedElement, + null, + "There should be no focused element" + ); + return null; + } + + return ContentTask.spawn(browser, null, async function() { + Assert.equal( + Services.focus.focusedElement, + null, + "There should be no focused element" + ); + }); +} diff --git a/browser/components/urlbar/tests/browser/browser_locationBarExternalLoad.js b/browser/components/urlbar/tests/browser/browser_locationBarExternalLoad.js new file mode 100644 index 0000000000..3560179be4 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_locationBarExternalLoad.js @@ -0,0 +1,94 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(async function() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.autoFill", false]], + }); + const url = "data:text/html,<body>hi"; + await testURL(url, urlEnter); + await testURL(url, urlClick); +}); + +function urlEnter(url) { + gURLBar.value = url; + gURLBar.focus(); + EventUtils.synthesizeKey("KEY_Enter"); +} + +function urlClick(url) { + gURLBar.focus(); + gURLBar.value = ""; + EventUtils.sendString(url); + EventUtils.synthesizeMouseAtCenter(gURLBar.goButton, {}); +} + +function promiseNewTabSwitched() { + return new Promise(resolve => { + gBrowser.addEventListener( + "TabSwitchDone", + function() { + executeSoon(resolve); + }, + { once: true } + ); + }); +} + +function promiseLoaded(browser) { + return SpecialPowers.spawn(browser, [undefined], async () => { + if (!["interactive", "complete"].includes(content.document.readyState)) { + await new Promise(resolve => + docShell.chromeEventHandler.addEventListener( + "DOMContentLoaded", + resolve, + { + once: true, + capture: true, + } + ) + ); + } + }); +} + +async function testURL(url, loadFunc, endFunc) { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + let browser = tab.linkedBrowser; + + let pagePrincipal = gBrowser.contentPrincipal; + // We need to ensure that we set the pageshow event listener before running + // loadFunc, otherwise there's a chance that the content process will finish + // loading the page and fire pageshow before the event listener gets set. + let pageShowPromise = BrowserTestUtils.waitForContentEvent( + browser, + "pageshow" + ); + loadFunc(url); + await pageShowPromise; + + await SpecialPowers.spawn( + browser, + [{ isRemote: gMultiProcessBrowser }], + async function(arg) { + Assert.equal( + Services.focus.focusedElement, + null, + "focusedElement not null" + ); + } + ); + + is(document.activeElement, browser, "content window should be focused"); + + ok( + !gBrowser.contentPrincipal.equals(pagePrincipal), + "load of " + + url + + " by " + + loadFunc.name + + " should produce a page with a different principal" + ); + + await BrowserTestUtils.removeTab(tab); +} diff --git a/browser/components/urlbar/tests/browser/browser_locationchange_urlbar_edit_dos.js b/browser/components/urlbar/tests/browser/browser_locationchange_urlbar_edit_dos.js new file mode 100644 index 0000000000..925f8b0ac5 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_locationchange_urlbar_edit_dos.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URL = `${TEST_BASE_URL}file_urlbar_edit_dos.html`; + +async function checkURLBarValueStays(browser) { + gURLBar.select(); + EventUtils.sendString("a"); + is(gURLBar.value, "a", "URL bar value should match after sending a key"); + await new Promise(resolve => { + let listener = { + onLocationChange(aWebProgress, aRequest, aLocation, aFlags) { + ok( + aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT, + "Should only get a same document location change" + ); + gBrowser.selectedBrowser.removeProgressListener(filter); + filter = null; + // Wait an extra tick before resolving. We want to make sure that other + // web progress listeners queued after this one are called before we + // continue the test, in case the remainder of the test depends on those + // listeners. That should happen anyway since promises are resolved on + // the next tick, but do this to be a little safer. In particular we + // want to avoid having the test pass when it should fail. + executeSoon(resolve); + }, + }; + let filter = Cc[ + "@mozilla.org/appshell/component/browser-status-filter;1" + ].createInstance(Ci.nsIWebProgress); + filter.addProgressListener(listener, Ci.nsIWebProgress.NOTIFY_ALL); + gBrowser.selectedBrowser.addProgressListener(filter); + }); + is( + gURLBar.value, + "a", + "URL bar should not have been changed by location changes." + ); +} + +add_task(async function() { + // Disable autofill so that when checkURLBarValueStays types "a", it's not + // autofilled to addons.mozilla.org (or anything else). + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.autoFill", false]], + }); + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: TEST_URL, + }, + async function(browser) { + let promise1 = checkURLBarValueStays(browser); + SpecialPowers.spawn(browser, [""], function() { + content.wrappedJSObject.dos_hash(); + }); + await promise1; + let promise2 = checkURLBarValueStays(browser); + SpecialPowers.spawn(browser, [""], function() { + content.wrappedJSObject.dos_pushState(); + }); + await promise2; + } + ); +}); diff --git a/browser/components/urlbar/tests/browser/browser_new_tab_urlbar_reset.js b/browser/components/urlbar/tests/browser/browser_new_tab_urlbar_reset.js new file mode 100644 index 0000000000..fc1319b631 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_new_tab_urlbar_reset.js @@ -0,0 +1,39 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Verify that urlbar state is reset when opening a new tab, so searching for the + * same text will reopen the results popup. + */ +add_task(async function() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank", + false + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "m", + }); + assertOpen(); + + let tab2 = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank", + false + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "m", + }); + assertOpen(); + + BrowserTestUtils.removeTab(tab); + BrowserTestUtils.removeTab(tab2); +}); + +function assertOpen() { + Assert.equal(gURLBar.view.isOpen, true, "Should be showing the popup"); +} diff --git a/browser/components/urlbar/tests/browser/browser_oneOffs.js b/browser/components/urlbar/tests/browser/browser_oneOffs.js new file mode 100644 index 0000000000..16f2f51e14 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_oneOffs.js @@ -0,0 +1,961 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests the one-off search buttons in the urlbar. + */ + +"use strict"; + +const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml"; + +let gMaxResults; +let engine; + +XPCOMUtils.defineLazyGetter(this, "oneOffSearchButtons", () => { + return UrlbarTestUtils.getOneOffSearchButtons(window); +}); + +add_task(async function init() { + gMaxResults = Services.prefs.getIntPref("browser.urlbar.maxRichResults"); + + // Add a search suggestion engine and move it to the front so that it appears + // as the first one-off. + engine = await SearchTestUtils.promiseNewSearchEngine( + getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME + ); + await Services.search.moveEngine(engine, 0); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.search.separatePrivateDefault.ui.enabled", false]], + }); + + registerCleanupFunction(async function() { + await PlacesUtils.history.clear(); + await UrlbarTestUtils.formHistory.clear(); + }); + + // Initialize history with enough visits to fill up the view. + await PlacesUtils.history.clear(); + await UrlbarTestUtils.formHistory.clear(); + for (let i = 0; i < gMaxResults; i++) { + await PlacesTestUtils.addVisits( + "http://example.com/browser_urlbarOneOffs.js/?" + i + ); + } + + // Add some more visits to the last URL added above so that the top-sites view + // will be non-empty. + for (let i = 0; i < 5; i++) { + await PlacesTestUtils.addVisits( + "http://example.com/browser_urlbarOneOffs.js/?" + (gMaxResults - 1) + ); + } + await updateTopSites(sites => { + return sites && sites[0] && sites[0].url.startsWith("http://example.com/"); + }); + + // Move the mouse away from the view so that a result or one-off isn't + // inadvertently highlighted. See bug 1659011. + EventUtils.synthesizeMouse( + gURLBar.inputField, + 0, + 0, + { type: "mousemove" }, + window + ); +}); + +// Opens the view without showing the one-offs. They should be hidden and arrow +// key selection should work properly. +add_task(async function noOneOffs() { + // Do a search for "@" since we hide the one-offs in that case. + let value = "@"; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value, + fireInputEvent: true, + }); + await TestUtils.waitForCondition( + () => !oneOffSearchButtons._rebuilding, + "Waiting for one-offs to finish rebuilding" + ); + + Assert.equal( + UrlbarTestUtils.getOneOffSearchButtonsVisible(window), + false, + "One-offs should be hidden" + ); + assertState(-1, -1, value); + + // Get the result count. We don't particularly care what the results are, + // just what the count is so that we can key through them all. + let resultCount = UrlbarTestUtils.getResultCount(window); + + // Key down through all results. + for (let i = 0; i < resultCount; i++) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + assertState(i, -1); + } + + // Key down again. Nothing should be selected. + EventUtils.synthesizeKey("KEY_ArrowDown"); + assertState(-1, -1, value); + + // Key down again. The first result should be selected. + EventUtils.synthesizeKey("KEY_ArrowDown"); + assertState(0, -1); + + // Key up. Nothing should be selected. + EventUtils.synthesizeKey("KEY_ArrowUp"); + assertState(-1, -1, value); + + // Key up through all the results. + for (let i = resultCount - 1; i >= 0; i--) { + EventUtils.synthesizeKey("KEY_ArrowUp"); + assertState(i, -1); + } + + // Key up again. Nothing should be selected. + EventUtils.synthesizeKey("KEY_ArrowUp"); + assertState(-1, -1, value); + + await hidePopup(); +}); + +// Opens the top-sites view. The one-offs should be shown. +add_task(async function topSites() { + // Do a search that shows top sites. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + fireInputEvent: true, + }); + await TestUtils.waitForCondition( + () => !oneOffSearchButtons._rebuilding, + "Waiting for one-offs to finish rebuilding" + ); + + // There's one top sites result, the page with a lot of visits from init. + let resultURL = "example.com/browser_urlbarOneOffs.js/?" + (gMaxResults - 1); + Assert.equal(UrlbarTestUtils.getResultCount(window), 1, "Result count"); + + Assert.equal( + UrlbarTestUtils.getOneOffSearchButtonsVisible(window), + true, + "One-offs are visible" + ); + + assertState(-1, -1, ""); + + // Key down into the result. + EventUtils.synthesizeKey("KEY_ArrowDown"); + assertState(0, -1, resultURL); + + // Key down through each one-off. + let numButtons = oneOffSearchButtons.getSelectableButtons(true).length; + for (let i = 0; i < numButtons; i++) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + assertState(-1, i, ""); + } + + // Key down again. The selection should go away. + EventUtils.synthesizeKey("KEY_ArrowDown"); + assertState(-1, -1, ""); + + // Key down again. The result should be selected. + EventUtils.synthesizeKey("KEY_ArrowDown"); + assertState(0, -1, resultURL); + + // Key back up. The selection should go away. + EventUtils.synthesizeKey("KEY_ArrowUp"); + assertState(-1, -1, ""); + + // Key up again. The selection should wrap back around to the one-offs. Key + // up through all the one-offs. + for (let i = numButtons - 1; i >= 0; i--) { + EventUtils.synthesizeKey("KEY_ArrowUp"); + assertState(-1, i, ""); + } + + // Key up. The result should be selected. + EventUtils.synthesizeKey("KEY_ArrowUp"); + assertState(0, -1, resultURL); + + // Key up again. The selection should go away. + EventUtils.synthesizeKey("KEY_ArrowUp"); + assertState(-1, -1, ""); + + await hidePopup(); +}); + +// Keys up and down through the non-top-sites view, i.e., the view that's shown +// when the input has been edited. +add_task(async function editedView() { + // Use a typed value that returns the visits added above but that doesn't + // trigger autofill since that would complicate the test. + let typedValue = "browser_urlbarOneOffs"; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: typedValue, + fireInputEvent: true, + }); + await UrlbarTestUtils.waitForAutocompleteResultAt(window, gMaxResults - 1); + let heuristicResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + assertState(0, -1, typedValue); + + // Key down through each result. The first result is already selected, which + // is why gMaxResults - 1 is the correct number of times to do this. + for (let i = 0; i < gMaxResults - 1; i++) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + // i starts at zero so that the textValue passed to assertState is correct. + // But that means that i + 1 is the expected selected index, since initially + // (when this loop starts) the first result is selected. + assertState( + i + 1, + -1, + "example.com/browser_urlbarOneOffs.js/?" + (gMaxResults - i - 1) + ); + Assert.ok( + !BrowserTestUtils.is_visible(heuristicResult.element.action), + "The heuristic action should not be visible" + ); + } + + // Key down through each one-off. + let numButtons = oneOffSearchButtons.getSelectableButtons(true).length; + for (let i = 0; i < numButtons; i++) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + assertState(-1, i, typedValue); + Assert.equal( + BrowserTestUtils.is_visible(heuristicResult.element.action), + !oneOffSearchButtons.selectedButton.classList.contains( + "search-setting-button-compact" + ), + "The heuristic action should be visible when a one-off button is selected" + ); + } + + // Key down once more. The selection should wrap around to the first result. + EventUtils.synthesizeKey("KEY_ArrowDown"); + assertState(0, -1, typedValue); + Assert.ok( + BrowserTestUtils.is_visible(heuristicResult.element.action), + "The heuristic action should be visible" + ); + + // Now key up. The selection should wrap back around to the one-offs. Key + // up through all the one-offs. + for (let i = numButtons - 1; i >= 0; i--) { + EventUtils.synthesizeKey("KEY_ArrowUp"); + assertState(-1, i, typedValue); + Assert.equal( + BrowserTestUtils.is_visible(heuristicResult.element.action), + !oneOffSearchButtons.selectedButton.classList.contains( + "search-setting-button-compact" + ), + "The heuristic action should be visible when a one-off button is selected" + ); + } + + // Key up through each non-heuristic result. + for (let i = gMaxResults - 2; i >= 0; i--) { + EventUtils.synthesizeKey("KEY_ArrowUp"); + assertState( + i + 1, + -1, + "example.com/browser_urlbarOneOffs.js/?" + (gMaxResults - i - 1) + ); + Assert.ok( + !BrowserTestUtils.is_visible(heuristicResult.element.action), + "The heuristic action should not be visible" + ); + } + + // Key up once more. The heuristic result should be selected. + EventUtils.synthesizeKey("KEY_ArrowUp"); + assertState(0, -1, typedValue); + Assert.ok( + BrowserTestUtils.is_visible(heuristicResult.element.action), + "The heuristic action should be visible" + ); + + await hidePopup(); +}); + +// Checks that "Search with Current Search Engine" items are updated to "Search +// with One-Off Engine" when a one-off is selected. +add_task(async function searchWith() { + // Enable suggestions for this subtest so we can check non-heuristic results. + let oldDefaultEngine = await Services.search.getDefault(); + await Services.search.setDefault(engine); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.searches", true]], + }); + + let typedValue = "foo"; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: typedValue, + }); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + assertState(0, -1, typedValue); + + Assert.equal( + result.displayed.action, + "Search with " + (await Services.search.getDefault()).name, + "Sanity check: first result's action text" + ); + + // Alt+Down to the second one-off. Now the first result and the second + // one-off should both be selected. + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true, repeat: 2 }); + assertState(0, 1, typedValue); + + let engineName = oneOffSearchButtons.selectedButton.engine.name; + Assert.notEqual( + engineName, + (await Services.search.getDefault()).name, + "Sanity check: Second one-off engine should not be the current engine" + ); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + result.displayed.action, + "Search with " + engineName, + "First result's action text should be updated" + ); + + // Check non-heuristic results. + await hidePopup(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: typedValue, + }); + + EventUtils.synthesizeKey("KEY_ArrowDown"); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + assertState(1, -1, typedValue + "foo"); + Assert.equal( + result.displayed.action, + "Search with " + engine.name, + "Sanity check: second result's action text" + ); + Assert.ok(!result.heuristic, "The second result is not heuristic."); + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true, repeat: 2 }); + assertState(1, 1, typedValue + "foo"); + + engineName = oneOffSearchButtons.selectedButton.engine.name; + Assert.notEqual( + engineName, + (await Services.search.getDefault()).name, + "Sanity check: Second one-off engine should not be the current engine" + ); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + + Assert.equal( + result.displayed.action, + "Search with " + engineName, + "Second result's action text should be updated" + ); + + await SpecialPowers.popPrefEnv(); + await Services.search.setDefault(oldDefaultEngine); + await hidePopup(); +}); + +// Clicks a one-off with an engine. +add_task(async function oneOffClick() { + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + + // We are explicitly using something that looks like a url, to make the test + // stricter. Even if it looks like a url, we should search. + let typedValue = "foo.bar"; + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: typedValue, + }); + await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + assertState(0, -1, typedValue); + let oneOffs = oneOffSearchButtons.getSelectableButtons(true); + + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeMouseAtCenter(oneOffs[0], {}); + await searchPromise; + Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Urlbar view is still open."); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: oneOffs[0].engine.name, + entry: "oneoff", + }); + await UrlbarTestUtils.exitSearchMode(window, { backspace: true }); + + gBrowser.removeTab(gBrowser.selectedTab); + await UrlbarTestUtils.formHistory.clear(); +}); + +// Presses the Return key when a one-off with an engine is selected. +add_task(async function oneOffReturn() { + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + + // We are explicitly using something that looks like a url, to make the test + // stricter. Even if it looks like a url, we should search. + let typedValue = "foo.bar"; + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: typedValue, + fireInputEvent: true, + }); + await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + assertState(0, -1, typedValue); + let oneOffs = oneOffSearchButtons.getSelectableButtons(true); + + // Alt+Down to select the first one-off. + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + assertState(0, 0, typedValue); + + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey("KEY_Enter"); + await searchPromise; + Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Urlbar view is still open."); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: oneOffs[0].engine.name, + entry: "oneoff", + }); + await UrlbarTestUtils.exitSearchMode(window, { backspace: true }); + + gBrowser.removeTab(gBrowser.selectedTab); + await UrlbarTestUtils.formHistory.clear(); + await hidePopup(); +}); + +// When all engines and local shortcuts are hidden except for the current +// engine, the one-offs container should be hidden. +add_task(async function allOneOffsHiddenExceptCurrentEngine() { + // Disable all the engines but the current one, check the oneoffs are + // hidden and that moving up selects the last match. + let defaultEngine = await Services.search.getDefault(); + let engines = (await Services.search.getVisibleEngines()).filter( + e => e.name != defaultEngine.name + ); + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.search.hiddenOneOffs", engines.map(e => e.name).join(",")], + ...UrlbarUtils.LOCAL_SEARCH_MODES.map(m => [ + `browser.urlbar.${m.pref}`, + false, + ]), + ], + }); + + let typedValue = "foo"; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: typedValue, + fireInputEvent: true, + }); + await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + assertState(0, -1); + Assert.equal( + UrlbarTestUtils.getOneOffSearchButtonsVisible(window), + false, + "The one-off buttons should be hidden" + ); + EventUtils.synthesizeKey("KEY_ArrowUp"); + assertState(0, -1); + await hidePopup(); + await SpecialPowers.popPrefEnv(); +}); + +// The one-offs should be hidden when searching with an "@engine" search engine +// alias. +add_task(async function hiddenWhenUsingSearchAlias() { + let typedValue = "@example"; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: typedValue, + fireInputEvent: true, + }); + await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + UrlbarTestUtils.getOneOffSearchButtonsVisible(window), + false, + "Should not be showing the one-off buttons" + ); + await hidePopup(); + + typedValue = "not an engine alias"; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: typedValue, + fireInputEvent: true, + }); + await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + UrlbarTestUtils.getOneOffSearchButtonsVisible(window), + true, + "Should be showing the one-off buttons" + ); + await hidePopup(); +}); + +// Makes sure the local shortcuts exist. +add_task(async function localShortcuts() { + oneOffSearchButtons.invalidateCache(); + await doLocalShortcutsShownTest(); +}); + +// Clicks a local shortcut button. +add_task(async function localShortcutClick() { + // We are explicitly using something that looks like a url, to make the test + // stricter. Even if it looks like a url, we should search. + let typedValue = "foo.bar"; + + oneOffSearchButtons.invalidateCache(); + let rebuildPromise = BrowserTestUtils.waitForEvent( + oneOffSearchButtons, + "rebuild" + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: typedValue, + }); + await rebuildPromise; + + let buttons = oneOffSearchButtons.localButtons; + Assert.ok(buttons.length, "Sanity check: Local shortcuts exist"); + + for (let button of buttons) { + Assert.ok(button.source, "Sanity check: Button has a source"); + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeMouseAtCenter(button, {}); + await searchPromise; + Assert.ok( + UrlbarTestUtils.isPopupOpen(window), + "Urlbar view is still open." + ); + await UrlbarTestUtils.assertSearchMode(window, { + source: button.source, + entry: "oneoff", + }); + } + + await UrlbarTestUtils.exitSearchMode(window, { backspace: true }); + await hidePopup(); +}); + +// Presses the Return key when a local shortcut is selected. +add_task(async function localShortcutReturn() { + // We are explicitly using something that looks like a url, to make the test + // stricter. Even if it looks like a url, we should search. + let typedValue = "foo.bar"; + + oneOffSearchButtons.invalidateCache(); + let rebuildPromise = BrowserTestUtils.waitForEvent( + oneOffSearchButtons, + "rebuild" + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: typedValue, + }); + await rebuildPromise; + + let buttons = oneOffSearchButtons.localButtons; + Assert.ok(buttons.length, "Sanity check: Local shortcuts exist"); + + let allButtons = oneOffSearchButtons.getSelectableButtons(false); + let firstLocalIndex = allButtons.length - buttons.length; + + for (let i = 0; i < buttons.length; i++) { + let button = buttons[i]; + + // Alt+Down enough times to select the button. + let index = firstLocalIndex + i; + EventUtils.synthesizeKey("KEY_ArrowDown", { + altKey: true, + repeat: index + 1, + }); + await TestUtils.waitForCondition( + () => oneOffSearchButtons.selectedButtonIndex == index, + "Waiting for local shortcut to become selected" + ); + + let expectedSelectedResultIndex = -1; + let count = UrlbarTestUtils.getResultCount(window); + if (count > 0) { + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + if (result.heuristic) { + expectedSelectedResultIndex = 0; + } + } + assertState(expectedSelectedResultIndex, index, typedValue); + + Assert.ok(button.source, "Sanity check: Button has a source"); + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey("KEY_Enter"); + await searchPromise; + Assert.ok( + UrlbarTestUtils.isPopupOpen(window), + "Urlbar view is still open." + ); + await UrlbarTestUtils.assertSearchMode(window, { + source: button.source, + entry: "oneoff", + }); + } + + await UrlbarTestUtils.exitSearchMode(window, { backspace: true }); + await hidePopup(); +}); + +// With an empty search string, clicking a local shortcut should result in no +// heuristic result. +add_task(async function localShortcutEmptySearchString() { + oneOffSearchButtons.invalidateCache(); + let rebuildPromise = BrowserTestUtils.waitForEvent( + oneOffSearchButtons, + "rebuild" + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + await rebuildPromise; + + Assert.equal( + UrlbarTestUtils.getOneOffSearchButtonsVisible(window), + true, + "One-offs are visible" + ); + + let buttons = oneOffSearchButtons.localButtons; + Assert.ok(buttons.length, "Sanity check: Local shortcuts exist"); + + for (let button of buttons) { + Assert.ok(button.source, "Sanity check: Button has a source"); + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeMouseAtCenter(button, {}); + await searchPromise; + Assert.ok( + UrlbarTestUtils.isPopupOpen(window), + "Urlbar view is still open." + ); + Assert.equal( + UrlbarTestUtils.getOneOffSearchButtonsVisible(window), + true, + "One-offs are visible" + ); + await UrlbarTestUtils.assertSearchMode(window, { + source: button.source, + entry: "oneoff", + }); + + let resultCount = UrlbarTestUtils.getResultCount(window); + if (!resultCount) { + Assert.equal( + gURLBar.panel.getAttribute("noresults"), + "true", + "Panel has no results, therefore should have noresults attribute" + ); + continue; + } + Assert.ok( + !gURLBar.panel.hasAttribute("noresults"), + "Panel has results, therefore should not have noresults attribute" + ); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(!result.heuristic, "The first result should not be heuristic"); + } + + await UrlbarTestUtils.exitSearchMode(window, { backspace: true }); + + await hidePopup(); +}); + +// Trigger SearchOneOffs.willHide() outside of SearchOneOffs.__rebuild(). Ensure +// that we always show the correct engines in the one-offs. This effectively +// tests SearchOneOffs._engineInfo.domWasUpdated. +add_task(async function avoidWillHideRace() { + // We set maxHistoricalSearchSuggestions to 0 since this test depends on + // UrlbarView calling SearchOneOffs.willHide(). That only happens when the + // Urlbar is in search mode after a query that returned no results. + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.maxHistoricalSearchSuggestions", 0]], + }); + + oneOffSearchButtons.invalidateCache(); + + // Accel+K triggers SearchOneOffs.willHide() from UrlbarView instead of from + // SearchOneOffs.__rebuild. + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey("k", { accelKey: true }); + await searchPromise; + Assert.ok( + UrlbarTestUtils.getOneOffSearchButtonsVisible(window), + "One-offs should be visible" + ); + await UrlbarTestUtils.promisePopupClose(window); + + info("Hide all engines but the test engine."); + let oldDefaultEngine = await Services.search.getDefault(); + await Services.search.setDefault(engine); + let engines = (await Services.search.getVisibleEngines()).filter( + e => e.name != engine.name + ); + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.search.hiddenOneOffs", engines.map(e => e.name).join(",")], + ...UrlbarUtils.LOCAL_SEARCH_MODES.map(m => [ + `browser.urlbar.${m.pref}`, + false, + ]), + ], + }); + Assert.ok( + !oneOffSearchButtons._engineInfo, + "_engineInfo should be nulled out." + ); + + // This call to SearchOneOffs.willHide() should repopulate _engineInfo but not + // rebuild the one-offs. _engineInfo.willHide will be true and thus UrlbarView + // will not open. + EventUtils.synthesizeKey("k", { accelKey: true }); + // We can't wait for UrlbarTestUtils.promiseSearchComplete here since we + // expect the popup will not open. We wait for _engineInfo to be populated + // instead. + await BrowserTestUtils.waitForCondition( + () => !!oneOffSearchButtons._engineInfo, + "_engineInfo is set." + ); + Assert.ok(!UrlbarTestUtils.isPopupOpen(window), "The UrlbarView is closed."); + Assert.equal( + oneOffSearchButtons._engineInfo.willHide, + true, + "_engineInfo should be repopulated and willHide should be true." + ); + Assert.equal( + oneOffSearchButtons._engineInfo.domWasUpdated, + undefined, + "domWasUpdated should not be populated since we haven't yet tried to rebuild the one-offs." + ); + + // Now search. The view will open and the one-offs will rebuild, although + // the one-offs will not be shown since there is only one engine. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + Assert.equal( + oneOffSearchButtons._engineInfo.domWasUpdated, + true, + "domWasUpdated should be true" + ); + Assert.ok( + !UrlbarTestUtils.getOneOffSearchButtonsVisible(window), + "One-offs should be hidden since there is only one engine." + ); + await UrlbarTestUtils.promisePopupClose(window); + + await SpecialPowers.popPrefEnv(); + await Services.search.setDefault(oldDefaultEngine); + await SpecialPowers.popPrefEnv(); +}); + +// Hides each of the local shortcuts one at a time. The search buttons should +// automatically rebuild themselves. +add_task(async function individualLocalShortcutsHidden() { + for (let { pref, source } of UrlbarUtils.LOCAL_SEARCH_MODES) { + await SpecialPowers.pushPrefEnv({ + set: [[`browser.urlbar.${pref}`, false]], + }); + + let rebuildPromise = BrowserTestUtils.waitForEvent( + oneOffSearchButtons, + "rebuild" + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + await rebuildPromise; + + Assert.equal( + UrlbarTestUtils.getOneOffSearchButtonsVisible(window), + true, + "One-offs are visible" + ); + + let buttons = oneOffSearchButtons.localButtons; + Assert.ok(buttons.length, "Sanity check: Local shortcuts exist"); + + let otherModes = UrlbarUtils.LOCAL_SEARCH_MODES.filter( + m => m.source != source + ); + Assert.equal( + buttons.length, + otherModes.length, + "Expected number of enabled local shortcut buttons" + ); + + for (let i = 0; i < buttons.length; i++) { + Assert.equal( + buttons[i].source, + otherModes[i].source, + "Button has the expected source" + ); + } + + await hidePopup(); + await SpecialPowers.popPrefEnv(); + } +}); + +// Hides all the local shortcuts at once. +add_task(async function allLocalShortcutsHidden() { + await SpecialPowers.pushPrefEnv({ + set: UrlbarUtils.LOCAL_SEARCH_MODES.map(m => [ + `browser.urlbar.${m.pref}`, + false, + ]), + }); + + let rebuildPromise = BrowserTestUtils.waitForEvent( + oneOffSearchButtons, + "rebuild" + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + await rebuildPromise; + + Assert.equal( + UrlbarTestUtils.getOneOffSearchButtonsVisible(window), + true, + "One-offs are visible" + ); + + Assert.equal( + oneOffSearchButtons.localButtons.length, + 0, + "All local shortcuts should be hidden" + ); + + Assert.greater( + oneOffSearchButtons.getSelectableButtons(false).filter(b => b.engine) + .length, + 0, + "Engine one-offs should not be hidden" + ); + + await hidePopup(); + await SpecialPowers.popPrefEnv(); +}); + +// Hides all the engines but none of the local shortcuts. +add_task(async function localShortcutsShownWhenEnginesHidden() { + let engines = await Services.search.getVisibleEngines(); + await SpecialPowers.pushPrefEnv({ + set: [["browser.search.hiddenOneOffs", engines.map(e => e.name).join(",")]], + }); + + let rebuildPromise = BrowserTestUtils.waitForEvent( + oneOffSearchButtons, + "rebuild" + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + await rebuildPromise; + + Assert.equal( + UrlbarTestUtils.getOneOffSearchButtonsVisible(window), + true, + "One-offs are visible" + ); + + Assert.equal( + oneOffSearchButtons.localButtons.length, + UrlbarUtils.LOCAL_SEARCH_MODES.length, + "All local shortcuts are visible" + ); + + Assert.equal( + oneOffSearchButtons.getSelectableButtons(false).filter(b => b.engine) + .length, + 0, + "All engine one-offs are hidden" + ); + + await hidePopup(); + await SpecialPowers.popPrefEnv(); +}); + +/** + * Checks that the local shortcuts are shown correctly. + */ +async function doLocalShortcutsShownTest() { + let rebuildPromise = BrowserTestUtils.waitForEvent( + oneOffSearchButtons, + "rebuild" + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "doLocalShortcutsShownTest", + }); + await rebuildPromise; + + let buttons = oneOffSearchButtons.localButtons; + Assert.equal(buttons.length, 3, "Expected number of local shortcuts"); + + let expectedSource; + let seenIDs = new Set(); + for (let button of buttons) { + Assert.ok( + !seenIDs.has(button.id), + "Should not have already seen button.id" + ); + seenIDs.add(button.id); + switch (button.id) { + case "urlbar-engine-one-off-item-bookmarks": + expectedSource = UrlbarUtils.RESULT_SOURCE.BOOKMARKS; + break; + case "urlbar-engine-one-off-item-tabs": + expectedSource = UrlbarUtils.RESULT_SOURCE.TABS; + break; + case "urlbar-engine-one-off-item-history": + expectedSource = UrlbarUtils.RESULT_SOURCE.HISTORY; + break; + default: + Assert.ok(false, `Unexpected local shortcut ID: ${button.id}`); + break; + } + Assert.equal(button.source, expectedSource, "Expected button.source"); + } + + await hidePopup(); +} + +function assertState(result, oneOff, textValue = undefined) { + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + result, + "Expected result should be selected" + ); + Assert.equal( + oneOffSearchButtons.selectedButtonIndex, + oneOff, + "Expected one-off should be selected" + ); + if (textValue !== undefined) { + Assert.equal(gURLBar.value, textValue, "Expected value"); + } +} + +function hidePopup() { + return UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_Escape"); + }); +} diff --git a/browser/components/urlbar/tests/browser/browser_oneOffs_contextMenu.js b/browser/components/urlbar/tests/browser/browser_oneOffs_contextMenu.js new file mode 100644 index 0000000000..60d46608cd --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_oneOffs_contextMenu.js @@ -0,0 +1,80 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that the right-click menu works correctly for the one-off buttons. + */ + +const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml"; + +let gMaxResults; + +XPCOMUtils.defineLazyGetter(this, "oneOffSearchButtons", () => { + return UrlbarTestUtils.getOneOffSearchButtons(window); +}); + +let originalEngine; +let newEngine; + +// The one-off context menu should not be shown. +add_task(async function contextMenu_not_shown() { + // Add a popupshown listener on the context menu that sets this + // popupshownFired boolean. + let popupshownFired = false; + let onPopupshown = () => { + popupshownFired = true; + }; + let contextMenu = oneOffSearchButtons.querySelector( + ".search-one-offs-context-menu" + ); + contextMenu.addEventListener("popupshown", onPopupshown); + + // Do a search to open the view. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "foo", + }); + + // First, try to open the context menu on a remote engine. + let allOneOffs = oneOffSearchButtons.getSelectableButtons(true); + Assert.greater(allOneOffs.length, 0, "There should be at least one one-off"); + Assert.ok( + allOneOffs[0].engine, + "The first one-off should be a remote one-off" + ); + EventUtils.synthesizeMouseAtCenter(allOneOffs[0], { + type: "contextmenu", + button: 2, + }); + let timeout = 500; + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, timeout)); + Assert.ok( + !popupshownFired, + "popupshown should not be fired on a remote one-off" + ); + + // Now try to open the context menu on a local one-off. + let localOneOffs = oneOffSearchButtons.localButtons; + Assert.greater( + localOneOffs.length, + 0, + "There should be at least one local one-off" + ); + EventUtils.synthesizeMouseAtCenter(localOneOffs[0], { + type: "contextmenu", + button: 2, + }); + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, timeout)); + Assert.ok( + !popupshownFired, + "popupshown should not be fired on a local one-off" + ); + + contextMenu.removeEventListener("popupshown", onPopupshown); + await UrlbarTestUtils.promisePopupClose(window); + await SpecialPowers.popPrefEnv(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_oneOffs_heuristicRestyle.js b/browser/components/urlbar/tests/browser/browser_oneOffs_heuristicRestyle.js new file mode 100644 index 0000000000..e0bf868ee9 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_oneOffs_heuristicRestyle.js @@ -0,0 +1,506 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that heuristic results are updated/restyled to search results when a + * one-off is selected. + */ + +"use strict"; + +XPCOMUtils.defineLazyGetter(this, "oneOffSearchButtons", () => { + return UrlbarTestUtils.getOneOffSearchButtons(window); +}); + +const TEST_DEFAULT_ENGINE_NAME = "Test"; + +const HISTORY_URL = "https://mozilla.org/"; + +const KEYWORD = "kw"; +const KEYWORD_URL = "https://mozilla.org/search?q=%s"; + +// Expected result data for our test results. +const RESULT_DATA_BY_TYPE = { + [UrlbarUtils.RESULT_TYPE.URL]: { + icon: `page-icon:${HISTORY_URL}`, + actionL10n: { + id: "urlbar-result-action-visit", + }, + }, + [UrlbarUtils.RESULT_TYPE.SEARCH]: { + icon: "chrome://browser/skin/search-glass.svg", + actionL10n: { + id: "urlbar-result-action-search-w-engine", + args: { engine: TEST_DEFAULT_ENGINE_NAME }, + }, + }, + [UrlbarUtils.RESULT_TYPE.KEYWORD]: { + icon: `page-icon:${KEYWORD_URL}`, + }, +}; + +function getSourceIcon(source) { + switch (source) { + case UrlbarUtils.RESULT_SOURCE.BOOKMARKS: + return "chrome://browser/skin/bookmark.svg"; + case UrlbarUtils.RESULT_SOURCE.HISTORY: + return "chrome://browser/skin/history.svg"; + case UrlbarUtils.RESULT_SOURCE.TABS: + return "chrome://browser/skin/tab.svg"; + default: + return null; + } +} + +/** + * Asserts that the heuristic result is *not* restyled to look like a search + * result. + * + * @param {UrlbarUtils.RESULT_TYPE} expectedType + * The expected type of the heuristic. + * @param {object} resultDetails + * The return value of UrlbarTestUtils.getDetailsOfResultAt(window, 0). + */ +async function heuristicIsNotRestyled(expectedType, resultDetails) { + Assert.equal( + resultDetails.type, + expectedType, + "The restyled result is the expected type." + ); + + Assert.equal( + resultDetails.displayed.title, + resultDetails.title, + "The displayed title is equal to the payload title." + ); + + let data = RESULT_DATA_BY_TYPE[expectedType]; + Assert.ok(data, "Sanity check: Expected type is recognized"); + + let [actionText] = data.actionL10n + ? await document.l10n.formatValues([data.actionL10n]) + : [""]; + Assert.equal( + resultDetails.displayed.action, + actionText, + "The result has the expected non-styled action text." + ); + + Assert.equal( + BrowserTestUtils.is_visible(resultDetails.element.separator), + !!actionText, + "The title separator is " + (actionText ? "visible" : "hidden") + ); + Assert.equal( + BrowserTestUtils.is_visible(resultDetails.element.action), + !!actionText, + "The action text is " + (actionText ? "visible" : "hidden") + ); + + Assert.equal( + resultDetails.image, + data.icon, + "The result has the expected non-styled icon." + ); +} + +/** + * Asserts that the heuristic result is restyled to look like a search result. + * + * @param {UrlbarUtils.RESULT_TYPE} expectedType + * The expected type of the heuristic. + * @param {object} resultDetails + * The return value of UrlbarTestUtils.getDetailsOfResultAt(window, 0). + * @param {string} searchString + * The current search string. The restyled heuristic result's title is + * expected to be this string. + * @param {element} selectedOneOff + * The selected one-off button. + */ +async function heuristicIsRestyled( + expectedType, + resultDetails, + searchString, + selectedOneOff +) { + let engine = selectedOneOff.engine; + let source = selectedOneOff.source; + if (!engine && !source) { + Assert.fail("An invalid one-off was passed to urlbarResultIsRestyled"); + return; + } + Assert.equal( + resultDetails.type, + expectedType, + "The restyled result is still the expected type." + ); + + let actionText; + if (engine) { + [actionText] = await document.l10n.formatValues([ + { + id: "urlbar-result-action-search-w-engine", + args: { engine: engine.name }, + }, + ]); + } else if (source) { + [actionText] = await document.l10n.formatValues([ + { + id: `urlbar-result-action-search-${UrlbarUtils.getResultSourceName( + source + )}`, + }, + ]); + } + Assert.equal( + resultDetails.displayed.action, + actionText, + "Restyled result's action text should be updated" + ); + + Assert.equal( + resultDetails.displayed.title, + searchString, + "The restyled result's title should be equal to the search string." + ); + + Assert.ok( + BrowserTestUtils.is_visible(resultDetails.element.separator), + "The restyled result's title separator should be visible" + ); + Assert.ok( + BrowserTestUtils.is_visible(resultDetails.element.action), + "The restyled result's action text should be visible" + ); + + if (engine) { + Assert.equal( + resultDetails.image, + engine.iconURI?.spec || UrlbarUtils.ICON.SEARCH_GLASS, + "The restyled result's icon should be the engine's icon." + ); + } else if (source) { + Assert.equal( + resultDetails.image, + getSourceIcon(source), + "The restyled result's icon should be the local one-off's icon." + ); + } +} + +/** + * Asserts that the specified one-off (if any) is selected and that the + * heuristic result is either restyled or not restyled as appropriate. If + * there's a selected one-off, then the heuristic is expected to be restyled; if + * there's no selected one-off, then it's expected not to be restyled. + * + * @param {string} searchString + * The current search string. If a one-off is selected, then the restyled + * heuristic result's title is expected to be this string. + * @param {UrlbarUtils.RESULT_TYPE} expectedHeuristicType + * The expected type of the heuristic. + * @param {number} expectedSelectedOneOffIndex + * The index of the expected selected one-off button. If no one-off is + * expected to be selected, then pass -1. + */ +async function assertState( + searchString, + expectedHeuristicType, + expectedSelectedOneOffIndex +) { + Assert.equal( + oneOffSearchButtons.selectedButtonIndex, + expectedSelectedOneOffIndex, + "Expected one-off should be selected" + ); + + let resultDetails = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + if (expectedSelectedOneOffIndex >= 0) { + await heuristicIsRestyled( + expectedHeuristicType, + resultDetails, + searchString, + oneOffSearchButtons.selectedButton + ); + } else { + await heuristicIsNotRestyled(expectedHeuristicType, resultDetails); + } +} + +add_task(async function init() { + let oldDefaultEngine = await Services.search.getDefault(); + let engine = await Services.search.addEngineWithDetails( + TEST_DEFAULT_ENGINE_NAME, + { + template: `http://example.com/?search={searchTerms}`, + } + ); + await Services.search.setDefault(engine); + await Services.search.moveEngine(engine, 0); + + for (let i = 0; i < 5; i++) { + await PlacesTestUtils.addVisits(HISTORY_URL); + } + + await PlacesUtils.keywords.insert({ + keyword: KEYWORD, + url: KEYWORD_URL, + }); + + registerCleanupFunction(async function() { + await Services.search.setDefault(oldDefaultEngine); + await Services.search.removeEngine(engine); + await PlacesUtils.history.clear(); + await PlacesUtils.keywords.remove(KEYWORD); + }); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.searches", false]], + }); + + // Move the mouse away from the view so that a result or one-off isn't + // inadvertently highlighted. See bug 1659011. + EventUtils.synthesizeMouse( + gURLBar.inputField, + 0, + 0, + { type: "mousemove" }, + window + ); +}); + +add_task(async function arrow_engine_url() { + await doArrowTest("mozilla.or", UrlbarUtils.RESULT_TYPE.URL, false); +}); + +add_task(async function arrow_engine_search() { + await doArrowTest("test", UrlbarUtils.RESULT_TYPE.SEARCH, false); +}); + +add_task(async function arrow_engine_keyword() { + await doArrowTest(`${KEYWORD} test`, UrlbarUtils.RESULT_TYPE.KEYWORD, false); +}); + +add_task(async function arrow_local_url() { + await doArrowTest("mozilla.or", UrlbarUtils.RESULT_TYPE.URL, true); +}); + +add_task(async function arrow_local_search() { + await doArrowTest("test", UrlbarUtils.RESULT_TYPE.SEARCH, true); +}); + +add_task(async function arrow_local_keyword() { + await doArrowTest(`${KEYWORD} test`, UrlbarUtils.RESULT_TYPE.KEYWORD, true); +}); + +/** + * Arrows down to the one-offs, checks the heuristic, and clicks it. + * + * @param {string} searchString + * The search string to use. + * @param {UrlbarUtils.RESULT_TYPE} expectedHeuristicType + * The type of heuristic result that the search string is expected to trigger. + * @param {boolean} useLocal + * Whether to test a local one-off or an engine one-off. If true, test a + * local one-off. If false, test an engine one-off. + */ +async function doArrowTest(searchString, expectedHeuristicType, useLocal) { + await doTest(searchString, expectedHeuristicType, useLocal, async () => { + info( + "Arrow down to the one-offs, observe heuristic is restyled as a search result." + ); + let resultCount = UrlbarTestUtils.getResultCount(window); + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey("KEY_ArrowDown", { repeat: resultCount }); + await searchPromise; + await assertState(searchString, expectedHeuristicType, 0); + + let depth = 1; + if (useLocal) { + for (; !oneOffSearchButtons.selectedButton.source; depth++) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + } + Assert.ok( + oneOffSearchButtons.selectedButton.source, + "Selected one-off is local" + ); + await assertState(searchString, expectedHeuristicType, depth - 1); + } + + info( + "Arrow up out of the one-offs, observe heuristic styling is restored." + ); + EventUtils.synthesizeKey("KEY_ArrowUp", { repeat: depth }); + await assertState(searchString, expectedHeuristicType, -1); + + info( + "Arrow back down into the one-offs, observe heuristic is restyled as a search result." + ); + EventUtils.synthesizeKey("KEY_ArrowDown", { repeat: depth }); + await assertState(searchString, expectedHeuristicType, depth - 1); + }); +} + +add_task(async function altArrow_engine_url() { + await doAltArrowTest("mozilla.or", UrlbarUtils.RESULT_TYPE.URL, false); +}); + +add_task(async function altArrow_engine_search() { + await doAltArrowTest("test", UrlbarUtils.RESULT_TYPE.SEARCH, false); +}); + +add_task(async function altArrow_engine_keyword() { + await doAltArrowTest( + `${KEYWORD} test`, + UrlbarUtils.RESULT_TYPE.KEYWORD, + false + ); +}); + +add_task(async function altArrow_local_url() { + await doAltArrowTest("mozilla.or", UrlbarUtils.RESULT_TYPE.URL, true); +}); + +add_task(async function altArrow_local_search() { + await doAltArrowTest("test", UrlbarUtils.RESULT_TYPE.SEARCH, true); +}); + +add_task(async function altArrow_local_keyword() { + await doAltArrowTest( + `${KEYWORD} test`, + UrlbarUtils.RESULT_TYPE.KEYWORD, + true + ); +}); + +/** + * Alt-arrows down to the one-offs so that the heuristic remains selected, + * checks the heuristic, and clicks it. + * + * @param {string} searchString + * The search string to use. + * @param {UrlbarUtils.RESULT_TYPE} expectedHeuristicType + * The type of heuristic result that the search string is expected to trigger. + * @param {boolean} useLocal + * Whether to test a local one-off or an engine one-off. If true, test a + * local one-off. If false, test an engine one-off. + */ +async function doAltArrowTest(searchString, expectedHeuristicType, useLocal) { + await doTest(searchString, expectedHeuristicType, useLocal, async () => { + info( + "Alt+down into the one-offs, observe heuristic is restyled as a search result." + ); + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + await searchPromise; + await assertState(searchString, expectedHeuristicType, 0); + + let depth = 1; + if (useLocal) { + for (; !oneOffSearchButtons.selectedButton.source; depth++) { + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + } + Assert.ok( + oneOffSearchButtons.selectedButton.source, + "Selected one-off is local" + ); + await assertState(searchString, expectedHeuristicType, depth - 1); + } + + info( + "Arrow down and then up to re-select the heuristic, observe its styling is restored." + ); + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_ArrowUp"); + await assertState(searchString, expectedHeuristicType, -1); + + info( + "Alt+down into the one-offs, observe the heuristic is restyled as a search result." + ); + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true, repeat: depth }); + await assertState(searchString, expectedHeuristicType, depth - 1); + + info("Alt+up out of the one-offs, observe the heuristic is restored."); + EventUtils.synthesizeKey("KEY_ArrowUp", { altKey: true, repeat: depth }); + await assertState(searchString, expectedHeuristicType, -1); + + info( + "Alt+down into the one-offs, observe the heuristic is restyled as a search result." + ); + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true, repeat: depth }); + await assertState(searchString, expectedHeuristicType, depth - 1); + }); +} + +/** + * The main test function. Starts a search, asserts that the heuristic has the + * expected type, calls a callback to run more checks, and then finally clicks + * the restyled heuristic to make sure search mode is confirmed. + * + * @param {string} searchString + * The search string to use. + * @param {UrlbarUtils.RESULT_TYPE} expectedHeuristicType + * The type of heuristic result that the search string is expected to trigger. + * @param {boolean} useLocal + * Whether to test a local one-off or an engine one-off. If true, test a + * local one-off. If false, test an engine one-off. + * @param {function} callback + * This is called after the search completes. It should perform whatever + * checks are necessary for the test task. Important: When it returns, it + * should make sure that the first one-off is selected. + */ +async function doTest(searchString, expectedHeuristicType, useLocal, callback) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: searchString, + fireInputEvent: true, + }); + await TestUtils.waitForCondition( + () => !oneOffSearchButtons._rebuilding, + "Waiting for one-offs to finish rebuilding" + ); + + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(result.heuristic, "First result is heuristic"); + Assert.equal( + result.type, + expectedHeuristicType, + "Heuristic is expected type" + ); + await assertState(searchString, expectedHeuristicType, -1); + + await callback(); + + Assert.ok( + oneOffSearchButtons.selectedButton, + "The callback should leave a one-off selected so that the heuristic remains re-styled" + ); + + info("Click the heuristic result and observe it confirms search mode."); + let selectedButton = oneOffSearchButtons.selectedButton; + let expectedSearchMode = { + entry: "oneoff", + isPreview: true, + }; + if (useLocal) { + expectedSearchMode.source = selectedButton.source; + } else { + expectedSearchMode.engineName = selectedButton.engine.name; + } + + await UrlbarTestUtils.assertSearchMode(window, expectedSearchMode); + + let heuristicRow = await UrlbarTestUtils.waitForAutocompleteResultAt( + window, + 0 + ); + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeMouseAtCenter(heuristicRow, {}); + await searchPromise; + + expectedSearchMode.isPreview = false; + await UrlbarTestUtils.assertSearchMode(window, expectedSearchMode); + + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); +} diff --git a/browser/components/urlbar/tests/browser/browser_oneOffs_keyModifiers.js b/browser/components/urlbar/tests/browser/browser_oneOffs_keyModifiers.js new file mode 100644 index 0000000000..786818b9be --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_oneOffs_keyModifiers.js @@ -0,0 +1,389 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that one-offs behave differently with key modifiers. + */ + +"use strict"; + +const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml"; +const SEARCH_STRING = "foo.bar"; + +XPCOMUtils.defineLazyGetter(this, "oneOffSearchButtons", () => { + return UrlbarTestUtils.getOneOffSearchButtons(window); +}); + +let engine; + +async function searchAndOpenPopup(value) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value, + fireInputEvent: true, + }); + await TestUtils.waitForCondition( + () => !oneOffSearchButtons._rebuilding, + "Waiting for one-offs to finish rebuilding" + ); +} + +add_task(async function init() { + // Add a search suggestion engine and move it to the front so that it appears + // as the first one-off. + engine = await SearchTestUtils.promiseNewSearchEngine( + getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME + ); + await Services.search.moveEngine(engine, 0); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.search.separatePrivateDefault.ui.enabled", false]], + }); + + registerCleanupFunction(async function() { + await PlacesUtils.history.clear(); + await UrlbarTestUtils.formHistory.clear(); + }); + + // Initialize history with enough visits to fill up the view. + await PlacesUtils.history.clear(); + await UrlbarTestUtils.formHistory.clear(); + let maxResults = Services.prefs.getIntPref("browser.urlbar.maxRichResults"); + for (let i = 0; i < maxResults; i++) { + await PlacesTestUtils.addVisits( + "http://mochi.test:8888/browser_urlbarOneOffs.js/?" + i + ); + } + + // Add some more visits to the last URL added above so that the top-sites view + // will be non-empty. + for (let i = 0; i < 5; i++) { + await PlacesTestUtils.addVisits( + "http://mochi.test:8888/browser_urlbarOneOffs.js/?" + (maxResults - 1) + ); + } + await updateTopSites(sites => { + return ( + sites && sites[0] && sites[0].url.startsWith("http://mochi.test:8888/") + ); + }); + + // Move the mouse away from the view so that a result or one-off isn't + // inadvertently highlighted. See bug 1659011. + EventUtils.synthesizeMouse( + gURLBar.inputField, + 0, + 0, + { type: "mousemove" }, + window + ); +}); + +// Shift clicking with no search string should open search mode, like an +// unmodified click. +add_task(async function shift_click_empty() { + await searchAndOpenPopup(""); + let oneOffs = oneOffSearchButtons.getSelectableButtons(true); + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeMouseAtCenter(oneOffs[0], { shiftKey: true }); + await searchPromise; + await UrlbarTestUtils.assertSearchMode(window, { + engineName: oneOffs[0].engine.name, + entry: "oneoff", + }); + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); +}); + +// Shift clicking with a search string should perform a search in the current +// tab. +add_task(async function shift_click_search() { + await searchAndOpenPopup(SEARCH_STRING); + let resultsPromise = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + "http://mochi.test:8888/?terms=foo.bar" + ); + let oneOffs = oneOffSearchButtons.getSelectableButtons(true); + EventUtils.synthesizeMouseAtCenter(oneOffs[0], { shiftKey: true }); + await resultsPromise; + await UrlbarTestUtils.assertSearchMode(window, null); + await UrlbarTestUtils.promisePopupClose(window); +}); + +// Pressing Shift+Enter on a one-off with no search string should open search +// mode, like an unmodified click. +add_task(async function shift_enter_empty() { + await searchAndOpenPopup(""); + // Alt+Down to select the first one-off. + let oneOffs = oneOffSearchButtons.getSelectableButtons(true); + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey("KEY_Enter", { shiftKey: true }); + await searchPromise; + await UrlbarTestUtils.assertSearchMode(window, { + engineName: oneOffs[0].engine.name, + entry: "oneoff", + }); + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); +}); + +// Pressing Shift+Enter on a one-off with a search string should perform a +// search in the current tab. +add_task(async function shift_enter_search() { + await searchAndOpenPopup(SEARCH_STRING); + let resultsPromise = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + "http://mochi.test:8888/?terms=foo.bar" + ); + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + EventUtils.synthesizeKey("KEY_Enter", { shiftKey: true }); + await resultsPromise; + await UrlbarTestUtils.assertSearchMode(window, null); + await UrlbarTestUtils.promisePopupClose(window); +}); + +// Pressing Alt+Enter on a one-off on an "empty" page (e.g. new tab) should open +// search mode in the current tab. +add_task(async function alt_enter_emptypage() { + await BrowserTestUtils.withNewTab("about:home", async function(browser) { + await searchAndOpenPopup(SEARCH_STRING); + let oneOffs = oneOffSearchButtons.getSelectableButtons(true); + // Alt+Down to select the first one-off. + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey("KEY_Enter", { altKey: true }); + await searchPromise; + Assert.equal( + browser, + gBrowser.selectedBrowser, + "The foreground tab hasn't changed." + ); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: oneOffs[0].engine.name, + entry: "oneoff", + }); + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); + }); +}); + +// Pressing Alt+Enter on a one-off with no search string and on a "non-empty" +// page should open search mode in a new foreground tab. +add_task(async function alt_enter_empty() { + await BrowserTestUtils.withNewTab("about:robots", async function(browser) { + await searchAndOpenPopup(""); + let oneOffs = oneOffSearchButtons.getSelectableButtons(true); + // Alt+Down to select the first one-off. + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + let tabOpenPromise = BrowserTestUtils.waitForEvent( + gBrowser.tabContainer, + "TabOpen" + ); + EventUtils.synthesizeKey("KEY_Enter", { altKey: true }); + await tabOpenPromise; + Assert.notEqual( + browser, + gBrowser.selectedBrowser, + "The current foreground tab is new." + ); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: oneOffs[0].engine.name, + entry: "oneoff", + }); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + Assert.equal( + browser, + gBrowser.selectedBrowser, + "We're back in the original tab." + ); + await UrlbarTestUtils.assertSearchMode(window, null); + await UrlbarTestUtils.promisePopupClose(window); + }); +}); + +// Pressing Alt+Enter on a remote one-off with a search string and on a +// "non-empty" page should perform a search in a new foreground tab. +add_task(async function alt_enter_search_remote() { + await BrowserTestUtils.withNewTab("about:robots", async function(browser) { + await searchAndOpenPopup(SEARCH_STRING); + // Alt+Down to select the first one-off. + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + let tabOpenPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + "http://mochi.test:8888/?terms=foo.bar", + true + ); + EventUtils.synthesizeKey("KEY_Enter", { altKey: true }); + // This implictly checks the correct page is loaded. + let newTab = await tabOpenPromise; + Assert.equal( + newTab, + gBrowser.selectedTab, + "The current foreground tab is new." + ); + // Check search mode is not activated in the new tab. + await UrlbarTestUtils.assertSearchMode(window, null); + BrowserTestUtils.removeTab(newTab); + Assert.equal( + browser, + gBrowser.selectedBrowser, + "We're back in the original tab." + ); + await UrlbarTestUtils.assertSearchMode(window, null); + await UrlbarTestUtils.promisePopupClose(window); + }); +}); + +// Pressing Alt+Enter on a local one-off with a search string and on a +// "non-empty" page should open search mode in a new foreground tab with the +// search string already populated. +add_task(async function alt_enter_search_local() { + await BrowserTestUtils.withNewTab("about:robots", async function(browser) { + await searchAndOpenPopup(SEARCH_STRING); + // Alt+Down to select the first local one-off. + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + while ( + oneOffSearchButtons.selectedButton.id != + "urlbar-engine-one-off-item-bookmarks" + ) { + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + } + let tabOpenPromise = BrowserTestUtils.waitForEvent( + gBrowser.tabContainer, + "TabOpen" + ); + EventUtils.synthesizeKey("KEY_Enter", { altKey: true }); + await tabOpenPromise; + Assert.notEqual( + browser, + gBrowser.selectedBrowser, + "The current foreground tab is new." + ); + await UrlbarTestUtils.assertSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + entry: "oneoff", + }); + Assert.equal( + gURLBar.value, + SEARCH_STRING, + "The search term was duplicated to the new tab." + ); + BrowserTestUtils.removeTab(gBrowser.selectedTab); + Assert.equal( + browser, + gBrowser.selectedBrowser, + "We're back in the original tab." + ); + await UrlbarTestUtils.assertSearchMode(window, null); + await UrlbarTestUtils.promisePopupClose(window); + }); +}); + +// Accel+Clicking a one-off with an empty search string should open search mode +// in a new background tab. +add_task(async function accel_click_empty() { + await searchAndOpenPopup(""); + let oneOffs = oneOffSearchButtons.getSelectableButtons(true); + + // We have to listen for the new tab using this brute force method. + // about:newtab is preloaded in the background. When about:newtab is opened, + // the cached version is shown. Since the page is already loaded, + // waitForNewTab does not detect it. It also doesn't fire the TabOpen event. + let tabCount = gBrowser.tabs.length; + let tabOpenPromise = TestUtils.waitForCondition( + () => + gBrowser.tabs.length == tabCount + 1 + ? gBrowser.tabs[gBrowser.tabs.length - 1] + : false, + "Waiting for background about:newtab to open." + ); + EventUtils.synthesizeMouseAtCenter(oneOffs[0], { accelKey: true }); + let newTab = await tabOpenPromise; + Assert.notEqual( + newTab.linkedBrowser, + gBrowser.selectedBrowser, + "The foreground tab hasn't changed." + ); + await UrlbarTestUtils.assertSearchMode(window, null); + BrowserTestUtils.switchTab(gBrowser, newTab); + // Check the new background tab is already in search mode. + await UrlbarTestUtils.assertSearchMode(window, { + engineName: oneOffs[0].engine.name, + entry: "oneoff", + }); + BrowserTestUtils.removeTab(newTab); + await UrlbarTestUtils.promisePopupClose(window); +}); + +// Accel+Clicking a remote one-off with a search string should execute a search +// in a new background tab. +add_task(async function accel_click_search_remote() { + await searchAndOpenPopup(SEARCH_STRING); + let oneOffs = oneOffSearchButtons.getSelectableButtons(true); + let tabOpenPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + "http://mochi.test:8888/?terms=foo.bar", + true + ); + EventUtils.synthesizeMouseAtCenter(oneOffs[0], { accelKey: true }); + // This implictly checks the correct page is loaded. + let newTab = await tabOpenPromise; + Assert.notEqual( + gBrowser.selectedTab, + newTab, + "The foreground tab hasn't changed." + ); + await UrlbarTestUtils.assertSearchMode(window, null); + // Switch to the background tab, which is the last tab in gBrowser.tabs. + BrowserTestUtils.switchTab(gBrowser, newTab); + // Check the new background tab is not search mode. + await UrlbarTestUtils.assertSearchMode(window, null); + BrowserTestUtils.removeTab(newTab); + await UrlbarTestUtils.promisePopupClose(window); +}); + +// Accel+Clicking a local one-off with a search string should open search mode +// in a new background tab with the search string already populated. +add_task(async function accel_click_search_local() { + await searchAndOpenPopup(SEARCH_STRING); + let oneOffs = oneOffSearchButtons.getSelectableButtons(true); + let oneOff; + for (oneOff of oneOffs) { + if (oneOff.id == "urlbar-engine-one-off-item-bookmarks") { + break; + } + } + let tabCount = gBrowser.tabs.length; + let tabOpenPromise = TestUtils.waitForCondition( + () => + gBrowser.tabs.length == tabCount + 1 + ? gBrowser.tabs[gBrowser.tabs.length - 1] + : false, + "Waiting for background about:newtab to open." + ); + EventUtils.synthesizeMouseAtCenter(oneOff, { accelKey: true }); + let newTab = await tabOpenPromise; + Assert.notEqual( + newTab.linkedBrowser, + gBrowser.selectedBrowser, + "The foreground tab hasn't changed." + ); + await UrlbarTestUtils.assertSearchMode(window, null); + BrowserTestUtils.switchTab(gBrowser, newTab); + // Check the new background tab is already in search mode. + await UrlbarTestUtils.assertSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + entry: "oneoff", + }); + // Check the search string is already populated. + Assert.equal( + gURLBar.value, + SEARCH_STRING, + "The search term was duplicated to the new tab." + ); + BrowserTestUtils.removeTab(newTab); + await UrlbarTestUtils.promisePopupClose(window); +}); diff --git a/browser/components/urlbar/tests/browser/browser_oneOffs_searchSuggestions.js b/browser/components/urlbar/tests/browser/browser_oneOffs_searchSuggestions.js new file mode 100644 index 0000000000..183e6275df --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_oneOffs_searchSuggestions.js @@ -0,0 +1,354 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests various actions relating to search suggestions and the one-off buttons. + */ + +const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml"; +const TEST_ENGINE2_BASENAME = "searchSuggestionEngine2.xml"; + +const serverInfo = { + scheme: "http", + host: "localhost", + port: 20709, // Must be identical to what is in searchSuggestionEngine2.xml +}; + +var gEngine; +var gEngine2; + +add_task(async function init() { + await PlacesUtils.history.clear(); + await UrlbarTestUtils.formHistory.clear(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.searches", true], + ["browser.urlbar.maxHistoricalSearchSuggestions", 2], + ], + }); + gEngine = await SearchTestUtils.promiseNewSearchEngine( + getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME + ); + gEngine2 = await SearchTestUtils.promiseNewSearchEngine( + getRootDirectory(gTestPath) + TEST_ENGINE2_BASENAME + ); + let oldDefaultEngine = await Services.search.getDefault(); + await Services.search.moveEngine(gEngine2, 0); + await Services.search.moveEngine(gEngine, 0); + await Services.search.setDefault(gEngine); + registerCleanupFunction(async function() { + await Services.search.setDefault(oldDefaultEngine); + + await PlacesUtils.history.clear(); + await UrlbarTestUtils.formHistory.clear(); + }); +}); + +async function withSuggestions(testFn) { + // First run with remote suggestions, and then run with form history. + await withSuggestionOnce(false, testFn); + await withSuggestionOnce(true, testFn); +} + +async function withSuggestionOnce(useFormHistory, testFn) { + if (useFormHistory) { + // Add foofoo twice so it's more frecent so it appears first so that the + // order of form history results matches the order of remote suggestion + // results. + await UrlbarTestUtils.formHistory.add(["foofoo", "foofoo", "foobar"]); + } + await BrowserTestUtils.withNewTab(gBrowser, async () => { + let value = "foo"; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value, + fireInputEvent: true, + }); + let index = await UrlbarTestUtils.promiseSuggestionsPresent(window); + await assertState({ + inputValue: value, + resultIndex: 0, + }); + await withHttpServer(serverInfo, () => { + return testFn(index, useFormHistory); + }); + }); + await PlacesUtils.history.clear(); + await UrlbarTestUtils.formHistory.clear(); +} + +async function selectSecondSuggestion(index, isFormHistory) { + // Down to select the first search suggestion. + for (let i = index; i > 0; --i) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + } + await assertState({ + inputValue: "foofoo", + resultIndex: index, + suggestion: { + isFormHistory, + }, + }); + + // Down to select the next search suggestion. + EventUtils.synthesizeKey("KEY_ArrowDown"); + await assertState({ + inputValue: "foobar", + resultIndex: index + 1, + suggestion: { + isFormHistory, + }, + }); +} + +// Presses the Return key when a one-off is selected after selecting a search +// suggestion. +add_task(async function test_returnAfterSuggestion() { + await withSuggestions(async (index, usingFormHistory) => { + await selectSecondSuggestion(index, usingFormHistory); + + // Alt+Down to select the first one-off. + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + await assertState({ + inputValue: "foobar", + resultIndex: index + 1, + oneOffIndex: 0, + suggestion: { + isFormHistory: usingFormHistory, + }, + }); + + let heuristicResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok( + !BrowserTestUtils.is_visible(heuristicResult.element.action), + "The heuristic action should not be visible" + ); + + let resultsPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey("KEY_Enter"); + await resultsPromise; + await UrlbarTestUtils.assertSearchMode(window, { + engineName: gEngine.name, + entry: "oneoff", + }); + await UrlbarTestUtils.exitSearchMode(window, { backspace: true }); + }); +}); + +// Presses the Return key when a non-default one-off is selected after selecting +// a search suggestion. +add_task(async function test_returnAfterSuggestion_nonDefault() { + await withSuggestions(async (index, usingFormHistory) => { + await selectSecondSuggestion(index, usingFormHistory); + + // Alt+Down twice to select the second one-off. + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + await assertState({ + inputValue: "foobar", + resultIndex: index + 1, + oneOffIndex: 1, + suggestion: { + isFormHistory: usingFormHistory, + }, + }); + + let resultsPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey("KEY_Enter"); + await resultsPromise; + await UrlbarTestUtils.assertSearchMode(window, { + engineName: gEngine2.name, + entry: "oneoff", + }); + await UrlbarTestUtils.exitSearchMode(window, { backspace: true }); + }); +}); + +// Clicks a one-off engine after selecting a search suggestion. +add_task(async function test_clickAfterSuggestion() { + await withSuggestions(async (index, usingFormHistory) => { + await selectSecondSuggestion(index, usingFormHistory); + + let oneOffs = UrlbarTestUtils.getOneOffSearchButtons( + window + ).getSelectableButtons(true); + let resultsPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeMouseAtCenter(oneOffs[1], {}); + await resultsPromise; + await UrlbarTestUtils.assertSearchMode(window, { + engineName: gEngine2.name, + entry: "oneoff", + }); + await UrlbarTestUtils.exitSearchMode(window, { backspace: true }); + }); +}); + +// Clicks a non-default one-off engine after selecting a search suggestion. +add_task(async function test_clickAfterSuggestion_nonDefault() { + await withSuggestions(async (index, usingFormHistory) => { + await selectSecondSuggestion(index, usingFormHistory); + + let oneOffs = UrlbarTestUtils.getOneOffSearchButtons( + window + ).getSelectableButtons(true); + let resultsPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeMouseAtCenter(oneOffs[1], {}); + await resultsPromise; + await UrlbarTestUtils.assertSearchMode(window, { + engineName: gEngine2.name, + entry: "oneoff", + }); + await UrlbarTestUtils.exitSearchMode(window, { backspace: true }); + }); +}); + +// Selects a non-default one-off engine and then clicks a search suggestion. +add_task(async function test_selectOneOffThenSuggestion() { + await withSuggestions(async (index, usingFormHistory) => { + // Select a non-default one-off engine. + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + await assertState({ + inputValue: "foo", + resultIndex: 0, + oneOffIndex: 1, + }); + + let heuristicResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok( + BrowserTestUtils.is_visible(heuristicResult.element.action), + "The heuristic action should be visible because the result is selected" + ); + + // Now click the second suggestion. + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, index + 1); + // Note search history results don't change their engine when the selected + // one-off button changes! + let resultsPromise = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + usingFormHistory + ? `http://mochi.test:8888/?terms=foobar` + : `http://localhost:20709/?terms=foobar` + ); + EventUtils.synthesizeMouseAtCenter(result.element.row, {}); + await resultsPromise; + }); +}); + +add_task(async function overridden_engine_not_reused() { + info( + "An overridden search suggestion item should not be reused by a search with another engine" + ); + await BrowserTestUtils.withNewTab(gBrowser, async () => { + let typedValue = "foo"; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: typedValue, + fireInputEvent: true, + }); + let index = await UrlbarTestUtils.promiseSuggestionsPresent(window); + // Down to select the first search suggestion. + for (let i = index; i > 0; --i) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + } + await assertState({ + inputValue: "foofoo", + resultIndex: index, + suggestion: { + isFormHistory: false, + }, + }); + + // ALT+Down to select the second search engine. + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + await assertState({ + inputValue: "foofoo", + resultIndex: index, + oneOffIndex: 1, + suggestion: { + isFormHistory: false, + }, + }); + + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + let label = result.displayed.action; + // Run again the query, check the label has been replaced. + await UrlbarTestUtils.promisePopupClose(window); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: typedValue, + fireInputEvent: true, + }); + index = await UrlbarTestUtils.promiseSuggestionsPresent(window); + await assertState({ + inputValue: "foo", + resultIndex: 0, + }); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, index); + Assert.notEqual( + result.displayed.action, + label, + "The label should have been updated" + ); + }); +}); + +async function assertState({ + resultIndex, + inputValue, + oneOffIndex = -1, + suggestion = null, +}) { + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + resultIndex, + "Expected result should be selected" + ); + Assert.equal( + UrlbarTestUtils.getOneOffSearchButtons(window).selectedButtonIndex, + oneOffIndex, + "Expected one-off should be selected" + ); + if (inputValue !== undefined) { + Assert.equal(gURLBar.value, inputValue, "Expected input value"); + } + + if (suggestion) { + let result = await UrlbarTestUtils.getDetailsOfResultAt( + window, + resultIndex + ); + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.SEARCH, + "Result type should be SEARCH" + ); + if (suggestion.isFormHistory) { + Assert.equal( + result.source, + UrlbarUtils.RESULT_SOURCE.HISTORY, + "Result source should be HISTORY" + ); + } else { + Assert.equal( + result.source, + UrlbarUtils.RESULT_SOURCE.SEARCH, + "Result source should be SEARCH" + ); + } + Assert.equal( + typeof result.searchParams.suggestion, + "string", + "Result should have a suggestion" + ); + Assert.equal( + result.searchParams.suggestion, + suggestion.value || inputValue, + "Result should have the expected suggestion" + ); + } +} diff --git a/browser/components/urlbar/tests/browser/browser_oneOffs_settings.js b/browser/components/urlbar/tests/browser/browser_oneOffs_settings.js new file mode 100644 index 0000000000..e34284a4f7 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_oneOffs_settings.js @@ -0,0 +1,88 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This tests that the settings button in the one-off buttons display correctly + * loads the search preferences. + */ + +let gMaxResults; + +add_task(async function init() { + gMaxResults = Services.prefs.getIntPref("browser.urlbar.maxRichResults"); + + registerCleanupFunction(async function() { + await PlacesUtils.history.clear(); + await UrlbarTestUtils.formHistory.clear(); + }); + + await PlacesUtils.history.clear(); + await UrlbarTestUtils.formHistory.clear(); + + let visits = []; + for (let i = 0; i < gMaxResults; i++) { + visits.push({ + uri: makeURI("http://example.com/browser_urlbarOneOffs.js/?" + i), + // TYPED so that the visit shows up when the urlbar's drop-down arrow is + // pressed. + transition: Ci.nsINavHistoryService.TRANSITION_TYPED, + }); + } + await PlacesTestUtils.addVisits(visits); +}); + +async function selectSettings(activateFn) { + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:blank" }, + async browser => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "example.com", + }); + await UrlbarTestUtils.waitForAutocompleteResultAt( + window, + gMaxResults - 1 + ); + + await UrlbarTestUtils.promisePopupClose(window, async () => { + let prefPaneLoaded = TestUtils.topicObserved( + "sync-pane-loaded", + () => true + ); + + activateFn(); + + await prefPaneLoaded; + }); + + Assert.equal( + gBrowser.contentWindow.history.state, + "paneSearch", + "Should have opened the search preferences pane" + ); + } + ); +} + +add_task(async function test_open_settings_with_enter() { + await selectSettings(() => { + EventUtils.synthesizeKey("KEY_ArrowUp"); + + Assert.ok( + UrlbarTestUtils.getOneOffSearchButtons( + window + ).selectedButton.classList.contains("search-setting-button-compact"), + "Should have selected the settings button" + ); + + EventUtils.synthesizeKey("KEY_Enter"); + }); +}); + +add_task(async function test_open_settings_with_click() { + await selectSettings(() => { + UrlbarTestUtils.getOneOffSearchButtons(window).settingsButton.click(); + }); +}); diff --git a/browser/components/urlbar/tests/browser/browser_pasteAndGo.js b/browser/components/urlbar/tests/browser/browser_pasteAndGo.js new file mode 100644 index 0000000000..980f84139e --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_pasteAndGo.js @@ -0,0 +1,92 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests for the paste and go functionality of the urlbar. + */ + +add_task(async function() { + const kURLs = [ + "http://example.com/1", + "http://example.org/2\n", + "http://\nexample.com/3\n", + ]; + for (let url of kURLs) { + await BrowserTestUtils.withNewTab("about:blank", async function(browser) { + gURLBar.focus(); + + await SimpleTest.promiseClipboardChange(url, () => { + clipboardHelper.copyString(url); + }); + let menuitem = await promiseContextualMenuitem("paste-and-go"); + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + browser, + false, + url.replace(/\n/g, "") + ); + EventUtils.synthesizeMouseAtCenter(menuitem, {}); + // Using toSource in order to get the newlines escaped: + info("Paste and go, loading " + url.toSource()); + await browserLoadedPromise; + ok(true, "Successfully loaded " + url); + }); + } +}); + +add_task(async function test_invisible_char() { + const url = "http://example.com/4\u2028"; + await BrowserTestUtils.withNewTab("about:blank", async function(browser) { + gURLBar.focus(); + await SimpleTest.promiseClipboardChange(url, () => { + clipboardHelper.copyString(url); + }); + let menuitem = await promiseContextualMenuitem("paste-and-go"); + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + browser, + false, + url.replace(/\u2028/g, "") + ); + EventUtils.synthesizeMouseAtCenter(menuitem, {}); + // Using toSource in order to get the newlines escaped: + info("Paste and go, loading " + url.toSource()); + await browserLoadedPromise; + ok(true, "Successfully loaded " + url); + }); +}); + +add_task(async function test_with_input_and_results() { + // Test paste and go When there's some input and the results pane is open. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "foo", + }); + const url = "http://example.com/"; + await SimpleTest.promiseClipboardChange(url, () => { + clipboardHelper.copyString(url); + }); + let menuitem = await promiseContextualMenuitem("paste-and-go"); + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + url + ); + EventUtils.synthesizeMouseAtCenter(menuitem, {}); + // Using toSource in order to get the newlines escaped: + info("Paste and go, loading " + url.toSource()); + await browserLoadedPromise; + ok(true, "Successfully loaded " + url); +}); + +async function promiseContextualMenuitem(anonid) { + let textBox = gURLBar.querySelector("moz-input-box"); + let cxmenu = textBox.menupopup; + let cxmenuPromise = BrowserTestUtils.waitForEvent(cxmenu, "popupshown"); + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, { + type: "contextmenu", + button: 2, + }); + await cxmenuPromise; + return textBox.getMenuItem(anonid); +} diff --git a/browser/components/urlbar/tests/browser/browser_percent_encoded.js b/browser/components/urlbar/tests/browser/browser_percent_encoded.js new file mode 100644 index 0000000000..10b581b342 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_percent_encoded.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that searching history works for both encoded or decoded strings. + +add_task(async function test() { + const decoded = "日本"; + const TEST_URL = TEST_BASE_URL + "?" + decoded; + registerCleanupFunction(async () => { + await PlacesUtils.history.clear(); + }); + + // Visit url in a new tab, going through normal urlbar workflow. + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + let promise = PlacesTestUtils.waitForNotification( + "page-visited", + visits => { + Assert.equal( + visits.length, + 1, + "Was notified for the right number of visits." + ); + let { url, transitionType } = visits[0]; + return ( + url == encodeURI(TEST_URL) && + transitionType == PlacesUtils.history.TRANSITIONS.TYPED + ); + }, + "places" + ); + gURLBar.focus(); + gURLBar.value = TEST_URL; + info("Visiting url"); + EventUtils.synthesizeKey("KEY_Enter"); + await promise; + gBrowser.removeCurrentTab({ skipPermitUnload: true }); + + info("Search for the decoded string."); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: decoded, + }); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 2, + "Check number of results" + ); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal(result.url, encodeURI(TEST_URL), "Check result url"); + + info("Search for the encoded string."); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: encodeURIComponent(decoded), + }); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 2, + "Check number of results" + ); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal(result.url, encodeURI(TEST_URL), "Check result url"); +}); diff --git a/browser/components/urlbar/tests/browser/browser_placeholder.js b/browser/components/urlbar/tests/browser/browser_placeholder.js new file mode 100644 index 0000000000..240392527f --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_placeholder.js @@ -0,0 +1,295 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This test ensures the placeholder is set correctly for different search + * engines. + */ + +"use strict"; + +const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml"; +const TEST_PRIVATE_ENGINE_BASENAME = "searchSuggestionEngine2.xml"; + +var originalEngine, extraEngine, extraPrivateEngine, expectedString; +var tabs = []; + +var noEngineString; + +add_task(async function setup() { + originalEngine = await Services.search.getDefault(); + [noEngineString, expectedString] = ( + await document.l10n.formatMessages([ + { id: "urlbar-placeholder" }, + { + id: "urlbar-placeholder-with-name", + args: { name: originalEngine.name }, + }, + ]) + ).map(msg => msg.attributes[0].value); + + let rootDir = getRootDirectory(gTestPath); + extraEngine = await SearchTestUtils.promiseNewSearchEngine( + rootDir + TEST_ENGINE_BASENAME + ); + extraPrivateEngine = await SearchTestUtils.promiseNewSearchEngine( + rootDir + TEST_PRIVATE_ENGINE_BASENAME + ); + + // Force display of a tab with a URL bar, to clear out any possible placeholder + // initialization listeners that happen on startup. + let urlTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:mozilla" + ); + BrowserTestUtils.removeTab(urlTab); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.search.separatePrivateDefault.ui.enabled", true], + ["browser.search.separatePrivateDefault", false], + ], + }); + + registerCleanupFunction(async () => { + await Services.search.setDefault(originalEngine); + for (let tab of tabs) { + BrowserTestUtils.removeTab(tab); + } + }); +}); + +add_task(async function test_change_default_engine_updates_placeholder() { + tabs.push(await BrowserTestUtils.openNewForegroundTab(gBrowser)); + + await Services.search.setDefault(extraEngine); + + await TestUtils.waitForCondition( + () => gURLBar.placeholder == noEngineString, + "The placeholder should match the default placeholder for non-built-in engines." + ); + Assert.equal(gURLBar.placeholder, noEngineString); + + await Services.search.setDefault(originalEngine); + + await TestUtils.waitForCondition( + () => gURLBar.placeholder == expectedString, + "The placeholder should include the engine name for built-in engines." + ); + Assert.equal(gURLBar.placeholder, expectedString); +}); + +add_task(async function test_delayed_update_placeholder() { + // We remove the change of engine listener here as that is set so that + // if the engine is changed by the user then the placeholder is always updated + // straight away. As we want to test the delay update here, we remove the + // listener and call the placeholder update manually with the delay flag. + Services.obs.removeObserver(BrowserSearch, "browser-search-engine-modified"); + + // Since we can't easily test for startup changes, we'll at least test the delay + // of update for the placeholder works. + let urlTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:mozilla" + ); + tabs.push(urlTab); + + // Open a tab with a blank URL bar. + let blankTab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + tabs.push(blankTab); + + await Services.search.setDefault(extraEngine); + // Pretend we've "initialized". + BrowserSearch._updateURLBarPlaceholder(extraEngine.name, false, true); + + Assert.equal( + gURLBar.placeholder, + expectedString, + "Placeholder should be unchanged." + ); + + // Now switch to a tab with something in the URL Bar. + await BrowserTestUtils.switchTab(gBrowser, urlTab); + + await TestUtils.waitForCondition( + () => gURLBar.placeholder == noEngineString, + "The placeholder should have updated in the background." + ); + + // Do it the other way to check both named engine and fallback code paths. + await BrowserTestUtils.switchTab(gBrowser, blankTab); + + await Services.search.setDefault(originalEngine); + BrowserSearch._updateURLBarPlaceholder(originalEngine.name, false, true); + + Assert.equal( + gURLBar.placeholder, + noEngineString, + "Placeholder should be unchanged." + ); + Assert.deepEqual( + document.l10n.getAttributes(gURLBar.inputField), + { id: "urlbar-placeholder", args: null }, + "Placeholder data should be unchanged." + ); + + await BrowserTestUtils.switchTab(gBrowser, urlTab); + + await TestUtils.waitForCondition( + () => gURLBar.placeholder == expectedString, + "The placeholder should include the engine name for built-in engines." + ); + + // Now check when we have a URL displayed, the placeholder is updated straight away. + BrowserSearch._updateURLBarPlaceholder(extraEngine.name, false); + + await TestUtils.waitForCondition( + () => gURLBar.placeholder == noEngineString, + "The placeholder should go back to the default" + ); + Assert.equal( + gURLBar.placeholder, + noEngineString, + "Placeholder should be the default." + ); + + Services.obs.addObserver(BrowserSearch, "browser-search-engine-modified"); +}); + +add_task(async function test_private_window_no_separate_engine() { + const win = await BrowserTestUtils.openNewBrowserWindow({ private: true }); + + await Services.search.setDefault(extraEngine); + + await TestUtils.waitForCondition( + () => win.gURLBar.placeholder == noEngineString, + "The placeholder should match the default placeholder for non-built-in engines." + ); + Assert.equal(win.gURLBar.placeholder, noEngineString); + + await Services.search.setDefault(originalEngine); + + await TestUtils.waitForCondition( + () => win.gURLBar.placeholder == expectedString, + "The placeholder should include the engine name for built-in engines." + ); + Assert.equal(win.gURLBar.placeholder, expectedString); + + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function test_private_window_separate_engine() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.search.separatePrivateDefault", true]], + }); + const originalPrivateEngine = await Services.search.getDefaultPrivate(); + registerCleanupFunction(async () => { + await Services.search.setDefaultPrivate(originalPrivateEngine); + }); + + const win = await BrowserTestUtils.openNewBrowserWindow({ private: true }); + + // Keep the normal default as a different string to the private, so that we + // can be sure we're testing the right thing. + await Services.search.setDefault(originalEngine); + await Services.search.setDefaultPrivate(extraPrivateEngine); + + await TestUtils.waitForCondition( + () => win.gURLBar.placeholder == noEngineString, + "The placeholder should match the default placeholder for non-built-in engines." + ); + Assert.equal(win.gURLBar.placeholder, noEngineString); + + await Services.search.setDefault(extraEngine); + await Services.search.setDefaultPrivate(originalEngine); + + await TestUtils.waitForCondition( + () => win.gURLBar.placeholder == expectedString, + "The placeholder should include the engine name for built-in engines." + ); + Assert.equal(win.gURLBar.placeholder, expectedString); + + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function test_search_mode_engine_web() { + // Add our test engine to WEB_ENGINE_NAMES so that it's recognized as a web + // engine. + UrlbarUtils.WEB_ENGINE_NAMES.add(extraEngine.name); + + await doSearchModeTest( + { + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + engineName: extraEngine.name, + }, + { + id: "urlbar-placeholder-search-mode-web-2", + args: { name: extraEngine.name }, + } + ); + + UrlbarUtils.WEB_ENGINE_NAMES.delete(extraEngine.name); +}); + +add_task(async function test_search_mode_engine_other() { + await doSearchModeTest( + { engineName: extraEngine.name }, + { + id: "urlbar-placeholder-search-mode-other-engine", + args: { name: extraEngine.name }, + } + ); +}); + +add_task(async function test_search_mode_bookmarks() { + await doSearchModeTest( + { source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS }, + { id: "urlbar-placeholder-search-mode-other-bookmarks", args: null } + ); +}); + +add_task(async function test_search_mode_tabs() { + await doSearchModeTest( + { source: UrlbarUtils.RESULT_SOURCE.TABS }, + { id: "urlbar-placeholder-search-mode-other-tabs", args: null } + ); +}); + +add_task(async function test_search_mode_history() { + await doSearchModeTest( + { source: UrlbarUtils.RESULT_SOURCE.HISTORY }, + { id: "urlbar-placeholder-search-mode-other-history", args: null } + ); +}); + +/** + * Opens the view, clicks a one-off button to enter search mode, and asserts + * that the placeholder is corrrect. + * + * @param {object} expectedSearchMode + * The expected search mode object for the one-off. + * @param {object} expectedPlaceholderL10n + * The expected l10n object for the one-off. + */ +async function doSearchModeTest(expectedSearchMode, expectedPlaceholderL10n) { + // Click the urlbar to open the top-sites view. + if (gURLBar.getAttribute("pageproxystate") == "invalid") { + gURLBar.handleRevert(); + } + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + + // Enter search mode. + await UrlbarTestUtils.enterSearchMode(window, expectedSearchMode); + + // Check the placeholder. + Assert.deepEqual( + document.l10n.getAttributes(gURLBar.inputField), + expectedPlaceholderL10n, + "Placeholder has expected l10n" + ); + + await UrlbarTestUtils.exitSearchMode(window, { clickClose: true }); + await UrlbarTestUtils.promisePopupClose(window); +} diff --git a/browser/components/urlbar/tests/browser/browser_populateAfterPushState.js b/browser/components/urlbar/tests/browser/browser_populateAfterPushState.js new file mode 100644 index 0000000000..f799f56c3c --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_populateAfterPushState.js @@ -0,0 +1,32 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* When a user clears the URL bar, and then the page pushes state, we should + * re-fill the URL bar so it doesn't remain empty indefinitely. See bug 1441039. + * For normal loads, this happens automatically because a non-same-document state + * change takes place. + */ +add_task(async function() { + await BrowserTestUtils.withNewTab( + TEST_BASE_URL + "dummy_page.html", + async function(browser) { + gURLBar.value = ""; + + let locationChangePromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + TEST_BASE_URL + "dummy_page2.html" + ); + await SpecialPowers.spawn(browser, [], function() { + content.history.pushState({}, "Page 2", "dummy_page2.html"); + }); + await locationChangePromise; + is( + gURLBar.value, + TEST_BASE_URL + "dummy_page2.html", + "Should have updated the URL bar." + ); + } + ); +}); diff --git a/browser/components/urlbar/tests/browser/browser_primary_selection_safe_on_new_tab.js b/browser/components/urlbar/tests/browser/browser_primary_selection_safe_on_new_tab.js new file mode 100644 index 0000000000..6a2e8c9e00 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_primary_selection_safe_on_new_tab.js @@ -0,0 +1,71 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Verify that the primary selection is unaffected by opening a new tab. + * + * The steps here follow STR for regression + * https://bugzilla.mozilla.org/show_bug.cgi?id=1457355. + */ + +"use strict"; + +let tabs = []; +let supportsPrimary = Services.clipboard.supportsSelectionClipboard(); +const NON_EMPTY_URL = "data:text/html,Hello"; +const TEXT_FOR_PRIMARY = "Text for PRIMARY selection"; + +add_task(async function() { + tabs.push( + await BrowserTestUtils.openNewForegroundTab(gBrowser, NON_EMPTY_URL) + ); + + // Bug 1457355 reproduced only when the url had a non-empty selection. + gURLBar.select(); + Assert.equal(gURLBar.inputField.selectionStart, 0); + Assert.equal( + gURLBar.inputField.selectionEnd, + gURLBar.inputField.value.length + ); + + if (supportsPrimary) { + clipboardHelper.copyStringToClipboard( + TEXT_FOR_PRIMARY, + Services.clipboard.kSelectionClipboard + ); + } + + tabs.push( + await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: () => { + // Simulate tab open from user input such as keyboard shortcut or new + // tab button. + let userInput = window.windowUtils.setHandlingUserInput(true); + try { + BrowserOpenTab(); + } finally { + userInput.destruct(); + } + }, + waitForLoad: false, + }) + ); + + if (!supportsPrimary) { + info("Primary selection not supported. Skipping assertion."); + return; + } + + let primaryAsText = SpecialPowers.getClipboardData( + "text/unicode", + SpecialPowers.Ci.nsIClipboard.kSelectionClipboard + ); + Assert.equal(primaryAsText, TEXT_FOR_PRIMARY); +}); + +registerCleanupFunction(() => { + for (let tab of tabs) { + BrowserTestUtils.removeTab(tab); + } +}); diff --git a/browser/components/urlbar/tests/browser/browser_privateBrowsingWindowChange.js b/browser/components/urlbar/tests/browser/browser_privateBrowsingWindowChange.js new file mode 100644 index 0000000000..50b97a77d2 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_privateBrowsingWindowChange.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test that when opening a private browsing window and typing in it before + * about:privatebrowsing loads, we don't clear the URL bar. + */ +add_task(async function() { + let urlbarTestValue = "Mary had a little lamb"; + let win = OpenBrowserWindow({ private: true }); + registerCleanupFunction(() => BrowserTestUtils.closeWindow(win)); + await BrowserTestUtils.waitForEvent(win, "load"); + let promise = new Promise(resolve => { + let wpl = { + onLocationChange(aWebProgress, aRequest, aLocation) { + if (aLocation && aLocation.spec == "about:privatebrowsing") { + win.gBrowser.removeProgressListener(wpl); + resolve(); + } + }, + }; + win.gBrowser.addProgressListener(wpl); + }); + Assert.notEqual( + win.gBrowser.selectedBrowser.currentURI.spec, + "about:privatebrowsing", + "Check privatebrowsing page has not been loaded yet" + ); + info("Search in urlbar"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: urlbarTestValue, + fireInputEvent: true, + }); + info("waiting for about:privatebrowsing load"); + await promise; + + let urlbar = win.gURLBar; + is( + urlbar.value, + urlbarTestValue, + "URL bar value should be the same once about:privatebrowsing has loaded" + ); + is( + win.gBrowser.selectedBrowser.userTypedValue, + urlbarTestValue, + "User typed value should be the same once about:privatebrowsing has loaded" + ); +}); diff --git a/browser/components/urlbar/tests/browser/browser_raceWithTabs.js b/browser/components/urlbar/tests/browser/browser_raceWithTabs.js new file mode 100644 index 0000000000..852ffd11fc --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_raceWithTabs.js @@ -0,0 +1,86 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +const TEST_URL = `${TEST_BASE_URL}dummy_page.html`; + +async function addBookmark(bookmark) { + info("Creating bookmark and keyword"); + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: bookmark.url, + title: bookmark.title, + }); + if (bookmark.keyword) { + await PlacesUtils.keywords.insert({ + keyword: bookmark.keyword, + url: bookmark.url, + }); + } + + registerCleanupFunction(async function() { + if (bookmark.keyword) { + await PlacesUtils.keywords.remove(bookmark.keyword); + } + await PlacesUtils.bookmarks.remove(bm); + }); +} + +/** + * Check that if the user hits enter and ctrl-t at the same time, we open the + * URL in the right tab. + */ +add_task(async function hitEnterLoadInRightTab() { + await addBookmark({ + title: "Test for keyword bookmark and URL", + url: TEST_URL, + keyword: "urlbarkeyword", + }); + + info("Opening a tab"); + let oldTabOpenPromise = BrowserTestUtils.waitForEvent( + gBrowser.tabContainer, + "TabOpen" + ); + BrowserOpenTab(); + let oldTab = (await oldTabOpenPromise).target; + let oldTabLoadedPromise = BrowserTestUtils.browserLoaded( + oldTab.linkedBrowser, + false, + TEST_URL + ).then(() => info("Old tab loaded")); + + info("Filling URL bar, sending <return> and opening a tab"); + let tabOpenPromise = BrowserTestUtils.waitForEvent( + gBrowser.tabContainer, + "TabOpen" + ); + gURLBar.value = "urlbarkeyword"; + gURLBar.focus(); + gURLBar.select(); + EventUtils.sendKey("return"); + + info("Immediately open a second tab"); + BrowserOpenTab(); + let newTab = (await tabOpenPromise).target; + + info("Created new tab; waiting for tabs to load"); + let newTabLoadedPromise = BrowserTestUtils.browserLoaded( + newTab.linkedBrowser, + false, + "about:newtab" + ).then(() => info("New tab loaded")); + // If one of the tabs loads the wrong page, this will timeout, and that + // indicates we regressed this bug fix. + await Promise.all([newTabLoadedPromise, oldTabLoadedPromise]); + // These are not particularly useful, but the test must contain some checks. + is( + newTab.linkedBrowser.currentURI.spec, + "about:newtab", + "New tab loaded about:newtab" + ); + is(oldTab.linkedBrowser.currentURI.spec, TEST_URL, "Old tab loaded URL"); + + info("Closing tabs"); + BrowserTestUtils.removeTab(newTab); + BrowserTestUtils.removeTab(oldTab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_redirect_error.js b/browser/components/urlbar/tests/browser/browser_redirect_error.js new file mode 100644 index 0000000000..c7d72425b9 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_redirect_error.js @@ -0,0 +1,134 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const REDIRECT_FROM = `${TEST_BASE_URL}redirect_error.sjs`; + +const REDIRECT_TO = "https://www.bank1.com/"; // Bad-cert host. + +function isRedirectedURISpec(aURISpec) { + return isRedirectedURI(Services.io.newURI(aURISpec)); +} + +function isRedirectedURI(aURI) { + // Compare only their before-hash portion. + return Services.io.newURI(REDIRECT_TO).equalsExceptRef(aURI); +} + +/* + Test. + +1. Load redirect_bug623155.sjs#BG in a background tab. + +2. The redirected URI is <https://www.bank1.com/#BG>, which displayes a cert + error page. + +3. Switch the tab to foreground. + +4. Check the URLbar's value, expecting <https://www.bank1.com/#BG> + +5. Load redirect_bug623155.sjs#FG in the foreground tab. + +6. The redirected URI is <https://www.bank1.com/#FG>. And this is also + a cert-error page. + +7. Check the URLbar's value, expecting <https://www.bank1.com/#FG> + +8. End. + + */ + +var gNewTab; + +function test() { + waitForExplicitFinish(); + + // Load a URI in the background. + gNewTab = BrowserTestUtils.addTab(gBrowser, REDIRECT_FROM + "#BG"); + gBrowser + .getBrowserForTab(gNewTab) + .webProgress.addProgressListener( + gWebProgressListener, + Ci.nsIWebProgress.NOTIFY_LOCATION + ); +} + +var gWebProgressListener = { + QueryInterface: ChromeUtils.generateQI([ + "nsIWebProgressListener", + "nsISupportsWeakReference", + ]), + + // --------------------------------------------------------------------------- + // NOTIFY_LOCATION mode should work fine without these methods. + // + // onStateChange: function() {}, + // onStatusChange: function() {}, + // onProgressChange: function() {}, + // onSecurityChange: function() {}, + // ---------------------------------------------------------------------------- + + onLocationChange(aWebProgress, aRequest, aLocation, aFlags) { + if (!aRequest) { + // This is bug 673752, or maybe initial "about:blank". + return; + } + + ok(gNewTab, "There is a new tab."); + ok( + isRedirectedURI(aLocation), + "onLocationChange catches only redirected URI." + ); + + if (aLocation.ref == "BG") { + // This is background tab's request. + isnot(gNewTab, gBrowser.selectedTab, "This is a background tab."); + } else if (aLocation.ref == "FG") { + // This is foreground tab's request. + is(gNewTab, gBrowser.selectedTab, "This is a foreground tab."); + } else { + // We shonuld not reach here. + ok(false, "This URI hash is not expected:" + aLocation.ref); + } + + let isSelectedTab = gNewTab.selected; + setTimeout(delayed, 0, isSelectedTab); + }, +}; + +function delayed(aIsSelectedTab) { + // Switch tab and confirm URL bar. + if (!aIsSelectedTab) { + gBrowser.selectedTab = gNewTab; + } + + let currentURI = gBrowser.selectedBrowser.currentURI.spec; + ok( + isRedirectedURISpec(currentURI), + "The content area is redirected. aIsSelectedTab:" + aIsSelectedTab + ); + is( + gURLBar.value, + currentURI, + "The URL bar shows the content URI. aIsSelectedTab:" + aIsSelectedTab + ); + + if (!aIsSelectedTab) { + // If this was a background request, go on a foreground request. + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, REDIRECT_FROM + "#FG"); + } else { + // Othrewise, nothing to do remains. + finish(); + } +} + +/* Cleanup */ +registerCleanupFunction(function() { + if (gNewTab) { + gBrowser + .getBrowserForTab(gNewTab) + .webProgress.removeProgressListener(gWebProgressListener); + + gBrowser.removeTab(gNewTab); + } + gNewTab = null; +}); diff --git a/browser/components/urlbar/tests/browser/browser_remoteness_switch.js b/browser/components/urlbar/tests/browser/browser_remoteness_switch.js new file mode 100644 index 0000000000..c81e9e232a --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_remoteness_switch.js @@ -0,0 +1,51 @@ +"use strict"; + +/** + * Verify that when loading and going back/forward through history between URLs + * loaded in the content process, and URLs loaded in the parent process, we + * don't set the URL for the tab to about:blank inbetween the loads. + */ +add_task(async function() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.navigation.requireUserInteraction", false]], + }); + let url = "http://www.example.com/foo.html"; + await BrowserTestUtils.withNewTab({ gBrowser, url }, async function(browser) { + let wpl = { + onLocationChange(unused, unused2, location) { + if (location.schemeIs("about")) { + is( + location.spec, + "about:config", + "Only about: location change should be for about:preferences" + ); + } else { + is( + location.spec, + url, + "Only non-about: location change should be for the http URL we're dealing with." + ); + } + }, + }; + gBrowser.addProgressListener(wpl); + + let didLoad = BrowserTestUtils.browserLoaded(browser, null, function( + loadedURL + ) { + return loadedURL == "about:config"; + }); + BrowserTestUtils.loadURI(browser, "about:config"); + await didLoad; + + gBrowser.goBack(); + await BrowserTestUtils.browserLoaded(browser, null, function(loadedURL) { + return url == loadedURL; + }); + gBrowser.goForward(); + await BrowserTestUtils.browserLoaded(browser, null, function(loadedURL) { + return loadedURL == "about:config"; + }); + gBrowser.removeProgressListener(wpl); + }); +}); diff --git a/browser/components/urlbar/tests/browser/browser_remotetab.js b/browser/components/urlbar/tests/browser/browser_remotetab.js new file mode 100644 index 0000000000..51bb79eac2 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_remotetab.js @@ -0,0 +1,111 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This tests checks that the remote tab result is displayed and can be + * selected. + */ + +"use strict"; + +const { SyncedTabs } = ChromeUtils.import( + "resource://services-sync/SyncedTabs.jsm" +); + +const TEST_URL = `${TEST_BASE_URL}dummy_page.html`; + +const REMOTE_TAB = { + id: "7cqCr77ptzX3", + type: "client", + lastModified: 1492201200, + name: "zcarter's Nightly on MacBook-Pro-25", + clientType: "desktop", + tabs: [ + { + type: "tab", + title: "Test Remote", + url: TEST_URL, + icon: UrlbarUtils.ICON.DEFAULT, + client: "7cqCr77ptzX3", + lastUsed: 1452124677, + }, + ], +}; + +add_task(async function setup() { + sandbox = sinon.createSandbox(); + + let originalSyncedTabsInternal = SyncedTabs._internal; + SyncedTabs._internal = { + isConfiguredToSyncTabs: true, + hasSyncedThisSession: true, + getTabClients() { + return Promise.resolve([]); + }, + syncTabs() { + return Promise.resolve(); + }, + }; + + // Tell the Sync XPCOM service it is initialized. + let weaveXPCService = Cc["@mozilla.org/weave/service;1"].getService( + Ci.nsISupports + ).wrappedJSObject; + let oldWeaveServiceReady = weaveXPCService.ready; + weaveXPCService.ready = true; + + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.autoFill", false], + ["services.sync.username", "fake"], + ["services.sync.syncedTabs.showRemoteTabs", true], + ], + }); + + sandbox + .stub(SyncedTabs._internal, "getTabClients") + .callsFake(() => Promise.resolve(Cu.cloneInto([REMOTE_TAB], {}))); + + registerCleanupFunction(async () => { + sandbox.restore(); + weaveXPCService.ready = oldWeaveServiceReady; + SyncedTabs._internal = originalSyncedTabsInternal; + }); +}); + +add_task(async function test_remotetab_opens() { + await BrowserTestUtils.withNewTab( + { url: "about:robots", gBrowser }, + async function() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "Test Remote", + }); + + // There should be two items in the pop-up, the first is the default search + // suggestion, the second is the remote tab. + + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.REMOTE_TAB, + "Should be the remote tab entry" + ); + + // The URL is going to open in the current tab as it is currently about:blank + let promiseTabLoaded = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser + ); + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_Enter"); + await promiseTabLoaded; + + Assert.equal( + gBrowser.selectedTab.linkedBrowser.currentURI.spec, + TEST_URL, + "correct URL loaded" + ); + } + ); +}); diff --git a/browser/components/urlbar/tests/browser/browser_removeUnsafeProtocolsFromURLBarPaste.js b/browser/components/urlbar/tests/browser/browser_removeUnsafeProtocolsFromURLBarPaste.js new file mode 100644 index 0000000000..3793b69bd0 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_removeUnsafeProtocolsFromURLBarPaste.js @@ -0,0 +1,95 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Ensures that pasting unsafe protocols in the urlbar have the protocol + * correctly stripped. + */ + +var pairs = [ + ["javascript:", ""], + ["javascript:1+1", "1+1"], + ["javascript:document.domain", "document.domain"], + [ + " \u0001\u0002\u0003\u0004\u0005\u0006\u0007\u0008\u0009javascript:document.domain", + "document.domain", + ], + ["java\nscript:foo", "foo"], + ["java\tscript:foo", "foo"], + ["http://\nexample.com", "http://example.com"], + ["http://\nexample.com\n", "http://example.com"], + ["data:text/html,<body>hi</body>", "data:text/html,<body>hi</body>"], + ["javaScript:foopy", "foopy"], + ["javaScript:javaScript:alert('hi')", "alert('hi')"], + // Nested things get confusing because some things don't parse as URIs: + ["javascript:javascript:alert('hi!')", "alert('hi!')"], + [ + "data:data:text/html,<body>hi</body>", + "data:data:text/html,<body>hi</body>", + ], + ["javascript:data:javascript:alert('hi!')", "data:javascript:alert('hi!')"], + [ + "javascript:data:text/html,javascript:alert('hi!')", + "data:text/html,javascript:alert('hi!')", + ], + [ + "data:data:text/html,javascript:alert('hi!')", + "data:data:text/html,javascript:alert('hi!')", + ], +]; + +let supportsNullBytes = AppConstants.platform == "macosx"; +// Note that \u000d (\r) is missing here; we test it separately because it +// makes the test sad on Windows. +let gobbledygook = + "\u000a\u000b\u000c\u000e\u000f\u0010\u0011\u0012\u0013\u0014javascript:foo"; +if (supportsNullBytes) { + gobbledygook = "\u0000" + gobbledygook; +} +pairs.push([gobbledygook, "foo"]); + +let supportsReturnWithoutNewline = + AppConstants.platform != "win" && AppConstants.platform != "linux"; +if (supportsReturnWithoutNewline) { + pairs.push(["java\rscript:foo", "foo"]); +} + +async function paste(input) { + try { + await SimpleTest.promiseClipboardChange( + aData => { + // This test checks how "\r" is treated. Therefore, we cannot specify + // string here and instead, we need to compare strictly with this + // function. + return aData === input; + }, + () => { + clipboardHelper.copyString(input); + } + ); + } catch (ex) { + Assert.ok(false, "Failed to copy string '" + input + "' to clipboard"); + } + + document.commandDispatcher + .getControllerForCommand("cmd_paste") + .doCommand("cmd_paste"); +} + +add_task(async function test_stripUnsafeProtocolPaste() { + for (let [inputValue, expectedURL] of pairs) { + gURLBar.value = ""; + gURLBar.focus(); + await paste(inputValue); + + Assert.equal( + gURLBar.value, + expectedURL, + `entering ${inputValue} strips relevant bits.` + ); + + await new Promise(resolve => setTimeout(resolve, 0)); + } +}); diff --git a/browser/components/urlbar/tests/browser/browser_remove_match.js b/browser/components/urlbar/tests/browser/browser_remove_match.js new file mode 100644 index 0000000000..1b227c9ea8 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_remove_match.js @@ -0,0 +1,227 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +XPCOMUtils.defineLazyModuleGetters(this, { + FormHistory: "resource://gre/modules/FormHistory.jsm", +}); + +add_task(async function test_remove_history() { + const TEST_URL = "http://remove.me/from_urlbar/"; + await PlacesTestUtils.addVisits(TEST_URL); + + registerCleanupFunction(async function() { + await PlacesUtils.history.clear(); + }); + + let promiseVisitRemoved = PlacesTestUtils.waitForNotification( + "onDeleteURI", + uri => uri.spec == TEST_URL, + "history" + ); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "from_urlbar", + }); + + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal(result.url, TEST_URL, "Found the expected result"); + + let expectedResultCount = UrlbarTestUtils.getResultCount(window) - 1; + + EventUtils.synthesizeKey("KEY_ArrowDown"); + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), 1); + EventUtils.synthesizeKey("KEY_Delete", { shiftKey: true }); + await promiseVisitRemoved; + await TestUtils.waitForCondition( + () => UrlbarTestUtils.getResultCount(window) == expectedResultCount, + "Waiting for the result to disappear" + ); + + for (let i = 0; i < expectedResultCount; i++) { + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + Assert.notEqual( + details.url, + TEST_URL, + "Should not find the test URL in the remaining results" + ); + } + + await UrlbarTestUtils.promisePopupClose(window); +}); + +add_task(async function test_remove_form_history() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.searches", true], + ["browser.urlbar.maxHistoricalSearchSuggestions", 1], + ], + }); + + await Services.search.addEngineWithDetails("test", { + method: "GET", + template: "http://example.com/?q={searchTerms}", + }); + let engine = Services.search.getEngineByName("test"); + let originalEngine = await Services.search.getDefault(); + await Services.search.setDefault(engine); + + let formHistoryValue = "foobar"; + await UrlbarTestUtils.formHistory.add([formHistoryValue]); + + let formHistory = ( + await UrlbarTestUtils.formHistory.search({ + value: formHistoryValue, + }) + ).map(entry => entry.value); + Assert.deepEqual( + formHistory, + [formHistoryValue], + "Should find form history after adding it" + ); + + let promiseRemoved = UrlbarTestUtils.formHistory.promiseChanged("remove"); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "foo", + }); + + let index = 1; + let count = UrlbarTestUtils.getResultCount(window); + for (; index < count; index++) { + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, index); + if ( + result.type == UrlbarUtils.RESULT_TYPE.SEARCH && + result.source == UrlbarUtils.RESULT_SOURCE.HISTORY + ) { + break; + } + } + Assert.ok(index < count, "Result found"); + + EventUtils.synthesizeKey("KEY_ArrowDown", { repeat: index }); + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), index); + EventUtils.synthesizeKey("KEY_Delete", { shiftKey: true }); + await promiseRemoved; + + await TestUtils.waitForCondition( + () => UrlbarTestUtils.getResultCount(window) == count - 1, + "Waiting for the result to disappear" + ); + + for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) { + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + Assert.ok( + result.type != UrlbarUtils.RESULT_TYPE.SEARCH || + result.source != UrlbarUtils.RESULT_SOURCE.HISTORY, + "Should not find the form history result in the remaining results" + ); + } + + await UrlbarTestUtils.promisePopupClose(window); + + formHistory = ( + await UrlbarTestUtils.formHistory.search({ + value: formHistoryValue, + }) + ).map(entry => entry.value); + Assert.deepEqual( + formHistory, + [], + "Should not find form history after removing it" + ); + + await SpecialPowers.popPrefEnv(); + await Services.search.setDefault(originalEngine); + await Services.search.removeEngine(engine); +}); + +// We shouldn't be able to remove a bookmark item. +add_task(async function test_remove_bookmark_doesnt() { + const TEST_URL = "http://dont.remove.me/from_urlbar/"; + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "test", + url: TEST_URL, + }); + + registerCleanupFunction(async function() { + await PlacesUtils.bookmarks.eraseEverything(); + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "from_urlbar", + }); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal(result.url, TEST_URL, "Found the expected result"); + + EventUtils.synthesizeKey("KEY_ArrowDown"); + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), 1); + EventUtils.synthesizeKey("KEY_Delete", { shiftKey: true }); + + // We don't have an easy way of determining if the event was process or not, + // so let any event queues clear before testing. + await new Promise(resolve => setTimeout(resolve, 0)); + await PlacesTestUtils.promiseAsyncUpdates(); + + Assert.ok( + await PlacesUtils.bookmarks.fetch({ url: TEST_URL }), + "Should still have the URL bookmarked." + ); +}); + +add_task(async function test_searchMode_removeRestyledHistory() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.searches", true], + ["browser.urlbar.maxHistoricalSearchSuggestions", 1], + ], + }); + + let engine = await Services.search.addEngineWithDetails("test", { + method: "GET", + template: "http://example.com/", + searchGetParams: "q={searchTerms}", + }); + let originalEngine = await Services.search.getDefault(); + await Services.search.setDefault(engine); + await Services.search.moveEngine(engine, 0); + + let query = "ciao"; + let url = `http://example.com/?q=${query}bar`; + await PlacesTestUtils.addVisits(url); + + await BrowserTestUtils.withNewTab("about:robots", async function(browser) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: query, + }); + await UrlbarTestUtils.enterSearchMode(window); + + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.SEARCH); + Assert.equal(result.source, UrlbarUtils.RESULT_SOURCE.HISTORY); + + EventUtils.synthesizeKey("KEY_ArrowDown"); + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), 1); + EventUtils.synthesizeKey("KEY_Delete", { shiftKey: true }); + await TestUtils.waitForCondition( + async () => !(await PlacesTestUtils.isPageInDB(url)), + "Wait for url to be removed from history" + ); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 1, + "Urlbar result should be removed" + ); + + await UrlbarTestUtils.exitSearchMode(window, { clickClose: true }); + await UrlbarTestUtils.promisePopupClose(window); + }); + await PlacesUtils.history.clear(); + await SpecialPowers.popPrefEnv(); + await Services.search.setDefault(originalEngine); + await Services.search.removeEngine(engine); +}); diff --git a/browser/components/urlbar/tests/browser/browser_restoreEmptyInput.js b/browser/components/urlbar/tests/browser/browser_restoreEmptyInput.js new file mode 100644 index 0000000000..50ac4044e6 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_restoreEmptyInput.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// When the input is empty and the view is opened, keying down through the +// results and then out of the results should restore the empty input. + +"use strict"; + +add_task(async function test() { + for (let i = 0; i < 5; i++) { + await PlacesTestUtils.addVisits("http://example.com/"); + } + // Update Top Sites to make sure the last Top Site is a URL. Otherwise, it + // would be a search shortcut and thus would not fill the Urlbar when + // selected. + await updateTopSites(sites => { + return ( + sites && + sites[sites.length - 1] && + sites[sites.length - 1].url == "http://example.com/" + ); + }); + + registerCleanupFunction(async function() { + await PlacesUtils.history.clear(); + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + fireInputEvent: true, + }); + + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + -1, + "Nothing selected" + ); + + let resultCount = UrlbarTestUtils.getResultCount(window); + Assert.greater(resultCount, 0, "At least one result"); + + for (let i = 0; i < resultCount; i++) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + } + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + resultCount - 1, + "Last result selected" + ); + Assert.notEqual(gURLBar.value, "", "Input should not be empty"); + + EventUtils.synthesizeKey("KEY_ArrowDown"); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + -1, + "Nothing selected" + ); + Assert.equal(gURLBar.value, "", "Input should be empty"); +}); diff --git a/browser/components/urlbar/tests/browser/browser_resultSpan.js b/browser/components/urlbar/tests/browser/browser_resultSpan.js new file mode 100644 index 0000000000..29f8a50d44 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_resultSpan.js @@ -0,0 +1,264 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that displaying results with resultSpan > 1 limits other results in +// the view. + +const TEST_RESULTS = [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://mozilla.org/1" } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TIP, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + text: "This is a test tip.", + buttonText: "Done", + type: "test", + helpUrl: "about:about", + } + ), +]; + +const MAX_RESULTS = UrlbarPrefs.get("maxRichResults"); +const TIP_SPAN = UrlbarUtils.getSpanForResult({ + type: UrlbarUtils.RESULT_TYPE.TIP, +}); + +add_task(async function init() { + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); +}); + +// A restricting provider with one tip result and many history results. +add_task(async function oneTip() { + let results = Array.from(TEST_RESULTS); + for (let i = TEST_RESULTS.length; i < MAX_RESULTS; i++) { + results.push( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: `http://mozilla.org/${i}` } + ) + ); + } + + let expectedResults = Array.from(results).slice( + 0, + MAX_RESULTS - TIP_SPAN + 1 + ); + + let provider = new UrlbarTestUtils.TestProvider({ results, priority: 1 }); + UrlbarProvidersManager.registerProvider(provider); + + let context = await UrlbarTestUtils.promiseAutocompleteResultPopup({ + value: "test", + window, + }); + + checkResults(context.results, expectedResults); + + UrlbarProvidersManager.unregisterProvider(provider); + gURLBar.view.close(); +}); + +// A restricting provider with three tip results and many history results. +add_task(async function threeTips() { + let results = Array.from(TEST_RESULTS); + for (let i = 1; i < 3; i++) { + results.push( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TIP, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + text: "This is a test tip.", + buttonText: "Done", + type: "test", + helpUrl: `about:about#${i}`, + } + ) + ); + } + for (let i = 2; i < 15; i++) { + results.push( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: `http://mozilla.org/${i}` } + ) + ); + } + + let expectedResults = Array.from(results).slice( + 0, + MAX_RESULTS - 3 * (TIP_SPAN - 1) + ); + + let provider = new UrlbarTestUtils.TestProvider({ results, priority: 1 }); + UrlbarProvidersManager.registerProvider(provider); + + let context = await UrlbarTestUtils.promiseAutocompleteResultPopup({ + value: "test", + window, + }); + + checkResults(context.results, expectedResults); + + UrlbarProvidersManager.unregisterProvider(provider); + gURLBar.view.close(); +}); + +// A non-restricting provider with one tip result and many history results. +add_task(async function oneTip_nonRestricting() { + let results = Array.from(TEST_RESULTS); + for (let i = 2; i < 15; i++) { + results.push( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: `http://mozilla.org/${i}` } + ) + ); + } + + let expectedResults = Array.from(results); + + // UnifiedComplete's heuristic search result + expectedResults.unshift({ + type: UrlbarUtils.RESULT_TYPE.SEARCH, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + payload: { + engine: Services.search.defaultEngine.name, + query: "test", + }, + }); + + expectedResults = expectedResults.slice(0, MAX_RESULTS - TIP_SPAN + 1); + + let provider = new UrlbarTestUtils.TestProvider({ results }); + UrlbarProvidersManager.registerProvider(provider); + + let context = await UrlbarTestUtils.promiseAutocompleteResultPopup({ + value: "test", + window, + }); + + checkResults(context.results, expectedResults); + + UrlbarProvidersManager.unregisterProvider(provider); + gURLBar.view.close(); +}); + +// A non-restricting provider with three tip results and many history results. +add_task(async function threeTips_nonRestricting() { + let results = Array.from(TEST_RESULTS); + for (let i = 1; i < 3; i++) { + results.push( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TIP, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + text: "This is a test tip.", + buttonText: "Done", + type: "test", + helpUrl: `about:about#${i}`, + } + ) + ); + } + for (let i = 2; i < 15; i++) { + results.push( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: `http://mozilla.org/${i}` } + ) + ); + } + + let expectedResults = Array.from(results); + + // UnifiedComplete's heuristic search result + expectedResults.unshift({ + type: UrlbarUtils.RESULT_TYPE.SEARCH, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + payload: { + engine: Services.search.defaultEngine.name, + query: "test", + }, + }); + + expectedResults = expectedResults.slice(0, MAX_RESULTS - 3 * (TIP_SPAN - 1)); + + let provider = new UrlbarTestUtils.TestProvider({ results }); + UrlbarProvidersManager.registerProvider(provider); + + let context = await UrlbarTestUtils.promiseAutocompleteResultPopup({ + value: "test", + window, + }); + + checkResults(context.results, expectedResults); + + UrlbarProvidersManager.unregisterProvider(provider); + gURLBar.view.close(); +}); + +add_task(async function customValue() { + let results = []; + for (let i = 0; i < 15; i++) { + results.push( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: `http://mozilla.org/${i}` } + ) + ); + } + + results[1].resultSpan = 5; + + let expectedResults = Array.from(results); + expectedResults = expectedResults.slice(0, 6); + + let provider = new UrlbarTestUtils.TestProvider({ results }); + UrlbarProvidersManager.registerProvider(provider); + + let context = await UrlbarTestUtils.promiseAutocompleteResultPopup({ + value: "test", + window, + }); + + checkResults(context.results, expectedResults); + + UrlbarProvidersManager.unregisterProvider(provider); + gURLBar.view.close(); +}); + +function checkResults(actual, expected) { + Assert.equal(actual.length, expected.length, "Number of results"); + for (let i = 0; i < expected.length; i++) { + info(`Checking results at index ${i}`); + let actualResult = collectExpectedProperties(actual[i], expected[i]); + Assert.deepEqual(actualResult, expected[i], "Actual vs. expected result"); + } +} + +function collectExpectedProperties(actualObj, expectedObj) { + let newActualObj = {}; + for (let name in expectedObj) { + if (typeof expectedObj[name] == "object") { + newActualObj[name] = collectExpectedProperties( + actualObj[name], + expectedObj[name] + ); + } else { + newActualObj[name] = expectedObj[name]; + } + } + return newActualObj; +} diff --git a/browser/components/urlbar/tests/browser/browser_result_onSelection.js b/browser/components/urlbar/tests/browser/browser_result_onSelection.js new file mode 100644 index 0000000000..329091ccf8 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_result_onSelection.js @@ -0,0 +1,55 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test() { + let results = [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://mozilla.org/1" } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://mozilla.org/2" } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TIP, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + text: "This is a test tip.", + buttonText: "Done", + type: "test", + helpUrl: "about:about", + } + ), + ]; + + results[0].heuristic = true; + + let selectionCount = 0; + let provider = new UrlbarTestUtils.TestProvider({ + results, + priority: 1, + onSelection: (result, element) => { + selectionCount++; + }, + }); + UrlbarProvidersManager.registerProvider(provider); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + }); + + let oneOffs = UrlbarTestUtils.getOneOffSearchButtons(window); + + while (!oneOffs.selectedButton) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + } + + Assert.equal(selectionCount, 4, "We selected the four elements in the view."); + UrlbarProvidersManager.unregisterProvider(provider); +}); diff --git a/browser/components/urlbar/tests/browser/browser_retainedResultsOnFocus.js b/browser/components/urlbar/tests/browser/browser_retainedResultsOnFocus.js new file mode 100644 index 0000000000..911f398614 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_retainedResultsOnFocus.js @@ -0,0 +1,435 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests retained results. +// When there is a pending search (user typed a search string and blurred +// without picking a result), on focus we should the search results again. + +async function checkPanelStatePersists(win, isOpen) { + // Check for popup events, we should not see any of them because the urlbar + // popup state should not change. This also ensures we don't cause flickering + // open/close actions. + function handler(event) { + Assert.ok(false, `Received unexpected event ${event.type}`); + } + win.gURLBar.addEventListener("popupshowing", handler); + win.gURLBar.addEventListener("popuphiding", handler); + // Because the panel opening may not be immediate, we must wait a bit. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 300)); + win.gURLBar.removeEventListener("popupshowing", handler); + win.gURLBar.removeEventListener("popuphiding", handler); + Assert.equal( + isOpen, + win.gURLBar.view.isOpen, + `check urlbar remains ${isOpen ? "open" : "closed"}` + ); +} + +async function checkOpensOnFocus(win, state) { + Assert.ok(!win.gURLBar.view.isOpen, "check urlbar panel is not open"); + win.gURLBar.blur(); + + info("Check the keyboard shortcut."); + await UrlbarTestUtils.promisePopupOpen(win, () => { + win.document.getElementById("Browser:OpenLocation").doCommand(); + }); + + await UrlbarTestUtils.promiseSearchComplete(win); + Assert.equal(state.selectionStart, win.gURLBar.selectionStart); + Assert.equal(state.selectionEnd, win.gURLBar.selectionEnd); + + await UrlbarTestUtils.promisePopupClose(win, () => { + win.gURLBar.blur(); + }); + info("Focus with the mouse."); + await UrlbarTestUtils.promisePopupOpen(win, () => { + EventUtils.synthesizeMouseAtCenter(win.gURLBar.inputField, {}, win); + }); + + await UrlbarTestUtils.promiseSearchComplete(win); + Assert.equal(state.selectionStart, win.gURLBar.selectionStart); + Assert.equal(state.selectionEnd, win.gURLBar.selectionEnd); +} + +async function checkDoesNotOpenOnFocus(win) { + Assert.ok(!win.gURLBar.view.isOpen, "check urlbar panel is not open"); + win.gURLBar.blur(); + + info("Check the keyboard shortcut."); + let promiseState = checkPanelStatePersists(win, false); + win.document.getElementById("Browser:OpenLocation").doCommand(); + await promiseState; + win.gURLBar.blur(); + info("Focus with the mouse."); + promiseState = checkPanelStatePersists(win, false); + EventUtils.synthesizeMouseAtCenter(win.gURLBar.inputField, {}, win); + await promiseState; +} + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.autoFill", true]], + }); + // Add some history for the empty panel and autofill. + await PlacesTestUtils.addVisits([ + { + uri: "http://example.com/", + transition: PlacesUtils.history.TRANSITIONS.TYPED, + }, + { + uri: "http://example.com/foo/", + transition: PlacesUtils.history.TRANSITIONS.TYPED, + }, + ]); + registerCleanupFunction(async function() { + await PlacesUtils.history.clear(); + }); +}); + +async function test_window(win) { + for (let url of ["about:newtab", "about:home", "http://example.com/"]) { + // withNewTab may hang on preloaded pages, thus instead of waiting for load + // we just wait for the expected currentURI value. + await BrowserTestUtils.withNewTab( + { gBrowser: win.gBrowser, url, waitForLoad: false }, + async browser => { + await TestUtils.waitForCondition( + () => win.gBrowser.currentURI.spec == url, + "Ensure we're on the expected page" + ); + + // In one case use a value that triggers autofill. + let autofill = url == "http://example.com/"; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: autofill ? "ex" : "foo", + fireInputEvent: true, + }); + let { value, selectionStart, selectionEnd } = win.gURLBar; + if (!autofill) { + selectionStart = 0; + } + info("expected " + value + " " + selectionStart + " " + selectionEnd); + await UrlbarTestUtils.promisePopupClose(win, () => { + win.gURLBar.blur(); + }); + + info("The panel should open when there's a search string"); + await checkOpensOnFocus(win, { value, selectionStart, selectionEnd }); + await UrlbarTestUtils.promisePopupClose(win, () => { + win.gURLBar.blur(); + }); + } + ); + } +} + +add_task(async function test_normalWindow() { + let win = await BrowserTestUtils.openNewBrowserWindow(); + await test_window(win); + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function test_privateWindow() { + let privateWin = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + await test_window(privateWin); + await BrowserTestUtils.closeWindow(privateWin); +}); + +add_task(async function test_tabSwitch() { + info("Check that switching tabs reopens the view."); + let win = await BrowserTestUtils.openNewBrowserWindow(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "ex", + fireInputEvent: true, + }); + let { value, selectionStart, selectionEnd } = win.gURLBar; + Assert.equal(value, "example.com/", "Check autofill value"); + Assert.ok( + selectionStart > 0 && selectionEnd > selectionStart, + "Check autofill selection" + ); + + Assert.ok(win.gURLBar.focused, "The urlbar should be focused"); + let tab1 = win.gBrowser.selectedTab; + + async function check_autofill() { + // The urlbar code waits for both TabSelect and the focus change, thus + // we can't just wait for search completion here, we have to poll for a + // value. + await TestUtils.waitForCondition( + () => win.gURLBar.value == "example.com/", + "wait for autofill value" + ); + // Ensure stable results. + await UrlbarTestUtils.promiseSearchComplete(win); + Assert.equal(selectionStart, win.gURLBar.selectionStart); + Assert.equal(selectionEnd, win.gURLBar.selectionEnd); + } + + info("Open a new tab with the same search"); + let tab2 = await BrowserTestUtils.openNewForegroundTab(win.gBrowser); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "ex", + fireInputEvent: true, + }); + + info("Switch across tabs"); + for (let tab of win.gBrowser.tabs) { + await UrlbarTestUtils.promisePopupOpen(win, async () => { + await BrowserTestUtils.switchTab(win.gBrowser, tab); + }); + await check_autofill(); + } + + info("Close tab and check the view is open."); + await UrlbarTestUtils.promisePopupOpen(win, () => { + BrowserTestUtils.removeTab(tab2); + }); + await check_autofill(); + + info("Open a new tab with a different search"); + tab2 = await BrowserTestUtils.openNewForegroundTab(win.gBrowser); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "xam", + fireInputEvent: true, + }); + + info("Switch to the first tab and check the panel remains open"); + let promiseState = checkPanelStatePersists(win, true); + await BrowserTestUtils.switchTab(win.gBrowser, tab1); + await promiseState; + await UrlbarTestUtils.promiseSearchComplete(win); + await check_autofill(); + + info("Switch to the second tab and check the panel remains open"); + promiseState = checkPanelStatePersists(win, true); + await BrowserTestUtils.switchTab(win.gBrowser, tab2); + await promiseState; + await UrlbarTestUtils.promiseSearchComplete(win); + Assert.equal(win.gURLBar.value, "xam", "check value"); + Assert.equal(win.gURLBar.selectionStart, 3); + Assert.equal(win.gURLBar.selectionEnd, 3); + + info("autofill in tab2, switch to tab1, then back to tab2 with the mouse"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "e", + fireInputEvent: true, + }); + // Adjust selection start, we are using a different search string. + await BrowserTestUtils.switchTab(win.gBrowser, tab1); + await UrlbarTestUtils.promiseSearchComplete(win); + await check_autofill(); + tab2.click(); + selectionStart = 1; + await check_autofill(); + + info("Check we don't rerun a search if the shortcut is used on an open view"); + EventUtils.synthesizeKey("KEY_Backspace", {}, win); + await UrlbarTestUtils.promiseSearchComplete(win); + Assert.ok(win.gURLBar.view.isOpen, "The view should be open"); + Assert.equal(win.gURLBar.value, "e", "The value should be the typed one"); + win.document.getElementById("Browser:OpenLocation").doCommand(); + // A search should not run here, so there's nothing to wait for. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 300)); + Assert.ok(win.gURLBar.view.isOpen, "The view should be open"); + Assert.equal(win.gURLBar.value, "e", "The value should not change"); + + info( + "Tab switch from an empty search tab with unfocused urlbar to a tab with a search string and a focused urlbar" + ); + win.gURLBar.value = ""; + win.gURLBar.blur(); + await UrlbarTestUtils.promisePopupOpen(win, async () => { + await BrowserTestUtils.switchTab(win.gBrowser, tab1); + }); + + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function test_tabSwitch_pageproxystate() { + info("Switching tabs on valid pageproxystate doesn't reopen."); + + info("Adding some visits for the empty panel"); + await PlacesTestUtils.addVisits([ + "http://example.com/", + "http://mochi.test:8888/", + ]); + registerCleanupFunction(PlacesUtils.history.clear); + + let win = await BrowserTestUtils.openNewBrowserWindow(); + BrowserTestUtils.loadURI(win.gBrowser.selectedBrowser, "about:robots"); + let tab1 = win.gBrowser.selectedTab; + + info("Open a new tab and the empty search"); + let tab2 = await BrowserTestUtils.openNewForegroundTab(win.gBrowser); + await UrlbarTestUtils.promisePopupOpen(win, async () => { + win.gURLBar.focus(); + // On Linux and Mac down moves caret to the end of the text unless it's + // there already. + win.gURLBar.selectionStart = win.gURLBar.selectionEnd = + win.gURLBar.value.length; + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + }); + await UrlbarTestUtils.promiseSearchComplete(win); + let result = await UrlbarTestUtils.getDetailsOfResultAt(win, 0); + Assert.notEqual(result.url, "about:robots"); + + info("Switch to the first tab and start searching with DOWN"); + await UrlbarTestUtils.promisePopupClose(win, async () => { + await BrowserTestUtils.switchTab(win.gBrowser, tab1); + }); + await checkPanelStatePersists(win, false); + await UrlbarTestUtils.promisePopupOpen(win, async () => { + win.gURLBar.focus(); + // On Linux and Mac down moves caret to the end of the text unless it's + // there already. + win.gURLBar.selectionStart = win.gURLBar.selectionEnd = + win.gURLBar.value.length; + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + }); + await UrlbarTestUtils.promiseSearchComplete(win); + + info("Switcihng to the second tab should not reopen the search"); + await UrlbarTestUtils.promisePopupClose(win, async () => { + await BrowserTestUtils.switchTab(win.gBrowser, tab2); + }); + await checkPanelStatePersists(win, false); + + info("Switching to the first tab should not reopen the search"); + let promiseState = await checkPanelStatePersists(win, false); + await BrowserTestUtils.switchTab(win.gBrowser, tab1); + await promiseState; + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function test_tabSwitch_emptySearch() { + info("Switching between empty-search tabs should not reopen the view."); + let win = await BrowserTestUtils.openNewBrowserWindow(); + + info("Open the empty search"); + let tab1 = win.gBrowser.selectedTab; + await UrlbarTestUtils.promisePopupOpen(win, async () => { + win.gURLBar.focus(); + // On Linux and Mac down moves caret to the end of the text unless it's + // there already. + win.gURLBar.selectionStart = win.gURLBar.selectionEnd = + win.gURLBar.value.length; + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + }); + await UrlbarTestUtils.promiseSearchComplete(win); + + info("Open a new tab and the empty search"); + let tab2 = await BrowserTestUtils.openNewForegroundTab(win.gBrowser); + await UrlbarTestUtils.promisePopupOpen(win, async () => { + win.gURLBar.focus(); + // On Linux and Mac down moves caret to the end of the text unless it's + // there already. + win.gURLBar.selectionStart = win.gURLBar.selectionEnd = + win.gURLBar.value.length; + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + }); + await UrlbarTestUtils.promiseSearchComplete(win); + + info("Switching to the first tab should not reopen the view"); + await UrlbarTestUtils.promisePopupClose(win, async () => { + await BrowserTestUtils.switchTab(win.gBrowser, tab1); + }); + await checkPanelStatePersists(win, false); + + info("Switching to the second tab should not reopen the view"); + let promiseState = await checkPanelStatePersists(win, false); + await BrowserTestUtils.switchTab(win.gBrowser, tab2); + await promiseState; + + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function test_pageproxystate_valid() { + info("Focusing on valid pageproxystate should not reopen the view."); + let win = await BrowserTestUtils.openNewBrowserWindow(); + + info("Search for a full url and confirm it with Enter"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "about:robots", + fireInputEvent: true, + }); + let loadedPromise = BrowserTestUtils.browserLoaded( + win.gBrowser.selectedBrowser + ); + EventUtils.synthesizeKey("KEY_Enter", {}, win); + await loadedPromise; + + Assert.ok(!win.gURLBar.focused, "The urlbar should not be focused"); + info("Focus the urlbar"); + win.document.getElementById("Browser:OpenLocation").doCommand(); + + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function test_allowAutofill() { + info("Check we respect allowAutofill from the last search"); + let win = await BrowserTestUtils.openNewBrowserWindow(); + + await UrlbarTestUtils.promisePopupOpen(win, async () => { + await selectAndPaste("e", win); + }); + Assert.equal(win.gURLBar.value, "e", "Should not autofill"); + let context = await win.gURLBar.lastQueryContextPromise; + Assert.equal(context.allowAutofill, false, "Check initial allowAutofill"); + await UrlbarTestUtils.promisePopupClose(win); + + await UrlbarTestUtils.promisePopupOpen(win, async () => { + win.document.getElementById("Browser:OpenLocation").doCommand(); + }); + await UrlbarTestUtils.promiseSearchComplete(win); + Assert.equal(win.gURLBar.value, "e", "Should not autofill"); + context = await win.gURLBar.lastQueryContextPromise; + Assert.equal(context.allowAutofill, false, "Check reopened allowAutofill"); + + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function test_clicks_after_autofill() { + info( + "Check that clickin on an autofilled input field doesn't requery, causing loss of the caret position" + ); + let win = await BrowserTestUtils.openNewBrowserWindow(); + info("autofill in tab2, switch to tab1, then back to tab2 with the mouse"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "e", + fireInputEvent: true, + }); + Assert.equal(win.gURLBar.value, "example.com/", "Should have autofilled"); + + // Check single click. + let input = win.gURLBar.inputField; + EventUtils.synthesizeMouse(input, 30, 10, {}, win); + // Wait a bit, in case of a mistake this would run a query, otherwise not. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 300)); + Assert.ok(win.gURLBar.selectionStart < win.gURLBar.value.length); + Assert.equal(win.gURLBar.selectionStart, win.gURLBar.selectionEnd); + + // Check double click. + EventUtils.synthesizeMouse(input, 30, 10, { clickCount: 2 }, win); + // Wait a bit, in case of a mistake this would run a query, otherwise not. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 300)); + Assert.ok(win.gURLBar.selectionStart < win.gURLBar.value.length); + Assert.ok(win.gURLBar.selectionEnd > win.gURLBar.selectionStart); + + await BrowserTestUtils.closeWindow(win); +}); diff --git a/browser/components/urlbar/tests/browser/browser_revert.js b/browser/components/urlbar/tests/browser/browser_revert.js new file mode 100644 index 0000000000..1b47189145 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_revert.js @@ -0,0 +1,33 @@ +// Test reverting the urlbar value with ESC after a tab switch. + +add_task(async function() { + registerCleanupFunction(PlacesUtils.history.clear); + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: "http://example.com", + }, + async function(browser) { + let originalValue = gURLBar.value; + let tab = gBrowser.selectedTab; + info("Put a typed value."); + gBrowser.userTypedValue = "foobar"; + info("Switch tabs."); + gBrowser.selectedTab = gBrowser.tabs[0]; + gBrowser.selectedTab = tab; + Assert.equal( + gURLBar.value, + "foobar", + "location bar displays typed value" + ); + + gURLBar.focus(); + EventUtils.synthesizeKey("KEY_Escape"); + Assert.equal( + gURLBar.value, + originalValue, + "ESC reverted the location bar value" + ); + } + ); +}); diff --git a/browser/components/urlbar/tests/browser/browser_searchFunction.js b/browser/components/urlbar/tests/browser/browser_searchFunction.js new file mode 100644 index 0000000000..f0ccd59f3e --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchFunction.js @@ -0,0 +1,256 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This test checks the urlbar.search() function. + +"use strict"; + +const ALIAS = "@enginealias"; +let aliasEngine; + +add_task(async function init() { + // Run this in a new tab, to ensure all the locationchange notifications have + // fired. + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + aliasEngine = await Services.search.addEngineWithDetails("Test", { + alias: ALIAS, + template: "http://example.com/?search={searchTerms}", + }); + registerCleanupFunction(async function() { + await Services.search.removeEngine(aliasEngine); + BrowserTestUtils.removeTab(tab); + gURLBar.handleRevert(); + }); +}); + +// Calls search() with a normal, non-"@engine" search-string argument. +add_task(async function basic() { + gURLBar.blur(); + gURLBar.search("basic"); + ok(gURLBar.hasAttribute("focused"), "url bar is focused"); + await assertUrlbarValue("basic"); + + assertOneOffButtonsVisible(true); + + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Escape") + ); +}); + +// Calls search() with an invalid "@engine" search engine alias so that the +// one-off search buttons are disabled. +add_task(async function searchEngineAlias() { + gURLBar.blur(); + await UrlbarTestUtils.promisePopupOpen(window, () => + gURLBar.search("@example") + ); + ok(gURLBar.hasAttribute("focused"), "url bar is focused"); + UrlbarTestUtils.assertSearchMode(window, null); + await assertUrlbarValue("@example"); + + assertOneOffButtonsVisible(false); + + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Escape") + ); + + // Open the popup again (by doing another search) to make sure the one-off + // buttons are shown -- i.e., that we didn't accidentally break them. + await UrlbarTestUtils.promisePopupOpen(window, () => + gURLBar.search("not an engine alias") + ); + await assertUrlbarValue("not an engine alias"); + assertOneOffButtonsVisible(true); + + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Escape") + ); +}); + +add_task(async function searchRestriction() { + gURLBar.blur(); + await UrlbarTestUtils.promisePopupOpen(window, () => + gURLBar.search(UrlbarTokenizer.RESTRICT.SEARCH) + ); + ok(gURLBar.hasAttribute("focused"), "url bar is focused"); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: UrlbarSearchUtils.getDefaultEngine().name, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + // Entry is "other" because we didn't pass searchModeEntry to search(). + entry: "other", + }); + assertOneOffButtonsVisible(true); + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); +}); + +add_task(async function historyRestriction() { + gURLBar.blur(); + await UrlbarTestUtils.promisePopupOpen(window, () => + gURLBar.search(UrlbarTokenizer.RESTRICT.HISTORY) + ); + ok(gURLBar.hasAttribute("focused"), "url bar is focused"); + await UrlbarTestUtils.assertSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + entry: "other", + }); + assertOneOffButtonsVisible(true); + Assert.ok(!gURLBar.value, "The Urlbar has no value."); + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); +}); + +add_task(async function historyRestrictionWithString() { + gURLBar.blur(); + // The leading and trailing spaces are intentional to verify that search() + // preserves them. + let searchString = " foo bar "; + await UrlbarTestUtils.promisePopupOpen(window, () => + gURLBar.search(`${UrlbarTokenizer.RESTRICT.HISTORY} ${searchString}`) + ); + ok(gURLBar.hasAttribute("focused"), "url bar is focused"); + await UrlbarTestUtils.assertSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + entry: "other", + }); + // We don't use assertUrlbarValue here since we expect to open a local search + // mode. In those modes, we don't show a heuristic search result, which + // assertUrlbarValue checks for. + await UrlbarTestUtils.promiseSearchComplete(window); + Assert.equal( + gURLBar.value, + searchString, + "The Urlbar value should be the search string." + ); + assertOneOffButtonsVisible(true); + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); +}); + +add_task(async function tagRestriction() { + gURLBar.blur(); + await UrlbarTestUtils.promisePopupOpen(window, () => + gURLBar.search(UrlbarTokenizer.RESTRICT.TAG) + ); + ok(gURLBar.hasAttribute("focused"), "url bar is focused"); + // Since tags are not a supported search mode, we should just insert the tag + // restriction token and not enter search mode. + await UrlbarTestUtils.assertSearchMode(window, null); + await assertUrlbarValue(`${UrlbarTokenizer.RESTRICT.TAG} `); + assertOneOffButtonsVisible(true); + await UrlbarTestUtils.promisePopupClose(window); +}); + +// Calls search() twice with the same value. The popup should reopen. +add_task(async function searchTwice() { + gURLBar.blur(); + await UrlbarTestUtils.promisePopupOpen(window, () => gURLBar.search("test")); + ok(gURLBar.hasAttribute("focused"), "url bar is focused"); + await assertUrlbarValue("test"); + assertOneOffButtonsVisible(true); + await UrlbarTestUtils.promisePopupClose(window); + + await UrlbarTestUtils.promisePopupOpen(window, () => gURLBar.search("test")); + ok(gURLBar.hasAttribute("focused"), "url bar is focused"); + await assertUrlbarValue("test"); + assertOneOffButtonsVisible(true); + await UrlbarTestUtils.promisePopupClose(window); +}); + +// Calls search() during an IME composition. +add_task(async function searchIME() { + // First run a search. + gURLBar.blur(); + await UrlbarTestUtils.promisePopupOpen(window, () => gURLBar.search("test")); + ok(gURLBar.hasAttribute("focused"), "url bar is focused"); + await assertUrlbarValue("test"); + // Start composition. + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeComposition({ type: "compositionstart" }) + ); + + gURLBar.search("test"); + // Unfortunately there's no other way to check we don't open the view than to + // wait for it. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 1000)); + ok(!UrlbarTestUtils.isPopupOpen(window), "The panel should still be closed"); + + await UrlbarTestUtils.promisePopupOpen(window, () => + EventUtils.synthesizeComposition({ type: "compositioncommitasis" }) + ); + + assertOneOffButtonsVisible(true); + + await UrlbarTestUtils.promisePopupClose(window); +}); + +// Calls search() with an engine alias. +add_task(async function searchWithAlias() { + await UrlbarTestUtils.promisePopupOpen(window, async () => + gURLBar.search(`${ALIAS} test`, { + searchEngine: aliasEngine, + searchModeEntry: "handoff", + }) + ); + Assert.ok(gURLBar.hasAttribute("focused"), "Urlbar is focused"); + + await UrlbarTestUtils.assertSearchMode(window, { + engineName: aliasEngine.name, + entry: "handoff", + }); + await assertUrlbarValue("test"); + assertOneOffButtonsVisible(true); + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); +}); + +/** + * Asserts that the one-off search buttons are or aren't visible. + * + * @param {boolean} visible + * True if they should be visible, false if not. + */ +function assertOneOffButtonsVisible(visible) { + Assert.equal( + UrlbarTestUtils.getOneOffSearchButtonsVisible(window), + visible, + "Should show or not the one-off search buttons" + ); +} + +/** + * Asserts that the urlbar's input value is the given value. Also asserts that + * the first (heuristic) result in the popup is a search suggestion whose search + * query is the given value. + * + * @param {string} value + * The urlbar's expected value. + */ +async function assertUrlbarValue(value) { + await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0); + + Assert.equal(gURLBar.value, value); + Assert.greater( + UrlbarTestUtils.getResultCount(window), + 0, + "Should have at least one result" + ); + + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.SEARCH, + "Should have type search for the first result" + ); + // Strip search restriction token from value. + if (value[0] == UrlbarTokenizer.RESTRICT.SEARCH) { + value = value.substring(1).trim(); + } + Assert.equal( + result.searchParams.query, + value, + "Should have the correct query for the first result" + ); +} diff --git a/browser/components/urlbar/tests/browser/browser_searchHistoryLimit.js b/browser/components/urlbar/tests/browser/browser_searchHistoryLimit.js new file mode 100644 index 0000000000..9410a8a604 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchHistoryLimit.js @@ -0,0 +1,92 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This test checks that search values longer than + * SearchSuggestionController.SEARCH_HISTORY_MAX_VALUE_LENGTH are not added to + * search history. + */ + +"use strict"; + +const { SearchSuggestionController } = ChromeUtils.import( + "resource://gre/modules/SearchSuggestionController.jsm" +); + +let gEngine; + +add_task(async function setup() { + gEngine = await Services.search.addEngineWithDetails("TestLimit", { + template: "http://example.com/?search={searchTerms}", + }); + let oldDefaultEngine = await Services.search.getDefault(); + await Services.search.setDefault(gEngine); + await UrlbarTestUtils.formHistory.clear(); + + registerCleanupFunction(async function() { + await Services.search.setDefault(oldDefaultEngine); + await Services.search.removeEngine(gEngine); + await UrlbarTestUtils.formHistory.clear(); + }); +}); + +add_task(async function sanityCheckShortString() { + const shortString = new Array( + SearchSuggestionController.SEARCH_HISTORY_MAX_VALUE_LENGTH + ) + .fill("a") + .join(""); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: shortString, + }); + let url = gEngine.getSubmission(shortString).uri.spec; + let loadPromise = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + url + ); + let addPromise = UrlbarTestUtils.formHistory.promiseChanged("add"); + EventUtils.synthesizeKey("VK_RETURN"); + await Promise.all([loadPromise, addPromise]); + + let formHistory = ( + await UrlbarTestUtils.formHistory.search({ source: gEngine.name }) + ).map(entry => entry.value); + Assert.deepEqual( + formHistory, + [shortString], + "Should find form history after adding it" + ); + + await UrlbarTestUtils.formHistory.clear(); +}); + +add_task(async function urlbar_checkLongString() { + const longString = new Array( + SearchSuggestionController.SEARCH_HISTORY_MAX_VALUE_LENGTH + 1 + ) + .fill("a") + .join(""); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: longString, + }); + let url = gEngine.getSubmission(longString).uri.spec; + let loadPromise = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + url + ); + EventUtils.synthesizeKey("VK_RETURN"); + await loadPromise; + // There's nothing we can wait for, since addition should not be happening. + /* eslint-disable mozilla/no-arbitrary-setTimeout */ + await new Promise(resolve => setTimeout(resolve, 500)); + let formHistory = ( + await UrlbarTestUtils.formHistory.search({ source: gEngine.name }) + ).map(entry => entry.value); + Assert.deepEqual(formHistory, [], "Should not find form history"); + + await UrlbarTestUtils.formHistory.clear(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_alias_replacement.js b/browser/components/urlbar/tests/browser/browser_searchMode_alias_replacement.js new file mode 100644 index 0000000000..0190c57add --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_alias_replacement.js @@ -0,0 +1,280 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that user-defined aliases are replaced by the search mode indicator. + */ + +const ALIAS = "testalias"; +const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml"; + +// We make sure that aliases and search terms are correctly recognized when they +// are separated by each of these different types of spaces and combinations of +// spaces. U+3000 is the ideographic space in CJK and is commonly used by CJK +// speakers. +const TEST_SPACES = [" ", "\u3000", " \u3000", "\u3000 "]; + +let defaultEngine, aliasEngine; + +add_task(async function setup() { + defaultEngine = await SearchTestUtils.promiseNewSearchEngine( + getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME + ); + defaultEngine.alias = "@default"; + let oldDefaultEngine = await Services.search.getDefault(); + Services.search.setDefault(defaultEngine); + aliasEngine = await Services.search.addEngineWithDetails("Test", { + alias: ALIAS, + template: "http://example.com/?search={searchTerms}", + }); + + registerCleanupFunction(async function() { + await Services.search.removeEngine(aliasEngine); + Services.search.setDefault(oldDefaultEngine); + }); +}); + +// An incomplete alias should not be replaced. +add_task(async function incompleteAlias() { + // Check that a non-fully typed alias is not replaced. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: ALIAS.slice(0, -1), + }); + await UrlbarTestUtils.assertSearchMode(window, null); + + // Type a space just to make sure it's not replaced. + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey(" "); + await searchPromise; + + await UrlbarTestUtils.assertSearchMode(window, null); + Assert.equal( + gURLBar.value, + ALIAS.slice(0, -1) + " ", + "The typed value should be unchanged except for the space." + ); + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Escape") + ); +}); + +// A complete alias without a trailing space should not be replaced. +add_task(async function noTrailingSpace() { + let value = ALIAS; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value, + }); + await UrlbarTestUtils.assertSearchMode(window, null); + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Escape") + ); +}); + +// A complete typed alias without a trailing space should not be replaced. +add_task(async function noTrailingSpace_typed() { + // Start by searching for the alias minus its last char. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: ALIAS.slice(0, -1), + }); + await UrlbarTestUtils.assertSearchMode(window, null); + + // Now type the last char. + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey(ALIAS.slice(-1)); + await searchPromise; + + await UrlbarTestUtils.assertSearchMode(window, null); + Assert.equal( + gURLBar.value, + ALIAS, + "The typed value should be the full alias." + ); + + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Escape") + ); +}); + +// A complete alias with a trailing space should be replaced. +add_task(async function trailingSpace() { + for (let spaces of TEST_SPACES) { + info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) })); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: ALIAS + spaces, + }); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: aliasEngine.name, + entry: "typed", + }); + Assert.ok(!gURLBar.value, "The urlbar value should be cleared."); + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); + } +}); + +// A complete alias should be replaced after typing a space. +add_task(async function trailingSpace_typed() { + for (let spaces of TEST_SPACES) { + if (spaces.length != 1) { + continue; + } + info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) })); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: ALIAS, + }); + await UrlbarTestUtils.assertSearchMode(window, null); + + // We need to wait for two searches: The first enters search mode, the second + // does the search in search mode. + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey(spaces); + await searchPromise; + + await UrlbarTestUtils.assertSearchMode(window, { + engineName: aliasEngine.name, + entry: "typed", + }); + Assert.ok(!gURLBar.value, "The urlbar value should be cleared."); + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); + } +}); + +// A complete alias with a trailing space should be replaced, and the query +// after the trailing space should be the new value of the input. +add_task(async function trailingSpace_query() { + for (let spaces of TEST_SPACES) { + info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) })); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: ALIAS + spaces + "query", + }); + + await UrlbarTestUtils.assertSearchMode(window, { + engineName: aliasEngine.name, + entry: "typed", + }); + Assert.equal( + gURLBar.value, + "query", + "The urlbar value should be the query." + ); + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); + } +}); + +add_task(async function() { + info("Test search mode when typing an alias after selecting one-off button"); + + info("Open the result popup"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + + info("Select one of one-off button"); + const oneOffs = UrlbarTestUtils.getOneOffSearchButtons(window); + await TestUtils.waitForCondition( + () => !oneOffs._rebuilding, + "Waiting for one-offs to finish rebuilding" + ); + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + ok(oneOffs.selectedButton, "There is a selected one-off button"); + const selectedEngine = oneOffs.selectedButton.engine; + await UrlbarTestUtils.assertSearchMode(window, { + engineName: selectedEngine.name, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + entry: "oneoff", + isPreview: true, + }); + + info("Type a search engine alias and query"); + const inputString = "@default query"; + inputString.split("").forEach(c => EventUtils.synthesizeKey(c)); + await UrlbarTestUtils.promiseSearchComplete(window); + Assert.equal( + gURLBar.value, + inputString, + "Alias and query is inputed correctly to the urlbar" + ); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: selectedEngine.name, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + entry: "oneoff", + }); + + // When starting typing, as the search mode is confirmed, the one-off + // selection is removed. + ok(!oneOffs.selectedButton, "There is no any selected one-off button"); + + // Clean up + gURLBar.value = ""; + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); +}); + +add_task(async function() { + info( + "Test search mode after removing current search mode when multiple aliases are written" + ); + + info("Open the result popup with multiple aliases"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "@default testalias @default", + }); + + await UrlbarTestUtils.assertSearchMode(window, { + engineName: defaultEngine.name, + entry: "typed", + }); + Assert.equal( + gURLBar.value, + "testalias @default", + "The value on the urlbar is correct" + ); + + info("Exit search mode by clicking"); + const indicator = gURLBar.querySelector("#urlbar-search-mode-indicator"); + EventUtils.synthesizeMouseAtCenter(indicator, { type: "mouseover" }, window); + const closeButton = gURLBar.querySelector( + "#urlbar-search-mode-indicator-close" + ); + const searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeMouseAtCenter(closeButton, {}, window); + await searchPromise; + + await UrlbarTestUtils.assertSearchMode(window, { + engineName: aliasEngine.name, + entry: "typed", + }); + Assert.equal(gURLBar.value, "@default", "The value on the urlbar is correct"); + + // Clean up + gURLBar.value = ""; + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); +}); + +/** + * Returns an array of code points in the given string. Each code point is + * returned as a hexidecimal string. + * + * @param {string} str + * The code points of this string will be returned. + * @returns {array} + * Array of code points in the string, where each is a hexidecimal string. + */ +function codePoints(str) { + return str.split("").map(s => s.charCodeAt(0).toString(16)); +} diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_autofill.js b/browser/components/urlbar/tests/browser/browser_searchMode_autofill.js new file mode 100644 index 0000000000..4ddb11bff2 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_autofill.js @@ -0,0 +1,137 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that autofill is cleared if a remote search mode is entered but still + * works for local search modes. + */ + +"use strict"; + +const DEFAULT_ENGINE_NAME = "Test"; + +add_task(async function setup() { + for (let i = 0; i < 5; i++) { + await PlacesTestUtils.addVisits([{ uri: "http://example.com/" }]); + } + + let oldDefaultEngine = await Services.search.getDefault(); + let defaultEngine = await Services.search.addEngineWithDetails( + DEFAULT_ENGINE_NAME, + { + template: "http://example.com/?search={searchTerms}", + } + ); + await Services.search.setDefault(defaultEngine); + await Services.search.moveEngine(defaultEngine, 0); + + registerCleanupFunction(async () => { + await PlacesUtils.history.clear(); + await Services.search.setDefault(oldDefaultEngine); + await Services.search.removeEngine(defaultEngine); + }); +}); + +// Tests that autofill is cleared when entering a remote search mode and that +// autofill doesn't happen when in that mode. +add_task(async function remote() { + info("Sanity check: we autofill in a normal search."); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "ex", + fireInputEvent: true, + }); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill, "We're autofilling."); + Assert.equal( + gURLBar.value, + "example.com/", + "Urlbar contains the autofilled URL." + ); + info("Enter remote search mode and check autofill is cleared."); + await UrlbarTestUtils.enterSearchMode(window); + Assert.equal(gURLBar.value, "ex", "Urlbar contains the typed string."); + + info("Continue typing and check that we're not autofilling."); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exa", + fireInputEvent: true, + }); + + details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(!details.autofill, "We're not autofilling."); + Assert.equal(gURLBar.value, "exa", "Urlbar contains the typed string."); + + info("Exit remote search mode and check that we now autofill."); + await UrlbarTestUtils.exitSearchMode(window, { backspace: true }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exam", + fireInputEvent: true, + }); + details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill, "We're autofilling."); + Assert.equal( + gURLBar.value, + "example.com/", + "Urlbar contains the typed string." + ); +}); + +// Tests that autofill works as normal when entering and when in a local search +// mode. +add_task(async function local() { + info("Sanity check: we autofill in a normal search."); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "ex", + fireInputEvent: true, + }); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill, "We're autofilling."); + Assert.equal( + gURLBar.value, + "example.com/", + "Urlbar contains the autofilled URL." + ); + info("Enter local search mode and check autofill is preserved."); + await UrlbarTestUtils.enterSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + }); + Assert.equal( + gURLBar.value, + "example.com/", + "Urlbar contains the autofilled URL." + ); + + info("Continue typing and check that we're autofilling."); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exa", + fireInputEvent: true, + }); + + details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill, "We're autofilling."); + Assert.equal( + gURLBar.value, + "example.com/", + "Urlbar contains the autofilled URL." + ); + + info("Exit local search mode and check that nothing has changed."); + await UrlbarTestUtils.exitSearchMode(window, { backspace: true }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exam", + fireInputEvent: true, + }); + details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(details.autofill, "We're autofilling."); + Assert.equal( + gURLBar.value, + "example.com/", + "Urlbar contains the typed string." + ); +}); diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_clickLink.js b/browser/components/urlbar/tests/browser/browser_searchMode_clickLink.js new file mode 100644 index 0000000000..d037c77bbb --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_clickLink.js @@ -0,0 +1,94 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that search mode is exited after clicking a link and loading a page in + * the current tab. + */ + +"use strict"; + +const LINK_PAGE_URL = + "http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/dummy_page.html"; + +// Opens a new tab containing a link, enters search mode, and clicks the link. +// Uses a variety of search strings and link hrefs in order to hit different +// branches in setURI. Search mode should be exited in all cases, and the href +// in the link should be opened. +add_task(async function clickLink() { + for (let test of [ + // searchString, href to use in the link + [LINK_PAGE_URL, LINK_PAGE_URL], + [LINK_PAGE_URL, "http://www.example.com/"], + ["test", LINK_PAGE_URL], + ["test", "http://www.example.com/"], + [null, LINK_PAGE_URL], + [null, "http://www.example.com/"], + ]) { + await doClickLinkTest(...test); + } +}); + +async function doClickLinkTest(searchString, href) { + info( + "doClickLinkTest with args: " + + JSON.stringify({ + searchString, + href, + }) + ); + + await BrowserTestUtils.withNewTab(LINK_PAGE_URL, async () => { + if (searchString) { + // Do a search with the search string. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: searchString, + fireInputEvent: true, + }); + Assert.ok( + gBrowser.selectedBrowser.userTypedValue, + "userTypedValue should be defined" + ); + } else { + // Open top sites. + await UrlbarTestUtils.promisePopupOpen(window, () => { + document.getElementById("Browser:OpenLocation").doCommand(); + }); + Assert.strictEqual( + gBrowser.selectedBrowser.userTypedValue, + null, + "userTypedValue should be null" + ); + } + + // Enter search mode and then close the popup so we can click the link in + // the page. + await UrlbarTestUtils.enterSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + }); + await UrlbarTestUtils.promisePopupClose(window); + await UrlbarTestUtils.assertSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + entry: "oneoff", + }); + + // Add a link to the page and click it. + let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + await ContentTask.spawn(gBrowser.selectedBrowser, href, async cHref => { + let link = this.content.document.createElement("a"); + link.textContent = "Click me"; + link.href = cHref; + this.content.document.body.append(link); + link.click(); + }); + await loadPromise; + Assert.equal( + gBrowser.currentURI.spec, + href, + "Should have loaded the href URL" + ); + + await UrlbarTestUtils.assertSearchMode(window, null); + }); +} diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_engineRemoval.js b/browser/components/urlbar/tests/browser/browser_searchMode_engineRemoval.js new file mode 100644 index 0000000000..ef2fc15d17 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_engineRemoval.js @@ -0,0 +1,103 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that we exit search mode when the search mode engine is removed. + */ + +"use strict"; + +// Tests that we exit search mode in the active tab when the search mode engine +// is removed. +add_task(async function activeTab() { + let engine = await Services.search.addEngineWithDetails("Test", { + template: "http://example.com/?search={searchTerms}", + }); + await Services.search.moveEngine(engine, 0); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "ex", + }); + await UrlbarTestUtils.enterSearchMode(window); + // Sanity check: we are in the correct search mode. + await UrlbarTestUtils.assertSearchMode(window, { + engineName: engine.name, + entry: "oneoff", + }); + await Services.search.removeEngine(engine); + // Check that we are no longer in search mode. + await UrlbarTestUtils.assertSearchMode(window, null); +}); + +// Tests that we exit search mode in a background tab when the search mode +// engine is removed. +add_task(async function backgroundTab() { + let engine = await Services.search.addEngineWithDetails("Test", { + template: "http://example.com/?search={searchTerms}", + }); + await Services.search.moveEngine(engine, 0); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "ex", + }); + await UrlbarTestUtils.enterSearchMode(window); + let tab1 = gBrowser.selectedTab; + let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + // Sanity check: tab1 is still in search mode. + await BrowserTestUtils.switchTab(gBrowser, tab1); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: engine.name, + entry: "oneoff", + }); + + // Switch back to tab2 so tab1 is in the background when the engine is + // removed. + await BrowserTestUtils.switchTab(gBrowser, tab2); + // tab2 shouldn't be in search mode. + await UrlbarTestUtils.assertSearchMode(window, null); + await Services.search.removeEngine(engine); + + // tab1 should have exited search mode. + await BrowserTestUtils.switchTab(gBrowser, tab1); + await UrlbarTestUtils.assertSearchMode(window, null); + BrowserTestUtils.removeTab(tab2); +}); + +// Tests that we exit search mode in a background window when the search mode +// engine is removed. +add_task(async function backgroundWindow() { + let engine = await Services.search.addEngineWithDetails("Test", { + template: "http://example.com/?search={searchTerms}", + }); + await Services.search.moveEngine(engine, 0); + + let win1 = window; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win1, + value: "ex", + }); + await UrlbarTestUtils.enterSearchMode(win1); + let win2 = await BrowserTestUtils.openNewBrowserWindow(); + + // Sanity check: win1 is still in search mode. + win1.focus(); + await UrlbarTestUtils.assertSearchMode(win1, { + engineName: engine.name, + entry: "oneoff", + }); + + // Switch back to win2 so win1 is in the background when the engine is + // removed. + win2.focus(); + // win2 shouldn't be in search mode. + await UrlbarTestUtils.assertSearchMode(win2, null); + await Services.search.removeEngine(engine); + + // win1 should not be in search mode. + win1.focus(); + await UrlbarTestUtils.assertSearchMode(win1, null); + await BrowserTestUtils.closeWindow(win2); +}); diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_excludeResults.js b/browser/components/urlbar/tests/browser/browser_searchMode_excludeResults.js new file mode 100644 index 0000000000..b8deaea9ec --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_excludeResults.js @@ -0,0 +1,210 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that results with hostnames other than the search mode engine are not + * shown. + */ + +"use strict"; + +XPCOMUtils.defineLazyModuleGetters(this, { + SyncedTabs: "resource://services-sync/SyncedTabs.jsm", +}); + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.searches", false], + ["browser.urlbar.autoFill", false], + // Special prefs for remote tabs. + ["services.sync.username", "fake"], + ["services.sync.syncedTabs.showRemoteTabs", true], + ], + }); + + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + + let oldDefaultEngine = await Services.search.getDefault(); + // Note that the result domain is subdomain.example.ca. We still expect to + // match with example.com results because we ignore subdomains and the public + // suffix in this check. + let engine = await Services.search.addEngineWithDetails("Test", { + template: `http://subdomain.example.ca/?search={searchTerms}`, + }); + await Services.search.setDefault(engine); + await Services.search.moveEngine(engine, 0); + + const REMOTE_TAB = { + id: "7cqCr77ptzX3", + type: "client", + lastModified: 1492201200, + name: "Nightly on MacBook-Pro", + clientType: "desktop", + tabs: [ + { + type: "tab", + title: "Test Remote", + url: "http://example.com", + icon: UrlbarUtils.ICON.DEFAULT, + client: "7cqCr77ptzX3", + lastUsed: 1452124677, + }, + { + type: "tab", + title: "Test Remote 2", + url: "http://example-2.com", + icon: UrlbarUtils.ICON.DEFAULT, + client: "7cqCr77ptzX3", + lastUsed: 1452124677, + }, + ], + }; + + const sandbox = sinon.createSandbox(); + + let originalSyncedTabsInternal = SyncedTabs._internal; + SyncedTabs._internal = { + isConfiguredToSyncTabs: true, + hasSyncedThisSession: true, + getTabClients() { + return Promise.resolve([]); + }, + syncTabs() { + return Promise.resolve(); + }, + }; + + // Tell the Sync XPCOM service it is initialized. + let weaveXPCService = Cc["@mozilla.org/weave/service;1"].getService( + Ci.nsISupports + ).wrappedJSObject; + let oldWeaveServiceReady = weaveXPCService.ready; + weaveXPCService.ready = true; + + sandbox + .stub(SyncedTabs._internal, "getTabClients") + .callsFake(() => Promise.resolve(Cu.cloneInto([REMOTE_TAB], {}))); + + // Reset internal cache in PlacesRemoteTabsAutocompleteProvider. + Services.obs.notifyObservers(null, "weave:engine:sync:finish", "tabs"); + + registerCleanupFunction(async function() { + sandbox.restore(); + weaveXPCService.ready = oldWeaveServiceReady; + SyncedTabs._internal = originalSyncedTabsInternal; + await Services.search.setDefault(oldDefaultEngine); + await Services.search.removeEngine(engine); + await PlacesUtils.history.clear(); + }); +}); + +add_task(async function basic() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "example", + }); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 3, + "We have three results" + ); + let firstResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + firstResult.type, + UrlbarUtils.RESULT_TYPE.SEARCH, + "The first result is the heuristic search result." + ); + let secondResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal( + secondResult.type, + UrlbarUtils.RESULT_TYPE.REMOTE_TAB, + "The second result is a remote tab." + ); + let thirdResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal( + thirdResult.type, + UrlbarUtils.RESULT_TYPE.REMOTE_TAB, + "The third result is a remote tab." + ); + + await UrlbarTestUtils.enterSearchMode(window); + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 2, + "We have two results. The second remote tab result is excluded despite matching the search string." + ); + firstResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + firstResult.type, + UrlbarUtils.RESULT_TYPE.SEARCH, + "The first result is the heuristic search result." + ); + secondResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal( + secondResult.type, + UrlbarUtils.RESULT_TYPE.REMOTE_TAB, + "The second result is a remote tab." + ); + + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); +}); + +// For engines with an invalid TLD, we filter on the entire domain. +add_task(async function malformedEngine() { + let badEngine = await Services.search.addEngineWithDetails("TestMalformed", { + template: `http://example.foobar/?search={searchTerms}`, + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "example", + }); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 3, + "We have three results" + ); + let firstResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + firstResult.type, + UrlbarUtils.RESULT_TYPE.SEARCH, + "The first result is the heuristic search result." + ); + let secondResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal( + secondResult.type, + UrlbarUtils.RESULT_TYPE.REMOTE_TAB, + "The second result is a remote tab." + ); + let thirdResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal( + thirdResult.type, + UrlbarUtils.RESULT_TYPE.REMOTE_TAB, + "The third result is a remote tab." + ); + + await UrlbarTestUtils.enterSearchMode(window, { + engineName: badEngine.name, + }); + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 1, + "We only have one result." + ); + firstResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(firstResult.heuristic, "The first result is heuristic."); + Assert.equal( + firstResult.type, + UrlbarUtils.RESULT_TYPE.SEARCH, + "The first result is the heuristic search result." + ); + + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); + await Services.search.removeEngine(badEngine); +}); diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_heuristic.js b/browser/components/urlbar/tests/browser/browser_searchMode_heuristic.js new file mode 100644 index 0000000000..0505448d63 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_heuristic.js @@ -0,0 +1,226 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests heuristic results in search mode. + */ + +"use strict"; + +add_task(async function setup() { + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + + // Add a new mock default engine so we don't hit the network. + let oldDefaultEngine = await Services.search.getDefault(); + let engine = await Services.search.addEngineWithDetails("Test", { + template: "http://example.com/?search={searchTerms}", + }); + await Services.search.setDefault(engine); + registerCleanupFunction(async () => { + await Services.search.setDefault(oldDefaultEngine); + await Services.search.removeEngine(engine); + }); + + // Add one bookmark we'll use below. + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.com/bookmark", + }); + registerCleanupFunction(async () => { + await PlacesUtils.bookmarks.eraseEverything(); + }); +}); + +// Enters search mode with no results. +add_task(async function noResults() { + // Do a search that doesn't match our bookmark and enter bookmark search mode. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "doesn't match anything", + }); + await UrlbarTestUtils.enterSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + }); + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 0, + "Zero results since no bookmark matches" + ); + + // Press enter. Nothing should happen. + let loadPromise = waitForLoadOrTimeout(); + EventUtils.synthesizeKey("KEY_Enter"); + let loadEvent = await loadPromise; + Assert.ok(!loadEvent, "Nothing should have loaded"); + + await UrlbarTestUtils.promisePopupClose(window); +}); + +// Enters a local search mode (bookmarks) with a matching result. No heuristic +// should be present. +add_task(async function localNoHeuristic() { + // Do a search that matches our bookmark and enter bookmarks search mode. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "bookmark", + }); + await UrlbarTestUtils.enterSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + }); + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 1, + "There should be one result" + ); + + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + result.source, + UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + "Result source should be BOOKMARKS" + ); + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.URL, + "Result type should be URL" + ); + Assert.equal( + result.url, + "http://example.com/bookmark", + "Result URL is our bookmark URL" + ); + Assert.ok(!result.heuristic, "Result should not be heuristic"); + + // Press enter. Nothing should happen. + let loadPromise = waitForLoadOrTimeout(); + EventUtils.synthesizeKey("KEY_Enter"); + let loadEvent = await loadPromise; + Assert.ok(!loadEvent, "Nothing should have loaded"); + + await UrlbarTestUtils.promisePopupClose(window); +}); + +// Enters a local search mode (bookmarks) with a matching autofill result. The +// result should be the heuristic. +add_task(async function localAutofill() { + await BrowserTestUtils.withNewTab("about:blank", async () => { + // Do a search that autofills our bookmark's origin and enter bookmarks + // search mode. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "example", + }); + await UrlbarTestUtils.enterSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + }); + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 2, + "There should be two results" + ); + + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + result.source, + UrlbarUtils.RESULT_SOURCE.HISTORY, + "Result source should be HISTORY" + ); + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.URL, + "Result type should be URL" + ); + Assert.equal( + result.url, + "http://example.com/", + "Result URL is our bookmark's origin" + ); + Assert.ok(result.heuristic, "Result should be heuristic"); + Assert.ok(result.autofill, "Result should be autofill"); + + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal( + result.source, + UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + "Result source should be BOOKMARKS" + ); + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.URL, + "Result type should be URL" + ); + Assert.equal( + result.url, + "http://example.com/bookmark", + "Result URL is our bookmark URL" + ); + + // Press enter. Our bookmark's origin should be loaded. + let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + EventUtils.synthesizeKey("KEY_Enter"); + await loadPromise; + Assert.equal( + gBrowser.currentURI.spec, + "http://example.com/", + "Bookmark's origin should have loaded" + ); + }); +}); + +// Enters a remote engine search mode. There should be a heuristic. +add_task(async function remote() { + await BrowserTestUtils.withNewTab("about:blank", async () => { + // Do a search and enter search mode with our test engine. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "remote", + }); + await UrlbarTestUtils.enterSearchMode(window, { + engineName: "Test", + }); + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 1, + "There should be one result" + ); + + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + result.source, + UrlbarUtils.RESULT_SOURCE.SEARCH, + "Result source should be SEARCH" + ); + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.SEARCH, + "Result type should be SEARCH" + ); + Assert.ok(result.searchParams, "searchParams should be present"); + Assert.equal( + result.searchParams.engine, + "Test", + "searchParams.engine should be our test engine" + ); + Assert.equal( + result.searchParams.query, + "remote", + "searchParams.query should be our query" + ); + Assert.ok(result.heuristic, "Result should be heuristic"); + + // Press enter. The engine's SERP should load. + let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + EventUtils.synthesizeKey("KEY_Enter"); + await loadPromise; + Assert.equal( + gBrowser.currentURI.spec, + "http://example.com/?search=remote", + "Engine's SERP should have loaded" + ); + }); +}); diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_indicator.js b/browser/components/urlbar/tests/browser/browser_searchMode_indicator.js new file mode 100644 index 0000000000..6412021a71 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_indicator.js @@ -0,0 +1,388 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests interactions with the search mode indicator. See browser_oneOffs.js for + * more coverage. + */ + +const TEST_QUERY = "test string"; +const DEFAULT_ENGINE_NAME = "Test"; +const SUGGESTIONS_ENGINE_NAME = "searchSuggestionEngine.xml"; + +// These need to have different domains because otherwise new tab and/or +// activity stream collapses them. +const TOP_SITES_URLS = [ + "http://top-site-0.com/", + "http://top-site-1.com/", + "http://top-site-2.com/", +]; + +let suggestionsEngine; +let defaultEngine; + +add_task(async function setup() { + suggestionsEngine = await SearchTestUtils.promiseNewSearchEngine( + getRootDirectory(gTestPath) + SUGGESTIONS_ENGINE_NAME + ); + + let oldDefaultEngine = await Services.search.getDefault(); + defaultEngine = await Services.search.addEngineWithDetails( + DEFAULT_ENGINE_NAME, + { + template: "http://example.com/?search={searchTerms}", + } + ); + await Services.search.setDefault(defaultEngine); + await Services.search.moveEngine(suggestionsEngine, 0); + + registerCleanupFunction(async () => { + await Services.search.setDefault(oldDefaultEngine); + await Services.search.removeEngine(defaultEngine); + }); + + // Set our top sites. + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "browser.newtabpage.activity-stream.default.sites", + TOP_SITES_URLS.join(","), + ], + ], + }); + await updateTopSites(sites => + ObjectUtils.deepEqual( + sites.map(s => s.url), + TOP_SITES_URLS + ) + ); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.search.separatePrivateDefault.ui.enabled", false]], + }); +}); + +async function verifySearchModeResultsAdded(window) { + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 3, + "There should be three results." + ); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + result.searchParams.engine, + suggestionsEngine.name, + "The first result should be a search result for our suggestion engine." + ); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal( + result.searchParams.suggestion, + `${TEST_QUERY}foo`, + "The second result should be a suggestion result." + ); + Assert.equal( + result.searchParams.engine, + suggestionsEngine.name, + "The second result should be a search result for our suggestion engine." + ); +} + +async function verifySearchModeResultsRemoved(window) { + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 1, + "There should only be one result." + ); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + result.searchParams.engine, + defaultEngine.name, + "The first result should be a search result for our default engine." + ); +} + +async function verifyTopSitesResultsAdded(window) { + Assert.equal( + UrlbarTestUtils.getResultCount(window), + TOP_SITES_URLS.length, + "Expected number of top sites results" + ); + for (let i = 0; i < TOP_SITES_URLS; i++) { + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + Assert.equal( + result.url, + TOP_SITES_URLS[i], + `Expected top sites result URL at index ${i}` + ); + } +} + +// Tests that the indicator is removed when backspacing at the beginning of +// the search string. +add_task(async function backspace() { + // View open, with string. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TEST_QUERY, + }); + await UrlbarTestUtils.enterSearchMode(window); + await verifySearchModeResultsAdded(window); + await UrlbarTestUtils.exitSearchMode(window, { backspace: true }); + await verifySearchModeResultsRemoved(window); + Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Urlbar view is open."); + + // View open, no string (i.e., top sites). + await UrlbarTestUtils.promisePopupOpen(window, () => { + if (gURLBar.getAttribute("pageproxystate") == "invalid") { + gURLBar.handleRevert(); + } + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + await UrlbarTestUtils.enterSearchMode(window); + await UrlbarTestUtils.exitSearchMode(window, { backspace: true }); + Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Urlbar view is open."); + await verifyTopSitesResultsAdded(window); + await UrlbarTestUtils.promisePopupClose(window); + + // View closed, with string. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TEST_QUERY, + }); + await UrlbarTestUtils.enterSearchMode(window); + await verifySearchModeResultsAdded(window); + await UrlbarTestUtils.promisePopupClose(window); + await UrlbarTestUtils.exitSearchMode(window, { backspace: true }); + await verifySearchModeResultsRemoved(window); + Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Urlbar view is now open."); + + // View closed, no string (i.e., top sites). + await UrlbarTestUtils.promisePopupOpen(window, () => { + if (gURLBar.getAttribute("pageproxystate") == "invalid") { + gURLBar.handleRevert(); + } + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + await UrlbarTestUtils.enterSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); + await UrlbarTestUtils.exitSearchMode(window, { backspace: true }); + Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Urlbar view is open."); + await verifyTopSitesResultsAdded(window); + await UrlbarTestUtils.promisePopupClose(window); +}); + +add_task(async function escapeOnInitialPage() { + info("Tests the indicator's interaction with the ESC key"); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TEST_QUERY, + }); + await UrlbarTestUtils.enterSearchMode(window); + await verifySearchModeResultsAdded(window); + + EventUtils.synthesizeKey("KEY_Escape"); + Assert.ok(!UrlbarTestUtils.isPopupOpen(window, "UrlbarView is closed.")); + Assert.equal(gURLBar.value, TEST_QUERY, "Urlbar value hasn't changed."); + + let oneOffs = UrlbarTestUtils.getOneOffSearchButtons( + window + ).getSelectableButtons(true); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: oneOffs[0].engine.name, + entry: "oneoff", + }); + + EventUtils.synthesizeKey("KEY_Escape"); + Assert.ok(!UrlbarTestUtils.isPopupOpen(window, "UrlbarView is closed.")); + Assert.ok(!gURLBar.value, "Urlbar value is empty."); + await UrlbarTestUtils.assertSearchMode(window, null); +}); + +add_task(async function escapeOnBrowsingPage() { + info("Tests the indicator's interaction with the ESC key on browsing page"); + + await BrowserTestUtils.withNewTab("http://example.com", async browser => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TEST_QUERY, + }); + await UrlbarTestUtils.enterSearchMode(window); + await verifySearchModeResultsAdded(window); + + EventUtils.synthesizeKey("KEY_Escape"); + Assert.ok(!UrlbarTestUtils.isPopupOpen(window, "UrlbarView is closed.")); + Assert.equal(gURLBar.value, TEST_QUERY, "Urlbar value hasn't changed."); + + const oneOffs = UrlbarTestUtils.getOneOffSearchButtons( + window + ).getSelectableButtons(true); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: oneOffs[0].engine.name, + entry: "oneoff", + }); + + EventUtils.synthesizeKey("KEY_Escape"); + Assert.ok(!UrlbarTestUtils.isPopupOpen(window, "UrlbarView is closed.")); + Assert.equal( + gURLBar.value, + "example.com", + "Urlbar value indicates the browsing page." + ); + await UrlbarTestUtils.assertSearchMode(window, null); + }); +}); + +// Tests that the indicator is removed when its close button is clicked. +add_task(async function click_close() { + // Clicking close with the view open, with string. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TEST_QUERY, + }); + await UrlbarTestUtils.enterSearchMode(window); + await verifySearchModeResultsAdded(window); + await UrlbarTestUtils.exitSearchMode(window, { clickClose: true }); + await verifySearchModeResultsRemoved(window); + Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Urlbar view is open."); + await UrlbarTestUtils.promisePopupClose(window); + + // Clicking close with the view open, no string (i.e., top sites). + await UrlbarTestUtils.promisePopupOpen(window, () => { + if (gURLBar.getAttribute("pageproxystate") == "invalid") { + gURLBar.handleRevert(); + } + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + await UrlbarTestUtils.enterSearchMode(window); + await UrlbarTestUtils.exitSearchMode(window, { clickClose: true }); + Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Urlbar view is open."); + await verifyTopSitesResultsAdded(window); + await UrlbarTestUtils.promisePopupClose(window); + + // Clicking close with the view closed, with string. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TEST_QUERY, + }); + await UrlbarTestUtils.enterSearchMode(window); + await verifySearchModeResultsAdded(window); + await UrlbarTestUtils.promisePopupClose(window); + await UrlbarTestUtils.exitSearchMode(window, { + clickClose: true, + waitForSearch: false, + }); + Assert.ok(!UrlbarTestUtils.isPopupOpen(window), "Urlbar view is closed."); + + // Clicking close with the view closed, no string (i.e., top sites). + await UrlbarTestUtils.promisePopupOpen(window, () => { + if (gURLBar.getAttribute("pageproxystate") == "invalid") { + gURLBar.handleRevert(); + } + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + await UrlbarTestUtils.enterSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); + await UrlbarTestUtils.exitSearchMode(window, { + clickClose: true, + waitForSearch: false, + }); + Assert.ok(!UrlbarTestUtils.isPopupOpen(window), "Urlbar view is closed."); +}); + +// Tests that Accel+K enters search mode with the default engine. Also tests +// that Accel+K highlights the typed search string. +add_task(async function keyboard_shortcut() { + const query = "test query"; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: query, + }); + await UrlbarTestUtils.assertSearchMode(window, null); + Assert.equal( + gURLBar.selectionStart, + gURLBar.selectionEnd, + "The search string is not highlighted." + ); + EventUtils.synthesizeKey("k", { accelKey: true }); + await UrlbarTestUtils.assertSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + engineName: defaultEngine.name, + entry: "shortcut", + }); + Assert.equal(gURLBar.value, query, "The search string was not cleared."); + Assert.equal(gURLBar.selectionStart, 0); + Assert.equal( + gURLBar.selectionEnd, + query.length, + "The search string is highlighted." + ); + await UrlbarTestUtils.exitSearchMode(window, { + clickClose: true, + waitForSearch: false, + }); + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); +}); + +// Tests that the Tools:Search menu item enters search mode with the default +// engine. Also tests that Tools:Search highlights the typed search string. +add_task(async function menubar_item() { + const query = "test query 2"; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: query, + }); + await UrlbarTestUtils.assertSearchMode(window, null); + Assert.equal( + gURLBar.selectionStart, + gURLBar.selectionEnd, + "The search string is not highlighted." + ); + let command = window.document.getElementById("Tools:Search"); + command.doCommand(); + await UrlbarTestUtils.assertSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + engineName: defaultEngine.name, + entry: "shortcut", + }); + Assert.equal(gURLBar.value, query, "The search string was not cleared."); + Assert.equal(gURLBar.selectionStart, 0); + Assert.equal( + gURLBar.selectionEnd, + query.length, + "The search string is highlighted." + ); + await UrlbarTestUtils.exitSearchMode(window, { + clickClose: true, + waitForSearch: false, + }); + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); +}); + +// Tests that entering search mode invalidates pageproxystate and that +// pageproxystate remains invalid after exiting search mode. +add_task(async function invalidate_pageproxystate() { + await BrowserTestUtils.withNewTab("about:robots", async function(browser) { + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + Assert.equal(gURLBar.getAttribute("pageproxystate"), "valid"); + await UrlbarTestUtils.enterSearchMode(window); + Assert.equal( + gURLBar.getAttribute("pageproxystate"), + "invalid", + "Entering search mode should clear pageproxystate." + ); + Assert.equal(gURLBar.value, "", "Urlbar value should be cleared."); + await UrlbarTestUtils.exitSearchMode(window, { clickClose: true }); + Assert.equal( + gURLBar.getAttribute("pageproxystate"), + "invalid", + "Pageproxystate should still be invalid after exiting search mode." + ); + }); +}); diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_localOneOffs_actionText.js b/browser/components/urlbar/tests/browser/browser_searchMode_localOneOffs_actionText.js new file mode 100644 index 0000000000..1c426a3a8e --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_localOneOffs_actionText.js @@ -0,0 +1,462 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests action text shown on heuristic and search suggestions when keyboard + * navigating local one-off buttons. + */ + +"use strict"; + +const DEFAULT_ENGINE_NAME = "Test"; +const SUGGESTIONS_ENGINE_NAME = "searchSuggestionEngine.xml"; + +let engine; + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.searches", true]], + }); + engine = await SearchTestUtils.promiseNewSearchEngine( + getRootDirectory(gTestPath) + SUGGESTIONS_ENGINE_NAME + ); + let oldDefaultEngine = await Services.search.getDefault(); + await Services.search.setDefault(engine); + await Services.search.moveEngine(engine, 0); + + await PlacesUtils.history.clear(); + + registerCleanupFunction(async () => { + await Services.search.setDefault(oldDefaultEngine); + await PlacesUtils.history.clear(); + }); +}); + +add_task(async function localOneOff() { + info("Type some text, select a local one-off, check heuristic action"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "query", + }); + Assert.ok(UrlbarTestUtils.getResultCount(window) > 1, "Sanity check results"); + + info("Alt UP to select the last local one-off."); + EventUtils.synthesizeKey("KEY_ArrowUp", { altKey: true }); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 0, + "the heuristic result should be selected" + ); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + let oneOffButtons = UrlbarTestUtils.getOneOffSearchButtons(window); + Assert.equal( + oneOffButtons.selectedButton.source, + UrlbarUtils.RESULT_SOURCE.HISTORY, + "A local one-off button should be selected" + ); + Assert.ok( + BrowserTestUtils.is_visible(result.element.action), + "The heuristic action should be visible" + ); + let [actionHistory, actionBookmarks] = await document.l10n.formatValues([ + { id: "urlbar-result-action-search-history" }, + { id: "urlbar-result-action-search-bookmarks" }, + ]); + Assert.equal( + result.displayed.action, + actionHistory, + "Check the heuristic action" + ); + Assert.equal( + result.image, + "chrome://browser/skin/history.svg", + "Check the heuristic icon" + ); + + info("Move to an engine one-off and check heuristic action"); + EventUtils.synthesizeKey("KEY_ArrowUp", { altKey: true }); + EventUtils.synthesizeKey("KEY_ArrowUp", { altKey: true }); + EventUtils.synthesizeKey("KEY_ArrowUp", { altKey: true }); + Assert.ok( + oneOffButtons.selectedButton.engine, + "A one-off search button should be selected" + ); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok( + BrowserTestUtils.is_visible(result.element.action), + "The heuristic action should be visible" + ); + Assert.ok( + result.displayed.action.includes(oneOffButtons.selectedButton.engine.name), + "Check the heuristic action" + ); + Assert.equal( + result.image, + oneOffButtons.selectedButton.engine.iconURI.spec, + "Check the heuristic icon" + ); + + info("Move again to a local one-off, deselect and reselect the heuristic"); + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + oneOffButtons.selectedButton.source, + UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + "A local one-off button should be selected" + ); + Assert.equal( + result.displayed.action, + actionBookmarks, + "Check the heuristic action" + ); + Assert.equal( + result.image, + "chrome://browser/skin/bookmark.svg", + "Check the heuristic icon" + ); + + info( + "Select the next result, then reselect the heuristic, check it's a search with the default engine" + ); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 0, + "the heuristic result should be selected" + ); + EventUtils.synthesizeKey("KEY_ArrowDown", {}); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 1, + "the heuristic result should not be selected" + ); + EventUtils.synthesizeKey("KEY_ArrowUp", {}); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 0, + "the heuristic result should be selected" + ); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.SEARCH); + Assert.equal(result.searchParams.engine, engine.name); + Assert.ok( + result.displayed.action.includes(engine.name), + "Check the heuristic action" + ); + Assert.equal( + result.image, + "chrome://browser/skin/search-glass.svg", + "Check the heuristic icon" + ); +}); + +add_task(async function localOneOff_withVisit() { + info("Type a url, select a local one-off, check heuristic action"); + await PlacesUtils.history.clear(); + for (let i = 0; i < 5; i++) { + await PlacesTestUtils.addVisits("https://mozilla.org/"); + await PlacesTestUtils.addVisits("https://other.mozilla.org/"); + } + const searchString = "mozilla.org"; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: searchString, + }); + Assert.ok(UrlbarTestUtils.getResultCount(window) > 1, "Sanity check results"); + let oneOffButtons = UrlbarTestUtils.getOneOffSearchButtons(window); + + let [ + actionHistory, + actionTabs, + actionBookmarks, + actionVisit, + ] = await document.l10n.formatValues([ + { id: "urlbar-result-action-search-history" }, + { id: "urlbar-result-action-search-tabs" }, + { id: "urlbar-result-action-search-bookmarks" }, + { id: "urlbar-result-action-visit" }, + ]); + + info("Alt UP to select the history one-off."); + EventUtils.synthesizeKey("KEY_ArrowUp", { altKey: true }); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 0, + "the heuristic result should be selected" + ); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + oneOffButtons.selectedButton.source, + UrlbarUtils.RESULT_SOURCE.HISTORY, + "The history one-off button should be selected" + ); + Assert.ok( + BrowserTestUtils.is_visible(result.element.action), + "The heuristic action should be visible" + ); + Assert.equal( + result.displayed.action, + actionHistory, + "Check the heuristic action" + ); + Assert.equal( + result.image, + "chrome://browser/skin/history.svg", + "Check the heuristic icon" + ); + let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0); + Assert.equal( + row.querySelector(".urlbarView-title").textContent, + searchString, + "Check that the result title has been replaced with the search string." + ); + + info("Alt UP to select the tabs one-off."); + EventUtils.synthesizeKey("KEY_ArrowUp", { altKey: true }); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + oneOffButtons.selectedButton.source, + UrlbarUtils.RESULT_SOURCE.TABS, + "The tabs one-off button should be selected" + ); + Assert.ok( + BrowserTestUtils.is_visible(result.element.action), + "The heuristic action should be visible" + ); + Assert.equal( + result.displayed.action, + actionTabs, + "Check the heuristic action" + ); + Assert.equal( + result.image, + "chrome://browser/skin/tab.svg", + "Check the heuristic icon" + ); + + info("Alt UP to select the bookmarks one-off."); + EventUtils.synthesizeKey("KEY_ArrowUp", { altKey: true }); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + oneOffButtons.selectedButton.source, + UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + "The bookmarks one-off button should be selected" + ); + Assert.ok( + BrowserTestUtils.is_visible(result.element.action), + "The heuristic action should be visible" + ); + Assert.equal( + result.displayed.action, + actionBookmarks, + "Check the heuristic action" + ); + Assert.equal( + result.image, + "chrome://browser/skin/bookmark.svg", + "Check the heuristic icon" + ); + + info( + "Select the next result, then reselect the heuristic, check it's a visit" + ); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 0, + "the heuristic result should be selected" + ); + EventUtils.synthesizeKey("KEY_ArrowDown", {}); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 1, + "the heuristic result should not be selected" + ); + EventUtils.synthesizeKey("KEY_ArrowUp", {}); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 0, + "the heuristic result should be selected" + ); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + oneOffButtons.selectedButton, + null, + "No one-off button should be selected" + ); + Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.URL); + Assert.equal( + result.displayed.action, + actionVisit, + "Check the heuristic action" + ); + Assert.notEqual( + result.image, + "chrome://browser/skin/history.svg", + "Check the heuristic icon" + ); + row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0); + Assert.equal( + row.querySelector(".urlbarView-title").textContent, + `https://${searchString}`, + "Check that the result title has been restored to the fixed-up URI." + ); + + await PlacesUtils.history.clear(); +}); + +add_task(async function localOneOff_suggestion() { + info("Type some text, select the first suggestion, then a local one-off"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "query", + }); + let count = UrlbarTestUtils.getResultCount(window); + Assert.ok(count > 1, "Sanity check results"); + let result = null; + let suggestionIndex = -1; + for (let i = 1; i < count; ++i) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + let index = await UrlbarTestUtils.getSelectedRowIndex(window); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, index); + if ( + result.type == UrlbarUtils.RESULT_TYPE.SEARCH && + result.searchParams.suggestion + ) { + suggestionIndex = i; + break; + } + } + Assert.ok( + result.searchParams.suggestion, + "Should have selected a search suggestion" + ); + Assert.ok( + result.displayed.action.includes(result.searchParams.engine), + "Check the search suggestion action" + ); + + info("Alt UP to select the last local one-off."); + EventUtils.synthesizeKey("KEY_ArrowUp", { altKey: true }); + Assert.equal( + await UrlbarTestUtils.getSelectedRowIndex(window), + suggestionIndex, + "the suggestion should still be selected" + ); + + let [actionHistory] = await document.l10n.formatValues([ + { id: "urlbar-result-action-search-history" }, + ]); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, suggestionIndex); + Assert.equal( + result.displayed.action, + actionHistory, + "Check the search suggestion action changed to local one-off" + ); + // Like in the normal engine one-offs case, we don't replace the favicon. + Assert.equal( + result.image, + "chrome://browser/skin/search-glass.svg", + "Check the suggestion icon" + ); + + info("DOWN to select the next suggestion"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + result = await UrlbarTestUtils.getDetailsOfResultAt( + window, + suggestionIndex + 1 + ); + Assert.ok( + result.searchParams.suggestion, + "Should have selected a search suggestion" + ); + Assert.ok( + result.displayed.action.includes(result.searchParams.engine), + "Check the search suggestion action" + ); + Assert.equal( + result.image, + "chrome://browser/skin/search-glass.svg", + "Check the suggestion icon" + ); + + info("UP back to the previous suggestion"); + EventUtils.synthesizeKey("KEY_ArrowUp"); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, suggestionIndex); + Assert.ok( + result.displayed.action.includes(result.searchParams.engine), + "Check the search suggestion action" + ); + Assert.equal( + result.image, + "chrome://browser/skin/search-glass.svg", + "Check the suggestion icon" + ); +}); + +add_task(async function localOneOff_shortcut() { + info("Select a search shortcut, then a local one-off"); + + await PlacesUtils.history.clear(); + // Enough vists to get this site into Top Sites. + for (let i = 0; i < 5; i++) { + await PlacesTestUtils.addVisits("http://example.com/"); + } + + await updateTopSites( + sites => sites && sites[0] && sites[0].searchTopSite, + true + ); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + let count = UrlbarTestUtils.getResultCount(window); + Assert.ok(count > 1, "Sanity check results"); + let result = null; + let shortcutIndex = -1; + for (let i = 0; i < count; ++i) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + let index = await UrlbarTestUtils.getSelectedRowIndex(window); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, index); + if ( + result.type == UrlbarUtils.RESULT_TYPE.SEARCH && + result.searchParams.keyword + ) { + shortcutIndex = i; + break; + } + } + Assert.ok(result.searchParams.keyword, "Should have selected a shortcut"); + let shortcutEngine = result.searchParams.engine; + + info("Alt UP to select the last local one-off."); + EventUtils.synthesizeKey("KEY_ArrowUp", { altKey: true }); + Assert.equal( + await UrlbarTestUtils.getSelectedRowIndex(window), + shortcutIndex, + "the shortcut should still be selected" + ); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, shortcutIndex); + Assert.equal( + result.displayed.action, + "", + "Check the shortcut action is empty" + ); + Assert.equal( + result.searchParams.engine, + shortcutEngine, + "Check the shortcut engine" + ); + Assert.ok( + result.displayed.title.includes(shortcutEngine), + "Check the shortcut title" + ); + Assert.notEqual( + result.image, + "chrome://browser/skin/search-glass.svg", + "Check the icon was not replaced" + ); + + await UrlbarTestUtils.exitSearchMode(window); + await PlacesUtils.history.clear(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_no_results.js b/browser/components/urlbar/tests/browser/browser_searchMode_no_results.js new file mode 100644 index 0000000000..96da1d9721 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_no_results.js @@ -0,0 +1,290 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests entering search mode and there are no results in the view. + */ + +"use strict"; + +const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml"; + +add_task(async function setup() { + // In order to open the view without any results, we need to be in search mode + // with an empty search string so that no heuristic result is shown, and the + // empty search must yield zero additional results. We'll enter search mode + // using the bookmarks one-off, and first we'll delete all bookmarks so that + // there are no results. + await PlacesUtils.bookmarks.eraseEverything(); + + // Also clear history so that using the alias of our test engine doesn't + // inadvertently return any history results due to bug 1658646. + await PlacesUtils.history.clear(); + + // Add a top site so we're guaranteed the view has at least one result to + // show initially with an empty search. Otherwise the view won't even open. + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "browser.newtabpage.activity-stream.default.sites", + "http://example.com/", + ], + ], + }); + await updateTopSites(sites => sites.length); +}); + +// Basic test for entering search mode with no results. +add_task(async function basic() { + await withNewWindow(async win => { + // Do an empty search. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "", + }); + + // Initially there should be at least the top site we added above. + Assert.greater( + UrlbarTestUtils.getResultCount(win), + 0, + "Top sites should be present initially" + ); + Assert.ok( + !win.gURLBar.panel.hasAttribute("noresults"), + "Panel has results, therefore should not have noresults attribute" + ); + + // Enter search mode by clicking the bookmarks one-off. + await UrlbarTestUtils.enterSearchMode(win, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + }); + + Assert.equal( + UrlbarTestUtils.getResultCount(win), + 0, + "Zero results since no bookmarks exist" + ); + Assert.equal( + win.gURLBar.panel.getAttribute("noresults"), + "true", + "Panel has no results, therefore should have noresults attribute" + ); + + // Exit search mode by backspacing. The top sites should be shown again. + await UrlbarTestUtils.exitSearchMode(win, { backspace: true }); + Assert.greater( + UrlbarTestUtils.getResultCount(win), + 0, + "Top sites should be present again" + ); + Assert.ok( + !win.gURLBar.panel.hasAttribute("noresults"), + "noresults attribute should be absent again" + ); + + await UrlbarTestUtils.promisePopupClose(win); + }); +}); + +// When the urlbar is in search mode, has no results, and is not focused, +// focusing it should auto-open the view. +add_task(async function autoOpen() { + await withNewWindow(async win => { + // Do an empty search. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "", + }); + + // Initially there should be at least the top site we added above. + Assert.greater( + UrlbarTestUtils.getResultCount(win), + 0, + "Top sites should be present initially" + ); + Assert.ok( + !win.gURLBar.panel.hasAttribute("noresults"), + "Panel has results, therefore should not have noresults attribute" + ); + + // Enter search mode by clicking the bookmarks one-off. + await UrlbarTestUtils.enterSearchMode(win, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + }); + Assert.equal( + UrlbarTestUtils.getResultCount(win), + 0, + "Zero results since no bookmarks exist" + ); + Assert.equal( + win.gURLBar.panel.getAttribute("noresults"), + "true", + "Panel has no results, therefore should have noresults attribute" + ); + + // Blur the urlbar. + win.gURLBar.blur(); + await UrlbarTestUtils.assertSearchMode(win, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + entry: "oneoff", + }); + + // Click the urlbar. + await UrlbarTestUtils.promisePopupOpen(win, () => { + EventUtils.synthesizeMouseAtCenter(win.gURLBar.inputField, {}, win); + }); + Assert.equal( + UrlbarTestUtils.getResultCount(win), + 0, + "Still zero results since no bookmarks exist" + ); + Assert.equal( + win.gURLBar.panel.getAttribute("noresults"), + "true", + "Panel still has no results, therefore should have noresults attribute" + ); + + // Exit search mode by backspacing. The top sites should be shown again. + await UrlbarTestUtils.exitSearchMode(win, { backspace: true }); + Assert.greater( + UrlbarTestUtils.getResultCount(win), + 0, + "Top sites should be present again" + ); + Assert.ok( + !win.gURLBar.panel.hasAttribute("noresults"), + "noresults attribute should be absent again" + ); + + await UrlbarTestUtils.promisePopupClose(win); + }); +}); + +// When the urlbar is in search mode, the user backspaces over the final char +// (but remains in search mode), and there are no results, the view should +// remain open. +add_task(async function backspaceRemainOpen() { + await withNewWindow(async win => { + // Do a one-char search. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "x", + }); + Assert.greater( + UrlbarTestUtils.getResultCount(win), + 0, + "At least the heuristic result should be shown" + ); + Assert.ok( + !win.gURLBar.panel.hasAttribute("noresults"), + "Panel has results, therefore should not have noresults attribute" + ); + + // Enter search mode by clicking the bookmarks one-off. + await UrlbarTestUtils.enterSearchMode(win, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + }); + + // The heursitic should not be shown since we don't show it in local search + // modes. + Assert.equal( + UrlbarTestUtils.getResultCount(win), + 0, + "No results should be present" + ); + Assert.ok( + win.gURLBar.panel.hasAttribute("noresults"), + "Panel has no results, therefore should have noresults attribute" + ); + + // Backspace. The search string will now be empty. + let searchPromise = UrlbarTestUtils.promiseSearchComplete(win); + EventUtils.synthesizeKey("KEY_Backspace", {}, win); + await searchPromise; + Assert.ok(UrlbarTestUtils.isPopupOpen(win), "View remains open"); + await UrlbarTestUtils.assertSearchMode(win, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + entry: "oneoff", + }); + Assert.equal( + UrlbarTestUtils.getResultCount(win), + 0, + "Zero results since no bookmarks exist" + ); + Assert.equal( + win.gURLBar.panel.getAttribute("noresults"), + "true", + "Panel has no results, therefore should have noresults attribute" + ); + + // Exit search mode by backspacing. The top sites should be shown. + await UrlbarTestUtils.exitSearchMode(win, { backspace: true }); + Assert.greater( + UrlbarTestUtils.getResultCount(win), + 0, + "Top sites should be present again" + ); + Assert.ok( + !win.gURLBar.panel.hasAttribute("noresults"), + "noresults attribute should be absent again" + ); + + await UrlbarTestUtils.promisePopupClose(win); + }); +}); + +// Types a search alias and then a space to enter search mode, with no results. +// The one-offs should be shown. +add_task(async function spaceToEnterSearchMode() { + let engine = await SearchTestUtils.promiseNewSearchEngine( + getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME + ); + engine.alias = "@test"; + + await withNewWindow(async win => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: engine.alias, + }); + + // We need to wait for two searches: The first enters search mode, the + // second does the search in search mode. + let searchPromise = UrlbarTestUtils.promiseSearchComplete(win); + EventUtils.synthesizeKey(" ", {}, win); + await searchPromise; + + Assert.equal(UrlbarTestUtils.getResultCount(win), 0, "Zero results"); + Assert.equal( + win.gURLBar.panel.getAttribute("noresults"), + "true", + "Panel has no results, therefore should have noresults attribute" + ); + await UrlbarTestUtils.assertSearchMode(win, { + engineName: engine.name, + entry: "typed", + }); + this.Assert.equal( + UrlbarTestUtils.getOneOffSearchButtonsVisible(window), + true, + "One-offs are visible" + ); + + await UrlbarTestUtils.exitSearchMode(win, { backspace: true }); + await UrlbarTestUtils.promisePopupClose(win); + }); +}); + +/** + * Opens a new window, waits for it to load, calls a callback, and closes the + * window. We use a new window in each task so that the view starts with a + * blank slate each time. + * + * @param {function} callback + * Will be called as: callback(newWindow) + */ +async function withNewWindow(callback) { + // Start in a new window so we have a blank slate. + let win = await BrowserTestUtils.openNewBrowserWindow(); + await callback(win); + await BrowserTestUtils.closeWindow(win); +} diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_oneOffButton.js b/browser/components/urlbar/tests/browser/browser_searchMode_oneOffButton.js new file mode 100644 index 0000000000..8ebf4152a0 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_oneOffButton.js @@ -0,0 +1,118 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests one-off search button behavior with search mode. + */ + +const TEST_ENGINE = { + name: "test engine", + details: { + alias: "@test", + template: "http://example.com/?search={searchTerms}", + }, +}; + +add_task(async function setup() { + const engine = await Services.search.addEngineWithDetails( + TEST_ENGINE.name, + TEST_ENGINE.details + ); + + registerCleanupFunction(async () => { + await Services.search.removeEngine(engine); + }); +}); + +add_task(async function test() { + info("Test no one-off buttons are selected when entering search mode"); + + info("Open the result popup"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + + info("Select one of one-off button"); + const oneOffs = UrlbarTestUtils.getOneOffSearchButtons(window); + await TestUtils.waitForCondition( + () => !oneOffs._rebuilding, + "Waiting for one-offs to finish rebuilding" + ); + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + ok(oneOffs.selectedButton, "There is a selected one-off button"); + + info("Enter search mode"); + await UrlbarTestUtils.enterSearchMode(window, { + engineName: TEST_ENGINE.name, + }); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: TEST_ENGINE.name, + entry: "oneoff", + }); + ok(!oneOffs.selectedButton, "There is no selected one-off button"); +}); + +add_task(async function() { + info( + "Test the status of the selected one-off button when exiting search mode with backspace" + ); + + info("Open the result popup"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + + info("Select one of one-off button"); + const oneOffs = UrlbarTestUtils.getOneOffSearchButtons(window); + await TestUtils.waitForCondition( + () => !oneOffs._rebuilding, + "Waiting for one-offs to finish rebuilding" + ); + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + ok(oneOffs.selectedButton, "There is a selected one-off button"); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: oneOffs.selectedButton.engine.name, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + entry: "oneoff", + isPreview: true, + }); + + info("Exit from search mode"); + await UrlbarTestUtils.exitSearchMode(window); + ok(!oneOffs.selectedButton, "There is no any selected one-off button"); +}); + +add_task(async function() { + info( + "Test the status of the selected one-off button when exiting search mode with clicking close button" + ); + + info("Open the result popup"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + + info("Select one of one-off button"); + const oneOffs = UrlbarTestUtils.getOneOffSearchButtons(window); + await TestUtils.waitForCondition( + () => !oneOffs._rebuilding, + "Waiting for one-offs to finish rebuilding" + ); + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + ok(oneOffs.selectedButton, "There is a selected one-off button"); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: oneOffs.selectedButton.engine.name, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + entry: "oneoff", + isPreview: true, + }); + + info("Exit from search mode"); + await UrlbarTestUtils.exitSearchMode(window, { clickClose: true }); + ok(!oneOffs.selectedButton, "There is no any selected one-off button"); +}); diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_pickResult.js b/browser/components/urlbar/tests/browser/browser_searchMode_pickResult.js new file mode 100644 index 0000000000..98416ee106 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_pickResult.js @@ -0,0 +1,89 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that search mode is exited after picking a result. + */ + +"use strict"; + +const BOOKMARK_URL = "http://www.example.com/browser_searchMode_pickResult.js"; + +add_task(async function setup() { + // Add a bookmark so we can enter bookmarks search mode and pick it. + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: BOOKMARK_URL, + }); + registerCleanupFunction(async () => { + await PlacesUtils.bookmarks.eraseEverything(); + }); +}); + +// Opens a new tab, enters search mode, does a search for our test bookmark, and +// picks it. Uses a variety of initial URLs and search strings in order to hit +// different branches in setURI. Search mode should be exited in all cases. +add_task(async function pickResult() { + for (let test of [ + // initialURL, searchString + ["about:blank", BOOKMARK_URL], + ["about:blank", new URL(BOOKMARK_URL).origin], + ["about:blank", new URL(BOOKMARK_URL).pathname], + [BOOKMARK_URL, BOOKMARK_URL], + [BOOKMARK_URL, new URL(BOOKMARK_URL).origin], + [BOOKMARK_URL, new URL(BOOKMARK_URL).pathname], + ]) { + await doPickResultTest(...test); + } +}); + +async function doPickResultTest(initialURL, searchString) { + info( + "doPickResultTest with args: " + + JSON.stringify({ + initialURL, + searchString, + }) + ); + + await BrowserTestUtils.withNewTab(initialURL, async () => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: searchString, + fireInputEvent: true, + }); + + await UrlbarTestUtils.enterSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + }); + + // Arrow down to the bookmark result. + let firstResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + if (!firstResult.heuristic) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + } + let foundResult = false; + for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) { + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + if (result.url == BOOKMARK_URL) { + foundResult = true; + break; + } + EventUtils.synthesizeKey("KEY_ArrowDown"); + } + Assert.ok(foundResult, "The bookmark result should have been found"); + + // Press enter. + let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + EventUtils.synthesizeKey("KEY_Enter"); + await loadPromise; + Assert.equal( + gBrowser.currentURI.spec, + BOOKMARK_URL, + "Should have loaded the bookmarked URL" + ); + + await UrlbarTestUtils.assertSearchMode(window, null); + }); +} diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_preview.js b/browser/components/urlbar/tests/browser/browser_searchMode_preview.js new file mode 100644 index 0000000000..d7531f6e7b --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_preview.js @@ -0,0 +1,494 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests search mode preview. + */ + +"use strict"; + +const TEST_ENGINE_NAME = "Test"; +const TEST_ENGINE_DOMAIN = "example.com"; + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [ + // TODO (Bug 1675558) - This should not be a requirement for the whole test. + ["browser.urlbar.update2.emptySearchBehavior", 2], + ], + }); + let testEngine = await Services.search.addEngineWithDetails( + TEST_ENGINE_NAME, + { + alias: "@test", + template: `http://${TEST_ENGINE_DOMAIN}/?search={searchTerms}`, + } + ); + + registerCleanupFunction(async () => { + await Services.search.removeEngine(testEngine); + await PlacesUtils.history.clear(); + }); +}); + +/** + * @param {Node} button + * A one-off button. + * @param {boolean} [isPreview] + * Whether the expected search mode should be a preview. Defaults to true. + * @returns {object} + * The search mode object expected when that one-off is selected. + */ +function getExpectedSearchMode(button, isPreview = true) { + let expectedSearchMode = { + entry: "oneoff", + isPreview, + }; + if (button.engine) { + expectedSearchMode.engineName = button.engine.name; + if (UrlbarUtils.WEB_ENGINE_NAMES.has(button.engine.name)) { + expectedSearchMode.source = UrlbarUtils.RESULT_SOURCE.SEARCH; + } + } else { + expectedSearchMode.source = button.source; + } + + return expectedSearchMode; +} + +// Tests that cycling through token alias engines enters search mode preview. +add_task(async function tokenAlias() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "@", + }); + + let result; + while (gURLBar.searchMode?.engineName != TEST_ENGINE_NAME) { + EventUtils.synthesizeKey("KEY_ArrowDown", {}, window); + let index = UrlbarTestUtils.getSelectedRowIndex(window); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, index); + let expectedSearchMode = { + engineName: result.searchParams.engine, + isPreview: true, + entry: "keywordoffer", + }; + if (UrlbarUtils.WEB_ENGINE_NAMES.has(result.searchParams.engine)) { + expectedSearchMode.source = UrlbarUtils.RESULT_SOURCE.SEARCH; + } + await UrlbarTestUtils.assertSearchMode(window, expectedSearchMode); + } + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey("KEY_Enter"); + await searchPromise; + // Test that we are in confirmed search mode. + await UrlbarTestUtils.assertSearchMode(window, { + engineName: result.searchParams.engine, + entry: "keywordoffer", + }); + await UrlbarTestUtils.exitSearchMode(window); +}); + +// Tests that starting to type a query exits search mode preview in favour of +// full search mode. +add_task(async function startTyping() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "@", + }); + while (gURLBar.searchMode?.engineName != TEST_ENGINE_NAME) { + EventUtils.synthesizeKey("KEY_ArrowDown", {}, window); + } + + await UrlbarTestUtils.assertSearchMode(window, { + engineName: TEST_ENGINE_NAME, + isPreview: true, + entry: "keywordoffer", + }); + + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey("M"); + await searchPromise; + await UrlbarTestUtils.assertSearchMode(window, { + engineName: TEST_ENGINE_NAME, + entry: "keywordoffer", + }); + await UrlbarTestUtils.exitSearchMode(window); +}); + +// Tests that highlighting a search shortcut Top Site enters search mode +// preview. +add_task(async function topSites() { + // Enable search shortcut Top Sites. + await PlacesUtils.history.clear(); + await updateTopSites( + sites => sites && sites[0] && sites[0].searchTopSite, + true + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + fireInputEvent: true, + }); + + // We previously verified that the first Top Site is a search shortcut. + EventUtils.synthesizeKey("KEY_ArrowDown", {}, window); + let searchTopSite = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: searchTopSite.searchParams.engine, + isPreview: true, + entry: "topsites_urlbar", + }); + await UrlbarTestUtils.exitSearchMode(window); +}); + +// Tests that search mode preview is exited when the view is closed. +add_task(async function closeView() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "@", + }); + + while (gURLBar.searchMode?.engineName != TEST_ENGINE_NAME) { + EventUtils.synthesizeKey("KEY_ArrowDown", {}, window); + } + await UrlbarTestUtils.assertSearchMode(window, { + engineName: TEST_ENGINE_NAME, + isPreview: true, + entry: "keywordoffer", + }); + + // We should close search mode when closing the view. + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); + await UrlbarTestUtils.assertSearchMode(window, null); + + // Check search mode isn't re-entered when re-opening the view. + await UrlbarTestUtils.promisePopupOpen(window, () => { + if (gURLBar.getAttribute("pageproxystate") == "invalid") { + gURLBar.handleRevert(); + } + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + await UrlbarTestUtils.assertSearchMode(window, null); + await UrlbarTestUtils.promisePopupClose(window); +}); + +// Tests that search more preview is exited when the user switches tabs. +add_task(async function tabSwitch() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "@", + }); + + while (gURLBar.searchMode?.engineName != TEST_ENGINE_NAME) { + EventUtils.synthesizeKey("KEY_ArrowDown", {}, window); + } + await UrlbarTestUtils.assertSearchMode(window, { + engineName: TEST_ENGINE_NAME, + isPreview: true, + entry: "keywordoffer", + }); + + // Open a new tab then switch back to the original tab. + let tab1 = gBrowser.selectedTab; + let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser); + await BrowserTestUtils.switchTab(gBrowser, tab1); + + await UrlbarTestUtils.assertSearchMode(window, null); + BrowserTestUtils.removeTab(tab2); +}); + +// Tests that search mode is previewed when the user down arrows through the +// one-offs. +add_task(async function oneOff_downArrow() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + let oneOffs = UrlbarTestUtils.getOneOffSearchButtons(window); + await TestUtils.waitForCondition( + () => !oneOffs._rebuilding, + "Waiting for one-offs to finish rebuilding" + ); + let resultCount = UrlbarTestUtils.getResultCount(window); + + // Key down through all results. + for (let i = 0; i < resultCount; i++) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + } + + // Key down again. The first one-off should be selected. + EventUtils.synthesizeKey("KEY_ArrowDown"); + + // Check for the one-off's search mode previews. + while (oneOffs.selectedButton != oneOffs.settingsButtonCompact) { + await UrlbarTestUtils.assertSearchMode( + window, + getExpectedSearchMode(oneOffs.selectedButton) + ); + EventUtils.synthesizeKey("KEY_ArrowDown"); + } + + // Check that selecting the search settings button leaves search mode preview. + Assert.equal( + oneOffs.selectedButton, + oneOffs.settingsButtonCompact, + "The settings button is selected." + ); + await UrlbarTestUtils.assertSearchMode(window, null); + + // Closing the view should also exit search mode preview. + await UrlbarTestUtils.promisePopupClose(window); + await UrlbarTestUtils.assertSearchMode(window, null); +}); + +// Tests that search mode is previewed when the user Alt+down arrows through the +// one-offs. This subtest also highlights a keywordoffer result (the first Top +// Site) before Alt+Arrowing to the one-offs. This checks that the search mode +// previews from keywordoffer results are overwritten by selected one-offs. +add_task(async function oneOff_alt_downArrow() { + // Add some visits to a URL so we have multiple Top Sites. + await PlacesUtils.history.clear(); + for (let i = 0; i < 5; i++) { + await PlacesTestUtils.addVisits("http://example.com/"); + } + await updateTopSites( + sites => + sites && + sites[0]?.searchTopSite && + sites[1]?.url == "http://example.com/", + true + ); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + let oneOffs = UrlbarTestUtils.getOneOffSearchButtons(window); + await TestUtils.waitForCondition( + () => !oneOffs._rebuilding, + "Waiting for one-offs to finish rebuilding" + ); + + // Key down to the first result and check that it enters search mode preview. + EventUtils.synthesizeKey("KEY_ArrowDown"); + let searchTopSite = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: searchTopSite.searchParams.engine, + isPreview: true, + entry: "topsites_urlbar", + }); + + // Alt+down. The first one-off should be selected. + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + // Check for the one-offs' search mode previews. + while (oneOffs.selectedButton) { + await UrlbarTestUtils.assertSearchMode( + window, + getExpectedSearchMode(oneOffs.selectedButton) + ); + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + } + + // Now key down without a modifier. We should move to the second result and + // have no search mode preview. + EventUtils.synthesizeKey("KEY_ArrowDown"); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 1, + "The second result is selected." + ); + await UrlbarTestUtils.assertSearchMode(window, null); + + // Arrow back up to the keywordoffer result and check for search mode preview. + EventUtils.synthesizeKey("KEY_ArrowUp"); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: searchTopSite.searchParams.engine, + isPreview: true, + entry: "topsites_urlbar", + }); + + await PlacesUtils.history.clear(); + await UrlbarTestUtils.promisePopupClose(window); + await UrlbarTestUtils.assertSearchMode(window, null); +}); + +// Tests that search mode is previewed when the user is in full search mode +// and down arrows through the one-offs. +add_task(async function fullSearchMode_oneOff_downArrow() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + let oneOffs = UrlbarTestUtils.getOneOffSearchButtons(window); + await TestUtils.waitForCondition( + () => !oneOffs._rebuilding, + "Waiting for one-offs to finish rebuilding" + ); + let oneOffButtons = oneOffs.getSelectableButtons(true); + + await UrlbarTestUtils.enterSearchMode(window); + let expectedSearchMode = getExpectedSearchMode(oneOffButtons[0], false); + // Sanity check: we are in the correct search mode. + await UrlbarTestUtils.assertSearchMode(window, expectedSearchMode); + + // Key down through all results. + let resultCount = UrlbarTestUtils.getResultCount(window); + for (let i = 0; i < resultCount; i++) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + // If the result is a shortcut, it will enter preview mode. + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + await UrlbarTestUtils.assertSearchMode( + window, + Object.assign(expectedSearchMode, { + isPreview: !!result.searchParams.keyword, + }) + ); + } + + // Key down again. The first one-off should be selected. + EventUtils.synthesizeKey("KEY_ArrowDown"); + // Check that we show the correct preview as we cycle through the one-offs. + while (oneOffs.selectedButton != oneOffs.settingsButtonCompact) { + await UrlbarTestUtils.assertSearchMode( + window, + getExpectedSearchMode(oneOffs.selectedButton, true) + ); + EventUtils.synthesizeKey("KEY_ArrowDown"); + } + + // We should still be in the same search mode after cycling through all the + // one-offs. + await UrlbarTestUtils.assertSearchMode(window, expectedSearchMode); + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); +}); + +// Tests that search mode is previewed when the user is in full search mode +// and alt+down arrows through the one-offs. This subtest also checks that +// exiting full search mode while alt+arrowing through the one-offs enters +// search mode preview for subsequent one-offs. +add_task(async function fullSearchMode_oneOff_alt_downArrow() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + let oneOffs = UrlbarTestUtils.getOneOffSearchButtons(window); + let oneOffButtons = oneOffs.getSelectableButtons(true); + await TestUtils.waitForCondition( + () => !oneOffs._rebuilding, + "Waiting for one-offs to finish rebuilding" + ); + + await UrlbarTestUtils.enterSearchMode(window); + let expectedSearchMode = getExpectedSearchMode(oneOffButtons[0], false); + await UrlbarTestUtils.assertSearchMode(window, expectedSearchMode); + + // Key down to the first result. + EventUtils.synthesizeKey("KEY_ArrowDown"); + + // Alt+down. The first one-off should be selected. + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + // Cycle through the first half of the one-offs and verify that search mode + // preview is entered. + Assert.greater( + oneOffButtons.length, + 1, + "Sanity check: We should have at least two one-offs." + ); + for (let i = 1; i < oneOffButtons.length / 2; i++) { + await UrlbarTestUtils.assertSearchMode( + window, + getExpectedSearchMode(oneOffs.selectedButton, true) + ); + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + } + // Now click out of search mode. + await UrlbarTestUtils.exitSearchMode(window, { clickClose: true }); + // Now check for the remaining one-offs' search mode previews. + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + while (oneOffs.selectedButton) { + await UrlbarTestUtils.assertSearchMode( + window, + getExpectedSearchMode(oneOffs.selectedButton, true) + ); + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + } + + await UrlbarTestUtils.promisePopupClose(window); + await UrlbarTestUtils.assertSearchMode(window, null); +}); + +// Tests that the original search mode is preserved when going through some +// one-off buttons and then back down in the results list. +add_task(async function fullSearchMode_oneOff_restore_on_down() { + info("Add a few visits to top sites"); + for (let i = 0; i < 5; i++) { + await PlacesTestUtils.addVisits([ + "http://1.example.com/", + "http://2.example.com/", + "http://3.example.com/", + ]); + } + await updateTopSites(sites => sites?.length > 2, false); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + let oneOffs = UrlbarTestUtils.getOneOffSearchButtons(window); + let oneOffButtons = oneOffs.getSelectableButtons(true); + await TestUtils.waitForCondition( + () => !oneOffs._rebuilding, + "Waiting for one-offs to finish rebuilding" + ); + + await UrlbarTestUtils.enterSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + }); + let expectedSearchMode = getExpectedSearchMode( + oneOffButtons.find(b => b.source == UrlbarUtils.RESULT_SOURCE.HISTORY), + false + ); + await UrlbarTestUtils.assertSearchMode(window, expectedSearchMode); + info("Down to the first result"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + await UrlbarTestUtils.assertSearchMode(window, expectedSearchMode); + info("Alt+down to the first one-off."); + Assert.greater( + oneOffButtons.length, + 1, + "Sanity check: We should have at least two one-offs." + ); + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + await UrlbarTestUtils.assertSearchMode( + window, + getExpectedSearchMode(oneOffs.selectedButton, true) + ); + info("Go again down through the list of results"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + await UrlbarTestUtils.assertSearchMode(window, expectedSearchMode); + + // Now do a similar test without initial search mode. + info("Exit search mode."); + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + info("Down to the first result"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + await UrlbarTestUtils.assertSearchMode(window, null); + info("select a one-off to start preview"); + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + await UrlbarTestUtils.assertSearchMode( + window, + getExpectedSearchMode(oneOffs.selectedButton, true) + ); + info("Go again through the list of results"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + await UrlbarTestUtils.assertSearchMode(window, null); + + await UrlbarTestUtils.promisePopupClose(window); + await PlacesUtils.history.clear(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_sessionStore.js b/browser/components/urlbar/tests/browser/browser_searchMode_sessionStore.js new file mode 100644 index 0000000000..fb132eff36 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_sessionStore.js @@ -0,0 +1,313 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests search mode and session store. Also tests that search mode is + * duplicated when duplicating tabs, since tab duplication is handled by session + * store. + */ + +"use strict"; + +XPCOMUtils.defineLazyModuleGetters(this, { + SessionStore: "resource:///modules/sessionstore/SessionStore.jsm", + TabStateFlusher: "resource:///modules/sessionstore/TabStateFlusher.jsm", +}); + +// This test takes a long time on the OS X 10.14 machines, so request a longer +// timeout. See bug 1671045. This may also fix a different failure on Linux in +// bug 1671087, but it's not clear. Regardless, a longer timeout won't hurt. +requestLongerTimeout(5); + +const SEARCH_STRING = "test browser_sessionStore.js"; +const URL = "http://example.com/"; + +// A URL in gInitialPages. We test this separately since SessionStore sometimes +// takes different paths for these URLs. +const INITIAL_URL = "about:newtab"; + +// The following tasks make sure non-null search mode is restored. + +add_task(async function initialPageOnRestore() { + await doTest({ + urls: [INITIAL_URL], + searchModeTabIndex: 0, + exitSearchMode: false, + switchTabsAfterEnteringSearchMode: false, + }); +}); + +add_task(async function switchToInitialPage() { + await doTest({ + urls: [URL, INITIAL_URL], + searchModeTabIndex: 1, + exitSearchMode: false, + switchTabsAfterEnteringSearchMode: true, + }); +}); + +add_task(async function nonInitialPageOnRestore() { + await doTest({ + urls: [URL], + searchModeTabIndex: 0, + exitSearchMode: false, + switchTabsAfterEnteringSearchMode: false, + }); +}); + +add_task(async function switchToNonInitialPage() { + await doTest({ + urls: [INITIAL_URL, URL], + searchModeTabIndex: 1, + exitSearchMode: false, + switchTabsAfterEnteringSearchMode: true, + }); +}); + +// The following tasks enter and then exit search mode to make sure that no +// search mode is restored. + +add_task(async function initialPageOnRestore_exit() { + await doTest({ + urls: [INITIAL_URL], + searchModeTabIndex: 0, + exitSearchMode: true, + switchTabsAfterEnteringSearchMode: false, + }); +}); + +add_task(async function switchToInitialPage_exit() { + await doTest({ + urls: [URL, INITIAL_URL], + searchModeTabIndex: 1, + exitSearchMode: true, + switchTabsAfterEnteringSearchMode: true, + }); +}); + +add_task(async function nonInitialPageOnRestore_exit() { + await doTest({ + urls: [URL], + searchModeTabIndex: 0, + exitSearchMode: true, + switchTabsAfterEnteringSearchMode: false, + }); +}); + +add_task(async function switchToNonInitialPage_exit() { + await doTest({ + urls: [INITIAL_URL, URL], + searchModeTabIndex: 1, + exitSearchMode: true, + switchTabsAfterEnteringSearchMode: true, + }); +}); + +/** + * The main test function. Opens some URLs in a new window, enters search mode + * in one of the tabs, closes the window, restores it, and makes sure that + * search mode is restored properly. + * + * @param {array} urls + * Array of string URLs to open. + * @param {number} searchModeTabIndex + * The index of the tab in which to enter search mode. + * @param {boolean} exitSearchMode + * If true, search mode will be immediately exited after entering it. Use + * this to make sure search mode is *not* restored after it's exited. + * @param {boolean} switchTabsAfterEnteringSearchMode + * If true, we'll switch to a tab other than the one that search mode was + * entered in before closing the window. `urls` should contain more than one + * URL in this case. + */ +async function doTest({ + urls, + searchModeTabIndex, + exitSearchMode, + switchTabsAfterEnteringSearchMode, +}) { + let searchModeURL = urls[searchModeTabIndex]; + let otherTabIndex = (searchModeTabIndex + 1) % urls.length; + let otherURL = urls[otherTabIndex]; + + await withNewWindow(urls, async win => { + if (win.gBrowser.selectedTab != win.gBrowser.tabs[searchModeTabIndex]) { + await BrowserTestUtils.switchTab( + win.gBrowser, + win.gBrowser.tabs[searchModeTabIndex] + ); + } + + Assert.equal( + win.gBrowser.currentURI.spec, + searchModeURL, + `Sanity check: Tab at index ${searchModeTabIndex} is correct` + ); + Assert.equal( + searchModeURL == INITIAL_URL, + win.gInitialPages.includes(win.gBrowser.currentURI.spec), + `Sanity check: ${searchModeURL} is or is not in gInitialPages as expected` + ); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: SEARCH_STRING, + fireInputEvent: true, + }); + await UrlbarTestUtils.enterSearchMode(win, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + }); + + if (exitSearchMode) { + await UrlbarTestUtils.exitSearchMode(win); + } + + // Make sure session store is updated. + await TabStateFlusher.flush(win.gBrowser.selectedBrowser); + + if (switchTabsAfterEnteringSearchMode) { + await BrowserTestUtils.switchTab( + win.gBrowser, + win.gBrowser.tabs[otherTabIndex] + ); + } + }); + + let restoredURL = switchTabsAfterEnteringSearchMode + ? otherURL + : searchModeURL; + + let win = await restoreWindow(restoredURL); + + Assert.equal( + win.gBrowser.currentURI.spec, + restoredURL, + "Sanity check: Initially selected tab in restored window is correct" + ); + + if (switchTabsAfterEnteringSearchMode) { + // Switch back to the tab with search mode. + await BrowserTestUtils.switchTab( + win.gBrowser, + win.gBrowser.tabs[searchModeTabIndex] + ); + } + + if (exitSearchMode) { + // If we exited search mode, it should be null. + await new Promise(r => win.setTimeout(r, 500)); + await UrlbarTestUtils.assertSearchMode(win, null); + } else { + // If we didn't exit search mode, it should be restored. + await TestUtils.waitForCondition( + () => win.gURLBar.searchMode, + "Waiting for search mode to be restored" + ); + await UrlbarTestUtils.assertSearchMode(win, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + entry: "oneoff", + }); + Assert.equal( + win.gURLBar.value, + SEARCH_STRING, + "Search string should be restored" + ); + } + + await BrowserTestUtils.closeWindow(win); +} + +// Tests that search mode is duplicated when duplicating tabs. Note that tab +// duplication is handled by session store. +add_task(async function duplicateTabs() { + // Enter search mode with a search string in the current tab. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: SEARCH_STRING, + fireInputEvent: true, + }); + await UrlbarTestUtils.enterSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + }); + + // Now duplicate the current tab using the context menu item. First we need + // to set TabContextMenu.contextTab because that's how the menu item's command + // determines which tab to duplicate. + window.TabContextMenu.contextTab = gBrowser.selectedTab; + let tabPromise = BrowserTestUtils.waitForNewTab( + gBrowser, + gBrowser.currentURI.spec + ); + let menuitem = document.getElementById("context_duplicateTab"); + menuitem.click(); + let newTab = await tabPromise; + Assert.equal( + gBrowser.selectedTab, + newTab, + "Sanity check: The duplicated tab is now the selected tab" + ); + + // Wait for search mode, then check it and the input value. + await TestUtils.waitForCondition( + () => gURLBar.searchMode, + "Waiting for search mode to be duplicated/restored" + ); + await UrlbarTestUtils.assertSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + entry: "oneoff", + }); + Assert.equal( + gURLBar.value, + SEARCH_STRING, + "Search string should be duplicated/restored" + ); + + BrowserTestUtils.removeTab(newTab); + gURLBar.handleRevert(); +}); + +/** + * Opens a new browser window with the given URLs, calls a callback, and then + * closes the window. + * + * @param {array} urls + * Array of string URLs to open. + * @param {function} callback + * The callback. + */ +async function withNewWindow(urls, callback) { + let win = await BrowserTestUtils.openNewBrowserWindow(); + for (let url of urls) { + await BrowserTestUtils.openNewForegroundTab({ + url, + gBrowser: win.gBrowser, + waitForLoad: url != "about:newtab", + }); + if (url == "about:newtab") { + await TestUtils.waitForCondition( + () => win.gBrowser.currentURI.spec == "about:newtab", + "Waiting for about:newtab" + ); + } + } + BrowserTestUtils.removeTab(win.gBrowser.tabs[0]); + await callback(win); + await BrowserTestUtils.closeWindow(win); +} + +/** + * Uses SessionStore to reopen the last closed window. + * + * @param {string} expectedRestoredURL + * The URL you expect will be restored in the selected browser. + */ +async function restoreWindow(expectedRestoredURL) { + let winPromise = BrowserTestUtils.waitForNewWindow(); + let win = SessionStore.undoCloseWindow(0); + await winPromise; + await TestUtils.waitForCondition( + () => win.gBrowser.currentURI.spec == expectedRestoredURL, + "Waiting for restored selected browser to have expected URI" + ); + return win; +} diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_setURI.js b/browser/components/urlbar/tests/browser/browser_searchMode_setURI.js new file mode 100644 index 0000000000..46f0a84256 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_setURI.js @@ -0,0 +1,119 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that search mode remains active or is exited when setURI is called, + * depending on the situation. + */ + +"use strict"; + +// Opens a new tab, does a search, enters search mode, and then manually calls +// setURI. Uses a variety of initial URLs, search strings, and setURI arguments +// in order to hit different branches in setURI. Search mode should remain +// active or be exited as appropriate. +add_task(async function setURI() { + for (let test of [ + // initialURL, searchString, url, expectSearchMode + + ["about:blank", "", null, true], + ["about:blank", "", "about:blank", true], + ["about:blank", "", "http://www.example.com/", true], + + ["about:blank", "about:blank", null, false], + ["about:blank", "about:blank", "about:blank", false], + ["about:blank", "about:blank", "http://www.example.com/", false], + + ["about:blank", "http://www.example.com/", null, true], + ["about:blank", "http://www.example.com/", "about:blank", true], + ["about:blank", "http://www.example.com/", "http://www.example.com/", true], + + ["about:blank", "not a URL", null, true], + ["about:blank", "not a URL", "about:blank", true], + ["about:blank", "not a URL", "http://www.example.com/", true], + + ["http://www.example.com/", "", null, true], + ["http://www.example.com/", "", "about:blank", true], + ["http://www.example.com/", "", "http://www.example.com/", true], + + ["http://www.example.com/", "about:blank", null, false], + ["http://www.example.com/", "about:blank", "about:blank", false], + [ + "http://www.example.com/", + "about:blank", + "http://www.example.com/", + false, + ], + + ["http://www.example.com/", "http://www.example.com/", null, true], + ["http://www.example.com/", "http://www.example.com/", "about:blank", true], + [ + "http://www.example.com/", + "http://www.example.com/", + "http://www.example.com/", + true, + ], + + ["http://www.example.com/", "not a URL", null, true], + ["http://www.example.com/", "not a URL", "about:blank", true], + ["http://www.example.com/", "not a URL", "http://www.example.com/", true], + ]) { + await doSetURITest(...test); + } +}); + +async function doSetURITest(initialURL, searchString, url, expectSearchMode) { + info( + "doSetURITest with args: " + + JSON.stringify({ + initialURL, + searchString, + url, + expectSearchMode, + }) + ); + + await BrowserTestUtils.withNewTab(initialURL, async () => { + if (searchString) { + // Do a search with the search string. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: searchString, + fireInputEvent: true, + }); + } else { + // Open top sites. + await UrlbarTestUtils.promisePopupOpen(window, () => { + document.getElementById("Browser:OpenLocation").doCommand(); + }); + } + + // Enter search mode and close the view. + await UrlbarTestUtils.enterSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + }); + await UrlbarTestUtils.promisePopupClose(window); + Assert.strictEqual( + gBrowser.selectedBrowser.userTypedValue, + searchString, + `userTypedValue should be ${searchString}` + ); + + // Call setURI. + let uri = url ? Services.io.newURI(url) : null; + gURLBar.setURI(uri); + + await UrlbarTestUtils.assertSearchMode( + window, + !expectSearchMode + ? null + : { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + entry: "oneoff", + } + ); + + gURLBar.handleRevert(); + await UrlbarTestUtils.assertSearchMode(window, null); + }); +} diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_suggestions.js b/browser/components/urlbar/tests/browser/browser_searchMode_suggestions.js new file mode 100644 index 0000000000..aef08b7c91 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_suggestions.js @@ -0,0 +1,585 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests search suggestions in search mode. + */ + +const DEFAULT_ENGINE_NAME = "Test"; +const SUGGESTIONS_ENGINE_NAME = "searchSuggestionEngine.xml"; +const MANY_SUGGESTIONS_ENGINE_NAME = "searchSuggestionEngineMany.xml"; +const MAX_HISTORICAL_SEARCH_SUGGESTIONS = UrlbarPrefs.get( + "maxHistoricalSearchSuggestions" +); + +let suggestionsEngine; +let defaultEngine; +let expectedFormHistoryResults = []; + +add_task(async function setup() { + suggestionsEngine = await SearchTestUtils.promiseNewSearchEngine( + getRootDirectory(gTestPath) + SUGGESTIONS_ENGINE_NAME + ); + + let oldDefaultEngine = await Services.search.getDefault(); + defaultEngine = await Services.search.addEngineWithDetails( + DEFAULT_ENGINE_NAME, + { + template: "http://example.com/?search={searchTerms}", + } + ); + await Services.search.setDefault(defaultEngine); + await Services.search.moveEngine(suggestionsEngine, 0); + + async function cleanup() { + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + } + await cleanup(); + registerCleanupFunction(cleanup); + + // Add some form history for our test engine. Add more than + // maxHistoricalSearchSuggestions so we can verify that excess form history is + // added after remote suggestions. + for (let i = 0; i < MAX_HISTORICAL_SEARCH_SUGGESTIONS + 1; i++) { + let value = `hello formHistory ${i}`; + await UrlbarTestUtils.formHistory.add([ + { value, source: suggestionsEngine.name }, + ]); + expectedFormHistoryResults.push({ + heuristic: false, + type: UrlbarUtils.RESULT_TYPE.SEARCH, + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + searchParams: { + suggestion: value, + engine: suggestionsEngine.name, + }, + }); + } + + // Add other form history. + await UrlbarTestUtils.formHistory.add([ + { value: "hello formHistory global" }, + { value: "hello formHistory other", source: "other engine" }, + ]); + + registerCleanupFunction(async () => { + await Services.search.setDefault(oldDefaultEngine); + await Services.search.removeEngine(defaultEngine); + await UrlbarTestUtils.formHistory.clear(); + }); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.search.separatePrivateDefault.ui.enabled", false]], + }); +}); + +add_task(async function emptySearch() { + await BrowserTestUtils.withNewTab("about:robots", async function(browser) { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.update2.emptySearchBehavior", 2]], + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + await UrlbarTestUtils.enterSearchMode(window); + Assert.equal(gURLBar.value, "", "Urlbar value should be cleared."); + // For the empty search case, we expect to get the form history relative to + // the picked engine and no heuristic. + await checkResults(expectedFormHistoryResults); + await UrlbarTestUtils.exitSearchMode(window, { clickClose: true }); + await UrlbarTestUtils.promisePopupClose(window); + await SpecialPowers.popPrefEnv(); + }); +}); + +add_task(async function emptySearch_withRestyledHistory() { + // URLs with the same host as the search engine. + await PlacesTestUtils.addVisits([ + `http://mochi.test/`, + `http://mochi.test/redirect`, + // Should not be returned because it's a redirect target. + { + uri: `http://mochi.test/target`, + transition: PlacesUtils.history.TRANSITIONS.REDIRECT_TEMPORARY, + referrer: `http://mochi.test/redirect`, + }, + // Can be restyled and dupes form history. + "http://mochi.test:8888/?terms=hello+formHistory+0", + // Can be restyled but does not dupe form history. + "http://mochi.test:8888/?terms=ciao", + ]); + await BrowserTestUtils.withNewTab("about:robots", async function(browser) { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.update2.emptySearchBehavior", 2]], + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + await UrlbarTestUtils.enterSearchMode(window); + Assert.equal(gURLBar.value, "", "Urlbar value should be cleared."); + // For the empty search case, we expect to get the form history relative to + // the picked engine, history without redirects, and no heuristic. + await checkResults([ + { + heuristic: false, + type: UrlbarUtils.RESULT_TYPE.SEARCH, + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + searchParams: { + suggestion: "ciao", + engine: suggestionsEngine.name, + }, + }, + ...expectedFormHistoryResults, + { + heuristic: false, + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + url: `http://mochi.test/redirect`, + }, + { + heuristic: false, + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + url: `http://mochi.test/`, + }, + ]); + + await UrlbarTestUtils.exitSearchMode(window, { clickClose: true }); + await UrlbarTestUtils.promisePopupClose(window); + await SpecialPowers.popPrefEnv(); + }); + + await PlacesUtils.history.clear(); +}); + +add_task(async function emptySearch_withRestyledHistory_noSearchHistory() { + // URLs with the same host as the search engine. + await PlacesTestUtils.addVisits([ + `http://mochi.test/`, + `http://mochi.test/redirect`, + // Can be restyled but does not dupe form history. + "http://mochi.test:8888/?terms=ciao", + ]); + await BrowserTestUtils.withNewTab("about:robots", async function(browser) { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.update2.emptySearchBehavior", 2], + ["browser.urlbar.maxHistoricalSearchSuggestions", 0], + ], + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + await UrlbarTestUtils.enterSearchMode(window); + Assert.equal(gURLBar.value, "", "Urlbar value should be cleared."); + // For the empty search case, we expect to get the form history relative to + // the picked engine, history without redirects, and no heuristic. + await checkResults([ + { + heuristic: false, + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + url: `http://mochi.test/redirect`, + }, + { + heuristic: false, + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + url: `http://mochi.test/`, + }, + ]); + + await UrlbarTestUtils.exitSearchMode(window, { clickClose: true }); + await UrlbarTestUtils.promisePopupClose(window); + await SpecialPowers.popPrefEnv(); + }); + + await PlacesUtils.history.clear(); +}); + +add_task(async function emptySearch_behavior() { + // URLs with the same host as the search engine. + await PlacesTestUtils.addVisits([`http://mochi.test/`]); + + await BrowserTestUtils.withNewTab("about:robots", async function(browser) { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.update2.emptySearchBehavior", 0]], + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + await UrlbarTestUtils.enterSearchMode(window); + Assert.equal(gURLBar.value, "", "Urlbar value should be cleared."); + // For the empty search case, we expect to get the form history relative to + // the picked engine, history without redirects, and no heuristic. + await checkResults([]); + await UrlbarTestUtils.exitSearchMode(window, { clickClose: true }); + + // We should still show history for empty searches when not in search mode. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: " ", + }); + await checkResults([ + { + heuristic: true, + type: UrlbarUtils.RESULT_TYPE.SEARCH, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + searchParams: { + query: " ", + engine: defaultEngine.name, + }, + }, + { + heuristic: false, + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + url: `http://mochi.test/`, + }, + ]); + await UrlbarTestUtils.promisePopupClose(window); + await SpecialPowers.popPrefEnv(); + }); + + await BrowserTestUtils.withNewTab("about:robots", async function(browser) { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.update2.emptySearchBehavior", 1]], + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + await UrlbarTestUtils.enterSearchMode(window); + Assert.equal(gURLBar.value, "", "Urlbar value should be cleared."); + // For the empty search case, we expect to get the form history relative to + // the picked engine, history without redirects, and no heuristic. + await checkResults([...expectedFormHistoryResults]); + await UrlbarTestUtils.exitSearchMode(window, { clickClose: true }); + await UrlbarTestUtils.promisePopupClose(window); + await SpecialPowers.popPrefEnv(); + }); + + await PlacesUtils.history.clear(); +}); + +add_task(async function emptySearch_local() { + await PlacesTestUtils.addVisits([`http://mochi.test/`]); + + await BrowserTestUtils.withNewTab("about:robots", async function(browser) { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.update2.emptySearchBehavior", 0]], + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + await UrlbarTestUtils.enterSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + }); + Assert.equal(gURLBar.value, "", "Urlbar value should be cleared."); + // Even when emptySearchBehavior is 0, we still show the user's most frecent + // history for an empty search. + await checkResults([ + { + heuristic: false, + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + url: `http://mochi.test/`, + }, + ]); + await UrlbarTestUtils.exitSearchMode(window, { clickClose: true }); + await UrlbarTestUtils.promisePopupClose(window); + await SpecialPowers.popPrefEnv(); + }); + + await PlacesUtils.history.clear(); +}); + +add_task(async function nonEmptySearch() { + await BrowserTestUtils.withNewTab("about:robots", async function(browser) { + let query = "hello"; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: query, + }); + await UrlbarTestUtils.enterSearchMode(window); + Assert.equal(gURLBar.value, query, "Urlbar value should be set."); + // We expect to get the heuristic and all the suggestions. + await checkResults([ + { + heuristic: true, + type: UrlbarUtils.RESULT_TYPE.SEARCH, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + searchParams: { + query, + engine: suggestionsEngine.name, + }, + }, + ...expectedFormHistoryResults.slice(0, 2), + { + heuristic: false, + type: UrlbarUtils.RESULT_TYPE.SEARCH, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + searchParams: { + query, + suggestion: `${query}foo`, + engine: suggestionsEngine.name, + }, + }, + { + heuristic: false, + type: UrlbarUtils.RESULT_TYPE.SEARCH, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + searchParams: { + query, + suggestion: `${query}bar`, + engine: suggestionsEngine.name, + }, + }, + ...expectedFormHistoryResults.slice(2, 4), + ]); + + await UrlbarTestUtils.exitSearchMode(window, { clickClose: true }); + await UrlbarTestUtils.promisePopupClose(window); + }); +}); + +add_task(async function nonEmptySearch_nonMatching() { + await BrowserTestUtils.withNewTab("about:robots", async function(browser) { + let query = "ciao"; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: query, + }); + await UrlbarTestUtils.enterSearchMode(window); + Assert.equal(gURLBar.value, query, "Urlbar value should be set."); + // We expect to get the heuristic and the remote suggestions since the local + // ones don't match. + await checkResults([ + { + heuristic: true, + type: UrlbarUtils.RESULT_TYPE.SEARCH, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + searchParams: { + query, + engine: suggestionsEngine.name, + }, + }, + { + heuristic: false, + type: UrlbarUtils.RESULT_TYPE.SEARCH, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + searchParams: { + query, + suggestion: `${query}foo`, + engine: suggestionsEngine.name, + }, + }, + { + heuristic: false, + type: UrlbarUtils.RESULT_TYPE.SEARCH, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + searchParams: { + query, + suggestion: `${query}bar`, + engine: suggestionsEngine.name, + }, + }, + ]); + + await UrlbarTestUtils.exitSearchMode(window, { clickClose: true }); + await UrlbarTestUtils.promisePopupClose(window); + }); +}); + +add_task(async function nonEmptySearch_withHistory() { + let manySuggestionsEngine = await SearchTestUtils.promiseNewSearchEngine( + getRootDirectory(gTestPath) + MANY_SUGGESTIONS_ENGINE_NAME + ); + // URLs with the same host as the search engine. + let query = "ciao"; + await PlacesTestUtils.addVisits([ + `http://mochi.test/${query}`, + `http://mochi.test/${query}1`, + // Should not be returned because it has a different host, even if it + // matches the host in the path. + `http://example.com/mochi.test/${query}`, + ]); + + function makeSuggestionResult(suffix) { + return { + heuristic: false, + type: UrlbarUtils.RESULT_TYPE.SEARCH, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + searchParams: { + query, + suggestion: `${query}${suffix}`, + engine: manySuggestionsEngine.name, + }, + }; + } + + await BrowserTestUtils.withNewTab("about:robots", async function(browser) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: query, + }); + await UrlbarTestUtils.enterSearchMode(window, { + engineName: manySuggestionsEngine.name, + }); + Assert.equal(gURLBar.value, query, "Urlbar value should be set."); + + await checkResults([ + { + heuristic: true, + type: UrlbarUtils.RESULT_TYPE.SEARCH, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + searchParams: { + query, + engine: manySuggestionsEngine.name, + }, + }, + makeSuggestionResult("foo"), + makeSuggestionResult("bar"), + makeSuggestionResult("1"), + makeSuggestionResult("2"), + { + heuristic: false, + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + url: `http://mochi.test/${query}1`, + }, + { + heuristic: false, + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + url: `http://mochi.test/${query}`, + }, + makeSuggestionResult("3"), + makeSuggestionResult("4"), + makeSuggestionResult("5"), + ]); + + await UrlbarTestUtils.exitSearchMode(window, { clickClose: true }); + await UrlbarTestUtils.promisePopupClose(window); + + info("Test again with history before suggestions"); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.matchBuckets", "general:5,suggestion:Infinity"]], + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: query, + }); + await UrlbarTestUtils.enterSearchMode(window, { + engineName: manySuggestionsEngine.name, + }); + Assert.equal(gURLBar.value, query, "Urlbar value should be set."); + + await checkResults([ + { + heuristic: true, + type: UrlbarUtils.RESULT_TYPE.SEARCH, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + searchParams: { + query, + engine: manySuggestionsEngine.name, + }, + }, + makeSuggestionResult("foo"), + makeSuggestionResult("bar"), + makeSuggestionResult("1"), + makeSuggestionResult("2"), + makeSuggestionResult("3"), + makeSuggestionResult("4"), + makeSuggestionResult("5"), + { + heuristic: false, + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + url: `http://mochi.test/${query}1`, + }, + { + heuristic: false, + type: UrlbarUtils.RESULT_TYPE.URL, + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + url: `http://mochi.test/${query}`, + }, + ]); + + await UrlbarTestUtils.exitSearchMode(window, { clickClose: true }); + await UrlbarTestUtils.promisePopupClose(window); + await SpecialPowers.popPrefEnv(); + }); + + await PlacesUtils.history.clear(); +}); + +add_task(async function nonEmptySearch_url() { + await BrowserTestUtils.withNewTab("about:robots", async function(browser) { + let query = "http://www.example.com/"; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: query, + }); + await UrlbarTestUtils.enterSearchMode(window); + + // The heuristic result for a search that's a valid URL should be a search + // result, not a URL result. + await checkResults([ + { + heuristic: true, + type: UrlbarUtils.RESULT_TYPE.SEARCH, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + searchParams: { + query, + engine: suggestionsEngine.name, + }, + }, + ]); + + await UrlbarTestUtils.exitSearchMode(window, { clickClose: true }); + await UrlbarTestUtils.promisePopupClose(window); + }); +}); + +async function checkResults(expectedResults) { + Assert.equal( + UrlbarTestUtils.getResultCount(window), + expectedResults.length, + "Check results count." + ); + for (let i = 0; i < expectedResults.length; ++i) { + info(`Checking result at index ${i}`); + let expected = expectedResults[i]; + let actual = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + + // Check each property defined in the expected result against the property + // in the actual result. + for (let key of Object.keys(expected)) { + // For searchParams, remove undefined properties in the actual result so + // that the expected result doesn't need to include them. + if (key == "searchParams") { + let actualSearchParams = actual.searchParams; + for (let spKey of Object.keys(actualSearchParams)) { + if (actualSearchParams[spKey] === undefined) { + delete actualSearchParams[spKey]; + } + } + } + Assert.deepEqual( + actual[key], + expected[key], + `${key} should match at result index ${i}.` + ); + } + } +} diff --git a/browser/components/urlbar/tests/browser/browser_searchMode_switchTabs.js b/browser/components/urlbar/tests/browser/browser_searchMode_switchTabs.js new file mode 100644 index 0000000000..7a32bbc10c --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchMode_switchTabs.js @@ -0,0 +1,305 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that search mode is stored per tab and restored when switching tabs. + */ + +"use strict"; + +// Enters search mode using the one-off buttons. +add_task(async function switchTabs() { + // Open three tabs. We'll enter search mode in tabs 0 and 2. + let tabs = []; + for (let i = 0; i < 3; i++) { + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: "http://example.com/" + i, + }); + tabs.push(tab); + } + + // Switch to tab 0. + await BrowserTestUtils.switchTab(gBrowser, tabs[0]); + + // Do a search and enter search mode. Pass fireInputEvent so that + // userTypedValue is set and restored when we switch back to this tab. This + // isn't really necessary but it simulates the user's typing, and it also + // means that we'll start a search when we switch back to this tab. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + fireInputEvent: true, + }); + await UrlbarTestUtils.enterSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + }); + + // Switch to tab 1. Search mode should be exited. + await BrowserTestUtils.switchTab(gBrowser, tabs[1]); + await UrlbarTestUtils.assertSearchMode(window, null); + + // Switch back to tab 0. We should do a search (for "test") and re-enter + // search mode. + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + await BrowserTestUtils.switchTab(gBrowser, tabs[0]); + await searchPromise; + await UrlbarTestUtils.assertSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + entry: "oneoff", + }); + Assert.equal( + gURLBar.value, + "test", + "Value should remain the search string after switching back" + ); + + // Switch to tab 2. Search mode should be exited. + await BrowserTestUtils.switchTab(gBrowser, tabs[2]); + await UrlbarTestUtils.assertSearchMode(window, null); + + // Do another search (in tab 2) and enter search mode. Use a different source + // from tab 0 just to use something different. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test tab 2", + fireInputEvent: true, + }); + await UrlbarTestUtils.enterSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.TABS, + }); + + // Switch back to tab 0. We should do a search and still be in search mode. + searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + await BrowserTestUtils.switchTab(gBrowser, tabs[0]); + await searchPromise; + await UrlbarTestUtils.assertSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + entry: "oneoff", + }); + Assert.equal( + gURLBar.value, + "test", + "Value should remain the search string after switching back" + ); + + // Switch to tab 1. Search mode should be exited. + await BrowserTestUtils.switchTab(gBrowser, tabs[1]); + await UrlbarTestUtils.assertSearchMode(window, null); + + // Switch back to tab 2. We should do a search and re-enter search mode. + searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + await BrowserTestUtils.switchTab(gBrowser, tabs[2]); + await searchPromise; + await UrlbarTestUtils.assertSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.TABS, + entry: "oneoff", + }); + Assert.equal( + gURLBar.value, + "test tab 2", + "Value should remain the search string after switching back" + ); + + // Exit search mode. + await UrlbarTestUtils.exitSearchMode(window, { clickClose: true }); + + // Switch to tab 0. We should do a search and re-enter search mode. + searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + await BrowserTestUtils.switchTab(gBrowser, tabs[0]); + await searchPromise; + await UrlbarTestUtils.assertSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + entry: "oneoff", + }); + Assert.equal( + gURLBar.value, + "test", + "Value should remain the search string after switching back" + ); + + // Switch back to tab 2. We should do a search but search mode should be + // inactive. + searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + await BrowserTestUtils.switchTab(gBrowser, tabs[2]); + await searchPromise; + await UrlbarTestUtils.assertSearchMode(window, null); + Assert.equal( + gURLBar.value, + "test tab 2", + "Value should remain the search string after switching back" + ); + + // Switch back to tab 0. We should do a search and re-enter search mode. + searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + await BrowserTestUtils.switchTab(gBrowser, tabs[0]); + await searchPromise; + await UrlbarTestUtils.assertSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + entry: "oneoff", + }); + Assert.equal( + gURLBar.value, + "test", + "Value should remain the search string after switching back" + ); + + // Exit search mode. + await UrlbarTestUtils.exitSearchMode(window, { clickClose: true }); + + // Switch back to tab 2. We should do a search but search mode should be + // inactive. + searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + await BrowserTestUtils.switchTab(gBrowser, tabs[2]); + await searchPromise; + await UrlbarTestUtils.assertSearchMode(window, null); + Assert.equal( + gURLBar.value, + "test tab 2", + "Value should remain the search string after switching back" + ); + + // Switch back to tab 0. We should do a search but search mode should be + // inactive. + searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + await BrowserTestUtils.switchTab(gBrowser, tabs[0]); + await searchPromise; + await UrlbarTestUtils.assertSearchMode(window, null); + Assert.equal( + gURLBar.value, + "test", + "Value should remain the search string after switching back" + ); + + await UrlbarTestUtils.promisePopupClose(window); + for (let tab of tabs) { + BrowserTestUtils.removeTab(tab); + } +}); + +// Start loading a SERP from search mode then immediately switch to a new tab so +// the SERP finishes loading in the background. Switch back to the SERP tab and +// observe that we don't re-enter search mode despite having an entry for that +// tab in UrlbarInput._searchModesByBrowser. See bug 1675926. +// +// This subtest intermittently does not test bug 1675926 (NB: this does not mean +// it is an intermittent failure). The false-positive occurs if the SERP page +// finishes loading before we switch tabs. We include this subtest so we have +// one covering real-world behaviour. A subtest that is guaranteed to test this +// behaviour that does not simulate real world behaviour is included below. +add_task(async function slow_load() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.searches", false]], + }); + const engineName = "Test"; + let engine = await Services.search.addEngineWithDetails(engineName, { + template: "http://example.com/?search={searchTerms}", + }); + const originalTab = gBrowser.selectedTab; + const newTab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + fireInputEvent: true, + }); + await UrlbarTestUtils.enterSearchMode(window, { engineName }); + + const loadPromise = BrowserTestUtils.browserLoaded(newTab.linkedBrowser); + // Select the search mode heuristic to load the example.com SERP. + EventUtils.synthesizeKey("KEY_Enter"); + // Switch away from the tab before we let it load. + await BrowserTestUtils.switchTab(gBrowser, originalTab); + await loadPromise; + + // Switch back to the search mode tab and confirm we don't restore search + // mode. + await BrowserTestUtils.switchTab(gBrowser, newTab); + await UrlbarTestUtils.assertSearchMode(window, null); + + BrowserTestUtils.removeTab(newTab); + await Services.search.removeEngine(engine); + await SpecialPowers.popPrefEnv(); +}); + +// Tests the same behaviour as slow_load, but in a more reliable way using +// non-real-world behaviour. +add_task(async function slow_load_guaranteed() { + const engineName = "Test"; + let engine = await Services.search.addEngineWithDetails(engineName, { + template: "http://example.com/?search={searchTerms}", + }); + const backgroundTab = BrowserTestUtils.addTab(gBrowser); + + // Simulate a tab that was in search mode, loaded a SERP, then was switched + // away from before setURI was called. + backgroundTab.ownerGlobal.gURLBar.searchMode = { engineName }; + let loadPromise = BrowserTestUtils.browserLoaded(backgroundTab.linkedBrowser); + BrowserTestUtils.loadURI( + backgroundTab.linkedBrowser, + "http://example.com/?search=test" + ); + await loadPromise; + + // Switch to the background mode tab and confirm we don't restore search mode. + await BrowserTestUtils.switchTab(gBrowser, backgroundTab); + await UrlbarTestUtils.assertSearchMode(window, null); + + BrowserTestUtils.removeTab(backgroundTab); + await Services.search.removeEngine(engine); +}); + +// Enters search mode by typing a restriction char with no search string. +// Search mode and the search string should be restored after switching back to +// the tab. +add_task(async function userTypedValue_empty() { + await doUserTypedValueTest(""); +}); + +// Enters search mode by typing a restriction char followed by a search string. +// Search mode and the search string should be restored after switching back to +// the tab. +add_task(async function userTypedValue_nonEmpty() { + await doUserTypedValueTest("foo bar"); +}); + +/** + * Enters search mode by typing a restriction char followed by a search string, + * opens a new tab and immediately closes it so we switch back to the search + * mode tab, and checks the search mode state and input value. + * + * @param {string} searchString + * The search string to enter search mode with. + */ +async function doUserTypedValueTest(searchString) { + let value = `${UrlbarTokenizer.RESTRICT.BOOKMARK} ${searchString}`; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value, + fireInputEvent: true, + }); + await UrlbarTestUtils.assertSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + entry: "typed", + }); + Assert.equal( + gURLBar.value, + searchString, + "Sanity check: Value is the search string" + ); + + let tab = await BrowserTestUtils.openNewForegroundTab({ gBrowser }); + BrowserTestUtils.removeTab(tab); + + await UrlbarTestUtils.assertSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + entry: "typed", + }); + Assert.equal( + gURLBar.value, + searchString, + "Value should remain the search string after switching back" + ); + + gURLBar.handleRevert(); +} diff --git a/browser/components/urlbar/tests/browser/browser_searchSettings.js b/browser/components/urlbar/tests/browser/browser_searchSettings.js new file mode 100644 index 0000000000..ff89a868df --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchSettings.js @@ -0,0 +1,32 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function() { + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:blank" }, + async function() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "a", + }); + + // Since the current tab is blank the preferences pane will load there + let loaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + await UrlbarTestUtils.promisePopupClose(window, () => { + let button = document.getElementById( + "urlbar-anon-search-settings-compact" + ); + EventUtils.synthesizeMouseAtCenter(button, {}); + }); + await loaded; + + is( + gBrowser.selectedBrowser.currentURI.spec, + "about:preferences#search", + "Should have loaded the right page" + ); + } + ); +}); diff --git a/browser/components/urlbar/tests/browser/browser_searchSingleWordNotification.js b/browser/components/urlbar/tests/browser/browser_searchSingleWordNotification.js new file mode 100644 index 0000000000..3218d2d30d --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchSingleWordNotification.js @@ -0,0 +1,356 @@ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ +"use strict"; + +let gDNSResolved = false; +let gRealDNSService = gDNSService; +add_task(async function setup() { + gDNSService = { + asyncResolve() { + gDNSResolved = true; + return gRealDNSService.asyncResolve(...arguments); + }, + }; + registerCleanupFunction(function() { + gDNSService = gRealDNSService; + Services.prefs.clearUserPref("browser.fixup.domainwhitelist.localhost"); + }); +}); + +function promiseNotification(aBrowser, value, expected, input) { + return new Promise(resolve => { + let notificationBox = aBrowser.getNotificationBox(aBrowser.selectedBrowser); + if (expected) { + info("Waiting for " + value + " notification"); + resolve( + BrowserTestUtils.waitForNotificationInNotificationBox( + notificationBox, + value + ) + ); + } else { + setTimeout(() => { + is( + notificationBox.getNotificationWithValue(value), + null, + `We are expecting to not get a notification for ${input}` + ); + resolve(); + }, 1000); + } + }); +} + +async function runURLBarSearchTest({ + valueToOpen, + enterSearchMode, + expectSearch, + expectNotification, + expectDNSResolve, + aWindow = window, +}) { + gDNSResolved = false; + // Test both directly setting a value and pressing enter, or setting the + // value through input events, like the user would do. + const setValueFns = [ + value => { + aWindow.gURLBar.value = value; + if (enterSearchMode) { + // Ensure to open the panel. + UrlbarTestUtils.fireInputEvent(aWindow); + } + }, + value => { + return UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: aWindow, + value, + }); + }, + ]; + + for (let i = 0; i < setValueFns.length; ++i) { + await setValueFns[i](valueToOpen); + if (enterSearchMode) { + if (!expectSearch) { + throw new Error("Must execute a search in search mode"); + } + await UrlbarTestUtils.enterSearchMode(aWindow); + } + + let expectedURI; + if (!expectSearch) { + expectedURI = "http://" + valueToOpen + "/"; + } else { + expectedURI = (await Services.search.getDefault()).getSubmission( + valueToOpen, + null, + "keyword" + ).uri.spec; + } + aWindow.gURLBar.focus(); + let docLoadPromise = BrowserTestUtils.waitForDocLoadAndStopIt( + expectedURI, + aWindow.gBrowser.selectedBrowser + ); + EventUtils.synthesizeKey("VK_RETURN", {}, aWindow); + + if (!enterSearchMode) { + await promiseNotification( + aWindow.gBrowser, + "keyword-uri-fixup", + expectNotification, + valueToOpen + ); + } + await docLoadPromise; + + if (expectNotification) { + let notificationBox = aWindow.gBrowser.getNotificationBox( + aWindow.gBrowser.selectedBrowser + ); + let notification = notificationBox.getNotificationWithValue( + "keyword-uri-fixup" + ); + // Confirm the notification only on the last loop. + if (i == setValueFns.length - 1) { + docLoadPromise = BrowserTestUtils.waitForDocLoadAndStopIt( + "http://" + valueToOpen + "/", + aWindow.gBrowser.selectedBrowser + ); + notification.querySelector("button").click(); + await docLoadPromise; + } else { + notificationBox.currentNotification.close(); + } + } + Assert.equal( + gDNSResolved, + expectDNSResolve, + `Should${expectDNSResolve ? "" : " not"} DNS resolve "${valueToOpen}"` + ); + } +} + +add_task(async function test_navigate_full_domain() { + let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab( + gBrowser, + "about:blank" + )); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await runURLBarSearchTest({ + valueToOpen: "www.singlewordtest.org", + expectSearch: false, + expectNotification: false, + expectDNSResolve: false, + }); + gBrowser.removeTab(tab); +}); + +add_task(async function test_navigate_decimal_ip() { + let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab( + gBrowser, + "about:blank" + )); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await runURLBarSearchTest({ + valueToOpen: "1234", + expectSearch: true, + expectNotification: false, + expectDNSResolve: false, // Possible IP in numeric format. + }); + gBrowser.removeTab(tab); +}); + +add_task(async function test_navigate_decimal_ip_with_path() { + let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab( + gBrowser, + "about:blank" + )); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await runURLBarSearchTest({ + valueToOpen: "1234/12", + expectSearch: true, + expectNotification: false, + expectDNSResolve: false, + }); + gBrowser.removeTab(tab); +}); + +add_task(async function test_navigate_large_number() { + let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab( + gBrowser, + "about:blank" + )); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await runURLBarSearchTest({ + valueToOpen: "123456789012345", + expectSearch: true, + expectNotification: false, + expectDNSResolve: false, // Possible IP in numeric format. + }); + gBrowser.removeTab(tab); +}); + +add_task(async function test_navigate_small_hex_number() { + let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab( + gBrowser, + "about:blank" + )); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await runURLBarSearchTest({ + valueToOpen: "0x1f00ffff", + expectSearch: true, + expectNotification: false, + expectDNSResolve: false, // Possible IP in numeric format. + }); + gBrowser.removeTab(tab); +}); + +add_task(async function test_navigate_large_hex_number() { + let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab( + gBrowser, + "about:blank" + )); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await runURLBarSearchTest({ + valueToOpen: "0x7f0000017f000001", + expectSearch: true, + expectNotification: false, + expectDNSResolve: false, // Possible IP in numeric format. + }); + gBrowser.removeTab(tab); +}); + +function get_test_function_for_localhost_with_hostname( + hostName, + isPrivate = false +) { + return async function test_navigate_single_host() { + info(`Test ${hostName}${isPrivate ? " in Private Browsing mode" : ""}`); + const pref = "browser.fixup.domainwhitelist.localhost"; + let win; + if (isPrivate) { + let promiseWin = BrowserTestUtils.waitForNewWindow(); + win = OpenBrowserWindow({ private: true }); + await promiseWin; + await SimpleTest.promiseFocus(win); + // We can do this since the window will be gone shortly. + delete win.gDNSService; + win.gDNSService = { + asyncResolve() { + gDNSResolved = true; + return gRealDNSService.asyncResolve(...arguments); + }, + }; + } else { + win = window; + } + + // Remove the domain from the whitelist, the notification sould appear, + // unless we are in private browsing mode. + Services.prefs.setBoolPref(pref, false); + + await BrowserTestUtils.withNewTab( + { + gBrowser: win.gBrowser, + url: "about:blank", + }, + browser => + runURLBarSearchTest({ + valueToOpen: hostName, + expectSearch: true, + expectNotification: true, + expectDNSResolve: true, + aWindow: win, + }) + ); + + // check pref value + let prefValue = Services.prefs.getBoolPref(pref); + is(prefValue, !isPrivate, "Pref should have the correct state."); + + // Now try again with the pref set. + // In a private window, the notification should appear again. + await BrowserTestUtils.withNewTab( + { + gBrowser: win.gBrowser, + url: "about:blank", + }, + browser => + runURLBarSearchTest({ + valueToOpen: hostName, + expectSearch: isPrivate, + expectNotification: isPrivate, + expectDNSResolve: isPrivate, + aWindow: win, + }) + ); + + if (isPrivate) { + info("Waiting for private window to close"); + await BrowserTestUtils.closeWindow(win); + await SimpleTest.promiseFocus(window); + } + }; +} + +add_task(get_test_function_for_localhost_with_hostname("localhost")); +add_task(get_test_function_for_localhost_with_hostname("localhost.")); +add_task(get_test_function_for_localhost_with_hostname("localhost", true)); + +add_task(async function test_dnsResolveSingleWordsAfterSearch() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.dnsResolveSingleWordsAfterSearch", 0], + ["browser.fixup.domainwhitelist.localhost", false], + ], + }); + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: "about:blank", + }, + browser => + runURLBarSearchTest({ + valueToOpen: "localhost", + expectSearch: true, + expectNotification: false, + expectDNSResolve: false, + }) + ); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_navigate_invalid_url() { + let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab( + gBrowser, + "about:blank" + )); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await runURLBarSearchTest({ + valueToOpen: "mozilla is awesome", + expectSearch: true, + expectNotification: false, + expectDNSResolve: false, + }); + gBrowser.removeTab(tab); +}); + +add_task(async function test_search_mode() { + info("When in search mode we should never query the DNS"); + await SpecialPowers.pushPrefEnv({ + set: [["browser.search.suggest.enabled", false]], + }); + let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab( + gBrowser, + "about:blank" + )); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await runURLBarSearchTest({ + enterSearchMode: true, + valueToOpen: "mozilla", + expectSearch: true, + expectNotification: false, + expectDNSResolve: false, + }); + gBrowser.removeTab(tab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_searchSuggestions.js b/browser/components/urlbar/tests/browser/browser_searchSuggestions.js new file mode 100644 index 0000000000..d325129788 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchSuggestions.js @@ -0,0 +1,343 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This tests checks that search suggestions can be acted upon correctly + * e.g. selection with modifiers, copying text. + */ + +"use strict"; + +const SUGGEST_URLBAR_PREF = "browser.urlbar.suggest.searches"; +const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml"; + +const MAX_CHARS_PREF = "browser.urlbar.maxCharsForSearchSuggestions"; + +// Must run first. +add_task(async function prepare() { + let suggestionsEnabled = Services.prefs.getBoolPref(SUGGEST_URLBAR_PREF); + Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, true); + let engine = await SearchTestUtils.promiseNewSearchEngine( + getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME + ); + let oldDefaultEngine = await Services.search.getDefault(); + await Services.search.setDefault(engine); + await UrlbarTestUtils.formHistory.clear(); + registerCleanupFunction(async function() { + Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, suggestionsEnabled); + await Services.search.setDefault(oldDefaultEngine); + + // Clicking suggestions causes visits to search results pages, so clear that + // history now. + await PlacesUtils.history.clear(); + await UrlbarTestUtils.formHistory.clear(); + }); +}); + +add_task(async function clickSuggestion() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + gURLBar.focus(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "foo", + }); + let [idx, suggestion, engineName] = await getFirstSuggestion(); + Assert.equal( + engineName, + "browser_searchSuggestionEngine searchSuggestionEngine.xml", + "Expected suggestion engine" + ); + + let uri = (await Services.search.getDefault()).getSubmission(suggestion).uri; + let loadPromise = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + uri.spec + ); + let element = await UrlbarTestUtils.waitForAutocompleteResultAt(window, idx); + EventUtils.synthesizeMouseAtCenter(element, {}, window); + await loadPromise; + + let formHistory = ( + await UrlbarTestUtils.formHistory.search({ source: engineName }) + ).map(entry => entry.value); + Assert.deepEqual( + formHistory, + ["foofoo"], + "Should find form history after adding it" + ); + + BrowserTestUtils.removeTab(tab); + await UrlbarTestUtils.formHistory.clear(); +}); + +async function testPressEnterOnSuggestion( + expectedUrl = null, + keyModifiers = {} +) { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + gURLBar.focus(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "foo", + }); + let [idx, suggestion, engineName] = await getFirstSuggestion(); + Assert.equal( + engineName, + "browser_searchSuggestionEngine searchSuggestionEngine.xml", + "Expected suggestion engine" + ); + + let hasExpectedUrl = !!expectedUrl; + if (!expectedUrl) { + expectedUrl = (await Services.search.getDefault()).getSubmission(suggestion) + .uri.spec; + } + + let promiseLoad = BrowserTestUtils.waitForDocLoadAndStopIt( + expectedUrl, + gBrowser.selectedBrowser + ); + + let promiseFormHistory; + if (!hasExpectedUrl) { + promiseFormHistory = UrlbarTestUtils.formHistory.promiseChanged("add"); + } + + for (let i = 0; i < idx; ++i) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + } + EventUtils.synthesizeKey("KEY_Enter", keyModifiers); + + await promiseLoad; + + if (!hasExpectedUrl) { + await promiseFormHistory; + let formHistory = ( + await UrlbarTestUtils.formHistory.search({ source: engineName }) + ).map(entry => entry.value); + Assert.deepEqual( + formHistory, + ["foofoo"], + "Should find form history after adding it" + ); + } + + BrowserTestUtils.removeTab(tab); + await UrlbarTestUtils.formHistory.clear(); +} + +add_task(async function plainEnterOnSuggestion() { + await testPressEnterOnSuggestion(); +}); + +add_task(async function ctrlEnterOnSuggestion() { + await testPressEnterOnSuggestion("https://www.foofoo.com/", { + ctrlKey: true, + }); +}); + +add_task(async function copySuggestionText() { + gURLBar.focus(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "foo", + }); + let [idx, suggestion] = await getFirstSuggestion(); + for (let i = 0; i < idx; ++i) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + } + gURLBar.select(); + await SimpleTest.promiseClipboardChange(suggestion, () => { + goDoCommand("cmd_copy"); + }); +}); + +add_task(async function typeMaxChars() { + gURLBar.focus(); + + let maxChars = 10; + await SpecialPowers.pushPrefEnv({ + set: [[MAX_CHARS_PREF, maxChars]], + }); + + // Make a string as long as maxChars and type it. + let value = ""; + for (let i = 0; i < maxChars; i++) { + value += String.fromCharCode("a".charCodeAt(0) + i); + } + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value, + }); + + // Suggestions should be fetched since we allow them when typing, and the + // value so far isn't longer than maxChars anyway. + await assertSuggestions([value + "foo", value + "bar"]); + + // Now type some additional chars. Suggestions should still be fetched since + // we allow them when typing. + for (let i = 0; i < 3; i++) { + let char = String.fromCharCode("z".charCodeAt(0) - i); + value += char; + EventUtils.synthesizeKey(char); + await assertSuggestions([value + "foo", value + "bar"]); + } + + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function pasteMaxChars() { + gURLBar.focus(); + + let maxChars = 10; + await SpecialPowers.pushPrefEnv({ + set: [[MAX_CHARS_PREF, maxChars]], + }); + + // Make a string as long as maxChars and paste it. + let value = ""; + for (let i = 0; i < maxChars; i++) { + value += String.fromCharCode("a".charCodeAt(0) + i); + } + await selectAndPaste(value); + + // Suggestions should be fetched since the pasted string is not longer than + // maxChars. + await assertSuggestions([value + "foo", value + "bar"]); + + // Now type some additional chars. Suggestions should still be fetched since + // we allow them when typing. + for (let i = 0; i < 3; i++) { + let char = String.fromCharCode("z".charCodeAt(0) - i); + value += char; + EventUtils.synthesizeKey(char); + await assertSuggestions([value + "foo", value + "bar"]); + } + + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function pasteMoreThanMaxChars() { + gURLBar.focus(); + + let maxChars = 10; + await SpecialPowers.pushPrefEnv({ + set: [[MAX_CHARS_PREF, maxChars]], + }); + + // Make a string longer than maxChars and paste it. + let value = ""; + for (let i = 0; i < 2 * maxChars; i++) { + value += String.fromCharCode("a".charCodeAt(0) + i); + } + await selectAndPaste(value); + + // Suggestions should not be fetched since the value was pasted and it was + // longer than maxChars. + await assertSuggestions([]); + + // Now type some additional chars. Suggestions should now be fetched since we + // allow them when typing. + for (let i = 0; i < 3; i++) { + let char = String.fromCharCode("z".charCodeAt(0) - i); + value += char; + EventUtils.synthesizeKey(char); + await assertSuggestions([value + "foo", value + "bar"]); + } + + // Paste again. The string is longer than maxChars, so suggestions should not + // be fetched. + await selectAndPaste(value); + await assertSuggestions([]); + + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function heuristicAddsFormHistory() { + await UrlbarTestUtils.formHistory.clear(); + let formHistory = (await UrlbarTestUtils.formHistory.search()).map( + entry => entry.value + ); + Assert.deepEqual(formHistory, [], "Form history should be empty initially"); + + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + gURLBar.focus(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "foo", + }); + + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(result.heuristic); + Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.SEARCH); + Assert.equal(result.searchParams.query, "foo"); + + let uri = (await Services.search.getDefault()).getSubmission("foo").uri; + let loadPromise = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + uri.spec + ); + let formHistoryPromise = UrlbarTestUtils.formHistory.promiseChanged("add"); + let element = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0); + EventUtils.synthesizeMouseAtCenter(element, {}, window); + await loadPromise; + + await formHistoryPromise; + formHistory = ( + await UrlbarTestUtils.formHistory.search({ + source: result.searchParams.engine, + }) + ).map(entry => entry.value); + Assert.deepEqual( + formHistory, + ["foo"], + "Should find form history after adding it" + ); + + BrowserTestUtils.removeTab(tab); + await UrlbarTestUtils.formHistory.clear(); +}); + +async function getFirstSuggestion() { + let results = await getSuggestionResults(); + if (!results.length) { + return [-1, null, null]; + } + let result = results[0]; + return [ + result.index, + result.searchParams.suggestion, + result.searchParams.engine, + ]; +} + +async function getSuggestionResults() { + await UrlbarTestUtils.promiseSearchComplete(window); + + let results = []; + let matchCount = UrlbarTestUtils.getResultCount(window); + for (let i = 0; i < matchCount; i++) { + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + if ( + result.type == UrlbarUtils.RESULT_TYPE.SEARCH && + result.searchParams.suggestion + ) { + result.index = i; + results.push(result); + } + } + return results; +} + +async function assertSuggestions(expectedSuggestions) { + let results = await getSuggestionResults(); + let actualSuggestions = results.map(r => r.searchParams.suggestion); + Assert.deepEqual( + actualSuggestions, + expectedSuggestions, + "Expected suggestions" + ); +} diff --git a/browser/components/urlbar/tests/browser/browser_searchTelemetry.js b/browser/components/urlbar/tests/browser/browser_searchTelemetry.js new file mode 100644 index 0000000000..219a7610c3 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_searchTelemetry.js @@ -0,0 +1,225 @@ +"use strict"; + +const SUGGEST_URLBAR_PREF = "browser.urlbar.suggest.searches"; +const MAX_FORM_HISTORY_PREF = "browser.urlbar.maxHistoricalSearchSuggestions"; +const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml"; + +// Must run first. +add_task(async function prepare() { + await SpecialPowers.pushPrefEnv({ + set: [ + [SUGGEST_URLBAR_PREF, true], + [MAX_FORM_HISTORY_PREF, 2], + ], + }); + + let engine = await SearchTestUtils.promiseNewSearchEngine( + getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME + ); + let oldDefaultEngine = await Services.search.getDefault(); + await Services.search.setDefault(engine); + + registerCleanupFunction(async function() { + await Services.search.setDefault(oldDefaultEngine); + + // Clicking urlbar results causes visits to their associated pages, so clear + // that history now. + await PlacesUtils.history.clear(); + await UrlbarTestUtils.formHistory.clear(); + }); + + // Move the mouse away from the urlbar one-offs so that a one-off engine is + // not inadvertently selected. + await new Promise(resolve => { + EventUtils.synthesizeNativeMouseMove( + window.document.documentElement, + 0, + 0, + resolve + ); + }); +}); + +add_task(async function heuristicResultMouse() { + await compareCounts(async function() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + gURLBar.focus(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "heuristicResult", + }); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.SEARCH, + "Should be of type search" + ); + let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + let element = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0); + EventUtils.synthesizeMouseAtCenter(element, {}); + await loadPromise; + BrowserTestUtils.removeTab(tab); + await UrlbarTestUtils.formHistory.clear(); + }); +}); + +add_task(async function heuristicResultKeyboard() { + await compareCounts(async function() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + gURLBar.focus(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "heuristicResult", + }); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.SEARCH, + "Should be of type search" + ); + let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + EventUtils.sendKey("return"); + await loadPromise; + BrowserTestUtils.removeTab(tab); + await UrlbarTestUtils.formHistory.clear(); + }); +}); + +add_task(async function searchSuggestionMouse() { + await compareCounts(async function() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + gURLBar.focus(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "searchSuggestion", + }); + let idx = await getFirstSuggestionIndex(); + Assert.greaterOrEqual(idx, 0, "there should be a first suggestion"); + let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + let element = await UrlbarTestUtils.waitForAutocompleteResultAt( + window, + idx + ); + EventUtils.synthesizeMouseAtCenter(element, {}); + await loadPromise; + BrowserTestUtils.removeTab(tab); + await UrlbarTestUtils.formHistory.clear(); + }); +}); + +add_task(async function searchSuggestionKeyboard() { + await compareCounts(async function() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + gURLBar.focus(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "searchSuggestion", + }); + let idx = await getFirstSuggestionIndex(); + Assert.greaterOrEqual(idx, 0, "there should be a first suggestion"); + let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + while (idx--) { + EventUtils.sendKey("down"); + } + EventUtils.sendKey("return"); + await loadPromise; + BrowserTestUtils.removeTab(tab); + await UrlbarTestUtils.formHistory.clear(); + }); +}); + +add_task(async function formHistoryMouse() { + await compareCounts(async function() { + await UrlbarTestUtils.formHistory.add(["foofoo", "foobar"]); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + gURLBar.focus(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "foo", + }); + let index = await getFirstSuggestionIndex(); + Assert.greaterOrEqual(index, 0, "there should be a first suggestion"); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, index); + Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.SEARCH); + Assert.equal(result.source, UrlbarUtils.RESULT_SOURCE.HISTORY); + let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + let element = await UrlbarTestUtils.waitForAutocompleteResultAt( + window, + index + ); + EventUtils.synthesizeMouseAtCenter(element, {}); + await loadPromise; + BrowserTestUtils.removeTab(tab); + await UrlbarTestUtils.formHistory.clear(); + }); +}); + +add_task(async function formHistoryKeyboard() { + await compareCounts(async function() { + await UrlbarTestUtils.formHistory.add(["foofoo", "foobar"]); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + gURLBar.focus(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "foo", + }); + let index = await getFirstSuggestionIndex(); + Assert.greaterOrEqual(index, 0, "there should be a first suggestion"); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, index); + Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.SEARCH); + Assert.equal(result.source, UrlbarUtils.RESULT_SOURCE.HISTORY); + let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + while (index--) { + EventUtils.sendKey("down"); + } + EventUtils.sendKey("return"); + await loadPromise; + BrowserTestUtils.removeTab(tab); + await UrlbarTestUtils.formHistory.clear(); + }); +}); + +/** + * This does three things: gets current telemetry/FHR counts, calls + * clickCallback, gets telemetry/FHR counts again to compare them to the old + * counts. + * + * @param {function} clickCallback Use this to open the urlbar popup and choose + * and click a result. + */ +async function compareCounts(clickCallback) { + // Search events triggered by clicks (not the Return key in the urlbar) are + // recorded in three places: + // * Telemetry histogram named "SEARCH_COUNTS" + // * FHR + + let engine = await Services.search.getDefault(); + + let histogramKey = `other-${engine.name}.urlbar`; + let histogram = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS"); + histogram.clear(); + + gURLBar.focus(); + await clickCallback(); + + TelemetryTestUtils.assertKeyedHistogramSum(histogram, histogramKey, 1); +} + +/** + * Returns the index of the first search suggestion in the urlbar results. + * + * @returns {number} An index, or -1 if there are no search suggestions. + */ +async function getFirstSuggestionIndex() { + const matchCount = UrlbarTestUtils.getResultCount(window); + for (let i = 0; i < matchCount; i++) { + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + if ( + result.type == UrlbarUtils.RESULT_TYPE.SEARCH && + result.searchParams.suggestion + ) { + return i; + } + } + return -1; +} diff --git a/browser/components/urlbar/tests/browser/browser_selectStaleResults.js b/browser/components/urlbar/tests/browser/browser_selectStaleResults.js new file mode 100644 index 0000000000..8732a6d356 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_selectStaleResults.js @@ -0,0 +1,301 @@ +/* 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/. */ + +// This test makes sure that arrowing down and up through the view's results +// works correctly with regard to stale results. + +"use strict"; + +XPCOMUtils.defineLazyModuleGetters(this, { + UrlbarView: "resource:///modules/UrlbarView.jsm", +}); + +add_task(async function init() { + // Increase the timeout of the remove-stale-rows timer so that it doesn't + // interfere with the tests. + let originalRemoveStaleRowsTimeout = UrlbarView.removeStaleRowsTimeout; + UrlbarView.removeStaleRowsTimeout = 1000; + registerCleanupFunction(() => { + UrlbarView.removeStaleRowsTimeout = originalRemoveStaleRowsTimeout; + }); +}); + +// This tests the case where queryContext.results.length < the number of rows in +// the view, i.e., the view contains stale rows. +add_task(async function viewContainsStaleRows() { + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + + let maxResults = UrlbarPrefs.get("maxRichResults"); + let halfResults = Math.floor(maxResults / 2); + + // Add enough visits to pages with "xx" in the title to fill up half the view. + for (let i = 0; i < halfResults; i++) { + await PlacesTestUtils.addVisits({ + uri: "http://mochi.test:8888/" + i, + title: "xx" + i, + }); + } + + // Add enough visits to pages with "x" in the title to fill up the entire + // view. + for (let i = 0; i < maxResults; i++) { + await PlacesTestUtils.addVisits({ + uri: "http://example.com/" + i, + title: "x" + i, + }); + } + + gURLBar.focus(); + + // Search for "x" and wait for the search to finish. All the "x" results + // added above should be in the view. (Actually one fewer will be in the + // view due to the heuristic result, but that's not important.) + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "x", + fireInputEvent: true, + }); + + // Below we'll do a search for "xx". Get the row that will show the last + // result in that search. + let row = gURLBar.view._rows.children[halfResults]; + + // Add a mutation listener on that row. Wait for its "stale" attribute to be + // removed. + let mutationPromise = new Promise(resolve => { + let observer = new MutationObserver(mutations => { + for (let mut of mutations) { + if (mut.attributeName == "stale" && !row.hasAttribute("stale")) { + observer.disconnect(); + resolve(); + break; + } + } + }); + observer.observe(row, { attributes: true }); + }); + + // Type another "x" so that we search for "xx", but don't wait for the search + // to finish. Instead, wait for the row's stale attribute to be removed. + EventUtils.synthesizeKey("x"); + info("Waiting for 'stale' attribute to be removed... "); + await mutationPromise; + + // Now arrow down. The search, which is still ongoing, will now stop and the + // view won't be updated anymore. + EventUtils.synthesizeKey("KEY_ArrowDown"); + + // Wait for the search to stop. + info("Waiting for the search to stop... "); + await gURLBar.lastQueryContextPromise; + + // The query context for the last search ("xx") should contain only + // halfResults + 1 results (+ 1 for the heuristic). + Assert.ok(gURLBar.controller._lastQueryContextWrapper); + let { queryContext } = gURLBar.controller._lastQueryContextWrapper; + Assert.ok(queryContext); + Assert.equal(queryContext.results.length, halfResults + 1); + + // But there should be maxResults visible rows in the view. + let items = Array.from(gURLBar.view._rows.children).filter(r => + gURLBar.view._isElementVisible(r) + ); + Assert.equal(items.length, maxResults); + + // Arrow down through all the results. After arrowing down from the last "xx" + // result, the stale "x" results should be selected. We should *not* enter + // the one-off search buttons at that point. + for (let i = 1; i < maxResults; i++) { + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), i); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + Assert.equal(result.element.row.result.rowIndex, i); + EventUtils.synthesizeKey("KEY_ArrowDown"); + } + + // Now the first one-off should be selected. + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), -1); + Assert.equal(gURLBar.view.oneOffSearchButtons.selectedButtonIndex, 0); + + // Arrow back up through all the results. + for (let i = maxResults - 1; i >= 0; i--) { + EventUtils.synthesizeKey("KEY_ArrowUp"); + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), i); + } + + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Escape") + ); +}); + +// This tests the case where, before the search finishes, stale results have +// been removed and replaced with non-stale results. +add_task(async function staleReplacedWithFresh() { + // For this test, we need one set of results that's added quickly and another + // set that's added after a delay. We do an initial search and wait for both + // sets to be added. Then we do another search, but this time only wait for + // the fast results to be added, and then we arrow down to stop the search + // before the delayed results are added. The order in which things should + // happen after the second search goes like this: + // + // (1) second search + // (2) fast results are added + // (3) remove-stale-rows timer fires and removes stale rows (the rows from + // the delayed set of results from the first search) + // (4) we arrow down to stop the search + // + // We use history for the fast results and a slow search engine for the + // delayed results. + // + // NB: If this test ends up failing, it may be because the remove-stale-rows + // timer fires before the history results are added. i.e., steps 2 and 3 + // above happen out of order. If that happens, try increasing + // UrlbarView.removeStaleRowsTimeout above. + + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + + // Enable search suggestions, and add an engine that returns suggestions on a + // delay. + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.searches", true]], + }); + let engine = await SearchTestUtils.promiseNewSearchEngine( + getRootDirectory(gTestPath) + "searchSuggestionEngineSlow.xml" + ); + let oldDefaultEngine = await Services.search.getDefault(); + await Services.search.moveEngine(engine, 0); + await Services.search.setDefault(engine); + + let maxResults = UrlbarPrefs.get("maxRichResults"); + + // Add enough visits to pages with "test" in the title to fill up the entire + // view. + for (let i = 0; i < maxResults; i++) { + await PlacesTestUtils.addVisits({ + uri: "http://example.com/" + i, + title: "test" + i, + }); + } + + gURLBar.focus(); + + // Search for "tes" and wait for the search to finish. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "tes", + fireInputEvent: true, + }); + + // Sanity check the results. They should be: + // + // tes -- Search with searchSuggestionEngineSlow [heuristic] + // tesfoo [search suggestion] + // tesbar [search suggestion] + // test9 [history] + // test8 [history] + // test7 [history] + // test6 [history] + // test5 [history] + // test4 [history] + // test3 [history] + let count = UrlbarTestUtils.getResultCount(window); + Assert.equal(count, maxResults); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(result.heuristic); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.ok(result.searchParams); + Assert.equal(result.searchParams.suggestion, "tesfoo"); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2); + Assert.ok(result.searchParams); + Assert.equal(result.searchParams.suggestion, "tesbar"); + for (let i = 3; i < maxResults; i++) { + result = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.URL); + Assert.equal(result.title, "test" + (maxResults - i + 2)); + } + + // Below we'll do a search for "test" *but* not wait for the two search + // suggestion results to be added. We'll only wait for the history results to + // be added. To determine when the history results are added, use a mutation + // listener on the node containing the rows, and wait until the title of the + // next-to-last row is "test2". At that point, the results should be: + // + // test -- Search with searchSuggestionEngineSlow + // test9 + // test8 + // test7 + // test6 + // test5 + // test4 + // test3 + // test2 + // test1 + let mutationPromise = new Promise(resolve => { + let observer = new MutationObserver(mutations => { + let row = gURLBar.view._rows.children[maxResults - 2]; + if (row && row._elements.get("title").textContent == "test2") { + observer.disconnect(); + resolve(); + } + }); + observer.observe(gURLBar.view._rows, { + subtree: true, + characterData: true, + childList: true, + attributes: true, + }); + }); + + // Now type a "t" so that we search for "test", but only wait for history + // results to be added, as described above. + EventUtils.synthesizeKey("t"); + info("Waiting for the 'test2' row... "); + await mutationPromise; + + // Now arrow down. The search, which is still ongoing, will now stop and the + // view won't be updated anymore. + EventUtils.synthesizeKey("KEY_ArrowDown"); + + // Wait for the search to stop. + info("Waiting for the search to stop... "); + await gURLBar.lastQueryContextPromise; + + // Sanity check the results. They should be as described above. + count = UrlbarTestUtils.getResultCount(window); + Assert.equal(count, maxResults); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(result.heuristic); + Assert.equal(result.element.row.result.rowIndex, 0); + for (let i = 1; i < maxResults; i++) { + result = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.URL); + Assert.equal(result.title, "test" + (maxResults - i)); + Assert.equal(result.element.row.result.rowIndex, i); + } + + // Arrow down through all the results. After arrowing down from "test3", we + // should continue on to "test2". We should *not* enter the one-off search + // buttons at that point. + for (let i = 1; i < maxResults; i++) { + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), i); + EventUtils.synthesizeKey("KEY_ArrowDown"); + } + + // Now the first one-off should be selected. + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), -1); + Assert.equal(gURLBar.view.oneOffSearchButtons.selectedButtonIndex, 0); + + // Arrow back up through all the results. + for (let i = maxResults - 1; i >= 0; i--) { + EventUtils.synthesizeKey("KEY_ArrowUp"); + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), i); + } + + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Escape") + ); + await SpecialPowers.popPrefEnv(); + await Services.search.setDefault(oldDefaultEngine); +}); diff --git a/browser/components/urlbar/tests/browser/browser_selectionKeyNavigation.js b/browser/components/urlbar/tests/browser/browser_selectionKeyNavigation.js new file mode 100644 index 0000000000..99f0a41d7d --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_selectionKeyNavigation.js @@ -0,0 +1,158 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This test makes sure that the up/down and page-up/down properly adjust the +// selection. See also browser_caret_navigation.js and +// browser_urlbar_tabKeyBehavior.js. + +"use strict"; + +const MAX_RESULTS = UrlbarPrefs.get("maxRichResults"); + +add_task(async function init() { + for (let i = 0; i < MAX_RESULTS; i++) { + await PlacesTestUtils.addVisits("http://example.com/" + i); + } + registerCleanupFunction(async function() { + await PlacesUtils.history.clear(); + }); +}); + +add_task(async function downKey() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exam", + fireInputEvent: true, + }); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 0, + "The heuristic autofill result should be selected initially" + ); + for (let i = 1; i < MAX_RESULTS; i++) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), i); + } + EventUtils.synthesizeKey("KEY_ArrowDown"); + let oneOffs = UrlbarTestUtils.getOneOffSearchButtons(window); + Assert.ok(oneOffs.selectedButton, "A one-off should now be selected"); + while (oneOffs.selectedButton) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + } + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 0, + "The heuristic autofill result should be selected again" + ); +}); + +add_task(async function upKey() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exam", + fireInputEvent: true, + }); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 0, + "The heuristic autofill result should be selected initially" + ); + EventUtils.synthesizeKey("KEY_ArrowUp"); + let oneOffs = UrlbarTestUtils.getOneOffSearchButtons(window); + Assert.ok(oneOffs.selectedButton, "A one-off should now be selected"); + while (oneOffs.selectedButton) { + EventUtils.synthesizeKey("KEY_ArrowUp"); + } + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + MAX_RESULTS - 1, + "The last result should be selected" + ); + for (let i = 1; i < MAX_RESULTS; i++) { + EventUtils.synthesizeKey("KEY_ArrowUp"); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + MAX_RESULTS - i - 1 + ); + } +}); + +add_task(async function pageDownKey() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exam", + fireInputEvent: true, + }); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 0, + "The heuristic autofill result should be selected initially" + ); + let pageCount = Math.ceil((MAX_RESULTS - 1) / UrlbarUtils.PAGE_UP_DOWN_DELTA); + for (let i = 0; i < pageCount; i++) { + EventUtils.synthesizeKey("KEY_PageDown"); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + Math.min((i + 1) * UrlbarUtils.PAGE_UP_DOWN_DELTA, MAX_RESULTS - 1) + ); + } + EventUtils.synthesizeKey("KEY_PageDown"); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 0, + "Page down at end should wrap around to first result" + ); +}); + +add_task(async function pageUpKey() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exam", + fireInputEvent: true, + }); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 0, + "The heuristic autofill result should be selected initially" + ); + EventUtils.synthesizeKey("KEY_PageUp"); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + MAX_RESULTS - 1, + "Page up at start should wrap around to last result" + ); + let pageCount = Math.ceil((MAX_RESULTS - 1) / UrlbarUtils.PAGE_UP_DOWN_DELTA); + for (let i = 0; i < pageCount; i++) { + EventUtils.synthesizeKey("KEY_PageUp"); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + Math.max(MAX_RESULTS - 1 - (i + 1) * UrlbarUtils.PAGE_UP_DOWN_DELTA, 0) + ); + } +}); + +add_task(async function pageDownKeyShowsView() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exam", + fireInputEvent: true, + }); + await UrlbarTestUtils.promisePopupClose(window); + EventUtils.synthesizeKey("KEY_PageDown"); + await UrlbarTestUtils.promiseSearchComplete(window); + Assert.ok(UrlbarTestUtils.isPopupOpen(window)); + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), 0); +}); + +add_task(async function pageUpKeyShowsView() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exam", + fireInputEvent: true, + }); + await UrlbarTestUtils.promisePopupClose(window); + EventUtils.synthesizeKey("KEY_PageUp"); + await UrlbarTestUtils.promiseSearchComplete(window); + Assert.ok(UrlbarTestUtils.isPopupOpen(window)); + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), 0); +}); diff --git a/browser/components/urlbar/tests/browser/browser_separatePrivateDefault.js b/browser/components/urlbar/tests/browser/browser_separatePrivateDefault.js new file mode 100644 index 0000000000..9d997f1663 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_separatePrivateDefault.js @@ -0,0 +1,215 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests the 'Search in a Private Window' result of the address bar. +// Tests here don't have a different private engine, for that see +// browser_separatePrivateDefault_differentPrivateEngine.js + +const serverInfo = { + scheme: "http", + host: "localhost", + port: 20709, // Must be identical to what is in searchSuggestionEngine2.xml +}; + +let aliasEngine; + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.search.separatePrivateDefault.ui.enabled", true], + ["browser.search.separatePrivateDefault", true], + ["browser.urlbar.suggest.searches", true], + ], + }); + + // Add some history for the empty panel. + await PlacesTestUtils.addVisits([ + { + uri: "http://example.com/", + transition: PlacesUtils.history.TRANSITIONS.TYPED, + }, + ]); + + // Add a search suggestion engine and move it to the front so that it appears + // as the first one-off. + let oldDefaultEngine = await Services.search.getDefault(); + let oldDefaultPrivateEngine = await Services.search.getDefaultPrivate(); + let engine = await SearchTestUtils.promiseNewSearchEngine( + getRootDirectory(gTestPath) + "searchSuggestionEngine.xml" + ); + await Services.search.setDefault(engine); + await Services.search.setDefaultPrivate(engine); + + // Add another engine in the first one-off position. + let engine2 = await SearchTestUtils.promiseNewSearchEngine( + getRootDirectory(gTestPath) + "POSTSearchEngine.xml" + ); + await Services.search.moveEngine(engine2, 0); + + // Add an engine with an alias. + aliasEngine = await Services.search.addEngineWithDetails("MozSearch", { + alias: "alias", + method: "GET", + template: "http://example.com/?q={searchTerms}", + }); + + registerCleanupFunction(async () => { + await Services.search.setDefault(oldDefaultEngine); + await Services.search.setDefaultPrivate(oldDefaultPrivateEngine); + await Services.search.removeEngine(aliasEngine); + await PlacesUtils.history.clear(); + }); +}); + +async function AssertNoPrivateResult(win) { + let count = await UrlbarTestUtils.getResultCount(win); + Assert.ok(count > 0, "Sanity check result count"); + for (let i = 0; i < count; ++i) { + let result = await UrlbarTestUtils.getDetailsOfResultAt(win, i); + Assert.ok( + result.type != UrlbarUtils.RESULT_TYPE.SEARCH || + !result.searchParams.inPrivateWindow, + "Check this result is not a 'Search in a Private Window' one" + ); + } +} + +async function AssertPrivateResult(win, engine, isPrivateEngine) { + let count = await UrlbarTestUtils.getResultCount(win); + Assert.ok(count > 1, "Sanity check result count"); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.SEARCH, + "Check result type" + ); + Assert.ok(result.searchParams.inPrivateWindow, "Check inPrivateWindow"); + Assert.equal( + result.searchParams.isPrivateEngine, + isPrivateEngine, + "Check isPrivateEngine" + ); + Assert.equal( + result.searchParams.engine, + engine.name, + "Check the search engine" + ); + return result; +} + +add_task(async function test_nonsearch() { + info( + "Test that 'Search in a Private Window' does not appear with non-search results" + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exa", + }); + await AssertNoPrivateResult(window); +}); + +add_task(async function test_search() { + info( + "Test that 'Search in a Private Window' appears with only search results" + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "unique198273982173", + }); + await AssertPrivateResult(window, await Services.search.getDefault(), false); +}); + +add_task(async function test_search_disabled_suggestions() { + info( + "Test that 'Search in a Private Window' appears if suggestions are disabled" + ); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.searches", false]], + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "unique198273982173", + }); + await AssertPrivateResult(window, await Services.search.getDefault(), false); + await SpecialPowers.popPrefEnv(); +}); + +// TODO: (Bug 1658620) Write a new subtest for this behaviour with the update2 +// pref on. +// add_task(async function test_oneoff_selected_keyboard() { +// info( +// "Test that 'Search in a Private Window' with keyboard opens the selected one-off engine if there's no private engine" +// ); +// await SpecialPowers.pushPrefEnv({ +// set: [ +// ["browser.urlbar.update2", false], +// ["browser.urlbar.update2.oneOffsRefresh", false], +// ], +// }); +// await UrlbarTestUtils.promiseAutocompleteResultPopup({ +// window, +// value: "unique198273982173", +// }); +// await AssertPrivateResult(window, await Services.search.getDefault(), false); +// // Select the 'Search in a Private Window' result, alt down to select the +// // first one-off button, Enter. It should open a pb window, but using the +// // selected one-off engine. +// let promiseWindow = BrowserTestUtils.waitForNewWindow({ +// url: +// "http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/print_postdata.sjs", +// }); +// // Select the private result. +// EventUtils.synthesizeKey("KEY_ArrowDown"); +// // Select the first one-off button. +// EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); +// EventUtils.synthesizeKey("VK_RETURN"); +// let win = await promiseWindow; +// Assert.ok( +// PrivateBrowsingUtils.isWindowPrivate(win), +// "Should open a private window" +// ); +// await BrowserTestUtils.closeWindow(win); +// await SpecialPowers.popPrefEnv(); +// }); + +// TODO: (Bug 1658620) Write a new subtest for this behaviour with the update2 +// pref on. +// add_task(async function test_oneoff_selected_mouse() { +// info( +// "Test that 'Search in a Private Window' with mouse opens the selected one-off engine if there's no private engine" +// ); +// await SpecialPowers.pushPrefEnv({ +// set: [ +// ["browser.urlbar.update2", false], +// ["browser.urlbar.update2.oneOffsRefresh", false], +// ], +// }); +// await UrlbarTestUtils.promiseAutocompleteResultPopup({ +// window, +// value: "unique198273982173", +// }); +// await AssertPrivateResult(window, await Services.search.getDefault(), false); +// // Select the 'Search in a Private Window' result, alt down to select the +// // first one-off button, Enter. It should open a pb window, but using the +// // selected one-off engine. +// let promiseWindow = BrowserTestUtils.waitForNewWindow({ +// url: +// "http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/print_postdata.sjs", +// }); +// // Select the private result. +// EventUtils.synthesizeKey("KEY_ArrowDown"); +// // Select the first one-off button. +// EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); +// // Click on the result. +// let element = UrlbarTestUtils.getSelectedRow(window); +// EventUtils.synthesizeMouseAtCenter(element, {}); +// let win = await promiseWindow; +// Assert.ok( +// PrivateBrowsingUtils.isWindowPrivate(win), +// "Should open a private window" +// ); +// await BrowserTestUtils.closeWindow(win); +// await SpecialPowers.popPrefEnv(); +// }); diff --git a/browser/components/urlbar/tests/browser/browser_separatePrivateDefault_differentEngine.js b/browser/components/urlbar/tests/browser/browser_separatePrivateDefault_differentEngine.js new file mode 100644 index 0000000000..23d7712de1 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_separatePrivateDefault_differentEngine.js @@ -0,0 +1,358 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests the 'Search in a Private Window' result of the address bar. + +const serverInfo = { + scheme: "http", + host: "localhost", + port: 20709, // Must be identical to what is in searchSuggestionEngine2.xml +}; + +let gAliasEngine; +let gPrivateEngine; + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.search.separatePrivateDefault.ui.enabled", true], + ["browser.search.separatePrivateDefault", true], + ["browser.urlbar.suggest.searches", true], + ], + }); + + // Add some history for the empty panel. + await PlacesTestUtils.addVisits([ + { + uri: "http://example.com/", + transition: PlacesUtils.history.TRANSITIONS.TYPED, + }, + ]); + + // Add a search suggestion engine and move it to the front so that it appears + // as the first one-off. + let oldDefaultEngine = await Services.search.getDefault(); + let oldDefaultPrivateEngine = await Services.search.getDefaultPrivate(); + let engine = await SearchTestUtils.promiseNewSearchEngine( + getRootDirectory(gTestPath) + "searchSuggestionEngine.xml" + ); + await Services.search.setDefault(engine); + gPrivateEngine = await SearchTestUtils.promiseNewSearchEngine( + getRootDirectory(gTestPath) + "searchSuggestionEngine2.xml" + ); + await Services.search.setDefaultPrivate(gPrivateEngine); + + // Add another engine in the first one-off position. + let engine2 = await SearchTestUtils.promiseNewSearchEngine( + getRootDirectory(gTestPath) + "POSTSearchEngine.xml" + ); + await Services.search.moveEngine(engine2, 0); + + // Add an engine with an alias. + gAliasEngine = await Services.search.addEngineWithDetails("MozSearch", { + alias: "alias", + method: "GET", + template: "http://example.com/?q={searchTerms}", + }); + + registerCleanupFunction(async () => { + await Services.search.setDefault(oldDefaultEngine); + await Services.search.setDefaultPrivate(oldDefaultPrivateEngine); + await Services.search.removeEngine(gAliasEngine); + await PlacesUtils.history.clear(); + }); +}); + +async function AssertNoPrivateResult(win) { + let count = await UrlbarTestUtils.getResultCount(win); + for (let i = 0; i < count; ++i) { + let result = await UrlbarTestUtils.getDetailsOfResultAt(win, i); + Assert.ok( + result.type != UrlbarUtils.RESULT_TYPE.SEARCH || + !result.searchParams.inPrivateWindow, + "Check this result is not a 'Search in a Private Window' one" + ); + } +} + +async function AssertPrivateResult(win, engine, isPrivateEngine) { + let count = await UrlbarTestUtils.getResultCount(win); + Assert.ok(count > 1, "Sanity check result count"); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.SEARCH, + "Check result type" + ); + Assert.ok(result.searchParams.inPrivateWindow, "Check inPrivateWindow"); + Assert.equal( + result.searchParams.isPrivateEngine, + isPrivateEngine, + "Check isPrivateEngine" + ); + Assert.equal( + result.searchParams.engine, + engine.name, + "Check the search engine" + ); + return result; +} + +// Tests from here on have a different default private engine. + +add_task(async function test_search_private_engine() { + info( + "Test that 'Search in a Private Window' reports a separate private engine" + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "unique198273982173", + }); + await AssertPrivateResult(window, gPrivateEngine, true); +}); + +add_task(async function test_privateWindow() { + info( + "Test that 'Search in a Private Window' does not appear in a private window" + ); + let privateWin = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: privateWin, + value: "unique198273982173", + }); + await AssertNoPrivateResult(privateWin); + await BrowserTestUtils.closeWindow(privateWin); +}); + +add_task(async function test_permanentPB() { + info( + "Test that 'Search in a Private Window' does not appear in Permanent Private Browsing" + ); + await SpecialPowers.pushPrefEnv({ + set: [["browser.privatebrowsing.autostart", true]], + }); + let win = await BrowserTestUtils.openNewBrowserWindow(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "unique198273982173", + }); + await AssertNoPrivateResult(win); + await BrowserTestUtils.closeWindow(win); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_openPBWindow() { + info( + "Test that 'Search in a Private Window' opens the search in a new Private Window" + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "unique198273982173", + }); + await AssertPrivateResult( + window, + await Services.search.getDefaultPrivate(), + true + ); + + await withHttpServer(serverInfo, async () => { + let promiseWindow = BrowserTestUtils.waitForNewWindow({ + url: "http://localhost:20709/?terms=unique198273982173", + maybeErrorPage: true, + }); + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("VK_RETURN"); + let win = await promiseWindow; + Assert.ok( + PrivateBrowsingUtils.isWindowPrivate(win), + "Should open a private window" + ); + await BrowserTestUtils.closeWindow(win); + }); +}); + +// TODO: (Bug 1658620) Write a new subtest for this behaviour with the update2 +// pref on. +// add_task(async function test_oneoff_selected_with_private_engine_mouse() { +// info( +// "Test that 'Search in a Private Window' opens the private engine even if a one-off is selected" +// ); +// await SpecialPowers.pushPrefEnv({ +// set: [ +// ["browser.urlbar.update2", false], +// ["browser.urlbar.update2.oneOffsRefresh", false], +// ], +// }); +// await UrlbarTestUtils.promiseAutocompleteResultPopup({ +// window, +// value: "unique198273982173", +// }); +// await AssertPrivateResult( +// window, +// await Services.search.getDefaultPrivate(), +// true +// ); + +// await withHttpServer(serverInfo, async () => { +// // Select the 'Search in a Private Window' result, alt down to select the +// // first one-off button, Click on the result. It should open a pb window using +// // the private search engine, because it has been set. +// let promiseWindow = BrowserTestUtils.waitForNewWindow({ +// url: "http://localhost:20709/?terms=unique198273982173", +// maybeErrorPage: true, +// }); +// // Select the private result. +// EventUtils.synthesizeKey("KEY_ArrowDown"); +// // Select the first one-off button. +// EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); +// // Click on the result. +// let element = UrlbarTestUtils.getSelectedRow(window); +// EventUtils.synthesizeMouseAtCenter(element, {}); +// let win = await promiseWindow; +// Assert.ok( +// PrivateBrowsingUtils.isWindowPrivate(win), +// "Should open a private window" +// ); +// await BrowserTestUtils.closeWindow(win); +// }); +// await SpecialPowers.popPrefEnv(); +// }); + +// TODO: (Bug 1658620) Write a new subtest for this behaviour with the update2 +// pref on. +// add_task(async function test_oneoff_selected_with_private_engine_keyboard() { +// info( +// "Test that 'Search in a Private Window' opens the private engine even if a one-off is selected" +// ); +// await SpecialPowers.pushPrefEnv({ +// set: [ +// ["browser.urlbar.update2", false], +// ["browser.urlbar.update2.oneOffsRefresh", false], +// ], +// }); +// await UrlbarTestUtils.promiseAutocompleteResultPopup({ +// window, +// value: "unique198273982173", +// }); +// await AssertPrivateResult( +// window, +// await Services.search.getDefaultPrivate(), +// true +// ); + +// await withHttpServer(serverInfo, async () => { +// // Select the 'Search in a Private Window' result, alt down to select the +// // first one-off button, Enter. It should open a pb window, but using the +// // selected one-off engine. +// let promiseWindow = BrowserTestUtils.waitForNewWindow({ +// url: "http://localhost:20709/?terms=unique198273982173", +// maybeErrorPage: true, +// }); +// // Select the private result. +// EventUtils.synthesizeKey("KEY_ArrowDown"); +// // Select the first one-off button. +// EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); +// EventUtils.synthesizeKey("VK_RETURN"); +// let win = await promiseWindow; +// Assert.ok( +// PrivateBrowsingUtils.isWindowPrivate(win), +// "Should open a private window" +// ); +// await BrowserTestUtils.closeWindow(win); +// }); +// await SpecialPowers.popPrefEnv(); +// }); + +add_task(async function test_alias_no_query() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.update2.emptySearchBehavior", 2]], + }); + info( + "Test that 'Search in a Private Window' doesn't appear if an alias is typed with no query" + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "alias ", + }); + // Wait for the second new search that starts when search mode is entered. + await UrlbarTestUtils.promiseSearchComplete(window); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: gAliasEngine.name, + entry: "typed", + }); + await AssertNoPrivateResult(window); + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_alias_query() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.update2.emptySearchBehavior", 2]], + }); + info( + "Test that 'Search in a Private Window' appears when an alias is typed with a query" + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "alias something", + }); + // Wait for the second new search that starts when search mode is entered. + await UrlbarTestUtils.promiseSearchComplete(window); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: "MozSearch", + entry: "typed", + }); + await AssertPrivateResult(window, gAliasEngine, true); + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_restrict() { + info( + "Test that 'Search in a Private Window' doesn's appear for just the restriction token" + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: UrlbarTokenizer.RESTRICT.SEARCH, + }); + await AssertNoPrivateResult(window); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: UrlbarTokenizer.RESTRICT.SEARCH + " ", + }); + await AssertNoPrivateResult(window); + await UrlbarTestUtils.exitSearchMode(window); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: " " + UrlbarTokenizer.RESTRICT.SEARCH, + }); + await AssertNoPrivateResult(window); +}); + +add_task(async function test_restrict_search() { + info( + "Test that 'Search in a Private Window' has the right string with the restriction token" + ); + let engine = await Services.search.getDefaultPrivate(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: UrlbarTokenizer.RESTRICT.SEARCH + "test", + }); + let result = await AssertPrivateResult(window, engine, true); + Assert.equal(result.searchParams.query, "test"); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test" + UrlbarTokenizer.RESTRICT.SEARCH, + }); + result = await AssertPrivateResult(window, engine, true); + Assert.equal(result.searchParams.query, "test"); +}); diff --git a/browser/components/urlbar/tests/browser/browser_speculative_connect.js b/browser/components/urlbar/tests/browser/browser_speculative_connect.js new file mode 100644 index 0000000000..2b4d132812 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_speculative_connect.js @@ -0,0 +1,202 @@ +/* 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 test ensures that we setup a speculative network connection to +// the site in various cases: +// 1. search engine if it's the first result +// 2. mousedown event before the http request happens(in mouseup). + +const TEST_ENGINE_BASENAME = "searchSuggestionEngine2.xml"; + +const serverInfo = { + scheme: "http", + host: "localhost", + port: 20709, // Must be identical to what is in searchSuggestionEngine2.xml +}; + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.autoFill", true], + ["browser.urlbar.suggest.searches", true], + ["browser.urlbar.speculativeConnect.enabled", true], + // In mochitest this number is 0 by default but we have to turn it on. + ["network.http.speculative-parallel-limit", 6], + // The http server is using IPv4, so it's better to disable IPv6 to avoid + // weird networking problem. + ["network.dns.disableIPv6", true], + ], + }); + + // Ensure we start from a clean situation. + await PlacesUtils.history.clear(); + + await PlacesTestUtils.addVisits([ + { + uri: `${serverInfo.scheme}://${serverInfo.host}:${serverInfo.port}`, + title: "test visit for speculative connection", + transition: Ci.nsINavHistoryService.TRANSITION_TYPED, + }, + ]); + + let engine = await SearchTestUtils.promiseNewSearchEngine( + getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME + ); + let oldCurrentEngine = Services.search.defaultEngine; + Services.search.defaultEngine = engine; + + registerCleanupFunction(async function() { + await PlacesUtils.history.clear(); + Services.search.defaultEngine = oldCurrentEngine; + }); +}); + +add_task(async function search_test() { + // We speculative connect to the search engine only if suggestions are enabled. + await SpecialPowers.pushPrefEnv({ + set: [["browser.search.suggest.enabled", true]], + }); + await withHttpServer(serverInfo, async server => { + let connectionNumber = server.connectionNumber; + info("Searching for 'foo'"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "foo", + fireInputEvent: true, + }); + // Check if the first result is with type "searchengine" + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + details.type, + UrlbarUtils.RESULT_TYPE.SEARCH, + "The first result is a search" + ); + await UrlbarTestUtils.promiseSpeculativeConnections( + server, + connectionNumber + 1 + ); + }); +}); + +add_task(async function popup_mousedown_test() { + // Disable search suggestions and autofill, to avoid other speculative + // connections. + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.search.suggest.enabled", false], + ["browser.urlbar.autoFill", false], + ], + }); + await withHttpServer(serverInfo, async server => { + let connectionNumber = server.connectionNumber; + let searchString = "ocal"; + let completeValue = `${serverInfo.scheme}://${serverInfo.host}:${serverInfo.port}/`; + info(`Searching for '${searchString}'`); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: searchString, + fireInputEvent: true, + }); + let listitem = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal( + details.url, + completeValue, + "The second item has the url we visited." + ); + + info("Clicking on the second result"); + EventUtils.synthesizeMouseAtCenter(listitem, { type: "mousedown" }, window); + Assert.equal( + UrlbarTestUtils.getSelectedRow(window), + listitem, + "The second item is selected" + ); + await UrlbarTestUtils.promiseSpeculativeConnections( + server, + connectionNumber + 1 + ); + }); +}); + +add_task(async function test_autofill() { + // Disable search suggestions but enable autofill. + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.search.suggest.enabled", false], + ["browser.urlbar.autoFill", true], + ], + }); + await withHttpServer(serverInfo, async server => { + let connectionNumber = server.connectionNumber; + let searchString = serverInfo.host; + info(`Searching for '${searchString}'`); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: searchString, + fireInputEvent: true, + }); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + let completeValue = `${serverInfo.scheme}://${serverInfo.host}:${serverInfo.port}/`; + Assert.equal(details.url, completeValue, `Autofilled value is as expected`); + await UrlbarTestUtils.promiseSpeculativeConnections( + server, + connectionNumber + 1 + ); + }); +}); + +add_task(async function test_autofill_privateContext() { + info("Autofill in private context."); + let privateWin = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + registerCleanupFunction(async () => { + let promisePBExit = TestUtils.topicObserved("last-pb-context-exited"); + await BrowserTestUtils.closeWindow(privateWin); + await promisePBExit; + }); + await withHttpServer(serverInfo, async server => { + let connectionNumber = server.connectionNumber; + let searchString = serverInfo.host; + info(`Searching for '${searchString}'`); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: privateWin, + value: searchString, + fireInputEvent: true, + }); + let details = await UrlbarTestUtils.getDetailsOfResultAt(privateWin, 0); + let completeValue = `${serverInfo.scheme}://${serverInfo.host}:${serverInfo.port}/`; + Assert.equal(details.url, completeValue, `Autofilled value is as expected`); + await UrlbarTestUtils.promiseSpeculativeConnections( + server, + connectionNumber + ); + }); +}); + +add_task(async function test_no_heuristic_result() { + info("Don't speculative connect on results addition if there's no heuristic"); + await withHttpServer(serverInfo, async server => { + let connectionNumber = server.connectionNumber; + info(`Searching for the empty string`); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + fireInputEvent: true, + }); + ok(UrlbarTestUtils.getResultCount(window) > 0, "Has results"); + let result = await UrlbarTestUtils.getSelectedRow(window); + Assert.strictEqual(result, null, `Should have no selection`); + await UrlbarTestUtils.promiseSpeculativeConnections( + server, + connectionNumber + ); + }); +}); diff --git a/browser/components/urlbar/tests/browser/browser_speculative_connect_not_with_client_cert.js b/browser/components/urlbar/tests/browser/browser_speculative_connect_not_with_client_cert.js new file mode 100644 index 0000000000..958a239ec5 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_speculative_connect_not_with_client_cert.js @@ -0,0 +1,219 @@ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ +/* 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"; + +// Tests that we don't speculatively connect when user certificates are installed + +const { MockRegistrar } = ChromeUtils.import( + "resource://testing-common/MockRegistrar.jsm" +); + +const certService = Cc["@mozilla.org/security/local-cert-service;1"].getService( + Ci.nsILocalCertService +); +const certOverrideService = Cc[ + "@mozilla.org/security/certoverride;1" +].getService(Ci.nsICertOverrideService); + +const host = "localhost"; +let uri; +let handshakeDone = false; +let expectingChooseCertificate = false; +let chooseCertificateCalled = false; + +const clientAuthDialogs = { + chooseCertificate( + hostname, + port, + organization, + issuerOrg, + certList, + selectedIndex, + rememberClientAuthCertificate + ) { + ok( + expectingChooseCertificate, + `${ + expectingChooseCertificate ? "" : "not " + }expecting chooseCertificate to be called` + ); + is(certList.length, 1, "should have only one client certificate available"); + selectedIndex.value = 0; + rememberClientAuthCertificate.value = false; + chooseCertificateCalled = true; + return true; + }, + + QueryInterface: ChromeUtils.generateQI(["nsIClientAuthDialogs"]), +}; + +function startServer(cert) { + let tlsServer = Cc["@mozilla.org/network/tls-server-socket;1"].createInstance( + Ci.nsITLSServerSocket + ); + tlsServer.init(-1, true, -1); + tlsServer.serverCert = cert; + + let input, output; + + let listener = { + onSocketAccepted(socket, transport) { + info("Accepted TLS client connection"); + let connectionInfo = transport.securityInfo.QueryInterface( + Ci.nsITLSServerConnectionInfo + ); + connectionInfo.setSecurityObserver(listener); + input = transport.openInputStream(0, 0, 0); + output = transport.openOutputStream(0, 0, 0); + }, + + onHandshakeDone(socket, status) { + info("TLS handshake done"); + handshakeDone = true; + + input.asyncWait( + { + onInputStreamReady(readyInput) { + try { + let request = NetUtil.readInputStreamToString( + readyInput, + readyInput.available() + ); + ok( + request.startsWith("GET /") && request.includes("HTTP/1.1"), + "expecting an HTTP/1.1 GET request" + ); + let response = + "HTTP/1.1 200 OK\r\nContent-Type:text/plain\r\n" + + "Connection:Close\r\nContent-Length:2\r\n\r\nOK"; + output.write(response, response.length); + } catch (e) { + // This will fail when we close the speculative connection. + } + }, + }, + 0, + 0, + Services.tm.currentThread + ); + }, + + onStopListening() { + info("onStopListening"); + input.close(); + output.close(); + }, + }; + + tlsServer.setSessionTickets(false); + tlsServer.setRequestClientCertificate(Ci.nsITLSServerSocket.REQUEST_ALWAYS); + + tlsServer.asyncListen(listener); + + return tlsServer; +} + +let server; + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.autoFill", true], + // Turn off search suggestion so we won't speculative connect to the search engine. + ["browser.search.suggest.enabled", false], + ["browser.urlbar.speculativeConnect.enabled", true], + // In mochitest this number is 0 by default but we have to turn it on. + ["network.http.speculative-parallel-limit", 6], + // The http server is using IPv4, so it's better to disable IPv6 to avoid weird + // networking problem. + ["network.dns.disableIPv6", true], + ["security.default_personal_cert", "Ask Every Time"], + ], + }); + + let clientAuthDialogsCID = MockRegistrar.register( + "@mozilla.org/nsClientAuthDialogs;1", + clientAuthDialogs + ); + + let cert = await new Promise((resolve, reject) => { + certService.getOrCreateCert("speculative-connect", { + handleCert(c, rv) { + if (!Components.isSuccessCode(rv)) { + reject(rv); + return; + } + resolve(c); + }, + }); + }); + server = startServer(cert); + uri = `https://${host}:${server.port}/`; + info(`running tls server at ${uri}`); + await PlacesTestUtils.addVisits([ + { + uri, + title: "test visit for speculative connection", + transition: Ci.nsINavHistoryService.TRANSITION_TYPED, + }, + ]); + + let overrideBits = + Ci.nsICertOverrideService.ERROR_UNTRUSTED | + Ci.nsICertOverrideService.ERROR_MISMATCH; + certOverrideService.rememberValidityOverride( + "localhost", + server.port, + cert, + overrideBits, + true + ); + + registerCleanupFunction(async function() { + await PlacesUtils.history.clear(); + MockRegistrar.unregister(clientAuthDialogsCID); + certOverrideService.clearValidityOverride("localhost", server.port); + }); +}); + +add_task( + async function popup_mousedown_no_client_cert_dialog_until_navigate_test() { + // To not trigger autofill, search keyword starts from the second character. + let searchString = host.substr(1, 4); + let completeValue = uri; + info(`Searching for '${searchString}'`); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: searchString, + fireInputEvent: true, + }); + let listitem = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1); + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + info(`The url of the second item is ${details.url}`); + is(details.url, completeValue, "The second item has the url we visited."); + + expectingChooseCertificate = false; + EventUtils.synthesizeMouseAtCenter(listitem, { type: "mousedown" }, window); + is( + UrlbarTestUtils.getSelectedRow(window), + listitem, + "The second item is selected" + ); + + // We shouldn't have triggered a speculative connection, because a client + // certificate is installed. + SimpleTest.requestFlakyTimeout("Wait for UI"); + await new Promise(resolve => setTimeout(resolve, 200)); + + // Now mouseup, expect that we choose a client certificate, and expect that + // we successfully load a page. + expectingChooseCertificate = true; + EventUtils.synthesizeMouseAtCenter(listitem, { type: "mouseup" }, window); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + ok(chooseCertificateCalled, "chooseCertificate must have been called"); + server.close(); + } +); diff --git a/browser/components/urlbar/tests/browser/browser_stop.js b/browser/components/urlbar/tests/browser/browser_stop.js new file mode 100644 index 0000000000..1bc011a31c --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_stop.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This tests ensures the urlbar reflects the correct value if a page load is + * stopped immediately after loading. + */ + +"use strict"; + +const goodURL = "http://mochi.test:8888/"; +const badURL = "http://mochi.test:8888/whatever.html"; + +add_task(async function() { + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, goodURL); + await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser); + is( + gURLBar.value, + BrowserUtils.trimURL(goodURL), + "location bar reflects loaded page" + ); + + await typeAndSubmitAndStop(badURL); + is( + gURLBar.value, + BrowserUtils.trimURL(goodURL), + "location bar reflects loaded page after stop()" + ); + gBrowser.removeCurrentTab(); + + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank"); + is(gURLBar.value, "", "location bar is empty"); + + await typeAndSubmitAndStop(badURL); + is( + gURLBar.value, + BrowserUtils.trimURL(badURL), + "location bar reflects stopped page in an empty tab" + ); + gBrowser.removeCurrentTab(); +}); + +async function typeAndSubmitAndStop(url) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: url, + fireInputEvent: true, + }); + + let docLoadPromise = BrowserTestUtils.waitForDocLoadAndStopIt( + url, + gBrowser.selectedBrowser + ); + + // When the load is stopped, tabbrowser calls gURLBar.setURI and then calls + // onStateChange on its progress listeners. So to properly wait until the + // urlbar value has been updated, add our own progress listener here. + let progressPromise = new Promise(resolve => { + let listener = { + onStateChange(browser, webProgress, request, stateFlags, status) { + if ( + webProgress.isTopLevel && + stateFlags & Ci.nsIWebProgressListener.STATE_STOP + ) { + gBrowser.removeTabsProgressListener(listener); + resolve(); + } + }, + }; + gBrowser.addTabsProgressListener(listener); + }); + + gURLBar.handleCommand(); + await Promise.all([docLoadPromise, progressPromise]); +} diff --git a/browser/components/urlbar/tests/browser/browser_stopSearchOnSelection.js b/browser/components/urlbar/tests/browser/browser_stopSearchOnSelection.js new file mode 100644 index 0000000000..3fc05a196f --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_stopSearchOnSelection.js @@ -0,0 +1,115 @@ +/* eslint-disable mozilla/no-arbitrary-setTimeout */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This tests that when a search is stopped due to the user selecting a result, + * the view doesn't update after that. + */ + +"use strict"; + +const TEST_ENGINE_BASENAME = "searchSuggestionEngineSlow.xml"; + +// This should match the `timeout` query param used in the suggestions URL in +// the test engine. +const TEST_ENGINE_SUGGESTIONS_TIMEOUT = 3000; + +// The number of suggestions returned by the test engine. +const TEST_ENGINE_NUM_EXPECTED_RESULTS = 2; + +add_task(async function init() { + await PlacesUtils.history.clear(); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.searches", true]], + }); + // Add a test search engine that returns suggestions on a delay. + let engine = await SearchTestUtils.promiseNewSearchEngine( + getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME + ); + let oldDefaultEngine = await Services.search.getDefault(); + await Services.search.moveEngine(engine, 0); + await Services.search.setDefault(engine); + registerCleanupFunction(async () => { + await Services.search.setDefault(oldDefaultEngine); + await PlacesUtils.history.clear(); + }); +}); + +add_task(async function mainTest() { + // Open a tab that will match the search string below so that we're guaranteed + // to have more than one result (the heuristic result) so that we can change + // the selected result. We open a tab instead of adding a page in history + // because open tabs are kept in a memory SQLite table, so open-tab results + // are more likely than history results to be fetched before our slow search + // suggestions. This is important when the test runs on slow debug builds on + // slow machines. + await BrowserTestUtils.withNewTab("http://example.com/", async () => { + await BrowserTestUtils.withNewTab("about:blank", async () => { + // Do an initial search. There should be 4 results: heuristic, open tab, + // and the two suggestions. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "amp", + }); + await TestUtils.waitForCondition(() => { + return ( + UrlbarTestUtils.getResultCount(window) == + 2 + TEST_ENGINE_NUM_EXPECTED_RESULTS + ); + }); + + // Type a character to start a new search. The new search should still + // match the open tab so that the open-tab result appears again. + EventUtils.synthesizeKey("l"); + + // There should be 2 results immediately: heuristic and open tab. + await TestUtils.waitForCondition(() => { + return UrlbarTestUtils.getResultCount(window) == 2; + }); + + // Before the search completes, change the selected result. Pressing only + // the down arrow key ends up selecting the first one-off on Linux debug + // builds on the infrastructure for some reason, so arrow back up to + // select the heuristic result again. The important thing is to change + // the selection. It doesn't matter which result ends up selected. + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_ArrowUp"); + + // Wait for the new search to complete. It should be canceled due to the + // selection change, but it should still complete. + await UrlbarTestUtils.promiseSearchComplete(window); + + // To make absolutely sure the suggestions don't appear after the search + // completes, wait a bit. + await new Promise(r => + setTimeout(r, 1 + TEST_ENGINE_SUGGESTIONS_TIMEOUT) + ); + + // The heuristic result should reflect the new search, "ampl". + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.SEARCH, + "Should have the correct result type" + ); + Assert.equal( + result.searchParams.query, + "ampl", + "Should have the correct query" + ); + + // None of the other results should be "ampl" suggestions, i.e., amplfoo + // and amplbar should not be in the results. + let count = UrlbarTestUtils.getResultCount(window); + for (let i = 1; i < count; i++) { + result = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + Assert.ok( + result.type != UrlbarUtils.RESULT_TYPE.SEARCH || + !["amplfoo", "amplbar"].includes(result.searchParams.suggestion), + "Suggestions should not contain the typed l char" + ); + } + }); + }); +}); diff --git a/browser/components/urlbar/tests/browser/browser_stop_pending.js b/browser/components/urlbar/tests/browser/browser_stop_pending.js new file mode 100644 index 0000000000..3b9c509f95 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_stop_pending.js @@ -0,0 +1,220 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-disable mozilla/no-arbitrary-setTimeout */ +"use strict"; + +const SLOW_PAGE = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://www.example.com" + ) + "slow-page.sjs"; +const SLOW_PAGE2 = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://mochi.test:8888" + ) + "slow-page.sjs?faster"; + +/** + * Check that if we: + * 1) have a loaded page + * 2) load a separate URL + * 3) before the URL for step 2 has finished loading, load a third URL + * we don't revert to the URL from (1). + */ +add_task(async function() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "http://example.com", + true, + true + ); + + let initialValue = gURLBar.untrimmedValue; + let expectedURLBarChange = SLOW_PAGE; + let sawChange = false; + let handler = () => { + isnot( + gURLBar.untrimmedValue, + initialValue, + "Should not revert URL bar value!" + ); + if (gURLBar.getAttribute("pageproxystate") == "valid") { + sawChange = true; + is( + gURLBar.untrimmedValue, + expectedURLBarChange, + "Should set expected URL bar value!" + ); + } + }; + + let obs = new MutationObserver(handler); + + obs.observe(gURLBar.textbox, { attributes: true }); + gURLBar.value = SLOW_PAGE; + gURLBar.handleCommand(); + + // If this ever starts going intermittent, we've broken this. + await new Promise(resolve => setTimeout(resolve, 200)); + expectedURLBarChange = SLOW_PAGE2; + let pageLoadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + gURLBar.value = expectedURLBarChange; + gURLBar.handleCommand(); + is( + gURLBar.untrimmedValue, + expectedURLBarChange, + "Should not have changed URL bar value synchronously." + ); + await pageLoadPromise; + ok( + sawChange, + "The URL bar change handler should have been called by the time the page was loaded" + ); + obs.disconnect(); + obs = null; + BrowserTestUtils.removeTab(tab); +}); + +/** + * Check that if we: + * 1) middle-click a link to a separate page whose server doesn't respond + * 2) we switch to that tab and stop the request + * + * The URL bar continues to contain the URL of the page we wanted to visit. + */ +add_task(async function() { + let socket = Cc["@mozilla.org/network/server-socket;1"].createInstance( + Ci.nsIServerSocket + ); + socket.init(-1, true, -1); + const PORT = socket.port; + registerCleanupFunction(() => { + socket.close(); + }); + + const BASE_PAGE = TEST_BASE_URL + "dummy_page.html"; + const SLOW_HOST = `https://localhost:${PORT}/`; + info("Using URLs: " + SLOW_HOST); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_PAGE); + info("opened tab"); + await SpecialPowers.spawn(tab.linkedBrowser, [SLOW_HOST], URL => { + let link = content.document.createElement("a"); + link.href = URL; + link.textContent = "click me to open a slow page"; + link.id = "clickme"; + content.document.body.appendChild(link); + }); + info("added link"); + let newTabPromise = BrowserTestUtils.waitForEvent( + gBrowser.tabContainer, + "TabOpen" + ); + // Middle click the link: + await BrowserTestUtils.synthesizeMouseAtCenter( + "#clickme", + { button: 1 }, + tab.linkedBrowser + ); + // get new tab, switch to it + let newTab = (await newTabPromise).target; + await BrowserTestUtils.switchTab(gBrowser, newTab); + is(gURLBar.untrimmedValue, SLOW_HOST, "Should have slow page in URL bar"); + let browserStoppedPromise = BrowserTestUtils.browserStopped( + newTab.linkedBrowser, + null, + true + ); + BrowserStop(); + await browserStoppedPromise; + + is( + gURLBar.untrimmedValue, + SLOW_HOST, + "Should still have slow page in URL bar after stop" + ); + BrowserTestUtils.removeTab(newTab); + BrowserTestUtils.removeTab(tab); +}); +/** + * Check that if we: + * 1) middle-click a link to a separate page whose server doesn't respond + * 2) we alter the URL on that page to some other server that doesn't respond + * 3) we stop the request + * + * The URL bar continues to contain the second URL. + */ +add_task(async function() { + let socket = Cc["@mozilla.org/network/server-socket;1"].createInstance( + Ci.nsIServerSocket + ); + socket.init(-1, true, -1); + const PORT1 = socket.port; + let socket2 = Cc["@mozilla.org/network/server-socket;1"].createInstance( + Ci.nsIServerSocket + ); + socket2.init(-1, true, -1); + const PORT2 = socket2.port; + registerCleanupFunction(() => { + socket.close(); + socket2.close(); + }); + + const BASE_PAGE = TEST_BASE_URL + "dummy_page.html"; + const SLOW_HOST1 = `https://localhost:${PORT1}/`; + const SLOW_HOST2 = `https://localhost:${PORT2}/`; + info("Using URLs: " + SLOW_HOST1 + " and " + SLOW_HOST2); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, BASE_PAGE); + info("opened tab"); + await SpecialPowers.spawn(tab.linkedBrowser, [SLOW_HOST1], URL => { + let link = content.document.createElement("a"); + link.href = URL; + link.textContent = "click me to open a slow page"; + link.id = "clickme"; + content.document.body.appendChild(link); + }); + info("added link"); + let newTabPromise = BrowserTestUtils.waitForEvent( + gBrowser.tabContainer, + "TabOpen" + ); + // Middle click the link: + await BrowserTestUtils.synthesizeMouseAtCenter( + "#clickme", + { button: 1 }, + tab.linkedBrowser + ); + // get new tab, switch to it + let newTab = (await newTabPromise).target; + await BrowserTestUtils.switchTab(gBrowser, newTab); + is(gURLBar.untrimmedValue, SLOW_HOST1, "Should have slow page in URL bar"); + let browserStoppedPromise = BrowserTestUtils.browserStopped( + newTab.linkedBrowser, + null, + true + ); + gURLBar.value = SLOW_HOST2; + gURLBar.handleCommand(); + await browserStoppedPromise; + + is( + gURLBar.untrimmedValue, + SLOW_HOST2, + "Should have second slow page in URL bar" + ); + browserStoppedPromise = BrowserTestUtils.browserStopped( + newTab.linkedBrowser, + null, + true + ); + BrowserStop(); + await browserStoppedPromise; + + is( + gURLBar.untrimmedValue, + SLOW_HOST2, + "Should still have second slow page in URL bar after stop" + ); + BrowserTestUtils.removeTab(newTab); + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_suggestedIndex.js b/browser/components/urlbar/tests/browser/browser_suggestedIndex.js new file mode 100644 index 0000000000..563202036a --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_suggestedIndex.js @@ -0,0 +1,120 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that results with a suggestedIndex property end up in the expected +// position. + +add_task(async function suggestedIndex() { + let result1 = new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://mozilla.org/1" } + ); + result1.suggestedIndex = 2; + let result2 = new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://mozilla.org/2" } + ); + result2.suggestedIndex = 6; + + let provider = new UrlbarTestUtils.TestProvider({ + results: [result1, result2], + }); + UrlbarProvidersManager.registerProvider(provider); + async function clean() { + UrlbarProvidersManager.unregisterProvider(provider); + await PlacesUtils.history.clear(); + } + registerCleanupFunction(clean); + + let urls = []; + let maxResults = UrlbarPrefs.get("maxRichResults"); + // Add more results, so that the sum of these results plus the above ones, + // will be greater than maxResults. + for (let i = 0; i < maxResults; ++i) { + urls.push("http://example.com/foo" + i); + } + await PlacesTestUtils.addVisits(urls); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "foo", + }); + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + maxResults, + `There should be ${maxResults} results in the view.` + ); + + urls.reverse(); + urls.unshift( + (await Services.search.getDefault()).getSubmission("foo").uri.spec + ); + urls.splice(result1.suggestedIndex, 0, result1.payload.url); + urls.splice(result2.suggestedIndex, 0, result2.payload.url); + urls = urls.slice(0, maxResults); + + let expected = []; + for (let i = 0; i < maxResults; ++i) { + let url = (await UrlbarTestUtils.getDetailsOfResultAt(window, i)).url; + expected.push(url); + } + // Check all the results. + Assert.deepEqual(expected, urls); + + await clean(); + await UrlbarTestUtils.promisePopupClose(window); +}); + +add_task(async function suggestedIndex_append() { + // When suggestedIndex is greater than the number of results the result is + // appended. + let result = new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://mozilla.org/append/" } + ); + result.suggestedIndex = 4; + + let provider = new UrlbarTestUtils.TestProvider({ results: [result] }); + UrlbarProvidersManager.registerProvider(provider); + async function clean() { + UrlbarProvidersManager.unregisterProvider(provider); + await PlacesUtils.history.clear(); + } + registerCleanupFunction(clean); + + await PlacesTestUtils.addVisits("http://example.com/bar"); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "bar", + }); + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 3, + `There should be 3 results in the view.` + ); + + let urls = [ + (await Services.search.getDefault()).getSubmission("bar").uri.spec, + "http://example.com/bar", + "http://mozilla.org/append/", + ]; + + let expected = []; + for (let i = 0; i < 3; ++i) { + let url = (await UrlbarTestUtils.getDetailsOfResultAt(window, i)).url; + expected.push(url); + } + // Check all the results. + Assert.deepEqual(expected, urls); + + await clean(); + await UrlbarTestUtils.promisePopupClose(window); +}); diff --git a/browser/components/urlbar/tests/browser/browser_switchTab_closesUrlbarPopup.js b/browser/components/urlbar/tests/browser/browser_switchTab_closesUrlbarPopup.js new file mode 100644 index 0000000000..d172176494 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_switchTab_closesUrlbarPopup.js @@ -0,0 +1,42 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * Checks that switching tabs closes the urlbar popup. + */ + +"use strict"; + +add_task(async function() { + let tab1 = BrowserTestUtils.addTab(gBrowser); + let tab2 = BrowserTestUtils.addTab(gBrowser); + + registerCleanupFunction(async () => { + await PlacesUtils.history.clear(); + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); + }); + + // Add a couple of dummy entries to ensure the history popup will open. + await PlacesTestUtils.addVisits([ + { uri: makeURI("http://example.com/foo") }, + { uri: makeURI("http://example.com/foo/bar") }, + ]); + + // When urlbar in a new tab is focused, and a tab switch occurs, + // the urlbar popup should be closed + await BrowserTestUtils.switchTab(gBrowser, tab2); + gURLBar.focus(); // focus the urlbar in the tab we will switch to + await BrowserTestUtils.switchTab(gBrowser, tab1); + // Now open the popup. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + // Check that the popup closes when we switch tab. + await UrlbarTestUtils.promisePopupClose(window, () => { + return BrowserTestUtils.switchTab(gBrowser, tab2); + }); + Assert.ok(true, "Popup was successfully closed"); +}); diff --git a/browser/components/urlbar/tests/browser/browser_switchTab_currentTab.js b/browser/components/urlbar/tests/browser/browser_switchTab_currentTab.js new file mode 100644 index 0000000000..eccee800e3 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_switchTab_currentTab.js @@ -0,0 +1,41 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This test ensures that switch to tab still works when the URI contains an + * encoded part. + */ + +"use strict"; + +add_task(async function test_switchTab_currentTab() { + registerCleanupFunction(PlacesUtils.history.clear); + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:robots#1" }, + async () => { + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:robots#2" }, + async () => { + let context = await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "robot", + }); + Assert.ok( + context.results.some( + result => + result.type == UrlbarUtils.RESULT_TYPE.TAB_SWITCH && + result.payload.url == "about:robots#1" + ) + ); + Assert.ok( + !context.results.some( + result => + result.type == UrlbarUtils.RESULT_TYPE.TAB_SWITCH && + result.payload.url == "about:robots#2" + ) + ); + } + ); + } + ); +}); diff --git a/browser/components/urlbar/tests/browser/browser_switchTab_decodeuri.js b/browser/components/urlbar/tests/browser/browser_switchTab_decodeuri.js new file mode 100644 index 0000000000..fe23eceaf9 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_switchTab_decodeuri.js @@ -0,0 +1,51 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This test ensures that switch to tab still works when the URI contains an + * encoded part. + */ + +"use strict"; + +const TEST_URL = `${TEST_BASE_URL}dummy_page.html#test%7C1`; + +add_task(async function test_switchtab_decodeuri() { + info("Opening first tab"); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + info("Opening and selecting second tab"); + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + + info("Wait for autocomplete"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "dummy_page", + }); + + info("Select autocomplete popup entry"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + let result = await UrlbarTestUtils.getDetailsOfResultAt( + window, + UrlbarTestUtils.getSelectedRowIndex(window) + ); + Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.TAB_SWITCH); + + info("switch-to-tab"); + let tabSelectPromise = BrowserTestUtils.waitForEvent( + window, + "TabSelect", + false + ); + EventUtils.synthesizeKey("KEY_Enter"); + await tabSelectPromise; + + Assert.equal( + gBrowser.selectedTab, + tab, + "Should have switched to the right tab" + ); + + gBrowser.removeCurrentTab(); + await PlacesUtils.history.clear(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_switchTab_override.js b/browser/components/urlbar/tests/browser/browser_switchTab_override.js new file mode 100644 index 0000000000..dabce43612 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_switchTab_override.js @@ -0,0 +1,100 @@ +/* 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/. */ + +/** + * This test ensures that overriding switch-to-tab correctly loads the page + * rather than switching to it. + */ + +"use strict"; + +const TEST_URL = `${TEST_BASE_URL}dummy_page.html`; + +add_task(async function test_switchtab_override() { + info("Opening first tab"); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + + info("Opening and selecting second tab"); + let secondTab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + registerCleanupFunction(() => { + try { + gBrowser.removeTab(tab); + gBrowser.removeTab(secondTab); + } catch (ex) { + /* tabs may have already been closed in case of failure */ + } + }); + + info("Wait for autocomplete"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "dummy_page", + }); + + info("Select second autocomplete popup entry"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + let result = await UrlbarTestUtils.getDetailsOfResultAt( + window, + UrlbarTestUtils.getSelectedRowIndex(window) + ); + Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.TAB_SWITCH); + + // Check to see if the switchtab label is visible and + // all other labels are hidden + const allLabels = document.getElementById("urlbar-label-box").children; + for (let label of allLabels) { + if (label.id == "urlbar-label-switchtab") { + Assert.ok(BrowserTestUtils.is_visible(label)); + } else { + Assert.ok(BrowserTestUtils.is_hidden(label)); + } + } + + info("Override switch-to-tab"); + let deferred = PromiseUtils.defer(); + // In case of failure this would switch tab. + let onTabSelect = event => { + deferred.reject(new Error("Should have overridden switch to tab")); + }; + gBrowser.tabContainer.addEventListener("TabSelect", onTabSelect); + registerCleanupFunction(() => { + gBrowser.tabContainer.removeEventListener("TabSelect", onTabSelect); + }); + // Otherwise it would load the page. + BrowserTestUtils.browserLoaded(secondTab.linkedBrowser).then( + deferred.resolve + ); + + EventUtils.synthesizeKey("KEY_Shift", { type: "keydown" }); + + // Checks that all labels are hidden when Shift is held down on the SwitchToTab result + for (let label of allLabels) { + Assert.ok(BrowserTestUtils.is_hidden(label)); + } + + registerCleanupFunction(() => { + // Avoid confusing next tests by leaving a pending keydown. + EventUtils.synthesizeKey("KEY_Shift", { type: "keyup" }); + }); + + let attribute = "actionoverride"; + Assert.ok( + gURLBar.view.panel.hasAttribute(attribute), + "We should be overriding" + ); + + EventUtils.synthesizeKey("KEY_Enter"); + info(`gURLBar.value = ${gURLBar.value}`); + await deferred.promise; + + // Blurring the urlbar should have cleared the override. + Assert.ok( + !gURLBar.view.panel.hasAttribute(attribute), + "We should not be overriding anymore" + ); + + await PlacesUtils.history.clear(); + gBrowser.removeTab(tab); + gBrowser.removeTab(secondTab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_switchToTabHavingURI_aOpenParams.js b/browser/components/urlbar/tests/browser/browser_switchToTabHavingURI_aOpenParams.js new file mode 100644 index 0000000000..6f9722776b --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_switchToTabHavingURI_aOpenParams.js @@ -0,0 +1,216 @@ +/* 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/. */ + +add_task(async function test_ignoreFragment() { + let tabRefAboutHome = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:home#1" + ); + await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:mozilla"); + let numTabsAtStart = gBrowser.tabs.length; + + switchTab("about:home#1", true); + switchTab("about:mozilla", true); + + let hashChangePromise = ContentTask.spawn( + tabRefAboutHome.linkedBrowser, + [], + async function() { + await ContentTaskUtils.waitForEvent(this, "hashchange", true); + } + ); + switchTab("about:home#2", true, { + ignoreFragment: "whenComparingAndReplace", + }); + is( + tabRefAboutHome, + gBrowser.selectedTab, + "The same about:home tab should be switched to" + ); + await hashChangePromise; + is(gBrowser.currentURI.ref, "2", "The ref should be updated to the new ref"); + switchTab("about:mozilla", true); + switchTab("about:home#3", true, { ignoreFragment: "whenComparing" }); + is( + tabRefAboutHome, + gBrowser.selectedTab, + "The same about:home tab should be switched to" + ); + is( + gBrowser.currentURI.ref, + "2", + "The ref should be unchanged since the fragment is only ignored when comparing" + ); + switchTab("about:mozilla", true); + switchTab("about:home#1", false); + isnot( + tabRefAboutHome, + gBrowser.selectedTab, + "Selected tab should not be initial about:blank tab" + ); + is( + gBrowser.tabs.length, + numTabsAtStart + 1, + "Should have one new tab opened" + ); + switchTab("about:mozilla", true); + switchTab("about:home", true, { ignoreFragment: "whenComparingAndReplace" }); + await BrowserTestUtils.waitForCondition(function() { + return tabRefAboutHome.linkedBrowser.currentURI.spec == "about:home"; + }); + is( + tabRefAboutHome.linkedBrowser.currentURI.spec, + "about:home", + "about:home shouldn't have hash" + ); + switchTab("about:about", false, { + ignoreFragment: "whenComparingAndReplace", + }); + cleanupTestTabs(); +}); + +add_task(async function test_ignoreQueryString() { + let tabRefAboutHome = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:home?hello=firefox" + ); + await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:mozilla"); + + switchTab("about:home?hello=firefox", true); + switchTab("about:home?hello=firefoxos", false); + // Remove the last opened tab to test ignoreQueryString option. + gBrowser.removeCurrentTab(); + switchTab("about:home?hello=firefoxos", true, { ignoreQueryString: true }); + is( + tabRefAboutHome, + gBrowser.selectedTab, + "Selected tab should be the initial about:home tab" + ); + is( + gBrowser.currentURI.spec, + "about:home?hello=firefox", + "The spec should NOT be updated to the new query string" + ); + cleanupTestTabs(); +}); + +add_task(async function test_replaceQueryString() { + let tabRefAboutHome = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:home?hello=firefox" + ); + await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:mozilla"); + + switchTab("about:home", false); + switchTab("about:home?hello=firefox", true); + switchTab("about:home?hello=firefoxos", false); + // Remove the last opened tab to test replaceQueryString option. + gBrowser.removeCurrentTab(); + switchTab("about:home?hello=firefoxos", true, { replaceQueryString: true }); + is( + tabRefAboutHome, + gBrowser.selectedTab, + "Selected tab should be the initial about:home tab" + ); + // Wait for the tab to load the new URI spec. + await BrowserTestUtils.browserLoaded(tabRefAboutHome.linkedBrowser); + is( + gBrowser.currentURI.spec, + "about:home?hello=firefoxos", + "The spec should be updated to the new spec" + ); + cleanupTestTabs(); +}); + +add_task(async function test_replaceQueryStringAndFragment() { + let tabRefAboutHome = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:home?hello=firefox#aaa" + ); + let tabRefAboutMozilla = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:mozilla?hello=firefoxos#aaa" + ); + + switchTab("about:home", false); + gBrowser.removeCurrentTab(); + switchTab("about:home?hello=firefox#aaa", true); + is( + tabRefAboutHome, + gBrowser.selectedTab, + "Selected tab should be the initial about:home tab" + ); + switchTab("about:mozilla?hello=firefox#bbb", true, { + replaceQueryString: true, + ignoreFragment: "whenComparingAndReplace", + }); + is( + tabRefAboutMozilla, + gBrowser.selectedTab, + "Selected tab should be the initial about:mozilla tab" + ); + switchTab("about:home?hello=firefoxos#bbb", true, { + ignoreQueryString: true, + ignoreFragment: "whenComparingAndReplace", + }); + is( + tabRefAboutHome, + gBrowser.selectedTab, + "Selected tab should be the initial about:home tab" + ); + cleanupTestTabs(); +}); + +add_task(async function test_ignoreQueryStringIgnoresFragment() { + let tabRefAboutHome = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:home?hello=firefox#aaa" + ); + await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:mozilla?hello=firefoxos#aaa" + ); + + switchTab("about:home?hello=firefox#bbb", false, { ignoreQueryString: true }); + gBrowser.removeCurrentTab(); + switchTab("about:home?hello=firefoxos#aaa", true, { + ignoreQueryString: true, + }); + is( + tabRefAboutHome, + gBrowser.selectedTab, + "Selected tab should be the initial about:home tab" + ); + cleanupTestTabs(); +}); + +// Begin helpers + +function cleanupTestTabs() { + while (gBrowser.tabs.length > 1) { + gBrowser.removeCurrentTab(); + } +} + +function switchTab(aURI, aShouldFindExistingTab, aOpenParams = {}) { + // Build the description before switchToTabHavingURI deletes the object properties. + let msg = + `Should switch to existing ${aURI} tab if one existed, ` + + `${ + aOpenParams.ignoreFragment ? "ignoring" : "including" + } fragment portion, `; + if (aOpenParams.replaceQueryString) { + msg += "replacing"; + } else if (aOpenParams.ignoreQueryString) { + msg += "ignoring"; + } else { + msg += "including"; + } + msg += " query string."; + aOpenParams.triggeringPrincipal = Services.scriptSecurityManager.getSystemPrincipal(); + let tabFound = switchToTabHavingURI(aURI, true, aOpenParams); + is(tabFound, aShouldFindExistingTab, msg); +} + +registerCleanupFunction(cleanupTestTabs); diff --git a/browser/components/urlbar/tests/browser/browser_switchToTab_closes_newtab.js b/browser/components/urlbar/tests/browser/browser_switchToTab_closes_newtab.js new file mode 100644 index 0000000000..5031491d7e --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_switchToTab_closes_newtab.js @@ -0,0 +1,63 @@ +/* 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/. */ + +/** + * This tests that switch to tab from a blank tab switches and then closes + * the blank tab. + */ + +"use strict"; + +add_task(async function test_switchToTab_closes() { + let testURL = + "http://example.org/browser/browser/components/urlbar/tests/browser/dummy_page.html"; + + // Open the base tab + let baseTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, testURL); + + if (baseTab.linkedBrowser.currentURI.spec == "about:blank") { + return; + } + + // Open a blank tab to start the test from. + let testTab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + // Functions for TabClose and TabSelect + let tabClosePromise = BrowserTestUtils.waitForEvent( + gBrowser.tabContainer, + "TabClose", + false, + event => { + return event.originalTarget == testTab; + } + ); + let tabSelectPromise = BrowserTestUtils.waitForEvent( + gBrowser.tabContainer, + "TabSelect", + false, + event => { + return event.originalTarget == baseTab; + } + ); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "dummy", + }); + + // The first result is the heuristic, the second will be the switch to tab. + let element = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1); + EventUtils.synthesizeMouseAtCenter(element, {}, window); + + await Promise.all([tabSelectPromise, tabClosePromise]); + + // Confirm that the selected tab is now the base tab + Assert.equal( + gBrowser.selectedTab, + baseTab, + "Should have switched to the correct tab" + ); + + gBrowser.removeTab(baseTab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_switchToTab_fullUrl_repeatedKeydown.js b/browser/components/urlbar/tests/browser/browser_switchToTab_fullUrl_repeatedKeydown.js new file mode 100644 index 0000000000..8f80ac5841 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_switchToTab_fullUrl_repeatedKeydown.js @@ -0,0 +1,60 @@ +/* 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/. */ + +/** + * This tests that typing a url and picking a switch to tab actually switches + * to the right tab. Also tests repeated keydown/keyup events don't confuse + * override. + */ + +"use strict"; + +add_task(async function test_switchToTab_url() { + const TEST_URL = "https://example.org/browser/"; + + let baseTab = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL); + let testTab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + // Functions for TabClose and TabSelect + let tabClosePromise = BrowserTestUtils.waitForEvent( + gBrowser.tabContainer, + "TabClose", + false, + event => event.target == testTab + ); + let tabSelectPromise = BrowserTestUtils.waitForEvent( + gBrowser.tabContainer, + "TabSelect", + false, + event => event.target == baseTab + ); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TEST_URL, + fireInputEvent: true, + }); + // The first result is the heuristic, the second will be the switch to tab. + await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1); + + // Simulate a long press, on some platforms (Windows) it can generate multiple + // keydown events. + EventUtils.synthesizeKey("VK_SHIFT", { type: "keydown", repeat: 3 }); + EventUtils.synthesizeKey("VK_SHIFT", { type: "keyup" }); + + // Pick the switch to tab result. + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_Enter"); + + await Promise.all([tabSelectPromise, tabClosePromise]); + + // Confirm that the selected tab is now the base tab + Assert.equal( + gBrowser.selectedTab, + baseTab, + "Should have switched to the correct tab" + ); + + gBrowser.removeTab(baseTab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_tabKeyBehavior.js b/browser/components/urlbar/tests/browser/browser_tabKeyBehavior.js new file mode 100644 index 0000000000..d08d129c20 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_tabKeyBehavior.js @@ -0,0 +1,322 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This test makes sure that the tab key properly adjusts the selection or moves +// through toolbar items, depending on the urlbar state. +// When the view is open, tab should go through results if the urlbar was +// focused with the mouse, or has a typed string. + +"use strict"; + +add_task(async function init() { + for (let i = 0; i < UrlbarPrefs.get("maxRichResults"); i++) { + await PlacesTestUtils.addVisits("http://example.com/" + i); + } + + registerCleanupFunction(PlacesUtils.history.clear); +}); + +add_task(async function tabWithSearchString() { + info("Tab with a search string"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exam", + fireInputEvent: true, + }); + await expectTabThroughResults(); + info("Reverse Tab with a search string"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exam", + fireInputEvent: true, + }); + await expectTabThroughResults({ reverse: true }); +}); + +add_task(async function tabNoSearchString() { + info("Tab without a search string"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + fireInputEvent: true, + }); + await expectTabThroughToolbar(); + info("Reverse Tab without a search string"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + fireInputEvent: true, + }); + await expectTabThroughToolbar({ reverse: true }); +}); + +add_task(async function tabAfterBlur() { + info("Tab after closing the view"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exam", + fireInputEvent: true, + }); + await UrlbarTestUtils.promisePopupClose(window); + await expectTabThroughToolbar(); +}); + +add_task(async function tabNoSearchStringMouseFocus() { + info("Tab in a new blank tab after mouse focus"); + await BrowserTestUtils.withNewTab("about:blank", async () => { + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + await UrlbarTestUtils.promiseSearchComplete(window); + await expectTabThroughResults(); + }); + info("Tab in a loaded tab after mouse focus"); + await BrowserTestUtils.withNewTab("example.com", async () => { + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + await UrlbarTestUtils.promiseSearchComplete(window); + await expectTabThroughResults(); + }); +}); + +add_task(async function tabNoSearchStringKeyboardFocus() { + info("Tab in a new blank tab after keyboard focus"); + await BrowserTestUtils.withNewTab("about:blank", async () => { + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeKey("l", { accelKey: true }); + }); + await UrlbarTestUtils.promiseSearchComplete(window); + await expectTabThroughToolbar(); + }); + info("Tab in a loaded tab after keyboard focus"); + await BrowserTestUtils.withNewTab("example.com", async () => { + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeKey("l", { accelKey: true }); + }); + await UrlbarTestUtils.promiseSearchComplete(window); + await expectTabThroughToolbar(); + }); +}); + +add_task(async function tabRetainedResultMouseFocus() { + info("Tab after retained results with mouse focus"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exam", + fireInputEvent: true, + }); + await UrlbarTestUtils.promisePopupClose(window); + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + await UrlbarTestUtils.promiseSearchComplete(window); + await expectTabThroughResults(); +}); + +add_task(async function tabRetainedResultsKeyboardFocus() { + info("Tab after retained results with keyboard focus"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exam", + fireInputEvent: true, + }); + await UrlbarTestUtils.promisePopupClose(window); + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeKey("l", { accelKey: true }); + }); + await UrlbarTestUtils.promiseSearchComplete(window); + await expectTabThroughResults(); +}); + +add_task(async function tabRetainedResults() { + info("Tab with a search string after mouse focus."); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "exam", + fireInputEvent: true, + }); + await UrlbarTestUtils.promisePopupClose(window); + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + await expectTabThroughResults(); +}); + +add_task(async function tabSearchModePreview() { + info( + "Tab past a search mode preview keywordoffer after focusing with the keyboard." + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "@", + fireInputEvent: true, + }); + await UrlbarTestUtils.promisePopupClose(window); + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeKey("l", { accelKey: true }); + }); + await UrlbarTestUtils.promiseSearchComplete(window); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok( + result.searchParams.keyword, + "The first result is a keyword offer." + ); + + // Sanity check: the Urlbar value is cleared when keywordoffer results are + // selected. + EventUtils.synthesizeKey("KEY_ArrowDown"); + Assert.ok(!gURLBar.value, "The Urlbar should have no value."); + EventUtils.synthesizeKey("KEY_ArrowUp"); + + await expectTabThroughResults(); + + await UrlbarTestUtils.promisePopupClose(window, async () => { + gURLBar.blur(); + // Verify that blur closes search mode preview. + await UrlbarTestUtils.assertSearchMode(window, null); + }); +}); + +add_task(async function tabTabToSearch() { + info("Tab past a tab-to-search result after focusing with the keyboard."); + let engineDomain = "example.com"; + let testEngine = await Services.search.addEngineWithDetails("Test", { + template: `http://${engineDomain}/?search={searchTerms}`, + }); + for (let i = 0; i < 3; i++) { + await PlacesTestUtils.addVisits([`https://${engineDomain}/`]); + } + + // Search for a tab-to-search result. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: engineDomain.slice(0, 4), + }); + await UrlbarTestUtils.promisePopupClose(window); + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeKey("l", { accelKey: true }); + }); + await UrlbarTestUtils.promiseSearchComplete(window); + let tabToSearchResult = ( + await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1) + ).result; + Assert.equal( + tabToSearchResult.providerName, + "TabToSearch", + "The second result is a tab-to-search result." + ); + + await expectTabThroughResults(); + + await UrlbarTestUtils.promisePopupClose(window, async () => { + gURLBar.blur(); + await UrlbarTestUtils.assertSearchMode(window, null); + }); + await PlacesUtils.history.clear(); + await Services.search.removeEngine(testEngine); +}); + +add_task(async function tabNoSearchStringSearchMode() { + info( + "Tab through the toolbar when refocusing a Urlbar in search mode with the keyboard." + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + fireInputEvent: true, + }); + // Enter history search mode to avoid hitting the network. + await UrlbarTestUtils.enterSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + }); + await UrlbarTestUtils.promisePopupClose(window); + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeKey("l", { accelKey: true }); + }); + await UrlbarTestUtils.promiseSearchComplete(window); + + await expectTabThroughToolbar(); + + // We have to reopen the view to exit search mode. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + fireInputEvent: true, + }); + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); +}); + +async function expectTabThroughResults(options = { reverse: false }) { + let resultCount = UrlbarTestUtils.getResultCount(window); + Assert.ok(resultCount > 0, "There should be results"); + + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + + let initiallySelectedIndex = result.heuristic ? 0 : -1; + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + initiallySelectedIndex, + "Check the initial selection." + ); + + for (let i = initiallySelectedIndex + 1; i < resultCount; i++) { + EventUtils.synthesizeKey("KEY_Tab", { shiftKey: options.reverse }); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + options.reverse ? resultCount - i : i + ); + } + + EventUtils.synthesizeKey("KEY_Tab"); + + if (!options.reverse) { + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + initiallySelectedIndex, + "Should be back at the initial selection." + ); + } + + await UrlbarTestUtils.promisePopupClose(window, () => { + gURLBar.blur(); + }); +} + +async function expectTabThroughToolbar(options = { reverse: false }) { + if (gURLBar.getAttribute("pageproxystate") == "valid") { + Assert.equal(document.activeElement, gURLBar.inputField); + EventUtils.synthesizeKey("KEY_Tab"); + Assert.notEqual(document.activeElement, gURLBar.inputField); + } else { + let focusPromise = waitForFocusOnNextFocusableElement(options.reverse); + EventUtils.synthesizeKey("KEY_Tab", { shiftKey: options.reverse }); + await focusPromise; + } + Assert.ok(!gURLBar.view.isOpen, "The urlbar view should be closed."); +} + +async function waitForFocusOnNextFocusableElement(reverse = false) { + let urlbar = document.getElementById("urlbar-container"); + let nextFocusableElement = reverse + ? urlbar.previousElementSibling + : urlbar.nextElementSibling; + info(nextFocusableElement); + while ( + nextFocusableElement && + (!nextFocusableElement.classList.contains("toolbarbutton-1") || + nextFocusableElement.hasAttribute("hidden")) + ) { + nextFocusableElement = reverse + ? nextFocusableElement.previousElementSibling + : nextFocusableElement.nextElementSibling; + } + + Assert.ok( + nextFocusableElement.classList.contains("toolbarbutton-1"), + "We should have a reference to the next focusable element after the Urlbar." + ); + + return BrowserTestUtils.waitForCondition( + () => nextFocusableElement.tabIndex == -1 + ); +} diff --git a/browser/components/urlbar/tests/browser/browser_tabMatchesInAwesomebar.js b/browser/components/urlbar/tests/browser/browser_tabMatchesInAwesomebar.js new file mode 100644 index 0000000000..0e8ea75c1f --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_tabMatchesInAwesomebar.js @@ -0,0 +1,224 @@ +/* -*- 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/. */ + +"use strict"; + +/** + * Tests for ensuring that the tab switch results correctly match what is + * currently available. + */ + +requestLongerTimeout(2); + +const TEST_URL_BASES = [ + `${TEST_BASE_URL}dummy_page.html#tabmatch`, + `${TEST_BASE_URL}moz.png#tabmatch`, +]; + +const RESTRICT_TOKEN_OPENPAGE = "%"; + +var gTabCounter = 0; + +add_task(async function step_1() { + info("Running step 1"); + let maxResults = Services.prefs.getIntPref("browser.urlbar.maxRichResults"); + let promises = []; + for (let i = 0; i < maxResults - 1; i++) { + let tab = BrowserTestUtils.addTab(gBrowser); + promises.push(loadTab(tab, TEST_URL_BASES[0] + ++gTabCounter)); + } + + await Promise.all(promises); + await ensure_opentabs_match_db(); +}); + +add_task(async function step_2() { + info("Running step 2"); + gBrowser.selectTabAtIndex(1); + gBrowser.removeCurrentTab(); + gBrowser.selectTabAtIndex(1); + gBrowser.removeCurrentTab(); + gBrowser.selectTabAtIndex(0); + + let promises = []; + for (let i = 1; i < gBrowser.tabs.length; i++) { + promises.push(loadTab(gBrowser.tabs[i], TEST_URL_BASES[1] + ++gTabCounter)); + } + + await Promise.all(promises); + await ensure_opentabs_match_db(); +}); + +add_task(async function step_3() { + info("Running step 3"); + let promises = []; + for (let i = 1; i < gBrowser.tabs.length; i++) { + promises.push(loadTab(gBrowser.tabs[i], TEST_URL_BASES[0] + gTabCounter)); + } + + await Promise.all(promises); + await ensure_opentabs_match_db(); +}); + +add_task(async function step_4() { + info("Running step 4 - ensure we don't register subframes as open pages"); + let tab = BrowserTestUtils.addTab( + gBrowser, + 'data:text/html,<body><iframe src=""></iframe></body>' + ); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async function() { + let iframe_loaded = ContentTaskUtils.waitForEvent( + content.document, + "load", + true + ); + content.document.querySelector("iframe").src = "http://test2.example.org/"; + await iframe_loaded; + }); + + await ensure_opentabs_match_db(); +}); + +add_task(async function step_5() { + info("Running step 5 - remove tab immediately"); + let tab = BrowserTestUtils.addTab(gBrowser, "about:logo"); + BrowserTestUtils.removeTab(tab); + await ensure_opentabs_match_db(); +}); + +add_task(async function step_6() { + info( + "Running step 6 - check swapBrowsersAndCloseOther preserves registered switch-to-tab result" + ); + let tabToKeep = BrowserTestUtils.addTab(gBrowser); + let tab = BrowserTestUtils.addTab(gBrowser, "about:mozilla"); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + + gBrowser.updateBrowserRemoteness(tabToKeep.linkedBrowser, { + remoteType: tab.linkedBrowser.isRemoteBrowser + ? E10SUtils.DEFAULT_REMOTE_TYPE + : E10SUtils.NOT_REMOTE, + }); + gBrowser.swapBrowsersAndCloseOther(tabToKeep, tab); + + await ensure_opentabs_match_db(); + + BrowserTestUtils.removeTab(tabToKeep); + + await ensure_opentabs_match_db(); +}); + +add_task(async function step_7() { + info("Running step 7 - close all tabs"); + + Services.prefs.clearUserPref("browser.sessionstore.restore_on_demand"); + + BrowserTestUtils.addTab(gBrowser, "about:blank", { skipAnimation: true }); + while (gBrowser.tabs.length > 1) { + info("Removing tab: " + gBrowser.tabs[0].linkedBrowser.currentURI.spec); + gBrowser.selectTabAtIndex(0); + gBrowser.removeCurrentTab(); + } + + await ensure_opentabs_match_db(); +}); + +add_task(async function cleanup() { + info("Cleaning up"); + + await PlacesUtils.history.clear(); +}); + +function loadTab(tab, url) { + // Because adding visits is async, we will not be notified immediately. + let loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + let visited = new Promise(resolve => { + Services.obs.addObserver(function observer(aSubject, aTopic, aData) { + if (url != aSubject.QueryInterface(Ci.nsIURI).spec) { + return; + } + Services.obs.removeObserver(observer, aTopic); + resolve(); + }, "uri-visit-saved"); + }); + + info("Loading page: " + url); + BrowserTestUtils.loadURI(tab.linkedBrowser, url); + return Promise.all([loaded, visited]); +} + +function ensure_opentabs_match_db() { + let tabs = {}; + + for (let browserWin of Services.wm.getEnumerator("navigator:browser")) { + // skip closed-but-not-destroyed windows + if (browserWin.closed) { + continue; + } + + for (let i = 0; i < browserWin.gBrowser.tabs.length; i++) { + let browser = browserWin.gBrowser.getBrowserAtIndex(i); + let url = browser.currentURI.spec; + if (browserWin.isBlankPageURL(url)) { + continue; + } + if (!(url in tabs)) { + tabs[url] = 1; + } else { + tabs[url]++; + } + } + } + + return checkAutocompleteResults(tabs); +} + +async function checkAutocompleteResults(expected) { + info("Searching open pages."); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: RESTRICT_TOKEN_OPENPAGE, + }); + + let resultCount = UrlbarTestUtils.getResultCount(window); + for (let i = 0; i < resultCount; i++) { + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + if (result.heuristic) { + info("Skip heuristic match"); + continue; + } + + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.TAB_SWITCH, + "Should have a tab switch result" + ); + + let url = result.url; + + info(`Search for ${url} in open tabs.`); + let inExpected = url in expected; + Assert.ok( + inExpected, + `${url} was found in autocomplete, was ${ + inExpected ? "" : "not " + } expected` + ); + // Remove the found entry from expected results. + delete expected[url]; + } + + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Escape") + ); + + // Make sure there is no reported open page that is not open. + for (let entry in expected) { + Assert.ok(!entry, `Should have been found in autocomplete`); + } +} diff --git a/browser/components/urlbar/tests/browser/browser_tabMatchesInAwesomebar_perwindowpb.js b/browser/components/urlbar/tests/browser/browser_tabMatchesInAwesomebar_perwindowpb.js new file mode 100644 index 0000000000..04cca75021 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_tabMatchesInAwesomebar_perwindowpb.js @@ -0,0 +1,119 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test ensures that we don't move switch between tabs when one is in + * private browsing and the other is normal, or vice-versa. + */ + +const TEST_URL = `${TEST_BASE_URL}dummy_page.html`; + +add_task(async function() { + let normalWindow = await BrowserTestUtils.openNewBrowserWindow(); + let privateWindow = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + await runTest(normalWindow, privateWindow, false); + await BrowserTestUtils.closeWindow(normalWindow); + await BrowserTestUtils.closeWindow(privateWindow); + + normalWindow = await BrowserTestUtils.openNewBrowserWindow(); + privateWindow = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + await runTest(privateWindow, normalWindow, false); + await BrowserTestUtils.closeWindow(normalWindow); + await BrowserTestUtils.closeWindow(privateWindow); + + privateWindow = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + await runTest(privateWindow, privateWindow, false); + await BrowserTestUtils.closeWindow(privateWindow); + + normalWindow = await BrowserTestUtils.openNewBrowserWindow(); + await runTest(normalWindow, normalWindow, true); + await BrowserTestUtils.closeWindow(normalWindow); +}); + +async function runTest(aSourceWindow, aDestWindow, aExpectSwitch, aCallback) { + await BrowserTestUtils.openNewForegroundTab(aSourceWindow.gBrowser, TEST_URL); + let testTab = await BrowserTestUtils.openNewForegroundTab( + aDestWindow.gBrowser + ); + + info("waiting for focus on the window"); + await SimpleTest.promiseFocus(aDestWindow); + info("got focus on the window"); + + // Select the testTab + aDestWindow.gBrowser.selectedTab = testTab; + + // Ensure that this tab has no history entries + let sessionHistoryCount = await new Promise(resolve => { + SessionStore.getSessionHistory(gBrowser.selectedTab, function( + sessionHistory + ) { + resolve(sessionHistory.entries.length); + }); + }); + + ok( + sessionHistoryCount < 2, + `The test tab has 1 or fewer history entries. sessionHistoryCount=${sessionHistoryCount}` + ); + // Ensure that this tab is on about:blank + is( + testTab.linkedBrowser.currentURI.spec, + "about:blank", + "The test tab is on about:blank" + ); + // Ensure that this tab's document has no child nodes + await SpecialPowers.spawn(testTab.linkedBrowser, [], async function() { + ok( + !content.document.body.hasChildNodes(), + "The test tab has no child nodes" + ); + }); + ok( + !testTab.hasAttribute("busy"), + "The test tab doesn't have the busy attribute" + ); + + // Wait for the Awesomebar popup to appear. + let searchString = TEST_URL; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: aDestWindow, + value: searchString, + }); + + info(`awesomebar popup appeared. aExpectSwitch: ${aExpectSwitch}`); + // Make sure the last match is selected. + while ( + UrlbarTestUtils.getSelectedRowIndex(aDestWindow) < + UrlbarTestUtils.getResultCount(aDestWindow) - 1 + ) { + info("handling key navigation for DOM_VK_DOWN key"); + EventUtils.synthesizeKey("KEY_ArrowDown", {}, aDestWindow); + } + + let awaitTabSwitch; + if (aExpectSwitch) { + awaitTabSwitch = BrowserTestUtils.waitForTabClosing(testTab); + } + + // Execute the selected action. + EventUtils.synthesizeKey("KEY_Enter", {}, aDestWindow); + info("sent Enter command to the controller"); + + if (aExpectSwitch) { + // If we expect a tab switch then the current tab + // will be closed and we switch to the other tab. + await awaitTabSwitch; + } else { + // If we don't expect a tab switch then wait for the tab to load. + await BrowserTestUtils.browserLoaded(testTab.linkedBrowser); + } +} diff --git a/browser/components/urlbar/tests/browser/browser_tabToSearch.js b/browser/components/urlbar/tests/browser/browser_tabToSearch.js new file mode 100644 index 0000000000..42cd27ce97 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_tabToSearch.js @@ -0,0 +1,679 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests tab-to-search results. See also + * browser/components/urlbar/tests/unit/test_providerTabToSearch.js. + */ + +"use strict"; + +const TEST_ENGINE_NAME = "Test"; +const TEST_ENGINE_DOMAIN = "example.com"; + +const DYNAMIC_RESULT_TYPE = "onboardTabToSearch"; + +XPCOMUtils.defineLazyModuleGetters(this, { + UrlbarProviderTabToSearch: + "resource:///modules/UrlbarProviderTabToSearch.jsm", +}); + +add_task(async function setup() { + await PlacesUtils.history.clear(); + await SpecialPowers.pushPrefEnv({ + set: [ + // Disable onboarding results for general tests. They are enabled in tests + // that specifically address onboarding. + ["browser.urlbar.tabToSearch.onboard.interactionsLeft", 0], + ], + }); + let testEngine = await Services.search.addEngineWithDetails( + TEST_ENGINE_NAME, + { + alias: "@test", + template: `http://${TEST_ENGINE_DOMAIN}/?search={searchTerms}`, + } + ); + for (let i = 0; i < 3; i++) { + await PlacesTestUtils.addVisits([`https://${TEST_ENGINE_DOMAIN}/`]); + } + + registerCleanupFunction(async () => { + await PlacesUtils.history.clear(); + await Services.search.removeEngine(testEngine); + }); +}); + +// Tests that tab-to-search results preview search mode when highlighted. These +// results are worth testing separately since they do not set the +// payload.keyword parameter. +add_task(async function basic() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TEST_ENGINE_DOMAIN.slice(0, 4), + }); + let autofillResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(autofillResult.autofill); + Assert.equal( + autofillResult.url, + `https://${TEST_ENGINE_DOMAIN}/`, + "The autofilled URL matches the engine domain." + ); + + let tabToSearchResult = ( + await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1) + ).result; + Assert.equal( + tabToSearchResult.providerName, + "TabToSearch", + "The second result is a tab-to-search result." + ); + Assert.equal( + tabToSearchResult.payload.engine, + TEST_ENGINE_NAME, + "The tab-to-search result is for the correct engine." + ); + let tabToSearchDetails = await UrlbarTestUtils.getDetailsOfResultAt( + window, + 1 + ); + let [actionTabToSearch] = await document.l10n.formatValues([ + { + id: UrlbarUtils.WEB_ENGINE_NAMES.has( + tabToSearchDetails.searchParams.engine + ) + ? "urlbar-result-action-tabtosearch-web" + : "urlbar-result-action-tabtosearch-other-engine", + args: { engine: tabToSearchDetails.searchParams.engine }, + }, + ]); + Assert.equal( + tabToSearchDetails.displayed.title, + `Search with ${tabToSearchDetails.searchParams.engine}`, + "The result's title is set correctly." + ); + Assert.equal( + tabToSearchDetails.displayed.action, + actionTabToSearch, + "The correct action text is displayed in the tab-to-search result." + ); + + EventUtils.synthesizeKey("KEY_Tab"); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 1, + "Sanity check: The second result is selected." + ); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: TEST_ENGINE_NAME, + entry: "tabtosearch", + isPreview: true, + }); + + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); +}); + +// Tests that we do not set aria-activedescendant after tabbing to a +// tab-to-search result when the pref +// browser.urlbar.accessibility.tabToSearch.announceResults is true. If that +// pref is true, the result was already announced while the user was typing. +add_task(async function activedescendant_tab() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.accessibility.tabToSearch.announceResults", true]], + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TEST_ENGINE_DOMAIN.slice(0, 4), + }); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 2, + "There should be two results." + ); + let tabToSearchRow = await UrlbarTestUtils.waitForAutocompleteResultAt( + window, + 1 + ); + Assert.equal( + tabToSearchRow.result.providerName, + "TabToSearch", + "The second result is a tab-to-search result." + ); + + EventUtils.synthesizeKey("KEY_Tab"); + + await UrlbarTestUtils.assertSearchMode(window, { + engineName: TEST_ENGINE_NAME, + entry: "tabtosearch", + isPreview: true, + }); + let aadID = gURLBar.inputField.getAttribute("aria-activedescendant"); + Assert.ok(!aadID, "aria-activedescendant was not set."); + + // Cycle through all the results then return to the tab-to-search result. It + // should be announced. + EventUtils.synthesizeKey("KEY_Tab"); + aadID = gURLBar.inputField.getAttribute("aria-activedescendant"); + let firstRow = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0); + Assert.equal( + aadID, + firstRow.id, + "aria-activedescendant was set to the row after the tab-to-search result." + ); + EventUtils.synthesizeKey("KEY_Tab"); + aadID = gURLBar.inputField.getAttribute("aria-activedescendant"); + Assert.equal( + aadID, + tabToSearchRow.id, + "aria-activedescendant was set to the tab-to-search result." + ); + + // Now close and reopen the view, then do another search that yields a + // tab-to-search result. aria-activedescendant should not be set when it is + // selected. + await UrlbarTestUtils.promisePopupClose(window); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TEST_ENGINE_DOMAIN.slice(0, 4), + }); + tabToSearchRow = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1); + Assert.equal( + tabToSearchRow.result.providerName, + "TabToSearch", + "The second result is a tab-to-search result." + ); + + EventUtils.synthesizeKey("KEY_Tab"); + + await UrlbarTestUtils.assertSearchMode(window, { + engineName: TEST_ENGINE_NAME, + entry: "tabtosearch", + isPreview: true, + }); + aadID = gURLBar.inputField.getAttribute("aria-activedescendant"); + Assert.ok(!aadID, "aria-activedescendant was not set."); + + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); + await SpecialPowers.popPrefEnv(); +}); + +// Tests that we set aria-activedescendant after accessing a tab-to-search +// result with the arrow keys. +add_task(async function activedescendant_arrow() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TEST_ENGINE_DOMAIN.slice(0, 4), + }); + let tabToSearchRow = await UrlbarTestUtils.waitForAutocompleteResultAt( + window, + 1 + ); + Assert.equal( + tabToSearchRow.result.providerName, + "TabToSearch", + "The second result is a tab-to-search result." + ); + + EventUtils.synthesizeKey("KEY_ArrowDown"); + + await UrlbarTestUtils.assertSearchMode(window, { + engineName: TEST_ENGINE_NAME, + entry: "tabtosearch", + isPreview: true, + }); + let aadID = gURLBar.inputField.getAttribute("aria-activedescendant"); + Assert.equal( + aadID, + tabToSearchRow.id, + "aria-activedescendant was set to the tab-to-search result." + ); + + // Move selection away from the tab-to-search result then return. It should + // be announced. + EventUtils.synthesizeKey("KEY_ArrowDown"); + aadID = gURLBar.inputField.getAttribute("aria-activedescendant"); + Assert.equal( + aadID, + UrlbarTestUtils.getOneOffSearchButtons(window).selectedButton.id, + tabToSearchRow.id, + "aria-activedescendant was moved to the first one-off." + ); + EventUtils.synthesizeKey("KEY_ArrowUp"); + aadID = gURLBar.inputField.getAttribute("aria-activedescendant"); + Assert.equal( + aadID, + tabToSearchRow.id, + "aria-activedescendant was set to the tab-to-search result." + ); + + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); +}); + +add_task(async function tab_key_race() { + // Mac Debug tinderboxes are just too slow and fail intermittently + // even if the EventBufferer timeout is set to an high value. + if (AppConstants.platform == "macosx" && AppConstants.DEBUG) { + return; + } + info( + "Test typing a letter followed shortly by Tab consistently selects a tab-to-search result" + ); + Assert.equal(gURLBar.value, "", "Sanity check urlbar is empty"); + let promiseQueryStarted = new Promise(resolve => { + /** + * A no-op test provider. + * We use this to wait for the query to start, because otherwise TAB will + * move to the next widget since the panel is closed and there's no running + * query. This means waiting for the UrlbarProvidersManager to at least + * evaluate the isActive status of providers. + * In the future we should try to reduce this latency, to defer user events + * even more efficiently. + */ + class ListeningTestProvider extends UrlbarProvider { + constructor() { + super(); + } + get name() { + return "ListeningTestProvider"; + } + get type() { + return UrlbarUtils.PROVIDER_TYPE.PROFILE; + } + isActive(context) { + executeSoon(resolve); + return false; + } + isRestricting(context) { + return false; + } + async startQuery(context, addCallback) { + // Nothing to do. + } + } + let provider = new ListeningTestProvider(); + UrlbarProvidersManager.registerProvider(provider); + registerCleanupFunction(async function() { + UrlbarProvidersManager.unregisterProvider(provider); + }); + }); + info("Type the beginning of the search string to get tab-to-search"); + EventUtils.synthesizeKey(TEST_ENGINE_DOMAIN.slice(0, 1)); + info("Awaiting for the query to start"); + await promiseQueryStarted; + EventUtils.synthesizeKey("KEY_Tab"); + await UrlbarTestUtils.promiseSearchComplete(window); + await TestUtils.waitForCondition( + () => UrlbarTestUtils.getSelectedRowIndex(window) == 1, + "Wait for tab key to be handled" + ); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: TEST_ENGINE_NAME, + entry: "tabtosearch", + isPreview: true, + }); + + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); +}); + +// Test that large-style onboarding results appear and have the correct +// properties. +add_task(async function onboard() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.tabToSearch.onboard.interactionsLeft", 3]], + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TEST_ENGINE_DOMAIN.slice(0, 4), + }); + let autofillResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(autofillResult.autofill); + Assert.equal( + autofillResult.url, + `https://${TEST_ENGINE_DOMAIN}/`, + "The autofilled URL matches the engine domain." + ); + EventUtils.synthesizeKey("KEY_Tab"); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 1, + "Sanity check: The second result is selected." + ); + + // Now check the properties of the onboarding result. + let onboardingElement = await UrlbarTestUtils.waitForAutocompleteResultAt( + window, + 1 + ); + Assert.equal( + onboardingElement.result.payload.dynamicType, + DYNAMIC_RESULT_TYPE, + "The tab-to-search result is an onboarding result." + ); + Assert.equal( + onboardingElement.result.resultSpan, + 2, + "The correct resultSpan was set." + ); + Assert.ok( + onboardingElement + .querySelector(".urlbarView-row-inner") + .hasAttribute("selected"), + "The onboarding element set the selected attribute." + ); + + let [ + titleOnboarding, + actionOnboarding, + descriptionOnboarding, + ] = await document.l10n.formatValues([ + { + id: "urlbar-result-action-search-w-engine", + args: { + engine: onboardingElement.result.payload.engine, + }, + }, + { + id: UrlbarUtils.WEB_ENGINE_NAMES.has( + onboardingElement.result.payload.engine + ) + ? "urlbar-result-action-tabtosearch-web" + : "urlbar-result-action-tabtosearch-other-engine", + args: { engine: onboardingElement.result.payload.engine }, + }, + { + id: "urlbar-tabtosearch-onboard", + }, + ]); + let onboardingDetails = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal( + onboardingDetails.displayed.title, + titleOnboarding, + "The correct title was set." + ); + Assert.equal( + onboardingDetails.displayed.action, + actionOnboarding, + "The correct action text was set." + ); + Assert.equal( + onboardingDetails.element.row.querySelector( + ".urlbarView-dynamic-onboardTabToSearch-description" + ).textContent, + descriptionOnboarding, + "The correct description was set." + ); + + // Check that the onboarding result enters search mode. + await UrlbarTestUtils.assertSearchMode(window, { + engineName: TEST_ENGINE_NAME, + entry: "tabtosearch_onboard", + isPreview: true, + }); + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey("KEY_Enter"); + await searchPromise; + await UrlbarTestUtils.assertSearchMode(window, { + engineName: TEST_ENGINE_NAME, + entry: "tabtosearch_onboard", + }); + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window); + UrlbarPrefs.set("tabToSearch.onboard.interactionsLeft", 3); + delete UrlbarProviderTabToSearch.onboardingInteractionAtTime; + await SpecialPowers.popPrefEnv(); +}); + +// Tests that we show the onboarding result until the user interacts with it +// `browser.urlbar.tabToSearch.onboard.interactionsLeft` times. +add_task(async function onboard_limit() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.tabToSearch.onboard.interactionsLeft", 3]], + }); + + Assert.equal( + UrlbarPrefs.get("tabToSearch.onboard.interactionsLeft"), + 3, + "Sanity check: interactionsLeft is 3." + ); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TEST_ENGINE_DOMAIN.slice(0, 4), + }); + let onboardingResult = ( + await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1) + ).result; + Assert.equal( + onboardingResult.payload.dynamicType, + DYNAMIC_RESULT_TYPE, + "The second result is an onboarding result." + ); + await EventUtils.synthesizeKey("KEY_ArrowDown"); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: TEST_ENGINE_NAME, + entry: "tabtosearch_onboard", + isPreview: true, + }); + Assert.equal(UrlbarPrefs.get("tabToSearch.onboard.interactionsLeft"), 2); + await UrlbarTestUtils.exitSearchMode(window); + + // We don't increment the counter if we showed the onboarding result less than + // 5 minutes ago. + for (let i = 0; i < 5; i++) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TEST_ENGINE_DOMAIN.slice(0, 4), + }); + onboardingResult = ( + await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1) + ).result; + Assert.equal( + onboardingResult.payload.dynamicType, + DYNAMIC_RESULT_TYPE, + "The second result is an onboarding result." + ); + await EventUtils.synthesizeKey("KEY_ArrowDown"); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: TEST_ENGINE_NAME, + entry: "tabtosearch_onboard", + isPreview: true, + }); + Assert.equal( + UrlbarPrefs.get("tabToSearch.onboard.interactionsLeft"), + 2, + "We shouldn't decrement interactionsLeft if an onboarding result was just shown." + ); + await UrlbarTestUtils.exitSearchMode(window); + } + + // If the user doesn't interact with the result, we don't increment the + // counter. + for (let i = 0; i < 5; i++) { + delete UrlbarProviderTabToSearch.onboardingInteractionAtTime; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TEST_ENGINE_DOMAIN.slice(0, 4), + }); + let tabToSearchResult = ( + await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1) + ).result; + Assert.equal( + tabToSearchResult.providerName, + "TabToSearch", + "The second result is a tab-to-search result." + ); + Assert.equal( + tabToSearchResult.type, + UrlbarUtils.RESULT_TYPE.DYNAMIC, + "The tab-to-search result is an onboarding result." + ); + Assert.equal( + UrlbarPrefs.get("tabToSearch.onboard.interactionsLeft"), + 2, + "We shouldn't decrement interactionsLeft if the user doesn't interact with onboarding." + ); + } + + // Test that we increment the counter if the user interacts with the result + // and it's been 5+ minutes since they last interacted with it. + for (let i = 1; i >= 0; i--) { + delete UrlbarProviderTabToSearch.onboardingInteractionAtTime; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TEST_ENGINE_DOMAIN.slice(0, 4), + }); + let tabToSearchResult = ( + await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1) + ).result; + Assert.equal( + tabToSearchResult.providerName, + "TabToSearch", + "The second result is a tab-to-search result." + ); + Assert.equal( + onboardingResult.payload.dynamicType, + DYNAMIC_RESULT_TYPE, + "The second result is an onboarding result." + ); + await EventUtils.synthesizeKey("KEY_ArrowDown"); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: TEST_ENGINE_NAME, + entry: "tabtosearch_onboard", + isPreview: true, + }); + Assert.equal( + UrlbarPrefs.get("tabToSearch.onboard.interactionsLeft"), + i, + "We decremented interactionsLeft." + ); + await UrlbarTestUtils.exitSearchMode(window); + } + + delete UrlbarProviderTabToSearch.onboardingInteractionAtTime; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TEST_ENGINE_DOMAIN.slice(0, 4), + }); + let tabToSearchResult = ( + await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1) + ).result; + Assert.equal( + tabToSearchResult.providerName, + "TabToSearch", + "The second result is a tab-to-search result." + ); + Assert.notEqual( + tabToSearchResult.type, + UrlbarUtils.RESULT_TYPE.DYNAMIC, + "Now that interactionsLeft is 0, we don't show onboarding results." + ); + + await UrlbarTestUtils.promisePopupClose(window); + UrlbarPrefs.set("tabToSearch.onboard.interactionsLeft", 3); + delete UrlbarProviderTabToSearch.onboardingInteractionAtTime; + await SpecialPowers.popPrefEnv(); +}); + +// Tests that we show at most one onboarding result at a time. See +// tests/unit/test_providerTabToSearch.js:multipleEnginesForHostname for a test +// that ensures only one normal tab-to-search result is shown in this scenario. +add_task(async function onboard_multipleEnginesForHostname() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.tabToSearch.onboard.interactionsLeft", 3]], + }); + + let testEngineMaps = await Services.search.addEngineWithDetails( + `${TEST_ENGINE_NAME}Maps`, + { + template: `http://${TEST_ENGINE_DOMAIN}/maps/?search={searchTerms}`, + } + ); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TEST_ENGINE_DOMAIN.slice(0, 4), + }); + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 2, + "Only two results are shown." + ); + let firstResult = ( + await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0) + ).result; + Assert.notEqual( + firstResult.providerName, + "TabToSearch", + "The first result is not from TabToSearch." + ); + let secondResult = ( + await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1) + ).result; + Assert.equal( + secondResult.providerName, + "TabToSearch", + "The second result is from TabToSearch." + ); + Assert.equal( + secondResult.type, + UrlbarUtils.RESULT_TYPE.DYNAMIC, + "The tab-to-search result is the only onboarding result." + ); + await Services.search.removeEngine(testEngineMaps); + await UrlbarTestUtils.promisePopupClose(window); + UrlbarPrefs.set("tabToSearch.onboard.interactionsLeft", 3); + delete UrlbarProviderTabToSearch.onboardingInteractionAtTime; + await SpecialPowers.popPrefEnv(); +}); + +// Tests that engines with names containing extended Unicode characters can be +// recognized as general-web engines and that their tab-to-search results +// display the correct string. +add_task(async function extended_unicode_in_engine() { + // Baidu's localized name. We expect this tab-to-search result shows the + // general-web engine string because Baidu is included in WEB_ENGINE_NAMES. + let engineName = "百度"; + let engineDomain = "example-2.com"; + let testEngine = await Services.search.addEngineWithDetails(engineName, { + template: `http://${engineDomain}/?search={searchTerms}`, + }); + for (let i = 0; i < 3; i++) { + await PlacesTestUtils.addVisits([`https://${engineDomain}/`]); + } + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: engineDomain.slice(0, 4), + }); + let tabToSearchDetails = await UrlbarTestUtils.getDetailsOfResultAt( + window, + 1 + ); + Assert.equal( + tabToSearchDetails.searchParams.engine, + engineName, + "The tab-to-search engine name contains extended Unicode characters." + ); + let [actionTabToSearch] = await document.l10n.formatValues([ + { + id: "urlbar-result-action-tabtosearch-web", + args: { engine: tabToSearchDetails.searchParams.engine }, + }, + ]); + Assert.equal( + tabToSearchDetails.displayed.action, + actionTabToSearch, + "The correct action text is displayed in the tab-to-search result." + ); + + await UrlbarTestUtils.promisePopupClose(window); + await PlacesUtils.history.clear(); + await Services.search.removeEngine(testEngine); +}); diff --git a/browser/components/urlbar/tests/browser/browser_textruns.js b/browser/components/urlbar/tests/browser/browser_textruns.js new file mode 100644 index 0000000000..f09303cd99 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_textruns.js @@ -0,0 +1,58 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test ensures that we limit textruns in case of very long urls or titles. + */ + +add_task(async function() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.searches", true]], + }); + let engine = await Services.search.addEngineWithDetails("Test", { + template: "http://example.com/?search={searchTerms}", + }); + let oldDefaultEngine = await Services.search.getDefault(); + await Services.search.setDefault(engine); + + let lotsOfSpaces = "%20".repeat(300); + await PlacesTestUtils.addVisits({ + uri: `https://textruns.mozilla.org/${lotsOfSpaces}/test/`, + title: `A long ${lotsOfSpaces} title`, + }); + await UrlbarTestUtils.formHistory.add([ + { value: `A long ${lotsOfSpaces} textruns suggestion` }, + ]); + + registerCleanupFunction(async function() { + await PlacesUtils.history.clear(); + await UrlbarTestUtils.formHistory.clear(); + await Services.search.setDefault(oldDefaultEngine); + await Services.search.removeEngine(engine); + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "textruns", + }); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal(result.searchParams.engine, "Test", "Sanity check engine"); + Assert.equal( + result.displayed.title.length, + UrlbarUtils.MAX_TEXT_LENGTH, + "Result title should be limited" + ); + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 2); + Assert.equal( + result.displayed.title.length, + UrlbarUtils.MAX_TEXT_LENGTH, + "Result title should be limited" + ); + Assert.equal( + result.displayed.url.length, + UrlbarUtils.MAX_TEXT_LENGTH, + "Result url should be limited" + ); +}); diff --git a/browser/components/urlbar/tests/browser/browser_tokenAlias.js b/browser/components/urlbar/tests/browser/browser_tokenAlias.js new file mode 100644 index 0000000000..e0951ff3ec --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_tokenAlias.js @@ -0,0 +1,861 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This test checks "@" search engine aliases ("token aliases") in the urlbar. + +"use strict"; + +const ALIAS = "@test"; +const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml"; + +// We make sure that aliases and search terms are correctly recognized when they +// are separated by each of these different types of spaces and combinations of +// spaces. U+3000 is the ideographic space in CJK and is commonly used by CJK +// speakers. +const TEST_SPACES = [" ", "\u3000", " \u3000", "\u3000 "]; + +let testEngine; + +// Allow more time for Mac machines so they don't time out in verify mode. See +// bug 1673062. +if (AppConstants.platform == "macosx") { + requestLongerTimeout(5); +} + +add_task(async function init() { + // Add a default engine with suggestions, to avoid hitting the network when + // fetching them. + let defaultEngine = await SearchTestUtils.promiseNewSearchEngine( + getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME + ); + defaultEngine.alias = "@default"; + let oldDefaultEngine = await Services.search.getDefault(); + Services.search.setDefault(defaultEngine); + testEngine = await Services.search.addEngineWithDetails("Test", { + alias: ALIAS, + template: "http://example.com/?search={searchTerms}", + }); + registerCleanupFunction(async function() { + await Services.search.removeEngine(testEngine); + Services.search.setDefault(oldDefaultEngine); + }); + + // Search results aren't shown in quantumbar unless search suggestions are + // enabled. + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.searches", true]], + }); +}); + +// Simple test that tries different variations of an alias, without reverting +// the urlbar value in between. +add_task(async function testNoRevert() { + await doSimpleTest(false); +}); + +// Simple test that tries different variations of an alias, reverting the urlbar +// value in between. +add_task(async function testRevert() { + await doSimpleTest(true); +}); + +async function doSimpleTest(revertBetweenSteps) { + // When autofill is enabled, searching for "@tes" will autofill to "@test", + // which gets in the way of this test task, so temporarily disable it. + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.autoFill", false]], + }); + + // "@tes" -- not an alias, no search mode + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: ALIAS.substr(0, ALIAS.length - 1), + }); + await UrlbarTestUtils.assertSearchMode(window, null); + Assert.equal( + gURLBar.value, + ALIAS.substr(0, ALIAS.length - 1), + "value should be alias substring" + ); + + if (revertBetweenSteps) { + gURLBar.handleRevert(); + gURLBar.blur(); + } + + // "@test" -- alias but no trailing space, no search mode + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: ALIAS, + }); + await UrlbarTestUtils.assertSearchMode(window, null); + Assert.equal(gURLBar.value, ALIAS, "value should be alias"); + + if (revertBetweenSteps) { + gURLBar.handleRevert(); + gURLBar.blur(); + } + + // "@test " -- alias with trailing space, search mode + for (let spaces of TEST_SPACES) { + info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) })); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: ALIAS + spaces, + }); + // Wait for the second new search that starts when search mode is entered. + await UrlbarTestUtils.promiseSearchComplete(window); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: testEngine.name, + entry: "typed", + }); + Assert.equal(gURLBar.value, "", "value should be empty"); + await UrlbarTestUtils.exitSearchMode(window); + + if (revertBetweenSteps) { + gURLBar.handleRevert(); + gURLBar.blur(); + } + } + + // "@test foo" -- alias, search mode + for (let spaces of TEST_SPACES) { + info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) })); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: ALIAS + spaces + "foo", + }); + // Wait for the second new search that starts when search mode is entered. + await UrlbarTestUtils.promiseSearchComplete(window); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: testEngine.name, + entry: "typed", + }); + Assert.equal(gURLBar.value, "foo", "value should be query"); + await UrlbarTestUtils.exitSearchMode(window); + + if (revertBetweenSteps) { + gURLBar.handleRevert(); + gURLBar.blur(); + } + } + + // "@test " -- alias with trailing space, search mode + for (let spaces of TEST_SPACES) { + info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) })); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: ALIAS + spaces, + }); + // Wait for the second new search that starts when search mode is entered. + await UrlbarTestUtils.promiseSearchComplete(window); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: testEngine.name, + entry: "typed", + }); + Assert.equal(gURLBar.value, "", "value should be empty"); + await UrlbarTestUtils.exitSearchMode(window); + + if (revertBetweenSteps) { + gURLBar.handleRevert(); + gURLBar.blur(); + } + } + + // "@test" -- alias but no trailing space, no highlight + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: ALIAS, + }); + await UrlbarTestUtils.assertSearchMode(window, null); + Assert.equal(gURLBar.value, ALIAS, "value should be alias"); + + if (revertBetweenSteps) { + gURLBar.handleRevert(); + gURLBar.blur(); + } + + // "@tes" -- not an alias, no highlight + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: ALIAS.substr(0, ALIAS.length - 1), + }); + await UrlbarTestUtils.assertSearchMode(window, null); + Assert.equal( + gURLBar.value, + ALIAS.substr(0, ALIAS.length - 1), + "value should be alias substring" + ); + + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Escape") + ); + + await SpecialPowers.popPrefEnv(); +} + +// An alias should be recognized even when there are spaces before it, and +// search mode should be entered. +add_task(async function spacesBeforeAlias() { + for (let spaces of TEST_SPACES) { + info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) })); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: spaces + ALIAS + spaces, + }); + // Wait for the second new search that starts when search mode is entered. + await UrlbarTestUtils.promiseSearchComplete(window); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: testEngine.name, + entry: "typed", + }); + Assert.equal(gURLBar.value, "", "value should be empty"); + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Escape") + ); + } +}); + +// An alias in the middle of a string should not be recognized and search mode +// should not be entered. +add_task(async function charsBeforeAlias() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "not an alias " + ALIAS + " ", + }); + await UrlbarTestUtils.assertSearchMode(window, null); + Assert.equal( + gURLBar.value, + "not an alias " + ALIAS + " ", + "value should be unchanged" + ); + + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Escape") + ); +}); + +// While already in search mode, an alias should not be recognized. +add_task(async function alreadyInSearchMode() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "", + }); + await UrlbarTestUtils.enterSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: ALIAS + " ", + }); + + // Search mode source should still be bookmarks. + await UrlbarTestUtils.assertSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + entry: "oneoff", + }); + Assert.equal(gURLBar.value, ALIAS + " ", "value should be unchanged"); + + // Exit search mode, but first remove the value in the input. Since the value + // is "alias ", we'd actually immediately re-enter search mode otherwise. + gURLBar.value = ""; + + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Escape") + ); +}); + +// Types a space while typing an alias to ensure we stop autofilling. +add_task(async function spaceWhileTypingAlias() { + for (let spaces of TEST_SPACES) { + if (spaces.length != 1) { + continue; + } + info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) })); + + let value = ALIAS.substring(0, ALIAS.length - 1); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value, + selectionStart: value.length, + selectionEnd: value.length, + }); + Assert.equal(gURLBar.value, ALIAS + " ", "Alias should be autofilled"); + + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey(spaces); + await searchPromise; + + Assert.equal( + gURLBar.value, + value + spaces, + "Alias should not be autofilled" + ); + await UrlbarTestUtils.assertSearchMode(window, null); + + await UrlbarTestUtils.promisePopupClose(window); + } +}); + +// Aliases are case insensitive. Make sure that searching with an alias using a +// weird case still causes the alias to be recognized and search mode entered. +add_task(async function aliasCase() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "@TeSt ", + }); + // Wait for the second new search that starts when search mode is entered. + await UrlbarTestUtils.promiseSearchComplete(window); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: testEngine.name, + entry: "typed", + }); + Assert.equal(gURLBar.value, "", "value should be empty"); + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Escape") + ); +}); + +// Same as previous but with a query. +add_task(async function aliasCase_query() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "@tEsT query", + }); + // Wait for the second new search that starts when search mode is entered. + await UrlbarTestUtils.promiseSearchComplete(window); + await UrlbarTestUtils.assertSearchMode(window, { + engineName: testEngine.name, + entry: "typed", + }); + Assert.equal(gURLBar.value, "query", "value should be query"); + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Escape") + ); +}); + +// Selecting a non-heuristic (non-first) search engine result with an alias and +// empty query should put the alias in the urlbar and highlight it. +// Also checks that internal aliases appear with the "@" keyword. +add_task(async function nonHeuristicAliases() { + // Get the list of token alias engines (those with aliases that start with + // "@"). + let tokenEngines = []; + for (let engine of await Services.search.getEngines()) { + let aliases = []; + if (engine.alias) { + aliases.push(engine.alias); + } + aliases.push(...engine.aliases); + let tokenAliases = aliases.filter(a => a.startsWith("@")); + if (tokenAliases.length) { + tokenEngines.push({ engine, tokenAliases }); + } + } + if (!tokenEngines.length) { + Assert.ok(true, "No token alias engines, skipping task."); + return; + } + info( + "Got token alias engines: " + tokenEngines.map(({ engine }) => engine.name) + ); + + // Populate the results with the list of token alias engines by searching for + // "@". + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "@", + }); + await UrlbarTestUtils.waitForAutocompleteResultAt( + window, + tokenEngines.length - 1 + ); + // Key down to select each result in turn. The urlbar should preview search + // mode for each engine. + for (let { tokenAliases } of tokenEngines) { + let alias = tokenAliases[0]; + let engineName = (await UrlbarSearchUtils.engineForAlias(alias)).name; + EventUtils.synthesizeKey("KEY_ArrowDown"); + let expectedSearchMode = { + engineName, + entry: "keywordoffer", + isPreview: true, + }; + if (UrlbarUtils.WEB_ENGINE_NAMES.has(engineName)) { + expectedSearchMode.source = UrlbarUtils.RESULT_SOURCE.SEARCH; + } + await UrlbarTestUtils.assertSearchMode(window, expectedSearchMode); + Assert.ok(!gURLBar.value, "The Urlbar should be empty."); + } + + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Escape") + ); +}); + +// Clicking on an @ alias offer (an @ alias with an empty search string) in the +// view should enter search mode. +add_task(async function clickAndFillAlias() { + // Do a search for "@" to show all the @ aliases. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "@", + }); + + // Find our test engine in the results. It's probably last, but for + // robustness don't assume it is. + let testEngineItem; + for (let i = 0; !testEngineItem; i++) { + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + Assert.equal( + details.displayed.title, + `Search with ${details.searchParams.engine}`, + "The result's title is set correctly." + ); + Assert.ok(!details.action, "The result should have no action text."); + if (details.searchParams && details.searchParams.keyword == ALIAS) { + testEngineItem = await UrlbarTestUtils.waitForAutocompleteResultAt( + window, + i + ); + } + } + + // Click it. + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeMouseAtCenter(testEngineItem, {}); + await searchPromise; + + await UrlbarTestUtils.assertSearchMode(window, { + engineName: testEngineItem.result.payload.engine, + entry: "keywordoffer", + }); + + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Escape") + ); +}); + +// Pressing enter on an @ alias offer (an @ alias with an empty search string) +// in the view should enter search mode. +add_task(async function enterAndFillAlias() { + // Do a search for "@" to show all the @ aliases. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "@", + }); + + // Find our test engine in the results. It's probably last, but for + // robustness don't assume it is. + let details; + let index = 0; + for (; ; index++) { + details = await UrlbarTestUtils.getDetailsOfResultAt(window, index); + if (details.searchParams && details.searchParams.keyword == ALIAS) { + index++; + break; + } + } + + // Key down to it and press enter. + EventUtils.synthesizeKey("KEY_ArrowDown", { repeat: index }); + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey("KEY_Enter"); + await searchPromise; + + await UrlbarTestUtils.assertSearchMode(window, { + engineName: details.searchParams.engine, + entry: "keywordoffer", + }); + + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Escape") + ); +}); + +// Pressing Enter on an @ alias autofill should enter search mode. +add_task(async function enterAutofillsAlias() { + for (let value of [ALIAS.substring(0, ALIAS.length - 1), ALIAS]) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value, + selectionStart: value.length, + selectionEnd: value.length, + }); + + // Press Enter. + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey("KEY_Enter"); + await searchPromise; + + await UrlbarTestUtils.assertSearchMode(window, { + engineName: testEngine.name, + entry: "keywordoffer", + }); + + await UrlbarTestUtils.exitSearchMode(window); + } + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Escape") + ); +}); + +// Pressing Right on an @ alias autofill should enter search mode. +add_task(async function rightEntersSearchMode() { + for (let value of [ALIAS.substring(0, ALIAS.length - 1), ALIAS]) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value, + selectionStart: value.length, + selectionEnd: value.length, + }); + + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey("KEY_ArrowRight"); + await searchPromise; + + await UrlbarTestUtils.assertSearchMode(window, { + engineName: testEngine.name, + entry: "typed", + }); + Assert.equal(gURLBar.value, "", "value should be empty"); + await UrlbarTestUtils.exitSearchMode(window); + } + + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Escape") + ); +}); + +// Pressing Tab when an @ alias is autofilled should enter search mode preview. +add_task(async function rightEntersSearchMode() { + for (let value of [ALIAS.substring(0, ALIAS.length - 1), ALIAS]) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value, + selectionStart: value.length, + selectionEnd: value.length, + }); + + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + -1, + "There is no selected result." + ); + + EventUtils.synthesizeKey("KEY_Tab"); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 0, + "The first result is selected." + ); + + await UrlbarTestUtils.assertSearchMode(window, { + engineName: testEngine.name, + entry: "keywordoffer", + isPreview: true, + }); + Assert.equal(gURLBar.value, "", "value should be empty"); + + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey("KEY_Enter"); + await searchPromise; + + await UrlbarTestUtils.assertSearchMode(window, { + engineName: testEngine.name, + entry: "keywordoffer", + isPreview: false, + }); + await UrlbarTestUtils.exitSearchMode(window); + } + + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Escape") + ); +}); + +/** + * This test checks that if an engine is marked as hidden then + * it should not appear in the popup when using the "@" token alias in the search bar. + */ +add_task(async function hiddenEngine() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "@", + fireInputEvent: true, + }); + + const defaultEngine = await Services.search.getDefault(); + + let foundDefaultEngineInPopup = false; + + // Checks that the default engine appears in the urlbar's popup. + for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) { + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + if (defaultEngine.name == details.searchParams.engine) { + foundDefaultEngineInPopup = true; + break; + } + } + Assert.ok(foundDefaultEngineInPopup, "Default engine appears in the popup."); + + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Escape") + ); + + // Checks that a hidden default engine (i.e. an engine removed from + // a user's search settings) does not appear in the urlbar's popup. + defaultEngine.hidden = true; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "@", + fireInputEvent: true, + }); + foundDefaultEngineInPopup = false; + for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) { + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + if (defaultEngine.name == details.searchParams.engine) { + foundDefaultEngineInPopup = true; + break; + } + } + Assert.ok( + !foundDefaultEngineInPopup, + "Hidden default engine does not appear in the popup" + ); + + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Escape") + ); + + defaultEngine.hidden = false; +}); + +/** + * This test checks that if an engines alias is not prefixed with + * @ it still appears in the popup when using the "@" token + * alias in the search bar. + */ +add_task(async function nonPrefixedKeyword() { + let name = "Custom"; + let alias = "customkeyword"; + let engine = await Services.search.addEngineWithDetails(name, { + alias, + template: "http://example.com/?search={searchTerms}", + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "@", + }); + + let foundEngineInPopup = false; + + // Checks that the default engine appears in the urlbar's popup. + for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) { + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + if (details.searchParams.engine === name) { + foundEngineInPopup = true; + break; + } + } + Assert.ok(foundEngineInPopup, "Custom engine appears in the popup."); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "@" + alias, + }); + + let keywordOfferResult = await UrlbarTestUtils.getDetailsOfResultAt( + window, + 0 + ); + + Assert.equal( + keywordOfferResult.searchParams.engine, + name, + "The first result should be a keyword search result with the correct engine." + ); + + await Services.search.removeEngine(engine); +}); + +// Tests that we show all engines with a token alias that match the search +// string. +add_task(async function multipleMatchingEngines() { + let testEngineFoo = await Services.search.addEngineWithDetails("TestFoo", { + alias: `${ALIAS}foo`, + template: "http://example-2.com/?search={searchTerms}", + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "@te", + fireInputEvent: true, + }); + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 2, + "Two results are shown." + ); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + -1, + "Neither result is selected." + ); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.ok(result.autofill, "The first result is autofilling."); + Assert.equal( + result.searchParams.keyword, + ALIAS, + "The autofilled engine is shown first." + ); + + result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal( + result.searchParams.keyword, + `${ALIAS}foo`, + "The other engine is shown second." + ); + + EventUtils.synthesizeKey("KEY_Tab"); + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), 0); + Assert.equal(gURLBar.value, "", "Urlbar should be empty."); + EventUtils.synthesizeKey("KEY_Tab"); + Assert.equal(UrlbarTestUtils.getSelectedRowIndex(window), 1); + Assert.equal(gURLBar.value, "", "Urlbar should be empty."); + EventUtils.synthesizeKey("KEY_Tab"); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + -1, + "Tabbing all the way through the matching engines should return to the input." + ); + Assert.equal( + gURLBar.value, + "@te", + "Urlbar should contain the search string." + ); + + await Services.search.removeEngine(testEngineFoo); +}); + +// Tests that UrlbarProviderTokenAliasEngines is disabled in search mode. +add_task(async function doNotShowInSearchMode() { + // Do a search for "@" to show all the @ aliases. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "@", + }); + + // Find our test engine in the results. It's probably last, but for + // robustness don't assume it is. + let testEngineItem; + for (let i = 0; !testEngineItem; i++) { + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + if (details.searchParams && details.searchParams.keyword == ALIAS) { + testEngineItem = await UrlbarTestUtils.waitForAutocompleteResultAt( + window, + i + ); + } + } + + Assert.equal( + testEngineItem.result.payload.keyword, + ALIAS, + "Sanity check: we found our engine." + ); + + // Click it. + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeMouseAtCenter(testEngineItem, {}); + await searchPromise; + await UrlbarTestUtils.assertSearchMode(window, { + engineName: testEngineItem.result.payload.engine, + entry: "keywordoffer", + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "@", + fireInputEvent: true, + }); + + let resultCount = UrlbarTestUtils.getResultCount(window); + for (let i = 0; i < resultCount; i++) { + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + Assert.ok( + !result.searchParams.keyword, + `Result at index ${i} is not a keywordoffer.` + ); + } +}); + +async function assertFirstResultIsAlias(isAlias, expectedAlias) { + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + result.type, + UrlbarUtils.RESULT_TYPE.SEARCH, + "Should have the correct type" + ); + + if (isAlias) { + Assert.equal( + result.searchParams.keyword, + expectedAlias, + "Payload keyword should be the alias" + ); + } else { + Assert.notEqual( + result.searchParams.keyword, + expectedAlias, + "Payload keyword should be absent or not the alias" + ); + } +} + +function assertHighlighted(highlighted, expectedAlias) { + let selection = gURLBar.editor.selectionController.getSelection( + Ci.nsISelectionController.SELECTION_FIND + ); + Assert.ok(selection); + if (!highlighted) { + Assert.equal(selection.rangeCount, 0); + return; + } + Assert.equal(selection.rangeCount, 1); + let index = gURLBar.value.indexOf(expectedAlias); + Assert.ok( + index >= 0, + `gURLBar.value="${gURLBar.value}" expectedAlias="${expectedAlias}"` + ); + let range = selection.getRangeAt(0); + Assert.ok(range); + Assert.equal(range.startOffset, index); + Assert.equal(range.endOffset, index + expectedAlias.length); +} + +/** + * Returns an array of code points in the given string. Each code point is + * returned as a hexidecimal string. + * + * @param {string} str + * The code points of this string will be returned. + * @returns {array} + * Array of code points in the string, where each is a hexidecimal string. + */ +function codePoints(str) { + return str.split("").map(s => s.charCodeAt(0).toString(16)); +} diff --git a/browser/components/urlbar/tests/browser/browser_top_sites.js b/browser/components/urlbar/tests/browser/browser_top_sites.js new file mode 100644 index 0000000000..080d1fcac5 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_top_sites.js @@ -0,0 +1,477 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +XPCOMUtils.defineLazyModuleGetters(this, { + AboutNewTab: "resource:///modules/AboutNewTab.jsm", + NewTabUtils: "resource://gre/modules/NewTabUtils.jsm", +}); + +const EN_US_TOPSITES = + "https://www.youtube.com/,https://www.facebook.com/,https://www.amazon.com/,https://www.reddit.com/,https://www.wikipedia.org/,https://twitter.com/"; + +async function addTestVisits() { + // Add some visits to a URL. + for (let i = 0; i < 5; i++) { + await PlacesTestUtils.addVisits("http://example.com/"); + } + + // Wait for example.com to be listed first. + await updateTopSites(sites => { + return sites && sites[0] && sites[0].url == "http://example.com/"; + }); + + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "https://www.youtube.com/", + title: "YouTube", + }); +} + +async function checkDoesNotOpenOnFocus(win = window) { + // The view should not open when the input is focused programmatically. + win.gURLBar.blur(); + win.gURLBar.focus(); + Assert.ok(!win.gURLBar.view.isOpen, "check urlbar panel is not open"); + win.gURLBar.blur(); + + // Check the keyboard shortcut. + win.document.getElementById("Browser:OpenLocation").doCommand(); + // Because the panel opening may not be immediate, we must wait a bit. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 500)); + Assert.ok(!win.gURLBar.view.isOpen, "check urlbar panel is not open"); + win.gURLBar.blur(); + + // Focus with the mouse. + EventUtils.synthesizeMouseAtCenter(win.gURLBar.inputField, {}, win); + // Because the panel opening may not be immediate, we must wait a bit. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 500)); + Assert.ok(!win.gURLBar.view.isOpen, "check urlbar panel is not open"); + win.gURLBar.blur(); +} + +add_task(async function init() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.topsites", true], + ["browser.newtabpage.activity-stream.default.sites", EN_US_TOPSITES], + ], + }); + + await updateTopSites( + sites => sites && sites.length == EN_US_TOPSITES.split(",").length + ); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "http://example.com/" + ); + + registerCleanupFunction(() => { + BrowserTestUtils.removeTab(tab); + }); +}); + +add_task(async function topSitesShown() { + let sites = AboutNewTab.getTopSites(); + + for (let prefVal of [true, false]) { + // This test should work regardless of whether Top Sites are enabled on + // about:newtab. + await SpecialPowers.pushPrefEnv({ + set: [["browser.newtabpage.activity-stream.feeds.topsites", prefVal]], + }); + // We don't expect this to change, but we run updateTopSites just in case + // feeds.topsites were to have an effect on the composition of Top Sites. + await updateTopSites(siteList => siteList.length == 6); + + await UrlbarTestUtils.promisePopupOpen(window, () => { + if (gURLBar.getAttribute("pageproxystate") == "invalid") { + gURLBar.handleRevert(); + } + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + Assert.ok(gURLBar.view.isOpen, "UrlbarView should be open."); + await UrlbarTestUtils.promiseSearchComplete(window); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + sites.length, + "The number of results should be the same as the number of Top Sites (6)." + ); + + for (let i = 0; i < sites.length; i++) { + let site = sites[i]; + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + if (site.searchTopSite) { + Assert.equal( + result.searchParams.keyword, + site.label, + "The search Top Site should have an alias." + ); + continue; + } + + Assert.equal( + site.url, + result.url, + "The Top Site URL and the result URL shoud match." + ); + Assert.equal( + site.label || site.title || site.hostname, + result.title, + "The Top Site title and the result title shoud match." + ); + } + await UrlbarTestUtils.promisePopupClose(window, () => { + gURLBar.blur(); + }); + // This pops updateTopSites changes. + await SpecialPowers.popPrefEnv(); + await SpecialPowers.popPrefEnv(); + } +}); + +add_task(async function selectSearchTopSite() { + await updateTopSites( + sites => sites && sites[0] && sites[0].searchTopSite, + true + ); + await UrlbarTestUtils.promisePopupOpen(window, () => { + if (gURLBar.getAttribute("pageproxystate") == "invalid") { + gURLBar.handleRevert(); + } + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + await UrlbarTestUtils.promiseSearchComplete(window); + + let amazonSearch = await UrlbarTestUtils.waitForAutocompleteResultAt( + window, + 0 + ); + + Assert.equal( + amazonSearch.result.type, + UrlbarUtils.RESULT_TYPE.SEARCH, + "First result should have SEARCH type." + ); + + Assert.equal( + amazonSearch.result.payload.keyword, + "@amazon", + "First result should have the Amazon keyword." + ); + + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeMouseAtCenter(amazonSearch, {}); + await searchPromise; + await UrlbarTestUtils.assertSearchMode(window, { + engineName: amazonSearch.result.payload.engine, + entry: "topsites_urlbar", + }); + await UrlbarTestUtils.exitSearchMode(window, { backspace: true }); + + await UrlbarTestUtils.promisePopupClose(window, () => { + gURLBar.blur(); + }); +}); + +add_task(async function topSitesBookmarksAndTabs() { + await addTestVisits(); + let sites = AboutNewTab.getTopSites(); + Assert.equal( + sites.length, + 7, + "The test suite browser should have 7 Top Sites." + ); + + await UrlbarTestUtils.promisePopupOpen(window, () => { + if (gURLBar.getAttribute("pageproxystate") == "invalid") { + gURLBar.handleRevert(); + } + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + Assert.ok(gURLBar.view.isOpen, "UrlbarView should be open."); + await UrlbarTestUtils.promiseSearchComplete(window); + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 7, + "The number of results should be the same as the number of Top Sites (7)." + ); + + let exampleResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + exampleResult.url, + "http://example.com/", + "The example.com Top Site should be the first result." + ); + Assert.equal( + exampleResult.source, + UrlbarUtils.RESULT_SOURCE.TABS, + "The example.com Top Site should appear in the view as an open tab result." + ); + + let youtubeResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal( + youtubeResult.url, + "https://www.youtube.com/", + "The YouTube Top Site should be the second result." + ); + Assert.equal( + youtubeResult.source, + UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + "The YouTube Top Site should appear in the view as a bookmark result." + ); + await UrlbarTestUtils.promisePopupClose(window, () => { + gURLBar.blur(); + }); + + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); +}); + +add_task(async function topSitesKeywordNavigationPageproxystate() { + await addTestVisits(); + Assert.equal( + gURLBar.getAttribute("pageproxystate"), + "valid", + "Sanity check initial state" + ); + + await UrlbarTestUtils.promisePopupOpen(window, () => { + if (gURLBar.getAttribute("pageproxystate") == "invalid") { + gURLBar.handleRevert(); + } + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + Assert.ok(gURLBar.view.isOpen, "UrlbarView should be open."); + await UrlbarTestUtils.promiseSearchComplete(window); + + let count = UrlbarTestUtils.getResultCount(window); + Assert.equal(count, 7, "The number of results should be the expected one."); + + for (let i = 0; i < count; ++i) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + Assert.equal( + gURLBar.getAttribute("pageproxystate"), + "invalid", + "Moving through results" + ); + } + for (let i = 0; i < count; ++i) { + EventUtils.synthesizeKey("KEY_ArrowUp"); + Assert.equal( + gURLBar.getAttribute("pageproxystate"), + "invalid", + "Moving through results" + ); + } + + // Double ESC should restore state. + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_Escape"); + }); + EventUtils.synthesizeKey("KEY_Escape"); + Assert.equal( + gURLBar.getAttribute("pageproxystate"), + "valid", + "Double ESC should restore state" + ); + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); +}); + +add_task(async function topSitesPinned() { + await addTestVisits(); + let info = { url: "http://example.com/" }; + NewTabUtils.pinnedLinks.pin(info, 0); + + await updateTopSites(sites => sites && sites[0] && sites[0].isPinned); + + let sites = AboutNewTab.getTopSites(); + Assert.equal( + sites.length, + 7, + "The test suite browser should have 7 Top Sites." + ); + + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + Assert.ok(gURLBar.view.isOpen, "UrlbarView should be open."); + await UrlbarTestUtils.promiseSearchComplete(window); + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 7, + "The number of results should be the same as the number of Top Sites (7)." + ); + + let exampleResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + exampleResult.url, + "http://example.com/", + "The example.com Top Site should be the first result." + ); + + Assert.equal( + exampleResult.source, + UrlbarUtils.RESULT_SOURCE.TABS, + "The example.com Top Site should be an open tab result." + ); + + Assert.ok( + exampleResult.element.row.hasAttribute("pinned"), + "The example.com Top Site should have the pinned property." + ); + + await UrlbarTestUtils.promisePopupClose(window, () => { + gURLBar.blur(); + }); + NewTabUtils.pinnedLinks.unpin(info); + + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); +}); + +add_task(async function topSitesBookmarksAndTabsDisabled() { + await addTestVisits(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.openpage", false], + ["browser.urlbar.suggest.bookmark", false], + ], + }); + + let sites = AboutNewTab.getTopSites(); + Assert.equal( + sites.length, + 7, + "The test suite browser should have 7 Top Sites." + ); + + await UrlbarTestUtils.promisePopupOpen(window, () => { + if (gURLBar.getAttribute("pageproxystate") == "invalid") { + gURLBar.handleRevert(); + } + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + Assert.ok(gURLBar.view.isOpen, "UrlbarView should be open."); + await UrlbarTestUtils.promiseSearchComplete(window); + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 7, + "The number of results should be the same as the number of Top Sites (7)." + ); + + let exampleResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + Assert.equal( + exampleResult.url, + "http://example.com/", + "The example.com Top Site should be the second result." + ); + Assert.equal( + exampleResult.source, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + "The example.com Top Site should appear as a normal result even though it's open in a tab." + ); + + let youtubeResult = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + Assert.equal( + youtubeResult.url, + "https://www.youtube.com/", + "The YouTube Top Site should be the third result." + ); + Assert.equal( + youtubeResult.source, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + "The YouTube Top Site should appear as a normal result even though it's bookmarked." + ); + await UrlbarTestUtils.promisePopupClose(window, () => { + gURLBar.blur(); + }); + + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function topSitesDisabled() { + // Disable Top Sites feed. + await SpecialPowers.pushPrefEnv({ + set: [["browser.newtabpage.activity-stream.feeds.system.topsites", false]], + }); + await checkDoesNotOpenOnFocus(); + await SpecialPowers.popPrefEnv(); + + // Top Sites should also not be shown when Urlbar Top Sites are disabled. + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.topsites", false]], + }); + await checkDoesNotOpenOnFocus(); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function topSitesNumber() { + // Add some visits + for (let i = 0; i < 5; i++) { + await PlacesTestUtils.addVisits([ + "http://example-a.com/", + "http://example-b.com/", + "http://example-c.com/", + "http://example-d.com/", + "http://example-e.com/", + ]); + } + + // Wait for the expected number of Top sites. + await updateTopSites(sites => sites && sites.length == 8); + Assert.equal( + AboutNewTab.getTopSites().length, + 8, + "The test suite browser should have 8 Top Sites." + ); + + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + Assert.ok(gURLBar.view.isOpen, "UrlbarView should be open."); + await UrlbarTestUtils.promiseSearchComplete(window); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 8, + "The number of results should be the default (8)." + ); + await UrlbarTestUtils.promisePopupClose(window); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.newtabpage.activity-stream.topSitesRows", 2]], + }); + // Wait for the expected number of Top sites. + await updateTopSites(sites => sites && sites.length == 11); + Assert.equal( + AboutNewTab.getTopSites().length, + 11, + "The test suite browser should have 11 Top Sites." + ); + + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + Assert.ok(gURLBar.view.isOpen, "UrlbarView should be open."); + await UrlbarTestUtils.promiseSearchComplete(window); + + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 10, + "The number of results should be maxRichResults (10)." + ); + await UrlbarTestUtils.promisePopupClose(window); + + await SpecialPowers.popPrefEnv(); + await PlacesUtils.history.clear(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_top_sites_private.js b/browser/components/urlbar/tests/browser/browser_top_sites_private.js new file mode 100644 index 0000000000..570e623d57 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_top_sites_private.js @@ -0,0 +1,170 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +XPCOMUtils.defineLazyModuleGetters(this, { + AboutNewTab: "resource:///modules/AboutNewTab.jsm", + NewTabUtils: "resource://gre/modules/NewTabUtils.jsm", +}); + +const EN_US_TOPSITES = + "https://www.youtube.com/,https://www.facebook.com/,https://www.amazon.com/,https://www.reddit.com/,https://www.wikipedia.org/,https://twitter.com/"; + +async function addTestVisits() { + // Add some visits to a URL. + for (let i = 0; i < 5; i++) { + await PlacesTestUtils.addVisits("http://example.com/"); + } + + // Wait for example.com to be listed first. + await updateTopSites(sites => { + return sites && sites[0] && sites[0].url == "http://example.com/"; + }); + + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "https://www.youtube.com/", + title: "YouTube", + }); +} + +add_task(async function init() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.topsites", true], + ["browser.newtabpage.activity-stream.default.sites", EN_US_TOPSITES], + ], + }); + + await updateTopSites( + sites => sites && sites.length == EN_US_TOPSITES.split(",").length + ); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "http://example.com/" + ); + + registerCleanupFunction(() => { + BrowserTestUtils.removeTab(tab); + }); +}); + +add_task(async function topSitesPrivateWindow() { + // Top Sites should also be shown in private windows. + let privateWin = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + await addTestVisits(); + let sites = AboutNewTab.getTopSites(); + Assert.equal( + sites.length, + 7, + "The test suite browser should have 7 Top Sites." + ); + let urlbar = privateWin.gURLBar; + await UrlbarTestUtils.promisePopupOpen(privateWin, () => { + if (urlbar.getAttribute("pageproxystate") == "invalid") { + urlbar.handleRevert(); + } + EventUtils.synthesizeMouseAtCenter(urlbar.inputField, {}, privateWin); + }); + Assert.ok(urlbar.view.isOpen, "UrlbarView should be open."); + await UrlbarTestUtils.promiseSearchComplete(privateWin); + + Assert.equal( + UrlbarTestUtils.getResultCount(privateWin), + 7, + "The number of results should be the same as the number of Top Sites (7)." + ); + + // Top sites should also be shown in a private window if the search string + // gets cleared. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: privateWin, + value: "example", + }); + urlbar.select(); + EventUtils.synthesizeKey("KEY_Backspace", {}, privateWin); + Assert.ok(urlbar.view.isOpen, "UrlbarView should be open."); + await UrlbarTestUtils.promiseSearchComplete(privateWin); + Assert.equal( + UrlbarTestUtils.getResultCount(privateWin), + 7, + "The number of results should be the same as the number of Top Sites (7)." + ); + + await BrowserTestUtils.closeWindow(privateWin); + + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); + await UrlbarTestUtils.promisePopupClose(window, () => { + gURLBar.blur(); + }); +}); + +add_task(async function topSitesTabSwitch() { + // Add some visits + for (let i = 0; i < 5; i++) { + await PlacesTestUtils.addVisits(["http://example.com/"]); + } + + // Switch to the originating tab, to check for switch to the current tab. + gBrowser.selectedTab = gBrowser.tabs[0]; + + // Wait for the expected number of Top sites. + await updateTopSites(sites => sites?.length == 7); + Assert.equal( + AboutNewTab.getTopSites().length, + 7, + "The test suite browser should have 7 Top Sites." + ); + + async function checkResults(win, expectedResultType) { + let resultCount = UrlbarTestUtils.getResultCount(win); + let result; + for (let i = 0; i < resultCount; ++i) { + result = await UrlbarTestUtils.getDetailsOfResultAt(win, i); + if (result.url == "http://example.com/") { + break; + } + } + Assert.equal( + result.type, + expectedResultType, + `Should provide a result of type ${expectedResultType}.` + ); + } + + info("Test in a non-private window"); + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + Assert.ok(gURLBar.view.isOpen, "UrlbarView should be open."); + await UrlbarTestUtils.promiseSearchComplete(window); + await checkResults(window, UrlbarUtils.RESULT_TYPE.TAB_SWITCH); + await UrlbarTestUtils.promisePopupClose(window); + + info("Test in a private window, switch to tab should not be offered"); + // Top Sites should also be shown in private windows. + let privateWin = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + }); + + let urlbar = privateWin.gURLBar; + await UrlbarTestUtils.promisePopupOpen(privateWin, () => { + if (urlbar.getAttribute("pageproxystate") == "invalid") { + urlbar.handleRevert(); + } + EventUtils.synthesizeMouseAtCenter(urlbar.inputField, {}, privateWin); + }); + + Assert.ok(urlbar.view.isOpen, "UrlbarView should be open."); + await UrlbarTestUtils.promiseSearchComplete(privateWin); + await checkResults(privateWin, UrlbarUtils.RESULT_TYPE.URL); + await UrlbarTestUtils.promisePopupClose(privateWin); + await BrowserTestUtils.closeWindow(privateWin); + + await PlacesUtils.history.clear(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_typed_value.js b/browser/components/urlbar/tests/browser/browser_typed_value.js new file mode 100644 index 0000000000..a01ee3e08e --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_typed_value.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This test ensures that the urlbar is restored to the typed value on blur. + +"use strict"; + +add_task(async function setup() { + registerCleanupFunction(async function() { + Services.prefs.clearUserPref("browser.urlbar.autoFill"); + gURLBar.handleRevert(); + await PlacesUtils.history.clear(); + }); + Services.prefs.setBoolPref("browser.urlbar.autoFill", true); + + await PlacesTestUtils.addVisits([ + "http://example.com/", + "http://example.com/foo", + ]); +}); + +add_task(async function test_autofill() { + let typed = "ex"; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: typed, + fireInputEvent: true, + }); + Assert.equal(gURLBar.value, "example.com/", "autofilled value as expected"); + Assert.equal(gURLBar.selectionStart, typed.length); + Assert.equal(gURLBar.selectionEnd, gURLBar.value.length); + + gURLBar.blur(); + Assert.equal(gURLBar.value, typed, "Value should have been restored"); + gURLBar.focus(); + Assert.equal(gURLBar.value, typed, "Value should not have changed"); + Assert.equal(gURLBar.selectionStart, typed.length); + Assert.equal(gURLBar.selectionEnd, typed.length); +}); + +add_task(async function test_complete_selection() { + let typed = "ex"; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: typed, + fireInputEvent: true, + }); + Assert.equal(gURLBar.value, "example.com/", "autofilled value as expected"); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 2, + "Should have the correct number of matches" + ); + EventUtils.synthesizeKey("KEY_ArrowDown"); + Assert.equal( + gURLBar.value, + "example.com/foo", + "Value should have been completed" + ); + + gURLBar.blur(); + Assert.equal(gURLBar.value, typed, "Value should have been restored"); + gURLBar.focus(); + Assert.equal(gURLBar.value, typed, "Value should not have changed"); + Assert.equal(gURLBar.selectionStart, typed.length); + Assert.equal(gURLBar.selectionEnd, typed.length); +}); diff --git a/browser/components/urlbar/tests/browser/browser_updateForDomainCompletion.js b/browser/components/urlbar/tests/browser/browser_updateForDomainCompletion.js new file mode 100644 index 0000000000..27b097cdd7 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_updateForDomainCompletion.js @@ -0,0 +1,24 @@ +"use strict"; + +/** + * Disable keyword.enabled (so no keyword search), and check that when you type in + * "example" and hit enter, the browser loads and the URL bar is updated accordingly. + */ +add_task(async function() { + await SpecialPowers.pushPrefEnv({ set: [["keyword.enabled", false]] }); + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:blank" }, + async function(browser) { + gURLBar.value = "example"; + gURLBar.select(); + const loadPromise = BrowserTestUtils.waitForDocLoadAndStopIt( + "https://www.example.com/", + gBrowser.selectedBrowser + ); + + EventUtils.sendKey("return"); + await loadPromise; + ok(true, "https://www.example.com is loaded correctly"); + } + ); +}); diff --git a/browser/components/urlbar/tests/browser/browser_updateRows.js b/browser/components/urlbar/tests/browser/browser_updateRows.js new file mode 100644 index 0000000000..b764c166bf --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_updateRows.js @@ -0,0 +1,240 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests row updating and reuse. + +"use strict"; + +let TEST_BASE_URL = "http://example.com/"; + +add_task(async function init() { + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); +}); + +// A URL result is replaced with a tip result and then vice versa. +add_task(async function urlToTip() { + // Add some visits that will be matched by a "test" search string. + await PlacesTestUtils.addVisits([ + "http://example.com/testxx", + "http://example.com/test", + ]); + + // Add a provider that returns a tip result when the search string is "testx". + let tipResult = new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TIP, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + text: "This is a test tip.", + buttonText: "OK", + helpUrl: "http://example.com/", + type: "test", + } + ); + tipResult.suggestedIndex = 1; + let provider = new UrlbarTestUtils.TestProvider({ + results: [tipResult], + }); + provider.isActive = context => context.searchString == "testx"; + UrlbarProvidersManager.registerProvider(provider); + + // Search for "test". + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + value: "test", + window, + }); + + // The result at index 1 should be the http://example.com/test visit. + await checkResult( + 1, + UrlbarUtils.RESULT_TYPE.URL, + { + title: "test visit for http://example.com/test", + tagsContainer: null, + titleSeparator: null, + action: "", + url: TEST_BASE_URL + "test", + }, + ["tipButton", "helpButton"] + ); + + // Type an "x" so that the search string is "testx". + EventUtils.synthesizeKey("x"); + await UrlbarTestUtils.promiseSearchComplete(window); + + // Now the result at index 1 should be the tip from our provider. + await checkResult( + 1, + UrlbarUtils.RESULT_TYPE.TIP, + { + title: "This is a test tip.", + tipButton: "OK", + helpButton: null, + }, + ["tagsContainer", "titleSeparator", "action", "url"] + ); + + // Type another "x" so that the search string is "testxx". + EventUtils.synthesizeKey("x"); + await UrlbarTestUtils.promiseSearchComplete(window); + + // The result at index 1 should be the http://example.com/testxx visit. + await checkResult( + 1, + UrlbarUtils.RESULT_TYPE.URL, + { + title: "test visit for http://example.com/testxx", + tagsContainer: null, + titleSeparator: null, + action: "", + url: TEST_BASE_URL + "testxx", + }, + ["tipButton", "helpButton"] + ); + + // Backspace so that the search string is "testx" again. + EventUtils.synthesizeKey("KEY_Backspace"); + await UrlbarTestUtils.promiseSearchComplete(window); + + // The result at index 1 should be the tip again. + await checkResult( + 1, + UrlbarUtils.RESULT_TYPE.TIP, + { + title: "This is a test tip.", + tipButton: "OK", + helpButton: null, + }, + ["tagsContainer", "titleSeparator", "action", "url"] + ); + + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_Escape"); + }); + UrlbarProvidersManager.unregisterProvider(provider); + await PlacesUtils.history.clear(); +}); + +// A tip result is replaced with URL result and then vice versa. +add_task(async function tipToURL() { + // Add a visit that will be matched by a "testx" search string. + await PlacesTestUtils.addVisits("http://example.com/testx"); + + // Add a provider that returns a tip result when the search string is "test" + // or "testxx". + let tipResult = new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TIP, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + text: "This is a test tip.", + buttonText: "OK", + helpUrl: "http://example.com/", + type: "test", + } + ); + tipResult.suggestedIndex = 1; + let provider = new UrlbarTestUtils.TestProvider({ + results: [tipResult], + }); + provider.isActive = context => + ["test", "testxx"].includes(context.searchString); + UrlbarProvidersManager.registerProvider(provider); + + // Search for "test". + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + value: "test", + window, + }); + + // The result at index 1 should be the tip from our provider. + await checkResult( + 1, + UrlbarUtils.RESULT_TYPE.TIP, + { + title: "This is a test tip.", + tipButton: "OK", + helpButton: null, + }, + ["tagsContainer", "titleSeparator", "action", "url"] + ); + + // Type an "x" so that the search string is "testx". + EventUtils.synthesizeKey("x"); + await UrlbarTestUtils.promiseSearchComplete(window); + + // Now the result at index 1 should be the visit. + await checkResult( + 1, + UrlbarUtils.RESULT_TYPE.URL, + { + title: "test visit for http://example.com/testx", + tagsContainer: null, + titleSeparator: null, + action: "", + url: TEST_BASE_URL + "testx", + }, + ["tipButton", "helpButton"] + ); + + // Type another "x" so that the search string is "testxx". + EventUtils.synthesizeKey("x"); + await UrlbarTestUtils.promiseSearchComplete(window); + + // The result at index 1 should be the tip again. + await checkResult( + 1, + UrlbarUtils.RESULT_TYPE.TIP, + { + title: "This is a test tip.", + tipButton: "OK", + helpButton: null, + }, + ["tagsContainer", "titleSeparator", "action", "url"] + ); + + // Backspace so that the search string is "testx" again. + EventUtils.synthesizeKey("KEY_Backspace"); + await UrlbarTestUtils.promiseSearchComplete(window); + + // The result at index 1 should be the visit again. + await checkResult( + 1, + UrlbarUtils.RESULT_TYPE.URL, + { + title: "test visit for http://example.com/testx", + tagsContainer: null, + titleSeparator: null, + action: "", + url: TEST_BASE_URL + "testx", + }, + ["tipButton", "helpButton"] + ); + + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeKey("KEY_Escape"); + }); + UrlbarProvidersManager.unregisterProvider(provider); + await PlacesUtils.history.clear(); +}); + +async function checkResult(index, type, presentElements, absentElements) { + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, index); + Assert.equal(result.type, type, "Expected result type"); + + for (let [name, value] of Object.entries(presentElements)) { + let element = result.element.row._elements.get(name); + Assert.ok(element, `${name} should be present`); + if (typeof value == "string") { + Assert.equal( + element.textContent, + value, + `${name} value should be correct` + ); + } + } + + for (let name of absentElements) { + let element = result.element.row._elements.get(name); + Assert.ok(!element, `${name} should be absent`); + } +} diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_event_telemetry.js b/browser/components/urlbar/tests/browser/browser_urlbar_event_telemetry.js new file mode 100644 index 0000000000..221128d36e --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_event_telemetry.js @@ -0,0 +1,1498 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +XPCOMUtils.defineLazyModuleGetters(this, { + UrlbarProvider: "resource:///modules/UrlbarUtils.jsm", + UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.jsm", + UrlbarUtils: "resource:///modules/UrlbarUtils.jsm", +}); + +const TEST_ENGINE_NAME = "Test"; +const TEST_ENGINE_ALIAS = "@test"; +const TEST_ENGINE_DOMAIN = "example.com"; + +function copyToClipboard(str) { + return new Promise((resolve, reject) => { + waitForClipboard( + str, + () => { + Cc["@mozilla.org/widget/clipboardhelper;1"] + .getService(Ci.nsIClipboardHelper) + .copyString(str); + }, + resolve, + reject + ); + }); +} + +// Each test is a function that executes an urlbar action and returns the +// expected event object, or null if no event is expected. +const tests = [ + /* + * Engagement tests. + */ + async function(win) { + info("Type something, press Enter."); + win.gURLBar.select(); + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "x", + fireInputEvent: true, + }); + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + return { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "1", + numWords: "1", + selIndex: "0", + selType: "searchengine", + provider: "HeuristicFallback", + }, + }; + }, + + async function(win) { + info("Type a multi-word query, press Enter."); + win.gURLBar.select(); + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "multi word query ", + fireInputEvent: true, + }); + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + return { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "17", + numWords: "3", + selIndex: "0", + selType: "searchengine", + provider: "HeuristicFallback", + }, + }; + }, + + async function(win) { + info("Paste something, press Enter."); + win.gURLBar.select(); + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await SimpleTest.promiseClipboardChange("test", () => { + clipboardHelper.copyString("test"); + }); + win.document.commandDispatcher + .getControllerForCommand("cmd_paste") + .doCommand("cmd_paste"); + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + return { + category: "urlbar", + method: "engagement", + object: "enter", + value: "pasted", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "4", + numWords: "1", + selIndex: "0", + selType: "searchengine", + provider: "HeuristicFallback", + }, + }; + }, + + async function(win) { + info("Type something, click one-off and press enter."); + win.gURLBar.select(); + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "test", + fireInputEvent: true, + }); + + let searchPromise = UrlbarTestUtils.promiseSearchComplete(win); + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }, win); + let selectedOneOff = UrlbarTestUtils.getOneOffSearchButtons(win) + .selectedButton; + selectedOneOff.click(); + await searchPromise; + await UrlbarTestUtils.assertSearchMode(window, { + engineName: selectedOneOff.engine.name, + entry: "oneoff", + }); + + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + return { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "4", + numWords: "1", + selIndex: "0", + selType: "searchengine", + provider: "HeuristicFallback", + }, + }; + }, + + async function(win) { + info( + "Type something, select one-off with enter, and select result with enter." + ); + win.gURLBar.select(); + + let searchPromise = UrlbarTestUtils.promiseSearchComplete(win); + + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "test", + fireInputEvent: true, + }); + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }, win); + let selectedOneOff = UrlbarTestUtils.getOneOffSearchButtons(win) + .selectedButton; + Assert.ok(selectedOneOff); + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await searchPromise; + + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + return { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "4", + numWords: "1", + selIndex: "0", + selType: "searchengine", + provider: "HeuristicFallback", + }, + }; + }, + + async function(win) { + info("Type something, ESC, type something else, press Enter."); + win.gURLBar.select(); + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + EventUtils.synthesizeKey("x", {}, win); + EventUtils.synthesizeKey("VK_ESCAPE", {}, win); + EventUtils.synthesizeKey("y", {}, win); + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + return { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "1", + numWords: "1", + selIndex: "0", + selType: "searchengine", + provider: "HeuristicFallback", + }, + }; + }, + + async function(win) { + info("Type a keyword, Enter."); + win.gURLBar.select(); + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "kw test", + fireInputEvent: true, + }); + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + return { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "7", + numWords: "2", + selIndex: "0", + selType: "keyword", + provider: "UnifiedComplete", + }, + }; + }, + + async function(win) { + let tipProvider = registerTipProvider(); + info("Selecting a tip's main button, enter."); + win.gURLBar.search("x"); + await UrlbarTestUtils.promiseSearchComplete(win); + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + EventUtils.synthesizeKey("VK_RETURN", {}, win); + unregisterTipProvider(tipProvider); + return { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "1", + numWords: "1", + selIndex: "1", + selType: "tip", + provider: tipProvider.name, + }, + }; + }, + + async function(win) { + let tipProvider = registerTipProvider(); + info("Selecting a tip's help button, enter."); + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + win.gURLBar.search("x"); + await UrlbarTestUtils.promiseSearchComplete(win); + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + unregisterTipProvider(tipProvider); + return { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "1", + numWords: "1", + selIndex: "1", + selType: "tiphelp", + provider: tipProvider.name, + }, + }; + }, + + async function(win) { + info("Type something and canonize"); + win.gURLBar.select(); + const promise = BrowserTestUtils.waitForDocLoadAndStopIt( + "https://www.example.com/", + gBrowser.selectedBrowser + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "example", + fireInputEvent: true, + }); + EventUtils.synthesizeKey("VK_RETURN", { ctrlKey: true }, win); + await promise; + return { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "7", + numWords: "1", + selIndex: "0", + selType: "canonized", + provider: "Autofill", + }, + }; + }, + + async function(win) { + info("Type something, click on bookmark entry."); + win.gURLBar.select(); + let url = "http://example.com/?q=%s"; + let promise = BrowserTestUtils.browserLoaded( + win.gBrowser.selectedBrowser, + false, + url + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "exa", + fireInputEvent: true, + }); + while (win.gURLBar.untrimmedValue != url) { + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + } + let element = UrlbarTestUtils.getSelectedRow(win); + EventUtils.synthesizeMouseAtCenter(element, {}, win); + await promise; + return { + category: "urlbar", + method: "engagement", + object: "click", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "3", + numWords: "1", + selIndex: val => parseInt(val) > 0, + selType: "bookmark", + provider: "UnifiedComplete", + }, + }; + }, + + async function(win) { + info("Type an autofilled string, Enter."); + win.gURLBar.select(); + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "exa", + fireInputEvent: true, + }); + // Check it's autofilled. + Assert.equal(win.gURLBar.selectionStart, 3); + Assert.equal(win.gURLBar.selectionEnd, 12); + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + return { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "3", + numWords: "1", + selIndex: "0", + selType: "autofill", + provider: "Autofill", + }, + }; + }, + + async function(win) { + info("Type something, select bookmark entry, Enter."); + win.gURLBar.select(); + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "exa", + fireInputEvent: true, + }); + while (win.gURLBar.untrimmedValue != "http://example.com/?q=%s") { + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + } + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + return { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "3", + numWords: "1", + selIndex: val => parseInt(val) > 0, + selType: "bookmark", + provider: "UnifiedComplete", + }, + }; + }, + + async function(win) { + info("Type something, select remote search suggestion, Enter."); + win.gURLBar.select(); + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "foo", + fireInputEvent: true, + }); + while (win.gURLBar.untrimmedValue != "foofoo") { + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + } + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + return { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "3", + numWords: "1", + selIndex: val => parseInt(val) > 0, + selType: "searchsuggestion", + provider: "SearchSuggestions", + }, + }; + }, + + async function(win) { + info("Type something, select form history, Enter."); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.maxHistoricalSearchSuggestions", 2]], + }); + await UrlbarTestUtils.formHistory.add(["foofoo", "foobar"]); + win.gURLBar.select(); + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "foo", + fireInputEvent: true, + }); + while (win.gURLBar.untrimmedValue != "foofoo") { + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + } + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + await SpecialPowers.popPrefEnv(); + return { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "3", + numWords: "1", + selIndex: val => parseInt(val) > 0, + selType: "formhistory", + provider: "SearchSuggestions", + }, + }; + }, + + async function(win) { + info("Type @, enter on a keywordoffer, then search and press enter."); + win.gURLBar.select(); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "@", + fireInputEvent: true, + }); + + while (win.gURLBar.searchMode?.engineName != TEST_ENGINE_NAME) { + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + } + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await UrlbarTestUtils.promiseSearchComplete(win); + + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "moz", + fireInputEvent: true, + }); + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + + return { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "3", + numWords: "1", + selIndex: "0", + selType: "searchengine", + provider: "HeuristicFallback", + }, + }; + }, + + async function(win) { + info("Type an @alias, then space, then search and press enter."); + const alias = "testalias"; + let aliasEngine = await Services.search.addEngineWithDetails("AliasTest", { + alias, + template: "http://example.com/?search={searchTerms}", + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: `${alias} `, + }); + + await UrlbarTestUtils.assertSearchMode(window, { + engineName: "AliasTest", + entry: "typed", + }); + + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "moz", + fireInputEvent: true, + }); + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + + await Services.search.removeEngine(aliasEngine); + return { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "3", + numWords: "1", + selIndex: "0", + selType: "searchengine", + provider: "HeuristicFallback", + }, + }; + }, + + async function(win) { + info("Drop something."); + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + EventUtils.synthesizeDrop( + win.document.getElementById("home-button"), + win.gURLBar.inputField, + [[{ type: "text/plain", data: "www.example.com" }]], + "copy", + win + ); + await promise; + return { + category: "urlbar", + method: "engagement", + object: "drop_go", + value: "dropped", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "15", + numWords: "1", + selIndex: "-1", + selType: "none", + provider: "", + }, + }; + }, + + async function(win) { + info("Paste & Go something."); + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await SimpleTest.promiseClipboardChange("www.example.com", () => { + clipboardHelper.copyString("www.example.com"); + }); + let inputBox = win.gURLBar.querySelector("moz-input-box"); + let cxmenu = inputBox.menupopup; + let cxmenuPromise = BrowserTestUtils.waitForEvent(cxmenu, "popupshown"); + EventUtils.synthesizeMouseAtCenter( + win.gURLBar.inputField, + { + type: "contextmenu", + button: 2, + }, + win + ); + await cxmenuPromise; + let menuitem = inputBox.getMenuItem("paste-and-go"); + EventUtils.synthesizeMouseAtCenter(menuitem, {}, win); + await promise; + return { + category: "urlbar", + method: "engagement", + object: "paste_go", + value: "pasted", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "15", + numWords: "1", + selIndex: "-1", + selType: "none", + provider: "", + }, + }; + }, + + // The URLs in the down arrow/autoOpen tests must vary from test to test, + // else the first Top Site results will be a switch-to-tab result and a page + // load will not occur. + async function(win) { + info("Open the panel with DOWN, select with DOWN, Enter."); + await addTopSite("http://example.org/"); + win.gURLBar.value = ""; + win.gURLBar.select(); + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await UrlbarTestUtils.promisePopupOpen(win, () => { + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + }); + await UrlbarTestUtils.promiseSearchComplete(win); + while (win.gURLBar.untrimmedValue != "http://example.org/") { + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + } + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + return { + category: "urlbar", + method: "engagement", + object: "enter", + value: "topsites", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "0", + numWords: "0", + selType: "history", + selIndex: val => parseInt(val) >= 0, + provider: "UrlbarProviderTopSites", + }, + }; + }, + + async function(win) { + info("Open the panel with DOWN, click on entry."); + await addTopSite("http://example.com/"); + win.gURLBar.value = ""; + win.gURLBar.select(); + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await UrlbarTestUtils.promisePopupOpen(win, () => { + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + }); + while (win.gURLBar.untrimmedValue != "http://example.com/") { + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + } + let element = UrlbarTestUtils.getSelectedRow(win); + EventUtils.synthesizeMouseAtCenter(element, {}, win); + await promise; + return { + category: "urlbar", + method: "engagement", + object: "click", + value: "topsites", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "0", + numWords: "0", + selType: "history", + selIndex: "0", + provider: "UrlbarProviderTopSites", + }, + }; + }, + + // The URLs in the autoOpen tests must vary from test to test, else + // the first Top Site results will be a switch-to-tab result and a page load + // will not occur. + async function(win) { + info( + "With pageproxystate=valid, autoopen the panel, select with DOWN, Enter." + ); + await addTopSite("http://example.org/"); + win.gURLBar.value = ""; + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await UrlbarTestUtils.promisePopupOpen(win, () => { + win.document.getElementById("Browser:OpenLocation").doCommand(); + }); + await UrlbarTestUtils.promiseSearchComplete(win); + while (win.gURLBar.untrimmedValue != "http://example.org/") { + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + } + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + return { + category: "urlbar", + method: "engagement", + object: "enter", + value: "topsites", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "0", + numWords: "0", + selType: "history", + selIndex: val => parseInt(val) >= 0, + provider: "UrlbarProviderTopSites", + }, + }; + }, + + async function(win) { + info("With pageproxystate=valid, autoopen the panel, click on entry."); + await addTopSite("http://example.com/"); + win.gURLBar.value = ""; + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await UrlbarTestUtils.promisePopupOpen(win, () => { + win.document.getElementById("Browser:OpenLocation").doCommand(); + }); + await UrlbarTestUtils.promiseSearchComplete(win); + while (win.gURLBar.untrimmedValue != "http://example.com/") { + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + } + let element = UrlbarTestUtils.getSelectedRow(win); + EventUtils.synthesizeMouseAtCenter(element, {}, win); + await promise; + return { + category: "urlbar", + method: "engagement", + object: "click", + value: "topsites", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "0", + numWords: "0", + selType: "history", + selIndex: "0", + provider: "UrlbarProviderTopSites", + }, + }; + }, + + async function(win) { + info("With pageproxystate=invalid, open retained results, Enter."); + await addTopSite("http://example.org/"); + win.gURLBar.value = "example.org"; + win.gURLBar.setPageProxyState("invalid"); + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await UrlbarTestUtils.promisePopupOpen(win, () => { + win.document.getElementById("Browser:OpenLocation").doCommand(); + }); + await UrlbarTestUtils.promiseSearchComplete(win); + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + return { + category: "urlbar", + method: "engagement", + object: "enter", + value: "returned", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "11", + numWords: "1", + selType: "autofill", + selIndex: "0", + provider: "Autofill", + }, + }; + }, + + async function(win) { + info("With pageproxystate=invalid, open retained results, click on entry."); + // This value must be different from the previous test, to avoid reopening + // the view. + win.gURLBar.value = "example.com"; + win.gURLBar.setPageProxyState("invalid"); + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await UrlbarTestUtils.promisePopupOpen(win, () => { + win.document.getElementById("Browser:OpenLocation").doCommand(); + }); + await UrlbarTestUtils.promiseSearchComplete(win); + let element = UrlbarTestUtils.getSelectedRow(win); + EventUtils.synthesizeMouseAtCenter(element, {}, win); + await promise; + return { + category: "urlbar", + method: "engagement", + object: "click", + value: "returned", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "11", + numWords: "1", + selType: "autofill", + selIndex: "0", + provider: "Autofill", + }, + }; + }, + + async function(win) { + info("Reopen the view: type, blur, focus, confirm."); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "search", + fireInputEvent: true, + }); + await UrlbarTestUtils.promisePopupClose(win, () => { + win.gURLBar.blur(); + }); + await UrlbarTestUtils.promisePopupOpen(win, () => { + win.document.getElementById("Browser:OpenLocation").doCommand(); + }); + await UrlbarTestUtils.promiseSearchComplete(win); + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + return [ + { + category: "urlbar", + method: "abandonment", + object: "blur", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "6", + numWords: "1", + }, + }, + { + category: "urlbar", + method: "engagement", + object: "enter", + value: "returned", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "6", + numWords: "1", + selType: "searchengine", + selIndex: "0", + provider: "HeuristicFallback", + }, + }, + ]; + }, + + async function(win) { + info("Open search mode with a keyboard shortcut."); + let defaultEngine = await Services.search.getDefault(); + win.gURLBar.select(); + EventUtils.synthesizeKey("k", { accelKey: true }); + await UrlbarTestUtils.assertSearchMode(win, { + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + engineName: defaultEngine.name, + entry: "shortcut", + }); + + // Execute a search to finish the engagement. + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "moz", + fireInputEvent: true, + }); + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + + return { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "3", + numWords: "1", + selIndex: "0", + selType: "searchengine", + provider: "HeuristicFallback", + }, + }; + }, + + async function(win) { + info("Open search mode from a tab-to-search result."); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.tabToSearch.onboard.interactionsLeft", 0]], + }); + + await PlacesUtils.history.clear(); + for (let i = 0; i < 3; i++) { + await PlacesTestUtils.addVisits([`https://${TEST_ENGINE_DOMAIN}/`]); + } + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: TEST_ENGINE_DOMAIN.slice(0, 4), + }); + + let tabToSearchResult = ( + await UrlbarTestUtils.waitForAutocompleteResultAt(win, 1) + ).result; + Assert.equal( + tabToSearchResult.providerName, + "TabToSearch", + "The second result is a tab-to-search result." + ); + + // Select the tab-to-search result. + EventUtils.synthesizeKey("KEY_ArrowDown"); + let searchPromise = UrlbarTestUtils.promiseSearchComplete(win); + EventUtils.synthesizeKey("KEY_Enter"); + await searchPromise; + await UrlbarTestUtils.assertSearchMode(win, { + engineName: TEST_ENGINE_NAME, + entry: "tabtosearch", + }); + + // Execute a search to finish the engagement. + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "moz", + fireInputEvent: true, + }); + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + + await PlacesUtils.history.clear(); + await SpecialPowers.popPrefEnv(); + + return { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "3", + numWords: "1", + selIndex: "0", + selType: "searchengine", + provider: "HeuristicFallback", + }, + }; + }, + + async function(win) { + info("Sanity check we are not stuck on 'returned'"); + win.gURLBar.select(); + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "x", + fireInputEvent: true, + }); + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + return { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "1", + numWords: "1", + selIndex: "0", + selType: "searchengine", + provider: "HeuristicFallback", + }, + }; + }, + + async function(win) { + info("Reopen the view: type, blur, focus, backspace, type, confirm."); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "search", + fireInputEvent: true, + }); + await UrlbarTestUtils.promisePopupClose(win, () => { + win.gURLBar.blur(); + }); + await UrlbarTestUtils.promisePopupOpen(win, () => { + win.document.getElementById("Browser:OpenLocation").doCommand(); + }); + EventUtils.synthesizeKey("VK_RIGHT", {}, win); + EventUtils.synthesizeKey("VK_BACK_SPACE", {}, win); + EventUtils.synthesizeKey("x", {}, win); + await UrlbarTestUtils.promiseSearchComplete(win); + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + return [ + { + category: "urlbar", + method: "abandonment", + object: "blur", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "6", + numWords: "1", + }, + }, + { + category: "urlbar", + method: "engagement", + object: "enter", + value: "returned", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "6", + numWords: "1", + selType: "searchengine", + selIndex: "0", + provider: "HeuristicFallback", + }, + }, + ]; + }, + + async function(win) { + info("Reopen the view: type, blur, focus, type (overwrite), confirm."); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "search", + fireInputEvent: true, + }); + await UrlbarTestUtils.promisePopupClose(win, () => { + win.gURLBar.blur(); + }); + await UrlbarTestUtils.promisePopupOpen(win, () => { + win.document.getElementById("Browser:OpenLocation").doCommand(); + }); + EventUtils.synthesizeKey("x", {}, win); + await UrlbarTestUtils.promiseSearchComplete(win); + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + return [ + { + category: "urlbar", + method: "abandonment", + object: "blur", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "6", + numWords: "1", + }, + }, + { + category: "urlbar", + method: "engagement", + object: "enter", + value: "restarted", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "1", + numWords: "1", + selType: "searchengine", + selIndex: "0", + provider: "HeuristicFallback", + }, + }, + ]; + }, + + async function(win) { + info("Sanity check we are not stuck on 'restarted'"); + win.gURLBar.select(); + let promise = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "x", + fireInputEvent: true, + }); + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + return { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "1", + numWords: "1", + selIndex: "0", + selType: "searchengine", + provider: "HeuristicFallback", + }, + }; + }, + + /* + * Abandonment tests. + */ + + async function(win) { + info("Type something, blur."); + win.gURLBar.select(); + EventUtils.synthesizeKey("x", {}, win); + win.gURLBar.blur(); + return { + category: "urlbar", + method: "abandonment", + object: "blur", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "1", + numWords: "1", + }, + }; + }, + + async function(win) { + info("Open the panel with DOWN, don't type, blur it."); + await addTopSite("http://example.org/"); + win.gURLBar.value = ""; + win.gURLBar.select(); + await UrlbarTestUtils.promisePopupOpen(win, () => { + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + }); + win.gURLBar.blur(); + return { + category: "urlbar", + method: "abandonment", + object: "blur", + value: "topsites", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "0", + numWords: "0", + }, + }; + }, + + async function(win) { + info("With pageproxystate=valid, autoopen the panel, don't type, blur it."); + win.gURLBar.value = ""; + await UrlbarTestUtils.promisePopupOpen(win, () => { + win.document.getElementById("Browser:OpenLocation").doCommand(); + }); + win.gURLBar.blur(); + return { + category: "urlbar", + method: "abandonment", + object: "blur", + value: "topsites", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "0", + numWords: "0", + }, + }; + }, + + async function(win) { + info("Enter search mode from Top Sites."); + await updateTopSites(sites => true, /* enableSearchShorcuts */ true); + + win.gURLBar.value = ""; + win.gURLBar.select(); + await UrlbarTestUtils.promisePopupOpen(win, () => { + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + }); + while (win.gURLBar.searchMode?.engineName != "Google") { + EventUtils.synthesizeKey("KEY_ArrowDown", {}, win); + } + let element = UrlbarTestUtils.getSelectedRow(win); + Assert.ok( + element.result.source == UrlbarUtils.RESULT_SOURCE.SEARCH, + "The selected result is a search Top Site." + ); + + let engine = element.result.payload.engine; + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeMouseAtCenter(element, {}, win); + await searchPromise; + await UrlbarTestUtils.assertSearchMode(win, { + engineName: engine, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + entry: "topsites_urlbar", + }); + + await UrlbarTestUtils.exitSearchMode(win); + + // To avoid needing to add a custom search shortcut Top Site, we just + // abandon this interaction. + await UrlbarTestUtils.promisePopupClose(win, () => { + win.gURLBar.blur(); + }); + + return { + category: "urlbar", + method: "abandonment", + object: "blur", + value: "topsites", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "0", + numWords: "0", + }, + }; + }, + + async function(win) { + info("Open search mode from a tab-to-search result."); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.tabToSearch.onboard.interactionsLeft", 0]], + }); + + await PlacesUtils.history.clear(); + for (let i = 0; i < 3; i++) { + await PlacesTestUtils.addVisits([`https://${TEST_ENGINE_DOMAIN}/`]); + } + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: TEST_ENGINE_DOMAIN.slice(0, 4), + }); + + let tabToSearchResult = ( + await UrlbarTestUtils.waitForAutocompleteResultAt(win, 1) + ).result; + Assert.equal( + tabToSearchResult.providerName, + "TabToSearch", + "The second result is a tab-to-search result." + ); + + // Select the tab-to-search result. + EventUtils.synthesizeKey("KEY_ArrowDown"); + let searchPromise = UrlbarTestUtils.promiseSearchComplete(win); + EventUtils.synthesizeKey("KEY_Enter"); + await searchPromise; + await UrlbarTestUtils.assertSearchMode(win, { + engineName: TEST_ENGINE_NAME, + entry: "tabtosearch", + }); + + // Abandon the interaction since simply entering search mode is not + // considered the end of an engagement. + await UrlbarTestUtils.promisePopupClose(win, () => { + win.gURLBar.blur(); + }); + + await PlacesUtils.history.clear(); + await SpecialPowers.popPrefEnv(); + + return { + category: "urlbar", + method: "abandonment", + object: "blur", + value: "typed", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "0", + numWords: "0", + }, + }; + }, + + async function(win) { + info( + "With pageproxystate=invalid, open retained results, don't type, blur it." + ); + win.gURLBar.value = "mochi.test"; + win.gURLBar.setPageProxyState("invalid"); + await UrlbarTestUtils.promisePopupOpen(win, () => { + win.document.getElementById("Browser:OpenLocation").doCommand(); + }); + win.gURLBar.blur(); + return { + category: "urlbar", + method: "abandonment", + object: "blur", + value: "returned", + extra: { + elapsed: val => parseInt(val) > 0, + numChars: "10", + numWords: "1", + }, + }; + }, +]; + +const noEventTests = [ + async function(win) { + info("Type something, click on search settings."); + await BrowserTestUtils.withNewTab( + { gBrowser: win.gBrowser, url: "about:blank" }, + async browser => { + win.gURLBar.select(); + let promise = BrowserTestUtils.browserLoaded(browser); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "x", + fireInputEvent: true, + }); + UrlbarTestUtils.getOneOffSearchButtons(win).settingsButton.click(); + await promise; + } + ); + return null; + }, + + async function(win) { + info("Type something, Up, Enter on search settings."); + await BrowserTestUtils.withNewTab( + { gBrowser: win.gBrowser, url: "about:blank" }, + async browser => { + win.gURLBar.select(); + let promise = BrowserTestUtils.browserLoaded(browser); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + value: "x", + fireInputEvent: true, + }); + EventUtils.synthesizeKey("KEY_ArrowUp", {}, win); + Assert.ok( + UrlbarTestUtils.getOneOffSearchButtons( + win + ).selectedButton.classList.contains("search-setting-button-compact"), + "Should have selected the settings button" + ); + EventUtils.synthesizeKey("VK_RETURN", {}, win); + await promise; + } + ); + return null; + }, +]; + +add_task(async function test() { + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.eventTelemetry.enabled", true], + ["browser.urlbar.suggest.searches", true], + ], + }); + // Create a new search engine and mark it as default + let engine = await SearchTestUtils.promiseNewSearchEngine( + getRootDirectory(gTestPath) + "searchSuggestionEngine.xml" + ); + let oldDefaultEngine = await Services.search.getDefault(); + await Services.search.setDefault(engine); + await Services.search.moveEngine(engine, 0); + + let aliasEngine = await Services.search.addEngineWithDetails( + TEST_ENGINE_NAME, + { + alias: TEST_ENGINE_ALIAS, + template: `http://${TEST_ENGINE_DOMAIN}/?search={searchTerms}`, + } + ); + + // Add a bookmark and a keyword. + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.com/?q=%s", + title: "test", + }); + await PlacesUtils.keywords.insert({ + keyword: "kw", + url: "http://example.com/?q=%s", + }); + await PlacesTestUtils.addVisits([ + { + uri: "http://mochi.test:8888/", + transition: PlacesUtils.history.TRANSITIONS.TYPED, + }, + ]); + + registerCleanupFunction(async function() { + await Services.search.setDefault(oldDefaultEngine); + await Services.search.removeEngine(aliasEngine); + await PlacesUtils.keywords.remove("kw"); + await PlacesUtils.bookmarks.remove(bm); + await PlacesUtils.history.clear(); + await UrlbarTestUtils.formHistory.clear(window); + }); + + // This is not necessary after each loop, because assertEvents does it. + Services.telemetry.clearEvents(); + + for (let i = 0; i < tests.length; i++) { + info(`Running test at index ${i}`); + let events = await tests[i](window); + if (!Array.isArray(events)) { + events = [events]; + } + // Always blur to ensure it's not accounted as an additional abandonment. + window.gURLBar.setSearchMode({}); + gURLBar.blur(); + TelemetryTestUtils.assertEvents(events, { category: "urlbar" }); + await UrlbarTestUtils.formHistory.clear(window); + } + + for (let i = 0; i < noEventTests.length; i++) { + info(`Running no event test at index ${i}`); + await noEventTests[i](window); + // Always blur to ensure it's not accounted as an additional abandonment. + gURLBar.blur(); + TelemetryTestUtils.assertEvents([], { category: "urlbar" }); + } +}); + +/** + * Replaces the contents of Top Sites with the specified site. + * @param {string} site + * A site to add to Top Sites. + */ +async function addTopSite(site) { + await PlacesUtils.history.clear(); + for (let i = 0; i < 5; i++) { + await PlacesTestUtils.addVisits(site); + } + + await updateTopSites(sites => sites && sites[0] && sites[0].url == site); +} + +function registerTipProvider() { + let provider = new UrlbarTestUtils.TestProvider({ + results: tipMatches, + priority: 1, + }); + UrlbarProvidersManager.registerProvider(provider); + return provider; +} + +function unregisterTipProvider(provider) { + UrlbarProvidersManager.unregisterProvider(provider); +} + +let tipMatches = [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://mozilla.org/a" } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TIP, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + text: "This is a test intervention.", + buttonText: "Done", + type: "test", + helpUrl: "about:about", + } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://mozilla.org/b" } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://mozilla.org/c" } + ), +]; diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_selection.js b/browser/components/urlbar/tests/browser/browser_urlbar_selection.js new file mode 100644 index 0000000000..e890ed17cf --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_selection.js @@ -0,0 +1,299 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const exampleSearch = "f oo bar"; +const exampleUrl = "https://example.com/1"; + +function click(target) { + let promise = BrowserTestUtils.waitForEvent(target, "click"); + EventUtils.synthesizeMouseAtCenter(target, {}, target.ownerGlobal); + return promise; +} + +function openContextMenu(target) { + let popupShownPromise = BrowserTestUtils.waitForEvent( + target.ownerGlobal, + "contextmenu" + ); + + EventUtils.synthesizeMouseAtCenter( + target, + { + type: "contextmenu", + button: 2, + }, + target.ownerGlobal + ); + return popupShownPromise; +} + +function drag(target, fromX, fromY, toX, toY) { + let promise = BrowserTestUtils.waitForEvent(target, "mouseup"); + EventUtils.synthesizeMouse( + target, + fromX, + fromY, + { type: "mousemove" }, + target.ownerGlobal + ); + EventUtils.synthesizeMouse( + target, + fromX, + fromY, + { type: "mousedown" }, + target.ownerGlobal + ); + EventUtils.synthesizeMouse( + target, + toX, + toY, + { type: "mousemove" }, + target.ownerGlobal + ); + EventUtils.synthesizeMouse( + target, + toX, + toY, + { type: "mouseup" }, + target.ownerGlobal + ); + return promise; +} + +function resetPrimarySelection(val = "") { + if (Services.clipboard.supportsSelectionClipboard()) { + // Reset the clipboard. + clipboardHelper.copyStringToClipboard( + val, + Services.clipboard.kSelectionClipboard + ); + } +} + +function checkPrimarySelection(expectedVal = "") { + if (Services.clipboard.supportsSelectionClipboard()) { + let primaryAsText = SpecialPowers.getClipboardData( + "text/unicode", + SpecialPowers.Ci.nsIClipboard.kSelectionClipboard + ); + Assert.equal(primaryAsText, expectedVal); + } +} + +add_task(async function setup() { + // On macOS, we must "warm up" the Urlbar to get the first test to pass. + gURLBar.value = ""; + await click(gURLBar.inputField); + gURLBar.blur(); +}); + +add_task(async function leftClickSelectsAll() { + resetPrimarySelection(); + gURLBar.value = exampleSearch; + await click(gURLBar.inputField); + Assert.equal( + gURLBar.selectionStart, + 0, + "The entire search term should be selected." + ); + Assert.equal( + gURLBar.selectionEnd, + exampleSearch.length, + "The entire search term should be selected." + ); + gURLBar.blur(); + checkPrimarySelection(); +}); + +add_task(async function leftClickSelectsUrl() { + resetPrimarySelection(); + gURLBar.value = exampleUrl; + await click(gURLBar.inputField); + Assert.equal(gURLBar.selectionStart, 0, "The entire url should be selected."); + Assert.equal( + gURLBar.selectionEnd, + exampleUrl.length, + "The entire url should be selected." + ); + gURLBar.blur(); + checkPrimarySelection(); +}); + +add_task(async function rightClickSelectsAll() { + gURLBar.inputField.focus(); + gURLBar.value = exampleUrl; + + // Remove the selection so the focus() call above doesn't influence the test. + gURLBar.selectionStart = gURLBar.selectionEnd = 0; + + resetPrimarySelection(); + + await openContextMenu(gURLBar.inputField); + + Assert.equal(gURLBar.selectionStart, 0, "The entire URL should be selected."); + Assert.equal( + gURLBar.selectionEnd, + exampleUrl.length, + "The entire URL should be selected." + ); + + checkPrimarySelection(); + + let contextMenu = gURLBar.querySelector("moz-input-box").menupopup; + + // While the context menu is open, test the "Select All" button. + let contextMenuItem = contextMenu.firstElementChild; + while ( + contextMenuItem.nextElementSibling && + contextMenuItem.getAttribute("cmd") != "cmd_selectAll" + ) { + contextMenuItem = contextMenuItem.nextElementSibling; + } + Assert.ok( + contextMenuItem, + "The context menu should have the select all menu item." + ); + + let controller = document.commandDispatcher.getControllerForCommand( + contextMenuItem.getAttribute("cmd") + ); + let enabled = controller.isCommandEnabled( + contextMenuItem.getAttribute("cmd") + ); + Assert.ok(enabled, "The context menu select all item should be enabled."); + + await click(contextMenuItem); + Assert.equal( + gURLBar.selectionStart, + 0, + "The entire URL should be selected after clicking selectAll button." + ); + Assert.equal( + gURLBar.selectionEnd, + exampleUrl.length, + "The entire URL should be selected after clicking selectAll button." + ); + + gURLBar.querySelector("moz-input-box").menupopup.hidePopup(); + gURLBar.blur(); + checkPrimarySelection(gURLBar.value); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function contextMenuDoesNotCancelSelection() { + gURLBar.inputField.focus(); + gURLBar.value = exampleUrl; + + gURLBar.selectionStart = 3; + gURLBar.selectionEnd = 7; + + resetPrimarySelection(); + + await openContextMenu(gURLBar.inputField); + + Assert.equal( + gURLBar.selectionStart, + 3, + "The selection should not have changed." + ); + Assert.equal( + gURLBar.selectionEnd, + 7, + "The selection should not have changed." + ); + + gURLBar.querySelector("moz-input-box").menupopup.hidePopup(); + gURLBar.blur(); + checkPrimarySelection(); +}); + +add_task(async function dragSelect() { + resetPrimarySelection(); + gURLBar.value = exampleSearch.repeat(10); + // Drags from an artibrary offset of 30 to test for bug 1562145: that the + // selection does not start at the beginning. + await drag(gURLBar.inputField, 30, 0, 60, 0); + Assert.greater( + gURLBar.selectionStart, + 0, + "Selection should not start at the beginning of the string." + ); + + let selectedVal = gURLBar.value.substring( + gURLBar.selectionStart, + gURLBar.selectionEnd + ); + gURLBar.blur(); + checkPrimarySelection(selectedVal); +}); + +/** + * Testing for bug 1571018: that the entire Urlbar isn't selected when the + * Urlbar is dragged following a selectsAll event then a blur. + */ +add_task(async function dragAfterSelectAll() { + resetPrimarySelection(); + gURLBar.value = exampleSearch.repeat(10); + await click(gURLBar.inputField); + Assert.equal( + gURLBar.selectionStart, + 0, + "The entire search term should be selected." + ); + Assert.equal( + gURLBar.selectionEnd, + exampleSearch.repeat(10).length, + "The entire search term should be selected." + ); + + gURLBar.blur(); + checkPrimarySelection(); + + // The offset of 30 is arbitrary. + await drag(gURLBar.inputField, 30, 0, 60, 0); + + Assert.notEqual( + gURLBar.selectionStart, + 0, + "Only part of the search term should be selected." + ); + Assert.notEqual( + gURLBar.selectionEnd, + exampleSearch.repeat(10).length, + "Only part of the search term should be selected." + ); + + checkPrimarySelection( + gURLBar.value.substring(gURLBar.selectionStart, gURLBar.selectionEnd) + ); +}); + +/** + * Testing for bug 1571018: that the entire Urlbar is selected when the Urlbar + * is refocused following a partial text selection then a blur. + */ +add_task(async function selectAllAfterDrag() { + gURLBar.value = exampleSearch; + + gURLBar.selectionStart = 3; + gURLBar.selectionEnd = 7; + + gURLBar.blur(); + + await click(gURLBar.inputField); + + Assert.equal( + gURLBar.selectionStart, + 0, + "The entire search term should be selected." + ); + Assert.equal( + gURLBar.selectionEnd, + exampleSearch.length, + "The entire search term should be selected." + ); + + gURLBar.blur(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry.js new file mode 100644 index 0000000000..1e59b99eb1 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry.js @@ -0,0 +1,1346 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This file tests urlbar telemetry with search related actions. + */ + +"use strict"; + +const SCALAR_URLBAR = "browser.engagement.navigation.urlbar"; +const SCALAR_SEARCHMODE = "browser.engagement.navigation.urlbar_searchmode"; + +// The preference to enable suggestions in the urlbar. +const SUGGEST_URLBAR_PREF = "browser.urlbar.suggest.searches"; + +XPCOMUtils.defineLazyModuleGetters(this, { + SearchSERPTelemetry: "resource:///modules/SearchSERPTelemetry.jsm", + UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.jsm", +}); + +function searchInAwesomebar(value, win = window) { + return UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + waitForFocus, + value, + fireInputEvent: true, + }); +} + +/** + * Click one of the entries in the urlbar suggestion popup. + * + * @param {string} resultTitle + * The title of the result to click on. + * @param {number} button [optional] + * which button to click. + * @returns {number} + * The index of the result that was clicked, or -1 if not found. + */ +async function clickURLBarSuggestion(resultTitle, button = 1) { + await UrlbarTestUtils.promiseSearchComplete(window); + + const count = UrlbarTestUtils.getResultCount(window); + for (let i = 0; i < count; i++) { + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + if (result.displayed.title == resultTitle) { + // This entry is the search suggestion we're looking for. + let element = await UrlbarTestUtils.waitForAutocompleteResultAt( + window, + i + ); + if (button == 1) { + EventUtils.synthesizeMouseAtCenter(element, {}); + } else if (button == 2) { + EventUtils.synthesizeMouseAtCenter(element, { + type: "mousedown", + button: 2, + }); + } + return i; + } + } + return -1; +} + +/** + * Create an engine to generate search suggestions and add it as default + * for this test. + * + * @param {function} taskFn + * The function to run with the new search engine as default. + */ +async function withNewSearchEngine(taskFn) { + const url = + getRootDirectory(gTestPath) + "urlbarTelemetrySearchSuggestions.xml"; + let suggestionEngine = await Services.search.addOpenSearchEngine(url, ""); + let previousEngine = await Services.search.getDefault(); + await Services.search.setDefault(suggestionEngine); + + try { + await taskFn(suggestionEngine); + } finally { + await Services.search.setDefault(previousEngine); + await Services.search.removeEngine(suggestionEngine); + } +} + +add_task(async function setup() { + // Create a new search engine. + await Services.search.addEngineWithDetails("MozSearch", { + alias: "mozalias", + method: "GET", + template: "http://example.com/?q={searchTerms}", + }); + + // Make it the default search engine. + let engine = Services.search.getEngineByName("MozSearch"); + let originalEngine = await Services.search.getDefault(); + await Services.search.setDefault(engine); + + // And the first one-off engine. + await Services.search.moveEngine(engine, 0); + + // Enable search suggestions in the urlbar. + let suggestionsEnabled = Services.prefs.getBoolPref(SUGGEST_URLBAR_PREF); + Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, true); + + // Enable local telemetry recording for the duration of the tests. + let oldCanRecord = Services.telemetry.canRecordExtended; + Services.telemetry.canRecordExtended = true; + + // Enable event recording for the events tested here. + Services.telemetry.setEventRecordingEnabled("navigation", true); + + // Clear history so that history added by previous tests doesn't mess up this + // test when it selects results in the urlbar. + await PlacesUtils.history.clear(); + + // Clear historical search suggestions to avoid interference from previous + // tests. + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.maxHistoricalSearchSuggestions", 0]], + }); + + // Use the default matching bucket configuration. + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.matchBuckets", "general:5,suggestion:4"]], + }); + + // Allows UrlbarTestUtils to access this scope's test helpers, like Assert. + UrlbarTestUtils.init(this); + + // Make sure to restore the engine once we're done. + registerCleanupFunction(async function() { + Services.telemetry.canRecordExtended = oldCanRecord; + await Services.search.setDefault(originalEngine); + await Services.search.removeEngine(engine); + Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, suggestionsEnabled); + await PlacesUtils.history.clear(); + await UrlbarTestUtils.formHistory.clear(); + Services.telemetry.setEventRecordingEnabled("navigation", false); + UrlbarTestUtils.uninit(); + }); +}); + +add_task(async function test_simpleQuery() { + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + + let resultIndexHist = TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_INDEX" + ); + let resultTypeHist = TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_TYPE_2" + ); + let resultIndexByTypeHist = TelemetryTestUtils.getAndClearKeyedHistogram( + "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE_2" + ); + let resultMethodHist = TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ); + let search_hist = TelemetryTestUtils.getAndClearKeyedHistogram( + "SEARCH_COUNTS" + ); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + info("Simulate entering a simple search."); + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await searchInAwesomebar("simple query"); + EventUtils.synthesizeKey("KEY_Enter"); + await p; + + // Check if the scalars contain the expected values. + const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false); + TelemetryTestUtils.assertKeyedScalar( + scalars, + SCALAR_URLBAR, + "search_enter", + 1 + ); + Assert.equal( + Object.keys(scalars[SCALAR_URLBAR]).length, + 1, + "This search must only increment one entry in the scalar." + ); + + // SEARCH_COUNTS should be incremented, but only the urlbar source since an + // internal @search keyword was not used. + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "other-MozSearch.urlbar", + 1 + ); + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "other-MozSearch.alias", + undefined + ); + + // Also check events. + TelemetryTestUtils.assertEvents( + [ + [ + "navigation", + "search", + "urlbar", + "enter", + { engine: "other-MozSearch" }, + ], + ], + { category: "navigation", method: "search" } + ); + + // Check the histograms as well. + TelemetryTestUtils.assertHistogram(resultIndexHist, 0, 1); + + TelemetryTestUtils.assertHistogram( + resultTypeHist, + UrlbarUtils.SELECTED_RESULT_TYPES.searchengine, + 1 + ); + + TelemetryTestUtils.assertKeyedHistogramValue( + resultIndexByTypeHist, + "searchengine", + 0, + 1 + ); + + TelemetryTestUtils.assertHistogram( + resultMethodHist, + UrlbarTestUtils.SELECTED_RESULT_METHODS.enter, + 1 + ); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_searchMode_enter() { + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + + let resultIndexHist = TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_INDEX" + ); + let resultTypeHist = TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_TYPE_2" + ); + let resultIndexByTypeHist = TelemetryTestUtils.getAndClearKeyedHistogram( + "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE_2" + ); + let resultMethodHist = TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + info("Enter search mode using an alias and a query."); + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await searchInAwesomebar("mozalias query"); + EventUtils.synthesizeKey("KEY_Enter"); + await p; + + // Check if the scalars contain the expected values. + const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false); + TelemetryTestUtils.assertKeyedScalar( + scalars, + SCALAR_SEARCHMODE, + "search_enter", + 1 + ); + Assert.equal( + Object.keys(scalars[SCALAR_SEARCHMODE]).length, + 1, + "This search must only increment one entry in the scalar." + ); + + // Also check events. + TelemetryTestUtils.assertEvents( + [ + [ + "navigation", + "search", + "urlbar_searchmode", + "enter", + { engine: "other-MozSearch" }, + ], + ], + { category: "navigation", method: "search" } + ); + + // Check the histograms as well. + TelemetryTestUtils.assertHistogram(resultIndexHist, 0, 1); + + TelemetryTestUtils.assertHistogram( + resultTypeHist, + UrlbarUtils.SELECTED_RESULT_TYPES.searchengine, + 1 + ); + + TelemetryTestUtils.assertKeyedHistogramValue( + resultIndexByTypeHist, + "searchengine", + 0, + 1 + ); + + TelemetryTestUtils.assertHistogram( + resultMethodHist, + UrlbarTestUtils.SELECTED_RESULT_METHODS.enter, + 1 + ); + + BrowserTestUtils.removeTab(tab); +}); + +// Performs a search using the first result, a one-off button, and the Return +// (Enter) key. +add_task(async function test_oneOff_enter() { + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + + let resultIndexHist = TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_INDEX" + ); + let resultTypeHist = TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_TYPE_2" + ); + let resultIndexByTypeHist = TelemetryTestUtils.getAndClearKeyedHistogram( + "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE_2" + ); + let resultMethodHist = TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ); + let search_hist = TelemetryTestUtils.getAndClearKeyedHistogram( + "SEARCH_COUNTS" + ); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + info("Perform a one-off search using the first engine."); + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await searchInAwesomebar("query"); + + info("Pressing Alt+Down to take us to the first one-off engine."); + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + let engine = UrlbarTestUtils.getOneOffSearchButtons(window).selectedButton + .engine; + EventUtils.synthesizeKey("KEY_Enter"); + await searchPromise; + + await UrlbarTestUtils.assertSearchMode(window, { + engineName: engine.name, + entry: "oneoff", + }); + + // Now that we're in search mode, execute the search. + EventUtils.synthesizeKey("KEY_Enter"); + await p; + + // Check if the scalars contain the expected values. + const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false); + TelemetryTestUtils.assertKeyedScalar( + scalars, + SCALAR_SEARCHMODE, + "search_enter", + 1 + ); + Assert.equal( + Object.keys(scalars[SCALAR_SEARCHMODE]).length, + 1, + "This search must only increment one entry in the scalar." + ); + + // SEARCH_COUNTS should be incremented, but only the urlbar-searchmode source + // since aliases aren't counted separately in search mode. + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "other-MozSearch.urlbar-searchmode", + 1 + ); + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "other-MozSearch.alias", + undefined + ); + + // Also check events. + TelemetryTestUtils.assertEvents( + [ + [ + "navigation", + "search", + "urlbar_searchmode", + "enter", + { engine: "other-MozSearch" }, + ], + ], + { category: "navigation", method: "search" } + ); + + // Check the histograms as well. + TelemetryTestUtils.assertHistogram(resultIndexHist, 0, 1); + + TelemetryTestUtils.assertHistogram( + resultTypeHist, + UrlbarUtils.SELECTED_RESULT_TYPES.searchengine, + 1 + ); + + TelemetryTestUtils.assertKeyedHistogramValue( + resultIndexByTypeHist, + "searchengine", + 0, + 1 + ); + + TelemetryTestUtils.assertHistogram( + resultMethodHist, + UrlbarTestUtils.SELECTED_RESULT_METHODS.enter, + 1 + ); + + BrowserTestUtils.removeTab(tab); +}); + +// Performs a search using the second result, a one-off button, and the Return +// (Enter) key. This only tests the FX_URLBAR_SELECTED_RESULT_METHOD histogram +// since test_oneOff_enter covers everything else. +add_task(async function test_oneOff_enterSelection() { + Services.telemetry.clearScalars(); + let resultMethodHist = TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ); + + await withNewSearchEngine(async function() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + info("Type a query. Suggestions should be generated by the test engine."); + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await searchInAwesomebar("query"); + + info( + "Select the second result, press Alt+Down to take us to the first one-off engine." + ); + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true }); + let engine = UrlbarTestUtils.getOneOffSearchButtons(window).selectedButton + .engine; + EventUtils.synthesizeKey("KEY_Enter"); + await searchPromise; + + await UrlbarTestUtils.assertSearchMode(window, { + engineName: engine.name, + entry: "oneoff", + }); + + // Now that we're in search mode, execute the search. + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_Enter"); + await p; + + TelemetryTestUtils.assertHistogram( + resultMethodHist, + UrlbarTestUtils.SELECTED_RESULT_METHODS.arrowEnterSelection, + 1 + ); + + BrowserTestUtils.removeTab(tab); + }); +}); + +// Performs a search using a click on a one-off button. This only tests the +// FX_URLBAR_SELECTED_RESULT_METHOD histogram since test_oneOff_enter covers +// everything else. +add_task(async function test_oneOff_click() { + Services.telemetry.clearScalars(); + + let resultMethodHist = TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + info("Type a query."); + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await searchInAwesomebar("query"); + + info("Click the first one-off button."); + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + let oneOffButton = UrlbarTestUtils.getOneOffSearchButtons( + window + ).getSelectableButtons(false)[0]; + oneOffButton.click(); + await searchPromise; + + await UrlbarTestUtils.assertSearchMode(window, { + engineName: oneOffButton.engine.name, + entry: "oneoff", + }); + + // Now that we're in search mode, execute the search. + let element = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0); + Assert.ok(element, "Found result after entering search mode."); + EventUtils.synthesizeMouseAtCenter(element, {}); + await p; + + TelemetryTestUtils.assertHistogram( + resultMethodHist, + UrlbarTestUtils.SELECTED_RESULT_METHODS.click, + 1 + ); + + BrowserTestUtils.removeTab(tab); +}); + +// Clicks the first suggestion offered by the test search engine. +add_task(async function test_suggestion_click() { + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + await UrlbarTestUtils.formHistory.clear(); + + let resultIndexHist = TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_INDEX" + ); + let resultTypeHist = TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_TYPE_2" + ); + let resultIndexByTypeHist = TelemetryTestUtils.getAndClearKeyedHistogram( + "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE_2" + ); + let resultMethodHist = TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ); + let search_hist = TelemetryTestUtils.getAndClearKeyedHistogram( + "SEARCH_COUNTS" + ); + + await withNewSearchEngine(async function(engine) { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + info("Type a query. Suggestions should be generated by the test engine."); + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await searchInAwesomebar("query"); + info("Clicking the urlbar suggestion."); + await clickURLBarSuggestion("queryfoo"); + await p; + + // Check if the scalars contain the expected values. + const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false); + TelemetryTestUtils.assertKeyedScalar( + scalars, + SCALAR_URLBAR, + "search_suggestion", + 1 + ); + Assert.equal( + Object.keys(scalars[SCALAR_URLBAR]).length, + 1, + "This search must only increment one entry in the scalar." + ); + + // SEARCH_COUNTS should be incremented. + let searchEngineId = "other-" + engine.name; + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + searchEngineId + ".urlbar", + 1 + ); + + // Also check events. + TelemetryTestUtils.assertEvents( + [ + [ + "navigation", + "search", + "urlbar", + "suggestion", + { engine: searchEngineId }, + ], + ], + { category: "navigation", method: "search" } + ); + + // Check the histograms as well. + TelemetryTestUtils.assertHistogram(resultIndexHist, 3, 1); + + TelemetryTestUtils.assertHistogram( + resultTypeHist, + UrlbarUtils.SELECTED_RESULT_TYPES.searchsuggestion, + 1 + ); + + TelemetryTestUtils.assertKeyedHistogramValue( + resultIndexByTypeHist, + "searchsuggestion", + 3, + 1 + ); + + TelemetryTestUtils.assertHistogram( + resultMethodHist, + UrlbarTestUtils.SELECTED_RESULT_METHODS.click, + 1 + ); + + BrowserTestUtils.removeTab(tab); + }); +}); + +// Selects and presses the Return (Enter) key on the first suggestion offered by +// the test search engine. This only tests the FX_URLBAR_SELECTED_RESULT_METHOD +// histogram since test_suggestion_click covers everything else. +add_task(async function test_suggestion_arrowEnterSelection() { + Services.telemetry.clearScalars(); + let resultMethodHist = TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ); + + await withNewSearchEngine(async function() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + info("Type a query. Suggestions should be generated by the test engine."); + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await searchInAwesomebar("query"); + info("Select the second result and press Return."); + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_Enter"); + await p; + + TelemetryTestUtils.assertHistogram( + resultMethodHist, + UrlbarTestUtils.SELECTED_RESULT_METHODS.arrowEnterSelection, + 1 + ); + + BrowserTestUtils.removeTab(tab); + }); +}); + +// Selects through tab and presses the Return (Enter) key on the first +// suggestion offered by the test search engine. +add_task(async function test_suggestion_tabEnterSelection() { + Services.telemetry.clearScalars(); + let resultMethodHist = TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ); + + await withNewSearchEngine(async function() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + info("Type a query. Suggestions should be generated by the test engine."); + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await searchInAwesomebar("query"); + info("Select the second result and press Return."); + EventUtils.synthesizeKey("KEY_Tab"); + EventUtils.synthesizeKey("KEY_Enter"); + await p; + + TelemetryTestUtils.assertHistogram( + resultMethodHist, + UrlbarTestUtils.SELECTED_RESULT_METHODS.tabEnterSelection, + 1 + ); + + BrowserTestUtils.removeTab(tab); + }); +}); + +// Selects through code and presses the Return (Enter) key on the first +// suggestion offered by the test search engine. +add_task(async function test_suggestion_enterSelection() { + Services.telemetry.clearScalars(); + let resultMethodHist = TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ); + + await withNewSearchEngine(async function() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + info("Type a query. Suggestions should be generated by the test engine."); + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await searchInAwesomebar("query"); + info("Select the second result and press Return."); + UrlbarTestUtils.setSelectedRowIndex(window, 1); + EventUtils.synthesizeKey("KEY_Enter"); + await p; + + TelemetryTestUtils.assertHistogram( + resultMethodHist, + UrlbarTestUtils.SELECTED_RESULT_METHODS.enterSelection, + 1 + ); + + BrowserTestUtils.removeTab(tab); + }); +}); + +// Clicks the first suggestion offered by the test search engine when in search +// mode. +add_task(async function test_searchmode_suggestion_click() { + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + + let resultIndexHist = TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_INDEX" + ); + let resultTypeHist = TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_TYPE_2" + ); + let resultIndexByTypeHist = TelemetryTestUtils.getAndClearKeyedHistogram( + "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE_2" + ); + let resultMethodHist = TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ); + let search_hist = TelemetryTestUtils.getAndClearKeyedHistogram( + "SEARCH_COUNTS" + ); + + await withNewSearchEngine(async function(engine) { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + info("Type a query. Suggestions should be generated by the test engine."); + await searchInAwesomebar("query"); + await UrlbarTestUtils.enterSearchMode(window, { + engineName: engine.name, + }); + info("Clicking the urlbar suggestion."); + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await clickURLBarSuggestion("queryfoo"); + await p; + + // Check if the scalars contain the expected values. + const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false); + TelemetryTestUtils.assertKeyedScalar( + scalars, + SCALAR_SEARCHMODE, + "search_suggestion", + 1 + ); + Assert.equal( + Object.keys(scalars[SCALAR_SEARCHMODE]).length, + 1, + "This search must only increment one entry in the scalar." + ); + + // SEARCH_COUNTS should be incremented. + let searchEngineId = "other-" + engine.name; + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + searchEngineId + ".urlbar-searchmode", + 1 + ); + + // Also check events. + TelemetryTestUtils.assertEvents( + [ + [ + "navigation", + "search", + "urlbar_searchmode", + "suggestion", + { engine: searchEngineId }, + ], + ], + { category: "navigation", method: "search" } + ); + + // Check the histograms as well. + TelemetryTestUtils.assertHistogram(resultIndexHist, 1, 1); + + TelemetryTestUtils.assertHistogram( + resultTypeHist, + UrlbarUtils.SELECTED_RESULT_TYPES.searchsuggestion, + 1 + ); + + TelemetryTestUtils.assertKeyedHistogramValue( + resultIndexByTypeHist, + "searchsuggestion", + 1, + 1 + ); + + TelemetryTestUtils.assertHistogram( + resultMethodHist, + UrlbarTestUtils.SELECTED_RESULT_METHODS.click, + 1 + ); + + BrowserTestUtils.removeTab(tab); + }); +}); + +// Selects and presses the Return (Enter) key on the first suggestion offered by +// the test search engine in search mode. This only tests the +// FX_URLBAR_SELECTED_RESULT_METHOD histogram since +// test_searchmode_suggestion_click covers everything else. +add_task(async function test_searchmode_suggestion_arrowEnterSelection() { + Services.telemetry.clearScalars(); + let resultMethodHist = TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ); + + await withNewSearchEngine(async function(engine) { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + info("Type a query. Suggestions should be generated by the test engine."); + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await searchInAwesomebar("query"); + await UrlbarTestUtils.enterSearchMode(window, { + engineName: engine.name, + }); + info("Select the second result and press Return."); + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_Enter"); + await p; + + TelemetryTestUtils.assertHistogram( + resultMethodHist, + UrlbarTestUtils.SELECTED_RESULT_METHODS.arrowEnterSelection, + 1 + ); + BrowserTestUtils.removeTab(tab); + }); +}); + +// Selects through tab and presses the Return (Enter) key on the first +// suggestion offered by the test search engine in search mode. +add_task(async function test_suggestion_tabEnterSelection() { + Services.telemetry.clearScalars(); + let resultMethodHist = TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ); + + await withNewSearchEngine(async function(engine) { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + info("Type a query. Suggestions should be generated by the test engine."); + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await searchInAwesomebar("query"); + await UrlbarTestUtils.enterSearchMode(window, { + engineName: engine.name, + }); + info("Select the second result and press Return."); + EventUtils.synthesizeKey("KEY_Tab"); + EventUtils.synthesizeKey("KEY_Enter"); + await p; + + TelemetryTestUtils.assertHistogram( + resultMethodHist, + UrlbarTestUtils.SELECTED_RESULT_METHODS.tabEnterSelection, + 1 + ); + + BrowserTestUtils.removeTab(tab); + }); +}); + +// Selects through code and presses the Return (Enter) key on the first +// suggestion offered by the test search engine in search mode. +add_task(async function test_suggestion_enterSelection() { + Services.telemetry.clearScalars(); + let resultMethodHist = TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ); + + await withNewSearchEngine(async function(engine) { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + info("Type a query. Suggestions should be generated by the test engine."); + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await searchInAwesomebar("query"); + await UrlbarTestUtils.enterSearchMode(window, { + engineName: engine.name, + }); + info("Select the second result and press Return."); + UrlbarTestUtils.setSelectedRowIndex(window, 1); + EventUtils.synthesizeKey("KEY_Enter"); + await p; + + TelemetryTestUtils.assertHistogram( + resultMethodHist, + UrlbarTestUtils.SELECTED_RESULT_METHODS.enterSelection, + 1 + ); + + BrowserTestUtils.removeTab(tab); + }); +}); + +// Clicks a form history result. +add_task(async function test_formHistory_click() { + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + await UrlbarTestUtils.formHistory.clear(); + await UrlbarTestUtils.formHistory.add(["foobar"]); + + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.maxHistoricalSearchSuggestions", 1]], + }); + + let resultIndexHist = TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_INDEX" + ); + let resultTypeHist = TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_TYPE_2" + ); + let resultIndexByTypeHist = TelemetryTestUtils.getAndClearKeyedHistogram( + "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE_2" + ); + let resultMethodHist = TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ); + let search_hist = TelemetryTestUtils.getAndClearKeyedHistogram( + "SEARCH_COUNTS" + ); + + await withNewSearchEngine(async engine => { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + info("Type a query. There should be form history."); + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await searchInAwesomebar("foo"); + info("Clicking the form history."); + let foobarIndex = await clickURLBarSuggestion("foobar"); + await p; + + // Check if the scalars contain the expected values. + const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false); + TelemetryTestUtils.assertKeyedScalar( + scalars, + SCALAR_URLBAR, + "search_formhistory", + 1 + ); + Assert.equal( + Object.keys(scalars[SCALAR_URLBAR]).length, + 1, + "This search must only increment one entry in the scalar." + ); + + // SEARCH_COUNTS should be incremented. + let searchEngineId = "other-" + engine.name; + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + searchEngineId + ".urlbar", + 1 + ); + + // Also check events. + TelemetryTestUtils.assertEvents( + [ + [ + "navigation", + "search", + "urlbar", + "formhistory", + { engine: searchEngineId }, + ], + ], + { category: "navigation", method: "search" } + ); + + // Check the histograms as well. + TelemetryTestUtils.assertHistogram(resultIndexHist, foobarIndex, 1); + + TelemetryTestUtils.assertHistogram( + resultTypeHist, + UrlbarUtils.SELECTED_RESULT_TYPES.formhistory, + 1 + ); + + TelemetryTestUtils.assertKeyedHistogramValue( + resultIndexByTypeHist, + "formhistory", + foobarIndex, + 1 + ); + + TelemetryTestUtils.assertHistogram( + resultMethodHist, + UrlbarTestUtils.SELECTED_RESULT_METHODS.click, + 1 + ); + + BrowserTestUtils.removeTab(tab); + await UrlbarTestUtils.formHistory.clear(); + await SpecialPowers.popPrefEnv(); + }); +}); + +// Selects and presses the Return (Enter) key on a form history result. This +// only tests the FX_URLBAR_SELECTED_RESULT_METHOD histogram since +// test_formHistory_click covers everything else. +add_task(async function test_formHistory_arrowEnterSelection() { + Services.telemetry.clearScalars(); + let resultMethodHist = TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ); + + await UrlbarTestUtils.formHistory.clear(); + await UrlbarTestUtils.formHistory.add(["foobar"]); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.maxHistoricalSearchSuggestions", 1]], + }); + + await withNewSearchEngine(async function() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + info("Type a query. There should be form history."); + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await searchInAwesomebar("foo"); + info("Select the form history result and press Return."); + while (gURLBar.untrimmedValue != "foobar") { + EventUtils.synthesizeKey("KEY_ArrowDown"); + } + EventUtils.synthesizeKey("KEY_Enter"); + await p; + + TelemetryTestUtils.assertHistogram( + resultMethodHist, + UrlbarTestUtils.SELECTED_RESULT_METHODS.arrowEnterSelection, + 1 + ); + + BrowserTestUtils.removeTab(tab); + await UrlbarTestUtils.formHistory.clear(); + await SpecialPowers.popPrefEnv(); + }); +}); + +// Selects through tab and presses the Return (Enter) key on a form history +// result. +add_task(async function test_formHistory_tabEnterSelection() { + Services.telemetry.clearScalars(); + let resultMethodHist = TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ); + + await UrlbarTestUtils.formHistory.clear(); + await UrlbarTestUtils.formHistory.add(["foobar"]); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.maxHistoricalSearchSuggestions", 1]], + }); + + await withNewSearchEngine(async function() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + info("Type a query. There should be form history."); + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await searchInAwesomebar("foo"); + info("Select the form history result and press Return."); + while (gURLBar.untrimmedValue != "foobar") { + EventUtils.synthesizeKey("KEY_Tab"); + } + EventUtils.synthesizeKey("KEY_Enter"); + await p; + + TelemetryTestUtils.assertHistogram( + resultMethodHist, + UrlbarTestUtils.SELECTED_RESULT_METHODS.tabEnterSelection, + 1 + ); + + BrowserTestUtils.removeTab(tab); + await UrlbarTestUtils.formHistory.clear(); + await SpecialPowers.popPrefEnv(); + }); +}); + +// Selects through code and presses the Return (Enter) key on a form history +// result. +add_task(async function test_formHistory_enterSelection() { + Services.telemetry.clearScalars(); + let resultMethodHist = TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ); + + await UrlbarTestUtils.formHistory.clear(); + await UrlbarTestUtils.formHistory.add(["foobar"]); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.maxHistoricalSearchSuggestions", 1]], + }); + + await withNewSearchEngine(async function() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + info("Type a query. There should be form history."); + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await searchInAwesomebar("foo"); + info("Select the second result and press Return."); + let index = 1; + while (gURLBar.untrimmedValue != "foobar") { + UrlbarTestUtils.setSelectedRowIndex(window, index++); + } + EventUtils.synthesizeKey("KEY_Enter"); + await p; + + TelemetryTestUtils.assertHistogram( + resultMethodHist, + UrlbarTestUtils.SELECTED_RESULT_METHODS.enterSelection, + 1 + ); + + BrowserTestUtils.removeTab(tab); + await UrlbarTestUtils.formHistory.clear(); + await SpecialPowers.popPrefEnv(); + }); +}); + +add_task(async function test_privateWindow() { + // Override the search telemetry search provider info to + // count in-content SEARCH_COUNTs telemetry for our test engine. + SearchSERPTelemetry.overrideSearchTelemetryForTests([ + { + telemetryId: "example", + searchPageRegexp: "^http://example\\.com/", + queryParamName: "q", + }, + ]); + + let search_hist = TelemetryTestUtils.getAndClearKeyedHistogram( + "SEARCH_COUNTS" + ); + + // First, do a bunch of searches in a private window. + let win = await BrowserTestUtils.openNewBrowserWindow({ private: true }); + + info("Search in a private window and the pref does not exist"); + let p = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await searchInAwesomebar("query", win); + EventUtils.synthesizeKey("KEY_Enter", undefined, win); + await p; + + // SEARCH_COUNTS should be incremented. + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "other-MozSearch.urlbar", + 1 + ); + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "example.in-content:organic:none", + 1 + ); + + info("Search again in a private window after setting the pref to true"); + Services.prefs.setBoolPref("browser.engagement.search_counts.pbm", true); + p = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await searchInAwesomebar("another query", win); + EventUtils.synthesizeKey("KEY_Enter", undefined, win); + await p; + + // SEARCH_COUNTS should *not* be incremented. + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "other-MozSearch.urlbar", + 1 + ); + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "example.in-content:organic:none", + 1 + ); + + info("Search again in a private window after setting the pref to false"); + Services.prefs.setBoolPref("browser.engagement.search_counts.pbm", false); + p = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await searchInAwesomebar("another query", win); + EventUtils.synthesizeKey("KEY_Enter", undefined, win); + await p; + + // SEARCH_COUNTS should be incremented. + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "other-MozSearch.urlbar", + 2 + ); + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "example.in-content:organic:none", + 2 + ); + + info("Search again in a private window after clearing the pref"); + Services.prefs.clearUserPref("browser.engagement.search_counts.pbm"); + p = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await searchInAwesomebar("another query", win); + EventUtils.synthesizeKey("KEY_Enter", undefined, win); + await p; + + // SEARCH_COUNTS should be incremented. + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "other-MozSearch.urlbar", + 3 + ); + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "example.in-content:organic:none", + 3 + ); + + await BrowserTestUtils.closeWindow(win); + + // Now, do a bunch of searches in a non-private window. Telemetry should + // always be recorded regardless of the pref's value. + win = await BrowserTestUtils.openNewBrowserWindow(); + + info("Search in a non-private window and the pref does not exist"); + p = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await searchInAwesomebar("query", win); + EventUtils.synthesizeKey("KEY_Enter", undefined, win); + await p; + + // SEARCH_COUNTS should be incremented. + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "other-MozSearch.urlbar", + 4 + ); + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "example.in-content:organic:none", + 4 + ); + + info("Search again in a non-private window after setting the pref to true"); + Services.prefs.setBoolPref("browser.engagement.search_counts.pbm", true); + p = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await searchInAwesomebar("another query", win); + EventUtils.synthesizeKey("KEY_Enter", undefined, win); + await p; + + // SEARCH_COUNTS should be incremented. + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "other-MozSearch.urlbar", + 5 + ); + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "example.in-content:organic:none", + 5 + ); + + info("Search again in a non-private window after setting the pref to false"); + Services.prefs.setBoolPref("browser.engagement.search_counts.pbm", false); + p = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await searchInAwesomebar("another query", win); + EventUtils.synthesizeKey("KEY_Enter", undefined, win); + await p; + + // SEARCH_COUNTS should be incremented. + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "other-MozSearch.urlbar", + 6 + ); + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "example.in-content:organic:none", + 6 + ); + + info("Search again in a non-private window after clearing the pref"); + Services.prefs.clearUserPref("browser.engagement.search_counts.pbm"); + p = BrowserTestUtils.browserLoaded(win.gBrowser.selectedBrowser); + await searchInAwesomebar("another query", win); + EventUtils.synthesizeKey("KEY_Enter", undefined, win); + await p; + + // SEARCH_COUNTS should be incremented. + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "other-MozSearch.urlbar", + 7 + ); + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "example.in-content:organic:none", + 7 + ); + + await BrowserTestUtils.closeWindow(win); + + // Reset the search provider info. + SearchSERPTelemetry.overrideSearchTelemetryForTests(); + await UrlbarTestUtils.formHistory.clear(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_dynamic.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_dynamic.js new file mode 100644 index 0000000000..20dadd0d7d --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_dynamic.js @@ -0,0 +1,160 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This file tests urlbar telemetry for dynamic results. + */ + +"use strict"; + +XPCOMUtils.defineLazyModuleGetters(this, { + UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.jsm", + UrlbarResult: "resource:///modules/UrlbarResult.jsm", + UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.jsm", + UrlbarView: "resource:///modules/UrlbarView.jsm", +}); + +const DYNAMIC_TYPE_NAME = "test"; + +/** + * A test URLBar provider. + */ +class TestProvider extends UrlbarTestUtils.TestProvider { + constructor() { + super({ + priority: Infinity, + results: [ + Object.assign( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.DYNAMIC, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + dynamicType: DYNAMIC_TYPE_NAME, + } + ), + { heuristic: true } + ), + ], + }); + } + + getViewUpdate(result, idsByName) { + return { + title: { + textContent: "This is a dynamic result.", + }, + button: { + textContent: "Click Me", + }, + }; + } +} + +add_task(async function test() { + // Add a dynamic result type. + UrlbarResult.addDynamicResultType(DYNAMIC_TYPE_NAME); + UrlbarView.addDynamicViewTemplate(DYNAMIC_TYPE_NAME, { + stylesheet: + getRootDirectory(gTestPath) + "urlbarTelemetryUrlbarDynamic.css", + children: [ + { + name: "title", + tag: "span", + }, + { + name: "buttonSpacer", + tag: "span", + }, + { + name: "button", + tag: "span", + attributes: { + role: "button", + }, + }, + ], + }); + registerCleanupFunction(() => { + UrlbarView.removeDynamicViewTemplate(DYNAMIC_TYPE_NAME); + UrlbarResult.removeDynamicResultType(DYNAMIC_TYPE_NAME); + }); + + // Register a provider that returns the dynamic result type. + let provider = new TestProvider(); + UrlbarProvidersManager.registerProvider(provider); + registerCleanupFunction(() => { + UrlbarProvidersManager.unregisterProvider(provider); + }); + + const histograms = snapshotHistograms(); + + // Do a search to show the dynamic result. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + waitForFocus, + value: "test", + fireInputEvent: true, + }); + + // Press enter on the result's button. It will be preselected since the + // result is the heuristic. + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Enter") + ); + + assertTelemetryResults( + histograms, + "dynamic", + 0, + UrlbarTestUtils.SELECTED_RESULT_METHODS.enter + ); + + // Clean up for subsequent tests. + gURLBar.handleRevert(); +}); + +function snapshotHistograms() { + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + return { + resultIndexHist: TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_INDEX" + ), + resultTypeHist: TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_TYPE_2" + ), + resultIndexByTypeHist: TelemetryTestUtils.getAndClearKeyedHistogram( + "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE_2" + ), + resultMethodHist: TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ), + }; +} + +function assertTelemetryResults(histograms, type, index, method) { + TelemetryTestUtils.assertHistogram(histograms.resultIndexHist, index, 1); + + TelemetryTestUtils.assertHistogram( + histograms.resultTypeHist, + UrlbarUtils.SELECTED_RESULT_TYPES[type], + 1 + ); + + TelemetryTestUtils.assertKeyedHistogramValue( + histograms.resultIndexByTypeHist, + type, + index, + 1 + ); + + TelemetryTestUtils.assertHistogram(histograms.resultMethodHist, method, 1); + + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + `urlbar.picked.${type}`, + index, + 1 + ); +} diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_extension.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_extension.js new file mode 100644 index 0000000000..89914b10ba --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_extension.js @@ -0,0 +1,181 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This file tests urlbar telemetry with extension actions. + */ + +"use strict"; + +const SCALAR_URLBAR = "browser.engagement.navigation.urlbar"; + +XPCOMUtils.defineLazyModuleGetters(this, { + UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.jsm", +}); + +function assertSearchTelemetryEmpty(search_hist) { + const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false); + Assert.ok( + !(SCALAR_URLBAR in scalars), + `Should not have recorded ${SCALAR_URLBAR}` + ); + + // Make sure SEARCH_COUNTS contains identical values. + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "other-MozSearch.urlbar", + undefined + ); + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "other-MozSearch.alias", + undefined + ); + + // Also check events. + let events = Services.telemetry.snapshotEvents( + Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, + false + ); + events = (events.parent || []).filter( + e => e[1] == "navigation" && e[2] == "search" + ); + Assert.deepEqual( + events, + [], + "Should not have recorded any navigation search events" + ); +} + +function snapshotHistograms() { + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + return { + resultIndexHist: TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_INDEX" + ), + resultTypeHist: TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_TYPE_2" + ), + resultIndexByTypeHist: TelemetryTestUtils.getAndClearKeyedHistogram( + "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE_2" + ), + resultMethodHist: TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ), + search_hist: TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"), + }; +} + +function assertTelemetryResults(histograms, type, index, method) { + TelemetryTestUtils.assertHistogram(histograms.resultIndexHist, index, 1); + + TelemetryTestUtils.assertHistogram( + histograms.resultTypeHist, + UrlbarUtils.SELECTED_RESULT_TYPES[type], + 1 + ); + + TelemetryTestUtils.assertKeyedHistogramValue( + histograms.resultIndexByTypeHist, + type, + index, + 1 + ); + + TelemetryTestUtils.assertHistogram(histograms.resultMethodHist, method, 1); + + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + `urlbar.picked.${type}`, + index, + 1 + ); +} + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [ + // Disable search suggestions in the urlbar. + ["browser.urlbar.suggest.searches", false], + // Clear historical search suggestions to avoid interference from previous + // tests. + ["browser.urlbar.maxHistoricalSearchSuggestions", 0], + // Use the default matching bucket configuration. + ["browser.urlbar.matchBuckets", "general:5,suggestion:4"], + // Turn autofill off. + ["browser.urlbar.autoFill", false], + ], + }); + + // Enable local telemetry recording for the duration of the tests. + let oldCanRecord = Services.telemetry.canRecordExtended; + Services.telemetry.canRecordExtended = true; + + // Enable event recording for the events tested here. + Services.telemetry.setEventRecordingEnabled("navigation", true); + + // Clear history so that history added by previous tests doesn't mess up this + // test when it selects results in the urlbar. + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + + // Make sure to restore the engine once we're done. + registerCleanupFunction(async function() { + Services.telemetry.canRecordExtended = oldCanRecord; + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + Services.telemetry.setEventRecordingEnabled("navigation", false); + }); +}); + +add_task(async function test_extension() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + omnibox: { + keyword: "omniboxtest", + }, + + background() { + /* global browser */ + browser.omnibox.setDefaultSuggestion({ + description: "doit", + }); + // Just do nothing for this test. + browser.omnibox.onInputEntered.addListener(() => {}); + browser.omnibox.onInputChanged.addListener((text, suggest) => { + suggest([]); + }); + }, + }, + }); + + await extension.startup(); + + const histograms = snapshotHistograms(); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + waitForFocus, + value: "omniboxtest ", + fireInputEvent: true, + }); + EventUtils.synthesizeKey("KEY_Enter"); + + assertSearchTelemetryEmpty(histograms.search_hist); + assertTelemetryResults( + histograms, + "extension", + 0, + UrlbarTestUtils.SELECTED_RESULT_METHODS.enter + ); + + await extension.unload(); + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_places.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_places.js new file mode 100644 index 0000000000..fb0c5e3b46 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_places.js @@ -0,0 +1,330 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This file tests urlbar telemetry with places related actions (e.g. history/ + * bookmark selection). + */ + +"use strict"; + +const SCALAR_URLBAR = "browser.engagement.navigation.urlbar"; + +const TEST_URL = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://mochi.test:8888" +); + +XPCOMUtils.defineLazyModuleGetters(this, { + UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.jsm", +}); + +function searchInAwesomebar(value, win = window) { + return UrlbarTestUtils.promiseAutocompleteResultPopup({ + window: win, + waitForFocus, + value, + fireInputEvent: true, + }); +} + +function assertSearchTelemetryEmpty(search_hist) { + const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false); + Assert.ok( + !(SCALAR_URLBAR in scalars), + `Should not have recorded ${SCALAR_URLBAR}` + ); + + // Make sure SEARCH_COUNTS contains identical values. + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "other-MozSearch.urlbar", + undefined + ); + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "other-MozSearch.alias", + undefined + ); + + // Also check events. + let events = Services.telemetry.snapshotEvents( + Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, + false + ); + events = (events.parent || []).filter( + e => e[1] == "navigation" && e[2] == "search" + ); + Assert.deepEqual( + events, + [], + "Should not have recorded any navigation search events" + ); +} + +function snapshotHistograms() { + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + return { + resultIndexHist: TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_INDEX" + ), + resultTypeHist: TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_TYPE_2" + ), + resultIndexByTypeHist: TelemetryTestUtils.getAndClearKeyedHistogram( + "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE_2" + ), + resultMethodHist: TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ), + search_hist: TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"), + }; +} + +function assertTelemetryResults(histograms, type, index, method) { + TelemetryTestUtils.assertHistogram(histograms.resultIndexHist, index, 1); + + TelemetryTestUtils.assertHistogram( + histograms.resultTypeHist, + UrlbarUtils.SELECTED_RESULT_TYPES[type], + 1 + ); + + TelemetryTestUtils.assertKeyedHistogramValue( + histograms.resultIndexByTypeHist, + type, + index, + 1 + ); + + TelemetryTestUtils.assertHistogram(histograms.resultMethodHist, method, 1); + + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + `urlbar.picked.${type}`, + index, + 1 + ); +} + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [ + // Disable search suggestions in the urlbar. + ["browser.urlbar.suggest.searches", false], + // Clear historical search suggestions to avoid interference from previous + // tests. + ["browser.urlbar.maxHistoricalSearchSuggestions", 0], + // Use the default matching bucket configuration. + ["browser.urlbar.matchBuckets", "general:5,suggestion:4"], + // Turn autofill off. + ["browser.urlbar.autoFill", false], + ], + }); + + // Enable local telemetry recording for the duration of the tests. + let oldCanRecord = Services.telemetry.canRecordExtended; + Services.telemetry.canRecordExtended = true; + + // Enable event recording for the events tested here. + Services.telemetry.setEventRecordingEnabled("navigation", true); + + // Clear history so that history added by previous tests doesn't mess up this + // test when it selects results in the urlbar. + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + + await PlacesUtils.keywords.insert({ + keyword: "get", + url: TEST_URL + "?q=%s", + }); + + // Make sure to restore the engine once we're done. + registerCleanupFunction(async function() { + await PlacesUtils.keywords.remove("get"); + Services.telemetry.canRecordExtended = oldCanRecord; + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + Services.telemetry.setEventRecordingEnabled("navigation", false); + }); +}); + +add_task(async function test_history() { + const histograms = snapshotHistograms(); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + await PlacesTestUtils.addVisits([ + { + uri: "http://example.com", + title: "example", + transition: Ci.nsINavHistoryService.TRANSITION_TYPED, + }, + ]); + + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await searchInAwesomebar("example"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_Enter"); + await p; + + assertSearchTelemetryEmpty(histograms.search_hist); + assertTelemetryResults( + histograms, + "history", + 1, + UrlbarTestUtils.SELECTED_RESULT_METHODS.arrowEnterSelection + ); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_bookmark() { + const histograms = snapshotHistograms(); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + let bm = await PlacesUtils.bookmarks.insert({ + url: "http://example.com", + title: "example", + parentGuid: PlacesUtils.bookmarks.menuGuid, + }); + + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await searchInAwesomebar("example"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_Enter"); + await p; + + assertSearchTelemetryEmpty(histograms.search_hist); + assertTelemetryResults( + histograms, + "bookmark", + 1, + UrlbarTestUtils.SELECTED_RESULT_METHODS.arrowEnterSelection + ); + + await PlacesUtils.bookmarks.remove(bm); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_keyword() { + const histograms = snapshotHistograms(); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await searchInAwesomebar("get example"); + EventUtils.synthesizeKey("KEY_Enter"); + await p; + + assertSearchTelemetryEmpty(histograms.search_hist); + assertTelemetryResults( + histograms, + "keyword", + 0, + UrlbarTestUtils.SELECTED_RESULT_METHODS.enter + ); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_switchtab() { + const histograms = snapshotHistograms(); + + let homeTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:buildconfig" + ); + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:mozilla" + ); + + let p = BrowserTestUtils.waitForEvent(gBrowser, "TabSwitchDone"); + await searchInAwesomebar("about:buildconfig"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_Enter"); + await p; + + assertSearchTelemetryEmpty(histograms.search_hist); + assertTelemetryResults( + histograms, + "switchtab", + 1, + UrlbarTestUtils.SELECTED_RESULT_METHODS.arrowEnterSelection + ); + + BrowserTestUtils.removeTab(tab); + BrowserTestUtils.removeTab(homeTab); +}); + +add_task(async function test_visitURL() { + const histograms = snapshotHistograms(); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await searchInAwesomebar("http://example.com"); + EventUtils.synthesizeKey("KEY_Enter"); + await p; + + assertSearchTelemetryEmpty(histograms.search_hist); + assertTelemetryResults( + histograms, + "visiturl", + 0, + UrlbarTestUtils.SELECTED_RESULT_METHODS.enter + ); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_autofill() { + const histograms = snapshotHistograms(); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + await PlacesTestUtils.addVisits([ + { + uri: "http://example.com/mypage", + title: "example", + transition: Ci.nsINavHistoryService.TRANSITION_TYPED, + }, + ]); + + Services.prefs.setBoolPref("browser.urlbar.autoFill", true); + + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await searchInAwesomebar("example.com/my"); + EventUtils.synthesizeKey("KEY_Enter"); + await p; + + assertSearchTelemetryEmpty(histograms.search_hist); + assertTelemetryResults( + histograms, + "autofill", + 0, + UrlbarTestUtils.SELECTED_RESULT_METHODS.enter + ); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_remotetab.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_remotetab.js new file mode 100644 index 0000000000..be6c0acf51 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_remotetab.js @@ -0,0 +1,211 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This file tests urlbar telemetry with remote tab action. + */ + +"use strict"; + +const SCALAR_URLBAR = "browser.engagement.navigation.urlbar"; + +XPCOMUtils.defineLazyModuleGetters(this, { + SyncedTabs: "resource://services-sync/SyncedTabs.jsm", + UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.jsm", +}); + +function assertSearchTelemetryEmpty(search_hist) { + const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false); + Assert.ok( + !(SCALAR_URLBAR in scalars), + `Should not have recorded ${SCALAR_URLBAR}` + ); + + // Make sure SEARCH_COUNTS contains identical values. + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "other-MozSearch.urlbar", + undefined + ); + TelemetryTestUtils.assertKeyedHistogramSum( + search_hist, + "other-MozSearch.alias", + undefined + ); + + // Also check events. + let events = Services.telemetry.snapshotEvents( + Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, + false + ); + events = (events.parent || []).filter( + e => e[1] == "navigation" && e[2] == "search" + ); + Assert.deepEqual( + events, + [], + "Should not have recorded any navigation search events" + ); +} + +function snapshotHistograms() { + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + return { + resultIndexHist: TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_INDEX" + ), + resultTypeHist: TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_TYPE_2" + ), + resultIndexByTypeHist: TelemetryTestUtils.getAndClearKeyedHistogram( + "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE_2" + ), + resultMethodHist: TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ), + search_hist: TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"), + }; +} + +function assertTelemetryResults(histograms, type, index, method) { + TelemetryTestUtils.assertHistogram(histograms.resultIndexHist, index, 1); + + TelemetryTestUtils.assertHistogram( + histograms.resultTypeHist, + UrlbarUtils.SELECTED_RESULT_TYPES[type], + 1 + ); + + TelemetryTestUtils.assertKeyedHistogramValue( + histograms.resultIndexByTypeHist, + type, + index, + 1 + ); + + TelemetryTestUtils.assertHistogram(histograms.resultMethodHist, method, 1); + + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + `urlbar.picked.${type}`, + index, + 1 + ); +} + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [ + // Disable search suggestions in the urlbar. + ["browser.urlbar.suggest.searches", false], + // Clear historical search suggestions to avoid interference from previous + // tests. + ["browser.urlbar.maxHistoricalSearchSuggestions", 0], + // Use the default matching bucket configuration. + ["browser.urlbar.matchBuckets", "general:5,suggestion:4"], + // Turn autofill off. + ["browser.urlbar.autoFill", false], + // Special prefs for remote tabs. + ["services.sync.username", "fake"], + ["services.sync.syncedTabs.showRemoteTabs", true], + ], + }); + + // Enable local telemetry recording for the duration of the tests. + let oldCanRecord = Services.telemetry.canRecordExtended; + Services.telemetry.canRecordExtended = true; + + // Enable event recording for the events tested here. + Services.telemetry.setEventRecordingEnabled("navigation", true); + + // Clear history so that history added by previous tests doesn't mess up this + // test when it selects results in the urlbar. + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + + const REMOTE_TAB = { + id: "7cqCr77ptzX3", + type: "client", + lastModified: 1492201200, + name: "zcarter's Nightly on MacBook-Pro-25", + clientType: "desktop", + tabs: [ + { + type: "tab", + title: "Test Remote", + url: "http://example.com", + icon: UrlbarUtils.ICON.DEFAULT, + client: "7cqCr77ptzX3", + lastUsed: 1452124677, + }, + ], + }; + + const sandbox = sinon.createSandbox(); + + let originalSyncedTabsInternal = SyncedTabs._internal; + SyncedTabs._internal = { + isConfiguredToSyncTabs: true, + hasSyncedThisSession: true, + getTabClients() { + return Promise.resolve([]); + }, + syncTabs() { + return Promise.resolve(); + }, + }; + + // Tell the Sync XPCOM service it is initialized. + let weaveXPCService = Cc["@mozilla.org/weave/service;1"].getService( + Ci.nsISupports + ).wrappedJSObject; + let oldWeaveServiceReady = weaveXPCService.ready; + weaveXPCService.ready = true; + + sandbox + .stub(SyncedTabs._internal, "getTabClients") + .callsFake(() => Promise.resolve(Cu.cloneInto([REMOTE_TAB], {}))); + + // Make sure to restore the engine once we're done. + registerCleanupFunction(async function() { + sandbox.restore(); + weaveXPCService.ready = oldWeaveServiceReady; + SyncedTabs._internal = originalSyncedTabsInternal; + Services.telemetry.canRecordExtended = oldCanRecord; + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + Services.telemetry.setEventRecordingEnabled("navigation", false); + }); +}); + +add_task(async function test_remotetab() { + const histograms = snapshotHistograms(); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + waitForFocus, + value: "example", + fireInputEvent: true, + }); + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_Enter"); + await p; + + assertSearchTelemetryEmpty(histograms.search_hist); + assertTelemetryResults( + histograms, + "remotetab", + 1, + UrlbarTestUtils.SELECTED_RESULT_METHODS.arrowEnterSelection + ); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_searchmode.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_searchmode.js new file mode 100644 index 0000000000..54332259d4 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_searchmode.js @@ -0,0 +1,579 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This file tests the urlbar.searchmode.* scalars telemetry with search mode + * related actions. + */ + +"use strict"; + +const ENTRY_SCALAR_PREFIX = "urlbar.searchmode."; +const PICKED_SCALAR_PREFIX = "urlbar.picked.searchmode."; +const ENGINE_ALIAS = "alias"; +const TEST_QUERY = "test"; +let engineName; +let engineDomain; + +// The preference to enable suggestions. +const SUGGEST_PREF = "browser.search.suggest.enabled"; + +XPCOMUtils.defineLazyModuleGetters(this, { + AppConstants: "resource://gre/modules/AppConstants.jsm", + UrlbarProviderTabToSearch: + "resource:///modules/UrlbarProviderTabToSearch.jsm", + UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.jsm", +}); + +XPCOMUtils.defineLazyServiceGetter( + this, + "TouchBarHelper", + "@mozilla.org/widget/touchbarhelper;1", + "nsITouchBarHelper" +); + +/** + * Asserts that search mode telemetry was recorded correctly. Checks both the + * urlbar.searchmode.* and urlbar.searchmode_picked.* probes. + * @param {string} entry + * A search mode entry point. + * @param {string} engineOrSource + * An engine name or a search mode source. + * @param {number} [resultIndex] + * The index of the result picked while in search mode. Only pass this + * parameter if a result is picked. + */ +function assertSearchModeScalars(entry, engineOrSource, resultIndex = -1) { + // Check if the urlbar.searchmode.entry scalar contains the expected value. + const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false); + TelemetryTestUtils.assertKeyedScalar( + scalars, + ENTRY_SCALAR_PREFIX + entry, + engineOrSource, + 1 + ); + + for (let e of UrlbarUtils.SEARCH_MODE_ENTRY) { + if (e == entry) { + Assert.equal( + Object.keys(scalars[ENTRY_SCALAR_PREFIX + entry]).length, + 1, + `This search must only increment one entry in the correct scalar: ${e}` + ); + } else { + Assert.ok( + !scalars[ENTRY_SCALAR_PREFIX + e], + `No other urlbar.searchmode scalars should be recorded. Checking ${e}` + ); + } + } + + if (resultIndex >= 0) { + TelemetryTestUtils.assertKeyedScalar( + scalars, + PICKED_SCALAR_PREFIX + entry, + resultIndex, + 1 + ); + } + + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); +} + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [ + // Disable tab-to-search onboarding results for general tests. They are + // enabled in tests that specifically address onboarding. + ["browser.urlbar.tabToSearch.onboard.interactionsLeft", 0], + ], + }); + + // Create an engine to generate search suggestions and add it as default + // for this test. + const url = + getRootDirectory(gTestPath) + "urlbarTelemetrySearchSuggestions.xml"; + let suggestionEngine = await Services.search.addOpenSearchEngine(url, ""); + suggestionEngine.alias = ENGINE_ALIAS; + engineDomain = suggestionEngine.getResultDomain(); + engineName = suggestionEngine.name; + + // Make it the default search engine. + let originalEngine = await Services.search.getDefault(); + await Services.search.setDefault(suggestionEngine); + + // And the first one-off engine. + await Services.search.moveEngine(suggestionEngine, 0); + + // Enable local telemetry recording for the duration of the tests. + let oldCanRecord = Services.telemetry.canRecordExtended; + Services.telemetry.canRecordExtended = true; + + // Clear history so that history added by previous tests doesn't mess up this + // test when it selects results in the urlbar. + await PlacesUtils.history.clear(); + + // Clear historical search suggestions to avoid interference from previous + // tests. + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.maxHistoricalSearchSuggestions", 0]], + }); + + // Allows UrlbarTestUtils to access this scope's test helpers, like Assert. + UrlbarTestUtils.init(this); + + // Make sure to restore the engine once we're done. + registerCleanupFunction(async function() { + Services.telemetry.canRecordExtended = oldCanRecord; + await Services.search.setDefault(originalEngine); + await Services.search.removeEngine(suggestionEngine); + await PlacesUtils.history.clear(); + Services.telemetry.setEventRecordingEnabled("navigation", false); + UrlbarTestUtils.uninit(); + }); +}); + +// Clicks the first one off. +add_task(async function test_oneoff_remote() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TEST_QUERY, + }); + // Enters search mode by clicking a one-off. + await UrlbarTestUtils.enterSearchMode(window); + let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + EventUtils.synthesizeKey("KEY_Enter"); + await loadPromise; + assertSearchModeScalars("oneoff", "other", 0); + + BrowserTestUtils.removeTab(tab); +}); + +// Clicks the history one off. +add_task(async function test_oneoff_local() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TEST_QUERY, + }); + // Enters search mode by clicking a one-off. + await UrlbarTestUtils.enterSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + }); + let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_Enter"); + await loadPromise; + assertSearchModeScalars("oneoff", "history", 0); + + BrowserTestUtils.removeTab(tab); +}); + +// Checks that the Amazon search mode name is collapsed to "Amazon". +add_task(async function test_oneoff_amazon() { + // Disable suggestions to avoid hitting Amazon servers. + await SpecialPowers.pushPrefEnv({ + set: [[SUGGEST_PREF, false]], + }); + + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TEST_QUERY, + }); + // Enters search mode by clicking a one-off. + await UrlbarTestUtils.enterSearchMode(window, { + engineName: "Amazon.com", + }); + assertSearchModeScalars("oneoff", "Amazon"); + await UrlbarTestUtils.exitSearchMode(window); + + BrowserTestUtils.removeTab(tab); + await SpecialPowers.popPrefEnv(); +}); + +// Checks that the Wikipedia search mode name is collapsed to "Wikipedia". +add_task(async function test_oneoff_wikipedia() { + // Disable suggestions to avoid hitting Wikipedia servers. + await SpecialPowers.pushPrefEnv({ + set: [[SUGGEST_PREF, false]], + }); + + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TEST_QUERY, + }); + // Enters search mode by clicking a one-off. + await UrlbarTestUtils.enterSearchMode(window, { + engineName: "Wikipedia (en)", + }); + assertSearchModeScalars("oneoff", "Wikipedia"); + await UrlbarTestUtils.exitSearchMode(window); + + BrowserTestUtils.removeTab(tab); + await SpecialPowers.popPrefEnv(); +}); + +// Enters search mode by pressing the keyboard shortcut. +add_task(async function test_shortcut() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TEST_QUERY, + }); + // Enter search mode by pressing the keyboard shortcut. + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey("k", { accelKey: true }); + await searchPromise; + + await UrlbarTestUtils.assertSearchMode(window, { + engineName, + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + entry: "shortcut", + }); + assertSearchModeScalars("shortcut", "other"); + + BrowserTestUtils.removeTab(tab); +}); + +// Enters search mode by selecting a Top Site from the Urlbar. +add_task(async function test_topsites_urlbar() { + // Disable suggestions to avoid hitting Amazon servers. + await SpecialPowers.pushPrefEnv({ + set: [[SUGGEST_PREF, false]], + }); + + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + // Enter search mode by selecting a Top Site from the Urlbar. + await UrlbarTestUtils.promisePopupOpen(window, () => { + if (gURLBar.getAttribute("pageproxystate") == "invalid") { + gURLBar.handleRevert(); + } + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + await UrlbarTestUtils.promiseSearchComplete(window); + + let amazonSearch = await UrlbarTestUtils.waitForAutocompleteResultAt( + window, + 0 + ); + Assert.equal( + amazonSearch.result.payload.keyword, + "@amazon", + "First result should have the Amazon keyword." + ); + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeMouseAtCenter(amazonSearch, {}); + await searchPromise; + + await UrlbarTestUtils.assertSearchMode(window, { + engineName: amazonSearch.result.payload.engine, + entry: "topsites_urlbar", + }); + assertSearchModeScalars("topsites_urlbar", "Amazon"); + await UrlbarTestUtils.exitSearchMode(window); + + BrowserTestUtils.removeTab(tab); + await SpecialPowers.popPrefEnv(); +}); + +// Enters search mode by selecting a keyword offer result. +add_task(async function test_keywordoffer() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + // Do a search for "@" + our test alias. It should autofill with a trailing + // space, and the heuristic result should be an autofill result with a keyword + // offer. + let alias = "@" + ENGINE_ALIAS; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: alias, + }); + let keywordOfferResult = await UrlbarTestUtils.getDetailsOfResultAt( + window, + 0 + ); + Assert.equal( + keywordOfferResult.searchParams.keyword, + alias, + "The first result should be a keyword search result with the correct alias." + ); + + // Pick the keyword offer result. + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey("KEY_Enter"); + await searchPromise; + + await UrlbarTestUtils.assertSearchMode(window, { + engineName, + entry: "keywordoffer", + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "foo", + }); + let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + EventUtils.synthesizeKey("KEY_Enter"); + await loadPromise; + assertSearchModeScalars("keywordoffer", "other", 0); + + BrowserTestUtils.removeTab(tab); +}); + +// Enters search mode by typing an alias. +add_task(async function test_typed() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + // Enter search mode by selecting a keywordoffer result. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: `${ENGINE_ALIAS} `, + }); + + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey(" "); + await searchPromise; + + await UrlbarTestUtils.assertSearchMode(window, { + engineName, + entry: "typed", + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "foo", + }); + let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + EventUtils.synthesizeKey("KEY_Enter"); + await loadPromise; + assertSearchModeScalars("typed", "other", 0); + + BrowserTestUtils.removeTab(tab); +}); + +// Enters search mode by calling the same function called by the Search +// Bookmarks menu item in Library > Bookmarks. +add_task(async function test_bookmarkmenu() { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + PlacesCommandHook.searchBookmarks(); + await searchPromise; + + await UrlbarTestUtils.assertSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + entry: "bookmarkmenu", + }); + assertSearchModeScalars("bookmarkmenu", "bookmarks"); + BrowserTestUtils.removeTab(tab); +}); + +// Enters search mode by calling the same function called by the Search Tabs +// menu item in the tab overflow menu. +add_task(async function test_tabmenu() { + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + gTabsPanel.searchTabs(); + await searchPromise; + + await UrlbarTestUtils.assertSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.TABS, + entry: "tabmenu", + }); + assertSearchModeScalars("tabmenu", "tabs"); +}); + +// Enters search mode by performing a search handoff on about:privatebrowsing. +// NOTE: We don't test handoff on about:home. Running mochitests on about:home +// is quite difficult. This subtest verifies that `handoff` is a valid scalar +// suffix and that a call to +// UrlbarInput.search(value, { searchEngine, searchModeEntry: "handoff" }) records values in +// the urlbar.searchmode.handoff scalar. PlacesFeed.test.js verfies that +// about:home handoff makes that exact call. +add_task(async function test_handoff_pbm() { + let win = await BrowserTestUtils.openNewBrowserWindow({ + private: true, + waitForTabURL: "about:privatebrowsing", + }); + let tab = win.gBrowser.selectedBrowser; + + await SpecialPowers.spawn(tab, [], async function() { + let btn = content.document.getElementById("search-handoff-button"); + btn.click(); + }); + + let searchPromise = UrlbarTestUtils.promiseSearchComplete(win); + await new Promise(r => EventUtils.synthesizeKey("f", {}, win, r)); + await searchPromise; + await UrlbarTestUtils.assertSearchMode(win, { + engineName, + entry: "handoff", + }); + assertSearchModeScalars("handoff", "other"); + + await UrlbarTestUtils.exitSearchMode(win); + await UrlbarTestUtils.promisePopupClose(win); + await BrowserTestUtils.closeWindow(win); +}); + +// Enters search mode by tapping a search shortcut on the Touch Bar. +add_task(async function test_touchbar() { + if (AppConstants.platform != "macosx") { + return; + } + + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: TEST_QUERY, + }); + // We have to fake the tap on the Touch Bar since mochitests have no way of + // interacting with the Touch Bar. + TouchBarHelper.insertRestrictionInUrlbar(UrlbarTokenizer.RESTRICT.HISTORY); + await UrlbarTestUtils.assertSearchMode(window, { + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + entry: "touchbar", + }); + let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_Enter"); + await loadPromise; + assertSearchModeScalars("touchbar", "history", 0); + BrowserTestUtils.removeTab(tab); +}); + +// Enters search mode by selecting a tab-to-search result. +// Tests that tab-to-search results preview search mode when highlighted. These +// results are worth testing separately since they do not set the +// payload.keyword parameter. +add_task(async function test_tabtosearch() { + await SpecialPowers.pushPrefEnv({ + set: [ + // Do not show the onboarding result for this subtest. + ["browser.urlbar.tabToSearch.onboard.interactionsLeft", 0], + ], + }); + await PlacesTestUtils.addVisits([`http://${engineDomain}/`]); + + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: engineDomain.slice(0, 4), + }); + let tabToSearchResult = ( + await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1) + ).result; + Assert.equal( + tabToSearchResult.providerName, + "TabToSearch", + "The second result is a tab-to-search result." + ); + Assert.equal( + tabToSearchResult.payload.engine, + engineName, + "The tab-to-search result is for the correct engine." + ); + await UrlbarTestUtils.assertSearchMode(window, null); + EventUtils.synthesizeKey("KEY_ArrowDown"); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 1, + "Sanity check: The second result is selected." + ); + // Pick the tab-to-search result. + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey("KEY_Enter"); + await searchPromise; + + await UrlbarTestUtils.assertSearchMode(window, { + engineName, + entry: "tabtosearch", + }); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "foo", + }); + let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + EventUtils.synthesizeKey("KEY_Enter"); + await loadPromise; + assertSearchModeScalars("tabtosearch", "other", 0); + + BrowserTestUtils.removeTab(tab); + + await PlacesUtils.history.clear(); + await SpecialPowers.popPrefEnv(); +}); + +// Enters search mode by selecting a tab-to-search onboarding result. +add_task(async function test_tabtosearch_onboard() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.tabToSearch.onboard.interactionsLeft", 3]], + }); + await PlacesTestUtils.addVisits([`http://${engineDomain}/`]); + + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: engineDomain.slice(0, 4), + }); + let tabToSearchResult = ( + await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1) + ).result; + Assert.equal( + tabToSearchResult.providerName, + "TabToSearch", + "The second result is a tab-to-search result." + ); + Assert.equal( + tabToSearchResult.payload.engine, + engineName, + "The tab-to-search result is for the correct engine." + ); + Assert.equal( + tabToSearchResult.payload.dynamicType, + "onboardTabToSearch", + "The tab-to-search result is an onboarding result." + ); + await UrlbarTestUtils.assertSearchMode(window, null); + EventUtils.synthesizeKey("KEY_ArrowDown"); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 1, + "Sanity check: The second result is selected." + ); + // Pick the tab-to-search onboarding result. + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey("KEY_Enter"); + await searchPromise; + + await UrlbarTestUtils.assertSearchMode(window, { + engineName, + entry: "tabtosearch_onboard", + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "foo", + }); + let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + EventUtils.synthesizeKey("KEY_Enter"); + await loadPromise; + assertSearchModeScalars("tabtosearch_onboard", "other", 0); + + UrlbarPrefs.set("tabToSearch.onboard.interactionsLeft", 3); + delete UrlbarProviderTabToSearch.onboardingInteractionAtTime; + + BrowserTestUtils.removeTab(tab); + await PlacesUtils.history.clear(); + await SpecialPowers.popPrefEnv(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_tabtosearch.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_tabtosearch.js new file mode 100644 index 0000000000..386e4268ea --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_tabtosearch.js @@ -0,0 +1,356 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This file tests telemetry for tabtosearch results. + * NB: This file does not test the search mode `entry` field for tab-to-search + * results. That is tested in browser_UsageTelemetry_urlbar_searchmode.js. + */ + +"use strict"; + +const ENGINE_NAME = "MozSearch"; +const ENGINE_DOMAIN = "example.com"; + +XPCOMUtils.defineLazyModuleGetters(this, { + UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.jsm", +}); + +function snapshotHistograms() { + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + return { + resultIndexHist: TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_INDEX" + ), + resultTypeHist: TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_TYPE_2" + ), + resultIndexByTypeHist: TelemetryTestUtils.getAndClearKeyedHistogram( + "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE_2" + ), + resultMethodHist: TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ), + search_hist: TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"), + }; +} + +function assertTelemetryResults(histograms, type, index, method) { + TelemetryTestUtils.assertHistogram(histograms.resultIndexHist, index, 1); + + TelemetryTestUtils.assertHistogram( + histograms.resultTypeHist, + UrlbarUtils.SELECTED_RESULT_TYPES[type], + 1 + ); + + TelemetryTestUtils.assertKeyedHistogramValue( + histograms.resultIndexByTypeHist, + type, + index, + 1 + ); + + TelemetryTestUtils.assertHistogram(histograms.resultMethodHist, method, 1); + + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + `urlbar.picked.${type}`, + index, + 1 + ); +} + +/** + * Checks to see if the second result in the Urlbar is an onboarding result + * with the correct engine. + * + * @param {string} engineName + * The expected engine name. + */ +async function checkForOnboardingResult(engineName) { + Assert.ok(UrlbarTestUtils.isPopupOpen(window), "Popup should be open."); + let tabToSearchResult = ( + await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1) + ).result; + Assert.equal( + tabToSearchResult.providerName, + "TabToSearch", + "The second result is a tab-to-search result." + ); + Assert.equal( + tabToSearchResult.payload.engine, + engineName, + "The tab-to-search result is for the first engine." + ); + Assert.equal( + tabToSearchResult.payload.dynamicType, + "onboardTabToSearch", + "The tab-to-search result is an onboarding result." + ); +} + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.tabToSearch.onboard.interactionsLeft", 0]], + }); + + let engine = await Services.search.addEngineWithDetails(ENGINE_NAME, { + template: `http://${ENGINE_DOMAIN}/?q={searchTerms}`, + }); + + UrlbarTestUtils.init(this); + + registerCleanupFunction(async () => { + UrlbarTestUtils.uninit(); + await Services.search.removeEngine(engine); + }); +}); + +add_task(async function test() { + await BrowserTestUtils.withNewTab("about:blank", async () => { + const histograms = snapshotHistograms(); + + for (let i = 0; i < 3; i++) { + await PlacesTestUtils.addVisits([`https://${ENGINE_DOMAIN}/`]); + } + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: ENGINE_DOMAIN.slice(0, 4), + }); + + let tabToSearchResult = ( + await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1) + ).result; + Assert.equal( + tabToSearchResult.providerName, + "TabToSearch", + "The second result is a tab-to-search result." + ); + Assert.equal( + tabToSearchResult.payload.engine, + ENGINE_NAME, + "The tab-to-search result is for the correct engine." + ); + EventUtils.synthesizeKey("KEY_ArrowDown"); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 1, + "Sanity check: The second result is selected." + ); + + // Select the tab-to-search result. + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey("KEY_Enter"); + await searchPromise; + + await UrlbarTestUtils.assertSearchMode(window, { + engineName: ENGINE_NAME, + entry: "tabtosearch", + }); + + assertTelemetryResults( + histograms, + "tabtosearch", + 1, + UrlbarTestUtils.SELECTED_RESULT_METHODS.arrowEnterSelection + ); + + await UrlbarTestUtils.exitSearchMode(window); + await UrlbarTestUtils.promisePopupClose(window, () => { + gURLBar.blur(); + }); + await PlacesUtils.history.clear(); + }); +}); + +add_task(async function onboarding_impressions() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.tabToSearch.onboard.interactionsLeft", 3]], + }); + + await BrowserTestUtils.withNewTab("about:blank", async browser => { + const firstEngineHost = "example"; + let secondEngine = await Services.search.addEngineWithDetails( + `${ENGINE_NAME}2`, + { template: `http://${firstEngineHost}-2.com/?q={searchTerms}` } + ); + + for (let i = 0; i < 3; i++) { + await PlacesTestUtils.addVisits([`https://${firstEngineHost}-2.com`]); + await PlacesTestUtils.addVisits([`https://${ENGINE_DOMAIN}/`]); + } + + // First do multiple searches for substrings of firstEngineHost. The view + // should show the same tab-to-search onboarding result the entire time, so + // we should not continue to increment urlbar.tips. + for (let i = 1; i < firstEngineHost.length; i++) { + info( + `Search for "${firstEngineHost.slice( + 0, + i + )}". Only record one impression for this sequence.` + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: firstEngineHost.slice(0, i), + fireInputEvent: true, + }); + await checkForOnboardingResult(ENGINE_NAME); + } + + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true), + "urlbar.tips", + "tabtosearch_onboard-shown", + 1 + ); + + info("Type through autofill to second engine hostname. Record impression."); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: firstEngineHost, + fireInputEvent: true, + }); + await checkForOnboardingResult(ENGINE_NAME); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: `${firstEngineHost}-`, + fireInputEvent: true, + }); + await checkForOnboardingResult(`${ENGINE_NAME}2`); + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); + // Since the user typed past the autofill for the first engine, we showed a + // different onboarding result and now we increment + // tabtosearch_onboard-shown. + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true), + "urlbar.tips", + "tabtosearch_onboard-shown", + 3 + ); + + info("Make a typo and return to autofill. Do not record impression."); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: `${firstEngineHost}-`, + fireInputEvent: true, + }); + await checkForOnboardingResult(`${ENGINE_NAME}2`); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: `${firstEngineHost}-3`, + fireInputEvent: true, + }); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + 1, + "We are not showing a tab-to-search result." + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: `${firstEngineHost}-2`, + fireInputEvent: true, + }); + await checkForOnboardingResult(`${ENGINE_NAME}2`); + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true), + "urlbar.tips", + "tabtosearch_onboard-shown", + 4 + ); + + info("Cancel then restart autofill. Do not record impression."); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: `${firstEngineHost}-2`, + fireInputEvent: true, + }); + await checkForOnboardingResult(`${ENGINE_NAME}2`); + let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey("KEY_Backspace"); + await searchPromise; + Assert.greater( + UrlbarTestUtils.getResultCount(window), + 1, + "Sanity check: we have more than one result." + ); + let result = (await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1)) + .result; + Assert.notEqual( + result.type, + UrlbarUtils.RESULT_TYPE.SEARCH, + "The second result is not a tab-to-search result." + ); + searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + // Type the "." from `example-2.com`. + EventUtils.synthesizeKey("."); + await searchPromise; + await checkForOnboardingResult(`${ENGINE_NAME}2`); + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true), + "urlbar.tips", + "tabtosearch_onboard-shown", + 5 + ); + + // See javadoc for UrlbarProviderTabToSearch.onEngagement for discussion + // about retained results. + info("Reopen the result set with retained results. Record impression."); + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + await checkForOnboardingResult(`${ENGINE_NAME}2`); + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true), + "urlbar.tips", + "tabtosearch_onboard-shown", + 6 + ); + + info( + "Open a result page and then autofill engine host. Record impression." + ); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: firstEngineHost, + fireInputEvent: true, + }); + await checkForOnboardingResult(ENGINE_NAME); + // Press enter on the heuristic result so we visit example.com without + // doing an additional search. + let loadPromise = BrowserTestUtils.browserLoaded(browser); + EventUtils.synthesizeKey("KEY_Enter"); + await loadPromise; + // Click the Urlbar and type to simulate what a user would actually do. If + // we use promiseAutocompleteResultPopup, no query would be made between + // this one and the previous tab-to-search query. Thus + // `onboardingEnginesShown` would not be cleared. This would not happen + // in real-world usage. + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + searchPromise = UrlbarTestUtils.promiseSearchComplete(window); + EventUtils.synthesizeKey(firstEngineHost.slice(0, 4)); + await searchPromise; + await checkForOnboardingResult(ENGINE_NAME); + await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur()); + // We clear the scalar this time. + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + "urlbar.tips", + "tabtosearch_onboard-shown", + 8 + ); + + await PlacesUtils.history.clear(); + await Services.search.removeEngine(secondEngine); + }); +}); diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_tip.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_tip.js new file mode 100644 index 0000000000..a23fffdb3f --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_tip.js @@ -0,0 +1,149 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This file tests urlbar telemetry for tip results. + */ + +"use strict"; + +XPCOMUtils.defineLazyModuleGetters(this, { + UrlbarProvider: "resource:///modules/UrlbarUtils.jsm", + UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.jsm", + UrlbarResult: "resource:///modules/UrlbarResult.jsm", + UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.jsm", +}); + +function snapshotHistograms() { + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + return { + resultIndexHist: TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_INDEX" + ), + resultTypeHist: TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_TYPE_2" + ), + resultIndexByTypeHist: TelemetryTestUtils.getAndClearKeyedHistogram( + "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE_2" + ), + resultMethodHist: TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ), + search_hist: TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"), + }; +} + +function assertTelemetryResults(histograms, type, index, method) { + TelemetryTestUtils.assertHistogram(histograms.resultIndexHist, index, 1); + + TelemetryTestUtils.assertHistogram( + histograms.resultTypeHist, + UrlbarUtils.SELECTED_RESULT_TYPES[type], + 1 + ); + + TelemetryTestUtils.assertKeyedHistogramValue( + histograms.resultIndexByTypeHist, + type, + index, + 1 + ); + + TelemetryTestUtils.assertHistogram(histograms.resultMethodHist, method, 1); + + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + `urlbar.picked.${type}`, + index, + 1 + ); +} + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [ + // Disable search suggestions in the urlbar. + ["browser.urlbar.suggest.searches", false], + // Turn autofill off. + ["browser.urlbar.autoFill", false], + ], + }); + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(async function test() { + // Add a restricting provider that returns a preselected heuristic tip result. + let provider = new TipProvider([ + Object.assign( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TIP, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + icon: "", + text: "This is a test tip.", + buttonText: "OK", + type: "test", + } + ), + { heuristic: true } + ), + ]); + UrlbarProvidersManager.registerProvider(provider); + + const histograms = snapshotHistograms(); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:blank" + ); + + // Show the view and press enter to select the tip. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + waitForFocus, + value: "test", + fireInputEvent: true, + }); + EventUtils.synthesizeKey("KEY_Enter"); + + assertTelemetryResults( + histograms, + "tip", + 0, + UrlbarTestUtils.SELECTED_RESULT_METHODS.enter + ); + + UrlbarProvidersManager.unregisterProvider(provider); + BrowserTestUtils.removeTab(tab); +}); + +/** + * A test URLBar provider. + */ +class TipProvider extends UrlbarProvider { + constructor(results) { + super(); + this._results = results; + } + get name() { + return "TestProviderTip"; + } + get type() { + return UrlbarUtils.PROVIDER_TYPE.PROFILE; + } + isActive(context) { + return true; + } + getPriority(context) { + return 1; + } + async startQuery(context, addCallback) { + context.preselected = true; + for (const result of this._results) { + addCallback(this, result); + } + } +} diff --git a/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_topsite.js b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_topsite.js new file mode 100644 index 0000000000..352e08cf8f --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_urlbar_telemetry_topsite.js @@ -0,0 +1,151 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This file tests urlbar telemetry for topsite results. + */ + +"use strict"; + +XPCOMUtils.defineLazyModuleGetters(this, { + AboutNewTab: "resource:///modules/AboutNewTab.jsm", + UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.jsm", +}); + +const EN_US_TOPSITES = + "https://www.youtube.com/,https://www.facebook.com/,https://www.amazon.com/,https://www.reddit.com/,https://www.wikipedia.org/,https://twitter.com/"; + +function snapshotHistograms() { + Services.telemetry.clearScalars(); + Services.telemetry.clearEvents(); + return { + resultIndexHist: TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_INDEX" + ), + resultTypeHist: TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_TYPE_2" + ), + resultIndexByTypeHist: TelemetryTestUtils.getAndClearKeyedHistogram( + "FX_URLBAR_SELECTED_RESULT_INDEX_BY_TYPE_2" + ), + resultMethodHist: TelemetryTestUtils.getAndClearHistogram( + "FX_URLBAR_SELECTED_RESULT_METHOD" + ), + search_hist: TelemetryTestUtils.getAndClearKeyedHistogram("SEARCH_COUNTS"), + }; +} + +function assertTelemetryResults(histograms, type, index, method) { + TelemetryTestUtils.assertHistogram(histograms.resultIndexHist, index, 1); + + TelemetryTestUtils.assertHistogram( + histograms.resultTypeHist, + UrlbarUtils.SELECTED_RESULT_TYPES[type], + 1 + ); + + TelemetryTestUtils.assertKeyedHistogramValue( + histograms.resultIndexByTypeHist, + type, + index, + 1 + ); + + TelemetryTestUtils.assertHistogram(histograms.resultMethodHist, method, 1); + + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + `urlbar.picked.${type}`, + index, + 1 + ); +} + +/** + * Updates the Top Sites feed. + * @param {function} condition + * A callback that returns true after Top Sites are successfully updated. + * @param {boolean} searchShortcuts + * True if Top Sites search shortcuts should be enabled. + */ +async function updateTopSites(condition, searchShortcuts = false) { + // Toggle the pref to clear the feed cache and force an update. + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.newtabpage.activity-stream.feeds.system.topsites", false], + ["browser.newtabpage.activity-stream.feeds.system.topsites", true], + [ + "browser.newtabpage.activity-stream.improvesearch.topSiteSearchShortcuts", + searchShortcuts, + ], + ], + }); + + // Wait for the feed to be updated. + await TestUtils.waitForCondition(() => { + let sites = AboutNewTab.getTopSites(); + return condition(sites); + }, "Waiting for top sites to be updated"); +} + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.topsites", true], + ["browser.newtabpage.activity-stream.default.sites", EN_US_TOPSITES], + ], + }); + await updateTopSites( + sites => sites && sites.length == EN_US_TOPSITES.split(",").length + ); +}); + +add_task(async function test() { + await BrowserTestUtils.withNewTab("about:blank", async () => { + let sites = AboutNewTab.getTopSites(); + Assert.equal( + sites.length, + 6, + "The test suite browser should have 6 Top Sites." + ); + + const histograms = snapshotHistograms(); + + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeMouseAtCenter(gURLBar.inputField, {}); + }); + + await UrlbarTestUtils.promiseSearchComplete(window); + Assert.equal( + UrlbarTestUtils.getResultCount(window), + sites.length, + "The number of results should be the same as the number of Top Sites (6)." + ); + // Select the first resultm and confirm it. + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + EventUtils.synthesizeKey("KEY_ArrowDown"); + Assert.equal( + UrlbarTestUtils.getSelectedRowIndex(window), + 0, + "The first result should be selected" + ); + + let loadPromise = BrowserTestUtils.waitForDocLoadAndStopIt( + result.url, + gBrowser.selectedBrowser + ); + EventUtils.synthesizeKey("KEY_Enter"); + await loadPromise; + + assertTelemetryResults( + histograms, + "topsite", + 0, + UrlbarTestUtils.SELECTED_RESULT_METHODS.arrowEnterSelection + ); + await UrlbarTestUtils.promisePopupClose(window, () => { + gURLBar.blur(); + }); + }); +}); diff --git a/browser/components/urlbar/tests/browser/browser_userTypedValue.js b/browser/components/urlbar/tests/browser/browser_userTypedValue.js new file mode 100644 index 0000000000..8319b37962 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_userTypedValue.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +function test() { + const URI = TEST_BASE_URL + "file_userTypedValue.html"; + window.browserDOMWindow.openURI( + makeURI(URI), + null, + Ci.nsIBrowserDOMWindow.OPEN_NEWTAB, + Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL, + Services.scriptSecurityManager.getSystemPrincipal() + ); + + is(gBrowser.userTypedValue, URI, "userTypedValue matches test URI"); + is(gURLBar.value, URI, "location bar value matches test URI"); + + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser); + gBrowser.removeCurrentTab({ skipPermitUnload: true }); + is( + gBrowser.userTypedValue, + URI, + "userTypedValue matches test URI after switching tabs" + ); + is( + gURLBar.value, + URI, + "location bar value matches test URI after switching tabs" + ); + + waitForExplicitFinish(); + BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(() => { + is( + gBrowser.userTypedValue, + null, + "userTypedValue is null as the page has loaded" + ); + is( + gURLBar.value, + URI, + "location bar value matches test URI as the page has loaded" + ); + + gBrowser.removeCurrentTab({ skipPermitUnload: true }); + finish(); + }); +} diff --git a/browser/components/urlbar/tests/browser/browser_valueOnTabSwitch.js b/browser/components/urlbar/tests/browser/browser_valueOnTabSwitch.js new file mode 100644 index 0000000000..d1053dfc8e --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_valueOnTabSwitch.js @@ -0,0 +1,166 @@ +/* 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/. */ + +/** + * This tests for the correct URL being displayed in the URL bar after switching + * tabs which are in different states (e.g. deleted, partially deleted). + */ + +"use strict"; + +const TEST_URL = `${TEST_BASE_URL}dummy_page.html`; + +add_task(async function() { + // autofill may conflict with the test scope, by filling missing parts of + // the url due to autoOpen. + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.autoFill", false]], + }); + + let charsToDelete, + deletedURLTab, + fullURLTab, + partialURLTab, + testPartialURL, + testURL; + + charsToDelete = 5; + deletedURLTab = BrowserTestUtils.addTab(gBrowser); + fullURLTab = BrowserTestUtils.addTab(gBrowser); + partialURLTab = BrowserTestUtils.addTab(gBrowser); + testURL = TEST_URL; + + let loaded1 = BrowserTestUtils.browserLoaded( + deletedURLTab.linkedBrowser, + false, + testURL + ); + let loaded2 = BrowserTestUtils.browserLoaded( + fullURLTab.linkedBrowser, + false, + testURL + ); + let loaded3 = BrowserTestUtils.browserLoaded( + partialURLTab.linkedBrowser, + false, + testURL + ); + BrowserTestUtils.loadURI(deletedURLTab.linkedBrowser, testURL); + BrowserTestUtils.loadURI(fullURLTab.linkedBrowser, testURL); + BrowserTestUtils.loadURI(partialURLTab.linkedBrowser, testURL); + await Promise.all([loaded1, loaded2, loaded3]); + + testURL = BrowserUtils.trimURL(testURL); + testPartialURL = testURL.substr(0, testURL.length - charsToDelete); + + function cleanUp() { + gBrowser.removeTab(fullURLTab); + gBrowser.removeTab(partialURLTab); + gBrowser.removeTab(deletedURLTab); + } + + async function cycleTabs() { + await BrowserTestUtils.switchTab(gBrowser, fullURLTab); + is( + gURLBar.value, + testURL, + "gURLBar.value should be testURL after switching back to fullURLTab" + ); + + await BrowserTestUtils.switchTab(gBrowser, partialURLTab); + is( + gURLBar.value, + testPartialURL, + "gURLBar.value should be testPartialURL after switching back to partialURLTab" + ); + await BrowserTestUtils.switchTab(gBrowser, deletedURLTab); + is( + gURLBar.value, + "", + 'gURLBar.value should be "" after switching back to deletedURLTab' + ); + + await BrowserTestUtils.switchTab(gBrowser, fullURLTab); + is( + gURLBar.value, + testURL, + "gURLBar.value should be testURL after switching back to fullURLTab" + ); + } + + function urlbarBackspace(removeAll) { + return new Promise((resolve, reject) => { + gBrowser.selectedBrowser.focus(); + gURLBar.addEventListener( + "input", + function() { + resolve(); + }, + { once: true } + ); + gURLBar.focus(); + if (removeAll) { + gURLBar.select(); + } else { + gURLBar.selectionStart = gURLBar.selectionEnd = gURLBar.value.length; + } + EventUtils.synthesizeKey("KEY_Backspace"); + }); + } + + async function prepareDeletedURLTab() { + await BrowserTestUtils.switchTab(gBrowser, deletedURLTab); + is( + gURLBar.value, + testURL, + "gURLBar.value should be testURL after initial switch to deletedURLTab" + ); + + // simulate the user removing the whole url from the location bar + await urlbarBackspace(true); + is(gURLBar.value, "", 'gURLBar.value should be "" (just set)'); + } + + async function prepareFullURLTab() { + await BrowserTestUtils.switchTab(gBrowser, fullURLTab); + is( + gURLBar.value, + testURL, + "gURLBar.value should be testURL after initial switch to fullURLTab" + ); + } + + async function preparePartialURLTab() { + await BrowserTestUtils.switchTab(gBrowser, partialURLTab); + is( + gURLBar.value, + testURL, + "gURLBar.value should be testURL after initial switch to partialURLTab" + ); + + // simulate the user removing part of the url from the location bar + let deleted = 0; + while (deleted < charsToDelete) { + await urlbarBackspace(false); + deleted++; + } + + is( + gURLBar.value, + testPartialURL, + "gURLBar.value should be testPartialURL (just set)" + ); + } + + // prepare the three tabs required by this test + + // First tab + await prepareFullURLTab(); + await preparePartialURLTab(); + await prepareDeletedURLTab(); + + // now cycle the tabs and make sure everything looks good + await cycleTabs(); + cleanUp(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_view_emptyResultSet.js b/browser/components/urlbar/tests/browser/browser_view_emptyResultSet.js new file mode 100644 index 0000000000..77b11adba8 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_view_emptyResultSet.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Tests that the view results are cleared and the view is closed, when an empty +// result set arrives after a non-empty one. + +add_task(async function() { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "foo", + }); + Assert.ok( + UrlbarTestUtils.getResultCount(window) > 0, + `There should be some results in the view.` + ); + Assert.ok(gURLBar.view.isOpen, `The view should be open.`); + + // Register an high priority empty result provider. + let provider = new UrlbarTestUtils.TestProvider({ + results: [], + priority: 999, + }); + UrlbarProvidersManager.registerProvider(provider); + registerCleanupFunction(async function() { + UrlbarProvidersManager.unregisterProvider(provider); + await PlacesUtils.history.clear(); + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "foo", + }); + Assert.ok( + UrlbarTestUtils.getResultCount(window) == 0, + `There should be no results in the view.` + ); + Assert.ok(!gURLBar.view.isOpen, `The view should have been closed.`); +}); diff --git a/browser/components/urlbar/tests/browser/browser_view_resultDisplay.js b/browser/components/urlbar/tests/browser/browser_view_resultDisplay.js new file mode 100644 index 0000000000..63de190935 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_view_resultDisplay.js @@ -0,0 +1,354 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Tests that a result has the various elements displayed in the URL bar as + * we expect them to be. + */ + +add_task(async function setup() { + await PlacesUtils.history.clear(); + + registerCleanupFunction(async function() { + await PlacesUtils.history.clear(); + Services.prefs.clearUserPref("browser.urlbar.trimURLs"); + }); +}); + +async function testResult(input, expected, index = 1) { + const ESCAPED_URL = encodeURI(input.url); + + await PlacesUtils.history.clear(); + if (index > 0) { + await PlacesTestUtils.addVisits({ + uri: input.url, + title: input.title, + }); + } + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: input.query, + }); + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, index); + Assert.equal(result.url, ESCAPED_URL, "Should have the correct url to load"); + Assert.equal( + result.displayed.url, + expected.displayedUrl, + "Should have the correct displayed url" + ); + Assert.equal( + result.displayed.title, + input.title, + "Should have the expected title" + ); + Assert.equal( + result.displayed.typeIcon, + "none", + "Should not have a type icon" + ); + if (index > 0) { + Assert.equal( + result.image, + `page-icon:${ESCAPED_URL}`, + "Should have the correct favicon" + ); + } + + assertDisplayedHighlights( + "title", + result.element.title, + expected.highlightedTitle + ); + + assertDisplayedHighlights("url", result.element.url, expected.highlightedUrl); +} + +function assertDisplayedHighlights(elementName, element, expectedResults) { + Assert.equal( + element.childNodes.length, + expectedResults.length, + `Should have the correct number of child nodes for ${elementName}` + ); + + for (let i = 0; i < element.childNodes.length; i++) { + let child = element.childNodes[i]; + Assert.equal( + child.textContent, + expectedResults[i][0], + `Should have the correct text for the ${i} part of the ${elementName}` + ); + Assert.equal( + child.nodeName, + expectedResults[i][1] ? "strong" : "#text", + `Should have the correct text/strong status for the ${i} part of the ${elementName}` + ); + } +} + +add_task(async function test_url_result() { + await testResult( + { + query: "\u6e2C\u8a66", + title: "The \u6e2C\u8a66 URL", + url: "https://example.com/\u6e2C\u8a66test", + }, + { + displayedUrl: "example.com/\u6e2C\u8a66test", + highlightedTitle: [ + ["The ", false], + ["\u6e2C\u8a66", true], + [" URL", false], + ], + highlightedUrl: [ + ["example.com/", false], + ["\u6e2C\u8a66", true], + ["test", false], + ], + } + ); +}); + +add_task(async function test_url_result_no_path() { + await testResult( + { + query: "ample", + title: "The Title", + url: "https://example.com/", + }, + { + displayedUrl: "example.com", + highlightedTitle: [["The Title", false]], + highlightedUrl: [ + ["ex", false], + ["ample", true], + [".com", false], + ], + } + ); +}); + +add_task(async function test_url_result_www() { + await testResult( + { + query: "ample", + title: "The Title", + url: "https://www.example.com/", + }, + { + displayedUrl: "example.com", + highlightedTitle: [["The Title", false]], + highlightedUrl: [ + ["ex", false], + ["ample", true], + [".com", false], + ], + } + ); +}); + +add_task(async function test_url_result_no_trimming() { + Services.prefs.setBoolPref("browser.urlbar.trimURLs", false); + + await testResult( + { + query: "\u6e2C\u8a66", + title: "The \u6e2C\u8a66 URL", + url: "http://example.com/\u6e2C\u8a66test", + }, + { + displayedUrl: "http://example.com/\u6e2C\u8a66test", + highlightedTitle: [ + ["The ", false], + ["\u6e2C\u8a66", true], + [" URL", false], + ], + highlightedUrl: [ + ["http://example.com/", false], + ["\u6e2C\u8a66", true], + ["test", false], + ], + } + ); + + Services.prefs.clearUserPref("browser.urlbar.trimURLs"); +}); + +add_task(async function test_case_insensitive_highlights_1() { + await testResult( + { + query: "exam", + title: "The examPLE URL EXAMple", + url: "https://example.com/ExAm", + }, + { + displayedUrl: "example.com/ExAm", + highlightedTitle: [ + ["The ", false], + ["exam", true], + ["PLE URL ", false], + ["EXAM", true], + ["ple", false], + ], + highlightedUrl: [ + ["exam", true], + ["ple.com/", false], + ["ExAm", true], + ], + } + ); +}); + +add_task(async function test_case_insensitive_highlights_2() { + await testResult( + { + query: "EXAM", + title: "The examPLE URL EXAMple", + url: "https://example.com/ExAm", + }, + { + displayedUrl: "example.com/ExAm", + highlightedTitle: [ + ["The ", false], + ["exam", true], + ["PLE URL ", false], + ["EXAM", true], + ["ple", false], + ], + highlightedUrl: [ + ["exam", true], + ["ple.com/", false], + ["ExAm", true], + ], + } + ); +}); + +add_task(async function test_case_insensitive_highlights_3() { + await testResult( + { + query: "eXaM", + title: "The examPLE URL EXAMple", + url: "https://example.com/ExAm", + }, + { + displayedUrl: "example.com/ExAm", + highlightedTitle: [ + ["The ", false], + ["exam", true], + ["PLE URL ", false], + ["EXAM", true], + ["ple", false], + ], + highlightedUrl: [ + ["exam", true], + ["ple.com/", false], + ["ExAm", true], + ], + } + ); +}); + +add_task(async function test_case_insensitive_highlights_4() { + await testResult( + { + query: "ExAm", + title: "The examPLE URL EXAMple", + url: "https://example.com/ExAm", + }, + { + displayedUrl: "example.com/ExAm", + highlightedTitle: [ + ["The ", false], + ["exam", true], + ["PLE URL ", false], + ["EXAM", true], + ["ple", false], + ], + highlightedUrl: [ + ["exam", true], + ["ple.com/", false], + ["ExAm", true], + ], + } + ); +}); + +add_task(async function test_case_insensitive_highlights_5() { + await testResult( + { + query: "exam foo", + title: "The examPLE URL foo EXAMple FOO", + url: "https://example.com/ExAm/fOo", + }, + { + displayedUrl: "example.com/ExAm/fOo", + highlightedTitle: [ + ["The ", false], + ["exam", true], + ["PLE URL ", false], + ["foo", true], + [" ", false], + ["EXAM", true], + ["ple ", false], + ["FOO", true], + ], + highlightedUrl: [ + ["exam", true], + ["ple.com/", false], + ["ExAm", true], + ["/", false], + ["fOo", true], + ], + } + ); +}); + +add_task(async function test_case_insensitive_highlights_6() { + await testResult( + { + query: "EXAM FOO", + title: "The examPLE URL foo EXAMple FOO", + url: "https://example.com/ExAm/fOo", + }, + { + displayedUrl: "example.com/ExAm/fOo", + highlightedTitle: [ + ["The ", false], + ["exam", true], + ["PLE URL ", false], + ["foo", true], + [" ", false], + ["EXAM", true], + ["ple ", false], + ["FOO", true], + ], + highlightedUrl: [ + ["exam", true], + ["ple.com/", false], + ["ExAm", true], + ["/", false], + ["fOo", true], + ], + } + ); +}); + +add_task(async function test_no_highlight_fallback_heuristic_url() { + info("Test unvisited heuristic (fallback provider)"); + await testResult( + { + query: "nonexisting.com", + title: "http://nonexisting.com/", + url: "http://nonexisting.com/", + }, + { + displayedUrl: "", // URL heuristic only has title. + highlightedTitle: [["http://nonexisting.com/", false]], + highlightedUrl: [], + }, + 0 // Test the heuristic result. + ); +}); diff --git a/browser/components/urlbar/tests/browser/browser_view_resultTypes_display.js b/browser/components/urlbar/tests/browser/browser_view_resultTypes_display.js new file mode 100644 index 0000000000..494f728f75 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_view_resultTypes_display.js @@ -0,0 +1,319 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { SyncedTabs } = ChromeUtils.import( + "resource://services-sync/SyncedTabs.jsm" +); + +const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml"; + +function assertElementsDisplayed(details, expected) { + Assert.equal( + details.type, + expected.type, + "Should be displaying a row of the correct type" + ); + Assert.equal( + details.title, + expected.title, + "Should be displaying the correct title" + ); + let separatorVisible = + window.getComputedStyle(details.element.separator).display != "none" && + window.getComputedStyle(details.element.separator).visibility != "collapse"; + Assert.equal( + expected.separator, + separatorVisible, + `Should${expected.separator ? " " : " not "}be displaying a separator` + ); +} + +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.suggest.searches", false], + // Disable search suggestions in the urlbar. + ["browser.urlbar.suggest.searches", false], + // Clear historical search suggestions to avoid interference from previous + // tests. + ["browser.urlbar.maxHistoricalSearchSuggestions", 0], + // Use the default matching bucket configuration. + ["browser.urlbar.matchBuckets", "general:5,suggestion:4"], + // Turn autofill off. + ["browser.urlbar.autoFill", false], + ], + }); + + let engine = await SearchTestUtils.promiseNewSearchEngine( + getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME + ); + let oldDefaultEngine = await Services.search.getDefault(); + await Services.search.setDefault(engine); + + registerCleanupFunction(async () => { + await Services.search.setDefault(oldDefaultEngine); + }); + + // Move the mouse away from the results panel, because hovering a result may + // change its aspect (e.g. by showing a " - search with Engine" suffix). + await EventUtils.synthesizeNativeMouseMove(gURLBar.inputField); +}); + +add_task(async function test_tab_switch_result() { + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "about:mozilla" + ); + + await BrowserTestUtils.withNewTab({ gBrowser }, async () => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "about:mozilla", + fireInputEvent: true, + }); + + const details = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + + assertElementsDisplayed(details, { + separator: true, + title: "about:mozilla", + type: UrlbarUtils.RESULT_TYPE.TAB_SWITCH, + }); + }); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_search_result() { + Services.prefs.setBoolPref("browser.urlbar.suggest.searches", true); + + await BrowserTestUtils.withNewTab({ gBrowser }, async () => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "foo", + fireInputEvent: true, + }); + + let index = await UrlbarTestUtils.promiseSuggestionsPresent(window); + const details = await UrlbarTestUtils.getDetailsOfResultAt(window, index); + + // We'll initially display no separator. + assertElementsDisplayed(details, { + separator: false, + title: "foofoo", + type: UrlbarUtils.RESULT_TYPE.SEARCH, + }); + + // Down to select the first search suggestion. + for (let i = index; i > 0; --i) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + } + + // We should now be displaying one. + assertElementsDisplayed(details, { + separator: true, + title: "foofoo", + type: UrlbarUtils.RESULT_TYPE.SEARCH, + }); + }); + + await PlacesUtils.history.clear(); + Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false); +}); + +add_task(async function test_url_result() { + await PlacesTestUtils.addVisits([ + { + uri: "http://example.com", + title: "example", + transition: Ci.nsINavHistoryService.TRANSITION_TYPED, + }, + ]); + + await BrowserTestUtils.withNewTab({ gBrowser }, async () => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "example", + fireInputEvent: true, + }); + + const details = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + + assertElementsDisplayed(details, { + separator: true, + title: "example", + type: UrlbarUtils.RESULT_TYPE.URL, + }); + }); + + await PlacesUtils.history.clear(); +}); + +add_task(async function test_keyword_result() { + const TEST_URL = `${TEST_BASE_URL}print_postdata.sjs`; + + await PlacesUtils.keywords.insert({ + keyword: "get", + url: TEST_URL + "?q=%s", + }); + + await BrowserTestUtils.withNewTab({ gBrowser }, async () => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "get ", + fireInputEvent: true, + }); + + let details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + + // Because only the keyword is typed, we show the bookmark url. + assertElementsDisplayed(details, { + separator: true, + title: TEST_URL + "?q=", + type: UrlbarUtils.RESULT_TYPE.KEYWORD, + }); + + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "get test", + fireInputEvent: true, + }); + + details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + + assertElementsDisplayed(details, { + separator: false, + title: "example.com: test", + type: UrlbarUtils.RESULT_TYPE.KEYWORD, + }); + }); +}); + +add_task(async function test_omnibox_result() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + omnibox: { + keyword: "omniboxtest", + }, + + background() { + /* global browser */ + browser.omnibox.setDefaultSuggestion({ + description: "doit", + }); + // Just do nothing for this test. + browser.omnibox.onInputEntered.addListener(() => {}); + browser.omnibox.onInputChanged.addListener((text, suggest) => { + suggest([]); + }); + }, + }, + }); + + await extension.startup(); + + await BrowserTestUtils.withNewTab({ gBrowser }, async () => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "omniboxtest ", + fireInputEvent: true, + }); + + const details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + + assertElementsDisplayed(details, { + separator: true, + title: "Generated extension", + type: UrlbarUtils.RESULT_TYPE.OMNIBOX, + }); + }); + + await extension.unload(); +}); + +add_task(async function test_remote_tab_result() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["services.sync.username", "fake"], + ["services.sync.syncedTabs.showRemoteTabs", true], + ], + }); + // Clear history so that history added by previous tests doesn't mess up this + // test when it selects results in the urlbar. + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + + const REMOTE_TAB = { + id: "7cqCr77ptzX3", + type: "client", + lastModified: 1492201200, + name: "zcarter's Nightly on MacBook-Pro-25", + clientType: "desktop", + tabs: [ + { + type: "tab", + title: "Test Remote", + url: "http://example.com", + icon: UrlbarUtils.ICON.DEFAULT, + client: "7cqCr77ptzX3", + lastUsed: parseInt(Date.now() / 1000), + }, + ], + }; + + const sandbox = sinon.createSandbox(); + + let originalSyncedTabsInternal = SyncedTabs._internal; + SyncedTabs._internal = { + isConfiguredToSyncTabs: true, + hasSyncedThisSession: true, + getTabClients() { + return Promise.resolve([]); + }, + syncTabs() { + return Promise.resolve(); + }, + }; + + // Tell the Sync XPCOM service it is initialized. + let weaveXPCService = Cc["@mozilla.org/weave/service;1"].getService( + Ci.nsISupports + ).wrappedJSObject; + let oldWeaveServiceReady = weaveXPCService.ready; + weaveXPCService.ready = true; + + sandbox + .stub(SyncedTabs._internal, "getTabClients") + .callsFake(() => Promise.resolve(Cu.cloneInto([REMOTE_TAB], {}))); + + // Reset internal cache in PlacesRemoteTabsAutocompleteProvider. + Services.obs.notifyObservers(null, "weave:engine:sync:finish", "tabs"); + + registerCleanupFunction(async function() { + sandbox.restore(); + weaveXPCService.ready = oldWeaveServiceReady; + SyncedTabs._internal = originalSyncedTabsInternal; + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + Services.telemetry.setEventRecordingEnabled("navigation", false); + }); + + await BrowserTestUtils.withNewTab({ gBrowser }, async () => { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "example", + fireInputEvent: true, + }); + + const details = await UrlbarTestUtils.getDetailsOfResultAt(window, 1); + + assertElementsDisplayed(details, { + separator: true, + title: "Test Remote", + type: UrlbarUtils.RESULT_TYPE.REMOTE_TAB, + }); + }); + await SpecialPowers.popPrefEnv(); +}); diff --git a/browser/components/urlbar/tests/browser/browser_waitForLoadOrTimeout.js b/browser/components/urlbar/tests/browser/browser_waitForLoadOrTimeout.js new file mode 100644 index 0000000000..352e37b9d0 --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_waitForLoadOrTimeout.js @@ -0,0 +1,37 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests the waitForLoadOrTimeout test helper function in head.js. + */ + +"use strict"; + +add_task(async function load() { + await BrowserTestUtils.withNewTab("about:blank", async () => { + let url = "http://example.com/"; + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: url, + }); + + let loadPromise = waitForLoadOrTimeout(); + EventUtils.synthesizeKey("KEY_Enter"); + let loadEvent = await loadPromise; + + Assert.ok(loadEvent, "Page should have loaded before timeout"); + Assert.equal( + loadEvent.target.currentURI.spec, + url, + "example.com should have loaded" + ); + }); +}); + +add_task(async function timeout() { + let loadEvent = await waitForLoadOrTimeout(); + Assert.ok( + !loadEvent, + "No page should have loaded, and timeout should have fired" + ); +}); diff --git a/browser/components/urlbar/tests/browser/browser_whereToOpen.js b/browser/components/urlbar/tests/browser/browser_whereToOpen.js new file mode 100644 index 0000000000..a2d0d907df --- /dev/null +++ b/browser/components/urlbar/tests/browser/browser_whereToOpen.js @@ -0,0 +1,194 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const NON_EMPTY_TAB = "example.com/non-empty"; +const EMPTY_TAB = "about:blank"; +const META_KEY = AppConstants.platform == "macosx" ? "metaKey" : "ctrlKey"; +const ENTER = new KeyboardEvent("keydown", {}); +const ALT_ENTER = new KeyboardEvent("keydown", { altKey: true }); +const ALTGR_ENTER = new KeyboardEvent("keydown", { modifierAltGraph: true }); +const CLICK = new MouseEvent("click", { button: 0 }); +const META_CLICK = new MouseEvent("click", { button: 0, [META_KEY]: true }); +const MIDDLE_CLICK = new MouseEvent("click", { button: 1 }); + +let old_openintab = Preferences.get("browser.urlbar.openintab"); +registerCleanupFunction(async function() { + Preferences.set("browser.urlbar.openintab", old_openintab); +}); + +add_task(async function openInTab() { + // Open a non-empty tab. + let tab = (gBrowser.selectedTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + NON_EMPTY_TAB + )); + + for (let test of [ + { + pref: false, + event: ALT_ENTER, + desc: "Alt+Enter, non-empty tab, default prefs", + }, + { + pref: false, + event: ALTGR_ENTER, + desc: "AltGr+Enter, non-empty tab, default prefs", + }, + { + pref: false, + event: META_CLICK, + desc: "Meta+click, non-empty tab, default prefs", + }, + { + pref: false, + event: MIDDLE_CLICK, + desc: "Middle click, non-empty tab, default prefs", + }, + { pref: true, event: ENTER, desc: "Enter, non-empty tab, openInTab" }, + { + pref: true, + event: CLICK, + desc: "Normal click, non-empty tab, openInTab", + }, + ]) { + info(test.desc); + + Preferences.set("browser.urlbar.openintab", test.pref); + let where = gURLBar._whereToOpen(test.event); + is(where, "tab", "URL would be loaded in a new tab"); + } + + // Clean up. + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function keepEmptyTab() { + // Open an empty tab. + let tab = (gBrowser.selectedTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + EMPTY_TAB + )); + + for (let test of [ + { + pref: false, + event: META_CLICK, + desc: "Meta+click, empty tab, default prefs", + }, + { + pref: false, + event: MIDDLE_CLICK, + desc: "Middle click, empty tab, default prefs", + }, + ]) { + info(test.desc); + + Preferences.set("browser.urlbar.openintab", test.pref); + let where = gURLBar._whereToOpen(test.event); + is(where, "tab", "URL would be loaded in a new tab"); + } + + // Clean up. + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function reuseEmptyTab() { + // Open an empty tab. + let tab = (gBrowser.selectedTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + EMPTY_TAB + )); + + for (let test of [ + { + pref: false, + event: ALT_ENTER, + desc: "Alt+Enter, empty tab, default prefs", + }, + { + pref: false, + event: ALTGR_ENTER, + desc: "AltGr+Enter, empty tab, default prefs", + }, + { pref: true, event: ENTER, desc: "Enter, empty tab, openInTab" }, + { pref: true, event: CLICK, desc: "Normal click, empty tab, openInTab" }, + ]) { + info(test.desc); + Preferences.set("browser.urlbar.openintab", test.pref); + let where = gURLBar._whereToOpen(test.event); + is(where, "current", "New URL would reuse the current empty tab"); + } + + // Clean up. + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function openInCurrentTab() { + for (let test of [ + { + pref: false, + url: NON_EMPTY_TAB, + event: ENTER, + desc: "Enter, non-empty tab, default prefs", + }, + { + pref: false, + url: NON_EMPTY_TAB, + event: CLICK, + desc: "Normal click, non-empty tab, default prefs", + }, + { + pref: false, + url: EMPTY_TAB, + event: ENTER, + desc: "Enter, empty tab, default prefs", + }, + { + pref: false, + url: EMPTY_TAB, + event: CLICK, + desc: "Normal click, empty tab, default prefs", + }, + { + pref: true, + url: NON_EMPTY_TAB, + event: ALT_ENTER, + desc: "Alt+Enter, non-empty tab, openInTab", + }, + { + pref: true, + url: NON_EMPTY_TAB, + event: ALTGR_ENTER, + desc: "AltGr+Enter, non-empty tab, openInTab", + }, + { + pref: true, + url: NON_EMPTY_TAB, + event: META_CLICK, + desc: "Meta+click, non-empty tab, openInTab", + }, + { + pref: true, + url: NON_EMPTY_TAB, + event: MIDDLE_CLICK, + desc: "Middle click, non-empty tab, openInTab", + }, + ]) { + info(test.desc); + + // Open a new tab. + let tab = (gBrowser.selectedTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + test.url + )); + + Preferences.set("browser.urlbar.openintab", test.pref); + let where = gURLBar._whereToOpen(test.event); + is(where, "current", "URL would open in the current tab"); + + // Clean up. + BrowserTestUtils.removeTab(tab); + } +}); diff --git a/browser/components/urlbar/tests/browser/dummy_page.html b/browser/components/urlbar/tests/browser/dummy_page.html new file mode 100644 index 0000000000..1a87e28408 --- /dev/null +++ b/browser/components/urlbar/tests/browser/dummy_page.html @@ -0,0 +1,9 @@ +<html> +<head> +<title>Dummy test page</title> +<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta> +</head> +<body> +<p>Dummy test page</p> +</body> +</html> diff --git a/browser/components/urlbar/tests/browser/dynamicResult0.css b/browser/components/urlbar/tests/browser/dynamicResult0.css new file mode 100644 index 0000000000..328127b594 --- /dev/null +++ b/browser/components/urlbar/tests/browser/dynamicResult0.css @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +#urlbar { + --testDynamicResult0: ok0; +} + +.urlbarView-row[dynamicType=test] > .urlbarView-row-inner { + display: flex; + align-items: center; + min-height: 32px; + width: 100%; +} + +.urlbarView-dynamic-test-buttonBox { + display: flex; + align-items: center; + min-height: 32px; +} + +.urlbarView-dynamic-test-text { + flex-grow: 1; + flex-shrink: 1; + padding: 10px; +} + +.urlbarView-dynamic-test-selectable, +.urlbarView-dynamic-test-button1, +.urlbarView-dynamic-test-button2 { + min-height: 16px; + padding: 8px; + border: none; + border-radius: 2px; + font-size: 0.93em; + color: inherit; + background-color: var(--urlbarView-button-background); + min-width: 8.75em; + text-align: center; + flex-basis: initial; + flex-shrink: 0; + margin-inline-end: 10px; +} + +.urlbarView-dynamic-test-selectable[selected], +.urlbarView-dynamic-test-button1[selected], +.urlbarView-dynamic-test-button2[selected] { + color: white; + background-color: var(--urlbarView-primary-button-background); + box-shadow: 0 0 0 1px #0a84ff inset, 0 0 0 1px #0a84ff, 0 0 0 4px rgba(10, 132, 255, 0.3); +} diff --git a/browser/components/urlbar/tests/browser/dynamicResult1.css b/browser/components/urlbar/tests/browser/dynamicResult1.css new file mode 100644 index 0000000000..ae43fd3f9a --- /dev/null +++ b/browser/components/urlbar/tests/browser/dynamicResult1.css @@ -0,0 +1,50 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +#urlbar { + --testDynamicResult1: ok1; +} + +.urlbarView-row[dynamicType=test] > .urlbarView-row-inner { + display: flex; + align-items: center; + min-height: 32px; + width: 100%; +} + +.urlbarView-dynamic-test-buttonBox { + display: flex; + align-items: center; + min-height: 32px; +} + +.urlbarView-dynamic-test-text { + flex-grow: 1; + flex-shrink: 1; + padding: 10px; +} + +.urlbarView-dynamic-test-selectable, +.urlbarView-dynamic-test-button1, +.urlbarView-dynamic-test-button2 { + min-height: 16px; + padding: 8px; + border: none; + border-radius: 2px; + font-size: 0.93em; + color: inherit; + background-color: var(--urlbarView-button-background); + min-width: 8.75em; + text-align: center; + flex-basis: initial; + flex-shrink: 0; + margin-inline-end: 10px; +} + +.urlbarView-dynamic-test-selectable[selected], +.urlbarView-dynamic-test-button1[selected], +.urlbarView-dynamic-test-button2[selected] { + color: white; + background-color: var(--urlbarView-primary-button-background); + box-shadow: 0 0 0 1px #0a84ff inset, 0 0 0 1px #0a84ff, 0 0 0 4px rgba(10, 132, 255, 0.3); +} diff --git a/browser/components/urlbar/tests/browser/file_blank_but_not_blank.html b/browser/components/urlbar/tests/browser/file_blank_but_not_blank.html new file mode 100644 index 0000000000..1f5fea8dcf --- /dev/null +++ b/browser/components/urlbar/tests/browser/file_blank_but_not_blank.html @@ -0,0 +1,2 @@ +<script>var q = "1";</script> +<a href="javascript:q">Click me</a> diff --git a/browser/components/urlbar/tests/browser/file_urlbar_edit_dos.html b/browser/components/urlbar/tests/browser/file_urlbar_edit_dos.html new file mode 100644 index 0000000000..e02242f6a1 --- /dev/null +++ b/browser/components/urlbar/tests/browser/file_urlbar_edit_dos.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html> +<head> +<title>Try editing the URL bar</title> +<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta> +</head> +<body> +<script> +function dos_hash() { + location.hash = "#"; +} + +function dos_pushState() { + history.pushState({}, "Some title", ""); +} +</script> +</body> +</html> diff --git a/browser/components/urlbar/tests/browser/file_userTypedValue.html b/browser/components/urlbar/tests/browser/file_userTypedValue.html new file mode 100644 index 0000000000..a787b70898 --- /dev/null +++ b/browser/components/urlbar/tests/browser/file_userTypedValue.html @@ -0,0 +1 @@ +<html><body>bug562649</body></html> diff --git a/browser/components/urlbar/tests/browser/head-common.js b/browser/components/urlbar/tests/browser/head-common.js new file mode 100644 index 0000000000..e03b45851c --- /dev/null +++ b/browser/components/urlbar/tests/browser/head-common.js @@ -0,0 +1,85 @@ +/* eslint-env mozilla/frame-script */ + +var { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); + +XPCOMUtils.defineLazyModuleGetters(this, { + HttpServer: "resource://testing-common/httpd.js", + PlacesUtils: "resource://gre/modules/PlacesUtils.jsm", + PlacesTestUtils: "resource://testing-common/PlacesTestUtils.jsm", + Preferences: "resource://gre/modules/Preferences.jsm", + UrlbarProvider: "resource:///modules/UrlbarUtils.jsm", + UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.jsm", + UrlbarResult: "resource:///modules/UrlbarResult.jsm", + UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.jsm", + UrlbarUtils: "resource:///modules/UrlbarUtils.jsm", +}); + +XPCOMUtils.defineLazyGetter(this, "TEST_BASE_URL", () => + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) +); + +XPCOMUtils.defineLazyServiceGetter( + this, + "clipboardHelper", + "@mozilla.org/widget/clipboardhelper;1", + "nsIClipboardHelper" +); + +XPCOMUtils.defineLazyGetter(this, "UrlbarTestUtils", () => { + const { UrlbarTestUtils: module } = ChromeUtils.import( + "resource://testing-common/UrlbarTestUtils.jsm" + ); + module.init(this); + return module; +}); + +XPCOMUtils.defineLazyGetter(this, "SearchTestUtils", () => { + const { SearchTestUtils: module } = ChromeUtils.import( + "resource://testing-common/SearchTestUtils.jsm" + ); + module.init(this); + return module; +}); + +/** + * Initializes an HTTP Server, and runs a task with it. + * @param {object} details {scheme, host, port} + * @param {function} taskFn The task to run, gets the server as argument. + */ +async function withHttpServer( + details = { scheme: "http", host: "localhost", port: -1 }, + taskFn +) { + let server = new HttpServer(); + let url = `${details.scheme}://${details.host}:${details.port}`; + try { + info(`starting HTTP Server for ${url}`); + try { + server.start(details.port); + details.port = server.identity.primaryPort; + server.identity.setPrimary(details.scheme, details.host, details.port); + } catch (ex) { + throw new Error("We can't launch our http server successfully. " + ex); + } + Assert.ok( + server.identity.has(details.scheme, details.host, details.port), + `${url} is listening.` + ); + try { + await taskFn(server); + } catch (ex) { + throw new Error("Exception in the task function " + ex); + } + } finally { + server.identity.remove(details.scheme, details.host, details.port); + try { + await new Promise(resolve => server.stop(resolve)); + } catch (ex) {} + server = null; + } +} diff --git a/browser/components/urlbar/tests/browser/head.js b/browser/components/urlbar/tests/browser/head.js new file mode 100644 index 0000000000..f8e0527240 --- /dev/null +++ b/browser/components/urlbar/tests/browser/head.js @@ -0,0 +1,114 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * These tests unit test the result/url loading functionality of UrlbarController. + */ + +"use strict"; + +let sandbox; + +var { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); +XPCOMUtils.defineLazyModuleGetters(this, { + AboutNewTab: "resource:///modules/AboutNewTab.jsm", + AppConstants: "resource://gre/modules/AppConstants.jsm", + ObjectUtils: "resource://gre/modules/ObjectUtils.jsm", + PromiseUtils: "resource://gre/modules/PromiseUtils.jsm", + ResetProfile: "resource://gre/modules/ResetProfile.jsm", + TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.jsm", + UrlbarController: "resource:///modules/UrlbarController.jsm", + UrlbarQueryContext: "resource:///modules/UrlbarUtils.jsm", + UrlbarResult: "resource:///modules/UrlbarResult.jsm", + UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.jsm", + UrlbarUtils: "resource:///modules/UrlbarUtils.jsm", + UrlbarView: "resource:///modules/UrlbarView.jsm", +}); + +/* import-globals-from head-common.js */ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/components/urlbar/tests/browser/head-common.js", + this +); + +const { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm"); + +registerCleanupFunction(async () => { + // Ensure the Urlbar popup is always closed at the end of a test, to save having + // to do it within each test. + await UrlbarTestUtils.promisePopupClose(window); +}); + +async function selectAndPaste(str, win = window) { + await SimpleTest.promiseClipboardChange(str, () => { + clipboardHelper.copyString(str); + }); + win.gURLBar.select(); + win.document.commandDispatcher + .getControllerForCommand("cmd_paste") + .doCommand("cmd_paste"); +} + +/** + * Updates the Top Sites feed. + * + * @param {function} condition + * A callback that returns true after Top Sites are successfully updated. + * @param {boolean} searchShortcuts + * True if Top Sites search shortcuts should be enabled. + */ +async function updateTopSites(condition, searchShortcuts = false) { + // Toggle the pref to clear the feed cache and force an update. + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.newtabpage.activity-stream.feeds.system.topsites", false], + ["browser.newtabpage.activity-stream.feeds.system.topsites", true], + [ + "browser.newtabpage.activity-stream.improvesearch.topSiteSearchShortcuts", + searchShortcuts, + ], + ], + }); + + // Wait for the feed to be updated. + await TestUtils.waitForCondition(() => { + let sites = AboutNewTab.getTopSites(); + return condition(sites); + }, "Waiting for top sites to be updated"); +} + +/** + * Waits for a load in any browser or a timeout, whichever comes first. + * + * @param {window} win + * The top-level browser window to listen in. + * @param {number} timeoutMs + * The timeout in ms. + * @returns {event|null} + * If a load event was detected before the timeout fired, then the event is + * returned. event.target will be the browser in which the load occurred. If + * the timeout fired before a load was detected, null is returned. + */ +async function waitForLoadOrTimeout(win = window, timeoutMs = 1000) { + let event; + let listener; + let timeout; + let eventName = "BrowserTestUtils:ContentEvent:load"; + try { + event = await Promise.race([ + new Promise(resolve => { + listener = resolve; + win.addEventListener(eventName, listener, true); + }), + new Promise(resolve => { + timeout = win.setTimeout(resolve, timeoutMs); + }), + ]); + } finally { + win.removeEventListener(eventName, listener, true); + win.clearTimeout(timeout); + } + return event || null; +} diff --git a/browser/components/urlbar/tests/browser/moz.png b/browser/components/urlbar/tests/browser/moz.png Binary files differnew file mode 100644 index 0000000000..769c636340 --- /dev/null +++ b/browser/components/urlbar/tests/browser/moz.png diff --git a/browser/components/urlbar/tests/browser/print_postdata.sjs b/browser/components/urlbar/tests/browser/print_postdata.sjs new file mode 100644 index 0000000000..4175a24805 --- /dev/null +++ b/browser/components/urlbar/tests/browser/print_postdata.sjs @@ -0,0 +1,22 @@ +const CC = Components.Constructor; +const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream"); + +function handleRequest(request, response) { + response.setHeader("Content-Type", "text/plain", false); + if (request.method == "GET") { + response.write(request.queryString); + } else { + var body = new BinaryInputStream(request.bodyInputStream); + + var avail; + var bytes = []; + + while ((avail = body.available()) > 0) + Array.prototype.push.apply(bytes, body.readByteArray(avail)); + + var data = String.fromCharCode.apply(null, bytes); + response.bodyOutputStream.write(data, data.length); + } +} diff --git a/browser/components/urlbar/tests/browser/redirect_error.sjs b/browser/components/urlbar/tests/browser/redirect_error.sjs new file mode 100644 index 0000000000..64c6f143b0 --- /dev/null +++ b/browser/components/urlbar/tests/browser/redirect_error.sjs @@ -0,0 +1,16 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const REDIRECT_TO = "https://www.bank1.com/"; // Bad-cert host. + +function handleRequest(aRequest, aResponse) { + // Set HTTP Status + aResponse.setStatusLine(aRequest.httpVersion, 301, "Moved Permanently"); + + // Set redirect URI, mirroring the hash value. + let hash = (/\#.+/.test(aRequest.path))? + "#" + aRequest.path.split("#")[1]: + ""; + aResponse.setHeader("Location", REDIRECT_TO + hash); +} diff --git a/browser/components/urlbar/tests/browser/searchSuggestionEngine.sjs b/browser/components/urlbar/tests/browser/searchSuggestionEngine.sjs new file mode 100644 index 0000000000..770474e745 --- /dev/null +++ b/browser/components/urlbar/tests/browser/searchSuggestionEngine.sjs @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +let gTimer; + +function handleRequest(req, resp) { + // Parse the query params. If the params aren't in the form "foo=bar", then + // treat the entire query string as a search string. + let params = req.queryString.split("&").reduce((memo, pair) => { + let [key, val] = pair.split("="); + if (!val) { + // This part isn't in the form "foo=bar". Treat it as the search string + // (the "query"). + val = key; + key = "query"; + } + memo[decode(key)] = decode(val); + return memo; + }, {}); + + let timeout = parseInt(params["timeout"]); + if (timeout) { + // Write the response after a timeout. + resp.processAsync(); + gTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + gTimer.init(() => { + writeResponse(params, resp); + resp.finish(); + }, timeout, Ci.nsITimer.TYPE_ONE_SHOT); + return; + } + + writeResponse(params, resp); +} + +function writeResponse(params, resp) { + // Echo back the search string with "foo" and "bar" appended. + let suffixes = ["foo", "bar"]; + if (params["count"]) { + // Add more suffixes. + let serial = 0; + while (suffixes.length < params["count"]) { + suffixes.push(++serial); + } + } + let data = [params["query"], suffixes.map(s => params["query"] + s)]; + resp.setHeader("Content-Type", "application/json", false); + resp.write(JSON.stringify(data)); +} + +function decode(str) { + return decodeURIComponent(str.replace(/\+/g, encodeURIComponent(" "))); +} diff --git a/browser/components/urlbar/tests/browser/searchSuggestionEngine.xml b/browser/components/urlbar/tests/browser/searchSuggestionEngine.xml new file mode 100644 index 0000000000..142c91849c --- /dev/null +++ b/browser/components/urlbar/tests/browser/searchSuggestionEngine.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> + +<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/"> +<ShortName>browser_searchSuggestionEngine searchSuggestionEngine.xml</ShortName> +<Url type="application/x-suggestions+json" method="GET" template="http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/searchSuggestionEngine.sjs?{searchTerms}"/> +<Url type="text/html" method="GET" template="http://mochi.test:8888/" rel="searchform"> + <Param name="terms" value="{searchTerms}"/> +</Url> +</SearchPlugin> diff --git a/browser/components/urlbar/tests/browser/searchSuggestionEngine2.xml b/browser/components/urlbar/tests/browser/searchSuggestionEngine2.xml new file mode 100644 index 0000000000..565aaf2bc0 --- /dev/null +++ b/browser/components/urlbar/tests/browser/searchSuggestionEngine2.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> + +<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/"> +<ShortName>browser_searchSuggestionEngine2 searchSuggestionEngine2.xml</ShortName> +<!-- Redirect the actual search request to the test-server because of proxy restriction --> +<Url type="application/x-suggestions+json" method="GET" template="http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/searchSuggestionEngine.sjs?{searchTerms}"/> +<!-- Redirect speculative connect to a local http server we run for this test --> +<Url type="text/html" method="GET" template="http://localhost:20709/" rel="searchform"> + <Param name="terms" value="{searchTerms}"/> +</Url> +</SearchPlugin> diff --git a/browser/components/urlbar/tests/browser/searchSuggestionEngineMany.xml b/browser/components/urlbar/tests/browser/searchSuggestionEngineMany.xml new file mode 100644 index 0000000000..7e77e32029 --- /dev/null +++ b/browser/components/urlbar/tests/browser/searchSuggestionEngineMany.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> + +<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/"> +<ShortName>browser_searchSuggestionEngineMany searchSuggestionEngineMany.xml</ShortName> +<Url type="application/x-suggestions+json" method="GET" template="http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/searchSuggestionEngine.sjs?{searchTerms}&count=10"/> +<Url type="text/html" method="GET" template="http://mochi.test:8888/" rel="searchform"> + <Param name="terms" value="{searchTerms}"/> +</Url> +</SearchPlugin> diff --git a/browser/components/urlbar/tests/browser/searchSuggestionEngineSlow.xml b/browser/components/urlbar/tests/browser/searchSuggestionEngineSlow.xml new file mode 100644 index 0000000000..e7214e65cc --- /dev/null +++ b/browser/components/urlbar/tests/browser/searchSuggestionEngineSlow.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> + +<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/"> +<ShortName>searchSuggestionEngineSlow.xml</ShortName> +<Url type="application/x-suggestions+json" method="GET" template="http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/searchSuggestionEngine.sjs?query={searchTerms}&timeout=3000"/> +<Url type="text/html" method="GET" template="http://mochi.test:8888/" rel="searchform"> + <Param name="terms" value="{searchTerms}"/> +</Url> +</SearchPlugin> diff --git a/browser/components/urlbar/tests/browser/slow-page.sjs b/browser/components/urlbar/tests/browser/slow-page.sjs new file mode 100644 index 0000000000..ce9a759744 --- /dev/null +++ b/browser/components/urlbar/tests/browser/slow-page.sjs @@ -0,0 +1,23 @@ +"use strict"; + +let timer; + +const DELAY_MS = 5000; +function handleRequest(request, response) { + if (request.queryString.endsWith("faster")) { + response.setHeader("Content-Type", "text/html", false); + response.write("<body>Not so slow!</body>"); + return; + } + response.processAsync(); + timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.init( + () => { + response.setHeader("Content-Type", "text/html", false); + response.write("<body>This is a slow loading page.</body>"); + response.finish(); + }, + DELAY_MS, + Ci.nsITimer.TYPE_ONE_SHOT + ); +} diff --git a/browser/components/urlbar/tests/browser/urlbarTelemetrySearchSuggestions.sjs b/browser/components/urlbar/tests/browser/urlbarTelemetrySearchSuggestions.sjs new file mode 100644 index 0000000000..1978b4f665 --- /dev/null +++ b/browser/components/urlbar/tests/browser/urlbarTelemetrySearchSuggestions.sjs @@ -0,0 +1,9 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +function handleRequest(req, resp) { + let suffixes = ["foo", "bar"]; + let data = [req.queryString, suffixes.map(s => req.queryString + s)]; + resp.setHeader("Content-Type", "application/json", false); + resp.write(JSON.stringify(data)); +} diff --git a/browser/components/urlbar/tests/browser/urlbarTelemetrySearchSuggestions.xml b/browser/components/urlbar/tests/browser/urlbarTelemetrySearchSuggestions.xml new file mode 100644 index 0000000000..8ed4fef6f1 --- /dev/null +++ b/browser/components/urlbar/tests/browser/urlbarTelemetrySearchSuggestions.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/"> +<ShortName>browser_urlbar_telemetry urlbarTelemetrySearchSuggestions.xml</ShortName> +<Url type="application/x-suggestions+json" method="GET" template="http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/urlbarTelemetrySearchSuggestions.sjs?{searchTerms}"/> +<Url type="text/html" method="GET" template="http://example.com" rel="searchform"/> +</SearchPlugin> diff --git a/browser/components/urlbar/tests/browser/urlbarTelemetryUrlbarDynamic.css b/browser/components/urlbar/tests/browser/urlbarTelemetryUrlbarDynamic.css new file mode 100644 index 0000000000..e81052522f --- /dev/null +++ b/browser/components/urlbar/tests/browser/urlbarTelemetryUrlbarDynamic.css @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +.urlbarView-row[dynamicType=test] > .urlbarView-row-inner { + display: flex; + align-items: center; + min-height: 32px; + width: 100%; +} + +.urlbarView-dynamic-test-button { + min-height: 16px; + padding: 8px; + border: none; + border-radius: 2px; + font-size: 0.93em; + color: inherit; + background-color: var(--urlbarView-button-background); + min-width: 8.75em; + text-align: center; + flex-basis: initial; + flex-shrink: 0; +} + +.urlbarView-dynamic-test-button[selected] { + color: white; + background-color: var(--urlbarView-primary-button-background); + box-shadow: 0 0 0 1px #0a84ff inset, 0 0 0 1px #0a84ff, 0 0 0 4px rgba(10, 132, 255, 0.3); +} + +.urlbarView-dynamic-test-button:hover { + color: white; + background-color: var(--urlbarView-primary-button-background-hover); +} + +.urlbarView-dynamic-test-button:active { + color: white; + background-color: var(--urlbarView-primary-button-background-active); +} + +.urlbarView-dynamic-test-buttonSpacer { + flex-basis: 48px; + flex-grow: 1; + flex-shrink: 1; +} diff --git a/browser/components/urlbar/tests/ext/api.js b/browser/components/urlbar/tests/ext/api.js new file mode 100644 index 0000000000..44f4802db1 --- /dev/null +++ b/browser/components/urlbar/tests/ext/api.js @@ -0,0 +1,256 @@ +/* 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/. */ + +/* global ExtensionAPI, ExtensionCommon */ + +"use strict"; + +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); + +XPCOMUtils.defineLazyModuleGetters(this, { + BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm", + Preferences: "resource://gre/modules/Preferences.jsm", + UrlbarProviderExtension: "resource:///modules/UrlbarProviderExtension.jsm", + UrlbarResult: "resource:///modules/UrlbarResult.jsm", + UrlbarView: "resource:///modules/UrlbarView.jsm", +}); + +XPCOMUtils.defineLazyGetter( + this, + "defaultPreferences", + () => new Preferences({ defaultBranch: true }) +); + +let { EventManager } = ExtensionCommon; + +this.experiments_urlbar = class extends ExtensionAPI { + getAPI(context) { + return { + experiments: { + urlbar: { + addDynamicResultType: (name, type) => { + this._addDynamicResultType(name, type); + }, + + addDynamicViewTemplate: (name, viewTemplate) => { + this._addDynamicViewTemplate(name, viewTemplate); + }, + + attributionURL: this._getDefaultSettingsAPI( + "browser.partnerlink.attributionURL" + ), + + clearInput() { + let window = BrowserWindowTracker.getTopWindow(); + window.gURLBar.value = ""; + window.gURLBar.setPageProxyState("invalid"); + }, + + engagementTelemetry: this._getDefaultSettingsAPI( + "browser.urlbar.eventTelemetry.enabled" + ), + + extensionTimeout: this._getDefaultSettingsAPI( + "browser.urlbar.extension.timeout" + ), + + onViewUpdateRequested: new EventManager({ + context, + name: "experiments.urlbar.onViewUpdateRequested", + register: (fire, providerName) => { + let provider = UrlbarProviderExtension.getOrCreate(providerName); + provider.setEventListener( + "getViewUpdate", + (result, idsByName) => { + return fire.async(result.payload, idsByName).catch(error => { + throw context.normalizeError(error); + }); + } + ); + return () => provider.setEventListener("getViewUpdate", null); + }, + }).api(), + }, + }, + }; + } + + onShutdown() { + // Reset the default prefs. This is necessary because + // ExtensionPreferencesManager doesn't properly reset prefs set on the + // default branch. See bug 1586543, bug 1578513, bug 1578508. + if (this._initialDefaultPrefs) { + for (let [pref, value] of this._initialDefaultPrefs.entries()) { + defaultPreferences.set(pref, value); + } + } + + this._removeDynamicViewTemplates(); + this._removeDynamicResultTypes(); + } + + _getDefaultSettingsAPI(pref) { + return { + get: details => { + return { + value: Preferences.get(pref), + + // Nothing actually uses this, but on debug builds there are extra + // checks enabled in Schema.jsm that fail if it's not present. The + // value doesn't matter. + levelOfControl: "controllable_by_this_extension", + }; + }, + set: details => { + if (!this._initialDefaultPrefs) { + this._initialDefaultPrefs = new Map(); + } + if (!this._initialDefaultPrefs.has(pref)) { + this._initialDefaultPrefs.set(pref, defaultPreferences.get(pref)); + } + defaultPreferences.set(pref, details.value); + return true; + }, + clear: details => { + if (this._initialDefaultPrefs && this._initialDefaultPrefs.has(pref)) { + defaultPreferences.set(pref, this._initialDefaultPrefs.get(pref)); + return true; + } + return false; + }, + }; + } + + // We use the following four properties as bookkeeping to keep track of + // dynamic result types and view templates registered by extensions so that + // they can be properly removed on extension shutdown. + + // Names of dynamic result types added by this extension. + _dynamicResultTypeNames = new Set(); + + // Names of dynamic result type view templates added by this extension. + _dynamicViewTemplateNames = new Set(); + + // Maps dynamic result type names to Sets of IDs of extensions that have + // registered those types. + static extIDsByDynamicResultTypeName = new Map(); + + // Maps dynamic result type view template names to Sets of IDs of extensions + // that have registered those view templates. + static extIDsByDynamicViewTemplateName = new Map(); + + /** + * Adds a dynamic result type and includes it in our bookkeeping. See + * UrlbarResult.addDynamicResultType(). + * + * @param {string} name + * The name of the dynamic result type. + * @param {object} type + * The type. + */ + _addDynamicResultType(name, type) { + this._dynamicResultTypeNames.add(name); + this._addExtIDToDynamicResultTypeMap( + experiments_urlbar.extIDsByDynamicResultTypeName, + name + ); + UrlbarResult.addDynamicResultType(name, type); + } + + /** + * Removes all dynamic result types added by the extension. + */ + _removeDynamicResultTypes() { + for (let name of this._dynamicResultTypeNames) { + let allRemoved = this._removeExtIDFromDynamicResultTypeMap( + experiments_urlbar.extIDsByDynamicResultTypeName, + name + ); + if (allRemoved) { + UrlbarResult.removeDynamicResultType(name); + } + } + } + + /** + * Adds a dynamic result type view template and includes it in our + * bookkeeping. See UrlbarView.addDynamicViewTemplate(). + * + * @param {string} name + * The view template will be registered for the dynamic result type with + * this name. + * @param {object} viewTemplate + * The view template. + */ + _addDynamicViewTemplate(name, viewTemplate) { + this._dynamicViewTemplateNames.add(name); + this._addExtIDToDynamicResultTypeMap( + experiments_urlbar.extIDsByDynamicViewTemplateName, + name + ); + if (viewTemplate.stylesheet) { + viewTemplate.stylesheet = this.extension.baseURI.resolve( + viewTemplate.stylesheet + ); + } + UrlbarView.addDynamicViewTemplate(name, viewTemplate); + } + + /** + * Removes all dynamic result type view templates added by the extension. + */ + _removeDynamicViewTemplates() { + for (let name of this._dynamicViewTemplateNames) { + let allRemoved = this._removeExtIDFromDynamicResultTypeMap( + experiments_urlbar.extIDsByDynamicViewTemplateName, + name + ); + if (allRemoved) { + UrlbarView.removeDynamicViewTemplate(name); + } + } + } + + /** + * Adds a dynamic result type name and this extension's ID to a bookkeeping + * map. + * + * @param {Map} map + * Either extIDsByDynamicResultTypeName or extIDsByDynamicViewTemplateName. + * @param {string} dynamicTypeName + * The dynamic result type name. + */ + _addExtIDToDynamicResultTypeMap(map, dynamicTypeName) { + let extIDs = map.get(dynamicTypeName); + if (!extIDs) { + extIDs = new Set(); + map.set(dynamicTypeName, extIDs); + } + extIDs.add(this.extension.id); + } + + /** + * Removes a dynamic result type name and this extension's ID from a + * bookkeeping map. + * + * @param {Map} map + * Either extIDsByDynamicResultTypeName or extIDsByDynamicViewTemplateName. + * @param {string} dynamicTypeName + * The dynamic result type name. + * @returns {boolean} + * True if no other extension IDs are in the map under the same + * dynamicTypeName, and false otherwise. + */ + _removeExtIDFromDynamicResultTypeMap(map, dynamicTypeName) { + let extIDs = map.get(dynamicTypeName); + extIDs.delete(this.extension.id); + if (!extIDs.size) { + map.delete(dynamicTypeName); + return true; + } + return false; + } +}; diff --git a/browser/components/urlbar/tests/ext/browser/.eslintrc.js b/browser/components/urlbar/tests/ext/browser/.eslintrc.js new file mode 100644 index 0000000000..e57058ecb1 --- /dev/null +++ b/browser/components/urlbar/tests/ext/browser/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + env: { + webextensions: true, + }, +}; diff --git a/browser/components/urlbar/tests/ext/browser/browser.ini b/browser/components/urlbar/tests/ext/browser/browser.ini new file mode 100644 index 0000000000..416fc52eb3 --- /dev/null +++ b/browser/components/urlbar/tests/ext/browser/browser.ini @@ -0,0 +1,18 @@ +# 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/. + +[DEFAULT] +support-files = + ../../browser/head-common.js + ../api.js + ../schema.json + head.js + +[browser_ext_urlbar_attributionURL.js] +[browser_ext_urlbar_clearInput.js] +[browser_ext_urlbar_dynamicResult.js] +support-files = + dynamicResult.css +[browser_ext_urlbar_engagementTelemetry.js] +[browser_ext_urlbar_extensionTimeout.js] diff --git a/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_attributionURL.js b/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_attributionURL.js new file mode 100644 index 0000000000..a5bccc8eba --- /dev/null +++ b/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_attributionURL.js @@ -0,0 +1,16 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* global browser */ + +// This tests the browser.experiments.urlbar.engagementTelemetry WebExtension +// Experiment API. + +"use strict"; + +add_settings_tasks("browser.partnerlink.attributionURL", "string", () => { + browser.test.onMessage.addListener(async (method, arg) => { + let result = await browser.experiments.urlbar.attributionURL[method](arg); + browser.test.sendMessage("done", result); + }); +}); diff --git a/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_clearInput.js b/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_clearInput.js new file mode 100644 index 0000000000..afeff3b8a1 --- /dev/null +++ b/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_clearInput.js @@ -0,0 +1,31 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* global browser */ + +// This tests the browser.experiments.urlbar.clearInput WebExtension Experiment +// API. + +"use strict"; + +add_task(async function test() { + // Load a page so that pageproxystate is valid. When the extension calls + // clearInput, the pageproxystate should become invalid. + await BrowserTestUtils.withNewTab("http://example.com/", async () => { + Assert.notEqual(gURLBar.value, "", "Input is not empty"); + Assert.equal(gURLBar.getAttribute("pageproxystate"), "valid"); + + let ext = await loadExtension({ + background: async () => { + await browser.experiments.urlbar.clearInput(); + browser.test.sendMessage("done"); + }, + }); + await ext.awaitMessage("done"); + + Assert.equal(gURLBar.value, "", "Input is empty"); + Assert.equal(gURLBar.getAttribute("pageproxystate"), "invalid"); + + await ext.unload(); + }); +}); diff --git a/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_dynamicResult.js b/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_dynamicResult.js new file mode 100644 index 0000000000..a710d8949d --- /dev/null +++ b/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_dynamicResult.js @@ -0,0 +1,137 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* global browser */ + +// This tests dynamic results using the WebExtension Experiment API. + +"use strict"; + +add_task(async function test() { + let ext = await loadExtension({ + extraFiles: { + "dynamicResult.css": await ( + await fetch("file://" + getTestFilePath("dynamicResult.css")) + ).text(), + }, + background: async () => { + browser.experiments.urlbar.addDynamicResultType("testDynamicType"); + browser.experiments.urlbar.addDynamicViewTemplate("testDynamicType", { + stylesheet: "dynamicResult.css", + children: [ + { + name: "text", + tag: "span", + }, + { + name: "button", + tag: "span", + attributes: { + role: "button", + }, + }, + ], + }); + browser.urlbar.onBehaviorRequested.addListener(query => { + return "restricting"; + }, "test"); + browser.urlbar.onResultsRequested.addListener(query => { + return [ + { + type: "dynamic", + source: "local", + heuristic: true, + payload: { + dynamicType: "testDynamicType", + }, + }, + ]; + }, "test"); + browser.experiments.urlbar.onViewUpdateRequested.addListener(payload => { + return { + text: { + textContent: "This is a dynamic result.", + }, + button: { + textContent: "Click Me", + }, + }; + }, "test"); + browser.urlbar.onResultPicked.addListener((payload, elementName) => { + browser.test.sendMessage("onResultPicked", [payload, elementName]); + }, "test"); + }, + }); + + // Wait for the provider and dynamic type to be registered before continuing. + await TestUtils.waitForCondition( + () => + UrlbarProvidersManager.getProvider("test") && + UrlbarResult.getDynamicResultType("testDynamicType"), + "Waiting for provider and dynamic type to be registered" + ); + Assert.ok( + UrlbarProvidersManager.getProvider("test"), + "Provider should be registered" + ); + Assert.ok( + UrlbarResult.getDynamicResultType("testDynamicType"), + "Dynamic type should be registered" + ); + + // Do a search. + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: "test", + waitForFocus: SimpleTest.waitForFocus, + }); + + // Get the row. + let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 0); + Assert.equal( + row.result.type, + UrlbarUtils.RESULT_TYPE.DYNAMIC, + "row.result.type" + ); + Assert.equal( + row.getAttribute("dynamicType"), + "testDynamicType", + "row[dynamicType]" + ); + + let text = row.querySelector(".urlbarView-dynamic-testDynamicType-text"); + + // The view's call to provider.getViewUpdate is async, so we need to make sure + // the update has been applied before continuing to avoid intermittent + // failures. + await TestUtils.waitForCondition( + () => text.textContent == "This is a dynamic result." + ); + + // Check the elements. + Assert.equal( + text.textContent, + "This is a dynamic result.", + "text.textContent" + ); + let button = row.querySelector(".urlbarView-dynamic-testDynamicType-button"); + Assert.equal(button.textContent, "Click Me", "button.textContent"); + + // The result's button should be selected since the result is the heuristic. + Assert.equal( + UrlbarTestUtils.getSelectedElement(window), + button, + "Button should be selected" + ); + + // Pick the button. + let pickPromise = ext.awaitMessage("onResultPicked"); + await UrlbarTestUtils.promisePopupClose(window, () => + EventUtils.synthesizeKey("KEY_Enter") + ); + let [payload, elementName] = await pickPromise; + Assert.equal(payload.dynamicType, "testDynamicType", "Picked payload"); + Assert.equal(elementName, "button", "Picked element name"); + + await ext.unload(); +}); diff --git a/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_engagementTelemetry.js b/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_engagementTelemetry.js new file mode 100644 index 0000000000..50ded14d4e --- /dev/null +++ b/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_engagementTelemetry.js @@ -0,0 +1,18 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* global browser */ + +// This tests the browser.experiments.urlbar.engagementTelemetry WebExtension +// Experiment API. + +"use strict"; + +add_settings_tasks("browser.urlbar.eventTelemetry.enabled", "boolean", () => { + browser.test.onMessage.addListener(async (method, arg) => { + let result = await browser.experiments.urlbar.engagementTelemetry[method]( + arg + ); + browser.test.sendMessage("done", result); + }); +}); diff --git a/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_extensionTimeout.js b/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_extensionTimeout.js new file mode 100644 index 0000000000..de09ef263c --- /dev/null +++ b/browser/components/urlbar/tests/ext/browser/browser_ext_urlbar_extensionTimeout.js @@ -0,0 +1,16 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* global browser */ + +// This tests the browser.experiments.urlbar.engagementTelemetry WebExtension +// Experiment API. + +"use strict"; + +add_settings_tasks("browser.urlbar.extension.timeout", "number", () => { + browser.test.onMessage.addListener(async (method, arg) => { + let result = await browser.experiments.urlbar.extensionTimeout[method](arg); + browser.test.sendMessage("done", result); + }); +}); diff --git a/browser/components/urlbar/tests/ext/browser/dynamicResult.css b/browser/components/urlbar/tests/ext/browser/dynamicResult.css new file mode 100644 index 0000000000..efd0c8c950 --- /dev/null +++ b/browser/components/urlbar/tests/ext/browser/dynamicResult.css @@ -0,0 +1,36 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +.urlbarView-row[dynamicType=testDynamicType] > .urlbarView-row-inner { + display: flex; + align-items: center; + min-height: 32px; + width: 100%; +} + +.urlbarView-dynamic-testDynamicType-text { + flex-grow: 1; + flex-shrink: 1; + padding: 10px; +} + +.urlbarView-dynamic-testDynamicType-button { + min-height: 16px; + padding: 8px; + border: none; + border-radius: 2px; + font-size: 0.93em; + color: inherit; + background-color: var(--urlbarView-button-background); + min-width: 8.75em; + text-align: center; + flex-basis: initial; + flex-shrink: 0; + margin-inline-end: 10px; +} + +.urlbarView-dynamic-testDynamicType-button[selected] { + color: white; + background-color: var(--urlbarView-primary-button-background); + box-shadow: 0 0 0 1px #0a84ff inset, 0 0 0 1px #0a84ff, 0 0 0 4px rgba(10, 132, 255, 0.3); +} diff --git a/browser/components/urlbar/tests/ext/browser/head.js b/browser/components/urlbar/tests/ext/browser/head.js new file mode 100644 index 0000000000..e5b2d7631d --- /dev/null +++ b/browser/components/urlbar/tests/ext/browser/head.js @@ -0,0 +1,251 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * The files in this directory test the browser.urlbarExperiments WebExtension + * Experiment APIs, which are the WebExtension APIs we ship in our urlbar + * experiment extensions. Unlike the WebExtension APIs we ship in mozilla- + * central, which have continuous test coverage [1], our WebExtension Experiment + * APIs would not have continuous test coverage were it not for the fact that we + * copy and test them here. This is especially useful for APIs that are used in + * experiments that target multiple versions of Firefox, and for APIs that are + * reused in multiple experiments. See [2] and [3] for more info on + * experiments. + * + * [1] See browser/components/extensions/test + * [2] browser/components/urlbar/docs/experiments.rst + * [3] https://firefox-source-docs.mozilla.org/toolkit/components/extensions/webextensions/basics.html#webextensions-experiments + */ + +"use strict"; + +/* import-globals-from ../../browser/head-common.js */ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/components/urlbar/tests/ext/browser/head-common.js", + this +); + +XPCOMUtils.defineLazyModuleGetters(this, { + Preferences: "resource://gre/modules/Preferences.jsm", +}); + +const SCHEMA_BASENAME = "schema.json"; +const SCRIPT_BASENAME = "api.js"; + +const SCHEMA_PATH = getTestFilePath(SCHEMA_BASENAME); +const SCRIPT_PATH = getTestFilePath(SCRIPT_BASENAME); + +let schemaSource; +let scriptSource; + +add_task(async function loadSource() { + schemaSource = await (await fetch("file://" + SCHEMA_PATH)).text(); + scriptSource = await (await fetch("file://" + SCRIPT_PATH)).text(); +}); + +/** + * Loads a mock extension with our browser.experiments.urlbar API and a + * background script. Be sure to call `await ext.unload()` when you're done + * with it. + * + * @param {function} background + * This function is serialized and becomes the background script. + * @param {object} extraFiles + * Extra files to load in the extension. + * @returns {object} + * The extension. + */ +async function loadExtension({ background, extraFiles = {} }) { + let ext = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["urlbar"], + experiment_apis: { + experiments_urlbar: { + schema: SCHEMA_BASENAME, + parent: { + scopes: ["addon_parent"], + paths: [["experiments", "urlbar"]], + script: SCRIPT_BASENAME, + }, + }, + }, + }, + files: { + [SCHEMA_BASENAME]: schemaSource, + [SCRIPT_BASENAME]: scriptSource, + ...extraFiles, + }, + isPrivileged: true, + background, + }); + await ext.startup(); + return ext; +} + +/** + * Tests toggling a preference value via an experiments.urlbar API. + * + * @param {string} prefName + * The name of the pref to be tested. + * @param {string} type + * The type of the pref being set. One of "string", "boolean", or "number". + * @param {function} background + * Boilerplate function that returns the value from calling the + * browser.experiments.urlbar.prefName[method] APIs. + */ +function add_settings_tasks(prefName, type, background) { + let defaultPreferences = new Preferences({ defaultBranch: true }); + + let originalValue = defaultPreferences.get(prefName); + registerCleanupFunction(() => { + defaultPreferences.set(prefName, originalValue); + }); + + let firstValue, secondValue; + switch (type) { + case "string": + firstValue = "test value 1"; + secondValue = "test value 2"; + break; + case "number": + firstValue = 10; + secondValue = 100; + break; + case "boolean": + firstValue = false; + secondValue = true; + break; + default: + Assert.fail( + `"type" parameter must be one of "string", "number", or "boolean"` + ); + } + + add_task(async function get() { + let ext = await loadExtension({ background }); + + defaultPreferences.set(prefName, firstValue); + ext.sendMessage("get", {}); + let result = await ext.awaitMessage("done"); + Assert.strictEqual(result.value, firstValue); + + defaultPreferences.set(prefName, secondValue); + ext.sendMessage("get", {}); + result = await ext.awaitMessage("done"); + Assert.strictEqual(result.value, secondValue); + + await ext.unload(); + }); + + add_task(async function set() { + let ext = await loadExtension({ background }); + + defaultPreferences.set(prefName, firstValue); + ext.sendMessage("set", { value: secondValue }); + let result = await ext.awaitMessage("done"); + Assert.strictEqual(result, true); + Assert.strictEqual(defaultPreferences.get(prefName), secondValue); + + ext.sendMessage("set", { value: firstValue }); + result = await ext.awaitMessage("done"); + Assert.strictEqual(result, true); + Assert.strictEqual(defaultPreferences.get(prefName), firstValue); + + await ext.unload(); + }); + + add_task(async function clear() { + // no set() + defaultPreferences.set(prefName, firstValue); + let ext = await loadExtension({ background }); + ext.sendMessage("clear", {}); + let result = await ext.awaitMessage("done"); + Assert.strictEqual(result, false); + Assert.strictEqual(defaultPreferences.get(prefName), firstValue); + await ext.unload(); + + // firstValue -> secondValue + defaultPreferences.set(prefName, firstValue); + ext = await loadExtension({ background }); + ext.sendMessage("set", { value: secondValue }); + await ext.awaitMessage("done"); + ext.sendMessage("clear", {}); + result = await ext.awaitMessage("done"); + Assert.strictEqual(result, true); + Assert.strictEqual(defaultPreferences.get(prefName), firstValue); + await ext.unload(); + + // secondValue -> firstValue + defaultPreferences.set(prefName, secondValue); + ext = await loadExtension({ background }); + ext.sendMessage("set", { value: firstValue }); + await ext.awaitMessage("done"); + ext.sendMessage("clear", {}); + result = await ext.awaitMessage("done"); + Assert.strictEqual(result, true); + Assert.strictEqual(defaultPreferences.get(prefName), secondValue); + await ext.unload(); + + // firstValue -> firstValue + defaultPreferences.set(prefName, firstValue); + ext = await loadExtension({ background }); + ext.sendMessage("set", { value: firstValue }); + await ext.awaitMessage("done"); + ext.sendMessage("clear", {}); + result = await ext.awaitMessage("done"); + Assert.strictEqual(result, true); + Assert.strictEqual(defaultPreferences.get(prefName), firstValue); + await ext.unload(); + + // secondValue -> secondValue + defaultPreferences.set(prefName, secondValue); + ext = await loadExtension({ background }); + ext.sendMessage("set", { value: secondValue }); + await ext.awaitMessage("done"); + ext.sendMessage("clear", {}); + result = await ext.awaitMessage("done"); + Assert.strictEqual(result, true); + Assert.strictEqual(defaultPreferences.get(prefName), secondValue); + await ext.unload(); + }); + + add_task(async function shutdown() { + // no set() + defaultPreferences.set(prefName, firstValue); + let ext = await loadExtension({ background }); + await ext.unload(); + Assert.strictEqual(defaultPreferences.get(prefName), firstValue); + + // firstValue -> secondValue + defaultPreferences.set(prefName, firstValue); + ext = await loadExtension({ background }); + ext.sendMessage("set", { value: secondValue }); + await ext.awaitMessage("done"); + await ext.unload(); + Assert.strictEqual(defaultPreferences.get(prefName), firstValue); + + // secondValue -> firstValue + defaultPreferences.set(prefName, secondValue); + ext = await loadExtension({ background }); + ext.sendMessage("set", { value: firstValue }); + await ext.awaitMessage("done"); + await ext.unload(); + Assert.strictEqual(defaultPreferences.get(prefName), secondValue); + + // firstValue -> firstValue + defaultPreferences.set(prefName, firstValue); + ext = await loadExtension({ background }); + ext.sendMessage("set", { value: firstValue }); + await ext.awaitMessage("done"); + await ext.unload(); + Assert.strictEqual(defaultPreferences.get(prefName), firstValue); + + // secondValue -> secondValue + defaultPreferences.set(prefName, secondValue); + ext = await loadExtension({ background }); + ext.sendMessage("set", { value: secondValue }); + await ext.awaitMessage("done"); + await ext.unload(); + Assert.strictEqual(defaultPreferences.get(prefName), secondValue); + }); +} diff --git a/browser/components/urlbar/tests/ext/schema.json b/browser/components/urlbar/tests/ext/schema.json new file mode 100644 index 0000000000..26b0df5a34 --- /dev/null +++ b/browser/components/urlbar/tests/ext/schema.json @@ -0,0 +1,113 @@ +[ + { + "namespace": "experiments.urlbar", + "description": "APIs supporting urlbar experiments", + "types": [ + { + "id": "DynamicResultType", + "type": "object", + "description": "Describes a dynamic result type.", + "properties": { + "viewTemplate": { + "type": "object", + "description": "An object describing the type's view.", + "additionalProperties": true + } + } + } + ], + "properties": { + "attributionURL": { + "$ref": "types.Setting", + "description": "Gets or sets the attribution URL for the current browser session." + }, + "engagementTelemetry": { + "$ref": "types.Setting", + "description": "Enables or disables the engagement telemetry for the current browser session." + }, + "extensionTimeout": { + "$ref": "types.Setting", + "description": "Sets the amount of time in ms that extensions have to return results to the browser.urlbar API." + } + }, + "events": [ + { + "name": "onViewUpdateRequested", + "type": "function", + "description": "Fired when the urlbar view updates the view of one of the results of the provider.", + "parameters": [ + { + "name": "payload", + "type": "object", + "description": "The result's payload." + }, + { + "name": "idsByName", + "type": "object", + "description": "A Map from an element's name, as defined by the provider; to its ID in the DOM, as defined by the browser." + } + ], + "extraParameters": [ + { + "name": "providerName", + "type": "string", + "pattern": "^[a-zA-Z0-9_-]+$", + "description": "The name of the provider you want to provide updates for." + } + ], + "returns": { + "type": "object", + "description": "An object describing the view update." + } + } + ], + "functions": [ + { + "name": "addDynamicResultType", + "type": "function", + "async": true, + "description": "Adds a dynamic result type. See UrlbarResult.addDynamicResultType().", + "parameters": [ + { + "name": "name", + "type": "string", + "description": "The name of the result type." + }, + { + "name": "type", + "type": "object", + "default": {}, + "optional": true, + "description": "The result type. Currently this should be an empty object (which is the default value)." + } + ] + }, + { + "name": "addDynamicViewTemplate", + "type": "function", + "async": true, + "description": "Adds a view template for a dynamic result type. See UrlbarView.addDynamicViewTemplate().", + "parameters": [ + { + "name": "name", + "type": "string", + "description": "The view template will be registered for the dynamic result type with this name." + }, + { + "name": "viewTemplate", + "type": "object", + "additionalProperties": true, + "description": "The view template." + } + ] + }, + { + "name": "clearInput", + "type": "function", + "async": true, + "description": "Sets urlbar.value to the empty string and the pageproxystate to invalid.", + "parameters": [] + } + ] + } +] diff --git a/browser/components/urlbar/tests/unit/data/engine-suggestions.xml b/browser/components/urlbar/tests/unit/data/engine-suggestions.xml new file mode 100644 index 0000000000..2f7a6f7b09 --- /dev/null +++ b/browser/components/urlbar/tests/unit/data/engine-suggestions.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> + +<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/"> +<ShortName>engine-suggestions.xml</ShortName> +<Url type="application/x-suggestions+json" + method="GET" + template="http://localhost:9000/suggest?{searchTerms}"/> +<Url type="text/html" + method="GET" + template="http://localhost:9000/search" + rel="searchform"> + <Param name="terms" value="{searchTerms}"/> +</Url> +</SearchPlugin> diff --git a/browser/components/urlbar/tests/unit/data/engine-tail-suggestions.xml b/browser/components/urlbar/tests/unit/data/engine-tail-suggestions.xml new file mode 100644 index 0000000000..65f208884d --- /dev/null +++ b/browser/components/urlbar/tests/unit/data/engine-tail-suggestions.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> + +<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/"> +<ShortName>engine-tail-suggestions.xml</ShortName> +<Url type="application/x-suggestions+json" + method="GET" + template="http://localhost:9001/suggest?{searchTerms}"/> +<Url type="text/html" + method="GET" + template="http://localhost:9001/search" + rel="searchform"/> +</SearchPlugin> diff --git a/browser/components/urlbar/tests/unit/head.js b/browser/components/urlbar/tests/unit/head.js new file mode 100644 index 0000000000..04c5e42eb9 --- /dev/null +++ b/browser/components/urlbar/tests/unit/head.js @@ -0,0 +1,946 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); + +var { + UrlbarMuxer, + UrlbarProvider, + UrlbarQueryContext, + UrlbarUtils, +} = ChromeUtils.import("resource:///modules/UrlbarUtils.jsm"); +XPCOMUtils.defineLazyModuleGetters(this, { + AddonTestUtils: "resource://testing-common/AddonTestUtils.jsm", + AppConstants: "resource://gre/modules/AppConstants.jsm", + HttpServer: "resource://testing-common/httpd.js", + PlacesTestUtils: "resource://testing-common/PlacesTestUtils.jsm", + PlacesUtils: "resource://gre/modules/PlacesUtils.jsm", + PromiseUtils: "resource://gre/modules/PromiseUtils.jsm", + Services: "resource://gre/modules/Services.jsm", + TestUtils: "resource://testing-common/TestUtils.jsm", + UrlbarController: "resource:///modules/UrlbarController.jsm", + UrlbarInput: "resource:///modules/UrlbarInput.jsm", + UrlbarPrefs: "resource:///modules/UrlbarPrefs.jsm", + UrlbarProviderOpenTabs: "resource:///modules/UrlbarProviderOpenTabs.jsm", + UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.jsm", + UrlbarResult: "resource:///modules/UrlbarResult.jsm", + UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.jsm", + UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.jsm", +}); +const { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm"); + +AddonTestUtils.init(this, false); +AddonTestUtils.createAppInfo( + "xpcshell@tests.mozilla.org", + "XPCShell", + "42", + "42" +); + +add_task(async function initXPCShellDependencies() { + await UrlbarTestUtils.initXPCShellDependencies(); +}); + +/** + * Gets the database connection. If the Places connection is invalid it will + * try to create a new connection. + * + * @param [optional] aForceNewConnection + * Forces creation of a new connection to the database. When a + * connection is asyncClosed it cannot anymore schedule async statements, + * though connectionReady will keep returning true (Bug 726990). + * + * @return The database connection or null if unable to get one. + */ +var gDBConn; +function DBConn(aForceNewConnection) { + if (!aForceNewConnection) { + let db = PlacesUtils.history.DBConnection; + if (db.connectionReady) { + return db; + } + } + + // If the Places database connection has been closed, create a new connection. + if (!gDBConn || aForceNewConnection) { + let file = Services.dirsvc.get("ProfD", Ci.nsIFile); + file.append("places.sqlite"); + let dbConn = (gDBConn = Services.storage.openDatabase(file)); + + TestUtils.topicObserved("profile-before-change").then(() => + dbConn.asyncClose() + ); + } + + return gDBConn.connectionReady ? gDBConn : null; +} + +/** + * @param {string} searchString The search string to insert into the context. + * @param {object} properties Overrides for the default values. + * @returns {UrlbarQueryContext} Creates a dummy query context with pre-filled + * required options. + */ +function createContext(searchString = "foo", properties = {}) { + info(`Creating new queryContext with searchString: ${searchString}`); + let context = new UrlbarQueryContext( + Object.assign( + { + allowAutofill: UrlbarPrefs.get("autoFill"), + isPrivate: true, + maxResults: UrlbarPrefs.get("maxRichResults"), + searchString, + }, + properties + ) + ); + UrlbarTokenizer.tokenize(context); + return context; +} + +/** + * Waits for the given notification from the supplied controller. + * + * @param {UrlbarController} controller The controller to wait for a response from. + * @param {string} notification The name of the notification to wait for. + * @param {boolean} expected Wether the notification is expected. + * @returns {Promise} A promise that is resolved with the arguments supplied to + * the notification. + */ +function promiseControllerNotification( + controller, + notification, + expected = true +) { + return new Promise((resolve, reject) => { + let proxifiedObserver = new Proxy( + {}, + { + get: (target, name) => { + if (name == notification) { + return (...args) => { + controller.removeQueryListener(proxifiedObserver); + if (expected) { + resolve(args); + } else { + reject(); + } + }; + } + return () => false; + }, + } + ); + controller.addQueryListener(proxifiedObserver); + }); +} + +/** + * A basic test provider, returning all the provided matches. + */ +class TestProvider extends UrlbarTestUtils.TestProvider { + isActive(context) { + Assert.ok(context, "context is passed-in"); + return true; + } + getPriority(context) { + Assert.ok(context, "context is passed-in"); + return 0; + } + async startQuery(context, add) { + Assert.ok(context, "context is passed-in"); + Assert.equal(typeof add, "function", "add is a callback"); + this._context = context; + for (const result of this._results) { + add(this, result); + } + } + cancelQuery(context) { + // If the query was created but didn't run, this_context will be undefined. + if (this._context) { + Assert.equal(this._context, context, "cancelQuery: context is the same"); + } + if (this._onCancel) { + this._onCancel(); + } + } +} + +function convertToUtf8(str) { + return String.fromCharCode(...new TextEncoder().encode(str)); +} + +/** + * Helper function to clear the existing providers and register a basic provider + * that returns only the results given. + * + * @param {array} results The results for the provider to return. + * @param {function} [onCancel] Optional, called when the query provider + * receives a cancel instruction. + * @param {UrlbarUtils.PROVIDER_TYPE} type The provider type. + * @returns {string} name of the registered provider + */ +function registerBasicTestProvider(results = [], onCancel, type) { + let provider = new TestProvider({ results, onCancel, type }); + UrlbarProvidersManager.registerProvider(provider); + return provider.name; +} + +// Creates an HTTP server for the test. +function makeTestServer(port = -1) { + let httpServer = new HttpServer(); + httpServer.start(port); + registerCleanupFunction(() => httpServer.stop(() => {})); + return httpServer; +} + +/** + * Adds a search engine to the Search Service. + * + * @param {string} basename + * Basename for the engine. + * @param {object} httpServer [optional] HTTP Server to use. + * @returns {Promise} Resolved once the addition is complete. + */ +async function addTestEngine(basename, httpServer = undefined) { + httpServer = httpServer || makeTestServer(); + httpServer.registerDirectory("/", do_get_cwd()); + let dataUrl = + "http://localhost:" + httpServer.identity.primaryPort + "/data/"; + + // 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)); + + 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; +} + +/** + * Sets up a search engine that provides some tail suggestions by creating an + * array that mimics Google's tail suggestion responses. + * + * @param {function} suggestionsFn + * A function that returns an array that mimics Google's tail suggestion + * responses. See bug 1626897. + * NOTE: Consumers specifying suggestionsFn must include searchStr as a + * part of the array returned by suggestionsFn. + * @returns {nsISearchEngine} The new engine. + */ +async function addTestTailSuggestionsEngine(suggestionsFn = null) { + // This port number should match the number in engine-tail-suggestions.xml. + let server = makeTestServer(9001); + 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) + : [ + "what time is it in t", + ["what is the time today texas"].concat( + ["toronto", "tunisia"].map(s => searchStr + s.slice(1)) + ), + [], + { + "google:irrelevantparameter": [], + "google:suggestdetail": [{}].concat( + ["toronto", "tunisia"].map(s => ({ + mp: "… ", + t: s, + })) + ), + }, + ]; + let data = suggestions; + let jsonString = JSON.stringify(data); + // This script must be evaluated as UTF-8 for this to write out the bytes of + // the string in UTF-8. If it's evaluated as Latin-1, the written bytes + // will be the result of UTF-8-encoding the result-string *twice*, which + // will break the "… " match prefixes. + let stringOfUtf8Bytes = convertToUtf8(jsonString); + resp.setHeader("Content-Type", "application/json", false); + resp.write(stringOfUtf8Bytes); + }); + let engine = await addTestEngine("engine-tail-suggestions.xml", server); + return engine; +} + +/** + * Helper for tests that generate search results but aren't interested in + * suggestions, such as autofill tests. Installs a test engine and disables + * suggestions. + */ +function testEngine_setup() { + add_task(async function setup() { + await cleanupPlaces(); + let engine = await addTestSuggestionsEngine(); + let oldDefaultEngine = await Services.search.getDefault(); + + registerCleanupFunction(async () => { + Services.prefs.clearUserPref("browser.urlbar.suggest.searches"); + Services.prefs.clearUserPref( + "browser.search.separatePrivateDefault.ui.enabled" + ); + Services.search.setDefault(oldDefaultEngine); + }); + + Services.search.setDefault(engine); + Services.prefs.setBoolPref( + "browser.search.separatePrivateDefault.ui.enabled", + false + ); + Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false); + }); +} + +async function cleanupPlaces() { + Services.prefs.clearUserPref("browser.urlbar.autoFill"); + Services.prefs.clearUserPref("browser.urlbar.autoFill.searchEngines"); + + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); +} + +/** + * Returns the frecency of a url. + * + * @param {string} aURI The URI or spec to get frecency for. + * @returns {number} the frecency value. + */ +function frecencyForUrl(aURI) { + let url = aURI; + if (aURI instanceof Ci.nsIURI) { + url = aURI.spec; + } else if (aURI instanceof URL) { + url = aURI.href; + } + let stmt = DBConn().createStatement( + "SELECT frecency FROM moz_places WHERE url_hash = hash(?1) AND url = ?1" + ); + stmt.bindByIndex(0, url); + try { + if (!stmt.executeStep()) { + throw new Error("No result for frecency."); + } + return stmt.getInt32(0); + } finally { + stmt.finalize(); + } +} + +/** + * Creates a UrlbarResult for a bookmark result. + * @param {UrlbarQueryContext} queryContext + * The context that this result will be displayed in. + * @param {string} options.title + * The page title. + * @param {string} options.uri + * The page URI. + * @param {string} [options.iconUri] + * A URI for the page's icon. + * @param {array} [options.tags] + * An array of string tags. Defaults to an empty array. + * @param {boolean} [options.heuristic] + * True if this is a heuristic result. Defaults to false. + * @returns {UrlbarResult} + */ +function makeBookmarkResult( + queryContext, + { + title, + uri, + iconUri, + tags = [], + heuristic = false, + source = UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + } +) { + let result = new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + source, + ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, { + url: [uri, UrlbarUtils.HIGHLIGHT.TYPED], + // Check against undefined so consumers can pass in the empty string. + icon: [typeof iconUri != "undefined" ? iconUri : `page-icon:${uri}`], + title: [title, UrlbarUtils.HIGHLIGHT.TYPED], + tags: [tags, UrlbarUtils.HIGHLIGHT.TYPED], + }) + ); + + result.heuristic = heuristic; + return result; +} + +/** + * Creates a UrlbarResult for a form history result. + * @param {UrlbarQueryContext} queryContext + * The context that this result will be displayed in. + * @param {string} options.suggestion + * The form history suggestion. + * @param {string} options.engineName + * The name of the engine that will do the search when the result is picked. + * @returns {UrlbarResult} + */ +function makeFormHistoryResult(queryContext, { suggestion, engineName }) { + return new UrlbarResult( + UrlbarUtils.RESULT_TYPE.SEARCH, + UrlbarUtils.RESULT_SOURCE.HISTORY, + ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, { + engine: engineName, + suggestion: [suggestion, UrlbarUtils.HIGHLIGHT.SUGGESTED], + lowerCaseSuggestion: suggestion.toLocaleLowerCase(), + }) + ); +} + +/** + * Creates a UrlbarResult for an omnibox extension result. For more information, + * see the documentation for omnibox.SuggestResult: + * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/omnibox/SuggestResult + * @param {UrlbarQueryContext} queryContext + * The context that this result will be displayed in. + * @param {string} options.content + * The string displayed when the result is highlighted. + * @param {string} options.description + * The string displayed in the address bar dropdown. + * @param {string} options.keyword + * The keyword associated with the extension returning the result. + * @param {boolean} [options.heuristic] + * True if this is a heuristic result. Defaults to false. + * @returns {UrlbarResult} + */ +function makeOmniboxResult( + queryContext, + { content, description, keyword, heuristic = false } +) { + let result = new UrlbarResult( + UrlbarUtils.RESULT_TYPE.OMNIBOX, + UrlbarUtils.RESULT_SOURCE.OTHER_NETWORK, + ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, { + title: [description, UrlbarUtils.HIGHLIGHT.TYPED], + content: [content, UrlbarUtils.HIGHLIGHT.TYPED], + keyword: [keyword, UrlbarUtils.HIGHLIGHT.TYPED], + icon: [UrlbarUtils.ICON.EXTENSION], + }) + ); + result.heuristic = heuristic; + return result; +} + +/** + * Creates a UrlbarResult for a keyword search result. + * @param {UrlbarQueryContext} queryContext + * The context that this result will be displayed in. + * @param {string} options.uri + * The page URI. + * @param {string} options.keyword + * The page's search keyword. + * @param {string} [options.title] + * The title for the bookmarked keyword page. + * @param {string} [options.iconUri] + * A URI for the engine's icon. + * @param {string} [options.postData] + * The search POST data. + * @param {boolean} [options.heuristic] + * True if this is a heuristic result. Defaults to false. + * @returns {UrlbarResult} + */ +function makeKeywordSearchResult( + queryContext, + { uri, keyword, title, iconUri, postData, heuristic = false } +) { + let result = new UrlbarResult( + UrlbarUtils.RESULT_TYPE.KEYWORD, + UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, { + title: [title ? title : uri, UrlbarUtils.HIGHLIGHT.TYPED], + url: [uri, UrlbarUtils.HIGHLIGHT.TYPED], + keyword: [keyword, UrlbarUtils.HIGHLIGHT.TYPED], + input: [queryContext.searchString, UrlbarUtils.HIGHLIGHT.TYPED], + postData, + icon: typeof iconUri != "undefined" ? iconUri : `page-icon:${uri}`, + }) + ); + + if (heuristic) { + result.heuristic = heuristic; + } + return result; +} + +/** + * Creates a UrlbarResult for a priority search result. + * @param {UrlbarQueryContext} queryContext + * The context that this result will be displayed in. + * @param {string} [options.engineName] + * The name of the engine providing the suggestion. Leave blank if there + * is no suggestion. + * @param {string} [options.engineIconUri] + * A URI for the engine's icon. + * @param {boolean} [options.heuristic] + * True if this is a heuristic result. Defaults to false. + * @returns {UrlbarResult} + */ +function makePrioritySearchResult( + queryContext, + { engineName, engineIconUri, heuristic } +) { + let result = new UrlbarResult( + UrlbarUtils.RESULT_TYPE.SEARCH, + UrlbarUtils.RESULT_SOURCE.SEARCH, + ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, { + engine: [engineName, UrlbarUtils.HIGHLIGHT.TYPED], + icon: engineIconUri, + }) + ); + + if (heuristic) { + result.heuristic = heuristic; + } + return result; +} + +/** + * Creates a UrlbarResult for a search result. + * @param {UrlbarQueryContext} queryContext + * The context that this result will be displayed in. + * @param {string} [options.suggestion] + * The suggestion offered by the search engine. + * @param {string} [options.engineName] + * The name of the engine providing the suggestion. Leave blank if there + * is no suggestion. + * @param {string} [options.uri] + * The URI that the search result will navigate to. + * @param {string} [options.query] + * The query that started the search. This overrides + * `queryContext.searchString`. This is useful when the query that will show + * up in the result object will be different from what was typed. For example, + * if a leading restriction token will be used. + * @param {string} [options.alias] + * The alias for the search engine, if the search is an alias search. + * @param {string} [options.engineIconUri] + * A URI for the engine's icon. + * @param {boolean} [options.heuristic] + * True if this is a heuristic result. Defaults to false. + * @param {boolean} [options.providesSearchMode] + * Whether search mode is entered when this result is selected. + * @param {string} [options.providerName] + * The name of the provider offering this result. The test suite will not + * check which provider offered a result unless this option is specified. + * @returns {UrlbarResult} + */ +function makeSearchResult( + queryContext, + { + suggestion, + tailPrefix, + tail, + tailOffsetIndex, + engineName, + alias, + uri, + query, + engineIconUri, + providesSearchMode, + providerName, + inPrivateWindow, + isPrivateEngine, + heuristic = false, + type = UrlbarUtils.RESULT_TYPE.SEARCH, + source = UrlbarUtils.RESULT_SOURCE.SEARCH, + satisfiesAutofillThreshold = false, + } +) { + // Tail suggestion common cases, handled here to reduce verbosity in tests. + if (tail) { + if (!tailPrefix) { + tailPrefix = "… "; + } + if (!tailOffsetIndex) { + tailOffsetIndex = suggestion.indexOf(tail); + } + } + + let payload = { + engine: [engineName, UrlbarUtils.HIGHLIGHT.TYPED], + suggestion: [suggestion, UrlbarUtils.HIGHLIGHT.SUGGESTED], + tailPrefix, + tail: [tail, UrlbarUtils.HIGHLIGHT.SUGGESTED], + tailOffsetIndex, + keyword: [ + alias, + providesSearchMode + ? UrlbarUtils.HIGHLIGHT.TYPED + : UrlbarUtils.HIGHLIGHT.NONE, + ], + // Check against undefined so consumers can pass in the empty string. + query: [ + typeof query != "undefined" ? query : queryContext.trimmedSearchString, + UrlbarUtils.HIGHLIGHT.TYPED, + ], + icon: engineIconUri, + providesSearchMode, + inPrivateWindow, + isPrivateEngine, + }; + + // Passing even an undefined URL in the payload creates a potentially-unwanted + // displayUrl parameter, so we add it only if specified. + if (uri) { + payload.url = uri; + } + if (providerName == "TabToSearch") { + payload.satisfiesAutofillThreshold = satisfiesAutofillThreshold; + if (payload.url.startsWith("www.")) { + payload.url = payload.url.substring(4); + } + } + + let result = new UrlbarResult( + type, + source, + ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, payload) + ); + + if (typeof suggestion == "string") { + result.payload.lowerCaseSuggestion = result.payload.suggestion.toLocaleLowerCase(); + } + + if (providerName) { + result.providerName = providerName; + } + + result.heuristic = heuristic; + return result; +} + +/** + * Creates a UrlbarResult for a history result. + * @param {UrlbarQueryContext} queryContext + * The context that this result will be displayed in. + * @param {string} options.title + * The page title. + * @param {string} options.uri + * The page URI. + * @param {array} [options.tags] + * An array of string tags. Defaults to an empty array. + * @param {string} [options.iconUri] + * A URI for the page's icon. + * @param {boolean} [options.heuristic] + * True if this is a heuristic result. Defaults to false. + * * @param {string} providerName + * The name of the provider offering this result. The test suite will not + * check which provider offered a result unless this option is specified. + * @returns {UrlbarResult} + */ +function makeVisitResult( + queryContext, + { + title, + uri, + iconUri, + providerName, + tags = null, + heuristic = false, + source = UrlbarUtils.RESULT_SOURCE.HISTORY, + } +) { + let payload = { + url: [uri, UrlbarUtils.HIGHLIGHT.TYPED], + title: [title, UrlbarUtils.HIGHLIGHT.TYPED], + }; + + if (iconUri) { + payload.icon = iconUri; + } else if ( + iconUri === undefined && + source != UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL + ) { + payload.icon = `page-icon:${uri}`; + } + + if (!heuristic || tags) { + payload.tags = [tags || [], UrlbarUtils.HIGHLIGHT.TYPED]; + } + + let result = new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + source, + ...UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, payload) + ); + + if (providerName) { + result.providerName = providerName; + } + + result.heuristic = heuristic; + return result; +} + +/** + * Checks that the results returned by a UrlbarController match those in + * the param `matches`. + * @param {UrlbarQueryContext} context + * The context for this query. + * @param {string} [incompleteSearch] + * A search will be fired for this string and then be immediately canceled by + * the query in `context`. + * @param {string} [autofilled] + * The autofilled value in the first result. + * @param {string} [completed] + * The value that would be filled if the autofill result was confirmed. + * Has no effect if `autofilled` is not specified. + * @param {array} matches + * An array of UrlbarResults. + * @param {boolean} [isPrivate] + * Set this to `true` to simulate a search in a private window. + */ +async function check_results({ + context, + incompleteSearch, + autofilled, + completed, + matches = [], +} = {}) { + if (!context) { + return; + } + + // 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(); + + let controller = UrlbarTestUtils.newMockController({ + input: { + isPrivate: context.isPrivate, + onFirstResult() { + return false; + }, + window: { + location: { + href: AppConstants.BROWSER_CHROME_URL, + }, + }, + }, + }); + + if (incompleteSearch) { + let incompleteContext = createContext(incompleteSearch, { + isPrivate: context.isPrivate, + }); + controller.startQuery(incompleteContext); + } + await controller.startQuery(context); + + if (autofilled) { + Assert.ok(context.results[0], "There is a first result."); + Assert.ok( + context.results[0].autofill, + "The first result is an autofill result" + ); + Assert.equal( + context.results[0].autofill.value, + autofilled, + "The correct value was autofilled." + ); + if (completed) { + Assert.equal( + context.results[0].payload.url, + completed, + "The completed autofill value is correct." + ); + } + } + if (context.results.length != matches.length) { + info("Actual results: " + JSON.stringify(context.results)); + } + Assert.equal( + context.results.length, + matches.length, + "Found the expected number of results." + ); + + function getPayload(result) { + let payload = {}; + for (let [key, value] of Object.entries(result.payload)) { + if (value !== undefined) { + payload[key] = value; + } + } + return payload; + } + + for (let i = 0; i < matches.length; i++) { + let actual = context.results[i]; + let expected = matches[i]; + info( + `Comparing results at index ${i}:` + + " actual=" + + JSON.stringify(actual) + + " expected=" + + JSON.stringify(expected) + ); + Assert.equal( + actual.type, + expected.type, + `result.type at result index ${i}` + ); + Assert.equal( + actual.source, + expected.source, + `result.source at result index ${i}` + ); + Assert.equal( + actual.heuristic, + expected.heuristic, + `result.heuristic at result index ${i}` + ); + if (expected.providerName) { + Assert.equal( + actual.providerName, + expected.providerName, + `result.providerName at result index ${i}` + ); + } + Assert.deepEqual( + getPayload(actual), + getPayload(expected), + `result.payload at result index ${i}` + ); + } +} + +/** + * Returns the frecency of an origin. + * + * @param {string} prefix + * The origin's prefix, e.g., "http://". + * @param {string} aHost + * The origin's host. + * @returns {number} The origin's frecency. + */ +async function getOriginFrecency(prefix, aHost) { + let db = await PlacesUtils.promiseDBConnection(); + let rows = await db.execute( + ` + SELECT frecency + FROM moz_origins + WHERE prefix = :prefix AND host = :host + `, + { prefix, host: aHost } + ); + Assert.equal(rows.length, 1); + return rows[0].getResultByIndex(0); +} + +/** + * Returns the origin frecency stats. + * + * @returns {object} + * An object { count, sum, squares }. + */ +async function getOriginFrecencyStats() { + let db = await PlacesUtils.promiseDBConnection(); + let rows = await db.execute(` + SELECT + IFNULL((SELECT value FROM moz_meta WHERE key = 'origin_frecency_count'), 0), + IFNULL((SELECT value FROM moz_meta WHERE key = 'origin_frecency_sum'), 0), + IFNULL((SELECT value FROM moz_meta WHERE key = 'origin_frecency_sum_of_squares'), 0) + `); + let count = rows[0].getResultByIndex(0); + let sum = rows[0].getResultByIndex(1); + let squares = rows[0].getResultByIndex(2); + return { count, sum, squares }; +} + +/** + * Returns the origin autofill frecency threshold. + * + * @returns {number} + * The threshold. + */ +async function getOriginAutofillThreshold() { + let { count, sum, squares } = await getOriginFrecencyStats(); + if (!count) { + return 0; + } + if (count == 1) { + return sum; + } + let stddevMultiplier = UrlbarPrefs.get("autoFill.stddevMultiplier"); + return ( + sum / count + + stddevMultiplier * Math.sqrt((squares - (sum * sum) / count) / count) + ); +} + +/** + * Checks that origins appear in a given order in the database. + * @param {string} host The "fixed" host, without "www." + * @param {Array} prefixOrder The prefixes (scheme + www.) sorted appropriately. + */ +async function checkOriginsOrder(host, prefixOrder) { + await PlacesUtils.withConnectionWrapper("checkOriginsOrder", async db => { + let prefixes = ( + await db.execute( + `SELECT prefix || iif(instr(host, "www.") = 1, "www.", "") + FROM moz_origins + WHERE host = :host OR host = "www." || :host + ORDER BY ROWID ASC + `, + { host } + ) + ).map(r => r.getResultByIndex(0)); + Assert.deepEqual(prefixes, prefixOrder); + }); +} diff --git a/browser/components/urlbar/tests/unit/test_UrlbarController_integration.js b/browser/components/urlbar/tests/unit/test_UrlbarController_integration.js new file mode 100644 index 0000000000..6da8255b5a --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_UrlbarController_integration.js @@ -0,0 +1,104 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * These tests test the UrlbarController in association with the model. + */ + +"use strict"; + +const { PromiseUtils } = ChromeUtils.import( + "resource://gre/modules/PromiseUtils.jsm" +); + +const TEST_URL = "http://example.com"; +const match = new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TAB_SWITCH, + UrlbarUtils.RESULT_SOURCE.TABS, + { url: TEST_URL } +); +let controller; + +/** + * Asserts that the query context has the expected values. + * + * @param {UrlbarQueryContext} context + * @param {object} expectedValues The expected values for the UrlbarQueryContext. + */ +function assertContextMatches(context, expectedValues) { + Assert.ok( + context instanceof UrlbarQueryContext, + "Should be a UrlbarQueryContext" + ); + + for (let [key, value] of Object.entries(expectedValues)) { + Assert.equal( + context[key], + value, + `Should have the expected value for ${key} in the UrlbarQueryContext` + ); + } +} + +add_task(async function setup() { + controller = UrlbarTestUtils.newMockController(); +}); + +add_task(async function test_basic_search() { + let providerName = registerBasicTestProvider([match]); + const context = createContext(TEST_URL, { providers: [providerName] }); + + let startedPromise = promiseControllerNotification( + controller, + "onQueryStarted" + ); + let resultsPromise = promiseControllerNotification( + controller, + "onQueryResults" + ); + + controller.startQuery(context); + + let params = await startedPromise; + + Assert.equal(params[0], context); + + params = await resultsPromise; + + Assert.deepEqual( + params[0].results, + [match], + "Should have the expected match" + ); +}); + +add_task(async function test_cancel_search() { + let providerCanceledDeferred = PromiseUtils.defer(); + let providerName = registerBasicTestProvider( + [match], + providerCanceledDeferred.resolve + ); + const context = createContext(TEST_URL, { providers: [providerName] }); + + let startedPromise = promiseControllerNotification( + controller, + "onQueryStarted" + ); + let cancelPromise = promiseControllerNotification( + controller, + "onQueryCancelled" + ); + + controller.startQuery(context); + + let params = await startedPromise; + + controller.cancelQuery(context); + + Assert.equal(params[0], context); + + info("Should tell the provider the query is canceled"); + await providerCanceledDeferred.promise; + + params = await cancelPromise; +}); diff --git a/browser/components/urlbar/tests/unit/test_UrlbarController_telemetry.js b/browser/components/urlbar/tests/unit/test_UrlbarController_telemetry.js new file mode 100644 index 0000000000..d103eb10f5 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_UrlbarController_telemetry.js @@ -0,0 +1,256 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * These tests unit test the functionality of UrlbarController by stubbing out the + * model and providing stubs to be called. + */ + +"use strict"; + +const TEST_URL = "http://example.com"; +const MATCH = new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TAB_SWITCH, + UrlbarUtils.RESULT_SOURCE.TABS, + { url: TEST_URL } +); +const TELEMETRY_1ST_RESULT = "PLACES_AUTOCOMPLETE_1ST_RESULT_TIME_MS"; +const TELEMETRY_6_FIRST_RESULTS = "PLACES_AUTOCOMPLETE_6_FIRST_RESULTS_TIME_MS"; + +let controller; +let firstHistogram; +let sixthHistogram; + +/** + * A delayed test provider, allowing the query to be delayed for an amount of time. + */ +class DelayedProvider extends TestProvider { + async startQuery(context, add) { + Assert.ok(context, "context is passed-in"); + Assert.equal(typeof add, "function", "add is a callback"); + this._add = add; + await new Promise(resolve => { + this._resultsAdded = resolve; + }); + } + async addResults(matches, finish = true) { + // startQuery may have not been invoked yet, so wait for it + await TestUtils.waitForCondition( + () => !!this._add, + "Waiting for the _add callback" + ); + for (const match of matches) { + this._add(this, match); + } + if (finish) { + this._add = null; + this._resultsAdded(); + } + } +} + +/** + * Returns the number of reports sent recorded within the histogram results. + * + * @param {object} results a snapshot of histogram results to check. + * @returns {number} The count of reports recorded in the histogram. + */ +function getHistogramReportsCount(results) { + let sum = 0; + for (let [, value] of Object.entries(results.values)) { + sum += value; + } + return sum; +} + +add_task(function setup() { + controller = UrlbarTestUtils.newMockController(); + + firstHistogram = Services.telemetry.getHistogramById(TELEMETRY_1ST_RESULT); + sixthHistogram = Services.telemetry.getHistogramById( + TELEMETRY_6_FIRST_RESULTS + ); +}); + +add_task(async function test_n_autocomplete_cancel() { + firstHistogram.clear(); + sixthHistogram.clear(); + + let providerCanceledDeferred = PromiseUtils.defer(); + let provider = new TestProvider({ + results: [], + onCancel: providerCanceledDeferred.resolve, + }); + UrlbarProvidersManager.registerProvider(provider); + const context = createContext(TEST_URL, { providers: [provider.name] }); + + Assert.ok( + !TelemetryStopwatch.running(TELEMETRY_1ST_RESULT, context), + "Should not have started first result stopwatch" + ); + Assert.ok( + !TelemetryStopwatch.running(TELEMETRY_6_FIRST_RESULTS, context), + "Should not have started first 6 results stopwatch" + ); + + controller.startQuery(context); + + Assert.ok( + TelemetryStopwatch.running(TELEMETRY_1ST_RESULT, context), + "Should have started first result stopwatch" + ); + Assert.ok( + TelemetryStopwatch.running(TELEMETRY_6_FIRST_RESULTS, context), + "Should have started first 6 results stopwatch" + ); + + controller.cancelQuery(context); + + await providerCanceledDeferred.promise; + + Assert.ok( + !TelemetryStopwatch.running(TELEMETRY_1ST_RESULT, context), + "Should have canceled first result stopwatch" + ); + Assert.ok( + !TelemetryStopwatch.running(TELEMETRY_6_FIRST_RESULTS, context), + "Should have canceled first 6 results stopwatch" + ); + + let results = firstHistogram.snapshot(); + Assert.equal( + results.sum, + 0, + "Should not have recorded any times (first result)" + ); + results = sixthHistogram.snapshot(); + Assert.equal( + results.sum, + 0, + "Should not have recorded any times (first 6 results)" + ); +}); + +add_task(async function test_n_autocomplete_results() { + firstHistogram.clear(); + sixthHistogram.clear(); + + let provider = new DelayedProvider(); + UrlbarProvidersManager.registerProvider(provider); + const context = createContext(TEST_URL, { providers: [provider.name] }); + + let resultsPromise = promiseControllerNotification( + controller, + "onQueryResults" + ); + + Assert.ok( + !TelemetryStopwatch.running(TELEMETRY_1ST_RESULT, context), + "Should not have started first result stopwatch" + ); + Assert.ok( + !TelemetryStopwatch.running(TELEMETRY_6_FIRST_RESULTS, context), + "Should not have started first 6 results stopwatch" + ); + + controller.startQuery(context); + + Assert.ok( + TelemetryStopwatch.running(TELEMETRY_1ST_RESULT, context), + "Should have started first result stopwatch" + ); + Assert.ok( + TelemetryStopwatch.running(TELEMETRY_6_FIRST_RESULTS, context), + "Should have started first 6 results stopwatch" + ); + + await provider.addResults([MATCH], false); + await resultsPromise; + + Assert.ok( + !TelemetryStopwatch.running(TELEMETRY_1ST_RESULT, context), + "Should have stopped the first stopwatch" + ); + Assert.ok( + TelemetryStopwatch.running(TELEMETRY_6_FIRST_RESULTS, context), + "Should have kept the first 6 results stopwatch running" + ); + + let firstResults = firstHistogram.snapshot(); + let first6Results = sixthHistogram.snapshot(); + Assert.equal( + getHistogramReportsCount(firstResults), + 1, + "Should have recorded one time for the first result" + ); + Assert.equal( + getHistogramReportsCount(first6Results), + 0, + "Should not have recorded any times (first 6 results)" + ); + + // Now add 5 more results, so that the first 6 results is triggered. + for (let i = 0; i < 5; i++) { + resultsPromise = promiseControllerNotification( + controller, + "onQueryResults" + ); + await provider.addResults( + [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TAB_SWITCH, + UrlbarUtils.RESULT_SOURCE.TABS, + { url: TEST_URL + "/i" } + ), + ], + false + ); + await resultsPromise; + } + + Assert.ok( + !TelemetryStopwatch.running(TELEMETRY_1ST_RESULT, context), + "Should have stopped the first stopwatch" + ); + Assert.ok( + !TelemetryStopwatch.running(TELEMETRY_6_FIRST_RESULTS, context), + "Should have stopped the first 6 results stopwatch" + ); + + let updatedResults = firstHistogram.snapshot(); + let updated6Results = sixthHistogram.snapshot(); + Assert.deepEqual( + updatedResults, + firstResults, + "Should not have changed the histogram for the first result" + ); + Assert.equal( + getHistogramReportsCount(updated6Results), + 1, + "Should have recorded one time for the first 6 results" + ); + + // Add one more, to check neither are updated. + resultsPromise = promiseControllerNotification(controller, "onQueryResults"); + await provider.addResults([ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TAB_SWITCH, + UrlbarUtils.RESULT_SOURCE.TABS, + { url: TEST_URL + "/6" } + ), + ]); + await resultsPromise; + + let secondUpdateResults = firstHistogram.snapshot(); + let secondUpdate6Results = sixthHistogram.snapshot(); + Assert.deepEqual( + secondUpdateResults, + firstResults, + "Should not have changed the histogram for the first result" + ); + Assert.equal( + getHistogramReportsCount(secondUpdate6Results), + 1, + "Should not have changed the histogram for the first 6 results" + ); +}); diff --git a/browser/components/urlbar/tests/unit/test_UrlbarController_unit.js b/browser/components/urlbar/tests/unit/test_UrlbarController_unit.js new file mode 100644 index 0000000000..c62df478f1 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_UrlbarController_unit.js @@ -0,0 +1,389 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * These tests unit test the functionality of UrlbarController by stubbing out the + * model and providing stubs to be called. + */ + +"use strict"; + +// A fake ProvidersManager. +let fPM; +let sandbox; +let generalListener; +let controller; + +/** + * Asserts that the query context has the expected values. + * + * @param {UrlbarQueryContext} context + * @param {object} expectedValues The expected values for the UrlbarQueryContext. + */ +function assertContextMatches(context, expectedValues) { + Assert.ok( + context instanceof UrlbarQueryContext, + "Should be a UrlbarQueryContext" + ); + + for (let [key, value] of Object.entries(expectedValues)) { + Assert.equal( + context[key], + value, + `Should have the expected value for ${key} in the UrlbarQueryContext` + ); + } +} + +add_task(function setup() { + sandbox = sinon.createSandbox(); + + fPM = { + startQuery: sandbox.stub(), + cancelQuery: sandbox.stub(), + }; + + generalListener = { + onQueryStarted: sandbox.stub(), + onQueryResults: sandbox.stub(), + onQueryCancelled: sandbox.stub(), + }; + + controller = UrlbarTestUtils.newMockController({ + manager: fPM, + }); + controller.addQueryListener(generalListener); +}); + +add_task(function test_constructor_throws() { + Assert.throws( + () => new UrlbarController(), + /Missing options: input/, + "Should throw if the input was not supplied" + ); + Assert.throws( + () => new UrlbarController({ input: {} }), + /input is missing 'window' property/, + "Should throw if the input is not a UrlbarInput" + ); + Assert.throws( + () => new UrlbarController({ input: { window: {} } }), + /input.window should be an actual browser window/, + "Should throw if the input.window is not a window" + ); + Assert.throws( + () => + new UrlbarController({ + input: { + window: { + location: "about:fake", + }, + }, + }), + /input.window should be an actual browser window/, + "Should throw if the input.window is not an object" + ); + Assert.throws( + () => + new UrlbarController({ + input: { + window: { + location: { + href: "about:fake", + }, + }, + }, + }), + /input.window should be an actual browser window/, + "Should throw if the input.window does not have the correct location" + ); + Assert.throws( + () => + new UrlbarController({ + input: { + window: { + location: { + href: AppConstants.BROWSER_CHROME_URL, + }, + }, + }, + }), + /input.isPrivate must be set/, + "Should throw if input.isPrivate is not set" + ); + + new UrlbarController({ + input: { + isPrivate: false, + window: { + location: { + href: AppConstants.BROWSER_CHROME_URL, + }, + }, + }, + }); + Assert.ok(true, "Correct call should not throw"); +}); + +add_task(function test_add_and_remove_listeners() { + Assert.throws( + () => controller.addQueryListener(null), + /Expected listener to be an object/, + "Should throw for a null listener" + ); + Assert.throws( + () => controller.addQueryListener(123), + /Expected listener to be an object/, + "Should throw for a non-object listener" + ); + + const listener = {}; + + controller.addQueryListener(listener); + + Assert.ok( + controller._listeners.has(listener), + "Should have added the listener to the list." + ); + + // Adding a non-existent listener shouldn't throw. + controller.removeQueryListener(123); + + controller.removeQueryListener(listener); + + Assert.ok( + !controller._listeners.has(listener), + "Should have removed the listener from the list" + ); + + sandbox.resetHistory(); +}); + +add_task(function test__notify() { + const listener1 = { + onFake: sandbox.stub().callsFake(() => { + throw new Error("fake error"); + }), + }; + const listener2 = { + onFake: sandbox.stub(), + }; + + controller.addQueryListener(listener1); + controller.addQueryListener(listener2); + + const param = "1234"; + + controller.notify("onFake", param); + + Assert.equal( + listener1.onFake.callCount, + 1, + "Should have called the first listener method." + ); + Assert.deepEqual( + listener1.onFake.args[0], + [param], + "Should have called the first listener with the correct argument" + ); + Assert.equal( + listener2.onFake.callCount, + 1, + "Should have called the second listener method." + ); + Assert.deepEqual( + listener2.onFake.args[0], + [param], + "Should have called the first listener with the correct argument" + ); + + controller.removeQueryListener(listener2); + controller.removeQueryListener(listener1); + + // This should succeed without errors. + controller.notify("onNewFake"); + + sandbox.resetHistory(); +}); + +add_task(function test_handle_query_starts_search() { + const context = createContext(); + controller.startQuery(context); + + Assert.equal( + fPM.startQuery.callCount, + 1, + "Should have called startQuery once" + ); + Assert.equal( + fPM.startQuery.args[0].length, + 2, + "Should have called startQuery with two arguments" + ); + + assertContextMatches(fPM.startQuery.args[0][0], {}); + Assert.equal( + fPM.startQuery.args[0][1], + controller, + "Should have passed the controller as the second argument" + ); + + Assert.equal( + generalListener.onQueryStarted.callCount, + 1, + "Should have called onQueryStarted for the listener" + ); + Assert.deepEqual( + generalListener.onQueryStarted.args[0], + [context], + "Should have called onQueryStarted with the context" + ); + + sandbox.resetHistory(); +}); + +add_task(async function test_handle_query_starts_search_sets_allowAutofill() { + let originalValue = Services.prefs.getBoolPref("browser.urlbar.autoFill"); + Services.prefs.setBoolPref("browser.urlbar.autoFill", !originalValue); + + await controller.startQuery(createContext()); + + Assert.equal( + fPM.startQuery.callCount, + 1, + "Should have called startQuery once" + ); + Assert.equal( + fPM.startQuery.args[0].length, + 2, + "Should have called startQuery with two arguments" + ); + + assertContextMatches(fPM.startQuery.args[0][0], { + allowAutofill: !originalValue, + }); + Assert.equal( + fPM.startQuery.args[0][1], + controller, + "Should have passed the controller as the second argument" + ); + + sandbox.resetHistory(); + + Services.prefs.clearUserPref("browser.urlbar.autoFill"); +}); + +add_task(function test_cancel_query() { + const context = createContext(); + controller.startQuery(context); + + controller.cancelQuery(); + + Assert.equal( + fPM.cancelQuery.callCount, + 1, + "Should have called cancelQuery once" + ); + Assert.equal( + fPM.cancelQuery.args[0].length, + 1, + "Should have called cancelQuery with one argument" + ); + + Assert.equal( + generalListener.onQueryCancelled.callCount, + 1, + "Should have called onQueryCancelled for the listener" + ); + Assert.deepEqual( + generalListener.onQueryCancelled.args[0], + [context], + "Should have called onQueryCancelled with the context" + ); + + sandbox.resetHistory(); +}); + +add_task(function test_receiveResults() { + const context = createContext(); + context.results = []; + controller.receiveResults(context); + + Assert.equal( + generalListener.onQueryResults.callCount, + 1, + "Should have called onQueryResults for the listener" + ); + Assert.deepEqual( + generalListener.onQueryResults.args[0], + [context], + "Should have called onQueryResults with the context" + ); + + sandbox.resetHistory(); +}); + +add_task(async function test_notifications_order() { + // Clear any pending notifications. + const context = createContext(); + await controller.startQuery(context); + + // Check that when multiple queries are executed, the notifications arrive + // in the proper order. + let collectingListener = new Proxy( + {}, + { + _notifications: [], + get(target, name) { + if (name == "notifications") { + return this._notifications; + } + return () => { + this._notifications.push(name); + }; + }, + } + ); + controller.addQueryListener(collectingListener); + controller.startQuery(context); + Assert.deepEqual( + ["onQueryStarted"], + collectingListener.notifications, + "Check onQueryStarted is fired synchronously" + ); + controller.startQuery(context); + Assert.deepEqual( + ["onQueryStarted", "onQueryCancelled", "onQueryFinished", "onQueryStarted"], + collectingListener.notifications, + "Check order of notifications" + ); + controller.cancelQuery(); + Assert.deepEqual( + [ + "onQueryStarted", + "onQueryCancelled", + "onQueryFinished", + "onQueryStarted", + "onQueryCancelled", + "onQueryFinished", + ], + collectingListener.notifications, + "Check order of notifications" + ); + await controller.startQuery(context); + controller.cancelQuery(); + Assert.deepEqual( + [ + "onQueryStarted", + "onQueryCancelled", + "onQueryFinished", + "onQueryStarted", + "onQueryCancelled", + "onQueryFinished", + "onQueryStarted", + "onQueryFinished", + ], + collectingListener.notifications, + "Check order of notifications" + ); +}); diff --git a/browser/components/urlbar/tests/unit/test_UrlbarPrefs.js b/browser/components/urlbar/tests/unit/test_UrlbarPrefs.js new file mode 100644 index 0000000000..b08a5fff2a --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_UrlbarPrefs.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(function test() { + Assert.throws( + () => UrlbarPrefs.get("browser.migration.version"), + /Trying to access an unknown pref/, + "Should throw when passing an untracked pref" + ); + + Assert.throws( + () => UrlbarPrefs.set("browser.migration.version", 100), + /Trying to access an unknown pref/, + "Should throw when passing an untracked pref" + ); + Assert.throws( + () => UrlbarPrefs.set("maxRichResults", "10"), + /Invalid value/, + "Should throw when passing an invalid value type" + ); + + Assert.deepEqual(UrlbarPrefs.get("formatting.enabled"), true); + UrlbarPrefs.set("formatting.enabled", false); + Assert.deepEqual(UrlbarPrefs.get("formatting.enabled"), false); + + Assert.deepEqual(UrlbarPrefs.get("maxRichResults"), 10); + UrlbarPrefs.set("maxRichResults", 6); + Assert.deepEqual(UrlbarPrefs.get("maxRichResults"), 6); + + Assert.deepEqual(UrlbarPrefs.get("autoFill.stddevMultiplier"), 0.0); + UrlbarPrefs.set("autoFill.stddevMultiplier", 0.01); + // Due to rounding errors, floats are slightly imprecise, so we can't + // directly compare what we set to what we retrieve. + Assert.deepEqual( + parseFloat(UrlbarPrefs.get("autoFill.stddevMultiplier").toFixed(2)), + 0.01 + ); +}); diff --git a/browser/components/urlbar/tests/unit/test_UrlbarQueryContext.js b/browser/components/urlbar/tests/unit/test_UrlbarQueryContext.js new file mode 100644 index 0000000000..e30e2fa0eb --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_UrlbarQueryContext.js @@ -0,0 +1,73 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(function test_constructor() { + Assert.throws( + () => new UrlbarQueryContext(), + /Missing or empty allowAutofill provided to UrlbarQueryContext/, + "Should throw with no arguments" + ); + + Assert.throws( + () => + new UrlbarQueryContext({ + allowAutofill: true, + isPrivate: false, + searchString: "foo", + }), + /Missing or empty maxResults provided to UrlbarQueryContext/, + "Should throw with a missing maxResults parameter" + ); + + Assert.throws( + () => + new UrlbarQueryContext({ + allowAutofill: true, + maxResults: 1, + searchString: "foo", + }), + /Missing or empty isPrivate provided to UrlbarQueryContext/, + "Should throw with a missing isPrivate parameter" + ); + + Assert.throws( + () => + new UrlbarQueryContext({ + isPrivate: false, + maxResults: 1, + searchString: "foo", + }), + /Missing or empty allowAutofill provided to UrlbarQueryContext/, + "Should throw with a missing allowAutofill parameter" + ); + + let qc = new UrlbarQueryContext({ + allowAutofill: false, + isPrivate: true, + maxResults: 1, + searchString: "foo", + }); + + Assert.strictEqual( + qc.allowAutofill, + false, + "Should have saved the correct value for allowAutofill" + ); + Assert.strictEqual( + qc.isPrivate, + true, + "Should have saved the correct value for isPrivate" + ); + Assert.equal( + qc.maxResults, + 1, + "Should have saved the correct value for maxResults" + ); + Assert.equal( + qc.searchString, + "foo", + "Should have saved the correct value for searchString" + ); +}); diff --git a/browser/components/urlbar/tests/unit/test_UrlbarQueryContext_restrictSource.js b/browser/components/urlbar/tests/unit/test_UrlbarQueryContext_restrictSource.js new file mode 100644 index 0000000000..056110fed9 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_UrlbarQueryContext_restrictSource.js @@ -0,0 +1,145 @@ +/* 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/. */ + +/** + * Test for restrictions set through UrlbarQueryContext.sources. + */ + +add_task(async function setup() { + let engine = await addTestSuggestionsEngine(); + let oldDefaultEngine = await Services.search.getDefault(); + Services.search.setDefault(engine); + registerCleanupFunction(async () => + Services.search.setDefault(oldDefaultEngine) + ); +}); + +XPCOMUtils.defineLazyServiceGetter( + this, + "unifiedComplete", + "@mozilla.org/autocomplete/search;1?name=unifiedcomplete", + "nsIAutoCompleteSearch" +); + +add_task(async function test_restrictions() { + await PlacesTestUtils.addVisits([ + { uri: "http://history.com/", title: "match" }, + ]); + await PlacesUtils.bookmarks.insert({ + url: "http://bookmark.com/", + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "match", + }); + await UrlbarProviderOpenTabs.registerOpenTab("http://openpagematch.com/"); + + info("Bookmark restrict"); + let results = await get_results({ + sources: [UrlbarUtils.RESULT_SOURCE.BOOKMARKS], + searchString: "match", + }); + // Skip the heuristic result. + Assert.deepEqual( + results.filter(r => !r.heuristic).map(r => r.payload.url), + ["http://bookmark.com/"] + ); + + info("History restrict"); + results = await get_results({ + sources: [UrlbarUtils.RESULT_SOURCE.HISTORY], + searchString: "match", + }); + // Skip the heuristic result. + Assert.deepEqual( + results.filter(r => !r.heuristic).map(r => r.payload.url), + ["http://history.com/"] + ); + + info("tabs restrict"); + results = await get_results({ + sources: [UrlbarUtils.RESULT_SOURCE.TABS], + searchString: "match", + }); + // Skip the heuristic result. + Assert.deepEqual( + results.filter(r => !r.heuristic).map(r => r.payload.url), + ["http://openpagematch.com/"] + ); + + info("search restrict"); + results = await get_results({ + sources: [UrlbarUtils.RESULT_SOURCE.SEARCH], + searchString: "match", + }); + Assert.ok( + !results.some(r => r.payload.engine != "engine-suggestions.xml"), + "All the results should be search results" + ); + + info("search restrict should ignore restriction token"); + results = await get_results({ + sources: [UrlbarUtils.RESULT_SOURCE.SEARCH], + searchString: `${UrlbarTokenizer.RESTRICT.BOOKMARKS} match`, + }); + Assert.ok( + !results.some(r => r.payload.engine != "engine-suggestions.xml"), + "All the results should be search results" + ); + Assert.equal( + results[0].payload.query, + `${UrlbarTokenizer.RESTRICT.BOOKMARKS} match`, + "The restriction token should be ignored and not stripped" + ); + + info("search restrict with alias"); + let aliasEngine = await Services.search.addEngineWithDetails("Test", { + alias: "match", + template: "http://example.com/?search={searchTerms}", + }); + registerCleanupFunction(async function() { + await Services.search.removeEngine(aliasEngine); + }); + results = await get_results({ + sources: [UrlbarUtils.RESULT_SOURCE.SEARCH], + searchString: "match this", + }); + Assert.ok( + !results.some(r => r.payload.engine != "engine-suggestions.xml"), + "All the results should be search results and the alias should be ignored" + ); + Assert.equal( + results[0].payload.query, + `match this`, + "The restriction token should be ignored and not stripped" + ); + + info("search restrict with other engine"); + results = await get_results({ + sources: [UrlbarUtils.RESULT_SOURCE.SEARCH], + searchString: "match", + engineName: "Test", + }); + Assert.ok( + !results.some(r => r.payload.engine != "Test"), + "All the results should be search results from the Test engine" + ); +}); + +async function get_results(test) { + let controller = UrlbarTestUtils.newMockController(); + let options = { + allowAutofill: false, + isPrivate: false, + maxResults: 10, + sources: test.sources, + }; + if (test.engineName) { + options.searchMode = { + source: UrlbarUtils.RESULT_SOURCE.SEARCH, + engineName: test.engineName, + }; + } + let queryContext = createContext(test.searchString, options); + await controller.startQuery(queryContext); + return queryContext.results; +} diff --git a/browser/components/urlbar/tests/unit/test_UrlbarSearchUtils.jsm b/browser/components/urlbar/tests/unit/test_UrlbarSearchUtils.jsm new file mode 100644 index 0000000000..92b2cfbb4b --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_UrlbarSearchUtils.jsm @@ -0,0 +1,292 @@ +/* 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 { UrlbarSearchUtils } = ChromeUtils.import( + "resource:///modules/UrlbarSearchUtils.jsm" +); + +add_task(async function() { + await UrlbarSearchUtils.init(); + // Tell the search service we are running in the US. This also has the + // desired side-effect of preventing our geoip lookup. + Services.prefs.setCharPref("browser.search.region", "US"); + + Services.search.restoreDefaultEngines(); + Services.search.resetToOriginalDefaultEngine(); +}); + +add_task(async function search_engine_match() { + let engine = await Services.search.getDefault(); + let domain = engine.getResultDomain(); + let token = domain.substr(0, 1); + let matchedEngine = ( + await UrlbarSearchUtils.enginesForDomainPrefix(token) + )[0]; + Assert.equal(matchedEngine, engine); +}); + +add_task(async function no_match() { + Assert.equal( + 0, + (await UrlbarSearchUtils.enginesForDomainPrefix("test")).length + ); +}); + +add_task(async function hide_search_engine_nomatch() { + let engine = await Services.search.getDefault(); + let domain = engine.getResultDomain(); + let token = domain.substr(0, 1); + let promiseTopic = promiseSearchTopic("engine-changed"); + await Promise.all([Services.search.removeEngine(engine), promiseTopic]); + Assert.ok(engine.hidden); + let matchedEngines = await UrlbarSearchUtils.enginesForDomainPrefix(token); + Assert.ok( + !matchedEngines.length || matchedEngines[0].getResultDomain() != domain + ); + engine.hidden = false; + await TestUtils.waitForCondition( + async () => (await UrlbarSearchUtils.enginesForDomainPrefix(token)).length + ); + let matchedEngine2 = ( + await UrlbarSearchUtils.enginesForDomainPrefix(token) + )[0]; + Assert.ok(matchedEngine2); +}); + +add_task(async function onlyEnabled_option_nomatch() { + let engine = await Services.search.getDefault(); + let domain = engine.getResultDomain(); + let token = domain.substr(0, 1); + Services.prefs.setCharPref("browser.search.hiddenOneOffs", engine.name); + let matchedEngines = await UrlbarSearchUtils.enginesForDomainPrefix(token, { + onlyEnabled: true, + }); + Assert.ok( + !matchedEngines.length || matchedEngines[0].getResultDomain() != domain + ); + Services.prefs.clearUserPref("browser.search.hiddenOneOffs"); + matchedEngines = await UrlbarSearchUtils.enginesForDomainPrefix(token, { + onlyEnabled: true, + }); + Assert.ok( + matchedEngines.length && matchedEngines[0].getResultDomain() == domain + ); +}); + +add_task(async function add_search_engine_match() { + let promiseTopic = promiseSearchTopic("engine-added"); + Assert.equal( + 0, + (await UrlbarSearchUtils.enginesForDomainPrefix("bacon")).length + ); + await Promise.all([ + Services.search.addEngineWithDetails("bacon", { + alias: "pork", + description: "Search Bacon", + method: "GET", + template: "http://www.bacon.moz/?search={searchTerms}", + }), + promiseTopic, + ]); + await promiseTopic; + let matchedEngine = ( + await UrlbarSearchUtils.enginesForDomainPrefix("bacon") + )[0]; + Assert.ok(matchedEngine); + Assert.equal(matchedEngine.searchForm, "http://www.bacon.moz"); + Assert.equal(matchedEngine.name, "bacon"); + Assert.equal(matchedEngine.iconURI, null); + info("also type part of the public suffix"); + matchedEngine = ( + await UrlbarSearchUtils.enginesForDomainPrefix("bacon.m") + )[0]; + Assert.ok(matchedEngine); + Assert.equal(matchedEngine.searchForm, "http://www.bacon.moz"); + Assert.equal(matchedEngine.name, "bacon"); + Assert.equal(matchedEngine.iconURI, null); +}); + +add_task(async function match_multiple_search_engines() { + let promiseTopic = promiseSearchTopic("engine-added"); + Assert.equal( + 0, + (await UrlbarSearchUtils.enginesForDomainPrefix("baseball")).length + ); + await Promise.all([ + Services.search.addEngineWithDetails("baseball", { + description: "Search Baseball", + method: "GET", + template: "http://www.baseball.moz/?search={searchTerms}", + }), + promiseTopic, + ]); + await promiseTopic; + let matchedEngines = await UrlbarSearchUtils.enginesForDomainPrefix("ba"); + Assert.equal( + matchedEngines.length, + 2, + "enginesForDomainPrefix returned two engines." + ); + Assert.equal(matchedEngines[0].searchForm, "http://www.bacon.moz"); + Assert.equal(matchedEngines[0].name, "bacon"); + Assert.equal(matchedEngines[1].searchForm, "http://www.baseball.moz"); + Assert.equal(matchedEngines[1].name, "baseball"); +}); + +add_task(async function test_aliased_search_engine_match() { + Assert.equal(null, await UrlbarSearchUtils.engineForAlias("sober")); + // Lower case + let matchedEngine = await UrlbarSearchUtils.engineForAlias("pork"); + Assert.ok(matchedEngine); + Assert.equal(matchedEngine.name, "bacon"); + Assert.ok(matchedEngine.aliases.includes("pork")); + Assert.equal(matchedEngine.iconURI, null); + // Upper case + matchedEngine = await UrlbarSearchUtils.engineForAlias("PORK"); + Assert.ok(matchedEngine); + Assert.equal(matchedEngine.name, "bacon"); + Assert.ok(matchedEngine.aliases.includes("pork")); + Assert.equal(matchedEngine.iconURI, null); + // Cap case + matchedEngine = await UrlbarSearchUtils.engineForAlias("Pork"); + Assert.ok(matchedEngine); + Assert.equal(matchedEngine.name, "bacon"); + Assert.ok(matchedEngine.aliases.includes("pork")); + Assert.equal(matchedEngine.iconURI, null); +}); + +add_task(async function test_aliased_search_engine_match_upper_case_alias() { + let promiseTopic = promiseSearchTopic("engine-added"); + Assert.equal( + 0, + (await UrlbarSearchUtils.enginesForDomainPrefix("patch")).length + ); + await Promise.all([ + Services.search.addEngineWithDetails("patch", { + alias: "PR", + description: "Search Patch", + method: "GET", + template: "http://www.patch.moz/?search={searchTerms}", + }), + promiseTopic, + ]); + // lower case + let matchedEngine = await UrlbarSearchUtils.engineForAlias("pr"); + Assert.ok(matchedEngine); + Assert.equal(matchedEngine.name, "patch"); + Assert.ok(matchedEngine.aliases.includes("PR")); + Assert.equal(matchedEngine.iconURI, null); + // Upper case + matchedEngine = await UrlbarSearchUtils.engineForAlias("PR"); + Assert.ok(matchedEngine); + Assert.equal(matchedEngine.name, "patch"); + Assert.ok(matchedEngine.aliases.includes("PR")); + Assert.equal(matchedEngine.iconURI, null); + // Cap case + matchedEngine = await UrlbarSearchUtils.engineForAlias("Pr"); + Assert.ok(matchedEngine); + Assert.equal(matchedEngine.name, "patch"); + Assert.ok(matchedEngine.aliases.includes("PR")); + Assert.equal(matchedEngine.iconURI, null); +}); + +add_task(async function remove_search_engine_nomatch() { + let engine = Services.search.getEngineByName("bacon"); + let promiseTopic = promiseSearchTopic("engine-removed"); + await Promise.all([Services.search.removeEngine(engine), promiseTopic]); + Assert.equal( + 0, + (await UrlbarSearchUtils.enginesForDomainPrefix("bacon")).length + ); +}); + +add_task(async function test_builtin_aliased_search_engine_match() { + let engine = await UrlbarSearchUtils.engineForAlias("@google"); + Assert.ok(engine); + Assert.equal(engine.name, "Google"); + let promiseTopic = promiseSearchTopic("engine-changed"); + await Promise.all([Services.search.removeEngine(engine), promiseTopic]); + let matchedEngine = await UrlbarSearchUtils.engineForAlias("@google"); + Assert.ok(!matchedEngine); + engine.hidden = false; + await TestUtils.waitForCondition(() => + UrlbarSearchUtils.engineForAlias("@google") + ); + engine = await UrlbarSearchUtils.engineForAlias("@google"); + Assert.ok(engine); +}); + +add_task(async function test_serps_are_equivalent() { + info("Subset URL has extraneous parameters."); + let url1 = "https://example.com/search?q=test&type=images"; + let url2 = "https://example.com/search?q=test"; + Assert.ok(!UrlbarSearchUtils.serpsAreEquivalent(url1, url2)); + info("Superset URL has extraneous parameters."); + Assert.ok(UrlbarSearchUtils.serpsAreEquivalent(url2, url1)); + + info("Same keys, different values."); + url1 = "https://example.com/search?q=test&type=images"; + url2 = "https://example.com/search?q=test123&type=maps"; + Assert.ok(!UrlbarSearchUtils.serpsAreEquivalent(url1, url2)); + Assert.ok(!UrlbarSearchUtils.serpsAreEquivalent(url2, url1)); + + info("Subset matching isn't strict (URL is subset of itself)."); + Assert.ok(UrlbarSearchUtils.serpsAreEquivalent(url1, url1)); + + info("Origin and pathname are ignored."); + url1 = "https://example.com/search?q=test"; + url2 = "https://example-1.com/maps?q=test"; + Assert.ok(UrlbarSearchUtils.serpsAreEquivalent(url1, url2)); + Assert.ok(UrlbarSearchUtils.serpsAreEquivalent(url2, url1)); + + info("Params can be optionally ignored"); + url1 = "https://example.com/search?q=test&abc=123&foo=bar"; + url2 = "https://example.com/search?q=test"; + Assert.ok(!UrlbarSearchUtils.serpsAreEquivalent(url1, url2)); + Assert.ok(UrlbarSearchUtils.serpsAreEquivalent(url1, url2, ["abc", "foo"])); +}); + +add_task(async function test_get_root_domain_from_engine() { + let engine = await Services.search.addEngineWithDetails("TestEngine2", { + template: "http://example.com", + }); + Assert.equal(UrlbarSearchUtils.getRootDomainFromEngine(engine), "example"); + await Services.search.removeEngine(engine); + + engine = await Services.search.addEngineWithDetails("TestEngine", { + template: "http://www.subdomain.othersubdomain.example.com", + }); + Assert.equal(UrlbarSearchUtils.getRootDomainFromEngine(engine), "example"); + await Services.search.removeEngine(engine); + + // We let engines with URL ending in .test through even though its not a valid + // TLD. + engine = await Services.search.addEngineWithDetails("TestMalformed", { + template: `http://mochi.test/?search={searchTerms}`, + }); + Assert.equal(UrlbarSearchUtils.getRootDomainFromEngine(engine), "mochi"); + await Services.search.removeEngine(engine); + + // We return the domain for engines with a malformed URL. + engine = await Services.search.addEngineWithDetails("TestMalformed", { + template: `http://subdomain.foobar/?search={searchTerms}`, + }); + Assert.equal( + UrlbarSearchUtils.getRootDomainFromEngine(engine), + "subdomain.foobar" + ); + await Services.search.removeEngine(engine); +}); + +function promiseSearchTopic(expectedVerb) { + return new Promise(resolve => { + Services.obs.addObserver(function observe(subject, topic, verb) { + info("browser-search-engine-modified: " + verb); + if (verb == expectedVerb) { + Services.obs.removeObserver(observe, "browser-search-engine-modified"); + resolve(); + } + }, "browser-search-engine-modified"); + }); +} diff --git a/browser/components/urlbar/tests/unit/test_UrlbarUtils_addToUrlbarHistory.js b/browser/components/urlbar/tests/unit/test_UrlbarUtils_addToUrlbarHistory.js new file mode 100644 index 0000000000..d2d30e1516 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_UrlbarUtils_addToUrlbarHistory.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * These tests unit test the functionality of the functions in UrlbarUtils. + * Some functions are bigger, and split out into sepearate test_UrlbarUtils_* files. + */ + +"use strict"; + +const { PrivateBrowsingUtils } = ChromeUtils.import( + "resource://gre/modules/PrivateBrowsingUtils.jsm" +); +const { PlacesUIUtils } = ChromeUtils.import( + "resource:///modules/PlacesUIUtils.jsm" +); + +let sandbox; + +add_task(function setup() { + sandbox = sinon.createSandbox(); +}); + +add_task(function test_addToUrlbarHistory() { + sandbox.stub(PlacesUIUtils, "markPageAsTyped"); + sandbox.stub(PrivateBrowsingUtils, "isWindowPrivate").returns(false); + + UrlbarUtils.addToUrlbarHistory("http://example.com"); + Assert.ok( + PlacesUIUtils.markPageAsTyped.calledOnce, + "Should have marked a simple URL as typed." + ); + PlacesUIUtils.markPageAsTyped.resetHistory(); + + UrlbarUtils.addToUrlbarHistory(); + Assert.ok( + PlacesUIUtils.markPageAsTyped.notCalled, + "Should not have attempted to mark a null URL as typed." + ); + PlacesUIUtils.markPageAsTyped.resetHistory(); + + UrlbarUtils.addToUrlbarHistory("http://exam ple.com"); + Assert.ok( + PlacesUIUtils.markPageAsTyped.notCalled, + "Should not have marked a URL containing a space as typed." + ); + PlacesUIUtils.markPageAsTyped.resetHistory(); + + UrlbarUtils.addToUrlbarHistory("http://exam\x01ple.com"); + Assert.ok( + PlacesUIUtils.markPageAsTyped.notCalled, + "Should not have marked a URL containing a control character as typed." + ); + PlacesUIUtils.markPageAsTyped.resetHistory(); + + PrivateBrowsingUtils.isWindowPrivate.returns(true); + UrlbarUtils.addToUrlbarHistory("http://example.com"); + Assert.ok( + PlacesUIUtils.markPageAsTyped.notCalled, + "Should not have marked a URL provided by a private browsing page as typed." + ); + PlacesUIUtils.markPageAsTyped.resetHistory(); +}); diff --git a/browser/components/urlbar/tests/unit/test_UrlbarUtils_getShortcutOrURIAndPostData.js b/browser/components/urlbar/tests/unit/test_UrlbarUtils_getShortcutOrURIAndPostData.js new file mode 100644 index 0000000000..c363b4b6ec --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_UrlbarUtils_getShortcutOrURIAndPostData.js @@ -0,0 +1,257 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * These tests unit test the functionality of UrlbarController by stubbing out the + * model and providing stubs to be called. + */ + +"use strict"; + +function getPostDataString(aIS) { + if (!aIS) { + return null; + } + + let sis = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ); + sis.init(aIS); + let dataLines = sis.read(aIS.available()).split("\n"); + + // only want the last line + return dataLines[dataLines.length - 1]; +} + +function keywordResult(aURL, aPostData, aIsUnsafe) { + this.url = aURL; + this.postData = aPostData; + this.isUnsafe = aIsUnsafe; +} + +function keyWordData() {} +keyWordData.prototype = { + init(aKeyWord, aURL, aPostData, aSearchWord) { + this.keyword = aKeyWord; + this.uri = Services.io.newURI(aURL); + this.postData = aPostData; + this.searchWord = aSearchWord; + + this.method = this.postData ? "POST" : "GET"; + }, +}; + +function bmKeywordData(aKeyWord, aURL, aPostData, aSearchWord) { + this.init(aKeyWord, aURL, aPostData, aSearchWord); +} +bmKeywordData.prototype = new keyWordData(); + +function searchKeywordData(aKeyWord, aURL, aPostData, aSearchWord) { + this.init(aKeyWord, aURL, aPostData, aSearchWord); +} +searchKeywordData.prototype = new keyWordData(); + +var testData = [ + [ + new bmKeywordData("bmget", "http://bmget/search=%s", null, "foo"), + new keywordResult("http://bmget/search=foo", null), + ], + + [ + new bmKeywordData("bmpost", "http://bmpost/", "search=%s", "foo2"), + new keywordResult("http://bmpost/", "search=foo2"), + ], + + [ + new bmKeywordData( + "bmpostget", + "http://bmpostget/search1=%s", + "search2=%s", + "foo3" + ), + new keywordResult("http://bmpostget/search1=foo3", "search2=foo3"), + ], + + [ + new bmKeywordData("bmget-nosearch", "http://bmget-nosearch/", null, ""), + new keywordResult("http://bmget-nosearch/", null), + ], + + [ + new searchKeywordData( + "searchget", + "http://searchget/?search={searchTerms}", + null, + "foo4" + ), + new keywordResult("http://searchget/?search=foo4", null, true), + ], + + [ + new searchKeywordData( + "searchpost", + "http://searchpost/", + "search={searchTerms}", + "foo5" + ), + new keywordResult("http://searchpost/", "search=foo5", true), + ], + + [ + new searchKeywordData( + "searchpostget", + "http://searchpostget/?search1={searchTerms}", + "search2={searchTerms}", + "foo6" + ), + new keywordResult( + "http://searchpostget/?search1=foo6", + "search2=foo6", + true + ), + ], + + // Bookmark keywords that don't take parameters should not be activated if a + // parameter is passed (bug 420328). + [ + new bmKeywordData("bmget-noparam", "http://bmget-noparam/", null, "foo7"), + new keywordResult(null, null, true), + ], + [ + new bmKeywordData( + "bmpost-noparam", + "http://bmpost-noparam/", + "not_a=param", + "foo8" + ), + new keywordResult(null, null, true), + ], + + // Test escaping (%s = escaped, %S = raw) + // UTF-8 default + [ + new bmKeywordData( + "bmget-escaping", + "http://bmget/?esc=%s&raw=%S", + null, + "fo\xE9" + ), + new keywordResult("http://bmget/?esc=fo%C3%A9&raw=fo\xE9", null), + ], + // Explicitly-defined ISO-8859-1 + [ + new bmKeywordData( + "bmget-escaping2", + "http://bmget/?esc=%s&raw=%S&mozcharset=ISO-8859-1", + null, + "fo\xE9" + ), + new keywordResult("http://bmget/?esc=fo%E9&raw=fo\xE9", null), + ], + + // Bug 359809: Test escaping +, /, and @ + // UTF-8 default + [ + new bmKeywordData( + "bmget-escaping", + "http://bmget/?esc=%s&raw=%S", + null, + "+/@" + ), + new keywordResult("http://bmget/?esc=%2B%2F%40&raw=+/@", null), + ], + // Explicitly-defined ISO-8859-1 + [ + new bmKeywordData( + "bmget-escaping2", + "http://bmget/?esc=%s&raw=%S&mozcharset=ISO-8859-1", + null, + "+/@" + ), + new keywordResult("http://bmget/?esc=%2B%2F%40&raw=+/@", null), + ], + + // Test using a non-bmKeywordData object, to test the behavior of + // getShortcutOrURIAndPostData for non-keywords (setupKeywords only adds keywords for + // bmKeywordData objects) + [{ keyword: "http://gavinsharp.com" }, new keywordResult(null, null, true)], +]; + +add_task(async function test_getshortcutoruri() { + await setupKeywords(); + + for (let item of testData) { + let [data, result] = item; + + let query = data.keyword; + if (data.searchWord) { + query += " " + data.searchWord; + } + let returnedData = await UrlbarUtils.getShortcutOrURIAndPostData(query); + // null result.url means we should expect the same query we sent in + let expected = result.url || query; + Assert.equal( + returnedData.url, + expected, + "got correct URL for " + data.keyword + ); + Assert.equal( + getPostDataString(returnedData.postData), + result.postData, + "got correct postData for " + data.keyword + ); + Assert.equal( + returnedData.mayInheritPrincipal, + !result.isUnsafe, + "got correct mayInheritPrincipal for " + data.keyword + ); + } + + await cleanupKeywords(); +}); + +var folder = null; +var gAddedEngines = []; + +async function setupKeywords() { + folder = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "keyword-test", + }); + for (let item of testData) { + let data = item[0]; + if (data instanceof bmKeywordData) { + await PlacesUtils.bookmarks.insert({ + url: data.uri, + parentGuid: folder.guid, + }); + await PlacesUtils.keywords.insert({ + keyword: data.keyword, + url: data.uri.spec, + postData: data.postData, + }); + } + + if (data instanceof searchKeywordData) { + let addedEngine = await Services.search.addEngineWithDetails( + data.keyword, + { + alias: data.keyword, + method: data.method, + template: data.uri.spec, + searchPostParams: data.postData, + } + ); + gAddedEngines.push(addedEngine); + } + } +} + +async function cleanupKeywords() { + await PlacesUtils.bookmarks.remove(folder); + for (let engine of gAddedEngines) { + await Services.search.removeEngine(engine); + } + gAddedEngines = []; +} diff --git a/browser/components/urlbar/tests/unit/test_UrlbarUtils_getTokenMatches.js b/browser/components/urlbar/tests/unit/test_UrlbarUtils_getTokenMatches.js new file mode 100644 index 0000000000..bae6ffc879 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_UrlbarUtils_getTokenMatches.js @@ -0,0 +1,294 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests UrlbarUtils.getTokenMatches. + */ + +"use strict"; + +add_task(function test() { + const tests = [ + { + tokens: ["mozilla", "is", "i"], + phrase: "mozilla is for the Open Web", + expected: [ + [0, 7], + [8, 2], + ], + }, + { + tokens: ["mozilla", "is", "i"], + phrase: "MOZILLA IS for the Open Web", + expected: [ + [0, 7], + [8, 2], + ], + }, + { + tokens: ["mozilla", "is", "i"], + phrase: "MoZiLlA Is for the Open Web", + expected: [ + [0, 7], + [8, 2], + ], + }, + { + tokens: ["MOZILLA", "IS", "I"], + phrase: "mozilla is for the Open Web", + expected: [ + [0, 7], + [8, 2], + ], + }, + { + tokens: ["MoZiLlA", "Is", "I"], + phrase: "mozilla is for the Open Web", + expected: [ + [0, 7], + [8, 2], + ], + }, + { + tokens: ["mo", "b"], + phrase: "mozilla is for the Open Web", + expected: [ + [0, 2], + [26, 1], + ], + }, + { + tokens: ["mo", "b"], + phrase: "MOZILLA is for the OPEN WEB", + expected: [ + [0, 2], + [26, 1], + ], + }, + { + tokens: ["MO", "B"], + phrase: "mozilla is for the Open Web", + expected: [ + [0, 2], + [26, 1], + ], + }, + { + tokens: ["mo", ""], + phrase: "mozilla is for the Open Web", + expected: [[0, 2]], + }, + { + tokens: ["mozilla"], + phrase: "mozilla", + expected: [[0, 7]], + }, + { + tokens: ["mozilla"], + phrase: "MOZILLA", + expected: [[0, 7]], + }, + { + tokens: ["mozilla"], + phrase: "MoZiLlA", + expected: [[0, 7]], + }, + { + tokens: ["mozilla"], + phrase: "mOzIlLa", + expected: [[0, 7]], + }, + { + tokens: ["MOZILLA"], + phrase: "mozilla", + expected: [[0, 7]], + }, + { + tokens: ["MoZiLlA"], + phrase: "mozilla", + expected: [[0, 7]], + }, + { + tokens: ["mOzIlLa"], + phrase: "mozilla", + expected: [[0, 7]], + }, + { + tokens: ["\u9996"], + phrase: "Test \u9996\u9875 Test", + expected: [[5, 1]], + }, + { + tokens: ["mo", "zilla"], + phrase: "mozilla", + expected: [[0, 7]], + }, + { + tokens: ["mo", "zilla"], + phrase: "MOZILLA", + expected: [[0, 7]], + }, + { + tokens: ["mo", "zilla"], + phrase: "MoZiLlA", + expected: [[0, 7]], + }, + { + tokens: ["mo", "zilla"], + phrase: "mOzIlLa", + expected: [[0, 7]], + }, + { + tokens: ["MO", "ZILLA"], + phrase: "mozilla", + expected: [[0, 7]], + }, + { + tokens: ["Mo", "Zilla"], + phrase: "mozilla", + expected: [[0, 7]], + }, + { + tokens: ["moz", "zilla"], + phrase: "mozilla", + expected: [[0, 7]], + }, + { + tokens: [""], // Should never happen in practice. + phrase: "mozilla", + expected: [], + }, + { + tokens: ["mo", "om"], + phrase: "mozilla mozzarella momo", + expected: [ + [0, 2], + [8, 2], + [19, 4], + ], + }, + { + tokens: ["mo", "om"], + phrase: "MOZILLA MOZZARELLA MOMO", + expected: [ + [0, 2], + [8, 2], + [19, 4], + ], + }, + { + tokens: ["MO", "OM"], + phrase: "mozilla mozzarella momo", + expected: [ + [0, 2], + [8, 2], + [19, 4], + ], + }, + { + tokens: ["resume"], + phrase: "résumé", + expected: [[0, 6]], + }, + { + // This test should succeed even in a Spanish locale where N and Ñ are + // considered distinct letters. + tokens: ["jalapeno"], + phrase: "jalapeño", + expected: [[0, 8]], + }, + ]; + for (let { tokens, phrase, expected } of tests) { + tokens = tokens.map(t => ({ + value: t, + lowerCaseValue: t.toLocaleLowerCase(), + })); + Assert.deepEqual( + UrlbarUtils.getTokenMatches(tokens, phrase, UrlbarUtils.HIGHLIGHT.TYPED), + expected, + `Match "${tokens.map(t => t.value).join(", ")}" on "${phrase}"` + ); + } +}); + +/** + * Tests suggestion highlighting. Note that suggestions are only highlighted if + * the matching token is at the beginning of a word in the matched string. + */ +add_task(function testSuggestions() { + const tests = [ + { + tokens: ["mozilla", "is", "i"], + phrase: "mozilla is for the Open Web", + expected: [ + [7, 1], + [10, 17], + ], + }, + { + tokens: ["\u9996"], + phrase: "Test \u9996\u9875 Test", + expected: [ + [0, 5], + [6, 6], + ], + }, + { + tokens: ["mo", "zilla"], + phrase: "mOzIlLa", + expected: [[2, 5]], + }, + { + tokens: ["MO", "ZILLA"], + phrase: "mozilla", + expected: [[2, 5]], + }, + { + tokens: [""], // Should never happen in practice. + phrase: "mozilla", + expected: [[0, 7]], + }, + { + tokens: ["mo", "om", "la"], + phrase: "mozilla mozzarella momo", + expected: [ + [2, 6], + [10, 9], + [21, 2], + ], + }, + { + tokens: ["mo", "om", "la"], + phrase: "MOZILLA MOZZARELLA MOMO", + expected: [ + [2, 6], + [10, 9], + [21, 2], + ], + }, + { + tokens: ["MO", "OM", "LA"], + phrase: "mozilla mozzarella momo", + expected: [ + [2, 6], + [10, 9], + [21, 2], + ], + }, + ]; + for (let { tokens, phrase, expected } of tests) { + tokens = tokens.map(t => ({ + value: t, + lowerCaseValue: t.toLocaleLowerCase(), + })); + Assert.deepEqual( + UrlbarUtils.getTokenMatches( + tokens, + phrase, + UrlbarUtils.HIGHLIGHT.SUGGESTED + ), + expected, + `Match "${tokens.map(t => t.value).join(", ")}" on "${phrase}"` + ); + } +}); diff --git a/browser/components/urlbar/tests/unit/test_autofill_about_urls.js b/browser/components/urlbar/tests/unit/test_autofill_about_urls.js new file mode 100644 index 0000000000..4a15aa5e9b --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_autofill_about_urls.js @@ -0,0 +1,100 @@ +/* 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"; + +const ENGINE_NAME = "engine-suggestions.xml"; + +testEngine_setup(); + +// "about:ab" should match "about:about" +add_task(async function aboutAb() { + let context = createContext("about:ab", { isPrivate: false }); + await check_results({ + context, + autofilled: "about:about", + completed: "about:about", + matches: [ + makeVisitResult(context, { + uri: "about:about", + title: "about:about", + heuristic: true, + }), + ], + }); +}); + +// "about:Ab" should match "about:about" +add_task(async function aboutAb() { + let context = createContext("about:Ab", { isPrivate: false }); + await check_results({ + context, + autofilled: "about:About", + completed: "about:about", + matches: [ + makeVisitResult(context, { + uri: "about:about", + title: "about:about", + heuristic: true, + }), + ], + }); +}); + +// "about:about" should match "about:about" +add_task(async function aboutAbout() { + let context = createContext("about:about", { isPrivate: false }); + await check_results({ + context, + autofilled: "about:about", + completed: "about:about", + matches: [ + makeVisitResult(context, { + uri: "about:about", + title: "about:about", + heuristic: true, + }), + ], + }); +}); + +// "about:a" should complete to "about:about" and also match "about:addons" +add_task(async function aboutAboutAndAboutAddons() { + let context = createContext("about:a", { isPrivate: false }); + await check_results({ + context, + search: "about:a", + autofilled: "about:about", + completed: "about:about", + matches: [ + makeVisitResult(context, { + uri: "about:about", + title: "about:about", + heuristic: true, + }), + makeVisitResult(context, { + uri: "about:addons", + title: "about:addons", + iconUri: "", + providerName: "UnifiedComplete", + }), + ], + }); +}); + +// "about:" should *not* match anything +add_task(async function aboutColonHasNoMatch() { + let context = createContext("about:", { isPrivate: false }); + await check_results({ + context, + search: "about:", + matches: [ + makeSearchResult(context, { + engineName: ENGINE_NAME, + providerName: "HeuristicFallback", + heuristic: true, + }), + ], + }); +}); diff --git a/browser/components/urlbar/tests/unit/test_autofill_bookmarked.js b/browser/components/urlbar/tests/unit/test_autofill_bookmarked.js new file mode 100644 index 0000000000..39483e993a --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_autofill_bookmarked.js @@ -0,0 +1,148 @@ +/* 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/. */ + +// This is a specific autofill test to ensure we pick the correct bookmarked +// state of an origin. Regardless of the order of origins, we should always pick +// the correct bookmarked status. + +add_task(async function() { + registerCleanupFunction(async () => { + Services.prefs.clearUserPref("browser.urlbar.suggest.searches"); + }); + Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false); + + let host = "example.com"; + // Add a bookmark to the http version, but ensure the https version has an + // higher frecency. + let bookmark = await PlacesUtils.bookmarks.insert({ + url: `http://${host}`, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + for (let i = 0; i < 3; i++) { + await PlacesTestUtils.addVisits(`https://${host}`); + } + // ensure both fall below the threshold. + for (let i = 0; i < 15; i++) { + await PlacesTestUtils.addVisits(`https://not-${host}`); + } + + async function check_autofill() { + let threshold = await getOriginAutofillThreshold(); + let httpOriginFrecency = await getOriginFrecency("http://", host); + Assert.less( + httpOriginFrecency, + threshold, + "Http origin frecency should be below the threshold" + ); + let httpsOriginFrecency = await getOriginFrecency("https://", host); + Assert.less( + httpsOriginFrecency, + threshold, + "Https origin frecency should be below the threshold" + ); + Assert.less( + httpOriginFrecency, + httpsOriginFrecency, + "Http origin frecency should be below the https origin frecency" + ); + + // The http version should be filled because it's bookmarked, but with the + // https prefix that is more frecent. + let context = createContext("ex", { isPrivate: false }); + await check_results({ + context, + autofilled: `${host}/`, + completed: `https://${host}/`, + matches: [ + makeVisitResult(context, { + uri: `https://${host}/`, + title: `https://${host}`, + heuristic: true, + }), + makeVisitResult(context, { + uri: `https://not-${host}/`, + title: `test visit for https://not-${host}/`, + }), + ], + }); + } + + await check_autofill(); + + // Now remove the bookmark, ensure to remove the orphans, then reinsert the + // bookmark; thus we physically invert the order of the rows in the table. + await checkOriginsOrder(host, ["http://", "https://"]); + await PlacesUtils.bookmarks.remove(bookmark); + await PlacesUtils.withConnectionWrapper("removeOrphans", async db => { + db.execute(`DELETE FROM moz_places WHERE url = :url`, { + url: `http://${host}/`, + }); + db.execute( + `DELETE FROM moz_origins WHERE prefix = "http://" AND host = :host`, + { host } + ); + }); + bookmark = await PlacesUtils.bookmarks.insert({ + url: `http://${host}`, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + + await checkOriginsOrder(host, ["https://", "http://"]); + + await check_autofill(); + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.remove(bookmark); +}); + +add_task(async function test_www() { + // Add a bookmark to the www version + let host = "example.com"; + await PlacesUtils.bookmarks.insert({ + url: `http://www.${host}`, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + + info("search for start of www."); + let context = createContext("w", { isPrivate: false }); + await check_results({ + context, + autofilled: `www.${host}/`, + completed: `http://www.${host}/`, + matches: [ + makeVisitResult(context, { + uri: `http://www.${host}/`, + title: `www.${host}`, + heuristic: true, + }), + ], + }); + info("search for full www."); + context = createContext("www.", { isPrivate: false }); + await check_results({ + context, + autofilled: `www.${host}/`, + completed: `http://www.${host}/`, + matches: [ + makeVisitResult(context, { + uri: `http://www.${host}/`, + title: `www.${host}`, + heuristic: true, + }), + ], + }); + info("search for host without www."); + context = createContext("ex", { isPrivate: false }); + await check_results({ + context, + autofilled: `${host}/`, + completed: `http://www.${host}/`, + matches: [ + makeVisitResult(context, { + uri: `http://www.${host}/`, + title: `www.${host}`, + heuristic: true, + }), + ], + }); +}); diff --git a/browser/components/urlbar/tests/unit/test_autofill_functional.js b/browser/components/urlbar/tests/unit/test_autofill_functional.js new file mode 100644 index 0000000000..662cf420b8 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_autofill_functional.js @@ -0,0 +1,112 @@ +/* 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/. */ + +// Functional tests for inline autocomplete + +add_task(async function setup() { + registerCleanupFunction(async () => { + Services.prefs.clearUserPref("browser.urlbar.suggest.searches"); + }); + + Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false); +}); + +add_task(async function test_urls_order() { + info("Add urls, check for correct order"); + let places = [ + { uri: Services.io.newURI("http://visit1.mozilla.org") }, + { uri: Services.io.newURI("http://visit2.mozilla.org") }, + ]; + await PlacesTestUtils.addVisits(places); + let context = createContext("vis", { isPrivate: false }); + await check_results({ + context, + autofilled: "visit2.mozilla.org/", + completed: "http://visit2.mozilla.org/", + matches: [ + makeVisitResult(context, { + uri: "http://visit2.mozilla.org/", + title: "visit2.mozilla.org", + heuristic: true, + }), + makeVisitResult(context, { + uri: "http://visit1.mozilla.org/", + title: "test visit for http://visit1.mozilla.org/", + }), + ], + }); + await cleanupPlaces(); +}); + +add_task(async function test_bookmark_first() { + info("With a bookmark and history, the query result should be the bookmark"); + await PlacesTestUtils.addBookmarkWithDetails({ + uri: Services.io.newURI("http://bookmark1.mozilla.org/"), + }); + await PlacesTestUtils.addVisits( + Services.io.newURI("http://bookmark1.mozilla.org/foo") + ); + let context = createContext("bookmark", { isPrivate: false }); + await check_results({ + context, + autofilled: "bookmark1.mozilla.org/", + completed: "http://bookmark1.mozilla.org/", + matches: [ + makeVisitResult(context, { + uri: "http://bookmark1.mozilla.org/", + title: "bookmark1.mozilla.org", + heuristic: true, + }), + makeVisitResult(context, { + uri: "http://bookmark1.mozilla.org/foo", + title: "test visit for http://bookmark1.mozilla.org/foo", + }), + ], + }); + await cleanupPlaces(); +}); + +add_task(async function test_complete_querystring() { + info("Check to make sure we autocomplete after ?"); + await PlacesTestUtils.addVisits( + Services.io.newURI("http://smokey.mozilla.org/foo?bacon=delicious") + ); + let context = createContext("smokey.mozilla.org/foo?", { isPrivate: false }); + await check_results({ + context, + autofilled: "smokey.mozilla.org/foo?bacon=delicious", + completed: "http://smokey.mozilla.org/foo?bacon=delicious", + matches: [ + makeVisitResult(context, { + uri: "http://smokey.mozilla.org/foo?bacon=delicious", + title: "smokey.mozilla.org/foo?bacon=delicious", + heuristic: true, + }), + ], + }); + await cleanupPlaces(); +}); + +add_task(async function test_complete_fragment() { + info("Check to make sure we autocomplete after #"); + await PlacesTestUtils.addVisits( + Services.io.newURI("http://smokey.mozilla.org/foo?bacon=delicious#bar") + ); + let context = createContext("smokey.mozilla.org/foo?bacon=delicious#bar", { + isPrivate: false, + }); + await check_results({ + context, + autofilled: "smokey.mozilla.org/foo?bacon=delicious#bar", + completed: "http://smokey.mozilla.org/foo?bacon=delicious#bar", + matches: [ + makeVisitResult(context, { + uri: "http://smokey.mozilla.org/foo?bacon=delicious#bar", + title: "smokey.mozilla.org/foo?bacon=delicious#bar", + heuristic: true, + }), + ], + }); + await cleanupPlaces(); +}); diff --git a/browser/components/urlbar/tests/unit/test_autofill_origins.js b/browser/components/urlbar/tests/unit/test_autofill_origins.js new file mode 100644 index 0000000000..af9685286f --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_autofill_origins.js @@ -0,0 +1,638 @@ +/* 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"; + +const ENGINE_NAME = "engine-suggestions.xml"; +const HEURISTIC_FALLBACK_PROVIDERNAME = "HeuristicFallback"; + +const origin = "example.com"; + +async function cleanup() { + let suggestPrefs = ["history", "bookmark", "openpage"]; + for (let type of suggestPrefs) { + Services.prefs.clearUserPref("browser.urlbar.suggest." + type); + } + await cleanupPlaces(); +} + +testEngine_setup(); + +// "example.com/" should match http://example.com/. i.e., the search string +// should be treated as if it didn't have the trailing slash. +add_task(async function trailingSlash() { + await PlacesTestUtils.addVisits([ + { + uri: "http://example.com/", + }, + ]); + + let context = createContext(`${origin}/`, { isPrivate: false }); + await check_results({ + context, + autofilled: `${origin}/`, + completed: `http://${origin}/`, + matches: [ + makeVisitResult(context, { + uri: `http://${origin}/`, + title: `${origin}/`, + heuristic: true, + }), + ], + }); + await cleanup(); +}); + +// "example.com/" should match http://www.example.com/. i.e., the search string +// should be treated as if it didn't have the trailing slash. +add_task(async function trailingSlashWWW() { + await PlacesTestUtils.addVisits([ + { + uri: "http://www.example.com/", + }, + ]); + let context = createContext(`${origin}/`, { isPrivate: false }); + await check_results({ + context, + autofilled: "example.com/", + completed: "http://www.example.com/", + matches: [ + makeVisitResult(context, { + uri: `http://www.${origin}/`, + title: `www.${origin}/`, + heuristic: true, + }), + ], + }); + await cleanup(); +}); + +// "ex" should match http://example.com:8888/, and the port should be completed. +add_task(async function port() { + await PlacesTestUtils.addVisits([ + { + uri: "http://example.com:8888/", + }, + ]); + let context = createContext("ex", { isPrivate: false }); + await check_results({ + context, + autofilled: "example.com:8888/", + completed: "http://example.com:8888/", + matches: [ + makeVisitResult(context, { + uri: `http://${origin}:8888/`, + title: `${origin}:8888`, + heuristic: true, + }), + ], + }); + await cleanup(); +}); + +// "example.com:8" should match http://example.com:8888/, and the port should +// be completed. +add_task(async function portPartial() { + await PlacesTestUtils.addVisits([ + { + uri: "http://example.com:8888/", + }, + ]); + let context = createContext(`${origin}:8`, { isPrivate: false }); + await check_results({ + context, + autofilled: "example.com:8888/", + completed: "http://example.com:8888/", + matches: [ + makeVisitResult(context, { + uri: `http://${origin}:8888/`, + title: `${origin}:8888`, + heuristic: true, + }), + ], + }); + await cleanup(); +}); + +// "EXaM" should match http://example.com/ and the case of the search string +// should be preserved in the autofilled value. +add_task(async function preserveCase() { + await PlacesTestUtils.addVisits([ + { + uri: "http://example.com/", + }, + ]); + let context = createContext("EXaM", { isPrivate: false }); + await check_results({ + context, + autofilled: "EXaMple.com/", + completed: "http://example.com/", + matches: [ + makeVisitResult(context, { + uri: `http://${origin}/`, + title: `${origin}`, + heuristic: true, + }), + ], + }); + await cleanup(); +}); + +// "EXaM" should match http://example.com:8888/, the port should be completed, +// and the case of the search string should be preserved in the autofilled +// value. +add_task(async function preserveCasePort() { + await PlacesTestUtils.addVisits([ + { + uri: "http://example.com:8888/", + }, + ]); + let context = createContext("EXaM", { isPrivate: false }); + await check_results({ + context, + autofilled: "EXaMple.com:8888/", + completed: "http://example.com:8888/", + matches: [ + makeVisitResult(context, { + uri: `http://${origin}:8888/`, + title: `${origin}:8888`, + heuristic: true, + }), + ], + }); + await cleanup(); +}); + +// "example.com:89" should *not* match http://example.com:8888/. +add_task(async function portNoMatch1() { + await PlacesTestUtils.addVisits([ + { + uri: "http://example.com:8888/", + }, + ]); + let context = createContext(`${origin}:89`, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: `http://${origin}:89/`, + title: `http://${origin}:89/`, + iconUri: "", + heuristic: true, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + ], + }); + await cleanup(); +}); + +// "example.com:9" should *not* match http://example.com:8888/. +add_task(async function portNoMatch2() { + await PlacesTestUtils.addVisits([ + { + uri: "http://example.com:8888/", + }, + ]); + let context = createContext(`${origin}:9`, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: `http://${origin}:9/`, + title: `http://${origin}:9/`, + iconUri: "", + heuristic: true, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + ], + }); + await cleanup(); +}); + +// "example/" should *not* match http://example.com/. +add_task(async function trailingSlash_2() { + await PlacesTestUtils.addVisits([ + { + uri: "http://example.com/", + }, + ]); + let context = createContext("example/", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: "http://example/", + title: "http://example/", + iconUri: "page-icon:http://example/", + heuristic: true, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + ], + }); + await cleanup(); +}); + +// multi.dotted.domain, search up to dot. +add_task(async function multidotted() { + await PlacesTestUtils.addVisits([ + { + uri: "http://www.example.co.jp:8888/", + }, + ]); + let context = createContext("www.example.co.", { isPrivate: false }); + await check_results({ + context, + autofilled: "www.example.co.jp:8888/", + completed: "http://www.example.co.jp:8888/", + matches: [ + makeVisitResult(context, { + uri: "http://www.example.co.jp:8888/", + title: "www.example.co.jp:8888", + heuristic: true, + }), + ], + }); + await cleanup(); +}); + +add_task(async function test_ip() { + // IP addresses have complicated rules around whether they show + // HeuristicFallback's backup search result. Flip this pref to disable that + // backup search and simplify ths subtest. + Services.prefs.setBoolPref("keyword.enabled", false); + for (let str of [ + "192.168.1.1/", + "255.255.255.255:8080/", + "[2001:db8::1428:57ab]/", + "[::c0a8:5909]/", + "[::1]/", + ]) { + info("testing " + str); + await PlacesTestUtils.addVisits("http://" + str); + for (let i = 1; i < str.length; ++i) { + let context = createContext(str.substring(0, i), { isPrivate: false }); + await check_results({ + context, + autofilled: str, + completed: "http://" + str, + matches: [ + makeVisitResult(context, { + uri: "http://" + str, + title: str.replace(/\/$/, ""), // strip trailing slash + heuristic: true, + }), + ], + }); + } + await cleanup(); + } + Services.prefs.clearUserPref("keyword.enabled"); +}); + +// host starting with large number. +add_task(async function large_number_host() { + await PlacesTestUtils.addVisits([ + { + uri: "http://12345example.it:8888/", + }, + ]); + let context = createContext("1234", { isPrivate: false }); + await check_results({ + context, + autofilled: "12345example.it:8888/", + completed: "http://12345example.it:8888/", + matches: [ + makeVisitResult(context, { + uri: "http://12345example.it:8888/", + title: "12345example.it:8888", + heuristic: true, + }), + ], + }); + await cleanup(); +}); + +// When determining which origins should be autofilled, all the origins sharing +// a host should be added together to get their combined frecency -- i.e., +// prefixes should be collapsed. And then from that list, the origin with the +// highest frecency should be chosen. +add_task(async function groupByHost() { + // Add some visits to the same host, example.com. Add one http and two https + // so that https has a higher frecency and is therefore the origin that should + // be autofilled. Also add another origin that has a higher frecency than + // both so that alone, neither http nor https would be autofilled, but added + // together they should be. + await PlacesTestUtils.addVisits([ + { uri: "http://example.com/" }, + + { uri: "https://example.com/" }, + { uri: "https://example.com/" }, + + { uri: "https://mozilla.org/" }, + { uri: "https://mozilla.org/" }, + { uri: "https://mozilla.org/" }, + { uri: "https://mozilla.org/" }, + ]); + + let httpFrec = frecencyForUrl("http://example.com/"); + let httpsFrec = frecencyForUrl("https://example.com/"); + let otherFrec = frecencyForUrl("https://mozilla.org/"); + Assert.less(httpFrec, httpsFrec, "Sanity check"); + Assert.less(httpsFrec, otherFrec, "Sanity check"); + + // Make sure the frecencies of the three origins are as expected in relation + // to the threshold. + let threshold = await getOriginAutofillThreshold(); + Assert.less(httpFrec, threshold, "http origin should be < threshold"); + Assert.less(httpsFrec, threshold, "https origin should be < threshold"); + Assert.ok(threshold <= otherFrec, "Other origin should cross threshold"); + + Assert.ok( + threshold <= httpFrec + httpsFrec, + "http and https origin added together should cross threshold" + ); + + // The https origin should be autofilled. + let context = createContext("ex", { isPrivate: false }); + await check_results({ + context, + autofilled: "example.com/", + completed: "https://example.com/", + matches: [ + makeVisitResult(context, { + uri: "https://example.com/", + title: "https://example.com", + heuristic: true, + }), + ], + }); + + await cleanup(); +}); + +// This is the same as the previous (groupByHost), but it changes the standard +// deviation multiplier by setting the corresponding pref. This makes sure that +// the pref is respected. +add_task(async function groupByHostNonDefaultStddevMultiplier() { + let stddevMultiplier = 1.5; + Services.prefs.setCharPref( + "browser.urlbar.autoFill.stddevMultiplier", + Number(stddevMultiplier).toFixed(1) + ); + + await PlacesTestUtils.addVisits([ + { uri: "http://example.com/" }, + { uri: "http://example.com/" }, + + { uri: "https://example.com/" }, + { uri: "https://example.com/" }, + { uri: "https://example.com/" }, + + { uri: "https://foo.com/" }, + { uri: "https://foo.com/" }, + { uri: "https://foo.com/" }, + + { uri: "https://mozilla.org/" }, + { uri: "https://mozilla.org/" }, + { uri: "https://mozilla.org/" }, + { uri: "https://mozilla.org/" }, + { uri: "https://mozilla.org/" }, + ]); + + let httpFrec = frecencyForUrl("http://example.com/"); + let httpsFrec = frecencyForUrl("https://example.com/"); + let otherFrec = frecencyForUrl("https://mozilla.org/"); + Assert.less(httpFrec, httpsFrec, "Sanity check"); + Assert.less(httpsFrec, otherFrec, "Sanity check"); + + // Make sure the frecencies of the three origins are as expected in relation + // to the threshold. + let threshold = await getOriginAutofillThreshold(); + Assert.less(httpFrec, threshold, "http origin should be < threshold"); + Assert.less(httpsFrec, threshold, "https origin should be < threshold"); + Assert.ok(threshold <= otherFrec, "Other origin should cross threshold"); + + Assert.ok( + threshold <= httpFrec + httpsFrec, + "http and https origin added together should cross threshold" + ); + + // The https origin should be autofilled. + let context = createContext("ex", { isPrivate: false }); + await check_results({ + context, + autofilled: "example.com/", + completed: "https://example.com/", + matches: [ + makeVisitResult(context, { + uri: "https://example.com/", + title: "https://example.com", + heuristic: true, + }), + ], + }); + + Services.prefs.clearUserPref("browser.urlbar.autoFill.stddevMultiplier"); + + await cleanup(); +}); + +// This is similar to suggestHistoryFalse_bookmark_0 in test_autofill_tasks.js, +// but it adds unbookmarked visits for multiple URLs with the same origin. +add_task(async function suggestHistoryFalse_bookmark_multiple() { + // Force only bookmarked pages to be suggested and therefore only bookmarked + // pages to be completed. + Services.prefs.setBoolPref("browser.urlbar.suggest.history", false); + + let search = "ex"; + let baseURL = "http://example.com/"; + let bookmarkedURL = baseURL + "bookmarked"; + + // Add visits for three different URLs all sharing the same origin, and then + // bookmark the second one. After that, the origin should be autofilled. The + // reason for adding unbookmarked visits before and after adding the + // bookmarked visit is to make sure our aggregate SQL query for determining + // whether an origin is bookmarked is correct. + + await PlacesTestUtils.addVisits([ + { + uri: baseURL + "other1", + }, + ]); + let context = createContext(search, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: ENGINE_NAME, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + heuristic: true, + }), + ], + }); + + await PlacesTestUtils.addVisits([ + { + uri: bookmarkedURL, + }, + ]); + context = createContext(search, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: ENGINE_NAME, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + heuristic: true, + }), + ], + }); + + await PlacesTestUtils.addVisits([ + { + uri: baseURL + "other2", + }, + ]); + context = createContext(search, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: ENGINE_NAME, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + heuristic: true, + }), + ], + }); + + // Now bookmark the second URL. It should be suggested and completed. + await PlacesTestUtils.addBookmarkWithDetails({ + uri: bookmarkedURL, + }); + context = createContext(search, { isPrivate: false }); + await check_results({ + context, + autofilled: "example.com/", + completed: baseURL, + matches: [ + makeVisitResult(context, { + uri: baseURL, + title: "example.com", + heuristic: true, + }), + makeBookmarkResult(context, { + uri: bookmarkedURL, + title: "A bookmark", + }), + ], + }); + + await cleanup(); +}); + +// This is similar to suggestHistoryFalse_bookmark_prefix_0 in +// autofill_tasks.js, but it adds unbookmarked visits for multiple URLs with the +// same origin. +add_task(async function suggestHistoryFalse_bookmark_prefix_multiple() { + // Force only bookmarked pages to be suggested and therefore only bookmarked + // pages to be completed. + Services.prefs.setBoolPref("browser.urlbar.suggest.history", false); + + let search = "http://ex"; + let baseURL = "http://example.com/"; + let bookmarkedURL = baseURL + "bookmarked"; + + // Add visits for three different URLs all sharing the same origin, and then + // bookmark the second one. After that, the origin should be autofilled. The + // reason for adding unbookmarked visits before and after adding the + // bookmarked visit is to make sure our aggregate SQL query for determining + // whether an origin is bookmarked is correct. + + await PlacesTestUtils.addVisits([ + { + uri: baseURL + "other1", + }, + ]); + let context = createContext(search, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: `${search}/`, + title: `${search}/`, + iconUri: "", + heuristic: true, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + ], + }); + + await PlacesTestUtils.addVisits([ + { + uri: bookmarkedURL, + }, + ]); + context = createContext(search, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: `${search}/`, + title: `${search}/`, + iconUri: "", + heuristic: true, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + ], + }); + + await PlacesTestUtils.addVisits([ + { + uri: baseURL + "other2", + }, + ]); + context = createContext(search, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: `${search}/`, + title: `${search}/`, + iconUri: "", + heuristic: true, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + ], + }); + + // Now bookmark the second URL. It should be suggested and completed. + await PlacesTestUtils.addBookmarkWithDetails({ + uri: bookmarkedURL, + }); + context = createContext(search, { isPrivate: false }); + await check_results({ + context, + autofilled: "http://example.com/", + completed: baseURL, + matches: [ + makeVisitResult(context, { + uri: baseURL, + title: "example.com", + heuristic: true, + }), + makeBookmarkResult(context, { + uri: bookmarkedURL, + title: "A bookmark", + }), + ], + }); + + await cleanup(); +}); diff --git a/browser/components/urlbar/tests/unit/test_autofill_originsAndQueries.js b/browser/components/urlbar/tests/unit/test_autofill_originsAndQueries.js new file mode 100644 index 0000000000..5ad440596b --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_autofill_originsAndQueries.js @@ -0,0 +1,2408 @@ +/* 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 ENGINE_NAME = "engine-suggestions.xml"; +const HEURISTIC_FALLBACK_PROVIDERNAME = "HeuristicFallback"; +const UNIFIEDCOMPLETE_PROVIDERNAME = "UnifiedComplete"; + +/** + * Helpful reminder of the `autofilled` and `completed` properties in the + * object passed to check_results: + * autofilled: expected input.value after autofill + * completed: expected input.value after autofill and enter is pressed + * + * `completed` is the URL that the controller sets to input.value, and the URL + * that will ultimately be loaded when you press enter. + **/ + +async function cleanup() { + let suggestPrefs = ["history", "bookmark", "openpage"]; + for (let type of suggestPrefs) { + Services.prefs.clearUserPref("browser.urlbar.suggest." + type); + } + await cleanupPlaces(); +} + +testEngine_setup(); + +let path; +let search; +let searchCase; +let title; +let url; +const host = "example.com"; +let origins; + +function add_autofill_task(callback) { + let func = async () => { + info(`Running subtest with origins disabled: ${callback.name}`); + origins = false; + path = "/foo"; + search = "example.com/f"; + searchCase = "EXAMPLE.COM/f"; + title = "example.com/foo"; + url = host + path; + await callback(); + + info(`Running subtest with origins enabled: ${callback.name}`); + origins = true; + path = "/"; + search = "ex"; + searchCase = "EX"; + title = "example.com"; + url = host + path; + await callback(); + }; + Object.defineProperty(func, "name", { value: callback.name }); + add_task(func); +} + +// "ex" should match http://example.com/. +add_autofill_task(async function basic() { + await PlacesTestUtils.addVisits([ + { + uri: "http://" + url, + }, + ]); + let context = createContext(search, { isPrivate: false }); + await check_results({ + context, + autofilled: url, + completed: "http://" + url, + matches: [ + makeVisitResult(context, { + uri: "http://" + url, + title, + heuristic: true, + }), + ], + }); + await cleanup(); +}); + +// "EX" should match http://example.com/. +add_autofill_task(async function basicCase() { + await PlacesTestUtils.addVisits([ + { + uri: "http://" + url, + }, + ]); + let context = createContext(searchCase, { isPrivate: false }); + await check_results({ + context, + autofilled: searchCase + url.substr(searchCase.length), + completed: "http://" + url, + matches: [ + makeVisitResult(context, { + uri: "http://" + url, + title, + heuristic: true, + }), + ], + }); + await cleanup(); +}); + +// "ex" should match http://www.example.com/. +add_autofill_task(async function noWWWShouldMatchWWW() { + await PlacesTestUtils.addVisits([ + { + uri: "http://www." + url, + }, + ]); + let context = createContext(search, { isPrivate: false }); + await check_results({ + context, + autofilled: url, + completed: "http://www." + url, + matches: [ + makeVisitResult(context, { + uri: "http://www." + url, + title: "www." + title, + heuristic: true, + }), + ], + }); + await cleanup(); +}); + +// "EX" should match http://www.example.com/. +add_autofill_task(async function noWWWShouldMatchWWWCase() { + await PlacesTestUtils.addVisits([ + { + uri: "http://www." + url, + }, + ]); + let context = createContext(searchCase, { isPrivate: false }); + await check_results({ + context, + autofilled: searchCase + url.substr(searchCase.length), + completed: "http://www." + url, + matches: [ + makeVisitResult(context, { + uri: "http://www." + url, + title: "www." + title, + heuristic: true, + }), + ], + }); + await cleanup(); +}); + +// "www.ex" should *not* match http://example.com/. +add_autofill_task(async function wwwShouldNotMatchNoWWW() { + await PlacesTestUtils.addVisits([ + { + uri: "http://" + url, + }, + ]); + let context = createContext("www." + search, { isPrivate: false }); + if (origins) { + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: "http://www." + search + "/", + title: "http://www." + search + "/", + displayUrl: "http://www." + search, + heuristic: true, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + makeSearchResult(context, { + engineName: ENGINE_NAME, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + ], + }); + } else { + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: "http://www." + search, + title: "http://www." + search, + iconUri: `page-icon:http://www.${host}/`, + heuristic: true, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + ], + }); + } + await cleanup(); +}); + +// "http://ex" should match http://example.com/. +add_autofill_task(async function prefix() { + await PlacesTestUtils.addVisits([ + { + uri: "http://" + url, + }, + ]); + let context = createContext("http://" + search, { isPrivate: false }); + await check_results({ + context, + autofilled: "http://" + url, + completed: "http://" + url, + matches: [ + makeVisitResult(context, { + uri: "http://" + url, + title, + heuristic: true, + }), + ], + }); + await cleanup(); +}); + +// "HTTP://EX" should match http://example.com/. +add_autofill_task(async function prefixCase() { + await PlacesTestUtils.addVisits([ + { + uri: "http://" + url, + }, + ]); + let context = createContext("HTTP://" + searchCase, { isPrivate: false }); + await check_results({ + context, + autofilled: "HTTP://" + searchCase + url.substr(searchCase.length), + completed: "http://" + url, + matches: [ + makeVisitResult(context, { + uri: "http://" + url, + title, + heuristic: true, + }), + ], + }); + await cleanup(); +}); + +// "http://ex" should match http://www.example.com/. +add_autofill_task(async function prefixNoWWWShouldMatchWWW() { + await PlacesTestUtils.addVisits([ + { + uri: "http://www." + url, + }, + ]); + let context = createContext("http://" + search, { isPrivate: false }); + await check_results({ + context, + autofilled: "http://" + url, + completed: "http://www." + url, + matches: [ + makeVisitResult(context, { + uri: "http://www." + url, + title: "www." + title, + heuristic: true, + }), + ], + }); + await cleanup(); +}); + +// "HTTP://EX" should match http://www.example.com/. +add_autofill_task(async function prefixNoWWWShouldMatchWWWCase() { + await PlacesTestUtils.addVisits([ + { + uri: "http://www." + url, + }, + ]); + let context = createContext("HTTP://" + searchCase, { isPrivate: false }); + await check_results({ + context, + autofilled: "HTTP://" + searchCase + url.substr(searchCase.length), + completed: "http://www." + url, + matches: [ + makeVisitResult(context, { + uri: "http://www." + url, + title: "www." + title, + heuristic: true, + }), + ], + }); + await cleanup(); +}); + +// "http://www.ex" should *not* match http://example.com/. +add_autofill_task(async function prefixWWWShouldNotMatchNoWWW() { + await PlacesTestUtils.addVisits([ + { + uri: "http://" + url, + }, + ]); + let context = createContext("http://www." + search, { isPrivate: false }); + let prefixedUrl = origins ? `http://www.${search}/` : `http://www.${search}`; + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: prefixedUrl, + title: prefixedUrl, + heuristic: true, + iconUri: origins ? "" : `page-icon:http://www.${host}/`, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + ], + }); + await cleanup(); +}); + +// "http://ex" should *not* match https://example.com/. +add_autofill_task(async function httpPrefixShouldNotMatchHTTPS() { + await PlacesTestUtils.addVisits([ + { + uri: "https://" + url, + }, + ]); + let context = createContext("http://" + search, { isPrivate: false }); + let prefixedUrl = origins ? `http://${search}/` : `http://${search}`; + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: prefixedUrl, + title: prefixedUrl, + heuristic: true, + iconUri: origins ? "" : `page-icon:http://${host}/`, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + makeVisitResult(context, { + uri: "https://" + url, + title: "test visit for https://" + url, + providerName: UNIFIEDCOMPLETE_PROVIDERNAME, + }), + ], + }); + await cleanup(); +}); + +// "ex" should match https://example.com/. +add_autofill_task(async function httpsBasic() { + await PlacesTestUtils.addVisits([ + { + uri: "https://" + url, + }, + ]); + let context = createContext(search, { isPrivate: false }); + await check_results({ + context, + autofilled: url, + completed: "https://" + url, + matches: [ + makeVisitResult(context, { + uri: "https://" + url, + title: "https://" + title, + heuristic: true, + }), + ], + }); + await cleanup(); +}); + +// "ex" should match https://www.example.com/. +add_autofill_task(async function httpsNoWWWShouldMatchWWW() { + await PlacesTestUtils.addVisits([ + { + uri: "https://www." + url, + }, + ]); + let context = createContext(search, { isPrivate: false }); + await check_results({ + context, + autofilled: url, + completed: "https://www." + url, + matches: [ + makeVisitResult(context, { + uri: "https://www." + url, + title: "https://www." + title, + heuristic: true, + }), + ], + }); + await cleanup(); +}); + +// "www.ex" should *not* match https://example.com/. +add_autofill_task(async function httpsWWWShouldNotMatchNoWWW() { + await PlacesTestUtils.addVisits([ + { + uri: "https://" + url, + }, + ]); + let context = createContext("www." + search, { isPrivate: false }); + if (origins) { + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: "http://www." + search + "/", + title: "http://www." + search + "/", + displayUrl: "http://www." + search, + heuristic: true, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + makeSearchResult(context, { + engineName: ENGINE_NAME, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + ], + }); + } else { + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: "http://www." + search, + title: "http://www." + search, + iconUri: `page-icon:http://www.${host}/`, + heuristic: true, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + ], + }); + } + await cleanup(); +}); + +// "https://ex" should match https://example.com/. +add_autofill_task(async function httpsPrefix() { + await PlacesTestUtils.addVisits([ + { + uri: "https://" + url, + }, + ]); + let context = createContext("https://" + search, { isPrivate: false }); + await check_results({ + context, + autofilled: "https://" + url, + completed: "https://" + url, + matches: [ + makeVisitResult(context, { + uri: "https://" + url, + title: "https://" + title, + heuristic: true, + }), + ], + }); + await cleanup(); +}); + +// "https://ex" should match https://www.example.com/. +add_autofill_task(async function httpsPrefixNoWWWShouldMatchWWW() { + await PlacesTestUtils.addVisits([ + { + uri: "https://www." + url, + }, + ]); + let context = createContext("https://" + search, { isPrivate: false }); + await check_results({ + context, + autofilled: "https://" + url, + completed: "https://www." + url, + matches: [ + makeVisitResult(context, { + uri: "https://www." + url, + title: "https://www." + title, + heuristic: true, + }), + ], + }); + await cleanup(); +}); + +// "https://www.ex" should *not* match https://example.com/. +add_autofill_task(async function httpsPrefixWWWShouldNotMatchNoWWW() { + await PlacesTestUtils.addVisits([ + { + uri: "https://" + url, + }, + ]); + let context = createContext("https://www." + search, { isPrivate: false }); + let prefixedUrl = origins + ? `https://www.${search}/` + : `https://www.${search}`; + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: prefixedUrl, + title: prefixedUrl, + heuristic: true, + iconUri: origins ? "" : `page-icon:https://www.${host}/`, + providerame: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + ], + }); + await cleanup(); +}); + +// "https://ex" should *not* match http://example.com/. +add_autofill_task(async function httpsPrefixShouldNotMatchHTTP() { + await PlacesTestUtils.addVisits([ + { + uri: "http://" + url, + }, + ]); + let context = createContext("https://" + search, { isPrivate: false }); + let prefixedUrl = origins ? `https://${search}/` : `https://${search}`; + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: prefixedUrl, + title: prefixedUrl, + heuristic: true, + iconUri: origins ? "" : `page-icon:https://${host}/`, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + makeVisitResult(context, { + uri: "http://" + url, + title: "test visit for http://" + url, + providerName: UNIFIEDCOMPLETE_PROVIDERNAME, + }), + ], + }); + await cleanup(); +}); + +// "https://ex" should *not* match http://example.com/, even if the latter is +// more frecent and both could be autofilled. +add_autofill_task(async function httpsPrefixShouldNotMatchMoreFrecentHTTP() { + await PlacesTestUtils.addVisits([ + { + uri: "http://" + url, + transition: PlacesUtils.history.TRANSITIONS.TYPED, + }, + { + uri: "http://" + url, + }, + { + uri: "https://" + url, + transition: PlacesUtils.history.TRANSITIONS.TYPED, + }, + { + uri: "http://otherpage", + }, + ]); + let context = createContext("https://" + search, { isPrivate: false }); + await check_results({ + context, + autofilled: "https://" + url, + completed: "https://" + url, + matches: [ + makeVisitResult(context, { + uri: "https://" + url, + title: "https://" + title, + heuristic: true, + }), + ], + }); + await cleanup(); +}); + +// Autofill should respond to frecency changes. +add_autofill_task(async function frecency() { + // Start with an http visit. It should be completed. + await PlacesTestUtils.addVisits([ + { + uri: "http://" + url, + }, + ]); + let context = createContext(search, { isPrivate: false }); + await check_results({ + context, + autofilled: url, + completed: "http://" + url, + matches: [ + makeVisitResult(context, { + uri: "http://" + url, + title, + heuristic: true, + }), + ], + }); + + // Add two https visits. https should now be completed. + for (let i = 0; i < 2; i++) { + await PlacesTestUtils.addVisits([{ uri: "https://" + url }]); + } + context = createContext(search, { isPrivate: false }); + await check_results({ + context, + autofilled: url, + completed: "https://" + url, + matches: [ + makeVisitResult(context, { + uri: "https://" + url, + title: "https://" + title, + heuristic: true, + }), + ], + }); + + // Add two more http visits, three total. http should now be completed + // again. + for (let i = 0; i < 2; i++) { + await PlacesTestUtils.addVisits([{ uri: "http://" + url }]); + } + context = createContext(search, { isPrivate: false }); + await check_results({ + context, + autofilled: url, + completed: "http://" + url, + matches: [ + makeVisitResult(context, { + uri: "http://" + url, + title, + heuristic: true, + }), + makeVisitResult(context, { + uri: "https://" + url, + title: "test visit for https://" + url, + providerName: UNIFIEDCOMPLETE_PROVIDERNAME, + }), + ], + }); + + // Add four www https visits. www https should now be completed. + for (let i = 0; i < 4; i++) { + await PlacesTestUtils.addVisits([{ uri: "https://www." + url }]); + } + context = createContext(search, { isPrivate: false }); + await check_results({ + context, + autofilled: url, + completed: "https://www." + url, + matches: [ + makeVisitResult(context, { + uri: "https://www." + url, + title: "https://www." + title, + heuristic: true, + }), + makeVisitResult(context, { + uri: "https://" + url, + title: "test visit for https://" + url, + providerName: UNIFIEDCOMPLETE_PROVIDERNAME, + }), + ], + }); + + // Remove the www https page. + await PlacesUtils.history.remove(["https://www." + url]); + + // http should now be completed again. + context = createContext(search, { isPrivate: false }); + await check_results({ + context, + autofilled: url, + completed: "http://" + url, + matches: [ + makeVisitResult(context, { + uri: "http://" + url, + title, + heuristic: true, + }), + makeVisitResult(context, { + uri: "https://" + url, + title: "test visit for https://" + url, + providerName: UNIFIEDCOMPLETE_PROVIDERNAME, + }), + ], + }); + + // Remove the http page. + await PlacesUtils.history.remove(["http://" + url]); + + // https should now be completed again. + context = createContext(search, { isPrivate: false }); + await check_results({ + context, + autofilled: url, + completed: "https://" + url, + matches: [ + makeVisitResult(context, { + uri: "https://" + url, + title: "https://" + title, + heuristic: true, + }), + ], + }); + + // Add a visit with a different host so that "ex" doesn't autofill it. + // https://example.com/ should still have a higher frecency though, so it + // should still be autofilled. + await PlacesTestUtils.addVisits([{ uri: "https://not-" + url }]); + context = createContext(search, { isPrivate: false }); + await check_results({ + context, + autofilled: url, + completed: "https://" + url, + matches: [ + makeVisitResult(context, { + uri: "https://" + url, + title: "https://" + title, + heuristic: true, + }), + makeVisitResult(context, { + uri: "https://not-" + url, + title: "test visit for https://not-" + url, + providerName: UNIFIEDCOMPLETE_PROVIDERNAME, + }), + ], + }); + + // Now add 10 more visits to the different host so that the frecency of + // https://example.com/ falls below the autofill threshold. It should not + // be autofilled now. + for (let i = 0; i < 10; i++) { + await PlacesTestUtils.addVisits([{ uri: "https://not-" + url }]); + } + + // In the `origins` case, the failure to make an autofill + // match means UnifiedComplete should not create a heuristic result. In the + // `!origins` case, autofill should still happen since there's no threshold + // comparison. + context = createContext(search, { isPrivate: false }); + if (origins) { + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: ENGINE_NAME, + heuristic: true, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + makeVisitResult(context, { + uri: "https://not-" + url, + title: "test visit for https://not-" + url, + providerName: UNIFIEDCOMPLETE_PROVIDERNAME, + }), + makeVisitResult(context, { + uri: "https://" + url, + title: "test visit for https://" + url, + providerName: UNIFIEDCOMPLETE_PROVIDERNAME, + }), + ], + }); + } else { + await check_results({ + context, + autofilled: url, + completed: "https://" + url, + matches: [ + makeVisitResult(context, { + uri: "https://" + url, + title: "https://" + title, + heuristic: true, + }), + makeVisitResult(context, { + uri: "https://not-" + url, + title: "test visit for https://not-" + url, + providerName: UNIFIEDCOMPLETE_PROVIDERNAME, + }), + ], + }); + } + + // Remove the visits to the different host. + await PlacesUtils.history.remove(["https://not-" + url]); + + // https should be completed again. + context = createContext(search, { isPrivate: false }); + await check_results({ + context, + autofilled: url, + completed: "https://" + url, + matches: [ + makeVisitResult(context, { + uri: "https://" + url, + title: "https://" + title, + heuristic: true, + }), + ], + }); + + // Remove the https visits. + await PlacesUtils.history.remove(["https://" + url]); + + // Now nothing should be completed. + context = createContext(search, { isPrivate: false }); + if (origins) { + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: ENGINE_NAME, + heuristic: true, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + ], + }); + } else { + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: "http://" + search, + title: "http://" + search, + iconUri: `page-icon:http://${host}/`, + heuristic: true, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + ], + }); + } + + await cleanup(); +}); + +// Bookmarked places should always be autofilled, even when they don't meet +// the threshold. +add_autofill_task(async function bookmarkBelowThreshold() { + // Add some visits to a URL so that the origin autofill threshold is large. + for (let i = 0; i < 3; i++) { + await PlacesTestUtils.addVisits([ + { + uri: "http://not-" + url, + }, + ]); + } + + // Now bookmark another URL. + await PlacesTestUtils.addBookmarkWithDetails({ + uri: "http://" + url, + }); + + // Make sure the bookmarked origin and place frecencies are below the + // threshold so that the origin/URL otherwise would not be autofilled. + let placeFrecency = await PlacesTestUtils.fieldInDB( + "http://" + url, + "frecency" + ); + let originFrecency = await getOriginFrecency("http://", host); + let threshold = await getOriginAutofillThreshold(); + Assert.ok( + placeFrecency < threshold, + `Place frecency should be below the threshold: ` + + `placeFrecency=${placeFrecency} threshold=${threshold}` + ); + Assert.ok( + originFrecency < threshold, + `Origin frecency should be below the threshold: ` + + `originFrecency=${originFrecency} threshold=${threshold}` + ); + + // The bookmark should be autofilled. + let context = createContext(search, { isPrivate: false }); + await check_results({ + context, + autofilled: url, + completed: "http://" + url, + matches: [ + makeVisitResult(context, { + uri: "http://" + url, + title, + heuristic: true, + }), + makeVisitResult(context, { + uri: "http://not-" + url, + title: "test visit for http://not-" + url, + providerName: UNIFIEDCOMPLETE_PROVIDERNAME, + }), + ], + }); + + await cleanup(); +}); + +// Bookmarked places should be autofilled when they *do* meet the threshold. +add_autofill_task(async function bookmarkAboveThreshold() { + // Bookmark a URL. + await PlacesTestUtils.addBookmarkWithDetails({ + uri: "http://" + url, + }); + + // The frecencies of the place and origin should be >= the threshold. In + // fact they should be the same as the threshold since the place is the only + // place in the database. + let placeFrecency = await PlacesTestUtils.fieldInDB( + "http://" + url, + "frecency" + ); + let originFrecency = await getOriginFrecency("http://", host); + let threshold = await getOriginAutofillThreshold(); + Assert.equal(placeFrecency, threshold); + Assert.equal(originFrecency, threshold); + + // The bookmark should be autofilled. + let context = createContext(search, { isPrivate: false }); + await check_results({ + context, + autofilled: url, + completed: "http://" + url, + matches: [ + makeVisitResult(context, { + uri: "http://" + url, + title, + heuristic: true, + }), + ], + }); + + await cleanup(); +}); + +// Bookmark a page and then clear history. The bookmarked origin/URL should +// be autofilled even though its frecency is <= 0 since the autofill threshold +// is 0. +add_autofill_task(async function zeroThreshold() { + await PlacesTestUtils.addBookmarkWithDetails({ + uri: "http://" + url, + }); + + await PlacesUtils.history.clear(); + + // Make sure the place's frecency is <= 0. (It will be reset to -1 on the + // history.clear() above, and then on idle it will be reset to 0. xpcshell + // tests disable the idle service, so in practice it should always be -1, + // but in order to avoid possible intermittent failures in the future, don't + // assume that.) + let placeFrecency = await PlacesTestUtils.fieldInDB( + "http://" + url, + "frecency" + ); + Assert.ok(placeFrecency <= 0); + + // Make sure the origin's frecency is 0. + let originFrecency = await getOriginFrecency("http://", host); + Assert.equal(originFrecency, 0); + + // Make sure the autofill threshold is 0. + let threshold = await getOriginAutofillThreshold(); + Assert.equal(threshold, 0); + + let context = createContext(search, { isPrivate: false }); + await check_results({ + context, + autofilled: url, + completed: "http://" + url, + matches: [ + makeVisitResult(context, { + uri: "http://" + url, + title, + heuristic: true, + }), + ], + }); + + await cleanup(); +}); + +// Tests interaction between the suggest.history and suggest.bookmark prefs. +// +// Config: +// suggest.history = false +// suggest.bookmark = true +// search for: visit +// prefix search: no +// prefix matches search: n/a +// origin matches search: yes +// +// Expected result: +// should autofill: no +add_autofill_task(async function suggestHistoryFalse_visit() { + await PlacesTestUtils.addVisits("http://" + url); + let context = createContext(search, { isPrivate: false }); + await check_results({ + context, + autofilled: url, + completed: "http://" + url, + matches: [ + makeVisitResult(context, { + uri: "http://" + url, + title, + heuristic: true, + }), + ], + }); + Services.prefs.setBoolPref("browser.urlbar.suggest.history", false); + context = createContext(search, { isPrivate: false }); + if (origins) { + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: ENGINE_NAME, + heuristic: true, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + ], + }); + } else { + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: "http://" + search, + title: "http://" + search, + iconUri: `page-icon:http://${host}/`, + heuristic: true, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + ], + }); + } + await cleanup(); +}); + +// Tests interaction between the suggest.history and suggest.bookmark prefs. +// +// Config: +// suggest.history = false +// suggest.bookmark = true +// search for: visit +// prefix search: yes +// prefix matches search: yes +// origin matches search: yes +// +// Expected result: +// should autofill: no +add_autofill_task(async function suggestHistoryFalse_visit_prefix() { + await PlacesTestUtils.addVisits("http://" + url); + let context = createContext("http://" + search, { isPrivate: false }); + await check_results({ + context, + autofilled: "http://" + url, + completed: "http://" + url, + matches: [ + makeVisitResult(context, { + uri: "http://" + url, + title, + heuristic: true, + }), + ], + }); + Services.prefs.setBoolPref("browser.urlbar.suggest.history", false); + context = createContext(search, { isPrivate: false }); + if (origins) { + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: ENGINE_NAME, + heuristic: true, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + ], + }); + } else { + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: "http://" + search, + title: "http://" + search, + iconUri: `page-icon:http://${host}/`, + heuristic: true, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + ], + }); + } + await cleanup(); +}); + +// Tests interaction between the suggest.history and suggest.bookmark prefs. +// +// Config: +// suggest.history = false +// suggest.bookmark = true +// search for: bookmark +// prefix search: no +// prefix matches search: n/a +// origin matches search: yes +// +// Expected result: +// should autofill: yes +add_autofill_task(async function suggestHistoryFalse_bookmark_0() { + // Add the bookmark. + await PlacesTestUtils.addBookmarkWithDetails({ + uri: "http://" + url, + }); + + // Make the bookmark fall below the autofill frecency threshold so we ensure + // the bookmark is always autofilled in this case, even if it doesn't meet + // the threshold. + let meetsThreshold = true; + while (meetsThreshold) { + // Add a visit to another origin to boost the threshold. + await PlacesTestUtils.addVisits("http://foo-" + url); + let originFrecency = await getOriginFrecency("http://", host); + let threshold = await getOriginAutofillThreshold(); + meetsThreshold = threshold <= originFrecency; + } + + // At this point, the bookmark doesn't meet the threshold, but it should + // still be autofilled. + let originFrecency = await getOriginFrecency("http://", host); + let threshold = await getOriginAutofillThreshold(); + Assert.ok(originFrecency < threshold); + + Services.prefs.setBoolPref("browser.urlbar.suggest.history", false); + let context = createContext(search, { isPrivate: false }); + await check_results({ + context, + autofilled: url, + completed: "http://" + url, + matches: [ + makeVisitResult(context, { + uri: "http://" + url, + title, + heuristic: true, + }), + ], + }); + await cleanup(); +}); + +// Tests interaction between the suggest.history and suggest.bookmark prefs. +// +// Config: +// suggest.history = false +// suggest.bookmark = true +// search for: bookmark +// prefix search: no +// prefix matches search: n/a +// origin matches search: no +// +// Expected result: +// should autofill: no +add_autofill_task(async function suggestHistoryFalse_bookmark_1() { + Services.prefs.setBoolPref("browser.urlbar.suggest.history", false); + await PlacesTestUtils.addBookmarkWithDetails({ + uri: "http://non-matching-" + url, + }); + let context = createContext(search, { isPrivate: false }); + let matches = [ + makeBookmarkResult(context, { + uri: "http://non-matching-" + url, + title: "A bookmark", + }), + ]; + if (origins) { + matches.unshift( + makeSearchResult(context, { + engineName: ENGINE_NAME, + heuristic: true, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }) + ); + } else { + matches.unshift( + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: "http://" + search, + title: "http://" + search, + iconUri: `page-icon:http://${host}/`, + heuristic: true, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }) + ); + } + await check_results({ + context, + matches, + }); + await cleanup(); +}); + +// Tests interaction between the suggest.history and suggest.bookmark prefs. +// +// Config: +// suggest.history = false +// suggest.bookmark = true +// search for: bookmark +// prefix search: yes +// prefix matches search: yes +// origin matches search: yes +// +// Expected result: +// should autofill: yes +add_autofill_task(async function suggestHistoryFalse_bookmark_prefix_0() { + // Add the bookmark. + await PlacesTestUtils.addBookmarkWithDetails({ + uri: "http://" + url, + }); + + // Make the bookmark fall below the autofill frecency threshold so we ensure + // the bookmark is always autofilled in this case, even if it doesn't meet + // the threshold. + let meetsThreshold = true; + while (meetsThreshold) { + // Add a visit to another origin to boost the threshold. + await PlacesTestUtils.addVisits("http://foo-" + url); + let originFrecency = await getOriginFrecency("http://", host); + let threshold = await getOriginAutofillThreshold(); + meetsThreshold = threshold <= originFrecency; + } + + // At this point, the bookmark doesn't meet the threshold, but it should + // still be autofilled. + let originFrecency = await getOriginFrecency("http://", host); + let threshold = await getOriginAutofillThreshold(); + Assert.ok(originFrecency < threshold); + + Services.prefs.setBoolPref("browser.urlbar.suggest.history", false); + await PlacesTestUtils.addBookmarkWithDetails({ + uri: "http://" + url, + }); + let context = createContext("http://" + search, { isPrivate: false }); + await check_results({ + context, + autofilled: "http://" + url, + completed: "http://" + url, + matches: [ + makeVisitResult(context, { + uri: "http://" + url, + title, + heuristic: true, + }), + ], + }); + await cleanup(); +}); + +// Tests interaction between the suggest.history and suggest.bookmark prefs. +// +// Config: +// suggest.history = false +// suggest.bookmark = true +// search for: bookmark +// prefix search: yes +// prefix matches search: no +// origin matches search: yes +// +// Expected result: +// should autofill: no +add_autofill_task(async function suggestHistoryFalse_bookmark_prefix_1() { + Services.prefs.setBoolPref("browser.urlbar.suggest.history", false); + await PlacesTestUtils.addBookmarkWithDetails({ + uri: "ftp://" + url, + }); + let context = createContext("http://" + search, { isPrivate: false }); + let prefixedUrl = origins ? `http://${search}/` : `http://${search}`; + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: prefixedUrl, + title: prefixedUrl, + heuristic: true, + iconUri: origins ? "" : `page-icon:http://${host}/`, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + makeBookmarkResult(context, { + uri: "ftp://" + url, + title: "A bookmark", + }), + ], + }); + await cleanup(); +}); + +// Tests interaction between the suggest.history and suggest.bookmark prefs. +// +// Config: +// suggest.history = false +// suggest.bookmark = true +// search for: bookmark +// prefix search: yes +// prefix matches search: yes +// origin matches search: no +// +// Expected result: +// should autofill: no +add_autofill_task(async function suggestHistoryFalse_bookmark_prefix_2() { + Services.prefs.setBoolPref("browser.urlbar.suggest.history", false); + await PlacesTestUtils.addBookmarkWithDetails({ + uri: "http://non-matching-" + url, + }); + let context = createContext("http://" + search, { isPrivate: false }); + let prefixedUrl = origins ? `http://${search}/` : `http://${search}`; + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: prefixedUrl, + title: prefixedUrl, + heuristic: true, + iconUri: origins ? "" : `page-icon:http://${host}/`, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + makeBookmarkResult(context, { + uri: "http://non-matching-" + url, + title: "A bookmark", + }), + ], + }); + await cleanup(); +}); + +// Tests interaction between the suggest.history and suggest.bookmark prefs. +// +// Config: +// suggest.history = false +// suggest.bookmark = true +// search for: bookmark +// prefix search: yes +// prefix matches search: no +// origin matches search: no +// +// Expected result: +// should autofill: no +add_autofill_task(async function suggestHistoryFalse_bookmark_prefix_3() { + Services.prefs.setBoolPref("browser.urlbar.suggest.history", false); + await PlacesTestUtils.addBookmarkWithDetails({ + uri: "ftp://non-matching-" + url, + }); + let context = createContext("http://" + search, { isPrivate: false }); + let prefixedUrl = origins ? `http://${search}/` : `http://${search}`; + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: prefixedUrl, + title: prefixedUrl, + heuristic: true, + iconUri: origins ? "" : `page-icon:http://${host}/`, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + makeBookmarkResult(context, { + uri: "ftp://non-matching-" + url, + title: "A bookmark", + }), + ], + }); + await cleanup(); +}); + +// Tests interaction between the suggest.history and suggest.bookmark prefs. +// +// Config: +// suggest.history = true +// suggest.bookmark = false +// search for: visit +// prefix search: no +// prefix matches search: n/a +// origin matches search: yes +// +// Expected result: +// should autofill: yes +add_autofill_task(async function suggestBookmarkFalse_visit_0() { + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false); + await PlacesTestUtils.addVisits("http://" + url); + let context = createContext(search, { isPrivate: false }); + await check_results({ + context, + autofilled: url, + completed: "http://" + url, + matches: [ + makeVisitResult(context, { + uri: "http://" + url, + title, + heuristic: true, + }), + ], + }); + await cleanup(); +}); + +// Tests interaction between the suggest.history and suggest.bookmark prefs. +// +// Config: +// suggest.history = true +// suggest.bookmark = false +// search for: visit +// prefix search: no +// prefix matches search: n/a +// origin matches search: no +// +// Expected result: +// should autofill: no +add_autofill_task(async function suggestBookmarkFalse_visit_1() { + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false); + await PlacesTestUtils.addVisits("http://non-matching-" + url); + let context = createContext(search, { isPrivate: false }); + let prefixedUrl = origins ? `http://${search}/` : `http://${search}`; + let matches = [ + makeVisitResult(context, { + uri: "http://non-matching-" + url, + title: "test visit for http://non-matching-" + url, + providerName: UNIFIEDCOMPLETE_PROVIDERNAME, + }), + ]; + if (origins) { + matches.unshift( + makeSearchResult(context, { + engineName: ENGINE_NAME, + heuristic: true, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }) + ); + } else { + matches.unshift( + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: prefixedUrl, + title: prefixedUrl, + heuristic: true, + iconUri: origins ? "" : `page-icon:http://${host}/`, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }) + ); + } + await check_results({ + context, + matches, + }); + await cleanup(); +}); + +// Tests interaction between the suggest.history and suggest.bookmark prefs. +// +// Config: +// suggest.history = true +// suggest.bookmark = false +// search for: visit +// prefix search: yes +// prefix matches search: yes +// origin matches search: yes +// +// Expected result: +// should autofill: yes +add_autofill_task(async function suggestBookmarkFalse_visit_prefix_0() { + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false); + await PlacesTestUtils.addVisits("http://" + url); + let context = createContext("http://" + search, { isPrivate: false }); + await check_results({ + context, + autofilled: "http://" + url, + completed: "http://" + url, + matches: [ + makeVisitResult(context, { + uri: "http://" + url, + title, + heuristic: true, + }), + ], + }); + await cleanup(); +}); + +// Tests interaction between the suggest.history and suggest.bookmark prefs. +// +// Config: +// suggest.history = true +// suggest.bookmark = false +// search for: visit +// prefix search: yes +// prefix matches search: no +// origin matches search: yes +// +// Expected result: +// should autofill: no +add_autofill_task(async function suggestBookmarkFalse_visit_prefix_1() { + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false); + await PlacesTestUtils.addVisits("ftp://" + url); + let context = createContext("http://" + search, { isPrivate: false }); + let prefixedUrl = origins ? `http://${search}/` : `http://${search}`; + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: prefixedUrl, + title: prefixedUrl, + heuristic: true, + iconUri: origins ? "" : `page-icon:http://${host}/`, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + makeVisitResult(context, { + uri: "ftp://" + url, + title: "test visit for ftp://" + url, + providerName: UNIFIEDCOMPLETE_PROVIDERNAME, + }), + ], + }); + await cleanup(); +}); + +// Tests interaction between the suggest.history and suggest.bookmark prefs. +// +// Config: +// suggest.history = true +// suggest.bookmark = false +// search for: visit +// prefix search: yes +// prefix matches search: yes +// origin matches search: no +// +// Expected result: +// should autofill: no +add_autofill_task(async function suggestBookmarkFalse_visit_prefix_2() { + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false); + await PlacesTestUtils.addVisits("http://non-matching-" + url); + let context = createContext("http://" + search, { isPrivate: false }); + let prefixedUrl = origins ? `http://${search}/` : `http://${search}`; + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: prefixedUrl, + title: prefixedUrl, + heuristic: true, + iconUri: origins ? "" : `page-icon:http://${host}/`, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + makeVisitResult(context, { + uri: "http://non-matching-" + url, + title: "test visit for http://non-matching-" + url, + providerName: UNIFIEDCOMPLETE_PROVIDERNAME, + }), + ], + }); + await cleanup(); +}); + +// Tests interaction between the suggest.history and suggest.bookmark prefs. +// +// Config: +// suggest.history = true +// suggest.bookmark = false +// search for: visit +// prefix search: yes +// prefix matches search: no +// origin matches search: no +// +// Expected result: +// should autofill: no +add_autofill_task(async function suggestBookmarkFalse_visit_prefix_3() { + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false); + await PlacesTestUtils.addVisits("ftp://non-matching-" + url); + let context = createContext("http://" + search, { isPrivate: false }); + let prefixedUrl = origins ? `http://${search}/` : `http://${search}`; + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: prefixedUrl, + title: prefixedUrl, + heuristic: true, + iconUri: origins ? "" : `page-icon:http://${host}/`, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + makeVisitResult(context, { + uri: "ftp://non-matching-" + url, + title: "test visit for ftp://non-matching-" + url, + providerName: UNIFIEDCOMPLETE_PROVIDERNAME, + }), + ], + }); + await cleanup(); +}); + +// Tests interaction between the suggest.history and suggest.bookmark prefs. +// +// Config: +// suggest.history = true +// suggest.bookmark = false +// search for: unvisited bookmark +// prefix search: no +// prefix matches search: n/a +// origin matches search: yes +// +// Expected result: +// should autofill: no +add_autofill_task(async function suggestBookmarkFalse_unvisitedBookmark() { + await PlacesTestUtils.addBookmarkWithDetails({ + uri: "http://" + url, + }); + let context = createContext(search, { isPrivate: false }); + await check_results({ + context, + autofilled: url, + completed: "http://" + url, + matches: [ + makeVisitResult(context, { + uri: "http://" + url, + title, + heuristic: true, + }), + ], + }); + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false); + context = createContext(search, { isPrivate: false }); + if (origins) { + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: ENGINE_NAME, + heuristic: true, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + ], + }); + } else { + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: "http://" + search, + title: "http://" + search, + iconUri: `page-icon:http://${host}/`, + heuristic: true, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + ], + }); + } + await cleanup(); +}); + +// Tests interaction between the suggest.history and suggest.bookmark prefs. +// +// Config: +// suggest.history = true +// suggest.bookmark = false +// search for: unvisited bookmark +// prefix search: yes +// prefix matches search: yes +// origin matches search: yes +// +// Expected result: +// should autofill: no +add_autofill_task( + async function suggestBookmarkFalse_unvisitedBookmark_prefix_0() { + await PlacesTestUtils.addBookmarkWithDetails({ + uri: "http://" + url, + }); + let context = createContext("http://" + search, { isPrivate: false }); + await check_results({ + context, + autofilled: "http://" + url, + completed: "http://" + url, + matches: [ + makeVisitResult(context, { + uri: "http://" + url, + title, + heuristic: true, + }), + ], + }); + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false); + context = createContext("http://" + search, { isPrivate: false }); + let prefixedUrl = origins ? `http://${search}/` : `http://${search}`; + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: prefixedUrl, + title: prefixedUrl, + heuristic: true, + iconUri: origins ? "" : `page-icon:http://${host}/`, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + ], + }); + await cleanup(); + } +); + +// Tests interaction between the suggest.history and suggest.bookmark prefs. +// +// Config: +// suggest.history = true +// suggest.bookmark = false +// search for: unvisited bookmark +// prefix search: yes +// prefix matches search: no +// origin matches search: yes +// +// Expected result: +// should autofill: no +add_autofill_task( + async function suggestBookmarkFalse_unvisitedBookmark_prefix_1() { + await PlacesTestUtils.addBookmarkWithDetails({ + uri: "ftp://" + url, + }); + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false); + let context = createContext("http://" + search, { isPrivate: false }); + let prefixedUrl = origins ? `http://${search}/` : `http://${search}`; + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: prefixedUrl, + title: prefixedUrl, + heuristic: true, + iconUri: origins ? "" : `page-icon:http://${host}/`, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + ], + }); + await cleanup(); + } +); + +// Tests interaction between the suggest.history and suggest.bookmark prefs. +// +// Config: +// suggest.history = true +// suggest.bookmark = false +// search for: unvisited bookmark +// prefix search: yes +// prefix matches search: yes +// origin matches search: no +// +// Expected result: +// should autofill: no +add_autofill_task( + async function suggestBookmarkFalse_unvisitedBookmark_prefix_2() { + await PlacesTestUtils.addBookmarkWithDetails({ + uri: "http://non-matching-" + url, + }); + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false); + let context = createContext("http://" + search, { isPrivate: false }); + let prefixedUrl = origins ? `http://${search}/` : `http://${search}`; + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: prefixedUrl, + title: prefixedUrl, + heuristic: true, + iconUri: origins ? "" : `page-icon:http://${host}/`, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + ], + }); + await cleanup(); + } +); + +// Tests interaction between the suggest.history and suggest.bookmark prefs. +// +// Config: +// suggest.history = true +// suggest.bookmark = false +// search for: unvisited bookmark +// prefix search: yes +// prefix matches search: no +// origin matches search: no +// +// Expected result: +// should autofill: no +add_autofill_task( + async function suggestBookmarkFalse_unvisitedBookmark_prefix_3() { + await PlacesTestUtils.addBookmarkWithDetails({ + uri: "ftp://non-matching-" + url, + }); + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false); + let context = createContext("http://" + search, { isPrivate: false }); + let prefixedUrl = origins ? `http://${search}/` : `http://${search}`; + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: prefixedUrl, + title: prefixedUrl, + heuristic: true, + iconUri: origins ? "" : `page-icon:http://${host}/`, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + ], + }); + await cleanup(); + } +); + +// Tests interaction between the suggest.history and suggest.bookmark prefs. +// +// Config: +// suggest.history = true +// suggest.bookmark = false +// search for: visited bookmark above autofill threshold +// prefix search: no +// prefix matches search: n/a +// origin matches search: yes +// +// Expected result: +// should autofill: yes +add_autofill_task(async function suggestBookmarkFalse_visitedBookmark_above() { + await PlacesTestUtils.addVisits("http://" + url); + await PlacesTestUtils.addBookmarkWithDetails({ + uri: "http://" + url, + }); + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false); + let context = createContext(search, { isPrivate: false }); + await check_results({ + context, + autofilled: url, + completed: "http://" + url, + matches: [ + makeVisitResult(context, { + uri: "http://" + url, + title, + heuristic: true, + }), + ], + }); + await cleanup(); +}); + +// Tests interaction between the suggest.history and suggest.bookmark prefs. +// +// Config: +// suggest.history = true +// suggest.bookmark = false +// search for: visited bookmark above autofill threshold +// prefix search: yes +// prefix matches search: yes +// origin matches search: yes +// +// Expected result: +// should autofill: yes +add_autofill_task( + async function suggestBookmarkFalse_visitedBookmarkAbove_prefix_0() { + await PlacesTestUtils.addVisits("http://" + url); + await PlacesTestUtils.addBookmarkWithDetails({ + uri: "http://" + url, + }); + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false); + let context = createContext("http://" + search, { isPrivate: false }); + await check_results({ + context, + autofilled: "http://" + url, + completed: "http://" + url, + matches: [ + makeVisitResult(context, { + uri: "http://" + url, + title, + heuristic: true, + }), + ], + }); + await cleanup(); + } +); + +// Tests interaction between the suggest.history and suggest.bookmark prefs. +// +// Config: +// suggest.history = true +// suggest.bookmark = false +// search for: visited bookmark above autofill threshold +// prefix search: yes +// prefix matches search: no +// origin matches search: yes +// +// Expected result: +// should autofill: no +add_autofill_task( + async function suggestBookmarkFalse_visitedBookmarkAbove_prefix_1() { + await PlacesTestUtils.addVisits("ftp://" + url); + await PlacesTestUtils.addBookmarkWithDetails({ + uri: "ftp://" + url, + }); + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false); + let context = createContext("http://" + search, { isPrivate: false }); + let prefixedUrl = origins ? `http://${search}/` : `http://${search}`; + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: prefixedUrl, + title: prefixedUrl, + heuristic: true, + iconUri: origins ? "" : `page-icon:http://${host}/`, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + makeBookmarkResult(context, { + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + uri: "ftp://" + url, + title: "A bookmark", + }), + ], + }); + await cleanup(); + } +); + +// Tests interaction between the suggest.history and suggest.bookmark prefs. +// +// Config: +// suggest.history = true +// suggest.bookmark = false +// search for: visited bookmark above autofill threshold +// prefix search: yes +// prefix matches search: yes +// origin matches search: no +// +// Expected result: +// should autofill: no +add_autofill_task( + async function suggestBookmarkFalse_visitedBookmarkAbove_prefix_2() { + await PlacesTestUtils.addVisits("http://non-matching-" + url); + await PlacesTestUtils.addBookmarkWithDetails({ + uri: "http://non-matching-" + url, + }); + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false); + let context = createContext("http://" + search, { isPrivate: false }); + let prefixedUrl = origins ? `http://${search}/` : `http://${search}`; + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: prefixedUrl, + title: prefixedUrl, + heuristic: true, + iconUri: origins ? "" : `page-icon:http://${host}/`, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + makeBookmarkResult(context, { + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + uri: "http://non-matching-" + url, + title: "A bookmark", + }), + ], + }); + await cleanup(); + } +); + +// Tests interaction between the suggest.history and suggest.bookmark prefs. +// +// Config: +// suggest.history = true +// suggest.bookmark = false +// search for: visited bookmark above autofill threshold +// prefix search: yes +// prefix matches search: no +// origin matches search: no +// +// Expected result: +// should autofill: no +add_autofill_task( + async function suggestBookmarkFalse_visitedBookmarkAbove_prefix_3() { + await PlacesTestUtils.addVisits("ftp://non-matching-" + url); + await PlacesTestUtils.addBookmarkWithDetails({ + uri: "ftp://non-matching-" + url, + }); + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false); + let context = createContext("http://" + search, { isPrivate: false }); + let prefixedUrl = origins ? `http://${search}/` : `http://${search}`; + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: prefixedUrl, + title: prefixedUrl, + heuristic: true, + iconUri: origins ? "" : `page-icon:http://${host}/`, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + makeBookmarkResult(context, { + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + uri: "ftp://non-matching-" + url, + title: "A bookmark", + }), + ], + }); + await cleanup(); + } +); + +// The following suggestBookmarkFalse_visitedBookmarkBelow* tests are similar +// to the suggestBookmarkFalse_visitedBookmarkAbove* tests, but instead of +// checking visited bookmarks above the autofill threshold, they check visited +// bookmarks below the threshold. These tests don't make sense for URL +// queries (as opposed to origin queries) because URL queries don't use the +// same autofill threshold, so we skip them when !origins. + +// Tests interaction between the suggest.history and suggest.bookmark prefs. +// +// Config: +// suggest.history = true +// suggest.bookmark = false +// search for: visited bookmark below autofill threshold +// prefix search: no +// prefix matches search: n/a +// origin matches search: yes +// +// Expected result: +// should autofill: no +add_autofill_task(async function suggestBookmarkFalse_visitedBookmarkBelow() { + if (!origins) { + // See comment above suggestBookmarkFalse_visitedBookmarkBelow. + return; + } + // First, make sure that `url` is below the autofill threshold. + await PlacesTestUtils.addVisits("http://" + url); + for (let i = 0; i < 3; i++) { + await PlacesTestUtils.addVisits("http://some-other-" + url); + } + let context = createContext(search, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: ENGINE_NAME, + heuristic: true, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + makeVisitResult(context, { + uri: "http://some-other-" + url, + title: "test visit for http://some-other-" + url, + providerName: UNIFIEDCOMPLETE_PROVIDERNAME, + }), + makeVisitResult(context, { + uri: "http://" + url, + title: "test visit for http://" + url, + providerName: UNIFIEDCOMPLETE_PROVIDERNAME, + }), + ], + }); + // Now bookmark it and set suggest.bookmark to false. + await PlacesTestUtils.addBookmarkWithDetails({ + uri: "http://" + url, + }); + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false); + context = createContext(search, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: ENGINE_NAME, + heuristic: true, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + makeVisitResult(context, { + uri: "http://some-other-" + url, + title: "test visit for http://some-other-" + url, + providerName: UNIFIEDCOMPLETE_PROVIDERNAME, + }), + makeVisitResult(context, { + uri: "http://" + url, + title: "A bookmark", + providerName: UNIFIEDCOMPLETE_PROVIDERNAME, + }), + ], + }); + await cleanup(); +}); + +// Tests interaction between the suggest.history and suggest.bookmark prefs. +// +// Config: +// suggest.history = true +// suggest.bookmark = false +// search for: visited bookmark below autofill threshold +// prefix search: yes +// prefix matches search: yes +// origin matches search: yes +// +// Expected result: +// should autofill: no +add_autofill_task( + async function suggestBookmarkFalse_visitedBookmarkBelow_prefix_0() { + if (!origins) { + // See comment above suggestBookmarkFalse_visitedBookmarkBelow. + return; + } + // First, make sure that `url` is below the autofill threshold. + await PlacesTestUtils.addVisits("http://" + url); + for (let i = 0; i < 3; i++) { + await PlacesTestUtils.addVisits("http://some-other-" + url); + } + let context = createContext("http://" + search, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: `http://${search}/`, + title: `http://${search}/`, + heuristic: true, + iconUri: "", + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + makeVisitResult(context, { + uri: "http://some-other-" + url, + title: "test visit for http://some-other-" + url, + providerName: UNIFIEDCOMPLETE_PROVIDERNAME, + }), + makeVisitResult(context, { + uri: "http://" + url, + title: "test visit for http://" + url, + providerName: UNIFIEDCOMPLETE_PROVIDERNAME, + }), + ], + }); + // Now bookmark it and set suggest.bookmark to false. + await PlacesTestUtils.addBookmarkWithDetails({ + uri: "http://" + url, + }); + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false); + context = createContext("http://" + search, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: `http://${search}/`, + title: `http://${search}/`, + heuristic: true, + iconUri: "", + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + makeVisitResult(context, { + uri: "http://some-other-" + url, + title: "test visit for http://some-other-" + url, + providerName: UNIFIEDCOMPLETE_PROVIDERNAME, + }), + makeVisitResult(context, { + uri: "http://" + url, + title: "A bookmark", + providerName: UNIFIEDCOMPLETE_PROVIDERNAME, + }), + ], + }); + await cleanup(); + } +); + +// Tests interaction between the suggest.history and suggest.bookmark prefs. +// +// Config: +// suggest.history = true +// suggest.bookmark = false +// search for: visited bookmark below autofill threshold +// prefix search: yes +// prefix matches search: no +// origin matches search: yes +// +// Expected result: +// should autofill: no +add_autofill_task( + async function suggestBookmarkFalse_visitedBookmarkBelow_prefix_1() { + if (!origins) { + // See comment above suggestBookmarkFalse_visitedBookmarkBelow. + return; + } + // First, make sure that `url` is below the autofill threshold. + await PlacesTestUtils.addVisits("ftp://" + url); + for (let i = 0; i < 3; i++) { + await PlacesTestUtils.addVisits("ftp://some-other-" + url); + } + let context = createContext("http://" + search, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: `http://${search}/`, + title: `http://${search}/`, + heuristic: true, + iconUri: "", + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + makeVisitResult(context, { + uri: "ftp://some-other-" + url, + title: "test visit for ftp://some-other-" + url, + providerName: UNIFIEDCOMPLETE_PROVIDERNAME, + }), + makeVisitResult(context, { + uri: "ftp://" + url, + title: "test visit for ftp://" + url, + providerName: UNIFIEDCOMPLETE_PROVIDERNAME, + }), + ], + }); + // Now bookmark it and set suggest.bookmark to false. + await PlacesTestUtils.addBookmarkWithDetails({ + uri: "ftp://" + url, + }); + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false); + context = createContext("http://" + search, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: `http://${search}/`, + title: `http://${search}/`, + heuristic: true, + iconUri: "", + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + makeVisitResult(context, { + uri: "ftp://some-other-" + url, + title: "test visit for ftp://some-other-" + url, + providerName: UNIFIEDCOMPLETE_PROVIDERNAME, + }), + makeVisitResult(context, { + uri: "ftp://" + url, + title: "A bookmark", + providerName: UNIFIEDCOMPLETE_PROVIDERNAME, + }), + ], + }); + await cleanup(); + } +); + +// Tests interaction between the suggest.history and suggest.bookmark prefs. +// +// Config: +// suggest.history = true +// suggest.bookmark = false +// search for: visited bookmark below autofill threshold +// prefix search: yes +// prefix matches search: yes +// origin matches search: no +// +// Expected result: +// should autofill: no +add_autofill_task( + async function suggestBookmarkFalse_visitedBookmarkBelow_prefix_2() { + if (!origins) { + // See comment above suggestBookmarkFalse_visitedBookmarkBelow. + return; + } + // First, make sure that `url` is below the autofill threshold. + await PlacesTestUtils.addVisits("http://non-matching-" + url); + for (let i = 0; i < 3; i++) { + await PlacesTestUtils.addVisits("http://some-other-" + url); + } + let context = createContext("http://" + search, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: `http://${search}/`, + title: `http://${search}/`, + heuristic: true, + iconUri: "", + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + makeVisitResult(context, { + uri: "http://some-other-" + url, + title: "test visit for http://some-other-" + url, + providerName: UNIFIEDCOMPLETE_PROVIDERNAME, + }), + makeVisitResult(context, { + uri: "http://non-matching-" + url, + title: "test visit for http://non-matching-" + url, + providerName: UNIFIEDCOMPLETE_PROVIDERNAME, + }), + ], + }); + // Now bookmark it and set suggest.bookmark to false. + await PlacesTestUtils.addBookmarkWithDetails({ + uri: "http://non-matching-" + url, + }); + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false); + context = createContext("http://" + search, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: `http://${search}/`, + title: `http://${search}/`, + heuristic: true, + iconUri: "", + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + makeVisitResult(context, { + uri: "http://some-other-" + url, + title: "test visit for http://some-other-" + url, + providerName: UNIFIEDCOMPLETE_PROVIDERNAME, + }), + makeVisitResult(context, { + uri: "http://non-matching-" + url, + title: "A bookmark", + providerName: UNIFIEDCOMPLETE_PROVIDERNAME, + }), + ], + }); + await cleanup(); + } +); + +// Tests interaction between the suggest.history and suggest.bookmark prefs. +// +// Config: +// suggest.history = true +// suggest.bookmark = false +// search for: visited bookmark below autofill threshold +// prefix search: yes +// prefix matches search: no +// origin matches search: no +// +// Expected result: +// should autofill: no +add_autofill_task( + async function suggestBookmarkFalse_visitedBookmarkBelow_prefix_3() { + if (!origins) { + // See comment above suggestBookmarkFalse_visitedBookmarkBelow. + return; + } + // First, make sure that `url` is below the autofill threshold. + await PlacesTestUtils.addVisits("ftp://non-matching-" + url); + for (let i = 0; i < 3; i++) { + await PlacesTestUtils.addVisits("ftp://some-other-" + url); + } + let context = createContext("http://" + search, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: `http://${search}/`, + title: `http://${search}/`, + heuristic: true, + iconUri: "", + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + makeVisitResult(context, { + uri: "ftp://some-other-" + url, + title: "test visit for ftp://some-other-" + url, + providerName: UNIFIEDCOMPLETE_PROVIDERNAME, + }), + makeVisitResult(context, { + uri: "ftp://non-matching-" + url, + title: "test visit for ftp://non-matching-" + url, + providerName: UNIFIEDCOMPLETE_PROVIDERNAME, + }), + ], + }); + // Now bookmark it and set suggest.bookmark to false. + await PlacesTestUtils.addBookmarkWithDetails({ + uri: "ftp://non-matching-" + url, + }); + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false); + context = createContext("http://" + search, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: `http://${search}/`, + title: `http://${search}/`, + heuristic: true, + iconUri: "", + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + makeVisitResult(context, { + uri: "ftp://some-other-" + url, + title: "test visit for ftp://some-other-" + url, + providerName: UNIFIEDCOMPLETE_PROVIDERNAME, + }), + makeVisitResult(context, { + uri: "ftp://non-matching-" + url, + title: "A bookmark", + providerName: UNIFIEDCOMPLETE_PROVIDERNAME, + }), + ], + }); + await cleanup(); + } +); diff --git a/browser/components/urlbar/tests/unit/test_autofill_prefix_fallback.js b/browser/components/urlbar/tests/unit/test_autofill_prefix_fallback.js new file mode 100644 index 0000000000..ef3366d8db --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_autofill_prefix_fallback.js @@ -0,0 +1,74 @@ +/* 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/. */ + +// This tests autofill prefix fallback in case multiple origins have the same +// exact frecency. +// We should prefer https, or in case of other prefixes just sort by descending +// id. + +add_task(async function() { + registerCleanupFunction(async () => { + Services.prefs.clearUserPref("browser.urlbar.suggest.searches"); + }); + Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false); + + let host = "example.com"; + let prefixes = ["https://", "https://www.", "http://", "http://www."]; + for (let prefix of prefixes) { + await PlacesUtils.bookmarks.insert({ + url: `${prefix}${host}`, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + } + await checkOriginsOrder(host, prefixes); + + // The https://www version should be filled because it's https and the www + // version has been added later so it has an higher id. + let context = createContext("ex", { isPrivate: false }); + await check_results({ + context, + autofilled: `${host}/`, + completed: `https://www.${host}/`, + matches: [ + makeVisitResult(context, { + uri: `https://www.${host}/`, + title: `https://www.${host}`, + heuristic: true, + }), + makeBookmarkResult(context, { + uri: `https://${host}/`, + title: `${host}`, + }), + ], + }); + + // Remove and reinsert bookmarks in another order. + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); + prefixes = ["https://www.", "http://", "https://", "http://www."]; + for (let prefix of prefixes) { + await PlacesUtils.bookmarks.insert({ + url: `${prefix}${host}`, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + } + await checkOriginsOrder(host, prefixes); + + await check_results({ + context, + autofilled: `${host}/`, + completed: `https://${host}/`, + matches: [ + makeVisitResult(context, { + uri: `https://${host}/`, + title: `https://${host}`, + heuristic: true, + }), + makeBookmarkResult(context, { + uri: `https://www.${host}/`, + title: `www.${host}`, + }), + ], + }); +}); diff --git a/browser/components/urlbar/tests/unit/test_autofill_search_engine_aliases.js b/browser/components/urlbar/tests/unit/test_autofill_search_engine_aliases.js new file mode 100644 index 0000000000..9f87947770 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_autofill_search_engine_aliases.js @@ -0,0 +1,90 @@ +/* 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/. */ + +// Tests autofilling search engine token ("@") aliases. + +"use strict"; + +const TEST_ENGINE_NAME = "test autofill aliases"; +const TEST_ENGINE_ALIAS = "@autofilltest"; + +add_task(async function init() { + // Add an engine with an "@" alias. + await Services.search.addEngineWithDetails(TEST_ENGINE_NAME, { + alias: TEST_ENGINE_ALIAS, + template: "http://example.com/?search={searchTerms}", + }); + registerCleanupFunction(async () => { + let engine = Services.search.getEngineByName(TEST_ENGINE_NAME); + Assert.ok(engine); + await Services.search.removeEngine(engine); + }); +}); + +// Searching for @autofi should autofill to @autofilltest. +add_task(async function basic() { + // Add a history visit that should normally match but for the fact that the + // search uses an @ alias. When an @ alias is autofilled, there should be no + // other matches except the autofill heuristic match. + await PlacesTestUtils.addVisits({ + uri: "http://example.com/", + title: TEST_ENGINE_ALIAS, + }); + + let search = TEST_ENGINE_ALIAS.substr( + 0, + Math.round(TEST_ENGINE_ALIAS.length / 2) + ); + let autofilledValue = TEST_ENGINE_ALIAS + " "; + let context = createContext(search, { isPrivate: false }); + await check_results({ + context, + autofilled: autofilledValue, + matches: [ + makeSearchResult(context, { + engineName: TEST_ENGINE_NAME, + alias: TEST_ENGINE_ALIAS, + query: "", + providesSearchMode: true, + heuristic: false, + }), + ], + }); + await cleanupPlaces(); +}); + +// Searching for @AUTOFI should autofill to @AUTOFIlltest, preserving the case +// in the search string. +add_task(async function preserveCase() { + // Add a history visit that should normally match but for the fact that the + // search uses an @ alias. When an @ alias is autofilled, there should be no + // other matches except the autofill heuristic match. + await PlacesTestUtils.addVisits({ + uri: "http://example.com/", + title: TEST_ENGINE_ALIAS, + }); + + let search = TEST_ENGINE_ALIAS.toUpperCase().substr( + 0, + Math.round(TEST_ENGINE_ALIAS.length / 2) + ); + let alias = search + TEST_ENGINE_ALIAS.substr(search.length); + + let autofilledValue = alias + " "; + let context = createContext(search, { isPrivate: false }); + await check_results({ + context, + autofilled: autofilledValue, + matches: [ + makeSearchResult(context, { + engineName: TEST_ENGINE_NAME, + alias, + query: "", + providesSearchMode: true, + heuristic: false, + }), + ], + }); + await cleanupPlaces(); +}); diff --git a/browser/components/urlbar/tests/unit/test_autofill_search_engines.js b/browser/components/urlbar/tests/unit/test_autofill_search_engines.js new file mode 100644 index 0000000000..63af7115f2 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_autofill_search_engines.js @@ -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/. */ + +// The autoFill.searchEngines pref autofills the domains of engines registered +// with the search service. That's what this test checks. It's a different +// path in UnifiedComplete.js from normal moz_places autofill, which is tested +// in test_autofill_origins.js and test_autofill_urls.js. + +"use strict"; + +const ENGINE_NAME = "TestEngine"; + +add_task(async function searchEngines() { + Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true); + Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false); + Services.prefs.setBoolPref( + "browser.search.separatePrivateDefault.ui.enabled", + false + ); + + registerCleanupFunction(() => { + Services.prefs.clearUserPref("browser.urlbar.autoFill.searchEngines"); + Services.prefs.clearUserPref("browser.urlbar.suggest.searches"); + Services.prefs.clearUserPref( + "browser.search.separatePrivateDefault.ui.enabled" + ); + }); + + let schemes = ["http", "https"]; + for (let i = 0; i < schemes.length; i++) { + let scheme = schemes[i]; + let engine = await Services.search.addEngineWithDetails(ENGINE_NAME, { + method: "GET", + template: scheme + "://www.example.com/", + searchGetParams: "q={searchTerms}", + }); + + let context = createContext("ex", { isPrivate: false }); + await check_results({ + context, + autofilled: "example.com/", + matches: [ + makePrioritySearchResult(context, { + engineName: ENGINE_NAME, + heuristic: true, + }), + ], + }); + + context = createContext("example.com", { isPrivate: false }); + await check_results({ + context, + autofilled: "example.com/", + matches: [ + makePrioritySearchResult(context, { + engineName: ENGINE_NAME, + heuristic: true, + }), + ], + }); + + context = createContext("example.com/", { isPrivate: false }); + await check_results({ + context, + autofilled: "example.com/", + matches: [ + makePrioritySearchResult(context, { + engineName: ENGINE_NAME, + heuristic: true, + }), + ], + }); + + context = createContext("www.ex", { isPrivate: false }); + await check_results({ + context, + autofilled: "www.example.com/", + matches: [ + makePrioritySearchResult(context, { + engineName: ENGINE_NAME, + heuristic: true, + }), + ], + }); + + context = createContext("www.example.com", { isPrivate: false }); + await check_results({ + context, + autofilled: "www.example.com/", + matches: [ + makePrioritySearchResult(context, { + engineName: ENGINE_NAME, + heuristic: true, + }), + ], + }); + + context = createContext("www.example.com/", { isPrivate: false }); + await check_results({ + context, + autofilled: "www.example.com/", + matches: [ + makePrioritySearchResult(context, { + engineName: ENGINE_NAME, + heuristic: true, + }), + ], + }); + + context = createContext(scheme + "://ex", { isPrivate: false }); + await check_results({ + context, + autofilled: scheme + "://example.com/", + matches: [ + makePrioritySearchResult(context, { + engineName: ENGINE_NAME, + heuristic: true, + }), + ], + }); + + context = createContext(scheme + "://example.com", { isPrivate: false }); + await check_results({ + context, + autofilled: scheme + "://example.com/", + matches: [ + makePrioritySearchResult(context, { + engineName: ENGINE_NAME, + heuristic: true, + }), + ], + }); + + context = createContext(scheme + "://example.com/", { isPrivate: false }); + await check_results({ + context, + autofilled: scheme + "://example.com/", + matches: [ + makePrioritySearchResult(context, { + engineName: ENGINE_NAME, + heuristic: true, + }), + ], + }); + + context = createContext(scheme + "://www.ex", { isPrivate: false }); + await check_results({ + context, + autofilled: scheme + "://www.example.com/", + matches: [ + makePrioritySearchResult(context, { + engineName: ENGINE_NAME, + heuristic: true, + }), + ], + }); + + context = createContext(scheme + "://www.example.com", { + isPrivate: false, + }); + await check_results({ + context, + autofilled: scheme + "://www.example.com/", + matches: [ + makePrioritySearchResult(context, { + engineName: ENGINE_NAME, + heuristic: true, + }), + ], + }); + + context = createContext(scheme + "://www.example.com/", { + isPrivate: false, + }); + await check_results({ + context, + search: scheme + "://www.example.com/", + autofilled: scheme + "://www.example.com/", + matches: [ + makePrioritySearchResult(context, { + engineName: ENGINE_NAME, + heuristic: true, + }), + ], + }); + + // We should just get a normal heuristic result from HeuristicFallback for + // these queries. + let otherScheme = schemes[(i + 1) % schemes.length]; + context = createContext(otherScheme + "://ex", { isPrivate: false }); + await check_results({ + context, + search: otherScheme + "://ex", + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: otherScheme + "://ex/", + title: otherScheme + "://ex/", + heuristic: true, + }), + ], + }); + context = createContext(otherScheme + "://www.ex", { isPrivate: false }); + await check_results({ + context, + search: otherScheme + "://www.ex", + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: otherScheme + "://www.ex/", + title: otherScheme + "://www.ex/", + heuristic: true, + }), + ], + }); + + context = createContext("example/", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: "http://example/", + title: "http://example/", + iconUri: "page-icon:http://example/", + heuristic: true, + }), + ], + }); + + await Services.search.removeEngine(engine); + } +}); diff --git a/browser/components/urlbar/tests/unit/test_autofill_urls.js b/browser/components/urlbar/tests/unit/test_autofill_urls.js new file mode 100644 index 0000000000..d0424a4b8d --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_autofill_urls.js @@ -0,0 +1,218 @@ +/* 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"; + +const HEURISTIC_FALLBACK_PROVIDERNAME = "HeuristicFallback"; +const UNIFIEDCOMPLETE_PROVIDERNAME = "UnifiedComplete"; + +// "example.com/foo/" should match http://example.com/foo/. +testEngine_setup(); + +add_task(async function multipleSlashes() { + await PlacesTestUtils.addVisits([ + { + uri: "http://example.com/foo/", + }, + ]); + let context = createContext("example.com/foo/", { isPrivate: false }); + await check_results({ + context, + autofilled: "example.com/foo/", + completed: "http://example.com/foo/", + matches: [ + makeVisitResult(context, { + uri: "http://example.com/foo/", + title: "example.com/foo/", + heuristic: true, + }), + ], + }); + await cleanupPlaces(); +}); + +// "example.com:8888/f" should match http://example.com:8888/foo. +add_task(async function port() { + await PlacesTestUtils.addVisits([ + { + uri: "http://example.com:8888/foo", + }, + ]); + let context = createContext("example.com:8888/f", { isPrivate: false }); + await check_results({ + context, + autofilled: "example.com:8888/foo", + completed: "http://example.com:8888/foo", + matches: [ + makeVisitResult(context, { + uri: "http://example.com:8888/foo", + title: "example.com:8888/foo", + heuristic: true, + }), + ], + }); + await cleanupPlaces(); +}); + +// "example.com:8999/f" should *not* autofill http://example.com:8888/foo. +add_task(async function portNoMatch() { + await PlacesTestUtils.addVisits([ + { + uri: "http://example.com:8888/foo", + }, + ]); + let context = createContext("example.com:8999/f", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: "http://example.com:8999/f", + title: "http://example.com:8999/f", + iconUri: "page-icon:http://example.com:8999/", + heuristic: true, + providerName: HEURISTIC_FALLBACK_PROVIDERNAME, + }), + ], + }); + await cleanupPlaces(); +}); + +// autofill to the next slash +add_task(async function port() { + await PlacesTestUtils.addVisits([ + { + uri: "http://example.com:8888/foo/bar/baz", + }, + ]); + let context = createContext("example.com:8888/foo/b", { isPrivate: false }); + await check_results({ + context, + autofilled: "example.com:8888/foo/bar/", + completed: "http://example.com:8888/foo/bar/", + matches: [ + makeVisitResult(context, { + uri: "http://example.com:8888/foo/bar/", + title: "example.com:8888/foo/bar/", + heuristic: true, + }), + makeVisitResult(context, { + uri: "http://example.com:8888/foo/bar/baz", + title: "test visit for http://example.com:8888/foo/bar/baz", + tags: [], + providerName: UNIFIEDCOMPLETE_PROVIDERNAME, + }), + ], + }); + await cleanupPlaces(); +}); + +// autofill to the next slash, end of url +add_task(async function port() { + await PlacesTestUtils.addVisits([ + { + uri: "http://example.com:8888/foo/bar/baz", + }, + ]); + let context = createContext("example.com:8888/foo/bar/b", { + isPrivate: false, + }); + await check_results({ + context, + autofilled: "example.com:8888/foo/bar/baz", + completed: "http://example.com:8888/foo/bar/baz", + matches: [ + makeVisitResult(context, { + uri: "http://example.com:8888/foo/bar/baz", + title: "example.com:8888/foo/bar/baz", + heuristic: true, + }), + ], + }); + await cleanupPlaces(); +}); + +// autofill with case insensitive from history and bookmark. +add_task(async function caseInsensitiveFromHistoryAndBookmark() { + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", true); + Services.prefs.setBoolPref("browser.urlbar.suggest.history", true); + + await PlacesTestUtils.addVisits([ + { + uri: "http://example.com/foo", + }, + ]); + + await testCaseInsensitive(); + + Services.prefs.clearUserPref("browser.urlbar.suggest.bookmark"); + Services.prefs.clearUserPref("browser.urlbar.suggest.history"); + await cleanupPlaces(); +}); + +// autofill with case insensitive from history. +add_task(async function caseInsensitiveFromHistory() { + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false); + Services.prefs.setBoolPref("browser.urlbar.suggest.history", true); + + await PlacesTestUtils.addVisits([ + { + uri: "http://example.com/foo", + }, + ]); + + await testCaseInsensitive(); + + Services.prefs.clearUserPref("browser.urlbar.suggest.bookmark"); + Services.prefs.clearUserPref("browser.urlbar.suggest.history"); + await cleanupPlaces(); +}); + +// autofill with case insensitive from bookmark. +add_task(async function caseInsensitiveFromBookmark() { + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", true); + Services.prefs.setBoolPref("browser.urlbar.suggest.history", false); + + await PlacesTestUtils.addBookmarkWithDetails({ + uri: "http://example.com/foo", + }); + + await testCaseInsensitive(); + + Services.prefs.clearUserPref("browser.urlbar.suggest.bookmark"); + Services.prefs.clearUserPref("browser.urlbar.suggest.history"); + await cleanupPlaces(); +}); + +async function testCaseInsensitive() { + const testData = [ + { + input: "example.com/F", + expectedAutofill: "example.com/Foo", + }, + { + // Test with prefix. + input: "http://example.com/F", + expectedAutofill: "http://example.com/Foo", + }, + ]; + + for (const { input, expectedAutofill } of testData) { + const context = createContext(input, { + isPrivate: false, + }); + await check_results({ + context, + autofilled: expectedAutofill, + completed: "http://example.com/foo", + matches: [ + makeVisitResult(context, { + uri: "http://example.com/foo", + title: "example.com/foo", + heuristic: true, + }), + ], + }); + } +} diff --git a/browser/components/urlbar/tests/unit/test_avoid_middle_complete.js b/browser/components/urlbar/tests/unit/test_avoid_middle_complete.js new file mode 100644 index 0000000000..a6de9f3a3a --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_avoid_middle_complete.js @@ -0,0 +1,284 @@ +/* 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 ENGINE_NAME = "engine-suggestions.xml"; + +testEngine_setup(); + +add_task(async function test_prefix_space_noautofill() { + await PlacesTestUtils.addVisits({ + uri: Services.io.newURI("http://moz.org/test/"), + }); + + info("Should not try to autoFill if search string contains a space"); + let context = createContext(" mo", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: ENGINE_NAME, + query: " mo", + heuristic: true, + }), + makeVisitResult(context, { + uri: "http://moz.org/test/", + title: "test visit for http://moz.org/test/", + }), + ], + }); + + await cleanupPlaces(); +}); + +add_task(async function test_trailing_space_noautofill() { + await PlacesTestUtils.addVisits({ + uri: Services.io.newURI("http://moz.org/test/"), + }); + + info("Should not try to autoFill if search string contains a space"); + let context = createContext("mo ", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: ENGINE_NAME, + query: "mo ", + heuristic: true, + }), + makeVisitResult(context, { + uri: "http://moz.org/test/", + title: "test visit for http://moz.org/test/", + }), + ], + }); + + await cleanupPlaces(); +}); + +add_task(async function test_searchEngine_autofill() { + Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true); + let engine = await Services.search.addEngineWithDetails("CakeSearch", { + method: "GET", + template: "http://cake.search/", + searchGetParams: "q={searchTerms}", + }); + registerCleanupFunction(async () => Services.search.removeEngine(engine)); + + info( + "Should autoFill search engine if search string does not contains a space" + ); + let context = createContext("ca", { isPrivate: false }); + await check_results({ + context, + matches: [ + makePrioritySearchResult(context, { + engineName: "CakeSearch", + heuristic: true, + }), + ], + }); + + await cleanupPlaces(); +}); + +add_task(async function test_searchEngine_prefix_space_noautofill() { + Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true); + let engine = await Services.search.addEngineWithDetails("CupcakeSearch", { + method: "GET", + template: "http://cupcake.search/", + searchGetParams: "q={searchTerms}", + }); + registerCleanupFunction(async () => Services.search.removeEngine(engine)); + + info( + "Should not try to autoFill search engine if search string contains a space" + ); + let context = createContext(" cu", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: ENGINE_NAME, + query: " cu", + heuristic: true, + }), + ], + }); + + await cleanupPlaces(); +}); + +add_task(async function test_searchEngine_trailing_space_noautofill() { + Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true); + let engine = await Services.search.addEngineWithDetails("BaconSearch", { + method: "GET", + template: "http://bacon.search/", + searchGetParams: "q={searchTerms}", + }); + registerCleanupFunction(async () => Services.search.removeEngine(engine)); + + info( + "Should not try to autoFill search engine if search string contains a space" + ); + let context = createContext("ba ", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: ENGINE_NAME, + query: "ba ", + heuristic: true, + }), + ], + }); + + await cleanupPlaces(); +}); + +add_task(async function test_searchEngine_www_noautofill() { + Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true); + let engine = await Services.search.addEngineWithDetails("HamSearch", { + method: "GET", + template: "http://ham.search/", + searchGetParams: "q={searchTerms}", + }); + registerCleanupFunction(async () => Services.search.removeEngine(engine)); + + info( + "Should not autoFill search engine if search string contains www. but engine doesn't" + ); + let context = createContext("www.ham", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: "http://www.ham/", + title: "http://www.ham/", + displayUrl: "http://www.ham", + heuristic: true, + }), + makeSearchResult(context, { + engineName: ENGINE_NAME, + query: "www.ham", + }), + ], + }); + + await cleanupPlaces(); +}); + +add_task(async function test_searchEngine_different_scheme_noautofill() { + Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true); + let engine = await Services.search.addEngineWithDetails("PieSearch", { + method: "GET", + template: "https://pie.search/", + searchGetParams: "q={searchTerms}", + }); + registerCleanupFunction(async () => Services.search.removeEngine(engine)); + + info( + "Should not autoFill search engine if search string has a different scheme." + ); + let context = createContext("http://pie", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: "http://pie/", + title: "http://pie/", + iconUri: "", + heuristic: true, + }), + ], + }); + + await cleanupPlaces(); +}); + +add_task(async function test_searchEngine_matching_prefix_autofill() { + Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true); + let engine = await Services.search.addEngineWithDetails("BeanSearch", { + method: "GET", + template: "http://www.bean.search/", + searchGetParams: "q={searchTerms}", + }); + registerCleanupFunction(async () => Services.search.removeEngine(engine)); + + info("Should autoFill search engine if search string has matching prefix."); + let context = createContext("http://www.be", { isPrivate: false }); + await check_results({ + context, + autofilled: "http://www.bean.search/", + matches: [ + makePrioritySearchResult(context, { + engineName: "BeanSearch", + heuristic: true, + }), + ], + }); + + info("Should autoFill search engine if search string has www prefix."); + context = createContext("www.be", { isPrivate: false }); + await check_results({ + context, + autofilled: "www.bean.search/", + matches: [ + makePrioritySearchResult(context, { + engineName: "BeanSearch", + heuristic: true, + }), + ], + }); + + info("Should autoFill search engine if search string has matching scheme."); + context = createContext("http://be", { isPrivate: false }); + await check_results({ + context, + autofilled: "http://bean.search/", + matches: [ + makePrioritySearchResult(context, { + engineName: "BeanSearch", + heuristic: true, + }), + ], + }); + + await cleanupPlaces(); +}); + +add_task(async function test_prefix_autofill() { + await PlacesTestUtils.addVisits({ + uri: Services.io.newURI("http://mozilla.org/test/"), + }); + await PlacesTestUtils.addVisits({ + uri: Services.io.newURI("http://moz.org/test/"), + }); + + info( + "Should not try to autoFill in-the-middle if a search is canceled immediately" + ); + let context = createContext("mozi", { isPrivate: false }); + await check_results({ + context, + incompleteSearch: "moz", + autofilled: "mozilla.org/", + completed: "http://mozilla.org/", + matches: [ + makeVisitResult(context, { + uri: "http://mozilla.org/", + title: "mozilla.org", + heuristic: true, + }), + makeVisitResult(context, { + uri: "http://mozilla.org/test/", + title: "test visit for http://mozilla.org/test/", + providerName: "UnifiedComplete", + }), + ], + }); + + await cleanupPlaces(); +}); diff --git a/browser/components/urlbar/tests/unit/test_avoid_stripping_to_empty_tokens.js b/browser/components/urlbar/tests/unit/test_avoid_stripping_to_empty_tokens.js new file mode 100644 index 0000000000..b9e3227874 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_avoid_stripping_to_empty_tokens.js @@ -0,0 +1,121 @@ +/* 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 ENGINE_NAME = "engine-suggestions.xml"; + +testEngine_setup(); + +add_task(async function test_protocol_trimming() { + for (let prot of ["http", "https", "ftp"]) { + let visit = { + // Include the protocol in the query string to ensure we get matches (see bug 1059395) + uri: Services.io.newURI( + prot + + "://www.mozilla.org/test/?q=" + + prot + + encodeURIComponent("://") + + "www.foo" + ), + title: "Test title", + }; + await PlacesTestUtils.addVisits(visit); + + let input = prot + "://www."; + info("Searching for: " + input); + let context = createContext(input, { isPrivate: false }); + await check_results({ + context, + autofilled: prot + "://www.mozilla.org/", + completed: prot + "://www.mozilla.org/", + matches: [ + makeVisitResult(context, { + uri: prot + "://www.mozilla.org/", + title: + prot == "http" ? "www.mozilla.org" : prot + "://www.mozilla.org", + heuristic: true, + }), + makeVisitResult(context, { + uri: visit.uri.spec, + title: visit.title, + }), + ], + }); + + input = "www."; + info("Searching for: " + input); + context = createContext(input, { isPrivate: false }); + await check_results({ + context, + autofilled: "www.mozilla.org/", + completed: prot + "://www.mozilla.org/", + matches: [ + makeVisitResult(context, { + uri: prot + "://www.mozilla.org/", + title: + prot == "http" ? "www.mozilla.org" : prot + "://www.mozilla.org", + heuristic: true, + }), + makeVisitResult(context, { + uri: visit.uri.spec, + title: visit.title, + }), + ], + }); + + input = prot + "://www. "; + info("Searching for: " + input); + context = createContext(input, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: `${input.trim()}/`, + title: `${input.trim()}/`, + iconUri: "", + heuristic: true, + providerName: "HeuristicFallback", + }), + makeVisitResult(context, { + uri: visit.uri.spec, + title: visit.title, + providerName: "UnifiedComplete", + }), + ], + }); + + let inputs = [ + prot + "://", + prot + ":// ", + prot + ":// mo", + prot + "://mo te", + prot + "://www. mo", + prot + "://www.mo te", + "www. ", + "www. mo", + "www.mo te", + ]; + for (input of inputs) { + info("Searching for: " + input); + context = createContext(input, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: ENGINE_NAME, + query: input, + heuristic: true, + }), + makeVisitResult(context, { + uri: visit.uri.spec, + title: visit.title, + providerName: "UnifiedComplete", + }), + ], + }); + } + + await cleanupPlaces(); + } +}); diff --git a/browser/components/urlbar/tests/unit/test_casing.js b/browser/components/urlbar/tests/unit/test_casing.js new file mode 100644 index 0000000000..89e58c45a9 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_casing.js @@ -0,0 +1,356 @@ +/* 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 ENGINE_NAME = "engine-suggestions.xml"; +const AUTOFILL_PROVIDERNAME = "Autofill"; +const HEURISTIC_FALLBACK_PROVIDERNAME = "HeuristicFallback"; +const UNIFIEDCOMPLETE_PROVIDERNAME = "UnifiedComplete"; + +testEngine_setup(); + +add_task(async function test_casing_1() { + info("Searching for cased entry 1"); + await PlacesTestUtils.addVisits({ + uri: Services.io.newURI("http://mozilla.org/test/"), + }); + let context = createContext("MOZ", { isPrivate: false }); + await check_results({ + context, + autofilled: "MOZilla.org/", + completed: "http://mozilla.org/", + matches: [ + makeVisitResult(context, { + uri: "http://mozilla.org/", + title: "mozilla.org", + heuristic: true, + }), + makeVisitResult(context, { + uri: "http://mozilla.org/test/", + title: "test visit for http://mozilla.org/test/", + providerName: UNIFIEDCOMPLETE_PROVIDERNAME, + }), + ], + }); + await cleanupPlaces(); +}); + +add_task(async function test_casing_2() { + info("Searching for cased entry 2"); + await PlacesTestUtils.addVisits({ + uri: Services.io.newURI("http://mozilla.org/test/"), + }); + let context = createContext("mozilla.org/T", { isPrivate: false }); + await check_results({ + context, + autofilled: "mozilla.org/Test/", + completed: "http://mozilla.org/test/", + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + uri: "http://mozilla.org/test/", + title: "mozilla.org/test/", + iconUri: "page-icon:http://mozilla.org/test/", + heuristic: true, + providerName: AUTOFILL_PROVIDERNAME, + }), + ], + }); + await cleanupPlaces(); +}); + +add_task(async function test_casing_3() { + info("Searching for cased entry 3"); + await PlacesTestUtils.addVisits({ + uri: Services.io.newURI("http://mozilla.org/Test/"), + }); + let context = createContext("mozilla.org/T", { isPrivate: false }); + await check_results({ + context, + autofilled: "mozilla.org/Test/", + completed: "http://mozilla.org/Test/", + matches: [ + makeVisitResult(context, { + uri: "http://mozilla.org/Test/", + title: "mozilla.org/Test/", + heuristic: true, + }), + ], + }); + await cleanupPlaces(); +}); + +add_task(async function test_casing_4() { + info("Searching for cased entry 4"); + await PlacesTestUtils.addVisits({ + uri: Services.io.newURI("http://mozilla.org/Test/"), + }); + let context = createContext("mOzilla.org/t", { isPrivate: false }); + await check_results({ + context, + autofilled: "mOzilla.org/test/", + completed: "http://mozilla.org/Test/", + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + uri: "http://mozilla.org/Test/", + title: "mozilla.org/Test/", + iconUri: "page-icon:http://mozilla.org/Test/", + heuristic: true, + providerName: AUTOFILL_PROVIDERNAME, + }), + ], + }); + await cleanupPlaces(); +}); + +add_task(async function test_casing_5() { + info("Searching for cased entry 5"); + await PlacesTestUtils.addVisits({ + uri: Services.io.newURI("http://mozilla.org/Test/"), + }); + let context = createContext("mOzilla.org/T", { isPrivate: false }); + await check_results({ + context, + autofilled: "mOzilla.org/Test/", + completed: "http://mozilla.org/Test/", + matches: [ + makeVisitResult(context, { + uri: "http://mozilla.org/Test/", + title: "mozilla.org/Test/", + heuristic: true, + }), + ], + }); + await cleanupPlaces(); +}); + +add_task(async function test_untrimmed_casing() { + info("Searching for untrimmed cased entry"); + await PlacesTestUtils.addVisits({ + uri: Services.io.newURI("http://mozilla.org/Test/"), + }); + let context = createContext("http://mOz", { isPrivate: false }); + await check_results({ + context, + autofilled: "http://mOzilla.org/", + completed: "http://mozilla.org/", + matches: [ + makeVisitResult(context, { + uri: "http://mozilla.org/", + title: "mozilla.org", + heuristic: true, + }), + makeVisitResult(context, { + uri: "http://mozilla.org/Test/", + title: "test visit for http://mozilla.org/Test/", + providerName: UNIFIEDCOMPLETE_PROVIDERNAME, + }), + ], + }); + await cleanupPlaces(); +}); + +add_task(async function test_untrimmed_www_casing() { + info("Searching for untrimmed cased entry with www"); + await PlacesTestUtils.addVisits({ + uri: Services.io.newURI("http://www.mozilla.org/Test/"), + }); + let context = createContext("http://www.mOz", { isPrivate: false }); + await check_results({ + context, + autofilled: "http://www.mOzilla.org/", + completed: "http://www.mozilla.org/", + matches: [ + makeVisitResult(context, { + uri: "http://www.mozilla.org/", + title: "www.mozilla.org", + heuristic: true, + }), + makeVisitResult(context, { + uri: "http://www.mozilla.org/Test/", + title: "test visit for http://www.mozilla.org/Test/", + providerName: UNIFIEDCOMPLETE_PROVIDERNAME, + }), + ], + }); + await cleanupPlaces(); +}); + +add_task(async function test_untrimmed_path_casing() { + info("Searching for untrimmed cased entry with path"); + await PlacesTestUtils.addVisits({ + uri: Services.io.newURI("http://mozilla.org/Test/"), + }); + let context = createContext("http://mOzilla.org/t", { isPrivate: false }); + await check_results({ + context, + autofilled: "http://mOzilla.org/test/", + completed: "http://mozilla.org/Test/", + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + uri: "http://mozilla.org/Test/", + title: "mozilla.org/Test/", + iconUri: "page-icon:http://mozilla.org/Test/", + heuristic: true, + providerName: AUTOFILL_PROVIDERNAME, + }), + ], + }); + await cleanupPlaces(); +}); + +add_task(async function test_untrimmed_path_casing_2() { + info("Searching for untrimmed cased entry with path 2"); + await PlacesTestUtils.addVisits({ + uri: Services.io.newURI("http://mozilla.org/Test/"), + }); + let context = createContext("http://mOzilla.org/T", { isPrivate: false }); + await check_results({ + context, + autofilled: "http://mOzilla.org/Test/", + completed: "http://mozilla.org/Test/", + matches: [ + makeVisitResult(context, { + uri: "http://mozilla.org/Test/", + title: "mozilla.org/Test/", + heuristic: true, + }), + ], + }); + await cleanupPlaces(); +}); + +add_task(async function test_untrimmed_path_www_casing() { + info("Searching for untrimmed cased entry with www and path"); + await PlacesTestUtils.addVisits({ + uri: Services.io.newURI("http://www.mozilla.org/Test/"), + }); + let context = createContext("http://www.mOzilla.org/t", { isPrivate: false }); + await check_results({ + context, + autofilled: "http://www.mOzilla.org/test/", + completed: "http://www.mozilla.org/Test/", + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + uri: "http://www.mozilla.org/Test/", + title: "www.mozilla.org/Test/", + iconUri: "page-icon:http://www.mozilla.org/Test/", + heuristic: true, + providerName: AUTOFILL_PROVIDERNAME, + }), + ], + }); + await cleanupPlaces(); +}); + +add_task(async function test_untrimmed_path_www_casing_2() { + info("Searching for untrimmed cased entry with www and path 2"); + await PlacesTestUtils.addVisits({ + uri: Services.io.newURI("http://www.mozilla.org/Test/"), + }); + let context = createContext("http://www.mOzilla.org/T", { isPrivate: false }); + await check_results({ + context, + autofilled: "http://www.mOzilla.org/Test/", + completed: "http://www.mozilla.org/Test/", + matches: [ + makeVisitResult(context, { + uri: "http://www.mozilla.org/Test/", + title: "www.mozilla.org/Test/", + heuristic: true, + }), + ], + }); + await cleanupPlaces(); +}); + +add_task(async function test_searching() { + let uri1 = Services.io.newURI("http://dummy/1/"); + let uri2 = Services.io.newURI("http://dummy/2/"); + let uri3 = Services.io.newURI("http://dummy/3/"); + let uri4 = Services.io.newURI("http://dummy/4/"); + let uri5 = Services.io.newURI("http://dummy/5/"); + + await PlacesTestUtils.addVisits([ + { uri: uri1, title: "uppercase lambda \u039B" }, + { uri: uri2, title: "lowercase lambda \u03BB" }, + { uri: uri3, title: "symbol \u212A" }, // kelvin + { uri: uri4, title: "uppercase K" }, + { uri: uri5, title: "lowercase k" }, + ]); + + info("Search for lowercase lambda"); + let context = createContext("\u03BB", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + makeVisitResult(context, { + uri: uri2.spec, + title: "lowercase lambda \u03BB", + }), + makeVisitResult(context, { + uri: uri1.spec, + title: "uppercase lambda \u039B", + }), + ], + }); + + info("Search for uppercase lambda"); + context = createContext("\u039B", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + makeVisitResult(context, { + uri: uri2.spec, + title: "lowercase lambda \u03BB", + }), + makeVisitResult(context, { + uri: uri1.spec, + title: "uppercase lambda \u039B", + }), + ], + }); + + info("Search for kelvin sign"); + context = createContext("\u212A", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + makeVisitResult(context, { uri: uri5.spec, title: "lowercase k" }), + makeVisitResult(context, { uri: uri4.spec, title: "uppercase K" }), + makeVisitResult(context, { uri: uri3.spec, title: "symbol \u212A" }), + ], + }); + + info("Search for lowercase k"); + context = createContext("k", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + makeVisitResult(context, { uri: uri5.spec, title: "lowercase k" }), + makeVisitResult(context, { uri: uri4.spec, title: "uppercase K" }), + makeVisitResult(context, { uri: uri3.spec, title: "symbol \u212A" }), + ], + }); + + info("Search for uppercase k"); + context = createContext("K", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + makeVisitResult(context, { uri: uri5.spec, title: "lowercase k" }), + makeVisitResult(context, { uri: uri4.spec, title: "uppercase K" }), + makeVisitResult(context, { uri: uri3.spec, title: "symbol \u212A" }), + ], + }); + + await cleanupPlaces(); +}); diff --git a/browser/components/urlbar/tests/unit/test_dedupe_prefix.js b/browser/components/urlbar/tests/unit/test_dedupe_prefix.js new file mode 100644 index 0000000000..58f223fbc4 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_dedupe_prefix.js @@ -0,0 +1,116 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Testing that we dedupe results that have the same URL and title as another +// except for their prefix (e.g. http://www.). +add_task(async function dedupe_prefix() { + // We need to set the title or else we won't dedupe. We only dedupe when + // titles match up to mitigate deduping when the www. version of a site is + // completely different from it's www-less counterpart and thus presumably + // has a different title. + await PlacesTestUtils.addVisits([ + { + uri: "http://example.com/foo/", + title: "Example Page", + }, + { + uri: "http://www.example.com/foo/", + title: "Example Page", + }, + { + uri: "https://example.com/foo/", + title: "Example Page", + }, + { + uri: "https://www.example.com/foo/", + title: "Example Page", + }, + { + uri: "https://www.example.com/foo/", + title: "Example Page", + }, + ]); + + // We should get https://www. as the heuristic result but https:// in the + // results since the latter's prefix is a higher priority. + let context = createContext("example.com/foo/", { isPrivate: false }); + await check_results({ + context, + autofilled: "example.com/foo/", + completed: "https://www.example.com/foo/", + matches: [ + makeVisitResult(context, { + uri: "https://www.example.com/foo/", + title: "https://www.example.com/foo/", + heuristic: true, + }), + makeVisitResult(context, { + uri: "https://example.com/foo/", + title: "Example Page", + }), + ], + }); + + // Add more visits to the lowest-priority prefix. It should be the heuristic + // result but we should still show our highest-priority result. https://www. + // should not appear at all. + for (let i = 0; i < 3; i++) { + await PlacesTestUtils.addVisits([ + { + uri: "http://www.example.com/foo/", + title: "Example Page", + }, + ]); + } + + context = createContext("example.com/foo/", { isPrivate: false }); + await check_results({ + context, + autofilled: "example.com/foo/", + completed: "http://www.example.com/foo/", + matches: [ + makeVisitResult(context, { + uri: "http://www.example.com/foo/", + title: "www.example.com/foo/", + heuristic: true, + }), + makeVisitResult(context, { + uri: "https://example.com/foo/", + title: "Example Page", + }), + ], + }); + + // Add enough https:// vists for it to have the highest frecency. It should + // be the heuristic result. We should still get the https://www. result + // because we still show results with the same key and protocol if they differ + // from the heuristic result in having www. + for (let i = 0; i < 5; i++) { + await PlacesTestUtils.addVisits([ + { + uri: "https://example.com/foo/", + title: "Example Page", + }, + ]); + } + + context = createContext("example.com/foo/", { isPrivate: false }); + await check_results({ + context, + autofilled: "example.com/foo/", + completed: "https://example.com/foo/", + matches: [ + makeVisitResult(context, { + uri: "https://example.com/foo/", + title: "https://example.com/foo/", + heuristic: true, + }), + makeVisitResult(context, { + uri: "https://www.example.com/foo/", + title: "Example Page", + }), + ], + }); + + await cleanupPlaces(); +}); diff --git a/browser/components/urlbar/tests/unit/test_dupe_urls.js b/browser/components/urlbar/tests/unit/test_dupe_urls.js new file mode 100644 index 0000000000..9707233279 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_dupe_urls.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Ensure inline autocomplete doesn't return zero frecency pages. + +add_task(async function setup() { + registerCleanupFunction(async () => { + Services.prefs.clearUserPref("browser.urlbar.suggest.searches"); + }); + + Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false); +}); + +add_task(async function test_dupe_urls() { + info("Searching for urls with dupes should only show one"); + await PlacesTestUtils.addVisits( + { + uri: Services.io.newURI("http://mozilla.org/"), + }, + { + uri: Services.io.newURI("http://mozilla.org/?"), + } + ); + let context = createContext("moz", { isPrivate: false }); + await check_results({ + context, + autofilled: "mozilla.org/", + completed: "http://mozilla.org/", + matches: [ + makeVisitResult(context, { + uri: "http://mozilla.org/", + title: "mozilla.org", + heuristic: true, + }), + ], + }); + await cleanupPlaces(); +}); + +add_task(async function test_dupe_secure_urls() { + await PlacesTestUtils.addVisits( + { + uri: Services.io.newURI("https://example.org/"), + }, + { + uri: Services.io.newURI("https://example.org/?"), + } + ); + let context = createContext("exam", { isPrivate: false }); + await check_results({ + context, + autofilled: "example.org/", + completed: "https://example.org/", + matches: [ + makeVisitResult(context, { + uri: "https://example.org/", + title: "https://example.org", + heuristic: true, + }), + ], + }); + await cleanupPlaces(); +}); diff --git a/browser/components/urlbar/tests/unit/test_encoded_urls.js b/browser/components/urlbar/tests/unit/test_encoded_urls.js new file mode 100644 index 0000000000..5260683ddd --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_encoded_urls.js @@ -0,0 +1,97 @@ +add_task(async function test_encoded() { + info("Searching for over encoded url should not break it"); + let url = "https://www.mozilla.com/search/top/?q=%25%32%35"; + await PlacesTestUtils.addVisits({ + uri: Services.io.newURI(url), + title: url, + }); + let context = createContext(url, { isPrivate: false }); + await check_results({ + context, + autofilled: url, + completed: url, + matches: [ + makeVisitResult(context, { + uri: url, + title: url, + heuristic: true, + }), + ], + }); + await cleanupPlaces(); +}); + +add_task(async function test_encoded_trimmed() { + info("Searching for over encoded url should not break it"); + let url = "https://www.mozilla.com/search/top/?q=%25%32%35"; + await PlacesTestUtils.addVisits({ + uri: Services.io.newURI(url), + title: url, + }); + let context = createContext("mozilla.com/search/top/?q=%25%32%35", { + isPrivate: false, + }); + await check_results({ + context, + autofilled: "mozilla.com/search/top/?q=%25%32%35", + completed: url, + matches: [ + makeVisitResult(context, { + uri: url, + title: url, + heuristic: true, + }), + ], + }); + await cleanupPlaces(); +}); + +add_task(async function test_encoded_partial() { + info("Searching for over encoded url should not break it"); + let url = "https://www.mozilla.com/search/top/?q=%25%32%35"; + await PlacesTestUtils.addVisits({ + uri: Services.io.newURI(url), + title: url, + }); + let context = createContext("https://www.mozilla.com/search/top/?q=%25", { + isPrivate: false, + }); + await check_results({ + context, + autofilled: url, + completed: url, + matches: [ + makeVisitResult(context, { + uri: url, + title: url, + heuristic: true, + }), + ], + }); + await cleanupPlaces(); +}); + +add_task(async function test_encoded_path() { + info("Searching for over encoded url should not break it"); + let url = "https://www.mozilla.com/%25%32%35/top/"; + await PlacesTestUtils.addVisits({ + uri: Services.io.newURI(url), + title: url, + }); + let context = createContext("https://www.mozilla.com/%25%32%35/t", { + isPrivate: false, + }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + uri: url, + title: url, + heuristic: true, + }), + ], + autofilled: url, + completed: url, + }); + await cleanupPlaces(); +}); diff --git a/browser/components/urlbar/tests/unit/test_heuristic_cancel.js b/browser/components/urlbar/tests/unit/test_heuristic_cancel.js new file mode 100644 index 0000000000..8806fe0601 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_heuristic_cancel.js @@ -0,0 +1,136 @@ +/* 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/. */ + +/** + * Tests that old results from UrlbarProviderAutofill do not overwrite results + * from UrlbarProviderHeuristicFallback after the autofillable query is + * cancelled. See bug 1653436. + */ + +const { setTimeout } = ChromeUtils.import("resource://gre/modules/Timer.jsm"); + +/** + * A test provider that waits before returning results to simulate a slow DB + * lookup. + */ +class SlowHeuristicProvider extends TestProvider { + get type() { + return UrlbarUtils.PROVIDER_TYPE.HEURISTIC; + } + + async startQuery(context, add) { + this._context = context; + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 300)); + for (let result of this._results) { + add(this, result); + } + } +} + +/** + * A fast provider that alterts the test when it has added its results. + */ +class FastHeuristicProvider extends TestProvider { + get type() { + return UrlbarUtils.PROVIDER_TYPE.HEURISTIC; + } + + async startQuery(context, add) { + this._context = context; + for (let result of this._results) { + add(this, result); + } + Services.obs.notifyObservers(null, "results-added"); + } +} + +add_task(async function setup() { + registerCleanupFunction(async () => { + Services.prefs.clearUserPref("browser.urlbar.suggest.searches"); + }); + + Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false); +}); + +add_task(async function() { + let context = createContext("m", { isPrivate: false }); + await PlacesTestUtils.promiseAsyncUpdates(); + info("Manually set up query and then overwrite it."); + // slowProvider is a stand-in for a slow UnifiedComplete returning a + // non-heuristic result. + let slowProvider = new SlowHeuristicProvider({ + results: [ + makeVisitResult(context, { + uri: `http://mozilla.org/`, + title: `mozilla.org/`, + }), + ], + }); + UrlbarProvidersManager.registerProvider(slowProvider); + + // fastProvider is a stand-in for a fast Autofill returning a heuristic + // result. + let fastProvider = new FastHeuristicProvider({ + results: [ + makeVisitResult(context, { + uri: `http://mozilla.com/`, + title: `mozilla.com/`, + heuristic: true, + }), + ], + }); + UrlbarProvidersManager.registerProvider(fastProvider); + let firstContext = createContext("m", { + providers: [slowProvider.name, fastProvider.name], + }); + let secondContext = createContext("ma", { + providers: [slowProvider.name, fastProvider.name], + }); + + let controller = UrlbarTestUtils.newMockController(); + let queryRecieved, queryCancelled; + const controllerListener = { + onQueryResults(queryContext) { + console.trace(`finished query. context: ${JSON.stringify(queryContext)}`); + Assert.equal( + queryContext, + secondContext, + "Only the second query should finish." + ); + queryRecieved = true; + }, + onQueryCancelled(queryContext) { + Assert.equal( + queryContext, + firstContext, + "The first query should be cancelled." + ); + Assert.ok(!queryCancelled, "No more than one query should be cancelled."); + queryCancelled = true; + }, + }; + controller.addQueryListener(controllerListener); + + // Wait until FastProvider sends its results to the providers manager. + // Then they will be queued up in a _heuristicProvidersTimer, waiting for + // the results from SlowProvider. + let resultsAddedPromise = new Promise(resolve => { + let observe = async (subject, topic, data) => { + Services.obs.removeObserver(observe, "results-added"); + // Fire the second query to cancel the first. + await controller.startQuery(secondContext); + resolve(); + }; + + Services.obs.addObserver(observe, "results-added"); + }); + + controller.startQuery(firstContext); + await resultsAddedPromise; + + Assert.ok(queryCancelled, "At least one query was cancelled."); + Assert.ok(queryRecieved, "At least one query finished."); + controller.removeQueryListener(controllerListener); +}); diff --git a/browser/components/urlbar/tests/unit/test_keywords.js b/browser/components/urlbar/tests/unit/test_keywords.js new file mode 100644 index 0000000000..04984662d3 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_keywords.js @@ -0,0 +1,207 @@ +/* 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 ENGINE_NAME = "engine-suggestions.xml"; + +testEngine_setup(); + +add_task(async function test_non_keyword() { + info("Searching for non-keyworded entry should autoFill it"); + await PlacesTestUtils.addVisits({ + uri: Services.io.newURI("http://mozilla.org/test/"), + }); + await PlacesTestUtils.addBookmarkWithDetails({ + uri: Services.io.newURI("http://mozilla.org/test/"), + }); + let context = createContext("moz", { isPrivate: false }); + await check_results({ + context, + autofilled: "mozilla.org/", + completed: "http://mozilla.org/", + matches: [ + makeVisitResult(context, { + uri: "http://mozilla.org/", + title: "mozilla.org", + heuristic: true, + }), + makeBookmarkResult(context, { + uri: "http://mozilla.org/test/", + title: "A bookmark", + }), + ], + }); + await cleanupPlaces(); +}); + +add_task(async function test_keyword() { + info("Searching for keyworded entry should not autoFill it"); + await PlacesTestUtils.addVisits({ + uri: Services.io.newURI("http://mozilla.org/test/"), + }); + await PlacesTestUtils.addBookmarkWithDetails({ + uri: Services.io.newURI("http://mozilla.org/test/"), + keyword: "moz", + }); + let context = createContext("moz", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeKeywordSearchResult(context, { + uri: "http://mozilla.org/test/", + keyword: "moz", + heuristic: true, + }), + ], + }); + await cleanupPlaces(); +}); + +add_task(async function test_more_than_keyword() { + info("Searching for more than keyworded entry should autoFill it"); + await PlacesTestUtils.addVisits({ + uri: Services.io.newURI("http://mozilla.org/test/"), + }); + await PlacesTestUtils.addBookmarkWithDetails({ + uri: Services.io.newURI("http://mozilla.org/test/"), + keyword: "moz", + }); + let context = createContext("mozi", { isPrivate: false }); + await check_results({ + context, + autofilled: "mozilla.org/", + completed: "http://mozilla.org/", + matches: [ + makeVisitResult(context, { + uri: "http://mozilla.org/", + title: "mozilla.org", + heuristic: true, + }), + makeBookmarkResult(context, { + uri: "http://mozilla.org/test/", + title: "A bookmark", + }), + ], + }); + await cleanupPlaces(); +}); + +add_task(async function test_less_than_keyword() { + info("Searching for less than keyworded entry should autoFill it"); + await PlacesTestUtils.addVisits({ + uri: Services.io.newURI("http://mozilla.org/test/"), + }); + await PlacesTestUtils.addBookmarkWithDetails({ + uri: Services.io.newURI("http://mozilla.org/test/"), + keyword: "moz", + }); + let context = createContext("mo", { isPrivate: false }); + await check_results({ + context, + search: "mo", + autofilled: "mozilla.org/", + completed: "http://mozilla.org/", + matches: [ + makeVisitResult(context, { + uri: "http://mozilla.org/", + title: "mozilla.org", + heuristic: true, + }), + makeBookmarkResult(context, { + uri: "http://mozilla.org/test/", + title: "A bookmark", + }), + ], + }); + await cleanupPlaces(); +}); + +add_task(async function test_keyword_casing() { + info("Searching for keyworded entry is case-insensitive"); + await PlacesTestUtils.addVisits({ + uri: Services.io.newURI("http://mozilla.org/test/"), + }); + await PlacesTestUtils.addBookmarkWithDetails({ + uri: Services.io.newURI("http://mozilla.org/test/"), + keyword: "moz", + }); + let context = createContext("MoZ", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeKeywordSearchResult(context, { + uri: "http://mozilla.org/test/", + keyword: "MoZ", + heuristic: true, + }), + ], + }); + await cleanupPlaces(); +}); + +add_task(async function test_less_then_equal_than_keyword_bug_1124238() { + info("Searching for less than keyworded entry should autoFill it"); + await PlacesTestUtils.addVisits({ + uri: Services.io.newURI("http://mozilla.org/test/"), + }); + await PlacesTestUtils.addVisits("http://mozilla.com/"); + PlacesTestUtils.addBookmarkWithDetails({ + uri: Services.io.newURI("http://mozilla.com/"), + keyword: "moz", + }); + + let context = createContext("mo", { isPrivate: false }); + await check_results({ + context, + search: "mo", + autofilled: "mozilla.com/", + completed: "http://mozilla.com/", + matches: [ + makeVisitResult(context, { + uri: "http://mozilla.com/", + title: "mozilla.com", + heuristic: true, + }), + makeVisitResult(context, { + uri: "http://mozilla.org/test/", + title: "test visit for http://mozilla.org/test/", + }), + ], + }); + + // Search with an additional character. As the input matches a keyword, the + // completion should equal the keyword and not the URI as before. + context = createContext("moz", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeKeywordSearchResult(context, { + uri: "http://mozilla.com/", + keyword: "moz", + heuristic: true, + }), + ], + }); + + // Search with an additional character. The input doesn't match a keyword + // anymore, it should be autofilled. + context = createContext("mozi", { isPrivate: false }); + await check_results({ + context, + autofilled: "mozilla.com/", + completed: "http://mozilla.com/", + matches: [ + makeVisitResult(context, { + uri: "http://mozilla.com/", + title: "mozilla.com", + heuristic: true, + }), + makeVisitResult(context, { + uri: "http://mozilla.org/test/", + title: "test visit for http://mozilla.org/test/", + }), + ], + }); + + await cleanupPlaces(); +}); diff --git a/browser/components/urlbar/tests/unit/test_muxer.js b/browser/components/urlbar/tests/unit/test_muxer.js new file mode 100644 index 0000000000..b714ee50c1 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_muxer.js @@ -0,0 +1,246 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_muxer() { + Assert.throws( + () => UrlbarProvidersManager.registerMuxer(), + /invalid muxer/, + "Should throw with no arguments" + ); + Assert.throws( + () => UrlbarProvidersManager.registerMuxer({}), + /invalid muxer/, + "Should throw with empty object" + ); + Assert.throws( + () => + UrlbarProvidersManager.registerMuxer({ + name: "", + }), + /invalid muxer/, + "Should throw with empty name" + ); + Assert.throws( + () => + UrlbarProvidersManager.registerMuxer({ + name: "test", + sort: "no", + }), + /invalid muxer/, + "Should throw with invalid sort" + ); + + let matches = [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TAB_SWITCH, + UrlbarUtils.RESULT_SOURCE.TABS, + { url: "http://mozilla.org/tab/" } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + { url: "http://mozilla.org/bookmark/" } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://mozilla.org/history/" } + ), + ]; + + let providerName = registerBasicTestProvider(matches); + let context = createContext(undefined, { providers: [providerName] }); + let controller = UrlbarTestUtils.newMockController(); + /** + * A test muxer. + */ + class TestMuxer extends UrlbarMuxer { + get name() { + return "TestMuxer"; + } + sort(queryContext) { + queryContext.results.sort((a, b) => { + if (b.source == UrlbarUtils.RESULT_SOURCE.TABS) { + return -1; + } + if (b.source == UrlbarUtils.RESULT_SOURCE.BOOKMARKS) { + return 1; + } + return a.source == UrlbarUtils.RESULT_SOURCE.BOOKMARKS ? -1 : 1; + }); + } + } + let muxer = new TestMuxer(); + + UrlbarProvidersManager.registerMuxer(muxer); + context.muxer = "TestMuxer"; + + info("Check results, the order should be: bookmark, history, tab"); + await UrlbarProvidersManager.startQuery(context, controller); + Assert.deepEqual(context.results, [matches[1], matches[2], matches[0]]); + + // Sanity check, should not throw. + UrlbarProvidersManager.unregisterMuxer(muxer); + UrlbarProvidersManager.unregisterMuxer("TestMuxer"); // no-op. +}); + +add_task(async function test_preselectedHeuristic_singleProvider() { + let matches = [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://mozilla.org/a" } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://mozilla.org/b" } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://mozilla.org/c" } + ), + ]; + matches[1].heuristic = true; + + let providerName = registerBasicTestProvider(matches); + let context = createContext(undefined, { + providers: [providerName], + }); + let controller = UrlbarTestUtils.newMockController(); + + info("Check results, the order should be: b (heuristic), a, c"); + await UrlbarProvidersManager.startQuery(context, controller); + Assert.deepEqual(context.results, [matches[1], matches[0], matches[2]]); +}); + +add_task(async function test_preselectedHeuristic_multiProviders() { + let matches1 = [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://mozilla.org/a" } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://mozilla.org/b" } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://mozilla.org/c" } + ), + ]; + + let matches2 = [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://mozilla.org/d" } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://mozilla.org/e" } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://mozilla.org/f" } + ), + ]; + matches2[1].heuristic = true; + + let provider1Name = registerBasicTestProvider(matches1); + let provider2Name = registerBasicTestProvider(matches2); + + let context = createContext(undefined, { + providers: [provider1Name, provider2Name], + }); + let controller = UrlbarTestUtils.newMockController(); + + info("Check results, the order should be: e (heuristic), a, b, c, d, f"); + await UrlbarProvidersManager.startQuery(context, controller); + Assert.deepEqual(context.results, [ + matches2[1], + ...matches1, + matches2[0], + matches2[2], + ]); +}); + +add_task(async function test_suggestions() { + Services.prefs.setIntPref("browser.urlbar.maxHistoricalSearchSuggestions", 1); + + let matches = [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://mozilla.org/a" } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://mozilla.org/b" } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.SEARCH, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { + engine: "mozSearch", + query: "moz", + suggestion: "mozzarella", + lowerCaseSuggestion: "mozzarella", + } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.SEARCH, + UrlbarUtils.RESULT_SOURCE.SEARCH, + { + engine: "mozSearch", + query: "moz", + suggestion: "mozilla", + lowerCaseSuggestion: "mozilla", + } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.SEARCH, + UrlbarUtils.RESULT_SOURCE.SEARCH, + { + engine: "mozSearch", + query: "moz", + providesSearchMode: true, + keyword: "@moz", + } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://mozilla.org/c" } + ), + ]; + + let providerName = registerBasicTestProvider(matches); + + let context = createContext(undefined, { + providers: [providerName], + }); + let controller = UrlbarTestUtils.newMockController(); + + info("Check results, the order should be: mozzarella, moz, a, b, @moz, c"); + await UrlbarProvidersManager.startQuery(context, controller); + Assert.deepEqual(context.results, [ + matches[2], + matches[3], + matches[0], + matches[1], + matches[4], + matches[5], + ]); + + Services.prefs.clearUserPref("browser.urlbar.maxHistoricalSearchSuggestions"); +}); diff --git a/browser/components/urlbar/tests/unit/test_providerHeuristicFallback.js b/browser/components/urlbar/tests/unit/test_providerHeuristicFallback.js new file mode 100644 index 0000000000..ce323a5027 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_providerHeuristicFallback.js @@ -0,0 +1,613 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that visit-url and search engine heuristic results are returned by + * UrlbarProviderHeuristicFallback. + */ + +const ENGINE_NAME = "engine-suggestions.xml"; +const SUGGEST_PREF = "browser.urlbar.suggest.searches"; +const SUGGEST_ENABLED_PREF = "browser.search.suggest.enabled"; +const PRIVATE_SEARCH_PREF = "browser.search.separatePrivateDefault.ui.enabled"; + +// We make sure that restriction tokens and search terms are correctly +// recognized when they are separated by each of these different types of spaces +// and combinations of spaces. U+3000 is the ideographic space in CJK and is +// commonly used by CJK speakers. +const TEST_SPACES = [" ", "\u3000", " \u3000", "\u3000 "]; + +add_task(async function setup() { + // Install a test engine so we're sure of ENGINE_NAME. + let engine = await addTestSuggestionsEngine(); + + // Install the test engine. + let oldDefaultEngine = await Services.search.getDefault(); + registerCleanupFunction(async () => { + Services.search.setDefault(oldDefaultEngine); + Services.prefs.clearUserPref(SUGGEST_PREF); + Services.prefs.clearUserPref(SUGGEST_ENABLED_PREF); + Services.prefs.clearUserPref(PRIVATE_SEARCH_PREF); + Services.prefs.clearUserPref("keyword.enabled"); + }); + Services.search.setDefault(engine); + Services.prefs.setBoolPref(SUGGEST_PREF, false); + Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, false); + Services.prefs.setBoolPref(PRIVATE_SEARCH_PREF, false); +}); + +add_task(async function() { + info("visit url, no protocol"); + let query = "mozilla.org"; + let context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: `http://${query}/`, + title: `http://${query}/`, + heuristic: true, + }), + makeSearchResult(context, { + engineName: ENGINE_NAME, + }), + ], + }); + + info("visit url, no protocol but with 2 dots"); + query = "www.mozilla.org"; + context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: `http://${query}/`, + title: `http://${query}/`, + heuristic: true, + }), + makeSearchResult(context, { + engineName: ENGINE_NAME, + }), + ], + }); + + info("visit url, no protocol, e-mail like"); + query = "a@b.com"; + context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: `http://${query}/`, + title: `http://${query}/`, + heuristic: true, + }), + makeSearchResult(context, { + engineName: ENGINE_NAME, + }), + ], + }); + + info("visit url, with protocol but with 2 dots"); + query = "https://www.mozilla.org"; + context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: `${query}/`, + title: `${query}/`, + heuristic: true, + }), + ], + }); + + // info("visit url, with protocol but with 3 dots"); + query = "https://www.mozilla.org.tw"; + context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: `${query}/`, + title: `${query}/`, + heuristic: true, + }), + ], + }); + + info("visit url, with protocol"); + query = "https://mozilla.org"; + context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: `${query}/`, + title: `${query}/`, + heuristic: true, + }), + ], + }); + + info("visit url, about: protocol (no host)"); + query = "about:nonexistent"; + context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: query, + title: query, + heuristic: true, + }), + ], + }); + + info("visit url, with non-standard whitespace"); + query = "https://mozilla.org"; + context = createContext(`${query}\u2028`, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: `${query}/`, + title: `${query}/`, + heuristic: true, + }), + ], + }); + + // This is distinct because of how we predict being able to url autofill via + // host lookups. + info("visit url, host matching visited host but not visited url"); + await PlacesTestUtils.addVisits([ + { + uri: Services.io.newURI("http://mozilla.org/wine/"), + title: "Mozilla Wine", + transition: PlacesUtils.history.TRANSITION_TYPED, + }, + ]); + query = "mozilla.org/rum"; + context = createContext(`${query}\u2028`, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: `http://${query}`, + title: `http://${query}`, + iconUri: "page-icon:http://mozilla.org/", + heuristic: true, + }), + ], + }); + await PlacesUtils.history.clear(); + + // And hosts with no dot in them are special, due to requiring safelisting. + info("unknown host"); + query = "firefox"; + context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: ENGINE_NAME, + heuristic: true, + }), + ], + }); + + info("string with known host"); + query = "firefox/get"; + context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: ENGINE_NAME, + heuristic: true, + }), + ], + }); + + Services.prefs.setBoolPref("browser.fixup.domainwhitelist.firefox", true); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("browser.fixup.domainwhitelist.firefox"); + }); + + info("known host"); + query = "firefox"; + context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: `http://${query}/`, + title: `http://${query}/`, + heuristic: true, + }), + makeSearchResult(context, { + engineName: ENGINE_NAME, + }), + ], + }); + + info("url with known host"); + query = "firefox/get"; + context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: `http://${query}`, + title: `http://${query}`, + iconUri: "page-icon:http://firefox/", + heuristic: true, + }), + ], + }); + + info("visit url, host matching visited host but not visited url, known host"); + Services.prefs.setBoolPref("browser.fixup.domainwhitelist.mozilla", true); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("browser.fixup.domainwhitelist.mozilla"); + }); + query = "mozilla/rum"; + context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: `http://${query}`, + title: `http://${query}`, + iconUri: "page-icon:http://mozilla/", + heuristic: true, + }), + ], + }); + + // ipv4 and ipv6 literal addresses should offer to visit. + info("visit url, ipv4 literal"); + query = "127.0.0.1"; + context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: `http://${query}/`, + title: `http://${query}/`, + heuristic: true, + }), + ], + }); + + info("visit url, ipv6 literal"); + query = "[2001:db8::1]"; + context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: `http://${query}/`, + title: `http://${query}/`, + heuristic: true, + }), + ], + }); + + // Setting keyword.enabled to false should always try to visit. + let keywordEnabled = Services.prefs.getBoolPref("keyword.enabled"); + Services.prefs.setBoolPref("keyword.enabled", false); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("keyword.enabled"); + }); + info("visit url, keyword.enabled = false"); + query = "bacon"; + context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: `http://${query}/`, + title: `http://${query}/`, + heuristic: true, + }), + ], + }); + + info("visit two word query, keyword.enabled = false"); + query = "bacon lovers"; + context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: query, + title: query, + heuristic: true, + }), + ], + }); + + info("Forced search through a restriction token, keyword.enabled = false"); + query = "?bacon"; + context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: ENGINE_NAME, + heuristic: true, + query: "bacon", + }), + ], + }); + + Services.prefs.setBoolPref("keyword.enabled", true); + info("visit two word query, keyword.enabled = true"); + query = "bacon lovers"; + context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: ENGINE_NAME, + heuristic: true, + }), + ], + }); + + Services.prefs.setBoolPref("keyword.enabled", keywordEnabled); + + info("visit url, scheme+host"); + query = "http://example"; + context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: `${query}/`, + title: `${query}/`, + heuristic: true, + }), + ], + }); + + info("visit url, scheme+host"); + query = "ftp://example"; + context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: `${query}/`, + title: `${query}/`, + heuristic: true, + }), + ], + }); + + info("visit url, host+port"); + query = "example:8080"; + context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: `http://${query}/`, + title: `http://${query}/`, + heuristic: true, + }), + ], + }); + + info("numerical operations that look like urls should search"); + query = "123/12"; + context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: ENGINE_NAME, + heuristic: true, + }), + ], + }); + + info("numerical operations that look like urls should search"); + query = "123.12/12.1"; + context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: ENGINE_NAME, + heuristic: true, + }), + ], + }); + + query = "resource:///modules"; + context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: query, + title: query, + heuristic: true, + }), + ], + }); + + info("access resource://app/modules"); + query = "resource://app/modules"; + context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: query, + title: query, + heuristic: true, + }), + ], + }); + + info("protocol with an extra slash"); + query = "http:///"; + context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: ENGINE_NAME, + heuristic: true, + }), + ], + }); + + info("change default engine"); + let originalTestEngine = Services.search.getEngineByName(ENGINE_NAME); + let engine2 = await Services.search.addEngineWithDetails("AliasEngine", { + alias: "alias", + method: "GET", + template: "http://example.com/?q={searchTerms}", + }); + Assert.notEqual( + Services.search.defaultEngine, + engine2, + "New engine shouldn't be the current engine yet" + ); + await Services.search.setDefault(engine2); + query = "toronto"; + context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: "AliasEngine", + heuristic: true, + }), + ], + }); + await Services.search.setDefault(originalTestEngine); + + info( + "Leading search-mode restriction tokens are removed from the search result." + ); + for (let token of UrlbarTokenizer.SEARCH_MODE_RESTRICT) { + for (let spaces of TEST_SPACES) { + query = token + spaces + "query"; + info("Testing: " + JSON.stringify({ query, spaces: codePoints(spaces) })); + let expectedQuery = query.substring(1).trimStart(); + context = createContext(query, { isPrivate: false }); + info(`Searching for "${query}", expecting "${expectedQuery}"`); + let payload = { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + heuristic: true, + query: expectedQuery, + alias: token, + }; + if (token == UrlbarTokenizer.RESTRICT.SEARCH) { + payload.source = UrlbarUtils.RESULT_SOURCE.SEARCH; + payload.engineName = ENGINE_NAME; + } + await check_results({ + context, + matches: [makeSearchResult(context, payload)], + }); + } + } + + info( + "Leading search-mode restriction tokens are removed from the search result with keyword.enabled = false." + ); + Services.prefs.setBoolPref("keyword.enabled", false); + for (let token of UrlbarTokenizer.SEARCH_MODE_RESTRICT) { + for (let spaces of TEST_SPACES) { + query = token + spaces + "query"; + info("Testing: " + JSON.stringify({ query, spaces: codePoints(spaces) })); + let expectedQuery = query.substring(1).trimStart(); + context = createContext(query, { isPrivate: false }); + info(`Searching for "${query}", expecting "${expectedQuery}"`); + let payload = { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + heuristic: true, + query: expectedQuery, + alias: token, + }; + if (token == UrlbarTokenizer.RESTRICT.SEARCH) { + payload.source = UrlbarUtils.RESULT_SOURCE.SEARCH; + payload.engineName = ENGINE_NAME; + } + await check_results({ + context, + matches: [makeSearchResult(context, payload)], + }); + } + } + Services.prefs.clearUserPref("keyword.enabled"); + + info( + "Leading non-search-mode restriction tokens are not removed from the search result." + ); + for (let token of Object.values(UrlbarTokenizer.RESTRICT)) { + if (UrlbarTokenizer.SEARCH_MODE_RESTRICT.has(token)) { + continue; + } + for (let spaces of TEST_SPACES) { + query = token + spaces + "query"; + info("Testing: " + JSON.stringify({ query, spaces: codePoints(spaces) })); + let expectedQuery = query; + context = createContext(query, { isPrivate: false }); + info(`Searching for "${query}", expecting "${expectedQuery}"`); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + heuristic: true, + query: expectedQuery, + engineName: ENGINE_NAME, + }), + ], + }); + } + } + + await Services.search.removeEngine(engine2); +}); + +/** + * Returns an array of code points in the given string. Each code point is + * returned as a hexidecimal string. + * + * @param {string} str + * The code points of this string will be returned. + * @returns {array} + * Array of code points in the string, where each is a hexidecimal string. + */ +function codePoints(str) { + return str.split("").map(s => s.charCodeAt(0).toString(16)); +} diff --git a/browser/components/urlbar/tests/unit/test_providerOmnibox.js b/browser/components/urlbar/tests/unit/test_providerOmnibox.js new file mode 100644 index 0000000000..09fdfdec05 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_providerOmnibox.js @@ -0,0 +1,818 @@ +/* -*- 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/. */ + +const { ExtensionSearchHandler } = ChromeUtils.import( + "resource://gre/modules/ExtensionSearchHandler.jsm" +); + +let controller = Cc["@mozilla.org/autocomplete/controller;1"].getService( + Ci.nsIAutoCompleteController +); + +const ENGINE_NAME = "engine-suggestions.xml"; +const SUGGEST_PREF = "browser.urlbar.suggest.searches"; +const SUGGEST_ENABLED_PREF = "browser.search.suggest.enabled"; + +async function cleanup() { + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); +} + +add_task(function setup() { + Services.prefs.setBoolPref(SUGGEST_PREF, false); + Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, false); + + registerCleanupFunction(async () => { + Services.prefs.clearUserPref(SUGGEST_PREF); + Services.prefs.clearUserPref(SUGGEST_ENABLED_PREF); + }); +}); + +add_task(async 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: () => {} }), + /The keyword provided is already registered/ + ); + + // Register a different keyword. + ExtensionSearchHandler.registerKeyword(anotherKeyword, { emit: () => {} }); + + // Try calling handleSearch for an unregistered keyword. + let searchData = { + keyword: unregisteredKeyword, + text: `${unregisteredKeyword} `, + }; + Assert.throws( + () => ExtensionSearchHandler.handleSearch(searchData, () => {}), + /The keyword provided is not registered/ + ); + + // Try calling handleSearch without a callback. + Assert.throws( + () => ExtensionSearchHandler.handleSearch(searchData), + /The keyword provided is not registered/ + ); + + // Try getting the description for a keyword which isn't registered. + Assert.throws( + () => ExtensionSearchHandler.getDescription(unregisteredKeyword), + /The keyword provided is not registered/ + ); + + // Try setting the default suggestion for a keyword which isn't registered. + Assert.throws( + () => + ExtensionSearchHandler.setDefaultSuggestion( + unregisteredKeyword, + "suggestion" + ), + /The keyword provided is not registered/ + ); + + // Try calling handleInputCancelled when there is no active input session. + Assert.throws( + () => ExtensionSearchHandler.handleInputCancelled(), + /There is no active input session/ + ); + + // Try calling handleInputEntered when there is no active input session. + Assert.throws( + () => + ExtensionSearchHandler.handleInputEntered( + anotherKeyword, + `${anotherKeyword} test`, + "tab" + ), + /There is no active input session/ + ); + + // Start a session by calling handleSearch with the registered keyword. + searchData = { + keyword, + text: `${keyword} test`, + }; + ExtensionSearchHandler.handleSearch(searchData, () => {}); + + // Try providing suggestions for an unregistered keyword. + Assert.throws( + () => ExtensionSearchHandler.addSuggestions(unregisteredKeyword, 0, []), + /The keyword provided is not registered/ + ); + + // Try providing suggestions for an inactive keyword. + Assert.throws( + () => ExtensionSearchHandler.addSuggestions(anotherKeyword, 0, []), + /The keyword provided is not apart of an active input session/ + ); + + // Try calling handleSearch for an inactive keyword. + searchData = { + keyword: anotherKeyword, + text: `${anotherKeyword} `, + }; + Assert.throws( + () => ExtensionSearchHandler.handleSearch(searchData, () => {}), + /A different input session is already ongoing/ + ); + + // Try calling addSuggestions with an old callback ID. + Assert.throws( + () => ExtensionSearchHandler.addSuggestions(keyword, 0, []), + /The callback is no longer active for the keyword provided/ + ); + + // 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, []), + /The callback is no longer active for the keyword provided/ + ); + + // End the input session by calling handleInputCancelled. + ExtensionSearchHandler.handleInputCancelled(); + + // Try calling handleInputCancelled after the session has ended. + Assert.throws( + () => ExtensionSearchHandler.handleInputCancelled(), + /There is no active input sessio/ + ); + + // Try calling handleSearch that doesn't have a space after the keyword. + searchData = { + keyword: anotherKeyword, + text: `${anotherKeyword}`, + }; + Assert.throws( + () => ExtensionSearchHandler.handleSearch(searchData, () => {}), + /The text provided must start with/ + ); + + // Try calling handleSearch with text starting with the wrong keyword. + searchData = { + keyword: anotherKeyword, + text: `${keyword} test`, + }; + Assert.throws( + () => ExtensionSearchHandler.handleSearch(searchData, () => {}), + /The text provided must start with/ + ); + + // Start a new session by calling handleSearch with a different keyword + searchData = { + keyword: anotherKeyword, + text: `${anotherKeyword} test`, + }; + ExtensionSearchHandler.handleSearch(searchData, () => {}); + + // Try adding suggestions again with the same callback ID now that the input session has ended. + Assert.throws( + () => ExtensionSearchHandler.addSuggestions(keyword, 1, []), + /The keyword provided is not apart of an active input session/ + ); + + // 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, []), + /The keyword provided is not apart of an active input session/ + ); + + // Try adding suggestions with a valid callback ID but an unregistered keyword. + Assert.throws( + () => ExtensionSearchHandler.addSuggestions(unregisteredKeyword, 2, []), + /The keyword provided is not registered/ + ); + + // 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" + ), + /A different input session is already ongoing/ + ); + + // Try calling handleInputEntered with invalid text. + Assert.throws( + () => + ExtensionSearchHandler.handleInputEntered(anotherKeyword, ` test`, "tab"), + /The text provided must start with/ + ); + + // Try calling handleInputEntered with an invalid disposition. + Assert.throws( + () => + ExtensionSearchHandler.handleInputEntered( + anotherKeyword, + `${anotherKeyword} test`, + "invalid" + ), + /Invalid "where" argument/ + ); + + // 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" + ), + /There is no active input session/ + ); + + // Unregister the keyword. + ExtensionSearchHandler.unregisterKeyword(keyword); + + // Try setting the default suggestion for the unregistered keyword. + Assert.throws( + () => + ExtensionSearchHandler.setDefaultSuggestion(keyword, { + description: "test", + }), + /The keyword provided is not registered/ + ); + + // Try handling a search with the unregistered keyword. + searchData = { + keyword, + text: `${keyword} test`, + }; + Assert.throws( + () => ExtensionSearchHandler.handleSearch(searchData, () => {}), + /The keyword provided is not registered/ + ); + + // Try unregistering the keyword again. + Assert.throws( + () => ExtensionSearchHandler.unregisterKeyword(keyword), + /The keyword provided is not registered/ + ); + + // Unregister the other keyword. + ExtensionSearchHandler.unregisterKeyword(anotherKeyword); + + // Try unregistering the word which was never registered. + Assert.throws( + () => ExtensionSearchHandler.unregisterKeyword(unregisteredKeyword), + /The keyword provided is not registered/ + ); + + // Try setting the default suggestion for a word that was never registered. + Assert.throws( + () => + ExtensionSearchHandler.setDefaultSuggestion(unregisteredKeyword, { + description: "test", + }), + /The keyword provided is not registered/ + ); + + await cleanup(); +}); + +add_task(async function test_extension_private_browsing() { + let events = []; + let mockExtension = { + emit: message => events.push(message), + privateBrowsingAllowed: false, + }; + + let keyword = "foo"; + + ExtensionSearchHandler.registerKeyword(keyword, mockExtension); + + let searchData = { + keyword, + text: `${keyword} test`, + inPrivateWindow: true, + }; + let result = await ExtensionSearchHandler.handleSearch(searchData); + Assert.equal(result, false, "unable to handle search for private window"); + + // Try calling handleInputEntered after the session has ended. + Assert.throws( + () => + ExtensionSearchHandler.handleInputEntered( + keyword, + `${keyword} test`, + "tab" + ), + /There is no active input session/ + ); + + ExtensionSearchHandler.unregisterKeyword(keyword); + await cleanup(); +}); + +add_task(async 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); + + let searchData = { + keyword, + text: `${keyword} `, + }; + ExtensionSearchHandler.handleSearch(searchData, () => {}); + checkEvents([ExtensionSearchHandler.MSG_INPUT_STARTED]); + + searchData.text = `${keyword} f`; + ExtensionSearchHandler.handleSearch(searchData, () => {}); + checkEvents([ExtensionSearchHandler.MSG_INPUT_CHANGED]); + + ExtensionSearchHandler.handleInputEntered(keyword, searchData.text, "tab"); + checkEvents([ExtensionSearchHandler.MSG_INPUT_ENTERED]); + + ExtensionSearchHandler.handleSearch(searchData, () => {}); + checkEvents([ + ExtensionSearchHandler.MSG_INPUT_STARTED, + ExtensionSearchHandler.MSG_INPUT_CHANGED, + ]); + + ExtensionSearchHandler.handleInputCancelled(); + checkEvents([ExtensionSearchHandler.MSG_INPUT_CANCELLED]); + + ExtensionSearchHandler.handleSearch( + { + keyword: anotherKeyword, + text: `${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(async 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" }, + ]); + // The API doesn't have a way to notify when addition is complete. + do_timeout(1000, () => { + controller.stopSearch(); + }); + } + }, + }; + + ExtensionSearchHandler.registerKeyword(keyword, mockExtension); + + let query = `${keyword} unmatched`; + let context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeOmniboxResult(context, { + heuristic: true, + keyword, + description: extensionName, + content: `${keyword} unmatched`, + }), + makeOmniboxResult(context, { + keyword, + content: `${keyword} foo`, + description: "first suggestion", + }), + makeOmniboxResult(context, { + keyword, + content: `${keyword} bar`, + description: "second suggestion", + }), + makeOmniboxResult(context, { + keyword, + content: `${keyword} baz`, + description: "third suggestion", + }), + ], + }); + + query = `${keyword} foo`; + context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeOmniboxResult(context, { + heuristic: true, + keyword, + description: extensionName, + content: `${keyword} foo`, + }), + makeOmniboxResult(context, { + keyword, + content: `${keyword} bar`, + description: "second suggestion", + }), + makeOmniboxResult(context, { + keyword, + content: `${keyword} baz`, + description: "third suggestion", + }), + ], + }); + + query = `${keyword} bar`; + context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeOmniboxResult(context, { + heuristic: true, + keyword, + description: extensionName, + content: `${keyword} bar`, + }), + makeOmniboxResult(context, { + keyword, + content: `${keyword} foo`, + description: "first suggestion", + }), + makeOmniboxResult(context, { + keyword, + content: `${keyword} baz`, + description: "third suggestion", + }), + ], + }); + + query = `${keyword} baz`; + context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeOmniboxResult(context, { + heuristic: true, + keyword, + description: extensionName, + content: `${keyword} baz`, + }), + makeOmniboxResult(context, { + keyword, + content: `${keyword} foo`, + description: "first suggestion", + }), + makeOmniboxResult(context, { + keyword, + content: `${keyword} bar`, + description: "second suggestion", + }), + ], + }); + + ExtensionSearchHandler.unregisterKeyword(keyword); + await cleanup(); +}); + +add_task(async function test_extension_results_should_come_first() { + let keyword = "test"; + let extensionName = "Omnibox Example"; + + let uri = Services.io.newURI(`http://a.com/b`); + await 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, text: `${keyword} ` }, + () => {} + ); + + let query = `${keyword} -`; + let context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeOmniboxResult(context, { + heuristic: true, + keyword, + description: extensionName, + content: `${keyword} -`, + }), + makeOmniboxResult(context, { + keyword, + content: `${keyword} foo`, + description: "first suggestion", + }), + makeOmniboxResult(context, { + keyword, + content: `${keyword} bar`, + description: "second suggestion", + }), + makeOmniboxResult(context, { + keyword, + content: `${keyword} baz`, + description: "third suggestion", + }), + makeVisitResult(context, { + uri: `http://a.com/b`, + title: `${keyword} -`, + }), + ], + }); + + ExtensionSearchHandler.unregisterKeyword(keyword); + await cleanup(); +}); + +add_task(async 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, []); + // The API doesn't have a way to notify when addition is complete. + do_timeout(1000, () => { + controller.stopSearch(); + }); + } + }, + }; + + ExtensionSearchHandler.registerKeyword(keyword, mockExtension); + + ExtensionSearchHandler.setDefaultSuggestion(keyword, { + description: "hello world", + }); + + let query = `${keyword} search query`; + let context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeOmniboxResult(context, { + heuristic: true, + keyword, + description: "hello world", + content: query, + }), + ], + }); + + ExtensionSearchHandler.setDefaultSuggestion(keyword, { + description: "foo bar", + }); + + context = createContext(query, { isPrivate: false }); + await check_results({ + context, + searchParam: "enable-actions", + matches: [ + makeOmniboxResult(context, { + heuristic: true, + keyword, + description: "foo bar", + content: query, + }), + ], + }); + + ExtensionSearchHandler.unregisterKeyword(keyword); + await cleanup(); +}); + +add_task(async 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" }, + ]); + // The API doesn't have a way to notify when addition is complete. + do_timeout(1000, () => { + controller.stopSearch(); + }); + } + }, + }; + + ExtensionSearchHandler.registerKeyword(keyword, mockExtension); + + // Start an input session before testing MSG_INPUT_CHANGED. + ExtensionSearchHandler.handleSearch( + { keyword, text: `${keyword} ` }, + () => {} + ); + + let query = `${keyword} #`; + let context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeOmniboxResult(context, { + heuristic: true, + keyword, + description: extensionName, + content: `${keyword} #`, + }), + makeOmniboxResult(context, { + keyword, + content: `${keyword} a`, + description: "first suggestion", + }), + makeOmniboxResult(context, { + keyword, + content: `${keyword} b`, + description: "second suggestion", + }), + makeOmniboxResult(context, { + keyword, + content: `${keyword} c`, + description: "third suggestion", + }), + makeOmniboxResult(context, { + keyword, + content: `${keyword} d`, + description: "fourth suggestion", + }), + makeOmniboxResult(context, { + keyword, + content: `${keyword} e`, + description: "fifth suggestion", + }), + ], + }); + + ExtensionSearchHandler.unregisterKeyword(keyword); + await cleanup(); +}); + +add_task(async function conflicting_alias() { + Services.prefs.setBoolPref(SUGGEST_PREF, true); + Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true); + + let engine = await addTestSuggestionsEngine(); + let keyword = "test"; + engine.alias = keyword; + let oldDefaultEngine = await Services.search.getDefault(); + Services.search.setDefault(engine); + + let extensionName = "Omnibox Example"; + + 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" }, + ]); + // The API doesn't have a way to notify when addition is complete. + do_timeout(1000, () => { + controller.stopSearch(); + }); + } + }, + }; + + ExtensionSearchHandler.registerKeyword(keyword, mockExtension); + let query = `${keyword} unmatched`; + let context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeOmniboxResult(context, { + heuristic: true, + keyword, + description: extensionName, + content: `${keyword} unmatched`, + }), + makeOmniboxResult(context, { + keyword, + content: `${keyword} foo`, + description: "first suggestion", + }), + makeOmniboxResult(context, { + keyword, + content: `${keyword} bar`, + description: "second suggestion", + }), + makeOmniboxResult(context, { + keyword, + content: `${keyword} baz`, + description: "third suggestion", + }), + makeSearchResult(context, { + query: "unmatched", + engineName: ENGINE_NAME, + alias: keyword, + suggestion: "unmatched", + }), + makeSearchResult(context, { + query: "unmatched", + engineName: ENGINE_NAME, + alias: keyword, + suggestion: "unmatched foo", + }), + makeSearchResult(context, { + query: "unmatched", + engineName: ENGINE_NAME, + alias: keyword, + suggestion: "unmatched bar", + }), + ], + }); + + Services.search.setDefault(oldDefaultEngine); + Services.prefs.setBoolPref(SUGGEST_PREF, false); + Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, false); + await cleanup(); +}); diff --git a/browser/components/urlbar/tests/unit/test_providerOpenTabs.js b/browser/components/urlbar/tests/unit/test_providerOpenTabs.js new file mode 100644 index 0000000000..898bb6885e --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_providerOpenTabs.js @@ -0,0 +1,45 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_openTabs() { + const userContextId = 5; + const url = "http://foo.mozilla.org/"; + UrlbarProviderOpenTabs.registerOpenTab(url, userContextId); + UrlbarProviderOpenTabs.registerOpenTab(url, userContextId); + Assert.equal( + UrlbarProviderOpenTabs.openTabs.get(userContextId).length, + 2, + "Found all the expected tabs" + ); + UrlbarProviderOpenTabs.unregisterOpenTab(url, userContextId); + Assert.equal( + UrlbarProviderOpenTabs.openTabs.get(userContextId).length, + 1, + "Found all the expected tabs" + ); + + let context = createContext(); + let matchCount = 0; + let callback = function(provider, match) { + matchCount++; + Assert.ok( + provider instanceof UrlbarProviderOpenTabs, + "Got the expected provider" + ); + Assert.equal( + match.type, + UrlbarUtils.RESULT_TYPE.TAB_SWITCH, + "Got the expected result type" + ); + Assert.equal(match.payload.url, url, "Got the expected url"); + Assert.equal(match.payload.title, undefined, "Got the expected title"); + }; + + let provider = new UrlbarProviderOpenTabs(); + await provider.startQuery(context, callback); + Assert.equal(matchCount, 1, "Found the expected number of matches"); + // Sanity check that this doesn't throw. + provider.cancelQuery(context); +}); diff --git a/browser/components/urlbar/tests/unit/test_providerTabToSearch.js b/browser/components/urlbar/tests/unit/test_providerTabToSearch.js new file mode 100644 index 0000000000..ca08493579 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_providerTabToSearch.js @@ -0,0 +1,477 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests UrlbarProviderTabToSearch. See also + * browser/components/urlbar/tests/browser/browser_tabToSearch.js + */ + +"use strict"; + +let testEngine; + +add_task(async function init() { + // Disable search suggestions for a less verbose test. + Services.prefs.setBoolPref("browser.search.suggest.enabled", false); + // Disable tab-to-search onboarding results. Those are covered in + // browser/components/urlbar/tests/browser/browser_tabToSearch.js. + Services.prefs.setIntPref( + "browser.urlbar.tabToSearch.onboard.interactionsLeft", + 0 + ); + testEngine = await Services.search.addEngineWithDetails("Test", { + template: "https://example.com/?search={searchTerms}", + }); + + registerCleanupFunction(async () => { + await Services.search.removeEngine(testEngine); + Services.prefs.clearUserPref( + "browser.urlbar.tabToSearch.onboard.interactionsLeft" + ); + Services.prefs.clearUserPref("browser.search.suggest.enabled"); + }); +}); + +// Tests that tab-to-search results appear when the engine's result domain is +// autofilled. +add_task(async function basic() { + await PlacesTestUtils.addVisits(["https://example.com/"]); + let context = createContext("examp", { isPrivate: false }); + await check_results({ + context, + autofilled: "example.com/", + completed: "https://example.com/", + matches: [ + makeVisitResult(context, { + uri: "https://example.com/", + title: "https://example.com", + heuristic: true, + providerName: "Autofill", + }), + makeSearchResult(context, { + engineName: testEngine.name, + engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS_INVERTED, + uri: UrlbarUtils.stripPublicSuffixFromHost( + testEngine.getResultDomain() + ), + providesSearchMode: true, + query: "", + providerName: "TabToSearch", + }), + ], + }); + + info("Repeat the search but with tab-to-search disabled through pref."); + Services.prefs.setBoolPref("browser.urlbar.suggest.engines", false); + await check_results({ + context, + autofilled: "example.com/", + completed: "https://example.com/", + matches: [ + makeVisitResult(context, { + uri: "https://example.com/", + title: "https://example.com", + heuristic: true, + providerName: "Autofill", + }), + ], + }); + Services.prefs.clearUserPref("browser.urlbar.suggest.engines"); + + await cleanupPlaces(); +}); + +// Tests that tab-to-search results aren't shown when the typed string matches +// an engine domain but there is no autofill. +add_task(async function noAutofill() { + // Note we are not adding any history visits. + let context = createContext("examp", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: Services.search.defaultEngine.name, + engineIconUri: Services.search.defaultEngine.iconURI?.spec, + heuristic: true, + providerName: "HeuristicFallback", + }), + ], + }); +}); + +// Tests that tab-to-search results are not shown when the typed string matches +// an engine domain, but something else is being autofilled. +add_task(async function autofillDoesNotMatchEngine() { + await PlacesTestUtils.addVisits(["https://example.test.ca/"]); + let context = createContext("example", { isPrivate: false }); + await check_results({ + context, + autofilled: "example.test.ca/", + completed: "https://example.test.ca/", + matches: [ + makeVisitResult(context, { + uri: "https://example.test.ca/", + title: "https://example.test.ca", + heuristic: true, + providerName: "Autofill", + }), + ], + }); + + await cleanupPlaces(); +}); + +// Tests that www. is ignored for the purposes of matching autofill to +// tab-to-search. +add_task(async function ignoreWww() { + // The history result has www., the engine does not. + await PlacesTestUtils.addVisits(["https://www.example.com/"]); + let context = createContext("www.examp", { isPrivate: false }); + await check_results({ + context, + autofilled: "www.example.com/", + completed: "https://www.example.com/", + matches: [ + makeVisitResult(context, { + uri: "https://www.example.com/", + title: "https://www.example.com", + heuristic: true, + providerName: "Autofill", + }), + makeSearchResult(context, { + engineName: testEngine.name, + engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS_INVERTED, + uri: UrlbarUtils.stripPublicSuffixFromHost( + testEngine.getResultDomain() + ), + providesSearchMode: true, + query: "", + providerName: "TabToSearch", + }), + ], + }); + await cleanupPlaces(); + + // The engine has www., the history result does not. + await PlacesTestUtils.addVisits(["https://foo.bar/"]); + let wwwTestEngine = await Services.search.addEngineWithDetails("TestWww", { + template: "https://www.foo.bar/?search={searchTerms}", + }); + context = createContext("foo", { isPrivate: false }); + await check_results({ + context, + autofilled: "foo.bar/", + completed: "https://foo.bar/", + matches: [ + makeVisitResult(context, { + uri: "https://foo.bar/", + title: "https://foo.bar", + heuristic: true, + providerName: "Autofill", + }), + makeSearchResult(context, { + engineName: wwwTestEngine.name, + engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS_INVERTED, + uri: UrlbarUtils.stripPublicSuffixFromHost( + wwwTestEngine.getResultDomain() + ), + providesSearchMode: true, + query: "", + providerName: "TabToSearch", + }), + ], + }); + await cleanupPlaces(); + + // Both the engine and the history result have www. + await PlacesTestUtils.addVisits(["https://www.foo.bar/"]); + context = createContext("foo", { isPrivate: false }); + await check_results({ + context, + autofilled: "foo.bar/", + completed: "https://www.foo.bar/", + matches: [ + makeVisitResult(context, { + uri: "https://www.foo.bar/", + title: "https://www.foo.bar", + heuristic: true, + providerName: "Autofill", + }), + makeSearchResult(context, { + engineName: wwwTestEngine.name, + engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS_INVERTED, + uri: UrlbarUtils.stripPublicSuffixFromHost( + wwwTestEngine.getResultDomain() + ), + providesSearchMode: true, + query: "", + providerName: "TabToSearch", + }), + ], + }); + await cleanupPlaces(); + + await Services.search.removeEngine(wwwTestEngine); +}); + +// Tests that when a user's query causes autofill to replace one engine's domain +// with another, the correct tab-to-search results are shown. +add_task(async function conflictingEngines() { + for (let i = 0; i < 3; i++) { + await PlacesTestUtils.addVisits([ + "https://foobar.com/", + "https://foo.com/", + ]); + } + let fooBarTestEngine = await Services.search.addEngineWithDetails( + "TestFooBar", + { template: "https://foobar.com/?search={searchTerms}" } + ); + let fooTestEngine = await Services.search.addEngineWithDetails("TestFoo", { + template: "https://foo.com/?search={searchTerms}", + }); + + // Search for "foo", autofilling foo.com. Observe that the foo.com + // tab-to-search result is shown, even though the foobar.com engine was added + // first (and thus enginesForDomainPrefix puts it earlier in its returned + // array.) + let context = createContext("foo", { isPrivate: false }); + await check_results({ + context, + autofilled: "foo.com/", + completed: "https://foo.com/", + matches: [ + makeVisitResult(context, { + uri: "https://foo.com/", + title: "https://foo.com", + heuristic: true, + providerName: "Autofill", + }), + makeSearchResult(context, { + engineName: fooTestEngine.name, + engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS_INVERTED, + uri: UrlbarUtils.stripPublicSuffixFromHost( + fooTestEngine.getResultDomain() + ), + providesSearchMode: true, + query: "", + providerName: "TabToSearch", + }), + makeVisitResult(context, { + uri: "https://foobar.com/", + title: "test visit for https://foobar.com/", + providerName: "UnifiedComplete", + }), + ], + }); + + // Search for "foob", autofilling foobar.com. Observe that the foo.com + // tab-to-search result is replaced with the foobar.com tab-to-search result. + context = createContext("foob", { isPrivate: false }); + await check_results({ + context, + autofilled: "foobar.com/", + completed: "https://foobar.com/", + matches: [ + makeVisitResult(context, { + uri: "https://foobar.com/", + title: "https://foobar.com", + heuristic: true, + providerName: "Autofill", + }), + makeSearchResult(context, { + engineName: fooBarTestEngine.name, + engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS_INVERTED, + uri: UrlbarUtils.stripPublicSuffixFromHost( + fooBarTestEngine.getResultDomain() + ), + providesSearchMode: true, + query: "", + providerName: "TabToSearch", + }), + ], + }); + + await cleanupPlaces(); + await Services.search.removeEngine(fooTestEngine); + await Services.search.removeEngine(fooBarTestEngine); +}); + +add_task(async function multipleEnginesForHostname() { + info( + "In case of multiple engines only one tab-to-search result should be returned" + ); + let mapsEngine = await Services.search.addEngineWithDetails("TestMaps", { + template: "https://example.com/maps/?search={searchTerms}", + }); + await PlacesTestUtils.addVisits(["https://example.com/"]); + let context = createContext("examp", { isPrivate: false }); + await check_results({ + context, + autofilled: "example.com/", + completed: "https://example.com/", + matches: [ + makeVisitResult(context, { + uri: "https://example.com/", + title: "https://example.com", + heuristic: true, + providerName: "Autofill", + }), + makeSearchResult(context, { + engineName: testEngine.name, + engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS_INVERTED, + uri: UrlbarUtils.stripPublicSuffixFromHost( + testEngine.getResultDomain() + ), + providesSearchMode: true, + query: "", + providerName: "TabToSearch", + }), + ], + }); + await cleanupPlaces(); + await Services.search.removeEngine(mapsEngine); +}); + +add_task(async function test_casing() { + info("Tab-to-search results appear also in case of different casing."); + await PlacesTestUtils.addVisits(["https://example.com/"]); + let context = createContext("eXAm", { isPrivate: false }); + await check_results({ + context, + autofilled: "eXAmple.com/", + completed: "https://example.com/", + matches: [ + makeVisitResult(context, { + uri: "https://example.com/", + title: "https://example.com", + heuristic: true, + providerName: "Autofill", + }), + makeSearchResult(context, { + engineName: testEngine.name, + engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS_INVERTED, + uri: UrlbarUtils.stripPublicSuffixFromHost( + testEngine.getResultDomain() + ), + providesSearchMode: true, + query: "", + providerName: "TabToSearch", + }), + ], + }); + await cleanupPlaces(); +}); + +add_task(async function test_publicSuffix() { + info("Tab-to-search results appear also in case of partial host match."); + let engine = await Services.search.addEngineWithDetails("MyTest", { + template: "https://test.mytest.it/?search={searchTerms}", + }); + await PlacesTestUtils.addVisits(["https://test.mytest.it/"]); + let context = createContext("my", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: Services.search.defaultEngine.name, + engineIconUri: Services.search.defaultEngine.iconURI?.spec, + heuristic: true, + providerName: "HeuristicFallback", + }), + makeSearchResult(context, { + engineName: engine.name, + engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS_INVERTED, + uri: UrlbarUtils.stripPublicSuffixFromHost(engine.getResultDomain()), + providesSearchMode: true, + query: "", + providerName: "TabToSearch", + satisfiesAutofillThreshold: true, + }), + makeVisitResult(context, { + uri: "https://test.mytest.it/", + title: "test visit for https://test.mytest.it/", + providerName: "UnifiedComplete", + }), + ], + }); + await cleanupPlaces(); + await Services.search.removeEngine(engine); +}); + +add_task(async function test_publicSuffixIsHost() { + info("Tab-to-search results does not appear in case we autofill a suffix."); + let suffixEngine = await Services.search.addEngineWithDetails("SuffixTest", { + template: "https://somesuffix.com.mx/?search={searchTerms}", + }); + // The top level domain will be autofilled, not the full domain. + await PlacesTestUtils.addVisits(["https://com.mx/"]); + let context = createContext("co", { isPrivate: false }); + await check_results({ + context, + autofilled: "com.mx/", + completed: "https://com.mx/", + matches: [ + makeVisitResult(context, { + uri: "https://com.mx/", + title: "https://com.mx", + heuristic: true, + providerName: "Autofill", + }), + ], + }); + await cleanupPlaces(); + await Services.search.removeEngine(suffixEngine); +}); + +add_task(async function test_disabledEngine() { + info("Tab-to-search results does not appear for a Pref-disabled engine."); + let engine = await Services.search.addEngineWithDetails("Disabled", { + template: "https://disabled.com/?search={searchTerms}", + }); + await PlacesTestUtils.addVisits(["https://disabled.com/"]); + let context = createContext("dis", { isPrivate: false }); + + info("Sanity check that the engine would appear."); + await check_results({ + context, + autofilled: "disabled.com/", + completed: "https://disabled.com/", + matches: [ + makeVisitResult(context, { + uri: "https://disabled.com/", + title: "https://disabled.com", + heuristic: true, + providerName: "Autofill", + }), + makeSearchResult(context, { + engineName: engine.name, + engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS_INVERTED, + uri: UrlbarUtils.stripPublicSuffixFromHost(engine.getResultDomain()), + providesSearchMode: true, + query: "", + providerName: "TabToSearch", + }), + ], + }); + + info("Now disable the engine."); + Services.prefs.setCharPref("browser.search.hiddenOneOffs", engine.name); + await check_results({ + context, + autofilled: "disabled.com/", + completed: "https://disabled.com/", + matches: [ + makeVisitResult(context, { + uri: "https://disabled.com/", + title: "https://disabled.com", + heuristic: true, + providerName: "Autofill", + }), + ], + }); + Services.prefs.clearUserPref("browser.search.hiddenOneOffs"); + + await cleanupPlaces(); + await Services.search.removeEngine(engine); +}); diff --git a/browser/components/urlbar/tests/unit/test_providerTabToSearch_partialHost.js b/browser/components/urlbar/tests/unit/test_providerTabToSearch_partialHost.js new file mode 100644 index 0000000000..4644117b07 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_providerTabToSearch_partialHost.js @@ -0,0 +1,150 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +// Search engine origins are autofilled normally when they get over the +// threshold, though certain origins redirect to localized subdomains, that +// the user is unlikely to type, for example wikipedia.org => en.wikipedia.org. +// We should get a tab to search result also for these cases, where a normal +// autofill wouldn't happen. + +"use strict"; + +add_task(async function setup() { + Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false); + // Disable tab-to-search onboarding results. + Services.prefs.setIntPref( + "browser.urlbar.tabToSearch.onboard.interactionsLeft", + 0 + ); + Services.prefs.setBoolPref( + "browser.search.separatePrivateDefault.ui.enabled", + false + ); + + registerCleanupFunction(() => { + Services.prefs.clearUserPref("browser.urlbar.suggest.searches"); + Services.prefs.clearUserPref( + "browser.search.separatePrivateDefault.ui.enabled" + ); + Services.prefs.clearUserPref( + "browser.urlbar.tabToSearch.onboard.interactionsLeft" + ); + }); + + let url = "https://en.example.com/"; + let engine = await Services.search.addEngineWithDetails("TestEngine", { + method: "GET", + template: url, + searchGetParams: "q={searchTerms}", + }); + let defaultEngine = await Services.search.getDefault(); + await Services.search.setDefault(engine); + registerCleanupFunction(async () => { + await Services.search.setDefault(defaultEngine); + await Services.search.removeEngine(engine); + }); + // Make sure the engine domain would be autofilled. + await PlacesUtils.bookmarks.insert({ + url, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "bookmark", + }); + + info("Test matching cases"); + + for (let searchStr of ["ex", "example.c"]) { + info("Searching for " + searchStr); + let context = createContext(searchStr, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: Services.search.defaultEngine.name, + providerName: "HeuristicFallback", + heuristic: true, + }), + makeSearchResult(context, { + engineName: engine.name, + engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS_INVERTED, + uri: "en.example.", + providesSearchMode: true, + query: "", + providerName: "TabToSearch", + satisfiesAutofillThreshold: true, + }), + makeBookmarkResult(context, { + uri: url, + title: "bookmark", + }), + ], + }); + } + + info("Test a www engine"); + let url2 = "https://www.it.mochi.com/"; + let engine2 = await Services.search.addEngineWithDetails("TestEngine2", { + method: "GET", + template: url2, + searchGetParams: "q={searchTerms}", + }); + registerCleanupFunction(async () => { + await Services.search.removeEngine(engine2); + }); + // Make sure the engine domain would be autofilled. + await PlacesUtils.bookmarks.insert({ + url: url2, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "bookmark", + }); + + for (let searchStr of ["mo", "mochi.c"]) { + info("Searching for " + searchStr); + let context = createContext(searchStr, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: Services.search.defaultEngine.name, + providerName: "HeuristicFallback", + heuristic: true, + }), + makeSearchResult(context, { + engineName: engine2.name, + engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS_INVERTED, + uri: "www.it.mochi.", + providesSearchMode: true, + query: "", + providerName: "TabToSearch", + satisfiesAutofillThreshold: true, + }), + makeBookmarkResult(context, { + uri: url2, + title: "bookmark", + }), + ], + }); + } + + info("Test non-matching cases"); + + for (let searchStr of ["www.en", "www.ex", "https://ex"]) { + info("Searching for " + searchStr); + let context = createContext(searchStr, { isPrivate: false }); + // We don't want to generate all the possible results here, just check + // the heuristic result is not autofill. + let controller = UrlbarTestUtils.newMockController(); + await UrlbarProvidersManager.startQuery(context, controller); + Assert.ok(context.results[0].heuristic, "Check heuristic result"); + Assert.notEqual(context.results[0].providerName, "Autofill"); + } + + info("Restricting to history should not autofill our bookmark"); + let context = createContext("ex", { + isPrivate: false, + sources: [UrlbarUtils.RESULT_SOURCE.HISTORY], + }); + let controller = UrlbarTestUtils.newMockController(); + await UrlbarProvidersManager.startQuery(context, controller); + Assert.ok(context.results[0].heuristic, "Check heuristic result"); + Assert.notEqual(context.results[0].providerName, "Autofill"); +}); diff --git a/browser/components/urlbar/tests/unit/test_providerUnifiedComplete.js b/browser/components/urlbar/tests/unit/test_providerUnifiedComplete.js new file mode 100644 index 0000000000..3b78564484 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_providerUnifiedComplete.js @@ -0,0 +1,242 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// This is a simple test to check the UnifiedComplete provider works, it is not +// intended to check all the edge cases, because that component is already +// covered by a good amount of tests. + +const SUGGEST_PREF = "browser.urlbar.suggest.searches"; +const SUGGEST_ENABLED_PREF = "browser.search.suggest.enabled"; + +add_task(async function test_unifiedComplete() { + Services.prefs.setBoolPref(SUGGEST_PREF, true); + Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true); + let engine = await addTestSuggestionsEngine(); + Services.search.defaultEngine = engine; + let oldCurrentEngine = Services.search.defaultEngine; + registerCleanupFunction(() => { + Services.prefs.clearUserPref(SUGGEST_PREF); + Services.prefs.clearUserPref(SUGGEST_ENABLED_PREF); + Services.search.defaultEngine = oldCurrentEngine; + }); + + let controller = UrlbarTestUtils.newMockController(); + // Also check case insensitivity. + let searchString = "MoZ oRg"; + let context = createContext(searchString, { isPrivate: false }); + + // Add entries from multiple sources. + await PlacesUtils.bookmarks.insert({ + url: "https://bookmark.mozilla.org/", + title: "Test bookmark", + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + PlacesUtils.tagging.tagURI( + Services.io.newURI("https://bookmark.mozilla.org/"), + ["mozilla", "org", "ham", "moz", "bacon"] + ); + await PlacesTestUtils.addVisits([ + { uri: "https://history.mozilla.org/", title: "Test history" }, + { uri: "https://tab.mozilla.org/", title: "Test tab" }, + ]); + UrlbarProviderOpenTabs.registerOpenTab("https://tab.mozilla.org/", 0); + + await controller.startQuery(context); + + info( + "Results:\n" + + context.results.map(m => `${m.title} - ${m.payload.url}`).join("\n") + ); + Assert.equal( + context.results.length, + 6, + "Found the expected number of matches" + ); + + Assert.deepEqual( + [ + UrlbarUtils.RESULT_TYPE.SEARCH, + UrlbarUtils.RESULT_TYPE.SEARCH, + UrlbarUtils.RESULT_TYPE.SEARCH, + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_TYPE.TAB_SWITCH, + UrlbarUtils.RESULT_TYPE.URL, + ], + context.results.map(m => m.type), + "Check result types" + ); + + Assert.deepEqual( + [ + searchString, + searchString + " foo", + searchString + " bar", + "Test bookmark", + "Test tab", + "Test history", + ], + context.results.map(m => m.title), + "Check match titles" + ); + + Assert.deepEqual( + context.results[3].payload.tags, + ["moz", "mozilla", "org"], + "Check tags" + ); + + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); + UrlbarProviderOpenTabs.unregisterOpenTab("https://tab.mozilla.org/", 0); +}); + +add_task(async function test_bookmarkBehaviorDisabled_tagged() { + Services.prefs.setBoolPref(SUGGEST_PREF, false); + Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, false); + + // Disable the bookmark behavior in UnifiedComplete. + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false); + + let controller = UrlbarTestUtils.newMockController(); + // Also check case insensitivity. + let searchString = "MoZ oRg"; + let context = createContext(searchString, { isPrivate: false }); + + // Add a tagged bookmark that's also visited. + await PlacesUtils.bookmarks.insert({ + url: "https://bookmark.mozilla.org/", + title: "Test bookmark", + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + PlacesUtils.tagging.tagURI( + Services.io.newURI("https://bookmark.mozilla.org/"), + ["mozilla", "org", "ham", "moz", "bacon"] + ); + await PlacesTestUtils.addVisits("https://bookmark.mozilla.org/"); + + await controller.startQuery(context); + + info( + "Results:\n" + + context.results.map(m => `${m.title} - ${m.payload.url}`).join("\n") + ); + Assert.equal( + context.results.length, + 2, + "Found the expected number of matches" + ); + + Assert.deepEqual( + [UrlbarUtils.RESULT_TYPE.SEARCH, UrlbarUtils.RESULT_TYPE.URL], + context.results.map(m => m.type), + "Check result types" + ); + + Assert.deepEqual( + [searchString, "Test bookmark"], + context.results.map(m => m.title), + "Check match titles" + ); + + Assert.deepEqual(context.results[1].payload.tags, [], "Check tags"); + + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(async function test_bookmarkBehaviorDisabled_untagged() { + Services.prefs.setBoolPref(SUGGEST_PREF, false); + Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, false); + + // Disable the bookmark behavior in UnifiedComplete. + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false); + + let controller = UrlbarTestUtils.newMockController(); + // Also check case insensitivity. + let searchString = "MoZ oRg"; + let context = createContext(searchString, { isPrivate: false }); + + // Add an *untagged* bookmark that's also visited. + await PlacesUtils.bookmarks.insert({ + url: "https://bookmark.mozilla.org/", + title: "Test bookmark", + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + await PlacesTestUtils.addVisits("https://bookmark.mozilla.org/"); + + await controller.startQuery(context); + + info( + "Results:\n" + + context.results.map(m => `${m.title} - ${m.payload.url}`).join("\n") + ); + Assert.equal( + context.results.length, + 2, + "Found the expected number of matches" + ); + + Assert.deepEqual( + [UrlbarUtils.RESULT_TYPE.SEARCH, UrlbarUtils.RESULT_TYPE.URL], + context.results.map(m => m.type), + "Check result types" + ); + + Assert.deepEqual( + [searchString, "Test bookmark"], + context.results.map(m => m.title), + "Check match titles" + ); + + Assert.deepEqual(context.results[1].payload.tags, [], "Check tags"); + + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(async function test_diacritics() { + Services.prefs.setBoolPref(SUGGEST_PREF, false); + Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, false); + + // Enable the bookmark behavior in UnifiedComplete. + Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", true); + + let controller = UrlbarTestUtils.newMockController(); + let searchString = "agui"; + let context = createContext(searchString, { isPrivate: false }); + + await PlacesUtils.bookmarks.insert({ + url: "https://bookmark.mozilla.org/%C3%A3g%CC%83u%C4%A9", + title: "Test bookmark with accents in path", + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + + await controller.startQuery(context); + + info( + "Results:\n" + + context.results.map(m => `${m.title} - ${m.payload.url}`).join("\n") + ); + Assert.equal( + context.results.length, + 2, + "Found the expected number of matches" + ); + + Assert.deepEqual( + [UrlbarUtils.RESULT_TYPE.SEARCH, UrlbarUtils.RESULT_TYPE.URL], + context.results.map(m => m.type), + "Check result types" + ); + + Assert.deepEqual( + [searchString, "Test bookmark with accents in path"], + context.results.map(m => m.title), + "Check match titles" + ); + + await PlacesUtils.history.clear(); + await PlacesUtils.bookmarks.eraseEverything(); +}); diff --git a/browser/components/urlbar/tests/unit/test_providerUnifiedComplete_duplicate_entries.js b/browser/components/urlbar/tests/unit/test_providerUnifiedComplete_duplicate_entries.js new file mode 100644 index 0000000000..7533921fc6 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_providerUnifiedComplete_duplicate_entries.js @@ -0,0 +1,42 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_duplicates() { + const TEST_URL = "https://history.mozilla.org/"; + await PlacesTestUtils.addVisits([ + { uri: TEST_URL, title: "Test history" }, + { uri: TEST_URL + "?#", title: "Test history" }, + { uri: TEST_URL + "#", title: "Test history" }, + ]); + + let controller = UrlbarTestUtils.newMockController(); + let searchString = "^Hist"; + let context = createContext(searchString, { isPrivate: false }); + await controller.startQuery(context); + + // The first result will be a search heuristic, which we don't care about for + // this test. + info( + "Results:\n" + + context.results.map(m => `${m.title} - ${m.payload.url}`).join("\n") + ); + Assert.equal( + context.results.length, + 2, + "Found the expected number of matches" + ); + Assert.equal( + context.results[1].type, + UrlbarUtils.RESULT_TYPE.URL, + "Should have a history result" + ); + Assert.equal( + context.results[1].payload.url, + TEST_URL + "#", + "Check result URL" + ); + + await PlacesUtils.history.clear(); +}); diff --git a/browser/components/urlbar/tests/unit/test_providersManager.js b/browser/components/urlbar/tests/unit/test_providersManager.js new file mode 100644 index 0000000000..57598448ea --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_providersManager.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_providers() { + Assert.throws( + () => UrlbarProvidersManager.registerProvider(), + /invalid provider/, + "Should throw with no arguments" + ); + Assert.throws( + () => UrlbarProvidersManager.registerProvider({}), + /invalid provider/, + "Should throw with empty object" + ); + Assert.throws( + () => + UrlbarProvidersManager.registerProvider({ + name: "", + }), + /invalid provider/, + "Should throw with empty name" + ); + Assert.throws( + () => + UrlbarProvidersManager.registerProvider({ + name: "test", + startQuery: "no", + }), + /invalid provider/, + "Should throw with invalid startQuery" + ); + Assert.throws( + () => + UrlbarProvidersManager.registerProvider({ + name: "test", + startQuery: () => {}, + cancelQuery: "no", + }), + /invalid provider/, + "Should throw with invalid cancelQuery" + ); + + let match = new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TAB_SWITCH, + UrlbarUtils.RESULT_SOURCE.TABS, + { url: "http://mozilla.org/foo/" } + ); + + let providerName = registerBasicTestProvider([match]); + let context = createContext(undefined, { providers: [providerName] }); + let controller = UrlbarTestUtils.newMockController(); + let resultsPromise = promiseControllerNotification( + controller, + "onQueryResults" + ); + + await UrlbarProvidersManager.startQuery(context, controller); + // Sanity check that this doesn't throw. It should be a no-op since we await + // for startQuery. + UrlbarProvidersManager.cancelQuery(context); + + let params = await resultsPromise; + Assert.deepEqual(params[0].results, [match]); +}); + +add_task(async function test_criticalSection() { + // Just a sanity check, this shouldn't throw. + await UrlbarProvidersManager.runInCriticalSection(async () => { + let db = await PlacesUtils.promiseLargeCacheDBConnection(); + await db.execute(`PRAGMA page_cache`); + }); +}); diff --git a/browser/components/urlbar/tests/unit/test_providersManager_filtering.js b/browser/components/urlbar/tests/unit/test_providersManager_filtering.js new file mode 100644 index 0000000000..206dd98896 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_providersManager_filtering.js @@ -0,0 +1,405 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_filtering_disable_only_source() { + let match = new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TAB_SWITCH, + UrlbarUtils.RESULT_SOURCE.TABS, + { url: "http://mozilla.org/foo/" } + ); + let providerName = registerBasicTestProvider([match]); + let context = createContext(undefined, { providers: [providerName] }); + let controller = UrlbarTestUtils.newMockController(); + + info("Disable the only available source, should get no matches"); + Services.prefs.setBoolPref("browser.urlbar.suggest.openpage", false); + let promise = Promise.race([ + promiseControllerNotification(controller, "onQueryResults", false), + promiseControllerNotification(controller, "onQueryFinished"), + ]); + await controller.startQuery(context); + await promise; + Services.prefs.clearUserPref("browser.urlbar.suggest.openpage"); + UrlbarProvidersManager.unregisterProvider({ name: providerName }); +}); + +add_task(async function test_filtering_disable_one_source() { + let matches = [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TAB_SWITCH, + UrlbarUtils.RESULT_SOURCE.TABS, + { url: "http://mozilla.org/foo/" } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TAB_SWITCH, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://mozilla.org/foo/" } + ), + ]; + let providerName = registerBasicTestProvider(matches); + let context = createContext(undefined, { providers: [providerName] }); + let controller = UrlbarTestUtils.newMockController(); + + info("Disable one of the sources, should get a single match"); + Services.prefs.setBoolPref("browser.urlbar.suggest.history", false); + let promise = Promise.all([ + promiseControllerNotification(controller, "onQueryResults"), + promiseControllerNotification(controller, "onQueryFinished"), + ]); + await controller.startQuery(context, controller); + await promise; + Assert.deepEqual(context.results, matches.slice(0, 1)); + Services.prefs.clearUserPref("browser.urlbar.suggest.history"); + UrlbarProvidersManager.unregisterProvider({ name: providerName }); +}); + +add_task(async function test_filtering_restriction_token() { + let matches = [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TAB_SWITCH, + UrlbarUtils.RESULT_SOURCE.TABS, + { url: "http://mozilla.org/foo/" } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TAB_SWITCH, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://mozilla.org/foo/" } + ), + ]; + let providerName = registerBasicTestProvider(matches); + let context = createContext(`foo ${UrlbarTokenizer.RESTRICT.OPENPAGE}`, { + providers: [providerName], + }); + let controller = UrlbarTestUtils.newMockController(); + + info("Use a restriction character, should get a single match"); + let promise = Promise.all([ + promiseControllerNotification(controller, "onQueryResults"), + promiseControllerNotification(controller, "onQueryFinished"), + ]); + await controller.startQuery(context, controller); + await promise; + Assert.deepEqual(context.results, matches.slice(0, 1)); + UrlbarProvidersManager.unregisterProvider({ name: providerName }); +}); + +add_task(async function test_filter_javascript() { + let match = new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TAB_SWITCH, + UrlbarUtils.RESULT_SOURCE.TABS, + { url: "http://mozilla.org/foo/" } + ); + let jsMatch = new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TAB_SWITCH, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "javascript:foo" } + ); + let providerName = registerBasicTestProvider([match, jsMatch]); + let context = createContext(undefined, { providers: [providerName] }); + let controller = UrlbarTestUtils.newMockController(); + + info("By default javascript should be filtered out"); + let promise = promiseControllerNotification(controller, "onQueryResults"); + await controller.startQuery(context, controller); + await promise; + Assert.deepEqual(context.results, [match]); + + info("Except when the user explicitly starts the search with javascript:"); + context = createContext(`javascript: ${UrlbarTokenizer.RESTRICT.HISTORY}`, { + providers: [providerName], + }); + promise = promiseControllerNotification(controller, "onQueryResults"); + await controller.startQuery(context, controller); + await promise; + Assert.deepEqual(context.results, [jsMatch]); + + info("Disable javascript filtering"); + Services.prefs.setBoolPref("browser.urlbar.filter.javascript", false); + context = createContext(undefined, { providers: [providerName] }); + promise = promiseControllerNotification(controller, "onQueryResults"); + await controller.startQuery(context, controller); + await promise; + Assert.deepEqual(context.results, [match, jsMatch]); + Services.prefs.clearUserPref("browser.urlbar.filter.javascript"); + UrlbarProvidersManager.unregisterProvider({ name: providerName }); +}); + +add_task(async function test_filter_isActive() { + let goodMatches = [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TAB_SWITCH, + UrlbarUtils.RESULT_SOURCE.TABS, + { url: "http://mozilla.org/foo/" } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://mozilla.org/foo/" } + ), + ]; + let providerName = registerBasicTestProvider(goodMatches); + + let badMatches = [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + { url: "http://mozilla.org/foo/" } + ), + ]; + /** + * A test provider that should not be invoked. + */ + class NoInvokeProvider extends UrlbarProvider { + get name() { + return "BadProvider"; + } + get type() { + return UrlbarUtils.PROVIDER_TYPE.PROFILE; + } + isActive(context) { + info("Acceptable sources: " + context.sources); + return context.sources.includes(UrlbarUtils.RESULT_SOURCE.BOOKMARKS); + } + async startQuery(context, add) { + Assert.ok(false, "Provider should no be invoked"); + for (const match of badMatches) { + add(this, match); + } + } + } + UrlbarProvidersManager.registerProvider(new NoInvokeProvider()); + + let context = createContext(undefined, { + sources: [UrlbarUtils.RESULT_SOURCE.TABS], + providers: [providerName, "BadProvider"], + }); + let controller = UrlbarTestUtils.newMockController(); + + info("Only tabs should be returned"); + let promise = promiseControllerNotification(controller, "onQueryResults"); + await controller.startQuery(context, controller); + await promise; + Assert.deepEqual(context.results.length, 1, "Should find only one match"); + Assert.deepEqual( + context.results[0].source, + UrlbarUtils.RESULT_SOURCE.TABS, + "Should find only a tab match" + ); + UrlbarProvidersManager.unregisterProvider({ name: providerName }); + UrlbarProvidersManager.unregisterProvider({ name: "BadProvider" }); +}); + +add_task(async function test_filter_queryContext() { + let providerName = registerBasicTestProvider(); + + /** + * A test provider that should not be invoked because of queryContext.providers. + */ + class NoInvokeProvider extends UrlbarProvider { + get name() { + return "BadProvider"; + } + get type() { + return UrlbarUtils.PROVIDER_TYPE.PROFILE; + } + isActive(context) { + return true; + } + async startQuery(context, add) { + Assert.ok(false, "Provider should no be invoked"); + } + } + UrlbarProvidersManager.registerProvider(new NoInvokeProvider()); + + let context = createContext(undefined, { + providers: [providerName], + }); + let controller = UrlbarTestUtils.newMockController(); + + await controller.startQuery(context, controller); + UrlbarProvidersManager.unregisterProvider({ name: providerName }); + UrlbarProvidersManager.unregisterProvider({ name: "BadProvider" }); +}); + +add_task(async function test_nofilter_heuristic() { + // Checks that even if a provider returns a result that should be filtered out + // it will still be invoked if it's of type heuristic, and only the heuristic + // result is returned. + let matches = [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TAB_SWITCH, + UrlbarUtils.RESULT_SOURCE.TABS, + { url: "http://mozilla.org/foo/" } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TAB_SWITCH, + UrlbarUtils.RESULT_SOURCE.TABS, + { url: "http://mozilla.org/foo2/" } + ), + ]; + matches[0].heuristic = true; + let providerName = registerBasicTestProvider( + matches, + undefined, + UrlbarUtils.PROVIDER_TYPE.HEURISTIC + ); + + let context = createContext(undefined, { + sources: [UrlbarUtils.RESULT_SOURCE.SEARCH], + providers: [providerName], + }); + let controller = UrlbarTestUtils.newMockController(); + + // Disable search matches through prefs. + Services.prefs.setBoolPref("browser.urlbar.suggest.openpage", false); + info("Only 1 heuristic tab result should be returned"); + let promise = promiseControllerNotification(controller, "onQueryResults"); + await controller.startQuery(context, controller); + await promise; + Services.prefs.clearUserPref("browser.urlbar.suggest.openpage"); + Assert.deepEqual(context.results.length, 1, "Should find only one match"); + Assert.deepEqual( + context.results[0].source, + UrlbarUtils.RESULT_SOURCE.TABS, + "Should find only a tab match" + ); + UrlbarProvidersManager.unregisterProvider({ name: providerName }); +}); + +add_task(async function test_nofilter_restrict() { + // Checks that even if a pref is disabled, we still return results on a + // restriction token. + let matches = [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TAB_SWITCH, + UrlbarUtils.RESULT_SOURCE.TABS, + { url: "http://mozilla.org/foo_tab/" } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.BOOKMARKS, + { url: "http://mozilla.org/foo_bookmark/" } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://mozilla.org/foo_history/" } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.SEARCH, + UrlbarUtils.RESULT_SOURCE.SEARCH, + { engine: "noengine" } + ), + ]; + /** + * A test provider. + */ + class TestProvider extends UrlbarProvider { + get name() { + return "MyProvider"; + } + get type() { + return UrlbarUtils.PROVIDER_TYPE.PROFILE; + } + isActive(context) { + Assert.equal(context.sources.length, 1, "Check acceptable sources"); + return true; + } + async startQuery(context, add) { + Assert.ok(true, "expected provider was invoked"); + for (let match of matches) { + add(this, match); + } + } + } + let provider = new TestProvider(); + UrlbarProvidersManager.registerProvider(provider); + + let typeToPropertiesMap = new Map([ + ["HISTORY", { source: "HISTORY", pref: "history" }], + ["BOOKMARK", { source: "BOOKMARKS", pref: "bookmark" }], + ["OPENPAGE", { source: "TABS", pref: "openpage" }], + ["SEARCH", { source: "SEARCH", pref: "searches" }], + ]); + for (let [type, token] of Object.entries(UrlbarTokenizer.RESTRICT)) { + let properties = typeToPropertiesMap.get(type); + if (!properties) { + continue; + } + info("Restricting on " + type); + let context = createContext(token + " foo", { + providers: ["MyProvider"], + }); + let controller = UrlbarTestUtils.newMockController(); + // Disable the corresponding pref. + const pref = "browser.urlbar.suggest." + properties.pref; + info("Disabling " + pref); + Services.prefs.setBoolPref(pref, false); + await controller.startQuery(context, controller); + Assert.equal(context.results.length, 1, "Should find one result"); + Assert.equal( + context.results[0].source, + UrlbarUtils.RESULT_SOURCE[properties.source], + "Check result source" + ); + Services.prefs.clearUserPref(pref); + } + UrlbarProvidersManager.unregisterProvider(provider); +}); + +add_task(async function test_filter_priority() { + /** + * A test provider. + */ + class TestProvider extends UrlbarTestUtils.TestProvider { + constructor(priority, shouldBeInvoked, namePart = "") { + super(); + this._priority = priority; + this._name = `${priority}` + namePart; + this._shouldBeInvoked = shouldBeInvoked; + } + async startQuery(context, add) { + Assert.ok(this._shouldBeInvoked, `${this.name} was invoked`); + } + } + + // Test all possible orderings of the providers to make sure the logic that + // finds the highest priority providers is correct. + let providerPerms = permute([ + new TestProvider(0, false), + new TestProvider(1, false), + new TestProvider(2, true, "a"), + new TestProvider(2, true, "b"), + ]); + for (let providers of providerPerms) { + for (let provider of providers) { + UrlbarProvidersManager.registerProvider(provider); + } + let providerNames = providers.map(p => p.name); + let context = createContext(undefined, { providers: providerNames }); + let controller = UrlbarTestUtils.newMockController(); + await controller.startQuery(context, controller); + for (let name of providerNames) { + UrlbarProvidersManager.unregisterProvider({ name }); + } + } +}); + +function permute(objects) { + if (objects.length <= 1) { + return [objects]; + } + let perms = []; + for (let i = 0; i < objects.length; i++) { + let otherObjects = objects.slice(); + otherObjects.splice(i, 1); + let otherPerms = permute(otherObjects); + for (let perm of otherPerms) { + perm.unshift(objects[i]); + } + perms = perms.concat(otherPerms); + } + return perms; +} diff --git a/browser/components/urlbar/tests/unit/test_providersManager_maxResults.js b/browser/components/urlbar/tests/unit/test_providersManager_maxResults.js new file mode 100644 index 0000000000..c3f6e5c2e3 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_providersManager_maxResults.js @@ -0,0 +1,37 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_maxResults() { + const MATCHES_LENGTH = 20; + let matches = []; + for (let i = 0; i < MATCHES_LENGTH; i++) { + matches.push( + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TAB_SWITCH, + UrlbarUtils.RESULT_SOURCE.TABS, + { url: `http://mozilla.org/foo/${i}` } + ) + ); + } + let providerName = registerBasicTestProvider(matches); + let context = createContext(undefined, { providers: [providerName] }); + let controller = UrlbarTestUtils.newMockController(); + + async function test_count(count) { + let promise = promiseControllerNotification(controller, "onQueryFinished"); + context.maxResults = count; + await controller.startQuery(context); + await promise; + Assert.equal( + context.results.length, + Math.min(MATCHES_LENGTH, count), + "Check count" + ); + Assert.deepEqual(context.results, matches.slice(0, count), "Check results"); + } + await test_count(10); + await test_count(1); + await test_count(30); +}); diff --git a/browser/components/urlbar/tests/unit/test_queryScorer.js b/browser/components/urlbar/tests/unit/test_queryScorer.js new file mode 100644 index 0000000000..4af1b26112 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_queryScorer.js @@ -0,0 +1,405 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +XPCOMUtils.defineLazyModuleGetters(this, { + QueryScorer: "resource:///modules/UrlbarProviderInterventions.jsm", +}); + +const DISTANCE_THRESHOLD = 1; + +const DOCUMENTS = { + clear: [ + "cache firefox", + "clear cache firefox", + "clear cache in firefox", + "clear cookies firefox", + "clear firefox cache", + "clear history firefox", + "cookies firefox", + "delete cookies firefox", + "delete history firefox", + "firefox cache", + "firefox clear cache", + "firefox clear cookies", + "firefox clear history", + "firefox cookie", + "firefox cookies", + "firefox delete cookies", + "firefox delete history", + "firefox history", + "firefox not loading pages", + "history firefox", + "how to clear cache", + "how to clear history", + ], + refresh: [ + "firefox crashing", + "firefox keeps crashing", + "firefox not responding", + "firefox not working", + "firefox refresh", + "firefox slow", + "how to reset firefox", + "refresh firefox", + "reset firefox", + ], + update: [ + "download firefox", + "download mozilla", + "firefox browser", + "firefox download", + "firefox for mac", + "firefox for windows", + "firefox free download", + "firefox install", + "firefox installer", + "firefox latest version", + "firefox mac", + "firefox quantum", + "firefox update", + "firefox version", + "firefox windows", + "get firefox", + "how to update firefox", + "install firefox", + "mozilla download", + "mozilla firefox 2019", + "mozilla firefox 2020", + "mozilla firefox download", + "mozilla firefox for mac", + "mozilla firefox for windows", + "mozilla firefox free download", + "mozilla firefox mac", + "mozilla firefox update", + "mozilla firefox windows", + "mozilla update", + "update firefox", + "update mozilla", + "www.firefox.com", + ], +}; + +const VARIATIONS = new Map([["firefox", ["fire fox", "fox fire", "foxfire"]]]); + +let tests = [ + { + query: "firefox", + matches: [ + { id: "clear", score: Infinity }, + { id: "refresh", score: Infinity }, + { id: "update", score: Infinity }, + ], + }, + { + query: "bogus", + matches: [ + { id: "clear", score: Infinity }, + { id: "refresh", score: Infinity }, + { id: "update", score: Infinity }, + ], + }, + { + query: "no match", + matches: [ + { id: "clear", score: Infinity }, + { id: "refresh", score: Infinity }, + { id: "update", score: Infinity }, + ], + }, + + // clear + { + query: "firefox histo", + matches: [ + { id: "clear", score: Infinity }, + { id: "refresh", score: Infinity }, + { id: "update", score: Infinity }, + ], + }, + { + query: "firefox histor", + matches: [ + { id: "clear", score: 1 }, + { id: "refresh", score: Infinity }, + { id: "update", score: Infinity }, + ], + }, + { + query: "firefox history", + matches: [ + { id: "clear", score: 0 }, + { id: "refresh", score: Infinity }, + { id: "update", score: Infinity }, + ], + }, + { + query: "firefox history we'll keep matching once we match", + matches: [ + { id: "clear", score: 0 }, + { id: "refresh", score: Infinity }, + { id: "update", score: Infinity }, + ], + }, + + { + query: "firef history", + matches: [ + { id: "clear", score: Infinity }, + { id: "refresh", score: Infinity }, + { id: "update", score: Infinity }, + ], + }, + { + query: "firefo history", + matches: [ + { id: "clear", score: 1 }, + { id: "refresh", score: Infinity }, + { id: "update", score: Infinity }, + ], + }, + { + query: "firefo histor", + matches: [ + { id: "clear", score: 2 }, + { id: "refresh", score: Infinity }, + { id: "update", score: Infinity }, + ], + }, + { + query: "firefo histor we'll keep matching once we match", + matches: [ + { id: "clear", score: 2 }, + { id: "refresh", score: Infinity }, + { id: "update", score: Infinity }, + ], + }, + + { + query: "fire fox history", + matches: [ + { id: "clear", score: 0 }, + { id: "refresh", score: Infinity }, + { id: "update", score: Infinity }, + ], + }, + { + query: "fox fire history", + matches: [ + { id: "clear", score: 0 }, + { id: "refresh", score: Infinity }, + { id: "update", score: Infinity }, + ], + }, + { + query: "foxfire history", + matches: [ + { id: "clear", score: 0 }, + { id: "refresh", score: Infinity }, + { id: "update", score: Infinity }, + ], + }, + + // refresh + { + query: "firefox sl", + matches: [ + { id: "clear", score: Infinity }, + { id: "refresh", score: Infinity }, + { id: "update", score: Infinity }, + ], + }, + { + query: "firefox slo", + matches: [ + { id: "refresh", score: 1 }, + { id: "clear", score: Infinity }, + { id: "update", score: Infinity }, + ], + }, + { + query: "firefox slow", + matches: [ + { id: "refresh", score: 0 }, + { id: "clear", score: Infinity }, + { id: "update", score: Infinity }, + ], + }, + { + query: "firefox slow we'll keep matching once we match", + matches: [ + { id: "refresh", score: 0 }, + { id: "clear", score: Infinity }, + { id: "update", score: Infinity }, + ], + }, + + { + query: "firef slow", + matches: [ + { id: "clear", score: Infinity }, + { id: "refresh", score: Infinity }, + { id: "update", score: Infinity }, + ], + }, + { + query: "firefo slow", + matches: [ + { id: "refresh", score: 1 }, + { id: "clear", score: Infinity }, + { id: "update", score: Infinity }, + ], + }, + { + query: "firefo slo", + matches: [ + { id: "refresh", score: 2 }, + { id: "clear", score: Infinity }, + { id: "update", score: Infinity }, + ], + }, + { + query: "firefo slo we'll keep matching once we match", + matches: [ + { id: "refresh", score: 2 }, + { id: "clear", score: Infinity }, + { id: "update", score: Infinity }, + ], + }, + + { + query: "fire fox slow", + matches: [ + { id: "refresh", score: 0 }, + { id: "clear", score: Infinity }, + { id: "update", score: Infinity }, + ], + }, + { + query: "fox fire slow", + matches: [ + { id: "refresh", score: 0 }, + { id: "clear", score: Infinity }, + { id: "update", score: Infinity }, + ], + }, + { + query: "foxfire slow", + matches: [ + { id: "refresh", score: 0 }, + { id: "clear", score: Infinity }, + { id: "update", score: Infinity }, + ], + }, + + // update + { + query: "firefox upda", + matches: [ + { id: "clear", score: Infinity }, + { id: "refresh", score: Infinity }, + { id: "update", score: Infinity }, + ], + }, + { + query: "firefox updat", + matches: [ + { id: "update", score: 1 }, + { id: "clear", score: Infinity }, + { id: "refresh", score: Infinity }, + ], + }, + { + query: "firefox update", + matches: [ + { id: "update", score: 0 }, + { id: "clear", score: Infinity }, + { id: "refresh", score: Infinity }, + ], + }, + { + query: "firefox update we'll keep matching once we match", + matches: [ + { id: "update", score: 0 }, + { id: "clear", score: Infinity }, + { id: "refresh", score: Infinity }, + ], + }, + + { + query: "firef update", + matches: [ + { id: "clear", score: Infinity }, + { id: "refresh", score: Infinity }, + { id: "update", score: Infinity }, + ], + }, + { + query: "firefo update", + matches: [ + { id: "update", score: 1 }, + { id: "clear", score: Infinity }, + { id: "refresh", score: Infinity }, + ], + }, + { + query: "firefo updat", + matches: [ + { id: "update", score: 2 }, + { id: "clear", score: Infinity }, + { id: "refresh", score: Infinity }, + ], + }, + { + query: "firefo updat we'll keep matching once we match", + matches: [ + { id: "update", score: 2 }, + { id: "clear", score: Infinity }, + { id: "refresh", score: Infinity }, + ], + }, + + { + query: "fire fox update", + matches: [ + { id: "update", score: 0 }, + { id: "clear", score: Infinity }, + { id: "refresh", score: Infinity }, + ], + }, + { + query: "fox fire update", + matches: [ + { id: "update", score: 0 }, + { id: "clear", score: Infinity }, + { id: "refresh", score: Infinity }, + ], + }, + { + query: "foxfire update", + matches: [ + { id: "update", score: 0 }, + { id: "clear", score: Infinity }, + { id: "refresh", score: Infinity }, + ], + }, +]; + +add_task(async function test() { + let qs = new QueryScorer({ + distanceThreshold: DISTANCE_THRESHOLD, + variations: VARIATIONS, + }); + + for (let [id, phrases] of Object.entries(DOCUMENTS)) { + qs.addDocument({ id, phrases }); + } + + for (let { query, matches } of tests) { + let actual = qs + .score(query) + .map(result => ({ id: result.document.id, score: result.score })); + Assert.deepEqual(actual, matches, `Query: "${query}"`); + } +}); diff --git a/browser/components/urlbar/tests/unit/test_query_url.js b/browser/components/urlbar/tests/unit/test_query_url.js new file mode 100644 index 0000000000..464cae1064 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_query_url.js @@ -0,0 +1,123 @@ +/* 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 ENGINE_NAME = "engine-suggestions.xml"; +const HEURISTIC_FALLBACK_PROVIDERNAME = "HeuristicFallback"; +const UNIFIEDCOMPLETE_PROVIDERNAME = "UnifiedComplete"; + +testEngine_setup(); + +add_task(async function test_no_slash() { + info("Searching for host match without slash should match host"); + await PlacesTestUtils.addVisits([ + { uri: "http://file.org/test/" }, + { uri: "file:///c:/test.html" }, + ]); + let context = createContext("file", { isPrivate: false }); + await check_results({ + context, + autofilled: "file.org/", + completed: "http://file.org/", + matches: [ + makeVisitResult(context, { + uri: "http://file.org/", + title: "file.org", + heuristic: true, + }), + makeVisitResult(context, { + uri: "file:///c:/test.html", + title: "test visit for file:///c:/test.html", + iconUri: UrlbarUtils.ICON.DEFAULT, + providerName: UNIFIEDCOMPLETE_PROVIDERNAME, + }), + makeVisitResult(context, { + uri: "http://file.org/test/", + title: "test visit for http://file.org/test/", + providerName: UNIFIEDCOMPLETE_PROVIDERNAME, + }), + ], + }); + await cleanupPlaces(); +}); + +add_task(async function test_w_slash() { + info("Searching match with slash at the end should match url"); + await PlacesTestUtils.addVisits( + { + uri: Services.io.newURI("http://file.org/test/"), + }, + { + uri: Services.io.newURI("file:///c:/test.html"), + } + ); + let context = createContext("file.org/", { isPrivate: false }); + await check_results({ + context, + autofilled: "file.org/", + completed: "http://file.org/", + matches: [ + makeVisitResult(context, { + uri: "http://file.org/", + title: "file.org/", + heuristic: true, + }), + makeVisitResult(context, { + uri: "http://file.org/test/", + title: "test visit for http://file.org/test/", + providerName: UNIFIEDCOMPLETE_PROVIDERNAME, + }), + ], + }); + await cleanupPlaces(); +}); + +add_task(async function test_middle() { + info("Searching match with slash in the middle should match url"); + await PlacesTestUtils.addVisits( + { + uri: Services.io.newURI("http://file.org/test/"), + }, + { + uri: Services.io.newURI("file:///c:/test.html"), + } + ); + let context = createContext("file.org/t", { isPrivate: false }); + await check_results({ + context, + autofilled: "file.org/test/", + completed: "http://file.org/test/", + matches: [ + makeVisitResult(context, { + uri: "http://file.org/test/", + title: "file.org/test/", + heuristic: true, + }), + ], + }); + await cleanupPlaces(); +}); + +add_task(async function test_nonhost() { + info("Searching for non-host match without slash should not match url"); + await PlacesTestUtils.addVisits({ + uri: Services.io.newURI("file:///c:/test.html"), + }); + let context = createContext("file", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: ENGINE_NAME, + heuristic: true, + }), + makeVisitResult(context, { + uri: "file:///c:/test.html", + title: "test visit for file:///c:/test.html", + iconUri: UrlbarUtils.ICON.DEFAULT, + providerName: UNIFIEDCOMPLETE_PROVIDERNAME, + }), + ], + }); + await cleanupPlaces(); +}); diff --git a/browser/components/urlbar/tests/unit/test_search_engine_host.js b/browser/components/urlbar/tests/unit/test_search_engine_host.js new file mode 100644 index 0000000000..73c455e9c4 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_search_engine_host.js @@ -0,0 +1,97 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +let engine; + +add_task(async function test_searchEngine_autoFill() { + Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true); + Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false); + await Services.search.addEngineWithDetails("MySearchEngine", { + method: "GET", + template: "http://my.search.com/", + }); + engine = Services.search.getEngineByName("MySearchEngine"); + registerCleanupFunction(async () => { + Services.prefs.clearUserPref("browser.urlbar.autoFill.searchEngines"); + Services.prefs.clearUserPref("browser.urlbar.suggest.searches"); + Services.search.removeEngine(engine); + }); + + // Add an uri that matches the search string with high frecency. + let uri = Services.io.newURI("http://www.example.com/my/"); + let visits = []; + for (let i = 0; i < 100; ++i) { + visits.push({ uri, title: "Terms - SearchEngine Search" }); + } + await PlacesTestUtils.addVisits(visits); + await PlacesTestUtils.addBookmarkWithDetails({ + uri, + title: "Example bookmark", + }); + await PlacesTestUtils.promiseAsyncUpdates(); + ok( + frecencyForUrl(uri) > 10000, + "Added URI should have expected high frecency" + ); + + info( + "Check search domain is autoFilled even if there's an higher frecency match" + ); + let context = createContext("my", { isPrivate: false }); + await check_results({ + search: "my", + autofilled: "my.search.com/", + matches: [ + makePrioritySearchResult(context, { + engineName: "MySearchEngine", + heuristic: true, + }), + ], + }); + + await cleanupPlaces(); +}); + +add_task(async function test_searchEngine_noautoFill() { + Services.prefs.setIntPref( + "browser.urlbar.tabToSearch.onboard.interactionsLeft", + 0 + ); + await PlacesTestUtils.addVisits( + Services.io.newURI("http://my.search.com/samplepage/") + ); + + info("Check search domain is not autoFilled if it matches a visited domain"); + let context = createContext("my", { isPrivate: false }); + await check_results({ + context, + autofilled: "my.search.com/", + completed: "http://my.search.com/", + matches: [ + // Note this result is a normal Autofill result and not a priority engine. + makeVisitResult(context, { + uri: "http://my.search.com/", + title: "my.search.com", + heuristic: true, + }), + makeSearchResult(context, { + engineName: engine.name, + engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS_INVERTED, + uri: UrlbarUtils.stripPublicSuffixFromHost(engine.getResultDomain()), + providesSearchMode: true, + query: "", + providerName: "TabToSearch", + }), + makeVisitResult(context, { + uri: "http://my.search.com/samplepage/", + title: "test visit for http://my.search.com/samplepage/", + providerName: "UnifiedComplete", + }), + ], + }); + + await cleanupPlaces(); + Services.prefs.clearUserPref( + "browser.urlbar.tabToSearch.onboard.interactionsLeft" + ); +}); diff --git a/browser/components/urlbar/tests/unit/test_search_suggestions.js b/browser/components/urlbar/tests/unit/test_search_suggestions.js new file mode 100644 index 0000000000..0b1180959a --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_search_suggestions.js @@ -0,0 +1,1695 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that search engine suggestions are returned by + * UrlbarProviderSearchSuggestions. + */ + +const { FormHistory } = ChromeUtils.import( + "resource://gre/modules/FormHistory.jsm" +); + +const ENGINE_NAME = "engine-suggestions.xml"; +// This is fixed to match the port number in engine-suggestions.xml. +const SERVER_PORT = 9000; +const SUGGEST_PREF = "browser.urlbar.suggest.searches"; +const SUGGEST_ENABLED_PREF = "browser.search.suggest.enabled"; +const PRIVATE_ENABLED_PREF = "browser.search.suggest.enabled.private"; +const PRIVATE_SEARCH_PREF = "browser.search.separatePrivateDefault.ui.enabled"; +const MAX_RICH_RESULTS_PREF = "browser.urlbar.maxRichResults"; +const MAX_FORM_HISTORY_PREF = "browser.urlbar.maxHistoricalSearchSuggestions"; +const SEARCH_STRING = "hello"; +const MATCH_BUCKETS_VALUE = "general:5,suggestion:Infinity"; + +var suggestionsFn; +var previousSuggestionsFn; + +/** + * Set the current suggestion funciton. + * @param {function} fn + * A function that that a search string and returns an array of strings that + * will be used as search suggestions. + * Note: `fn` should return > 0 suggestions in most cases. Otherwise, you may + * encounter unexpected behaviour with UrlbarProviderSuggestion's + * _lastLowResultsSearchSuggestion safeguard. + */ +function setSuggestionsFn(fn) { + previousSuggestionsFn = suggestionsFn; + suggestionsFn = fn; +} + +async function cleanup() { + Services.prefs.clearUserPref("browser.urlbar.autoFill"); + Services.prefs.clearUserPref("browser.urlbar.autoFill.searchEngines"); + Services.prefs.clearUserPref(SUGGEST_PREF); + Services.prefs.clearUserPref(SUGGEST_ENABLED_PREF); + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); +} + +async function cleanUpSuggestions() { + await cleanup(); + if (previousSuggestionsFn) { + suggestionsFn = previousSuggestionsFn; + previousSuggestionsFn = null; + } +} + +function makeExpectedFormHistoryResults(context, minCount = 0) { + let count = Math.max( + minCount, + Services.prefs.getIntPref(MAX_FORM_HISTORY_PREF, 0) + ); + let results = []; + for (let i = 0; i < count; i++) { + results.push( + makeFormHistoryResult(context, { + suggestion: `${SEARCH_STRING} world Form History ${i}`, + engineName: ENGINE_NAME, + }) + ); + } + return results; +} + +function makeExpectedRemoteSuggestionResults( + context, + { suggestionPrefix = SEARCH_STRING, query = undefined } = {} +) { + return [ + makeSearchResult(context, { + query, + engineName: ENGINE_NAME, + suggestion: suggestionPrefix + " foo", + }), + makeSearchResult(context, { + query, + engineName: ENGINE_NAME, + suggestion: suggestionPrefix + " bar", + }), + ]; +} + +function makeExpectedSuggestionResults( + context, + { suggestionPrefix = SEARCH_STRING, query = undefined } = {} +) { + return [ + ...makeExpectedFormHistoryResults(context), + ...makeExpectedRemoteSuggestionResults(context, { + suggestionPrefix, + query, + }), + ]; +} + +add_task(async function setup() { + Services.prefs.setCharPref( + "browser.urlbar.matchBuckets", + MATCH_BUCKETS_VALUE + ); + + let engine = await addTestSuggestionsEngine(searchStr => { + return suggestionsFn(searchStr); + }); + setSuggestionsFn(searchStr => { + let suffixes = ["foo", "bar"]; + return [searchStr].concat(suffixes.map(s => searchStr + " " + s)); + }); + + // Install the test engine. + let oldDefaultEngine = await Services.search.getDefault(); + registerCleanupFunction(async () => { + Services.search.setDefault(oldDefaultEngine); + Services.prefs.clearUserPref(PRIVATE_SEARCH_PREF); + }); + Services.search.setDefault(engine); + Services.prefs.setBoolPref(PRIVATE_SEARCH_PREF, false); + + // Add some form history. + let context = createContext(SEARCH_STRING, { isPrivate: false }); + let entries = makeExpectedFormHistoryResults(context, 2).map(r => ({ + value: r.payload.suggestion, + source: ENGINE_NAME, + })); + await UrlbarTestUtils.formHistory.add(entries); +}); + +add_task(async function disabled_urlbarSuggestions() { + Services.prefs.setBoolPref(SUGGEST_PREF, false); + Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true); + let context = createContext(SEARCH_STRING, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + ], + }); + await cleanUpSuggestions(); +}); + +add_task(async function disabled_allSuggestions() { + Services.prefs.setBoolPref(SUGGEST_PREF, true); + Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, false); + let context = createContext(SEARCH_STRING, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + ], + }); + await cleanUpSuggestions(); +}); + +add_task(async function disabled_privateWindow() { + Services.prefs.setBoolPref(SUGGEST_PREF, true); + Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true); + Services.prefs.setBoolPref(PRIVATE_ENABLED_PREF, false); + let context = createContext(SEARCH_STRING, { isPrivate: true }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + ], + }); + await cleanUpSuggestions(); +}); + +add_task(async function disabled_urlbarSuggestions_withRestrictionToken() { + Services.prefs.setBoolPref(SUGGEST_PREF, false); + Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true); + let context = createContext( + `${UrlbarTokenizer.RESTRICT.SEARCH} ${SEARCH_STRING}`, + { isPrivate: false } + ); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + query: SEARCH_STRING, + alias: UrlbarTokenizer.RESTRICT.SEARCH, + engineName: ENGINE_NAME, + heuristic: true, + }), + ...makeExpectedSuggestionResults(context, { + query: SEARCH_STRING, + }), + ], + }); + await cleanUpSuggestions(); +}); + +add_task( + async function disabled_urlbarSuggestions_withRestrictionToken_private() { + Services.prefs.setBoolPref(SUGGEST_PREF, false); + Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true); + Services.prefs.setBoolPref(PRIVATE_ENABLED_PREF, false); + let context = createContext( + `${UrlbarTokenizer.RESTRICT.SEARCH} ${SEARCH_STRING}`, + { isPrivate: true } + ); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + query: SEARCH_STRING, + alias: UrlbarTokenizer.RESTRICT.SEARCH, + engineName: ENGINE_NAME, + heuristic: true, + }), + ], + }); + await cleanUpSuggestions(); + } +); + +add_task( + async function disabled_urlbarSuggestions_withRestrictionToken_private_enabled() { + Services.prefs.setBoolPref(SUGGEST_PREF, false); + Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true); + Services.prefs.setBoolPref(PRIVATE_ENABLED_PREF, true); + let context = createContext( + `${UrlbarTokenizer.RESTRICT.SEARCH} ${SEARCH_STRING}`, + { isPrivate: true } + ); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + query: SEARCH_STRING, + alias: UrlbarTokenizer.RESTRICT.SEARCH, + engineName: ENGINE_NAME, + heuristic: true, + }), + ...makeExpectedSuggestionResults(context, { + query: SEARCH_STRING, + }), + ], + }); + await cleanUpSuggestions(); + } +); + +add_task(async function enabled_by_pref_privateWindow() { + Services.prefs.setBoolPref(SUGGEST_PREF, true); + Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true); + Services.prefs.setBoolPref(PRIVATE_ENABLED_PREF, true); + let context = createContext(SEARCH_STRING, { isPrivate: true }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + ...makeExpectedSuggestionResults(context), + ], + }); + await cleanUpSuggestions(); + + Services.prefs.clearUserPref(PRIVATE_ENABLED_PREF); +}); + +add_task(async function singleWordQuery() { + Services.prefs.setBoolPref(SUGGEST_PREF, true); + Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true); + let context = createContext(SEARCH_STRING, { isPrivate: false }); + + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + ...makeExpectedSuggestionResults(context), + ], + }); + + await cleanUpSuggestions(); +}); + +add_task(async function multiWordQuery() { + Services.prefs.setBoolPref(SUGGEST_PREF, true); + Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true); + const query = `${SEARCH_STRING} world`; + let context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + ...makeExpectedSuggestionResults(context, { suggestionPrefix: query }), + ], + }); + + await cleanUpSuggestions(); +}); + +add_task(async function suffixMatch() { + Services.prefs.setBoolPref(SUGGEST_PREF, true); + Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true); + + setSuggestionsFn(searchStr => { + let prefixes = ["baz", "quux"]; + return prefixes.map(p => p + " " + searchStr); + }); + + let context = createContext(SEARCH_STRING, { isPrivate: false }); + + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + ...makeExpectedFormHistoryResults(context), + makeSearchResult(context, { + engineName: ENGINE_NAME, + suggestion: "baz " + SEARCH_STRING, + }), + makeSearchResult(context, { + engineName: ENGINE_NAME, + suggestion: "quux " + SEARCH_STRING, + }), + ], + }); + + await cleanUpSuggestions(); +}); + +add_task(async function queryIsNotASubstring() { + Services.prefs.setBoolPref(SUGGEST_PREF, true); + + setSuggestionsFn(searchStr => { + return ["aaa", "bbb"]; + }); + + let context = createContext(SEARCH_STRING, { isPrivate: false }); + + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + ...makeExpectedFormHistoryResults(context), + makeSearchResult(context, { + engineName: ENGINE_NAME, + suggestion: "aaa", + }), + makeSearchResult(context, { + engineName: ENGINE_NAME, + suggestion: "bbb", + }), + ], + }); + + await cleanUpSuggestions(); +}); + +add_task(async function restrictToken() { + Services.prefs.setBoolPref(SUGGEST_PREF, true); + Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true); + + // Add a visit and a bookmark. Actually, make the bookmark visited too so + // that it's guaranteed, with its higher frecency, to appear above the search + // suggestions. + await PlacesTestUtils.addVisits([ + { + uri: Services.io.newURI(`http://example.com/${SEARCH_STRING}-visit`), + title: `${SEARCH_STRING} visit`, + }, + { + uri: Services.io.newURI(`http://example.com/${SEARCH_STRING}-bookmark`), + title: `${SEARCH_STRING} bookmark`, + }, + ]); + + await PlacesTestUtils.addBookmarkWithDetails({ + uri: Services.io.newURI(`http://example.com/${SEARCH_STRING}-bookmark`), + title: `${SEARCH_STRING} bookmark`, + }); + + let context = createContext(SEARCH_STRING, { isPrivate: false }); + + // Do an unrestricted search to make sure everything appears in it, including + // the visit and bookmark. + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + makeBookmarkResult(context, { + uri: `http://example.com/${SEARCH_STRING}-bookmark`, + title: `${SEARCH_STRING} bookmark`, + }), + makeVisitResult(context, { + uri: `http://example.com/${SEARCH_STRING}-visit`, + title: `${SEARCH_STRING} visit`, + }), + ...makeExpectedSuggestionResults(context), + ], + }); + + // Now do a restricted search to make sure only suggestions appear. + context = createContext( + `${UrlbarTokenizer.RESTRICT.SEARCH} ${SEARCH_STRING}`, + { + isPrivate: false, + } + ); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: ENGINE_NAME, + alias: UrlbarTokenizer.RESTRICT.SEARCH, + query: SEARCH_STRING, + heuristic: true, + }), + ...makeExpectedSuggestionResults(context, { + suggestionPrefix: SEARCH_STRING, + query: SEARCH_STRING, + }), + ], + }); + + // Typing the search restriction char shows the Search Engine entry and local + // results. + context = createContext(UrlbarTokenizer.RESTRICT.SEARCH, { + isPrivate: false, + }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: ENGINE_NAME, + query: "", + heuristic: true, + }), + ...makeExpectedFormHistoryResults(context), + ], + }); + + // Also if followed by multiple spaces. + context = createContext(`${UrlbarTokenizer.RESTRICT.SEARCH} `, { + isPrivate: false, + }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: ENGINE_NAME, + alias: UrlbarTokenizer.RESTRICT.SEARCH, + query: "", + heuristic: true, + }), + ...makeExpectedFormHistoryResults(context), + ], + }); + + // If followed by any char we should fetch suggestions. + // Note this uses "h" to match form history. + context = createContext(`${UrlbarTokenizer.RESTRICT.SEARCH}h`, { + isPrivate: false, + }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: ENGINE_NAME, + query: "h", + heuristic: true, + }), + ...makeExpectedSuggestionResults(context, { + suggestionPrefix: "h", + query: "h", + }), + ], + }); + + // Also if followed by a space and single char. + context = createContext(`${UrlbarTokenizer.RESTRICT.SEARCH} h`, { + isPrivate: false, + }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: ENGINE_NAME, + alias: UrlbarTokenizer.RESTRICT.SEARCH, + query: "h", + heuristic: true, + }), + ...makeExpectedSuggestionResults(context, { + suggestionPrefix: "h", + query: "h", + }), + ], + }); + + // Leading search-mode restriction tokens are removed. + context = createContext( + `${UrlbarTokenizer.RESTRICT.BOOKMARK} ${SEARCH_STRING}`, + { isPrivate: false } + ); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + heuristic: true, + query: SEARCH_STRING, + alias: UrlbarTokenizer.RESTRICT.BOOKMARK, + }), + makeBookmarkResult(context, { + uri: `http://example.com/${SEARCH_STRING}-bookmark`, + title: `${SEARCH_STRING} bookmark`, + }), + ], + }); + + // Non-search-mode restriction tokens remain in the query and heuristic search + // result. + let token; + for (let t of Object.values(UrlbarTokenizer.RESTRICT)) { + if (!UrlbarTokenizer.SEARCH_MODE_RESTRICT.has(t)) { + token = t; + break; + } + } + Assert.ok( + token, + "Non-search-mode restrict token exists -- if not, you can probably remove me!" + ); + context = createContext(token, { + isPrivate: false, + }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + ], + }); + + await cleanUpSuggestions(); +}); + +add_task(async function mixup_frecency() { + Services.prefs.setBoolPref(SUGGEST_PREF, true); + // At most, we should have 14 results in this subtest. We set this to 20 to + // make we're not cutting off any results and we are actually getting 12. + Services.prefs.setIntPref(MAX_RICH_RESULTS_PREF, 20); + + // Add a visit and a bookmark. Actually, make the bookmark visited too so + // that it's guaranteed, with its higher frecency, to appear above the search + // suggestions. + await PlacesTestUtils.addVisits([ + { + uri: Services.io.newURI("http://example.com/lo0"), + title: `${SEARCH_STRING} low frecency 0`, + }, + { + uri: Services.io.newURI("http://example.com/lo1"), + title: `${SEARCH_STRING} low frecency 1`, + }, + { + uri: Services.io.newURI("http://example.com/lo2"), + title: `${SEARCH_STRING} low frecency 2`, + }, + { + uri: Services.io.newURI("http://example.com/lo3"), + title: `${SEARCH_STRING} low frecency 3`, + }, + { + uri: Services.io.newURI("http://example.com/lo4"), + title: `${SEARCH_STRING} low frecency 4`, + }, + ]); + + for (let i = 0; i < 5; i++) { + await PlacesTestUtils.addVisits([ + { + uri: Services.io.newURI("http://example.com/hi0"), + title: `${SEARCH_STRING} high frecency 0`, + transition: Ci.nsINavHistoryService.TRANSITION_TYPED, + }, + { + uri: Services.io.newURI("http://example.com/hi1"), + title: `${SEARCH_STRING} high frecency 1`, + transition: Ci.nsINavHistoryService.TRANSITION_TYPED, + }, + { + uri: Services.io.newURI("http://example.com/hi2"), + title: `${SEARCH_STRING} high frecency 2`, + transition: Ci.nsINavHistoryService.TRANSITION_TYPED, + }, + { + uri: Services.io.newURI("http://example.com/hi3"), + title: `${SEARCH_STRING} high frecency 3`, + transition: Ci.nsINavHistoryService.TRANSITION_TYPED, + }, + ]); + } + + for (let i = 0; i < 4; i++) { + let href = `http://example.com/hi${i}`; + await PlacesTestUtils.addBookmarkWithDetails({ + uri: href, + title: `${SEARCH_STRING} high frecency ${i}`, + }); + } + + // Do an unrestricted search to make sure everything appears in it, including + // the visit and bookmark. + let context = createContext(SEARCH_STRING, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + makeBookmarkResult(context, { + uri: "http://example.com/hi3", + title: `${SEARCH_STRING} high frecency 3`, + }), + makeBookmarkResult(context, { + uri: "http://example.com/hi2", + title: `${SEARCH_STRING} high frecency 2`, + }), + makeBookmarkResult(context, { + uri: "http://example.com/hi1", + title: `${SEARCH_STRING} high frecency 1`, + }), + makeBookmarkResult(context, { + uri: "http://example.com/hi0", + title: `${SEARCH_STRING} high frecency 0`, + }), + makeVisitResult(context, { + uri: "http://example.com/lo4", + title: `${SEARCH_STRING} low frecency 4`, + }), + ...makeExpectedSuggestionResults(context), + makeVisitResult(context, { + uri: "http://example.com/lo3", + title: `${SEARCH_STRING} low frecency 3`, + }), + makeVisitResult(context, { + uri: "http://example.com/lo2", + title: `${SEARCH_STRING} low frecency 2`, + }), + makeVisitResult(context, { + uri: "http://example.com/lo1", + title: `${SEARCH_STRING} low frecency 1`, + }), + makeVisitResult(context, { + uri: "http://example.com/lo0", + title: `${SEARCH_STRING} low frecency 0`, + }), + ], + }); + + // Change the "general" context mixup. + Services.prefs.setCharPref( + "browser.urlbar.matchBuckets", + "suggestion:1,general:5,suggestion:1" + ); + + // Do an unrestricted search to make sure everything appears in it, including + // the visits and bookmarks. + context = createContext(SEARCH_STRING, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + ...makeExpectedSuggestionResults(context).slice(0, 1), + makeBookmarkResult(context, { + uri: "http://example.com/hi3", + title: `${SEARCH_STRING} high frecency 3`, + }), + makeBookmarkResult(context, { + uri: "http://example.com/hi2", + title: `${SEARCH_STRING} high frecency 2`, + }), + makeBookmarkResult(context, { + uri: "http://example.com/hi1", + title: `${SEARCH_STRING} high frecency 1`, + }), + makeBookmarkResult(context, { + uri: "http://example.com/hi0", + title: `${SEARCH_STRING} high frecency 0`, + }), + makeVisitResult(context, { + uri: "http://example.com/lo4", + title: `${SEARCH_STRING} low frecency 4`, + }), + ...makeExpectedSuggestionResults(context).slice(1), + makeVisitResult(context, { + uri: "http://example.com/lo3", + title: `${SEARCH_STRING} low frecency 3`, + }), + makeVisitResult(context, { + uri: "http://example.com/lo2", + title: `${SEARCH_STRING} low frecency 2`, + }), + makeVisitResult(context, { + uri: "http://example.com/lo1", + title: `${SEARCH_STRING} low frecency 1`, + }), + makeVisitResult(context, { + uri: "http://example.com/lo0", + title: `${SEARCH_STRING} low frecency 0`, + }), + ], + }); + + // Change the "search" context mixup. + Services.prefs.setCharPref( + "browser.urlbar.matchBucketsSearch", + "suggestion:2,general:4" + ); + + context = createContext(SEARCH_STRING, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + ...makeExpectedSuggestionResults(context).slice(0, 2), + makeBookmarkResult(context, { + uri: "http://example.com/hi3", + title: `${SEARCH_STRING} high frecency 3`, + }), + makeBookmarkResult(context, { + uri: "http://example.com/hi2", + title: `${SEARCH_STRING} high frecency 2`, + }), + makeBookmarkResult(context, { + uri: "http://example.com/hi1", + title: `${SEARCH_STRING} high frecency 1`, + }), + makeBookmarkResult(context, { + uri: "http://example.com/hi0", + title: `${SEARCH_STRING} high frecency 0`, + }), + ...makeExpectedSuggestionResults(context).slice(2), + makeVisitResult(context, { + uri: "http://example.com/lo4", + title: `${SEARCH_STRING} low frecency 4`, + }), + makeVisitResult(context, { + uri: "http://example.com/lo3", + title: `${SEARCH_STRING} low frecency 3`, + }), + makeVisitResult(context, { + uri: "http://example.com/lo2", + title: `${SEARCH_STRING} low frecency 2`, + }), + makeVisitResult(context, { + uri: "http://example.com/lo1", + title: `${SEARCH_STRING} low frecency 1`, + }), + makeVisitResult(context, { + uri: "http://example.com/lo0", + title: `${SEARCH_STRING} low frecency 0`, + }), + ], + }); + + Services.prefs.setCharPref( + "browser.urlbar.matchBuckets", + MATCH_BUCKETS_VALUE + ); + Services.prefs.clearUserPref("browser.urlbar.matchBucketsSearch"); + Services.prefs.clearUserPref(MAX_RICH_RESULTS_PREF); + await cleanUpSuggestions(); +}); + +add_task(async function prohibit_suggestions() { + Services.prefs.setBoolPref(SUGGEST_PREF, true); + Services.prefs.setBoolPref( + `browser.fixup.domainwhitelist.${SEARCH_STRING}`, + false + ); + + let context = createContext(SEARCH_STRING, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + ...makeExpectedSuggestionResults(context), + ], + }); + + Services.prefs.setBoolPref( + `browser.fixup.domainwhitelist.${SEARCH_STRING}`, + true + ); + registerCleanupFunction(() => { + Services.prefs.setBoolPref( + `browser.fixup.domainwhitelist.${SEARCH_STRING}`, + false + ); + }); + context = createContext(SEARCH_STRING, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: `http://${SEARCH_STRING}/`, + title: `http://${SEARCH_STRING}/`, + iconUri: "", + heuristic: true, + }), + makeSearchResult(context, { + engineName: ENGINE_NAME, + heuristic: false, + }), + ...makeExpectedFormHistoryResults(context), + ], + }); + + // When using multiple words, we should still get suggestions: + let query = `${SEARCH_STRING} world`; + context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + ...makeExpectedSuggestionResults(context, { suggestionPrefix: query }), + ], + }); + + // Clear the whitelist for SEARCH_STRING and try preferring DNS for any single + // word instead: + Services.prefs.setBoolPref( + `browser.fixup.domainwhitelist.${SEARCH_STRING}`, + false + ); + Services.prefs.setBoolPref("browser.fixup.dns_first_for_single_words", true); + registerCleanupFunction(() => { + Services.prefs.clearUserPref("browser.fixup.dns_first_for_single_words"); + }); + + context = createContext(SEARCH_STRING, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: `http://${SEARCH_STRING}/`, + title: `http://${SEARCH_STRING}/`, + iconUri: "", + heuristic: true, + }), + makeSearchResult(context, { + engineName: ENGINE_NAME, + heuristic: false, + }), + ...makeExpectedFormHistoryResults(context), + ], + }); + + context = createContext("somethingelse", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: "http://somethingelse/", + title: "http://somethingelse/", + iconUri: "", + heuristic: true, + }), + makeSearchResult(context, { + engineName: ENGINE_NAME, + heuristic: false, + }), + ], + }); + + // When using multiple words, we should still get suggestions: + query = `${SEARCH_STRING} world`; + context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + ...makeExpectedSuggestionResults(context, { suggestionPrefix: query }), + ], + }); + + Services.prefs.clearUserPref("browser.fixup.dns_first_for_single_words"); + + context = createContext("http://1.2.3.4/", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: "http://1.2.3.4/", + title: "http://1.2.3.4/", + iconUri: "page-icon:http://1.2.3.4/", + heuristic: true, + }), + ], + }); + + context = createContext("[2001::1]:30", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: "http://[2001::1]:30/", + title: "http://[2001::1]:30/", + iconUri: "", + heuristic: true, + }), + ], + }); + + context = createContext("user:pass@test", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: "http://user:pass@test/", + title: "http://user:pass@test/", + iconUri: "", + heuristic: true, + }), + ], + }); + + context = createContext("data:text/plain,Content", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: "data:text/plain,Content", + title: "data:text/plain,Content", + iconUri: "", + heuristic: true, + }), + ], + }); + + context = createContext("a", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + ], + }); + + await cleanUpSuggestions(); +}); + +add_task(async function uri_like_queries() { + Services.prefs.setBoolPref(SUGGEST_PREF, true); + Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true); + + // We should not fetch any suggestions for an actual URL. + let query = "mozilla.org"; + let context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + title: `http://${query}/`, + uri: `http://${query}/`, + iconUri: "", + heuristic: true, + }), + makeSearchResult(context, { query, engineName: ENGINE_NAME }), + ], + }); + + // We should also not fetch suggestions for a partially-typed URL. + query = "mozilla.o"; + context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + ], + }); + + // Now trying queries that could be confused for URLs. They should return + // results. + const uriLikeQueries = [ + "mozilla.org is a great website", + "I like mozilla.org", + "a/b testing", + "he/him", + "Google vs.", + "5.8 cm", + ]; + for (query of uriLikeQueries) { + context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + ...makeExpectedRemoteSuggestionResults(context, { + suggestionPrefix: query, + }), + ], + }); + } + + await cleanUpSuggestions(); +}); + +add_task(async function avoid_remote_url_suggestions_1() { + Services.prefs.setBoolPref(SUGGEST_PREF, true); + Services.prefs.setIntPref(MAX_FORM_HISTORY_PREF, 1); + + setSuggestionsFn(searchStr => { + let suffixes = [".com", "/test", ":1]", "@test", ". com"]; + return suffixes.map(s => searchStr + s); + }); + + const query = "test"; + + await UrlbarTestUtils.formHistory.add([`${query}.com`]); + + let context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + makeFormHistoryResult(context, { + engineName: ENGINE_NAME, + suggestion: `${query}.com`, + }), + makeSearchResult(context, { + engineName: ENGINE_NAME, + suggestion: `${query}. com`, + }), + ], + }); + + await cleanUpSuggestions(); + await UrlbarTestUtils.formHistory.remove([`${query}.com`]); + Services.prefs.clearUserPref(MAX_FORM_HISTORY_PREF); +}); + +add_task(async function avoid_remote_url_suggestions_2() { + Services.prefs.setBoolPref(SUGGEST_PREF, true); + Services.prefs.setBoolPref("browser.urlbar.autoFill", false); + + setSuggestionsFn(searchStr => { + let suffixes = ["ed", "eds"]; + return suffixes.map(s => searchStr + s); + }); + + let context = createContext("htt", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + makeSearchResult(context, { + engineName: ENGINE_NAME, + suggestion: "htted", + }), + makeSearchResult(context, { + engineName: ENGINE_NAME, + suggestion: "htteds", + }), + ], + }); + + context = createContext("ftp", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + makeSearchResult(context, { + engineName: ENGINE_NAME, + suggestion: "ftped", + }), + makeSearchResult(context, { + engineName: ENGINE_NAME, + suggestion: "ftpeds", + }), + ], + }); + + context = createContext("http", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + makeSearchResult(context, { + engineName: ENGINE_NAME, + suggestion: "httped", + }), + makeSearchResult(context, { + engineName: ENGINE_NAME, + suggestion: "httpeds", + }), + ], + }); + + context = createContext("http:", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + ], + }); + + context = createContext("https", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + makeSearchResult(context, { + engineName: ENGINE_NAME, + suggestion: "httpsed", + }), + makeSearchResult(context, { + engineName: ENGINE_NAME, + suggestion: "httpseds", + }), + ], + }); + + context = createContext("https:", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + ], + }); + + context = createContext("httpd", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + makeSearchResult(context, { + engineName: ENGINE_NAME, + suggestion: "httpded", + }), + makeSearchResult(context, { + engineName: ENGINE_NAME, + suggestion: "httpdeds", + }), + ], + }); + + // Check FTP enabled + Services.prefs.setBoolPref("network.ftp.enabled", true); + registerCleanupFunction(() => + Services.prefs.clearUserPref("network.ftp.enabled") + ); + + context = createContext("ftp:", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + ], + }); + + context = createContext("ftp:/", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + ], + }); + + context = createContext("ftp://", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + ], + }); + + context = createContext("ftp://test", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: "ftp://test/", + title: "ftp://test/", + iconUri: "", + heuristic: true, + }), + ], + }); + + // Check FTP disabled + Services.prefs.setBoolPref("network.ftp.enabled", false); + context = createContext("ftp:", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + ], + }); + + context = createContext("ftp:/", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + ], + }); + + context = createContext("ftp://", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + ], + }); + + context = createContext("ftp://test", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: "ftp://test/", + title: "ftp://test/", + iconUri: "", + heuristic: true, + }), + ], + }); + + context = createContext("http:/", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + ], + }); + + context = createContext("https:/", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + ], + }); + + context = createContext("http://", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + ], + }); + + context = createContext("https://", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + ], + }); + + context = createContext("http://www", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: "http://www/", + title: "http://www/", + iconUri: "", + heuristic: true, + }), + ], + }); + + context = createContext("https://www", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: "https://www/", + title: "https://www/", + iconUri: "", + heuristic: true, + }), + ], + }); + + context = createContext("http://test", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: "http://test/", + title: "http://test/", + iconUri: "", + heuristic: true, + }), + ], + }); + + context = createContext("https://test", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: "https://test/", + title: "https://test/", + iconUri: "", + heuristic: true, + }), + ], + }); + + context = createContext("http://www.test", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: "http://www.test/", + title: "http://www.test/", + iconUri: "", + heuristic: true, + }), + ], + }); + + context = createContext("http://www.test.com", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: "http://www.test.com/", + title: "http://www.test.com/", + iconUri: "", + heuristic: true, + }), + ], + }); + + context = createContext("file", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + makeSearchResult(context, { + engineName: ENGINE_NAME, + suggestion: "fileed", + }), + makeSearchResult(context, { + engineName: ENGINE_NAME, + suggestion: "fileeds", + }), + ], + }); + + context = createContext("file:", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + ], + }); + + context = createContext("file:///Users", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: "file:///Users", + title: "file:///Users", + iconUri: "", + heuristic: true, + }), + ], + }); + + context = createContext("moz-test://", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + ], + }); + + context = createContext("moz+test://", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + ], + }); + + context = createContext("about", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + makeSearchResult(context, { + engineName: ENGINE_NAME, + suggestion: "abouted", + }), + makeSearchResult(context, { + engineName: ENGINE_NAME, + suggestion: "abouteds", + }), + ], + }); + + context = createContext("about:", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + ], + }); + + await cleanUpSuggestions(); +}); + +add_task(async function restrict_remote_suggestions_after_no_results() { + // We don't fetch remote suggestions if a query with a length over + // maxCharsForSearchSuggestions returns 0 results. We set it to 4 here to + // avoid constructing a 100+ character string. + Services.prefs.setIntPref("browser.urlbar.maxCharsForSearchSuggestions", 4); + setSuggestionsFn(searchStr => { + return []; + }); + + const query = SEARCH_STRING.substring(0, SEARCH_STRING.length - 1); + let context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + ...makeExpectedFormHistoryResults(context), + ], + }); + + context = createContext(SEARCH_STRING, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + ...makeExpectedFormHistoryResults(context), + // Because the previous search returned no suggestions, we will not fetch + // remote suggestions for this query that is just a longer version of the + // previous query. + ], + }); + + // Do one more search before resetting maxCharsForSearchSuggestions to reset + // the search suggestion provider's _lastLowResultsSearchSuggestion property. + // Otherwise it will be stuck at SEARCH_STRING, which interferes with + // subsequent tests. + context = createContext("not the search string", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + ], + }); + + Services.prefs.clearUserPref("browser.urlbar.maxCharsForSearchSuggestions"); + + await cleanUpSuggestions(); +}); + +add_task(async function formHistory() { + Services.prefs.setBoolPref(SUGGEST_PREF, true); + Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true); + + // Setting maxHistoricalSearchSuggestions = 0 is special and indicates that + // the user has opted out of form history, so we should include form history + // neither before the expected remote results nor after, unlike the other + // checks below, where remaining form history is included after the expected + // remote results. + Services.prefs.setIntPref(MAX_FORM_HISTORY_PREF, 0); + let context = createContext(SEARCH_STRING, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + ...makeExpectedRemoteSuggestionResults(context), + ], + }); + + Services.prefs.setIntPref(MAX_FORM_HISTORY_PREF, 1); + context = createContext(SEARCH_STRING, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + ...makeExpectedFormHistoryResults(context, 2).slice(0, 1), + ...makeExpectedRemoteSuggestionResults(context), + ...makeExpectedFormHistoryResults(context, 2).slice(1), + ], + }); + + Services.prefs.setIntPref(MAX_FORM_HISTORY_PREF, 2); + context = createContext(SEARCH_STRING, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + ...makeExpectedFormHistoryResults(context, 2), + ...makeExpectedRemoteSuggestionResults(context), + ], + }); + + // Do a search for exactly the suggestion of the first form history result. + // The heuristic's query should be the suggestion; the first form history + // result should not be included since it dupes the heuristic; the second form + // history result should not be included since it doesn't match; and both + // remote suggestions should be included. + let firstSuggestion = makeExpectedFormHistoryResults(context)[0].payload + .suggestion; + context = createContext(firstSuggestion, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + ...makeExpectedRemoteSuggestionResults(context, { + suggestionPrefix: firstSuggestion, + }), + ], + }); + + // Add these form history strings to use below. + let formHistoryStrings = ["foo", "foobar", "fooquux"]; + await UrlbarTestUtils.formHistory.add(formHistoryStrings); + + // Search for "foo". "foo" shouldn't be included since it dupes the + // heuristic. Both "foobar" and "fooquux" should be included even though the + // max form history count is only two and there are three matching form + // history results (including "foo"). + context = createContext("foo", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + makeFormHistoryResult(context, { + suggestion: "foobar", + engineName: ENGINE_NAME, + }), + ...makeExpectedRemoteSuggestionResults(context, { + suggestionPrefix: "foo", + }), + // Note that the second form history result appears after the remote + // suggestions. This isn't ideal because it should appear right after the + // first form history result, but it doesn't because the actual first form + // history result duped the heuristic, so the muxer discarded it. + makeFormHistoryResult(context, { + suggestion: "fooquux", + engineName: ENGINE_NAME, + }), + ], + }); + + // Add a visit that matches "foo" and will autofill so that the heuristic is + // not a search result. Now the "foo" and "foobar" form history should be + // included. + await PlacesTestUtils.addVisits("http://foo.example.com/"); + context = createContext("foo", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.HISTORY, + uri: "http://foo.example.com/", + title: "foo.example.com", + heuristic: true, + }), + makeFormHistoryResult(context, { + suggestion: "foo", + engineName: ENGINE_NAME, + }), + makeFormHistoryResult(context, { + suggestion: "foobar", + engineName: ENGINE_NAME, + }), + ...makeExpectedRemoteSuggestionResults(context, { + suggestionPrefix: "foo", + }), + makeFormHistoryResult(context, { + suggestion: "fooquux", + engineName: ENGINE_NAME, + }), + ], + }); + await PlacesUtils.history.clear(); + + // Add SERPs for "foobar" and "food" and search for "foo". The "foo" form + // history should be excluded since it dupes the heuristic; the "foobar" and + // "fooquux" form history should be included; the "food" SERP should be + // included since it doesn't dupe either form history result; and the "foobar" + // SERP depends on the match buckets, see below. + let engine = await Services.search.getDefault(); + let [serpURL1] = UrlbarUtils.getSearchQueryUrl(engine, "foobar"); + let [serpURL2] = UrlbarUtils.getSearchQueryUrl(engine, "food"); + await PlacesTestUtils.addVisits([serpURL1, serpURL2]); + + // First, use the MATCH_BUCKETS_VALUE that the test set above. General + // results appear before suggestions, which means that the muxer visits the + // "foobar" SERP before visiting the "foobar" form history, and so it doesn't + // see that the SERP dupes the form history. The "foobar" SERP is therefore + // included. + context = createContext("foo", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + makeVisitResult(context, { + uri: "http://localhost:9000/search?terms=food", + title: "test visit for http://localhost:9000/search?terms=food", + }), + makeVisitResult(context, { + uri: "http://localhost:9000/search?terms=foobar", + title: "test visit for http://localhost:9000/search?terms=foobar", + }), + makeFormHistoryResult(context, { + suggestion: "foobar", + engineName: ENGINE_NAME, + }), + ...makeExpectedRemoteSuggestionResults(context, { + suggestionPrefix: "foo", + }), + makeFormHistoryResult(context, { + suggestion: "fooquux", + engineName: ENGINE_NAME, + }), + ], + }); + + // Now use Firefox's default match buckets, where suggestions appear before + // general results. Now the muxer will see that the "foobar" SERP dupes the + // "foobar" form history, so it will exclude the SERP. + Services.prefs.setCharPref( + "browser.urlbar.matchBuckets", + "suggestion:4,general:Infinity" + ); + context = createContext("foo", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + // Note that the remote suggestions appear in between the two form history + // results. Ideally the form history would appear together before the + // remote suggestions, but they don't because the actual first form + // history result duped the heuristic, so the muxer discarded it. + makeFormHistoryResult(context, { + suggestion: "foobar", + engineName: ENGINE_NAME, + }), + ...makeExpectedRemoteSuggestionResults(context, { + suggestionPrefix: "foo", + }), + makeFormHistoryResult(context, { + suggestion: "fooquux", + engineName: ENGINE_NAME, + }), + makeVisitResult(context, { + uri: "http://localhost:9000/search?terms=food", + title: "test visit for http://localhost:9000/search?terms=food", + }), + ], + }); + Services.prefs.setCharPref( + "browser.urlbar.matchBuckets", + MATCH_BUCKETS_VALUE + ); + + await PlacesUtils.history.clear(); + + await UrlbarTestUtils.formHistory.remove(formHistoryStrings); + + await cleanUpSuggestions(); + await PlacesUtils.history.clear(); + Services.prefs.clearUserPref(MAX_FORM_HISTORY_PREF); +}); diff --git a/browser/components/urlbar/tests/unit/test_search_suggestions_aliases.js b/browser/components/urlbar/tests/unit/test_search_suggestions_aliases.js new file mode 100644 index 0000000000..09a671b635 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_search_suggestions_aliases.js @@ -0,0 +1,359 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that an engine with suggestions works with our alias autocomplete + * behavior. + */ + +const DEFAULT_ENGINE_NAME = "TestDefaultEngine"; +const SUGGESTIONS_ENGINE_NAME = "engine-suggestions.xml"; +const SUGGEST_PREF = "browser.urlbar.suggest.searches"; +const SUGGEST_ENABLED_PREF = "browser.search.suggest.enabled"; +const HISTORY_TITLE = "fire"; + +// We make sure that aliases and search terms are correctly recognized when they +// are separated by each of these different types of spaces and combinations of +// spaces. U+3000 is the ideographic space in CJK and is commonly used by CJK +// speakers. +const TEST_SPACES = [" ", "\u3000", " \u3000", "\u3000 "]; + +let engine; + +add_task(async function setup() { + engine = await addTestSuggestionsEngine(); + + // Set a mock engine as the default so we don't hit the network below when we + // do searches that return the default engine heuristic result. + Services.search.defaultEngine = await Services.search.addEngineWithDetails( + DEFAULT_ENGINE_NAME, + { template: "http://example.com/?s=%S" } + ); + + // History matches should not appear with @aliases, so this visit should not + // appear when searching with @aliases below. + await PlacesTestUtils.addVisits({ + uri: engine.searchForm, + title: HISTORY_TITLE, + }); +}); + +// A non-token alias without a trailing space shouldn't be recognized as a +// keyword. It should be treated as part of the search string. +add_task(async function nonTokenAlias_noTrailingSpace() { + Services.prefs.setBoolPref( + "browser.search.separatePrivateDefault.ui.enabled", + false + ); + + let alias = "moz"; + engine.alias = alias; + Assert.equal(engine.alias, alias); + let context = createContext(alias, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: DEFAULT_ENGINE_NAME, + query: alias, + heuristic: true, + }), + ], + }); + Services.prefs.clearUserPref( + "browser.search.separatePrivateDefault.ui.enabled" + ); +}); + +// A non-token alias with a trailing space should be recognized as a keyword, +// and the history result should be included. +add_task(async function nonTokenAlias_trailingSpace() { + let alias = "moz"; + engine.alias = alias; + Assert.equal(engine.alias, alias); + for (let isPrivate of [false, true]) { + for (let spaces of TEST_SPACES) { + info( + "Testing: " + JSON.stringify({ isPrivate, spaces: codePoints(spaces) }) + ); + let context = createContext(alias + spaces, { isPrivate }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + alias, + query: "", + heuristic: true, + }), + makeVisitResult(context, { + uri: "http://localhost:9000/search?terms=", + title: HISTORY_TITLE, + }), + ], + }); + } + } +}); + +// Search for "alias HISTORY_TITLE" with a non-token alias in a non-private +// context. The remote suggestions and history result should be shown. +add_task(async function nonTokenAlias_history_nonPrivate() { + let alias = "moz"; + engine.alias = alias; + Assert.equal(engine.alias, alias); + for (let spaces of TEST_SPACES) { + info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) })); + let context = createContext(alias + spaces + HISTORY_TITLE, { + isPrivate: false, + }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + alias, + query: HISTORY_TITLE, + heuristic: true, + }), + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + alias, + query: HISTORY_TITLE, + suggestion: `${HISTORY_TITLE} foo`, + }), + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + alias, + query: HISTORY_TITLE, + suggestion: `${HISTORY_TITLE} bar`, + }), + makeVisitResult(context, { + uri: "http://localhost:9000/search?terms=", + title: HISTORY_TITLE, + }), + ], + }); + } +}); + +// Search for "alias HISTORY_TITLE" with a non-token alias in a private context. +// The history result should be shown, but not the remote suggestions. +add_task(async function nonTokenAlias_history_private() { + let alias = "moz"; + engine.alias = alias; + Assert.equal(engine.alias, alias); + for (let spaces of TEST_SPACES) { + info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) })); + let context = createContext(alias + spaces + HISTORY_TITLE, { + isPrivate: true, + }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + alias, + query: HISTORY_TITLE, + heuristic: true, + }), + makeVisitResult(context, { + uri: "http://localhost:9000/search?terms=", + title: HISTORY_TITLE, + }), + ], + }); + } +}); + +// A token alias without a trailing space should be autofilled with a trailing +// space and recognized as a keyword with a keyword offer. +add_task(async function tokenAlias_noTrailingSpace() { + let alias = "@moz"; + engine.alias = alias; + Assert.equal(engine.alias, alias); + for (let isPrivate of [false, true]) { + let context = createContext(alias, { isPrivate }); + await check_results({ + context, + autofilled: alias + " ", + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + alias, + providesSearchMode: true, + query: "", + heuristic: false, + }), + ], + }); + } +}); + +// A token alias with a trailing space should be recognized as a keyword without +// a keyword offer. +add_task(async function tokenAlias_trailingSpace() { + let alias = "@moz"; + engine.alias = alias; + Assert.equal(engine.alias, alias); + for (let isPrivate of [false, true]) { + for (let spaces of TEST_SPACES) { + info( + "Testing: " + JSON.stringify({ isPrivate, spaces: codePoints(spaces) }) + ); + let context = createContext(alias + spaces, { isPrivate }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + alias, + query: "", + heuristic: true, + }), + ], + }); + } + } +}); + +// Search for "alias HISTORY_TITLE" with a token alias in a non-private context. +// The remote suggestions should be shown, but not the history result. +add_task(async function tokenAlias_history_nonPrivate() { + let alias = "@moz"; + engine.alias = alias; + Assert.equal(engine.alias, alias); + for (let spaces of TEST_SPACES) { + info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) })); + let context = createContext(alias + spaces + HISTORY_TITLE, { + isPrivate: false, + }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + alias, + query: HISTORY_TITLE, + heuristic: true, + }), + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + alias, + query: HISTORY_TITLE, + suggestion: `${HISTORY_TITLE} foo`, + }), + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + alias, + query: HISTORY_TITLE, + suggestion: `${HISTORY_TITLE} bar`, + }), + ], + }); + } +}); + +// Search for "alias HISTORY_TITLE" with a token alias in a private context. +// Neither the history result nor the remote suggestions should be shown. +add_task(async function tokenAlias_history_private() { + let alias = "@moz"; + engine.alias = alias; + Assert.equal(engine.alias, alias); + for (let spaces of TEST_SPACES) { + info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) })); + let context = createContext(alias + spaces + HISTORY_TITLE, { + isPrivate: true, + }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + alias, + query: HISTORY_TITLE, + heuristic: true, + }), + ], + }); + } +}); + +// Even when they're disabled, suggestions should still be returned when using a +// token alias in a non-private context. +add_task(async function suggestionsDisabled_nonPrivate() { + Services.prefs.setBoolPref(SUGGEST_PREF, false); + Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true); + let alias = "@moz"; + engine.alias = alias; + Assert.equal(engine.alias, alias); + for (let spaces of TEST_SPACES) { + info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) })); + let context = createContext(alias + spaces + "term", { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + alias, + query: "term", + heuristic: true, + }), + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + alias, + query: "term", + suggestion: "term foo", + }), + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + alias, + query: "term", + suggestion: "term bar", + }), + ], + }); + } + Services.prefs.clearUserPref(SUGGEST_PREF); + Services.prefs.clearUserPref(SUGGEST_ENABLED_PREF); +}); + +// Suggestions should not be returned when using a token alias in a private +// context. +add_task(async function suggestionsDisabled_private() { + Services.prefs.setBoolPref(SUGGEST_PREF, false); + Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true); + let alias = "@moz"; + engine.alias = alias; + Assert.equal(engine.alias, alias); + for (let spaces of TEST_SPACES) { + info("Testing: " + JSON.stringify({ spaces: codePoints(spaces) })); + let context = createContext(alias + spaces + "term", { isPrivate: true }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: SUGGESTIONS_ENGINE_NAME, + alias, + query: "term", + heuristic: true, + }), + ], + }); + Services.prefs.clearUserPref(SUGGEST_PREF); + Services.prefs.clearUserPref(SUGGEST_ENABLED_PREF); + } +}); + +/** + * Returns an array of code points in the given string. Each code point is + * returned as a hexidecimal string. + * + * @param {string} str + * The code points of this string will be returned. + * @returns {array} + * Array of code points in the string, where each is a hexidecimal string. + */ +function codePoints(str) { + return str.split("").map(s => s.charCodeAt(0).toString(16)); +} diff --git a/browser/components/urlbar/tests/unit/test_search_suggestions_tail.js b/browser/components/urlbar/tests/unit/test_search_suggestions_tail.js new file mode 100644 index 0000000000..6d690c1b99 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_search_suggestions_tail.js @@ -0,0 +1,358 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that tailed search engine suggestions are returned by + * UrlbarProviderSearchSuggestions when available. + */ + +const { FormHistory } = ChromeUtils.import( + "resource://gre/modules/FormHistory.jsm" +); + +const ENGINE_NAME = "engine-tail-suggestions.xml"; +const SUGGEST_PREF = "browser.urlbar.suggest.searches"; +const SUGGEST_ENABLED_PREF = "browser.search.suggest.enabled"; +const PRIVATE_SEARCH_PREF = "browser.search.separatePrivateDefault.ui.enabled"; +const TAIL_SUGGESTIONS_PREF = "browser.urlbar.richSuggestions.tail"; + +var suggestionsFn; +var previousSuggestionsFn; + +/** + * Set the current suggestion funciton. + * @param {function} fn + * A function that that a search string and returns an array of strings that + * will be used as search suggestions. + * Note: `fn` should return > 1 suggestion in most cases. Otherwise, you may + * encounter unexceptede behaviour with UrlbarProviderSuggestion's + * _lastLowResultsSearchSuggestion safeguard. + */ +function setSuggestionsFn(fn) { + previousSuggestionsFn = suggestionsFn; + suggestionsFn = fn; +} + +async function cleanup() { + await PlacesUtils.bookmarks.eraseEverything(); + await PlacesUtils.history.clear(); +} + +async function cleanUpSuggestions() { + await cleanup(); + if (previousSuggestionsFn) { + suggestionsFn = previousSuggestionsFn; + previousSuggestionsFn = null; + } +} + +add_task(async function setup() { + Services.prefs.setCharPref( + "browser.urlbar.matchBuckets", + "general:5,suggestion:Infinity" + ); + + let engine = await addTestTailSuggestionsEngine(searchStr => { + return suggestionsFn(searchStr); + }); + setSuggestionsFn(searchStr => { + let suffixes = ["toronto", "tunisia"]; + return [ + "what time is it in t", + suffixes.map(s => searchStr + s.slice(1)), + [], + { + "google:irrelevantparameter": [], + "google:suggestdetail": suffixes.map(s => ({ + mp: "… ", + t: s, + })), + }, + ]; + }); + + // Install the test engine. + let oldDefaultEngine = await Services.search.getDefault(); + registerCleanupFunction(async () => { + Services.search.setDefault(oldDefaultEngine); + Services.prefs.clearUserPref(PRIVATE_SEARCH_PREF); + Services.prefs.clearUserPref(TAIL_SUGGESTIONS_PREF); + Services.prefs.clearUserPref(SUGGEST_ENABLED_PREF); + }); + Services.search.setDefault(engine); + Services.prefs.setBoolPref(PRIVATE_SEARCH_PREF, false); + Services.prefs.setBoolPref(TAIL_SUGGESTIONS_PREF, true); + Services.prefs.setBoolPref(SUGGEST_ENABLED_PREF, true); +}); + +/** + * Tests that non-tail suggestion providers still return results correctly when + * the tailSuggestions pref is enabled. + */ +add_task(async function normal_suggestions_provider() { + let engine = await addTestSuggestionsEngine(); + let tailEngine = await Services.search.getDefault(); + Services.search.setDefault(engine); + + const query = "hello world"; + let context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { + engineName: "engine-suggestions.xml", + heuristic: true, + }), + makeSearchResult(context, { + engineName: "engine-suggestions.xml", + suggestion: query + " foo", + }), + makeSearchResult(context, { + engineName: "engine-suggestions.xml", + suggestion: query + " bar", + }), + ], + }); + + Services.search.setDefault(tailEngine); + await cleanUpSuggestions(); +}); + +/** + * Tests a suggestions provider that returns only tail suggestions. + */ +add_task(async function basic_tail() { + const query = "what time is it in t"; + let context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + makeSearchResult(context, { + engineName: ENGINE_NAME, + suggestion: query + "oronto", + tail: "toronto", + }), + makeSearchResult(context, { + engineName: ENGINE_NAME, + suggestion: query + "unisia", + tail: "tunisia", + }), + ], + }); + await cleanUpSuggestions(); +}); + +/** + * Tests a suggestions provider that returns both normal and tail suggestions. + * Only normal results should be shown. + */ +add_task(async function mixed_suggestions() { + // When normal suggestions are mixed with tail suggestions, they appear at the + // correct position in the google:suggestdetail array as empty objects. + setSuggestionsFn(searchStr => { + let suffixes = ["toronto", "tunisia"]; + return [ + "what time is it in t", + ["what is the time today texas"].concat( + suffixes.map(s => searchStr + s.slice(1)) + ), + [], + { + "google:irrelevantparameter": [], + "google:suggestdetail": [{}].concat( + suffixes.map(s => ({ + mp: "… ", + t: s, + })) + ), + }, + ]; + }); + + const query = "what time is it in t"; + let context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + makeSearchResult(context, { + engineName: ENGINE_NAME, + suggestion: "what is the time today texas", + tail: undefined, + }), + ], + }); + await cleanUpSuggestions(); +}); + +/** + * Tests a suggestions provider that returns both normal and tail suggestions, + * with tail suggestions listed before normal suggestions. In the real world + * we don't expect that to happen, but we should handle it by showing only the + * normal suggestions. + */ +add_task(async function mixed_suggestions_tail_first() { + setSuggestionsFn(searchStr => { + let suffixes = ["toronto", "tunisia"]; + return [ + "what time is it in t", + suffixes + .map(s => searchStr + s.slice(1)) + .concat(["what is the time today texas"]), + [], + { + "google:irrelevantparameter": [], + "google:suggestdetail": suffixes + .map(s => ({ + mp: "… ", + t: s, + })) + .concat([{}]), + }, + ]; + }); + + const query = "what time is it in t"; + let context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + makeSearchResult(context, { + engineName: ENGINE_NAME, + suggestion: "what is the time today texas", + tail: undefined, + }), + ], + }); + await cleanUpSuggestions(); +}); + +/** + * Tests a search that returns history results, bookmark results and tail + * suggestions. Only the history and bookmark results should be shown. + */ +add_task(async function mixed_results() { + await PlacesTestUtils.addVisits([ + { + uri: Services.io.newURI("http://example.com/1"), + title: "what time is", + }, + ]); + + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.com/2", + title: "what time is", + }); + + // Tail suggestions should not be shown. + const query = "what time is"; + let context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + makeBookmarkResult(context, { + uri: "http://example.com/2", + title: "what time is", + }), + makeVisitResult(context, { + uri: "http://example.com/1", + title: "what time is", + }), + ], + }); + + // Once we make the query specific enough to exclude the history and bookmark + // results, we should show tail suggestions. + const tQuery = "what time is it in t"; + context = createContext(tQuery, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + makeSearchResult(context, { + engineName: ENGINE_NAME, + suggestion: tQuery + "oronto", + tail: "toronto", + }), + makeSearchResult(context, { + engineName: ENGINE_NAME, + suggestion: tQuery + "unisia", + tail: "tunisia", + }), + ], + }); + + await cleanUpSuggestions(); +}); + +/** + * Tests that tail suggestions are deduped if their full-text form is a dupe of + * a local search suggestion. Remaining tail suggestions should also not be + * shown since we do not mix tail and non-tail suggestions. + */ +add_task(async function dedupe_local() { + Services.prefs.setIntPref("browser.urlbar.maxHistoricalSearchSuggestions", 1); + await UrlbarTestUtils.formHistory.add(["what time is it in toronto"]); + + const query = "what time is it in t"; + let context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + makeFormHistoryResult(context, { + engineName: ENGINE_NAME, + suggestion: query + "oronto", + }), + ], + }); + + Services.prefs.clearUserPref("browser.urlbar.maxHistoricalSearchSuggestions"); + await cleanUpSuggestions(); +}); + +/** + * Tests that the correct number of suggestion results are displayed if + * maxResults is limited, even when tail suggestions are returned. + */ +add_task(async function limit_results() { + await UrlbarTestUtils.formHistory.clear(); + const query = "what time is it in t"; + let context = createContext(query, { isPrivate: false }); + context.maxResults = 2; + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + makeSearchResult(context, { + engineName: ENGINE_NAME, + suggestion: query + "oronto", + tail: "toronto", + }), + ], + }); + await cleanUpSuggestions(); +}); + +/** + * Tests that tail suggestions are hidden if the pref is disabled. + */ +add_task(async function disable_pref() { + let oldPrefValue = Services.prefs.getBoolPref(TAIL_SUGGESTIONS_PREF); + Services.prefs.setBoolPref(TAIL_SUGGESTIONS_PREF, false); + const query = "what time is it in t"; + let context = createContext(query, { isPrivate: false }); + await check_results({ + context, + matches: [ + makeSearchResult(context, { engineName: ENGINE_NAME, heuristic: true }), + ], + }); + + Services.prefs.setBoolPref(TAIL_SUGGESTIONS_PREF, oldPrefValue); + await cleanUpSuggestions(); +}); diff --git a/browser/components/urlbar/tests/unit/test_tokenizer.js b/browser/components/urlbar/tests/unit/test_tokenizer.js new file mode 100644 index 0000000000..22dc629a46 --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_tokenizer.js @@ -0,0 +1,450 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(async function test_tokenizer() { + let testContexts = [ + { desc: "Empty string", searchString: "", expectedTokens: [] }, + { desc: "Spaces string", searchString: " ", expectedTokens: [] }, + { + desc: "Single word string", + searchString: "test", + expectedTokens: [{ value: "test", type: UrlbarTokenizer.TYPE.TEXT }], + }, + { + desc: "Multi word string with mixed whitespace types", + searchString: " test1 test2\u1680test3\u2004test4\u1680", + expectedTokens: [ + { value: "test1", type: UrlbarTokenizer.TYPE.TEXT }, + { value: "test2", type: UrlbarTokenizer.TYPE.TEXT }, + { value: "test3", type: UrlbarTokenizer.TYPE.TEXT }, + { value: "test4", type: UrlbarTokenizer.TYPE.TEXT }, + ], + }, + { + desc: "separate restriction char at beginning", + searchString: `${UrlbarTokenizer.RESTRICT.BOOKMARK} test`, + expectedTokens: [ + { + value: UrlbarTokenizer.RESTRICT.BOOKMARK, + type: UrlbarTokenizer.TYPE.RESTRICT_BOOKMARK, + }, + { value: "test", type: UrlbarTokenizer.TYPE.TEXT }, + ], + }, + { + desc: "separate restriction char at end", + searchString: `test ${UrlbarTokenizer.RESTRICT.BOOKMARK}`, + expectedTokens: [ + { value: "test", type: UrlbarTokenizer.TYPE.TEXT }, + { + value: UrlbarTokenizer.RESTRICT.BOOKMARK, + type: UrlbarTokenizer.TYPE.RESTRICT_BOOKMARK, + }, + ], + }, + { + desc: "boundary restriction char at end", + searchString: `test${UrlbarTokenizer.RESTRICT.BOOKMARK}`, + expectedTokens: [ + { + value: `test${UrlbarTokenizer.RESTRICT.BOOKMARK}`, + type: UrlbarTokenizer.TYPE.TEXT, + }, + ], + }, + { + desc: "boundary search restriction char at end", + searchString: `test${UrlbarTokenizer.RESTRICT.SEARCH}`, + expectedTokens: [ + { value: "test", type: UrlbarTokenizer.TYPE.TEXT }, + { + value: UrlbarTokenizer.RESTRICT.SEARCH, + type: UrlbarTokenizer.TYPE.RESTRICT_SEARCH, + }, + ], + }, + { + desc: "separate restriction char in the middle", + searchString: `test ${UrlbarTokenizer.RESTRICT.BOOKMARK} test`, + expectedTokens: [ + { value: "test", type: UrlbarTokenizer.TYPE.TEXT }, + { + value: UrlbarTokenizer.RESTRICT.BOOKMARK, + type: UrlbarTokenizer.TYPE.TEXT, + }, + { value: "test", type: UrlbarTokenizer.TYPE.TEXT }, + ], + }, + { + desc: "restriction char in the middle", + searchString: `test${UrlbarTokenizer.RESTRICT.BOOKMARK}test`, + expectedTokens: [ + { + value: `test${UrlbarTokenizer.RESTRICT.BOOKMARK}test`, + type: UrlbarTokenizer.TYPE.TEXT, + }, + ], + }, + { + desc: "restriction char in the middle 2", + searchString: `test${UrlbarTokenizer.RESTRICT.BOOKMARK} test`, + expectedTokens: [ + { + value: `test${UrlbarTokenizer.RESTRICT.BOOKMARK}`, + type: UrlbarTokenizer.TYPE.TEXT, + }, + { value: `test`, type: UrlbarTokenizer.TYPE.TEXT }, + ], + }, + { + desc: "double boundary restriction char", + searchString: `${UrlbarTokenizer.RESTRICT.BOOKMARK}test${UrlbarTokenizer.RESTRICT.TITLE}`, + expectedTokens: [ + { + value: UrlbarTokenizer.RESTRICT.BOOKMARK, + type: UrlbarTokenizer.TYPE.RESTRICT_BOOKMARK, + }, + { + value: `test${UrlbarTokenizer.RESTRICT.TITLE}`, + type: UrlbarTokenizer.TYPE.TEXT, + }, + ], + }, + { + desc: "double non-combinable restriction char, single char string", + searchString: `t${UrlbarTokenizer.RESTRICT.BOOKMARK}${UrlbarTokenizer.RESTRICT.SEARCH}`, + expectedTokens: [ + { + value: `t${UrlbarTokenizer.RESTRICT.BOOKMARK}`, + type: UrlbarTokenizer.TYPE.TEXT, + }, + { + value: UrlbarTokenizer.RESTRICT.SEARCH, + type: UrlbarTokenizer.TYPE.RESTRICT_SEARCH, + }, + ], + }, + { + desc: "only boundary restriction chars", + searchString: `${UrlbarTokenizer.RESTRICT.BOOKMARK}${UrlbarTokenizer.RESTRICT.TITLE}`, + expectedTokens: [ + { + value: UrlbarTokenizer.RESTRICT.BOOKMARK, + type: UrlbarTokenizer.TYPE.RESTRICT_BOOKMARK, + }, + { + value: UrlbarTokenizer.RESTRICT.TITLE, + type: UrlbarTokenizer.TYPE.RESTRICT_TITLE, + }, + ], + }, + { + desc: "only the boundary restriction char", + searchString: UrlbarTokenizer.RESTRICT.BOOKMARK, + expectedTokens: [ + { + value: UrlbarTokenizer.RESTRICT.BOOKMARK, + type: UrlbarTokenizer.TYPE.RESTRICT_BOOKMARK, + }, + ], + }, + // Some restriction chars may be # or ?, that are also valid path parts. + // The next 2 tests will check we consider those as part of url paths. + { + desc: "boundary # char on path", + searchString: "test/#", + expectedTokens: [ + { value: "test/#", type: UrlbarTokenizer.TYPE.POSSIBLE_URL }, + ], + }, + { + desc: "boundary ? char on path", + searchString: "test/?", + expectedTokens: [ + { value: "test/?", type: UrlbarTokenizer.TYPE.POSSIBLE_URL }, + ], + }, + { + desc: "multiple boundary restriction chars suffix", + searchString: `test ${UrlbarTokenizer.RESTRICT.HISTORY} ${UrlbarTokenizer.RESTRICT.TAG}`, + expectedTokens: [ + { value: "test", type: UrlbarTokenizer.TYPE.TEXT }, + { + value: UrlbarTokenizer.RESTRICT.HISTORY, + type: UrlbarTokenizer.TYPE.TEXT, + }, + { + value: UrlbarTokenizer.RESTRICT.TAG, + type: UrlbarTokenizer.TYPE.RESTRICT_TAG, + }, + ], + }, + { + desc: "multiple boundary restriction chars prefix", + searchString: `${UrlbarTokenizer.RESTRICT.HISTORY} ${UrlbarTokenizer.RESTRICT.TAG} test`, + expectedTokens: [ + { + value: UrlbarTokenizer.RESTRICT.HISTORY, + type: UrlbarTokenizer.TYPE.RESTRICT_HISTORY, + }, + { + value: UrlbarTokenizer.RESTRICT.TAG, + type: UrlbarTokenizer.TYPE.TEXT, + }, + { value: "test", type: UrlbarTokenizer.TYPE.TEXT }, + ], + }, + { + desc: "Math with division", + searchString: "3.6/1.2", + expectedTokens: [{ value: "3.6/1.2", type: UrlbarTokenizer.TYPE.TEXT }], + }, + { + desc: "ipv4 in bookmarks", + searchString: `${UrlbarTokenizer.RESTRICT.BOOKMARK} 192.168.1.1:8`, + expectedTokens: [ + { + value: UrlbarTokenizer.RESTRICT.BOOKMARK, + type: UrlbarTokenizer.TYPE.RESTRICT_BOOKMARK, + }, + { value: "192.168.1.1:8", type: UrlbarTokenizer.TYPE.POSSIBLE_ORIGIN }, + ], + }, + { + desc: "email", + searchString: "test@mozilla.com", + expectedTokens: [ + { value: "test@mozilla.com", type: UrlbarTokenizer.TYPE.TEXT }, + ], + }, + { + desc: "email2", + searchString: "test.test@mozilla.co.uk", + expectedTokens: [ + { value: "test.test@mozilla.co.uk", type: UrlbarTokenizer.TYPE.TEXT }, + ], + }, + { + desc: "protocol", + searchString: "http://test", + expectedTokens: [ + { value: "http://test", type: UrlbarTokenizer.TYPE.POSSIBLE_URL }, + ], + }, + { + desc: + "bogus protocol with host (we allow visits to http://///example.com)", + searchString: "http:///test", + expectedTokens: [ + { value: "http:///test", type: UrlbarTokenizer.TYPE.POSSIBLE_URL }, + ], + }, + { + desc: "file protocol with path", + searchString: "file:///home", + expectedTokens: [ + { value: "file:///home", type: UrlbarTokenizer.TYPE.POSSIBLE_URL }, + ], + }, + { + desc: "almost a protocol", + searchString: "http:", + expectedTokens: [ + { value: "http:", type: UrlbarTokenizer.TYPE.POSSIBLE_URL }, + ], + }, + { + desc: "almost a protocol 2", + searchString: "http:/", + expectedTokens: [ + { value: "http:/", type: UrlbarTokenizer.TYPE.POSSIBLE_URL }, + ], + }, + { + desc: "bogus protocol (we allow visits to http://///example.com)", + searchString: "http:///", + expectedTokens: [ + { value: "http:///", type: UrlbarTokenizer.TYPE.POSSIBLE_URL }, + ], + }, + { + desc: "file protocol", + searchString: "file:///", + expectedTokens: [ + { value: "file:///", type: UrlbarTokenizer.TYPE.POSSIBLE_URL }, + ], + }, + { + desc: "userinfo", + searchString: "user:pass@test", + expectedTokens: [ + { value: "user:pass@test", type: UrlbarTokenizer.TYPE.POSSIBLE_ORIGIN }, + ], + }, + { + desc: "domain", + searchString: "www.mozilla.org", + expectedTokens: [ + { + value: "www.mozilla.org", + type: UrlbarTokenizer.TYPE.POSSIBLE_ORIGIN, + }, + ], + }, + { + desc: "data uri", + searchString: "data:text/plain,Content", + expectedTokens: [ + { + value: "data:text/plain,Content", + type: UrlbarTokenizer.TYPE.POSSIBLE_URL, + }, + ], + }, + { + desc: "ipv6", + searchString: "[2001:db8::1]", + expectedTokens: [ + { value: "[2001:db8::1]", type: UrlbarTokenizer.TYPE.POSSIBLE_ORIGIN }, + ], + }, + { + desc: "numeric domain", + searchString: "test1001.com", + expectedTokens: [ + { value: "test1001.com", type: UrlbarTokenizer.TYPE.POSSIBLE_ORIGIN }, + ], + }, + { + desc: "invalid ip", + searchString: "192.2134.1.2", + expectedTokens: [ + { value: "192.2134.1.2", type: UrlbarTokenizer.TYPE.TEXT }, + ], + }, + { + desc: "ipv4", + searchString: "1.2.3.4", + expectedTokens: [ + { value: "1.2.3.4", type: UrlbarTokenizer.TYPE.POSSIBLE_ORIGIN }, + ], + }, + { + desc: "host/path", + searchString: "test/test", + expectedTokens: [ + { value: "test/test", type: UrlbarTokenizer.TYPE.POSSIBLE_URL }, + ], + }, + { + desc: "percent encoded string", + searchString: "%E6%97%A5%E6%9C%AC", + expectedTokens: [ + { value: "%E6%97%A5%E6%9C%AC", type: UrlbarTokenizer.TYPE.TEXT }, + ], + }, + { + desc: "Uppercase", + searchString: "TEST", + expectedTokens: [{ value: "TEST", type: UrlbarTokenizer.TYPE.TEXT }], + }, + { + desc: "Mixed case 1", + searchString: "TeSt", + expectedTokens: [{ value: "TeSt", type: UrlbarTokenizer.TYPE.TEXT }], + }, + { + desc: "Mixed case 2", + searchString: "tEsT", + expectedTokens: [{ value: "tEsT", type: UrlbarTokenizer.TYPE.TEXT }], + }, + { + desc: "Uppercase with spaces", + searchString: "TEST EXAMPLE", + expectedTokens: [ + { value: "TEST", type: UrlbarTokenizer.TYPE.TEXT }, + { value: "EXAMPLE", type: UrlbarTokenizer.TYPE.TEXT }, + ], + }, + { + desc: "Mixed case with spaces", + searchString: "TeSt eXaMpLe", + expectedTokens: [ + { value: "TeSt", type: UrlbarTokenizer.TYPE.TEXT }, + { value: "eXaMpLe", type: UrlbarTokenizer.TYPE.TEXT }, + ], + }, + { + desc: "plain number", + searchString: "1001", + expectedTokens: [{ value: "1001", type: UrlbarTokenizer.TYPE.TEXT }], + }, + { + desc: "data uri with spaces", + searchString: "data:text/html,oh hi?", + expectedTokens: [ + { + value: "data:text/html,oh hi?", + type: UrlbarTokenizer.TYPE.POSSIBLE_URL, + }, + ], + }, + { + desc: "data uri with spaces ignored with other tokens", + searchString: "hi data:text/html,oh hi?", + expectedTokens: [ + { + value: "hi", + type: UrlbarTokenizer.TYPE.TEXT, + }, + { + value: "data:text/html,oh", + type: UrlbarTokenizer.TYPE.POSSIBLE_URL, + }, + { + value: "hi", + type: UrlbarTokenizer.TYPE.TEXT, + }, + { + value: UrlbarTokenizer.RESTRICT.SEARCH, + type: UrlbarTokenizer.TYPE.RESTRICT_SEARCH, + }, + ], + }, + { + desc: "whitelisted host", + searchString: "test whitelisted", + expectedTokens: [ + { + value: "test", + type: UrlbarTokenizer.TYPE.TEXT, + }, + { + value: "whitelisted", + type: UrlbarTokenizer.TYPE.POSSIBLE_ORIGIN, + }, + ], + }, + ]; + + Services.prefs.setBoolPref("browser.fixup.domainwhitelist.whitelisted", true); + + for (let queryContext of testContexts) { + info(queryContext.desc); + queryContext.trimmedSearchString = queryContext.searchString.trim(); + for (let token of queryContext.expectedTokens) { + token.lowerCaseValue = token.value.toLocaleLowerCase(); + } + let newQueryContext = UrlbarTokenizer.tokenize(queryContext); + Assert.equal( + queryContext, + newQueryContext, + "The queryContext object is the same" + ); + Assert.deepEqual( + queryContext.tokens, + queryContext.expectedTokens, + "Check the expected tokens" + ); + } +}); diff --git a/browser/components/urlbar/tests/unit/test_trimming.js b/browser/components/urlbar/tests/unit/test_trimming.js new file mode 100644 index 0000000000..1d275fef3a --- /dev/null +++ b/browser/components/urlbar/tests/unit/test_trimming.js @@ -0,0 +1,222 @@ +/* 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/. */ + +add_task(async function setup() { + registerCleanupFunction(async () => { + Services.prefs.clearUserPref("browser.urlbar.suggest.searches"); + }); + Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false); +}); + +add_task(async function test_untrimmed_secure_www() { + info("Searching for untrimmed https://www entry"); + await PlacesTestUtils.addVisits({ + uri: Services.io.newURI("https://www.mozilla.org/test/"), + }); + let context = createContext("mo", { isPrivate: false }); + await check_results({ + context, + autofilled: "mozilla.org/", + completed: "https://www.mozilla.org/", + matches: [ + makeVisitResult(context, { + uri: "https://www.mozilla.org/", + title: "https://www.mozilla.org", + heuristic: true, + }), + makeVisitResult(context, { + uri: "https://www.mozilla.org/test/", + title: "test visit for https://www.mozilla.org/test/", + }), + ], + }); + await cleanupPlaces(); +}); + +add_task(async function test_untrimmed_secure_www_path() { + info("Searching for untrimmed https://www entry with path"); + await PlacesTestUtils.addVisits({ + uri: Services.io.newURI("https://www.mozilla.org/test/"), + }); + let context = createContext("mozilla.org/t", { isPrivate: false }); + await check_results({ + context, + autofilled: "mozilla.org/test/", + completed: "https://www.mozilla.org/test/", + matches: [ + makeVisitResult(context, { + uri: "https://www.mozilla.org/test/", + title: "https://www.mozilla.org/test/", + heuristic: true, + }), + ], + }); + await cleanupPlaces(); +}); + +add_task(async function test_untrimmed_secure() { + info("Searching for untrimmed https:// entry"); + await PlacesTestUtils.addVisits({ + uri: Services.io.newURI("https://mozilla.org/test/"), + }); + let context = createContext("mo", { isPrivate: false }); + await check_results({ + context, + autofilled: "mozilla.org/", + completed: "https://mozilla.org/", + matches: [ + makeVisitResult(context, { + uri: "https://mozilla.org/", + title: "https://mozilla.org", + heuristic: true, + }), + makeVisitResult(context, { + uri: "https://mozilla.org/test/", + title: "test visit for https://mozilla.org/test/", + }), + ], + }); + await cleanupPlaces(); +}); + +add_task(async function test_untrimmed_secure_path() { + info("Searching for untrimmed https:// entry with path"); + await PlacesTestUtils.addVisits({ + uri: Services.io.newURI("https://mozilla.org/test/"), + }); + let context = createContext("mozilla.org/t", { isPrivate: false }); + await check_results({ + context, + autofilled: "mozilla.org/test/", + completed: "https://mozilla.org/test/", + matches: [ + makeVisitResult(context, { + uri: "https://mozilla.org/test/", + title: "https://mozilla.org/test/", + heuristic: true, + }), + ], + }); + await cleanupPlaces(); +}); + +add_task(async function test_untrimmed_www() { + info("Searching for untrimmed http://www entry"); + await PlacesTestUtils.addVisits({ + uri: Services.io.newURI("http://www.mozilla.org/test/"), + }); + let context = createContext("mo", { isPrivate: false }); + await check_results({ + context, + autofilled: "mozilla.org/", + completed: "http://www.mozilla.org/", + matches: [ + makeVisitResult(context, { + uri: "http://www.mozilla.org/", + title: "www.mozilla.org", + heuristic: true, + }), + makeVisitResult(context, { + uri: "http://www.mozilla.org/test/", + title: "test visit for http://www.mozilla.org/test/", + }), + ], + }); + await cleanupPlaces(); +}); + +add_task(async function test_untrimmed_www_path() { + info("Searching for untrimmed http://www entry with path"); + await PlacesTestUtils.addVisits({ + uri: Services.io.newURI("http://www.mozilla.org/test/"), + }); + let context = createContext("mozilla.org/t", { isPrivate: false }); + await check_results({ + context, + autofilled: "mozilla.org/test/", + completed: "http://www.mozilla.org/test/", + matches: [ + makeVisitResult(context, { + uri: "http://www.mozilla.org/test/", + title: "www.mozilla.org/test/", + heuristic: true, + }), + ], + }); + await cleanupPlaces(); +}); + +add_task(async function test_untrimmed_ftp() { + info("Searching for untrimmed ftp:// entry"); + await PlacesTestUtils.addVisits({ + uri: Services.io.newURI("ftp://mozilla.org/test/"), + }); + let context = createContext("mo", { isPrivate: false }); + await check_results({ + context, + autofilled: "mozilla.org/", + completed: "ftp://mozilla.org/", + matches: [ + makeVisitResult(context, { + uri: "ftp://mozilla.org/", + title: "ftp://mozilla.org", + heuristic: true, + }), + makeVisitResult(context, { + uri: "ftp://mozilla.org/test/", + title: "test visit for ftp://mozilla.org/test/", + }), + ], + }); + await cleanupPlaces(); +}); + +add_task(async function test_untrimmed_ftp_path() { + info("Searching for untrimmed ftp:// entry with path"); + await PlacesTestUtils.addVisits({ + uri: Services.io.newURI("ftp://mozilla.org/test/"), + }); + let context = createContext("mozilla.org/t", { isPrivate: false }); + await check_results({ + context, + autofilled: "mozilla.org/test/", + completed: "ftp://mozilla.org/test/", + matches: [ + makeVisitResult(context, { + uri: "ftp://mozilla.org/test/", + title: "ftp://mozilla.org/test/", + heuristic: true, + }), + ], + }); + await cleanupPlaces(); +}); + +add_task(async function test_escaped_chars() { + info("Searching for URL with characters that are normally escaped"); + await PlacesTestUtils.addVisits({ + uri: Services.io.newURI("https://www.mozilla.org/啊-test"), + }); + let context = createContext("https://www.mozilla.org/啊-test", { + isPrivate: false, + }); + await check_results({ + context, + matches: [ + makeVisitResult(context, { + source: UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + uri: "https://www.mozilla.org/%E5%95%8A-test", + title: "https://www.mozilla.org/啊-test", + iconUri: "page-icon:https://www.mozilla.org/", + heuristic: true, + }), + // UnifiedComplete escapes this character. + makeVisitResult(context, { + uri: "https://www.mozilla.org/%E5%95%8A-test", + title: "test visit for https://www.mozilla.org/%E5%95%8A-test", + }), + ], + }); + await cleanupPlaces(); +}); diff --git a/browser/components/urlbar/tests/unit/xpcshell.ini b/browser/components/urlbar/tests/unit/xpcshell.ini new file mode 100644 index 0000000000..555f8ac7ce --- /dev/null +++ b/browser/components/urlbar/tests/unit/xpcshell.ini @@ -0,0 +1,54 @@ +[DEFAULT] +head = head.js +firefox-appdir = browser +support-files = + data/engine-suggestions.xml + data/engine-tail-suggestions.xml + +[test_autofill_about_urls.js] +[test_autofill_bookmarked.js] +[test_autofill_functional.js] +[test_autofill_origins.js] +[test_autofill_originsAndQueries.js] +[test_autofill_prefix_fallback.js] +[test_autofill_search_engines.js] +[test_autofill_search_engine_aliases.js] +[test_autofill_urls.js] +[test_avoid_middle_complete.js] +[test_avoid_stripping_to_empty_tokens.js] +[test_casing.js] +[test_dedupe_prefix.js] +[test_dupe_urls.js] +[test_encoded_urls.js] +[test_heuristic_cancel.js] +[test_keywords.js] +skip-if = os == 'linux' # bug 1474616 +[test_muxer.js] +[test_providerHeuristicFallback.js] +[test_providerOmnibox.js] +[test_providerOpenTabs.js] +[test_providersManager.js] +[test_providersManager_filtering.js] +[test_providersManager_maxResults.js] +[test_providerTabToSearch.js] +[test_providerTabToSearch_partialHost.js] +[test_providerUnifiedComplete.js] +[test_providerUnifiedComplete_duplicate_entries.js] +[test_query_url.js] +[test_queryScorer.js] +[test_search_engine_host.js] +[test_search_suggestions.js] +[test_search_suggestions_aliases.js] +[test_search_suggestions_tail.js] +[test_tokenizer.js] +[test_trimming.js] +[test_UrlbarController_integration.js] +[test_UrlbarController_telemetry.js] +[test_UrlbarController_unit.js] +[test_UrlbarPrefs.js] +[test_UrlbarQueryContext.js] +[test_UrlbarQueryContext_restrictSource.js] +[test_UrlbarSearchUtils.jsm] +[test_UrlbarUtils_addToUrlbarHistory.js] +[test_UrlbarUtils_getShortcutOrURIAndPostData.js] +[test_UrlbarUtils_getTokenMatches.js] |