From d6e02ea5030826f7200029f990b966505ab8e7a1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 26 Apr 2024 17:29:28 +0200 Subject: [PATCH] Test TimelineController --- .../impl/timeline/TimelineControllerTest.kt | 217 ++++++++++++++++++ .../matrix/test/room/FakeMatrixRoom.kt | 10 +- .../matrix/test/timeline/FakeTimeline.kt | 10 +- 3 files changed, 234 insertions(+), 3 deletions(-) create mode 100644 features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineControllerTest.kt diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineControllerTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineControllerTest.kt new file mode 100644 index 0000000000..5bd1145aa3 --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineControllerTest.kt @@ -0,0 +1,217 @@ +/* + * Copyright (c) 2024 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.features.messages.impl.timeline + +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.room.Mention +import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem +import io.element.android.libraries.matrix.api.timeline.Timeline +import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.A_UNIQUE_ID +import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.timeline.FakeTimeline +import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem +import io.element.android.tests.testutils.lambda.lambdaRecorder +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class TimelineControllerTest { + @Test + fun `test switching between live and detached timeline`() = runTest { + val liveTimeline = FakeTimeline(name = "live") + val detachedTimeline = FakeTimeline(name = "detached") + val matrixRoom = FakeMatrixRoom( + liveTimeline = liveTimeline + ) + matrixRoom.givenTimelineFocusedOnEventResult(Result.success(detachedTimeline)) + val sut = TimelineController(matrixRoom) + + sut.activeTimelineFlow().test { + awaitItem().also { state -> + assertThat(state).isEqualTo(liveTimeline) + } + assertThat(sut.isLive().first()).isTrue() + sut.focusOnEvent(AN_EVENT_ID) + awaitItem().also { state -> + assertThat(state).isEqualTo(detachedTimeline) + } + assertThat(sut.isLive().first()).isFalse() + assertThat(detachedTimeline.closeCounter).isEqualTo(0) + sut.focusOnLive() + assertThat(sut.isLive().first()).isTrue() + awaitItem().also { state -> + assertThat(state).isEqualTo(liveTimeline) + } + assertThat(detachedTimeline.closeCounter).isEqualTo(1) + } + } + + @Test + fun `test switching between detached 1 and detached 2 should close detached 1`() = runTest { + val liveTimeline = FakeTimeline(name = "live") + val detachedTimeline1 = FakeTimeline(name = "detached 1") + val detachedTimeline2 = FakeTimeline(name = "detached 2") + val matrixRoom = FakeMatrixRoom( + liveTimeline = liveTimeline + ) + val sut = TimelineController(matrixRoom) + + sut.activeTimelineFlow().test { + awaitItem().also { state -> + assertThat(state).isEqualTo(liveTimeline) + } + matrixRoom.givenTimelineFocusedOnEventResult(Result.success(detachedTimeline1)) + sut.focusOnEvent(AN_EVENT_ID) + awaitItem().also { state -> + assertThat(state).isEqualTo(detachedTimeline1) + } + assertThat(detachedTimeline1.closeCounter).isEqualTo(0) + assertThat(detachedTimeline2.closeCounter).isEqualTo(0) + // Focus on another event should close the previous detached timeline + matrixRoom.givenTimelineFocusedOnEventResult(Result.success(detachedTimeline2)) + sut.focusOnEvent(AN_EVENT_ID) + awaitItem().also { state -> + assertThat(state).isEqualTo(detachedTimeline2) + } + assertThat(detachedTimeline1.closeCounter).isEqualTo(1) + assertThat(detachedTimeline2.closeCounter).isEqualTo(0) + } + } + + @Test + fun `test switching to live when already in live should have no effect`() = runTest { + val liveTimeline = FakeTimeline(name = "live") + val matrixRoom = FakeMatrixRoom( + liveTimeline = liveTimeline + ) + val sut = TimelineController(matrixRoom) + sut.activeTimelineFlow().test { + awaitItem().also { state -> + assertThat(state).isEqualTo(liveTimeline) + } + assertThat(sut.isLive().first()).isTrue() + sut.focusOnLive() + assertThat(sut.isLive().first()).isTrue() + } + } + + @Test + fun `test closing the TimelineController should close the detached timeline`() = runTest { + val liveTimeline = FakeTimeline(name = "live") + val detachedTimeline = FakeTimeline(name = "detached") + val matrixRoom = FakeMatrixRoom( + liveTimeline = liveTimeline + ) + matrixRoom.givenTimelineFocusedOnEventResult(Result.success(detachedTimeline)) + val sut = TimelineController(matrixRoom) + + sut.activeTimelineFlow().test { + awaitItem().also { state -> + assertThat(state).isEqualTo(liveTimeline) + } + sut.focusOnEvent(AN_EVENT_ID) + awaitItem().also { state -> + assertThat(state).isEqualTo(detachedTimeline) + } + assertThat(detachedTimeline.closeCounter).isEqualTo(0) + sut.close() + assertThat(detachedTimeline.closeCounter).isEqualTo(1) + } + } + + @Test + fun `test getting timeline item`() = runTest { + val liveTimeline = FakeTimeline( + name = "live", + timelineItems = flowOf( + listOf( + MatrixTimelineItem.Event(A_UNIQUE_ID, anEventTimelineItem()) + ) + ) + ) + val matrixRoom = FakeMatrixRoom( + liveTimeline = liveTimeline + ) + val sut = TimelineController(matrixRoom) + assertThat(sut.timelineItems().first()).hasSize(1) + } + + @Test + fun `test invokeOnCurrentTimeline use the detached timeline and not the live timeline`() = runTest { + val lambdaForDetached = lambdaRecorder { _: String, _: String?, _: List -> + Result.success(Unit) + } + val lambdaForLive = lambdaRecorder(ensureNeverCalled = true) { _: String, _: String?, _: List -> + Result.success(Unit) + } + val liveTimeline = FakeTimeline(name = "live").apply { + sendMessageLambda = lambdaForLive + } + val detachedTimeline = FakeTimeline(name = "detached").apply { + sendMessageLambda = lambdaForDetached + } + val matrixRoom = FakeMatrixRoom( + liveTimeline = liveTimeline + ) + matrixRoom.givenTimelineFocusedOnEventResult(Result.success(detachedTimeline)) + val sut = TimelineController(matrixRoom) + sut.focusOnEvent(AN_EVENT_ID) + sut.activeTimelineFlow().test { + awaitItem().also { state -> + assertThat(state).isEqualTo(detachedTimeline) + } + sut.invokeOnCurrentTimeline { + sendMessage("body", "htmlBody", emptyList()) + } + lambdaForDetached.assertions().isCalledOnce() + } + } + + @Test + fun `test last forward pagination on a detached timeline should switch to live timeline`() = runTest { + val liveTimeline = FakeTimeline(name = "live") + val detachedTimeline = FakeTimeline(name = "detached") + val matrixRoom = FakeMatrixRoom( + liveTimeline = liveTimeline + ) + matrixRoom.givenTimelineFocusedOnEventResult(Result.success(detachedTimeline)) + val sut = TimelineController(matrixRoom) + + sut.activeTimelineFlow().test { + awaitItem().also { state -> + assertThat(state).isEqualTo(liveTimeline) + } + sut.focusOnEvent(AN_EVENT_ID) + awaitItem().also { state -> + assertThat(state).isEqualTo(detachedTimeline) + } + val paginateLambda = lambdaRecorder { _: Timeline.PaginationDirection -> + Result.success(true) + } + detachedTimeline.apply { + this.paginateLambda = paginateLambda + } + sut.paginate(Timeline.PaginationDirection.FORWARDS) + awaitItem().also { state -> + assertThat(state).isEqualTo(liveTimeline) + } + } + } +} diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index d3d89a0788..7a6920e4dc 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -215,8 +215,14 @@ class FakeMatrixRoom( override val syncUpdateFlow: StateFlow = MutableStateFlow(0L) - override suspend fun timelineFocusedOnEvent(eventId: EventId): Result { - return Result.success(FakeTimeline()) + private var timelineFocusedOnEventResult: Result = Result.success(FakeTimeline()) + + fun givenTimelineFocusedOnEventResult(result: Result) { + timelineFocusedOnEventResult = result + } + + override suspend fun timelineFocusedOnEvent(eventId: EventId): Result = simulateLongTask { + timelineFocusedOnEventResult } override suspend fun subscribeToSync() = Unit diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt index c99698fcb4..6ec8386651 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeTimeline.kt @@ -39,6 +39,7 @@ import kotlinx.coroutines.flow.StateFlow import java.io.File class FakeTimeline( + private val name: String = "FakeTimeline", override val timelineItems: Flow> = MutableStateFlow(emptyList()), private val backwardPaginationStatus: MutableStateFlow = MutableStateFlow( Timeline.PaginationStatus( @@ -360,5 +361,12 @@ class FakeTimeline( } } - override fun close() = Unit + var closeCounter = 0 + private set + + override fun close() { + closeCounter++ + } + + override fun toString() = "FakeTimeline: $name" }