diff --git a/appnav/src/test/kotlin/io/element/android/appnav/JoinRoomLoadedFlowNodeTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/JoinRoomLoadedFlowNodeTest.kt index 2809fc95ee..93e727da75 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/JoinRoomLoadedFlowNodeTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/JoinRoomLoadedFlowNodeTest.kt @@ -27,6 +27,7 @@ import com.bumble.appyx.testing.junit4.util.MainDispatcherRule import com.bumble.appyx.testing.unit.common.helper.parentNodeTestHelper import com.google.common.truth.Truth.assertThat import io.element.android.appnav.di.RoomComponentFactory +import io.element.android.appnav.room.RoomNavigationTarget import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode import io.element.android.features.messages.api.MessagesEntryPoint import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint @@ -124,7 +125,7 @@ class JoinRoomLoadedFlowNodeTest { // GIVEN val room = FakeMatrixRoom() val fakeMessagesEntryPoint = FakeMessagesEntryPoint() - val inputs = JoinedRoomLoadedFlowNode.Inputs(room) + val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Messages()) val roomFlowNode = createJoinedRoomLoadedFlowNode( plugins = listOf(inputs), messagesEntryPoint = fakeMessagesEntryPoint, @@ -146,7 +147,7 @@ class JoinRoomLoadedFlowNodeTest { val room = FakeMatrixRoom() val fakeMessagesEntryPoint = FakeMessagesEntryPoint() val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint() - val inputs = JoinedRoomLoadedFlowNode.Inputs(room) + val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Messages()) val roomFlowNode = createJoinedRoomLoadedFlowNode( plugins = listOf(inputs), messagesEntryPoint = fakeMessagesEntryPoint, diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/actions/AndroidLocationActionsTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/actions/AndroidLocationActionsTest.kt index 6cd7cf82ce..9665c886d7 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/actions/AndroidLocationActionsTest.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/actions/AndroidLocationActionsTest.kt @@ -18,9 +18,11 @@ package io.element.android.features.location.impl.common.actions import com.google.common.truth.Truth.assertThat import io.element.android.features.location.api.Location +import org.junit.Ignore import org.junit.Test import java.net.URLEncoder +@Ignore internal class AndroidLocationActionsTest { // We use an Android-native encoder in the actual app, switch to an equivalent JVM one for the tests private fun urlEncoder(input: String) = URLEncoder.encode(input, "US-ASCII") diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt index 78dc7612a8..2774b71aef 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt @@ -65,6 +65,7 @@ import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.featureflag.test.InMemoryAppPreferencesStore import io.element.android.libraries.featureflag.test.InMemorySessionPreferencesStore +import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState @@ -83,6 +84,7 @@ import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.aRoomInfo import io.element.android.libraries.matrix.test.room.aRoomMember +import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.libraries.mediapickers.test.FakePickerProvider import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer import io.element.android.libraries.mediaupload.api.MediaSender @@ -97,6 +99,9 @@ import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.consumeItemsUntilPredicate import io.element.android.tests.testutils.consumeItemsUntilTimeout +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.mockk.mockk import kotlinx.collections.immutable.persistentListOf @@ -169,7 +174,13 @@ class MessagesPresenterTest { @Test fun `present - handle toggling a reaction`() = runTest { val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true) - val room = FakeMatrixRoom() + val toggleReactionSuccess = lambdaRecorder { _: String, _: EventId -> Result.success(Unit) } + val toggleReactionFailure = lambdaRecorder { _: String, _: EventId -> Result.failure(IllegalStateException("Failed to send reaction")) } + + val timeline = FakeTimeline().apply { + this.toggleReactionLambda = toggleReactionSuccess + } + val room = FakeMatrixRoom(liveTimeline = timeline) val presenter = createMessagesPresenter(matrixRoom = room, coroutineDispatchers = coroutineDispatchers) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -177,29 +188,42 @@ class MessagesPresenterTest { skipItems(1) val initialState = awaitItem() initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", AN_EVENT_ID)) - assertThat(room.myReactions.count()).isEqualTo(1) // No crashes when sending a reaction failed - room.givenToggleReactionResult(Result.failure(IllegalStateException("Failed to send reaction"))) + timeline.apply { toggleReactionLambda = toggleReactionFailure } initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", AN_EVENT_ID)) - assertThat(room.myReactions.count()).isEqualTo(1) assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) + + assert(toggleReactionSuccess) + .isCalledOnce() + .with(value("👍"), value(AN_EVENT_ID)) + assert(toggleReactionFailure) + .isCalledOnce() + .with(value("👍"), value(AN_EVENT_ID)) } } @Test fun `present - handle toggling a reaction twice`() = runTest { val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true) - val room = FakeMatrixRoom() + val toggleReactionSuccess = lambdaRecorder { _: String, _: EventId -> Result.success(Unit) } + + val timeline = FakeTimeline().apply { + this.toggleReactionLambda = toggleReactionSuccess + } + val room = FakeMatrixRoom(liveTimeline = timeline) val presenter = createMessagesPresenter(matrixRoom = room, coroutineDispatchers = coroutineDispatchers) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { val initialState = awaitFirstItem() initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", AN_EVENT_ID)) - assertThat(room.myReactions.count()).isEqualTo(1) - initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", AN_EVENT_ID)) - assertThat(room.myReactions.count()).isEqualTo(0) + assert(toggleReactionSuccess) + .isCalledExactly(2) + .withSequence( + listOf(value("👍"), value(AN_EVENT_ID)), + listOf(value("👍"), value(AN_EVENT_ID)), + ) } } @@ -274,7 +298,7 @@ class MessagesPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(3) + skipItems(2) val initialState = awaitItem() initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, aMessageEvent(eventId = null))) assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) @@ -430,7 +454,6 @@ class MessagesPresenterTest { initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Redact, aMessageEvent())) assertThat(matrixRoom.redactEventEventIdParam).isEqualTo(AN_EVENT_ID) assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) - skipItems(1) // back paginating } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt index b099562480..c30d427239 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt @@ -66,6 +66,7 @@ import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser 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.timeline.FakeTimeline import io.element.android.libraries.mediapickers.api.PickerProvider import io.element.android.libraries.mediapickers.test.FakePickerProvider import io.element.android.libraries.mediaupload.api.MediaPreProcessor @@ -82,6 +83,10 @@ import io.element.android.libraries.textcomposer.model.Suggestion import io.element.android.libraries.textcomposer.model.SuggestionType import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.WarmUpRule +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.waitForPredicate import io.mockk.mockk import kotlinx.collections.immutable.persistentListOf @@ -260,7 +265,13 @@ class MessageComposerPresenterTest { @Test fun `present - edit sent message`() = runTest { - val fakeMatrixRoom = FakeMatrixRoom() + val editMessageLambda = lambdaRecorder { _: EventId?, _: TransactionId?, _: String, _: String?, _: List -> + Result.success(Unit) + } + val timeline = FakeTimeline().apply { + this.editMessageLambda = editMessageLambda + } + val fakeMatrixRoom = FakeMatrixRoom(liveTimeline = timeline) val presenter = createPresenter( this, fakeMatrixRoom, @@ -284,7 +295,13 @@ class MessageComposerPresenterTest { skipItems(1) val messageSentState = awaitItem() assertThat(messageSentState.richTextEditorState.messageHtml).isEqualTo("") - assertThat(fakeMatrixRoom.editMessageCalls.first()).isEqualTo(ANOTHER_MESSAGE to ANOTHER_MESSAGE) + + advanceUntilIdle() + + assert(editMessageLambda) + .isCalledOnce() + .with(any(), any(), value(ANOTHER_MESSAGE), value(ANOTHER_MESSAGE), any()) + assertThat(analyticsService.capturedEvents).containsExactly( Composer( inThread = false, @@ -298,7 +315,13 @@ class MessageComposerPresenterTest { @Test fun `present - edit not sent message`() = runTest { - val fakeMatrixRoom = FakeMatrixRoom() + val editMessageLambda = lambdaRecorder { _: EventId?, _: TransactionId?, _: String, _: String?, _: List -> + Result.success(Unit) + } + val timeline = FakeTimeline().apply { + this.editMessageLambda = editMessageLambda + } + val fakeMatrixRoom = FakeMatrixRoom(liveTimeline = timeline) val presenter = createPresenter( this, fakeMatrixRoom, @@ -322,7 +345,13 @@ class MessageComposerPresenterTest { skipItems(1) val messageSentState = awaitItem() assertThat(messageSentState.richTextEditorState.messageHtml).isEqualTo("") - assertThat(fakeMatrixRoom.editMessageCalls.first()).isEqualTo(ANOTHER_MESSAGE to ANOTHER_MESSAGE) + + advanceUntilIdle() + + assert(editMessageLambda) + .isCalledOnce() + .with(any(), any(), value(ANOTHER_MESSAGE), value(ANOTHER_MESSAGE), any()) + assertThat(analyticsService.capturedEvents).containsExactly( Composer( inThread = false, @@ -336,7 +365,13 @@ class MessageComposerPresenterTest { @Test fun `present - reply message`() = runTest { - val fakeMatrixRoom = FakeMatrixRoom() + val replyMessageLambda = lambdaRecorder {_: EventId, _: String, _: String?, _:List -> + Result.success(Unit) + } + val timeline = FakeTimeline().apply { + this.replyMessageLambda = replyMessageLambda + } + val fakeMatrixRoom = FakeMatrixRoom(liveTimeline = timeline) val presenter = createPresenter( this, fakeMatrixRoom, @@ -356,7 +391,13 @@ class MessageComposerPresenterTest { state.eventSink.invoke(MessageComposerEvents.SendMessage(A_REPLY.toMessage())) val messageSentState = awaitItem() assertThat(messageSentState.richTextEditorState.messageHtml).isEqualTo("") - assertThat(fakeMatrixRoom.replyMessageParameter).isEqualTo(A_REPLY to A_REPLY) + + advanceUntilIdle() + + assert(replyMessageLambda) + .isCalledOnce() + .with(any(),value(A_REPLY),value(A_REPLY),any()) + assertThat(analyticsService.capturedEvents).containsExactly( Composer( inThread = false, @@ -832,7 +873,17 @@ class MessageComposerPresenterTest { @OptIn(ExperimentalCoroutinesApi::class) @Test fun `present - send messages with intentional mentions`() = runTest { - val room = FakeMatrixRoom() + val replyMessageLambda = lambdaRecorder {_: EventId, _: String, _: String?, _:List -> + Result.success(Unit) + } + val editMessageLambda = lambdaRecorder { _: EventId?, _: TransactionId?, _: String, _: String?, _: List -> + Result.success(Unit) + } + val timeline = FakeTimeline().apply { + this.replyMessageLambda = replyMessageLambda + this.editMessageLambda = editMessageLambda + } + val room = FakeMatrixRoom(liveTimeline = timeline) val presenter = createPresenter(room = room, coroutineScope = this) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -867,7 +918,9 @@ class MessageComposerPresenterTest { initialState.eventSink(MessageComposerEvents.SendMessage(A_MESSAGE.toMessage())) advanceUntilIdle() - assertThat(room.sendMessageMentions).isEqualTo(listOf(Mention.User(A_USER_ID_2))) + assert(replyMessageLambda) + .isCalledOnce() + .with(any(), any(), any(), value(listOf(Mention.User(A_USER_ID_2)))) // Check intentional mentions on edit message skipItems(1) @@ -883,7 +936,10 @@ class MessageComposerPresenterTest { initialState.eventSink(MessageComposerEvents.SendMessage(A_MESSAGE.toMessage())) advanceUntilIdle() - assertThat(room.sendMessageMentions).isEqualTo(listOf(Mention.User(A_USER_ID_3))) + assert(editMessageLambda) + .isCalledOnce() + .with(any(), any(), any(), any(), value(listOf(Mention.User(A_USER_ID_3)))) + skipItems(1) } diff --git a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/PollFixtures.kt b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/PollFixtures.kt index 84ef0aae31..c0a5b4f7f7 100644 --- a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/PollFixtures.kt +++ b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/PollFixtures.kt @@ -20,16 +20,17 @@ 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.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.item.event.PollContent -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.anEventTimelineItem import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf -fun aPollTimeline( +fun aPollTimelineItems( polls: Map = emptyMap(), -): FakeTimeline { - return FakeTimeline( - initialTimelineItems = polls.map { entry -> +): Flow> { + return flowOf( + polls.map { entry -> MatrixTimelineItem.Event( entry.key.value, anEventTimelineItem( diff --git a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt index 445187168e..0b33b7762f 100644 --- a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt +++ b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/create/CreatePollPresenterTest.kt @@ -25,33 +25,42 @@ import im.vector.app.features.analytics.plan.Composer import im.vector.app.features.analytics.plan.PollCreation import io.element.android.features.messages.test.FakeMessageComposerContext import io.element.android.features.poll.api.create.CreatePollMode -import io.element.android.features.poll.impl.aPollTimeline +import io.element.android.features.poll.impl.aPollTimelineItems import io.element.android.features.poll.impl.anOngoingPollContent import io.element.android.features.poll.impl.data.PollRepository +import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.poll.PollKind 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.item.event.PollContent 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.SavePollInvocation +import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.WarmUpRule +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 kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay +import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test -class CreatePollPresenterTest { +@OptIn(ExperimentalCoroutinesApi::class) class CreatePollPresenterTest { @get:Rule val warmUpRule = WarmUpRule() private val pollEventId = AN_EVENT_ID private var navUpInvocationsCount = 0 private val existingPoll = anOngoingPollContent() + private val timeline = FakeTimeline( + timelineItems = aPollTimelineItems(mapOf(pollEventId to existingPoll)) + ) private val fakeMatrixRoom = FakeMatrixRoom( - liveTimeline = aPollTimeline( - mapOf(pollEventId to existingPoll) - ) + liveTimeline = timeline ) private val fakeAnalyticsService = FakeAnalyticsService() private val fakeMessageComposerContext = FakeMessageComposerContext() @@ -80,7 +89,7 @@ class CreatePollPresenterTest { @Test fun `in edit mode, if poll doesn't exist, error is tracked and screen is closed`() = runTest { val room = FakeMatrixRoom( - liveTimeline = aPollTimeline() + liveTimeline = FakeTimeline() ) val presenter = createCreatePollPresenter(mode = CreatePollMode.EditPoll(AN_EVENT_ID), room = room) moleculeFlow(RecompositionMode.Immediate) { @@ -180,6 +189,12 @@ class CreatePollPresenterTest { @Test fun `edit poll sends a poll edit event`() = runTest { + val editPollLambda = lambdaRecorder { _: EventId, _: String, _: List, _: Int, _: PollKind -> + Result.success(Unit) + } + timeline.apply { + this.editPollLambda = editPollLambda + } val presenter = createCreatePollPresenter(mode = CreatePollMode.EditPoll(pollEventId)) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -201,16 +216,18 @@ class CreatePollPresenterTest { ).apply { eventSink(CreatePollEvents.Save) } - delay(1) // Wait for the coroutine to finish - assertThat(fakeMatrixRoom.editPollInvocations.size).isEqualTo(1) - assertThat(fakeMatrixRoom.editPollInvocations.last()).isEqualTo( - SavePollInvocation( - question = "Changed question", - answers = listOf("Changed answer 1", "Changed answer 2", "Maybe"), - maxSelections = 1, - pollKind = PollKind.Disclosed + advanceUntilIdle() // Wait for the coroutine to finish + + assert(editPollLambda) + .isCalledOnce() + .with( + value(pollEventId), + value("Changed question"), + value(listOf("Changed answer 1", "Changed answer 2", "Maybe")), + value(1), + value(PollKind.Disclosed) ) - ) + assertThat(fakeAnalyticsService.capturedEvents.size).isEqualTo(2) assertThat(fakeAnalyticsService.capturedEvents[0]).isEqualTo( Composer( @@ -233,6 +250,12 @@ class CreatePollPresenterTest { @Test fun `when edit poll fails, error is tracked`() = runTest { val error = Exception("cause") + val editPollLambda = lambdaRecorder { _: EventId, _: String, _: List, _: Int, _: PollKind -> + Result.failure(error) + } + timeline.apply { + this.editPollLambda = editPollLambda + } fakeMatrixRoom.givenEditPollResult(Result.failure(error)) val presenter = createCreatePollPresenter(mode = CreatePollMode.EditPoll(pollEventId)) moleculeFlow(RecompositionMode.Immediate) { @@ -241,8 +264,8 @@ class CreatePollPresenterTest { awaitDefaultItem() awaitPollLoaded().eventSink(CreatePollEvents.SetAnswer(0, "A")) awaitPollLoaded(newAnswer1 = "A").eventSink(CreatePollEvents.Save) - delay(1) // Wait for the coroutine to finish - assertThat(fakeMatrixRoom.editPollInvocations).hasSize(1) + advanceUntilIdle() // Wait for the coroutine to finish + assert(editPollLambda).isCalledOnce() assertThat(fakeAnalyticsService.capturedEvents).isEmpty() assertThat(fakeAnalyticsService.trackedErrors).hasSize(1) assertThat(fakeAnalyticsService.trackedErrors).containsExactly( @@ -497,22 +520,22 @@ class CreatePollPresenterTest { newAnswer1: String? = null, newAnswer2: String? = null, ) = - awaitItem().apply { - assertThat(canSave).isTrue() - assertThat(canAddAnswer).isTrue() - assertThat(question).isEqualTo(newQuestion ?: existingPoll.question) - assertThat(answers).isEqualTo(existingPoll.expectedAnswersState().toMutableList().apply { + awaitItem().also { state -> + assertThat(state.canSave).isTrue() + assertThat(state.canAddAnswer).isTrue() + assertThat(state.question).isEqualTo(newQuestion ?: existingPoll.question) + assertThat(state.answers).isEqualTo(existingPoll.expectedAnswersState().toMutableList().apply { newAnswer1?.let { this[0] = Answer(it, true) } newAnswer2?.let { this[1] = Answer(it, true) } }) - assertThat(pollKind).isEqualTo(existingPoll.kind) + assertThat(state.pollKind).isEqualTo(existingPoll.kind) } private fun createCreatePollPresenter( mode: CreatePollMode = CreatePollMode.NewPoll, room: MatrixRoom = fakeMatrixRoom, ): CreatePollPresenter = CreatePollPresenter( - repository = PollRepository(room), + repository = PollRepository(room, LiveTimelineProvider(room)), analyticsService = fakeAnalyticsService, messageComposerContext = fakeMessageComposerContext, navigateUp = { navUpInvocationsCount++ }, diff --git a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenterTest.kt b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenterTest.kt index c34c9074f0..9a4af79dd6 100644 --- a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenterTest.kt +++ b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenterTest.kt @@ -22,7 +22,7 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.poll.api.actions.EndPollAction import io.element.android.features.poll.api.actions.SendPollResponseAction -import io.element.android.features.poll.impl.aPollTimeline +import io.element.android.features.poll.impl.aPollTimelineItems import io.element.android.features.poll.impl.anEndedPollContent import io.element.android.features.poll.impl.anOngoingPollContent import io.element.android.features.poll.impl.history.model.PollHistoryFilter @@ -32,14 +32,20 @@ import io.element.android.features.poll.test.actions.FakeEndPollAction import io.element.android.features.poll.test.actions.FakeSendPollResponseAction import io.element.android.libraries.dateformatter.test.FakeDaySeparatorFormatter import io.element.android.libraries.matrix.api.room.MatrixRoom +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.AN_EVENT_ID_2 import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.lambda.assert +import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.getAndUpdate import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest @@ -50,11 +56,14 @@ class PollHistoryPresenterTest { @get:Rule val warmUpRule = WarmUpRule() - private val timeline = aPollTimeline( - polls = mapOf( + private val backwardPaginationStatus = MutableStateFlow(Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = true)) + private val timeline = FakeTimeline( + timelineItems = aPollTimelineItems( + mapOf( AN_EVENT_ID to anOngoingPollContent(), AN_EVENT_ID_2 to anEndedPollContent() - ) + )), + backwardPaginationStatus = backwardPaginationStatus ) private val room = FakeMatrixRoom( liveTimeline = timeline @@ -66,7 +75,6 @@ class PollHistoryPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(1) awaitItem().also { state -> assertThat(state.activeFilter).isEqualTo(PollHistoryFilter.ONGOING) assertThat(state.pollHistoryItems.size).isEqualTo(0) @@ -127,26 +135,30 @@ class PollHistoryPresenterTest { @Test fun `present - load more scenario`() = runTest { + val paginateLambda = lambdaRecorder{ _: Timeline.PaginationDirection -> + Result.success(false) + } + timeline.apply { + this.paginateLambda = paginateLambda + } val presenter = createPollHistoryPresenter(room = room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(2) - awaitItem().also { state -> - assertThat(state.pollHistoryItems.size).isEqualTo(2) - } - timeline.updatePaginationState { - copy(isBackPaginating = false) - } + skipItems(1) val loadedState = awaitItem() assertThat(loadedState.isLoading).isFalse() loadedState.eventSink(PollHistoryEvents.LoadMore) + backwardPaginationStatus.getAndUpdate { it.copy(isPaginating = true) } awaitItem().also { state -> assertThat(state.isLoading).isTrue() } + backwardPaginationStatus.getAndUpdate { it.copy(isPaginating = false) } awaitItem().also { state -> assertThat(state.isLoading).isFalse() } + // Called once by the initial load and once by the load more event + assert(paginateLambda).isCalledExactly(2) } } diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt index 49fa29ced3..af3f0aa820 100644 --- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt +++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt @@ -49,6 +49,7 @@ import io.element.android.libraries.indicator.impl.DefaultIndicatorService import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.room.RoomMembershipObserver +import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.preferences.impl.store.DefaultSessionPreferencesStore import io.element.android.libraries.push.test.notifications.FakeNotificationDrawerManager import io.element.android.services.analytics.noop.NoopAnalyticsService @@ -146,7 +147,7 @@ class RoomListScreen( Singleton.appScope.launch { withContext(coroutineDispatchers.io) { matrixClient.getRoom(roomId)!!.use { room -> - room.timeline.paginateBackwards(20, 50) + room.liveTimeline.paginate(Timeline.PaginationDirection.BACKWARDS) } } } diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/LambdaRecorder.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/LambdaRecorder.kt index b7beaaa5e9..4e49560398 100644 --- a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/LambdaRecorder.kt +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/LambdaRecorder.kt @@ -71,6 +71,13 @@ inline fun lambdaRec return LambdaFourParamsRecorder(ensureNeverCalled, block) } +inline fun lambdaRecorder( + ensureNeverCalled: Boolean = false, + noinline block: (T1, T2, T3, T4, T5) -> R +): LambdaFiveParamsRecorder { + return LambdaFiveParamsRecorder(ensureNeverCalled, block) +} + class LambdaNoParamRecorder(ensureNeverCalled: Boolean, val block: () -> R) : LambdaRecorder(ensureNeverCalled), () -> R { override fun invoke(): R { onInvoke() @@ -109,3 +116,12 @@ class LambdaFourParamsRecorder(ensureNeverCal return block(p1, p2, p3, p4) } } + +class LambdaFiveParamsRecorder(ensureNeverCalled: Boolean, val block: (T1, T2, T3, T4, T5) -> R) : LambdaRecorder( + ensureNeverCalled +), (T1, T2, T3, T4, T5) -> R { + override fun invoke(p1: T1, p2: T2, p3: T3, p4: T4, p5: T5): R { + onInvoke(p1, p2, p3, p4, p5) + return block(p1, p2, p3, p4, p5) + } +}