diff --git a/features/messages/impl/build.gradle.kts b/features/messages/impl/build.gradle.kts index e392c96e04..c6570db10e 100644 --- a/features/messages/impl/build.gradle.kts +++ b/features/messages/impl/build.gradle.kts @@ -97,6 +97,8 @@ dependencies { testImplementation(libs.test.mockk) testImplementation(libs.test.junitext) testImplementation(libs.test.robolectric) + testImplementation(projects.features.poll.test) + testImplementation(projects.features.poll.impl) ksp(libs.showkase.processor) } 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 1bed98ae06..bf4e03581d 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 @@ -47,6 +47,8 @@ import io.element.android.features.messages.impl.voicemessages.composer.VoiceMes import io.element.android.features.messages.impl.voicemessages.timeline.FakeRedactedVoiceMessageManager import io.element.android.features.messages.test.FakeMessageComposerContext import io.element.android.features.networkmonitor.test.FakeNetworkMonitor +import io.element.android.features.poll.test.actions.FakeEndPollAction +import io.element.android.features.poll.test.actions.FakeSendPollResponseAction import io.element.android.libraries.androidutils.clipboard.FakeClipboardHelper import io.element.android.libraries.architecture.Async import io.element.android.libraries.core.coroutine.CoroutineDispatchers @@ -705,11 +707,12 @@ class MessagesPresenterTest { dispatchers = coroutineDispatchers, appScope = this, navigator = navigator, - analyticsService = analyticsService, encryptionService = FakeEncryptionService(), verificationService = FakeSessionVerificationService(), featureFlagService = FakeFeatureFlagService(), redactedVoiceMessageManager = FakeRedactedVoiceMessageManager(), + endPollAction = FakeEndPollAction(), + sendPollResponseAction = FakeSendPollResponseAction(), ) val timelinePresenterFactory = object: TimelinePresenter.Factory { override fun create(navigator: MessagesNavigator): TimelinePresenter { diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/timelineItemsFactory.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/timelineItemsFactory.kt index 879333f690..23b3d1aea0 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/timelineItemsFactory.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/timelineItemsFactory.kt @@ -32,6 +32,7 @@ import io.element.android.features.messages.impl.timeline.factories.event.Timeli import io.element.android.features.messages.impl.timeline.factories.virtual.TimelineItemDaySeparatorFactory import io.element.android.features.messages.impl.timeline.factories.virtual.TimelineItemVirtualFactory import io.element.android.features.messages.impl.timeline.groups.TimelineItemGrouper +import io.element.android.features.poll.test.pollcontent.FakePollContentStateFactory import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter import io.element.android.libraries.dateformatter.test.FakeDaySeparatorFormatter import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter @@ -57,7 +58,7 @@ internal fun TestScope.aTimelineItemsFactory(): TimelineItemsFactory { ), redactedMessageFactory = TimelineItemContentRedactedFactory(), stickerFactory = TimelineItemContentStickerFactory(), - pollFactory = TimelineItemContentPollFactory(matrixClient, FakeFeatureFlagService()), + pollFactory = TimelineItemContentPollFactory(FakeFeatureFlagService(), FakePollContentStateFactory()), utdFactory = TimelineItemContentUTDFactory(), roomMembershipFactory = TimelineItemContentRoomMembershipFactory(timelineEventFormatter), profileChangeFactory = TimelineItemContentProfileChangeFactory(timelineEventFormatter), diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt index dea1975c6c..c07286dddc 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt @@ -20,10 +20,7 @@ import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import im.vector.app.features.analytics.plan.PollEnd -import im.vector.app.features.analytics.plan.PollVote import io.element.android.features.messages.impl.FakeMessagesNavigator -import io.element.android.features.messages.impl.fixtures.aMessageEvent import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactory import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory import io.element.android.features.messages.impl.timeline.model.NewEventState @@ -32,8 +29,11 @@ import io.element.android.features.messages.impl.timeline.session.SessionState import io.element.android.features.messages.impl.voicemessages.timeline.FakeRedactedVoiceMessageManager import io.element.android.features.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager import io.element.android.features.messages.impl.voicemessages.timeline.aRedactedMatrixTimeline +import io.element.android.features.poll.api.actions.EndPollAction +import io.element.android.features.poll.api.actions.SendPollResponseAction +import io.element.android.features.poll.test.actions.FakeEndPollAction +import io.element.android.features.poll.test.actions.FakeSendPollResponseAction import io.element.android.libraries.featureflag.test.FakeFeatureFlagService -import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.item.event.EventReaction @@ -48,13 +48,11 @@ import io.element.android.libraries.matrix.test.room.anEventTimelineItem import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService import io.element.android.libraries.matrix.ui.components.aMatrixUserList -import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.WarmUpRule 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.testCoroutineDispatchers -import io.element.android.tests.testutils.waitForPredicate import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.delay import kotlinx.coroutines.test.TestScope @@ -295,12 +293,10 @@ class TimelinePresenterTest { } @Test - fun `present - PollAnswerSelected event calls into rust room api and analytics`() = runTest { - val room = FakeMatrixRoom() - val analyticsService = FakeAnalyticsService() + fun `present - PollAnswerSelected event`() = runTest { + val sendPollResponseAction = FakeSendPollResponseAction() val presenter = createTimelinePresenter( - room = room, - analyticsService = analyticsService, + sendPollResponseAction = sendPollResponseAction, ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -309,34 +305,23 @@ class TimelinePresenterTest { initialState.eventSink.invoke(TimelineEvents.PollAnswerSelected(AN_EVENT_ID, "anAnswerId")) } delay(1) - assertThat(room.sendPollResponseInvocations.size).isEqualTo(1) - assertThat(room.sendPollResponseInvocations.first().answers).isEqualTo(listOf("anAnswerId")) - assertThat(room.sendPollResponseInvocations.first().pollStartId).isEqualTo(AN_EVENT_ID) - assertThat(analyticsService.capturedEvents.size).isEqualTo(1) - assertThat(analyticsService.capturedEvents.last()).isEqualTo(PollVote()) + sendPollResponseAction.verifyExecutionCount(1) } @Test - fun `present - PollEndClicked event calls into rust room api and analytics`() = runTest { - val room = FakeMatrixRoom() - val analyticsService = FakeAnalyticsService() + fun `present - PollEndClicked event`() = runTest { + val endPollAction = FakeEndPollAction() val presenter = createTimelinePresenter( - room = room, - analyticsService = analyticsService, + endPollAction = endPollAction, ) moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { val initialState = awaitItem() - initialState.eventSink(TimelineEvents.PollEndClicked(aMessageEvent().eventId!!)) - waitForPredicate { room.endPollInvocations.size == 1 } - cancelAndIgnoreRemainingEvents() - assertThat(room.endPollInvocations.size).isEqualTo(1) - assertThat(room.endPollInvocations.first().pollStartId).isEqualTo(AN_EVENT_ID) - assertThat(room.endPollInvocations.first().text).isEqualTo("The poll with event id: \$anEventId has ended.") - assertThat(analyticsService.capturedEvents.size).isEqualTo(1) - assertThat(analyticsService.capturedEvents.last()).isEqualTo(PollEnd()) + initialState.eventSink.invoke(TimelineEvents.PollEndClicked(AN_EVENT_ID)) } + delay(1) + endPollAction.verifyExecutionCount(1) } @Test @@ -379,36 +364,21 @@ class TimelinePresenterTest { timelineItemsFactory: TimelineItemsFactory = aTimelineItemsFactory(), redactedVoiceMessageManager: RedactedVoiceMessageManager = FakeRedactedVoiceMessageManager(), messagesNavigator: FakeMessagesNavigator = FakeMessagesNavigator(), - ): TimelinePresenter { + endPollAction: EndPollAction = FakeEndPollAction(), + sendPollResponseAction: SendPollResponseAction = FakeSendPollResponseAction(), + ): TimelinePresenter { return TimelinePresenter( timelineItemsFactory = timelineItemsFactory, room = FakeMatrixRoom(matrixTimeline = timeline), dispatchers = testCoroutineDispatchers(), appScope = this, navigator = messagesNavigator, - analyticsService = FakeAnalyticsService(), encryptionService = FakeEncryptionService(), verificationService = FakeSessionVerificationService(), featureFlagService = FakeFeatureFlagService(), redactedVoiceMessageManager = redactedVoiceMessageManager, - ) - } - - private fun TestScope.createTimelinePresenter( - room: MatrixRoom, - analyticsService: FakeAnalyticsService = FakeAnalyticsService(), - ): TimelinePresenter { - return TimelinePresenter( - timelineItemsFactory = aTimelineItemsFactory(), - room = room, - dispatchers = testCoroutineDispatchers(), - appScope = this, - navigator = FakeMessagesNavigator(), - analyticsService = analyticsService, - encryptionService = FakeEncryptionService(), - verificationService = FakeSessionVerificationService(), - featureFlagService = FakeFeatureFlagService(), - redactedVoiceMessageManager = FakeRedactedVoiceMessageManager(), + endPollAction = endPollAction, + sendPollResponseAction = sendPollResponseAction, ) } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentPollFactoryTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentPollFactoryTest.kt deleted file mode 100644 index 144c70f55d..0000000000 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentPollFactoryTest.kt +++ /dev/null @@ -1,328 +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.features.messages.impl.timeline.factories.event - -import com.google.common.truth.Truth -import io.element.android.features.messages.impl.timeline.model.event.TimelineItemPollContent -import io.element.android.features.poll.api.pollcontent.PollAnswerItem -import io.element.android.libraries.featureflag.api.FeatureFlags -import io.element.android.libraries.featureflag.test.FakeFeatureFlagService -import io.element.android.libraries.matrix.api.core.EventId -import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.api.poll.PollAnswer -import io.element.android.libraries.matrix.api.poll.PollKind -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.A_USER_ID -import io.element.android.libraries.matrix.test.A_USER_ID_10 -import io.element.android.libraries.matrix.test.A_USER_ID_2 -import io.element.android.libraries.matrix.test.A_USER_ID_3 -import io.element.android.libraries.matrix.test.A_USER_ID_4 -import io.element.android.libraries.matrix.test.A_USER_ID_5 -import io.element.android.libraries.matrix.test.A_USER_ID_6 -import io.element.android.libraries.matrix.test.A_USER_ID_7 -import io.element.android.libraries.matrix.test.A_USER_ID_8 -import io.element.android.libraries.matrix.test.A_USER_ID_9 -import io.element.android.libraries.matrix.test.FakeMatrixClient -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.ImmutableMap -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.persistentMapOf -import kotlinx.collections.immutable.toImmutableMap -import kotlinx.coroutines.test.runTest -import org.junit.Test - -internal class TimelineItemContentPollFactoryTest { - - private val factory = TimelineItemContentPollFactory( - matrixClient = FakeMatrixClient(), - featureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.Polls.key to true)), - ) - - @Test - fun `Disclosed poll - not ended, no votes`() = runTest { - Truth.assertThat(factory.create(aPollContent(), eventId = null, eventTimelineItem.isOwn)).isEqualTo(aTimelineItemPollContent()) - } - - @Test - fun `Disclosed poll - not ended, some votes, including one from current user`() = runTest { - val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap() - Truth.assertThat( - factory.create(aPollContent(votes = votes), eventId = null, eventTimelineItem.isOwn) - ) - .isEqualTo( - aTimelineItemPollContent( - answerItems = listOf( - aPollAnswerItem(answer = A_POLL_ANSWER_1, votesCount = 3, percentage = 0.3f), - aPollAnswerItem(answer = A_POLL_ANSWER_2, isSelected = true, votesCount = 6, percentage = 0.6f), - aPollAnswerItem(answer = A_POLL_ANSWER_3), - aPollAnswerItem(answer = A_POLL_ANSWER_4, votesCount = 1, percentage = 0.1f), - ), - ) - ) - } - - @Test - fun `Disclosed poll - ended, no votes, no winner`() = runTest { - Truth.assertThat( - factory.create(aPollContent(endTime = 1UL), eventId = null, eventTimelineItem.isOwn) - ).isEqualTo( - aTimelineItemPollContent().let { - it.copy( - answerItems = it.answerItems.map { answerItem -> answerItem.copy(isEnabled = false) }, - isEnded = true, - ) - } - ) - } - - @Test - fun `Disclosed poll - ended, some votes, including one from current user (winner)`() = runTest { - val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap() - Truth.assertThat( - factory.create( - aPollContent(votes = votes, endTime = 1UL), - eventId = null, - eventTimelineItem.isOwn - ) - ) - .isEqualTo( - aTimelineItemPollContent( - answerItems = listOf( - aPollAnswerItem(answer = A_POLL_ANSWER_1, isEnabled = false, votesCount = 3, percentage = 0.3f), - aPollAnswerItem(answer = A_POLL_ANSWER_2, isSelected = true, isEnabled = false, isWinner = true, votesCount = 6, percentage = 0.6f), - aPollAnswerItem(answer = A_POLL_ANSWER_3, isEnabled = false), - aPollAnswerItem(answer = A_POLL_ANSWER_4, isEnabled = false, votesCount = 1, percentage = 0.1f), - ), - isEnded = true, - ) - ) - } - - @Test - fun `Disclosed poll - ended, some votes, including one from current user (not winner) and two winning votes`() = runTest { - val votes = OTHER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap() - Truth.assertThat( - factory.create( - aPollContent(votes = votes, endTime = 1UL), - eventId = null, - eventTimelineItem.isOwn - ) - ) - .isEqualTo( - aTimelineItemPollContent( - answerItems = listOf( - aPollAnswerItem(answer = A_POLL_ANSWER_1, isEnabled = false, isWinner = true, votesCount = 4, percentage = 0.4f), - aPollAnswerItem(answer = A_POLL_ANSWER_2, isSelected = true, isEnabled = false, votesCount = 2, percentage = 0.2f), - aPollAnswerItem(answer = A_POLL_ANSWER_3, isEnabled = false), - aPollAnswerItem(answer = A_POLL_ANSWER_4, isEnabled = false, isWinner = true, votesCount = 4, percentage = 0.4f), - ), - isEnded = true, - ) - ) - } - - @Test - fun `Undisclosed poll - not ended, no votes`() = runTest { - Truth.assertThat( - factory.create( - aPollContent(PollKind.Undisclosed).copy(), - eventId = null, - eventTimelineItem.isOwn - ) - ).isEqualTo( - aTimelineItemPollContent(pollKind = PollKind.Undisclosed).let { - it.copy(answerItems = it.answerItems.map { answerItem -> answerItem.copy(isDisclosed = false) }) - } - ) - } - - @Test - fun `Undisclosed poll - not ended, some votes, including one from current user`() = runTest { - val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap() - Truth.assertThat( - factory.create( - aPollContent(pollKind = PollKind.Undisclosed, votes = votes), - eventId = null, - eventTimelineItem.isOwn - ) - ) - .isEqualTo( - aTimelineItemPollContent( - pollKind = PollKind.Undisclosed, - answerItems = listOf( - aPollAnswerItem(answer = A_POLL_ANSWER_1, isDisclosed = false, votesCount = 3, percentage = 0.3f), - aPollAnswerItem(answer = A_POLL_ANSWER_2, isDisclosed = false, isSelected = true, votesCount = 6, percentage = 0.6f), - aPollAnswerItem(answer = A_POLL_ANSWER_3, isDisclosed = false), - aPollAnswerItem(answer = A_POLL_ANSWER_4, isDisclosed = false, votesCount = 1, percentage = 0.1f), - ), - ) - ) - } - - @Test - fun `Undisclosed poll - ended, no votes, no winner`() = runTest { - Truth.assertThat( - factory.create( - aPollContent(pollKind = PollKind.Undisclosed, endTime = 1UL), - eventId = null, - eventTimelineItem.isOwn - ) - ).isEqualTo( - aTimelineItemPollContent().let { - it.copy( - pollKind = PollKind.Undisclosed, - answerItems = it.answerItems.map { answerItem -> - answerItem.copy(isDisclosed = true, isEnabled = false, isWinner = false) - }, - isEnded = true, - ) - } - ) - } - - @Test - fun `Undisclosed poll - ended, some votes, including one from current user (winner)`() = runTest { - val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap() - Truth.assertThat( - factory.create( - aPollContent(pollKind = PollKind.Undisclosed, votes = votes, endTime = 1UL), - eventId = null, - eventTimelineItem.isOwn - ) - ) - .isEqualTo( - aTimelineItemPollContent( - pollKind = PollKind.Undisclosed, - answerItems = listOf( - aPollAnswerItem(answer = A_POLL_ANSWER_1, isEnabled = false, votesCount = 3, percentage = 0.3f), - aPollAnswerItem(answer = A_POLL_ANSWER_2, isSelected = true, isEnabled = false, isWinner = true, votesCount = 6, percentage = 0.6f), - aPollAnswerItem(answer = A_POLL_ANSWER_3, isEnabled = false), - aPollAnswerItem(answer = A_POLL_ANSWER_4, isEnabled = false, votesCount = 1, percentage = 0.1f), - ), - isEnded = true, - ) - ) - } - - @Test - fun `Undisclosed poll - ended, some votes, including one from current user (not winner) and two winning votes`() = runTest { - val votes = OTHER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap() - Truth.assertThat( - factory.create( - aPollContent(PollKind.Undisclosed).copy(votes = votes, endTime = 1UL), - eventId = null, - eventTimelineItem.isOwn - ) - ) - .isEqualTo( - aTimelineItemPollContent( - pollKind = PollKind.Undisclosed, - answerItems = listOf( - aPollAnswerItem(A_POLL_ANSWER_1, isEnabled = false, isWinner = true, votesCount = 4, percentage = 0.4f), - aPollAnswerItem(A_POLL_ANSWER_2, isSelected = true, isEnabled = false, votesCount = 2, percentage = 0.2f), - aPollAnswerItem(A_POLL_ANSWER_3, isEnabled = false), - aPollAnswerItem(A_POLL_ANSWER_4, isEnabled = false, isWinner = true, votesCount = 4, percentage = 0.4f), - ), - isEnded = true, - ) - ) - } - - @Test - fun `eventId is populated`() = runTest { - Truth.assertThat(factory.create(aPollContent(), eventId = null, eventTimelineItem.isOwn)) - .isEqualTo(aTimelineItemPollContent(eventId = null)) - - Truth.assertThat(factory.create( - aPollContent(), - eventId = AN_EVENT_ID, - eventTimelineItem.isOwn - )) - .isEqualTo(aTimelineItemPollContent(eventId = AN_EVENT_ID)) - } - - private fun aPollContent( - pollKind: PollKind = PollKind.Disclosed, - votes: ImmutableMap> = persistentMapOf(), - endTime: ULong? = null, - ): PollContent = PollContent( - question = A_POLL_QUESTION, - kind = pollKind, - maxSelections = 1UL, - answers = persistentListOf(A_POLL_ANSWER_1, A_POLL_ANSWER_2, A_POLL_ANSWER_3, A_POLL_ANSWER_4), - votes = votes, - endTime = endTime, - ) - - private fun aTimelineItemPollContent( - eventId: EventId? = null, - pollKind: PollKind = PollKind.Disclosed, - answerItems: List = listOf( - aPollAnswerItem(A_POLL_ANSWER_1), - aPollAnswerItem(A_POLL_ANSWER_2), - aPollAnswerItem(A_POLL_ANSWER_3), - aPollAnswerItem(A_POLL_ANSWER_4), - ), - isEnded: Boolean = false, - ) = TimelineItemPollContent( - eventId = eventId, - question = A_POLL_QUESTION, - answerItems = answerItems, - pollKind = pollKind, - isEnded = isEnded, - ) - - private fun aPollAnswerItem( - answer: PollAnswer, - isSelected: Boolean = false, - isEnabled: Boolean = true, - isWinner: Boolean = false, - isDisclosed: Boolean = true, - votesCount: Int = 0, - percentage: Float = 0f, - ) = PollAnswerItem( - answer = answer, - isSelected = isSelected, - isEnabled = isEnabled, - isWinner = isWinner, - isDisclosed = isDisclosed, - votesCount = votesCount, - percentage = percentage, - ) - - private companion object TestData { - private const val A_POLL_QUESTION = "What is your favorite food?" - private val A_POLL_ANSWER_1 = PollAnswer("id_1", "Pizza") - private val A_POLL_ANSWER_2 = PollAnswer("id_2", "Pasta") - private val A_POLL_ANSWER_3 = PollAnswer("id_3", "French Fries") - private val A_POLL_ANSWER_4 = PollAnswer("id_4", "Hamburger") - - private val MY_USER_WINNING_VOTES = persistentMapOf( - A_POLL_ANSWER_1 to persistentListOf(A_USER_ID_2, A_USER_ID_3, A_USER_ID_4), - A_POLL_ANSWER_2 to persistentListOf(A_USER_ID /* my vote */, A_USER_ID_5, A_USER_ID_6, A_USER_ID_7, A_USER_ID_8, A_USER_ID_9), // winner - A_POLL_ANSWER_3 to persistentListOf(), - A_POLL_ANSWER_4 to persistentListOf(A_USER_ID_10), - ) - private val OTHER_WINNING_VOTES = persistentMapOf( - A_POLL_ANSWER_1 to persistentListOf(A_USER_ID_2, A_USER_ID_3, A_USER_ID_4, A_USER_ID_5), // winner - A_POLL_ANSWER_2 to persistentListOf(A_USER_ID /* my vote */, A_USER_ID_6), - A_POLL_ANSWER_3 to persistentListOf(), - A_POLL_ANSWER_4 to persistentListOf(A_USER_ID_7, A_USER_ID_8, A_USER_ID_9, A_USER_ID_10), // winner - ) - } -} diff --git a/features/poll/impl/build.gradle.kts b/features/poll/impl/build.gradle.kts index db91377660..bfa74d3684 100644 --- a/features/poll/impl/build.gradle.kts +++ b/features/poll/impl/build.gradle.kts @@ -52,6 +52,8 @@ dependencies { testImplementation(projects.services.analytics.test) testImplementation(projects.features.messages.test) testImplementation(projects.tests.testutils) + testImplementation(projects.libraries.dateformatter.test) + testImplementation(projects.features.poll.test) ksp(libs.showkase.processor) } 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 new file mode 100644 index 0000000000..bfe925a993 --- /dev/null +++ b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/PollFixtures.kt @@ -0,0 +1,55 @@ +/* + * 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.features.poll.impl + +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.FakeMatrixTimeline +import io.element.android.libraries.matrix.test.timeline.aPollContent +import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem +import kotlinx.collections.immutable.persistentListOf + +fun aPollTimeline( + polls: Map = emptyMap(), +): FakeMatrixTimeline { + return FakeMatrixTimeline( + initialTimelineItems = polls.map { entry -> + MatrixTimelineItem.Event( + entry.key.hashCode().toLong(), + anEventTimelineItem( + eventId = entry.key, + content = entry.value, + ) + ) + } + ) +} + +fun anOngoingPollContent() = aPollContent( + question = "Do you like polls?", + answers = persistentListOf( + PollAnswer("1", "Yes"), + PollAnswer("2", "No"), + PollAnswer("2", "Maybe"), + ), +) + +fun anEndedPollContent() = anOngoingPollContent().copy( + endTime = 1702400215U +) 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 8cb55704d5..114e7cb48f 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,21 +25,17 @@ 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.anOngoingPollContent import io.element.android.features.poll.impl.data.PollRepository -import io.element.android.libraries.matrix.api.poll.PollAnswer 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.MatrixTimelineItem 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.SavePollInvocation import io.element.android.libraries.matrix.test.room.FakeMatrixRoom -import io.element.android.libraries.matrix.test.room.aPollContent -import io.element.android.libraries.matrix.test.room.anEventTimelineItem -import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline +import io.element.android.libraries.matrix.test.room.SavePollInvocation import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.tests.testutils.WarmUpRule -import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.delay import kotlinx.coroutines.test.runTest import org.junit.Rule @@ -52,8 +48,12 @@ class CreatePollPresenterTest { private val pollEventId = AN_EVENT_ID private var navUpInvocationsCount = 0 - private val existingPoll = anExistingPoll() - private val fakeMatrixRoom = createFakeMatrixRoom(existingPoll) + private val existingPoll = anOngoingPollContent() + private val fakeMatrixRoom = FakeMatrixRoom( + matrixTimeline = aPollTimeline( + mapOf(pollEventId to existingPoll) + ) + ) private val fakeAnalyticsService = FakeAnalyticsService() private val fakeMessageComposerContext = FakeMessageComposerContext() @@ -80,7 +80,9 @@ class CreatePollPresenterTest { @Test fun `in edit mode, if poll doesn't exist, error is tracked and screen is closed`() = runTest { - val room = createFakeMatrixRoom(existingPoll = null) + val room = FakeMatrixRoom( + matrixTimeline = aPollTimeline() + ) val presenter = createCreatePollPresenter(mode = CreatePollMode.EditPoll(AN_EVENT_ID), room = room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -475,7 +477,6 @@ class CreatePollPresenterTest { } } - private suspend fun TurbineTestContext.awaitDefaultItem() = awaitItem().apply { Truth.assertThat(canSave).isFalse() @@ -518,35 +519,8 @@ class CreatePollPresenterTest { navigateUp = { navUpInvocationsCount++ }, mode = mode, ) - - private fun createFakeMatrixRoom( - existingPoll: PollContent? = anExistingPoll(), - ) = FakeMatrixRoom( - matrixTimeline = FakeMatrixTimeline( - initialTimelineItems = existingPoll?.let { - listOf( - MatrixTimelineItem.Event( - 0, - anEventTimelineItem( - eventId = pollEventId, - content = it, - ) - ) - ) - }.orEmpty() - ) - ) } -private fun anExistingPoll() = aPollContent( - question = "Do you like polls?", - answers = persistentListOf( - PollAnswer("1", "Yes"), - PollAnswer("2", "No"), - PollAnswer("2", "Maybe"), - ), -) - private fun PollContent.expectedAnswersState() = answers.map { answer -> Answer( text = answer.text, 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 new file mode 100644 index 0000000000..172ec5fa28 --- /dev/null +++ b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenterTest.kt @@ -0,0 +1,169 @@ +/* + * 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.features.poll.impl.history + +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +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.anEndedPollContent +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.PollHistoryItemsFactory +import io.element.android.features.poll.impl.model.DefaultPollContentStateFactory +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.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.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.consumeItemsUntilPredicate +import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.Test + +class PollHistoryPresenterTest { + + @get:Rule + val warmUpRule = WarmUpRule() + + private val room = FakeMatrixRoom( + matrixTimeline = aPollTimeline( + polls = mapOf( + AN_EVENT_ID to anOngoingPollContent(), + AN_EVENT_ID_2 to anEndedPollContent() + ) + ) + ) + + @Test + fun `present - initial states`() = runTest { + val presenter = createPollHistoryPresenter(room = room) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().also { state -> + assertThat(state.activeFilter).isEqualTo(PollHistoryFilter.ONGOING) + assertThat(state.pollHistoryItems.size).isEqualTo(0) + assertThat(state.isLoading).isTrue() + assertThat(state.hasMoreToLoad).isTrue() + } + consumeItemsUntilPredicate { + it.pollHistoryItems.size == 2 + }.last().also { state -> + assertThat(state.pollHistoryItems.size).isEqualTo(2) + assertThat(state.pollHistoryItems.ongoing).hasSize(1) + assertThat(state.pollHistoryItems.past).hasSize(1) + } + } + } + + @Test + fun `present - change filter scenario`() = runTest { + val presenter = createPollHistoryPresenter(room = room) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + awaitItem().also { state -> + assertThat(state.activeFilter).isEqualTo(PollHistoryFilter.ONGOING) + state.eventSink(PollHistoryEvents.OnFilterSelected(PollHistoryFilter.PAST)) + } + consumeItemsUntilPredicate { + it.activeFilter == PollHistoryFilter.PAST + }.last().also { state -> + state.eventSink(PollHistoryEvents.OnFilterSelected(PollHistoryFilter.ONGOING)) + } + consumeItemsUntilPredicate { + it.activeFilter == PollHistoryFilter.ONGOING + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `present - poll actions scenario`() = runTest { + val sendPollResponseAction = FakeSendPollResponseAction() + val endPollAction = FakeEndPollAction() + val presenter = createPollHistoryPresenter( + sendPollResponseAction = sendPollResponseAction, + endPollAction = endPollAction + ) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + val state = awaitItem() + state.eventSink(PollHistoryEvents.PollEndClicked(AN_EVENT_ID)) + runCurrent() + endPollAction.verifyExecutionCount(1) + state.eventSink(PollHistoryEvents.PollAnswerSelected(AN_EVENT_ID, "answer")) + runCurrent() + sendPollResponseAction.verifyExecutionCount(1) + cancelAndConsumeRemainingEvents() + } + } + + @Test + fun `present - load more scenario`() = runTest { + val presenter = createPollHistoryPresenter(room = room) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + consumeItemsUntilPredicate { + it.pollHistoryItems.size == 2 && !it.isLoading + }.last().also { state -> + state.eventSink(PollHistoryEvents.LoadMore) + } + consumeItemsUntilPredicate { + it.isLoading + } + consumeItemsUntilPredicate { + !it.isLoading + } + } + } + + private fun TestScope.createPollHistoryPresenter( + room: MatrixRoom = FakeMatrixRoom(), + appCoroutineScope: CoroutineScope = this, + endPollAction: EndPollAction = FakeEndPollAction(), + sendPollResponseAction: SendPollResponseAction = FakeSendPollResponseAction(), + pollHistoryItemFactory: PollHistoryItemsFactory = PollHistoryItemsFactory( + pollContentStateFactory = DefaultPollContentStateFactory(FakeMatrixClient()), + daySeparatorFormatter = FakeDaySeparatorFormatter(), + dispatchers = testCoroutineDispatchers(), + ), + ): PollHistoryPresenter { + return PollHistoryPresenter( + room = room, + appCoroutineScope = appCoroutineScope, + sendPollResponseAction = sendPollResponseAction, + endPollAction = endPollAction, + pollHistoryItemFactory = pollHistoryItemFactory, + ) + } +} diff --git a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/pollcontent/PollContentStateFactoryTest.kt b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/pollcontent/PollContentStateFactoryTest.kt new file mode 100644 index 0000000000..1c2bf88fbf --- /dev/null +++ b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/pollcontent/PollContentStateFactoryTest.kt @@ -0,0 +1,288 @@ +/* + * 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.features.poll.impl.pollcontent + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.poll.api.pollcontent.PollAnswerItem +import io.element.android.features.poll.api.pollcontent.PollContentState +import io.element.android.features.poll.impl.model.DefaultPollContentStateFactory +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.poll.PollAnswer +import io.element.android.libraries.matrix.api.poll.PollKind +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.A_USER_ID +import io.element.android.libraries.matrix.test.A_USER_ID_10 +import io.element.android.libraries.matrix.test.A_USER_ID_2 +import io.element.android.libraries.matrix.test.A_USER_ID_3 +import io.element.android.libraries.matrix.test.A_USER_ID_4 +import io.element.android.libraries.matrix.test.A_USER_ID_5 +import io.element.android.libraries.matrix.test.A_USER_ID_6 +import io.element.android.libraries.matrix.test.A_USER_ID_7 +import io.element.android.libraries.matrix.test.A_USER_ID_8 +import io.element.android.libraries.matrix.test.A_USER_ID_9 +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableMap +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.persistentMapOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toImmutableMap +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class PollContentStateFactoryTest { + + private val factory = DefaultPollContentStateFactory(FakeMatrixClient()) + private val eventTimelineItem = anEventTimelineItem() + + @Test + fun `Disclosed poll - not ended, no votes`() = runTest { + val state = factory.create(eventTimelineItem, aPollContent()) + val expectedState = aPollContentState() + assertThat(state).isEqualTo(expectedState) + } + + @Test + fun `Disclosed poll - not ended, some votes, including one from current user`() = runTest { + val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap() + val state = factory.create( + eventTimelineItem, aPollContent(votes = votes) + ) + val expectedState = aPollContentState( + answerItems = listOf( + aPollAnswerItem(answer = A_POLL_ANSWER_1, votesCount = 3, percentage = 0.3f), + aPollAnswerItem(answer = A_POLL_ANSWER_2, isSelected = true, votesCount = 6, percentage = 0.6f), + aPollAnswerItem(answer = A_POLL_ANSWER_3), + aPollAnswerItem(answer = A_POLL_ANSWER_4, votesCount = 1, percentage = 0.1f), + ) + ) + assertThat(state).isEqualTo(expectedState) + } + + @Test + fun `Disclosed poll - ended, no votes, no winner`() = runTest { + val state = factory.create(eventTimelineItem, aPollContent(endTime = 1UL)) + val expectedState = aPollContentState().let { + it.copy( + answerItems = it.answerItems.map { answerItem -> answerItem.copy(isEnabled = false) }.toImmutableList(), + isPollEnded = true, + ) + } + assertThat(state).isEqualTo(expectedState) + } + + @Test + fun `Disclosed poll - ended, some votes, including one from current user (winner)`() = runTest { + val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap() + val state = factory.create( + eventTimelineItem, aPollContent(votes = votes, endTime = 1UL) + ) + val expectedState = aPollContentState( + answerItems = listOf( + aPollAnswerItem(answer = A_POLL_ANSWER_1, isEnabled = false, votesCount = 3, percentage = 0.3f), + aPollAnswerItem(answer = A_POLL_ANSWER_2, isSelected = true, isEnabled = false, isWinner = true, votesCount = 6, percentage = 0.6f), + aPollAnswerItem(answer = A_POLL_ANSWER_3, isEnabled = false), + aPollAnswerItem(answer = A_POLL_ANSWER_4, isEnabled = false, votesCount = 1, percentage = 0.1f), + ), + isEnded = true, + ) + assertThat(state).isEqualTo(expectedState) + } + + @Test + fun `Disclosed poll - ended, some votes, including one from current user (not winner) and two winning votes`() = runTest { + val votes = OTHER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap() + val state = factory.create( + eventTimelineItem, aPollContent(votes = votes, endTime = 1UL) + ) + val expectedState = aPollContentState( + answerItems = listOf( + aPollAnswerItem(answer = A_POLL_ANSWER_1, isEnabled = false, isWinner = true, votesCount = 4, percentage = 0.4f), + aPollAnswerItem(answer = A_POLL_ANSWER_2, isSelected = true, isEnabled = false, votesCount = 2, percentage = 0.2f), + aPollAnswerItem(answer = A_POLL_ANSWER_3, isEnabled = false), + aPollAnswerItem(answer = A_POLL_ANSWER_4, isEnabled = false, isWinner = true, votesCount = 4, percentage = 0.4f), + ), + isEnded = true, + ) + assertThat(state).isEqualTo(expectedState) + } + + @Test + fun `Undisclosed poll - not ended, no votes`() = runTest { + val state = factory.create(eventTimelineItem, aPollContent(PollKind.Undisclosed)) + val expectedState = aPollContentState(pollKind = PollKind.Undisclosed).let { + it.copy( + answerItems = it.answerItems.map { answerItem -> answerItem.copy(isDisclosed = false) }.toImmutableList() + ) + } + assertThat(state).isEqualTo(expectedState) + } + + @Test + fun `Undisclosed poll - not ended, some votes, including one from current user`() = runTest { + val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap() + val state = factory.create( + eventTimelineItem, aPollContent(PollKind.Undisclosed, votes = votes) + ) + val expectedState = aPollContentState( + pollKind = PollKind.Undisclosed, + answerItems = listOf( + aPollAnswerItem(answer = A_POLL_ANSWER_1, isDisclosed = false, votesCount = 3, percentage = 0.3f), + aPollAnswerItem(answer = A_POLL_ANSWER_2, isDisclosed = false, isSelected = true, votesCount = 6, percentage = 0.6f), + aPollAnswerItem(answer = A_POLL_ANSWER_3, isDisclosed = false), + aPollAnswerItem(answer = A_POLL_ANSWER_4, isDisclosed = false, votesCount = 1, percentage = 0.1f), + ), + ) + assertThat(state).isEqualTo(expectedState) + } + + @Test + fun `Undisclosed poll - ended, no votes, no winner`() = runTest { + val state = factory.create(eventTimelineItem, aPollContent(PollKind.Undisclosed, endTime = 1UL)) + val expectedState = aPollContentState( + isEnded = true, + pollKind = PollKind.Undisclosed + ).let { + it.copy( + answerItems = it.answerItems.map { answerItem -> answerItem.copy(isDisclosed = true, isEnabled = false) }.toImmutableList(), + ) + } + assertThat(state).isEqualTo(expectedState) + } + + @Test + fun `Undisclosed poll - ended, some votes, including one from current user (winner)`() = runTest { + val votes = MY_USER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap() + val state = factory.create( + eventTimelineItem, aPollContent(PollKind.Undisclosed, votes = votes, endTime = 1UL) + ) + val expectedState = aPollContentState( + pollKind = PollKind.Undisclosed, + answerItems = listOf( + aPollAnswerItem(answer = A_POLL_ANSWER_1, isEnabled = false, votesCount = 3, percentage = 0.3f), + aPollAnswerItem(answer = A_POLL_ANSWER_2, isSelected = true, isEnabled = false, isWinner = true, votesCount = 6, percentage = 0.6f), + aPollAnswerItem(answer = A_POLL_ANSWER_3, isEnabled = false), + aPollAnswerItem(answer = A_POLL_ANSWER_4, isEnabled = false, votesCount = 1, percentage = 0.1f), + ), + isEnded = true, + ) + assertThat(state).isEqualTo(expectedState) + } + + @Test + fun `Undisclosed poll - ended, some votes, including one from current user (not winner) and two winning votes`() = runTest { + val votes = OTHER_WINNING_VOTES.mapKeys { it.key.id }.toImmutableMap() + val state = factory.create( + eventTimelineItem, aPollContent(PollKind.Undisclosed, votes = votes, endTime = 1UL) + ) + val expectedState = aPollContentState( + pollKind = PollKind.Undisclosed, + answerItems = listOf( + aPollAnswerItem(answer = A_POLL_ANSWER_1, isEnabled = false, isWinner = true, votesCount = 4, percentage = 0.4f), + aPollAnswerItem(answer = A_POLL_ANSWER_2, isSelected = true, isEnabled = false, votesCount = 2, percentage = 0.2f), + aPollAnswerItem(answer = A_POLL_ANSWER_3, isEnabled = false), + aPollAnswerItem(answer = A_POLL_ANSWER_4, isEnabled = false, isWinner = true, votesCount = 4, percentage = 0.4f), + ), + isEnded = true, + ) + assertThat(state).isEqualTo(expectedState) + } + + @Test + fun `eventId is populated`() = runTest { + val state = factory.create(eventTimelineItem, aPollContent()) + assertThat(state.eventId).isEqualTo(eventTimelineItem.eventId) + } + + private fun aPollContent( + pollKind: PollKind = PollKind.Disclosed, + votes: ImmutableMap> = persistentMapOf(), + endTime: ULong? = null, + ): PollContent = PollContent( + question = A_POLL_QUESTION, + kind = pollKind, + maxSelections = 1UL, + answers = persistentListOf(A_POLL_ANSWER_1, A_POLL_ANSWER_2, A_POLL_ANSWER_3, A_POLL_ANSWER_4), + votes = votes, + endTime = endTime, + ) + + private fun aPollContentState( + eventId: EventId? = AN_EVENT_ID, + pollKind: PollKind = PollKind.Disclosed, + answerItems: List = listOf( + aPollAnswerItem(A_POLL_ANSWER_1), + aPollAnswerItem(A_POLL_ANSWER_2), + aPollAnswerItem(A_POLL_ANSWER_3), + aPollAnswerItem(A_POLL_ANSWER_4), + ), + isEnded: Boolean = false, + isMine: Boolean = false, + isEditable: Boolean = false, + question: String = A_POLL_QUESTION, + ) = PollContentState( + eventId = eventId, + question = question, + answerItems = answerItems.toImmutableList(), + pollKind = pollKind, + isMine = isMine, + isPollEnded = isEnded, + isPollEditable = isEditable, + ) + + private fun aPollAnswerItem( + answer: PollAnswer, + isSelected: Boolean = false, + isEnabled: Boolean = true, + isWinner: Boolean = false, + isDisclosed: Boolean = true, + votesCount: Int = 0, + percentage: Float = 0f, + ) = PollAnswerItem( + answer = answer, + isSelected = isSelected, + isEnabled = isEnabled, + isWinner = isWinner, + isDisclosed = isDisclosed, + votesCount = votesCount, + percentage = percentage, + ) + + private companion object TestData { + private const val A_POLL_QUESTION = "What is your favorite food?" + private val A_POLL_ANSWER_1 = PollAnswer("id_1", "Pizza") + private val A_POLL_ANSWER_2 = PollAnswer("id_2", "Pasta") + private val A_POLL_ANSWER_3 = PollAnswer("id_3", "French Fries") + private val A_POLL_ANSWER_4 = PollAnswer("id_4", "Hamburger") + + private val MY_USER_WINNING_VOTES = persistentMapOf( + A_POLL_ANSWER_1 to persistentListOf(A_USER_ID_2, A_USER_ID_3, A_USER_ID_4), + A_POLL_ANSWER_2 to persistentListOf(A_USER_ID /* my vote */, A_USER_ID_5, A_USER_ID_6, A_USER_ID_7, A_USER_ID_8, A_USER_ID_9), // winner + A_POLL_ANSWER_3 to persistentListOf(), + A_POLL_ANSWER_4 to persistentListOf(A_USER_ID_10), + ) + private val OTHER_WINNING_VOTES = persistentMapOf( + A_POLL_ANSWER_1 to persistentListOf(A_USER_ID_2, A_USER_ID_3, A_USER_ID_4, A_USER_ID_5), // winner + A_POLL_ANSWER_2 to persistentListOf(A_USER_ID /* my vote */, A_USER_ID_6), + A_POLL_ANSWER_3 to persistentListOf(), + A_POLL_ANSWER_4 to persistentListOf(A_USER_ID_7, A_USER_ID_8, A_USER_ID_9, A_USER_ID_10), // winner + ) + } +} diff --git a/features/poll/test/build.gradle.kts b/features/poll/test/build.gradle.kts new file mode 100644 index 0000000000..2bae89d155 --- /dev/null +++ b/features/poll/test/build.gradle.kts @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2022 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. + */ + +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.features.poll.test" +} + +dependencies { + implementation(projects.libraries.matrix.api) + api(projects.features.poll.api) + implementation(libs.kotlinx.collections.immutable) +} diff --git a/features/poll/test/src/main/kotlin/io/element/android/features/poll/test/actions/FakeEndPollAction.kt b/features/poll/test/src/main/kotlin/io/element/android/features/poll/test/actions/FakeEndPollAction.kt new file mode 100644 index 0000000000..285db277e4 --- /dev/null +++ b/features/poll/test/src/main/kotlin/io/element/android/features/poll/test/actions/FakeEndPollAction.kt @@ -0,0 +1,34 @@ +/* + * 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.features.poll.test.actions + +import io.element.android.features.poll.api.actions.EndPollAction +import io.element.android.libraries.matrix.api.core.EventId + +class FakeEndPollAction : EndPollAction { + + private var executionCount = 0 + + fun verifyExecutionCount(count: Int) { + assert(executionCount == count) + } + + override suspend fun execute(pollStartId: EventId): Result { + executionCount++ + return Result.success(Unit) + } +} diff --git a/features/poll/test/src/main/kotlin/io/element/android/features/poll/test/actions/FakeSendPollResponseAction.kt b/features/poll/test/src/main/kotlin/io/element/android/features/poll/test/actions/FakeSendPollResponseAction.kt new file mode 100644 index 0000000000..f8fa3316d9 --- /dev/null +++ b/features/poll/test/src/main/kotlin/io/element/android/features/poll/test/actions/FakeSendPollResponseAction.kt @@ -0,0 +1,34 @@ +/* + * 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.features.poll.test.actions + +import io.element.android.features.poll.api.actions.SendPollResponseAction +import io.element.android.libraries.matrix.api.core.EventId + +class FakeSendPollResponseAction : SendPollResponseAction { + + private var executionCount = 0 + + fun verifyExecutionCount(count: Int) { + assert(executionCount == count) + } + + override suspend fun execute(pollStartId: EventId, answerId: String): Result { + executionCount++ + return Result.success(Unit) + } +} diff --git a/features/poll/test/src/main/kotlin/io/element/android/features/poll/test/pollcontent/FakePollContentStateFactory.kt b/features/poll/test/src/main/kotlin/io/element/android/features/poll/test/pollcontent/FakePollContentStateFactory.kt new file mode 100644 index 0000000000..626843f370 --- /dev/null +++ b/features/poll/test/src/main/kotlin/io/element/android/features/poll/test/pollcontent/FakePollContentStateFactory.kt @@ -0,0 +1,39 @@ +/* + * 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.features.poll.test.pollcontent + +import io.element.android.features.poll.api.pollcontent.PollAnswerItem +import io.element.android.features.poll.api.pollcontent.PollContentState +import io.element.android.features.poll.api.pollcontent.PollContentStateFactory +import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem +import io.element.android.libraries.matrix.api.timeline.item.event.PollContent +import kotlinx.collections.immutable.toImmutableList + +class FakePollContentStateFactory : PollContentStateFactory { + + override suspend fun create(event: EventTimelineItem, content: PollContent): PollContentState { + return PollContentState( + eventId = event.eventId, + isMine = event.isOwn, + question = content.question, + answerItems = emptyList().toImmutableList(), + pollKind = content.kind, + isPollEditable = event.isEditable, + isPollEnded = content.endTime != null, + ) + } +} 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 f16fdfbe98..db263fee94 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 @@ -431,7 +431,8 @@ class FakeMatrixRoom( ): Result = generateWidgetWebViewUrlResult override fun getWidgetDriver(widgetSettings: MatrixWidgetSettings): Result = getWidgetDriverResult - override suspend fun pollHistory(): MatrixTimeline { + + override fun pollHistory(): MatrixTimeline { return FakeMatrixTimeline() } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt index 2ff8ef1745..79d3acb58b 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt @@ -18,35 +18,17 @@ package io.element.android.libraries.matrix.test.room 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.TransactionId import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.api.poll.PollAnswer -import io.element.android.libraries.matrix.api.poll.PollKind import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.message.RoomMessage import io.element.android.libraries.matrix.api.roomlist.RoomSummary import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails -import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo -import io.element.android.libraries.matrix.api.timeline.item.event.EventContent -import io.element.android.libraries.matrix.api.timeline.item.event.EventReaction import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem -import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo -import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState -import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent -import io.element.android.libraries.matrix.api.timeline.item.event.MessageType -import io.element.android.libraries.matrix.api.timeline.item.event.PollContent -import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent -import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails -import io.element.android.libraries.matrix.api.timeline.item.event.Receipt -import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_NAME import io.element.android.libraries.matrix.test.A_USER_ID -import io.element.android.libraries.matrix.test.A_USER_NAME -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.persistentMapOf +import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem fun aRoomSummaryFilled( roomId: RoomId = A_ROOM_ID, @@ -101,95 +83,3 @@ fun aRoomMessage( sender = userId, originServerTs = timestamp, ) - -fun anEventTimelineItem( - eventId: EventId = AN_EVENT_ID, - transactionId: TransactionId? = null, - isEditable: Boolean = false, - isLocal: Boolean = false, - isOwn: Boolean = false, - isRemote: Boolean = false, - localSendState: LocalEventSendState? = null, - reactions: ImmutableList = persistentListOf(), - receipts: ImmutableList = persistentListOf(), - sender: UserId = A_USER_ID, - senderProfile: ProfileTimelineDetails = aProfileTimelineDetails(), - timestamp: Long = 0L, - content: EventContent = aProfileChangeMessageContent(), - debugInfo: TimelineItemDebugInfo = aTimelineItemDebugInfo(), -) = EventTimelineItem( - eventId = eventId, - transactionId = transactionId, - isEditable = isEditable, - isLocal = isLocal, - isOwn = isOwn, - isRemote = isRemote, - localSendState = localSendState, - reactions = reactions, - receipts = receipts, - sender = sender, - senderProfile = senderProfile, - timestamp = timestamp, - content = content, - debugInfo = debugInfo, - origin = null, -) - -fun aProfileTimelineDetails( - displayName: String? = A_USER_NAME, - displayNameAmbiguous: Boolean = false, - avatarUrl: String? = null -): ProfileTimelineDetails = ProfileTimelineDetails.Ready( - displayName = displayName, - displayNameAmbiguous = displayNameAmbiguous, - avatarUrl = avatarUrl, -) - -fun aProfileChangeMessageContent( - displayName: String? = null, - prevDisplayName: String? = null, - avatarUrl: String? = null, - prevAvatarUrl: String? = null, -) = ProfileChangeContent( - displayName = displayName, - prevDisplayName = prevDisplayName, - avatarUrl = avatarUrl, - prevAvatarUrl = prevAvatarUrl, -) - -fun aMessageContent( - body: String = "body", - inReplyTo: InReplyTo? = null, - isEdited: Boolean = false, - isThreaded: Boolean = false, - messageType: MessageType = TextMessageType( - body = body, - formatted = null - ) -) = MessageContent( - body = body, - inReplyTo = inReplyTo, - isEdited = isEdited, - isThreaded = isThreaded, - type = messageType -) - -fun aTimelineItemDebugInfo( - model: String = "Rust(Model())", - originalJson: String? = null, - latestEditedJson: String? = null, -) = TimelineItemDebugInfo( - model, originalJson, latestEditedJson -) - -fun aPollContent( - question: String = "Do you like polls?", - answers: ImmutableList = persistentListOf(PollAnswer("1", "Yes"), PollAnswer("2", "No")), -) = PollContent( - question = question, - kind = PollKind.Disclosed, - maxSelections = 1u, - answers = answers, - votes = persistentMapOf(), - endTime = null -) diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/TimelineFixture.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/TimelineFixture.kt new file mode 100644 index 0000000000..606aade962 --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/TimelineFixture.kt @@ -0,0 +1,139 @@ +/* + * 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.TransactionId +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.poll.PollAnswer +import io.element.android.libraries.matrix.api.poll.PollKind +import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo +import io.element.android.libraries.matrix.api.timeline.item.event.EventContent +import io.element.android.libraries.matrix.api.timeline.item.event.EventReaction +import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem +import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo +import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState +import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent +import io.element.android.libraries.matrix.api.timeline.item.event.MessageType +import io.element.android.libraries.matrix.api.timeline.item.event.PollContent +import io.element.android.libraries.matrix.api.timeline.item.event.ProfileChangeContent +import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails +import io.element.android.libraries.matrix.api.timeline.item.event.Receipt +import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType +import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.matrix.test.A_USER_NAME +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableMap +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.persistentMapOf + +fun anEventTimelineItem( + eventId: EventId = AN_EVENT_ID, + transactionId: TransactionId? = null, + isEditable: Boolean = false, + isLocal: Boolean = false, + isOwn: Boolean = false, + isRemote: Boolean = false, + localSendState: LocalEventSendState? = null, + reactions: ImmutableList = persistentListOf(), + receipts: ImmutableList = persistentListOf(), + sender: UserId = A_USER_ID, + senderProfile: ProfileTimelineDetails = aProfileTimelineDetails(), + timestamp: Long = 0L, + content: EventContent = aProfileChangeMessageContent(), + debugInfo: TimelineItemDebugInfo = aTimelineItemDebugInfo(), +) = EventTimelineItem( + eventId = eventId, + transactionId = transactionId, + isEditable = isEditable, + isLocal = isLocal, + isOwn = isOwn, + isRemote = isRemote, + localSendState = localSendState, + reactions = reactions, + receipts = receipts, + sender = sender, + senderProfile = senderProfile, + timestamp = timestamp, + content = content, + debugInfo = debugInfo, + origin = null, +) + +fun aProfileTimelineDetails( + displayName: String? = A_USER_NAME, + displayNameAmbiguous: Boolean = false, + avatarUrl: String? = null +): ProfileTimelineDetails = ProfileTimelineDetails.Ready( + displayName = displayName, + displayNameAmbiguous = displayNameAmbiguous, + avatarUrl = avatarUrl, +) + +fun aProfileChangeMessageContent( + displayName: String? = null, + prevDisplayName: String? = null, + avatarUrl: String? = null, + prevAvatarUrl: String? = null, +) = ProfileChangeContent( + displayName = displayName, + prevDisplayName = prevDisplayName, + avatarUrl = avatarUrl, + prevAvatarUrl = prevAvatarUrl, +) + +fun aMessageContent( + body: String = "body", + inReplyTo: InReplyTo? = null, + isEdited: Boolean = false, + isThreaded: Boolean = false, + messageType: MessageType = TextMessageType( + body = body, + formatted = null + ) +) = MessageContent( + body = body, + inReplyTo = inReplyTo, + isEdited = isEdited, + isThreaded = isThreaded, + type = messageType +) + +fun aTimelineItemDebugInfo( + model: String = "Rust(Model())", + originalJson: String? = null, + latestEditedJson: String? = null, +) = TimelineItemDebugInfo( + model, originalJson, latestEditedJson +) + +fun aPollContent( + question: String = "Do you like polls?", + answers: ImmutableList = persistentListOf(PollAnswer("1", "Yes"), PollAnswer("2", "No")), + kind: PollKind = PollKind.Disclosed, + maxSelections: ULong = 1u, + votes: ImmutableMap> = persistentMapOf(), + endTime: ULong? = null, +) = PollContent( + question = question, + kind = kind, + maxSelections = maxSelections, + answers = answers, + votes = votes, + endTime = endTime, +)