From b03d4a86e7155e8f102182aedb776cfaddbc30c8 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Fri, 22 May 2026 23:19:52 -0400 Subject: [PATCH] fix(android): drop Loading-state assertions in LibraryViewModelTest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The three failing assertions tested an implementation detail. Under UnconfinedTestDispatcher (MainDispatcherExtension's default), stateIn's upstream Flow runs synchronously when the first subscriber attaches, so the `Loading` initialValue gets replaced by the upstream emission before Turbine's .test{} sees it. The observable behavior we care about is the resolved state — Empty/Success/Error — not the intermediate Loading. Tests now collect the resolved state as the first awaitItem(), which is what users actually see. The Loading state still exists in production (StateFlow initialValue is preserved across the brief window before stateIn collects the first upstream value when the real dispatcher isn't unconfined). Also cleared two compile warnings the run surfaced: - AuthCookieInterceptorTest: added @OptIn(ExperimentalCoroutinesApi) for UnconfinedTestDispatcher - LibraryRepositoryTest: hoisted the Json instance into a companion object (detekt warned about per-call creation) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../minstrel/api/AuthCookieInterceptorTest.kt | 2 ++ .../minstrel/library/data/LibraryRepositoryTest.kt | 9 +++++---- .../minstrel/library/ui/LibraryViewModelTest.kt | 10 +++++++--- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/android/app/src/test/java/com/fabledsword/minstrel/api/AuthCookieInterceptorTest.kt b/android/app/src/test/java/com/fabledsword/minstrel/api/AuthCookieInterceptorTest.kt index e9c39baa..61a3c582 100644 --- a/android/app/src/test/java/com/fabledsword/minstrel/api/AuthCookieInterceptorTest.kt +++ b/android/app/src/test/java/com/fabledsword/minstrel/api/AuthCookieInterceptorTest.kt @@ -5,6 +5,7 @@ import com.fabledsword.minstrel.cache.db.dao.AuthSessionDao import io.mockk.coEvery import io.mockk.every import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher @@ -18,6 +19,7 @@ import org.junit.jupiter.api.Test import kotlin.test.assertEquals import kotlin.test.assertNull +@OptIn(ExperimentalCoroutinesApi::class) class AuthCookieInterceptorTest { private lateinit var server: MockWebServer private lateinit var authStore: AuthStore diff --git a/android/app/src/test/java/com/fabledsword/minstrel/library/data/LibraryRepositoryTest.kt b/android/app/src/test/java/com/fabledsword/minstrel/library/data/LibraryRepositoryTest.kt index c35988b7..cf096803 100644 --- a/android/app/src/test/java/com/fabledsword/minstrel/library/data/LibraryRepositoryTest.kt +++ b/android/app/src/test/java/com/fabledsword/minstrel/library/data/LibraryRepositoryTest.kt @@ -37,13 +37,14 @@ class LibraryRepositoryTest { Retrofit.Builder() .baseUrl(server.url("/")) .client(OkHttpClient.Builder().build()) - .addConverterFactory( - Json { ignoreUnknownKeys = true } - .asConverterFactory("application/json".toMediaType()), - ) + .addConverterFactory(json.asConverterFactory("application/json".toMediaType())) .build() } + private companion object { + private val json = Json { ignoreUnknownKeys = true } + } + @AfterEach fun teardown() { server.shutdown() diff --git a/android/app/src/test/java/com/fabledsword/minstrel/library/ui/LibraryViewModelTest.kt b/android/app/src/test/java/com/fabledsword/minstrel/library/ui/LibraryViewModelTest.kt index 01000eb0..17c33486 100644 --- a/android/app/src/test/java/com/fabledsword/minstrel/library/ui/LibraryViewModelTest.kt +++ b/android/app/src/test/java/com/fabledsword/minstrel/library/ui/LibraryViewModelTest.kt @@ -30,6 +30,13 @@ class LibraryViewModelTest { assertEquals(LibraryUiState.Loading, vm.uiState.value) } + // Note: tests below collect the resolved state directly. With + // UnconfinedTestDispatcher, stateIn's upstream Flow runs synchronously + // when the first subscriber attaches — so the `Loading` initialValue + // is replaced by the upstream emission before .test{} sees it. The + // observable behavior we care about is the resolved state, not the + // intermediate Loading. + @Test fun `Empty when DAOs emit empty lists`() = runTest { val repo = mockk() @@ -39,7 +46,6 @@ class LibraryViewModelTest { val vm = LibraryViewModel(repo) vm.uiState.test { - assertEquals(LibraryUiState.Loading, awaitItem()) assertEquals(LibraryUiState.Empty, awaitItem()) cancelAndConsumeRemainingEvents() } @@ -56,7 +62,6 @@ class LibraryViewModelTest { val vm = LibraryViewModel(repo) vm.uiState.test { - assertEquals(LibraryUiState.Loading, awaitItem()) val success = awaitItem() as LibraryUiState.Success assertEquals(1, success.artists.size) assertEquals(1, success.albums.size) @@ -76,7 +81,6 @@ class LibraryViewModelTest { val vm = LibraryViewModel(repo) vm.uiState.test { - assertEquals(LibraryUiState.Loading, awaitItem()) val error = awaitItem() as LibraryUiState.Error assertTrue(error.message.contains("DAO blew up")) cancelAndConsumeRemainingEvents()