diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailComponents.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailComponents.kt index 5e66e454ef..d76933fbc7 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailComponents.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailComponents.kt @@ -93,6 +93,7 @@ internal fun InfoItem( icon: ImageVector, modifier: Modifier = Modifier, valueStyle: TextStyle = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold), + valueColor: Color = MaterialTheme.colorScheme.onSurface, iconTint: Color = MaterialTheme.colorScheme.primary.copy(alpha = 0.8f), iconSize: Dp = 14.dp, onClick: (() -> Unit)? = null, @@ -130,7 +131,7 @@ internal fun InfoItem( ) } Spacer(Modifier.height(4.dp)) - Text(text = value, style = valueStyle, color = MaterialTheme.colorScheme.onSurface) + Text(text = value, style = valueStyle, color = valueColor) } } diff --git a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt index c2d1f7160d..d71a399a4b 100644 --- a/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt +++ b/feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.kt @@ -82,6 +82,7 @@ import org.meshtastic.core.resources.transport import org.meshtastic.core.resources.uptime import org.meshtastic.core.resources.user_id import org.meshtastic.core.ui.component.SignedNodeDialog +import org.meshtastic.core.ui.component.determineSignalQuality import org.meshtastic.core.ui.component.transportInfo import org.meshtastic.core.ui.icon.ArrowCircleUp import org.meshtastic.core.ui.icon.DeviceNumbers @@ -98,6 +99,7 @@ import org.meshtastic.core.ui.icon.Snr import org.meshtastic.core.ui.icon.Verified import org.meshtastic.core.ui.icon.role import org.meshtastic.core.ui.theme.StatusColors.StatusGreen +import org.meshtastic.core.ui.util.LocalModemPreset import org.meshtastic.core.ui.util.createClipEntry import org.meshtastic.core.ui.util.formatAgo import org.meshtastic.proto.MeshPacket.TransportMechanism @@ -292,9 +294,12 @@ private fun SignalRow(node: Node) { Row(modifier = Modifier.fillMaxWidth()) { val snr = node.snrOrNull if (snr != null) { + val quality = determineSignalQuality(snr, LocalModemPreset.current) + // Value-before-quality with " · " matches the node-list signal pill in SignalInfo.kt. InfoItem( label = stringResource(Res.string.snr), - value = MetricFormatter.snr(snr), + value = "${MetricFormatter.snr(snr)} · ${stringResource(quality.nameRes)}", + valueColor = quality.color(), icon = MeshtasticIcons.Snr, modifier = Modifier.weight(1f), ) @@ -303,6 +308,7 @@ private fun SignalRow(node: Node) { } val rssi = node.rssiOrNull if (rssi != null) { + // No quality word here: RSSI alone can't be rated without the noise floor - see determineSignalQuality. InfoItem( label = stringResource(Res.string.rssi), value = MetricFormatter.rssi(rssi), diff --git a/feature/node/src/jvmTest/kotlin/org/meshtastic/feature/node/component/SignalRowQualityLabelTest.kt b/feature/node/src/jvmTest/kotlin/org/meshtastic/feature/node/component/SignalRowQualityLabelTest.kt new file mode 100644 index 0000000000..f2d4816446 --- /dev/null +++ b/feature/node/src/jvmTest/kotlin/org/meshtastic/feature/node/component/SignalRowQualityLabelTest.kt @@ -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. If not, see . + */ +package org.meshtastic.feature.node.component + +import androidx.compose.material3.Surface +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.test.ComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.v2.runComposeUiTest +import org.jetbrains.compose.resources.StringResource +import org.meshtastic.core.common.util.MetricFormatter +import org.meshtastic.core.model.Node +import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.bad +import org.meshtastic.core.resources.fair +import org.meshtastic.core.resources.getString +import org.meshtastic.core.resources.good +import org.meshtastic.core.resources.none_quality +import org.meshtastic.core.resources.snr +import org.meshtastic.core.ui.theme.AppTheme +import org.meshtastic.core.ui.util.LocalModemPreset +import org.meshtastic.proto.Config.LoRaConfig.ModemPreset +import kotlin.test.Test + +/** + * Node Details' SignalRow reuses the already-tested [determineSignalQuality] to label SNR (see + * [LoraSignalIndicatorTest] for the underlying threshold coverage). RSSI intentionally keeps showing the raw value + * only - RSSI alone cannot indicate quality without the noise floor. + */ +@OptIn(ExperimentalTestApi::class) +class SignalRowQualityLabelTest { + + // Boundary values relative to ModemPreset.LONG_FAST's -17.5 dB demod floor, mirroring LoraSignalIndicatorTest. + private val preset = ModemPreset.LONG_FAST + + @Test + fun signalRow_labelsGoodSnr() = runComposeUiTest { + assertSnrLabel(snr = -17f, expectedQualityRes = Res.string.good) + } + + @Test + fun signalRow_labelsFairSnr() = runComposeUiTest { + assertSnrLabel(snr = -17.5f, expectedQualityRes = Res.string.fair) + } + + @Test + fun signalRow_labelsBadSnr() = runComposeUiTest { assertSnrLabel(snr = -23f, expectedQualityRes = Res.string.bad) } + + @Test + fun signalRow_labelsNoneSnr() = runComposeUiTest { + assertSnrLabel(snr = -30f, expectedQualityRes = Res.string.none_quality) + } + + @Test + fun signalRow_rssiShowsOnlyTheRawValue() = runComposeUiTest { + setNodeDetails(Node(num = 1, snr = -17f, rssi = -100, hopsAway = 0)) + onNodeWithText(MetricFormatter.rssi(-100)).assertExists() + } + + @Test + fun signalRow_hidesWhenNodeHasNoSnrReading() = runComposeUiTest { + // hopsAway = 0 so SignalRow itself renders; default snr is the SNR_UNSET sentinel -> snrOrNull is null. + setNodeDetails(Node(num = 1, hopsAway = 0)) + onNodeWithText(getString(Res.string.snr)).assertDoesNotExist() + } + + private fun ComposeUiTest.assertSnrLabel(snr: Float, expectedQualityRes: StringResource) { + // hopsAway = 0 - matches MainNodeDetails' guard for rendering SignalRow at all. + setNodeDetails(Node(num = 1, snr = snr, rssi = -100, hopsAway = 0)) + // Value-before-quality order matches the node-list signal pill in SignalInfo.kt. + val expected = "${MetricFormatter.snr(snr)} · ${getString(expectedQualityRes)}" + onNodeWithText(expected).assertExists() + } + + private fun ComposeUiTest.setNodeDetails(node: Node) = setContent { + CompositionLocalProvider(LocalModemPreset provides preset) { + AppTheme { Surface { NodeDetailsSection(node = node) } } + } + } +} diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentLocal_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentLocal_Dark_d19fbf1f_0.png index 110c886b73..d330b168fe 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentLocal_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentLocal_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentLocal_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentLocal_Light_b29dc7a7_0.png index 4077ee2bd9..10a45d1c5d 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentLocal_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentLocal_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentRemote_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentRemote_Dark_d19fbf1f_0.png index dba88945f3..75ac803b58 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentRemote_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentRemote_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentRemote_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentRemote_Light_b29dc7a7_0.png index c02399c867..2e32ca0cf4 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentRemote_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailContentRemote_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSectionSigned_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSectionSigned_Dark_d19fbf1f_0.png index 5dcaa88093..145df97dc7 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSectionSigned_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSectionSigned_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSectionSigned_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSectionSigned_Light_b29dc7a7_0.png index c58936f921..4c76a60aa0 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSectionSigned_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSectionSigned_Light_b29dc7a7_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSection_Dark_d19fbf1f_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSection_Dark_d19fbf1f_0.png index 632f6bcc6b..d91ce72352 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSection_Dark_d19fbf1f_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSection_Dark_d19fbf1f_0.png differ diff --git a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSection_Light_b29dc7a7_0.png b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSection_Light_b29dc7a7_0.png index 1b2748a520..87ea7e72ca 100644 Binary files a/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSection_Light_b29dc7a7_0.png and b/screenshot-tests/src/screenshotTestDebug/reference/org/meshtastic/screenshots/feature/NodeScreenshotTestsKt/ScreenshotNodeDetailsSection_Light_b29dc7a7_0.png differ