From 0be602a92678e29ebb2deff44acc6d0ab010c7eb Mon Sep 17 00:00:00 2001 From: Karan Sharma <55722391+ksharma-xyz@users.noreply.github.com> Date: Sun, 5 Jan 2025 16:17:36 +1100 Subject: [PATCH] Add UT for LoadTimeTable --- .../test/fakes/FakeTripPlanningService.kt | 28 +++- .../test/fakes/FakeTripResponseBuilder.kt | 45 ++++++ .../test/viewmodels/TimeTableViewModelTest.kt | 137 ++++++++++++++++++ .../ui/timetable/TimeTableViewModel.kt | 18 ++- 4 files changed, 222 insertions(+), 6 deletions(-) create mode 100644 core/test/src/commonTest/kotlin/xyz/ksharma/core/test/fakes/FakeTripResponseBuilder.kt diff --git a/core/test/src/commonTest/kotlin/xyz/ksharma/core/test/fakes/FakeTripPlanningService.kt b/core/test/src/commonTest/kotlin/xyz/ksharma/core/test/fakes/FakeTripPlanningService.kt index 58207cd0..e1f02a89 100644 --- a/core/test/src/commonTest/kotlin/xyz/ksharma/core/test/fakes/FakeTripPlanningService.kt +++ b/core/test/src/commonTest/kotlin/xyz/ksharma/core/test/fakes/FakeTripPlanningService.kt @@ -1,13 +1,19 @@ package xyz.ksharma.core.test.fakes +import xyz.ksharma.core.test.fakes.FakeTripResponseBuilder.buildDestinationStopSequence +import xyz.ksharma.core.test.fakes.FakeTripResponseBuilder.buildOriginStopSequence +import xyz.ksharma.core.test.fakes.FakeTripResponseBuilder.buildTransportation import xyz.ksharma.krail.trip.planner.network.api.model.StopFinderResponse import xyz.ksharma.krail.trip.planner.network.api.model.StopType import xyz.ksharma.krail.trip.planner.network.api.model.TripResponse +import xyz.ksharma.krail.trip.planner.network.api.model.TripResponse.StopSequence import xyz.ksharma.krail.trip.planner.network.api.service.DepArr import xyz.ksharma.krail.trip.planner.network.api.service.TripPlanningService class FakeTripPlanningService : TripPlanningService { + var isSuccess: Boolean = true + override suspend fun trip( originStopId: String, destinationStopId: String, @@ -15,9 +21,27 @@ class FakeTripPlanningService : TripPlanningService { date: String?, time: String?, ): TripResponse { + return if (isSuccess) + // Return a fake TripResponse - return TripResponse( - ) + TripResponse( + journeys = listOf( + TripResponse.Journey( + legs = listOf( + TripResponse.Leg( + origin = buildOriginStopSequence(), + destination = buildDestinationStopSequence(), + stopSequence = listOf( + buildOriginStopSequence(), + ), + transportation = buildTransportation(), + duration = 100, + ), + ), + ), + ) + ) + else throw IllegalStateException("Failed to fetch trip") } override suspend fun stopFinder( diff --git a/core/test/src/commonTest/kotlin/xyz/ksharma/core/test/fakes/FakeTripResponseBuilder.kt b/core/test/src/commonTest/kotlin/xyz/ksharma/core/test/fakes/FakeTripResponseBuilder.kt new file mode 100644 index 00000000..c2019389 --- /dev/null +++ b/core/test/src/commonTest/kotlin/xyz/ksharma/core/test/fakes/FakeTripResponseBuilder.kt @@ -0,0 +1,45 @@ +package xyz.ksharma.core.test.fakes + +import xyz.ksharma.krail.trip.planner.network.api.model.StopType +import xyz.ksharma.krail.trip.planner.network.api.model.TripResponse +import xyz.ksharma.krail.trip.planner.network.api.model.TripResponse.StopSequence + +object FakeTripResponseBuilder { + + fun buildOriginStopSequence() = StopSequence( + arrivalTimePlanned = "2024-09-24T19:00:00Z", + arrivalTimeEstimated = "2024-09-24T19:10:00Z", + departureTimePlanned = "2024-09-24T19:10:00Z", + departureTimeEstimated = "2024-09-24T19:10:00Z", + name = "Origin Stop", + disassembledName = "Origin Name", + id = "Origin_stop_id", + type = StopType.STOP.type, + ) + + fun buildDestinationStopSequence() = StopSequence( + arrivalTimePlanned = "2024-09-24T20:00:00Z", + arrivalTimeEstimated = "2024-09-24T20:10:00Z", + departureTimePlanned = "2024-09-24T20:10:00Z", + departureTimeEstimated = "2024-09-24T20:10:00Z", + name = "Destination Stop", + disassembledName = "Destination Name", + id = "Destination_stop_id", + type = StopType.STOP.type, + ) + + fun buildTransportation() = TripResponse.Transportation( + disassembledName = "Transportation Name", + product = TripResponse.Product( + productClass = 1, + name = "Train", + ), + destination = TripResponse.OperatorClass( + name = "Destination Operator", + id = "Destination Operator Id", + ), + name = "Transportation Name", + id = "Transportation Id", + description = "Transportation Description", + ) +} diff --git a/core/test/src/commonTest/kotlin/xyz/ksharma/core/test/viewmodels/TimeTableViewModelTest.kt b/core/test/src/commonTest/kotlin/xyz/ksharma/core/test/viewmodels/TimeTableViewModelTest.kt index 4f3d2734..235cff48 100644 --- a/core/test/src/commonTest/kotlin/xyz/ksharma/core/test/viewmodels/TimeTableViewModelTest.kt +++ b/core/test/src/commonTest/kotlin/xyz/ksharma/core/test/viewmodels/TimeTableViewModelTest.kt @@ -1,9 +1,12 @@ package xyz.ksharma.core.test.viewmodels +import app.cash.turbine.test import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain import xyz.ksharma.core.test.fakes.FakeAnalytics import xyz.ksharma.core.test.fakes.FakeRateLimiter @@ -11,9 +14,17 @@ import xyz.ksharma.core.test.fakes.FakeSandook import xyz.ksharma.core.test.fakes.FakeTripPlanningService import xyz.ksharma.krail.core.analytics.Analytics import xyz.ksharma.krail.sandook.Sandook +import xyz.ksharma.krail.trip.planner.ui.state.timetable.TimeTableUiEvent +import xyz.ksharma.krail.trip.planner.ui.state.timetable.Trip import xyz.ksharma.krail.trip.planner.ui.timetable.TimeTableViewModel import kotlin.test.AfterTest import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertIs +import kotlin.test.assertNull +import kotlin.test.assertTrue @OptIn(ExperimentalCoroutinesApi::class) class TimeTableViewModelTest { @@ -42,4 +53,130 @@ class TimeTableViewModelTest { fun tearDown() { Dispatchers.resetMain() } + + @Test + fun `GIVEN initial state WHEN observer is active THEN fetchTrip and trackScreenViewEvent should be called`() = + runTest { + // Ensure analytics events have not been tracked before observation + assertFalse((analytics as FakeAnalytics).isEventTracked("view_screen")) + + viewModel.isLoading.test { + val isLoadingState = awaitItem() + assertEquals(isLoadingState, true) + + advanceUntilIdle() + assertTrue(analytics.isEventTracked("view_screen")) + + cancelAndConsumeRemainingEvents() + } + } + + @Test + fun `GIVEN a trip WHEN LoadTimeTable event is triggered and Trip API is success response THEN UI State must update with journeyList`() = + runTest { + // GIVEN a trip + val trip = Trip( + fromStopId = "FROM_STOP_ID_1", + fromStopName = "STOP_NAME_1", + toStopId = "TO_STOP_ID_1", + toStopName = "STOP_NAME_2" + ) + tripPlanningService.isSuccess = true + + // THEN verify that the UI state is updated correctly + viewModel.uiState.test { + val initialState = awaitItem() + initialState.run { + assertTrue(isLoading) + assertNull(initialState.trip) + assertFalse(isError) + assertFalse(isTripSaved) + } + + // WHEN the LoadTimeTable event is triggered + viewModel.onEvent(TimeTableUiEvent.LoadTimeTable(trip)) + viewModel.fetchTrip() // Manually call fetchTrip() to simulate the actual behavior + awaitItem().run { + assertTrue(isLoading) + assertFalse(silentLoading) + assertFalse(isError) + assertTrue(journeyList.isEmpty()) + } + + // need to skip two items, because silentLoading will be toggled, as we manually call fetchTrip() + skipItems(2) +/* + awaitItem().run { + assertTrue(silentLoading) + } + awaitItem().run { + assertFalse(silentLoading) + } +*/ + + awaitItem().run { + assertFalse(isLoading) + assertFalse(silentLoading) + assertTrue(journeyList.isNotEmpty()) + assertEquals(expected = 1, journeyList.size) + } + + cancelAndConsumeRemainingEvents() + } + } + + @Test + fun `GIVEN a trip WHEN LoadTimeTable event is triggered and Trip API is error response THEN UIState should have isError as true`() = + runTest { + // GIVEN a trip + val trip = Trip( + fromStopId = "FROM_STOP_ID_1", + fromStopName = "STOP_NAME_1", + toStopId = "TO_STOP_ID_1", + toStopName = "STOP_NAME_2" + ) + tripPlanningService.isSuccess = false + + // THEN verify that the UI state is updated correctly + viewModel.uiState.test { + val initialState = awaitItem() + initialState.run { + assertTrue(isLoading) + assertNull(initialState.trip) + assertFalse(isError) + assertFalse(isTripSaved) + } + + // WHEN the LoadTimeTable event is triggered + viewModel.onEvent(TimeTableUiEvent.LoadTimeTable(trip)) + viewModel.fetchTrip() // Manually call fetchTrip() to simulate the actual behavior + awaitItem().run { + assertTrue(isLoading) + assertFalse(silentLoading) + assertFalse(isError) + assertTrue(journeyList.isEmpty()) + } + + // need to skip two items, because silentLoading will be toggled, as we manually call fetchTrip() + skipItems(2) + /* + awaitItem().run { + assertTrue(silentLoading) + } + awaitItem().run { + assertFalse(silentLoading) + } + */ + + awaitItem().run { + assertFalse(isLoading) + assertFalse(silentLoading) + assertTrue(journeyList.isEmpty()) + assertTrue(isError) + } + + cancelAndConsumeRemainingEvents() + } + } + } diff --git a/feature/trip-planner/ui/src/commonMain/kotlin/xyz/ksharma/krail/trip/planner/ui/timetable/TimeTableViewModel.kt b/feature/trip-planner/ui/src/commonMain/kotlin/xyz/ksharma/krail/trip/planner/ui/timetable/TimeTableViewModel.kt index 523031ee..fd860e42 100644 --- a/feature/trip-planner/ui/src/commonMain/kotlin/xyz/ksharma/krail/trip/planner/ui/timetable/TimeTableViewModel.kt +++ b/feature/trip-planner/ui/src/commonMain/kotlin/xyz/ksharma/krail/trip/planner/ui/timetable/TimeTableViewModel.kt @@ -1,5 +1,6 @@ package xyz.ksharma.krail.trip.planner.ui.timetable +import androidx.annotation.VisibleForTesting import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.collections.immutable.toImmutableList @@ -169,7 +170,8 @@ class TimeTableViewModel( } } - private fun fetchTrip() { + @VisibleForTesting + fun fetchTrip() { log("fetchTrip API Call") fetchTripJob?.cancel() updateUiState { copy(silentLoading = true) } @@ -181,6 +183,7 @@ class TimeTableViewModel( }.catch { e -> log("Error while fetching trip: $e") }.collectLatest { result -> + println("result Success: $result") updateUiState { copy(silentLoading = false) } result.onSuccess { response -> updateTripsCache(response) @@ -239,12 +242,17 @@ class TimeTableViewModel( } private fun updateUiStateWithFilteredTrips() { + println("updateUiStateWithFilteredTrips") + + val journeyList = updateJourneyCardInfoTimeText(journeys.values.toList()) + .sortedBy { it.originUtcDateTime.utcToLocalDateTimeAEST() } + .toImmutableList() + + println("updateUiStateWithFilteredTrips: ${journeyList.size}") updateUiState { copy( isLoading = false, - journeyList = updateJourneyCardInfoTimeText(journeys.values.toList()) - .sortedBy { it.originUtcDateTime.utcToLocalDateTimeAEST() } - .toImmutableList(), + journeyList = journeyList, isError = false, ) } @@ -268,6 +276,8 @@ class TimeTableViewModel( else -> DepArr.DEP } ) + println("tripResponse: $tripResponse") + Result.success(tripResponse) }.getOrElse { error -> Result.failure(error)