diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarCFRPresenter.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarCFRPresenter.kt
new file mode 100644
index 0000000000..c62c5c8cae
--- /dev/null
+++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarCFRPresenter.kt
@@ -0,0 +1,129 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.components.toolbar
+
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+import androidx.compose.foundation.clickable
+import androidx.compose.material.Text
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.mapNotNull
+import kotlinx.coroutines.flow.transformWhile
+import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.browser.toolbar.BrowserToolbar
+import mozilla.components.lib.state.ext.flowScoped
+import org.mozilla.fenix.R
+import org.mozilla.fenix.compose.cfr.CFRPopup
+import org.mozilla.fenix.compose.cfr.CFRPopup.PopupAlignment.INDICATOR_CENTERED_IN_ANCHOR
+import org.mozilla.fenix.compose.cfr.CFRPopupProperties
+import org.mozilla.fenix.ext.components
+import org.mozilla.fenix.settings.SupportUtils
+import org.mozilla.fenix.settings.SupportUtils.SumoTopic.TOTAL_COOKIE_PROTECTION
+import org.mozilla.fenix.theme.FirefoxTheme
+import org.mozilla.fenix.utils.Settings
+
+/**
+ * Vertical padding needed to improve the visual alignment of the popup and respect the UX design.
+ */
+private const val CFR_TO_ANCHOR_VERTICAL_PADDING = -6
+
+/**
+ * Delegate for handling all the business logic for showing CFRs in the toolbar.
+ *
+ * @param context used for various Android interactions.
+ * @param browserStore will be observed for tabs updates
+ * @param settings used to read and write persistent user settings
+ * @param toolbar will serve as anchor for the CFRs
+ * @param sessionId optional custom tab id used to identify the custom tab in which to show a CFR.
+ */
+class BrowserToolbarCFRPresenter(
+ private val context: Context,
+ private val browserStore: BrowserStore,
+ private val settings: Settings,
+ private val toolbar: BrowserToolbar,
+ private val sessionId: String? = null
+) {
+ @VisibleForTesting
+ internal var tcpCfrScope: CoroutineScope? = null
+ @VisibleForTesting
+ internal var tcpCfrPopup: CFRPopup? = null
+
+ /**
+ * Start observing [browserStore] for updates which may trigger showing a CFR.
+ */
+ @Suppress("MagicNumber")
+ fun start() {
+ if (settings.shouldShowTotalCookieProtectionCFR) {
+ tcpCfrScope = browserStore.flowScoped { flow ->
+ flow
+ .mapNotNull { it.findCustomTabOrSelectedTab(sessionId)?.content?.progress }
+ // The "transformWhile" below ensures that the 100% progress is only collected once.
+ .transformWhile { progress ->
+ emit(progress)
+ progress != 100
+ }
+ .filter { it == 100 }
+ .collect {
+ tcpCfrScope?.cancel()
+ showTcpCfr()
+ }
+ }
+ }
+ }
+
+ /**
+ * Stop listening for [browserStore] updates.
+ * CFRs already shown are not automatically dismissed.
+ */
+ fun stop() {
+ tcpCfrScope?.cancel()
+ }
+
+ @VisibleForTesting
+ internal fun showTcpCfr() {
+ CFRPopup(
+ text = context.getString(R.string.tcp_cfr_message),
+ anchor = toolbar.findViewById(
+ R.id.mozac_browser_toolbar_security_indicator
+ ),
+ properties = CFRPopupProperties(
+ popupAlignment = INDICATOR_CENTERED_IN_ANCHOR,
+ indicatorDirection = if (settings.toolbarPosition == ToolbarPosition.TOP) {
+ CFRPopup.IndicatorDirection.UP
+ } else {
+ CFRPopup.IndicatorDirection.DOWN
+ },
+ popupVerticalOffset = CFR_TO_ANCHOR_VERTICAL_PADDING.dp
+ ),
+ ) {
+ Text(
+ text = context.getString(R.string.tcp_cfr_learn_more),
+ color = FirefoxTheme.colors.textOnColorPrimary,
+ modifier = Modifier.clickable {
+ context.components.useCases.tabsUseCases.selectOrAddTab.invoke(
+ SupportUtils.getSumoURLForTopic(
+ context,
+ TOTAL_COOKIE_PROTECTION
+ )
+ )
+ tcpCfrPopup?.dismiss()
+ },
+ style = FirefoxTheme.typography.body2.copy(
+ textDecoration = TextDecoration.Underline
+ )
+ )
+ }.run {
+ settings.shouldShowTotalCookieProtectionCFR = false
+ tcpCfrPopup = this
+ show()
+ }
+ }
+}
diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarIntegration.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarIntegration.kt
index 5960b573e9..4eb077f436 100644
--- a/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarIntegration.kt
+++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarIntegration.kt
@@ -5,6 +5,7 @@
package org.mozilla.fenix.components.toolbar
import android.content.Context
+import androidx.annotation.VisibleForTesting
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
import mozilla.components.browser.domains.autocomplete.DomainAutocompleteProvider
@@ -95,6 +96,15 @@ class DefaultToolbarIntegration(
renderStyle = ToolbarFeature.RenderStyle.UncoloredUrl
) {
+ @VisibleForTesting
+ internal var cfrPresenter = BrowserToolbarCFRPresenter(
+ context = context,
+ browserStore = context.components.core.store,
+ settings = context.settings(),
+ toolbar = toolbar,
+ sessionId = sessionId
+ )
+
init {
toolbar.display.menuBuilder = toolbarMenu.menuBuilder
toolbar.private = isPrivate
@@ -150,4 +160,14 @@ class DefaultToolbarIntegration(
}
}
}
+
+ override fun start() {
+ super.start()
+ cfrPresenter.start()
+ }
+
+ override fun stop() {
+ cfrPresenter.stop()
+ super.stop()
+ }
}
diff --git a/app/src/main/java/org/mozilla/fenix/compose/cfr/CFRPopup.kt b/app/src/main/java/org/mozilla/fenix/compose/cfr/CFRPopup.kt
index 84aeaa263d..2504fdebf3 100644
--- a/app/src/main/java/org/mozilla/fenix/compose/cfr/CFRPopup.kt
+++ b/app/src/main/java/org/mozilla/fenix/compose/cfr/CFRPopup.kt
@@ -54,11 +54,11 @@ data class CFRPopupProperties(
* @param action Optional other composable to show just below the popup text.
*/
class CFRPopup(
- private val text: String,
- private val anchor: View,
- private val properties: CFRPopupProperties = CFRPopupProperties(),
- private val onDismiss: (Boolean) -> Unit = {},
- private val action: @Composable (() -> Unit) = {}
+ @get:VisibleForTesting internal val text: String,
+ @get:VisibleForTesting internal val anchor: View,
+ @get:VisibleForTesting internal val properties: CFRPopupProperties = CFRPopupProperties(),
+ @get:VisibleForTesting internal val onDismiss: (Boolean) -> Unit = {},
+ @get:VisibleForTesting internal val action: @Composable (() -> Unit) = {}
) {
// This is just a facade for the CFRPopupFullScreenLayout composable offering a cleaner API.
diff --git a/app/src/main/java/org/mozilla/fenix/settings/SupportUtils.kt b/app/src/main/java/org/mozilla/fenix/settings/SupportUtils.kt
index 59e4636385..c8bc515fac 100644
--- a/app/src/main/java/org/mozilla/fenix/settings/SupportUtils.kt
+++ b/app/src/main/java/org/mozilla/fenix/settings/SupportUtils.kt
@@ -42,6 +42,7 @@ object SupportUtils {
PRIVATE_BROWSING_MYTHS("common-myths-about-private-browsing"),
YOUR_RIGHTS("your-rights"),
TRACKING_PROTECTION("tracking-protection-firefox-android"),
+ TOTAL_COOKIE_PROTECTION("enhanced-tracking-protection-android"),
WHATS_NEW("whats-new-firefox-preview"),
OPT_OUT_STUDIES("how-opt-out-studies-firefox-android"),
SEND_TABS("send-tab-preview"),
diff --git a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt
index bb8e9b4ddc..f5e57c7067 100644
--- a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt
+++ b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt
@@ -574,6 +574,14 @@ class Settings(private val appContext: Context) : PreferencesHolder {
val enabledTotalCookieProtectionSetting: Boolean
get() = mr2022Sections[Mr2022Section.TCP_CFR] == true
+ /**
+ * Indicates if the total cookie protection CRF should be shown.
+ */
+ var shouldShowTotalCookieProtectionCFR by booleanPreference(
+ appContext.getPreferenceKey(R.string.pref_key_should_show_total_cookie_protection_popup),
+ default = FxNimbus.features.engineSettings.value().totalCookieProtectionEnabled
+ )
+
val blockCookiesSelectionInCustomTrackingProtection by stringPreference(
key = appContext.getPreferenceKey(R.string.pref_key_tracking_protection_custom_cookies_select),
default = if (enabledTotalCookieProtectionSetting) {
diff --git a/app/src/main/res/values/preference_keys.xml b/app/src/main/res/values/preference_keys.xml
index 6a21c12fa5..5e3b7a1dd4 100644
--- a/app/src/main/res/values/preference_keys.xml
+++ b/app/src/main/res/values/preference_keys.xml
@@ -241,6 +241,8 @@
pref_key_should_show_home_onboarding_dialog
pref_key_show_first_run_onboarding_update
+
+ pref_key_should_show_total_cookie_protection_popup
pref_key_debug_settings
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index d4f8cfb72b..6152a1cb92 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -76,6 +76,12 @@
Dismiss
+
+
+ Our most powerful privacy feature yet isolates cross-site trackers.
+
+ Learn about Total Cookie Protection
+
Camera access needed. Go to Android settings, tap permissions, and tap allow.
diff --git a/app/src/test/java/org/mozilla/fenix/components/toolbar/BrowserToolbarCFRPresenterTest.kt b/app/src/test/java/org/mozilla/fenix/components/toolbar/BrowserToolbarCFRPresenterTest.kt
new file mode 100644
index 0000000000..5787b1e48c
--- /dev/null
+++ b/app/src/test/java/org/mozilla/fenix/components/toolbar/BrowserToolbarCFRPresenterTest.kt
@@ -0,0 +1,243 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.components.toolbar
+
+import android.content.Context
+import android.view.View
+import androidx.compose.ui.unit.dp
+import io.mockk.Runs
+import io.mockk.every
+import io.mockk.just
+import io.mockk.mockk
+import io.mockk.spyk
+import io.mockk.verify
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.cancel
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.CustomTabSessionState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.state.createCustomTab
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.browser.toolbar.BrowserToolbar
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.rule.MainCoroutineRule
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.mozilla.fenix.R
+import org.mozilla.fenix.compose.cfr.CFRPopup
+import org.mozilla.fenix.utils.Settings
+
+class BrowserToolbarCFRPresenterTest {
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ @Test
+ fun `GIVEN the TCP CFR should be shown for a custom tab WHEN the custom tab is fully loaded THEN the TCP CFR is shown`() {
+ val customTab = createCustomTab(url = "")
+ val browserStore = createBrowserStore(customTab = customTab)
+ val presenter = createPresenterThatShowsCFRs(
+ browserStore = browserStore,
+ sessionId = customTab.id,
+ )
+
+ presenter.start()
+
+ assertNotNull(presenter.tcpCfrScope)
+
+ browserStore.dispatch(ContentAction.UpdateProgressAction(customTab.id, 0)).joinBlocking()
+ verify(exactly = 0) { presenter.showTcpCfr() }
+
+ browserStore.dispatch(ContentAction.UpdateProgressAction(customTab.id, 33)).joinBlocking()
+ verify(exactly = 0) { presenter.showTcpCfr() }
+
+ browserStore.dispatch(ContentAction.UpdateProgressAction(customTab.id, 100)).joinBlocking()
+ verify { presenter.showTcpCfr() }
+ }
+
+ @Test
+ fun `GIVEN the TCP CFR should be shown WHEN the current normal tab is fully loaded THEN the TCP CFR is shown`() {
+ val normalTab = createTab(url = "", private = false)
+ val browserStore = createBrowserStore(
+ tab = normalTab,
+ selectedTabId = normalTab.id,
+ )
+ val presenter = createPresenterThatShowsCFRs(browserStore = browserStore)
+
+ presenter.start()
+
+ assertNotNull(presenter.tcpCfrScope)
+
+ browserStore.dispatch(ContentAction.UpdateProgressAction(normalTab.id, 1)).joinBlocking()
+ verify(exactly = 0) { presenter.showTcpCfr() }
+
+ browserStore.dispatch(ContentAction.UpdateProgressAction(normalTab.id, 98)).joinBlocking()
+ verify(exactly = 0) { presenter.showTcpCfr() }
+
+ browserStore.dispatch(ContentAction.UpdateProgressAction(normalTab.id, 100)).joinBlocking()
+ verify { presenter.showTcpCfr() }
+ }
+
+ @Test
+ fun `GIVEN the TCP CFR should be shown WHEN the current private tab is fully loaded THEN the TCP CFR is shown`() {
+ val privateTab = createTab(url = "", private = true)
+ val browserStore = createBrowserStore(
+ tab = privateTab,
+ selectedTabId = privateTab.id,
+ )
+ val presenter = createPresenterThatShowsCFRs(browserStore = browserStore)
+
+ presenter.start()
+
+ assertNotNull(presenter.tcpCfrScope)
+
+ browserStore.dispatch(ContentAction.UpdateProgressAction(privateTab.id, 14)).joinBlocking()
+ verify(exactly = 0) { presenter.showTcpCfr() }
+
+ browserStore.dispatch(ContentAction.UpdateProgressAction(privateTab.id, 99)).joinBlocking()
+ verify(exactly = 0) { presenter.showTcpCfr() }
+
+ browserStore.dispatch(ContentAction.UpdateProgressAction(privateTab.id, 100)).joinBlocking()
+ verify { presenter.showTcpCfr() }
+ }
+
+ @Test
+ fun `GIVEN the TCP CFR should be shown WHEN the current tab is fully loaded THEN the TCP CFR is only shown once`() {
+ val tab = createTab(url = "")
+ val browserStore = createBrowserStore(
+ tab = tab,
+ selectedTabId = tab.id,
+ )
+ val presenter = createPresenterThatShowsCFRs(browserStore = browserStore)
+
+ presenter.start()
+
+ assertNotNull(presenter.tcpCfrScope)
+
+ browserStore.dispatch(ContentAction.UpdateProgressAction(tab.id, 99)).joinBlocking()
+ browserStore.dispatch(ContentAction.UpdateProgressAction(tab.id, 100)).joinBlocking()
+ browserStore.dispatch(ContentAction.UpdateProgressAction(tab.id, 100)).joinBlocking()
+ browserStore.dispatch(ContentAction.UpdateProgressAction(tab.id, 100)).joinBlocking()
+ verify(exactly = 1) { presenter.showTcpCfr() }
+ }
+
+ @Test
+ fun `GIVEN the TCP CFR should not be shown WHEN the feature starts THEN don't observe the store for updates`() {
+ val presenter = createPresenter(
+ settings = mockk {
+ every { shouldShowTotalCookieProtectionCFR } returns false
+ },
+ )
+
+ presenter.start()
+
+ assertNull(presenter.tcpCfrScope)
+ }
+
+ @Test
+ fun `GIVEN the store is observed for updates WHEN the presenter is stopped THEN stop observing the store`() {
+ val tcpScope: CoroutineScope = mockk {
+ every { cancel() } just Runs
+ }
+ val presenter = createPresenter()
+ presenter.tcpCfrScope = tcpScope
+
+ presenter.stop()
+
+ verify { tcpScope.cancel() }
+ }
+
+ @Test
+ fun `WHEN the TCP CFR is to be shown THEN instantiate a new one and remember to not show it again`() {
+ val settings: Settings = mockk(relaxed = true)
+ val presenter = createPresenter(
+ anchor = mockk(relaxed = true),
+ settings = settings,
+ )
+
+ presenter.showTcpCfr()
+
+ verify { settings.shouldShowTotalCookieProtectionCFR = false }
+ assertNotNull(presenter.tcpCfrPopup)
+ }
+
+ @Test
+ fun `WHEN the TCP CFR is instantiated THEN set the intended properties`() {
+ val anchor: View = mockk(relaxed = true)
+ val settings: Settings = mockk(relaxed = true)
+ val presenter = createPresenter(
+ anchor = anchor,
+ settings = settings,
+ )
+
+ presenter.showTcpCfr()
+
+ verify { settings.shouldShowTotalCookieProtectionCFR = false }
+ assertNotNull(presenter.tcpCfrPopup)
+ presenter.tcpCfrPopup?.let {
+ assertEquals("Test", it.text)
+ assertEquals(anchor, it.anchor)
+ assertEquals(CFRPopup.DEFAULT_WIDTH.dp, it.properties.popupWidth)
+ assertEquals(CFRPopup.IndicatorDirection.DOWN, it.properties.indicatorDirection)
+ assertTrue(it.properties.dismissOnBackPress)
+ assertTrue(it.properties.dismissOnClickOutside)
+ assertFalse(it.properties.overlapAnchor)
+ assertEquals(CFRPopup.DEFAULT_INDICATOR_START_OFFSET.dp, it.properties.indicatorArrowStartOffset)
+ }
+ }
+
+ /**
+ * Creates and return a [spyk] of a [BrowserToolbarCFRPresenter] that can handle actually showing CFRs.
+ */
+ private fun createPresenterThatShowsCFRs(
+ context: Context = mockk(),
+ anchor: View = mockk(),
+ browserStore: BrowserStore = mockk(),
+ settings: Settings = mockk { every { shouldShowTotalCookieProtectionCFR } returns true },
+ toolbar: BrowserToolbar = mockk(),
+ sessionId: String? = null
+ ) = spyk(createPresenter(context, anchor, browserStore, settings, toolbar, sessionId)) {
+ every { showTcpCfr() } just Runs
+ }
+
+ /**
+ * Create and return a [BrowserToolbarCFRPresenter] with all constructor properties mocked by default.
+ * Calls to show a CFR will fail. If this behavior is needed to work use [createPresenterThatShowsCFRs].
+ */
+ private fun createPresenter(
+ context: Context = mockk { every { getString(R.string.tcp_cfr_message) } returns "Test" },
+ anchor: View = mockk(),
+ browserStore: BrowserStore = mockk(),
+ settings: Settings = mockk(relaxed = true) { every { shouldShowTotalCookieProtectionCFR } returns true },
+ toolbar: BrowserToolbar = mockk {
+ every { findViewById(R.id.mozac_browser_toolbar_security_indicator) } returns anchor
+ },
+ sessionId: String? = null
+ ) = BrowserToolbarCFRPresenter(
+ context = context,
+ browserStore = browserStore,
+ settings = settings,
+ toolbar = toolbar,
+ sessionId = sessionId
+ )
+
+ private fun createBrowserStore(
+ tab: TabSessionState? = null,
+ customTab: CustomTabSessionState? = null,
+ selectedTabId: String? = null
+ ) = BrowserStore(
+ initialState = BrowserState(
+ tabs = if (tab != null) listOf(tab) else listOf(),
+ customTabs = if (customTab != null) listOf(customTab) else listOf(),
+ selectedTabId = selectedTabId
+ )
+ )
+}
diff --git a/app/src/test/java/org/mozilla/fenix/components/toolbar/DefaultToolbarIntegrationTest.kt b/app/src/test/java/org/mozilla/fenix/components/toolbar/DefaultToolbarIntegrationTest.kt
new file mode 100644
index 0000000000..279903037a
--- /dev/null
+++ b/app/src/test/java/org/mozilla/fenix/components/toolbar/DefaultToolbarIntegrationTest.kt
@@ -0,0 +1,73 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.components.toolbar
+
+import android.content.Context
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.mockkStatic
+import io.mockk.unmockkStatic
+import io.mockk.verify
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mozilla.fenix.ext.components
+import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
+
+@RunWith(FenixRobolectricTestRunner::class)
+class DefaultToolbarIntegrationTest {
+ private lateinit var feature: DefaultToolbarIntegration
+
+ @Before
+ fun setup() {
+ mockkStatic("org.mozilla.fenix.ext.ContextKt")
+ every { any().components } returns mockk {
+ every { core } returns mockk {
+ every { store } returns BrowserStore()
+ }
+ every { publicSuffixList } returns mockk()
+ every { settings } returns mockk(relaxed = true)
+ }
+
+ feature = DefaultToolbarIntegration(
+ context = testContext,
+ toolbar = mockk(relaxed = true),
+ toolbarMenu = mockk(relaxed = true),
+ domainAutocompleteProvider = mockk(),
+ historyStorage = mockk(),
+ lifecycleOwner = mockk(),
+ sessionId = null,
+ isPrivate = false,
+ interactor = mockk(),
+ engine = mockk(),
+ )
+ }
+
+ @After
+ fun teardown() {
+ unmockkStatic("org.mozilla.fenix.ext.ContextKt")
+ }
+
+ @Test
+ fun `WHEN the feature starts THEN start the cfr presenter`() {
+ feature.cfrPresenter = mockk(relaxed = true)
+
+ feature.start()
+
+ verify { feature.cfrPresenter.start() }
+ }
+
+ @Test
+ fun `WHEN the feature stops THEN stop the cfr presenter`() {
+ feature.cfrPresenter = mockk(relaxed = true)
+
+ feature.stop()
+
+ verify { feature.cfrPresenter.stop() }
+ }
+}