1 ChromeUtils.defineESModuleGetters(this, {
2 AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
3 AddonTestUtils: "resource://testing-common/AddonTestUtils.sys.mjs",
4 AboutNewTab: "resource:///modules/AboutNewTab.sys.mjs",
5 AppConstants: "resource://gre/modules/AppConstants.sys.mjs",
6 ASRouterTargeting: "resource:///modules/asrouter/ASRouterTargeting.sys.mjs",
7 AttributionCode: "resource:///modules/AttributionCode.sys.mjs",
8 BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
9 BuiltInThemes: "resource:///modules/BuiltInThemes.sys.mjs",
10 CFRMessageProvider: "resource:///modules/asrouter/CFRMessageProvider.sys.mjs",
11 ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs",
12 ExperimentFakes: "resource://testing-common/NimbusTestUtils.sys.mjs",
13 FxAccounts: "resource://gre/modules/FxAccounts.sys.mjs",
14 HomePage: "resource:///modules/HomePage.sys.mjs",
15 NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs",
16 NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
17 PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
18 ProfileAge: "resource://gre/modules/ProfileAge.sys.mjs",
19 QueryCache: "resource:///modules/asrouter/ASRouterTargeting.sys.mjs",
20 Region: "resource://gre/modules/Region.sys.mjs",
21 ShellService: "resource:///modules/ShellService.sys.mjs",
22 TargetingContext: "resource://messaging-system/targeting/Targeting.sys.mjs",
23 TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.sys.mjs",
24 TelemetrySession: "resource://gre/modules/TelemetrySession.sys.mjs",
27 function sendFormAutofillMessage(name, data) {
29 gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.getActor(
32 return actor.receiveMessage({ name, data });
35 async function removeAutofillRecords() {
37 await sendFormAutofillMessage("FormAutofill:GetRecords", {
38 collectionName: "addresses",
41 if (addresses.length) {
42 let observePromise = TestUtils.topicObserved(
43 "formautofill-storage-changed"
45 await sendFormAutofillMessage("FormAutofill:RemoveAddresses", {
46 guids: addresses.map(address => address.guid),
51 await sendFormAutofillMessage("FormAutofill:GetRecords", {
52 collectionName: "creditCards",
55 if (creditCards.length) {
56 let observePromise = TestUtils.topicObserved(
57 "formautofill-storage-changed"
59 await sendFormAutofillMessage("FormAutofill:RemoveCreditCards", {
60 guids: creditCards.map(cc => cc.guid),
66 // ASRouterTargeting.findMatchingMessage
67 add_task(async function find_matching_message() {
69 { id: "foo", targeting: "FOO" },
70 { id: "bar", targeting: "!FOO" },
72 const context = { FOO: true };
74 const match = await ASRouterTargeting.findMatchingMessage({
79 is(match, messages[0], "should match and return the correct message");
82 add_task(async function return_nothing_for_no_matching_message() {
83 const messages = [{ id: "bar", targeting: "!FOO" }];
84 const context = { FOO: true };
86 const match = await ASRouterTargeting.findMatchingMessage({
91 ok(!match, "should return nothing since no matching message exists");
94 add_task(async function check_other_error_handling() {
96 function onError(...args) {
100 const messages = [{ id: "foo", targeting: "foo" }];
103 throw new Error("test error");
106 const match = await ASRouterTargeting.findMatchingMessage({
112 ok(!match, "should return nothing since no valid matching message exists");
114 Assert.ok(called, "Attribute error caught");
117 // ASRouterTargeting.Environment
118 add_task(async function check_locale() {
120 Services.locale.appLocaleAsBCP47,
121 "Services.locale.appLocaleAsBCP47 exists"
125 targeting: `locale == "${Services.locale.appLocaleAsBCP47}"`,
128 await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
130 "should select correct item when filtering by locale"
133 add_task(async function check_localeLanguageCode() {
134 const currentLanguageCode = Services.locale.appLocaleAsBCP47.substr(0, 2);
136 Services.locale.negotiateLanguages(
137 [currentLanguageCode],
138 [Services.locale.appLocaleAsBCP47]
140 Services.locale.appLocaleAsBCP47,
141 "currentLanguageCode should resolve to the current locale (e.g en => en-US)"
145 targeting: `localeLanguageCode == "${currentLanguageCode}"`,
148 await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
150 "should select correct item when filtering by localeLanguageCode"
154 add_task(async function checkProfileAgeCreated() {
155 let profileAccessor = await ProfileAge();
157 await ASRouterTargeting.Environment.profileAgeCreated,
158 await profileAccessor.created,
159 "should return correct profile age creation date"
164 targeting: `profileAgeCreated > ${(await profileAccessor.created) - 100}`,
167 await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
169 "should select correct item by profile age created"
173 add_task(async function checkProfileAgeReset() {
174 let profileAccessor = await ProfileAge();
176 await ASRouterTargeting.Environment.profileAgeReset,
177 await profileAccessor.reset,
178 "should return correct profile age reset"
183 targeting: `profileAgeReset == ${await profileAccessor.reset}`,
186 await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
188 "should select correct item by profile age reset"
192 add_task(async function checkCurrentDate() {
195 targeting: `currentDate < '${new Date(Date.now() + 5000)}'|date`,
198 await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
200 "should select message based on currentDate < timestamp"
205 targeting: `currentDate > '${new Date(Date.now() - 5000)}'|date`,
208 await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
210 "should select message based on currentDate > timestamp"
214 add_task(async function check_usesFirefoxSync() {
215 await pushPrefs(["services.sync.username", "someone@foo.com"]);
217 await ASRouterTargeting.Environment.usesFirefoxSync,
219 "should return true if a fx account is set"
222 const message = { id: "foo", targeting: "usesFirefoxSync" };
224 await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
226 "should select correct item by usesFirefoxSync"
230 add_task(async function check_isFxAEnabled() {
231 await pushPrefs(["identity.fxaccounts.enabled", false]);
233 await ASRouterTargeting.Environment.isFxAEnabled,
235 "should return false if fxa is disabled"
238 const message = { id: "foo", targeting: "isFxAEnabled" };
240 !(await ASRouterTargeting.findMatchingMessage({ messages: [message] })),
241 "should not select a message if fxa is disabled"
245 add_task(async function check_isFxAEnabled() {
246 await pushPrefs(["identity.fxaccounts.enabled", true]);
248 await ASRouterTargeting.Environment.isFxAEnabled,
250 "should return true if fxa is enabled"
253 const message = { id: "foo", targeting: "isFxAEnabled" };
255 await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
257 "should select the correct message"
261 add_task(async function check_isFxASignedIn_false() {
263 ["identity.fxaccounts.enabled", true],
264 ["services.sync.username", ""]
266 const sandbox = sinon.createSandbox();
267 registerCleanupFunction(async () => sandbox.restore());
268 sandbox.stub(FxAccounts.prototype, "getSignedInUser").resolves(null);
270 await ASRouterTargeting.Environment.isFxASignedIn,
272 "user should not appear signed in"
275 const message = { id: "foo", targeting: "isFxASignedIn" };
277 await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
279 "should not select the message since user is not signed in"
285 add_task(async function check_isFxASignedIn_true() {
287 ["identity.fxaccounts.enabled", true],
288 ["services.sync.username", ""]
290 const sandbox = sinon.createSandbox();
291 registerCleanupFunction(async () => sandbox.restore());
292 sandbox.stub(FxAccounts.prototype, "getSignedInUser").resolves({});
294 await ASRouterTargeting.Environment.isFxASignedIn,
296 "user should appear signed in"
299 const message = { id: "foo", targeting: "isFxASignedIn" };
301 await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
303 "should select the correct message"
309 add_task(async function check_totalBookmarksCount() {
310 // Make sure we remove default bookmarks so they don't interfere
311 await clearHistoryAndBookmarks();
312 const message = { id: "foo", targeting: "totalBookmarksCount > 0" };
314 const results = await ASRouterTargeting.findMatchingMessage({
318 !(results ? JSON.stringify(results) : results),
319 "Should not select any message because bookmarks count is not 0"
322 const bookmark = await PlacesUtils.bookmarks.insert({
323 parentGuid: PlacesUtils.bookmarks.unfiledGuid,
325 url: "https://mozilla1.com/nowNew",
328 QueryCache.queries.TotalBookmarksCount.expire();
331 await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
333 "Should select correct item after bookmarks are added."
337 await PlacesUtils.bookmarks.remove(bookmark.guid);
340 add_task(async function check_needsUpdate() {
341 QueryCache.queries.CheckBrowserNeedsUpdate.setUp(true);
343 const message = { id: "foo", targeting: "needsUpdate" };
346 await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
348 "Should select message because update count > 0"
351 QueryCache.queries.CheckBrowserNeedsUpdate.setUp(false);
354 await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
356 "Should not select message because update count == 0"
360 add_task(async function checksearchEngines() {
361 const result = await ASRouterTargeting.Environment.searchEngines;
362 const expectedInstalled = (await Services.search.getAppProvidedEngines())
363 .map(engine => engine.identifier)
367 result.installed.length,
368 "searchEngines.installed should be a non-empty array"
371 result.installed.sort().join(","),
373 "searchEngines.installed should be an array of visible search engines"
376 result.current && typeof result.current === "string",
377 "searchEngines.current should be a truthy string"
381 (await Services.search.getDefault()).identifier,
382 "searchEngines.current should be the current engine name"
387 targeting: `searchEngines[.current == ${
388 (await Services.search.getDefault()).identifier
392 await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
394 "should select correct item by searchEngines.current"
399 targeting: `searchEngines[${
400 (await Services.search.getAppProvidedEngines())[0].identifier
404 await ASRouterTargeting.findMatchingMessage({ messages: [message2] }),
406 "should select correct item by searchEngines.installed"
410 add_task(async function checkisDefaultBrowser() {
411 const expected = ShellService.isDefaultBrowser();
412 const result = await ASRouterTargeting.Environment.isDefaultBrowser;
413 is(typeof result, "boolean", "isDefaultBrowser should be a boolean value");
417 "isDefaultBrowser should be equal to ShellService.isDefaultBrowser()"
421 targeting: `isDefaultBrowser == ${expected.toString()}`,
424 await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
426 "should select correct item by isDefaultBrowser"
430 add_task(async function checkdevToolsOpenedCount() {
431 await pushPrefs(["devtools.selfxss.count", 5]);
433 ASRouterTargeting.Environment.devToolsOpenedCount,
435 "devToolsOpenedCount should be equal to devtools.selfxss.count pref value"
437 const message = { id: "foo", targeting: "devToolsOpenedCount >= 5" };
439 await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
441 "should select correct item by devToolsOpenedCount"
445 add_task(async function check_platformName() {
448 targeting: `platformName == "${AppConstants.platform}"`,
451 await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
453 "should select correct item by platformName"
457 AddonTestUtils.initMochitest(this);
459 add_task(async function checkAddonsInfo() {
460 const FAKE_ID = "testaddon@tests.mozilla.org";
461 const FAKE_NAME = "Test Addon";
462 const FAKE_VERSION = "0.5.7";
464 const xpi = AddonTestUtils.createTempWebExtensionFile({
466 browser_specific_settings: { gecko: { id: FAKE_ID } },
468 version: FAKE_VERSION,
473 AddonTestUtils.promiseWebExtensionStartup(FAKE_ID),
474 AddonManager.installTemporaryAddon(xpi),
477 const { addons } = await AddonManager.getActiveAddons([
482 const { addons: asRouterAddons, isFullData } = await ASRouterTargeting
483 .Environment.addonsInfo;
486 addons.every(({ id }) => asRouterAddons[id]),
487 "should contain every addon"
491 Object.getOwnPropertyNames(asRouterAddons).every(id =>
492 addons.some(addon => addon.id === id)
494 "should contain no incorrect addons"
497 const testAddon = asRouterAddons[FAKE_ID];
500 Object.prototype.hasOwnProperty.call(testAddon, "version") &&
501 testAddon.version === FAKE_VERSION,
502 "should correctly provide `version` property"
506 Object.prototype.hasOwnProperty.call(testAddon, "type") &&
507 testAddon.type === "extension",
508 "should correctly provide `type` property"
512 Object.prototype.hasOwnProperty.call(testAddon, "isSystem") &&
513 testAddon.isSystem === false,
514 "should correctly provide `isSystem` property"
518 Object.prototype.hasOwnProperty.call(testAddon, "isWebExtension") &&
519 testAddon.isWebExtension === true,
520 "should correctly provide `isWebExtension` property"
523 // As we installed our test addon the addons database must be initialised, so
524 // (in this test environment) we expect to receive "full" data
526 ok(isFullData, "should receive full data");
529 Object.prototype.hasOwnProperty.call(testAddon, "name") &&
530 testAddon.name === FAKE_NAME,
531 "should correctly provide `name` property from full data"
535 Object.prototype.hasOwnProperty.call(testAddon, "userDisabled") &&
536 testAddon.userDisabled === false,
537 "should correctly provide `userDisabled` property from full data"
541 Object.prototype.hasOwnProperty.call(testAddon, "installDate") &&
542 Math.abs(Date.now() - new Date(testAddon.installDate)) < 60 * 1000,
543 "should correctly provide `installDate` property from full data"
547 add_task(async function checkFrecentSites() {
548 const now = Date.now();
549 const timeDaysAgo = numDays => now - numDays * 24 * 60 * 60 * 1000;
552 for (const [uri, count, visitDate] of [
553 ["https://mozilla1.com/", 10, timeDaysAgo(0)], // frecency 1000
554 ["https://mozilla2.com/", 5, timeDaysAgo(1)], // frecency 500
555 ["https://mozilla3.com/", 1, timeDaysAgo(2)], // frecency 100
557 [...Array(count).keys()].forEach(() =>
560 visitDate: visitDate * 1000, // Places expects microseconds
565 await PlacesTestUtils.addVisits(visits);
569 targeting: "'mozilla3.com' in topFrecentSites|mapToProperty('host')",
572 await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
574 "should select correct item by host in topFrecentSites"
579 targeting: "'non-existent.com' in topFrecentSites|mapToProperty('host')",
582 !(await ASRouterTargeting.findMatchingMessage({ messages: [message] })),
583 "should not select incorrect item by host in topFrecentSites"
589 "'mozilla2.com' in topFrecentSites[.frecency >= 400]|mapToProperty('host')",
592 await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
594 "should select correct item when filtering by frecency"
600 "'mozilla2.com' in topFrecentSites[.frecency >= 600]|mapToProperty('host')",
603 !(await ASRouterTargeting.findMatchingMessage({ messages: [message] })),
604 "should not select incorrect item when filtering by frecency"
609 targeting: `'mozilla2.com' in topFrecentSites[.lastVisitDate >= ${
611 }]|mapToProperty('host')`,
614 await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
616 "should select correct item when filtering by lastVisitDate"
621 targeting: `'mozilla2.com' in topFrecentSites[.lastVisitDate >= ${
623 }]|mapToProperty('host')`,
626 !(await ASRouterTargeting.findMatchingMessage({ messages: [message] })),
627 "should not select incorrect item when filtering by lastVisitDate"
632 targeting: `(topFrecentSites[.frecency >= 900 && .lastVisitDate >= ${
634 }]|mapToProperty('host') intersect ['mozilla3.com', 'mozilla2.com', 'mozilla1.com'])|length > 0`,
637 await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
639 "should select correct item when filtering by frecency and lastVisitDate with multiple candidate domains"
643 await clearHistoryAndBookmarks();
646 add_task(async function check_pinned_sites() {
647 // Fresh profiles come with an empty set of pinned websites (pref doesn't
648 // exist). Search shortcut topsites make this test more complicated because
649 // the feature pins a new website on startup. Behaviour can vary when running
650 // with --verify so it's more predictable to clear pins entirely.
651 Services.prefs.clearUserPref("browser.newtabpage.pinned");
652 NewTabUtils.pinnedLinks.resetCache();
653 const originalPin = JSON.stringify(NewTabUtils.pinnedLinks.links);
655 { url: "https://foo.com" },
656 { url: "https://bloo.com" },
657 { url: "https://floogle.com", searchTopSite: true },
659 sitesToPin.forEach(site =>
660 NewTabUtils.pinnedLinks.pin(site, NewTabUtils.pinnedLinks.links.length)
663 // Unpinning adds null to the list of pinned sites, which we should test that we handle gracefully for our targeting
664 NewTabUtils.pinnedLinks.unpin(sitesToPin[1]);
666 NewTabUtils.pinnedLinks.links.includes(null),
667 "should have set an item in pinned links to null via unpinning for testing"
674 targeting: "'https://foo.com' in pinnedSites|mapToProperty('url')",
677 await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
679 "should select correct item by url in pinnedSites"
684 targeting: "'foo.com' in pinnedSites|mapToProperty('host')",
687 await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
689 "should select correct item by host in pinnedSites"
695 "'floogle.com' in pinnedSites[.searchTopSite == true]|mapToProperty('host')",
698 await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
700 "should select correct item by host and searchTopSite in pinnedSites"
704 sitesToPin.forEach(site => NewTabUtils.pinnedLinks.unpin(site));
706 await clearHistoryAndBookmarks();
707 Services.prefs.clearUserPref("browser.newtabpage.pinned");
708 NewTabUtils.pinnedLinks.resetCache();
710 JSON.stringify(NewTabUtils.pinnedLinks.links),
712 "should restore pinned sites to its original state"
716 add_task(async function check_firefox_version() {
717 const message = { id: "foo", targeting: "firefoxVersion > 0" };
719 await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
721 "should select correct item when filtering by firefox version"
725 add_task(async function check_region() {
726 Region._setHomeRegion("DE", false);
727 const message = { id: "foo", targeting: "region in ['DE']" };
729 await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
731 "should select correct item when filtering by firefox geo"
735 add_task(async function check_browserSettings() {
737 await JSON.stringify(ASRouterTargeting.Environment.browserSettings.update),
738 JSON.stringify(TelemetryEnvironment.currentEnvironment.settings.update),
739 "should return correct update info"
743 add_task(async function check_sync() {
745 await ASRouterTargeting.Environment.sync.desktopDevices,
746 Services.prefs.getIntPref("services.sync.clients.devices.desktop", 0),
747 "should return correct desktopDevices info"
750 await ASRouterTargeting.Environment.sync.mobileDevices,
751 Services.prefs.getIntPref("services.sync.clients.devices.mobile", 0),
752 "should return correct mobileDevices info"
755 await ASRouterTargeting.Environment.sync.totalDevices,
756 Services.prefs.getIntPref("services.sync.numClients", 0),
757 "should return correct mobileDevices info"
761 add_task(async function check_provider_cohorts() {
763 "browser.newtabpage.activity-stream.asrouter.providers.onboarding",
772 "browser.newtabpage.activity-stream.asrouter.providers.cfr",
773 JSON.stringify({ id: "cfr", enabled: true, cohort: "bar" }),
776 await ASRouterTargeting.Environment.providerCohorts.onboarding,
778 "should have cohort foo for onboarding"
781 await ASRouterTargeting.Environment.providerCohorts.cfr,
783 "should have cohort bar for cfr"
787 add_task(async function check_xpinstall_enabled() {
788 // should default to true if pref doesn't exist
789 is(await ASRouterTargeting.Environment.xpinstallEnabled, true);
790 // flip to false, check targeting reflects that
791 await pushPrefs(["xpinstall.enabled", false]);
792 is(await ASRouterTargeting.Environment.xpinstallEnabled, false);
793 // flip to true, check targeting reflects that
794 await pushPrefs(["xpinstall.enabled", true]);
795 is(await ASRouterTargeting.Environment.xpinstallEnabled, true);
798 add_task(async function check_pinned_tabs() {
799 await BrowserTestUtils.withNewTab(
800 { gBrowser, url: "about:blank" },
803 await ASRouterTargeting.Environment.hasPinnedTabs,
808 let tab = gBrowser.getTabForBrowser(browser);
809 gBrowser.pinTab(tab);
812 await ASRouterTargeting.Environment.hasPinnedTabs,
814 "Should detect pinned tab"
817 gBrowser.unpinTab(tab);
822 add_task(async function check_hasAccessedFxAPanel() {
824 await ASRouterTargeting.Environment.hasAccessedFxAPanel,
829 await pushPrefs(["identity.fxaccounts.toolbar.accessed", true]);
832 await ASRouterTargeting.Environment.hasAccessedFxAPanel,
834 "Should detect panel access"
838 add_task(async function checkCFRFeaturesUserPref() {
840 "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features",
844 ASRouterTargeting.Environment.userPrefs.cfrFeatures,
846 "cfrFeature should be false according to pref"
848 const message = { id: "foo", targeting: "userPrefs.cfrFeatures == false" };
850 await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
852 "should select correct item by cfrFeature"
856 add_task(async function checkCFRAddonsUserPref() {
858 "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons",
862 ASRouterTargeting.Environment.userPrefs.cfrAddons,
864 "cfrFeature should be false according to pref"
866 const message = { id: "foo", targeting: "userPrefs.cfrAddons == false" };
868 await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
870 "should select correct item by cfrAddons"
874 add_task(async function check_blockedCountByType() {
878 "blockedCountByType.cryptominerCount == 0 && blockedCountByType.socialCount == 0",
882 await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
884 "should select correct item"
888 add_task(async function checkPatternMatches() {
889 const now = Date.now();
890 const timeMinutesAgo = numMinutes => now - numMinutes * 60 * 1000;
893 id: "message_with_pattern",
895 trigger: { id: "frequentVisits", patterns: ["*://*.github.com/"] },
899 id: "frequentVisits",
902 { timestamp: timeMinutesAgo(33) },
903 { timestamp: timeMinutesAgo(17) },
904 { timestamp: timeMinutesAgo(1) },
907 param: { host: "github.com", url: "https://gist.github.com" },
911 (await ASRouterTargeting.findMatchingMessage({ messages, trigger })).id,
912 "message_with_pattern",
913 "should select PIN_TAB mesage"
917 add_task(async function checkPatternsValid() {
918 const messages = (await CFRMessageProvider.getMessages()).filter(
919 m => m.trigger?.patterns
922 for (const message of messages) {
923 Assert.ok(new MatchPatternSet(message.trigger.patterns));
927 add_task(async function check_isChinaRepack() {
928 const prefDefaultBranch = Services.prefs.getDefaultBranch("distribution.");
930 { id: "msg_for_china_repack", targeting: "isChinaRepack == true" },
931 { id: "msg_for_everyone_else", targeting: "isChinaRepack == false" },
935 await ASRouterTargeting.Environment.isChinaRepack,
937 "Fx w/o partner repack info set is not China repack"
940 (await ASRouterTargeting.findMatchingMessage({ messages })).id,
941 "msg_for_everyone_else",
942 "should select the message for non China repack users"
945 prefDefaultBranch.setCharPref("id", "MozillaOnline");
948 await ASRouterTargeting.Environment.isChinaRepack,
950 "Fx with `distribution.id` set to `MozillaOnline` is China repack"
953 (await ASRouterTargeting.findMatchingMessage({ messages })).id,
954 "msg_for_china_repack",
955 "should select the message for China repack users"
958 prefDefaultBranch.setCharPref("id", "Example");
961 await ASRouterTargeting.Environment.isChinaRepack,
963 "Fx with `distribution.id` set to other string is not China repack"
966 (await ASRouterTargeting.findMatchingMessage({ messages })).id,
967 "msg_for_everyone_else",
968 "should select the message for non China repack users"
971 prefDefaultBranch.deleteBranch("");
974 add_task(async function check_userId() {
975 await SpecialPowers.pushPrefEnv({
976 set: [["app.normandy.user_id", "foo123"]],
979 await ASRouterTargeting.Environment.userId,
981 "should read userID from normandy user id pref"
985 add_task(async function check_profileRestartCount() {
987 !isNaN(ASRouterTargeting.Environment.profileRestartCount),
988 "it should return a number"
992 add_task(async function check_homePageSettings_default() {
993 let settings = ASRouterTargeting.Environment.homePageSettings;
995 ok(settings.isDefault, "should set as default");
996 ok(!settings.isLocked, "should not set as locked");
997 ok(!settings.isWebExt, "should not be web extension");
998 ok(!settings.isCustomUrl, "should not be custom URL");
999 is(settings.urls.length, 1, "should be an 1-entry array");
1000 is(settings.urls[0].url, "about:home", "should be about:home");
1001 is(settings.urls[0].host, "", "should be an empty string");
1004 add_task(async function check_homePageSettings_locked() {
1005 const PREF = "browser.startup.homepage";
1006 Services.prefs.lockPref(PREF);
1007 let settings = ASRouterTargeting.Environment.homePageSettings;
1009 ok(settings.isDefault, "should set as default");
1010 ok(settings.isLocked, "should set as locked");
1011 ok(!settings.isWebExt, "should not be web extension");
1012 ok(!settings.isCustomUrl, "should not be custom URL");
1013 is(settings.urls.length, 1, "should be an 1-entry array");
1014 is(settings.urls[0].url, "about:home", "should be about:home");
1015 is(settings.urls[0].host, "", "should be an empty string");
1016 Services.prefs.unlockPref(PREF);
1019 add_task(async function check_homePageSettings_customURL() {
1020 await HomePage.set("https://www.google.com");
1021 let settings = ASRouterTargeting.Environment.homePageSettings;
1023 ok(!settings.isDefault, "should not be the default");
1024 ok(!settings.isLocked, "should set as locked");
1025 ok(!settings.isWebExt, "should not be web extension");
1026 ok(settings.isCustomUrl, "should be custom URL");
1027 is(settings.urls.length, 1, "should be an 1-entry array");
1028 is(settings.urls[0].url, "https://www.google.com", "should be a custom URL");
1030 settings.urls[0].host,
1032 "should be the host name without 'www.'"
1038 add_task(async function check_homePageSettings_customURL_multiple() {
1039 await HomePage.set("https://www.google.com|https://www.youtube.com");
1040 let settings = ASRouterTargeting.Environment.homePageSettings;
1042 ok(!settings.isDefault, "should not be the default");
1043 ok(!settings.isLocked, "should not set as locked");
1044 ok(!settings.isWebExt, "should not be web extension");
1045 ok(settings.isCustomUrl, "should be custom URL");
1046 is(settings.urls.length, 2, "should be a 2-entry array");
1047 is(settings.urls[0].url, "https://www.google.com", "should be a custom URL");
1049 settings.urls[0].host,
1051 "should be the host name without 'www.'"
1053 is(settings.urls[1].url, "https://www.youtube.com", "should be a custom URL");
1055 settings.urls[1].host,
1057 "should be the host name without 'www.'"
1063 add_task(async function check_homePageSettings_webExtension() {
1065 "moz-extension://0d735548-ba3c-aa43-a0e4-7089584fbb53/homepage.html";
1066 await HomePage.set(extURI);
1067 let settings = ASRouterTargeting.Environment.homePageSettings;
1069 ok(!settings.isDefault, "should not be the default");
1070 ok(!settings.isLocked, "should not set as locked");
1071 ok(settings.isWebExt, "should be a web extension");
1072 ok(!settings.isCustomUrl, "should be custom URL");
1073 is(settings.urls.length, 1, "should be an 1-entry array");
1074 is(settings.urls[0].url, extURI, "should be a webExtension URI");
1075 is(settings.urls[0].host, "", "should be an empty string");
1080 add_task(async function check_newtabSettings_default() {
1081 let settings = ASRouterTargeting.Environment.newtabSettings;
1083 ok(settings.isDefault, "should set as default");
1084 ok(!settings.isWebExt, "should not be web extension");
1085 ok(!settings.isCustomUrl, "should not be custom URL");
1086 is(settings.url, "about:newtab", "should be about:home");
1087 is(settings.host, "", "should be an empty string");
1090 add_task(async function check_newTabSettings_customURL() {
1091 AboutNewTab.newTabURL = "https://www.google.com";
1092 let settings = ASRouterTargeting.Environment.newtabSettings;
1094 ok(!settings.isDefault, "should not be the default");
1095 ok(!settings.isWebExt, "should not be web extension");
1096 ok(settings.isCustomUrl, "should be custom URL");
1097 is(settings.url, "https://www.google.com", "should be a custom URL");
1098 is(settings.host, "google.com", "should be the host name without 'www.'");
1100 AboutNewTab.resetNewTabURL();
1103 add_task(async function check_newTabSettings_webExtension() {
1105 "moz-extension://0d735548-ba3c-aa43-a0e4-7089584fbb53/homepage.html";
1106 AboutNewTab.newTabURL = extURI;
1107 let settings = ASRouterTargeting.Environment.newtabSettings;
1109 ok(!settings.isDefault, "should not be the default");
1110 ok(settings.isWebExt, "should not be web extension");
1111 ok(!settings.isCustomUrl, "should be custom URL");
1112 is(settings.url, extURI, "should be the web extension URI");
1113 is(settings.host, "", "should be an empty string");
1115 AboutNewTab.resetNewTabURL();
1118 add_task(async function check_openUrlTrigger_context() {
1120 ...(await CFRMessageProvider.getMessages()).find(
1121 m => m.id === "YOUTUBE_ENHANCE_3"
1123 targeting: "visitsCount == 3",
1127 context: { visitsCount: 3 },
1128 param: { host: "youtube.com", url: "https://www.youtube.com" },
1133 await ASRouterTargeting.findMatchingMessage({
1134 messages: [message],
1139 `should select ${message.id} mesage`
1143 add_task(async function check_is_major_upgrade() {
1145 id: "check_is_major_upgrade",
1146 targeting: `isMajorUpgrade != undefined && isMajorUpgrade == ${
1147 Cc["@mozilla.org/browser/clh;1"].getService(Ci.nsIBrowserHandler)
1153 (await ASRouterTargeting.findMatchingMessage({ messages: [message] })).id,
1155 "Should select the message"
1159 add_task(async function check_userMonthlyActivity() {
1161 Array.isArray(await ASRouterTargeting.Environment.userMonthlyActivity),
1166 add_task(async function check_doesAppNeedPin() {
1168 typeof (await ASRouterTargeting.Environment.doesAppNeedPin),
1170 "Should return a boolean"
1174 add_task(async function check_doesAppNeedPrivatePin() {
1176 typeof (await ASRouterTargeting.Environment.doesAppNeedPrivatePin),
1178 "Should return a boolean"
1182 add_task(async function check_isBackgroundTaskMode() {
1183 if (!AppConstants.MOZ_BACKGROUNDTASKS) {
1184 // `mochitest-browser` suite `add_task` does not yet support
1185 // `properties.skip_if`.
1186 ok(true, "Skipping because !AppConstants.MOZ_BACKGROUNDTASKS");
1190 const bts = Cc["@mozilla.org/backgroundtasks;1"].getService(
1191 Ci.nsIBackgroundTasks
1194 // Pretend that this is a background task.
1195 bts.overrideBackgroundTaskNameForTesting("taskName");
1197 await ASRouterTargeting.Environment.isBackgroundTaskMode,
1199 "Is in background task mode"
1202 await ASRouterTargeting.Environment.backgroundTaskName,
1204 "Has expected background task name"
1207 // Unset, so that subsequent test functions don't see background task mode.
1208 bts.overrideBackgroundTaskNameForTesting(null);
1210 await ASRouterTargeting.Environment.isBackgroundTaskMode,
1212 "Is not in background task mode"
1215 await ASRouterTargeting.Environment.backgroundTaskName,
1217 "Has no background task name"
1221 add_task(async function check_userPrefersReducedMotion() {
1223 typeof (await ASRouterTargeting.Environment.userPrefersReducedMotion),
1225 "Should return a boolean"
1229 add_task(async function test_mr2022Holdback() {
1230 await ExperimentAPI.ready();
1233 !ASRouterTargeting.Environment.inMr2022Holdback,
1234 "Should not be in holdback (no experiment)"
1238 const doExperimentCleanup = await ExperimentFakes.enrollWithFeatureConfig({
1239 featureId: "majorRelease2022",
1246 !ASRouterTargeting.Environment.inMr2022Holdback,
1247 "Should not be in holdback (onboarding = true)"
1250 await doExperimentCleanup();
1254 const doExperimentCleanup = await ExperimentFakes.enrollWithFeatureConfig({
1255 featureId: "majorRelease2022",
1262 ASRouterTargeting.Environment.inMr2022Holdback,
1263 "Should be in holdback (onboarding = false)"
1266 await doExperimentCleanup();
1270 add_task(async function test_distributionId() {
1272 ASRouterTargeting.Environment.distributionId,
1274 "Should return an empty distribution Id"
1277 Services.prefs.getDefaultBranch(null).setCharPref("distribution.id", "test");
1280 ASRouterTargeting.Environment.distributionId,
1282 "Should return the correct distribution Id"
1286 add_task(async function test_fxViewButtonAreaType_default() {
1288 typeof (await ASRouterTargeting.Environment.fxViewButtonAreaType),
1290 "Should return a string"
1294 await ASRouterTargeting.Environment.fxViewButtonAreaType,
1296 "Should return name of container if button hasn't been removed"
1300 add_task(async function test_fxViewButtonAreaType_removed() {
1301 CustomizableUI.removeWidgetFromArea("firefox-view-button");
1304 await ASRouterTargeting.Environment.fxViewButtonAreaType,
1306 "Should return null if button has been removed"
1308 CustomizableUI.reset();
1311 add_task(async function test_creditCardsSaved() {
1312 await SpecialPowers.pushPrefEnv({
1314 ["extensions.formautofill.creditCards.supported", "on"],
1315 ["extensions.formautofill.creditCards.enabled", true],
1320 await ASRouterTargeting.Environment.creditCardsSaved,
1322 "Should return 0 when no credit cards are saved"
1326 "cc-name": "Test User",
1327 "cc-number": "5038146897157463",
1328 "cc-exp-month": "11",
1329 "cc-exp-year": "20",
1332 // Intermittently fails on macOS, likely related to Bug 1714221. So, mock the
1334 if (AppConstants.platform === "macosx") {
1335 const sandbox = sinon.createSandbox();
1336 registerCleanupFunction(async () => sandbox.restore());
1339 gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.getActor(
1346 name: "FormAutofill:GetRecords",
1347 data: { collectionName: "creditCards" },
1350 .resolves({ records: [creditcard] })
1354 await ASRouterTargeting.Environment.creditCardsSaved,
1356 "Should return 1 when 1 credit card is saved"
1359 stub.calledWithMatch({ name: "FormAutofill:GetRecords" }),
1360 "Targeting called FormAutofill:GetRecords"
1365 let observePromise = TestUtils.topicObserved(
1366 "formautofill-storage-changed"
1368 await sendFormAutofillMessage("FormAutofill:SaveCreditCard", {
1371 await observePromise;
1374 await ASRouterTargeting.Environment.creditCardsSaved,
1376 "Should return 1 when 1 credit card is saved"
1378 await removeAutofillRecords();
1381 await SpecialPowers.popPrefEnv();
1384 add_task(async function test_addressesSaved() {
1385 await SpecialPowers.pushPrefEnv({
1387 ["extensions.formautofill.addresses.supported", "on"],
1388 ["extensions.formautofill.addresses.enabled", true],
1393 await ASRouterTargeting.Environment.addressesSaved,
1395 "Should return 0 when no addresses are saved"
1398 let observePromise = TestUtils.topicObserved("formautofill-storage-changed");
1399 await sendFormAutofillMessage("FormAutofill:SaveAddress", {
1401 "given-name": "John",
1402 "additional-name": "R.",
1403 "family-name": "Smith",
1404 organization: "World Wide Web Consortium",
1405 "street-address": "32 Vassar Street\nMIT Room 32-G524",
1406 "address-level2": "Cambridge",
1407 "address-level1": "MA",
1408 "postal-code": "02139",
1410 tel: "+16172535702",
1411 email: "timbl@w3.org",
1414 await observePromise;
1417 await ASRouterTargeting.Environment.addressesSaved,
1419 "Should return 1 when 1 address is saved"
1422 await removeAutofillRecords();
1423 await SpecialPowers.popPrefEnv();
1426 add_task(async function test_migrationInteractions() {
1427 const PREF_GETTER_MAPPING = new Map([
1428 ["browser.migrate.interactions.bookmarks", "hasMigratedBookmarks"],
1429 ["browser.migrate.interactions.csvpasswords", "hasMigratedCSVPasswords"],
1430 ["browser.migrate.interactions.history", "hasMigratedHistory"],
1431 ["browser.migrate.interactions.passwords", "hasMigratedPasswords"],
1434 for (let [pref, getterName] of PREF_GETTER_MAPPING) {
1435 await pushPrefs([pref, false]);
1436 ok(!(await ASRouterTargeting.Environment[getterName]));
1437 await pushPrefs([pref, true]);
1438 ok(await ASRouterTargeting.Environment[getterName]);
1442 add_task(async function check_useEmbeddedMigrationWizard() {
1444 "browser.migrate.content-modal.about-welcome-behavior",
1448 ok(!(await ASRouterTargeting.Environment.useEmbeddedMigrationWizard));
1451 "browser.migrate.content-modal.about-welcome-behavior",
1455 ok(!(await ASRouterTargeting.Environment.useEmbeddedMigrationWizard));
1458 "browser.migrate.content-modal.about-welcome-behavior",
1462 ok(await ASRouterTargeting.Environment.useEmbeddedMigrationWizard);
1465 "browser.migrate.content-modal.about-welcome-behavior",
1469 ok(!(await ASRouterTargeting.Environment.useEmbeddedMigrationWizard));
1472 add_task(async function check_isRTAMO() {
1474 typeof ASRouterTargeting.Environment.isRTAMO,
1476 "Should return a boolean"
1479 const TEST_CASES = [
1481 title: "no attribution data",
1482 attributionData: {},
1486 title: "null attribution data",
1487 attributionData: null,
1491 title: "no content",
1493 source: "addons.mozilla.org",
1498 title: "empty content",
1500 source: "addons.mozilla.org",
1506 title: "null content",
1508 source: "addons.mozilla.org",
1514 title: "empty source",
1521 title: "null source",
1528 title: "valid attribution data for RTAMO with content not encoded",
1530 source: "addons.mozilla.org",
1531 content: "rta:<encoded-addon-id>",
1536 title: "valid attribution data for RTAMO with content encoded once",
1538 source: "addons.mozilla.org",
1539 content: "rta%3A<encoded-addon-id>",
1544 title: "valid attribution data for RTAMO with content encoded twice",
1546 source: "addons.mozilla.org",
1547 content: "rta%253A<encoded-addon-id>",
1552 title: "invalid source",
1554 source: "www.mozilla.org",
1555 content: "rta%3A<encoded-addon-id>",
1561 const sandbox = sinon.createSandbox();
1562 registerCleanupFunction(async () => {
1566 const stub = sandbox.stub(AttributionCode, "getCachedAttributionData");
1568 for (const { title, attributionData, expected } of TEST_CASES) {
1569 stub.returns(attributionData);
1572 ASRouterTargeting.Environment.isRTAMO,
1574 `${title} - Expected isRTAMO to have the expected value`
1581 add_task(async function check_isDeviceMigration() {
1583 typeof ASRouterTargeting.Environment.isDeviceMigration,
1585 "Should return a boolean"
1588 const TEST_CASES = [
1590 title: "no attribution data",
1591 attributionData: {},
1595 title: "null attribution data",
1596 attributionData: null,
1600 title: "no campaign",
1602 source: "support.mozilla.org",
1607 title: "empty campaign",
1609 source: "support.mozilla.org",
1615 title: "null campaign",
1617 source: "addons.mozilla.org",
1623 title: "empty source",
1630 title: "null source",
1637 title: "other source",
1639 source: "www.mozilla.org",
1640 campaign: "migration",
1645 title: "valid attribution data for isDeviceMigration",
1647 source: "support.mozilla.org",
1648 campaign: "migration",
1654 const sandbox = sinon.createSandbox();
1655 registerCleanupFunction(async () => {
1659 const stub = sandbox.stub(AttributionCode, "getCachedAttributionData");
1661 for (const { title, attributionData, expected } of TEST_CASES) {
1662 stub.returns(attributionData);
1665 ASRouterTargeting.Environment.isDeviceMigration,
1667 `${title} - Expected isDeviceMigration to have the expected value`
1674 add_task(async function check_primaryResolution() {
1676 typeof ASRouterTargeting.Environment.primaryResolution,
1678 "Should return an object"
1682 typeof ASRouterTargeting.Environment.primaryResolution.width,
1684 "Width property should return a number"
1688 typeof ASRouterTargeting.Environment.primaryResolution.height,
1690 "Height property should return a number"
1694 add_task(async function check_archBits() {
1695 const bits = ASRouterTargeting.Environment.archBits;
1696 is(typeof bits, "number", "archBits should be a number");
1697 ok(bits === 32 || bits === 64, "archBits is either 32 or 64");
1700 add_task(async function check_memoryMB() {
1701 const memory = ASRouterTargeting.Environment.memoryMB;
1702 is(typeof memory, "number", "Memory is a number");
1703 // To make sure we get a sensible number we verify that whatever system
1704 // runs this unit test it has between 500MB and 1TB of RAM.
1705 ok(memory > 500 && memory < 5_000_000);