Skip to content

Commit 7492a33

Browse files
CopilotjamesarichCopilot
authored
Fix node-details remove action to preserve confirmation flow (#5192)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jamesarich <2199651+jamesarich@users.noreply.github.com> Co-authored-by: James Rich <james.a.rich@gmail.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 2b47da3 commit 7492a33

5 files changed

Lines changed: 119 additions & 8 deletions

File tree

feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/HandleNodeAction.kt

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,7 @@ internal fun handleNodeAction(
4343
val route = viewModel.getDirectMessageRoute(menuAction.node, uiState.ourNode)
4444
navigateToMessages(route)
4545
}
46-
is NodeMenuAction.Remove -> {
47-
viewModel.handleNodeMenuAction(menuAction)
48-
onNavigateUp()
49-
}
46+
is NodeMenuAction.Remove -> viewModel.handleNodeMenuAction(menuAction, onNavigateUp)
5047
else -> viewModel.handleNodeMenuAction(menuAction)
5148
}
5249
}

feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModel.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,10 @@ class NodeDetailViewModel(
8989
}
9090

9191
/** Dispatches high-level node management actions like removal, muting, or favoriting. */
92-
fun handleNodeMenuAction(action: NodeMenuAction) {
92+
fun handleNodeMenuAction(action: NodeMenuAction, onAfterRemove: () -> Unit = {}) {
9393
when (action) {
94-
is NodeMenuAction.Remove -> nodeManagementActions.requestRemoveNode(viewModelScope, action.node)
94+
is NodeMenuAction.Remove ->
95+
nodeManagementActions.requestRemoveNode(viewModelScope, action.node, onAfterRemove)
9596
is NodeMenuAction.Ignore -> nodeManagementActions.requestIgnoreNode(viewModelScope, action.node)
9697
is NodeMenuAction.Mute -> nodeManagementActions.requestMuteNode(viewModelScope, action.node)
9798
is NodeMenuAction.Favorite -> nodeManagementActions.requestFavoriteNode(viewModelScope, action.node)

feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeManagementActions.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,14 @@ constructor(
5050
private val radioController: RadioController,
5151
private val alertManager: AlertManager,
5252
) {
53-
open fun requestRemoveNode(scope: CoroutineScope, node: Node) {
53+
open fun requestRemoveNode(scope: CoroutineScope, node: Node, onAfterRemove: () -> Unit = {}) {
5454
alertManager.showAlert(
5555
titleRes = Res.string.remove,
5656
messageRes = Res.string.remove_node_text,
57-
onConfirm = { removeNode(scope, node.num) },
57+
onConfirm = {
58+
removeNode(scope, node.num)
59+
onAfterRemove()
60+
},
5861
)
5962
}
6063

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* Copyright (c) 2026 Meshtastic LLC
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
16+
*/
17+
package org.meshtastic.feature.node.detail
18+
19+
import androidx.lifecycle.SavedStateHandle
20+
import dev.mokkery.answering.returns
21+
import dev.mokkery.every
22+
import dev.mokkery.matcher.any
23+
import dev.mokkery.mock
24+
import dev.mokkery.verify
25+
import kotlinx.coroutines.Dispatchers
26+
import kotlinx.coroutines.ExperimentalCoroutinesApi
27+
import kotlinx.coroutines.flow.emptyFlow
28+
import kotlinx.coroutines.test.UnconfinedTestDispatcher
29+
import kotlinx.coroutines.test.resetMain
30+
import kotlinx.coroutines.test.runTest
31+
import kotlinx.coroutines.test.setMain
32+
import org.meshtastic.core.model.Node
33+
import org.meshtastic.core.repository.ServiceRepository
34+
import org.meshtastic.feature.node.component.NodeMenuAction
35+
import org.meshtastic.feature.node.domain.usecase.GetNodeDetailsUseCase
36+
import org.meshtastic.feature.node.model.NodeDetailAction
37+
import org.meshtastic.proto.User
38+
import kotlin.test.AfterTest
39+
import kotlin.test.BeforeTest
40+
import kotlin.test.Test
41+
import kotlin.test.assertFalse
42+
43+
@OptIn(ExperimentalCoroutinesApi::class)
44+
class HandleNodeActionTest {
45+
46+
private val testDispatcher = UnconfinedTestDispatcher()
47+
private val nodeManagementActions: NodeManagementActions = mock()
48+
private val nodeRequestActions: NodeRequestActions = mock()
49+
private val serviceRepository: ServiceRepository = mock()
50+
private val getNodeDetailsUseCase: GetNodeDetailsUseCase = mock()
51+
52+
@BeforeTest
53+
fun setUp() {
54+
Dispatchers.setMain(testDispatcher)
55+
every { getNodeDetailsUseCase(any()) } returns emptyFlow()
56+
}
57+
58+
@AfterTest
59+
fun tearDown() {
60+
Dispatchers.resetMain()
61+
}
62+
63+
@Test
64+
fun `remove action delegates to viewModel and does not navigate up immediately`() = runTest(testDispatcher) {
65+
val node = Node(num = 1234, user = User(id = "!1234"))
66+
every { nodeManagementActions.requestRemoveNode(any(), any(), any()) } returns Unit
67+
val viewModel = createViewModel()
68+
var navigateUpCalled = false
69+
70+
handleNodeAction(
71+
action = NodeDetailAction.HandleNodeMenuAction(NodeMenuAction.Remove(node)),
72+
uiState = NodeDetailUiState(),
73+
navigateToMessages = {},
74+
onNavigateUp = { navigateUpCalled = true },
75+
onNavigate = {},
76+
viewModel = viewModel,
77+
)
78+
79+
verify { nodeManagementActions.requestRemoveNode(any(), node, any()) }
80+
assertFalse(navigateUpCalled)
81+
}
82+
83+
private fun createViewModel() = NodeDetailViewModel(
84+
savedStateHandle = SavedStateHandle(mapOf("destNum" to 1234)),
85+
nodeManagementActions = nodeManagementActions,
86+
nodeRequestActions = nodeRequestActions,
87+
serviceRepository = serviceRepository,
88+
getNodeDetailsUseCase = getNodeDetailsUseCase,
89+
)
90+
}

feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/detail/NodeManagementActionsTest.kt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import org.meshtastic.core.testing.FakeRadioController
3030
import org.meshtastic.core.ui.util.AlertManager
3131
import org.meshtastic.proto.User
3232
import kotlin.test.Test
33+
import kotlin.test.assertTrue
3334

3435
@OptIn(ExperimentalCoroutinesApi::class)
3536
class NodeManagementActionsTest {
@@ -69,4 +70,23 @@ class NodeManagementActionsTest {
6970
)
7071
}
7172
}
73+
74+
@Test
75+
fun requestRemoveNode_invokes_onAfterRemove_when_user_confirms() {
76+
val realAlertManager = AlertManager()
77+
val actionsWithRealAlert =
78+
NodeManagementActions(
79+
nodeRepository = nodeRepository,
80+
serviceRepository = serviceRepository,
81+
radioController = radioController,
82+
alertManager = realAlertManager,
83+
)
84+
val node = Node(num = 123, user = User(long_name = "Test Node"))
85+
var afterRemoveCalled = false
86+
87+
actionsWithRealAlert.requestRemoveNode(testScope, node) { afterRemoveCalled = true }
88+
realAlertManager.currentAlert.value?.onConfirm?.invoke()
89+
90+
assertTrue(afterRemoveCalled)
91+
}
7292
}

0 commit comments

Comments
 (0)