Fix pagination restart issue and cover by unit test.
This commit is contained in:
committed by
Benoit Marty
parent
4ae773cfd6
commit
31a7d3f3bb
@@ -55,8 +55,6 @@ import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.getAndUpdate
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
@@ -213,8 +211,8 @@ class RustTimeline(
|
||||
|
||||
override val timelineItems: Flow<List<MatrixTimelineItem>> = combine(
|
||||
_timelineItems,
|
||||
backPaginationStatus.filter { !it.isPaginating }.distinctUntilChanged(),
|
||||
forwardPaginationStatus.filter { !it.isPaginating }.distinctUntilChanged(),
|
||||
backPaginationStatus,
|
||||
forwardPaginationStatus,
|
||||
matrixRoom.roomInfoFlow.map { it.creator },
|
||||
isTimelineInitialized,
|
||||
) { timelineItems,
|
||||
|
||||
@@ -8,10 +8,12 @@
|
||||
package io.element.android.libraries.matrix.impl.fixtures.fakes
|
||||
|
||||
import org.matrix.rustcomponents.sdk.NoPointer
|
||||
import org.matrix.rustcomponents.sdk.PaginationStatusListener
|
||||
import org.matrix.rustcomponents.sdk.TaskHandle
|
||||
import org.matrix.rustcomponents.sdk.Timeline
|
||||
import org.matrix.rustcomponents.sdk.TimelineDiff
|
||||
import org.matrix.rustcomponents.sdk.TimelineListener
|
||||
import uniffi.matrix_sdk_ui.LiveBackPaginationStatus
|
||||
|
||||
class FakeRustTimeline : Timeline(NoPointer) {
|
||||
private var listener: TimelineListener? = null
|
||||
@@ -23,4 +25,16 @@ class FakeRustTimeline : Timeline(NoPointer) {
|
||||
fun emitDiff(diff: List<TimelineDiff>) {
|
||||
listener!!.onUpdate(diff)
|
||||
}
|
||||
|
||||
private var paginationStatusListener: PaginationStatusListener? = null
|
||||
override suspend fun subscribeToBackPaginationStatus(listener: PaginationStatusListener): TaskHandle {
|
||||
this.paginationStatusListener = listener
|
||||
return FakeRustTaskHandle()
|
||||
}
|
||||
|
||||
fun emitPaginationStatus(status: LiveBackPaginationStatus) {
|
||||
paginationStatusListener!!.onUpdate(status)
|
||||
}
|
||||
|
||||
override suspend fun fetchMembers() = Unit
|
||||
}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
||||
package io.element.android.libraries.matrix.impl.timeline
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
|
||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoomListService
|
||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustTimeline
|
||||
import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustTimelineDiff
|
||||
import io.element.android.libraries.matrix.impl.room.RoomContentForwarder
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||
import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP
|
||||
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runCurrent
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import org.matrix.rustcomponents.sdk.TimelineChange
|
||||
import uniffi.matrix_sdk_ui.LiveBackPaginationStatus
|
||||
import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline
|
||||
|
||||
class RustTimelineTest {
|
||||
@Test
|
||||
fun `ensure that the timeline emits new loading item when pagination does not bring new events`() = runTest {
|
||||
val inner = FakeRustTimeline()
|
||||
val systemClock = FakeSystemClock()
|
||||
val sut = createRustTimeline(
|
||||
inner = inner,
|
||||
systemClock = systemClock,
|
||||
)
|
||||
sut.timelineItems.test {
|
||||
// Give time for the listener to be set
|
||||
runCurrent()
|
||||
inner.emitDiff(
|
||||
listOf(
|
||||
FakeRustTimelineDiff(
|
||||
item = null,
|
||||
change = TimelineChange.RESET,
|
||||
)
|
||||
)
|
||||
)
|
||||
with(awaitItem()) {
|
||||
assertThat(size).isEqualTo(1)
|
||||
// Typing notification
|
||||
assertThat((get(0) as MatrixTimelineItem.Virtual).virtual).isEqualTo(VirtualTimelineItem.TypingNotification)
|
||||
}
|
||||
with(awaitItem()) {
|
||||
assertThat(size).isEqualTo(2)
|
||||
// The loading
|
||||
assertThat((get(0) as MatrixTimelineItem.Virtual).virtual).isEqualTo(
|
||||
VirtualTimelineItem.LoadingIndicator(
|
||||
direction = Timeline.PaginationDirection.BACKWARDS,
|
||||
timestamp = A_FAKE_TIMESTAMP,
|
||||
)
|
||||
)
|
||||
// Typing notification
|
||||
assertThat((get(1) as MatrixTimelineItem.Virtual).virtual).isEqualTo(VirtualTimelineItem.TypingNotification)
|
||||
}
|
||||
systemClock.epochMillisResult = A_FAKE_TIMESTAMP + 1
|
||||
// Start pagination
|
||||
sut.paginate(Timeline.PaginationDirection.BACKWARDS)
|
||||
// Simulate SDK starting pagination
|
||||
inner.emitPaginationStatus(LiveBackPaginationStatus.Paginating)
|
||||
// No new events received
|
||||
// Simulate SDK stopping pagination, more event to load
|
||||
inner.emitPaginationStatus(LiveBackPaginationStatus.Idle(hitStartOfTimeline = false))
|
||||
// expect an item to be emitted, with an updated timestamp
|
||||
with(awaitItem()) {
|
||||
assertThat(size).isEqualTo(2)
|
||||
// The loading
|
||||
assertThat((get(0) as MatrixTimelineItem.Virtual).virtual).isEqualTo(
|
||||
VirtualTimelineItem.LoadingIndicator(
|
||||
direction = Timeline.PaginationDirection.BACKWARDS,
|
||||
timestamp = A_FAKE_TIMESTAMP + 1,
|
||||
)
|
||||
)
|
||||
// Typing notification
|
||||
assertThat((get(1) as MatrixTimelineItem.Virtual).virtual).isEqualTo(VirtualTimelineItem.TypingNotification)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun TestScope.createRustTimeline(
|
||||
inner: InnerTimeline,
|
||||
mode: Timeline.Mode = Timeline.Mode.LIVE,
|
||||
systemClock: SystemClock = FakeSystemClock(),
|
||||
matrixRoom: MatrixRoom = FakeMatrixRoom().apply { givenRoomInfo(aRoomInfo()) },
|
||||
coroutineScope: CoroutineScope = backgroundScope,
|
||||
dispatcher: CoroutineDispatcher = testCoroutineDispatchers().io,
|
||||
roomContentForwarder: RoomContentForwarder = RoomContentForwarder(FakeRustRoomListService()),
|
||||
featureFlagsService: FeatureFlagService = FakeFeatureFlagService(),
|
||||
onNewSyncedEvent: () -> Unit = {},
|
||||
): RustTimeline {
|
||||
return RustTimeline(
|
||||
inner = inner,
|
||||
mode = mode,
|
||||
systemClock = systemClock,
|
||||
matrixRoom = matrixRoom,
|
||||
coroutineScope = coroutineScope,
|
||||
dispatcher = dispatcher,
|
||||
roomContentForwarder = roomContentForwarder,
|
||||
featureFlagsService = featureFlagsService,
|
||||
onNewSyncedEvent = onNewSyncedEvent,
|
||||
)
|
||||
}
|
||||
@@ -11,8 +11,8 @@ import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||
|
||||
const val A_FAKE_TIMESTAMP = 123L
|
||||
|
||||
class FakeSystemClock : SystemClock {
|
||||
override fun epochMillis(): Long {
|
||||
return A_FAKE_TIMESTAMP
|
||||
}
|
||||
class FakeSystemClock(
|
||||
var epochMillisResult: Long = A_FAKE_TIMESTAMP
|
||||
) : SystemClock {
|
||||
override fun epochMillis() = epochMillisResult
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user