/* 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/. */ package org.mozilla.fenix.telemetry import androidx.test.core.app.ApplicationProvider import io.mockk.mockk import mozilla.components.browser.state.action.ContentAction import mozilla.components.browser.state.action.EngineAction import mozilla.components.browser.state.action.TabListAction import mozilla.components.browser.state.engine.EngineMiddleware import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.state.createTab import mozilla.components.browser.state.state.recover.RecoverableTab import mozilla.components.browser.state.state.recover.TabState import mozilla.components.browser.state.store.BrowserStore import mozilla.components.service.glean.testing.GleanTestRule import mozilla.components.support.base.android.Clock import mozilla.components.support.test.ext.joinBlocking import mozilla.components.support.test.robolectric.testContext import mozilla.components.support.test.rule.MainCoroutineRule import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mozilla.fenix.GleanMetrics.Events import org.mozilla.fenix.GleanMetrics.Metrics import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.helpers.FenixRobolectricTestRunner import org.mozilla.fenix.utils.Settings import org.mozilla.fenix.GleanMetrics.EngineTab as EngineMetrics @RunWith(FenixRobolectricTestRunner::class) class TelemetryMiddlewareTest { private lateinit var store: BrowserStore private lateinit var settings: Settings private lateinit var telemetryMiddleware: TelemetryMiddleware @get:Rule val coroutinesTestRule = MainCoroutineRule() @get:Rule val gleanRule = GleanTestRule(ApplicationProvider.getApplicationContext()) private val clock = FakeClock() private val metrics: MetricController = mockk() @Before fun setUp() { Clock.delegate = clock settings = Settings(testContext) telemetryMiddleware = TelemetryMiddleware(settings, metrics) store = BrowserStore( middleware = listOf(telemetryMiddleware) + EngineMiddleware.create(engine = mockk()), initialState = BrowserState(), ) } @After fun tearDown() { Clock.reset() } @Test fun `WHEN a tab is added THEN the open tab count is updated`() { assertEquals(0, settings.openTabsCount) assertNull(Metrics.hasOpenTabs.testGetValue()) store.dispatch(TabListAction.AddTabAction(createTab("https://mozilla.org"))).joinBlocking() assertEquals(1, settings.openTabsCount) assertTrue(Metrics.hasOpenTabs.testGetValue()!!) } @Test fun `WHEN a private tab is added THEN the open tab count is not updated`() { assertEquals(0, settings.openTabsCount) assertNull(Metrics.hasOpenTabs.testGetValue()) store.dispatch(TabListAction.AddTabAction(createTab("https://mozilla.org", private = true))).joinBlocking() assertEquals(0, settings.openTabsCount) assertFalse(Metrics.hasOpenTabs.testGetValue()!!) } @Test fun `WHEN multiple tabs are added THEN the open tab count is updated`() { assertEquals(0, settings.openTabsCount) assertNull(Metrics.hasOpenTabs.testGetValue()) store.dispatch( TabListAction.AddMultipleTabsAction( listOf( createTab("https://mozilla.org"), createTab("https://firefox.com"), ), ), ).joinBlocking() assertEquals(2, settings.openTabsCount) assertTrue(Metrics.hasOpenTabs.testGetValue()!!) } @Test fun `WHEN a tab is removed THEN the open tab count is updated`() { assertNull(Metrics.hasOpenTabs.testGetValue()) store.dispatch( TabListAction.AddMultipleTabsAction( listOf( createTab(id = "1", url = "https://mozilla.org"), createTab(id = "2", url = "https://firefox.com"), ), ), ).joinBlocking() assertEquals(2, settings.openTabsCount) store.dispatch(TabListAction.RemoveTabAction("1")).joinBlocking() assertEquals(1, settings.openTabsCount) assertTrue(Metrics.hasOpenTabs.testGetValue()!!) } @Test fun `WHEN all tabs are removed THEN the open tab count is updated`() { assertNull(Metrics.hasOpenTabs.testGetValue()) store.dispatch( TabListAction.AddMultipleTabsAction( listOf( createTab("https://mozilla.org"), createTab("https://firefox.com"), ), ), ).joinBlocking() assertEquals(2, settings.openTabsCount) assertTrue(Metrics.hasOpenTabs.testGetValue()!!) store.dispatch(TabListAction.RemoveAllTabsAction()).joinBlocking() assertEquals(0, settings.openTabsCount) assertFalse(Metrics.hasOpenTabs.testGetValue()!!) } @Test fun `WHEN all normal tabs are removed THEN the open tab count is updated`() { assertNull(Metrics.hasOpenTabs.testGetValue()) store.dispatch( TabListAction.AddMultipleTabsAction( listOf( createTab("https://mozilla.org"), createTab("https://firefox.com"), createTab("https://getpocket.com", private = true), ), ), ).joinBlocking() assertEquals(2, settings.openTabsCount) assertTrue(Metrics.hasOpenTabs.testGetValue()!!) store.dispatch(TabListAction.RemoveAllNormalTabsAction).joinBlocking() assertEquals(0, settings.openTabsCount) assertFalse(Metrics.hasOpenTabs.testGetValue()!!) } @Test fun `WHEN tabs are restored THEN the open tab count is updated`() { assertEquals(0, settings.openTabsCount) assertNull(Metrics.hasOpenTabs.testGetValue()) val tabsToRestore = listOf( RecoverableTab(null, TabState(url = "https://mozilla.org", id = "1")), RecoverableTab(null, TabState(url = "https://firefox.com", id = "2")), ) store.dispatch( TabListAction.RestoreAction( tabs = tabsToRestore, restoreLocation = TabListAction.RestoreAction.RestoreLocation.BEGINNING, ), ).joinBlocking() assertEquals(2, settings.openTabsCount) assertTrue(Metrics.hasOpenTabs.testGetValue()!!) } @Test fun `GIVEN a normal page is loading WHEN loading is complete THEN we record a UriOpened event`() { val tab = createTab(id = "1", url = "https://mozilla.org") assertNull(Events.normalAndPrivateUriCount.testGetValue()) store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking() store.dispatch(ContentAction.UpdateLoadingStateAction(tab.id, true)).joinBlocking() assertNull(Events.normalAndPrivateUriCount.testGetValue()) store.dispatch(ContentAction.UpdateLoadingStateAction(tab.id, false)).joinBlocking() val count = Events.normalAndPrivateUriCount.testGetValue()!! assertEquals(1, count) } @Test fun `GIVEN a private page is loading WHEN loading is complete THEN we record a UriOpened event`() { val tab = createTab(id = "1", url = "https://mozilla.org", private = true) assertNull(Events.normalAndPrivateUriCount.testGetValue()) store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking() store.dispatch(ContentAction.UpdateLoadingStateAction(tab.id, true)).joinBlocking() assertNull(Events.normalAndPrivateUriCount.testGetValue()) store.dispatch(ContentAction.UpdateLoadingStateAction(tab.id, false)).joinBlocking() val count = Events.normalAndPrivateUriCount.testGetValue()!! assertEquals(1, count) } @Test fun `WHEN foreground tab getting killed THEN middleware counts it`() { store.dispatch( TabListAction.RestoreAction( listOf( RecoverableTab(null, TabState(url = "https://www.mozilla.org", id = "foreground")), RecoverableTab(null, TabState(url = "https://getpocket.com", id = "background_pocket")), RecoverableTab(null, TabState(url = "https://theverge.com", id = "background_verge")), ), selectedTabId = "foreground", restoreLocation = TabListAction.RestoreAction.RestoreLocation.BEGINNING, ), ).joinBlocking() assertNull(EngineMetrics.kills["foreground"].testGetValue()) assertNull(EngineMetrics.kills["background"].testGetValue()) store.dispatch( EngineAction.KillEngineSessionAction("foreground"), ).joinBlocking() assertNotNull(EngineMetrics.kills["foreground"].testGetValue()) } @Test fun `WHEN background tabs getting killed THEN middleware counts it`() { store.dispatch( TabListAction.RestoreAction( listOf( RecoverableTab(null, TabState(url = "https://www.mozilla.org", id = "foreground")), RecoverableTab(null, TabState(url = "https://getpocket.com", id = "background_pocket")), RecoverableTab(null, TabState(url = "https://theverge.com", id = "background_verge")), ), selectedTabId = "foreground", restoreLocation = TabListAction.RestoreAction.RestoreLocation.BEGINNING, ), ).joinBlocking() assertNull(EngineMetrics.kills["foreground"].testGetValue()) assertNull(EngineMetrics.kills["background"].testGetValue()) store.dispatch( EngineAction.KillEngineSessionAction("background_pocket"), ).joinBlocking() assertNull(EngineMetrics.kills["foreground"].testGetValue()) assertEquals(1, EngineMetrics.kills["background"].testGetValue()) store.dispatch( EngineAction.KillEngineSessionAction("background_verge"), ).joinBlocking() assertNull(EngineMetrics.kills["foreground"].testGetValue()) assertEquals(2, EngineMetrics.kills["background"].testGetValue()) } @Test fun `WHEN foreground tab gets killed THEN middleware records foreground age`() { store.dispatch( TabListAction.RestoreAction( listOf( RecoverableTab(null, TabState(url = "https://www.mozilla.org", id = "foreground")), RecoverableTab(null, TabState(url = "https://getpocket.com", id = "background_pocket")), RecoverableTab(null, TabState(url = "https://theverge.com", id = "background_verge")), ), selectedTabId = "foreground", restoreLocation = TabListAction.RestoreAction.RestoreLocation.BEGINNING, ), ).joinBlocking() clock.elapsedTime = 100 store.dispatch( EngineAction.LinkEngineSessionAction( tabId = "foreground", engineSession = mockk(relaxed = true), ), ).joinBlocking() assertNull(EngineMetrics.killForegroundAge.testGetValue()) assertNull(EngineMetrics.killBackgroundAge.testGetValue()) clock.elapsedTime = 500 store.dispatch( EngineAction.KillEngineSessionAction("foreground"), ).joinBlocking() assertNull(EngineMetrics.killBackgroundAge.testGetValue()) assertEquals(400_000_000, EngineMetrics.killForegroundAge.testGetValue()!!.sum) } @Test fun `WHEN background tab gets killed THEN middleware records background age`() { store.dispatch( TabListAction.RestoreAction( listOf( RecoverableTab(null, TabState(url = "https://www.mozilla.org", id = "foreground")), RecoverableTab(null, TabState(url = "https://getpocket.com", id = "background_pocket")), RecoverableTab(null, TabState(url = "https://theverge.com", id = "background_verge")), ), selectedTabId = "foreground", restoreLocation = TabListAction.RestoreAction.RestoreLocation.BEGINNING, ), ).joinBlocking() clock.elapsedTime = 100 store.dispatch( EngineAction.LinkEngineSessionAction( tabId = "background_pocket", engineSession = mockk(relaxed = true), ), ).joinBlocking() clock.elapsedTime = 700 assertNull(EngineMetrics.killForegroundAge.testGetValue()) assertNull(EngineMetrics.killBackgroundAge.testGetValue()) store.dispatch( EngineAction.KillEngineSessionAction("background_pocket"), ).joinBlocking() assertNull(EngineMetrics.killForegroundAge.testGetValue()) assertEquals(600_000_000, EngineMetrics.killBackgroundAge.testGetValue()!!.sum) } } internal class FakeClock : Clock.Delegate { var elapsedTime: Long = 0 override fun elapsedRealtime(): Long = elapsedTime }