mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-03-29 11:13:41 -04:00
Refactor command handling, enhance tests, and improve discovery logic (#4878)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
package org.meshtastic.feature.settings.radio
|
||||
|
||||
import dev.mokkery.MockMode
|
||||
import dev.mokkery.answering.returns
|
||||
import dev.mokkery.everySuspend
|
||||
import dev.mokkery.matcher.any
|
||||
import dev.mokkery.mock
|
||||
@@ -58,7 +59,7 @@ class CleanNodeDatabaseViewModelTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getNodesToDelete updates state`() = runTest {
|
||||
fun getNodesToDelete_updates_state() = runTest {
|
||||
val nodes = listOf(Node(num = 1), Node(num = 2))
|
||||
everySuspend { cleanNodeDatabaseUseCase.getNodesToClean(any(), any(), any()) } returns nodes
|
||||
|
||||
@@ -69,7 +70,7 @@ class CleanNodeDatabaseViewModelTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cleanNodes calls useCase and clears state`() = runTest {
|
||||
fun cleanNodes_calls_useCase_and_clears_state() = runTest {
|
||||
val nodes = listOf(Node(num = 1))
|
||||
everySuspend { cleanNodeDatabaseUseCase.getNodesToClean(any(), any(), any()) } returns nodes
|
||||
viewModel.getNodesToDelete()
|
||||
@@ -20,7 +20,6 @@ import android.app.Activity
|
||||
import android.content.Intent
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@@ -36,7 +35,6 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.common.util.nowMillis
|
||||
import org.meshtastic.core.common.util.toDate
|
||||
@@ -46,23 +44,18 @@ import org.meshtastic.core.navigation.Route
|
||||
import org.meshtastic.core.navigation.SettingsRoutes
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.bottom_nav_settings
|
||||
import org.meshtastic.core.resources.choose_theme
|
||||
import org.meshtastic.core.resources.dynamic
|
||||
import org.meshtastic.core.resources.export_configuration
|
||||
import org.meshtastic.core.resources.import_configuration
|
||||
import org.meshtastic.core.resources.preferences_language
|
||||
import org.meshtastic.core.resources.remotely_administrating
|
||||
import org.meshtastic.core.resources.theme_dark
|
||||
import org.meshtastic.core.resources.theme_light
|
||||
import org.meshtastic.core.resources.theme_system
|
||||
import org.meshtastic.core.ui.component.ListItem
|
||||
import org.meshtastic.core.ui.component.MainAppBar
|
||||
import org.meshtastic.core.ui.component.MeshtasticDialog
|
||||
import org.meshtastic.core.ui.theme.MODE_DYNAMIC
|
||||
import org.meshtastic.feature.settings.component.AppInfoSection
|
||||
import org.meshtastic.feature.settings.component.AppearanceSection
|
||||
import org.meshtastic.feature.settings.component.PersistenceSection
|
||||
import org.meshtastic.feature.settings.component.PrivacySection
|
||||
import org.meshtastic.feature.settings.component.ThemePickerDialog
|
||||
import org.meshtastic.feature.settings.navigation.ConfigRoute
|
||||
import org.meshtastic.feature.settings.navigation.ModuleRoute
|
||||
import org.meshtastic.feature.settings.radio.RadioConfigItemList
|
||||
@@ -269,28 +262,3 @@ private fun LanguagePickerDialog(onDismiss: () -> Unit) {
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private enum class ThemeOption(val label: StringResource, val mode: Int) {
|
||||
DYNAMIC(label = Res.string.dynamic, mode = MODE_DYNAMIC),
|
||||
LIGHT(label = Res.string.theme_light, mode = AppCompatDelegate.MODE_NIGHT_NO),
|
||||
DARK(label = Res.string.theme_dark, mode = AppCompatDelegate.MODE_NIGHT_YES),
|
||||
SYSTEM(label = Res.string.theme_system, mode = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM),
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ThemePickerDialog(onClickTheme: (Int) -> Unit, onDismiss: () -> Unit) {
|
||||
MeshtasticDialog(
|
||||
title = stringResource(Res.string.choose_theme),
|
||||
onDismiss = onDismiss,
|
||||
text = {
|
||||
Column {
|
||||
ThemeOption.entries.forEach { option ->
|
||||
ListItem(text = stringResource(option.label), trailingIcon = null) {
|
||||
onClickTheme(option.mode)
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -57,32 +57,7 @@ private suspend fun exportAllLogsToUri(context: Context, targetUri: Uri, logs: L
|
||||
}
|
||||
|
||||
context.contentResolver.openOutputStream(targetUri)?.use { os ->
|
||||
OutputStreamWriter(os, StandardCharsets.UTF_8).use { writer ->
|
||||
logs.forEach { log ->
|
||||
writer.write("${log.formattedReceivedDate} [${log.messageType}]\n")
|
||||
writer.write(log.logMessage)
|
||||
log.decodedPayload?.let { decodedPayload ->
|
||||
if (decodedPayload.isNotBlank()) {
|
||||
writer.write("\n\nDecoded Payload:\n{\n")
|
||||
// Redact Decoded keys.
|
||||
decodedPayload.lineSequence().forEach { line ->
|
||||
var outputLine = line
|
||||
val redacted = redactedKeys.firstOrNull { line.contains(it) }
|
||||
if (redacted != null) {
|
||||
val idx = line.indexOf(':')
|
||||
if (idx != -1) {
|
||||
outputLine = line.take(idx + 1)
|
||||
outputLine += "<redacted>"
|
||||
}
|
||||
}
|
||||
writer.write(outputLine)
|
||||
writer.write("\n")
|
||||
}
|
||||
writer.write("}\n\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
OutputStreamWriter(os, StandardCharsets.UTF_8).use { writer -> formatLogsTo(writer, logs) }
|
||||
}
|
||||
Logger.i { "MeshLog exported successfully to $targetUri" }
|
||||
withContext(Dispatchers.Main) { context.showToast(Res.string.debug_export_success, logs.size) }
|
||||
@@ -91,5 +66,3 @@ private suspend fun exportAllLogsToUri(context: Context, targetUri: Uri, logs: L
|
||||
withContext(Dispatchers.Main) { context.showToast(Res.string.debug_export_failed, e.message ?: "") }
|
||||
}
|
||||
}
|
||||
|
||||
private val redactedKeys = listOf("session_passkey", "private_key", "admin_key")
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
@file:Suppress("MatchingDeclarationName")
|
||||
|
||||
package org.meshtastic.feature.settings.component
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.runtime.Composable
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.choose_theme
|
||||
import org.meshtastic.core.resources.dynamic
|
||||
import org.meshtastic.core.resources.theme_dark
|
||||
import org.meshtastic.core.resources.theme_light
|
||||
import org.meshtastic.core.resources.theme_system
|
||||
import org.meshtastic.core.ui.component.ListItem
|
||||
import org.meshtastic.core.ui.component.MeshtasticDialog
|
||||
import org.meshtastic.core.ui.theme.MODE_DYNAMIC
|
||||
|
||||
/** Theme modes that match AppCompatDelegate constants for cross-platform use. */
|
||||
enum class ThemeOption(val label: StringResource, val mode: Int) {
|
||||
DYNAMIC(label = Res.string.dynamic, mode = MODE_DYNAMIC),
|
||||
LIGHT(label = Res.string.theme_light, mode = 1), // AppCompatDelegate.MODE_NIGHT_NO
|
||||
DARK(label = Res.string.theme_dark, mode = 2), // AppCompatDelegate.MODE_NIGHT_YES
|
||||
SYSTEM(label = Res.string.theme_system, mode = -1), // AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||
}
|
||||
|
||||
/** Shared dialog for picking a theme option. Used by both Android and Desktop settings screens. */
|
||||
@Composable
|
||||
fun ThemePickerDialog(onClickTheme: (Int) -> Unit, onDismiss: () -> Unit) {
|
||||
MeshtasticDialog(
|
||||
title = stringResource(Res.string.choose_theme),
|
||||
onDismiss = onDismiss,
|
||||
text = {
|
||||
Column {
|
||||
ThemeOption.entries.forEach { option ->
|
||||
ListItem(text = stringResource(option.label), trailingIcon = null) {
|
||||
onClickTheme(option.mode)
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2026 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
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.feature.settings.debugging
|
||||
|
||||
internal val redactedKeys = listOf("session_passkey", "private_key", "admin_key")
|
||||
|
||||
/**
|
||||
* Formats a list of [DebugViewModel.UiMeshLog] entries into the given [Appendable], redacting sensitive keys in decoded
|
||||
* payloads.
|
||||
*/
|
||||
internal fun formatLogsTo(out: Appendable, logs: List<DebugViewModel.UiMeshLog>) {
|
||||
logs.forEach { log ->
|
||||
out.append("${log.formattedReceivedDate} [${log.messageType}]\n")
|
||||
out.append(log.logMessage)
|
||||
val decodedPayload = log.decodedPayload
|
||||
if (!decodedPayload.isNullOrBlank()) {
|
||||
appendRedactedPayload(out, decodedPayload)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun appendRedactedPayload(out: Appendable, payload: String) {
|
||||
out.append("\n\nDecoded Payload:\n{\n")
|
||||
payload.lineSequence().forEach { line ->
|
||||
out.append(redactLine(line))
|
||||
out.append("\n")
|
||||
}
|
||||
out.append("}\n\n")
|
||||
}
|
||||
|
||||
private fun redactLine(line: String): String {
|
||||
if (redactedKeys.none { line.contains(it) }) return line
|
||||
val idx = line.indexOf(':')
|
||||
return if (idx != -1) line.take(idx + 1) + "<redacted>" else line
|
||||
}
|
||||
@@ -17,15 +17,17 @@
|
||||
package org.meshtastic.feature.settings.filter
|
||||
|
||||
import dev.mokkery.MockMode
|
||||
import dev.mokkery.answering.returns
|
||||
import dev.mokkery.every
|
||||
import dev.mokkery.matcher.any
|
||||
import dev.mokkery.mock
|
||||
import dev.mokkery.verify
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import org.meshtastic.core.repository.FilterPrefs
|
||||
import org.meshtastic.core.repository.MessageFilter
|
||||
import kotlin.test.BeforeTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class FilterSettingsViewModelTest {
|
||||
|
||||
@@ -34,23 +36,23 @@ class FilterSettingsViewModelTest {
|
||||
|
||||
private lateinit var viewModel: FilterSettingsViewModel
|
||||
|
||||
@Before
|
||||
@BeforeTest
|
||||
fun setUp() {
|
||||
every { filterPrefs.filterEnabled.value } returns true
|
||||
every { filterPrefs.filterWords.value } returns setOf("apple", "banana")
|
||||
every { filterPrefs.filterEnabled } returns MutableStateFlow(true)
|
||||
every { filterPrefs.filterWords } returns MutableStateFlow(setOf("apple", "banana"))
|
||||
|
||||
viewModel = FilterSettingsViewModel(filterPrefs = filterPrefs, messageFilter = messageFilter)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `setFilterEnabled updates prefs and state`() {
|
||||
fun setFilterEnabled_updates_prefs_and_state() {
|
||||
viewModel.setFilterEnabled(false)
|
||||
verify { filterPrefs.setFilterEnabled(false) }
|
||||
assertEquals(false, viewModel.filterEnabled.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `addFilterWord updates prefs and rebuilds patterns`() {
|
||||
fun addFilterWord_updates_prefs_and_rebuilds_patterns() {
|
||||
viewModel.addFilterWord("cherry")
|
||||
|
||||
verify { filterPrefs.setFilterWords(any()) }
|
||||
@@ -59,7 +61,7 @@ class FilterSettingsViewModelTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `removeFilterWord updates prefs and rebuilds patterns`() {
|
||||
fun removeFilterWord_updates_prefs_and_rebuilds_patterns() {
|
||||
viewModel.removeFilterWord("apple")
|
||||
|
||||
verify { filterPrefs.setFilterWords(any()) }
|
||||
@@ -42,7 +42,6 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.meshtastic.core.database.DatabaseConstants
|
||||
import org.meshtastic.core.navigation.Route
|
||||
@@ -52,28 +51,23 @@ import org.meshtastic.core.resources.acknowledgements
|
||||
import org.meshtastic.core.resources.app_settings
|
||||
import org.meshtastic.core.resources.app_version
|
||||
import org.meshtastic.core.resources.bottom_nav_settings
|
||||
import org.meshtastic.core.resources.choose_theme
|
||||
import org.meshtastic.core.resources.device_db_cache_limit
|
||||
import org.meshtastic.core.resources.device_db_cache_limit_summary
|
||||
import org.meshtastic.core.resources.dynamic
|
||||
import org.meshtastic.core.resources.info
|
||||
import org.meshtastic.core.resources.modules_already_unlocked
|
||||
import org.meshtastic.core.resources.modules_unlocked
|
||||
import org.meshtastic.core.resources.preferences_language
|
||||
import org.meshtastic.core.resources.remotely_administrating
|
||||
import org.meshtastic.core.resources.theme
|
||||
import org.meshtastic.core.resources.theme_dark
|
||||
import org.meshtastic.core.resources.theme_light
|
||||
import org.meshtastic.core.resources.theme_system
|
||||
import org.meshtastic.core.ui.component.DropDownPreference
|
||||
import org.meshtastic.core.ui.component.ListItem
|
||||
import org.meshtastic.core.ui.component.MainAppBar
|
||||
import org.meshtastic.core.ui.component.MeshtasticDialog
|
||||
import org.meshtastic.core.ui.theme.MODE_DYNAMIC
|
||||
import org.meshtastic.core.ui.util.rememberShowToastResource
|
||||
import org.meshtastic.feature.settings.component.ExpressiveSection
|
||||
import org.meshtastic.feature.settings.component.HomoglyphSetting
|
||||
import org.meshtastic.feature.settings.component.NotificationSection
|
||||
import org.meshtastic.feature.settings.component.ThemePickerDialog
|
||||
import org.meshtastic.feature.settings.navigation.ConfigRoute
|
||||
import org.meshtastic.feature.settings.navigation.ModuleRoute
|
||||
import org.meshtastic.feature.settings.radio.RadioConfigItemList
|
||||
@@ -291,31 +285,6 @@ private fun DesktopAppVersionButton(
|
||||
}
|
||||
}
|
||||
|
||||
private enum class ThemeOption(val label: StringResource, val mode: Int) {
|
||||
DYNAMIC(label = Res.string.dynamic, mode = MODE_DYNAMIC),
|
||||
LIGHT(label = Res.string.theme_light, mode = 1), // MODE_NIGHT_NO
|
||||
DARK(label = Res.string.theme_dark, mode = 2), // MODE_NIGHT_YES
|
||||
SYSTEM(label = Res.string.theme_system, mode = -1), // MODE_NIGHT_FOLLOW_SYSTEM
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ThemePickerDialog(onClickTheme: (Int) -> Unit, onDismiss: () -> Unit) {
|
||||
MeshtasticDialog(
|
||||
title = stringResource(Res.string.choose_theme),
|
||||
onDismiss = onDismiss,
|
||||
text = {
|
||||
Column {
|
||||
ThemeOption.entries.forEach { option ->
|
||||
ListItem(text = stringResource(option.label), trailingIcon = null) {
|
||||
onClickTheme(option.mode)
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Supported languages — tag must match the CMP `values-<qualifier>` directory names. Empty tag means system default.
|
||||
* Display names are written in the native language for clarity.
|
||||
|
||||
@@ -54,32 +54,7 @@ actual fun rememberLogExporter(logsProvider: suspend () -> List<DebugViewModel.U
|
||||
val exportFile = File(directory, selectedFile)
|
||||
try {
|
||||
FileOutputStream(exportFile).use { fos ->
|
||||
OutputStreamWriter(fos, StandardCharsets.UTF_8).use { writer ->
|
||||
logs.forEach { log ->
|
||||
writer.write("${log.formattedReceivedDate} [${log.messageType}]\n")
|
||||
writer.write(log.logMessage)
|
||||
log.decodedPayload?.let { decodedPayload ->
|
||||
if (decodedPayload.isNotBlank()) {
|
||||
writer.write("\n\nDecoded Payload:\n{\n")
|
||||
// Redact Decoded keys.
|
||||
decodedPayload.lineSequence().forEach { line ->
|
||||
var outputLine = line
|
||||
val redacted = redactedKeys.firstOrNull { line.contains(it) }
|
||||
if (redacted != null) {
|
||||
val idx = line.indexOf(':')
|
||||
if (idx != -1) {
|
||||
outputLine = line.take(idx + 1)
|
||||
outputLine += "<redacted>"
|
||||
}
|
||||
}
|
||||
writer.write(outputLine)
|
||||
writer.write("\n")
|
||||
}
|
||||
writer.write("}\n\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
OutputStreamWriter(fos, StandardCharsets.UTF_8).use { writer -> formatLogsTo(writer, logs) }
|
||||
}
|
||||
Logger.i { "MeshLog exported successfully to ${exportFile.absolutePath}" }
|
||||
} catch (e: java.io.IOException) {
|
||||
@@ -92,5 +67,3 @@ actual fun rememberLogExporter(logsProvider: suspend () -> List<DebugViewModel.U
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val redactedKeys = listOf("session_passkey", "private_key", "admin_key")
|
||||
|
||||
@@ -1,140 +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.feature.settings
|
||||
|
||||
import dev.mokkery.MockMode
|
||||
import dev.mokkery.every
|
||||
import dev.mokkery.mock
|
||||
import dev.mokkery.verify
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.resetMain
|
||||
import kotlinx.coroutines.test.setMain
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.meshtastic.core.common.BuildConfigProvider
|
||||
import org.meshtastic.core.common.database.DatabaseManager
|
||||
import org.meshtastic.core.domain.usecase.settings.ExportDataUseCase
|
||||
import org.meshtastic.core.domain.usecase.settings.IsOtaCapableUseCase
|
||||
import org.meshtastic.core.domain.usecase.settings.MeshLocationUseCase
|
||||
import org.meshtastic.core.domain.usecase.settings.SetAppIntroCompletedUseCase
|
||||
import org.meshtastic.core.domain.usecase.settings.SetDatabaseCacheLimitUseCase
|
||||
import org.meshtastic.core.domain.usecase.settings.SetMeshLogSettingsUseCase
|
||||
import org.meshtastic.core.domain.usecase.settings.SetProvideLocationUseCase
|
||||
import org.meshtastic.core.domain.usecase.settings.SetThemeUseCase
|
||||
import org.meshtastic.core.model.RadioController
|
||||
import org.meshtastic.core.repository.MeshLogPrefs
|
||||
import org.meshtastic.core.repository.NodeRepository
|
||||
import org.meshtastic.core.repository.RadioConfigRepository
|
||||
import org.meshtastic.core.repository.UiPrefs
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Config(sdk = [34])
|
||||
class LegacySettingsViewModelTest {
|
||||
|
||||
private val testDispatcher = StandardTestDispatcher()
|
||||
|
||||
private val radioConfigRepository: RadioConfigRepository = mock(MockMode.autofill)
|
||||
private val radioController: RadioController = mock(MockMode.autofill)
|
||||
private val nodeRepository: NodeRepository = mock(MockMode.autofill)
|
||||
private val uiPrefs: UiPrefs = mock(MockMode.autofill)
|
||||
private val buildConfigProvider: BuildConfigProvider = mock(MockMode.autofill)
|
||||
private val databaseManager: DatabaseManager = mock(MockMode.autofill)
|
||||
private val meshLogPrefs: MeshLogPrefs = mock(MockMode.autofill)
|
||||
|
||||
private lateinit var setThemeUseCase: SetThemeUseCase
|
||||
private lateinit var setAppIntroCompletedUseCase: SetAppIntroCompletedUseCase
|
||||
private lateinit var setProvideLocationUseCase: SetProvideLocationUseCase
|
||||
private lateinit var setDatabaseCacheLimitUseCase: SetDatabaseCacheLimitUseCase
|
||||
private lateinit var setMeshLogSettingsUseCase: SetMeshLogSettingsUseCase
|
||||
private lateinit var meshLocationUseCase: MeshLocationUseCase
|
||||
private lateinit var exportDataUseCase: ExportDataUseCase
|
||||
private lateinit var isOtaCapableUseCase: IsOtaCapableUseCase
|
||||
|
||||
private lateinit var viewModel: SettingsViewModel
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
Dispatchers.setMain(testDispatcher)
|
||||
|
||||
setThemeUseCase = mock(MockMode.autofill)
|
||||
setAppIntroCompletedUseCase = mock(MockMode.autofill)
|
||||
setProvideLocationUseCase = mock(MockMode.autofill)
|
||||
setDatabaseCacheLimitUseCase = mock(MockMode.autofill)
|
||||
setMeshLogSettingsUseCase = mock(MockMode.autofill)
|
||||
meshLocationUseCase = mock(MockMode.autofill)
|
||||
exportDataUseCase = mock(MockMode.autofill)
|
||||
isOtaCapableUseCase = mock(MockMode.autofill)
|
||||
|
||||
// Return real StateFlows to avoid ClassCastException
|
||||
every { databaseManager.cacheLimit } returns MutableStateFlow(100)
|
||||
every { nodeRepository.myNodeInfo } returns MutableStateFlow(null)
|
||||
every { nodeRepository.ourNodeInfo } returns MutableStateFlow(null)
|
||||
every { radioConfigRepository.localConfigFlow } returns MutableStateFlow(org.meshtastic.proto.LocalConfig())
|
||||
every { radioController.connectionState } returns
|
||||
MutableStateFlow(org.meshtastic.core.model.ConnectionState.Connected)
|
||||
every { isOtaCapableUseCase() } returns flowOf(false)
|
||||
|
||||
viewModel =
|
||||
SettingsViewModel(
|
||||
app = mock(),
|
||||
radioConfigRepository = radioConfigRepository,
|
||||
radioController = radioController,
|
||||
nodeRepository = nodeRepository,
|
||||
uiPrefs = uiPrefs,
|
||||
buildConfigProvider = buildConfigProvider,
|
||||
databaseManager = databaseManager,
|
||||
meshLogPrefs = meshLogPrefs,
|
||||
setThemeUseCase = setThemeUseCase,
|
||||
setAppIntroCompletedUseCase = setAppIntroCompletedUseCase,
|
||||
setProvideLocationUseCase = setProvideLocationUseCase,
|
||||
setDatabaseCacheLimitUseCase = setDatabaseCacheLimitUseCase,
|
||||
setMeshLogSettingsUseCase = setMeshLogSettingsUseCase,
|
||||
meshLocationUseCase = meshLocationUseCase,
|
||||
exportDataUseCase = exportDataUseCase,
|
||||
isOtaCapableUseCase = isOtaCapableUseCase,
|
||||
)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
Dispatchers.resetMain()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `setTheme calls useCase`() {
|
||||
viewModel.setTheme(1)
|
||||
verify { setThemeUseCase(1) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `setDbCacheLimit calls useCase`() {
|
||||
viewModel.setDbCacheLimit(50)
|
||||
verify { setDatabaseCacheLimitUseCase(50) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `startProvidingLocation calls useCase`() {
|
||||
viewModel.startProvidingLocation()
|
||||
verify { meshLocationUseCase.startProvidingLocation() }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user