diff --git a/features/messages/build.gradle.kts b/features/messages/build.gradle.kts index 975c7b5c0e..b6f6fe4f33 100644 --- a/features/messages/build.gradle.kts +++ b/features/messages/build.gradle.kts @@ -44,7 +44,14 @@ dependencies { implementation(libs.accompanist.flowlayout) implementation(libs.androidx.recyclerview) implementation(libs.jsoup) + testImplementation(libs.test.junit) + testImplementation(libs.coroutines.test) + testImplementation(libs.molecule.runtime) + testImplementation(libs.test.truth) + testImplementation(libs.test.turbine) + testImplementation(projects.libraries.matrixtest) + androidTestImplementation(libs.test.junitext) ksp(libs.showkase.processor) } diff --git a/features/messages/src/test/kotlin/io/element/android/features/messages/ExampleUnitTest.kt b/features/messages/src/test/kotlin/io/element/android/features/messages/MessagePresenterTest.kt similarity index 53% rename from features/messages/src/test/kotlin/io/element/android/features/messages/ExampleUnitTest.kt rename to features/messages/src/test/kotlin/io/element/android/features/messages/MessagePresenterTest.kt index 83296930a7..c50932f6b8 100644 --- a/features/messages/src/test/kotlin/io/element/android/features/messages/ExampleUnitTest.kt +++ b/features/messages/src/test/kotlin/io/element/android/features/messages/MessagePresenterTest.kt @@ -14,19 +14,28 @@ * limitations under the License. */ +@file:OptIn(ExperimentalCoroutinesApi::class) + package io.element.android.features.messages -import org.junit.Assert.assertEquals +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest import org.junit.Test -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { +class MessagePresenterTest { @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) + fun `present - initial state`() = runTest { + /* + TO BE COMPLETED + val presenter = MessagesPresenter( + FakeMatrixClient(SessionId("sessionId")), + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.logoutAction).isEqualTo(Async.Uninitialized) + } + */ } } diff --git a/features/messages/src/test/kotlin/io/element/android/features/messages/timeline/Test.kt b/features/messages/src/test/kotlin/io/element/android/features/messages/timeline/Test.kt new file mode 100644 index 0000000000..dd6872124b --- /dev/null +++ b/features/messages/src/test/kotlin/io/element/android/features/messages/timeline/Test.kt @@ -0,0 +1,31 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.features.messages.timeline + +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher + +// TODO Move to common module to reuse +fun testCoroutineDispatchers() = CoroutineDispatchers( + io = UnconfinedTestDispatcher(), + computation = UnconfinedTestDispatcher(), + main = UnconfinedTestDispatcher(), + diffUpdateDispatcher = UnconfinedTestDispatcher(), +) diff --git a/features/messages/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt b/features/messages/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt new file mode 100644 index 0000000000..a1106f738e --- /dev/null +++ b/features/messages/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt @@ -0,0 +1,93 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.features.messages.timeline + +import app.cash.molecule.RecompositionClock +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrixtest.FakeMatrixClient +import io.element.android.libraries.matrixtest.core.A_ROOM_ID +import io.element.android.libraries.matrixtest.room.FakeMatrixRoom +import io.element.android.libraries.matrixtest.timeline.AN_EVENT_ID +import io.element.android.libraries.matrixtest.timeline.FakeMatrixTimeline +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class TimelinePresenterTest { + @Test + fun `present - initial state`() = runTest { + val presenter = TimelinePresenter( + testCoroutineDispatchers(), + FakeMatrixClient(), + FakeMatrixRoom(A_ROOM_ID) + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.timelineItems.size).isEqualTo(0) + } + } + + @Test + fun `present - load more`() = runTest { + val matrixTimeline = FakeMatrixTimeline() + val matrixRoom = FakeMatrixRoom(A_ROOM_ID, matrixTimeline = matrixTimeline) + val presenter = TimelinePresenter( + testCoroutineDispatchers(), + FakeMatrixClient(), + matrixRoom + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.hasMoreToLoad).isTrue() + matrixTimeline.givenHasMoreToLoad(false) + initialState.eventSink.invoke(TimelineEvents.LoadMore) + val loadedState = awaitItem() + assertThat(loadedState.hasMoreToLoad).isFalse() + } + } + + @Test + fun `present - set highlighted event`() = runTest { + val matrixTimeline = FakeMatrixTimeline() + val matrixRoom = FakeMatrixRoom(A_ROOM_ID, matrixTimeline = matrixTimeline) + val presenter = TimelinePresenter( + testCoroutineDispatchers(), + FakeMatrixClient(), + matrixRoom + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.highlightedEventId).isNull() + initialState.eventSink.invoke(TimelineEvents.SetHighlightedEvent(AN_EVENT_ID)) + val withHighlightedState = awaitItem() + assertThat(withHighlightedState.highlightedEventId).isEqualTo(AN_EVENT_ID) + initialState.eventSink.invoke(TimelineEvents.SetHighlightedEvent(null)) + val withoutHighlightedState = awaitItem() + assertThat(withoutHighlightedState.highlightedEventId).isNull() + } + } +} diff --git a/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/FakeMatrixClient.kt b/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/FakeMatrixClient.kt index 8cf2056c38..c7aab0952a 100644 --- a/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/FakeMatrixClient.kt +++ b/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/FakeMatrixClient.kt @@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.core.UserId import io.element.android.libraries.matrix.media.MediaResolver import io.element.android.libraries.matrix.room.MatrixRoom import io.element.android.libraries.matrix.room.RoomSummaryDataSource +import io.element.android.libraries.matrixtest.auth.A_SESSION_ID import io.element.android.libraries.matrixtest.media.FakeMediaResolver import io.element.android.libraries.matrixtest.room.FakeMatrixRoom import io.element.android.libraries.matrixtest.room.FakeRoomSummaryDataSource @@ -30,7 +31,7 @@ import kotlinx.coroutines.delay import org.matrix.rustcomponents.sdk.MediaSource class FakeMatrixClient( - override val sessionId: SessionId, + override val sessionId: SessionId = SessionId(A_SESSION_ID), val roomSummaryDataSource: RoomSummaryDataSource = FakeRoomSummaryDataSource() ) : MatrixClient { diff --git a/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/room/FakeMatrixRoom.kt b/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/room/FakeMatrixRoom.kt index 76da14418d..7e46219126 100644 --- a/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/room/FakeMatrixRoom.kt +++ b/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/room/FakeMatrixRoom.kt @@ -30,7 +30,8 @@ class FakeMatrixRoom( override val bestName: String = "", override val displayName: String = "", override val topic: String? = null, - override val avatarUrl: String? = null + override val avatarUrl: String? = null, + private val matrixTimeline: MatrixTimeline = FakeMatrixTimeline(), ) : MatrixRoom { override fun syncUpdateFlow(): Flow { @@ -38,7 +39,7 @@ class FakeMatrixRoom( } override fun timeline(): MatrixTimeline { - return FakeMatrixTimeline() + return matrixTimeline } override suspend fun userDisplayName(userId: String): Result { diff --git a/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/timeline/FakeMatrixTimeline.kt b/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/timeline/FakeMatrixTimeline.kt index 60fa211b1d..665a2eccfb 100644 --- a/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/timeline/FakeMatrixTimeline.kt +++ b/libraries/matrixtest/src/main/kotlin/io/element/android/libraries/matrixtest/timeline/FakeMatrixTimeline.kt @@ -19,24 +19,35 @@ package io.element.android.libraries.matrixtest.timeline import io.element.android.libraries.matrix.core.EventId import io.element.android.libraries.matrix.timeline.MatrixTimeline import io.element.android.libraries.matrix.timeline.MatrixTimelineItem +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow import org.matrix.rustcomponents.sdk.TimelineListener +const val AN_EVENT_ID_VALUE = "!anEventId" +val AN_EVENT_ID = EventId(AN_EVENT_ID_VALUE) + class FakeMatrixTimeline : MatrixTimeline { override var callback: MatrixTimeline.Callback? get() = null set(value) {} + private var hasMoreToLoadValue: Boolean = true + + fun givenHasMoreToLoad(hasMoreToLoad: Boolean) { + this.hasMoreToLoadValue = hasMoreToLoad + } + override val hasMoreToLoad: Boolean - get() = true + get() = hasMoreToLoadValue override fun timelineItems(): Flow> { return emptyFlow() } override suspend fun paginateBackwards(count: Int): Result { + delay(100) return Result.success(Unit) }