Merge pull request #2433 from element-hq/feature/bma/testLogoutDialog
Test direct logout dialog and RoomDetailsView
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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.logout.api.direct
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
|
||||
open class DirectLogoutStateProvider : PreviewParameterProvider<DirectLogoutState> {
|
||||
override val values: Sequence<DirectLogoutState>
|
||||
get() = sequenceOf(
|
||||
aDirectLogoutState(),
|
||||
aDirectLogoutState(logoutAction = AsyncAction.Confirming),
|
||||
aDirectLogoutState(logoutAction = AsyncAction.Loading),
|
||||
aDirectLogoutState(logoutAction = AsyncAction.Failure(Exception("Error"))),
|
||||
aDirectLogoutState(logoutAction = AsyncAction.Success("success")),
|
||||
)
|
||||
}
|
||||
|
||||
fun aDirectLogoutState(
|
||||
canDoDirectSignOut: Boolean = true,
|
||||
logoutAction: AsyncAction<String?> = AsyncAction.Uninitialized,
|
||||
eventSink: (DirectLogoutEvents) -> Unit = {},
|
||||
) = DirectLogoutState(
|
||||
canDoDirectSignOut = canDoDirectSignOut,
|
||||
logoutAction = logoutAction,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
@@ -17,11 +17,15 @@
|
||||
package io.element.android.features.logout.impl.direct
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.logout.api.direct.DirectLogoutEvents
|
||||
import io.element.android.features.logout.api.direct.DirectLogoutState
|
||||
import io.element.android.features.logout.api.direct.DirectLogoutStateProvider
|
||||
import io.element.android.features.logout.api.direct.DirectLogoutView
|
||||
import io.element.android.features.logout.impl.ui.LogoutActionDialog
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -50,3 +54,14 @@ class DefaultDirectLogoutView @Inject constructor() : DirectLogoutView {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun DefaultDirectLogoutViewPreview(
|
||||
@PreviewParameter(DirectLogoutStateProvider::class) state: DirectLogoutState,
|
||||
) = ElementPreview {
|
||||
DefaultDirectLogoutView().Render(
|
||||
state = state,
|
||||
onSuccessLogout = {},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package io.element.android.features.logout.impl
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
@@ -32,6 +33,7 @@ import io.element.android.tests.testutils.pressBack
|
||||
import io.element.android.tests.testutils.pressTag
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@@ -41,16 +43,11 @@ class LogoutViewTest {
|
||||
@Test
|
||||
fun `clicking on logout sends a LogoutEvents`() {
|
||||
val eventsRecorder = EventsRecorder<LogoutEvents>()
|
||||
rule.setContent {
|
||||
LogoutView(
|
||||
aLogoutState(
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
onChangeRecoveryKeyClicked = EnsureNeverCalled(),
|
||||
onBackClicked = EnsureNeverCalled(),
|
||||
onSuccessLogout = EnsureNeverCalledWithParam(),
|
||||
)
|
||||
}
|
||||
rule.setLogoutView(
|
||||
aLogoutState(
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_signout)
|
||||
eventsRecorder.assertSingle(LogoutEvents.Logout(false))
|
||||
}
|
||||
@@ -58,17 +55,12 @@ class LogoutViewTest {
|
||||
@Test
|
||||
fun `confirming logout sends a LogoutEvents`() {
|
||||
val eventsRecorder = EventsRecorder<LogoutEvents>()
|
||||
rule.setContent {
|
||||
LogoutView(
|
||||
aLogoutState(
|
||||
logoutAction = AsyncAction.Confirming,
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
onChangeRecoveryKeyClicked = EnsureNeverCalled(),
|
||||
onBackClicked = EnsureNeverCalled(),
|
||||
onSuccessLogout = EnsureNeverCalledWithParam(),
|
||||
)
|
||||
}
|
||||
rule.setLogoutView(
|
||||
aLogoutState(
|
||||
logoutAction = AsyncAction.Confirming,
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
)
|
||||
rule.pressTag(TestTags.dialogPositive.value)
|
||||
eventsRecorder.assertSingle(LogoutEvents.Logout(false))
|
||||
}
|
||||
@@ -77,16 +69,12 @@ class LogoutViewTest {
|
||||
fun `clicking on back invoke back callback`() {
|
||||
val eventsRecorder = EventsRecorder<LogoutEvents>(expectEvents = false)
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setContent {
|
||||
LogoutView(
|
||||
aLogoutState(
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
onChangeRecoveryKeyClicked = EnsureNeverCalled(),
|
||||
onBackClicked = callback,
|
||||
onSuccessLogout = EnsureNeverCalledWithParam(),
|
||||
)
|
||||
}
|
||||
rule.setLogoutView(
|
||||
aLogoutState(
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
onBackClicked = callback,
|
||||
)
|
||||
rule.pressBack()
|
||||
}
|
||||
}
|
||||
@@ -94,17 +82,12 @@ class LogoutViewTest {
|
||||
@Test
|
||||
fun `clicking on confirm after error sends a LogoutEvents`() {
|
||||
val eventsRecorder = EventsRecorder<LogoutEvents>()
|
||||
rule.setContent {
|
||||
LogoutView(
|
||||
aLogoutState(
|
||||
logoutAction = AsyncAction.Failure(Exception("Failed to logout")),
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
onChangeRecoveryKeyClicked = EnsureNeverCalled(),
|
||||
onBackClicked = EnsureNeverCalled(),
|
||||
onSuccessLogout = EnsureNeverCalledWithParam(),
|
||||
)
|
||||
}
|
||||
rule.setLogoutView(
|
||||
aLogoutState(
|
||||
logoutAction = AsyncAction.Failure(Exception("Failed to logout")),
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_signout_anyway)
|
||||
eventsRecorder.assertSingle(LogoutEvents.Logout(true))
|
||||
}
|
||||
@@ -112,17 +95,12 @@ class LogoutViewTest {
|
||||
@Test
|
||||
fun `clicking on cancel after error sends a LogoutEvents`() {
|
||||
val eventsRecorder = EventsRecorder<LogoutEvents>()
|
||||
rule.setContent {
|
||||
LogoutView(
|
||||
aLogoutState(
|
||||
logoutAction = AsyncAction.Failure(Exception("Failed to logout")),
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
onChangeRecoveryKeyClicked = EnsureNeverCalled(),
|
||||
onBackClicked = EnsureNeverCalled(),
|
||||
onSuccessLogout = EnsureNeverCalledWithParam(),
|
||||
)
|
||||
}
|
||||
rule.setLogoutView(
|
||||
aLogoutState(
|
||||
logoutAction = AsyncAction.Failure(Exception("Failed to logout")),
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_cancel)
|
||||
eventsRecorder.assertSingle(LogoutEvents.CloseDialogs)
|
||||
}
|
||||
@@ -132,17 +110,13 @@ class LogoutViewTest {
|
||||
val data = "data"
|
||||
val eventsRecorder = EventsRecorder<LogoutEvents>(expectEvents = false)
|
||||
ensureCalledOnceWithParam<String?>(data) { callback ->
|
||||
rule.setContent {
|
||||
LogoutView(
|
||||
aLogoutState(
|
||||
logoutAction = AsyncAction.Success(data),
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
onChangeRecoveryKeyClicked = EnsureNeverCalled(),
|
||||
onBackClicked = EnsureNeverCalled(),
|
||||
onSuccessLogout = callback,
|
||||
)
|
||||
}
|
||||
rule.setLogoutView(
|
||||
aLogoutState(
|
||||
logoutAction = AsyncAction.Success(data),
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
onSuccessLogout = callback,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,18 +124,30 @@ class LogoutViewTest {
|
||||
fun `last session setting button invoke onChangeRecoveryKeyClicked`() {
|
||||
val eventsRecorder = EventsRecorder<LogoutEvents>(expectEvents = false)
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setContent {
|
||||
LogoutView(
|
||||
aLogoutState(
|
||||
isLastDevice = true,
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
onChangeRecoveryKeyClicked = callback,
|
||||
onBackClicked = EnsureNeverCalled(),
|
||||
onSuccessLogout = EnsureNeverCalledWithParam(),
|
||||
)
|
||||
}
|
||||
rule.setLogoutView(
|
||||
aLogoutState(
|
||||
isLastDevice = true,
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
onChangeRecoveryKeyClicked = callback,
|
||||
)
|
||||
rule.clickOn(CommonStrings.common_settings)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setLogoutView(
|
||||
state: LogoutState,
|
||||
onChangeRecoveryKeyClicked: () -> Unit = EnsureNeverCalled(),
|
||||
onBackClicked: () -> Unit = EnsureNeverCalled(),
|
||||
onSuccessLogout: (logoutUrlResult: String?) -> Unit = EnsureNeverCalledWithParam()
|
||||
) {
|
||||
setContent {
|
||||
LogoutView(
|
||||
state = state,
|
||||
onChangeRecoveryKeyClicked = onChangeRecoveryKeyClicked,
|
||||
onBackClicked = onBackClicked,
|
||||
onSuccessLogout = onSuccessLogout,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* 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.logout.impl.direct
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import io.element.android.features.logout.api.direct.DirectLogoutEvents
|
||||
import io.element.android.features.logout.api.direct.DirectLogoutState
|
||||
import io.element.android.features.logout.api.direct.aDirectLogoutState
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
|
||||
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.tests.testutils.pressBackKey
|
||||
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 DefaultDirectLogoutViewTest {
|
||||
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun `clicking on confirm logout sends expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<DirectLogoutEvents>()
|
||||
rule.setDefaultDirectLogoutView(
|
||||
state = aDirectLogoutState(
|
||||
logoutAction = AsyncAction.Confirming,
|
||||
eventSink = eventsRecorder,
|
||||
)
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_signout)
|
||||
eventsRecorder.assertSingle(DirectLogoutEvents.Logout(false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on cancel logout sends expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<DirectLogoutEvents>()
|
||||
rule.setDefaultDirectLogoutView(
|
||||
state = aDirectLogoutState(
|
||||
logoutAction = AsyncAction.Confirming,
|
||||
eventSink = eventsRecorder,
|
||||
)
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_cancel)
|
||||
eventsRecorder.assertSingle(DirectLogoutEvents.CloseDialogs)
|
||||
}
|
||||
|
||||
@Ignore("Pressing back key should dismiss the dialog, and so generate the expected event, but it's not the case.")
|
||||
@Test
|
||||
fun `clicking on back invoke back callback`() {
|
||||
val eventsRecorder = EventsRecorder<DirectLogoutEvents>()
|
||||
rule.setDefaultDirectLogoutView(
|
||||
state = aDirectLogoutState(
|
||||
logoutAction = AsyncAction.Confirming,
|
||||
eventSink = eventsRecorder,
|
||||
)
|
||||
)
|
||||
rule.pressBackKey()
|
||||
eventsRecorder.assertSingle(DirectLogoutEvents.CloseDialogs)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on confirm after error sends expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<DirectLogoutEvents>()
|
||||
rule.setDefaultDirectLogoutView(
|
||||
state = aDirectLogoutState(
|
||||
logoutAction = AsyncAction.Failure(Exception("Error")),
|
||||
eventSink = eventsRecorder,
|
||||
)
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_signout_anyway)
|
||||
eventsRecorder.assertSingle(DirectLogoutEvents.Logout(true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on cancel after error sends expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<DirectLogoutEvents>()
|
||||
rule.setDefaultDirectLogoutView(
|
||||
state = aDirectLogoutState(
|
||||
logoutAction = AsyncAction.Failure(Exception("Error")),
|
||||
eventSink = eventsRecorder,
|
||||
)
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_cancel)
|
||||
eventsRecorder.assertSingle(DirectLogoutEvents.CloseDialogs)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `success logout invoke expected callback and sends expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<DirectLogoutEvents>(expectEvents = false)
|
||||
ensureCalledOnceWithParam<String?>(null) { callback ->
|
||||
rule.setDefaultDirectLogoutView(
|
||||
state = aDirectLogoutState(
|
||||
logoutAction = AsyncAction.Success(null),
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
onSuccessLogout = callback
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `success logout invoke expected callback and sends expected Event with data`() {
|
||||
val eventsRecorder = EventsRecorder<DirectLogoutEvents>(expectEvents = false)
|
||||
val data = "data"
|
||||
ensureCalledOnceWithParam<String?>(data) { callback ->
|
||||
rule.setDefaultDirectLogoutView(
|
||||
state = aDirectLogoutState(
|
||||
logoutAction = AsyncAction.Success(data),
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
onSuccessLogout = callback
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setDefaultDirectLogoutView(
|
||||
state: DirectLogoutState,
|
||||
onSuccessLogout: (String?) -> Unit = EnsureNeverCalledWithParam(),
|
||||
) {
|
||||
setContent {
|
||||
DefaultDirectLogoutView().Render(
|
||||
state,
|
||||
onSuccessLogout = onSuccessLogout,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -16,8 +16,7 @@
|
||||
|
||||
package io.element.android.features.preferences.impl.root
|
||||
|
||||
import io.element.android.features.logout.api.direct.DirectLogoutState
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.features.logout.api.direct.aDirectLogoutState
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@@ -37,9 +36,3 @@ fun aPreferencesRootState() = PreferencesRootState(
|
||||
snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete),
|
||||
directLogoutState = aDirectLogoutState(),
|
||||
)
|
||||
|
||||
fun aDirectLogoutState() = DirectLogoutState(
|
||||
canDoDirectSignOut = true,
|
||||
logoutAction = AsyncAction.Uninitialized,
|
||||
eventSink = {},
|
||||
)
|
||||
|
||||
@@ -51,6 +51,7 @@ dependencies {
|
||||
implementation(projects.libraries.featureflag.api)
|
||||
implementation(projects.libraries.permissions.api)
|
||||
implementation(projects.libraries.preferences.api)
|
||||
implementation(projects.libraries.testtags)
|
||||
api(projects.features.roomdetails.api)
|
||||
api(projects.libraries.usersearch.api)
|
||||
api(projects.services.apperror.api)
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
package io.element.android.features.roomdetails.impl
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||
import io.element.android.features.leaveroom.api.aLeaveRoomState
|
||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState
|
||||
import io.element.android.features.roomdetails.impl.members.details.aRoomMemberDetailsState
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
@@ -29,18 +31,18 @@ open class RoomDetailsStateProvider : PreviewParameterProvider<RoomDetailsState>
|
||||
override val values: Sequence<RoomDetailsState>
|
||||
get() = sequenceOf(
|
||||
aRoomDetailsState(),
|
||||
aRoomDetailsState().copy(roomTopic = RoomTopicState.Hidden),
|
||||
aRoomDetailsState().copy(roomTopic = RoomTopicState.CanAddTopic),
|
||||
aRoomDetailsState().copy(isEncrypted = false),
|
||||
aRoomDetailsState().copy(roomAlias = null),
|
||||
aDmRoomDetailsState().copy(roomName = "Daniel"),
|
||||
aDmRoomDetailsState(isDmMemberIgnored = true).copy(roomName = "Daniel"),
|
||||
aRoomDetailsState().copy(canInvite = true),
|
||||
aRoomDetailsState().copy(isFavorite = true),
|
||||
aRoomDetailsState().copy(
|
||||
aRoomDetailsState(roomTopic = RoomTopicState.Hidden),
|
||||
aRoomDetailsState(roomTopic = RoomTopicState.CanAddTopic),
|
||||
aRoomDetailsState(isEncrypted = false),
|
||||
aRoomDetailsState(roomAlias = null),
|
||||
aDmRoomDetailsState(),
|
||||
aDmRoomDetailsState(isDmMemberIgnored = true),
|
||||
aRoomDetailsState(canInvite = true),
|
||||
aRoomDetailsState(isFavorite = true),
|
||||
aRoomDetailsState(
|
||||
canEdit = true,
|
||||
// Also test the roomNotificationSettings ALL_MESSAGES in the same screenshot. Icon 'Mute' should be displayed
|
||||
roomNotificationSettings = RoomNotificationSettings(mode = RoomNotificationMode.ALL_MESSAGES, isDefault = true)
|
||||
roomNotificationSettings = aRoomNotificationSettings(mode = RoomNotificationMode.ALL_MESSAGES, isDefault = true)
|
||||
),
|
||||
// Add other state here
|
||||
)
|
||||
@@ -68,32 +70,61 @@ fun aDmRoomMember(
|
||||
role = role,
|
||||
)
|
||||
|
||||
fun aRoomDetailsState() = RoomDetailsState(
|
||||
roomId = "a room id",
|
||||
roomName = "Marketing",
|
||||
roomAlias = "#marketing:domain.com",
|
||||
roomAvatarUrl = null,
|
||||
roomTopic = RoomTopicState.ExistingTopic(
|
||||
fun aRoomDetailsState(
|
||||
roomId: String = "a room id",
|
||||
roomName: String = "Marketing",
|
||||
roomAlias: String? = "#marketing:domain.com",
|
||||
roomAvatarUrl: String? = null,
|
||||
roomTopic: RoomTopicState = RoomTopicState.ExistingTopic(
|
||||
"Welcome to #marketing, home of the Marketing team " +
|
||||
"|| WIKI PAGE: https://domain.org/wiki/Marketing " +
|
||||
"|| MAIL iki/Marketing " +
|
||||
"|| MAI iki/Marketing " +
|
||||
"|| MAI iki/Marketing..."
|
||||
),
|
||||
memberCount = 32,
|
||||
isEncrypted = true,
|
||||
canInvite = false,
|
||||
canEdit = false,
|
||||
canShowNotificationSettings = true,
|
||||
roomType = RoomDetailsType.Room,
|
||||
roomMemberDetailsState = null,
|
||||
leaveRoomState = aLeaveRoomState(),
|
||||
roomNotificationSettings = RoomNotificationSettings(mode = RoomNotificationMode.MUTE, isDefault = false),
|
||||
isFavorite = false,
|
||||
eventSink = {}
|
||||
memberCount: Long = 32,
|
||||
isEncrypted: Boolean = true,
|
||||
canInvite: Boolean = false,
|
||||
canEdit: Boolean = false,
|
||||
canShowNotificationSettings: Boolean = true,
|
||||
roomType: RoomDetailsType = RoomDetailsType.Room,
|
||||
roomMemberDetailsState: RoomMemberDetailsState? = null,
|
||||
leaveRoomState: LeaveRoomState = aLeaveRoomState(),
|
||||
roomNotificationSettings: RoomNotificationSettings = aRoomNotificationSettings(),
|
||||
isFavorite: Boolean = false,
|
||||
eventSink: (RoomDetailsEvent) -> Unit = {},
|
||||
) = RoomDetailsState(
|
||||
roomId = roomId,
|
||||
roomName = roomName,
|
||||
roomAlias = roomAlias,
|
||||
roomAvatarUrl = roomAvatarUrl,
|
||||
roomTopic = roomTopic,
|
||||
memberCount = memberCount,
|
||||
isEncrypted = isEncrypted,
|
||||
canInvite = canInvite,
|
||||
canEdit = canEdit,
|
||||
canShowNotificationSettings = canShowNotificationSettings,
|
||||
roomType = roomType,
|
||||
roomMemberDetailsState = roomMemberDetailsState,
|
||||
leaveRoomState = leaveRoomState,
|
||||
roomNotificationSettings = roomNotificationSettings,
|
||||
isFavorite = isFavorite,
|
||||
eventSink = eventSink
|
||||
)
|
||||
|
||||
fun aDmRoomDetailsState(isDmMemberIgnored: Boolean = false) = aRoomDetailsState().copy(
|
||||
fun aRoomNotificationSettings(
|
||||
mode: RoomNotificationMode = RoomNotificationMode.MUTE,
|
||||
isDefault: Boolean = false,
|
||||
) = RoomNotificationSettings(
|
||||
mode = mode,
|
||||
isDefault = isDefault
|
||||
)
|
||||
|
||||
fun aDmRoomDetailsState(
|
||||
isDmMemberIgnored: Boolean = false,
|
||||
roomName: String = "Daniel",
|
||||
) = aRoomDetailsState(
|
||||
roomName = roomName,
|
||||
roomType = RoomDetailsType.Dm(aDmRoomMember(isIgnored = isDmMemberIgnored)),
|
||||
roomMemberDetailsState = aRoomMemberDetailsState()
|
||||
)
|
||||
|
||||
@@ -53,6 +53,7 @@ import io.element.android.features.roomdetails.impl.blockuser.BlockUserDialogs
|
||||
import io.element.android.features.roomdetails.impl.blockuser.BlockUserSection
|
||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberHeaderSection
|
||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberMainActionsSection
|
||||
import io.element.android.libraries.architecture.coverage.ExcludeFromCoverage
|
||||
import io.element.android.libraries.designsystem.components.ClickableLinkText
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
@@ -80,6 +81,8 @@ import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||
import io.element.android.libraries.matrix.api.room.getBestName
|
||||
import io.element.android.libraries.testtags.TestTags
|
||||
import io.element.android.libraries.testtags.testTag
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
@@ -221,7 +224,7 @@ private fun RoomDetailsTopBar(
|
||||
actions = {
|
||||
if (showEdit) {
|
||||
IconButton(onClick = { showMenu = !showMenu }) {
|
||||
Icon(Icons.Default.MoreVert, "")
|
||||
Icon(Icons.Default.MoreVert, stringResource(id = CommonStrings.a11y_user_menu))
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = showMenu,
|
||||
@@ -299,6 +302,7 @@ private fun RoomHeaderSection(
|
||||
modifier = Modifier
|
||||
.size(70.dp)
|
||||
.clickable(enabled = avatarUrl != null) { openAvatarPreview(avatarUrl!!) }
|
||||
.testTag(TestTags.roomDetailAvatar)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Text(
|
||||
@@ -463,6 +467,7 @@ internal fun RoomDetailsPreview(@PreviewParameter(RoomDetailsStateProvider::clas
|
||||
internal fun RoomDetailsDarkPreview(@PreviewParameter(RoomDetailsStateProvider::class) state: RoomDetailsState) =
|
||||
ElementPreviewDark { ContentToPreview(state) }
|
||||
|
||||
@ExcludeFromCoverage
|
||||
@Composable
|
||||
private fun ContentToPreview(state: RoomDetailsState) {
|
||||
RoomDetailsView(
|
||||
|
||||
@@ -37,6 +37,8 @@ import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.testtags.TestTags
|
||||
import io.element.android.libraries.testtags.testTag
|
||||
|
||||
@Composable
|
||||
fun RoomMemberHeaderSection(
|
||||
@@ -53,6 +55,7 @@ fun RoomMemberHeaderSection(
|
||||
modifier = Modifier
|
||||
.clickable(enabled = avatarUrl != null) { openAvatarPreview(avatarUrl!!) }
|
||||
.fillMaxSize()
|
||||
.testTag(TestTags.memberDetailAvatar)
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
@@ -17,10 +17,10 @@
|
||||
package io.element.android.features.roomdetails.impl.notificationsettings
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.roomdetails.impl.aRoomNotificationSettings
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||
import io.element.android.libraries.matrix.api.room.RoomNotificationSettings
|
||||
|
||||
internal class RoomNotificationSettingsStateProvider : PreviewParameterProvider<RoomNotificationSettingsState> {
|
||||
override val values: Sequence<RoomNotificationSettingsState>
|
||||
@@ -43,7 +43,7 @@ internal class RoomNotificationSettingsStateProvider : PreviewParameterProvider<
|
||||
return RoomNotificationSettingsState(
|
||||
showUserDefinedSettingStyle = false,
|
||||
roomName = "Room 1",
|
||||
AsyncData.Success(RoomNotificationSettings(
|
||||
AsyncData.Success(aRoomNotificationSettings(
|
||||
mode = RoomNotificationMode.MUTE,
|
||||
isDefault = isDefault
|
||||
)),
|
||||
|
||||
@@ -17,10 +17,10 @@
|
||||
package io.element.android.features.roomdetails.impl.notificationsettings
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.roomdetails.impl.aRoomNotificationSettings
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||
import io.element.android.libraries.matrix.api.room.RoomNotificationSettings
|
||||
|
||||
internal class UserDefinedRoomNotificationSettingsStateProvider : PreviewParameterProvider<RoomNotificationSettingsState> {
|
||||
override val values: Sequence<RoomNotificationSettingsState>
|
||||
@@ -29,7 +29,7 @@ internal class UserDefinedRoomNotificationSettingsStateProvider : PreviewParamet
|
||||
showUserDefinedSettingStyle = false,
|
||||
roomName = "Room 1",
|
||||
AsyncData.Success(
|
||||
RoomNotificationSettings(
|
||||
aRoomNotificationSettings(
|
||||
mode = RoomNotificationMode.MUTE,
|
||||
isDefault = false
|
||||
)
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
/*
|
||||
* 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.roomdetails.impl
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||
import io.element.android.libraries.testtags.TestTags
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.tests.testutils.EnsureCalledOnceWithTwoParams
|
||||
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.pressBack
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class RoomDetailsViewTest {
|
||||
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun `click on back invokes expected callback`() {
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setRoomDetailView(
|
||||
goBack = callback,
|
||||
)
|
||||
rule.pressBack()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `click on share invokes expected callback`() {
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setRoomDetailView(
|
||||
onShareRoom = callback,
|
||||
)
|
||||
rule.clickOn(R.string.screen_room_details_share_room_title)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `click on share member invokes expected callback`() {
|
||||
val state = aDmRoomDetailsState()
|
||||
val roomMember = (state.roomType as RoomDetailsType.Dm).roomMember
|
||||
ensureCalledOnceWithParam(roomMember) { callback ->
|
||||
rule.setRoomDetailView(
|
||||
state = aDmRoomDetailsState(),
|
||||
onShareMember = callback,
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_share)
|
||||
}
|
||||
}
|
||||
|
||||
@Config(qualifiers = "h1024dp")
|
||||
@Test
|
||||
fun `click on room members invokes expected callback`() {
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setRoomDetailView(
|
||||
openRoomMemberList = callback,
|
||||
)
|
||||
rule.clickOn(CommonStrings.common_people)
|
||||
}
|
||||
}
|
||||
|
||||
@Config(qualifiers = "h1024dp")
|
||||
@Test
|
||||
fun `click on polls invokes expected callback`() {
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setRoomDetailView(
|
||||
openPollHistory = callback,
|
||||
)
|
||||
rule.clickOn(R.string.screen_polls_history_title)
|
||||
}
|
||||
}
|
||||
|
||||
@Config(qualifiers = "h1024dp")
|
||||
@Test
|
||||
fun `click on notification invokes expected callback`() {
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setRoomDetailView(
|
||||
openRoomNotificationSettings = callback,
|
||||
)
|
||||
rule.clickOn(R.string.screen_room_details_notification_title)
|
||||
}
|
||||
}
|
||||
|
||||
@Config(qualifiers = "h1024dp")
|
||||
@Test
|
||||
fun `click on invite people invokes expected callback`() {
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setRoomDetailView(
|
||||
state = aRoomDetailsState(
|
||||
eventSink = EventsRecorder(expectEvents = false),
|
||||
canInvite = true,
|
||||
),
|
||||
invitePeople = callback,
|
||||
)
|
||||
rule.clickOn(R.string.screen_room_details_invite_people_title)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `click on add topic emit expected event`() {
|
||||
ensureCalledOnceWithParam<RoomDetailsAction>(RoomDetailsAction.AddTopic) { callback ->
|
||||
rule.setRoomDetailView(
|
||||
state = aRoomDetailsState(
|
||||
eventSink = EventsRecorder(expectEvents = false),
|
||||
roomTopic = RoomTopicState.CanAddTopic,
|
||||
),
|
||||
onActionClicked = callback,
|
||||
)
|
||||
rule.clickOn(R.string.screen_room_details_add_topic_title)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `click on menu edit emit expected event`() {
|
||||
ensureCalledOnceWithParam<RoomDetailsAction>(RoomDetailsAction.Edit) { callback ->
|
||||
rule.setRoomDetailView(
|
||||
state = aRoomDetailsState(
|
||||
eventSink = EventsRecorder(expectEvents = false),
|
||||
canEdit = true,
|
||||
),
|
||||
onActionClicked = callback,
|
||||
)
|
||||
val menuContentDescription = rule.activity.getString(CommonStrings.a11y_user_menu)
|
||||
rule.onNodeWithContentDescription(menuContentDescription).performClick()
|
||||
rule.clickOn(CommonStrings.action_edit)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `click on avatar test`() {
|
||||
val eventsRecorder = EventsRecorder<RoomDetailsEvent>(expectEvents = false)
|
||||
val state = aRoomDetailsState(
|
||||
eventSink = eventsRecorder,
|
||||
roomAvatarUrl = "an_avatar_url",
|
||||
)
|
||||
val callback = EnsureCalledOnceWithTwoParams(state.roomName, "an_avatar_url")
|
||||
rule.setRoomDetailView(
|
||||
state = state,
|
||||
openAvatarPreview = callback,
|
||||
)
|
||||
rule.onNodeWithTag(TestTags.roomDetailAvatar.value).performClick()
|
||||
callback.assertSuccess()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `click on avatar test on DM`() {
|
||||
val eventsRecorder = EventsRecorder<RoomDetailsEvent>(expectEvents = false)
|
||||
val state = aRoomDetailsState(
|
||||
roomType = RoomDetailsType.Dm(aDmRoomMember(avatarUrl = "an_avatar_url")),
|
||||
eventSink = eventsRecorder,
|
||||
)
|
||||
val callback = EnsureCalledOnceWithTwoParams("Daniel", "an_avatar_url")
|
||||
rule.setRoomDetailView(
|
||||
state = state,
|
||||
openAvatarPreview = callback,
|
||||
)
|
||||
rule.onNodeWithTag(TestTags.memberDetailAvatar.value).performClick()
|
||||
callback.assertSuccess()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `click on mute emit expected event`() {
|
||||
val eventsRecorder = EventsRecorder<RoomDetailsEvent>()
|
||||
val state = aRoomDetailsState(
|
||||
eventSink = eventsRecorder,
|
||||
roomNotificationSettings = aRoomNotificationSettings(mode = RoomNotificationMode.ALL_MESSAGES),
|
||||
)
|
||||
rule.setRoomDetailView(
|
||||
state = state,
|
||||
)
|
||||
rule.clickOn(CommonStrings.common_mute)
|
||||
eventsRecorder.assertSingle(RoomDetailsEvent.MuteNotification)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `click on unmute emit expected event`() {
|
||||
val eventsRecorder = EventsRecorder<RoomDetailsEvent>()
|
||||
val state = aRoomDetailsState(
|
||||
eventSink = eventsRecorder,
|
||||
roomNotificationSettings = aRoomNotificationSettings(mode = RoomNotificationMode.MUTE),
|
||||
)
|
||||
rule.setRoomDetailView(
|
||||
state = state,
|
||||
)
|
||||
rule.clickOn(CommonStrings.common_unmute)
|
||||
eventsRecorder.assertSingle(RoomDetailsEvent.UnmuteNotification)
|
||||
}
|
||||
|
||||
@Config(qualifiers = "h1024dp")
|
||||
@Test
|
||||
fun `click on favorite emit expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<RoomDetailsEvent>()
|
||||
rule.setRoomDetailView(
|
||||
state = aRoomDetailsState(
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.clickOn(CommonStrings.common_favourite)
|
||||
eventsRecorder.assertSingle(RoomDetailsEvent.SetFavorite(true))
|
||||
}
|
||||
|
||||
@Config(qualifiers = "h1024dp")
|
||||
@Test
|
||||
fun `click on leave emit expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<RoomDetailsEvent>()
|
||||
rule.setRoomDetailView(
|
||||
state = aRoomDetailsState(
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
)
|
||||
rule.clickOn(R.string.screen_room_details_leave_room_title)
|
||||
eventsRecorder.assertSingle(RoomDetailsEvent.LeaveRoom)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomDetailView(
|
||||
state: RoomDetailsState = aRoomDetailsState(
|
||||
eventSink = EventsRecorder(expectEvents = false),
|
||||
),
|
||||
goBack: () -> Unit = EnsureNeverCalled(),
|
||||
onActionClicked: (RoomDetailsAction) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onShareRoom: () -> Unit = EnsureNeverCalled(),
|
||||
onShareMember: (RoomMember) -> Unit = EnsureNeverCalledWithParam(),
|
||||
openRoomMemberList: () -> Unit = EnsureNeverCalled(),
|
||||
openRoomNotificationSettings: () -> Unit = EnsureNeverCalled(),
|
||||
invitePeople: () -> Unit = EnsureNeverCalled(),
|
||||
openAvatarPreview: (name: String, url: String) -> Unit = EnsureNeverCalledWithTwoParams(),
|
||||
openPollHistory: () -> Unit = EnsureNeverCalled(),
|
||||
) {
|
||||
setContent {
|
||||
RoomDetailsView(
|
||||
state = state,
|
||||
goBack = goBack,
|
||||
onActionClicked = onActionClicked,
|
||||
onShareRoom = onShareRoom,
|
||||
onShareMember = onShareMember,
|
||||
openRoomMemberList = openRoomMemberList,
|
||||
openRoomNotificationSettings = openRoomNotificationSettings,
|
||||
invitePeople = invitePeople,
|
||||
openAvatarPreview = openAvatarPreview,
|
||||
openPollHistory = openPollHistory,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,16 @@ object TestTags {
|
||||
*/
|
||||
val homeScreenSettings = TestTag("home_screen-settings")
|
||||
|
||||
/**
|
||||
* Room detail screen.
|
||||
*/
|
||||
val roomDetailAvatar = TestTag("room_detail-avatar")
|
||||
|
||||
/**
|
||||
* Room member screen.
|
||||
*/
|
||||
val memberDetailAvatar = TestTag("member_detail-avatar")
|
||||
|
||||
/**
|
||||
* Welcome screen.
|
||||
*/
|
||||
|
||||
@@ -55,6 +55,25 @@ class EnsureCalledOnceWithParam<T, R>(
|
||||
}
|
||||
}
|
||||
|
||||
class EnsureCalledOnceWithTwoParams<T, U>(
|
||||
private val expectedParam1: T,
|
||||
private val expectedParam2: U,
|
||||
) : (T, U) -> Unit {
|
||||
private var counter = 0
|
||||
override fun invoke(p1: T, p2: U) {
|
||||
if (p1 != expectedParam1 || p2 != expectedParam2) {
|
||||
throw AssertionError("Expected to be called with $expectedParam1 and $expectedParam2, but was called with $p1 and $p2")
|
||||
}
|
||||
counter++
|
||||
}
|
||||
|
||||
fun assertSuccess() {
|
||||
if (counter != 1) {
|
||||
throw AssertionError("Expected to be called once, but was called $counter times")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortcut for [<T, R> ensureCalledOnceWithParam] with Unit result.
|
||||
*/
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user