Merge branch 'develop' into feature/bma/removeExternalCallSupport

This commit is contained in:
Benoit Marty 2026-04-30 16:58:11 +02:00
commit e21276f323
122 changed files with 2266 additions and 2352 deletions

View file

@ -6,13 +6,15 @@
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.messages.impl
import androidx.activity.ComponentActivity
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.longClick
import androidx.compose.ui.test.onAllNodesWithContentDescription
import androidx.compose.ui.test.onAllNodesWithTag
@ -25,6 +27,7 @@ import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.test.swipeRight
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.compose.ui.text.AnnotatedString
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.emojibasebindings.Emoji
@ -78,82 +81,78 @@ import io.element.android.tests.testutils.pressBack
import io.element.android.tests.testutils.setSafeContent
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
import kotlin.time.Duration.Companion.milliseconds
@RunWith(AndroidJUnit4::class)
class MessagesViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test
fun `clicking on back invoke expected callback`() {
fun `clicking on back invoke expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<MessagesEvent>(expectEvents = false)
val state = aMessagesState(
eventSink = eventsRecorder
)
ensureCalledOnce { callback ->
rule.setMessagesView(
setMessagesView(
state = state,
onBackClick = callback,
)
rule.pressBack()
pressBack()
}
}
@Test
fun `clicking on room name invoke expected callback`() {
fun `clicking on room name invoke expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<MessagesEvent>(expectEvents = false)
val state = aMessagesState(
eventSink = eventsRecorder
)
ensureCalledOnce { callback ->
rule.setMessagesView(
setMessagesView(
state = state,
onRoomDetailsClick = callback,
)
rule.onNodeWithText(state.roomName.orEmpty(), useUnmergedTree = true).performClick()
onNodeWithText(state.roomName.orEmpty(), useUnmergedTree = true).performClick()
}
}
@Test
fun `clicking on join call invoke expected callback`() {
fun `clicking on join call invoke expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<MessagesEvent>(expectEvents = false)
val state = aMessagesState(
eventSink = eventsRecorder
)
ensureCalledOnceWithParam(false) { callback ->
rule.setMessagesView(
setMessagesView(
state = state,
onJoinCallClick = callback,
)
val joinCallContentDescription = rule.activity.getString(CommonStrings.a11y_start_call)
rule.onNodeWithContentDescription(joinCallContentDescription).performClick()
val joinCallContentDescription = activity!!.getString(CommonStrings.a11y_start_call)
onNodeWithContentDescription(joinCallContentDescription).performClick()
}
}
@Test
fun `clicking on join voice call invoke expected callback`() {
fun `clicking on join voice call invoke expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<MessagesEvent>(expectEvents = false)
val state = aMessagesState(
eventSink = eventsRecorder,
roomCallState = aStandByCallState(isDM = true)
)
ensureCalledOnceWithParam(true) { callback ->
rule.setMessagesView(
setMessagesView(
state = state,
onJoinCallClick = callback,
)
val joinVoiceCallContentDescription = rule.activity.getString(CommonStrings.a11y_start_voice_call)
rule.onNodeWithContentDescription(joinVoiceCallContentDescription).performClick()
val joinVoiceCallContentDescription = activity!!.getString(CommonStrings.a11y_start_voice_call)
onNodeWithContentDescription(joinVoiceCallContentDescription).performClick()
}
}
@Test
fun `clicking on an Event invoke expected callback`() {
fun `clicking on an Event invoke expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<MessagesEvent>(expectEvents = false)
val state = aMessagesState(
timelineState = aTimelineState(
@ -167,12 +166,12 @@ class MessagesViewTest {
expectedParam2 = timelineItem,
result = true,
)
rule.setMessagesView(
setMessagesView(
state = state,
onEventClick = callback,
)
// Cannot perform click on "Text", it's not detected. Use tag instead
rule.onAllNodesWithTag(TestTags.messageBubble.value).onFirst().performClick()
onAllNodesWithTag(TestTags.messageBubble.value).onFirst().performClick()
callback.assertSuccess()
}
@ -202,7 +201,7 @@ class MessagesViewTest {
userHasPermissionToRedactOther: Boolean = false,
userHasPermissionToSendReaction: Boolean = false,
userCanPinEvent: Boolean = false,
) {
) = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ActionListEvent>()
val state = aMessagesState(
actionListState = anActionListState(
@ -220,11 +219,11 @@ class MessagesViewTest {
),
)
val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event
rule.setMessagesView(
setMessagesView(
state = state,
)
// Cannot perform click on "Text", it's not detected. Use tag instead
rule.onAllNodesWithTag(TestTags.messageBubble.value).onFirst().performTouchInput { longClick() }
onAllNodesWithTag(TestTags.messageBubble.value).onFirst().performTouchInput { longClick() }
eventsRecorder.assertSingle(
ActionListEvent.ComputeForMessage(
event = timelineItem,
@ -235,7 +234,7 @@ class MessagesViewTest {
@Test
@Config(qualifiers = "h1024dp")
fun `clicking on a read receipt list emits the expected Event`() {
fun `clicking on a read receipt list emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ReadReceiptBottomSheetEvent>()
val state = aMessagesState(
timelineState = aTimelineState(
@ -255,10 +254,10 @@ class MessagesViewTest {
),
)
val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event
rule.setMessagesView(
setMessagesView(
state = state,
)
rule.onNodeWithTag(TestTags.messageReadReceipts.value, useUnmergedTree = true).performClick()
onNodeWithTag(TestTags.messageReadReceipts.value, useUnmergedTree = true).performClick()
eventsRecorder.assertSingle(ReadReceiptBottomSheetEvent.EventSelected(timelineItem))
}
@ -272,7 +271,7 @@ class MessagesViewTest {
swipeTest(userHasPermissionToSendMessage = false)
}
private fun swipeTest(userHasPermissionToSendMessage: Boolean) {
private fun swipeTest(userHasPermissionToSendMessage: Boolean) = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<MessagesEvent>()
val canBeRepliedEvent = aTimelineItemEvent(canBeRepliedTo = true)
val cannotBeRepliedEvent = aTimelineItemEvent(canBeRepliedTo = false)
@ -285,10 +284,10 @@ class MessagesViewTest {
),
eventSink = eventsRecorder,
)
rule.setMessagesView(
setMessagesView(
state = state,
)
rule.onAllNodesWithTag(TestTags.messageBubble.value).apply {
onAllNodesWithTag(TestTags.messageBubble.value).apply {
onFirst().performTouchInput { swipeRight(endX = 200f) }
onLast().performTouchInput { swipeRight(endX = 200f) }
}
@ -300,7 +299,7 @@ class MessagesViewTest {
}
@Test
fun `clicking on send location invoke expected callback`() {
fun `clicking on send location invoke expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<MessagesEvent>(expectEvents = false)
val state = aMessagesState(
composerState = aMessageComposerState(
@ -309,16 +308,16 @@ class MessagesViewTest {
eventSink = eventsRecorder
)
ensureCalledOnce { callback ->
rule.setMessagesView(
setMessagesView(
state = state,
onSendLocationClick = callback,
)
rule.clickOn(R.string.screen_room_attachment_source_location)
clickOn(R.string.screen_room_attachment_source_location)
}
}
@Test
fun `clicking on create poll invoke expected callback`() {
fun `clicking on create poll invoke expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<MessagesEvent>(expectEvents = false)
val state = aMessagesState(
composerState = aMessageComposerState(
@ -327,25 +326,25 @@ class MessagesViewTest {
eventSink = eventsRecorder
)
ensureCalledOnce { callback ->
rule.setMessagesView(
setMessagesView(
state = state,
onCreatePollClick = callback,
)
// Then click on the poll action
rule.clickOn(R.string.screen_room_attachment_source_poll)
clickOn(R.string.screen_room_attachment_source_poll)
}
}
@Test
@Config(qualifiers = "h1024dp")
fun `clicking on the avatar of the sender of an Event emits the expected event`() {
fun `clicking on the avatar of the sender of an Event emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<MessagesEvent>()
val state = aMessagesState(
eventSink = eventsRecorder
)
val timelineEvent = state.timelineState.timelineItems.filterIsInstance<TimelineItem.Event>().first()
rule.setMessagesView(state = state)
rule.onNodeWithTag(TestTags.timelineItemSenderAvatar.value, useUnmergedTree = true).performClick()
setMessagesView(state = state)
onNodeWithTag(TestTags.timelineItemSenderAvatar.value, useUnmergedTree = true).performClick()
eventsRecorder.assertSingle(
MessagesEvent.OnUserClicked(
MatrixUser(
@ -359,12 +358,12 @@ class MessagesViewTest {
@Test
@Config(qualifiers = "h1024dp")
fun `clicking on the display name of the sender of an Event emits expected event`() {
fun `clicking on the display name of the sender of an Event emits expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<MessagesEvent>()
val state = aMessagesState(eventSink = eventsRecorder)
val timelineEvent = state.timelineState.timelineItems.filterIsInstance<TimelineItem.Event>().first()
rule.setMessagesView(state = state)
rule.onNodeWithTag(TestTags.timelineItemSenderAvatar.value, useUnmergedTree = true).performClick()
setMessagesView(state = state)
onNodeWithTag(TestTags.timelineItemSenderAvatar.value, useUnmergedTree = true).performClick()
eventsRecorder.assertSingle(
MessagesEvent.OnUserClicked(
MatrixUser(
@ -377,7 +376,7 @@ class MessagesViewTest {
}
@Test
fun `selecting a action on a message emits the expected Event`() {
fun `selecting a action on a message emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<MessagesEvent>()
val state = aMessagesState(
eventSink = eventsRecorder
@ -395,17 +394,17 @@ class MessagesViewTest {
)
),
)
rule.setMessagesView(
setMessagesView(
state = stateWithMessageAction,
)
rule.clickOn(CommonStrings.action_edit)
clickOn(CommonStrings.action_edit)
// Give time for the close animation to complete
rule.mainClock.advanceTimeBy(milliseconds = 1_000)
mainClock.advanceTimeBy(milliseconds = 1_000)
eventsRecorder.assertSingle(MessagesEvent.HandleAction(TimelineItemAction.Edit, timelineItem))
}
@Test
fun `clicking on a reaction emits the expected Event`() {
fun `clicking on a reaction emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<MessagesEvent>()
val state = aMessagesState(
timelineState = aTimelineState(
@ -414,10 +413,10 @@ class MessagesViewTest {
eventSink = eventsRecorder,
)
val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event
rule.setMessagesView(
setMessagesView(
state = state,
)
rule.onAllNodesWithText(
onAllNodesWithText(
text = "👍️",
useUnmergedTree = true,
).onFirst().performClick()
@ -425,7 +424,7 @@ class MessagesViewTest {
}
@Test
fun `long clicking on a reaction emits the expected Event`() {
fun `long clicking on a reaction emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ReactionSummaryEvent>()
val state = aMessagesState(
timelineState = aTimelineState(
@ -437,10 +436,10 @@ class MessagesViewTest {
),
)
val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event
rule.setMessagesView(
setMessagesView(
state = state,
)
rule.onAllNodesWithText(
onAllNodesWithText(
text = "👍️",
useUnmergedTree = true,
).onFirst().performTouchInput { longClick() }
@ -448,7 +447,7 @@ class MessagesViewTest {
}
@Test
fun `clicking on more reaction emits the expected Event`() {
fun `clicking on more reaction emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<CustomReactionEvent>()
val state = aMessagesState(
timelineState = aTimelineState(
@ -459,16 +458,16 @@ class MessagesViewTest {
),
)
val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event
rule.setMessagesView(
setMessagesView(
state = state,
)
val moreReactionContentDescription = rule.activity.getString(R.string.screen_room_timeline_add_reaction)
rule.onAllNodesWithContentDescription(moreReactionContentDescription).onFirst().performClick()
val moreReactionContentDescription = activity!!.getString(R.string.screen_room_timeline_add_reaction)
onAllNodesWithContentDescription(moreReactionContentDescription).onFirst().performClick()
eventsRecorder.assertSingle(CustomReactionEvent.ShowCustomReactionSheet(timelineItem))
}
@Test
fun `clicking on more reaction from action list emits the expected Event`() {
fun `clicking on more reaction from action list emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<CustomReactionEvent>()
val state = aMessagesState(
timelineState = aTimelineState(
@ -491,18 +490,18 @@ class MessagesViewTest {
eventSink = eventsRecorder
),
)
rule.setMessagesView(
setMessagesView(
state = stateWithActionListState,
)
val moreReactionContentDescription = rule.activity.getString(CommonStrings.a11y_react_with_other_emojis)
rule.onNodeWithContentDescription(moreReactionContentDescription).performClick()
val moreReactionContentDescription = activity!!.getString(CommonStrings.a11y_react_with_other_emojis)
onNodeWithContentDescription(moreReactionContentDescription).performClick()
// Give time for the close animation to complete
rule.mainClock.advanceTimeBy(milliseconds = 1_000)
mainClock.advanceTimeBy(milliseconds = 1_000)
eventsRecorder.assertSingle(CustomReactionEvent.ShowCustomReactionSheet(timelineItem))
}
@Test
fun `clicking on verified user send failure from action list emits the expected Event`() {
fun `clicking on verified user send failure from action list emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<TimelineEvent>()
val state = aMessagesState()
val timelineItem = state.timelineState.timelineItems.first() as TimelineItem.Event
@ -519,21 +518,21 @@ class MessagesViewTest {
),
timelineState = aTimelineState(eventSink = eventsRecorder)
)
rule.setMessagesView(
setMessagesView(
state = stateWithActionListState,
)
// Clear initial 'LoadMore' event emitted when setting the state
eventsRecorder.clear()
val verifiedUserSendFailure = rule.activity.getString(CommonStrings.screen_timeline_item_menu_send_failure_changed_identity, "Alice")
rule.onNodeWithText(verifiedUserSendFailure).performClick()
val verifiedUserSendFailure = activity!!.getString(CommonStrings.screen_timeline_item_menu_send_failure_changed_identity, "Alice")
onNodeWithText(verifiedUserSendFailure).performClick()
// Give time for the close animation to complete
rule.mainClock.advanceTimeBy(milliseconds = 1_000)
mainClock.advanceTimeBy(milliseconds = 1_000)
eventsRecorder.assertSingle(TimelineEvent.ComputeVerifiedUserSendFailure(timelineItem))
}
@Test
fun `clicking on a custom emoji emits the expected Events`() {
fun `clicking on a custom emoji emits the expected Events`() = runAndroidComposeUiTest {
val aUnicode = "🙈"
val customReactionStateEventsRecorder = EventsRecorder<CustomReactionEvent>()
val eventsRecorder = EventsRecorder<MessagesEvent>()
@ -563,18 +562,18 @@ class MessagesViewTest {
eventSink = customReactionStateEventsRecorder
),
)
rule.setMessagesView(
setMessagesView(
state = stateWithCustomReactionState,
)
rule.onNodeWithText(aUnicode, useUnmergedTree = true).performClick()
onNodeWithText(aUnicode, useUnmergedTree = true).performClick()
// Give time for the close animation to complete
rule.mainClock.advanceTimeBy(milliseconds = 1_000)
mainClock.advanceTimeBy(milliseconds = 1_000)
customReactionStateEventsRecorder.assertSingle(CustomReactionEvent.DismissCustomReactionSheet)
eventsRecorder.assertSingle(MessagesEvent.ToggleReaction(aUnicode, timelineItem.eventOrTransactionId))
}
@Test
fun `clicking on pinned messages banner emits the expected Event`() {
fun `clicking on pinned messages banner emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<TimelineEvent>()
val state = aMessagesState(
timelineState = aTimelineState(eventSink = eventsRecorder),
@ -587,16 +586,16 @@ class MessagesViewTest {
),
),
)
rule.setMessagesView(state = state)
setMessagesView(state = state)
// Clear initial 'LoadMore' event emitted when setting the state
eventsRecorder.clear()
rule.onNodeWithText("This is a pinned message").performClick()
onNodeWithText("This is a pinned message").performClick()
eventsRecorder.assertSingle(TimelineEvent.FocusOnEvent(AN_EVENT_ID, debounce = FOCUS_ON_PINNED_EVENT_DEBOUNCE_DURATION_IN_MILLIS.milliseconds))
}
@Test
fun `clicking on successor room button emits expected event`() {
fun `clicking on successor room button emits expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<TimelineEvent>()
val successorRoomId = RoomId("!successor:server.org")
val state = aMessagesState(
@ -606,18 +605,18 @@ class MessagesViewTest {
),
timelineState = aTimelineState(eventSink = eventsRecorder)
)
rule.setMessagesView(state = state)
setMessagesView(state = state)
// Clear initial 'LoadMore' event emitted when setting the state
eventsRecorder.clear()
val text = rule.activity.getString(R.string.screen_room_timeline_tombstoned_room_action)
val text = activity!!.getString(R.string.screen_room_timeline_tombstoned_room_action)
// The bottomsheet subcompose seems to make the node to appear twice
rule.onAllNodesWithText(text).onFirst().performClick()
onAllNodesWithText(text).onFirst().performClick()
eventsRecorder.assertSingle(TimelineEvent.NavigateToPredecessorOrSuccessorRoom(successorRoomId))
}
@Test
fun `clicking on threads list button calls the expected function`() {
fun `clicking on threads list button calls the expected function`() = runAndroidComposeUiTest {
val state = aMessagesState(
threads = MessagesState.Threads(
hasThreads = true,
@ -625,28 +624,28 @@ class MessagesViewTest {
)
)
val onThreadsListClicked = lambdaRecorder<Unit> {}
rule.setMessagesView(
setMessagesView(
state = state,
onThreadsListClicked = onThreadsListClicked,
)
rule.onNodeWithContentDescription("Threads").performClick()
onNodeWithContentDescription("Threads").performClick()
onThreadsListClicked.assertions().isCalledOnce()
}
@Test
fun `no banner shown when there is no successor room`() {
fun `no banner shown when there is no successor room`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<MessagesEvent>(expectEvents = false)
val state = aMessagesState(
successorRoom = null,
eventSink = eventsRecorder
)
rule.setMessagesView(state = state)
rule.assertNoNodeWithText(R.string.screen_room_timeline_tombstoned_room_message)
rule.assertNoNodeWithText(R.string.screen_room_timeline_tombstoned_room_action)
setMessagesView(state = state)
assertNoNodeWithText(R.string.screen_room_timeline_tombstoned_room_message)
assertNoNodeWithText(R.string.screen_room_timeline_tombstoned_room_action)
}
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setMessagesView(
private fun AndroidComposeUiTest<ComponentActivity>.setMessagesView(
state: MessagesState,
onBackClick: () -> Unit = EnsureNeverCalled(),
onRoomDetailsClick: () -> Unit = EnsureNeverCalled(),

View file

@ -6,12 +6,15 @@
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.messages.impl.crypto.identity
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
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 androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.designsystem.components.avatar.anAvatarData
import io.element.android.libraries.matrix.api.core.UserId
@ -21,19 +24,15 @@ import io.element.android.libraries.matrix.ui.room.RoomMemberIdentityStateChange
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class IdentityChangeStateViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test
fun `show and resolve pin violation`() {
fun `show and resolve pin violation`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<IdentityChangeEvent>()
rule.setIdentityChangeStateView(
setIdentityChangeStateView(
state = anIdentityChangeState(
listOf(
RoomMemberIdentityStateChange(
@ -45,18 +44,18 @@ class IdentityChangeStateViewTest {
),
)
rule.onNodeWithText("identity was reset", substring = true).assertExists("should display pin violation warning")
rule.onNodeWithText("@alice:localhost", substring = true).assertExists("should display user mxid")
rule.onNodeWithText("Alice", substring = true).assertExists("should display user displayname")
onNodeWithText("identity was reset", substring = true).assertExists("should display pin violation warning")
onNodeWithText("@alice:localhost", substring = true).assertExists("should display user mxid")
onNodeWithText("Alice", substring = true).assertExists("should display user displayname")
rule.clickOn(res = CommonStrings.action_dismiss)
clickOn(res = CommonStrings.action_dismiss)
eventsRecorder.assertSingle(IdentityChangeEvent.PinIdentity(UserId("@alice:localhost")))
}
@Test
fun `show and resolve verification violation`() {
fun `show and resolve verification violation`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<IdentityChangeEvent>()
rule.setIdentityChangeStateView(
setIdentityChangeStateView(
state = anIdentityChangeState(
listOf(
RoomMemberIdentityStateChange(
@ -68,17 +67,17 @@ class IdentityChangeStateViewTest {
),
)
rule.onNodeWithText("identity was reset", substring = true).assertExists("should display verification violation warning")
rule.onNodeWithText("@alice:localhost", substring = true).assertExists("should display user mxid")
rule.onNodeWithText("Alice", substring = true).assertExists("should display user displayname")
onNodeWithText("identity was reset", substring = true).assertExists("should display verification violation warning")
onNodeWithText("@alice:localhost", substring = true).assertExists("should display user mxid")
onNodeWithText("Alice", substring = true).assertExists("should display user displayname")
rule.clickOn(res = CommonStrings.crypto_identity_change_withdraw_verification_action)
clickOn(res = CommonStrings.crypto_identity_change_withdraw_verification_action)
eventsRecorder.assertSingle(IdentityChangeEvent.WithdrawVerification(UserId("@alice:localhost")))
}
@Test
fun `Should not show any banner if no violations`() {
rule.setIdentityChangeStateView(
fun `Should not show any banner if no violations`() = runAndroidComposeUiTest {
setIdentityChangeStateView(
state = anIdentityChangeState(
listOf(
RoomMemberIdentityStateChange(
@ -93,10 +92,10 @@ class IdentityChangeStateViewTest {
),
)
rule.onNodeWithText("identity was reset", substring = true).assertDoesNotExist()
onNodeWithText("identity was reset", substring = true).assertDoesNotExist()
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setIdentityChangeStateView(
private fun AndroidComposeUiTest<ComponentActivity>.setIdentityChangeStateView(
state: IdentityChangeState,
) {
setContent {

View file

@ -6,54 +6,53 @@
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.messages.impl.crypto.sendfailure.resolve
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.setSafeContent
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ResolveVerifiedUserSendFailureViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test
fun `clicking on resolve and resend emit the expected event`() {
fun `clicking on resolve and resend emit the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ResolveVerifiedUserSendFailureEvent>()
rule.setResolveVerifiedUserSendFailureView(
setResolveVerifiedUserSendFailureView(
state = aResolveVerifiedUserSendFailureState(
verifiedUserSendFailure = aChangedIdentitySendFailure(),
eventSink = eventsRecorder,
),
)
rule.clickOn(res = CommonStrings.screen_resolve_send_failure_changed_identity_primary_button_title)
clickOn(res = CommonStrings.screen_resolve_send_failure_changed_identity_primary_button_title)
eventsRecorder.assertSingle(ResolveVerifiedUserSendFailureEvent.ResolveAndResend)
}
@Test
fun `clicking on retry emit the expected event`() {
fun `clicking on retry emit the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ResolveVerifiedUserSendFailureEvent>()
rule.setResolveVerifiedUserSendFailureView(
setResolveVerifiedUserSendFailureView(
state = aResolveVerifiedUserSendFailureState(
verifiedUserSendFailure = aChangedIdentitySendFailure(),
eventSink = eventsRecorder,
),
)
rule.clickOn(res = CommonStrings.action_retry)
clickOn(res = CommonStrings.action_retry)
eventsRecorder.assertSingle(ResolveVerifiedUserSendFailureEvent.Retry)
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setResolveVerifiedUserSendFailureView(
private fun AndroidComposeUiTest<ComponentActivity>.setResolveVerifiedUserSendFailureView(
state: ResolveVerifiedUserSendFailureState,
) {
setSafeContent {

View file

@ -6,11 +6,14 @@
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.messages.impl.link
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.ui.strings.CommonStrings
@ -19,51 +22,46 @@ import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnceWithParam
import io.element.android.wysiwyg.link.Link
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class LinkViewTest {
@get:Rule
val rule = createAndroidComposeRule<ComponentActivity>()
@Test
fun `clicking on cancel emits the expected event`() {
fun `clicking on cancel emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<LinkEvent>()
rule.setLinkView(
setLinkView(
aLinkState(
linkClick = ConfirmingLinkClick(aLink),
eventSink = eventsRecorder,
),
)
rule.clickOn(CommonStrings.action_cancel)
clickOn(CommonStrings.action_cancel)
eventsRecorder.assertSingle(
LinkEvent.Cancel
)
}
@Test
fun `clicking on continue emits the expected event`() {
fun `clicking on continue emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<LinkEvent>()
rule.setLinkView(
setLinkView(
aLinkState(
linkClick = ConfirmingLinkClick(aLink),
eventSink = eventsRecorder,
),
)
rule.clickOn(CommonStrings.action_continue)
clickOn(CommonStrings.action_continue)
eventsRecorder.assertSingle(
LinkEvent.Confirm
)
}
@Test
fun `success state invokes the callback and emits the expected event`() {
fun `success state invokes the callback and emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<LinkEvent>()
ensureCalledOnceWithParam(aLink) { callback ->
rule.setLinkView(
setLinkView(
aLinkState(
linkClick = AsyncAction.Success(aLink),
eventSink = eventsRecorder,
@ -77,7 +75,7 @@ class LinkViewTest {
}
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setLinkView(
private fun AndroidComposeUiTest<ComponentActivity>.setLinkView(
state: LinkState,
onLinkValid: (Link) -> Unit = EnsureNeverCalledWithParam(),
) {

View file

@ -6,13 +6,16 @@
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.messages.impl.pinned.banner
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.ui.strings.CommonStrings
@ -22,49 +25,45 @@ import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.ensureCalledOnceWithParam
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class PinnedMessagesBannerViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test
fun `clicking on the banner invoke expected callback`() {
fun `clicking on the banner invoke expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PinnedMessagesBannerEvent>()
val state = aLoadedPinnedMessagesBannerState(
eventSink = eventsRecorder
)
val pinnedEventId = state.currentPinnedMessage.eventId
ensureCalledOnceWithParam(pinnedEventId) { callback ->
rule.setPinnedMessagesBannerView(
setPinnedMessagesBannerView(
state = state,
onClick = callback
)
rule.onRoot().performClick()
onRoot().performClick()
eventsRecorder.assertSingle(PinnedMessagesBannerEvent.MoveToNextPinned)
}
}
@Test
fun `clicking on view all emit the expected event`() {
fun `clicking on view all emit the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PinnedMessagesBannerEvent>(expectEvents = true)
val state = aLoadedPinnedMessagesBannerState(
eventSink = eventsRecorder
)
ensureCalledOnce { callback ->
rule.setPinnedMessagesBannerView(
setPinnedMessagesBannerView(
state = state,
onViewAllClick = callback
)
rule.clickOn(CommonStrings.screen_room_pinned_banner_view_all_button_title)
clickOn(CommonStrings.screen_room_pinned_banner_view_all_button_title)
}
}
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setPinnedMessagesBannerView(
private fun AndroidComposeUiTest<ComponentActivity>.setPinnedMessagesBannerView(
state: PinnedMessagesBannerState,
onClick: (EventId) -> Unit = EnsureNeverCalledWithParam(),
onViewAllClick: () -> Unit = EnsureNeverCalled(),

View file

@ -6,16 +6,19 @@
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.messages.impl.pinned.list
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.longClick
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onFirst
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.messages.impl.actionlist.ActionListEvent
import io.element.android.features.messages.impl.actionlist.anActionListState
@ -31,33 +34,28 @@ import io.element.android.tests.testutils.ensureCalledOnceWithParam
import io.element.android.tests.testutils.pressBack
import io.element.android.tests.testutils.setSafeContent
import io.element.android.wysiwyg.link.Link
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class PinnedMessagesListViewTest {
@get:Rule
val rule = createAndroidComposeRule<ComponentActivity>()
@Test
fun `clicking on back calls the expected callback`() {
fun `clicking on back calls the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PinnedMessagesListEvent>(expectEvents = false)
val state = aLoadedPinnedMessagesListState(
eventSink = eventsRecorder
)
ensureCalledOnce { callback ->
rule.setPinnedMessagesListView(
setPinnedMessagesListView(
state = state,
onBackClick = callback
)
rule.pressBack()
pressBack()
}
}
@Test
fun `click on an event calls the expected callback`() {
fun `click on an event calls the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<PinnedMessagesListEvent>(expectEvents = false)
val content = aTimelineItemFileContent()
val state = aLoadedPinnedMessagesListState(
@ -67,16 +65,16 @@ class PinnedMessagesListViewTest {
val event = state.timelineItems.first() as TimelineItem.Event
ensureCalledOnceWithParam(event) { callback ->
rule.setPinnedMessagesListView(
setPinnedMessagesListView(
state = state,
onEventClick = callback
)
rule.onAllNodesWithText(content.filename).onFirst().performClick()
onAllNodesWithText(content.filename).onFirst().performClick()
}
}
@Test
fun `long click on an event emits the expected event`() {
fun `long click on an event emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ActionListEvent>(expectEvents = true)
val content = aTimelineItemFileContent()
val state = aLoadedPinnedMessagesListState(
@ -84,10 +82,10 @@ class PinnedMessagesListViewTest {
actionListState = anActionListState(eventSink = eventsRecorder)
)
rule.setPinnedMessagesListView(
setPinnedMessagesListView(
state = state,
)
rule.onAllNodesWithText(content.filename).onFirst()
onAllNodesWithText(content.filename).onFirst()
.performTouchInput {
longClick()
}
@ -96,7 +94,7 @@ class PinnedMessagesListViewTest {
}
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setPinnedMessagesListView(
private fun AndroidComposeUiTest<ComponentActivity>.setPinnedMessagesListView(
state: PinnedMessagesListState,
onBackClick: () -> Unit = EnsureNeverCalled(),
onEventClick: (event: TimelineItem.Event) -> Unit = EnsureNeverCalledWithParam(),

View file

@ -6,11 +6,14 @@
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.messages.impl.timeline
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.v2.runComposeUiTest
import com.google.common.truth.Truth.assertThat
import io.element.android.features.messages.impl.utils.FakeMentionSpanFormatter
import io.element.android.libraries.core.extensions.runCatchingExceptions
@ -18,15 +21,12 @@ import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
import io.element.android.libraries.textcomposer.mentions.MentionSpanProvider
import io.element.android.libraries.textcomposer.mentions.MentionSpanTheme
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
@RunWith(RobolectricTestRunner::class)
class DefaultHtmlConverterProviderTest {
@get:Rule val composeTestRule = createComposeRule()
private val provider = DefaultHtmlConverterProvider(
mentionSpanProvider = MentionSpanProvider(
permalinkParser = FakePermalinkParser(),
@ -43,8 +43,8 @@ class DefaultHtmlConverterProviderTest {
}
@Test
fun `calling provide after calling Update first should return an HtmlConverter`() {
composeTestRule.setContent {
fun `calling provide after calling Update first should return an HtmlConverter`() = runComposeUiTest {
setContent {
CompositionLocalProvider(LocalInspectionMode provides true) {
provider.Update()
}

View file

@ -6,15 +6,18 @@
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.messages.impl.timeline
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollToIndex
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.messages.impl.timeline.components.MessageShieldData
import io.element.android.features.messages.impl.timeline.components.aCriticalShield
@ -39,19 +42,15 @@ import io.element.android.wysiwyg.link.Link
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class TimelineViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test
fun `reaching the end of the timeline with more events to load emits a LoadMore event`() {
fun `reaching the end of the timeline with more events to load emits a LoadMore event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<TimelineEvent>()
rule.setTimelineView(
setTimelineView(
state = aTimelineState(
timelineItems = persistentListOf<TimelineItem>(
TimelineItem.Virtual(
@ -66,9 +65,9 @@ class TimelineViewTest {
}
@Test
fun `reaching the end of the timeline does not send a LoadMore event`() {
fun `reaching the end of the timeline does not send a LoadMore event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<TimelineEvent>()
rule.setTimelineView(
setTimelineView(
state = aTimelineState(
timelineItems = persistentListOf(aTimelineItemEvent(content = aTimelineItemImageContent())),
eventSink = eventsRecorder,
@ -78,9 +77,9 @@ class TimelineViewTest {
}
@Test
fun `scroll to bottom on live timeline does not emit the Event`() {
fun `scroll to bottom on live timeline does not emit the Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<TimelineEvent>()
rule.setTimelineView(
setTimelineView(
state = aTimelineState(
timelineItems = persistentListOf(aTimelineItemEvent(content = aTimelineItemImageContent())),
isLive = true,
@ -92,14 +91,14 @@ class TimelineViewTest {
eventsRecorder.assertSingle(TimelineEvent.OnScrollFinished(firstIndex = 0))
eventsRecorder.clear()
val contentDescription = rule.activity.getString(CommonStrings.a11y_jump_to_bottom)
rule.onNodeWithContentDescription(contentDescription).performClick()
val contentDescription = activity!!.getString(CommonStrings.a11y_jump_to_bottom)
onNodeWithContentDescription(contentDescription).performClick()
}
@Test
fun `scroll to bottom on detached timeline emits the expected Event`() {
fun `scroll to bottom on detached timeline emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<TimelineEvent>()
rule.setTimelineView(
setTimelineView(
state = aTimelineState(
timelineItems = persistentListOf(aTimelineItemEvent(content = aTimelineItemImageContent())),
isLive = false,
@ -110,15 +109,15 @@ class TimelineViewTest {
eventsRecorder.assertSingle(TimelineEvent.OnScrollFinished(firstIndex = 0))
eventsRecorder.clear()
val contentDescription = rule.activity.getString(CommonStrings.a11y_jump_to_bottom)
rule.onNodeWithContentDescription(contentDescription).performClick()
val contentDescription = activity!!.getString(CommonStrings.a11y_jump_to_bottom)
onNodeWithContentDescription(contentDescription).performClick()
eventsRecorder.assertSingle(TimelineEvent.JumpToLive)
}
@Test
fun `an empty timeline triggers a prefetch`() {
fun `an empty timeline triggers a prefetch`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<TimelineEvent>()
rule.setTimelineView(
setTimelineView(
state = aTimelineState(
timelineItems = persistentListOf(),
eventSink = eventsRecorder,
@ -129,9 +128,9 @@ class TimelineViewTest {
}
@Test
fun `show shield dialog`() {
fun `show shield dialog`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<TimelineEvent>()
rule.setTimelineView(
setTimelineView(
state = aTimelineState(
timelineItems = persistentListOf<TimelineItem>(
aTimelineItemEvent(
@ -143,8 +142,8 @@ class TimelineViewTest {
eventSink = eventsRecorder,
),
)
val contentDescription = rule.activity.getString(CommonStrings.a11y_encryption_details)
rule.onNodeWithContentDescription(contentDescription).performClick()
val contentDescription = activity!!.getString(CommonStrings.a11y_encryption_details)
onNodeWithContentDescription(contentDescription).performClick()
eventsRecorder.assertList(
listOf(
TimelineEvent.OnScrollFinished(0),
@ -154,9 +153,9 @@ class TimelineViewTest {
}
@Test
fun `hide shield dialog`() {
fun `hide shield dialog`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<TimelineEvent>()
rule.setTimelineView(
setTimelineView(
state = aTimelineState(
timelineItems = persistentListOf(aTimelineItemEvent(content = aTimelineItemImageContent())),
isLive = false,
@ -167,16 +166,16 @@ class TimelineViewTest {
eventsRecorder.assertSingle(TimelineEvent.OnScrollFinished(firstIndex = 0))
eventsRecorder.clear()
rule.clickOn(CommonStrings.action_ok)
clickOn(CommonStrings.action_ok)
eventsRecorder.assertSingle(TimelineEvent.HideShieldDialog)
}
@Ignore(
"performScrollToIndex in compose tests no longer sets LazyListState.isScrollInProgress to true, so the LoadMore event is not emitted." +
"This needs to be reworked to use a different approach to check the LoadMore event was emitted."
"This needs to be reworked to use a different approach to check the LoadMore event was emitted."
)
@Test
fun `scrolling near to the start of the loaded items triggers a pre-fetch`() {
fun `scrolling near to the start of the loaded items triggers a pre-fetch`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<TimelineEvent>()
val items = List<TimelineItem>(200) {
aTimelineItemEvent(
@ -185,7 +184,7 @@ class TimelineViewTest {
)
}.toImmutableList()
rule.setTimelineView(
setTimelineView(
state = aTimelineState(
timelineItems = items,
eventSink = eventsRecorder,
@ -194,9 +193,9 @@ class TimelineViewTest {
),
)
rule.onNodeWithTag("timeline").performScrollToIndex(180)
onNodeWithTag("timeline").performScrollToIndex(180)
rule.mainClock.advanceTimeBy(1000)
mainClock.advanceTimeBy(1000)
eventsRecorder.assertList(
listOf(
@ -207,7 +206,7 @@ class TimelineViewTest {
}
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setTimelineView(
private fun AndroidComposeUiTest<ComponentActivity>.setTimelineView(
state: TimelineState,
timelineProtectionState: TimelineProtectionState = aTimelineProtectionState(),
onUserDataClick: (MatrixUser) -> Unit = EnsureNeverCalledWithParam(),

View file

@ -6,12 +6,15 @@
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.messages.impl.timeline.components.event
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.messages.impl.timeline.TimelineEvent
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemPollContent
@ -20,14 +23,11 @@ import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.pressTag
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class TimelineItemPollViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test
fun `answering a poll with first answer should emit a PollAnswerSelected event`() {
testAnswer(answerIndex = 0)
@ -38,17 +38,17 @@ class TimelineItemPollViewTest {
testAnswer(answerIndex = 1)
}
private fun testAnswer(answerIndex: Int) {
private fun testAnswer(answerIndex: Int) = runAndroidComposeUiTest<ComponentActivity> {
val eventsRecorder = EventsRecorder<TimelineEvent.TimelineItemPollEvent>()
val content = aTimelineItemPollContent()
rule.setContent {
setContent {
TimelineItemPollView(
content = content,
eventSink = eventsRecorder
)
}
val answer = content.answerItems[answerIndex].answer
rule.onNode(
onNode(
matcher = hasText(answer.text),
useUnmergedTree = true,
).performClick()
@ -56,38 +56,38 @@ class TimelineItemPollViewTest {
}
@Test
fun `editing a poll should emit a PollEditClicked event`() {
fun `editing a poll should emit a PollEditClicked event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<TimelineEvent.TimelineItemPollEvent>()
val content = aTimelineItemPollContent(
isMine = true,
isEditable = true,
)
rule.setContent {
setContent {
TimelineItemPollView(
content = content,
eventSink = eventsRecorder
)
}
rule.clickOn(CommonStrings.action_edit_poll)
clickOn(CommonStrings.action_edit_poll)
eventsRecorder.assertSingle(TimelineEvent.EditPoll(content.eventId!!))
}
@Test
fun `closing a poll should emit a PollEndClicked event`() {
fun `closing a poll should emit a PollEndClicked event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<TimelineEvent.TimelineItemPollEvent>()
val content = aTimelineItemPollContent(
isMine = true,
)
rule.setContent {
setContent {
TimelineItemPollView(
content = content,
eventSink = eventsRecorder
)
}
rule.clickOn(CommonStrings.action_end_poll)
clickOn(CommonStrings.action_end_poll)
// A confirmation dialog should be shown
eventsRecorder.assertEmpty()
rule.pressTag(TestTags.dialogPositive.value)
pressTag(TestTags.dialogPositive.value)
eventsRecorder.assertSingle(TimelineEvent.EndPoll(content.eventId!!))
}
}

View file

@ -6,14 +6,17 @@
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.messages.impl.timeline.components.event
import android.text.SpannableString
import android.text.SpannedString
import androidx.activity.ComponentActivity
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.AndroidComposeUiTest
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.core.text.buildSpannedString
import androidx.core.text.inSpans
import androidx.test.ext.junit.runners.AndroidJUnit4
@ -38,45 +41,40 @@ import io.element.android.tests.testutils.lambda.assert
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.wysiwyg.view.spans.CustomMentionSpan
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class TimelineTextViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
private val mentionSpanTheme = MentionSpanTheme(currentUserId = A_USER_ID)
private val formatLambda = lambdaRecorder<MentionType, CharSequence> { mentionType -> mentionType.toString() }
private val mentionSpanFormatter = FakeMentionSpanFormatter(formatLambda)
@Test
fun `getTextWithResolvedMentions - does nothing for a non spannable CharSequence`() = runTest {
fun `getTextWithResolvedMentions - does nothing for a non spannable CharSequence`() = runAndroidComposeUiTest {
val charSequence = "Hello <a href=\"https://matrix.to/#/@alice:example.com\">@alice:example.com</a>"
val mentionSpanUpdater = aMentionSpanUpdater()
val result = rule.getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence))
val result = getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence))
assertThat(result.getMentionSpans()).isEmpty()
assert(formatLambda).isNeverCalled()
}
@Test
fun `getTextWithResolvedMentions - does nothing if there are no mentions`() = runTest {
fun `getTextWithResolvedMentions - does nothing if there are no mentions`() = runAndroidComposeUiTest {
val charSequence = SpannableString("Hello <a href=\"https://matrix.to/#/@alice:example.com\">@alice:example.com</a>")
val mentionSpanUpdater = aMentionSpanUpdater()
val result = rule.getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence))
val result = getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence))
assertThat(result.getMentionSpans()).isEmpty()
assert(formatLambda).isNeverCalled()
}
@Test
fun `getTextWithResolvedMentions - just returns the body if there is no formattedBody`() = runTest {
fun `getTextWithResolvedMentions - just returns the body if there is no formattedBody`() = runAndroidComposeUiTest {
val charSequence = "Hello <a href=\"https://matrix.to/#/@alice:example.com\">@alice:example.com</a>"
val mentionSpanUpdater = aMentionSpanUpdater()
val result = rule.getText(mentionSpanUpdater, aTextContentWithFormattedBody(body = charSequence, formattedBody = null))
val result = getText(mentionSpanUpdater, aTextContentWithFormattedBody(body = charSequence, formattedBody = null))
assertThat(result.getMentionSpans()).isEmpty()
assertThat(result.toString()).isEqualTo(charSequence)
@ -84,7 +82,7 @@ class TimelineTextViewTest {
}
@Test
fun `getTextWithResolvedMentions - with Room mention format correctly`() = runTest {
fun `getTextWithResolvedMentions - with Room mention format correctly`() = runAndroidComposeUiTest {
val mentionType = MentionType.Room(roomIdOrAlias = A_ROOM_ID_2.toRoomIdOrAlias())
val charSequence = buildSpannedString {
append("Hello ")
@ -93,7 +91,7 @@ class TimelineTextViewTest {
}
}
val mentionSpanUpdater = aMentionSpanUpdater()
val result = rule.getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence))
val result = getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence))
val expectedDisplayText = mentionType.toString()
assertThat(result.getMentionSpans().firstOrNull()?.displayText.toString()).isEqualTo(expectedDisplayText)
@ -102,7 +100,7 @@ class TimelineTextViewTest {
}
@Test
fun `getTextWithResolvedMentions - replaces MentionSpan's text`() = runTest {
fun `getTextWithResolvedMentions - replaces MentionSpan's text`() = runAndroidComposeUiTest {
val mentionType = MentionType.User(userId = A_USER_ID)
val charSequence = buildSpannedString {
append("Hello ")
@ -111,7 +109,7 @@ class TimelineTextViewTest {
}
}
val mentionSpanUpdater = aMentionSpanUpdater()
val result = rule.getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence))
val result = getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence))
val expectedDisplayText = mentionType.toString()
assertThat(result.getMentionSpans().firstOrNull()?.displayText.toString()).isEqualTo(expectedDisplayText)
@ -119,7 +117,7 @@ class TimelineTextViewTest {
}
@Test
fun `getTextWithResolvedMentions - replaces MentionSpan's text inside CustomMentionSpan`() = runTest {
fun `getTextWithResolvedMentions - replaces MentionSpan's text inside CustomMentionSpan`() = runAndroidComposeUiTest {
val mentionType = MentionType.User(userId = A_USER_ID)
val charSequence = buildSpannedString {
append("Hello ")
@ -129,12 +127,12 @@ class TimelineTextViewTest {
}
val mentionSpanUpdater = aMentionSpanUpdater()
val expectedDisplayText = mentionType.toString()
val result = rule.getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence))
val result = getText(mentionSpanUpdater, aTextContentWithFormattedBody(charSequence))
assertThat(result.getMentionSpans().firstOrNull()?.displayText.toString()).isEqualTo(expectedDisplayText)
assert(formatLambda).isCalledOnce()
}
private suspend fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.getText(
private suspend fun AndroidComposeUiTest<ComponentActivity>.getText(
mentionSpanUpdater: MentionSpanUpdater,
content: TimelineItemTextBasedContent,
): CharSequence {

View file

@ -6,56 +6,55 @@
* Please see LICENSE files in the repository root for full details.
*/
@file:OptIn(ExperimentalTestApi::class)
package io.element.android.features.messages.impl.timeline.protection
import androidx.activity.ComponentActivity
import androidx.compose.runtime.Composable
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
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 androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce
import io.element.android.tests.testutils.lambda.lambdaError
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ProtectedViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test
fun `when hideContent is false, the content is rendered`() {
rule.setProtectedView(
fun `when hideContent is false, the content is rendered`() = runAndroidComposeUiTest {
setProtectedView(
hideContent = false,
content = {
Text("Hello")
}
)
rule.onNodeWithText("Hello").assertExists()
onNodeWithText("Hello").assertExists()
}
@Test
fun `when hideContent is true, the content is not rendered, and user can reveal it`() {
fun `when hideContent is true, the content is not rendered, and user can reveal it`() = runAndroidComposeUiTest {
ensureCalledOnce {
rule.setProtectedView(
setProtectedView(
hideContent = true,
onShowClick = it,
content = {
Text("Hello")
}
)
rule.onNodeWithText("Hello").assertDoesNotExist()
rule.clickOn(CommonStrings.action_show)
onNodeWithText("Hello").assertDoesNotExist()
clickOn(CommonStrings.action_show)
}
}
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setProtectedView(
private fun AndroidComposeUiTest<ComponentActivity>.setProtectedView(
hideContent: Boolean = false,
onShowClick: () -> Unit = { lambdaError() },
content: @Composable () -> Unit = {},