Browse Source
* UX cleanup: user profile. - Move send DM to a CTA button. - Add 'Call' CTA button too when there is a DM with that user and a call is possible. - Add missing tests. * Update screenshots * Add tests for clicking on the avatar --------- Co-authored-by: ElementBot <benoitm+elementbot@element.io>pull/2826/head
Jorge Martin Espinosa
4 months ago
committed by
GitHub
38 changed files with 396 additions and 56 deletions
@ -0,0 +1 @@ |
|||||||
|
UX cleanup: user profile. Move send DM to a call to action button, add 'Call' CTA too. |
@ -0,0 +1,235 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2024 New Vector Ltd |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package io.element.android.features.userprofile |
||||||
|
|
||||||
|
import androidx.activity.ComponentActivity |
||||||
|
import androidx.compose.ui.test.hasTestTag |
||||||
|
import androidx.compose.ui.test.junit4.AndroidComposeTestRule |
||||||
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule |
||||||
|
import androidx.compose.ui.test.performClick |
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4 |
||||||
|
import io.element.android.features.userprofile.shared.R |
||||||
|
import io.element.android.features.userprofile.shared.UserProfileEvents |
||||||
|
import io.element.android.features.userprofile.shared.UserProfileState |
||||||
|
import io.element.android.features.userprofile.shared.UserProfileView |
||||||
|
import io.element.android.features.userprofile.shared.aUserProfileState |
||||||
|
import io.element.android.libraries.architecture.AsyncData |
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId |
||||||
|
import io.element.android.libraries.matrix.test.AN_AVATAR_URL |
||||||
|
import io.element.android.libraries.matrix.test.A_ROOM_ID |
||||||
|
import io.element.android.libraries.matrix.test.A_USER_NAME |
||||||
|
import io.element.android.libraries.testtags.TestTags |
||||||
|
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.EnsureNeverCalledWithTwoParams |
||||||
|
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 io.element.android.tests.testutils.ensureCalledOnceWithTwoParams |
||||||
|
import io.element.android.tests.testutils.pressBack |
||||||
|
import io.element.android.tests.testutils.pressBackKey |
||||||
|
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 UserProfileViewTest { |
||||||
|
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>() |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `on back key press - the expected callback is called`() = runTest { |
||||||
|
ensureCalledOnce { callback -> |
||||||
|
rule.setUserProfileView( |
||||||
|
goBack = callback, |
||||||
|
) |
||||||
|
rule.pressBackKey() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `on back button click - the expected callback is called`() = runTest { |
||||||
|
ensureCalledOnce { callback -> |
||||||
|
rule.setUserProfileView( |
||||||
|
goBack = callback, |
||||||
|
) |
||||||
|
rule.pressBack() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `on avatar clicked - the expected callback is called`() = runTest { |
||||||
|
ensureCalledOnceWithTwoParams(A_USER_NAME, AN_AVATAR_URL) { callback -> |
||||||
|
rule.setUserProfileView( |
||||||
|
state = aUserProfileState(userName = A_USER_NAME, avatarUrl = AN_AVATAR_URL), |
||||||
|
openAvatarPreview = callback, |
||||||
|
) |
||||||
|
rule.onNode(hasTestTag(TestTags.memberDetailAvatar.value)).performClick() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `on avatar clicked with no avatar - nothing happens`() = runTest { |
||||||
|
val callback = EnsureNeverCalledWithTwoParams<String, String>() |
||||||
|
rule.setUserProfileView( |
||||||
|
state = aUserProfileState(userName = A_USER_NAME, avatarUrl = null), |
||||||
|
openAvatarPreview = callback, |
||||||
|
) |
||||||
|
rule.onNode(hasTestTag(TestTags.memberDetailAvatar.value)).performClick() |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `on Share clicked - the expected callback is called`() = runTest { |
||||||
|
ensureCalledOnce { callback -> |
||||||
|
rule.setUserProfileView( |
||||||
|
onShareUser = callback, |
||||||
|
) |
||||||
|
rule.clickOn(CommonStrings.action_share) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `on Message clicked - the StartDm event is emitted`() = runTest { |
||||||
|
val eventsRecorder = EventsRecorder<UserProfileEvents>() |
||||||
|
rule.setUserProfileView( |
||||||
|
state = aUserProfileState( |
||||||
|
dmRoomId = A_ROOM_ID, |
||||||
|
eventSink = eventsRecorder, |
||||||
|
), |
||||||
|
) |
||||||
|
rule.clickOn(CommonStrings.action_message) |
||||||
|
eventsRecorder.assertSingle(UserProfileEvents.StartDM) |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `on Call clicked - the expected callback is called`() = runTest { |
||||||
|
ensureCalledOnceWithParam(A_ROOM_ID) { callback -> |
||||||
|
rule.setUserProfileView( |
||||||
|
state = aUserProfileState( |
||||||
|
dmRoomId = A_ROOM_ID, |
||||||
|
canCall = true, |
||||||
|
), |
||||||
|
onStartCall = callback, |
||||||
|
) |
||||||
|
rule.clickOn(CommonStrings.action_call) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `on Block user clicked - a BlockUser event is emitted with needsConfirmation`() = runTest { |
||||||
|
val eventsRecorder = EventsRecorder<UserProfileEvents>() |
||||||
|
rule.setUserProfileView( |
||||||
|
state = aUserProfileState( |
||||||
|
eventSink = eventsRecorder, |
||||||
|
), |
||||||
|
) |
||||||
|
rule.clickOn(R.string.screen_dm_details_block_user) |
||||||
|
eventsRecorder.assertSingle(UserProfileEvents.BlockUser(needsConfirmation = true)) |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `on confirming block user - a BlockUser event is emitted without needsConfirmation`() = runTest { |
||||||
|
val eventsRecorder = EventsRecorder<UserProfileEvents>() |
||||||
|
rule.setUserProfileView( |
||||||
|
state = aUserProfileState( |
||||||
|
displayConfirmationDialog = UserProfileState.ConfirmationDialog.Block, |
||||||
|
eventSink = eventsRecorder, |
||||||
|
), |
||||||
|
) |
||||||
|
rule.clickOn(R.string.screen_dm_details_block_alert_action) |
||||||
|
eventsRecorder.assertSingle(UserProfileEvents.BlockUser(needsConfirmation = false)) |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `on canceling blocking a user - a ClearConfirmationDialog event is emitted`() = runTest { |
||||||
|
val eventsRecorder = EventsRecorder<UserProfileEvents>() |
||||||
|
rule.setUserProfileView( |
||||||
|
state = aUserProfileState( |
||||||
|
displayConfirmationDialog = UserProfileState.ConfirmationDialog.Block, |
||||||
|
eventSink = eventsRecorder, |
||||||
|
), |
||||||
|
) |
||||||
|
rule.clickOn(CommonStrings.action_cancel) |
||||||
|
eventsRecorder.assertSingle(UserProfileEvents.ClearConfirmationDialog) |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `on Unblock user clicked - an UnblockUser event is emitted with needsConfirmation`() = runTest { |
||||||
|
val eventsRecorder = EventsRecorder<UserProfileEvents>() |
||||||
|
rule.setUserProfileView( |
||||||
|
state = aUserProfileState( |
||||||
|
isBlocked = AsyncData.Success(true), |
||||||
|
eventSink = eventsRecorder, |
||||||
|
), |
||||||
|
) |
||||||
|
rule.clickOn(R.string.screen_dm_details_unblock_user) |
||||||
|
eventsRecorder.assertSingle(UserProfileEvents.UnblockUser(needsConfirmation = true)) |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `on confirming Unblock user - an UnblockUser event is emitted without needsConfirmation`() = runTest { |
||||||
|
val eventsRecorder = EventsRecorder<UserProfileEvents>() |
||||||
|
rule.setUserProfileView( |
||||||
|
state = aUserProfileState( |
||||||
|
isBlocked = AsyncData.Success(true), |
||||||
|
displayConfirmationDialog = UserProfileState.ConfirmationDialog.Unblock, |
||||||
|
eventSink = eventsRecorder, |
||||||
|
), |
||||||
|
) |
||||||
|
rule.clickOn(R.string.screen_dm_details_unblock_alert_action) |
||||||
|
eventsRecorder.assertSingle(UserProfileEvents.UnblockUser(needsConfirmation = false)) |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
fun `on canceling unblocking a user - a ClearConfirmationDialog event is emitted`() = runTest { |
||||||
|
val eventsRecorder = EventsRecorder<UserProfileEvents>() |
||||||
|
rule.setUserProfileView( |
||||||
|
state = aUserProfileState( |
||||||
|
isBlocked = AsyncData.Success(true), |
||||||
|
displayConfirmationDialog = UserProfileState.ConfirmationDialog.Unblock, |
||||||
|
eventSink = eventsRecorder, |
||||||
|
), |
||||||
|
) |
||||||
|
rule.clickOn(CommonStrings.action_cancel) |
||||||
|
eventsRecorder.assertSingle(UserProfileEvents.ClearConfirmationDialog) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setUserProfileView( |
||||||
|
state: UserProfileState = aUserProfileState( |
||||||
|
eventSink = EventsRecorder(expectEvents = false), |
||||||
|
), |
||||||
|
onShareUser: () -> Unit = EnsureNeverCalled(), |
||||||
|
onDmStarted: (RoomId) -> Unit = EnsureNeverCalledWithParam(), |
||||||
|
onStartCall: (RoomId) -> Unit = EnsureNeverCalledWithParam(), |
||||||
|
goBack: () -> Unit = EnsureNeverCalled(), |
||||||
|
openAvatarPreview: (String, String) -> Unit = EnsureNeverCalledWithTwoParams(), |
||||||
|
) { |
||||||
|
setContent { |
||||||
|
UserProfileView( |
||||||
|
state = state, |
||||||
|
onShareUser = onShareUser, |
||||||
|
onDmStarted = onDmStarted, |
||||||
|
onStartCall = onStartCall, |
||||||
|
goBack = goBack, |
||||||
|
openAvatarPreview = openAvatarPreview, |
||||||
|
) |
||||||
|
} |
||||||
|
} |
@ -1,3 +1,3 @@ |
|||||||
version https://git-lfs.github.com/spec/v1 |
version https://git-lfs.github.com/spec/v1 |
||||||
oid sha256:8fc6369578a3684c1a88e904848bcb16a7e4cde46c887938db909563c6c54d2d |
oid sha256:96414480d9e361319abf7543324927300f841633f628da68901d6291b8cfd675 |
||||||
size 23031 |
size 22667 |
||||||
|
@ -1,3 +1,3 @@ |
|||||||
version https://git-lfs.github.com/spec/v1 |
version https://git-lfs.github.com/spec/v1 |
||||||
oid sha256:59e3eccfa962e9d667aaa326ab3258214334db558983bc251543835585d78453 |
oid sha256:7388e54cc1fccc87a3257b099aa832f851aabace145cc09f965431653fc0cf35 |
||||||
size 20930 |
size 20661 |
||||||
|
@ -1,3 +1,3 @@ |
|||||||
version https://git-lfs.github.com/spec/v1 |
version https://git-lfs.github.com/spec/v1 |
||||||
oid sha256:07418c91e8efee98f7b9e7d069bf3aef2b674164dad0795719f97ad6b214225b |
oid sha256:a5ca4c3f9c7376656357f07a6b1777bca92808eeb972f9f7d03bdac89a6c5867 |
||||||
size 23550 |
size 22987 |
||||||
|
@ -1,3 +1,3 @@ |
|||||||
version https://git-lfs.github.com/spec/v1 |
version https://git-lfs.github.com/spec/v1 |
||||||
oid sha256:90fe61eb831db5e1f1eb213fcdd690f06f2e547b8ad864680288d529d379d30e |
oid sha256:2348853735daae624c99658d832382fd834a770b40fde0004089f6378ecb954c |
||||||
size 43651 |
size 41498 |
||||||
|
@ -1,3 +1,3 @@ |
|||||||
version https://git-lfs.github.com/spec/v1 |
version https://git-lfs.github.com/spec/v1 |
||||||
oid sha256:a6b0328e4f5a06b59ac5b73f4230bd950bed6847af275b9e6851ffdb9403742b |
oid sha256:364172de34c530ce2cb4799e0b5ac5aa810068794bab10c9f04f72f709ebd8fa |
||||||
size 35523 |
size 32482 |
||||||
|
@ -1,3 +1,3 @@ |
|||||||
version https://git-lfs.github.com/spec/v1 |
version https://git-lfs.github.com/spec/v1 |
||||||
oid sha256:7536568491a41526b48354b60db41e47d40b6677e5b38f464c57715499514171 |
oid sha256:1a956800e49a48da34a49f7ec4c2647641a1c64af70e4ccbcdbdac0fb0cc5616 |
||||||
size 24193 |
size 23513 |
||||||
|
@ -1,3 +1,3 @@ |
|||||||
version https://git-lfs.github.com/spec/v1 |
version https://git-lfs.github.com/spec/v1 |
||||||
oid sha256:902bd8ddad85ee2ea52898a910baf0c710880d94c36f56e22916bf4d15f4469c |
oid sha256:01308cc0d4361e2685a16ce02526a9efdc4f81b86fdb982b016e090bcafa9bbb |
||||||
size 25864 |
size 23486 |
||||||
|
@ -0,0 +1,3 @@ |
|||||||
|
version https://git-lfs.github.com/spec/v1 |
||||||
|
oid sha256:ab8949d70455a9b3ef5d6414b788cb1cddec95fc238df8277dfd41c405a4060c |
||||||
|
size 23581 |
@ -0,0 +1,3 @@ |
|||||||
|
version https://git-lfs.github.com/spec/v1 |
||||||
|
oid sha256:96414480d9e361319abf7543324927300f841633f628da68901d6291b8cfd675 |
||||||
|
size 22667 |
@ -1,3 +1,3 @@ |
|||||||
version https://git-lfs.github.com/spec/v1 |
version https://git-lfs.github.com/spec/v1 |
||||||
oid sha256:1744d73991a0987c53d214041a26eaf7a3ce8a2ff4c90739f65f8641c4af921d |
oid sha256:4a6807e02706f29b5b748cfbbb13f9e1437e38438da1c81fa568709e5f88d7a4 |
||||||
size 22351 |
size 22041 |
||||||
|
@ -1,3 +1,3 @@ |
|||||||
version https://git-lfs.github.com/spec/v1 |
version https://git-lfs.github.com/spec/v1 |
||||||
oid sha256:4598d748f84146e66b547a5d9310c7e67477e1eb66d1c58bfb23f28807b7f54d |
oid sha256:b9b5cf8ae7992a8ef83056daa2fb8ebff0c60310ec675efa0e45faa58c0e58ad |
||||||
size 20263 |
size 20049 |
||||||
|
@ -1,3 +1,3 @@ |
|||||||
version https://git-lfs.github.com/spec/v1 |
version https://git-lfs.github.com/spec/v1 |
||||||
oid sha256:3d448afa778c03771454d5493f15748dce3eeb7a7b35cd33cb38bb5a06646f5d |
oid sha256:e256df9ae5a854a906a21868ecdf21a5d2ce37577a69a1da557949562d1da45d |
||||||
size 22550 |
size 22193 |
||||||
|
@ -1,3 +1,3 @@ |
|||||||
version https://git-lfs.github.com/spec/v1 |
version https://git-lfs.github.com/spec/v1 |
||||||
oid sha256:e000e964df93279e8325219a8a9ae74d1f66c533d418fabb88a208bc1165dbed |
oid sha256:aefcccd96088200097cd12a934b27df29819044ab315f3e0a3e1df9228336669 |
||||||
size 38724 |
size 36801 |
||||||
|
@ -1,3 +1,3 @@ |
|||||||
version https://git-lfs.github.com/spec/v1 |
version https://git-lfs.github.com/spec/v1 |
||||||
oid sha256:bf1d5e3367a9684cd4fd8ffebd4c38475fad0a6a6fa122df19281c1a3bf7349f |
oid sha256:561230a37864cced2286168b6bfe7c9e461177c056386dc08e538643d58b15a6 |
||||||
size 30814 |
size 28162 |
||||||
|
@ -1,3 +1,3 @@ |
|||||||
version https://git-lfs.github.com/spec/v1 |
version https://git-lfs.github.com/spec/v1 |
||||||
oid sha256:15c3812a12a76e5701bc1f487703ee32d6920f3c2d790f5b1d1a4b468da28000 |
oid sha256:125d38504d4500506fcb9d85eb9ce057620e683674efb2e6b33356764a14a1af |
||||||
size 23047 |
size 22677 |
||||||
|
@ -1,3 +1,3 @@ |
|||||||
version https://git-lfs.github.com/spec/v1 |
version https://git-lfs.github.com/spec/v1 |
||||||
oid sha256:a3fefee021f7c3bc6a8b950a129fd7549b05416ff72b32b2b24a4317555f43d8 |
oid sha256:5600ca86ffac9b33c320432dfb75b6242d7726ef39de0bc0e227b166565fa91f |
||||||
size 22916 |
size 20622 |
||||||
|
@ -0,0 +1,3 @@ |
|||||||
|
version https://git-lfs.github.com/spec/v1 |
||||||
|
oid sha256:8cc9c9f6cdf542c4817dfcf54f43ebd3e8c7fc47d36a50e60be28642dfbdf8ae |
||||||
|
size 22868 |
Loading…
Reference in new issue