diff --git a/app/src/main/java/org/mozilla/fenix/components/Components.kt b/app/src/main/java/org/mozilla/fenix/components/Components.kt index b93e78517..719adfd39 100644 --- a/app/src/main/java/org/mozilla/fenix/components/Components.kt +++ b/app/src/main/java/org/mozilla/fenix/components/Components.kt @@ -4,9 +4,9 @@ package org.mozilla.fenix.components -import android.app.Application import android.content.Context import android.content.Intent +import androidx.core.content.getSystemService import androidx.core.net.toUri import mozilla.components.feature.addons.AddonManager import mozilla.components.feature.addons.amo.AddonCollectionProvider @@ -106,5 +106,5 @@ class Components(private val context: Context) { val migrationStore by lazy { MigrationStore() } val performance by lazy { PerformanceComponent() } val push by lazy { Push(context, analytics.crashReporter) } - val wifiConnectionMonitor by lazy { WifiConnectionMonitor(context as Application) } + val wifiConnectionMonitor by lazy { WifiConnectionMonitor(context.getSystemService()!!) } } diff --git a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsIntegration.kt b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsIntegration.kt index 04459010c..b9a7068d6 100644 --- a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsIntegration.kt +++ b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsIntegration.kt @@ -12,6 +12,11 @@ import mozilla.components.concept.sync.OAuthAccount import mozilla.components.service.fxa.manager.FxaAccountManager import org.mozilla.fenix.ext.components +/** + * Starts and stops SyncedTabsStorage based on the authentication state. + * @param context Used to get synced tabs storage, due to cyclic dependency. + * @param accountManager Used to check and observe account authentication state. + */ class SyncedTabsIntegration( private val context: Context, private val accountManager: FxaAccountManager diff --git a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsViewHolder.kt b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsViewHolder.kt index 240a83e8c..bcd494cf0 100644 --- a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsViewHolder.kt @@ -47,8 +47,8 @@ sealed class SyncedTabsViewHolder(itemView: View) : RecyclerView.ViewHolder(item private fun bindHeader(device: AdapterItem.Device) { val deviceLogoDrawable = when (device.device.deviceType) { - DeviceType.DESKTOP -> { R.drawable.mozac_ic_device_desktop } - else -> { R.drawable.mozac_ic_device_mobile } + DeviceType.DESKTOP -> R.drawable.mozac_ic_device_desktop + else -> R.drawable.mozac_ic_device_mobile } itemView.synced_tabs_group_name.text = device.device.displayName diff --git a/app/src/main/java/org/mozilla/fenix/wifi/SitePermissionsWifiIntegration.kt b/app/src/main/java/org/mozilla/fenix/wifi/SitePermissionsWifiIntegration.kt index eec7d25e6..cee0a6a03 100644 --- a/app/src/main/java/org/mozilla/fenix/wifi/SitePermissionsWifiIntegration.kt +++ b/app/src/main/java/org/mozilla/fenix/wifi/SitePermissionsWifiIntegration.kt @@ -18,29 +18,21 @@ import org.mozilla.fenix.utils.Settings class SitePermissionsWifiIntegration( private val settings: Settings, private val wifiConnectionMonitor: WifiConnectionMonitor -) : LifecycleAwareFeature { +) : LifecycleAwareFeature, WifiConnectionMonitor.Observer { /** * Adds listener for autoplay setting [AUTOPLAY_ALLOW_ON_WIFI]. Sets all autoplay to allowed when * WIFI is connected, blocked otherwise. */ - private val wifiConnectedListener: ((Boolean) -> Unit) by lazy { - { connected: Boolean -> - val setting = - if (connected) SitePermissionsRules.Action.ALLOWED else SitePermissionsRules.Action.BLOCKED - if (settings.getAutoplayUserSetting(default = AUTOPLAY_BLOCK_ALL) == AUTOPLAY_ALLOW_ON_WIFI) { - settings.setSitePermissionsPhoneFeatureAction( - PhoneFeature.AUTOPLAY_AUDIBLE, - setting - ) - settings.setSitePermissionsPhoneFeatureAction( - PhoneFeature.AUTOPLAY_INAUDIBLE, - setting - ) - } else { - // The autoplay setting has changed, we can remove the listener - removeWifiConnectedListener() - } + override fun onWifiConnectionChanged(connected: Boolean) { + val setting = + if (connected) SitePermissionsRules.Action.ALLOWED else SitePermissionsRules.Action.BLOCKED + if (settings.getAutoplayUserSetting(default = AUTOPLAY_BLOCK_ALL) == AUTOPLAY_ALLOW_ON_WIFI) { + settings.setSitePermissionsPhoneFeatureAction(PhoneFeature.AUTOPLAY_AUDIBLE, setting) + settings.setSitePermissionsPhoneFeatureAction(PhoneFeature.AUTOPLAY_INAUDIBLE, setting) + } else { + // The autoplay setting has changed, we can remove the listener + removeWifiConnectedListener() } } @@ -55,11 +47,11 @@ class SitePermissionsWifiIntegration( } fun addWifiConnectedListener() { - wifiConnectionMonitor.addOnWifiConnectedChangedListener(wifiConnectedListener) + wifiConnectionMonitor.register(this) } fun removeWifiConnectedListener() { - wifiConnectionMonitor.removeOnWifiConnectedChangedListener(wifiConnectedListener) + wifiConnectionMonitor.unregister(this) } // Until https://bugzilla.mozilla.org/show_bug.cgi?id=1621825 is fixed, AUTOPLAY_ALLOW_ALL diff --git a/app/src/main/java/org/mozilla/fenix/wifi/WifiConnectionMonitor.kt b/app/src/main/java/org/mozilla/fenix/wifi/WifiConnectionMonitor.kt index 34a6e5f7a..b8e0c2ee9 100644 --- a/app/src/main/java/org/mozilla/fenix/wifi/WifiConnectionMonitor.kt +++ b/app/src/main/java/org/mozilla/fenix/wifi/WifiConnectionMonitor.kt @@ -5,11 +5,12 @@ package org.mozilla.fenix.wifi import android.app.Application -import android.content.Context import android.net.ConnectivityManager import android.net.Network import android.net.NetworkCapabilities import android.net.NetworkRequest +import mozilla.components.support.base.observer.Observable +import mozilla.components.support.base.observer.ObserverRegistry /** * Attaches itself to the [Application] and listens for WIFI available/not available events. This @@ -25,30 +26,28 @@ import android.net.NetworkRequest * app.components.wifiConnectionListener.start() * ``` */ -class WifiConnectionMonitor(app: Application) { - private val callbacks = mutableSetOf<(Boolean) -> Unit>() - private val connectivityManager = app.getSystemService(Context.CONNECTIVITY_SERVICE) as - ConnectivityManager +class WifiConnectionMonitor( + private val connectivityManager: ConnectivityManager +) : Observable by ObserverRegistry() { - private var lastKnownStateWasAvailable: Boolean? = null + private var callbackReceived: Boolean = false private var isRegistered = false private val frameworkListener = object : ConnectivityManager.NetworkCallback() { override fun onLost(network: Network?) { - callbacks.forEach { it(false) } - lastKnownStateWasAvailable = false + notifyAtLeastOneObserver { onWifiConnectionChanged(connected = false) } + callbackReceived = true } override fun onAvailable(network: Network?) { - callbacks.forEach { it(true) } - lastKnownStateWasAvailable = true + notifyAtLeastOneObserver { onWifiConnectionChanged(connected = true) } + callbackReceived = true } } /** * Attaches the [WifiConnectionMonitor] to the application. After this has been called, callbacks - * added via [addOnWifiConnectedChangedListener] will be called until either the app exits, or - * [stop] is called. + * added via [register] will be called until either the app exits, or [stop] is called. * * Any existing callbacks will be called with the current state when this is called. */ @@ -62,10 +61,8 @@ class WifiConnectionMonitor(app: Application) { // AFAICT, the framework does not send an event when a new NetworkCallback is registered // while the WIFI is not connected, so we push this manually. If the WIFI is on, it will send // a follow up event shortly - val noCallbacksReceivedYet = lastKnownStateWasAvailable == null - if (noCallbacksReceivedYet) { - lastKnownStateWasAvailable = false - callbacks.forEach { it(false) } + if (!callbackReceived) { + notifyAtLeastOneObserver { onWifiConnectionChanged(connected = false) } } connectivityManager.registerNetworkCallback(request, frameworkListener) @@ -74,7 +71,7 @@ class WifiConnectionMonitor(app: Application) { /** * Detatches the [WifiConnectionMonitor] from the app. No callbacks added via - * [addOnWifiConnectedChangedListener] will be called after this has been called. + * [register] will be called after this has been called. */ fun stop() { // Framework code will throw if an unregistered listener attempts to unregister. @@ -83,25 +80,7 @@ class WifiConnectionMonitor(app: Application) { isRegistered = false } - /** - * Adds [onWifiChanged] to a list of listeners that will be called whenever WIFI connects or - * disconnects. - * - * If [onWifiChanged] is successfully added (i.e., it is a new listener), it will be immediately - * called with the last known state. - */ - fun addOnWifiConnectedChangedListener(onWifiChanged: (Boolean) -> Unit) { - val lastKnownState = lastKnownStateWasAvailable - if (callbacks.add(onWifiChanged) && lastKnownState != null) { - onWifiChanged(lastKnownState) - } - } - - /** - * Removes [onWifiChanged] from the list of listeners to be called whenever WIFI connects or - * disconnects. - */ - fun removeOnWifiConnectedChangedListener(onWifiChanged: (Boolean) -> Unit) { - callbacks.remove(onWifiChanged) + interface Observer { + fun onWifiConnectionChanged(connected: Boolean) } } diff --git a/app/src/test/java/org/mozilla/fenix/push/LeanplumNotificationCustomizerTest.kt b/app/src/test/java/org/mozilla/fenix/push/LeanplumNotificationCustomizerTest.kt new file mode 100644 index 000000000..709b07c16 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/push/LeanplumNotificationCustomizerTest.kt @@ -0,0 +1,34 @@ +/* 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.push + +import android.app.Notification +import androidx.core.app.NotificationCompat +import io.mockk.Called +import io.mockk.mockk +import io.mockk.verify +import org.junit.Test +import org.mozilla.fenix.R + +class LeanplumNotificationCustomizerTest { + + private val customizer = LeanplumNotificationCustomizer() + + @Test + fun `customize adds icon`() { + val builder = mockk(relaxed = true) + customizer.customize(builder, mockk()) + + verify { builder.setSmallIcon(R.drawable.ic_status_logo) } + } + + @Test + fun `customize for BigPictureStyle does nothing`() { + val builder = mockk() + customizer.customize(builder, mockk(), mockk()) + + verify { builder wasNot Called } + } +} diff --git a/app/src/test/java/org/mozilla/fenix/sync/SyncedTabsIntegrationTest.kt b/app/src/test/java/org/mozilla/fenix/sync/SyncedTabsIntegrationTest.kt new file mode 100644 index 000000000..83086d21a --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/sync/SyncedTabsIntegrationTest.kt @@ -0,0 +1,55 @@ +/* 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.sync + +import android.content.Context +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.just +import io.mockk.mockk +import io.mockk.slot +import io.mockk.verify +import mozilla.components.concept.sync.AccountObserver +import mozilla.components.feature.syncedtabs.storage.SyncedTabsStorage +import mozilla.components.service.fxa.manager.FxaAccountManager +import org.junit.Before +import org.junit.Test +import org.mozilla.fenix.FenixApplication + +class SyncedTabsIntegrationTest { + + @MockK private lateinit var context: Context + @MockK private lateinit var syncedTabsStorage: SyncedTabsStorage + @MockK private lateinit var accountManager: FxaAccountManager + + @Before + fun setup() { + MockKAnnotations.init(this) + every { syncedTabsStorage.stop() } just Runs + every { accountManager.register(any(), owner = any(), autoPause = true) } just Runs + every { context.applicationContext } returns mockk { + every { components } returns mockk { + every { backgroundServices.syncedTabsStorage } returns syncedTabsStorage + } + } + } + + @Test + fun `starts and stops syncedTabsStorage on user authentication`() { + val observer = slot() + SyncedTabsIntegration(context, accountManager).launch() + verify { accountManager.register(capture(observer), owner = any(), autoPause = true) } + + every { syncedTabsStorage.start() } just Runs + observer.captured.onAuthenticated(mockk(), mockk()) + verify { syncedTabsStorage.start() } + + every { syncedTabsStorage.stop() } just Runs + observer.captured.onLoggedOut() + verify { syncedTabsStorage.stop() } + } +} diff --git a/app/src/test/java/org/mozilla/fenix/sync/SyncedTabsViewHolderTest.kt b/app/src/test/java/org/mozilla/fenix/sync/SyncedTabsViewHolderTest.kt new file mode 100644 index 000000000..b81a0fc3b --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/sync/SyncedTabsViewHolderTest.kt @@ -0,0 +1,112 @@ +/* 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.sync + +import android.view.LayoutInflater +import android.view.View +import android.widget.TextView +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.android.synthetic.main.sync_tabs_list_item.view.* +import kotlinx.android.synthetic.main.view_synced_tabs_group.view.* +import mozilla.components.browser.storage.sync.Tab +import mozilla.components.browser.storage.sync.TabEntry +import mozilla.components.concept.sync.Device +import mozilla.components.concept.sync.DeviceType +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.mozilla.fenix.R +import org.mozilla.fenix.helpers.FenixRobolectricTestRunner + +@RunWith(FenixRobolectricTestRunner::class) +class SyncedTabsViewHolderTest { + + private lateinit var tabViewHolder: SyncedTabsViewHolder.TabViewHolder + private lateinit var tabView: View + private lateinit var deviceViewHolder: SyncedTabsViewHolder.DeviceViewHolder + private lateinit var deviceView: View + private lateinit var deviceViewGroupName: TextView + + private val tab = Tab( + history = listOf( + mockk(), + TabEntry( + title = "Firefox", + url = "https://firefox.com", + iconUrl = "https://firefox.com/favicon.ico" + ), + mockk() + ), + active = 1, + lastUsed = 0L + ) + + @Before + fun setup() { + val inflater = LayoutInflater.from(testContext) + + tabView = inflater.inflate(SyncedTabsViewHolder.TabViewHolder.LAYOUT_ID, null) + tabViewHolder = SyncedTabsViewHolder.TabViewHolder(tabView) + + deviceViewGroupName = mockk(relaxUnitFun = true) + deviceView = mockk { + every { synced_tabs_group_name } returns deviceViewGroupName + } + deviceViewHolder = SyncedTabsViewHolder.DeviceViewHolder(deviceView) + } + + @Test + fun `TabViewHolder binds active tab`() { + tabViewHolder.bind(SyncedTabsAdapter.AdapterItem.Tab(tab), mockk()) + + assertEquals("Firefox", tabView.synced_tab_item_title.text) + assertEquals("https://firefox.com", tabView.synced_tab_item_url.text) + } + + @Test + fun `TabViewHolder calls interactor on click`() { + val interactor = mockk<(Tab) -> Unit>(relaxed = true) + tabViewHolder.bind(SyncedTabsAdapter.AdapterItem.Tab(tab), interactor) + + tabView.performClick() + verify { interactor(tab) } + } + + @Test + fun `DeviceViewHolder binds desktop device`() { + val device = mockk { + every { displayName } returns "Charcoal" + every { deviceType } returns DeviceType.DESKTOP + } + deviceViewHolder.bind(SyncedTabsAdapter.AdapterItem.Device(device), mockk()) + + verify { deviceViewGroupName.text = "Charcoal" } + verify { + deviceViewGroupName.setCompoundDrawablesWithIntrinsicBounds( + R.drawable.mozac_ic_device_desktop, 0, 0, 0 + ) + } + } + + @Test + fun `DeviceViewHolder binds mobile device`() { + val device = mockk { + every { displayName } returns "Emerald" + every { deviceType } returns DeviceType.MOBILE + } + deviceViewHolder.bind(SyncedTabsAdapter.AdapterItem.Device(device), mockk()) + + verify { deviceViewGroupName.text = "Emerald" } + verify { + deviceViewGroupName.setCompoundDrawablesWithIntrinsicBounds( + R.drawable.mozac_ic_device_mobile, 0, 0, 0 + ) + } + } +} diff --git a/app/src/test/java/org/mozilla/fenix/wifi/SitePermissionsWifiIntegrationTest.kt b/app/src/test/java/org/mozilla/fenix/wifi/SitePermissionsWifiIntegrationTest.kt new file mode 100644 index 000000000..77c481bf2 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/wifi/SitePermissionsWifiIntegrationTest.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.fenix.wifi + +import io.mockk.Called +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.verify +import mozilla.components.feature.sitepermissions.SitePermissionsRules.Action +import org.junit.Before +import org.junit.Test +import org.mozilla.fenix.settings.PhoneFeature.AUTOPLAY_AUDIBLE +import org.mozilla.fenix.settings.PhoneFeature.AUTOPLAY_INAUDIBLE +import org.mozilla.fenix.settings.sitepermissions.AUTOPLAY_ALLOW_ALL +import org.mozilla.fenix.settings.sitepermissions.AUTOPLAY_ALLOW_ON_WIFI +import org.mozilla.fenix.settings.sitepermissions.AUTOPLAY_BLOCK_ALL +import org.mozilla.fenix.utils.Settings + +class SitePermissionsWifiIntegrationTest { + + private lateinit var settings: Settings + private lateinit var wifiConnectionMonitor: WifiConnectionMonitor + private lateinit var wifiIntegration: SitePermissionsWifiIntegration + + @Before + fun setup() { + settings = mockk() + wifiConnectionMonitor = mockk(relaxed = true) + wifiIntegration = SitePermissionsWifiIntegration(settings, wifiConnectionMonitor) + + every { settings.setSitePermissionsPhoneFeatureAction(any(), any()) } just Runs + } + + @Test + fun `add and remove wifi connected listener`() { + wifiIntegration.addWifiConnectedListener() + verify { wifiConnectionMonitor.register(any()) } + + wifiIntegration.removeWifiConnectedListener() + verify { wifiConnectionMonitor.unregister(any()) } + } + + @Test + fun `start and stop wifi connection monitor`() { + wifiIntegration.start() + verify { wifiConnectionMonitor.start() } + + wifiIntegration.stop() + verify { wifiConnectionMonitor.stop() } + } + + @Test + fun `add only if autoplay is only allowed on wifi`() { + every { settings.getAutoplayUserSetting(default = AUTOPLAY_BLOCK_ALL) } returns AUTOPLAY_ALLOW_ALL + wifiIntegration.maybeAddWifiConnectedListener() + verify { wifiConnectionMonitor wasNot Called } + + every { settings.getAutoplayUserSetting(default = AUTOPLAY_BLOCK_ALL) } returns AUTOPLAY_ALLOW_ON_WIFI + wifiIntegration.maybeAddWifiConnectedListener() + verify { wifiConnectionMonitor.register(any()) } + } + + @Test + fun `listener removes itself if autoplay is not only allowed on wifi`() { + every { settings.getAutoplayUserSetting(default = AUTOPLAY_BLOCK_ALL) } returns AUTOPLAY_ALLOW_ALL + wifiIntegration.onWifiConnectionChanged(connected = true) + verify { wifiConnectionMonitor.unregister(any()) } + } + + @Test + fun `listener sets audible and inaudible settings to allowed on connect`() { + every { settings.getAutoplayUserSetting(default = AUTOPLAY_BLOCK_ALL) } returns AUTOPLAY_ALLOW_ON_WIFI + wifiIntegration.onWifiConnectionChanged(connected = true) + verify { settings.setSitePermissionsPhoneFeatureAction(AUTOPLAY_AUDIBLE, Action.ALLOWED) } + verify { settings.setSitePermissionsPhoneFeatureAction(AUTOPLAY_INAUDIBLE, Action.ALLOWED) } + } + + @Test + fun `listener sets audible and inaudible settings to blocked on disconnected`() { + every { settings.getAutoplayUserSetting(default = AUTOPLAY_BLOCK_ALL) } returns AUTOPLAY_ALLOW_ON_WIFI + wifiIntegration.onWifiConnectionChanged(connected = false) + verify { settings.setSitePermissionsPhoneFeatureAction(AUTOPLAY_AUDIBLE, Action.BLOCKED) } + verify { settings.setSitePermissionsPhoneFeatureAction(AUTOPLAY_INAUDIBLE, Action.BLOCKED) } + } +} diff --git a/app/src/test/java/org/mozilla/fenix/wifi/WifiConnectionMonitorTest.kt b/app/src/test/java/org/mozilla/fenix/wifi/WifiConnectionMonitorTest.kt new file mode 100644 index 000000000..94b8a7f10 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/wifi/WifiConnectionMonitorTest.kt @@ -0,0 +1,91 @@ +/* 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.wifi + +import android.net.ConnectivityManager +import android.net.NetworkRequest +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.mockkConstructor +import io.mockk.slot +import io.mockk.unmockkConstructor +import io.mockk.verify +import org.junit.After +import org.junit.Before +import org.junit.Test + +class WifiConnectionMonitorTest { + + private lateinit var connectivityManager: ConnectivityManager + private lateinit var wifiConnectionMonitor: WifiConnectionMonitor + + @Before + fun setup() { + mockkConstructor(NetworkRequest.Builder::class) + connectivityManager = mockk(relaxUnitFun = true) + wifiConnectionMonitor = WifiConnectionMonitor(connectivityManager) + + every { + anyConstructed().addTransportType(any()) + } answers { self as NetworkRequest.Builder } + } + + @After + fun teardown() { + unmockkConstructor(NetworkRequest.Builder::class) + } + + @Test + fun `start runs only once`() { + wifiConnectionMonitor.start() + wifiConnectionMonitor.start() + + verify(exactly = 1) { + connectivityManager.registerNetworkCallback(any(), any()) + } + } + + @Test + fun `stop only runs after start`() { + wifiConnectionMonitor.stop() + verify(exactly = 0) { + connectivityManager.unregisterNetworkCallback(any()) + } + + wifiConnectionMonitor.start() + wifiConnectionMonitor.stop() + verify { + connectivityManager.unregisterNetworkCallback(any()) + } + } + + @Test + fun `passes results from connectivity manager to observers`() { + val slot = slot() + every { connectivityManager.registerNetworkCallback(any(), capture(slot)) } just Runs + + wifiConnectionMonitor.start() + + // Immediately notifies observer when registered + val observer = mockk(relaxed = true) + wifiConnectionMonitor.register(observer) + verify { observer.onWifiConnectionChanged(connected = false) } + + // Notifies observer when network is available or lost + slot.captured.onAvailable(mockk()) + verify { observer.onWifiConnectionChanged(connected = true) } + + slot.captured.onLost(mockk()) + verify { observer.onWifiConnectionChanged(connected = false) } + } + + private fun captureNetworkCallback(): ConnectivityManager.NetworkCallback { + val slot = slot() + verify { connectivityManager.registerNetworkCallback(any(), capture(slot)) } + return slot.captured + } +}