BIT-541, 1370 Adding icon loading for login items (#654)

This commit is contained in:
Oleg Semenenko authored and Álison Fernandes committed 2024-06-20 17:08:07 +01:00
1 parent ff7a015472
commit a87bcd28ff
22 files changed
+599 -67

No files matched your search

@@ -170,6 +170,39 @@ class EnvironmentUrlsDataJsonExtensionsTest {
(null as EnvironmentUrlDataJson?).toEnvironmentUrlsOrDefault(),
)
}
@Test
fun `toIconBaseurl should return icon if value is present`() {
assertEquals(
DEFAULT_CUSTOM_ENVIRONMENT_URL_DATA.baseIconUrl,
"icon",
)
}
@Test
fun `toIconBaseurl should return base value if icon is null`() {
assertEquals(
DEFAULT_CUSTOM_ENVIRONMENT_URL_DATA
.copy(icon = null)
.baseIconUrl,
"base/icons",
)
}
@Test
fun `toIconBaseurl should return default url if base is empty and icon is null`() {
val expectedUrl = "https://icons.bitwarden.net/"
assertEquals(
expectedUrl,
DEFAULT_CUSTOM_ENVIRONMENT_URL_DATA
.copy(
base = "",
icon = null,
)
.baseIconUrl,
)
}
}
private val DEFAULT_CUSTOM_ENVIRONMENT_URL_DATA = EnvironmentUrlDataJson(
@@ -85,7 +85,7 @@ fun createMockLoginView(number: Int): LoginView =
*/
fun createMockUriView(number: Int): LoginUriView =
LoginUriView(
uri = "mockUri-$number",
uri = "www.mockuri$number.com",
match = UriMatchType.HOST,
)
@@ -10,9 +10,12 @@ import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollToNode
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.data.platform.repository.util.baseIconUrl
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.components.model.IconData
import com.x8bit.bitwarden.ui.util.isProgressBar
import io.mockk.every
import io.mockk.mockk
@@ -393,6 +396,8 @@ class VaultItemListingScreenTest : BaseComposeTest() {
private val DEFAULT_STATE = VaultItemListingState(
itemListingType = VaultItemListingState.ItemListingType.Login,
viewState = VaultItemListingState.ViewState.Loading,
isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
)
private fun createDisplayItem(number: Int): VaultItemListingState.DisplayItem =
@@ -400,6 +405,5 @@ private fun createDisplayItem(number: Int): VaultItemListingState.DisplayItem =
id = "mockId-$number",
title = "mockTitle-$number",
subtitle = "mockSubtitle-$number",
uri = "mockUri-$number",
iconRes = R.drawable.ic_card_item,
iconData = IconData.Local(R.drawable.ic_card_item),
)
@@ -1,9 +1,14 @@
package com.x8bit.bitwarden.ui.vault.feature.itemlisting
import android.net.Uri
import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.model.DataState
import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.data.platform.repository.util.baseIconUrl
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFolderView
@@ -17,10 +22,14 @@ import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.createMockItemListi
import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkStatic
import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
class VaultItemListingViewModelTest : BaseViewModelTest() {
@@ -31,6 +40,16 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
every { vaultDataStateFlow } returns mutableVaultDataStateFlow
every { sync() } returns Unit
}
private val environmentRepository: EnvironmentRepository = mockk {
every { environment } returns Environment.Us
every { environmentStateFlow } returns mockk()
}
private val mutableIsIconLoadingDisabledFlow = MutableStateFlow(false)
private val settingsRepository: SettingsRepository = mockk() {
every { isIconLoadingDisabled } returns false
every { isIconLoadingDisabledFlow } returns mutableIsIconLoadingDisabledFlow
}
private val initialState = createVaultItemListingState()
private val initialSavedStateHandle = createSavedStateHandleWithVaultItemListingType(
vaultItemListingType = VaultItemListingType.Login,
@@ -92,6 +111,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
@Test
fun `vaultDataStateFlow Loaded with items should update ViewState to Content`() =
runTest {
setupMockUri()
mutableVaultDataStateFlow.tryEmit(
value = DataState.Loaded(
data = VaultData(
@@ -178,6 +199,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
@Test
fun `vaultDataStateFlow Pending with data should update state to Content`() = runTest {
setupMockUri()
mutableVaultDataStateFlow.tryEmit(
value = DataState.Pending(
data = VaultData(
@@ -267,6 +290,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
@Test
fun `vaultDataStateFlow Error with data should update state to Content`() = runTest {
setupMockUri()
mutableVaultDataStateFlow.tryEmit(
value = DataState.Error(
data = VaultData(
@@ -291,6 +316,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
),
viewModel.stateFlow.value,
)
unmockkStatic(Uri::class)
}
@Test
@@ -363,6 +390,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
@Test
fun `vaultDataStateFlow NoNetwork with data should update state to Content`() = runTest {
setupMockUri()
mutableVaultDataStateFlow.tryEmit(
value = DataState.NoNetwork(
data = VaultData(
@@ -386,6 +415,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
),
viewModel.stateFlow.value,
)
unmockkStatic(Uri::class)
}
@Test
@@ -434,6 +465,16 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
)
}
@Test
fun `icon loading state updates should update isIconLoadingDisabled`() = runTest {
val viewModel = createVaultItemListingViewModel()
assertFalse(viewModel.stateFlow.value.isIconLoadingDisabled)
mutableIsIconLoadingDisabledFlow.value = true
assertTrue(viewModel.stateFlow.value.isIconLoadingDisabled)
}
@Suppress("CyclomaticComplexMethod")
private fun createSavedStateHandleWithVaultItemListingType(
vaultItemListingType: VaultItemListingType,
@@ -464,6 +505,13 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
)
}
private fun setupMockUri() {
mockkStatic(Uri::class)
val uriMock = mockk<Uri>()
every { Uri.parse(any()) } returns uriMock
every { uriMock.host } returns "www.mockuri.com"
}
private fun createVaultItemListingViewModel(
savedStateHandle: SavedStateHandle = initialSavedStateHandle,
vaultRepository: VaultRepository = this.vaultRepository,
@@ -471,6 +519,8 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
VaultItemListingViewModel(
savedStateHandle = savedStateHandle,
vaultRepository = vaultRepository,
environmentRepository = environmentRepository,
settingsRepository = settingsRepository,
)
@Suppress("MaxLineLength")
@@ -481,5 +531,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
VaultItemListingState(
itemListingType = itemListingType,
viewState = viewState,
baseIconUrl = environmentRepository.environment.environmentUrlData.baseIconUrl,
isIconLoadingDisabled = settingsRepository.isIconLoadingDisabled,
)
}
@@ -1,10 +1,17 @@
package com.x8bit.bitwarden.ui.vault.feature.itemlisting.util
import android.net.Uri
import com.bitwarden.core.CipherType
import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.data.platform.repository.util.baseIconUrl
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFolderView
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.VaultItemListingState
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkStatic
import org.junit.Assert.assertEquals
import org.junit.Test
@@ -244,6 +251,11 @@ class VaultItemListingDataExtensionsTest {
@Test
fun `toViewState should transform a list of CipherViews into a ViewState`() {
mockkStatic(Uri::class)
val uriMock = mockk<Uri>()
every { Uri.parse(any()) } returns uriMock
every { uriMock.host } returns "www.mockuri.com"
val cipherViewList = listOf(
createMockCipherView(
number = 1,
@@ -267,7 +279,10 @@ class VaultItemListingDataExtensionsTest {
),
)
val result = cipherViewList.toViewState()
val result = cipherViewList.toViewState(
isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
)
assertEquals(
VaultItemListingState.ViewState.Content(
@@ -292,6 +307,8 @@ class VaultItemListingDataExtensionsTest {
),
result,
)
unmockkStatic(Uri::class)
}
@Test
@@ -2,6 +2,7 @@ package com.x8bit.bitwarden.ui.vault.feature.itemlisting.util
import com.bitwarden.core.CipherType
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.model.IconData
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.VaultItemListingState
/**
@@ -17,8 +18,10 @@ fun createMockItemListingDisplayItem(
id = "mockId-$number",
title = "mockName-$number",
subtitle = "mockUsername-$number",
iconRes = R.drawable.ic_login_item,
uri = "mockUri-$number",
iconData = IconData.Network(
"https://vault.bitwarden.com/icons/www.mockuri.com/icon.png",
fallbackIconRes = R.drawable.ic_login_item,
),
)
}
@@ -27,8 +30,7 @@ fun createMockItemListingDisplayItem(
id = "mockId-$number",
title = "mockName-$number",
subtitle = null,
iconRes = R.drawable.ic_secure_note_item,
uri = null,
iconData = IconData.Local(R.drawable.ic_secure_note_item),
)
}
@@ -37,8 +39,7 @@ fun createMockItemListingDisplayItem(
id = "mockId-$number",
title = "mockName-$number",
subtitle = "er-$number",
iconRes = R.drawable.ic_card_item,
uri = null,
iconData = IconData.Local(R.drawable.ic_card_item),
)
}
@@ -47,8 +48,7 @@ fun createMockItemListingDisplayItem(
id = "mockId-$number",
title = "mockName-$number",
subtitle = "mockFirstName-${number}mockLastName-$number",
iconRes = R.drawable.ic_identity_item,
uri = null,
iconData = IconData.Local(R.drawable.ic_identity_item),
)
}
}
@@ -16,6 +16,8 @@ import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollToNode
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.data.platform.repository.util.baseIconUrl
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
import com.x8bit.bitwarden.ui.platform.base.util.asText
@@ -1071,6 +1073,8 @@ private val DEFAULT_STATE: VaultState = VaultState(
viewState = VaultState.ViewState.Loading,
isPremium = false,
isPullToRefreshSettingEnabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
isIconLoadingDisabled = false,
)
private val DEFAULT_CONTENT_VIEW_STATE: VaultState.ViewState.Content = VaultState.ViewState.Content(
@@ -10,6 +10,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.UserState.SpecialCircumsta
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.repository.model.DataState
import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.data.platform.repository.util.baseIconUrl
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFolderView
@@ -31,12 +32,15 @@ import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
@Suppress("LargeClass")
class VaultViewModelTest : BaseViewModelTest() {
private val mutablePullToRefreshEnabledFlow = MutableStateFlow(false)
private val mutableIsIconLoadingDisabledFlow = MutableStateFlow(false)
private val mutableUserStateFlow =
MutableStateFlow<UserState?>(DEFAULT_USER_STATE)
@@ -57,6 +61,8 @@ class VaultViewModelTest : BaseViewModelTest() {
private val settingsRepository: SettingsRepository = mockk {
every { getPullToRefreshEnabledFlow() } returns mutablePullToRefreshEnabledFlow
every { isIconLoadingDisabledFlow } returns mutableIsIconLoadingDisabledFlow
every { isIconLoadingDisabled } returns false
}
private val vaultRepository: VaultRepository =
@@ -357,6 +363,8 @@ class VaultViewModelTest : BaseViewModelTest() {
viewState = vaultData.toViewState(
isPremium = true,
vaultFilterType = VaultFilterType.AllVaults,
isIconLoadingDisabled = viewModel.stateFlow.value.isIconLoadingDisabled,
baseIconUrl = viewModel.stateFlow.value.baseIconUrl,
),
)
.copy(
@@ -378,6 +386,8 @@ class VaultViewModelTest : BaseViewModelTest() {
viewState = vaultData.toViewState(
isPremium = true,
vaultFilterType = VaultFilterType.MyVault,
isIconLoadingDisabled = viewModel.stateFlow.value.isIconLoadingDisabled,
baseIconUrl = viewModel.stateFlow.value.baseIconUrl,
),
),
viewModel.stateFlow.value,
@@ -1032,6 +1042,16 @@ class VaultViewModelTest : BaseViewModelTest() {
)
}
@Test
fun `icon loading state updates should update isIconLoadingDisabled`() {
val viewModel = createViewModel()
assertFalse(viewModel.stateFlow.value.isIconLoadingDisabled)
mutableIsIconLoadingDisabledFlow.value = true
assertTrue(viewModel.stateFlow.value.isIconLoadingDisabled)
}
private fun createViewModel(): VaultViewModel =
VaultViewModel(
authRepository = authRepository,
@@ -1120,4 +1140,6 @@ private fun createMockVaultState(
isSwitchingAccounts = false,
isPremium = true,
isPullToRefreshSettingEnabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
isIconLoadingDisabled = false,
)
@@ -1,13 +1,25 @@
package com.x8bit.bitwarden.ui.vault.feature.vault.util
import android.net.Uri
import com.bitwarden.core.CipherType
import com.bitwarden.core.LoginUriView
import com.bitwarden.core.UriMatchType
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.data.platform.repository.util.baseIconUrl
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFolderView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSendView
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.components.model.IconData
import com.x8bit.bitwarden.ui.vault.feature.vault.VaultState
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkStatic
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
@@ -25,6 +37,8 @@ class VaultDataExtensionsTest {
val actual = vaultData.toViewState(
isPremium = true,
isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
vaultFilterType = VaultFilterType.AllVaults,
)
@@ -72,6 +86,8 @@ class VaultDataExtensionsTest {
val actual = vaultData.toViewState(
isPremium = true,
isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
vaultFilterType = VaultFilterType.MyVault,
)
@@ -116,6 +132,8 @@ class VaultDataExtensionsTest {
val actual = vaultData.toViewState(
isPremium = true,
isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
vaultFilterType = VaultFilterType.OrganizationVault(
organizationId = "mockOrganizationId-1",
organizationName = "Mock Organization 1",
@@ -156,6 +174,8 @@ class VaultDataExtensionsTest {
val actual = vaultData.toViewState(
isPremium = true,
isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
vaultFilterType = VaultFilterType.AllVaults,
)
@@ -176,6 +196,8 @@ class VaultDataExtensionsTest {
val actual = vaultData.toViewState(
isPremium = true,
isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
vaultFilterType = VaultFilterType.AllVaults,
)
@@ -197,6 +219,8 @@ class VaultDataExtensionsTest {
val actual = vaultData.toViewState(
isPremium = true,
isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
vaultFilterType = VaultFilterType.AllVaults,
)
@@ -230,6 +254,8 @@ class VaultDataExtensionsTest {
val actual = vaultData.toViewState(
isPremium = false,
vaultFilterType = VaultFilterType.AllVaults,
isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
)
assertEquals(
@@ -248,4 +274,111 @@ class VaultDataExtensionsTest {
actual,
)
}
@Suppress("MaxLineLength")
@Test
fun `toLoginIconData should return a IconData Local type if isIconLoadingDisabled is true`() {
val actual =
createMockCipherView(
number = 1,
cipherType = CipherType.LOGIN,
)
.login
?.uris
.toLoginIconData(
isIconLoadingDisabled = true,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
)
val expected = IconData.Local(iconRes = R.drawable.ic_login_item)
assertEquals(expected, actual)
}
@Suppress("MaxLineLength")
@Test
fun `toLoginIconData should return a IconData Local type if no valid uris are found`() {
val actual = listOf(
LoginUriView(
uri = "",
match = UriMatchType.HOST,
),
)
.toLoginIconData(
isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
)
val expected = IconData.Local(iconRes = R.drawable.ic_login_item)
assertEquals(expected, actual)
}
@Suppress("MaxLineLength")
@Test
fun `toLoginIconData should return a IconData Local type if an Android uri is detected`() {
val actual = listOf(
LoginUriView(
uri = "androidapp://test.com",
match = UriMatchType.HOST,
),
)
.toLoginIconData(
isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
)
val expected = IconData.Local(iconRes = R.drawable.ic_settings)
assertEquals(expected, actual)
}
@Suppress("MaxLineLength")
@Test
fun `toLoginIconData should return a IconData Local type if an iOS uri is detected`() {
val actual = listOf(
LoginUriView(
uri = "iosapp://test.com",
match = UriMatchType.HOST,
),
)
.toLoginIconData(
isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
)
val expected = IconData.Local(iconRes = R.drawable.ic_settings)
assertEquals(expected, actual)
}
@Suppress("MaxLineLength")
@Test
fun `toLoginIconData should return IconData Network type if isIconLoadingDisabled is false`() {
mockkStatic(Uri::class)
val uriMock = mockk<Uri>()
every { Uri.parse(any()) } returns uriMock
every { uriMock.host } returns "www.mockuri1.com"
val actual =
createMockCipherView(
number = 1,
cipherType = CipherType.LOGIN,
)
.login
?.uris
.toLoginIconData(
isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
)
val expected = IconData.Network(
uri = "https://vault.bitwarden.com/icons/www.mockuri1.com/icon.png",
fallbackIconRes = R.drawable.ic_login_item,
)
assertEquals(expected, actual)
unmockkStatic(Uri::class)
}
}