Backed out 2 changesets (bug 1864896) for causing node failures. CLOSED TREE
[gecko.git] / browser / components / asrouter / tests / browser / browser_asrouter_targeting.js
blob432b4b75a73e253114d8419d9e931ac867067cdb
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",
25 });
27 function sendFormAutofillMessage(name, data) {
28   let actor =
29     gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.getActor(
30       "FormAutofill"
31     );
32   return actor.receiveMessage({ name, data });
35 async function removeAutofillRecords() {
36   let addresses = (
37     await sendFormAutofillMessage("FormAutofill:GetRecords", {
38       collectionName: "addresses",
39     })
40   ).records;
41   if (addresses.length) {
42     let observePromise = TestUtils.topicObserved(
43       "formautofill-storage-changed"
44     );
45     await sendFormAutofillMessage("FormAutofill:RemoveAddresses", {
46       guids: addresses.map(address => address.guid),
47     });
48     await observePromise;
49   }
50   let creditCards = (
51     await sendFormAutofillMessage("FormAutofill:GetRecords", {
52       collectionName: "creditCards",
53     })
54   ).records;
55   if (creditCards.length) {
56     let observePromise = TestUtils.topicObserved(
57       "formautofill-storage-changed"
58     );
59     await sendFormAutofillMessage("FormAutofill:RemoveCreditCards", {
60       guids: creditCards.map(cc => cc.guid),
61     });
62     await observePromise;
63   }
66 // ASRouterTargeting.findMatchingMessage
67 add_task(async function find_matching_message() {
68   const messages = [
69     { id: "foo", targeting: "FOO" },
70     { id: "bar", targeting: "!FOO" },
71   ];
72   const context = { FOO: true };
74   const match = await ASRouterTargeting.findMatchingMessage({
75     messages,
76     context,
77   });
79   is(match, messages[0], "should match and return the correct message");
80 });
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({
87     messages,
88     context,
89   });
91   ok(!match, "should return nothing since no matching message exists");
92 });
94 add_task(async function check_other_error_handling() {
95   let called = false;
96   function onError(...args) {
97     called = true;
98   }
100   const messages = [{ id: "foo", targeting: "foo" }];
101   const context = {
102     get foo() {
103       throw new Error("test error");
104     },
105   };
106   const match = await ASRouterTargeting.findMatchingMessage({
107     messages,
108     context,
109     onError,
110   });
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() {
119   ok(
120     Services.locale.appLocaleAsBCP47,
121     "Services.locale.appLocaleAsBCP47 exists"
122   );
123   const message = {
124     id: "foo",
125     targeting: `locale == "${Services.locale.appLocaleAsBCP47}"`,
126   };
127   is(
128     await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
129     message,
130     "should select correct item when filtering by locale"
131   );
133 add_task(async function check_localeLanguageCode() {
134   const currentLanguageCode = Services.locale.appLocaleAsBCP47.substr(0, 2);
135   is(
136     Services.locale.negotiateLanguages(
137       [currentLanguageCode],
138       [Services.locale.appLocaleAsBCP47]
139     )[0],
140     Services.locale.appLocaleAsBCP47,
141     "currentLanguageCode should resolve to the current locale (e.g en => en-US)"
142   );
143   const message = {
144     id: "foo",
145     targeting: `localeLanguageCode == "${currentLanguageCode}"`,
146   };
147   is(
148     await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
149     message,
150     "should select correct item when filtering by localeLanguageCode"
151   );
154 add_task(async function checkProfileAgeCreated() {
155   let profileAccessor = await ProfileAge();
156   is(
157     await ASRouterTargeting.Environment.profileAgeCreated,
158     await profileAccessor.created,
159     "should return correct profile age creation date"
160   );
162   const message = {
163     id: "foo",
164     targeting: `profileAgeCreated > ${(await profileAccessor.created) - 100}`,
165   };
166   is(
167     await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
168     message,
169     "should select correct item by profile age created"
170   );
173 add_task(async function checkProfileAgeReset() {
174   let profileAccessor = await ProfileAge();
175   is(
176     await ASRouterTargeting.Environment.profileAgeReset,
177     await profileAccessor.reset,
178     "should return correct profile age reset"
179   );
181   const message = {
182     id: "foo",
183     targeting: `profileAgeReset == ${await profileAccessor.reset}`,
184   };
185   is(
186     await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
187     message,
188     "should select correct item by profile age reset"
189   );
192 add_task(async function checkCurrentDate() {
193   let message = {
194     id: "foo",
195     targeting: `currentDate < '${new Date(Date.now() + 5000)}'|date`,
196   };
197   is(
198     await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
199     message,
200     "should select message based on currentDate < timestamp"
201   );
203   message = {
204     id: "foo",
205     targeting: `currentDate > '${new Date(Date.now() - 5000)}'|date`,
206   };
207   is(
208     await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
209     message,
210     "should select message based on currentDate > timestamp"
211   );
214 add_task(async function check_usesFirefoxSync() {
215   await pushPrefs(["services.sync.username", "someone@foo.com"]);
216   is(
217     await ASRouterTargeting.Environment.usesFirefoxSync,
218     true,
219     "should return true if a fx account is set"
220   );
222   const message = { id: "foo", targeting: "usesFirefoxSync" };
223   is(
224     await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
225     message,
226     "should select correct item by usesFirefoxSync"
227   );
230 add_task(async function check_isFxAEnabled() {
231   await pushPrefs(["identity.fxaccounts.enabled", false]);
232   is(
233     await ASRouterTargeting.Environment.isFxAEnabled,
234     false,
235     "should return false if fxa is disabled"
236   );
238   const message = { id: "foo", targeting: "isFxAEnabled" };
239   ok(
240     !(await ASRouterTargeting.findMatchingMessage({ messages: [message] })),
241     "should not select a message if fxa is disabled"
242   );
245 add_task(async function check_isFxAEnabled() {
246   await pushPrefs(["identity.fxaccounts.enabled", true]);
247   is(
248     await ASRouterTargeting.Environment.isFxAEnabled,
249     true,
250     "should return true if fxa is enabled"
251   );
253   const message = { id: "foo", targeting: "isFxAEnabled" };
254   is(
255     await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
256     message,
257     "should select the correct message"
258   );
261 add_task(async function check_isFxASignedIn_false() {
262   await pushPrefs(
263     ["identity.fxaccounts.enabled", true],
264     ["services.sync.username", ""]
265   );
266   const sandbox = sinon.createSandbox();
267   registerCleanupFunction(async () => sandbox.restore());
268   sandbox.stub(FxAccounts.prototype, "getSignedInUser").resolves(null);
269   is(
270     await ASRouterTargeting.Environment.isFxASignedIn,
271     false,
272     "user should not appear signed in"
273   );
275   const message = { id: "foo", targeting: "isFxASignedIn" };
276   isnot(
277     await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
278     message,
279     "should not select the message since user is not signed in"
280   );
282   sandbox.restore();
285 add_task(async function check_isFxASignedIn_true() {
286   await pushPrefs(
287     ["identity.fxaccounts.enabled", true],
288     ["services.sync.username", ""]
289   );
290   const sandbox = sinon.createSandbox();
291   registerCleanupFunction(async () => sandbox.restore());
292   sandbox.stub(FxAccounts.prototype, "getSignedInUser").resolves({});
293   is(
294     await ASRouterTargeting.Environment.isFxASignedIn,
295     true,
296     "user should appear signed in"
297   );
299   const message = { id: "foo", targeting: "isFxASignedIn" };
300   is(
301     await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
302     message,
303     "should select the correct message"
304   );
306   sandbox.restore();
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({
315     messages: [message],
316   });
317   ok(
318     !(results ? JSON.stringify(results) : results),
319     "Should not select any message because bookmarks count is not 0"
320   );
322   const bookmark = await PlacesUtils.bookmarks.insert({
323     parentGuid: PlacesUtils.bookmarks.unfiledGuid,
324     title: "foo",
325     url: "https://mozilla1.com/nowNew",
326   });
328   QueryCache.queries.TotalBookmarksCount.expire();
330   is(
331     await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
332     message,
333     "Should select correct item after bookmarks are added."
334   );
336   // Cleanup
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" };
345   is(
346     await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
347     message,
348     "Should select message because update count > 0"
349   );
351   QueryCache.queries.CheckBrowserNeedsUpdate.setUp(false);
353   is(
354     await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
355     null,
356     "Should not select message because update count == 0"
357   );
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)
364     .sort()
365     .join(",");
366   ok(
367     result.installed.length,
368     "searchEngines.installed should be a non-empty array"
369   );
370   is(
371     result.installed.sort().join(","),
372     expectedInstalled,
373     "searchEngines.installed should be an array of visible search engines"
374   );
375   ok(
376     result.current && typeof result.current === "string",
377     "searchEngines.current should be a truthy string"
378   );
379   is(
380     result.current,
381     (await Services.search.getDefault()).identifier,
382     "searchEngines.current should be the current engine name"
383   );
385   const message = {
386     id: "foo",
387     targeting: `searchEngines[.current == ${
388       (await Services.search.getDefault()).identifier
389     }]`,
390   };
391   is(
392     await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
393     message,
394     "should select correct item by searchEngines.current"
395   );
397   const message2 = {
398     id: "foo",
399     targeting: `searchEngines[${
400       (await Services.search.getAppProvidedEngines())[0].identifier
401     } in .installed]`,
402   };
403   is(
404     await ASRouterTargeting.findMatchingMessage({ messages: [message2] }),
405     message2,
406     "should select correct item by searchEngines.installed"
407   );
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");
414   is(
415     result,
416     expected,
417     "isDefaultBrowser should be equal to ShellService.isDefaultBrowser()"
418   );
419   const message = {
420     id: "foo",
421     targeting: `isDefaultBrowser == ${expected.toString()}`,
422   };
423   is(
424     await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
425     message,
426     "should select correct item by isDefaultBrowser"
427   );
430 add_task(async function checkdevToolsOpenedCount() {
431   await pushPrefs(["devtools.selfxss.count", 5]);
432   is(
433     ASRouterTargeting.Environment.devToolsOpenedCount,
434     5,
435     "devToolsOpenedCount should be equal to devtools.selfxss.count pref value"
436   );
437   const message = { id: "foo", targeting: "devToolsOpenedCount >= 5" };
438   is(
439     await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
440     message,
441     "should select correct item by devToolsOpenedCount"
442   );
445 add_task(async function check_platformName() {
446   const message = {
447     id: "foo",
448     targeting: `platformName == "${AppConstants.platform}"`,
449   };
450   is(
451     await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
452     message,
453     "should select correct item by platformName"
454   );
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({
465     manifest: {
466       browser_specific_settings: { gecko: { id: FAKE_ID } },
467       name: FAKE_NAME,
468       version: FAKE_VERSION,
469     },
470   });
472   await Promise.all([
473     AddonTestUtils.promiseWebExtensionStartup(FAKE_ID),
474     AddonManager.installTemporaryAddon(xpi),
475   ]);
477   const { addons } = await AddonManager.getActiveAddons([
478     "extension",
479     "service",
480   ]);
482   const { addons: asRouterAddons, isFullData } = await ASRouterTargeting
483     .Environment.addonsInfo;
485   ok(
486     addons.every(({ id }) => asRouterAddons[id]),
487     "should contain every addon"
488   );
490   ok(
491     Object.getOwnPropertyNames(asRouterAddons).every(id =>
492       addons.some(addon => addon.id === id)
493     ),
494     "should contain no incorrect addons"
495   );
497   const testAddon = asRouterAddons[FAKE_ID];
499   ok(
500     Object.prototype.hasOwnProperty.call(testAddon, "version") &&
501       testAddon.version === FAKE_VERSION,
502     "should correctly provide `version` property"
503   );
505   ok(
506     Object.prototype.hasOwnProperty.call(testAddon, "type") &&
507       testAddon.type === "extension",
508     "should correctly provide `type` property"
509   );
511   ok(
512     Object.prototype.hasOwnProperty.call(testAddon, "isSystem") &&
513       testAddon.isSystem === false,
514     "should correctly provide `isSystem` property"
515   );
517   ok(
518     Object.prototype.hasOwnProperty.call(testAddon, "isWebExtension") &&
519       testAddon.isWebExtension === true,
520     "should correctly provide `isWebExtension` property"
521   );
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");
528   ok(
529     Object.prototype.hasOwnProperty.call(testAddon, "name") &&
530       testAddon.name === FAKE_NAME,
531     "should correctly provide `name` property from full data"
532   );
534   ok(
535     Object.prototype.hasOwnProperty.call(testAddon, "userDisabled") &&
536       testAddon.userDisabled === false,
537     "should correctly provide `userDisabled` property from full data"
538   );
540   ok(
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"
544   );
547 add_task(async function checkFrecentSites() {
548   const now = Date.now();
549   const timeDaysAgo = numDays => now - numDays * 24 * 60 * 60 * 1000;
551   const visits = [];
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
556   ]) {
557     [...Array(count).keys()].forEach(() =>
558       visits.push({
559         uri,
560         visitDate: visitDate * 1000, // Places expects microseconds
561       })
562     );
563   }
565   await PlacesTestUtils.addVisits(visits);
567   let message = {
568     id: "foo",
569     targeting: "'mozilla3.com' in topFrecentSites|mapToProperty('host')",
570   };
571   is(
572     await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
573     message,
574     "should select correct item by host in topFrecentSites"
575   );
577   message = {
578     id: "foo",
579     targeting: "'non-existent.com' in topFrecentSites|mapToProperty('host')",
580   };
581   ok(
582     !(await ASRouterTargeting.findMatchingMessage({ messages: [message] })),
583     "should not select incorrect item by host in topFrecentSites"
584   );
586   message = {
587     id: "foo",
588     targeting:
589       "'mozilla2.com' in topFrecentSites[.frecency >= 400]|mapToProperty('host')",
590   };
591   is(
592     await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
593     message,
594     "should select correct item when filtering by frecency"
595   );
597   message = {
598     id: "foo",
599     targeting:
600       "'mozilla2.com' in topFrecentSites[.frecency >= 600]|mapToProperty('host')",
601   };
602   ok(
603     !(await ASRouterTargeting.findMatchingMessage({ messages: [message] })),
604     "should not select incorrect item when filtering by frecency"
605   );
607   message = {
608     id: "foo",
609     targeting: `'mozilla2.com' in topFrecentSites[.lastVisitDate >= ${
610       timeDaysAgo(1) - 1
611     }]|mapToProperty('host')`,
612   };
613   is(
614     await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
615     message,
616     "should select correct item when filtering by lastVisitDate"
617   );
619   message = {
620     id: "foo",
621     targeting: `'mozilla2.com' in topFrecentSites[.lastVisitDate >= ${
622       timeDaysAgo(0) - 1
623     }]|mapToProperty('host')`,
624   };
625   ok(
626     !(await ASRouterTargeting.findMatchingMessage({ messages: [message] })),
627     "should not select incorrect item when filtering by lastVisitDate"
628   );
630   message = {
631     id: "foo",
632     targeting: `(topFrecentSites[.frecency >= 900 && .lastVisitDate >= ${
633       timeDaysAgo(1) - 1
634     }]|mapToProperty('host') intersect ['mozilla3.com', 'mozilla2.com', 'mozilla1.com'])|length > 0`,
635   };
636   is(
637     await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
638     message,
639     "should select correct item when filtering by frecency and lastVisitDate with multiple candidate domains"
640   );
642   // Cleanup
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);
654   const sitesToPin = [
655     { url: "https://foo.com" },
656     { url: "https://bloo.com" },
657     { url: "https://floogle.com", searchTopSite: true },
658   ];
659   sitesToPin.forEach(site =>
660     NewTabUtils.pinnedLinks.pin(site, NewTabUtils.pinnedLinks.links.length)
661   );
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]);
665   ok(
666     NewTabUtils.pinnedLinks.links.includes(null),
667     "should have set an item in pinned links to null via unpinning for testing"
668   );
670   let message;
672   message = {
673     id: "foo",
674     targeting: "'https://foo.com' in pinnedSites|mapToProperty('url')",
675   };
676   is(
677     await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
678     message,
679     "should select correct item by url in pinnedSites"
680   );
682   message = {
683     id: "foo",
684     targeting: "'foo.com' in pinnedSites|mapToProperty('host')",
685   };
686   is(
687     await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
688     message,
689     "should select correct item by host in pinnedSites"
690   );
692   message = {
693     id: "foo",
694     targeting:
695       "'floogle.com' in pinnedSites[.searchTopSite == true]|mapToProperty('host')",
696   };
697   is(
698     await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
699     message,
700     "should select correct item by host and searchTopSite in pinnedSites"
701   );
703   // Cleanup
704   sitesToPin.forEach(site => NewTabUtils.pinnedLinks.unpin(site));
706   await clearHistoryAndBookmarks();
707   Services.prefs.clearUserPref("browser.newtabpage.pinned");
708   NewTabUtils.pinnedLinks.resetCache();
709   is(
710     JSON.stringify(NewTabUtils.pinnedLinks.links),
711     originalPin,
712     "should restore pinned sites to its original state"
713   );
716 add_task(async function check_firefox_version() {
717   const message = { id: "foo", targeting: "firefoxVersion > 0" };
718   is(
719     await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
720     message,
721     "should select correct item when filtering by firefox version"
722   );
725 add_task(async function check_region() {
726   Region._setHomeRegion("DE", false);
727   const message = { id: "foo", targeting: "region in ['DE']" };
728   is(
729     await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
730     message,
731     "should select correct item when filtering by firefox geo"
732   );
735 add_task(async function check_browserSettings() {
736   is(
737     await JSON.stringify(ASRouterTargeting.Environment.browserSettings.update),
738     JSON.stringify(TelemetryEnvironment.currentEnvironment.settings.update),
739     "should return correct update info"
740   );
743 add_task(async function check_sync() {
744   is(
745     await ASRouterTargeting.Environment.sync.desktopDevices,
746     Services.prefs.getIntPref("services.sync.clients.devices.desktop", 0),
747     "should return correct desktopDevices info"
748   );
749   is(
750     await ASRouterTargeting.Environment.sync.mobileDevices,
751     Services.prefs.getIntPref("services.sync.clients.devices.mobile", 0),
752     "should return correct mobileDevices info"
753   );
754   is(
755     await ASRouterTargeting.Environment.sync.totalDevices,
756     Services.prefs.getIntPref("services.sync.numClients", 0),
757     "should return correct mobileDevices info"
758   );
761 add_task(async function check_provider_cohorts() {
762   await pushPrefs([
763     "browser.newtabpage.activity-stream.asrouter.providers.onboarding",
764     JSON.stringify({
765       id: "onboarding",
766       messages: [],
767       enabled: true,
768       cohort: "foo",
769     }),
770   ]);
771   await pushPrefs([
772     "browser.newtabpage.activity-stream.asrouter.providers.cfr",
773     JSON.stringify({ id: "cfr", enabled: true, cohort: "bar" }),
774   ]);
775   is(
776     await ASRouterTargeting.Environment.providerCohorts.onboarding,
777     "foo",
778     "should have cohort foo for onboarding"
779   );
780   is(
781     await ASRouterTargeting.Environment.providerCohorts.cfr,
782     "bar",
783     "should have cohort bar for cfr"
784   );
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" },
801     async browser => {
802       is(
803         await ASRouterTargeting.Environment.hasPinnedTabs,
804         false,
805         "No pin tabs yet"
806       );
808       let tab = gBrowser.getTabForBrowser(browser);
809       gBrowser.pinTab(tab);
811       is(
812         await ASRouterTargeting.Environment.hasPinnedTabs,
813         true,
814         "Should detect pinned tab"
815       );
817       gBrowser.unpinTab(tab);
818     }
819   );
822 add_task(async function check_hasAccessedFxAPanel() {
823   is(
824     await ASRouterTargeting.Environment.hasAccessedFxAPanel,
825     false,
826     "Not accessed yet"
827   );
829   await pushPrefs(["identity.fxaccounts.toolbar.accessed", true]);
831   is(
832     await ASRouterTargeting.Environment.hasAccessedFxAPanel,
833     true,
834     "Should detect panel access"
835   );
838 add_task(async function checkCFRFeaturesUserPref() {
839   await pushPrefs([
840     "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features",
841     false,
842   ]);
843   is(
844     ASRouterTargeting.Environment.userPrefs.cfrFeatures,
845     false,
846     "cfrFeature should be false according to pref"
847   );
848   const message = { id: "foo", targeting: "userPrefs.cfrFeatures == false" };
849   is(
850     await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
851     message,
852     "should select correct item by cfrFeature"
853   );
856 add_task(async function checkCFRAddonsUserPref() {
857   await pushPrefs([
858     "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons",
859     false,
860   ]);
861   is(
862     ASRouterTargeting.Environment.userPrefs.cfrAddons,
863     false,
864     "cfrFeature should be false according to pref"
865   );
866   const message = { id: "foo", targeting: "userPrefs.cfrAddons == false" };
867   is(
868     await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
869     message,
870     "should select correct item by cfrAddons"
871   );
874 add_task(async function check_blockedCountByType() {
875   const message = {
876     id: "foo",
877     targeting:
878       "blockedCountByType.cryptominerCount == 0 && blockedCountByType.socialCount == 0",
879   };
881   is(
882     await ASRouterTargeting.findMatchingMessage({ messages: [message] }),
883     message,
884     "should select correct item"
885   );
888 add_task(async function checkPatternMatches() {
889   const now = Date.now();
890   const timeMinutesAgo = numMinutes => now - numMinutes * 60 * 1000;
891   const messages = [
892     {
893       id: "message_with_pattern",
894       targeting: "true",
895       trigger: { id: "frequentVisits", patterns: ["*://*.github.com/"] },
896     },
897   ];
898   const trigger = {
899     id: "frequentVisits",
900     context: {
901       recentVisits: [
902         { timestamp: timeMinutesAgo(33) },
903         { timestamp: timeMinutesAgo(17) },
904         { timestamp: timeMinutesAgo(1) },
905       ],
906     },
907     param: { host: "github.com", url: "https://gist.github.com" },
908   };
910   is(
911     (await ASRouterTargeting.findMatchingMessage({ messages, trigger })).id,
912     "message_with_pattern",
913     "should select PIN_TAB mesage"
914   );
917 add_task(async function checkPatternsValid() {
918   const messages = (await CFRMessageProvider.getMessages()).filter(
919     m => m.trigger?.patterns
920   );
922   for (const message of messages) {
923     Assert.ok(new MatchPatternSet(message.trigger.patterns));
924   }
927 add_task(async function check_isChinaRepack() {
928   const prefDefaultBranch = Services.prefs.getDefaultBranch("distribution.");
929   const messages = [
930     { id: "msg_for_china_repack", targeting: "isChinaRepack == true" },
931     { id: "msg_for_everyone_else", targeting: "isChinaRepack == false" },
932   ];
934   is(
935     await ASRouterTargeting.Environment.isChinaRepack,
936     false,
937     "Fx w/o partner repack info set is not China repack"
938   );
939   is(
940     (await ASRouterTargeting.findMatchingMessage({ messages })).id,
941     "msg_for_everyone_else",
942     "should select the message for non China repack users"
943   );
945   prefDefaultBranch.setCharPref("id", "MozillaOnline");
947   is(
948     await ASRouterTargeting.Environment.isChinaRepack,
949     true,
950     "Fx with `distribution.id` set to `MozillaOnline` is China repack"
951   );
952   is(
953     (await ASRouterTargeting.findMatchingMessage({ messages })).id,
954     "msg_for_china_repack",
955     "should select the message for China repack users"
956   );
958   prefDefaultBranch.setCharPref("id", "Example");
960   is(
961     await ASRouterTargeting.Environment.isChinaRepack,
962     false,
963     "Fx with `distribution.id` set to other string is not China repack"
964   );
965   is(
966     (await ASRouterTargeting.findMatchingMessage({ messages })).id,
967     "msg_for_everyone_else",
968     "should select the message for non China repack users"
969   );
971   prefDefaultBranch.deleteBranch("");
974 add_task(async function check_userId() {
975   await SpecialPowers.pushPrefEnv({
976     set: [["app.normandy.user_id", "foo123"]],
977   });
978   is(
979     await ASRouterTargeting.Environment.userId,
980     "foo123",
981     "should read userID from normandy user id pref"
982   );
985 add_task(async function check_profileRestartCount() {
986   ok(
987     !isNaN(ASRouterTargeting.Environment.profileRestartCount),
988     "it should return a number"
989   );
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");
1029   is(
1030     settings.urls[0].host,
1031     "google.com",
1032     "should be the host name without 'www.'"
1033   );
1035   HomePage.reset();
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");
1048   is(
1049     settings.urls[0].host,
1050     "google.com",
1051     "should be the host name without 'www.'"
1052   );
1053   is(settings.urls[1].url, "https://www.youtube.com", "should be a custom URL");
1054   is(
1055     settings.urls[1].host,
1056     "youtube.com",
1057     "should be the host name without 'www.'"
1058   );
1060   HomePage.reset();
1063 add_task(async function check_homePageSettings_webExtension() {
1064   const extURI =
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");
1077   HomePage.reset();
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() {
1104   const extURI =
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() {
1119   const message = {
1120     ...(await CFRMessageProvider.getMessages()).find(
1121       m => m.id === "YOUTUBE_ENHANCE_3"
1122     ),
1123     targeting: "visitsCount == 3",
1124   };
1125   const trigger = {
1126     id: "openURL",
1127     context: { visitsCount: 3 },
1128     param: { host: "youtube.com", url: "https://www.youtube.com" },
1129   };
1131   is(
1132     (
1133       await ASRouterTargeting.findMatchingMessage({
1134         messages: [message],
1135         trigger,
1136       })
1137     ).id,
1138     message.id,
1139     `should select ${message.id} mesage`
1140   );
1143 add_task(async function check_is_major_upgrade() {
1144   let message = {
1145     id: "check_is_major_upgrade",
1146     targeting: `isMajorUpgrade != undefined && isMajorUpgrade == ${
1147       Cc["@mozilla.org/browser/clh;1"].getService(Ci.nsIBrowserHandler)
1148         .majorUpgrade
1149     }`,
1150   };
1152   is(
1153     (await ASRouterTargeting.findMatchingMessage({ messages: [message] })).id,
1154     message.id,
1155     "Should select the message"
1156   );
1159 add_task(async function check_userMonthlyActivity() {
1160   ok(
1161     Array.isArray(await ASRouterTargeting.Environment.userMonthlyActivity),
1162     "value is an array"
1163   );
1166 add_task(async function check_doesAppNeedPin() {
1167   is(
1168     typeof (await ASRouterTargeting.Environment.doesAppNeedPin),
1169     "boolean",
1170     "Should return a boolean"
1171   );
1174 add_task(async function check_doesAppNeedPrivatePin() {
1175   is(
1176     typeof (await ASRouterTargeting.Environment.doesAppNeedPrivatePin),
1177     "boolean",
1178     "Should return a boolean"
1179   );
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");
1187     return;
1188   }
1190   const bts = Cc["@mozilla.org/backgroundtasks;1"].getService(
1191     Ci.nsIBackgroundTasks
1192   );
1194   // Pretend that this is a background task.
1195   bts.overrideBackgroundTaskNameForTesting("taskName");
1196   is(
1197     await ASRouterTargeting.Environment.isBackgroundTaskMode,
1198     true,
1199     "Is in background task mode"
1200   );
1201   is(
1202     await ASRouterTargeting.Environment.backgroundTaskName,
1203     "taskName",
1204     "Has expected background task name"
1205   );
1207   // Unset, so that subsequent test functions don't see background task mode.
1208   bts.overrideBackgroundTaskNameForTesting(null);
1209   is(
1210     await ASRouterTargeting.Environment.isBackgroundTaskMode,
1211     false,
1212     "Is not in background task mode"
1213   );
1214   is(
1215     await ASRouterTargeting.Environment.backgroundTaskName,
1216     null,
1217     "Has no background task name"
1218   );
1221 add_task(async function check_userPrefersReducedMotion() {
1222   is(
1223     typeof (await ASRouterTargeting.Environment.userPrefersReducedMotion),
1224     "boolean",
1225     "Should return a boolean"
1226   );
1229 add_task(async function test_mr2022Holdback() {
1230   await ExperimentAPI.ready();
1232   ok(
1233     !ASRouterTargeting.Environment.inMr2022Holdback,
1234     "Should not be in holdback (no experiment)"
1235   );
1237   {
1238     const doExperimentCleanup = await ExperimentFakes.enrollWithFeatureConfig({
1239       featureId: "majorRelease2022",
1240       value: {
1241         onboarding: true,
1242       },
1243     });
1245     ok(
1246       !ASRouterTargeting.Environment.inMr2022Holdback,
1247       "Should not be in holdback (onboarding = true)"
1248     );
1250     await doExperimentCleanup();
1251   }
1253   {
1254     const doExperimentCleanup = await ExperimentFakes.enrollWithFeatureConfig({
1255       featureId: "majorRelease2022",
1256       value: {
1257         onboarding: false,
1258       },
1259     });
1261     ok(
1262       ASRouterTargeting.Environment.inMr2022Holdback,
1263       "Should be in holdback (onboarding = false)"
1264     );
1266     await doExperimentCleanup();
1267   }
1270 add_task(async function test_distributionId() {
1271   is(
1272     ASRouterTargeting.Environment.distributionId,
1273     "",
1274     "Should return an empty distribution Id"
1275   );
1277   Services.prefs.getDefaultBranch(null).setCharPref("distribution.id", "test");
1279   is(
1280     ASRouterTargeting.Environment.distributionId,
1281     "test",
1282     "Should return the correct distribution Id"
1283   );
1286 add_task(async function test_fxViewButtonAreaType_default() {
1287   is(
1288     typeof (await ASRouterTargeting.Environment.fxViewButtonAreaType),
1289     "string",
1290     "Should return a string"
1291   );
1293   is(
1294     await ASRouterTargeting.Environment.fxViewButtonAreaType,
1295     "toolbar",
1296     "Should return name of container if button hasn't been removed"
1297   );
1300 add_task(async function test_fxViewButtonAreaType_removed() {
1301   CustomizableUI.removeWidgetFromArea("firefox-view-button");
1303   is(
1304     await ASRouterTargeting.Environment.fxViewButtonAreaType,
1305     null,
1306     "Should return null if button has been removed"
1307   );
1308   CustomizableUI.reset();
1311 add_task(async function test_creditCardsSaved() {
1312   await SpecialPowers.pushPrefEnv({
1313     set: [
1314       ["extensions.formautofill.creditCards.supported", "on"],
1315       ["extensions.formautofill.creditCards.enabled", true],
1316     ],
1317   });
1319   is(
1320     await ASRouterTargeting.Environment.creditCardsSaved,
1321     0,
1322     "Should return 0 when no credit cards are saved"
1323   );
1325   let creditcard = {
1326     "cc-name": "Test User",
1327     "cc-number": "5038146897157463",
1328     "cc-exp-month": "11",
1329     "cc-exp-year": "20",
1330   };
1332   // Intermittently fails on macOS, likely related to Bug 1714221. So, mock the
1333   // autofill actor.
1334   if (AppConstants.platform === "macosx") {
1335     const sandbox = sinon.createSandbox();
1336     registerCleanupFunction(async () => sandbox.restore());
1337     let stub = sandbox
1338       .stub(
1339         gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.getActor(
1340           "FormAutofill"
1341         ),
1342         "receiveMessage"
1343       )
1344       .withArgs(
1345         sandbox.match({
1346           name: "FormAutofill:GetRecords",
1347           data: { collectionName: "creditCards" },
1348         })
1349       )
1350       .resolves({ records: [creditcard] })
1351       .callThrough();
1353     is(
1354       await ASRouterTargeting.Environment.creditCardsSaved,
1355       1,
1356       "Should return 1 when 1 credit card is saved"
1357     );
1358     ok(
1359       stub.calledWithMatch({ name: "FormAutofill:GetRecords" }),
1360       "Targeting called FormAutofill:GetRecords"
1361     );
1363     sandbox.restore();
1364   } else {
1365     let observePromise = TestUtils.topicObserved(
1366       "formautofill-storage-changed"
1367     );
1368     await sendFormAutofillMessage("FormAutofill:SaveCreditCard", {
1369       creditcard,
1370     });
1371     await observePromise;
1373     is(
1374       await ASRouterTargeting.Environment.creditCardsSaved,
1375       1,
1376       "Should return 1 when 1 credit card is saved"
1377     );
1378     await removeAutofillRecords();
1379   }
1381   await SpecialPowers.popPrefEnv();
1384 add_task(async function test_addressesSaved() {
1385   await SpecialPowers.pushPrefEnv({
1386     set: [
1387       ["extensions.formautofill.addresses.supported", "on"],
1388       ["extensions.formautofill.addresses.enabled", true],
1389     ],
1390   });
1392   is(
1393     await ASRouterTargeting.Environment.addressesSaved,
1394     0,
1395     "Should return 0 when no addresses are saved"
1396   );
1398   let observePromise = TestUtils.topicObserved("formautofill-storage-changed");
1399   await sendFormAutofillMessage("FormAutofill:SaveAddress", {
1400     address: {
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",
1409       country: "US",
1410       tel: "+16172535702",
1411       email: "timbl@w3.org",
1412     },
1413   });
1414   await observePromise;
1416   is(
1417     await ASRouterTargeting.Environment.addressesSaved,
1418     1,
1419     "Should return 1 when 1 address is saved"
1420   );
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"],
1432   ]);
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]);
1439   }
1442 add_task(async function check_useEmbeddedMigrationWizard() {
1443   await pushPrefs([
1444     "browser.migrate.content-modal.about-welcome-behavior",
1445     "default",
1446   ]);
1448   ok(!(await ASRouterTargeting.Environment.useEmbeddedMigrationWizard));
1450   await pushPrefs([
1451     "browser.migrate.content-modal.about-welcome-behavior",
1452     "autoclose",
1453   ]);
1455   ok(!(await ASRouterTargeting.Environment.useEmbeddedMigrationWizard));
1457   await pushPrefs([
1458     "browser.migrate.content-modal.about-welcome-behavior",
1459     "embedded",
1460   ]);
1462   ok(await ASRouterTargeting.Environment.useEmbeddedMigrationWizard);
1464   await pushPrefs([
1465     "browser.migrate.content-modal.about-welcome-behavior",
1466     "standalone",
1467   ]);
1469   ok(!(await ASRouterTargeting.Environment.useEmbeddedMigrationWizard));
1472 add_task(async function check_isRTAMO() {
1473   is(
1474     typeof ASRouterTargeting.Environment.isRTAMO,
1475     "boolean",
1476     "Should return a boolean"
1477   );
1479   const TEST_CASES = [
1480     {
1481       title: "no attribution data",
1482       attributionData: {},
1483       expected: false,
1484     },
1485     {
1486       title: "null attribution data",
1487       attributionData: null,
1488       expected: false,
1489     },
1490     {
1491       title: "no content",
1492       attributionData: {
1493         source: "addons.mozilla.org",
1494       },
1495       expected: false,
1496     },
1497     {
1498       title: "empty content",
1499       attributionData: {
1500         source: "addons.mozilla.org",
1501         content: "",
1502       },
1503       expected: false,
1504     },
1505     {
1506       title: "null content",
1507       attributionData: {
1508         source: "addons.mozilla.org",
1509         content: null,
1510       },
1511       expected: false,
1512     },
1513     {
1514       title: "empty source",
1515       attributionData: {
1516         source: "",
1517       },
1518       expected: false,
1519     },
1520     {
1521       title: "null source",
1522       attributionData: {
1523         source: null,
1524       },
1525       expected: false,
1526     },
1527     {
1528       title: "valid attribution data for RTAMO with content not encoded",
1529       attributionData: {
1530         source: "addons.mozilla.org",
1531         content: "rta:<encoded-addon-id>",
1532       },
1533       expected: true,
1534     },
1535     {
1536       title: "valid attribution data for RTAMO with content encoded once",
1537       attributionData: {
1538         source: "addons.mozilla.org",
1539         content: "rta%3A<encoded-addon-id>",
1540       },
1541       expected: true,
1542     },
1543     {
1544       title: "valid attribution data for RTAMO with content encoded twice",
1545       attributionData: {
1546         source: "addons.mozilla.org",
1547         content: "rta%253A<encoded-addon-id>",
1548       },
1549       expected: true,
1550     },
1551     {
1552       title: "invalid source",
1553       attributionData: {
1554         source: "www.mozilla.org",
1555         content: "rta%3A<encoded-addon-id>",
1556       },
1557       expected: false,
1558     },
1559   ];
1561   const sandbox = sinon.createSandbox();
1562   registerCleanupFunction(async () => {
1563     sandbox.restore();
1564   });
1566   const stub = sandbox.stub(AttributionCode, "getCachedAttributionData");
1568   for (const { title, attributionData, expected } of TEST_CASES) {
1569     stub.returns(attributionData);
1571     is(
1572       ASRouterTargeting.Environment.isRTAMO,
1573       expected,
1574       `${title} - Expected isRTAMO to have the expected value`
1575     );
1576   }
1578   sandbox.restore();
1581 add_task(async function check_isDeviceMigration() {
1582   is(
1583     typeof ASRouterTargeting.Environment.isDeviceMigration,
1584     "boolean",
1585     "Should return a boolean"
1586   );
1588   const TEST_CASES = [
1589     {
1590       title: "no attribution data",
1591       attributionData: {},
1592       expected: false,
1593     },
1594     {
1595       title: "null attribution data",
1596       attributionData: null,
1597       expected: false,
1598     },
1599     {
1600       title: "no campaign",
1601       attributionData: {
1602         source: "support.mozilla.org",
1603       },
1604       expected: false,
1605     },
1606     {
1607       title: "empty campaign",
1608       attributionData: {
1609         source: "support.mozilla.org",
1610         campaign: "",
1611       },
1612       expected: false,
1613     },
1614     {
1615       title: "null campaign",
1616       attributionData: {
1617         source: "addons.mozilla.org",
1618         campaign: null,
1619       },
1620       expected: false,
1621     },
1622     {
1623       title: "empty source",
1624       attributionData: {
1625         source: "",
1626       },
1627       expected: false,
1628     },
1629     {
1630       title: "null source",
1631       attributionData: {
1632         source: null,
1633       },
1634       expected: false,
1635     },
1636     {
1637       title: "other source",
1638       attributionData: {
1639         source: "www.mozilla.org",
1640         campaign: "migration",
1641       },
1642       expected: true,
1643     },
1644     {
1645       title: "valid attribution data for isDeviceMigration",
1646       attributionData: {
1647         source: "support.mozilla.org",
1648         campaign: "migration",
1649       },
1650       expected: true,
1651     },
1652   ];
1654   const sandbox = sinon.createSandbox();
1655   registerCleanupFunction(async () => {
1656     sandbox.restore();
1657   });
1659   const stub = sandbox.stub(AttributionCode, "getCachedAttributionData");
1661   for (const { title, attributionData, expected } of TEST_CASES) {
1662     stub.returns(attributionData);
1664     is(
1665       ASRouterTargeting.Environment.isDeviceMigration,
1666       expected,
1667       `${title} - Expected isDeviceMigration to have the expected value`
1668     );
1669   }
1671   sandbox.restore();
1674 add_task(async function check_primaryResolution() {
1675   is(
1676     typeof ASRouterTargeting.Environment.primaryResolution,
1677     "object",
1678     "Should return an object"
1679   );
1681   is(
1682     typeof ASRouterTargeting.Environment.primaryResolution.width,
1683     "number",
1684     "Width property should return a number"
1685   );
1687   is(
1688     typeof ASRouterTargeting.Environment.primaryResolution.height,
1689     "number",
1690     "Height property should return a number"
1691   );
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);