diff options
| author | Kalle Lindström <karl.lindstrom@mullvad.net> | 2025-04-15 15:59:12 +0200 |
|---|---|---|
| committer | Kalle Lindström <karl.lindstrom@mullvad.net> | 2025-04-22 12:12:12 +0200 |
| commit | 926468949b2facb49182c1a5a7597b720f9e5cc7 (patch) | |
| tree | 30950262e3f6adb784a12d89f8c8def37a604356 /android/app/src/test | |
| parent | 3a58848059a1fb8c429a49acff9e2f57b1d91fd2 (diff) | |
| download | mullvadvpn-926468949b2facb49182c1a5a7597b720f9e5cc7.tar.xz mullvadvpn-926468949b2facb49182c1a5a7597b720f9e5cc7.zip | |
Implement manage devices screen
Diffstat (limited to 'android/app/src/test')
2 files changed, 243 insertions, 0 deletions
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/utils/AppendTextWithStyledSubstringTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/utils/AppendTextWithStyledSubstringTest.kt new file mode 100644 index 0000000000..90024be351 --- /dev/null +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/utils/AppendTextWithStyledSubstringTest.kt @@ -0,0 +1,96 @@ +package net.mullvad.mullvadvpn.utils + +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import kotlin.test.assertEquals +import net.mullvad.mullvadvpn.util.appendTextWithStyledSubstring +import org.junit.jupiter.api.Test + +class AppendTextWithStyledSubstringTest { + @Test + fun `empty input should result in empty output`() { + + val output = buildAnnotatedString { + appendTextWithStyledSubstring( + text = "", + substring = "abc", + substringStyle = SpanStyle(), + ) + } + + assertEquals("", output.text) + } + + @Test + fun `split only should result in split only`() { + + val split = "abc" + + val output = buildAnnotatedString { + appendTextWithStyledSubstring( + text = split, + substring = split, + substringStyle = SpanStyle(), + ) + } + + assertEquals(split, output.text) + } + + @Test + fun `split twice should result in split twice`() { + + val split = "abcabc" + + val output = buildAnnotatedString { + appendTextWithStyledSubstring( + text = split, + substring = split, + substringStyle = SpanStyle(), + ) + } + + assertEquals(split, output.text) + } + + @Test + fun `split anywhere should return the input text`() { + + val text = "abca longer abc string to split abc" + val split = "abc" + + val output = buildAnnotatedString { + appendTextWithStyledSubstring( + text = text, + substring = split, + substringStyle = SpanStyle(), + ) + } + + assertEquals(text, output.text) + } + + @Test + fun `span styles should be applied to all matching substrings`() { + + val text = "Cool Cat: your username is Cool Cat." + val split = "Cool Cat" + + val output = buildAnnotatedString { + appendTextWithStyledSubstring( + text = text, + substring = split, + substringStyle = SpanStyle(), + ) + } + + assertEquals(text, output.text) + assertEquals(2, output.spanStyles.size) + + assertEquals(0, output.spanStyles[0].start) + assertEquals(8, output.spanStyles[0].end) + + assertEquals(27, output.spanStyles[1].start) + assertEquals(35, output.spanStyles[1].end) + } +} diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ManageDevicesViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ManageDevicesViewModelTest.kt new file mode 100644 index 0000000000..ac5446cc07 --- /dev/null +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ManageDevicesViewModelTest.kt @@ -0,0 +1,147 @@ +package net.mullvad.mullvadvpn.viewmodel + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import app.cash.turbine.test +import arrow.core.left +import arrow.core.right +import com.ramcosta.composedestinations.generated.navargs.toSavedStateHandle +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import io.mockk.unmockkAll +import java.time.ZonedDateTime +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import net.mullvad.mullvadvpn.compose.screen.DeviceListNavArgs +import net.mullvad.mullvadvpn.compose.state.ManageDevicesUiState +import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule +import net.mullvad.mullvadvpn.lib.model.AccountNumber +import net.mullvad.mullvadvpn.lib.model.Device +import net.mullvad.mullvadvpn.lib.model.DeviceId +import net.mullvad.mullvadvpn.lib.model.DeviceState +import net.mullvad.mullvadvpn.lib.model.GetDeviceListError +import net.mullvad.mullvadvpn.lib.shared.DeviceRepository +import net.mullvad.mullvadvpn.util.Lce +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExperimentalCoroutinesApi +@ExtendWith(TestCoroutineRule::class) +class ManageDevicesViewModelTest { + + private val mockDeviceRepository: DeviceRepository = mockk() + private val mockSavedStateHandle: SavedStateHandle = mockk(relaxed = true) + + private lateinit var viewModel: ManageDevicesViewModel + + @BeforeEach + fun setup() { + // Mock SavedStateHandle to return the account number + every { mockSavedStateHandle.get<AccountNumber>("accountNumber") } returns testAccountNumber + + // Mock successful device list fetch by default + coEvery { mockDeviceRepository.deviceList(testAccountNumber) } returns + testDeviceList.right() + + every { mockDeviceRepository.deviceState } returns MutableStateFlow(deviceState) + + viewModel = + ManageDevicesViewModel( + deviceRepository = mockDeviceRepository, + deviceListViewModel = + DeviceListViewModel( + deviceRepository = mockDeviceRepository, + dispatcher = UnconfinedTestDispatcher(), + savedStateHandle = + DeviceListNavArgs(accountNumber = testAccountNumber) + .toSavedStateHandle(), + ), + ) + } + + @AfterEach + fun tearDown() { + viewModel.viewModelScope.coroutineContext.cancel() + unmockkAll() + } + + @Test + fun `initial state should be Loading followed by Content`() = runTest { + // Initial state is Loading + assertIs<Lce.Loading>(viewModel.uiState.value) + + viewModel.uiState.test { + val contentState = awaitItem() + assertIs<Lce.Content<ManageDevicesUiState>>(contentState) + assertEquals(3, contentState.value.devices.size) + } + } + + @Test + fun `fetchDevices should update state to Error on failure`() = runTest { + val error = GetDeviceListError.Unknown(RuntimeException("Network failed")) + coEvery { mockDeviceRepository.deviceList(testAccountNumber) } returns error.left() + + viewModel.uiState.test { + val errorState = awaitItem() + assertIs<Lce.Error<GetDeviceListError>>(errorState) + assertEquals(error, errorState.error) + } + } + + @Test + fun `the logged in device should appear first in the list`() = runTest { + viewModel.uiState.test { + val contentState = awaitItem() + assertIs<Lce.Content<ManageDevicesUiState>>(contentState) + + val devices = contentState.value.devices + assertEquals(testDeviceId2, devices[0].device.id) + assertTrue(devices[0].isCurrentDevice) + assertFalse(devices[1].isCurrentDevice) + assertFalse(devices[2].isCurrentDevice) + } + } + + companion object { + private val testAccountNumber = AccountNumber("1234567890123456") + private val testDeviceId1 = DeviceId.fromString("12345678-1234-5678-1234-567812345678") + private val testDeviceId2 = DeviceId.fromString("87654321-1234-5678-1234-567812345678") + private val testDeviceId3 = DeviceId.fromString("87654321-4321-5678-1234-567812345678") + + private val testDevice1 = + Device( + id = testDeviceId1, + name = "Device 1", + creationDate = ZonedDateTime.now().minusSeconds(100), + ) + + private val testDevice2 = + Device( + id = testDeviceId2, + name = "Device 2", + creationDate = ZonedDateTime.now().minusSeconds(200), + ) + + private val testDevice3 = + Device( + id = testDeviceId3, + name = "Device 3", + creationDate = ZonedDateTime.now().minusSeconds(300), + ) + private val testDeviceList = listOf(testDevice1, testDevice2, testDevice3) + + private val deviceState = + DeviceState.LoggedIn(accountNumber = testAccountNumber, device = testDevice2) + } +} |
