From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- .../test/browser/browser_asrouter_targeting.js | 1697 ++++++++++++++++++++ 1 file changed, 1697 insertions(+) create mode 100644 browser/components/newtab/test/browser/browser_asrouter_targeting.js (limited to 'browser/components/newtab/test/browser/browser_asrouter_targeting.js') diff --git a/browser/components/newtab/test/browser/browser_asrouter_targeting.js b/browser/components/newtab/test/browser/browser_asrouter_targeting.js new file mode 100644 index 0000000000..21429f5bd3 --- /dev/null +++ b/browser/components/newtab/test/browser/browser_asrouter_targeting.js @@ -0,0 +1,1697 @@ +XPCOMUtils.defineLazyModuleGetters(this, { + AboutNewTab: "resource:///modules/AboutNewTab.jsm", + ASRouterTargeting: "resource://activity-stream/lib/ASRouterTargeting.jsm", + BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm", + HomePage: "resource:///modules/HomePage.jsm", + QueryCache: "resource://activity-stream/lib/ASRouterTargeting.jsm", +}); +ChromeUtils.defineESModuleGetters(this, { + AddonManager: "resource://gre/modules/AddonManager.sys.mjs", + AddonTestUtils: "resource://testing-common/AddonTestUtils.sys.mjs", + AppConstants: "resource://gre/modules/AppConstants.sys.mjs", + AttributionCode: "resource:///modules/AttributionCode.sys.mjs", + BuiltInThemes: "resource:///modules/BuiltInThemes.sys.mjs", + CFRMessageProvider: + "resource://activity-stream/lib/CFRMessageProvider.sys.mjs", + ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs", + ExperimentFakes: "resource://testing-common/NimbusTestUtils.sys.mjs", + FxAccounts: "resource://gre/modules/FxAccounts.sys.mjs", + NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs", + NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", + PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs", + ProfileAge: "resource://gre/modules/ProfileAge.sys.mjs", + Region: "resource://gre/modules/Region.sys.mjs", + ShellService: "resource:///modules/ShellService.sys.mjs", + TargetingContext: "resource://messaging-system/targeting/Targeting.sys.mjs", + TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.sys.mjs", + TelemetrySession: "resource://gre/modules/TelemetrySession.sys.mjs", +}); + +function sendFormAutofillMessage(name, data) { + let actor = + gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.getActor( + "FormAutofill" + ); + return actor.receiveMessage({ name, data }); +} + +async function removeAutofillRecords() { + let addresses = await sendFormAutofillMessage("FormAutofill:GetRecords", { + collectionName: "addresses", + }); + if (addresses.length) { + let observePromise = TestUtils.topicObserved( + "formautofill-storage-changed" + ); + await sendFormAutofillMessage("FormAutofill:RemoveAddresses", { + guids: addresses.map(address => address.guid), + }); + await observePromise; + } + let creditCards = await sendFormAutofillMessage("FormAutofill:GetRecords", { + collectionName: "creditCards", + }); + if (creditCards.length) { + let observePromise = TestUtils.topicObserved( + "formautofill-storage-changed" + ); + await sendFormAutofillMessage("FormAutofill:RemoveCreditCards", { + guids: creditCards.map(cc => cc.guid), + }); + await observePromise; + } +} + +// ASRouterTargeting.findMatchingMessage +add_task(async function find_matching_message() { + const messages = [ + { id: "foo", targeting: "FOO" }, + { id: "bar", targeting: "!FOO" }, + ]; + const context = { FOO: true }; + + const match = await ASRouterTargeting.findMatchingMessage({ + messages, + context, + }); + + is(match, messages[0], "should match and return the correct message"); +}); + +add_task(async function return_nothing_for_no_matching_message() { + const messages = [{ id: "bar", targeting: "!FOO" }]; + const context = { FOO: true }; + + const match = await ASRouterTargeting.findMatchingMessage({ + messages, + context, + }); + + ok(!match, "should return nothing since no matching message exists"); +}); + +add_task(async function check_other_error_handling() { + let called = false; + function onError(...args) { + called = true; + } + + const messages = [{ id: "foo", targeting: "foo" }]; + const context = { + get foo() { + throw new Error("test error"); + }, + }; + const match = await ASRouterTargeting.findMatchingMessage({ + messages, + context, + onError, + }); + + ok(!match, "should return nothing since no valid matching message exists"); + + Assert.ok(called, "Attribute error caught"); +}); + +// ASRouterTargeting.Environment +add_task(async function check_locale() { + ok( + Services.locale.appLocaleAsBCP47, + "Services.locale.appLocaleAsBCP47 exists" + ); + const message = { + id: "foo", + targeting: `locale == "${Services.locale.appLocaleAsBCP47}"`, + }; + is( + await ASRouterTargeting.findMatchingMessage({ messages: [message] }), + message, + "should select correct item when filtering by locale" + ); +}); +add_task(async function check_localeLanguageCode() { + const currentLanguageCode = Services.locale.appLocaleAsBCP47.substr(0, 2); + is( + Services.locale.negotiateLanguages( + [currentLanguageCode], + [Services.locale.appLocaleAsBCP47] + )[0], + Services.locale.appLocaleAsBCP47, + "currentLanguageCode should resolve to the current locale (e.g en => en-US)" + ); + const message = { + id: "foo", + targeting: `localeLanguageCode == "${currentLanguageCode}"`, + }; + is( + await ASRouterTargeting.findMatchingMessage({ messages: [message] }), + message, + "should select correct item when filtering by localeLanguageCode" + ); +}); + +add_task(async function checkProfileAgeCreated() { + let profileAccessor = await ProfileAge(); + is( + await ASRouterTargeting.Environment.profileAgeCreated, + await profileAccessor.created, + "should return correct profile age creation date" + ); + + const message = { + id: "foo", + targeting: `profileAgeCreated > ${(await profileAccessor.created) - 100}`, + }; + is( + await ASRouterTargeting.findMatchingMessage({ messages: [message] }), + message, + "should select correct item by profile age created" + ); +}); + +add_task(async function checkProfileAgeReset() { + let profileAccessor = await ProfileAge(); + is( + await ASRouterTargeting.Environment.profileAgeReset, + await profileAccessor.reset, + "should return correct profile age reset" + ); + + const message = { + id: "foo", + targeting: `profileAgeReset == ${await profileAccessor.reset}`, + }; + is( + await ASRouterTargeting.findMatchingMessage({ messages: [message] }), + message, + "should select correct item by profile age reset" + ); +}); + +add_task(async function checkCurrentDate() { + let message = { + id: "foo", + targeting: `currentDate < '${new Date(Date.now() + 5000)}'|date`, + }; + is( + await ASRouterTargeting.findMatchingMessage({ messages: [message] }), + message, + "should select message based on currentDate < timestamp" + ); + + message = { + id: "foo", + targeting: `currentDate > '${new Date(Date.now() - 5000)}'|date`, + }; + is( + await ASRouterTargeting.findMatchingMessage({ messages: [message] }), + message, + "should select message based on currentDate > timestamp" + ); +}); + +add_task(async function check_usesFirefoxSync() { + await pushPrefs(["services.sync.username", "someone@foo.com"]); + is( + await ASRouterTargeting.Environment.usesFirefoxSync, + true, + "should return true if a fx account is set" + ); + + const message = { id: "foo", targeting: "usesFirefoxSync" }; + is( + await ASRouterTargeting.findMatchingMessage({ messages: [message] }), + message, + "should select correct item by usesFirefoxSync" + ); +}); + +add_task(async function check_isFxAEnabled() { + await pushPrefs(["identity.fxaccounts.enabled", false]); + is( + await ASRouterTargeting.Environment.isFxAEnabled, + false, + "should return false if fxa is disabled" + ); + + const message = { id: "foo", targeting: "isFxAEnabled" }; + ok( + !(await ASRouterTargeting.findMatchingMessage({ messages: [message] })), + "should not select a message if fxa is disabled" + ); +}); + +add_task(async function check_isFxAEnabled() { + await pushPrefs(["identity.fxaccounts.enabled", true]); + is( + await ASRouterTargeting.Environment.isFxAEnabled, + true, + "should return true if fxa is enabled" + ); + + const message = { id: "foo", targeting: "isFxAEnabled" }; + is( + await ASRouterTargeting.findMatchingMessage({ messages: [message] }), + message, + "should select the correct message" + ); +}); + +add_task(async function check_isFxASignedIn_false() { + await pushPrefs( + ["identity.fxaccounts.enabled", true], + ["services.sync.username", ""] + ); + const sandbox = sinon.createSandbox(); + registerCleanupFunction(async () => sandbox.restore()); + sandbox.stub(FxAccounts.prototype, "getSignedInUser").resolves(null); + is( + await ASRouterTargeting.Environment.isFxASignedIn, + false, + "user should not appear signed in" + ); + + const message = { id: "foo", targeting: "isFxASignedIn" }; + isnot( + await ASRouterTargeting.findMatchingMessage({ messages: [message] }), + message, + "should not select the message since user is not signed in" + ); + + sandbox.restore(); +}); + +add_task(async function check_isFxASignedIn_true() { + await pushPrefs( + ["identity.fxaccounts.enabled", true], + ["services.sync.username", ""] + ); + const sandbox = sinon.createSandbox(); + registerCleanupFunction(async () => sandbox.restore()); + sandbox.stub(FxAccounts.prototype, "getSignedInUser").resolves({}); + is( + await ASRouterTargeting.Environment.isFxASignedIn, + true, + "user should appear signed in" + ); + + const message = { id: "foo", targeting: "isFxASignedIn" }; + is( + await ASRouterTargeting.findMatchingMessage({ messages: [message] }), + message, + "should select the correct message" + ); + + sandbox.restore(); +}); + +add_task(async function check_totalBookmarksCount() { + // Make sure we remove default bookmarks so they don't interfere + await clearHistoryAndBookmarks(); + const message = { id: "foo", targeting: "totalBookmarksCount > 0" }; + + const results = await ASRouterTargeting.findMatchingMessage({ + messages: [message], + }); + ok( + !(results ? JSON.stringify(results) : results), + "Should not select any message because bookmarks count is not 0" + ); + + const bookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "foo", + url: "https://mozilla1.com/nowNew", + }); + + QueryCache.queries.TotalBookmarksCount.expire(); + + is( + await ASRouterTargeting.findMatchingMessage({ messages: [message] }), + message, + "Should select correct item after bookmarks are added." + ); + + // Cleanup + await PlacesUtils.bookmarks.remove(bookmark.guid); +}); + +add_task(async function check_needsUpdate() { + QueryCache.queries.CheckBrowserNeedsUpdate.setUp(true); + + const message = { id: "foo", targeting: "needsUpdate" }; + + is( + await ASRouterTargeting.findMatchingMessage({ messages: [message] }), + message, + "Should select message because update count > 0" + ); + + QueryCache.queries.CheckBrowserNeedsUpdate.setUp(false); + + is( + await ASRouterTargeting.findMatchingMessage({ messages: [message] }), + null, + "Should not select message because update count == 0" + ); +}); + +add_task(async function checksearchEngines() { + const result = await ASRouterTargeting.Environment.searchEngines; + const expectedInstalled = (await Services.search.getAppProvidedEngines()) + .map(engine => engine.identifier) + .sort() + .join(","); + ok( + result.installed.length, + "searchEngines.installed should be a non-empty array" + ); + is( + result.installed.sort().join(","), + expectedInstalled, + "searchEngines.installed should be an array of visible search engines" + ); + ok( + result.current && typeof result.current === "string", + "searchEngines.current should be a truthy string" + ); + is( + result.current, + (await Services.search.getDefault()).identifier, + "searchEngines.current should be the current engine name" + ); + + const message = { + id: "foo", + targeting: `searchEngines[.current == ${ + (await Services.search.getDefault()).identifier + }]`, + }; + is( + await ASRouterTargeting.findMatchingMessage({ messages: [message] }), + message, + "should select correct item by searchEngines.current" + ); + + const message2 = { + id: "foo", + targeting: `searchEngines[${ + (await Services.search.getAppProvidedEngines())[0].identifier + } in .installed]`, + }; + is( + await ASRouterTargeting.findMatchingMessage({ messages: [message2] }), + message2, + "should select correct item by searchEngines.installed" + ); +}); + +add_task(async function checkisDefaultBrowser() { + const expected = ShellService.isDefaultBrowser(); + const result = await ASRouterTargeting.Environment.isDefaultBrowser; + is(typeof result, "boolean", "isDefaultBrowser should be a boolean value"); + is( + result, + expected, + "isDefaultBrowser should be equal to ShellService.isDefaultBrowser()" + ); + const message = { + id: "foo", + targeting: `isDefaultBrowser == ${expected.toString()}`, + }; + is( + await ASRouterTargeting.findMatchingMessage({ messages: [message] }), + message, + "should select correct item by isDefaultBrowser" + ); +}); + +add_task(async function checkdevToolsOpenedCount() { + await pushPrefs(["devtools.selfxss.count", 5]); + is( + ASRouterTargeting.Environment.devToolsOpenedCount, + 5, + "devToolsOpenedCount should be equal to devtools.selfxss.count pref value" + ); + const message = { id: "foo", targeting: "devToolsOpenedCount >= 5" }; + is( + await ASRouterTargeting.findMatchingMessage({ messages: [message] }), + message, + "should select correct item by devToolsOpenedCount" + ); +}); + +add_task(async function check_platformName() { + const message = { + id: "foo", + targeting: `platformName == "${AppConstants.platform}"`, + }; + is( + await ASRouterTargeting.findMatchingMessage({ messages: [message] }), + message, + "should select correct item by platformName" + ); +}); + +AddonTestUtils.initMochitest(this); + +add_task(async function checkAddonsInfo() { + const FAKE_ID = "testaddon@tests.mozilla.org"; + const FAKE_NAME = "Test Addon"; + const FAKE_VERSION = "0.5.7"; + + const xpi = AddonTestUtils.createTempWebExtensionFile({ + manifest: { + browser_specific_settings: { gecko: { id: FAKE_ID } }, + name: FAKE_NAME, + version: FAKE_VERSION, + }, + }); + + await Promise.all([ + AddonTestUtils.promiseWebExtensionStartup(FAKE_ID), + AddonManager.installTemporaryAddon(xpi), + ]); + + const { addons } = await AddonManager.getActiveAddons([ + "extension", + "service", + ]); + + const { addons: asRouterAddons, isFullData } = await ASRouterTargeting + .Environment.addonsInfo; + + ok( + addons.every(({ id }) => asRouterAddons[id]), + "should contain every addon" + ); + + ok( + Object.getOwnPropertyNames(asRouterAddons).every(id => + addons.some(addon => addon.id === id) + ), + "should contain no incorrect addons" + ); + + const testAddon = asRouterAddons[FAKE_ID]; + + ok( + Object.prototype.hasOwnProperty.call(testAddon, "version") && + testAddon.version === FAKE_VERSION, + "should correctly provide `version` property" + ); + + ok( + Object.prototype.hasOwnProperty.call(testAddon, "type") && + testAddon.type === "extension", + "should correctly provide `type` property" + ); + + ok( + Object.prototype.hasOwnProperty.call(testAddon, "isSystem") && + testAddon.isSystem === false, + "should correctly provide `isSystem` property" + ); + + ok( + Object.prototype.hasOwnProperty.call(testAddon, "isWebExtension") && + testAddon.isWebExtension === true, + "should correctly provide `isWebExtension` property" + ); + + // As we installed our test addon the addons database must be initialised, so + // (in this test environment) we expect to receive "full" data + + ok(isFullData, "should receive full data"); + + ok( + Object.prototype.hasOwnProperty.call(testAddon, "name") && + testAddon.name === FAKE_NAME, + "should correctly provide `name` property from full data" + ); + + ok( + Object.prototype.hasOwnProperty.call(testAddon, "userDisabled") && + testAddon.userDisabled === false, + "should correctly provide `userDisabled` property from full data" + ); + + ok( + Object.prototype.hasOwnProperty.call(testAddon, "installDate") && + Math.abs(Date.now() - new Date(testAddon.installDate)) < 60 * 1000, + "should correctly provide `installDate` property from full data" + ); +}); + +add_task(async function checkFrecentSites() { + const now = Date.now(); + const timeDaysAgo = numDays => now - numDays * 24 * 60 * 60 * 1000; + + const visits = []; + for (const [uri, count, visitDate] of [ + ["https://mozilla1.com/", 10, timeDaysAgo(0)], // frecency 1000 + ["https://mozilla2.com/", 5, timeDaysAgo(1)], // frecency 500 + ["https://mozilla3.com/", 1, timeDaysAgo(2)], // frecency 100 + ]) { + [...Array(count).keys()].forEach(() => + visits.push({ + uri, + visitDate: visitDate * 1000, // Places expects microseconds + }) + ); + } + + await PlacesTestUtils.addVisits(visits); + + let message = { + id: "foo", + targeting: "'mozilla3.com' in topFrecentSites|mapToProperty('host')", + }; + is( + await ASRouterTargeting.findMatchingMessage({ messages: [message] }), + message, + "should select correct item by host in topFrecentSites" + ); + + message = { + id: "foo", + targeting: "'non-existent.com' in topFrecentSites|mapToProperty('host')", + }; + ok( + !(await ASRouterTargeting.findMatchingMessage({ messages: [message] })), + "should not select incorrect item by host in topFrecentSites" + ); + + message = { + id: "foo", + targeting: + "'mozilla2.com' in topFrecentSites[.frecency >= 400]|mapToProperty('host')", + }; + is( + await ASRouterTargeting.findMatchingMessage({ messages: [message] }), + message, + "should select correct item when filtering by frecency" + ); + + message = { + id: "foo", + targeting: + "'mozilla2.com' in topFrecentSites[.frecency >= 600]|mapToProperty('host')", + }; + ok( + !(await ASRouterTargeting.findMatchingMessage({ messages: [message] })), + "should not select incorrect item when filtering by frecency" + ); + + message = { + id: "foo", + targeting: `'mozilla2.com' in topFrecentSites[.lastVisitDate >= ${ + timeDaysAgo(1) - 1 + }]|mapToProperty('host')`, + }; + is( + await ASRouterTargeting.findMatchingMessage({ messages: [message] }), + message, + "should select correct item when filtering by lastVisitDate" + ); + + message = { + id: "foo", + targeting: `'mozilla2.com' in topFrecentSites[.lastVisitDate >= ${ + timeDaysAgo(0) - 1 + }]|mapToProperty('host')`, + }; + ok( + !(await ASRouterTargeting.findMatchingMessage({ messages: [message] })), + "should not select incorrect item when filtering by lastVisitDate" + ); + + message = { + id: "foo", + targeting: `(topFrecentSites[.frecency >= 900 && .lastVisitDate >= ${ + timeDaysAgo(1) - 1 + }]|mapToProperty('host') intersect ['mozilla3.com', 'mozilla2.com', 'mozilla1.com'])|length > 0`, + }; + is( + await ASRouterTargeting.findMatchingMessage({ messages: [message] }), + message, + "should select correct item when filtering by frecency and lastVisitDate with multiple candidate domains" + ); + + // Cleanup + await clearHistoryAndBookmarks(); +}); + +add_task(async function check_pinned_sites() { + // Fresh profiles come with an empty set of pinned websites (pref doesn't + // exist). Search shortcut topsites make this test more complicated because + // the feature pins a new website on startup. Behaviour can vary when running + // with --verify so it's more predictable to clear pins entirely. + Services.prefs.clearUserPref("browser.newtabpage.pinned"); + NewTabUtils.pinnedLinks.resetCache(); + const originalPin = JSON.stringify(NewTabUtils.pinnedLinks.links); + const sitesToPin = [ + { url: "https://foo.com" }, + { url: "https://bloo.com" }, + { url: "https://floogle.com", searchTopSite: true }, + ]; + sitesToPin.forEach(site => + NewTabUtils.pinnedLinks.pin(site, NewTabUtils.pinnedLinks.links.length) + ); + + // Unpinning adds null to the list of pinned sites, which we should test that we handle gracefully for our targeting + NewTabUtils.pinnedLinks.unpin(sitesToPin[1]); + ok( + NewTabUtils.pinnedLinks.links.includes(null), + "should have set an item in pinned links to null via unpinning for testing" + ); + + let message; + + message = { + id: "foo", + targeting: "'https://foo.com' in pinnedSites|mapToProperty('url')", + }; + is( + await ASRouterTargeting.findMatchingMessage({ messages: [message] }), + message, + "should select correct item by url in pinnedSites" + ); + + message = { + id: "foo", + targeting: "'foo.com' in pinnedSites|mapToProperty('host')", + }; + is( + await ASRouterTargeting.findMatchingMessage({ messages: [message] }), + message, + "should select correct item by host in pinnedSites" + ); + + message = { + id: "foo", + targeting: + "'floogle.com' in pinnedSites[.searchTopSite == true]|mapToProperty('host')", + }; + is( + await ASRouterTargeting.findMatchingMessage({ messages: [message] }), + message, + "should select correct item by host and searchTopSite in pinnedSites" + ); + + // Cleanup + sitesToPin.forEach(site => NewTabUtils.pinnedLinks.unpin(site)); + + await clearHistoryAndBookmarks(); + Services.prefs.clearUserPref("browser.newtabpage.pinned"); + NewTabUtils.pinnedLinks.resetCache(); + is( + JSON.stringify(NewTabUtils.pinnedLinks.links), + originalPin, + "should restore pinned sites to its original state" + ); +}); + +add_task(async function check_firefox_version() { + const message = { id: "foo", targeting: "firefoxVersion > 0" }; + is( + await ASRouterTargeting.findMatchingMessage({ messages: [message] }), + message, + "should select correct item when filtering by firefox version" + ); +}); + +add_task(async function check_region() { + Region._setHomeRegion("DE", false); + const message = { id: "foo", targeting: "region in ['DE']" }; + is( + await ASRouterTargeting.findMatchingMessage({ messages: [message] }), + message, + "should select correct item when filtering by firefox geo" + ); +}); + +add_task(async function check_browserSettings() { + is( + await JSON.stringify(ASRouterTargeting.Environment.browserSettings.update), + JSON.stringify(TelemetryEnvironment.currentEnvironment.settings.update), + "should return correct update info" + ); +}); + +add_task(async function check_sync() { + is( + await ASRouterTargeting.Environment.sync.desktopDevices, + Services.prefs.getIntPref("services.sync.clients.devices.desktop", 0), + "should return correct desktopDevices info" + ); + is( + await ASRouterTargeting.Environment.sync.mobileDevices, + Services.prefs.getIntPref("services.sync.clients.devices.mobile", 0), + "should return correct mobileDevices info" + ); + is( + await ASRouterTargeting.Environment.sync.totalDevices, + Services.prefs.getIntPref("services.sync.numClients", 0), + "should return correct mobileDevices info" + ); +}); + +add_task(async function check_provider_cohorts() { + await pushPrefs([ + "browser.newtabpage.activity-stream.asrouter.providers.onboarding", + JSON.stringify({ + id: "onboarding", + messages: [], + enabled: true, + cohort: "foo", + }), + ]); + await pushPrefs([ + "browser.newtabpage.activity-stream.asrouter.providers.cfr", + JSON.stringify({ id: "cfr", enabled: true, cohort: "bar" }), + ]); + is( + await ASRouterTargeting.Environment.providerCohorts.onboarding, + "foo", + "should have cohort foo for onboarding" + ); + is( + await ASRouterTargeting.Environment.providerCohorts.cfr, + "bar", + "should have cohort bar for cfr" + ); +}); + +add_task(async function check_xpinstall_enabled() { + // should default to true if pref doesn't exist + is(await ASRouterTargeting.Environment.xpinstallEnabled, true); + // flip to false, check targeting reflects that + await pushPrefs(["xpinstall.enabled", false]); + is(await ASRouterTargeting.Environment.xpinstallEnabled, false); + // flip to true, check targeting reflects that + await pushPrefs(["xpinstall.enabled", true]); + is(await ASRouterTargeting.Environment.xpinstallEnabled, true); +}); + +add_task(async function check_pinned_tabs() { + await BrowserTestUtils.withNewTab( + { gBrowser, url: "about:blank" }, + async browser => { + is( + await ASRouterTargeting.Environment.hasPinnedTabs, + false, + "No pin tabs yet" + ); + + let tab = gBrowser.getTabForBrowser(browser); + gBrowser.pinTab(tab); + + is( + await ASRouterTargeting.Environment.hasPinnedTabs, + true, + "Should detect pinned tab" + ); + + gBrowser.unpinTab(tab); + } + ); +}); + +add_task(async function check_hasAccessedFxAPanel() { + is( + await ASRouterTargeting.Environment.hasAccessedFxAPanel, + false, + "Not accessed yet" + ); + + await pushPrefs(["identity.fxaccounts.toolbar.accessed", true]); + + is( + await ASRouterTargeting.Environment.hasAccessedFxAPanel, + true, + "Should detect panel access" + ); +}); + +add_task(async function checkCFRFeaturesUserPref() { + await pushPrefs([ + "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features", + false, + ]); + is( + ASRouterTargeting.Environment.userPrefs.cfrFeatures, + false, + "cfrFeature should be false according to pref" + ); + const message = { id: "foo", targeting: "userPrefs.cfrFeatures == false" }; + is( + await ASRouterTargeting.findMatchingMessage({ messages: [message] }), + message, + "should select correct item by cfrFeature" + ); +}); + +add_task(async function checkCFRAddonsUserPref() { + await pushPrefs([ + "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons", + false, + ]); + is( + ASRouterTargeting.Environment.userPrefs.cfrAddons, + false, + "cfrFeature should be false according to pref" + ); + const message = { id: "foo", targeting: "userPrefs.cfrAddons == false" }; + is( + await ASRouterTargeting.findMatchingMessage({ messages: [message] }), + message, + "should select correct item by cfrAddons" + ); +}); + +add_task(async function check_blockedCountByType() { + const message = { + id: "foo", + targeting: + "blockedCountByType.cryptominerCount == 0 && blockedCountByType.socialCount == 0", + }; + + is( + await ASRouterTargeting.findMatchingMessage({ messages: [message] }), + message, + "should select correct item" + ); +}); + +add_task(async function checkPatternMatches() { + const now = Date.now(); + const timeMinutesAgo = numMinutes => now - numMinutes * 60 * 1000; + const messages = [ + { + id: "message_with_pattern", + targeting: "true", + trigger: { id: "frequentVisits", patterns: ["*://*.github.com/"] }, + }, + ]; + const trigger = { + id: "frequentVisits", + context: { + recentVisits: [ + { timestamp: timeMinutesAgo(33) }, + { timestamp: timeMinutesAgo(17) }, + { timestamp: timeMinutesAgo(1) }, + ], + }, + param: { host: "github.com", url: "https://gist.github.com" }, + }; + + is( + (await ASRouterTargeting.findMatchingMessage({ messages, trigger })).id, + "message_with_pattern", + "should select PIN_TAB mesage" + ); +}); + +add_task(async function checkPatternsValid() { + const messages = (await CFRMessageProvider.getMessages()).filter( + m => m.trigger?.patterns + ); + + for (const message of messages) { + Assert.ok(new MatchPatternSet(message.trigger.patterns)); + } +}); + +add_task(async function check_isChinaRepack() { + const prefDefaultBranch = Services.prefs.getDefaultBranch("distribution."); + const messages = [ + { id: "msg_for_china_repack", targeting: "isChinaRepack == true" }, + { id: "msg_for_everyone_else", targeting: "isChinaRepack == false" }, + ]; + + is( + await ASRouterTargeting.Environment.isChinaRepack, + false, + "Fx w/o partner repack info set is not China repack" + ); + is( + (await ASRouterTargeting.findMatchingMessage({ messages })).id, + "msg_for_everyone_else", + "should select the message for non China repack users" + ); + + prefDefaultBranch.setCharPref("id", "MozillaOnline"); + + is( + await ASRouterTargeting.Environment.isChinaRepack, + true, + "Fx with `distribution.id` set to `MozillaOnline` is China repack" + ); + is( + (await ASRouterTargeting.findMatchingMessage({ messages })).id, + "msg_for_china_repack", + "should select the message for China repack users" + ); + + prefDefaultBranch.setCharPref("id", "Example"); + + is( + await ASRouterTargeting.Environment.isChinaRepack, + false, + "Fx with `distribution.id` set to other string is not China repack" + ); + is( + (await ASRouterTargeting.findMatchingMessage({ messages })).id, + "msg_for_everyone_else", + "should select the message for non China repack users" + ); + + prefDefaultBranch.deleteBranch(""); +}); + +add_task(async function check_userId() { + await SpecialPowers.pushPrefEnv({ + set: [["app.normandy.user_id", "foo123"]], + }); + is( + await ASRouterTargeting.Environment.userId, + "foo123", + "should read userID from normandy user id pref" + ); +}); + +add_task(async function check_profileRestartCount() { + ok( + !isNaN(ASRouterTargeting.Environment.profileRestartCount), + "it should return a number" + ); +}); + +add_task(async function check_homePageSettings_default() { + let settings = ASRouterTargeting.Environment.homePageSettings; + + ok(settings.isDefault, "should set as default"); + ok(!settings.isLocked, "should not set as locked"); + ok(!settings.isWebExt, "should not be web extension"); + ok(!settings.isCustomUrl, "should not be custom URL"); + is(settings.urls.length, 1, "should be an 1-entry array"); + is(settings.urls[0].url, "about:home", "should be about:home"); + is(settings.urls[0].host, "", "should be an empty string"); +}); + +add_task(async function check_homePageSettings_locked() { + const PREF = "browser.startup.homepage"; + Services.prefs.lockPref(PREF); + let settings = ASRouterTargeting.Environment.homePageSettings; + + ok(settings.isDefault, "should set as default"); + ok(settings.isLocked, "should set as locked"); + ok(!settings.isWebExt, "should not be web extension"); + ok(!settings.isCustomUrl, "should not be custom URL"); + is(settings.urls.length, 1, "should be an 1-entry array"); + is(settings.urls[0].url, "about:home", "should be about:home"); + is(settings.urls[0].host, "", "should be an empty string"); + Services.prefs.unlockPref(PREF); +}); + +add_task(async function check_homePageSettings_customURL() { + await HomePage.set("https://www.google.com"); + let settings = ASRouterTargeting.Environment.homePageSettings; + + ok(!settings.isDefault, "should not be the default"); + ok(!settings.isLocked, "should set as locked"); + ok(!settings.isWebExt, "should not be web extension"); + ok(settings.isCustomUrl, "should be custom URL"); + is(settings.urls.length, 1, "should be an 1-entry array"); + is(settings.urls[0].url, "https://www.google.com", "should be a custom URL"); + is( + settings.urls[0].host, + "google.com", + "should be the host name without 'www.'" + ); + + HomePage.reset(); +}); + +add_task(async function check_homePageSettings_customURL_multiple() { + await HomePage.set("https://www.google.com|https://www.youtube.com"); + let settings = ASRouterTargeting.Environment.homePageSettings; + + ok(!settings.isDefault, "should not be the default"); + ok(!settings.isLocked, "should not set as locked"); + ok(!settings.isWebExt, "should not be web extension"); + ok(settings.isCustomUrl, "should be custom URL"); + is(settings.urls.length, 2, "should be a 2-entry array"); + is(settings.urls[0].url, "https://www.google.com", "should be a custom URL"); + is( + settings.urls[0].host, + "google.com", + "should be the host name without 'www.'" + ); + is(settings.urls[1].url, "https://www.youtube.com", "should be a custom URL"); + is( + settings.urls[1].host, + "youtube.com", + "should be the host name without 'www.'" + ); + + HomePage.reset(); +}); + +add_task(async function check_homePageSettings_webExtension() { + const extURI = + "moz-extension://0d735548-ba3c-aa43-a0e4-7089584fbb53/homepage.html"; + await HomePage.set(extURI); + let settings = ASRouterTargeting.Environment.homePageSettings; + + ok(!settings.isDefault, "should not be the default"); + ok(!settings.isLocked, "should not set as locked"); + ok(settings.isWebExt, "should be a web extension"); + ok(!settings.isCustomUrl, "should be custom URL"); + is(settings.urls.length, 1, "should be an 1-entry array"); + is(settings.urls[0].url, extURI, "should be a webExtension URI"); + is(settings.urls[0].host, "", "should be an empty string"); + + HomePage.reset(); +}); + +add_task(async function check_newtabSettings_default() { + let settings = ASRouterTargeting.Environment.newtabSettings; + + ok(settings.isDefault, "should set as default"); + ok(!settings.isWebExt, "should not be web extension"); + ok(!settings.isCustomUrl, "should not be custom URL"); + is(settings.url, "about:newtab", "should be about:home"); + is(settings.host, "", "should be an empty string"); +}); + +add_task(async function check_newTabSettings_customURL() { + AboutNewTab.newTabURL = "https://www.google.com"; + let settings = ASRouterTargeting.Environment.newtabSettings; + + ok(!settings.isDefault, "should not be the default"); + ok(!settings.isWebExt, "should not be web extension"); + ok(settings.isCustomUrl, "should be custom URL"); + is(settings.url, "https://www.google.com", "should be a custom URL"); + is(settings.host, "google.com", "should be the host name without 'www.'"); + + AboutNewTab.resetNewTabURL(); +}); + +add_task(async function check_newTabSettings_webExtension() { + const extURI = + "moz-extension://0d735548-ba3c-aa43-a0e4-7089584fbb53/homepage.html"; + AboutNewTab.newTabURL = extURI; + let settings = ASRouterTargeting.Environment.newtabSettings; + + ok(!settings.isDefault, "should not be the default"); + ok(settings.isWebExt, "should not be web extension"); + ok(!settings.isCustomUrl, "should be custom URL"); + is(settings.url, extURI, "should be the web extension URI"); + is(settings.host, "", "should be an empty string"); + + AboutNewTab.resetNewTabURL(); +}); + +add_task(async function check_openUrlTrigger_context() { + const message = { + ...(await CFRMessageProvider.getMessages()).find( + m => m.id === "YOUTUBE_ENHANCE_3" + ), + targeting: "visitsCount == 3", + }; + const trigger = { + id: "openURL", + context: { visitsCount: 3 }, + param: { host: "youtube.com", url: "https://www.youtube.com" }, + }; + + is( + ( + await ASRouterTargeting.findMatchingMessage({ + messages: [message], + trigger, + }) + ).id, + message.id, + `should select ${message.id} mesage` + ); +}); + +add_task(async function check_is_major_upgrade() { + let message = { + id: "check_is_major_upgrade", + targeting: `isMajorUpgrade != undefined && isMajorUpgrade == ${ + Cc["@mozilla.org/browser/clh;1"].getService(Ci.nsIBrowserHandler) + .majorUpgrade + }`, + }; + + is( + (await ASRouterTargeting.findMatchingMessage({ messages: [message] })).id, + message.id, + "Should select the message" + ); +}); + +add_task(async function check_userMonthlyActivity() { + ok( + Array.isArray(await ASRouterTargeting.Environment.userMonthlyActivity), + "value is an array" + ); +}); + +add_task(async function check_doesAppNeedPin() { + is( + typeof (await ASRouterTargeting.Environment.doesAppNeedPin), + "boolean", + "Should return a boolean" + ); +}); + +add_task(async function check_doesAppNeedPrivatePin() { + is( + typeof (await ASRouterTargeting.Environment.doesAppNeedPrivatePin), + "boolean", + "Should return a boolean" + ); +}); + +add_task(async function check_isBackgroundTaskMode() { + if (!AppConstants.MOZ_BACKGROUNDTASKS) { + // `mochitest-browser` suite `add_task` does not yet support + // `properties.skip_if`. + ok(true, "Skipping because !AppConstants.MOZ_BACKGROUNDTASKS"); + return; + } + + const bts = Cc["@mozilla.org/backgroundtasks;1"].getService( + Ci.nsIBackgroundTasks + ); + + // Pretend that this is a background task. + bts.overrideBackgroundTaskNameForTesting("taskName"); + is( + await ASRouterTargeting.Environment.isBackgroundTaskMode, + true, + "Is in background task mode" + ); + is( + await ASRouterTargeting.Environment.backgroundTaskName, + "taskName", + "Has expected background task name" + ); + + // Unset, so that subsequent test functions don't see background task mode. + bts.overrideBackgroundTaskNameForTesting(null); + is( + await ASRouterTargeting.Environment.isBackgroundTaskMode, + false, + "Is not in background task mode" + ); + is( + await ASRouterTargeting.Environment.backgroundTaskName, + null, + "Has no background task name" + ); +}); + +add_task(async function check_userPrefersReducedMotion() { + is( + typeof (await ASRouterTargeting.Environment.userPrefersReducedMotion), + "boolean", + "Should return a boolean" + ); +}); + +add_task(async function test_mr2022Holdback() { + await ExperimentAPI.ready(); + + ok( + !ASRouterTargeting.Environment.inMr2022Holdback, + "Should not be in holdback (no experiment)" + ); + + { + const doExperimentCleanup = await ExperimentFakes.enrollWithFeatureConfig({ + featureId: "majorRelease2022", + value: { + onboarding: true, + }, + }); + + ok( + !ASRouterTargeting.Environment.inMr2022Holdback, + "Should not be in holdback (onboarding = true)" + ); + + await doExperimentCleanup(); + } + + { + const doExperimentCleanup = await ExperimentFakes.enrollWithFeatureConfig({ + featureId: "majorRelease2022", + value: { + onboarding: false, + }, + }); + + ok( + ASRouterTargeting.Environment.inMr2022Holdback, + "Should be in holdback (onboarding = false)" + ); + + await doExperimentCleanup(); + } +}); + +add_task(async function test_distributionId() { + is( + ASRouterTargeting.Environment.distributionId, + "", + "Should return an empty distribution Id" + ); + + Services.prefs.getDefaultBranch(null).setCharPref("distribution.id", "test"); + + is( + ASRouterTargeting.Environment.distributionId, + "test", + "Should return the correct distribution Id" + ); +}); + +add_task(async function test_fxViewButtonAreaType_default() { + is( + typeof (await ASRouterTargeting.Environment.fxViewButtonAreaType), + "string", + "Should return a string" + ); + + is( + await ASRouterTargeting.Environment.fxViewButtonAreaType, + "toolbar", + "Should return name of container if button hasn't been removed" + ); +}); + +add_task(async function test_fxViewButtonAreaType_removed() { + CustomizableUI.removeWidgetFromArea("firefox-view-button"); + + is( + await ASRouterTargeting.Environment.fxViewButtonAreaType, + null, + "Should return null if button has been removed" + ); + CustomizableUI.reset(); +}); + +add_task(async function test_creditCardsSaved() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["extensions.formautofill.creditCards.supported", "on"], + ["extensions.formautofill.creditCards.enabled", true], + ], + }); + + is( + await ASRouterTargeting.Environment.creditCardsSaved, + 0, + "Should return 0 when no credit cards are saved" + ); + + let creditcard = { + "cc-name": "Test User", + "cc-number": "5038146897157463", + "cc-exp-month": "11", + "cc-exp-year": "20", + }; + + // Intermittently fails on macOS, likely related to Bug 1714221. So, mock the + // autofill actor. + if (AppConstants.platform === "macosx") { + const sandbox = sinon.createSandbox(); + registerCleanupFunction(async () => sandbox.restore()); + let stub = sandbox + .stub( + gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.getActor( + "FormAutofill" + ), + "receiveMessage" + ) + .withArgs( + sandbox.match({ + name: "FormAutofill:GetRecords", + data: { collectionName: "creditCards" }, + }) + ) + .resolves([creditcard]) + .callThrough(); + + is( + await ASRouterTargeting.Environment.creditCardsSaved, + 1, + "Should return 1 when 1 credit card is saved" + ); + ok( + stub.calledWithMatch({ name: "FormAutofill:GetRecords" }), + "Targeting called FormAutofill:GetRecords" + ); + + sandbox.restore(); + } else { + let observePromise = TestUtils.topicObserved( + "formautofill-storage-changed" + ); + await sendFormAutofillMessage("FormAutofill:SaveCreditCard", { + creditcard, + }); + await observePromise; + + is( + await ASRouterTargeting.Environment.creditCardsSaved, + 1, + "Should return 1 when 1 credit card is saved" + ); + await removeAutofillRecords(); + } + + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_addressesSaved() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["extensions.formautofill.addresses.supported", "on"], + ["extensions.formautofill.addresses.enabled", true], + ], + }); + + is( + await ASRouterTargeting.Environment.addressesSaved, + 0, + "Should return 0 when no addresses are saved" + ); + + let observePromise = TestUtils.topicObserved("formautofill-storage-changed"); + await sendFormAutofillMessage("FormAutofill:SaveAddress", { + address: { + "given-name": "John", + "additional-name": "R.", + "family-name": "Smith", + organization: "World Wide Web Consortium", + "street-address": "32 Vassar Street\nMIT Room 32-G524", + "address-level2": "Cambridge", + "address-level1": "MA", + "postal-code": "02139", + country: "US", + tel: "+16172535702", + email: "timbl@w3.org", + }, + }); + await observePromise; + + is( + await ASRouterTargeting.Environment.addressesSaved, + 1, + "Should return 1 when 1 address is saved" + ); + + await removeAutofillRecords(); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_migrationInteractions() { + await pushPrefs( + ["browser.migrate.interactions.bookmarks", false], + ["browser.migrate.interactions.history", false], + ["browser.migrate.interactions.passwords", false] + ); + + ok(!(await ASRouterTargeting.Environment.hasMigratedBookmarks)); + ok(!(await ASRouterTargeting.Environment.hasMigratedHistory)); + ok(!(await ASRouterTargeting.Environment.hasMigratedPasswords)); + + await pushPrefs( + ["browser.migrate.interactions.bookmarks", true], + ["browser.migrate.interactions.history", false], + ["browser.migrate.interactions.passwords", false] + ); + + ok(await ASRouterTargeting.Environment.hasMigratedBookmarks); + ok(!(await ASRouterTargeting.Environment.hasMigratedHistory)); + ok(!(await ASRouterTargeting.Environment.hasMigratedPasswords)); + + await pushPrefs( + ["browser.migrate.interactions.bookmarks", true], + ["browser.migrate.interactions.history", true], + ["browser.migrate.interactions.passwords", false] + ); + + ok(await ASRouterTargeting.Environment.hasMigratedBookmarks); + ok(await ASRouterTargeting.Environment.hasMigratedHistory); + ok(!(await ASRouterTargeting.Environment.hasMigratedPasswords)); + + await pushPrefs( + ["browser.migrate.interactions.bookmarks", true], + ["browser.migrate.interactions.history", true], + ["browser.migrate.interactions.passwords", true] + ); + + ok(await ASRouterTargeting.Environment.hasMigratedBookmarks); + ok(await ASRouterTargeting.Environment.hasMigratedHistory); + ok(await ASRouterTargeting.Environment.hasMigratedPasswords); +}); + +add_task(async function check_useEmbeddedMigrationWizard() { + await pushPrefs([ + "browser.migrate.content-modal.about-welcome-behavior", + "default", + ]); + + ok(!(await ASRouterTargeting.Environment.useEmbeddedMigrationWizard)); + + await pushPrefs([ + "browser.migrate.content-modal.about-welcome-behavior", + "autoclose", + ]); + + ok(!(await ASRouterTargeting.Environment.useEmbeddedMigrationWizard)); + + await pushPrefs([ + "browser.migrate.content-modal.about-welcome-behavior", + "embedded", + ]); + + ok(await ASRouterTargeting.Environment.useEmbeddedMigrationWizard); + + await pushPrefs([ + "browser.migrate.content-modal.about-welcome-behavior", + "standalone", + ]); + + ok(!(await ASRouterTargeting.Environment.useEmbeddedMigrationWizard)); +}); + +add_task(async function check_isRTAMO() { + is( + typeof ASRouterTargeting.Environment.isRTAMO, + "boolean", + "Should return a boolean" + ); + + const TEST_CASES = [ + { + title: "no attribution data", + attributionData: {}, + expected: false, + }, + { + title: "null attribution data", + attributionData: null, + expected: false, + }, + { + title: "no content", + attributionData: { + source: "addons.mozilla.org", + }, + expected: false, + }, + { + title: "empty content", + attributionData: { + source: "addons.mozilla.org", + content: "", + }, + expected: false, + }, + { + title: "null content", + attributionData: { + source: "addons.mozilla.org", + content: null, + }, + expected: false, + }, + { + title: "empty source", + attributionData: { + source: "", + }, + expected: false, + }, + { + title: "null source", + attributionData: { + source: null, + }, + expected: false, + }, + { + title: "valid attribution data for RTAMO with content not encoded", + attributionData: { + source: "addons.mozilla.org", + content: "rta:", + }, + expected: true, + }, + { + title: "valid attribution data for RTAMO with content encoded once", + attributionData: { + source: "addons.mozilla.org", + content: "rta%3A", + }, + expected: true, + }, + { + title: "valid attribution data for RTAMO with content encoded twice", + attributionData: { + source: "addons.mozilla.org", + content: "rta%253A", + }, + expected: true, + }, + { + title: "invalid source", + attributionData: { + source: "www.mozilla.org", + content: "rta%3A", + }, + expected: false, + }, + ]; + + const sandbox = sinon.createSandbox(); + registerCleanupFunction(async () => { + sandbox.restore(); + }); + + const stub = sandbox.stub(AttributionCode, "getCachedAttributionData"); + + for (const { title, attributionData, expected } of TEST_CASES) { + stub.returns(attributionData); + + is( + ASRouterTargeting.Environment.isRTAMO, + expected, + `${title} - Expected isRTAMO to have the expected value` + ); + } + + sandbox.restore(); +}); + +add_task(async function check_isDeviceMigration() { + is( + typeof ASRouterTargeting.Environment.isDeviceMigration, + "boolean", + "Should return a boolean" + ); + + const TEST_CASES = [ + { + title: "no attribution data", + attributionData: {}, + expected: false, + }, + { + title: "null attribution data", + attributionData: null, + expected: false, + }, + { + title: "no campaign", + attributionData: { + source: "support.mozilla.org", + }, + expected: false, + }, + { + title: "empty campaign", + attributionData: { + source: "support.mozilla.org", + campaign: "", + }, + expected: false, + }, + { + title: "null campaign", + attributionData: { + source: "addons.mozilla.org", + campaign: null, + }, + expected: false, + }, + { + title: "empty source", + attributionData: { + source: "", + }, + expected: false, + }, + { + title: "null source", + attributionData: { + source: null, + }, + expected: false, + }, + { + title: "other source", + attributionData: { + source: "www.mozilla.org", + campaign: "migration", + }, + expected: true, + }, + { + title: "valid attribution data for isDeviceMigration", + attributionData: { + source: "support.mozilla.org", + campaign: "migration", + }, + expected: true, + }, + ]; + + const sandbox = sinon.createSandbox(); + registerCleanupFunction(async () => { + sandbox.restore(); + }); + + const stub = sandbox.stub(AttributionCode, "getCachedAttributionData"); + + for (const { title, attributionData, expected } of TEST_CASES) { + stub.returns(attributionData); + + is( + ASRouterTargeting.Environment.isDeviceMigration, + expected, + `${title} - Expected isDeviceMigration to have the expected value` + ); + } + + sandbox.restore(); +}); -- cgit v1.2.3