summaryrefslogtreecommitdiffstats
path: root/browser/components/urlbar/tests/quicksuggest/browser/head.js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /browser/components/urlbar/tests/quicksuggest/browser/head.js
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/components/urlbar/tests/quicksuggest/browser/head.js')
-rw-r--r--browser/components/urlbar/tests/quicksuggest/browser/head.js693
1 files changed, 693 insertions, 0 deletions
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..7d62a44d45
--- /dev/null
+++ b/browser/components/urlbar/tests/quicksuggest/browser/head.js
@@ -0,0 +1,693 @@
+/* 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",
+});
+
+ChromeUtils.defineLazyGetter(this, "QuickSuggestTestUtils", () => {
+ const { QuickSuggestTestUtils: module } = ChromeUtils.importESModule(
+ "resource://testing-common/QuickSuggestTestUtils.sys.mjs"
+ );
+ module.init(this);
+ return module;
+});
+
+ChromeUtils.defineLazyGetter(this, "MerinoTestUtils", () => {
+ const { MerinoTestUtils: module } = ChromeUtils.importESModule(
+ "resource://testing-common/MerinoTestUtils.sys.mjs"
+ );
+ module.init(this);
+ return module;
+});
+
+ChromeUtils.defineLazyGetter(this, "PlacesFrecencyRecalculator", () => {
+ return Cc["@mozilla.org/places/frecency-recalculator;1"].getService(
+ Ci.nsIObserver
+ ).wrappedJSObject;
+});
+
+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.remoteSettingsRecords
+ * See `QuickSuggestTestUtils.ensureQuickSuggestInit()`.
+ * @param {Array} options.merinoSuggestions
+ * See `QuickSuggestTestUtils.ensureQuickSuggestInit()`.
+ * @param {Array} options.config
+ * See `QuickSuggestTestUtils.ensureQuickSuggestInit()`.
+ */
+async function setUpTelemetryTest({
+ remoteSettingsRecords,
+ merinoSuggestions = null,
+ config = QuickSuggestTestUtils.DEFAULT_CONFIG,
+}) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // 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();
+
+ // Add a mock engine so we don't hit the network.
+ await SearchTestUtils.installSearchExtension({}, { setAsDefault: true });
+
+ await QuickSuggestTestUtils.ensureQuickSuggestInit({
+ remoteSettingsRecords,
+ 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.click
+ * An object describing the expected click telemetry. It must have the same
+ * properties as `impressionOnly` except `ping` must be `pings` (plural), an
+ * array of expected pings.
+ * @param {Array} options.commands
+ * Each element in this array is an object that describes the expected
+ * telemetry for a result menu command. Each object must have the following
+ * properties:
+ * {string|Array} command
+ * A command name or array; this is passed directly to
+ * `UrlbarTestUtils.openResultMenuAndClickItem()` as the `commandOrArray`
+ * arg, so see its documentation for details.
+ * {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,
+ click,
+ commands,
+ 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,
+ }),
+}) {
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+
+ await doImpressionOnlyTest({
+ index,
+ suggestion,
+ providerName,
+ showSuggestion,
+ expected: impressionOnly,
+ });
+
+ await doClickTest({
+ suggestion,
+ providerName,
+ showSuggestion,
+ index,
+ expected: click,
+ });
+
+ for (let command of commands) {
+ await doCommandTest({
+ suggestion,
+ providerName,
+ showSuggestion,
+ index,
+ commandOrArray: command.command,
+ expected: command,
+ });
+
+ if (teardown) {
+ info("Calling teardown");
+ await teardown();
+ info("Finished teardown");
+ }
+ }
+}
+
+/**
+ * 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.
+ */
+async function doImpressionOnlyTest({
+ index,
+ suggestion,
+ providerName,
+ expected,
+ showSuggestion,
+}) {
+ info("Starting impression-only test");
+
+ Services.telemetry.clearEvents();
+
+ let expectedPings = expected.ping ? [expected.ping] : [];
+ let gleanPingCount = watchGleanPings(expectedPings);
+
+ 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"
+ );
+ return;
+ }
+
+ // 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"
+ );
+ return;
+ }
+
+ // 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]);
+
+ Assert.equal(
+ expectedPings.length,
+ gleanPingCount.value,
+ "Submitted one Glean ping per expected ping"
+ );
+
+ // Clean up.
+ await PlacesUtils.history.clear();
+ await UrlbarTestUtils.formHistory.clear();
+
+ info("Finished impression-only test");
+}
+
+/**
+ * Helper for `doTelemetryTest()` that clicks 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 {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 doClickTest({
+ index,
+ suggestion,
+ providerName,
+ expected,
+ showSuggestion,
+}) {
+ info("Starting click test");
+
+ Services.telemetry.clearEvents();
+
+ let expectedPings = expected.pings ?? [];
+ let gleanPingCount = watchGleanPings(expectedPings);
+
+ info("Showing suggestion");
+ await showSuggestion();
+
+ let row = await validateSuggestionRow(index, suggestion, providerName);
+ if (!row) {
+ Assert.ok(false, "Couldn't get suggestion row, stopping click test");
+ return;
+ }
+
+ // We assume clicking the row will load a page in the current browser.
+ let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ info("Clicking row");
+ EventUtils.synthesizeMouseAtCenter(row, {});
+
+ info("Waiting for load");
+ await loadPromise;
+ await TestUtils.waitForTick();
+
+ info("Checking scalars. Expected: " + JSON.stringify(expected.scalars));
+ QuickSuggestTestUtils.assertScalars(expected.scalars);
+
+ info("Checking events. Expected: " + JSON.stringify([expected.event]));
+ QuickSuggestTestUtils.assertEvents([expected.event]);
+
+ Assert.equal(
+ expectedPings.length,
+ gleanPingCount.value,
+ "Submitted one Glean ping per expected ping"
+ );
+
+ await PlacesUtils.history.clear();
+
+ info("Finished click test");
+}
+
+/**
+ * Helper for `doTelemetryTest()` that clicks a result menu command for a
+ * suggestion 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|Array} options.commandOrArray
+ * A command name or array; this is passed directly to
+ * `UrlbarTestUtils.openResultMenuAndClickItem()` as the `commandOrArray` arg,
+ * so see its documentation for details.
+ * @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 doCommandTest({
+ index,
+ suggestion,
+ providerName,
+ commandOrArray,
+ expected,
+ showSuggestion,
+}) {
+ info("Starting command test: " + JSON.stringify({ commandOrArray }));
+
+ Services.telemetry.clearEvents();
+
+ let expectedPings = expected.pings ?? [];
+ let gleanPingCount = watchGleanPings(expectedPings);
+
+ info("Showing suggestion");
+ await showSuggestion();
+
+ let row = await validateSuggestionRow(index, suggestion, providerName);
+ if (!row) {
+ Assert.ok(false, "Couldn't get suggestion row, stopping click test");
+ return;
+ }
+
+ let command =
+ typeof commandOrArray == "string"
+ ? commandOrArray
+ : commandOrArray[commandOrArray.length - 1];
+
+ let loadPromise;
+ if (command == "help") {
+ // We assume clicking "help" will load a page in a new tab.
+ loadPromise = BrowserTestUtils.waitForNewTab(gBrowser);
+ }
+
+ info("Clicking command");
+ await UrlbarTestUtils.openResultMenuAndClickItem(window, commandOrArray, {
+ resultIndex: index,
+ openByMouse: true,
+ });
+
+ if (loadPromise) {
+ info("Waiting for load");
+ await loadPromise;
+ await TestUtils.waitForTick();
+ if (command == "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]);
+
+ Assert.equal(
+ expectedPings.length,
+ gleanPingCount.value,
+ "Submitted one Glean ping per expected ping"
+ );
+
+ if (command == "dismiss") {
+ await QuickSuggest.blockedSuggestions.clear();
+ }
+ await PlacesUtils.history.clear();
+
+ info("Finished command test: " + JSON.stringify({ commandOrArray }));
+}
+
+/**
+ * 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;
+}
+
+function watchGleanPings(pings) {
+ let countObject = { value: 0 };
+
+ let checkPing = (ping, next) => {
+ countObject.value++;
+ _assertGleanPing(ping);
+ if (next) {
+ GleanPings.quickSuggest.testBeforeNextSubmit(next);
+ }
+ };
+
+ // Build the chain of `testBeforeNextSubmit`s backwards.
+ let next = undefined;
+ pings
+ .slice()
+ .reverse()
+ .forEach(ping => {
+ next = checkPing.bind(null, ping, next);
+ });
+ if (next) {
+ GleanPings.quickSuggest.testBeforeNextSubmit(next);
+ }
+
+ return countObject;
+}
+
+function _assertGleanPing(ping) {
+ Assert.equal(Glean.quickSuggest.pingType.testGetValue(), ping.type);
+ const keymap = {
+ // present in all pings
+ source: Glean.quickSuggest.source,
+ match_type: Glean.quickSuggest.matchType,
+ position: Glean.quickSuggest.position,
+ suggested_index: Glean.quickSuggest.suggestedIndex,
+ suggested_index_relative_to_group:
+ Glean.quickSuggest.suggestedIndexRelativeToGroup,
+ improve_suggest_experience_checked:
+ Glean.quickSuggest.improveSuggestExperience,
+ block_id: Glean.quickSuggest.blockId,
+ advertiser: Glean.quickSuggest.advertiser,
+ request_id: Glean.quickSuggest.requestId,
+ context_id: Glean.quickSuggest.contextId,
+ // impression and click pings
+ reporting_url: Glean.quickSuggest.reportingUrl,
+ // impression ping
+ is_clicked: Glean.quickSuggest.isClicked,
+ // block/dismiss ping
+ iab_category: Glean.quickSuggest.iabCategory,
+ };
+ for (let [key, value] of Object.entries(ping.payload)) {
+ Assert.ok(key in keymap, `A Glean metric exists for field ${key}`);
+
+ // Merino results may contain empty strings, but Glean will represent these
+ // as nulls.
+ if (value === "") {
+ value = null;
+ }
+
+ Assert.equal(
+ keymap[key].testGetValue(),
+ value ?? null,
+ `Glean metric field ${key} should be the expected value`
+ );
+ }
+}
+
+/**
+ * Adds two tasks: One with the Rust backend disabled and one with it enabled.
+ * The names of the task functions will be the name of the passed-in task
+ * function appended with "_rustDisabled" and "_rustEnabled" respectively. Call
+ * with the usual `add_task()` arguments.
+ *
+ * @param {...any} args
+ * The usual `add_task()` arguments.
+ */
+function add_tasks_with_rust(...args) {
+ let taskFnIndex = args.findIndex(a => typeof a == "function");
+ let taskFn = args[taskFnIndex];
+
+ for (let rustEnabled of [false, true]) {
+ let newTaskFn = async (...taskFnArgs) => {
+ info("add_tasks_with_rust: Setting rustEnabled: " + rustEnabled);
+ UrlbarPrefs.set("quicksuggest.rustEnabled", rustEnabled);
+ info("add_tasks_with_rust: Done setting rustEnabled: " + rustEnabled);
+
+ // The current backend may now start syncing, so wait for it to finish.
+ info("add_tasks_with_rust: Forcing sync");
+ await QuickSuggestTestUtils.forceSync();
+ info("add_tasks_with_rust: Done forcing sync");
+
+ let rv;
+ try {
+ info(
+ "add_tasks_with_rust: Calling original task function: " + taskFn.name
+ );
+ rv = await taskFn(...taskFnArgs);
+ } finally {
+ info(
+ "add_tasks_with_rust: Done calling original task function: " +
+ taskFn.name
+ );
+ info("add_tasks_with_rust: Clearing rustEnabled");
+ UrlbarPrefs.clear("quicksuggest.rustEnabled");
+ info("add_tasks_with_rust: Done clearing rustEnabled");
+
+ // The current backend may now start syncing, so wait for it to finish.
+ info("add_tasks_with_rust: Forcing sync");
+ await QuickSuggestTestUtils.forceSync();
+ info("add_tasks_with_rust: Done forcing sync");
+ }
+ return rv;
+ };
+
+ Object.defineProperty(newTaskFn, "name", {
+ value: taskFn.name + (rustEnabled ? "_rustEnabled" : "_rustDisabled"),
+ });
+ let addTaskArgs = [...args];
+ addTaskArgs[taskFnIndex] = newTaskFn;
+ add_task(...addTaskArgs);
+ }
+}