/* 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://global/skin/icons/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]) : [""]; if ( expectedType === UrlbarUtils.RESULT_TYPE.URL && resultDetails.result.heuristic && resultDetails.result.payload.title ) { Assert.equal( resultDetails.displayed.url, resultDetails.result.payload.displayUrl ); } else { 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.ok(false, "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_setup(async function () { await SearchTestUtils.installSearchExtension( { name: TEST_DEFAULT_ENGINE_NAME, keyword: "@test", }, { setAsDefault: true } ); let engine = Services.search.getEngineByName(TEST_DEFAULT_ENGINE_NAME); 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 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); }