This commit is contained in:
1 file changed
+169
+169
@@ -0,0 +1,169 @@
|
||||
package com.m57.hermescontrol.ui.chat.components
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListItemInfo
|
||||
import androidx.compose.foundation.lazy.LazyListLayoutInfo
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.resetMain
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.coroutines.test.setMain
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class ChatScrollControllerTest {
|
||||
private val testDispatcher = StandardTestDispatcher()
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
Dispatchers.setMain(testDispatcher)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
Dispatchers.resetMain()
|
||||
}
|
||||
|
||||
private fun mockLazyListStateAtBottom(): LazyListState {
|
||||
val listState = mockk<LazyListState>(relaxed = true)
|
||||
val layoutInfo = mockk<LazyListLayoutInfo>(relaxed = true)
|
||||
val itemInfo = mockk<LazyListItemInfo>(relaxed = true)
|
||||
|
||||
every { layoutInfo.totalItemsCount } returns 5
|
||||
every { layoutInfo.viewportEndOffset } returns 1000
|
||||
every { layoutInfo.afterContentPadding } returns 0
|
||||
every { itemInfo.index } returns 4
|
||||
every { itemInfo.offset } returns 800
|
||||
every { itemInfo.size } returns 100
|
||||
every { layoutInfo.visibleItemsInfo } returns listOf(itemInfo)
|
||||
every { listState.layoutInfo } returns layoutInfo
|
||||
|
||||
return listState
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial state has isFollowingBottom true and pendingCount 0`() {
|
||||
val listState = mockk<LazyListState>(relaxed = true)
|
||||
val scope = TestScope(testDispatcher)
|
||||
val controller = ChatScrollController(listState, scope)
|
||||
|
||||
assertTrue(controller.isFollowingBottom)
|
||||
assertEquals(0, controller.pendingCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `new tail while following keeps pendingCount 0 and requests scroll`() =
|
||||
runTest(testDispatcher) {
|
||||
val listState = mockLazyListStateAtBottom()
|
||||
val controller = ChatScrollController(listState, this)
|
||||
|
||||
controller.onTailChanged(tailKey = "msg-1", messageCount = 1)
|
||||
advanceUntilIdle()
|
||||
|
||||
assertTrue(controller.isFollowingBottom)
|
||||
assertEquals(0, controller.pendingCount)
|
||||
coVerify(atLeast = 1) { listState.scrollToItem(4, any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `pauseFollowing followed by new messages increments pendingCount`() {
|
||||
val listState = mockk<LazyListState>(relaxed = true)
|
||||
val scope = TestScope(testDispatcher)
|
||||
val controller = ChatScrollController(listState, scope)
|
||||
|
||||
controller.pauseFollowing()
|
||||
assertFalse(controller.isFollowingBottom)
|
||||
|
||||
controller.onTailChanged(tailKey = "msg-1", messageCount = 1)
|
||||
assertEquals(1, controller.pendingCount)
|
||||
|
||||
controller.onTailChanged(tailKey = "msg-2", messageCount = 3)
|
||||
assertEquals(3, controller.pendingCount)
|
||||
assertFalse(controller.isFollowingBottom)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resumeFollowing clears pendingCount and restores isFollowingBottom`() =
|
||||
runTest(testDispatcher) {
|
||||
val listState = mockLazyListStateAtBottom()
|
||||
val controller = ChatScrollController(listState, this)
|
||||
|
||||
controller.pauseFollowing()
|
||||
controller.onTailChanged(tailKey = "msg-1", messageCount = 2)
|
||||
assertEquals(2, controller.pendingCount)
|
||||
assertFalse(controller.isFollowingBottom)
|
||||
|
||||
controller.resumeFollowing()
|
||||
advanceUntilIdle()
|
||||
|
||||
assertTrue(controller.isFollowingBottom)
|
||||
assertEquals(0, controller.pendingCount)
|
||||
coVerify(atLeast = 1) { listState.animateScrollToItem(4, any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `jumpToBottom clears pendingCount and restores isFollowingBottom`() =
|
||||
runTest(testDispatcher) {
|
||||
val listState = mockLazyListStateAtBottom()
|
||||
val controller = ChatScrollController(listState, this)
|
||||
|
||||
controller.pauseFollowing()
|
||||
controller.onTailChanged(tailKey = "msg-1", messageCount = 4)
|
||||
assertEquals(4, controller.pendingCount)
|
||||
assertFalse(controller.isFollowingBottom)
|
||||
|
||||
controller.jumpToBottom(animated = false)
|
||||
advanceUntilIdle()
|
||||
|
||||
assertTrue(controller.isFollowingBottom)
|
||||
assertEquals(0, controller.pendingCount)
|
||||
coVerify(atLeast = 1) { listState.scrollToItem(4, any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `tail key unchanged causes no change to pendingCount or scroll`() =
|
||||
runTest(testDispatcher) {
|
||||
val listState = mockLazyListStateAtBottom()
|
||||
val controller = ChatScrollController(listState, this)
|
||||
|
||||
controller.pauseFollowing()
|
||||
controller.onTailChanged(tailKey = "same-key", messageCount = 1)
|
||||
assertEquals(1, controller.pendingCount)
|
||||
|
||||
// Repeating the same tailKey should be a no-op
|
||||
controller.onTailChanged(tailKey = "same-key", messageCount = 5)
|
||||
assertEquals(1, controller.pendingCount)
|
||||
|
||||
advanceUntilIdle()
|
||||
coVerify(exactly = 0) { listState.scrollToItem(any(), any()) }
|
||||
coVerify(exactly = 0) { listState.animateScrollToItem(any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `showFab returns true only when paused and content is present`() {
|
||||
val listState = mockk<LazyListState>(relaxed = true)
|
||||
val scope = TestScope(testDispatcher)
|
||||
val controller = ChatScrollController(listState, scope)
|
||||
|
||||
// Following bottom -> false regardless of contentPresent
|
||||
assertFalse(controller.showFab(contentPresent = true))
|
||||
assertFalse(controller.showFab(contentPresent = false))
|
||||
|
||||
controller.pauseFollowing()
|
||||
// Paused -> true only if contentPresent
|
||||
assertTrue(controller.showFab(contentPresent = true))
|
||||
assertFalse(controller.showFab(contentPresent = false))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user