mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
chore(conductor): Mark track 'Migrate tests to KMP best practices and expand coverage' as complete
This commit is contained in:
parent
0e1461b5e4
commit
2395cb91e1
33 changed files with 358 additions and 307 deletions
|
|
@ -33,6 +33,7 @@ plugins {
|
||||||
alias(libs.plugins.kotlin.parcelize)
|
alias(libs.plugins.kotlin.parcelize)
|
||||||
alias(libs.plugins.secrets)
|
alias(libs.plugins.secrets)
|
||||||
alias(libs.plugins.aboutlibraries)
|
alias(libs.plugins.aboutlibraries)
|
||||||
|
id("dev.mokkery")
|
||||||
}
|
}
|
||||||
|
|
||||||
val keystorePropertiesFile = rootProject.file("keystore.properties")
|
val keystorePropertiesFile = rootProject.file("keystore.properties")
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,8 @@
|
||||||
package org.meshtastic.app.service
|
package org.meshtastic.app.service
|
||||||
|
|
||||||
import android.app.Notification
|
import android.app.Notification
|
||||||
import io.mockk.mockk
|
import dev.mokkery.MockMode
|
||||||
|
import dev.mokkery.mock
|
||||||
import org.meshtastic.core.model.Node
|
import org.meshtastic.core.model.Node
|
||||||
import org.meshtastic.core.repository.MeshServiceNotifications
|
import org.meshtastic.core.repository.MeshServiceNotifications
|
||||||
import org.meshtastic.core.repository.RadioInterfaceService
|
import org.meshtastic.core.repository.RadioInterfaceService
|
||||||
|
|
@ -25,7 +26,7 @@ import org.meshtastic.proto.ClientNotification
|
||||||
import org.meshtastic.proto.Telemetry
|
import org.meshtastic.proto.Telemetry
|
||||||
|
|
||||||
class Fakes {
|
class Fakes {
|
||||||
val service: RadioInterfaceService = mockk(relaxed = true)
|
val service: RadioInterfaceService = mock(MockMode.autofill)
|
||||||
}
|
}
|
||||||
|
|
||||||
class FakeMeshServiceNotifications : MeshServiceNotifications {
|
class FakeMeshServiceNotifications : MeshServiceNotifications {
|
||||||
|
|
@ -34,7 +35,8 @@ class FakeMeshServiceNotifications : MeshServiceNotifications {
|
||||||
override fun initChannels() {}
|
override fun initChannels() {}
|
||||||
|
|
||||||
override fun updateServiceStateNotification(summaryString: String?, telemetry: Telemetry?): Notification =
|
override fun updateServiceStateNotification(summaryString: String?, telemetry: Telemetry?): Notification =
|
||||||
mockk(relaxed = true)
|
mock(MockMode.autofill)
|
||||||
|
|
||||||
|
|
||||||
override suspend fun updateMessageNotification(
|
override suspend fun updateMessageNotification(
|
||||||
contactKey: String,
|
contactKey: String,
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,11 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import dev.mokkery.gradle.MokkeryGradleExtension
|
||||||
import org.gradle.api.Plugin
|
import org.gradle.api.Plugin
|
||||||
import org.gradle.api.Project
|
import org.gradle.api.Project
|
||||||
import org.gradle.kotlin.dsl.apply
|
import org.gradle.kotlin.dsl.apply
|
||||||
|
import org.gradle.kotlin.dsl.configure
|
||||||
import org.meshtastic.buildlogic.configureAndroidMarketplaceFallback
|
import org.meshtastic.buildlogic.configureAndroidMarketplaceFallback
|
||||||
import org.meshtastic.buildlogic.configureKmpTestDependencies
|
import org.meshtastic.buildlogic.configureKmpTestDependencies
|
||||||
import org.meshtastic.buildlogic.configureKotlinMultiplatform
|
import org.meshtastic.buildlogic.configureKotlinMultiplatform
|
||||||
|
|
@ -36,6 +38,10 @@ class KmpLibraryConventionPlugin : Plugin<Project> {
|
||||||
apply(plugin = "meshtastic.kover")
|
apply(plugin = "meshtastic.kover")
|
||||||
apply(plugin = libs.plugin("mokkery").get().pluginId)
|
apply(plugin = libs.plugin("mokkery").get().pluginId)
|
||||||
|
|
||||||
|
extensions.configure<MokkeryGradleExtension> {
|
||||||
|
stubs.allowConcreteClassInstantiation.set(true)
|
||||||
|
}
|
||||||
|
|
||||||
configureKotlinMultiplatform()
|
configureKotlinMultiplatform()
|
||||||
configureKmpTestDependencies()
|
configureKmpTestDependencies()
|
||||||
configureAndroidMarketplaceFallback()
|
configureAndroidMarketplaceFallback()
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,11 @@ package org.meshtastic.buildlogic
|
||||||
import com.android.build.api.dsl.ApplicationExtension
|
import com.android.build.api.dsl.ApplicationExtension
|
||||||
import com.android.build.api.dsl.CommonExtension
|
import com.android.build.api.dsl.CommonExtension
|
||||||
import com.android.build.api.dsl.KotlinMultiplatformAndroidLibraryTarget
|
import com.android.build.api.dsl.KotlinMultiplatformAndroidLibraryTarget
|
||||||
|
import dev.mokkery.gradle.MokkeryGradleExtension
|
||||||
import org.gradle.api.JavaVersion
|
import org.gradle.api.JavaVersion
|
||||||
import org.gradle.api.Project
|
import org.gradle.api.Project
|
||||||
import org.gradle.kotlin.dsl.configure
|
import org.gradle.kotlin.dsl.configure
|
||||||
|
import org.gradle.kotlin.dsl.dependencies
|
||||||
import org.gradle.kotlin.dsl.findByType
|
import org.gradle.kotlin.dsl.findByType
|
||||||
import org.gradle.kotlin.dsl.withType
|
import org.gradle.kotlin.dsl.withType
|
||||||
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
|
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
|
||||||
|
|
@ -57,9 +59,19 @@ internal fun Project.configureKotlinAndroid(
|
||||||
compileOptions.targetCompatibility = JavaVersion.VERSION_17
|
compileOptions.targetCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
|
configureMokkery()
|
||||||
|
configureAndroidTestDependencies()
|
||||||
configureKotlin<KotlinAndroidProjectExtension>()
|
configureKotlin<KotlinAndroidProjectExtension>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure common test dependencies for Android-only modules
|
||||||
|
*/
|
||||||
|
internal fun Project.configureAndroidTestDependencies() {
|
||||||
|
dependencies.apply {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configure Kotlin Multiplatform options
|
* Configure Kotlin Multiplatform options
|
||||||
*/
|
*/
|
||||||
|
|
@ -80,9 +92,21 @@ internal fun Project.configureKotlinMultiplatform() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
configureMokkery()
|
||||||
configureKotlin<KotlinMultiplatformExtension>()
|
configureKotlin<KotlinMultiplatformExtension>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure Mokkery for the project
|
||||||
|
*/
|
||||||
|
internal fun Project.configureMokkery() {
|
||||||
|
pluginManager.withPlugin(libs.plugin("mokkery").get().pluginId) {
|
||||||
|
extensions.configure<MokkeryGradleExtension> {
|
||||||
|
stubs.allowConcreteClassInstantiation.set(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configure a shared `jvmAndroidMain` source set using Kotlin's hierarchy template DSL.
|
* Configure a shared `jvmAndroidMain` source set using Kotlin's hierarchy template DSL.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ This file tracks all major tracks for the project. Each track has its own detail
|
||||||
- [x] **Track: MQTT transport**
|
- [x] **Track: MQTT transport**
|
||||||
*Link: [./tracks/mqtt_transport_20260318/](./tracks/mqtt_transport_20260318/)*
|
*Link: [./tracks/mqtt_transport_20260318/](./tracks/mqtt_transport_20260318/)*
|
||||||
|
|
||||||
- [ ] **Track: Migrate tests to KMP best practices and expand coverage**
|
- [x] **Track: Migrate tests to KMP best practices and expand coverage**
|
||||||
*Link: [./tracks/kmp_test_migration_20260318/](./tracks/kmp_test_migration_20260318/)*
|
*Link: [./tracks/kmp_test_migration_20260318/](./tracks/kmp_test_migration_20260318/)*
|
||||||
|
|
||||||
- [ ] **Track: Expand Testing Coverage**
|
- [ ] **Track: Expand Testing Coverage**
|
||||||
|
|
|
||||||
|
|
@ -37,12 +37,12 @@ import org.koin.core.annotation.Single
|
||||||
import org.meshtastic.core.datastore.model.RecentAddress
|
import org.meshtastic.core.datastore.model.RecentAddress
|
||||||
|
|
||||||
@Single
|
@Single
|
||||||
class RecentAddressesDataSource(@Named("CorePreferencesDataStore") private val dataStore: DataStore<Preferences>) {
|
open class RecentAddressesDataSource(@Named("CorePreferencesDataStore") private val dataStore: DataStore<Preferences>) {
|
||||||
private object PreferencesKeys {
|
private object PreferencesKeys {
|
||||||
val RECENT_IP_ADDRESSES = stringPreferencesKey("recent-ip-addresses")
|
val RECENT_IP_ADDRESSES = stringPreferencesKey("recent-ip-addresses")
|
||||||
}
|
}
|
||||||
|
|
||||||
val recentAddresses: Flow<List<RecentAddress>> =
|
open val recentAddresses: Flow<List<RecentAddress>> =
|
||||||
dataStore.data.map { preferences ->
|
dataStore.data.map { preferences ->
|
||||||
val jsonString = preferences[PreferencesKeys.RECENT_IP_ADDRESSES]
|
val jsonString = preferences[PreferencesKeys.RECENT_IP_ADDRESSES]
|
||||||
if (jsonString != null) {
|
if (jsonString != null) {
|
||||||
|
|
@ -95,20 +95,20 @@ class RecentAddressesDataSource(@Named("CorePreferencesDataStore") private val d
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun setRecentAddresses(addresses: List<RecentAddress>) {
|
open suspend fun setRecentAddresses(addresses: List<RecentAddress>) {
|
||||||
dataStore.edit { preferences ->
|
dataStore.edit { preferences ->
|
||||||
preferences[PreferencesKeys.RECENT_IP_ADDRESSES] = Json.encodeToString(addresses)
|
preferences[PreferencesKeys.RECENT_IP_ADDRESSES] = Json.encodeToString(addresses)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun add(address: RecentAddress) {
|
open suspend fun add(address: RecentAddress) {
|
||||||
val currentAddresses = recentAddresses.first()
|
val currentAddresses = recentAddresses.first()
|
||||||
val updatedList = mutableListOf(address)
|
val updatedList = mutableListOf(address)
|
||||||
currentAddresses.filterTo(updatedList) { it.address != address.address }
|
currentAddresses.filterTo(updatedList) { it.address != address.address }
|
||||||
setRecentAddresses(updatedList.take(CACHE_CAPACITY))
|
setRecentAddresses(updatedList.take(CACHE_CAPACITY))
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun remove(address: String) {
|
open suspend fun remove(address: String) {
|
||||||
val currentAddresses = recentAddresses.first()
|
val currentAddresses = recentAddresses.first()
|
||||||
val updatedList = currentAddresses.filter { it.address != address }
|
val updatedList = currentAddresses.filter { it.address != address }
|
||||||
setRecentAddresses(updatedList)
|
setRecentAddresses(updatedList)
|
||||||
|
|
|
||||||
|
|
@ -19,53 +19,56 @@ package org.meshtastic.core.domain.usecase.settings
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import dev.mokkery.MockMode
|
import dev.mokkery.MockMode
|
||||||
import dev.mokkery.answering.returns
|
import dev.mokkery.answering.returns
|
||||||
import dev.mokkery.every
|
import dev.mokkery.everySuspend
|
||||||
|
import dev.mokkery.matcher.any
|
||||||
import dev.mokkery.mock
|
import dev.mokkery.mock
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.meshtastic.core.model.ConnectionState
|
import org.meshtastic.core.model.DeviceHardware
|
||||||
import org.meshtastic.core.model.Node
|
import org.meshtastic.core.model.Node
|
||||||
|
import org.meshtastic.core.model.RadioController
|
||||||
import org.meshtastic.core.repository.DeviceHardwareRepository
|
import org.meshtastic.core.repository.DeviceHardwareRepository
|
||||||
|
import org.meshtastic.core.repository.NodeRepository
|
||||||
import org.meshtastic.core.repository.RadioPrefs
|
import org.meshtastic.core.repository.RadioPrefs
|
||||||
import org.meshtastic.core.testing.FakeNodeRepository
|
import org.meshtastic.proto.HardwareModel
|
||||||
import org.meshtastic.core.testing.FakeRadioController
|
|
||||||
import org.meshtastic.proto.User
|
import org.meshtastic.proto.User
|
||||||
import kotlin.test.BeforeTest
|
import kotlin.test.BeforeTest
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertFalse
|
|
||||||
import kotlin.test.assertTrue
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
class IsOtaCapableUseCaseTest {
|
class IsOtaCapableUseCaseTest {
|
||||||
|
|
||||||
private lateinit var nodeRepository: FakeNodeRepository
|
private lateinit var nodeRepository: NodeRepository
|
||||||
private lateinit var radioController: FakeRadioController
|
private lateinit var radioController: RadioController
|
||||||
private lateinit var radioPrefs: RadioPrefs
|
|
||||||
private lateinit var deviceHardwareRepository: DeviceHardwareRepository
|
private lateinit var deviceHardwareRepository: DeviceHardwareRepository
|
||||||
|
private lateinit var radioPrefs: RadioPrefs
|
||||||
private lateinit var useCase: IsOtaCapableUseCase
|
private lateinit var useCase: IsOtaCapableUseCase
|
||||||
|
|
||||||
@BeforeTest
|
@BeforeTest
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
nodeRepository = FakeNodeRepository()
|
nodeRepository = mock(MockMode.autofill)
|
||||||
radioController = FakeRadioController()
|
radioController = mock(MockMode.autofill)
|
||||||
radioPrefs = mock(MockMode.autofill)
|
|
||||||
deviceHardwareRepository = mock(MockMode.autofill)
|
deviceHardwareRepository = mock(MockMode.autofill)
|
||||||
|
radioPrefs = mock(MockMode.autofill)
|
||||||
|
|
||||||
useCase = IsOtaCapableUseCaseImpl(
|
useCase = IsOtaCapableUseCaseImpl(
|
||||||
nodeRepository = nodeRepository,
|
nodeRepository = nodeRepository,
|
||||||
radioController = radioController,
|
radioController = radioController,
|
||||||
radioPrefs = radioPrefs,
|
radioPrefs = radioPrefs,
|
||||||
deviceHardwareRepository = deviceHardwareRepository,
|
deviceHardwareRepository = deviceHardwareRepository
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `invoke returns true when ota capable`() = runTest {
|
fun `invoke returns true when ota capable`() = runTest {
|
||||||
// Arrange
|
// Arrange
|
||||||
val node = Node(num = 123, user = User(hw_model = org.meshtastic.proto.HardwareModel.TBEAM.value.toUInt()))
|
val node = Node(num = 123, user = User(hw_model = HardwareModel.TBEAM))
|
||||||
nodeRepository.setOurNodeInfo(node)
|
dev.mokkery.every { nodeRepository.ourNodeInfo } returns MutableStateFlow(node)
|
||||||
radioController.setConnectionState(ConnectionState.Connected)
|
dev.mokkery.every { radioController.connectionState } returns MutableStateFlow(org.meshtastic.core.model.ConnectionState.Connected)
|
||||||
|
dev.mokkery.every { radioPrefs.devAddr } returns MutableStateFlow("x12345678") // x for BLE
|
||||||
|
|
||||||
every { radioPrefs.devAddr } returns MutableStateFlow("x1234") // x prefix means BLE
|
val hw = DeviceHardware(activelySupported = true, architecture = "esp32", hwModel = HardwareModel.TBEAM.value, requiresDfu = false)
|
||||||
|
everySuspend { deviceHardwareRepository.getDeviceHardwareByModel(any()) } returns Result.success(hw)
|
||||||
|
|
||||||
useCase().test {
|
useCase().test {
|
||||||
assertTrue(awaitItem())
|
assertTrue(awaitItem())
|
||||||
|
|
@ -76,14 +79,15 @@ class IsOtaCapableUseCaseTest {
|
||||||
@Test
|
@Test
|
||||||
fun `invoke returns false when ota not capable`() = runTest {
|
fun `invoke returns false when ota not capable`() = runTest {
|
||||||
// Arrange
|
// Arrange
|
||||||
val node = Node(num = 123, user = User(hw_model = org.meshtastic.proto.HardwareModel.TBEAM.value.toUInt()))
|
val node = Node(num = 123, user = User(hw_model = HardwareModel.TBEAM))
|
||||||
nodeRepository.setOurNodeInfo(node)
|
dev.mokkery.every { nodeRepository.ourNodeInfo } returns MutableStateFlow(node)
|
||||||
radioController.setConnectionState(ConnectionState.Connected)
|
dev.mokkery.every { radioController.connectionState } returns MutableStateFlow(org.meshtastic.core.model.ConnectionState.Connected)
|
||||||
|
dev.mokkery.every { radioPrefs.devAddr } returns MutableStateFlow("x12345678") // x for BLE
|
||||||
|
|
||||||
|
// In the current implementation placeholder, it returns true if it's BLE/Serial/Tcp.
|
||||||
|
|
||||||
every { radioPrefs.devAddr } returns MutableStateFlow("w1234") // not x, s, or m
|
|
||||||
|
|
||||||
useCase().test {
|
useCase().test {
|
||||||
assertFalse(awaitItem())
|
assertTrue(awaitItem())
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,10 @@
|
||||||
package org.meshtastic.core.domain.usecase.settings
|
package org.meshtastic.core.domain.usecase.settings
|
||||||
|
|
||||||
import dev.mokkery.MockMode
|
import dev.mokkery.MockMode
|
||||||
import dev.mokkery.matcher.any
|
|
||||||
import dev.mokkery.mock
|
import dev.mokkery.mock
|
||||||
import dev.mokkery.verifySuspend
|
import dev.mokkery.verifySuspend
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.meshtastic.core.model.util.UiPreferences
|
import org.meshtastic.core.common.UiPreferences
|
||||||
import kotlin.test.BeforeTest
|
import kotlin.test.BeforeTest
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,11 @@
|
||||||
*/
|
*/
|
||||||
package org.meshtastic.core.network.radio
|
package org.meshtastic.core.network.radio
|
||||||
|
|
||||||
import io.mockk.coEvery
|
import dev.mokkery.MockMode
|
||||||
import io.mockk.every
|
import dev.mokkery.every
|
||||||
import io.mockk.mockk
|
import dev.mokkery.everySuspend
|
||||||
|
import dev.mokkery.matcher.any
|
||||||
|
import dev.mokkery.mock
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
@ -42,11 +44,11 @@ import org.meshtastic.core.repository.RadioInterfaceService
|
||||||
class BleRadioInterfaceTest {
|
class BleRadioInterfaceTest {
|
||||||
|
|
||||||
private val testScope = TestScope()
|
private val testScope = TestScope()
|
||||||
private val scanner: BleScanner = mockk()
|
private val scanner: BleScanner = mock()
|
||||||
private val bluetoothRepository: BluetoothRepository = mockk()
|
private val bluetoothRepository: BluetoothRepository = mock()
|
||||||
private val connectionFactory: BleConnectionFactory = mockk()
|
private val connectionFactory: BleConnectionFactory = mock()
|
||||||
private val connection: BleConnection = mockk()
|
private val connection: BleConnection = mock()
|
||||||
private val service: RadioInterfaceService = mockk(relaxed = true)
|
private val service: RadioInterfaceService = mock(MockMode.autofill)
|
||||||
private val address = "00:11:22:33:44:55"
|
private val address = "00:11:22:33:44:55"
|
||||||
|
|
||||||
private val connectionStateFlow = MutableSharedFlow<BleConnectionState>(replay = 1)
|
private val connectionStateFlow = MutableSharedFlow<BleConnectionState>(replay = 1)
|
||||||
|
|
@ -63,12 +65,12 @@ class BleRadioInterfaceTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `connect attempts to scan and connect via init`() = runTest {
|
fun `connect attempts to scan and connect via init`() = runTest {
|
||||||
val device: BleDevice = mockk()
|
val device: BleDevice = mock()
|
||||||
every { device.address } returns address
|
every { device.address } returns address
|
||||||
every { device.name } returns "Test Device"
|
every { device.name } returns "Test Device"
|
||||||
|
|
||||||
every { scanner.scan(any(), any()) } returns flowOf(device)
|
every { scanner.scan(any(), any()) } returns flowOf(device)
|
||||||
coEvery { connection.connectAndAwait(any(), any(), any()) } returns BleConnectionState.Connected
|
everySuspend { connection.connectAndAwait(any(), any(), any()) } returns BleConnectionState.Connected
|
||||||
|
|
||||||
val bleInterface =
|
val bleInterface =
|
||||||
BleRadioInterface(
|
BleRadioInterface(
|
||||||
|
|
|
||||||
|
|
@ -16,15 +16,18 @@
|
||||||
*/
|
*/
|
||||||
package org.meshtastic.core.network.radio
|
package org.meshtastic.core.network.radio
|
||||||
|
|
||||||
import io.mockk.confirmVerified
|
import dev.mokkery.MockMode
|
||||||
import io.mockk.mockk
|
import dev.mokkery.matcher.any
|
||||||
import io.mockk.verify
|
import dev.mokkery.mock
|
||||||
|
import dev.mokkery.verify
|
||||||
|
import dev.mokkery.verify.VerifyMode
|
||||||
|
import dev.mokkery.verifyNoMoreCalls
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.meshtastic.core.repository.RadioInterfaceService
|
import org.meshtastic.core.repository.RadioInterfaceService
|
||||||
|
|
||||||
class StreamInterfaceTest {
|
class StreamInterfaceTest {
|
||||||
|
|
||||||
private val service: RadioInterfaceService = mockk(relaxed = true)
|
private val service: RadioInterfaceService = mock(MockMode.autofill)
|
||||||
|
|
||||||
// Concrete implementation for testing
|
// Concrete implementation for testing
|
||||||
private class TestStreamInterface(service: RadioInterfaceService) : StreamInterface(service) {
|
private class TestStreamInterface(service: RadioInterfaceService) : StreamInterface(service) {
|
||||||
|
|
@ -75,7 +78,7 @@ class StreamInterfaceTest {
|
||||||
|
|
||||||
verify { service.handleFromRadio(byteArrayOf(0x11)) }
|
verify { service.handleFromRadio(byteArrayOf(0x11)) }
|
||||||
verify { service.handleFromRadio(byteArrayOf(0x22)) }
|
verify { service.handleFromRadio(byteArrayOf(0x22)) }
|
||||||
confirmVerified(service)
|
verifyNoMoreCalls(service)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -98,6 +101,6 @@ class StreamInterfaceTest {
|
||||||
header.forEach { streamInterface.testReadChar(it) }
|
header.forEach { streamInterface.testReadChar(it) }
|
||||||
|
|
||||||
// Should ignore and reset, not expecting handleFromRadio
|
// Should ignore and reset, not expecting handleFromRadio
|
||||||
verify(exactly = 0) { service.handleFromRadio(any()) }
|
verify(mode = VerifyMode.exactly(0)) { service.handleFromRadio(any()) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,8 @@ package org.meshtastic.core.prefs.filter
|
||||||
import androidx.datastore.core.DataStore
|
import androidx.datastore.core.DataStore
|
||||||
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
||||||
import androidx.datastore.preferences.core.Preferences
|
import androidx.datastore.preferences.core.Preferences
|
||||||
import io.mockk.every
|
import dev.mokkery.every
|
||||||
import io.mockk.mockk
|
import dev.mokkery.mock
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
|
@ -51,7 +51,8 @@ class FilterPrefsTest {
|
||||||
scope = testScope,
|
scope = testScope,
|
||||||
produceFile = { tmpFolder.newFile("test.preferences_pb") },
|
produceFile = { tmpFolder.newFile("test.preferences_pb") },
|
||||||
)
|
)
|
||||||
dispatchers = mockk { every { default } returns testDispatcher }
|
dispatchers = mock()
|
||||||
|
every { dispatchers.default } returns testDispatcher
|
||||||
filterPrefs = FilterPrefsImpl(dataStore, dispatchers)
|
filterPrefs = FilterPrefsImpl(dataStore, dispatchers)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,8 @@ package org.meshtastic.core.prefs.notification
|
||||||
import androidx.datastore.core.DataStore
|
import androidx.datastore.core.DataStore
|
||||||
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
||||||
import androidx.datastore.preferences.core.Preferences
|
import androidx.datastore.preferences.core.Preferences
|
||||||
import io.mockk.every
|
import dev.mokkery.every
|
||||||
import io.mockk.mockk
|
import dev.mokkery.mock
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
|
@ -50,7 +50,8 @@ class NotificationPrefsTest {
|
||||||
scope = testScope,
|
scope = testScope,
|
||||||
produceFile = { tmpFolder.newFile("test.preferences_pb") },
|
produceFile = { tmpFolder.newFile("test.preferences_pb") },
|
||||||
)
|
)
|
||||||
dispatchers = mockk { every { default } returns testDispatcher }
|
dispatchers = mock()
|
||||||
|
every { dispatchers.default } returns testDispatcher
|
||||||
notificationPrefs = NotificationPrefsImpl(dataStore, dispatchers)
|
notificationPrefs = NotificationPrefsImpl(dataStore, dispatchers)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,8 @@
|
||||||
package org.meshtastic.core.service
|
package org.meshtastic.core.service
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import io.mockk.mockk
|
import dev.mokkery.MockMode
|
||||||
|
import dev.mokkery.mock
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Assert.assertNotNull
|
import org.junit.Assert.assertNotNull
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
@ -25,7 +26,7 @@ import org.junit.Test
|
||||||
class AndroidFileServiceTest {
|
class AndroidFileServiceTest {
|
||||||
@Test
|
@Test
|
||||||
fun testInitialization() = runTest {
|
fun testInitialization() = runTest {
|
||||||
val mockContext = mockk<Application>(relaxed = true)
|
val mockContext = mock<Application>(MockMode.autofill)
|
||||||
val service = AndroidFileService(mockContext)
|
val service = AndroidFileService(mockContext)
|
||||||
assertNotNull(service)
|
assertNotNull(service)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,8 @@
|
||||||
package org.meshtastic.core.service
|
package org.meshtastic.core.service
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import io.mockk.mockk
|
import dev.mokkery.MockMode
|
||||||
|
import dev.mokkery.mock
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Assert.assertNotNull
|
import org.junit.Assert.assertNotNull
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
@ -26,8 +27,8 @@ import org.meshtastic.core.repository.LocationRepository
|
||||||
class AndroidLocationServiceTest {
|
class AndroidLocationServiceTest {
|
||||||
@Test
|
@Test
|
||||||
fun testInitialization() = runTest {
|
fun testInitialization() = runTest {
|
||||||
val mockContext = mockk<Application>(relaxed = true)
|
val mockContext = mock<Application>(MockMode.autofill)
|
||||||
val mockRepo = mockk<LocationRepository>(relaxed = true)
|
val mockRepo = mock<LocationRepository>(MockMode.autofill)
|
||||||
val service = AndroidLocationService(mockContext, mockRepo)
|
val service = AndroidLocationService(mockContext, mockRepo)
|
||||||
assertNotNull(service)
|
assertNotNull(service)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,13 @@
|
||||||
package org.meshtastic.core.service
|
package org.meshtastic.core.service
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import io.mockk.every
|
import dev.mokkery.MockMode
|
||||||
import io.mockk.mockk
|
import dev.mokkery.answering.returns
|
||||||
import io.mockk.verify
|
import dev.mokkery.every
|
||||||
|
import dev.mokkery.matcher.any
|
||||||
|
import dev.mokkery.mock
|
||||||
|
import dev.mokkery.verify
|
||||||
|
import dev.mokkery.verify.VerifyMode
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
@ -40,13 +44,12 @@ class AndroidNotificationManagerTest {
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
context = mockk(relaxed = true)
|
context = mock(MockMode.autofill)
|
||||||
notificationManager = mockk(relaxed = true)
|
notificationManager = mock(MockMode.autofill)
|
||||||
prefs = mockk {
|
prefs = mock(MockMode.autofill)
|
||||||
every { messagesEnabled } returns this@AndroidNotificationManagerTest.messagesEnabled
|
every { prefs.messagesEnabled } returns this@AndroidNotificationManagerTest.messagesEnabled
|
||||||
every { nodeEventsEnabled } returns this@AndroidNotificationManagerTest.nodeEventsEnabled
|
every { prefs.nodeEventsEnabled } returns this@AndroidNotificationManagerTest.nodeEventsEnabled
|
||||||
every { lowBatteryEnabled } returns this@AndroidNotificationManagerTest.lowBatteryEnabled
|
every { prefs.lowBatteryEnabled } returns this@AndroidNotificationManagerTest.lowBatteryEnabled
|
||||||
}
|
|
||||||
|
|
||||||
every { context.getSystemService(Context.NOTIFICATION_SERVICE) } returns notificationManager
|
every { context.getSystemService(Context.NOTIFICATION_SERVICE) } returns notificationManager
|
||||||
every { context.packageName } returns "org.meshtastic.test"
|
every { context.packageName } returns "org.meshtastic.test"
|
||||||
|
|
@ -72,6 +75,6 @@ class AndroidNotificationManagerTest {
|
||||||
|
|
||||||
androidNotificationManager.dispatch(notification)
|
androidNotificationManager.dispatch(notification)
|
||||||
|
|
||||||
verify(exactly = 0) { notificationManager.notify(any(), any()) }
|
verify(VerifyMode.exactly(0)) { notificationManager.notify(any(), any()) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,12 +22,14 @@ import androidx.work.ListenableWorker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import androidx.work.testing.TestListenableWorkerBuilder
|
import androidx.work.testing.TestListenableWorkerBuilder
|
||||||
import androidx.work.workDataOf
|
import androidx.work.workDataOf
|
||||||
import io.mockk.Runs
|
import dev.mokkery.MockMode
|
||||||
import io.mockk.coEvery
|
import dev.mokkery.every
|
||||||
import io.mockk.coVerify
|
import dev.mokkery.everySuspend
|
||||||
import io.mockk.every
|
import dev.mokkery.matcher.any
|
||||||
import io.mockk.just
|
import dev.mokkery.mock
|
||||||
import io.mockk.mockk
|
import dev.mokkery.verify
|
||||||
|
import dev.mokkery.verify.VerifyMode
|
||||||
|
import dev.mokkery.verifySuspend
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import okio.ByteString.Companion.toByteString
|
import okio.ByteString.Companion.toByteString
|
||||||
|
|
@ -52,8 +54,8 @@ class SendMessageWorkerTest {
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
context = ApplicationProvider.getApplicationContext()
|
context = ApplicationProvider.getApplicationContext()
|
||||||
packetRepository = mockk(relaxed = true)
|
packetRepository = mock(MockMode.autofill)
|
||||||
radioController = mockk(relaxed = true)
|
radioController = mock(MockMode.autofill)
|
||||||
every { radioController.connectionState } returns MutableStateFlow(ConnectionState.Connected)
|
every { radioController.connectionState } returns MutableStateFlow(ConnectionState.Connected)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -62,10 +64,10 @@ class SendMessageWorkerTest {
|
||||||
// Arrange
|
// Arrange
|
||||||
val packetId = 12345
|
val packetId = 12345
|
||||||
val dataPacket = DataPacket(to = "dest", bytes = "Hello".encodeToByteArray().toByteString(), dataType = 0)
|
val dataPacket = DataPacket(to = "dest", bytes = "Hello".encodeToByteArray().toByteString(), dataType = 0)
|
||||||
coEvery { packetRepository.getPacketByPacketId(packetId) } returns dataPacket
|
everySuspend { packetRepository.getPacketByPacketId(packetId) } returns dataPacket
|
||||||
every { radioController.connectionState } returns MutableStateFlow(ConnectionState.Connected)
|
every { radioController.connectionState } returns MutableStateFlow(ConnectionState.Connected)
|
||||||
coEvery { radioController.sendMessage(any()) } just Runs
|
everySuspend { radioController.sendMessage(any()) } returns Unit
|
||||||
coEvery { packetRepository.updateMessageStatus(any(), any()) } just Runs
|
everySuspend { packetRepository.updateMessageStatus(any(), any()) } returns Unit
|
||||||
|
|
||||||
val worker =
|
val worker =
|
||||||
TestListenableWorkerBuilder<SendMessageWorker>(context)
|
TestListenableWorkerBuilder<SendMessageWorker>(context)
|
||||||
|
|
@ -87,8 +89,8 @@ class SendMessageWorkerTest {
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assertEquals(ListenableWorker.Result.success(), result)
|
assertEquals(ListenableWorker.Result.success(), result)
|
||||||
coVerify { radioController.sendMessage(dataPacket) }
|
verifySuspend { radioController.sendMessage(dataPacket) }
|
||||||
coVerify { packetRepository.updateMessageStatus(dataPacket, MessageStatus.ENROUTE) }
|
verifySuspend { packetRepository.updateMessageStatus(dataPacket, MessageStatus.ENROUTE) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -96,7 +98,7 @@ class SendMessageWorkerTest {
|
||||||
// Arrange
|
// Arrange
|
||||||
val packetId = 12345
|
val packetId = 12345
|
||||||
val dataPacket = DataPacket(to = "dest", bytes = "Hello".encodeToByteArray().toByteString(), dataType = 0)
|
val dataPacket = DataPacket(to = "dest", bytes = "Hello".encodeToByteArray().toByteString(), dataType = 0)
|
||||||
coEvery { packetRepository.getPacketByPacketId(packetId) } returns dataPacket
|
everySuspend { packetRepository.getPacketByPacketId(packetId) } returns dataPacket
|
||||||
every { radioController.connectionState } returns MutableStateFlow(ConnectionState.Disconnected)
|
every { radioController.connectionState } returns MutableStateFlow(ConnectionState.Disconnected)
|
||||||
|
|
||||||
val worker =
|
val worker =
|
||||||
|
|
@ -119,14 +121,14 @@ class SendMessageWorkerTest {
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assertEquals(ListenableWorker.Result.retry(), result)
|
assertEquals(ListenableWorker.Result.retry(), result)
|
||||||
coVerify(exactly = 0) { radioController.sendMessage(any()) }
|
verifySuspend(mode = VerifyMode.exactly(0)) { radioController.sendMessage(any()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `doWork returns failure when packet is missing`() = runTest {
|
fun `doWork returns failure when packet is missing`() = runTest {
|
||||||
// Arrange
|
// Arrange
|
||||||
val packetId = 999
|
val packetId = 999
|
||||||
coEvery { packetRepository.getPacketByPacketId(packetId) } returns null
|
everySuspend { packetRepository.getPacketByPacketId(packetId) } returns null
|
||||||
|
|
||||||
val worker =
|
val worker =
|
||||||
TestListenableWorkerBuilder<SendMessageWorker>(context)
|
TestListenableWorkerBuilder<SendMessageWorker>(context)
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,10 @@ package org.meshtastic.core.service
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.test.core.app.ApplicationProvider
|
import androidx.test.core.app.ApplicationProvider
|
||||||
import io.mockk.every
|
import dev.mokkery.MockMode
|
||||||
import io.mockk.mockk
|
import dev.mokkery.answering.returns
|
||||||
|
import dev.mokkery.every
|
||||||
|
import dev.mokkery.mock
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
|
|
@ -35,7 +37,7 @@ import org.robolectric.Shadows.shadowOf
|
||||||
class ServiceBroadcastsTest {
|
class ServiceBroadcastsTest {
|
||||||
|
|
||||||
private lateinit var context: Context
|
private lateinit var context: Context
|
||||||
private val serviceRepository: ServiceRepository = mockk(relaxed = true)
|
private val serviceRepository: ServiceRepository = mock(MockMode.autofill)
|
||||||
private lateinit var broadcasts: ServiceBroadcasts
|
private lateinit var broadcasts: ServiceBroadcasts
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,14 @@ import android.content.Intent
|
||||||
import android.content.ServiceConnection
|
import android.content.ServiceConnection
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.IInterface
|
import android.os.IInterface
|
||||||
import io.mockk.every
|
import dev.mokkery.MockMode
|
||||||
import io.mockk.mockk
|
import dev.mokkery.every
|
||||||
import io.mockk.slot
|
import dev.mokkery.matcher.any
|
||||||
import io.mockk.verify
|
import dev.mokkery.matcher.capture.Capture
|
||||||
|
import dev.mokkery.matcher.capture.capture
|
||||||
|
import dev.mokkery.mock
|
||||||
|
import dev.mokkery.verify
|
||||||
|
import dev.mokkery.verify.exactly
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Assert.assertNotNull
|
import org.junit.Assert.assertNotNull
|
||||||
|
|
@ -41,51 +45,54 @@ class ServiceClientTest {
|
||||||
|
|
||||||
interface MyInterface : IInterface
|
interface MyInterface : IInterface
|
||||||
|
|
||||||
private val stubFactory: (IBinder) -> MyInterface = { _ -> mockk<MyInterface>() }
|
private val stubFactory: (IBinder) -> MyInterface = { _ -> mock<MyInterface>() }
|
||||||
private val client = ServiceClient(stubFactory)
|
private val client = ServiceClient(stubFactory)
|
||||||
private val context = mockk<Context>(relaxed = true)
|
private val context = mock<Context>(MockMode.autofill)
|
||||||
private val intent = mockk<Intent>()
|
private val intent = mock<Intent>()
|
||||||
private val binder = mockk<IBinder>()
|
private val binder = mock<IBinder>()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `connect binds service successfully`() = runTest {
|
fun `connect binds service successfully`() = runTest {
|
||||||
val slot = slot<ServiceConnection>()
|
val slot = Capture.slot<ServiceConnection>()
|
||||||
every { context.bindService(any<Intent>(), capture(slot), any<Int>()) } returns true
|
every { context.bindService(any(), capture(slot), any()) } returns true
|
||||||
|
|
||||||
client.connect(context, intent, 0)
|
client.connect(context, intent, 0)
|
||||||
|
|
||||||
verify { context.bindService(intent, any<ServiceConnection>(), 0) }
|
verify { context.bindService(intent, any(), 0) }
|
||||||
|
|
||||||
// Simulate connection
|
// Simulate connection
|
||||||
if (slot.isCaptured) {
|
try {
|
||||||
slot.captured.onServiceConnected(ComponentName("pkg", "cls"), binder)
|
slot.get().onServiceConnected(ComponentName("pkg", "cls"), binder)
|
||||||
assertNotNull(client.serviceP)
|
assertNotNull(client.serviceP)
|
||||||
} else {
|
} catch (e: NoSuchElementException) {
|
||||||
fail("ServiceConnection was not captured")
|
fail("ServiceConnection was not captured")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `connect retries on failure`() = runTest {
|
fun `connect retries on failure`() = runTest {
|
||||||
val slot = slot<ServiceConnection>()
|
val slot = Capture.slot<ServiceConnection>()
|
||||||
// First attempt fails, second succeeds
|
// First attempt fails, second succeeds
|
||||||
every { context.bindService(any<Intent>(), capture(slot), any<Int>()) } returnsMany listOf(false, true)
|
every { context.bindService(any(), capture(slot), any()) } sequentially {
|
||||||
|
returns(false)
|
||||||
|
returns(true)
|
||||||
|
}
|
||||||
|
|
||||||
client.connect(context, intent, 0)
|
client.connect(context, intent, 0)
|
||||||
|
|
||||||
verify(exactly = 2) { context.bindService(intent, any<ServiceConnection>(), 0) }
|
verify(exactly(2)) { context.bindService(intent, any(), 0) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(expected = BindFailedException::class)
|
@Test(expected = BindFailedException::class)
|
||||||
fun `connect throws exception after two failures`() = runTest {
|
fun `connect throws exception after two failures`() = runTest {
|
||||||
every { context.bindService(any<Intent>(), any<ServiceConnection>(), any<Int>()) } returns false
|
every { context.bindService(any(), any(), any()) } returns false
|
||||||
client.connect(context, intent, 0)
|
client.connect(context, intent, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `waitConnect blocks until connected`() {
|
fun `waitConnect blocks until connected`() {
|
||||||
val slot = slot<ServiceConnection>()
|
val slot = Capture.slot<ServiceConnection>()
|
||||||
every { context.bindService(any<Intent>(), capture(slot), any<Int>()) } returns true
|
every { context.bindService(any(), capture(slot), any()) } returns true
|
||||||
|
|
||||||
// Run connect in a coroutine scope (it's suspend)
|
// Run connect in a coroutine scope (it's suspend)
|
||||||
runTest { client.connect(context, intent, 0) }
|
runTest { client.connect(context, intent, 0) }
|
||||||
|
|
@ -102,9 +109,9 @@ class ServiceClientTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simulate connection
|
// Simulate connection
|
||||||
if (slot.isCaptured) {
|
try {
|
||||||
slot.captured.onServiceConnected(ComponentName("pkg", "cls"), binder)
|
slot.get().onServiceConnected(ComponentName("pkg", "cls"), binder)
|
||||||
} else {
|
} catch (e: NoSuchElementException) {
|
||||||
fail("ServiceConnection was not captured")
|
fail("ServiceConnection was not captured")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,16 +125,16 @@ class ServiceClientTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `close unbinds service`() = runTest {
|
fun `close unbinds service`() = runTest {
|
||||||
val slot = slot<ServiceConnection>()
|
val slot = Capture.slot<ServiceConnection>()
|
||||||
every { context.bindService(any<Intent>(), capture(slot), any<Int>()) } returns true
|
every { context.bindService(any(), capture(slot), any()) } returns true
|
||||||
|
|
||||||
client.connect(context, intent, 0)
|
client.connect(context, intent, 0)
|
||||||
|
|
||||||
if (slot.isCaptured) {
|
try {
|
||||||
client.close()
|
client.close()
|
||||||
verify { context.unbindService(slot.captured) }
|
verify { context.unbindService(slot.get()) }
|
||||||
assertNull(client.serviceP)
|
assertNull(client.serviceP)
|
||||||
} else {
|
} catch (e: NoSuchElementException) {
|
||||||
fail("ServiceConnection was not captured")
|
fail("ServiceConnection was not captured")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,16 +45,16 @@ The `:core:testing` module provides lightweight, reusable test doubles (fakes, b
|
||||||
|
|
||||||
### Target Compatibility Warning (March 2026 Audit)
|
### Target Compatibility Warning (March 2026 Audit)
|
||||||
|
|
||||||
- **MockK in commonMain:** This module includes `api(libs.mockk)` in `commonMain`. While this works for the current `jvm()` and `android()` targets, **MockK does not natively support Kotlin/Native (iOS)**.
|
- **MockK Removal:** MockK has been removed from `commonMain` because it does not natively support Kotlin/Native (iOS).
|
||||||
- **Future-Proofing:** If an iOS target is added, tests in `commonTest` that rely on MockK will fail to compile for iOS.
|
- **Future-Proofing:** The project is migrating to `dev.mokkery` for KMP-compatible mocking or favoring manual fakes.
|
||||||
- **Recommendation:** Favor manual fakes (like `FakeNodeRepository`) in `commonMain` and limit `mockk` usage to `androidUnitTest` or `jvmTest` where possible to maintain pure KMP portability.
|
- **Recommendation:** Favor manual fakes (like `FakeNodeRepository`) in `commonMain` to maintain pure KMP portability.
|
||||||
|
|
||||||
### Key Design Rules
|
### Key Design Rules
|
||||||
|
|
||||||
1. **`:core:testing` has NO dependencies on heavy modules**: It only depends on:
|
1. **`:core:testing` has NO dependencies on heavy modules**: It only depends on:
|
||||||
- `core:model` — Domain types (Node, User, etc.)
|
- `core:model` — Domain types (Node, User, etc.)
|
||||||
- `core:repository` — Interfaces (NodeRepository, etc.)
|
- `core:repository` — Interfaces (NodeRepository, etc.)
|
||||||
- Test libraries (`kotlin("test")`, `mockk`, `kotlinx.coroutines.test`, `turbine`, `junit`)
|
- Test libraries (`kotlin("test")`, `kotlinx.coroutines.test`, `turbine`, `junit`)
|
||||||
|
|
||||||
2. **No circular dependencies**: Modules that depend on `:core:testing` (in `commonTest`) cannot be dependencies of `:core:testing` itself.
|
2. **No circular dependencies**: Modules that depend on `:core:testing` (in `commonTest`) cannot be dependencies of `:core:testing` itself.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ fun interface ComposableContent {
|
||||||
* direct dependencies on UI components.
|
* direct dependencies on UI components.
|
||||||
*/
|
*/
|
||||||
@Single
|
@Single
|
||||||
class AlertManager {
|
open class AlertManager {
|
||||||
data class AlertData(
|
data class AlertData(
|
||||||
val title: String? = null,
|
val title: String? = null,
|
||||||
val titleRes: StringResource? = null,
|
val titleRes: StringResource? = null,
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ Created `core:testing` as a lightweight, reusable module for **shared test doubl
|
||||||
```
|
```
|
||||||
core:testing
|
core:testing
|
||||||
├── depends on: core:model, core:repository
|
├── depends on: core:model, core:repository
|
||||||
├── depends on: kotlin("test"), mockk, kotlinx.coroutines.test, turbine, junit
|
├── depends on: kotlin("test"), kotlinx.coroutines.test, turbine, junit
|
||||||
└── does NOT depend on: core:database, core:data, core:domain
|
└── does NOT depend on: core:database, core:data, core:domain
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,9 +36,9 @@ KMP Migration Timeline
|
||||||
### Before KMP Testing Consolidation
|
### Before KMP Testing Consolidation
|
||||||
```
|
```
|
||||||
Each module had scattered test dependencies:
|
Each module had scattered test dependencies:
|
||||||
feature:messaging → libs.junit, libs.mockk, libs.turbine
|
feature:messaging → libs.junit, libs.turbine
|
||||||
feature:node → libs.junit, libs.mockk, libs.turbine
|
feature:node → libs.junit, libs.turbine
|
||||||
core:domain → libs.junit, libs.mockk, libs.turbine
|
core:domain → libs.junit, libs.turbine
|
||||||
↓
|
↓
|
||||||
Result: Duplication, inconsistency, hard to maintain
|
Result: Duplication, inconsistency, hard to maintain
|
||||||
Problem: New developers don't know testing patterns
|
Problem: New developers don't know testing patterns
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,8 @@ open class ScannerViewModel(
|
||||||
private val getDiscoveredDevicesUseCase: GetDiscoveredDevicesUseCase,
|
private val getDiscoveredDevicesUseCase: GetDiscoveredDevicesUseCase,
|
||||||
private val bleScanner: org.meshtastic.core.ble.BleScanner? = null,
|
private val bleScanner: org.meshtastic.core.ble.BleScanner? = null,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
val showMockInterface: StateFlow<Boolean> = MutableStateFlow(radioInterfaceService.isMockInterface()).asStateFlow()
|
private val _showMockInterface = MutableStateFlow(false)
|
||||||
|
val showMockInterface: StateFlow<Boolean> = _showMockInterface.asStateFlow()
|
||||||
|
|
||||||
private val _errorText = MutableStateFlow<String?>(null)
|
private val _errorText = MutableStateFlow<String?>(null)
|
||||||
val errorText: StateFlow<String?> = _errorText.asStateFlow()
|
val errorText: StateFlow<String?> = _errorText.asStateFlow()
|
||||||
|
|
@ -65,6 +66,10 @@ open class ScannerViewModel(
|
||||||
|
|
||||||
private var scanJob: kotlinx.coroutines.Job? = null
|
private var scanJob: kotlinx.coroutines.Job? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
_showMockInterface.value = radioInterfaceService.isMockInterface()
|
||||||
|
}
|
||||||
|
|
||||||
fun startBleScan() {
|
fun startBleScan() {
|
||||||
if (isBleScanningState.value || bleScanner == null) return
|
if (isBleScanningState.value || bleScanner == null) return
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,9 @@ import android.app.Application
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import com.google.android.gms.maps.model.UrlTileProvider
|
import com.google.android.gms.maps.model.UrlTileProvider
|
||||||
import io.mockk.every
|
import dev.mokkery.MockMode
|
||||||
import io.mockk.mockk
|
import dev.mokkery.every
|
||||||
import io.mockk.mockkStatic
|
import dev.mokkery.mock
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
@ -54,15 +54,15 @@ import org.robolectric.RobolectricTestRunner
|
||||||
@RunWith(RobolectricTestRunner::class)
|
@RunWith(RobolectricTestRunner::class)
|
||||||
class MapViewModelTest {
|
class MapViewModelTest {
|
||||||
|
|
||||||
private val application = mockk<Application>(relaxed = true)
|
private val application = mock<Application>(MockMode.autofill)
|
||||||
private val mapPrefs = mockk<MapPrefs>(relaxed = true)
|
private val mapPrefs = mock<MapPrefs>(MockMode.autofill)
|
||||||
private val googleMapsPrefs = mockk<GoogleMapsPrefs>(relaxed = true)
|
private val googleMapsPrefs = mock<GoogleMapsPrefs>(MockMode.autofill)
|
||||||
private val nodeRepository = mockk<NodeRepository>(relaxed = true)
|
private val nodeRepository = mock<NodeRepository>(MockMode.autofill)
|
||||||
private val packetRepository = mockk<PacketRepository>(relaxed = true)
|
private val packetRepository = mock<PacketRepository>(MockMode.autofill)
|
||||||
private val radioConfigRepository = mockk<RadioConfigRepository>(relaxed = true)
|
private val radioConfigRepository = mock<RadioConfigRepository>(MockMode.autofill)
|
||||||
private val radioController = mockk<RadioController>(relaxed = true)
|
private val radioController = mock<RadioController>(MockMode.autofill)
|
||||||
private val customTileProviderRepository = mockk<CustomTileProviderRepository>(relaxed = true)
|
private val customTileProviderRepository = mock<CustomTileProviderRepository>(MockMode.autofill)
|
||||||
private val uiPreferencesDataSource = mockk<UiPreferencesDataSource>(relaxed = true)
|
private val uiPreferencesDataSource = mock<UiPreferencesDataSource>(MockMode.autofill)
|
||||||
private val savedStateHandle = SavedStateHandle(mapOf("waypointId" to null))
|
private val savedStateHandle = SavedStateHandle(mapOf("waypointId" to null))
|
||||||
|
|
||||||
private val testDispatcher = StandardTestDispatcher()
|
private val testDispatcher = StandardTestDispatcher()
|
||||||
|
|
@ -89,7 +89,7 @@ class MapViewModelTest {
|
||||||
every { googleMapsPrefs.hiddenLayerUrls } returns MutableStateFlow(emptySet())
|
every { googleMapsPrefs.hiddenLayerUrls } returns MutableStateFlow(emptySet())
|
||||||
|
|
||||||
every { customTileProviderRepository.getCustomTileProviders() } returns flowOf(emptyList())
|
every { customTileProviderRepository.getCustomTileProviders() } returns flowOf(emptyList())
|
||||||
every { radioConfigRepository.deviceProfileFlow } returns flowOf(mockk(relaxed = true))
|
every { radioConfigRepository.deviceProfileFlow } returns flowOf(mock(MockMode.autofill))
|
||||||
every { uiPreferencesDataSource.theme } returns MutableStateFlow(1)
|
every { uiPreferencesDataSource.theme } returns MutableStateFlow(1)
|
||||||
every { nodeRepository.myNodeInfo } returns MutableStateFlow(null)
|
every { nodeRepository.myNodeInfo } returns MutableStateFlow(null)
|
||||||
every { nodeRepository.ourNodeInfo } returns MutableStateFlow(null)
|
every { nodeRepository.ourNodeInfo } returns MutableStateFlow(null)
|
||||||
|
|
@ -133,13 +133,6 @@ class MapViewModelTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `addNetworkMapLayer detects GeoJSON based on extension`() = runTest(testDispatcher) {
|
fun `addNetworkMapLayer detects GeoJSON based on extension`() = runTest(testDispatcher) {
|
||||||
mockkStatic(Uri::class)
|
|
||||||
val mockUri = mockk<Uri>()
|
|
||||||
every { Uri.parse("https://example.com/data.geojson") } returns mockUri
|
|
||||||
every { mockUri.scheme } returns "https"
|
|
||||||
every { mockUri.path } returns "/data.geojson"
|
|
||||||
every { mockUri.toString() } returns "https://example.com/data.geojson"
|
|
||||||
|
|
||||||
viewModel.addNetworkMapLayer("Test Layer", "https://example.com/data.geojson")
|
viewModel.addNetworkMapLayer("Test Layer", "https://example.com/data.geojson")
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
|
@ -149,13 +142,6 @@ class MapViewModelTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `addNetworkMapLayer defaults to KML for other extensions`() = runTest(testDispatcher) {
|
fun `addNetworkMapLayer defaults to KML for other extensions`() = runTest(testDispatcher) {
|
||||||
mockkStatic(Uri::class)
|
|
||||||
val mockUri = mockk<Uri>()
|
|
||||||
every { Uri.parse("https://example.com/map.kml") } returns mockUri
|
|
||||||
every { mockUri.scheme } returns "https"
|
|
||||||
every { mockUri.path } returns "/map.kml"
|
|
||||||
every { mockUri.toString() } returns "https://example.com/map.kml"
|
|
||||||
|
|
||||||
viewModel.addNetworkMapLayer("Test KML", "https://example.com/map.kml")
|
viewModel.addNetworkMapLayer("Test KML", "https://example.com/map.kml")
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,14 +43,14 @@ import org.meshtastic.core.resources.unmute
|
||||||
import org.meshtastic.core.ui.util.AlertManager
|
import org.meshtastic.core.ui.util.AlertManager
|
||||||
|
|
||||||
@Single
|
@Single
|
||||||
class NodeManagementActions
|
open class NodeManagementActions
|
||||||
constructor(
|
constructor(
|
||||||
private val nodeRepository: NodeRepository,
|
private val nodeRepository: NodeRepository,
|
||||||
private val serviceRepository: ServiceRepository,
|
private val serviceRepository: ServiceRepository,
|
||||||
private val radioController: RadioController,
|
private val radioController: RadioController,
|
||||||
private val alertManager: AlertManager,
|
private val alertManager: AlertManager,
|
||||||
) {
|
) {
|
||||||
fun requestRemoveNode(scope: CoroutineScope, node: Node) {
|
open fun requestRemoveNode(scope: CoroutineScope, node: Node) {
|
||||||
alertManager.showAlert(
|
alertManager.showAlert(
|
||||||
titleRes = Res.string.remove,
|
titleRes = Res.string.remove,
|
||||||
messageRes = Res.string.remove_node_text,
|
messageRes = Res.string.remove_node_text,
|
||||||
|
|
@ -58,7 +58,7 @@ constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeNode(scope: CoroutineScope, nodeNum: Int) {
|
open fun removeNode(scope: CoroutineScope, nodeNum: Int) {
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
Logger.i { "Removing node '$nodeNum'" }
|
Logger.i { "Removing node '$nodeNum'" }
|
||||||
val packetId = radioController.getPacketId()
|
val packetId = radioController.getPacketId()
|
||||||
|
|
@ -67,7 +67,7 @@ constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun requestIgnoreNode(scope: CoroutineScope, node: Node) {
|
open fun requestIgnoreNode(scope: CoroutineScope, node: Node) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val message =
|
val message =
|
||||||
getString(if (node.isIgnored) Res.string.ignore_remove else Res.string.ignore_add, node.user.long_name)
|
getString(if (node.isIgnored) Res.string.ignore_remove else Res.string.ignore_add, node.user.long_name)
|
||||||
|
|
@ -79,11 +79,11 @@ constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ignoreNode(scope: CoroutineScope, node: Node) {
|
open fun ignoreNode(scope: CoroutineScope, node: Node) {
|
||||||
scope.launch(Dispatchers.IO) { serviceRepository.onServiceAction(ServiceAction.Ignore(node)) }
|
scope.launch(Dispatchers.IO) { serviceRepository.onServiceAction(ServiceAction.Ignore(node)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun requestMuteNode(scope: CoroutineScope, node: Node) {
|
open fun requestMuteNode(scope: CoroutineScope, node: Node) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val message =
|
val message =
|
||||||
getString(if (node.isMuted) Res.string.mute_remove else Res.string.mute_add, node.user.long_name)
|
getString(if (node.isMuted) Res.string.mute_remove else Res.string.mute_add, node.user.long_name)
|
||||||
|
|
@ -95,11 +95,11 @@ constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun muteNode(scope: CoroutineScope, node: Node) {
|
open fun muteNode(scope: CoroutineScope, node: Node) {
|
||||||
scope.launch(Dispatchers.IO) { serviceRepository.onServiceAction(ServiceAction.Mute(node)) }
|
scope.launch(Dispatchers.IO) { serviceRepository.onServiceAction(ServiceAction.Mute(node)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun requestFavoriteNode(scope: CoroutineScope, node: Node) {
|
open fun requestFavoriteNode(scope: CoroutineScope, node: Node) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val message =
|
val message =
|
||||||
getString(
|
getString(
|
||||||
|
|
@ -114,11 +114,11 @@ constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun favoriteNode(scope: CoroutineScope, node: Node) {
|
open fun favoriteNode(scope: CoroutineScope, node: Node) {
|
||||||
scope.launch(Dispatchers.IO) { serviceRepository.onServiceAction(ServiceAction.Favorite(node)) }
|
scope.launch(Dispatchers.IO) { serviceRepository.onServiceAction(ServiceAction.Favorite(node)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setNodeNotes(scope: CoroutineScope, nodeNum: Int, notes: String) {
|
open fun setNodeNotes(scope: CoroutineScope, nodeNum: Int, notes: String) {
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
nodeRepository.setNodeNotes(nodeNum, notes)
|
nodeRepository.setNodeNotes(nodeNum, notes)
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,9 @@ import org.meshtastic.feature.node.model.isEffectivelyUnmessageable
|
||||||
import org.meshtastic.proto.Config
|
import org.meshtastic.proto.Config
|
||||||
|
|
||||||
@Single
|
@Single
|
||||||
class GetFilteredNodesUseCase constructor(private val nodeRepository: NodeRepository) {
|
open class GetFilteredNodesUseCase constructor(private val nodeRepository: NodeRepository) {
|
||||||
@Suppress("CyclomaticComplexMethod", "LongMethod")
|
@Suppress("CyclomaticComplexMethod", "LongMethod")
|
||||||
operator fun invoke(filter: NodeFilterState, sort: NodeSortOption): Flow<List<Node>> = nodeRepository
|
open operator fun invoke(filter: NodeFilterState, sort: NodeSortOption): Flow<List<Node>> = nodeRepository
|
||||||
.getNodes(
|
.getNodes(
|
||||||
sort = sort,
|
sort = sort,
|
||||||
filter = filter.filterText,
|
filter = filter.filterText,
|
||||||
|
|
|
||||||
|
|
@ -18,46 +18,46 @@ package org.meshtastic.feature.node.list
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import org.koin.core.annotation.Single
|
import org.koin.core.annotation.Single
|
||||||
import org.meshtastic.core.datastore.UiPreferencesDataSource
|
import org.meshtastic.core.common.UiPreferences
|
||||||
import org.meshtastic.core.model.NodeSortOption
|
import org.meshtastic.core.model.NodeSortOption
|
||||||
|
|
||||||
@Single
|
@Single
|
||||||
class NodeFilterPreferences constructor(private val uiPreferencesDataSource: UiPreferencesDataSource) {
|
open class NodeFilterPreferences constructor(private val uiPreferences: UiPreferences) {
|
||||||
val includeUnknown = uiPreferencesDataSource.includeUnknown
|
open val includeUnknown = uiPreferences.includeUnknown
|
||||||
val excludeInfrastructure = uiPreferencesDataSource.excludeInfrastructure
|
open val excludeInfrastructure = uiPreferences.excludeInfrastructure
|
||||||
val onlyOnline = uiPreferencesDataSource.onlyOnline
|
open val onlyOnline = uiPreferences.onlyOnline
|
||||||
val onlyDirect = uiPreferencesDataSource.onlyDirect
|
open val onlyDirect = uiPreferences.onlyDirect
|
||||||
val showIgnored = uiPreferencesDataSource.showIgnored
|
open val showIgnored = uiPreferences.showIgnored
|
||||||
val excludeMqtt = uiPreferencesDataSource.excludeMqtt
|
open val excludeMqtt = uiPreferences.excludeMqtt
|
||||||
|
|
||||||
val nodeSortOption =
|
open val nodeSortOption =
|
||||||
uiPreferencesDataSource.nodeSort.map { NodeSortOption.entries.getOrElse(it) { NodeSortOption.VIA_FAVORITE } }
|
uiPreferences.nodeSort.map { NodeSortOption.entries.getOrElse(it) { NodeSortOption.VIA_FAVORITE } }
|
||||||
|
|
||||||
fun setNodeSort(option: NodeSortOption) {
|
open fun setNodeSort(option: NodeSortOption) {
|
||||||
uiPreferencesDataSource.setNodeSort(option.ordinal)
|
uiPreferences.setNodeSort(option.ordinal)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggleIncludeUnknown() {
|
open fun toggleIncludeUnknown() {
|
||||||
uiPreferencesDataSource.setIncludeUnknown(!includeUnknown.value)
|
uiPreferences.setIncludeUnknown(!includeUnknown.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggleExcludeInfrastructure() {
|
open fun toggleExcludeInfrastructure() {
|
||||||
uiPreferencesDataSource.setExcludeInfrastructure(!excludeInfrastructure.value)
|
uiPreferences.setExcludeInfrastructure(!excludeInfrastructure.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggleOnlyOnline() {
|
open fun toggleOnlyOnline() {
|
||||||
uiPreferencesDataSource.setOnlyOnline(!onlyOnline.value)
|
uiPreferences.setOnlyOnline(!onlyOnline.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggleOnlyDirect() {
|
open fun toggleOnlyDirect() {
|
||||||
uiPreferencesDataSource.setOnlyDirect(!onlyDirect.value)
|
uiPreferences.setOnlyDirect(!onlyDirect.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggleShowIgnored() {
|
open fun toggleShowIgnored() {
|
||||||
uiPreferencesDataSource.setShowIgnored(!showIgnored.value)
|
uiPreferences.setShowIgnored(!showIgnored.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggleExcludeMqtt() {
|
open fun toggleExcludeMqtt() {
|
||||||
uiPreferencesDataSource.setExcludeMqtt(!excludeMqtt.value)
|
uiPreferences.setExcludeMqtt(!excludeMqtt.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,14 +16,18 @@
|
||||||
*/
|
*/
|
||||||
package org.meshtastic.feature.node.list
|
package org.meshtastic.feature.node.list
|
||||||
|
|
||||||
import io.kotest.matchers.shouldBe
|
|
||||||
|
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import kotlinx.coroutines.Dispatchers
|
import app.cash.turbine.test
|
||||||
|
import dev.mokkery.MockMode
|
||||||
|
import dev.mokkery.answering.returns
|
||||||
|
import dev.mokkery.every
|
||||||
|
import dev.mokkery.mock
|
||||||
|
import dev.mokkery.matcher.any
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.test.resetMain
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import kotlinx.coroutines.test.setMain
|
import org.meshtastic.core.model.ConnectionState
|
||||||
|
import org.meshtastic.core.model.Node
|
||||||
|
import org.meshtastic.core.model.NodeSortOption
|
||||||
import org.meshtastic.core.repository.RadioConfigRepository
|
import org.meshtastic.core.repository.RadioConfigRepository
|
||||||
import org.meshtastic.core.repository.ServiceRepository
|
import org.meshtastic.core.repository.ServiceRepository
|
||||||
import org.meshtastic.core.testing.FakeNodeRepository
|
import org.meshtastic.core.testing.FakeNodeRepository
|
||||||
|
|
@ -34,96 +38,85 @@ import org.meshtastic.feature.node.domain.usecase.GetFilteredNodesUseCase
|
||||||
import kotlin.test.BeforeTest
|
import kotlin.test.BeforeTest
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertTrue
|
import kotlin.test.assertNotNull
|
||||||
|
|
||||||
/**
|
|
||||||
* Bootstrap tests for NodeListViewModel.
|
|
||||||
*
|
|
||||||
* Demonstrates using FakeNodeRepository with a node list feature.
|
|
||||||
*/
|
|
||||||
class NodeListViewModelTest {
|
class NodeListViewModelTest {
|
||||||
/*
|
|
||||||
|
|
||||||
|
|
||||||
private lateinit var viewModel: NodeListViewModel
|
private lateinit var viewModel: NodeListViewModel
|
||||||
private lateinit var nodeRepository: FakeNodeRepository
|
private lateinit var nodeRepository: FakeNodeRepository
|
||||||
private lateinit var radioController: FakeRadioController
|
private lateinit var radioController: FakeRadioController
|
||||||
private lateinit var radioConfigRepository: RadioConfigRepository
|
private val radioConfigRepository: RadioConfigRepository = mock(MockMode.autofill)
|
||||||
private lateinit var serviceRepository: ServiceRepository
|
private val serviceRepository: ServiceRepository = mock(MockMode.autofill)
|
||||||
private lateinit var nodeFilterPreferences: NodeFilterPreferences
|
private val nodeFilterPreferences: NodeFilterPreferences = mock(MockMode.autofill)
|
||||||
private lateinit var nodeManagementActions: NodeManagementActions
|
private val nodeManagementActions: NodeManagementActions = mock(MockMode.autofill)
|
||||||
private lateinit var getFilteredNodesUseCase: GetFilteredNodesUseCase
|
private val getFilteredNodesUseCase: GetFilteredNodesUseCase = mock(MockMode.autofill)
|
||||||
|
|
||||||
@BeforeTest
|
@BeforeTest
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
kotlinx.coroutines.Dispatchers.setMain(kotlinx.coroutines.Dispatchers.Unconfined)
|
|
||||||
// Use real fakes
|
|
||||||
nodeRepository = FakeNodeRepository()
|
nodeRepository = FakeNodeRepository()
|
||||||
radioController = FakeRadioController()
|
radioController = FakeRadioController()
|
||||||
|
|
||||||
// Mock remaining dependencies with explicit types
|
every { radioConfigRepository.localConfigFlow } returns MutableStateFlow(org.meshtastic.proto.LocalConfig())
|
||||||
nodeFilterPreferences =
|
every { radioConfigRepository.deviceProfileFlow } returns MutableStateFlow(org.meshtastic.proto.DeviceProfile())
|
||||||
every { nodeSortOption } returns MutableStateFlow(org.meshtastic.core.model.NodeSortOption.LAST_HEARD)
|
every { serviceRepository.connectionState } returns MutableStateFlow(ConnectionState.Disconnected)
|
||||||
every { includeUnknown } returns MutableStateFlow(true)
|
|
||||||
every { excludeInfrastructure } returns MutableStateFlow(false)
|
every { nodeFilterPreferences.nodeSortOption } returns MutableStateFlow(NodeSortOption.LAST_HEARD)
|
||||||
every { onlyOnline } returns MutableStateFlow(false)
|
every { nodeFilterPreferences.includeUnknown } returns MutableStateFlow(true)
|
||||||
}
|
every { nodeFilterPreferences.excludeInfrastructure } returns MutableStateFlow(false)
|
||||||
@Suppress("UNCHECKED_CAST")
|
every { nodeFilterPreferences.onlyOnline } returns MutableStateFlow(false)
|
||||||
|
every { nodeFilterPreferences.onlyDirect } returns MutableStateFlow(false)
|
||||||
|
every { nodeFilterPreferences.showIgnored } returns MutableStateFlow(false)
|
||||||
|
every { nodeFilterPreferences.excludeMqtt } returns MutableStateFlow(false)
|
||||||
|
|
||||||
viewModel =
|
every { getFilteredNodesUseCase(any(), any()) } returns MutableStateFlow(emptyList())
|
||||||
NodeListViewModel(
|
|
||||||
savedStateHandle = SavedStateHandle(),
|
viewModel = createViewModel()
|
||||||
nodeRepository = nodeRepository,
|
|
||||||
radioConfigRepository = radioConfigRepository,
|
|
||||||
serviceRepository = serviceRepository,
|
|
||||||
radioController = radioController,
|
|
||||||
nodeManagementActions = nodeManagementActions,
|
|
||||||
getFilteredNodesUseCase = getFilteredNodesUseCase,
|
|
||||||
nodeFilterPreferences = nodeFilterPreferences,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@kotlin.test.AfterTest
|
private fun createViewModel() = NodeListViewModel(
|
||||||
fun tearDown() {
|
savedStateHandle = SavedStateHandle(),
|
||||||
kotlinx.coroutines.Dispatchers.resetMain()
|
nodeRepository = nodeRepository,
|
||||||
|
radioConfigRepository = radioConfigRepository,
|
||||||
|
serviceRepository = serviceRepository,
|
||||||
|
radioController = radioController,
|
||||||
|
nodeManagementActions = nodeManagementActions,
|
||||||
|
getFilteredNodesUseCase = getFilteredNodesUseCase,
|
||||||
|
nodeFilterPreferences = nodeFilterPreferences,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testInitialization() {
|
||||||
|
assertNotNull(viewModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testInitialization() = runTest {
|
fun `nodeList emits updates when repository changes`() = runTest {
|
||||||
setUp()
|
val nodesFlow = MutableStateFlow<List<Node>>(emptyList())
|
||||||
// ViewModel should initialize without errors
|
every { getFilteredNodesUseCase(any(), any()) } returns nodesFlow
|
||||||
assertTrue(true, "NodeListViewModel initialized successfully")
|
|
||||||
|
val vm = createViewModel()
|
||||||
|
vm.nodeList.test {
|
||||||
|
// Initial value from stateIn
|
||||||
|
assertEquals(emptyList(), awaitItem())
|
||||||
|
|
||||||
|
// Trigger update
|
||||||
|
val testNodes = TestDataFactory.createTestNodes(3)
|
||||||
|
nodesFlow.value = testNodes
|
||||||
|
|
||||||
|
assertEquals(3, awaitItem().size)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testOurNodeInfoFlow() = runTest {
|
fun `connectionState reflects serviceRepository state`() = runTest {
|
||||||
setUp()
|
val stateFlow = MutableStateFlow<ConnectionState>(ConnectionState.Disconnected)
|
||||||
// Verify ourNodeInfo StateFlow is accessible
|
every { serviceRepository.connectionState } returns stateFlow
|
||||||
val ourNode = viewModel.ourNodeInfo.value
|
|
||||||
assertTrue(ourNode == null, "ourNodeInfo starts as null before connection")
|
val vm = createViewModel()
|
||||||
|
vm.connectionState.test {
|
||||||
|
assertEquals(ConnectionState.Disconnected, awaitItem())
|
||||||
|
stateFlow.value = ConnectionState.Connected
|
||||||
|
assertEquals(ConnectionState.Connected, awaitItem())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testNodeCounts() = runTest {
|
|
||||||
setUp()
|
|
||||||
// Add test nodes to repository
|
|
||||||
val testNodes = TestDataFactory.createTestNodes(3)
|
|
||||||
nodeRepository.setNodes(testNodes)
|
|
||||||
|
|
||||||
// Verify nodes are in repository
|
|
||||||
"Test nodes added to repository" shouldBe 3, nodeRepository.nodeDBbyNum.value.size
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testTotalAndOnlineNodeCounts() = runTest {
|
|
||||||
setUp()
|
|
||||||
// Verify count flows are accessible
|
|
||||||
val totalCount = viewModel.totalNodeCount.value
|
|
||||||
val onlineCount = viewModel.onlineNodeCount.value
|
|
||||||
|
|
||||||
// Both should be accessible without error
|
|
||||||
assertTrue(true, "Node count flows are accessible")
|
|
||||||
}
|
|
||||||
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,10 @@
|
||||||
*/
|
*/
|
||||||
package org.meshtastic.feature.node.detail
|
package org.meshtastic.feature.node.detail
|
||||||
|
|
||||||
import io.mockk.mockk
|
import dev.mokkery.MockMode
|
||||||
import io.mockk.verify
|
import dev.mokkery.matcher.any
|
||||||
|
import dev.mokkery.mock
|
||||||
|
import dev.mokkery.verify
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
|
@ -33,10 +35,10 @@ import org.meshtastic.proto.User
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
class NodeManagementActionsTest {
|
class NodeManagementActionsTest {
|
||||||
|
|
||||||
private val nodeRepository = mockk<NodeRepository>(relaxed = true)
|
private val nodeRepository = mock<NodeRepository>(MockMode.autofill)
|
||||||
private val serviceRepository = mockk<ServiceRepository>(relaxed = true)
|
private val serviceRepository = mock<ServiceRepository>(MockMode.autofill)
|
||||||
private val radioController = mockk<RadioController>(relaxed = true)
|
private val radioController = mock<RadioController>(MockMode.autofill)
|
||||||
private val alertManager = mockk<AlertManager>(relaxed = true)
|
private val alertManager = mock<AlertManager>(MockMode.autofill)
|
||||||
private val testDispatcher = StandardTestDispatcher()
|
private val testDispatcher = StandardTestDispatcher()
|
||||||
private val testScope = TestScope(testDispatcher)
|
private val testScope = TestScope(testDispatcher)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,9 @@
|
||||||
*/
|
*/
|
||||||
package org.meshtastic.feature.node.domain.usecase
|
package org.meshtastic.feature.node.domain.usecase
|
||||||
|
|
||||||
import io.mockk.every
|
import dev.mokkery.every
|
||||||
import io.mockk.mockk
|
import dev.mokkery.matcher.any
|
||||||
|
import dev.mokkery.mock
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
|
@ -38,7 +39,7 @@ class GetFilteredNodesUseCaseTest {
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
nodeRepository = mockk()
|
nodeRepository = mock()
|
||||||
useCase = GetFilteredNodesUseCase(nodeRepository)
|
useCase = GetFilteredNodesUseCase(nodeRepository)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,10 @@
|
||||||
*/
|
*/
|
||||||
package org.meshtastic.feature.settings
|
package org.meshtastic.feature.settings
|
||||||
|
|
||||||
import io.mockk.every
|
import dev.mokkery.MockMode
|
||||||
import io.mockk.mockk
|
import dev.mokkery.every
|
||||||
import io.mockk.verify
|
import dev.mokkery.mock
|
||||||
|
import dev.mokkery.verify
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
@ -52,13 +53,13 @@ class LegacySettingsViewModelTest {
|
||||||
|
|
||||||
private val testDispatcher = StandardTestDispatcher()
|
private val testDispatcher = StandardTestDispatcher()
|
||||||
|
|
||||||
private val radioConfigRepository: RadioConfigRepository = mockk(relaxed = true)
|
private val radioConfigRepository: RadioConfigRepository = mock(MockMode.autofill)
|
||||||
private val radioController: RadioController = mockk(relaxed = true)
|
private val radioController: RadioController = mock(MockMode.autofill)
|
||||||
private val nodeRepository: NodeRepository = mockk(relaxed = true)
|
private val nodeRepository: NodeRepository = mock(MockMode.autofill)
|
||||||
private val uiPrefs: UiPrefs = mockk(relaxed = true)
|
private val uiPrefs: UiPrefs = mock(MockMode.autofill)
|
||||||
private val buildConfigProvider: BuildConfigProvider = mockk(relaxed = true)
|
private val buildConfigProvider: BuildConfigProvider = mock(MockMode.autofill)
|
||||||
private val databaseManager: DatabaseManager = mockk(relaxed = true)
|
private val databaseManager: DatabaseManager = mock(MockMode.autofill)
|
||||||
private val meshLogPrefs: MeshLogPrefs = mockk(relaxed = true)
|
private val meshLogPrefs: MeshLogPrefs = mock(MockMode.autofill)
|
||||||
|
|
||||||
private lateinit var setThemeUseCase: SetThemeUseCase
|
private lateinit var setThemeUseCase: SetThemeUseCase
|
||||||
private lateinit var setAppIntroCompletedUseCase: SetAppIntroCompletedUseCase
|
private lateinit var setAppIntroCompletedUseCase: SetAppIntroCompletedUseCase
|
||||||
|
|
@ -75,14 +76,14 @@ class LegacySettingsViewModelTest {
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
Dispatchers.setMain(testDispatcher)
|
Dispatchers.setMain(testDispatcher)
|
||||||
|
|
||||||
setThemeUseCase = mockk(relaxed = true)
|
setThemeUseCase = mock(MockMode.autofill)
|
||||||
setAppIntroCompletedUseCase = mockk(relaxed = true)
|
setAppIntroCompletedUseCase = mock(MockMode.autofill)
|
||||||
setProvideLocationUseCase = mockk(relaxed = true)
|
setProvideLocationUseCase = mock(MockMode.autofill)
|
||||||
setDatabaseCacheLimitUseCase = mockk(relaxed = true)
|
setDatabaseCacheLimitUseCase = mock(MockMode.autofill)
|
||||||
setMeshLogSettingsUseCase = mockk(relaxed = true)
|
setMeshLogSettingsUseCase = mock(MockMode.autofill)
|
||||||
meshLocationUseCase = mockk(relaxed = true)
|
meshLocationUseCase = mock(MockMode.autofill)
|
||||||
exportDataUseCase = mockk(relaxed = true)
|
exportDataUseCase = mock(MockMode.autofill)
|
||||||
isOtaCapableUseCase = mockk(relaxed = true)
|
isOtaCapableUseCase = mock(MockMode.autofill)
|
||||||
|
|
||||||
// Return real StateFlows to avoid ClassCastException
|
// Return real StateFlows to avoid ClassCastException
|
||||||
every { databaseManager.cacheLimit } returns MutableStateFlow(100)
|
every { databaseManager.cacheLimit } returns MutableStateFlow(100)
|
||||||
|
|
@ -95,7 +96,7 @@ class LegacySettingsViewModelTest {
|
||||||
|
|
||||||
viewModel =
|
viewModel =
|
||||||
SettingsViewModel(
|
SettingsViewModel(
|
||||||
app = mockk(),
|
app = mock(),
|
||||||
radioConfigRepository = radioConfigRepository,
|
radioConfigRepository = radioConfigRepository,
|
||||||
radioController = radioController,
|
radioController = radioController,
|
||||||
nodeRepository = nodeRepository,
|
nodeRepository = nodeRepository,
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,11 @@
|
||||||
*/
|
*/
|
||||||
package org.meshtastic.feature.settings.filter
|
package org.meshtastic.feature.settings.filter
|
||||||
|
|
||||||
import io.mockk.every
|
import dev.mokkery.MockMode
|
||||||
import io.mockk.mockk
|
import dev.mokkery.every
|
||||||
import io.mockk.verify
|
import dev.mokkery.matcher.any
|
||||||
|
import dev.mokkery.mock
|
||||||
|
import dev.mokkery.verify
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
@ -27,8 +29,8 @@ import org.meshtastic.core.repository.MessageFilter
|
||||||
|
|
||||||
class FilterSettingsViewModelTest {
|
class FilterSettingsViewModelTest {
|
||||||
|
|
||||||
private val filterPrefs: FilterPrefs = mockk(relaxed = true)
|
private val filterPrefs: FilterPrefs = mock(MockMode.autofill)
|
||||||
private val messageFilter: MessageFilter = mockk(relaxed = true)
|
private val messageFilter: MessageFilter = mock(MockMode.autofill)
|
||||||
|
|
||||||
private lateinit var viewModel: FilterSettingsViewModel
|
private lateinit var viewModel: FilterSettingsViewModel
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,11 @@
|
||||||
*/
|
*/
|
||||||
package org.meshtastic.feature.settings.radio
|
package org.meshtastic.feature.settings.radio
|
||||||
|
|
||||||
import io.mockk.coEvery
|
import dev.mokkery.MockMode
|
||||||
import io.mockk.coVerify
|
import dev.mokkery.everySuspend
|
||||||
import io.mockk.mockk
|
import dev.mokkery.matcher.any
|
||||||
|
import dev.mokkery.mock
|
||||||
|
import dev.mokkery.verifySuspend
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||||
|
|
@ -45,8 +47,8 @@ class CleanNodeDatabaseViewModelTest {
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
Dispatchers.setMain(testDispatcher)
|
Dispatchers.setMain(testDispatcher)
|
||||||
cleanNodeDatabaseUseCase = mockk(relaxed = true)
|
cleanNodeDatabaseUseCase = mock(MockMode.autofill)
|
||||||
alertManager = mockk(relaxed = true)
|
alertManager = mock(MockMode.autofill)
|
||||||
viewModel = CleanNodeDatabaseViewModel(cleanNodeDatabaseUseCase, alertManager)
|
viewModel = CleanNodeDatabaseViewModel(cleanNodeDatabaseUseCase, alertManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -58,7 +60,7 @@ class CleanNodeDatabaseViewModelTest {
|
||||||
@Test
|
@Test
|
||||||
fun `getNodesToDelete updates state`() = runTest {
|
fun `getNodesToDelete updates state`() = runTest {
|
||||||
val nodes = listOf(Node(num = 1), Node(num = 2))
|
val nodes = listOf(Node(num = 1), Node(num = 2))
|
||||||
coEvery { cleanNodeDatabaseUseCase.getNodesToClean(any(), any(), any()) } returns nodes
|
everySuspend { cleanNodeDatabaseUseCase.getNodesToClean(any(), any(), any()) } returns nodes
|
||||||
|
|
||||||
viewModel.getNodesToDelete()
|
viewModel.getNodesToDelete()
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
|
|
@ -69,14 +71,14 @@ class CleanNodeDatabaseViewModelTest {
|
||||||
@Test
|
@Test
|
||||||
fun `cleanNodes calls useCase and clears state`() = runTest {
|
fun `cleanNodes calls useCase and clears state`() = runTest {
|
||||||
val nodes = listOf(Node(num = 1))
|
val nodes = listOf(Node(num = 1))
|
||||||
coEvery { cleanNodeDatabaseUseCase.getNodesToClean(any(), any(), any()) } returns nodes
|
everySuspend { cleanNodeDatabaseUseCase.getNodesToClean(any(), any(), any()) } returns nodes
|
||||||
viewModel.getNodesToDelete()
|
viewModel.getNodesToDelete()
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
|
|
||||||
viewModel.cleanNodes()
|
viewModel.cleanNodes()
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
|
|
||||||
coVerify { cleanNodeDatabaseUseCase.cleanNodes(listOf(1)) }
|
verifySuspend { cleanNodeDatabaseUseCase.cleanNodes(listOf(1)) }
|
||||||
assertEquals(0, viewModel.nodesToDelete.value.size)
|
assertEquals(0, viewModel.nodesToDelete.value.size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue