diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /browser/components/urlbar/tests/browser-tips | |
parent | Initial commit. (diff) | |
download | firefox-43a97878ce14b72f0981164f87f2e35e14151312.tar.xz firefox-43a97878ce14b72f0981164f87f2e35e14151312.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/components/urlbar/tests/browser-tips')
14 files changed, 3362 insertions, 0 deletions
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..e27e214650 --- /dev/null +++ b/browser/components/urlbar/tests/browser-tips/browser.ini @@ -0,0 +1,27 @@ +# 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_glean_telemetry_engagement.js] +[browser_interventions.js] +[browser_picks.js] +[browser_searchTips_interaction.js] +https_first_disabled = true +[browser_searchTips.js] +support-files = + ../browser/slow-page.sjs + slow-page.html +https_first_disabled = true +[browser_selection.js] +[browser_updateAsk.js] +skip-if = os == 'win' && msix # Updater is disabled in MSIX builds +[browser_updateRefresh.js] +skip-if = os == 'win' && msix # Updater is disabled in MSIX builds +[browser_updateRestart.js] +skip-if = os == 'win' && msix # Updater is disabled in MSIX builds +[browser_updateWeb.js] +skip-if = os == 'win' && msix # Updater is disabled in MSIX builds diff --git a/browser/components/urlbar/tests/browser-tips/browser_glean_telemetry_engagement.js b/browser/components/urlbar/tests/browser-tips/browser_glean_telemetry_engagement.js new file mode 100644 index 0000000000..0dbb8ed9e0 --- /dev/null +++ b/browser/components/urlbar/tests/browser-tips/browser_glean_telemetry_engagement.js @@ -0,0 +1,214 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test for engagement telemetry for tips using Glean. + +ChromeUtils.defineESModuleGetters(this, { + PromiseUtils: "resource://gre/modules/PromiseUtils.sys.mjs", +}); + +add_setup(async function() { + makeProfileResettable(); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.searchEngagementTelemetry.enabled", true], + ["browser.urlbar.quickactions.enabled", true], + ["browser.urlbar.suggest.quickactions", true], + ["browser.urlbar.quickactions.showInZeroPrefix", true], + ], + }); + registerCleanupFunction(async function() { + await SpecialPowers.popPrefEnv(); + }); +}); + +add_task(async function selected_result_tip() { + const testData = [ + { + type: "searchTip_onboard", + expected: "tip_onboard", + }, + { + type: "searchTip_persist", + expected: "tip_persist", + }, + { + type: "searchTip_redirect", + expected: "tip_redirect", + }, + { + type: "test", + expected: "tip_unknown", + }, + ]; + + for (const { type, expected } of testData) { + const deferred = PromiseUtils.defer(); + const provider = new UrlbarTestUtils.TestProvider({ + results: [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TIP, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + type, + helpUrl: "https://example.com/", + titleL10n: { id: "urlbar-search-tips-confirm" }, + buttons: [ + { + url: "https://example.com/", + l10n: { id: "urlbar-search-tips-confirm" }, + }, + ], + } + ), + ], + priority: 1, + onEngagement: () => { + deferred.resolve(); + }, + }); + UrlbarProvidersManager.registerProvider(provider); + + await doTest(async browser => { + await openPopup("example"); + await selectRow(type); + EventUtils.synthesizeKey("VK_RETURN"); + await deferred.promise; + + assertGleanTelemetry([ + { + selected_result: expected, + results: expected, + }, + ]); + }); + + UrlbarProvidersManager.unregisterProvider(provider); + } +}); + +add_task(async function selected_result_intervention_clear() { + await doInterventionTest( + SEARCH_STRINGS.CLEAR, + "intervention_clear", + "chrome://browser/content/sanitize.xhtml", + [ + { + selected_result: "intervention_clear", + results: "search_engine,intervention_clear", + }, + ] + ); +}); + +add_task(async function selected_result_intervention_refresh() { + await doInterventionTest( + SEARCH_STRINGS.REFRESH, + "intervention_refresh", + "chrome://global/content/resetProfile.xhtml", + [ + { + selected_result: "intervention_refresh", + results: "search_engine,intervention_refresh", + }, + ] + ); +}); + +add_task(async function selected_result_intervention_update() { + // Updates are disabled for MSIX packages, this test is irrelevant for them. + if ( + AppConstants.platform === "win" && + Services.sysinfo.getProperty("hasWinPackageId") + ) { + return; + } + await UpdateUtils.setAppUpdateAutoEnabled(false); + await initUpdate({ queryString: "&noUpdates=1" }); + UrlbarProviderInterventions.checkForBrowserUpdate(true); + await processUpdateSteps([ + { + panelId: "checkingForUpdates", + checkActiveUpdate: null, + continueFile: CONTINUE_CHECK, + }, + { + panelId: "noUpdatesFound", + checkActiveUpdate: null, + continueFile: null, + }, + ]); + + await doInterventionTest( + SEARCH_STRINGS.UPDATE, + "intervention_update_refresh", + "chrome://global/content/resetProfile.xhtml", + [ + { + selected_result: "intervention_update", + results: "search_engine,action,intervention_update", + }, + ] + ); +}); + +function assertGleanTelemetry(expectedExtraList) { + const telemetries = Glean.urlbar.engagement.testGetValue(); + Assert.equal(telemetries.length, expectedExtraList.length); + + for (let i = 0; i < telemetries.length; i++) { + const telemetry = telemetries[i]; + Assert.equal(telemetry.category, "urlbar"); + Assert.equal(telemetry.name, "engagement"); + + const expectedExtra = expectedExtraList[i]; + for (const key of Object.keys(expectedExtra)) { + Assert.equal( + telemetry.extra[key], + expectedExtra[key], + `${key} is correct` + ); + } + } +} + +async function doInterventionTest(keyword, type, dialog, expectedTelemetry) { + await doTest(async browser => { + await openPopup(keyword); + await selectRow(type); + const onDialog = BrowserTestUtils.promiseAlertDialog("cancel", dialog, { + isSubDialog: true, + }); + EventUtils.synthesizeKey("VK_RETURN"); + await onDialog; + + assertGleanTelemetry(expectedTelemetry); + }); +} + +async function doTest(testFn) { + Services.fog.testResetFOG(); + + await BrowserTestUtils.withNewTab(gBrowser, testFn); +} + +async function openPopup(input) { + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + value: input, + fireInputEvent: true, + }); +} + +async function selectRow(type) { + for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) { + const detail = await UrlbarTestUtils.getDetailsOfResultAt(window, i); + if (detail.result.payload.type === type) { + UrlbarTestUtils.setSelectedRowIndex(window, i); + return; + } + } +} 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..6add58746a --- /dev/null +++ b/browser/components/urlbar/tests/browser-tips/browser_interventions.js @@ -0,0 +1,290 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + UrlbarProviderInterventions: + "resource:///modules/UrlbarProviderInterventions.sys.mjs", +}); + +add_setup(async function() { + Services.telemetry.clearEvents(); + Services.telemetry.clearScalars(); + 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 BrowserTestUtils.promiseAlertDialog( + "cancel", + "chrome://global/content/resetProfile.xhtml", + { isSubDialog: true } + ); + }, + }); +}); + +// 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 BrowserTestUtils.promiseAlertDialog( + "cancel", + "chrome://browser/content/sanitize.xhtml", + { + isSubDialog: true, + } + ); + }, + }); +}); + +// 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 + ); +}); + +// Test the result of UrlbarProviderInterventions.isActive() +// and whether or not the function calucates the score. +add_task(async function testIsActive() { + const testData = [ + { + description: "Test for search string that activates the intervention", + searchString: "firefox slow", + expectedActive: true, + expectedScoreCalculated: true, + }, + { + description: + "Test for search string that does not activate the intervention", + searchString: "example slow", + expectedActive: false, + expectedScoreCalculated: true, + }, + { + description: "Test for empty search string", + searchString: "", + expectedActive: false, + expectedScoreCalculated: false, + }, + { + description: "Test for an URL", + searchString: "https://firefox/slow", + expectedActive: false, + expectedScoreCalculated: false, + }, + { + description: "Test for a data URL", + searchString: "data:text/html,<div>firefox slow</div>", + expectedActive: false, + expectedScoreCalculated: false, + }, + { + description: "Test for string like URL", + searchString: "firefox://slow", + expectedActive: false, + expectedScoreCalculated: false, + }, + ]; + + for (const { + description, + searchString, + expectedActive, + expectedScoreCalculated, + } of testData) { + info(description); + + // Set null to currentTip to know whether or not UrlbarProviderInterventions + // calculated the score. + UrlbarProviderInterventions.currentTip = null; + + const isActive = UrlbarProviderInterventions.isActive({ searchString }); + Assert.equal(isActive, expectedActive, "Result of isAcitive is correct"); + const isScoreCalculated = UrlbarProviderInterventions.currentTip !== null; + Assert.equal( + isScoreCalculated, + expectedScoreCalculated, + "The score is calculated correctly" + ); + } +}); + +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, + titleL10n: { id: "intervention-clear-data" }, + buttons: [ + { + l10n: { id: "intervention-clear-data-confirm" }, + }, + ], + helpUrl, + helpL10n: { id: "urlbar-tip-help-icon" }, + } + ), + ]; + 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._buttons.get("help"); + Assert.ok(helpButton, "Help button exists"); + Assert.ok( + BrowserTestUtils.is_visible(helpButton), + "Help button is visible" + ); + 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..51ebc7725b --- /dev/null +++ b/browser/components/urlbar/tests/browser-tips/browser_picks.js @@ -0,0 +1,210 @@ +/* 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_setup(async function() { + 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 = [makeTipResult({ buttonUrl: TIP_URL, helpUrl: HELP_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._buttons.get("0"), + "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._buttons.get("0"), + "The main button element should remain selected" + ); + + await UrlbarTestUtils.promisePopupClose(window); + UrlbarProvidersManager.unregisterProvider(provider); +}); + +/** + * Runs this test's main checks. + * + * @param {object} options + * Options for the test. + * @param {boolean} options.click + * Pass true to trigger a click, false to trigger an enter key. + * @param {string} [options.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} [options.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: [makeTipResult({ 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._buttons.get("0"); + let helpButton = row._buttons.get("help"); + 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); + } +} + +function makeTipResult({ buttonUrl, helpUrl }) { + return new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TIP, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + type: "test", + titleL10n: { id: "urlbar-search-tips-confirm" }, + buttons: [ + { + url: buttonUrl, + l10n: { id: "urlbar-search-tips-confirm" }, + }, + ], + helpUrl, + helpL10n: { id: "urlbar-search-tips-confirm" }, + } + ); +} 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..651731ab32 --- /dev/null +++ b/browser/components/urlbar/tests/browser-tips/browser_searchTips.js @@ -0,0 +1,522 @@ +/* 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"; + +ChromeUtils.defineESModuleGetters(this, { + AppMenuNotifications: "resource://gre/modules/AppMenuNotifications.sys.mjs", + ProfileAge: "resource://gre/modules/ProfileAge.sys.mjs", + PromiseUtils: "resource://gre/modules/PromiseUtils.sys.mjs", + UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs", + UrlbarProviderSearchTips: + "resource:///modules/UrlbarProviderSearchTips.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(this, { + HttpServer: "resource://testing-common/httpd.js", +}); + +// 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", +]; + +// In order for the persist tip to appear, the scheme of the +// search engine has to be the same as the scheme of the SERP url. +// withDNSRedirect() loads an http: url while the searchform +// of the default engine uses https. To enable the search term +// to be shown, we use the Example engine because it doesn't require +// a redirect. +const SEARCH_SERP_URL = "https://example.com/?q=chocolate"; + +add_setup(async function() { + 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.PERSIST}`, + 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."); + + // Add a mock engine so we don't hit the network loading the SERP. + await SearchTestUtils.installSearchExtension(); + + 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 + ); +}); + +// The persist tip should show on default SERPs. +// This test also has an implied check that the SERP +// is receiving an originalURI. +// This is because the page the test is attempting to load +// will differ from the page that's actually loaded due to +// the DNS redirect. +add_task(async function persistTipOnDefault() { + await setDefaultEngine("Example"); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.showSearchTerms.featureGate", true]], + }); + await checkTab( + window, + SEARCH_SERP_URL, + UrlbarProviderSearchTips.TIP_TYPE.PERSIST + ); + await SpecialPowers.popPrefEnv(); +}); + +// The persist tip should not show on non-default SERPs. +add_task(async function noPersistTipOnNonDefault() { + await setDefaultEngine("DuckDuckGo"); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.showSearchTerms.featureGate", true]], + }); + await checkTab( + window, + SEARCH_SERP_URL, + UrlbarProviderSearchTips.TIP_TYPE.NONE + ); + await SpecialPowers.popPrefEnv(); +}); + +// The persist tip should only show up once a session. +add_task(async function persistTipOnceOnDefaultSerp() { + await setDefaultEngine("Example"); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.showSearchTerms.featureGate", true]], + }); + await checkTab( + window, + SEARCH_SERP_URL, + UrlbarProviderSearchTips.TIP_TYPE.PERSIST + ); + await checkTab( + window, + SEARCH_SERP_URL, + UrlbarProviderSearchTips.TIP_TYPE.NONE + ); + await SpecialPowers.popPrefEnv(); +}); + +// The persist tip should not show in a window +// with a selected tab containing a non-SERP url. +add_task(async function noPersistTipInWindowWithNonSerpTab() { + await setDefaultEngine("Example"); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.showSearchTerms.featureGate", true]], + }); + + // Create a new window for the SERP to be loaded into. + let newWindow = await BrowserTestUtils.openNewBrowserWindow(); + + // Focus on the original window. + window.focus(); + await waitForBrowserWindowActive(window); + + // Load the SERP in the new window to initiate a background load. + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + newWindow.gBrowser.selectedBrowser, + false, + SEARCH_SERP_URL + ); + BrowserTestUtils.loadURI(newWindow.gBrowser.selectedBrowser, SEARCH_SERP_URL); + await browserLoadedPromise; + + // Wait longer than the persist tip delay to check that the search tip + // doesn't show on the non-SERP tab. + await new Promise(resolve => + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + setTimeout(resolve, UrlbarProviderSearchTips.SHOW_PERSIST_TIP_DELAY_MS * 2) + ); + Assert.ok(!window.gURLBar.view.isOpen); + + // Clean up. + await BrowserTestUtils.closeWindow(newWindow); + await SpecialPowers.popPrefEnv(); + resetSearchTipsProvider(); +}); + +// 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); + }); + await setDefaultEngine("Example"); + await checkTab( + window, + SEARCH_SERP_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 + ); +}); + +// Don't show the persist search tip when the browser loads +// a different page from the page the tip was supposed to show on. +add_task(async function noSearchTipWhileAnotherPageLoads() { + await setDefaultEngine("Example"); + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.showSearchTerms.featureGate", true]], + }); + + // Create a slow endpoint. + const SLOW_PAGE = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "http://www.example.com" + ) + "slow-page.sjs"; + + let tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url: SEARCH_SERP_URL, + }); + + // Load a slow URI to cause an onStateChange event but + // not an onLocationChange event. + BrowserTestUtils.loadURI(tab.linkedBrowser, SLOW_PAGE); + + // Wait roughly for the amount of time it would take for the + // persist search tip to show. + await new Promise(resolve => + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + setTimeout(resolve, UrlbarProviderSearchTips.SHOW_PERSIST_TIP_DELAY_MS * 2) + ); + + // Check the search tip didn't show while the page was loading. + Assert.equal( + UrlbarPrefs.get( + `tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.PERSIST}` + ), + 0, + "The shownCount pref should be 0." + ); + + Assert.equal(false, window.gURLBar.view.isOpen, "Urlbar should be closed."); + + // Clean up. + await SpecialPowers.popPrefEnv(); + resetSearchTipsProvider(); + BrowserTestUtils.removeTab(tab); +}); + +// Show the persist search tip when the browser is still loading +// resources from the page the tip is supposed to show on. +add_task(async function searchTipWhilePageLoads() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.showSearchTerms.featureGate", true]], + }); + + // Create a search engine endpoint that will still + // be loading resources on the page load. + const SLOW_PAGE = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://www.example.com" + ) + "slow-page.html"; + + await SearchTestUtils.installSearchExtension({ + name: "Slow Engine", + search_url: SLOW_PAGE, + search_url_get_params: "search={searchTerms}", + }); + await setDefaultEngine("Slow Engine"); + + let engine = Services.search.getEngineByName("Slow Engine"); + let [expectedSearchUrl] = UrlbarUtils.getSearchQueryUrl(engine, "chocolate"); + + // Load a slow SERP. + await checkTab( + window, + expectedSearchUrl, + UrlbarProviderSearchTips.TIP_TYPE.PERSIST + ); + + // Clean up. + await SpecialPowers.popPrefEnv(); + resetSearchTipsProvider(); +}); + +function waitForBrowserWindowActive(win) { + return new Promise(resolve => { + if (Services.focus.activeWindow == win) { + resolve(); + } else { + win.addEventListener( + "activate", + () => { + resolve(); + }, + { once: true } + ); + } + }); +} 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..7e4f0bbc65 --- /dev/null +++ b/browser/components/urlbar/tests/browser-tips/browser_searchTips_interaction.js @@ -0,0 +1,848 @@ +/* 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"; + +ChromeUtils.defineESModuleGetters(this, { + AppMenuNotifications: "resource://gre/modules/AppMenuNotifications.sys.mjs", + ProfileAge: "resource://gre/modules/ProfileAge.sys.mjs", + UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs", + UrlbarProviderSearchTips: + "resource:///modules/UrlbarProviderSearchTips.sys.mjs", + UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(this, { + HttpServer: "resource://testing-common/httpd.js", +}); + +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", +]; + +// In order for the persist tip to appear, the scheme of the +// search engine has to be the same as the scheme of the SERP url. +// withDNSRedirect() loads an http: url while the searchform +// of the default engine uses https. To enable the search term +// to be shown, we use the Example engine because it doesn't require +// a redirect. +const SEARCH_TERM = "chocolate"; +const SEARCH_SERP_URL = `https://example.com/?q=${SEARCH_TERM}`; + +add_setup(async function() { + 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.PERSIST}`, + 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."); + + // Add a mock engine so we don't hit the network loading the SERP. + await SearchTestUtils.installSearchExtension(); + + 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._buttons.get("0"); + 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._buttons.get("0"); + 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(); +}); + +// Picking the tip's button should cause the Urlbar to keep its current +// value and the tip to be not to be shown again in any session. +// Telemetry should be updated. +add_task(async function pickButton_persist() { + UrlbarProviderSearchTips.disableTipsForCurrentSession = false; + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.eventTelemetry.enabled", true], + ["browser.urlbar.showSearchTerms.featureGate", true], + ], + }); + Services.telemetry.clearEvents(); + + await setDefaultEngine("Example"); + + await BrowserTestUtils.withNewTab("about:blank", async () => { + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + SEARCH_SERP_URL + ); + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, SEARCH_SERP_URL); + await browserLoadedPromise; + await checkTip(window, UrlbarProviderSearchTips.TIP_TYPE.PERSIST, false); + + let result = await UrlbarTestUtils.getDetailsOfResultAt(window, 0); + let button = result.element.row._buttons.get("0"); + + await UrlbarTestUtils.promisePopupClose(window, () => { + EventUtils.synthesizeMouseAtCenter(button, {}); + }); + gURLBar.blur(); + + Assert.equal( + gURLBar.value, + SEARCH_TERM, + "The Urlbar should keep its existing value." + ); + }); + + // Check telemetry. + const scalars = TelemetryTestUtils.getProcessScalars("parent", true, true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "urlbar.tips", + `${UrlbarProviderSearchTips.TIP_TYPE.PERSIST}-picked`, + 1 + ); + TelemetryTestUtils.assertEvents( + [ + { + category: "urlbar", + method: "engagement", + object: "click", + value: "typed", + }, + ], + { category: "urlbar" } + ); + + Assert.equal( + UrlbarPrefs.get( + `tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.PERSIST}` + ), + MAX_SHOWN_COUNT, + "Persist 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(); + + await setDefaultEngine("Google"); + 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(); + + await setDefaultEngine("Google"); + 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(); +}); + +// Clicking in the input while the persist tip is showing should have the same +// effect as picking the tip. +add_task(async function clickInInput_persist() { + UrlbarProviderSearchTips.disableTipsForCurrentSession = false; + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.eventTelemetry.enabled", true], + ["browser.urlbar.showSearchTerms.featureGate", true], + ], + }); + Services.telemetry.clearEvents(); + + await setDefaultEngine("Example"); + await BrowserTestUtils.withNewTab("about:blank", async () => { + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + SEARCH_SERP_URL + ); + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, SEARCH_SERP_URL); + await browserLoadedPromise; + await checkTip(window, UrlbarProviderSearchTips.TIP_TYPE.PERSIST, false); + + // Click in the input. + // The popup should be shown because the search term shouldn't be + // cleared when the user focuses on the urlbar. + await UrlbarTestUtils.promisePopupOpen(window, () => { + EventUtils.synthesizeMouseAtCenter(gURLBar.textbox.parentNode, {}); + }); + gURLBar.blur(); + Assert.equal( + gURLBar.value, + SEARCH_TERM, + "The Urlbar should keep its existing value." + ); + }); + + // Check telemetry. + const scalars = TelemetryTestUtils.getProcessScalars("parent", true, true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "urlbar.tips", + `${UrlbarProviderSearchTips.TIP_TYPE.PERSIST}-picked`, + 1 + ); + // This includes the abandonment engagement event whereas the others don't. + // It might be because the persist tip keeps the existing search term + // whereas onboard and redirect don't. + TelemetryTestUtils.assertEvents( + [ + { + category: "urlbar", + method: "engagement", + object: "click", + value: "typed", + }, + { + category: "urlbar", + method: "abandonment", + object: "blur", + value: "returned", + }, + ], + { category: "urlbar" } + ); + + Assert.equal( + UrlbarPrefs.get( + `tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.PERSIST}` + ), + MAX_SHOWN_COUNT, + "Persist 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(); +}); + +// Pressing Ctrl+L (the open location command) while the persist tip is showing +// should have the same effect as picking the tip. +add_task(async function openLocation_persist() { + UrlbarProviderSearchTips.disableTipsForCurrentSession = false; + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.urlbar.eventTelemetry.enabled", true], + ["browser.urlbar.showSearchTerms.featureGate", true], + ], + }); + Services.telemetry.clearEvents(); + + await setDefaultEngine("Example"); + await BrowserTestUtils.withNewTab("about:blank", async () => { + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + gBrowser.selectedBrowser, + false, + SEARCH_SERP_URL + ); + BrowserTestUtils.loadURI(gBrowser.selectedBrowser, SEARCH_SERP_URL); + await browserLoadedPromise; + await checkTip(window, UrlbarProviderSearchTips.TIP_TYPE.PERSIST, false); + + // Trigger the open location command. + await UrlbarTestUtils.promisePopupClose(window, () => { + document.getElementById("Browser:OpenLocation").doCommand(); + }); + gURLBar.blur(); + Assert.equal( + gURLBar.value, + SEARCH_TERM, + "The Urlbar should keep its existing value." + ); + }); + + // Check telemetry. + const scalars = TelemetryTestUtils.getProcessScalars("parent", true, true); + TelemetryTestUtils.assertKeyedScalar( + scalars, + "urlbar.tips", + `${UrlbarProviderSearchTips.TIP_TYPE.PERSIST}-picked`, + 1 + ); + TelemetryTestUtils.assertEvents( + [ + { + category: "urlbar", + method: "engagement", + object: "enter", + value: "typed", + }, + ], + { category: "urlbar" } + ); + + Assert.equal( + UrlbarPrefs.get( + `tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.PERSIST}` + ), + MAX_SHOWN_COUNT, + "Persist 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; + await setDefaultEngine("Google"); + 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._buttons.get("0"); + 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("urlbar-test", { + label: "Test", + priority: box.PRIORITY_INFO_HIGH, + }); + // 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() { + await setDefaultEngine("Example"); + await doPasteAndGoTest( + "pasteAndGo_nonURL", + "https://example.com/?q=pasteAndGo_nonURL" + ); + await setDefaultEngine("Google"); +}); + +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 + ); + cxmenu.activateItem(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..1002bb3fe4 --- /dev/null +++ b/browser/components/urlbar/tests/browser-tips/browser_selection.js @@ -0,0 +1,251 @@ +/* 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" } + ), + makeTipResult({ buttonUrl: TIP_URL, helpUrl: HELP_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-button-0" + ), + "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-button-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-button-help" + ), + "The selected element should be the tip help button." + ); + + gURLBar.view.close(); + UrlbarProvidersManager.unregisterProvider(provider); +}); + +add_task(async function tipIsOnlyResult() { + let results = [makeTipResult({ buttonUrl: TIP_URL, helpUrl: HELP_URL })]; + + 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-button-0" + ), + "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-button-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-button-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" } + ), + makeTipResult({ 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-button-0" + ), + "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-button-0" + ), + "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..3f2014d8c0 --- /dev/null +++ b/browser/components/urlbar/tests/browser-tips/browser_updateAsk.js @@ -0,0 +1,74 @@ +/* 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() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.quickactions", false]], + }); + + // 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..79b4435ef5 --- /dev/null +++ b/browser/components/urlbar/tests/browser-tips/browser_updateRefresh.js @@ -0,0 +1,53 @@ +/* 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() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.quickactions", false]], + }); + + 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", + { isSubDialog: true } + ); + }, + }); +}); 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..75e92910f0 --- /dev/null +++ b/browser/components/urlbar/tests/browser-tips/browser_updateRestart.js @@ -0,0 +1,48 @@ +/* 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], + ["browser.urlbar.suggest.quickactions", false], + ], + }); + + // 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..daca12fea4 --- /dev/null +++ b/browser/components/urlbar/tests/browser-tips/browser_updateWeb.js @@ -0,0 +1,52 @@ +/* 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() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.urlbar.suggest.quickactions", false]], + }); + + // 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..418f46da4f --- /dev/null +++ b/browser/components/urlbar/tests/browser-tips/head.js @@ -0,0 +1,759 @@ +/* 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 +); + +ChromeUtils.defineESModuleGetters(this, { + ResetProfile: "resource://gre/modules/ResetProfile.sys.mjs", + TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs", + UrlbarProviderInterventions: + "resource:///modules/UrlbarProviderInterventions.sys.mjs", + UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.sys.mjs", + UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(this, { + HttpServer: "resource://testing-common/httpd.js", +}); + +XPCOMUtils.defineLazyGetter(this, "UrlbarTestUtils", () => { + const { UrlbarTestUtils: module } = ChromeUtils.importESModule( + "resource://testing-common/UrlbarTestUtils.sys.mjs" + ); + module.init(this); + return module; +}); + +XPCOMUtils.defineLazyGetter(this, "SearchTestUtils", () => { + const { SearchTestUtils: module } = ChromeUtils.importESModule( + "resource://testing-common/SearchTestUtils.sys.mjs" + ); + 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", +}; + +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 = Services.env.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) { + Services.env.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 ( + panelId == "downloading" && + gAUS.currentState == Ci.nsIApplicationUpdateService.STATE_IDLE + ) { + // Now that `AUS.downloadUpdate` is async, we start showing the + // downloading panel while `AUS.downloadUpdate` is still resolving. + // But the below checks assume that this resolution has already + // happened. So we need to wait for things to actually resolve. + await gAUS.stateTransition; + } + + 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 {object} options + * Options for the test + * @param {string} options.searchString + * The search string. + * @param {string} options.tip + * The expected tip type. + * @param {string | RegExp} options.title + * The expected tip title. + * @param {string | RegExp} options.button + * The expected button title. + * @param {Function} options.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._buttons.get("0").textContent; + if (typeof button == "string") { + Assert.equal(actualButton, button, "Button string"); + } else { + // regexp + Assert.ok(button.test(actualButton), "Button regexp"); + } + + Assert.ok(element._buttons.has("help"), "Tip has a help button"); + + // 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._buttons.get("0"); + 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 {object} options + * Options for the test + * @param {string} options.searchString + * The search string. + * @param {TIPS} options.tip + * The expected tip type. + * @param {string} options.title + * The expected tip title. + * @param {string} options.button + * The expected button title. + * @param {Function} options.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._buttons.get("0").textContent; + if (typeof button == "string") { + Assert.equal(actualButton, button, "Button string"); + } else { + // regexp + Assert.ok(button.test(actualButton), "Button regexp"); + } + + let helpButton = element._buttons.get("help"); + Assert.ok(helpButton, "Help button exists"); + Assert.ok( + BrowserTestUtils.is_visible(helpButton), + "Help button is visible" + ); + + 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 + * The host window. + */ +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); + } +} + +/** + * 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; + case UrlbarProviderSearchTips.TIP_TYPE.PERSIST: + heuristic = true; + title = + "Searching just got simpler." + + " Try making your search more specific here in the address bar." + + " To show the URL instead, visit Search, in settings."; + break; + } + Assert.equal(result.heuristic, heuristic); + Assert.equal(result.displayed.title, title); + Assert.equal( + result.element.row._buttons.get("0").textContent, + expectedTip == UrlbarProviderSearchTips.TIP_TYPE.PERSIST + ? `Got it` + : `Okay, Got It` + ); + Assert.ok(!result.element.row._buttons.has("help")); + + 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); + } +} + +function makeTipResult({ buttonUrl, helpUrl = undefined }) { + return new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TIP, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + helpUrl, + type: "test", + titleL10n: { id: "urlbar-search-tips-confirm" }, + buttons: [ + { + url: buttonUrl, + l10n: { id: "urlbar-search-tips-confirm" }, + }, + ], + } + ); +} + +/** + * 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(); +} + +function resetSearchTipsProvider() { + Services.prefs.clearUserPref( + `browser.urlbar.tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.ONBOARD}` + ); + Services.prefs.clearUserPref( + `browser.urlbar.tipShownCount.${UrlbarProviderSearchTips.TIP_TYPE.PERSIST}` + ); + 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, + Ci.nsISearchService.CHANGE_REASON_UNKNOWN + ); +} diff --git a/browser/components/urlbar/tests/browser-tips/slow-page.html b/browser/components/urlbar/tests/browser-tips/slow-page.html new file mode 100644 index 0000000000..f58a44dc62 --- /dev/null +++ b/browser/components/urlbar/tests/browser-tips/slow-page.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> +<html> + <body> + <h1>Search Engine Results Page that is loading a slow resource.</h1> + </body> + <script src="https://www.example.com/browser/browser/components/urlbar/tests/browser-tips/slow-page.sjs"></script> +</html> |