diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/signin/SignInActivity.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/signin/SignInActivity.kt index 81e9d037c..c934ad12c 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/signin/SignInActivity.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/signin/SignInActivity.kt @@ -14,6 +14,7 @@ import com.festago.festago.databinding.ActivitySignInBinding import com.festago.festago.presentation.ui.customview.OkDialogFragment import com.festago.festago.presentation.ui.home.HomeActivity import com.festago.festago.presentation.util.loginWithKakao +import com.festago.festago.presentation.util.repeatOnStarted import com.kakao.sdk.user.UserApiClient import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch @@ -44,15 +45,19 @@ class SignInActivity : AppCompatActivity() { } private fun initObserve() { - vm.event.observe(this) { event -> - when (event) { - SignInEvent.ShowSignInPage -> handleSignInEvent() - SignInEvent.SignInSuccess -> handleSuccessEvent() - SignInEvent.SignInFailure -> handleFailureEvent() + repeatOnStarted(this) { + vm.event.collect { + handleEvent(it) } } } + private fun handleEvent(event: SignInEvent) = when (event) { + SignInEvent.ShowSignInPage -> handleSignInEvent() + SignInEvent.SignInSuccess -> handleSuccessEvent() + SignInEvent.SignInFailure -> handleFailureEvent() + } + private fun initComment() { val spannableStringBuilder = SpannableStringBuilder( getString(R.string.mypage_tv_signin_description), diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/signin/SignInViewModel.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/signin/SignInViewModel.kt index cb2aba5eb..512845288 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/signin/SignInViewModel.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/signin/SignInViewModel.kt @@ -4,11 +4,10 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.festago.festago.analytics.AnalyticsHelper import com.festago.festago.analytics.logNetworkFailure -import com.festago.festago.presentation.util.MutableSingleLiveData -import com.festago.festago.presentation.util.SingleLiveData import com.festago.festago.repository.AuthRepository import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.launch import javax.inject.Inject @@ -18,26 +17,22 @@ class SignInViewModel @Inject constructor( private val analyticsHelper: AnalyticsHelper, ) : ViewModel() { - private val _event = MutableSingleLiveData() - val event: SingleLiveData = _event - - private val exceptionHandler: CoroutineExceptionHandler = - CoroutineExceptionHandler { _, throwable -> - _event.setValue(SignInEvent.SignInFailure) - analyticsHelper.logNetworkFailure(KEY_SIGN_IN_LOG, throwable.message.toString()) - } + private val _event = MutableSharedFlow() + val event: SharedFlow = _event fun signInKakao() { - _event.setValue(SignInEvent.ShowSignInPage) + viewModelScope.launch { + _event.emit(SignInEvent.ShowSignInPage) + } } fun signIn(token: String) { - viewModelScope.launch(exceptionHandler) { + viewModelScope.launch { authRepository.signIn(SOCIAL_TYPE_KAKAO, token) .onSuccess { - _event.setValue(SignInEvent.SignInSuccess) + _event.emit(SignInEvent.SignInSuccess) }.onFailure { - _event.setValue(SignInEvent.SignInFailure) + _event.emit(SignInEvent.SignInFailure) analyticsHelper.logNetworkFailure(KEY_SIGN_IN_LOG, it.message.toString()) } } diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketentry/TicketEntryActivity.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketentry/TicketEntryActivity.kt index 076646850..9e61406a2 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketentry/TicketEntryActivity.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketentry/TicketEntryActivity.kt @@ -14,6 +14,7 @@ import com.festago.festago.presentation.util.repeatOnStarted import com.google.zxing.BarcodeFormat import com.journeyapps.barcodescanner.BarcodeEncoder import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.collectLatest @AndroidEntryPoint class TicketEntryActivity : AppCompatActivity() { @@ -52,12 +53,14 @@ class TicketEntryActivity : AppCompatActivity() { } private fun initObserve() { - vm.uiState.observe(this) { uiState -> - binding.uiState = uiState - when (uiState) { - is TicketEntryUiState.Loading, is TicketEntryUiState.Error -> Unit - is TicketEntryUiState.Success -> { - handleSuccess(uiState) + repeatOnStarted(this) { + vm.uiState.collectLatest { uiState -> + binding.uiState = uiState + when (uiState) { + is TicketEntryUiState.Loading, is TicketEntryUiState.Error -> Unit + is TicketEntryUiState.Success -> { + handleSuccess(uiState) + } } } } @@ -75,6 +78,7 @@ class TicketEntryActivity : AppCompatActivity() { private fun initView(currentTicketId: Long) { vm.loadTicket(currentTicketId) + vm.loadTicketCode(currentTicketId) } private fun handleSuccess(uiState: TicketEntryUiState.Success) { diff --git a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketentry/TicketEntryViewModel.kt b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketentry/TicketEntryViewModel.kt index 9377ba926..b2edea862 100644 --- a/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketentry/TicketEntryViewModel.kt +++ b/android/festago/app/src/main/java/com/festago/festago/presentation/ui/ticketentry/TicketEntryViewModel.kt @@ -1,16 +1,21 @@ package com.festago.festago.presentation.ui.ticketentry -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.festago.festago.analytics.AnalyticsHelper import com.festago.festago.analytics.logNetworkFailure +import com.festago.festago.model.Ticket import com.festago.festago.model.TicketCode import com.festago.festago.model.timer.Timer import com.festago.festago.model.timer.TimerListener import com.festago.festago.repository.TicketRepository import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch import javax.inject.Inject @@ -20,84 +25,81 @@ class TicketEntryViewModel @Inject constructor( private val analyticsHelper: AnalyticsHelper, ) : ViewModel() { - private val _uiState = MutableLiveData() - val uiState: LiveData = _uiState + private val ticketFlow = MutableSharedFlow>() + + private val ticketCodeFlow = MutableSharedFlow>() + + private val _uiState: MutableStateFlow = + MutableStateFlow(TicketEntryUiState.Loading) + val uiState: StateFlow = _uiState.asStateFlow() private val timer: Timer = Timer() - fun loadTicketCode(ticketId: Long) { + init { viewModelScope.launch { - ticketRepository.loadTicketCode(ticketId) - .onSuccess { - val state = uiState.value - if (state is TicketEntryUiState.Success) { - _uiState.value = state.copy(ticketCode = it, remainTime = it.period) - setTimer(ticketId, it) - } - }.onFailure { - _uiState.value = TicketEntryUiState.Error - analyticsHelper.logNetworkFailure( - key = KEY_LOAD_CODE_LOG, - value = it.message.toString(), + combine(ticketFlow, ticketCodeFlow) { ticketResult, ticketCodeResult -> + runCatching { + val ticket = ticketResult.getOrThrowWithLog() + val ticketCode = ticketCodeResult.getOrThrowWithLog() + + setTimer(ticket.id, ticketCode) + + TicketEntryUiState.Success( + ticket = ticket, + ticketCode = ticketCode, + remainTime = ticketCode.period, ) - } + }.getOrElse { TicketEntryUiState.Error } + }.collectLatest { _uiState.value = it } + } + } + + fun loadTicketCode(ticketId: Long) { + viewModelScope.launch { + ticketCodeFlow.emit(ticketRepository.loadTicketCode(ticketId)) } } fun loadTicket(ticketId: Long) { viewModelScope.launch { - _uiState.value = TicketEntryUiState.Loading - ticketRepository.loadTicket(ticketId) - .onSuccess { ticket -> - ticketRepository.loadTicketCode(ticketId) - .onSuccess { ticketCode -> - _uiState.value = TicketEntryUiState.Success( - ticket = ticket, - ticketCode = ticketCode, - remainTime = ticketCode.period, - ) - setTimer(ticketId, ticketCode) - }.onFailure { - _uiState.value = TicketEntryUiState.Error - analyticsHelper.logNetworkFailure( - key = KEY_LOAD_CODE_LOG, - value = it.message.toString(), - ) - } - }.onFailure { - _uiState.value = TicketEntryUiState.Error - analyticsHelper.logNetworkFailure( - key = KEY_LOAD_Ticket_LOG, - value = it.message.toString(), - ) - } + ticketFlow.emit(ticketRepository.loadTicket(ticketId)) } } private suspend fun setTimer(ticketId: Long, ticketCode: TicketCode) { - timer.timerListener = createTimerListener( - ticketId = ticketId, - period = ticketCode.period, - ) + timer.timerListener = createTimerListener(ticketId) timer.start(ticketCode.period) } - private fun createTimerListener(ticketId: Long, period: Int): TimerListener = - object : TimerListener { - override fun onTick(current: Int) { - val state = uiState.value - if (state is TicketEntryUiState.Success) { - _uiState.value = state.copy(remainTime = current) - } + private fun createTimerListener(ticketId: Long): TimerListener = object : TimerListener { + override fun onTick(current: Int) { + val state = uiState.value + if (state is TicketEntryUiState.Success) { + _uiState.value = state.copy(remainTime = current) } + } - override fun onFinish() { - viewModelScope.launch { - timer.start(period) - loadTicketCode(ticketId) - } - } + override fun onFinish() { + loadTicketCode(ticketId) } + } + + private fun Result.getOrThrowWithLog(): Ticket = getOrElse { throwable -> + analyticsHelper.logNetworkFailure( + key = KEY_LOAD_Ticket_LOG, + value = throwable.message.toString(), + ) + + throw throwable + } + + private fun Result.getOrThrowWithLog(): TicketCode = getOrElse { throwable -> + analyticsHelper.logNetworkFailure( + key = KEY_LOAD_CODE_LOG, + value = throwable.message.toString(), + ) + throw throwable + } companion object { private const val KEY_LOAD_Ticket_LOG = "load_ticket" diff --git a/android/festago/app/src/test/java/com/festago/festago/presentation/ui/signin/SignInViewModelTest.kt b/android/festago/app/src/test/java/com/festago/festago/presentation/ui/signin/SignInViewModelTest.kt index 8c23f486a..04b0f293d 100644 --- a/android/festago/app/src/test/java/com/festago/festago/presentation/ui/signin/SignInViewModelTest.kt +++ b/android/festago/app/src/test/java/com/festago/festago/presentation/ui/signin/SignInViewModelTest.kt @@ -1,6 +1,6 @@ package com.festago.festago.presentation.ui.signin -import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import app.cash.turbine.test import com.festago.festago.analytics.AnalyticsHelper import com.festago.festago.repository.AuthRepository import io.mockk.coEvery @@ -9,11 +9,11 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain import org.assertj.core.api.Assertions.assertThat import org.junit.After import org.junit.Before -import org.junit.Rule import org.junit.Test class SignInViewModelTest { @@ -21,9 +21,6 @@ class SignInViewModelTest { private lateinit var authRepository: AuthRepository private lateinit var analyticsHelper: AnalyticsHelper - @get:Rule - val instantExecutorRule = InstantTaskExecutorRule() - @OptIn(ExperimentalCoroutinesApi::class) @Before fun setUp() { @@ -40,49 +37,48 @@ class SignInViewModelTest { Dispatchers.resetMain() } - @Test - fun `로그인 성공하면 성공 이벤트가 발생한다`() { - // given - coEvery { authRepository.signIn(any(), any()) } answers { Result.success(Unit) } - - // when - vm.signIn("testToken") - - // then - assertThat(vm.event.getValue()).isExactlyInstanceOf(SignInEvent.SignInSuccess::class.java) + private fun `로그인 결과가 다음과 같을 때`(result: Result) { + coEvery { authRepository.signIn(any(), any()) } returns result } @Test - fun `로그인 실패하면 실패 이벤트가 발생한다`() { + fun `로그인 성공하면 성공 이벤트가 발생한다`() = runTest { // given - coEvery { - authRepository.signIn(any(), any()) - } answers { Result.failure(Exception()) } + `로그인 결과가 다음과 같을 때`(Result.success(Unit)) - // when - vm.signIn("testToken") + vm.event.test { + // when + vm.signIn("testToken") - // then - assertThat(vm.event.getValue()).isExactlyInstanceOf(SignInEvent.SignInFailure::class.java) + // then + assertThat(awaitItem()).isExactlyInstanceOf(SignInEvent.SignInSuccess::class.java) + } } @Test - fun `로그인을 요청하면 로그인 화면을 보여주는 이벤트가 발생한다`() { + fun `로그인 실패하면 실패 이벤트가 발생한다`() = runTest { // given - // when - vm.signInKakao() + `로그인 결과가 다음과 같을 때`(Result.failure(Exception())) + + vm.event.test { + // when + vm.signIn("testToken") - // then - assertThat(vm.event.getValue()).isExactlyInstanceOf(SignInEvent.ShowSignInPage::class.java) + // then + assertThat(awaitItem()).isExactlyInstanceOf(SignInEvent.SignInFailure::class.java) + } } @Test - fun `FCM 토큰을 불러오지 못하면 실패 이벤트가 발생한다`() { + fun `로그인을 요청하면 로그인 화면을 보여주는 이벤트가 발생한다`() = runTest { // given - // when - vm.signIn("testToken") - // then - assertThat(vm.event.getValue()).isExactlyInstanceOf(SignInEvent.SignInFailure::class.java) + vm.event.test { + // when + vm.signInKakao() + + // then + assertThat(awaitItem()).isExactlyInstanceOf(SignInEvent.ShowSignInPage::class.java) + } } } diff --git a/android/festago/app/src/test/java/com/festago/festago/presentation/ui/ticketentry/TicketEntryViewModelTest.kt b/android/festago/app/src/test/java/com/festago/festago/presentation/ui/ticketentry/TicketEntryViewModelTest.kt index 55fec66bd..9369a2a87 100644 --- a/android/festago/app/src/test/java/com/festago/festago/presentation/ui/ticketentry/TicketEntryViewModelTest.kt +++ b/android/festago/app/src/test/java/com/festago/festago/presentation/ui/ticketentry/TicketEntryViewModelTest.kt @@ -1,7 +1,7 @@ package com.festago.festago.presentation.ui.ticketentry -import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.festago.festago.analytics.AnalyticsHelper +import com.festago.festago.model.Ticket import com.festago.festago.model.TicketCode import com.festago.festago.presentation.fixture.TicketFixture import com.festago.festago.repository.TicketRepository @@ -9,14 +9,13 @@ import io.mockk.coEvery import io.mockk.mockk import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.delay import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain import org.assertj.core.api.SoftAssertions import org.junit.After import org.junit.Before -import org.junit.Rule import org.junit.Test class TicketEntryViewModelTest { @@ -24,9 +23,6 @@ class TicketEntryViewModelTest { private lateinit var ticketRepository: TicketRepository private lateinit var analyticsHelper: AnalyticsHelper - @get:Rule - val instantExecutorRule = InstantTaskExecutorRule() - @OptIn(ExperimentalCoroutinesApi::class) @Before fun setUp() { @@ -42,32 +38,32 @@ class TicketEntryViewModelTest { Dispatchers.resetMain() } + private fun `티켓 요쳥 결과는 다음과 같을 때`(result: Result) { + coEvery { ticketRepository.loadTicket(any()) } returns result + } + + private fun `티켓 코드 요청 결과는 다음과 같을 때`(result: Result) { + coEvery { ticketRepository.loadTicketCode(any()) } returns result + } + @Test fun `티켓 받아오기에 성공하면 성공 상태이고 티켓 코드와 티켓을 가지고 있다`() { // given - coEvery { - ticketRepository.loadTicket(any()) - } answers { - Result.success(TicketFixture.getMemberTicket()) - } - - coEvery { - ticketRepository.loadTicketCode(any()) - } answers { - Result.success(getFakeTicketCode()) - } + `티켓 요쳥 결과는 다음과 같을 때`(Result.success(TicketFixture.getMemberTicket())) + `티켓 코드 요청 결과는 다음과 같을 때`(Result.success(getFakeTicketCode())) // when vm.loadTicket(1L) + vm.loadTicketCode(1L) // then val softly = SoftAssertions().apply { assertThat(vm.uiState.value).isInstanceOf(TicketEntryUiState.Success::class.java) // and - assertThat(vm.uiState.value?.shouldShowSuccess).isEqualTo(true) - assertThat(vm.uiState.value?.shouldShowLoading).isEqualTo(false) - assertThat(vm.uiState.value?.shouldShowError).isEqualTo(false) + assertThat(vm.uiState.value.shouldShowSuccess).isEqualTo(true) + assertThat(vm.uiState.value.shouldShowLoading).isEqualTo(false) + assertThat(vm.uiState.value.shouldShowError).isEqualTo(false) // and val actualTicket = (vm.uiState.value as TicketEntryUiState.Success).ticket @@ -81,36 +77,71 @@ class TicketEntryViewModelTest { @Test fun `티켓 받아오기에 실패하면 에러 상태다`() { // given - coEvery { - ticketRepository.loadTicket(any()) - } answers { - Result.failure(Exception()) + `티켓 요쳥 결과는 다음과 같을 때`(Result.failure(Exception())) + `티켓 코드 요청 결과는 다음과 같을 때`(Result.success(getFakeTicketCode())) + + // when + vm.loadTicket(1L) + vm.loadTicketCode(1L) + + // then + val softly = SoftAssertions().apply { + assertThat(vm.uiState.value).isInstanceOf(TicketEntryUiState.Error::class.java) + + // and + assertThat(vm.uiState.value.shouldShowSuccess).isEqualTo(false) + assertThat(vm.uiState.value.shouldShowLoading).isEqualTo(false) + assertThat(vm.uiState.value.shouldShowError).isEqualTo(true) } + softly.assertAll() + } + + @Test + fun `티켓 코드 받아오기에 실패하면 에러 상태다`() { + // given + `티켓 요쳥 결과는 다음과 같을 때`(Result.success(TicketFixture.getMemberTicket())) + `티켓 코드 요청 결과는 다음과 같을 때`(Result.failure(Exception())) // when vm.loadTicket(1L) + vm.loadTicketCode(1L) // then val softly = SoftAssertions().apply { assertThat(vm.uiState.value).isInstanceOf(TicketEntryUiState.Error::class.java) // and - assertThat(vm.uiState.value?.shouldShowSuccess).isEqualTo(false) - assertThat(vm.uiState.value?.shouldShowLoading).isEqualTo(false) - assertThat(vm.uiState.value?.shouldShowError).isEqualTo(true) + assertThat(vm.uiState.value.shouldShowSuccess).isEqualTo(false) + assertThat(vm.uiState.value.shouldShowLoading).isEqualTo(false) + assertThat(vm.uiState.value.shouldShowError).isEqualTo(true) } softly.assertAll() } @Test - fun `티켓 받아오는 중이면 로딩 상태다`() { + fun `티켓만 받아오면 성공해도 로딩 상태다`() = runTest { // given - coEvery { - ticketRepository.loadTicket(any()) - } coAnswers { - delay(1000) - Result.success(TicketFixture.getMemberTicket()) + `티켓 요쳥 결과는 다음과 같을 때`(Result.success(TicketFixture.getMemberTicket())) + + // when + vm.loadTicket(1L) + + // then + val softly = SoftAssertions().apply { + assertThat(vm.uiState.value).isInstanceOf(TicketEntryUiState.Loading::class.java) + + // and + assertThat(vm.uiState.value.shouldShowSuccess).isEqualTo(false) + assertThat(vm.uiState.value.shouldShowLoading).isEqualTo(true) + assertThat(vm.uiState.value.shouldShowError).isEqualTo(false) } + softly.assertAll() + } + + @Test + fun `티켓만 받으면 실패해도 로딩 상태다`() { + // given + `티켓 요쳥 결과는 다음과 같을 때`(Result.failure(Exception())) // when vm.loadTicket(1L) @@ -120,33 +151,49 @@ class TicketEntryViewModelTest { assertThat(vm.uiState.value).isInstanceOf(TicketEntryUiState.Loading::class.java) // and - assertThat(vm.uiState.value?.shouldShowSuccess).isEqualTo(false) - assertThat(vm.uiState.value?.shouldShowLoading).isEqualTo(true) - assertThat(vm.uiState.value?.shouldShowError).isEqualTo(false) + assertThat(vm.uiState.value.shouldShowSuccess).isEqualTo(false) + assertThat(vm.uiState.value.shouldShowLoading).isEqualTo(true) + assertThat(vm.uiState.value.shouldShowError).isEqualTo(false) } softly.assertAll() } @Test - fun `티켓 코드를 받아오기에 실패하면 에러 상태다`() { + fun `티켓코드만 받아오면 결과가 성공해도 로딩 상태다`() { // given - coEvery { - ticketRepository.loadTicketCode(any()) - } answers { - Result.failure(Exception()) + `티켓 코드 요청 결과는 다음과 같을 때`(Result.success(getFakeTicketCode())) + + // when + vm.loadTicketCode(1L) + + // then + val softly = SoftAssertions().apply { + assertThat(vm.uiState.value).isInstanceOf(TicketEntryUiState.Loading::class.java) + + // and + assertThat(vm.uiState.value.shouldShowSuccess).isEqualTo(false) + assertThat(vm.uiState.value.shouldShowLoading).isEqualTo(true) + assertThat(vm.uiState.value.shouldShowError).isEqualTo(false) } + softly.assertAll() + } + + @Test + fun `티켓 코드만 받으면 결과에 실패해도 로딩 상태다`() { + // given + `티켓 코드 요청 결과는 다음과 같을 때`(Result.failure(Exception())) // when vm.loadTicketCode(1L) // then val softly = SoftAssertions().apply { - assertThat(vm.uiState.value).isInstanceOf(TicketEntryUiState.Error::class.java) + assertThat(vm.uiState.value).isInstanceOf(TicketEntryUiState.Loading::class.java) // and - assertThat(vm.uiState.value?.shouldShowSuccess).isEqualTo(false) - assertThat(vm.uiState.value?.shouldShowLoading).isEqualTo(false) - assertThat(vm.uiState.value?.shouldShowError).isEqualTo(true) + assertThat(vm.uiState.value.shouldShowSuccess).isEqualTo(false) + assertThat(vm.uiState.value.shouldShowLoading).isEqualTo(true) + assertThat(vm.uiState.value.shouldShowError).isEqualTo(false) } softly.assertAll() }