Fix pagination restart issue and cover by unit test.
This commit is contained in:
parent
4ae773cfd6
commit
31a7d3f3bb
4 changed files with 144 additions and 8 deletions
|
|
@ -55,8 +55,6 @@ import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
|
||||||
import kotlinx.coroutines.flow.filter
|
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.getAndUpdate
|
import kotlinx.coroutines.flow.getAndUpdate
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
|
@ -213,8 +211,8 @@ class RustTimeline(
|
||||||
|
|
||||||
override val timelineItems: Flow<List<MatrixTimelineItem>> = combine(
|
override val timelineItems: Flow<List<MatrixTimelineItem>> = combine(
|
||||||
_timelineItems,
|
_timelineItems,
|
||||||
backPaginationStatus.filter { !it.isPaginating }.distinctUntilChanged(),
|
backPaginationStatus,
|
||||||
forwardPaginationStatus.filter { !it.isPaginating }.distinctUntilChanged(),
|
forwardPaginationStatus,
|
||||||
matrixRoom.roomInfoFlow.map { it.creator },
|
matrixRoom.roomInfoFlow.map { it.creator },
|
||||||
isTimelineInitialized,
|
isTimelineInitialized,
|
||||||
) { timelineItems,
|
) { timelineItems,
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,12 @@
|
||||||
package io.element.android.libraries.matrix.impl.fixtures.fakes
|
package io.element.android.libraries.matrix.impl.fixtures.fakes
|
||||||
|
|
||||||
import org.matrix.rustcomponents.sdk.NoPointer
|
import org.matrix.rustcomponents.sdk.NoPointer
|
||||||
|
import org.matrix.rustcomponents.sdk.PaginationStatusListener
|
||||||
import org.matrix.rustcomponents.sdk.TaskHandle
|
import org.matrix.rustcomponents.sdk.TaskHandle
|
||||||
import org.matrix.rustcomponents.sdk.Timeline
|
import org.matrix.rustcomponents.sdk.Timeline
|
||||||
import org.matrix.rustcomponents.sdk.TimelineDiff
|
import org.matrix.rustcomponents.sdk.TimelineDiff
|
||||||
import org.matrix.rustcomponents.sdk.TimelineListener
|
import org.matrix.rustcomponents.sdk.TimelineListener
|
||||||
|
import uniffi.matrix_sdk_ui.LiveBackPaginationStatus
|
||||||
|
|
||||||
class FakeRustTimeline : Timeline(NoPointer) {
|
class FakeRustTimeline : Timeline(NoPointer) {
|
||||||
private var listener: TimelineListener? = null
|
private var listener: TimelineListener? = null
|
||||||
|
|
@ -23,4 +25,16 @@ class FakeRustTimeline : Timeline(NoPointer) {
|
||||||
fun emitDiff(diff: List<TimelineDiff>) {
|
fun emitDiff(diff: List<TimelineDiff>) {
|
||||||
listener!!.onUpdate(diff)
|
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
|
const val A_FAKE_TIMESTAMP = 123L
|
||||||
|
|
||||||
class FakeSystemClock : SystemClock {
|
class FakeSystemClock(
|
||||||
override fun epochMillis(): Long {
|
var epochMillisResult: Long = A_FAKE_TIMESTAMP
|
||||||
return A_FAKE_TIMESTAMP
|
) : SystemClock {
|
||||||
}
|
override fun epochMillis() = epochMillisResult
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue