summaryrefslogtreecommitdiffstats
path: root/browser/components/urlbar/tests/browser-tips
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/urlbar/tests/browser-tips')
-rw-r--r--browser/components/urlbar/tests/browser-tips/README.txt7
-rw-r--r--browser/components/urlbar/tests/browser-tips/browser.ini27
-rw-r--r--browser/components/urlbar/tests/browser-tips/browser_glean_telemetry_engagement.js214
-rw-r--r--browser/components/urlbar/tests/browser-tips/browser_interventions.js290
-rw-r--r--browser/components/urlbar/tests/browser-tips/browser_picks.js210
-rw-r--r--browser/components/urlbar/tests/browser-tips/browser_searchTips.js522
-rw-r--r--browser/components/urlbar/tests/browser-tips/browser_searchTips_interaction.js848
-rw-r--r--browser/components/urlbar/tests/browser-tips/browser_selection.js251
-rw-r--r--browser/components/urlbar/tests/browser-tips/browser_updateAsk.js74
-rw-r--r--browser/components/urlbar/tests/browser-tips/browser_updateRefresh.js53
-rw-r--r--browser/components/urlbar/tests/browser-tips/browser_updateRestart.js48
-rw-r--r--browser/components/urlbar/tests/browser-tips/browser_updateWeb.js52
-rw-r--r--browser/components/urlbar/tests/browser-tips/head.js759
-rw-r--r--browser/components/urlbar/tests/browser-tips/slow-page.html7
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>