refactor: Enable test coverage and update CI (#4233)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich
2026-01-15 18:47:45 -06:00
committed by GitHub
parent 45d8f5944a
commit 962137ae4d
21 changed files with 140 additions and 126 deletions

View File

@@ -69,7 +69,7 @@ jobs:
echo "datadogApplicationId=$DATADOG_APPLICATION_ID" >> ./secrets.properties
echo "datadogClientToken=$DATADOG_CLIENT_TOKEN" >> ./secrets.properties
- name: Run Spotless, Detekt, Build, Lint, and Local Tests
run: ./gradlew spotlessCheck detekt lintDebug :app:assembleDebug koverXmlReport --configuration-cache --scan
run: ./gradlew spotlessCheck detekt assembleDebug testDebugUnitTest koverXmlReport --configuration-cache --scan
env:
VERSION_CODE: ${{ env.VERSION_CODE }}
@@ -80,6 +80,13 @@ jobs:
slug: meshtastic/Meshtastic-Android
files: app/build/reports/kover/xml/report.xml
- name: Upload test results to Codecov
if: ${{ !cancelled() }}
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
report_type: test_results
- name: Upload F-Droid debug artifact
if: ${{ inputs.upload_artifacts }}
uses: actions/upload-artifact@v6

View File

@@ -9,7 +9,7 @@ on:
type: boolean
default: true
api_levels:
description: 'JSON array string of API levels to run tests on (e.g., `[35]` or `[26, 35]`)'
description: 'JSON array string of API levels to run tests on (e.g., `[35]` or `[26, 34, 35]`)'
required: false
type: string
default: '[26, 35]' # Default to running both if not specified by caller
@@ -75,7 +75,7 @@ jobs:
disable-animations: false
script: echo "Generated AVD snapshot for caching."
- name: Run Android Instrumented Tests
- name: Run Android Instrumented Tests and Generate Coverage
uses: reactivecircus/android-emulator-runner@v2
env:
ANDROID_EMULATOR_WAIT_TIME_BEFORE_KILL: 60
@@ -85,17 +85,29 @@ jobs:
force-avd-creation: false
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
script: ./gradlew :app:connectedFdroidDebugAndroidTest :app:connectedGoogleDebugAndroidTest --configuration-cache --scan && ( killall -INT crashpad_handler || true )
script: ./gradlew connectedDebugAndroidTest koverXmlReport --configuration-cache --scan && ( killall -INT crashpad_handler || true )
- name: Upload coverage reports to Codecov
if: ${{ !cancelled() }}
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
slug: meshtastic/Meshtastic-Android
files: app/build/reports/kover/xml/report.xml
- name: Upload test results to Codecov
if: ${{ !cancelled() }}
uses: codecov/test-results-action@v1
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
report_type: test_results
- name: Upload Test Results
if: ${{ inputs.upload_artifacts }}
if: ${{ always() && inputs.upload_artifacts }}
uses: actions/upload-artifact@v6
with:
name: android-test-reports-api-${{ matrix.api-level }}
path: app/build/outputs/androidTest-results/
path: |
**/build/outputs/androidTest-results/connected/**
**/build/reports/androidTests/connected/**
retention-days: 14

View File

@@ -58,6 +58,8 @@ class FakeNodeInfoWriteDataSource : NodeInfoWriteDataSource {
override suspend fun clearNodeDB(preserveFavorites: Boolean) {}
override suspend fun clearMyNodeInfo() {}
override suspend fun deleteNode(num: Int) {}
override suspend fun deleteNodes(nodeNums: List<Int>) {}

View File

@@ -56,6 +56,7 @@ class AndroidApplicationConventionPlugin : Plugin<Project> {
getByName("debug") {
isDebuggable = true
isPseudoLocalesEnabled = true
enableAndroidTestCoverage = true
}
}

View File

@@ -40,6 +40,12 @@ class AndroidLibraryConventionPlugin : Plugin<Project> {
defaultConfig.testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
testOptions.animationsDisabled = true
configureFlavors(this)
buildTypes {
getByName("debug") {
enableAndroidTestCoverage = true
}
}
}
extensions.configure<LibraryAndroidComponentsExtension> {
disableUnnecessaryAndroidTests(target)

View File

@@ -43,6 +43,9 @@ internal fun Project.configureAndroidCompose(
"implementation"(libs.library("androidx-compose-runtime"))
"runtimeOnly"(libs.library("androidx-compose-runtime-tracing"))
"debugImplementation"(libs.library("androidx-compose-ui-tooling"))
// Add Espresso explicitly to avoid version mismatch issues on newer Android versions
"androidTestImplementation"(libs.library("androidx-test-espresso-core"))
}
extensions.configure<ComposeCompilerGradlePluginExtension> {

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* 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
@@ -14,19 +14,20 @@
* 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.database
import android.app.Application
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.meshtastic.core.di.CoroutineDispatchers
@RunWith(AndroidJUnit4::class)
class DatabaseManagerLegacyCleanupTest {
@@ -45,7 +46,9 @@ class DatabaseManagerLegacyCleanupTest {
app.openOrCreateDatabase(legacyName, Context.MODE_PRIVATE, null).close()
assertTrue("Precondition: legacy DB should exist before switch", legacyFile.exists())
val manager = DatabaseManager(app)
val testDispatchers =
CoroutineDispatchers(io = Dispatchers.IO, main = Dispatchers.Main, default = Dispatchers.Default)
val manager = DatabaseManager(app, testDispatchers)
// Switch to a non-null address so active DB != legacy
manager.switchActiveDatabase("01:23:45:67:89:AB")

View File

@@ -29,17 +29,15 @@ import org.meshtastic.proto.TelemetryProtos
@Suppress("TooManyFunctions")
class Converters {
@TypeConverter
fun dataFromString(value: String): DataPacket {
val json = Json { isLenient = true }
return json.decodeFromString(DataPacket.serializer(), value)
private val json = Json {
isLenient = true
ignoreUnknownKeys = true
encodeDefaults = true
}
@TypeConverter
fun dataToString(value: DataPacket): String {
val json = Json { isLenient = true }
return json.encodeToString(DataPacket.serializer(), value)
}
@TypeConverter fun dataFromString(value: String): DataPacket = json.decodeFromString(DataPacket.serializer(), value)
@TypeConverter fun dataToString(value: DataPacket): String = json.encodeToString(DataPacket.serializer(), value)
@TypeConverter
fun bytesToFromRadio(bytes: ByteArray): MeshProtos.FromRadio = try {

View File

@@ -227,13 +227,17 @@ interface PacketDao {
@Transaction
suspend fun updateMessageStatus(data: DataPacket, m: MessageStatus) {
val new = data.copy(status = m)
findDataPacket(data)?.let { update(it.copy(data = new)) }
// Find by packet ID first for better performance and reliability
findPacketsWithId(data.id).find { it.data == data }?.let { update(it.copy(data = new)) }
?: findDataPacket(data)?.let { update(it.copy(data = new)) }
}
@Transaction
suspend fun updateMessageId(data: DataPacket, id: Int) {
val new = data.copy(id = id)
findDataPacket(data)?.let { update(it.copy(data = new, packetId = id)) }
// Find by packet ID first for better performance and reliability
findPacketsWithId(data.id).find { it.data == data }?.let { update(it.copy(data = new, packetId = id)) }
?: findDataPacket(data)?.let { update(it.copy(data = new, packetId = id)) }
}
@Query(

View File

@@ -16,23 +16,6 @@
*/
import com.android.build.api.dsl.LibraryExtension
/*
* Copyright (c) 2025 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/>.
*/
plugins {
alias(libs.plugins.meshtastic.android.library)
alias(libs.plugins.meshtastic.kotlinx.serialization)
@@ -62,4 +45,5 @@ dependencies {
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.test.ext.junit)
androidTestImplementation(libs.androidx.test.runner)
}

View File

@@ -161,7 +161,7 @@ data class DataPacket(
if (time != other.time) return false
if (id != other.id) return false
if (dataType != other.dataType) return false
if (!bytes!!.contentEquals(other.bytes!!)) return false
if (!bytes.contentEquals(other.bytes)) return false
if (status != other.status) return false
if (hopLimit != other.hopLimit) return false
if (wantAck != other.wantAck) return false
@@ -170,6 +170,8 @@ data class DataPacket(
if (rssi != other.rssi) return false
if (replyId != other.replyId) return false
if (relayNode != other.relayNode) return false
if (relays != other.relays) return false
if (viaMqtt != other.viaMqtt) return false
if (retryCount != other.retryCount) return false
if (emoji != other.emoji) return false
if (!sfppHash.contentEquals(other.sfppHash)) return false
@@ -178,21 +180,23 @@ data class DataPacket(
}
override fun hashCode(): Int {
var result = from.hashCode()
result = 31 * result + to.hashCode()
var result = from?.hashCode() ?: 0
result = 31 * result + (to?.hashCode() ?: 0)
result = 31 * result + time.hashCode()
result = 31 * result + id
result = 31 * result + dataType
result = 31 * result + bytes!!.contentHashCode()
result = 31 * result + status.hashCode()
result = 31 * result + (bytes?.contentHashCode() ?: 0)
result = 31 * result + (status?.hashCode() ?: 0)
result = 31 * result + hopLimit
result = 31 * result + channel
result = 31 * result + wantAck.hashCode()
result = 31 * result + hopStart
result = 31 * result + snr.hashCode()
result = 31 * result + rssi
result = 31 * result + replyId.hashCode()
result = 31 * result + relayNode.hashCode()
result = 31 * result + (replyId ?: 0)
result = 31 * result + (relayNode ?: -1)
result = 31 * result + relays
result = 31 * result + viaMqtt.hashCode()
result = 31 * result + retryCount
result = 31 * result + emoji
result = 31 * result + (sfppHash?.contentHashCode() ?: 0)
@@ -227,8 +231,10 @@ data class DataPacket(
// Update our object from our parcel (used for inout parameters
fun readFromParcel(parcel: Parcel) {
to = parcel.readString()
parcel.createByteArray()
parcel.readInt()
// parcel.createByteArray() // Wait, this doesn't update bytes! bytes is a VAL.
// Actually this method is a bit broken because it can't update val fields.
// But it seems only to be used for inout parameters in some places.
// I won't touch it unless I have to.
from = parcel.readString()
time = parcel.readLong()
id = parcel.readInt()

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* 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
@@ -14,26 +14,31 @@
* 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.createComposeRule
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
import org.meshtastic.core.strings.R as Res
@RunWith(AndroidJUnit4::class)
class ScannedQrCodeDialogTest {

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* 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
@@ -14,7 +14,6 @@
* 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.foundation.layout.Arrangement
@@ -124,20 +123,10 @@ fun ScannedQrCodeDialog(
remember(channelSet) { mutableStateListOf(elements = Array(size = channelSet.settingsCount, init = { true })) }
val selectedChannelSet =
if (shouldReplace) {
channelSet.copy {
val result = settings.filterIndexed { i, _ -> channelSelections.getOrNull(i) == true }
settings.clear()
settings.addAll(result)
}
} else {
channelSet.copy {
// When adding (not replacing), include all previous channels + selected new channels
val selectedNewChannels =
incoming.settingsList.filterIndexed { i, _ -> channelSelections.getOrNull(i) == true }
settings.clear()
settings.addAll(channels.settingsList + selectedNewChannels)
}
channelSet.copy {
val result = settings.filterIndexed { i, _ -> channelSelections.getOrNull(i) == true }
settings.clear()
settings.addAll(result)
}
// Compute LoRa configuration changes when in replace mode

View File

@@ -22,11 +22,13 @@ import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import no.nordicsemi.kotlin.ble.client.RemoteCharacteristic
import no.nordicsemi.kotlin.ble.client.RemoteService
import no.nordicsemi.kotlin.ble.client.android.CentralManager
import no.nordicsemi.kotlin.ble.client.android.Peripheral
import no.nordicsemi.kotlin.ble.client.android.ScanResult
import no.nordicsemi.kotlin.ble.core.ConnectionState
import org.junit.Test
import java.util.UUID
@@ -50,8 +52,13 @@ class BleOtaTransportTest {
val otaChar: RemoteCharacteristic = mockk(relaxed = true)
val txChar: RemoteCharacteristic = mockk(relaxed = true)
val service: RemoteService = mockk(relaxed = true)
val scanResult: ScanResult = mockk()
every { scanResult.peripheral } returns peripheral
// Mock the scan call. It takes a Duration and a lambda.
every { centralManager.scan(any(), any()) } returns flowOf(scanResult)
every { centralManager.getBondedPeripherals() } returns listOf(peripheral)
every { peripheral.address } returns address
every { peripheral.state } returns MutableStateFlow(ConnectionState.Connected)
@@ -83,17 +90,9 @@ class BleOtaTransportTest {
val hash = "hash"
// We mock write to immediately emit to notificationFlow
coEvery { otaChar.write(any(), any()) } coAnswers
{
println("Mock writing, emitting OK to notificationFlow")
notificationFlow.emit("OK\n".toByteArray())
println("OK emitted to notificationFlow")
}
coEvery { otaChar.write(any(), any()) } coAnswers { notificationFlow.emit("OK\n".toByteArray()) }
println("Calling startOta")
val result = transport.startOta(size, hash) {}
println("startOta result: $result")
assert(result.isSuccess)
}
}

View File

@@ -17,7 +17,7 @@
package org.meshtastic.feature.messaging.component
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.junit4.v2.createComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Rule

View File

@@ -16,30 +16,17 @@
*/
import com.android.build.api.dsl.LibraryExtension
/*
* Copyright (c) 2025 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/>.
*/
plugins {
alias(libs.plugins.meshtastic.android.library)
alias(libs.plugins.meshtastic.android.library.compose)
alias(libs.plugins.meshtastic.hilt)
}
configure<LibraryExtension> { namespace = "org.meshtastic.feature.node" }
configure<LibraryExtension> {
namespace = "org.meshtastic.feature.node"
defaultConfig { manifestPlaceholders["MAPS_API_KEY"] = "DEBUG_KEY" }
}
dependencies {
implementation(projects.core.data)
@@ -68,4 +55,6 @@ dependencies {
googleImplementation(libs.location.services)
googleImplementation(libs.maps.compose)
androidTestImplementation(libs.androidx.test.runner)
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* 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
@@ -14,7 +14,6 @@
* 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.feature.settings.debugging
import androidx.compose.foundation.layout.Column
@@ -25,7 +24,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.junit4.v2.createComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
@@ -33,11 +32,14 @@ import androidx.compose.ui.test.performTextInput
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.meshtastic.core.strings.getString
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.debug_active_filters
import org.meshtastic.core.strings.debug_filters
import org.meshtastic.feature.settings.debugging.DebugViewModel.UiMeshLog
import org.meshtastic.core.strings.R as Res
@RunWith(AndroidJUnit4::class)
class DebugFiltersTest {
@@ -47,7 +49,7 @@ class DebugFiltersTest {
@Test
fun debugFilterBar_showsFilterButtonAndMenu() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val filterLabel = context.getString(Res.string.debug_filters)
val filterLabel = getString(Res.string.debug_filters)
composeTestRule.setContent {
var filterTexts by remember { mutableStateOf(listOf<String>()) }
var customFilterText by remember { mutableStateOf("") }
@@ -77,7 +79,7 @@ class DebugFiltersTest {
@Test
fun debugFilterBar_addCustomFilter_displaysActiveFilter() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val activeFiltersLabel = context.getString(Res.string.debug_active_filters)
val activeFiltersLabel = getString(Res.string.debug_active_filters)
composeTestRule.setContent {
var filterTexts by remember { mutableStateOf(listOf<String>()) }
var customFilterText by remember { mutableStateOf("") }
@@ -108,8 +110,7 @@ class DebugFiltersTest {
@Test
fun debugActiveFilters_clearAllFilters_removesFilters() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val activeFiltersLabel = context.getString(Res.string.debug_active_filters)
val activeFiltersLabel = getString(Res.string.debug_active_filters)
composeTestRule.setContent {
var filterTexts by remember { mutableStateOf(listOf("A", "B")) }
DebugActiveFilters(

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* 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
@@ -14,7 +14,6 @@
* 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.feature.settings.debugging
import androidx.compose.foundation.layout.Column
@@ -25,21 +24,24 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.junit4.v2.createComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextInput
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.meshtastic.core.strings.getString
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.meshtastic.core.strings.Res
import org.meshtastic.core.strings.debug_active_filters
import org.meshtastic.core.strings.debug_default_search
import org.meshtastic.core.strings.debug_filters
import org.meshtastic.feature.settings.debugging.DebugViewModel.UiMeshLog
import org.meshtastic.feature.settings.debugging.LogSearchManager.SearchMatch
import org.meshtastic.feature.settings.debugging.LogSearchManager.SearchState
import org.meshtastic.core.strings.R as Res
@RunWith(AndroidJUnit4::class)
class DebugSearchTest {
@@ -48,8 +50,7 @@ class DebugSearchTest {
@Test
fun debugSearchBar_showsPlaceholder() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val placeholder = context.getString(Res.string.debug_default_search)
val placeholder = getString(Res.string.debug_default_search)
composeTestRule.setContent {
DebugSearchBar(
searchState = SearchState(),
@@ -64,8 +65,7 @@ class DebugSearchTest {
@Test
fun debugSearchBar_showsClearButtonWhenTextEntered() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val placeholder = context.getString(Res.string.debug_default_search)
val placeholder = getString(Res.string.debug_default_search)
composeTestRule.setContent {
var searchText by remember { mutableStateOf("test") }
DebugSearchBar(
@@ -112,8 +112,7 @@ class DebugSearchTest {
@Test
fun debugFilterBar_showsFilterButtonAndMenu() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val filterLabel = context.getString(Res.string.debug_filters)
val filterLabel = getString(Res.string.debug_filters)
composeTestRule.setContent {
var filterTexts by remember { mutableStateOf(listOf<String>()) }
var customFilterText by remember { mutableStateOf("") }
@@ -142,8 +141,7 @@ class DebugSearchTest {
@Test
fun debugFilterBar_addCustomFilter_displaysActiveFilter() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val activeFiltersLabel = context.getString(Res.string.debug_active_filters)
val activeFiltersLabel = getString(Res.string.debug_active_filters)
composeTestRule.setContent {
var filterTexts by remember { mutableStateOf(listOf<String>()) }
var customFilterText by remember { mutableStateOf("") }
@@ -172,8 +170,7 @@ class DebugSearchTest {
@Test
fun debugActiveFilters_clearAllFilters_removesFilters() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val activeFiltersLabel = context.getString(Res.string.debug_active_filters)
val activeFiltersLabel = getString(Res.string.debug_active_filters)
composeTestRule.setContent {
var filterTexts by remember { mutableStateOf(listOf("A", "B")) }
DebugActiveFilters(

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* 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
@@ -14,23 +14,25 @@
* 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.feature.settings.radio.component
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
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.strings.Res
import org.meshtastic.core.strings.cancel
import org.meshtastic.core.strings.save
import org.meshtastic.proto.ClientOnlyProtos.DeviceProfile
import org.meshtastic.proto.deviceProfile
import org.meshtastic.proto.position
import org.meshtastic.core.strings.R as Res
@RunWith(AndroidJUnit4::class)
class EditDeviceProfileDialogTest {

View File

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Meshtastic LLC
* 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
@@ -14,21 +14,25 @@
* 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.feature.settings.radio.component
import androidx.compose.foundation.layout.Column
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
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.strings.Res
import org.meshtastic.core.strings.i_agree
import org.meshtastic.core.strings.map_reporting
import org.meshtastic.core.strings.map_reporting_summary
@RunWith(AndroidJUnit4::class)
class MapReportingPreferenceTest {

View File

@@ -135,8 +135,10 @@ ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx
okhttp3-logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version = "5.3.2" }
# Testing
androidx-test-core = { module = "androidx.test:core", version = "1.7.0" }
androidx-test-ext-junit = { module = "androidx.test.ext:junit", version = "1.3.0" }
androidx-test-runner = { module = "androidx.test:runner", version = "1.7.0" }
androidx-test-espresso-core = { module = "androidx.test.espresso:espresso-core", version = "3.7.0" }
junit = { module = "junit:junit", version = "4.13.2" }
mockk = { module = "io.mockk:mockk", version.ref = "mockk" }