Merge main into test-220-native-lib-before-keyed-open

This commit is contained in:
Jason Ross
2026-07-03 22:37:29 -05:00
committed by GitHub
4 changed files with 141 additions and 4 deletions
@@ -83,6 +83,10 @@ class FakeMailRepository(
private val unreadCounts: List<UnreadCount> = emptyList(),
drafts: List<Draft> = emptyList(),
outbox: List<OutboxMessage> = emptyList(),
// When set, every paged query returns this instead of a static page over [messages]. Lets a UI test
// drive an explicit LoadState (e.g. refresh = Loading) through collectAsLazyPagingItems to exercise
// the empty-state gate (issue #219).
private val pagedOverride: Flow<PagingData<Message>>? = null,
) : MailRepository {
val sentMessages = mutableListOf<OutgoingMessage>()
@@ -104,19 +108,21 @@ class FakeMailRepository(
private val outboxFlow = MutableStateFlow(outbox)
override fun pagedUnifiedFolderMessages(folder: String): Flow<PagingData<Message>> =
flowOf(PagingData.from(messages.filter { it.folder == folder && it.inInbox }))
pagedOverride ?: flowOf(PagingData.from(messages.filter { it.folder == folder && it.inInbox }))
override fun pagedFolderMessages(accountId: String, folder: String): Flow<PagingData<Message>> =
flowOf(PagingData.from(messages.filter { it.accountId == accountId && it.folder == folder && it.inInbox }))
pagedOverride ?: flowOf(
PagingData.from(messages.filter { it.accountId == accountId && it.folder == folder && it.inInbox }),
)
override fun pagedUnifiedSearchMessages(folder: String, query: String): Flow<PagingData<Message>> =
flowOf(PagingData.from(messages.filter { it.folder == folder && it.matchesSearch(query) }))
pagedOverride ?: flowOf(PagingData.from(messages.filter { it.folder == folder && it.matchesSearch(query) }))
override fun pagedFolderSearchMessages(
accountId: String,
folder: String,
query: String,
): Flow<PagingData<Message>> = flowOf(
): Flow<PagingData<Message>> = pagedOverride ?: flowOf(
PagingData.from(
messages.filter { it.accountId == accountId && it.folder == folder && it.matchesSearch(query) },
),
@@ -14,7 +14,11 @@ import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTouchInput
import androidx.lifecycle.SavedStateHandle
import androidx.paging.LoadState
import androidx.paging.LoadStates
import androidx.paging.PagingData
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.flow.flowOf
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
@@ -284,4 +288,53 @@ class MailboxScreenTest {
composeTestRule.onNodeWithContentDescription(string(R.string.message_available_offline)).assertIsDisplayed()
}
// Issue #219: on return from the reader the inbox's LazyPagingItems is cold — it presents
// itemCount == 0 with refresh == Loading before the window repopulates. The empty-state gate must
// hold "No messages yet" back through that window, so the empty state never flashes.
@Test
fun emptyState_isHidden_whileTheInboxPagerIsStillLoading() {
val loadingEmpty = flowOf(
PagingData.from(
emptyList<Message>(),
LoadStates(
refresh = LoadState.Loading,
prepend = LoadState.NotLoading(endOfPaginationReached = false),
append = LoadState.NotLoading(endOfPaginationReached = false),
),
),
)
setContent(FakeMailRepository(pagedOverride = loadingEmpty))
// Assert only the gate — no positive "screen composed" anchor. Under a never-completing
// refresh == Loading pager the compose tree never settles deterministically, so no node (not even
// the always-present FAB) is reliable to assert *present* here: it flaked as not-displayed
// (assertIsDisplayed), not-found (assertExists), and waitForText-timeout across CI runs.
// The gate under test (issue #219): the empty state stays hidden while refresh == Loading, so
// "No messages yet" never flashes on return from the reader. This is the stable, meaningful
// assertion — NoMessagesState is composed only once the pager is "settled" (refresh NotLoading
// AND append end-of-pagination reached), which this frozen Loading state never reaches, so the
// string can never appear regardless of when the tree happens to settle.
composeTestRule.onNodeWithText(string(R.string.mailbox_empty)).assertDoesNotExist()
}
// The flip side of the gate: once the pager settles (refresh done, no further page) with no rows,
// the inbox really is empty and "No messages yet" must show.
@Test
fun emptyState_isShown_onceTheInboxPagerSettlesEmpty() {
val settledEmpty = flowOf(
PagingData.from(
emptyList<Message>(),
LoadStates(
refresh = LoadState.NotLoading(endOfPaginationReached = false),
prepend = LoadState.NotLoading(endOfPaginationReached = true),
append = LoadState.NotLoading(endOfPaginationReached = true),
),
),
)
setContent(FakeMailRepository(pagedOverride = settledEmpty))
waitForText(string(R.string.mailbox_empty))
composeTestRule.onNodeWithText(string(R.string.mailbox_empty)).assertIsDisplayed()
}
}