Skip to content
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
Expand Up @@ -24,7 +24,6 @@ import io.homeassistant.companion.android.util.HAWebChromeClient
import io.homeassistant.companion.android.util.HAWebViewClient
import io.homeassistant.companion.android.util.HAWebViewClientFactory
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.ExperimentalForInheritanceCoroutinesApi
import kotlinx.coroutines.Job
Expand All @@ -35,7 +34,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
Expand All @@ -50,9 +48,6 @@ import timber.log.Timber
@VisibleForTesting
val CONNECTION_TIMEOUT = 10.seconds

/** Delay before stopping shared flows after the last subscriber disconnects. */
private val SUBSCRIPTION_STOP_DELAY = 500.milliseconds

/**
* ViewModel for frontend screen.
*
Expand Down Expand Up @@ -144,12 +139,12 @@ internal class FrontendViewModel @VisibleForTesting constructor(
override val urlFlow: StateFlow<String?> =
_viewState.map { it.url }
.distinctUntilChanged()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(SUBSCRIPTION_STOP_DELAY), null)
.stateIn(viewModelScope, SharingStarted.Eagerly, _viewState.value.url)

Comment thread
TimoPtr marked this conversation as resolved.
override val errorFlow: StateFlow<FrontendConnectionError?> =
_viewState.map { state -> (state as? FrontendViewState.Error)?.error }
.distinctUntilChanged()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(SUBSCRIPTION_STOP_DELAY), null)
.stateIn(viewModelScope, SharingStarted.Eagerly, (_viewState.value as? FrontendViewState.Error)?.error)

/** Flow of scripts to be evaluated in the WebView. */
val scriptsToEvaluate: Flow<WebViewScript> = frontendMessageHandler.scriptsToEvaluate()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -275,24 +275,47 @@ class FrontendViewModelTest {
}

@Nested
inner class ErrorFlow {
inner class DerivedFlows {

@Test
fun `Given loading state when error occurs then error flow is updated`() = runTest {
fun `Given url manager returns success then urlFlow value matches viewState url without subscribers`() = runTest {
every { urlManager.serverUrlFlow(any(), any()) } returns flowOf(
UrlLoadResult.Success(url = testUrlWithAuth, serverId = serverId),
)

val viewModel = createViewModel()
advanceTimeBy(CONNECTION_TIMEOUT - 1.seconds)

// No collector on urlFlow — value must still be up to date (SharingStarted.Eagerly)
assertEquals(testUrlWithAuth, viewModel.urlFlow.value)
}

@Test
fun `Given error state then errorFlow value matches viewState error without subscribers`() = runTest {
every { urlManager.serverUrlFlow(any(), any()) } returns flowOf(
UrlLoadResult.ServerNotFound(serverId),
)

val viewModel = createViewModel()
advanceUntilIdle()

// Subscribe to errorFlow to activate the stateIn with WhileSubscribed
val errors = mutableListOf<FrontendConnectionError?>()
val job = backgroundScope.launch { viewModel.errorFlow.collect { errors.add(it) } }
assertTrue(viewModel.errorFlow.value is FrontendConnectionError.UnreachableError)
}
}

@Nested
inner class ErrorFlow {

@Test
fun `Given loading state when error occurs then error flow is updated`() = runTest {
every { urlManager.serverUrlFlow(any(), any()) } returns flowOf(
UrlLoadResult.ServerNotFound(serverId),
)

val viewModel = createViewModel()
advanceUntilIdle()

assertTrue(errors.any { it is FrontendConnectionError.UnreachableError })
job.cancel()
assertTrue(viewModel.errorFlow.value is FrontendConnectionError.UnreachableError)
}

@Test
Expand All @@ -301,25 +324,19 @@ class FrontendViewModelTest {
every { urlManager.serverUrlFlow(any(), any()) } returns urlResults

val viewModel = createViewModel()

// Subscribe to errorFlow to activate the stateIn with WhileSubscribed
val errors = mutableListOf<FrontendConnectionError?>()
val job = backgroundScope.launch { viewModel.errorFlow.collect { errors.add(it) } }

advanceUntilIdle()

// Verify error exists
assertTrue(errors.any { it != null })
assertTrue(viewModel.errorFlow.value != null)

// Setup successful response for retry
urlResults.value = UrlLoadResult.Success(url = testUrlWithAuth, serverId = serverId)

viewModel.onRetry()
advanceTimeBy(CONNECTION_TIMEOUT - 1.seconds)

// Error should be cleared - the last emitted error should be null
assertTrue(errors.last() == null)
job.cancel()
// Error should be cleared
assertEquals(null, viewModel.errorFlow.value)
}

@Test
Expand Down
Loading