Improve device transfer test coverage in regV5.

This commit is contained in:
Greyson Parrelli
2026-06-29 14:45:34 -04:00
committed by Michelle Tang
parent b10cd9c5d3
commit bd42444e00
4 changed files with 521 additions and 21 deletions
@@ -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()) }
}
}
@@ -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)
}
}
@@ -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
}
@@ -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
}