Browse Source

Timeline permalink : start updating tests

pull/2759/head
ganfra 5 months ago
parent
commit
bf87b975fc
  1. 2
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt
  2. 5
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenter.kt
  3. 6
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt
  4. 37
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt
  5. 3
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt
  6. 4
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt
  7. 4
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt
  8. 6
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt
  9. 6
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/TimelineItemsFactoryFixtures.kt
  10. 3
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenterTests.kt
  11. 2
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt
  12. 287
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt
  13. 18
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt
  14. 6
      features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/PollFixtures.kt
  15. 4
      features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt
  16. 2
      features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenterTest.kt
  17. 9
      libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt
  18. 96
      libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeMatrixTimeline.kt
  19. 205
      libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt

2
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt

@ -288,7 +288,7 @@ class MessagesPresenter @AssistedInject constructor(
emoji: String, emoji: String,
eventId: EventId, eventId: EventId,
) = launch(dispatchers.io) { ) = launch(dispatchers.io) {
timelineController.invokeOnTimeline { timelineController.invokeOnCurrentTimeline {
toggleReaction(emoji, eventId) toggleReaction(emoji, eventId)
.onFailure { Timber.e(it) } .onFailure { Timber.e(it) }
} }

5
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenter.kt

@ -30,6 +30,7 @@ import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.timeline.TimelineProvider
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toPersistentList import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -37,8 +38,8 @@ import kotlinx.coroutines.launch
class ForwardMessagesPresenter @AssistedInject constructor( class ForwardMessagesPresenter @AssistedInject constructor(
@Assisted eventId: String, @Assisted eventId: String,
private val room: MatrixRoom,
private val matrixCoroutineScope: CoroutineScope, private val matrixCoroutineScope: CoroutineScope,
private val timelineProvider: TimelineProvider,
) : Presenter<ForwardMessagesState> { ) : Presenter<ForwardMessagesState> {
private val eventId: EventId = EventId(eventId) private val eventId: EventId = EventId(eventId)
@ -79,7 +80,7 @@ class ForwardMessagesPresenter @AssistedInject constructor(
isForwardMessagesState: MutableState<AsyncData<ImmutableList<RoomId>>>, isForwardMessagesState: MutableState<AsyncData<ImmutableList<RoomId>>>,
) = launch { ) = launch {
isForwardMessagesState.value = AsyncData.Loading() isForwardMessagesState.value = AsyncData.Loading()
room.forwardEvent(eventId, roomIds).fold( timelineProvider.getActiveTimeline().forwardEvent(eventId, roomIds).fold(
{ isForwardMessagesState.value = AsyncData.Success(roomIds) }, { isForwardMessagesState.value = AsyncData.Success(roomIds) },
{ isForwardMessagesState.value = AsyncData.Failure(it) } { isForwardMessagesState.value = AsyncData.Failure(it) }
) )

6
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt

@ -266,7 +266,7 @@ class MessageComposerPresenter @Inject constructor(
is MessageComposerMode.Quote -> null is MessageComposerMode.Quote -> null
}.let { relatedEventId -> }.let { relatedEventId ->
appCoroutineScope.launch { appCoroutineScope.launch {
timelineController.invokeOnTimeline { timelineController.invokeOnCurrentTimeline {
enterSpecialMode(relatedEventId) enterSpecialMode(relatedEventId)
} }
} }
@ -390,14 +390,14 @@ class MessageComposerPresenter @Inject constructor(
is MessageComposerMode.Edit -> { is MessageComposerMode.Edit -> {
val eventId = capturedMode.eventId val eventId = capturedMode.eventId
val transactionId = capturedMode.transactionId val transactionId = capturedMode.transactionId
timelineController.invokeOnTimeline { timelineController.invokeOnCurrentTimeline {
editMessage(eventId, transactionId, message.markdown, message.html, mentions) editMessage(eventId, transactionId, message.markdown, message.html, mentions)
} }
} }
is MessageComposerMode.Quote -> TODO() is MessageComposerMode.Quote -> TODO()
is MessageComposerMode.Reply -> { is MessageComposerMode.Reply -> {
timelineController.invokeOnTimeline { timelineController.invokeOnCurrentTimeline {
replyMessage(capturedMode.eventId, message.markdown, message.html, mentions) replyMessage(capturedMode.eventId, message.markdown, message.html, mentions)
} }
} }

37
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineController.kt

@ -16,9 +16,7 @@
package io.element.android.features.messages.impl.timeline package io.element.android.features.messages.impl.timeline
import androidx.compose.runtime.MutableState
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.di.SingleIn import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
@ -26,9 +24,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.timeline.LiveTimelineProvider import io.element.android.libraries.matrix.api.timeline.LiveTimelineProvider
import io.element.android.libraries.matrix.api.timeline.TimelineProvider import io.element.android.libraries.matrix.api.timeline.TimelineProvider
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.ReceiptType
import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.Timeline
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -65,7 +61,7 @@ class TimelineController @Inject constructor(
return detachedTimeline.map { !it.isPresent } return detachedTimeline.map { !it.isPresent }
} }
suspend fun invokeOnTimeline(block: suspend (Timeline.() -> Any)) { suspend fun invokeOnCurrentTimeline(block: suspend (Timeline.() -> Any)) {
currentTimelineFlow().first().run { currentTimelineFlow().first().run {
block(this) block(this)
} }
@ -124,37 +120,6 @@ class TimelineController @Inject constructor(
} }
} }
suspend fun sendReadReceiptIfNeeded(
firstVisibleIndex: Int,
timelineItems: ImmutableList<TimelineItem>,
lastReadReceiptId: MutableState<EventId?>,
readReceiptType: ReceiptType,
) {
// If we are at the bottom of timeline, we mark the room as read.
if (firstVisibleIndex == 0) {
room.markAsRead(receiptType = readReceiptType)
} else {
// Get last valid EventId seen by the user, as the first index might refer to a Virtual item
val eventId = getLastEventIdBeforeOrAt(firstVisibleIndex, timelineItems)
if (eventId != null && eventId != lastReadReceiptId.value) {
lastReadReceiptId.value = eventId
currentTimelineFlow()
.first()
.sendReadReceipt(eventId = eventId, receiptType = readReceiptType)
}
}
}
private fun getLastEventIdBeforeOrAt(index: Int, items: ImmutableList<TimelineItem>): EventId? {
for (i in index until items.count()) {
val item = items[i]
if (item is TimelineItem.Event) {
return item.eventId
}
}
return null
}
override suspend fun getActiveTimeline(): Timeline { override suspend fun getActiveTimeline(): Timeline {
return currentTimelineFlow().first() return currentTimelineFlow().first()
} }

3
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt

@ -111,6 +111,7 @@ class TimelinePresenter @AssistedInject constructor(
if (event.firstIndex == 0) { if (event.firstIndex == 0) {
newEventState.value = NewEventState.None newEventState.value = NewEventState.None
} }
println("## sendReadReceiptIfNeeded firstVisibleIndex: ${event.firstIndex}")
appScope.sendReadReceiptIfNeeded( appScope.sendReadReceiptIfNeeded(
firstVisibleIndex = event.firstIndex, firstVisibleIndex = event.firstIndex,
timelineItems = timelineItems, timelineItems = timelineItems,
@ -256,7 +257,7 @@ class TimelinePresenter @AssistedInject constructor(
val eventId = getLastEventIdBeforeOrAt(firstVisibleIndex, timelineItems) val eventId = getLastEventIdBeforeOrAt(firstVisibleIndex, timelineItems)
if (eventId != null && eventId != lastReadReceiptId.value) { if (eventId != null && eventId != lastReadReceiptId.value) {
lastReadReceiptId.value = eventId lastReadReceiptId.value = eventId
//timeline.sendReadReceipt(eventId = eventId, receiptType = readReceiptType) room.liveTimeline.sendReadReceipt(eventId = eventId, receiptType = readReceiptType)
} }
} }
} }

4
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt

@ -116,10 +116,6 @@ fun TimelineView(
// TODO implement this logic once we have support to 'jump to event X' in sliding sync // TODO implement this logic once we have support to 'jump to event X' in sliding sync
} }
LaunchedEffect(key1 = state.timelineItems) {
Timber.d("TimelineView - timelineItem identifiers: ${state.timelineItems.joinToString(", ") { it.identifier() }}")
}
// Animate alpha when timeline is first displayed, to avoid flashes or glitching when viewing rooms // Animate alpha when timeline is first displayed, to avoid flashes or glitching when viewing rooms
AnimatedVisibility(visible = true, enter = fadeIn()) { AnimatedVisibility(visible = true, enter = fadeIn()) {
Box(modifier) { Box(modifier) {

4
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt

@ -62,10 +62,6 @@ class TimelineItemsFactory @Inject constructor(
} }
} }
fun items(): StateFlow<ImmutableList<TimelineItem>> {
return timelineItems
}
@Composable @Composable
fun collectItemsAsState(): State<ImmutableList<TimelineItem>> { fun collectItemsAsState(): State<ImmutableList<TimelineItem>> {
return timelineItems.collectAsState() return timelineItems.collectAsState()

6
features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt

@ -31,6 +31,8 @@ import io.element.android.features.messages.impl.messagecomposer.MessageComposer
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
import io.element.android.features.messages.impl.messagesummary.FakeMessageSummaryFormatter import io.element.android.features.messages.impl.messagesummary.FakeMessageSummaryFormatter
import io.element.android.features.messages.impl.textcomposer.TestRichTextEditorStateFactory import io.element.android.features.messages.impl.textcomposer.TestRichTextEditorStateFactory
import io.element.android.features.messages.impl.timeline.TimelineController
import io.element.android.features.messages.impl.timeline.TimelineItemIndexer
import io.element.android.features.messages.impl.timeline.TimelinePresenter import io.element.android.features.messages.impl.timeline.TimelinePresenter
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionPresenter import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionPresenter
import io.element.android.features.messages.impl.timeline.components.customreaction.FakeEmojibaseProvider import io.element.android.features.messages.impl.timeline.components.customreaction.FakeEmojibaseProvider
@ -748,6 +750,7 @@ class MessagesPresenterTest {
currentSessionIdHolder = CurrentSessionIdHolder(FakeMatrixClient(A_SESSION_ID)), currentSessionIdHolder = CurrentSessionIdHolder(FakeMatrixClient(A_SESSION_ID)),
permalinkParser = FakePermalinkParser(), permalinkParser = FakePermalinkParser(),
permalinkBuilder = FakePermalinkBuilder(), permalinkBuilder = FakePermalinkBuilder(),
timelineController = TimelineController(matrixRoom),
) )
val voiceMessageComposerPresenter = VoiceMessageComposerPresenter( val voiceMessageComposerPresenter = VoiceMessageComposerPresenter(
this, this,
@ -768,6 +771,8 @@ class MessagesPresenterTest {
endPollAction = endPollAction, endPollAction = endPollAction,
sendPollResponseAction = FakeSendPollResponseAction(), sendPollResponseAction = FakeSendPollResponseAction(),
sessionPreferencesStore = sessionPreferencesStore, sessionPreferencesStore = sessionPreferencesStore,
timelineItemIndexer = TimelineItemIndexer(),
timelineController = TimelineController(matrixRoom),
) )
val timelinePresenterFactory = object : TimelinePresenter.Factory { val timelinePresenterFactory = object : TimelinePresenter.Factory {
override fun create(navigator: MessagesNavigator): TimelinePresenter { override fun create(navigator: MessagesNavigator): TimelinePresenter {
@ -804,6 +809,7 @@ class MessagesPresenterTest {
buildMeta = aBuildMeta(), buildMeta = aBuildMeta(),
dispatchers = coroutineDispatchers, dispatchers = coroutineDispatchers,
htmlConverterProvider = FakeHtmlConverterProvider(), htmlConverterProvider = FakeHtmlConverterProvider(),
timelineController = TimelineController(matrixRoom),
) )
} }
} }

6
features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/TimelineItemsFactoryFixtures.kt

@ -16,6 +16,7 @@
package io.element.android.features.messages.impl.fixtures package io.element.android.features.messages.impl.fixtures
import io.element.android.features.messages.impl.timeline.TimelineItemIndexer
import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentFactory import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentFactory
import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentFailedToParseMessageFactory import io.element.android.features.messages.impl.timeline.factories.event.TimelineItemContentFailedToParseMessageFactory
@ -46,7 +47,9 @@ import io.element.android.libraries.mediaviewer.api.util.FileExtensionExtractorW
import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
internal fun TestScope.aTimelineItemsFactory(): TimelineItemsFactory { internal fun TestScope.aTimelineItemsFactory(
timelineItemIndexer: TimelineItemIndexer = TimelineItemIndexer()
): TimelineItemsFactory {
val timelineEventFormatter = aTimelineEventFormatter() val timelineEventFormatter = aTimelineEventFormatter()
val matrixClient = FakeMatrixClient() val matrixClient = FakeMatrixClient()
return TimelineItemsFactory( return TimelineItemsFactory(
@ -83,6 +86,7 @@ internal fun TestScope.aTimelineItemsFactory(): TimelineItemsFactory {
), ),
), ),
timelineItemGrouper = TimelineItemGrouper(), timelineItemGrouper = TimelineItemGrouper(),
timelineItemIndexer = timelineItemIndexer,
) )
} }

3
features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenterTests.kt

@ -21,6 +21,7 @@ import app.cash.molecule.moleculeFlow
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.timeline.LiveTimelineProvider
import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.aRoomSummaryDetails import io.element.android.libraries.matrix.test.room.aRoomSummaryDetails
@ -91,7 +92,7 @@ class ForwardMessagesPresenterTests {
coroutineScope: CoroutineScope = this, coroutineScope: CoroutineScope = this,
) = ForwardMessagesPresenter( ) = ForwardMessagesPresenter(
eventId = eventId.value, eventId = eventId.value,
room = fakeMatrixRoom, timelineProvider = LiveTimelineProvider(fakeMatrixRoom),
matrixCoroutineScope = coroutineScope, matrixCoroutineScope = coroutineScope,
) )
} }

