From aad11d06f8e23a4d36b841ddba28797e11f2a466 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Wed, 18 Mar 2026 20:10:32 -0500 Subject: [PATCH] test(node): Add commonTest for NodeDetailViewModel --- .../node/detail/NodeDetailViewModelTest.kt | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModelTest.kt diff --git a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModelTest.kt b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModelTest.kt new file mode 100644 index 000000000..1a978af20 --- /dev/null +++ b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModelTest.kt @@ -0,0 +1,125 @@ +/* + * 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.detail + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import app.cash.turbine.test +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.flow.emptyFlow +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.repository.ServiceRepository +import org.meshtastic.feature.node.component.NodeMenuAction +import org.meshtastic.feature.node.domain.usecase.GetNodeDetailsUseCase +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 + +@OptIn(ExperimentalCoroutinesApi::class) +class NodeDetailViewModelTest { + + private val testDispatcher = StandardTestDispatcher() + private lateinit var viewModel: NodeDetailViewModel + private val nodeManagementActions: NodeManagementActions = mock() + private val nodeRequestActions: NodeRequestActions = mock() + private val serviceRepository: ServiceRepository = mock() + private val getNodeDetailsUseCase: GetNodeDetailsUseCase = mock() + + @BeforeTest + fun setUp() { + Dispatchers.setMain(testDispatcher) + + every { getNodeDetailsUseCase(any()) } returns emptyFlow() + every { nodeRequestActions.effects } returns kotlinx.coroutines.flow.MutableSharedFlow() + + viewModel = createViewModel(1234) + } + + private fun createViewModel(nodeId: Int?) = NodeDetailViewModel( + savedStateHandle = SavedStateHandle(if (nodeId != null) mapOf("destNum" to nodeId) else emptyMap()), + nodeManagementActions = nodeManagementActions, + nodeRequestActions = nodeRequestActions, + serviceRepository = serviceRepository, + getNodeDetailsUseCase = getNodeDetailsUseCase, + ) + + @AfterTest + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun testInitialization() { + assertNotNull(viewModel) + } + + @Test + fun `uiState emits updates from useCase`() = runTest { + val stateFlow = MutableStateFlow(NodeDetailUiState(node = Node(num = 1234, user = User(id = "!1234")))) + every { getNodeDetailsUseCase(1234) } returns stateFlow + + val vm = createViewModel(1234) + + vm.uiState.test { + // Initial empty state from stateIn + assertEquals(null, awaitItem().node) + + // State from useCase + val state = awaitItem() + assertEquals(1234, state.node?.num) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `handleNodeMenuAction delegates to nodeManagementActions for Mute`() = runTest { + val node = Node(num = 1234, user = User(id = "!1234")) + every { nodeManagementActions.requestMuteNode(any(), any()) } returns Unit + + viewModel.handleNodeMenuAction(NodeMenuAction.Mute(node)) + + verify { nodeManagementActions.requestMuteNode(any(), node) } + } + + @Test + fun `handleNodeMenuAction delegates to nodeRequestActions for Traceroute`() = runTest { + val node = Node(num = 1234, user = User(id = "!1234", long_name = "Test Node")) + every { nodeRequestActions.requestTraceroute(any(), any(), any()) } returns Unit + + viewModel.handleNodeMenuAction(NodeMenuAction.TraceRoute(node)) + + verify { nodeRequestActions.requestTraceroute(any(), 1234, "Test Node") } + } +}