From 3f2a1d95fcef783a7d03805d25b121fb665ef68d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 20 Aug 2026 19:41:02 +0200 Subject: [PATCH] Fix issues and add missing text. --- .../model/TimelineItemActionComparator.kt | 2 + .../components/TimelineEventTimestampView.kt | 5 +- .../sendfailure/SendFailureDialogView.kt | 7 +- .../messages/impl/MessagesPresenterTest.kt | 52 +++++++++ .../messages/impl/MessagesViewTest.kt | 67 +++++++++++ .../impl/timeline/TimelinePresenterTest.kt | 110 +++++++++++++++++- .../impl/timeline/TimelineViewTest.kt | 41 +++++++ .../sendfailure/SendFailureDialogViewTest.kt | 105 +++++++++++++++++ 8 files changed, 383 insertions(+), 6 deletions(-) create mode 100644 features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/sendfailure/SendFailureDialogViewTest.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemActionComparator.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemActionComparator.kt index 0a0d9b17af..2f6fa4e4e0 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemActionComparator.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemActionComparator.kt @@ -32,6 +32,8 @@ class TimelineItemActionComparator : Comparator { TimelineItemAction.ViewSource, TimelineItemAction.ReportContent, TimelineItemAction.Redact, + // Not rendered in the action list, but has to be listed here for exhaustiveness. + TimelineItemAction.RetrySending, ) override fun compare(o1: TimelineItemAction, o2: TimelineItemAction): Int { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt index c82be7b10a..f124d45d99 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt @@ -56,10 +56,11 @@ fun TimelineEventTimestampView( val isVerifiedUserSendFailure = event.localSendState is LocalEventSendState.Failed.VerifiedUser val onClickLabel = when { shield != null -> stringResource(CommonStrings.a11y_view_details) - hasError && isVerifiedUserSendFailure -> stringResource(CommonStrings.action_open_context_menu) + isVerifiedUserSendFailure -> stringResource(CommonStrings.action_open_context_menu) + hasError -> stringResource(CommonStrings.a11y_view_details) else -> null } - val clickableModifier = remember(shield, hasError) { + val clickableModifier = remember(event) { when { shield != null -> { Modifier.clickable( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/sendfailure/SendFailureDialogView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/sendfailure/SendFailureDialogView.kt index 0054880a76..ecab806857 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/sendfailure/SendFailureDialogView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/sendfailure/SendFailureDialogView.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog +import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.ui.strings.CommonStrings @@ -45,13 +46,13 @@ fun SendFailureDialogView( @PreviewsDayNight @Composable -fun SendFailureDialogViewPreview( +internal fun SendFailureDialogViewPreview( @PreviewParameter(SendFailureDialogStateProvider::class) sendFailureDialogState: SendFailureDialogState, -) { +) = ElementPreview { SendFailureDialogView( sendFailureDialogState = sendFailureDialogState, onDismiss = {}, onRetry = {}, - onRemoveMessage = {} + onRemoveMessage = {}, ) } 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 0a0d310b16..227088f78c 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 @@ -72,6 +72,7 @@ import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId +import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId import io.element.android.libraries.matrix.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.AN_EVENT_ID @@ -83,6 +84,7 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID_2 import io.element.android.libraries.matrix.test.A_THREAD_ID import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID_2 +import io.element.android.libraries.matrix.test.core.FakeSendHandle import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser @@ -1043,6 +1045,56 @@ class MessagesPresenterTest { } } + @Test + fun `present - handle action retry sending`() = runTest { + val retryLambda = lambdaRecorder> { Result.success(Unit) } + val messageEvent = aMessageEvent( + sendState = LocalEventSendState.Failed.Unknown("Error"), + sendHandleProvider = { FakeSendHandle(retryLambda = retryLambda) }, + ) + val presenter = createMessagesPresenter() + presenter.testWithLifecycleOwner { + skipItems(1) + val initialState = awaitItem() + initialState.eventSink(MessagesEvent.HandleAction(TimelineItemAction.RetrySending, messageEvent)) + advanceUntilIdle() + retryLambda.assertions().isCalledOnce() + } + } + + @Test + fun `present - handle action retry sending - failure is ignored`() = runTest { + val retryLambda = lambdaRecorder> { Result.failure(AN_EXCEPTION) } + val messageEvent = aMessageEvent( + sendState = LocalEventSendState.Failed.Unknown("Error"), + sendHandleProvider = { FakeSendHandle(retryLambda = retryLambda) }, + ) + val presenter = createMessagesPresenter() + presenter.testWithLifecycleOwner { + skipItems(1) + val initialState = awaitItem() + initialState.eventSink(MessagesEvent.HandleAction(TimelineItemAction.RetrySending, messageEvent)) + advanceUntilIdle() + retryLambda.assertions().isCalledOnce() + } + } + + @Test + fun `present - handle action retry sending - no send handle, it should have no effect`() = runTest { + val messageEvent = aMessageEvent( + sendState = LocalEventSendState.Failed.Unknown("Error"), + sendHandleProvider = { null }, + ) + val presenter = createMessagesPresenter() + presenter.testWithLifecycleOwner { + skipItems(1) + val initialState = awaitItem() + initialState.eventSink(MessagesEvent.HandleAction(TimelineItemAction.RetrySending, messageEvent)) + advanceUntilIdle() + // No op! + } + } + @Test fun `present - handle action view in timeline, it should have no effect`() = runTest { val messageEvent = aMessageEvent( diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt index 1350f1dfa0..2a6353de87 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt @@ -55,12 +55,14 @@ import io.element.android.features.messages.impl.timeline.components.receipt.aRe import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetEvent import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent +import io.element.android.features.messages.impl.timeline.sendfailure.SendFailureDialogState import io.element.android.features.roomcall.api.aStandByCallState import io.element.android.libraries.emoji.impl.picker.DefaultEmojiPickerRenderer import io.element.android.libraries.emoji.impl.picker.anEmojiPickerState import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.tombstone.SuccessorRoom +import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState import io.element.android.libraries.matrix.api.timeline.item.event.getAvatarUrl import io.element.android.libraries.matrix.api.timeline.item.event.getDisplayName import io.element.android.libraries.matrix.api.user.MatrixUser @@ -558,6 +560,71 @@ class MessagesViewTest : RobolectricTest() { eventsRecorder.assertSingle(TimelineEvent.ComputeVerifiedUserSendFailure(timelineItem)) } + @Test + fun `clicking on retry in the send failure dialog emits the expected Events`() = runAndroidComposeUiTest { + val messagesEventsRecorder = EventsRecorder() + val timelineEventsRecorder = EventsRecorder() + val timelineItem = aTimelineItemEvent(sendState = LocalEventSendState.Failed.Unknown("Error")) + setMessagesView( + state = aMessagesState( + timelineState = aTimelineState( + sendFailureDialogState = SendFailureDialogState.Show(event = timelineItem, message = "Error"), + eventSink = timelineEventsRecorder, + ), + eventSink = messagesEventsRecorder, + ), + ) + // Clear initial 'LoadMore' event emitted when setting the state + timelineEventsRecorder.clear() + + clickOn(CommonStrings.action_retry) + messagesEventsRecorder.assertSingle(MessagesEvent.HandleAction(TimelineItemAction.RetrySending, timelineItem)) + timelineEventsRecorder.assertSingle(TimelineEvent.HideSendFailureDialog) + } + + @Test + fun `clicking on remove message in the send failure dialog emits the expected Events`() = runAndroidComposeUiTest { + val messagesEventsRecorder = EventsRecorder() + val timelineEventsRecorder = EventsRecorder() + val timelineItem = aTimelineItemEvent(sendState = LocalEventSendState.Failed.Unknown("Error")) + setMessagesView( + state = aMessagesState( + timelineState = aTimelineState( + sendFailureDialogState = SendFailureDialogState.Show(event = timelineItem, message = "Error"), + eventSink = timelineEventsRecorder, + ), + eventSink = messagesEventsRecorder, + ), + ) + // Clear initial 'LoadMore' event emitted when setting the state + timelineEventsRecorder.clear() + + clickOn(CommonStrings.action_remove_message) + messagesEventsRecorder.assertSingle(MessagesEvent.HandleAction(TimelineItemAction.Redact, timelineItem)) + timelineEventsRecorder.assertSingle(TimelineEvent.HideSendFailureDialog) + } + + @Test + fun `cancelling the send failure dialog emits the expected Event`() = runAndroidComposeUiTest { + val messagesEventsRecorder = EventsRecorder(expectEvents = false) + val timelineEventsRecorder = EventsRecorder() + val timelineItem = aTimelineItemEvent(sendState = LocalEventSendState.Failed.Unknown("Error")) + setMessagesView( + state = aMessagesState( + timelineState = aTimelineState( + sendFailureDialogState = SendFailureDialogState.Show(event = timelineItem, message = "Error"), + eventSink = timelineEventsRecorder, + ), + eventSink = messagesEventsRecorder, + ), + ) + // Clear initial 'LoadMore' event emitted when setting the state + timelineEventsRecorder.clear() + + clickOn(CommonStrings.action_cancel) + timelineEventsRecorder.assertSingle(TimelineEvent.HideSendFailureDialog) + } + @Test fun `clicking on a custom emoji emits the expected Events`() = runAndroidComposeUiTest { val aUnicode = "🙈" 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 3b0794305f..3e4e8bc0ae 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 @@ -12,6 +12,8 @@ import app.cash.turbine.ReceiveTurbine import com.google.common.truth.Truth.assertThat import io.element.android.features.location.test.FakeActiveLiveLocationShareManager import io.element.android.features.messages.impl.FakeMessagesNavigator +import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailureEvent +import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailureState import io.element.android.features.messages.impl.crypto.sendfailure.resolve.aResolveVerifiedUserSendFailureState import io.element.android.features.messages.impl.fixtures.aMessageEvent import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactoryCreator @@ -21,6 +23,7 @@ import io.element.android.features.messages.impl.timeline.model.NewEventState import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState import io.element.android.features.messages.impl.timeline.protection.aTimelineProtectionState +import io.element.android.features.messages.impl.timeline.sendfailure.SendFailureDialogState import io.element.android.features.messages.impl.typing.aTypingNotificationState import io.element.android.features.messages.impl.voicemessages.timeline.FakeRedactedVoiceMessageManager import io.element.android.features.messages.impl.voicemessages.timeline.RedactedVoiceMessageManager @@ -45,6 +48,7 @@ import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.ReceiptType import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.item.event.EventReaction +import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState import io.element.android.libraries.matrix.api.timeline.item.event.ReactionSender import io.element.android.libraries.matrix.api.timeline.item.event.Receipt import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent @@ -1390,6 +1394,109 @@ class TimelinePresenterTest { } } + @Test + fun `present - show and hide send failure dialog`() = runTest { + val presenter = createTimelinePresenter() + val event = aMessageEvent(sendState = LocalEventSendState.Failed.Unknown("An error")) + presenter.test { + val initialState = awaitFirstItem() + assertThat(initialState.sendFailureDialogState).isEqualTo(SendFailureDialogState.Hidden) + initialState.eventSink(TimelineEvent.ShowSendFailureDialog(event)) + awaitItem().also { state -> + assertThat(state.sendFailureDialogState).isEqualTo( + SendFailureDialogState.Show(event = event, message = "An error") + ) + state.eventSink(TimelineEvent.HideSendFailureDialog) + } + awaitItem().also { state -> + assertThat(state.sendFailureDialogState).isEqualTo(SendFailureDialogState.Hidden) + } + } + } + + @Test + fun `present - show send failure dialog - unknown error with a blank reason`() { + assertSendFailureDialogMessage( + sendState = LocalEventSendState.Failed.Unknown(error = ""), + expectedMessage = "Unknown error", + ) + } + + @Test + fun `present - show send failure dialog - invalid mime type`() { + assertSendFailureDialogMessage( + sendState = LocalEventSendState.Failed.InvalidMimeType(mimeType = "invalid/mimeType"), + expectedMessage = "Invalid mime type: invalid/mimeType", + ) + } + + @Test + fun `present - show send failure dialog - missing media content`() { + assertSendFailureDialogMessage( + sendState = LocalEventSendState.Failed.MissingMediaContent, + expectedMessage = "Missing media content", + ) + } + + @Test + fun `present - show send failure dialog - sending from unverified device`() { + assertSendFailureDialogMessage( + sendState = LocalEventSendState.Failed.SendingFromUnverifiedDevice, + expectedMessage = "Sending from unverified device", + ) + } + + private fun assertSendFailureDialogMessage( + sendState: LocalEventSendState.Failed, + expectedMessage: String, + ) = runTest { + val presenter = createTimelinePresenter() + val event = aMessageEvent(sendState = sendState) + presenter.test { + val initialState = awaitFirstItem() + initialState.eventSink(TimelineEvent.ShowSendFailureDialog(event)) + assertThat(awaitItem().sendFailureDialogState).isEqualTo( + SendFailureDialogState.Show(event = event, message = expectedMessage) + ) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `present - show send failure dialog - verified user failure is delegated to the resolve presenter`() = runTest { + val resolveEventSinkRecorder = lambdaRecorder { } + val presenter = createTimelinePresenter( + resolveVerifiedUserSendFailurePresenter = { + aResolveVerifiedUserSendFailureState(eventSink = resolveEventSinkRecorder) + }, + ) + val event = aMessageEvent( + sendState = LocalEventSendState.Failed.VerifiedUserChangedIdentity(users = listOf(A_USER_ID)), + ) + presenter.test { + val initialState = awaitFirstItem() + initialState.eventSink(TimelineEvent.ShowSendFailureDialog(event)) + // The generic dialog is not displayed, the dedicated one is. + assertThat(consumeItemsUntilTimeout()).isEmpty() + assertThat(initialState.sendFailureDialogState).isEqualTo(SendFailureDialogState.Hidden) + resolveEventSinkRecorder.assertions() + .isCalledOnce() + .with(value(ResolveVerifiedUserSendFailureEvent.ComputeForMessage(event))) + } + } + + @Test + fun `present - show send failure dialog - no effect if the event did not fail to send`() = runTest { + val presenter = createTimelinePresenter() + val event = aMessageEvent(sendState = LocalEventSendState.Sending.Event) + presenter.test { + val initialState = awaitFirstItem() + initialState.eventSink(TimelineEvent.ShowSendFailureDialog(event)) + assertThat(consumeItemsUntilTimeout()).isEmpty() + assertThat(initialState.sendFailureDialogState).isEqualTo(SendFailureDialogState.Hidden) + } + } + @Test fun `present - when room member info is loaded, read receipts info should be updated`() = runTest { val timeline = FakeTimeline( @@ -1589,6 +1696,7 @@ class TimelinePresenterTest { liveLocationShareManager: FakeActiveLiveLocationShareManager = FakeActiveLiveLocationShareManager(), markAsFullyRead: MarkAsFullyRead = FakeMarkAsFullyRead { _, _ -> }, timelineProtectionPresenter: Presenter = { aTimelineProtectionState() }, + resolveVerifiedUserSendFailurePresenter: Presenter = { aResolveVerifiedUserSendFailureState() }, ): TimelinePresenter { return TimelinePresenter( timelineItemsFactoryCreator = aTimelineItemsFactoryCreator(), @@ -1602,7 +1710,7 @@ class TimelinePresenterTest { sessionPreferencesStore = sessionPreferencesStore, timelineItemIndexer = timelineItemIndexer, timelineController = TimelineController(room, timeline), - resolveVerifiedUserSendFailurePresenter = { aResolveVerifiedUserSendFailureState() }, + resolveVerifiedUserSendFailurePresenter = resolveVerifiedUserSendFailurePresenter, typingNotificationPresenter = { aTypingNotificationState() }, roomCallStatePresenter = { aStandByCallState() }, featureFlagService = featureFlagService, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt index f733f116d6..ac84d48c95 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelineViewTest.kt @@ -33,6 +33,7 @@ import io.element.android.features.messages.impl.timeline.protection.aTimelinePr import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UniqueId import io.element.android.libraries.matrix.api.timeline.Timeline +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.MessageShield import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType @@ -178,6 +179,46 @@ class TimelineViewTest : RobolectricTest() { eventsRecorder.assertSingle(TimelineEvent.HideShieldDialog) } + @Test + fun `clicking on the send failure indicator emits the expected Event`() = runAndroidComposeUiTest { + val eventsRecorder = EventsRecorder() + val timelineItem = aTimelineItemEvent( + content = aTimelineItemTextContent(), + sendState = LocalEventSendState.Failed.Unknown("Error"), + ) + setTimelineView( + state = aTimelineState( + timelineItems = persistentListOf(timelineItem), + eventSink = eventsRecorder, + ), + ) + eventsRecorder.clear() + + val contentDescription = activity!!.getString(CommonStrings.common_sending_failed) + onNodeWithContentDescription(contentDescription).performClick() + eventsRecorder.assertSingle(TimelineEvent.ShowSendFailureDialog(timelineItem)) + } + + @Test + fun `clicking on the send failure indicator of a verified user failure emits the expected Event`() = runAndroidComposeUiTest { + val eventsRecorder = EventsRecorder() + val timelineItem = aTimelineItemEvent( + content = aTimelineItemTextContent(), + sendState = LocalEventSendState.Failed.VerifiedUserChangedIdentity(users = listOf(A_USER_ID)), + ) + setTimelineView( + state = aTimelineState( + timelineItems = persistentListOf(timelineItem), + eventSink = eventsRecorder, + ), + ) + eventsRecorder.clear() + + val contentDescription = activity!!.getString(CommonStrings.common_sending_failed) + onNodeWithContentDescription(contentDescription).performClick() + eventsRecorder.assertSingle(TimelineEvent.ShowSendFailureDialog(timelineItem)) + } + @Test fun `clicking on a reply to a loaded event emits the expected Event`() = runAndroidComposeUiTest { val eventsRecorder = EventsRecorder() diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/sendfailure/SendFailureDialogViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/sendfailure/SendFailureDialogViewTest.kt new file mode 100644 index 0000000000..204d367b0f --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/sendfailure/SendFailureDialogViewTest.kt @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +@file:OptIn(ExperimentalTestApi::class) + +package io.element.android.features.messages.impl.timeline.sendfailure + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.v2.runAndroidComposeUiTest +import io.element.android.features.messages.impl.timeline.aTimelineItemEvent +import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.tests.testutils.EnsureNeverCalled +import io.element.android.tests.testutils.EnsureNeverCalledWithParam +import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.ensureCalledOnce +import io.element.android.tests.testutils.ensureCalledOnceWithParam +import io.element.android.tests.testutils.robolectric.RobolectricTest +import io.element.android.tests.testutils.setSafeContent +import org.junit.Test + +class SendFailureDialogViewTest : RobolectricTest() { + @Test + fun `hidden state renders nothing`() = runAndroidComposeUiTest { + setSendFailureDialogView( + sendFailureDialogState = SendFailureDialogState.Hidden, + ) + onNodeWithText(activity!!.getString(CommonStrings.common_sending_failed)).assertDoesNotExist() + } + + @Test + fun `show state renders the failure reason`() = runAndroidComposeUiTest { + setSendFailureDialogView( + sendFailureDialogState = aSendFailureDialogStateShow(message = "A failure reason"), + ) + onNodeWithText(activity!!.getString(CommonStrings.common_sending_failed)).assertExists() + onNodeWithText("A failure reason").assertExists() + } + + @Test + fun `clicking on retry invokes the expected callback`() = runAndroidComposeUiTest { + val state = aSendFailureDialogStateShow() + ensureCalledOnceWithParam(state.event) { callback -> + setSendFailureDialogView( + sendFailureDialogState = state, + onRetry = callback, + ) + clickOn(CommonStrings.action_retry) + } + } + + @Test + fun `clicking on remove message invokes the expected callback`() = runAndroidComposeUiTest { + val state = aSendFailureDialogStateShow() + ensureCalledOnceWithParam(state.event) { callback -> + setSendFailureDialogView( + sendFailureDialogState = state, + onRemoveMessage = callback, + ) + clickOn(CommonStrings.action_remove_message) + } + } + + @Test + fun `clicking on cancel invokes the expected callback`() = runAndroidComposeUiTest { + ensureCalledOnce { callback -> + setSendFailureDialogView( + sendFailureDialogState = aSendFailureDialogStateShow(), + onDismiss = callback, + ) + clickOn(CommonStrings.action_cancel) + } + } + + private fun aSendFailureDialogStateShow( + message: String = "A failure reason", + ) = SendFailureDialogState.Show( + event = aTimelineItemEvent(sendState = LocalEventSendState.Failed.Unknown(message)), + message = message, + ) + + private fun AndroidComposeUiTest.setSendFailureDialogView( + sendFailureDialogState: SendFailureDialogState, + onDismiss: () -> Unit = EnsureNeverCalled(), + onRetry: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(), + onRemoveMessage: (TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(), + ) { + setSafeContent { + SendFailureDialogView( + sendFailureDialogState = sendFailureDialogState, + onDismiss = onDismiss, + onRetry = onRetry, + onRemoveMessage = onRemoveMessage, + ) + } + } +}