2
features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt

@ -32,6 +32,7 @@ import io.element.android.features.messages.impl.messagecomposer.MessageComposer
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
import io.element.android.features.messages.impl.timeline.TimelineController
import io.element.android.features.preferences.api.store.SessionPreferencesStore import io.element.android.features.preferences.api.store.SessionPreferencesStore
import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
@ -968,6 +969,7 @@ class MessageComposerPresenterTest {
permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionPresenter), permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionPresenter),
permalinkParser = FakePermalinkParser(), permalinkParser = FakePermalinkParser(),
permalinkBuilder = permalinkBuilder, permalinkBuilder = permalinkBuilder,
timelineController = TimelineController(room),
) )
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T { private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {

287
features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt

@ -35,9 +35,11 @@ import io.element.android.features.poll.test.actions.FakeEndPollAction
import io.element.android.features.poll.test.actions.FakeSendPollResponseAction import io.element.android.features.poll.test.actions.FakeSendPollResponseAction
import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.InMemorySessionPreferencesStore import io.element.android.libraries.featureflag.test.InMemorySessionPreferencesStore
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.api.timeline.ReceiptType
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.event.EventReaction import io.element.android.libraries.matrix.api.timeline.item.event.EventReaction
import io.element.android.libraries.matrix.api.timeline.item.event.ReactionSender import io.element.android.libraries.matrix.api.timeline.item.event.ReactionSender
import io.element.android.libraries.matrix.api.timeline.item.event.Receipt import io.element.android.libraries.matrix.api.timeline.item.event.Receipt
@ -47,20 +49,28 @@ import io.element.android.libraries.matrix.test.AN_EVENT_ID_2
import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.aRoomMember import io.element.android.libraries.matrix.test.room.aRoomMember
import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline import io.element.android.libraries.matrix.test.timeline.FakeTimeline
import io.element.android.libraries.matrix.test.timeline.aMessageContent import io.element.android.libraries.matrix.test.timeline.aMessageContent
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
import io.element.android.libraries.matrix.ui.components.aMatrixUserList import io.element.android.libraries.matrix.ui.components.aMatrixUserList
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.awaitLastSequentialItem import io.element.android.tests.testutils.awaitLastSequentialItem
import io.element.android.tests.testutils.awaitWithLatch
import io.element.android.tests.testutils.consumeItemsUntilPredicate import io.element.android.tests.testutils.consumeItemsUntilPredicate
import io.element.android.tests.testutils.lambda.any
import io.element.android.tests.testutils.lambda.assert
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.getAndUpdate
import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
@ -71,7 +81,7 @@ import kotlin.time.Duration.Companion.seconds
private const val FAKE_UNIQUE_ID = "FAKE_UNIQUE_ID" private const val FAKE_UNIQUE_ID = "FAKE_UNIQUE_ID"
private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2" private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2"
class TimelinePresenterTest { @OptIn(ExperimentalCoroutinesApi::class) class TimelinePresenterTest {
@get:Rule @get:Rule
val warmUpRule = WarmUpRule() val warmUpRule = WarmUpRule()
@ -83,58 +93,49 @@ class TimelinePresenterTest {
}.test { }.test {
val initialState = awaitFirstItem() val initialState = awaitFirstItem()
assertThat(initialState.timelineItems).isEmpty() assertThat(initialState.timelineItems).isEmpty()
val loadedNoTimelineState = awaitItem() assertThat(initialState.isLive).isTrue()
assertThat(loadedNoTimelineState.timelineItems).isEmpty() assertThat(initialState.newEventState).isEqualTo(NewEventState.None)
assertThat(initialState.focusedEventId).isNull()
assertThat(initialState.focusRequestState).isEqualTo(FocusRequestState.None)
} }
} }
@Test @Test
fun `present - load more`() = runTest { fun `present - load more`() = runTest {
val presenter = createTimelinePresenter() val paginateLambda = lambdaRecorder { _: Timeline.PaginationDirection ->
moleculeFlow(RecompositionMode.Immediate) { Result.success(false)
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.backPaginationStatus.hasMoreToLoadBackwards).isTrue()
assertThat(initialState.backPaginationStatus.isBackPaginating).isFalse()
initialState.eventSink.invoke(TimelineEvents.LoadMore)
val inPaginationState = awaitItem()
assertThat(inPaginationState.backPaginationStatus.isBackPaginating).isTrue()
assertThat(inPaginationState.backPaginationStatus.hasMoreToLoadBackwards).isTrue()
val postPaginationState = awaitItem()
assertThat(postPaginationState.backPaginationStatus.hasMoreToLoadBackwards).isTrue()
assertThat(postPaginationState.backPaginationStatus.isBackPaginating).isFalse()
} }
} val timeline = FakeTimeline().apply {
this.paginateLambda = paginateLambda
@Test }
fun `present - set highlighted event`() = runTest { val presenter = createTimelinePresenter(timeline = timeline)
val presenter = createTimelinePresenter()
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitFirstItem() val initialState = awaitItem()
skipItems(1) initialState.eventSink.invoke(TimelineEvents.LoadMore(Timeline.PaginationDirection.BACKWARDS))
assertThat(initialState.highlightedEventId).isNull() initialState.eventSink.invoke(TimelineEvents.LoadMore(Timeline.PaginationDirection.FORWARDS))
initialState.eventSink.invoke(TimelineEvents.SetHighlightedEvent(AN_EVENT_ID)) assert(paginateLambda)
val withHighlightedState = awaitItem() .isCalledExactly(2)
assertThat(withHighlightedState.highlightedEventId).isEqualTo(AN_EVENT_ID) .withSequence(
initialState.eventSink.invoke(TimelineEvents.SetHighlightedEvent(null)) listOf(value(Timeline.PaginationDirection.BACKWARDS)),
val withoutHighlightedState = awaitItem() listOf(value(Timeline.PaginationDirection.FORWARDS))
assertThat(withoutHighlightedState.highlightedEventId).isNull() )
} }
} }
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
@Test @Test
fun `present - on scroll finished mark a room as read if the first visible index is 0`() = runTest(StandardTestDispatcher()) { fun `present - on scroll finished mark a room as read if the first visible index is 0`() = runTest(StandardTestDispatcher()) {
val timeline = FakeMatrixTimeline( val timeline = FakeTimeline(
initialTimelineItems = listOf( timelineItems = flowOf(
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem()) listOf(
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem())
)
) )
) )
val room = FakeMatrixRoom(liveTimeline = timeline)
val sessionPreferencesStore = InMemorySessionPreferencesStore(isSendPublicReadReceiptsEnabled = false) val sessionPreferencesStore = InMemorySessionPreferencesStore(isSendPublicReadReceiptsEnabled = false)
val room = FakeMatrixRoom(matrixTimeline = timeline)
val presenter = createTimelinePresenter( val presenter = createTimelinePresenter(
timeline = timeline, timeline = timeline,
room = room, room = room,
@ -143,7 +144,6 @@ class TimelinePresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
assertThat(timeline.sentReadReceipts).isEmpty()
val initialState = awaitFirstItem() val initialState = awaitFirstItem()
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0)) initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0))
runCurrent() runCurrent()
@ -154,48 +154,62 @@ class TimelinePresenterTest {
@Test @Test
fun `present - on scroll finished send read receipt if an event is before the index`() = runTest { fun `present - on scroll finished send read receipt if an event is before the index`() = runTest {
val timeline = FakeMatrixTimeline( val sendReadReceiptsLambda = lambdaRecorder { _: EventId, _: ReceiptType ->
initialTimelineItems = listOf( Result.success(Unit)
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem()), }
MatrixTimelineItem.Event( val timeline = FakeTimeline(
uniqueId = FAKE_UNIQUE_ID_2, timelineItems = flowOf(
event = anEventTimelineItem( listOf(
eventId = AN_EVENT_ID_2, MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem()),
content = aMessageContent("Test message") MatrixTimelineItem.Event(
uniqueId = FAKE_UNIQUE_ID_2,
event = anEventTimelineItem(
eventId = AN_EVENT_ID_2,
content = aMessageContent("Test message")
)
) )
) )
) )
) ).apply {
this.sendReadReceiptLambda = sendReadReceiptsLambda
}
val presenter = createTimelinePresenter(timeline) val presenter = createTimelinePresenter(timeline)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
assertThat(timeline.sentReadReceipts).isEmpty() skipItems(1)
val initialState = awaitFirstItem() awaitItem().run {
awaitWithLatch { latch -> eventSink.invoke(TimelineEvents.OnScrollFinished(1))
timeline.sendReadReceiptLatch = latch
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(1))
} }
assertThat(timeline.sentReadReceipts).isNotEmpty() advanceUntilIdle()
assertThat(timeline.sentReadReceipts.first().second).isEqualTo(ReceiptType.READ) assert(sendReadReceiptsLambda)
.isCalledOnce()
.with(any(), value(ReceiptType.READ))
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
} }
} }
@Test @Test
fun `present - on scroll finished send a private read receipt if an event is at an index other than 0 and public read receipts are disabled`() = runTest { fun `present - on scroll finished send a private read receipt if an event is at an index other than 0 and public read receipts are disabled`() = runTest {
val timeline = FakeMatrixTimeline( val sendReadReceiptsLambda = lambdaRecorder { _: EventId, _: ReceiptType ->
initialTimelineItems = listOf( Result.success(Unit)
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem()), }
MatrixTimelineItem.Event( val timeline = FakeTimeline(
uniqueId = FAKE_UNIQUE_ID_2, timelineItems = flowOf(
event = anEventTimelineItem( listOf(
eventId = AN_EVENT_ID_2, MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem()),
content = aMessageContent("Test message") MatrixTimelineItem.Event(
uniqueId = FAKE_UNIQUE_ID_2,
event = anEventTimelineItem(
eventId = AN_EVENT_ID_2,
content = aMessageContent("Test message")
)
) )
) )
) )
) ).apply {
this.sendReadReceiptLambda = sendReadReceiptsLambda
}
val sessionPreferencesStore = InMemorySessionPreferencesStore(isSendPublicReadReceiptsEnabled = false) val sessionPreferencesStore = InMemorySessionPreferencesStore(isSendPublicReadReceiptsEnabled = false)
val presenter = createTimelinePresenter( val presenter = createTimelinePresenter(
timeline = timeline, timeline = timeline,
@ -204,75 +218,86 @@ class TimelinePresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
assertThat(timeline.sentReadReceipts).isEmpty() skipItems(1)
val initialState = awaitFirstItem() awaitItem().run {
awaitWithLatch { latch -> eventSink.invoke(TimelineEvents.OnScrollFinished(0))
timeline.sendReadReceiptLatch = latch eventSink.invoke(TimelineEvents.OnScrollFinished(1))
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0))
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(1))
} }
assertThat(timeline.sentReadReceipts).isNotEmpty() advanceUntilIdle()
assertThat(timeline.sentReadReceipts.first().second).isEqualTo(ReceiptType.READ_PRIVATE) assert(sendReadReceiptsLambda)
.isCalledOnce()
.with(any(), value(ReceiptType.READ_PRIVATE))
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
} }
} }
@Test @Test
fun `present - on scroll finished will not send read receipt the first visible event is the same as before`() = runTest { fun `present - on scroll finished will not send read receipt the first visible event is the same as before`() = runTest {
val timeline = FakeMatrixTimeline( val sendReadReceiptsLambda = lambdaRecorder { _: EventId, _: ReceiptType ->
initialTimelineItems = listOf( Result.success(Unit)
MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem()), }
MatrixTimelineItem.Event( val timeline = FakeTimeline(
uniqueId = FAKE_UNIQUE_ID_2, timelineItems = flowOf(
event = anEventTimelineItem( listOf(
eventId = AN_EVENT_ID_2, MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem()),
content = aMessageContent("Test message") MatrixTimelineItem.Event(
uniqueId = FAKE_UNIQUE_ID_2,
event = anEventTimelineItem(
eventId = AN_EVENT_ID_2,
content = aMessageContent("Test message")
)
) )
) )
) )
) ).apply {
this.sendReadReceiptLambda = sendReadReceiptsLambda
}
val presenter = createTimelinePresenter(timeline) val presenter = createTimelinePresenter(timeline)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
assertThat(timeline.sentReadReceipts).isEmpty() skipItems(1)
val initialState = awaitFirstItem() awaitItem().run {
awaitWithLatch { latch -> eventSink.invoke(TimelineEvents.OnScrollFinished(1))
timeline.sendReadReceiptLatch = latch eventSink.invoke(TimelineEvents.OnScrollFinished(1))
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(1))
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(1))
} }
assertThat(timeline.sentReadReceipts).hasSize(1) advanceUntilIdle()
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
assert(sendReadReceiptsLambda).isCalledOnce()
} }
} }
@Test @Test
fun `present - on scroll finished will not send read receipt only virtual events exist before the index`() = runTest { fun `present - on scroll finished will not send read receipt only virtual events exist before the index`() = runTest {
val timeline = FakeMatrixTimeline( val sendReadReceiptsLambda = lambdaRecorder { _: EventId, _: ReceiptType ->
initialTimelineItems = listOf( Result.success(Unit)
MatrixTimelineItem.Virtual(FAKE_UNIQUE_ID, VirtualTimelineItem.ReadMarker), }
MatrixTimelineItem.Virtual(FAKE_UNIQUE_ID, VirtualTimelineItem.ReadMarker) val timeline = FakeTimeline(
timelineItems = flowOf(
listOf(
MatrixTimelineItem.Virtual(FAKE_UNIQUE_ID, VirtualTimelineItem.ReadMarker),
MatrixTimelineItem.Virtual(FAKE_UNIQUE_ID, VirtualTimelineItem.ReadMarker)
)
) )
) ).apply {
this.sendReadReceiptLambda = sendReadReceiptsLambda
}
val presenter = createTimelinePresenter(timeline) val presenter = createTimelinePresenter(timeline)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
assertThat(timeline.sentReadReceipts).isEmpty() skipItems(1)
val initialState = awaitFirstItem() val initialState = awaitFirstItem()
awaitWithLatch { latch -> initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(1))
timeline.sendReadReceiptLatch = latch
initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(1))
}
assertThat(timeline.sentReadReceipts).isEmpty()
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
assert(sendReadReceiptsLambda).isNeverCalled()
} }
} }
@Test @Test
fun `present - covers newEventState scenarios`() = runTest { fun `present - covers newEventState scenarios`() = runTest {
val timeline = FakeMatrixTimeline() val timelineItems = MutableStateFlow(emptyList<MatrixTimelineItem>())
val timeline = FakeTimeline(timelineItems = timelineItems)
val presenter = createTimelinePresenter(timeline) val presenter = createTimelinePresenter(timeline)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
@ -280,12 +305,12 @@ class TimelinePresenterTest {
val initialState = awaitFirstItem() val initialState = awaitFirstItem()
assertThat(initialState.newEventState).isEqualTo(NewEventState.None) assertThat(initialState.newEventState).isEqualTo(NewEventState.None)
assertThat(initialState.timelineItems.size).isEqualTo(0) assertThat(initialState.timelineItems.size).isEqualTo(0)
timeline.updateTimelineItems { timelineItems.emit(
listOf(MatrixTimelineItem.Event("0", anEventTimelineItem(content = aMessageContent()))) listOf(MatrixTimelineItem.Event("0", anEventTimelineItem(content = aMessageContent())))
} )
consumeItemsUntilPredicate { it.timelineItems.size == 1 } consumeItemsUntilPredicate { it.timelineItems.size == 1 }
// Mimics sending a message, and assert newEventState is FromMe // Mimics sending a message, and assert newEventState is FromMe
timeline.updateTimelineItems { items -> timelineItems.getAndUpdate { items ->
val event = anEventTimelineItem(content = aMessageContent(), isOwn = true) val event = anEventTimelineItem(content = aMessageContent(), isOwn = true)
items + listOf(MatrixTimelineItem.Event("1", event)) items + listOf(MatrixTimelineItem.Event("1", event))
} }
@ -294,7 +319,7 @@ class TimelinePresenterTest {
assertThat(state.newEventState).isEqualTo(NewEventState.FromMe) assertThat(state.newEventState).isEqualTo(NewEventState.FromMe)
} }
// Mimics receiving a message without clearing the previous FromMe // Mimics receiving a message without clearing the previous FromMe
timeline.updateTimelineItems { items -> timelineItems.getAndUpdate { items ->
val event = anEventTimelineItem(content = aMessageContent()) val event = anEventTimelineItem(content = aMessageContent())
items + listOf(MatrixTimelineItem.Event("2", event)) items + listOf(MatrixTimelineItem.Event("2", event))
} }
@ -306,7 +331,7 @@ class TimelinePresenterTest {
assertThat(state.newEventState).isEqualTo(NewEventState.None) assertThat(state.newEventState).isEqualTo(NewEventState.None)
} }
// Mimics receiving a message and assert newEventState is FromOther // Mimics receiving a message and assert newEventState is FromOther
timeline.updateTimelineItems { items -> timelineItems.getAndUpdate { items ->
val event = anEventTimelineItem(content = aMessageContent()) val event = anEventTimelineItem(content = aMessageContent())
items + listOf(MatrixTimelineItem.Event("3", event)) items + listOf(MatrixTimelineItem.Event("3", event))
} }
@ -320,7 +345,10 @@ class TimelinePresenterTest {
@Test @Test
fun `present - reaction ordering`() = runTest { fun `present - reaction ordering`() = runTest {
val timeline = FakeMatrixTimeline() val timelineItems = MutableStateFlow(emptyList<MatrixTimelineItem>())
val timeline = FakeTimeline(
timelineItems = timelineItems,
)
val presenter = createTimelinePresenter(timeline) val presenter = createTimelinePresenter(timeline)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
@ -348,10 +376,9 @@ class TimelinePresenterTest {
senders = persistentListOf(charlie) senders = persistentListOf(charlie)
), ),
) )
timeline.updateTimelineItems { timelineItems.emit(
listOf(MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem(reactions = oneReaction))) listOf(MatrixTimelineItem.Event(FAKE_UNIQUE_ID, anEventTimelineItem(reactions = oneReaction)))
} )
skipItems(1)
val item = awaitItem().timelineItems.first() val item = awaitItem().timelineItems.first()
assertThat(item).isInstanceOf(TimelineItem.Event::class.java) assertThat(item).isInstanceOf(TimelineItem.Event::class.java)
val event = item as TimelineItem.Event val event = item as TimelineItem.Event
@ -423,8 +450,10 @@ class TimelinePresenterTest {
fun `present - side effect on redacted items is invoked`() = runTest { fun `present - side effect on redacted items is invoked`() = runTest {
val redactedVoiceMessageManager = FakeRedactedVoiceMessageManager() val redactedVoiceMessageManager = FakeRedactedVoiceMessageManager()
val presenter = createTimelinePresenter( val presenter = createTimelinePresenter(
timeline = FakeMatrixTimeline( timeline = FakeTimeline(
initialTimelineItems = aRedactedMatrixTimeline(AN_EVENT_ID), timelineItems = flowOf(
aRedactedMatrixTimeline(AN_EVENT_ID),
)
), ),
redactedVoiceMessageManager = redactedVoiceMessageManager, redactedVoiceMessageManager = redactedVoiceMessageManager,
) )
@ -432,32 +461,32 @@ class TimelinePresenterTest {
presenter.present() presenter.present()
}.test { }.test {
assertThat(redactedVoiceMessageManager.invocations.size).isEqualTo(0) assertThat(redactedVoiceMessageManager.invocations.size).isEqualTo(0)
awaitFirstItem().let { skipItems(2)
assertThat(it.timelineItems).isNotEmpty()
}
assertThat(redactedVoiceMessageManager.invocations.size).isEqualTo(1) assertThat(redactedVoiceMessageManager.invocations.size).isEqualTo(1)
} }
} }
@Test @Test
fun `present - when room member info is loaded, read receipts info should be updated`() = runTest { fun `present - when room member info is loaded, read receipts info should be updated`() = runTest {
val timeline = FakeMatrixTimeline( val timeline = FakeTimeline(
listOf( timelineItems = flowOf(
MatrixTimelineItem.Event( listOf(
FAKE_UNIQUE_ID, MatrixTimelineItem.Event(
anEventTimelineItem( FAKE_UNIQUE_ID,
sender = A_USER_ID, anEventTimelineItem(
receipts = persistentListOf( sender = A_USER_ID,
Receipt( receipts = persistentListOf(
userId = A_USER_ID, Receipt(
timestamp = 0L, userId = A_USER_ID,
timestamp = 0L,
)
) )
) )
) )
) )
) )
) )
val room = FakeMatrixRoom(matrixTimeline = timeline).apply { val room = FakeMatrixRoom(liveTimeline = timeline).apply {
givenRoomMembersState(MatrixRoomMembersState.Unknown) givenRoomMembersState(MatrixRoomMembersState.Unknown)
} }
@ -484,16 +513,12 @@ class TimelinePresenterTest {
} }
private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T { private suspend fun <T> ReceiveTurbine<T>.awaitFirstItem(): T {
// Skip 1 item if Mentions feature is enabled
if (FeatureFlags.Mentions.defaultValue) {
skipItems(1)
}
return awaitItem() return awaitItem()
} }
private fun TestScope.createTimelinePresenter( private fun TestScope.createTimelinePresenter(
timeline: MatrixTimeline = FakeMatrixTimeline(), timeline: Timeline = FakeTimeline(),
room: FakeMatrixRoom = FakeMatrixRoom(matrixTimeline = timeline), room: FakeMatrixRoom = FakeMatrixRoom(liveTimeline = timeline),
timelineItemsFactory: TimelineItemsFactory = aTimelineItemsFactory(), timelineItemsFactory: TimelineItemsFactory = aTimelineItemsFactory(),
redactedVoiceMessageManager: RedactedVoiceMessageManager = FakeRedactedVoiceMessageManager(), redactedVoiceMessageManager: RedactedVoiceMessageManager = FakeRedactedVoiceMessageManager(),
messagesNavigator: FakeMessagesNavigator = FakeMessagesNavigator(), messagesNavigator: FakeMessagesNavigator = FakeMessagesNavigator(),
@ -511,6 +536,8 @@ class TimelinePresenterTest {
endPollAction = endPollAction, endPollAction = endPollAction,
sendPollResponseAction = sendPollResponseAction, sendPollResponseAction = sendPollResponseAction,
sessionPreferencesStore = sessionPreferencesStore, sessionPreferencesStore = sessionPreferencesStore,
timelineItemIndexer = TimelineItemIndexer(),
timelineController = TimelineController(room),
) )
} }
} }

