BIT-1282: Add UI for Vault Sync (#740)

This commit is contained in:
David Perez 2024-01-23 19:47:18 -06:00 committed by Álison Fernandes
parent 376278e97a
commit de99c36b20
8 changed files with 191 additions and 6 deletions

View file

@ -6,6 +6,7 @@ import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppLang
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import java.time.Instant
/** /**
* Provides an API for observing and modifying settings state. * Provides an API for observing and modifying settings state.
@ -27,6 +28,16 @@ interface SettingsRepository {
*/ */
val appThemeStateFlow: StateFlow<AppTheme> val appThemeStateFlow: StateFlow<AppTheme>
/**
* The currently stored last time the vault was synced.
*/
var vaultLastSync: Instant?
/**
* Tracks changes to the [vaultLastSync].
*/
val vaultLastSyncStateFlow: StateFlow<Instant?>
/** /**
* The current setting for getting login item icons. * The current setting for getting login item icons.
*/ */

View file

@ -20,6 +20,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.time.Instant
/** /**
* Primary implementation of [SettingsRepository]. * Primary implementation of [SettingsRepository].
@ -61,6 +62,26 @@ class SettingsRepositoryImpl(
initialValue = settingsDiskSource.appTheme, initialValue = settingsDiskSource.appTheme,
) )
override var vaultLastSync: Instant?
get() = vaultLastSyncStateFlow.value
set(value) {
val userId = activeUserId ?: return
settingsDiskSource.storeLastSyncTime(userId = userId, lastSyncTime = value)
}
override val vaultLastSyncStateFlow: StateFlow<Instant?>
get() = activeUserId
?.let {
settingsDiskSource
.getLastSyncTimeFlow(userId = it)
.stateIn(
scope = unconfinedScope,
started = SharingStarted.Eagerly,
initialValue = settingsDiskSource.getLastSyncTime(userId = it),
)
}
?: MutableStateFlow(value = null)
override var isIconLoadingDisabled: Boolean override var isIconLoadingDisabled: Boolean
get() = settingsDiskSource.isIconLoadingDisabled ?: false get() = settingsDiskSource.isIconLoadingDisabled ?: false
set(value) { set(value) {

View file

@ -14,6 +14,7 @@ import com.bitwarden.crypto.Kdf
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams
import com.x8bit.bitwarden.data.auth.repository.util.toUpdatedUserStateJson import com.x8bit.bitwarden.data.auth.repository.util.toUpdatedUserStateJson
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.datasource.network.util.isNoConnectionError import com.x8bit.bitwarden.data.platform.datasource.network.util.isNoConnectionError
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.repository.model.DataState import com.x8bit.bitwarden.data.platform.repository.model.DataState
@ -78,6 +79,7 @@ import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.time.Clock
import java.time.Instant import java.time.Instant
/** /**
@ -97,9 +99,11 @@ class VaultRepositoryImpl(
private val vaultDiskSource: VaultDiskSource, private val vaultDiskSource: VaultDiskSource,
private val vaultSdkSource: VaultSdkSource, private val vaultSdkSource: VaultSdkSource,
private val authDiskSource: AuthDiskSource, private val authDiskSource: AuthDiskSource,
private val settingsDiskSource: SettingsDiskSource,
private val fileManager: FileManager, private val fileManager: FileManager,
private val vaultLockManager: VaultLockManager, private val vaultLockManager: VaultLockManager,
private val totpCodeManager: TotpCodeManager, private val totpCodeManager: TotpCodeManager,
private val clock: Clock,
dispatcherManager: DispatcherManager, dispatcherManager: DispatcherManager,
) : VaultRepository, ) : VaultRepository,
VaultLockManager by vaultLockManager { VaultLockManager by vaultLockManager {
@ -230,6 +234,7 @@ class VaultRepositoryImpl(
unlockVaultForOrganizationsIfNecessary(syncResponse = syncResponse) unlockVaultForOrganizationsIfNecessary(syncResponse = syncResponse)
storeProfileData(syncResponse = syncResponse) storeProfileData(syncResponse = syncResponse)
vaultDiskSource.replaceVaultData(userId = userId, vault = syncResponse) vaultDiskSource.replaceVaultData(userId = userId, vault = syncResponse)
settingsDiskSource.storeLastSyncTime(userId = userId, clock.instant())
}, },
onFailure = { throwable -> onFailure = { throwable ->
mutableCiphersStateFlow.update { currentState -> mutableCiphersStateFlow.update { currentState ->

View file

@ -1,6 +1,7 @@
package com.x8bit.bitwarden.data.vault.repository.di package com.x8bit.bitwarden.data.vault.repository.di
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource import com.x8bit.bitwarden.data.vault.datasource.disk.VaultDiskSource
import com.x8bit.bitwarden.data.vault.datasource.network.service.CiphersService import com.x8bit.bitwarden.data.vault.datasource.network.service.CiphersService
@ -16,6 +17,7 @@ import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import java.time.Clock
import javax.inject.Singleton import javax.inject.Singleton
/** /**
@ -34,10 +36,12 @@ object VaultRepositoryModule {
vaultDiskSource: VaultDiskSource, vaultDiskSource: VaultDiskSource,
vaultSdkSource: VaultSdkSource, vaultSdkSource: VaultSdkSource,
authDiskSource: AuthDiskSource, authDiskSource: AuthDiskSource,
settingsDiskSource: SettingsDiskSource,
fileManager: FileManager, fileManager: FileManager,
vaultLockManager: VaultLockManager, vaultLockManager: VaultLockManager,
dispatcherManager: DispatcherManager, dispatcherManager: DispatcherManager,
totpCodeManager: TotpCodeManager, totpCodeManager: TotpCodeManager,
clock: Clock,
): VaultRepository = VaultRepositoryImpl( ): VaultRepository = VaultRepositoryImpl(
syncService = syncService, syncService = syncService,
sendsService = sendsService, sendsService = sendsService,
@ -45,9 +49,11 @@ object VaultRepositoryModule {
vaultDiskSource = vaultDiskSource, vaultDiskSource = vaultDiskSource,
vaultSdkSource = vaultSdkSource, vaultSdkSource = vaultSdkSource,
authDiskSource = authDiskSource, authDiskSource = authDiskSource,
settingsDiskSource = settingsDiskSource,
fileManager = fileManager, fileManager = fileManager,
vaultLockManager = vaultLockManager, vaultLockManager = vaultLockManager,
dispatcherManager = dispatcherManager, dispatcherManager = dispatcherManager,
totpCodeManager = totpCodeManager, totpCodeManager = totpCodeManager,
clock = clock,
) )
} }

View file

@ -2,24 +2,34 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.other
import android.os.Parcelable import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.x8bit.bitwarden.R import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.vault.repository.VaultRepository import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.ui.platform.base.util.Text import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.util.toFormattedPattern
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import java.time.Clock
import java.time.Instant
import javax.inject.Inject import javax.inject.Inject
private const val KEY_STATE = "state" private const val KEY_STATE = "state"
private const val VAULT_LAST_SYNC_TIME_PATTERN: String = "M/d/yyyy h:mm a"
/** /**
* View model for the other screen. * View model for the other screen.
*/ */
@HiltViewModel @HiltViewModel
class OtherViewModel @Inject constructor( class OtherViewModel @Inject constructor(
private val clock: Clock,
private val settingsRepo: SettingsRepository, private val settingsRepo: SettingsRepository,
private val vaultRepo: VaultRepository, private val vaultRepo: VaultRepository,
savedStateHandle: SavedStateHandle, savedStateHandle: SavedStateHandle,
@ -29,16 +39,28 @@ class OtherViewModel @Inject constructor(
allowScreenCapture = false, allowScreenCapture = false,
allowSyncOnRefresh = settingsRepo.getPullToRefreshEnabledFlow().value, allowSyncOnRefresh = settingsRepo.getPullToRefreshEnabledFlow().value,
clearClipboardFrequency = OtherState.ClearClipboardFrequency.DEFAULT, clearClipboardFrequency = OtherState.ClearClipboardFrequency.DEFAULT,
lastSyncTime = "5/14/2023 4:52 PM", lastSyncTime = settingsRepo
.vaultLastSync
?.toFormattedPattern(VAULT_LAST_SYNC_TIME_PATTERN, clock.zone)
.orEmpty(),
dialogState = null, dialogState = null,
), ),
) { ) {
init {
settingsRepo
.vaultLastSyncStateFlow
.map { OtherAction.Internal.VaultLastSyncReceive(it) }
.onEach(::sendAction)
.launchIn(viewModelScope)
}
override fun handleAction(action: OtherAction): Unit = when (action) { override fun handleAction(action: OtherAction): Unit = when (action) {
is OtherAction.AllowScreenCaptureToggle -> handleAllowScreenCaptureToggled(action) is OtherAction.AllowScreenCaptureToggle -> handleAllowScreenCaptureToggled(action)
is OtherAction.AllowSyncToggle -> handleAllowSyncToggled(action) is OtherAction.AllowSyncToggle -> handleAllowSyncToggled(action)
OtherAction.BackClick -> handleBackClicked() OtherAction.BackClick -> handleBackClicked()
is OtherAction.ClearClipboardFrequencyChange -> handleClearClipboardFrequencyChanged(action) is OtherAction.ClearClipboardFrequencyChange -> handleClearClipboardFrequencyChanged(action)
OtherAction.SyncNowButtonClick -> handleSyncNowButtonClicked() OtherAction.SyncNowButtonClick -> handleSyncNowButtonClicked()
is OtherAction.Internal -> handleInternalAction(action)
} }
private fun handleAllowScreenCaptureToggled(action: OtherAction.AllowScreenCaptureToggle) { private fun handleAllowScreenCaptureToggled(action: OtherAction.AllowScreenCaptureToggle) {
@ -67,9 +89,29 @@ class OtherViewModel @Inject constructor(
} }
private fun handleSyncNowButtonClicked() { private fun handleSyncNowButtonClicked() {
// TODO BIT-1282 add full support and visual feedback mutableStateFlow.update {
it.copy(dialogState = OtherState.DialogState.Loading(R.string.syncing.asText()))
}
vaultRepo.sync() vaultRepo.sync()
} }
private fun handleInternalAction(action: OtherAction.Internal) {
when (action) {
is OtherAction.Internal.VaultLastSyncReceive -> handleVaultDataReceive(action)
}
}
private fun handleVaultDataReceive(action: OtherAction.Internal.VaultLastSyncReceive) {
mutableStateFlow.update {
it.copy(
lastSyncTime = action
.vaultLastSyncTime
?.toFormattedPattern(VAULT_LAST_SYNC_TIME_PATTERN, clock.zone)
.orEmpty(),
dialogState = null,
)
}
}
} }
/** /**
@ -154,4 +196,16 @@ sealed class OtherAction {
* Indicates that the user clicked the Sync Now button. * Indicates that the user clicked the Sync Now button.
*/ */
data object SyncNowButtonClick : OtherAction() data object SyncNowButtonClick : OtherAction()
/**
* Models actions that the [OtherViewModel] itself might send.
*/
sealed class Internal : OtherAction() {
/**
* Indicates last sync time of the vault has been received.
*/
data class VaultLastSyncReceive(
val vaultLastSyncTime: Instant?,
) : Internal()
}
} }

