/* 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/. */ // Tests impression frequency capping for quick suggest results. "use strict"; ChromeUtils.defineESModuleGetters(this, { AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs", setTimeout: "resource://gre/modules/Timer.sys.mjs", }); const REMOTE_SETTINGS_RESULTS = [ { id: 1, url: "http://example.com/sponsored", title: "Sponsored suggestion", keywords: ["sponsored"], click_url: "http://example.com/click", impression_url: "http://example.com/impression", advertiser: "TestAdvertiser", iab_category: "22 - Shopping", }, { id: 2, url: "http://example.com/nonsponsored", title: "Non-sponsored suggestion", keywords: ["nonsponsored"], click_url: "http://example.com/click", impression_url: "http://example.com/impression", advertiser: "TestAdvertiser", iab_category: "5 - Education", }, ]; const EXPECTED_SPONSORED_URLBAR_RESULT = { type: UrlbarUtils.RESULT_TYPE.URL, source: UrlbarUtils.RESULT_SOURCE.SEARCH, heuristic: false, payload: { telemetryType: "adm_sponsored", url: "http://example.com/sponsored", originalUrl: "http://example.com/sponsored", displayUrl: "http://example.com/sponsored", title: "Sponsored suggestion", qsSuggestion: "sponsored", icon: null, isSponsored: true, sponsoredImpressionUrl: "http://example.com/impression", sponsoredClickUrl: "http://example.com/click", sponsoredBlockId: 1, sponsoredAdvertiser: "TestAdvertiser", sponsoredIabCategory: "22 - Shopping", helpUrl: QuickSuggest.HELP_URL, helpL10n: { id: UrlbarPrefs.get("resultMenu") ? "urlbar-result-menu-learn-more-about-firefox-suggest" : "firefox-suggest-urlbar-learn-more", }, isBlockable: UrlbarPrefs.get("quickSuggestBlockingEnabled"), blockL10n: { id: UrlbarPrefs.get("resultMenu") ? "urlbar-result-menu-dismiss-firefox-suggest" : "firefox-suggest-urlbar-block", }, source: "remote-settings", }, }; const EXPECTED_NONSPONSORED_URLBAR_RESULT = { type: UrlbarUtils.RESULT_TYPE.URL, source: UrlbarUtils.RESULT_SOURCE.SEARCH, heuristic: false, payload: { telemetryType: "adm_nonsponsored", url: "http://example.com/nonsponsored", originalUrl: "http://example.com/nonsponsored", displayUrl: "http://example.com/nonsponsored", title: "Non-sponsored suggestion", qsSuggestion: "nonsponsored", icon: null, isSponsored: false, sponsoredImpressionUrl: "http://example.com/impression", sponsoredClickUrl: "http://example.com/click", sponsoredBlockId: 2, sponsoredAdvertiser: "TestAdvertiser", sponsoredIabCategory: "5 - Education", helpUrl: QuickSuggest.HELP_URL, helpL10n: { id: UrlbarPrefs.get("resultMenu") ? "urlbar-result-menu-learn-more-about-firefox-suggest" : "firefox-suggest-urlbar-learn-more", }, isBlockable: UrlbarPrefs.get("quickSuggestBlockingEnabled"), blockL10n: { id: UrlbarPrefs.get("resultMenu") ? "urlbar-result-menu-dismiss-firefox-suggest" : "firefox-suggest-urlbar-block", }, source: "remote-settings", }, }; let gSandbox; let gDateNowStub; let gStartupDateMsStub; add_task(async function init() { UrlbarPrefs.set("quicksuggest.enabled", true); UrlbarPrefs.set("quicksuggest.impressionCaps.sponsoredEnabled", true); UrlbarPrefs.set("quicksuggest.impressionCaps.nonSponsoredEnabled", true); UrlbarPrefs.set("suggest.quicksuggest.nonsponsored", true); UrlbarPrefs.set("suggest.quicksuggest.sponsored", true); UrlbarPrefs.set("bestMatch.enabled", false); // Disable search suggestions so we don't hit the network. Services.prefs.setBoolPref("browser.search.suggest.enabled", false); await QuickSuggestTestUtils.ensureQuickSuggestInit({ remoteSettingsResults: [ { type: "data", attachment: REMOTE_SETTINGS_RESULTS, }, ], }); // Set up a sinon stub for the `Date.now()` implementation inside of // UrlbarProviderQuickSuggest. This lets us test searches performed at // specific times. See `doTimedCallbacks()` for more info. gSandbox = sinon.createSandbox(); gDateNowStub = gSandbox.stub( Cu.getGlobalForObject(UrlbarProviderQuickSuggest).Date, "now" ); // Set up a sinon stub for `UrlbarProviderQuickSuggest._getStartupDateMs()` to // let the test override the startup date. gStartupDateMsStub = gSandbox.stub( QuickSuggest.impressionCaps, "_getStartupDateMs" ); gStartupDateMsStub.returns(0); }); // Tests a single interval. add_task(async function oneInterval() { await doTest({ config: { impression_caps: { sponsored: { custom: [{ interval_s: 3, max_count: 1 }], }, }, }, callback: async () => { await doTimedSearches("sponsored", { 0: { results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], telemetry: { events: [ { object: "hit", extra: { eventDate: "0", intervalSeconds: "3", maxCount: "1", startDate: "0", impressionDate: "0", count: "1", type: "sponsored", eventCount: "1", }, }, ], }, }, 1: { results: [[]], }, 2: { results: [[]], }, 3: { results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], telemetry: { events: [ { object: "reset", extra: { eventDate: "3000", intervalSeconds: "3", maxCount: "1", startDate: "0", impressionDate: "0", count: "1", type: "sponsored", eventCount: "1", }, }, { object: "hit", extra: { eventDate: "3000", intervalSeconds: "3", maxCount: "1", startDate: "3000", impressionDate: "3000", count: "1", type: "sponsored", eventCount: "1", }, }, ], }, }, 4: { results: [[]], }, 5: { results: [[]], }, }); }, }); }); // Tests multiple intervals. add_task(async function multipleIntervals() { await doTest({ config: { impression_caps: { sponsored: { custom: [ { interval_s: 1, max_count: 1 }, { interval_s: 5, max_count: 3 }, { interval_s: 10, max_count: 5 }, ], }, }, }, callback: async () => { await doTimedSearches("sponsored", { // 0s: 1 new impression; 1 impression total 0: { results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], telemetry: { events: [ // hit: interval_s: 1, max_count: 1 { object: "hit", extra: { eventDate: "0", intervalSeconds: "1", maxCount: "1", startDate: "0", impressionDate: "0", count: "1", type: "sponsored", eventCount: "1", }, }, ], }, }, // 1s: 1 new impression; 2 impressions total 1: { results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], telemetry: { events: [ // reset: interval_s: 1, max_count: 1 { object: "reset", extra: { eventDate: "1000", intervalSeconds: "1", maxCount: "1", startDate: "0", impressionDate: "0", count: "1", type: "sponsored", eventCount: "1", }, }, // hit: interval_s: 1, max_count: 1 { object: "hit", extra: { eventDate: "1000", intervalSeconds: "1", maxCount: "1", startDate: "1000", impressionDate: "1000", count: "1", type: "sponsored", eventCount: "1", }, }, ], }, }, // 2s: 1 new impression; 3 impressions total 2: { results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], telemetry: { events: [ // reset: interval_s: 1, max_count: 1 { object: "reset", extra: { eventDate: "2000", intervalSeconds: "1", maxCount: "1", startDate: "1000", impressionDate: "1000", count: "1", type: "sponsored", eventCount: "1", }, }, // hit: interval_s: 1, max_count: 1 { object: "hit", extra: { eventDate: "2000", intervalSeconds: "1", maxCount: "1", startDate: "2000", impressionDate: "2000", count: "1", type: "sponsored", eventCount: "1", }, }, // hit: interval_s: 5, max_count: 3 { object: "hit", extra: { eventDate: "2000", intervalSeconds: "5", maxCount: "3", startDate: "0", impressionDate: "2000", count: "3", type: "sponsored", eventCount: "1", }, }, ], }, }, // 3s: no new impressions; 3 impressions total 3: { results: [[]], telemetry: { events: [ // reset: interval_s: 1, max_count: 1 { object: "reset", extra: { eventDate: "3000", intervalSeconds: "1", maxCount: "1", startDate: "2000", impressionDate: "2000", count: "1", type: "sponsored", eventCount: "1", }, }, ], }, }, // 4s: no new impressions; 3 impressions total 4: { results: [[]], telemetry: { events: [ // reset: interval_s: 1, max_count: 1 { object: "reset", extra: { eventDate: "4000", intervalSeconds: "1", maxCount: "1", startDate: "3000", impressionDate: "2000", count: "0", type: "sponsored", eventCount: "1", }, }, ], }, }, // 5s: 1 new impression; 4 impressions total 5: { results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], telemetry: { events: [ // reset: interval_s: 1, max_count: 1 { object: "reset", extra: { eventDate: "5000", intervalSeconds: "1", maxCount: "1", startDate: "4000", impressionDate: "2000", count: "0", type: "sponsored", eventCount: "1", }, }, // reset: interval_s: 5, max_count: 3 { object: "reset", extra: { eventDate: "5000", intervalSeconds: "5", maxCount: "3", startDate: "0", impressionDate: "2000", count: "3", type: "sponsored", eventCount: "1", }, }, // hit: interval_s: 1, max_count: 1 { object: "hit", extra: { eventDate: "5000", intervalSeconds: "1", maxCount: "1", startDate: "5000", impressionDate: "5000", count: "1", type: "sponsored", eventCount: "1", }, }, ], }, }, // 6s: 1 new impression; 5 impressions total 6: { results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], telemetry: { events: [ // reset: interval_s: 1, max_count: 1 { object: "reset", extra: { eventDate: "6000", intervalSeconds: "1", maxCount: "1", startDate: "5000", impressionDate: "5000", count: "1", type: "sponsored", eventCount: "1", }, }, // hit: interval_s: 1, max_count: 1 { object: "hit", extra: { eventDate: "6000", intervalSeconds: "1", maxCount: "1", startDate: "6000", impressionDate: "6000", count: "1", type: "sponsored", eventCount: "1", }, }, // hit: interval_s: 10, max_count: 5 { object: "hit", extra: { eventDate: "6000", intervalSeconds: "10", maxCount: "5", startDate: "0", impressionDate: "6000", count: "5", type: "sponsored", eventCount: "1", }, }, ], }, }, // 7s: no new impressions; 5 impressions total 7: { results: [[]], telemetry: { events: [ // reset: interval_s: 1, max_count: 1 { object: "reset", extra: { eventDate: "7000", intervalSeconds: "1", maxCount: "1", startDate: "6000", impressionDate: "6000", count: "1", type: "sponsored", eventCount: "1", }, }, ], }, }, // 8s: no new impressions; 5 impressions total 8: { results: [[]], telemetry: { events: [ // reset: interval_s: 1, max_count: 1 { object: "reset", extra: { eventDate: "8000", intervalSeconds: "1", maxCount: "1", startDate: "7000", impressionDate: "6000", count: "0", type: "sponsored", eventCount: "1", }, }, ], }, }, // 9s: no new impressions; 5 impressions total 9: { results: [[]], telemetry: { events: [ // reset: interval_s: 1, max_count: 1 { object: "reset", extra: { eventDate: "9000", intervalSeconds: "1", maxCount: "1", startDate: "8000", impressionDate: "6000", count: "0", type: "sponsored", eventCount: "1", }, }, ], }, }, // 10s: 1 new impression; 6 impressions total 10: { results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], telemetry: { events: [ // reset: interval_s: 1, max_count: 1 { object: "reset", extra: { eventDate: "10000", intervalSeconds: "1", maxCount: "1", startDate: "9000", impressionDate: "6000", count: "0", type: "sponsored", eventCount: "1", }, }, // reset: interval_s: 5, max_count: 3 { object: "reset", extra: { eventDate: "10000", intervalSeconds: "5", maxCount: "3", startDate: "5000", impressionDate: "6000", count: "2", type: "sponsored", eventCount: "1", }, }, // reset: interval_s: 10, max_count: 5 { object: "reset", extra: { eventDate: "10000", intervalSeconds: "10", maxCount: "5", startDate: "0", impressionDate: "6000", count: "5", type: "sponsored", eventCount: "1", }, }, // hit: interval_s: 1, max_count: 1 { object: "hit", extra: { eventDate: "10000", intervalSeconds: "1", maxCount: "1", startDate: "10000", impressionDate: "10000", count: "1", type: "sponsored", eventCount: "1", }, }, ], }, }, // 11s: 1 new impression; 7 impressions total 11: { results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], telemetry: { events: [ // reset: interval_s: 1, max_count: 1 { object: "reset", extra: { eventDate: "11000", intervalSeconds: "1", maxCount: "1", startDate: "10000", impressionDate: "10000", count: "1", type: "sponsored", eventCount: "1", }, }, // hit: interval_s: 1, max_count: 1 { object: "hit", extra: { eventDate: "11000", intervalSeconds: "1", maxCount: "1", startDate: "11000", impressionDate: "11000", count: "1", type: "sponsored", eventCount: "1", }, }, ], }, }, // 12s: 1 new impression; 8 impressions total 12: { results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], telemetry: { events: [ // reset: interval_s: 1, max_count: 1 { object: "reset", extra: { eventDate: "12000", intervalSeconds: "1", maxCount: "1", startDate: "11000", impressionDate: "11000", count: "1", type: "sponsored", eventCount: "1", }, }, // hit: interval_s: 1, max_count: 1 { object: "hit", extra: { eventDate: "12000", intervalSeconds: "1", maxCount: "1", startDate: "12000", impressionDate: "12000", count: "1", type: "sponsored", eventCount: "1", }, }, // hit: interval_s: 5, max_count: 3 { object: "hit", extra: { eventDate: "12000", intervalSeconds: "5", maxCount: "3", startDate: "10000", impressionDate: "12000", count: "3", type: "sponsored", eventCount: "1", }, }, ], }, }, // 13s: no new impressions; 8 impressions total 13: { results: [[]], telemetry: { events: [ // reset: interval_s: 1, max_count: 1 { object: "reset", extra: { eventDate: "13000", intervalSeconds: "1", maxCount: "1", startDate: "12000", impressionDate: "12000", count: "1", type: "sponsored", eventCount: "1", }, }, ], }, }, // 14s: no new impressions; 8 impressions total 14: { results: [[]], telemetry: { events: [ // reset: interval_s: 1, max_count: 1 { object: "reset", extra: { eventDate: "14000", intervalSeconds: "1", maxCount: "1", startDate: "13000", impressionDate: "12000", count: "0", type: "sponsored", eventCount: "1", }, }, ], }, }, // 15s: 1 new impression; 9 impressions total 15: { results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], telemetry: { events: [ // reset: interval_s: 1, max_count: 1 { object: "reset", extra: { eventDate: "15000", intervalSeconds: "1", maxCount: "1", startDate: "14000", impressionDate: "12000", count: "0", type: "sponsored", eventCount: "1", }, }, // reset: interval_s: 5, max_count: 3 { object: "reset", extra: { eventDate: "15000", intervalSeconds: "5", maxCount: "3", startDate: "10000", impressionDate: "12000", count: "3", type: "sponsored", eventCount: "1", }, }, // hit: interval_s: 1, max_count: 1 { object: "hit", extra: { eventDate: "15000", intervalSeconds: "1", maxCount: "1", startDate: "15000", impressionDate: "15000", count: "1", type: "sponsored", eventCount: "1", }, }, ], }, }, // 16s: 1 new impression; 10 impressions total 16: { results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], telemetry: { events: [ // reset: interval_s: 1, max_count: 1 { object: "reset", extra: { eventDate: "16000", intervalSeconds: "1", maxCount: "1", startDate: "15000", impressionDate: "15000", count: "1", type: "sponsored", eventCount: "1", }, }, // hit: interval_s: 1, max_count: 1 { object: "hit", extra: { eventDate: "16000", intervalSeconds: "1", maxCount: "1", startDate: "16000", impressionDate: "16000", count: "1", type: "sponsored", eventCount: "1", }, }, // hit: interval_s: 10, max_count: 5 { object: "hit", extra: { eventDate: "16000", intervalSeconds: "10", maxCount: "5", startDate: "10000", impressionDate: "16000", count: "5", type: "sponsored", eventCount: "1", }, }, ], }, }, // 17s: no new impressions; 10 impressions total 17: { results: [[]], telemetry: { events: [ // reset: interval_s: 1, max_count: 1 { object: "reset", extra: { eventDate: "17000", intervalSeconds: "1", maxCount: "1", startDate: "16000", impressionDate: "16000", count: "1", type: "sponsored", eventCount: "1", }, }, ], }, }, // 18s: no new impressions; 10 impressions total 18: { results: [[]], telemetry: { events: [ // reset: interval_s: 1, max_count: 1 { object: "reset", extra: { eventDate: "18000", intervalSeconds: "1", maxCount: "1", startDate: "17000", impressionDate: "16000", count: "0", type: "sponsored", eventCount: "1", }, }, ], }, }, // 19s: no new impressions; 10 impressions total 19: { results: [[]], telemetry: { events: [ // reset: interval_s: 1, max_count: 1 { object: "reset", extra: { eventDate: "19000", intervalSeconds: "1", maxCount: "1", startDate: "18000", impressionDate: "16000", count: "0", type: "sponsored", eventCount: "1", }, }, ], }, }, // 20s: 1 new impression; 11 impressions total 20: { results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], telemetry: { events: [ // reset: interval_s: 1, max_count: 1 { object: "reset", extra: { eventDate: "20000", intervalSeconds: "1", maxCount: "1", startDate: "19000", impressionDate: "16000", count: "0", type: "sponsored", eventCount: "1", }, }, // reset: interval_s: 5, max_count: 3 { object: "reset", extra: { eventDate: "20000", intervalSeconds: "5", maxCount: "3", startDate: "15000", impressionDate: "16000", count: "2", type: "sponsored", eventCount: "1", }, }, // reset: interval_s: 10, max_count: 5 { object: "reset", extra: { eventDate: "20000", intervalSeconds: "10", maxCount: "5", startDate: "10000", impressionDate: "16000", count: "5", type: "sponsored", eventCount: "1", }, }, // hit: interval_s: 1, max_count: 1 { object: "hit", extra: { eventDate: "20000", intervalSeconds: "1", maxCount: "1", startDate: "20000", impressionDate: "20000", count: "1", type: "sponsored", eventCount: "1", }, }, ], }, }, }); }, }); }); // Tests a lifetime cap. add_task(async function lifetime() { await doTest({ config: { impression_caps: { sponsored: { lifetime: 3, }, }, }, callback: async () => { await doTimedSearches("sponsored", { 0: { results: [ [EXPECTED_SPONSORED_URLBAR_RESULT], [EXPECTED_SPONSORED_URLBAR_RESULT], [EXPECTED_SPONSORED_URLBAR_RESULT], [], ], telemetry: { events: [ { object: "hit", extra: { eventDate: "0", intervalSeconds: "Infinity", maxCount: "3", startDate: "0", impressionDate: "0", count: "3", type: "sponsored", eventCount: "1", }, }, ], }, }, 1: { results: [[]], }, }); }, }); }); // Tests one interval and a lifetime cap together. add_task(async function intervalAndLifetime() { await doTest({ config: { impression_caps: { sponsored: { lifetime: 3, custom: [{ interval_s: 1, max_count: 1 }], }, }, }, callback: async () => { await doTimedSearches("sponsored", { // 0s: 1 new impression; 1 impression total 0: { results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], telemetry: { events: [ // hit: interval_s: 1, max_count: 1 { object: "hit", extra: { eventDate: "0", intervalSeconds: "1", maxCount: "1", startDate: "0", impressionDate: "0", count: "1", type: "sponsored", eventCount: "1", }, }, ], }, }, // 1s: 1 new impression; 2 impressions total 1: { results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], telemetry: { events: [ // reset: interval_s: 1, max_count: 1 { object: "reset", extra: { eventDate: "1000", intervalSeconds: "1", maxCount: "1", startDate: "0", impressionDate: "0", count: "1", type: "sponsored", eventCount: "1", }, }, // hit: interval_s: 1, max_count: 1 { object: "hit", extra: { eventDate: "1000", intervalSeconds: "1", maxCount: "1", startDate: "1000", impressionDate: "1000", count: "1", type: "sponsored", eventCount: "1", }, }, ], }, }, // 2s: 1 new impression; 3 impressions total 2: { results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], telemetry: { events: [ // reset: interval_s: 1, max_count: 1 { object: "reset", extra: { eventDate: "2000", intervalSeconds: "1", maxCount: "1", startDate: "1000", impressionDate: "1000", count: "1", type: "sponsored", eventCount: "1", }, }, // hit: interval_s: 1, max_count: 1 { object: "hit", extra: { eventDate: "2000", intervalSeconds: "1", maxCount: "1", startDate: "2000", impressionDate: "2000", count: "1", type: "sponsored", eventCount: "1", }, }, // hit: interval_s: Infinity, max_count: 3 { object: "hit", extra: { eventDate: "2000", intervalSeconds: "Infinity", maxCount: "3", startDate: "0", impressionDate: "2000", count: "3", type: "sponsored", eventCount: "1", }, }, ], }, }, 3: { results: [[]], telemetry: { events: [ // reset: interval_s: 1, max_count: 1 { object: "reset", extra: { eventDate: "3000", intervalSeconds: "1", maxCount: "1", startDate: "2000", impressionDate: "2000", count: "1", type: "sponsored", eventCount: "1", }, }, ], }, }, }); }, }); }); // Tests multiple intervals and a lifetime cap together. add_task(async function multipleIntervalsAndLifetime() { await doTest({ config: { impression_caps: { sponsored: { lifetime: 4, custom: [ { interval_s: 1, max_count: 1 }, { interval_s: 5, max_count: 3 }, ], }, }, }, callback: async () => { await doTimedSearches("sponsored", { // 0s: 1 new impression; 1 impression total 0: { results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], telemetry: { events: [ // hit: interval_s: 1, max_count: 1 { object: "hit", extra: { eventDate: "0", intervalSeconds: "1", maxCount: "1", startDate: "0", impressionDate: "0", count: "1", type: "sponsored", eventCount: "1", }, }, ], }, }, // 1s: 1 new impression; 2 impressions total 1: { results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], telemetry: { events: [ // reset: interval_s: 1, max_count: 1 { object: "reset", extra: { eventDate: "1000", intervalSeconds: "1", maxCount: "1", startDate: "0", impressionDate: "0", count: "1", type: "sponsored", eventCount: "1", }, }, // hit: interval_s: 1, max_count: 1 { object: "hit", extra: { eventDate: "1000", intervalSeconds: "1", maxCount: "1", startDate: "1000", impressionDate: "1000", count: "1", type: "sponsored", eventCount: "1", }, }, ], }, }, // 2s: 1 new impression; 3 impressions total 2: { results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], telemetry: { events: [ // reset: interval_s: 1, max_count: 1 { object: "reset", extra: { eventDate: "2000", intervalSeconds: "1", maxCount: "1", startDate: "1000", impressionDate: "1000", count: "1", type: "sponsored", eventCount: "1", }, }, // hit: interval_s: 1, max_count: 1 { object: "hit", extra: { eventDate: "2000", intervalSeconds: "1", maxCount: "1", startDate: "2000", impressionDate: "2000", count: "1", type: "sponsored", eventCount: "1", }, }, // hit: interval_s: 5, max_count: 3 { object: "hit", extra: { eventDate: "2000", intervalSeconds: "5", maxCount: "3", startDate: "0", impressionDate: "2000", count: "3", type: "sponsored", eventCount: "1", }, }, ], }, }, // 3s: no new impressions; 3 impressions total 3: { results: [[]], telemetry: { events: [ // reset: interval_s: 1, max_count: 1 { object: "reset", extra: { eventDate: "3000", intervalSeconds: "1", maxCount: "1", startDate: "2000", impressionDate: "2000", count: "1", type: "sponsored", eventCount: "1", }, }, ], }, }, // 4s: no new impressions; 3 impressions total 4: { results: [[]], telemetry: { events: [ // reset: interval_s: 1, max_count: 1 { object: "reset", extra: { eventDate: "4000", intervalSeconds: "1", maxCount: "1", startDate: "3000", impressionDate: "2000", count: "0", type: "sponsored", eventCount: "1", }, }, ], }, }, // 5s: 1 new impression; 4 impressions total 5: { results: [[EXPECTED_SPONSORED_URLBAR_RESULT], []], telemetry: { events: [ // reset: interval_s: 1, max_count: 1 { object: "reset", extra: { eventDate: "5000", intervalSeconds: "1", maxCount: "1", startDate: "4000", impressionDate: "2000", count: "0", type: "sponsored", eventCount: "1", }, }, // reset: interval_s: 5, max_count: 3 { object: "reset", extra: { eventDate: "5000", intervalSeconds: "5", maxCount: "3", startDate: "0", impressionDate: "2000", count: "3", type: "sponsored", eventCount: "1", }, }, // hit: interval_s: 1, max_count: 1 { object: "hit", extra: { eventDate: "5000", intervalSeconds: "1", maxCount: "1", startDate: "5000", impressionDate: "5000", count: "1", type: "sponsored", eventCount: "1", }, }, // hit: interval_s: Infinity, max_count: 4 { object: "hit", extra: { eventDate: "5000", intervalSeconds: "Infinity", maxCount: "4", startDate: "0", impressionDate: "5000", count: "4", type: "sponsored", eventCount: "1", }, }, ], }, }, // 6s: no new impressions; 4 impressions total 6: { results: [[]], telemetry: { events: [ // reset: interval_s: 1, max_count: 1 { object: "reset", extra: { eventDate: "6000", intervalSeconds: "1", maxCount: "1", startDate: "5000", impressionDate: "5000", count: "1", type: "sponsored", eventCount: "1", }, }, ], }, }, // 7s: no new impressions; 4 impressions total 7: { results: [[]], telemetry: { events: [ // reset: interval_s: 1, max_count: 1 { object: "reset", extra: { eventDate: "7000", intervalSeconds: "1", maxCount: "1", startDate: "6000", impressionDate: "5000", count: "0", type: "sponsored", eventCount: "1", }, }, ], }, }, }); }, }); }); // Smoke test for non-sponsored caps. Most tasks use sponsored results and caps, // but sponsored and non-sponsored should behave the same since they use the // same code paths. add_task(async function nonsponsored() { await doTest({ config: { impression_caps: { nonsponsored: { lifetime: 4, custom: [ { interval_s: 1, max_count: 1 }, { interval_s: 5, max_count: 3 }, ], }, }, }, callback: async () => { await doTimedSearches("nonsponsored", { // 0s: 1 new impression; 1 impression total 0: { results: [[EXPECTED_NONSPONSORED_URLBAR_RESULT], []], telemetry: { events: [ // hit: interval_s: 1, max_count: 1 { object: "hit", extra: { eventDate: "0", intervalSeconds: "1", maxCount: "1", startDate: "0", impressionDate: "0", count: "1", type: "nonsponsored", eventCount: "1", }, }, ], }, }, // 1s: 1 new impression; 2 impressions total 1: { results: [[EXPECTED_NONSPONSORED_URLBAR_RESULT], []], telemetry: { events: [ // reset: interval_s: 1, max_count: 1 { object: "reset", extra: { eventDate: "1000", intervalSeconds: "1", maxCount: "1", startDate: "0", impressionDate: "0", count: "1", type: "nonsponsored", eventCount: "1", }, }, // hit: interval_s: 1, max_count: 1 { object: "hit", extra: { eventDate: "1000", intervalSeconds: "1", maxCount: "1", startDate: "1000", impressionDate: "1000", count: "1", type: "nonsponsored", eventCount: "1", }, }, ], }, }, // 2s: 1 new impression; 3 impressions total 2: { results: [[EXPECTED_NONSPONSORED_URLBAR_RESULT], []], telemetry: { events: [ // reset: interval_s: 1, max_count: 1 { object: "reset", extra: { eventDate: "2000", intervalSeconds: "1", maxCount: "1", startDate: "1000", impressionDate: "1000", count: "1", type: "nonsponsored", eventCount: "1", }, }, // hit: interval_s: 1, max_count: 1 { object: "hit", extra: { eventDate: "2000", intervalSeconds: "1", maxCount: "1", startDate: "2000", impressionDate: "2000", count: "1", type: "nonsponsored", eventCount: "1", }, }, // hit: interval_s: 5, max_count: 3 { object: "hit", extra: { eventDate: "2000", intervalSeconds: "5", maxCount: "3", startDate: "0", impressionDate: "2000", count: "3", type: "nonsponsored", eventCount: "1", }, }, ], }, }, // 3s: no new impressions; 3 impressions total 3: { results: [[]], telemetry: { events: [ // reset: interval_s: 1, max_count: 1 { object: "reset", extra: { eventDate: "3000", intervalSeconds: "1", maxCount: "1", startDate: "2000", impressionDate: "2000", count: "1", type: "nonsponsored", eventCount: "1", }, }, ], }, }, // 4s: no new impressions; 3 impressions total 4: { results: [[]], telemetry: { events: [ // reset: interval_s: 1, max_count: 1 { object: "reset", extra: { eventDate: "4000", intervalSeconds: "1", maxCount: "1", startDate: "3000", impressionDate: "2000", count: "0", type: "nonsponsored", eventCount: "1", }, }, ], }, }, // 5s: 1 new impression; 4 impressions total 5: { results: [[EXPECTED_NONSPONSORED_URLBAR_RESULT], []], telemetry: { events: [ // reset: interval_s: 1, max_count: 1 { object: "reset", extra: { eventDate: "5000", intervalSeconds: "1", maxCount: "1", startDate: "4000", impressionDate: "2000", count: "0", type: "nonsponsored", eventCount: "1", }, }, // reset: interval_s: 5, max_count: 3 { object: "reset", extra: { eventDate: "5000", intervalSeconds: "5", maxCount: "3", startDate: "0", impressionDate: "2000", count: "3", type: "nonsponsored", eventCount: "1", }, }, // hit: interval_s: 1, max_count: 1 { object: "hit", extra: { eventDate: "5000", intervalSeconds: "1", maxCount: "1", startDate: "5000", impressionDate: "5000", count: "1", type: "nonsponsored", eventCount: "1", }, }, // hit: interval_s: Infinity, max_count: 4 { object: "hit", extra: { eventDate: "5000", intervalSeconds: "Infinity", maxCount: "4", startDate: "0", impressionDate: "5000", count: "4", type: "nonsponsored", eventCount: "1", }, }, ], }, }, // 6s: no new impressions; 4 impressions total 6: { results: [[]], telemetry: { events: [ // reset: interval_s: 1, max_count: 1 { object: "reset", extra: { eventDate: "6000", intervalSeconds: "1", maxCount: "1", startDate: "5000", impressionDate: "5000", count: "1", type: "nonsponsored", eventCount: "1", }, }, ], }, }, // 7s: no new impressions; 4 impressions total 7: { results: [[]], telemetry: { events: [ // reset: interval_s: 1, max_count: 1 { object: "reset", extra: { eventDate: "7000", intervalSeconds: "1", maxCount: "1", startDate: "6000", impressionDate: "5000", count: "0", type: "nonsponsored", eventCount: "1", }, }, ], }, }, }); }, }); }); // Smoke test for sponsored and non-sponsored caps together. Most tasks use only // sponsored results and caps, but sponsored and non-sponsored should behave the // same since they use the same code paths. add_task(async function sponsoredAndNonsponsored() { await doTest({ config: { impression_caps: { sponsored: { lifetime: 2, }, nonsponsored: { lifetime: 3, }, }, }, callback: async () => { // 1st searches await checkSearch({ name: "sponsored 1", searchString: "sponsored", expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], }); await checkSearch({ name: "nonsponsored 1", searchString: "nonsponsored", expectedResults: [EXPECTED_NONSPONSORED_URLBAR_RESULT], }); await checkTelemetryEvents([]); // 2nd searches await checkSearch({ name: "sponsored 2", searchString: "sponsored", expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], }); await checkSearch({ name: "nonsponsored 2", searchString: "nonsponsored", expectedResults: [EXPECTED_NONSPONSORED_URLBAR_RESULT], }); await checkTelemetryEvents([ { object: "hit", extra: { eventDate: "0", intervalSeconds: "Infinity", maxCount: "2", startDate: "0", impressionDate: "0", count: "2", type: "sponsored", eventCount: "1", }, }, ]); // 3rd searches await checkSearch({ name: "sponsored 3", searchString: "sponsored", expectedResults: [], }); await checkSearch({ name: "nonsponsored 3", searchString: "nonsponsored", expectedResults: [EXPECTED_NONSPONSORED_URLBAR_RESULT], }); await checkTelemetryEvents([ { object: "hit", extra: { eventDate: "0", intervalSeconds: "Infinity", maxCount: "3", startDate: "0", impressionDate: "0", count: "3", type: "nonsponsored", eventCount: "1", }, }, ]); // 4th searches await checkSearch({ name: "sponsored 4", searchString: "sponsored", expectedResults: [], }); await checkSearch({ name: "nonsponsored 4", searchString: "nonsponsored", expectedResults: [], }); await checkTelemetryEvents([]); }, }); }); // Tests with an empty config to make sure results are not capped. add_task(async function emptyConfig() { await doTest({ config: {}, callback: async () => { for (let i = 0; i < 2; i++) { await checkSearch({ name: "sponsored " + i, searchString: "sponsored", expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], }); await checkSearch({ name: "nonsponsored " + i, searchString: "nonsponsored", expectedResults: [EXPECTED_NONSPONSORED_URLBAR_RESULT], }); } await checkTelemetryEvents([]); }, }); }); // Tests with sponsored caps disabled. Non-sponsored should still be capped. add_task(async function sponsoredCapsDisabled() { UrlbarPrefs.set("quicksuggest.impressionCaps.sponsoredEnabled", false); await doTest({ config: { impression_caps: { sponsored: { lifetime: 0, }, nonsponsored: { lifetime: 3, }, }, }, callback: async () => { for (let i = 0; i < 3; i++) { await checkSearch({ name: "sponsored " + i, searchString: "sponsored", expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], }); await checkSearch({ name: "nonsponsored " + i, searchString: "nonsponsored", expectedResults: [EXPECTED_NONSPONSORED_URLBAR_RESULT], }); } await checkTelemetryEvents([ { object: "hit", extra: { eventDate: "0", intervalSeconds: "Infinity", maxCount: "3", startDate: "0", impressionDate: "0", count: "3", type: "nonsponsored", eventCount: "1", }, }, ]); await checkSearch({ name: "sponsored additional", searchString: "sponsored", expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], }); await checkSearch({ name: "nonsponsored additional", searchString: "nonsponsored", expectedResults: [], }); await checkTelemetryEvents([]); }, }); UrlbarPrefs.set("quicksuggest.impressionCaps.sponsoredEnabled", true); }); // Tests with non-sponsored caps disabled. Sponsored should still be capped. add_task(async function nonsponsoredCapsDisabled() { UrlbarPrefs.set("quicksuggest.impressionCaps.nonSponsoredEnabled", false); await doTest({ config: { impression_caps: { sponsored: { lifetime: 3, }, nonsponsored: { lifetime: 0, }, }, }, callback: async () => { for (let i = 0; i < 3; i++) { await checkSearch({ name: "sponsored " + i, searchString: "sponsored", expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], }); await checkSearch({ name: "nonsponsored " + i, searchString: "nonsponsored", expectedResults: [EXPECTED_NONSPONSORED_URLBAR_RESULT], }); } await checkTelemetryEvents([ { object: "hit", extra: { eventDate: "0", intervalSeconds: "Infinity", maxCount: "3", startDate: "0", impressionDate: "0", count: "3", type: "sponsored", eventCount: "1", }, }, ]); await checkSearch({ name: "sponsored additional", searchString: "sponsored", expectedResults: [], }); await checkSearch({ name: "nonsponsored additional", searchString: "nonsponsored", expectedResults: [EXPECTED_NONSPONSORED_URLBAR_RESULT], }); await checkTelemetryEvents([]); }, }); UrlbarPrefs.set("quicksuggest.impressionCaps.nonSponsoredEnabled", true); }); // Tests a config change: 1 interval -> same interval with lower cap, with the // old cap already reached add_task(async function configChange_sameIntervalLowerCap_1() { await doTest({ config: { impression_caps: { sponsored: { custom: [{ interval_s: 3, max_count: 3 }], }, }, }, callback: async () => { await doTimedCallbacks({ 0: async () => { for (let i = 0; i < 3; i++) { await checkSearch({ name: "0s " + i, searchString: "sponsored", expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], }); } await checkSearch({ name: "0s additional", searchString: "sponsored", expectedResults: [], }); await checkTelemetryEvents([ { object: "hit", extra: { eventDate: "0", intervalSeconds: "3", maxCount: "3", startDate: "0", impressionDate: "0", count: "3", type: "sponsored", eventCount: "1", }, }, ]); await QuickSuggestTestUtils.setConfig({ impression_caps: { sponsored: { custom: [{ interval_s: 3, max_count: 1 }], }, }, }); }, 1: async () => { await checkSearch({ name: "1s", searchString: "sponsored", expectedResults: [], }); await checkTelemetryEvents([]); }, 3: async () => { await checkSearch({ name: "3s 0", searchString: "sponsored", expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], }); await checkSearch({ name: "3s additional", searchString: "sponsored", expectedResults: [], }); await checkTelemetryEvents([ { object: "reset", extra: { eventDate: "3000", intervalSeconds: "3", maxCount: "1", startDate: "0", impressionDate: "0", count: "3", type: "sponsored", eventCount: "1", }, }, { object: "hit", extra: { eventDate: "3000", intervalSeconds: "3", maxCount: "1", startDate: "3000", impressionDate: "3000", count: "1", type: "sponsored", eventCount: "1", }, }, ]); }, }); }, }); }); // Tests a config change: 1 interval -> same interval with lower cap, with the // old cap not reached add_task(async function configChange_sameIntervalLowerCap_2() { await doTest({ config: { impression_caps: { sponsored: { custom: [{ interval_s: 3, max_count: 3 }], }, }, }, callback: async () => { await doTimedCallbacks({ 0: async () => { for (let i = 0; i < 2; i++) { await checkSearch({ name: "0s " + i, searchString: "sponsored", expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], }); } await checkTelemetryEvents([]); await QuickSuggestTestUtils.setConfig({ impression_caps: { sponsored: { custom: [{ interval_s: 3, max_count: 1 }], }, }, }); }, 1: async () => { await checkSearch({ name: "1s", searchString: "sponsored", expectedResults: [], }); }, 3: async () => { await checkSearch({ name: "3s 0", searchString: "sponsored", expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], }); await checkSearch({ name: "3s additional", searchString: "sponsored", expectedResults: [], }); await checkTelemetryEvents([ { object: "reset", extra: { eventDate: "3000", intervalSeconds: "3", maxCount: "1", startDate: "0", impressionDate: "0", count: "2", type: "sponsored", eventCount: "1", }, }, { object: "hit", extra: { eventDate: "3000", intervalSeconds: "3", maxCount: "1", startDate: "3000", impressionDate: "3000", count: "1", type: "sponsored", eventCount: "1", }, }, ]); }, }); }, }); }); // Tests a config change: 1 interval -> same interval with higher cap add_task(async function configChange_sameIntervalHigherCap() { await doTest({ config: { impression_caps: { sponsored: { custom: [{ interval_s: 3, max_count: 3 }], }, }, }, callback: async () => { await doTimedCallbacks({ 0: async () => { for (let i = 0; i < 3; i++) { await checkSearch({ name: "0s " + i, searchString: "sponsored", expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], }); } await checkSearch({ name: "0s additional", searchString: "sponsored", expectedResults: [], }); await checkTelemetryEvents([ { object: "hit", extra: { eventDate: "0", intervalSeconds: "3", maxCount: "3", startDate: "0", impressionDate: "0", count: "3", type: "sponsored", eventCount: "1", }, }, ]); await QuickSuggestTestUtils.setConfig({ impression_caps: { sponsored: { custom: [{ interval_s: 3, max_count: 5 }], }, }, }); }, 1: async () => { for (let i = 0; i < 2; i++) { await checkSearch({ name: "1s " + i, searchString: "sponsored", expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], }); } await checkSearch({ name: "1s additional", searchString: "sponsored", expectedResults: [], }); await checkTelemetryEvents([ { object: "hit", extra: { eventDate: "1000", intervalSeconds: "3", maxCount: "5", startDate: "0", impressionDate: "1000", count: "5", type: "sponsored", eventCount: "1", }, }, ]); }, 3: async () => { for (let i = 0; i < 5; i++) { await checkSearch({ name: "3s " + i, searchString: "sponsored", expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], }); } await checkSearch({ name: "3s additional", searchString: "sponsored", expectedResults: [], }); await checkTelemetryEvents([ { object: "reset", extra: { eventDate: "3000", intervalSeconds: "3", maxCount: "5", startDate: "0", impressionDate: "1000", count: "5", type: "sponsored", eventCount: "1", }, }, { object: "hit", extra: { eventDate: "3000", intervalSeconds: "3", maxCount: "5", startDate: "3000", impressionDate: "3000", count: "5", type: "sponsored", eventCount: "1", }, }, ]); }, }); }, }); }); // Tests a config change: 1 interval -> 2 new intervals with higher timeouts. // Impression counts for the old interval should contribute to the new // intervals. add_task(async function configChange_1IntervalTo2NewIntervalsHigher() { await doTest({ config: { impression_caps: { sponsored: { custom: [{ interval_s: 3, max_count: 3 }], }, }, }, callback: async () => { await doTimedCallbacks({ 0: async () => { for (let i = 0; i < 3; i++) { await checkSearch({ name: "0s " + i, searchString: "sponsored", expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], }); } await checkTelemetryEvents([ { object: "hit", extra: { eventDate: "0", intervalSeconds: "3", maxCount: "3", startDate: "0", impressionDate: "0", count: "3", type: "sponsored", eventCount: "1", }, }, ]); await QuickSuggestTestUtils.setConfig({ impression_caps: { sponsored: { custom: [ { interval_s: 5, max_count: 3 }, { interval_s: 10, max_count: 5 }, ], }, }, }); }, 3: async () => { await checkSearch({ name: "3s", searchString: "sponsored", expectedResults: [], }); await checkTelemetryEvents([]); }, 4: async () => { await checkSearch({ name: "4s", searchString: "sponsored", expectedResults: [], }); await checkTelemetryEvents([]); }, 5: async () => { for (let i = 0; i < 2; i++) { await checkSearch({ name: "5s " + i, searchString: "sponsored", expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], }); } await checkSearch({ name: "5s additional", searchString: "sponsored", expectedResults: [], }); await checkTelemetryEvents([ { object: "reset", extra: { eventDate: "5000", intervalSeconds: "5", maxCount: "3", startDate: "0", impressionDate: "0", count: "3", type: "sponsored", eventCount: "1", }, }, { object: "hit", extra: { eventDate: "5000", intervalSeconds: "10", maxCount: "5", startDate: "0", impressionDate: "5000", count: "5", type: "sponsored", eventCount: "1", }, }, ]); }, }); }, }); }); // Tests a config change: 2 intervals -> 1 new interval with higher timeout. // Impression counts for the old intervals should contribute to the new // interval. add_task(async function configChange_2IntervalsTo1NewIntervalHigher() { await doTest({ config: { impression_caps: { sponsored: { custom: [ { interval_s: 2, max_count: 2 }, { interval_s: 4, max_count: 4 }, ], }, }, }, callback: async () => { await doTimedCallbacks({ 0: async () => { for (let i = 0; i < 2; i++) { await checkSearch({ name: "0s " + i, searchString: "sponsored", expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], }); } await checkTelemetryEvents([ { object: "hit", extra: { eventDate: "0", intervalSeconds: "2", maxCount: "2", startDate: "0", impressionDate: "0", count: "2", type: "sponsored", eventCount: "1", }, }, ]); }, 2: async () => { for (let i = 0; i < 2; i++) { await checkSearch({ name: "2s " + i, searchString: "sponsored", expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], }); } await checkTelemetryEvents([ { object: "reset", extra: { eventDate: "2000", intervalSeconds: "2", maxCount: "2", startDate: "0", impressionDate: "0", count: "2", type: "sponsored", eventCount: "1", }, }, { object: "hit", extra: { eventDate: "2000", intervalSeconds: "2", maxCount: "2", startDate: "2000", impressionDate: "2000", count: "2", type: "sponsored", eventCount: "1", }, }, { object: "hit", extra: { eventDate: "2000", intervalSeconds: "4", maxCount: "4", startDate: "0", impressionDate: "2000", count: "4", type: "sponsored", eventCount: "1", }, }, ]); await QuickSuggestTestUtils.setConfig({ impression_caps: { sponsored: { custom: [{ interval_s: 6, max_count: 5 }], }, }, }); }, 4: async () => { await checkSearch({ name: "4s 0", searchString: "sponsored", expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], }); await checkSearch({ name: "4s 1", searchString: "sponsored", expectedResults: [], }); await checkTelemetryEvents([ { object: "hit", extra: { eventDate: "4000", intervalSeconds: "6", maxCount: "5", startDate: "0", impressionDate: "4000", count: "5", type: "sponsored", eventCount: "1", }, }, ]); }, 5: async () => { await checkSearch({ name: "5s", searchString: "sponsored", expectedResults: [], }); await checkTelemetryEvents([]); }, 6: async () => { for (let i = 0; i < 5; i++) { await checkSearch({ name: "6s " + i, searchString: "sponsored", expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], }); } await checkSearch({ name: "6s additional", searchString: "sponsored", expectedResults: [], }); await checkTelemetryEvents([ { object: "reset", extra: { eventDate: "6000", intervalSeconds: "6", maxCount: "5", startDate: "0", impressionDate: "4000", count: "5", type: "sponsored", eventCount: "1", }, }, { object: "hit", extra: { eventDate: "6000", intervalSeconds: "6", maxCount: "5", startDate: "6000", impressionDate: "6000", count: "5", type: "sponsored", eventCount: "1", }, }, ]); }, }); }, }); }); // Tests a config change: 1 interval -> 1 new interval with lower timeout. // Impression counts for the old interval should not contribute to the new // interval since the new interval has a lower timeout. add_task(async function configChange_1IntervalTo1NewIntervalLower() { await doTest({ config: { impression_caps: { sponsored: { custom: [{ interval_s: 5, max_count: 3 }], }, }, }, callback: async () => { await doTimedCallbacks({ 0: async () => { for (let i = 0; i < 3; i++) { await checkSearch({ name: "0s " + i, searchString: "sponsored", expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], }); } await checkTelemetryEvents([ { object: "hit", extra: { eventDate: "0", intervalSeconds: "5", maxCount: "3", startDate: "0", impressionDate: "0", count: "3", type: "sponsored", eventCount: "1", }, }, ]); await QuickSuggestTestUtils.setConfig({ impression_caps: { sponsored: { custom: [{ interval_s: 3, max_count: 3 }], }, }, }); }, 1: async () => { for (let i = 0; i < 3; i++) { await checkSearch({ name: "3s " + i, searchString: "sponsored", expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], }); } await checkSearch({ name: "3s additional", searchString: "sponsored", expectedResults: [], }); await checkTelemetryEvents([ { object: "hit", extra: { eventDate: "1000", intervalSeconds: "3", maxCount: "3", startDate: "0", impressionDate: "1000", count: "3", type: "sponsored", eventCount: "1", }, }, ]); }, }); }, }); }); // Tests a config change: 1 interval -> lifetime. // Impression counts for the old interval should contribute to the new lifetime // cap. add_task(async function configChange_1IntervalToLifetime() { await doTest({ config: { impression_caps: { sponsored: { custom: [{ interval_s: 3, max_count: 3 }], }, }, }, callback: async () => { await doTimedCallbacks({ 0: async () => { for (let i = 0; i < 3; i++) { await checkSearch({ name: "0s " + i, searchString: "sponsored", expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], }); } await checkTelemetryEvents([ { object: "hit", extra: { eventDate: "0", intervalSeconds: "3", maxCount: "3", startDate: "0", impressionDate: "0", count: "3", type: "sponsored", eventCount: "1", }, }, ]); await QuickSuggestTestUtils.setConfig({ impression_caps: { sponsored: { lifetime: 3, }, }, }); }, 3: async () => { await checkSearch({ name: "3s", searchString: "sponsored", expectedResults: [], }); await checkTelemetryEvents([]); }, }); }, }); }); // Tests a config change: lifetime cap -> higher lifetime cap add_task(async function configChange_lifetimeCapHigher() { await doTest({ config: { impression_caps: { sponsored: { lifetime: 3, }, }, }, callback: async () => { await doTimedCallbacks({ 0: async () => { for (let i = 0; i < 3; i++) { await checkSearch({ name: "0s " + i, searchString: "sponsored", expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], }); } await checkSearch({ name: "0s additional", searchString: "sponsored", expectedResults: [], }); await checkTelemetryEvents([ { object: "hit", extra: { eventDate: "0", intervalSeconds: "Infinity", maxCount: "3", startDate: "0", impressionDate: "0", count: "3", type: "sponsored", eventCount: "1", }, }, ]); await QuickSuggestTestUtils.setConfig({ impression_caps: { sponsored: { lifetime: 5, }, }, }); }, 1: async () => { for (let i = 0; i < 2; i++) { await checkSearch({ name: "1s " + i, searchString: "sponsored", expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], }); } await checkSearch({ name: "1s additional", searchString: "sponsored", expectedResults: [], }); await checkTelemetryEvents([ { object: "hit", extra: { eventDate: "1000", intervalSeconds: "Infinity", maxCount: "5", startDate: "0", impressionDate: "1000", count: "5", type: "sponsored", eventCount: "1", }, }, ]); }, }); }, }); }); // Tests a config change: lifetime cap -> lower lifetime cap add_task(async function configChange_lifetimeCapLower() { await doTest({ config: { impression_caps: { sponsored: { lifetime: 3, }, }, }, callback: async () => { await doTimedCallbacks({ 0: async () => { for (let i = 0; i < 3; i++) { await checkSearch({ name: "0s " + i, searchString: "sponsored", expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], }); } await checkSearch({ name: "0s additional", searchString: "sponsored", expectedResults: [], }); await checkTelemetryEvents([ { object: "hit", extra: { eventDate: "0", intervalSeconds: "Infinity", maxCount: "3", startDate: "0", impressionDate: "0", count: "3", type: "sponsored", eventCount: "1", }, }, ]); await QuickSuggestTestUtils.setConfig({ impression_caps: { sponsored: { lifetime: 1, }, }, }); }, 1: async () => { await checkSearch({ name: "1s", searchString: "sponsored", expectedResults: [], }); await checkTelemetryEvents([]); }, }); }, }); }); // Makes sure stats are serialized to and from the pref correctly. add_task(async function prefSync() { await doTest({ config: { impression_caps: { sponsored: { lifetime: 5, custom: [ { interval_s: 3, max_count: 2 }, { interval_s: 5, max_count: 4 }, ], }, }, }, callback: async () => { for (let i = 0; i < 2; i++) { await checkSearch({ name: i, searchString: "sponsored", expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], }); } let json = UrlbarPrefs.get("quicksuggest.impressionCaps.stats"); Assert.ok(json, "JSON is non-empty"); Assert.deepEqual( JSON.parse(json), { sponsored: [ { intervalSeconds: 3, count: 2, maxCount: 2, startDateMs: 0, impressionDateMs: 0, }, { intervalSeconds: 5, count: 2, maxCount: 4, startDateMs: 0, impressionDateMs: 0, }, { intervalSeconds: null, count: 2, maxCount: 5, startDateMs: 0, impressionDateMs: 0, }, ], }, "JSON is correct" ); QuickSuggest.impressionCaps._test_reloadStats(); Assert.deepEqual( QuickSuggest.impressionCaps._test_stats, { sponsored: [ { intervalSeconds: 3, count: 2, maxCount: 2, startDateMs: 0, impressionDateMs: 0, }, { intervalSeconds: 5, count: 2, maxCount: 4, startDateMs: 0, impressionDateMs: 0, }, { intervalSeconds: Infinity, count: 2, maxCount: 5, startDateMs: 0, impressionDateMs: 0, }, ], }, "Impression stats were properly restored from the pref" ); }, }); }); // Tests direct changes to the stats pref. add_task(async function prefDirectlyChanged() { await doTest({ config: { impression_caps: { sponsored: { lifetime: 5, custom: [{ interval_s: 3, max_count: 3 }], }, }, }, callback: async () => { let expectedStats = { sponsored: [ { intervalSeconds: 3, count: 0, maxCount: 3, startDateMs: 0, impressionDateMs: 0, }, { intervalSeconds: Infinity, count: 0, maxCount: 5, startDateMs: 0, impressionDateMs: 0, }, ], }; UrlbarPrefs.set("quicksuggest.impressionCaps.stats", "bogus"); Assert.deepEqual( QuickSuggest.impressionCaps._test_stats, expectedStats, "Expected stats for 'bogus'" ); UrlbarPrefs.set("quicksuggest.impressionCaps.stats", JSON.stringify({})); Assert.deepEqual( QuickSuggest.impressionCaps._test_stats, expectedStats, "Expected stats for {}" ); UrlbarPrefs.set( "quicksuggest.impressionCaps.stats", JSON.stringify({ sponsored: "bogus" }) ); Assert.deepEqual( QuickSuggest.impressionCaps._test_stats, expectedStats, "Expected stats for { sponsored: 'bogus' }" ); UrlbarPrefs.set( "quicksuggest.impressionCaps.stats", JSON.stringify({ sponsored: [ { intervalSeconds: 3, count: 0, maxCount: 3, startDateMs: 0, impressionDateMs: 0, }, { intervalSeconds: "bogus", count: 0, maxCount: 99, startDateMs: 0, impressionDateMs: 0, }, { intervalSeconds: Infinity, count: 0, maxCount: 5, startDateMs: 0, impressionDateMs: 0, }, ], }) ); Assert.deepEqual( QuickSuggest.impressionCaps._test_stats, expectedStats, "Expected stats with intervalSeconds: 'bogus'" ); UrlbarPrefs.set( "quicksuggest.impressionCaps.stats", JSON.stringify({ sponsored: [ { intervalSeconds: 3, count: 0, maxCount: 123, startDateMs: 0, impressionDateMs: 0, }, { intervalSeconds: Infinity, count: 0, maxCount: 456, startDateMs: 0, impressionDateMs: 0, }, ], }) ); Assert.deepEqual( QuickSuggest.impressionCaps._test_stats, expectedStats, "Expected stats with `maxCount` values different from caps" ); let stats = { sponsored: [ { intervalSeconds: 3, count: 1, maxCount: 3, startDateMs: 99, impressionDateMs: 99, }, { intervalSeconds: Infinity, count: 7, maxCount: 5, startDateMs: 1337, impressionDateMs: 1337, }, ], }; UrlbarPrefs.set( "quicksuggest.impressionCaps.stats", JSON.stringify(stats) ); Assert.deepEqual( QuickSuggest.impressionCaps._test_stats, stats, "Expected stats with valid JSON" ); }, }); }); // Tests multiple interval periods where the cap is not hit. Telemetry should be // recorded for these periods. add_task(async function intervalsElapsedButCapNotHit() { await doTest({ config: { impression_caps: { sponsored: { custom: [{ interval_s: 1, max_count: 3 }], }, }, }, callback: async () => { await doTimedCallbacks({ // 1s 1: async () => { await checkSearch({ name: "1s", searchString: "sponsored", expectedResults: [EXPECTED_SPONSORED_URLBAR_RESULT], }); }, // 10s 10: async () => { QuickSuggest.impressionCaps._test_resetElapsedCounters(); let expectedEvents = [ // 1s: reset with count = 0 { object: "reset", extra: { eventDate: "1000", intervalSeconds: "1", maxCount: "3", startDate: "0", impressionDate: "0", count: "0", type: "sponsored", eventCount: "1", }, }, // 2-10s: reset with count = 1, eventCount = 9 { object: "reset", extra: { eventDate: "10000", intervalSeconds: "1", maxCount: "3", startDate: "1000", impressionDate: "1000", count: "1", type: "sponsored", eventCount: "9", }, }, ]; await checkTelemetryEvents(expectedEvents); }, }); }, }); }); // Simulates reset events across a restart with the following: // // S S R // >----|----|----|----|----|----|----|----|----|----| // 0s 1 2 3 4 5 6 7 8 9 10 // // 1. Startup at 0s // 2. Caps and stats initialized with interval_s: 1 // 3. Startup at 4.5s // 4. Reset triggered at 10s // // Expected: // At 10s: 6 batched resets for periods starting at 4s add_task(async function restart_1() { await doTest({ config: { impression_caps: { sponsored: { custom: [{ interval_s: 1, max_count: 1 }], }, }, }, callback: async () => { gStartupDateMsStub.returns(4500); await doTimedCallbacks({ // 10s: 6 batched resets for periods starting at 4s 10: async () => { QuickSuggest.impressionCaps._test_resetElapsedCounters(); await checkTelemetryEvents([ { object: "reset", extra: { eventDate: "10000", intervalSeconds: "1", maxCount: "1", startDate: "4000", impressionDate: "0", count: "0", type: "sponsored", eventCount: "6", }, }, ]); }, }); }, }); gStartupDateMsStub.returns(0); }); // Simulates reset events across a restart with the following: // // S S R // >----|----|----|----|----|----|----|----|----|----| // 0s 1 2 3 4 5 6 7 8 9 10 // // 1. Startup at 0s // 2. Caps and stats initialized with interval_s: 1 // 3. Startup at 5s // 4. Reset triggered at 10s // // Expected: // At 10s: 5 batched resets for periods starting at 5s add_task(async function restart_2() { await doTest({ config: { impression_caps: { sponsored: { custom: [{ interval_s: 1, max_count: 1 }], }, }, }, callback: async () => { gStartupDateMsStub.returns(5000); await doTimedCallbacks({ // 10s: 5 batched resets for periods starting at 5s 10: async () => { QuickSuggest.impressionCaps._test_resetElapsedCounters(); await checkTelemetryEvents([ { object: "reset", extra: { eventDate: "10000", intervalSeconds: "1", maxCount: "1", startDate: "5000", impressionDate: "0", count: "0", type: "sponsored", eventCount: "5", }, }, ]); }, }); }, }); gStartupDateMsStub.returns(0); }); // Simulates reset events across a restart with the following: // // S S R // >----|----|----|----|----|----|----|----|----|----| // 0s 1 2 3 4 5 6 7 8 9 10 // // 1. Startup at 0s // 2. Caps and stats initialized with interval_s: 1 // 3. Startup at 5.5s // 4. Reset triggered at 10s // // Expected: // At 10s: 5 batched resets for periods starting at 5s add_task(async function restart_3() { await doTest({ config: { impression_caps: { sponsored: { custom: [{ interval_s: 1, max_count: 1 }], }, }, }, callback: async () => { gStartupDateMsStub.returns(5500); await doTimedCallbacks({ // 10s: 5 batched resets for periods starting at 5s 10: async () => { QuickSuggest.impressionCaps._test_resetElapsedCounters(); await checkTelemetryEvents([ { object: "reset", extra: { eventDate: "10000", intervalSeconds: "1", maxCount: "1", startDate: "5000", impressionDate: "0", count: "0", type: "sponsored", eventCount: "5", }, }, ]); }, }); }, }); gStartupDateMsStub.returns(0); }); // Simulates reset events across a restart with the following: // // S S RR RR // >---------|---------| // 0s 10 20 // // 1. Startup at 0s // 2. Caps and stats initialized with interval_s: 10 // 3. Startup at 5s // 4. Resets triggered at 9s, 10s, 19s, 20s // // Expected: // At 10s: 1 reset for period starting at 0s // At 20s: 1 reset for period starting at 10s add_task(async function restart_4() { await doTest({ config: { impression_caps: { sponsored: { custom: [{ interval_s: 10, max_count: 1 }], }, }, }, callback: async () => { gStartupDateMsStub.returns(5000); await doTimedCallbacks({ // 9s: no resets 9: async () => { QuickSuggest.impressionCaps._test_resetElapsedCounters(); await checkTelemetryEvents([]); }, // 10s: 1 reset for period starting at 0s 10: async () => { QuickSuggest.impressionCaps._test_resetElapsedCounters(); await checkTelemetryEvents([ { object: "reset", extra: { eventDate: "10000", intervalSeconds: "10", maxCount: "1", startDate: "0", impressionDate: "0", count: "0", type: "sponsored", eventCount: "1", }, }, ]); }, // 19s: no resets 19: async () => { QuickSuggest.impressionCaps._test_resetElapsedCounters(); await checkTelemetryEvents([]); }, // 20s: 1 reset for period starting at 10s 20: async () => { QuickSuggest.impressionCaps._test_resetElapsedCounters(); await checkTelemetryEvents([ { object: "reset", extra: { eventDate: "20000", intervalSeconds: "10", maxCount: "1", startDate: "10000", impressionDate: "0", count: "0", type: "sponsored", eventCount: "1", }, }, ]); }, }); }, }); gStartupDateMsStub.returns(0); }); // Simulates reset events across a restart with the following: // // S S R // >---------|---------| // 0s 10 20 // // 1. Startup at 0s // 2. Caps and stats initialized with interval_s: 10 // 3. Startup at 5s // 4. Reset triggered at 20s // // Expected: // At 20s: 2 batched resets for periods starting at 0s add_task(async function restart_5() { await doTest({ config: { impression_caps: { sponsored: { custom: [{ interval_s: 10, max_count: 1 }], }, }, }, callback: async () => { gStartupDateMsStub.returns(5000); await doTimedCallbacks({ // 20s: 2 batches resets for periods starting at 0s 20: async () => { QuickSuggest.impressionCaps._test_resetElapsedCounters(); await checkTelemetryEvents([ { object: "reset", extra: { eventDate: "20000", intervalSeconds: "10", maxCount: "1", startDate: "0", impressionDate: "0", count: "0", type: "sponsored", eventCount: "2", }, }, ]); }, }); }, }); gStartupDateMsStub.returns(0); }); // Simulates reset events across a restart with the following: // // S S RR RR // >---------|---------|---------| // 0s 10 20 30 // // 1. Startup at 0s // 2. Caps and stats initialized with interval_s: 10 // 3. Startup at 15s // 4. Resets triggered at 19s, 20s, 29s, 30s // // Expected: // At 20s: 1 reset for period starting at 10s // At 30s: 1 reset for period starting at 20s add_task(async function restart_6() { await doTest({ config: { impression_caps: { sponsored: { custom: [{ interval_s: 10, max_count: 1 }], }, }, }, callback: async () => { gStartupDateMsStub.returns(15000); await doTimedCallbacks({ // 19s: no resets 19: async () => { QuickSuggest.impressionCaps._test_resetElapsedCounters(); await checkTelemetryEvents([]); }, // 20s: 1 reset for period starting at 10s 20: async () => { QuickSuggest.impressionCaps._test_resetElapsedCounters(); await checkTelemetryEvents([ { object: "reset", extra: { eventDate: "20000", intervalSeconds: "10", maxCount: "1", startDate: "10000", impressionDate: "0", count: "0", type: "sponsored", eventCount: "1", }, }, ]); }, // 29s: no resets 29: async () => { QuickSuggest.impressionCaps._test_resetElapsedCounters(); await checkTelemetryEvents([]); }, // 30s: 1 reset for period starting at 20s 30: async () => { QuickSuggest.impressionCaps._test_resetElapsedCounters(); await checkTelemetryEvents([ { object: "reset", extra: { eventDate: "30000", intervalSeconds: "10", maxCount: "1", startDate: "20000", impressionDate: "0", count: "0", type: "sponsored", eventCount: "1", }, }, ]); }, }); }, }); gStartupDateMsStub.returns(0); }); // Simulates reset events across a restart with the following: // // S S R // >---------|---------|---------| // 0s 10 20 30 // // 1. Startup at 0s // 2. Caps and stats initialized with interval_s: 10 // 3. Startup at 15s // 4. Reset triggered at 30s // // Expected: // At 30s: 2 batched resets for periods starting at 10s add_task(async function restart_7() { await doTest({ config: { impression_caps: { sponsored: { custom: [{ interval_s: 10, max_count: 1 }], }, }, }, callback: async () => { gStartupDateMsStub.returns(15000); await doTimedCallbacks({ // 30s: 2 batched resets for periods starting at 10s 30: async () => { QuickSuggest.impressionCaps._test_resetElapsedCounters(); await checkTelemetryEvents([ { object: "reset", extra: { eventDate: "30000", intervalSeconds: "10", maxCount: "1", startDate: "10000", impressionDate: "0", count: "0", type: "sponsored", eventCount: "2", }, }, ]); }, }); }, }); gStartupDateMsStub.returns(0); }); // Tests reset telemetry recorded on shutdown. add_task(async function shutdown() { await doTest({ config: { impression_caps: { sponsored: { custom: [{ interval_s: 1, max_count: 1 }], }, }, }, callback: async () => { // Make `Date.now()` return 10s. Since the cap's `interval_s` is 1s and // before this `Date.now()` returned 0s, 10 reset events should be // recorded on shutdown. gDateNowStub.returns(10000); // Simulate shutdown. Services.prefs.setBoolPref("toolkit.asyncshutdown.testing", true); AsyncShutdown.profileChangeTeardown._trigger(); await checkTelemetryEvents([ { object: "reset", extra: { eventDate: "10000", intervalSeconds: "1", maxCount: "1", startDate: "0", impressionDate: "0", count: "0", type: "sponsored", eventCount: "10", }, }, ]); gDateNowStub.returns(0); Services.prefs.clearUserPref("toolkit.asyncshutdown.testing"); }, }); }); // Tests the reset interval in realtime. add_task(async function resetInterval() { // Remove the test stubs so we can test in realtime. gDateNowStub.restore(); gStartupDateMsStub.restore(); await doTest({ config: { impression_caps: { sponsored: { custom: [{ interval_s: 0.1, max_count: 1 }], }, }, }, callback: async () => { // Restart the reset interval now with a 1s period. Since the cap's // `interval_s` is 0.1s, at least 10 reset events should be recorded the // first time the reset interval fires. The exact number depends on timing // and the machine running the test: how many 0.1s intervals elapse // between when the config is set to when the reset interval fires. For // that reason, we allow some leeway when checking `eventCount` below to // avoid intermittent failures. QuickSuggest.impressionCaps._test_setCountersResetInterval(1000); // eslint-disable-next-line mozilla/no-arbitrary-setTimeout await new Promise(r => setTimeout(r, 1100)); // Restore the reset interval to its default. QuickSuggest.impressionCaps._test_setCountersResetInterval(); await checkTelemetryEvents([ { object: "reset", extra: { eventDate: /^[0-9]+$/, intervalSeconds: "0.1", maxCount: "1", startDate: /^[0-9]+$/, impressionDate: "0", count: "0", type: "sponsored", // See comment above on allowing leeway for `eventCount`. eventCount: str => { info(`Checking 'eventCount': ${str}`); let count = parseInt(str); return 10 <= count && count < 20; }, }, }, ]); }, }); // Recreate the test stubs. gDateNowStub = gSandbox.stub( Cu.getGlobalForObject(UrlbarProviderQuickSuggest).Date, "now" ); gStartupDateMsStub = gSandbox.stub( QuickSuggest.impressionCaps, "_getStartupDateMs" ); gStartupDateMsStub.returns(0); }); /** * Main test helper. Sets up state, calls your callback, and resets state. * * @param {object} options * Options object. * @param {object} options.config * The quick suggest config to use during the test. * @param {Function} options.callback * The callback that will be run with the {@link config} */ async function doTest({ config, callback }) { Services.telemetry.clearEvents(); // Make `Date.now()` return 0 to start with. It's necessary to do this before // calling `withConfig()` because when a new config is set, the provider // validates its impression stats, whose `startDateMs` values depend on // `Date.now()`. gDateNowStub.returns(0); info(`Clearing stats and setting config`); UrlbarPrefs.clear("quicksuggest.impressionCaps.stats"); QuickSuggest.impressionCaps._test_reloadStats(); await QuickSuggestTestUtils.withConfig({ config, callback }); } /** * Does a series of timed searches and checks their results and telemetry. This * function relies on `doTimedCallbacks()`, so it may be helpful to look at it * too. * * @param {string} searchString * The query that should be timed * @param {object} expectedBySecond * An object that maps from seconds to objects that describe the searches to * perform, their expected results, and the expected telemetry. For a given * entry `S -> E` in this object, searches are performed S seconds after this * function is called. `E` is an object that looks like this: * * { results, telemetry } * * {array} results * An array of arrays. A search is performed for each sub-array in * `results`, and the contents of the sub-array are the expected results * for that search. * {object} telemetry * An object like this: { events } * {array} events * An array of expected telemetry events after all searches are done. * Telemetry events are cleared after checking these. If not present, * then it will be asserted that no events were recorded. * * Example: * * { * 0: { * results: [[R1], []], * telemetry: { * events: [ * someExpectedEvent, * ], * }, * } * 1: { * results: [[]], * }, * } * * 0 seconds after `doTimedSearches()` is called, two searches are * performed. The first one is expected to return a single result R1, and * the second search is expected to return no results. After the searches * are done, one telemetry event is expected to be recorded. * * 1 second after `doTimedSearches()` is called, one search is performed. * It's expected to return no results, and no telemetry is expected to be * recorded. */ async function doTimedSearches(searchString, expectedBySecond) { await doTimedCallbacks( Object.entries(expectedBySecond).reduce( (memo, [second, { results, telemetry }]) => { memo[second] = async () => { for (let i = 0; i < results.length; i++) { let expectedResults = results[i]; await checkSearch({ searchString, expectedResults, name: `${second}s search ${i + 1} of ${results.length}`, }); } let { events } = telemetry || {}; await checkTelemetryEvents(events || []); }; return memo; }, {} ) ); } /** * Takes a series a callbacks and times at which they should be called, and * calls them accordingly. This function is specifically designed for * UrlbarProviderQuickSuggest and its impression capping implementation because * it works by stubbing `Date.now()` within UrlbarProviderQuickSuggest. The * callbacks are not actually called at the given times but instead `Date.now()` * is stubbed so that UrlbarProviderQuickSuggest will think they are being * called at the given times. * * A more general implementation of this helper function that isn't tailored to * UrlbarProviderQuickSuggest is commented out below, and unfortunately it * doesn't work properly on macOS. * * @param {object} callbacksBySecond * An object that maps from seconds to callback functions. For a given entry * `S -> F` in this object, the callback F is called S seconds after * `doTimedCallbacks()` is called. */ async function doTimedCallbacks(callbacksBySecond) { let entries = Object.entries(callbacksBySecond).sort(([t1], [t2]) => t1 - t2); for (let [timeoutSeconds, callback] of entries) { gDateNowStub.returns(1000 * timeoutSeconds); await callback(); } } /* // This is the original implementation of `doTimedCallbacks()`, left here for // reference or in case the macOS problem described below is fixed. Instead of // stubbing `Date.now()` within UrlbarProviderQuickSuggest, it starts parallel // timers so that the callbacks are actually called at appropriate times. This // version of `doTimedCallbacks()` is therefore more generally useful, but it // has the drawback that your test has to run in real time. e.g., if one of your // callbacks needs to run 10s from now, the test must actually wait 10s. // // Unfortunately macOS seems to have some kind of limit of ~33 total 1-second // timers during any xpcshell test -- not 33 simultaneous timers but 33 total // timers. After that, timers fire randomly and with huge timeout periods that // are often exactly 10s greater than the specified period, as if some 10s // timeout internal to macOS is being hit. This problem does not seem to happen // when running the full browser, only during xpcshell tests. In fact the // problem can be reproduced in an xpcshell test that simply creates an interval // timer whose period is 1s (e.g., using `setInterval()` from Timer.sys.mjs). // After ~33 ticks, the timer's period jumps to ~10s. async function doTimedCallbacks(callbacksBySecond) { await Promise.all( Object.entries(callbacksBySecond).map( ([timeoutSeconds, callback]) => new Promise( resolve => setTimeout( () => callback().then(resolve), 1000 * parseInt(timeoutSeconds) ) ) ) ); } */ /** * Does a search, triggers an engagement, and checks the results. * * @param {object} options * Options object. * @param {string} options.name * This value is the name of the search and will be logged in messages to make * debugging easier. * @param {string} options.searchString * The query that should be searched. * @param {Array} options.expectedResults * The results that are expected from the search. */ async function checkSearch({ name, searchString, expectedResults }) { info(`Preparing search "${name}" with search string "${searchString}"`); let context = createContext(searchString, { providers: [UrlbarProviderQuickSuggest.name], isPrivate: false, }); info(`Doing search: ${name}`); await check_results({ context, matches: expectedResults, }); info(`Finished search: ${name}`); // Impression stats are updated only on engagement, so force one now. // `selIndex` doesn't really matter but since we're not trying to simulate a // click on the suggestion, pass in -1 to ensure we don't record a click. Pass // in true for `isPrivate` so we don't attempt to record the impression ping // because otherwise the following PingCentre error is logged: // "Structured Ingestion ping failure with error: undefined" let isPrivate = true; if (UrlbarProviderQuickSuggest._resultFromLastQuery) { UrlbarProviderQuickSuggest._resultFromLastQuery.isVisible = true; } UrlbarProviderQuickSuggest.onEngagement(isPrivate, "engagement", context, { selIndex: -1, }); } async function checkTelemetryEvents(expectedEvents) { QuickSuggestTestUtils.assertEvents( expectedEvents.map(event => ({ ...event, category: QuickSuggest.TELEMETRY_EVENT_CATEGORY, method: "impression_cap", })), // Filter in only impression_cap events. { method: "impression_cap" } ); }