conductor(checkpoint): Checkpoint end of Phase 5

This commit is contained in:
James Rich 2026-03-18 20:51:08 -05:00
parent b585dcaad3
commit e321cf0403
9 changed files with 588 additions and 155 deletions

View file

@ -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).

View file

@ -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) }

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<String?>(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") }
}
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<List<Node>>(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)) }
}
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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) }
}
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<List<QuickChatAction>>(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) }
}
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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()
}
}
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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()
}
}
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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)
}
}