diff --git a/feature/registration/src/test/java/org/signal/registration/screens/devicetransfer/complete/DeviceTransferCompleteViewModelTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/devicetransfer/complete/DeviceTransferCompleteViewModelTest.kt index a53918e870..0ca8a266c3 100644 --- a/feature/registration/src/test/java/org/signal/registration/screens/devicetransfer/complete/DeviceTransferCompleteViewModelTest.kt +++ b/feature/registration/src/test/java/org/signal/registration/screens/devicetransfer/complete/DeviceTransferCompleteViewModelTest.kt @@ -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()) } + } } diff --git a/feature/registration/src/test/java/org/signal/registration/screens/devicetransfer/instructions/DeviceTransferInstructionsViewModelTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/devicetransfer/instructions/DeviceTransferInstructionsViewModelTest.kt index fe7706ac3f..b45f9d0f3c 100644 --- a/feature/registration/src/test/java/org/signal/registration/screens/devicetransfer/instructions/DeviceTransferInstructionsViewModelTest.kt +++ b/feature/registration/src/test/java/org/signal/registration/screens/devicetransfer/instructions/DeviceTransferInstructionsViewModelTest.kt @@ -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) } } diff --git a/feature/registration/src/test/java/org/signal/registration/screens/devicetransfer/progress/DeviceTransferProgressViewModelTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/devicetransfer/progress/DeviceTransferProgressViewModelTest.kt index b1f7e73458..c9bb08c1fb 100644 --- a/feature/registration/src/test/java/org/signal/registration/screens/devicetransfer/progress/DeviceTransferProgressViewModelTest.kt +++ b/feature/registration/src/test/java/org/signal/registration/screens/devicetransfer/progress/DeviceTransferProgressViewModelTest.kt @@ -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 private lateinit var emittedEvents: MutableList private lateinit var parentEventEmitter: (RegistrationFlowEvent) -> Unit + private lateinit var emittedStates: MutableList + 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 } diff --git a/feature/registration/src/test/java/org/signal/registration/screens/devicetransfer/setup/DeviceTransferSetupViewModelTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/devicetransfer/setup/DeviceTransferSetupViewModelTest.kt index 99699079a8..ac4355f6f8 100644 --- a/feature/registration/src/test/java/org/signal/registration/screens/devicetransfer/setup/DeviceTransferSetupViewModelTest.kt +++ b/feature/registration/src/test/java/org/signal/registration/screens/devicetransfer/setup/DeviceTransferSetupViewModelTest.kt @@ -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 private lateinit var emittedEvents: MutableList private lateinit var parentEventEmitter: (RegistrationFlowEvent) -> Unit + private lateinit var emittedStates: MutableList + 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 }