feat: Enhance test coverage (#4847)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich 2026-03-18 22:09:19 -05:00 committed by GitHub
parent 1b0dc75dfe
commit 06b9f8c77a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 1715 additions and 502 deletions

View file

@ -52,9 +52,9 @@ open class AlertManager {
)
private val _currentAlert = MutableStateFlow<AlertData?>(null)
val currentAlert = _currentAlert.asStateFlow()
open val currentAlert = _currentAlert.asStateFlow()
fun showAlert(
open fun showAlert(
title: String? = null,
titleRes: StringResource? = null,
message: String? = null,
@ -97,7 +97,7 @@ open class AlertManager {
)
}
fun dismissAlert() {
open fun dismissAlert() {
_currentAlert.value = null
}
}

View file

@ -0,0 +1,58 @@
/*
* 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.core.ui.emoji
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 kotlinx.coroutines.flow.MutableStateFlow
import org.meshtastic.core.repository.CustomEmojiPrefs
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
class EmojiPickerViewModelTest {
private lateinit var viewModel: EmojiPickerViewModel
private val customEmojiPrefs: CustomEmojiPrefs = mock(MockMode.autofill)
private val frequencyFlow = MutableStateFlow<String?>(null)
@BeforeTest
fun setUp() {
every { customEmojiPrefs.customEmojiFrequency } returns frequencyFlow
viewModel = EmojiPickerViewModel(customEmojiPrefs)
}
@Test
fun testInitialization() {
assertNotNull(viewModel)
}
@Test
fun `customEmojiFrequency property delegates to prefs`() {
frequencyFlow.value = "👍=10"
assertEquals("👍=10", viewModel.customEmojiFrequency)
every { customEmojiPrefs.setCustomEmojiFrequency(any()) } returns Unit
viewModel.customEmojiFrequency = "❤️=5"
verify { customEmojiPrefs.setCustomEmojiFrequency("❤️=5") }
}
}

View file

@ -0,0 +1,96 @@
/*
* 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.core.ui.share
import app.cash.turbine.test
import dev.mokkery.MockMode
import dev.mokkery.answering.returns
import dev.mokkery.every
import dev.mokkery.everySuspend
import dev.mokkery.matcher.any
import dev.mokkery.mock
import dev.mokkery.verifySuspend
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.service.ServiceAction
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.proto.SharedContact
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
@OptIn(ExperimentalCoroutinesApi::class)
class SharedContactViewModelTest {
private val testDispatcher = UnconfinedTestDispatcher()
private lateinit var viewModel: SharedContactViewModel
private val nodeRepository: NodeRepository = mock(MockMode.autofill)
private val serviceRepository: ServiceRepository = mock(MockMode.autofill)
@BeforeTest
fun setUp() {
Dispatchers.setMain(testDispatcher)
every { nodeRepository.getNodes() } returns MutableStateFlow(emptyList())
viewModel = SharedContactViewModel(nodeRepository, serviceRepository)
}
@AfterTest
fun tearDown() {
Dispatchers.resetMain()
}
@Test
fun testInitialization() {
assertNotNull(viewModel)
}
@Test
fun `unfilteredNodes reflects repository updates`() = runTest(testDispatcher) {
val nodesFlow = MutableStateFlow<List<Node>>(emptyList())
every { nodeRepository.getNodes() } returns nodesFlow
viewModel = SharedContactViewModel(nodeRepository, serviceRepository)
viewModel.unfilteredNodes.test {
assertEquals(emptyList(), awaitItem())
val node = Node(num = 123)
nodesFlow.value = listOf(node)
assertEquals(listOf(node), awaitItem())
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `addSharedContact delegates to serviceRepository`() = runTest(testDispatcher) {
val contact = SharedContact(node_num = 123)
everySuspend { serviceRepository.onServiceAction(any()) } returns Unit
val job = viewModel.addSharedContact(contact)
job.join()
verifySuspend { serviceRepository.onServiceAction(ServiceAction.ImportContact(contact)) }
}
}

View file

@ -0,0 +1,91 @@
/*
* 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.core.ui.viewmodel
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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.RadioConfigRepository
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.repository.UiPrefs
import org.meshtastic.proto.LocalConfig
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
@OptIn(ExperimentalCoroutinesApi::class)
class ConnectionsViewModelTest {
private val testDispatcher = StandardTestDispatcher()
private lateinit var viewModel: ConnectionsViewModel
private val radioConfigRepository: RadioConfigRepository = mock(MockMode.autofill)
private val serviceRepository: ServiceRepository = mock(MockMode.autofill)
private val nodeRepository: NodeRepository = mock(MockMode.autofill)
private val uiPrefs: UiPrefs = mock(MockMode.autofill)
@BeforeTest
fun setUp() {
Dispatchers.setMain(testDispatcher)
every { radioConfigRepository.localConfigFlow } returns MutableStateFlow(LocalConfig())
every { serviceRepository.connectionState } returns
MutableStateFlow(org.meshtastic.core.model.ConnectionState.Disconnected)
every { nodeRepository.myNodeInfo } returns MutableStateFlow(null)
every { nodeRepository.ourNodeInfo } returns MutableStateFlow(null)
every { uiPrefs.hasShownNotPairedWarning } returns MutableStateFlow(false)
viewModel =
ConnectionsViewModel(
radioConfigRepository = radioConfigRepository,
serviceRepository = serviceRepository,
nodeRepository = nodeRepository,
uiPrefs = uiPrefs,
)
}
@AfterTest
fun tearDown() {
Dispatchers.resetMain()
}
@Test
fun testInitialization() {
assertNotNull(viewModel)
}
@Test
fun `suppressNoPairedWarning updates state and prefs`() {
every { uiPrefs.setHasShownNotPairedWarning(any()) } returns Unit
viewModel.suppressNoPairedWarning()
assertEquals(true, viewModel.hasShownNotPairedWarning.value)
verify { uiPrefs.setHasShownNotPairedWarning(true) }
}
}