Updated on 2026-08-14
This commit is contained in:
commit
824f96fb12
6 changed files with 471 additions and 3 deletions
|
|
@ -17,6 +17,10 @@ sealed class AddressBookEvents(
|
|||
) : AnalyticsEvent(ADDRESS_BOOK_CATEGORY, event, params) {
|
||||
|
||||
// region Contact creation
|
||||
data object SaveToButtonClicked : AddressBookEvents(event = "Button - Save To")
|
||||
|
||||
data object AddressScreenOpened : AddressBookEvents(event = "Address Screen Opened")
|
||||
|
||||
class ContactListScreenOpened(
|
||||
walletId: UserWalletId,
|
||||
source: Source,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,112 @@
|
|||
package com.tangem.features.addressbook.common
|
||||
|
||||
import com.tangem.common.routing.entity.AddressBookOpenMode
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.domain.addressbook.error.AddressBookSyncError
|
||||
import com.tangem.domain.addressbook.error.SaveContactError
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.features.addressbook.analytics.AddressBookEvents
|
||||
import com.tangem.features.addressbook.analytics.AddressBookEvents.ContactListScreenOpened.Source
|
||||
import com.tangem.features.addressbook.analytics.AddressBookEvents.SaveErrorShown.ErrorType
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
internal class AddressBookAnalyticsSender @Inject constructor(
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
) {
|
||||
|
||||
fun sendContactListScreenOpened(mode: AddressBookOpenMode, scope: CoroutineScope) {
|
||||
scope.launch {
|
||||
analyticsEventHandler.send(
|
||||
AddressBookEvents.ContactListScreenOpened(
|
||||
walletId = selectedWalletId(),
|
||||
source = mode.toAnalyticsSource(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun sendAddContactTapped(fromSendSuccess: Boolean, scope: CoroutineScope) {
|
||||
scope.launch {
|
||||
analyticsEventHandler.send(
|
||||
AddressBookEvents.AddContactTapped(
|
||||
walletId = selectedWalletId(),
|
||||
source = if (fromSendSuccess) {
|
||||
AddressBookEvents.AddContactTapped.Source.SendSuccess
|
||||
} else {
|
||||
AddressBookEvents.AddContactTapped.Source.Settings
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun sendContactSaved(walletId: UserWalletId, contactId: String, isEdit: Boolean) {
|
||||
analyticsEventHandler.send(
|
||||
AddressBookEvents.ContactSaved(
|
||||
walletId = walletId,
|
||||
contactId = contactId,
|
||||
mode = if (isEdit) {
|
||||
AddressBookEvents.ContactSaved.Mode.Edit
|
||||
} else {
|
||||
AddressBookEvents.ContactSaved.Mode.Create
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fired when a save failure is surfaced to the user. [contactId] is set only in edit mode. Validation failures
|
||||
* ([SaveContactError.Name]/[SaveContactError.Address]) are shown inline rather than as a save error, so they do
|
||||
* not produce this event.
|
||||
*/
|
||||
fun sendSaveErrorShown(walletId: UserWalletId, contactId: String?, error: SaveContactError) {
|
||||
val errorType = error.toErrorType() ?: return
|
||||
analyticsEventHandler.send(
|
||||
AddressBookEvents.SaveErrorShown(
|
||||
walletId = walletId,
|
||||
contactId = contactId,
|
||||
errorType = errorType,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun SaveContactError.toErrorType(): ErrorType? = when (this) {
|
||||
is SaveContactError.Signing -> ErrorType.Signing
|
||||
// Network means the backend was unreachable; every other backend outcome (5xx, 412, other codes) is Server.
|
||||
is SaveContactError.Backend -> when (error) {
|
||||
AddressBookSyncError.Network -> ErrorType.Network
|
||||
else -> ErrorType.Server
|
||||
}
|
||||
is SaveContactError.Name,
|
||||
is SaveContactError.Address,
|
||||
-> null
|
||||
}
|
||||
|
||||
fun sendSaveToButtonClicked() {
|
||||
analyticsEventHandler.send(AddressBookEvents.SaveToButtonClicked)
|
||||
}
|
||||
|
||||
fun sendAddressScreenOpened() {
|
||||
analyticsEventHandler.send(AddressBookEvents.AddressScreenOpened)
|
||||
}
|
||||
|
||||
private suspend fun selectedWalletId(): UserWalletId = userWalletsListRepository.selectedUserWallet
|
||||
.filterNotNull()
|
||||
.first()
|
||||
.walletId
|
||||
|
||||
private fun AddressBookOpenMode.toAnalyticsSource(): Source = when (this) {
|
||||
AddressBookOpenMode.Default -> Source.Settings
|
||||
is AddressBookOpenMode.ContactSelection,
|
||||
is AddressBookOpenMode.WithContactCreation,
|
||||
-> Source.SendFlow
|
||||
}
|
||||
}
|
||||
|
|
@ -30,6 +30,7 @@ internal class DefaultAddressBookComponent @AssistedInject constructor(
|
|||
private val childFactory: AddressBookChildFactory,
|
||||
private val resultHolder: AddressBookResultHolder,
|
||||
private val selectNetworksResultHolder: SelectNetworksResultHolder,
|
||||
private val analyticsSender: AddressBookAnalyticsSender,
|
||||
) : AddressBookComponent, AppComponentContext by context {
|
||||
|
||||
private val navigation = StackNavigation<AddressBookRoute>()
|
||||
|
|
@ -38,6 +39,8 @@ internal class DefaultAddressBookComponent @AssistedInject constructor(
|
|||
// Drop any results left over from a previous session before the (possibly preloaded) stack starts collecting.
|
||||
resultHolder.clear()
|
||||
selectNetworksResultHolder.clear()
|
||||
|
||||
analyticsSender.sendContactListScreenOpened(mode = params.addressBookOpenMode, scope = componentScope)
|
||||
}
|
||||
|
||||
private val clickIntents = object : AddressBookClickIntents {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import com.tangem.domain.models.account.CryptoPortfolioIcon
|
|||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.models.wallet.isLocked
|
||||
import com.tangem.features.addressbook.common.AddressBookAnalyticsSender
|
||||
import com.tangem.features.addressbook.common.AddressBookResultHolder
|
||||
import com.tangem.features.addressbook.editcontact.DefaultEditContactComponent
|
||||
import com.tangem.features.addressbook.editcontact.state.EditContactStateController
|
||||
|
|
@ -50,6 +51,7 @@ internal class EditContactModel @Inject constructor(
|
|||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val validateContactNameUseCase: ValidateContactNameUseCase,
|
||||
private val saveContactInteractor: SaveContactInteractor,
|
||||
private val analyticsSender: AddressBookAnalyticsSender,
|
||||
val portfolioSelectorController: PortfolioSelectorController,
|
||||
portfolioFetcherFactory: PortfolioFetcher.Factory,
|
||||
) : Model() {
|
||||
|
|
@ -86,6 +88,12 @@ internal class EditContactModel @Inject constructor(
|
|||
observeWalletBlock()
|
||||
observeNameValidation()
|
||||
observeSaveButton()
|
||||
sendAddContactTappedEvent()
|
||||
}
|
||||
|
||||
private fun sendAddContactTappedEvent() {
|
||||
if (params.contactId != null) return
|
||||
analyticsSender.sendAddContactTapped(fromSendSuccess = params.predefinedAddress != null, scope = modelScope)
|
||||
}
|
||||
|
||||
/** In WithContactCreation mode the contact opens with the already-known address attached. */
|
||||
|
|
@ -151,6 +159,7 @@ internal class EditContactModel @Inject constructor(
|
|||
|
||||
private fun onWalletBlockClick() {
|
||||
if (isWalletChangeable(userWalletsListRepository.userWallets.value)) {
|
||||
analyticsSender.sendSaveToButtonClicked()
|
||||
portfolioSelectorNavigation.activate(Unit)
|
||||
}
|
||||
}
|
||||
|
|
@ -214,8 +223,22 @@ internal class EditContactModel @Inject constructor(
|
|||
addressEntries = addressEntries,
|
||||
)
|
||||
result.fold(
|
||||
ifLeft = ::handleSaveError,
|
||||
ifRight = { params.onBackClick() },
|
||||
ifLeft = { error ->
|
||||
handleSaveError(error)
|
||||
analyticsSender.sendSaveErrorShown(
|
||||
walletId = userWallet.walletId,
|
||||
contactId = params.contactId?.value,
|
||||
error = error,
|
||||
)
|
||||
},
|
||||
ifRight = { contact ->
|
||||
analyticsSender.sendContactSaved(
|
||||
walletId = userWallet.walletId,
|
||||
contactId = contact.id.value,
|
||||
isEdit = params.contactId != null,
|
||||
)
|
||||
params.onBackClick()
|
||||
},
|
||||
)
|
||||
} finally {
|
||||
refreshSaveButton()
|
||||
|
|
@ -247,6 +270,7 @@ internal class EditContactModel @Inject constructor(
|
|||
),
|
||||
)
|
||||
} else {
|
||||
analyticsSender.sendAddressScreenOpened()
|
||||
params.onAddAddressClick()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,182 @@
|
|||
package com.tangem.features.addressbook.common
|
||||
|
||||
import com.tangem.common.routing.entity.AddressBookOpenMode
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.domain.addressbook.error.AddressBookSyncError
|
||||
import com.tangem.domain.addressbook.error.SaveContactError
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.features.addressbook.analytics.AddressBookEvents
|
||||
import com.tangem.features.addressbook.analytics.AddressBookEvents.AddContactTapped
|
||||
import com.tangem.features.addressbook.analytics.AddressBookEvents.ContactListScreenOpened.Source
|
||||
import com.tangem.features.addressbook.analytics.AddressBookEvents.ContactSaved
|
||||
import com.tangem.features.addressbook.analytics.AddressBookEvents.SaveErrorShown.ErrorType
|
||||
import com.tangem.test.core.ProvideTestModels
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.junit.jupiter.params.provider.MethodSource
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class AddressBookAnalyticsSenderTest {
|
||||
|
||||
private val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxed = true)
|
||||
private val userWalletsListRepository: UserWalletsListRepository = mockk()
|
||||
|
||||
private val sender = AddressBookAnalyticsSender(
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
userWalletsListRepository = userWalletsListRepository,
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
clearMocks(analyticsEventHandler)
|
||||
val wallet = mockk<UserWallet> { every { walletId } returns EXPECTED_WALLET_ID }
|
||||
every { userWalletsListRepository.selectedUserWallet } returns MutableStateFlow(wallet)
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun sendContactListScreenOpened(model: ScreenOpenedModel) = runTest {
|
||||
// Act
|
||||
sender.sendContactListScreenOpened(mode = model.mode, scope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
val expected = AddressBookEvents.ContactListScreenOpened(
|
||||
walletId = EXPECTED_WALLET_ID,
|
||||
source = model.expectedSource,
|
||||
)
|
||||
verify(exactly = 1) { analyticsEventHandler.send(expected) }
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("provideAddContactModels")
|
||||
fun sendAddContactTapped(model: AddContactModel) = runTest {
|
||||
// Act
|
||||
sender.sendAddContactTapped(fromSendSuccess = model.fromSendSuccess, scope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
val expected = AddressBookEvents.AddContactTapped(
|
||||
walletId = EXPECTED_WALLET_ID,
|
||||
source = model.expectedSource,
|
||||
)
|
||||
verify(exactly = 1) { analyticsEventHandler.send(expected) }
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("provideContactSavedModels")
|
||||
fun sendContactSaved(model: ContactSavedModel) = runTest {
|
||||
// Act
|
||||
sender.sendContactSaved(walletId = EXPECTED_WALLET_ID, contactId = CONTACT_ID, isEdit = model.isEdit)
|
||||
|
||||
// Assert
|
||||
val expected = AddressBookEvents.ContactSaved(
|
||||
walletId = EXPECTED_WALLET_ID,
|
||||
contactId = CONTACT_ID,
|
||||
mode = model.expectedMode,
|
||||
)
|
||||
verify(exactly = 1) { analyticsEventHandler.send(expected) }
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("provideSaveErrorModels")
|
||||
fun sendSaveErrorShown(model: SaveErrorModel) = runTest {
|
||||
// Act
|
||||
sender.sendSaveErrorShown(walletId = EXPECTED_WALLET_ID, contactId = CONTACT_ID, error = model.error)
|
||||
|
||||
// Assert
|
||||
val expectedType = model.expectedType
|
||||
if (expectedType == null) {
|
||||
verify(exactly = 0) { analyticsEventHandler.send(any()) }
|
||||
} else {
|
||||
val expected = AddressBookEvents.SaveErrorShown(
|
||||
walletId = EXPECTED_WALLET_ID,
|
||||
contactId = CONTACT_ID,
|
||||
errorType = expectedType,
|
||||
)
|
||||
verify(exactly = 1) { analyticsEventHandler.send(expected) }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN sendSaveToButtonClicked THEN event sent`() {
|
||||
// Act
|
||||
sender.sendSaveToButtonClicked()
|
||||
|
||||
// Assert
|
||||
verify(exactly = 1) { analyticsEventHandler.send(AddressBookEvents.SaveToButtonClicked) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN sendAddressScreenOpened THEN event sent`() {
|
||||
// Act
|
||||
sender.sendAddressScreenOpened()
|
||||
|
||||
// Assert
|
||||
verify(exactly = 1) { analyticsEventHandler.send(AddressBookEvents.AddressScreenOpened) }
|
||||
}
|
||||
|
||||
internal data class ScreenOpenedModel(val mode: AddressBookOpenMode, val expectedSource: Source)
|
||||
|
||||
internal data class AddContactModel(val fromSendSuccess: Boolean, val expectedSource: AddContactTapped.Source)
|
||||
|
||||
internal data class ContactSavedModel(val isEdit: Boolean, val expectedMode: ContactSaved.Mode)
|
||||
|
||||
internal data class SaveErrorModel(val error: SaveContactError, val expectedType: ErrorType?)
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
ScreenOpenedModel(mode = AddressBookOpenMode.Default, expectedSource = Source.Settings),
|
||||
ScreenOpenedModel(
|
||||
mode = AddressBookOpenMode.ContactSelection(networkId = "ethereum"),
|
||||
expectedSource = Source.SendFlow,
|
||||
),
|
||||
ScreenOpenedModel(
|
||||
mode = AddressBookOpenMode.WithContactCreation(address = "0xABC", networkId = "ethereum"),
|
||||
expectedSource = Source.SendFlow,
|
||||
),
|
||||
)
|
||||
|
||||
private fun provideAddContactModels() = listOf(
|
||||
AddContactModel(fromSendSuccess = false, expectedSource = AddContactTapped.Source.Settings),
|
||||
AddContactModel(fromSendSuccess = true, expectedSource = AddContactTapped.Source.SendSuccess),
|
||||
)
|
||||
|
||||
private fun provideContactSavedModels() = listOf(
|
||||
ContactSavedModel(isEdit = false, expectedMode = ContactSaved.Mode.Create),
|
||||
ContactSavedModel(isEdit = true, expectedMode = ContactSaved.Mode.Edit),
|
||||
)
|
||||
|
||||
private fun provideSaveErrorModels() = listOf(
|
||||
SaveErrorModel(error = SaveContactError.Signing(mockk()), expectedType = ErrorType.Signing),
|
||||
SaveErrorModel(error = SaveContactError.Backend(AddressBookSyncError.Network), expectedType = ErrorType.Network),
|
||||
// 412
|
||||
SaveErrorModel(error = SaveContactError.Backend(AddressBookSyncError.Conflict), expectedType = ErrorType.Server),
|
||||
// 5xx / unmapped
|
||||
SaveErrorModel(error = SaveContactError.Backend(AddressBookSyncError.Unknown), expectedType = ErrorType.Server),
|
||||
SaveErrorModel(
|
||||
error = SaveContactError.Backend(AddressBookSyncError.BadRequest),
|
||||
expectedType = ErrorType.Server,
|
||||
),
|
||||
// Validation failures are shown inline, not as a save error.
|
||||
SaveErrorModel(error = SaveContactError.Name(mockk()), expectedType = null),
|
||||
SaveErrorModel(error = SaveContactError.Address(mockk()), expectedType = null),
|
||||
)
|
||||
|
||||
private companion object {
|
||||
val EXPECTED_WALLET_ID = UserWalletId("0011223344")
|
||||
const val CONTACT_ID = "contact-42"
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ import com.tangem.core.decompose.ui.UiMessageSender
|
|||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.domain.addressbook.error.AddressBookSyncError
|
||||
import com.tangem.domain.addressbook.error.ContactNameValidationError
|
||||
import com.tangem.domain.addressbook.error.SaveContactError
|
||||
import com.tangem.domain.addressbook.interactor.SaveContactInteractor
|
||||
|
|
@ -23,6 +24,7 @@ import com.tangem.domain.models.account.AccountStatus
|
|||
import com.tangem.domain.models.account.CryptoPortfolioIcon
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.features.addressbook.common.AddressBookAnalyticsSender
|
||||
import com.tangem.features.addressbook.common.AddressBookResultHolder
|
||||
import com.tangem.features.addressbook.editcontact.DefaultEditContactComponent
|
||||
import com.tangem.features.addressbook.editcontact.state.EditContactStateController
|
||||
|
|
@ -60,6 +62,7 @@ internal class EditContactModelTest {
|
|||
private val portfolioSelectorController: PortfolioSelectorController = mockk()
|
||||
private val portfolioFetcher: PortfolioFetcher = mockk(relaxed = true)
|
||||
private val portfolioFetcherFactory: PortfolioFetcher.Factory = mockk()
|
||||
private val analyticsSender: AddressBookAnalyticsSender = mockk(relaxed = true)
|
||||
|
||||
// Drives the wallet picked in the reused portfolio selector; `first` of the pair is the chosen wallet.
|
||||
private val selectedWalletData =
|
||||
|
|
@ -121,6 +124,41 @@ internal class EditContactModelTest {
|
|||
assertThat(state).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN new contact without predefined address WHEN created THEN AddContactTapped sent from settings`() =
|
||||
runTest {
|
||||
// Act
|
||||
createModel(testScope = this, params = createParams(contactId = null, predefinedAddress = null))
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
verify(exactly = 1) { analyticsSender.sendAddContactTapped(fromSendSuccess = false, scope = any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN new contact with predefined address WHEN created THEN AddContactTapped sent from send success`() =
|
||||
runTest {
|
||||
// Arrange
|
||||
val predefined = ValidatedAddress(address = "0xABC", networkIds = persistentListOf("ethereum"))
|
||||
|
||||
// Act
|
||||
createModel(testScope = this, params = createParams(predefinedAddress = predefined))
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
verify(exactly = 1) { analyticsSender.sendAddContactTapped(fromSendSuccess = true, scope = any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN existing contactId WHEN model created THEN AddContactTapped not sent`() = runTest {
|
||||
// Act
|
||||
createModel(testScope = this, params = createParams(contactId = ContactId(value = "contact-id")))
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
verify(exactly = 0) { analyticsSender.sendAddContactTapped(any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN existing contactId WHEN model created THEN title is contact`() = runTest {
|
||||
// Arrange
|
||||
|
|
@ -216,6 +254,7 @@ internal class EditContactModelTest {
|
|||
// Assert
|
||||
assertThat(addClicked).isTrue()
|
||||
verify(exactly = 0) { messageSender.send(any<DialogMessage>()) }
|
||||
verify(exactly = 1) { analyticsSender.sendAddressScreenOpened() }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -240,8 +279,40 @@ internal class EditContactModelTest {
|
|||
assertThat(model.state.value.isAddAddressEnabled).isFalse()
|
||||
assertThat(addClicked).isFalse()
|
||||
verify { messageSender.send(any<DialogMessage>()) }
|
||||
verify(exactly = 0) { analyticsSender.sendAddressScreenOpened() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN changeable wallet block WHEN clicked THEN SaveToButtonClicked sent`() = runTest {
|
||||
// Arrange
|
||||
val walletA = createWallet(id = "aa", name = "Wallet A")
|
||||
val walletB = createWallet(id = "bb", name = "Wallet B")
|
||||
setupWallets(wallets = listOf(walletA, walletB), selected = walletA)
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Act
|
||||
model.state.value.walletBlock.onClick()
|
||||
|
||||
// Assert
|
||||
verify(exactly = 1) { analyticsSender.sendSaveToButtonClicked() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN non-changeable wallet block WHEN clicked THEN SaveToButtonClicked not sent`() = runTest {
|
||||
// Arrange
|
||||
val walletA = createWallet(id = "aa", name = "Wallet A")
|
||||
setupWallets(wallets = listOf(walletA), selected = walletA)
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Act
|
||||
model.state.value.walletBlock.onClick()
|
||||
|
||||
// Assert
|
||||
verify(exactly = 0) { analyticsSender.sendSaveToButtonClicked() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN new contact AND multiple unlocked wallets WHEN created THEN wallet block changeable`() = runTest {
|
||||
// Arrange
|
||||
|
|
@ -391,7 +462,8 @@ internal class EditContactModelTest {
|
|||
var navigatedBack = false
|
||||
val walletA = createWallet(id = "aa", name = "Wallet A")
|
||||
setupWallets(wallets = listOf(walletA), selected = walletA)
|
||||
coEvery { saveContactInteractor.createContact(any(), any(), any(), any()) } returns mockk<Contact>().right()
|
||||
val savedContact = mockk<Contact> { every { id } returns ContactId(value = "saved-1") }
|
||||
coEvery { saveContactInteractor.createContact(any(), any(), any(), any()) } returns savedContact.right()
|
||||
val model = createModel(testScope = this, params = createParams(onBackClick = { navigatedBack = true }))
|
||||
advanceUntilIdle()
|
||||
model.state.value.onNameChange("Satoshi")
|
||||
|
|
@ -411,9 +483,79 @@ internal class EditContactModelTest {
|
|||
addressEntries = any(),
|
||||
)
|
||||
}
|
||||
verify(exactly = 1) {
|
||||
analyticsSender.sendContactSaved(walletId = walletA.walletId, contactId = "saved-1", isEdit = false)
|
||||
}
|
||||
assertThat(navigatedBack).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN save fails WHEN save clicked THEN ContactSaved not sent`() = runTest {
|
||||
// Arrange
|
||||
val walletA = createWallet(id = "aa", name = "Wallet A")
|
||||
setupWallets(wallets = listOf(walletA), selected = walletA)
|
||||
coEvery { saveContactInteractor.createContact(any(), any(), any(), any()) } returns
|
||||
SaveContactError.Name(ContactNameValidationError.Duplicate).left()
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
model.state.value.onNameChange("Satoshi")
|
||||
resultHolder.setConfirmedAddress(ValidatedAddress(address = "0xABC", networkIds = persistentListOf("ethereum")))
|
||||
advanceUntilIdle()
|
||||
|
||||
// Act
|
||||
model.state.value.saveButton.onClick()
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
verify(exactly = 0) { analyticsSender.sendContactSaved(any(), any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN new contact AND save fails WHEN save clicked THEN SaveErrorShown sent with null contactId`() = runTest {
|
||||
// Arrange
|
||||
val walletA = createWallet(id = "aa", name = "Wallet A")
|
||||
setupWallets(wallets = listOf(walletA), selected = walletA)
|
||||
val error = SaveContactError.Backend(AddressBookSyncError.Network)
|
||||
coEvery { saveContactInteractor.createContact(any(), any(), any(), any()) } returns error.left()
|
||||
val model = createModel(testScope = this, params = createParams(contactId = null))
|
||||
advanceUntilIdle()
|
||||
model.state.value.onNameChange("Satoshi")
|
||||
resultHolder.setConfirmedAddress(ValidatedAddress(address = "0xABC", networkIds = persistentListOf("ethereum")))
|
||||
advanceUntilIdle()
|
||||
|
||||
// Act
|
||||
model.state.value.saveButton.onClick()
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
verify(exactly = 1) {
|
||||
analyticsSender.sendSaveErrorShown(walletId = walletA.walletId, contactId = null, error = error)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN existing contact AND save fails WHEN save clicked THEN SaveErrorShown sent with contactId`() = runTest {
|
||||
// Arrange
|
||||
val walletA = createWallet(id = "aa", name = "Wallet A")
|
||||
setupWallets(wallets = listOf(walletA), selected = walletA)
|
||||
val error = SaveContactError.Backend(AddressBookSyncError.Network)
|
||||
coEvery { saveContactInteractor.createContact(any(), any(), any(), any()) } returns error.left()
|
||||
val model = createModel(testScope = this, params = createParams(contactId = ContactId(value = "contact-id")))
|
||||
advanceUntilIdle()
|
||||
model.state.value.onNameChange("Satoshi")
|
||||
resultHolder.setConfirmedAddress(ValidatedAddress(address = "0xABC", networkIds = persistentListOf("ethereum")))
|
||||
advanceUntilIdle()
|
||||
|
||||
// Act
|
||||
model.state.value.saveButton.onClick()
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
verify(exactly = 1) {
|
||||
analyticsSender.sendSaveErrorShown(walletId = walletA.walletId, contactId = "contact-id", error = error)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN save returns name error WHEN save clicked THEN inline name error shown`() = runTest {
|
||||
// Arrange
|
||||
|
|
@ -462,6 +604,7 @@ internal class EditContactModelTest {
|
|||
userWalletsListRepository = userWalletsListRepository,
|
||||
validateContactNameUseCase = validateContactNameUseCase,
|
||||
saveContactInteractor = saveContactInteractor,
|
||||
analyticsSender = analyticsSender,
|
||||
portfolioSelectorController = portfolioSelectorController,
|
||||
portfolioFetcherFactory = portfolioFetcherFactory,
|
||||
).also { model = it }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue