From 729df51c513cdd676ffc4e974dcf6a3960d55cac Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Tue, 17 Dec 2024 09:51:51 +0100 Subject: [PATCH 1/6] remove extra padding --- .../feature/terminateinsurance/ui/TerminationScaffold.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/ui/TerminationScaffold.kt b/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/ui/TerminationScaffold.kt index e8d41b0b31..52526b25ca 100644 --- a/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/ui/TerminationScaffold.kt +++ b/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/ui/TerminationScaffold.kt @@ -137,7 +137,6 @@ private fun ExplanationBottomSheet(onDismiss: () -> Unit, text: String, isVisibl text = stringResource(id = R.string.TERMINATION_FLOW_CANCEL_INFO_TITLE), modifier = Modifier .fillMaxWidth() - .padding(horizontal = 24.dp), ) Spacer(Modifier.height(8.dp)) HedvigText( @@ -145,7 +144,6 @@ private fun ExplanationBottomSheet(onDismiss: () -> Unit, text: String, isVisibl color = HedvigTheme.colorScheme.textSecondary, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 24.dp), ) HedvigButton( onClick = onDismiss, From f99046a4935da3ca82e2cba55e7312dbe90cc356 Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Tue, 17 Dec 2024 10:19:27 +0100 Subject: [PATCH 2/6] SubmitAddonPurchaseUseCaseImplTest --- .../SubmitAddonPurchaseUseCaseImplTest.kt | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 app/feature/feature-addon-purchase/src/test/kotlin/data/SubmitAddonPurchaseUseCaseImplTest.kt diff --git a/app/feature/feature-addon-purchase/src/test/kotlin/data/SubmitAddonPurchaseUseCaseImplTest.kt b/app/feature/feature-addon-purchase/src/test/kotlin/data/SubmitAddonPurchaseUseCaseImplTest.kt new file mode 100644 index 0000000000..005c198c1c --- /dev/null +++ b/app/feature/feature-addon-purchase/src/test/kotlin/data/SubmitAddonPurchaseUseCaseImplTest.kt @@ -0,0 +1,95 @@ +package data + +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.prop +import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.annotations.ApolloExperimental +import com.apollographql.apollo.api.Error +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.common.ErrorMessage +import com.hedvig.android.core.common.test.isLeft +import com.hedvig.android.core.common.test.isRight +import com.hedvig.android.feature.addon.purchase.data.SubmitAddonPurchaseUseCaseImpl +import com.hedvig.android.logger.TestLogcatLoggingRule +import kotlinx.coroutines.test.runTest +import octopus.UpsellTravelAddonActivateMutation +import octopus.type.buildUpsellTravelAddonActivationOutput +import octopus.type.buildUserError +import org.junit.Rule +import org.junit.Test + +class SubmitAddonPurchaseUseCaseImplTest { + @get:Rule + val testLogcatLogger = TestLogcatLoggingRule() + + @get:Rule + val testApolloClientRule = TestApolloClientRule(TestNetworkTransportType.MAP) + + val testId = "jhjhjh" + + @OptIn(ApolloExperimental::class) + private val apolloClientWithGoodResponseNullError: ApolloClient + get() = testApolloClientRule.apolloClient.apply { + registerTestResponse( + operation = UpsellTravelAddonActivateMutation(testId, testId), + data = UpsellTravelAddonActivateMutation.Data(OctopusFakeResolver) { + upsellTravelAddonActivate = buildUpsellTravelAddonActivationOutput { + userError = null + } + }, + ) + } + + @OptIn(ApolloExperimental::class) + private val apolloClientWithUserError: ApolloClient + get() = testApolloClientRule.apolloClient.apply { + registerTestResponse( + operation = UpsellTravelAddonActivateMutation(testId, testId), + data = UpsellTravelAddonActivateMutation.Data(OctopusFakeResolver) { + upsellTravelAddonActivate = buildUpsellTravelAddonActivationOutput { + userError = buildUserError { + message = "Bad message" + } + } + }, + ) + } + + @OptIn(ApolloExperimental::class) + private val apolloClientWithBadResponse: ApolloClient + get() = testApolloClientRule.apolloClient.apply { + registerTestResponse( + operation = UpsellTravelAddonActivateMutation(testId, testId), + data = null, + errors = listOf(Error.Builder(message = "Bad message").build()), + ) + } + + @Test + fun `if BE response is good return Unit`() = runTest { + val sut = SubmitAddonPurchaseUseCaseImpl(apolloClientWithGoodResponseNullError) + val result = sut.invoke(testId, testId) + assertThat(result) + .isRight().isEqualTo(Unit) + } + + @Test + fun `if BE response is UserError return ErrorMessage with the msg from BE`() = runTest { + val sut = SubmitAddonPurchaseUseCaseImpl(apolloClientWithUserError) + val result = sut.invoke(testId, testId) + assertThat(result) + .isLeft().prop(ErrorMessage::message).isEqualTo("Bad message") + } + + @Test + fun `if BE response is error return ErrorMessage with null message`() = runTest { + val sut = SubmitAddonPurchaseUseCaseImpl(apolloClientWithBadResponse) + val result = sut.invoke(testId, testId) + assertThat(result) + .isLeft().prop(ErrorMessage::message).isEqualTo(null) + } +} From d4a59fb725a4e28db180b95fc3d1b2c950ca22e6 Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Tue, 17 Dec 2024 11:18:06 +0100 Subject: [PATCH 3/6] SelectInsuranceForAddonPresenterTest --- .../SelectInsuranceForAddonPresenterTest.kt | 173 ++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 app/feature/feature-addon-purchase/src/test/kotlin/ui/SelectInsuranceForAddonPresenterTest.kt diff --git a/app/feature/feature-addon-purchase/src/test/kotlin/ui/SelectInsuranceForAddonPresenterTest.kt b/app/feature/feature-addon-purchase/src/test/kotlin/ui/SelectInsuranceForAddonPresenterTest.kt new file mode 100644 index 0000000000..9ffa4ac111 --- /dev/null +++ b/app/feature/feature-addon-purchase/src/test/kotlin/ui/SelectInsuranceForAddonPresenterTest.kt @@ -0,0 +1,173 @@ +package ui + + +import app.cash.turbine.Turbine +import arrow.core.Either +import arrow.core.left +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.data.contract.ContractGroup +import com.hedvig.android.feature.addon.purchase.data.GetInsuranceForTravelAddonUseCase +import com.hedvig.android.feature.addon.purchase.data.InsuranceForAddon +import com.hedvig.android.feature.addon.purchase.ui.selectinsurance.SelectInsuranceForAddonEvent +import com.hedvig.android.feature.addon.purchase.ui.selectinsurance.SelectInsuranceForAddonPresenter +import com.hedvig.android.feature.addon.purchase.ui.selectinsurance.SelectInsuranceForAddonState +import com.hedvig.android.logger.TestLogcatLoggingRule +import com.hedvig.android.molecule.test.test +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class SelectInsuranceForAddonPresenterTest { + @get:Rule + val testLogcatLogger = TestLogcatLoggingRule() + + val testId = "test1" + val testIds = listOf(testId, "test2") + val emptyIds = listOf() + val listWithLonelyId = listOf(testId) + + @Test + fun `if receive error instead of list of insurances show error screen`() = runTest { + val useCase = FakeGetInsuranceForTravelAddonUseCase() + val presenter = SelectInsuranceForAddonPresenter( + getInsuranceForTravelAddonUseCase = useCase, + ids = testIds, + ) + presenter.test(SelectInsuranceForAddonState.Loading) { + skipItems(1) + useCase.turbine.add(flowOf(ErrorMessage().left())) + val state = awaitItem() + assertThat(state).isInstanceOf(SelectInsuranceForAddonState.Failure::class) + } + } + + @Test + fun `if ids are empty show error screen without loading anything`() = runTest { + val useCase = FakeGetInsuranceForTravelAddonUseCase() + val presenter = SelectInsuranceForAddonPresenter( + getInsuranceForTravelAddonUseCase = useCase, + ids = emptyIds, + ) + presenter.test(SelectInsuranceForAddonState.Loading) { + skipItems(1) + val state = awaitItem() + assertThat(state).isInstanceOf(SelectInsuranceForAddonState.Failure::class) + } + } + + + @Test + fun `if id list have only 1 item navigate further without loading anything`() = runTest { + val useCase = FakeGetInsuranceForTravelAddonUseCase() + val presenter = SelectInsuranceForAddonPresenter( + getInsuranceForTravelAddonUseCase = useCase, + ids = listWithLonelyId, + ) + presenter.test(SelectInsuranceForAddonState.Loading) { + skipItems(1) + val state = awaitItem() + assertThat(state).isEqualTo( + SelectInsuranceForAddonState.Success( + listOfInsurances = emptyList(), + insuranceIdToContinue = listWithLonelyId[0], + currentlySelected = null, + ), + ) + } + } + + @Test + fun `if receive more than one customisable insurance show list of insurances to choose from`() = runTest { + val useCase = FakeGetInsuranceForTravelAddonUseCase() + val presenter = SelectInsuranceForAddonPresenter( + getInsuranceForTravelAddonUseCase = useCase, + ids = testIds, + ) + presenter.test(SelectInsuranceForAddonState.Loading) { + skipItems(1) + useCase.turbine.add(flowOf(listOfInsurances.right())) + val state = awaitItem() + assertThat(state).isInstanceOf(SelectInsuranceForAddonState.Success::class) + .prop(SelectInsuranceForAddonState.Success::listOfInsurances).isEqualTo(listOfInsurances) + } + } + + @Test + fun `if receive more than one customisable insurance none is pre-chosen and continue button is disabled`() = runTest { + val useCase = FakeGetInsuranceForTravelAddonUseCase() + val presenter = SelectInsuranceForAddonPresenter( + getInsuranceForTravelAddonUseCase = useCase, + ids = testIds, + ) + presenter.test(SelectInsuranceForAddonState.Loading) { + skipItems(1) + useCase.turbine.add(flowOf(listOfInsurances.right())) + val state = awaitItem() + assertThat(state).isInstanceOf(SelectInsuranceForAddonState.Success::class) + .prop(SelectInsuranceForAddonState.Success::currentlySelected).isEqualTo(null) + } + } + + @Test + fun `when insurance is chosen enable continue button`() = runTest { + val useCase = FakeGetInsuranceForTravelAddonUseCase() + val presenter = SelectInsuranceForAddonPresenter( + getInsuranceForTravelAddonUseCase = useCase, + ids = testIds, + ) + presenter.test(SelectInsuranceForAddonState.Loading) { + useCase.turbine.add(flowOf(listOfInsurances.right())) + skipItems(2) + sendEvent(SelectInsuranceForAddonEvent.SelectInsurance(listOfInsurances[0])) + assertThat(awaitItem()).isInstanceOf(SelectInsuranceForAddonState.Success::class) + .prop(SelectInsuranceForAddonState.Success::currentlySelected).isEqualTo(listOfInsurances[0]) + } + } + + @Test + fun `on continue navigate further with chosen insurance id`() = runTest { + val useCase = FakeGetInsuranceForTravelAddonUseCase() + val presenter = SelectInsuranceForAddonPresenter( + getInsuranceForTravelAddonUseCase = useCase, + ids = testIds, + ) + presenter.test(SelectInsuranceForAddonState.Loading) { + useCase.turbine.add(flowOf(listOfInsurances.right())) + sendEvent(SelectInsuranceForAddonEvent.SelectInsurance(listOfInsurances[0])) + skipItems(3) + sendEvent(SelectInsuranceForAddonEvent.SubmitSelected(listOfInsurances[0])) + assertThat(awaitItem()).isInstanceOf(SelectInsuranceForAddonState.Success::class) + .prop(SelectInsuranceForAddonState.Success::insuranceIdToContinue).isEqualTo(listOfInsurances[0].id) + } + } +} + +private class FakeGetInsuranceForTravelAddonUseCase() : GetInsuranceForTravelAddonUseCase { + val turbine = Turbine>>>() + + override suspend fun invoke(ids: List): Flow>> { + return turbine.awaitItem() + } +} + +private val listOfInsurances = listOf( + InsuranceForAddon( + "id", + "displayName", + "ExposureName", + ContractGroup.RENTAL, + ), + InsuranceForAddon( + "id2", + "displayName2", + "ExposureName2", + ContractGroup.HOMEOWNER, + ), +) From 41964765e02a7a47edb64cf7c5ec83d4f72ce1e0 Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Tue, 17 Dec 2024 15:09:58 +0100 Subject: [PATCH 4/6] fix selectedOptionInDialog not restoring --- .../CustomizeTravelAddonViewModel.kt | 10 +- .../ui/CustomizeTravelAddonPresenterTest.kt | 119 ++++++++++++++++++ .../SelectInsuranceForAddonPresenterTest.kt | 2 - .../ui/TerminationScaffold.kt | 4 +- 4 files changed, 130 insertions(+), 5 deletions(-) create mode 100644 app/feature/feature-addon-purchase/src/test/kotlin/ui/CustomizeTravelAddonPresenterTest.kt diff --git a/app/feature/feature-addon-purchase/src/main/kotlin/com/hedvig/android/feature/addon/purchase/ui/customize/CustomizeTravelAddonViewModel.kt b/app/feature/feature-addon-purchase/src/main/kotlin/com/hedvig/android/feature/addon/purchase/ui/customize/CustomizeTravelAddonViewModel.kt index 5bd0374d5b..aa09b597d4 100644 --- a/app/feature/feature-addon-purchase/src/main/kotlin/com/hedvig/android/feature/addon/purchase/ui/customize/CustomizeTravelAddonViewModel.kt +++ b/app/feature/feature-addon-purchase/src/main/kotlin/com/hedvig/android/feature/addon/purchase/ui/customize/CustomizeTravelAddonViewModel.kt @@ -38,7 +38,15 @@ internal class CustomizeTravelAddonPresenter( ): CustomizeTravelAddonState { var currentState by remember { mutableStateOf(lastState) } var loadIteration by remember { mutableIntStateOf(0) } - var selectedOptionInDialog by remember { mutableStateOf(null) } + var selectedOptionInDialog by remember { + mutableStateOf( + if (lastState is CustomizeTravelAddonState.Success) { + lastState.currentlyChosenOptionInDialog + } else { + null + }, + ) + } CollectEvents { event -> when (event) { CustomizeTravelAddonEvent.Reload -> loadIteration++ diff --git a/app/feature/feature-addon-purchase/src/test/kotlin/ui/CustomizeTravelAddonPresenterTest.kt b/app/feature/feature-addon-purchase/src/test/kotlin/ui/CustomizeTravelAddonPresenterTest.kt new file mode 100644 index 0000000000..a8a517351d --- /dev/null +++ b/app/feature/feature-addon-purchase/src/test/kotlin/ui/CustomizeTravelAddonPresenterTest.kt @@ -0,0 +1,119 @@ +package ui + +import app.cash.turbine.Turbine +import arrow.core.Either +import arrow.core.left +import arrow.core.nonEmptyListOf +import assertk.assertThat +import assertk.assertions.isInstanceOf +import com.hedvig.android.core.common.ErrorMessage +import com.hedvig.android.core.uidata.UiCurrencyCode +import com.hedvig.android.core.uidata.UiMoney +import com.hedvig.android.feature.addon.purchase.data.Addon +import com.hedvig.android.feature.addon.purchase.data.Addon.TravelAddonOffer +import com.hedvig.android.feature.addon.purchase.data.AddonVariant +import com.hedvig.android.feature.addon.purchase.data.GetTravelAddonOfferUseCase +import com.hedvig.android.feature.addon.purchase.data.TravelAddonQuote +import com.hedvig.android.feature.addon.purchase.ui.customize.CustomizeTravelAddonPresenter +import com.hedvig.android.feature.addon.purchase.ui.customize.CustomizeTravelAddonState +import com.hedvig.android.logger.TestLogcatLoggingRule +import com.hedvig.android.molecule.test.test +import kotlinx.coroutines.test.runTest +import kotlinx.datetime.LocalDate +import org.junit.Rule +import org.junit.Test + +class CustomizeTravelAddonPresenterTest { + @get:Rule + val testLogcatLogger = TestLogcatLoggingRule() + + val insuranceId = "test" + + @Test + fun `if receive error show error screen`() = runTest { + val useCase = FakeGetTravelAddonOfferUseCase() + val presenter = CustomizeTravelAddonPresenter( + getTravelAddonOfferUseCase = useCase, + insuranceId = insuranceId, + ) + presenter.test(CustomizeTravelAddonState.Loading) { + skipItems(1) + useCase.turbine.add(ErrorMessage().left()) + val state = awaitItem() + assertThat(state).isInstanceOf(CustomizeTravelAddonState.Failure::class) + } + } + + @Test + fun `do not trigger reload if returning from success last state`() = runTest { + val useCase = FakeGetTravelAddonOfferUseCase() + val presenter = CustomizeTravelAddonPresenter( + getTravelAddonOfferUseCase = useCase, + insuranceId = insuranceId, + ) + presenter.test( + CustomizeTravelAddonState.Success( + travelAddonOffer = TravelAddonOffer( + addonOptions = nonEmptyListOf(fakeTravelAddonQuote1, fakeTravelAddonQuote2), + title = "", + description = "", + activationDate = LocalDate(2025, 1, 1), + currentTravelAddon = null, + ), + currentlyChosenOption = fakeTravelAddonQuote1, + currentlyChosenOptionInDialog = fakeTravelAddonQuote1, + ), + ) { + skipItems(1) + useCase.turbine.add(ErrorMessage().left()) + expectNoEvents() + } + } +} + +private class FakeGetTravelAddonOfferUseCase() : GetTravelAddonOfferUseCase { + val turbine = Turbine>() + + override suspend fun invoke(id: String): Either { + return turbine.awaitItem() + } +} + +private val fakeTravelAddonQuote1 = TravelAddonQuote( + quoteId = "id", + addonId = "addonId1", + displayName = "45 days", + addonVariant = AddonVariant( + termsVersion = "terms", + documents = listOf(), + displayDetails = listOf(), + ), + price = UiMoney( + 49.0, + UiCurrencyCode.SEK, + ), +) +private val fakeTravelAddonQuote2 = TravelAddonQuote( + displayName = "60 days", + addonId = "addonId1", + quoteId = "id", + addonVariant = AddonVariant( + termsVersion = "terms", + documents = listOf(), + displayDetails = listOf(), + ), + price = UiMoney( + 60.0, + UiCurrencyCode.SEK, + ), +) +private val fakeTravelAddon = TravelAddonOffer( + addonOptions = nonEmptyListOf( + fakeTravelAddonQuote1, + fakeTravelAddonQuote2, + ), + title = "Travel Plus", + description = "For those who travel often: luggage protection and 24/7 assistance worldwide", + activationDate = LocalDate(2024, 12, 30), + currentTravelAddon = null, +) diff --git a/app/feature/feature-addon-purchase/src/test/kotlin/ui/SelectInsuranceForAddonPresenterTest.kt b/app/feature/feature-addon-purchase/src/test/kotlin/ui/SelectInsuranceForAddonPresenterTest.kt index 9ffa4ac111..7378660b69 100644 --- a/app/feature/feature-addon-purchase/src/test/kotlin/ui/SelectInsuranceForAddonPresenterTest.kt +++ b/app/feature/feature-addon-purchase/src/test/kotlin/ui/SelectInsuranceForAddonPresenterTest.kt @@ -1,6 +1,5 @@ package ui - import app.cash.turbine.Turbine import arrow.core.Either import arrow.core.left @@ -62,7 +61,6 @@ class SelectInsuranceForAddonPresenterTest { } } - @Test fun `if id list have only 1 item navigate further without loading anything`() = runTest { val useCase = FakeGetInsuranceForTravelAddonUseCase() diff --git a/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/ui/TerminationScaffold.kt b/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/ui/TerminationScaffold.kt index 52526b25ca..8b21b04713 100644 --- a/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/ui/TerminationScaffold.kt +++ b/app/feature/feature-terminate-insurance/src/main/kotlin/com/hedvig/android/feature/terminateinsurance/ui/TerminationScaffold.kt @@ -136,14 +136,14 @@ private fun ExplanationBottomSheet(onDismiss: () -> Unit, text: String, isVisibl HedvigText( text = stringResource(id = R.string.TERMINATION_FLOW_CANCEL_INFO_TITLE), modifier = Modifier - .fillMaxWidth() + .fillMaxWidth(), ) Spacer(Modifier.height(8.dp)) HedvigText( text = text, color = HedvigTheme.colorScheme.textSecondary, modifier = Modifier - .fillMaxWidth() + .fillMaxWidth(), ) HedvigButton( onClick = onDismiss, From f7cfc150e6ffbe192537b433323a8cb63046921f Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Tue, 17 Dec 2024 16:09:55 +0100 Subject: [PATCH 5/6] CustomizeTravelAddonPresenterTest --- .../ui/CustomizeTravelAddonPresenterTest.kt | 165 +++++++++++++++++- 1 file changed, 156 insertions(+), 9 deletions(-) diff --git a/app/feature/feature-addon-purchase/src/test/kotlin/ui/CustomizeTravelAddonPresenterTest.kt b/app/feature/feature-addon-purchase/src/test/kotlin/ui/CustomizeTravelAddonPresenterTest.kt index a8a517351d..209fe99283 100644 --- a/app/feature/feature-addon-purchase/src/test/kotlin/ui/CustomizeTravelAddonPresenterTest.kt +++ b/app/feature/feature-addon-purchase/src/test/kotlin/ui/CustomizeTravelAddonPresenterTest.kt @@ -4,16 +4,23 @@ import app.cash.turbine.Turbine import arrow.core.Either import arrow.core.left import arrow.core.nonEmptyListOf +import arrow.core.right 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.prop import com.hedvig.android.core.common.ErrorMessage import com.hedvig.android.core.uidata.UiCurrencyCode import com.hedvig.android.core.uidata.UiMoney -import com.hedvig.android.feature.addon.purchase.data.Addon import com.hedvig.android.feature.addon.purchase.data.Addon.TravelAddonOffer import com.hedvig.android.feature.addon.purchase.data.AddonVariant import com.hedvig.android.feature.addon.purchase.data.GetTravelAddonOfferUseCase import com.hedvig.android.feature.addon.purchase.data.TravelAddonQuote +import com.hedvig.android.feature.addon.purchase.navigation.SummaryParameters +import com.hedvig.android.feature.addon.purchase.ui.customize.CustomizeTravelAddonEvent import com.hedvig.android.feature.addon.purchase.ui.customize.CustomizeTravelAddonPresenter import com.hedvig.android.feature.addon.purchase.ui.customize.CustomizeTravelAddonState import com.hedvig.android.logger.TestLogcatLoggingRule @@ -53,13 +60,7 @@ class CustomizeTravelAddonPresenterTest { ) presenter.test( CustomizeTravelAddonState.Success( - travelAddonOffer = TravelAddonOffer( - addonOptions = nonEmptyListOf(fakeTravelAddonQuote1, fakeTravelAddonQuote2), - title = "", - description = "", - activationDate = LocalDate(2025, 1, 1), - currentTravelAddon = null, - ), + travelAddonOffer = fakeTravelOfferTwoOptions, currentlyChosenOption = fakeTravelAddonQuote1, currentlyChosenOptionInDialog = fakeTravelAddonQuote1, ), @@ -69,6 +70,142 @@ class CustomizeTravelAddonPresenterTest { expectNoEvents() } } + + + @Test + fun `if receive good response but only one addon redirect to next screen and pop this destination`() = runTest { + val useCase = FakeGetTravelAddonOfferUseCase() + val presenter = CustomizeTravelAddonPresenter( + getTravelAddonOfferUseCase = useCase, + insuranceId = insuranceId, + ) + presenter.test( + CustomizeTravelAddonState.Loading, + ) { + skipItems(1) + useCase.turbine.add(fakeTravelOfferOnlyOneOption.right()) + val state = awaitItem() + assertThat(state).isInstanceOf(CustomizeTravelAddonState.Success::class) + .apply { + prop(CustomizeTravelAddonState.Success::summaryParamsToNavigateFurther) + .isEqualTo( + SummaryParameters( + offerDisplayName = fakeTravelOfferOnlyOneOption.title, + quote = fakeTravelOfferOnlyOneOption.addonOptions[0], + activationDate = fakeTravelOfferOnlyOneOption.activationDate, + currentTravelAddon = fakeTravelOfferOnlyOneOption.currentTravelAddon, + popCustomizeDestination = true, + ), + ) + } + + } + } + + @Test + fun `if receive good response return correct data, pre-choose first addon and do not navigate further`() = runTest { + val useCase = FakeGetTravelAddonOfferUseCase() + val presenter = CustomizeTravelAddonPresenter( + getTravelAddonOfferUseCase = useCase, + insuranceId = insuranceId, + ) + presenter.test( + CustomizeTravelAddonState.Loading, + ) { + skipItems(1) + useCase.turbine.add(fakeTravelOfferTwoOptions.right()) + val state = awaitItem() + assertThat(state).isInstanceOf(CustomizeTravelAddonState.Success::class) + .apply { + prop(CustomizeTravelAddonState.Success::summaryParamsToNavigateFurther) + .isNull() + prop(CustomizeTravelAddonState.Success::travelAddonOffer).isEqualTo(fakeTravelOfferTwoOptions) + prop(CustomizeTravelAddonState.Success::currentlyChosenOption).isEqualTo(fakeTravelOfferTwoOptions.addonOptions[0]) + prop(CustomizeTravelAddonState.Success::currentlyChosenOptionInDialog).isEqualTo(fakeTravelOfferTwoOptions.addonOptions[0]) + } + + } + } + + @Test + fun `if choose option in dialog show it in the dialog ui but do not change currently chosen until select button is not clicked`() = + runTest { + val useCase = FakeGetTravelAddonOfferUseCase() + val presenter = CustomizeTravelAddonPresenter( + getTravelAddonOfferUseCase = useCase, + insuranceId = insuranceId, + ) + presenter.test( + CustomizeTravelAddonState.Loading, + ) { + useCase.turbine.add(fakeTravelOfferTwoOptions.right()) + skipItems(2) + sendEvent(CustomizeTravelAddonEvent.ChooseOptionInDialog(fakeTravelAddonQuote2)) + assertThat(awaitItem()).isInstanceOf(CustomizeTravelAddonState.Success::class) + .apply { + prop(CustomizeTravelAddonState.Success::currentlyChosenOption).isEqualTo(fakeTravelAddonQuote1) + prop(CustomizeTravelAddonState.Success::currentlyChosenOptionInDialog).isEqualTo(fakeTravelAddonQuote2) + } + sendEvent(CustomizeTravelAddonEvent.ChooseSelectedOption) + assertThat(awaitItem()).isInstanceOf(CustomizeTravelAddonState.Success::class) + .apply { + prop(CustomizeTravelAddonState.Success::currentlyChosenOption).isEqualTo(fakeTravelAddonQuote2) + prop(CustomizeTravelAddonState.Success::currentlyChosenOptionInDialog).isEqualTo(fakeTravelAddonQuote2) + } + } + } + + @Test + fun `if reload trigger new data load`() = + runTest { + val useCase = FakeGetTravelAddonOfferUseCase() + val presenter = CustomizeTravelAddonPresenter( + getTravelAddonOfferUseCase = useCase, + insuranceId = insuranceId, + ) + presenter.test( + CustomizeTravelAddonState.Loading, + ) { + skipItems(1) + useCase.turbine.add(ErrorMessage().left()) + assertThat(awaitItem()).isInstanceOf(CustomizeTravelAddonState.Failure::class) + sendEvent(CustomizeTravelAddonEvent.Reload) + useCase.turbine.add(fakeTravelOfferTwoOptions.right()) + skipItems(1) + assertThat(awaitItem()).isInstanceOf(CustomizeTravelAddonState.Success::class) + } + } + + @Test + fun `on submit navigate further with currently chosen option and do not pop this destination`() = + runTest { + val useCase = FakeGetTravelAddonOfferUseCase() + val presenter = CustomizeTravelAddonPresenter( + getTravelAddonOfferUseCase = useCase, + insuranceId = insuranceId, + ) + presenter.test( + CustomizeTravelAddonState.Loading, + ) { + useCase.turbine.add(fakeTravelOfferTwoOptions.right()) + skipItems(2) + sendEvent(CustomizeTravelAddonEvent.ChooseOptionInDialog(fakeTravelOfferTwoOptions.addonOptions[1])) + sendEvent(CustomizeTravelAddonEvent.ChooseSelectedOption) + skipItems(2) + sendEvent(CustomizeTravelAddonEvent.SubmitSelected) + assertThat(awaitItem()).isInstanceOf(CustomizeTravelAddonState.Success::class) + .apply { + prop(CustomizeTravelAddonState.Success::summaryParamsToNavigateFurther) + .isNotNull().apply { + prop(SummaryParameters::popCustomizeDestination).isFalse() + prop(SummaryParameters::quote).isEqualTo(fakeTravelOfferTwoOptions.addonOptions[1]) + prop(SummaryParameters::currentTravelAddon).isEqualTo(fakeTravelOfferTwoOptions.currentTravelAddon) + prop(SummaryParameters::activationDate).isEqualTo(fakeTravelOfferTwoOptions.activationDate) + prop(SummaryParameters::offerDisplayName).isEqualTo(fakeTravelOfferTwoOptions.title) + } + } + } + } } private class FakeGetTravelAddonOfferUseCase() : GetTravelAddonOfferUseCase { @@ -107,7 +244,17 @@ private val fakeTravelAddonQuote2 = TravelAddonQuote( UiCurrencyCode.SEK, ), ) -private val fakeTravelAddon = TravelAddonOffer( +private val fakeTravelOfferOnlyOneOption = TravelAddonOffer( + addonOptions = nonEmptyListOf( + fakeTravelAddonQuote1, + ), + title = "Travel Plus", + description = "For those who travel often: luggage protection and 24/7 assistance worldwide", + activationDate = LocalDate(2024, 12, 30), + currentTravelAddon = null, +) + +private val fakeTravelOfferTwoOptions = TravelAddonOffer( addonOptions = nonEmptyListOf( fakeTravelAddonQuote1, fakeTravelAddonQuote2, From ecb47d9f6f56b3c1502ea529df0d9248a3060620 Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Wed, 18 Dec 2024 09:37:38 +0100 Subject: [PATCH 6/6] AddonSummaryPresenterTest --- .../ui/summary/AddonSummaryViewModel.kt | 2 +- .../kotlin/ui/AddonSummaryPresenterTest.kt | 181 ++++++++++++++++++ .../ui/CustomizeTravelAddonPresenterTest.kt | 101 +++++----- 3 files changed, 232 insertions(+), 52 deletions(-) create mode 100644 app/feature/feature-addon-purchase/src/test/kotlin/ui/AddonSummaryPresenterTest.kt diff --git a/app/feature/feature-addon-purchase/src/main/kotlin/com/hedvig/android/feature/addon/purchase/ui/summary/AddonSummaryViewModel.kt b/app/feature/feature-addon-purchase/src/main/kotlin/com/hedvig/android/feature/addon/purchase/ui/summary/AddonSummaryViewModel.kt index 4106c8a051..5f7bc84b7b 100644 --- a/app/feature/feature-addon-purchase/src/main/kotlin/com/hedvig/android/feature/addon/purchase/ui/summary/AddonSummaryViewModel.kt +++ b/app/feature/feature-addon-purchase/src/main/kotlin/com/hedvig/android/feature/addon/purchase/ui/summary/AddonSummaryViewModel.kt @@ -69,7 +69,7 @@ internal class AddonSummaryPresenter( } } -private fun getInitialState(summaryParameters: SummaryParameters): Content { +internal fun getInitialState(summaryParameters: SummaryParameters): Content { val total = if (summaryParameters.currentTravelAddon == null) { summaryParameters.quote.price } else { diff --git a/app/feature/feature-addon-purchase/src/test/kotlin/ui/AddonSummaryPresenterTest.kt b/app/feature/feature-addon-purchase/src/test/kotlin/ui/AddonSummaryPresenterTest.kt new file mode 100644 index 0000000000..114685eea1 --- /dev/null +++ b/app/feature/feature-addon-purchase/src/test/kotlin/ui/AddonSummaryPresenterTest.kt @@ -0,0 +1,181 @@ +package ui + +import app.cash.turbine.Turbine +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.isInstanceOf +import assertk.assertions.isNotNull +import assertk.assertions.isTrue +import assertk.assertions.prop +import com.hedvig.android.core.common.ErrorMessage +import com.hedvig.android.core.uidata.UiCurrencyCode +import com.hedvig.android.core.uidata.UiMoney +import com.hedvig.android.data.productvariant.InsuranceVariantDocument +import com.hedvig.android.feature.addon.purchase.data.AddonVariant +import com.hedvig.android.feature.addon.purchase.data.CurrentTravelAddon +import com.hedvig.android.feature.addon.purchase.data.SubmitAddonPurchaseUseCase +import com.hedvig.android.feature.addon.purchase.data.TravelAddonQuote +import com.hedvig.android.feature.addon.purchase.navigation.SummaryParameters +import com.hedvig.android.feature.addon.purchase.ui.summary.AddonSummaryEvent +import com.hedvig.android.feature.addon.purchase.ui.summary.AddonSummaryPresenter +import com.hedvig.android.feature.addon.purchase.ui.summary.AddonSummaryState +import com.hedvig.android.feature.addon.purchase.ui.summary.getInitialState +import com.hedvig.android.logger.TestLogcatLoggingRule +import com.hedvig.android.molecule.test.test +import kotlinx.coroutines.test.runTest +import kotlinx.datetime.LocalDate +import org.junit.Rule +import org.junit.Test + +class AddonSummaryPresenterTest { + @get:Rule + val testLogcatLogger = TestLogcatLoggingRule() + + @Test + fun `if receive error navigate to failure screen`() = runTest { + val useCase = FakeSubmitAddonPurchaseUseCase() + val presenter = AddonSummaryPresenter( + submitAddonPurchaseUseCase = useCase, + summaryParameters = testSummaryParametersWithCurrentAddon, + ) + presenter.test(getInitialState(testSummaryParametersWithCurrentAddon)) { + skipItems(1) + sendEvent(AddonSummaryEvent.Submit) + useCase.turbine.add(ErrorMessage().left()) + skipItems(1) + assertThat(awaitItem()).isInstanceOf(AddonSummaryState.Content::class) + .prop(AddonSummaryState.Content::navigateToFailure).isTrue() + } + } + + @Test + fun `if receive no errors navigate to success screen with activationDate from previous parameters`() = runTest { + val useCase = FakeSubmitAddonPurchaseUseCase() + val presenter = AddonSummaryPresenter( + submitAddonPurchaseUseCase = useCase, + summaryParameters = testSummaryParametersWithCurrentAddon, + ) + presenter.test(getInitialState(testSummaryParametersWithCurrentAddon)) { + skipItems(1) + sendEvent(AddonSummaryEvent.Submit) + useCase.turbine.add(Unit.right()) + skipItems(1) + assertThat(awaitItem()).isInstanceOf(AddonSummaryState.Content::class) + .prop(AddonSummaryState.Content::activationDateForSuccessfullyPurchasedAddon) + .isNotNull() + .isEqualTo(testSummaryParametersWithCurrentAddon.activationDate) + } + } + + @Test + fun `the difference between current addon price and new addon price is shown correctly`() = runTest { + val params = testSummaryParametersWithCurrentAddon + val diff = getInitialState(params).totalPriceChange + assertThat(diff).isEqualTo(UiMoney(11.0, testSummaryParametersWithCurrentAddon.quote.price.currencyCode)) + val params2 = testSummaryParametersWithMoreExpensiveCurrentAddon + val diff2 = getInitialState(params2).totalPriceChange + assertThat( + diff2, + ).isEqualTo(UiMoney(-19.0, testSummaryParametersWithMoreExpensiveCurrentAddon.quote.price.currencyCode)) + } + + @Test + fun `if there is no current addon, total price change should show the price of the quote`() = runTest { + val params = testSummaryParametersNoCurrentAddon + val diff = getInitialState(params).totalPriceChange + assertThat(diff).isEqualTo(testSummaryParametersNoCurrentAddon.quote.price) + } +} + +private class FakeSubmitAddonPurchaseUseCase() : SubmitAddonPurchaseUseCase { + val turbine = Turbine>() + + override suspend fun invoke(quoteId: String, addonId: String): Either { + return turbine.awaitItem() + } +} + +private val newQuote = TravelAddonQuote( + displayName = "60 days", + addonId = "addonId1", + quoteId = "id", + addonVariant = AddonVariant( + termsVersion = "terms", + displayDetails = listOf( + "Amount of insured people" to "You +1", + "Coverage" to "60 days", + ), + documents = listOf( + InsuranceVariantDocument( + "Terms and Conditions", + "url", + InsuranceVariantDocument.InsuranceDocumentType.TERMS_AND_CONDITIONS, + ), + ), + ), + price = UiMoney( + 60.0, + UiCurrencyCode.SEK, + ), +) + +private val newQuote2 = TravelAddonQuote( + displayName = "60 days", + addonId = "addonId1", + quoteId = "id", + addonVariant = AddonVariant( + termsVersion = "terms", + displayDetails = listOf( + "Amount of insured people" to "You +1", + "Coverage" to "60 days", + ), + documents = listOf( + InsuranceVariantDocument( + "Terms and Conditions", + "url", + InsuranceVariantDocument.InsuranceDocumentType.TERMS_AND_CONDITIONS, + ), + ), + ), + price = UiMoney( + 60.0, + UiCurrencyCode.NOK, + ), +) + +private val currentAddon = CurrentTravelAddon( + UiMoney(49.0, UiCurrencyCode.SEK), + listOf("Coverage" to "45 days", "Insured people" to "You+1"), +) + +private val moreExpensiveCurrentAddon = CurrentTravelAddon( + UiMoney(79.0, UiCurrencyCode.SEK), + listOf("Coverage" to "45 days", "Insured people" to "You+1"), +) + +private val testSummaryParametersWithCurrentAddon = SummaryParameters( + offerDisplayName = "fakeTravelOfferOnlyOneOption.title", + quote = newQuote, + activationDate = LocalDate(2024, 12, 30), + currentTravelAddon = currentAddon, + popCustomizeDestination = true, +) + +private val testSummaryParametersWithMoreExpensiveCurrentAddon = SummaryParameters( + offerDisplayName = "fakeTravelOfferOnlyOneOption.title", + quote = newQuote2, + activationDate = LocalDate(2024, 12, 30), + currentTravelAddon = moreExpensiveCurrentAddon, + popCustomizeDestination = true, +) + +private val testSummaryParametersNoCurrentAddon = SummaryParameters( + offerDisplayName = "fakeTravelOfferOnlyOneOption.title", + quote = newQuote, + activationDate = LocalDate(2024, 12, 30), + currentTravelAddon = null, + popCustomizeDestination = true, +) diff --git a/app/feature/feature-addon-purchase/src/test/kotlin/ui/CustomizeTravelAddonPresenterTest.kt b/app/feature/feature-addon-purchase/src/test/kotlin/ui/CustomizeTravelAddonPresenterTest.kt index 209fe99283..b246f5c03a 100644 --- a/app/feature/feature-addon-purchase/src/test/kotlin/ui/CustomizeTravelAddonPresenterTest.kt +++ b/app/feature/feature-addon-purchase/src/test/kotlin/ui/CustomizeTravelAddonPresenterTest.kt @@ -71,7 +71,6 @@ class CustomizeTravelAddonPresenterTest { } } - @Test fun `if receive good response but only one addon redirect to next screen and pop this destination`() = runTest { val useCase = FakeGetTravelAddonOfferUseCase() @@ -98,7 +97,6 @@ class CustomizeTravelAddonPresenterTest { ), ) } - } } @@ -120,10 +118,13 @@ class CustomizeTravelAddonPresenterTest { prop(CustomizeTravelAddonState.Success::summaryParamsToNavigateFurther) .isNull() prop(CustomizeTravelAddonState.Success::travelAddonOffer).isEqualTo(fakeTravelOfferTwoOptions) - prop(CustomizeTravelAddonState.Success::currentlyChosenOption).isEqualTo(fakeTravelOfferTwoOptions.addonOptions[0]) - prop(CustomizeTravelAddonState.Success::currentlyChosenOptionInDialog).isEqualTo(fakeTravelOfferTwoOptions.addonOptions[0]) + prop( + CustomizeTravelAddonState.Success::currentlyChosenOption, + ).isEqualTo(fakeTravelOfferTwoOptions.addonOptions[0]) + prop( + CustomizeTravelAddonState.Success::currentlyChosenOptionInDialog, + ).isEqualTo(fakeTravelOfferTwoOptions.addonOptions[0]) } - } } @@ -156,56 +157,54 @@ class CustomizeTravelAddonPresenterTest { } @Test - fun `if reload trigger new data load`() = - runTest { - val useCase = FakeGetTravelAddonOfferUseCase() - val presenter = CustomizeTravelAddonPresenter( - getTravelAddonOfferUseCase = useCase, - insuranceId = insuranceId, - ) - presenter.test( - CustomizeTravelAddonState.Loading, - ) { - skipItems(1) - useCase.turbine.add(ErrorMessage().left()) - assertThat(awaitItem()).isInstanceOf(CustomizeTravelAddonState.Failure::class) - sendEvent(CustomizeTravelAddonEvent.Reload) - useCase.turbine.add(fakeTravelOfferTwoOptions.right()) - skipItems(1) - assertThat(awaitItem()).isInstanceOf(CustomizeTravelAddonState.Success::class) - } + fun `if reload trigger new data load`() = runTest { + val useCase = FakeGetTravelAddonOfferUseCase() + val presenter = CustomizeTravelAddonPresenter( + getTravelAddonOfferUseCase = useCase, + insuranceId = insuranceId, + ) + presenter.test( + CustomizeTravelAddonState.Loading, + ) { + skipItems(1) + useCase.turbine.add(ErrorMessage().left()) + assertThat(awaitItem()).isInstanceOf(CustomizeTravelAddonState.Failure::class) + sendEvent(CustomizeTravelAddonEvent.Reload) + useCase.turbine.add(fakeTravelOfferTwoOptions.right()) + skipItems(1) + assertThat(awaitItem()).isInstanceOf(CustomizeTravelAddonState.Success::class) } + } @Test - fun `on submit navigate further with currently chosen option and do not pop this destination`() = - runTest { - val useCase = FakeGetTravelAddonOfferUseCase() - val presenter = CustomizeTravelAddonPresenter( - getTravelAddonOfferUseCase = useCase, - insuranceId = insuranceId, - ) - presenter.test( - CustomizeTravelAddonState.Loading, - ) { - useCase.turbine.add(fakeTravelOfferTwoOptions.right()) - skipItems(2) - sendEvent(CustomizeTravelAddonEvent.ChooseOptionInDialog(fakeTravelOfferTwoOptions.addonOptions[1])) - sendEvent(CustomizeTravelAddonEvent.ChooseSelectedOption) - skipItems(2) - sendEvent(CustomizeTravelAddonEvent.SubmitSelected) - assertThat(awaitItem()).isInstanceOf(CustomizeTravelAddonState.Success::class) - .apply { - prop(CustomizeTravelAddonState.Success::summaryParamsToNavigateFurther) - .isNotNull().apply { - prop(SummaryParameters::popCustomizeDestination).isFalse() - prop(SummaryParameters::quote).isEqualTo(fakeTravelOfferTwoOptions.addonOptions[1]) - prop(SummaryParameters::currentTravelAddon).isEqualTo(fakeTravelOfferTwoOptions.currentTravelAddon) - prop(SummaryParameters::activationDate).isEqualTo(fakeTravelOfferTwoOptions.activationDate) - prop(SummaryParameters::offerDisplayName).isEqualTo(fakeTravelOfferTwoOptions.title) - } - } - } + fun `on submit navigate further with currently chosen option and do not pop this destination`() = runTest { + val useCase = FakeGetTravelAddonOfferUseCase() + val presenter = CustomizeTravelAddonPresenter( + getTravelAddonOfferUseCase = useCase, + insuranceId = insuranceId, + ) + presenter.test( + CustomizeTravelAddonState.Loading, + ) { + useCase.turbine.add(fakeTravelOfferTwoOptions.right()) + skipItems(2) + sendEvent(CustomizeTravelAddonEvent.ChooseOptionInDialog(fakeTravelOfferTwoOptions.addonOptions[1])) + sendEvent(CustomizeTravelAddonEvent.ChooseSelectedOption) + skipItems(2) + sendEvent(CustomizeTravelAddonEvent.SubmitSelected) + assertThat(awaitItem()).isInstanceOf(CustomizeTravelAddonState.Success::class) + .apply { + prop(CustomizeTravelAddonState.Success::summaryParamsToNavigateFurther) + .isNotNull().apply { + prop(SummaryParameters::popCustomizeDestination).isFalse() + prop(SummaryParameters::quote).isEqualTo(fakeTravelOfferTwoOptions.addonOptions[1]) + prop(SummaryParameters::currentTravelAddon).isEqualTo(fakeTravelOfferTwoOptions.currentTravelAddon) + prop(SummaryParameters::activationDate).isEqualTo(fakeTravelOfferTwoOptions.activationDate) + prop(SummaryParameters::offerDisplayName).isEqualTo(fakeTravelOfferTwoOptions.title) + } + } } + } } private class FakeGetTravelAddonOfferUseCase() : GetTravelAddonOfferUseCase {