diff options
Diffstat (limited to 'browser/components/search/test/browser/browser_search_telemetry_adImpression_component.js')
-rw-r--r-- | browser/components/search/test/browser/browser_search_telemetry_adImpression_component.js | 401 |
1 files changed, 401 insertions, 0 deletions
diff --git a/browser/components/search/test/browser/browser_search_telemetry_adImpression_component.js b/browser/components/search/test/browser/browser_search_telemetry_adImpression_component.js new file mode 100644 index 0000000000..c9a4c6a8be --- /dev/null +++ b/browser/components/search/test/browser/browser_search_telemetry_adImpression_component.js @@ -0,0 +1,401 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { SearchSERPTelemetry, SearchSERPTelemetryUtils } = + ChromeUtils.importESModule("resource:///modules/SearchSERPTelemetry.sys.mjs"); + +const WINDOW_HEIGHT = 768; +const WINDOW_WIDTH = 1024; + +// Note: example.org is used for the SERP page, and example.com is used to serve +// the ads. This is done to simulate different domains like the real servers. +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"], + extraAdServersRegexps: [/^https:\/\/example\.com\/ad/], + components: [ + { + type: SearchSERPTelemetryUtils.COMPONENTS.AD_CAROUSEL, + included: { + parent: { + selector: ".moz-carousel", + }, + children: [ + { + selector: ".moz-carousel-card", + countChildren: true, + }, + ], + }, + }, + { + type: SearchSERPTelemetryUtils.COMPONENTS.REFINED_SEARCH_BUTTONS, + included: { + parent: { + selector: ".refined-search-buttons", + }, + children: [ + { + selector: "a", + }, + ], + }, + topDown: true, + }, + { + type: SearchSERPTelemetryUtils.COMPONENTS.AD_LINK, + included: { + parent: { + selector: ".moz_ad", + }, + children: [ + { + selector: ".multi-col", + type: SearchSERPTelemetryUtils.COMPONENTS.AD_SITELINK, + }, + ], + }, + excluded: { + parent: { + selector: ".rhs", + }, + }, + }, + { + type: SearchSERPTelemetryUtils.COMPONENTS.AD_SIDEBAR, + included: { + parent: { + selector: ".rhs", + }, + }, + }, + { + type: SearchSERPTelemetryUtils.COMPONENTS.AD_LINK, + default: true, + }, + ], + }, +]; + +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 promiseAdImpressionReceived() { + return TestUtils.waitForCondition(() => { + let adImpressions = Glean.serp.adImpression.testGetValue() ?? []; + return adImpressions.length; + }, "Should have received an ad impression."); +} + +async function promiseResize(width, height) { + return TestUtils.waitForCondition(() => { + return window.outerWidth === width && window.outerHeight === height; + }, "Waiting for window to resize"); +} + +// sharedData messages are only passed to the child on idle. Therefore +// we wait for a few idles to try and ensure the messages have been able +// to be passed across and handled. +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], + ], + }); + + // The tests evaluate whether or not ads are visible depending on whether + // they are within the view of the window. To ensure the test results + // are consistent regardless of where they are launched, + // set the window size to something reasonable. + let originalWidth = window.outerWidth; + let originalHeight = window.outerHeight; + window.resizeTo(WINDOW_WIDTH, WINDOW_HEIGHT); + await promiseResize(WINDOW_WIDTH, WINDOW_HEIGHT); + + registerCleanupFunction(async () => { + SearchSERPTelemetry.overrideSearchTelemetryForTests(); + Services.telemetry.canRecordExtended = oldCanRecord; + window.resizeTo(originalWidth, originalHeight); + await promiseResize(originalWidth, originalHeight); + resetTelemetry(); + }); +}); + +add_task(async function test_ad_impressions_with_one_carousel() { + resetTelemetry(); + let url = getSERPUrl("searchTelemetryAd_components_carousel.html"); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); + + await promiseAdImpressionReceived(); + + assertAdImpressionEvents([ + { + component: SearchSERPTelemetryUtils.COMPONENTS.AD_CAROUSEL, + ads_loaded: "4", + ads_visible: "3", + ads_hidden: "0", + }, + ]); + + BrowserTestUtils.removeTab(tab); +}); + +// This is to ensure we're not counting two carousel components as two +// separate components but as one record with a sum of the results. +add_task(async function test_ad_impressions_with_two_carousels() { + resetTelemetry(); + let url = getSERPUrl("searchTelemetryAd_components_carousel_doubled.html"); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); + + // This is to ensure we've seen the other carousel regardless the + // size of the browser window. + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + let el = content.document + .getElementById("second-ad") + .getBoundingClientRect(); + // The 100 is just to guarantee we've scrolled past the element. + content.scrollTo(0, el.top + el.height + 100); + }); + + await promiseAdImpressionReceived(); + + assertAdImpressionEvents([ + { + component: SearchSERPTelemetryUtils.COMPONENTS.AD_CAROUSEL, + ads_loaded: "8", + ads_visible: "6", + ads_hidden: "0", + }, + ]); + + BrowserTestUtils.removeTab(tab); +}); + +add_task( + async function test_ad_impressions_with_carousels_with_outer_container() { + resetTelemetry(); + let url = getSERPUrl( + "searchTelemetryAd_components_carousel_outer_container.html" + ); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); + + await promiseAdImpressionReceived(); + + assertAdImpressionEvents([ + { + component: SearchSERPTelemetryUtils.COMPONENTS.AD_CAROUSEL, + ads_loaded: "4", + ads_visible: "3", + ads_hidden: "0", + }, + ]); + + BrowserTestUtils.removeTab(tab); + } +); + +add_task(async function test_ad_impressions_with_carousels_tabhistory() { + resetTelemetry(); + let url = getSERPUrl("searchTelemetryAd_components_carousel.html"); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); + + await promiseAdImpressionReceived(); + + // Reset telemetry because we care about the telemetry upon going back. + resetTelemetry(); + + let browserLoadedPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser); + BrowserTestUtils.loadURIString( + tab.linkedBrowser, + "https://www.example.com/some_url" + ); + await browserLoadedPromise; + + let pageShowPromise = BrowserTestUtils.waitForContentEvent( + tab.linkedBrowser, + "pageshow" + ); + tab.linkedBrowser.goBack(); + await pageShowPromise; + + await promiseAdImpressionReceived(); + + assertAdImpressionEvents([ + { + component: SearchSERPTelemetryUtils.COMPONENTS.AD_CAROUSEL, + ads_loaded: "4", + ads_visible: "3", + ads_hidden: "0", + }, + ]); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_ad_impressions_with_hidden_carousels() { + resetTelemetry(); + let url = getSERPUrl("searchTelemetryAd_components_carousel_hidden.html"); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); + + await promiseAdImpressionReceived(); + + assertAdImpressionEvents([ + { + component: SearchSERPTelemetryUtils.COMPONENTS.AD_CAROUSEL, + ads_loaded: "4", + ads_visible: "0", + ads_hidden: "4", + }, + ]); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_ad_impressions_with_carousel_scrolled_left() { + resetTelemetry(); + let url = getSERPUrl( + "searchTelemetryAd_components_carousel_first_element_non_visible.html" + ); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); + + await promiseAdImpressionReceived(); + + assertAdImpressionEvents([ + { + component: SearchSERPTelemetryUtils.COMPONENTS.AD_CAROUSEL, + ads_loaded: "4", + ads_visible: "2", + ads_hidden: "0", + }, + ]); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_ad_impressions_with_carousel_below_the_fold() { + resetTelemetry(); + let url = getSERPUrl( + "searchTelemetryAd_components_carousel_below_the_fold.html" + ); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); + + await promiseAdImpressionReceived(); + + assertAdImpressionEvents([ + { + component: SearchSERPTelemetryUtils.COMPONENTS.AD_CAROUSEL, + ads_loaded: "4", + ads_visible: "0", + ads_hidden: "0", + }, + ]); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_ad_impressions_with_text_links() { + resetTelemetry(); + let url = getSERPUrl("searchTelemetryAd_components_text.html"); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); + + await promiseAdImpressionReceived(); + + assertAdImpressionEvents([ + { + component: SearchSERPTelemetryUtils.COMPONENTS.AD_SITELINK, + ads_loaded: "1", + ads_visible: "1", + ads_hidden: "0", + }, + { + component: SearchSERPTelemetryUtils.COMPONENTS.AD_LINK, + ads_loaded: "2", + ads_visible: "2", + ads_hidden: "0", + }, + { + component: SearchSERPTelemetryUtils.COMPONENTS.AD_SIDEBAR, + ads_loaded: "1", + ads_visible: "1", + ads_hidden: "0", + }, + ]); + BrowserTestUtils.removeTab(tab); +}); + +// An ad is considered visible if at least one link is within the viewable +// content area when the impression was taken. Since the user can scroll +// the page before ad impression is recorded, we should ensure that an +// ad that was scrolled onto the screen before the impression is taken is +// properly recorded. Additionally, some ads might have a large content +// area that extends beyond the viewable area, but as long as a single +// ad link was viewable within the area, we should count the ads as visible. +add_task(async function test_ad_visibility() { + resetTelemetry(); + let url = getSERPUrl("searchTelemetryAd_components_visibility.html"); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => { + let el = content.document + .getElementById("second-ad") + .getBoundingClientRect(); + // The 100 is just to guarantee we've scrolled past the element. + content.scrollTo(0, el.top + el.height + 100); + }); + + await promiseAdImpressionReceived(); + + assertAdImpressionEvents([ + { + component: SearchSERPTelemetryUtils.COMPONENTS.AD_LINK, + ads_loaded: "6", + ads_visible: "4", + ads_hidden: "0", + }, + ]); + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_impressions_without_ads() { + resetTelemetry(); + let url = getSERPUrl("searchTelemetryAd_searchbox_with_content.html"); + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); + + await promiseAdImpressionReceived(); + + assertAdImpressionEvents([ + { + component: SearchSERPTelemetryUtils.COMPONENTS.REFINED_SEARCH_BUTTONS, + ads_loaded: "1", + ads_visible: "1", + ads_hidden: "0", + }, + ]); + BrowserTestUtils.removeTab(tab); +}); |