summaryrefslogtreecommitdiffstats
path: root/mobile/android/focus-android/app/src/test
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-15 03:34:50 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-15 03:34:50 +0000
commitdef92d1b8e9d373e2f6f27c366d578d97d8960c6 (patch)
tree2ef34b9ad8bb9a9220e05d60352558b15f513894 /mobile/android/focus-android/app/src/test
parentAdding debian version 125.0.3-1. (diff)
downloadfirefox-def92d1b8e9d373e2f6f27c366d578d97d8960c6.tar.xz
firefox-def92d1b8e9d373e2f6f27c366d578d97d8960c6.zip
Merging upstream version 126.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'mobile/android/focus-android/app/src/test')
-rw-r--r--mobile/android/focus-android/app/src/test/java/org/mozilla/focus/BrowserFragmentTest.kt108
-rw-r--r--mobile/android/focus-android/app/src/test/java/org/mozilla/focus/TestFocusApplication.kt95
-rw-r--r--mobile/android/focus-android/app/src/test/java/org/mozilla/focus/animation/TransitionDrawableGroupTest.java42
-rw-r--r--mobile/android/focus-android/app/src/test/java/org/mozilla/focus/biometrics/BiometricAuthenticationFragmentTest.kt70
-rw-r--r--mobile/android/focus-android/app/src/test/java/org/mozilla/focus/browser/integration/BrowserToolbarIntegrationTest.kt234
-rw-r--r--mobile/android/focus-android/app/src/test/java/org/mozilla/focus/browser/integration/FindInPageIntegrationTest.kt46
-rw-r--r--mobile/android/focus-android/app/src/test/java/org/mozilla/focus/browser/integration/FullScreenIntegrationTest.kt374
-rw-r--r--mobile/android/focus-android/app/src/test/java/org/mozilla/focus/browser/integration/InputToolbarIntegrationTest.kt82
-rw-r--r--mobile/android/focus-android/app/src/test/java/org/mozilla/focus/cfr/CfrMiddlewareTest.kt127
-rw-r--r--mobile/android/focus-android/app/src/test/java/org/mozilla/focus/contextmenu/ContextMenuCandidatesTest.kt81
-rw-r--r--mobile/android/focus-android/app/src/test/java/org/mozilla/focus/experiments/NimbusSetupTest.kt25
-rw-r--r--mobile/android/focus-android/app/src/test/java/org/mozilla/focus/ext/BrowserToolbarTest.kt80
-rw-r--r--mobile/android/focus-android/app/src/test/java/org/mozilla/focus/ext/StringTest.kt80
-rw-r--r--mobile/android/focus-android/app/src/test/java/org/mozilla/focus/ext/UriTest.kt82
-rw-r--r--mobile/android/focus-android/app/src/test/java/org/mozilla/focus/locale/LocalesTest.kt46
-rw-r--r--mobile/android/focus-android/app/src/test/java/org/mozilla/focus/menu/BrowserMenuControllerTest.kt162
-rw-r--r--mobile/android/focus-android/app/src/test/java/org/mozilla/focus/onboarding/OnboardingControllerTest.kt72
-rw-r--r--mobile/android/focus-android/app/src/test/java/org/mozilla/focus/onboarding/OnboardingStorageTest.kt79
-rw-r--r--mobile/android/focus-android/app/src/test/java/org/mozilla/focus/searchsuggestions/SearchSuggestionsViewModelTest.kt59
-rw-r--r--mobile/android/focus-android/app/src/test/java/org/mozilla/focus/searchwidget/ExternalIntentNavigationTest.kt203
-rw-r--r--mobile/android/focus-android/app/src/test/java/org/mozilla/focus/searchwidget/SearchWidgetProviderTest.kt65
-rw-r--r--mobile/android/focus-android/app/src/test/java/org/mozilla/focus/settings/SearchEngineValidationTest.kt94
-rw-r--r--mobile/android/focus-android/app/src/test/java/org/mozilla/focus/shortcut/HomeScreenTest.kt20
-rw-r--r--mobile/android/focus-android/app/src/test/java/org/mozilla/focus/shortcut/IconGeneratorTest.kt59
-rw-r--r--mobile/android/focus-android/app/src/test/java/org/mozilla/focus/sitepermissions/SitePermissionOptionsStorageTest.kt342
-rw-r--r--mobile/android/focus-android/app/src/test/java/org/mozilla/focus/sitepermissions/SitePermissionOptionsStoreTest.kt89
-rw-r--r--mobile/android/focus-android/app/src/test/java/org/mozilla/focus/sitepermissions/SitePermissionsFragmentTest.kt57
-rw-r--r--mobile/android/focus-android/app/src/test/java/org/mozilla/focus/telemetry/GleanMetricsServiceTest.kt42
-rw-r--r--mobile/android/focus-android/app/src/test/java/org/mozilla/focus/telemetry/ProfilerMarkerFactProcessorTest.kt96
-rw-r--r--mobile/android/focus-android/app/src/test/java/org/mozilla/focus/telemetry/StartupActivityLogTest.kt99
-rw-r--r--mobile/android/focus-android/app/src/test/java/org/mozilla/focus/telemetry/StartupPathProviderTest.kt213
-rw-r--r--mobile/android/focus-android/app/src/test/java/org/mozilla/focus/telemetry/StartupStateProviderTest.kt417
-rw-r--r--mobile/android/focus-android/app/src/test/java/org/mozilla/focus/telemetry/StartupTypeTelemetryTest.kt152
-rw-r--r--mobile/android/focus-android/app/src/test/java/org/mozilla/focus/topsites/DefaultTopSitesStorageTest.kt136
-rw-r--r--mobile/android/focus-android/app/src/test/java/org/mozilla/focus/utils/IntentUtilsTest.kt21
-rw-r--r--mobile/android/focus-android/app/src/test/java/org/mozilla/focus/utils/SupportUtilsTest.kt64
-rw-r--r--mobile/android/focus-android/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker2
-rw-r--r--mobile/android/focus-android/app/src/test/resources/robolectric.properties3
38 files changed, 4118 insertions, 0 deletions
diff --git a/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/BrowserFragmentTest.kt b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/BrowserFragmentTest.kt
new file mode 100644
index 0000000000..dd89505a94
--- /dev/null
+++ b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/BrowserFragmentTest.kt
@@ -0,0 +1,108 @@
+/* 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.focus
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.View
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.EngineView
+import mozilla.components.concept.engine.selection.SelectionActionDelegate
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertNotNull
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mozilla.focus.databinding.FragmentBrowserBinding
+import org.mozilla.focus.widget.ResizableKeyboardCoordinatorLayout
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class BrowserFragmentTest {
+ @Test
+ fun testEngineViewInflationAndParentInteraction() {
+ val layoutInflater = LayoutInflater.from(testContext)
+
+ // Intercept the inflation process
+ layoutInflater.factory2 = object : LayoutInflater.Factory2 {
+ override fun onCreateView(
+ parent: View?,
+ name: String,
+ context: Context,
+ attrs: AttributeSet,
+ ): View? {
+ // Inflate a DummyEngineView when trying to inflate an EngineView
+ if (name == EngineView::class.java.name) {
+ return DummyEngineView(testContext)
+ }
+
+ // For other types of views, let the system handle it
+ return null
+ }
+
+ override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
+ return onCreateView(null, name, context, attrs)
+ }
+ }
+
+ val binding = FragmentBrowserBinding.inflate(LayoutInflater.from(testContext))
+ val engineView: EngineView = binding.engineView
+
+ assertNotNull(engineView)
+
+ // Get the layout parent of the EngineView
+ val engineViewParent = spy(
+ (engineView as View).parent as ResizableKeyboardCoordinatorLayout,
+ )
+
+ assertNotNull(engineViewParent)
+
+ engineViewParent.requestDisallowInterceptTouchEvent(true)
+
+ // Verify that the EngineView's parent does not propagate requestDisallowInterceptTouchEvent
+ verify(engineViewParent).requestDisallowInterceptTouchEvent(true)
+ // If propagated, an additional ViewGroup.requestDisallowInterceptTouchEvent would have been registered.
+ verifyNoMoreInteractions(engineViewParent)
+ }
+}
+
+/**
+ * Dummy implementation of the EngineView interface.
+ */
+class DummyEngineView(context: Context) : View(context), EngineView {
+ init {
+ id = R.id.engineView
+ }
+
+ override fun render(session: EngineSession) {
+ // no-op
+ }
+
+ override fun release() {
+ // no-op
+ }
+
+ override fun captureThumbnail(onFinish: (Bitmap?) -> Unit) {
+ // no-op
+ }
+
+ override fun setVerticalClipping(clippingHeight: Int) {
+ // no-op
+ }
+
+ override fun setDynamicToolbarMaxHeight(height: Int) {
+ // no-op
+ }
+
+ override fun setActivityContext(context: Context?) {
+ // no-op
+ }
+
+ override var selectionActionDelegate: SelectionActionDelegate? = null
+}
diff --git a/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/TestFocusApplication.kt b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/TestFocusApplication.kt
new file mode 100644
index 0000000000..7892c347b6
--- /dev/null
+++ b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/TestFocusApplication.kt
@@ -0,0 +1,95 @@
+/* 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.focus
+
+import android.content.Context
+import android.util.AttributeSet
+import android.util.JsonReader
+import android.util.JsonWriter
+import mozilla.components.browser.engine.gecko.profiler.Profiler
+import mozilla.components.concept.engine.DefaultSettings
+import mozilla.components.concept.engine.Engine
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.EngineSessionState
+import mozilla.components.concept.engine.EngineView
+import mozilla.components.concept.engine.Settings
+import mozilla.components.concept.engine.utils.EngineVersion
+import mozilla.components.concept.engine.webextension.WebExtensionDelegate
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.fetch.Request
+import mozilla.components.concept.fetch.Response
+import org.json.JSONObject
+
+/**
+ * [FocusApplication] override for unit tests. This allows us to override some parameters and inputs
+ * since an application object gets created without much control otherwise.
+ */
+class TestFocusApplication : FocusApplication() {
+ override val components: Components by lazy {
+ Components(this, engineOverride = FakeEngine(), clientOverride = FakeClient())
+ }
+
+ override fun initializeNimbus() = Unit
+}
+
+/**
+ * Empty [FocusApplication] override for unit tests.
+ */
+class EmptyFocusApplication : FocusApplication() {
+ override fun onCreate() {
+ //
+ }
+}
+
+// Borrowed this from AC unit tests. This is something we should consider moving to support-test, so
+// that everyone who needs an Engine in unit tests can use it. It also allows us to enhance this mock
+// and maybe do some actual things that help in tests. :)
+class FakeEngine : Engine {
+ override val version: EngineVersion
+ get() = throw NotImplementedError("Not needed for test")
+
+ override fun createView(context: Context, attrs: AttributeSet?): EngineView =
+ throw UnsupportedOperationException()
+
+ override fun createSession(private: Boolean, contextId: String?): EngineSession =
+ throw UnsupportedOperationException()
+
+ override fun createSessionState(json: JSONObject) = FakeEngineSessionState()
+
+ override fun createSessionStateFrom(reader: JsonReader): EngineSessionState {
+ reader.beginObject()
+ reader.endObject()
+ return FakeEngineSessionState()
+ }
+
+ override fun name(): String =
+ throw UnsupportedOperationException()
+
+ override fun speculativeConnect(url: String) =
+ throw UnsupportedOperationException()
+
+ override val profiler: Profiler
+ get() = throw NotImplementedError("Not needed for test")
+
+ override val settings: Settings = DefaultSettings()
+
+ override fun registerWebExtensionDelegate(webExtensionDelegate: WebExtensionDelegate) {
+ // Intentionally empty to avoid "UnsupportedOperationException: Web extension support
+ // is not available in this engine" error in unit tests
+ }
+}
+
+class FakeEngineSessionState : EngineSessionState {
+ override fun writeTo(writer: JsonWriter) {
+ writer.beginObject()
+ writer.endObject()
+ }
+}
+
+class FakeClient : Client() {
+ override fun fetch(request: Request): Response {
+ throw UnsupportedOperationException()
+ }
+}
diff --git a/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/animation/TransitionDrawableGroupTest.java b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/animation/TransitionDrawableGroupTest.java
new file mode 100644
index 0000000000..d21307eb4b
--- /dev/null
+++ b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/animation/TransitionDrawableGroupTest.java
@@ -0,0 +1,42 @@
+/* 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.focus.animation;
+
+import android.graphics.drawable.TransitionDrawable;
+
+import org.junit.Test;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+public class TransitionDrawableGroupTest {
+ @Test
+ public void testStartIsCalledOnAllItems() {
+ final TransitionDrawable transitionDrawable1 = mock(TransitionDrawable.class);
+ final TransitionDrawable transitionDrawable2 = mock(TransitionDrawable.class);
+
+ final TransitionDrawableGroup group = new TransitionDrawableGroup(
+ transitionDrawable1, transitionDrawable2);
+
+ group.startTransition(2500);
+
+ verify(transitionDrawable1).startTransition(2500);
+ verify(transitionDrawable2).startTransition(2500);
+ }
+
+ @Test
+ public void testResetIsCalledOnAllItems() {
+ final TransitionDrawable transitionDrawable1 = mock(TransitionDrawable.class);
+ final TransitionDrawable transitionDrawable2 = mock(TransitionDrawable.class);
+
+ final TransitionDrawableGroup group = new TransitionDrawableGroup(
+ transitionDrawable1, transitionDrawable2);
+
+ group.resetTransition();
+
+ verify(transitionDrawable1).resetTransition();
+ verify(transitionDrawable2).resetTransition();
+ }
+}
diff --git a/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/biometrics/BiometricAuthenticationFragmentTest.kt b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/biometrics/BiometricAuthenticationFragmentTest.kt
new file mode 100644
index 0000000000..7c19fcdb91
--- /dev/null
+++ b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/biometrics/BiometricAuthenticationFragmentTest.kt
@@ -0,0 +1,70 @@
+/* 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.focus.biometrics
+
+import android.content.Context
+import android.os.Build
+import androidx.fragment.app.FragmentActivity
+import androidx.fragment.app.FragmentManager
+import androidx.fragment.app.FragmentTransaction
+import mozilla.components.lib.auth.AuthenticationDelegate
+import mozilla.components.lib.auth.BiometricPromptAuth
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.robolectric.annotation.Config
+
+class BiometricAuthenticationFragmentTest {
+ private lateinit var biometricPromptAuth: BiometricPromptAuth
+ private lateinit var fragment: BiometricAuthenticationFragment
+ private val activity: FragmentActivity = mock()
+ private val testContext: Context = mock()
+
+ private val fragmentManger: FragmentManager = mock()
+ private val fragmentTransaction: FragmentTransaction = mock()
+
+ @Before
+ fun setup() {
+ fragment = spy(BiometricAuthenticationFragment())
+ doReturn(testContext).`when`(fragment).context
+ doReturn(activity).`when`(fragment).requireActivity()
+ doReturn(fragmentManger).`when`(activity).supportFragmentManager
+ doReturn(fragmentTransaction).`when`(fragmentManger).beginTransaction()
+ biometricPromptAuth = spy(
+ BiometricPromptAuth(
+ testContext,
+ fragment,
+ object : AuthenticationDelegate {
+ override fun onAuthError(errorText: String) {
+ }
+ override fun onAuthFailure() {
+ }
+ override fun onAuthSuccess() {
+ }
+ },
+ ),
+ )
+ }
+
+ @Config(sdk = [Build.VERSION_CODES.LOLLIPOP])
+ @Test
+ fun `GIVEN biometric authentication fragment WHEN show biometric prompt is called and can use feature returns false THEN request authentication is not called`() {
+ fragment.showBiometricPrompt(biometricPromptAuth, "title", "subtitle")
+
+ verify(biometricPromptAuth, never()).requestAuthentication("title", "subtitle")
+ }
+
+ @Test
+ fun `GIVEN biometric authentication fragment WHEN on Auth Error is called THEN biometricErrorText should be updated`() {
+ fragment.onAuthError("Fingerprint operation canceled by user.")
+
+ assertEquals(fragment.biometricErrorText.value, "Fingerprint operation canceled by user.")
+ }
+}
diff --git a/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/browser/integration/BrowserToolbarIntegrationTest.kt b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/browser/integration/BrowserToolbarIntegrationTest.kt
new file mode 100644
index 0000000000..179fafeb2c
--- /dev/null
+++ b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/browser/integration/BrowserToolbarIntegrationTest.kt
@@ -0,0 +1,234 @@
+/* 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.focus.browser.integration
+
+import android.view.View
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.setMain
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.SecurityInfoState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.browser.toolbar.BrowserToolbar
+import mozilla.components.browser.toolbar.display.DisplayToolbar.Indicators
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.whenever
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+import org.mozilla.focus.fragment.BrowserFragment
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class BrowserToolbarIntegrationTest {
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val selectedTab = createSecureTab()
+
+ private lateinit var toolbar: BrowserToolbar
+
+ @Mock
+ private lateinit var fragment: BrowserFragment
+
+ private lateinit var browserToolbarIntegration: BrowserToolbarIntegration
+
+ @Mock
+ private lateinit var fragmentView: View
+
+ private lateinit var store: BrowserStore
+
+ @Before
+ @ExperimentalCoroutinesApi
+ fun setUp() {
+ MockitoAnnotations.openMocks(this)
+ Dispatchers.setMain(testDispatcher)
+ store = spy(
+ BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(selectedTab),
+ selectedTabId = selectedTab.id,
+ ),
+ ),
+ )
+
+ toolbar = BrowserToolbar(testContext)
+
+ whenever(fragment.resources).thenReturn(testContext.resources)
+ whenever(fragment.context).thenReturn(testContext)
+ whenever(fragment.view).thenReturn(fragmentView)
+ whenever(fragment.requireContext()).thenReturn(testContext)
+
+ browserToolbarIntegration = spy(
+ BrowserToolbarIntegration(
+ store = store,
+ toolbar = toolbar,
+ fragment = fragment,
+ controller = mock(),
+ sessionUseCases = mock(),
+ customTabsUseCases = mock(),
+ onUrlLongClicked = { false },
+ eraseActionListener = {},
+ tabCounterListener = {},
+ inTesting = true,
+ ),
+ )
+ }
+
+ @After
+ @ExperimentalCoroutinesApi
+ fun tearDown() {
+ Dispatchers.resetMain()
+ }
+
+ @Test
+ fun `WHEN starting THEN observe security changes`() {
+ doNothing().`when`(browserToolbarIntegration).observerSecurityIndicatorChanges()
+
+ browserToolbarIntegration.start()
+
+ verify(browserToolbarIntegration).observerSecurityIndicatorChanges()
+ }
+
+ @Test
+ fun `WHEN start method is called THEN observe erase tabs CFR changes`() {
+ doNothing().`when`(browserToolbarIntegration).observeEraseCfr()
+
+ browserToolbarIntegration.start()
+
+ verify(browserToolbarIntegration).observeEraseCfr()
+ }
+
+ @Test
+ fun `WHEN start method is called THEN observe tracking protection CFR changes`() {
+ doNothing().`when`(browserToolbarIntegration).observeTrackingProtectionCfr()
+
+ browserToolbarIntegration.start()
+
+ verify(browserToolbarIntegration).observeTrackingProtectionCfr()
+ }
+
+ @Test
+ fun `WHEN stopping THEN stop tracking protection CFR changes`() {
+ doNothing().`when`(browserToolbarIntegration).stopObserverTrackingProtectionCfrChanges()
+
+ browserToolbarIntegration.stop()
+
+ verify(browserToolbarIntegration).stopObserverTrackingProtectionCfrChanges()
+ }
+
+ @Test
+ fun `WHEN stopping THEN stop erase tabs CFR changes`() {
+ doNothing().`when`(browserToolbarIntegration).stopObserverEraseTabsCfrChanges()
+
+ browserToolbarIntegration.stop()
+
+ verify(browserToolbarIntegration).stopObserverEraseTabsCfrChanges()
+ }
+
+ @Test
+ fun `WHEN stopping THEN stop observe security changes`() {
+ doNothing().`when`(browserToolbarIntegration).stopObserverSecurityIndicatorChanges()
+
+ browserToolbarIntegration.stop()
+
+ verify(browserToolbarIntegration).stopObserverSecurityIndicatorChanges()
+ }
+
+ @Test
+ fun `GIVEN an insecure site WHEN observing security changes THEN add the security icon`() {
+ browserToolbarIntegration.start()
+
+ updateSecurityStatus(secure = false)
+
+ verify(browserToolbarIntegration).addSecurityIndicator()
+ assertEquals(listOf(Indicators.SECURITY), toolbar.display.indicators)
+ }
+
+ @Test
+ fun `GIVEN an about site WHEN observing security changes THEN DO NOT add the security icon`() {
+ browserToolbarIntegration.start()
+
+ updateTabUrl("about:")
+
+ verify(browserToolbarIntegration, times(0)).addSecurityIndicator()
+ assertEquals(listOf(Indicators.TRACKING_PROTECTION), toolbar.display.indicators)
+ }
+
+ @Test
+ fun `GIVEN a secure site after a previous insecure site WHEN observing security changes THEN add the tracking protection icon`() {
+ browserToolbarIntegration.start()
+
+ updateSecurityStatus(secure = false)
+
+ verify(browserToolbarIntegration).addSecurityIndicator()
+
+ updateSecurityStatus(secure = true)
+
+ verify(browserToolbarIntegration).addTrackingProtectionIndicator()
+ assertEquals(listOf(Indicators.TRACKING_PROTECTION), toolbar.display.indicators)
+ }
+
+ @Test
+ fun `WHEN the integration starts THEN start the toolbarController`() {
+ browserToolbarIntegration.toolbarController = mock()
+
+ browserToolbarIntegration.start()
+
+ verify(browserToolbarIntegration.toolbarController).start()
+ }
+
+ @Test
+ fun `WHEN the integration stops THEN stop the toolbarController`() {
+ browserToolbarIntegration.toolbarController = mock()
+
+ browserToolbarIntegration.stop()
+
+ verify(browserToolbarIntegration.toolbarController).stop()
+ }
+
+ private fun updateSecurityStatus(secure: Boolean) {
+ store.dispatch(
+ ContentAction.UpdateSecurityInfoAction(
+ selectedTab.id,
+ SecurityInfoState(
+ secure = secure,
+ host = "mozilla.org",
+ issuer = "Mozilla",
+ ),
+ ),
+ ).joinBlocking()
+
+ testDispatcher.scheduler.advanceUntilIdle()
+ }
+
+ private fun updateTabUrl(url: String) {
+ store.dispatch(
+ ContentAction.UpdateUrlAction(selectedTab.id, url),
+ ).joinBlocking()
+
+ testDispatcher.scheduler.advanceUntilIdle()
+ }
+
+ private fun createSecureTab(): TabSessionState {
+ val tab = createTab("https://www.mozilla.org", id = "1")
+ return tab.copy(
+ content = tab.content.copy(securityInfo = SecurityInfoState(secure = true)),
+ )
+ }
+}
diff --git a/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/browser/integration/FindInPageIntegrationTest.kt b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/browser/integration/FindInPageIntegrationTest.kt
new file mode 100644
index 0000000000..21222e9e8c
--- /dev/null
+++ b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/browser/integration/FindInPageIntegrationTest.kt
@@ -0,0 +1,46 @@
+/* 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.focus.browser.integration
+
+import androidx.core.view.isVisible
+import mozilla.components.browser.state.state.ContentState
+import mozilla.components.browser.state.state.EngineState
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.toolbar.BrowserToolbar
+import mozilla.components.feature.findinpage.view.FindInPageBar
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.whenever
+import org.junit.Test
+import org.mockito.Mockito
+import org.mockito.Mockito.spy
+
+internal class FindInPageIntegrationTest {
+ // For ease of tests naming "find in page bar" is referred to as FIPB.
+ val toolbar: BrowserToolbar = mock()
+ val findInPageBar: FindInPageBar = mock()
+
+ @Test
+ fun `GIVEN FIPB not shown WHEN show is called THEN FIPB is shown`() {
+ val feature = spy(FindInPageIntegration(mock(), findInPageBar, toolbar, mock()))
+ val sessionState: SessionState = mock()
+ val contentState: ContentState = mock()
+ val engineState: EngineState = mock()
+ whenever(sessionState.content).thenReturn(contentState)
+ whenever(sessionState.engineState).thenReturn(engineState)
+
+ feature.show(sessionState)
+
+ Mockito.verify(findInPageBar).isVisible = true
+ }
+
+ @Test
+ fun `GIVEN FIPB is shown WHEN hide is called THEN FIPB is hidden`() {
+ val feature = spy(FindInPageIntegration(mock(), findInPageBar, toolbar, mock()))
+
+ feature.hide()
+
+ Mockito.verify(findInPageBar).isVisible = false
+ }
+}
diff --git a/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/browser/integration/FullScreenIntegrationTest.kt b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/browser/integration/FullScreenIntegrationTest.kt
new file mode 100644
index 0000000000..3f9d9270c4
--- /dev/null
+++ b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/browser/integration/FullScreenIntegrationTest.kt
@@ -0,0 +1,374 @@
+/* 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.focus.browser.integration
+
+import android.app.Activity
+import android.content.res.Resources
+import android.view.View
+import android.view.Window
+import android.view.WindowManager
+import androidx.core.view.isVisible
+import mozilla.components.browser.engine.gecko.GeckoEngineView
+import mozilla.components.browser.toolbar.BrowserToolbar
+import mozilla.components.feature.prompts.dialog.FullScreenNotification
+import mozilla.components.feature.session.FullScreenFeature
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Test
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mozilla.focus.ext.disableDynamicBehavior
+import org.mozilla.focus.ext.enableDynamicBehavior
+import org.mozilla.focus.ext.hide
+import org.mozilla.focus.ext.showAsFixed
+import org.mozilla.focus.utils.Settings
+import org.robolectric.Robolectric
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+internal class FullScreenIntegrationTest {
+ @Test
+ fun `WHEN the integration is started THEN start FullScreenFeature`() {
+ val feature: FullScreenFeature = mock()
+ val integration = FullScreenIntegration(
+ mock(),
+ mock(),
+ null,
+ mock(),
+ mock(),
+ mock(),
+ mock(),
+ mock(),
+ mock(),
+ ).apply {
+ this.feature = feature
+ }
+
+ integration.start()
+
+ verify(feature).start()
+ }
+
+ @Test
+ fun `WHEN the integration is stopped THEN stop FullScreenFeature`() {
+ val feature: FullScreenFeature = mock()
+ val integration = FullScreenIntegration(
+ mock(),
+ mock(),
+ null,
+ mock(),
+ mock(),
+ mock(),
+ mock(),
+ mock(),
+ mock(),
+ ).apply {
+ this.feature = feature
+ }
+
+ integration.stop()
+
+ verify(feature).stop()
+ }
+
+ @Test
+ fun `WHEN back is pressed THEN send this to the feature`() {
+ val feature: FullScreenFeature = mock()
+ val integration = FullScreenIntegration(
+ mock(),
+ mock(),
+ null,
+ mock(),
+ mock(),
+ mock(),
+ mock(),
+ mock(),
+ mock(),
+ ).apply {
+ this.feature = feature
+ }
+
+ integration.onBackPressed()
+
+ verify(feature).onBackPressed()
+ }
+
+ @Test
+ fun `WHEN the viewport changes THEN update layoutInDisplayCutoutMode`() {
+ val windowAttributes = WindowManager.LayoutParams()
+ val activityWindow: Window = mock()
+ val activity: Activity = mock()
+ doReturn(activityWindow).`when`(activity).window
+ doReturn(windowAttributes).`when`(activityWindow).attributes
+ val integration = FullScreenIntegration(
+ activity,
+ mock(),
+ null,
+ mock(),
+ mock(),
+ mock(),
+ mock(),
+ mock(),
+ mock(),
+ )
+
+ integration.viewportFitChanged(33)
+
+ assertEquals(33, windowAttributes.layoutInDisplayCutoutMode)
+ }
+
+ @Test
+ @Suppress("DEPRECATION")
+ fun `WHEN entering immersive mode THEN hide all system bars`() {
+ val decorView: View = mock()
+ val activityWindow: Window = mock()
+ val activity: Activity = mock()
+ val layoutParams: WindowManager.LayoutParams = mock()
+ doReturn(activityWindow).`when`(activity).window
+ doReturn(decorView).`when`(activityWindow).decorView
+ doReturn(layoutParams).`when`(activityWindow).attributes
+
+ val integration = FullScreenIntegration(
+ activity,
+ mock(),
+ null,
+ mock(),
+ mock(),
+ mock(),
+ mock(),
+ mock(),
+ mock(),
+ )
+
+ integration.switchToImmersiveMode()
+
+ // verify entering immersive mode
+ verify(decorView).systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN
+ verify(decorView).systemUiVisibility = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
+ // verify that the immersive mode restoration is set as expected
+ verify(decorView).setOnApplyWindowInsetsListener(any())
+ }
+
+ @Test
+ @Suppress("DEPRECATION")
+ fun `GIVEN immersive mode WHEN exitImmersiveModeIfNeeded is called THEN show the system bars`() {
+ val decorView: View = mock()
+ val activityWindow: Window = mock()
+ val activity: Activity = mock()
+ val layoutParams: WindowManager.LayoutParams = mock()
+ doReturn(activityWindow).`when`(activity).window
+ doReturn(decorView).`when`(activityWindow).decorView
+ doReturn(layoutParams).`when`(activityWindow).attributes
+ val integration = FullScreenIntegration(
+ activity,
+ mock(),
+ null,
+ mock(),
+ mock(),
+ mock(),
+ mock(),
+ mock(),
+ mock(),
+ )
+
+ integration.exitImmersiveMode()
+ // Hiding the system bar hides the status and navigation bars.
+ // setSystemUiVisibility will be called twice by WindowInsetsControllerCompat
+ // once for setting the status bar and another for setting the navigation bar
+ verify(decorView, times(2)).systemUiVisibility
+ verify(decorView).setOnApplyWindowInsetsListener(null)
+ }
+
+ @Test
+ fun `GIVEN a11y is enabled WHEN enterBrowserFullscreen THEN hide the toolbar`() {
+ val toolbar: BrowserToolbar = mock()
+ val engineView: GeckoEngineView = mock()
+ doReturn(mock<View>()).`when`(engineView).asView()
+ val settings: Settings = mock()
+ doReturn(true).`when`(settings).isAccessibilityEnabled()
+ val integration = FullScreenIntegration(
+ mock(),
+ mock(),
+ null,
+ mock(),
+ settings,
+ toolbar,
+ mock(),
+ engineView,
+ mock(),
+ )
+
+ integration.enterBrowserFullscreen()
+
+ verify(toolbar).hide(engineView)
+ verify(toolbar, never()).collapse()
+ verify(toolbar, never()).disableDynamicBehavior(engineView)
+ }
+
+ @Test
+ fun `GIVEN a11y is disabled WHEN enterBrowserFullscreen THEN collapse and disable the dynamic toolbar`() {
+ val toolbar: BrowserToolbar = mock()
+ val engineView: GeckoEngineView = mock()
+ doReturn(mock<View>()).`when`(engineView).asView()
+ val settings: Settings = mock()
+ doReturn(false).`when`(settings).isAccessibilityEnabled()
+ val integration = FullScreenIntegration(
+ mock(),
+ mock(),
+ null,
+ mock(),
+ settings,
+ toolbar,
+ mock(),
+ engineView,
+ mock(),
+ )
+
+ integration.enterBrowserFullscreen()
+
+ verify(toolbar, never()).hide(engineView)
+ with(inOrder(toolbar)) {
+ verify(toolbar).collapse()
+ verify(toolbar).disableDynamicBehavior(engineView)
+ }
+ }
+
+ @Test
+ fun `GIVEN a11y is enabled WHEN exitBrowserFullscreen THEN show the toolbar`() {
+ val toolbar: BrowserToolbar = mock()
+ val engineView: GeckoEngineView = mock()
+ doReturn(mock<View>()).`when`(engineView).asView()
+ val settings: Settings = mock()
+ doReturn(true).`when`(settings).isAccessibilityEnabled()
+ val resources: Resources = mock()
+ val activity: Activity = mock()
+ doReturn(resources).`when`(activity).resources
+ val integration = FullScreenIntegration(
+ activity,
+ mock(),
+ null,
+ mock(),
+ settings,
+ toolbar,
+ mock(),
+ engineView,
+ mock(),
+ )
+
+ integration.exitBrowserFullscreen()
+
+ verify(toolbar).showAsFixed(activity, engineView)
+ verify(toolbar, never()).expand()
+ verify(toolbar, never()).enableDynamicBehavior(activity, engineView)
+ }
+
+ @Test
+ fun `GIVEN a11y is disabled WHEN exitBrowserFullscreen THEN enable the dynamic toolbar and expand it`() {
+ val toolbar: BrowserToolbar = mock()
+ val engineView: GeckoEngineView = mock()
+ doReturn(mock<View>()).`when`(engineView).asView()
+ val settings: Settings = mock()
+ doReturn(false).`when`(settings).isAccessibilityEnabled()
+ val resources: Resources = mock()
+ val activity: Activity = mock()
+ doReturn(resources).`when`(activity).resources
+ val integration = FullScreenIntegration(
+ activity,
+ mock(),
+ null,
+ mock(),
+ settings,
+ toolbar,
+ mock(),
+ engineView,
+ mock(),
+ )
+
+ integration.exitBrowserFullscreen()
+
+ verify(toolbar, never()).showAsFixed(activity, engineView)
+ with(inOrder(toolbar)) {
+ verify(toolbar).enableDynamicBehavior(activity, engineView)
+ verify(toolbar).expand()
+ }
+ }
+
+ @Test
+ fun `WHEN entering fullscreen THEN put browser in fullscreen, hide system bars and enter immersive mode`() {
+ val toolbar = BrowserToolbar(testContext)
+ val engineView: GeckoEngineView = mock()
+ doReturn(mock<View>()).`when`(engineView).asView()
+ val settings: Settings = mock()
+ doReturn(false).`when`(settings).isAccessibilityEnabled()
+ val activity = Robolectric.buildActivity(Activity::class.java).get()
+ val statusBar: View = mock()
+ val integration = spy(
+ FullScreenIntegration(
+ activity,
+ mock(),
+ null,
+ mock(),
+ settings,
+ toolbar,
+ statusBar,
+ engineView,
+ mock(),
+ ),
+ )
+
+ val fullScreenNotification = mock<FullScreenNotification>()
+ integration.fullScreenChanged(true, fullScreenNotification)
+
+ verify(integration).enterBrowserFullscreen()
+ verify(statusBar).isVisible = false
+ verify(fullScreenNotification).show(any())
+ verify(integration).switchToImmersiveMode()
+ }
+
+ @Test
+ fun `WHEN exiting fullscreen THEN put browser in fullscreen, hide system bars and enter immersive mode`() {
+ val toolbar: BrowserToolbar = mock()
+ val engineView: GeckoEngineView = mock()
+ doReturn(mock<View>()).`when`(engineView).asView()
+ val settings: Settings = mock()
+ doReturn(false).`when`(settings).isAccessibilityEnabled()
+ val resources: Resources = mock()
+ val activityWindow: Window = mock()
+ val decorView: View = mock()
+ val windowAttributes = WindowManager.LayoutParams()
+ val activity: Activity = mock()
+ doReturn(activityWindow).`when`(activity).window
+ doReturn(decorView).`when`(activityWindow).decorView
+ doReturn(windowAttributes).`when`(activityWindow).attributes
+ doReturn(resources).`when`(activity).resources
+ val statusBar: View = mock()
+ val integration = spy(
+ FullScreenIntegration(
+ activity,
+ mock(),
+ null,
+ mock(),
+ settings,
+ toolbar,
+ statusBar,
+ engineView,
+ mock(),
+ ),
+ )
+
+ integration.fullScreenChanged(false)
+
+ verify(integration).exitBrowserFullscreen()
+ verify(integration).exitImmersiveMode()
+ verify(statusBar).isVisible = true
+ }
+}
diff --git a/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/browser/integration/InputToolbarIntegrationTest.kt b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/browser/integration/InputToolbarIntegrationTest.kt
new file mode 100644
index 0000000000..c2ac323e70
--- /dev/null
+++ b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/browser/integration/InputToolbarIntegrationTest.kt
@@ -0,0 +1,82 @@
+/* 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.focus.browser.integration
+
+import android.view.View
+import kotlinx.coroutines.isActive
+import mozilla.components.browser.toolbar.BrowserToolbar
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.whenever
+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.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import org.mozilla.focus.ext.components
+import org.mozilla.focus.fragment.UrlInputFragment
+import org.mozilla.focus.input.InputToolbarIntegration
+import org.mozilla.focus.state.AppAction
+import org.mozilla.focus.state.AppStore
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class InputToolbarIntegrationTest {
+ private lateinit var toolbar: BrowserToolbar
+
+ @Mock
+ private lateinit var fragment: UrlInputFragment
+
+ @Mock
+ private lateinit var fragmentView: View
+
+ private lateinit var inputToolbarIntegration: InputToolbarIntegration
+
+ private val appStore: AppStore = testContext.components.appStore
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.openMocks(this)
+
+ toolbar = BrowserToolbar(testContext)
+ whenever(fragment.resources).thenReturn(testContext.resources)
+ whenever(fragment.context).thenReturn(testContext)
+ whenever(fragment.view).thenReturn(fragmentView)
+
+ inputToolbarIntegration = InputToolbarIntegration(
+ toolbar,
+ fragment,
+ mock(),
+ mock(),
+ )
+ }
+
+ @Test
+ fun `GIVEN app fresh install WHEN input toolbar integration is starting THEN start browsing scope is populated`() {
+ appStore.dispatch(AppAction.ShowStartBrowsingCfrChange(true)).joinBlocking()
+
+ assertNull(inputToolbarIntegration.startBrowsingCfrScope)
+
+ inputToolbarIntegration.start()
+
+ assertNotNull(inputToolbarIntegration.startBrowsingCfrScope)
+ }
+
+ @Test
+ fun `GIVEN app fresh install WHEN input toolbar integration is stoping THEN start browsing scope is canceled`() {
+ inputToolbarIntegration.start()
+
+ assertTrue(inputToolbarIntegration.startBrowsingCfrScope?.isActive ?: true)
+
+ inputToolbarIntegration.stop()
+
+ assertFalse(inputToolbarIntegration.startBrowsingCfrScope?.isActive ?: false)
+ }
+}
diff --git a/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/cfr/CfrMiddlewareTest.kt b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/cfr/CfrMiddlewareTest.kt
new file mode 100644
index 0000000000..72999a1748
--- /dev/null
+++ b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/cfr/CfrMiddlewareTest.kt
@@ -0,0 +1,127 @@
+/* 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.focus.cfr
+
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.action.TrackingProtectionAction
+import mozilla.components.browser.state.state.SecurityInfoState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.content.blocking.Tracker
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.libstate.ext.waitUntilIdle
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.MockitoAnnotations
+import org.mozilla.focus.TestFocusApplication
+import org.mozilla.focus.ext.components
+import org.mozilla.focus.nimbus.FocusNimbus
+import org.mozilla.focus.nimbus.Onboarding
+import org.mozilla.focus.state.AppStore
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+
+@RunWith(RobolectricTestRunner::class)
+@Config(application = TestFocusApplication::class)
+class CfrMiddlewareTest {
+ private lateinit var onboardingExperiment: Onboarding
+ private val browserStore: BrowserStore = testContext.components.store
+ private val appStore: AppStore = testContext.components.appStore
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.openMocks(this)
+ onboardingExperiment = FocusNimbus.features.onboarding.value()
+ }
+
+ @Test
+ fun `GIVEN shouldShowCfrForTrackingProtection is true WHEN UpdateSecurityInfoAction is intercepted THEN showTrackingProtectionCfr is changed to true`() {
+ if (onboardingExperiment.isCfrEnabled) {
+ val updateSecurityInfoAction = ContentAction.UpdateSecurityInfoAction(
+ "1",
+ SecurityInfoState(
+ secure = true,
+ host = "test.org",
+ issuer = "Test",
+ ),
+ )
+ val trackerBlockedAction = TrackingProtectionAction.TrackerBlockedAction(
+ tabId = "1",
+ tracker = Tracker(
+ url = "test.org",
+ trackingCategories = listOf(EngineSession.TrackingProtectionPolicy.TrackingCategory.CRYPTOMINING),
+ cookiePolicies = listOf(EngineSession.TrackingProtectionPolicy.CookiePolicy.ACCEPT_NONE),
+ ),
+ )
+
+ browserStore.dispatch(updateSecurityInfoAction).joinBlocking()
+ browserStore.dispatch(trackerBlockedAction).joinBlocking()
+ appStore.waitUntilIdle()
+
+ assertTrue(appStore.state.showTrackingProtectionCfrForTab.getOrDefault("1", false))
+ }
+ }
+
+ @Test
+ fun `GIVEN insecure tab WHEN UpdateSecurityInfoAction is intercepted THEN showTrackingProtectionCfr is not changed to true`() {
+ if (onboardingExperiment.isCfrEnabled) {
+ val insecureTab = createTab(isSecure = false)
+ val updateSecurityInfoAction = ContentAction.UpdateSecurityInfoAction(
+ "1",
+ SecurityInfoState(
+ secure = false,
+ host = "test.org",
+ issuer = "Test",
+ ),
+ )
+
+ browserStore.dispatch(TabListAction.AddTabAction(insecureTab)).joinBlocking()
+ browserStore.dispatch(updateSecurityInfoAction).joinBlocking()
+ appStore.waitUntilIdle()
+
+ assertFalse(appStore.state.showTrackingProtectionCfrForTab.getOrDefault("1", false))
+ }
+ }
+
+ @Test
+ fun `GIVEN mozilla tab WHEN UpdateSecurityInfoAction is intercepted THEN showTrackingProtectionCfr is not changed to true`() {
+ if (onboardingExperiment.isCfrEnabled) {
+ val mozillaTab = createTab(id = "1", url = "https://www.mozilla.org")
+ val updateSecurityInfoAction = ContentAction.UpdateSecurityInfoAction(
+ "1",
+ SecurityInfoState(
+ secure = true,
+ host = "test.org",
+ issuer = "Test",
+ ),
+ )
+ browserStore.dispatch(TabListAction.AddTabAction(mozillaTab)).joinBlocking()
+ browserStore.dispatch(updateSecurityInfoAction).joinBlocking()
+ appStore.waitUntilIdle()
+
+ assertFalse(appStore.state.showTrackingProtectionCfrForTab.getOrDefault("1", false))
+ }
+ }
+
+ private fun createTab(
+ tabUrl: String = "https://www.test.org",
+ tabId: Int = 1,
+ isSecure: Boolean = true,
+ ): TabSessionState {
+ val tab = createTab(tabUrl, id = tabId.toString())
+ return tab.copy(
+ content = tab.content.copy(
+ private = true,
+ securityInfo = SecurityInfoState(secure = isSecure),
+ ),
+ )
+ }
+}
diff --git a/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/contextmenu/ContextMenuCandidatesTest.kt b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/contextmenu/ContextMenuCandidatesTest.kt
new file mode 100644
index 0000000000..71ebb4ead3
--- /dev/null
+++ b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/contextmenu/ContextMenuCandidatesTest.kt
@@ -0,0 +1,81 @@
+/* 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.focus.contextmenu
+
+import android.content.Context
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentMatchers.anyInt
+
+class ContextMenuCandidatesTest {
+ private val testContext: Context = mock()
+
+ @Before
+ fun setUp() {
+ whenever(testContext.getString(anyInt())).thenReturn("dummy label")
+ }
+
+ @Test
+ fun `WHEN the tab is a custom tab THEN the proper context candidates are created `() {
+ // the expected list is the same as in a Fenix custom tab.
+ val expectedCandidatesIDs = listOf(
+ "mozac.feature.contextmenu.copy_link",
+ "mozac.feature.contextmenu.share_link",
+ "mozac.feature.contextmenu.save_image",
+ "mozac.feature.contextmenu.save_video",
+ "mozac.feature.contextmenu.copy_image_location",
+ )
+
+ val actualListIDs = ContextMenuCandidates.get(
+ testContext,
+ mock(),
+ mock(),
+ mock(),
+ mock(),
+ mock(),
+ true,
+ ).map {
+ it.id
+ }
+
+ assertEquals(expectedCandidatesIDs, actualListIDs)
+ }
+
+ @Test
+ fun `WHEN the tab is NOT a custom tab THEN the proper context candidates are created `() {
+ val expectedCandidatesIDs = listOf(
+ "mozac.feature.contextmenu.open_in_private_tab",
+ "mozac.feature.contextmenu.copy_link",
+ "mozac.feature.contextmenu.download_link",
+ "mozac.feature.contextmenu.share_link",
+ "mozac.feature.contextmenu.share_image",
+ "mozac.feature.contextmenu.open_image_in_new_tab",
+ "mozac.feature.contextmenu.save_image",
+ "mozac.feature.contextmenu.save_video",
+ "mozac.feature.contextmenu.copy_image_location",
+ "mozac.feature.contextmenu.add_to_contact",
+ "mozac.feature.contextmenu.share_email",
+ "mozac.feature.contextmenu.copy_email_address",
+ "mozac.feature.contextmenu.open_in_external_app",
+ )
+
+ val actualListIDs = ContextMenuCandidates.get(
+ testContext,
+ mock(),
+ mock(),
+ mock(),
+ mock(),
+ mock(),
+ false,
+ ).map {
+ it.id
+ }
+
+ assertEquals(expectedCandidatesIDs, actualListIDs)
+ }
+}
diff --git a/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/experiments/NimbusSetupTest.kt b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/experiments/NimbusSetupTest.kt
new file mode 100644
index 0000000000..b63db61fa9
--- /dev/null
+++ b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/experiments/NimbusSetupTest.kt
@@ -0,0 +1,25 @@
+/* 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.focus.experiments
+
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.mozilla.experiments.nimbus.internal.NimbusException
+
+class NimbusSetupTest {
+ @Test
+ fun `WHEN error is reportable THEN return true`() {
+ val error = NimbusException.IoException("IOException")
+
+ assertTrue(error.isReportableError())
+ }
+
+ @Test
+ fun `WHEN error is non-reportable THEN return false`() {
+ val error = NimbusException.ClientException("ResponseException")
+
+ assertFalse(error.isReportableError())
+ }
+}
diff --git a/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/ext/BrowserToolbarTest.kt b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/ext/BrowserToolbarTest.kt
new file mode 100644
index 0000000000..4407e03a76
--- /dev/null
+++ b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/ext/BrowserToolbarTest.kt
@@ -0,0 +1,80 @@
+/* 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.focus.ext
+
+import androidx.coordinatorlayout.widget.CoordinatorLayout
+import androidx.core.view.isVisible
+import mozilla.components.browser.engine.gecko.GeckoEngineView
+import mozilla.components.browser.toolbar.BrowserToolbar
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.ui.widgets.behavior.EngineViewClippingBehavior
+import mozilla.components.ui.widgets.behavior.EngineViewScrollingBehavior
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mozilla.focus.R
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+internal class BrowserToolbarTest {
+ private val parent = CoordinatorLayout(testContext)
+ private val toolbar = spy(BrowserToolbar(testContext))
+ private val engineView = spy(GeckoEngineView(testContext))
+ private val toolbarHeight = testContext.resources.getDimensionPixelSize(R.dimen.browser_toolbar_height)
+
+ init {
+ doReturn(toolbarHeight).`when`(toolbar).height
+ parent.addView(toolbar)
+ parent.addView(engineView)
+ }
+
+ @Test
+ fun `GIVEN a BrowserToolbar WHEN enableDynamicBehavior THEN set custom behaviors for the toolbar and engineView`() {
+ // Simulate previously having a fixed toolbar
+ (engineView.layoutParams as? CoordinatorLayout.LayoutParams)?.topMargin = 222
+
+ toolbar.enableDynamicBehavior(testContext, engineView)
+
+ assertTrue((toolbar.layoutParams as? CoordinatorLayout.LayoutParams)?.behavior is EngineViewScrollingBehavior)
+ assertTrue((engineView.layoutParams as? CoordinatorLayout.LayoutParams)?.behavior is EngineViewClippingBehavior)
+ assertEquals(0, (engineView.layoutParams as? CoordinatorLayout.LayoutParams)?.topMargin)
+ verify(engineView).setDynamicToolbarMaxHeight(toolbarHeight)
+ }
+
+ @Test
+ fun `GIVEN a BrowserToolbar WHEN disableDynamicBehavior THEN set null behaviors for the toolbar and engineView`() {
+ engineView.asView().translationY = 123f
+
+ toolbar.disableDynamicBehavior(engineView)
+
+ assertNull((toolbar.layoutParams as? CoordinatorLayout.LayoutParams)?.behavior)
+ assertNull((engineView.layoutParams as? CoordinatorLayout.LayoutParams)?.behavior)
+ assertEquals(0f, engineView.asView().translationY)
+ verify(engineView).setDynamicToolbarMaxHeight(0)
+ }
+
+ @Test
+ fun `GIVEN a BrowserToolbar WHEN showAsFixed is called THEN show the toolbar with the engineView below it`() {
+ toolbar.showAsFixed(testContext, engineView)
+
+ verify(toolbar).isVisible = true
+ verify(engineView).setDynamicToolbarMaxHeight(0)
+ assertEquals(toolbarHeight, (engineView.layoutParams as? CoordinatorLayout.LayoutParams)?.topMargin)
+ }
+
+ @Test
+ fun `GIVEN a BrowserToolbar WHEN hide is called THEN show the toolbar with the engineView below it`() {
+ toolbar.hide(engineView)
+
+ verify(toolbar).isVisible = false
+ verify(engineView).setDynamicToolbarMaxHeight(0)
+ assertEquals(0, (engineView.layoutParams as? CoordinatorLayout.LayoutParams)?.topMargin)
+ }
+}
diff --git a/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/ext/StringTest.kt b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/ext/StringTest.kt
new file mode 100644
index 0000000000..33090def9e
--- /dev/null
+++ b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/ext/StringTest.kt
@@ -0,0 +1,80 @@
+/* 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.focus.ext
+
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class StringTest {
+ @Test
+ fun testBeautifyUrl() {
+ assertEqualsBeautified(
+ "wikipedia.org/wiki/Adler_Planetarium",
+ "https://en.m.wikipedia.org/wiki/Adler_Planetarium",
+ )
+
+ assertEqualsBeautified(
+ "youtube.com/watch?v=WXqGDW7kuAk",
+ "https://youtube.com/watch?v=WXqGDW7kuAk",
+ )
+
+ assertEqualsBeautified(
+ "spotify.com/album/6JVdzwuTEaLj7Tga8DpFpz",
+ "http://open.spotify.com/album/6JVdzwuTEaLj7Tga8DpFpz",
+ )
+
+ assertEqualsBeautified(
+ "google.com/mail/…/0#inbox/15e34774924ddfb5",
+ "https://mail.google.com/mail/u/0/#inbox/15e34774924ddfb5",
+ )
+
+ assertEqualsBeautified(
+ "google.com/store/…/details?id=com.facebook.katana",
+ "https://play.google.com/store/apps/details?id=com.facebook.katana&hl=en",
+ )
+
+ assertEqualsBeautified(
+ "amazon.com/Mockingjay-Hunger-Games-Suzanne-Collins/…/ref=pd_sim_14_2?_encoding=UTF8",
+ "http://amazon.com/Mockingjay-Hunger-Games-Suzanne-Collins/dp/0545663261/ref=pd_sim_14_2?_encoding=UTF8&psc=1&refRID=90ZHE3V976TKBGDR9VAM",
+ )
+
+ assertEqualsBeautified(
+ "usbank.com/Auth/Login",
+ "https://onlinebanking.usbank.com/Auth/Login",
+ )
+
+ assertEqualsBeautified(
+ "wsj.com/articles/mexican-presidential-candidate-calls-for-nafta-talks-to-be-suspended-1504137175",
+ "https://www.wsj.com/articles/mexican-presidential-candidate-calls-for-nafta-talks-to-be-suspended-1504137175",
+ )
+
+ assertEqualsBeautified(
+ "nytimes.com/2017/…/princess-diana-death-anniversary.html?hp",
+ "https://www.nytimes.com/2017/08/30/world/europe/princess-diana-death-anniversary.html?hp&action=click&pgtype=Homepage&clickSource=story-heading&module=second-column-region&region=top-news&WT.nav=top-news",
+ )
+
+ assertEqualsBeautified(
+ "yahoo.co.jp/hl?a=20170830-00000008-jct-soci",
+ "https://headlines.yahoo.co.jp/hl?a=20170830-00000008-jct-soci",
+ )
+
+ assertEqualsBeautified(
+ "tomshardware.co.uk/answers/…/running-guest-network-channel-interference.html",
+ "http://www.tomshardware.co.uk/answers/id-2025922/running-guest-network-channel-interference.html",
+ )
+
+ assertEqualsBeautified(
+ "github.com/mozilla-mobile/…/1231",
+ "https://github.com/mozilla-mobile/focus-android/issues/1231",
+ )
+ }
+
+ private fun assertEqualsBeautified(expected: String, url: String) {
+ assertEquals("beautify($url)", expected, url.beautifyUrl())
+ }
+}
diff --git a/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/ext/UriTest.kt b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/ext/UriTest.kt
new file mode 100644
index 0000000000..8559f251c8
--- /dev/null
+++ b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/ext/UriTest.kt
@@ -0,0 +1,82 @@
+/* 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.focus.ext
+
+import androidx.core.net.toUri
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class UriTest {
+ @Test
+ fun testTruncatedHostWithCommonUrls() {
+ assertTruncatedHost("mozilla.org", "https://www.mozilla.org")
+ assertTruncatedHost("wikipedia.org", "https://en.m.wikipedia.org/wiki/")
+ assertTruncatedHost("example.org", "http://example.org")
+ assertTruncatedHost("youtube.com", "https://www.youtube.com/watch?v=oHg5SJYRHA0")
+ assertTruncatedHost("facebook.com", "https://www.facebook.com/Firefox/")
+ assertTruncatedHost("yahoo.com", "https://de.search.yahoo.com/search?p=mozilla&fr=yfp-t&fp=1&toggle=1&cop=mss&ei=UTF-8")
+ assertTruncatedHost("amazon.co.uk", "https://www.amazon.co.uk/Doctor-Who-10-Part-DVD/dp/B06XCMVY1H")
+ }
+
+ @Test
+ fun testTruncatedHostWithEmptyHost() {
+ assertTruncatedHost("", "tel://")
+ }
+
+ @Test
+ fun testTruncatedPathWithEmptySegments() {
+ assertTruncatedPath("", "https://www.mozilla.org")
+ assertTruncatedPath("", "https://www.mozilla.org/")
+ assertTruncatedPath("", "https://www.mozilla.org///")
+ }
+
+ @Test
+ fun testTrunactedPathWithOneSegment() {
+ assertTruncatedPath("/space", "https://www.theverge.com/space")
+ }
+
+ @Test
+ fun testTruncatedPathWithTwoSegments() {
+ assertTruncatedPath("/en-US/firefox", "https://www.mozilla.org/en-US/firefox/")
+ assertTruncatedPath("/mozilla-mobile/focus-android", "https://github.com/mozilla-mobile/focus-android")
+ }
+
+ @Test
+ fun testTruncatedPathWithMultipleSegments() {
+ assertTruncatedPath("/en-US/…/fast", "https://www.mozilla.org/en-US/firefox/features/fast/")
+
+ assertTruncatedPath(
+ "/2017/…/nasa-hi-seas-mars-analogue-mission-hawaii-mauna-loa",
+ "https://www.theverge.com/2017/9/24/16356876/nasa-hi-seas-mars-analogue-mission-hawaii-mauna-loa",
+ )
+ }
+
+ @Test
+ fun testTruncatedPathWithMultipleSegmentsAndFragment() {
+ assertTruncatedPath(
+ "/@bfrancis/the-story-of-firefox-os-cb5bf796e8fb",
+ "https://medium.com/@bfrancis/the-story-of-firefox-os-cb5bf796e8fb#931a",
+ )
+ }
+
+ private fun assertTruncatedHost(expectedTruncatedPath: String, url: String) {
+ assertEquals(
+ "truncatedHost($url)",
+ expectedTruncatedPath,
+ url.toUri().truncatedHost(),
+ )
+ }
+
+ private fun assertTruncatedPath(expectedTruncatedPath: String, url: String) {
+ assertEquals(
+ "truncatedPath($url)",
+ expectedTruncatedPath,
+ url.toUri().truncatedPath(),
+ )
+ }
+}
diff --git a/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/locale/LocalesTest.kt b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/locale/LocalesTest.kt
new file mode 100644
index 0000000000..7a7f37d14a
--- /dev/null
+++ b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/locale/LocalesTest.kt
@@ -0,0 +1,46 @@
+/* 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.focus.locale
+
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.mozilla.focus.locale.Locales.getLanguage
+import org.mozilla.focus.locale.Locales.getLanguageTag
+import java.util.Locale
+
+class LocalesTest {
+ @Test
+ fun testLanguage() {
+ assertEquals("en", getLanguage(Locale.getDefault()))
+ }
+
+ @Test
+ fun testHebrewIsrael() {
+ val locale = Locale("iw", "IL")
+ assertEquals("he", getLanguage(locale))
+ assertEquals("he-IL", getLanguageTag(locale))
+ }
+
+ @Test
+ fun testIndonesianIndonesia() {
+ val locale = Locale("in", "ID")
+ assertEquals("id", getLanguage(locale))
+ assertEquals("id-ID", getLanguageTag(locale))
+ }
+
+ @Test
+ fun testYiddishUnitedStates() {
+ val locale = Locale("ji", "US")
+ assertEquals("yi", getLanguage(locale))
+ assertEquals("yi-US", getLanguageTag(locale))
+ }
+
+ @Test
+ fun testEmptyCountry() {
+ val locale = Locale("en")
+ assertEquals("en", getLanguage(locale))
+ assertEquals("en", getLanguageTag(locale))
+ }
+}
diff --git a/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/menu/BrowserMenuControllerTest.kt b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/menu/BrowserMenuControllerTest.kt
new file mode 100644
index 0000000000..a5547b617d
--- /dev/null
+++ b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/menu/BrowserMenuControllerTest.kt
@@ -0,0 +1,162 @@
+/* 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.focus.menu
+
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.feature.session.SessionUseCases
+import mozilla.components.feature.top.sites.TopSitesUseCases
+import mozilla.components.support.test.any
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.MockitoAnnotations
+import org.mockito.Spy
+import org.mozilla.focus.browser.integration.BrowserMenuController
+import org.mozilla.focus.state.AppStore
+
+class BrowserMenuControllerTest {
+ private lateinit var browserMenuController: BrowserMenuController
+
+ @Spy
+ private lateinit var sessionUseCases: SessionUseCases
+
+ @Mock
+ private lateinit var appStore: AppStore
+
+ @Mock
+ private lateinit var topSitesUseCases: TopSitesUseCases
+
+ private val currentTabId: String = "1"
+ private val selectedTab = createTab("https://www.mozilla.org", id = "1")
+ private val shareCallback: () -> Unit = {}
+
+ @Mock
+ private lateinit var requestDesktopCallback: (isChecked: Boolean) -> Unit
+
+ @Mock
+ private lateinit var addToHomeScreenCallback: () -> Unit
+
+ @Mock
+ private lateinit var showFindInPageCallback: () -> Unit
+
+ @Mock
+ private lateinit var openInCallback: () -> Unit
+
+ // NB: we should avoid mocking lambdas..
+ @Mock
+ private lateinit var openInBrowser: () -> Unit
+
+ @Mock
+ private lateinit var showShortcutAddedSnackBar: () -> Unit
+
+ @Before
+ fun setup() {
+ val store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(selectedTab),
+ selectedTabId = selectedTab.id,
+ ),
+ )
+ sessionUseCases = SessionUseCases(store)
+ MockitoAnnotations.openMocks(this)
+
+ browserMenuController = spy(
+ BrowserMenuController(
+ sessionUseCases,
+ appStore,
+ store,
+ topSitesUseCases,
+ currentTabId,
+ shareCallback,
+ requestDesktopCallback,
+ addToHomeScreenCallback,
+ showFindInPageCallback,
+ openInCallback,
+ openInBrowser,
+ showShortcutAddedSnackBar,
+ ),
+ )
+
+ doNothing().`when`(browserMenuController).recordBrowserMenuTelemetry(any())
+ }
+
+ @Test
+ fun `GIVEN Back menu item WHEN the item is pressed THEN goBack use case is called`() {
+ val menuItem = ToolbarMenu.Item.Back
+ browserMenuController.handleMenuInteraction(menuItem)
+ Mockito.verify(sessionUseCases, times(1)).goBack
+ }
+
+ @Test
+ fun `GIVEN Forward menu item WHEN the item is pressed THEN goForward use case is called`() {
+ val menuItem = ToolbarMenu.Item.Forward
+ browserMenuController.handleMenuInteraction(menuItem)
+ Mockito.verify(sessionUseCases, times(1)).goForward
+ }
+
+ @Test
+ fun `GIVEN Reload menu item WHEN the item is pressed THEN reload use case is called`() {
+ val menuItem = ToolbarMenu.Item.Reload
+ browserMenuController.handleMenuInteraction(menuItem)
+ Mockito.verify(sessionUseCases, times(1)).reload
+ }
+
+ @Test
+ fun `GIVEN Stop menu item WHEN the item is pressed THEN stopLoading use case is called`() {
+ val menuItem = ToolbarMenu.Item.Stop
+ browserMenuController.handleMenuInteraction(menuItem)
+ Mockito.verify(sessionUseCases, times(1)).stopLoading
+ }
+
+ @Test
+ @Suppress("MaxLineLength")
+ fun `GIVEN RequestDesktop menu item WHEN the item is switched to false THEN requestDesktopCallback with false is called`() {
+ val menuItem = ToolbarMenu.Item.RequestDesktop(isChecked = false)
+ browserMenuController.handleMenuInteraction(menuItem)
+ Mockito.verify(requestDesktopCallback, times(1)).invoke(false)
+ }
+
+ @Test
+ @Suppress("MaxLineLength")
+ fun `GIVEN RequestDesktop menu item WHEN the item is switched to true THEN requestDesktopCallback with true is called`() {
+ val menuItem = ToolbarMenu.Item.RequestDesktop(isChecked = true)
+ browserMenuController.handleMenuInteraction(menuItem)
+ Mockito.verify(requestDesktopCallback, times(1)).invoke(true)
+ }
+
+ @Test
+ fun `GIVEN OpenInBrowser menu item WHEN the item is pressed THEN openInBrowser is called`() {
+ val menuItem = ToolbarMenu.CustomTabItem.OpenInBrowser
+ browserMenuController.handleMenuInteraction(menuItem)
+ Mockito.verify(openInBrowser, times(1)).invoke()
+ }
+
+ @Test
+ fun `GIVEN OpenIn menu item WHEN the item is pressed THEN openInCallback is called`() {
+ val menuItem = ToolbarMenu.Item.OpenInApp
+ browserMenuController.handleMenuInteraction(menuItem)
+ Mockito.verify(openInCallback, times(1)).invoke()
+ }
+
+ @Test
+ fun `GIVEN FindInPage menu item WHEN the item is pressed THEN findInPageMenuEvent method is called`() {
+ val menuItem = ToolbarMenu.Item.FindInPage
+ browserMenuController.handleMenuInteraction(menuItem)
+ Mockito.verify(showFindInPageCallback, times(1)).invoke()
+ }
+
+ @Test
+ fun `Given AddToShortCut menu item WHEN the item is pressed THEN showShortcutAddedSnackBar is called`() {
+ val menuItem = ToolbarMenu.Item.AddToShortcuts
+ browserMenuController.handleMenuInteraction(menuItem)
+ Mockito.verify(showShortcutAddedSnackBar, times(1)).invoke()
+ }
+}
diff --git a/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/onboarding/OnboardingControllerTest.kt b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/onboarding/OnboardingControllerTest.kt
new file mode 100644
index 0000000000..5d02674913
--- /dev/null
+++ b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/onboarding/OnboardingControllerTest.kt
@@ -0,0 +1,72 @@
+/* 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.focus.onboarding
+
+import android.os.Build
+import androidx.preference.PreferenceManager
+import androidx.test.core.app.ApplicationProvider
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+import org.mozilla.focus.R
+import org.mozilla.focus.fragment.onboarding.DefaultOnboardingController
+import org.mozilla.focus.fragment.onboarding.OnboardingController
+import org.mozilla.focus.fragment.onboarding.OnboardingStorage
+import org.mozilla.focus.state.AppStore
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+
+@RunWith(RobolectricTestRunner::class)
+class OnboardingControllerTest {
+
+ @Mock
+ private lateinit var appStore: AppStore
+
+ @Mock
+ private lateinit var onboardingStorage: OnboardingStorage
+ private lateinit var onboardingController: OnboardingController
+
+ @Before
+ fun init() {
+ MockitoAnnotations.openMocks(this)
+ onboardingController = spy(
+ DefaultOnboardingController(
+ onboardingStorage,
+ appStore,
+ ApplicationProvider.getApplicationContext(),
+ "1",
+ ),
+ )
+ }
+
+ @Test
+ fun `GIVEN onBoarding, WHEN start browsing is pressed, THEN onBoarding flag is true`() {
+ DefaultOnboardingController(
+ onboardingStorage,
+ appStore,
+ ApplicationProvider.getApplicationContext(),
+ "1",
+ ).handleFinishOnBoarding()
+
+ val prefManager =
+ PreferenceManager.getDefaultSharedPreferences(ApplicationProvider.getApplicationContext())
+
+ assertEquals(false, prefManager.getBoolean(testContext.getString(R.string.firstrun_shown), false))
+ }
+
+ @Config(sdk = [Build.VERSION_CODES.M])
+ @Test
+ fun `GIVEN onBoarding and build version is M, WHEN get started button is pressed, THEN onBoarding flow must end`() {
+ onboardingController.handleGetStartedButtonClicked()
+
+ verify(onboardingController, times(1)).handleFinishOnBoarding()
+ }
+}
diff --git a/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/onboarding/OnboardingStorageTest.kt b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/onboarding/OnboardingStorageTest.kt
new file mode 100644
index 0000000000..39d9f8c603
--- /dev/null
+++ b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/onboarding/OnboardingStorageTest.kt
@@ -0,0 +1,79 @@
+/* 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.focus.onboarding
+
+import androidx.preference.PreferenceManager
+import androidx.test.core.app.ApplicationProvider
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.spy
+import org.mozilla.focus.R
+import org.mozilla.focus.fragment.onboarding.OnboardingStep
+import org.mozilla.focus.fragment.onboarding.OnboardingStorage
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class OnboardingStorageTest {
+ private lateinit var onBoardingStorage: OnboardingStorage
+
+ @Before
+ fun setup() {
+ onBoardingStorage = spy(OnboardingStorage(testContext))
+ }
+
+ @Test
+ fun `GIVEN at the first onboarding step WHEN querying the current onboarding step from storage THEN get the first step`() {
+ doReturn(testContext.getString(R.string.pref_key_first_screen))
+ .`when`(onBoardingStorage).getCurrentOnboardingStepFromSharedPref()
+
+ assertEquals(
+ OnboardingStep.ON_BOARDING_FIRST_SCREEN,
+ onBoardingStorage.getCurrentOnboardingStep(),
+ )
+ }
+
+ @Test
+ fun `GIVEN at the second onboarding step WHEN querying the current onboarding step from storage THEN get the second step`() {
+ doReturn(testContext.getString(R.string.pref_key_second_screen))
+ .`when`(onBoardingStorage).getCurrentOnboardingStepFromSharedPref()
+
+ assertEquals(
+ OnboardingStep.ON_BOARDING_SECOND_SCREEN,
+ onBoardingStorage.getCurrentOnboardingStep(),
+ )
+ }
+
+ @Test
+ fun `GIVEN onboarding not started WHEN querying the current onboarding step from storage THEN get the first step`() {
+ assertEquals(
+ OnboardingStep.ON_BOARDING_FIRST_SCREEN,
+ onBoardingStorage.getCurrentOnboardingStep(),
+ )
+ }
+
+ @Test
+ fun `GIVEN saveCurrentOnBoardingStepInSharePref is called WHEN ONBOARDING_FIRST_SCREEN is saved, THEN value for pref_key_onboarding_step is pref_key_first_screen`() {
+ onBoardingStorage.saveCurrentOnboardingStepInSharePref(OnboardingStep.ON_BOARDING_FIRST_SCREEN)
+
+ val prefManager =
+ PreferenceManager.getDefaultSharedPreferences(ApplicationProvider.getApplicationContext())
+
+ assertEquals(testContext.getString(OnboardingStep.ON_BOARDING_FIRST_SCREEN.prefId), prefManager.getString(testContext.getString(R.string.pref_key_onboarding_step), ""))
+ }
+
+ @Test
+ fun `GIVEN saveCurrentOnBoardingStepInSharePref is called WHEN ONBOARDING_SECOND_SCREEN is saved, THEN value for pref_key_onboarding_step is pref_key_second_screen`() {
+ onBoardingStorage.saveCurrentOnboardingStepInSharePref(OnboardingStep.ON_BOARDING_SECOND_SCREEN)
+
+ val prefManager =
+ PreferenceManager.getDefaultSharedPreferences(ApplicationProvider.getApplicationContext())
+
+ assertEquals(testContext.getString(OnboardingStep.ON_BOARDING_SECOND_SCREEN.prefId), prefManager.getString(testContext.getString(R.string.pref_key_onboarding_step), ""))
+ }
+}
diff --git a/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/searchsuggestions/SearchSuggestionsViewModelTest.kt b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/searchsuggestions/SearchSuggestionsViewModelTest.kt
new file mode 100644
index 0000000000..c2c686c8e0
--- /dev/null
+++ b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/searchsuggestions/SearchSuggestionsViewModelTest.kt
@@ -0,0 +1,59 @@
+/* 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.focus.searchsuggestions
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.LifecycleRegistry
+import androidx.lifecycle.Observer
+import androidx.test.core.app.ApplicationProvider
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TestRule
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class SearchSuggestionsViewModelTest {
+ @get:Rule
+ var rule: TestRule = InstantTaskExecutorRule()
+
+ @Mock
+ private lateinit var observer: Observer<String?>
+
+ private lateinit var lifecycle: LifecycleRegistry
+ private lateinit var viewModel: SearchSuggestionsViewModel
+
+ @Before
+ fun setup() {
+ lifecycle = LifecycleRegistry(mock(LifecycleOwner::class.java))
+ MockitoAnnotations.openMocks(this)
+
+ viewModel = SearchSuggestionsViewModel(ApplicationProvider.getApplicationContext())
+ }
+
+ @Test
+ fun setSearchQuery() {
+ viewModel.searchQuery.observeForever(observer)
+
+ viewModel.setSearchQuery("Mozilla")
+ verify(observer).onChanged("Mozilla")
+ }
+
+ @Test
+ fun alwaysSearchSelected() {
+ viewModel.selectedSearchSuggestion.observeForever(observer)
+
+ viewModel.selectSearchSuggestion("mozilla.com", "google", true)
+ verify(observer).onChanged("mozilla.com")
+ assertEquals(true, viewModel.alwaysSearch)
+ }
+}
diff --git a/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/searchwidget/ExternalIntentNavigationTest.kt b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/searchwidget/ExternalIntentNavigationTest.kt
new file mode 100644
index 0000000000..c1156022bd
--- /dev/null
+++ b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/searchwidget/ExternalIntentNavigationTest.kt
@@ -0,0 +1,203 @@
+/* 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.focus.searchwidget
+
+import android.app.Activity
+import android.content.Context
+import android.os.Bundle
+import mozilla.components.browser.state.selector.allTabs
+import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab
+import mozilla.components.browser.state.selector.privateTabs
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.feature.search.widget.BaseVoiceSearchActivity
+import mozilla.components.support.test.libstate.ext.waitUntilIdle
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.telemetry.glean.testing.GleanTestRule
+import org.junit.Assert.assertNotNull
+import org.junit.Rule
+import org.junit.Test
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertFalse
+import org.junit.jupiter.api.Assertions.assertNull
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.runner.RunWith
+import org.mozilla.focus.GleanMetrics.SearchWidget
+import org.mozilla.focus.TestFocusApplication
+import org.mozilla.focus.activity.IntentReceiverActivity
+import org.mozilla.focus.ext.components
+import org.mozilla.focus.ext.settings
+import org.mozilla.focus.perf.Performance
+import org.mozilla.focus.state.AppAction
+import org.mozilla.focus.state.Screen
+import org.mozilla.focus.utils.SearchUtils
+import org.robolectric.Robolectric
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.Implementation
+import org.robolectric.annotation.Implements
+
+@RunWith(RobolectricTestRunner::class)
+@Config(application = TestFocusApplication::class)
+internal class ExternalIntentNavigationTest {
+ @get:Rule
+ val gleanTestRule = GleanTestRule(testContext)
+
+ private val activity: Activity = Robolectric.buildActivity(Activity::class.java).setup().get()
+ private val appStore = activity.components.appStore
+
+ @Test
+ fun `GIVEN the onboarding should be shown and the app is not used in performance tests WHEN the app is opened THEN show the onboarding`() {
+ activity.settings.isFirstRun = true
+
+ ExternalIntentNavigation.handleAppOpened(null, activity)
+ appStore.waitUntilIdle()
+
+ assertEquals(Screen.FirstRun, appStore.state.screen)
+ }
+
+ @Test
+ @Config(shadows = [ShadowPerformance::class])
+ fun `GIVEN the onboarding should be shown and the app is used in a performance test WHEN the app is opened THEN show the homescreen`() {
+ // The AppStore is initialized before the test runs. By default isFirstRun is true. Simulate it being false.
+ appStore.dispatch(AppAction.ShowHomeScreen)
+ appStore.waitUntilIdle()
+ activity.settings.isFirstRun = true
+
+ ExternalIntentNavigation.handleAppOpened(null, activity)
+ appStore.waitUntilIdle()
+
+ assertEquals(Screen.Home, appStore.state.screen)
+ }
+
+ @Test
+ @Config(shadows = [ShadowPerformance::class])
+ fun `GIVEN the onboarding should not be shown and in a performance test WHEN the app is opened THEN show the home screen`() {
+ // The AppStore is initialized before the test runs. By default isFirstRun is true. Simulate it being false.
+ appStore.dispatch(AppAction.ShowHomeScreen)
+ appStore.waitUntilIdle()
+ activity.settings.isFirstRun = false
+
+ ExternalIntentNavigation.handleAppOpened(null, activity)
+ appStore.waitUntilIdle()
+
+ assertEquals(Screen.Home, appStore.state.screen)
+ }
+
+ @Test
+ fun `GIVEN the onboarding should not be shown and not in a performance test WHEN the app is opened THEN show the home screen`() {
+ // The AppStore is initialized before the test runs. By default isFirstRun is true. Simulate it being false.
+ appStore.dispatch(AppAction.ShowHomeScreen)
+ appStore.waitUntilIdle()
+ activity.settings.isFirstRun = false
+
+ ExternalIntentNavigation.handleAppOpened(null, activity)
+ appStore.waitUntilIdle()
+
+ assertEquals(Screen.Home, appStore.state.screen)
+ }
+
+ @Test
+ fun `GIVEN a tab is already open WHEN trying to navigate to the current tab THEN navigate to it and return true`() {
+ activity.components.tabsUseCases.addTab(url = "https://mozilla.com")
+ activity.components.store.waitUntilIdle()
+ val result = ExternalIntentNavigation.handleBrowserTabAlreadyOpen(activity)
+ activity.components.appStore.waitUntilIdle()
+
+ assertTrue(result)
+ val selectedTabId = activity.components.store.state.selectedTabId!!
+ assertEquals(Screen.Browser(selectedTabId, false), activity.components.appStore.state.screen)
+ }
+
+ @Test
+ fun `GIVEN no tabs are currently open WHEN trying to navigate to the current tab THEN navigate home and return false`() {
+ // The AppStore is initialized before the test runs. By default isFirstRun is true. Simulate it being false.
+ appStore.dispatch(AppAction.ShowHomeScreen)
+ appStore.waitUntilIdle()
+ val result = ExternalIntentNavigation.handleBrowserTabAlreadyOpen(activity)
+ activity.components.appStore.waitUntilIdle()
+
+ assertFalse(result)
+ assertEquals(Screen.Home, activity.components.appStore.state.screen)
+ }
+
+ @Test
+ fun `GIVEN a text search from the search widget WHEN handling widget interactions THEN record telemetry, show the home screen and return true`() {
+ val bundle = Bundle().apply {
+ putBoolean(IntentReceiverActivity.SEARCH_WIDGET_EXTRA, true)
+ }
+
+ val result = ExternalIntentNavigation.handleWidgetTextSearch(bundle, activity)
+ appStore.waitUntilIdle()
+
+ assertTrue(result)
+ assertNotNull(SearchWidget.newTabButton.testGetValue())
+ assertEquals(Screen.Home, appStore.state.screen)
+ }
+
+ @Test
+ fun `GIVEN no text search from the search widget WHEN handling widget interactions THEN don't record telemetry, show the home screen and false true`() {
+ // The AppStore is initialized before the test runs. By default isFirstRun is true. Simulate it being false.
+ appStore.dispatch(AppAction.ShowHomeScreen)
+ appStore.waitUntilIdle()
+ val bundle = Bundle()
+
+ val result = ExternalIntentNavigation.handleWidgetTextSearch(bundle, activity)
+ appStore.waitUntilIdle()
+
+ assertFalse(result)
+ assertNull(SearchWidget.newTabButton.testGetValue())
+ assertEquals(Screen.Home, appStore.state.screen)
+ }
+
+ @Test
+ fun `GIVEN a voice search WHEN handling widget interactions THEN create and open a new tab and return true`() {
+ val browserStore = activity.components.store
+ val searchArgument = "test"
+ val bundle = Bundle().apply {
+ putString(BaseVoiceSearchActivity.SPEECH_PROCESSING, searchArgument)
+ }
+
+ val result = ExternalIntentNavigation.handleWidgetVoiceSearch(bundle, activity)
+
+ assertTrue(result)
+ browserStore.waitUntilIdle()
+ assertEquals(1, browserStore.state.allTabs.size)
+ assertEquals(1, browserStore.state.privateTabs.size)
+ val voiceSearchTab = browserStore.state.privateTabs[0]
+ assertEquals(voiceSearchTab, browserStore.state.findCustomTabOrSelectedTab())
+ assertEquals(SearchUtils.createSearchUrl(activity, searchArgument), voiceSearchTab.content.url)
+ assertEquals(SessionState.Source.External.ActionSend(null), voiceSearchTab.source)
+ assertEquals(searchArgument, voiceSearchTab.content.searchTerms)
+ appStore.waitUntilIdle()
+ assertEquals(Screen.Browser(voiceSearchTab.id, false), appStore.state.screen)
+ }
+
+ @Test
+ fun `GIVEN no voice search WHEN handling widget interactions THEN don't open a new tab and return false`() {
+ // The AppStore is initialized before the test runs. By default isFirstRun is true. Simulate it being false.
+ appStore.dispatch(AppAction.ShowHomeScreen)
+ appStore.waitUntilIdle()
+ val browserStore = activity.components.store
+ val bundle = Bundle()
+
+ val result = ExternalIntentNavigation.handleWidgetVoiceSearch(bundle, activity)
+
+ assertFalse(result)
+ browserStore.waitUntilIdle()
+ assertEquals(0, browserStore.state.allTabs.size)
+ appStore.waitUntilIdle()
+ assertEquals(Screen.Home, appStore.state.screen)
+ }
+}
+
+/**
+ * Shadow of [Performance] that will have [processIntentIfPerformanceTest] always return `true`.
+ */
+@Implements(Performance::class)
+class ShadowPerformance {
+ @Implementation
+ @Suppress("Unused_Parameter")
+ fun processIntentIfPerformanceTest(bundle: Bundle?, context: Context) = true
+}
diff --git a/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/searchwidget/SearchWidgetProviderTest.kt b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/searchwidget/SearchWidgetProviderTest.kt
new file mode 100644
index 0000000000..6ad9a7b3b5
--- /dev/null
+++ b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/searchwidget/SearchWidgetProviderTest.kt
@@ -0,0 +1,65 @@
+/* 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.focus.searchwidget
+
+import android.app.PendingIntent
+import android.content.Intent
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.utils.PendingIntentUtils
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.spy
+import org.mozilla.focus.activity.IntentReceiverActivity
+import org.mozilla.focus.ext.components
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class SearchWidgetProviderTest {
+
+ private lateinit var searchWidgetProvider: SearchWidgetProvider
+
+ @Before
+ fun setup() {
+ searchWidgetProvider = spy(SearchWidgetProvider())
+ }
+
+ @Test
+ fun `GIVEN search widget provider WHEN onEnabled is called THEN searchWidgetInstalled from Settings should return true`() {
+ searchWidgetProvider.onEnabled(testContext)
+
+ assertEquals(testContext.components.settings.searchWidgetInstalled, true)
+ }
+
+ @Test
+ fun `GIVEN search widget provider WHEN onDeleted is called THEN searchWidgetInstalled from Settings should return false`() {
+ searchWidgetProvider.onDeleted(testContext, intArrayOf(1))
+
+ assertEquals(testContext.components.settings.searchWidgetInstalled, false)
+ }
+
+ @Test
+ fun `GIVEN search widget provider WHEN createTextSearchIntent is called THEN an PendingIntent should be return`() {
+ val textSearchIntent = Intent(testContext, IntentReceiverActivity::class.java)
+ .apply {
+ this.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
+ this.putExtra(IntentReceiverActivity.SEARCH_WIDGET_EXTRA, true)
+ }
+ val dummyPendingIntent = PendingIntent.getActivity(
+ testContext,
+ SearchWidgetProvider.REQUEST_CODE_NEW_TAB,
+ textSearchIntent,
+ PendingIntentUtils.defaultFlags or
+ PendingIntent.FLAG_UPDATE_CURRENT,
+ )
+
+ assertEquals(searchWidgetProvider.createTextSearchIntent(testContext), dummyPendingIntent)
+ }
+
+ @Test
+ fun `GIVEN search widget provider WHEN voiceSearchActivity is called THEN VoiceSearchActivity should be return`() {
+ assertEquals(searchWidgetProvider.voiceSearchActivity(), VoiceSearchActivity::class.java)
+ }
+}
diff --git a/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/settings/SearchEngineValidationTest.kt b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/settings/SearchEngineValidationTest.kt
new file mode 100644
index 0000000000..07c032b5bd
--- /dev/null
+++ b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/settings/SearchEngineValidationTest.kt
@@ -0,0 +1,94 @@
+/* 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.focus.settings
+
+import mozilla.components.concept.fetch.Client
+import mozilla.components.concept.fetch.Request
+import mozilla.components.concept.fetch.Response
+import mozilla.components.lib.fetch.okhttp.OkHttpClient
+import okhttp3.mockwebserver.MockResponse
+import okhttp3.mockwebserver.MockWebServer
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mozilla.focus.settings.ManualAddSearchEngineSettingsFragment.Companion.isValidSearchQueryURL
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+// This unit test is not running on an Android device. Allow me to use spaces in function names.
+@Suppress("IllegalIdentifier")
+class SearchEngineValidationTest {
+
+ lateinit var client: Client
+
+ @Before
+ fun setup() {
+ client = OkHttpWrapper()
+ }
+
+ @Test
+ fun `URL returning 200 OK is valid`() = withMockWebServer(responseWithStatus(200)) {
+ assertTrue(isValidSearchQueryURL(client, it.rootUrl()))
+ }
+
+ @Test
+ fun `URL using HTTP redirect is invalid`() = withMockWebServer(responseWithStatus(301)) {
+ // We now follow redirects(Issue #1976). This test now asserts false.
+ assertFalse(isValidSearchQueryURL(client, it.rootUrl()))
+ }
+
+ @Test
+ fun `URL returning 404 NOT FOUND is not valid`() = withMockWebServer(responseWithStatus(404)) {
+ assertFalse(isValidSearchQueryURL(client, it.rootUrl()))
+ }
+
+ @Test
+ fun `URL returning server error is not valid`() = withMockWebServer(responseWithStatus(500)) {
+ assertFalse(isValidSearchQueryURL(client, it.rootUrl()))
+ }
+
+ @Test
+ fun `URL timing out is not valid`() = withMockWebServer {
+ // Without queuing a response MockWebServer will not return anything and keep the connection open
+ assertFalse(isValidSearchQueryURL(client, it.rootUrl()))
+ }
+}
+
+/**
+ * Helper for creating a test that uses a mock webserver instance.
+ */
+private fun withMockWebServer(vararg responses: MockResponse, block: (MockWebServer) -> Unit) {
+ val server = MockWebServer()
+
+ responses.forEach { server.enqueue(it) }
+
+ server.start()
+
+ try {
+ block(server)
+ } finally {
+ server.shutdown()
+ }
+}
+
+private fun MockWebServer.rootUrl(): String = url("/").toString()
+
+private fun responseWithStatus(status: Int) =
+ MockResponse()
+ .setResponseCode(status)
+ .setBody("")
+
+private class OkHttpWrapper : Client() {
+ private val actual = OkHttpClient()
+
+ override fun fetch(request: Request): Response {
+ // OkHttpClient does not support private requests. Therefore we make them non-private for
+ // testing purposes
+ val nonPrivate = request.copy(private = false)
+ return actual.fetch(nonPrivate)
+ }
+}
diff --git a/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/shortcut/HomeScreenTest.kt b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/shortcut/HomeScreenTest.kt
new file mode 100644
index 0000000000..7cb9e889f7
--- /dev/null
+++ b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/shortcut/HomeScreenTest.kt
@@ -0,0 +1,20 @@
+/* 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.focus.shortcut
+
+import junit.framework.TestCase.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mozilla.focus.shortcut.HomeScreen.generateTitleFromUrl
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class HomeScreenTest {
+ @Test
+ fun testGenerateTitleFromUrl() {
+ assertEquals("mozilla.org", generateTitleFromUrl("https://www.mozilla.org"))
+ assertEquals("facebook.com", generateTitleFromUrl("http://m.facebook.com/home"))
+ assertEquals("", generateTitleFromUrl("mozilla"))
+ }
+}
diff --git a/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/shortcut/IconGeneratorTest.kt b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/shortcut/IconGeneratorTest.kt
new file mode 100644
index 0000000000..f84c3d4d5d
--- /dev/null
+++ b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/shortcut/IconGeneratorTest.kt
@@ -0,0 +1,59 @@
+/* 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.focus.shortcut
+
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class IconGeneratorTest {
+ @Test
+ fun testRepresentativeCharacter() {
+ assertEquals('M', IconGenerator.getRepresentativeCharacter("https://mozilla.org"))
+ assertEquals('W', IconGenerator.getRepresentativeCharacter("http://wikipedia.org"))
+ assertEquals('P', IconGenerator.getRepresentativeCharacter("http://plus.google.com"))
+ assertEquals('E', IconGenerator.getRepresentativeCharacter("https://en.m.wikipedia.org/wiki/Main_Page"))
+
+ // Stripping common prefixes
+ assertEquals('T', IconGenerator.getRepresentativeCharacter("http://www.theverge.com"))
+ assertEquals('F', IconGenerator.getRepresentativeCharacter("https://m.facebook.com"))
+ assertEquals('T', IconGenerator.getRepresentativeCharacter("https://mobile.twitter.com"))
+
+ // Special urls
+ assertEquals('?', IconGenerator.getRepresentativeCharacter("file:///"))
+ assertEquals('S', IconGenerator.getRepresentativeCharacter("file:///system/"))
+ assertEquals('P', IconGenerator.getRepresentativeCharacter("ftp://people.mozilla.org/test"))
+
+ // No values
+ assertEquals('?', IconGenerator.getRepresentativeCharacter(""))
+ assertEquals('?', IconGenerator.getRepresentativeCharacter(null))
+
+ // Rubbish
+ assertEquals('Z', IconGenerator.getRepresentativeCharacter("zZz"))
+ assertEquals('Ö', IconGenerator.getRepresentativeCharacter("ölkfdpou3rkjaslfdköasdfo8"))
+ assertEquals('?', IconGenerator.getRepresentativeCharacter("_*+*'##"))
+ assertEquals('ツ', IconGenerator.getRepresentativeCharacter("¯\\_(ツ)_/¯"))
+ assertEquals('ಠ', IconGenerator.getRepresentativeCharacter("ಠ_ಠ Look of Disapproval"))
+
+ // Non-ASCII
+ assertEquals('Ä', IconGenerator.getRepresentativeCharacter("http://www.ätzend.de"))
+ assertEquals('名', IconGenerator.getRepresentativeCharacter("http://名がドメイン.com"))
+ assertEquals('C', IconGenerator.getRepresentativeCharacter("http://√.com"))
+ assertEquals('ß', IconGenerator.getRepresentativeCharacter("http://ß.de"))
+ assertEquals('Ԛ', IconGenerator.getRepresentativeCharacter("http://ԛәлп.com/")) // cyrillic
+
+ // Punycode
+ assertEquals('X', IconGenerator.getRepresentativeCharacter("http://xn--tzend-fra.de")) // ätzend.de
+ assertEquals('X', IconGenerator.getRepresentativeCharacter("http://xn--V8jxj3d1dzdz08w.com")) // 名がドメイン.com
+
+ // Numbers
+ assertEquals('1', IconGenerator.getRepresentativeCharacter("https://www.1and1.com/"))
+
+ // IP
+ assertEquals('1', IconGenerator.getRepresentativeCharacter("https://192.168.0.1"))
+ }
+}
diff --git a/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/sitepermissions/SitePermissionOptionsStorageTest.kt b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/sitepermissions/SitePermissionOptionsStorageTest.kt
new file mode 100644
index 0000000000..80168da22d
--- /dev/null
+++ b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/sitepermissions/SitePermissionOptionsStorageTest.kt
@@ -0,0 +1,342 @@
+/* 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.focus.sitepermissions
+
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.spy
+import org.mozilla.focus.R
+import org.mozilla.focus.settings.permissions.AutoplayOption
+import org.mozilla.focus.settings.permissions.SitePermissionOption
+import org.mozilla.focus.settings.permissions.permissionoptions.SitePermission
+import org.mozilla.focus.settings.permissions.permissionoptions.SitePermissionOptionsStorage
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class SitePermissionOptionsStorageTest {
+ private lateinit var storage: SitePermissionOptionsStorage
+
+ @Before
+ fun setup() {
+ storage = spy(SitePermissionOptionsStorage(testContext))
+ }
+
+ @Test
+ fun `GIVEN get site permission option selected label is called WHEN camera permission isn't granted THEN blocked by android is return`() {
+ doReturn(false).`when`(storage).isAndroidPermissionGranted(SitePermission.CAMERA)
+
+ assertEquals(
+ testContext.getString(R.string.phone_feature_blocked_by_android),
+ storage.getSitePermissionOptionSelectedLabel(SitePermission.CAMERA),
+ )
+ }
+
+ @Test
+ fun `GIVEN get site permission option selected label is called WHEN camera permission is granted THEN default value ask to allow option is return`() {
+ doReturn(true).`when`(storage).isAndroidPermissionGranted(SitePermission.CAMERA)
+
+ assertEquals(
+ testContext.getString(R.string.preference_option_phone_feature_ask_to_allow),
+ storage.getSitePermissionOptionSelectedLabel(SitePermission.CAMERA),
+ )
+ }
+
+ @Test
+ fun `GIVEN get site permission option selected label is called WHEN autoplay permission is granted THEN default value block audio only return`() {
+ doReturn(true).`when`(storage).isAndroidPermissionGranted(SitePermission.AUTOPLAY)
+
+ assertEquals(
+ testContext.getString(R.string.preference_block_autoplay_audio_only),
+ storage.getSitePermissionOptionSelectedLabel(SitePermission.AUTOPLAY),
+ )
+ }
+
+ @Test
+ fun `GIVEN get site permission option selected label is called WHEN location permission is granted THEN default value ask to allow option is return`() {
+ doReturn(true).`when`(storage).isAndroidPermissionGranted(SitePermission.LOCATION)
+
+ assertEquals(
+ testContext.getString(R.string.preference_option_phone_feature_ask_to_allow),
+ storage.getSitePermissionOptionSelectedLabel(SitePermission.LOCATION),
+ )
+ }
+
+ @Test
+ fun `GIVEN get site permission options is called WHEN location permission is passed as argument THEN available options are return`() {
+ assertEquals(
+ listOf(SitePermissionOption.AskToAllow(), SitePermissionOption.Blocked()),
+ storage.getSitePermissionOptions(SitePermission.LOCATION),
+ )
+ }
+
+ @Test
+ fun `GIVEN get site permission options is called WHEN camera permission is passed as argument THEN available options are return`() {
+ assertEquals(
+ listOf(SitePermissionOption.AskToAllow(), SitePermissionOption.Blocked()),
+ storage.getSitePermissionOptions(SitePermission.CAMERA),
+ )
+ }
+
+ @Test
+ fun `GIVEN get site permission options is called WHEN microphone permission is passed as argument THEN available options are return`() {
+ assertEquals(
+ listOf(SitePermissionOption.AskToAllow(), SitePermissionOption.Blocked()),
+ storage.getSitePermissionOptions(SitePermission.MICROPHONE),
+ )
+ }
+
+ @Test
+ fun `GIVEN get site permission options is called WHEN notification permission is passed as argument THEN available options are return`() {
+ assertEquals(
+ listOf(SitePermissionOption.AskToAllow(), SitePermissionOption.Blocked()),
+ storage.getSitePermissionOptions(SitePermission.NOTIFICATION),
+ )
+ }
+
+ @Test
+ fun `GIVEN get site permission options is called WHEN media key system access permission is passed as argument THEN available options are return`() {
+ assertEquals(
+ listOf(SitePermissionOption.AskToAllow(), SitePermissionOption.Blocked(), SitePermissionOption.Allowed()),
+ storage.getSitePermissionOptions(SitePermission.MEDIA_KEY_SYSTEM_ACCESS),
+ )
+ }
+
+ @Test
+ fun `GIVEN get site permission options is called WHEN autoplay permission is passed as argument THEN available options are return`() {
+ assertEquals(
+ listOf(AutoplayOption.AllowAudioVideo(), AutoplayOption.BlockAudioOnly(), AutoplayOption.BlockAudioVideo()),
+ storage.getSitePermissionOptions(SitePermission.AUTOPLAY),
+ )
+ }
+
+ @Test
+ fun `GIVEN get site permission label is called WHEN camera permission is passed as argument THEN permission label is return`() {
+ assertEquals(
+ testContext.getString(R.string.preference_phone_feature_camera),
+ storage.getSitePermissionLabel(SitePermission.CAMERA),
+ )
+ }
+
+ @Test
+ fun `GIVEN get site permission label is called WHEN location permission is passed as argument THEN permission label is return`() {
+ assertEquals(
+ testContext.getString(R.string.preference_phone_feature_location),
+ storage.getSitePermissionLabel(SitePermission.LOCATION),
+ )
+ }
+
+ @Test
+ fun `GIVEN get site permission label is called WHEN microphone permission is passed as argument THEN permission label is return`() {
+ assertEquals(
+ testContext.getString(R.string.preference_phone_feature_microphone),
+ storage.getSitePermissionLabel(SitePermission.MICROPHONE),
+ )
+ }
+
+ @Test
+ fun `GIVEN get site permission label is called WHEN notification permission is passed as argument THEN permission label is return`() {
+ assertEquals(
+ testContext.getString(R.string.preference_phone_feature_notification),
+ storage.getSitePermissionLabel(SitePermission.NOTIFICATION),
+ )
+ }
+
+ @Test
+ fun `GIVEN get site permission label is called WHEN media key system access permission is passed as argument THEN permission label is return`() {
+ assertEquals(
+ testContext.getString(R.string.preference_phone_feature_media_key_system_access),
+ storage.getSitePermissionLabel(SitePermission.MEDIA_KEY_SYSTEM_ACCESS),
+ )
+ }
+
+ @Test
+ fun `GIVEN get site permission label is called WHEN autoplay permission is passed as argument THEN permission label is return`() {
+ assertEquals(
+ testContext.getString(R.string.preference_autoplay),
+ storage.getSitePermissionLabel(SitePermission.AUTOPLAY),
+ )
+ }
+
+ @Test
+ fun `GIVEN get site permission default option is called WHEN camera permission is passed as argument THEN default option is return`() {
+ assertEquals(
+ SitePermissionOption.AskToAllow(),
+ storage.getSitePermissionDefaultOption(SitePermission.CAMERA),
+ )
+ }
+
+ @Test
+ fun `GIVEN get site permission default option is called WHEN location permission is passed as argument THEN default option is return`() {
+ assertEquals(
+ SitePermissionOption.AskToAllow(),
+ storage.getSitePermissionDefaultOption(SitePermission.LOCATION),
+ )
+ }
+
+ @Test
+ fun `GIVEN get site permission default option is called WHEN microphone permission is passed as argument THEN default option is return`() {
+ assertEquals(
+ SitePermissionOption.AskToAllow(),
+ storage.getSitePermissionDefaultOption(SitePermission.MICROPHONE),
+ )
+ }
+
+ @Test
+ fun `GIVEN get site permission default option is called WHEN notification permission is passed as argument THEN default option is return`() {
+ assertEquals(
+ SitePermissionOption.AskToAllow(),
+ storage.getSitePermissionDefaultOption(SitePermission.NOTIFICATION),
+ )
+ }
+
+ @Test
+ fun `GIVEN get site permission default option is called WHEN media key system access permission is passed as argument THEN default option is return`() {
+ assertEquals(
+ SitePermissionOption.AskToAllow(),
+ storage.getSitePermissionDefaultOption(SitePermission.MEDIA_KEY_SYSTEM_ACCESS),
+ )
+ }
+
+ @Test
+ fun `GIVEN get site permission default option is called WHEN autoplay permission is passed as argument THEN default option is return`() {
+ assertEquals(
+ AutoplayOption.BlockAudioOnly(),
+ storage.getSitePermissionDefaultOption(SitePermission.AUTOPLAY),
+ )
+ }
+
+ @Test
+ fun `GIVEN get permission selected option is called WHEN pref_key_allowed is saved in SharedPreferences THEN site permission Allowed is return`() {
+ doReturn(testContext.getString(R.string.pref_key_allowed))
+ .`when`(storage).permissionSelectedOptionByKey(R.string.pref_key_phone_feature_camera)
+
+ assertEquals(
+ SitePermissionOption.Allowed(),
+ storage.permissionSelectedOption(SitePermission.CAMERA),
+ )
+ }
+
+ @Test
+ fun `GIVEN get permission selected option is called WHEN pref_key_allow_autoplay_audio_video is saved in SharedPreferences THEN site permission Allow Audio and Video is return`() {
+ doReturn(testContext.getString(R.string.pref_key_allow_autoplay_audio_video))
+ .`when`(storage).permissionSelectedOptionByKey(R.string.pref_key_phone_feature_camera)
+
+ assertEquals(
+ AutoplayOption.AllowAudioVideo(),
+ storage.permissionSelectedOption(SitePermission.CAMERA),
+ )
+ }
+
+ @Test
+ fun `GIVEN get permission selected option is called WHEN pref_key_block_autoplay_audio_video is saved in SharedPreferences THEN site permission Block Audio and Video is return`() {
+ doReturn(testContext.getString(R.string.pref_key_block_autoplay_audio_video))
+ .`when`(storage).permissionSelectedOptionByKey(R.string.pref_key_phone_feature_camera)
+
+ assertEquals(
+ AutoplayOption.BlockAudioVideo(),
+ storage.permissionSelectedOption(SitePermission.CAMERA),
+ )
+ }
+
+ @Test
+ fun `GIVEN get permission selected option is called WHEN pref_key_blocked is saved in SharedPreferences THEN site permission Blocked is return`() {
+ doReturn(testContext.getString(R.string.pref_key_blocked))
+ .`when`(storage).permissionSelectedOptionByKey(R.string.pref_key_phone_feature_camera)
+
+ assertEquals(
+ SitePermissionOption.Blocked(),
+ storage.permissionSelectedOption(SitePermission.CAMERA),
+ )
+ }
+
+ @Test
+ fun `GIVEN get permission selected option is called WHEN pref_key_ask_to_allow is saved in SharedPreferences THEN site permission Ask to allow is return`() {
+ doReturn(testContext.getString(R.string.pref_key_ask_to_allow))
+ .`when`(storage).permissionSelectedOptionByKey(R.string.pref_key_phone_feature_camera)
+
+ assertEquals(
+ SitePermissionOption.AskToAllow(),
+ storage.permissionSelectedOption(SitePermission.CAMERA),
+ )
+ }
+
+ @Test
+ fun `GIVEN get permission selected option is called WHEN pref_key_block_autoplay_audio_only is saved in SharedPreferences THEN site permission Block Audio only is return`() {
+ doReturn(testContext.getString(R.string.pref_key_block_autoplay_audio_only))
+ .`when`(storage).permissionSelectedOptionByKey(R.string.pref_key_phone_feature_camera)
+
+ assertEquals(
+ AutoplayOption.BlockAudioOnly(),
+ storage.permissionSelectedOption(SitePermission.CAMERA),
+ )
+ }
+
+ @Test
+ fun `GIVEN get site permission preference id is called WHEN site permission camera is passed as argument THEN pref_key_phone_feature_camera is return`() {
+ assertEquals(
+ R.string.pref_key_phone_feature_camera,
+ storage.getSitePermissionPreferenceId(SitePermission.CAMERA),
+ )
+ }
+
+ @Test
+ fun `GIVEN get site permission preference id is called WHEN site permission location is passed as argument THEN pref_key_phone_feature_location is return`() {
+ assertEquals(
+ R.string.pref_key_phone_feature_location,
+ storage.getSitePermissionPreferenceId(SitePermission.LOCATION),
+ )
+ }
+
+ @Test
+ fun `GIVEN get site permission preference id is called WHEN site permission microphone is passed as argument THEN pref_key_phone_feature_microphone is return`() {
+ assertEquals(
+ R.string.pref_key_phone_feature_microphone,
+ storage.getSitePermissionPreferenceId(SitePermission.MICROPHONE),
+ )
+ }
+
+ @Test
+ fun `GIVEN get site permission preference id is called WHEN site permission notification is passed as argument THEN pref_key_phone_feature_notification is return`() {
+ assertEquals(
+ R.string.pref_key_phone_feature_notification,
+ storage.getSitePermissionPreferenceId(SitePermission.NOTIFICATION),
+ )
+ }
+
+ @Test
+ fun `GIVEN get site permission preference id is called WHEN site permission autoplay is passed as argument THEN pref_key_autoplay is return`() {
+ assertEquals(
+ R.string.pref_key_autoplay,
+ storage.getSitePermissionPreferenceId(SitePermission.AUTOPLAY),
+ )
+ }
+
+ @Test
+ fun `GIVEN get site permission preference id is called WHEN site permission autoplay audible is passed as argument THEN pref_key_allow_autoplay_audio_video is return`() {
+ assertEquals(
+ R.string.pref_key_allow_autoplay_audio_video,
+ storage.getSitePermissionPreferenceId(SitePermission.AUTOPLAY_AUDIBLE),
+ )
+ }
+
+ @Test
+ fun `GIVEN get site permission preference id is called WHEN site permission autoplay inaudible is passed as argument THEN pref_key_block_autoplay_audio_video is return`() {
+ assertEquals(
+ R.string.pref_key_block_autoplay_audio_video,
+ storage.getSitePermissionPreferenceId(SitePermission.AUTOPLAY_INAUDIBLE),
+ )
+ }
+
+ @Test
+ fun `GIVEN get site permission preference id is called WHEN site permission media key system access is passed as argument THEN pref_key_browser_feature_media_key_system_access is return`() {
+ assertEquals(
+ R.string.pref_key_browser_feature_media_key_system_access,
+ storage.getSitePermissionPreferenceId(SitePermission.MEDIA_KEY_SYSTEM_ACCESS),
+ )
+ }
+}
diff --git a/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/sitepermissions/SitePermissionOptionsStoreTest.kt b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/sitepermissions/SitePermissionOptionsStoreTest.kt
new file mode 100644
index 0000000000..a76ecb95d6
--- /dev/null
+++ b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/sitepermissions/SitePermissionOptionsStoreTest.kt
@@ -0,0 +1,89 @@
+/* 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.focus.sitepermissions
+
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.verify
+import org.mozilla.focus.settings.permissions.AutoplayOption
+import org.mozilla.focus.settings.permissions.SitePermissionOption
+import org.mozilla.focus.settings.permissions.permissionoptions.SitePermission
+import org.mozilla.focus.settings.permissions.permissionoptions.SitePermissionOptionsScreenAction
+import org.mozilla.focus.settings.permissions.permissionoptions.SitePermissionOptionsScreenState
+import org.mozilla.focus.settings.permissions.permissionoptions.SitePermissionOptionsScreenStore
+import org.mozilla.focus.settings.permissions.permissionoptions.SitePermissionOptionsStorage
+import org.mozilla.focus.settings.permissions.permissionoptions.SitePermissionOptionsStorageMiddleware
+
+class SitePermissionOptionsStoreTest {
+ private lateinit var sitePermissionOptionsStorageMiddleware: SitePermissionOptionsStorageMiddleware
+ private lateinit var store: SitePermissionOptionsScreenStore
+ private lateinit var sitePermissionState: SitePermissionOptionsScreenState
+ private val storage: SitePermissionOptionsStorage = mock()
+
+ @Before
+ fun setup() {
+ sitePermissionOptionsStorageMiddleware = SitePermissionOptionsStorageMiddleware(SitePermission.CAMERA, storage)
+ sitePermissionState = SitePermissionOptionsScreenState()
+
+ doReturn(listOf(SitePermissionOption.AskToAllow(), SitePermissionOption.Blocked())).`when`(storage).getSitePermissionOptions(SitePermission.CAMERA)
+ doReturn(SitePermissionOption.AskToAllow()).`when`(storage).permissionSelectedOption(SitePermission.CAMERA)
+ doReturn("Camera").`when`(storage).getSitePermissionLabel(SitePermission.CAMERA)
+ doReturn(false).`when`(storage).isAndroidPermissionGranted(SitePermission.CAMERA)
+
+ store = SitePermissionOptionsScreenStore(sitePermissionState, listOf(sitePermissionOptionsStorageMiddleware))
+ }
+
+ @Test
+ fun `GIVEN site permission screen store WHEN android permission action is dispatched THEN site permission options screen state is updated`() {
+ store.dispatch(SitePermissionOptionsScreenAction.AndroidPermission(true)).joinBlocking()
+
+ verify(storage).getSitePermissionOptions(SitePermission.CAMERA)
+ verify(storage).permissionSelectedOption(SitePermission.CAMERA)
+ verify(storage).getSitePermissionLabel(SitePermission.CAMERA)
+ verify(storage).isAndroidPermissionGranted(SitePermission.CAMERA)
+ assertTrue(store.state.isAndroidPermissionGranted)
+ }
+
+ @Test
+ fun `GIVEN site permission screen store WHEN select permission action is dispatched THEN site permission options screen state is updated`() {
+ store.dispatch(SitePermissionOptionsScreenAction.Select(SitePermissionOption.Blocked())).joinBlocking()
+
+ verify(storage).saveCurrentSitePermissionOptionInSharePref(SitePermissionOption.Blocked(), SitePermission.CAMERA)
+ assertEquals(SitePermissionOption.Blocked(), store.state.selectedSitePermissionOption)
+ }
+
+ @Test
+ fun `GIVEN site permission screen store WHEN update site permission options is dispatched THEN site permission options screen state is updated`() {
+ val sitePermissionLabel = "Autoplay"
+ store.dispatch(
+ SitePermissionOptionsScreenAction.UpdateSitePermissionOptions(
+ listOf(
+ AutoplayOption.BlockAudioOnly(),
+ AutoplayOption.AllowAudioVideo(),
+ AutoplayOption.BlockAudioVideo(),
+ ),
+ AutoplayOption.AllowAudioVideo(),
+ sitePermissionLabel,
+ true,
+ ),
+ ).joinBlocking()
+
+ assertEquals(AutoplayOption.AllowAudioVideo(), store.state.selectedSitePermissionOption)
+ assertTrue(store.state.isAndroidPermissionGranted)
+ assertEquals(
+ listOf(
+ AutoplayOption.BlockAudioOnly(),
+ AutoplayOption.AllowAudioVideo(),
+ AutoplayOption.BlockAudioVideo(),
+ ),
+ store.state.sitePermissionOptionList,
+ )
+ assertEquals(sitePermissionLabel, store.state.sitePermissionLabel)
+ }
+}
diff --git a/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/sitepermissions/SitePermissionsFragmentTest.kt b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/sitepermissions/SitePermissionsFragmentTest.kt
new file mode 100644
index 0000000000..0072c55dee
--- /dev/null
+++ b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/sitepermissions/SitePermissionsFragmentTest.kt
@@ -0,0 +1,57 @@
+/* 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.focus.sitepermissions
+
+import androidx.preference.Preference
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mozilla.focus.R
+import org.mozilla.focus.settings.permissions.SitePermissionsFragment
+import org.mozilla.focus.settings.permissions.permissionoptions.SitePermission
+import org.mozilla.focus.settings.permissions.permissionoptions.SitePermissionOptionsStorage
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class SitePermissionsFragmentTest {
+
+ private val storage: SitePermissionOptionsStorage = mock()
+ private lateinit var fragment: SitePermissionsFragment
+
+ @Before
+ fun setup() {
+ fragment = spy(SitePermissionsFragment())
+ doReturn(testContext).`when`(fragment).context
+ fragment.storage = storage
+ }
+
+ @Test
+ fun `GIVEN site permission fragment WHEN fragment is created THEN summary for Preference is set from storage`() {
+ val cameraPreference = Preference(testContext)
+ doReturn(cameraPreference).`when`(fragment).getPreference(R.string.pref_key_phone_feature_camera)
+ val expectedSummary = testContext.getString(R.string.phone_feature_blocked_by_android)
+ doReturn(expectedSummary).`when`(storage).getSitePermissionOptionSelectedLabel(SitePermission.CAMERA)
+
+ fragment.initPhoneFeature(SitePermission.CAMERA)
+
+ assertEquals(expectedSummary, cameraPreference.summary)
+ }
+
+ @Test
+ fun `GIVEN site permission fragment WHEN preference is clicked THEN navigate to site permission options screen it called`() {
+ val cameraPreference = Preference(testContext)
+ doReturn(cameraPreference).`when`(fragment).getPreference(R.string.pref_key_phone_feature_camera)
+
+ fragment.initPhoneFeature(SitePermission.CAMERA)
+ cameraPreference.performClick()
+
+ verify(fragment).navigateToSitePermissionOptionsScreen(SitePermission.CAMERA)
+ }
+}
diff --git a/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/telemetry/GleanMetricsServiceTest.kt b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/telemetry/GleanMetricsServiceTest.kt
new file mode 100644
index 0000000000..28166ce263
--- /dev/null
+++ b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/telemetry/GleanMetricsServiceTest.kt
@@ -0,0 +1,42 @@
+/* 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.focus.telemetry
+
+import android.content.Context
+import kotlinx.coroutines.runBlocking
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.Engine
+import mozilla.components.feature.search.telemetry.SearchProviderModel
+import mozilla.components.feature.search.telemetry.ads.AdsTelemetry
+import mozilla.components.feature.search.telemetry.incontent.InContentTelemetry
+import org.junit.Test
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.mozilla.focus.Components
+
+class GleanMetricsServiceTest {
+ @Test
+ fun `WHEN installSearchTelemetryExtensions is called THEN install the ads and search telemetry extensions`() {
+ val components = mock(Components::class.java)
+ val store = mock(BrowserStore::class.java)
+ val engine = mock(Engine::class.java)
+ val adsExtension = mock(AdsTelemetry::class.java)
+ val searchExtension = mock(InContentTelemetry::class.java)
+ val providerList: List<SearchProviderModel> = mock()
+ doReturn(engine).`when`(components).engine
+ doReturn(store).`when`(components).store
+ doReturn(adsExtension).`when`(components).adsTelemetry
+ doReturn(searchExtension).`when`(components).searchTelemetry
+ val glean = GleanMetricsService(mock(Context::class.java))
+
+ runBlocking {
+ glean.installSearchTelemetryExtensions(components, providerList)
+
+ verify(adsExtension).install(engine, store, providerList)
+ verify(searchExtension).install(engine, store, providerList)
+ }
+ }
+}
diff --git a/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/telemetry/ProfilerMarkerFactProcessorTest.kt b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/telemetry/ProfilerMarkerFactProcessorTest.kt
new file mode 100644
index 0000000000..d95da8aac5
--- /dev/null
+++ b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/telemetry/ProfilerMarkerFactProcessorTest.kt
@@ -0,0 +1,96 @@
+/* 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.focus.telemetry
+
+import android.os.Handler
+import android.os.Looper
+import mozilla.components.concept.base.profiler.Profiler
+import mozilla.components.support.base.Component
+import mozilla.components.support.base.facts.Action
+import mozilla.components.support.base.facts.Fact
+import mozilla.components.support.test.any
+import mozilla.components.support.test.argumentCaptor
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoInteractions
+import org.mockito.Mockito.`when`
+
+class ProfilerMarkerFactProcessorTest {
+
+ private val profiler: Profiler = mock(Profiler::class.java)
+ private val mainHandler: Handler = mock(Handler::class.java)
+ lateinit var processor: ProfilerMarkerFactProcessor
+
+ var myLooper: Looper? = null
+
+ @Before
+ fun setUp() {
+ myLooper = null
+ processor = ProfilerMarkerFactProcessor({ profiler }, mainHandler, { myLooper })
+ }
+
+ @Test
+ fun `Test on the main thread`() {
+ // GIVEN we are on the main thread
+ myLooper = mainHandler.looper // main thread
+
+ // WHEN a fact with an implementation detail action is received
+ val fact = newFact(Action.IMPLEMENTATION_DETAIL)
+ processor.process(fact)
+
+ // THEN a profiler marker is added now
+ verify(profiler).addMarker(fact.item)
+ }
+
+ @Test
+ fun `Test not on the main thread`() {
+ // GIVEN we are not on the main thread
+ myLooper = mock(Looper::class.java) // off main thread
+
+ // WHEN a fact with an implementation detail action is received
+ val mainThreadPostedArg = argumentCaptor<Runnable>()
+ `when`(profiler.getProfilerTime()).thenReturn(100.0)
+
+ val fact = newFact(Action.IMPLEMENTATION_DETAIL)
+ processor.process(fact)
+
+ // THEN adding the marker is posted to the main thread
+ verify(mainHandler).post(mainThreadPostedArg.capture())
+ verifyProfilerAddMarkerWasNotCalled()
+
+ mainThreadPostedArg.value.run()
+ verify(profiler).addMarker(fact.item, 100.0, 100.0, null)
+ }
+
+ @Test
+ fun `Test non-implementation detail`() {
+ // WHEN a fact with a non-implementation detail action is received
+ val fact = newFact(Action.CANCEL)
+ processor.process(fact)
+
+ // THEN no profiler marker is added
+ verifyNoInteractions(profiler)
+ }
+
+ private fun verifyProfilerAddMarkerWasNotCalled() {
+ verify(profiler, never()).addMarker(any())
+ verify(profiler, never()).addMarker(any(), any() as Double?)
+ verify(profiler, never()).addMarker(any(), any() as String?)
+ verify(profiler, never()).addMarker(any(), any(), any())
+ verify(profiler, never()).addMarker(any(), any(), any(), any())
+ }
+}
+
+private fun newFact(
+ action: Action,
+ item: String = "itemName",
+) = Fact(
+ Component.BROWSER_SESSION_STORAGE,
+ action,
+ item,
+)
diff --git a/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/telemetry/StartupActivityLogTest.kt b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/telemetry/StartupActivityLogTest.kt
new file mode 100644
index 0000000000..b08ebb9f6e
--- /dev/null
+++ b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/telemetry/StartupActivityLogTest.kt
@@ -0,0 +1,99 @@
+/* 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.focus.telemetry
+
+import android.app.Activity
+import android.app.Application
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mozilla.focus.telemetry.startuptelemetry.StartupActivityLog
+import org.mozilla.focus.telemetry.startuptelemetry.StartupActivityLog.LogEntry
+
+class StartupActivityLogTest {
+
+ private lateinit var log: StartupActivityLog
+ private lateinit var appObserver: StartupActivityLog.StartupLogAppLifecycleObserver
+ private lateinit var activityCallbacks: StartupActivityLog.StartupLogActivityLifecycleCallbacks
+
+ @Before
+ fun setUp() {
+ log = StartupActivityLog()
+ val (appObserver, activityCallbacks) = log.getObserversForTesting()
+ this.appObserver = appObserver
+ this.activityCallbacks = activityCallbacks
+ }
+
+ @Test
+ fun `WHEN register is called THEN it is registered`() {
+ val app: Application = mock()
+ val lifecycleOwner: LifecycleOwner = mock()
+ val mLifecycle: Lifecycle = mock()
+ `when`(lifecycleOwner.lifecycle).thenReturn(mLifecycle)
+
+ log.registerInAppOnCreate(app, lifecycleOwner)
+
+ verify(app).registerActivityLifecycleCallbacks(any())
+ }
+
+ @Test // we test start and stop individually due to the clear-on-stop behavior.
+ fun `WHEN app observer start is called THEN it is added directly to the log`() {
+ assertTrue(log.log.isEmpty())
+
+ appObserver.onStart(mock())
+ assertEquals(listOf(LogEntry.AppStarted), log.log)
+
+ appObserver.onStart(mock())
+ assertEquals(listOf(LogEntry.AppStarted, LogEntry.AppStarted), log.log)
+ }
+
+ @Test // we test start and stop individually due to the clear-on-stop behavior.
+ fun `WHEN app observer stop is called THEN it is added directly to the log`() {
+ assertTrue(log.log.isEmpty())
+
+ appObserver.onStop(mock())
+ assertEquals(listOf(LogEntry.AppStopped), log.log)
+ }
+
+ @Test
+ fun `WHEN activity callback methods are called THEN they are added directly to the log`() {
+ assertTrue(log.log.isEmpty())
+ val expected = mutableListOf<LogEntry>()
+
+ val activityClass = mock<Activity>()::class.java // mockk can't mock Class<...>
+
+ activityCallbacks.onActivityCreated(mock(), null)
+ expected.add(LogEntry.ActivityCreated(activityClass))
+ assertEquals(expected, log.log)
+
+ activityCallbacks.onActivityStarted(mock())
+ expected.add(LogEntry.ActivityStarted(activityClass))
+ assertEquals(expected, log.log)
+
+ activityCallbacks.onActivityStopped(mock())
+ expected.add(LogEntry.ActivityStopped(activityClass))
+ assertEquals(expected, log.log)
+ }
+
+ @Test
+ fun `WHEN app STOPPED is called THEN the log is emptied expect for the stop event`() {
+ assertTrue(log.log.isEmpty())
+
+ activityCallbacks.onActivityCreated(mock(), null)
+ activityCallbacks.onActivityStarted(mock())
+ appObserver.onStart(mock())
+ assertEquals(3, log.log.size)
+
+ appObserver.onStop(mock())
+ assertEquals(listOf(LogEntry.AppStopped), log.log)
+ }
+}
diff --git a/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/telemetry/StartupPathProviderTest.kt b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/telemetry/StartupPathProviderTest.kt
new file mode 100644
index 0000000000..cc023ee079
--- /dev/null
+++ b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/telemetry/StartupPathProviderTest.kt
@@ -0,0 +1,213 @@
+/* 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.focus.telemetry
+
+import android.content.Intent
+import androidx.lifecycle.Lifecycle
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mozilla.focus.telemetry.startuptelemetry.StartupPathProvider
+import org.mozilla.focus.telemetry.startuptelemetry.StartupPathProvider.StartupPath
+
+class StartupPathProviderTest {
+
+ private lateinit var provider: StartupPathProvider
+ private lateinit var callbacks: StartupPathProvider.StartupPathLifecycleObserver
+
+ private val intent: Intent = mock()
+
+ @Before
+ fun setUp() {
+ provider = StartupPathProvider()
+ callbacks = provider.getTestCallbacks()
+ }
+
+ @Test
+ fun `WHEN attach is called THEN the provider is registered to the lifecycle`() {
+ val lifecycle = mock<Lifecycle>()
+ provider.attachOnActivityOnCreate(lifecycle, null)
+
+ verify(lifecycle).addObserver(any())
+ }
+
+ @Test
+ fun `WHEN calling attach THEN the intent is passed to on intent received`() {
+ // With this test, we're basically saying, "attach..." does the same thing as
+ // "onIntentReceived" so we don't need to duplicate all the tests we run for
+ // "onIntentReceived".
+ val spyProvider = spy(provider)
+ spyProvider.attachOnActivityOnCreate(mock(), intent)
+
+ verify(spyProvider).onIntentReceived(intent)
+ }
+
+ @Test
+ fun `GIVEN no intent is received and the activity is not started WHEN getting the start up path THEN it is not set`() {
+ assertEquals(StartupPath.NOT_SET, provider.startupPathForActivity)
+ }
+
+ @Test
+ fun `GIVEN a main intent is received but the activity is not started yet WHEN getting the start up path THEN main is returned`() {
+ doReturn(Intent.ACTION_MAIN).`when`(intent).action
+ provider.onIntentReceived(intent)
+ assertEquals(StartupPath.MAIN, provider.startupPathForActivity)
+ }
+
+ @Test
+ fun `GIVEN a main intent is received and the app is started WHEN getting the start up path THEN it is main`() {
+ doReturn(Intent.ACTION_MAIN).`when`(intent).action
+ callbacks.onCreate(mock())
+ provider.onIntentReceived(intent)
+ callbacks.onStart(mock())
+
+ assertEquals(StartupPath.MAIN, provider.startupPathForActivity)
+ }
+
+ @Test
+ fun `GIVEN the app is launched from the homeScreen WHEN getting the start up path THEN it is main`() {
+ // There's technically more to a homeScreen Intent but it's fine for now.
+ doReturn(Intent.ACTION_MAIN).`when`(intent).action
+ launchApp(intent)
+ assertEquals(StartupPath.MAIN, provider.startupPathForActivity)
+ }
+
+ @Test
+ fun `GIVEN the app is launched by app link WHEN getting the start up path THEN it is view`() {
+ // There's technically more to a homeScreen Intent but it's fine for now.
+ doReturn(Intent.ACTION_VIEW).`when`(intent).action
+
+ launchApp(intent)
+
+ assertEquals(StartupPath.VIEW, provider.startupPathForActivity)
+ }
+
+ @Test
+ fun `GIVEN the app is launched by a send action WHEN getting the start up path THEN it is unknown`() {
+ doReturn(Intent.ACTION_SEND).`when`(intent).action
+
+ launchApp(intent)
+
+ assertEquals(StartupPath.UNKNOWN, provider.startupPathForActivity)
+ }
+
+ @Test
+ fun `GIVEN the app is launched by a null intent (is this possible) WHEN getting the start up path THEN it is not set`() {
+ callbacks.onCreate(mock())
+ provider.onIntentReceived(null)
+ callbacks.onStart(mock())
+ callbacks.onResume(mock())
+
+ assertEquals(StartupPath.NOT_SET, provider.startupPathForActivity)
+ }
+
+ @Test
+ fun `GIVEN the app is launched to the homeScreen and stopped WHEN getting the start up path THEN it is not set`() {
+ doReturn(Intent.ACTION_MAIN).`when`(intent).action
+
+ launchApp(intent)
+ stopLaunchedApp()
+
+ assertEquals(StartupPath.NOT_SET, provider.startupPathForActivity)
+ }
+
+ @Test
+ fun `GIVEN the app is launched to the homeScreen, stopped, and relaunched warm from app link WHEN getting the start up path THEN it is view`() {
+ doReturn(Intent.ACTION_MAIN).`when`(intent).action
+
+ launchApp(intent)
+ stopLaunchedApp()
+
+ doReturn(Intent.ACTION_VIEW).`when`(intent).action
+
+ startStoppedApp(intent)
+
+ assertEquals(StartupPath.VIEW, provider.startupPathForActivity)
+ }
+
+ @Test
+ fun `GIVEN the app is launched to the homeScreen, stopped, and relaunched warm from the app switcher WHEN getting the start up path THEN it is not set`() {
+ doReturn(Intent.ACTION_MAIN).`when`(intent).action
+
+ launchApp(intent)
+ stopLaunchedApp()
+ startStoppedAppFromAppSwitcher()
+
+ assertEquals(StartupPath.NOT_SET, provider.startupPathForActivity)
+ }
+
+ @Test
+ fun `GIVEN the app is launched to the homeScreen, paused, and resumed WHEN getting the start up path THEN it returns the initial intent value`() {
+ doReturn(Intent.ACTION_MAIN).`when`(intent).action
+
+ launchApp(intent)
+ callbacks.onPause(mock())
+ callbacks.onResume(mock())
+
+ assertEquals(StartupPath.MAIN, provider.startupPathForActivity)
+ }
+
+ @Test
+ fun `GIVEN the app is launched with an intent and receives an intent while the activity is foregrounded WHEN getting the start up path THEN it returns the initial intent value`() {
+ doReturn(Intent.ACTION_MAIN).`when`(intent).action
+
+ launchApp(intent)
+ doReturn(Intent.ACTION_VIEW).`when`(intent).action
+
+ receiveIntentInForeground(intent)
+
+ assertEquals(StartupPath.MAIN, provider.startupPathForActivity)
+ }
+
+ @Test
+ fun `GIVEN the app is launched, stopped, started from the app switcher and receives an intent in the foreground WHEN getting the start up path THEN it returns not set`() {
+ doReturn(Intent.ACTION_MAIN).`when`(intent).action
+
+ launchApp(intent)
+ stopLaunchedApp()
+ startStoppedAppFromAppSwitcher()
+ doReturn(Intent.ACTION_VIEW).`when`(intent).action
+
+ receiveIntentInForeground(intent)
+
+ assertEquals(StartupPath.NOT_SET, provider.startupPathForActivity)
+ }
+
+ private fun launchApp(intent: Intent) {
+ callbacks.onCreate(mock())
+ provider.onIntentReceived(intent)
+ callbacks.onStart(mock())
+ callbacks.onResume(mock())
+ }
+
+ private fun stopLaunchedApp() {
+ callbacks.onPause(mock())
+ callbacks.onStop(mock())
+ }
+
+ private fun startStoppedApp(intent: Intent) {
+ callbacks.onStart(mock())
+ provider.onIntentReceived(intent)
+ callbacks.onResume(mock())
+ }
+
+ private fun startStoppedAppFromAppSwitcher() {
+ // What makes the app switcher case special is it starts the app without an intent.
+ callbacks.onStart(mock())
+ callbacks.onResume(mock())
+ }
+
+ private fun receiveIntentInForeground(intent: Intent) {
+ // To my surprise, the app is paused before receiving an intent on Pixel 2.
+ callbacks.onPause(mock())
+ provider.onIntentReceived(intent)
+ callbacks.onResume(mock())
+ }
+}
diff --git a/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/telemetry/StartupStateProviderTest.kt b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/telemetry/StartupStateProviderTest.kt
new file mode 100644
index 0000000000..db8d452b63
--- /dev/null
+++ b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/telemetry/StartupStateProviderTest.kt
@@ -0,0 +1,417 @@
+/* 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.focus.telemetry
+
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito
+import org.mozilla.focus.activity.IntentReceiverActivity
+import org.mozilla.focus.activity.MainActivity
+import org.mozilla.focus.telemetry.startuptelemetry.AppStartReasonProvider
+import org.mozilla.focus.telemetry.startuptelemetry.AppStartReasonProvider.StartReason
+import org.mozilla.focus.telemetry.startuptelemetry.StartupActivityLog
+import org.mozilla.focus.telemetry.startuptelemetry.StartupActivityLog.LogEntry
+import org.mozilla.focus.telemetry.startuptelemetry.StartupStateProvider
+import org.mozilla.focus.telemetry.startuptelemetry.StartupStateProvider.StartupState
+
+class StartupStateProviderTest {
+
+ private lateinit var provider: StartupStateProvider
+ private var startupActivityLog: StartupActivityLog = mock()
+ private var startReasonProvider: AppStartReasonProvider = mock()
+
+ private lateinit var logEntries: MutableList<LogEntry>
+
+ private val mainActivityClass = MainActivity::class.java
+ private val irActivityClass = IntentReceiverActivity::class.java
+
+ @Before
+ fun setUp() {
+ provider = StartupStateProvider(startupActivityLog, startReasonProvider)
+
+ logEntries = mutableListOf()
+ Mockito.doReturn(logEntries).`when`(startupActivityLog).log
+ Mockito.doReturn(logEntries).`when`(startupActivityLog).log
+ Mockito.doReturn(StartReason.ACTIVITY).`when`(startReasonProvider).reason
+ }
+
+ @Test
+ fun `GIVEN the app started for an activity WHEN is cold start THEN cold start is true`() {
+ forEachColdStartEntries { index ->
+ assertTrue("$index", provider.isColdStartForStartedActivity(mainActivityClass))
+ }
+ }
+
+ @Test
+ fun `GIVEN the app started for an activity WHEN warm start THEN cold start is false`() {
+ forEachWarmStartEntries { index ->
+ assertFalse("$index", provider.isColdStartForStartedActivity(mainActivityClass))
+ }
+ }
+
+ @Test
+ fun `GIVEN the app started for an activity WHEN hot start THEN cold start is false`() {
+ forEachHotStartEntries { index ->
+ assertFalse("$index", provider.isColdStartForStartedActivity(mainActivityClass))
+ }
+ }
+
+ @Test
+ fun `GIVEN the app started for an activity WHEN is cold start THEN warm start is false`() {
+ forEachColdStartEntries { index ->
+ assertFalse("$index", provider.isWarmStartForStartedActivity(mainActivityClass))
+ }
+ }
+
+ @Test
+ fun `GIVEN the app started for an activity WHEN is warm start THEN warm start is true`() {
+ forEachWarmStartEntries { index ->
+ assertTrue("$index", provider.isWarmStartForStartedActivity(mainActivityClass))
+ }
+ }
+
+ @Test
+ fun `GIVEN the app started for an activity WHEN is hot start THEN warm start is false`() {
+ forEachHotStartEntries { index ->
+ assertFalse("$index", provider.isWarmStartForStartedActivity(mainActivityClass))
+ }
+ }
+
+ @Test
+ fun `GIVEN the app started for an activity WHEN is cold start THEN hot start is false`() {
+ forEachColdStartEntries { index ->
+ assertFalse("$index", provider.isHotStartForStartedActivity(mainActivityClass))
+ }
+ }
+
+ @Test
+ fun `GIVEN the app started for an activity WHEN is warm start THEN hot start is false`() {
+ forEachWarmStartEntries { index ->
+ assertFalse("$index", provider.isHotStartForStartedActivity(mainActivityClass))
+ }
+ }
+
+ @Test
+ fun `GIVEN the app started for an activity WHEN is hot start THEN hot start is true`() {
+ forEachHotStartEntries { index ->
+ assertTrue("$index", provider.isHotStartForStartedActivity(mainActivityClass))
+ }
+ }
+
+ @Test
+ fun `GIVEN the app started for an activity WHEN we launched HA through a drawing IntentRA THEN start up is not cold`() {
+ // These entries mimic observed behavior for local code changes.
+ logEntries.addAll(
+ listOf(
+ LogEntry.ActivityCreated(irActivityClass),
+ LogEntry.ActivityStarted(irActivityClass),
+ LogEntry.AppStarted,
+ LogEntry.ActivityCreated(mainActivityClass),
+ LogEntry.ActivityStarted(mainActivityClass),
+ LogEntry.ActivityStopped(irActivityClass),
+ ),
+ )
+ assertFalse(provider.isColdStartForStartedActivity(mainActivityClass))
+ }
+
+ @Test
+ fun `GIVEN the app started for an activity WHEN we launched HA through a drawing IntentRA THEN start up is not warm`() {
+ // These entries mimic observed behavior for local code changes.
+ logEntries.addAll(
+ listOf(
+ LogEntry.AppStopped,
+ LogEntry.ActivityStopped(mainActivityClass),
+ LogEntry.ActivityCreated(irActivityClass),
+ LogEntry.ActivityStarted(irActivityClass),
+ LogEntry.AppStarted,
+ LogEntry.ActivityCreated(mainActivityClass),
+ LogEntry.ActivityStarted(mainActivityClass),
+ LogEntry.ActivityStopped(irActivityClass),
+ ),
+ )
+ assertFalse(provider.isWarmStartForStartedActivity(mainActivityClass))
+ }
+
+ @Test
+ fun `GIVEN the app started for an activity WHEN we launched HA through a drawing IntentRA THEN start up is not hot`() {
+ // These entries mimic observed behavior for local code changes.
+ logEntries.addAll(
+ listOf(
+ LogEntry.AppStopped,
+ LogEntry.ActivityStopped(mainActivityClass),
+ LogEntry.ActivityCreated(irActivityClass),
+ LogEntry.ActivityStarted(irActivityClass),
+ LogEntry.AppStarted,
+ LogEntry.ActivityStarted(mainActivityClass),
+ LogEntry.ActivityStopped(irActivityClass),
+ ),
+ )
+ assertFalse(provider.isHotStartForStartedActivity(mainActivityClass))
+ }
+
+ @Test
+ fun `GIVEN the app started for an activity WHEN two MainActivities are created THEN start up is not cold`() {
+ // We're making an assumption about how this would work based on previous observed patterns.
+ // AIUI, we should never have more than one MainActivity.
+ logEntries.addAll(
+ listOf(
+ LogEntry.ActivityCreated(mainActivityClass),
+ LogEntry.ActivityStarted(mainActivityClass),
+ LogEntry.AppStarted,
+ LogEntry.ActivityCreated(mainActivityClass),
+ LogEntry.ActivityStarted(mainActivityClass),
+ LogEntry.ActivityStopped(mainActivityClass),
+ ),
+ )
+ assertFalse(provider.isColdStartForStartedActivity(mainActivityClass))
+ }
+
+ @Test
+ fun `GIVEN the app started for an activity WHEN an activity hasn't been created yet THEN start up is not cold`() {
+ assertFalse(provider.isColdStartForStartedActivity(mainActivityClass))
+ }
+
+ @Test
+ fun `GIVEN the app started for an activity WHEN an activity hasn't started yet THEN start up is not cold`() {
+ logEntries.addAll(
+ listOf(
+ LogEntry.ActivityCreated(mainActivityClass),
+ ),
+ )
+ assertFalse(provider.isColdStartForStartedActivity(mainActivityClass))
+ }
+
+ @Test
+ fun `GIVEN the app did not start for an activity WHEN is cold is checked THEN it returns false`() {
+ Mockito.doReturn(StartReason.NON_ACTIVITY).`when`(startReasonProvider).reason
+ assertFalse(provider.isColdStartForStartedActivity(mainActivityClass))
+
+ forEachColdStartEntries { index ->
+ assertFalse("$index", provider.isColdStartForStartedActivity(mainActivityClass))
+ }
+ }
+
+ @Test
+ fun `GIVEN the app has not been stopped WHEN an activity has not been created THEN it's not a warm start`() {
+ assertFalse(provider.isWarmStartForStartedActivity(mainActivityClass))
+ }
+
+ @Test
+ fun `GIVEN the app has been stopped WHEN an activity has not been created THEN it's not a warm start`() {
+ logEntries.add(LogEntry.AppStopped)
+ assertFalse(provider.isWarmStartForStartedActivity(mainActivityClass))
+ }
+
+ @Test
+ fun `GIVEN the app has been stopped WHEN an activity has not been started THEN it's not a warm start`() {
+ logEntries.addAll(
+ listOf(
+ LogEntry.AppStopped,
+ LogEntry.ActivityCreated(mainActivityClass),
+ ),
+ )
+ assertFalse(provider.isWarmStartForStartedActivity(mainActivityClass))
+ }
+
+ @Test
+ fun `GIVEN the app has not been stopped WHEN an activity has not been created THEN it's not a hot start`() {
+ assertFalse(provider.isHotStartForStartedActivity(mainActivityClass))
+ }
+
+ @Test
+ fun `GIVEN the app has been stopped WHEN an activity has not been created THEN it's not a hot start`() {
+ logEntries.add(LogEntry.AppStopped)
+ assertFalse(provider.isHotStartForStartedActivity(mainActivityClass))
+ }
+
+ @Test
+ fun `GIVEN the app has been stopped WHEN an activity has not been started THEN it's not a hot start`() {
+ logEntries.addAll(
+ listOf(
+ LogEntry.AppStopped,
+ LogEntry.ActivityCreated(mainActivityClass),
+ ),
+ )
+ assertFalse(provider.isHotStartForStartedActivity(mainActivityClass))
+ }
+
+ @Test
+ fun `GIVEN the app started for an activity WHEN it is a cold start THEN get startup state is cold`() {
+ forEachColdStartEntries { index ->
+ assertEquals("$index", StartupState.COLD, provider.getStartupStateForStartedActivity(mainActivityClass))
+ }
+ }
+
+ @Test
+ fun `WHEN it is a warm start THEN get startup state is warm`() {
+ forEachWarmStartEntries { index ->
+ assertEquals("$index", StartupState.WARM, provider.getStartupStateForStartedActivity(mainActivityClass))
+ }
+ }
+
+ @Test
+ fun `WHEN it is a hot start THEN get startup state is hot`() {
+ forEachHotStartEntries { index ->
+ assertEquals("$index", StartupState.HOT, provider.getStartupStateForStartedActivity(mainActivityClass))
+ }
+ }
+
+ @Test
+ fun `WHEN two activities are started THEN get startup state is unknown`() {
+ logEntries.addAll(
+ listOf(
+ LogEntry.ActivityCreated(irActivityClass),
+ LogEntry.ActivityStarted(irActivityClass),
+ LogEntry.AppStarted,
+ LogEntry.ActivityCreated(mainActivityClass),
+ LogEntry.ActivityStarted(mainActivityClass),
+ LogEntry.ActivityStopped(irActivityClass),
+ ),
+ )
+
+ assertEquals(StartupState.UNKNOWN, provider.getStartupStateForStartedActivity(mainActivityClass))
+ }
+
+ private fun forEachColdStartEntries(block: (index: Int) -> Unit) {
+ // These entries mimic observed behavior.
+ //
+ // MAIN: open HomeActivity directly.
+ val coldStartEntries = listOf(
+ listOf(
+ LogEntry.ActivityCreated(mainActivityClass),
+ LogEntry.ActivityStarted(mainActivityClass),
+ LogEntry.AppStarted,
+
+ // VIEW: open non-drawing IntentReceiverActivity, then HomeActivity.
+ ),
+ listOf(
+ LogEntry.ActivityCreated(irActivityClass),
+ LogEntry.ActivityCreated(mainActivityClass),
+ LogEntry.ActivityStarted(mainActivityClass),
+ LogEntry.AppStarted,
+ ),
+ )
+
+ forEachStartEntry(coldStartEntries, block)
+ }
+
+ private fun forEachWarmStartEntries(block: (index: Int) -> Unit) {
+ // These entries mimic observed behavior. We test both truncated (i.e. the current behavior
+ // with the optimization to prevent an infinite log) and untruncated (the behavior without
+ // such an optimization).
+ //
+ // truncated MAIN: open HomeActivity directly.
+ val warmStartEntries = listOf(
+ listOf(
+ LogEntry.AppStopped,
+ LogEntry.ActivityStopped(mainActivityClass),
+ LogEntry.ActivityCreated(mainActivityClass),
+ LogEntry.ActivityStarted(mainActivityClass),
+ LogEntry.AppStarted,
+
+ // untruncated MAIN: open MainActivity directly.
+ ),
+ listOf(
+ LogEntry.ActivityCreated(mainActivityClass),
+ LogEntry.ActivityStarted(mainActivityClass),
+ LogEntry.AppStarted,
+ LogEntry.AppStopped,
+ LogEntry.ActivityStopped(mainActivityClass),
+ LogEntry.ActivityCreated(mainActivityClass),
+ LogEntry.ActivityStarted(mainActivityClass),
+ LogEntry.AppStarted,
+
+ // truncated VIEW: open non-drawing IntentReceiverActivity, then MainActivity.
+ ),
+ listOf(
+ LogEntry.AppStopped,
+ LogEntry.ActivityStopped(mainActivityClass),
+ LogEntry.ActivityCreated(irActivityClass),
+ LogEntry.ActivityCreated(mainActivityClass),
+ LogEntry.ActivityStarted(mainActivityClass),
+ LogEntry.AppStarted,
+
+ // untruncated VIEW: open non-drawing IntentReceiverActivity, then MainActivity.
+ ),
+ listOf(
+ LogEntry.ActivityCreated(irActivityClass),
+ LogEntry.ActivityCreated(mainActivityClass),
+ LogEntry.ActivityStarted(mainActivityClass),
+ LogEntry.AppStarted,
+ LogEntry.AppStopped,
+ LogEntry.ActivityStopped(mainActivityClass),
+ LogEntry.ActivityCreated(irActivityClass),
+ LogEntry.ActivityCreated(mainActivityClass),
+ LogEntry.ActivityStarted(mainActivityClass),
+ LogEntry.AppStarted,
+ ),
+ )
+
+ forEachStartEntry(warmStartEntries, block)
+ }
+
+ private fun forEachHotStartEntries(block: (index: Int) -> Unit) {
+ // These entries mimic observed behavior. We test both truncated (i.e. the current behavior
+ // with the optimization to prevent an infinite log) and untruncated (the behavior without
+ // such an optimization).
+ //
+ // truncated MAIN: open HomeActivity directly.
+ val hotStartEntries = listOf(
+ listOf(
+ LogEntry.AppStopped,
+ LogEntry.ActivityStopped(mainActivityClass),
+ LogEntry.ActivityStarted(mainActivityClass),
+ LogEntry.AppStarted,
+
+ // untruncated MAIN: open HomeActivity directly.
+ ),
+ listOf(
+ LogEntry.ActivityCreated(mainActivityClass),
+ LogEntry.ActivityStarted(mainActivityClass),
+ LogEntry.AppStarted,
+ LogEntry.AppStopped,
+ LogEntry.ActivityStopped(mainActivityClass),
+ LogEntry.ActivityStarted(mainActivityClass),
+ LogEntry.AppStarted,
+
+ // truncated VIEW: open non-drawing IntentReceiverActivity, then HomeActivity.
+ ),
+ listOf(
+ LogEntry.AppStopped,
+ LogEntry.ActivityStopped(mainActivityClass),
+ LogEntry.ActivityCreated(irActivityClass),
+ LogEntry.ActivityStarted(mainActivityClass),
+ LogEntry.AppStarted,
+
+ // untruncated VIEW: open non-drawing IntentReceiverActivity, then HomeActivity.
+ ),
+ listOf(
+ LogEntry.ActivityCreated(irActivityClass),
+ LogEntry.ActivityCreated(mainActivityClass),
+ LogEntry.ActivityStarted(mainActivityClass),
+ LogEntry.AppStarted,
+ LogEntry.AppStopped,
+ LogEntry.ActivityStopped(mainActivityClass),
+ LogEntry.ActivityCreated(irActivityClass),
+ LogEntry.ActivityStarted(mainActivityClass),
+ LogEntry.AppStarted,
+ ),
+ )
+
+ forEachStartEntry(hotStartEntries, block)
+ }
+
+ private fun forEachStartEntry(entries: List<List<LogEntry>>, block: (index: Int) -> Unit) {
+ entries.forEachIndexed { index, startEntry ->
+ logEntries.clear()
+ logEntries.addAll(startEntry)
+ block(index)
+ }
+ }
+}
diff --git a/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/telemetry/StartupTypeTelemetryTest.kt b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/telemetry/StartupTypeTelemetryTest.kt
new file mode 100644
index 0000000000..2d37068eda
--- /dev/null
+++ b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/telemetry/StartupTypeTelemetryTest.kt
@@ -0,0 +1,152 @@
+/* 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.focus.telemetry
+
+import androidx.lifecycle.Lifecycle
+import kotlinx.coroutines.test.advanceUntilIdle
+import mozilla.components.support.ktx.kotlin.crossProduct
+import mozilla.components.support.test.any
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.components.support.test.rule.runTestOnMain
+import mozilla.telemetry.glean.testing.GleanTestRule
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mozilla.focus.GleanMetrics.PerfStartup
+import org.mozilla.focus.activity.MainActivity
+import org.mozilla.focus.telemetry.startuptelemetry.StartupPathProvider
+import org.mozilla.focus.telemetry.startuptelemetry.StartupPathProvider.StartupPath
+import org.mozilla.focus.telemetry.startuptelemetry.StartupStateProvider
+import org.mozilla.focus.telemetry.startuptelemetry.StartupStateProvider.StartupState
+import org.mozilla.focus.telemetry.startuptelemetry.StartupTypeTelemetry
+import org.mozilla.focus.telemetry.startuptelemetry.StartupTypeTelemetry.StartupTypeLifecycleObserver
+import org.robolectric.RobolectricTestRunner
+
+private val validTelemetryLabels = run {
+ val allStates = listOf("cold", "warm", "hot", "unknown")
+ val allPaths = listOf("main", "view", "unknown")
+
+ allStates.crossProduct(allPaths) { state, path -> "${state}_$path" }.toSet()
+}
+
+private val activityClass = MainActivity::class.java
+
+@RunWith(RobolectricTestRunner::class)
+class StartupTypeTelemetryTest {
+
+ private lateinit var telemetry: StartupTypeTelemetry
+ private lateinit var callbacks: StartupTypeLifecycleObserver
+ private var stateProvider: StartupStateProvider = mock()
+ private var pathProvider: StartupPathProvider = mock()
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ @get:Rule
+ val gleanTestRule = GleanTestRule(testContext)
+
+ @Before
+ fun setUp() {
+ telemetry = spy(StartupTypeTelemetry(stateProvider, pathProvider))
+ callbacks = telemetry.getTestCallbacks()
+ }
+
+ @Test
+ fun `WHEN attach is called THEN it is registered to the lifecycle`() {
+ val lifecycle = mock<Lifecycle>()
+
+ telemetry.attachOnMainActivityOnCreate(lifecycle)
+
+ verify(lifecycle).addObserver(any())
+ }
+
+ @Test
+ fun `GIVEN all possible path and state combinations WHEN record telemetry THEN the labels are incremented the appropriate number of times`() = runTestOnMain {
+ val allPossibleInputArgs = StartupState.values().toList().crossProduct(
+ StartupPath.values().toList(),
+ ) { state, path ->
+ Pair(state, path)
+ }
+
+ allPossibleInputArgs.forEach { (state, path) ->
+ doReturn(state).`when`(stateProvider).getStartupStateForStartedActivity(activityClass)
+ doReturn(path).`when`(pathProvider).startupPathForActivity
+
+ telemetry.record(coroutinesTestRule.testDispatcher)
+ advanceUntilIdle()
+ }
+
+ validTelemetryLabels.forEach { label ->
+ // Path == NOT_SET gets bucketed with Path == UNKNOWN so we'll increment twice for those.
+ val expected = if (label.endsWith("unknown")) 2 else 1
+ assertEquals("label: $label", expected, PerfStartup.startupType[label].testGetValue())
+ }
+
+ // All invalid labels go to a single bucket: let's verify it has no value.
+ assertNull(PerfStartup.startupType["__other__"].testGetValue())
+ }
+
+ @Test
+ fun `WHEN record is called THEN telemetry is recorded with the appropriate label`() = runTestOnMain {
+ doReturn(StartupState.COLD).`when`(stateProvider).getStartupStateForStartedActivity(activityClass)
+ doReturn(StartupPath.MAIN).`when`(pathProvider).startupPathForActivity
+
+ telemetry.record(coroutinesTestRule.testDispatcher)
+ advanceUntilIdle()
+
+ assertEquals(1, PerfStartup.startupType["cold_main"].testGetValue())
+ }
+
+ @Test
+ fun `GIVEN the activity is launched WHEN onResume is called THEN we record the telemetry`() {
+ launchApp()
+ verify(telemetry).record(any())
+ }
+
+ @Test
+ fun `GIVEN the activity is launched WHEN the activity is paused and resumed THEN record is not called`() {
+ // This part of the test duplicates another test but it's needed to initialize the state of this test.
+ launchApp()
+ verify(telemetry).record(any())
+
+ callbacks.onPause(mock())
+ callbacks.onResume(mock())
+
+ verify(telemetry).record(any()) // i.e. this shouldn't be called again.
+ }
+
+ @Test
+ fun `GIVEN the activity is launched WHEN the activity is stopped and resumed THEN record is called again`() {
+ // This part of the test duplicates another test but it's needed to initialize the state of this test.
+ launchApp()
+ verify(telemetry).record(any())
+
+ callbacks.onPause(mock())
+ callbacks.onStop(mock())
+ callbacks.onStart(mock())
+ callbacks.onResume(mock())
+
+ verify(telemetry, times(2)).record(any())
+ }
+
+ private fun launchApp() {
+ // What these return isn't important.
+ doReturn(StartupState.COLD).`when`(stateProvider).getStartupStateForStartedActivity(activityClass)
+ doReturn(StartupPath.MAIN).`when`(pathProvider).startupPathForActivity
+
+ callbacks.onCreate(mock())
+ callbacks.onStart(mock())
+ callbacks.onResume(mock())
+ }
+}
diff --git a/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/topsites/DefaultTopSitesStorageTest.kt b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/topsites/DefaultTopSitesStorageTest.kt
new file mode 100644
index 0000000000..27ff6a5378
--- /dev/null
+++ b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/topsites/DefaultTopSitesStorageTest.kt
@@ -0,0 +1,136 @@
+/* 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.focus.topsites
+
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import mozilla.components.feature.top.sites.PinnedSiteStorage
+import mozilla.components.feature.top.sites.TopSite
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+@ExperimentalCoroutinesApi
+class DefaultTopSitesStorageTest {
+
+ private val pinnedSitesStorage: PinnedSiteStorage = mock()
+
+ @Test
+ fun `WHEN a top site is added THEN the pinned sites storage is called`() = runTest(UnconfinedTestDispatcher()) {
+ val defaultTopSitesStorage = DefaultTopSitesStorage(
+ pinnedSitesStorage,
+ coroutineContext,
+ )
+
+ defaultTopSitesStorage.addTopSite("Mozilla", "https://mozilla.com", isDefault = false)
+
+ verify(pinnedSitesStorage).addPinnedSite(
+ "Mozilla",
+ "https://mozilla.com",
+ isDefault = false,
+ )
+ }
+
+ @Test
+ fun `WHEN a top site is removed THEN the pinned sites storage is called`() = runTest(UnconfinedTestDispatcher()) {
+ val defaultTopSitesStorage = DefaultTopSitesStorage(
+ pinnedSitesStorage,
+ coroutineContext,
+ )
+
+ val pinnedSite = TopSite.Pinned(
+ id = 2,
+ title = "Firefox",
+ url = "https://firefox.com",
+ createdAt = 2,
+ )
+
+ defaultTopSitesStorage.removeTopSite(pinnedSite)
+
+ verify(pinnedSitesStorage).removePinnedSite(pinnedSite)
+ }
+
+ @Test
+ fun `WHEN a top site is updated THEN the pinned sites storage is called`() = runTest(UnconfinedTestDispatcher()) {
+ val defaultTopSitesStorage = DefaultTopSitesStorage(
+ pinnedSitesStorage,
+ coroutineContext,
+ )
+
+ val pinnedSite = TopSite.Pinned(
+ id = 2,
+ title = "Wikipedia",
+ url = "https://wikipedia.com",
+ createdAt = 2,
+ )
+ defaultTopSitesStorage.updateTopSite(
+ pinnedSite,
+ "Wiki",
+ "https://en.wikipedia.org/wiki/Wiki",
+ )
+
+ verify(pinnedSitesStorage).updatePinnedSite(
+ pinnedSite,
+ "Wiki",
+ "https://en.wikipedia.org/wiki/Wiki",
+ )
+ }
+
+ @Test
+ fun `WHEN getTopSites is called THEN the appropriate top sites are returned`() = runTest {
+ val defaultTopSitesStorage = DefaultTopSitesStorage(
+ pinnedSitesStorage,
+ coroutineContext,
+ )
+
+ val pinnedSite1 = TopSite.Pinned(
+ id = 2,
+ title = "Wikipedia",
+ url = "https://wikipedia.com",
+ createdAt = 2,
+ )
+ val pinnedSite2 = TopSite.Pinned(
+ id = 3,
+ title = "Example",
+ url = "https://example.com",
+ createdAt = 3,
+ )
+
+ whenever(pinnedSitesStorage.getPinnedSites()).thenReturn(
+ listOf(
+ pinnedSite1,
+ pinnedSite2,
+ ),
+ )
+
+ var topSites = defaultTopSitesStorage.getTopSites(
+ totalSites = 0,
+ frecencyConfig = null,
+ )
+
+ assertTrue(topSites.isEmpty())
+
+ topSites = defaultTopSitesStorage.getTopSites(totalSites = 1)
+
+ assertEquals(1, topSites.size)
+ assertEquals(pinnedSite1, topSites[0])
+
+ topSites = defaultTopSitesStorage.getTopSites(totalSites = 2)
+
+ assertEquals(2, topSites.size)
+ assertEquals(pinnedSite1, topSites[0])
+ assertEquals(pinnedSite2, topSites[1])
+
+ topSites = defaultTopSitesStorage.getTopSites(totalSites = 5)
+
+ assertEquals(2, topSites.size)
+ assertEquals(pinnedSite1, topSites[0])
+ assertEquals(pinnedSite2, topSites[1])
+ }
+}
diff --git a/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/utils/IntentUtilsTest.kt b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/utils/IntentUtilsTest.kt
new file mode 100644
index 0000000000..dcbfd876a4
--- /dev/null
+++ b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/utils/IntentUtilsTest.kt
@@ -0,0 +1,21 @@
+/* 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.focus.utils
+
+import android.app.PendingIntent
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class IntentUtilsTest {
+
+ @Test
+ fun `given a build version lower than 23, when defaultIntentPendingFlags is called, then flag 0 should be returned`() {
+ assertEquals(0, IntentUtils.defaultIntentPendingFlags(22))
+ }
+
+ @Test
+ fun `given a build version bigger than 22, when defaultIntentPendingFlags is called, then flag FLAG_IMMUTABLE should be returned`() {
+ assertEquals(PendingIntent.FLAG_IMMUTABLE, IntentUtils.defaultIntentPendingFlags(23))
+ }
+}
diff --git a/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/utils/SupportUtilsTest.kt b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/utils/SupportUtilsTest.kt
new file mode 100644
index 0000000000..fe55d8d4f8
--- /dev/null
+++ b/mobile/android/focus-android/app/src/test/java/org/mozilla/focus/utils/SupportUtilsTest.kt
@@ -0,0 +1,64 @@
+/* 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.focus.utils
+
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import java.util.Locale
+
+class SupportUtilsTest {
+
+ @Test
+ fun cleanup() {
+ // Other tests might get confused by our locale fiddling, so lets go back to the default:
+ Locale.setDefault(Locale.ENGLISH)
+ }
+
+ /*
+ * Super simple sumo URL test - it exists primarily to verify that we're setting the language
+ * and page tags correctly. appVersion is null in tests, so we just test that there's a null there,
+ * which doesn't seem too useful...
+ */
+ @Test
+ @Throws(Exception::class)
+ fun getSumoURLForTopic() {
+ val versionName = "testVersion"
+
+ val testTopic = SupportUtils.SumoTopic.TRACKERS
+ val testTopicStr = testTopic.topicStr
+
+ Locale.setDefault(Locale.GERMANY)
+ assertEquals(
+ "https://support.mozilla.org/1/mobile/$versionName/Android/de-DE/$testTopicStr",
+ SupportUtils.getSumoURLForTopic(versionName, testTopic),
+ )
+
+ Locale.setDefault(Locale.CANADA_FRENCH)
+ assertEquals(
+ "https://support.mozilla.org/1/mobile/$versionName/Android/fr-CA/$testTopicStr",
+ SupportUtils.getSumoURLForTopic(versionName, testTopic),
+ )
+ }
+
+ /**
+ * This is a pretty boring tests - it exists primarily to verify that we're actually setting
+ * a langtag in the manfiesto URL.
+ */
+ @Test
+ @Throws(Exception::class)
+ fun getManifestoURL() {
+ Locale.setDefault(Locale.UK)
+ assertEquals(
+ "https://www.mozilla.org/en-GB/about/manifesto/",
+ SupportUtils.manifestoURL,
+ )
+
+ Locale.setDefault(Locale.KOREA)
+ assertEquals(
+ "https://www.mozilla.org/ko-KR/about/manifesto/",
+ SupportUtils.manifestoURL,
+ )
+ }
+}
diff --git a/mobile/android/focus-android/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mobile/android/focus-android/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..cf1c399ea8
--- /dev/null
+++ b/mobile/android/focus-android/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1,2 @@
+mock-maker-inline
+// This allows mocking final classes (classes are final by default in Kotlin)
diff --git a/mobile/android/focus-android/app/src/test/resources/robolectric.properties b/mobile/android/focus-android/app/src/test/resources/robolectric.properties
new file mode 100644
index 0000000000..4359826c57
--- /dev/null
+++ b/mobile/android/focus-android/app/src/test/resources/robolectric.properties
@@ -0,0 +1,3 @@
+# Needed until Robolectric supports SDK 29+
+sdk=28
+application=org.mozilla.focus.EmptyFocusApplication