From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- browser/components/search/test/browser/head.js | 395 +++++++++++++++++++++++++ 1 file changed, 395 insertions(+) create mode 100644 browser/components/search/test/browser/head.js (limited to 'browser/components/search/test/browser/head.js') diff --git a/browser/components/search/test/browser/head.js b/browser/components/search/test/browser/head.js new file mode 100644 index 0000000000..730ea45c12 --- /dev/null +++ b/browser/components/search/test/browser/head.js @@ -0,0 +1,395 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +ChromeUtils.defineESModuleGetters(this, { + ADLINK_CHECK_TIMEOUT_MS: + "resource:///actors/SearchSERPTelemetryChild.sys.mjs", + AddonTestUtils: "resource://testing-common/AddonTestUtils.sys.mjs", + CustomizableUITestUtils: + "resource://testing-common/CustomizableUITestUtils.sys.mjs", + FormHistory: "resource://gre/modules/FormHistory.sys.mjs", + FormHistoryTestUtils: + "resource://testing-common/FormHistoryTestUtils.sys.mjs", + PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", + SearchTestUtils: "resource://testing-common/SearchTestUtils.sys.mjs", + SearchUtils: "resource://gre/modules/SearchUtils.sys.mjs", + TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs", + UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.sys.mjs", +}); + +ChromeUtils.defineLazyGetter(this, "UrlbarTestUtils", () => { + const { UrlbarTestUtils: module } = ChromeUtils.importESModule( + "resource://testing-common/UrlbarTestUtils.sys.mjs" + ); + module.init(this); + return module; +}); + +let gCUITestUtils = new CustomizableUITestUtils(window); + +AddonTestUtils.initMochitest(this); +SearchTestUtils.init(this); + +const UUID_REGEX = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + +/** + * Recursively compare two objects and check that every property of expectedObj has the same value + * on actualObj. + * + * @param {object} expectedObj + * The expected object to find. + * @param {object} actualObj + * The object to inspect. + * @param {string} name + * The name of the engine, used for test detail logging. + */ +function isSubObjectOf(expectedObj, actualObj, name) { + for (let prop in expectedObj) { + if (typeof expectedObj[prop] == "function") { + continue; + } + if (expectedObj[prop] instanceof Object) { + is( + actualObj[prop].length, + expectedObj[prop].length, + name + "[" + prop + "]" + ); + isSubObjectOf( + expectedObj[prop], + actualObj[prop], + name + "[" + prop + "]" + ); + } else { + is(actualObj[prop], expectedObj[prop], name + "[" + prop + "]"); + } + } +} + +function getLocale() { + return Services.locale.requestedLocale || undefined; +} + +function promiseEvent(aTarget, aEventName, aPreventDefault) { + function cancelEvent(event) { + if (aPreventDefault) { + event.preventDefault(); + } + + return true; + } + + return BrowserTestUtils.waitForEvent(aTarget, aEventName, false, cancelEvent); +} + +// Get an array of the one-off buttons. +function getOneOffs() { + let oneOffs = []; + let searchPopup = document.getElementById("PopupSearchAutoComplete"); + let oneOffsContainer = searchPopup.searchOneOffsContainer; + let oneOff = oneOffsContainer.querySelector(".search-panel-one-offs"); + for (oneOff = oneOff.firstChild; oneOff; oneOff = oneOff.nextSibling) { + if (oneOff.nodeType == Node.ELEMENT_NODE) { + oneOffs.push(oneOff); + } + } + return oneOffs; +} + +async function typeInSearchField(browser, text, fieldName) { + await SpecialPowers.spawn( + browser, + [[fieldName, text]], + async function ([contentFieldName, contentText]) { + // Put the focus on the search box. + let searchInput = content.document.getElementById(contentFieldName); + searchInput.focus(); + searchInput.value = contentText; + } + ); +} + +XPCOMUtils.defineLazyGetter(this, "searchCounts", () => { + return Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS"); +}); + +XPCOMUtils.defineLazyGetter(this, "SEARCH_AD_CLICK_SCALARS", () => { + const sources = [ + ...BrowserSearchTelemetry.KNOWN_SEARCH_SOURCES.values(), + "unknown", + ]; + return [ + ...sources.map(v => `browser.search.withads.${v}`), + ...sources.map(v => `browser.search.adclicks.${v}`), + ]; +}); + +// Ad links are processed after a small delay. We need to allow tests to wait +// for that before checking telemetry, otherwise the received values may be +// too small in some cases. +function promiseWaitForAdLinkCheck() { + return new Promise(resolve => + /* eslint-disable-next-line mozilla/no-arbitrary-setTimeout */ + setTimeout(resolve, ADLINK_CHECK_TIMEOUT_MS) + ); +} + +async function assertSearchSourcesTelemetry( + expectedHistograms, + expectedScalars +) { + let histSnapshot = {}; + let scalars = {}; + + // This used to rely on the implied 100ms initial timer of + // TestUtils.waitForCondition. See bug 1515466. + await new Promise(resolve => setTimeout(resolve, 100)); + + await TestUtils.waitForCondition(() => { + histSnapshot = searchCounts.snapshot(); + return ( + Object.getOwnPropertyNames(histSnapshot).length == + Object.getOwnPropertyNames(expectedHistograms).length + ); + }, "should have the correct number of histograms"); + + if (Object.entries(expectedScalars).length) { + await TestUtils.waitForCondition(() => { + scalars = + Services.telemetry.getSnapshotForKeyedScalars("main", false).parent || + {}; + return Object.getOwnPropertyNames(expectedScalars).every( + scalar => scalar in scalars + ); + }, "should have the expected keyed scalars"); + } + + Assert.equal( + Object.getOwnPropertyNames(histSnapshot).length, + Object.getOwnPropertyNames(expectedHistograms).length, + "Should only have one key" + ); + + for (let [key, value] of Object.entries(expectedHistograms)) { + Assert.ok( + key in histSnapshot, + `Histogram should have the expected key: ${key}` + ); + Assert.equal( + histSnapshot[key].sum, + value, + `Should have counted the correct number of visits for ${key}` + ); + } + + for (let [name, value] of Object.entries(expectedScalars)) { + Assert.ok(name in scalars, `Scalar ${name} should have been added.`); + Assert.deepEqual( + scalars[name], + value, + `Should have counted the correct number of visits for ${name}` + ); + } + + for (let name of SEARCH_AD_CLICK_SCALARS) { + Assert.equal( + name in scalars, + name in expectedScalars, + `Should have matched ${name} in scalars and expectedScalars` + ); + } +} + +async function searchInSearchbar(inputText, win = window) { + await new Promise(r => waitForFocus(r, win)); + let sb = win.BrowserSearch.searchBar; + // Write the search query in the searchbar. + sb.focus(); + sb.value = inputText; + sb.textbox.controller.startSearch(inputText); + // Wait for the popup to show. + await BrowserTestUtils.waitForEvent(sb.textbox.popup, "popupshown"); + // And then for the search to complete. + await TestUtils.waitForCondition( + () => + sb.textbox.controller.searchStatus >= + Ci.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH, + "The search in the searchbar must complete." + ); + return sb.textbox.popup; +} + +function clearSearchbarHistory(win = window) { + info("cleanup the search history"); + return FormHistory.update({ op: "remove", fieldname: "searchbar-history" }); +} + +function resetTelemetry() { + searchCounts.clear(); + Services.telemetry.clearScalars(); + Services.fog.testResetFOG(); +} + +/** + * First checks that we get the correct number of recorded Glean impression events + * and the recorded Glean impression events have the correct keys and values. + * + * Then it checks that there are the the correct engagement events associated with the + * impression events. + * + * @param {Array} expectedEvents The expected impression events whose keys and + * values we use to validate the recorded Glean impression events. + */ +function assertImpressionEvents(expectedEvents) { + // A single test might run assertImpressionEvents more than once + // so the Set needs to be cleared or else the impression event + // check will throw. + const impressionIdsSet = new Set(); + + let recordedImpressions = Glean.serp.impression.testGetValue() ?? []; + + Assert.equal( + recordedImpressions.length, + expectedEvents.length, + "Should have the correct number of impressions." + ); + + // Assert the impression events. + for (let [idx, expectedEvent] of expectedEvents.entries()) { + let impressionId = recordedImpressions[idx].extra.impression_id; + Assert.ok( + UUID_REGEX.test(impressionId), + "Should have an impression_id with a valid UUID." + ); + + Assert.ok( + !impressionIdsSet.has(impressionId), + "Should have a unique impression_id." + ); + + impressionIdsSet.add(impressionId); + + // If we want to use deepEqual checks, we have to add the impressionId + // to each impression since they are randomly generated at runtime. + expectedEvent.impression.impression_id = impressionId; + + Assert.deepEqual( + recordedImpressions[idx].extra, + expectedEvent.impression, + "Should have matched impression values." + ); + + // Once the impression check is sufficient, add the impression_id to + // each of the expected engagements for later deep equal checks. + if (expectedEvent.engagements) { + for (let expectedEngagment of expectedEvent.engagements) { + expectedEngagment.impression_id = impressionId; + } + } + } + + // Group engagement events into separate array fetchable by their + // impression_id. + let recordedEngagements = Glean.serp.engagement.testGetValue() ?? []; + let idToEngagements = new Map(); + let totalExpectedEngagements = 0; + + for (let recordedEngagement of recordedEngagements) { + let impressionId = recordedEngagement.extra.impression_id; + Assert.ok( + impressionId, + "Should have an engagement event with an impression_id" + ); + + let arr = idToEngagements.get(impressionId) ?? []; + arr.push(recordedEngagement.extra); + + idToEngagements.set(impressionId, arr); + } + + // Assert the engagement events. + for (let expectedEvent of expectedEvents) { + let impressionId = expectedEvent.impression.impression_id; + let expectedEngagements = expectedEvent.engagements; + if (expectedEngagements) { + let recorded = idToEngagements.get(impressionId); + Assert.deepEqual( + recorded, + expectedEngagements, + "Should have matched engagement values." + ); + totalExpectedEngagements += expectedEngagements.length; + } + } + + Assert.equal( + recordedEngagements.length, + totalExpectedEngagements, + "Should have equal number of engagements." + ); +} + +function assertAdImpressionEvents(expectedAdImpressions) { + let adImpressions = Glean.serp.adImpression.testGetValue() ?? []; + let impressions = Glean.serp.impression.testGetValue() ?? []; + + Assert.equal(impressions.length, 1, "Should have a SERP impression event."); + Assert.equal( + adImpressions.length, + expectedAdImpressions.length, + "Should have equal number of ad impression events." + ); + + expectedAdImpressions = expectedAdImpressions.map(expectedAdImpression => { + expectedAdImpression.impression_id = impressions[0].extra.impression_id; + return expectedAdImpression; + }); + + for (let [index, expectedAdImpression] of expectedAdImpressions.entries()) { + Assert.deepEqual( + adImpressions[index]?.extra, + expectedAdImpression, + "Should have equal values for an ad impression." + ); + } +} + +function assertAbandonmentEvent(expectedAbandonment) { + let recordedAbandonment = Glean.serp.abandonment.testGetValue() ?? []; + + Assert.equal( + recordedAbandonment[0].extra.reason, + expectedAbandonment.abandonment.reason, + "Should have the correct abandonment reason." + ); +} + +async function promiseAdImpressionReceived(num) { + if (num) { + return TestUtils.waitForCondition(() => { + let adImpressions = Glean.serp.adImpression.testGetValue() ?? []; + return adImpressions.length == num; + }, `Should have received ${num} ad impressions.`); + } + return TestUtils.waitForCondition(() => { + let adImpressions = Glean.serp.adImpression.testGetValue() ?? []; + return adImpressions.length; + }, "Should have received an ad impression."); +} + +async function waitForPageWithAdImpressions() { + return new Promise(resolve => { + let listener = win => { + Services.obs.removeObserver( + listener, + "reported-page-with-ad-impressions" + ); + resolve(); + }; + Services.obs.addObserver(listener, "reported-page-with-ad-impressions"); + }); +} + +registerCleanupFunction(async () => { + await PlacesUtils.history.clear(); +}); -- cgit v1.2.3