mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-07-20 20:44:44 +01:00
Improve device transfer test coverage in regV5.
This commit is contained in:
committed by
Michelle Tang
parent
b10cd9c5d3
commit
bd42444e00
+59
@@ -5,7 +5,12 @@
|
||||
|
||||
package org.signal.registration.screens.devicetransfer.complete
|
||||
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.hasSize
|
||||
import assertk.assertions.isEmpty
|
||||
import assertk.assertions.isNull
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.coVerifyOrder
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
@@ -61,4 +66,58 @@ class DeviceTransferCompleteViewModelTest {
|
||||
coVerify { mockRepository.setRestoreDecision(RestoreDecision.COMPLETED) }
|
||||
coVerify { mockRepository.finishRegistrationOrCreateProfile(parentEventEmitter, any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ContinueClicked records the restore decision before finishing registration`() = runTest {
|
||||
viewModel.applyEvent(
|
||||
DeviceTransferCompleteState(),
|
||||
DeviceTransferCompleteScreenEvents.ContinueClicked,
|
||||
parentEventEmitter,
|
||||
mockRepository,
|
||||
stateEmitter
|
||||
)
|
||||
|
||||
coVerifyOrder {
|
||||
mockRepository.setRestoreDecision(RestoreDecision.COMPLETED)
|
||||
mockRepository.finishRegistrationOrCreateProfile(parentEventEmitter, any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ContinueClicked does not emit any state itself`() = runTest {
|
||||
viewModel.applyEvent(
|
||||
DeviceTransferCompleteState(),
|
||||
DeviceTransferCompleteScreenEvents.ContinueClicked,
|
||||
parentEventEmitter,
|
||||
mockRepository,
|
||||
stateEmitter
|
||||
)
|
||||
|
||||
assertThat(emittedStates).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ConsumeOneTimeEvent clears the one-time event without touching the repository`() = runTest {
|
||||
viewModel.applyEvent(
|
||||
DeviceTransferCompleteState(),
|
||||
DeviceTransferCompleteScreenEvents.ConsumeOneTimeEvent,
|
||||
parentEventEmitter,
|
||||
mockRepository,
|
||||
stateEmitter
|
||||
)
|
||||
|
||||
assertThat(emittedStates).hasSize(1)
|
||||
assertThat(emittedStates.last().oneTimeEvent).isNull()
|
||||
coVerify(exactly = 0) { mockRepository.setRestoreDecision(any()) }
|
||||
coVerify(exactly = 0) { mockRepository.finishRegistrationOrCreateProfile(any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ContinueClicked through the real event channel hands off to the repository`() = runTest {
|
||||
viewModel.onEvent(DeviceTransferCompleteScreenEvents.ContinueClicked)
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
coVerify { mockRepository.setRestoreDecision(RestoreDecision.COMPLETED) }
|
||||
coVerify { mockRepository.finishRegistrationOrCreateProfile(parentEventEmitter, any()) }
|
||||
}
|
||||
}
|
||||
|
||||
+35
@@ -7,6 +7,9 @@ package org.signal.registration.screens.devicetransfer.instructions
|
||||
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.containsExactly
|
||||
import assertk.assertions.hasSize
|
||||
import assertk.assertions.isEmpty
|
||||
import assertk.assertions.isNull
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
@@ -55,6 +58,7 @@ class DeviceTransferInstructionsViewModelTest {
|
||||
)
|
||||
|
||||
assertThat(emittedEvents).containsExactly(RegistrationFlowEvent.NavigateToScreen(RegistrationRoute.DeviceTransferSetup))
|
||||
assertThat(emittedStates).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -66,6 +70,37 @@ class DeviceTransferInstructionsViewModelTest {
|
||||
stateEmitter
|
||||
)
|
||||
|
||||
assertThat(emittedEvents).containsExactly(RegistrationFlowEvent.NavigateBack)
|
||||
assertThat(emittedStates).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ConsumeOneTimeEvent clears the one-time event and emits no navigation`() = runTest {
|
||||
viewModel.applyEvent(
|
||||
DeviceTransferInstructionsState(),
|
||||
DeviceTransferInstructionsScreenEvents.ConsumeOneTimeEvent,
|
||||
parentEventEmitter,
|
||||
stateEmitter
|
||||
)
|
||||
|
||||
assertThat(emittedStates).hasSize(1)
|
||||
assertThat(emittedStates.last().oneTimeEvent).isNull()
|
||||
assertThat(emittedEvents).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ContinueClicked through the real event channel navigates to Setup`() = runTest {
|
||||
viewModel.onEvent(DeviceTransferInstructionsScreenEvents.ContinueClicked)
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
assertThat(emittedEvents).containsExactly(RegistrationFlowEvent.NavigateToScreen(RegistrationRoute.DeviceTransferSetup))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `BackClicked through the real event channel navigates back`() = runTest {
|
||||
viewModel.onEvent(DeviceTransferInstructionsScreenEvents.BackClicked)
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
assertThat(emittedEvents).containsExactly(RegistrationFlowEvent.NavigateBack)
|
||||
}
|
||||
}
|
||||
|
||||
+156
-2
@@ -9,7 +9,12 @@ import android.content.Context
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.contains
|
||||
import assertk.assertions.containsExactly
|
||||
import assertk.assertions.isEmpty
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isFalse
|
||||
import assertk.assertions.isNull
|
||||
import assertk.assertions.isTrue
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
@@ -36,6 +41,8 @@ class DeviceTransferProgressViewModelTest {
|
||||
private lateinit var progressEvents: MutableSharedFlow<NewDeviceRestoreStatus>
|
||||
private lateinit var emittedEvents: MutableList<RegistrationFlowEvent>
|
||||
private lateinit var parentEventEmitter: (RegistrationFlowEvent) -> Unit
|
||||
private lateinit var emittedStates: MutableList<DeviceTransferProgressState>
|
||||
private lateinit var stateEmitter: (DeviceTransferProgressState) -> Unit
|
||||
private lateinit var viewModel: DeviceTransferProgressViewModel
|
||||
|
||||
@Before
|
||||
@@ -45,6 +52,8 @@ class DeviceTransferProgressViewModelTest {
|
||||
progressEvents = MutableSharedFlow(extraBufferCapacity = 16)
|
||||
emittedEvents = mutableListOf()
|
||||
parentEventEmitter = { emittedEvents.add(it) }
|
||||
emittedStates = mutableListOf()
|
||||
stateEmitter = { emittedStates.add(it) }
|
||||
viewModel = DeviceTransferProgressViewModel(context, progressEvents, parentEventEmitter)
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
}
|
||||
@@ -54,8 +63,18 @@ class DeviceTransferProgressViewModelTest {
|
||||
Dispatchers.resetMain()
|
||||
}
|
||||
|
||||
// region Progress (EventBus → state) mapping
|
||||
|
||||
@Test
|
||||
fun `InProgress updates message count`() = runTest {
|
||||
fun `default state is receiving with no error`() = runTest {
|
||||
assertThat(viewModel.state.value.status).isEqualTo(DeviceTransferProgressState.Status.RECEIVING)
|
||||
assertThat(viewModel.state.value.messageCount).isEqualTo(0L)
|
||||
assertThat(viewModel.state.value.errorReason).isNull()
|
||||
assertThat(viewModel.showCancelDialog.value).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `InProgress updates message count and keeps receiving`() = runTest {
|
||||
progressEvents.tryEmit(NewDeviceRestoreStatus(42, NewDeviceRestoreStatus.State.IN_PROGRESS))
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
@@ -64,10 +83,30 @@ class DeviceTransferProgressViewModelTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `RestoreComplete navigates to Complete screen`() = runTest {
|
||||
fun `successive InProgress events advance the message count`() = runTest {
|
||||
progressEvents.tryEmit(NewDeviceRestoreStatus(10, NewDeviceRestoreStatus.State.IN_PROGRESS))
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
progressEvents.tryEmit(NewDeviceRestoreStatus(99, NewDeviceRestoreStatus.State.IN_PROGRESS))
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
assertThat(viewModel.state.value.messageCount).isEqualTo(99L)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `TransferComplete moves to importing and retains message count`() = runTest {
|
||||
progressEvents.tryEmit(NewDeviceRestoreStatus(7, NewDeviceRestoreStatus.State.TRANSFER_COMPLETE))
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
assertThat(viewModel.state.value.status).isEqualTo(DeviceTransferProgressState.Status.IMPORTING)
|
||||
assertThat(viewModel.state.value.messageCount).isEqualTo(7L)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `RestoreComplete moves to finalizing and navigates to Complete screen`() = runTest {
|
||||
progressEvents.tryEmit(NewDeviceRestoreStatus(0, NewDeviceRestoreStatus.State.RESTORE_COMPLETE))
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
assertThat(viewModel.state.value.status).isEqualTo(DeviceTransferProgressState.Status.FINALIZING)
|
||||
assertThat(emittedEvents).contains(RegistrationFlowEvent.NavigateToScreen(RegistrationRoute.DeviceTransferComplete))
|
||||
}
|
||||
|
||||
@@ -79,4 +118,119 @@ class DeviceTransferProgressViewModelTest {
|
||||
assertThat(viewModel.state.value.status).isEqualTo(DeviceTransferProgressState.Status.FAILED)
|
||||
assertThat(viewModel.state.value.errorReason).isEqualTo(DeviceTransferProgressState.ErrorReason.VERSION_DOWNGRADE)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ForeignKey failure sets FAILED with correct reason`() = runTest {
|
||||
progressEvents.tryEmit(NewDeviceRestoreStatus(0, NewDeviceRestoreStatus.State.FAILURE_FOREIGN_KEY))
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
assertThat(viewModel.state.value.status).isEqualTo(DeviceTransferProgressState.Status.FAILED)
|
||||
assertThat(viewModel.state.value.errorReason).isEqualTo(DeviceTransferProgressState.ErrorReason.FOREIGN_KEY)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Unknown failure sets FAILED with correct reason`() = runTest {
|
||||
progressEvents.tryEmit(NewDeviceRestoreStatus(0, NewDeviceRestoreStatus.State.FAILURE_UNKNOWN))
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
assertThat(viewModel.state.value.status).isEqualTo(DeviceTransferProgressState.Status.FAILED)
|
||||
assertThat(viewModel.state.value.errorReason).isEqualTo(DeviceTransferProgressState.ErrorReason.UNKNOWN)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a failure does not navigate away from the progress screen`() = runTest {
|
||||
progressEvents.tryEmit(NewDeviceRestoreStatus(0, NewDeviceRestoreStatus.State.FAILURE_UNKNOWN))
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
assertThat(emittedEvents).isEmpty()
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Reducer (screen events)
|
||||
|
||||
@Test
|
||||
fun `CancelClicked shows the cancel dialog`() = runTest {
|
||||
viewModel.applyEvent(
|
||||
DeviceTransferProgressState(),
|
||||
DeviceTransferProgressScreenEvents.CancelClicked,
|
||||
parentEventEmitter,
|
||||
stateEmitter
|
||||
)
|
||||
|
||||
assertThat(viewModel.showCancelDialog.value).isTrue()
|
||||
assertThat(emittedEvents).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CancelDismissed hides the cancel dialog and stays on screen`() = runTest {
|
||||
viewModel.applyEvent(
|
||||
DeviceTransferProgressState(),
|
||||
DeviceTransferProgressScreenEvents.CancelClicked,
|
||||
parentEventEmitter,
|
||||
stateEmitter
|
||||
)
|
||||
viewModel.applyEvent(
|
||||
DeviceTransferProgressState(),
|
||||
DeviceTransferProgressScreenEvents.CancelDismissed,
|
||||
parentEventEmitter,
|
||||
stateEmitter
|
||||
)
|
||||
|
||||
assertThat(viewModel.showCancelDialog.value).isFalse()
|
||||
assertThat(emittedEvents).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CancelConfirmed hides the dialog and navigates back`() = runTest {
|
||||
viewModel.applyEvent(
|
||||
DeviceTransferProgressState(),
|
||||
DeviceTransferProgressScreenEvents.CancelClicked,
|
||||
parentEventEmitter,
|
||||
stateEmitter
|
||||
)
|
||||
viewModel.applyEvent(
|
||||
DeviceTransferProgressState(),
|
||||
DeviceTransferProgressScreenEvents.CancelConfirmed,
|
||||
parentEventEmitter,
|
||||
stateEmitter
|
||||
)
|
||||
|
||||
assertThat(viewModel.showCancelDialog.value).isFalse()
|
||||
assertThat(emittedEvents).containsExactly(RegistrationFlowEvent.NavigateBack)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `TryAgainClicked navigates back to the instructions screen`() = runTest {
|
||||
viewModel.applyEvent(
|
||||
DeviceTransferProgressState(status = DeviceTransferProgressState.Status.FAILED),
|
||||
DeviceTransferProgressScreenEvents.TryAgainClicked,
|
||||
parentEventEmitter,
|
||||
stateEmitter
|
||||
)
|
||||
|
||||
assertThat(emittedEvents).containsExactly(RegistrationFlowEvent.NavigateToScreen(RegistrationRoute.DeviceTransferInstructions))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ConsumeOneTimeEvent clears the one-time event`() = runTest {
|
||||
viewModel.applyEvent(
|
||||
DeviceTransferProgressState(oneTimeEvent = DeviceTransferProgressState.OneTimeEvent.TransferCanceled),
|
||||
DeviceTransferProgressScreenEvents.ConsumeOneTimeEvent,
|
||||
parentEventEmitter,
|
||||
stateEmitter
|
||||
)
|
||||
|
||||
assertThat(emittedStates.last().oneTimeEvent).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CancelConfirmed through the real event channel navigates back`() = runTest {
|
||||
viewModel.onEvent(DeviceTransferProgressScreenEvents.CancelConfirmed)
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
assertThat(emittedEvents).containsExactly(RegistrationFlowEvent.NavigateBack)
|
||||
}
|
||||
|
||||
// endregion
|
||||
}
|
||||
|
||||
+271
-19
@@ -9,7 +9,13 @@ import android.content.Context
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.contains
|
||||
import assertk.assertions.containsExactly
|
||||
import assertk.assertions.doesNotContain
|
||||
import assertk.assertions.isEmpty
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isFalse
|
||||
import assertk.assertions.isNull
|
||||
import assertk.assertions.isTrue
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
@@ -31,10 +37,10 @@ import org.signal.registration.RegistrationFlowState
|
||||
import org.signal.registration.RegistrationRoute
|
||||
|
||||
/**
|
||||
* Exercises the event-reducer half of the setup VM (TransferStatus → step mapping + parent
|
||||
* navigation). The OS gates (permissions / location services / wifi / Wi-Fi Direct) are driven
|
||||
* by concrete Android APIs against `context`; that side is covered by manual QA rather than unit
|
||||
* tests.
|
||||
* Exercises the event-reducer half of the setup VM (TransferStatus → step mapping, the
|
||||
* setting-up / waiting timeout jobs, and the screen-event reducer + parent navigation). The OS
|
||||
* gates (permissions / location services / wifi / Wi-Fi Direct) are driven by concrete Android
|
||||
* APIs against `context`; that side is covered by manual QA rather than unit tests.
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@@ -47,6 +53,9 @@ class DeviceTransferSetupViewModelTest {
|
||||
private lateinit var setupEvents: MutableSharedFlow<TransferStatus>
|
||||
private lateinit var emittedEvents: MutableList<RegistrationFlowEvent>
|
||||
private lateinit var parentEventEmitter: (RegistrationFlowEvent) -> Unit
|
||||
private lateinit var emittedStates: MutableList<DeviceTransferSetupState>
|
||||
private lateinit var stateEmitter: (DeviceTransferSetupState) -> Unit
|
||||
private lateinit var viewModel: DeviceTransferSetupViewModel
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
@@ -56,6 +65,10 @@ class DeviceTransferSetupViewModelTest {
|
||||
setupEvents = MutableSharedFlow(extraBufferCapacity = 16)
|
||||
emittedEvents = mutableListOf()
|
||||
parentEventEmitter = { emittedEvents.add(it) }
|
||||
emittedStates = mutableListOf()
|
||||
stateEmitter = { emittedStates.add(it) }
|
||||
viewModel = DeviceTransferSetupViewModel(context, networkController, setupEvents, MutableStateFlow(RegistrationFlowState()), parentEventEmitter)
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
}
|
||||
|
||||
@After
|
||||
@@ -63,39 +76,278 @@ class DeviceTransferSetupViewModelTest {
|
||||
Dispatchers.resetMain()
|
||||
}
|
||||
|
||||
// region TransferStatus → step mapping
|
||||
|
||||
@Test
|
||||
fun `Ready event moves to SETTING_UP`() = runTest {
|
||||
setupEvents.tryEmit(TransferStatus.ready())
|
||||
testDispatcher.scheduler.runCurrent()
|
||||
|
||||
assertThat(viewModel.state.value.step).isEqualTo(SetupStep.SETTING_UP)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `StartingUp event moves to SETTING_UP`() = runTest {
|
||||
setupEvents.tryEmit(TransferStatus.startingUp())
|
||||
testDispatcher.scheduler.runCurrent()
|
||||
|
||||
assertThat(viewModel.state.value.step).isEqualTo(SetupStep.SETTING_UP)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Discovery event moves to WAITING and clears takingTooLong`() = runTest {
|
||||
setupEvents.tryEmit(TransferStatus.discovery())
|
||||
testDispatcher.scheduler.runCurrent()
|
||||
|
||||
assertThat(viewModel.state.value.step).isEqualTo(SetupStep.WAITING)
|
||||
assertThat(viewModel.state.value.takingTooLong).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `VerificationRequired event moves to VERIFY with SAS code`() = runTest {
|
||||
val viewModel = DeviceTransferSetupViewModel(context, networkController, setupEvents, MutableStateFlow(RegistrationFlowState()), parentEventEmitter)
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
setupEvents.tryEmit(TransferStatus.verificationRequired(1234567))
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
testDispatcher.scheduler.runCurrent()
|
||||
|
||||
assertThat(viewModel.state.value.step).isEqualTo(SetupStep.VERIFY)
|
||||
assertThat(viewModel.state.value.authenticationCode).isEqualTo(1234567)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ServiceConnected event navigates to progress screen`() = runTest {
|
||||
val viewModel = DeviceTransferSetupViewModel(context, networkController, setupEvents, MutableStateFlow(RegistrationFlowState()), parentEventEmitter)
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
fun `ServiceConnected event moves to CONNECTED and navigates to progress screen`() = runTest {
|
||||
setupEvents.tryEmit(TransferStatus.serviceConnected())
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
testDispatcher.scheduler.runCurrent()
|
||||
|
||||
assertThat(viewModel.state.value.step).isEqualTo(SetupStep.CONNECTED)
|
||||
assertThat(emittedEvents).contains(RegistrationFlowEvent.NavigateToScreen(RegistrationRoute.DeviceTransferProgress))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Failed event moves to ERROR`() = runTest {
|
||||
val viewModel = DeviceTransferSetupViewModel(context, networkController, setupEvents, MutableStateFlow(RegistrationFlowState()), parentEventEmitter)
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
fun `Failed event moves to ERROR and shows error dialog`() = runTest {
|
||||
setupEvents.tryEmit(TransferStatus.failed())
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
testDispatcher.scheduler.runCurrent()
|
||||
|
||||
assertThat(viewModel.state.value.step).isEqualTo(SetupStep.ERROR)
|
||||
assertThat(viewModel.state.value.showErrorDialog).isEqualTo(true)
|
||||
assertThat(viewModel.state.value.showErrorDialog).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Shutdown event moves to ERROR and shows error dialog`() = runTest {
|
||||
setupEvents.tryEmit(TransferStatus.shutdown())
|
||||
testDispatcher.scheduler.runCurrent()
|
||||
|
||||
assertThat(viewModel.state.value.step).isEqualTo(SetupStep.ERROR)
|
||||
assertThat(viewModel.state.value.showErrorDialog).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Unavailable event is ignored`() = runTest {
|
||||
val before = viewModel.state.value.step
|
||||
setupEvents.tryEmit(TransferStatus.unavailable())
|
||||
testDispatcher.scheduler.runCurrent()
|
||||
|
||||
assertThat(viewModel.state.value.step).isEqualTo(before)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NetworkConnected event is ignored`() = runTest {
|
||||
val before = viewModel.state.value.step
|
||||
setupEvents.tryEmit(TransferStatus.networkConnected())
|
||||
testDispatcher.scheduler.runCurrent()
|
||||
|
||||
assertThat(viewModel.state.value.step).isEqualTo(before)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Timeout jobs
|
||||
|
||||
@Test
|
||||
fun `SETTING_UP that lingers flips takingTooLong after the prepare timeout`() = runTest {
|
||||
setupEvents.tryEmit(TransferStatus.ready())
|
||||
testDispatcher.scheduler.runCurrent()
|
||||
assertThat(viewModel.state.value.takingTooLong).isFalse()
|
||||
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
assertThat(viewModel.state.value.step).isEqualTo(SetupStep.SETTING_UP)
|
||||
assertThat(viewModel.state.value.takingTooLong).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WAITING that lingers moves to TROUBLESHOOTING after the waiting timeout`() = runTest {
|
||||
setupEvents.tryEmit(TransferStatus.discovery())
|
||||
testDispatcher.scheduler.runCurrent()
|
||||
assertThat(viewModel.state.value.step).isEqualTo(SetupStep.WAITING)
|
||||
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
assertThat(viewModel.state.value.step).isEqualTo(SetupStep.TROUBLESHOOTING)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `VerificationRequired cancels the waiting timeout`() = runTest {
|
||||
setupEvents.tryEmit(TransferStatus.discovery())
|
||||
testDispatcher.scheduler.runCurrent()
|
||||
setupEvents.tryEmit(TransferStatus.verificationRequired(7654321))
|
||||
testDispatcher.scheduler.runCurrent()
|
||||
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
assertThat(viewModel.state.value.step).isEqualTo(SetupStep.VERIFY)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `events are ignored after a waiting-timeout shutdown until a retry`() = runTest {
|
||||
setupEvents.tryEmit(TransferStatus.discovery())
|
||||
testDispatcher.scheduler.runCurrent()
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
assertThat(viewModel.state.value.step).isEqualTo(SetupStep.TROUBLESHOOTING)
|
||||
|
||||
setupEvents.tryEmit(TransferStatus.serviceConnected())
|
||||
testDispatcher.scheduler.runCurrent()
|
||||
|
||||
assertThat(viewModel.state.value.step).isEqualTo(SetupStep.TROUBLESHOOTING)
|
||||
assertThat(emittedEvents).doesNotContain(RegistrationFlowEvent.NavigateToScreen(RegistrationRoute.DeviceTransferProgress))
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Screen-event reducer
|
||||
|
||||
@Test
|
||||
fun `PermissionsDenied moves to PERMISSIONS_DENIED`() = runTest {
|
||||
viewModel.applyEvent(
|
||||
DeviceTransferSetupState(step = SetupStep.PERMISSIONS_CHECK),
|
||||
DeviceTransferSetupScreenEvents.PermissionsDenied,
|
||||
parentEventEmitter,
|
||||
stateEmitter
|
||||
)
|
||||
|
||||
assertThat(emittedStates.last().step).isEqualTo(SetupStep.PERMISSIONS_DENIED)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `RequestPermissionClicked requests the location permission`() = runTest {
|
||||
viewModel.applyEvent(
|
||||
DeviceTransferSetupState(),
|
||||
DeviceTransferSetupScreenEvents.RequestPermissionClicked,
|
||||
parentEventEmitter,
|
||||
stateEmitter
|
||||
)
|
||||
|
||||
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(DeviceTransferSetupState.OneTimeEvent.RequestLocationPermission)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `OpenLocationSettingsClicked emits the open-location one-time event`() = runTest {
|
||||
viewModel.applyEvent(
|
||||
DeviceTransferSetupState(),
|
||||
DeviceTransferSetupScreenEvents.OpenLocationSettingsClicked,
|
||||
parentEventEmitter,
|
||||
stateEmitter
|
||||
)
|
||||
|
||||
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(DeviceTransferSetupState.OneTimeEvent.OpenLocationSettings)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `OpenWifiSettingsClicked emits the open-wifi one-time event`() = runTest {
|
||||
viewModel.applyEvent(
|
||||
DeviceTransferSetupState(),
|
||||
DeviceTransferSetupScreenEvents.OpenWifiSettingsClicked,
|
||||
parentEventEmitter,
|
||||
stateEmitter
|
||||
)
|
||||
|
||||
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(DeviceTransferSetupState.OneTimeEvent.OpenWifiSettings)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `OpenAppSettingsClicked emits the open-app-settings one-time event`() = runTest {
|
||||
viewModel.applyEvent(
|
||||
DeviceTransferSetupState(),
|
||||
DeviceTransferSetupScreenEvents.OpenAppSettingsClicked,
|
||||
parentEventEmitter,
|
||||
stateEmitter
|
||||
)
|
||||
|
||||
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(DeviceTransferSetupState.OneTimeEvent.OpenAppSettings)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `UserVerifiedCode moves to WAITING_FOR_OTHER_TO_VERIFY`() = runTest {
|
||||
viewModel.applyEvent(
|
||||
DeviceTransferSetupState(step = SetupStep.VERIFY, authenticationCode = 12345),
|
||||
DeviceTransferSetupScreenEvents.UserVerifiedCode,
|
||||
parentEventEmitter,
|
||||
stateEmitter
|
||||
)
|
||||
|
||||
assertThat(emittedStates.last().step).isEqualTo(SetupStep.WAITING_FOR_OTHER_TO_VERIFY)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `UserRejectedCode shows the reject-confirmation dialog`() = runTest {
|
||||
viewModel.applyEvent(
|
||||
DeviceTransferSetupState(step = SetupStep.VERIFY),
|
||||
DeviceTransferSetupScreenEvents.UserRejectedCode,
|
||||
parentEventEmitter,
|
||||
stateEmitter
|
||||
)
|
||||
|
||||
assertThat(emittedStates.last().showVerifyRejectDialog).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `VerifyRejectConfirmed hides the dialog and navigates back`() = runTest {
|
||||
viewModel.applyEvent(
|
||||
DeviceTransferSetupState(step = SetupStep.VERIFY, showVerifyRejectDialog = true),
|
||||
DeviceTransferSetupScreenEvents.VerifyRejectConfirmed,
|
||||
parentEventEmitter,
|
||||
stateEmitter
|
||||
)
|
||||
|
||||
assertThat(emittedStates.last().showVerifyRejectDialog).isFalse()
|
||||
assertThat(emittedEvents).contains(RegistrationFlowEvent.NavigateBack)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `VerifyRejectDismissed hides the dialog and stays put`() = runTest {
|
||||
viewModel.applyEvent(
|
||||
DeviceTransferSetupState(step = SetupStep.VERIFY, showVerifyRejectDialog = true),
|
||||
DeviceTransferSetupScreenEvents.VerifyRejectDismissed,
|
||||
parentEventEmitter,
|
||||
stateEmitter
|
||||
)
|
||||
|
||||
assertThat(emittedStates.last().showVerifyRejectDialog).isFalse()
|
||||
assertThat(emittedEvents).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `BackClicked navigates back without emitting state`() = runTest {
|
||||
viewModel.applyEvent(
|
||||
DeviceTransferSetupState(step = SetupStep.WAITING),
|
||||
DeviceTransferSetupScreenEvents.BackClicked,
|
||||
parentEventEmitter,
|
||||
stateEmitter
|
||||
)
|
||||
|
||||
assertThat(emittedEvents).containsExactly(RegistrationFlowEvent.NavigateBack)
|
||||
assertThat(emittedStates).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ConsumeOneTimeEvent clears the one-time event`() = runTest {
|
||||
viewModel.applyEvent(
|
||||
DeviceTransferSetupState(oneTimeEvent = DeviceTransferSetupState.OneTimeEvent.OpenWifiSettings),
|
||||
DeviceTransferSetupScreenEvents.ConsumeOneTimeEvent,
|
||||
parentEventEmitter,
|
||||
stateEmitter
|
||||
)
|
||||
|
||||
assertThat(emittedStates.last().oneTimeEvent).isNull()
|
||||
}
|
||||
|
||||
// endregion
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user