Skip to content

Commit

Permalink
feat(android): leave / rejoin predictions & alerts after backgrounding
Browse files Browse the repository at this point in the history
  • Loading branch information
KaylaBrady committed Dec 16, 2024
1 parent 9fa0344 commit b2d38b7
Show file tree
Hide file tree
Showing 6 changed files with 228 additions and 226 deletions.
Original file line number Diff line number Diff line change
@@ -1,36 +1,21 @@
package com.mbta.tid.mbta_app.android.state

import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelStore
import com.mbta.tid.mbta_app.android.util.TimerViewModel
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.testing.TestLifecycleOwner
import com.mbta.tid.mbta_app.model.ObjectCollectionBuilder
import com.mbta.tid.mbta_app.model.response.AlertsStreamDataResponse
import com.mbta.tid.mbta_app.model.response.ApiResult
import com.mbta.tid.mbta_app.repositories.IAlertsRepository
import com.mbta.tid.mbta_app.repositories.MockAlertsRepository
import kotlin.test.assertEquals
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import org.junit.Assert
import org.junit.Rule
import org.junit.Test

class MockAlertsRepository(private val scope: CoroutineScope) : IAlertsRepository {
lateinit var alertsStreamDataResponse: AlertsStreamDataResponse
var disconnectHook: () -> Unit = { println("original disconnect hook called") }

override fun connect(onReceive: (ApiResult<AlertsStreamDataResponse>) -> Unit) {
scope.launch { onReceive(ApiResult.Ok(alertsStreamDataResponse)) }
}

override fun disconnect() {
disconnectHook()
}
}

class SubscribeToAlertsTest {
@get:Rule val composeRule = createComposeRule()

Expand All @@ -42,33 +27,58 @@ class SubscribeToAlertsTest {
header = "Alert 1"
description = "Description 1"
}

var connectCount = 0
val alertsStreamDataResponse = AlertsStreamDataResponse(builder)
val alertsRepo = MockAlertsRepository(this.backgroundScope)
alertsRepo.alertsStreamDataResponse = alertsStreamDataResponse
val alertsRepo = MockAlertsRepository(alertsStreamDataResponse, { connectCount += 1 })

var actualData: AlertsStreamDataResponse? = null
composeRule.setContent { actualData = subscribeToAlerts(alertsRepo) }
composeRule.awaitIdle()
composeRule.waitUntil { connectCount == 1 }
assertEquals(alertsStreamDataResponse, actualData)
}

@Test
fun testAlertsOnClear() = runTest {
var disconnectCalled = false
val mockAlertsRepository = MockAlertsRepository(this.backgroundScope)
mockAlertsRepository.disconnectHook = { disconnectCalled = true }
val viewModelStore = ViewModelStore()
val viewModelProvider =
ViewModelProvider(
viewModelStore,
object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return AlertsViewModel(mockAlertsRepository, TimerViewModel(1.seconds)) as T
}
}
fun testDisconnectsOnPause() = runTest {
val lifecycleOwner = TestLifecycleOwner(Lifecycle.State.RESUMED)

var connectCount = 0
var disconnectCount = 0

val builder = ObjectCollectionBuilder()
builder.alert {
id = "1"
header = "Alert 1"
description = "Description 1"
}

val alertsStreamDataResponse = AlertsStreamDataResponse(builder)
val alertsRepo =
MockAlertsRepository(
alertsStreamDataResponse,
{ connectCount += 1 },
{ disconnectCount += 1 }
)
viewModelProvider.get(AlertsViewModel::class)
viewModelStore.clear()
Assert.assertEquals(true, disconnectCalled)

var actualData: AlertsStreamDataResponse? = null

composeRule.setContent {
CompositionLocalProvider(LocalLifecycleOwner provides lifecycleOwner) {
actualData = subscribeToAlerts(alertsRepo)
}
}

composeRule.waitUntil { connectCount == 1 }
Assert.assertEquals(0, disconnectCount)

composeRule.runOnIdle { lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE) }

composeRule.waitUntil { disconnectCount == 1 }
Assert.assertEquals(1, connectCount)

composeRule.runOnIdle { lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_RESUME) }

composeRule.waitUntil { connectCount == 2 }
Assert.assertEquals(1, disconnectCount)
}
}
Original file line number Diff line number Diff line change
@@ -1,133 +1,108 @@
package com.mbta.tid.mbta_app.android.state

import androidx.activity.ComponentActivity
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelStore
import com.mbta.tid.mbta_app.android.util.TimerViewModel
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.testing.TestLifecycleOwner
import com.mbta.tid.mbta_app.model.ObjectCollectionBuilder
import com.mbta.tid.mbta_app.model.response.ApiResult
import com.mbta.tid.mbta_app.model.response.PredictionsByStopJoinResponse
import com.mbta.tid.mbta_app.model.response.PredictionsByStopMessageResponse
import com.mbta.tid.mbta_app.model.response.PredictionsStreamDataResponse
import com.mbta.tid.mbta_app.repositories.IPredictionsRepository
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import com.mbta.tid.mbta_app.repositories.MockPredictionsRepository
import kotlinx.coroutines.test.runTest
import kotlinx.datetime.Instant
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Rule
import org.junit.Test

class MockPredictionsRepository(private val scope: CoroutineScope) : IPredictionsRepository {
val stopIdsChannel = Channel<List<String>>()
lateinit var onJoin: (ApiResult<PredictionsByStopJoinResponse>) -> Unit
lateinit var onMessage: (ApiResult<PredictionsByStopMessageResponse>) -> Unit
var disconnectHook: () -> Unit = { println("original disconnect hook called") }

override fun connect(
stopIds: List<String>,
onReceive: (ApiResult<PredictionsStreamDataResponse>) -> Unit
) {
/* null-op */
}

override fun connectV2(
stopIds: List<String>,
onJoin: (ApiResult<PredictionsByStopJoinResponse>) -> Unit,
onMessage: (ApiResult<PredictionsByStopMessageResponse>) -> Unit
) {

this.onJoin = onJoin
scope.launch { stopIdsChannel.send(stopIds) }
}

override var lastUpdated: Instant? = null

override fun shouldForgetPredictions(predictionCount: Int) = false

override fun disconnect() {
disconnectHook()
}
}

