Fix pagination restart issue and cover by unit test.

This commit is contained in:
Benoit Marty
2025-01-23 17:44:31 +01:00
committed by Benoit Marty
parent 4ae773cfd6
commit 31a7d3f3bb
4 changed files with 144 additions and 8 deletions

View File

@@ -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,

View File

@@ -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
}

View File

@@ -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,
)
}

View File

@@ -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
}