diff --git a/app/data/data-changetier/src/main/kotlin/com/hedvig/android/data/changetier/data/CreateChangeTierDeductibleIntentUseCase.kt b/app/data/data-changetier/src/main/kotlin/com/hedvig/android/data/changetier/data/CreateChangeTierDeductibleIntentUseCase.kt index dfe5e3e730..f23600a1e1 100644 --- a/app/data/data-changetier/src/main/kotlin/com/hedvig/android/data/changetier/data/CreateChangeTierDeductibleIntentUseCase.kt +++ b/app/data/data-changetier/src/main/kotlin/com/hedvig/android/data/changetier/data/CreateChangeTierDeductibleIntentUseCase.kt @@ -137,11 +137,11 @@ private fun DeductibleFragment.toDeductible(): Deductible { } private fun List.toDisplayItems(): List { - return this.map { + return this.map { displayItemFragment -> ChangeTierDeductibleDisplayItem( - displayTitle = it.displayTitle, - displaySubtitle = it.displaySubtitle, - displayValue = it.displayValue, + displayTitle = displayItemFragment.displayTitle, + displaySubtitle = displayItemFragment.displaySubtitle, + displayValue = displayItemFragment.displayValue, ) } } diff --git a/app/data/data-changetier/src/test/kotlin/data/CreateChangeTierDeductibleIntentUseCaseImplTest.kt b/app/data/data-changetier/src/test/kotlin/data/CreateChangeTierDeductibleIntentUseCaseImplTest.kt index da708a715d..10e1c2ad65 100644 --- a/app/data/data-changetier/src/test/kotlin/data/CreateChangeTierDeductibleIntentUseCaseImplTest.kt +++ b/app/data/data-changetier/src/test/kotlin/data/CreateChangeTierDeductibleIntentUseCaseImplTest.kt @@ -1,5 +1,7 @@ package data +import assertk.assertions.first +import assertk.assertions.index import assertk.assertions.isEmpty import assertk.assertions.isEqualTo import assertk.assertions.isNotNull @@ -12,7 +14,9 @@ import com.hedvig.android.apollo.test.TestApolloClientRule import com.hedvig.android.apollo.test.TestNetworkTransportType import com.hedvig.android.core.common.test.isLeft import com.hedvig.android.core.common.test.isRight +import com.hedvig.android.core.uidata.UiMoney import com.hedvig.android.data.changetier.data.ChangeTierCreateSource +import com.hedvig.android.data.changetier.data.ChangeTierDeductibleDisplayItem import com.hedvig.android.data.changetier.data.ChangeTierDeductibleIntent import com.hedvig.android.data.changetier.data.CreateChangeTierDeductibleIntentUseCaseImpl import com.hedvig.android.data.changetier.data.TierConstants @@ -20,12 +24,16 @@ import com.hedvig.android.data.changetier.data.TierDeductibleQuote import com.hedvig.android.featureflags.flags.Feature import com.hedvig.android.featureflags.test.FakeFeatureManager2 import com.hedvig.android.logger.TestLogcatLoggingRule +import kotlin.collections.List import kotlinx.coroutines.test.runTest import kotlinx.datetime.LocalDate import octopus.ChangeTierDeductibleCreateIntentMutation import octopus.type.ChangeTierDeductibleSource.SELF_SERVICE import octopus.type.CurrencyCode.SEK +import octopus.type.buildAddonVariant +import octopus.type.buildChangeTierDeductibleAddonQuote import octopus.type.buildChangeTierDeductibleCreateIntentOutput +import octopus.type.buildChangeTierDeductibleDisplayItem import octopus.type.buildChangeTierDeductibleFromAgreement import octopus.type.buildChangeTierDeductibleIntent import octopus.type.buildChangeTierDeductibleQuote @@ -278,6 +286,78 @@ class CreateChangeTierDeductibleIntentUseCaseImplTest { @OptIn(ApolloExperimental::class) private val apolloClientWithGoodResponse: ApolloClient get() = testApolloClientRule.apolloClient.apply { + registerTestResponse( + operation = ChangeTierDeductibleCreateIntentMutation( + contractId = testId, + source = testSource, + addonsFlagOn = false, + ), + data = ChangeTierDeductibleCreateIntentMutation.Data(OctopusFakeResolver) { + changeTierDeductibleCreateIntent = buildChangeTierDeductibleCreateIntentOutput { + intent = buildChangeTierDeductibleIntent { + activationDate = activationDateNovember + agreementToChange = buildChangeTierDeductibleFromAgreement { + premium = buildMoney { + amount = 169.0 + currencyCode = SEK + } + deductible = buildDeductible { + displayText = "A very good deductible" + percentage = 0 + amount = buildMoney { + amount = 3000.0 + currencyCode = SEK + } + } + displayItems = listOf() + tierLevel = 1 + tierName = "STANDARD" + productVariant = buildProductVariant { + displayName = "Variant" + typeOfContract = "SE_APARTMENT_RENT" + partner = null + perils = listOf() + insurableLimits = listOf() + documents = listOf() + displayNameTier = "Standard" + tierDescription = "Our standard coverage" + } + } + quotes = List(1) { + buildChangeTierDeductibleQuote { + id = "id" + premium = buildMoney { + amount = 500.0 + currencyCode = SEK + } + deductible = buildDeductible { + displayText = "A very good deductible" + percentage = 0 + amount = buildMoney { + amount = 500.0 + currencyCode = SEK + } + } + displayItems = listOf() + tierLevel = 1 + tierName = "STANDARD" + productVariant = buildProductVariant { + displayName = "Variant" + typeOfContract = "SE_APARTMENT_RENT" + partner = null + perils = listOf() + insurableLimits = listOf() + documents = listOf() + displayNameTier = "Standard" + tierDescription = "Our standard coverage" + } + } + } + } + } + }, + ) + registerTestResponse( operation = ChangeTierDeductibleCreateIntentMutation( contractId = testId, @@ -333,6 +413,35 @@ class CreateChangeTierDeductibleIntentUseCaseImplTest { displayItems = listOf() tierLevel = 1 tierName = "STANDARD" + addons = List(1) { + buildChangeTierDeductibleAddonQuote { + addonId = "addonId" + displayName = "Travel Plus" + displayItems = List(1) { + buildChangeTierDeductibleDisplayItem { + displayTitle = "Coinsured people" + displaySubtitle = null + displayValue = "Only you" + } + } + previousPremium = buildMoney { + currencyCode = SEK + amount = 29.0 + } + premium = buildMoney { + currencyCode = SEK + amount = 30.0 + } + addonVariant = buildAddonVariant { + termsVersion = "terms" + displayName = "addonVariantDisplayName" + product = "product" + perils = emptyList() + insurableLimits = emptyList() + documents = emptyList() + } + } + } productVariant = buildProductVariant { displayName = "Variant" typeOfContract = "SE_APARTMENT_RENT" diff --git a/app/feature/feature-choose-tier/src/main/kotlin/com/hedvig/android/feature/change/tier/ui/stepsummary/SummaryViewModel.kt b/app/feature/feature-choose-tier/src/main/kotlin/com/hedvig/android/feature/change/tier/ui/stepsummary/SummaryViewModel.kt index 5cba384273..6ae9e42a7d 100644 --- a/app/feature/feature-choose-tier/src/main/kotlin/com/hedvig/android/feature/change/tier/ui/stepsummary/SummaryViewModel.kt +++ b/app/feature/feature-choose-tier/src/main/kotlin/com/hedvig/android/feature/change/tier/ui/stepsummary/SummaryViewModel.kt @@ -40,7 +40,7 @@ internal class SummaryViewModel( ), ) -private class SummaryPresenter( +internal class SummaryPresenter( private val params: SummaryParameters, private val tierRepository: ChangeTierRepository, private val getCurrentContractDataUseCase: GetCurrentContractDataUseCase, diff --git a/app/feature/feature-choose-tier/src/test/kotlin/CommonTestdata.kt b/app/feature/feature-choose-tier/src/test/kotlin/CommonTestdata.kt index d09990e461..9f17d49fa1 100644 --- a/app/feature/feature-choose-tier/src/test/kotlin/CommonTestdata.kt +++ b/app/feature/feature-choose-tier/src/test/kotlin/CommonTestdata.kt @@ -5,6 +5,7 @@ import com.hedvig.android.core.common.ErrorMessage import com.hedvig.android.core.uidata.UiCurrencyCode.SEK import com.hedvig.android.core.uidata.UiMoney import com.hedvig.android.data.changetier.data.ChangeTierCreateSource +import com.hedvig.android.data.changetier.data.ChangeTierDeductibleAddonQuote import com.hedvig.android.data.changetier.data.ChangeTierDeductibleDisplayItem import com.hedvig.android.data.changetier.data.ChangeTierDeductibleIntent import com.hedvig.android.data.changetier.data.ChangeTierRepository @@ -13,6 +14,7 @@ import com.hedvig.android.data.changetier.data.Tier import com.hedvig.android.data.changetier.data.TierDeductibleQuote import com.hedvig.android.data.contract.ContractGroup import com.hedvig.android.data.contract.ContractType +import com.hedvig.android.data.productvariant.AddonVariant import com.hedvig.android.data.productvariant.ProductVariant internal class FakeChangeTierRepository() : ChangeTierRepository { @@ -155,6 +157,133 @@ internal val testQuote3 = TierDeductibleQuote( addons = emptyList(), ) +internal val testQuoteWithOneAddon = TierDeductibleQuote( + id = "id3", + deductible = Deductible( + UiMoney(0.0, SEK), + deductiblePercentage = 25, + description = "Endast en rörlig del om 25% av skadekostnaden.", + ), + displayItems = listOf( + ChangeTierDeductibleDisplayItem( + displayValue = "hhh", + displaySubtitle = "mmm", + displayTitle = "ioi", + ), + ), + premium = UiMoney(205.0, SEK), + tier = standardTier, + productVariant = ProductVariant( + displayName = "Test", + contractGroup = ContractGroup.RENTAL, + contractType = ContractType.SE_APARTMENT_RENT, + partner = "test", + perils = listOf(), + insurableLimits = listOf(), + documents = listOf(), + displayTierName = "Bas", + tierDescription = "Our most basic coverage", + termsVersion = "termsVersion", + ), + addons = listOf( + ChangeTierDeductibleAddonQuote( + addonId = "addonId", + displayName = "Travel Plus", + displayItems = listOf( + ChangeTierDeductibleDisplayItem( + displayTitle = "Coinsured people", + displaySubtitle = null, + displayValue = "Only you", + ), + ), + previousPremium = UiMoney(29.0, SEK), + premium = UiMoney(30.0, SEK), + addonVariant = AddonVariant( + termsVersion = "terms", + displayName = "addonVariantDisplayName", + product = "product", + perils = emptyList(), + insurableLimits = emptyList(), + documents = emptyList(), + ), + ), + ), +) + +internal val testQuoteWithTwoAddons = TierDeductibleQuote( + id = "id3", + deductible = Deductible( + UiMoney(0.0, SEK), + deductiblePercentage = 25, + description = "Endast en rörlig del om 25% av skadekostnaden.", + ), + displayItems = listOf( + ChangeTierDeductibleDisplayItem( + displayValue = "hhh", + displaySubtitle = "mmm", + displayTitle = "ioi", + ), + ), + premium = UiMoney(205.0, SEK), + tier = standardTier, + productVariant = ProductVariant( + displayName = "Test", + contractGroup = ContractGroup.RENTAL, + contractType = ContractType.SE_APARTMENT_RENT, + partner = "test", + perils = listOf(), + insurableLimits = listOf(), + documents = listOf(), + displayTierName = "Bas", + tierDescription = "Our most basic coverage", + termsVersion = "termsVersion", + ), + addons = listOf( + ChangeTierDeductibleAddonQuote( + addonId = "addonId", + displayName = "Travel Plus", + displayItems = listOf( + ChangeTierDeductibleDisplayItem( + displayTitle = "Coinsured people", + displaySubtitle = null, + displayValue = "Only you", + ), + ), + previousPremium = UiMoney(29.0, SEK), + premium = UiMoney(30.0, SEK), + addonVariant = AddonVariant( + termsVersion = "terms", + displayName = "addonVariantDisplayName", + product = "product", + perils = emptyList(), + insurableLimits = emptyList(), + documents = emptyList(), + ), + ), + ChangeTierDeductibleAddonQuote( + addonId = "addonId2", + displayName = "Travel Plus", + displayItems = listOf( + ChangeTierDeductibleDisplayItem( + displayTitle = "Coinsured people", + displaySubtitle = null, + displayValue = "Only you", + ), + ), + previousPremium = UiMoney(70.0, SEK), + premium = UiMoney(80.0, SEK), + addonVariant = AddonVariant( + termsVersion = "terms", + displayName = "addonVariantDisplayName", + product = "product", + perils = emptyList(), + insurableLimits = emptyList(), + documents = emptyList(), + ), + ), + ), +) + internal val currentQuote = TierDeductibleQuote( id = CURRENT_ID, deductible = Deductible( diff --git a/app/feature/feature-choose-tier/src/test/kotlin/ui/stepsummary/SummaryPresenterTest.kt b/app/feature/feature-choose-tier/src/test/kotlin/ui/stepsummary/SummaryPresenterTest.kt new file mode 100644 index 0000000000..86f15e0ec4 --- /dev/null +++ b/app/feature/feature-choose-tier/src/test/kotlin/ui/stepsummary/SummaryPresenterTest.kt @@ -0,0 +1,91 @@ +package ui.stepsummary + +import FakeChangeTierRepository +import app.cash.turbine.Turbine +import arrow.core.Either +import arrow.core.right +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.isInstanceOf +import assertk.assertions.prop +import com.hedvig.android.core.common.ErrorMessage +import com.hedvig.android.core.uidata.UiMoney +import com.hedvig.android.feature.change.tier.data.CurrentContractData +import com.hedvig.android.feature.change.tier.data.GetCurrentContractDataUseCase +import com.hedvig.android.feature.change.tier.navigation.SummaryParameters +import com.hedvig.android.feature.change.tier.ui.stepsummary.SummaryPresenter +import com.hedvig.android.feature.change.tier.ui.stepsummary.SummaryState +import com.hedvig.android.feature.change.tier.ui.stepsummary.SummaryState.Loading +import com.hedvig.android.logger.TestLogcatLoggingRule +import com.hedvig.android.molecule.test.test +import currentQuote +import kotlinx.coroutines.test.runTest +import kotlinx.datetime.LocalDate +import org.junit.Rule +import org.junit.Test +import testQuote +import testQuoteWithOneAddon +import testQuoteWithTwoAddons + +class SummaryPresenterTest { + val summaryParams = SummaryParameters( + quoteIdToSubmit = "quoteId", + insuranceId = "insuranceId", + activationDate = LocalDate(2026, 1, 1), + ) + + @get:Rule + val testLogcatLogger = TestLogcatLoggingRule() + + @Test + fun `the uiState total is correctly calculated`() = runTest { + val tierRepo = FakeChangeTierRepository() + val useCase = FakeGetCurrentContractDataUseCase() + val presenter = SummaryPresenter( + tierRepository = tierRepo, + params = summaryParams, + getCurrentContractDataUseCase = useCase, + ) + presenter.test(Loading) { + useCase.turbine.add(CurrentContractData("currentExposureName").right()) + tierRepo.quoteTurbine.add(testQuoteWithOneAddon.right()) + tierRepo.quoteTurbine.add(currentQuote.right()) + skipItems(1) + val state = awaitItem() + assertThat(state) + .isInstanceOf(SummaryState.Success::class) + .prop(SummaryState.Success::total) + .isEqualTo(UiMoney(235.0, com.hedvig.android.core.uidata.UiCurrencyCode.SEK)) + } + presenter.test(Loading) { + useCase.turbine.add(CurrentContractData("currentExposureName").right()) + tierRepo.quoteTurbine.add(testQuoteWithTwoAddons.right()) + tierRepo.quoteTurbine.add(currentQuote.right()) + skipItems(1) + val state = awaitItem() + assertThat(state) + .isInstanceOf(SummaryState.Success::class) + .prop(SummaryState.Success::total) + .isEqualTo(UiMoney(315.0, com.hedvig.android.core.uidata.UiCurrencyCode.SEK)) + } + presenter.test(Loading) { + useCase.turbine.add(CurrentContractData("currentExposureName").right()) + tierRepo.quoteTurbine.add(testQuote.right()) + tierRepo.quoteTurbine.add(currentQuote.right()) + skipItems(1) + val state = awaitItem() + assertThat(state) + .isInstanceOf(SummaryState.Success::class) + .prop(SummaryState.Success::total) + .isEqualTo(UiMoney(299.0, com.hedvig.android.core.uidata.UiCurrencyCode.SEK)) + } + } +} + +private class FakeGetCurrentContractDataUseCase : GetCurrentContractDataUseCase { + val turbine = Turbine>() + + override suspend fun invoke(insuranceId: String): Either { + return turbine.awaitItem() + } +} diff --git a/app/feature/feature-movingflow/build.gradle.kts b/app/feature/feature-movingflow/build.gradle.kts index 668ef55b63..1d25239c9a 100644 --- a/app/feature/feature-movingflow/build.gradle.kts +++ b/app/feature/feature-movingflow/build.gradle.kts @@ -1,5 +1,3 @@ -import org.jetbrains.kotlin.fir.expressions.builder.buildImplicitInvokeCall - plugins { id("hedvig.gradle.plugin") id("hedvig.android.library") @@ -10,7 +8,9 @@ hedvig { serialization() compose() } - +android { + testOptions.unitTests.isReturnDefaultValues = true +} dependencies { api(libs.androidx.navigation.common) @@ -42,4 +42,20 @@ dependencies { implementation(libs.compose.richtext) implementation(libs.compose.richtextCommonmark) implementation(projects.featureFlagsPublic) + + testImplementation(libs.apollo.testingSupport) + testImplementation(libs.assertK) + testImplementation(libs.coroutines.test) + testImplementation(libs.junit) + testImplementation(libs.turbine) + testImplementation(projects.apolloOctopusTest) + testImplementation(projects.apolloTest) + testImplementation(projects.coreCommonTest) + testImplementation(projects.coreDatastoreTest) + testImplementation(projects.featureFlagsTest) + testImplementation(projects.languageTest) + testImplementation(projects.loggingTest) + testImplementation(projects.moleculeTest) + testImplementation(projects.testClock) + testImplementation(libs.mockk) } diff --git a/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/di/movingFlowModule.kt b/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/di/movingFlowModule.kt index 8329fa9508..a16473f70b 100644 --- a/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/di/movingFlowModule.kt +++ b/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/di/movingFlowModule.kt @@ -5,6 +5,7 @@ import androidx.datastore.preferences.core.Preferences import androidx.lifecycle.SavedStateHandle import com.apollographql.apollo.ApolloClient import com.hedvig.android.feature.movingflow.storage.MovingFlowRepository +import com.hedvig.android.feature.movingflow.storage.MovingFlowRepositoryImpl import com.hedvig.android.feature.movingflow.storage.MovingFlowStorage import com.hedvig.android.feature.movingflow.ui.addhouseinformation.AddHouseInformationViewModel import com.hedvig.android.feature.movingflow.ui.chosecoveragelevelanddeductible.ChoseCoverageLevelAndDeductibleViewModel @@ -20,7 +21,7 @@ val movingFlowModule = module { MovingFlowStorage(get>()) } single { - MovingFlowRepository(get()) + MovingFlowRepositoryImpl(get()) } viewModel { StartViewModel( diff --git a/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/storage/MovingFlowRepository.kt b/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/storage/MovingFlowRepositoryImpl.kt similarity index 82% rename from app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/storage/MovingFlowRepository.kt rename to app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/storage/MovingFlowRepositoryImpl.kt index e2abb8be0a..1b6830c8a3 100644 --- a/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/storage/MovingFlowRepository.kt +++ b/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/storage/MovingFlowRepositoryImpl.kt @@ -16,18 +16,45 @@ import kotlinx.datetime.LocalDate import octopus.feature.movingflow.fragment.MoveIntentFragment import octopus.feature.movingflow.fragment.MoveIntentQuotesFragment -internal class MovingFlowRepository( +internal interface MovingFlowRepository { + fun movingFlowState(): Flow + + suspend fun initiateNewMovingFlow(moveIntent: MoveIntentFragment, housingType: HousingType) + + suspend fun updateWithPropertyInput( + movingDate: LocalDate, + address: String, + postalCode: String, + squareMeters: Int, + numberCoInsured: Int, + isStudent: Boolean, + ) + + suspend fun updateWithHouseInput( + yearOfConstruction: Int, + ancillaryArea: Int, + numberOfBathrooms: Int, + isSublet: Boolean, + extraBuildings: List, + ): MovingFlowState? + + suspend fun updateWithMoveIntentQuotes(moveIntentQuotesFragment: MoveIntentQuotesFragment) + + suspend fun updatePreselectedHomeQuoteId(selectedHomeQuoteId: String) +} + +internal class MovingFlowRepositoryImpl( private val movingFlowStorage: MovingFlowStorage, -) { - fun movingFlowState(): Flow { +) : MovingFlowRepository { + override fun movingFlowState(): Flow { return movingFlowStorage.getMovingFlowState() } - suspend fun initiateNewMovingFlow(moveIntent: MoveIntentFragment, housingType: HousingType) { + override suspend fun initiateNewMovingFlow(moveIntent: MoveIntentFragment, housingType: HousingType) { movingFlowStorage.setMovingFlowState(MovingFlowState.fromFragments(moveIntent, null, housingType)) } - suspend fun updateWithPropertyInput( + override suspend fun updateWithPropertyInput( movingDate: LocalDate, address: String, postalCode: String, @@ -78,7 +105,7 @@ internal class MovingFlowRepository( } } - suspend fun updateWithHouseInput( + override suspend fun updateWithHouseInput( yearOfConstruction: Int, ancillaryArea: Int, numberOfBathrooms: Int, @@ -106,13 +133,13 @@ internal class MovingFlowRepository( } } - suspend fun updateWithMoveIntentQuotes(moveIntentQuotesFragment: MoveIntentQuotesFragment) { + override suspend fun updateWithMoveIntentQuotes(moveIntentQuotesFragment: MoveIntentQuotesFragment) { movingFlowStorage.editMovingFlowState { existingState -> existingState.copy(movingFlowQuotes = moveIntentQuotesFragment.toMovingFlowQuotes()) } } - suspend fun updatePreselectedHomeQuoteId(selectedHomeQuoteId: String) { + override suspend fun updatePreselectedHomeQuoteId(selectedHomeQuoteId: String) { movingFlowStorage.editMovingFlowState { existingState -> existingState.copy( lastSelectedHomeQuoteId = selectedHomeQuoteId, diff --git a/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/ui/chosecoveragelevelanddeductible/ChoseCoverageLevelAndDeductibleDestination.kt b/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/ui/chosecoveragelevelanddeductible/ChoseCoverageLevelAndDeductibleDestination.kt index aa5af5b5f9..c16c9a1916 100644 --- a/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/ui/chosecoveragelevelanddeductible/ChoseCoverageLevelAndDeductibleDestination.kt +++ b/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/ui/chosecoveragelevelanddeductible/ChoseCoverageLevelAndDeductibleDestination.kt @@ -111,8 +111,8 @@ internal fun ChoseCoverageLevelAndDeductibleDestination( navigateUp = navigateUp, popBackStack = popBackStack, exitFlow = exitFlow, - onSubmit = { selectedHomeQuoteId -> - viewModel.emit(ChoseCoverageLevelAndDeductibleEvent.SubmitSelectedHomeQuoteId(selectedHomeQuoteId)) + onSubmit = { + viewModel.emit(ChoseCoverageLevelAndDeductibleEvent.SubmitSelectedHomeQuoteId) }, onSelectCoverageOption = { viewModel.emit(ChoseCoverageLevelAndDeductibleEvent.SelectCoverage(it)) }, onSelectDeductibleOption = { viewModel.emit(ChoseCoverageLevelAndDeductibleEvent.SelectDeductible(it)) }, @@ -125,7 +125,7 @@ private fun ChoseCoverageLevelAndDeductibleScreen( navigateUp: () -> Unit, popBackStack: () -> Unit, exitFlow: () -> Unit, - onSubmit: (String) -> Unit, + onSubmit: () -> Unit, onSelectCoverageOption: (String) -> Unit, onSelectDeductibleOption: (String) -> Unit, onCompareCoverageClicked: () -> Unit, @@ -157,7 +157,7 @@ private fun ChoseCoverageLevelAndDeductibleScreen( is Content -> ChoseCoverageLevelAndDeductibleScreen( content = uiState, - onSubmit = uiState.tiersInfo.selectedHomeQuoteId?.let { { onSubmit(it) } }, + onSubmit = onSubmit, onSelectCoverageOption = onSelectCoverageOption, onSelectDeductibleOption = onSelectDeductibleOption, onCompareCoverageClicked = onCompareCoverageClicked, diff --git a/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/ui/chosecoveragelevelanddeductible/ChoseCoverageLevelAndDeductibleViewModel.kt b/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/ui/chosecoveragelevelanddeductible/ChoseCoverageLevelAndDeductibleViewModel.kt index 21764cdc6e..2c781c137f 100644 --- a/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/ui/chosecoveragelevelanddeductible/ChoseCoverageLevelAndDeductibleViewModel.kt +++ b/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/ui/chosecoveragelevelanddeductible/ChoseCoverageLevelAndDeductibleViewModel.kt @@ -38,7 +38,7 @@ internal class ChoseCoverageLevelAndDeductibleViewModel( ChoseCoverageLevelAndDeductiblePresenter(movingFlowRepository), ) -private class ChoseCoverageLevelAndDeductiblePresenter( +internal class ChoseCoverageLevelAndDeductiblePresenter( private val movingFlowRepository: MovingFlowRepository, ) : MoleculePresenter { @Composable @@ -62,7 +62,8 @@ private class ChoseCoverageLevelAndDeductiblePresenter( val newlySelectedQuote = currentContent.allOptions.firstOrNull { it.id == event.homeQuoteId } val newlySelectedTier = newlySelectedQuote?.tierName val quoteWithSameDeductibleAndNewTier = currentContent.allOptions.firstOrNull { - it.tierName == newlySelectedTier && it.deductible == currentDeductible + it.tierName == newlySelectedTier && + it.deductible == currentDeductible } val newSelectedCoverage = quoteWithSameDeductibleAndNewTier ?: newlySelectedQuote ?: return@CollectEvents if (newSelectedCoverage == currentContent.selectedCoverage) return@CollectEvents @@ -81,7 +82,8 @@ private class ChoseCoverageLevelAndDeductiblePresenter( } is SubmitSelectedHomeQuoteId -> { - submittingSelectedHomeQuoteId = event.homeQuoteId + val currentContent = tiersInfo.getOrNull() ?: return@CollectEvents + submittingSelectedHomeQuoteId = currentContent.selectedHomeQuoteId } NavigatedToSummary -> navigateToSummaryScreenWithHomeQuoteId = null @@ -177,7 +179,7 @@ sealed interface ChoseCoverageLevelAndDeductibleEvent { data class SelectDeductible(val homeQuoteId: String) : ChoseCoverageLevelAndDeductibleEvent - data class SubmitSelectedHomeQuoteId(val homeQuoteId: String) : ChoseCoverageLevelAndDeductibleEvent + data object SubmitSelectedHomeQuoteId : ChoseCoverageLevelAndDeductibleEvent data object NavigatedToSummary : ChoseCoverageLevelAndDeductibleEvent diff --git a/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/ui/enternewaddress/EnterNewAddressDestination.kt b/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/ui/enternewaddress/EnterNewAddressDestination.kt index 451c6c8b3c..b127dd3218 100644 --- a/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/ui/enternewaddress/EnterNewAddressDestination.kt +++ b/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/ui/enternewaddress/EnterNewAddressDestination.kt @@ -211,7 +211,8 @@ private fun EnterNewAddressScreen( if (it.isEmpty()) { uiState.postalCode.updateValue(null) } else if (it.isDigitsOnly()) { - uiState.postalCode.updateValue(it) + uiState + .postalCode.updateValue(it) } }, labelText = stringResource(R.string.CHANGE_ADDRESS_NEW_POSTAL_CODE_LABEL), diff --git a/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/ui/enternewaddress/EnterNewAddressViewModel.kt b/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/ui/enternewaddress/EnterNewAddressViewModel.kt index f10d742509..58bcf4ebee 100644 --- a/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/ui/enternewaddress/EnterNewAddressViewModel.kt +++ b/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/ui/enternewaddress/EnterNewAddressViewModel.kt @@ -84,7 +84,7 @@ internal class EnterNewAddressViewModel( ), ) -private class EnterNewAddressPresenter( +internal class EnterNewAddressPresenter( private val moveIntentId: String, private val movingFlowRepository: MovingFlowRepository, private val apolloClient: ApolloClient, diff --git a/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/ui/start/StartViewModel.kt b/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/ui/start/StartViewModel.kt index 1604fdb449..394c7bb2e3 100644 --- a/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/ui/start/StartViewModel.kt +++ b/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/ui/start/StartViewModel.kt @@ -20,6 +20,7 @@ import com.hedvig.android.feature.movingflow.ui.start.StartEvent.DismissStartErr import com.hedvig.android.feature.movingflow.ui.start.StartEvent.NavigatedToNextStep import com.hedvig.android.feature.movingflow.ui.start.StartEvent.SelectHousingType import com.hedvig.android.feature.movingflow.ui.start.StartEvent.SubmitHousingType +import com.hedvig.android.feature.movingflow.ui.start.StartUiState.Loading import com.hedvig.android.feature.movingflow.ui.start.StartUiState.StartError import com.hedvig.android.molecule.android.MoleculeViewModel import com.hedvig.android.molecule.public.MoleculePresenter @@ -35,7 +36,7 @@ internal class StartViewModel( StartPresenter(apolloClient, movingFlowRepository), ) -private class StartPresenter( +internal class StartPresenter( private val apolloClient: ApolloClient, private val movingFlowRepository: MovingFlowRepository, ) : MoleculePresenter { @@ -68,6 +69,9 @@ private class StartPresenter( LaunchedEffect(loadIteration) { either { + if (loadIteration > 0) { + currentState = Loading + } val moveIntentCreate = apolloClient .mutation(MoveIntentV2CreateMutation()) .safeExecute() diff --git a/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/ui/summary/SummaryDestination.kt b/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/ui/summary/SummaryDestination.kt index acdc11feec..fd619d7b4b 100644 --- a/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/ui/summary/SummaryDestination.kt +++ b/app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/ui/summary/SummaryDestination.kt @@ -243,7 +243,6 @@ private fun SummaryScreen( Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { QuoteCard(content.summaryInfo.moveHomeQuote) val description = stringResource(R.string.MOVING_FLOW_TRAVEL_ADDON_SUMMARY_DESCRIPTION) - // todo: add deep link to new conversation, not inbox! see: https://hedviginsurance.slack.com/archives/C07MM6F0DK2/p1734647206289359?thread_ts=1734613513.633699&cid=C07MM6F0DK2 for (addonQuote in content.summaryInfo.moveHomeQuote.relatedAddonQuotes) { val bottomSheetTitle = addonQuote.addonVariant.displayName AddonQuoteCard( diff --git a/app/feature/feature-movingflow/src/test/kotlin/storage/MovingFlowRepositoryImplTest.kt b/app/feature/feature-movingflow/src/test/kotlin/storage/MovingFlowRepositoryImplTest.kt new file mode 100644 index 0000000000..020a639999 --- /dev/null +++ b/app/feature/feature-movingflow/src/test/kotlin/storage/MovingFlowRepositoryImplTest.kt @@ -0,0 +1,316 @@ +package storage + +import app.cash.turbine.test +import assertk.all +import assertk.assertThat +import assertk.assertions.first +import assertk.assertions.isEqualTo +import assertk.assertions.isInstanceOf +import assertk.assertions.isNotNull +import assertk.assertions.prop +import com.hedvig.android.apollo.test.TestApolloClientRule +import com.hedvig.android.apollo.test.TestNetworkTransportType +import com.hedvig.android.core.datastore.TestPreferencesDataStore +import com.hedvig.android.feature.movingflow.data.HousingType +import com.hedvig.android.feature.movingflow.data.MovingFlowQuotes +import com.hedvig.android.feature.movingflow.data.MovingFlowQuotes.MoveHomeQuote +import com.hedvig.android.feature.movingflow.data.MovingFlowState +import com.hedvig.android.feature.movingflow.storage.MovingFlowRepositoryImpl +import com.hedvig.android.feature.movingflow.storage.MovingFlowStorage +import com.hedvig.android.logger.TestLogcatLoggingRule +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import kotlinx.datetime.LocalDate +import octopus.feature.movingflow.fragment.MoveIntentFragment +import octopus.feature.movingflow.fragment.MoveIntentQuotesFragment +import octopus.type.CurrencyCode +import octopus.type.MoveExtraBuildingType +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder + +class MovingFlowRepositoryImplTest { + @get:Rule + val testFolder = TemporaryFolder() + + @get:Rule + val testLogcatLogger = TestLogcatLoggingRule() + + @get:Rule + val testApolloClientRule = TestApolloClientRule(TestNetworkTransportType.MAP) + + val moveIntent = object : MoveIntentFragment { + override val id: String + get() = "ididid" + override val minMovingDate: LocalDate + get() = LocalDate(2026, 1, 1) + override val maxMovingDate: LocalDate + get() = LocalDate(2025, 1, 1) + override val maxHouseNumberCoInsured: Int? + get() = 6 + override val maxHouseSquareMeters: Int? + get() = 200 + override val maxApartmentNumberCoInsured: Int? + get() = 6 + override val maxApartmentSquareMeters: Int? + get() = 200 + override val isApartmentAvailableforStudent: Boolean? + get() = false + override val extraBuildingTypes: List + get() = listOf(MoveExtraBuildingType.GREENHOUSE) + override val suggestedNumberCoInsured: Int + get() = 2 + override val currentHomeAddresses: List + get() = listOf( + object : MoveIntentFragment.CurrentHomeAddress { + override val id: String + get() = "adsressid" + override val oldAddressCoverageDurationDays: Int? + get() = 30 + }, + ) + } + + @Test + fun `should initiate a new moving flow with given move intent and housing type`() = runTest { + val storage = movingFlowStorage() + val repo = MovingFlowRepositoryImpl(storage) + repo.initiateNewMovingFlow(moveIntent, HousingType.ApartmentOwn) + repo.movingFlowState().test { + val result = awaitItem() + assertThat(result).isNotNull().prop(MovingFlowState::housingType).isEqualTo(HousingType.ApartmentOwn) + assertThat(result).isNotNull().prop(MovingFlowState::propertyState).isInstanceOf( + MovingFlowState.PropertyState.ApartmentState::class, + ) + } + } + + @Test + fun `should update state with property input`() = runTest { + val storage = movingFlowStorage() + val repo = MovingFlowRepositoryImpl(storage) + repo.initiateNewMovingFlow(moveIntent, HousingType.ApartmentOwn) + repo.updateWithPropertyInput( + movingDate = LocalDate(2025, 2, 2), + address = "some addr", + postalCode = "some code", + squareMeters = 67, + numberCoInsured = 3, + isStudent = false, + ) + repo.movingFlowState().test { + val result = awaitItem() + assertThat(result).isNotNull().prop(MovingFlowState::addressInfo).isEqualTo( + MovingFlowState.AddressInfo( + street = "some addr", + postalCode = "some code", + ), + ) + assertThat(result).isNotNull().prop(MovingFlowState::propertyState).isInstanceOf( + MovingFlowState.PropertyState.ApartmentState::class, + ) + } + } + + @Test + fun `should update house state with house input details`() = runTest { + val storage = movingFlowStorage() + val repo = MovingFlowRepositoryImpl(storage) + repo.initiateNewMovingFlow(moveIntent, HousingType.Villa) + repo.updateWithHouseInput( + yearOfConstruction = 2000, + ancillaryArea = 33, + numberOfBathrooms = 5, + isSublet = false, + extraBuildings = emptyList(), + ) + repo.movingFlowState().test { + val result = awaitItem() + assertThat(result).isNotNull().prop(MovingFlowState::propertyState).isInstanceOf( + MovingFlowState.PropertyState.HouseState::class, + ).all { + prop(MovingFlowState.PropertyState.HouseState::yearOfConstruction).isEqualTo(2000) + prop(MovingFlowState.PropertyState.HouseState::ancillaryArea).isEqualTo(33) + prop(MovingFlowState.PropertyState.HouseState::numberOfBathrooms).isEqualTo(5) + prop(MovingFlowState.PropertyState.HouseState::isSublet).isEqualTo(false) + } + } + } + + @Test + fun `should not update when trying updating house state on a non-house state`() = runTest { + val storage = movingFlowStorage() + val repo = MovingFlowRepositoryImpl(storage) + repo.initiateNewMovingFlow(moveIntent, HousingType.ApartmentOwn) + repo.updateWithHouseInput( + yearOfConstruction = 2000, + ancillaryArea = 33, + numberOfBathrooms = 5, + isSublet = false, + extraBuildings = emptyList(), + ) + repo.movingFlowState().test { + val result = awaitItem() + assertThat(result).isNotNull().prop(MovingFlowState::propertyState).isInstanceOf( + MovingFlowState.PropertyState.ApartmentState::class, + ) + } + } + + @Test + fun `should update state with move intent quotes`() = runTest { + val storage = movingFlowStorage() + val repo = MovingFlowRepositoryImpl(storage) + repo.initiateNewMovingFlow(moveIntent, HousingType.ApartmentOwn) + repo.updateWithMoveIntentQuotes( + object : MoveIntentQuotesFragment { + override val homeQuotes: List? + get() = buildList { + add(homeQuote1) + add(homeQuote2) + } + override val mtaQuotes: List? + get() = emptyList() + }, + ) + repo.movingFlowState().test { + assertThat(awaitItem()).isNotNull().prop(MovingFlowState::movingFlowQuotes) + .isNotNull().prop(MovingFlowQuotes::homeQuotes).first().prop(MoveHomeQuote::id).isEqualTo(homeQuote1.id) + } + } + + @Test + fun `should update selected home quote id`() = runTest { + val storage = movingFlowStorage() + val repo = MovingFlowRepositoryImpl(storage) + repo.initiateNewMovingFlow(moveIntent, HousingType.ApartmentOwn) + repo.updateWithMoveIntentQuotes( + object : MoveIntentQuotesFragment { + override val homeQuotes: List? + get() = buildList { + add(homeQuote1) + add(homeQuote2) + } + override val mtaQuotes: List? + get() = emptyList() + }, + ) + repo.updatePreselectedHomeQuoteId(homeQuote2.id) + repo.movingFlowState().test { + assertThat(awaitItem()).isNotNull().prop(MovingFlowState::lastSelectedHomeQuoteId).isEqualTo(homeQuote2.id) + } + } + + private fun TestScope.movingFlowStorage() = MovingFlowStorage( + TestPreferencesDataStore( + datastoreTestFileDirectory = testFolder.newFolder("datastoreTempFolder"), + coroutineScope = backgroundScope, + ), + ) +} + +private val homeQuote1 = object : MoveIntentQuotesFragment.HomeQuote { + override val id: String + get() = "id1" + override val premium: MoveIntentQuotesFragment.HomeQuote.Premium + get() = object : MoveIntentQuotesFragment.HomeQuote.Premium { + override val __typename: String + get() = "str" + override val amount: Double + get() = 30.0 + override val currencyCode: CurrencyCode + get() = CurrencyCode.SEK + } + override val startDate: LocalDate + get() = LocalDate(2025, 6, 6) + override val defaultChoice: Boolean + get() = true + override val tierName: String + get() = "tierName" + override val tierLevel: Int + get() = 2 + override val deductible: MoveIntentQuotesFragment.HomeQuote.Deductible? + get() = null + override val displayItems: List + get() = emptyList() + override val exposureName: String + get() = "exposure" + override val productVariant: MoveIntentQuotesFragment.HomeQuote.ProductVariant + get() = object : MoveIntentQuotesFragment.HomeQuote.ProductVariant { + override val __typename: String + get() = "string" + override val displayName: String + get() = "string" + override val displayNameTier: String? + get() = "string" + override val tierDescription: String? + get() = "string" + override val typeOfContract: String + get() = "string" + override val partner: String? + get() = "string" + override val termsVersion: String + get() = "string" + override val perils: List + get() = emptyList() + override val insurableLimits: List + get() = emptyList() + override val documents: List + get() = emptyList() + } + override val addons: List? + get() = emptyList() +} + +private val homeQuote2 = object : MoveIntentQuotesFragment.HomeQuote { + override val id: String + get() = "id1" + override val premium: MoveIntentQuotesFragment.HomeQuote.Premium + get() = object : MoveIntentQuotesFragment.HomeQuote.Premium { + override val __typename: String + get() = "str" + override val amount: Double + get() = 30.0 + override val currencyCode: CurrencyCode + get() = CurrencyCode.SEK + } + override val startDate: LocalDate + get() = LocalDate(2025, 6, 6) + override val defaultChoice: Boolean + get() = true + override val tierName: String + get() = "tierName" + override val tierLevel: Int + get() = 2 + override val deductible: MoveIntentQuotesFragment.HomeQuote.Deductible? + get() = null + override val displayItems: List + get() = emptyList() + override val exposureName: String + get() = "exposure" + override val productVariant: MoveIntentQuotesFragment.HomeQuote.ProductVariant + get() = object : MoveIntentQuotesFragment.HomeQuote.ProductVariant { + override val __typename: String + get() = "string" + override val displayName: String + get() = "string" + override val displayNameTier: String? + get() = "string" + override val tierDescription: String? + get() = "string" + override val typeOfContract: String + get() = "string" + override val partner: String? + get() = "string" + override val termsVersion: String + get() = "string" + override val perils: List + get() = emptyList() + override val insurableLimits: List + get() = emptyList() + override val documents: List + get() = emptyList() + } + override val addons: List? + get() = emptyList() +} diff --git a/app/feature/feature-movingflow/src/test/kotlin/ui/AddHouseInformationPresenterTest.kt b/app/feature/feature-movingflow/src/test/kotlin/ui/AddHouseInformationPresenterTest.kt new file mode 100644 index 0000000000..2d1f2edd95 --- /dev/null +++ b/app/feature/feature-movingflow/src/test/kotlin/ui/AddHouseInformationPresenterTest.kt @@ -0,0 +1,62 @@ +package ui + +import kotlinx.coroutines.test.runTest +import org.junit.Test + +internal class AddHouseInformationPresenterTest { + @Test + fun `dismiss submission error clears the error state`() = runTest { + // todo() + } + + @Test + fun `submit event with valid content updates repository with house input`() = runTest { +// todo() + } + + @Test + fun `submit event with invalid content does nothing`() = runTest { +// todo() + } + + @Test + fun `show error section when repository update fails`() = runTest { +// todo() + } + + @Test + fun `when repository update is successful move intent request is triggered`() = runTest { +// todo() + } + + @Test + fun `if move intent request gets good response update repo moveIntentQuotes and navigate to choose coverage`() = + runTest { +// todo() + } + + @Test + fun `mutation request with user error sets submission info failure`() = runTest { +// todo() + } + + @Test + fun `mutation request success navigates to chose coverage`() = runTest { +// todo() + } + + @Test + fun `initial state is loading when last state is loading`() = runTest { +// todo() + } + + @Test + fun `initial state is missing ongoing moving flow when last state is missing`() = runTest { +// todo() + } + + @Test + fun `initial state is content when last state is content`() = runTest { + // todo() + } +} diff --git a/app/feature/feature-movingflow/src/test/kotlin/ui/ChooseCoverageTest.kt b/app/feature/feature-movingflow/src/test/kotlin/ui/ChooseCoverageTest.kt new file mode 100644 index 0000000000..8adc740cbf --- /dev/null +++ b/app/feature/feature-movingflow/src/test/kotlin/ui/ChooseCoverageTest.kt @@ -0,0 +1,215 @@ +package ui + +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.isInstanceOf +import assertk.assertions.isNotNull +import assertk.assertions.isNull +import assertk.assertions.isTrue +import assertk.assertions.prop +import assertk.assertions.size +import com.hedvig.android.feature.movingflow.data.MovingFlowQuotes +import com.hedvig.android.feature.movingflow.ui.chosecoveragelevelanddeductible.ChoseCoverageLevelAndDeductibleEvent +import com.hedvig.android.feature.movingflow.ui.chosecoveragelevelanddeductible.ChoseCoverageLevelAndDeductiblePresenter +import com.hedvig.android.feature.movingflow.ui.chosecoveragelevelanddeductible.ChoseCoverageLevelAndDeductibleUiState +import com.hedvig.android.feature.movingflow.ui.chosecoveragelevelanddeductible.TiersInfo +import com.hedvig.android.logger.TestLogcatLoggingRule +import com.hedvig.android.molecule.test.test +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +internal class ChooseCoverageTest { + @get:Rule + val testLogcatLogger = TestLogcatLoggingRule() + + @Test + fun `should send to repo quote with the new coverage and if there is any same deductible when new tier is selected`() = + runTest { + val repo = FakeMovingFlowRepository() + val presenter = ChoseCoverageLevelAndDeductiblePresenter( + movingFlowRepository = repo, + ) + presenter.test(ChoseCoverageLevelAndDeductibleUiState.Loading) { + repo.movingFlowStateTurbine.add(fakeMovingStateWithTiersAndDeductibles) + skipItems(2) + sendEvent(ChoseCoverageLevelAndDeductibleEvent.SelectCoverage(fakeHomeQuoteWithTiersDeductibles2.id)) + assertThat(repo.selectedQuoteIdParameterThatWasSentIn) + .isEqualTo(fakeHomeQuoteWithTiersDeductibles1.id) + } + } + + @Test + fun `should send to repo newly selected quote itself if there is no same deductible when new tier is selected`() = + runTest { + val repo = FakeMovingFlowRepository() + val presenter = ChoseCoverageLevelAndDeductiblePresenter( + movingFlowRepository = repo, + ) + presenter.test(ChoseCoverageLevelAndDeductibleUiState.Loading) { + repo.movingFlowStateTurbine.add(fakeMovingStateWithTiersAndDeductibles) + skipItems(2) + sendEvent(ChoseCoverageLevelAndDeductibleEvent.SelectCoverage(fakeHomeQuoteWithTiersDeductibles5.id)) + assertThat(repo.selectedQuoteIdParameterThatWasSentIn) + .isEqualTo(fakeHomeQuoteWithTiersDeductibles5.id) + } + } + + @Test + fun `should send to repo newly selected quote itself when new deductible is selected`() = runTest { + val repo = FakeMovingFlowRepository() + val presenter = ChoseCoverageLevelAndDeductiblePresenter( + movingFlowRepository = repo, + ) + presenter.test(ChoseCoverageLevelAndDeductibleUiState.Loading) { + repo.movingFlowStateTurbine.add(fakeMovingStateWithTiersAndDeductibles) + skipItems(2) + sendEvent(ChoseCoverageLevelAndDeductibleEvent.SelectDeductible(fakeHomeQuoteWithTiersDeductibles4.id)) + assertThat(repo.selectedQuoteIdParameterThatWasSentIn) + .isEqualTo(fakeHomeQuoteWithTiersDeductibles4.id) + } + } + + @Test + fun `should show button loading and navigate to summary with selected home quote ID when selected quote is submitted`() = + runTest { + val repo = FakeMovingFlowRepository() + val presenter = ChoseCoverageLevelAndDeductiblePresenter( + movingFlowRepository = repo, + ) + presenter.test(ChoseCoverageLevelAndDeductibleUiState.Loading) { + repo.movingFlowStateTurbine.add( + fakeMovingStateWithTiersAndDeductibles.copy( + lastSelectedHomeQuoteId = fakeHomeQuoteWithTiersDeductibles4.id, + ), + ) + skipItems(2) + sendEvent(ChoseCoverageLevelAndDeductibleEvent.SubmitSelectedHomeQuoteId) + val result = awaitItem() + assertThat(result).isInstanceOf(ChoseCoverageLevelAndDeductibleUiState.Content::class) + .prop(ChoseCoverageLevelAndDeductibleUiState.Content::isSubmitting) + .isTrue() + val result2 = awaitItem() + assertThat(result2).isInstanceOf(ChoseCoverageLevelAndDeductibleUiState.Content::class) + .prop(ChoseCoverageLevelAndDeductibleUiState.Content::navigateToSummaryScreenWithHomeQuoteId) + .isEqualTo(fakeHomeQuoteWithTiersDeductibles4.id) + } + } + + @Test + fun `should clear navigation parameter after navigating to summary`() = runTest { + val repo = FakeMovingFlowRepository() + val presenter = ChoseCoverageLevelAndDeductiblePresenter( + movingFlowRepository = repo, + ) + presenter.test(ChoseCoverageLevelAndDeductibleUiState.Loading) { + repo.movingFlowStateTurbine.add( + fakeMovingStateWithTiersAndDeductibles.copy( + lastSelectedHomeQuoteId = fakeHomeQuoteWithTiersDeductibles4.id, + ), + ) + sendEvent(ChoseCoverageLevelAndDeductibleEvent.SubmitSelectedHomeQuoteId) + skipItems(4) + sendEvent(ChoseCoverageLevelAndDeductibleEvent.NavigatedToSummary) + val result2 = awaitItem() + assertThat(result2).isInstanceOf(ChoseCoverageLevelAndDeductibleUiState.Content::class) + .prop(ChoseCoverageLevelAndDeductibleUiState.Content::navigateToSummaryScreenWithHomeQuoteId) + .isNull() + } + } + + @Test + fun `should clear navigation parameter after navigating to comparison`() = runTest { + val repo = FakeMovingFlowRepository() + val presenter = ChoseCoverageLevelAndDeductiblePresenter( + movingFlowRepository = repo, + ) + presenter.test(ChoseCoverageLevelAndDeductibleUiState.Loading) { + repo.movingFlowStateTurbine.add(fakeMovingStateWithTiersAndDeductibles) + repo.movingFlowStateTurbine.add( + fakeMovingStateWithTiersAndDeductibles.copy( + lastSelectedHomeQuoteId = fakeHomeQuoteWithTiersDeductibles4.id, + ), + ) + sendEvent(ChoseCoverageLevelAndDeductibleEvent.LaunchComparison) + skipItems(4) + sendEvent(ChoseCoverageLevelAndDeductibleEvent.ClearNavigateToComparison) + val result2 = awaitItem() + assertThat(result2).isInstanceOf(ChoseCoverageLevelAndDeductibleUiState.Content::class) + .prop(ChoseCoverageLevelAndDeductibleUiState.Content::comparisonParameters) + .isNull() + } + } + + @Test + fun `should navigate to comparison when LaunchComparison event is received`() = runTest { + val repo = FakeMovingFlowRepository() + val presenter = ChoseCoverageLevelAndDeductiblePresenter( + movingFlowRepository = repo, + ) + presenter.test(ChoseCoverageLevelAndDeductibleUiState.Loading) { + repo.movingFlowStateTurbine.add( + fakeMovingStateWithTiersAndDeductibles.copy( + lastSelectedHomeQuoteId = fakeHomeQuoteWithTiersDeductibles4.id, + ), + ) + sendEvent(ChoseCoverageLevelAndDeductibleEvent.LaunchComparison) + skipItems(2) + val result1 = awaitItem() + assertThat(result1).isInstanceOf(ChoseCoverageLevelAndDeductibleUiState.Content::class) + .prop(ChoseCoverageLevelAndDeductibleUiState.Content::comparisonParameters) + .isNotNull() + } + } + + @Test + fun `should correctly show tiers info`() = runTest { + val repo = FakeMovingFlowRepository() + val presenter = ChoseCoverageLevelAndDeductiblePresenter( + movingFlowRepository = repo, + ) + presenter.test(ChoseCoverageLevelAndDeductibleUiState.Loading) { + repo.movingFlowStateTurbine.add(fakeMovingStateWithTiersAndDeductibles) + skipItems(1) + val result1 = awaitItem() + assertThat(result1).isInstanceOf(ChoseCoverageLevelAndDeductibleUiState.Content::class) + .prop(ChoseCoverageLevelAndDeductibleUiState.Content::tiersInfo) + .isInstanceOf(TiersInfo::class) + .prop(TiersInfo::allOptions) + .size().isEqualTo(5) + } + } + + @Test + fun `show error section when home quotes are empty`() = runTest { + val repo = FakeMovingFlowRepository() + val presenter = ChoseCoverageLevelAndDeductiblePresenter( + movingFlowRepository = repo, + ) + presenter.test(ChoseCoverageLevelAndDeductibleUiState.Loading) { + repo.movingFlowStateTurbine.add(fakeMovingStateWithNoQuotes) + skipItems(1) + val result1 = awaitItem() + assertThat(result1).isInstanceOf(ChoseCoverageLevelAndDeductibleUiState.MissingOngoingMovingFlow::class) + } + } + + @Test + fun `when no home quote is selected choose the default option`() = runTest { + val repo = FakeMovingFlowRepository() + val presenter = ChoseCoverageLevelAndDeductiblePresenter( + movingFlowRepository = repo, + ) + presenter.test(ChoseCoverageLevelAndDeductibleUiState.Loading) { + repo.movingFlowStateTurbine.add(fakeMovingStateWithTiersAndDeductibles) + skipItems(1) + val result1 = awaitItem() + assertThat(result1).isInstanceOf(ChoseCoverageLevelAndDeductibleUiState.Content::class) + .prop(ChoseCoverageLevelAndDeductibleUiState.Content::tiersInfo) + .isInstanceOf(TiersInfo::class) + .prop(TiersInfo::selectedCoverage) + .prop(MovingFlowQuotes.MoveHomeQuote::id) + .isEqualTo(fakeHomeQuoteWithTiersDeductibles3.id) + } + } +} diff --git a/app/feature/feature-movingflow/src/test/kotlin/ui/CommonTestData.kt b/app/feature/feature-movingflow/src/test/kotlin/ui/CommonTestData.kt new file mode 100644 index 0000000000..cd33bc0fb3 --- /dev/null +++ b/app/feature/feature-movingflow/src/test/kotlin/ui/CommonTestData.kt @@ -0,0 +1,447 @@ +package ui + +import com.hedvig.android.core.uidata.UiCurrencyCode.SEK +import com.hedvig.android.core.uidata.UiMoney +import com.hedvig.android.data.contract.ContractGroup +import com.hedvig.android.data.contract.ContractType +import com.hedvig.android.data.productvariant.AddonVariant +import com.hedvig.android.data.productvariant.InsurableLimit +import com.hedvig.android.data.productvariant.InsuranceVariantDocument +import com.hedvig.android.data.productvariant.InsuranceVariantDocument.InsuranceDocumentType.CERTIFICATE +import com.hedvig.android.data.productvariant.ProductVariant +import com.hedvig.android.data.productvariant.ProductVariantPeril +import com.hedvig.android.feature.movingflow.data.HousingType +import com.hedvig.android.feature.movingflow.data.MovingFlowQuotes +import com.hedvig.android.feature.movingflow.data.MovingFlowQuotes.AddonQuote +import com.hedvig.android.feature.movingflow.data.MovingFlowQuotes.DisplayItem +import com.hedvig.android.feature.movingflow.data.MovingFlowQuotes.MoveHomeQuote +import com.hedvig.android.feature.movingflow.data.MovingFlowQuotes.MoveHomeQuote.Deductible +import com.hedvig.android.feature.movingflow.data.MovingFlowQuotes.MoveMtaQuote +import com.hedvig.android.feature.movingflow.data.MovingFlowState +import com.hedvig.android.feature.movingflow.data.MovingFlowState.AddressInfo +import com.hedvig.android.feature.movingflow.ui.summary.SummaryInfo +import kotlinx.datetime.LocalDate + +internal val fakeProductVariant = ProductVariant( + displayName = "Variant", + contractGroup = ContractGroup.RENTAL, + contractType = ContractType.SE_APARTMENT_RENT, + partner = null, + perils = listOf( + ProductVariantPeril( + "id", + "peril title", + "peril description", + emptyList(), + emptyList(), + null, + ), + ), + insurableLimits = listOf( + InsurableLimit( + label = "insurable limit label", + limit = "insurable limit limit", + description = "insurable limit description", + ), + ), + documents = listOf( + InsuranceVariantDocument( + displayName = "displayName", + url = "url", + type = CERTIFICATE, + ), + ), + displayTierName = "tierDescription", + tierDescription = "displayNameTier", + termsVersion = "termsVersion", +) +internal val fakeAddonVariant = AddonVariant( + termsVersion = "terrrms", + displayName = "Addon 1", + product = "product", + documents = listOf( + InsuranceVariantDocument( + displayName = "displayName", + url = "url", + type = CERTIFICATE, + ), + ), + perils = listOf(), + insurableLimits = listOf(), +) +internal val fakeStartDate = LocalDate.parse("2025-01-01") + +internal val fakeHomeQuoteWithAddon = MoveHomeQuote( + id = homeQuoteIdFake, + premium = UiMoney(99.0, SEK), + startDate = LocalDate(2025, 1, 1), + displayItems = listOf( + DisplayItem( + title = "display title", + subtitle = "display subtitle", + value = "display value", + ), + ), + exposureName = "exposureName", + productVariant = fakeProductVariant, + tierName = "tierName", + tierLevel = 1, + tierDescription = "tierDescription", + deductible = Deductible(UiMoney(1500.0, SEK), null, "displayText"), + defaultChoice = false, + relatedAddonQuotes = List(1) { + AddonQuote( + premium = UiMoney(129.0, SEK), + startDate = fakeStartDate, + displayItems = listOf( + DisplayItem( + title = "display title", + subtitle = "display subtitle", + value = "display value", + ), + ), + exposureName = "exposureName", + addonVariant = fakeAddonVariant, + ) + }, +) + +internal val fakeHomeQuoteNoAddon = MoveHomeQuote( + id = homeQuoteIdFake, + premium = UiMoney(99.0, SEK), + startDate = LocalDate(2025, 1, 1), + displayItems = listOf( + DisplayItem( + title = "display title", + subtitle = "display subtitle", + value = "display value", + ), + ), + exposureName = "exposureName", + productVariant = fakeProductVariant, + tierName = "tierName", + tierLevel = 1, + tierDescription = "tierDescription", + deductible = Deductible(UiMoney(1500.0, SEK), null, "displayText"), + defaultChoice = false, + relatedAddonQuotes = emptyList(), +) + +internal val fakeHomeQuoteWithTiersDeductibles1 = MoveHomeQuote( + id = "fakeHomeQuoteWithTiersDeductibles1", + premium = UiMoney(99.0, SEK), + startDate = LocalDate(2025, 1, 1), + displayItems = listOf( + DisplayItem( + title = "display title", + subtitle = "display subtitle", + value = "display value", + ), + ), + exposureName = "exposureName", + productVariant = fakeProductVariant, + tierName = "tierName", + tierLevel = 1, + tierDescription = "tierDescription", + deductible = Deductible(UiMoney(3000.0, SEK), null, "displayText"), + defaultChoice = false, + relatedAddonQuotes = emptyList(), +) + +internal val fakeHomeQuoteWithTiersDeductibles2 = MoveHomeQuote( + id = "fakeHomeQuoteWithTiersDeductibles2", + premium = UiMoney(105.0, SEK), + startDate = LocalDate(2025, 1, 1), + displayItems = listOf( + DisplayItem( + title = "display title", + subtitle = "display subtitle", + value = "display value", + ), + ), + exposureName = "exposureName", + productVariant = fakeProductVariant, + tierName = "tierName", + tierLevel = 1, + tierDescription = "tierDescription", + deductible = Deductible(UiMoney(1500.0, SEK), null, "displayText"), + defaultChoice = false, + relatedAddonQuotes = emptyList(), +) + +internal val fakeHomeQuoteWithTiersDeductibles3 = MoveHomeQuote( + id = "fakeHomeQuoteWithTiersDeductibles3", + premium = UiMoney(110.0, SEK), + startDate = LocalDate(2025, 1, 1), + displayItems = listOf( + DisplayItem( + title = "display title", + subtitle = "display subtitle", + value = "display value", + ), + ), + exposureName = "exposureName", + productVariant = fakeProductVariant, + tierName = "tierName 2", + tierLevel = 2, + tierDescription = "tierDescription", + deductible = Deductible(UiMoney(3000.0, SEK), null, "displayText"), + defaultChoice = true, + relatedAddonQuotes = emptyList(), +) + +internal val fakeHomeQuoteWithTiersDeductibles4 = MoveHomeQuote( + id = "fakeHomeQuoteWithTiersDeductibles4", + premium = UiMoney(115.0, SEK), + startDate = LocalDate(2025, 1, 1), + displayItems = listOf( + DisplayItem( + title = "display title", + subtitle = "display subtitle", + value = "display value", + ), + ), + exposureName = "exposureName", + productVariant = fakeProductVariant, + tierName = "tierName 2", + tierLevel = 2, + tierDescription = "tierDescription", + deductible = Deductible(UiMoney(1500.0, SEK), null, "displayText"), + defaultChoice = false, + relatedAddonQuotes = emptyList(), +) + +internal val fakeHomeQuoteWithTiersDeductibles5 = MoveHomeQuote( + id = "fakeHomeQuoteWithTiersDeductibles5", + premium = UiMoney(115.0, SEK), + startDate = LocalDate(2025, 1, 1), + displayItems = listOf( + DisplayItem( + title = "display title", + subtitle = "display subtitle", + value = "display value", + ), + ), + exposureName = "exposureName", + productVariant = fakeProductVariant, + tierName = "tierName 3", + tierLevel = 3, + tierDescription = "tierDescription", + deductible = Deductible(UiMoney(9900.0, SEK), null, "displayText"), + defaultChoice = false, + relatedAddonQuotes = emptyList(), +) + +internal val fakeMta1 = MoveMtaQuote( + premium = UiMoney(49.0, SEK), + exposureName = "exposureName", + productVariant = fakeProductVariant, + startDate = fakeStartDate, + displayItems = emptyList(), + relatedAddonQuotes = emptyList(), +) + +internal val fakeMta2 = MoveMtaQuote( + premium = UiMoney(59.0, SEK), + exposureName = "exposureName", + productVariant = fakeProductVariant, + startDate = fakeStartDate, + displayItems = emptyList(), + relatedAddonQuotes = emptyList(), +) + +internal val fakeMta2WithAddon = MoveMtaQuote( + premium = UiMoney(23.0, SEK), + exposureName = "exposureName", + productVariant = fakeProductVariant, + startDate = fakeStartDate, + displayItems = emptyList(), + relatedAddonQuotes = listOf( + AddonQuote( + premium = UiMoney(30.0, SEK), + startDate = fakeStartDate, + displayItems = listOf( + DisplayItem( + title = "display title", + subtitle = "display subtitle", + value = "display value", + ), + ), + exposureName = "exposureName", + addonVariant = fakeAddonVariant, + ), + ), +) + +internal val fakeSummaryInfoNoAddons = SummaryInfo( + moveHomeQuote = fakeHomeQuoteNoAddon, + moveMtaQuotes = listOf( + fakeMta1, + fakeMta2, + ), +) + +internal val fakeSummaryInfoWithTwoAddons = SummaryInfo( + moveHomeQuote = fakeHomeQuoteWithAddon, + moveMtaQuotes = listOf( + fakeMta1, + fakeMta2WithAddon, + ), +) + +internal val fakeSummaryInfoWithOnlyHomeQuote = SummaryInfo( + moveHomeQuote = fakeHomeQuoteNoAddon, + moveMtaQuotes = emptyList(), +) + +internal const val moveIntentIdFake = "moveIntentId" +internal const val homeQuoteIdFake = "homeQuoteId" + +internal val fakePropertyStateBRF = MovingFlowState.PropertyState.ApartmentState( + numberCoInsuredState = MovingFlowState.NumberCoInsuredState(allowedNumberCoInsuredRange = 0..3, 1), + squareMetersState = MovingFlowState.SquareMetersState(10..100, 69), + apartmentType = MovingFlowState.PropertyState.ApartmentState.ApartmentType.BRF, + isAvailableForStudentState = MovingFlowState.PropertyState.ApartmentState.IsAvailableForStudentState.NotAvailable, +) + +internal val fakePropertyStateRent = MovingFlowState.PropertyState.ApartmentState( + numberCoInsuredState = MovingFlowState.NumberCoInsuredState(allowedNumberCoInsuredRange = 0..3, 1), + squareMetersState = MovingFlowState.SquareMetersState(10..100, 69), + apartmentType = MovingFlowState.PropertyState.ApartmentState.ApartmentType.RENT, + isAvailableForStudentState = MovingFlowState.PropertyState.ApartmentState.IsAvailableForStudentState.NotAvailable, +) + +internal val fakeMovingStateNoAddons = MovingFlowState( + id = moveIntentIdFake, + moveFromAddressId = "id", + housingType = HousingType.ApartmentOwn, + addressInfo = AddressInfo("street", "18888"), + movingDateState = MovingFlowState.MovingDateState( + selectedMovingDate = null, + allowedMovingDateRange = LocalDate(2025, 1, 1)..LocalDate(2025, 3, 1), + ), + propertyState = fakePropertyStateBRF, + movingFlowQuotes = MovingFlowQuotes( + homeQuotes = listOf(fakeHomeQuoteNoAddon), + mtaQuotes = listOf( + fakeMta1, + fakeMta2, + ), + ), + lastSelectedHomeQuoteId = null, + oldAddressCoverageDurationDays = 30, +) + +internal val fakeMovingStateWithNoQuotes = MovingFlowState( + id = moveIntentIdFake, + moveFromAddressId = "id", + housingType = HousingType.ApartmentOwn, + addressInfo = AddressInfo("street", "18888"), + movingDateState = MovingFlowState.MovingDateState( + selectedMovingDate = null, + allowedMovingDateRange = LocalDate(2025, 1, 1)..LocalDate(2025, 3, 1), + ), + propertyState = fakePropertyStateBRF, + movingFlowQuotes = null, + lastSelectedHomeQuoteId = null, + oldAddressCoverageDurationDays = 30, +) + +internal val fakeMovingStateBeforeHomeQuotes = MovingFlowState( + id = moveIntentIdFake, + moveFromAddressId = "moveFromAddressId", + housingType = HousingType.ApartmentOwn, + addressInfo = AddressInfo(null, null), + movingDateState = MovingFlowState.MovingDateState( + selectedMovingDate = null, + allowedMovingDateRange = LocalDate(2025, 1, 1)..LocalDate(2025, 3, 1), + ), + propertyState = fakePropertyStateBRF, + movingFlowQuotes = MovingFlowQuotes( + homeQuotes = emptyList(), + mtaQuotes = emptyList(), + ), + lastSelectedHomeQuoteId = null, + oldAddressCoverageDurationDays = 30, +) + +internal val fakeMovingStateBeforeHomeQuotesFilled = MovingFlowState( + id = moveIntentIdFake, + moveFromAddressId = "moveFromAddressId", + housingType = HousingType.ApartmentOwn, + addressInfo = AddressInfo(street = "info", postalCode = 11111.toString()), + movingDateState = MovingFlowState.MovingDateState( + selectedMovingDate = LocalDate(2025, 3, 3), + allowedMovingDateRange = LocalDate(2025, 1, 1)..LocalDate(2025, 12, 1), + ), + propertyState = fakePropertyStateRent, + movingFlowQuotes = MovingFlowQuotes( + homeQuotes = emptyList(), + mtaQuotes = emptyList(), + ), + lastSelectedHomeQuoteId = null, + oldAddressCoverageDurationDays = 30, +) + +internal val fakeMovingStateWithOnlyHomeQuote = MovingFlowState( + id = moveIntentIdFake, + moveFromAddressId = "id", + housingType = HousingType.ApartmentOwn, + addressInfo = AddressInfo("street", "18888"), + movingDateState = MovingFlowState.MovingDateState( + selectedMovingDate = null, + allowedMovingDateRange = LocalDate(2025, 1, 1)..LocalDate(2025, 3, 1), + ), + propertyState = fakePropertyStateBRF, + movingFlowQuotes = MovingFlowQuotes( + homeQuotes = listOf(fakeHomeQuoteNoAddon), + mtaQuotes = emptyList(), + ), + lastSelectedHomeQuoteId = null, + oldAddressCoverageDurationDays = 30, +) + +internal val fakeMovingStateWithTiersAndDeductibles = MovingFlowState( + id = moveIntentIdFake, + moveFromAddressId = "id", + housingType = HousingType.ApartmentOwn, + addressInfo = AddressInfo("street", "18888"), + movingDateState = MovingFlowState.MovingDateState( + selectedMovingDate = null, + allowedMovingDateRange = LocalDate(2025, 1, 1)..LocalDate(2025, 3, 1), + ), + propertyState = fakePropertyStateBRF, + movingFlowQuotes = MovingFlowQuotes( + homeQuotes = listOf( + fakeHomeQuoteWithTiersDeductibles1, + fakeHomeQuoteWithTiersDeductibles2, + fakeHomeQuoteWithTiersDeductibles3, + fakeHomeQuoteWithTiersDeductibles4, + fakeHomeQuoteWithTiersDeductibles5, + ), + mtaQuotes = emptyList(), + ), + lastSelectedHomeQuoteId = null, + oldAddressCoverageDurationDays = 30, +) + +internal val fakeMovingStateWithTwoAddons = MovingFlowState( + id = moveIntentIdFake, + moveFromAddressId = "id", + housingType = HousingType.ApartmentOwn, + addressInfo = AddressInfo("street", "18888"), + movingDateState = MovingFlowState.MovingDateState( + selectedMovingDate = null, + allowedMovingDateRange = LocalDate(2025, 1, 1)..LocalDate(2025, 3, 1), + ), + propertyState = fakePropertyStateBRF, + movingFlowQuotes = MovingFlowQuotes( + homeQuotes = listOf(fakeHomeQuoteWithAddon), + mtaQuotes = listOf( + fakeMta1, + fakeMta2WithAddon, + ), + ), + lastSelectedHomeQuoteId = null, + oldAddressCoverageDurationDays = 30, +) diff --git a/app/feature/feature-movingflow/src/test/kotlin/ui/EnterNewAddressPresenterTest.kt b/app/feature/feature-movingflow/src/test/kotlin/ui/EnterNewAddressPresenterTest.kt new file mode 100644 index 0000000000..e1e8955866 --- /dev/null +++ b/app/feature/feature-movingflow/src/test/kotlin/ui/EnterNewAddressPresenterTest.kt @@ -0,0 +1,193 @@ +package ui + +import android.text.TextUtils +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.isFalse +import assertk.assertions.isInstanceOf +import assertk.assertions.isTrue +import assertk.assertions.prop +import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.annotations.ApolloExperimental +import com.apollographql.apollo.api.Optional.Present +import com.apollographql.apollo.testing.registerTestResponse +import com.hedvig.android.apollo.octopus.test.OctopusFakeResolver +import com.hedvig.android.apollo.test.TestApolloClientRule +import com.hedvig.android.apollo.test.TestNetworkTransportType +import com.hedvig.android.feature.movingflow.compose.ValidatedInput +import com.hedvig.android.feature.movingflow.ui.enternewaddress.EnterNewAddressEvent +import com.hedvig.android.feature.movingflow.ui.enternewaddress.EnterNewAddressPresenter +import com.hedvig.android.feature.movingflow.ui.enternewaddress.EnterNewAddressUiState +import com.hedvig.android.featureflags.test.FakeFeatureManager2 +import com.hedvig.android.logger.TestLogcatLoggingRule +import com.hedvig.android.molecule.test.test +import io.mockk.every +import io.mockk.mockkStatic +import kotlinx.coroutines.test.runTest +import octopus.feature.movingflow.MoveIntentV2RequestMutation +import octopus.type.MoveApartmentSubType +import octopus.type.MoveApiVersion +import octopus.type.MoveIntentRequestInput +import octopus.type.MoveToAddressInput +import octopus.type.MoveToApartmentInput +import octopus.type.buildMoveIntent +import octopus.type.buildMoveIntentMutationOutput +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +internal class EnterNewAddressPresenterTest { + @get:Rule + val testApolloClientRule = TestApolloClientRule(TestNetworkTransportType.MAP) + + @get:Rule + val testLogcatLogger = TestLogcatLoggingRule() + + @Before + fun setUp() { + mockkStatic(TextUtils::class) + every { TextUtils.isDigitsOnly(any()) } answers { true } + } + + @OptIn(ApolloExperimental::class) + private val apolloClientWithGoodResponseForFilledData: ApolloClient + get() = testApolloClientRule.apolloClient.apply { + registerTestResponse( + operation = MoveIntentV2RequestMutation( + fakeMovingStateBeforeHomeQuotesFilled.id, + addonsFlagOn = true, + moveIntentRequestInput = MoveIntentRequestInput( + apiVersion = Present(value = MoveApiVersion.V2_TIERS_AND_DEDUCTIBLES), + moveToAddress = MoveToAddressInput( + street = fakeMovingStateBeforeHomeQuotesFilled.addressInfo.street!!, + postalCode = fakeMovingStateBeforeHomeQuotesFilled.addressInfo.postalCode!!, + ), + moveFromAddressId = fakeMovingStateBeforeHomeQuotesFilled.moveFromAddressId, + movingDate = fakeMovingStateBeforeHomeQuotesFilled.movingDateState.selectedMovingDate!!, + numberCoInsured = fakeMovingStateBeforeHomeQuotesFilled.propertyState.numberCoInsuredState.selectedNumberCoInsured, + squareMeters = fakeMovingStateBeforeHomeQuotesFilled.propertyState.squareMetersState.selectedSquareMeters!!, + apartment = com.apollographql.apollo.api.Optional.present( + MoveToApartmentInput( + subType = MoveApartmentSubType.RENT, + isStudent = false, + ), + ), + ), + ), + data = MoveIntentV2RequestMutation.Data(OctopusFakeResolver) { + moveIntentRequest = buildMoveIntentMutationOutput { + moveIntent = buildMoveIntent { + homeQuotes = emptyList() + mtaQuotes = emptyList() + } + userError = null + } + }, + ) + } + +// data = MoveIntentV2RequestMutation.Data(OctopusFakeResolver) { +// moveIntentRequest = buildMoveIntentMutationOutput { +// moveIntent = null +// userError = buildUserError { +// message = "Somehow cannot get quotes for BRF" +// } +// } +// }, + + @Test + fun `movingFlowState displays correctly`() = runTest { + val repo = FakeMovingFlowRepository() + val featureManager = FakeFeatureManager2(true) + val sut = EnterNewAddressPresenter( + apolloClient = apolloClientWithGoodResponseForFilledData, + featureManager = featureManager, + movingFlowRepository = repo, + moveIntentId = moveIntentIdFake, + ) + sut.test(EnterNewAddressUiState.Loading) { + skipItems(1) + repo.movingFlowStateTurbine.add(fakeMovingStateBeforeHomeQuotes) + val result = awaitItem() + assertThat(result).isInstanceOf(EnterNewAddressUiState.Content::class) + .prop(EnterNewAddressUiState.Content::moveFromAddressId) + .isEqualTo("moveFromAddressId") + assertThat(result).isInstanceOf(EnterNewAddressUiState.Content::class) + .prop(EnterNewAddressUiState.Content::address) + .isInstanceOf(ValidatedInput::class) + } + } + + @Test + fun `navigating to chose coverage clears navigation flag`() = runTest { + val repo = FakeMovingFlowRepository() + val featureManager = FakeFeatureManager2(true) + val sut = EnterNewAddressPresenter( + apolloClient = apolloClientWithGoodResponseForFilledData, + featureManager = featureManager, + movingFlowRepository = repo, + moveIntentId = moveIntentIdFake, + ) + sut.test(EnterNewAddressUiState.Loading) { + skipItems(1) + repo.movingFlowStateTurbine.add(fakeMovingStateBeforeHomeQuotesFilled) + skipItems(1) + sendEvent(EnterNewAddressEvent.Submit) + skipItems(1) + assertThat(awaitItem()).isInstanceOf(EnterNewAddressUiState.Content::class) + .prop(EnterNewAddressUiState.Content::navigateToChoseCoverage) + .isTrue() + sendEvent(EnterNewAddressEvent.NavigatedToChoseCoverage) + val result = awaitItem() + assertThat(result).isInstanceOf(EnterNewAddressUiState.Content::class) + .prop(EnterNewAddressUiState.Content::navigateToChoseCoverage) + .isFalse() + } + // Simulate navigation to chose coverage and verify that navigateToChoseCoverage is reset to false + } + + @Test + fun `navigating to add house information clears navigation flag`() = runTest { + // Simulate navigation to add house information and verify that navigateToAddHouseInformation is reset to false + } + + @Test + fun `dismiss submission error clears error state`() = runTest { + // Simulate dismissing a submission error and verify that submittingInfoFailure is set to null + } + + @Test + fun `submit with valid content triggers repository update`() = runTest { + // Simulate a Submit event with valid content and verify that movingFlowRepository is updated with the correct data + } + + @Test + fun `submit with not valid content does nothing`() = runTest { + // Simulate a Submit event with valid content and verify that movingFlowRepository is updated with the correct data + } + + @Test + fun `submit navigates to add house information for house property type`() = runTest { + // Simulate a Submit event with a house property type and verify that navigateToAddHouseInformation is set to true + } + + @Test + fun `submit triggers move intent request for apartment property type mapped correctly`() = runTest { + // Simulate a Submit event with an apartment property type and verify that inputForSubmission is set correctly + } + + @Test + fun `if move intent request gets good response navigate to choose coverage`() = runTest { + // Simulate a scenario where inputForSubmission is set, and verify that the mutation is executed and a successful response is handled correctly + } + + @Test + fun `if move intent request gets user error with message show error section with this message`() = runTest { + // Simulate a scenario where inputForSubmission is set, and verify that the mutation handles network failure correctly + } + + @Test + fun `if move intent request gets bad response without specific error show general error section`() = runTest { + // Simulate a scenario where inputForSubmission is set, and verify that the mutation handles user errors correctly + } +} diff --git a/app/feature/feature-movingflow/src/test/kotlin/ui/FakeMovingFlowRepository.kt b/app/feature/feature-movingflow/src/test/kotlin/ui/FakeMovingFlowRepository.kt new file mode 100644 index 0000000000..7d92f78a44 --- /dev/null +++ b/app/feature/feature-movingflow/src/test/kotlin/ui/FakeMovingFlowRepository.kt @@ -0,0 +1,52 @@ +package ui + +import app.cash.turbine.Turbine +import com.hedvig.android.feature.movingflow.data.HousingType +import com.hedvig.android.feature.movingflow.data.MovingFlowState +import com.hedvig.android.feature.movingflow.storage.MovingFlowRepository +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.datetime.LocalDate +import octopus.feature.movingflow.fragment.MoveIntentFragment +import octopus.feature.movingflow.fragment.MoveIntentQuotesFragment + +internal class FakeMovingFlowRepository : MovingFlowRepository { + val movingFlowStateTurbine = Turbine(name = "movingFlowStateTurbine") + var selectedQuoteIdParameterThatWasSentIn = "start" + val movingFlowInitiatedTurbine = Turbine(name = "movingFlowInitiatedTurbine") + + override fun movingFlowState(): Flow { + return movingFlowStateTurbine.asChannel().receiveAsFlow() + } + + override suspend fun initiateNewMovingFlow(moveIntent: MoveIntentFragment, housingType: HousingType) { + delay(300) // to imitate button loading + movingFlowInitiatedTurbine.add(true) + } + + override suspend fun updateWithPropertyInput( + movingDate: LocalDate, + address: String, + postalCode: String, + squareMeters: Int, + numberCoInsured: Int, + isStudent: Boolean, + ) {} + + override suspend fun updateWithHouseInput( + yearOfConstruction: Int, + ancillaryArea: Int, + numberOfBathrooms: Int, + isSublet: Boolean, + extraBuildings: List, + ): MovingFlowState? { + return movingFlowStateTurbine.awaitItem() + } + + override suspend fun updateWithMoveIntentQuotes(moveIntentQuotesFragment: MoveIntentQuotesFragment) {} + + override suspend fun updatePreselectedHomeQuoteId(selectedHomeQuoteId: String) { + selectedQuoteIdParameterThatWasSentIn = selectedHomeQuoteId + } +} diff --git a/app/feature/feature-movingflow/src/test/kotlin/ui/StartPresenterTest.kt b/app/feature/feature-movingflow/src/test/kotlin/ui/StartPresenterTest.kt new file mode 100644 index 0000000000..165b95ee95 --- /dev/null +++ b/app/feature/feature-movingflow/src/test/kotlin/ui/StartPresenterTest.kt @@ -0,0 +1,180 @@ +package ui + +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.isFalse +import assertk.assertions.isInstanceOf +import assertk.assertions.isTrue +import assertk.assertions.prop +import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.annotations.ApolloExperimental +import com.apollographql.apollo.testing.registerTestResponse +import com.hedvig.android.apollo.octopus.test.OctopusFakeResolver +import com.hedvig.android.apollo.test.TestApolloClientRule +import com.hedvig.android.apollo.test.TestNetworkTransportType +import com.hedvig.android.feature.movingflow.data.HousingType +import com.hedvig.android.feature.movingflow.ui.start.StartEvent +import com.hedvig.android.feature.movingflow.ui.start.StartPresenter +import com.hedvig.android.feature.movingflow.ui.start.StartUiState +import com.hedvig.android.logger.TestLogcatLoggingRule +import com.hedvig.android.molecule.test.test +import kotlinx.coroutines.test.runTest +import kotlinx.datetime.LocalDate +import octopus.feature.movingflow.MoveIntentV2CreateMutation +import octopus.feature.movingflow.fragment.MoveIntentFragment +import octopus.type.MoveExtraBuildingType +import octopus.type.buildMoveAddress +import octopus.type.buildMoveIntent +import octopus.type.buildMoveIntentMutationOutput +import octopus.type.buildUserError +import org.junit.Rule +import org.junit.Test + +internal class StartPresenterTest { + @get:Rule + val testLogcatLogger = TestLogcatLoggingRule() + + @get:Rule + val testApolloClientRule = TestApolloClientRule(TestNetworkTransportType.MAP) + + val repo = FakeMovingFlowRepository() + + @OptIn(ApolloExperimental::class) + private val apolloClientWithGoodResponse: ApolloClient + get() = testApolloClientRule.apolloClient.apply { + registerTestResponse( + operation = MoveIntentV2CreateMutation(), + data = MoveIntentV2CreateMutation.Data(OctopusFakeResolver) { + moveIntentCreate = buildMoveIntentMutationOutput { + moveIntent = buildMoveIntent { + id = "ididid" + minMovingDate = LocalDate(2026, 1, 1) + maxMovingDate = LocalDate(2025, 1, 1) + maxHouseNumberCoInsured = 6 + maxHouseSquareMeters = 200 + maxApartmentNumberCoInsured = 6 + maxApartmentSquareMeters = 200 + isApartmentAvailableforStudent = false + extraBuildingTypes = listOf(MoveExtraBuildingType.GREENHOUSE) + suggestedNumberCoInsured = 2 + currentHomeAddresses = buildList { + add( + 0, + buildMoveAddress { + id = "adsressid" + oldAddressCoverageDurationDays = 30 + }, + ) + } + } + userError = null + } + }, + ) + } + + @OptIn(ApolloExperimental::class) + private val apolloClientWithBadResponse: ApolloClient + get() = testApolloClientRule.apolloClient.apply { + registerTestResponse( + operation = MoveIntentV2CreateMutation(), + data = MoveIntentV2CreateMutation.Data(OctopusFakeResolver) { + moveIntentCreate = buildMoveIntentMutationOutput { + moveIntent = null + userError = buildUserError { + message = "Bad but readable error" + } + } + }, + ) + } + + @Test + fun `test housing type selection updates state`() = runTest { + val presenter = StartPresenter( + apolloClient = apolloClientWithGoodResponse, + movingFlowRepository = repo, + ) + presenter.test(StartUiState.Loading) { + skipItems(1) + assertThat(awaitItem()).isInstanceOf(StartUiState.Content::class) + .prop(StartUiState.Content::selectedHousingType) + .isEqualTo(HousingType.entries.first()) + sendEvent(StartEvent.SelectHousingType(HousingType.Villa)) + val result = awaitItem() + assertThat(result).isInstanceOf(StartUiState.Content::class) + .prop(StartUiState.Content::selectedHousingType) + .isEqualTo(HousingType.Villa) + } + } + + @Test + fun `test submit housing type triggers loading state of button and initiates moving flow `() = runTest { + val presenter = StartPresenter( + apolloClient = apolloClientWithGoodResponse, + movingFlowRepository = repo, + ) + presenter.test(StartUiState.Loading) { + skipItems(2) + sendEvent(StartEvent.SelectHousingType(HousingType.Villa)) + skipItems(1) + sendEvent(StartEvent.SubmitHousingType) + assertThat(awaitItem()).isInstanceOf(StartUiState.Content::class) + .prop(StartUiState.Content::buttonLoading) + .isTrue() + assertThat(repo.movingFlowInitiatedTurbine.awaitItem()).isTrue() + assertThat(awaitItem()).isInstanceOf(StartUiState.Content::class) + .prop(StartUiState.Content::navigateToNextStep) + .isTrue() + } + } + + @Test + fun `after navigating clear navigateToNextStep`() = runTest { + val presenter = StartPresenter( + apolloClient = apolloClientWithGoodResponse, + movingFlowRepository = repo, + ) + presenter.test(StartUiState.Loading) { + skipItems(2) + sendEvent(StartEvent.SelectHousingType(HousingType.Villa)) + skipItems(1) + sendEvent(StartEvent.SubmitHousingType) + skipItems(2) + sendEvent(StartEvent.NavigatedToNextStep) + assertThat(awaitItem()).isInstanceOf(StartUiState.Content::class) + .prop(StartUiState.Content::navigateToNextStep) + .isFalse() + } + } + + @Test + fun `when apollo returns error with message show error section and on dismissing error reload data`() = runTest { + val presenter = StartPresenter( + apolloClient = apolloClientWithBadResponse, + movingFlowRepository = repo, + ) + presenter.test(StartUiState.Loading) { + skipItems(1) + assertThat(awaitItem()).isInstanceOf(StartUiState.StartError.UserPresentable::class) + sendEvent(StartEvent.DismissStartError) + assertThat(awaitItem()).isInstanceOf(StartUiState.Loading::class) + skipItems(1) + } + } + + @Test + fun `when apollo return good response show correct data`() = runTest { + val presenter = StartPresenter( + apolloClient = apolloClientWithGoodResponse, + movingFlowRepository = repo, + ) + presenter.test(StartUiState.Loading) { + skipItems(1) + assertThat(awaitItem()).isInstanceOf(StartUiState.Content::class) + .prop(StartUiState.Content::initiatedMovingIntent) + .prop(MoveIntentFragment::id) + .isEqualTo("ididid") + } + } +} diff --git a/app/feature/feature-movingflow/src/test/kotlin/ui/SummaryPresenterTest.kt b/app/feature/feature-movingflow/src/test/kotlin/ui/SummaryPresenterTest.kt new file mode 100644 index 0000000000..11bccdbe74 --- /dev/null +++ b/app/feature/feature-movingflow/src/test/kotlin/ui/SummaryPresenterTest.kt @@ -0,0 +1,353 @@ +package ui + +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.isFalse +import assertk.assertions.isInstanceOf +import assertk.assertions.isNotNull +import assertk.assertions.isNull +import assertk.assertions.isTrue +import assertk.assertions.prop +import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.annotations.ApolloExperimental +import com.apollographql.apollo.testing.registerTestResponse +import com.hedvig.android.apollo.octopus.test.OctopusFakeResolver +import com.hedvig.android.apollo.test.TestApolloClientRule +import com.hedvig.android.apollo.test.TestNetworkTransportType +import com.hedvig.android.core.uidata.UiCurrencyCode +import com.hedvig.android.core.uidata.UiMoney +import com.hedvig.android.feature.movingflow.MovingFlowDestinations.Summary +import com.hedvig.android.feature.movingflow.data.HousingType +import com.hedvig.android.feature.movingflow.data.MovingFlowState +import com.hedvig.android.feature.movingflow.data.MovingFlowState.AddressInfo +import com.hedvig.android.feature.movingflow.ui.summary.SummaryEvent +import com.hedvig.android.feature.movingflow.ui.summary.SummaryInfo +import com.hedvig.android.feature.movingflow.ui.summary.SummaryPresenter +import com.hedvig.android.feature.movingflow.ui.summary.SummaryUiState +import com.hedvig.android.feature.movingflow.ui.summary.SummaryUiState.Loading +import com.hedvig.android.logger.TestLogcatLoggingRule +import com.hedvig.android.molecule.test.test +import kotlinx.coroutines.test.runTest +import kotlinx.datetime.LocalDate +import octopus.feature.movingflow.MoveIntentV2CommitMutation +import octopus.type.buildMoveIntent +import octopus.type.buildMoveIntentMutationOutput +import octopus.type.buildUserError +import org.junit.Rule +import org.junit.Test + +internal class SummaryPresenterTest { + @get:Rule + val testLogcatLogger = TestLogcatLoggingRule() + + @get:Rule + val testApolloClientRule = TestApolloClientRule(TestNetworkTransportType.MAP) + + private val summaryRoute = Summary(moveIntentIdFake, homeQuoteIdFake) + + val repo = FakeMovingFlowRepository() + + @OptIn(ApolloExperimental::class) + private val apolloClientWithGoodResponse: ApolloClient + get() = testApolloClientRule.apolloClient.apply { + registerTestResponse( + operation = MoveIntentV2CommitMutation( + intentId = moveIntentIdFake, + homeQuoteId = homeQuoteIdFake, + ), + data = MoveIntentV2CommitMutation.Data(OctopusFakeResolver) { + moveIntentCommit = buildMoveIntentMutationOutput { + moveIntent = buildMoveIntent { + id = moveIntentIdFake + } + userError = null + } + }, + ) + } + + @OptIn(ApolloExperimental::class) + private val apolloClientWithBadResponse: ApolloClient + get() = testApolloClientRule.apolloClient.apply { + registerTestResponse( + operation = MoveIntentV2CommitMutation( + intentId = moveIntentIdFake, + homeQuoteId = homeQuoteIdFake, + ), + errors = listOf(com.apollographql.apollo.api.Error.Builder(message = "Bad error message").build()), + ) + } + + @OptIn(ApolloExperimental::class) + private val apolloClientWithUserError: ApolloClient + get() = testApolloClientRule.apolloClient.apply { + registerTestResponse( + operation = MoveIntentV2CommitMutation( + intentId = moveIntentIdFake, + homeQuoteId = homeQuoteIdFake, + ), + data = MoveIntentV2CommitMutation.Data(OctopusFakeResolver) { + moveIntentCommit = buildMoveIntentMutationOutput { + userError = buildUserError { + message = "Bad user error message" + } + } + }, + ) + } + + @Test + fun `the uiState total is correctly calculated with only home quote`() = runTest { + val presenter = SummaryPresenter( + summaryRoute = summaryRoute, + movingFlowRepository = repo, + apolloClient = apolloClientWithGoodResponse, + ) + + val totalPremiumOnlyHomeQuote = fakeMovingStateWithOnlyHomeQuote.movingFlowQuotes?.homeQuotes[0]?.premium + presenter.test(Loading) { + repo.movingFlowStateTurbine.add(fakeMovingStateWithOnlyHomeQuote) + assertThat(awaitItem()).isInstanceOf(SummaryUiState.Loading::class) + val state = awaitItem() + assertThat(state) + .isInstanceOf(SummaryUiState.Content::class) + .prop(SummaryUiState.Content::summaryInfo) + .prop(SummaryInfo::totalPremium) + .isEqualTo(totalPremiumOnlyHomeQuote) + } + } + + @Test + fun `the uiState total is correctly calculated with no addons`() = runTest { + val presenter = SummaryPresenter( + summaryRoute = summaryRoute, + movingFlowRepository = repo, + apolloClient = apolloClientWithGoodResponse, + ) + val noAddonsAmount = listOf( + fakeMovingStateNoAddons.movingFlowQuotes!!.homeQuotes[0].premium.amount, + fakeMovingStateNoAddons.movingFlowQuotes.mtaQuotes[0].premium.amount, + fakeMovingStateNoAddons.movingFlowQuotes.mtaQuotes[1].premium.amount, + ).sum() + val totalPremiumNoAddons = UiMoney(noAddonsAmount, UiCurrencyCode.SEK) + presenter.test(Loading) { + repo.movingFlowStateTurbine.add(fakeMovingStateNoAddons) + assertThat(awaitItem()).isInstanceOf(SummaryUiState.Loading::class) + val state = awaitItem() + assertThat(state) + .isInstanceOf(SummaryUiState.Content::class) + .prop(SummaryUiState.Content::summaryInfo) + .prop(SummaryInfo::totalPremium) + .isEqualTo(totalPremiumNoAddons) + } + } + + @Test + fun `the uiState total is correctly calculated with 2 addons`() = runTest { + val presenter = SummaryPresenter( + summaryRoute = summaryRoute, + movingFlowRepository = repo, + apolloClient = apolloClientWithGoodResponse, + ) + + val twoAddonsAmount = listOf( + fakeMovingStateWithTwoAddons.movingFlowQuotes!!.homeQuotes[0].premium.amount, + fakeMovingStateWithTwoAddons.movingFlowQuotes.homeQuotes[0].relatedAddonQuotes[0].premium.amount, + fakeMovingStateWithTwoAddons.movingFlowQuotes.mtaQuotes[0].premium.amount, + fakeMovingStateWithTwoAddons.movingFlowQuotes.mtaQuotes[1].premium.amount, + fakeMovingStateWithTwoAddons.movingFlowQuotes.mtaQuotes[1].relatedAddonQuotes[0].premium.amount, + ).sum() + val totalPremiumTwoAddons = UiMoney(twoAddonsAmount, UiCurrencyCode.SEK) + presenter.test(Loading) { + repo.movingFlowStateTurbine.add(fakeMovingStateWithTwoAddons) + assertThat(awaitItem()).isInstanceOf(SummaryUiState.Loading::class) + val state = awaitItem() + assertThat(state) + .isInstanceOf(SummaryUiState.Content::class) + .prop(SummaryUiState.Content::summaryInfo) + .prop(SummaryInfo::totalPremium) + .isEqualTo(totalPremiumTwoAddons) + } + } + + @Test + fun `if submit ends with error show error dialog`() = runTest { + val presenter = SummaryPresenter( + summaryRoute = summaryRoute, + movingFlowRepository = repo, + apolloClient = apolloClientWithBadResponse, + ) + presenter.test(Loading) { + repo.movingFlowStateTurbine.add(fakeMovingStateWithTwoAddons) + sendEvent(SummaryEvent.ConfirmChanges) + skipItems(3) + val state = awaitItem() + assertThat(state) + .isInstanceOf(SummaryUiState.Content::class) + .prop(SummaryUiState.Content::submitError) + .isNotNull() + assertThat(state) + .isInstanceOf(SummaryUiState.Content::class) + .prop(SummaryUiState.Content::navigateToFinishedScreenWithDate) + .isNull() + } + } + + @Test + fun `if submit ends with user error show error dialog`() = runTest { + val presenter = SummaryPresenter( + summaryRoute = summaryRoute, + movingFlowRepository = repo, + apolloClient = apolloClientWithUserError, + ) + presenter.test(Loading) { + repo.movingFlowStateTurbine.add(fakeMovingStateWithTwoAddons) + sendEvent(SummaryEvent.ConfirmChanges) + skipItems(3) + val state = awaitItem() + assertThat(state) + .isInstanceOf(SummaryUiState.Content::class) + .prop(SummaryUiState.Content::submitError) + .isNotNull() + assertThat(state) + .isInstanceOf(SummaryUiState.Content::class) + .prop(SummaryUiState.Content::navigateToFinishedScreenWithDate) + .isNull() + } + } + + @Test + fun `if submit ends with success navigate further`() = runTest { + val presenter = SummaryPresenter( + summaryRoute = summaryRoute, + movingFlowRepository = repo, + apolloClient = apolloClientWithGoodResponse, + ) + presenter.test(Loading) { + repo.movingFlowStateTurbine.add(fakeMovingStateWithTwoAddons) + sendEvent(SummaryEvent.ConfirmChanges) + skipItems(3) + val state = awaitItem() + assertThat(state) + .isInstanceOf(SummaryUiState.Content::class) + .prop(SummaryUiState.Content::submitError) + .isNull() + assertThat(state) + .isInstanceOf(SummaryUiState.Content::class) + .prop(SummaryUiState.Content::navigateToFinishedScreenWithDate) + .isNotNull() + } + } + + @Test + fun `if the matching quote is found in the repo show correct content`() = runTest { + val presenter = SummaryPresenter( + summaryRoute = summaryRoute, + movingFlowRepository = repo, + apolloClient = apolloClientWithGoodResponse, + ) + presenter.test(Loading) { + repo.movingFlowStateTurbine.add(fakeMovingStateWithTwoAddons) + skipItems(1) + val state = awaitItem() + assertThat(state) + .isInstanceOf(SummaryUiState.Content::class) + .prop(SummaryUiState.Content::summaryInfo) + .isEqualTo(fakeSummaryInfoWithTwoAddons) + } + } + + @Test + fun `if the quote with this id is not in the repo show error screen`() = runTest { + val presenter = SummaryPresenter( + summaryRoute = Summary("bad_id", "even_worse_id"), + movingFlowRepository = repo, + apolloClient = apolloClientWithGoodResponse, + ) + presenter.test(Loading) { + repo.movingFlowStateTurbine.add(fakeMovingStateWithTwoAddons) + skipItems(1) + val state = awaitItem() + assertThat(state) + .isInstanceOf(SummaryUiState.Error::class) + } + } + + @Test + fun `if there are no quotes in the repo show error screen`() = runTest { + val presenter = SummaryPresenter( + summaryRoute = summaryRoute, + movingFlowRepository = repo, + apolloClient = apolloClientWithGoodResponse, + ) + presenter.test(Loading) { + repo.movingFlowStateTurbine.add( + MovingFlowState( + id = moveIntentIdFake, + moveFromAddressId = "id", + housingType = HousingType.ApartmentOwn, + addressInfo = AddressInfo("street", "18888"), + movingDateState = MovingFlowState.MovingDateState( + selectedMovingDate = null, + allowedMovingDateRange = LocalDate(2025, 1, 1)..LocalDate(2025, 3, 1), + ), + propertyState = fakePropertyStateBRF, + movingFlowQuotes = null, + lastSelectedHomeQuoteId = null, + oldAddressCoverageDurationDays = 30, + ), + ) + skipItems(1) + val state = awaitItem() + assertThat(state) + .isInstanceOf(SummaryUiState.Error::class) + } + } + + @Test + fun `when click on confirm button launch submitting quotes and show loading button`() = runTest { + val presenter = SummaryPresenter( + summaryRoute = summaryRoute, + movingFlowRepository = repo, + apolloClient = apolloClientWithGoodResponse, + ) + presenter.test(Loading) { + repo.movingFlowStateTurbine.add(fakeMovingStateWithTwoAddons) + skipItems(1) + val state1 = awaitItem() + assertThat(state1) + .isInstanceOf(SummaryUiState.Content::class) + .prop(SummaryUiState.Content::isSubmitting).isFalse() + sendEvent(SummaryEvent.ConfirmChanges) + val state2 = awaitItem() + assertThat(state2) + .isInstanceOf(SummaryUiState.Content::class) + .prop(SummaryUiState.Content::isSubmitting).isTrue() + val state3 = awaitItem() + assertThat(state3) + .isInstanceOf(SummaryUiState.Content::class) + .prop(SummaryUiState.Content::isSubmitting).isFalse() + } + } + + @Test + fun `when click on close error dialog it does not show`() = runTest { + val presenter = SummaryPresenter( + summaryRoute = summaryRoute, + movingFlowRepository = repo, + apolloClient = apolloClientWithBadResponse, + ) + presenter.test(Loading) { + repo.movingFlowStateTurbine.add(fakeMovingStateWithTwoAddons) + sendEvent(SummaryEvent.ConfirmChanges) + skipItems(3) + assertThat(awaitItem()) + .isInstanceOf(SummaryUiState.Content::class) + .prop(SummaryUiState.Content::submitError).isNotNull() + sendEvent(SummaryEvent.DismissSubmissionError) + assertThat(awaitItem()) + .isInstanceOf(SummaryUiState.Content::class) + .prop(SummaryUiState.Content::submitError).isNull() + } + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e5c227ed2b..238ef3e9d9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -61,6 +61,7 @@ junit = "4.13.2" koinBom = "4.0.0" kotlinx-serialization = "1.7.3" kotlinxDatetime = "0.6.1" +mockk = "1.13.16" modalSheet = "0.7.0" moneta = "1.4.4" navigationRecentsUrlSharing = "1.0.0" @@ -173,6 +174,7 @@ kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version. kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinx-serialization" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } ktor = { module = "io.ktor:ktor-client-android", version.ref = "ktor" } +mockk = { module = "io.mockk:mockk", version.ref = "mockk" } modal-sheet = { module = "io.github.oleksandrbalan:modalsheet", version.ref = "modalSheet" } molecule = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" } navigationRecentsUrlSharing = { module = "com.stylianosgakis.navigation.recents.url.sharing:navigation-recents-url-sharing", version.ref = "navigationRecentsUrlSharing" }