Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(android): leave / rejoin predictions & alerts after backgrounding #588

Merged
merged 1 commit into from
Dec 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}
}
Loading
Loading