View file

@ -29,6 +29,7 @@ import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import java.time.Instant
class SettingsRepositoryTest { class SettingsRepositoryTest {
private val autofillManager: AutofillManager = mockk { private val autofillManager: AutofillManager = mockk {
@ -119,6 +120,36 @@ class SettingsRepositoryTest {
) )
} }
@Test
fun `vaultLastSync should pull from and update SettingsDiskSource`() {
fakeAuthDiskSource.userState = MOCK_USER_STATE
assertNull(settingsRepository.vaultLastSync)
val instant = Instant.ofEpochMilli(1_698_408_000_000L)
// Updates to the disk source change the repository value.
fakeSettingsDiskSource.storeLastSyncTime(
userId = MOCK_USER_STATE.activeUserId,
lastSyncTime = instant,
)
assertEquals(instant, settingsRepository.vaultLastSync)
}
@Test
fun `vaultLastSyncStateFlow should react to changes in SettingsDiskSource`() = runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
val instant = Instant.ofEpochMilli(1_698_408_000_000L)
settingsRepository
.vaultLastSyncStateFlow
.test {
assertNull(awaitItem())
fakeSettingsDiskSource.storeLastSyncTime(
userId = MOCK_USER_STATE.activeUserId,
lastSyncTime = instant,
)
assertEquals(instant, awaitItem())
}
}
@Test @Test
fun `isIconLoadingDisabled should pull from and update SettingsDiskSource`() { fun `isIconLoadingDisabled should pull from and update SettingsDiskSource`() {
assertFalse(settingsRepository.isIconLoadingDisabled) assertFalse(settingsRepository.isIconLoadingDisabled)

View file

@ -20,6 +20,7 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.util.FakeAuthDiskSource
import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams
import com.x8bit.bitwarden.data.auth.util.KdfParamsConstants.DEFAULT_PBKDF2_ITERATIONS import com.x8bit.bitwarden.data.auth.util.KdfParamsConstants.DEFAULT_PBKDF2_ITERATIONS
import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.repository.model.DataState import com.x8bit.bitwarden.data.platform.repository.model.DataState
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
@ -95,14 +96,20 @@ import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import java.net.UnknownHostException import java.net.UnknownHostException
import java.time.Clock
import java.time.Instant import java.time.Instant
import java.time.ZoneOffset
@Suppress("LargeClass") @Suppress("LargeClass")
class VaultRepositoryTest { class VaultRepositoryTest {
private val clock: Clock = Clock.fixed(
Instant.parse("2023-10-27T12:00:00Z"),
ZoneOffset.UTC,
)
private val dispatcherManager: DispatcherManager = FakeDispatcherManager() private val dispatcherManager: DispatcherManager = FakeDispatcherManager()
private val fileManager: FileManager = mockk() private val fileManager: FileManager = mockk()
private val fakeAuthDiskSource = FakeAuthDiskSource() private val fakeAuthDiskSource = FakeAuthDiskSource()
private val settingsDiskSource = mockk<SettingsDiskSource>()
private val syncService: SyncService = mockk() private val syncService: SyncService = mockk()
private val sendsService: SendsService = mockk() private val sendsService: SendsService = mockk()
private val ciphersService: CiphersService = mockk() private val ciphersService: CiphersService = mockk()
@ -132,10 +139,12 @@ class VaultRepositoryTest {
vaultDiskSource = vaultDiskSource, vaultDiskSource = vaultDiskSource,
vaultSdkSource = vaultSdkSource, vaultSdkSource = vaultSdkSource,
authDiskSource = fakeAuthDiskSource, authDiskSource = fakeAuthDiskSource,
settingsDiskSource = settingsDiskSource,
vaultLockManager = vaultLockManager, vaultLockManager = vaultLockManager,
dispatcherManager = dispatcherManager, dispatcherManager = dispatcherManager,
totpCodeManager = totpCodeManager, totpCodeManager = totpCodeManager,
fileManager = fileManager, fileManager = fileManager,
clock = clock,
) )
@BeforeEach @BeforeEach
@ -412,6 +421,9 @@ class VaultRepositoryTest {
vault = mockSyncResponse, vault = mockSyncResponse,
) )
} just runs } just runs
every {
settingsDiskSource.storeLastSyncTime(MOCK_USER_STATE.activeUserId, clock.instant())
} just runs
vaultRepository.sync() vaultRepository.sync()
@ -1051,6 +1063,9 @@ class VaultRepositoryTest {
vault = mockSyncResponse, vault = mockSyncResponse,
) )
} just runs } just runs
every {
settingsDiskSource.storeLastSyncTime(MOCK_USER_STATE.activeUserId, clock.instant())
} just runs
coEvery { coEvery {
vaultSdkSource.decryptSendList( vaultSdkSource.decryptSendList(
userId = userId, userId = userId,
@ -2217,6 +2232,9 @@ class VaultRepositoryTest {
vault = mockSyncResponse, vault = mockSyncResponse,
) )
} just runs } just runs
every {
settingsDiskSource.storeLastSyncTime(MOCK_USER_STATE.activeUserId, clock.instant())
} just runs
val stateFlow = MutableStateFlow<DataState<List<VerificationCodeItem>>>( val stateFlow = MutableStateFlow<DataState<List<VerificationCodeItem>>>(
DataState.Loading, DataState.Loading,

View file

@ -2,9 +2,11 @@ package com.x8bit.bitwarden.ui.platform.feature.settings.other
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test import app.cash.turbine.test
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.vault.repository.VaultRepository import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import com.x8bit.bitwarden.ui.platform.base.util.asText
import io.mockk.every import io.mockk.every
import io.mockk.just import io.mockk.just
import io.mockk.mockk import io.mockk.mockk
@ -14,11 +16,22 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import java.time.Clock
import java.time.Instant
import java.time.ZoneOffset
class OtherViewModelTest : BaseViewModelTest() { class OtherViewModelTest : BaseViewModelTest() {
private val clock: Clock = Clock.fixed(
Instant.parse("2023-10-27T12:00:00Z"),
ZoneOffset.UTC,
)
private val mutablePullToRefreshStateFlow = MutableStateFlow(false) private val mutablePullToRefreshStateFlow = MutableStateFlow(false)
private val instant: Instant = Instant.parse("2023-10-26T12:00:00Z")
private val mutableVaultLastSyncStateFlow = MutableStateFlow(instant)
private val settingsRepository = mockk<SettingsRepository> { private val settingsRepository = mockk<SettingsRepository> {
every { getPullToRefreshEnabledFlow() } returns mutablePullToRefreshStateFlow every { getPullToRefreshEnabledFlow() } returns mutablePullToRefreshStateFlow
every { vaultLastSyncStateFlow } returns mutableVaultLastSyncStateFlow
every { vaultLastSync } returns instant
} }
private val vaultRepository = mockk<VaultRepository>() private val vaultRepository = mockk<VaultRepository>()
@ -103,13 +116,38 @@ class OtherViewModelTest : BaseViewModelTest() {
} }
} }
@Test
fun `on VaultLastSyncReceive should sync repo`() = runTest {
val newSyncTime = Instant.parse("2023-10-27T12:00:00Z")
val viewModel = createViewModel()
viewModel.stateFlow.test {
assertEquals(DEFAULT_STATE, awaitItem())
mutableVaultLastSyncStateFlow.tryEmit(newSyncTime)
assertEquals(
DEFAULT_STATE.copy(
lastSyncTime = "10/27/2023 12:00 PM",
dialogState = null,
),
awaitItem(),
)
}
}
@Test @Test
fun `on SyncNowButtonClick should sync repo`() = runTest { fun `on SyncNowButtonClick should sync repo`() = runTest {
every { vaultRepository.sync() } just runs every { vaultRepository.sync() } just runs
val viewModel = createViewModel() val viewModel = createViewModel()
viewModel.eventFlow.test { viewModel.stateFlow.test {
assertEquals(DEFAULT_STATE, awaitItem())
viewModel.trySendAction(OtherAction.SyncNowButtonClick) viewModel.trySendAction(OtherAction.SyncNowButtonClick)
expectNoEvents() assertEquals(
DEFAULT_STATE.copy(
dialogState = OtherState.DialogState.Loading(
message = R.string.syncing.asText(),
),
),
awaitItem(),
)
} }
verify { vaultRepository.sync() } verify { vaultRepository.sync() }
} }
@ -117,6 +155,7 @@ class OtherViewModelTest : BaseViewModelTest() {
private fun createViewModel( private fun createViewModel(
state: OtherState? = null, state: OtherState? = null,
) = OtherViewModel( ) = OtherViewModel(
clock = clock,
settingsRepo = settingsRepository, settingsRepo = settingsRepository,
vaultRepo = vaultRepository, vaultRepo = vaultRepository,
savedStateHandle = SavedStateHandle().apply { savedStateHandle = SavedStateHandle().apply {
@ -129,7 +168,7 @@ class OtherViewModelTest : BaseViewModelTest() {
allowScreenCapture = false, allowScreenCapture = false,
allowSyncOnRefresh = false, allowSyncOnRefresh = false,
clearClipboardFrequency = OtherState.ClearClipboardFrequency.DEFAULT, clearClipboardFrequency = OtherState.ClearClipboardFrequency.DEFAULT,
lastSyncTime = "5/14/2023 4:52 PM", lastSyncTime = "10/26/2023 12:00 PM",
dialogState = null, dialogState = null,
) )
} }