From 01b7433542bae83c2660b2b48d2249b17b716266 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 25 Jun 2026 10:25:19 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../model/TangemPayEditDisplayNameModel.kt | 5 +- .../TangemPayEditDisplayNameModelTest.kt | 167 ++++++++++++++++++ 2 files changed, 169 insertions(+), 3 deletions(-) create mode 100644 features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/model/TangemPayEditDisplayNameModelTest.kt diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayEditDisplayNameModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayEditDisplayNameModel.kt index b9d33268ec..f0419494f2 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayEditDisplayNameModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayEditDisplayNameModel.kt @@ -107,9 +107,8 @@ internal class TangemPayEditDisplayNameModel @Inject constructor( } private fun onValueChanged(value: TextFieldValue) { - val displayName = CardDisplayName(value.text) - val isAvailableForConfirm = displayName.isRight() - uiState.update { it.copy(editingValue = value, isDoneEnabled = isAvailableForConfirm) } + if (value.text.length > CardDisplayName.MAX_LENGTH) return + uiState.update { it.copy(editingValue = value, isDoneEnabled = value.text.isNotBlank()) } } private fun onDoneClick() { diff --git a/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/model/TangemPayEditDisplayNameModelTest.kt b/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/model/TangemPayEditDisplayNameModelTest.kt new file mode 100644 index 0000000000..9cf134e92b --- /dev/null +++ b/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/model/TangemPayEditDisplayNameModelTest.kt @@ -0,0 +1,167 @@ +package com.tangem.features.tangempay.model + +import androidx.compose.ui.text.input.TextFieldValue +import arrow.core.right +import com.google.common.truth.Truth.assertThat +import com.tangem.core.decompose.model.MutableParamsContainer +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.message.DialogMessage +import com.tangem.domain.models.account.CardDisplayName +import com.tangem.domain.models.pay.TangemPayCard +import com.tangem.domain.models.pay.TangemPayCardFrozenState +import com.tangem.domain.models.pay.TangemPayCardState +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier +import com.tangem.domain.pay.usecase.UpdateTangemPayCardNameUseCase +import com.tangem.features.tangempay.TangemPayFeatureToggles +import com.tangem.features.tangempay.components.TangemPayEditDisplayNameComponent +import com.tangem.features.tangempay.model.controller.TangemPayCardDetailsController +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.flow.emptyFlow +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test + +internal class TangemPayEditDisplayNameModelTest { + + private val cardId = "card_1" + private val userWalletId = UserWalletId("123") + + private val router: Router = mockk(relaxed = true) + private val updateCardNameUseCase: UpdateTangemPayCardNameUseCase = mockk() + private val uiMessageSender: UiMessageSender = mockk(relaxed = true) + private val paymentAccountStatusSupplier: PaymentAccountStatusSupplier = mockk() + private val featureToggles: TangemPayFeatureToggles = mockk(relaxed = true) + private val cardDetailsControllerFactory: TangemPayCardDetailsController.Factory = mockk(relaxed = true) + + init { + every { paymentAccountStatusSupplier.invoke(any()) } returns emptyFlow() + } + + private fun createModel(displayName: CardDisplayName? = null) = TangemPayEditDisplayNameModel( + paramsContainer = MutableParamsContainer( + TangemPayEditDisplayNameComponent.Params( + card = card(displayName = displayName), + userWalletId = userWalletId, + ), + ), + dispatchers = TestingCoroutineDispatcherProvider(), + router = router, + updateCardNameUseCase = updateCardNameUseCase, + uiMessageSender = uiMessageSender, + paymentAccountStatusSupplier = paymentAccountStatusSupplier, + featureToggles = featureToggles, + cardDetailsControllerFactory = cardDetailsControllerFactory, + ) + + @Nested + inner class OnValueChanged { + + // [REDACTED_TASK_KEY]: invalid-but-present input must NOT disable the button, otherwise the + // user can never press Done to see the "Invalid characters" alert. + @Test + fun `GIVEN emoji input WHEN onValueChanged THEN button stays enabled`() { + val model = createModel() + + model.uiState.value.onValueChanged(TextFieldValue("Card 😀")) + + assertThat(model.uiState.value.isDoneEnabled).isTrue() + } + + @Test + fun `GIVEN special characters input WHEN onValueChanged THEN button stays enabled`() { + val model = createModel() + + model.uiState.value.onValueChanged(TextFieldValue("Card #1!")) + + assertThat(model.uiState.value.isDoneEnabled).isTrue() + } + + @Test + fun `GIVEN valid input WHEN onValueChanged THEN button enabled`() { + val model = createModel() + + model.uiState.value.onValueChanged(TextFieldValue("My Card")) + + assertThat(model.uiState.value.isDoneEnabled).isTrue() + } + + @Test + fun `GIVEN blank input WHEN onValueChanged THEN button disabled`() { + val model = createModel() + + model.uiState.value.onValueChanged(TextFieldValue(" ")) + + assertThat(model.uiState.value.isDoneEnabled).isFalse() + } + + @Test + fun `GIVEN input longer than max length WHEN onValueChanged THEN change is ignored`() { + val model = createModel() + val maxLengthText = "a".repeat(CardDisplayName.MAX_LENGTH) + model.uiState.value.onValueChanged(TextFieldValue(maxLengthText)) + + model.uiState.value.onValueChanged(TextFieldValue(maxLengthText + "b")) + + assertThat(model.uiState.value.editingValue.text).isEqualTo(maxLengthText) + } + } + + @Nested + inner class OnDoneClick { + + // [REDACTED_TASK_KEY]: pressing Done on an invalid name surfaces the "Invalid characters" alert + // and does not persist the name. + @Test + fun `GIVEN invalid name WHEN onDoneClick THEN error dialog shown and name not updated`() { + val model = createModel() + model.uiState.value.onValueChanged(TextFieldValue("Card 😀")) + + model.uiState.value.onDoneClick() + + verify(exactly = 1) { uiMessageSender.send(any()) } + coVerify(exactly = 0) { updateCardNameUseCase(any(), any(), any()) } + } + + @Test + fun `GIVEN valid changed name WHEN onDoneClick THEN name updated`() { + coEvery { updateCardNameUseCase(any(), any(), any()) } returns Unit.right() + val model = createModel() + model.uiState.value.onValueChanged(TextFieldValue("New Name")) + + model.uiState.value.onDoneClick() + + coVerify(exactly = 1) { + updateCardNameUseCase(cardId, userWalletId, CardDisplayName("New Name").getOrNull()!!) + } + verify(exactly = 0) { uiMessageSender.send(any()) } + } + + @Test + fun `GIVEN unchanged name WHEN onDoneClick THEN screen closed without update`() { + val model = createModel(displayName = CardDisplayName("My Card").getOrNull()) + + model.uiState.value.onDoneClick() + + verify(exactly = 1) { router.pop() } + coVerify(exactly = 0) { updateCardNameUseCase(any(), any(), any()) } + } + } + + private fun card(displayName: CardDisplayName? = null) = TangemPayCard( + id = cardId, + productInstanceId = "product", + cardStatus = TangemPayCard.Status.ACTIVE, + hasPinCode = true, + displayName = displayName, + limit = null, + frozenState = TangemPayCardFrozenState.Unfrozen, + lastDigits = "1234", + state = TangemPayCardState.Active, + ) +} \ No newline at end of file