18
features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt

@ -19,10 +19,14 @@ package io.element.android.features.messages.impl.timeline
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.messages.impl.timeline.model.TimelineItem
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemLoadingIndicatorModel
import io.element.android.features.messages.impl.typing.aTypingNotificationState import io.element.android.features.messages.impl.typing.aTypingNotificationState
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.tests.testutils.EnsureNeverCalledWithParam import io.element.android.tests.testutils.EnsureNeverCalledWithParam
import io.element.android.tests.testutils.EnsureNeverCalledWithTwoParams import io.element.android.tests.testutils.EnsureNeverCalledWithTwoParams
import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.EventsRecorder
import kotlinx.collections.immutable.persistentListOf
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@ -37,10 +41,13 @@ class TimelineViewTest {
rule.setContent { rule.setContent {
TimelineView( TimelineView(
aTimelineState( aTimelineState(
timelineItems = persistentListOf<TimelineItem>(
TimelineItem.Virtual(
id = "backward_pagination",
model = TimelineItemLoadingIndicatorModel(Timeline.PaginationDirection.BACKWARDS, 0)
),
),
eventSink = eventsRecorder, eventSink = eventsRecorder,
paginationState = aPaginationState(
hasMoreToLoadBackwards = true,
)
), ),
typingNotificationState = aTypingNotificationState(), typingNotificationState = aTypingNotificationState(),
onUserDataClicked = EnsureNeverCalledWithParam(), onUserDataClicked = EnsureNeverCalledWithParam(),
@ -55,7 +62,7 @@ class TimelineViewTest {
onReadReceiptClick = EnsureNeverCalledWithParam(), onReadReceiptClick = EnsureNeverCalledWithParam(),
) )
} }
eventsRecorder.assertSingle(TimelineEvents.LoadMore) eventsRecorder.assertSingle(TimelineEvents.LoadMore(Timeline.PaginationDirection.BACKWARDS))
} }
@Test @Test
@ -65,9 +72,6 @@ class TimelineViewTest {
TimelineView( TimelineView(
aTimelineState( aTimelineState(
eventSink = eventsRecorder, eventSink = eventsRecorder,
paginationState = aPaginationState(
hasMoreToLoadBackwards = false,
)
), ),
typingNotificationState = aTypingNotificationState(), typingNotificationState = aTypingNotificationState(),
onUserDataClicked = EnsureNeverCalledWithParam(), onUserDataClicked = EnsureNeverCalledWithParam(),

6
features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/PollFixtures.kt

@ -20,15 +20,15 @@ import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.poll.PollAnswer import io.element.android.libraries.matrix.api.poll.PollAnswer
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline import io.element.android.libraries.matrix.test.timeline.FakeTimeline
import io.element.android.libraries.matrix.test.timeline.aPollContent import io.element.android.libraries.matrix.test.timeline.aPollContent
import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
fun aPollTimeline( fun aPollTimeline(
polls: Map<EventId, PollContent> = emptyMap(), polls: Map<EventId, PollContent> = emptyMap(),
): FakeMatrixTimeline { ): FakeTimeline {
return FakeMatrixTimeline( return FakeTimeline(
initialTimelineItems = polls.map { entry -> initialTimelineItems = polls.map { entry ->
MatrixTimelineItem.Event( MatrixTimelineItem.Event(
entry.key.value, entry.key.value,

4
features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt

@ -49,7 +49,7 @@ class CreatePollPresenterTest {
private var navUpInvocationsCount = 0 private var navUpInvocationsCount = 0
private val existingPoll = anOngoingPollContent() private val existingPoll = anOngoingPollContent()
private val fakeMatrixRoom = FakeMatrixRoom( private val fakeMatrixRoom = FakeMatrixRoom(
matrixTimeline = aPollTimeline( liveTimeline = aPollTimeline(
mapOf(pollEventId to existingPoll) mapOf(pollEventId to existingPoll)
) )
) )
@ -80,7 +80,7 @@ class CreatePollPresenterTest {
@Test @Test
fun `in edit mode, if poll doesn't exist, error is tracked and screen is closed`() = runTest { fun `in edit mode, if poll doesn't exist, error is tracked and screen is closed`() = runTest {
val room = FakeMatrixRoom( val room = FakeMatrixRoom(
matrixTimeline = aPollTimeline() liveTimeline = aPollTimeline()
) )
val presenter = createCreatePollPresenter(mode = CreatePollMode.EditPoll(AN_EVENT_ID), room = room) val presenter = createCreatePollPresenter(mode = CreatePollMode.EditPoll(AN_EVENT_ID), room = room)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {

2
features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenterTest.kt

@ -57,7 +57,7 @@ class PollHistoryPresenterTest {
) )
) )
private val room = FakeMatrixRoom( private val room = FakeMatrixRoom(
matrixTimeline = timeline liveTimeline = timeline
) )
@Test @Test

9
libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt

@ -44,6 +44,7 @@ import io.element.android.libraries.matrix.api.room.location.AssetType
import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels
import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange
import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.api.timeline.ReceiptType
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings
@ -53,7 +54,7 @@ import io.element.android.libraries.matrix.test.A_ROOM_NAME
import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline import io.element.android.libraries.matrix.test.timeline.FakeTimeline
import io.element.android.libraries.matrix.test.widget.FakeWidgetDriver import io.element.android.libraries.matrix.test.widget.FakeWidgetDriver
import io.element.android.tests.testutils.simulateLongTask import io.element.android.tests.testutils.simulateLongTask
import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.ImmutableMap
@ -83,7 +84,7 @@ class FakeMatrixRoom(
override val joinedMemberCount: Long = 123L, override val joinedMemberCount: Long = 123L,
override val activeMemberCount: Long = 234L, override val activeMemberCount: Long = 234L,
val notificationSettingsService: NotificationSettingsService = FakeNotificationSettingsService(), val notificationSettingsService: NotificationSettingsService = FakeNotificationSettingsService(),
private val matrixTimeline: MatrixTimeline = FakeMatrixTimeline(), override val liveTimeline: Timeline = FakeTimeline(),
private var roomPermalinkResult: () -> Result<String> = { Result.success("room link") }, private var roomPermalinkResult: () -> Result<String> = { Result.success("room link") },
private var eventPermalinkResult: (EventId) -> Result<String> = { Result.success("event link") }, private var eventPermalinkResult: (EventId) -> Result<String> = { Result.success("event link") },
canRedactOwn: Boolean = false, canRedactOwn: Boolean = false,
@ -214,7 +215,9 @@ class FakeMatrixRoom(
override val syncUpdateFlow: StateFlow<Long> = MutableStateFlow(0L) override val syncUpdateFlow: StateFlow<Long> = MutableStateFlow(0L)
override val timeline: MatrixTimeline = matrixTimeline override suspend fun timelineFocusedOnEvent(eventId: EventId): Timeline {
return FakeTimeline()
}
override suspend fun subscribeToSync() = Unit override suspend fun subscribeToSync() = Unit

96
libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeMatrixTimeline.kt

@ -1,96 +0,0 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.test.timeline
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.ReceiptType
import io.element.android.tests.testutils.simulateLongTask
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.getAndUpdate
class FakeMatrixTimeline(
initialTimelineItems: List<MatrixTimelineItem> = emptyList(),
initialPaginationState: MatrixTimeline.PaginationState = MatrixTimeline.PaginationState(
hasMoreToLoadBackwards = true,
isBackPaginating = false,
beginningOfRoomReached = false,
)
) : MatrixTimeline {
private val _paginationState: MutableStateFlow<MatrixTimeline.PaginationState> = MutableStateFlow(initialPaginationState)
private val _timelineItems: MutableStateFlow<List<MatrixTimelineItem>> = MutableStateFlow(initialTimelineItems)
var sentReadReceipts = mutableListOf<Pair<EventId, ReceiptType>>()
private set
var sendReadReceiptLatch: CompletableDeferred<Unit>? = null
fun updatePaginationState(update: (MatrixTimeline.PaginationState.() -> MatrixTimeline.PaginationState)) {
_paginationState.getAndUpdate(update)
}
fun updateTimelineItems(update: (items: List<MatrixTimelineItem>) -> List<MatrixTimelineItem>) {
_timelineItems.getAndUpdate(update)
}
override val paginationState: StateFlow<MatrixTimeline.PaginationState> = _paginationState
override val timelineItems: Flow<List<MatrixTimelineItem>> = _timelineItems
override suspend fun paginateBackwards(requestSize: Int) = paginateBackwards()
override suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int) = paginateBackwards()
override val membershipChangeEventReceived = MutableSharedFlow<Unit>()
private suspend fun paginateBackwards(): Result<Unit> {
updatePaginationState {
copy(isBackPaginating = true)
}
delay(100)
updatePaginationState {
copy(isBackPaginating = false)
}
updateTimelineItems { timelineItems ->
timelineItems
}
return Result.success(Unit)
}
fun givenMembershipChangeEventReceived() {
membershipChangeEventReceived.tryEmit(Unit)
}
override suspend fun fetchDetailsForEvent(eventId: EventId): Result<Unit> = simulateLongTask {
Result.success(Unit)
}
override suspend fun sendReadReceipt(
eventId: EventId,
receiptType: ReceiptType,
): Result<Unit> = simulateLongTask {
sentReadReceipts.add(eventId to receiptType)
sendReadReceiptLatch?.complete(Unit)
Result.success(Unit)
}
override fun close() = Unit
}

205
libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt

@ -0,0 +1,205 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.test.timeline
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.ProgressCallback
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.TransactionId
import io.element.android.libraries.matrix.api.media.AudioInfo
import io.element.android.libraries.matrix.api.media.FileInfo
import io.element.android.libraries.matrix.api.media.ImageInfo
import io.element.android.libraries.matrix.api.media.MediaUploadHandler
import io.element.android.libraries.matrix.api.media.VideoInfo
import io.element.android.libraries.matrix.api.poll.PollKind
import io.element.android.libraries.matrix.api.room.Mention
import io.element.android.libraries.matrix.api.room.location.AssetType
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.ReceiptType
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
import io.element.android.tests.testutils.simulateLongTask
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import java.io.File
class FakeTimeline(
override val timelineItems: Flow<List<MatrixTimelineItem>> = MutableStateFlow(emptyList()),
private val backwardPaginationStatus: MutableStateFlow<Timeline.PaginationStatus> = MutableStateFlow(
Timeline.PaginationStatus(
isPaginating = false,
hasMoreToLoad = true
)
),
private val forwardPaginationStatus: MutableStateFlow<Timeline.PaginationStatus> = MutableStateFlow(
Timeline.PaginationStatus(
isPaginating = false,
hasMoreToLoad = false
)
),
override val membershipChangeEventReceived: Flow<Unit> = MutableSharedFlow(),
) : Timeline {
var sendMessageLambda: (body: String, htmlBody: String?, mentions: List<Mention>) -> Result<Unit> = { _, _, _ -> Result.success(Unit) }
override suspend fun sendMessage(body: String, htmlBody: String?, mentions: List<Mention>): Result<Unit> = sendMessageLambda(body, htmlBody, mentions)
var editMessageLambda: (originalEventId: EventId?, transactionId: TransactionId?, body: String, htmlBody: String?, mentions: List<Mention>) -> Result<Unit> =
{ _, _, _, _, _ -> Result.success(Unit) }
override suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, body: String, htmlBody: String?, mentions: List<Mention>): Result<Unit> = editMessageLambda(
originalEventId,
transactionId,
body,
htmlBody,
mentions
)
var enterSpecialModeLambda: (eventId: EventId?) -> Result<Unit> = { Result.success(Unit) }
override suspend fun enterSpecialMode(eventId: EventId?): Result<Unit> = enterSpecialModeLambda(eventId)
var replyMessageLambda: (eventId: EventId, body: String, htmlBody: String?, mentions: List<Mention>) -> Result<Unit> =
{ _, _, _, _ -> Result.success(Unit) }
override suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?, mentions: List<Mention>): Result<Unit> = replyMessageLambda(
eventId,
body,
htmlBody,
mentions
)
var sendImageLambda: (file: File, thumbnailFile: File?, imageInfo: ImageInfo, body: String?, formattedBody: String?, progressCallback: ProgressCallback?) -> Result<MediaUploadHandler> =
{ _, _, _, _, _, _ -> Result.success(FakeMediaUploadHandler()) }
override suspend fun sendImage(file: File, thumbnailFile: File?, imageInfo: ImageInfo, body: String?, formattedBody: String?, progressCallback: ProgressCallback?): Result<MediaUploadHandler> = sendImageLambda(
file,
thumbnailFile,
imageInfo,
body,
formattedBody,
progressCallback
)
var sendVideoLambda: (file: File, thumbnailFile: File?, videoInfo: VideoInfo, body: String?, formattedBody: String?, progressCallback: ProgressCallback?) -> Result<MediaUploadHandler> =
{ _, _, _, _, _, _ -> Result.success(FakeMediaUploadHandler()) }
override suspend fun sendVideo(file: File, thumbnailFile: File?, videoInfo: VideoInfo, body: String?, formattedBody: String?, progressCallback: ProgressCallback?): Result<MediaUploadHandler> = sendVideoLambda(
file,
thumbnailFile,
videoInfo,
body,
formattedBody,
progressCallback
)
var sendAudioLambda: (file: File, audioInfo: AudioInfo, progressCallback: ProgressCallback?) -> Result<MediaUploadHandler> =
{ _, _, _ -> Result.success(FakeMediaUploadHandler()) }
override suspend fun sendAudio(file: File, audioInfo: AudioInfo, progressCallback: ProgressCallback?): Result<MediaUploadHandler> = sendAudioLambda(
file,
audioInfo,
progressCallback
)
var sendFileLambda: (file: File, fileInfo: FileInfo, progressCallback: ProgressCallback?) -> Result<MediaUploadHandler> =
{ _, _, _ -> Result.success(FakeMediaUploadHandler()) }
override suspend fun sendFile(file: File, fileInfo: FileInfo, progressCallback: ProgressCallback?): Result<MediaUploadHandler> = sendFileLambda(
file,
fileInfo,
progressCallback
)
var toggleReactionLambda: (emoji: String, eventId: EventId) -> Result<Unit> = { _, _ -> Result.success(Unit) }
override suspend fun toggleReaction(emoji: String, eventId: EventId): Result<Unit> = toggleReactionLambda(emoji, eventId)
var forwardEventLambda: (eventId: EventId, roomIds: List<RoomId>) -> Result<Unit> = { _, _ -> Result.success(Unit) }
override suspend fun forwardEvent(eventId: EventId, roomIds: List<RoomId>): Result<Unit> = forwardEventLambda(eventId, roomIds)
var retrySendMessageLambda: (transactionId: TransactionId) -> Result<Unit> = { Result.success(Unit) }
override suspend fun retrySendMessage(transactionId: TransactionId): Result<Unit> = retrySendMessageLambda(transactionId)
var cancelSendLambda: (transactionId: TransactionId) -> Result<Unit> = { Result.success(Unit) }
override suspend fun cancelSend(transactionId: TransactionId): Result<Unit> = cancelSendLambda(transactionId)
var sendLocationLambda: (body: String, geoUri: String, description: String?, zoomLevel: Int?, assetType: AssetType?) -> Result<Unit> =
{ _, _, _, _, _ -> Result.success(Unit) }
override suspend fun sendLocation(body: String, geoUri: String, description: String?, zoomLevel: Int?, assetType: AssetType?): Result<Unit> = sendLocationLambda(
body,
geoUri,
description,
zoomLevel,
assetType
)
var createPollLambda: (question: String, answers: List<String>, maxSelections: Int, pollKind: PollKind) -> Result<Unit> =
{ _, _, _, _ -> Result.success(Unit) }
override suspend fun createPoll(question: String, answers: List<String>, maxSelections: Int, pollKind: PollKind): Result<Unit> = createPollLambda(
question,
answers,
maxSelections,
pollKind
)
var editPollLambda: (pollStartId: EventId, question: String, answers: List<String>, maxSelections: Int, pollKind: PollKind) -> Result<Unit> =
{ _, _, _, _, _ -> Result.success(Unit) }
override suspend fun editPoll(pollStartId: EventId, question: String, answers: List<String>, maxSelections: Int, pollKind: PollKind): Result<Unit> = editPollLambda(
pollStartId,
question,
answers,
maxSelections,
pollKind
)
var sendPollResponseLambda: (pollStartId: EventId, answers: List<String>) -> Result<Unit> = { _, _ -> Result.success(Unit) }
override suspend fun sendPollResponse(pollStartId: EventId, answers: List<String>): Result<Unit> = sendPollResponseLambda(pollStartId, answers)
var endPollLambda: (pollStartId: EventId, text: String) -> Result<Unit> = { _, _ -> Result.success(Unit) }
override suspend fun endPoll(pollStartId: EventId, text: String): Result<Unit> = endPollLambda(pollStartId, text)
var sendVoiceMessageLambda: (file: File, audioInfo: AudioInfo, waveform: List<Float>, progressCallback: ProgressCallback?) -> Result<MediaUploadHandler> =
{ _, _, _, _ -> Result.success(FakeMediaUploadHandler()) }
override suspend fun sendVoiceMessage(file: File, audioInfo: AudioInfo, waveform: List<Float>, progressCallback: ProgressCallback?): Result<MediaUploadHandler> = sendVoiceMessageLambda(
file,
audioInfo,
waveform,
progressCallback
)
var sendReadReceiptLambda: (eventId: EventId, receiptType: ReceiptType) -> Result<Unit> = { _, _ -> Result.success(Unit) }
override suspend fun sendReadReceipt(
eventId: EventId,
receiptType: ReceiptType,
): Result<Unit> = sendReadReceiptLambda(eventId, receiptType)
var paginateLambda: (direction: Timeline.PaginationDirection) -> Result<Boolean> = { Result.success(false) }
override suspend fun paginate(direction: Timeline.PaginationDirection): Result<Boolean> = paginateLambda(direction)
override fun paginationStatus(direction: Timeline.PaginationDirection): StateFlow<Timeline.PaginationStatus> {
return when (direction) {
Timeline.PaginationDirection.BACKWARDS -> backwardPaginationStatus
Timeline.PaginationDirection.FORWARDS -> forwardPaginationStatus
}
}
override fun close() = Unit
}
Loading…
Cancel
Save