test: add unit tests for manual relay input and relay selection

- Add tests for SettingsViewModel addManualPropagationNode and selectRelay
- Add tests for ManualRelayInput component (validation, callbacks)
- Add tests for RelaySelectionDialog (display, selection, dismissal)
- Add tests for relay selection hint visibility

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
torlando-tech
2025-12-18 15:18:49 -05:00
parent aef3b554fa
commit 2a566aba81
2 changed files with 358 additions and 4 deletions

View File

@@ -18,6 +18,7 @@ import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
import androidx.compose.ui.test.performTextClearance
import androidx.compose.ui.test.performTextInput
import com.lxmf.messenger.service.RelayInfo
import com.lxmf.messenger.test.MessageDeliveryRetrievalTestFixtures
import com.lxmf.messenger.test.MessageDeliveryRetrievalTestFixtures.CardConfig
import com.lxmf.messenger.test.RegisterComponentActivityRule
@@ -55,6 +56,8 @@ class MessageDeliveryRetrievalCardTest {
private var autoRetrieveToggled: Boolean? = null
private var intervalChanged: Int? = null
private var syncNowCalled = false
private var manualRelayAdded: Pair<String, String?>? = null
private var relaySelected: Pair<String, String>? = null
@Before
fun resetCallbackTrackers() {
@@ -64,11 +67,21 @@ class MessageDeliveryRetrievalCardTest {
autoRetrieveToggled = null
intervalChanged = null
syncNowCalled = false
manualRelayAdded = null
relaySelected = null
}
// ========== Setup Helper ==========
private fun setUpCardWithConfig(config: CardConfig) {
setUpCardWithConfigAndRelays(config, emptyList())
}
private fun setUpCardWithConfigAndRelays(
config: CardConfig,
availableRelays: List<RelayInfo>,
currentRelayHash: String? = null,
) {
composeTestRule.setContent {
// Wrap in a scrollable column so performScrollTo() works
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
@@ -77,14 +90,14 @@ class MessageDeliveryRetrievalCardTest {
tryPropagationOnFail = config.tryPropagationOnFail,
currentRelayName = config.currentRelayName,
currentRelayHops = config.currentRelayHops,
currentRelayHash = null,
currentRelayHash = currentRelayHash,
isAutoSelect = config.isAutoSelect,
availableRelays = emptyList(),
availableRelays = availableRelays,
onMethodChange = { methodChanged = it },
onTryPropagationToggle = { propagationToggled = it },
onAutoSelectToggle = { autoSelectToggled = it },
onAddManualRelay = { _, _ -> },
onSelectRelay = { _, _ -> },
onAddManualRelay = { hash, nickname -> manualRelayAdded = hash to nickname },
onSelectRelay = { hash, name -> relaySelected = hash to name },
autoRetrieveEnabled = config.autoRetrieveEnabled,
retrievalIntervalSeconds = config.retrievalIntervalSeconds,
lastSyncTimestamp = config.lastSyncTimestamp,
@@ -1095,4 +1108,302 @@ class MessageDeliveryRetrievalCardTest {
composeTestRule.onNodeWithText("Sync Now").performScrollTo().performClick()
assertTrue(syncNowCalled)
}
// ========== Category N: Manual Relay Input Tests (8 tests) ==========
@Test
fun manualInput_showsWhenManualSelectionMode() {
setUpCardWithConfig(MessageDeliveryRetrievalTestFixtures.manualRelaySelectionState())
composeTestRule.onNodeWithText("Enter relay destination hash:")
.performScrollTo()
.assertIsDisplayed()
}
@Test
fun manualInput_hiddenWhenAutoSelectMode() {
setUpCardWithConfig(MessageDeliveryRetrievalTestFixtures.defaultState())
composeTestRule.onNodeWithText("Enter relay destination hash:")
.assertDoesNotExist()
}
@Test
fun manualInput_displaysDestinationHashField() {
setUpCardWithConfig(MessageDeliveryRetrievalTestFixtures.manualRelaySelectionState())
composeTestRule.onNodeWithText("Destination Hash")
.performScrollTo()
.assertIsDisplayed()
}
@Test
fun manualInput_displaysNicknameField() {
setUpCardWithConfig(MessageDeliveryRetrievalTestFixtures.manualRelaySelectionState())
composeTestRule.onNodeWithText("Nickname (optional)")
.performScrollTo()
.assertIsDisplayed()
}
@Test
fun manualInput_displaysSetAsRelayButton() {
setUpCardWithConfig(MessageDeliveryRetrievalTestFixtures.manualRelaySelectionState())
composeTestRule.onNodeWithText("Set as Relay")
.performScrollTo()
.assertIsDisplayed()
}
@Test
fun manualInput_buttonDisabledWithEmptyHash() {
setUpCardWithConfig(MessageDeliveryRetrievalTestFixtures.manualRelaySelectionState())
composeTestRule.onNodeWithText("Set as Relay")
.performScrollTo()
.assertIsNotEnabled()
}
@Test
fun manualInput_buttonEnabledWithValidHash() {
setUpCardWithConfig(MessageDeliveryRetrievalTestFixtures.manualRelaySelectionState())
// Enter a valid 32-character hex hash
composeTestRule.onNodeWithText("Destination Hash")
.performScrollTo()
.performTextInput("abcd1234abcd1234abcd1234abcd1234")
composeTestRule.onNodeWithText("Set as Relay")
.performScrollTo()
.assertIsEnabled()
}
@Test
fun manualInput_showsCharacterCount() {
setUpCardWithConfig(MessageDeliveryRetrievalTestFixtures.manualRelaySelectionState())
composeTestRule.onNodeWithText("0/32")
.performScrollTo()
.assertIsDisplayed()
}
@Test
fun manualInput_showsErrorForInvalidHash() {
setUpCardWithConfig(MessageDeliveryRetrievalTestFixtures.manualRelaySelectionState())
// Enter an incomplete hash
composeTestRule.onNodeWithText("Destination Hash")
.performScrollTo()
.performTextInput("abcd1234")
// Error message format: "Hash must be 32 characters (got X)"
composeTestRule.onNodeWithText("Hash must be 32 characters (got 8)")
.performScrollTo()
.assertIsDisplayed()
}
@Test
fun manualInput_confirmInvokesCallback() {
setUpCardWithConfig(MessageDeliveryRetrievalTestFixtures.manualRelaySelectionState())
// Enter a valid hash
composeTestRule.onNodeWithText("Destination Hash")
.performScrollTo()
.performTextInput("abcd1234abcd1234abcd1234abcd1234")
// Enter a nickname
composeTestRule.onNodeWithText("Nickname (optional)")
.performScrollTo()
.performTextInput("My Relay")
// Click confirm
composeTestRule.onNodeWithText("Set as Relay")
.performScrollTo()
.performClick()
assertEquals("abcd1234abcd1234abcd1234abcd1234", manualRelayAdded?.first)
assertEquals("My Relay", manualRelayAdded?.second)
}
// ========== Category O: Relay Selection Hint Tests (2 tests) ==========
@Test
fun relaySelectionHint_displaysWhenRelayConfigured() {
setUpCardWithConfig(MessageDeliveryRetrievalTestFixtures.defaultState())
composeTestRule.onNodeWithText("Tap to select a different relay")
.performScrollTo()
.assertIsDisplayed()
}
@Test
fun relaySelectionHint_hiddenWhenNoRelay() {
setUpCardWithConfig(MessageDeliveryRetrievalTestFixtures.noRelayState())
composeTestRule.onNodeWithText("Tap to select a different relay")
.assertDoesNotExist()
}
// ========== Category P: Relay Selection Dialog Tests (6 tests) ==========
@Test
fun relaySelectionDialog_opensOnRelayCardClick() {
setUpCardWithConfig(MessageDeliveryRetrievalTestFixtures.defaultState())
// Click on the relay card
composeTestRule.onNodeWithText("TestRelay01")
.performScrollTo()
.performClick()
// Dialog should appear
composeTestRule.onNodeWithText("Select Relay")
.assertIsDisplayed()
}
@Test
fun relaySelectionDialog_showsNoRelaysMessage() {
setUpCardWithConfig(MessageDeliveryRetrievalTestFixtures.defaultState())
// Click on the relay card
composeTestRule.onNodeWithText("TestRelay01")
.performScrollTo()
.performClick()
// Should show no relays message since availableRelays is empty
composeTestRule.onNodeWithText("No propagation nodes discovered yet", substring = true)
.assertIsDisplayed()
}
@Test
fun relaySelectionDialog_showsAvailableRelays() {
val testRelays = listOf(
RelayInfo(
destinationHash = "hash1",
displayName = "Relay 1",
hops = 1,
isAutoSelected = false,
lastSeenTimestamp = System.currentTimeMillis(),
),
RelayInfo(
destinationHash = "hash2",
displayName = "Relay 2",
hops = 3,
isAutoSelected = false,
lastSeenTimestamp = System.currentTimeMillis(),
),
)
setUpCardWithConfigAndRelays(
MessageDeliveryRetrievalTestFixtures.defaultState(),
testRelays,
)
// Click on the relay card
composeTestRule.onNodeWithText("TestRelay01")
.performScrollTo()
.performClick()
// Dialog should show available relays
composeTestRule.onNodeWithText("Relay 1").assertIsDisplayed()
composeTestRule.onNodeWithText("Relay 2").assertIsDisplayed()
}
@Test
fun relaySelectionDialog_showsHopCount() {
val testRelays = listOf(
RelayInfo(
destinationHash = "hash1",
displayName = "Relay 1",
hops = 2,
isAutoSelected = false,
lastSeenTimestamp = System.currentTimeMillis(),
),
)
setUpCardWithConfigAndRelays(
MessageDeliveryRetrievalTestFixtures.defaultState(),
testRelays,
)
// Click on the relay card
composeTestRule.onNodeWithText("TestRelay01")
.performScrollTo()
.performClick()
// Dialog should show relay name and the hop count exists in the dialog
composeTestRule.onNodeWithText("Relay 1").assertIsDisplayed()
// Use assertExists() for the hop count since it may not be "displayed" due to LazyColumn
composeTestRule.onAllNodesWithText("2 hops away", substring = true)[0].assertExists()
}
@Test
fun relaySelectionDialog_selectRelay_invokesCallback() {
val testRelays = listOf(
RelayInfo(
destinationHash = "hash1",
displayName = "Relay 1",
hops = 1,
isAutoSelected = false,
lastSeenTimestamp = System.currentTimeMillis(),
),
)
setUpCardWithConfigAndRelays(
MessageDeliveryRetrievalTestFixtures.defaultState(),
testRelays,
)
// Click on the relay card
composeTestRule.onNodeWithText("TestRelay01")
.performScrollTo()
.performClick()
// Select the relay
composeTestRule.onNodeWithText("Relay 1")
.performClick()
assertEquals("hash1", relaySelected?.first)
assertEquals("Relay 1", relaySelected?.second)
}
@Test
fun relaySelectionDialog_cancelDismisses() {
setUpCardWithConfig(MessageDeliveryRetrievalTestFixtures.defaultState())
// Click on the relay card
composeTestRule.onNodeWithText("TestRelay01")
.performScrollTo()
.performClick()
// Click cancel
composeTestRule.onNodeWithText("Cancel")
.performClick()
// Dialog should be dismissed
composeTestRule.onNodeWithText("Select Relay")
.assertDoesNotExist()
}
@Test
fun relaySelectionDialog_showsViewAllRelaysOption() {
val testRelays = listOf(
RelayInfo(
destinationHash = "hash1",
displayName = "Relay 1",
hops = 1,
isAutoSelected = false,
lastSeenTimestamp = System.currentTimeMillis(),
),
)
setUpCardWithConfigAndRelays(
MessageDeliveryRetrievalTestFixtures.defaultState(),
testRelays,
)
// Click on the relay card
composeTestRule.onNodeWithText("TestRelay01")
.performScrollTo()
.performClick()
// Should show "View All Relays..." option
composeTestRule.onNodeWithText("View All Relays...")
.assertIsDisplayed()
}
}

View File

@@ -7,8 +7,10 @@ import com.lxmf.messenger.data.repository.IdentityRepository
import com.lxmf.messenger.repository.SettingsRepository
import com.lxmf.messenger.reticulum.model.NetworkStatus
import com.lxmf.messenger.reticulum.protocol.ReticulumProtocol
import com.lxmf.messenger.service.AvailableRelaysState
import com.lxmf.messenger.service.InterfaceConfigManager
import com.lxmf.messenger.service.PropagationNodeManager
import com.lxmf.messenger.service.RelayInfo
import com.lxmf.messenger.ui.theme.PresetTheme
import io.mockk.clearAllMocks
import io.mockk.coEvery
@@ -102,6 +104,8 @@ class SettingsViewModelTest {
every { propagationNodeManager.currentRelay } returns MutableStateFlow(null)
every { propagationNodeManager.isSyncing } returns MutableStateFlow(false)
every { propagationNodeManager.lastSyncTimestamp } returns MutableStateFlow(null)
every { propagationNodeManager.availableRelaysState } returns
MutableStateFlow(AvailableRelaysState.Loaded(emptyList()))
// Mock other required methods
coEvery { identityRepository.getActiveIdentitySync() } returns null
@@ -1882,4 +1886,43 @@ class SettingsViewModelTest {
}
// endregion
// region Manual Propagation Node Tests
@Test
fun `addManualPropagationNode calls propagationNodeManager setManualRelayByHash`() =
runTest {
viewModel = createViewModel()
val testHash = "abcd1234abcd1234abcd1234abcd1234"
val testNickname = "My Test Relay"
viewModel.addManualPropagationNode(testHash, testNickname)
coVerify { propagationNodeManager.setManualRelayByHash(testHash, testNickname) }
}
@Test
fun `addManualPropagationNode with null nickname calls propagationNodeManager`() =
runTest {
viewModel = createViewModel()
val testHash = "abcd1234abcd1234abcd1234abcd1234"
viewModel.addManualPropagationNode(testHash, null)
coVerify { propagationNodeManager.setManualRelayByHash(testHash, null) }
}
@Test
fun `selectRelay calls propagationNodeManager setManualRelay`() =
runTest {
viewModel = createViewModel()
val testHash = "abcd1234abcd1234abcd1234abcd1234"
val testName = "Selected Relay"
viewModel.selectRelay(testHash, testName)
coVerify { propagationNodeManager.setManualRelay(testHash, testName) }
}
// endregion
}