summaryrefslogtreecommitdiffstats
path: root/browser/components/urlbar/tests/quicksuggest/browser
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/urlbar/tests/quicksuggest/browser')
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/browser.ini37
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest.js88
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_addons.js560
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_block.js445
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_configuration.js2101
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_indexes.js425
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_merinoSessions.js142
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_onboardingDialog.js1596
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_dynamicWikipedia.js114
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_impressionEdgeCases.js477
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_navigationalSuggestions.js353
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_nonsponsored.js368
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_other.js409
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_sponsored.js367
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_weather.js152
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/browser_weather.js379
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/head.js569
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/searchSuggestionEngine.sjs57
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/searchSuggestionEngine.xml11
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/subdialog.xhtml14
20 files changed, 8664 insertions, 0 deletions
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser.ini b/browser/components/urlbar/tests/quicksuggest/browser/browser.ini
new file mode 100644
index 0000000000..a29ef67770
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/browser/browser.ini
@@ -0,0 +1,37 @@
+# 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
+ searchSuggestionEngine.xml
+ searchSuggestionEngine.sjs
+ subdialog.xhtml
+
+[browser_quicksuggest.js]
+[browser_quicksuggest_addons.js]
+[browser_quicksuggest_block.js]
+[browser_quicksuggest_configuration.js]
+[browser_quicksuggest_indexes.js]
+skip-if =
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+[browser_quicksuggest_merinoSessions.js]
+[browser_quicksuggest_onboardingDialog.js]
+skip-if =
+ os == 'linux' && bits == 64 # Bug 1773830
+[browser_telemetry_dynamicWikipedia.js]
+tags = search-telemetry
+[browser_telemetry_impressionEdgeCases.js]
+tags = search-telemetry
+[browser_telemetry_navigationalSuggestions.js]
+tags = search-telemetry
+[browser_telemetry_nonsponsored.js]
+tags = search-telemetry
+[browser_telemetry_other.js]
+tags = search-telemetry
+[browser_telemetry_sponsored.js]
+tags = search-telemetry
+[browser_telemetry_weather.js]
+tags = search-telemetry
+[browser_weather.js]
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest.js
new file mode 100644
index 0000000000..d11f6d6386
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest.js
@@ -0,0 +1,88 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests browser quick suggestions.
+ */
+
+const TEST_URL = "http://example.com/quicksuggest";
+
+const REMOTE_SETTINGS_RESULTS = [
+ {
+ id: 1,
+ url: `${TEST_URL}?q=frabbits`,
+ title: "frabbits",
+ keywords: ["fra", "frab"],
+ click_url: "http://click.reporting.test.com/",
+ impression_url: "http://impression.reporting.test.com/",
+ advertiser: "TestAdvertiser",
+ },
+ {
+ id: 2,
+ url: `${TEST_URL}?q=nonsponsored`,
+ title: "Non-Sponsored",
+ keywords: ["nonspon"],
+ click_url: "http://click.reporting.test.com/nonsponsored",
+ impression_url: "http://impression.reporting.test.com/nonsponsored",
+ advertiser: "TestAdvertiserNonSponsored",
+ iab_category: "5 - Education",
+ },
+];
+
+add_setup(async function () {
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await UrlbarTestUtils.formHistory.clear();
+
+ await QuickSuggestTestUtils.ensureQuickSuggestInit({
+ remoteSettingsResults: [
+ {
+ type: "data",
+ attachment: REMOTE_SETTINGS_RESULTS,
+ },
+ ],
+ });
+});
+
+// Tests a sponsored result and keyword highlighting.
+add_task(async function sponsored() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "fra",
+ });
+ await QuickSuggestTestUtils.assertIsQuickSuggest({
+ window,
+ index: 1,
+ isSponsored: true,
+ url: `${TEST_URL}?q=frabbits`,
+ });
+ let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, 1);
+ Assert.equal(
+ row.querySelector(".urlbarView-title").firstChild.textContent,
+ "fra",
+ "The part of the keyword that matches users input is not bold."
+ );
+ Assert.equal(
+ row.querySelector(".urlbarView-title > strong").textContent,
+ "b",
+ "The auto completed section of the keyword is bolded."
+ );
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+// Tests a non-sponsored result.
+add_task(async function nonSponsored() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "nonspon",
+ });
+ await QuickSuggestTestUtils.assertIsQuickSuggest({
+ window,
+ index: 1,
+ isSponsored: false,
+ url: `${TEST_URL}?q=nonsponsored`,
+ });
+ await UrlbarTestUtils.promisePopupClose(window);
+});
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_addons.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_addons.js
new file mode 100644
index 0000000000..e0b75d0e9b
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_addons.js
@@ -0,0 +1,560 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for addon suggestions.
+
+const TEST_MERINO_SUGGESTIONS = [
+ {
+ provider: "amo",
+ icon: "https://example.com/first.svg",
+ url: "https://example.com/first-addon",
+ title: "First Addon",
+ description: "This is a first addon",
+ custom_details: {
+ amo: {
+ rating: "5",
+ number_of_ratings: "1234567",
+ guid: "first@addon",
+ },
+ },
+ is_top_pick: true,
+ },
+ {
+ provider: "amo",
+ icon: "https://example.com/second.png",
+ url: "https://example.com/second-addon",
+ title: "Second Addon",
+ description: "This is a second addon",
+ custom_details: {
+ amo: {
+ rating: "4.5",
+ number_of_ratings: "123",
+ guid: "second@addon",
+ },
+ },
+ is_sponsored: true,
+ is_top_pick: false,
+ },
+ {
+ provider: "amo",
+ icon: "https://example.com/third.svg",
+ url: "https://example.com/third-addon",
+ title: "Third Addon",
+ description: "This is a third addon",
+ custom_details: {
+ amo: {
+ rating: "0",
+ number_of_ratings: "0",
+ guid: "third@addon",
+ },
+ },
+ is_top_pick: false,
+ },
+ {
+ provider: "amo",
+ icon: "https://example.com/fourth.svg",
+ url: "https://example.com/fourth-addon",
+ title: "Fourth Addon",
+ description: "This is a fourth addon",
+ custom_details: {
+ amo: {
+ rating: "4",
+ number_of_ratings: "4",
+ guid: "fourth@addon",
+ },
+ },
+ },
+];
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.quicksuggest.enabled", true],
+ ["browser.urlbar.quicksuggest.remoteSettings.enabled", false],
+ ["browser.urlbar.bestMatch.enabled", true],
+ ],
+ });
+
+ await SearchTestUtils.installSearchExtension({}, { setAsDefault: true });
+
+ await QuickSuggestTestUtils.ensureQuickSuggestInit({
+ merinoSuggestions: TEST_MERINO_SUGGESTIONS,
+ });
+});
+
+add_task(async function basic() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.addons.featureGate", true]],
+ });
+
+ for (const merinoSuggestion of TEST_MERINO_SUGGESTIONS) {
+ MerinoTestUtils.server.response.body.suggestions = [merinoSuggestion];
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "only match the Merino suggestion",
+ });
+ Assert.equal(UrlbarTestUtils.getResultCount(window), 2);
+
+ const { element, result } = await UrlbarTestUtils.getDetailsOfResultAt(
+ window,
+ 1
+ );
+ const row = element.row;
+ const icon = row.querySelector(".urlbarView-dynamic-addons-icon");
+ Assert.equal(icon.src, merinoSuggestion.icon);
+ const url = row.querySelector(".urlbarView-dynamic-addons-url");
+ Assert.equal(url.textContent, merinoSuggestion.url);
+ const title = row.querySelector(".urlbarView-dynamic-addons-title");
+ Assert.equal(title.textContent, merinoSuggestion.title);
+ const description = row.querySelector(
+ ".urlbarView-dynamic-addons-description"
+ );
+ Assert.equal(description.textContent, merinoSuggestion.description);
+ const reviews = row.querySelector(".urlbarView-dynamic-addons-reviews");
+ Assert.equal(
+ reviews.textContent,
+ `${new Intl.NumberFormat().format(
+ Number(merinoSuggestion.custom_details.amo.number_of_ratings)
+ )} reviews`
+ );
+
+ const isTopPick = merinoSuggestion.is_top_pick ?? true;
+ if (isTopPick) {
+ Assert.equal(result.suggestedIndex, 1);
+ } else if (merinoSuggestion.is_sponsored) {
+ Assert.equal(
+ result.suggestedIndex,
+ UrlbarPrefs.get("quickSuggestSponsoredIndex")
+ );
+ } else {
+ Assert.equal(
+ result.suggestedIndex,
+ UrlbarPrefs.get("quickSuggestNonSponsoredIndex")
+ );
+ }
+
+ const onLoad = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ merinoSuggestion.url
+ );
+ EventUtils.synthesizeMouseAtCenter(row, {});
+ await onLoad;
+ Assert.ok(true, "Expected page is loaded");
+
+ await PlacesUtils.history.clear();
+ }
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function ratings() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.addons.featureGate", true]],
+ });
+
+ const testRating = [
+ "0",
+ "0.24",
+ "0.25",
+ "0.74",
+ "0.75",
+ "1",
+ "1.24",
+ "1.25",
+ "1.74",
+ "1.75",
+ "2",
+ "2.24",
+ "2.25",
+ "2.74",
+ "2.75",
+ "3",
+ "3.24",
+ "3.25",
+ "3.74",
+ "3.75",
+ "4",
+ "4.24",
+ "4.25",
+ "4.74",
+ "4.75",
+ "5",
+ ];
+ const baseMerinoSuggestion = JSON.parse(
+ JSON.stringify(TEST_MERINO_SUGGESTIONS[0])
+ );
+
+ for (const rating of testRating) {
+ baseMerinoSuggestion.custom_details.amo.rating = rating;
+ MerinoTestUtils.server.response.body.suggestions = [baseMerinoSuggestion];
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "only match the Merino suggestion",
+ });
+ Assert.equal(UrlbarTestUtils.getResultCount(window), 2);
+
+ const { element } = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+
+ const ratingElements = element.row.querySelectorAll(
+ ".urlbarView-dynamic-addons-rating"
+ );
+ Assert.equal(ratingElements.length, 5);
+
+ for (let i = 0; i < ratingElements.length; i++) {
+ const ratingElement = ratingElements[i];
+
+ const distanceToFull = Number(rating) - i;
+ let fill = "full";
+ if (distanceToFull < 0.25) {
+ fill = "empty";
+ } else if (distanceToFull < 0.75) {
+ fill = "half";
+ }
+ Assert.equal(ratingElement.getAttribute("fill"), fill);
+ }
+ }
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function disable() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.addons.featureGate", false]],
+ });
+
+ // Restore AdmWikipedia suggestions.
+ MerinoTestUtils.server.reset();
+ // Add one Addon suggestion that is higher score than AdmWikipedia.
+ MerinoTestUtils.server.response.body.suggestions.push(
+ Object.assign({}, TEST_MERINO_SUGGESTIONS[0], { score: 2 })
+ );
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "only match the Merino suggestion",
+ });
+ Assert.equal(UrlbarTestUtils.getResultCount(window), 2);
+
+ const { result } = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ Assert.equal(result.payload.telemetryType, "adm_sponsored");
+
+ MerinoTestUtils.server.response.body.suggestions = TEST_MERINO_SUGGESTIONS;
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function resultMenu_showLessFrequently() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.addons.featureGate", true],
+ ["browser.urlbar.addons.showLessFrequentlyCount", 0],
+ ],
+ });
+
+ const cleanUpNimbus = await UrlbarTestUtils.initNimbusFeature({
+ addonsShowLessFrequentlyCap: 3,
+ });
+
+ // Sanity check.
+ Assert.equal(UrlbarPrefs.get("addonsShowLessFrequentlyCap"), 3);
+ Assert.equal(UrlbarPrefs.get("addons.showLessFrequentlyCount"), 0);
+
+ await doShowLessFrequently({
+ input: "aaa b",
+ expected: {
+ isSuggestionShown: true,
+ isMenuItemShown: true,
+ },
+ });
+ Assert.equal(UrlbarPrefs.get("addons.showLessFrequentlyCount"), 1);
+
+ await doShowLessFrequently({
+ input: "aaa b",
+ expected: {
+ isSuggestionShown: true,
+ isMenuItemShown: true,
+ },
+ });
+ Assert.equal(UrlbarPrefs.get("addons.showLessFrequentlyCount"), 2);
+
+ await doShowLessFrequently({
+ input: "aaa b",
+ expected: {
+ isSuggestionShown: true,
+ isMenuItemShown: true,
+ },
+ });
+ Assert.equal(UrlbarPrefs.get("addons.showLessFrequentlyCount"), 3);
+
+ await doShowLessFrequently({
+ input: "aaa b",
+ expected: {
+ // The suggestion should not display since addons.showLessFrequentlyCount
+ // is 3 and the substring (" b") after the first word ("aaa") is 2 chars
+ // long.
+ isSuggestionShown: false,
+ },
+ });
+
+ await doShowLessFrequently({
+ input: "aaa bb",
+ expected: {
+ // The suggestion should display, but item should not shown since the
+ // addons.showLessFrequentlyCount reached to addonsShowLessFrequentlyCap
+ // already.
+ isSuggestionShown: true,
+ isMenuItemShown: false,
+ },
+ });
+
+ await cleanUpNimbus();
+ await SpecialPowers.popPrefEnv();
+});
+
+// Tests the "Not interested" result menu dismissal command.
+add_task(async function resultMenu_notInterested() {
+ await doDismissTest("not_interested");
+});
+
+// Tests the "Not relevant" result menu dismissal command.
+add_task(async function notRelevant() {
+ await doDismissTest("not_relevant");
+});
+
+add_task(async function rowLabel() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.addons.featureGate", true]],
+ });
+
+ const testCases = [
+ {
+ bestMatch: true,
+ expected: "Firefox extension",
+ },
+ {
+ bestMatch: false,
+ expected: "Firefox Suggest",
+ },
+ ];
+
+ for (const { bestMatch, expected } of testCases) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.bestMatch.enabled", bestMatch]],
+ });
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "only match the Merino suggestion",
+ });
+ Assert.equal(UrlbarTestUtils.getResultCount(window), 2);
+
+ const { element } = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ const row = element.row;
+ Assert.equal(row.getAttribute("label"), expected);
+
+ await SpecialPowers.popPrefEnv();
+ }
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function treatmentB() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.addons.featureGate", true]],
+ });
+
+ const cleanUpNimbus = await UrlbarTestUtils.initNimbusFeature({
+ addonsUITreatment: "b",
+ });
+ // Sanity check.
+ Assert.equal(UrlbarPrefs.get("addonsUITreatment"), "b");
+
+ const merinoSuggestion = TEST_MERINO_SUGGESTIONS[0];
+ MerinoTestUtils.server.response.body.suggestions = [merinoSuggestion];
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "only match the Merino suggestion",
+ });
+ Assert.equal(UrlbarTestUtils.getResultCount(window), 2);
+
+ const { element } = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
+ const row = element.row;
+ const icon = row.querySelector(".urlbarView-dynamic-addons-icon");
+ Assert.equal(icon.src, merinoSuggestion.icon);
+ const url = row.querySelector(".urlbarView-dynamic-addons-url");
+ Assert.equal(url.textContent, merinoSuggestion.url);
+ const title = row.querySelector(".urlbarView-dynamic-addons-title");
+ Assert.equal(title.textContent, merinoSuggestion.title);
+ const description = row.querySelector(
+ ".urlbarView-dynamic-addons-description"
+ );
+ Assert.equal(description.textContent, merinoSuggestion.description);
+ const ratingContainer = row.querySelector(
+ ".urlbarView-dynamic-addons-ratingContainer"
+ );
+ Assert.ok(BrowserTestUtils.is_hidden(ratingContainer));
+ const reviews = row.querySelector(".urlbarView-dynamic-addons-reviews");
+ Assert.equal(reviews.textContent, "Recommended");
+
+ await cleanUpNimbus();
+ await SpecialPowers.popPrefEnv();
+});
+
+async function doShowLessFrequently({ input, expected }) {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: input,
+ });
+
+ if (!expected.isSuggestionShown) {
+ for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) {
+ const details = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ Assert.notEqual(
+ details.result.payload.dynamicType,
+ "addons",
+ `Addons suggestion should be absent (checking index ${i})`
+ );
+ }
+
+ return;
+ }
+
+ const resultIndex = 1;
+ const details = await UrlbarTestUtils.getDetailsOfResultAt(
+ window,
+ resultIndex
+ );
+ Assert.equal(
+ details.result.payload.dynamicType,
+ "addons",
+ `Addons suggestion should be present at expected index after ${input} search`
+ );
+
+ // Click the command.
+ try {
+ await UrlbarTestUtils.openResultMenuAndClickItem(
+ window,
+ "show_less_frequently",
+ {
+ resultIndex,
+ }
+ );
+ Assert.ok(expected.isMenuItemShown);
+ Assert.ok(
+ gURLBar.view.isOpen,
+ "The view should remain open clicking the command"
+ );
+ Assert.ok(
+ details.element.row.hasAttribute("feedback-acknowledgment"),
+ "Row should have feedback acknowledgment after clicking command"
+ );
+ } catch (e) {
+ Assert.ok(!expected.isMenuItemShown);
+ Assert.ok(
+ !details.element.row.hasAttribute("feedback-acknowledgment"),
+ "Row should not have feedback acknowledgment after clicking command"
+ );
+ Assert.equal(
+ e.message,
+ "Menu item not found for command: show_less_frequently"
+ );
+ }
+
+ await UrlbarTestUtils.promisePopupClose(window);
+}
+
+async function doDismissTest(command) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.addons.featureGate", true]],
+ });
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "123",
+ });
+
+ const resultCount = UrlbarTestUtils.getResultCount(window);
+ const resultIndex = 1;
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, resultIndex);
+ Assert.equal(
+ details.result.payload.dynamicType,
+ "addons",
+ "Addons suggestion should be present"
+ );
+
+ // Sanity check.
+ Assert.ok(UrlbarPrefs.get("suggest.addons"));
+
+ // Click the command.
+ await UrlbarTestUtils.openResultMenuAndClickItem(
+ window,
+ ["[data-l10n-id=firefox-suggest-command-dont-show-this]", command],
+ { resultIndex, openByMouse: true }
+ );
+
+ Assert.ok(
+ !UrlbarPrefs.get("suggest.addons"),
+ "suggest.addons pref should be set to false after dismissal"
+ );
+
+ // The row should be a tip now.
+ Assert.ok(gURLBar.view.isOpen, "The view should remain open after dismissal");
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ resultCount,
+ "The result count should not haved changed after dismissal"
+ );
+ details = await UrlbarTestUtils.getDetailsOfResultAt(window, resultIndex);
+ Assert.equal(
+ details.type,
+ UrlbarUtils.RESULT_TYPE.TIP,
+ "Row should be a tip after dismissal"
+ );
+ Assert.equal(
+ details.result.payload.type,
+ "dismissalAcknowledgment",
+ "Tip type should be dismissalAcknowledgment"
+ );
+ Assert.ok(
+ !details.element.row.hasAttribute("feedback-acknowledgment"),
+ "Row should not have feedback acknowledgment after dismissal"
+ );
+
+ // Get the dismissal acknowledgment's "Got it" button and click it.
+ let gotItButton = UrlbarTestUtils.getButtonForResultIndex(
+ window,
+ "0",
+ resultIndex
+ );
+ Assert.ok(gotItButton, "Row should have a 'Got it' button");
+ EventUtils.synthesizeMouseAtCenter(gotItButton, {}, window);
+
+ // The view should remain open and the tip row should be gone.
+ Assert.ok(
+ gURLBar.view.isOpen,
+ "The view should remain open clicking the 'Got it' button"
+ );
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ resultCount - 1,
+ "The result count should be one less after clicking 'Got it' button"
+ );
+ for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) {
+ details = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ Assert.ok(
+ details.type != UrlbarUtils.RESULT_TYPE.TIP &&
+ details.result.payload.dynamicType !== "addons",
+ "Tip result and addon result should not be present"
+ );
+ }
+
+ await UrlbarTestUtils.promisePopupClose(window);
+
+ await SpecialPowers.popPrefEnv();
+ UrlbarPrefs.clear("suggest.addons");
+}
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_block.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_block.js
new file mode 100644
index 0000000000..081818c02b
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_block.js
@@ -0,0 +1,445 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests blocking quick suggest results, including best matches. See also:
+//
+// browser_bestMatch.js
+// Includes tests for blocking best match rows independent of quick suggest,
+// especially the superficial UI part that should be common to all types of
+// best matches
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ CONTEXTUAL_SERVICES_PING_TYPES:
+ "resource:///modules/PartnerLinkAttribution.sys.mjs",
+});
+
+const { TELEMETRY_SCALARS } = UrlbarProviderQuickSuggest;
+const { TIMESTAMP_TEMPLATE } = QuickSuggest;
+
+// Include the timestamp template in the suggestion URLs so we can make sure
+// their original URLs with the unreplaced templates are blocked and not their
+// URLs with timestamps.
+const REMOTE_SETTINGS_RESULTS = [
+ {
+ id: 1,
+ url: `https://example.com/sponsored?t=${TIMESTAMP_TEMPLATE}`,
+ title: "Sponsored suggestion",
+ keywords: ["sponsored"],
+ click_url: "https://example.com/click",
+ impression_url: "https://example.com/impression",
+ advertiser: "TestAdvertiser",
+ iab_category: "22 - Shopping",
+ },
+ {
+ id: 2,
+ url: `https://example.com/nonsponsored?t=${TIMESTAMP_TEMPLATE}`,
+ title: "Non-sponsored suggestion",
+ keywords: ["nonsponsored"],
+ click_url: "https://example.com/click",
+ impression_url: "https://example.com/impression",
+ advertiser: "TestAdvertiser",
+ iab_category: "5 - Education",
+ },
+];
+
+// Spy for the custom impression/click sender
+let spy;
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.bestMatch.blockingEnabled", true],
+ ["browser.urlbar.quicksuggest.blockingEnabled", true],
+ ],
+ });
+
+ ({ spy } = QuickSuggestTestUtils.createTelemetryPingSpy());
+
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await UrlbarTestUtils.formHistory.clear();
+
+ await QuickSuggest.blockedSuggestions._test_readyPromise;
+ await QuickSuggest.blockedSuggestions.clear();
+
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+
+ await QuickSuggestTestUtils.ensureQuickSuggestInit({
+ remoteSettingsResults: [
+ {
+ type: "data",
+ attachment: REMOTE_SETTINGS_RESULTS,
+ },
+ ],
+ config: QuickSuggestTestUtils.BEST_MATCH_CONFIG,
+ });
+});
+
+/**
+ * Adds a test task that runs the given callback with combinations of the
+ * following:
+ *
+ * - Best match disabled and enabled
+ * - Each result in `REMOTE_SETTINGS_RESULTS`
+ *
+ * @param {Function} fn
+ * The callback function. It's passed: `{ isBestMatch, suggestion }`
+ */
+function add_combo_task(fn) {
+ let taskFn = async () => {
+ for (let isBestMatch of [false, true]) {
+ UrlbarPrefs.set("bestMatch.enabled", isBestMatch);
+ for (let result of REMOTE_SETTINGS_RESULTS) {
+ info(`Running ${fn.name}: ${JSON.stringify({ isBestMatch, result })}`);
+ await fn({ isBestMatch, result });
+ }
+ UrlbarPrefs.clear("bestMatch.enabled");
+ }
+ };
+ Object.defineProperty(taskFn, "name", { value: fn.name });
+ add_task(taskFn);
+}
+
+// Picks the block button with the keyboard.
+add_combo_task(async function basic_keyboard({ result, isBestMatch }) {
+ await doBasicBlockTest({
+ result,
+ isBestMatch,
+ block: async () => {
+ if (UrlbarPrefs.get("resultMenu")) {
+ await UrlbarTestUtils.openResultMenuAndPressAccesskey(window, "D", {
+ resultIndex: 1,
+ });
+ } else {
+ // TAB twice to select the block button: once to select the main
+ // part of the row, once to select the block button.
+ EventUtils.synthesizeKey("KEY_Tab", { repeat: 2 });
+ EventUtils.synthesizeKey("KEY_Enter");
+ }
+ },
+ });
+});
+
+// Picks the block button with the mouse.
+add_combo_task(async function basic_mouse({ result, isBestMatch }) {
+ await doBasicBlockTest({
+ result,
+ isBestMatch,
+ block: async () => {
+ if (UrlbarPrefs.get("resultMenu")) {
+ await UrlbarTestUtils.openResultMenuAndPressAccesskey(window, "D", {
+ resultIndex: 1,
+ openByMouse: true,
+ });
+ } else {
+ EventUtils.synthesizeMouseAtCenter(
+ UrlbarTestUtils.getButtonForResultIndex(window, "block", 1),
+ {}
+ );
+ }
+ },
+ });
+});
+
+// Uses the key shortcut to block a suggestion.
+add_combo_task(async function basic_keyShortcut({ result, isBestMatch }) {
+ await doBasicBlockTest({
+ result,
+ isBestMatch,
+ block: () => {
+ // Arrow down once to select the row.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ EventUtils.synthesizeKey("KEY_Delete", { shiftKey: true });
+ },
+ });
+});
+
+async function doBasicBlockTest({ result, isBestMatch, block }) {
+ spy.resetHistory();
+
+ // Do a search that triggers the suggestion.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: result.keywords[0],
+ });
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 2,
+ "Two rows are present after searching (heuristic + suggestion)"
+ );
+
+ let isSponsored = result.keywords[0] == "sponsored";
+ await QuickSuggestTestUtils.assertIsQuickSuggest({
+ window,
+ isBestMatch,
+ isSponsored,
+ originalUrl: result.url,
+ });
+
+ // Block the suggestion.
+ await block();
+
+ // The row should have been removed.
+ Assert.ok(
+ UrlbarTestUtils.isPopupOpen(window),
+ "View remains open after blocking result"
+ );
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 1,
+ "Only one row after blocking suggestion"
+ );
+ await QuickSuggestTestUtils.assertNoQuickSuggestResults(window);
+
+ // The URL should be blocked.
+ Assert.ok(
+ await QuickSuggest.blockedSuggestions.has(result.url),
+ "Suggestion is blocked"
+ );
+
+ // Check telemetry scalars.
+ let index = 2;
+ let scalars = {};
+ if (isSponsored) {
+ scalars[TELEMETRY_SCALARS.IMPRESSION_SPONSORED] = index;
+ scalars[TELEMETRY_SCALARS.BLOCK_SPONSORED] = index;
+ } else {
+ scalars[TELEMETRY_SCALARS.IMPRESSION_NONSPONSORED] = index;
+ scalars[TELEMETRY_SCALARS.BLOCK_NONSPONSORED] = index;
+ }
+ if (isBestMatch) {
+ if (isSponsored) {
+ scalars = {
+ ...scalars,
+ [TELEMETRY_SCALARS.IMPRESSION_SPONSORED_BEST_MATCH]: index,
+ [TELEMETRY_SCALARS.BLOCK_SPONSORED_BEST_MATCH]: index,
+ };
+ } else {
+ scalars = {
+ ...scalars,
+ [TELEMETRY_SCALARS.IMPRESSION_NONSPONSORED_BEST_MATCH]: index,
+ [TELEMETRY_SCALARS.BLOCK_NONSPONSORED_BEST_MATCH]: index,
+ };
+ }
+ }
+ QuickSuggestTestUtils.assertScalars(scalars);
+
+ // Check the engagement event.
+ let match_type = isBestMatch ? "best-match" : "firefox-suggest";
+ QuickSuggestTestUtils.assertEvents([
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "block",
+ extra: {
+ match_type,
+ position: String(index),
+ suggestion_type: isSponsored ? "sponsored" : "nonsponsored",
+ },
+ },
+ ]);
+
+ // Check the custom telemetry pings.
+ QuickSuggestTestUtils.assertPings(spy, [
+ {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION,
+ payload: {
+ match_type,
+ block_id: result.id,
+ is_clicked: false,
+ position: index,
+ },
+ },
+ {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_BLOCK,
+ payload: {
+ match_type,
+ block_id: result.id,
+ iab_category: result.iab_category,
+ position: index,
+ },
+ },
+ ]);
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ await QuickSuggest.blockedSuggestions.clear();
+}
+
+// Blocks multiple suggestions one after the other.
+add_task(async function blockMultiple() {
+ for (let isBestMatch of [false, true]) {
+ UrlbarPrefs.set("bestMatch.enabled", isBestMatch);
+ info(`Testing with best match enabled: ${isBestMatch}`);
+
+ for (let i = 0; i < REMOTE_SETTINGS_RESULTS.length; i++) {
+ // Do a search that triggers the i'th suggestion.
+ let { keywords, url } = REMOTE_SETTINGS_RESULTS[i];
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: keywords[0],
+ });
+ await QuickSuggestTestUtils.assertIsQuickSuggest({
+ window,
+ isBestMatch,
+ originalUrl: url,
+ isSponsored: keywords[0] == "sponsored",
+ });
+
+ // Block it.
+ if (UrlbarPrefs.get("resultMenu")) {
+ await UrlbarTestUtils.openResultMenuAndPressAccesskey(window, "D", {
+ resultIndex: 1,
+ });
+ } else {
+ EventUtils.synthesizeKey("KEY_Tab", { repeat: 2 });
+ EventUtils.synthesizeKey("KEY_Enter");
+ }
+ Assert.ok(
+ await QuickSuggest.blockedSuggestions.has(url),
+ "Suggestion is blocked after picking block button"
+ );
+
+ // Make sure all previous suggestions remain blocked and no other
+ // suggestions are blocked yet.
+ for (let j = 0; j < REMOTE_SETTINGS_RESULTS.length; j++) {
+ Assert.equal(
+ await QuickSuggest.blockedSuggestions.has(
+ REMOTE_SETTINGS_RESULTS[j].url
+ ),
+ j <= i,
+ `Suggestion at index ${j} is blocked or not as expected`
+ );
+ }
+ }
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ await QuickSuggest.blockedSuggestions.clear();
+ UrlbarPrefs.clear("bestMatch.enabled");
+ }
+});
+
+// Tests with blocking disabled for both best matches and non-best-matches.
+add_combo_task(async function disabled_both({ result, isBestMatch }) {
+ await doDisabledTest({
+ result,
+ isBestMatch,
+ quickSuggestBlockingEnabled: false,
+ bestMatchBlockingEnabled: false,
+ });
+});
+
+// Tests with blocking disabled only for non-best-matches.
+add_combo_task(async function disabled_quickSuggest({ result, isBestMatch }) {
+ await doDisabledTest({
+ result,
+ isBestMatch,
+ quickSuggestBlockingEnabled: false,
+ bestMatchBlockingEnabled: true,
+ });
+});
+
+// Tests with blocking disabled only for best matches.
+add_combo_task(async function disabled_bestMatch({ result, isBestMatch }) {
+ await doDisabledTest({
+ result,
+ isBestMatch,
+ quickSuggestBlockingEnabled: true,
+ bestMatchBlockingEnabled: false,
+ });
+});
+
+async function doDisabledTest({
+ result,
+ isBestMatch,
+ bestMatchBlockingEnabled,
+ quickSuggestBlockingEnabled,
+}) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.bestMatch.blockingEnabled", bestMatchBlockingEnabled],
+ [
+ "browser.urlbar.quicksuggest.blockingEnabled",
+ quickSuggestBlockingEnabled,
+ ],
+ ],
+ });
+
+ // Do a search to show a suggestion.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: result.keywords[0],
+ });
+ let expectedResultCount = 2;
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ expectedResultCount,
+ "Two rows are present after searching (heuristic + suggestion)"
+ );
+ let details = await QuickSuggestTestUtils.assertIsQuickSuggest({
+ window,
+ isBestMatch,
+ originalUrl: result.url,
+ isSponsored: result.keywords[0] == "sponsored",
+ });
+
+ // Arrow down to select the suggestion and press the key shortcut to block.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ EventUtils.synthesizeKey("KEY_Delete", { shiftKey: true });
+ Assert.ok(
+ UrlbarTestUtils.isPopupOpen(window),
+ "View remains open after trying to block result"
+ );
+
+ if (
+ (isBestMatch && !bestMatchBlockingEnabled) ||
+ (!isBestMatch && !quickSuggestBlockingEnabled)
+ ) {
+ // Blocking is disabled. The key shortcut shouldn't have done anything.
+ if (!UrlbarPrefs.get("resultMenu")) {
+ Assert.ok(
+ !details.element.row._buttons.get("block"),
+ "Block button is not present"
+ );
+ }
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ expectedResultCount,
+ "Same number of results after key shortcut"
+ );
+ await QuickSuggestTestUtils.assertIsQuickSuggest({
+ window,
+ isBestMatch,
+ originalUrl: result.url,
+ isSponsored: result.keywords[0] == "sponsored",
+ });
+ Assert.ok(
+ !(await QuickSuggest.blockedSuggestions.has(result.url)),
+ "Suggestion is not blocked"
+ );
+ } else {
+ // Blocking is enabled. The suggestion should have been blocked.
+ if (!UrlbarPrefs.get("resultMenu")) {
+ Assert.ok(
+ details.element.row._buttons.get("block"),
+ "Block button is present"
+ );
+ }
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ 1,
+ "Only one row after blocking suggestion"
+ );
+ await QuickSuggestTestUtils.assertNoQuickSuggestResults(window);
+ Assert.ok(
+ await QuickSuggest.blockedSuggestions.has(result.url),
+ "Suggestion is blocked"
+ );
+ await QuickSuggest.blockedSuggestions.clear();
+ }
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ await SpecialPowers.popPrefEnv();
+}
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_configuration.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_configuration.js
new file mode 100644
index 0000000000..066ffecd51
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_configuration.js
@@ -0,0 +1,2101 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests QuickSuggest configurations.
+ */
+
+ChromeUtils.defineESModuleGetters(this, {
+ EnterprisePolicyTesting:
+ "resource://testing-common/EnterprisePolicyTesting.sys.mjs",
+ sinon: "resource://testing-common/Sinon.sys.mjs",
+});
+
+// We use this pref in enterprise preference policy tests. We specifically use a
+// pref that's sticky and exposed in the UI to make sure it can be set properly.
+const POLICY_PREF = "suggest.quicksuggest.nonsponsored";
+
+let gDefaultBranch = Services.prefs.getDefaultBranch("browser.urlbar.");
+let gUserBranch = Services.prefs.getBranch("browser.urlbar.");
+
+add_setup(async function () {
+ await QuickSuggestTestUtils.ensureQuickSuggestInit();
+});
+
+// Makes sure `QuickSuggest._updateFeatureState()` is called when the
+// `browser.urlbar.quicksuggest.enabled` pref is changed.
+add_task(async function test_updateFeatureState_pref() {
+ Assert.ok(
+ UrlbarPrefs.get("quicksuggest.enabled"),
+ "Sanity check: quicksuggest.enabled is true by default"
+ );
+
+ let sandbox = sinon.createSandbox();
+ let spy = sandbox.spy(QuickSuggest, "_updateFeatureState");
+
+ UrlbarPrefs.set("quicksuggest.enabled", false);
+ Assert.equal(
+ spy.callCount,
+ 1,
+ "_updateFeatureState called once after changing pref"
+ );
+
+ UrlbarPrefs.clear("quicksuggest.enabled");
+ Assert.equal(
+ spy.callCount,
+ 2,
+ "_updateFeatureState called again after clearing pref"
+ );
+
+ sandbox.restore();
+});
+
+// Makes sure `QuickSuggest._updateFeatureState()` is called when a Nimbus
+// experiment is installed and uninstalled.
+add_task(async function test_updateFeatureState_experiment() {
+ let sandbox = sinon.createSandbox();
+ let spy = sandbox.spy(QuickSuggest, "_updateFeatureState");
+
+ await QuickSuggestTestUtils.withExperiment({
+ callback: () => {
+ Assert.equal(
+ spy.callCount,
+ 1,
+ "_updateFeatureState called once after installing experiment"
+ );
+ },
+ });
+
+ Assert.equal(
+ spy.callCount,
+ 2,
+ "_updateFeatureState called again after uninstalling experiment"
+ );
+
+ sandbox.restore();
+});
+
+add_task(async function test_indexes() {
+ await QuickSuggestTestUtils.withExperiment({
+ valueOverrides: {
+ quickSuggestNonSponsoredIndex: 99,
+ quickSuggestSponsoredIndex: -1337,
+ },
+ callback: () => {
+ Assert.equal(
+ UrlbarPrefs.get("quickSuggestNonSponsoredIndex"),
+ 99,
+ "quickSuggestNonSponsoredIndex"
+ );
+ Assert.equal(
+ UrlbarPrefs.get("quickSuggestSponsoredIndex"),
+ -1337,
+ "quickSuggestSponsoredIndex"
+ );
+ },
+ });
+});
+
+add_task(async function test_merino() {
+ await QuickSuggestTestUtils.withExperiment({
+ valueOverrides: {
+ merinoEnabled: true,
+ merinoEndpointURL: "http://example.com/test_merino_config",
+ merinoClientVariants: "test-client-variants",
+ merinoProviders: "test-providers",
+ },
+ callback: () => {
+ Assert.equal(UrlbarPrefs.get("merinoEnabled"), true, "merinoEnabled");
+ Assert.equal(
+ UrlbarPrefs.get("merinoEndpointURL"),
+ "http://example.com/test_merino_config",
+ "merinoEndpointURL"
+ );
+ Assert.equal(
+ UrlbarPrefs.get("merinoClientVariants"),
+ "test-client-variants",
+ "merinoClientVariants"
+ );
+ Assert.equal(
+ UrlbarPrefs.get("merinoProviders"),
+ "test-providers",
+ "merinoProviders"
+ );
+ },
+ });
+});
+
+add_task(async function test_scenario_online() {
+ await doBasicScenarioTest("online", {
+ urlbarPrefs: {
+ // prefs
+ "quicksuggest.scenario": "online",
+ "quicksuggest.enabled": true,
+ "quicksuggest.dataCollection.enabled": false,
+ "quicksuggest.shouldShowOnboardingDialog": true,
+ "suggest.quicksuggest.nonsponsored": true,
+ "suggest.quicksuggest.sponsored": true,
+
+ // Nimbus variables
+ quickSuggestScenario: "online",
+ quickSuggestEnabled: true,
+ quickSuggestShouldShowOnboardingDialog: true,
+ },
+ defaults: [
+ {
+ name: "browser.urlbar.quicksuggest.enabled",
+ value: true,
+ },
+ {
+ name: "browser.urlbar.quicksuggest.dataCollection.enabled",
+ value: false,
+ },
+ {
+ name: "browser.urlbar.quicksuggest.shouldShowOnboardingDialog",
+ value: true,
+ },
+ {
+ name: "browser.urlbar.suggest.quicksuggest.nonsponsored",
+ value: true,
+ },
+ {
+ name: "browser.urlbar.suggest.quicksuggest.sponsored",
+ value: true,
+ },
+ ],
+ });
+});
+
+add_task(async function test_scenario_offline() {
+ await doBasicScenarioTest("offline", {
+ urlbarPrefs: {
+ // prefs
+ "quicksuggest.scenario": "offline",
+ "quicksuggest.enabled": true,
+ "quicksuggest.dataCollection.enabled": false,
+ "quicksuggest.shouldShowOnboardingDialog": false,
+ "suggest.quicksuggest.nonsponsored": true,
+ "suggest.quicksuggest.sponsored": true,
+
+ // Nimbus variables
+ quickSuggestScenario: "offline",
+ quickSuggestEnabled: true,
+ quickSuggestShouldShowOnboardingDialog: false,
+ },
+ defaults: [
+ {
+ name: "browser.urlbar.quicksuggest.enabled",
+ value: true,
+ },
+ {
+ name: "browser.urlbar.quicksuggest.dataCollection.enabled",
+ value: false,
+ },
+ {
+ name: "browser.urlbar.quicksuggest.shouldShowOnboardingDialog",
+ value: false,
+ },
+ {
+ name: "browser.urlbar.suggest.quicksuggest.nonsponsored",
+ value: true,
+ },
+ {
+ name: "browser.urlbar.suggest.quicksuggest.sponsored",
+ value: true,
+ },
+ ],
+ });
+});
+
+add_task(async function test_scenario_history() {
+ await doBasicScenarioTest("history", {
+ urlbarPrefs: {
+ // prefs
+ "quicksuggest.scenario": "history",
+ "quicksuggest.enabled": false,
+
+ // Nimbus variables
+ quickSuggestScenario: "history",
+ quickSuggestEnabled: false,
+ },
+ defaults: [
+ {
+ name: "browser.urlbar.quicksuggest.enabled",
+ value: false,
+ },
+ ],
+ });
+});
+
+async function doBasicScenarioTest(scenario, expectedPrefs) {
+ await QuickSuggestTestUtils.withExperiment({
+ valueOverrides: {
+ quickSuggestScenario: scenario,
+ },
+ callback: () => {
+ // Pref updates should always settle down by the time enrollment is done.
+ Assert.ok(
+ !UrlbarPrefs.updatingFirefoxSuggestPrefs,
+ "updatingFirefoxSuggestPrefs is false"
+ );
+
+ assertScenarioPrefs(expectedPrefs);
+ },
+ });
+
+ // Similarly, pref updates should always settle down by the time unenrollment
+ // is done.
+ Assert.ok(
+ !UrlbarPrefs.updatingFirefoxSuggestPrefs,
+ "updatingFirefoxSuggestPrefs is false"
+ );
+
+ assertDefaultScenarioPrefs();
+}
+
+function assertScenarioPrefs({ urlbarPrefs, defaults }) {
+ for (let [name, value] of Object.entries(urlbarPrefs)) {
+ Assert.equal(UrlbarPrefs.get(name), value, `UrlbarPrefs.get("${name}")`);
+ }
+
+ let prefs = Services.prefs.getDefaultBranch("");
+ for (let { name, getter, value } of defaults) {
+ Assert.equal(
+ prefs[getter || "getBoolPref"](name),
+ value,
+ `Default branch pref: ${name}`
+ );
+ }
+}
+
+function assertDefaultScenarioPrefs() {
+ assertScenarioPrefs({
+ urlbarPrefs: {
+ "quicksuggest.scenario": "offline",
+ "quicksuggest.enabled": true,
+ "quicksuggest.dataCollection.enabled": false,
+ "quicksuggest.shouldShowOnboardingDialog": false,
+ "suggest.quicksuggest.nonsponsored": true,
+ "suggest.quicksuggest.sponsored": true,
+
+ // No Nimbus variables since they're only available when an experiment is
+ // installed.
+ },
+ defaults: [
+ {
+ name: "browser.urlbar.quicksuggest.enabled",
+ value: true,
+ },
+ {
+ name: "browser.urlbar.quicksuggest.dataCollection.enabled",
+ value: false,
+ },
+ {
+ name: "browser.urlbar.quicksuggest.shouldShowOnboardingDialog",
+ value: false,
+ },
+ {
+ name: "browser.urlbar.suggest.quicksuggest.nonsponsored",
+ value: true,
+ },
+ {
+ name: "browser.urlbar.suggest.quicksuggest.sponsored",
+ value: true,
+ },
+ ],
+ });
+}
+
+function clearOnboardingPrefs() {
+ UrlbarPrefs.clear("suggest.quicksuggest.nonsponsored");
+ UrlbarPrefs.clear("suggest.quicksuggest.sponsored");
+ UrlbarPrefs.clear("quicksuggest.dataCollection.enabled");
+ UrlbarPrefs.clear("quicksuggest.shouldShowOnboardingDialog");
+ UrlbarPrefs.clear("quicksuggest.showedOnboardingDialog");
+ UrlbarPrefs.clear("quicksuggest.seenRestarts");
+}
+
+// The following tasks test Nimbus enrollments
+
+// Initial state:
+// * History (quick suggest feature disabled)
+//
+// Enrollment:
+// * History
+//
+// Expected:
+// * All history prefs set on the default branch
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.history,
+ },
+ valueOverrides: {
+ quickSuggestScenario: "history",
+ },
+ expectedPrefs: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.history,
+ },
+ });
+});
+
+// Initial state:
+// * History (quick suggest feature disabled)
+//
+// Enrollment:
+// * Offline
+//
+// Expected:
+// * All offline prefs set on the default branch
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.history,
+ },
+ valueOverrides: {
+ quickSuggestScenario: "offline",
+ },
+ expectedPrefs: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ },
+ });
+});
+
+// Initial state:
+// * History (quick suggest feature disabled)
+//
+// Enrollment:
+// * Online
+//
+// Expected:
+// * All online prefs set on the default branch
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.history,
+ },
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ },
+ expectedPrefs: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ },
+ });
+});
+
+// The following tasks test OFFLINE TO OFFLINE
+
+// Initial state:
+// * Offline (suggestions on and data collection off by default)
+// * User did not override any defaults
+//
+// Enrollment:
+// * Offline
+//
+// Expected:
+// * All offline prefs set on the default branch
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ },
+ valueOverrides: {
+ quickSuggestScenario: "offline",
+ },
+ expectedPrefs: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ },
+ });
+});
+
+// Initial state:
+// * Offline (suggestions on and data collection off by default)
+// * Non-sponsored suggestions: user left on
+// * Sponsored suggestions: user turned off
+// * Data collection: user left off
+//
+// Enrollment:
+// * Offline
+//
+// Expected:
+// * Non-sponsored suggestions: remain on
+// * Sponsored suggestions: remain off
+// * Data collection: remains off
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ userBranch: {
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ valueOverrides: {
+ quickSuggestScenario: "offline",
+ },
+ expectedPrefs: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ userBranch: {
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Offline (suggestions on and data collection off by default)
+// * Non-sponsored suggestions: user turned off
+// * Sponsored suggestions: user left on
+// * Data collection: user left off
+//
+// Enrollment:
+// * Offline
+//
+// Expected:
+// * Non-sponsored suggestions: remain off
+// * Sponsored suggestions: remain on
+// * Data collection: remains off
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ },
+ },
+ valueOverrides: {
+ quickSuggestScenario: "offline",
+ },
+ expectedPrefs: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Offline (suggestions on and data collection off by default)
+// * Non-sponsored suggestions: user turned off
+// * Sponsored suggestions: user turned off
+// * Data collection: user left off
+//
+// Enrollment:
+// * Offline
+//
+// Expected:
+// * Non-sponsored suggestions: remain off
+// * Sponsored suggestions: remain off
+// * Data collection: remains off
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ valueOverrides: {
+ quickSuggestScenario: "offline",
+ },
+ expectedPrefs: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Offline (suggestions on and data collection off by default)
+// * Non-sponsored suggestions: user left on
+// * Sponsored suggestions: user left on
+// * Data collection: user turned on
+//
+// Enrollment:
+// * Offline
+//
+// Expected:
+// * Non-sponsored suggestions: remain on
+// * Sponsored suggestions: remain on
+// * Data collection: remains on
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ userBranch: {
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ valueOverrides: {
+ quickSuggestScenario: "offline",
+ },
+ expectedPrefs: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ userBranch: {
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Offline (suggestions on and data collection off by default)
+// * Non-sponsored suggestions: user turned off
+// * Sponsored suggestions: user turned off
+// * Data collection: user turned on
+//
+// Enrollment:
+// * Offline
+//
+// Expected:
+// * Non-sponsored suggestions: remain off
+// * Sponsored suggestions: remain off
+// * Data collection: remains on
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ valueOverrides: {
+ quickSuggestScenario: "offline",
+ },
+ expectedPrefs: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ });
+});
+
+// The following tasks test OFFLINE TO ONLINE
+
+// Initial state:
+// * Offline (suggestions on and data collection off by default)
+// * User did not override any defaults
+//
+// Enrollment:
+// * Online
+//
+// Expected:
+// * All online prefs set on the default branch
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ },
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ },
+ expectedPrefs: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ },
+ });
+});
+
+// Initial state:
+// * Offline (suggestions on and data collection off by default)
+// * Non-sponsored suggestions: user left on
+// * Sponsored suggestions: user turned off
+// * Data collection: user left off
+//
+// Enrollment:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: remain on
+// * Sponsored suggestions: remain off
+// * Data collection: remains off
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ userBranch: {
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ },
+ expectedPrefs: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Offline (suggestions on and data collection off by default)
+// * Non-sponsored suggestions: user turned off
+// * Sponsored suggestions: user left on
+// * Data collection: user left off
+//
+// Enrollment:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: remain off
+// * Sponsored suggestions: remain on
+// * Data collection: remains off
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ },
+ },
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ },
+ expectedPrefs: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Offline (suggestions on and data collection off by default)
+// * Non-sponsored suggestions: user turned off
+// * Sponsored suggestions: user turned off
+// * Data collection: user left off
+//
+// Enrollment:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: remain off
+// * Sponsored suggestions: remain off
+// * Data collection: remains off
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ },
+ expectedPrefs: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Offline (suggestions on and data collection off by default)
+// * Non-sponsored suggestions: user left on
+// * Sponsored suggestions: user left on
+// * Data collection: user turned on
+//
+// Enrollment:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: remain on
+// * Sponsored suggestions: remain on
+// * Data collection: remains on
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ userBranch: {
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ },
+ expectedPrefs: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ userBranch: {
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Offline (suggestions on and data collection off by default)
+// * Non-sponsored suggestions: user left on
+// * Sponsored suggestions: user turned off
+// * Data collection: user turned on
+//
+// Enrollment:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: remain on
+// * Sponsored suggestions: remain off
+// * Data collection: remains on
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ userBranch: {
+ "suggest.quicksuggest.sponsored": false,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ },
+ expectedPrefs: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.sponsored": false,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Offline (suggestions on and data collection off by default)
+// * Non-sponsored suggestions: user turned off
+// * Sponsored suggestions: user left on
+// * Data collection: user turned on
+//
+// Enrollment:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: remain off
+// * Sponsored suggestions: remain on
+// * Data collection: remains on
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ },
+ expectedPrefs: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Offline (suggestions on and data collection off by default)
+// * Non-sponsored suggestions: user turned off
+// * Sponsored suggestions: user turned off
+// * Data collection: user turned on
+//
+// Enrollment:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: remain off
+// * Sponsored suggestions: remain off
+// * Data collection: remains on
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ },
+ expectedPrefs: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ });
+});
+
+// The following tasks test ONLINE TO ONLINE
+
+// Initial state:
+// * Online (suggestions on and data collection off by default)
+// * User did not override any defaults
+//
+// Enrollment:
+// * Online
+//
+// Expected:
+// * All online prefs set on the default branch
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ },
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ },
+ expectedPrefs: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ },
+ });
+});
+
+// Initial state:
+// * Online (suggestions on and data collection off by default)
+// * Non-sponsored suggestions: user left on
+// * Sponsored suggestions: user turned off
+// * Data collection: user left off
+//
+// Enrollment:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: remain on
+// * Sponsored suggestions: remain off
+// * Data collection: remains off
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ },
+ expectedPrefs: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Online (suggestions on and data collection off by default)
+// * Non-sponsored suggestions: user turned off
+// * Sponsored suggestions: user left on
+// * Data collection: user left off
+//
+// Enrollment:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: remain off
+// * Sponsored suggestions: remain on
+// * Data collection: remains off
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ },
+ },
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ },
+ expectedPrefs: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Online (suggestions on and data collection off by default)
+// * Non-sponsored suggestions: user turned off
+// * Sponsored suggestions: user turned off
+// * Data collection: user left off
+//
+// Enrollment:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: remain off
+// * Sponsored suggestions: remain off
+// * Data collection: remains off
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ },
+ expectedPrefs: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Online (suggestions on and data collection off by default)
+// * Non-sponsored suggestions: user left on
+// * Sponsored suggestions: user left on
+// * Data collection: user turned on
+//
+// Enrollment:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: remain on
+// * Sponsored suggestions: remain on
+// * Data collection: remains on
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ userBranch: {
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ },
+ expectedPrefs: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ userBranch: {
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Online (suggestions on and data collection off by default)
+// * Non-sponsored suggestions: user left on
+// * Sponsored suggestions: user turned off
+// * Data collection: user turned on
+//
+// Enrollment:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: remain on
+// * Sponsored suggestions: remain off
+// * Data collection: remains on
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.sponsored": false,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ },
+ expectedPrefs: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.sponsored": false,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Online (suggestions on and data collection off by default)
+// * Non-sponsored suggestions: user turned off
+// * Sponsored suggestions: user left on
+// * Data collection: user turned on
+//
+// Enrollment:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: remain off
+// * Sponsored suggestions: remain on
+// * Data collection: remains on
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ },
+ expectedPrefs: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Online (suggestions on and data collection off by default)
+// * Non-sponsored suggestions: user turned off
+// * Sponsored suggestions: user turned off
+// * Data collection: user turned on
+//
+// Enrollment:
+// * Online
+//
+// Expected:
+// * Non-sponsored suggestions: remain off
+// * Sponsored suggestions: remain off
+// * Data collection: remains on
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ },
+ expectedPrefs: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ });
+});
+
+// The following tasks test scenarios in conjunction with individual Nimbus
+// variables
+
+// Initial state:
+// * Offline (suggestions on and data collection off by default)
+// * User did not override any defaults
+//
+// Enrollment:
+// * Offline
+// * Sponsored suggestions individually forced on
+//
+// Expected:
+// * Sponsored suggestions: on
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ },
+ valueOverrides: {
+ quickSuggestScenario: "offline",
+ quickSuggestSponsoredEnabled: true,
+ },
+ expectedPrefs: {
+ defaultBranch: {
+ ...UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ "suggest.quicksuggest.sponsored": true,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Offline (suggestions on and data collection off by default)
+// * Sponsored suggestions: user turned off
+//
+// Enrollment:
+// * Offline
+// * Sponsored suggestions individually forced on
+//
+// Expected:
+// * Sponsored suggestions: remain off
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ userBranch: {
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ valueOverrides: {
+ quickSuggestScenario: "offline",
+ quickSuggestSponsoredEnabled: true,
+ },
+ expectedPrefs: {
+ defaultBranch: {
+ ...UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ "suggest.quicksuggest.sponsored": true,
+ },
+ userBranch: {
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Offline (suggestions on and data collection off by default)
+// * User did not override any defaults
+//
+// Enrollment:
+// * Offline
+// * Data collection individually forced on
+//
+// Expected:
+// * Data collection: on
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ },
+ valueOverrides: {
+ quickSuggestScenario: "offline",
+ quickSuggestDataCollectionEnabled: true,
+ },
+ expectedPrefs: {
+ defaultBranch: {
+ ...UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Offline (suggestions on and data collection off by default)
+// * Data collection: user turned off (it's off by default, so this simulates
+// when the user toggled it on and then back off)
+//
+// Enrollment:
+// * Offline
+// * Data collection individually forced on
+//
+// Expected:
+// * Data collection: remains off
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ userBranch: {
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ },
+ valueOverrides: {
+ quickSuggestScenario: "offline",
+ quickSuggestDataCollectionEnabled: true,
+ },
+ expectedPrefs: {
+ defaultBranch: {
+ ...UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ userBranch: {
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Online (suggestions on and data collection off by default)
+// * User did not override any defaults
+//
+// Enrollment:
+// * Online
+// * Sponsored suggestions individually forced off
+//
+// Expected:
+// * Sponsored suggestions: off
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ },
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ quickSuggestSponsoredEnabled: false,
+ },
+ expectedPrefs: {
+ defaultBranch: {
+ ...UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Online (suggestions on and data collection off by default)
+// * Sponsored suggestions: user turned on (they're on by default, so this
+// simulates when the user toggled them off and then back on)
+//
+// Enrollment:
+// * Online
+// * Sponsored suggestions individually forced off
+//
+// Expected:
+// * Sponsored suggestions: remain on
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ userBranch: {
+ "suggest.quicksuggest.sponsored": true,
+ },
+ },
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ quickSuggestSponsoredEnabled: false,
+ },
+ expectedPrefs: {
+ defaultBranch: {
+ ...UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ userBranch: {
+ "suggest.quicksuggest.sponsored": true,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Online (suggestions on and data collection off by default)
+// * User did not override any defaults
+//
+// Enrollment:
+// * Online
+// * Data collection individually forced on
+//
+// Expected:
+// * Data collection: on
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ },
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ quickSuggestDataCollectionEnabled: true,
+ },
+ expectedPrefs: {
+ defaultBranch: {
+ ...UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Online (suggestions on and data collection off by default)
+// * Data collection: user turned off (it's off by default, so this simulates
+// when the user toggled it on and then back off)
+//
+// Enrollment:
+// * Online
+// * Data collection individually forced on
+//
+// Expected:
+// * Data collection: remains off
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ userBranch: {
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ },
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ quickSuggestDataCollectionEnabled: true,
+ },
+ expectedPrefs: {
+ defaultBranch: {
+ ...UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.online,
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ userBranch: {
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ },
+ });
+});
+
+// The following tasks test individual Nimbus variables without scenarios
+
+// Initial state:
+// * Suggestions on by default and user left them on
+//
+// 1. First enrollment:
+// * Suggestions forced off
+//
+// Expected:
+// * Suggestions off
+//
+// 2. User turns on suggestions
+// 3. Second enrollment:
+// * Suggestions forced off again
+//
+// Expected:
+// * Suggestions remain on
+add_task(async function () {
+ await checkEnrollments([
+ {
+ initialPrefsToSet: {
+ defaultBranch: {
+ "suggest.quicksuggest.nonsponsored": true,
+ "suggest.quicksuggest.sponsored": true,
+ },
+ },
+ valueOverrides: {
+ quickSuggestNonSponsoredEnabled: false,
+ quickSuggestSponsoredEnabled: false,
+ },
+ expectedPrefs: {
+ defaultBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ },
+ {
+ initialPrefsToSet: {
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": true,
+ "suggest.quicksuggest.sponsored": true,
+ },
+ },
+ valueOverrides: {
+ quickSuggestNonSponsoredEnabled: false,
+ quickSuggestSponsoredEnabled: false,
+ },
+ expectedPrefs: {
+ defaultBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": true,
+ "suggest.quicksuggest.sponsored": true,
+ },
+ },
+ },
+ ]);
+});
+
+// Initial state:
+// * Suggestions on by default but user turned them off
+//
+// Enrollment:
+// * Suggestions forced on
+//
+// Expected:
+// * Suggestions remain off
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: {
+ "suggest.quicksuggest.nonsponsored": true,
+ "suggest.quicksuggest.sponsored": true,
+ },
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ valueOverrides: {
+ quickSuggestNonSponsoredEnabled: true,
+ quickSuggestSponsoredEnabled: true,
+ },
+ expectedPrefs: {
+ defaultBranch: {
+ "suggest.quicksuggest.nonsponsored": true,
+ "suggest.quicksuggest.sponsored": true,
+ },
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Suggestions off by default and user left them off
+//
+// 1. First enrollment:
+// * Suggestions forced on
+//
+// Expected:
+// * Suggestions on
+//
+// 2. User turns off suggestions
+// 3. Second enrollment:
+// * Suggestions forced on again
+//
+// Expected:
+// * Suggestions remain off
+add_task(async function () {
+ await checkEnrollments([
+ {
+ initialPrefsToSet: {
+ defaultBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ valueOverrides: {
+ quickSuggestNonSponsoredEnabled: true,
+ quickSuggestSponsoredEnabled: true,
+ },
+ expectedPrefs: {
+ defaultBranch: {
+ "suggest.quicksuggest.nonsponsored": true,
+ "suggest.quicksuggest.sponsored": true,
+ },
+ },
+ },
+ {
+ initialPrefsToSet: {
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ valueOverrides: {
+ quickSuggestNonSponsoredEnabled: true,
+ quickSuggestSponsoredEnabled: true,
+ },
+ expectedPrefs: {
+ defaultBranch: {
+ "suggest.quicksuggest.nonsponsored": true,
+ "suggest.quicksuggest.sponsored": true,
+ },
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ },
+ },
+ ]);
+});
+
+// Initial state:
+// * Suggestions off by default but user turned them on
+//
+// Enrollment:
+// * Suggestions forced off
+//
+// Expected:
+// * Suggestions remain on
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": true,
+ "suggest.quicksuggest.sponsored": true,
+ },
+ },
+ valueOverrides: {
+ quickSuggestNonSponsoredEnabled: false,
+ quickSuggestSponsoredEnabled: false,
+ },
+ expectedPrefs: {
+ defaultBranch: {
+ "suggest.quicksuggest.nonsponsored": false,
+ "suggest.quicksuggest.sponsored": false,
+ },
+ userBranch: {
+ "suggest.quicksuggest.nonsponsored": true,
+ "suggest.quicksuggest.sponsored": true,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Data collection on by default and user left them on
+//
+// 1. First enrollment:
+// * Data collection forced off
+//
+// Expected:
+// * Data collection off
+//
+// 2. User turns on data collection
+// 3. Second enrollment:
+// * Data collection forced off again
+//
+// Expected:
+// * Data collection remains on
+add_task(async function () {
+ await checkEnrollments(
+ [
+ {
+ initialPrefsToSet: {
+ defaultBranch: {
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ valueOverrides: {
+ quickSuggestDataCollectionEnabled: false,
+ },
+ expectedPrefs: {
+ defaultBranch: {
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ },
+ },
+ ],
+ [
+ {
+ initialPrefsToSet: {
+ userBranch: {
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ valueOverrides: {
+ quickSuggestDataCollectionEnabled: false,
+ },
+ expectedPrefs: {
+ defaultBranch: {
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ userBranch: {
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ },
+ ]
+ );
+});
+
+// Initial state:
+// * Data collection on by default but user turned it off
+//
+// Enrollment:
+// * Data collection forced on
+//
+// Expected:
+// * Data collection remains off
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: {
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ userBranch: {
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ },
+ valueOverrides: {
+ quickSuggestDataCollectionEnabled: true,
+ },
+ expectedPrefs: {
+ defaultBranch: {
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ userBranch: {
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ },
+ });
+});
+
+// Initial state:
+// * Data collection off by default and user left it off
+//
+// 1. First enrollment:
+// * Data collection forced on
+//
+// Expected:
+// * Data collection on
+//
+// 2. User turns off data collection
+// 3. Second enrollment:
+// * Data collection forced on again
+//
+// Expected:
+// * Data collection remains off
+add_task(async function () {
+ await checkEnrollments(
+ [
+ {
+ initialPrefsToSet: {
+ defaultBranch: {
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ },
+ valueOverrides: {
+ quickSuggestDataCollectionEnabled: true,
+ },
+ expectedPrefs: {
+ defaultBranch: {
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ },
+ ],
+ [
+ {
+ initialPrefsToSet: {
+ userBranch: {
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ },
+ valueOverrides: {
+ quickSuggestDataCollectionEnabled: true,
+ },
+ expectedPrefs: {
+ defaultBranch: {
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ userBranch: {
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ },
+ },
+ ]
+ );
+});
+
+// Initial state:
+// * Data collection off by default but user turned it on
+//
+// Enrollment:
+// * Data collection forced off
+//
+// Expected:
+// * Data collection remains on
+add_task(async function () {
+ await checkEnrollments({
+ initialPrefsToSet: {
+ defaultBranch: {
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ userBranch: {
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ valueOverrides: {
+ quickSuggestDataCollectionEnabled: false,
+ },
+ expectedPrefs: {
+ defaultBranch: {
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ userBranch: {
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ },
+ });
+});
+
+/**
+ * Tests one or more enrollments. Sets an initial set of prefs on the default
+ * and/or user branches, enrolls in a mock Nimbus experiment, checks expected
+ * pref values, unenrolls, and finally checks prefs again.
+ *
+ * The given `options` value may be an object as described below or an array of
+ * such objects, one per enrollment.
+ *
+ * @param {object} options
+ * Function options.
+ * @param {object} options.initialPrefsToSet
+ * An object: { userBranch, defaultBranch }
+ * `userBranch` and `defaultBranch` are objects that map pref names (relative
+ * to `browser.urlbar`) to values. These prefs will be set on the appropriate
+ * branch before enrollment. Both `userBranch` and `defaultBranch` are
+ * optional.
+ * @param {object} options.valueOverrides
+ * The `valueOverrides` object passed to the mock experiment. It should map
+ * Nimbus variable names to values.
+ * @param {object} options.expectedPrefs
+ * Preferences that should be set after enrollment. It has the same shape as
+ * `options.initialPrefsToSet`.
+ */
+async function checkEnrollments(options) {
+ info("Testing: " + JSON.stringify(options));
+
+ let enrollments;
+ if (Array.isArray(options)) {
+ enrollments = options;
+ } else {
+ enrollments = [options];
+ }
+
+ // Do each enrollment.
+ for (let i = 0; i < enrollments.length; i++) {
+ info(
+ `Starting setup for enrollment ${i}: ` + JSON.stringify(enrollments[i])
+ );
+
+ let { initialPrefsToSet, valueOverrides, expectedPrefs } = enrollments[i];
+
+ // Set initial prefs.
+ UrlbarPrefs._updatingFirefoxSuggestScenario = true;
+ let { defaultBranch: initialDefaultBranch, userBranch: initialUserBranch } =
+ initialPrefsToSet;
+ initialDefaultBranch = initialDefaultBranch || {};
+ initialUserBranch = initialUserBranch || {};
+ for (let name of Object.keys(initialDefaultBranch)) {
+ // Clear user-branch values on the default prefs so the defaults aren't
+ // masked.
+ gUserBranch.clearUserPref(name);
+ }
+ for (let [branch, prefs] of [
+ [gDefaultBranch, initialDefaultBranch],
+ [gUserBranch, initialUserBranch],
+ ]) {
+ for (let [name, value] of Object.entries(prefs)) {
+ branch.setBoolPref(name, value);
+ }
+ }
+ UrlbarPrefs._updatingFirefoxSuggestScenario = false;
+
+ let {
+ defaultBranch: expectedDefaultBranch,
+ userBranch: expectedUserBranch,
+ } = expectedPrefs;
+ expectedDefaultBranch = expectedDefaultBranch || {};
+ expectedUserBranch = expectedUserBranch || {};
+
+ // Install the experiment.
+ info(`Installing experiment for enrollment ${i}`);
+ await QuickSuggestTestUtils.withExperiment({
+ valueOverrides,
+ callback: () => {
+ info(`Installed experiment for enrollment ${i}, now checking prefs`);
+
+ // Check expected pref values. Store expected effective values as we go
+ // so we can check them afterward. For a given pref, the expected
+ // effective value is the user value, or if there's not a user value,
+ // the default value.
+ let expectedEffectivePrefs = {};
+ for (let [branch, prefs, branchType] of [
+ [gDefaultBranch, expectedDefaultBranch, "default"],
+ [gUserBranch, expectedUserBranch, "user"],
+ ]) {
+ for (let [name, value] of Object.entries(prefs)) {
+ expectedEffectivePrefs[name] = value;
+ Assert.equal(
+ branch.getBoolPref(name),
+ value,
+ `Pref ${name} on ${branchType} branch`
+ );
+ if (branch == gUserBranch) {
+ Assert.ok(
+ gUserBranch.prefHasUserValue(name),
+ `Pref ${name} is on user branch`
+ );
+ }
+ }
+ }
+ for (let name of Object.keys(initialDefaultBranch)) {
+ if (!expectedUserBranch.hasOwnProperty(name)) {
+ Assert.ok(
+ !gUserBranch.prefHasUserValue(name),
+ `Pref ${name} is not on user branch`
+ );
+ }
+ }
+ for (let [name, value] of Object.entries(expectedEffectivePrefs)) {
+ Assert.equal(
+ UrlbarPrefs.get(name),
+ value,
+ `Pref ${name} effective value`
+ );
+ }
+
+ info(`Uninstalling experiment for enrollment ${i}`);
+ },
+ });
+
+ info(`Uninstalled experiment for enrollment ${i}, now checking prefs`);
+
+ // Check expected effective values after unenrollment. The expected
+ // effective value for a pref at this point is the value on the user branch,
+ // or if there's not a user value, the original value on the default branch
+ // before enrollment. This assumes the default values reflect the offline
+ // scenario (the case for the U.S. region).
+ let effectivePrefs = Object.assign(
+ {},
+ UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS.offline
+ );
+ for (let [name, value] of Object.entries(expectedUserBranch)) {
+ effectivePrefs[name] = value;
+ }
+ for (let [name, value] of Object.entries(effectivePrefs)) {
+ Assert.equal(
+ UrlbarPrefs.get(name),
+ value,
+ `Pref ${name} effective value after unenrolling`
+ );
+ }
+
+ // Clean up.
+ UrlbarPrefs._updatingFirefoxSuggestScenario = true;
+ for (let name of Object.keys(expectedUserBranch)) {
+ UrlbarPrefs.clear(name);
+ }
+ UrlbarPrefs._updatingFirefoxSuggestScenario = false;
+ }
+}
+
+// The following tasks test enterprise preference policies
+
+// Preference policy test for the following:
+// * Status: locked
+// * Value: false
+add_task(async function () {
+ await doPolicyTest({
+ prefPolicy: {
+ Status: "locked",
+ Value: false,
+ },
+ expectedDefault: false,
+ expectedUser: undefined,
+ expectedLocked: true,
+ });
+});
+
+// Preference policy test for the following:
+// * Status: locked
+// * Value: true
+add_task(async function () {
+ await doPolicyTest({
+ prefPolicy: {
+ Status: "locked",
+ Value: true,
+ },
+ expectedDefault: true,
+ expectedUser: undefined,
+ expectedLocked: true,
+ });
+});
+
+// Preference policy test for the following:
+// * Status: default
+// * Value: false
+add_task(async function () {
+ await doPolicyTest({
+ prefPolicy: {
+ Status: "default",
+ Value: false,
+ },
+ expectedDefault: false,
+ expectedUser: undefined,
+ expectedLocked: false,
+ });
+});
+
+// Preference policy test for the following:
+// * Status: default
+// * Value: true
+add_task(async function () {
+ await doPolicyTest({
+ prefPolicy: {
+ Status: "default",
+ Value: true,
+ },
+ expectedDefault: true,
+ expectedUser: undefined,
+ expectedLocked: false,
+ });
+});
+
+// Preference policy test for the following:
+// * Status: user
+// * Value: false
+add_task(async function () {
+ await doPolicyTest({
+ prefPolicy: {
+ Status: "user",
+ Value: false,
+ },
+ expectedDefault: true,
+ expectedUser: false,
+ expectedLocked: false,
+ });
+});
+
+// Preference policy test for the following:
+// * Status: user
+// * Value: true
+add_task(async function () {
+ await doPolicyTest({
+ prefPolicy: {
+ Status: "user",
+ Value: true,
+ },
+ expectedDefault: true,
+ // Because the pref is sticky, it's true on the user branch even though it's
+ // also true on the default branch. Sticky prefs retain their user-branch
+ // values even when they're the same as their default-branch values.
+ expectedUser: true,
+ expectedLocked: false,
+ });
+});
+
+/**
+ * This tests an enterprise preference policy with one of the quick suggest
+ * sticky prefs (defined by `POLICY_PREF`). Pref policies should apply to the
+ * quick suggest sticky prefs just as they do to non-sticky prefs.
+ *
+ * @param {object} options
+ * Options object.
+ * @param {object} options.prefPolicy
+ * An object `{ Status, Value }` that will be included in the policy.
+ * @param {boolean} options.expectedDefault
+ * The expected default-branch pref value after setting the policy.
+ * @param {boolean} options.expectedUser
+ * The expected user-branch pref value after setting the policy or undefined
+ * if the pref should not exist on the user branch.
+ * @param {boolean} options.expectedLocked
+ * Whether the pref is expected to be locked after setting the policy.
+ */
+async function doPolicyTest({
+ prefPolicy,
+ expectedDefault,
+ expectedUser,
+ expectedLocked,
+}) {
+ info(
+ "Starting pref policy test: " +
+ JSON.stringify({
+ prefPolicy,
+ expectedDefault,
+ expectedUser,
+ expectedLocked,
+ })
+ );
+
+ let pref = POLICY_PREF;
+
+ // Check initial state.
+ Assert.ok(
+ gDefaultBranch.getBoolPref(pref),
+ `${pref} is initially true on default branch (assuming en-US)`
+ );
+ Assert.ok(
+ !gUserBranch.prefHasUserValue(pref),
+ `${pref} does not have initial user value`
+ );
+
+ // Set up the policy.
+ await EnterprisePolicyTesting.setupPolicyEngineWithJson({
+ policies: {
+ Preferences: {
+ [`browser.urlbar.${pref}`]: prefPolicy,
+ },
+ },
+ });
+ Assert.equal(
+ Services.policies.status,
+ Ci.nsIEnterprisePolicies.ACTIVE,
+ "Policy engine is active"
+ );
+
+ // Check the default branch.
+ Assert.equal(
+ gDefaultBranch.getBoolPref(pref),
+ expectedDefault,
+ `${pref} has expected default-branch value after setting policy`
+ );
+
+ // Check the user branch.
+ Assert.equal(
+ gUserBranch.prefHasUserValue(pref),
+ expectedUser !== undefined,
+ `${pref} is on user branch as expected after setting policy`
+ );
+ if (expectedUser !== undefined) {
+ Assert.equal(
+ gUserBranch.getBoolPref(pref),
+ expectedUser,
+ `${pref} has expected user-branch value after setting policy`
+ );
+ }
+
+ // Check the locked state.
+ Assert.equal(
+ gDefaultBranch.prefIsLocked(pref),
+ expectedLocked,
+ `${pref} is locked as expected after setting policy`
+ );
+
+ // Clean up.
+ await EnterprisePolicyTesting.setupPolicyEngineWithJson("");
+ Assert.equal(
+ Services.policies.status,
+ Ci.nsIEnterprisePolicies.INACTIVE,
+ "Policy engine is inactive"
+ );
+
+ gDefaultBranch.unlockPref(pref);
+ gUserBranch.clearUserPref(pref);
+ await QuickSuggestTestUtils.setScenario(null);
+
+ Assert.ok(
+ !gDefaultBranch.prefIsLocked(pref),
+ `${pref} is not locked after cleanup`
+ );
+ Assert.ok(
+ gDefaultBranch.getBoolPref(pref),
+ `${pref} is true on default branch after cleanup (assuming en-US)`
+ );
+ Assert.ok(
+ !gUserBranch.prefHasUserValue(pref),
+ `${pref} does not have user value after cleanup`
+ );
+}
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_indexes.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_indexes.js
new file mode 100644
index 0000000000..282e1a2ba0
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_indexes.js
@@ -0,0 +1,425 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests the configurable indexes of sponsored and non-sponsored ("Firefox
+// Suggest") quick suggest results.
+
+"use strict";
+
+const SUGGESTIONS_FIRST_PREF = "browser.urlbar.showSearchSuggestionsFirst";
+const SUGGESTIONS_PREF = "browser.urlbar.suggest.searches";
+
+const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
+const MAX_RESULTS = UrlbarPrefs.get("maxRichResults");
+
+const SPONSORED_INDEX_PREF = "browser.urlbar.quicksuggest.sponsoredIndex";
+const NON_SPONSORED_INDEX_PREF =
+ "browser.urlbar.quicksuggest.nonSponsoredIndex";
+
+const SPONSORED_SEARCH_STRING = "frabbits";
+const NON_SPONSORED_SEARCH_STRING = "nonspon";
+
+const TEST_URL = "http://example.com/quicksuggest";
+
+const REMOTE_SETTINGS_RESULTS = [
+ {
+ id: 1,
+ url: `${TEST_URL}?q=${SPONSORED_SEARCH_STRING}`,
+ title: "frabbits",
+ keywords: [SPONSORED_SEARCH_STRING],
+ click_url: "http://click.reporting.test.com/",
+ impression_url: "http://impression.reporting.test.com/",
+ advertiser: "TestAdvertiser",
+ },
+ {
+ id: 2,
+ url: `${TEST_URL}?q=${NON_SPONSORED_SEARCH_STRING}`,
+ title: "Non-Sponsored",
+ keywords: [NON_SPONSORED_SEARCH_STRING],
+ click_url: "http://click.reporting.test.com/nonsponsored",
+ impression_url: "http://impression.reporting.test.com/nonsponsored",
+ advertiser: "TestAdvertiserNonSponsored",
+ iab_category: "5 - Education",
+ },
+];
+
+add_setup(async function () {
+ // This test intermittently times out on Mac TV WebRender.
+ if (AppConstants.platform == "macosx") {
+ requestLongerTimeout(3);
+ }
+
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await UrlbarTestUtils.formHistory.clear();
+
+ // Add a mock engine so we don't hit the network.
+ await SearchTestUtils.installSearchExtension({}, { setAsDefault: true });
+
+ await QuickSuggestTestUtils.ensureQuickSuggestInit({
+ remoteSettingsResults: [
+ {
+ type: "data",
+ attachment: REMOTE_SETTINGS_RESULTS,
+ },
+ ],
+ });
+
+ registerCleanupFunction(async () => {
+ await PlacesUtils.history.clear();
+ });
+});
+
+// Tests with history only
+add_task(async function noSuggestions() {
+ await doTestPermutations(({ withHistory, generalIndex }) => ({
+ expectedResultCount: withHistory ? MAX_RESULTS : 2,
+ expectedIndex: generalIndex == 0 || !withHistory ? 1 : MAX_RESULTS - 1,
+ }));
+});
+
+// Tests with suggestions followed by history
+add_task(async function suggestionsFirst() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[SUGGESTIONS_FIRST_PREF, true]],
+ });
+ await withSuggestions(async () => {
+ await doTestPermutations(({ withHistory, generalIndex }) => ({
+ expectedResultCount: withHistory ? MAX_RESULTS : 4,
+ expectedIndex: generalIndex == 0 || !withHistory ? 3 : MAX_RESULTS - 1,
+ }));
+ });
+ await SpecialPowers.popPrefEnv();
+});
+
+// Tests with history followed by suggestions
+add_task(async function suggestionsLast() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[SUGGESTIONS_FIRST_PREF, false]],
+ });
+ await withSuggestions(async () => {
+ await doTestPermutations(({ withHistory, generalIndex }) => ({
+ expectedResultCount: withHistory ? MAX_RESULTS : 4,
+ expectedIndex: generalIndex == 0 || !withHistory ? 1 : MAX_RESULTS - 3,
+ }));
+ });
+ await SpecialPowers.popPrefEnv();
+});
+
+// Tests with history only plus a suggestedIndex result with a resultSpan
+add_task(async function otherSuggestedIndex_noSuggestions() {
+ await doSuggestedIndexTest([
+ // heuristic
+ { heuristic: true },
+ // TestProvider result
+ { suggestedIndex: 1, resultSpan: 2 },
+ // history
+ { type: UrlbarUtils.RESULT_TYPE.URL },
+ { type: UrlbarUtils.RESULT_TYPE.URL },
+ { type: UrlbarUtils.RESULT_TYPE.URL },
+ { type: UrlbarUtils.RESULT_TYPE.URL },
+ { type: UrlbarUtils.RESULT_TYPE.URL },
+ { type: UrlbarUtils.RESULT_TYPE.URL },
+ // quick suggest
+ {
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ providerName: UrlbarProviderQuickSuggest.name,
+ },
+ ]);
+});
+
+// Tests with suggestions followed by history plus a suggestedIndex result with
+// a resultSpan
+add_task(async function otherSuggestedIndex_suggestionsFirst() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[SUGGESTIONS_FIRST_PREF, true]],
+ });
+ await withSuggestions(async () => {
+ await doSuggestedIndexTest([
+ // heuristic
+ { heuristic: true },
+ // TestProvider result
+ { suggestedIndex: 1, resultSpan: 2 },
+ // search suggestions
+ {
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ payload: { suggestion: SPONSORED_SEARCH_STRING + "foo" },
+ },
+ {
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ payload: { suggestion: SPONSORED_SEARCH_STRING + "bar" },
+ },
+ // history
+ { type: UrlbarUtils.RESULT_TYPE.URL },
+ { type: UrlbarUtils.RESULT_TYPE.URL },
+ { type: UrlbarUtils.RESULT_TYPE.URL },
+ { type: UrlbarUtils.RESULT_TYPE.URL },
+ // quick suggest
+ {
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ providerName: UrlbarProviderQuickSuggest.name,
+ },
+ ]);
+ });
+ await SpecialPowers.popPrefEnv();
+});
+
+// Tests with history followed by suggestions plus a suggestedIndex result with
+// a resultSpan
+add_task(async function otherSuggestedIndex_suggestionsLast() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[SUGGESTIONS_FIRST_PREF, false]],
+ });
+ await withSuggestions(async () => {
+ await doSuggestedIndexTest([
+ // heuristic
+ { heuristic: true },
+ // TestProvider result
+ { suggestedIndex: 1, resultSpan: 2 },
+ // history
+ { type: UrlbarUtils.RESULT_TYPE.URL },
+ { type: UrlbarUtils.RESULT_TYPE.URL },
+ { type: UrlbarUtils.RESULT_TYPE.URL },
+ { type: UrlbarUtils.RESULT_TYPE.URL },
+ // quick suggest
+ {
+ type: UrlbarUtils.RESULT_TYPE.URL,
+ providerName: UrlbarProviderQuickSuggest.name,
+ },
+ // search suggestions
+ {
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ payload: { suggestion: SPONSORED_SEARCH_STRING + "foo" },
+ },
+ {
+ type: UrlbarUtils.RESULT_TYPE.SEARCH,
+ payload: { suggestion: SPONSORED_SEARCH_STRING + "bar" },
+ },
+ ]);
+ });
+ await SpecialPowers.popPrefEnv();
+});
+
+/**
+ * A test provider that returns one result with a suggestedIndex and resultSpan.
+ */
+class TestProvider extends UrlbarTestUtils.TestProvider {
+ constructor() {
+ super({
+ results: [
+ Object.assign(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.URL,
+ UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
+ { url: "http://example.com/test" }
+ ),
+ {
+ suggestedIndex: 1,
+ resultSpan: 2,
+ }
+ ),
+ ],
+ });
+ }
+}
+
+/**
+ * Does a round of test permutations.
+ *
+ * @param {Function} callback
+ * For each permutation, this will be called with the arguments of `doTest()`,
+ * and it should return an object with the appropriate values of
+ * `expectedResultCount` and `expectedIndex`.
+ */
+async function doTestPermutations(callback) {
+ for (let isSponsored of [true, false]) {
+ for (let withHistory of [true, false]) {
+ for (let generalIndex of [0, -1]) {
+ let opts = {
+ isSponsored,
+ withHistory,
+ generalIndex,
+ };
+ await doTest(Object.assign(opts, callback(opts)));
+ }
+ }
+ }
+}
+
+/**
+ * Does one test run.
+ *
+ * @param {object} options
+ * Options for the test.
+ * @param {boolean} options.isSponsored
+ * True to use a sponsored result, false to use a non-sponsored result.
+ * @param {boolean} options.withHistory
+ * True to run with a bunch of history, false to run with no history.
+ * @param {number} options.generalIndex
+ * The value to set as the relevant index pref, i.e., the index within the
+ * general group of the quick suggest result.
+ * @param {number} options.expectedResultCount
+ * The expected total result count for sanity checking.
+ * @param {number} options.expectedIndex
+ * The expected index of the quick suggest result in the whole results list.
+ */
+async function doTest({
+ isSponsored,
+ withHistory,
+ generalIndex,
+ expectedResultCount,
+ expectedIndex,
+}) {
+ info(
+ "Running test with options: " +
+ JSON.stringify({
+ isSponsored,
+ withHistory,
+ generalIndex,
+ expectedResultCount,
+ expectedIndex,
+ })
+ );
+
+ // Set the index pref.
+ let indexPref = isSponsored ? SPONSORED_INDEX_PREF : NON_SPONSORED_INDEX_PREF;
+ await SpecialPowers.pushPrefEnv({
+ set: [[indexPref, generalIndex]],
+ });
+
+ // Add history.
+ if (withHistory) {
+ await addHistory();
+ }
+
+ // Do a search.
+ let value = isSponsored
+ ? SPONSORED_SEARCH_STRING
+ : NON_SPONSORED_SEARCH_STRING;
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value,
+ });
+
+ // Check the result count and quick suggest result.
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ expectedResultCount,
+ "Expected result count"
+ );
+ await QuickSuggestTestUtils.assertIsQuickSuggest({
+ window,
+ isSponsored,
+ index: expectedIndex,
+ url: isSponsored
+ ? `${TEST_URL}?q=${SPONSORED_SEARCH_STRING}`
+ : `${TEST_URL}?q=${NON_SPONSORED_SEARCH_STRING}`,
+ });
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ await PlacesUtils.history.clear();
+ await SpecialPowers.popPrefEnv();
+}
+
+/**
+ * Adds history that matches the sponsored and non-sponsored search strings.
+ */
+async function addHistory() {
+ for (let i = 0; i < MAX_RESULTS; i++) {
+ await PlacesTestUtils.addVisits([
+ "http://example.com/" + SPONSORED_SEARCH_STRING + i,
+ "http://example.com/" + NON_SPONSORED_SEARCH_STRING + i,
+ ]);
+ }
+}
+
+/**
+ * Adds a search engine that provides suggestions, calls your callback, and then
+ * removes the engine.
+ *
+ * @param {Function} callback
+ * Your callback function.
+ */
+async function withSuggestions(callback) {
+ await SpecialPowers.pushPrefEnv({
+ set: [[SUGGESTIONS_PREF, true]],
+ });
+ let engine = await SearchTestUtils.promiseNewSearchEngine({
+ url: getRootDirectory(gTestPath) + TEST_ENGINE_BASENAME,
+ });
+ let oldDefaultEngine = await Services.search.getDefault();
+ await Services.search.setDefault(
+ engine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ try {
+ await callback(engine);
+ } finally {
+ await Services.search.setDefault(
+ oldDefaultEngine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ await Services.search.removeEngine(engine);
+ await SpecialPowers.popPrefEnv();
+ }
+}
+
+/**
+ * Registers a test provider that returns a result with a suggestedIndex and
+ * resultSpan and asserts the given expected results match the actual results.
+ *
+ * @param {Array} expectedProps
+ * See `checkResults()`.
+ */
+async function doSuggestedIndexTest(expectedProps) {
+ await addHistory();
+ let provider = new TestProvider();
+ UrlbarProvidersManager.registerProvider(provider);
+
+ let context = await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: SPONSORED_SEARCH_STRING,
+ });
+ checkResults(context.results, expectedProps);
+ await UrlbarTestUtils.promisePopupClose(window);
+
+ UrlbarProvidersManager.unregisterProvider(provider);
+ await PlacesUtils.history.clear();
+}
+
+/**
+ * Asserts the given actual and expected results match.
+ *
+ * @param {Array} actualResults
+ * Array of actual results.
+ * @param {Array} expectedProps
+ * Array of expected result-like objects. Only the properties defined in each
+ * of these objects are compared against the corresponding actual result.
+ */
+function checkResults(actualResults, expectedProps) {
+ Assert.equal(
+ actualResults.length,
+ expectedProps.length,
+ "Expected result count"
+ );
+
+ let actualProps = actualResults.map((actual, i) => {
+ if (expectedProps.length <= i) {
+ return actual;
+ }
+ let props = {};
+ let expected = expectedProps[i];
+ for (let [key, expectedValue] of Object.entries(expected)) {
+ if (key != "payload") {
+ props[key] = actual[key];
+ } else {
+ props.payload = {};
+ for (let pkey of Object.keys(expectedValue)) {
+ props.payload[pkey] = actual.payload[pkey];
+ }
+ }
+ }
+ return props;
+ });
+ Assert.deepEqual(actualProps, expectedProps);
+}
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_merinoSessions.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_merinoSessions.js
new file mode 100644
index 0000000000..050ea31e12
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_merinoSessions.js
@@ -0,0 +1,142 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// End-to-end browser smoke test for Merino sessions. More comprehensive tests
+// are in test_quicksuggest_merinoSessions.js. This test essentially makes sure
+// engagements occur as expected when interacting with the urlbar. If you need
+// to add tests that do not depend on a new definition of "engagement", consider
+// adding them to test_quicksuggest_merinoSessions.js instead.
+
+"use strict";
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.urlbar.merino.enabled", true],
+ ["browser.urlbar.quicksuggest.remoteSettings.enabled", false],
+ ["browser.urlbar.quicksuggest.dataCollection.enabled", true],
+ ],
+ });
+
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await UrlbarTestUtils.formHistory.clear();
+
+ // Install a mock default engine so we don't hit the network.
+ await SearchTestUtils.installSearchExtension({}, { setAsDefault: true });
+
+ await MerinoTestUtils.server.start();
+});
+
+// In a single engagement, all requests should use the same session ID and the
+// sequence number should be incremented.
+add_task(async function singleEngagement() {
+ for (let i = 0; i < 3; i++) {
+ let searchString = "search" + i;
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: searchString,
+ fireInputEvent: true,
+ });
+
+ MerinoTestUtils.server.checkAndClearRequests([
+ {
+ params: {
+ [MerinoTestUtils.SEARCH_PARAMS.QUERY]: searchString,
+ [MerinoTestUtils.SEARCH_PARAMS.SEQUENCE_NUMBER]: i,
+ },
+ },
+ ]);
+ }
+
+ await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
+});
+
+// In a single engagement, all requests should use the same session ID and the
+// sequence number should be incremented. This task closes the panel between
+// searches but keeps the input focused, so the engagement should not end.
+add_task(async function singleEngagement_panelClosed() {
+ for (let i = 0; i < 3; i++) {
+ let searchString = "search" + i;
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: searchString,
+ fireInputEvent: true,
+ });
+
+ MerinoTestUtils.server.checkAndClearRequests([
+ {
+ params: {
+ [MerinoTestUtils.SEARCH_PARAMS.QUERY]: searchString,
+ [MerinoTestUtils.SEARCH_PARAMS.SEQUENCE_NUMBER]: i,
+ },
+ },
+ ]);
+
+ EventUtils.synthesizeKey("KEY_Escape");
+ Assert.ok(!UrlbarTestUtils.isPopupOpen(window), "Panel is closed");
+ Assert.ok(gURLBar.focused, "Input remains focused");
+ }
+
+ // End the engagement to reset the session for the next test.
+ gURLBar.blur();
+});
+
+// New engagements should not use the same session ID as previous engagements
+// and the sequence number should be reset. This task completes each engagement
+// successfully.
+add_task(async function manyEngagements_engagement() {
+ for (let i = 0; i < 3; i++) {
+ // Open a new tab since we'll load the mock default search engine page.
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ let searchString = "search" + i;
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: searchString,
+ fireInputEvent: true,
+ });
+
+ MerinoTestUtils.server.checkAndClearRequests([
+ {
+ params: {
+ [MerinoTestUtils.SEARCH_PARAMS.QUERY]: searchString,
+ [MerinoTestUtils.SEARCH_PARAMS.SEQUENCE_NUMBER]: 0,
+ },
+ },
+ ]);
+
+ // Press enter on the heuristic result to load the search engine page and
+ // complete the engagement.
+ let loadPromise = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser
+ );
+ EventUtils.synthesizeKey("KEY_Enter");
+ await loadPromise;
+ });
+ }
+});
+
+// New engagements should not use the same session ID as previous engagements
+// and the sequence number should be reset. This task abandons each engagement.
+add_task(async function manyEngagements_abandonment() {
+ for (let i = 0; i < 3; i++) {
+ let searchString = "search" + i;
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: searchString,
+ fireInputEvent: true,
+ });
+
+ MerinoTestUtils.server.checkAndClearRequests([
+ {
+ params: {
+ [MerinoTestUtils.SEARCH_PARAMS.QUERY]: searchString,
+ [MerinoTestUtils.SEARCH_PARAMS.SEQUENCE_NUMBER]: 0,
+ },
+ },
+ ]);
+
+ // Blur the urlbar to abandon the engagement.
+ await UrlbarTestUtils.promisePopupClose(window, () => gURLBar.blur());
+ }
+});
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_onboardingDialog.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_onboardingDialog.js
new file mode 100644
index 0000000000..92c3c7c95d
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_quicksuggest_onboardingDialog.js
@@ -0,0 +1,1596 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests the buttons in the onboarding dialog for quick suggest/Firefox Suggest.
+ */
+
+ChromeUtils.defineESModuleGetters(this, {
+ TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.sys.mjs",
+});
+
+const OTHER_DIALOG_URI = getRootDirectory(gTestPath) + "subdialog.xhtml";
+
+// Default-branch pref values in the offline scenario.
+const OFFLINE_DEFAULT_PREFS = {
+ "suggest.quicksuggest.nonsponsored": true,
+ "suggest.quicksuggest.sponsored": true,
+ "quicksuggest.dataCollection.enabled": false,
+};
+
+let gDefaultBranch = Services.prefs.getDefaultBranch("browser.urlbar.");
+let gUserBranch = Services.prefs.getBranch("browser.urlbar.");
+
+// Allow more time for Mac and Linux machines so they don't time out in verify mode.
+if (AppConstants.platform === "macosx") {
+ requestLongerTimeout(4);
+} else if (AppConstants.platform === "linux") {
+ requestLongerTimeout(2);
+}
+
+// Whether the tab key can move the focus. On macOS with full keyboard access
+// disabled (which is default), this will be false. See `canTabMoveFocus`.
+let gCanTabMoveFocus;
+add_setup(async function () {
+ gCanTabMoveFocus = await canTabMoveFocus();
+});
+
+// When the user has already enabled the data-collection pref, the dialog should
+// not appear.
+add_task(async function dataCollectionAlreadyEnabled() {
+ setDialogPrereqPrefs();
+ UrlbarPrefs.set("quicksuggest.dataCollection.enabled", true);
+
+ info("Calling maybeShowOnboardingDialog");
+ let showed = await QuickSuggest.maybeShowOnboardingDialog();
+ Assert.ok(!showed, "The dialog was not shown");
+
+ UrlbarPrefs.clear("quicksuggest.dataCollection.enabled");
+});
+
+// When the current tab is about:welcome, the dialog should not appear.
+add_task(async function aboutWelcome() {
+ setDialogPrereqPrefs();
+ await BrowserTestUtils.withNewTab("about:welcome", async () => {
+ info("Calling maybeShowOnboardingDialog");
+ let showed = await QuickSuggest.maybeShowOnboardingDialog();
+ Assert.ok(!showed, "The dialog was not shown");
+ });
+});
+
+// The Escape key should dismiss the dialog without opting in. This task tests
+// when Escape is pressed while the focus is inside the dialog.
+add_task(async function escKey_focusInsideDialog() {
+ await doDialogTest({
+ callback: async () => {
+ const { maybeShowPromise } = await showOnboardingDialog({
+ skipIntroduction: true,
+ });
+
+ const tabCount = gBrowser.tabs.length;
+ Assert.ok(
+ document.activeElement.classList.contains("dialogFrame"),
+ "dialogFrame is focused in the browser window"
+ );
+
+ info("Close the dialog");
+ EventUtils.synthesizeKey("KEY_Escape");
+
+ await maybeShowPromise;
+
+ Assert.equal(
+ gBrowser.currentURI.spec,
+ "about:blank",
+ "Nothing loaded in the current tab"
+ );
+ Assert.equal(gBrowser.tabs.length, tabCount, "No news tabs were opened");
+ },
+ onboardingDialogVersion: JSON.stringify({ version: 1 }),
+ onboardingDialogChoice: "dismiss_2",
+ expectedUserBranchPrefs: {
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ telemetryEvents: [
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "data_collect_toggled",
+ object: "disabled",
+ },
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "opt_in_dialog",
+ object: "dismiss_2",
+ },
+ ],
+ });
+});
+
+// The Escape key should dismiss the dialog without opting in. This task tests
+// when Escape is pressed while the focus is outside the dialog.
+add_task(async function escKey_focusOutsideDialog() {
+ await doDialogTest({
+ callback: async () => {
+ const { maybeShowPromise } = await showOnboardingDialog({
+ skipIntroduction: true,
+ });
+
+ document.documentElement.focus();
+ Assert.ok(
+ !document.activeElement.classList.contains("dialogFrame"),
+ "dialogFrame is not focused in the browser window"
+ );
+
+ info("Close the dialog");
+ EventUtils.synthesizeKey("KEY_Escape");
+
+ await maybeShowPromise;
+ },
+ onboardingDialogVersion: JSON.stringify({ version: 1 }),
+ onboardingDialogChoice: "dismiss_2",
+ expectedUserBranchPrefs: {
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ telemetryEvents: [
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "data_collect_toggled",
+ object: "disabled",
+ },
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "opt_in_dialog",
+ object: "dismiss_2",
+ },
+ ],
+ });
+});
+
+// The Escape key should dismiss the dialog without opting in when another
+// dialog is queued and shown before the onboarding. This task dismisses the
+// other dialog by pressing the Escape key.
+add_task(async function escKey_queued_esc() {
+ await doQueuedEscKeyTest("KEY_Escape");
+});
+
+// The Escape key should dismiss the dialog without opting in when another
+// dialog is queued and shown before the onboarding. This task dismisses the
+// other dialog by pressing the Enter key.
+add_task(async function escKey_queued_enter() {
+ await doQueuedEscKeyTest("KEY_Enter");
+});
+
+async function doQueuedEscKeyTest(otherDialogKey) {
+ await doDialogTest({
+ callback: async () => {
+ // Create promises that will resolve when each dialog is opened.
+ let uris = [OTHER_DIALOG_URI, QuickSuggest.ONBOARDING_URI];
+ let [otherOpenedPromise, onboardingOpenedPromise] = uris.map(uri =>
+ TestUtils.topicObserved(
+ "subdialog-loaded",
+ contentWin => contentWin.document.documentURI == uri
+ ).then(async ([contentWin]) => {
+ if (contentWin.document.readyState != "complete") {
+ await BrowserTestUtils.waitForEvent(contentWin, "load");
+ }
+ })
+ );
+
+ info("Queuing dialogs for opening");
+ let otherClosedPromise = gDialogBox.open(OTHER_DIALOG_URI);
+ let onboardingClosedPromise = QuickSuggest.maybeShowOnboardingDialog();
+
+ info("Waiting for the other dialog to open");
+ await otherOpenedPromise;
+
+ info(`Pressing ${otherDialogKey} and waiting for other dialog to close`);
+ EventUtils.synthesizeKey(otherDialogKey);
+ await otherClosedPromise;
+
+ info("Waiting for the onboarding dialog to open");
+ await onboardingOpenedPromise;
+
+ info("Pressing Escape and waiting for onboarding dialog to close");
+ EventUtils.synthesizeKey("KEY_Escape");
+ await onboardingClosedPromise;
+ },
+ onboardingDialogVersion: JSON.stringify({ version: 1 }),
+ onboardingDialogChoice: "dismiss_1",
+ expectedUserBranchPrefs: {
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ telemetryEvents: [
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "data_collect_toggled",
+ object: "disabled",
+ },
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "opt_in_dialog",
+ object: "dismiss_1",
+ },
+ ],
+ });
+}
+
+// Tests `dismissed_other` by closing the dialog programmatically.
+add_task(async function dismissed_other_on_introduction() {
+ await doDialogTest({
+ callback: async () => {
+ const { maybeShowPromise } = await showOnboardingDialog();
+ gDialogBox._dialog.close();
+ await maybeShowPromise;
+ },
+ onboardingDialogVersion: JSON.stringify({ version: 1 }),
+ onboardingDialogChoice: "dismiss_1",
+ expectedUserBranchPrefs: {
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ telemetryEvents: [
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "data_collect_toggled",
+ object: "disabled",
+ },
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "opt_in_dialog",
+ object: "dismiss_1",
+ },
+ ],
+ });
+});
+
+// The default is to wait for no browser restarts to show the onboarding dialog
+// on the first restart. This tests that we can override it by configuring the
+// `showOnboardingDialogOnNthRestart`
+add_task(async function nimbus_override_wait_after_n_restarts() {
+ UrlbarPrefs.clear("quicksuggest.shouldShowOnboardingDialog");
+ UrlbarPrefs.clear("quicksuggest.showedOnboardingDialog");
+ UrlbarPrefs.clear("quicksuggest.seenRestarts", 0);
+
+ await QuickSuggestTestUtils.withExperiment({
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ // Wait for 1 browser restart
+ quickSuggestShowOnboardingDialogAfterNRestarts: 1,
+ },
+ callback: async () => {
+ let prefPromise = TestUtils.waitForPrefChange(
+ "browser.urlbar.quicksuggest.showedOnboardingDialog",
+ value => value === true
+ ).then(() => info("Saw pref change"));
+
+ // Simulate 2 restarts. this function is only called by BrowserGlue
+ // on startup, the first restart would be where MR1 was shown then
+ // we will show onboarding the 2nd restart after that.
+ info("Simulating first restart");
+ await QuickSuggest.maybeShowOnboardingDialog();
+
+ info("Simulating second restart");
+ const dialogPromise = BrowserTestUtils.promiseAlertDialogOpen(
+ null,
+ QuickSuggest.ONBOARDING_URI,
+ { isSubDialog: true }
+ );
+ const maybeShowPromise = QuickSuggest.maybeShowOnboardingDialog();
+ const win = await dialogPromise;
+ if (win.document.readyState != "complete") {
+ await BrowserTestUtils.waitForEvent(win, "load");
+ }
+ // Close dialog.
+ EventUtils.synthesizeKey("KEY_Escape");
+
+ info("Waiting for maybeShowPromise and pref change");
+ await Promise.all([maybeShowPromise, prefPromise]);
+ },
+ });
+});
+
+add_task(async function nimbus_skip_onboarding_dialog() {
+ UrlbarPrefs.clear("quicksuggest.shouldShowOnboardingDialog");
+ UrlbarPrefs.clear("quicksuggest.showedOnboardingDialog");
+ UrlbarPrefs.clear("quicksuggest.seenRestarts", 0);
+
+ await QuickSuggestTestUtils.withExperiment({
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ quickSuggestShouldShowOnboardingDialog: false,
+ },
+ callback: async () => {
+ // Simulate 3 restarts.
+ for (let i = 0; i < 3; i++) {
+ info(`Simulating restart ${i + 1}`);
+ await QuickSuggest.maybeShowOnboardingDialog();
+ }
+ Assert.ok(
+ !Services.prefs.getBoolPref(
+ "browser.urlbar.quicksuggest.showedOnboardingDialog",
+ false
+ ),
+ "The showed onboarding dialog pref should not be set"
+ );
+ },
+ });
+});
+
+add_task(async function nimbus_exposure_event() {
+ const testData = [
+ {
+ experimentType: "modal",
+ expectedRecorded: true,
+ },
+ {
+ experimentType: "best-match",
+ expectedRecorded: false,
+ },
+ {
+ expectedRecorded: false,
+ },
+ ];
+
+ for (const { experimentType, expectedRecorded } of testData) {
+ info(`Nimbus exposure event test for type:[${experimentType}]`);
+ UrlbarPrefs.clear("quicksuggest.shouldShowOnboardingDialog");
+ UrlbarPrefs.clear("quicksuggest.showedOnboardingDialog");
+ UrlbarPrefs.clear("quicksuggest.seenRestarts", 0);
+
+ await QuickSuggestTestUtils.clearExposureEvent();
+
+ await QuickSuggestTestUtils.withExperiment({
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ experimentType,
+ },
+ callback: async () => {
+ info("Calling showOnboardingDialog");
+ const { maybeShowPromise } = await showOnboardingDialog();
+ EventUtils.synthesizeKey("KEY_Escape");
+ await maybeShowPromise;
+
+ info("Check the event");
+ await QuickSuggestTestUtils.assertExposureEvent(expectedRecorded);
+ },
+ });
+ }
+});
+
+const LOGO_TYPE = {
+ FIREFOX: 1,
+ MAGGLASS: 2,
+ ANIMATION_MAGGLASS: 3,
+};
+
+const VARIATION_TEST_DATA = [
+ {
+ name: "A",
+ introductionSection: {
+ logoType: LOGO_TYPE.ANIMATION_MAGGLASS,
+ l10n: {
+ onboardingNext: "firefox-suggest-onboarding-introduction-next-button-1",
+ "introduction-title": "firefox-suggest-onboarding-introduction-title-1",
+ },
+ visibility: {
+ "#onboardingLearnMoreOnIntroduction": false,
+ ".description-section": false,
+ ".pager": true,
+ },
+ defaultFocusOrder: [
+ "onboardingNext",
+ "onboardingClose",
+ "onboardingNext",
+ ],
+ actions: ["onboardingClose", "onboardingNext"],
+ },
+ mainSection: {
+ logoType: LOGO_TYPE.MAGGLASS,
+ l10n: {
+ "main-title": "firefox-suggest-onboarding-main-title-1",
+ "main-description": "firefox-suggest-onboarding-main-description-1",
+ "main-accept-option-label":
+ "firefox-suggest-onboarding-main-accept-option-label",
+ "main-accept-option-description":
+ "firefox-suggest-onboarding-main-accept-option-description-1",
+ "main-reject-option-label":
+ "firefox-suggest-onboarding-main-reject-option-label",
+ "main-reject-option-description":
+ "firefox-suggest-onboarding-main-reject-option-description-1",
+ },
+ visibility: {
+ "#main-privacy-first": false,
+ ".description-section #onboardingLearnMore": false,
+ ".accept #onboardingLearnMore": true,
+ ".pager": true,
+ },
+ defaultFocusOrder: [
+ "onboardingNext",
+ "onboardingAccept",
+ "onboardingLearnMore",
+ "onboardingReject",
+ "onboardingSkipLink",
+ "onboardingAccept",
+ ],
+ acceptFocusOrder: [
+ "onboardingAccept",
+ "onboardingLearnMore",
+ "onboardingSubmit",
+ "onboardingSkipLink",
+ "onboardingAccept",
+ ],
+ rejectFocusOrder: [
+ "onboardingReject",
+ "onboardingSubmit",
+ "onboardingSkipLink",
+ "onboardingLearnMore",
+ "onboardingReject",
+ ],
+ actions: [
+ "onboardingAccept",
+ "onboardingReject",
+ "onboardingSkipLink",
+ "onboardingLearnMore",
+ ],
+ },
+ },
+ {
+ // We don't need to test the focus order and actions because the layout of
+ // variation B-H is as same as A.
+ name: "B",
+ introductionSection: {
+ logoType: LOGO_TYPE.ANIMATION_MAGGLASS,
+ l10n: {
+ onboardingNext: "firefox-suggest-onboarding-introduction-next-button-1",
+ "introduction-title": "firefox-suggest-onboarding-introduction-title-2",
+ },
+ visibility: {
+ "#onboardingLearnMoreOnIntroduction": false,
+ ".description-section": false,
+ ".pager": true,
+ },
+ },
+ mainSection: {
+ logoType: LOGO_TYPE.MAGGLASS,
+ l10n: {
+ "main-title": "firefox-suggest-onboarding-main-title-2",
+ "main-description": "firefox-suggest-onboarding-main-description-2",
+ "main-accept-option-label":
+ "firefox-suggest-onboarding-main-accept-option-label",
+ "main-accept-option-description":
+ "firefox-suggest-onboarding-main-accept-option-description-1",
+ "main-reject-option-label":
+ "firefox-suggest-onboarding-main-reject-option-label",
+ "main-reject-option-description":
+ "firefox-suggest-onboarding-main-reject-option-description-1",
+ },
+ visibility: {
+ "#main-privacy-first": false,
+ ".description-section #onboardingLearnMore": false,
+ ".accept #onboardingLearnMore": true,
+ ".pager": true,
+ },
+ },
+ },
+ {
+ name: "C",
+ introductionSection: {
+ logoType: LOGO_TYPE.FIREFOX,
+ l10n: {
+ onboardingNext: "firefox-suggest-onboarding-introduction-next-button-1",
+ "introduction-title": "firefox-suggest-onboarding-introduction-title-3",
+ },
+ visibility: {
+ "#onboardingLearnMoreOnIntroduction": false,
+ ".description-section": false,
+ ".pager": true,
+ },
+ },
+ mainSection: {
+ logoType: LOGO_TYPE.FIREFOX,
+ l10n: {
+ "main-title": "firefox-suggest-onboarding-main-title-3",
+ "main-description": "firefox-suggest-onboarding-main-description-3",
+ "main-accept-option-label":
+ "firefox-suggest-onboarding-main-accept-option-label",
+ "main-accept-option-description":
+ "firefox-suggest-onboarding-main-accept-option-description-1",
+ "main-reject-option-label":
+ "firefox-suggest-onboarding-main-reject-option-label",
+ "main-reject-option-description":
+ "firefox-suggest-onboarding-main-reject-option-description-1",
+ },
+ visibility: {
+ "#main-privacy-first": false,
+ ".description-section #onboardingLearnMore": false,
+ ".accept #onboardingLearnMore": true,
+ ".pager": true,
+ },
+ },
+ },
+ {
+ name: "D",
+ introductionSection: {
+ logoType: LOGO_TYPE.ANIMATION_MAGGLASS,
+ l10n: {
+ onboardingNext: "firefox-suggest-onboarding-introduction-next-button-1",
+ "introduction-title": "firefox-suggest-onboarding-introduction-title-4",
+ },
+ visibility: {
+ "#onboardingLearnMoreOnIntroduction": false,
+ ".description-section": false,
+ ".pager": true,
+ },
+ },
+ mainSection: {
+ logoType: LOGO_TYPE.MAGGLASS,
+ l10n: {
+ "main-title": "firefox-suggest-onboarding-main-title-4",
+ "main-description": "firefox-suggest-onboarding-main-description-4",
+ "main-accept-option-label":
+ "firefox-suggest-onboarding-main-accept-option-label",
+ "main-accept-option-description":
+ "firefox-suggest-onboarding-main-accept-option-description-2",
+ "main-reject-option-label":
+ "firefox-suggest-onboarding-main-reject-option-label",
+ "main-reject-option-description":
+ "firefox-suggest-onboarding-main-reject-option-description-2",
+ },
+ visibility: {
+ "#main-privacy-first": false,
+ ".description-section #onboardingLearnMore": false,
+ ".accept #onboardingLearnMore": true,
+ ".pager": true,
+ },
+ },
+ },
+ {
+ name: "E",
+ introductionSection: {
+ logoType: LOGO_TYPE.FIREFOX,
+ l10n: {
+ onboardingNext: "firefox-suggest-onboarding-introduction-next-button-1",
+ "introduction-title": "firefox-suggest-onboarding-introduction-title-5",
+ },
+ visibility: {
+ "#onboardingLearnMoreOnIntroduction": false,
+ ".description-section": false,
+ ".pager": true,
+ },
+ },
+ mainSection: {
+ logoType: LOGO_TYPE.FIREFOX,
+ l10n: {
+ "main-title": "firefox-suggest-onboarding-main-title-5",
+ "main-description": "firefox-suggest-onboarding-main-description-5",
+ "main-accept-option-label":
+ "firefox-suggest-onboarding-main-accept-option-label",
+ "main-accept-option-description":
+ "firefox-suggest-onboarding-main-accept-option-description-2",
+ "main-reject-option-label":
+ "firefox-suggest-onboarding-main-reject-option-label",
+ "main-reject-option-description":
+ "firefox-suggest-onboarding-main-reject-option-description-2",
+ },
+ visibility: {
+ "#main-privacy-first": false,
+ ".description-section #onboardingLearnMore": false,
+ ".accept #onboardingLearnMore": true,
+ ".pager": true,
+ },
+ },
+ },
+ {
+ name: "F",
+ introductionSection: {
+ logoType: LOGO_TYPE.ANIMATION_MAGGLASS,
+ l10n: {
+ onboardingNext: "firefox-suggest-onboarding-introduction-next-button-2",
+ "introduction-title": "firefox-suggest-onboarding-introduction-title-6",
+ },
+ visibility: {
+ "#onboardingLearnMoreOnIntroduction": false,
+ ".description-section": false,
+ ".pager": true,
+ },
+ },
+ mainSection: {
+ logoType: LOGO_TYPE.MAGGLASS,
+ l10n: {
+ "main-title": "firefox-suggest-onboarding-main-title-6",
+ "main-description": "firefox-suggest-onboarding-main-description-6",
+ "main-accept-option-label":
+ "firefox-suggest-onboarding-main-accept-option-label",
+ "main-accept-option-description":
+ "firefox-suggest-onboarding-main-accept-option-description-2",
+ "main-reject-option-label":
+ "firefox-suggest-onboarding-main-reject-option-label",
+ "main-reject-option-description":
+ "firefox-suggest-onboarding-main-reject-option-description-2",
+ },
+ visibility: {
+ "#main-privacy-first": false,
+ ".description-section #onboardingLearnMore": false,
+ ".accept #onboardingLearnMore": true,
+ ".pager": true,
+ },
+ },
+ },
+ {
+ name: "G",
+ introductionSection: {
+ logoType: LOGO_TYPE.ANIMATION_MAGGLASS,
+ l10n: {
+ onboardingNext: "firefox-suggest-onboarding-introduction-next-button-1",
+ "introduction-title": "firefox-suggest-onboarding-introduction-title-7",
+ },
+ visibility: {
+ "#onboardingLearnMoreOnIntroduction": false,
+ ".description-section": false,
+ ".pager": true,
+ },
+ },
+ mainSection: {
+ logoType: LOGO_TYPE.MAGGLASS,
+ l10n: {
+ "main-title": "firefox-suggest-onboarding-main-title-7",
+ "main-description": "firefox-suggest-onboarding-main-description-7",
+ "main-accept-option-label":
+ "firefox-suggest-onboarding-main-accept-option-label",
+ "main-accept-option-description":
+ "firefox-suggest-onboarding-main-accept-option-description-2",
+ "main-reject-option-label":
+ "firefox-suggest-onboarding-main-reject-option-label",
+ "main-reject-option-description":
+ "firefox-suggest-onboarding-main-reject-option-description-2",
+ },
+ visibility: {
+ "#main-privacy-first": true,
+ ".description-section #onboardingLearnMore": false,
+ ".accept #onboardingLearnMore": true,
+ ".pager": true,
+ },
+ },
+ },
+ {
+ name: "H",
+ introductionSection: {
+ logoType: LOGO_TYPE.FIREFOX,
+ l10n: {
+ onboardingNext: "firefox-suggest-onboarding-introduction-next-button-1",
+ "introduction-title": "firefox-suggest-onboarding-introduction-title-2",
+ },
+ visibility: {
+ "#onboardingLearnMoreOnIntroduction": false,
+ ".description-section": false,
+ ".pager": true,
+ },
+ },
+ mainSection: {
+ logoType: LOGO_TYPE.FIREFOX,
+ l10n: {
+ "main-title": "firefox-suggest-onboarding-main-title-8",
+ "main-description": "firefox-suggest-onboarding-main-description-8",
+ "main-accept-option-label":
+ "firefox-suggest-onboarding-main-accept-option-label",
+ "main-accept-option-description":
+ "firefox-suggest-onboarding-main-accept-option-description-1",
+ "main-reject-option-label":
+ "firefox-suggest-onboarding-main-reject-option-label",
+ "main-reject-option-description":
+ "firefox-suggest-onboarding-main-reject-option-description-1",
+ },
+ visibility: {
+ "#main-privacy-first": false,
+ ".description-section #onboardingLearnMore": false,
+ ".accept #onboardingLearnMore": true,
+ ".pager": true,
+ },
+ },
+ },
+ {
+ name: "100-A",
+ introductionSection: {
+ logoType: LOGO_TYPE.FIREFOX,
+ l10n: {
+ onboardingNext: "firefox-suggest-onboarding-introduction-next-button-3",
+ "introduction-title": "firefox-suggest-onboarding-main-title-9",
+ },
+ visibility: {
+ "#onboardingLearnMoreOnIntroduction": true,
+ ".description-section": true,
+ ".pager": true,
+ },
+ defaultFocusOrder: [
+ "onboardingNext",
+ "onboardingLearnMoreOnIntroduction",
+ "onboardingClose",
+ "onboardingNext",
+ ],
+ actions: [
+ "onboardingClose",
+ "onboardingNext",
+ "onboardingLearnMoreOnIntroduction",
+ ],
+ },
+ mainSection: {
+ logoType: LOGO_TYPE.FIREFOX,
+ l10n: {
+ "main-title": "firefox-suggest-onboarding-main-title-9",
+ "main-description": "firefox-suggest-onboarding-main-description-9",
+ "main-accept-option-label":
+ "firefox-suggest-onboarding-main-accept-option-label-2",
+ "main-accept-option-description":
+ "firefox-suggest-onboarding-main-accept-option-description-3",
+ "main-reject-option-label":
+ "firefox-suggest-onboarding-main-reject-option-label-2",
+ "main-reject-option-description":
+ "firefox-suggest-onboarding-main-reject-option-description-3",
+ },
+ visibility: {
+ "#main-privacy-first": true,
+ ".description-section #onboardingLearnMore": true,
+ ".accept #onboardingLearnMore": false,
+ ".pager": false,
+ },
+ defaultFocusOrder: [
+ "onboardingNext",
+ "onboardingLearnMore",
+ "onboardingAccept",
+ "onboardingReject",
+ "onboardingSkipLink",
+ "onboardingLearnMore",
+ ],
+ acceptFocusOrder: [
+ "onboardingAccept",
+ "onboardingSubmit",
+ "onboardingSkipLink",
+ "onboardingLearnMore",
+ "onboardingAccept",
+ ],
+ rejectFocusOrder: [
+ "onboardingReject",
+ "onboardingSubmit",
+ "onboardingSkipLink",
+ "onboardingLearnMore",
+ "onboardingReject",
+ ],
+ actions: [
+ "onboardingAccept",
+ "onboardingReject",
+ "onboardingSkipLink",
+ "onboardingLearnMore",
+ ],
+ },
+ },
+ {
+ name: "100-B",
+ mainSection: {
+ logoType: LOGO_TYPE.FIREFOX,
+ l10n: {
+ "main-title": "firefox-suggest-onboarding-main-title-9",
+ "main-description": "firefox-suggest-onboarding-main-description-9",
+ "main-accept-option-label":
+ "firefox-suggest-onboarding-main-accept-option-label-2",
+ "main-accept-option-description":
+ "firefox-suggest-onboarding-main-accept-option-description-3",
+ "main-reject-option-label":
+ "firefox-suggest-onboarding-main-reject-option-label-2",
+ "main-reject-option-description":
+ "firefox-suggest-onboarding-main-reject-option-description-3",
+ },
+ visibility: {
+ "#main-privacy-first": true,
+ ".description-section #onboardingLearnMore": true,
+ ".accept #onboardingLearnMore": false,
+ ".pager": false,
+ },
+ // Layout of 100-B is same as 100-A, but since there is no the introduction
+ // pane, only the default focus order on the main pane is a bit diffrence.
+ defaultFocusOrder: [
+ "onboardingLearnMore",
+ "onboardingAccept",
+ "onboardingReject",
+ "onboardingSkipLink",
+ "onboardingLearnMore",
+ ],
+ },
+ },
+];
+
+/**
+ * This test checks for differences due to variations in logo type, l10n text,
+ * element visibility, order of focus, actions, etc. The designation is on
+ * VARIATION_TEST_DATA. The items that can be specified are below.
+ *
+ * name: Specify the variation name.
+ *
+ * The following items are specified for each section.
+ * (introductionSection, mainSection).
+ *
+ * logoType:
+ * Specify the expected logo type. Please refer to LOGO_TYPE about the type.
+ *
+ * l10n:
+ * Specify the expected l10n id applied to elements.
+ *
+ * visibility:
+ * Specify the expected visibility of elements. The way to specify the element
+ * is using selector.
+ *
+ * defaultFocusOrder:
+ * Specify the expected focus order right after the section is appeared. The
+ * way to specify the element is using id.
+ *
+ * acceptFocusOrder:
+ * Specify the expected focus order after selecting accept option.
+ *
+ * rejectFocusOrder:
+ * Specify the expected focus order after selecting reject option.
+ *
+ * actions:
+ * Specify the action we want to verify such as clicking the close button. The
+ * available actions are below.
+ * - onboardingClose:
+ * Action of the close button “x” by mouse/keyboard.
+ * - onboardingNext:
+ * Action of the next button that transits from the introduction section to
+ * the main section by mouse/keyboard.
+ * - onboardingAccept:
+ * Action of the submit button by mouse/keyboard after selecting accept
+ * option by mouse/keyboard.
+ * - onboardingReject:
+ * Action of the submit button by mouse/keyboard after selecting reject
+ * option by mouse/keyboard.
+ * - onboardingSkipLink:
+ * Action of the skip link by mouse/keyboard.
+ * - onboardingLearnMore:
+ * Action of the learn more link by mouse/keyboard.
+ * - onboardingLearnMoreOnIntroduction:
+ * Action of the learn more link on the introduction section by
+ * mouse/keyboard.
+ */
+add_task(async function variation_test() {
+ for (const variation of VARIATION_TEST_DATA) {
+ info(`Test for variation [${variation.name}]`);
+
+ info("Do layout test");
+ await doLayoutTest(variation);
+
+ for (const action of variation.introductionSection?.actions || []) {
+ info(
+ `${action} test on the introduction section for variation [${variation.name}]`
+ );
+ await this[action](variation);
+ }
+
+ for (const action of variation.mainSection?.actions || []) {
+ info(
+ `${action} test on the main section for variation [${variation.name}]`
+ );
+ await this[action](variation, !!variation.introductionSection);
+ }
+ }
+});
+
+async function doLayoutTest(variation) {
+ UrlbarPrefs.clear("quicksuggest.shouldShowOnboardingDialog");
+ UrlbarPrefs.clear("quicksuggest.showedOnboardingDialog");
+ UrlbarPrefs.clear("quicksuggest.seenRestarts", 0);
+
+ await QuickSuggestTestUtils.withExperiment({
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ quickSuggestOnboardingDialogVariation: variation.name,
+ },
+ callback: async () => {
+ info("Calling showOnboardingDialog");
+ const { win, maybeShowPromise } = await showOnboardingDialog();
+
+ const introductionSection = win.document.getElementById(
+ "introduction-section"
+ );
+ const mainSection = win.document.getElementById("main-section");
+
+ if (variation.introductionSection) {
+ info("Check the section visibility");
+ Assert.ok(BrowserTestUtils.is_visible(introductionSection));
+ Assert.ok(BrowserTestUtils.is_hidden(mainSection));
+
+ info("Check the introduction section");
+ await assertSection(introductionSection, variation.introductionSection);
+
+ info("Transition to the main section");
+ win.document.getElementById("onboardingNext").click();
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ BrowserTestUtils.is_hidden(introductionSection) &&
+ BrowserTestUtils.is_visible(mainSection)
+ );
+ } else {
+ info("Check the section visibility");
+ Assert.ok(BrowserTestUtils.is_hidden(introductionSection));
+ Assert.ok(BrowserTestUtils.is_visible(mainSection));
+ }
+
+ info("Check the main section");
+ await assertSection(mainSection, variation.mainSection);
+
+ info("Close the dialog");
+ EventUtils.synthesizeKey("KEY_Escape", {}, win);
+ await maybeShowPromise;
+ },
+ });
+}
+
+async function assertSection(sectionElement, expectedSection) {
+ info("Check the logo");
+ assertLogo(sectionElement, expectedSection.logoType);
+
+ info("Check the l10n");
+ assertL10N(sectionElement, expectedSection.l10n);
+
+ info("Check the visibility");
+ assertVisibility(sectionElement, expectedSection.visibility);
+
+ if (!gCanTabMoveFocus) {
+ Assert.ok(true, "Tab key can't move focus, skipping test for focus order");
+ return;
+ }
+
+ if (expectedSection.defaultFocusOrder) {
+ info("Check the default focus order");
+ assertFocusOrder(sectionElement, expectedSection.defaultFocusOrder);
+ }
+
+ if (expectedSection.acceptFocusOrder) {
+ info("Check the focus order after selecting accept option");
+ sectionElement.querySelector("#onboardingAccept").focus();
+ EventUtils.synthesizeKey("VK_SPACE", {}, sectionElement.ownerGlobal);
+ assertFocusOrder(sectionElement, expectedSection.acceptFocusOrder);
+ }
+
+ if (expectedSection.rejectFocusOrder) {
+ info("Check the focus order after selecting reject option");
+ sectionElement.querySelector("#onboardingReject").focus();
+ EventUtils.synthesizeKey("VK_SPACE", {}, sectionElement.ownerGlobal);
+ assertFocusOrder(sectionElement, expectedSection.rejectFocusOrder);
+ }
+}
+
+function assertLogo(sectionElement, expectedLogoType) {
+ let expectedLogoImage;
+ switch (expectedLogoType) {
+ case LOGO_TYPE.FIREFOX: {
+ expectedLogoImage = 'url("chrome://branding/content/about-logo.svg")';
+ break;
+ }
+ case LOGO_TYPE.MAGGLASS: {
+ expectedLogoImage =
+ 'url("chrome://browser/content/urlbar/quicksuggestOnboarding_magglass.svg")';
+ break;
+ }
+ case LOGO_TYPE.ANIMATION_MAGGLASS: {
+ const mediaQuery = sectionElement.ownerGlobal.matchMedia(
+ "(prefers-reduced-motion: no-preference)"
+ );
+ expectedLogoImage = mediaQuery.matches
+ ? 'url("chrome://browser/content/urlbar/quicksuggestOnboarding_magglass_animation.svg")'
+ : 'url("chrome://browser/content/urlbar/quicksuggestOnboarding_magglass.svg")';
+ break;
+ }
+ default: {
+ Assert.ok(false, `Unexpected image type ${expectedLogoType}`);
+ break;
+ }
+ }
+
+ const logo = sectionElement.querySelector(".logo");
+ Assert.ok(BrowserTestUtils.is_visible(logo));
+ const logoImage =
+ sectionElement.ownerGlobal.getComputedStyle(logo).backgroundImage;
+ Assert.equal(logoImage, expectedLogoImage);
+}
+
+function assertL10N(sectionElement, expectedL10N) {
+ for (const [id, l10n] of Object.entries(expectedL10N)) {
+ const element = sectionElement.querySelector("#" + id);
+ Assert.equal(element.getAttribute("data-l10n-id"), l10n);
+ }
+}
+
+function assertVisibility(sectionElement, expectedVisibility) {
+ for (const [selector, visibility] of Object.entries(expectedVisibility)) {
+ const element = sectionElement.querySelector(selector);
+ if (visibility) {
+ Assert.ok(BrowserTestUtils.is_visible(element));
+ } else {
+ if (!element) {
+ Assert.ok(true);
+ return;
+ }
+ Assert.ok(BrowserTestUtils.is_hidden(element));
+ }
+ }
+}
+
+function assertFocusOrder(sectionElement, expectedFocusOrder) {
+ const win = sectionElement.ownerGlobal;
+
+ // Check initial active element.
+ Assert.equal(win.document.activeElement.id, expectedFocusOrder[0]);
+
+ for (const next of expectedFocusOrder.slice(1)) {
+ EventUtils.synthesizeKey("KEY_Tab", {}, win);
+ Assert.equal(win.document.activeElement.id, next);
+ }
+}
+
+async function onboardingClose(variation) {
+ await doActionTest({
+ callback: async (win, userAction, maybeShowPromise) => {
+ info("Check the status of the close button");
+ const closeButton = win.document.getElementById("onboardingClose");
+ Assert.ok(BrowserTestUtils.is_visible(closeButton));
+ Assert.equal(closeButton.getAttribute("title"), "Close");
+
+ info("Commit the close button");
+ userAction(closeButton);
+
+ info("Waiting for maybeShowOnboardingDialog to finish");
+ await maybeShowPromise;
+ },
+ variation,
+ onboardingDialogVersion: JSON.stringify({
+ version: 1,
+ variation: variation.name.toLowerCase(),
+ }),
+ onboardingDialogChoice: "close_1",
+ expectedUserBranchPrefs: {
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ telemetryEvents: [
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "data_collect_toggled",
+ object: "disabled",
+ },
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "opt_in_dialog",
+ object: "close_1",
+ },
+ ],
+ });
+}
+
+async function onboardingNext(variation) {
+ await doActionTest({
+ callback: async (win, userAction, maybeShowPromise) => {
+ info("Check the status of the next button");
+ const nextButton = win.document.getElementById("onboardingNext");
+ Assert.ok(BrowserTestUtils.is_visible(nextButton));
+
+ info("Commit the next button");
+ userAction(nextButton);
+
+ const introductionSection = win.document.getElementById(
+ "introduction-section"
+ );
+ const mainSection = win.document.getElementById("main-section");
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ BrowserTestUtils.is_hidden(introductionSection) &&
+ BrowserTestUtils.is_visible(mainSection),
+ "Wait for the transition"
+ );
+
+ info("Exit");
+ EventUtils.synthesizeKey("KEY_Escape", {}, win);
+
+ info("Waiting for maybeShowOnboardingDialog to finish");
+ await maybeShowPromise;
+ },
+ variation,
+ onboardingDialogVersion: JSON.stringify({
+ version: 1,
+ variation: variation.name.toLowerCase(),
+ }),
+ onboardingDialogChoice: "dismiss_2",
+ expectedUserBranchPrefs: {
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ telemetryEvents: [
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "data_collect_toggled",
+ object: "disabled",
+ },
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "opt_in_dialog",
+ object: "dismiss_2",
+ },
+ ],
+ });
+}
+
+async function onboardingAccept(variation, skipIntroduction) {
+ await doActionTest({
+ callback: async (win, userAction, maybeShowPromise) => {
+ info("Check the status of the accept option and submit button");
+ const acceptOption = win.document.getElementById("onboardingAccept");
+ const submitButton = win.document.getElementById("onboardingSubmit");
+ Assert.ok(acceptOption);
+ Assert.ok(submitButton.disabled);
+
+ info("Select the accept option");
+ userAction(acceptOption);
+
+ info("Commit the submit button");
+ Assert.ok(!submitButton.disabled);
+ userAction(submitButton);
+
+ info("Waiting for maybeShowOnboardingDialog to finish");
+ await maybeShowPromise;
+ },
+ variation,
+ skipIntroduction,
+ onboardingDialogVersion: JSON.stringify({
+ version: 1,
+ variation: variation.name.toLowerCase(),
+ }),
+ onboardingDialogChoice: "accept_2",
+ expectedUserBranchPrefs: {
+ "quicksuggest.onboardingDialogVersion": JSON.stringify({ version: 1 }),
+ "quicksuggest.dataCollection.enabled": true,
+ },
+ telemetryEvents: [
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "data_collect_toggled",
+ object: "enabled",
+ },
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "opt_in_dialog",
+ object: "accept_2",
+ },
+ ],
+ });
+}
+
+async function onboardingReject(variation, skipIntroduction) {
+ await doActionTest({
+ callback: async (win, userAction, maybeShowPromise) => {
+ info("Check the status of the reject option and submit button");
+ const rejectOption = win.document.getElementById("onboardingReject");
+ const submitButton = win.document.getElementById("onboardingSubmit");
+ Assert.ok(rejectOption);
+ Assert.ok(submitButton.disabled);
+
+ info("Select the reject option");
+ userAction(rejectOption);
+
+ info("Commit the submit button");
+ Assert.ok(!submitButton.disabled);
+ userAction(submitButton);
+
+ info("Waiting for maybeShowOnboardingDialog to finish");
+ await maybeShowPromise;
+ },
+ variation,
+ skipIntroduction,
+ onboardingDialogVersion: JSON.stringify({
+ version: 1,
+ variation: variation.name.toLowerCase(),
+ }),
+ onboardingDialogChoice: "reject_2",
+ expectedUserBranchPrefs: {
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ telemetryEvents: [
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "data_collect_toggled",
+ object: "disabled",
+ },
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "opt_in_dialog",
+ object: "reject_2",
+ },
+ ],
+ });
+}
+
+async function onboardingSkipLink(variation, skipIntroduction) {
+ await doActionTest({
+ callback: async (win, userAction, maybeShowPromise) => {
+ info("Check the status of the skip link");
+ const skipLink = win.document.getElementById("onboardingSkipLink");
+ Assert.ok(BrowserTestUtils.is_visible(skipLink));
+
+ info("Commit the skip link");
+ const tabCount = gBrowser.tabs.length;
+ userAction(skipLink);
+
+ info("Waiting for maybeShowOnboardingDialog to finish");
+ await maybeShowPromise;
+
+ info("Check the current tab status");
+ Assert.equal(
+ gBrowser.currentURI.spec,
+ "about:blank",
+ "Nothing loaded in the current tab"
+ );
+ Assert.equal(gBrowser.tabs.length, tabCount, "No news tabs were opened");
+ },
+ variation,
+ skipIntroduction,
+ onboardingDialogVersion: JSON.stringify({
+ version: 1,
+ variation: variation.name.toLowerCase(),
+ }),
+ onboardingDialogChoice: "not_now_2",
+ expectedUserBranchPrefs: {
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ telemetryEvents: [
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "data_collect_toggled",
+ object: "disabled",
+ },
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "opt_in_dialog",
+ object: "not_now_2",
+ },
+ ],
+ });
+}
+
+async function onboardingLearnMore(variation, skipIntroduction) {
+ await doLearnMoreTest(
+ variation,
+ skipIntroduction,
+ "onboardingLearnMore",
+ "learn_more_2"
+ );
+}
+
+async function onboardingLearnMoreOnIntroduction(variation, skipIntroduction) {
+ await doLearnMoreTest(
+ variation,
+ skipIntroduction,
+ "onboardingLearnMoreOnIntroduction",
+ "learn_more_1"
+ );
+}
+
+async function doLearnMoreTest(variation, skipIntroduction, target, telemetry) {
+ await doActionTest({
+ callback: async (win, userAction, maybeShowPromise) => {
+ info("Check the status of the learn more link");
+ const learnMoreLink = win.document.getElementById(target);
+ Assert.ok(BrowserTestUtils.is_visible(learnMoreLink));
+
+ info("Commit the learn more link");
+ const loadPromise = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ QuickSuggest.HELP_URL
+ ).then(tab => {
+ info("Saw new tab");
+ return tab;
+ });
+ userAction(learnMoreLink);
+
+ info("Waiting for maybeShowOnboardingDialog to finish");
+ await maybeShowPromise;
+
+ info("Waiting for new tab");
+ let tab = await loadPromise;
+
+ info("Check the current tab status");
+ Assert.equal(gBrowser.selectedTab, tab, "Current tab is the new tab");
+ Assert.equal(
+ gBrowser.currentURI.spec,
+ QuickSuggest.HELP_URL,
+ "Current tab is the support page"
+ );
+ BrowserTestUtils.removeTab(tab);
+ },
+ variation,
+ skipIntroduction,
+ onboardingDialogChoice: telemetry,
+ onboardingDialogVersion: JSON.stringify({
+ version: 1,
+ variation: variation.name.toLowerCase(),
+ }),
+ expectedUserBranchPrefs: {
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ telemetryEvents: [
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "data_collect_toggled",
+ object: "disabled",
+ },
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "opt_in_dialog",
+ object: telemetry,
+ },
+ ],
+ });
+}
+
+async function doActionTest({
+ variation,
+ skipIntroduction,
+ callback,
+ onboardingDialogVersion,
+ onboardingDialogChoice,
+ expectedUserBranchPrefs,
+ telemetryEvents,
+}) {
+ const userClick = target => {
+ info("Click on the target");
+ target.click();
+ };
+ const userEnter = target => {
+ target.focus();
+ if (target.type === "radio") {
+ info("Space on the target");
+ EventUtils.synthesizeKey("VK_SPACE", {}, target.ownerGlobal);
+ } else {
+ info("Enter on the target");
+ EventUtils.synthesizeKey("KEY_Enter", {}, target.ownerGlobal);
+ }
+ };
+
+ for (const userAction of [userClick, userEnter]) {
+ UrlbarPrefs.clear("quicksuggest.shouldShowOnboardingDialog");
+ UrlbarPrefs.clear("quicksuggest.showedOnboardingDialog");
+ UrlbarPrefs.clear("quicksuggest.seenRestarts", 0);
+
+ await QuickSuggestTestUtils.withExperiment({
+ valueOverrides: {
+ quickSuggestScenario: "online",
+ quickSuggestOnboardingDialogVariation: variation.name,
+ },
+ callback: async () => {
+ await doDialogTest({
+ callback: async () => {
+ info("Calling showOnboardingDialog");
+ const { win, maybeShowPromise } = await showOnboardingDialog({
+ skipIntroduction,
+ });
+
+ await callback(win, userAction, maybeShowPromise);
+ },
+ onboardingDialogVersion,
+ onboardingDialogChoice,
+ expectedUserBranchPrefs,
+ telemetryEvents,
+ });
+ },
+ });
+ }
+}
+
+async function doDialogTest({
+ callback,
+ onboardingDialogVersion,
+ onboardingDialogChoice,
+ telemetryEvents,
+ expectedUserBranchPrefs,
+}) {
+ setDialogPrereqPrefs();
+
+ // Set initial prefs on the default branch.
+ let initialDefaultBranch = OFFLINE_DEFAULT_PREFS;
+ let originalDefaultBranch = {};
+ for (let [name, value] of Object.entries(initialDefaultBranch)) {
+ originalDefaultBranch = gDefaultBranch.getBoolPref(name);
+ gDefaultBranch.setBoolPref(name, value);
+ gUserBranch.clearUserPref(name);
+ }
+
+ // Setting the prefs just now triggered telemetry events, so clear them
+ // before calling the callback.
+ Services.telemetry.clearEvents();
+
+ // Call the callback, which should trigger the dialog and interact with it.
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ await callback();
+ });
+
+ // Now check all pref values on the default and user branches.
+ for (let [name, value] of Object.entries(initialDefaultBranch)) {
+ Assert.equal(
+ gDefaultBranch.getBoolPref(name),
+ value,
+ "Default-branch value for pref did not change after modal: " + name
+ );
+
+ let effectiveValue;
+ if (name in expectedUserBranchPrefs) {
+ effectiveValue = expectedUserBranchPrefs[name];
+ Assert.equal(
+ gUserBranch.getBoolPref(name),
+ effectiveValue,
+ "User-branch value for pref has expected value: " + name
+ );
+ } else {
+ effectiveValue = value;
+ Assert.ok(
+ !gUserBranch.prefHasUserValue(name),
+ "User-branch value for pref does not exist: " + name
+ );
+ }
+
+ // For good measure, check the value returned by UrlbarPrefs.
+ Assert.equal(
+ UrlbarPrefs.get(name),
+ effectiveValue,
+ "Effective value for pref is correct: " + name
+ );
+ }
+
+ Assert.equal(
+ UrlbarPrefs.get("quicksuggest.onboardingDialogVersion"),
+ onboardingDialogVersion,
+ "onboardingDialogVersion"
+ );
+ Assert.equal(
+ UrlbarPrefs.get("quicksuggest.onboardingDialogChoice"),
+ onboardingDialogChoice,
+ "onboardingDialogChoice"
+ );
+ Assert.equal(
+ TelemetryEnvironment.currentEnvironment.settings.userPrefs[
+ "browser.urlbar.quicksuggest.onboardingDialogChoice"
+ ],
+ onboardingDialogChoice,
+ "onboardingDialogChoice is correct in TelemetryEnvironment"
+ );
+
+ QuickSuggestTestUtils.assertEvents(telemetryEvents);
+
+ Assert.ok(
+ UrlbarPrefs.get("quicksuggest.showedOnboardingDialog"),
+ "quicksuggest.showedOnboardingDialog is true after showing dialog"
+ );
+
+ // Clean up.
+ for (let [name, value] of Object.entries(originalDefaultBranch)) {
+ gDefaultBranch.setBoolPref(name, value);
+ }
+ for (let name of Object.keys(expectedUserBranchPrefs)) {
+ gUserBranch.clearUserPref(name);
+ }
+}
+
+/**
+ * Show onbaording dialog.
+ *
+ * @param {object} [options]
+ * The object options.
+ * @param {boolean} [options.skipIntroduction]
+ * If true, return dialog with skipping the introduction section.
+ * @returns {{ window, maybeShowPromise: Promise }}
+ * win: window object of the dialog.
+ * maybeShowPromise: Promise of QuickSuggest.maybeShowOnboardingDialog().
+ */
+async function showOnboardingDialog({ skipIntroduction } = {}) {
+ const dialogPromise = BrowserTestUtils.promiseAlertDialogOpen(
+ null,
+ QuickSuggest.ONBOARDING_URI,
+ { isSubDialog: true }
+ );
+
+ const maybeShowPromise = QuickSuggest.maybeShowOnboardingDialog();
+
+ const win = await dialogPromise;
+ if (win.document.readyState != "complete") {
+ await BrowserTestUtils.waitForEvent(win, "load");
+ }
+
+ // Wait until all listers on onboarding dialog are ready.
+ await window._quicksuggestOnboardingReady;
+
+ if (!skipIntroduction) {
+ return { win, maybeShowPromise };
+ }
+
+ // Trigger the transition by pressing Enter on the Next button.
+ EventUtils.synthesizeKey("KEY_Enter");
+
+ const introductionSection = win.document.getElementById(
+ "introduction-section"
+ );
+ const mainSection = win.document.getElementById("main-section");
+
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ BrowserTestUtils.is_hidden(introductionSection) &&
+ BrowserTestUtils.is_visible(mainSection)
+ );
+
+ return { win, maybeShowPromise };
+}
+
+/**
+ * Sets all the required prefs for showing the onboarding dialog except for the
+ * prefs that are set when the dialog is accepted.
+ */
+function setDialogPrereqPrefs() {
+ UrlbarPrefs.set("quicksuggest.shouldShowOnboardingDialog", true);
+ UrlbarPrefs.set("quicksuggest.showedOnboardingDialog", false);
+}
+
+/**
+ * This is a real hacky way of determining whether the tab key can move focus.
+ * Windows and Linux both support it but macOS does not unless full keyboard
+ * access is enabled, so practically this is only useful on macOS. Gecko seems
+ * to know whether full keyboard access is enabled because it affects focus in
+ * Firefox and some code in nsXULElement.cpp and other places mention it, but
+ * there doesn't seem to be a way to access that information from JS. There is
+ * `Services.focus.elementIsFocusable`, but it returns true regardless of
+ * whether full access is enabled.
+ *
+ * So what we do here is open the dialog and synthesize a tab key. If the focus
+ * doesn't change, then we assume moving the focus via the tab key is not
+ * supported.
+ *
+ * Why not just always skip the focus tasks on Mac? Because individual
+ * developers (like the one writing this comment) may be running macOS with full
+ * keyboard access enabled and want to actually run the tasks on their machines.
+ *
+ * @returns {boolean}
+ */
+async function canTabMoveFocus() {
+ if (AppConstants.platform != "macosx") {
+ return true;
+ }
+
+ let canMove = false;
+ await doDialogTest({
+ callback: async () => {
+ const { win, maybeShowPromise } = await showOnboardingDialog({
+ skipIntroduction: true,
+ });
+
+ let doc = win.document;
+ doc.getElementById("onboardingAccept").focus();
+ EventUtils.synthesizeKey("KEY_Tab");
+
+ // Whether or not the focus can move to the link.
+ canMove = doc.activeElement.id === "onboardingLearnMore";
+
+ EventUtils.synthesizeKey("KEY_Escape");
+ await maybeShowPromise;
+ },
+ onboardingDialogVersion: JSON.stringify({ version: 1 }),
+ onboardingDialogChoice: "dismiss_2",
+ expectedUserBranchPrefs: {
+ "quicksuggest.dataCollection.enabled": false,
+ },
+ telemetryEvents: [
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "data_collect_toggled",
+ object: "disabled",
+ },
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "opt_in_dialog",
+ object: "dismiss_2",
+ },
+ ],
+ });
+
+ return canMove;
+}
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_dynamicWikipedia.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_dynamicWikipedia.js
new file mode 100644
index 0000000000..ece6239953
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_dynamicWikipedia.js
@@ -0,0 +1,114 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This file tests primary telemetry for dynamic Wikipedia suggestions.
+ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ CONTEXTUAL_SERVICES_PING_TYPES:
+ "resource:///modules/PartnerLinkAttribution.sys.mjs",
+});
+
+const { TELEMETRY_SCALARS } = UrlbarProviderQuickSuggest;
+
+const MERINO_SUGGESTION = {
+ block_id: 1,
+ url: "https://example.com/dynamic-wikipedia",
+ title: "Dynamic Wikipedia suggestion",
+ click_url: "https://example.com/click",
+ impression_url: "https://example.com/impression",
+ advertiser: "dynamic-wikipedia",
+ provider: "wikipedia",
+ iab_category: "5 - Education",
+};
+
+const suggestion_type = "dynamic-wikipedia";
+const match_type = "firefox-suggest";
+const index = 1;
+const position = index + 1;
+
+add_setup(async function () {
+ await setUpTelemetryTest({
+ merinoSuggestions: [MERINO_SUGGESTION],
+ });
+});
+
+add_task(async function () {
+ await doTelemetryTest({
+ index,
+ suggestion: MERINO_SUGGESTION,
+ // impression-only
+ impressionOnly: {
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_DYNAMIC_WIKIPEDIA]: position,
+ },
+ event: {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "impression_only",
+ extra: {
+ suggestion_type,
+ match_type,
+ position: position.toString(),
+ },
+ },
+ },
+ selectables: {
+ // click
+ "urlbarView-row-inner": {
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_DYNAMIC_WIKIPEDIA]: position,
+ [TELEMETRY_SCALARS.CLICK_DYNAMIC_WIKIPEDIA]: position,
+ "urlbar.picked.dynamic_wikipedia": index.toString(),
+ },
+ event: {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "click",
+ extra: {
+ suggestion_type,
+ match_type,
+ position: position.toString(),
+ },
+ },
+ },
+ // block
+ "urlbarView-button-block": {
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_DYNAMIC_WIKIPEDIA]: position,
+ [TELEMETRY_SCALARS.BLOCK_DYNAMIC_WIKIPEDIA]: position,
+ },
+ event: {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "block",
+ extra: {
+ suggestion_type,
+ match_type,
+ position: position.toString(),
+ },
+ },
+ },
+ // help
+ "urlbarView-button-help": {
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_DYNAMIC_WIKIPEDIA]: position,
+ [TELEMETRY_SCALARS.HELP_DYNAMIC_WIKIPEDIA]: position,
+ },
+ event: {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "help",
+ extra: {
+ suggestion_type,
+ match_type,
+ position: position.toString(),
+ },
+ },
+ },
+ },
+ });
+});
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_impressionEdgeCases.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_impressionEdgeCases.js
new file mode 100644
index 0000000000..c52d22a886
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_impressionEdgeCases.js
@@ -0,0 +1,477 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This file tests abandonment and edge cases related to impressions.
+ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ CONTEXTUAL_SERVICES_PING_TYPES:
+ "resource:///modules/PartnerLinkAttribution.sys.mjs",
+ UrlbarView: "resource:///modules/UrlbarView.sys.mjs",
+ sinon: "resource://testing-common/Sinon.sys.mjs",
+});
+
+const { TELEMETRY_SCALARS } = UrlbarProviderQuickSuggest;
+
+const REMOTE_SETTINGS_RESULTS = [
+ {
+ id: 1,
+ url: "https://example.com/sponsored",
+ title: "Sponsored suggestion",
+ keywords: ["sponsored"],
+ click_url: "https://example.com/click",
+ impression_url: "https://example.com/impression",
+ advertiser: "testadvertiser",
+ },
+ {
+ id: 2,
+ url: "https://example.com/nonsponsored",
+ title: "Non-sponsored suggestion",
+ keywords: ["nonsponsored"],
+ click_url: "https://example.com/click",
+ impression_url: "https://example.com/impression",
+ advertiser: "testadvertiser",
+ iab_category: "5 - Education",
+ },
+];
+
+const SPONSORED_RESULT = REMOTE_SETTINGS_RESULTS[0];
+
+// Spy for the custom impression/click sender
+let spy;
+
+add_setup(async function () {
+ ({ spy } = QuickSuggestTestUtils.createTelemetryPingSpy());
+
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await UrlbarTestUtils.formHistory.clear();
+
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+
+ // Add a mock engine so we don't hit the network.
+ await SearchTestUtils.installSearchExtension({}, { setAsDefault: true });
+
+ await QuickSuggestTestUtils.ensureQuickSuggestInit({
+ remoteSettingsResults: [
+ {
+ type: "data",
+ attachment: REMOTE_SETTINGS_RESULTS,
+ },
+ ],
+ });
+});
+
+// Makes sure impression telemetry is not recorded when the urlbar engagement is
+// abandoned.
+add_task(async function abandonment() {
+ Services.telemetry.clearEvents();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "sponsored",
+ fireInputEvent: true,
+ });
+ await QuickSuggestTestUtils.assertIsQuickSuggest({
+ window,
+ url: SPONSORED_RESULT.url,
+ });
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ gURLBar.blur();
+ });
+ QuickSuggestTestUtils.assertScalars({});
+ QuickSuggestTestUtils.assertEvents([]);
+ QuickSuggestTestUtils.assertPings(spy, []);
+});
+
+// Makes sure impression telemetry is not recorded when a quick suggest result
+// is not present.
+add_task(async function noQuickSuggestResult() {
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ Services.telemetry.clearEvents();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "noImpression_noQuickSuggestResult",
+ fireInputEvent: true,
+ });
+ await QuickSuggestTestUtils.assertNoQuickSuggestResults(window);
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ EventUtils.synthesizeKey("KEY_Enter");
+ });
+ QuickSuggestTestUtils.assertScalars({});
+ QuickSuggestTestUtils.assertEvents([]);
+ QuickSuggestTestUtils.assertPings(spy, []);
+ });
+ await PlacesUtils.history.clear();
+});
+
+// When a quick suggest result is added to the view but hidden during the view
+// update, impression telemetry should not be recorded for it.
+add_task(async function hiddenRow() {
+ Services.telemetry.clearEvents();
+
+ // Increase the timeout of the remove-stale-rows timer so that it doesn't
+ // interfere with this task.
+ let originalRemoveStaleRowsTimeout = UrlbarView.removeStaleRowsTimeout;
+ UrlbarView.removeStaleRowsTimeout = 30000;
+ registerCleanupFunction(() => {
+ UrlbarView.removeStaleRowsTimeout = originalRemoveStaleRowsTimeout;
+ });
+
+ // Set up a test provider that doesn't add any results until we resolve its
+ // `finishQueryPromise`. For the first search below, it will add many search
+ // suggestions.
+ let maxCount = UrlbarPrefs.get("maxRichResults");
+ let results = [];
+ for (let i = 0; i < maxCount; i++) {
+ results.push(
+ new UrlbarResult(
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ {
+ engine: "Example",
+ suggestion: "suggestion " + i,
+ lowerCaseSuggestion: "suggestion " + i,
+ query: "test",
+ }
+ )
+ );
+ }
+ let provider = new DelayingTestProvider({ results });
+ UrlbarProvidersManager.registerProvider(provider);
+
+ // Open a new tab since we'll load a page below.
+ let tab = await BrowserTestUtils.openNewForegroundTab({ gBrowser });
+
+ // Do a normal search and allow the test provider to finish.
+ provider.finishQueryPromise = Promise.resolve();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "test",
+ fireInputEvent: true,
+ });
+
+ // Sanity check the rows. After the heuristic, the remaining rows should be
+ // the search results added by the test provider.
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ maxCount,
+ "Row count after first search"
+ );
+ for (let i = 1; i < maxCount; i++) {
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ Assert.equal(
+ result.type,
+ UrlbarUtils.RESULT_TYPE.SEARCH,
+ "Expected result type at index " + i
+ );
+ Assert.equal(
+ result.source,
+ UrlbarUtils.RESULT_SOURCE.SEARCH,
+ "Expected result source at index " + i
+ );
+ }
+
+ // Now set up a second search that triggers a quick suggest result. Add a
+ // mutation listener to the view so we can tell when the quick suggest row is
+ // added.
+ let mutationPromise = new Promise(resolve => {
+ let observer = new MutationObserver(mutations => {
+ let rows = UrlbarTestUtils.getResultsContainer(window).children;
+ for (let row of rows) {
+ if (row.result.providerName == "UrlbarProviderQuickSuggest") {
+ observer.disconnect();
+ resolve(row);
+ return;
+ }
+ }
+ });
+ observer.observe(UrlbarTestUtils.getResultsContainer(window), {
+ childList: true,
+ });
+ });
+
+ // Set the test provider's `finishQueryPromise` to a promise that doesn't
+ // resolve. That will prevent the search from completing, which will prevent
+ // the view from removing stale rows and showing the quick suggest row.
+ let resolveQuery;
+ provider.finishQueryPromise = new Promise(
+ resolve => (resolveQuery = resolve)
+ );
+
+ // Start the second search but don't wait for it to finish.
+ gURLBar.focus();
+ let queryPromise = UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: REMOTE_SETTINGS_RESULTS[0].keywords[0],
+ fireInputEvent: true,
+ });
+
+ // Wait for the quick suggest row to be added to the view. It should be hidden
+ // because (a) quick suggest results have a `suggestedIndex`, and rows with
+ // suggested indexes can't replace rows without suggested indexes, and (b) the
+ // view already contains the maximum number of rows due to the first search.
+ // It should remain hidden until the search completes or the remove-stale-rows
+ // timer fires. Next, we'll hit enter, which will cancel the search and close
+ // the view, so the row should never appear.
+ let quickSuggestRow = await mutationPromise;
+ Assert.ok(
+ BrowserTestUtils.is_hidden(quickSuggestRow),
+ "Quick suggest row is hidden"
+ );
+
+ // Hit enter to pick the heuristic search result. This will cancel the search
+ // and notify the quick suggest provider that an engagement occurred.
+ let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ EventUtils.synthesizeKey("KEY_Enter");
+ });
+ await loadPromise;
+
+ // Resolve the test provider's promise finally.
+ resolveQuery();
+ await queryPromise;
+
+ // The quick suggest provider added a result but it wasn't visible in the
+ // view. No impression telemetry should be recorded for it.
+ QuickSuggestTestUtils.assertScalars({});
+ QuickSuggestTestUtils.assertEvents([]);
+ QuickSuggestTestUtils.assertPings(spy, []);
+
+ BrowserTestUtils.removeTab(tab);
+ UrlbarProvidersManager.unregisterProvider(provider);
+ UrlbarView.removeStaleRowsTimeout = originalRemoveStaleRowsTimeout;
+});
+
+// When a quick suggest result has not been added to the view, impression
+// telemetry should not be recorded for it even if it's the result most recently
+// returned by the provider.
+add_task(async function notAddedToView() {
+ Services.telemetry.clearEvents();
+
+ // Open a new tab since we'll load a page.
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ // Do an initial search that doesn't match any suggestions to make sure
+ // there aren't any quick suggest results in the view to start.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "this doesn't match anything",
+ fireInputEvent: true,
+ });
+ await QuickSuggestTestUtils.assertNoQuickSuggestResults(window);
+ await UrlbarTestUtils.promisePopupClose(window);
+
+ // Now do a search for a suggestion and hit enter after the provider adds it
+ // but before it appears in the view.
+ await doEngagementWithoutAddingResultToView(
+ REMOTE_SETTINGS_RESULTS[0].keywords[0]
+ );
+
+ // The quick suggest provider added a result but it wasn't visible in the
+ // view, and no other quick suggest results were visible in the view. No
+ // impression telemetry should be recorded.
+ QuickSuggestTestUtils.assertScalars({});
+ QuickSuggestTestUtils.assertEvents([]);
+ QuickSuggestTestUtils.assertPings(spy, []);
+ });
+});
+
+// When a quick suggest result is visible in the view, impression telemetry
+// should be recorded for it even if it's not the result most recently returned
+// by the provider.
+add_task(async function previousResultStillVisible() {
+ Services.telemetry.clearEvents();
+
+ // Open a new tab since we'll load a page.
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ // Do a search for the first suggestion.
+ let firstSuggestion = REMOTE_SETTINGS_RESULTS[0];
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: firstSuggestion.keywords[0],
+ fireInputEvent: true,
+ });
+
+ let index = 1;
+ await QuickSuggestTestUtils.assertIsQuickSuggest({
+ window,
+ index,
+ url: firstSuggestion.url,
+ });
+
+ // Without closing the view, do a second search for the second suggestion
+ // and hit enter after the provider adds it but before it appears in the
+ // view.
+ await doEngagementWithoutAddingResultToView(
+ REMOTE_SETTINGS_RESULTS[1].keywords[0],
+ index
+ );
+
+ // An impression for the first suggestion should be recorded since it's
+ // still visible in the view, not the second suggestion.
+ QuickSuggestTestUtils.assertScalars({
+ [TELEMETRY_SCALARS.IMPRESSION_SPONSORED]: index + 1,
+ });
+ QuickSuggestTestUtils.assertEvents([
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "impression_only",
+ extra: {
+ match_type: "firefox-suggest",
+ position: String(index + 1),
+ suggestion_type: "sponsored",
+ },
+ },
+ ]);
+ QuickSuggestTestUtils.assertPings(spy, [
+ {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION,
+ payload: {
+ improve_suggest_experience_checked: false,
+ block_id: firstSuggestion.id,
+ is_clicked: false,
+ match_type: "firefox-suggest",
+ position: index + 1,
+ },
+ },
+ ]);
+ });
+});
+
+/**
+ * Does a search that causes the quick suggest provider to return a result
+ * without adding it to the view and then hits enter to load a SERP and create
+ * an engagement.
+ *
+ * @param {string} searchString
+ * The search string.
+ * @param {number} previousResultIndex
+ * If the view is already open and showing a quick suggest result, pass its
+ * index here. Otherwise pass -1.
+ */
+async function doEngagementWithoutAddingResultToView(
+ searchString,
+ previousResultIndex = -1
+) {
+ // Set the timeout of the chunk timer to a really high value so that it will
+ // not fire. The view updates when the timer fires, which we specifically want
+ // to avoid here.
+ let originalChunkDelayMs = UrlbarProvidersManager._chunkResultsDelayMs;
+ UrlbarProvidersManager._chunkResultsDelayMs = 30000;
+ registerCleanupFunction(() => {
+ UrlbarProvidersManager._chunkResultsDelayMs = originalChunkDelayMs;
+ });
+
+ // Stub `UrlbarProviderQuickSuggest.getPriority()` to return Infinity.
+ let sandbox = sinon.createSandbox();
+ let getPriorityStub = sandbox.stub(UrlbarProviderQuickSuggest, "getPriority");
+ getPriorityStub.returns(Infinity);
+
+ // Spy on `UrlbarProviderQuickSuggest.onEngagement()`.
+ let onEngagementSpy = sandbox.spy(UrlbarProviderQuickSuggest, "onEngagement");
+
+ let sandboxCleanup = () => {
+ getPriorityStub?.restore();
+ getPriorityStub = null;
+ sandbox?.restore();
+ sandbox = null;
+ };
+ registerCleanupFunction(sandboxCleanup);
+
+ // In addition to setting the chunk timeout to a large value above, in order
+ // to prevent the view from updating there also needs to be a heuristic
+ // provider that takes a long time to add results. Set one up that doesn't add
+ // any results until we resolve its `finishQueryPromise`. Set its priority to
+ // Infinity too so that only it and the quick suggest provider will be active.
+ let provider = new DelayingTestProvider({
+ results: [],
+ priority: Infinity,
+ type: UrlbarUtils.PROVIDER_TYPE.HEURISTIC,
+ });
+ UrlbarProvidersManager.registerProvider(provider);
+
+ let resolveQuery;
+ provider.finishQueryPromise = new Promise(r => (resolveQuery = r));
+
+ // Add a query listener so we can grab the query context.
+ let context;
+ let queryListener = {
+ onQueryStarted: c => (context = c),
+ };
+ gURLBar.controller.addQueryListener(queryListener);
+
+ // Do a search but don't wait for it to finish.
+ gURLBar.focus();
+ UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: searchString,
+ fireInputEvent: true,
+ });
+
+ // Wait for the quick suggest provider to add its result to `context.unsortedResults`.
+ let result = await TestUtils.waitForCondition(() => {
+ let query = UrlbarProvidersManager.queries.get(context);
+ return query?.unsortedResults.find(
+ r => r.providerName == "UrlbarProviderQuickSuggest"
+ );
+ }, "Waiting for quick suggest result to be added to context.unsortedResults");
+
+ gURLBar.controller.removeQueryListener(queryListener);
+
+ // The view should not have updated, so the result's `rowIndex` should still
+ // have its initial value of -1.
+ Assert.equal(result.rowIndex, -1, "result.rowIndex is still -1");
+
+ // If there's a result from the previous query, assert it's still in the
+ // view. Otherwise assume that the view should be closed. These are mostly
+ // sanity checks because they should only fail if the telemetry assertions
+ // below also fail.
+ if (previousResultIndex >= 0) {
+ let rows = gURLBar.view.panel.querySelector(".urlbarView-results");
+ Assert.equal(
+ rows.children[previousResultIndex].result.providerName,
+ "UrlbarProviderQuickSuggest",
+ "Result already in view is a quick suggest"
+ );
+ } else {
+ Assert.ok(!gURLBar.view.isOpen, "View is closed");
+ }
+
+ // Hit enter to load a SERP for the search string. This should notify the
+ // quick suggest provider that an engagement occurred.
+ let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promisePopupClose(window, () => {
+ EventUtils.synthesizeKey("KEY_Enter");
+ });
+ await loadPromise;
+
+ let engagementCalls = onEngagementSpy.getCalls().filter(call => {
+ let state = call.args[1];
+ return state == "engagement";
+ });
+ Assert.equal(engagementCalls.length, 1, "One engagement occurred");
+
+ // Clean up.
+ resolveQuery();
+ UrlbarProvidersManager.unregisterProvider(provider);
+ UrlbarProvidersManager._chunkResultsDelayMs = originalChunkDelayMs;
+ sandboxCleanup();
+}
+
+/**
+ * A test provider that doesn't finish `startQuery()` until `finishQueryPromise`
+ * is resolved.
+ */
+class DelayingTestProvider extends UrlbarTestUtils.TestProvider {
+ finishQueryPromise = null;
+ async startQuery(context, addCallback) {
+ for (let result of this._results) {
+ addCallback(this, result);
+ }
+ await this.finishQueryPromise;
+ }
+}
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_navigationalSuggestions.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_navigationalSuggestions.js
new file mode 100644
index 0000000000..defb9bb76d
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_navigationalSuggestions.js
@@ -0,0 +1,353 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This file tests primary telemetry for navigational suggestions, a.k.a.
+ * navigational top picks.
+ */
+
+"use strict";
+
+const { TELEMETRY_SCALARS } = UrlbarProviderQuickSuggest;
+
+const MERINO_SUGGESTION = {
+ title: "Navigational suggestion",
+ url: "https://example.com/navigational-suggestion",
+ provider: "top_picks",
+ is_sponsored: false,
+ score: 0.25,
+ block_id: 0,
+ is_top_pick: true,
+};
+
+const suggestion_type = "navigational";
+const index = 1;
+const position = index + 1;
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // `bestMatch.enabled` must be set to show nav suggestions with the best
+ // match UI treatment.
+ ["browser.urlbar.bestMatch.enabled", true],
+ // Disable tab-to-search since like best match it's also shown with
+ // `suggestedIndex` = 1.
+ ["browser.urlbar.suggest.engines", false],
+ ],
+ });
+
+ await setUpTelemetryTest({
+ merinoSuggestions: [MERINO_SUGGESTION],
+ });
+});
+
+// Clicks the heuristic when a nav suggestion is not matched
+add_task(async function notMatched_clickHeuristic() {
+ await doTest({
+ suggestion: null,
+ shouldBeShown: false,
+ pickRowIndex: 0,
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_NAV_NOTMATCHED]: "search_engine",
+ [TELEMETRY_SCALARS.CLICK_NAV_NOTMATCHED]: "search_engine",
+ },
+ events: [],
+ });
+});
+
+// Clicks a non-heuristic row when a nav suggestion is not matched
+add_task(async function notMatched_clickOther() {
+ await PlacesTestUtils.addVisits("http://mochi.test:8888/example");
+ await doTest({
+ suggestion: null,
+ shouldBeShown: false,
+ pickRowIndex: 1,
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_NAV_NOTMATCHED]: "search_engine",
+ },
+ events: [],
+ });
+});
+
+// Clicks the heuristic when a nav suggestion is shown
+add_task(async function shown_clickHeuristic() {
+ await doTest({
+ suggestion: MERINO_SUGGESTION,
+ shouldBeShown: true,
+ pickRowIndex: 0,
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_NAV_SHOWN]: "search_engine",
+ [TELEMETRY_SCALARS.CLICK_NAV_SHOWN_HEURISTIC]: "search_engine",
+ },
+ events: [
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "impression_only",
+ extra: {
+ suggestion_type,
+ match_type: "best-match",
+ position: position.toString(),
+ source: "merino",
+ },
+ },
+ ],
+ });
+});
+
+// Clicks the nav suggestion
+add_task(async function shown_clickNavSuggestion() {
+ await doTest({
+ suggestion: MERINO_SUGGESTION,
+ shouldBeShown: true,
+ pickRowIndex: index,
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_NAV_SHOWN]: "search_engine",
+ [TELEMETRY_SCALARS.CLICK_NAV_SHOWN_NAV]: "search_engine",
+ "urlbar.picked.navigational": "1",
+ },
+ events: [
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "click",
+ extra: {
+ suggestion_type,
+ match_type: "best-match",
+ position: position.toString(),
+ source: "merino",
+ },
+ },
+ ],
+ });
+});
+
+// Clicks a non-heuristic non-nav-suggestion row when the nav suggestion is
+// shown
+add_task(async function shown_clickOther() {
+ await PlacesTestUtils.addVisits("http://mochi.test:8888/example");
+ await doTest({
+ suggestion: MERINO_SUGGESTION,
+ shouldBeShown: true,
+ pickRowIndex: 2,
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_NAV_SHOWN]: "search_engine",
+ },
+ events: [
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "impression_only",
+ extra: {
+ suggestion_type,
+ match_type: "best-match",
+ position: position.toString(),
+ source: "merino",
+ },
+ },
+ ],
+ });
+});
+
+// Clicks the heuristic when it dupes the nav suggestion
+add_task(async function duped_clickHeuristic() {
+ // Add enough visits to example.com so it autofills.
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits("https://example.com/");
+ }
+
+ // Set the nav suggestion's URL to the same URL, example.com.
+ let suggestion = {
+ ...MERINO_SUGGESTION,
+ url: "https://example.com/",
+ };
+
+ await doTest({
+ suggestion,
+ shouldBeShown: false,
+ pickRowIndex: 0,
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_NAV_SUPERCEDED]: "autofill_origin",
+ [TELEMETRY_SCALARS.CLICK_NAV_SUPERCEDED]: "autofill_origin",
+ },
+ events: [],
+ });
+});
+
+// Clicks a non-heuristic row when the heuristic dupes the nav suggestion
+add_task(async function duped_clickOther() {
+ // Add enough visits to example.com so it autofills.
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits("https://example.com/");
+ }
+
+ // Set the nav suggestion's URL to the same URL, example.com.
+ let suggestion = {
+ ...MERINO_SUGGESTION,
+ url: "https://example.com/",
+ };
+
+ // Add a visit to another URL so it appears in the search below.
+ await PlacesTestUtils.addVisits("https://example.com/some-other-url");
+
+ await doTest({
+ suggestion,
+ shouldBeShown: false,
+ pickRowIndex: 1,
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_NAV_SUPERCEDED]: "autofill_origin",
+ },
+ events: [],
+ });
+});
+
+// Telemetry specific to nav suggestions should not be recorded when the
+// `recordNavigationalSuggestionTelemetry` Nimbus variable is false.
+add_task(async function recordNavigationalSuggestionTelemetry_false() {
+ await doTest({
+ valueOverrides: {
+ recordNavigationalSuggestionTelemetry: false,
+ },
+ suggestion: MERINO_SUGGESTION,
+ shouldBeShown: true,
+ pickRowIndex: index,
+ scalars: {},
+ events: [
+ // The legacy engagement event should still be recorded as it is for all
+ // quick suggest results.
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "click",
+ extra: {
+ suggestion_type,
+ match_type: "best-match",
+ position: position.toString(),
+ source: "merino",
+ },
+ },
+ ],
+ });
+});
+
+// Telemetry specific to nav suggestions should not be recorded when the
+// `recordNavigationalSuggestionTelemetry` Nimbus variable is left out.
+add_task(async function recordNavigationalSuggestionTelemetry_undefined() {
+ await doTest({
+ valueOverrides: {},
+ suggestion: MERINO_SUGGESTION,
+ shouldBeShown: true,
+ pickRowIndex: index,
+ scalars: {},
+ events: [
+ // The legacy engagement event should still be recorded as it is for all
+ // quick suggest results.
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "click",
+ extra: {
+ suggestion_type,
+ match_type: "best-match",
+ position: position.toString(),
+ source: "merino",
+ },
+ },
+ ],
+ });
+});
+
+/**
+ * Does the following:
+ *
+ * 1. Sets up a Merino nav suggestion
+ * 2. Enrolls in a Nimbus experiment with the specified variables
+ * 3. Does a search
+ * 4. Makes sure the nav suggestion is or isn't shown as expected
+ * 5. Clicks a specified row
+ * 6. Makes sure the expected telemetry is recorded
+ *
+ * @param {object} options
+ * Options object
+ * @param {object} options.suggestion
+ * The nav suggestion or null if Merino shouldn't serve one.
+ * @param {boolean} options.shouldBeShown
+ * Whether the nav suggestion is expected to be shown.
+ * @param {number} options.pickRowIndex
+ * The index of the row to pick.
+ * @param {object} options.scalars
+ * An object that specifies the nav suggest keyed scalars that are expected to
+ * be recorded.
+ * @param {Array} options.events
+ * An object that specifies the legacy engagement events that are expected to
+ * be recorded.
+ * @param {object} options.valueOverrides
+ * The Nimbus variables to use.
+ */
+async function doTest({
+ suggestion,
+ shouldBeShown,
+ pickRowIndex,
+ scalars,
+ events,
+ valueOverrides = {
+ recordNavigationalSuggestionTelemetry: true,
+ },
+}) {
+ MerinoTestUtils.server.response.body.suggestions = suggestion
+ ? [suggestion]
+ : [];
+
+ Services.telemetry.clearEvents();
+ let { spy, spyCleanup } = QuickSuggestTestUtils.createTelemetryPingSpy();
+
+ await QuickSuggestTestUtils.withExperiment({
+ valueOverrides,
+ callback: async () => {
+ await BrowserTestUtils.withNewTab("about:blank", async () => {
+ gURLBar.focus();
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "example",
+ fireInputEvent: true,
+ });
+
+ if (shouldBeShown) {
+ await QuickSuggestTestUtils.assertIsQuickSuggest({
+ window,
+ index,
+ url: suggestion.url,
+ isBestMatch: true,
+ isSponsored: false,
+ });
+ } else {
+ await QuickSuggestTestUtils.assertNoQuickSuggestResults(window);
+ }
+
+ let loadPromise = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser
+ );
+ if (pickRowIndex > 0) {
+ info("Arrowing down to row index " + pickRowIndex);
+ EventUtils.synthesizeKey("KEY_ArrowDown", { repeat: pickRowIndex });
+ }
+ info("Pressing Enter and waiting for page load");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await loadPromise;
+ });
+ },
+ });
+
+ info("Checking scalars");
+ QuickSuggestTestUtils.assertScalars(scalars);
+
+ info("Checking events");
+ QuickSuggestTestUtils.assertEvents(events);
+
+ info("Checking pings");
+ QuickSuggestTestUtils.assertPings(spy, []);
+
+ await spyCleanup();
+ await PlacesUtils.history.clear();
+ MerinoTestUtils.server.response.body.suggestions = [MERINO_SUGGESTION];
+}
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_nonsponsored.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_nonsponsored.js
new file mode 100644
index 0000000000..c7ddcdd2ce
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_nonsponsored.js
@@ -0,0 +1,368 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This file tests primary telemetry for nonsponsored suggestions.
+ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ CONTEXTUAL_SERVICES_PING_TYPES:
+ "resource:///modules/PartnerLinkAttribution.sys.mjs",
+});
+
+const { TELEMETRY_SCALARS } = UrlbarProviderQuickSuggest;
+
+const REMOTE_SETTINGS_RESULT = {
+ id: 1,
+ url: "https://example.com/nonsponsored",
+ title: "Non-sponsored suggestion",
+ keywords: ["nonsponsored"],
+ click_url: "https://example.com/click",
+ impression_url: "https://example.com/impression",
+ advertiser: "testadvertiser",
+ iab_category: "5 - Education",
+};
+
+const suggestion_type = "nonsponsored";
+const index = 1;
+const position = index + 1;
+
+add_setup(async function () {
+ await setUpTelemetryTest({
+ remoteSettingsResults: [
+ {
+ type: "data",
+ attachment: [REMOTE_SETTINGS_RESULT],
+ },
+ ],
+ });
+});
+
+// nonsponsored
+add_task(async function nonsponsored() {
+ let match_type = "firefox-suggest";
+
+ // Make sure `improve_suggest_experience_checked` is recorded correctly
+ // depending on the value of the related pref.
+ for (let improve_suggest_experience_checked of [false, true]) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "browser.urlbar.quicksuggest.dataCollection.enabled",
+ improve_suggest_experience_checked,
+ ],
+ ],
+ });
+ await doTelemetryTest({
+ index,
+ suggestion: REMOTE_SETTINGS_RESULT,
+ // impression-only
+ impressionOnly: {
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_NONSPONSORED]: position,
+ },
+ event: {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "impression_only",
+ extra: {
+ suggestion_type,
+ match_type,
+ position: position.toString(),
+ },
+ },
+ ping: {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION,
+ payload: {
+ match_type,
+ position,
+ improve_suggest_experience_checked,
+ is_clicked: false,
+ block_id: REMOTE_SETTINGS_RESULT.id,
+ advertiser: REMOTE_SETTINGS_RESULT.advertiser,
+ },
+ },
+ },
+ selectables: {
+ // click
+ "urlbarView-row-inner": {
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_NONSPONSORED]: position,
+ [TELEMETRY_SCALARS.CLICK_NONSPONSORED]: position,
+ },
+ event: {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "click",
+ extra: {
+ suggestion_type,
+ match_type,
+ position: position.toString(),
+ },
+ },
+ pings: [
+ {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION,
+ payload: {
+ match_type,
+ position,
+ improve_suggest_experience_checked,
+ is_clicked: true,
+ block_id: REMOTE_SETTINGS_RESULT.id,
+ advertiser: REMOTE_SETTINGS_RESULT.advertiser,
+ },
+ },
+ {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_SELECTION,
+ payload: {
+ match_type,
+ position,
+ improve_suggest_experience_checked,
+ block_id: REMOTE_SETTINGS_RESULT.id,
+ advertiser: REMOTE_SETTINGS_RESULT.advertiser,
+ },
+ },
+ ],
+ },
+ // block
+ "urlbarView-button-block": {
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_NONSPONSORED]: position,
+ [TELEMETRY_SCALARS.BLOCK_NONSPONSORED]: position,
+ },
+ event: {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "block",
+ extra: {
+ suggestion_type,
+ match_type,
+ position: position.toString(),
+ },
+ },
+ pings: [
+ {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION,
+ payload: {
+ match_type,
+ position,
+ improve_suggest_experience_checked,
+ is_clicked: false,
+ block_id: REMOTE_SETTINGS_RESULT.id,
+ advertiser: REMOTE_SETTINGS_RESULT.advertiser,
+ },
+ },
+ {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_BLOCK,
+ payload: {
+ match_type,
+ position,
+ improve_suggest_experience_checked,
+ block_id: REMOTE_SETTINGS_RESULT.id,
+ advertiser: REMOTE_SETTINGS_RESULT.advertiser,
+ iab_category: REMOTE_SETTINGS_RESULT.iab_category,
+ },
+ },
+ ],
+ },
+ // help
+ "urlbarView-button-help": {
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_NONSPONSORED]: position,
+ [TELEMETRY_SCALARS.HELP_NONSPONSORED]: position,
+ },
+ event: {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "help",
+ extra: {
+ suggestion_type,
+ match_type,
+ position: position.toString(),
+ },
+ },
+ pings: [
+ {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION,
+ payload: {
+ match_type,
+ position,
+ improve_suggest_experience_checked,
+ is_clicked: false,
+ block_id: REMOTE_SETTINGS_RESULT.id,
+ advertiser: REMOTE_SETTINGS_RESULT.advertiser,
+ },
+ },
+ ],
+ },
+ },
+ });
+ await SpecialPowers.popPrefEnv();
+ }
+});
+
+// nonsponsored best match
+add_task(async function nonsponsoredBestMatch() {
+ let match_type = "best-match";
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.bestMatch.enabled", true]],
+ });
+ await QuickSuggestTestUtils.setConfig(
+ QuickSuggestTestUtils.BEST_MATCH_CONFIG
+ );
+ await doTelemetryTest({
+ index,
+ suggestion: REMOTE_SETTINGS_RESULT,
+ // impression-only
+ impressionOnly: {
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_NONSPONSORED]: position,
+ [TELEMETRY_SCALARS.IMPRESSION_NONSPONSORED_BEST_MATCH]: position,
+ },
+ event: {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "impression_only",
+ extra: {
+ suggestion_type,
+ match_type,
+ position: position.toString(),
+ },
+ },
+ ping: {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION,
+ payload: {
+ match_type,
+ position,
+ is_clicked: false,
+ improve_suggest_experience_checked: false,
+ block_id: REMOTE_SETTINGS_RESULT.id,
+ advertiser: REMOTE_SETTINGS_RESULT.advertiser,
+ },
+ },
+ },
+ selectables: {
+ // click
+ "urlbarView-row-inner": {
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_NONSPONSORED]: position,
+ [TELEMETRY_SCALARS.IMPRESSION_NONSPONSORED_BEST_MATCH]: position,
+ [TELEMETRY_SCALARS.CLICK_NONSPONSORED]: position,
+ [TELEMETRY_SCALARS.CLICK_NONSPONSORED_BEST_MATCH]: position,
+ },
+ event: {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "click",
+ extra: {
+ suggestion_type,
+ match_type,
+ position: position.toString(),
+ },
+ },
+ pings: [
+ {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION,
+ payload: {
+ match_type,
+ position,
+ is_clicked: true,
+ improve_suggest_experience_checked: false,
+ block_id: REMOTE_SETTINGS_RESULT.id,
+ advertiser: REMOTE_SETTINGS_RESULT.advertiser,
+ },
+ },
+ {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_SELECTION,
+ payload: {
+ match_type,
+ position,
+ improve_suggest_experience_checked: false,
+ block_id: REMOTE_SETTINGS_RESULT.id,
+ advertiser: REMOTE_SETTINGS_RESULT.advertiser,
+ },
+ },
+ ],
+ },
+ // block
+ "urlbarView-button-block": {
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_NONSPONSORED]: position,
+ [TELEMETRY_SCALARS.IMPRESSION_NONSPONSORED_BEST_MATCH]: position,
+ [TELEMETRY_SCALARS.BLOCK_NONSPONSORED]: position,
+ [TELEMETRY_SCALARS.BLOCK_NONSPONSORED_BEST_MATCH]: position,
+ },
+ event: {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "block",
+ extra: {
+ suggestion_type,
+ match_type,
+ position: position.toString(),
+ },
+ },
+ pings: [
+ {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION,
+ payload: {
+ match_type,
+ position,
+ is_clicked: false,
+ improve_suggest_experience_checked: false,
+ block_id: REMOTE_SETTINGS_RESULT.id,
+ advertiser: REMOTE_SETTINGS_RESULT.advertiser,
+ },
+ },
+ {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_BLOCK,
+ payload: {
+ match_type,
+ position,
+ improve_suggest_experience_checked: false,
+ block_id: REMOTE_SETTINGS_RESULT.id,
+ advertiser: REMOTE_SETTINGS_RESULT.advertiser,
+ iab_category: REMOTE_SETTINGS_RESULT.iab_category,
+ },
+ },
+ ],
+ },
+ // help
+ "urlbarView-button-help": {
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_NONSPONSORED]: position,
+ [TELEMETRY_SCALARS.IMPRESSION_NONSPONSORED_BEST_MATCH]: position,
+ [TELEMETRY_SCALARS.HELP_NONSPONSORED]: position,
+ [TELEMETRY_SCALARS.HELP_NONSPONSORED_BEST_MATCH]: position,
+ },
+ event: {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "help",
+ extra: {
+ suggestion_type,
+ match_type,
+ position: position.toString(),
+ },
+ },
+ pings: [
+ {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION,
+ payload: {
+ match_type,
+ position,
+ is_clicked: false,
+ improve_suggest_experience_checked: false,
+ block_id: REMOTE_SETTINGS_RESULT.id,
+ advertiser: REMOTE_SETTINGS_RESULT.advertiser,
+ },
+ },
+ ],
+ },
+ },
+ });
+ await QuickSuggestTestUtils.setConfig(QuickSuggestTestUtils.DEFAULT_CONFIG);
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_other.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_other.js
new file mode 100644
index 0000000000..d151ca81ad
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_other.js
@@ -0,0 +1,409 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This file tests ancillary quick suggest telemetry, i.e., telemetry that's not
+ * strongly related to showing suggestions in the urlbar.
+ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.sys.mjs",
+});
+
+const REMOTE_SETTINGS_RESULTS = [
+ {
+ id: 1,
+ url: "https://example.com/sponsored",
+ title: "Sponsored suggestion",
+ keywords: ["sponsored"],
+ click_url: "https://example.com/click",
+ impression_url: "https://example.com/impression",
+ advertiser: "testadvertiser",
+ },
+];
+
+add_setup(async function () {
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await UrlbarTestUtils.formHistory.clear();
+
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+
+ // Add a mock engine so we don't hit the network.
+ await SearchTestUtils.installSearchExtension({}, { setAsDefault: true });
+
+ await QuickSuggestTestUtils.ensureQuickSuggestInit({
+ remoteSettingsResults: [
+ {
+ type: "data",
+ attachment: REMOTE_SETTINGS_RESULTS,
+ },
+ ],
+ });
+});
+
+// Tests telemetry recorded when toggling the
+// `suggest.quicksuggest.nonsponsored` pref:
+// * contextservices.quicksuggest enable_toggled event telemetry
+// * TelemetryEnvironment
+add_task(async function enableToggled() {
+ Services.telemetry.clearEvents();
+
+ // Toggle the suggest.quicksuggest.nonsponsored pref twice. We should get two
+ // events.
+ let enabled = UrlbarPrefs.get("suggest.quicksuggest.nonsponsored");
+ for (let i = 0; i < 2; i++) {
+ enabled = !enabled;
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", enabled);
+ QuickSuggestTestUtils.assertEvents([
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "enable_toggled",
+ object: enabled ? "enabled" : "disabled",
+ },
+ ]);
+ Assert.equal(
+ TelemetryEnvironment.currentEnvironment.settings.userPrefs[
+ "browser.urlbar.suggest.quicksuggest.nonsponsored"
+ ],
+ enabled,
+ "suggest.quicksuggest.nonsponsored is correct in TelemetryEnvironment"
+ );
+ }
+
+ // Set the main quicksuggest.enabled pref to false and toggle the
+ // suggest.quicksuggest.nonsponsored pref again. We shouldn't get any events.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.quicksuggest.enabled", false]],
+ });
+ enabled = !enabled;
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", enabled);
+ QuickSuggestTestUtils.assertEvents([]);
+ await SpecialPowers.popPrefEnv();
+
+ // Set the pref back to what it was at the start of the task.
+ UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", !enabled);
+});
+
+// Tests telemetry recorded when toggling the `suggest.quicksuggest.sponsored`
+// pref:
+// * contextservices.quicksuggest enable_toggled event telemetry
+// * TelemetryEnvironment
+add_task(async function sponsoredToggled() {
+ Services.telemetry.clearEvents();
+
+ // Toggle the suggest.quicksuggest.sponsored pref twice. We should get two
+ // events.
+ let enabled = UrlbarPrefs.get("suggest.quicksuggest.sponsored");
+ for (let i = 0; i < 2; i++) {
+ enabled = !enabled;
+ UrlbarPrefs.set("suggest.quicksuggest.sponsored", enabled);
+ QuickSuggestTestUtils.assertEvents([
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "sponsored_toggled",
+ object: enabled ? "enabled" : "disabled",
+ },
+ ]);
+ Assert.equal(
+ TelemetryEnvironment.currentEnvironment.settings.userPrefs[
+ "browser.urlbar.suggest.quicksuggest.sponsored"
+ ],
+ enabled,
+ "suggest.quicksuggest.sponsored is correct in TelemetryEnvironment"
+ );
+ }
+
+ // Set the main quicksuggest.enabled pref to false and toggle the
+ // suggest.quicksuggest.sponsored pref again. We shouldn't get any events.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.quicksuggest.enabled", false]],
+ });
+ enabled = !enabled;
+ UrlbarPrefs.set("suggest.quicksuggest.sponsored", enabled);
+ QuickSuggestTestUtils.assertEvents([]);
+ await SpecialPowers.popPrefEnv();
+
+ // Set the pref back to what it was at the start of the task.
+ UrlbarPrefs.set("suggest.quicksuggest.sponsored", !enabled);
+});
+
+// Tests telemetry recorded when toggling the
+// `quicksuggest.dataCollection.enabled` pref:
+// * contextservices.quicksuggest data_collect_toggled event telemetry
+// * TelemetryEnvironment
+add_task(async function dataCollectionToggled() {
+ Services.telemetry.clearEvents();
+
+ // Toggle the quicksuggest.dataCollection.enabled pref twice. We should get
+ // two events.
+ let enabled = UrlbarPrefs.get("quicksuggest.dataCollection.enabled");
+ for (let i = 0; i < 2; i++) {
+ enabled = !enabled;
+ UrlbarPrefs.set("quicksuggest.dataCollection.enabled", enabled);
+ QuickSuggestTestUtils.assertEvents([
+ {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "data_collect_toggled",
+ object: enabled ? "enabled" : "disabled",
+ },
+ ]);
+ Assert.equal(
+ TelemetryEnvironment.currentEnvironment.settings.userPrefs[
+ "browser.urlbar.quicksuggest.dataCollection.enabled"
+ ],
+ enabled,
+ "quicksuggest.dataCollection.enabled is correct in TelemetryEnvironment"
+ );
+ }
+
+ // Set the main quicksuggest.enabled pref to false and toggle the data
+ // collection pref again. We shouldn't get any events.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.quicksuggest.enabled", false]],
+ });
+ enabled = !enabled;
+ UrlbarPrefs.set("quicksuggest.dataCollection.enabled", enabled);
+ QuickSuggestTestUtils.assertEvents([]);
+ await SpecialPowers.popPrefEnv();
+
+ // Set the pref back to what it was at the start of the task.
+ UrlbarPrefs.set("quicksuggest.dataCollection.enabled", !enabled);
+});
+
+// Tests telemetry recorded when clicking the checkbox for best match in
+// preferences UI. The telemetry will be stored as following keyed scalar.
+// scalar: browser.ui.interaction.preferences_panePrivacy
+// key: firefoxSuggestBestMatch
+add_task(async function bestmatchCheckbox() {
+ // Set the initial enabled status.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.bestMatch.enabled", true]],
+ });
+
+ // Open preferences page for best match.
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:preferences#privacy",
+ true
+ );
+
+ for (let i = 0; i < 2; i++) {
+ Services.telemetry.clearScalars();
+
+ // Click on the checkbox.
+ const doc = gBrowser.selectedBrowser.contentDocument;
+ const checkboxId = "firefoxSuggestBestMatch";
+ const checkbox = doc.getElementById(checkboxId);
+ checkbox.scrollIntoView();
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#" + checkboxId,
+ {},
+ gBrowser.selectedBrowser
+ );
+
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "browser.ui.interaction.preferences_panePrivacy",
+ checkboxId,
+ 1
+ );
+ }
+
+ // Clean up.
+ gBrowser.removeCurrentTab();
+ await SpecialPowers.popPrefEnv();
+});
+
+// Tests telemetry recorded when opening the learn more link for best match in
+// the preferences UI. The telemetry will be stored as following keyed scalar.
+// scalar: browser.ui.interaction.preferences_panePrivacy
+// key: firefoxSuggestBestMatchLearnMore
+add_task(async function bestmatchLearnMore() {
+ // Set the initial enabled status.
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.bestMatch.enabled", true]],
+ });
+
+ // Open preferences page for best match.
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:preferences#privacy",
+ true
+ );
+
+ // Click on the learn more link.
+ Services.telemetry.clearScalars();
+ const learnMoreLinkId = "firefoxSuggestBestMatchLearnMore";
+ const doc = gBrowser.selectedBrowser.contentDocument;
+ const link = doc.getElementById(learnMoreLinkId);
+ link.scrollIntoView();
+ const onLearnMoreOpenedByClick = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ QuickSuggest.HELP_URL
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#" + learnMoreLinkId,
+ {},
+ gBrowser.selectedBrowser
+ );
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "browser.ui.interaction.preferences_panePrivacy",
+ "firefoxSuggestBestMatchLearnMore",
+ 1
+ );
+ await onLearnMoreOpenedByClick;
+ gBrowser.removeCurrentTab();
+
+ // Type enter key on the learm more link.
+ Services.telemetry.clearScalars();
+ link.focus();
+ const onLearnMoreOpenedByKey = BrowserTestUtils.waitForNewTab(
+ gBrowser,
+ QuickSuggest.HELP_URL
+ );
+ await BrowserTestUtils.synthesizeKey(
+ "KEY_Enter",
+ {},
+ gBrowser.selectedBrowser
+ );
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "browser.ui.interaction.preferences_panePrivacy",
+ "firefoxSuggestBestMatchLearnMore",
+ 1
+ );
+ await onLearnMoreOpenedByKey;
+ gBrowser.removeCurrentTab();
+
+ // Clean up.
+ gBrowser.removeCurrentTab();
+ await SpecialPowers.popPrefEnv();
+});
+
+// Simulates the race on startup between telemetry environment initialization
+// and the initial update of the Suggest scenario. After startup is done,
+// telemetry environment should record the correct values for startup prefs.
+add_task(async function telemetryEnvironmentOnStartup() {
+ await QuickSuggestTestUtils.setScenario(null);
+
+ // Restart telemetry environment so we know it's watching its default set of
+ // prefs.
+ await TelemetryEnvironment.testCleanRestart().onInitialized();
+
+ // Get the prefs that UrlbarPrefs sets when the Suggest scenario is updated on
+ // startup. They're the union of the prefs exposed in the UI and the prefs
+ // that are set on the default branch per scenario.
+ let prefs = [
+ ...new Set([
+ ...Object.values(UrlbarPrefs.FIREFOX_SUGGEST_UI_PREFS_BY_VARIABLE),
+ ...Object.values(UrlbarPrefs.FIREFOX_SUGGEST_DEFAULT_PREFS)
+ .map(valuesByPrefName => Object.keys(valuesByPrefName))
+ .flat(),
+ ]),
+ ];
+
+ // Not all of the prefs are recorded in telemetry environment. Filter in the
+ // ones that are.
+ prefs = prefs.filter(
+ p =>
+ `browser.urlbar.${p}` in
+ TelemetryEnvironment.currentEnvironment.settings.userPrefs
+ );
+
+ info("Got startup prefs: " + JSON.stringify(prefs));
+
+ // Sanity check the expected prefs. This isn't strictly necessary since we
+ // programmatically get the prefs above, but it's an extra layer of defense,
+ // for example in case we accidentally filtered out some expected prefs above.
+ // If this fails, you might have added a startup pref but didn't update this
+ // array here.
+ Assert.deepEqual(
+ prefs.sort(),
+ [
+ "quicksuggest.dataCollection.enabled",
+ "suggest.quicksuggest.nonsponsored",
+ "suggest.quicksuggest.sponsored",
+ ],
+ "Expected startup prefs"
+ );
+
+ // Make sure the prefs don't have user values that would mask the default
+ // values.
+ for (let p of prefs) {
+ UrlbarPrefs.clear(p);
+ }
+
+ // Build a map of default values.
+ let defaultValues = Object.fromEntries(
+ prefs.map(p => [p, UrlbarPrefs.get(p)])
+ );
+
+ // Now simulate startup. Restart telemetry environment but don't wait for it
+ // to finish before calling `updateFirefoxSuggestScenario()`. This simulates
+ // startup where telemetry environment's initialization races the intial
+ // update of the Suggest scenario.
+ let environmentInitPromise =
+ TelemetryEnvironment.testCleanRestart().onInitialized();
+
+ // Update the scenario and force the startup prefs to take on values that are
+ // the inverse of what they are now.
+ await UrlbarPrefs.updateFirefoxSuggestScenario({
+ isStartup: true,
+ scenario: "online",
+ defaultPrefs: {
+ online: Object.fromEntries(
+ Object.entries(defaultValues).map(([p, value]) => [p, !value])
+ ),
+ },
+ });
+
+ // At this point telemetry environment should be done initializing since
+ // `updateFirefoxSuggestScenario()` waits for it, but await our promise now.
+ await environmentInitPromise;
+
+ // TelemetryEnvironment should have cached the new values.
+ for (let [p, value] of Object.entries(defaultValues)) {
+ let expected = !value;
+ Assert.strictEqual(
+ TelemetryEnvironment.currentEnvironment.settings.userPrefs[
+ `browser.urlbar.${p}`
+ ],
+ expected,
+ `Check 1: ${p} is ${expected} in TelemetryEnvironment`
+ );
+ }
+
+ // Simulate another startup and set all prefs back to their original default
+ // values.
+ environmentInitPromise =
+ TelemetryEnvironment.testCleanRestart().onInitialized();
+
+ await UrlbarPrefs.updateFirefoxSuggestScenario({
+ isStartup: true,
+ scenario: "online",
+ defaultPrefs: {
+ online: defaultValues,
+ },
+ });
+
+ await environmentInitPromise;
+
+ // TelemetryEnvironment should have cached the new (original) values.
+ for (let [p, value] of Object.entries(defaultValues)) {
+ let expected = value;
+ Assert.strictEqual(
+ TelemetryEnvironment.currentEnvironment.settings.userPrefs[
+ `browser.urlbar.${p}`
+ ],
+ expected,
+ `Check 2: ${p} is ${expected} in TelemetryEnvironment`
+ );
+ }
+
+ await TelemetryEnvironment.testCleanRestart().onInitialized();
+});
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_sponsored.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_sponsored.js
new file mode 100644
index 0000000000..498b942a12
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_sponsored.js
@@ -0,0 +1,367 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This file tests primary telemetry for sponsored suggestions.
+ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ CONTEXTUAL_SERVICES_PING_TYPES:
+ "resource:///modules/PartnerLinkAttribution.sys.mjs",
+});
+
+const { TELEMETRY_SCALARS } = UrlbarProviderQuickSuggest;
+
+const REMOTE_SETTINGS_RESULT = {
+ id: 1,
+ url: "https://example.com/sponsored",
+ title: "Sponsored suggestion",
+ keywords: ["sponsored"],
+ click_url: "https://example.com/click",
+ impression_url: "https://example.com/impression",
+ advertiser: "testadvertiser",
+};
+
+const suggestion_type = "sponsored";
+const index = 1;
+const position = index + 1;
+
+add_setup(async function () {
+ await setUpTelemetryTest({
+ remoteSettingsResults: [
+ {
+ type: "data",
+ attachment: [REMOTE_SETTINGS_RESULT],
+ },
+ ],
+ });
+});
+
+// sponsored
+add_task(async function sponsored() {
+ let match_type = "firefox-suggest";
+
+ // Make sure `improve_suggest_experience_checked` is recorded correctly
+ // depending on the value of the related pref.
+ for (let improve_suggest_experience_checked of [false, true]) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "browser.urlbar.quicksuggest.dataCollection.enabled",
+ improve_suggest_experience_checked,
+ ],
+ ],
+ });
+ await doTelemetryTest({
+ index,
+ suggestion: REMOTE_SETTINGS_RESULT,
+ // impression-only
+ impressionOnly: {
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_SPONSORED]: position,
+ },
+ event: {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "impression_only",
+ extra: {
+ suggestion_type,
+ match_type,
+ position: position.toString(),
+ },
+ },
+ ping: {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION,
+ payload: {
+ match_type,
+ position,
+ improve_suggest_experience_checked,
+ is_clicked: false,
+ block_id: REMOTE_SETTINGS_RESULT.id,
+ advertiser: REMOTE_SETTINGS_RESULT.advertiser,
+ },
+ },
+ },
+ selectables: {
+ // click
+ "urlbarView-row-inner": {
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_SPONSORED]: position,
+ [TELEMETRY_SCALARS.CLICK_SPONSORED]: position,
+ },
+ event: {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "click",
+ extra: {
+ suggestion_type,
+ match_type,
+ position: position.toString(),
+ },
+ },
+ pings: [
+ {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION,
+ payload: {
+ match_type,
+ position,
+ improve_suggest_experience_checked,
+ is_clicked: true,
+ block_id: REMOTE_SETTINGS_RESULT.id,
+ advertiser: REMOTE_SETTINGS_RESULT.advertiser,
+ },
+ },
+ {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_SELECTION,
+ payload: {
+ match_type,
+ position,
+ improve_suggest_experience_checked,
+ block_id: REMOTE_SETTINGS_RESULT.id,
+ advertiser: REMOTE_SETTINGS_RESULT.advertiser,
+ },
+ },
+ ],
+ },
+ // block
+ "urlbarView-button-block": {
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_SPONSORED]: position,
+ [TELEMETRY_SCALARS.BLOCK_SPONSORED]: position,
+ },
+ event: {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "block",
+ extra: {
+ suggestion_type,
+ match_type,
+ position: position.toString(),
+ },
+ },
+ pings: [
+ {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION,
+ payload: {
+ match_type,
+ position,
+ improve_suggest_experience_checked,
+ is_clicked: false,
+ block_id: REMOTE_SETTINGS_RESULT.id,
+ advertiser: REMOTE_SETTINGS_RESULT.advertiser,
+ },
+ },
+ {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_BLOCK,
+ payload: {
+ match_type,
+ position,
+ improve_suggest_experience_checked,
+ block_id: REMOTE_SETTINGS_RESULT.id,
+ advertiser: REMOTE_SETTINGS_RESULT.advertiser,
+ iab_category: REMOTE_SETTINGS_RESULT.iab_category,
+ },
+ },
+ ],
+ },
+ // help
+ "urlbarView-button-help": {
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_SPONSORED]: position,
+ [TELEMETRY_SCALARS.HELP_SPONSORED]: position,
+ },
+ event: {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "help",
+ extra: {
+ suggestion_type,
+ match_type,
+ position: position.toString(),
+ },
+ },
+ pings: [
+ {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION,
+ payload: {
+ match_type,
+ position,
+ improve_suggest_experience_checked,
+ is_clicked: false,
+ block_id: REMOTE_SETTINGS_RESULT.id,
+ advertiser: REMOTE_SETTINGS_RESULT.advertiser,
+ },
+ },
+ ],
+ },
+ },
+ });
+ await SpecialPowers.popPrefEnv();
+ }
+});
+
+// sponsored best match
+add_task(async function sponsoredBestMatch() {
+ let match_type = "best-match";
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.bestMatch.enabled", true]],
+ });
+ await QuickSuggestTestUtils.setConfig(
+ QuickSuggestTestUtils.BEST_MATCH_CONFIG
+ );
+ await doTelemetryTest({
+ index,
+ suggestion: REMOTE_SETTINGS_RESULT,
+ // impression-only
+ impressionOnly: {
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_SPONSORED]: position,
+ [TELEMETRY_SCALARS.IMPRESSION_SPONSORED_BEST_MATCH]: position,
+ },
+ event: {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "impression_only",
+ extra: {
+ suggestion_type,
+ match_type,
+ position: position.toString(),
+ },
+ },
+ ping: {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION,
+ payload: {
+ match_type,
+ position,
+ is_clicked: false,
+ improve_suggest_experience_checked: false,
+ block_id: REMOTE_SETTINGS_RESULT.id,
+ advertiser: REMOTE_SETTINGS_RESULT.advertiser,
+ },
+ },
+ },
+ selectables: {
+ // click
+ "urlbarView-row-inner": {
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_SPONSORED]: position,
+ [TELEMETRY_SCALARS.IMPRESSION_SPONSORED_BEST_MATCH]: position,
+ [TELEMETRY_SCALARS.CLICK_SPONSORED]: position,
+ [TELEMETRY_SCALARS.CLICK_SPONSORED_BEST_MATCH]: position,
+ },
+ event: {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "click",
+ extra: {
+ suggestion_type,
+ match_type,
+ position: position.toString(),
+ },
+ },
+ pings: [
+ {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION,
+ payload: {
+ match_type,
+ position,
+ is_clicked: true,
+ improve_suggest_experience_checked: false,
+ block_id: REMOTE_SETTINGS_RESULT.id,
+ advertiser: REMOTE_SETTINGS_RESULT.advertiser,
+ },
+ },
+ {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_SELECTION,
+ payload: {
+ match_type,
+ position,
+ improve_suggest_experience_checked: false,
+ block_id: REMOTE_SETTINGS_RESULT.id,
+ advertiser: REMOTE_SETTINGS_RESULT.advertiser,
+ },
+ },
+ ],
+ },
+ // block
+ "urlbarView-button-block": {
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_SPONSORED]: position,
+ [TELEMETRY_SCALARS.IMPRESSION_SPONSORED_BEST_MATCH]: position,
+ [TELEMETRY_SCALARS.BLOCK_SPONSORED]: position,
+ [TELEMETRY_SCALARS.BLOCK_SPONSORED_BEST_MATCH]: position,
+ },
+ event: {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "block",
+ extra: {
+ suggestion_type,
+ match_type,
+ position: position.toString(),
+ },
+ },
+ pings: [
+ {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION,
+ payload: {
+ match_type,
+ position,
+ is_clicked: false,
+ improve_suggest_experience_checked: false,
+ block_id: REMOTE_SETTINGS_RESULT.id,
+ advertiser: REMOTE_SETTINGS_RESULT.advertiser,
+ },
+ },
+ {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_BLOCK,
+ payload: {
+ match_type,
+ position,
+ improve_suggest_experience_checked: false,
+ block_id: REMOTE_SETTINGS_RESULT.id,
+ advertiser: REMOTE_SETTINGS_RESULT.advertiser,
+ iab_category: REMOTE_SETTINGS_RESULT.iab_category,
+ },
+ },
+ ],
+ },
+ // help
+ "urlbarView-button-help": {
+ scalars: {
+ [TELEMETRY_SCALARS.IMPRESSION_SPONSORED]: position,
+ [TELEMETRY_SCALARS.IMPRESSION_SPONSORED_BEST_MATCH]: position,
+ [TELEMETRY_SCALARS.HELP_SPONSORED]: position,
+ [TELEMETRY_SCALARS.HELP_SPONSORED_BEST_MATCH]: position,
+ },
+ event: {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "help",
+ extra: {
+ suggestion_type,
+ match_type,
+ position: position.toString(),
+ },
+ },
+ pings: [
+ {
+ type: CONTEXTUAL_SERVICES_PING_TYPES.QS_IMPRESSION,
+ payload: {
+ match_type,
+ position,
+ is_clicked: false,
+ improve_suggest_experience_checked: false,
+ block_id: REMOTE_SETTINGS_RESULT.id,
+ advertiser: REMOTE_SETTINGS_RESULT.advertiser,
+ },
+ },
+ ],
+ },
+ },
+ });
+ await QuickSuggestTestUtils.setConfig(QuickSuggestTestUtils.DEFAULT_CONFIG);
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_weather.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_weather.js
new file mode 100644
index 0000000000..b60fa9fe85
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_telemetry_weather.js
@@ -0,0 +1,152 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This file tests primary telemetry for weather suggestions.
+ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ UrlbarProviderWeather: "resource:///modules/UrlbarProviderWeather.sys.mjs",
+});
+
+const suggestion_type = "weather";
+const match_type = "firefox-suggest";
+const index = 1;
+const position = index + 1;
+
+const { TELEMETRY_SCALARS: WEATHER_SCALARS } = UrlbarProviderWeather;
+const { WEATHER_SUGGESTION: suggestion, WEATHER_RS_DATA } = MerinoTestUtils;
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Make sure quick actions are disabled because showing them in the top
+ // sites view interferes with this test.
+ ["browser.urlbar.suggest.quickactions", false],
+ ],
+ });
+
+ await setUpTelemetryTest({
+ suggestions: [],
+ remoteSettingsResults: [
+ {
+ type: "weather",
+ weather: WEATHER_RS_DATA,
+ },
+ ],
+ });
+ await MerinoTestUtils.initWeather();
+ await updateTopSitesAndAwaitChanged();
+});
+
+add_task(async function () {
+ await doTelemetryTest({
+ index,
+ suggestion,
+ providerName: UrlbarProviderWeather.name,
+ showSuggestion: async () => {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: MerinoTestUtils.WEATHER_KEYWORD,
+ });
+ },
+ teardown: async () => {
+ // Picking the block button sets this pref to false and disables weather
+ // suggestions. We need to flip it back to true and wait for the
+ // suggestion to be fetched again before continuing to the next selectable
+ // test. The view also also stay open, so close it afterward.
+ if (!UrlbarPrefs.get("suggest.weather")) {
+ await UrlbarTestUtils.promisePopupClose(window);
+ gURLBar.handleRevert();
+ let fetchPromise = QuickSuggest.weather.waitForFetches();
+ UrlbarPrefs.clear("suggest.weather");
+ await fetchPromise;
+ }
+ },
+ // impression-only
+ impressionOnly: {
+ scalars: {
+ [WEATHER_SCALARS.IMPRESSION]: position,
+ },
+ event: {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "impression_only",
+ extra: {
+ suggestion_type,
+ match_type,
+ position: position.toString(),
+ },
+ },
+ },
+ selectables: {
+ // click
+ "urlbarView-row-inner": {
+ scalars: {
+ [WEATHER_SCALARS.IMPRESSION]: position,
+ [WEATHER_SCALARS.CLICK]: position,
+ },
+ event: {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "click",
+ extra: {
+ suggestion_type,
+ match_type,
+ position: position.toString(),
+ },
+ },
+ },
+ // block
+ "urlbarView-button-block": {
+ scalars: {
+ [WEATHER_SCALARS.IMPRESSION]: position,
+ [WEATHER_SCALARS.BLOCK]: position,
+ },
+ event: {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "block",
+ extra: {
+ suggestion_type,
+ match_type,
+ position: position.toString(),
+ },
+ },
+ },
+ // help
+ "urlbarView-button-help": {
+ scalars: {
+ [WEATHER_SCALARS.IMPRESSION]: position,
+ [WEATHER_SCALARS.HELP]: position,
+ },
+ event: {
+ category: QuickSuggest.TELEMETRY_EVENT_CATEGORY,
+ method: "engagement",
+ object: "help",
+ extra: {
+ suggestion_type,
+ match_type,
+ position: position.toString(),
+ },
+ },
+ },
+ },
+ });
+});
+
+async function updateTopSitesAndAwaitChanged() {
+ let url = "http://mochi.test:8888/topsite";
+ for (let i = 0; i < 5; i++) {
+ await PlacesTestUtils.addVisits(url);
+ }
+
+ info("Updating top sites and awaiting newtab-top-sites-changed");
+ let changedPromise = TestUtils.topicObserved("newtab-top-sites-changed").then(
+ () => info("Observed newtab-top-sites-changed")
+ );
+ await updateTopSites(sites => sites?.length);
+ await changedPromise;
+}
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/browser_weather.js b/browser/components/urlbar/tests/quicksuggest/browser/browser_weather.js
new file mode 100644
index 0000000000..049b25dd09
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/browser/browser_weather.js
@@ -0,0 +1,379 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Browser test for the weather suggestion.
+ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ UrlbarProviderWeather: "resource:///modules/UrlbarProviderWeather.sys.mjs",
+});
+
+add_setup(async function () {
+ await QuickSuggestTestUtils.ensureQuickSuggestInit({
+ remoteSettingsResults: [
+ {
+ type: "weather",
+ weather: MerinoTestUtils.WEATHER_RS_DATA,
+ },
+ ],
+ });
+ await MerinoTestUtils.initWeather();
+});
+
+// This test ensures the browser navigates to the weather webpage after
+// the weather result is selected.
+add_task(async function test_weather_result_selection() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(
+ tab.linkedBrowser,
+ false
+ );
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: MerinoTestUtils.WEATHER_KEYWORD,
+ });
+
+ info(`Select the weather result`);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+
+ info(`Navigate to the weather url`);
+ EventUtils.synthesizeKey("KEY_Enter");
+
+ await browserLoadedPromise;
+
+ Assert.equal(
+ gBrowser.currentURI.spec,
+ "https://example.com/weather",
+ "Assert the page navigated to the weather webpage after selecting the weather result."
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ await PlacesUtils.history.clear();
+});
+
+// Does a search, clicks the "Show less frequently" result menu command, and
+// repeats both steps until the min keyword length cap is reached.
+add_task(async function showLessFrequentlyCapReached_manySearches() {
+ // Set up a min keyword length and cap.
+ await QuickSuggestTestUtils.setRemoteSettingsResults([
+ {
+ type: "weather",
+ weather: {
+ keywords: ["weather"],
+ min_keyword_length: 3,
+ min_keyword_length_cap: 4,
+ },
+ },
+ ]);
+
+ // Trigger the suggestion.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "wea",
+ });
+
+ let resultIndex = 1;
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, resultIndex);
+ Assert.equal(
+ details.result.providerName,
+ UrlbarProviderWeather.name,
+ "Weather suggestion should be present at expected index after 'wea' search"
+ );
+
+ // Click the command.
+ let command = "show_less_frequently";
+ await UrlbarTestUtils.openResultMenuAndClickItem(window, command, {
+ resultIndex,
+ });
+
+ Assert.ok(
+ gURLBar.view.isOpen,
+ "The view should remain open clicking the command"
+ );
+ Assert.ok(
+ details.element.row.hasAttribute("feedback-acknowledgment"),
+ "Row should have feedback acknowledgment after clicking command"
+ );
+ Assert.equal(
+ UrlbarPrefs.get("weather.minKeywordLength"),
+ 4,
+ "weather.minKeywordLength should be incremented once"
+ );
+
+ // Do the same search again. The suggestion should not appear.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "wea",
+ });
+
+ for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) {
+ details = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ Assert.notEqual(
+ details.result.providerName,
+ UrlbarProviderWeather.name,
+ `Weather suggestion should be absent (checking index ${i})`
+ );
+ }
+
+ // Do a search using one more character. The suggestion should appear.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "weat",
+ });
+
+ details = await UrlbarTestUtils.getDetailsOfResultAt(window, resultIndex);
+ Assert.equal(
+ details.result.providerName,
+ UrlbarProviderWeather.name,
+ "Weather suggestion should be present at expected index after 'weat' search"
+ );
+ Assert.ok(
+ !details.element.row.hasAttribute("feedback-acknowledgment"),
+ "Row should not have feedback acknowledgment after 'weat' search"
+ );
+
+ // Since the cap has been reached, the command should no longer appear in the
+ // result menu.
+ await UrlbarTestUtils.openResultMenu(window, { resultIndex });
+ let menuitem = gURLBar.view.resultMenu.querySelector(
+ `menuitem[data-command=${command}]`
+ );
+ Assert.ok(!menuitem, "Menuitem should be absent");
+ gURLBar.view.resultMenu.hidePopup(true);
+
+ await UrlbarTestUtils.promisePopupClose(window);
+ await QuickSuggestTestUtils.setRemoteSettingsResults([
+ {
+ type: "weather",
+ weather: MerinoTestUtils.WEATHER_RS_DATA,
+ },
+ ]);
+ UrlbarPrefs.clear("weather.minKeywordLength");
+});
+
+// Repeatedly clicks the "Show less frequently" result menu command after doing
+// a single search until the min keyword length cap is reached.
+add_task(async function showLessFrequentlyCapReached_oneSearch() {
+ // Set up a min keyword length and cap.
+ await QuickSuggestTestUtils.setRemoteSettingsResults([
+ {
+ type: "weather",
+ weather: {
+ keywords: ["weather"],
+ min_keyword_length: 3,
+ min_keyword_length_cap: 6,
+ },
+ },
+ ]);
+
+ // Trigger the suggestion.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "wea",
+ });
+
+ let resultIndex = 1;
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, resultIndex);
+ Assert.equal(
+ details.result.providerName,
+ UrlbarProviderWeather.name,
+ "Weather suggestion should be present at expected index after 'wea' search"
+ );
+
+ let command = "show_less_frequently";
+
+ for (let i = 0; i < 3; i++) {
+ await UrlbarTestUtils.openResultMenuAndClickItem(window, command, {
+ resultIndex,
+ openByMouse: true,
+ });
+
+ Assert.ok(
+ gURLBar.view.isOpen,
+ "The view should remain open clicking the command"
+ );
+ Assert.ok(
+ details.element.row.hasAttribute("feedback-acknowledgment"),
+ "Row should have feedback acknowledgment after clicking command"
+ );
+ Assert.equal(
+ UrlbarPrefs.get("weather.minKeywordLength"),
+ 4 + i,
+ "weather.minKeywordLength should be incremented once"
+ );
+ }
+
+ let menuitem = await UrlbarTestUtils.openResultMenuAndGetItem({
+ window,
+ command,
+ resultIndex,
+ });
+ Assert.ok(
+ !menuitem,
+ "The menuitem should not exist after the cap is reached"
+ );
+
+ gURLBar.view.resultMenu.hidePopup(true);
+ await UrlbarTestUtils.promisePopupClose(window);
+ await QuickSuggestTestUtils.setRemoteSettingsResults([
+ {
+ type: "weather",
+ weather: MerinoTestUtils.WEATHER_RS_DATA,
+ },
+ ]);
+ UrlbarPrefs.clear("weather.minKeywordLength");
+});
+
+// Tests the "Not interested" result menu dismissal command.
+add_task(async function notInterested() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: MerinoTestUtils.WEATHER_KEYWORD,
+ });
+ await doDismissTest("not_interested");
+});
+
+// Tests the "Not relevant" result menu dismissal command.
+add_task(async function notRelevant() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: MerinoTestUtils.WEATHER_KEYWORD,
+ });
+ await doDismissTest("not_relevant");
+});
+
+async function doDismissTest(command) {
+ let resultCount = UrlbarTestUtils.getResultCount(window);
+
+ let resultIndex = 1;
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, resultIndex);
+ Assert.equal(
+ details.result.providerName,
+ UrlbarProviderWeather.name,
+ "Weather suggestion should be present"
+ );
+
+ // Click the command.
+ await UrlbarTestUtils.openResultMenuAndClickItem(
+ window,
+ ["[data-l10n-id=firefox-suggest-command-dont-show-this]", command],
+ { resultIndex, openByMouse: true }
+ );
+
+ Assert.ok(
+ !UrlbarPrefs.get("suggest.weather"),
+ "suggest.weather pref should be set to false after dismissal"
+ );
+
+ // The row should be a tip now.
+ Assert.ok(gURLBar.view.isOpen, "The view should remain open after dismissal");
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ resultCount,
+ "The result count should not haved changed after dismissal"
+ );
+ details = await UrlbarTestUtils.getDetailsOfResultAt(window, resultIndex);
+ Assert.equal(
+ details.type,
+ UrlbarUtils.RESULT_TYPE.TIP,
+ "Row should be a tip after dismissal"
+ );
+ Assert.equal(
+ details.result.payload.type,
+ "dismissalAcknowledgment",
+ "Tip type should be dismissalAcknowledgment"
+ );
+ Assert.ok(
+ !details.element.row.hasAttribute("feedback-acknowledgment"),
+ "Row should not have feedback acknowledgment after dismissal"
+ );
+
+ // Get the dismissal acknowledgment's "Got it" button and click it.
+ let gotItButton = UrlbarTestUtils.getButtonForResultIndex(
+ window,
+ "0",
+ resultIndex
+ );
+ Assert.ok(gotItButton, "Row should have a 'Got it' button");
+ EventUtils.synthesizeMouseAtCenter(gotItButton, {}, window);
+
+ // The view should remain open and the tip row should be gone.
+ Assert.ok(
+ gURLBar.view.isOpen,
+ "The view should remain open clicking the 'Got it' button"
+ );
+ Assert.equal(
+ UrlbarTestUtils.getResultCount(window),
+ resultCount - 1,
+ "The result count should be one less after clicking 'Got it' button"
+ );
+ for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) {
+ details = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ Assert.ok(
+ details.type != UrlbarUtils.RESULT_TYPE.TIP &&
+ details.result.providerName != UrlbarProviderWeather.name,
+ "Tip result and weather result should not be present"
+ );
+ }
+
+ await UrlbarTestUtils.promisePopupClose(window);
+
+ // Enable the weather suggestion again and wait for it to be fetched.
+ let fetchPromise = QuickSuggest.weather.waitForFetches();
+ UrlbarPrefs.clear("suggest.weather");
+ info("Waiting for weather fetch after re-enabling the suggestion");
+ await fetchPromise;
+ info("Got weather fetch");
+}
+
+// Tests the "Report inaccurate location" result menu command immediately
+// followed by a dismissal command to make sure other commands still work
+// properly while the urlbar session remains ongoing.
+add_task(async function inaccurateLocationAndDismissal() {
+ await doSessionOngoingCommandTest("inaccurate_location");
+});
+
+// Tests the "Show less frequently" result menu command immediately followed by
+// a dismissal command to make sure other commands still work properly while the
+// urlbar session remains ongoing.
+add_task(async function showLessFrequentlyAndDismissal() {
+ await doSessionOngoingCommandTest("show_less_frequently");
+ UrlbarPrefs.clear("weather.minKeywordLength");
+});
+
+async function doSessionOngoingCommandTest(command) {
+ // Trigger the suggestion.
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: MerinoTestUtils.WEATHER_KEYWORD,
+ });
+
+ let resultIndex = 1;
+ let details = await UrlbarTestUtils.getDetailsOfResultAt(window, resultIndex);
+ Assert.equal(
+ details.result.providerName,
+ UrlbarProviderWeather.name,
+ "Weather suggestion should be present at expected index after search"
+ );
+
+ // Click the command.
+ await UrlbarTestUtils.openResultMenuAndClickItem(window, command, {
+ resultIndex,
+ });
+
+ Assert.ok(
+ gURLBar.view.isOpen,
+ "The view should remain open clicking the command"
+ );
+ Assert.ok(
+ details.element.row.hasAttribute("feedback-acknowledgment"),
+ "Row should have feedback acknowledgment after clicking command"
+ );
+
+ info("Doing dismissal");
+ await doDismissTest("not_interested");
+}
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/head.js b/browser/components/urlbar/tests/quicksuggest/browser/head.js
new file mode 100644
index 0000000000..07979080fd
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/browser/head.js
@@ -0,0 +1,569 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let sandbox;
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/browser/components/urlbar/tests/browser/head-common.js",
+ this
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ CONTEXTUAL_SERVICES_PING_TYPES:
+ "resource:///modules/PartnerLinkAttribution.jsm",
+ QuickSuggest: "resource:///modules/QuickSuggest.sys.mjs",
+ TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs",
+ UrlbarProviderQuickSuggest:
+ "resource:///modules/UrlbarProviderQuickSuggest.sys.mjs",
+});
+
+XPCOMUtils.defineLazyGetter(this, "QuickSuggestTestUtils", () => {
+ const { QuickSuggestTestUtils: module } = ChromeUtils.importESModule(
+ "resource://testing-common/QuickSuggestTestUtils.sys.mjs"
+ );
+ module.init(this);
+ return module;
+});
+
+XPCOMUtils.defineLazyGetter(this, "MerinoTestUtils", () => {
+ const { MerinoTestUtils: module } = ChromeUtils.importESModule(
+ "resource://testing-common/MerinoTestUtils.sys.mjs"
+ );
+ module.init(this);
+ return module;
+});
+
+registerCleanupFunction(async () => {
+ // Ensure the popup is always closed at the end of each test to avoid
+ // interfering with the next test.
+ await UrlbarTestUtils.promisePopupClose(window);
+});
+
+/**
+ * Updates the Top Sites feed.
+ *
+ * @param {Function} condition
+ * A callback that returns true after Top Sites are successfully updated.
+ * @param {boolean} searchShortcuts
+ * True if Top Sites search shortcuts should be enabled.
+ */
+async function updateTopSites(condition, searchShortcuts = false) {
+ // Toggle the pref to clear the feed cache and force an update.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "browser.newtabpage.activity-stream.discoverystream.endpointSpocsClear",
+ "",
+ ],
+ ["browser.newtabpage.activity-stream.feeds.system.topsites", false],
+ ["browser.newtabpage.activity-stream.feeds.system.topsites", true],
+ [
+ "browser.newtabpage.activity-stream.improvesearch.topSiteSearchShortcuts",
+ searchShortcuts,
+ ],
+ ],
+ });
+
+ // Wait for the feed to be updated.
+ await TestUtils.waitForCondition(() => {
+ let sites = AboutNewTab.getTopSites();
+ return condition(sites);
+ }, "Waiting for top sites to be updated");
+}
+
+/**
+ * Call this in your setup task if you use `doTelemetryTest()`.
+ *
+ * @param {object} options
+ * Options
+ * @param {Array} options.remoteSettingsResults
+ * Array of remote settings result objects. If not given, no suggestions
+ * will be present in remote settings.
+ * @param {Array} options.merinoSuggestions
+ * Array of Merino suggestion objects. If given, this function will start
+ * the mock Merino server and set `quicksuggest.dataCollection.enabled` to
+ * true so that `UrlbarProviderQuickSuggest` will fetch suggestions from it.
+ * Otherwise Merino will not serve suggestions, but you can still set up
+ * Merino without using this function by using `MerinoTestUtils` directly.
+ * @param {Array} options.config
+ * Quick suggest will be initialized with this config. Leave undefined to use
+ * the default config. See `QuickSuggestTestUtils` for details.
+ */
+async function setUpTelemetryTest({
+ remoteSettingsResults,
+ merinoSuggestions = null,
+ config = QuickSuggestTestUtils.DEFAULT_CONFIG,
+}) {
+ if (UrlbarPrefs.get("resultMenu")) {
+ todo(
+ false,
+ "telemetry for the result menu to be implemented in bug 1790020"
+ );
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.resultMenu", false]],
+ });
+ }
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Enable blocking on primary sponsored and nonsponsored suggestions so we
+ // can test the block button.
+ ["browser.urlbar.quicksuggest.blockingEnabled", true],
+ ["browser.urlbar.bestMatch.blockingEnabled", true],
+ // Switch-to-tab results can sometimes appear after the test clicks a help
+ // button and closes the new tab, which interferes with the expected
+ // indexes of quick suggest results, so disable them.
+ ["browser.urlbar.suggest.openpage", false],
+ // Disable the persisted-search-terms search tip because it can interfere.
+ ["browser.urlbar.tipShownCount.searchTip_persist", 999],
+ ],
+ });
+
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await UrlbarTestUtils.formHistory.clear();
+
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+
+ // Add a mock engine so we don't hit the network.
+ await SearchTestUtils.installSearchExtension({}, { setAsDefault: true });
+
+ await QuickSuggestTestUtils.ensureQuickSuggestInit({
+ remoteSettingsResults,
+ merinoSuggestions,
+ config,
+ });
+}
+
+/**
+ * Main entry point for testing primary telemetry for quick suggest suggestions:
+ * impressions, clicks, helps, and blocks. This can be used to declaratively
+ * test all primary telemetry for any suggestion type.
+ *
+ * @param {object} options
+ * Options
+ * @param {number} options.index
+ * The expected index of the suggestion in the results list.
+ * @param {object} options.suggestion
+ * The suggestion being tested.
+ * @param {object} options.impressionOnly
+ * An object describing the expected impression-only telemetry, i.e.,
+ * telemetry recorded when an impression occurs but not a click. It must have
+ * the following properties:
+ * {object} scalars
+ * An object that maps expected scalar names to values.
+ * {object} event
+ * The expected recorded event.
+ * {object} ping
+ * The expected recorded custom telemetry ping. If no ping is expected,
+ * leave this undefined or pass null.
+ * @param {object} options.selectables
+ * An object describing the telemetry that's expected to be recorded when each
+ * selectable element in the suggestion's row is picked. This object maps HTML
+ * class names to objects. Each property's name must be an HTML class name
+ * that uniquely identifies a selectable element within the row. The value
+ * must be an object that describes the telemetry that's expected to be
+ * recorded when that element is picked, and this inner object must have the
+ * following properties:
+ * {object} scalars
+ * An object that maps expected scalar names to values.
+ * {object} event
+ * The expected recorded event.
+ * {Array} pings
+ * A list of expected recorded custom telemetry pings. If no pings are
+ * expected, pass an empty array.
+ * @param {string} options.providerName
+ * The name of the provider that is expected to create the UrlbarResult for
+ * the suggestion.
+ * @param {Function} options.teardown
+ * If given, this function will be called after each selectable test. If
+ * picking an element causes side effects that need to be cleaned up before
+ * starting the next selectable test, they can be cleaned up here.
+ * @param {Function} options.showSuggestion
+ * This function should open the view and show the suggestion.
+ */
+async function doTelemetryTest({
+ index,
+ suggestion,
+ impressionOnly,
+ selectables,
+ providerName = UrlbarProviderQuickSuggest.name,
+ teardown = null,
+ showSuggestion = () =>
+ UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ // If the suggestion object is a remote settings result, it will have a
+ // `keywords` property. Otherwise the suggestion object must be a Merino
+ // suggestion, and the search string doesn't matter in that case because
+ // the mock Merino server will be set up to return suggestions regardless.
+ value: suggestion.keywords?.[0] || "test",
+ fireInputEvent: true,
+ }),
+}) {
+ // Do the impression-only test. It will return the `classList` values of all
+ // the selectable elements in the row so we can use them below.
+ let selectableClassLists = await doImpressionOnlyTest({
+ index,
+ suggestion,
+ providerName,
+ showSuggestion,
+ expected: impressionOnly,
+ });
+ if (!selectableClassLists) {
+ Assert.ok(
+ false,
+ "Impression test didn't complete successfully, stopping telemetry test"
+ );
+ return;
+ }
+
+ info(
+ "Got classLists of actual selectable elements in the row: " +
+ JSON.stringify(selectableClassLists)
+ );
+
+ let allMatchedExpectedClasses = new Set();
+
+ // For each actual selectable element in the row, do a selectable test by
+ // picking the element and checking telemetry.
+ for (let classList of selectableClassLists) {
+ info(
+ "Setting up selectable test for actual element with classList " +
+ JSON.stringify(classList)
+ );
+
+ // Each of the actual selectable elements should match exactly one of the
+ // test's expected selectable classes.
+ //
+ // * If an element doesn't match any expected class, then the test does not
+ // account for that element, which is an error in the test.
+ // * If an element matches more than one expected class, then the expected
+ // class is not specific enough, which is also an error in the test.
+
+ // Collect all the expected classes that match the actual element.
+ let matchingExpectedClasses = Object.keys(selectables).filter(className =>
+ classList.includes(className)
+ );
+
+ if (!matchingExpectedClasses.length) {
+ Assert.ok(
+ false,
+ "Actual selectable element doesn't match any expected classes. The element's classList is " +
+ JSON.stringify(classList)
+ );
+ continue;
+ }
+ if (matchingExpectedClasses.length > 1) {
+ Assert.ok(
+ false,
+ "Actual selectable element matches multiple expected classes. The element's classList is " +
+ JSON.stringify(classList)
+ );
+ continue;
+ }
+
+ let className = matchingExpectedClasses[0];
+ allMatchedExpectedClasses.add(className);
+
+ await doSelectableTest({
+ suggestion,
+ providerName,
+ showSuggestion,
+ index,
+ className,
+ expected: selectables[className],
+ });
+
+ if (teardown) {
+ info("Calling teardown");
+ await teardown();
+ info("Finished teardown");
+ }
+ }
+
+ // Finally, if an expected class doesn't match any actual element, then the
+ // test expects an element to be picked that either isn't present or isn't
+ // selectable, which is an error in the test.
+ Assert.deepEqual(
+ Object.keys(selectables).filter(
+ className => !allMatchedExpectedClasses.has(className)
+ ),
+ [],
+ "There should be no expected classes that didn't match actual selectable elements"
+ );
+}
+
+/**
+ * Helper for `doTelemetryTest()` that does an impression-only test.
+ *
+ * @param {object} options
+ * Options
+ * @param {number} options.index
+ * The expected index of the suggestion in the results list.
+ * @param {object} options.suggestion
+ * The suggestion being tested.
+ * @param {string} options.providerName
+ * The name of the provider that is expected to create the UrlbarResult for
+ * the suggestion.
+ * @param {object} options.expected
+ * An object describing the expected impression-only telemetry. It must have
+ * the following properties:
+ * {object} scalars
+ * An object that maps expected scalar names to values.
+ * {object} event
+ * The expected recorded event.
+ * {object} ping
+ * The expected recorded custom telemetry ping. If no ping is expected,
+ * leave this undefined or pass null.
+ * @param {Function} options.showSuggestion
+ * This function should open the view and show the suggestion.
+ * @returns {Array}
+ * The `classList` values of all the selectable elements in the suggestion's
+ * row. Each item in this array is a selectable element's `classList` that has
+ * been converted to an array of strings.
+ */
+async function doImpressionOnlyTest({
+ index,
+ suggestion,
+ providerName,
+ expected,
+ showSuggestion,
+}) {
+ info("Starting impression-only test");
+
+ Services.telemetry.clearEvents();
+ let { spy, spyCleanup } = QuickSuggestTestUtils.createTelemetryPingSpy();
+
+ info("Showing suggestion");
+ await showSuggestion();
+
+ // Get the suggestion row.
+ let row = await validateSuggestionRow(index, suggestion, providerName);
+ if (!row) {
+ Assert.ok(
+ false,
+ "Couldn't get suggestion row, stopping impression-only test"
+ );
+ await spyCleanup();
+ return null;
+ }
+
+ // We need to get a different selectable row so we can pick it to trigger
+ // impression-only telemetry. For simplicity we'll look for a row that will
+ // load a URL when picked. We'll also verify no other rows are from the
+ // expected provider.
+ let otherRow;
+ let rowCount = UrlbarTestUtils.getResultCount(window);
+ for (let i = 0; i < rowCount; i++) {
+ if (i != index) {
+ let r = await UrlbarTestUtils.waitForAutocompleteResultAt(window, i);
+ Assert.notEqual(
+ r.result.providerName,
+ providerName,
+ "No other row should be from expected provider: index = " + i
+ );
+ if (
+ !otherRow &&
+ (r.result.payload.url ||
+ (r.result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
+ (r.result.payload.query || r.result.payload.suggestion))) &&
+ r.hasAttribute("row-selectable")
+ ) {
+ otherRow = r;
+ }
+ }
+ }
+ if (!otherRow) {
+ Assert.ok(
+ false,
+ "Couldn't get a different selectable row with a URL, stopping impression-only test"
+ );
+ await spyCleanup();
+ return null;
+ }
+
+ // Collect the `classList` values for all selectable elements in the row.
+ let selectableClassLists = [];
+ let selectables = row.querySelectorAll(":is([selectable], [role=button])");
+ for (let element of selectables) {
+ selectableClassLists.push([...element.classList]);
+ }
+
+ // Pick the different row. Assumptions:
+ // * The middle of the row is selectable
+ // * Picking the row will load a page
+ info("Clicking different row and waiting for view to close");
+ let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ await UrlbarTestUtils.promisePopupClose(window, () =>
+ EventUtils.synthesizeMouseAtCenter(otherRow, {})
+ );
+
+ info("Waiting for page to load after clicking different row");
+ await loadPromise;
+
+ // Check telemetry.
+ info("Checking scalars. Expected: " + JSON.stringify(expected.scalars));
+ QuickSuggestTestUtils.assertScalars(expected.scalars);
+
+ info("Checking events. Expected: " + JSON.stringify([expected.event]));
+ QuickSuggestTestUtils.assertEvents([expected.event]);
+
+ let expectedPings = expected.ping ? [expected.ping] : [];
+ info("Checking pings. Expected: " + JSON.stringify(expectedPings));
+ QuickSuggestTestUtils.assertPings(spy, expectedPings);
+
+ // Clean up.
+ await PlacesUtils.history.clear();
+ await UrlbarTestUtils.formHistory.clear();
+ await spyCleanup();
+
+ info("Finished impression-only test");
+
+ return selectableClassLists;
+}
+
+/**
+ * Helper for `doTelemetryTest()` that picks a selectable element in a
+ * suggestion's row and checks telemetry.
+ *
+ * @param {object} options
+ * Options
+ * @param {number} options.index
+ * The expected index of the suggestion in the results list.
+ * @param {object} options.suggestion
+ * The suggestion being tested.
+ * @param {string} options.providerName
+ * The name of the provider that is expected to create the UrlbarResult for
+ * the suggestion.
+ * @param {string} options.className
+ * An HTML class name that should uniquely identify the selectable element
+ * within its row.
+ * @param {object} options.expected
+ * An object describing the telemetry that's expected to be recorded when the
+ * selectable element is picked. It must have the following properties:
+ * {object} scalars
+ * An object that maps expected scalar names to values.
+ * {object} event
+ * The expected recorded event.
+ * {Array} pings
+ * A list of expected recorded custom telemetry pings. If no pings are
+ * expected, leave this undefined or pass an empty array.
+ * @param {Function} options.showSuggestion
+ * This function should open the view and show the suggestion.
+ */
+async function doSelectableTest({
+ index,
+ suggestion,
+ providerName,
+ className,
+ expected,
+ showSuggestion,
+}) {
+ info("Starting selectable test: " + JSON.stringify({ className }));
+
+ Services.telemetry.clearEvents();
+ let { spy, spyCleanup } = QuickSuggestTestUtils.createTelemetryPingSpy();
+
+ info("Showing suggestion");
+ await showSuggestion();
+
+ let row = await validateSuggestionRow(index, suggestion, providerName);
+ if (!row) {
+ Assert.ok(false, "Couldn't get suggestion row, stopping selectable test");
+ await spyCleanup();
+ return;
+ }
+
+ let element = row.querySelector("." + className);
+ Assert.ok(element, "Sanity check: Target selectable element should exist");
+
+ let loadPromise;
+ if (className == "urlbarView-row-inner") {
+ // We assume clicking the row-inner will cause a page to load in the current
+ // browser.
+ loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ } else if (className == "urlbarView-button-help") {
+ loadPromise = BrowserTestUtils.waitForNewTab(gBrowser);
+ }
+
+ info("Clicking element: " + className);
+ EventUtils.synthesizeMouseAtCenter(element, {});
+
+ if (loadPromise) {
+ info("Waiting for load");
+ await loadPromise;
+ await TestUtils.waitForTick();
+ if (className == "urlbarView-button-help") {
+ info("Closing help tab");
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+ }
+
+ info("Checking scalars. Expected: " + JSON.stringify(expected.scalars));
+ QuickSuggestTestUtils.assertScalars(expected.scalars);
+
+ info("Checking events. Expected: " + JSON.stringify([expected.event]));
+ QuickSuggestTestUtils.assertEvents([expected.event]);
+
+ let expectedPings = expected.pings ?? [];
+ info("Checking pings. Expected: " + JSON.stringify(expectedPings));
+ QuickSuggestTestUtils.assertPings(spy, expectedPings);
+
+ if (className == "urlbarView-button-block") {
+ await QuickSuggest.blockedSuggestions.clear();
+ }
+ await PlacesUtils.history.clear();
+ await spyCleanup();
+
+ info("Finished selectable test: " + JSON.stringify({ className }));
+}
+
+/**
+ * Gets a row in the view, which is assumed to be open, and asserts that it's a
+ * particular quick suggest row. If it is, the row is returned. If it's not,
+ * null is returned.
+ *
+ * @param {number} index
+ * The expected index of the quick suggest row.
+ * @param {object} suggestion
+ * The expected suggestion.
+ * @param {string} providerName
+ * The name of the provider that is expected to create the UrlbarResult for
+ * the suggestion.
+ * @returns {Element}
+ * If the row is the expected suggestion, the row element is returned.
+ * Otherwise null is returned.
+ */
+async function validateSuggestionRow(index, suggestion, providerName) {
+ let rowCount = UrlbarTestUtils.getResultCount(window);
+ Assert.less(
+ index,
+ rowCount,
+ "Expected suggestion row index should be < row count"
+ );
+ if (rowCount <= index) {
+ return null;
+ }
+
+ let row = await UrlbarTestUtils.waitForAutocompleteResultAt(window, index);
+ Assert.equal(
+ row.result.providerName,
+ providerName,
+ "Expected suggestion row should be from expected provider"
+ );
+ Assert.equal(
+ row.result.payload.url,
+ suggestion.url,
+ "The suggestion row should represent the expected suggestion"
+ );
+ if (
+ row.result.providerName != providerName ||
+ row.result.payload.url != suggestion.url
+ ) {
+ return null;
+ }
+
+ return row;
+}
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/searchSuggestionEngine.sjs b/browser/components/urlbar/tests/quicksuggest/browser/searchSuggestionEngine.sjs
new file mode 100644
index 0000000000..145392fcf2
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/browser/searchSuggestionEngine.sjs
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let gTimer;
+
+function handleRequest(req, resp) {
+ // Parse the query params. If the params aren't in the form "foo=bar", then
+ // treat the entire query string as a search string.
+ let params = req.queryString.split("&").reduce((memo, pair) => {
+ let [key, val] = pair.split("=");
+ if (!val) {
+ // This part isn't in the form "foo=bar". Treat it as the search string
+ // (the "query").
+ val = key;
+ key = "query";
+ }
+ memo[decode(key)] = decode(val);
+ return memo;
+ }, {});
+
+ let timeout = parseInt(params.timeout);
+ if (timeout) {
+ // Write the response after a timeout.
+ resp.processAsync();
+ gTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ gTimer.init(
+ () => {
+ writeResponse(params, resp);
+ resp.finish();
+ },
+ timeout,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+ return;
+ }
+
+ writeResponse(params, resp);
+}
+
+function writeResponse(params, resp) {
+ // Echo back the search string with "foo" and "bar" appended.
+ let suffixes = ["foo", "bar"];
+ if (params.count) {
+ // Add more suffixes.
+ let serial = 0;
+ while (suffixes.length < params.count) {
+ suffixes.push(++serial);
+ }
+ }
+ let data = [params.query, suffixes.map(s => params.query + s)];
+ resp.setHeader("Content-Type", "application/json", false);
+ resp.write(JSON.stringify(data));
+}
+
+function decode(str) {
+ return decodeURIComponent(str.replace(/\+/g, encodeURIComponent(" ")));
+}
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/searchSuggestionEngine.xml b/browser/components/urlbar/tests/quicksuggest/browser/searchSuggestionEngine.xml
new file mode 100644
index 0000000000..142c91849c
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/browser/searchSuggestionEngine.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>browser_searchSuggestionEngine searchSuggestionEngine.xml</ShortName>
+<Url type="application/x-suggestions+json" method="GET" template="http://mochi.test:8888/browser/browser/components/urlbar/tests/browser/searchSuggestionEngine.sjs?{searchTerms}"/>
+<Url type="text/html" method="GET" template="http://mochi.test:8888/" rel="searchform">
+ <Param name="terms" value="{searchTerms}"/>
+</Url>
+</SearchPlugin>
diff --git a/browser/components/urlbar/tests/quicksuggest/browser/subdialog.xhtml b/browser/components/urlbar/tests/quicksuggest/browser/subdialog.xhtml
new file mode 100644
index 0000000000..67303f19ac
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/browser/subdialog.xhtml
@@ -0,0 +1,14 @@
+<?xml version="1.0"?>
+
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ title="Sample sub-dialog">
+<dialog id="subDialog">
+ <description id="desc">A sample sub-dialog for testing</description>
+</dialog>
+</window>