feat(test): Add comprehensive unit and instrumentation tests (#4260)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich 2026-01-19 19:52:03 -06:00 committed by GitHub
parent 4e2c429180
commit 45227fb142
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 1270 additions and 307 deletions

View file

@ -1,164 +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.core.ui.qr
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.v2.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.meshtastic.core.strings.getString
import org.junit.Assert
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.meshtastic.core.model.Channel
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.accept
import org.meshtastic.core.strings.add
import org.meshtastic.core.strings.cancel
import org.meshtastic.core.strings.new_channel_rcvd
import org.meshtastic.core.strings.replace
import org.meshtastic.proto.AppOnlyProtos.ChannelSet
import org.meshtastic.proto.ConfigProtos
import org.meshtastic.proto.channelSet
import org.meshtastic.proto.channelSettings
import org.meshtastic.proto.copy
@RunWith(AndroidJUnit4::class)
class ScannedQrCodeDialogTest {
@get:Rule val composeTestRule = createComposeRule()
private fun getString(id: Int): String = InstrumentationRegistry.getInstrumentation().targetContext.getString(id)
private fun getRandomKey() = Channel.getRandomKey()
private val channels = channelSet {
settings.add(Channel.default.settings)
loraConfig = Channel.default.loraConfig
}
private val incoming = channelSet {
settings.addAll(
listOf(
Channel.default.settings,
channelSettings {
name = "2"
psk = getRandomKey()
},
channelSettings {
name = "3"
psk = getRandomKey()
},
channelSettings {
name = "admin"
psk = getRandomKey()
},
),
)
loraConfig =
Channel.default.loraConfig.copy { modemPreset = ConfigProtos.Config.LoRaConfig.ModemPreset.SHORT_FAST }
}
private fun testScannedQrCodeDialog(onDismiss: () -> Unit = {}, onConfirm: (ChannelSet) -> Unit = {}) =
composeTestRule.setContent {
ScannedQrCodeDialog(channels = channels, incoming = incoming, onDismiss = onDismiss, onConfirm = onConfirm)
}
@Test
fun testScannedQrCodeDialog_showsDialogTitle() {
composeTestRule.apply {
testScannedQrCodeDialog()
// Verify that the dialog title is displayed
onNodeWithText(getString(Res.string.new_channel_rcvd)).assertIsDisplayed()
}
}
@Test
fun testScannedQrCodeDialog_showsAddAndReplaceButtons() {
composeTestRule.apply {
testScannedQrCodeDialog()
// Verify that the "Add" and "Replace" buttons are displayed
onNodeWithText(getString(Res.string.add)).assertIsDisplayed()
onNodeWithText(getString(Res.string.replace)).assertIsDisplayed()
}
}
@Test
fun testScannedQrCodeDialog_showsCancelAndAcceptButtons() {
composeTestRule.apply {
testScannedQrCodeDialog()
// Verify the "Cancel" and "Accept" buttons are displayed
onNodeWithText(getString(Res.string.cancel)).assertIsDisplayed()
onNodeWithText(getString(Res.string.accept)).assertIsDisplayed()
}
}
@Test
fun testScannedQrCodeDialog_clickCancelButton() {
var onDismissClicked = false
composeTestRule.apply {
testScannedQrCodeDialog(onDismiss = { onDismissClicked = true })
// Click the "Cancel" button
onNodeWithText(getString(Res.string.cancel)).performClick()
}
// Verify onDismiss is called
Assert.assertTrue(onDismissClicked)
}
@Test
fun testScannedQrCodeDialog_replaceChannels() {
var actualChannelSet: ChannelSet? = null
composeTestRule.apply {
testScannedQrCodeDialog(onConfirm = { actualChannelSet = it })
// Click the "Accept" button
onNodeWithText(getString(Res.string.accept)).performClick()
}
// Verify onConfirm is called with the correct ChannelSet
Assert.assertEquals(incoming, actualChannelSet)
}
@Test
fun testScannedQrCodeDialog_addChannels() {
var actualChannelSet: ChannelSet? = null
composeTestRule.apply {
testScannedQrCodeDialog(onConfirm = { actualChannelSet = it })
// Click the "Add" button then the "Accept" button
onNodeWithText(getString(Res.string.add)).performClick()
onNodeWithText(getString(Res.string.accept)).performClick()
}
// Verify onConfirm is called with the correct ChannelSet
val expectedChannelSet =
channels.copy {
val list = LinkedHashSet(settings + incoming.settingsList)
settings.clear()
settings.addAll(list)
}
Assert.assertEquals(expectedChannelSet, actualChannelSet)
}
}

View file

@ -54,9 +54,11 @@ import org.meshtastic.core.model.Channel
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.accept
import org.meshtastic.core.strings.add
import org.meshtastic.core.strings.add_channels_description
import org.meshtastic.core.strings.cancel
import org.meshtastic.core.strings.new_channel_rcvd
import org.meshtastic.core.strings.replace
import org.meshtastic.core.strings.replace_channels_and_settings_description
import org.meshtastic.core.ui.component.ChannelSelection
import org.meshtastic.proto.AppOnlyProtos.ChannelSet
import org.meshtastic.proto.ConfigProtos.Config.LoRaConfig.ModemPreset
@ -124,7 +126,13 @@ fun ScannedQrCodeDialog(
val selectedChannelSet =
channelSet.copy {
val result = settings.filterIndexed { i, _ -> channelSelections.getOrNull(i) == true }
// When adding (not replacing), include all previous channels + selected new channels.
// Since 'channelSet.settings' already contains the merged distinct list, we just filter it.
val result =
settings.filterIndexed { i, _ ->
val isExisting = !shouldReplace && i < channels.settingsCount
isExisting || channelSelections.getOrNull(i) == true
}
settings.clear()
settings.addAll(result)
}
@ -153,30 +161,6 @@ fun ScannedQrCodeDialog(
if (current.usePreset != new.usePreset) {
changes.add("Use Preset: ${current.usePreset} -> ${new.usePreset}")
}
if (current.txEnabled != new.txEnabled) {
changes.add("Transmit Enabled: ${current.txEnabled} -> ${new.txEnabled}")
}
if (current.channelNum != new.channelNum) {
changes.add("Channel Number: ${current.channelNum} -> ${new.channelNum}")
}
if (current.bandwidth != new.bandwidth) {
changes.add("Bandwidth: ${current.bandwidth} -> ${new.bandwidth}")
}
if (current.codingRate != new.codingRate) {
changes.add("Coding Rate: ${current.codingRate} -> ${new.codingRate}")
}
if (current.spreadFactor != new.spreadFactor) {
changes.add("Spread Factor: ${current.spreadFactor} -> ${new.spreadFactor}")
}
if (current.sx126XRxBoostedGain != new.sx126XRxBoostedGain) {
changes.add("RX Boosted Gain: ${current.sx126XRxBoostedGain} -> ${new.sx126XRxBoostedGain}")
}
if (current.overrideFrequency != new.overrideFrequency) {
changes.add("Override Frequency: ${current.overrideFrequency} -> ${new.overrideFrequency}")
}
if (current.ignoreMqtt != new.ignoreMqtt) {
changes.add("Ignore MQTT: ${current.ignoreMqtt} -> ${new.ignoreMqtt}")
}
changes
} else {
@ -204,13 +188,30 @@ fun ScannedQrCodeDialog(
style = MaterialTheme.typography.titleLarge,
)
}
item {
Text(
text =
stringResource(
if (shouldReplace) {
Res.string.replace_channels_and_settings_description
} else {
Res.string.add_channels_description
},
),
modifier = Modifier.padding(bottom = 16.dp),
style = MaterialTheme.typography.bodyMedium,
)
}
itemsIndexed(channelSet.settingsList) { index, channel ->
val isExisting = !shouldReplace && index < channels.settingsCount
val channelObj = Channel(channel, channelSet.loraConfig)
ChannelSelection(
index = index,
title = channel.name.ifEmpty { modemPresetName },
enabled = true,
isSelected = channelSelections[index],
enabled = !isExisting,
isSelected = if (isExisting) true else channelSelections[index],
onSelected = {
if (it || selectedChannelSet.settingsCount > 1) {
channelSelections[index] = it