/* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; // Test selection on result view by mouse. ChromeUtils.defineESModuleGetters(this, { UrlbarProviderQuickActions: "resource:///modules/UrlbarProviderQuickActions.sys.mjs", }); add_setup(async function () { await SpecialPowers.pushPrefEnv({ set: [ ["browser.urlbar.quickactions.enabled", true], ["browser.urlbar.suggest.quickactions", true], ["browser.urlbar.shortcuts.quickactions", true], ], }); UrlbarTestUtils.disableResultMenuAutohide(window); await SearchTestUtils.installSearchExtension({}, { setAsDefault: true }); UrlbarProviderQuickActions.addAction("test-addons", { commands: ["test-addons"], label: "quickactions-addons", onPick: () => BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, "about:about"), }); UrlbarProviderQuickActions.addAction("test-downloads", { commands: ["test-downloads"], label: "quickactions-downloads2", onPick: () => BrowserTestUtils.loadURIString( gBrowser.selectedBrowser, "about:downloads" ), }); registerCleanupFunction(function () { UrlbarProviderQuickActions.removeAction("test-addons"); UrlbarProviderQuickActions.removeAction("test-downloads"); }); }); add_task(async function basic() { const testData = [ { description: "Normal result to quick action button", mousedown: ".urlbarView-row:nth-child(1) > .urlbarView-row-inner", mouseup: ".urlbarView-quickaction-row[data-key=test-downloads]", expected: "about:downloads", }, { description: "Normal result to out of result", mousedown: ".urlbarView-row:nth-child(1) > .urlbarView-row-inner", mouseup: "body", expected: false, }, { description: "Quick action button to normal result", mousedown: ".urlbarView-quickaction-row[data-key=test-addons]", mouseup: ".urlbarView-row:nth-child(1)", expected: "https://example.com/?q=test", }, { description: "Quick action button to quick action button", mousedown: ".urlbarView-quickaction-row[data-key=test-addons]", mouseup: ".urlbarView-quickaction-row[data-key=test-downloads]", expected: "about:downloads", }, { description: "Quick action button to out of result", mousedown: ".urlbarView-quickaction-row[data-key=test-downloads]", mouseup: "body", expected: false, }, ]; for (const { description, mousedown, mouseup, expected } of testData) { info(description); await BrowserTestUtils.withNewTab("about:blank", async () => { await UrlbarTestUtils.promiseAutocompleteResultPopup({ value: "test", window, }); let [downElement, upElement] = await waitForElements([ mousedown, mouseup, ]); EventUtils.synthesizeMouseAtCenter(downElement, { type: "mousedown", }); Assert.ok( downElement.hasAttribute("selected"), "Mousedown element should be selected after mousedown" ); EventUtils.synthesizeMouseAtCenter(upElement, { type: "mouseup" }); Assert.ok( !downElement.hasAttribute("selected"), "Mousedown element should not be selected after mouseup" ); Assert.ok( !upElement.hasAttribute("selected"), "Mouseup element should not be selected after mouseup" ); if (expected) { await BrowserTestUtils.browserLoaded( gBrowser.selectedBrowser, false, expected ); Assert.ok(true, "Expected page is opened"); } }); } }); add_task(async function outOfBrowser() { const testData = [ { description: "Normal result to out of browser", mousedown: ".urlbarView-row:nth-child(1) > .urlbarView-row-inner", }, { description: "Normal result to out of result", mousedown: ".urlbarView-row:nth-child(1) > .urlbarView-row-inner", expected: false, }, { description: "Quick action button to out of browser", mousedown: ".urlbarView-quickaction-row[data-key=test-addons]", }, ]; for (const { description, mousedown } of testData) { info(description); await BrowserTestUtils.withNewTab("about:blank", async () => { await UrlbarTestUtils.promiseAutocompleteResultPopup({ value: "test", window, }); let [downElement] = await waitForElements([mousedown]); EventUtils.synthesizeMouseAtCenter(downElement, { type: "mousedown", }); Assert.ok( downElement.hasAttribute("selected"), "Mousedown element should be selected after mousedown" ); // Mouseup at out of browser. EventUtils.synthesizeMouse(document.documentElement, -1, -1, { type: "mouseup", }); Assert.ok( !downElement.hasAttribute("selected"), "Mousedown element should not be selected after mouseup" ); }); } }); add_task(async function withSelectionByKeyboard() { const testData = [ { description: "Select normal result, then click on out of result", mousedown: "body", mouseup: "body", expected: { selectedElementByKey: "#urlbar-results .urlbarView-row > .urlbarView-row-inner[selected]", selectedElementAfterMouseDown: "#urlbar-results .urlbarView-row > .urlbarView-row-inner[selected]", actionedPage: false, }, }, { description: "Select quick action button, then click on out of result", arrowDown: 1, mousedown: "body", mouseup: "body", expected: { selectedElementByKey: "#urlbar-results .urlbarView-quickaction-row[selected]", selectedElementAfterMouseDown: "#urlbar-results .urlbarView-quickaction-row[selected]", actionedPage: false, }, }, { description: "Select normal result, then click on about:downloads", mousedown: ".urlbarView-quickaction-row[data-key=test-downloads]", mouseup: ".urlbarView-quickaction-row[data-key=test-downloads]", expected: { selectedElementByKey: "#urlbar-results .urlbarView-row > .urlbarView-row-inner[selected]", selectedElementAfterMouseDown: ".urlbarView-quickaction-row[data-key=test-downloads]", actionedPage: "about:downloads", }, }, ]; for (const { description, arrowDown, mousedown, mouseup, expected, } of testData) { info(description); await BrowserTestUtils.withNewTab("about:blank", async () => { await UrlbarTestUtils.promiseAutocompleteResultPopup({ value: "test", window, }); let [downElement, upElement] = await waitForElements([ mousedown, mouseup, ]); if (arrowDown) { EventUtils.synthesizeKey( "KEY_ArrowDown", { repeat: arrowDown }, window ); } let [selectedElementByKey] = await waitForElements([ expected.selectedElementByKey, ]); Assert.ok( selectedElementByKey.hasAttribute("selected"), "selectedElementByKey should be selected after arrow down" ); EventUtils.synthesizeMouseAtCenter(downElement, { type: "mousedown", }); if ( expected.selectedElementByKey !== expected.selectedElementAfterMouseDown ) { let [selectedElementAfterMouseDown] = await waitForElements([ expected.selectedElementAfterMouseDown, ]); Assert.ok( selectedElementAfterMouseDown.hasAttribute("selected"), "selectedElementAfterMouseDown should be selected after mousedown" ); Assert.ok( !selectedElementByKey.hasAttribute("selected"), "selectedElementByKey should not be selected after mousedown" ); } EventUtils.synthesizeMouseAtCenter(upElement, { type: "mouseup", }); if (expected.actionedPage) { Assert.ok( !selectedElementByKey.hasAttribute("selected"), "selectedElementByKey should not be selected after page starts load" ); await BrowserTestUtils.browserLoaded( gBrowser.selectedBrowser, false, expected.actionedPage ); Assert.ok(true, "Expected page is opened"); } else { Assert.ok( selectedElementByKey.hasAttribute("selected"), "selectedElementByKey should remain selected" ); } }); } }); add_task(async function withDnsFirstForSingleWordsPref() { await SpecialPowers.pushPrefEnv({ set: [["browser.fixup.dns_first_for_single_words", true]], }); await PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, url: "https://example.org/", title: "example", }); await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(); await UrlbarTestUtils.promiseAutocompleteResultPopup({ value: "ex", window, }); const details = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); const target = details.element.action; EventUtils.synthesizeMouseAtCenter(target, { type: "mousedown" }); const onLoaded = BrowserTestUtils.browserLoaded( gBrowser.selectedBrowser, false, "https://example.org/" ); EventUtils.synthesizeMouseAtCenter(target, { type: "mouseup" }); await onLoaded; Assert.ok(true, "Expected page is opened"); await PlacesUtils.bookmarks.eraseEverything(); await SpecialPowers.popPrefEnv(); }); add_task(async function buttons() { let initialTabUrl = "https://example.com/initial"; let mainResultUrl = "https://example.com/main"; let mainResultHelpUrl = "https://example.com/help"; let otherResultUrl = "https://example.com/other"; let searchString = "test"; // Add a provider with two results: The first has buttons and the second has a // URL that should or shouldn't become the input's value when the block button // in the first result is clicked, depending on the test. let provider = new UrlbarTestUtils.TestProvider({ priority: Infinity, results: [ new UrlbarResult( UrlbarUtils.RESULT_TYPE.URL, UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, { url: mainResultUrl, helpUrl: mainResultHelpUrl, helpL10n: { id: UrlbarPrefs.get("resultMenu") ? "urlbar-result-menu-learn-more-about-firefox-suggest" : "firefox-suggest-urlbar-learn-more", }, isBlockable: true, blockL10n: { id: UrlbarPrefs.get("resultMenu") ? "urlbar-result-menu-dismiss-firefox-suggest" : "firefox-suggest-urlbar-block", }, } ), new UrlbarResult( UrlbarUtils.RESULT_TYPE.URL, UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, { url: otherResultUrl, } ), ], }); // Implement the provider's `onEngagement()` so it removes the result. let onEngagementCallCount = 0; provider.onEngagement = (isPrivate, state, queryContext, details) => { onEngagementCallCount++; queryContext.view.controller.removeResult(details.result); }; UrlbarProvidersManager.registerProvider(provider); let assertBlockResultCalled = () => { Assert.equal( onEngagementCallCount, 1, "blockResult() should have been called once" ); onEngagementCallCount = 0; let rowUrls = []; let rows = UrlbarTestUtils.getResultsContainer(window).children; for (let row of rows) { rowUrls.push(row.result.payload.url); } Assert.ok( !rowUrls.includes(mainResultUrl), "The main result should not be in the view after blocking it: " + JSON.stringify(rowUrls) ); }; let assertResultMenuOpen = () => { Assert.equal( gURLBar.view.resultMenu.state, "showing", "Result menu is showing" ); EventUtils.synthesizeKey("KEY_Escape"); }; let testData = [ { description: UrlbarPrefs.get("resultMenu") ? "Menu button to menu button" : "Block button to block button", mousedown: UrlbarPrefs.get("resultMenu") ? ".urlbarView-row:nth-child(1) .urlbarView-button-menu" : ".urlbarView-row:nth-child(1) .urlbarView-button-block", afterMouseupCallback: UrlbarPrefs.get("resultMenu") ? assertResultMenuOpen : assertBlockResultCalled, expected: { mousedownSelected: false, topSites: { pageProxyState: "valid", value: initialTabUrl, }, searchString: { pageProxyState: "invalid", value: searchString, }, }, }, { skip: UrlbarPrefs.get("resultMenu"), description: "Help button to help button", mousedown: ".urlbarView-row:nth-child(1) .urlbarView-button-help", expected: { mousedownSelected: false, url: mainResultHelpUrl, newTab: true, }, }, { description: UrlbarPrefs.get("resultMenu") ? "Row-inner to menu button" : "Row-inner to block button", mousedown: ".urlbarView-row:nth-child(1) > .urlbarView-row-inner", mouseup: UrlbarPrefs.get("resultMenu") ? ".urlbarView-row:nth-child(1) .urlbarView-button-menu" : ".urlbarView-row:nth-child(1) .urlbarView-button-block", afterMouseupCallback: UrlbarPrefs.get("resultMenu") ? assertResultMenuOpen : assertBlockResultCalled, expected: { mousedownSelected: true, topSites: { pageProxyState: "invalid", value: UrlbarPrefs.get("resultMenu") ? initialTabUrl : otherResultUrl, }, searchString: { pageProxyState: "invalid", value: UrlbarPrefs.get("resultMenu") ? searchString : otherResultUrl, }, }, }, { description: UrlbarPrefs.get("resultMenu") ? "Menu button to row-inner" : "Block button to row-inner", mousedown: UrlbarPrefs.get("resultMenu") ? ".urlbarView-row:nth-child(1) .urlbarView-button-menu" : ".urlbarView-row:nth-child(1) .urlbarView-button-block", mouseup: ".urlbarView-row:nth-child(1) > .urlbarView-row-inner", expected: { mousedownSelected: false, url: mainResultUrl, newTab: false, }, }, ]; for (let showTopSites of [true, false]) { for (let { description, mousedown, mouseup, expected, afterMouseupCallback = null, skip = false, } of testData) { if (skip) { info( `Skipping test with showTopSites = ${showTopSites}: ${description}` ); continue; } info(`Running test with showTopSites = ${showTopSites}: ${description}`); mouseup ||= mousedown; await BrowserTestUtils.withNewTab(initialTabUrl, async () => { Assert.equal( gURLBar.getAttribute("pageproxystate"), "valid", "Sanity check: pageproxystate should be valid initially" ); Assert.equal( gURLBar.value, initialTabUrl, "Sanity check: input.value should be the initial URL initially" ); if (showTopSites) { // Open the view and show top sites by performing the accel+L command. await SimpleTest.promiseFocus(window); let searchPromise = UrlbarTestUtils.promiseSearchComplete(window); document.getElementById("Browser:OpenLocation").doCommand(); await searchPromise; } else { // Do a search. await UrlbarTestUtils.promiseAutocompleteResultPopup({ window, value: searchString, }); } let [downElement, upElement] = await waitForElements([ mousedown, mouseup, ]); // Mousedown and check the selection. EventUtils.synthesizeMouseAtCenter(downElement, { type: "mousedown", }); if (expected.mousedownSelected) { Assert.ok( downElement.hasAttribute("selected"), "Mousedown element should be selected after mousedown" ); } else { Assert.ok( !downElement.hasAttribute("selected"), "Mousedown element should not be selected after mousedown" ); } let loadPromise; if (expected.url) { loadPromise = expected.newTab ? BrowserTestUtils.waitForNewTab(gBrowser, expected.url) : BrowserTestUtils.browserLoaded( gBrowser.selectedBrowser, null, expected.url ); } // Mouseup and check the selection. EventUtils.synthesizeMouseAtCenter(upElement, { type: "mouseup" }); Assert.ok( !downElement.hasAttribute("selected"), "Mousedown element should not be selected after mouseup" ); Assert.ok( !upElement.hasAttribute("selected"), "Mouseup element should not be selected after mouseup" ); // If we expect a URL to load, we're done since the view will close and // the input value will be set to the URL. if (loadPromise) { info("Waiting for URL to load: " + expected.url); let tab = await loadPromise; Assert.ok(true, "Expected URL loaded"); if (expected.newTab) { BrowserTestUtils.removeTab(tab); } return; } if (afterMouseupCallback) { await afterMouseupCallback(); } let state = showTopSites ? expected.topSites : expected.searchString; Assert.equal( gURLBar.getAttribute("pageproxystate"), state.pageProxyState, "pageproxystate should be as expected" ); Assert.equal( gURLBar.value, state.value, "input.value should be as expected" ); }); } } UrlbarProvidersManager.unregisterProvider(provider); }); async function waitForElements(selectors) { let elements; await BrowserTestUtils.waitForCondition(() => { elements = selectors.map(s => document.querySelector(s)); return elements.every(e => e && BrowserTestUtils.is_visible(e)); }, "Waiting for elements to become visible: " + JSON.stringify(selectors)); return elements; }