class SubscribeToPredictionsTest {
@get:Rule val composeTestRule = createAndroidComposeRule<ComponentActivity>()

@Test
fun testPredictions() = runTest {
fun buildSomePredictions(): PredictionsByStopJoinResponse {
val objects = ObjectCollectionBuilder()
objects.prediction()
objects.prediction()
return PredictionsByStopJoinResponse(objects)
}
val predictionsRepo = MockPredictionsRepository(this)
val objects = ObjectCollectionBuilder()
objects.prediction()
objects.prediction()
val predictionsOnJoin = PredictionsByStopJoinResponse(objects)

var connectProps: List<String>? = null
var disconnectCount = 0

val predictionsRepo =
MockPredictionsRepository(
{},
{ stops -> connectProps = stops },
{ disconnectCount += 1 },
null,
predictionsOnJoin
)

var stopIds by mutableStateOf(listOf("place-a"))
var unmounted by mutableStateOf(false)
var stopIds = mutableStateOf(listOf("place-a"))
var predictions: PredictionsStreamDataResponse? =
PredictionsStreamDataResponse(ObjectCollectionBuilder())

composeTestRule.setContent {
if (!unmounted) predictions = subscribeToPredictions(stopIds, predictionsRepo)
var stopIds by remember { stopIds }
predictions = subscribeToPredictions(stopIds, predictionsRepo)
}

composeTestRule.awaitIdle()
assertEquals(listOf("place-a"), predictionsRepo.stopIdsChannel.receive())
assertNull(predictions)

val expectedPredictions1 = buildSomePredictions()
predictionsRepo.onJoin(ApiResult.Ok(expectedPredictions1))
composeTestRule.awaitIdle()
assertEquals(expectedPredictions1.toPredictionsStreamDataResponse(), predictions)

stopIds = listOf("place-b")
composeTestRule.awaitIdle()
assertEquals(listOf("place-b"), predictionsRepo.stopIdsChannel.receive())
predictionsRepo.onJoin(ApiResult.Ok(expectedPredictions1))
composeTestRule.awaitIdle()
assertEquals(expectedPredictions1.toPredictionsStreamDataResponse(), predictions)

val expectedPredictions2 = buildSomePredictions()
predictionsRepo.onJoin(ApiResult.Ok(expectedPredictions2))
composeTestRule.awaitIdle()
assertEquals(expectedPredictions2.toPredictionsStreamDataResponse(), predictions)

unmounted = true
composeTestRule.awaitIdle()
composeTestRule.waitUntil { connectProps == listOf("place-a") }

composeTestRule.waitUntil {
predictions != null &&
predictions == predictionsOnJoin?.toPredictionsStreamDataResponse()
}

assertEquals(0, disconnectCount)

stopIds.value = listOf("place-b")
composeTestRule.waitUntil { disconnectCount == 1 }

composeTestRule.waitUntil { connectProps == listOf("place-b") }
}

@Test
fun testPredictionsOnClear() = runTest {
var disconnectCalled = false
val stopIds by mutableStateOf(listOf("place-a"))
val mockPredictionsRepository = MockPredictionsRepository(this.backgroundScope)
mockPredictionsRepository.disconnectHook = { disconnectCalled = true }

val viewModelStore = ViewModelStore()
val viewModelProvider =
ViewModelProvider(
viewModelStore,
object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return PredictionsViewModel(
stopIds,
mockPredictionsRepository,
TimerViewModel(1.seconds)
)
as T
}
}
fun testDisconnectsOnPause() = runTest {
val lifecycleOwner = TestLifecycleOwner(Lifecycle.State.RESUMED)

var connectCount = 0
var disconnectCount = 0

val predictionsRepo =
MockPredictionsRepository(
{},
{ stopIds -> connectCount += 1 },
{ disconnectCount += 1 },
null,
null
)
viewModelProvider.get(PredictionsViewModel::class)
viewModelStore.clear()
assertEquals(true, disconnectCalled)

var stopIds = mutableStateOf(listOf("place-a"))
var predictions: PredictionsStreamDataResponse? =
PredictionsStreamDataResponse(ObjectCollectionBuilder())

composeTestRule.setContent {
CompositionLocalProvider(LocalLifecycleOwner provides lifecycleOwner) {
var stopIds by remember { stopIds }
predictions = subscribeToPredictions(stopIds, predictionsRepo)
}
}

composeTestRule.waitUntil { connectCount == 1 }
assertEquals(0, disconnectCount)

composeTestRule.runOnIdle { lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE) }

composeTestRule.waitUntil { disconnectCount == 1 }
assertEquals(1, connectCount)

composeTestRule.runOnIdle { lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_RESUME) }

composeTestRule.waitUntil { connectCount == 2 }
assertEquals(1, disconnectCount)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ fun HomeMapView(
var railRouteLineData: List<RouteLineData>? by remember { mutableStateOf(null) }
var stopSourceData: FeatureCollection? by remember { mutableStateOf(null) }

val now = timer(updateInterval = 10.seconds)
val now = timer(updateInterval = 300.seconds)
val globalMapData =
remember(globalResponse, alertsData, now) {
if (globalResponse != null) {
Expand Down
Loading

0 comments on commit b2d38b7

Please sign in to comment.