mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-06 03:38:23 +00:00
test: wait for CalendarsViewModel scopes before resetMain
The calendar ViewModel tests cleared their store and then reset Main, but clearing only requests cancellation. The model's flowOn producers on Dispatchers.Default could still be finishing, and handing their completion back to the Main collector after resetMain threw "The main looper is not available" onto a background thread. runTest then failed whichever test started next with UncaughtExceptionsBeforeTest (seen on CI as Nip46ConsentInfoBuilderTest). Both tests now join the models' viewModelScope jobs before resetMain. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168wY9t7i9NC5u3svyMxEz6
This commit is contained in:
+10
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.calendar
|
|||||||
|
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.ViewModelStore
|
import androidx.lifecycle.ViewModelStore
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.vitorpamplona.amethyst.commons.feeds.FeedContentState
|
import com.vitorpamplona.amethyst.commons.feeds.FeedContentState
|
||||||
import com.vitorpamplona.amethyst.commons.model.AddressableNote
|
import com.vitorpamplona.amethyst.commons.model.AddressableNote
|
||||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||||
@@ -33,13 +34,16 @@ import com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarEvent
|
|||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.job
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.test.resetMain
|
import kotlinx.coroutines.test.resetMain
|
||||||
import kotlinx.coroutines.test.setMain
|
import kotlinx.coroutines.test.setMain
|
||||||
|
import kotlinx.coroutines.withTimeout
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
@@ -137,14 +141,20 @@ class CalendarsViewModelFlowTest {
|
|||||||
) = runBlocking {
|
) = runBlocking {
|
||||||
val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
||||||
val store = ViewModelStore()
|
val store = ViewModelStore()
|
||||||
|
var modelScope: Job? = null
|
||||||
try {
|
try {
|
||||||
val feed = FeedContentState(CalendarAppointmentsFeedFilter(seeEverythingAccount()), scope, LocalCache)
|
val feed = FeedContentState(CalendarAppointmentsFeedFilter(seeEverythingAccount()), scope, LocalCache)
|
||||||
val model = ViewModelProvider(store, ViewModelProvider.NewInstanceFactory())[CalendarsViewModel::class.java]
|
val model = ViewModelProvider(store, ViewModelProvider.NewInstanceFactory())[CalendarsViewModel::class.java]
|
||||||
|
modelScope = model.viewModelScope.coroutineContext.job
|
||||||
model.init(pubKey, feed)
|
model.init(pubKey, feed)
|
||||||
block(model)
|
block(model)
|
||||||
} finally {
|
} finally {
|
||||||
store.clear()
|
store.clear()
|
||||||
scope.cancel()
|
scope.cancel()
|
||||||
|
// Clearing only requests cancellation. The flowOn producers on Dispatchers.Default can
|
||||||
|
// still be finishing and hand their completion back to the Main collectors; wait for
|
||||||
|
// them here, while setMain is still in force, not after tearDown's resetMain.
|
||||||
|
withTimeout(AWAIT_MS) { modelScope?.join() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+22
-1
@@ -22,13 +22,19 @@ package com.vitorpamplona.amethyst.calendar
|
|||||||
|
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.ViewModelStore
|
import androidx.lifecycle.ViewModelStore
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarsViewMode
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarsViewMode
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarsViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarsViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.startOfWeek
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.startOfWeek
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.job
|
||||||
|
import kotlinx.coroutines.joinAll
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.test.resetMain
|
import kotlinx.coroutines.test.resetMain
|
||||||
import kotlinx.coroutines.test.setMain
|
import kotlinx.coroutines.test.setMain
|
||||||
|
import kotlinx.coroutines.withTimeout
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertNull
|
import org.junit.Assert.assertNull
|
||||||
@@ -58,13 +64,24 @@ class CalendarsViewModelTest {
|
|||||||
store = ViewModelStore()
|
store = ViewModelStore()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val scopes = mutableListOf<Job>()
|
||||||
|
|
||||||
@After
|
@After
|
||||||
fun tearDown() {
|
fun tearDown() {
|
||||||
store.clear()
|
store.clear()
|
||||||
|
// Clearing only requests cancellation: a producer the model runs on Dispatchers.Default
|
||||||
|
// (its flowOn) can still be finishing, and it hands its completion back to the Main
|
||||||
|
// collector. Waiting for the scopes keeps that inside setMain, instead of throwing onto a
|
||||||
|
// background thread after resetMain and failing whichever runTest starts next.
|
||||||
|
runBlocking { withTimeout(SCOPE_CLOSE_MS) { scopes.joinAll() } }
|
||||||
|
scopes.clear()
|
||||||
Dispatchers.resetMain()
|
Dispatchers.resetMain()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun newModel(): CalendarsViewModel = ViewModelProvider(store, ViewModelProvider.NewInstanceFactory())[CalendarsViewModel::class.java]
|
private fun newModel(): CalendarsViewModel =
|
||||||
|
ViewModelProvider(store, ViewModelProvider.NewInstanceFactory())[CalendarsViewModel::class.java].also {
|
||||||
|
scopes += it.viewModelScope.coroutineContext.job
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun aFreshScreenOpensOnToday() {
|
fun aFreshScreenOpensOnToday() {
|
||||||
@@ -148,4 +165,8 @@ class CalendarsViewModelTest {
|
|||||||
|
|
||||||
assertEquals("the lenses must not share one scroll offset", 4, states.distinct().size)
|
assertEquals("the lenses must not share one scroll offset", 4, states.distinct().size)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val SCOPE_CLOSE_MS = 5_000L
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user