/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; const { InAppNotifications } = ChromeUtils.importESModule( "moz-src:///comm/mail/components/inappnotifications/modules/InAppNotifications.sys.mjs" ); const { NotificationManager } = ChromeUtils.importESModule( "moz-src:///comm/mail/components/inappnotifications/modules/NotificationManager.sys.mjs" ); const { BrowserTestUtils } = ChromeUtils.importESModule( "resource://testing-common/BrowserTestUtils.sys.mjs" ); const { TestUtils } = ChromeUtils.importESModule( "resource://testing-common/TestUtils.sys.mjs" ); const { sinon } = ChromeUtils.importESModule( "resource://testing-common/Sinon.sys.mjs" ); const { clearInterval, clearTimeout } = ChromeUtils.importESModule( "resource://gre/modules/Timer.sys.mjs" ); const { NotificationUpdater } = ChromeUtils.importESModule( "moz-src:///comm/mail/components/inappnotifications/modules/NotificationUpdater.sys.mjs" ); const SAFETY_MARGIN_MS = 100000; function getMockNotifications() { const now = Date.now(); const startDate = new Date(now - SAFETY_MARGIN_MS).toISOString(); const endDate = new Date(now + SAFETY_MARGIN_MS).toISOString(); return [ { id: "foo", title: "lorem ipsum", start_at: startDate, end_at: endDate, severity: 1, targeting: {}, type: "donation", }, { id: "bar", title: "dolor sit amet", start_at: startDate, end_at: endDate, severity: 5, targeting: {}, type: "donation", }, ]; } add_setup(async () => { do_get_profile(); await InAppNotifications.init(); NotificationManager._PER_TIME_UNIT = 1; registerCleanupFunction(() => { NotificationUpdater._clearStateForTests(); clearTimeout(InAppNotifications._showNotificationTimer); }); }); add_task(function test_initializedData() { Assert.ok(InAppNotifications._jsonFile.dataReady, "JSON file is ready"); Assert.ok( InAppNotifications.notificationManager instanceof NotificationManager, "Should expose a NotificationManager instance" ); Assert.equal( typeof NotificationUpdater.onUpdate, "function", "Should register update callback" ); Assert.ok(NotificationUpdater._timeout, "Should initialize updater"); Assert.deepEqual( InAppNotifications._jsonFile.data.interactedWith, [], "Should initialize interacted with notifications" ); }); add_task(function test_initializedDataWithoutBuiltinNotifications() { Assert.deepEqual( InAppNotifications.getNotifications(), [], "Should initialize notifications with an empty array" ); Assert.deepEqual( InAppNotifications._jsonFile.data.seeds, {}, "Should initialize seeds" ); }); add_task(async function test_noReinitialization() { const currentNotificationManager = InAppNotifications.notificationManager; await InAppNotifications.init(); Assert.strictEqual( InAppNotifications.notificationManager, currentNotificationManager, "Should not initialize a new notification manager" ); }); add_task(async function test_markAsInteractedWith() { const mockData = getMockNotifications(); await InAppNotifications.updateNotifications(mockData); Assert.deepEqual( InAppNotifications.getNotifications(), mockData, "Should have all notifications" ); Assert.deepEqual( InAppNotifications._jsonFile.data.interactedWith, [], "Should start without any notifications having been interacted with" ); InAppNotifications.markAsInteractedWith("foo"); await InAppNotifications.updateNotifications(mockData); Assert.deepEqual( InAppNotifications.getNotifications(), [mockData[1]], "Should only have uninteracted notifications" ); InAppNotifications.markAsInteractedWith("foo"); Assert.deepEqual( InAppNotifications._jsonFile.data.interactedWith, ["foo"], "Should only store the ID once" ); await InAppNotifications.updateNotifications([]); Assert.deepEqual( InAppNotifications._jsonFile.data.interactedWith, [], "Should clear interaction store when there are no notifications" ); }); add_task(async function test_getNotifications_expiry() { const now = Date.now(); const mockData = [ { id: "foo", title: "lorem ipsum", start_at: new Date(now - SAFETY_MARGIN_MS).toISOString(), end_at: new Date(now + SAFETY_MARGIN_MS).toISOString(), targeting: {}, type: "donation", }, { id: "future bar", title: "dolor sit amet", start_at: new Date(now + SAFETY_MARGIN_MS).toISOString(), end_at: new Date(now + 2 * SAFETY_MARGIN_MS).toISOString(), targeting: {}, type: "donation", }, { id: "past bar", title: "back home now", start_at: new Date(now - 2 * SAFETY_MARGIN_MS).toISOString(), end_at: new Date(now - SAFETY_MARGIN_MS).toISOString(), targeting: {}, type: "donation", }, { id: "invalid", title: "invalid date strings", start_at: "foo", end_at: "bar", targeting: {}, type: "donation", }, ]; await InAppNotifications.updateNotifications(mockData); Assert.deepEqual( InAppNotifications.getNotifications(), [mockData[0]], "Should have only current notifications" ); await InAppNotifications.updateNotifications([]); }); add_task(async function test_notificationInteractionEvent() { const mockData = getMockNotifications(); await InAppNotifications.updateNotifications(mockData); InAppNotifications.notificationManager.dispatchEvent( new CustomEvent(NotificationManager.NOTIFICATION_INTERACTION_EVENT, { detail: mockData[0].id, }) ); Assert.deepEqual( InAppNotifications.getNotifications(), [mockData[1]], "Should no longer include the first notification" ); await InAppNotifications.updateNotifications([]); }); add_task(async function test_requestNotifictionsEvent() { const mockData = getMockNotifications(); await InAppNotifications.updateNotifications(mockData); InAppNotifications.notificationManager.updatedNotifications([]); const newNotificationEvent = BrowserTestUtils.waitForEvent( InAppNotifications.notificationManager, NotificationManager.NEW_NOTIFICATION_EVENT ); InAppNotifications.notificationManager.dispatchEvent( new CustomEvent(NotificationManager.REQUEST_NOTIFICATIONS_EVENT) ); const { detail: notification } = await newNotificationEvent; Assert.deepEqual(notification, mockData[0], "Should pick first notification"); }); add_task(async function test_getSeed() { const seedId = "foo"; const seed = InAppNotifications._getSeed(seedId); Assert.strictEqual( InAppNotifications._getSeed(seedId), seed, "Seed is constant for a given ID" ); // Test multiple seeds to sample more random values and find issues faster. const seeds = new Set([seed]); for (let attempt = 0; attempt < 100 && seeds.size < 10; ++attempt) { seeds.add(InAppNotifications._getSeed(`test${attempt}`)); } Assert.greaterOrEqual( seeds.size, 2, "Should get different seeds with enough attempts." ); Assert.strictEqual( InAppNotifications._getSeed(seedId), seed, "Seed is still constant for the same ID" ); Assert.strictEqual( InAppNotifications._jsonFile.data.seeds[seedId], seed, "Seed is stored in JSON storage" ); for (const testSeed of seeds) { Assert.ok(Number.isInteger(testSeed), `Seed ${testSeed} is an integer`); Assert.greaterOrEqual(testSeed, 0, "Seed is at least 0"); Assert.lessOrEqual(testSeed, 100, "Seed is at most 100"); } await InAppNotifications.updateNotifications([]); }); add_task(async function test_getNotification_seed() { const mockData = getMockNotifications(); mockData[0].targeting.percent_chance = 42; mockData[1].targeting.percent_chance = 42; await InAppNotifications.updateNotifications(mockData); InAppNotifications._jsonFile.data.seeds[mockData[0].id] = 2; InAppNotifications._jsonFile.data.seeds[mockData[1].id] = 100; const seededResult = InAppNotifications.getNotifications(); Assert.deepEqual( seededResult, [mockData[0]], "Should only see first notification based on percent chance seeds" ); Assert.deepEqual( InAppNotifications.getNotifications(), seededResult, "Resulting notifications are stable" ); await InAppNotifications.updateNotifications([]); }); add_task(async function test_updateNotificationManager() { const updatedNotificationsSpy = sinon.spy( InAppNotifications.notificationManager, "updatedNotifications" ); const now = Date.now(); const mockData = [ { id: "future bar", title: "dolor sit amet", start_at: new Date(now + SAFETY_MARGIN_MS).toISOString(), end_at: new Date(now + 2 * SAFETY_MARGIN_MS).toISOString(), targeting: {}, type: "donation", severity: 1, }, { id: "foo", title: "lorem ipsum", start_at: new Date(now - SAFETY_MARGIN_MS).toISOString(), end_at: new Date(now + SAFETY_MARGIN_MS).toISOString(), targeting: {}, type: "donation", severity: 5, }, ]; const newNotificationEvent = BrowserTestUtils.waitForEvent( InAppNotifications.notificationManager, NotificationManager.NEW_NOTIFICATION_EVENT ); await InAppNotifications.updateNotifications(mockData); const { detail: notification } = await newNotificationEvent; Assert.deepEqual( notification, mockData[1], "Should have filtered current notification" ); Assert.ok( updatedNotificationsSpy.calledWith( sinon.match(InAppNotifications.getNotifications()) ), "Should have passed the filtered list to updatedNotifiations" ); await InAppNotifications.updateNotifications([]); InAppNotifications.notificationManager.updatedNotifications.restore(); }); add_task(async function test_updateNotificationManager_duringLocaleDebounce() { InAppNotifications._localeChangeDebounce = "foo"; let hadEvent = false; const eventHandler = () => { hadEvent = true; Assert.ok(false, "Should not get new notification event"); }; InAppNotifications.notificationManager.addEventListener( NotificationManager.NEW_NOTIFICATION_EVENT, eventHandler ); await InAppNotifications.updateNotifications(getMockNotifications()); Assert.ok( !InAppNotifications._showNotificationTimer, "Should not have a timer for the next notification" ); Assert.ok(!hadEvent, "Should not have seen a new notification event"); InAppNotifications.notificationManager.removeEventListener( NotificationManager.NEW_NOTIFICATION_EVENT, eventHandler ); InAppNotifications._localeChangeDebounce = null; }); add_task(async function test_updateNotifications_filtered() { const now = Date.now(); const mockData = [ { id: "future bar", title: "dolor sit amet", start_at: new Date(now + SAFETY_MARGIN_MS).toISOString(), end_at: new Date(now + 2 * SAFETY_MARGIN_MS).toISOString(), targeting: {}, type: "donation", severity: 1, }, { id: "foo", title: "lorem ipsum", start_at: new Date(now - SAFETY_MARGIN_MS).toISOString(), end_at: new Date(now + SAFETY_MARGIN_MS).toISOString(), targeting: {}, type: "donation", severity: 5, }, ]; const newNotificationEvent = BrowserTestUtils.waitForEvent( InAppNotifications.notificationManager, NotificationManager.NEW_NOTIFICATION_EVENT ); await InAppNotifications.updateNotifications(mockData); const { detail: notification } = await newNotificationEvent; Assert.deepEqual( notification, mockData[1], "Should have filtered current notification" ); await InAppNotifications.updateNotifications([]); }); add_task(async function test_updateNotificationManager_localeChange() { const currentLocales = Services.locale.requestedLocales; const availableLocales = Services.locale.availableLocales; if (!availableLocales.includes("en-EU")) { Services.locale.availableLocales = ["en-EU", ...availableLocales]; } if (currentLocales.includes("en-EU")) { Services.locale.requestedLocales = ["en-US"]; } const now = Date.now(); const mockData = [ { id: "normal bar", title: "dolor sit amet", start_at: new Date(now - SAFETY_MARGIN_MS).toISOString(), end_at: new Date(now + SAFETY_MARGIN_MS).toISOString(), targeting: { exclude: [ { locales: ["en-EU"], }, ], }, type: "donation", severity: 5, }, { id: "foo weird", title: "lorem ipsum", start_at: new Date(now - SAFETY_MARGIN_MS).toISOString(), end_at: new Date(now + SAFETY_MARGIN_MS).toISOString(), targeting: { include: [ { locales: ["en-EU"], }, ], }, type: "donation", severity: 5, }, ]; await InAppNotifications.updateNotifications(mockData); const { detail: notification } = await BrowserTestUtils.waitForEvent( InAppNotifications.notificationManager, NotificationManager.NEW_NOTIFICATION_EVENT ); Assert.equal( notification.id, "normal bar", "Should see non-en-EU notification" ); const localeChanged = TestUtils.topicObserved("intl:app-locales-changed"); Services.locale.requestedLocales = ["en-EU"]; await localeChanged; const { detail: newNotification } = await BrowserTestUtils.waitForEvent( InAppNotifications.notificationManager, NotificationManager.NEW_NOTIFICATION_EVENT, false, event => event.detail.id !== notification.id ); Assert.notEqual( newNotification.id, notification.id, "Should get a different notification with the different locale" ); Assert.equal( newNotification.id, "foo weird", "Should see en-EU notification" ); Assert.ok( !InAppNotifications._localeChangeDebounce, "Should not have an active debounce timeout" ); Services.locale.availableLocales = availableLocales; Services.locale.requestedLocales = currentLocales; await InAppNotifications.updateNotifications([]); // Clear the locale change debounce from restoring the normal conditions // manually to accelerate the tests.. if (InAppNotifications._localeChangeDebounce) { clearTimeout(InAppNotifications._localeChangeDebounce); InAppNotifications._localeChangeDebounce = null; InAppNotifications._updateNotificationManager(); } }); add_task(async function test_scheduledNotification() { const now = Date.now(); const delay = 500; const mockData = [ { id: "future bar", title: "dolor sit amet", start_at: new Date(now + delay).toISOString(), end_at: new Date(now + delay + 2 * SAFETY_MARGIN_MS).toISOString(), targeting: {}, type: "donation", severity: 1, }, ]; const newNotificationEvent = BrowserTestUtils.waitForEvent( InAppNotifications.notificationManager, NotificationManager.NEW_NOTIFICATION_EVENT ); await InAppNotifications.updateNotifications(mockData); Assert.ok( InAppNotifications._showNotificationTimer, "Should have scheduled a timer for when the notification starts" ); const { detail: notification } = await newNotificationEvent; Assert.deepEqual( notification, mockData[0], "Should have sent future notification" ); Assert.greaterOrEqual( Date.now() - now, delay, "Should have waited until the notification became valid to send the event" ); Assert.ok( !InAppNotifications._showNotificationTimer, "Should have no timer for any future notifications" ); await InAppNotifications.updateNotifications([]); }); add_task(async function test_updateNotifications_noCleanup() { const mockData = getMockNotifications(); await InAppNotifications.updateNotifications(mockData); const notificationId = mockData[0].id; InAppNotifications.markAsInteractedWith(notificationId); const seed = InAppNotifications._getSeed(notificationId); await InAppNotifications.updateNotifications([], true); Assert.ok( InAppNotifications._jsonFile.data.interactedWith.includes(notificationId), `Should preserve interacted with ${notificationId}` ); Assert.equal( InAppNotifications._getSeed(notificationId), seed, `Seed for ${notificationId} should stay the same` ); await InAppNotifications.updateNotifications([]); });