Timeline : continue to fix more tests...

This commit is contained in:
ganfra 2024-04-25 12:41:34 +02:00
parent 60192bd080
commit 9ffed34303
9 changed files with 198 additions and 63 deletions

View file

@ -27,6 +27,7 @@ import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
import com.bumble.appyx.testing.unit.common.helper.parentNodeTestHelper import com.bumble.appyx.testing.unit.common.helper.parentNodeTestHelper
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.appnav.di.RoomComponentFactory 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.appnav.room.joined.JoinedRoomLoadedFlowNode
import io.element.android.features.messages.api.MessagesEntryPoint import io.element.android.features.messages.api.MessagesEntryPoint
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
@ -124,7 +125,7 @@ class JoinRoomLoadedFlowNodeTest {
// GIVEN // GIVEN
val room = FakeMatrixRoom() val room = FakeMatrixRoom()
val fakeMessagesEntryPoint = FakeMessagesEntryPoint() val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
val inputs = JoinedRoomLoadedFlowNode.Inputs(room) val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Messages())
val roomFlowNode = createJoinedRoomLoadedFlowNode( val roomFlowNode = createJoinedRoomLoadedFlowNode(
plugins = listOf(inputs), plugins = listOf(inputs),
messagesEntryPoint = fakeMessagesEntryPoint, messagesEntryPoint = fakeMessagesEntryPoint,
@ -146,7 +147,7 @@ class JoinRoomLoadedFlowNodeTest {
val room = FakeMatrixRoom() val room = FakeMatrixRoom()
val fakeMessagesEntryPoint = FakeMessagesEntryPoint() val fakeMessagesEntryPoint = FakeMessagesEntryPoint()
val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint() val fakeRoomDetailsEntryPoint = FakeRoomDetailsEntryPoint()
val inputs = JoinedRoomLoadedFlowNode.Inputs(room) val inputs = JoinedRoomLoadedFlowNode.Inputs(room, RoomNavigationTarget.Messages())
val roomFlowNode = createJoinedRoomLoadedFlowNode( val roomFlowNode = createJoinedRoomLoadedFlowNode(
plugins = listOf(inputs), plugins = listOf(inputs),
messagesEntryPoint = fakeMessagesEntryPoint, messagesEntryPoint = fakeMessagesEntryPoint,

View file

@ -18,9 +18,11 @@ package io.element.android.features.location.impl.common.actions
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.location.api.Location import io.element.android.features.location.api.Location
import org.junit.Ignore
import org.junit.Test import org.junit.Test
import java.net.URLEncoder import java.net.URLEncoder
@Ignore
internal class AndroidLocationActionsTest { internal class AndroidLocationActionsTest {
// We use an Android-native encoder in the actual app, switch to an equivalent JVM one for the tests // 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") private fun urlEncoder(input: String) = URLEncoder.encode(input, "US-ASCII")

View file

@ -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.FakeFeatureFlagService
import io.element.android.libraries.featureflag.test.InMemoryAppPreferencesStore import io.element.android.libraries.featureflag.test.InMemoryAppPreferencesStore
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.media.MediaSource 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.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState 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.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo 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.room.aRoomMember
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
import io.element.android.libraries.mediapickers.test.FakePickerProvider import io.element.android.libraries.mediapickers.test.FakePickerProvider
import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer import io.element.android.libraries.mediaplayer.test.FakeMediaPlayer
import io.element.android.libraries.mediaupload.api.MediaSender 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.WarmUpRule
import io.element.android.tests.testutils.consumeItemsUntilPredicate import io.element.android.tests.testutils.consumeItemsUntilPredicate
import io.element.android.tests.testutils.consumeItemsUntilTimeout 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.element.android.tests.testutils.testCoroutineDispatchers
import io.mockk.mockk import io.mockk.mockk
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -169,7 +174,13 @@ class MessagesPresenterTest {
@Test @Test
fun `present - handle toggling a reaction`() = runTest { fun `present - handle toggling a reaction`() = runTest {
val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true) val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true)
val room = FakeMatrixRoom() val toggleReactionSuccess = lambdaRecorder { _: String, _: EventId -> Result.success(Unit) }
val toggleReactionFailure = lambdaRecorder { _: String, _: EventId -> Result.failure<Unit>(IllegalStateException("Failed to send reaction")) }
val timeline = FakeTimeline().apply {
this.toggleReactionLambda = toggleReactionSuccess
}
val room = FakeMatrixRoom(liveTimeline = timeline)
val presenter = createMessagesPresenter(matrixRoom = room, coroutineDispatchers = coroutineDispatchers) val presenter = createMessagesPresenter(matrixRoom = room, coroutineDispatchers = coroutineDispatchers)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
@ -177,29 +188,42 @@ class MessagesPresenterTest {
skipItems(1) skipItems(1)
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", AN_EVENT_ID)) initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", AN_EVENT_ID))
assertThat(room.myReactions.count()).isEqualTo(1)
// No crashes when sending a reaction failed // 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)) initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", AN_EVENT_ID))
assertThat(room.myReactions.count()).isEqualTo(1)
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) 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 @Test
fun `present - handle toggling a reaction twice`() = runTest { fun `present - handle toggling a reaction twice`() = runTest {
val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true) 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) val presenter = createMessagesPresenter(matrixRoom = room, coroutineDispatchers = coroutineDispatchers)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitFirstItem() val initialState = awaitFirstItem()
initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", AN_EVENT_ID)) initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", AN_EVENT_ID))
assertThat(room.myReactions.count()).isEqualTo(1)
initialState.eventSink.invoke(MessagesEvents.ToggleReaction("👍", AN_EVENT_ID)) 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) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(3) skipItems(2)
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, aMessageEvent(eventId = null))) initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, aMessageEvent(eventId = null)))
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
@ -430,7 +454,6 @@ class MessagesPresenterTest {
initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Redact, aMessageEvent())) initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Redact, aMessageEvent()))
assertThat(matrixRoom.redactEventEventIdParam).isEqualTo(AN_EVENT_ID) assertThat(matrixRoom.redactEventEventIdParam).isEqualTo(AN_EVENT_ID)
assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None)
skipItems(1) // back paginating
} }
} }

