diff --git a/conductor/tracks/expand_testing_20260318/plan.md b/conductor/tracks/expand_testing_20260318/plan.md index 915f33553..2db9676bf 100644 --- a/conductor/tracks/expand_testing_20260318/plan.md +++ b/conductor/tracks/expand_testing_20260318/plan.md @@ -25,7 +25,7 @@ ## Phase 5: Final Measurement & Verification - [x] Task: Execute full test suite (`./gradlew test`) to ensure stability. 02fa96f37 - [x] Task: Execute `./gradlew koverLog` to generate and document the final coverage metrics. e3fe4ba1e -- [ ] Task: Conductor - User Manual Verification 'Phase 5: Final Measurement & Verification' (Protocol in workflow.md) +- [~] Task: Conductor - User Manual Verification 'Phase 5: Final Measurement & Verification' (Protocol in workflow.md) ## Phase 6: Documentation and Wrap-up - [ ] Task: Review previous steps and update project documentation (e.g., `README.md`, testing guides). diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 9b28e5bf4..5b6f20ddc 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -63,6 +63,8 @@ kotlin { implementation(libs.junit) implementation(libs.kotlinx.coroutines.test) implementation(libs.turbine) + implementation(libs.kotest.assertions) + implementation(libs.kotest.property) } androidUnitTest.dependencies { implementation(libs.androidx.test.runner) } diff --git a/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/emoji/EmojiPickerViewModelTest.kt b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/emoji/EmojiPickerViewModelTest.kt new file mode 100644 index 000000000..12441b429 --- /dev/null +++ b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/emoji/EmojiPickerViewModelTest.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.ui.emoji + +import dev.mokkery.MockMode +import dev.mokkery.answering.returns +import dev.mokkery.every +import dev.mokkery.matcher.any +import dev.mokkery.mock +import dev.mokkery.verify +import kotlinx.coroutines.flow.MutableStateFlow +import org.meshtastic.core.repository.CustomEmojiPrefs +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +class EmojiPickerViewModelTest { + + private lateinit var viewModel: EmojiPickerViewModel + private val customEmojiPrefs: CustomEmojiPrefs = mock(MockMode.autofill) + private val frequencyFlow = MutableStateFlow(null) + + @BeforeTest + fun setUp() { + every { customEmojiPrefs.customEmojiFrequency } returns frequencyFlow + viewModel = EmojiPickerViewModel(customEmojiPrefs) + } + + @Test + fun testInitialization() { + assertNotNull(viewModel) + } + + @Test + fun `customEmojiFrequency property delegates to prefs`() { + frequencyFlow.value = "👍=10" + assertEquals("👍=10", viewModel.customEmojiFrequency) + + every { customEmojiPrefs.setCustomEmojiFrequency(any()) } returns Unit + viewModel.customEmojiFrequency = "❤️=5" + verify { customEmojiPrefs.setCustomEmojiFrequency("❤️=5") } + } +} diff --git a/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/share/SharedContactViewModelTest.kt b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/share/SharedContactViewModelTest.kt new file mode 100644 index 000000000..b0ab02659 --- /dev/null +++ b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/share/SharedContactViewModelTest.kt @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.ui.share + +import app.cash.turbine.test +import dev.mokkery.MockMode +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.verifySuspend +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.meshtastic.core.model.Node +import org.meshtastic.core.model.service.ServiceAction +import org.meshtastic.core.repository.NodeRepository +import org.meshtastic.core.repository.ServiceRepository +import org.meshtastic.proto.SharedContact +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +@OptIn(ExperimentalCoroutinesApi::class) +class SharedContactViewModelTest { + + private val testDispatcher = StandardTestDispatcher() + private lateinit var viewModel: SharedContactViewModel + private val nodeRepository: NodeRepository = mock(MockMode.autofill) + private val serviceRepository: ServiceRepository = mock(MockMode.autofill) + + @BeforeTest + fun setUp() { + Dispatchers.setMain(testDispatcher) + every { nodeRepository.getNodes() } returns MutableStateFlow(emptyList()) + viewModel = SharedContactViewModel(nodeRepository, serviceRepository) + } + + @AfterTest + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun testInitialization() { + assertNotNull(viewModel) + } + + @Test + fun `unfilteredNodes reflects repository updates`() = runTest { + val nodesFlow = MutableStateFlow>(emptyList()) + every { nodeRepository.getNodes() } returns nodesFlow + + viewModel = SharedContactViewModel(nodeRepository, serviceRepository) + + viewModel.unfilteredNodes.test { + assertEquals(emptyList(), awaitItem()) + val node = Node(num = 123) + nodesFlow.value = listOf(node) + assertEquals(listOf(node), awaitItem()) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `addSharedContact delegates to serviceRepository`() = runTest { + val contact = SharedContact(node_num = 123) + everySuspend { serviceRepository.onServiceAction(any()) } returns Unit + + viewModel.addSharedContact(contact) + testScheduler.runCurrent() + + verifySuspend { serviceRepository.onServiceAction(ServiceAction.ImportContact(contact)) } + } +} diff --git a/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/viewmodel/ConnectionsViewModelTest.kt b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/viewmodel/ConnectionsViewModelTest.kt new file mode 100644 index 000000000..fb271d692 --- /dev/null +++ b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/viewmodel/ConnectionsViewModelTest.kt @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. See + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.ui.viewmodel + +import app.cash.turbine.test +import dev.mokkery.MockMode +import dev.mokkery.answering.returns +import dev.mokkery.every +import dev.mokkery.matcher.any +import dev.mokkery.mock +import dev.mokkery.verify +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.meshtastic.core.repository.NodeRepository +import org.meshtastic.core.repository.RadioConfigRepository +import org.meshtastic.core.repository.ServiceRepository +import org.meshtastic.core.repository.UiPrefs +import org.meshtastic.proto.LocalConfig +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +@OptIn(ExperimentalCoroutinesApi::class) +class ConnectionsViewModelTest { + + private val testDispatcher = StandardTestDispatcher() + private lateinit var viewModel: ConnectionsViewModel + private val radioConfigRepository: RadioConfigRepository = mock(MockMode.autofill) + private val serviceRepository: ServiceRepository = mock(MockMode.autofill) + private val nodeRepository: NodeRepository = mock(MockMode.autofill) + private val uiPrefs: UiPrefs = mock(MockMode.autofill) + + @BeforeTest + fun setUp() { + Dispatchers.setMain(testDispatcher) + + every { radioConfigRepository.localConfigFlow } returns MutableStateFlow(LocalConfig()) + every { serviceRepository.connectionState } returns MutableStateFlow(org.meshtastic.core.model.ConnectionState.Disconnected) + every { nodeRepository.myNodeInfo } returns MutableStateFlow(null) + every { nodeRepository.ourNodeInfo } returns MutableStateFlow(null) + every { uiPrefs.hasShownNotPairedWarning } returns MutableStateFlow(false) + + viewModel = ConnectionsViewModel( + radioConfigRepository = radioConfigRepository, + serviceRepository = serviceRepository, + nodeRepository = nodeRepository, + uiPrefs = uiPrefs, + ) + } + + @AfterTest + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun testInitialization() { + assertNotNull(viewModel) + } + + @Test + fun `suppressNoPairedWarning updates state and prefs`() { + every { uiPrefs.setHasShownNotPairedWarning(any()) } returns Unit + + viewModel.suppressNoPairedWarning() + + assertEquals(true, viewModel.hasShownNotPairedWarning.value) + verify { uiPrefs.setHasShownNotPairedWarning(true) } + } +} diff --git a/feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/QuickChatViewModelTest.kt b/feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/QuickChatViewModelTest.kt new file mode 100644 index 000000000..f4f67886b --- /dev/null +++ b/feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/QuickChatViewModelTest.kt @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.messaging + +import app.cash.turbine.test +import dev.mokkery.MockMode +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.verifySuspend +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.meshtastic.core.database.entity.QuickChatAction +import org.meshtastic.core.repository.QuickChatActionRepository +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +@OptIn(ExperimentalCoroutinesApi::class) +class QuickChatViewModelTest { + + private val testDispatcher = StandardTestDispatcher() + private lateinit var viewModel: QuickChatViewModel + private val quickChatActionRepository: QuickChatActionRepository = mock(MockMode.autofill) + + @BeforeTest + fun setUp() { + Dispatchers.setMain(testDispatcher) + every { quickChatActionRepository.getAllActions() } returns MutableStateFlow(emptyList()) + viewModel = QuickChatViewModel(quickChatActionRepository) + } + + @AfterTest + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun testInitialization() { + assertNotNull(viewModel) + } + + @Test + fun `quickChatActions reflects repository updates`() = runTest { + val actionsFlow = MutableStateFlow>(emptyList()) + every { quickChatActionRepository.getAllActions() } returns actionsFlow + + // Re-init + viewModel = QuickChatViewModel(quickChatActionRepository) + + viewModel.quickChatActions.test { + assertEquals(emptyList(), awaitItem()) + val action = QuickChatAction(uuid = 1L, name = "Test", message = "Hello", position = 0) + actionsFlow.value = listOf(action) + assertEquals(listOf(action), awaitItem()) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `addQuickChatAction delegates to repository`() = runTest { + val action = QuickChatAction(uuid = 1L, name = "Test", message = "Hello", position = 0) + everySuspend { quickChatActionRepository.upsert(any()) } returns Unit + + viewModel.addQuickChatAction(action) + testScheduler.runCurrent() + + verifySuspend { quickChatActionRepository.upsert(action) } + } +} diff --git a/feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsViewModelTest.kt b/feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsViewModelTest.kt new file mode 100644 index 000000000..5f531de11 --- /dev/null +++ b/feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsViewModelTest.kt @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. See + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.messaging.ui.contact + +import app.cash.turbine.test +import dev.mokkery.MockMode +import dev.mokkery.answering.returns +import dev.mokkery.every +import dev.mokkery.mock +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.meshtastic.core.model.ConnectionState +import org.meshtastic.core.repository.NodeRepository +import org.meshtastic.core.repository.PacketRepository +import org.meshtastic.core.repository.RadioConfigRepository +import org.meshtastic.core.repository.ServiceRepository +import org.meshtastic.proto.ChannelSet +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +@OptIn(ExperimentalCoroutinesApi::class) +class ContactsViewModelTest { + + private val testDispatcher = StandardTestDispatcher() + private lateinit var viewModel: ContactsViewModel + private val nodeRepository: NodeRepository = mock(MockMode.autofill) + private val packetRepository: PacketRepository = mock(MockMode.autofill) + private val radioConfigRepository: RadioConfigRepository = mock(MockMode.autofill) + private val serviceRepository: ServiceRepository = mock(MockMode.autofill) + + @BeforeTest + fun setUp() { + Dispatchers.setMain(testDispatcher) + + every { nodeRepository.ourNodeInfo } returns MutableStateFlow(null) + every { nodeRepository.myNodeInfo } returns MutableStateFlow(null) + every { nodeRepository.myId } returns MutableStateFlow(null) + every { nodeRepository.getNodes() } returns MutableStateFlow(emptyList()) + + every { serviceRepository.connectionState } returns MutableStateFlow(ConnectionState.Disconnected) + every { packetRepository.getUnreadCountTotal() } returns MutableStateFlow(0) + every { radioConfigRepository.channelSetFlow } returns MutableStateFlow(ChannelSet()) + + viewModel = ContactsViewModel( + nodeRepository = nodeRepository, + packetRepository = packetRepository, + radioConfigRepository = radioConfigRepository, + serviceRepository = serviceRepository, + ) + } + + @AfterTest + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun testInitialization() { + assertNotNull(viewModel) + } + + @Test + fun `unreadCountTotal reflects updates from repository`() = runTest { + val countFlow = MutableStateFlow(0) + every { packetRepository.getUnreadCountTotal() } returns countFlow + + // Re-init VM + viewModel = ContactsViewModel(nodeRepository, packetRepository, radioConfigRepository, serviceRepository) + + viewModel.unreadCountTotal.test { + assertEquals(0, awaitItem()) + countFlow.value = 5 + assertEquals(5, awaitItem()) + cancelAndIgnoreRemainingEvents() + } + } +} diff --git a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/compass/CompassViewModelTest.kt b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/compass/CompassViewModelTest.kt new file mode 100644 index 000000000..915d74f4e --- /dev/null +++ b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/compass/CompassViewModelTest.kt @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. See + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.feature.node.compass + +import app.cash.turbine.test +import dev.mokkery.answering.returns +import dev.mokkery.every +import dev.mokkery.matcher.any +import dev.mokkery.mock +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.meshtastic.core.di.CoroutineDispatchers +import org.meshtastic.core.model.Node +import org.meshtastic.proto.Config +import org.meshtastic.proto.User +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +@OptIn(ExperimentalCoroutinesApi::class) +class CompassViewModelTest { + + private val testDispatcher = UnconfinedTestDispatcher() + private val dispatchers = CoroutineDispatchers( + main = testDispatcher, + io = testDispatcher, + default = testDispatcher, + ) + + private lateinit var viewModel: CompassViewModel + private val headingProvider: CompassHeadingProvider = mock() + private val phoneLocationProvider: PhoneLocationProvider = mock() + private val magneticFieldProvider: MagneticFieldProvider = mock() + + private val headingFlow = MutableStateFlow(HeadingState()) + private val locationFlow = MutableStateFlow(PhoneLocationState(permissionGranted = true, providerEnabled = true)) + + @BeforeTest + fun setUp() { + Dispatchers.setMain(testDispatcher) + + every { headingProvider.headingUpdates() } returns headingFlow + every { phoneLocationProvider.locationUpdates() } returns locationFlow + every { magneticFieldProvider.getDeclination(any(), any(), any(), any()) } returns 0f + + viewModel = CompassViewModel( + headingProvider = headingProvider, + phoneLocationProvider = phoneLocationProvider, + magneticFieldProvider = magneticFieldProvider, + dispatchers = dispatchers, + ) + } + + @AfterTest + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun testInitialization() { + assertNotNull(viewModel) + } + + @Test + fun `uiState reflects target node info after start`() = runTest { + val node = Node(num = 1234, user = User(id = "!1234", long_name = "Target Node")) + + viewModel.start(node, Config.DisplayConfig.DisplayUnits.METRIC) + + viewModel.uiState.test { + val state = awaitItem() + assertEquals("Target Node", state.targetName) + assertEquals(Config.DisplayConfig.DisplayUnits.METRIC, state.displayUnits) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `uiState updates when heading and location change`() = runTest { + val node = Node( + num = 1234, + user = User(id = "!1234"), + position = org.meshtastic.proto.Position(latitude_i = 10000000, longitude_i = 10000000) // 1 deg North, 1 deg East + ) + + viewModel.start(node, Config.DisplayConfig.DisplayUnits.METRIC) + + viewModel.uiState.test { + // Skip initial states + awaitItem() + + // Update location and heading + locationFlow.value = PhoneLocationState( + permissionGranted = true, + providerEnabled = true, + location = PhoneLocation(0.0, 0.0, 0.0, 1000L) + ) + headingFlow.value = HeadingState(heading = 0f) + + // Wait for state with both bearing and heading + var state = awaitItem() + while (state.bearing == null || state.heading == null) { + state = awaitItem() + } + + // Bearing from (0,0) to (1,1) is approx 45 degrees + assertEquals(45f, state.bearing!!, 0.5f) + assertEquals(0f, state.heading!!, 0.1f) + assertTrue(state.hasTargetPosition) + + cancelAndIgnoreRemainingEvents() + } + } +} diff --git a/feature/node/src/test/kotlin/org/meshtastic/feature/node/domain/usecase/GetFilteredNodesUseCaseTest.kt b/feature/node/src/test/kotlin/org/meshtastic/feature/node/domain/usecase/GetFilteredNodesUseCaseTest.kt deleted file mode 100644 index 123dabeb5..000000000 --- a/feature/node/src/test/kotlin/org/meshtastic/feature/node/domain/usecase/GetFilteredNodesUseCaseTest.kt +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright (c) 2025-2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.feature.node.domain.usecase - -import dev.mokkery.every -import dev.mokkery.matcher.any -import dev.mokkery.mock -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.test.runTest -import org.junit.Assert.assertEquals -import org.junit.Before -import org.junit.Test -import org.meshtastic.core.model.Node -import org.meshtastic.core.model.NodeSortOption -import org.meshtastic.core.repository.NodeRepository -import org.meshtastic.feature.node.list.NodeFilterState -import org.meshtastic.proto.Config -import org.meshtastic.proto.User - -class GetFilteredNodesUseCaseTest { - - private lateinit var nodeRepository: NodeRepository - private lateinit var useCase: GetFilteredNodesUseCase - - @Before - fun setUp() { - nodeRepository = mock() - useCase = GetFilteredNodesUseCase(nodeRepository) - } - - private fun createNode( - num: Int, - role: Config.DeviceConfig.Role = Config.DeviceConfig.Role.CLIENT, - ignored: Boolean = false, - name: String = "Node$num", - viaMqtt: Boolean = false, - ): Node { - val user = User(id = "!$num", long_name = name, short_name = "N$num", role = role) - return Node(num = num, user = user, isIgnored = ignored, viaMqtt = viaMqtt) - } - - @Test - fun `invoke applies repository filters and returns nodes`() = runTest { - // Arrange - val nodes = listOf(createNode(1), createNode(2)) - val filter = NodeFilterState(filterText = "Node", includeUnknown = true) - - every { - nodeRepository.getNodes( - sort = NodeSortOption.LAST_HEARD, - filter = "Node", - includeUnknown = true, - onlyOnline = false, - onlyDirect = false, - ) - } returns flowOf(nodes) - - // Act - val result = useCase(filter, NodeSortOption.LAST_HEARD).first() - - // Assert - assertEquals(2, result.size) - assertEquals(1, result[0].num) - } - - @Test - fun `invoke filters out ignored nodes if showIgnored is false`() = runTest { - // Arrange - val normalNode = createNode(1, ignored = false) - val ignoredNode = createNode(2, ignored = true) - val filter = NodeFilterState(showIgnored = false) - - every { nodeRepository.getNodes(any(), any(), any(), any(), any()) } returns - flowOf(listOf(normalNode, ignoredNode)) - - // Act - val result = useCase(filter, NodeSortOption.LAST_HEARD).first() - - // Assert - assertEquals(1, result.size) - assertEquals(1, result.first().num) - } - - @Test - fun `invoke filters out infrastructure nodes if excludeInfrastructure is true`() = runTest { - // Arrange - val clientNode = createNode(1, role = Config.DeviceConfig.Role.CLIENT) - val routerNode = createNode(2, role = Config.DeviceConfig.Role.ROUTER) - - @Suppress("DEPRECATION") - val repeaterNode = createNode(3, role = Config.DeviceConfig.Role.REPEATER) - val clientBaseNode = createNode(4, role = Config.DeviceConfig.Role.CLIENT_BASE) - val filter = NodeFilterState(excludeInfrastructure = true) - - every { nodeRepository.getNodes(any(), any(), any(), any(), any()) } returns - flowOf(listOf(clientNode, routerNode, repeaterNode, clientBaseNode)) - - // Act - val result = useCase(filter, NodeSortOption.LAST_HEARD).first() - - // Assert - // Should only keep the CLIENT node, others are infrastructure - assertEquals(1, result.size) - assertEquals(1, result.first().num) - } - - @Test - fun `invoke filters out MQTT nodes if excludeMqtt is true`() = runTest { - // Arrange - val loraNode = createNode(1, viaMqtt = false) - val mqttNode = createNode(2, viaMqtt = true) - val filter = NodeFilterState(excludeMqtt = true) - - every { nodeRepository.getNodes(any(), any(), any(), any(), any()) } returns flowOf(listOf(loraNode, mqttNode)) - - // Act - val result = useCase(filter, NodeSortOption.LAST_HEARD).first() - - // Assert - assertEquals(1, result.size) - assertEquals(1, result.first().num) - } - - @Test - fun `invoke keeps MQTT nodes if excludeMqtt is false`() = runTest { - // Arrange - val loraNode = createNode(1, viaMqtt = false) - val mqttNode = createNode(2, viaMqtt = true) - val filter = NodeFilterState(excludeMqtt = false) - - every { nodeRepository.getNodes(any(), any(), any(), any(), any()) } returns flowOf(listOf(loraNode, mqttNode)) - - // Act - val result = useCase(filter, NodeSortOption.LAST_HEARD).first() - - // Assert - assertEquals(2, result.size) - } -}