mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 20:38:40 +00:00
Fix issues and add missing text.
This commit is contained in:
+2
@@ -32,6 +32,8 @@ class TimelineItemActionComparator : Comparator<TimelineItemAction> {
|
|||||||
TimelineItemAction.ViewSource,
|
TimelineItemAction.ViewSource,
|
||||||
TimelineItemAction.ReportContent,
|
TimelineItemAction.ReportContent,
|
||||||
TimelineItemAction.Redact,
|
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 {
|
override fun compare(o1: TimelineItemAction, o2: TimelineItemAction): Int {
|
||||||
|
|||||||
+3
-2
@@ -56,10 +56,11 @@ fun TimelineEventTimestampView(
|
|||||||
val isVerifiedUserSendFailure = event.localSendState is LocalEventSendState.Failed.VerifiedUser
|
val isVerifiedUserSendFailure = event.localSendState is LocalEventSendState.Failed.VerifiedUser
|
||||||
val onClickLabel = when {
|
val onClickLabel = when {
|
||||||
shield != null -> stringResource(CommonStrings.a11y_view_details)
|
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
|
else -> null
|
||||||
}
|
}
|
||||||
val clickableModifier = remember(shield, hasError) {
|
val clickableModifier = remember(event) {
|
||||||
when {
|
when {
|
||||||
shield != null -> {
|
shield != null -> {
|
||||||
Modifier.clickable(
|
Modifier.clickable(
|
||||||
|
|||||||
+4
-3
@@ -13,6 +13,7 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
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.components.dialogs.ConfirmationDialog
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
@@ -45,13 +46,13 @@ fun SendFailureDialogView(
|
|||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
fun SendFailureDialogViewPreview(
|
internal fun SendFailureDialogViewPreview(
|
||||||
@PreviewParameter(SendFailureDialogStateProvider::class) sendFailureDialogState: SendFailureDialogState,
|
@PreviewParameter(SendFailureDialogStateProvider::class) sendFailureDialogState: SendFailureDialogState,
|
||||||
) {
|
) = ElementPreview {
|
||||||
SendFailureDialogView(
|
SendFailureDialogView(
|
||||||
sendFailureDialogState = sendFailureDialogState,
|
sendFailureDialogState = sendFailureDialogState,
|
||||||
onDismiss = {},
|
onDismiss = {},
|
||||||
onRetry = {},
|
onRetry = {},
|
||||||
onRemoveMessage = {}
|
onRemoveMessage = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+52
@@ -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.Timeline
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
|
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.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.api.timeline.item.event.toEventOrTransactionId
|
||||||
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
||||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
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_THREAD_ID
|
||||||
import io.element.android.libraries.matrix.test.A_USER_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.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.core.aBuildMeta
|
||||||
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
||||||
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
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<Unit>> { 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<Unit>> { 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
|
@Test
|
||||||
fun `present - handle action view in timeline, it should have no effect`() = runTest {
|
fun `present - handle action view in timeline, it should have no effect`() = runTest {
|
||||||
val messageEvent = aMessageEvent(
|
val messageEvent = aMessageEvent(
|
||||||
|
|||||||
+67
@@ -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.components.receipt.bottomsheet.ReadReceiptBottomSheetEvent
|
||||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
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.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.features.roomcall.api.aStandByCallState
|
||||||
import io.element.android.libraries.emoji.impl.picker.DefaultEmojiPickerRenderer
|
import io.element.android.libraries.emoji.impl.picker.DefaultEmojiPickerRenderer
|
||||||
import io.element.android.libraries.emoji.impl.picker.anEmojiPickerState
|
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.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
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.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.getAvatarUrl
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.getDisplayName
|
import io.element.android.libraries.matrix.api.timeline.item.event.getDisplayName
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
@@ -558,6 +560,71 @@ class MessagesViewTest : RobolectricTest() {
|
|||||||
eventsRecorder.assertSingle(TimelineEvent.ComputeVerifiedUserSendFailure(timelineItem))
|
eventsRecorder.assertSingle(TimelineEvent.ComputeVerifiedUserSendFailure(timelineItem))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on retry in the send failure dialog emits the expected Events`() = runAndroidComposeUiTest {
|
||||||
|
val messagesEventsRecorder = EventsRecorder<MessagesEvent>()
|
||||||
|
val timelineEventsRecorder = EventsRecorder<TimelineEvent>()
|
||||||
|
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<MessagesEvent>()
|
||||||
|
val timelineEventsRecorder = EventsRecorder<TimelineEvent>()
|
||||||
|
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<MessagesEvent>(expectEvents = false)
|
||||||
|
val timelineEventsRecorder = EventsRecorder<TimelineEvent>()
|
||||||
|
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
|
@Test
|
||||||
fun `clicking on a custom emoji emits the expected Events`() = runAndroidComposeUiTest {
|
fun `clicking on a custom emoji emits the expected Events`() = runAndroidComposeUiTest {
|
||||||
val aUnicode = "🙈"
|
val aUnicode = "🙈"
|
||||||
|
|||||||
+109
-1
@@ -12,6 +12,8 @@ import app.cash.turbine.ReceiveTurbine
|
|||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.location.test.FakeActiveLiveLocationShareManager
|
import io.element.android.features.location.test.FakeActiveLiveLocationShareManager
|
||||||
import io.element.android.features.messages.impl.FakeMessagesNavigator
|
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.crypto.sendfailure.resolve.aResolveVerifiedUserSendFailureState
|
||||||
import io.element.android.features.messages.impl.fixtures.aMessageEvent
|
import io.element.android.features.messages.impl.fixtures.aMessageEvent
|
||||||
import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactoryCreator
|
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.model.TimelineItem
|
||||||
import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState
|
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.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.typing.aTypingNotificationState
|
||||||
import io.element.android.features.messages.impl.voicemessages.timeline.FakeRedactedVoiceMessageManager
|
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.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.ReceiptType
|
||||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
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.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.ReactionSender
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.Receipt
|
import io.element.android.libraries.matrix.api.timeline.item.event.Receipt
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent
|
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<ResolveVerifiedUserSendFailureEvent, Unit> { }
|
||||||
|
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
|
@Test
|
||||||
fun `present - when room member info is loaded, read receipts info should be updated`() = runTest {
|
fun `present - when room member info is loaded, read receipts info should be updated`() = runTest {
|
||||||
val timeline = FakeTimeline(
|
val timeline = FakeTimeline(
|
||||||
@@ -1589,6 +1696,7 @@ class TimelinePresenterTest {
|
|||||||
liveLocationShareManager: FakeActiveLiveLocationShareManager = FakeActiveLiveLocationShareManager(),
|
liveLocationShareManager: FakeActiveLiveLocationShareManager = FakeActiveLiveLocationShareManager(),
|
||||||
markAsFullyRead: MarkAsFullyRead = FakeMarkAsFullyRead { _, _ -> },
|
markAsFullyRead: MarkAsFullyRead = FakeMarkAsFullyRead { _, _ -> },
|
||||||
timelineProtectionPresenter: Presenter<TimelineProtectionState> = { aTimelineProtectionState() },
|
timelineProtectionPresenter: Presenter<TimelineProtectionState> = { aTimelineProtectionState() },
|
||||||
|
resolveVerifiedUserSendFailurePresenter: Presenter<ResolveVerifiedUserSendFailureState> = { aResolveVerifiedUserSendFailureState() },
|
||||||
): TimelinePresenter {
|
): TimelinePresenter {
|
||||||
return TimelinePresenter(
|
return TimelinePresenter(
|
||||||
timelineItemsFactoryCreator = aTimelineItemsFactoryCreator(),
|
timelineItemsFactoryCreator = aTimelineItemsFactoryCreator(),
|
||||||
@@ -1602,7 +1710,7 @@ class TimelinePresenterTest {
|
|||||||
sessionPreferencesStore = sessionPreferencesStore,
|
sessionPreferencesStore = sessionPreferencesStore,
|
||||||
timelineItemIndexer = timelineItemIndexer,
|
timelineItemIndexer = timelineItemIndexer,
|
||||||
timelineController = TimelineController(room, timeline),
|
timelineController = TimelineController(room, timeline),
|
||||||
resolveVerifiedUserSendFailurePresenter = { aResolveVerifiedUserSendFailureState() },
|
resolveVerifiedUserSendFailurePresenter = resolveVerifiedUserSendFailurePresenter,
|
||||||
typingNotificationPresenter = { aTypingNotificationState() },
|
typingNotificationPresenter = { aTypingNotificationState() },
|
||||||
roomCallStatePresenter = { aStandByCallState() },
|
roomCallStatePresenter = { aStandByCallState() },
|
||||||
featureFlagService = featureFlagService,
|
featureFlagService = featureFlagService,
|
||||||
|
|||||||
+41
@@ -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.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.UniqueId
|
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.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.MessageContent
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
|
import io.element.android.libraries.matrix.api.timeline.item.event.MessageShield
|
||||||
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
|
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
|
||||||
@@ -178,6 +179,46 @@ class TimelineViewTest : RobolectricTest() {
|
|||||||
eventsRecorder.assertSingle(TimelineEvent.HideShieldDialog)
|
eventsRecorder.assertSingle(TimelineEvent.HideShieldDialog)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on the send failure indicator emits the expected Event`() = runAndroidComposeUiTest {
|
||||||
|
val eventsRecorder = EventsRecorder<TimelineEvent>()
|
||||||
|
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<TimelineEvent>()
|
||||||
|
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
|
@Test
|
||||||
fun `clicking on a reply to a loaded event emits the expected Event`() = runAndroidComposeUiTest {
|
fun `clicking on a reply to a loaded event emits the expected Event`() = runAndroidComposeUiTest {
|
||||||
val eventsRecorder = EventsRecorder<TimelineEvent>()
|
val eventsRecorder = EventsRecorder<TimelineEvent>()
|
||||||
|
|||||||
+105
@@ -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<ComponentActivity>.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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user