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..f40d0ffbea --- /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..bcb64a9219 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeRustTimeline.kt @@ -0,0 +1,26 @@ +/* + * 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/roomdirectory/RustRoomDirectoryListTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryListTest.kt index 71003b5b9a..6dff45c815 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryListTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryListTest.kt @@ -13,12 +13,12 @@ import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryList import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomDescription import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRoomDirectorySearch import io.element.android.libraries.matrix.test.A_ROOM_ID_2 -import io.element.android.tests.testutils.runCancellableScopeTestWithTestScope import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest import org.junit.Test import org.matrix.rustcomponents.sdk.RoomDirectorySearch import org.matrix.rustcomponents.sdk.RoomDirectorySearchEntryUpdate @@ -26,15 +26,15 @@ import org.matrix.rustcomponents.sdk.RoomDirectorySearchEntryUpdate @OptIn(ExperimentalCoroutinesApi::class) class RustRoomDirectoryListTest { @Test - fun `check that the state emits the expected values`() = runCancellableScopeTestWithTestScope { testScope, cancellableScope -> + fun `check that the state emits the expected values`() = runTest { val fakeRoomDirectorySearch = FakeRoomDirectorySearch() val mapper = RoomDescriptionMapper() - val sut = testScope.createRustRoomDirectoryList( + val sut = createRustRoomDirectoryList( roomDirectorySearch = fakeRoomDirectorySearch, - scope = cancellableScope, + scope = backgroundScope, ) // Let the mxCallback be ready - testScope.runCurrent() + runCurrent() sut.state.test { sut.filter("", 20) fakeRoomDirectorySearch.emitResult( diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryServiceTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryServiceTest.kt index 06b26bcdd4..1e4664af7f 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryServiceTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomdirectory/RustRoomDirectoryServiceTest.kt @@ -8,18 +8,18 @@ package io.element.android.libraries.matrix.impl.roomdirectory import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustClient -import io.element.android.tests.testutils.runCancellableScopeTestWithTestScope import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest import org.junit.Test class RustRoomDirectoryServiceTest { @Test - fun test() = runCancellableScopeTestWithTestScope { testScope, cancellableScope -> + fun test() = runTest { val client = FakeRustClient() val sut = RustRoomDirectoryService( client = client, - sessionDispatcher = StandardTestDispatcher(testScope.testScheduler), + sessionDispatcher = StandardTestDispatcher(testScheduler), ) - sut.createRoomDirectoryList(cancellableScope) + sut.createRoomDirectoryList(backgroundScope) } } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFactoryTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFactoryTest.kt index cd961b54da..234a5c6f33 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFactoryTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RoomListFactoryTest.kt @@ -9,16 +9,16 @@ package io.element.android.libraries.matrix.impl.roomlist import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoomList import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoomListService -import io.element.android.tests.testutils.runCancellableScopeTest +import kotlinx.coroutines.test.runTest import org.junit.Test import kotlin.coroutines.EmptyCoroutineContext class RoomListFactoryTest { @Test - fun `createRoomList should work`() = runCancellableScopeTest { + fun `createRoomList should work`() = runTest { val sut = RoomListFactory( innerRoomListService = FakeRustRoomListService(), - sessionCoroutineScope = it, + sessionCoroutineScope = backgroundScope, ) sut.createRoomList( pageSize = 10, diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomListServiceTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomListServiceTest.kt index b8af0c2e65..9a04807d6d 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomListServiceTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/roomlist/RustRoomListServiceTest.kt @@ -11,13 +11,13 @@ import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeRustRoomListService import io.element.android.libraries.matrix.impl.room.RoomSyncSubscriber -import io.element.android.tests.testutils.runCancellableScopeTestWithTestScope import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest import org.junit.Test import org.matrix.rustcomponents.sdk.RoomListServiceSyncIndicator import org.matrix.rustcomponents.sdk.RoomListService as RustRoomListService @@ -25,14 +25,14 @@ import org.matrix.rustcomponents.sdk.RoomListService as RustRoomListService @OptIn(ExperimentalCoroutinesApi::class) class RustRoomListServiceTest { @Test - fun `syncIndicator should emit the expected values`() = runCancellableScopeTestWithTestScope { testScope, cancellableScope -> + fun `syncIndicator should emit the expected values`() = runTest { val roomListService = FakeRustRoomListService() - val sut = testScope.createRustRoomListService( - sessionCoroutineScope = cancellableScope, + val sut = createRustRoomListService( + sessionCoroutineScope = backgroundScope, roomListService = roomListService, ) // Give time for mxCallback to setup - testScope.runCurrent() + runCurrent() sut.syncIndicator.test { assertThat(awaitItem()).isEqualTo(RoomListService.SyncIndicator.Hide) roomListService.emitRoomListServiceSyncIndicator(RoomListServiceSyncIndicator.SHOW) 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 d442d004d6..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 @@ -31,7 +32,7 @@ class MatrixTimelineDiffProcessorTest { @Test fun `Append adds new entries at the end of the list`() = runTest { timelineItems.value = listOf(anEvent) - val processor = createProcessor() + val processor = createMatrixTimelineDiffProcessor(timelineItems) processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.APPEND))) assertThat(timelineItems.value.count()).isEqualTo(2) assertThat(timelineItems.value).containsExactly( @@ -43,7 +44,7 @@ class MatrixTimelineDiffProcessorTest { @Test fun `PushBack adds a new entry at the end of the list`() = runTest { timelineItems.value = listOf(anEvent) - val processor = createProcessor() + val processor = createMatrixTimelineDiffProcessor(timelineItems) processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.PUSH_BACK))) assertThat(timelineItems.value.count()).isEqualTo(2) assertThat(timelineItems.value).containsExactly( @@ -55,7 +56,7 @@ class MatrixTimelineDiffProcessorTest { @Test fun `PushFront inserts a new entry at the start of the list`() = runTest { timelineItems.value = listOf(anEvent) - val processor = createProcessor() + val processor = createMatrixTimelineDiffProcessor(timelineItems) processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.PUSH_FRONT))) assertThat(timelineItems.value.count()).isEqualTo(2) assertThat(timelineItems.value).containsExactly( @@ -67,7 +68,7 @@ class MatrixTimelineDiffProcessorTest { @Test fun `Set replaces an entry at some index`() = runTest { timelineItems.value = listOf(anEvent, anEvent2) - val processor = createProcessor() + val processor = createMatrixTimelineDiffProcessor(timelineItems) processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.SET))) assertThat(timelineItems.value.count()).isEqualTo(2) assertThat(timelineItems.value).containsExactly( @@ -79,7 +80,7 @@ class MatrixTimelineDiffProcessorTest { @Test fun `Insert inserts a new entry at the provided index`() = runTest { timelineItems.value = listOf(anEvent, anEvent2) - val processor = createProcessor() + val processor = createMatrixTimelineDiffProcessor(timelineItems) processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.INSERT))) assertThat(timelineItems.value.count()).isEqualTo(3) assertThat(timelineItems.value).containsExactly( @@ -92,7 +93,7 @@ class MatrixTimelineDiffProcessorTest { @Test fun `Remove removes an entry at some index`() = runTest { timelineItems.value = listOf(anEvent, MatrixTimelineItem.Other, anEvent2) - val processor = createProcessor() + val processor = createMatrixTimelineDiffProcessor(timelineItems) processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.REMOVE))) assertThat(timelineItems.value.count()).isEqualTo(2) assertThat(timelineItems.value).containsExactly( @@ -104,7 +105,7 @@ class MatrixTimelineDiffProcessorTest { @Test fun `PopBack removes an entry at the end of the list`() = runTest { timelineItems.value = listOf(anEvent, anEvent2) - val processor = createProcessor() + val processor = createMatrixTimelineDiffProcessor(timelineItems) processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.POP_BACK))) assertThat(timelineItems.value.count()).isEqualTo(1) assertThat(timelineItems.value).containsExactly( @@ -115,7 +116,7 @@ class MatrixTimelineDiffProcessorTest { @Test fun `PopFront removes an entry at the start of the list`() = runTest { timelineItems.value = listOf(anEvent, anEvent2) - val processor = createProcessor() + val processor = createMatrixTimelineDiffProcessor(timelineItems) processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.POP_FRONT))) assertThat(timelineItems.value.count()).isEqualTo(1) assertThat(timelineItems.value).containsExactly( @@ -126,7 +127,7 @@ class MatrixTimelineDiffProcessorTest { @Test fun `Clear removes all the entries`() = runTest { timelineItems.value = listOf(anEvent, anEvent2) - val processor = createProcessor() + val processor = createMatrixTimelineDiffProcessor(timelineItems) processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.CLEAR))) assertThat(timelineItems.value).isEmpty() } @@ -134,7 +135,7 @@ class MatrixTimelineDiffProcessorTest { @Test fun `Truncate removes all entries after the provided length`() = runTest { timelineItems.value = listOf(anEvent, MatrixTimelineItem.Other, anEvent2) - val processor = createProcessor() + val processor = createMatrixTimelineDiffProcessor(timelineItems) processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.TRUNCATE))) assertThat(timelineItems.value.count()).isEqualTo(1) assertThat(timelineItems.value).containsExactly( @@ -145,27 +146,29 @@ class MatrixTimelineDiffProcessorTest { @Test fun `Reset removes all entries and add the provided ones`() = runTest { timelineItems.value = listOf(anEvent, MatrixTimelineItem.Other, anEvent2) - val processor = createProcessor() + val processor = createMatrixTimelineDiffProcessor(timelineItems) processor.postDiffs(listOf(FakeRustTimelineDiff(change = TimelineChange.RESET))) assertThat(timelineItems.value.count()).isEqualTo(1) assertThat(timelineItems.value).containsExactly( MatrixTimelineItem.Other, ) } +} - private fun TestScope.createProcessor(): MatrixTimelineDiffProcessor { - val timelineEventContentMapper = TimelineEventContentMapper() - val timelineItemMapper = MatrixTimelineItemMapper( - fetchDetailsForEvent = { _ -> Result.success(Unit) }, - coroutineScope = this, - virtualTimelineItemMapper = VirtualTimelineItemMapper(), - eventTimelineItemMapper = EventTimelineItemMapper( - contentMapper = timelineEventContentMapper - ) - ) - return MatrixTimelineDiffProcessor( - timelineItems, - timelineItemFactory = timelineItemMapper, +internal fun TestScope.createMatrixTimelineDiffProcessor( + timelineItems: MutableSharedFlow>, +): MatrixTimelineDiffProcessor { + val timelineEventContentMapper = TimelineEventContentMapper() + val timelineItemMapper = MatrixTimelineItemMapper( + fetchDetailsForEvent = { _ -> Result.success(Unit) }, + coroutineScope = this, + virtualTimelineItemMapper = VirtualTimelineItemMapper(), + eventTimelineItemMapper = EventTimelineItemMapper( + contentMapper = timelineEventContentMapper ) - } + ) + return MatrixTimelineDiffProcessor( + timelineItems = timelineItems, + timelineItemFactory = timelineItemMapper, + ) } 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..4333525fe0 --- /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 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 kotlinx.coroutines.test.runTest +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`() = runTest { + val timelineItems: MutableSharedFlow> = + MutableSharedFlow(replay = 1, extraBufferCapacity = Int.MAX_VALUE) + val timeline = FakeRustTimeline() + val timelineItemsSubscriber = createTimelineItemsSubscriber( + coroutineScope = backgroundScope, + timeline = timeline, + timelineItems = timelineItems, + ) + timelineItems.test { + timelineItemsSubscriber.subscribeIfNeeded() + // Wait for the listener to be set. + 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`() = runTest { + val timelineItems: MutableSharedFlow> = + MutableSharedFlow(replay = 1, extraBufferCapacity = Int.MAX_VALUE) + val timeline = FakeRustTimeline() + val timelineItemsSubscriber = createTimelineItemsSubscriber( + coroutineScope = backgroundScope, + timeline = timeline, + timelineItems = timelineItems, + ) + timelineItems.test { + timelineItemsSubscriber.subscribeIfNeeded() + // Wait for the listener to be set. + 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`() = runTest { + val timelineItems: MutableSharedFlow> = + MutableSharedFlow(replay = 1, extraBufferCapacity = Int.MAX_VALUE) + val timeline = FakeRustTimeline() + val onNewSyncedEventRecorder = lambdaRecorder { } + val timelineItemsSubscriber = createTimelineItemsSubscriber( + coroutineScope = backgroundScope, + timeline = timeline, + timelineItems = timelineItems, + onNewSyncedEvent = onNewSyncedEventRecorder, + ) + timelineItems.test { + timelineItemsSubscriber.subscribeIfNeeded() + // Wait for the listener to be set. + 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`() = runTest { + val timelineItemsSubscriber = createTimelineItemsSubscriber( + coroutineScope = backgroundScope, + ) + 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, + ) +} diff --git a/services/analytics/impl/src/test/kotlin/io/element/android/services/analytics/impl/DefaultAnalyticsServiceTest.kt b/services/analytics/impl/src/test/kotlin/io/element/android/services/analytics/impl/DefaultAnalyticsServiceTest.kt index 2646d3c567..991e5f1732 100644 --- a/services/analytics/impl/src/test/kotlin/io/element/android/services/analytics/impl/DefaultAnalyticsServiceTest.kt +++ b/services/analytics/impl/src/test/kotlin/io/element/android/services/analytics/impl/DefaultAnalyticsServiceTest.kt @@ -24,7 +24,6 @@ import io.element.android.services.analyticsproviders.api.AnalyticsProvider import io.element.android.services.analyticsproviders.test.FakeAnalyticsProvider import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value -import io.element.android.tests.testutils.runCancellableScopeTest import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -37,13 +36,13 @@ import org.junit.Test class DefaultAnalyticsServiceTest { @Test - fun `getAvailableAnalyticsProviders return the set of provider`() = runCancellableScopeTest { + fun `getAvailableAnalyticsProviders return the set of provider`() = runTest { val providers = setOf( FakeAnalyticsProvider(name = "provider1", stopLambda = { }), FakeAnalyticsProvider(name = "provider2", stopLambda = { }), ) val sut = createDefaultAnalyticsService( - coroutineScope = it, + coroutineScope = backgroundScope, analyticsProviders = providers ) val result = sut.getAvailableAnalyticsProviders() @@ -51,17 +50,17 @@ class DefaultAnalyticsServiceTest { } @Test - fun `when consent is not provided, capture is no op`() = runCancellableScopeTest { - val sut = createDefaultAnalyticsService(it) + fun `when consent is not provided, capture is no op`() = runTest { + val sut = createDefaultAnalyticsService(backgroundScope) sut.capture(anEvent) } @Test - fun `when consent is provided, capture is sent to the AnalyticsProvider`() = runCancellableScopeTest { + fun `when consent is provided, capture is sent to the AnalyticsProvider`() = runTest { val initLambda = lambdaRecorder { } val captureLambda = lambdaRecorder { _ -> } val sut = createDefaultAnalyticsService( - coroutineScope = it, + coroutineScope = backgroundScope, analyticsStore = FakeAnalyticsStore(defaultUserConsent = true), analyticsProviders = setOf( FakeAnalyticsProvider( @@ -76,17 +75,17 @@ class DefaultAnalyticsServiceTest { } @Test - fun `when consent is not provided, screen is no op`() = runCancellableScopeTest { - val sut = createDefaultAnalyticsService(it) + fun `when consent is not provided, screen is no op`() = runTest { + val sut = createDefaultAnalyticsService(backgroundScope) sut.screen(aScreen) } @Test - fun `when consent is provided, screen is sent to the AnalyticsProvider`() = runCancellableScopeTest { + fun `when consent is provided, screen is sent to the AnalyticsProvider`() = runTest { val initLambda = lambdaRecorder { } val screenLambda = lambdaRecorder { _ -> } val sut = createDefaultAnalyticsService( - coroutineScope = it, + coroutineScope = backgroundScope, analyticsStore = FakeAnalyticsStore(defaultUserConsent = true), analyticsProviders = setOf( FakeAnalyticsProvider( @@ -101,17 +100,17 @@ class DefaultAnalyticsServiceTest { } @Test - fun `when consent is not provided, trackError is no op`() = runCancellableScopeTest { - val sut = createDefaultAnalyticsService(it) + fun `when consent is not provided, trackError is no op`() = runTest { + val sut = createDefaultAnalyticsService(backgroundScope) sut.trackError(anError) } @Test - fun `when consent is provided, trackError is sent to the AnalyticsProvider`() = runCancellableScopeTest { + fun `when consent is provided, trackError is sent to the AnalyticsProvider`() = runTest { val initLambda = lambdaRecorder { } val trackErrorLambda = lambdaRecorder { _ -> } val sut = createDefaultAnalyticsService( - coroutineScope = it, + coroutineScope = backgroundScope, analyticsStore = FakeAnalyticsStore(defaultUserConsent = true), analyticsProviders = setOf( FakeAnalyticsProvider( @@ -126,10 +125,10 @@ class DefaultAnalyticsServiceTest { } @Test - fun `setUserConsent is sent to the store`() = runCancellableScopeTest { + fun `setUserConsent is sent to the store`() = runTest { val store = FakeAnalyticsStore() val sut = createDefaultAnalyticsService( - coroutineScope = it, + coroutineScope = backgroundScope, analyticsStore = store, ) assertThat(store.userConsentFlow.first()).isFalse() @@ -140,10 +139,10 @@ class DefaultAnalyticsServiceTest { } @Test - fun `setAnalyticsId is sent to the store`() = runCancellableScopeTest { + fun `setAnalyticsId is sent to the store`() = runTest { val store = FakeAnalyticsStore() val sut = createDefaultAnalyticsService( - coroutineScope = it, + coroutineScope = backgroundScope, analyticsStore = store, ) assertThat(store.analyticsIdFlow.first()).isEqualTo("") @@ -154,10 +153,10 @@ class DefaultAnalyticsServiceTest { } @Test - fun `setDidAskUserConsent is sent to the store`() = runCancellableScopeTest { + fun `setDidAskUserConsent is sent to the store`() = runTest { val store = FakeAnalyticsStore() val sut = createDefaultAnalyticsService( - coroutineScope = it, + coroutineScope = backgroundScope, analyticsStore = store, ) assertThat(store.didAskUserConsentFlow.first()).isFalse() @@ -168,13 +167,13 @@ class DefaultAnalyticsServiceTest { } @Test - fun `when a session is deleted, the store is reset`() = runCancellableScopeTest { + fun `when a session is deleted, the store is reset`() = runTest { val resetLambda = lambdaRecorder { } val store = FakeAnalyticsStore( resetLambda = resetLambda, ) val sut = createDefaultAnalyticsService( - coroutineScope = it, + coroutineScope = backgroundScope, analyticsStore = store, ) sut.onSessionDeleted("userId") @@ -182,12 +181,12 @@ class DefaultAnalyticsServiceTest { } @Test - fun `when reset is invoked, the user consent is reset`() = runCancellableScopeTest { + fun `when reset is invoked, the user consent is reset`() = runTest { val store = FakeAnalyticsStore( defaultDidAskUserConsent = true, ) val sut = createDefaultAnalyticsService( - coroutineScope = it, + coroutineScope = backgroundScope, analyticsStore = store, ) assertThat(store.didAskUserConsentFlow.first()).isTrue() @@ -196,9 +195,9 @@ class DefaultAnalyticsServiceTest { } @Test - fun `when a session is added, nothing happen`() = runCancellableScopeTest { + fun `when a session is added, nothing happen`() = runTest { val sut = createDefaultAnalyticsService( - coroutineScope = it, + coroutineScope = backgroundScope, ) sut.onSessionCreated("userId") } @@ -231,10 +230,10 @@ class DefaultAnalyticsServiceTest { } @Test - fun `when consent is provided, updateUserProperties is sent to the provider`() = runCancellableScopeTest { + fun `when consent is provided, updateUserProperties is sent to the provider`() = runTest { val updateUserPropertiesLambda = lambdaRecorder { _ -> } val sut = createDefaultAnalyticsService( - coroutineScope = it, + coroutineScope = backgroundScope, analyticsProviders = setOf( FakeAnalyticsProvider( initLambda = { }, @@ -248,10 +247,10 @@ class DefaultAnalyticsServiceTest { } @Test - fun `when super properties are updated, updateSuperProperties is sent to the provider`() = runCancellableScopeTest { + fun `when super properties are updated, updateSuperProperties is sent to the provider`() = runTest { val updateSuperPropertiesLambda = lambdaRecorder { _ -> } val sut = createDefaultAnalyticsService( - coroutineScope = it, + coroutineScope = backgroundScope, analyticsProviders = setOf( FakeAnalyticsProvider( initLambda = { }, diff --git a/services/appnavstate/impl/src/test/kotlin/io/element/android/services/appnavstate/impl/DefaultNavigationStateServiceTest.kt b/services/appnavstate/impl/src/test/kotlin/io/element/android/services/appnavstate/impl/DefaultNavigationStateServiceTest.kt index ba4dc4d72e..306a935ab9 100644 --- a/services/appnavstate/impl/src/test/kotlin/io/element/android/services/appnavstate/impl/DefaultNavigationStateServiceTest.kt +++ b/services/appnavstate/impl/src/test/kotlin/io/element/android/services/appnavstate/impl/DefaultNavigationStateServiceTest.kt @@ -23,9 +23,9 @@ import io.element.android.services.appnavstate.test.A_SESSION_OWNER import io.element.android.services.appnavstate.test.A_SPACE_OWNER import io.element.android.services.appnavstate.test.A_THREAD_OWNER import io.element.android.services.appnavstate.test.FakeAppForegroundStateService -import io.element.android.tests.testutils.runCancellableScopeTest import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest import org.junit.Test class DefaultNavigationStateServiceTest { @@ -51,8 +51,8 @@ class DefaultNavigationStateServiceTest { ) @Test - fun testNavigation() = runCancellableScopeTest { scope -> - val service = createStateService(scope) + fun testNavigation() = runTest { + val service = createStateService(backgroundScope) assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoot) service.onNavigateToSession(A_SESSION_OWNER, A_SESSION_ID) assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSession) @@ -74,15 +74,15 @@ class DefaultNavigationStateServiceTest { } @Test - fun testFailure() = runCancellableScopeTest { scope -> - val service = createStateService(scope) + fun testFailure() = runTest { + val service = createStateService(backgroundScope) service.onNavigateToSpace(A_SPACE_OWNER, A_SPACE_ID) assertThat(service.appNavigationState.value.navigationState).isEqualTo(NavigationState.Root) } @Test - fun testOnNavigateToThread() = runCancellableScopeTest { scope -> - val service = createStateService(scope) + fun testOnNavigateToThread() = runTest { + val service = createStateService(backgroundScope) // From root (no effect) service.onNavigateToThread(A_THREAD_OWNER, A_THREAD_ID) assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoot) @@ -110,8 +110,8 @@ class DefaultNavigationStateServiceTest { } @Test - fun testOnNavigateToRoom() = runCancellableScopeTest { scope -> - val service = createStateService(scope) + fun testOnNavigateToRoom() = runTest { + val service = createStateService(backgroundScope) // From root (no effect) service.onNavigateToRoom(A_ROOM_OWNER, A_ROOM_ID) assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoot) @@ -139,8 +139,8 @@ class DefaultNavigationStateServiceTest { } @Test - fun testOnNavigateToSpace() = runCancellableScopeTest { scope -> - val service = createStateService(scope) + fun testOnNavigateToSpace() = runTest { + val service = createStateService(backgroundScope) // From root (no effect) service.onNavigateToSpace(A_SPACE_OWNER, A_SPACE_ID) assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoot) @@ -168,8 +168,8 @@ class DefaultNavigationStateServiceTest { } @Test - fun testOnNavigateToSession() = runCancellableScopeTest { scope -> - val service = createStateService(scope) + fun testOnNavigateToSession() = runTest { + val service = createStateService(backgroundScope) // From root service.onNavigateToSession(A_SESSION_OWNER, A_SESSION_ID) assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateSession) @@ -197,8 +197,8 @@ class DefaultNavigationStateServiceTest { } @Test - fun testOnLeavingThread() = runCancellableScopeTest { scope -> - val service = createStateService(scope) + fun testOnLeavingThread() = runTest { + val service = createStateService(backgroundScope) // From root (no effect) service.onLeavingThread(A_THREAD_OWNER) assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoot) @@ -225,8 +225,8 @@ class DefaultNavigationStateServiceTest { } @Test - fun testOnLeavingRoom() = runCancellableScopeTest { scope -> - val service = createStateService(scope) + fun testOnLeavingRoom() = runTest { + val service = createStateService(backgroundScope) // From root (no effect) service.onLeavingRoom(A_ROOM_OWNER) assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoot) @@ -253,8 +253,8 @@ class DefaultNavigationStateServiceTest { } @Test - fun testOnLeavingSpace() = runCancellableScopeTest { scope -> - val service = createStateService(scope) + fun testOnLeavingSpace() = runTest { + val service = createStateService(backgroundScope) // From root (no effect) service.onLeavingSpace(A_SPACE_OWNER) assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoot) @@ -281,8 +281,8 @@ class DefaultNavigationStateServiceTest { } @Test - fun testOnLeavingSession() = runCancellableScopeTest { scope -> - val service = createStateService(scope) + fun testOnLeavingSession() = runTest { + val service = createStateService(backgroundScope) // From root service.onLeavingSession(A_SESSION_OWNER) assertThat(service.appNavigationState.first().navigationState).isEqualTo(navigationStateRoot) diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/RunCancellableTest.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/RunCancellableTest.kt deleted file mode 100644 index c3074aaf35..0000000000 --- a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/RunCancellableTest.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2023, 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.tests.testutils - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runTest - -/** - * Run a test with a [CoroutineScope] that will be cancelled automatically and avoiding failing the test. - */ -fun runCancellableScopeTest(block: suspend (CoroutineScope) -> Unit) = runTest { - val scope = CoroutineScope(coroutineContext + SupervisorJob()) - block(scope) - scope.cancel() -} - -/** - * Run a test with a [CoroutineScope] that will be cancelled automatically and avoiding failing the test. - */ -fun runCancellableScopeTestWithTestScope(block: suspend (testScope: TestScope, cancellableScope: CoroutineScope) -> Unit) = runTest { - val scope = CoroutineScope(coroutineContext + SupervisorJob()) - block(this, scope) - scope.cancel() -}