[PM-15873] Fix PTR on item listing page (#4778)

This commit is contained in:
André Bispo
2025-02-25 16:14:18 +00:00
committed by GitHub
parent 00eb78f02e
commit ac7fbfd129
2 changed files with 127 additions and 12 deletions

View File

@@ -32,6 +32,7 @@ import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingMa
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent
import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManager
import com.x8bit.bitwarden.data.platform.manager.util.toAutofillSelectionDataOrNull
import com.x8bit.bitwarden.data.platform.manager.util.toFido2AssertionRequestOrNull
import com.x8bit.bitwarden.data.platform.manager.util.toFido2CreateRequestOrNull
@@ -83,6 +84,7 @@ import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType
import com.x8bit.bitwarden.ui.vault.util.toVaultItemCipherType
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
@@ -114,6 +116,7 @@ class VaultItemListingViewModel @Inject constructor(
private val fido2OriginManager: Fido2OriginManager,
private val fido2CredentialManager: Fido2CredentialManager,
private val organizationEventManager: OrganizationEventManager,
private val networkConnectionManager: NetworkConnectionManager,
) : BaseViewModel<VaultItemListingState, VaultItemListingEvent, VaultItemListingsAction>(
initialState = run {
val userState = requireNotNull(authRepository.userStateFlow.value)
@@ -306,11 +309,17 @@ class VaultItemListingViewModel @Inject constructor(
vaultRepository.sync(forced = true)
}
@Suppress("MagicNumber")
private fun handleRefreshPull() {
mutableStateFlow.update { it.copy(isRefreshing = true) }
// The Pull-To-Refresh composable is already in the refreshing state.
// We will reset that state when sendDataStateFlow emits later on.
vaultRepository.sync(forced = false)
viewModelScope.launch {
delay(250)
if (networkConnectionManager.isNetworkConnected) {
vaultRepository.sync(forced = false)
} else {
sendAction(VaultItemListingsAction.Internal.InternetConnectionErrorReceived)
}
}
}
private fun handleConfirmOverwriteExistingPasskeyClick(
@@ -1018,14 +1027,25 @@ class VaultItemListingViewModel @Inject constructor(
}
private fun handleSyncClick() {
mutableStateFlow.update {
it.copy(
dialogState = VaultItemListingState.DialogState.Loading(
message = R.string.syncing.asText(),
),
)
if (networkConnectionManager.isNetworkConnected) {
mutableStateFlow.update {
it.copy(
dialogState = VaultItemListingState.DialogState.Loading(
message = R.string.syncing.asText(),
),
)
}
vaultRepository.sync(forced = true)
} else {
mutableStateFlow.update {
it.copy(
dialogState = VaultItemListingState.DialogState.Error(
R.string.internet_connection_required_title.asText(),
R.string.internet_connection_required_message.asText(),
),
)
}
}
vaultRepository.sync(forced = true)
}
private fun handleSearchIconClick() {
@@ -1150,6 +1170,22 @@ class VaultItemListingViewModel @Inject constructor(
is VaultItemListingsAction.Internal.Fido2AssertionResultReceive -> {
handleFido2AssertionResultReceive(action)
}
VaultItemListingsAction.Internal.InternetConnectionErrorReceived -> {
handleInternetConnectionErrorReceived()
}
}
}
private fun handleInternetConnectionErrorReceived() {
mutableStateFlow.update {
it.copy(
isRefreshing = false,
dialogState = VaultItemListingState.DialogState.Error(
R.string.internet_connection_required_title.asText(),
R.string.internet_connection_required_message.asText(),
),
)
}
}
@@ -2768,6 +2804,11 @@ sealed class VaultItemListingsAction {
data class Fido2AssertionResultReceive(
val result: Fido2CredentialAssertionResult,
) : Internal()
/**
* Indicates that the there is not internet connection.
*/
data object InternetConnectionErrorReceived : Internal()
}
}

View File

@@ -42,6 +42,7 @@ import com.x8bit.bitwarden.data.platform.manager.event.OrganizationEventManager
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
import com.x8bit.bitwarden.data.platform.manager.model.OrganizationEvent
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
import com.x8bit.bitwarden.data.platform.manager.network.NetworkConnectionManager
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
@@ -89,8 +90,10 @@ import io.mockk.runs
import io.mockk.unmockkStatic
import io.mockk.verify
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
@@ -185,6 +188,10 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
every { trackEvent(event = any()) } just runs
}
private val networkConnectionManager: NetworkConnectionManager = mockk {
every { isNetworkConnected } returns true
}
private val initialState = createVaultItemListingState()
private val initialSavedStateHandle = createSavedStateHandleWithVaultItemListingType(
vaultItemListingType = VaultItemListingType.Login,
@@ -363,6 +370,27 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
}
}
@Test
fun `on SyncClick should show the no network dialog if no connection is available`() {
val viewModel = createVaultItemListingViewModel()
every {
networkConnectionManager.isNetworkConnected
} returns false
viewModel.trySendAction(VaultItemListingsAction.SyncClick)
assertEquals(
initialState.copy(
dialogState = VaultItemListingState.DialogState.Error(
R.string.internet_connection_required_title.asText(),
R.string.internet_connection_required_message.asText(),
),
),
viewModel.stateFlow.value,
)
verify(exactly = 0) {
vaultRepository.sync(forced = true)
}
}
@Suppress("MaxLineLength")
@Test
fun `ItemClick for vault item when accessibility autofill should post to the accessibilitySelectionManager`() =
@@ -2451,17 +2479,43 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
assertTrue(viewModel.stateFlow.value.isIconLoadingDisabled)
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `RefreshPull should call vault repository sync`() {
fun `RefreshPull should call vault repository sync`() = runTest {
val viewModel = createVaultItemListingViewModel()
viewModel.trySendAction(VaultItemListingsAction.RefreshPull)
advanceTimeBy(300)
verify(exactly = 1) {
vaultRepository.sync(forced = false)
}
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `RefreshPull should show network error if no internet connection`() = runTest {
val viewModel = createVaultItemListingViewModel()
every {
networkConnectionManager.isNetworkConnected
} returns false
viewModel.trySendAction(VaultItemListingsAction.RefreshPull)
advanceTimeBy(300)
assertEquals(
initialState.copy(
isRefreshing = false,
dialogState = VaultItemListingState.DialogState.Error(
R.string.internet_connection_required_title.asText(),
R.string.internet_connection_required_message.asText(),
),
),
viewModel.stateFlow.value,
)
verify(exactly = 0) {
vaultRepository.sync(forced = false)
}
}
@Test
fun `PullToRefreshEnableReceive should update isPullToRefreshEnabled`() = runTest {
val viewModel = createVaultItemListingViewModel()
@@ -4461,6 +4515,25 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
)
}
@Test
fun `InternetConnectionErrorReceived should show network error if no internet connection`() =
runTest {
val viewModel = createVaultItemListingViewModel()
viewModel.trySendAction(
VaultItemListingsAction.Internal.InternetConnectionErrorReceived,
)
assertEquals(
initialState.copy(
isRefreshing = false,
dialogState = VaultItemListingState.DialogState.Error(
R.string.internet_connection_required_title.asText(),
R.string.internet_connection_required_message.asText(),
),
),
viewModel.stateFlow.value,
)
}
@Suppress("CyclomaticComplexMethod")
private fun createSavedStateHandleWithVaultItemListingType(
vaultItemListingType: VaultItemListingType,
@@ -4523,6 +4596,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
fido2CredentialManager = fido2CredentialManager,
organizationEventManager = organizationEventManager,
fido2OriginManager = fido2OriginManager,
networkConnectionManager = networkConnectionManager,
)
@Suppress("MaxLineLength")