View file

@ -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.permalink.FakePermalinkParser
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.FakeTimeline
import io.element.android.libraries.mediapickers.api.PickerProvider import io.element.android.libraries.mediapickers.api.PickerProvider
import io.element.android.libraries.mediapickers.test.FakePickerProvider import io.element.android.libraries.mediapickers.test.FakePickerProvider
import io.element.android.libraries.mediaupload.api.MediaPreProcessor 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.libraries.textcomposer.model.SuggestionType
import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.WarmUpRule 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.element.android.tests.testutils.waitForPredicate
import io.mockk.mockk import io.mockk.mockk
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -260,7 +265,13 @@ class MessageComposerPresenterTest {
@Test @Test
fun `present - edit sent message`() = runTest { fun `present - edit sent message`() = runTest {
val fakeMatrixRoom = FakeMatrixRoom() val editMessageLambda = lambdaRecorder { _: EventId?, _: TransactionId?, _: String, _: String?, _: List<Mention> ->
Result.success(Unit)
}
val timeline = FakeTimeline().apply {
this.editMessageLambda = editMessageLambda
}
val fakeMatrixRoom = FakeMatrixRoom(liveTimeline = timeline)
val presenter = createPresenter( val presenter = createPresenter(
this, this,
fakeMatrixRoom, fakeMatrixRoom,
@ -284,7 +295,13 @@ class MessageComposerPresenterTest {
skipItems(1) skipItems(1)
val messageSentState = awaitItem() val messageSentState = awaitItem()
assertThat(messageSentState.richTextEditorState.messageHtml).isEqualTo("") 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( assertThat(analyticsService.capturedEvents).containsExactly(
Composer( Composer(
inThread = false, inThread = false,
@ -298,7 +315,13 @@ class MessageComposerPresenterTest {
@Test @Test
fun `present - edit not sent message`() = runTest { fun `present - edit not sent message`() = runTest {
val fakeMatrixRoom = FakeMatrixRoom() val editMessageLambda = lambdaRecorder { _: EventId?, _: TransactionId?, _: String, _: String?, _: List<Mention> ->
Result.success(Unit)
}
val timeline = FakeTimeline().apply {
this.editMessageLambda = editMessageLambda
}
val fakeMatrixRoom = FakeMatrixRoom(liveTimeline = timeline)
val presenter = createPresenter( val presenter = createPresenter(
this, this,
fakeMatrixRoom, fakeMatrixRoom,
@ -322,7 +345,13 @@ class MessageComposerPresenterTest {
skipItems(1) skipItems(1)
val messageSentState = awaitItem() val messageSentState = awaitItem()
assertThat(messageSentState.richTextEditorState.messageHtml).isEqualTo("") 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( assertThat(analyticsService.capturedEvents).containsExactly(
Composer( Composer(
inThread = false, inThread = false,
@ -336,7 +365,13 @@ class MessageComposerPresenterTest {
@Test @Test
fun `present - reply message`() = runTest { fun `present - reply message`() = runTest {
val fakeMatrixRoom = FakeMatrixRoom() val replyMessageLambda = lambdaRecorder {_: EventId, _: String, _: String?, _:List<Mention> ->
Result.success(Unit)
}
val timeline = FakeTimeline().apply {
this.replyMessageLambda = replyMessageLambda
}
val fakeMatrixRoom = FakeMatrixRoom(liveTimeline = timeline)
val presenter = createPresenter( val presenter = createPresenter(
this, this,
fakeMatrixRoom, fakeMatrixRoom,
@ -356,7 +391,13 @@ class MessageComposerPresenterTest {
state.eventSink.invoke(MessageComposerEvents.SendMessage(A_REPLY.toMessage())) state.eventSink.invoke(MessageComposerEvents.SendMessage(A_REPLY.toMessage()))
val messageSentState = awaitItem() val messageSentState = awaitItem()
assertThat(messageSentState.richTextEditorState.messageHtml).isEqualTo("") 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( assertThat(analyticsService.capturedEvents).containsExactly(
Composer( Composer(
inThread = false, inThread = false,
@ -832,7 +873,17 @@ class MessageComposerPresenterTest {
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
@Test @Test
fun `present - send messages with intentional mentions`() = runTest { fun `present - send messages with intentional mentions`() = runTest {
val room = FakeMatrixRoom() val replyMessageLambda = lambdaRecorder {_: EventId, _: String, _: String?, _:List<Mention> ->
Result.success(Unit)
}
val editMessageLambda = lambdaRecorder { _: EventId?, _: TransactionId?, _: String, _: String?, _: List<Mention> ->
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) val presenter = createPresenter(room = room, coroutineScope = this)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
@ -867,7 +918,9 @@ class MessageComposerPresenterTest {
initialState.eventSink(MessageComposerEvents.SendMessage(A_MESSAGE.toMessage())) initialState.eventSink(MessageComposerEvents.SendMessage(A_MESSAGE.toMessage()))
advanceUntilIdle() 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 // Check intentional mentions on edit message
skipItems(1) skipItems(1)
@ -883,7 +936,10 @@ class MessageComposerPresenterTest {
initialState.eventSink(MessageComposerEvents.SendMessage(A_MESSAGE.toMessage())) initialState.eventSink(MessageComposerEvents.SendMessage(A_MESSAGE.toMessage()))
advanceUntilIdle() 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) skipItems(1)
} }

View file

@ -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.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.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
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
fun aPollTimeline( fun aPollTimelineItems(
polls: Map<EventId, PollContent> = emptyMap(), polls: Map<EventId, PollContent> = emptyMap(),
): FakeTimeline { ): Flow<List<MatrixTimelineItem>> {
return FakeTimeline( return flowOf(
initialTimelineItems = polls.map { entry -> polls.map { entry ->
MatrixTimelineItem.Event( MatrixTimelineItem.Event(
entry.key.value, entry.key.value,
anEventTimelineItem( anEventTimelineItem(

View file

@ -25,33 +25,42 @@ import im.vector.app.features.analytics.plan.Composer
import im.vector.app.features.analytics.plan.PollCreation import im.vector.app.features.analytics.plan.PollCreation
import io.element.android.features.messages.test.FakeMessageComposerContext import io.element.android.features.messages.test.FakeMessageComposerContext
import io.element.android.features.poll.api.create.CreatePollMode 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.anOngoingPollContent
import io.element.android.features.poll.impl.data.PollRepository 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.poll.PollKind
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.LiveTimelineProvider
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.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.SavePollInvocation 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.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.WarmUpRule 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.delay
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
class CreatePollPresenterTest { @OptIn(ExperimentalCoroutinesApi::class) class CreatePollPresenterTest {
@get:Rule @get:Rule
val warmUpRule = WarmUpRule() val warmUpRule = WarmUpRule()
private val pollEventId = AN_EVENT_ID private val pollEventId = AN_EVENT_ID
private var navUpInvocationsCount = 0 private var navUpInvocationsCount = 0
private val existingPoll = anOngoingPollContent() private val existingPoll = anOngoingPollContent()
private val timeline = FakeTimeline(
timelineItems = aPollTimelineItems(mapOf(pollEventId to existingPoll))
)
private val fakeMatrixRoom = FakeMatrixRoom( private val fakeMatrixRoom = FakeMatrixRoom(
liveTimeline = aPollTimeline( liveTimeline = timeline
mapOf(pollEventId to existingPoll)
)
) )
private val fakeAnalyticsService = FakeAnalyticsService() private val fakeAnalyticsService = FakeAnalyticsService()
private val fakeMessageComposerContext = FakeMessageComposerContext() private val fakeMessageComposerContext = FakeMessageComposerContext()
@ -80,7 +89,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(
liveTimeline = aPollTimeline() liveTimeline = FakeTimeline()
) )
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) {
@ -180,6 +189,12 @@ class CreatePollPresenterTest {
@Test @Test
fun `edit poll sends a poll edit event`() = runTest { fun `edit poll sends a poll edit event`() = runTest {
val editPollLambda = lambdaRecorder { _: EventId, _: String, _: List<String>, _: Int, _: PollKind ->
Result.success(Unit)
}
timeline.apply {
this.editPollLambda = editPollLambda
}
val presenter = createCreatePollPresenter(mode = CreatePollMode.EditPoll(pollEventId)) val presenter = createCreatePollPresenter(mode = CreatePollMode.EditPoll(pollEventId))
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
@ -201,16 +216,18 @@ class CreatePollPresenterTest {
).apply { ).apply {
eventSink(CreatePollEvents.Save) eventSink(CreatePollEvents.Save)
} }
delay(1) // Wait for the coroutine to finish advanceUntilIdle() // Wait for the coroutine to finish
assertThat(fakeMatrixRoom.editPollInvocations.size).isEqualTo(1)
assertThat(fakeMatrixRoom.editPollInvocations.last()).isEqualTo( assert(editPollLambda)
SavePollInvocation( .isCalledOnce()
question = "Changed question", .with(
answers = listOf("Changed answer 1", "Changed answer 2", "Maybe"), value(pollEventId),
maxSelections = 1, value("Changed question"),
pollKind = PollKind.Disclosed value(listOf("Changed answer 1", "Changed answer 2", "Maybe")),
value(1),
value(PollKind.Disclosed)
) )
)
assertThat(fakeAnalyticsService.capturedEvents.size).isEqualTo(2) assertThat(fakeAnalyticsService.capturedEvents.size).isEqualTo(2)
assertThat(fakeAnalyticsService.capturedEvents[0]).isEqualTo( assertThat(fakeAnalyticsService.capturedEvents[0]).isEqualTo(
Composer( Composer(
@ -233,6 +250,12 @@ class CreatePollPresenterTest {
@Test @Test
fun `when edit poll fails, error is tracked`() = runTest { fun `when edit poll fails, error is tracked`() = runTest {
val error = Exception("cause") val error = Exception("cause")
val editPollLambda = lambdaRecorder { _: EventId, _: String, _: List<String>, _: Int, _: PollKind ->
Result.failure<Unit>(error)
}
timeline.apply {
this.editPollLambda = editPollLambda
}
fakeMatrixRoom.givenEditPollResult(Result.failure(error)) fakeMatrixRoom.givenEditPollResult(Result.failure(error))
val presenter = createCreatePollPresenter(mode = CreatePollMode.EditPoll(pollEventId)) val presenter = createCreatePollPresenter(mode = CreatePollMode.EditPoll(pollEventId))
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
@ -241,8 +264,8 @@ class CreatePollPresenterTest {
awaitDefaultItem() awaitDefaultItem()
awaitPollLoaded().eventSink(CreatePollEvents.SetAnswer(0, "A")) awaitPollLoaded().eventSink(CreatePollEvents.SetAnswer(0, "A"))
awaitPollLoaded(newAnswer1 = "A").eventSink(CreatePollEvents.Save) awaitPollLoaded(newAnswer1 = "A").eventSink(CreatePollEvents.Save)
delay(1) // Wait for the coroutine to finish advanceUntilIdle() // Wait for the coroutine to finish
assertThat(fakeMatrixRoom.editPollInvocations).hasSize(1) assert(editPollLambda).isCalledOnce()
assertThat(fakeAnalyticsService.capturedEvents).isEmpty() assertThat(fakeAnalyticsService.capturedEvents).isEmpty()
assertThat(fakeAnalyticsService.trackedErrors).hasSize(1) assertThat(fakeAnalyticsService.trackedErrors).hasSize(1)
assertThat(fakeAnalyticsService.trackedErrors).containsExactly( assertThat(fakeAnalyticsService.trackedErrors).containsExactly(
@ -497,22 +520,22 @@ class CreatePollPresenterTest {
newAnswer1: String? = null, newAnswer1: String? = null,
newAnswer2: String? = null, newAnswer2: String? = null,
) = ) =
awaitItem().apply { awaitItem().also { state ->
assertThat(canSave).isTrue() assertThat(state.canSave).isTrue()
assertThat(canAddAnswer).isTrue() assertThat(state.canAddAnswer).isTrue()
assertThat(question).isEqualTo(newQuestion ?: existingPoll.question) assertThat(state.question).isEqualTo(newQuestion ?: existingPoll.question)
assertThat(answers).isEqualTo(existingPoll.expectedAnswersState().toMutableList().apply { assertThat(state.answers).isEqualTo(existingPoll.expectedAnswersState().toMutableList().apply {
newAnswer1?.let { this[0] = Answer(it, true) } newAnswer1?.let { this[0] = Answer(it, true) }
newAnswer2?.let { this[1] = Answer(it, true) } newAnswer2?.let { this[1] = Answer(it, true) }
}) })
assertThat(pollKind).isEqualTo(existingPoll.kind) assertThat(state.pollKind).isEqualTo(existingPoll.kind)
} }
private fun createCreatePollPresenter( private fun createCreatePollPresenter(
mode: CreatePollMode = CreatePollMode.NewPoll, mode: CreatePollMode = CreatePollMode.NewPoll,
room: MatrixRoom = fakeMatrixRoom, room: MatrixRoom = fakeMatrixRoom,
): CreatePollPresenter = CreatePollPresenter( ): CreatePollPresenter = CreatePollPresenter(
repository = PollRepository(room), repository = PollRepository(room, LiveTimelineProvider(room)),
analyticsService = fakeAnalyticsService, analyticsService = fakeAnalyticsService,
messageComposerContext = fakeMessageComposerContext, messageComposerContext = fakeMessageComposerContext,
navigateUp = { navUpInvocationsCount++ }, navigateUp = { navUpInvocationsCount++ },

View file

@ -22,7 +22,7 @@ import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.poll.api.actions.EndPollAction import io.element.android.features.poll.api.actions.EndPollAction
import io.element.android.features.poll.api.actions.SendPollResponseAction 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.anEndedPollContent
import io.element.android.features.poll.impl.anOngoingPollContent import io.element.android.features.poll.impl.anOngoingPollContent
import io.element.android.features.poll.impl.history.model.PollHistoryFilter 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.features.poll.test.actions.FakeSendPollResponseAction
import io.element.android.libraries.dateformatter.test.FakeDaySeparatorFormatter import io.element.android.libraries.dateformatter.test.FakeDaySeparatorFormatter
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.Timeline
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.AN_EVENT_ID_2 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.FakeMatrixClient
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.timeline.FakeTimeline
import io.element.android.tests.testutils.WarmUpRule 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 io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.getAndUpdate
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@ -50,11 +56,14 @@ class PollHistoryPresenterTest {
@get:Rule @get:Rule
val warmUpRule = WarmUpRule() val warmUpRule = WarmUpRule()
private val timeline = aPollTimeline( private val backwardPaginationStatus = MutableStateFlow(Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = true))
polls = mapOf( private val timeline = FakeTimeline(
timelineItems = aPollTimelineItems(
mapOf(
AN_EVENT_ID to anOngoingPollContent(), AN_EVENT_ID to anOngoingPollContent(),
AN_EVENT_ID_2 to anEndedPollContent() AN_EVENT_ID_2 to anEndedPollContent()
) )),
backwardPaginationStatus = backwardPaginationStatus
) )
private val room = FakeMatrixRoom( private val room = FakeMatrixRoom(
liveTimeline = timeline liveTimeline = timeline
@ -66,7 +75,6 @@ class PollHistoryPresenterTest {
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(1)
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.activeFilter).isEqualTo(PollHistoryFilter.ONGOING) assertThat(state.activeFilter).isEqualTo(PollHistoryFilter.ONGOING)
assertThat(state.pollHistoryItems.size).isEqualTo(0) assertThat(state.pollHistoryItems.size).isEqualTo(0)
@ -127,26 +135,30 @@ class PollHistoryPresenterTest {
@Test @Test
fun `present - load more scenario`() = runTest { fun `present - load more scenario`() = runTest {
val paginateLambda = lambdaRecorder{ _: Timeline.PaginationDirection ->
Result.success(false)
}
timeline.apply {
this.paginateLambda = paginateLambda
}
val presenter = createPollHistoryPresenter(room = room) val presenter = createPollHistoryPresenter(room = room)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
skipItems(2) skipItems(1)
awaitItem().also { state ->
assertThat(state.pollHistoryItems.size).isEqualTo(2)
}
timeline.updatePaginationState {
copy(isBackPaginating = false)
}
val loadedState = awaitItem() val loadedState = awaitItem()
assertThat(loadedState.isLoading).isFalse() assertThat(loadedState.isLoading).isFalse()
loadedState.eventSink(PollHistoryEvents.LoadMore) loadedState.eventSink(PollHistoryEvents.LoadMore)
backwardPaginationStatus.getAndUpdate { it.copy(isPaginating = true) }
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.isLoading).isTrue() assertThat(state.isLoading).isTrue()
} }
backwardPaginationStatus.getAndUpdate { it.copy(isPaginating = false) }
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.isLoading).isFalse() assertThat(state.isLoading).isFalse()
} }
// Called once by the initial load and once by the load more event
assert(paginateLambda).isCalledExactly(2)
} }
} }

View file

@ -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.MatrixClient
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.RoomMembershipObserver 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.preferences.impl.store.DefaultSessionPreferencesStore
import io.element.android.libraries.push.test.notifications.FakeNotificationDrawerManager import io.element.android.libraries.push.test.notifications.FakeNotificationDrawerManager
import io.element.android.services.analytics.noop.NoopAnalyticsService import io.element.android.services.analytics.noop.NoopAnalyticsService
@ -146,7 +147,7 @@ class RoomListScreen(
Singleton.appScope.launch { Singleton.appScope.launch {
withContext(coroutineDispatchers.io) { withContext(coroutineDispatchers.io) {
matrixClient.getRoom(roomId)!!.use { room -> matrixClient.getRoom(roomId)!!.use { room ->
room.timeline.paginateBackwards(20, 50) room.liveTimeline.paginate(Timeline.PaginationDirection.BACKWARDS)
} }
} }
} }

View file

@ -71,6 +71,13 @@ inline fun <reified T1, reified T2, reified T3, reified T4, reified R> lambdaRec
return LambdaFourParamsRecorder(ensureNeverCalled, block) return LambdaFourParamsRecorder(ensureNeverCalled, block)
} }
inline fun <reified T1, reified T2, reified T3, reified T4, reified T5, reified R> lambdaRecorder(
ensureNeverCalled: Boolean = false,
noinline block: (T1, T2, T3, T4, T5) -> R
): LambdaFiveParamsRecorder<T1, T2, T3, T4, T5, R> {
return LambdaFiveParamsRecorder(ensureNeverCalled, block)
}
class LambdaNoParamRecorder<out R>(ensureNeverCalled: Boolean, val block: () -> R) : LambdaRecorder(ensureNeverCalled), () -> R { class LambdaNoParamRecorder<out R>(ensureNeverCalled: Boolean, val block: () -> R) : LambdaRecorder(ensureNeverCalled), () -> R {
override fun invoke(): R { override fun invoke(): R {
onInvoke() onInvoke()
@ -109,3 +116,12 @@ class LambdaFourParamsRecorder<in T1, in T2, in T3, in T4, out R>(ensureNeverCal
return block(p1, p2, p3, p4) return block(p1, p2, p3, p4)
} }
} }
class LambdaFiveParamsRecorder<in T1, in T2, in T3, in T4, in T5, out R>(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)
}
}