summaryrefslogtreecommitdiffstats
path: root/browser/components/search/test/browser/browser_search_telemetry_engagement_target.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/search/test/browser/browser_search_telemetry_engagement_target.js')
-rw-r--r--browser/components/search/test/browser/browser_search_telemetry_engagement_target.js433
1 files changed, 433 insertions, 0 deletions
diff --git a/browser/components/search/test/browser/browser_search_telemetry_engagement_target.js b/browser/components/search/test/browser/browser_search_telemetry_engagement_target.js
new file mode 100644
index 0000000000..d29169b776
--- /dev/null
+++ b/browser/components/search/test/browser/browser_search_telemetry_engagement_target.js
@@ -0,0 +1,433 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * These tests load SERPs and click on links that can either be ads or non ads
+ * and verifies that the engagement events and the target associated with them
+ * are correct.
+ */
+
+"use strict";
+
+const { SearchSERPTelemetry, SearchSERPTelemetryUtils } =
+ ChromeUtils.importESModule("resource:///modules/SearchSERPTelemetry.sys.mjs");
+
+const TEST_PROVIDER_INFO = [
+ {
+ telemetryId: "example",
+ searchPageRegexp:
+ /^https:\/\/example.org\/browser\/browser\/components\/search\/test\/browser\/searchTelemetryAd_/,
+ queryParamName: "s",
+ codeParamName: "abc",
+ taggedCodes: ["ff"],
+ adServerAttributes: ["mozAttr"],
+ nonAdsLinkRegexps: [
+ /^https:\/\/example.org\/browser\/browser\/components\/search\/test\/browser\/searchTelemetryAd_nonAdsLink_redirect.html/,
+ ],
+ extraAdServersRegexps: [/^https:\/\/example\.com\/ad/],
+ components: [
+ {
+ type: SearchSERPTelemetryUtils.COMPONENTS.AD_CAROUSEL,
+ included: {
+ parent: {
+ selector: ".moz-carousel",
+ },
+ children: [
+ {
+ selector: ".moz-carousel-card",
+ countChildren: true,
+ },
+ ],
+ related: {
+ selector: "button",
+ },
+ },
+ },
+ {
+ type: SearchSERPTelemetryUtils.COMPONENTS.AD_LINK,
+ included: {
+ parent: {
+ selector: ".moz_ad",
+ },
+ children: [
+ {
+ selector: ".multi-col",
+ type: SearchSERPTelemetryUtils.COMPONENTS.AD_SITELINK,
+ },
+ ],
+ related: {
+ selector: "button",
+ },
+ },
+ excluded: {
+ parent: {
+ selector: ".rhs",
+ },
+ },
+ },
+ {
+ type: SearchSERPTelemetryUtils.COMPONENTS.INCONTENT_SEARCHBOX,
+ included: {
+ parent: {
+ selector: "form",
+ },
+ children: [
+ {
+ // This isn't contained in any of the HTML examples but the
+ // presence of the entry ensures that if it is not found during
+ // a topDown examination, the next element in the array is
+ // inspected and found.
+ selector: "textarea",
+ },
+ {
+ selector: "input",
+ },
+ ],
+ related: {
+ selector: "div",
+ },
+ },
+ topDown: true,
+ },
+ {
+ type: SearchSERPTelemetryUtils.COMPONENTS.AD_LINK,
+ default: true,
+ },
+ ],
+ },
+];
+
+// This is used to check if not providing an nonAdsLinkRegexp can still
+// reliably categorize non_ads_links.
+const TEST_PROVIDER_INFO_NO_NON_ADS_REGEXP = [
+ {
+ ...TEST_PROVIDER_INFO[0],
+ nonAdsLinkRegexps: [],
+ },
+];
+
+function getSERPUrl(page, organic = false) {
+ let url =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.org"
+ ) + page;
+ return `${url}?s=test${organic ? "" : "&abc=ff"}`;
+}
+
+async function promiseImpressionReceived() {
+ return TestUtils.waitForCondition(() => {
+ let adImpressions = Glean.serp.adImpression.testGetValue() ?? [];
+ return adImpressions.length;
+ }, "Should have received an ad impression.");
+}
+
+async function waitForIdle() {
+ for (let i = 0; i < 10; i++) {
+ await new Promise(resolve => Services.tm.idleDispatchToMainThread(resolve));
+ }
+}
+
+add_setup(async function () {
+ SearchSERPTelemetry.overrideSearchTelemetryForTests(TEST_PROVIDER_INFO);
+ await waitForIdle();
+ // Enable local telemetry recording for the duration of the tests.
+ let oldCanRecord = Services.telemetry.canRecordExtended;
+ Services.telemetry.canRecordExtended = true;
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.search.log", true],
+ ["browser.search.serpEventTelemetry.enabled", true],
+ ],
+ });
+
+ registerCleanupFunction(async () => {
+ SearchSERPTelemetry.overrideSearchTelemetryForTests();
+ Services.telemetry.canRecordExtended = oldCanRecord;
+ resetTelemetry();
+ });
+});
+
+// This test ensures clicking a non-first link in a component registers the
+// proper component. This is because the first link of a component does the
+// heavy lifting in finding the parent and best categorization of the
+// component. Subsequent anchors that have the same parent get grouped into it.
+// Additionally, this test deliberately has ads with different paths so that
+// there are no collisions in hrefs.
+add_task(async function test_click_second_ad_in_component() {
+ resetTelemetry();
+ let url = getSERPUrl("searchTelemetryAd_components_text.html");
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+ await waitForPageWithAdImpressions();
+
+ let pageLoadPromise = BrowserTestUtils.waitForLocationChange(gBrowser);
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#deep_ad_sitelink",
+ {},
+ tab.linkedBrowser
+ );
+ await pageLoadPromise;
+
+ assertImpressionEvents([
+ {
+ impression: {
+ provider: "example",
+ tagged: "true",
+ partner_code: "ff",
+ source: "unknown",
+ is_shopping_page: "false",
+ shopping_tab_displayed: "false",
+ },
+ engagements: [
+ {
+ action: SearchSERPTelemetryUtils.ACTIONS.CLICKED,
+ target: SearchSERPTelemetryUtils.COMPONENTS.AD_SITELINK,
+ },
+ ],
+ },
+ ]);
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// If a provider appends query parameters to a link after the page has been
+// parsed, we should still be able to record the click.
+add_task(async function test_click_ads_link_modified() {
+ resetTelemetry();
+
+ let url = getSERPUrl("searchTelemetryAd_components_text.html");
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+ await waitForPageWithAdImpressions();
+
+ let browserLoadedPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ let target = content.document.getElementById("deep_ad_sitelink");
+ let href = target.getAttribute("href");
+ target.setAttribute("href", href + "?foo=bar");
+ content.document.getElementById("deep_ad_sitelink").click();
+ });
+ await browserLoadedPromise;
+
+ assertImpressionEvents([
+ {
+ impression: {
+ provider: "example",
+ tagged: "true",
+ partner_code: "ff",
+ source: "unknown",
+ is_shopping_page: "false",
+ shopping_tab_displayed: "false",
+ },
+ engagements: [
+ {
+ action: SearchSERPTelemetryUtils.ACTIONS.CLICKED,
+ target: SearchSERPTelemetryUtils.COMPONENTS.AD_SITELINK,
+ },
+ ],
+ },
+ ]);
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Search box is a special case which has to be tracked in the child process.
+add_task(async function test_click_and_submit_incontent_searchbox() {
+ resetTelemetry();
+ let url = getSERPUrl("searchTelemetryAd_searchbox.html");
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+ await waitForPageWithAdImpressions();
+
+ // Click on the searchbox.
+ let pageLoadPromise = BrowserTestUtils.waitForLocationChange(gBrowser, url);
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "form input",
+ {},
+ tab.linkedBrowser
+ );
+ EventUtils.synthesizeKey("KEY_Enter");
+ await pageLoadPromise;
+
+ await TestUtils.waitForCondition(() => {
+ return Glean.serp.impression?.testGetValue()?.length == 2;
+ }, "Should have two impressions.");
+
+ assertImpressionEvents([
+ {
+ impression: {
+ provider: "example",
+ tagged: "true",
+ partner_code: "ff",
+ source: "unknown",
+ is_shopping_page: "false",
+ shopping_tab_displayed: "false",
+ },
+ engagements: [
+ {
+ action: SearchSERPTelemetryUtils.ACTIONS.CLICKED,
+ target: SearchSERPTelemetryUtils.COMPONENTS.INCONTENT_SEARCHBOX,
+ },
+ {
+ action: SearchSERPTelemetryUtils.ACTIONS.SUBMITTED,
+ target: SearchSERPTelemetryUtils.COMPONENTS.INCONTENT_SEARCHBOX,
+ },
+ ],
+ },
+ {
+ impression: {
+ provider: "example",
+ tagged: "true",
+ partner_code: "ff",
+ source: "follow_on_from_refine_on_incontent_search",
+ is_shopping_page: "false",
+ shopping_tab_displayed: "false",
+ },
+ },
+ ]);
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Click an auto-suggested term. The element that is clicked is related
+// to the searchbox but not in search-telemetry-v2 because it can be too
+// difficult to determine ahead of time since the elements are generated
+// dynamically. So instead it should listen to an element higher in the DOM.
+add_task(async function test_click_autosuggest() {
+ resetTelemetry();
+ let url = getSERPUrl("searchTelemetryAd_searchbox.html");
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+ await waitForPageWithAdImpressions();
+
+ // Click an autosuggested term.
+ let pageLoadPromise = BrowserTestUtils.waitForLocationChange(gBrowser, url);
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#suggest",
+ {},
+ tab.linkedBrowser
+ );
+ await pageLoadPromise;
+
+ await TestUtils.waitForCondition(() => {
+ return Glean.serp.impression?.testGetValue()?.length == 2;
+ }, "Should have two impressions.");
+
+ assertImpressionEvents([
+ {
+ impression: {
+ provider: "example",
+ tagged: "true",
+ partner_code: "ff",
+ source: "unknown",
+ is_shopping_page: "false",
+ shopping_tab_displayed: "false",
+ },
+ engagements: [
+ {
+ action: SearchSERPTelemetryUtils.ACTIONS.SUBMITTED,
+ target: SearchSERPTelemetryUtils.COMPONENTS.INCONTENT_SEARCHBOX,
+ },
+ ],
+ },
+ {
+ impression: {
+ provider: "example",
+ tagged: "true",
+ partner_code: "ff",
+ source: "follow_on_from_refine_on_incontent_search",
+ is_shopping_page: "false",
+ shopping_tab_displayed: "false",
+ },
+ },
+ ]);
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Carousel related buttons expand content.
+add_task(async function test_click_carousel_expand() {
+ resetTelemetry();
+ let url = getSERPUrl("searchTelemetryAd_components_carousel.html");
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+ await waitForPageWithAdImpressions();
+
+ // Click a button that is expected to expand.
+ await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ content.document.querySelector("button").click();
+ });
+
+ assertImpressionEvents([
+ {
+ impression: {
+ provider: "example",
+ tagged: "true",
+ partner_code: "ff",
+ source: "unknown",
+ is_shopping_page: "false",
+ shopping_tab_displayed: "false",
+ },
+ engagements: [
+ {
+ action: SearchSERPTelemetryUtils.ACTIONS.EXPANDED,
+ target: SearchSERPTelemetryUtils.COMPONENTS.AD_CAROUSEL,
+ },
+ ],
+ },
+ ]);
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// This test clicks a link that has apostrophes in the both the path and list
+// of query parameters, and uses search telemetry with no nonAdsRegexps defined,
+// which will force us to cache every non ads link in a map and pass it back to
+// the parent.
+// If this test fails, it means we're doing the conversion wrong, because when
+// we observe the clicked URL in the parent process, it should look exactly the
+// same as how it was saved in the hrefToComponent map.
+add_task(async function test_click_link_with_special_characters_in_path() {
+ SearchSERPTelemetry.overrideSearchTelemetryForTests(
+ TEST_PROVIDER_INFO_NO_NON_ADS_REGEXP
+ );
+ await waitForIdle();
+
+ resetTelemetry();
+ let url = getSERPUrl("searchTelemetryAd_components_text.html");
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+ await waitForPageWithAdImpressions();
+
+ let pageLoadPromise = BrowserTestUtils.waitForLocationChange(
+ gBrowser,
+ "https://example.com/path'?hello_world&foo=bar%27s"
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#non_ads_link_with_special_characters_in_path",
+ {},
+ tab.linkedBrowser
+ );
+ await pageLoadPromise;
+
+ assertImpressionEvents([
+ {
+ impression: {
+ provider: "example",
+ tagged: "true",
+ partner_code: "ff",
+ source: "unknown",
+ is_shopping_page: "false",
+ shopping_tab_displayed: "false",
+ },
+ engagements: [
+ {
+ action: SearchSERPTelemetryUtils.ACTIONS.CLICKED,
+ target: SearchSERPTelemetryUtils.COMPONENTS.NON_ADS_LINK,
+ },
+ ],
+ },
+ ]);
+
+ BrowserTestUtils.removeTab(tab);
+
+ // Reset state for other tests.
+ SearchSERPTelemetry.overrideSearchTelemetryForTests(TEST_PROVIDER_INFO);
+ await waitForIdle();
+});