From 9f44cff4035030c90fc6c0d2a5b0a41c6aba3a71 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 27 Sep 2024 17:02:10 +0200 Subject: [PATCH] Add test on TimelineItemsSubscriber. --- .../factories/EventTimelineItemDebugInfo.kt | 20 +++ .../fakes/FakeRustEventTimelineItem.kt | 44 ++++++ .../impl/fixtures/fakes/FakeRustTimeline.kt | 27 ++++ .../fixtures/fakes/FakeRustTimelineItem.kt | 6 +- .../fakes/FakeRustTimelineItemContent.kt | 18 +++ .../MatrixTimelineDiffProcessorTest.kt | 3 +- .../timeline/TimelineItemsSubscriberTest.kt | 139 ++++++++++++++++++ 7 files changed, 254 insertions(+), 3 deletions(-) create mode 100644 libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/EventTimelineItemDebugInfo.kt create mode 100644 libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustEventTimelineItem.kt create mode 100644 libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTimeline.kt create mode 100644 libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTimelineItemContent.kt create mode 100644 libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/TimelineItemsSubscriberTest.kt diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/EventTimelineItemDebugInfo.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/EventTimelineItemDebugInfo.kt new file mode 100644 index 0000000000..6bf870b320 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/EventTimelineItemDebugInfo.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.fixtures.factories + +import org.matrix.rustcomponents.sdk.EventTimelineItemDebugInfo + +fun anEventTimelineItemDebugInfo( + model: String = "model", + originalJson: String? = null, + latestEditJson: String? = null, +) = EventTimelineItemDebugInfo( + model = model, + originalJson = originalJson, + latestEditJson = latestEditJson +) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustEventTimelineItem.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustEventTimelineItem.kt new file mode 100644 index 0000000000..3efb1ec151 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustEventTimelineItem.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.fixtures.fakes + +import io.element.android.libraries.matrix.impl.fixtures.factories.anEventTimelineItemDebugInfo +import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.A_USER_ID +import org.matrix.rustcomponents.sdk.EventSendState +import org.matrix.rustcomponents.sdk.EventTimelineItem +import org.matrix.rustcomponents.sdk.EventTimelineItemDebugInfo +import org.matrix.rustcomponents.sdk.NoPointer +import org.matrix.rustcomponents.sdk.ProfileDetails +import org.matrix.rustcomponents.sdk.Reaction +import org.matrix.rustcomponents.sdk.Receipt +import org.matrix.rustcomponents.sdk.ShieldState +import org.matrix.rustcomponents.sdk.TimelineItemContent +import uniffi.matrix_sdk_ui.EventItemOrigin + +class FakeRustEventTimelineItem( + private val origin: EventItemOrigin? = null, +) : EventTimelineItem(NoPointer) { + override fun origin(): EventItemOrigin? = origin + override fun eventId(): String = AN_EVENT_ID.value + override fun transactionId(): String? = null + override fun isEditable(): Boolean = false + override fun canBeRepliedTo(): Boolean = false + override fun isLocal(): Boolean = false + override fun isOwn(): Boolean = false + override fun isRemote(): Boolean = false + override fun localSendState(): EventSendState? = null + override fun reactions(): List = emptyList() + override fun readReceipts(): Map = emptyMap() + override fun sender(): String = A_USER_ID.value + override fun senderProfile(): ProfileDetails = ProfileDetails.Unavailable + override fun timestamp(): ULong = 0u + override fun content(): TimelineItemContent = FakeRustTimelineItemContent() + override fun debugInfo(): EventTimelineItemDebugInfo = anEventTimelineItemDebugInfo() + override fun getShield(strict: Boolean): ShieldState? = null +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTimeline.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTimeline.kt new file mode 100644 index 0000000000..605cc7d98d --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTimeline.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.fixtures.fakes + +import org.matrix.rustcomponents.sdk.NoPointer +import org.matrix.rustcomponents.sdk.TaskHandle +import org.matrix.rustcomponents.sdk.Timeline +import org.matrix.rustcomponents.sdk.TimelineDiff +import org.matrix.rustcomponents.sdk.TimelineListener + +class FakeRustTimeline( +) : Timeline(NoPointer) { + private var listener: TimelineListener? = null + override suspend fun addListener(listener: TimelineListener): TaskHandle { + this.listener = listener + return FakeRustTaskHandle() + } + + fun emitDiff(diff: List) { + listener!!.onUpdate(diff) + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTimelineItem.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTimelineItem.kt index c20698fe62..f6d46799c3 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTimelineItem.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTimelineItem.kt @@ -12,8 +12,10 @@ import org.matrix.rustcomponents.sdk.NoPointer import org.matrix.rustcomponents.sdk.TimelineItem import org.matrix.rustcomponents.sdk.VirtualTimelineItem -class FakeRustTimelineItem : TimelineItem(NoPointer) { - override fun asEvent(): EventTimelineItem? = null +class FakeRustTimelineItem( + private val asEventResult: EventTimelineItem? = null, +) : TimelineItem(NoPointer) { + override fun asEvent(): EventTimelineItem? = asEventResult override fun asVirtual(): VirtualTimelineItem? = null override fun fmtDebug(): String = "fmtDebug" override fun uniqueId(): String = "uniqueId" diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTimelineItemContent.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTimelineItemContent.kt new file mode 100644 index 0000000000..14bdde0682 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTimelineItemContent.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.fixtures.fakes + +import org.matrix.rustcomponents.sdk.Message +import org.matrix.rustcomponents.sdk.NoPointer +import org.matrix.rustcomponents.sdk.TimelineItemContent +import org.matrix.rustcomponents.sdk.TimelineItemContentKind + +class FakeRustTimelineItemContent : TimelineItemContent(NoPointer) { + override fun asMessage(): Message? = null + override fun kind(): TimelineItemContentKind = TimelineItemContentKind.Message +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineDiffProcessorTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineDiffProcessorTest.kt index 3327b2a9d5..2f0b27a833 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineDiffProcessorTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineDiffProcessorTest.kt @@ -16,6 +16,7 @@ import io.element.android.libraries.matrix.impl.timeline.item.virtual.VirtualTim import io.element.android.libraries.matrix.test.A_UNIQUE_ID import io.element.android.libraries.matrix.test.A_UNIQUE_ID_2 import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest @@ -155,7 +156,7 @@ class MatrixTimelineDiffProcessorTest { } internal fun TestScope.createMatrixTimelineDiffProcessor( - timelineItems: MutableStateFlow>, + timelineItems: MutableSharedFlow>, ): MatrixTimelineDiffProcessor { val timelineEventContentMapper = TimelineEventContentMapper() val timelineItemMapper = MatrixTimelineItemMapper( diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/TimelineItemsSubscriberTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/TimelineItemsSubscriberTest.kt new file mode 100644 index 0000000000..c47ec9f040 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/TimelineItemsSubscriberTest.kt @@ -0,0 +1,139 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +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.matrix.api.timeline.MatrixTimelineItem +import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustEventTimelineItem +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.fixtures.fakes.FakeRustTimelineItem +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.runCancellableScopeTestWithTestScope +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import org.junit.Test +import org.matrix.rustcomponents.sdk.Timeline +import org.matrix.rustcomponents.sdk.TimelineChange +import uniffi.matrix_sdk_ui.EventItemOrigin + +@OptIn(ExperimentalCoroutinesApi::class) +class TimelineItemsSubscriberTest { + @Test + fun `when timeline emits an empty list of items, the flow must emits an empty list`() = runCancellableScopeTestWithTestScope { testScope, cancellableScope -> + val timelineItems: MutableSharedFlow> = + MutableSharedFlow(replay = 1, extraBufferCapacity = Int.MAX_VALUE) + val timeline = FakeRustTimeline() + val timelineItemsSubscriber = testScope.createTimelineItemsSubscriber( + coroutineScope = cancellableScope, + timeline = timeline, + timelineItems = timelineItems, + ) + timelineItems.test { + timelineItemsSubscriber.subscribeIfNeeded() + // Wait for the listener to be set. + testScope.runCurrent() + timeline.emitDiff(listOf(FakeRustTimelineDiff(item = null, change = TimelineChange.RESET))) + val final = awaitItem() + assertThat(final).isEmpty() + timelineItemsSubscriber.unsubscribeIfNeeded() + } + } + + @Test + fun `when timeline emits a non empty list of items, the flow must emits a non empty list`() = runCancellableScopeTestWithTestScope { testScope, cancellableScope -> + val timelineItems: MutableSharedFlow> = + MutableSharedFlow(replay = 1, extraBufferCapacity = Int.MAX_VALUE) + val timeline = FakeRustTimeline() + val timelineItemsSubscriber = testScope.createTimelineItemsSubscriber( + coroutineScope = cancellableScope, + timeline = timeline, + timelineItems = timelineItems, + ) + timelineItems.test { + timelineItemsSubscriber.subscribeIfNeeded() + // Wait for the listener to be set. + testScope.runCurrent() + timeline.emitDiff(listOf(FakeRustTimelineDiff(item = FakeRustTimelineItem(), change = TimelineChange.RESET))) + val final = awaitItem() + assertThat(final).isNotEmpty() + timelineItemsSubscriber.unsubscribeIfNeeded() + } + } + + @Test + fun `when timeline emits an item with SYNC origin, the callback onNewSyncedEvent is invoked`() = runCancellableScopeTestWithTestScope { testScope, cancellableScope -> + val timelineItems: MutableSharedFlow> = + MutableSharedFlow(replay = 1, extraBufferCapacity = Int.MAX_VALUE) + val timeline = FakeRustTimeline() + val onNewSyncedEventRecorder = lambdaRecorder { } + val timelineItemsSubscriber = testScope.createTimelineItemsSubscriber( + coroutineScope = cancellableScope, + timeline = timeline, + timelineItems = timelineItems, + onNewSyncedEvent = onNewSyncedEventRecorder, + ) + timelineItems.test { + timelineItemsSubscriber.subscribeIfNeeded() + // Wait for the listener to be set. + testScope.runCurrent() + timeline.emitDiff( + listOf( + FakeRustTimelineDiff( + item = FakeRustTimelineItem( + asEventResult = FakeRustEventTimelineItem(origin = EventItemOrigin.SYNC) + ), + change = TimelineChange.RESET, + ) + ) + ) + val final = awaitItem() + assertThat(final).isNotEmpty() + timelineItemsSubscriber.unsubscribeIfNeeded() + } + onNewSyncedEventRecorder.assertions().isCalledOnce() + } + + @Test + fun `multiple subscriptions does not have side effect`() = runCancellableScopeTestWithTestScope { testScope, cancellableScope -> + val timelineItemsSubscriber = testScope.createTimelineItemsSubscriber( + coroutineScope = cancellableScope, + ) + timelineItemsSubscriber.subscribeIfNeeded() + timelineItemsSubscriber.subscribeIfNeeded() + timelineItemsSubscriber.unsubscribeIfNeeded() + timelineItemsSubscriber.unsubscribeIfNeeded() + } + + private fun TestScope.createTimelineItemsSubscriber( + coroutineScope: CoroutineScope, + timeline: Timeline = FakeRustTimeline(), + timelineItems: MutableSharedFlow> = MutableSharedFlow(replay = 1, extraBufferCapacity = Int.MAX_VALUE), + initLatch: CompletableDeferred = CompletableDeferred(), + isTimelineInitialized: MutableStateFlow = MutableStateFlow(false), + onNewSyncedEvent: () -> Unit = { lambdaError() }, + ): TimelineItemsSubscriber { + return TimelineItemsSubscriber( + timelineCoroutineScope = coroutineScope, + dispatcher = StandardTestDispatcher(testScheduler), + timeline = timeline, + timelineDiffProcessor = createMatrixTimelineDiffProcessor(timelineItems), + initLatch = initLatch, + isTimelineInitialized = isTimelineInitialized, + onNewSyncedEvent = onNewSyncedEvent, + ) + } +}